db.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565
  1. /**
  2. * TraCINg-Server - Gathering and visualizing cyber incidents on the world
  3. *
  4. * Copyright 2013 Matthias Gazzari, Annemarie Mattmann, André Wolski
  5. *
  6. * Licensed under the Apache License, Version 2.0 (the "License");
  7. * you may not use this file except in compliance with the License.
  8. * You may obtain a copy of the License at
  9. *
  10. * http://www.apache.org/licenses/LICENSE-2.0
  11. *
  12. * Unless required by applicable law or agreed to in writing, software
  13. * distributed under the License is distributed on an "AS IS" BASIS,
  14. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  15. * See the License for the specific language governing permissions and
  16. * limitations under the License.
  17. */
  18. var orm = require("orm");
  19. var db_driver;
  20. /* start add custom filter */
  21. var Filter = require('validator').Filter;
  22. Filter.prototype.nl2br = function() {
  23. this.modify(this.str.replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, '$1<br>$2'));
  24. return this;
  25. }
  26. /* end add custom filter */
  27. var sanitize = require('validator').sanitize;
  28. var Models = require("./models.js");
  29. var config = require("../../config.json");
  30. var fields = require("../fields.js");
  31. var ready = false;
  32. var Incident;
  33. orm.connect(config.db, function (err, db) {
  34. if(err){
  35. console.log(err);
  36. return;
  37. }
  38. db_driver = db.driver;
  39. Models.initialize(db);
  40. Incident = Models.Incident;
  41. // synchronize orm-layer -> create or update tables for our models in the database
  42. db.sync(function (err) {
  43. if(err){
  44. console.log(err);
  45. return;
  46. }
  47. ready = true;
  48. });
  49. });
  50. function convertToDBModel(items){
  51. var arr = [];
  52. for(var i = 0; i < items.length; i++){
  53. var data = items[i];
  54. var obj = {
  55. date: data.date,
  56. sensortype: sanitize(data.sensortype).chain().trim().entityEncode().value(),
  57. sensorname: sanitize(data.sensorname).chain().trim().entityEncode().value(),
  58. source_port: data.src.port,
  59. source_ip: data.src.ip,
  60. destination_port: data.dst.port,
  61. destination_ip: data.dst.ip,
  62. type: data.type,
  63. log: sanitize(data.log).chain().trim().entityEncode().nl2br().value(),
  64. md5sum: sanitize(data.md5sum).chain().trim().entityEncode().value(),
  65. authorized: data.authorized,
  66. };
  67. if(data.src.country){
  68. obj["source_country"] = data.src.country;
  69. obj["source_cc"] = data.src.cc;
  70. obj["source_city"] = data.src.city;
  71. obj["source_latitude"] = data.src.ll[0];
  72. obj["source_longitude"] = data.src.ll[1];
  73. }
  74. if(data.dst.country){
  75. obj["destination_country"] = data.dst.country;
  76. obj["destination_cc"] = data.dst.cc;
  77. obj["destination_city"] = data.dst.city;
  78. obj["destination_latitude"] = data.dst.ll[0];
  79. obj["destination_longitude"] = data.dst.ll[1];
  80. };
  81. arr.push(obj);
  82. }
  83. return arr;
  84. }
  85. function convertFromDBModel(input){
  86. var arr = [];
  87. for(key in input){
  88. var data = input[key];
  89. arr[key] = {
  90. id: data.id,
  91. date: data.date, // is converted with new Date(date) when called in frontend
  92. sensortype: data.sensortype,
  93. sensorname: data.sensorname,
  94. src: {
  95. //ip: data.source_ip, // do not send ip to clients
  96. port: data.source_port,
  97. country: data.source_country,
  98. cc: data.source_cc,
  99. city: data.source_city,
  100. ll: [data.source_latitude, data.source_longitude],
  101. },
  102. dst: {
  103. //ip: data.destination_ip, // do not send ip to client
  104. port: data.destination_port,
  105. country: data.destination_country,
  106. cc: data.destination_cc,
  107. city: data.destination_city,
  108. ll: [data.destination_latitude, data.destination_longitude],
  109. },
  110. type: data.type,
  111. hasLog: (data.log && data.log != ''),
  112. md5sum: data.md5sum,
  113. authorized: data.authorized || false,
  114. };
  115. }
  116. return arr;
  117. }
  118. exports.getSensorTypes = function(callback){
  119. if(!ready){
  120. console.log("db is not ready!");
  121. // TODO: callback is undefined
  122. callback({errno: 0, code: "The database is not ready"});
  123. return;
  124. }
  125. db_driver.execQuery("SELECT sensortype FROM incident GROUP BY sensortype", function (err, data) {
  126. if(err){
  127. callback(err);
  128. return;
  129. }
  130. var sensors = [];
  131. for(var i = 0; i < data.length; i++){
  132. sensors.push(data[i]["sensortype"]);
  133. }
  134. callback(null, sensors);
  135. });
  136. };
  137. /*
  138. getConditions translates the given filter to database conditions.
  139. If the client could pass the filter directly to the database it would be possible for him to filter everything from the database, even fields he should not know of.
  140. Also we can create more complex filters, eg. use orm.between
  141. accepted filters:
  142. - start: unix timestamp
  143. - end: unix timestamp
  144. - countries: array of country codes
  145. */
  146. function getConditions(filter){
  147. var conditions = {};
  148. if(filter.hasOwnProperty("start") && filter.hasOwnProperty("end")){
  149. conditions["date"] = orm.between(new Date(filter["start"]), new Date(filter["end"]));
  150. }
  151. else if(filter.hasOwnProperty("start")){
  152. conditions["date"] = orm.gt(new Date(filter["start"]));
  153. }
  154. else if(filter.hasOwnProperty("end")){
  155. conditions["date"] = orm.lt(new Date(filter["end"]));
  156. }
  157. if(filter.hasOwnProperty("countries")){
  158. conditions["source_cc"] = filter["countries"];
  159. }
  160. if(filter.hasOwnProperty("authorized")){
  161. conditions["authorized"] = true;
  162. }
  163. if(filter.hasOwnProperty("types")){
  164. var types = [];
  165. for(var i = 0; i < filter["types"].length; i++) {
  166. types.push(parseInt(filter["types"][i], 10));
  167. }
  168. if(types.indexOf(0) != -1){
  169. var not_in = [];
  170. for(var i = 0; i < fields.validTypes.length; i++) {
  171. var validType = fields.validTypes[i];
  172. if(filter["types"].indexOf(validType) == -1)
  173. not_in.push(validType);
  174. }
  175. conditions["type"] = orm.not_in(not_in);
  176. }
  177. else{
  178. conditions["type"] = types;
  179. }
  180. }
  181. if(filter.hasOwnProperty("sensors")){
  182. conditions["sensortype"] = filter["sensors"];
  183. }
  184. return conditions;
  185. }
  186. function select(callback, filter, only, order){
  187. if(!ready){
  188. console.log("db is not ready!");
  189. callback({errno: 0, code: "The database is not ready"});
  190. return;
  191. }
  192. var conditions = filter && getConditions(filter) || {};
  193. only = only || [];
  194. var chain = Incident.find(conditions).only(only);
  195. if(order)
  196. chain = chain.order(order);
  197. chain.run(function(err, data){
  198. if(err){
  199. console.log(err);
  200. callback(err);
  201. return;
  202. }
  203. var converted = convertFromDBModel(data);
  204. callback(err, converted);
  205. });
  206. };
  207. /*
  208. aggregateOptions:
  209. - fields: fields to select (array)
  210. - order (optional): array
  211. - conditions (optional): object
  212. - strftime (optional): object: {format, field, alias}
  213. - group_by (optional): array
  214. */
  215. function selectAggregate(callback, aggregateOptions){
  216. if(!ready){
  217. console.log("db is not ready!");
  218. callback({errno: 0, code: "The database is not ready"});
  219. return;
  220. }
  221. var conditions = aggregateOptions.conditions || {};
  222. var query = Incident.aggregate(aggregateOptions.fields, conditions);
  223. if(aggregateOptions.strftime){
  224. var time = aggregateOptions.strftime;
  225. query = query.call("strftime", [orm.Text(time.format), time.field]).as(time.alias);
  226. }
  227. if(aggregateOptions.group_by)
  228. query.groupBy.apply(query, aggregateOptions.group_by);
  229. if(aggregateOptions.order)
  230. query.order.apply(query, aggregateOptions.order);
  231. query.count();
  232. query.get(callback);
  233. }
  234. exports.insert = function(items, callback){
  235. if(!ready){
  236. console.log("db is not ready!");
  237. if(callback)
  238. callback({errno: 0, code: "The database is not ready"});
  239. return;
  240. }
  241. var arr = convertToDBModel(items);
  242. Incident.create(arr, function(err, newItems){
  243. if(callback){
  244. var converted = null;
  245. if(newItems){
  246. converted = convertFromDBModel(newItems);
  247. }
  248. callback(err, converted);
  249. }
  250. else{
  251. if(err)
  252. console.log(err);
  253. else
  254. console.log("db.insert: no callback given, but also no err, so everything is fine");
  255. }
  256. });
  257. };
  258. exports.requestAttacks = function(filter, callback){
  259. select(callback, filter, null, "date");
  260. };
  261. exports.getLog = function(id, callback){
  262. if(!ready){
  263. console.log("db is not ready!");
  264. callback({errno: 0, code: "The database is not ready"});
  265. return;
  266. }
  267. // TODO: remove {cache: false} when https://github.com/dresende/node-orm2/issues/350 is resolved and released with a new version of orm
  268. Incident.find({id: id} , {cache: false}).only(["log"]).run(function(err, data){
  269. if(err){
  270. console.log(err);
  271. callback(err, null);
  272. return;
  273. }
  274. if(data && data.length>0){
  275. var log = data[0].log;
  276. callback(null, log);
  277. }
  278. else{
  279. callback(null, null);
  280. }
  281. });
  282. };
  283. /*
  284. serieFieldHelper fills aggregateOptions depending on options.detailChartType and returns serieField
  285. */
  286. function serieFieldHelper(callback, options, aggregateOptions){
  287. var serieField = {};
  288. if(options.detailChartType == "typeDate"){
  289. serieField.name = "type";
  290. }
  291. else if(options.detailChartType == "countryDate"){
  292. serieField.name = "source_cc";
  293. serieField.displayName = "source_country";
  294. }
  295. else{
  296. callback("getStatistics: unknown detailChartType: " + detailChartType);
  297. return;
  298. }
  299. // select that field
  300. aggregateOptions.fields.push(serieField.name);
  301. // select also the displayName if given
  302. if(serieField.displayName){
  303. aggregateOptions.fields.push(serieField.displayName);
  304. }
  305. // group and order by it (unshift: put it at the beginning of the array)
  306. aggregateOptions.group_by.unshift(serieField.name);
  307. aggregateOptions.order.unshift(serieField.name);
  308. return serieField;
  309. }
  310. /*
  311. extractSeries converts plain rows to an array of series
  312. If no serieField given it just creates one serie with the name "total".
  313. It also translates the content of serieField to a human readable value (e.g. "DE" -> "Germany" for source_cc, 31 -> "MySQL" for "type")
  314. * if serieField.displayName is set it takes that column, otherwise it uses fields.translate
  315. The rows need to be sorted by serieField to detect change of that field // TODO: may implement it in a way that this is not required?
  316. example input with serieField.name="source_cc" and serieField.displayName="source_country":
  317. [
  318. {source_cc: "DE", source_country: "Germany", field1: "foo1", field2: "asdf"}, // = row0
  319. {source_cc: "DE", source_country: "Germany", field1: "foo2"}, // = row1
  320. {source_cc: "US", source_country: "United States", field1: "bar1"}, // = row2
  321. {source_cc: "US", source_country: "United States", field1: "bar2"}, // = row3
  322. ]
  323. example output:
  324. [
  325. {name: "Germany", data: [row0, row1]},
  326. {name: "United States", data: [row2, row3]}
  327. ]
  328. */
  329. function extractSeries(rows, serieField){
  330. var series = [];
  331. // create for each different `serieField` a new serie
  332. if(serieField){
  333. var currentSerieValue = null;
  334. var serie = null;
  335. for(var k in rows){
  336. var row = rows[k];
  337. var rowSerieValue = row[serieField.name];
  338. // check if `serieField` changed in this row from previous row (or if it is the first row)
  339. if(currentSerieValue != rowSerieValue){
  340. // if we filled a serie it is done now, push it to finished series
  341. if(serie)
  342. series.push(serie);
  343. currentSerieValue = rowSerieValue;
  344. // create a new serie
  345. serie = { data: [] };
  346. // set the name of the serie that should be displayed
  347. if(serieField.displayName)
  348. serie.name = row[serieField.displayName];
  349. else
  350. serie.name = fields.translate(serieField, currentSerieValue);
  351. }
  352. // push this row to the current serie
  353. serie.data.push(row);
  354. }
  355. // push last serie
  356. if(serie)
  357. series.push(serie);
  358. }
  359. else{
  360. // just one serie
  361. series.push({
  362. name: "total",
  363. data: rows,
  364. });
  365. }
  366. return series;
  367. }
  368. /*
  369. converts the dates to unix time in all data of all series
  370. determines minDate and maxDate if not given
  371. fills all series with 0-data on each date between minDate and maxDate if it has no data on that day
  372. */
  373. function convertDates(series, minTimestamp, maxTimestamp){
  374. var checkMinMax = !minTimestamp || !maxTimestamp; // no logical need for this check, but should give performance as in most cases min and max are given
  375. // convert the dates to UTS timestamps for each serie
  376. for(var i = 0; i < series.length; i++){
  377. // convert it for each data entry
  378. for (var j = 0; j < series[i].data.length; j++) {
  379. var date = new Date(series[i].data[j].date);
  380. var year = date.getUTCFullYear();
  381. var month = date.getUTCMonth();
  382. var day = date.getUTCDate();
  383. var timestamp = Date.UTC(year, month, day);
  384. series[i].data[j] = [timestamp, series[i].data[j].count];
  385. if(checkMinMax){
  386. if(!minTimestamp || minTimestamp > timestamp)
  387. minTimestamp = timestamp;
  388. if(!maxTimestamp || maxTimestamp < timestamp)
  389. maxTimestamp = timestamp;
  390. }
  391. }
  392. }
  393. for(var j = 0; j < series.length; j++){
  394. var currentTimestamp = minTimestamp;
  395. var i = 0;
  396. var maxI = series[j].data.length;
  397. var newData = [];
  398. while(currentTimestamp <= maxTimestamp){
  399. //console.log("convertDates.currentTimestamp:", currentTimestamp, new Date(currentTimestamp));
  400. if(i >= maxI || currentTimestamp < series[j].data[i][0]){
  401. newData.push([currentTimestamp, 0]);
  402. }
  403. else{
  404. newData.push([currentTimestamp, series[j].data[i][1]]);
  405. i++;
  406. }
  407. // next day
  408. currentTimestamp += 24 * 60 * 60 * 1000;
  409. }
  410. series[j].data = newData;
  411. }
  412. }
  413. exports.getStatistics = function(options, callback){
  414. console.log("db.js: getStatistics called");
  415. console.log("options: " + options);
  416. var aggregateOptions = {
  417. fields: ["date"], // selecting also date makes it currently easier to convert the selected date
  418. strftime: {format: "%Y-%m-%d", field: "date", alias: "dt"},
  419. group_by: ["dt"],
  420. order: ["date"], // it does not matter if we order by date or dt
  421. conditions: {},
  422. };
  423. var serieField = null;
  424. if(options.detail)
  425. serieField = serieFieldHelper(callback, options, aggregateOptions); // sets also aggregateOptions
  426. if(options.filter){
  427. aggregateOptions.conditions = getConditions(options.filter);
  428. }
  429. var innerCallback = function(err, rows){
  430. if(err){
  431. callback(err);
  432. }
  433. if(!rows || rows.length == 0){
  434. callback(null, []);
  435. return;
  436. }
  437. var series = extractSeries(rows, serieField);
  438. //for(var i = 0; i < series.length; i++)
  439. //console.log("raw series["+i+"]", series[i]);
  440. if(options.filter)
  441. convertDates(series, options.filter.start, options.filter.end);
  442. else
  443. convertDates(series);
  444. //for(var i = 0; i < series.length; i++)
  445. //console.log("series["+i+"]", series[i]);
  446. callback(null, series);
  447. };
  448. selectAggregate(innerCallback, aggregateOptions);
  449. };