diff --git a/thehive-misp/app/connectors/misp/MispSrv.scala b/thehive-misp/app/connectors/misp/MispSrv.scala index 0f0c9e5c5f..31711567fb 100644 --- a/thehive-misp/app/connectors/misp/MispSrv.scala +++ b/thehive-misp/app/connectors/misp/MispSrv.scala @@ -373,7 +373,7 @@ class MispSrv @Inject() ( def find(queryDef: QueryDef, range: Option[String], sortBy: Seq[String]): (Source[Misp, NotUsed], Future[Long]) = { import org.elastic4play.services.QueryDSL._ - findSrv[MispModel, Misp](mispModel, and(queryDef, not("eventStatus" in ("Ignore", "Imported"))), range, sortBy) + findSrv[MispModel, Misp](mispModel, queryDef, range, sortBy) } def stats(queryDef: QueryDef, aggs: Seq[Agg]) = findSrv(mispModel, queryDef, aggs: _*) diff --git a/ui/app/index.html b/ui/app/index.html index 57baf77e67..b167a0e54f 100644 --- a/ui/app/index.html +++ b/ui/app/index.html @@ -136,6 +136,7 @@ + @@ -193,6 +194,7 @@ + diff --git a/ui/app/scripts/controllers/misp/MispListCtrl.js b/ui/app/scripts/controllers/misp/MispListCtrl.js index e8e522d61e..01558f55c5 100644 --- a/ui/app/scripts/controllers/misp/MispListCtrl.js +++ b/ui/app/scripts/controllers/misp/MispListCtrl.js @@ -1,7 +1,7 @@ (function() { 'use strict'; angular.module('theHiveControllers') - .controller('MispListCtrl', function($scope, $q, $state, $uibModal, MispSrv, AlertSrv) { + .controller('MispListCtrl', function($scope, $q, $state, $uibModal, MispSrv, AlertSrv, FilteringSrv) { var self = this; self.list = []; @@ -9,8 +9,85 @@ self.menu = { follow: false, unfollow: false, + markAsRead: false, selectAll: false }; + self.filtering = new FilteringSrv('misp-section', { + defaults: { + showFilters: false, + showStats: false, + pageSize: 15, + sort: ['-publishDate'] + }, + defaultFilter: { + eventStatus: { + field: 'eventStatus', + label: 'Status', + value: [{ + text: 'New' + }, { + text: 'Update' + }], + filter: '(eventStatus:"New" OR eventStatus:"Update")' + } + }, + filterDefs: { + keyword: { + field: 'keyword', + type: 'string', + defaultValue: [] + }, + eventStatus: { + field: 'eventStatus', + type: 'list', + defaultValue: [], + label: 'Status' + }, + tags: { + field: 'tags', + type: 'list', + defaultValue: [], + label: 'Tags' + }, + org: { + field: 'org', + type: 'list', + defaultValue: [], + label: 'Source' + }, + info: { + field: 'info', + type: 'string', + defaultValue: '', + label: 'Title' + }, + publishDate: { + field: 'publishDate', + type: 'date', + defaultValue: { + from: null, + to: null + }, + label: 'Publish Date' + } + } + }); + self.filtering.initContext('list'); + self.searchForm = { + searchQuery: self.filtering.buildQuery() || '' + }; + + $scope.$watch('misp.list.pageSize', function (newValue) { + self.filtering.setPageSize(newValue); + }); + + this.toggleStats = function () { + this.filtering.toggleStats(); + }; + + this.toggleFilters = function () { + this.filtering.toggleFilters(); + }; self.follow = function(event) { var fn = angular.noop; @@ -120,7 +197,17 @@ }; self.load = function() { - self.list = MispSrv.list($scope, self.resetSelection); + var config = { + scope: $scope, + filter: self.searchForm.searchQuery !== '' ? { + _string: self.searchForm.searchQuery + } : '', + loadAll: false, + sort: self.filtering.context.sort, + pageSize: self.filtering.context.pageSize, + }; + + self.list = MispSrv.list(config, self.resetSelection); }; self.cancel = function() { @@ -132,6 +219,11 @@ self.menu.unfollow = temp.length === 1 && temp[0] === true; self.menu.follow = temp.length === 1 && temp[0] === false; + + + temp = _.uniq(_.pluck(self.selection, 'eventStatus')); + + self.menu.markAsRead = temp.indexOf('Ignore') === -1; }; self.select = function(event) { @@ -163,6 +255,103 @@ }; + this.filter = function () { + self.filtering.filter().then(this.applyFilters); + }; + + this.applyFilters = function () { + self.searchForm.searchQuery = self.filtering.buildQuery(); + self.search(); + }; + + this.clearFilters = function () { + self.filtering.clearFilters().then(this.applyFilters); + }; + + this.addFilter = function (field, value) { + self.filtering.addFilter(field, value).then(this.applyFilters); + }; + + this.removeFilter = function (field) { + self.filtering.removeFilter(field).then(this.applyFilters); + }; + + this.search = function () { + this.list.filter = { + _string: this.searchForm.searchQuery + }; + + this.list.update(); + }; + this.addFilterValue = function (field, value) { + var filterDef = self.filtering.filterDefs[field]; + var filter = self.filtering.activeFilters[field]; + var date; + + if (filter && filter.value) { + if (filterDef.type === 'list') { + if (_.pluck(filter.value, 'text').indexOf(value) === -1) { + filter.value.push({ + text: value + }); + } + } else if (filterDef.type === 'date') { + date = moment(value); + self.filtering.activeFilters[field] = { + value: { + from: date.hour(0).minutes(0).seconds(0).toDate(), + to: date.hour(23).minutes(59).seconds(59).toDate() + } + }; + } else { + filter.value = value; + } + } else { + if (filterDef.type === 'list') { + self.filtering.activeFilters[field] = { + value: [{ + text: value + }] + }; + } else if (filterDef.type === 'date') { + date = moment(value); + self.filtering.activeFilters[field] = { + value: { + from: date.hour(0).minutes(0).seconds(0).toDate(), + to: date.hour(23).minutes(59).seconds(59).toDate() + } + }; + } else { + self.filtering.activeFilters[field] = { + value: value + }; + } + } + + this.filter(); + }; + + this.filterByStatus = function(status) { + self.filtering.clearFilters() + .then(function(){ + self.addFilterValue('eventStatus', status); + }); + }; + + this.sortBy = function(sort) { + self.list.sort = sort; + self.list.update(); + self.filtering.setSort(sort); + }; + + this.getStatuses = function(query) { + return MispSrv.statuses(query); + }; + + this.getSources = function(query) { + return MispSrv.sources(query); + }; + self.load(); }); })(); diff --git a/ui/app/scripts/controllers/misp/MispStatsCtrl.js b/ui/app/scripts/controllers/misp/MispStatsCtrl.js new file mode 100644 index 0000000000..203244edcc --- /dev/null +++ b/ui/app/scripts/controllers/misp/MispStatsCtrl.js @@ -0,0 +1,87 @@ +/** + * Controller for About The Hive modal page + */ +(function() { + 'use strict'; + + angular.module('theHiveControllers').controller('MispStatsCtrl', + function($rootScope, $scope, $stateParams, $timeout, StatSrv, StreamStatSrv, FilteringSrv) { + var self = this; + + this.filtering = FilteringSrv; + + this.bySources = {}; + this.byStatus = {}; + this.byTags = {}; + + // Get stats by tags + StreamStatSrv({ + scope: $scope, + rootId: 'any', + query: {}, + objectType: 'connector/misp', + streamObjectType: 'misp', + field: 'tags', + sort: ['-count'], + limit: 5, + result: {}, + success: function(data){ + self.byTags = self.prepareResult(data); + } + }); + + // Get stats by type + StreamStatSrv({ + scope: $scope, + rootId: 'any', + query: {}, + objectType: 'connector/misp', + streamObjectType: 'misp', + field: 'eventStatus', + result: {}, + success: function(data){ + self.byStatus = self.prepareResult(data); + } + }); + + // Get stats by ioc + StreamStatSrv({ + scope: $scope, + rootId: 'any', + query: {}, + objectType: 'connector/misp', + streamObjectType: 'misp', + field: 'org', + sort: ['-count'], + limit: 5, + result: {}, + success: function(data){ + self.bySources = self.prepareResult(data); + } + }); + + this.prepareResult = function(rawStats) { + var total = rawStats.count; + + var keys = _.without(_.keys(rawStats), 'count'); + var columns = keys.map(function(key) { + return { + key: key, + count: rawStats[key].count + }; + }).sort(function(a, b) { + return a.count <= b.count; + }); + + return { + total: total, + details: columns + }; + }; + + this.filterBy = function(field, value) { + this.filtering.addFilter(field, value); + }; + } + ); +})(); diff --git a/ui/app/scripts/services/FilteringSrv.js b/ui/app/scripts/services/FilteringSrv.js new file mode 100644 index 0000000000..fa98d5e5f0 --- /dev/null +++ b/ui/app/scripts/services/FilteringSrv.js @@ -0,0 +1,200 @@ +(function() { + 'use strict'; + angular.module('theHiveServices') + .service('FilteringSrv', function($q, localStorageService) { + return function(sectionName, config) { + var self = this; + + this.sectionName = sectionName; + this.config = config; + this.defaults = config.defaults || {}; + this.filterDefs = config.filterDefs || {}; + this.defaultFilter = config.defaultFilter || {}; + + this.filters = {}; + this.activeFilters = {}; + this.context = { + state: null, + showFilters: false, + showStats: false, + pageSize: this.defaults.pageSize || 15, + sort: this.defaults.sort || [] + }; + + this.initContext = function(state) { + var storedContext = localStorageService.get(self.sectionName); + if (storedContext) { + self.context = storedContext; + self.filters = storedContext.filters || {}; + self.activeFilters = storedContext.activeFilters || {}; + return; + } else { + self.context = { + state: state, + showFilters: false, + showStats: false, + pageSize: self.defaults.pageSize || 15, + sort: self.defaults.sort || [] + }; + + self.filters = self.defaultFilter; + self.activeFilters = _.mapObject(self.defaultFilter || {}, function(val){ + return _.omit(val, 'field', 'filter'); + }); + + self.storeContext(); + } + }; + + this.initFilters = function() { + self.activeFilters = {}; + _.each(_.keys(self.filterDefs), function(key) { + var def = self.filterDefs[key]; + self.activeFilters[key] = { + field: def.field, + type: def.type, + value: self.hasFilter(key) ? angular.copy(self.getFilterValue(key)) : angular.copy(def.defaultValue) + }; + }); + }; + + this.isEmpty = function(value) { + return value === undefined || value === null || value.length === 0 || (angular.isObject(value) &&_.without(_.values(value), null, undefined, '').length === 0); + }; + + this.clearFilters = function() { + self.filters = {}; + self.activeFilters = {}; + self.storeContext(); + return $q.resolve({}); + }; + + this.filter = function() { + var activeFilters = self.activeFilters; + + _.each(activeFilters, function(filterValue, field /*, filters*/ ) { + var value = filterValue.value; + + if (!self.isEmpty(value)) { + self.addFilter(field, angular.copy(value)); + } else { + self.removeFilter(field); + } + }); + + self.storeContext(); + + return $q.resolve(); + }; + + self.addFilter = function(field, value) { + var query, + filterDef = self.filterDefs[field]; + + // Prepare the filter value + if (field === 'keyword') { + query = value; + } else if (angular.isArray(value) && value.length > 0) { + query = _.map(value, function(val) { + return field + ':"' + val.text + '"'; + }).join(' OR '); + query = '(' + query + ')'; + } else if (filterDef.type === 'date') { + var fromDate = value.from ? moment(value.from).hour(0).minutes(0).seconds(0).valueOf() : '*', + toDate = value.to ? moment(value.to).hour(23).minutes(59).seconds(59).valueOf() : '*'; + + query = field + ':[ ' + fromDate + ' TO ' + toDate + ' ]'; + + } else { + query = field + ':' + value; + } + + self.filters[field] = { + field: field, + label: filterDef.label || field, + value: value, + filter: query + }; + + return $q.resolve(self.filters); + }; + + this.removeFilter = function(field) { + var filter = self.activeFilters[field]; + + if(_.isObject(filter.value) && !_.isArray(filter.value)) { + _.each(filter.value, function(value, key) { + filter.value[key] = null; + }); + } + + delete self.filters[field]; + delete self.activeFilters[field]; + + self.storeContext(); + + return $q.resolve(self.filters); + }; + + this.hasFilter = function(field) { + return self.filters[field]; + }; + + this.hasFilters = function() { + return _.keys(self.filters).length > 0; + }; + + this.countFilters = function() { + return _.keys(self.filters).length; + }; + + this.countSorts = function() { + return self.context.sort.length; + }; + + this.getFilterValue = function(field) { + if (self.filters[field]) { + return self.filters[field].value; + } + }; + + this.buildQuery = function() { + if (_.keys(self.filters).length === 0) { + return; + } + + _.keys(self.filters).map(function(key) { + return self.filters[key].filter; + }).join(' AND '); + + return _.pluck(self.filters, 'filter').join(' AND '); + }; + + this.toggleStats = function() { + self.context.showStats = !self.context.showStats; + self.storeContext(); + }; + + this.toggleFilters = function() { + self.context.showFilters = !self.context.showFilters; + self.storeContext(); + }; + + this.setPageSize = function(pageSize) { + self.context.pageSize = pageSize; + self.storeContext(); + }; + + this.setSort = function(sorts) { + self.context.sort = sorts; + self.storeContext(); + }; + + this.storeContext = function() { + self.context.filters = self.filters; + self.context.activeFilters = self.activeFilters; + localStorageService.set(self.sectionName, self.context); + }; + } + }); +})(); diff --git a/ui/app/scripts/services/MispSrv.js b/ui/app/scripts/services/MispSrv.js index 14dac0cd34..2c38651928 100644 --- a/ui/app/scripts/services/MispSrv.js +++ b/ui/app/scripts/services/MispSrv.js @@ -6,12 +6,14 @@ var baseUrl = './api/connector/misp'; var factory = { - list: function(scope, callback) { + + list: function(config, callback) { return PSearchSrv(undefined, 'connector/misp', { - scope: scope, - sort: '-publishDate', - loadAll: false, - pageSize: 10, + scope: config.scope, + sort: config.sort || '-publishDate', + loadAll: config.loadAll || false, + pageSize: config.pageSize || 10, + filter: config.filter || '', onUpdate: callback || angular.noop, streamObjectType: 'misp' }); @@ -47,17 +49,9 @@ stats: function(scope) { var field = 'eventStatus', - mispStatQuery = { - _not: { - _in: { - _field: 'eventStatus', - _values: ['Ignore', 'Imported'] - } - } - }, result = {}, statConfig = { - query: mispStatQuery, + query: {}, objectType: 'connector/misp', field: field, result: result, @@ -77,6 +71,51 @@ }); return StatSrv.get(statConfig); + }, + + sources: function(query) { + var defer = $q.defer(); + + StatSrv.getPromise({ + objectType: 'connector/misp', + field: 'org', + limit: 1000 + }).then(function(response) { + var sources = []; + + sources = _.map(_.filter(_.keys(response.data), function(source) { + var regex = new RegExp(query, 'gi'); + return regex.test(source); + }), function(source) { + return {text: source}; + }); + + defer.resolve(sources); + }); + + return defer.promise; + }, + + statuses: function(query) { + var defer = $q.defer(); + + $q.resolve([ + {text: 'New'}, + {text: 'Update'}, + {text: 'Imported'}, + {text: 'Ignore'} + ]).then(function(response) { + var statuses = []; + + statuses = _.filter(response, function(status) { + var regex = new RegExp(query, 'gi'); + return regex.test(status.text); + }); + + defer.resolve(statuses); + }); + + return defer.promise; } }; diff --git a/ui/app/views/app.html b/ui/app/views/app.html index c889275e2b..cee2853f7a 100644 --- a/ui/app/views/app.html +++ b/ui/app/views/app.html @@ -45,7 +45,7 @@
- | Event ID | +Event ID | +Status | Title | Source | -Severity | +Severity | Attributes | -Tags | -Date | +Publish Date | - {{::event.eventId}} + {{::event.eventId}} + | ++ {{::event.eventStatus}} |
- {{::event.eventStatus}}
-
- {{::event.info}}
-
- {{::event.info}}
+
+
+ {{::event.info}}
+
+ {{::event.info}}
+
-
+
+
|
- {{event.org}} | +{{event.org}} |
|
{{::event.attributeCount}} | -- - {{tag}} - - | -{{event.publishDate | showDate}} | +{{event.publishDate | showDate}} |
-
+
diff --git a/ui/app/views/partials/misp/list/filters.html b/ui/app/views/partials/misp/list/filters.html
new file mode 100644
index 0000000000..91f2a8be8d
--- /dev/null
+++ b/ui/app/views/partials/misp/list/filters.html
@@ -0,0 +1,93 @@
+
+
diff --git a/ui/app/views/partials/misp/list/mini-stats.html b/ui/app/views/partials/misp/list/mini-stats.html
new file mode 100644
index 0000000000..6e9d3370a8
--- /dev/null
+++ b/ui/app/views/partials/misp/list/mini-stats.html
@@ -0,0 +1,61 @@
+
+
+Filters+ + +
+
+
diff --git a/ui/app/views/partials/misp/list/toolbar.html b/ui/app/views/partials/misp/list/toolbar.html
new file mode 100644
index 0000000000..f629a7fe57
--- /dev/null
+++ b/ui/app/views/partials/misp/list/toolbar.html
@@ -0,0 +1,83 @@
+
+
+ Statistics+
+
+
+
+
+ Events by Status
+
+
+
+
+
+
+
+ Top 5 Sources
+
+
+
+
+
+
+ Top 5 tags
+
+
+
+
diff --git a/ui/bower.json b/ui/bower.json
index 6e4e176cbb..5ee1ca6831 100644
--- a/ui/bower.json
+++ b/ui/bower.json
@@ -19,7 +19,7 @@
"bootstrap": "~3.3.7",
"bootstrap-sass-official": "~3.3.7",
"dropzone": "~4.3.0",
- "font-awesome": "fontawesome#~4.4.0",
+ "font-awesome": "fontawesome#^4.7.0",
"jquery": "~3.1.0",
"moment": "~2.14.1",
"ng-csv": "~0.3.6",
+
+
+ |
---|