From 423c028792be105269ccf09d3cd271199ba3babc Mon Sep 17 00:00:00 2001 From: Nabil Adouani Date: Mon, 15 Feb 2021 22:21:58 +0100 Subject: [PATCH] #1766 WIP: Add TTPs section to case details page --- frontend/app/index.html | 4 + frontend/app/scripts/app.js | 12 + .../controllers/case/CaseProceduresCtrl.js | 276 +++++++++++++++++ .../case/procedure/AddProcedureModalCtrl.js | 75 +++++ .../scripts/services/api/AttackPatternSrv.js | 124 +++++++- .../app/scripts/services/api/ProcedureSrv.js | 29 ++ .../app/scripts/services/ui/CaseTabsSrv.js | 6 + frontend/app/styles/case.css | 9 + frontend/app/styles/procedure.css | 80 +++++ frontend/app/views/app.case.html | 5 + .../app/views/directives/updatable-text.html | 2 +- .../views/partials/case/case.procedures.html | 289 ++++++++++++++++++ .../case/procedures/add-procedure.modal.html | 82 +++++ .../partials/case/procedures/filters.html | 39 +++ .../partials/case/procedures/toolbar.html | 23 ++ 15 files changed, 1051 insertions(+), 4 deletions(-) create mode 100644 frontend/app/scripts/controllers/case/CaseProceduresCtrl.js create mode 100644 frontend/app/scripts/controllers/case/procedure/AddProcedureModalCtrl.js create mode 100644 frontend/app/scripts/services/api/ProcedureSrv.js create mode 100644 frontend/app/styles/procedure.css create mode 100644 frontend/app/views/partials/case/case.procedures.html create mode 100644 frontend/app/views/partials/case/procedures/add-procedure.modal.html create mode 100644 frontend/app/views/partials/case/procedures/filters.html create mode 100644 frontend/app/views/partials/case/procedures/toolbar.html diff --git a/frontend/app/index.html b/frontend/app/index.html index 61bbbc08cb..13fb45716f 100644 --- a/frontend/app/index.html +++ b/frontend/app/index.html @@ -50,6 +50,7 @@ + @@ -188,6 +189,7 @@ + @@ -199,6 +201,7 @@ + @@ -301,6 +304,7 @@ + diff --git a/frontend/app/scripts/app.js b/frontend/app/scripts/app.js index 581b5bf4bd..aa39a7c584 100644 --- a/frontend/app/scripts/app.js +++ b/frontend/app/scripts/app.js @@ -513,6 +513,18 @@ angular.module('thehive', [ isSuperAdmin: false } }) + .state('app.case.procedures', { + url: '/procedures', + templateUrl: 'views/partials/case/case.procedures.html', + controller: 'CaseProceduresCtrl', + controllerAs: '$vm', + data: { + tab: 'procedures' + }, + guard: { + isSuperAdmin: false + } + }) .state('app.alert-list', { url: 'alert/list', templateUrl: 'views/partials/alert/list.html', diff --git a/frontend/app/scripts/controllers/case/CaseProceduresCtrl.js b/frontend/app/scripts/controllers/case/CaseProceduresCtrl.js new file mode 100644 index 0000000000..9aa5021db4 --- /dev/null +++ b/frontend/app/scripts/controllers/case/CaseProceduresCtrl.js @@ -0,0 +1,276 @@ +(function() { + 'use strict'; + angular.module('theHiveControllers') + .controller('CaseProceduresCtrl', CaseProceduresCtrl); + + function CaseProceduresCtrl($scope, $state, $stateParams, $uibModal, ModalUtilsSrv, AttackPatternSrv, FilteringSrv, CaseTabsSrv, ProcedureSrv, PaginatedQuerySrv, NotificationSrv, AppLayoutSrv) { + var self = this; + + CaseTabsSrv.activateTab($state.current.data.tab); + + this.caseId = $stateParams.caseId; + this.tactics = AttackPatternSrv.tactics.values; + + this.$onInit = function() { + self.filtering = new FilteringSrv('procedure', 'procedure.list', { + version: 'v1', + defaults: { + showFilters: true, + showStats: false, + pageSize: 15, + sort: ['-occurDate'], + }, + defaultFilter: [] + }); + + self.filtering.initContext(self.caseId) + .then(function() { + self.load(); + + $scope.$watchCollection('$vm.list.pageSize', function (newValue) { + self.filtering.setPageSize(newValue); + }); + }); + }; + + this.addProcedure = function() { + var modalInstance = $uibModal.open({ + animation: true, + // keyboard: false, + // backdrop: 'static', + templateUrl: 'views/partials/case/procedures/add-procedure.modal.html', + controller: 'AddProcedureModalCtrl', + controllerAs: '$modal', + size: 'lg', + resolve: { + caseId: function() { + return self.caseId; + } + } + }); + + return modalInstance.result + .then(function() { + self.load(); + }); + }; + + this.updateDescription = function(procedure) { + ProcedureSrv.update(procedure._id, { + description: procedure.description + }).then(function(response) { + console.log(response); + }).catch(function(err) { + NotificationSrv.error('ProcedureCtrl', err.data, err.status); + }); + }; + + // $scope.state = { + // isNewTask: false, + // showGrouped: !!AppLayoutSrv.layout.groupTasks + // }; + // $scope.newTask = { + // status: 'Waiting' + // }; + // $scope.taskResponders = null; + // $scope.collapseOptions = {}; + + // $scope.getAssignableUsers = function(taskId) { + // return [ + // {_name: 'getTask', idOrName: taskId}, + // {_name: 'assignableUsers'} + // ]; + // }; + + this.load = function() { + self.list = new PaginatedQuerySrv({ + name: 'case-procedures', + root: self.caseId, + // objectType: 'case_task', + version: 'v1', + scope: $scope, + sort: self.filtering.context.sort, + loadAll: false, + pageSize: self.filtering.context.pageSize, + filter: self.filtering.buildQuery(), + operations: [ + {'_name': 'getCase', "idOrName": self.caseId}, + {'_name': 'procedures'} + ], + extraData: ['pattern'] + }); + }; + + self.showPattern = function(patternId) { + $uibModal.open({ + animation: true, + templateUrl: 'views/partials/admin/attack/view.html', + controller: 'AttackPatternDialogCtrl', + controllerAs: '$modal', + size: 'max', + resolve: { + pattern: function() { + return AttackPatternSrv.get(patternId); + } + } + }); + }; + + this.toggleFilters = function () { + this.filtering.toggleFilters(); + }; + + + this.filter = function () { + self.filtering.filter().then(this.applyFilters); + }; + + this.clearFilters = function () { + this.filtering.clearFilters() + .then(self.search); + }; + + this.addFilter = function (field, value) { + self.filtering.addFilter(field, value).then(this.applyFilters); + }; + + this.removeFilter = function (index) { + self.filtering.removeFilter(index) + .then(self.search); + }; + + this.search = function () { + self.load(); + self.filtering.storeContext(); + }; + + this.addFilterValue = function (field, value) { + this.filtering.addFilterValue(field, value); + this.search(); + }; + + self.filterBy = function(field, value) { + self.filtering.clearFilters() + .then(function() { + self.addFilterValue(field, value); + }); + }; + + self.sortByField = function(field) { + var context = this.filtering.context; + var currentSort = Array.isArray(context.sort) ? context.sort[0] : context.sort; + var sort = null; + + if(currentSort.substr(1) !== field) { + sort = ['+' + field]; + } else { + sort = [(currentSort === '+' + field) ? '-'+field : '+'+field]; + } + + self.list.sort = sort; + self.list.update(); + self.filtering.setSort(sort); + }; + + // $scope.filterMyTasks = function() { + // $scope.filtering.clearFilters() + // .then(function() { + // var currentUser = AuthenticationSrv.currentUser; + // $scope.filtering.addFilter({ + // field: 'assignee', + // type: 'user', + // value: { + // list: [{ + // text: currentUser.login, + // label: currentUser.name + // }] + // } + // }); + // $scope.search(); + // }); + // }; + + // $scope.toggleGroupedView = function() { + // $scope.state.showGrouped = !$scope.state.showGrouped; + // + // AppLayoutSrv.groupTasks($scope.state.showGrouped); + // }; + + // $scope.buildTaskGroups = function(tasks) { + // // Sort tasks by order + // var orderedTasks = _.sortBy(_.map(tasks, function(t) { + // return _.pick(t, 'group', 'order'); + // }), 'order'); + // var groups = []; + // + // // Get group names by keeping the group orders + // _.each(orderedTasks, function(task) { + // if(groups.indexOf(task.group) === -1) { + // groups.push(task.group); + // } + // }); + // + // var groupedTasks = []; + // _.each(groups, function(group) { + // groupedTasks.push({ + // group: group, + // tasks: _.filter(tasks, function(t) { + // return t.group === group; + // }) + // }); + // }); + // + // $scope.groups = groups; + // $scope.groupedTasks = groupedTasks; + // }; + + // $scope.showTask = function(taskId) { + // $state.go('app.case.tasks-item', { + // itemId: taskId + // }); + // }; + + // $scope.updateField = function (fieldName, newValue, task) { + // var field = {}; + // field[fieldName] = newValue; + // return CaseTaskSrv.update({ + // taskId: task._id + // }, field, function () {}, function (response) { + // NotificationSrv.error('taskList', response.data, response.status); + // }); + // }; + + // $scope.addTask = function() { + // CaseTaskSrv.save({ + // 'caseId': $scope.caseId, + // 'flag': false + // }, $scope.newTask, function() { + // $scope.isNewTask = false; + // $scope.newTask.title = ''; + // $scope.newTask.group = ''; + // NotificationSrv.success('Task has been successfully added'); + // }, function(response) { + // NotificationSrv.error('taskList', response.data, response.status); + // }); + // }; + // + // $scope.removeTask = function(task) { + // + // ModalUtilsSrv.confirm('Delete task', 'Are you sure you want to delete the selected task?', { + // okText: 'Yes, remove it', + // flavor: 'danger' + // }).then(function() { + // CaseTaskSrv.update({ + // 'taskId': task._id + // }, { + // status: 'Cancel' + // }, function() { + // $scope.$emit('tasks:task-removed', task); + // NotificationSrv.success('Task has been successfully removed'); + // }, function(response) { + // NotificationSrv.error('taskList', response.data, response.status); + // }); + // }); + // }; + } +}()); diff --git a/frontend/app/scripts/controllers/case/procedure/AddProcedureModalCtrl.js b/frontend/app/scripts/controllers/case/procedure/AddProcedureModalCtrl.js new file mode 100644 index 0000000000..a4a90a56ba --- /dev/null +++ b/frontend/app/scripts/controllers/case/procedure/AddProcedureModalCtrl.js @@ -0,0 +1,75 @@ +/** + * Controller for About TheHive modal page + */ +(function() { + 'use strict'; + + angular.module('theHiveControllers').controller('AddProcedureModalCtrl', function($rootScope, $scope, $uibModalInstance, NotificationSrv, ProcedureSrv, AttackPatternSrv, caseId) { + var self = this; + + this.caseId = caseId; + + this.close = function() { + $uibModalInstance.close(); + }; + + this.cancel = function() { + $rootScope.markdownEditorObjects.procedure.hidePreview(); + + $uibModalInstance.dismiss(); + }; + + this.addProcedure = function() { + self.state.loading = true; + + ProcedureSrv.create({ + caseId: self.caseId, + tactic: self.procedure.tactic, + description: self.procedure.description, + patternId: self.procedure.patternId, + occurDate: self.procedure.occurDate + }).then(function(/*response*/) { + self.state.loading = false; + $uibModalInstance.close(); + NotificationSrv.log('Tactic, Technique and Procedure added successfully', 'success'); + }).catch(function(err) { + NotificationSrv.error('Add TTP', err.data, err.status); + self.state.loading = false; + }); + }; + + this.showTechniques = function() { + AttackPatternSrv.getByTactic(self.procedure.tactic) + .then(function(techniques) { + self.state.techniques = techniques; + + self.procedure.patternId = null; + }); + }; + + this.$onInit = function() { + this.markdownEditorOptions = { + iconlibrary: 'fa', + addExtraButtons: true, + resize: 'vertical' + }; + + this.procedure = { + tactic: null, + description: null, + patternId: null + }; + + this.tactics = AttackPatternSrv.tactics; + + this.state = { + loading: false, + selectedTactic: null, + techniques: null + }; + + $scope.$broadcast('beforeProcedureModalShow'); + }; + } + ); +})(); diff --git a/frontend/app/scripts/services/api/AttackPatternSrv.js b/frontend/app/scripts/services/api/AttackPatternSrv.js index 138e91ac25..53fc54ccd3 100644 --- a/frontend/app/scripts/services/api/AttackPatternSrv.js +++ b/frontend/app/scripts/services/api/AttackPatternSrv.js @@ -3,8 +3,74 @@ 'use strict'; angular.module('theHiveServices') .service('AttackPatternSrv', function($http, $q, QuerySrv) { + var self = this; var baseUrl = './api/v1/pattern'; + this.tacticsCache = {}; + + this.tactics = { + keys: [ + 'reconnaissance', + 'resource-development', + 'initial-access', + 'execution', + 'persistence', + 'privilege-escalation', + 'defense-evasion', + 'credential-access', + 'discovery', + 'lateral-movement', + 'collection', + 'command-and-control', + 'exfiltration', + 'impact' + ], + values: { + 'reconnaissance': { + label: 'Reconnaissance' + }, + 'resource-development': { + label: 'Resource Development' + }, + 'initial-access': { + label: 'Initial Access' + }, + 'execution': { + label: 'Execution' + }, + 'persistence': { + label: 'Persistence' + }, + 'privilege-escalation': { + label: 'Privilege Escalation' + }, + 'defense-evasion': { + label: 'Defense Evasion' + }, + 'credential-access': { + label: 'Credential Access' + }, + 'discovery': { + label: 'Discovery' + }, + 'lateral-movement': { + label: 'Lateral Movement' + }, + 'collection': { + label: 'Collection' + }, + 'command-and-control': { + label: 'Command and Control' + }, + 'exfiltration': { + label: 'Exfiltration' + }, + 'impact': { + label: 'Impact' + } + } + }; + this.list = function() { return QuerySrv.call('v1', [ { _name: 'listPattern' } @@ -13,6 +79,58 @@ }); }; + this.getByTactic = function(tactic) { + var defer = $q.defer(); + + if(self.tacticsCache[tactic]) { + console.log('get techniques from cache for ', tactic); + + defer.resolve(self.tacticsCache[tactic]); + } else { + console.log('get techniques from server for ', tactic); + + QuerySrv.call('v1', [ + { _name: 'listPattern'} + ], { + name:'list-attack-patterns-for-' + tactic, + filter: { + _and: [ + { + _field: 'patternType', + _value: 'attack-pattern' + }, { + _like: { + _field: 'tactics', + _value: tactic + } + }, + { + _field: 'revoked', + _value: false + }, + { + _not: { + _contains: 'parent' + } + } + ] + }, + sort: ['+patternId'], + page: { + from: 0, + to: 100, + extraData: ['children'] + } + }).then(function(techniques) { + self.tacticsCache[tactic] = techniques; + + defer.resolve(techniques); + }); + } + + return defer.promise; + }; + this.get = function(id) { var defer = $q.defer(); @@ -26,8 +144,8 @@ from: 0, to: 1, extraData: [ - "parent", - "children" + 'parent', + 'children' ] } }).then(function(response) { @@ -36,7 +154,7 @@ defer.reject(err); }); - return defer.promise; + return defer.promise; }; this.import = function(post) { diff --git a/frontend/app/scripts/services/api/ProcedureSrv.js b/frontend/app/scripts/services/api/ProcedureSrv.js new file mode 100644 index 0000000000..7ee6d90ea6 --- /dev/null +++ b/frontend/app/scripts/services/api/ProcedureSrv.js @@ -0,0 +1,29 @@ +(function() { + 'use strict'; + angular.module('theHiveServices') + .service('ProcedureSrv', function($q, $http) { + var self = this; + var baseUrl = './api/v1/procedure'; + + self.get = function(procedureId) { + return $http.get(baseUrl + '/' + procedureId) + .then(function(response) { + return $q.resolve(response.data); + }); + }; + + self.create = function(data) { + return $http.post(baseUrl, data || {}); + }; + + self.update = function(procedureId, updates) { + return $http.patch(baseUrl + '/' + procedureId, updates); + }; + + self.remove = function(procedureId, updates) { + return $http.delete(baseUrl + '/' + procedureId, updates); + }; + + }); + +})(); diff --git a/frontend/app/scripts/services/ui/CaseTabsSrv.js b/frontend/app/scripts/services/ui/CaseTabsSrv.js index 760537831f..456a687447 100644 --- a/frontend/app/scripts/services/ui/CaseTabsSrv.js +++ b/frontend/app/scripts/services/ui/CaseTabsSrv.js @@ -20,6 +20,12 @@ active: false, label: 'Observables', state: 'app.case.observables' + }, + 'procedures': { + name: 'procedures', + active: false, + label: 'TTPs', + state: 'app.case.procedures' } }; diff --git a/frontend/app/styles/case.css b/frontend/app/styles/case.css index 839531f7eb..e552d0d14f 100644 --- a/frontend/app/styles/case.css +++ b/frontend/app/styles/case.css @@ -95,3 +95,12 @@ pre.error-trace { white-space: pre-wrap; background-color: #f9f1f1; } + +.procedure-techniques-list { + height: 200px; + overflow-y: scroll; + border-radius: 0; + box-shadow: none; + border: 1px solid #d2d6de; + +} diff --git a/frontend/app/styles/procedure.css b/frontend/app/styles/procedure.css new file mode 100644 index 0000000000..0830b99657 --- /dev/null +++ b/frontend/app/styles/procedure.css @@ -0,0 +1,80 @@ +.ttp-item { + margin-bottom: 10px; + border: 1px solid #f5f5f5; +} + +.ttp-tactic { + width: 200px; + vertical-align: middle; +} +.ttp-name { + flex: 1; +} +.ttp-action { + width: 100px; + text-align: right +} +.ttp-date { + width: 140px; +} +.ttp-user { + width: 200px; +} + +.ttp-item .ttp-header { + display: flex; + align-items: stretch; + justify-content: space-between; + flex-direction: row; + border-bottom: 1px solid #f5f5f5; + background-color: #fcfcfc; +} +.ttp-item .ttp-header > div { + padding: 10px; + vertical-align: middle; +} + +.ttp-item .ttp-header .ttp-tactic { + background-color: #f5f5f5; + border-left: 4px solid #337ab7; + color: #337ab7; + display: flex; + align-items: center; +} +.ttp-item .ttp-header .ttp-tactic > div { + flex: 1 +} +.ttp-item .ttp-header .ttp-name { + flex: 1; + display: flex; + align-items: center; +} +.ttp-item .ttp-header .ttp-name > div { + flex: 1 +} +.ttp-item .ttp-header .ttp-date { + display: flex; + align-items: center; +} +.ttp-item .ttp-header .ttp-actions { + display: flex; + align-items: center; +} + +.ttp-item .ttp-body { + padding: 10px; +} + +.ttp-item-header { + display: flex; + align-items: center; + justify-content: space-between; + flex-direction: row; + margin-bottom: 10px; + border-bottom: 1px solid #f5f5f5; +} + +.ttp-item-header > div { + font-weight: bold; + padding: 10px; +} diff --git a/frontend/app/views/app.case.html b/frontend/app/views/app.case.html index 4a14ff4051..e548b9ce1f 100644 --- a/frontend/app/views/app.case.html +++ b/frontend/app/views/app.case.html @@ -23,6 +23,11 @@ {{observableCount}} + +    TTPs   + + + diff --git a/frontend/app/views/directives/updatable-text.html b/frontend/app/views/directives/updatable-text.html index ef2ec25a26..634725c570 100644 --- a/frontend/app/views/directives/updatable-text.html +++ b/frontend/app/views/directives/updatable-text.html @@ -10,7 +10,7 @@ - + diff --git a/frontend/app/views/partials/case/case.procedures.html b/frontend/app/views/partials/case/case.procedures.html new file mode 100644 index 0000000000..aabe59a4b5 --- /dev/null +++ b/frontend/app/views/partials/case/case.procedures.html @@ -0,0 +1,289 @@ +
+ +
+ + +
+
+
+

+ Tactics, Techniques and Procedures ({{$vm.list.values.length || 0}} of {{$vm.list.total}}) +

+
+ + +
+
+ + +
+
+
No records
+
+ +
+ + +
+ + +
Created By
+ +
Actions
+
+ +
+
+
+
+ + + + + {{$vm.tactics[proc.tactic].label}} + + +
+
+
+
+ {{proc.patternId}} - {{proc.extraData.parent}}:{{proc.extraData.pattern.name}} + + +
+
+
+
+ {{proc.patternId}} - {{proc.extraData.pattern.name}} + + +
+
+ +
+ +
+ +
+ + + + + + + + + + + +
+
+
+ +
+ +
+
+
+
+
+
+ + + + + +
+ + +
diff --git a/frontend/app/views/partials/case/procedures/add-procedure.modal.html b/frontend/app/views/partials/case/procedures/add-procedure.modal.html new file mode 100644 index 0000000000..0921114a21 --- /dev/null +++ b/frontend/app/views/partials/case/procedures/add-procedure.modal.html @@ -0,0 +1,82 @@ +
+ + + +
diff --git a/frontend/app/views/partials/case/procedures/filters.html b/frontend/app/views/partials/case/procedures/filters.html new file mode 100644 index 0000000000..12c948d197 --- /dev/null +++ b/frontend/app/views/partials/case/procedures/filters.html @@ -0,0 +1,39 @@ +
+
+

Filters

+ +
+
+
+
+ + + + +
+
+
+ +
+
+
+ +
+
+ +
+
diff --git a/frontend/app/views/partials/case/procedures/toolbar.html b/frontend/app/views/partials/case/procedures/toolbar.html new file mode 100644 index 0000000000..0e0bf88fa9 --- /dev/null +++ b/frontend/app/views/partials/case/procedures/toolbar.html @@ -0,0 +1,23 @@ +
+
+ +
+