diff --git a/CHANGELOG.md b/CHANGELOG.md index 840666dfb1..8a2f869586 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,23 @@ # Change Log -## [3.3.0-RC5](https://github.com/TheHive-Project/TheHive/tree/HEAD) (2019-02-24) +## [3.3.0-RC6](https://github.com/TheHive-Project/TheHive/tree/3.3.0-RC6) (2019-02-07) + +[Full Changelog](https://github.com/TheHive-Project/TheHive/compare/3.3.0-RC5...3.3.0-RC6) + +**Implemented enhancements:** + +- Add Tags to an Alert with Responder [\#912](https://github.com/TheHive-Project/TheHive/issues/912) +- Dashboards - Add text widget [\#908](https://github.com/TheHive-Project/TheHive/issues/908) +- Empty case still available when disabled [\#901](https://github.com/TheHive-Project/TheHive/issues/901) +- Support for filtering Tags by prefix \(using asterisk, % or something\) in search dialog [\#666](https://github.com/TheHive-Project/TheHive/issues/666) + +**Closed issues:** + +- Dynamic \(auto-refresh\) of cases is break in 3.3.0-RC5 [\#907](https://github.com/TheHive-Project/TheHive/issues/907) +- Hostname Artifact [\#900](https://github.com/TheHive-Project/TheHive/issues/900) +- DOS issue: Firefox crashing TheHive [\#899](https://github.com/TheHive-Project/TheHive/issues/899) + +## [3.3.0-RC5](https://github.com/TheHive-Project/TheHive/tree/3.3.0-RC5) (2019-02-23) [Full Changelog](https://github.com/TheHive-Project/TheHive/compare/3.3.0-RC4...3.3.0-RC5) **Implemented enhancements:** diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 7040cba8cf..31aac9bee6 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -20,7 +20,7 @@ object Dependencies { val reflections = "org.reflections" % "reflections" % "0.9.11" val zip4j = "net.lingala.zip4j" % "zip4j" % "1.3.2" - val elastic4play = "org.thehive-project" %% "elastic4play" % "1.8.0-1" + val elastic4play = "org.thehive-project" %% "elastic4play" % "1.9.0" val akkaCluster = "com.typesafe.akka" %% "akka-cluster" % "2.5.19" val akkaClusterTools = "com.typesafe.akka" %% "akka-cluster-tools" % "2.5.19" } diff --git a/thehive-cortex/app/connectors/cortex/services/ActionOperation.scala b/thehive-cortex/app/connectors/cortex/services/ActionOperation.scala index 8a13182648..dad270cbbd 100644 --- a/thehive-cortex/app/connectors/cortex/services/ActionOperation.scala +++ b/thehive-cortex/app/connectors/cortex/services/ActionOperation.scala @@ -61,6 +61,10 @@ case class AddLogToTask(content: String, owner: Option[String], status: ActionOp override def updateStatus(newStatus: Type, newMessage: String): ActionOperation = copy(status = newStatus, message = newMessage) } +case class AddTagToAlert(tag: String, status: ActionOperationStatus.Type = ActionOperationStatus.Waiting, message: String = "") extends ActionOperation { + override def updateStatus(newStatus: ActionOperationStatus.Type, newMessage: String): AddTagToAlert = copy(status = newStatus, message = newMessage) +} + object ActionOperation { val addTagToCaseWrites = Json.writes[AddTagToCase] val addTagToArtifactWrites = Json.writes[AddTagToArtifact] @@ -69,6 +73,7 @@ object ActionOperation { val closeTaskWrites = Json.writes[CloseTask] val markAlertAsReadWrites = Json.writes[MarkAlertAsRead] val addLogToTaskWrites = Json.writes[AddLogToTask] + val addTagToAlertWrites = Json.writes[AddTagToAlert] implicit val actionOperationReads: Reads[ActionOperation] = Reads[ActionOperation](json ⇒ (json \ "type").asOpt[String].fold[JsResult[ActionOperation]](JsError("type is missing in action operation")) { case "AddTagToCase" ⇒ (json \ "tag").validate[String].map(tag ⇒ AddTagToCase(tag)) @@ -85,6 +90,7 @@ object ActionOperation { content ← (json \ "content").validate[String] owner ← (json \ "owner").validateOpt[String] } yield AddLogToTask(content, owner) + case "AddTagToAlert" => (json \ "tag").validate[String].map(tag ⇒ AddTagToAlert(tag)) case other ⇒ JsError(s"Unknown operation $other") }) implicit val actionOperationWrites: Writes[ActionOperation] = Writes[ActionOperation] { @@ -95,6 +101,7 @@ object ActionOperation { case a: CloseTask ⇒ closeTaskWrites.writes(a) case a: MarkAlertAsRead ⇒ markAlertAsReadWrites.writes(a) case a: AddLogToTask ⇒ addLogToTaskWrites.writes(a) + case a: AddTagToAlert ⇒ addTagToAlertWrites.writes(a) case a ⇒ Json.obj("unsupported operation" → a.toString) } } @@ -198,6 +205,15 @@ class ActionOperationSrv @Inject() ( task ← findTaskEntity(entity) _ ← logSrv.create(task, Fields.empty.set("message", content).set("owner", owner.map(JsString))) } yield operation.updateStatus(ActionOperationStatus.Success, "") + case AddTagToAlert(tag, _, _) => + entity match { + case initialAlert: Alert ⇒ + for { + alert ← alertSrv.get(initialAlert.id) + _ ← alertSrv.update(alert.id, Fields.empty.set("tags", Json.toJson((alert.tags() :+ tag).distinct)), ModifyConfig(retryOnConflict = 0, version = Some(alert.version))) + } yield operation.updateStatus(ActionOperationStatus.Success, "") + case _ ⇒ Future.failed(BadRequestError("Alert not found")) + } case o ⇒ Future.successful(operation.updateStatus(ActionOperationStatus.Failure, s"Operation $o not supported")) } } diff --git a/ui/app/index.html b/ui/app/index.html index 8946fc036c..c5e868cf1e 100644 --- a/ui/app/index.html +++ b/ui/app/index.html @@ -186,6 +186,7 @@ + diff --git a/ui/app/scripts/controllers/dashboard/DashboardViewCtrl.js b/ui/app/scripts/controllers/dashboard/DashboardViewCtrl.js index 7e3805a96d..d254d4c939 100644 --- a/ui/app/scripts/controllers/dashboard/DashboardViewCtrl.js +++ b/ui/app/scripts/controllers/dashboard/DashboardViewCtrl.js @@ -57,11 +57,11 @@ this.canEditDashboard = function() { return (this.createdBy === this.currentUser.id) || (this.dashboardStatus = 'Shared' && AuthenticationSrv.isAdmin(this.currentUser)); - } + }; this.options = { dashboardAllowedTypes: ['container'], - containerAllowedTypes: ['bar', 'line', 'donut', 'counter', 'multiline'], + containerAllowedTypes: ['bar', 'line', 'donut', 'counter', 'text', 'multiline'], maxColumns: 3, cls: DashboardSrv.typeClasses, labels: { @@ -70,6 +70,7 @@ donut: 'Donut', line: 'Line', counter: 'Counter', + text: 'Text', multiline: 'Multi Lines' }, editLayout: !_.find(this.definition.items, function(row) { @@ -132,9 +133,9 @@ }, 0); }); - } + }; - this.itemInserted = function(item, rows, rowIndex, index) { + this.itemInserted = function(item, rows/*, rowIndex, index*/) { if(!item.id){ item.id = UtilsSrv.guid(); } diff --git a/ui/app/scripts/directives/dashboard/multiline.js b/ui/app/scripts/directives/dashboard/multiline.js index 4c86d6d871..6849c4992a 100644 --- a/ui/app/scripts/directives/dashboard/multiline.js +++ b/ui/app/scripts/directives/dashboard/multiline.js @@ -33,7 +33,7 @@ } return s; - } + }; scope.buildSerie = function(serie, q, index) { return { @@ -166,7 +166,7 @@ }; scope.chart = chart; - }, function(err) { + }, function(/*err*/) { scope.error = true; NotificationSrv.log('Failed to fetch data, please edit the widget definition', 'error'); }); diff --git a/ui/app/scripts/directives/dashboard/text.js b/ui/app/scripts/directives/dashboard/text.js new file mode 100644 index 0000000000..9fdf77c810 --- /dev/null +++ b/ui/app/scripts/directives/dashboard/text.js @@ -0,0 +1,99 @@ +(function() { + 'use strict'; + angular.module('theHiveDirectives').directive('dashboardText', function($q, $http, $state, DashboardSrv, GlobalSearchSrv, NotificationSrv) { + return { + restrict: 'E', + scope: { + filter: '=?', + options: '=', + entity: '=', + autoload: '=', + mode: '=', + refreshOn: '@', + resizeOn: '@', + metadata: '=' + }, + templateUrl: 'views/directives/dashboard/text/view.html', + link: function(scope, elem) { + + scope.error = false; + scope.data = null; + scope.globalQuery = null; + + scope.load = function() { + if(!scope.options.series || scope.options.series.length === 0) { + scope.error = true; + return; + } + + var query = DashboardSrv.buildChartQuery(scope.filter, scope.options.query); + scope.globalQuery = query; + + var stats = { + stats: _.map(scope.options.series || [], function(serie, index) { + var s = { + _agg: serie.agg, + _name: serie.name || 'agg_' + (index + 1), + _query: serie.query || {} + }; + + if(serie.agg !== 'count') { + s._field = serie.field; + } + + return { + model: serie.entity, + query: query, + stats: [s] + }; + }) + }; + + var statsPromise = $http.post('./api/_stats', stats); + + statsPromise.then(function(response) { + scope.error = false; + scope.data = response.data; + + var template = scope.options.template; + Object.keys(scope.data).forEach(function(key){ + var regex = new RegExp('{{' + key + '}}', 'gi'); + + template = template.replace(regex, scope.data[key]); + }); + + scope.content = template; + + }, function(/*err*/) { + scope.error = true; + + NotificationSrv.log('Failed to fetch data, please edit the widget definition', 'error'); + }); + }; + + scope.copyHTML = function() { + var html = elem[0].querySelector('.widget-content').innerHTML; + function listener(e) { + e.clipboardData.setData('text/html', html); + e.clipboardData.setData('text/plain', html); + e.preventDefault(); + } + document.addEventListener('copy', listener); + document.execCommand('copy'); + document.removeEventListener('copy', listener); + } + + if (scope.autoload === true) { + scope.load(); + } + + if (!_.isEmpty(scope.refreshOn)) { + scope.$on(scope.refreshOn, function(event, filter) { + scope.filter = filter; + scope.load(); + }); + } + } + }; + }); +})(); diff --git a/ui/app/scripts/services/DashboardSrv.js b/ui/app/scripts/services/DashboardSrv.js index 6a2bcda5d1..7e0475bbce 100644 --- a/ui/app/scripts/services/DashboardSrv.js +++ b/ui/app/scripts/services/DashboardSrv.js @@ -74,7 +74,8 @@ donut: 'fa-pie-chart', line: 'fa-line-chart', multiline: 'fa-area-chart', - counter: 'fa-calculator' + counter: 'fa-calculator', + text: 'fa-file' }; this.sortOptions = [{ @@ -113,6 +114,14 @@ entity: null } }, + { + type: 'text', + options: { + title: null, + template: null, + entity: null + } + }, { type: 'donut', options: { @@ -234,6 +243,7 @@ this.hasMinimalConfiguration = function(component) { switch (component.type) { case 'multiline': + case 'text': return component.options.series.length === _.without(_.pluck(component.options.series, 'entity'), undefined).length; default: return !!component.options.entity; diff --git a/ui/app/views/directives/dashboard/item.html b/ui/app/views/directives/dashboard/item.html index 08bd60745c..e6f6094b69 100644 --- a/ui/app/views/directives/dashboard/item.html +++ b/ui/app/views/directives/dashboard/item.html @@ -1,7 +1,7 @@