Skip to content

Commit

Permalink
#1619 Add the trigger "CaseShared" and the responders "RunAnalyzer" a…
Browse files Browse the repository at this point in the history
…nd "RunResponder"
  • Loading branch information
To-om committed Nov 5, 2020
1 parent adb032a commit 58fb2d3
Show file tree
Hide file tree
Showing 8 changed files with 248 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand All @@ -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")
()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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 ()
}
Original file line number Diff line number Diff line change
@@ -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 ()
}
1 change: 1 addition & 0 deletions thehive/app/org/thp/thehive/TheHiveModule.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: _*)
Expand Down Expand Up @@ -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 _ =>
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}

0 comments on commit 58fb2d3

Please sign in to comment.