From 58ee825063f502abfcc27ec4c8377816330e319b Mon Sep 17 00:00:00 2001 From: Nabil Adouani Date: Tue, 7 Jul 2020 19:12:31 +0200 Subject: [PATCH] #1423 Add case bulk edit feature --- frontend/app/index.html | 1 + .../scripts/controllers/case/CaseListCtrl.js | 74 +++++++- .../controllers/case/CaseUpdateCtrl.js | 175 ++++++++++++++++++ frontend/app/scripts/services/api/CaseSrv.js | 4 + frontend/app/scripts/services/api/TagSrv.js | 4 +- .../app/views/partials/case/case.list.html | 7 +- .../app/views/partials/case/case.update.html | 75 ++++++++ .../app/views/partials/case/list/toolbar.html | 12 ++ .../thp/thehive/controllers/v0/Router.scala | 4 +- 9 files changed, 349 insertions(+), 7 deletions(-) create mode 100644 frontend/app/scripts/controllers/case/CaseUpdateCtrl.js create mode 100644 frontend/app/views/partials/case/case.update.html diff --git a/frontend/app/index.html b/frontend/app/index.html index ec7744a6e2..620f05b24f 100644 --- a/frontend/app/index.html +++ b/frontend/app/index.html @@ -180,6 +180,7 @@ + diff --git a/frontend/app/scripts/controllers/case/CaseListCtrl.js b/frontend/app/scripts/controllers/case/CaseListCtrl.js index f24478b20e..497187bb59 100644 --- a/frontend/app/scripts/controllers/case/CaseListCtrl.js +++ b/frontend/app/scripts/controllers/case/CaseListCtrl.js @@ -3,7 +3,7 @@ angular.module('theHiveControllers') .controller('CaseListCtrl', CaseListCtrl); - function CaseListCtrl($scope, $q, $state, $window, FilteringSrv, StreamStatSrv, PaginatedQuerySrv, EntitySrv, TagSrv, UserSrv, AuthenticationSrv, CaseResolutionStatus, NotificationSrv, Severity, Tlp, CortexSrv) { + function CaseListCtrl($scope, $q, $state, $window, $uibModal, FilteringSrv, StreamStatSrv, PaginatedQuerySrv, EntitySrv, CaseSrv, UserSrv, AuthenticationSrv, CaseResolutionStatus, NotificationSrv, Severity, Tlp, CortexSrv) { var self = this; this.openEntity = EntitySrv.open; @@ -13,6 +13,11 @@ this.lastQuery = null; + self.selection = []; + self.menu = { + selectAll: false + }; + this.$onInit = function() { self.filtering = new FilteringSrv('case', 'case.list', { version: 'v1', @@ -67,8 +72,48 @@ operations: [ {'_name': 'listCase'} ], - extraData: ["observableStats", "taskStats", "isOwner", "shareCount"] + extraData: ["observableStats", "taskStats", "isOwner", "shareCount"], + onUpdate: function() { + self.resetSelection(); + } + }); + }; + + self.resetSelection = function() { + if (self.menu.selectAll) { + self.selectAll(); + } else { + self.selection = []; + self.menu.selectAll = false; + // self.updateMenu(); + } + }; + + self.select = function(caze) { + if (caze.selected) { + self.selection.push(caze); + } else { + self.selection = _.reject(self.selection, function(item) { + return item._id === caze._id; + }); + } + // self.updateMenu(); + }; + + self.selectAll = function() { + var selected = self.menu.selectAll; + _.each(self.list.values, function(item) { + item.selected = selected; }); + + if (selected) { + self.selection = self.list.values; + } else { + self.selection = []; + } + + //self.updateMenu(); + }; this.toggleStats = function () { @@ -151,6 +196,31 @@ this.filtering.setSort(sort); }; + this.bulkEdit = function() { + var modal = $uibModal.open({ + animation: 'true', + templateUrl: 'views/partials/case/case.update.html', + controller: 'CaseUpdateCtrl', + controllerAs: '$dialog', + size: 'lg', + resolve: { + selection: function() { + return self.selection; + } + } + }); + + modal.result.then(function(operations) { + console.log(operations); + + $q.all(_.map(operations, function(operation) { + return CaseSrv.bulkUpdate(operation.ids, operation.patch); + })).then(function(/*responses*/) { + NotificationSrv.log('Selected cases have been updated successfully', 'success'); + }); + }); + }; + this.getCaseResponders = function(caseId, force) { if (!force && this.caseResponders !== null) { return; diff --git a/frontend/app/scripts/controllers/case/CaseUpdateCtrl.js b/frontend/app/scripts/controllers/case/CaseUpdateCtrl.js new file mode 100644 index 0000000000..cb241c6a0f --- /dev/null +++ b/frontend/app/scripts/controllers/case/CaseUpdateCtrl.js @@ -0,0 +1,175 @@ +(function() { + 'use strict'; + angular.module('theHiveControllers').controller('CaseUpdateCtrl', + function($scope, $uibModalInstance, TagSrv, selection) { + var self = this; + + this.selection = selection; + this.state = { + all: false, + enableTlp: false, + enablePap: false, + enableSeverity: false, + enableAddTags: false, + enableRemoveTags: false + }; + + this.activeTlp = 'active'; + this.activePap = 'active'; + this.activeSeverity = true; + + this.params = { + ioc: false, + tlp: 2, + pap: 2, + severity: 2, + addTagNames: '', + removeTagNames: '' + }; + + this.toggleAll = function() { + + this.state.all = !this.state.all; + + this.state.enableTlp = this.state.all; + this.state.enablePap = this.state.all; + this.state.enableSeverity = this.state.all; + this.state.enableAddTags = this.state.all; + this.state.enableRemoveTags = this.state.all; + }; + + this.categorizeObservables = function() { + var data = { + withTags: [], + withoutTags: [] + }; + + _.each(this.selection, function(item) { + if(item.tags.length > 0) { + data.withTags.push(item); + } else { + data.withoutTags.push(item); + } + }); + + return data; + }; + + this.buildOperations = function(postData) { + var flags = _.pick(postData, 'pap', 'tlp', 'severity'); + + // Handle updates without tag changes + if(!postData.addTags && !postData.removeTags) { + return [ + { + ids: _.pluck(this.selection, '_id'), + patch: flags + } + ]; + } + + // Handle update with tag changes + var input = this.categorizeObservables(); + var operations = []; + if(input.withoutTags.length > 0) { + var tags = (postData.addTags || []).filter(function(i) { + return (postData.removeTags || []).indexOf(i) === -1; + }); + + operations.push({ + ids: _.pluck(input.withoutTags, '_id'), + patch: _.extend({}, flags ,{ + tags: _.unique(tags) + }) + }); + } + + if(input.withTags.length > 0) { + _.each(input.withTags, function(caze) { + tags = caze.tags.concat(postData.addTags || []).filter(function(i) { + return (postData.removeTags || []).indexOf(i) === -1; + }); + + operations.push({ + ids: [caze._id], + patch: _.extend({}, flags ,{ + tags: _.unique(tags) + }) + }); + }); + } + + return operations; + }; + + this.save = function() { + + var postData = {}; + + if(this.state.enableTlp) { + postData.tlp = this.params.tlp; + } + + if(this.state.enablePap) { + postData.pap = this.params.pap; + } + + if(this.state.enableSeverity) { + postData.severity = this.params.severity; + } + + if(this.state.enableAddTags) { + postData.addTags = _.pluck(this.params.addTags, 'text'); + } + + if(this.state.enableRemoveTags) { + postData.removeTags = _.pluck(this.params.removeTags, 'text'); + } + + $uibModalInstance.close(this.buildOperations(postData)); + }; + + this.cancel = function() { + $uibModalInstance.dismiss(); + }; + + this.getTags = function(query) { + return TagSrv.fromCases(query); + }; + + this.toggleTlp = function(value) { + this.params.tlp = value; + this.activeTlp = 'active'; + this.state.enableTlp = true; + }; + + this.togglePap = function(value) { + this.params.pap = value; + this.activePap = 'active'; + this.state.enablePap = true; + }; + + this.toggleSeverity = function(value) { + this.params.severity = value; + this.activeSeverity = true; + this.state.enableSeverity = true; + }; + + this.toggleAddTags = function() { + this.state.enableAddTags = true; + }; + + this.toggleRemoveTags = function() { + this.state.enableRemoveTags = true; + }; + + $scope.$watchCollection('$dialog.params.addTags', function(value) { + self.params.addTagNames = _.pluck(value, 'text').join(','); + }); + + $scope.$watchCollection('$dialog.params.removeTags', function(value) { + self.params.removeTagNames = _.pluck(value, 'text').join(','); + }); + } + ); +})(); diff --git a/frontend/app/scripts/services/api/CaseSrv.js b/frontend/app/scripts/services/api/CaseSrv.js index 8c884b2595..cfaf830261 100644 --- a/frontend/app/scripts/services/api/CaseSrv.js +++ b/frontend/app/scripts/services/api/CaseSrv.js @@ -48,6 +48,10 @@ this.merge = resource.merge; this.query = resource.query; + this.bulkUpdate = function(ids, update) { + return $http.patch('./api/case/_bulk', _.extend({ids: ids}, update)); + }; + this.getShares = function(id) { return $http.get('./api/case/' + id + '/shares'); }; diff --git a/frontend/app/scripts/services/api/TagSrv.js b/frontend/app/scripts/services/api/TagSrv.js index e961ec91d4..9c1a95656e 100644 --- a/frontend/app/scripts/services/api/TagSrv.js +++ b/frontend/app/scripts/services/api/TagSrv.js @@ -23,8 +23,8 @@ // Get the list QuerySrv.call('v0', operations) - .then(function(data) { - defer.resolve(_.map(data, function(tag) { + .then(function(data) { + defer.resolve(_.map(_.unique(data), function(tag) { return {text: tag}; })); }); diff --git a/frontend/app/views/partials/case/case.list.html b/frontend/app/views/partials/case/case.list.html index 1871bf6337..0f6330b532 100644 --- a/frontend/app/views/partials/case/case.list.html +++ b/frontend/app/views/partials/case/case.list.html @@ -38,6 +38,9 @@

List of cases ({{$vm.list.total || 0}} of {{$vm.caseStats. + @@ -52,7 +55,9 @@

List of cases ({{$vm.list.total || 0}} of {{$vm.caseStats.

- +
+ + Title
+ +
diff --git a/frontend/app/views/partials/case/case.update.html b/frontend/app/views/partials/case/case.update.html new file mode 100644 index 0000000000..a36677147d --- /dev/null +++ b/frontend/app/views/partials/case/case.update.html @@ -0,0 +1,75 @@ +
+ + + + + + + +
diff --git a/frontend/app/views/partials/case/list/toolbar.html b/frontend/app/views/partials/case/list/toolbar.html index 786d72a617..62f41b1443 100644 --- a/frontend/app/views/partials/case/list/toolbar.html +++ b/frontend/app/views/partials/case/list/toolbar.html @@ -2,6 +2,18 @@