From 4015c0c381a6a20aac1f08e147d8de7507ada4d2 Mon Sep 17 00:00:00 2001 From: Corey Ogburn Date: Tue, 30 Jul 2024 09:53:22 -0600 Subject: [PATCH] Overrides Added to Detection History Because we're putting a data table in a data table, we're disabling all the hover effects for a cleaner UI. Updated dateAwareSort to be aware of new date fields and to properly sort non-string fields (such as isEnabled). In the tuning table, I pass the `type` fields through i18n so they're the proper case. Duplicated the logic in findHistoryChange into findOverrideHistoryChange but refactored for the simpler case. We don't need to highlight changes while ignoring source code changes. Added tests. --- html/css/app.css | 12 + html/index.html | 403 +++++++++++++++++++------------ html/js/app.js | 6 +- html/js/i18n.js | 2 + html/js/routes/detection.js | 76 +++++- html/js/routes/detection.test.js | 178 +++++++++++++- 6 files changed, 516 insertions(+), 161 deletions(-) diff --git a/html/css/app.css b/html/css/app.css index ab21f820..4b79ad05 100644 --- a/html/css/app.css +++ b/html/css/app.css @@ -673,3 +673,15 @@ code { .warning-message a:visited { color: white; } + +tbody tr:hover { + background-color: transparent !important; +} + +.theme--dark tbody tr:hover:nth-of-type(even) { + background-color: rgba(45, 45, 45, 0.25) !important; +} + +.theme--light tbody tr:hover:nth-of-type(even) { + background-color: rgba(230, 230, 230, 0.25) !important; +} \ No newline at end of file diff --git a/html/index.html b/html/index.html index dd070bd2..4bf04846 100644 --- a/html/index.html +++ b/html/index.html @@ -1141,7 +1141,6 @@

{{ detect.title }}

fa-history
{{ i18n.history }}
- @@ -1288,7 +1287,6 @@

{{ i18n.commentAddDetection }}

- @@ -1310,7 +1308,6 @@

{{ i18n.commentAddDetection }}

- @@ -1395,8 +1392,9 @@

{{ i18n.commentAddDetection }}

- {{ item[field.value] }} - + + {{ $root.tryLocalize(item[field.value]) }} + {{ item[field.value] }}
@@ -1487,7 +1485,7 @@

{{ i18n.commentAddDetection }}

- IP: + {{i18n.ip}}:
{{item.ip}} @@ -1622,158 +1620,257 @@

{{ i18n.commentAddDetection }}

- - - - + + +
+ +
diff --git a/html/js/app.js b/html/js/app.js index bd357e9f..64f01b36 100644 --- a/html/js/app.js +++ b/html/js/app.js @@ -1208,7 +1208,7 @@ $(document).ready(function() { }, dateAwareSort(items, index, isDesc) { items.sort((a, b) => { - if (index[0] === 'createTime' || index[0] === 'updateTime') { + if (index[0] === 'createTime' || index[0] === 'updateTime' || index[0] === 'createdAt' || index[0] === 'updatedAt') { if (!isDesc[0]) { return new Date(a[index]) - new Date(b[index]); } @@ -1218,10 +1218,10 @@ $(document).ready(function() { if (typeof a[index] !== 'undefined') { if (!isDesc[0]) { - return a[index].toLowerCase().localeCompare(b[index].toLowerCase()); + return (a[index]+'').toLowerCase().localeCompare((b[index]+'').toLowerCase()); } - return b[index].toLowerCase().localeCompare(a[index].toLowerCase()); + return (b[index]+'').toLowerCase().localeCompare((a[index]+'').toLowerCase()); } }); diff --git a/html/js/i18n.js b/html/js/i18n.js index 8208bc65..38f45e62 100644 --- a/html/js/i18n.js +++ b/html/js/i18n.js @@ -537,6 +537,7 @@ const i18n = { invalid: 'Invalid', ioWait: 'I/O Wait', invalidCidrOrVar: 'Invalid CIDR Notation or Suricata Variable', + ip: 'IP', ipCidr: 'IP - CIDR Notation or Suricata Variable', job: 'Job', jobIncomplete: 'The job was unable to complete and will retry within a few minutes. Details are available below.', @@ -653,6 +654,7 @@ const i18n = { overrideDeleteHelp: 'Delete this rule override', overrideDocumentationHelp: 'Click for more information', overrideExpand: 'Show all override fields', + overrides: 'Overrides', overview: 'Overview', owner: 'Owner', packages: 'Packages', diff --git a/html/js/routes/detection.js b/html/js/routes/detection.js index cd1a0bc8..762653f7 100644 --- a/html/js/routes/detection.js +++ b/html/js/routes/detection.js @@ -62,7 +62,7 @@ routes.push({ path: '/detection/:id', name: 'detection', component: { overrideHeaders: { 'elastalert': [ { text: this.$root.i18n.enabled, value: 'isEnabled' }, - { text: this.$root.i18n.type, value: 'type' }, + { text: this.$root.i18n.type, value: 'type', localize: true }, { text: this.$root.i18n.track, value: 'track' }, { text: this.$root.i18n.dateCreated, value: 'createdAt', format: true }, { text: this.$root.i18n.dateModified, value: 'updatedAt', format: true }, @@ -70,7 +70,7 @@ routes.push({ path: '/detection/:id', name: 'detection', component: { 'strelka': [], // no overrides 'suricata': [ { text: this.$root.i18n.enabled, value: 'isEnabled' }, - { text: this.$root.i18n.type, value: 'type' }, + { text: this.$root.i18n.type, value: 'type', localize: true }, { text: this.$root.i18n.ipVar, value: 'ip' }, { text: this.$root.i18n.dateCreated, value: 'createdAt', format: true }, { text: this.$root.i18n.dateModified, value: 'updatedAt', format: true }, @@ -101,6 +101,38 @@ routes.push({ path: '/detection/:id', name: 'detection', component: { expanded: [], loading: false, }, + historyOverrideTableOpts: { + "elastalert": { + sortBy: 'updatedAt', + sortDesc: false, + headers: [ + { text: this.$root.i18n.actions, width: '10.0em' }, + { text: this.$root.i18n.kind, value: 'type' }, + { text: this.$root.i18n.time, value: 'updatedAt' }, + { text: this.$root.i18n.enabled, value: 'isEnabled' }, + ], + itemsPerPage: 10, + footerProps: { 'items-per-page-options': [10, 50, 250, 1000] }, + count: 500, + expanded: [], + loading: false, + }, + "suricata": { + sortBy: 'updatedAt', + sortDesc: false, + headers: [ + { text: this.$root.i18n.actions, width: '10.0em' }, + { text: this.$root.i18n.kind, value: 'type' }, + { text: this.$root.i18n.time, value: 'updatedAt' }, + { text: this.$root.i18n.enabled, value: 'isEnabled' }, + ], + itemsPerPage: 10, + footerProps: { 'items-per-page-options': [10, 50, 250, 1000] }, + count: 500, + expanded: [], + loading: false, + }, + }, extractedSummary: '', extractedReferences: [], extractedLogic: '', @@ -140,6 +172,7 @@ routes.push({ path: '/detection/:id', name: 'detection', component: { 'yara': 'strelka', }, changedKeys: {}, + changedOverrideKeys: {}, }}, created() { this.$root.initializeEditor(); @@ -504,10 +537,9 @@ routes.push({ path: '/detection/:id', name: 'detection', component: { if (response && response.data) { this.history = response.data; - //this.changedKeys[this.history[this.history.length - 1]['id']] = this.findHistoryChange(this.history); - for (var i = 0; i < this.history.length; i++) { this.$root.populateUserDetails(this.history[i], "userId", "owner"); + this.history[i].overrides = this.history[i].overrides || []; } } if (showLoadingIndicator) this.$root.stopLoading(); @@ -563,7 +595,40 @@ routes.push({ path: '/detection/:id', name: 'detection', component: { } } } + this.changedKeys[id] = retList; + + this.findOverrideHistoryChange(id); + }, + findOverrideHistoryChange(historyID) { + let index = this.history.findIndex((row) => row.id === historyID); + + if (index <= 0) return; + + let prev = this.history[index - 1]; + let parent = this.history[index]; + let releventKeys = ['isEnabled', 'customFilter', 'regex', 'value', 'track', 'ip', 'count', 'seconds']; + let overrideRetList = []; + + for (let i = 0; i < parent.overrides.length; i++) { + let retList = []; + let newOverride = parent.overrides[i]; + let oldOverride = prev.overrides.find(o => o.createdAt === newOverride.createdAt); + + if (oldOverride == null) { + return; + } + + for (let key of releventKeys) { + if (oldOverride[key] !== newOverride[key]) { + retList.push(key); + } + } + + overrideRetList.push(retList); + } + + this.changedOverrideKeys[historyID] = overrideRetList; }, getDefaultPreset(preset) { if (this.presets) { @@ -1354,6 +1419,9 @@ routes.push({ path: '/detection/:id', name: 'detection', component: { }, checkChangedKey(id, key) { return this.changedKeys[id]?.includes(key); + }, + checkOverrideChangedKey(id, index, key) { + return this.changedOverrideKeys?.[id]?.[index]?.includes(key); } } }}); diff --git a/html/js/routes/detection.test.js b/html/js/routes/detection.test.js index fe86ec9e..662f7f8c 100644 --- a/html/js/routes/detection.test.js +++ b/html/js/routes/detection.test.js @@ -659,7 +659,6 @@ test('getPresets', () => { }); test('findHistoryChange', () => { - // elastalert comp.history = [ { @@ -831,6 +830,183 @@ test('checkChangedKey', () => { expect(comp.checkChangedKey(id, 'severity')).toBe(false); }); +test('findOverrideHistoryChange', () => { + // elastalert + comp.history = [ + { + id: '1', + overrides: [ + { + type: "customFilter", + isEnabled: true, + createdAt: "2024-07-29T10:29:32.421321098-06:00", + updatedAt: "2024-07-29T10:29:32.421321098-06:00", + customFilter: "this:\n that" + }, + { + type: "customFilter", + isEnabled: false, + createdAt: "2024-07-29T11:54:34.569557439-06:00", + updatedAt: "2024-07-29T11:54:49.086205751-06:00", + customFilter: "another:\n one" + } + ], + }, + { + id: '2', + overrides: [ + { + type: "customFilter", + isEnabled: true, + createdAt: "2024-07-29T10:29:32.421321098-06:00", + updatedAt: "2024-07-29T10:29:32.421321098-06:00", + customFilter: "this:\n that" + }, + { + type: "customFilter", + isEnabled: false, + createdAt: "2024-07-29T11:54:34.569557439-06:00", + updatedAt: "2024-07-29T13:55:29.197427405-06:00", + customFilter: "another:\n two" + } + ], + } + ]; + + id = '2'; + comp.changedOverrideKeys = {}; + comp.findOverrideHistoryChange(id); + expect(Object.keys(comp.changedOverrideKeys).length).toBe(1); + + let overrideKeys = comp.changedOverrideKeys[id]; + expect(overrideKeys.length).toBe(2); + expect(overrideKeys[0].length).toBe(0); + expect(overrideKeys[1].length).toBe(1); + expect(overrideKeys[1][0]).toBe('customFilter'); + + // suricata + comp.history = [ + { + id: '1', + overrides: [ + { + type: "modify", + isEnabled: false, + createdAt: "2024-07-29T14:39:09.544042454-06:00", + updatedAt: "2024-07-29T14:39:21.947393163-06:00", + regex: "rev: 2", + value: "rev: 3" + }, + { + type: "suppress", + isEnabled: false, + createdAt: "2024-07-29T14:39:44.312946909-06:00", + updatedAt: "2024-07-29T14:57:49.637548072-06:00", + track: "by_either", + ip: "0.0.0.0/0" + }, + { + type: "threshold", + isEnabled: true, + createdAt: "2024-07-29T14:40:17.043093339-06:00", + updatedAt: "2024-07-29T14:40:17.043093339-06:00", + thresholdType: "both", + track: "by_src", + count: 10, + seconds: 60 + } + ] + }, + { + id: '2', + overrides: [ + { + type: "modify", + isEnabled: true, + createdAt: "2024-07-29T14:39:09.544042454-06:00", + updatedAt: "2024-07-29T14:39:20.947393160-06:00", + regex: "rev: 2", + value: "rev: 3" + }, + { + type: "suppress", + isEnabled: false, + createdAt: "2024-07-29T14:39:44.312946909-06:00", + updatedAt: "2024-07-29T14:57:50.637548070-06:00", + track: "by_src", + ip: "0.0.0.0/1" + }, + { + type: "threshold", + isEnabled: true, + createdAt: "2024-07-29T14:40:17.043093339-06:00", + updatedAt: "2024-07-29T14:40:17.043093339-06:00", + thresholdType: "both", + track: "by_src", + "count": 10, + "seconds": 60 + } + ], + } + ]; + + id = '2'; + comp.changedOverrideKeys = {}; + comp.findOverrideHistoryChange(id); + expect(Object.keys(comp.changedOverrideKeys).length).toBe(1); + + overrideKeys = comp.changedOverrideKeys[id]; + expect(overrideKeys.length).toBe(3); + expect(overrideKeys[0].length).toBe(1); + expect(overrideKeys[0][0]).toBe('isEnabled'); + expect(overrideKeys[1].length).toBe(2); + expect(overrideKeys[1][0]).toBe('track'); + expect(overrideKeys[1][1]).toBe('ip'); + expect(overrideKeys[2].length).toBe(0); + + id = '1'; + comp.changedOverrideKeys = {}; + comp.findOverrideHistoryChange(id); + expect(Object.keys(comp.changedOverrideKeys).length).toBe(0); + + // elastalert, nothing to diff + comp.history = [ + { + id: '1', + overrides: [], + }, + { + id: '2', + overrides: [ + { + type: "customFilter", + isEnabled: true, + createdAt: "2024-07-29T10:29:32.421321098-06:00", + updatedAt: "2024-07-29T10:29:32.421321098-06:00", + customFilter: "this:\n that" + }, + ], + } + ]; + + id = '2'; + comp.changedOverrideKeys = {}; + comp.findOverrideHistoryChange(id); + expect(Object.keys(comp.changedOverrideKeys).length).toBe(0); +}); + +test('checkOverrideChangedKey', () => { + expect(Object.keys(comp.changedKeys).length).toBe(0); + let id = "BHypPJABXppUUuo3UnEl"; + comp.changedOverrideKeys[id] = [[], ['track', 'ip']]; + expect(comp.checkOverrideChangedKey(id, 0, 'track')).toBe(false); + expect(comp.checkOverrideChangedKey(id, 1, 'track')).toBe(true); + expect(comp.checkOverrideChangedKey(id, 0, 'ip')).toBe(false); + expect(comp.checkOverrideChangedKey(id, 1, 'ip')).toBe(true); + expect(comp.checkOverrideChangedKey(id, 0, 'seconds')).toBe(false); + expect(comp.checkOverrideChangedKey(id, 1, 'seconds')).toBe(false); +}); + test('loadUrlParameters', () => { let nextTickCalled = 0; comp.$nextTick = (f) => {