diff --git a/CHANGELOG.md b/CHANGELOG.md index 2508527dce..cbce21785e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,26 +1,46 @@ # Change Log -## [2.12.0](https://github.com/CERT-BDF/TheHive/tree/2.12.0) +## [2.12.1](https://github.com/CERT-BDF/TheHive/tree/2.12.1) (2017-08-01) +[Full Changelog](https://github.com/CERT-BDF/TheHive/compare/2.12.0...2.12.1) + +**Implemented enhancements:** + +- Fix warnings in debian package [\#267](https://github.com/CERT-BDF/TheHive/issues/267) +- Merging alert into existing case does not merge alert description into case description [\#255](https://github.com/CERT-BDF/TheHive/issues/255) + +**Fixed bugs:** + +- Case similarity reports merged cases [\#272](https://github.com/CERT-BDF/TheHive/issues/272) +- Closing a case with an open task does not dismiss task in "My tasks" [\#269](https://github.com/CERT-BDF/TheHive/issues/269) +- API: cannot create alert if one alert artifact contains the IOC field set [\#268](https://github.com/CERT-BDF/TheHive/issues/268) +- Can't get logs of a task via API [\#259](https://github.com/CERT-BDF/TheHive/issues/259) +- Add multiple attachments in a single task log doesn't work [\#257](https://github.com/CERT-BDF/TheHive/issues/257) +- Cortex Connector Not Found [\#256](https://github.com/CERT-BDF/TheHive/issues/256) +- TheHive doesn't send the file name to Cortex [\#254](https://github.com/CERT-BDF/TheHive/issues/254) +- Renaming of users does not work [\#249](https://github.com/CERT-BDF/TheHive/issues/249) + +## [2.12.0](https://github.com/CERT-BDF/TheHive/tree/2.12.0) (2017-07-04) [Full Changelog](https://github.com/CERT-BDF/TheHive/compare/2.11.3...2.12.0) **Implemented enhancements:** +- Use local font files [\#250](https://github.com/CERT-BDF/TheHive/issues/250) - Sort the analyzers list in observable details page [\#245](https://github.com/CERT-BDF/TheHive/issues/245) - More options to sort cases [\#243](https://github.com/CERT-BDF/TheHive/issues/243) - Alert Preview and management improvements [\#232](https://github.com/CERT-BDF/TheHive/issues/232) -- Ability to Reopen Tasks [\#156](https://github.com/CERT-BDF/TheHive/issues/156) -- Display short reports on the Observables tab [\#131](https://github.com/CERT-BDF/TheHive/issues/131) -- Custom fields for case template [\#12](https://github.com/CERT-BDF/TheHive/issues/12) - Show case status and category \(FP, TP, IND\) in related cases [\#229](https://github.com/CERT-BDF/TheHive/issues/229) - Open External Links in New Tab [\#228](https://github.com/CERT-BDF/TheHive/issues/228) - Observable analyzers view reports. [\#191](https://github.com/CERT-BDF/TheHive/issues/191) - Specifying tags on statistics page or performing a search [\#186](https://github.com/CERT-BDF/TheHive/issues/186) - Choose case template while importing events from MISP [\#175](https://github.com/CERT-BDF/TheHive/issues/175) -- Use local font files [\#250](https://github.com/CERT-BDF/TheHive/issues/250) +- Ability to Reopen Tasks [\#156](https://github.com/CERT-BDF/TheHive/issues/156) +- Display short reports on the Observables tab [\#131](https://github.com/CERT-BDF/TheHive/issues/131) +- Custom fields for case template [\#12](https://github.com/CERT-BDF/TheHive/issues/12) **Fixed bugs:** +- A locked user can use the API to create / delete / list cases \(and more\) [\#251](https://github.com/CERT-BDF/TheHive/issues/251) - Fix case metrics malformed definitions [\#248](https://github.com/CERT-BDF/TheHive/issues/248) - Sorting alerts by severity fails [\#242](https://github.com/CERT-BDF/TheHive/issues/242) - Alerting Panel: Typo Correction [\#240](https://github.com/CERT-BDF/TheHive/issues/240) diff --git a/build.sbt b/build.sbt index 247d46066a..96d2bfee54 100644 --- a/build.sbt +++ b/build.sbt @@ -49,6 +49,7 @@ lazy val rpmPackageRelease = (project in file("package/rpm-release")) )) ) + Release.releaseVersionUIFile := baseDirectory.value / "ui" / "package.json" Release.changelogFile := baseDirectory.value / "CHANGELOG.md" @@ -79,24 +80,31 @@ mappings in Universal ~= { maintainer := "TheHive Project " packageSummary := "Scalable, Open Source and Free Security Incident Response Solutions" packageDescription := - """TheHive is a scalable 3-in-1 open source and free security incident response platform designed to make life easier - | for SOCs, CSIRTs, CERTs and any information security practitioner dealing with security incidents that need to be - | investigated and acted upon swiftly.""".stripMargin + """TheHive is a scalable 3-in-1 open source and free security incident response + | platform designed to make life easier for SOCs, CSIRTs, CERTs and any + | information security practitioner dealing with security incidents that need to + | be investigated and acted upon swiftly.""".stripMargin defaultLinuxInstallLocation := "/opt" linuxPackageMappings ~= { _.map { pm => val mappings = pm.mappings.filterNot { case (_, path) => path.startsWith("/opt/thehive/package") || path.startsWith("/opt/thehive/conf") } - com.typesafe.sbt.packager.linux.LinuxPackageMapping(mappings, pm.fileData).withConfig() - } :+ packageMapping( - file("package/thehive.service") -> "/etc/systemd/system/thehive.service", + com.typesafe.sbt.packager.linux.LinuxPackageMapping(mappings, pm.fileData) + } +} +linuxPackageMappings ++= Seq( + packageMapping( + file("package/thehive.service") -> "/usr/lib/systemd/system/thehive.service" + ).withPerms("644"), + packageMapping( file("package/thehive.conf") -> "/etc/init/thehive.conf", - file("package/thehive") -> "/etc/init.d/thehive", file("conf/application.sample") -> "/etc/thehive/application.conf", file("conf/logback.xml") -> "/etc/thehive/logback.xml" - ).withConfig() -} + ).withPerms("644").withConfig(), + packageMapping( + file("package/thehive") -> "/etc/init.d/thehive" + ).withPerms("755").withConfig()) packageBin := { (packageBin in Universal).value @@ -104,6 +112,7 @@ packageBin := { (packageBin in Rpm).value } // DEB // +linuxPackageMappings in Debian += packageMapping(file("LICENSE") -> "/usr/share/doc/thehive/copyright").withPerms("644") version in Debian := version.value + "-1" debianPackageRecommends := Seq("elasticsearch") debianPackageDependencies += "openjdk-8-jre-headless" diff --git a/package/thehive b/package/thehive index ba27b8cf3e..7e5a726bb9 100755 --- a/package/thehive +++ b/package/thehive @@ -113,6 +113,11 @@ case "$1" in start ;; + force-reload) + stop + start + ;; + *) log_action_msg "Usage: /etc/init.d/thehive {start|stop|restart|status}" || true exit 1 diff --git a/package/thehive.conf-perso b/package/thehive.conf-perso deleted file mode 100644 index a0d9d46088..0000000000 --- a/package/thehive.conf-perso +++ /dev/null @@ -1,49 +0,0 @@ -# thehive - a FOSS Information Security Incident Management Platform -# - -description "TheHive" - -start on runlevel [2345] -stop on runlevel [!2345] - -respawn -respawn limit 10 5 -umask 022 - - -env DAEMON=/opt/thehive/bin/thehive -env PIDFILE=/var/run/thehive.pid -env DAEMONUSER=thehive -env CONFIGFILE=/etc/thehive/application.conf -env LISTENPORT=9000 -env DAEMON_OPTS= -env DEFAULTFILE=/etc/default/thehive - - -pre-start script - echo Starting TheHive - if [ -f "$DEFAULTFILE" ]; then - . "$DEFAULTFILE" - fi - - if test ! -e $CONFIGFILE - then - mkdir -p $(dirname $CONFIGFILE) 2>/dev/null || true - cat >> $CONFIGFILE <<- _EOF_ - # Secret key - # ~~~~~ - # The secret key is used to secure cryptographics functions. - # If you deploy your application to several instances be sure to use the same key! - play.crypto.secret="$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 64 | head -n 1)" -_EOF_ - chown $DAEMONUSER $CONFIGFILE - chmod o-rwx $CONFIGFILE - fi - - test -x $DAEMON || { echo $DAEMON not found; stop; exit 0; } -end script - -script - cd $(dirname $DAEMON)/.. - $DAEMON -Dconfig.file=$CONFIGFILE -Dhttp.port=$LISTENPORT -Dpidfile.path=$PIDFILE $DAEMON_OPTS -end script diff --git a/thehive-backend/app/controllers/AlertCtrl.scala b/thehive-backend/app/controllers/AlertCtrl.scala index 2aba98681d..f90dff0be8 100644 --- a/thehive-backend/app/controllers/AlertCtrl.scala +++ b/thehive-backend/app/controllers/AlertCtrl.scala @@ -42,8 +42,8 @@ class AlertCtrl @Inject() ( for { alert ← alertSrv.get(alertId) caze ← caseSrv.get(caseId) - _ ← alertSrv.mergeWithCase(alert, caze) - } yield renderer.toOutput(CREATED, caze) + updatedCaze ← alertSrv.mergeWithCase(alert, caze) + } yield renderer.toOutput(CREATED, updatedCaze) } @Timed @@ -151,7 +151,7 @@ class AlertCtrl @Inject() ( } @Timed - def fixStatus() = authenticated(Role.admin).async { implicit request ⇒ + def fixStatus(): Action[AnyContent] = authenticated(Role.admin).async { implicit request ⇒ alertSrv.fixStatus() .map(_ ⇒ NoContent) } diff --git a/thehive-backend/app/controllers/CaseCtrl.scala b/thehive-backend/app/controllers/CaseCtrl.scala index a0e43c99b8..818d3219be 100644 --- a/thehive-backend/app/controllers/CaseCtrl.scala +++ b/thehive-backend/app/controllers/CaseCtrl.scala @@ -2,25 +2,21 @@ package controllers import javax.inject.{ Inject, Singleton } -import scala.concurrent.{ ExecutionContext, Future } -import scala.reflect.runtime.universe -import scala.util.{ Failure, Success } import akka.stream.Materializer import akka.stream.scaladsl.Sink +import models.CaseStatus +import org.elastic4play.controllers.{ Authenticated, Fields, FieldsBodyParser, Renderer } +import org.elastic4play.models.JsonFormat.baseModelEntityWrites +import org.elastic4play.services.JsonFormat.{ aggReads, queryReads } +import org.elastic4play.services._ +import org.elastic4play.{ BadRequestError, Timed } import play.api.Logger import play.api.http.Status import play.api.libs.json.{ JsArray, JsObject, Json } -import play.api.libs.json.Json.toJsFieldJsValueWrapper import play.api.mvc.{ Action, AnyContent, Controller } -import org.elastic4play.{ BadRequestError, CreateError, Timed } -import org.elastic4play.controllers.{ Authenticated, Fields, FieldsBodyParser, Renderer } -import org.elastic4play.models.JsonFormat.{ baseModelEntityWrites, multiFormat } -import org.elastic4play.services.{ Agg, AuxSrv } -import org.elastic4play.services.{ QueryDSL, QueryDef, Role } -import org.elastic4play.services.JsonFormat.{ aggReads, queryReads } -import models.{ Case, CaseStatus } import services.{ CaseMergeSrv, CaseSrv, CaseTemplateSrv, TaskSrv } +import scala.concurrent.{ ExecutionContext, Future } import scala.util.Try @Singleton diff --git a/thehive-backend/app/controllers/LogCtrl.scala b/thehive-backend/app/controllers/LogCtrl.scala index f27889fa6a..6dfffbd7cf 100644 --- a/thehive-backend/app/controllers/LogCtrl.scala +++ b/thehive-backend/app/controllers/LogCtrl.scala @@ -2,17 +2,17 @@ package controllers import javax.inject.{ Inject, Singleton } -import scala.concurrent.ExecutionContext -import scala.reflect.runtime.universe -import play.api.http.Status -import play.api.mvc.{ Action, AnyContent, Controller } import org.elastic4play.Timed import org.elastic4play.controllers.{ Authenticated, Fields, FieldsBodyParser, Renderer } -import org.elastic4play.models.JsonFormat.baseModelEntityWrites -import org.elastic4play.services.{ QueryDSL, QueryDef, Role } import org.elastic4play.services.JsonFormat.queryReads +import org.elastic4play.services.{ QueryDSL, QueryDef, Role } +import org.elastic4play.models.JsonFormat.baseModelEntityWrites +import play.api.http.Status +import play.api.mvc.{ Action, AnyContent, Controller } import services.LogSrv +import scala.concurrent.ExecutionContext + @Singleton class LogCtrl @Inject() ( logSrv: LogSrv, @@ -49,7 +49,7 @@ class LogCtrl @Inject() ( def findInTask(taskId: String): Action[Fields] = authenticated(Role.read).async(fieldsBodyParser) { implicit request ⇒ import org.elastic4play.services.QueryDSL._ val childQuery = request.body.getValue("query").fold[QueryDef](QueryDSL.any)(_.as[QueryDef]) - val query = and(childQuery, "_parent" ~= taskId) + val query = and(childQuery, parent("case_task", withId(taskId))) val range = request.body.getString("range") val sort = request.body.getStrings("sort").getOrElse(Nil) diff --git a/thehive-backend/app/models/Alert.scala b/thehive-backend/app/models/Alert.scala index 63b35ec76f..b6cb770234 100644 --- a/thehive-backend/app/models/Alert.scala +++ b/thehive-backend/app/models/Alert.scala @@ -41,7 +41,7 @@ trait AlertAttributes { Attribute("alert", "remoteAttachment", OptionalAttributeFormat(F.objectFmt(remoteAttachmentAttributes)), Nil, None, ""), Attribute("alert", "tlp", OptionalAttributeFormat(F.numberFmt), Nil, None, ""), Attribute("alert", "tags", MultiAttributeFormat(F.stringFmt), Nil, None, ""), - Attribute("alert", "ioc", OptionalAttributeFormat(F.stringFmt), Nil, None, "")) + Attribute("alert", "ioc", OptionalAttributeFormat(F.booleanFmt), Nil, None, "")) } val alertId: A[String] = attribute("_id", F.stringFmt, "Alert id", O.readonly) diff --git a/thehive-backend/app/services/AlertSrv.scala b/thehive-backend/app/services/AlertSrv.scala index f75ab78607..2ee217216a 100644 --- a/thehive-backend/app/services/AlertSrv.scala +++ b/thehive-backend/app/services/AlertSrv.scala @@ -11,12 +11,14 @@ import models._ import org.elastic4play.InternalError import org.elastic4play.controllers.{ Fields, FileInputValue } import org.elastic4play.services.JsonFormat.attachmentFormat +import org.elastic4play.services.QueryDSL.{ groupByField, parent, selectCount, withId } import org.elastic4play.services._ import play.api.libs.json._ import play.api.{ Configuration, Logger } import scala.collection.immutable import scala.concurrent.{ ExecutionContext, Future } +import scala.util.matching.Regex import scala.util.{ Failure, Try } trait AlertTransformer { @@ -28,7 +30,7 @@ trait AlertTransformer { case class CaseSimilarity(caze: Case, similarIOCCount: Int, iocCount: Int, similarArtifactCount: Int, artifactCount: Int) object AlertSrv { - val dataExtractor = "^(.*);(.*);(.*)".r + val dataExtractor: Regex = "^(.*);(.*);(.*)".r } class AlertSrv( @@ -80,6 +82,7 @@ class AlertSrv( mat) private[AlertSrv] lazy val logger = Logger(getClass) + import AlertSrv._ def create(fields: Fields)(implicit authContext: AuthContext): Future[Alert] = { @@ -158,7 +161,11 @@ class AlertSrv( case Some(id) ⇒ caseSrv.get(id) case None ⇒ connectors.get(alert.tpe()) match { - case Some(connector: AlertTransformer) ⇒ connector.createCase(alert, customCaseTemplate) + case Some(connector: AlertTransformer) ⇒ + for { + caze ← connector.createCase(alert, customCaseTemplate) + _ ← setCase(alert, caze) + } yield caze case _ ⇒ for { caseTemplate ← getCaseTemplate(alert, customCaseTemplate) @@ -171,61 +178,79 @@ class AlertSrv( .set("tlp", JsNumber(alert.tlp())) .set("status", CaseStatus.Open.toString), caseTemplate) - _ ← mergeWithCase(alert, caze) + _ ← importArtifacts(alert, caze) + _ ← setCase(alert, caze) } yield caze } } } override def mergeWithCase(alert: Alert, caze: Case)(implicit authContext: AuthContext): Future[Case] = { - setCase(alert, caze) - .map { _ ⇒ - val artifactsFields = alert.artifacts() - .map { artifact ⇒ - val tags = (artifact \ "tags").asOpt[Seq[JsString]].getOrElse(Nil) :+ JsString("src:" + alert.tpe()) - val message = (artifact \ "message").asOpt[JsString].getOrElse(JsString("")) - val artifactFields = Fields(artifact + - ("tags" → JsArray(tags)) + - ("message" → message)) - if (artifactFields.getString("dataType").contains("file")) { - artifactFields.getString("data") - .map { - case dataExtractor(filename, contentType, data) ⇒ - val f = Files.createTempFile("alert-", "-attachment") - Files.write(f, java.util.Base64.getDecoder.decode(data)) - artifactFields - .set("attachment", FileInputValue(filename, f, contentType)) - .unset("data") - case data ⇒ - logger.warn(s"Invalid data format for file artifact: $data") - artifactFields - } - .getOrElse(artifactFields) - } - else { - artifactFields - } - } + alert.caze() match { + case Some(id) ⇒ caseSrv.get(id) + case None ⇒ + connectors.get(alert.tpe()) match { + case Some(connector: AlertTransformer) ⇒ + for { + updatedCase ← connector.mergeWithCase(alert, caze) + _ ← setCase(alert, updatedCase) + } yield updatedCase + case _ ⇒ + for { + _ ← importArtifacts(alert, caze) + description = caze.description() + s"\n \n#### Merged with alert #${alert.sourceRef()} ${alert.title()}\n\n${alert.description().trim}" + updatedCase ← caseSrv.update(caze, Fields.empty.set("description", description)) + _ ← setCase(alert, caze) + } yield updatedCase + } + } + } - artifactSrv.create(caze, artifactsFields) - .map { - _.foreach { - case Failure(e) ⇒ logger.warn("Create artifact error", e) - case _ ⇒ + def importArtifacts(alert: Alert, caze: Case)(implicit authContext: AuthContext): Future[Case] = { + val artifactsFields = alert.artifacts() + .map { artifact ⇒ + val tags = (artifact \ "tags").asOpt[Seq[JsString]].getOrElse(Nil) :+ JsString("src:" + alert.tpe()) + val message = (artifact \ "message").asOpt[JsString].getOrElse(JsString("")) + val artifactFields = Fields(artifact + + ("tags" → JsArray(tags)) + + ("message" → message)) + if (artifactFields.getString("dataType").contains("file")) { + artifactFields.getString("data") + .map { + case dataExtractor(filename, contentType, data) ⇒ + val f = Files.createTempFile("alert-", "-attachment") + Files.write(f, java.util.Base64.getDecoder.decode(data)) + artifactFields + .set("attachment", FileInputValue(filename, f, contentType)) + .unset("data") + case data ⇒ + logger.warn(s"Invalid data format for file artifact: $data") + artifactFields } - } - .onComplete { _ ⇒ - // remove temporary files - artifactsFields - .flatMap(_.get("Attachment")) - .foreach { - case FileInputValue(_, file, _) ⇒ Files.delete(file) - case _ ⇒ - } - } - caze + .getOrElse(artifactFields) + } + else { + artifactFields + } } + val updatedCase = artifactSrv.create(caze, artifactsFields) + .map { artifacts ⇒ + artifacts.collect { + case Failure(e) ⇒ logger.warn("Create artifact error", e) + } + caze + } + updatedCase.onComplete { _ ⇒ + // remove temporary files + artifactsFields + .flatMap(_.get("Attachment")) + .foreach { + case FileInputValue(_, file, _) ⇒ Files.delete(file) + case _ ⇒ + } + } + updatedCase } def setCase(alert: Alert, caze: Case)(implicit authContext: AuthContext): Future[Alert] = { @@ -256,16 +281,6 @@ class AlertSrv( } yield artifactSrv.findSimilar(dataType, data, None, Some("all"), Nil)._1 } - def getCaseAndArtifactCount(caseId: String): Future[(Case, Int, Int)] = { - import org.elastic4play.services.QueryDSL._ - for { - caze ← caseSrv.get(caseId) - artifactCountJs ← artifactSrv.stats(parent("case", withId(caseId)), Seq(groupByField("ioc", selectCount))) - iocCount = (artifactCountJs \ "1" \ "count").asOpt[Int].getOrElse(0) - artifactCount = (artifactCountJs \\ "count").map(_.as[Int]).sum - } yield (caze, iocCount, artifactCount) - } - Source(alert.artifacts().to[immutable.Iterable]) .flatMapConcat { artifact ⇒ similarArtifacts(artifact) @@ -273,18 +288,27 @@ class AlertSrv( } .groupBy(100, _.parentId) .map { - case a if a.ioc() ⇒ (a.parentId, 1, 1) - case a ⇒ (a.parentId, 0, 1) + case a if a.ioc() ⇒ (a.parentId.getOrElse(sys.error("Artifact without case !")), 1, 1) + case a ⇒ (a.parentId.getOrElse(sys.error("Artifact without case !")), 0, 1) } - .reduce[(Option[String], Int, Int)] { - case ((caze, iocCount1, artifactCount1), (_, iocCount2, artifactCount2)) ⇒ (caze, iocCount1 + iocCount2, artifactCount1 + artifactCount2) + .reduce[(String, Int, Int)] { + case ((caseId, iocCount1, artifactCount1), (_, iocCount2, artifactCount2)) ⇒ (caseId, iocCount1 + iocCount2, artifactCount1 + artifactCount2) } .mergeSubstreams .mapAsyncUnordered(5) { - case (Some(caseId), similarIOCCount, similarArtifactCount) ⇒ - getCaseAndArtifactCount(caseId).map { - case (caze, iocCount, artifactCount) ⇒ CaseSimilarity(caze, similarIOCCount, iocCount, similarArtifactCount, artifactCount) - } + case (caseId, similarIOCCount, similarArtifactCount) ⇒ + caseSrv.get(caseId).map((_, similarIOCCount, similarArtifactCount)) + } + .filter { + case (caze, _, _) ⇒ caze.status() != CaseStatus.Deleted && caze.resolutionStatus != CaseResolutionStatus.Duplicated + } + .mapAsyncUnordered(5) { + case (caze, similarIOCCount, similarArtifactCount) ⇒ + for { + artifactCountJs ← artifactSrv.stats(parent("case", withId(caze.id)), Seq(groupByField("ioc", selectCount))) + iocCount = (artifactCountJs \ "1" \ "count").asOpt[Int].getOrElse(0) + artifactCount = (artifactCountJs \\ "count").map(_.as[Int]).sum + } yield CaseSimilarity(caze, similarIOCCount, iocCount, similarArtifactCount, artifactCount) case _ ⇒ Future.failed(InternalError("Case not found")) } .runWith(Sink.seq) diff --git a/thehive-backend/conf/routes b/thehive-backend/conf/routes index e7d927db73..884d0dc7da 100644 --- a/thehive-backend/conf/routes +++ b/thehive-backend/conf/routes @@ -44,6 +44,7 @@ PATCH /api/case/task/:taskId controllers.TaskCtrl.update(ta POST /api/case/:caseId/task controllers.TaskCtrl.create(caseId) GET /api/case/task/:taskId/log controllers.LogCtrl.findInTask(taskId) +POST /api/case/task/:taskId/log/_search controllers.LogCtrl.findInTask(taskId) POST /api/case/task/log/_search controllers.LogCtrl.find() POST /api/case/task/:taskId/log controllers.LogCtrl.create(taskId) PATCH /api/case/task/log/:logId controllers.LogCtrl.update(logId) diff --git a/thehive-cortex/app/connectors/cortex/services/CortexSrv.scala b/thehive-cortex/app/connectors/cortex/services/CortexSrv.scala index 6310664547..96b2e0e7e1 100644 --- a/thehive-cortex/app/connectors/cortex/services/CortexSrv.scala +++ b/thehive-cortex/app/connectors/cortex/services/CortexSrv.scala @@ -13,6 +13,7 @@ import connectors.cortex.models._ import models.Artifact import org.elastic4play.controllers.Fields import org.elastic4play.services._ +import org.elastic4play.services.JsonFormat.attachmentFormat import org.elastic4play.{ InternalError, NotFoundError } import play.api.libs.json.{ JsObject, Json } import play.api.libs.ws.WSClient @@ -257,7 +258,7 @@ class CortexSrv @Inject() ( "dataType" → artifact.dataType()) cortexArtifact = (artifact.data(), artifact.attachment()) match { case (Some(data), None) ⇒ DataArtifact(data, artifactAttributes) - case (None, Some(attachment)) ⇒ FileArtifact(attachmentSrv.source(attachment.id), artifactAttributes) + case (None, Some(attachment)) ⇒ FileArtifact(attachmentSrv.source(attachment.id), artifactAttributes + ("attachment" → Json.toJson(attachment))) case _ ⇒ throw InternalError(s"Artifact has invalid data : ${artifact.attributes}") } cortexJobJson ← cortex.analyze(analyzerId, cortexArtifact) diff --git a/thehive-misp/app/connectors/misp/MispSrv.scala b/thehive-misp/app/connectors/misp/MispSrv.scala index f9b541c3d2..6a5f4636d4 100644 --- a/thehive-misp/app/connectors/misp/MispSrv.scala +++ b/thehive-misp/app/connectors/misp/MispSrv.scala @@ -375,20 +375,27 @@ class MispSrv @Inject() ( for { caseTemplate ← alertSrv.getCaseTemplate(alert, customCaseTemplate) caze ← caseSrv.create(Fields(alert.toCaseJson), caseTemplate) - _ ← mergeWithCase(alert, caze) + _ ← importArtifacts(alert, caze) } yield caze } } - def mergeWithCase(alert: Alert, caze: Case)(implicit authContext: AuthContext): Future[Case] = { + def importArtifacts(alert: Alert, caze: Case)(implicit authContext: AuthContext): Future[Case] = { for { instanceConfig ← getInstanceConfig(alert.source()) - _ ← alertSrv.setCase(alert, caze) artifacts ← Future.sequence(alert.artifacts().flatMap(attributeToArtifact(instanceConfig, alert, _))) _ ← artifactSrv.create(caze, artifacts) } yield caze } + def mergeWithCase(alert: Alert, caze: Case)(implicit authContext: AuthContext): Future[Case] = { + for { + _ ← importArtifacts(alert, caze) + description = caze.description() + s"\n \n#### Merged with MISP event ${alert.title()}" + updatedCase ← caseSrv.update(caze, Fields.empty.set("description", description)) + } yield updatedCase + } + def updateMispAlertArtifact()(implicit authContext: AuthContext): Future[Unit] = { import org.elastic4play.services.QueryDSL._ logger.info("Update MISP attributes in alerts") diff --git a/ui/app/scripts/controllers/case/CaseTasksCtrl.js b/ui/app/scripts/controllers/case/CaseTasksCtrl.js index ac62a7fdf7..8d686c30dd 100644 --- a/ui/app/scripts/controllers/case/CaseTasksCtrl.js +++ b/ui/app/scripts/controllers/case/CaseTasksCtrl.js @@ -85,7 +85,8 @@ // open task tab with its details $scope.startTask = function(task) { if (task.status === 'Waiting') { - $scope.updateTaskStatus(task.id, 'InProgress'); + $scope.updateTaskStatus(task.id, 'InProgress') + .then($scope.showTask); } else { $scope.showTask(task); } diff --git a/ui/app/scripts/directives/fileChooser.js b/ui/app/scripts/directives/fileChooser.js index 3f33d904c0..be74bc84bb 100644 --- a/ui/app/scripts/directives/fileChooser.js +++ b/ui/app/scripts/directives/fileChooser.js @@ -10,11 +10,9 @@ $(element[0].children[0]).remove(); // create a Dropzone for the element with the given options dropzone = new Dropzone(element[0], { - // 'clickable' : '.dz-clickable', 'url': 'dummy', 'autoProcessQueue': false, 'maxFiles': 1, - // 'addRemoveLinks' : true, 'createImageThumbnails': (angular.isString(scope.preview)) ? (scope.preview === 'true') : true, 'acceptedFiles': (angular.isString(scope.accept)) ? scope.accept : undefined, 'previewTemplate': template @@ -26,12 +24,19 @@ }); }); dropzone.on('removedfile', function() { - setTimeout(function() { + var files = this.files; + + if(files && files.length !== 1) { + setTimeout(function() { + scope.$apply(function() { + delete scope.filemodel; + }); + }, 0); + } else { scope.$apply(function() { - delete scope.filemodel; - // scope.filemodel = undefined; + scope.filemodel = files[0]; }); - }, 0); + } }); dropzone.on('maxfilesexceeded', function(file) { this.removeFile(file); diff --git a/ui/app/scripts/services/StreamSrv.js b/ui/app/scripts/services/StreamSrv.js index 68aef07d0b..f4920f5a5b 100644 --- a/ui/app/scripts/services/StreamSrv.js +++ b/ui/app/scripts/services/StreamSrv.js @@ -23,10 +23,13 @@ var byRootIds = {}; var byObjectTypes = {}; var byRootIdsWithObjectTypes = {}; + var bySecondaryObjectTypes = {}; + angular.forEach(data, function(message) { var rootId = message.base.rootId; var objectType = message.base.objectType; var rootIdWithObjectType = rootId + '|' + objectType; + var secondaryObjectTypes = message.summary ? _.without(_.keys(message.summary), objectType) : []; if (rootId in byRootIds) { byRootIds[rootId].push(message); @@ -45,6 +48,15 @@ } else { byRootIdsWithObjectTypes[rootIdWithObjectType] = [message]; } + + _.each(secondaryObjectTypes, function(type) { + if (type in bySecondaryObjectTypes) { + bySecondaryObjectTypes[type].push(message); + } else { + bySecondaryObjectTypes[type] = [message]; + } + }); + }); angular.forEach(byRootIds, function(messages, rootId) { @@ -53,6 +65,12 @@ angular.forEach(byObjectTypes, function(messages, objectType) { self.runCallbacks('any', objectType, messages); }); + + // Trigger strem event for sub object types + angular.forEach(bySecondaryObjectTypes, function(messages, objectType) { + self.runCallbacks('any', objectType, messages); + }); + angular.forEach(byRootIdsWithObjectTypes, function(messages, rootIdWithObjectType) { var temp = rootIdWithObjectType.split('|', 2), rootId = temp[0], diff --git a/ui/app/views/directives/dropzone.html b/ui/app/views/directives/dropzone.html index e4d2f68260..3ca3555a2b 100644 --- a/ui/app/views/directives/dropzone.html +++ b/ui/app/views/directives/dropzone.html @@ -1,11 +1,11 @@
-
Drop file or click -
\ No newline at end of file + diff --git a/ui/app/views/partials/admin/users.html b/ui/app/views/partials/admin/users.html index 003c628f2f..0eabd04488 100644 --- a/ui/app/views/partials/admin/users.html +++ b/ui/app/views/partials/admin/users.html @@ -57,64 +57,64 @@

User management

- + {{user.id}} - -
- + +
+
+ +
+ + + + + New password +
+
+ + + + +
+
+
+ + Create API Key + Show API Key + {{usrKey[user.id]}} - - - -
- -
- - - - New password -
-
- - - + + + +
+ + + + -
-
-
- - Create API Key - Show API Key - {{usrKey[user.id]}} - - - - - -
- - - - - -
- - + + + diff --git a/ui/app/views/partials/case/case.tasks.item.html b/ui/app/views/partials/case/case.tasks.item.html index dbe9f988a4..d89bb2f306 100644 --- a/ui/app/views/partials/case/case.tasks.item.html +++ b/ui/app/views/partials/case/case.tasks.item.html @@ -98,7 +98,7 @@

Task logs

-
+
diff --git a/ui/bower.json b/ui/bower.json index 627e853c17..f5b2636fcb 100644 --- a/ui/bower.json +++ b/ui/bower.json @@ -1,6 +1,6 @@ { "name": "thehive", - "version": "2.12.0", + "version": "2.12.1", "license": "AGPL-3.0", "dependencies": { "angular": "1.5.8", diff --git a/ui/package.json b/ui/package.json index 88d3d49694..f32f835bb9 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,6 +1,6 @@ { "name": "thehive", - "version": "2.12.0", + "version": "2.12.1", "license": "AGPL-3.0", "repository": { "type": "git", diff --git a/version.sbt b/version.sbt index c02664b30a..e0fc553286 100644 --- a/version.sbt +++ b/version.sbt @@ -1 +1 @@ -version in ThisBuild := "2.12.0" +version in ThisBuild := "2.12.1"