diff --git a/CHANGELOG.md b/CHANGELOG.md index a1d82dbef3..0a8caaba86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Change Log +## [4.1.9](https://github.com/TheHive-Project/TheHive/milestone/78) (2021-07-23) + +**Implemented enhancements:** + +- [Enhancement] Add button for index rebuilding [\#2144](https://github.com/TheHive-Project/TheHive/issues/2144) + +**Fixed bugs:** + +- [Bug] MISP sync delete existing observables when updating existing Alert [\#2134](https://github.com/TheHive-Project/TheHive/issues/2134) +- [Bug] Livestream emptied of audit logs after TheHive reboot [\#2135](https://github.com/TheHive-Project/TheHive/issues/2135) +- [Bug] AddTagToCase operation does not work [\#2136](https://github.com/TheHive-Project/TheHive/issues/2136) + ## [4.1.8](https://github.com/TheHive-Project/TheHive/milestone/77) (2021-07-19) **Implemented enhancements:** diff --git a/build.sbt b/build.sbt index 0bf7efbc1c..6ddcfd53cc 100644 --- a/build.sbt +++ b/build.sbt @@ -2,7 +2,7 @@ import Dependencies._ import com.typesafe.sbt.packager.Keys.bashScriptDefines import org.thp.ghcl.Milestone -val thehiveVersion = "4.1.8-1" +val thehiveVersion = "4.1.9-1" val scala212 = "2.12.13" val scala213 = "2.13.1" val supportedScalaVersions = List(scala212, scala213) diff --git a/cortex/connector/src/main/scala/org/thp/thehive/connector/cortex/services/ActionSrv.scala b/cortex/connector/src/main/scala/org/thp/thehive/connector/cortex/services/ActionSrv.scala index 093b9686d6..1ee8097ba2 100644 --- a/cortex/connector/src/main/scala/org/thp/thehive/connector/cortex/services/ActionSrv.scala +++ b/cortex/connector/src/main/scala/org/thp/thehive/connector/cortex/services/ActionSrv.scala @@ -195,13 +195,13 @@ class ActionSrv @Inject() ( */ def relatedCase(id: EntityId)(implicit graph: Graph): Option[Case with Entity] = for { - richAction <- startTraversal.getByIds(id).richAction.getOrFail("Action").toOption + richAction <- startTraversal.getByIds(id).richAction.headOption relatedCase <- entityHelper.parentCase(richAction.context) } yield relatedCase def relatedTask(id: EntityId)(implicit graph: Graph): Option[Task with Entity] = for { - richAction <- startTraversal.getByIds(id).richAction.getOrFail("Action").toOption + richAction <- startTraversal.getByIds(id).richAction.headOption relatedTask <- entityHelper.parentTask(richAction.context) } yield relatedTask diff --git a/cortex/connector/src/main/scala/org/thp/thehive/connector/cortex/services/EntityHelper.scala b/cortex/connector/src/main/scala/org/thp/thehive/connector/cortex/services/EntityHelper.scala index af6bc89409..dcc31f761c 100644 --- a/cortex/connector/src/main/scala/org/thp/thehive/connector/cortex/services/EntityHelper.scala +++ b/cortex/connector/src/main/scala/org/thp/thehive/connector/cortex/services/EntityHelper.scala @@ -60,11 +60,12 @@ class EntityHelper @Inject() ( */ def parentCase(entity: Entity)(implicit graph: Graph): Option[Case with Entity] = entity._label match { - case "Task" => taskSrv.get(entity).`case`.headOption - case "Case" => caseSrv.get(entity).headOption - case "Log" => logSrv.get(entity).`case`.headOption - case "Alert" => None - case _ => None + case "Task" => taskSrv.get(entity).`case`.headOption + case "Case" => caseSrv.get(entity).headOption + case "Observable" => observableSrv.get(entity).`case`.headOption + case "Log" => logSrv.get(entity).`case`.headOption + case "Alert" => None + case _ => None } /** diff --git a/frontend/app/scripts/controllers/admin/platform/PlatformStatusCtrl.js b/frontend/app/scripts/controllers/admin/platform/PlatformStatusCtrl.js index 3718916b33..a0ed8a01b8 100644 --- a/frontend/app/scripts/controllers/admin/platform/PlatformStatusCtrl.js +++ b/frontend/app/scripts/controllers/admin/platform/PlatformStatusCtrl.js @@ -1,107 +1,131 @@ -(function() { +(function () { 'use strict'; - angular.module('theHiveControllers').controller('PlatformStatusCtrl', function(ModalSrv, PlatformSrv, NotificationSrv, appConfig) { - var self = this; + angular.module('theHiveControllers').controller('PlatformStatusCtrl', function (ModalSrv, PlatformSrv, NotificationSrv, appConfig) { + var self = this; - self.appConfig = appConfig; - self.indexStatus = {}; - self.checkStats = {}; - - self.loading = { - index: false, - check: false - } - - this.loadIndexStatus = function() { - self.indexStatus = {}; - self.loading.index = true; - - PlatformSrv.getIndexStatus() - .then(function(response) { - self.indexStatus = response.data; - self.loading.index = false; - }); - } + self.appConfig = appConfig; + self.indexStatus = {}; + self.checkStats = {}; - this.loadCheckStats = function() { - self.loading.check = true; + self.loading = { + index: false, + check: false + } - PlatformSrv.getCheckStats() - .then(function(response) { - self.checkStats = response.data; - self.loading.check = false; - }) - } + this.loadIndexStatus = function () { + self.indexStatus = {}; + self.loading.index = true; - this.$onInit = function() { - self.loadIndexStatus(); - self.loadCheckStats(); + PlatformSrv.getIndexStatus() + .then(function (response) { + self.indexStatus = response.data; + self.loading.index = false; + }); + } + + this.loadCheckStats = function () { + self.loading.check = true; + + PlatformSrv.getCheckStats() + .then(function (response) { + self.checkStats = response.data; + self.loading.check = false; + }) + } + + this.$onInit = function () { + self.loadIndexStatus(); + self.loadCheckStats(); + }; + + this.exportReport = function () { + var date = new moment().format('YYYYMMDD-HH:mmZ'); + var fileName = 'Platform-Status-Report-' + date + '.json'; + + var content = { + indexStatus: self.indexStatus, + checkStatus: self.checkStats, + schemaStatus: self.appConfig.schemaStatus }; - this.exportReport = function() { - var date = new moment().format('YYYYMMDD-HH:mmZ'); - var fileName = 'Platform-Status-Report-'+date+'.json'; - - var content = { - indexStatus: self.indexStatus, - checkStatus: self.checkStats, - schemaStatus: self.appConfig.schemaStatus - }; - - // Create a blob of the data - var fileToSave = new Blob([JSON.stringify(content)], { - type: 'application/json', - name: fileName + // Create a blob of the data + var fileToSave = new Blob([JSON.stringify(content)], { + type: 'application/json', + name: fileName + }); + + // Save the file + saveAs(fileToSave, fileName); + } + + this.reindex = function (indexName) { + var modalInstance = ModalSrv.confirm( + 'Reindex', + 'Are you sure you want to trigger ' + indexName + ' data reindex', { + okText: 'Yes, reindex it' + } + ); + + modalInstance.result + .then(function () { + PlatformSrv.runReindex(indexName); + }) + .then(function (/*response*/) { + NotificationSrv.success('Reindexing of ' + indexName + ' data started sucessfully'); + }) + .catch(function (err) { + if (!_.isString(err)) { + NotificationSrv.error('Platform status', err.data, err.status); + } }); - - // Save the file - saveAs(fileToSave, fileName); + }; + + this.rebuildIndex = function (indexName) { + var modalInstance = ModalSrv.confirm( + 'Drop & Rebuild Index', + 'Are you sure you want to delete and rebuild ' + indexName + ' data reindex. ' + + 'This operation will drop your existing data index and create a new one.', { + okText: 'Yes, rebuild it', + flavor: 'danger' } - - this.reindex = function(indexName) { - var modalInstance = ModalSrv.confirm( - 'Reindex', - 'Are you sure you want to trigger ' + indexName + ' data reindex', { - okText: 'Yes, reindex it' + ); + + modalInstance.result + .then(function () { + PlatformSrv.runRebuildIndex(indexName); + }) + .then(function (/*response*/) { + NotificationSrv.success('Rebuild of ' + indexName + ' data started sucessfully'); + }) + .catch(function (err) { + if (!_.isString(err)) { + NotificationSrv.error('Platform status', err.data, err.status); } - ); - - modalInstance.result - .then(function() { - PlatformSrv.runReindex(indexName); - }) - .then(function(/*response*/) { - NotificationSrv.success('Reindexing of ' + indexName + ' data started sucessfully'); - }) - .catch(function(err) { - if (!_.isString(err)) { - NotificationSrv.error('Platform status', err.data, err.status); - } - }); - }; + }); + }; - this.checkControl = function(checkName) { - var modalInstance = ModalSrv.confirm( - 'Data health check', - 'Are you sure you want to trigger ' + checkName + ' health check', { - okText: 'Yes, trigger it' - } - ); - - modalInstance.result - .then(function() { - PlatformSrv.runCheck(checkName); - }) - .then(function(/*response*/) { - NotificationSrv.success('Data health check of ' + checkName + ' started sucessfully'); - }) - .catch(function(err) { - if (!_.isString(err)) { - NotificationSrv.error('Platform status', err.data, err.status); - } - }); + this.checkControl = function (checkName) { + var modalInstance = ModalSrv.confirm( + 'Data health check', + 'Are you sure you want to trigger ' + checkName + ' health check', { + okText: 'Yes, trigger it' } + ); + + modalInstance.result + .then(function () { + PlatformSrv.runCheck(checkName); + }) + .then(function (/*response*/) { + NotificationSrv.success('Data health check of ' + checkName + ' started sucessfully'); + }) + .catch(function (err) { + if (!_.isString(err)) { + NotificationSrv.error('Platform status', err.data, err.status); + } + }); + } - }); + }); })(); diff --git a/frontend/app/scripts/services/api/PlatformSrv.js b/frontend/app/scripts/services/api/PlatformSrv.js index bd3451ec11..2aa910b1fc 100644 --- a/frontend/app/scripts/services/api/PlatformSrv.js +++ b/frontend/app/scripts/services/api/PlatformSrv.js @@ -1,22 +1,26 @@ -(function() { +(function () { 'use strict'; angular.module('theHiveServices') - .service('PlatformSrv', function($http) { + .service('PlatformSrv', function ($http) { - this.getIndexStatus = function() { + this.getIndexStatus = function () { return $http.get('./api/v1/admin/index/status') } - this.runReindex = function(indexName) { - return $http.get('./api/v1/admin/index/'+indexName+'/reindex'); + this.runReindex = function (indexName) { + return $http.post('./api/v1/admin/index/' + indexName + '/reindex'); } - this.getCheckStats = function() { + this.runRebuildIndex = function (indexName) { + return $http.post('./api/v1/admin/index/' + indexName + '/rebuild'); + } + + this.getCheckStats = function () { return $http.get('./api/v1/admin/check/stats') } - this.runCheck = function(checkName) { - return $http.get('./api/v1/admin/check/'+checkName+'/trigger'); + this.runCheck = function (checkName) { + return $http.get('./api/v1/admin/check/' + checkName + '/trigger'); } }); diff --git a/frontend/app/views/partials/admin/platform/status.html b/frontend/app/views/partials/admin/platform/status.html index 12b4c23851..6a25e82efd 100644 --- a/frontend/app/views/partials/admin/platform/status.html +++ b/frontend/app/views/partials/admin/platform/status.html @@ -55,16 +55,22 @@

Data index status Loading index status...
-
Index: {{index.name | uppercase}}
-
# Entities
-
- Reindex +
+ Index: {{index.name | uppercase}} + + + Reindex the data + + + Drop and rebuild the index + +
+
# Entities
-
+

{{item.name}}

@@ -75,8 +81,6 @@

{{item.count}}

- -
diff --git a/frontend/bower.json b/frontend/bower.json index 527d127d7c..387503dc96 100644 --- a/frontend/bower.json +++ b/frontend/bower.json @@ -1,6 +1,6 @@ { "name": "thehive", - "version": "4.1.8-1", + "version": "4.1.9-1", "license": "AGPL-3.0", "dependencies": { "jquery": "^3.4.1", diff --git a/frontend/package.json b/frontend/package.json index a55ad6e3f7..19c1d66ff0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "thehive", - "version": "4.1.8-1", + "version": "4.1.9-1", "license": "AGPL-3.0", "repository": { "type": "git", diff --git a/misp/client/src/main/scala/org/thp/misp/client/MispClient.scala b/misp/client/src/main/scala/org/thp/misp/client/MispClient.scala index 8f23e59300..5aba625329 100644 --- a/misp/client/src/main/scala/org/thp/misp/client/MispClient.scala +++ b/misp/client/src/main/scala/org/thp/misp/client/MispClient.scala @@ -189,18 +189,24 @@ class MispClient( .mapMaterializedValue(_ => NotUsed) } - def searchAttributes(eventId: String, publishDate: Option[Date])(implicit ec: ExecutionContext): Source[Attribute, NotUsed] = { + def searchAttributes(eventId: String, publishDate: Option[Date], deletedOnly: Boolean = false)(implicit + ec: ExecutionContext + ): Source[Attribute, NotUsed] = { logger.debug(s"Search MISP attributes for event #$eventId ${publishDate.fold("")("from " + _)}") Source .futureSource( postStream( "attributes/restSearch/json", - Json.obj("request" -> Json.obj("timestamp" -> publishDate.fold(0L)(_.getTime / 1000), "eventid" -> eventId)) + Json.obj( + "request" -> + Json + .obj("timestamp" -> publishDate.fold(0L)(_.getTime / 1000), "eventid" -> eventId) + .when(deletedOnly)(_ + ("deleted" -> JsString("only"))) + ) ) ) // add ("deleted" → 1) to see also deleted attributes // add ("deleted" → "only") to see only deleted attributes - // .via(JsonFraming.objectScanner(Int.MaxValue)) .via(JsonReader.select("$.response.Attribute[*]")) .mapConcat { data => val maybeAttribute = Try(Json.parse(data.toArray[Byte]).as[Attribute]) @@ -219,8 +225,6 @@ class MispClient( .mapMaterializedValue(_ => NotUsed) } - // .filter(_.date after refDate) - private val fileNameExtractor = """attachment; filename="(.*)"""".r def downloadAttachment(attachmentId: String)(implicit ec: ExecutionContext): Future[(String, String, Source[ByteString, _])] = diff --git a/misp/connector/src/main/scala/org/thp/thehive/connector/misp/services/MispImportSrv.scala b/misp/connector/src/main/scala/org/thp/thehive/connector/misp/services/MispImportSrv.scala index 96d80dd87f..e63afa5c8e 100644 --- a/misp/connector/src/main/scala/org/thp/thehive/connector/misp/services/MispImportSrv.scala +++ b/misp/connector/src/main/scala/org/thp/thehive/connector/misp/services/MispImportSrv.scala @@ -3,7 +3,6 @@ package org.thp.thehive.connector.misp.services import akka.stream.Materializer import akka.stream.scaladsl.{FileIO, Sink, Source} import akka.util.ByteString -import org.apache.tinkerpop.gremlin.process.traversal.P import org.thp.misp.dto.{Attribute, Event, Tag => MispTag} import org.thp.scalligraph.auth.{AuthContext, UserSrv} import org.thp.scalligraph.controllers.FFile @@ -40,6 +39,7 @@ class MispImportSrv @Inject() ( db: Database, auditSrv: AuditSrv, userSrv: UserSrv, + attachmentSrv: AttachmentSrv, implicit val ec: ExecutionContext, implicit val mat: Materializer ) { @@ -240,40 +240,41 @@ class MispImportSrv @Inject() ( src: Source[ByteString, _], creation: Boolean )(implicit graph: Graph, authContext: AuthContext): Try[Observable with Entity] = { - val existingObservable = - if (creation) None - else - alertSrv - .get(alert) - .observables - .filterOnType(observable.dataType) - .filterOnAttachmentName(filename) - .filterOnAttachmentName(contentType) - .richObservable - .headOption - existingObservable match { - case None => - logger.debug(s"Observable ${observable.dataType}:$filename:$contentType doesn't exist, create it") - val file = Files.createTempFile("misp-attachment-", "") - Await.result(src.runWith(FileIO.toPath(file)), 1.hour) - val fFile = FFile(filename, file, contentType) - val res = alertSrv.createObservable(alert, observable, fFile).map(_.observable) - Files.delete(file) - res - case Some(richObservable) => - logger.debug(s"Observable ${observable.dataType}:$filename:$contentType exists, update it") - for { - obs <- - observableSrv - .get(richObservable.observable) - .when(richObservable.message != observable.message)(_.update(_.message, observable.message)) - .when(richObservable.tlp != observable.tlp)(_.update(_.tlp, observable.tlp)) - .when(richObservable.ioc != observable.ioc)(_.update(_.ioc, observable.ioc)) - .when(richObservable.sighted != observable.sighted)(_.update(_.sighted, observable.sighted)) - .when(richObservable.tags.toSet != observable.tags.toSet)(_.update(_.tags, observable.tags)) - .getOrFail("Observable") - } yield obs - } + val file = Files.createTempFile("misp-attachment-", "") + try { + Await.result(src.runWith(FileIO.toPath(file)), 1.hour) + val hash = attachmentSrv.hashers.fromPath(file).head.toString + val existingObservable = + if (creation) None + else + alertSrv + .get(alert) + .observables + .filterOnType(observable.dataType) + .filterOnAttachmentName(filename) + .filterOnAttachmentContentType(contentType) + .filterOnAttachmentHash(hash) + .richObservable + .headOption + existingObservable match { + case None => + logger.debug(s"Observable ${observable.dataType}:$filename:$contentType doesn't exist, create it") + alertSrv.createObservable(alert, observable, FFile(filename, file, contentType)).map(_.observable) + case Some(richObservable) => + logger.debug(s"Observable ${observable.dataType}:$filename:$contentType exists, update it") + for { + obs <- + observableSrv + .get(richObservable.observable) + .when(richObservable.message != observable.message)(_.update(_.message, observable.message)) + .when(richObservable.tlp != observable.tlp)(_.update(_.tlp, observable.tlp)) + .when(richObservable.ioc != observable.ioc)(_.update(_.ioc, observable.ioc)) + .when(richObservable.sighted != observable.sighted)(_.update(_.sighted, observable.sighted)) + .when(richObservable.tags.toSet != observable.tags.toSet)(_.update(_.tags, observable.tags)) + .getOrFail("Observable") + } yield obs + } + } finally Files.delete(file) } def importAttributes( @@ -286,8 +287,35 @@ class MispImportSrv @Inject() ( graph: Graph, authContext: AuthContext ): Unit = { + logger.info("Removing old observables") + val deletedAttributes = client + .searchAttributes(event.id, lastSynchro, deletedOnly = true) + .mapConcat(attributeToObservable) + .runWith(Sink.queue[(Observable, Either[String, (String, String, Source[ByteString, _])])]) + + QueueIterator(deletedAttributes) + .flatMap { + case (observable, Left(data)) => + observableSrv + .startTraversal + .has(_.relatedId, alert._id) + .filterOnType(observable.dataType) + .filterOnData(data) + .toIterator + case (observable, Right((filename, contentType, src))) => + val hash = attachmentSrv.hashers.fromBinary(src).head.toString + observableSrv + .startTraversal + .has(_.relatedId, alert._id) + .filterOnType(observable.dataType) + .filterOnAttachmentContentType(contentType) + .filterOnAttachmentName(filename) + .filterOnAttachmentHash(hash) + .toIterator + } + .foreach(observableSrv.delete(_)) + logger.debug(s"importAttributes ${client.name}#${event.id}") - val startSyncDate = new Date val queue = client .searchAttributes(event.id, lastSynchro) @@ -336,24 +364,6 @@ class MispImportSrv @Inject() ( case Failure(error) => logger.error(s"Unable to create observable $observable ($filename) on alert", error) } } - - logger.info("Removing old observables") - alertSrv - .get(alert) - .observables - .filter( - _.or( - _.has(_._updatedAt, P.lt(startSyncDate)), - _.and(_.hasNot(_._updatedAt), _.has(_._createdAt, P.lt(startSyncDate))) - ) - ) - .toIterator - .foreach { obs => - logger.debug(s"Delete $obs") - observableSrv.delete(obs).recover { - case error => logger.error(s"Fail to delete observable $obs", error) - } - } } // def convertTag(mispTag: MispTag): Tag = tagSrv.parseString(mispTag.name + mispTag.colour.fold("")(c => f"#$c%06X")) diff --git a/thehive/app/org/thp/thehive/controllers/v1/AdminCtrl.scala b/thehive/app/org/thp/thehive/controllers/v1/AdminCtrl.scala index f91a146d3e..8ab98e950e 100644 --- a/thehive/app/org/thp/thehive/controllers/v1/AdminCtrl.scala +++ b/thehive/app/org/thp/thehive/controllers/v1/AdminCtrl.scala @@ -140,6 +140,15 @@ class AdminCtrl @Inject() ( Success(Results.NoContent) } + def rebuild(name: String): Action[AnyContent] = + entrypoint("Rebuild index") + .authPermitted(Permissions.managePlatform) { _ => + db + .removeIndex(name, IndexType.fulltext, Nil) + .flatMap(_ => schemas.toTry(db.addSchemaIndexes)) + .map(_ => Results.NoContent) + } + private val rangeRegex = "(\\d+)-(\\d+)".r private def getOperations(schemaName: String, select: Option[String], filter: Option[String]): Seq[(Operation, Int)] = { val ranges = select.fold(Seq(0 until Int.MaxValue))(_.split(',').toSeq.map { diff --git a/thehive/app/org/thp/thehive/controllers/v1/Router.scala b/thehive/app/org/thp/thehive/controllers/v1/Router.scala index 622167041a..0b98838365 100644 --- a/thehive/app/org/thp/thehive/controllers/v1/Router.scala +++ b/thehive/app/org/thp/thehive/controllers/v1/Router.scala @@ -46,7 +46,8 @@ class Router @Inject() ( case GET(p"/admin/check/stats") => adminCtrl.checkStats case GET(p"/admin/check/$name/trigger") => adminCtrl.triggerCheck(name) case GET(p"/admin/index/status") => adminCtrl.indexStatus - case GET(p"/admin/index/$name/reindex") => adminCtrl.reindex(name) + case POST(p"/admin/index/$name/reindex") => adminCtrl.reindex(name) + case POST(p"/admin/index/$name/rebuild") => adminCtrl.rebuild(name) case GET(p"/admin/log/set/$packageName/$level") => adminCtrl.setLogLevel(packageName, level) case POST(p"/admin/schema/repair/$schemaName" ? q_o"select=$select" ? q_o"filter=$filter") => adminCtrl.schemaRepair(schemaName, select, filter) case POST(p"/admin/schema/info/$schemaName" ? q_o"select=$select" ? q_o"filter=$filter") => adminCtrl.schemaInfo(schemaName, select, filter) diff --git a/thehive/app/org/thp/thehive/services/AlertSrv.scala b/thehive/app/org/thp/thehive/services/AlertSrv.scala index 8d91fd568f..0e10da152b 100644 --- a/thehive/app/org/thp/thehive/services/AlertSrv.scala +++ b/thehive/app/org/thp/thehive/services/AlertSrv.scala @@ -144,31 +144,6 @@ class AlertSrv @Inject() ( ): Try[RichObservable] = attachmentSrv.create(file).flatMap(attachment => createObservable(alert, observable, attachment)) - @deprecated("use createObservable", "0.2") - def addObservable(alert: Alert with Entity, richObservable: RichObservable)(implicit - graph: Graph, - authContext: AuthContext - ): Try[Unit] = { - val maybeExistingObservable = richObservable.dataOrAttachment match { - case Left(data) => get(alert).observables.filterOnData(data) - case Right(attachment) => get(alert).observables.filterOnAttachmentId(attachment.attachmentId) - } - maybeExistingObservable - .richObservable - .headOption - .fold { - for { - _ <- alertObservableSrv.create(AlertObservable(), alert, richObservable.observable) - _ <- auditSrv.observableInAlert.create(richObservable.observable, alert, richObservable.toJson) - } yield () - } { existingObservable => - val tags = (existingObservable.tags ++ richObservable.tags).toSet - if ((tags -- existingObservable.tags).nonEmpty) - observableSrv.updateTags(existingObservable.observable, tags) - Success(()) - } - } - def createCustomField( alert: Alert with Entity, inputCf: InputCustomFieldValue diff --git a/thehive/app/org/thp/thehive/services/FlowActor.scala b/thehive/app/org/thp/thehive/services/FlowActor.scala index bd715117d7..9fbd9883dc 100644 --- a/thehive/app/org/thp/thehive/services/FlowActor.scala +++ b/thehive/app/org/thp/thehive/services/FlowActor.scala @@ -109,12 +109,12 @@ class FlowActor extends Actor { case (id, organisations, cases) => organisations.foreach { organisation => val cacheKey = FlowId.toString(organisation, None) - val ids = cache.get[List[String]](cacheKey).getOrElse(Nil) - cache.set(cacheKey, (id :: ids).take(10)) + val ids = cache.get[Seq[String]](cacheKey).getOrElse(Nil) + cache.set(cacheKey, (id +: ids).take(10)) cases.foreach { caseId => val cacheKey: String = FlowId.toString(organisation, Some(caseId)) - val ids = cache.get[List[String]](cacheKey).getOrElse(Nil) - cache.set(cacheKey, (id :: ids).take(10)) + val ids = cache.get[Seq[String]](cacheKey).getOrElse(Nil) + cache.set(cacheKey, (id +: ids).take(10)) } } }