diff --git a/ui/app/index.html b/ui/app/index.html index d67b2fc8e9..11e036c760 100644 --- a/ui/app/index.html +++ b/ui/app/index.html @@ -28,7 +28,7 @@ - + @@ -38,6 +38,7 @@ +
@@ -90,8 +91,8 @@ - - + + @@ -123,6 +124,7 @@ + @@ -188,6 +190,7 @@ + @@ -205,6 +208,7 @@ + diff --git a/ui/app/scripts/app.js b/ui/app/scripts/app.js index 048a7577cf..e7e0a3a3b3 100644 --- a/ui/app/scripts/app.js +++ b/ui/app/scripts/app.js @@ -21,7 +21,7 @@ angular.module('thehive', ['ngAnimate', 'ngMessages', 'ui.bootstrap', 'ui.router .config(function($stateProvider, $urlRouterProvider) { 'use strict'; - $urlRouterProvider.otherwise('/main/'); + $urlRouterProvider.otherwise('/cases'); $stateProvider .state('login', { @@ -61,6 +61,13 @@ angular.module('thehive', ['ngAnimate', 'ngMessages', 'ui.bootstrap', 'ui.router templateUrl: 'views/app.main.html', controller: 'MainPageCtrl' }) + .state('app.cases', { + url: 'cases', + templateUrl: 'views/partials/case/case.list.html', + controller: 'CaseListCtrl', + controllerAs: '$vm', + title: 'Cases' + }) .state('app.search', { url: 'search?q', templateUrl: 'views/partials/search/list.html', @@ -125,9 +132,7 @@ angular.module('thehive', ['ngAnimate', 'ngMessages', 'ui.bootstrap', 'ui.router controller: 'AdminObservablesCtrl', title: 'Observable administration' }) - - - .state('app.case', { + .state('app.case', { abstract: true, url: 'case/{caseId}', templateUrl: 'views/app.case.html', diff --git a/ui/app/scripts/controllers/AuthenticationCtrl.js b/ui/app/scripts/controllers/AuthenticationCtrl.js index 7cb4129aa4..142930c379 100644 --- a/ui/app/scripts/controllers/AuthenticationCtrl.js +++ b/ui/app/scripts/controllers/AuthenticationCtrl.js @@ -12,7 +12,7 @@ $scope.login = function() { $scope.params.username = angular.lowercase($scope.params.username); AuthenticationSrv.login($scope.params.username, $scope.params.password, function() { - $state.go('app.main'); + $state.go('app.cases'); }, function(data, status) { if (status === 520) { AlertSrv.error('AuthenticationCtrl', data, status); diff --git a/ui/app/scripts/controllers/MigrationCtrl.js b/ui/app/scripts/controllers/MigrationCtrl.js index 62994bbbc5..8f6e17b81d 100644 --- a/ui/app/scripts/controllers/MigrationCtrl.js +++ b/ui/app/scripts/controllers/MigrationCtrl.js @@ -19,10 +19,10 @@ if (users.length === 0) { $scope.showUserForm = true; } else { - $state.go('app.main'); + $state.go('app.cases'); } }, function() { - $state.go('app.main'); + $state.go('app.cases'); }); } var current = 0; @@ -58,7 +58,7 @@ 'password': $scope.newUser.password, 'roles': ['read', 'write', 'admin'] }, function() { - $state.go('app.main'); + $state.go('app.cases'); }); }; } diff --git a/ui/app/scripts/controllers/SettingsCtrl.js b/ui/app/scripts/controllers/SettingsCtrl.js index a5458b6f9f..1492d46fb1 100644 --- a/ui/app/scripts/controllers/SettingsCtrl.js +++ b/ui/app/scripts/controllers/SettingsCtrl.js @@ -63,7 +63,7 @@ AlertSrv.error('SettingsCtrl', response.data, response.status); }); } else { - $state.go('app.main'); + $state.go('app.cases'); } }; @@ -82,7 +82,7 @@ }; $scope.cancel = function() { - $state.go('app.main'); + $state.go('app.cases'); }; $scope.clearAvatar = function(form) { diff --git a/ui/app/scripts/controllers/case/CaseListCtrl.js b/ui/app/scripts/controllers/case/CaseListCtrl.js new file mode 100644 index 0000000000..939e997169 --- /dev/null +++ b/ui/app/scripts/controllers/case/CaseListCtrl.js @@ -0,0 +1,151 @@ +(function() { + 'use strict'; + angular.module('theHiveControllers') + .controller('CaseListCtrl', CaseListCtrl); + + function CaseListCtrl($scope, $q, CasesUISrv, StreamStatSrv, PSearchSrv, EntitySrv, UserInfoSrv, TagSrv) { + var self = this; + + this.showFlow = true; + this.openEntity = EntitySrv.open; + this.getUserInfo = UserInfoSrv; + + this.uiSrv = CasesUISrv; + this.uiSrv.initContext('list'); + this.searchForm = { + searchQuery: this.uiSrv.buildQuery() || '' + }; + + this.list = PSearchSrv(undefined, 'case', { + filter: self.searchForm.searchQuery !== '' ? { + _string: self.searchForm.searchQuery + } : '', + loadAll: false, + sort: self.uiSrv.context.sort, + pageSize: self.uiSrv.context.pageSize, + nstats: true + }); + + this.caseStats = StreamStatSrv({ + rootId: 'any', + query: {}, + result: {}, + objectType: 'case', + field: 'status' + }); + + + $scope.$watch('$vm.list.pageSize', function (newValue) { + self.uiSrv.setPageSize(newValue); + }); + + this.toggleStats = function () { + this.uiSrv.toggleStats(); + }; + + this.toggleFilters = function () { + this.uiSrv.toggleFilters(); + }; + + this.filter = function () { + this.uiSrv.filter().then(this.applyFilters); + }; + + this.applyFilters = function () { + self.searchForm.searchQuery = self.uiSrv.buildQuery(); + self.search(); + }; + + this.clearFilters = function () { + this.uiSrv.clearFilters().then(this.applyFilters); + }; + + this.addFilter = function (field, value) { + this.uiSrv.addFilter(field, value).then(this.applyFilters); + }; + + this.removeFilter = function (field) { + this.uiSrv.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 = this.uiSrv.filterDefs[field]; + var filter = this.uiSrv.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, ['YYYYMMDDTHHmmZZ', 'DD-MM-YYYY HH:mm']); + this.uiSrv.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') { + this.uiSrv.activeFilters[field] = { + value: [{ + text: value + }] + }; + } else if (filterDef.type === 'date') { + date = moment(value, ['YYYYMMDDTHHmmZZ', 'DD-MM-YYYY HH:mm']); + this.uiSrv.activeFilters[field] = { + value: { + from: date.hour(0).minutes(0).seconds(0).toDate(), + to: date.hour(23).minutes(59).seconds(59).toDate() + } + }; + } else { + this.uiSrv.activeFilters[field] = { + value: value + }; + } + } + + this.filter(); + }; + + this.getStatuses = function() { + return $q.resolve([ + {text: 'Open'}, + {text: 'Resolved'} + ]); + }; + + this.getTags = function(query) { + return TagSrv.fromCases(query); + }; + + this.filterByStatus = function(status) { + this.uiSrv.clearFilters() + .then(function(){ + self.addFilterValue('status', status); + }); + }; + + this.sortBy = function(sort) { + this.list.sort = sort; + this.list.update(); + this.uiSrv.setSort(sort); + }; + + } +})(); diff --git a/ui/app/scripts/controllers/case/CaseMainCtrl.js b/ui/app/scripts/controllers/case/CaseMainCtrl.js index 1da68c6a65..55260cc704 100644 --- a/ui/app/scripts/controllers/case/CaseMainCtrl.js +++ b/ui/app/scripts/controllers/case/CaseMainCtrl.js @@ -115,7 +115,7 @@ if(switchToDetails) { $scope.openTab('details'); - } + } }; $scope.switchFlag = function() { @@ -168,7 +168,7 @@ }); modalInstance.result.then(function() { - $state.go('app.main', {viewId: 'currentcases'}); + $state.go('app.cases'); }); }; diff --git a/ui/app/scripts/directives/user.js b/ui/app/scripts/directives/user.js index bc1423b6e4..0737e41a9e 100644 --- a/ui/app/scripts/directives/user.js +++ b/ui/app/scripts/directives/user.js @@ -5,16 +5,31 @@ return { scope: { user: '=userId', - iconOnly: '@', + iconOnly: '=', iconSize: '@' }, templateUrl: 'views/directives/user.html', - link: function(scope) { + link: function(scope) { scope.userInfo = UserInfoSrv; - scope.userData = {}; + scope.initials = ''; + + scope.$watch('userData.name', function(value) { + if(!value) { + return; + } + + scope.initials = value.split(' ') + .map(function(item) { + return item[0]; + }) + .join('') + .substr(0, 3) + .toUpperCase(); + }); scope.$watch('user', function(value) { scope.userData = scope.userInfo.get(value); + }); } }; diff --git a/ui/app/scripts/services/CasesUISrv.js b/ui/app/scripts/services/CasesUISrv.js new file mode 100644 index 0000000000..af6bf89b33 --- /dev/null +++ b/ui/app/scripts/services/CasesUISrv.js @@ -0,0 +1,248 @@ +(function() { + 'use strict'; + angular.module('theHiveServices') + .factory('CasesUISrv', function($q, localStorageService) { + + var factory = { + filterDefs: { + keyword: { + field: 'keyword', + type: 'string', + defaultValue: [] + }, + data: { + field: 'data', + type: 'string', + defaultValue: '' + }, + status: { + field: 'status', + type: 'list', + defaultValue: [] + }, + tags: { + field: 'tags', + type: 'list', + defaultValue: [] + }, + tlp: { + field: 'tlp', + type: 'number', + defaultValue: null + }, + title: { + field: 'title', + type: 'string', + defaultValue: '' + }, + startDate: { + field: 'startDate', + type: 'date', + defaultValue: { + from: null, + to: null + } + } + }, + + filters: {}, + activeFilters: {}, + context: { + state: null, + showFilters: false, + showStats: false, + pageSize: 15 + }, + currentState: null, + currentPageSize: null, + + initContext: function(state) { + if (!factory.context.state) { + var storedContext = localStorageService.get('cases-section'); + + if (storedContext && storedContext.state && storedContext.state === state) { + factory.context = storedContext; + factory.filters = storedContext.filters || {}; + factory.activeFilters = storedContext.activeFilters || {}; + return; + } + } + + if (state !== factory.context.state) { + factory.context = { + state: state, + showFilters: false, + showStats: false, + pageSize: 15 + }; + + factory.filters = {}; + factory.activeFilters = {}; + + factory.storeContext(); + } + }, + + initFilters: function() { + factory.activeFilters = {}; + _.each(_.keys(factory.filterDefs), function(key) { + var def = factory.filterDefs[key]; + factory.activeFilters[key] = { + field: def.field, + type: def.type, + value: factory.hasFilter(key) ? angular.copy(factory.getFilterValue(key)) : angular.copy(def.defaultValue) + }; + }); + }, + + isEmpty: function(value) { + return value === undefined || value === null || value.length === 0 || (angular.isObject(value) &&_.without(_.values(value), null, undefined, '').length === 0); + }, + + clearFilters: function() { + factory.filters = {}; + factory.activeFilters = {}; + factory.storeContext(); + return $q.resolve({}); + }, + + filter: function() { + var activeFilters = factory.activeFilters; + + _.each(activeFilters, function(filterValue, field /*, filters*/ ) { + var value = filterValue.value; + + if (!factory.isEmpty(value)) { + factory.addFilter(field, angular.copy(value)); + } else { + factory.removeFilter(field); + } + }); + + factory.storeContext(); + + return $q.resolve(); + }, + + addFilter: function(field, value) { + var query, + filterDef = factory.filterDefs[field]; + + // Prepare the filter value + /* + if(factory.hasFilter(field)) { + var oldValue = factory.getFilterValue(field); + console.log('Filter ['+field+'] already exists = ' + oldValue); + + if(factory.filterDefs[field].type === 'list') { + value = angular.isArray(oldValue) ? oldValue.push({text: value}) : [{text: oldValue}, {text: 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; + } + + factory.filters[field] = { + field: field, + value: value, + filter: query + }; + + return $q.resolve(factory.filters); + }, + + removeFilter: function(field) { + var filter = factory.activeFilters[field]; + + if(angular.isObject(filter.value)) { + _.each(filter.value, function(value, key) { + filter.value[key] = null; + }); + } + + delete factory.filters[field]; + delete factory.activeFilters[field]; + + factory.storeContext(); + + return $q.resolve(factory.filters); + }, + + hasFilter: function(field) { + return factory.filters[field]; + }, + + hasFilters: function() { + return _.keys(factory.filters).length > 0; + }, + + countFilters: function() { + return _.keys(factory.filters).length; + }, + countSorts: function() { + return factory.context.sort.length; + }, + + getFilterValue: function(field) { + if (factory.filters[field]) { + return factory.filters[field].value; + } + }, + + buildQuery: function() { + if (_.keys(factory.filters).length === 0) { + return; + } + + _.keys(factory.filters).map(function(key) { + return factory.filters[key].filter; + }).join(' AND '); + + return _.pluck(factory.filters, 'filter').join(' AND '); + }, + + toggleStats: function() { + factory.context.showStats = !factory.context.showStats; + factory.storeContext(); + }, + + toggleFilters: function() { + factory.context.showFilters = !factory.context.showFilters; + factory.storeContext(); + }, + + setPageSize: function(pageSize) { + factory.context.pageSize = pageSize; + factory.storeContext(); + }, + + setSort: function(sorts) { + factory.context.sort = sorts; + factory.storeContext(); + }, + + storeContext: function() { + factory.context.filters = factory.filters; + factory.context.activeFilters = factory.activeFilters; + localStorageService.set('cases-section', factory.context); + } + }; + + return factory; + }); +})(); diff --git a/ui/app/scripts/services/TagSrv.js b/ui/app/scripts/services/TagSrv.js new file mode 100644 index 0000000000..fe3bd50995 --- /dev/null +++ b/ui/app/scripts/services/TagSrv.js @@ -0,0 +1,30 @@ +(function() { + 'use strict'; + angular.module('theHiveServices') + .service('TagSrv', function(StatSrv, $q) { + + this.fromCases = function(query) { + var defer = $q.defer(); + + StatSrv.getPromise({ + objectType: 'case', + field: 'tags', + limit: 1000 + }).then(function(response) { + var tags = []; + var regex = new RegExp(query); + + tags = _.map(_.filter(_.keys(response.data), function(tag) { + return regex.test(tag); + }), function(tag) { + return {text: tag}; + }); + + defer.resolve(tags); + }); + + return defer.promise; + }; + + }); +})(); diff --git a/ui/app/scripts/services/UserSrv.js b/ui/app/scripts/services/UserSrv.js index f881364fd4..69c78bbbda 100644 --- a/ui/app/scripts/services/UserSrv.js +++ b/ui/app/scripts/services/UserSrv.js @@ -35,7 +35,7 @@ angular.module('theHiveServices') } else { var ret = { 'name': login, - 'id': login + 'id': login }; res.get({ 'userId': login diff --git a/ui/app/styles/directives/user.css b/ui/app/styles/directives/user.css new file mode 100644 index 0000000000..d1a9363654 --- /dev/null +++ b/ui/app/styles/directives/user.css @@ -0,0 +1,47 @@ +.avatar .avatar-icon { + border-radius: 50px; +} + +.avatar .avatar-name { + margin-left: 8px; +} + +.avatar .avatar-icon { + position: absolute; +} + +.avatar div.avatar-icon { + background-color: #ccc; + font-weight: bold; + float: left; + text-align: center; + color: #000; +} + +.avatar.avatar-xs { + line-height: 30px; +} + +.avatar.avatar-m { + line-height: 40px; +} + +.avatar.avatar-xs .avatar-icon { + width: 30px; + height: 30px; + line-height: 30px; +} + +.avatar.avatar-m .avatar-icon { + width: 40px; + height: 40px; + line-height: 40px; +} + +.avatar.avatar-xs .avatar-name { + margin-left: 35px; +} + +.avatar.avatar-m .avatar-name { + margin-left:45px; +} diff --git a/ui/app/styles/main.css b/ui/app/styles/main.css index 8a5eb9a16b..ff9fa8fa6c 100644 --- a/ui/app/styles/main.css +++ b/ui/app/styles/main.css @@ -115,6 +115,10 @@ pre.clearpre { margin-top: 40px; background-color: #eee; } +.flexwrap{ + display: flex; + flex-wrap: wrap; +} .wrap { word-wrap: break-word; word-break: break-all; @@ -218,6 +222,13 @@ dl.dl-horizontal > dt.pull-left { dl.dl-horizontal.clear { overflow: hidden !important; } +dl.dl-horizontal > dt, +dl.dl-horizontal > dd { + line-height: 28px; +} +dl.dl-horizontal { + margin-bottom: 10px; +} ul.observable-reports-summary li { padding-bottom: 3px; padding-left: 2px !important; @@ -425,24 +436,6 @@ td.vmiddle { } } -.avatar .avatar-icon { - border-radius: 50px; -} - -.avatar .avatar-name { - margin-left: 5px; -} - -.avatar.avatar-xs .avatar-icon { - width: 30px; - height: 30px; -} - -.avatar.avatar-m .avatar-icon { - width: 40px; - height: 40px; -} - table.valigned td { vertical-align: middle !important; } diff --git a/ui/app/views/app.html b/ui/app/views/app.html index 60ec2041cd..cee14e5597 100644 --- a/ui/app/views/app.html +++ b/ui/app/views/app.html @@ -8,37 +8,16 @@ - +The case summary is required.
+ | Title | +Tags | +Tasks | +Observables | +Assignee | +Date | +
---|---|---|---|---|---|---|
+ |
+ #{{currentCase.caseId}}
+ -
+ {{currentCase.title}}
+
+
+ Merged from Case #{{currentCase.stats.mergeFrom[0].caseId}} and
+ Case #{{currentCase.stats.mergeFrom[1].caseId}}
+
+
+ |
+ + + + + | +
+
+ + |
+ + {{currentCase.stats.artifacts.count}} + | +
+ |
+ {{currentCase.startDate | showDate}} | +
{{item.key}} | ++ {{item.count}} + | +
{{(item.key === '0') ? 'Not IOC' : 'IOC' }} | ++ {{item.count}} + | +
{{item.key}} | ++ {{item.count}} + | +