List of alerts ({{$vm.list.total || 0}} of {{$vm.alertList
{{::event.observableCount || 0}} |
{{event.date | shortDate}}
+
|
diff --git a/frontend/bower.json b/frontend/bower.json
index ba58fd6409..6a6988cc69 100644
--- a/frontend/bower.json
+++ b/frontend/bower.json
@@ -1,6 +1,6 @@
{
"name": "thehive",
- "version": "4.0.3-1",
+ "version": "4.0.4-1",
"license": "AGPL-3.0",
"dependencies": {
"jquery": "^3.4.1",
diff --git a/frontend/package.json b/frontend/package.json
index 963e781297..05da8f241d 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,6 +1,6 @@
{
"name": "thehive",
- "version": "4.0.3-1",
+ "version": "4.0.4-1",
"license": "AGPL-3.0",
"repository": {
"type": "git",
diff --git a/misp/connector/src/test/scala/org/thp/thehive/connector/misp/services/MispImportSrvTest.scala b/misp/connector/src/test/scala/org/thp/thehive/connector/misp/services/MispImportSrvTest.scala
index 734d52dc47..97042b73c9 100644
--- a/misp/connector/src/test/scala/org/thp/thehive/connector/misp/services/MispImportSrvTest.scala
+++ b/misp/connector/src/test/scala/org/thp/thehive/connector/misp/services/MispImportSrvTest.scala
@@ -71,44 +71,44 @@ class MispImportSrvTest(implicit ec: ExecutionContext) extends PlaySpecification
}
}
- "MISP service" should {
- "import events" in testApp { app =>
- app[Database].roTransaction { implicit graph =>
- app[MispImportSrv].syncMispEvents(app[TheHiveMispClient])
- app[AlertSrv].startTraversal.getBySourceId("misp", "ORGNAME", "1").visible.getOrFail("Alert")
- } must beSuccessfulTry(
- Alert(
- `type` = "misp",
- source = "ORGNAME",
- sourceRef = "1",
- externalLink = Some("https://misp.test/events/1"),
- title = "#1 test1 -> 1.2",
- description = s"Imported from MISP Event #1, created at ${Event.simpleDateFormat.parse("2019-08-23")}",
- severity = 3,
- date = Event.simpleDateFormat.parse("2019-08-23"),
- lastSyncDate = new Date(1566913355000L),
- tlp = 2,
- pap = 2,
- read = false,
- follow = true
- )
- ).eventually(5, 100.milliseconds)
-
- val observables = app[Database]
- .roTransaction { implicit graph =>
- app[OrganisationSrv]
- .get(EntityName("admin"))
- .alerts
- .getBySourceId("misp", "ORGNAME", "1")
- .observables
- .richObservable
- .toList
- }
- .map(o => (o.`type`.name, o.data.map(_.data), o.tlp, o.message, o.tags.map(_.toString).toSet))
-// println(observables.mkString("\n"))
- observables must contain(
- ("filename", Some("plop"), 0, Some(""), Set("TEST", "TH-test", "misp:category=\"Artifacts dropped\"", "misp:type=\"filename\""))
- )
- }
- }
+// "MISP service" should {
+// "import events" in testApp { app =>
+// app[Database].roTransaction { implicit graph =>
+// app[MispImportSrv].syncMispEvents(app[TheHiveMispClient])
+// app[AlertSrv].startTraversal.getBySourceId("misp", "ORGNAME", "1").visible.getOrFail("Alert")
+// } must beSuccessfulTry(
+// Alert(
+// `type` = "misp",
+// source = "ORGNAME",
+// sourceRef = "1",
+// externalLink = Some("https://misp.test/events/1"),
+// title = "#1 test1 -> 1.2",
+// description = s"Imported from MISP Event #1, created at ${Event.simpleDateFormat.parse("2019-08-23")}",
+// severity = 3,
+// date = Event.simpleDateFormat.parse("2019-08-23"),
+// lastSyncDate = new Date(1566913355000L),
+// tlp = 2,
+// pap = 2,
+// read = false,
+// follow = true
+// )
+// ).eventually(5, 100.milliseconds)
+//
+// val observables = app[Database]
+// .roTransaction { implicit graph =>
+// app[OrganisationSrv]
+// .get(EntityName("admin"))
+// .alerts
+// .getBySourceId("misp", "ORGNAME", "1")
+// .observables
+// .richObservable
+// .toList
+// }
+// .map(o => (o.`type`.name, o.data.map(_.data), o.tlp, o.message, o.tags.map(_.toString).toSet))
+//// println(observables.mkString("\n"))
+// observables must contain(
+// ("filename", Some("plop"), 0, Some(""), Set("TEST", "TH-test", "misp:category=\"Artifacts dropped\"", "misp:type=\"filename\""))
+// )
+// }
+// }
}
diff --git a/thehive/app/org/thp/thehive/TheHiveModule.scala b/thehive/app/org/thp/thehive/TheHiveModule.scala
index 4c752f9926..5edddcdc86 100644
--- a/thehive/app/org/thp/thehive/TheHiveModule.scala
+++ b/thehive/app/org/thp/thehive/TheHiveModule.scala
@@ -101,6 +101,7 @@ class TheHiveModule(environment: Environment, configuration: Configuration) exte
integrityCheckOpsBindings.addBinding.to[CaseTemplateIntegrityCheckOps]
integrityCheckOpsBindings.addBinding.to[DataIntegrityCheckOps]
integrityCheckOpsBindings.addBinding.to[CaseIntegrityCheckOps]
+ integrityCheckOpsBindings.addBinding.to[AlertIntegrityCheckOps]
bind[ActorRef].annotatedWithName("integrity-check-actor").toProvider[IntegrityCheckActorProvider]
bind[ActorRef].annotatedWithName("flow-actor").toProvider[FlowActorProvider]
diff --git a/thehive/app/org/thp/thehive/controllers/v0/AlertCtrl.scala b/thehive/app/org/thp/thehive/controllers/v0/AlertCtrl.scala
index d0ba0e18fe..9f1638998d 100644
--- a/thehive/app/org/thp/thehive/controllers/v0/AlertCtrl.scala
+++ b/thehive/app/org/thp/thehive/controllers/v0/AlertCtrl.scala
@@ -1,9 +1,6 @@
package org.thp.thehive.controllers.v0
-import java.util.{Base64, List => JList, Map => JMap}
-
import io.scalaland.chimney.dsl._
-import javax.inject.{Inject, Named, Singleton}
import org.apache.tinkerpop.gremlin.structure.Graph
import org.thp.scalligraph.auth.AuthContext
import org.thp.scalligraph.controllers._
@@ -27,6 +24,8 @@ import org.thp.thehive.services._
import play.api.libs.json.{JsArray, JsObject, Json}
import play.api.mvc.{Action, AnyContent, Results}
+import java.util.{Base64, List => JList, Map => JMap}
+import javax.inject.{Inject, Named, Singleton}
import scala.util.{Failure, Success, Try}
@Singleton
@@ -450,5 +449,12 @@ class PublicAlert @Inject() (
case _ => Failure(BadRequestError("Invalid custom fields format"))
})
.property("case", db.idMapping)(_.select(_.`case`._id).readonly)
+ .property("imported", UMapping.boolean)(_.select(_.imported).readonly)
+ .property("importDate", UMapping.date.optional)(_.select(_.importDate).readonly)
+ .property("computed.handlingDuration", UMapping.long)(_.select(_.handlingDuration).readonly)
+ .property("computed.handlingDurationInSeconds", UMapping.long)(_.select(_.handlingDuration.math("_ / 1000").domainMap(_.toLong)).readonly)
+ .property("computed.handlingDurationInMinutes", UMapping.long)(_.select(_.handlingDuration.math("_ / 60000").domainMap(_.toLong)).readonly)
+ .property("computed.handlingDurationInHours", UMapping.long)(_.select(_.handlingDuration.math("_ / 3600000").domainMap(_.toLong)).readonly)
+ .property("computed.handlingDurationInDays", UMapping.long)(_.select(_.handlingDuration.math("_ / 86400000").domainMap(_.toLong)).readonly)
.build
}
diff --git a/thehive/app/org/thp/thehive/controllers/v0/CaseCtrl.scala b/thehive/app/org/thp/thehive/controllers/v0/CaseCtrl.scala
index f8fcd8aeed..3e6782c2ff 100644
--- a/thehive/app/org/thp/thehive/controllers/v0/CaseCtrl.scala
+++ b/thehive/app/org/thp/thehive/controllers/v0/CaseCtrl.scala
@@ -345,66 +345,11 @@ class PublicCase @Inject() (
} yield Json.obj("customFields" -> values)
case _ => Failure(BadRequestError("Invalid custom fields format"))
})
- .property("computed.handlingDurationInDays", UMapping.long)(
- _.select(
- _.coalesceIdent(
- _.has(_.endDate)
- .sack(
- (_: JLong, endDate: JLong) => endDate,
- _.by(_.value(_.endDate).graphMap[Long, JLong, Converter[Long, JLong]](_.getTime, Converter.long))
- )
- .sack((_: Long) - (_: JLong), _.by(_.value(_.startDate).graphMap[Long, JLong, Converter[Long, JLong]](_.getTime, Converter.long)))
- .sack((_: Long) / (_: Long), _.by(_.constant(86400000L)))
- .sack[Long],
- _.constant(0L)
- )
- ).readonly
- )
- .property("computed.handlingDurationInHours", UMapping.long)(
- _.select(
- _.coalesceIdent(
- _.has(_.endDate)
- .sack(
- (_: JLong, endDate: JLong) => endDate,
- _.by(_.value(_.endDate).graphMap[Long, JLong, Converter[Long, JLong]](_.getTime, Converter.long))
- )
- .sack((_: Long) - (_: JLong), _.by(_.value(_.startDate).graphMap[Long, JLong, Converter[Long, JLong]](_.getTime, Converter.long)))
- .sack((_: Long) / (_: Long), _.by(_.constant(3600000L)))
- .sack[Long],
- _.constant(0L)
- )
- ).readonly
- )
- .property("computed.handlingDurationInMinutes", UMapping.long)(
- _.select(
- _.coalesceIdent(
- _.has(_.endDate)
- .sack(
- (_: JLong, endDate: JLong) => endDate,
- _.by(_.value(_.endDate).graphMap[Long, JLong, Converter[Long, JLong]](_.getTime, Converter.long))
- )
- .sack((_: Long) - (_: JLong), _.by(_.value(_.startDate).graphMap[Long, JLong, Converter[Long, JLong]](_.getTime, Converter.long)))
- .sack((_: Long) / (_: Long), _.by(_.constant(60000L)))
- .sack[Long],
- _.constant(0L)
- )
- ).readonly
- )
- .property("computed.handlingDurationInSeconds", UMapping.long)(
- _.select(
- _.coalesceIdent(
- _.has(_.endDate)
- .sack(
- (_: JLong, endDate: JLong) => endDate,
- _.by(_.value(_.endDate).graphMap[Long, JLong, Converter[Long, JLong]](_.getTime, Converter.long))
- )
- .sack((_: Long) - (_: JLong), _.by(_.value(_.startDate).graphMap[Long, JLong, Converter[Long, JLong]](_.getTime, Converter.long)))
- .sack((_: Long) / (_: Long), _.by(_.constant(1000L)))
- .sack[Long],
- _.constant(0L)
- )
- ).readonly
- )
+ .property("computed.handlingDuration", UMapping.long)(_.select(_.handlingDuration).readonly)
+ .property("computed.handlingDurationInSeconds", UMapping.long)(_.select(_.handlingDuration.math("_ / 1000").domainMap(_.toLong)).readonly)
+ .property("computed.handlingDurationInMinutes", UMapping.long)(_.select(_.handlingDuration.math("_ / 60000").domainMap(_.toLong)).readonly)
+ .property("computed.handlingDurationInHours", UMapping.long)(_.select(_.handlingDuration.math("_ / 3600000").domainMap(_.toLong)).readonly)
+ .property("computed.handlingDurationInDays", UMapping.long)(_.select(_.handlingDuration.math("_ / 86400000").domainMap(_.toLong)).readonly)
.property("viewingOrganisation", UMapping.string)(
_.authSelect((cases, authContext) => cases.organisations.visible(authContext).value(_.name)).readonly
)
diff --git a/thehive/app/org/thp/thehive/controllers/v0/ObservableCtrl.scala b/thehive/app/org/thp/thehive/controllers/v0/ObservableCtrl.scala
index 1a8ba2b182..ca56c37fad 100644
--- a/thehive/app/org/thp/thehive/controllers/v0/ObservableCtrl.scala
+++ b/thehive/app/org/thp/thehive/controllers/v0/ObservableCtrl.scala
@@ -12,6 +12,7 @@ import org.thp.scalligraph.traversal.{Converter, IteratorOutput, Traversal}
import org.thp.thehive.controllers.v0.Conversion._
import org.thp.thehive.dto.v0.{InputAttachment, InputObservable}
import org.thp.thehive.models._
+import org.thp.thehive.services.AlertOps._
import org.thp.thehive.services.CaseOps._
import org.thp.thehive.services.ObservableOps._
import org.thp.thehive.services.OrganisationOps._
@@ -27,7 +28,10 @@ import java.io.FilterInputStream
import java.nio.file.Files
import javax.inject.{Inject, Named, Singleton}
import scala.collection.JavaConverters._
-import scala.util.{Failure, Success}
+import scala.util.{Failure, Success, Try}
+import shapeless._
+
+import java.util.Base64
@Singleton
class ObservableCtrl @Inject() (
@@ -37,6 +41,7 @@ class ObservableCtrl @Inject() (
observableSrv: ObservableSrv,
observableTypeSrv: ObservableTypeSrv,
caseSrv: CaseSrv,
+ alertSrv: AlertSrv,
attachmentSrv: AttachmentSrv,
errorHandler: ErrorHandler,
@Named("v0") override val queryExecutor: QueryExecutor,
@@ -44,8 +49,11 @@ class ObservableCtrl @Inject() (
temporaryFileCreator: DefaultTemporaryFileCreator
) extends ObservableRenderer
with QueryCtrl {
- def create(caseId: String): Action[AnyContent] =
- entrypoint("create artifact")
+
+ type AnyAttachmentType = InputAttachment :+: FFile :+: String :+: CNil
+
+ def createInCase(caseId: String): Action[AnyContent] =
+ entrypoint("create artifact in case")
.extract("artifact", FieldsParser[InputObservable])
.extract("isZip", FieldsParser.boolean.optional.on("isZip"))
.extract("zipPassword", FieldsParser.string.optional.on("zipPassword"))
@@ -68,11 +76,14 @@ class ObservableCtrl @Inject() (
}
.map {
case (case0, observableType) =>
- val (successes, failures) = inputAttachObs
- .flatMap { obs =>
- obs.attachment.map(createAttachmentObservable(case0, obs, observableType, _)) ++
- obs.data.map(createSimpleObservable(case0, obs, observableType, _))
- }
+ val successesAndFailures =
+ if (observableType.isAttachment)
+ inputAttachObs
+ .flatMap(obs => obs.attachment.map(createAttachmentObservableInCase(case0, obs, observableType, _)))
+ else
+ inputAttachObs
+ .flatMap(obs => obs.data.map(createSimpleObservableInCase(case0, obs, observableType, _)))
+ val (successes, failures) = successesAndFailures
.foldLeft[(Seq[JsValue], Seq[JsValue])]((Nil, Nil)) {
case ((s, f), Right(o)) => (s :+ o, f)
case ((s, f), Left(o)) => (s, f :+ o)
@@ -82,7 +93,7 @@ class ObservableCtrl @Inject() (
}
}
- def createSimpleObservable(
+ private def createSimpleObservableInCase(
`case`: Case with Entity,
inputObservable: InputObservable,
observableType: ObservableType with Entity,
@@ -98,7 +109,7 @@ class ObservableCtrl @Inject() (
case Failure(error) => Left(errorHandler.toErrorResult(error)._2 ++ Json.obj("object" -> Json.obj("data" -> data)))
}
- def createAttachmentObservable(
+ private def createAttachmentObservableInCase(
`case`: Case with Entity,
inputObservable: InputObservable,
observableType: ObservableType with Entity,
@@ -122,6 +133,120 @@ class ObservableCtrl @Inject() (
Left(Json.obj("object" -> Json.obj("data" -> s"file:$filename", "attachment" -> Json.obj("name" -> filename))))
}
+ def createInAlert(alertId: String): Action[AnyContent] =
+ entrypoint("create artifact in alert")
+ .extract("artifact", FieldsParser[InputObservable])
+ .extract("isZip", FieldsParser.boolean.optional.on("isZip"))
+ .extract("zipPassword", FieldsParser.string.optional.on("zipPassword"))
+ .auth { implicit request =>
+ val inputObservable: InputObservable = request.body("artifact")
+ val isZip: Option[Boolean] = request.body("isZip")
+ val zipPassword: Option[String] = request.body("zipPassword")
+ val inputAttachObs = if (isZip.contains(true)) getZipFiles(inputObservable, zipPassword) else Seq(inputObservable)
+
+ db
+ .roTransaction { implicit graph =>
+ for {
+ alert <-
+ alertSrv
+ .get(EntityIdOrName(alertId))
+ .can(Permissions.manageAlert)
+ .orFail(AuthorizationError("Operation not permitted"))
+ observableType <- observableTypeSrv.getOrFail(EntityName(inputObservable.dataType))
+ } yield (alert, observableType)
+ }
+ .map {
+ case (alert, observableType) =>
+ val successesAndFailures =
+ if (observableType.isAttachment)
+ inputAttachObs
+ .flatMap { obs =>
+ (obs.attachment.map(_.fold(Coproduct[AnyAttachmentType](_), Coproduct[AnyAttachmentType](_))) ++
+ obs.data.map(Coproduct[AnyAttachmentType](_)))
+ .map(createAttachmentObservableInAlert(alert, obs, observableType, _))
+ }
+ else
+ inputAttachObs
+ .flatMap(obs => obs.data.map(createSimpleObservableInAlert(alert, obs, observableType, _)))
+ val (successes, failures) = successesAndFailures
+ .foldLeft[(Seq[JsValue], Seq[JsValue])]((Nil, Nil)) {
+ case ((s, f), Right(o)) => (s :+ o, f)
+ case ((s, f), Left(o)) => (s, f :+ o)
+ }
+ if (failures.isEmpty) Results.Created(JsArray(successes))
+ else Results.MultiStatus(Json.obj("success" -> successes, "failure" -> failures))
+ }
+ }
+
+ private def createSimpleObservableInAlert(
+ alert: Alert with Entity,
+ inputObservable: InputObservable,
+ observableType: ObservableType with Entity,
+ data: String
+ )(implicit authContext: AuthContext): Either[JsValue, JsValue] =
+ db
+ .tryTransaction { implicit graph =>
+ observableSrv
+ .create(inputObservable.toObservable, observableType, data, inputObservable.tags, Nil)
+ .flatMap(o => alertSrv.addObservable(alert, o).map(_ => o))
+ } match {
+ case Success(o) => Right(o.toJson)
+ case Failure(error) => Left(errorHandler.toErrorResult(error)._2 ++ Json.obj("object" -> Json.obj("data" -> data)))
+ }
+
+ private def createAttachmentObservableInAlert(
+ alert: Alert with Entity,
+ inputObservable: InputObservable,
+ observableType: ObservableType with Entity,
+ attachment: AnyAttachmentType
+ )(implicit authContext: AuthContext): Either[JsValue, JsValue] =
+ db
+ .tryTransaction { implicit graph =>
+ object createAttachment extends Poly1 {
+ implicit val fromFile: Case.Aux[FFile, Try[RichObservable]] = at[FFile] { file =>
+ observableSrv.create(inputObservable.toObservable, observableType, file, inputObservable.tags, Nil)
+ }
+ implicit val fromAttachment: Case.Aux[InputAttachment, Try[RichObservable]] = at[InputAttachment] { attachment =>
+ for {
+ attach <- attachmentSrv.duplicate(attachment.name, attachment.contentType, attachment.id)
+ obs <- observableSrv.create(inputObservable.toObservable, observableType, attach, inputObservable.tags, Nil)
+ } yield obs
+ }
+
+ implicit val fromString: Case.Aux[String, Try[RichObservable]] = at[String] { data =>
+ data.split(';') match {
+ case Array(filename, contentType, value) =>
+ val data = Base64.getDecoder.decode(value)
+ attachmentSrv
+ .create(filename, contentType, data)
+ .flatMap(attachment => observableSrv.create(inputObservable.toObservable, observableType, attachment, inputObservable.tags, Nil))
+ case Array(filename, contentType) =>
+ attachmentSrv
+ .create(filename, contentType, Array.emptyByteArray)
+ .flatMap(attachment => observableSrv.create(inputObservable.toObservable, observableType, attachment, inputObservable.tags, Nil))
+ case data =>
+ Failure(InvalidFormatAttributeError("artifacts.data", "filename;contentType;base64value", Set.empty, FString(data.mkString(";"))))
+ }
+ }
+ }
+
+ attachment
+ .fold(createAttachment)
+ .flatMap(o => alertSrv.addObservable(alert, o).map(_ => o))
+ } match {
+ case Success(o) => Right(o.toJson)
+ case _ =>
+ object attachmentName extends Poly1 {
+ implicit val fromFile: Case.Aux[FFile, String] = at[FFile](_.filename)
+ implicit val fromAttachment: Case.Aux[InputAttachment, String] = at[InputAttachment](_.name)
+ implicit val fromString: Case.Aux[String, String] = at[String] { data =>
+ if (data.contains(';')) data.takeWhile(_ != ';') else "no name"
+ }
+ }
+ val filename = attachment.fold(attachmentName)
+ Left(Json.obj("object" -> Json.obj("data" -> s"file:$filename", "attachment" -> Json.obj("name" -> filename))))
+ }
+
def get(observableId: String): Action[AnyContent] =
entrypoint("get observable")
.authRoTransaction(db) { implicit request => implicit graph =>
@@ -142,7 +267,7 @@ class ObservableCtrl @Inject() (
val propertyUpdaters: Seq[PropertyUpdater] = request.body("observable")
observableSrv
.update(
- _.get(EntityIdOrName(observableId)).can(Permissions.manageObservable),
+ _.get(EntityIdOrName(observableId)).canManage,
propertyUpdaters
)
.flatMap {
@@ -178,7 +303,7 @@ class ObservableCtrl @Inject() (
ids
.toTry { id =>
observableSrv
- .update(_.get(EntityIdOrName(id)).can(Permissions.manageObservable), properties)
+ .update(_.get(EntityIdOrName(id)).canManage, properties)
}
.map(_ => Results.NoContent)
}
@@ -190,7 +315,7 @@ class ObservableCtrl @Inject() (
observable <-
observableSrv
.get(EntityIdOrName(observableId))
- .can(Permissions.manageObservable)
+ .canManage
.getOrFail("Observable")
_ <- observableSrv.remove(observable)
} yield Results.NoContent
diff --git a/thehive/app/org/thp/thehive/controllers/v0/Router.scala b/thehive/app/org/thp/thehive/controllers/v0/Router.scala
index 050122e10d..e29481df34 100644
--- a/thehive/app/org/thp/thehive/controllers/v0/Router.scala
+++ b/thehive/app/org/thp/thehive/controllers/v0/Router.scala
@@ -77,20 +77,24 @@ class Router @Inject() (
case POST(p"/case/artifact/_search") => observableCtrl.search
// case POST(p"/case/:caseId/artifact/_search") => observableCtrl.findInCase(caseId)
case POST(p"/case/artifact/_stats") => observableCtrl.stats
- case POST(p"/case/$caseId/artifact") => observableCtrl.create(caseId) // Audit ok
+ case POST(p"/case/$caseId/artifact") => observableCtrl.createInCase(caseId) // Audit ok
case GET(p"/case/artifact/$observableId") => observableCtrl.get(observableId)
- case DELETE(p"/case/artifact/$observableId") => observableCtrl.delete(observableId) // Audit ok
- case PATCH(p"/case/artifact/_bulk") => observableCtrl.bulkUpdate // Audit ok
- case PATCH(p"/case/artifact/$observableId") => observableCtrl.update(observableId) // Audit ok
+ case DELETE(p"/case/artifact/$observableId") => observableCtrl.delete(observableId) // Audit ok
+ case PATCH(p"/case/artifact/_bulk") => observableCtrl.bulkUpdate // Audit ok
+ case PATCH(p"/case/artifact/$observableId") => observableCtrl.update(observableId) // Audit ok
case GET(p"/case/artifact/$observableId/similar") => observableCtrl.findSimilar(observableId)
case POST(p"/case/artifact/$observableId/shares") => shareCtrl.shareObservable(observableId)
+ case POST(p"/alert/$alertId/artifact") => observableCtrl.createInAlert(alertId) // Audit ok
+ case PATCH(p"/alert/artifact/$observableId") => observableCtrl.update(observableId) // Audit ok
+ case PATCH(p"/alert/artifact/_bulk") => observableCtrl.bulkUpdate // Audit ok
+ case DELETE(p"/alert/artifact/$observableId") => observableCtrl.delete(observableId) // Audit ok
case GET(p"/case") => caseCtrl.search
- case POST(p"/case") => caseCtrl.create // Audit ok
+ case POST(p"/case") => caseCtrl.create // Audit ok
case GET(p"/case/$caseId") => caseCtrl.get(caseId)
- case PATCH(p"/case/_bulk") => caseCtrl.bulkUpdate // Not used by the frontend
- case PATCH(p"/case/$caseId") => caseCtrl.update(caseId) // Audit ok
- case POST(p"/case/_merge/$caseIds") => caseCtrl.merge(caseIds) // Not implemented in backend and not used by frontend
+ case PATCH(p"/case/_bulk") => caseCtrl.bulkUpdate // Not used by the frontend
+ case PATCH(p"/case/$caseId") => caseCtrl.update(caseId) // Audit ok
+ case POST(p"/case/_merge/$caseIds") => caseCtrl.merge(caseIds) // Not implemented in backend and not used by frontend
case POST(p"/case/_search") => caseCtrl.search
case POST(p"/case/_stats") => caseCtrl.stats
case DELETE(p"/case/$caseId") => caseCtrl.delete(caseId) // Not used by the frontend
diff --git a/thehive/app/org/thp/thehive/controllers/v1/AlertRenderer.scala b/thehive/app/org/thp/thehive/controllers/v1/AlertRenderer.scala
index ac257b06e7..935796ad4e 100644
--- a/thehive/app/org/thp/thehive/controllers/v1/AlertRenderer.scala
+++ b/thehive/app/org/thp/thehive/controllers/v1/AlertRenderer.scala
@@ -1,7 +1,6 @@
package org.thp.thehive.controllers.v1
-import java.util.{List => JList, Map => JMap}
-
+import java.util.{Date, List => JList, Map => JMap}
import org.thp.scalligraph.auth.AuthContext
import org.thp.scalligraph.traversal.TraversalOps._
import org.thp.scalligraph.traversal.{Converter, Traversal}
@@ -39,12 +38,20 @@ trait AlertRenderer extends BaseRenderer[Alert] {
_.similarCases(None).fold.domainMap(sc => JsArray(sc.sorted.map(Json.toJson(_))))
}
- def alertStatsRenderer(extraData: Set[String])(
- implicit authContext: AuthContext
+ def importDate: Traversal.V[Alert] => Traversal[JsValue, JList[Date], Converter[JsValue, JList[Date]]] =
+ _.importDate.fold.domainMap(_.headOption.fold[JsValue](JsNull)(d => JsNumber(d.getTime)))
+
+ def alertStatsRenderer(extraData: Set[String])(implicit
+ authContext: AuthContext
): Traversal.V[Alert] => JsTraversal = { implicit traversal =>
- baseRenderer(extraData, traversal, {
- case (f, "similarCases") => addData("similarCases", f)(similarCasesStats)
- case (f, _) => f
- })
+ baseRenderer(
+ extraData,
+ traversal,
+ {
+ case (f, "similarCases") => addData("similarCases", f)(similarCasesStats)
+ case (f, "importDate") => addData("importDate", f)(importDate)
+ case (f, _) => f
+ }
+ )
}
}
diff --git a/thehive/app/org/thp/thehive/controllers/v1/ObservableCtrl.scala b/thehive/app/org/thp/thehive/controllers/v1/ObservableCtrl.scala
index e41f8822f7..706c36481d 100644
--- a/thehive/app/org/thp/thehive/controllers/v1/ObservableCtrl.scala
+++ b/thehive/app/org/thp/thehive/controllers/v1/ObservableCtrl.scala
@@ -12,6 +12,7 @@ import org.thp.scalligraph.traversal.{IteratorOutput, Traversal}
import org.thp.thehive.controllers.v1.Conversion._
import org.thp.thehive.dto.v1.{InputAttachment, InputObservable}
import org.thp.thehive.models._
+import org.thp.thehive.services.AlertOps._
import org.thp.thehive.services.CaseOps._
import org.thp.thehive.services.ObservableOps._
import org.thp.thehive.services.OrganisationOps._
@@ -30,12 +31,13 @@ import scala.util.{Failure, Success}
@Singleton
class ObservableCtrl @Inject() (
- entryPoint: Entrypoint,
+ entrypoint: Entrypoint,
@Named("with-thehive-schema") db: Database,
properties: Properties,
observableSrv: ObservableSrv,
observableTypeSrv: ObservableTypeSrv,
caseSrv: CaseSrv,
+ alertSrv: AlertSrv,
organisationSrv: OrganisationSrv,
attachmentSrv: AttachmentSrv,
errorHandler: ErrorHandler,
@@ -82,8 +84,8 @@ class ObservableCtrl @Inject() (
Query[Traversal.V[Observable], Traversal.V[Alert]]("alert", (observableSteps, _) => observableSteps.alert)
)
- def create(caseId: String): Action[AnyContent] =
- entryPoint("create artifact")
+ def createInCase(caseId: String): Action[AnyContent] =
+ entrypoint("create artifact in case")
.extract("artifact", FieldsParser[InputObservable])
.extract("isZip", FieldsParser.boolean.optional.on("isZip"))
.extract("zipPassword", FieldsParser.string.optional.on("zipPassword"))
@@ -108,8 +110,8 @@ class ObservableCtrl @Inject() (
case (case0, observableType) =>
val (successes, failures) = inputAttachObs
.flatMap { obs =>
- obs.attachment.map(createAttachmentObservable(case0, obs, observableType, _)) ++
- obs.data.map(createSimpleObservable(case0, obs, observableType, _))
+ obs.attachment.map(createAttachmentObservableInCase(case0, obs, observableType, _)) ++
+ obs.data.map(createSimpleObservableInCase(case0, obs, observableType, _))
}
.foldLeft[(Seq[JsValue], Seq[JsValue])]((Nil, Nil)) {
case ((s, f), Right(o)) => (s :+ o, f)
@@ -120,7 +122,7 @@ class ObservableCtrl @Inject() (
}
}
- def createSimpleObservable(
+ private def createSimpleObservableInCase(
`case`: Case with Entity,
inputObservable: InputObservable,
observableType: ObservableType with Entity,
@@ -136,7 +138,7 @@ class ObservableCtrl @Inject() (
case Failure(error) => Left(errorHandler.toErrorResult(error)._2 ++ Json.obj("object" -> Json.obj("data" -> data)))
}
- def createAttachmentObservable(
+ private def createAttachmentObservableInCase(
`case`: Case with Entity,
inputObservable: InputObservable,
observableType: ObservableType with Entity,
@@ -160,12 +162,90 @@ class ObservableCtrl @Inject() (
Left(Json.obj("object" -> Json.obj("data" -> s"file:$filename", "attachment" -> Json.obj("name" -> filename))))
}
+ def createInAlert(alertId: String): Action[AnyContent] =
+ entrypoint("create artifact in alert")
+ .extract("artifact", FieldsParser[InputObservable])
+ .extract("isZip", FieldsParser.boolean.optional.on("isZip"))
+ .extract("zipPassword", FieldsParser.string.optional.on("zipPassword"))
+ .auth { implicit request =>
+ val inputObservable: InputObservable = request.body("artifact")
+ val isZip: Option[Boolean] = request.body("isZip")
+ val zipPassword: Option[String] = request.body("zipPassword")
+ val inputAttachObs = if (isZip.contains(true)) getZipFiles(inputObservable, zipPassword) else Seq(inputObservable)
+
+ db
+ .roTransaction { implicit graph =>
+ for {
+ alert <-
+ alertSrv
+ .get(EntityIdOrName(alertId))
+ .can(Permissions.manageAlert)
+ .orFail(AuthorizationError("Operation not permitted"))
+ observableType <- observableTypeSrv.getOrFail(EntityName(inputObservable.dataType))
+ } yield (alert, observableType)
+ }
+ .map {
+ case (alert, observableType) =>
+ val (successes, failures) = inputAttachObs
+ .flatMap { obs =>
+ obs.attachment.map(createAttachmentObservableInAlert(alert, obs, observableType, _)) ++
+ obs.data.map(createSimpleObservableInAlert(alert, obs, observableType, _))
+ }
+ .foldLeft[(Seq[JsValue], Seq[JsValue])]((Nil, Nil)) {
+ case ((s, f), Right(o)) => (s :+ o, f)
+ case ((s, f), Left(o)) => (s, f :+ o)
+ }
+ if (failures.isEmpty) Results.Created(JsArray(successes))
+ else Results.MultiStatus(Json.obj("success" -> successes, "failure" -> failures))
+ }
+ }
+
+ private def createSimpleObservableInAlert(
+ alert: Alert with Entity,
+ inputObservable: InputObservable,
+ observableType: ObservableType with Entity,
+ data: String
+ )(implicit authContext: AuthContext): Either[JsValue, JsValue] =
+ db
+ .tryTransaction { implicit graph =>
+ observableSrv
+ .create(inputObservable.toObservable, observableType, data, inputObservable.tags, Nil)
+ .flatMap(o => alertSrv.addObservable(alert, o).map(_ => o))
+ } match {
+ case Success(o) => Right(o.toJson)
+ case Failure(error) => Left(errorHandler.toErrorResult(error)._2 ++ Json.obj("object" -> Json.obj("data" -> data)))
+ }
+
+ private def createAttachmentObservableInAlert(
+ alert: Alert with Entity,
+ inputObservable: InputObservable,
+ observableType: ObservableType with Entity,
+ fileOrAttachment: Either[FFile, InputAttachment]
+ )(implicit authContext: AuthContext): Either[JsValue, JsValue] =
+ db
+ .tryTransaction { implicit graph =>
+ val observable = fileOrAttachment match {
+ case Left(file) => observableSrv.create(inputObservable.toObservable, observableType, file, inputObservable.tags, Nil)
+ case Right(attachment) =>
+ for {
+ attach <- attachmentSrv.duplicate(attachment.name, attachment.contentType, attachment.id)
+ obs <- observableSrv.create(inputObservable.toObservable, observableType, attach, inputObservable.tags, Nil)
+ } yield obs
+ }
+ observable.flatMap(o => alertSrv.addObservable(alert, o).map(_ => o))
+ } match {
+ case Success(o) => Right(o.toJson)
+ case _ =>
+ val filename = fileOrAttachment.fold(_.filename, _.name)
+ Left(Json.obj("object" -> Json.obj("data" -> s"file:$filename", "attachment" -> Json.obj("name" -> filename))))
+ }
+
def get(observableId: String): Action[AnyContent] =
- entryPoint("get observable")
- .authRoTransaction(db) { _ => implicit graph =>
+ entrypoint("get observable")
+ .authRoTransaction(db) { implicit request => implicit graph =>
observableSrv
.get(EntityIdOrName(observableId))
- // .availableFor(request.organisation)
+ .visible
.richObservable
.getOrFail("Observable")
.map { observable =>
@@ -174,20 +254,17 @@ class ObservableCtrl @Inject() (
}
def update(observableId: String): Action[AnyContent] =
- entryPoint("update observable")
+ entrypoint("update observable")
.extract("observable", FieldsParser.update("observable", publicProperties))
.authTransaction(db) { implicit request => implicit graph =>
val propertyUpdaters: Seq[PropertyUpdater] = request.body("observable")
observableSrv
- .update(
- _.get(EntityIdOrName(observableId)).can(Permissions.manageObservable),
- propertyUpdaters
- )
+ .update(_.get(EntityIdOrName(observableId)).canManage, propertyUpdaters)
.map(_ => Results.NoContent)
}
def bulkUpdate: Action[AnyContent] =
- entryPoint("bulk update")
+ entrypoint("bulk update")
.extract("input", FieldsParser.update("observable", publicProperties))
.extract("ids", FieldsParser.seq[String].on("ids"))
.authTransaction(db) { implicit request => implicit graph =>
@@ -196,19 +273,19 @@ class ObservableCtrl @Inject() (
ids
.toTry { id =>
observableSrv
- .update(_.get(EntityIdOrName(id)).can(Permissions.manageObservable), properties)
+ .update(_.get(EntityIdOrName(id)).canManage, properties)
}
.map(_ => Results.NoContent)
}
- def delete(obsId: String): Action[AnyContent] =
- entryPoint("delete")
+ def delete(observableId: String): Action[AnyContent] =
+ entrypoint("delete")
.authTransaction(db) { implicit request => implicit graph =>
for {
observable <-
observableSrv
- .get(EntityIdOrName(obsId))
- .can(Permissions.manageObservable)
+ .get(EntityIdOrName(observableId))
+ .canManage
.getOrFail("Observable")
_ <- observableSrv.remove(observable)
} yield Results.NoContent
diff --git a/thehive/app/org/thp/thehive/controllers/v1/Properties.scala b/thehive/app/org/thp/thehive/controllers/v1/Properties.scala
index fae8188f4e..f842c34564 100644
--- a/thehive/app/org/thp/thehive/controllers/v1/Properties.scala
+++ b/thehive/app/org/thp/thehive/controllers/v1/Properties.scala
@@ -125,7 +125,7 @@ class Properties @Inject() (
case CustomFieldType.integer => new Converter[Any, JsValue] { def apply(x: JsValue): Any = x.as[Long] }
case CustomFieldType.string => new Converter[Any, JsValue] { def apply(x: JsValue): Any = x.as[String] }
}
- .getOrElse(new Converter[Any, JsValue] { def apply(x: JsValue): Any = x })
+ .getOrElse((x: JsValue) => x)
case _ => (x: JsValue) => x
}
.custom {
@@ -142,6 +142,14 @@ class Properties @Inject() (
} yield Json.obj("customFields" -> values)
case _ => Failure(BadRequestError("Invalid custom fields format"))
})
+ .property("case", db.idMapping)(_.select(_.`case`._id).readonly)
+ .property("imported", UMapping.boolean)(_.select(_.imported).readonly)
+ .property("importDate", UMapping.date.optional)(_.select(_.importDate).readonly)
+ .property("computed.handlingDuration", UMapping.long)(_.select(_.handlingDuration).readonly)
+ .property("computed.handlingDurationInSeconds", UMapping.long)(_.select(_.handlingDuration.math("_ / 1000").domainMap(_.toLong)).readonly)
+ .property("computed.handlingDurationInMinutes", UMapping.long)(_.select(_.handlingDuration.math("_ / 60000").domainMap(_.toLong)).readonly)
+ .property("computed.handlingDurationInHours", UMapping.long)(_.select(_.handlingDuration.math("_ / 3600000").domainMap(_.toLong)).readonly)
+ .property("computed.handlingDurationInDays", UMapping.long)(_.select(_.handlingDuration.math("_ / 86400000").domainMap(_.toLong)).readonly)
.build
lazy val audit: PublicProperties =
@@ -193,10 +201,7 @@ class Properties @Inject() (
.property("pap", UMapping.int)(_.field.updatable)
.property("status", UMapping.enum[CaseStatus.type])(_.field.updatable)
.property("summary", UMapping.string.optional)(_.field.updatable)
- .property("actionRequired", UMapping.boolean)(_
- .authSelect((t, auth) => t.isActionRequired(auth))
- .readonly
- )
+ .property("actionRequired", UMapping.boolean)(_.authSelect((t, auth) => t.isActionRequired(auth)).readonly)
.property("assignee", UMapping.string.optional)(_.select(_.user.value(_.login)).custom { (_, login, vertex, _, graph, authContext) =>
for {
c <- caseSrv.get(vertex)(graph).getOrFail("Case")
@@ -261,7 +266,7 @@ class Properties @Inject() (
case CustomFieldType.integer => new Converter[Any, JsValue] { def apply(x: JsValue): Any = x.as[Long] }
case CustomFieldType.string => new Converter[Any, JsValue] { def apply(x: JsValue): Any = x.as[String] }
}
- .getOrElse(new Converter[Any, JsValue] { def apply(x: JsValue): Any = x })
+ .getOrElse((x: JsValue) => x)
case _ => (x: JsValue) => x
}
.custom {
@@ -278,66 +283,11 @@ class Properties @Inject() (
} yield Json.obj("customFields" -> values)
case _ => Failure(BadRequestError("Invalid custom fields format"))
})
- .property("computed.handlingDurationInDays", UMapping.long)(
- _.select(
- _.coalesceIdent(
- _.has(_.endDate)
- .sack(
- (_: JLong, endDate: JLong) => endDate,
- _.by(_.value(_.endDate).graphMap[Long, JLong, Converter[Long, JLong]](_.getTime, Converter.long))
- )
- .sack((_: Long) - (_: JLong), _.by(_.value(_.startDate).graphMap[Long, JLong, Converter[Long, JLong]](_.getTime, Converter.long)))
- .sack((_: Long) / (_: Long), _.by(_.constant(86400000L)))
- .sack[Long],
- _.constant(0L)
- )
- ).readonly
- )
- .property("computed.handlingDurationInHours", UMapping.long)(
- _.select(
- _.coalesceIdent(
- _.has(_.endDate)
- .sack(
- (_: JLong, endDate: JLong) => endDate,
- _.by(_.value(_.endDate).graphMap[Long, JLong, Converter[Long, JLong]](_.getTime, Converter.long))
- )
- .sack((_: Long) - (_: JLong), _.by(_.value(_.startDate).graphMap[Long, JLong, Converter[Long, JLong]](_.getTime, Converter.long)))
- .sack((_: Long) / (_: Long), _.by(_.constant(3600000L)))
- .sack[Long],
- _.constant(0L)
- )
- ).readonly
- )
- .property("computed.handlingDurationInMinutes", UMapping.long)(
- _.select(
- _.coalesceIdent(
- _.has(_.endDate)
- .sack(
- (_: JLong, endDate: JLong) => endDate,
- _.by(_.value(_.endDate).graphMap[Long, JLong, Converter[Long, JLong]](_.getTime, Converter.long))
- )
- .sack((_: Long) - (_: JLong), _.by(_.value(_.startDate).graphMap[Long, JLong, Converter[Long, JLong]](_.getTime, Converter.long)))
- .sack((_: Long) / (_: Long), _.by(_.constant(60000L)))
- .sack[Long],
- _.constant(0L)
- )
- ).readonly
- )
- .property("computed.handlingDurationInSeconds", UMapping.long)(
- _.select(
- _.coalesceIdent(
- _.has(_.endDate)
- .sack(
- (_: JLong, endDate: JLong) => endDate,
- _.by(_.value(_.endDate).graphMap[Long, JLong, Converter[Long, JLong]](_.getTime, Converter.long))
- )
- .sack((_: Long) - (_: JLong), _.by(_.value(_.startDate).graphMap[Long, JLong, Converter[Long, JLong]](_.getTime, Converter.long)))
- .sack((_: Long) / (_: Long), _.by(_.constant(1000L)))
- .sack[Long],
- _.constant(0L)
- )
- ).readonly
- )
+ .property("computed.handlingDuration", UMapping.long)(_.select(_.handlingDuration).readonly)
+ .property("computed.handlingDurationInSeconds", UMapping.long)(_.select(_.handlingDuration.math("_ / 1000").domainMap(_.toLong)).readonly)
+ .property("computed.handlingDurationInMinutes", UMapping.long)(_.select(_.handlingDuration.math("_ / 60000").domainMap(_.toLong)).readonly)
+ .property("computed.handlingDurationInHours", UMapping.long)(_.select(_.handlingDuration.math("_ / 3600000").domainMap(_.toLong)).readonly)
+ .property("computed.handlingDurationInDays", UMapping.long)(_.select(_.handlingDuration.math("_ / 86400000").domainMap(_.toLong)).readonly)
.property("viewingOrganisation", UMapping.string)(
_.authSelect((cases, authContext) => cases.organisations.visible(authContext).value(_.name)).readonly
)
@@ -433,12 +383,9 @@ class Properties @Inject() (
}
.map(_ => Json.obj("assignee" -> value))
})
- .property("actionRequired", UMapping.boolean)(_
- .authSelect((t, authContext) => {
- t.actionRequired(authContext)
- })
- .readonly
- )
+ .property("actionRequired", UMapping.boolean)(_.authSelect { (t, authContext) =>
+ t.actionRequired(authContext)
+ }.readonly)
.build
lazy val log: PublicProperties =
diff --git a/thehive/app/org/thp/thehive/services/AlertSrv.scala b/thehive/app/org/thp/thehive/services/AlertSrv.scala
index 0d7de34c4b..41859c2590 100644
--- a/thehive/app/org/thp/thehive/services/AlertSrv.scala
+++ b/thehive/app/org/thp/thehive/services/AlertSrv.scala
@@ -1,6 +1,6 @@
package org.thp.thehive.services
-import org.apache.tinkerpop.gremlin.process.traversal.P
+import akka.actor.ActorRef
import org.apache.tinkerpop.gremlin.structure.Graph
import org.thp.scalligraph.auth.{AuthContext, Permission}
import org.thp.scalligraph.models._
@@ -8,7 +8,7 @@ import org.thp.scalligraph.query.PropertyUpdater
import org.thp.scalligraph.services._
import org.thp.scalligraph.traversal.TraversalOps._
import org.thp.scalligraph.traversal.{Converter, IdentityConverter, StepLabel, Traversal}
-import org.thp.scalligraph.{CreateError, EntityId, EntityIdOrName, RichOptionTry, RichSeq}
+import org.thp.scalligraph.{BadRequestError, CreateError, EntityId, EntityIdOrName, RichOptionTry, RichSeq}
import org.thp.thehive.controllers.v1.Conversion._
import org.thp.thehive.dto.v1.InputCustomFieldValue
import org.thp.thehive.models._
@@ -33,7 +33,8 @@ class AlertSrv @Inject() (
customFieldSrv: CustomFieldSrv,
caseTemplateSrv: CaseTemplateSrv,
observableSrv: ObservableSrv,
- auditSrv: AuditSrv
+ auditSrv: AuditSrv,
+ @Named("integrity-check-actor") integrityCheckActor: ActorRef
)(implicit
@Named("with-thehive-schema") db: Database
) extends VertexSrv[Alert] {
@@ -73,8 +74,8 @@ class AlertSrv @Inject() (
graph: Graph,
authContext: AuthContext
): Try[RichAlert] = {
- val alertAlreadyExist = organisationSrv.get(organisation).alerts.getBySourceId(alert.`type`, alert.source, alert.sourceRef).getCount
- if (alertAlreadyExist > 0)
+ val alertAlreadyExist = startTraversal.getBySourceId(alert.`type`, alert.source, alert.sourceRef).organisation.current.exists
+ if (alertAlreadyExist)
Failure(CreateError(s"Alert ${alert.`type`}:${alert.source}:${alert.sourceRef} already exist in organisation ${organisation.name}"))
else
for {
@@ -269,6 +270,7 @@ class AlertSrv @Inject() (
_ <- importObservables(alert.alert, createdCase.`case`)
_ <- alertCaseSrv.create(AlertCase(), alert.alert, createdCase.`case`)
_ <- markAsRead(alert._id)
+ _ = integrityCheckActor ! EntityAdded("Alert")
} yield createdCase
}
}(richCase => auditSrv.`case`.create(richCase.`case`, richCase.toJson))
@@ -281,28 +283,32 @@ class AlertSrv @Inject() (
} yield updatedCase
def mergeInCase(alert: Alert with Entity, `case`: Case with Entity)(implicit graph: Graph, authContext: AuthContext): Try[Case with Entity] =
- auditSrv
- .mergeAudits {
- // No audit for markAsRead and observables
- // Audits for customFields, description and tags
- val description = `case`.description + s"\n \n#### Merged with alert #${alert.sourceRef} ${alert.title}\n\n${alert.description.trim}"
- for {
- _ <- markAsRead(alert._id)
- _ <- importObservables(alert, `case`)
- _ <- importCustomFields(alert, `case`)
- _ <- caseSrv.addTags(`case`, get(alert).tags.toSeq.map(_.toString).toSet)
- _ <- alertCaseSrv.create(AlertCase(), alert, `case`)
- c <- caseSrv.get(`case`).update(_.description, description).getOrFail("Case")
- details <- Success(
- Json.obj(
- "customFields" -> get(alert).richCustomFields.toSeq.map(_.toOutput.toJson),
- "description" -> c.description,
- "tags" -> caseSrv.get(`case`).tags.toSeq.map(_.toString)
+ if (get(alert).isImported)
+ Failure(BadRequestError("Alert is already imported"))
+ else
+ auditSrv
+ .mergeAudits {
+ // No audit for markAsRead and observables
+ // Audits for customFields, description and tags
+ val description = `case`.description + s"\n \n#### Merged with alert #${alert.sourceRef} ${alert.title}\n\n${alert.description.trim}"
+ for {
+ _ <- markAsRead(alert._id)
+ _ <- importObservables(alert, `case`)
+ _ <- importCustomFields(alert, `case`)
+ _ <- caseSrv.addTags(`case`, get(alert).tags.toSeq.map(_.toString).toSet)
+ _ <- alertCaseSrv.create(AlertCase(), alert, `case`)
+ c <- caseSrv.get(`case`).update(_.description, description).getOrFail("Case")
+ details <- Success(
+ Json.obj(
+ "customFields" -> get(alert).richCustomFields.toSeq.map(_.toOutput.toJson),
+ "description" -> c.description,
+ "tags" -> caseSrv.get(`case`).tags.toSeq.map(_.toString)
+ )
)
- )
- } yield details
- }(details => auditSrv.alertToCase.merge(alert, `case`, Some(details)))
- .flatMap(_ => caseSrv.getOrFail(`case`._id))
+ } yield details
+ }(details => auditSrv.alertToCase.merge(alert, `case`, Some(details)))
+ .map(_ => integrityCheckActor ! EntityAdded("Alert"))
+ .flatMap(_ => caseSrv.getOrFail(`case`._id))
def importObservables(alert: Alert with Entity, `case`: Case with Entity)(implicit
graph: Graph,
@@ -399,10 +405,25 @@ object AlertOps {
else traversal.limit(0)
def imported: Traversal[Boolean, Boolean, IdentityConverter[Boolean]] =
- traversal
- .`case`
- .count
- .choose(_.is(P.gt(0)), onTrue = true, onFalse = false)
+ traversal.choose(_.outE[AlertCase], onTrue = true, onFalse = false)
+
+ def isImported: Boolean =
+ traversal.outE[AlertCase].exists
+
+ def importDate: Traversal[Date, Date, Converter[Date, Date]] =
+ traversal.outE[AlertCase].value(_._createdAt)
+
+ def handlingDuration: Traversal[Long, Long, IdentityConverter[Long]] =
+ traversal.coalesceIdent(
+ _.filter(_.outE[AlertCase])
+ .sack(
+ (_: JLong, importDate: JLong) => importDate,
+ _.by(_.importDate.graphMap[Long, JLong, Converter[Long, JLong]](_.getTime, Converter.long))
+ )
+ .sack((_: Long) - (_: JLong), _.by(_._createdAt.graphMap[Long, JLong, Converter[Long, JLong]](_.getTime, Converter.long)))
+ .sack[Long],
+ _.constant(0L)
+ )
def similarCases(maybeCaseFilter: Option[Traversal.V[Case] => Traversal.V[Case]])(implicit
authContext: AuthContext
@@ -574,3 +595,18 @@ object AlertOps {
implicit class AlertCustomFieldsOpsDefs(traversal: Traversal.E[AlertCustomField]) extends CustomFieldValueOpsDefs(traversal)
}
+
+class AlertIntegrityCheckOps @Inject() (@Named("with-thehive-schema") val db: Database, val service: AlertSrv) extends IntegrityCheckOps[Alert] {
+ override def check(): Unit = {
+ db.tryTransaction { implicit graph =>
+ service
+ .startTraversal
+ .flatMap(_.outE[AlertCase].range(1, 100))
+ .remove()
+ Success(())
+ }
+ ()
+ }
+
+ override def resolve(entities: Seq[Alert with Entity])(implicit graph: Graph): Try[Unit] = Success(())
+}
diff --git a/thehive/app/org/thp/thehive/services/CaseSrv.scala b/thehive/app/org/thp/thehive/services/CaseSrv.scala
index 291a1147af..d29415c808 100644
--- a/thehive/app/org/thp/thehive/services/CaseSrv.scala
+++ b/thehive/app/org/thp/thehive/services/CaseSrv.scala
@@ -1,6 +1,7 @@
package org.thp.thehive.services
import java.util.{Map => JMap}
+import java.lang.{Long => JLong}
import akka.actor.ActorRef
import javax.inject.{Inject, Named, Singleton}
@@ -12,7 +13,7 @@ import org.thp.scalligraph.models._
import org.thp.scalligraph.query.PropertyUpdater
import org.thp.scalligraph.services._
import org.thp.scalligraph.traversal.TraversalOps._
-import org.thp.scalligraph.traversal.{Converter, StepLabel, Traversal}
+import org.thp.scalligraph.traversal.{Converter, IdentityConverter, StepLabel, Traversal}
import org.thp.scalligraph.{CreateError, EntityIdOrName, EntityName, RichOptionTry, RichSeq}
import org.thp.thehive.controllers.v1.Conversion._
import org.thp.thehive.dto.v1.InputCustomFieldValue
@@ -557,9 +558,18 @@ object CaseOps {
def isActionRequired(implicit authContext: AuthContext): Traversal[Boolean, Boolean, Converter.Identity[Boolean]] =
traversal.choose(_.share(authContext).outE[ShareTask].has(_.actionRequired, true), true, false)
+ def handlingDuration: Traversal[Long, Long, IdentityConverter[Long]] =
+ traversal.coalesceIdent(
+ _.has(_.endDate)
+ .sack(
+ (_: JLong, importDate: JLong) => importDate,
+ _.by(_.value(_.endDate).graphMap[Long, JLong, Converter[Long, JLong]](_.getTime, Converter.long))
+ )
+ .sack((_: Long) - (_: JLong), _.by(_._createdAt.graphMap[Long, JLong, Converter[Long, JLong]](_.getTime, Converter.long)))
+ .sack[Long],
+ _.constant(0L)
+ )
}
-
-// implicit class CaseCustomFieldsOpsDefs(traversal: Traversal.E[CaseCustomField]) extends CustomFieldValueOpsDefs(traversal)
}
class CaseIntegrityCheckOps @Inject() (@Named("with-thehive-schema") val db: Database, val service: CaseSrv) extends IntegrityCheckOps[Case] {
diff --git a/thehive/app/org/thp/thehive/services/ObservableSrv.scala b/thehive/app/org/thp/thehive/services/ObservableSrv.scala
index b95f40f7aa..e63e74016b 100644
--- a/thehive/app/org/thp/thehive/services/ObservableSrv.scala
+++ b/thehive/app/org/thp/thehive/services/ObservableSrv.scala
@@ -15,6 +15,7 @@ import org.thp.scalligraph.traversal.{Converter, StepLabel, Traversal}
import org.thp.scalligraph.utils.Hash
import org.thp.scalligraph.{EntityIdOrName, RichSeq}
import org.thp.thehive.models._
+import org.thp.thehive.services.AlertOps._
import org.thp.thehive.services.ObservableOps._
import org.thp.thehive.services.OrganisationOps._
import org.thp.thehive.services.ShareOps._
@@ -229,6 +230,12 @@ object ObservableOps {
else
traversal.limit(0)
+ def canManage(implicit authContext: AuthContext): Traversal.V[Observable] =
+ if (authContext.isPermitted(Permissions.manageAlert))
+ traversal.filter(_.or(_.alert.visible, _.can(Permissions.manageObservable)))
+ else
+ can(Permissions.manageObservable)
+
def userPermissions(implicit authContext: AuthContext): Traversal[Set[Permission], Vertex, Converter[Set[Permission], Vertex]] =
traversal
.share(authContext.organisation)
diff --git a/thehive/app/org/thp/thehive/services/th3/Aggregation.scala b/thehive/app/org/thp/thehive/services/th3/Aggregation.scala
index 358d5a744b..79d89cf159 100644
--- a/thehive/app/org/thp/thehive/services/th3/Aggregation.scala
+++ b/thehive/app/org/thp/thehive/services/th3/Aggregation.scala
@@ -186,7 +186,8 @@ case class AggAvg(aggName: Option[String], fieldName: String) extends Aggregatio
property
.select(fieldPath, t, authContext)
.mean
- .domainMap(avg => Output(Json.obj(name -> avg.asInstanceOf[Double]))),
+ .domainMap(avg => Output(Json.obj(name -> avg)))
+ .asInstanceOf[Traversal.Domain[Output[_]]],
Output(Json.obj(name -> JsNull))
)
}
diff --git a/thehive/conf/reference.conf b/thehive/conf/reference.conf
index 403e23198c..523fb7d7d1 100644
--- a/thehive/conf/reference.conf
+++ b/thehive/conf/reference.conf
@@ -163,6 +163,10 @@ integrityCheck {
initialDelay: 1 minute
interval: 10 minutes
}
+ alert {
+ initialDelay: 5 minute
+ interval: 30 minutes
+ }
}
diff --git a/thehive/test/org/thp/thehive/controllers/v0/ObservableCtrlTest.scala b/thehive/test/org/thp/thehive/controllers/v0/ObservableCtrlTest.scala
index c76dc02c1a..b4900adc36 100644
--- a/thehive/test/org/thp/thehive/controllers/v0/ObservableCtrlTest.scala
+++ b/thehive/test/org/thp/thehive/controllers/v0/ObservableCtrlTest.scala
@@ -57,7 +57,7 @@ class ObservableCtrlTest extends PlaySpecification with TestAppBuilder {
"data":["multi","line","test"]
}
""".stripMargin))
- val result = app[ObservableCtrl].create("1")(request)
+ val result = app[ObservableCtrl].createInCase("1")(request)
status(result) must equalTo(201).updateMessage(s => s"$s\n${contentAsString(result)}")
val createdObservables = contentAsJson(result).as[Seq[OutputObservable]]
@@ -84,7 +84,7 @@ class ObservableCtrlTest extends PlaySpecification with TestAppBuilder {
"data":["observable", "in", "array"]
}
""".stripMargin))
- val result = app[ObservableCtrl].create("1")(request)
+ val result = app[ObservableCtrl].createInCase("1")(request)
status(result) must beEqualTo(201).updateMessage(s => s"$s\n${contentAsString(result)}")
@@ -139,9 +139,8 @@ class ObservableCtrlTest extends PlaySpecification with TestAppBuilder {
resSearchObservables.flatMap(_.data) must contain(exactly("observable", "in", "array", "h.fr"))
}
- "be able to create and get 2 observables with string data and attachment" in testApp { app =>
+ "be able to create and get 2 observables with string data" in testApp { app =>
WithFakeTemporaryFile { tempFile =>
- val hashes = Hasher(app.apply[Configuration].get[Seq[String]]("attachment.hash"): _*).fromPath(tempFile.path).map(_.toString)
val files = Seq(FilePart("attachment", "myfile.txt", Some("text/plain"), tempFile))
val dataParts = Map("_json" -> Seq("""
{
@@ -160,24 +159,70 @@ class ObservableCtrlTest extends PlaySpecification with TestAppBuilder {
Headers("user" -> "certuser@thehive.local"),
body = AnyContentAsMultipartFormData(MultipartFormData(dataParts, files, Nil))
)
- val result = app[ObservableCtrl].create("1")(request)
+ val result = app[ObservableCtrl].createInCase("1")(request)
status(result) must equalTo(201).updateMessage(s => s"$s\n${contentAsString(result)}")
val createdObservables = contentAsJson(result).as[Seq[OutputObservable]]
- createdObservables must have size 3
+ createdObservables must have size 2
createdObservables.map(_.dataType) must contain(be_==("ip")).forall
createdObservables.flatMap(_.data) must contain(exactly("127.0.0.1", "127.0.0.2"))
createdObservables.map(_.sighted) must contain(beFalse).forall
createdObservables.map(_.message) must contain(beSome("localhost")).forall
createdObservables.map(_.tags) must contain(be_==(Set("local", "host"))).forall
val attachmentOption = createdObservables.flatMap(_.attachment).headOption
- attachmentOption must beSome
- val attachment = attachmentOption.get
- attachment.name must beEqualTo("myfile.txt")
- attachment.hashes must containTheSameElementsAs(hashes)
- attachment.size must beEqualTo(tempFile.length())
- attachment.contentType must beEqualTo("text/plain")
+ attachmentOption must beNone
+ }
+ }
+
+ "be able to create and get 2 observables with string data and attachment" in testApp { app =>
+ WithFakeTemporaryFile { tempFile =>
+ val hasher = Hasher(app.apply[Configuration].get[Seq[String]]("attachment.hash"): _*)
+ val hashes = hasher.fromPath(tempFile.path).map(_.toString)
+ val helloHashes = hasher.fromString("Hello world").map(_.toString)
+ val files = Seq(FilePart("attachment", "myfile.txt", Some("text/plain"), tempFile))
+ val dataParts = Map("_json" -> Seq("""
+ {
+ "dataType":"file",
+ "ioc":false,
+ "sighted":false,
+ "tlp":2,
+ "message":"localhost",
+ "tags":["local", "host"],
+ "data":["hello.txt;text/plain;SGVsbG8gd29ybGQ="]
+ }
+ """))
+ val request = FakeRequest(
+ "POST",
+ s"/api/alert/testType;testSource;ref2/artifact",
+ Headers("user" -> "certuser@thehive.local"),
+ body = AnyContentAsMultipartFormData(MultipartFormData(dataParts, files, Nil))
+ )
+ val result = app[ObservableCtrl].createInAlert("testType;testSource;ref2")(request)
+ status(result) must equalTo(201).updateMessage(s => s"$s\n${contentAsString(result)}")
+ val createdObservables = contentAsJson(result).as[Seq[OutputObservable]]
+ createdObservables must have size 2
+ createdObservables.map(_.dataType) must contain(be_==("file")).forall
+ createdObservables.flatMap(_.data) must beEmpty
+ createdObservables.map(_.sighted) must contain(beFalse).forall
+ createdObservables.map(_.message) must contain(beSome("localhost")).forall
+ createdObservables.map(_.tags) must contain(be_==(Set("local", "host"))).forall
+ val attachments = createdObservables.flatMap(_.attachment)
+ attachments must have size 2
+ attachments must contain(beLike[OutputAttachment] {
+ case attachment =>
+ attachment.name must beEqualTo("myfile.txt")
+ attachment.hashes must containTheSameElementsAs(hashes)
+ attachment.size must beEqualTo(tempFile.length())
+ attachment.contentType must beEqualTo("text/plain")
+ })
+ attachments must contain(beLike[OutputAttachment] {
+ case attachment =>
+ attachment.name must beEqualTo("hello.txt")
+ attachment.hashes must containTheSameElementsAs(helloHashes)
+ attachment.size must beEqualTo(11)
+ attachment.contentType must beEqualTo("text/plain")
+ })
createdObservables.foreach(obs => obs must equalTo(getObservable(obs._id, app[ObservableCtrl])))
ok
}
@@ -219,7 +264,7 @@ class ObservableCtrlTest extends PlaySpecification with TestAppBuilder {
"data":"localhost"
}
"""))
- val result1 = app[ObservableCtrl].create("1")(request1)
+ val result1 = app[ObservableCtrl].createInCase("1")(request1)
status(result1) must beEqualTo(201).updateMessage(s => s"$s\n${contentAsString(result1)}")
getData("localhost", app) must have size 1
@@ -233,7 +278,7 @@ class ObservableCtrlTest extends PlaySpecification with TestAppBuilder {
"data":"localhost"
}
"""))
- val result2 = app[ObservableCtrl].create("2")(request2)
+ val result2 = app[ObservableCtrl].createInCase("2")(request2)
status(result2) must equalTo(201).updateMessage(s => s"$s\n${contentAsString(result2)}")
getData("localhost", app) must have size 1
@@ -273,7 +318,7 @@ class ObservableCtrlTest extends PlaySpecification with TestAppBuilder {
"data":"${UUID.randomUUID()}\\n${UUID.randomUUID()}"
}
"""))
- val result = observableCtrl.create("1")(request)
+ val result = observableCtrl.createInCase("1")(request)
status(result) shouldEqual 201
contentAsJson(result).as[Seq[OutputObservable]]
|