From 58fb2d38a3e5fc3e74060ae651c73ad31d2bfde1 Mon Sep 17 00:00:00 2001 From: To-om Date: Thu, 5 Nov 2020 14:27:41 +0100 Subject: [PATCH] #1619 Add the trigger "CaseShared" and the responders "RunAnalyzer" and "RunResponder" --- .../connector/cortex/CortexModule.scala | 6 ++ .../cortex/services/AnalyzerSrv.scala | 24 ++++- .../cortex/services/ResponderSrv.scala | 12 ++- .../notification/notifiers/RunAnalyzer.scala | 83 +++++++++++++++ .../notification/notifiers/RunResponder.scala | 100 ++++++++++++++++++ .../app/org/thp/thehive/TheHiveModule.scala | 1 + .../notification/NotificationActor.scala | 4 +- .../notification/triggers/CaseShared.scala | 24 +++++ 8 files changed, 248 insertions(+), 6 deletions(-) create mode 100644 cortex/connector/src/main/scala/org/thp/thehive/connector/cortex/services/notification/notifiers/RunAnalyzer.scala create mode 100644 cortex/connector/src/main/scala/org/thp/thehive/connector/cortex/services/notification/notifiers/RunResponder.scala create mode 100644 thehive/app/org/thp/thehive/services/notification/triggers/CaseShared.scala diff --git a/cortex/connector/src/main/scala/org/thp/thehive/connector/cortex/CortexModule.scala b/cortex/connector/src/main/scala/org/thp/thehive/connector/cortex/CortexModule.scala index 7366bfcb1e..ccbdf83079 100644 --- a/cortex/connector/src/main/scala/org/thp/thehive/connector/cortex/CortexModule.scala +++ b/cortex/connector/src/main/scala/org/thp/thehive/connector/cortex/CortexModule.scala @@ -6,7 +6,9 @@ import org.thp.scalligraph.models.{Database, Schema} import org.thp.scalligraph.query.QueryExecutor import org.thp.thehive.connector.cortex.controllers.v0.{CortexQueryExecutor => CortexQueryExecutorV0} import org.thp.thehive.connector.cortex.models.{CortexSchemaDefinition, DatabaseProvider} +import org.thp.thehive.connector.cortex.services.notification.notifiers.{RunAnalyzerProvider, RunResponderProvider} import org.thp.thehive.connector.cortex.services.{Connector, CortexActor} +import org.thp.thehive.services.notification.notifiers.NotifierProvider import org.thp.thehive.services.{Connector => TheHiveConnector} import play.api.libs.concurrent.AkkaGuiceSupport import play.api.routing.{Router => PlayRouter} @@ -25,6 +27,10 @@ class CortexModule(environment: Environment, configuration: Configuration) exten val schemaBindings = ScalaMultibinder.newSetBinder[Schema](binder) schemaBindings.addBinding.to[CortexSchemaDefinition] + val notifierBindings = ScalaMultibinder.newSetBinder[NotifierProvider](binder) + notifierBindings.addBinding.to[RunResponderProvider] + notifierBindings.addBinding.to[RunAnalyzerProvider] + bind[Database].annotatedWithName("with-thehive-cortex-schema").toProvider[DatabaseProvider] bindActor[CortexActor]("cortex-actor") () diff --git a/cortex/connector/src/main/scala/org/thp/thehive/connector/cortex/services/AnalyzerSrv.scala b/cortex/connector/src/main/scala/org/thp/thehive/connector/cortex/services/AnalyzerSrv.scala index a319024ef7..f4eb6ebf74 100644 --- a/cortex/connector/src/main/scala/org/thp/thehive/connector/cortex/services/AnalyzerSrv.scala +++ b/cortex/connector/src/main/scala/org/thp/thehive/connector/cortex/services/AnalyzerSrv.scala @@ -2,9 +2,11 @@ package org.thp.thehive.connector.cortex.services import javax.inject.{Inject, Singleton} import org.thp.cortex.dto.v0.{OutputWorker => CortexWorker} -import org.thp.scalligraph.NotFoundError +import org.thp.scalligraph.{EntityIdOrName, NotFoundError} import org.thp.scalligraph.auth.AuthContext +import org.thp.cortex.dto.v0.OutputWorker import play.api.Logger +import play.api.libs.json.{JsObject, Json} import scala.concurrent.{ExecutionContext, Future} import scala.util.{Failure, Success} @@ -62,4 +64,24 @@ class AnalyzerSrv @Inject() (connector: Connector, serviceHelper: ServiceHelper, .headOption .fold[Future[(CortexWorker, Seq[String])]](Future.failed(NotFoundError(s"Analyzer $id not found")))(Future.successful) } + + def getAnalyzerByName(analyzerName: String, organisation: EntityIdOrName): Future[Map[CortexWorker, Seq[String]]] = + searchAnalyzers(Json.obj("query" -> Json.obj("_field" -> "name", "_value" -> analyzerName)), organisation) + + def searchAnalyzers(query: JsObject)(implicit authContext: AuthContext): Future[Map[OutputWorker, Seq[String]]] = + searchAnalyzers(query, authContext.organisation) + + def searchAnalyzers(query: JsObject, organisation: EntityIdOrName): Future[Map[OutputWorker, Seq[String]]] = + Future + .traverse(serviceHelper.availableCortexClients(connector.clients, organisation)) { client => + client + .searchResponders(query) + .transform { + case Success(analyzers) => Success(analyzers.map(_ -> client.name)) + case Failure(error) => + logger.error(s"List Cortex analyzers fails on ${client.name}", error) + Success(Nil) + } + } + .map(serviceHelper.flattenList) } diff --git a/cortex/connector/src/main/scala/org/thp/thehive/connector/cortex/services/ResponderSrv.scala b/cortex/connector/src/main/scala/org/thp/thehive/connector/cortex/services/ResponderSrv.scala index e350a2c8f6..18ded91397 100644 --- a/cortex/connector/src/main/scala/org/thp/thehive/connector/cortex/services/ResponderSrv.scala +++ b/cortex/connector/src/main/scala/org/thp/thehive/connector/cortex/services/ResponderSrv.scala @@ -9,7 +9,7 @@ import org.thp.scalligraph.models.Database import org.thp.thehive.controllers.v0.Conversion.toObjectType import org.thp.thehive.models.Permissions import play.api.Logger -import play.api.libs.json.JsObject +import play.api.libs.json.{JsObject, Json} import scala.concurrent.{ExecutionContext, Future} import scala.util.{Failure, Success} @@ -56,15 +56,21 @@ class ResponderSrv @Inject() ( ) } yield serviceHelper.flattenList(responders).filter { case (w, _) => w.maxTlp >= tlp && w.maxPap >= pap } + def getRespondersByName(responderName: String, organisation: EntityIdOrName): Future[Map[OutputWorker, Seq[String]]] = + searchResponders(Json.obj("query" -> Json.obj("_field" -> "name", "_value" -> responderName)), organisation) + /** - * Search responders, not used as of 08/19 + * Search responders * @param query the raw query from frontend * @param authContext auth context for organisation filter * @return */ def searchResponders(query: JsObject)(implicit authContext: AuthContext): Future[Map[OutputWorker, Seq[String]]] = + searchResponders(query, authContext.organisation) + + def searchResponders(query: JsObject, organisation: EntityIdOrName): Future[Map[OutputWorker, Seq[String]]] = Future - .traverse(serviceHelper.availableCortexClients(connector.clients, authContext.organisation)) { client => + .traverse(serviceHelper.availableCortexClients(connector.clients, organisation)) { client => client .searchResponders(query) .transform { diff --git a/cortex/connector/src/main/scala/org/thp/thehive/connector/cortex/services/notification/notifiers/RunAnalyzer.scala b/cortex/connector/src/main/scala/org/thp/thehive/connector/cortex/services/notification/notifiers/RunAnalyzer.scala new file mode 100644 index 0000000000..c292c513a9 --- /dev/null +++ b/cortex/connector/src/main/scala/org/thp/thehive/connector/cortex/services/notification/notifiers/RunAnalyzer.scala @@ -0,0 +1,83 @@ +package org.thp.thehive.connector.cortex.services.notification.notifiers + +import javax.inject.{Inject, Singleton} +import org.apache.tinkerpop.gremlin.structure.Graph +import org.thp.scalligraph.models.Entity +import org.thp.scalligraph.traversal.TraversalOps._ +import org.thp.scalligraph.{BadConfigurationError, NotFoundError, RichOption} +import org.thp.thehive.connector.cortex.services.{AnalyzerSrv, JobSrv} +import org.thp.thehive.controllers.v0.AuditRenderer +import org.thp.thehive.models._ +import org.thp.thehive.services.ObservableOps._ +import org.thp.thehive.services._ +import org.thp.thehive.services.notification.notifiers.{Notifier, NotifierProvider} +import play.api.Configuration + +import scala.concurrent.{ExecutionContext, Future} +import scala.util.{Failure, Try} + +@Singleton +class RunAnalyzerProvider @Inject() ( + analyzerSrv: AnalyzerSrv, + jobSrv: JobSrv, + caseSrv: CaseSrv, + observableSrv: ObservableSrv, + ec: ExecutionContext +) extends NotifierProvider { + override val name: String = "RunAnalyzer" + + override def apply(config: Configuration): Try[Notifier] = + config.getOrFail[String]("analyzerName").map { responderName => + new RunAnalyzer( + responderName, + analyzerSrv, + jobSrv, + caseSrv, + observableSrv, + ec + ) + } +} + +class RunAnalyzer( + analyzerName: String, + analyzerSrv: AnalyzerSrv, + jobSrv: JobSrv, + caseSrv: CaseSrv, + observableSrv: ObservableSrv, + implicit val ec: ExecutionContext +) extends Notifier + with AuditRenderer { + override val name: String = "RunAnalyzer" + + def getObservable(`object`: Option[Entity])(implicit graph: Graph): Future[RichObservable] = + `object` match { + case Some(o) if o._label == "Observable" => Future.fromTry(observableSrv.get(o._id).richObservable.getOrFail("Observable")) + case _ => Future.failed(NotFoundError("Audit object is not an observable")) + } + + def getCase(context: Option[Entity])(implicit graph: Graph): Future[Case with Entity] = + context match { + case Some(c) if c._label == "Case" => Future.fromTry(caseSrv.getOrFail(c._id)) + case _ => Future.failed(NotFoundError("Audit context is not a case")) + } + + override def execute( + audit: Audit with Entity, + context: Option[Entity], + `object`: Option[Entity], + organisation: Organisation with Entity, + user: Option[User with Entity] + )(implicit graph: Graph): Future[Unit] = + if (user.isDefined) + Future.failed(BadConfigurationError("The notification runAnalyzer must not be applied on user")) + else + for { + observable <- getObservable(`object`) + case0 <- getCase(context) + workers <- analyzerSrv.getAnalyzerByName(analyzerName, organisation._id) + (worker, cortexIds) <- Future.fromTry(workers.headOption.toTry(Failure(NotFoundError(s"Analyzer $analyzerName not found")))) + authContext = LocalUserSrv.getSystemAuthContext.changeOrganisation(organisation._id, Permissions.all) + _ <- jobSrv.submit(cortexIds.head, worker.id, observable, case0)(authContext) + } yield () +} diff --git a/cortex/connector/src/main/scala/org/thp/thehive/connector/cortex/services/notification/notifiers/RunResponder.scala b/cortex/connector/src/main/scala/org/thp/thehive/connector/cortex/services/notification/notifiers/RunResponder.scala new file mode 100644 index 0000000000..8d50288980 --- /dev/null +++ b/cortex/connector/src/main/scala/org/thp/thehive/connector/cortex/services/notification/notifiers/RunResponder.scala @@ -0,0 +1,100 @@ +package org.thp.thehive.connector.cortex.services.notification.notifiers + +import com.typesafe.config.ConfigRenderOptions +import javax.inject.{Inject, Singleton} +import org.apache.tinkerpop.gremlin.structure.Graph +import org.thp.scalligraph.models.Entity +import org.thp.scalligraph.traversal.TraversalOps._ +import org.thp.scalligraph.{BadConfigurationError, NotFoundError, RichOption} +import org.thp.thehive.connector.cortex.services.{ActionSrv, ResponderSrv} +import org.thp.thehive.controllers.v0.AuditRenderer +import org.thp.thehive.models.{Audit, Organisation, Permissions, User} +import org.thp.thehive.services._ +import org.thp.thehive.services.notification.notifiers.{Notifier, NotifierProvider} +import play.api.Configuration +import play.api.libs.json.{JsObject, Json, OWrites} + +import scala.concurrent.{ExecutionContext, Future} +import scala.util.{Failure, Try} + +@Singleton +class RunResponderProvider @Inject() ( + responderSrv: ResponderSrv, + actionSrv: ActionSrv, + taskSrv: TaskSrv, + caseSrv: CaseSrv, + observableSrv: ObservableSrv, + logSrv: LogSrv, + alertSrv: AlertSrv, + ec: ExecutionContext +) extends NotifierProvider { + override val name: String = "RunResponder" + + override def apply(config: Configuration): Try[Notifier] = { + + val parameters = Try(Json.parse(config.underlying.getValue("parameters").render(ConfigRenderOptions.concise())).as[JsObject]).toOption + config.getOrFail[String]("responderName").map { responderName => + new RunResponder( + responderName, + parameters.getOrElse(JsObject.empty), + responderSrv, + actionSrv, + taskSrv, + caseSrv, + observableSrv, + logSrv, + alertSrv, + ec + ) + } + } +} + +class RunResponder( + responderName: String, + parameters: JsObject, + responderSrv: ResponderSrv, + actionSrv: ActionSrv, + taskSrv: TaskSrv, + caseSrv: CaseSrv, + observableSrv: ObservableSrv, + logSrv: LogSrv, + alertSrv: AlertSrv, + implicit val ec: ExecutionContext +) extends Notifier + with AuditRenderer { + override val name: String = "RunResponder" + + def getEntity(audit: Audit)(implicit graph: Graph): Try[(Product with Entity, JsObject)] = + audit + .objectEntityId + .flatMap { objectId => + audit.objectType.map { + case "Task" => taskSrv.get(objectId).project(_.by.by(taskToJson)).getOrFail("Task") + case "Case" => caseSrv.get(objectId).project(_.by.by(caseToJson)).getOrFail("Case") + case "Observable" => observableSrv.get(objectId).project(_.by.by(observableToJson)).getOrFail("Observable") + case "Log" => logSrv.get(objectId).project(_.by.by(logToJson)).getOrFail("Log") + case "Alert" => alertSrv.get(objectId).project(_.by.by(alertToJson)).getOrFail("Alert") + case objectType => Failure(NotFoundError(s"objectType $objectType is not recognised")) + } + } + .getOrElse(Failure(NotFoundError("Object not present in the audit"))) + + override def execute( + audit: Audit with Entity, + context: Option[Entity], + `object`: Option[Entity], + organisation: Organisation with Entity, + user: Option[User with Entity] + )(implicit graph: Graph): Future[Unit] = + if (user.isDefined) + Future.failed(BadConfigurationError("The notification runResponder must not be applied on user")) + else + for { + (entity, entityJson) <- Future.fromTry(getEntity(audit)) + workers <- responderSrv.getRespondersByName(responderName, organisation._id) + (worker, cortexIds) <- Future.fromTry(workers.headOption.toTry(Failure(NotFoundError(s"Responder $responderName not found")))) + authContext = LocalUserSrv.getSystemAuthContext.changeOrganisation(organisation._id, Permissions.all) + _ <- actionSrv.execute(entity, cortexIds.headOption, worker.id, parameters)(OWrites[Entity](_ => entityJson), authContext) + } yield () +} diff --git a/thehive/app/org/thp/thehive/TheHiveModule.scala b/thehive/app/org/thp/thehive/TheHiveModule.scala index 8f44a0273b..4c752f9926 100644 --- a/thehive/app/org/thp/thehive/TheHiveModule.scala +++ b/thehive/app/org/thp/thehive/TheHiveModule.scala @@ -54,6 +54,7 @@ class TheHiveModule(environment: Environment, configuration: Configuration) exte triggerBindings.addBinding.to[JobFinishedProvider] triggerBindings.addBinding.to[LogInMyTaskProvider] triggerBindings.addBinding.to[TaskAssignedProvider] + triggerBindings.addBinding.to[CaseShareProvider] val notifierBindings = ScalaMultibinder.newSetBinder[NotifierProvider](binder) notifierBindings.addBinding.to[AppendToFileProvider] diff --git a/thehive/app/org/thp/thehive/services/notification/NotificationActor.scala b/thehive/app/org/thp/thehive/services/notification/NotificationActor.scala index a49724452e..390ba0fdaa 100644 --- a/thehive/app/org/thp/thehive/services/notification/NotificationActor.scala +++ b/thehive/app/org/thp/thehive/services/notification/NotificationActor.scala @@ -162,7 +162,7 @@ class NotificationActor @Inject() ( .getOrElse(organisation._id, Map.empty) .foreach { case (trigger, (inOrg, userIds)) if trigger.preFilter(audit, context, organisation) => - logger.debug(s"Notification trigger ${trigger.name} is applicable") + logger.debug(s"Notification trigger ${trigger.name} is applicable for $audit") if (userIds.nonEmpty) userSrv .getByIds(userIds: _*) @@ -198,7 +198,7 @@ class NotificationActor @Inject() ( } executeNotification(None, orgConfig, audit, context, obj, organisation) } - case (trigger, _) => logger.debug(s"Notification trigger ${trigger.name} is NOT applicable") + case (trigger, _) => logger.debug(s"Notification trigger ${trigger.name} is NOT applicable for $audit") } case _ => } diff --git a/thehive/app/org/thp/thehive/services/notification/triggers/CaseShared.scala b/thehive/app/org/thp/thehive/services/notification/triggers/CaseShared.scala new file mode 100644 index 0000000000..d439e38554 --- /dev/null +++ b/thehive/app/org/thp/thehive/services/notification/triggers/CaseShared.scala @@ -0,0 +1,24 @@ +package org.thp.thehive.services.notification.triggers + +import javax.inject.{Inject, Singleton} +import org.thp.scalligraph.models.Entity +import org.thp.thehive.models.{Audit, Organisation} +import play.api.Configuration +import play.api.libs.json.Json + +import scala.util.{Success, Try} + +@Singleton +class CaseShareProvider @Inject() extends TriggerProvider { + override val name: String = "CaseShared" + override def apply(config: Configuration): Try[Trigger] = Success(new CaseShared()) +} + +class CaseShared() extends Trigger { + override val name: String = "CaseShared" + + override def preFilter(audit: Audit with Entity, context: Option[Entity], organisation: Organisation with Entity): Boolean = + audit.action == Audit.update && audit + .objectType + .contains("Case") && audit.details.flatMap(d => Try(Json.parse(d)).toOption).exists(d => (d \ "share").isDefined) +}