diff --git a/ScalliGraph b/ScalliGraph index dbb55e144b..a35da92a5b 160000 --- a/ScalliGraph +++ b/ScalliGraph @@ -1 +1 @@ -Subproject commit dbb55e144b87d83d9bf520510e1a4b4d14a32fe0 +Subproject commit a35da92a5b313fe25fb924ecd90466823a2a71c6 diff --git a/cortex/connector/src/main/scala/org/thp/thehive/connector/cortex/controllers/v0/ActionCtrl.scala b/cortex/connector/src/main/scala/org/thp/thehive/connector/cortex/controllers/v0/ActionCtrl.scala index 7b92fdde83..9a4b1b4817 100644 --- a/cortex/connector/src/main/scala/org/thp/thehive/connector/cortex/controllers/v0/ActionCtrl.scala +++ b/cortex/connector/src/main/scala/org/thp/thehive/connector/cortex/controllers/v0/ActionCtrl.scala @@ -3,8 +3,8 @@ package org.thp.thehive.connector.cortex.controllers.v0 import javax.inject.{Inject, Named, Singleton} import org.thp.scalligraph.auth.AuthContext import org.thp.scalligraph.controllers.{Entrypoint, FieldsParser} -import org.thp.scalligraph.models.{Database, Entity, Schema} -import org.thp.scalligraph.query.{ParamQuery, PublicProperty, Query, SubType} +import org.thp.scalligraph.models.{Database, Entity, UMapping} +import org.thp.scalligraph.query._ import org.thp.scalligraph.traversal.TraversalOps._ import org.thp.scalligraph.traversal.{IteratorOutput, Traversal} import org.thp.thehive.connector.cortex.controllers.v0.Conversion._ @@ -13,7 +13,7 @@ import org.thp.thehive.connector.cortex.models.{Action, ActionContext, RichActio import org.thp.thehive.connector.cortex.services.ActionOps._ import org.thp.thehive.connector.cortex.services.{ActionSrv, EntityHelper} import org.thp.thehive.controllers.v0.Conversion.{toObjectType, _} -import org.thp.thehive.controllers.v0.{AuditRenderer, IdOrName, OutputParam, QueryableCtrl} +import org.thp.thehive.controllers.v0._ import org.thp.thehive.models._ import org.thp.thehive.services._ import play.api.libs.json.{JsObject, Json, OWrites} @@ -21,11 +21,11 @@ import play.api.mvc.{AnyContent, Results, Action => PlayAction} import scala.concurrent.{ExecutionContext, Future} import scala.reflect.runtime.{universe => ru} + @Singleton class ActionCtrl @Inject() ( - entrypoint: Entrypoint, - @Named("with-thehive-schema") db: Database, - properties: Properties, + override val entrypoint: Entrypoint, + @Named("with-thehive-schema") override val db: Database, actionSrv: ActionSrv, entityHelper: EntityHelper, caseSrv: CaseSrv, @@ -33,26 +33,51 @@ class ActionCtrl @Inject() ( observableSrv: ObservableSrv, logSrv: LogSrv, alertSrv: AlertSrv, - schema: Schema, - implicit val executionContext: ExecutionContext -) extends QueryableCtrl - with AuditRenderer { + implicit val executionContext: ExecutionContext, + override val queryExecutor: QueryExecutor, + override val publicData: PublicAction +) extends AuditRenderer + with QueryCtrl { implicit val entityWrites: OWrites[Entity] = OWrites[Entity] { entity => db.roTransaction { implicit graph => - entity match { - case c: Case => caseToJson(caseSrv.get(c)).getOrFail("Case") - case t: Task => taskToJson(taskSrv.get(t)).getOrFail("Task") - case o: Observable => observableToJson(observableSrv.get(o)).getOrFail("Observable") - case l: Log => logToJson(logSrv.get(l)).getOrFail("Log") - case a: Alert => alertToJson(alertSrv.get(a)).getOrFail("Alert") - } + entity match { + case c: Case => caseToJson(caseSrv.get(c)).getOrFail("Case") + case t: Task => taskToJson(taskSrv.get(t)).getOrFail("Task") + case o: Observable => observableToJson(observableSrv.get(o)).getOrFail("Observable") + case l: Log => logToJson(logSrv.get(l)).getOrFail("Log") + case a: Alert => alertToJson(alertSrv.get(a)).getOrFail("Alert") } - .getOrElse(Json.obj("_type" -> entity._label, "_id" -> entity._id)) + }.getOrElse(Json.obj("_type" -> entity._label, "_id" -> entity._id)) } - override val entityName: String = "action" - override val publicProperties: List[PublicProperty[_, _]] = properties.action + def create: PlayAction[AnyContent] = + entrypoint("create action") + .extract("action", FieldsParser[InputAction]) + .asyncAuth { implicit request => + val action: InputAction = request.body("action") + val tryEntity = db.roTransaction { implicit graph => + entityHelper.get(toObjectType(action.objectType), action.objectId, Permissions.manageAction) + } + for { + entity <- Future.fromTry(tryEntity) + action <- actionSrv.execute(entity, action.cortexId, action.responderId, action.parameters.getOrElse(JsObject.empty)) + } yield Results.Ok(action.toJson) + } + + def getByEntity(objectType: String, objectId: String): PlayAction[AnyContent] = + entrypoint("get by entity") + .authRoTransaction(db) { implicit request => implicit graph => + for { + entity <- entityHelper.get(toObjectType(objectType), objectId, Permissions.manageAction) + } yield Results.Ok(actionSrv.listForEntity(entity._id).toJson) + } +} + +@Singleton +class PublicAction @Inject() (actionSrv: ActionSrv) extends PublicData { + + override val entityName: String = "action" override val initialQuery: Query = Query.init[Traversal.V[Action]]("listAction", (graph, authContext) => actionSrv.startTraversal(graph).visible(authContext)) override val getQuery: ParamQuery[IdOrName] = Query.initWithParam[IdOrName, Traversal.V[Action]]( @@ -66,7 +91,6 @@ class ActionCtrl @Inject() ( (range, actionSteps, _) => actionSteps.richPage(range.from, range.to, withTotal = true)(_.richAction) ) override val outputQuery: Query = Query.output[RichAction, Traversal.V[Action]](_.richAction) - val actionsQuery: Query = new Query { override val name: String = "actions" override def checkFrom(t: ru.Type): Boolean = @@ -81,26 +105,15 @@ class ActionCtrl @Inject() ( } override val extraQueries: Seq[ParamQuery[_]] = Seq(actionsQuery) - - def create: PlayAction[AnyContent] = - entrypoint("create action") - .extract("action", FieldsParser[InputAction]) - .asyncAuth { implicit request => - val action: InputAction = request.body("action") - val tryEntity = db.roTransaction { implicit graph => - entityHelper.get(toObjectType(action.objectType), action.objectId, Permissions.manageAction) - } - for { - entity <- Future.fromTry(tryEntity) - action <- actionSrv.execute(entity, action.cortexId, action.responderId, action.parameters.getOrElse(JsObject.empty)) - } yield Results.Ok(action.toJson) - } - - def getByEntity(objectType: String, objectId: String): PlayAction[AnyContent] = - entrypoint("get by entity") - .authRoTransaction(db) { implicit request => implicit graph => - for { - entity <- entityHelper.get(toObjectType(objectType), objectId, Permissions.manageAction) - } yield Results.Ok(actionSrv.listForEntity(entity._id).toJson) - } + override val publicProperties: PublicProperties = + PublicPropertyListBuilder[Action] + .property("responderId", UMapping.string)(_.field.readonly) + .property("objectType", UMapping.string)(_.select(_.context.domainMap(o => fromObjectType(o._label))).readonly) + .property("status", UMapping.string)(_.field.readonly) + .property("startDate", UMapping.date)(_.field.readonly) + .property("objectId", UMapping.id)(_.select(_.out[ActionContext]._id).readonly) + .property("responderName", UMapping.string.optional)(_.field.readonly) + .property("cortexId", UMapping.string.optional)(_.field.readonly) + .property("tlp", UMapping.int.optional)(_.field.readonly) + .build } diff --git a/cortex/connector/src/main/scala/org/thp/thehive/connector/cortex/controllers/v0/AnalyzerTemplateCtrl.scala b/cortex/connector/src/main/scala/org/thp/thehive/connector/cortex/controllers/v0/AnalyzerTemplateCtrl.scala index 3a88f5a331..3ac3589927 100644 --- a/cortex/connector/src/main/scala/org/thp/thehive/connector/cortex/controllers/v0/AnalyzerTemplateCtrl.scala +++ b/cortex/connector/src/main/scala/org/thp/thehive/connector/cortex/controllers/v0/AnalyzerTemplateCtrl.scala @@ -5,8 +5,8 @@ import java.util.zip.ZipFile import com.google.inject.name.Named import javax.inject.{Inject, Singleton} import org.thp.scalligraph.controllers.{Entrypoint, FFile, FieldsParser} -import org.thp.scalligraph.models.{Database, Entity} -import org.thp.scalligraph.query.{ParamQuery, PropertyUpdater, PublicProperty, Query} +import org.thp.scalligraph.models.{Database, Entity, UMapping} +import org.thp.scalligraph.query._ import org.thp.scalligraph.traversal.TraversalOps._ import org.thp.scalligraph.traversal.{IteratorOutput, Traversal} import org.thp.thehive.connector.cortex.controllers.v0.Conversion._ @@ -14,9 +14,8 @@ import org.thp.thehive.connector.cortex.dto.v0.InputAnalyzerTemplate import org.thp.thehive.connector.cortex.models.AnalyzerTemplate import org.thp.thehive.connector.cortex.services.AnalyzerTemplateSrv import org.thp.thehive.controllers.v0.Conversion._ -import org.thp.thehive.controllers.v0.{IdOrName, OutputParam, QueryableCtrl} +import org.thp.thehive.controllers.v0.{IdOrName, OutputParam, PublicData, QueryCtrl} import org.thp.thehive.models.Permissions -import play.api.Logger import play.api.libs.json.{JsFalse, JsObject, JsTrue} import play.api.mvc.{Action, AnyContent, Results} @@ -24,29 +23,12 @@ import scala.util.{Failure, Success} @Singleton class AnalyzerTemplateCtrl @Inject() ( - entrypoint: Entrypoint, - @Named("with-thehive-cortex-schema") db: Database, - properties: Properties, - analyzerTemplateSrv: AnalyzerTemplateSrv -) extends QueryableCtrl { - - lazy val logger: Logger = Logger(getClass) - override val entityName: String = "analyzerTemplate" - override val publicProperties: List[PublicProperty[_, _]] = properties.analyzerTemplate - override val initialQuery: Query = - Query.init[Traversal.V[AnalyzerTemplate]]("listAnalyzerTemplate", (graph, _) => analyzerTemplateSrv.startTraversal(graph)) - override val getQuery: ParamQuery[IdOrName] = Query.initWithParam[IdOrName, Traversal.V[AnalyzerTemplate]]( - "getReportTemplace", - FieldsParser[IdOrName], - (param, graph, _) => analyzerTemplateSrv.get(param.idOrName)(graph) - ) - override val pageQuery: ParamQuery[OutputParam] = - Query.withParam[OutputParam, Traversal.V[AnalyzerTemplate], IteratorOutput]( - "page", - FieldsParser[OutputParam], - (range, analyzerTemplateTraversal, _) => analyzerTemplateTraversal.page(range.from, range.to, withTotal = true) - ) - override val outputQuery: Query = Query.output[AnalyzerTemplate with Entity] + override val entrypoint: Entrypoint, + @Named("with-thehive-cortex-schema") override val db: Database, + analyzerTemplateSrv: AnalyzerTemplateSrv, + override val queryExecutor: QueryExecutor, + override val publicData: PublicAnalyzerTemplate +) extends QueryCtrl { def get(id: String): Action[AnyContent] = entrypoint("get content") @@ -97,7 +79,7 @@ class AnalyzerTemplateCtrl @Inject() ( def update(id: String): Action[AnyContent] = entrypoint("update template") - .extract("template", FieldsParser.update("template", properties.analyzerTemplate)) + .extract("template", FieldsParser.update("template", publicData.publicProperties)) .authPermittedTransaction(db, Permissions.manageAnalyzerTemplate) { implicit request => implicit graph => val propertyUpdaters: Seq[PropertyUpdater] = request.body("template") @@ -108,3 +90,27 @@ class AnalyzerTemplateCtrl @Inject() ( } } + +@Singleton +class PublicAnalyzerTemplate @Inject() (analyzerTemplateSrv: AnalyzerTemplateSrv) extends PublicData { + override val entityName: String = "analyzerTemplate" + override val initialQuery: Query = + Query.init[Traversal.V[AnalyzerTemplate]]("listAnalyzerTemplate", (graph, _) => analyzerTemplateSrv.startTraversal(graph)) + override val getQuery: ParamQuery[IdOrName] = Query.initWithParam[IdOrName, Traversal.V[AnalyzerTemplate]]( + "getReportTemplate", + FieldsParser[IdOrName], + (param, graph, _) => analyzerTemplateSrv.get(param.idOrName)(graph) + ) + override val pageQuery: ParamQuery[OutputParam] = + Query.withParam[OutputParam, Traversal.V[AnalyzerTemplate], IteratorOutput]( + "page", + FieldsParser[OutputParam], + (range, analyzerTemplateTraversal, _) => analyzerTemplateTraversal.page(range.from, range.to, withTotal = true) + ) + override val outputQuery: Query = Query.output[AnalyzerTemplate with Entity] + override val publicProperties: PublicProperties = PublicPropertyListBuilder[AnalyzerTemplate] + .property("analyzerId", UMapping.string)(_.rename("workerId").readonly) + .property("reportType", UMapping.string)(_.field.readonly) + .property("content", UMapping.string)(_.field.updatable) + .build +} diff --git a/cortex/connector/src/main/scala/org/thp/thehive/connector/cortex/controllers/v0/CortexQueryExecutor.scala b/cortex/connector/src/main/scala/org/thp/thehive/connector/cortex/controllers/v0/CortexQueryExecutor.scala index 75b95fae78..36ebfe3e10 100644 --- a/cortex/connector/src/main/scala/org/thp/thehive/connector/cortex/controllers/v0/CortexQueryExecutor.scala +++ b/cortex/connector/src/main/scala/org/thp/thehive/connector/cortex/controllers/v0/CortexQueryExecutor.scala @@ -19,16 +19,15 @@ import scala.reflect.runtime.{universe => ru} @Singleton class CortexQueryExecutor @Inject() ( - queryCtrlBuilder: QueryCtrlBuilder, - @Named("with-thehive-cortex-schema") implicit val db: Database, - jobCtrl: JobCtrl, - reportCtrl: AnalyzerTemplateCtrl, - actionCtrl: ActionCtrl, - analyzerTemplateCtrl: AnalyzerTemplateCtrl + @Named("with-thehive-cortex-schema") implicit override val db: Database, + job: PublicJob, + report: PublicAnalyzerTemplate, + action: PublicAction, + analyzerTemplate: PublicAnalyzerTemplate ) extends QueryExecutor { - lazy val controllers: List[QueryableCtrl] = actionCtrl :: reportCtrl :: jobCtrl :: analyzerTemplateCtrl :: Nil + lazy val controllers: List[PublicData] = action :: report :: job :: analyzerTemplate :: Nil - override lazy val publicProperties: List[PublicProperty[_, _]] = controllers.flatMap(_.publicProperties) + override lazy val publicProperties: PublicProperties = controllers.map(_.publicProperties).reduce(_ ++ _) override lazy val queries: Seq[ParamQuery[_]] = controllers.map(_.initialQuery) ::: @@ -56,17 +55,12 @@ class CortexQueryExecutor @Inject() ( } override val version: (Int, Int) = 0 -> 1 - - val job: QueryCtrl = queryCtrlBuilder(jobCtrl, this) - val report: QueryCtrl = queryCtrlBuilder(analyzerTemplateCtrl, this) - val action: QueryCtrl = queryCtrlBuilder(actionCtrl, this) - val analyzerTemplate: QueryCtrl = queryCtrlBuilder(analyzerTemplateCtrl, this) } class CortexParentIdInputFilter(parentId: String) extends InputQuery[Traversal.Unk, Traversal.Unk] { override def apply( db: Database, - publicProperties: List[PublicProperty[_, _]], + publicProperties: PublicProperties, traversalType: ru.Type, traversal: Traversal.Unk, authContext: AuthContext @@ -84,7 +78,7 @@ class CortexParentIdInputFilter(parentId: String) extends InputQuery[Traversal.U class CortexParentQueryInputFilter(parentFilter: InputQuery[Traversal.Unk, Traversal.Unk]) extends InputQuery[Traversal.Unk, Traversal.Unk] { override def apply( db: Database, - publicProperties: List[PublicProperty[_, _]], + publicProperties: PublicProperties, traversalType: ru.Type, traversal: Traversal.Unk, authContext: AuthContext @@ -106,7 +100,7 @@ class CortexChildQueryInputFilter(childType: String, childFilter: InputQuery[Tra extends InputQuery[Traversal.Unk, Traversal.Unk] { override def apply( db: Database, - publicProperties: List[PublicProperty[_, _]], + publicProperties: PublicProperties, traversalType: ru.Type, traversal: Traversal.Unk, authContext: AuthContext diff --git a/cortex/connector/src/main/scala/org/thp/thehive/connector/cortex/controllers/v0/JobCtrl.scala b/cortex/connector/src/main/scala/org/thp/thehive/connector/cortex/controllers/v0/JobCtrl.scala index 84da6d1632..f3002105ee 100644 --- a/cortex/connector/src/main/scala/org/thp/thehive/connector/cortex/controllers/v0/JobCtrl.scala +++ b/cortex/connector/src/main/scala/org/thp/thehive/connector/cortex/controllers/v0/JobCtrl.scala @@ -3,8 +3,8 @@ package org.thp.thehive.connector.cortex.controllers.v0 import com.google.inject.name.Named import javax.inject.{Inject, Singleton} import org.thp.scalligraph.controllers.{Entrypoint, FieldsParser} -import org.thp.scalligraph.models.Database -import org.thp.scalligraph.query.{ParamQuery, PublicProperty, Query} +import org.thp.scalligraph.models.{Database, UMapping} +import org.thp.scalligraph.query._ import org.thp.scalligraph.traversal.TraversalOps._ import org.thp.scalligraph.traversal.{IteratorOutput, Traversal} import org.thp.scalligraph.{AuthorizationError, ErrorHandler} @@ -13,48 +13,25 @@ import org.thp.thehive.connector.cortex.models.{Job, RichJob} import org.thp.thehive.connector.cortex.services.JobOps._ import org.thp.thehive.connector.cortex.services.JobSrv import org.thp.thehive.controllers.v0.Conversion._ -import org.thp.thehive.controllers.v0.{IdOrName, OutputParam, QueryableCtrl} +import org.thp.thehive.controllers.v0.{IdOrName, OutputParam, PublicData, QueryCtrl} import org.thp.thehive.models.{Permissions, RichCase, RichObservable} import org.thp.thehive.services.ObservableOps._ import org.thp.thehive.services.ObservableSrv -import play.api.Logger import play.api.mvc.{Action, AnyContent, Results} import scala.concurrent.{ExecutionContext, Future} @Singleton class JobCtrl @Inject() ( - entrypoint: Entrypoint, - @Named("with-thehive-cortex-schema") db: Database, - properties: Properties, + override val entrypoint: Entrypoint, + @Named("with-thehive-cortex-schema") override val db: Database, jobSrv: JobSrv, observableSrv: ObservableSrv, errorHandler: ErrorHandler, - implicit val ec: ExecutionContext -) extends QueryableCtrl - with JobRenderer { - lazy val logger: Logger = Logger(getClass) - override val entityName: String = "job" - override val publicProperties: List[PublicProperty[_, _]] = properties.job - override val initialQuery: Query = - Query.init[Traversal.V[Job]]("listJob", (graph, authContext) => jobSrv.startTraversal(graph).visible(authContext)) - override val getQuery: ParamQuery[IdOrName] = Query.initWithParam[IdOrName, Traversal.V[Job]]( - "getJob", - FieldsParser[IdOrName], - (param, graph, authContext) => jobSrv.get(param.idOrName)(graph).visible(authContext) - ) - override val pageQuery: ParamQuery[OutputParam] = - Query.withParam[OutputParam, Traversal.V[Job], IteratorOutput]( - "page", - FieldsParser[OutputParam], { - case (OutputParam(from, to, _, withParents), jobSteps, authContext) if withParents > 0 => - jobSteps.richPage(from, to, withTotal = true)(_.richJobWithCustomRenderer(jobParents(_)(authContext))(authContext)) - case (range, jobSteps, authContext) => - jobSteps.richPage(range.from, range.to, withTotal = true)(_.richJob(authContext).domainMap((_, None: Option[(RichObservable, RichCase)]))) - } - ) - override val outputQuery: Query = Query.outputWithContext[RichJob, Traversal.V[Job]]((jobSteps, authContext) => jobSteps.richJob(authContext)) - + implicit val ec: ExecutionContext, + override val queryExecutor: QueryExecutor, + override val publicData: PublicJob +) extends QueryCtrl { def get(jobId: String): Action[AnyContent] = entrypoint("get job") .authRoTransaction(db) { implicit request => implicit graph => @@ -76,18 +53,51 @@ class JobCtrl @Inject() ( val analyzerId: String = request.body("analyzerId") val cortexId: String = request.body("cortexId") db.roTransaction { implicit graph => - val artifactId: String = request.body("artifactId") - for { - o <- observableSrv.getByIds(artifactId).richObservable.getOrFail("Observable") - c <- observableSrv.getByIds(artifactId).`case`.getOrFail("Case") - } yield (o, c) - } - .fold(error => errorHandler.onServerError(request, error), { + val artifactId: String = request.body("artifactId") + for { + o <- observableSrv.getByIds(artifactId).richObservable.getOrFail("Observable") + c <- observableSrv.getByIds(artifactId).`case`.getOrFail("Case") + } yield (o, c) + }.fold( + error => errorHandler.onServerError(request, error), + { case (o, c) => jobSrv .submit(cortexId, analyzerId, o, c) .map(j => Results.Created(j.toJson)) - }) + } + ) } else Future.failed(AuthorizationError("Job creation not allowed")) } } + +@Singleton +class PublicJob @Inject() (jobSrv: JobSrv) extends PublicData with JobRenderer { + override val entityName: String = "job" + override val initialQuery: Query = + Query.init[Traversal.V[Job]]("listJob", (graph, authContext) => jobSrv.startTraversal(graph).visible(authContext)) + override val getQuery: ParamQuery[IdOrName] = Query.initWithParam[IdOrName, Traversal.V[Job]]( + "getJob", + FieldsParser[IdOrName], + (param, graph, authContext) => jobSrv.get(param.idOrName)(graph).visible(authContext) + ) + override val pageQuery: ParamQuery[OutputParam] = + Query.withParam[OutputParam, Traversal.V[Job], IteratorOutput]( + "page", + FieldsParser[OutputParam], + { + case (OutputParam(from, to, _, withParents), jobSteps, authContext) if withParents > 0 => + jobSteps.richPage(from, to, withTotal = true)(_.richJobWithCustomRenderer(jobParents(_)(authContext))(authContext)) + case (range, jobSteps, authContext) => + jobSteps.richPage(range.from, range.to, withTotal = true)(_.richJob(authContext).domainMap((_, None: Option[(RichObservable, RichCase)]))) + } + ) + override val outputQuery: Query = Query.outputWithContext[RichJob, Traversal.V[Job]]((jobSteps, authContext) => jobSteps.richJob(authContext)) + override val publicProperties: PublicProperties = PublicPropertyListBuilder[Job] + .property("analyzerId", UMapping.string)(_.rename("workerId").readonly) + .property("cortexId", UMapping.string.optional)(_.field.readonly) + .property("startDate", UMapping.date)(_.field.readonly) + .property("status", UMapping.string)(_.field.readonly) + .property("analyzerDefinition", UMapping.string)(_.rename("workerDefinition").readonly) + .build +} diff --git a/cortex/connector/src/main/scala/org/thp/thehive/connector/cortex/controllers/v0/Properties.scala b/cortex/connector/src/main/scala/org/thp/thehive/connector/cortex/controllers/v0/Properties.scala deleted file mode 100644 index d2a3673eed..0000000000 --- a/cortex/connector/src/main/scala/org/thp/thehive/connector/cortex/controllers/v0/Properties.scala +++ /dev/null @@ -1,41 +0,0 @@ -package org.thp.thehive.connector.cortex.controllers.v0 - -import javax.inject.{Inject, Singleton} -import org.thp.scalligraph.models.UMapping -import org.thp.scalligraph.query.{PublicProperty, PublicPropertyListBuilder} -import org.thp.scalligraph.traversal.TraversalOps._ -import org.thp.thehive.connector.cortex.models.{Action, ActionContext, AnalyzerTemplate, Job} -import org.thp.thehive.connector.cortex.services.ActionOps._ -import org.thp.thehive.controllers.v0.Conversion.fromObjectType - -@Singleton -class Properties @Inject() () { - - lazy val action: List[PublicProperty[_, _]] = - PublicPropertyListBuilder[Action] - .property("responderId", UMapping.string)(_.field.readonly) - .property("objectType", UMapping.string)(_.select(_.context.domainMap(o => fromObjectType(o._label))).readonly) - .property("status", UMapping.string)(_.field.readonly) - .property("startDate", UMapping.date)(_.field.readonly) - .property("objectId", UMapping.id)(_.select(_.out[ActionContext]._id).readonly) - .property("responderName", UMapping.string.optional)(_.field.readonly) - .property("cortexId", UMapping.string.optional)(_.field.readonly) - .property("tlp", UMapping.int.optional)(_.field.readonly) - .build - - lazy val analyzerTemplate: List[PublicProperty[_, _]] = - PublicPropertyListBuilder[AnalyzerTemplate] - .property("analyzerId", UMapping.string)(_.rename("workerId").readonly) - .property("reportType", UMapping.string)(_.field.readonly) - .property("content", UMapping.string)(_.field.updatable) - .build - - val job: List[PublicProperty[_, _]] = - PublicPropertyListBuilder[Job] - .property("analyzerId", UMapping.string)(_.rename("workerId").readonly) - .property("cortexId", UMapping.string.optional)(_.field.readonly) - .property("startDate", UMapping.date)(_.field.readonly) - .property("status", UMapping.string)(_.field.readonly) - .property("analyzerDefinition", UMapping.string)(_.rename("workerDefinition").readonly) - .build -} diff --git a/cortex/connector/src/main/scala/org/thp/thehive/connector/cortex/controllers/v0/Router.scala b/cortex/connector/src/main/scala/org/thp/thehive/connector/cortex/controllers/v0/Router.scala index 9dac1e8383..7968aa17d4 100644 --- a/cortex/connector/src/main/scala/org/thp/thehive/connector/cortex/controllers/v0/Router.scala +++ b/cortex/connector/src/main/scala/org/thp/thehive/connector/cortex/controllers/v0/Router.scala @@ -7,37 +7,37 @@ import play.api.routing.sird._ @Singleton class Router @Inject() ( - jobCtrl: JobCtrl, + val jobCtrl: JobCtrl, analyzerCtrl: AnalyzerCtrl, - actionCtrl: ActionCtrl, + val actionCtrl: ActionCtrl, cortexQueryExecutor: CortexQueryExecutor, - reportCtrl: AnalyzerTemplateCtrl, + val reportCtrl: AnalyzerTemplateCtrl, responderCtrl: ResponderCtrl ) extends SimpleRouter { override def routes: Routes = { case GET(p"/job/$jobId<[^/]*>") => jobCtrl.get(jobId) - case POST(p"/job/_search") => cortexQueryExecutor.job.search - case POST(p"/job/_stats") => cortexQueryExecutor.job.stats + case POST(p"/job/_search") => jobCtrl.search + case POST(p"/job/_stats") => jobCtrl.stats case POST(p"/job") => jobCtrl.create // Audit ok case POST(p"/action") => actionCtrl.create // Audit ok - case POST(p"/action/_search") => cortexQueryExecutor.action.search - case POST(p"/action/_stats") => cortexQueryExecutor.action.stats - case GET(p"/action") => cortexQueryExecutor.action.search + case POST(p"/action/_search") => actionCtrl.search + case POST(p"/action/_stats") => actionCtrl.stats + case GET(p"/action") => actionCtrl.search case GET(p"/action/$entityType<[^/]*>/$entityId<[^/]*>") => actionCtrl.getByEntity(entityType, entityId) case GET(p"/analyzer/template/content/$analyzerId<[^/]*>$x*") => reportCtrl.get(analyzerId) - case POST(p"/analyzer/template/_import") => reportCtrl.importTemplates // Audit ok - case POST(p"/analyzer/template/_search") => cortexQueryExecutor.report.search - case POST(p"/analyzer/template") => reportCtrl.create() // Audit ok + case POST(p"/analyzer/template/_import") => reportCtrl.importTemplates // Audit ok + case POST(p"/analyzer/template/_search") => reportCtrl.search + case POST(p"/analyzer/template") => reportCtrl.create() // Audit ok case DELETE(p"/analyzer/template/$analyzerTemplateId<[^/]*>") => reportCtrl.delete(analyzerTemplateId) // Audit ok case GET(p"/analyzer/template/$analyzerTemplateId<[^/]*>") => reportCtrl.get(analyzerTemplateId) case PATCH(p"/analyzer/template/$analyzerTemplateId<[^/]*>") => reportCtrl.update(analyzerTemplateId) // Audit ok case GET(p"/report/template/content/$analyzerId<[^/]*>$x*") => reportCtrl.get(analyzerId) - case POST(p"/report/template/_import") => reportCtrl.importTemplates // Audit ok - case POST(p"/report/template/_search") => cortexQueryExecutor.report.search - case POST(p"/report/template") => reportCtrl.create() // Audit ok + case POST(p"/report/template/_import") => reportCtrl.importTemplates // Audit ok + case POST(p"/report/template/_search") => reportCtrl.search + case POST(p"/report/template") => reportCtrl.create() // Audit ok case DELETE(p"/report/template/$analyzerTemplateId<[^/]*>") => reportCtrl.delete(analyzerTemplateId) // Audit ok case GET(p"/report/template/$analyzerTemplateId<[^/]*>") => reportCtrl.get(analyzerTemplateId) case PATCH(p"/report/template/$analyzerTemplateId<[^/]*>") => reportCtrl.update(analyzerTemplateId) // Audit ok diff --git a/cortex/connector/src/test/scala/org/thp/thehive/connector/cortex/controllers/v0/JobCtrlTest.scala b/cortex/connector/src/test/scala/org/thp/thehive/connector/cortex/controllers/v0/JobCtrlTest.scala index d7183384c8..413cf23808 100644 --- a/cortex/connector/src/test/scala/org/thp/thehive/connector/cortex/controllers/v0/JobCtrlTest.scala +++ b/cortex/connector/src/test/scala/org/thp/thehive/connector/cortex/controllers/v0/JobCtrlTest.scala @@ -3,6 +3,7 @@ package org.thp.thehive.connector.cortex.controllers.v0 import org.thp.cortex.client.{CortexClient, TestCortexClientProvider} import org.thp.scalligraph.AppBuilder import org.thp.scalligraph.models.{Database, Schema} +import org.thp.scalligraph.query.QueryExecutor import org.thp.scalligraph.traversal.TraversalOps._ import org.thp.thehive.connector.cortex.models.TheHiveCortexSchemaProvider import org.thp.thehive.connector.cortex.services.{Connector, CortexActor, TestConnector} @@ -16,12 +17,12 @@ class JobCtrlTest extends PlaySpecification with TestAppBuilder { override def appConfigure: AppBuilder = super .appConfigure - .`override`(_.bindToProvider[Schema, TheHiveCortexSchemaProvider]) .`override`( _.bindActor[CortexActor]("cortex-actor") .bindToProvider[CortexClient, TestCortexClientProvider] .bind[Connector, TestConnector] .bindToProvider[Schema, TheHiveCortexSchemaProvider] + .bindToProvider[QueryExecutor, TheHiveCortexQueryExecutorProvider] .bindNamedToProvider[Database, BasicDatabaseProvider]("with-thehive-cortex-schema") ) @@ -49,7 +50,7 @@ class JobCtrlTest extends PlaySpecification with TestAppBuilder { } } """.stripMargin)) - val resultSearch = app[CortexQueryExecutor].job.search(requestSearch) + val resultSearch = app[JobCtrl].search(requestSearch) status(resultSearch) shouldEqual 200 } @@ -77,7 +78,7 @@ class JobCtrlTest extends PlaySpecification with TestAppBuilder { }] } """.stripMargin)) - val result = app[CortexQueryExecutor].job.stats(request) + val result = app[JobCtrl].stats(request) status(result) shouldEqual 200 } diff --git a/cortex/connector/src/test/scala/org/thp/thehive/connector/cortex/controllers/v0/TheHiveCortexQueryExecutorProvider.scala b/cortex/connector/src/test/scala/org/thp/thehive/connector/cortex/controllers/v0/TheHiveCortexQueryExecutorProvider.scala new file mode 100644 index 0000000000..9ff1cadd47 --- /dev/null +++ b/cortex/connector/src/test/scala/org/thp/thehive/connector/cortex/controllers/v0/TheHiveCortexQueryExecutorProvider.scala @@ -0,0 +1,10 @@ +package org.thp.thehive.connector.cortex.controllers.v0 + +import javax.inject.{Inject, Provider} +import org.thp.scalligraph.query.QueryExecutor +import org.thp.thehive.controllers.v0.TheHiveQueryExecutor + +class TheHiveCortexQueryExecutorProvider @Inject() (thehiveQueryExecutor: TheHiveQueryExecutor, cortexQueryExecutor: CortexQueryExecutor) + extends Provider[QueryExecutor] { + override def get(): QueryExecutor = thehiveQueryExecutor ++ cortexQueryExecutor +} diff --git a/thehive/app/org/thp/thehive/controllers/v0/AlertCtrl.scala b/thehive/app/org/thp/thehive/controllers/v0/AlertCtrl.scala index 08cf9dafdd..3d173d35c3 100644 --- a/thehive/app/org/thp/thehive/controllers/v0/AlertCtrl.scala +++ b/thehive/app/org/thp/thehive/controllers/v0/AlertCtrl.scala @@ -6,12 +6,12 @@ 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.{Entrypoint, FString, FieldsParser} -import org.thp.scalligraph.models.Database -import org.thp.scalligraph.query.{ParamQuery, PropertyUpdater, PublicProperty, Query} +import org.thp.scalligraph.controllers._ +import org.thp.scalligraph.models.{Database, IdMapping, UMapping} +import org.thp.scalligraph.query._ import org.thp.scalligraph.traversal.TraversalOps._ import org.thp.scalligraph.traversal.{Converter, IteratorOutput, Traversal} -import org.thp.scalligraph.{AuthorizationError, InvalidFormatAttributeError, RichSeq} +import org.thp.scalligraph.{AuthorizationError, BadRequestError, InvalidFormatAttributeError, RichSeq} import org.thp.thehive.controllers.v0.Conversion._ import org.thp.thehive.dto.v0.{InputAlert, InputObservable, OutputSimilarCase} import org.thp.thehive.models._ @@ -20,9 +20,9 @@ import org.thp.thehive.services.CaseOps._ import org.thp.thehive.services.CaseTemplateOps._ import org.thp.thehive.services.ObservableOps._ import org.thp.thehive.services.OrganisationOps._ +import org.thp.thehive.services.TagOps._ import org.thp.thehive.services.UserOps._ import org.thp.thehive.services._ -import play.api.Logger import play.api.libs.json.{JsArray, JsObject, Json} import play.api.mvc.{Action, AnyContent, Results} @@ -30,59 +30,19 @@ import scala.util.{Failure, Success, Try} @Singleton class AlertCtrl @Inject() ( - entrypoint: Entrypoint, - properties: Properties, + override val entrypoint: Entrypoint, alertSrv: AlertSrv, caseTemplateSrv: CaseTemplateSrv, observableSrv: ObservableSrv, observableTypeSrv: ObservableTypeSrv, attachmentSrv: AttachmentSrv, - organisationSrv: OrganisationSrv, auditSrv: AuditSrv, userSrv: UserSrv, caseSrv: CaseSrv, - @Named("with-thehive-schema") implicit val db: Database -) extends QueryableCtrl { - lazy val logger: Logger = Logger(getClass) - override val entityName: String = "alert" - override val publicProperties: List[PublicProperty[_, _]] = properties.alert - override val initialQuery: Query = - Query - .init[Traversal.V[Alert]]("listAlert", (graph, authContext) => organisationSrv.get(authContext.organisation)(graph).alerts) - override val getQuery: ParamQuery[IdOrName] = Query.initWithParam[IdOrName, Traversal.V[Alert]]( - "getAlert", - FieldsParser[IdOrName], - (param, graph, authContext) => alertSrv.get(param.idOrName)(graph).visible(authContext) - ) - override val pageQuery: ParamQuery[OutputParam] = - Query.withParam[OutputParam, Traversal.V[Alert], IteratorOutput]( - "page", - FieldsParser[OutputParam], - (range, alertSteps, _) => - alertSteps - .richPage(range.from, range.to, withTotal = true) { alerts => - alerts.project(_.by(_.richAlert).by(_.observables.richObservable.fold)) - } - ) - override val outputQuery: Query = Query.output[RichAlert, Traversal.V[Alert]](_.richAlert) - override val extraQueries: Seq[ParamQuery[_]] = Seq( - Query[Traversal.V[Alert], Traversal.V[Case]]("cases", (alertSteps, _) => alertSteps.`case`), - Query[Traversal.V[Alert], Traversal.V[Observable]]("observables", (alertSteps, _) => alertSteps.observables), - Query[ - Traversal.V[Alert], - Traversal[(RichAlert, Seq[RichObservable]), JMap[String, Any], Converter[(RichAlert, Seq[RichObservable]), JMap[String, Any]]] - ]( - "withObservables", - (alertSteps, _) => - alertSteps - .project( - _.by(_.richAlert) - .by(_.observables.richObservable.fold) - ) - ), - Query.output[(RichAlert, Seq[RichObservable])] - ) - + override val publicData: PublicAlert, + @Named("with-thehive-schema") implicit val db: Database, + override val queryExecutor: QueryExecutor +) extends QueryCtrl { def create: Action[AnyContent] = entrypoint("create alert") .extract("alert", FieldsParser[InputAlert]) @@ -95,19 +55,20 @@ class AlertCtrl @Inject() ( val customFields = inputAlert.customFields.map(c => c.name -> c.value).toMap val caseTemplate = caseTemplateName.flatMap(caseTemplateSrv.get(_).visible.headOption) for { - organisation <- userSrv - .current - .organisations(Permissions.manageAlert) - .get(request.organisation) - .orFail(AuthorizationError("Operation not permitted")) + organisation <- + userSrv + .current + .organisations(Permissions.manageAlert) + .get(request.organisation) + .orFail(AuthorizationError("Operation not permitted")) richObservables <- observables.toTry(createObservable).map(_.flatten) richAlert <- alertSrv.create(request.body("alert").toAlert, organisation, inputAlert.tags, customFields, caseTemplate) _ <- auditSrv.mergeAudits(richObservables.toTry(o => alertSrv.addObservable(richAlert.alert, o)))(_ => Success(())) } yield Results.Created((richAlert -> richObservables).toJson) } - def alertSimilarityRenderer( - implicit authContext: AuthContext + def alertSimilarityRenderer(implicit + authContext: AuthContext ): Traversal.V[Alert] => Traversal[JsArray, JList[JMap[String, Any]], Converter[JsArray, JList[JMap[String, Any]]]] = _.similarCases .fold @@ -165,7 +126,7 @@ class AlertCtrl @Inject() ( def update(alertId: String): Action[AnyContent] = entrypoint("update alert") - .extract("alert", FieldsParser.update("alert", publicProperties)) + .extract("alert", FieldsParser.update("alert", publicData.publicProperties)) .authTransaction(db) { implicit request => implicit graph => val propertyUpdaters: Seq[PropertyUpdater] = request.body("alert") alertSrv @@ -181,10 +142,11 @@ class AlertCtrl @Inject() ( entrypoint("delete alert") .authTransaction(db) { implicit request => implicit graph => for { - alert <- alertSrv - .get(alertId) - .can(Permissions.manageAlert) - .getOrFail("Alert") + alert <- + alertSrv + .get(alertId) + .can(Permissions.manageAlert) + .getOrFail("Alert") _ <- alertSrv.remove(alert) } yield Results.NoContent } @@ -197,10 +159,11 @@ class AlertCtrl @Inject() ( ids .toTry { alertId => for { - alert <- alertSrv - .get(alertId) - .can(Permissions.manageAlert) - .getOrFail("Alert") + alert <- + alertSrv + .get(alertId) + .can(Permissions.manageAlert) + .getOrFail("Alert") _ <- alertSrv.remove(alert) } yield () } @@ -270,11 +233,12 @@ class AlertCtrl @Inject() ( .authTransaction(db) { implicit request => implicit graph => val caseTemplate: Option[String] = request.body("caseTemplate") for { - (alert, organisation) <- alertSrv - .get(alertId) - .can(Permissions.manageAlert) - .alertUserOrganisation(Permissions.manageCase) - .getOrFail("Alert") + (alert, organisation) <- + alertSrv + .get(alertId) + .can(Permissions.manageAlert) + .alertUserOrganisation(Permissions.manageCase) + .getOrFail("Alert") alertWithCaseTemplate = caseTemplate.fold(alert)(ct => alert.copy(caseTemplate = Some(ct))) richCase <- alertSrv.createCase(alertWithCaseTemplate, None, organisation) } yield Results.Created(richCase.toJson) @@ -306,8 +270,8 @@ class AlertCtrl @Inject() ( } } - private def createObservable(observable: InputObservable)( - implicit graph: Graph, + private def createObservable(observable: InputObservable)(implicit + graph: Graph, authContext: AuthContext ): Try[Seq[RichObservable]] = observableTypeSrv @@ -326,3 +290,109 @@ class AlertCtrl @Inject() ( case dataType => observable.data.toTry(d => observableSrv.create(observable.toObservable, dataType, d, observable.tags, Nil)) } } + +@Singleton +class PublicAlert @Inject() ( + alertSrv: AlertSrv, + organisationSrv: OrganisationSrv, + customFieldSrv: CustomFieldSrv +) extends PublicData { + override val entityName: String = "alert" + override val initialQuery: Query = + Query + .init[Traversal.V[Alert]]("listAlert", (graph, authContext) => organisationSrv.get(authContext.organisation)(graph).alerts) + override val getQuery: ParamQuery[IdOrName] = Query.initWithParam[IdOrName, Traversal.V[Alert]]( + "getAlert", + FieldsParser[IdOrName], + (param, graph, authContext) => alertSrv.get(param.idOrName)(graph).visible(authContext) + ) + override val pageQuery: ParamQuery[OutputParam] = + Query.withParam[OutputParam, Traversal.V[Alert], IteratorOutput]( + "page", + FieldsParser[OutputParam], + (range, alertSteps, _) => + alertSteps + .richPage(range.from, range.to, withTotal = true) { alerts => + alerts.project(_.by(_.richAlert).by(_.observables.richObservable.fold)) + } + ) + override val outputQuery: Query = Query.output[RichAlert, Traversal.V[Alert]](_.richAlert) + override val extraQueries: Seq[ParamQuery[_]] = Seq( + Query[Traversal.V[Alert], Traversal.V[Case]]("cases", (alertSteps, _) => alertSteps.`case`), + Query[Traversal.V[Alert], Traversal.V[Observable]]("observables", (alertSteps, _) => alertSteps.observables), + Query[ + Traversal.V[Alert], + Traversal[(RichAlert, Seq[RichObservable]), JMap[String, Any], Converter[(RichAlert, Seq[RichObservable]), JMap[String, Any]]] + ]( + "withObservables", + (alertSteps, _) => + alertSteps + .project( + _.by(_.richAlert) + .by(_.observables.richObservable.fold) + ) + ), + Query.output[(RichAlert, Seq[RichObservable])] + ) + override val publicProperties: PublicProperties = + PublicPropertyListBuilder[Alert] + .property("type", UMapping.string)(_.field.updatable) + .property("source", UMapping.string)(_.field.updatable) + .property("sourceRef", UMapping.string)(_.field.updatable) + .property("title", UMapping.string)(_.field.updatable) + .property("description", UMapping.string)(_.field.updatable) + .property("severity", UMapping.int)(_.field.updatable) + .property("date", UMapping.date)(_.field.updatable) + .property("lastSyncDate", UMapping.date.optional)(_.field.updatable) + .property("tags", UMapping.string.set)( + _.select(_.tags.displayName) + .custom { (_, value, vertex, _, graph, authContext) => + alertSrv + .get(vertex)(graph) + .getOrFail("Alert") + .flatMap(alert => alertSrv.updateTagNames(alert, value)(graph, authContext)) + .map(_ => Json.obj("tags" -> value)) + } + ) + .property("flag", UMapping.boolean)(_.field.updatable) + .property("tlp", UMapping.int)(_.field.updatable) + .property("pap", UMapping.int)(_.field.updatable) + .property("read", UMapping.boolean)(_.field.updatable) + .property("follow", UMapping.boolean)(_.field.updatable) + .property("status", UMapping.string)( + _.select( + _.project( + _.byValue(_.read) + .by(_.`case`.limit(1).count) + ).domainMap { + case (false, caseCount) if caseCount == 0L => "New" + case (false, _) => "Updated" + case (true, caseCount) if caseCount == 0L => "Ignored" + case (true, _) => "Imported" + } + ).readonly + ) + .property("summary", UMapping.string.optional)(_.field.updatable) + .property("user", UMapping.string)(_.field.updatable) + .property("customFields", UMapping.jsonNative)(_.subSelect { + case (FPathElem(_, FPathElem(name, _)), alertSteps) => + alertSteps.customFields(name).jsonValue + case (_, alertSteps) => alertSteps.customFields.nameJsonValue.fold.domainMap(JsObject(_)) + }.custom { + case (FPathElem(_, FPathElem(name, _)), value, vertex, _, graph, authContext) => + for { + c <- alertSrv.getByIds(vertex.id.toString)(graph).getOrFail("Alert") + _ <- alertSrv.setOrCreateCustomField(c, name, Some(value))(graph, authContext) + } yield Json.obj(s"customField.$name" -> value) + case (FPathElem(_, FPathEmpty), values: JsObject, vertex, _, graph, authContext) => + for { + c <- alertSrv.get(vertex)(graph).getOrFail("Alert") + cfv <- values.fields.toTry { case (n, v) => customFieldSrv.getOrFail(n)(graph).map(_ -> v) } + _ <- alertSrv.updateCustomField(c, cfv)(graph, authContext) + } yield Json.obj("customFields" -> values) + + case _ => Failure(BadRequestError("Invalid custom fields format")) + }) + .property("case", IdMapping)(_.select(_.`case`._id).readonly) + .build +} diff --git a/thehive/app/org/thp/thehive/controllers/v0/AuditCtrl.scala b/thehive/app/org/thp/thehive/controllers/v0/AuditCtrl.scala index 7ca8c0e78e..feb5bb6a97 100644 --- a/thehive/app/org/thp/thehive/controllers/v0/AuditCtrl.scala +++ b/thehive/app/org/thp/thehive/controllers/v0/AuditCtrl.scala @@ -5,8 +5,8 @@ import akka.pattern.ask import akka.util.Timeout import javax.inject.{Inject, Named, Singleton} import org.thp.scalligraph.controllers.{Entrypoint, FieldsParser} -import org.thp.scalligraph.models.{Database, Schema} -import org.thp.scalligraph.query.{ParamQuery, Query} +import org.thp.scalligraph.models.{Database, IdMapping, UMapping} +import org.thp.scalligraph.query._ import org.thp.scalligraph.traversal.TraversalOps._ import org.thp.scalligraph.traversal.{IteratorOutput, Traversal} import org.thp.thehive.controllers.v0.Conversion._ @@ -22,43 +22,19 @@ import scala.concurrent.duration.DurationInt @Singleton class AuditCtrl @Inject() ( - entryPoint: Entrypoint, - properties: Properties, + override val entrypoint: Entrypoint, auditSrv: AuditSrv, @Named("flow-actor") flowActor: ActorRef, - val caseSrv: CaseSrv, - val taskSrv: TaskSrv, - val userSrv: UserSrv, - @Named("with-thehive-schema") implicit val db: Database, - implicit val schema: Schema, - implicit val ec: ExecutionContext -) extends QueryableCtrl - with AuditRenderer { - + override val publicData: PublicAudit, + @Named("with-thehive-schema") implicit override val db: Database, + implicit val ec: ExecutionContext, + val queryExecutor: QueryExecutor +) extends AuditRenderer + with QueryCtrl { implicit val timeout: Timeout = Timeout(5.minutes) - override val getQuery: ParamQuery[IdOrName] = Query.initWithParam[IdOrName, Traversal.V[Audit]]( - "getAudit", - FieldsParser[IdOrName], - (param, graph, authContext) => auditSrv.get(param.idOrName)(graph).visible(authContext) - ) - - override val entityName: String = "audit" - - override val initialQuery: Query = - Query.init[Traversal.V[Audit]]("listAudit", (graph, authContext) => auditSrv.startTraversal(graph).visible(authContext)) - override val publicProperties: List[org.thp.scalligraph.query.PublicProperty[_, _]] = properties.audit - - override val pageQuery: ParamQuery[org.thp.thehive.controllers.v0.OutputParam] = - Query.withParam[OutputParam, Traversal.V[Audit], IteratorOutput]( - "page", - FieldsParser[OutputParam], - (range, auditSteps, _) => auditSteps.richPage(range.from, range.to, withTotal = true)(_.richAudit) - ) - override val outputQuery: Query = Query.output[RichAudit, Traversal.V[Audit]](_.richAudit) - def flow(caseId: Option[String]): Action[AnyContent] = - entryPoint("audit flow") + entrypoint("audit flow") .asyncAuth { implicit request => (flowActor ? FlowId(request.organisation, caseId.filterNot(_ == "any"))).map { case AuditIds(auditIds) if auditIds.isEmpty => Results.Ok(JsArray.empty) @@ -86,3 +62,37 @@ class AuditCtrl @Inject() ( } } } + +@Singleton +class PublicAudit @Inject() (auditSrv: AuditSrv) extends PublicData { + override val getQuery: ParamQuery[IdOrName] = Query.initWithParam[IdOrName, Traversal.V[Audit]]( + "getAudit", + FieldsParser[IdOrName], + (param, graph, authContext) => auditSrv.get(param.idOrName)(graph).visible(authContext) + ) + + override val entityName: String = "audit" + + override val initialQuery: Query = + Query.init[Traversal.V[Audit]]("listAudit", (graph, authContext) => auditSrv.startTraversal(graph).visible(authContext)) + + override val pageQuery: ParamQuery[org.thp.thehive.controllers.v0.OutputParam] = + Query.withParam[OutputParam, Traversal.V[Audit], IteratorOutput]( + "page", + FieldsParser[OutputParam], + (range, auditSteps, _) => auditSteps.richPage(range.from, range.to, withTotal = true)(_.richAudit) + ) + override val outputQuery: Query = Query.output[RichAudit, Traversal.V[Audit]](_.richAudit) + + override val publicProperties: PublicProperties = + PublicPropertyListBuilder[Audit] + .property("operation", UMapping.string)(_.rename("action").readonly) + .property("details", UMapping.string)(_.field.readonly) + .property("objectType", UMapping.string.optional)(_.field.readonly) + .property("objectId", UMapping.string.optional)(_.field.readonly) + .property("base", UMapping.boolean)(_.rename("mainAction").readonly) + .property("startDate", UMapping.date)(_.rename("_createdAt").readonly) + .property("requestId", UMapping.string)(_.field.readonly) + .property("rootId", IdMapping)(_.select(_.context._id).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 260ec8306d..b7334a37d3 100644 --- a/thehive/app/org/thp/thehive/controllers/v0/CaseCtrl.scala +++ b/thehive/app/org/thp/thehive/controllers/v0/CaseCtrl.scala @@ -1,68 +1,42 @@ package org.thp.thehive.controllers.v0 +import java.lang.{Long => JLong} +import java.util.Date + import javax.inject.{Inject, Named, Singleton} -import org.thp.scalligraph.controllers.{Entrypoint, FieldsParser} -import org.thp.scalligraph.models.Database -import org.thp.scalligraph.query.{ParamQuery, PropertyUpdater, PublicProperty, Query} +import org.thp.scalligraph.controllers.{Entrypoint, FPathElem, FPathEmpty, FieldsParser} +import org.thp.scalligraph.models.{Database, UMapping} +import org.thp.scalligraph.query._ import org.thp.scalligraph.traversal.TraversalOps._ -import org.thp.scalligraph.traversal.{IteratorOutput, Traversal} +import org.thp.scalligraph.traversal.{Converter, IteratorOutput, Traversal} import org.thp.scalligraph.{RichSeq, _} import org.thp.thehive.controllers.v0.Conversion._ import org.thp.thehive.dto.v0.{InputCase, InputTask} import org.thp.thehive.models._ import org.thp.thehive.services.CaseOps._ import org.thp.thehive.services.CaseTemplateOps._ +import org.thp.thehive.services.CustomFieldOps._ import org.thp.thehive.services.OrganisationOps._ +import org.thp.thehive.services.TagOps._ import org.thp.thehive.services.UserOps._ import org.thp.thehive.services._ -import play.api.Logger -import play.api.libs.json.{JsArray, JsNumber, JsObject} +import play.api.libs.json._ import play.api.mvc.{Action, AnyContent, Results} -import scala.util.Success +import scala.util.{Failure, Success} @Singleton class CaseCtrl @Inject() ( - entrypoint: Entrypoint, - properties: Properties, + override val entrypoint: Entrypoint, caseSrv: CaseSrv, caseTemplateSrv: CaseTemplateSrv, tagSrv: TagSrv, userSrv: UserSrv, - organisationSrv: OrganisationSrv, - @Named("with-thehive-schema") implicit val db: Database -) extends QueryableCtrl - with CaseRenderer { - - lazy val logger: Logger = Logger(getClass) - override val entityName: String = "case" - override val publicProperties: List[PublicProperty[_, _]] = properties.`case` - override val initialQuery: Query = - Query.init[Traversal.V[Case]]("listCase", (graph, authContext) => organisationSrv.get(authContext.organisation)(graph).cases) - override val getQuery: ParamQuery[IdOrName] = Query.initWithParam[IdOrName, Traversal.V[Case]]( - "getCase", - FieldsParser[IdOrName], - (param, graph, authContext) => caseSrv.get(param.idOrName)(graph).visible(authContext) - ) - override val pageQuery: ParamQuery[OutputParam] = Query.withParam[OutputParam, Traversal.V[Case], IteratorOutput]( - "page", - FieldsParser[OutputParam], { - case (OutputParam(from, to, withStats, _), caseSteps, authContext) => - caseSteps - .richPage(from, to, withTotal = true) { - case c if withStats => - c.richCaseWithCustomRenderer(caseStatsRenderer(authContext))(authContext) - case c => - c.richCase(authContext).domainMap(_ -> JsObject.empty) - } - } - ) - override val outputQuery: Query = Query.outputWithContext[RichCase, Traversal.V[Case]]((caseSteps, authContext) => caseSteps.richCase(authContext)) - override val extraQueries: Seq[ParamQuery[_]] = Seq( - Query[Traversal.V[Case], Traversal.V[Observable]]("observables", (caseSteps, authContext) => caseSteps.observables(authContext)), - Query[Traversal.V[Case], Traversal.V[Task]]("tasks", (caseSteps, authContext) => caseSteps.tasks(authContext)) - ) - + override val publicData: PublicCase, + override val queryExecutor: QueryExecutor, + @Named("with-thehive-schema") implicit override val db: Database +) extends CaseRenderer + with QueryCtrl { def create: Action[AnyContent] = entrypoint("create case") .extract("case", FieldsParser[InputCase]) @@ -74,11 +48,12 @@ class CaseCtrl @Inject() ( val inputTasks: Seq[InputTask] = request.body("tasks") val customFields = inputCase.customFields.map(c => (c.name, c.value, c.order)) for { - organisation <- userSrv - .current - .organisations(Permissions.manageCase) - .get(request.organisation) - .orFail(AuthorizationError("Operation not permitted")) + organisation <- + userSrv + .current + .organisations(Permissions.manageCase) + .get(request.organisation) + .orFail(AuthorizationError("Operation not permitted")) caseTemplate <- caseTemplateName.map(caseTemplateSrv.get(_).visible.richCaseTemplate.getOrFail("CaseTemplate")).flip user <- inputCase.user.map(userSrv.get(_).visible.getOrFail("User")).flip tags <- inputCase.tags.toTry(tagSrv.getOrCreate) @@ -103,22 +78,21 @@ class CaseCtrl @Inject() ( .get(caseIdOrNumber) .visible val stats: Option[Boolean] = request.body("stats") - if (stats.contains(true)) { + if (stats.contains(true)) c.richCaseWithCustomRenderer(caseStatsRenderer(request)) .getOrFail("Case") .map { case (richCase, stats) => Results.Ok(richCase.toJson.as[JsObject] + ("stats" -> stats)) } - } else { + else c.richCase .getOrFail("Case") .map(richCase => Results.Ok(richCase.toJson)) - } } def update(caseIdOrNumber: String): Action[AnyContent] = entrypoint("update case") - .extract("case", FieldsParser.update("case", properties.`case`)) + .extract("case", FieldsParser.update("case", publicData.publicProperties)) .authTransaction(db) { implicit request => implicit graph => val propertyUpdaters: Seq[PropertyUpdater] = request.body("case") caseSrv @@ -138,7 +112,7 @@ class CaseCtrl @Inject() ( def bulkUpdate: Action[AnyContent] = entrypoint("update case") - .extract("case", FieldsParser.update("case", properties.`case`)) + .extract("case", FieldsParser.update("case", publicData.publicProperties)) .extract("idsOrNumbers", FieldsParser.seq[String].on("ids")) .authTransaction(db) { implicit request => implicit graph => val propertyUpdaters: Seq[PropertyUpdater] = request.body("case") @@ -175,10 +149,11 @@ class CaseCtrl @Inject() ( entrypoint("delete case") .authTransaction(db) { implicit request => implicit graph => for { - c <- caseSrv - .get(caseIdOrNumber) - .can(Permissions.manageCase) - .getOrFail("Case") + c <- + caseSrv + .get(caseIdOrNumber) + .can(Permissions.manageCase) + .getOrFail("Case") _ <- caseSrv.remove(c) } yield Results.NoContent } @@ -218,3 +193,161 @@ class CaseCtrl @Inject() ( Success(Results.Ok(JsArray(relatedCases))) } } + +@Singleton +class PublicCase @Inject() ( + caseSrv: CaseSrv, + organisationSrv: OrganisationSrv, + userSrv: UserSrv, + customFieldSrv: CustomFieldSrv, + @Named("with-thehive-schema") db: Database +) extends PublicData + with CaseRenderer { + override val entityName: String = "case" + override val initialQuery: Query = + Query.init[Traversal.V[Case]]("listCase", (graph, authContext) => organisationSrv.get(authContext.organisation)(graph).cases) + override val getQuery: ParamQuery[IdOrName] = Query.initWithParam[IdOrName, Traversal.V[Case]]( + "getCase", + FieldsParser[IdOrName], + (param, graph, authContext) => caseSrv.get(param.idOrName)(graph).visible(authContext) + ) + override val pageQuery: ParamQuery[OutputParam] = Query.withParam[OutputParam, Traversal.V[Case], IteratorOutput]( + "page", + FieldsParser[OutputParam], + { + case (OutputParam(from, to, withStats, _), caseSteps, authContext) => + caseSteps + .richPage(from, to, withTotal = true) { + case c if withStats => + c.richCaseWithCustomRenderer(caseStatsRenderer(authContext))(authContext) + case c => + c.richCase(authContext).domainMap(_ -> JsObject.empty) + } + } + ) + override val outputQuery: Query = Query.outputWithContext[RichCase, Traversal.V[Case]]((caseSteps, authContext) => caseSteps.richCase(authContext)) + override val extraQueries: Seq[ParamQuery[_]] = Seq( + Query[Traversal.V[Case], Traversal.V[Observable]]("observables", (caseSteps, authContext) => caseSteps.observables(authContext)), + Query[Traversal.V[Case], Traversal.V[Task]]("tasks", (caseSteps, authContext) => caseSteps.tasks(authContext)) + ) + override val publicProperties: PublicProperties = + PublicPropertyListBuilder[Case] + .property("caseId", UMapping.int)(_.rename("number").readonly) + .property("title", UMapping.string)(_.field.updatable) + .property("description", UMapping.string)(_.field.updatable) + .property("severity", UMapping.int)(_.field.updatable) + .property("startDate", UMapping.date)(_.field.updatable) + .property("endDate", UMapping.date.optional)(_.field.updatable) + .property("tags", UMapping.string.set)( + _.select(_.tags.displayName) + .custom { (_, value, vertex, _, graph, authContext) => + caseSrv + .get(vertex)(graph) + .getOrFail("Case") + .flatMap(`case` => caseSrv.updateTagNames(`case`, value)(graph, authContext)) + .map(_ => Json.obj("tags" -> value)) + } + ) + .property("flag", UMapping.boolean)(_.field.updatable) + .property("tlp", UMapping.int)(_.field.updatable) + .property("pap", UMapping.int)(_.field.updatable) + .property("status", UMapping.enum[CaseStatus.type])(_.field.updatable) + .property("summary", UMapping.string.optional)(_.field.updatable) + .property("owner", UMapping.string.optional)(_.select(_.user.value(_.login)).custom { (_, login, vertex, _, graph, authContext) => + for { + c <- caseSrv.get(vertex)(graph).getOrFail("Case") + user <- login.map(userSrv.get(_)(graph).getOrFail("User")).flip + _ <- user match { + case Some(u) => caseSrv.assign(c, u)(graph, authContext) + case None => caseSrv.unassign(c)(graph, authContext) + } + } yield Json.obj("owner" -> user.map(_.login)) + }) + .property("resolutionStatus", UMapping.string.optional)(_.select(_.resolutionStatus.value(_.value)).custom { + (_, resolutionStatus, vertex, _, graph, authContext) => + for { + c <- caseSrv.get(vertex)(graph).getOrFail("Case") + _ <- resolutionStatus match { + case Some(s) => caseSrv.setResolutionStatus(c, s)(graph, authContext) + case None => caseSrv.unsetResolutionStatus(c)(graph, authContext) + } + } yield Json.obj("resolutionStatus" -> resolutionStatus) + }) + .property("impactStatus", UMapping.string.optional)(_.select(_.impactStatus.value(_.value)).custom { + (_, impactStatus, vertex, _, graph, authContext) => + for { + c <- caseSrv.getByIds(vertex.id.toString)(graph).getOrFail("Case") + _ <- impactStatus match { + case Some(s) => caseSrv.setImpactStatus(c, s)(graph, authContext) + case None => caseSrv.unsetImpactStatus(c)(graph, authContext) + } + } yield Json.obj("impactStatus" -> impactStatus) + }) + .property("customFields", UMapping.jsonNative)(_.subSelect { + case (FPathElem(_, FPathElem(name, _)), caseSteps) => + caseSteps + .customFields(name) + .jsonValue + case (_, caseSteps) => caseSteps.customFields.nameJsonValue.fold.domainMap(JsObject(_)) + } + .filter { + case (FPathElem(_, FPathElem(name, _)), caseTraversal) => + db + .roTransaction(implicit graph => customFieldSrv.get(name).value(_.`type`).getOrFail("CustomField")) + .map { + case CustomFieldType.boolean => caseTraversal.customFields(name).value(_.booleanValue) + case CustomFieldType.date => caseTraversal.customFields(name).value(_.dateValue) + case CustomFieldType.float => caseTraversal.customFields(name).value(_.floatValue) + case CustomFieldType.integer => caseTraversal.customFields(name).value(_.integerValue) + case CustomFieldType.string => caseTraversal.customFields(name).value(_.stringValue) + } + .getOrElse(caseTraversal.constant2(null)) + case (_, caseTraversal) => caseTraversal.constant2(null) + } + .converter { + case FPathElem(_, FPathElem(name, _)) => + db + .roTransaction { implicit graph => + customFieldSrv.get(name).value(_.`type`).getOrFail("CustomField") + } + .map { + case CustomFieldType.boolean => new Converter[Any, JsValue] { def apply(x: JsValue): Any = x.as[Boolean] } + case CustomFieldType.date => new Converter[Any, JsValue] { def apply(x: JsValue): Any = x.as[Date] } + case CustomFieldType.float => new Converter[Any, JsValue] { def apply(x: JsValue): Any = x.as[Double] } + 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 }) + case _ => (x: JsValue) => x + } + .custom { + case (FPathElem(_, FPathElem(name, _)), value, vertex, _, graph, authContext) => + for { + c <- caseSrv.getByIds(vertex.id.toString)(graph).getOrFail("Case") + _ <- caseSrv.setOrCreateCustomField(c, name, Some(value), None)(graph, authContext) + } yield Json.obj(s"customField.$name" -> value) + case (FPathElem(_, FPathEmpty), values: JsObject, vertex, _, graph, authContext) => + for { + c <- caseSrv.get(vertex)(graph).getOrFail("Case") + cfv <- values.fields.toTry { case (n, v) => customFieldSrv.getOrFail(n)(graph).map(cf => (cf, v, None)) } + _ <- caseSrv.updateCustomField(c, cfv)(graph, authContext) + } yield Json.obj("customFields" -> values) + case _ => Failure(BadRequestError("Invalid custom fields format")) + }) + .property("computed.handlingDurationInHours", UMapping.long)( + _.select( + _.coalesce( + _.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 + ) + .build +} diff --git a/thehive/app/org/thp/thehive/controllers/v0/CaseTemplateCtrl.scala b/thehive/app/org/thp/thehive/controllers/v0/CaseTemplateCtrl.scala index dab4b233d4..bd429ce30f 100644 --- a/thehive/app/org/thp/thehive/controllers/v0/CaseTemplateCtrl.scala +++ b/thehive/app/org/thp/thehive/controllers/v0/CaseTemplateCtrl.scala @@ -1,50 +1,38 @@ package org.thp.thehive.controllers.v0 import javax.inject.{Inject, Named, Singleton} -import org.thp.scalligraph.RichSeq -import org.thp.scalligraph.controllers.{Entrypoint, FieldsParser} -import org.thp.scalligraph.models.Database -import org.thp.scalligraph.query.{ParamQuery, PropertyUpdater, PublicProperty, Query} +import org.scalactic.Accumulation._ +import org.thp.scalligraph.controllers._ +import org.thp.scalligraph.models.{Database, UMapping} +import org.thp.scalligraph.query._ import org.thp.scalligraph.traversal.TraversalOps._ import org.thp.scalligraph.traversal.{IteratorOutput, Traversal} +import org.thp.scalligraph.{AttributeCheckingError, BadRequestError, RichSeq} import org.thp.thehive.controllers.v0.Conversion._ -import org.thp.thehive.dto.v0.InputCaseTemplate +import org.thp.thehive.dto.v0.{InputCaseTemplate, InputTask} import org.thp.thehive.models.{CaseTemplate, Permissions, RichCaseTemplate} import org.thp.thehive.services.CaseTemplateOps._ import org.thp.thehive.services.OrganisationOps._ +import org.thp.thehive.services.TagOps._ +import org.thp.thehive.services.TaskOps._ import org.thp.thehive.services.UserOps._ import org.thp.thehive.services._ import play.api.Logger +import play.api.libs.json.{JsObject, Json} import play.api.mvc.{Action, AnyContent, Results} + +import scala.util.Failure @Singleton class CaseTemplateCtrl @Inject() ( - entrypoint: Entrypoint, - properties: Properties, + override val entrypoint: Entrypoint, caseTemplateSrv: CaseTemplateSrv, organisationSrv: OrganisationSrv, userSrv: UserSrv, auditSrv: AuditSrv, - @Named("with-thehive-schema") implicit val db: Database -) extends QueryableCtrl { - - lazy val logger: Logger = Logger(getClass) - override val entityName: String = "caseTemplate" - override val publicProperties: List[PublicProperty[_, _]] = properties.caseTemplate - override val initialQuery: Query = - Query - .init[Traversal.V[CaseTemplate]]("listCaseTemplate", (graph, authContext) => organisationSrv.get(authContext.organisation)(graph).caseTemplates) - override val getQuery: ParamQuery[IdOrName] = Query.initWithParam[IdOrName, Traversal.V[CaseTemplate]]( - "getCaseTemplate", - FieldsParser[IdOrName], - (param, graph, authContext) => caseTemplateSrv.get(param.idOrName)(graph).visible(authContext) - ) - override val pageQuery: ParamQuery[OutputParam] = Query.withParam[OutputParam, Traversal.V[CaseTemplate], IteratorOutput]( - "page", - FieldsParser[OutputParam], - (range, caseTemplateSteps, _) => caseTemplateSteps.richPage(range.from, range.to, withTotal = true)(_.richCaseTemplate) - ) - override val outputQuery: Query = Query.output[RichCaseTemplate, Traversal.V[CaseTemplate]](_.richCaseTemplate) - + override val publicData: PublicCaseTemplate, + @Named("with-thehive-schema") implicit override val db: Database, + override val queryExecutor: QueryExecutor +) extends QueryCtrl { def create: Action[AnyContent] = entrypoint("create case template") .extract("caseTemplate", FieldsParser[InputCaseTemplate]) @@ -71,7 +59,7 @@ class CaseTemplateCtrl @Inject() ( def update(caseTemplateNameOrId: String): Action[AnyContent] = entrypoint("update case template") - .extract("caseTemplate", FieldsParser.update("caseTemplate", publicProperties)) + .extract("caseTemplate", FieldsParser.update("caseTemplate", publicData.publicProperties)) .authTransaction(db) { implicit request => implicit graph => val propertyUpdaters: Seq[PropertyUpdater] = request.body("caseTemplate") caseTemplateSrv @@ -94,3 +82,83 @@ class CaseTemplateCtrl @Inject() ( } yield Results.Ok } } + +@Singleton +class PublicCaseTemplate @Inject() ( + caseTemplateSrv: CaseTemplateSrv, + organisationSrv: OrganisationSrv, + customFieldSrv: CustomFieldSrv, + userSrv: UserSrv, + taskSrv: TaskSrv +) extends PublicData { + lazy val logger: Logger = Logger(getClass) + override val entityName: String = "caseTemplate" + override val initialQuery: Query = + Query + .init[Traversal.V[CaseTemplate]]("listCaseTemplate", (graph, authContext) => organisationSrv.get(authContext.organisation)(graph).caseTemplates) + override val getQuery: ParamQuery[IdOrName] = Query.initWithParam[IdOrName, Traversal.V[CaseTemplate]]( + "getCaseTemplate", + FieldsParser[IdOrName], + (param, graph, authContext) => caseTemplateSrv.get(param.idOrName)(graph).visible(authContext) + ) + override val pageQuery: ParamQuery[OutputParam] = Query.withParam[OutputParam, Traversal.V[CaseTemplate], IteratorOutput]( + "page", + FieldsParser[OutputParam], + (range, caseTemplateSteps, _) => caseTemplateSteps.richPage(range.from, range.to, withTotal = true)(_.richCaseTemplate) + ) + override val outputQuery: Query = Query.output[RichCaseTemplate, Traversal.V[CaseTemplate]](_.richCaseTemplate) + override val publicProperties: PublicProperties = PublicPropertyListBuilder[CaseTemplate] + .property("name", UMapping.string)(_.field.updatable) + .property("displayName", UMapping.string)(_.field.updatable) + .property("titlePrefix", UMapping.string.optional)(_.field.updatable) + .property("description", UMapping.string.optional)(_.field.updatable) + .property("severity", UMapping.int.optional)(_.field.updatable) + .property("tags", UMapping.string.set)( + _.select(_.tags.displayName) + .custom { (_, value, vertex, _, graph, authContext) => + caseTemplateSrv + .get(vertex)(graph) + .getOrFail("CaseTemplate") + .flatMap(caseTemplate => caseTemplateSrv.updateTagNames(caseTemplate, value)(graph, authContext)) + .map(_ => Json.obj("tags" -> value)) + } + ) + .property("flag", UMapping.boolean)(_.field.updatable) + .property("tlp", UMapping.int.optional)(_.field.updatable) + .property("pap", UMapping.int.optional)(_.field.updatable) + .property("summary", UMapping.string.optional)(_.field.updatable) + .property("user", UMapping.string)(_.field.updatable) + .property("customFields", UMapping.jsonNative)(_.subSelect { + case (FPathElem(_, FPathElem(name, _)), caseTemplateSteps) => caseTemplateSteps.customFields(name).jsonValue + case (_, caseTemplateSteps) => caseTemplateSteps.customFields.nameJsonValue.fold.domainMap(JsObject(_)) + }.custom { + case (FPathElem(_, FPathElem(name, _)), value, vertex, _, graph, authContext) => + for { + c <- caseTemplateSrv.getByIds(vertex.id.toString)(graph).getOrFail("CaseTemplate") + _ <- caseTemplateSrv.setOrCreateCustomField(c, name, Some(value), None)(graph, authContext) + } yield Json.obj(s"customFields.$name" -> value) + case (FPathElem(_, FPathEmpty), values: JsObject, vertex, _, graph, authContext) => + for { + c <- caseTemplateSrv.get(vertex)(graph).getOrFail("CaseTemplate") + cfv <- values.fields.toTry { case (n, v) => customFieldSrv.getOrFail(n)(graph).map(_ -> v) } + _ <- caseTemplateSrv.updateCustomField(c, cfv)(graph, authContext) + } yield Json.obj("customFields" -> values) + case _ => Failure(BadRequestError("Invalid custom fields format")) + }) + .property("tasks", UMapping.jsonNative.sequence)(_.select(_.tasks.richTask.domainMap(_.toJson)).custom { // FIXME select the correct mapping + (_, value, vertex, _, graph, authContext) => + val fp = FieldsParser[InputTask] + + caseTemplateSrv.get(vertex)(graph).tasks.remove() + for { + caseTemplate <- caseTemplateSrv.getByIds(vertex.id.toString)(graph).getOrFail("CaseTemplate") + tasks <- value.validatedBy(t => fp(Field(t))).badMap(AttributeCheckingError(_)).toTry + createdTasks <- + tasks + .toTry(t => t.owner.map(userSrv.getOrFail(_)(graph)).flip.flatMap(owner => taskSrv.create(t.toTask, owner)(graph, authContext))) + _ <- createdTasks.toTry(t => caseTemplateSrv.addTask(caseTemplate, t.task)(graph, authContext)) + } yield Json.obj("tasks" -> createdTasks.map(_.toJson)) + }) + .build + +} diff --git a/thehive/app/org/thp/thehive/controllers/v0/CustomFieldCtrl.scala b/thehive/app/org/thp/thehive/controllers/v0/CustomFieldCtrl.scala index c9cc10b532..97f1f82062 100644 --- a/thehive/app/org/thp/thehive/controllers/v0/CustomFieldCtrl.scala +++ b/thehive/app/org/thp/thehive/controllers/v0/CustomFieldCtrl.scala @@ -2,12 +2,13 @@ package org.thp.thehive.controllers.v0 import javax.inject.{Inject, Named, Singleton} import org.thp.scalligraph.controllers.{Entrypoint, FieldsParser} -import org.thp.scalligraph.models.Database -import org.thp.scalligraph.query.PropertyUpdater +import org.thp.scalligraph.models.{Database, Entity, UMapping} +import org.thp.scalligraph.query._ import org.thp.scalligraph.traversal.TraversalOps._ +import org.thp.scalligraph.traversal.{IteratorOutput, Traversal} import org.thp.thehive.controllers.v0.Conversion._ import org.thp.thehive.dto.v0.InputCustomField -import org.thp.thehive.models.Permissions +import org.thp.thehive.models.{CustomField, Permissions} import org.thp.thehive.services.CustomFieldSrv import play.api.libs.json.{JsNumber, JsObject} import play.api.mvc.{Action, AnyContent, Results} @@ -16,12 +17,13 @@ import scala.util.Success @Singleton class CustomFieldCtrl @Inject() ( - entrypoint: Entrypoint, - @Named("with-thehive-schema") db: Database, - properties: Properties, - customFieldSrv: CustomFieldSrv -) extends AuditRenderer { - + override val entrypoint: Entrypoint, + @Named("with-thehive-schema") override val db: Database, + customFieldSrv: CustomFieldSrv, + override val publicData: PublicCustomField, + override val queryExecutor: QueryExecutor +) extends QueryCtrl + with AuditRenderer { def create: Action[AnyContent] = entrypoint("create custom field") .extract("customField", FieldsParser[InputCustomField]) @@ -60,7 +62,7 @@ class CustomFieldCtrl @Inject() ( def update(id: String): Action[AnyContent] = entrypoint("update custom field") - .extract("customField", FieldsParser.update("customField", properties.customField)) + .extract("customField", FieldsParser.update("customField", publicData.publicProperties)) .authPermittedTransaction(db, Permissions.manageCustomField) { implicit request => implicit graph => val propertyUpdaters: Seq[PropertyUpdater] = request.body("customField") @@ -82,3 +84,32 @@ class CustomFieldCtrl @Inject() ( } } } + +@Singleton +class PublicCustomField @Inject() (customFieldSrv: CustomFieldSrv) extends PublicData { + override val entityName: String = "CustomField" + override val initialQuery: Query = Query.init[Traversal.V[CustomField]]("listCustomField", (graph, _) => customFieldSrv.startTraversal(graph)) + override val pageQuery: ParamQuery[OutputParam] = Query.withParam[OutputParam, Traversal.V[CustomField], IteratorOutput]( + "page", + FieldsParser[OutputParam], + { + case (OutputParam(from, to, _, _), customFieldSteps, _) => + customFieldSteps.page(from, to, withTotal = true) + } + ) + override val outputQuery: Query = Query.output[CustomField with Entity] + override val getQuery: ParamQuery[IdOrName] = Query.initWithParam[IdOrName, Traversal.V[CustomField]]( + "getCustomField", + FieldsParser[IdOrName], + (param, graph, _) => customFieldSrv.get(param.idOrName)(graph) + ) + override val publicProperties: PublicProperties = + PublicPropertyListBuilder[CustomField] + .property("name", UMapping.string)(_.rename("displayName").updatable) + .property("description", UMapping.string)(_.field.updatable) + .property("reference", UMapping.string)(_.rename("name").readonly) + .property("mandatory", UMapping.boolean)(_.field.updatable) + .property("type", UMapping.string)(_.field.readonly) + .property("options", UMapping.json.sequence)(_.field.updatable) + .build +} diff --git a/thehive/app/org/thp/thehive/controllers/v0/DashboardCtrl.scala b/thehive/app/org/thp/thehive/controllers/v0/DashboardCtrl.scala index ed6196ff1c..ebe70400e8 100644 --- a/thehive/app/org/thp/thehive/controllers/v0/DashboardCtrl.scala +++ b/thehive/app/org/thp/thehive/controllers/v0/DashboardCtrl.scala @@ -1,9 +1,10 @@ package org.thp.thehive.controllers.v0 import javax.inject.{Inject, Named, Singleton} -import org.thp.scalligraph.controllers.{Entrypoint, FieldsParser} -import org.thp.scalligraph.models.Database -import org.thp.scalligraph.query.{ParamQuery, PropertyUpdater, PublicProperty, Query} +import org.thp.scalligraph.InvalidFormatAttributeError +import org.thp.scalligraph.controllers.{Entrypoint, FString, FieldsParser} +import org.thp.scalligraph.models.{Database, UMapping} +import org.thp.scalligraph.query._ import org.thp.scalligraph.traversal.TraversalOps._ import org.thp.scalligraph.traversal.{IteratorOutput, Traversal} import org.thp.thehive.controllers.v0.Conversion._ @@ -13,45 +14,20 @@ import org.thp.thehive.services.DashboardOps._ import org.thp.thehive.services.OrganisationOps._ import org.thp.thehive.services.UserOps._ import org.thp.thehive.services.{DashboardSrv, OrganisationSrv, UserSrv} +import play.api.libs.json.Json import play.api.mvc.{Action, AnyContent, Results} +import scala.util.Failure + @Singleton class DashboardCtrl @Inject() ( - entrypoint: Entrypoint, - properties: Properties, + override val entrypoint: Entrypoint, dashboardSrv: DashboardSrv, - organisationSrv: OrganisationSrv, userSrv: UserSrv, - @Named("with-thehive-schema") implicit val db: Database -) extends QueryableCtrl { - val entityName: String = "dashboard" - val publicProperties: List[PublicProperty[_, _]] = properties.dashboard - - val initialQuery: Query = - Query.init[Traversal.V[Dashboard]]( - "listDashboard", - (graph, authContext) => - Traversal - .union( - organisationSrv.filterTraversal(_).get(authContext.organisation).dashboards, - userSrv.filterTraversal(_).get(authContext.userId).dashboards - )(graph) - .dedup - ) - - override val getQuery: ParamQuery[IdOrName] = Query.initWithParam[IdOrName, Traversal.V[Dashboard]]( - "getDashboard", - FieldsParser[IdOrName], - (param, graph, authContext) => dashboardSrv.get(param.idOrName)(graph).visible(authContext) - ) - - val pageQuery: ParamQuery[OutputParam] = Query.withParam[OutputParam, Traversal.V[Dashboard], IteratorOutput]( - "page", - FieldsParser[OutputParam], - (range, dashboardSteps, _) => dashboardSteps.richPage(range.from, range.to, withTotal = true)(_.richDashboard) - ) - override val outputQuery: Query = Query.output[RichDashboard, Traversal.V[Dashboard]](_.richDashboard) - + @Named("with-thehive-schema") implicit val db: Database, + override val publicData: PublicDashboard, + override val queryExecutor: QueryExecutor +) extends QueryCtrl { def create: Action[AnyContent] = entrypoint("create dashboard") .extract("dashboard", FieldsParser[InputDashboard]) @@ -73,7 +49,7 @@ class DashboardCtrl @Inject() ( def update(dashboardId: String): Action[AnyContent] = entrypoint("update dashboard") - .extract("dashboard", FieldsParser.update("dashboard", properties.dashboard)) + .extract("dashboard", FieldsParser.update("dashboard", publicData.publicProperties)) .authTransaction(db) { implicit request => implicit graph => val propertyUpdaters: Seq[PropertyUpdater] = request.body("dashboard") dashboardSrv @@ -96,3 +72,67 @@ class DashboardCtrl @Inject() ( } } } + +@Singleton +class PublicDashboard @Inject() ( + dashboardSrv: DashboardSrv, + organisationSrv: OrganisationSrv, + userSrv: UserSrv, + @Named("with-thehive-schema") db: Database +) extends PublicData { + val entityName: String = "dashboard" + + val initialQuery: Query = + Query.init[Traversal.V[Dashboard]]( + "listDashboard", + (graph, authContext) => + Traversal + .union( + organisationSrv.filterTraversal(_).get(authContext.organisation)(db).dashboards, + userSrv.filterTraversal(_).get(authContext.userId)(db).dashboards + )(graph) + .dedup + ) + + override val getQuery: ParamQuery[IdOrName] = Query.initWithParam[IdOrName, Traversal.V[Dashboard]]( + "getDashboard", + FieldsParser[IdOrName], + (param, graph, authContext) => dashboardSrv.get(param.idOrName)(graph).visible(authContext) + ) + + val pageQuery: ParamQuery[OutputParam] = Query.withParam[OutputParam, Traversal.V[Dashboard], IteratorOutput]( + "page", + FieldsParser[OutputParam], + (range, dashboardSteps, _) => dashboardSteps.richPage(range.from, range.to, withTotal = true)(_.richDashboard) + ) + override val outputQuery: Query = Query.output[RichDashboard, Traversal.V[Dashboard]](_.richDashboard) + val publicProperties: PublicProperties = PublicPropertyListBuilder[Dashboard] + .property("title", UMapping.string)(_.field.updatable) + .property("description", UMapping.string)(_.field.updatable) + .property("definition", UMapping.string)(_.field.updatable) + .property("status", UMapping.string)( + _.select(_.organisation.fold.domainMap(d => if (d.isEmpty) "Private" else "Shared")).custom { // TODO replace by choose step + case (_, "Shared", vertex, _, graph, authContext) => + for { + dashboard <- dashboardSrv.get(vertex)(graph).filter(_.user.current(authContext)).getOrFail("Dashboard") + _ <- dashboardSrv.share(dashboard, authContext.organisation, writable = false)(graph, authContext) + } yield Json.obj("status" -> "Shared") + + case (_, "Private", vertex, _, graph, authContext) => + for { + d <- dashboardSrv.get(vertex)(graph).filter(_.user.current(authContext)).getOrFail("Dashboard") + _ <- dashboardSrv.unshare(d, authContext.organisation)(graph, authContext) + } yield Json.obj("status" -> "Private") + + case (_, "Deleted", vertex, _, graph, authContext) => + for { + d <- dashboardSrv.get(vertex)(graph).filter(_.user.current(authContext)).getOrFail("Dashboard") + _ <- dashboardSrv.remove(d)(graph, authContext) + } yield Json.obj("status" -> "Deleted") + + case (_, status, _, _, _, _) => + Failure(InvalidFormatAttributeError("status", "String", Set("Shared", "Private", "Deleted"), FString(status))) + } + ) + .build +} diff --git a/thehive/app/org/thp/thehive/controllers/v0/DescribeCtrl.scala b/thehive/app/org/thp/thehive/controllers/v0/DescribeCtrl.scala index a891d61be3..1aec31a7a1 100644 --- a/thehive/app/org/thp/thehive/controllers/v0/DescribeCtrl.scala +++ b/thehive/app/org/thp/thehive/controllers/v0/DescribeCtrl.scala @@ -67,83 +67,98 @@ class DescribeCtrl @Inject() ( path, injector .instanceOf(getClass.getClassLoader.loadClass(s"$packageName.$className")) - .asInstanceOf[QueryableCtrl] + .asInstanceOf[QueryCtrl] + .publicData .publicProperties + .list .flatMap(propertyToJson(name, _)) ) ).toOption val entityDescriptions: Seq[EntityDescription] = Seq( - EntityDescription("case", "/case", caseCtrl.publicProperties.flatMap(propertyToJson("case", _))), - EntityDescription("case_task", "/case/task", taskCtrl.publicProperties.flatMap(propertyToJson("case_task", _))), - EntityDescription("alert", "/alert", alertCtrl.publicProperties.flatMap(propertyToJson("alert", _))), - EntityDescription("case_artifact", "/case/artifact", observableCtrl.publicProperties.flatMap(propertyToJson("case_artifact", _))), - EntityDescription("user", "user", userCtrl.publicProperties.flatMap(propertyToJson("user", _))), - EntityDescription("case_task_log", "/case/task/log", logCtrl.publicProperties.flatMap(propertyToJson("case_task_log", _))), - EntityDescription("audit", "audit", auditCtrl.publicProperties.flatMap(propertyToJson("audit", _))) + EntityDescription("case", "/case", caseCtrl.publicData.publicProperties.list.flatMap(propertyToJson("case", _))), + EntityDescription("case_task", "/case/task", taskCtrl.publicData.publicProperties.list.flatMap(propertyToJson("case_task", _))), + EntityDescription("alert", "/alert", alertCtrl.publicData.publicProperties.list.flatMap(propertyToJson("alert", _))), + EntityDescription("case_artifact", "/case/artifact", observableCtrl.publicData.publicProperties.list.flatMap(propertyToJson("case_artifact", _))), + EntityDescription("user", "user", userCtrl.publicData.publicProperties.list.flatMap(propertyToJson("user", _))), + EntityDescription("case_task_log", "/case/task/log", logCtrl.publicData.publicProperties.list.flatMap(propertyToJson("case_task_log", _))), + EntityDescription("audit", "audit", auditCtrl.publicData.publicProperties.list.flatMap(propertyToJson("audit", _))) ) ++ describeCortexEntity("case_artifact_job", "/connector/cortex/job", "JobCtrl") ++ describeCortexEntity("action", "/connector/cortex/action", "ActionCtrl") implicit val propertyDescriptionWrites: Writes[PropertyDescription] = Json.writes[PropertyDescription].transform((_: JsObject) + ("description" -> JsString(""))) - def customFields: Seq[PropertyDescription] = db.roTransaction { implicit graph => - customFieldSrv.startTraversal.toSeq.map(cf => PropertyDescription(s"customFields.${cf.name}", cf.`type`.toString)) - } + def customFields: Seq[PropertyDescription] = + db.roTransaction { implicit graph => + customFieldSrv.startTraversal.toSeq.map(cf => PropertyDescription(s"customFields.${cf.name}", cf.`type`.toString)) + } - def customDescription(model: String, propertyName: String): Option[Seq[PropertyDescription]] = (model, propertyName) match { - case (_, "owner") => Some(Seq(PropertyDescription("owner", "user"))) - case ("case", "status") => - Some( - Seq(PropertyDescription("status", "enumeration", Seq(JsString("Open"), JsString("Resolved"), JsString("Deleted"), JsString("Duplicated")))) - ) - //case ("observable", "status") => - // Some(PropertyDescription("status", "enumeration", Seq(JsString("Ok")))) - //case ("observable", "dataType") => - // Some(PropertyDescription("status", "enumeration", Seq(JsString("sometesttype", "fqdn", "url", "regexp", "mail", "hash", "registry", "custom-type", "uri_path", "ip", "user-agent", "autonomous-system", "file", "mail_subject", "filename", "other", "domain")))) - case ("alert", "status") => - Some(Seq(PropertyDescription("status", "enumeration", Seq(JsString("New"), JsString("Updated"), JsString("Ignored"), JsString("Imported"))))) - case ("case_task", "status") => - Some( - Seq(PropertyDescription("status", "enumeration", Seq(JsString("Waiting"), JsString("InProgress"), JsString("Completed"), JsString("Cancel")))) - ) - case ("case", "impactStatus") => - Some(Seq(PropertyDescription("impactStatus", "enumeration", Seq(JsString("NoImpact"), JsString("WithImpact"), JsString("NotApplicable"))))) - case ("case", "resolutionStatus") => - Some( - Seq( - PropertyDescription( - "resolutionStatus", - "enumeration", - Seq(JsString("FalsePositive"), JsString("Duplicated"), JsString("Indeterminate"), JsString("TruePositive"), JsString("Other")) + def customDescription(model: String, propertyName: String): Option[Seq[PropertyDescription]] = + (model, propertyName) match { + case (_, "owner") => Some(Seq(PropertyDescription("owner", "user"))) + case ("case", "status") => + Some( + Seq(PropertyDescription("status", "enumeration", Seq(JsString("Open"), JsString("Resolved"), JsString("Deleted"), JsString("Duplicated")))) + ) + //case ("observable", "status") => + // Some(PropertyDescription("status", "enumeration", Seq(JsString("Ok")))) + //case ("observable", "dataType") => + // Some(PropertyDescription("status", "enumeration", Seq(JsString("sometesttype", "fqdn", "url", "regexp", "mail", "hash", "registry", "custom-type", "uri_path", "ip", "user-agent", "autonomous-system", "file", "mail_subject", "filename", "other", "domain")))) + case ("alert", "status") => + Some(Seq(PropertyDescription("status", "enumeration", Seq(JsString("New"), JsString("Updated"), JsString("Ignored"), JsString("Imported"))))) + case ("case_task", "status") => + Some( + Seq( + PropertyDescription("status", "enumeration", Seq(JsString("Waiting"), JsString("InProgress"), JsString("Completed"), JsString("Cancel"))) ) ) - ) - case (_, "tlp") => - Some(Seq(PropertyDescription("tlp", "number", Seq(JsNumber(0), JsNumber(1), JsNumber(2), JsNumber(3)), Seq("white", "green", "amber", "red")))) - case (_, "pap") => - Some(Seq(PropertyDescription("pap", "number", Seq(JsNumber(0), JsNumber(1), JsNumber(2), JsNumber(3)), Seq("white", "green", "amber", "red")))) - case (_, "severity") => - Some( - Seq( - PropertyDescription("severity", "number", Seq(JsNumber(1), JsNumber(2), JsNumber(3), JsNumber(4)), Seq("low", "medium", "high", "critical")) + case ("case", "impactStatus") => + Some(Seq(PropertyDescription("impactStatus", "enumeration", Seq(JsString("NoImpact"), JsString("WithImpact"), JsString("NotApplicable"))))) + case ("case", "resolutionStatus") => + Some( + Seq( + PropertyDescription( + "resolutionStatus", + "enumeration", + Seq(JsString("FalsePositive"), JsString("Duplicated"), JsString("Indeterminate"), JsString("TruePositive"), JsString("Other")) + ) + ) ) - ) - case (_, "createdBy") => Some(Seq(PropertyDescription("createdBy", "user"))) - case (_, "updatedBy") => Some(Seq(PropertyDescription("updatedBy", "user"))) - case (_, "customFields") => Some(customFields) - case ("case_artifact_job" | "action", "status") => - Some( - Seq( - PropertyDescription( - "status", - "enumeration", - Seq(JsString("InProgress"), JsString("Success"), JsString("Failure"), JsString("Waiting"), JsString("Deleted")) + case (_, "tlp") => + Some( + Seq(PropertyDescription("tlp", "number", Seq(JsNumber(0), JsNumber(1), JsNumber(2), JsNumber(3)), Seq("white", "green", "amber", "red"))) + ) + case (_, "pap") => + Some( + Seq(PropertyDescription("pap", "number", Seq(JsNumber(0), JsNumber(1), JsNumber(2), JsNumber(3)), Seq("white", "green", "amber", "red"))) + ) + case (_, "severity") => + Some( + Seq( + PropertyDescription( + "severity", + "number", + Seq(JsNumber(1), JsNumber(2), JsNumber(3), JsNumber(4)), + Seq("low", "medium", "high", "critical") + ) ) ) - ) - case _ => None - } + case (_, "createdBy") => Some(Seq(PropertyDescription("createdBy", "user"))) + case (_, "updatedBy") => Some(Seq(PropertyDescription("updatedBy", "user"))) + case (_, "customFields") => Some(customFields) + case ("case_artifact_job" | "action", "status") => + Some( + Seq( + PropertyDescription( + "status", + "enumeration", + Seq(JsString("InProgress"), JsString("Success"), JsString("Failure"), JsString("Waiting"), JsString("Deleted")) + ) + ) + ) + case _ => None + } def propertyToJson(model: String, prop: PublicProperty[_, _]): Seq[PropertyDescription] = customDescription(model, prop.propertyName).getOrElse { diff --git a/thehive/app/org/thp/thehive/controllers/v0/LogCtrl.scala b/thehive/app/org/thp/thehive/controllers/v0/LogCtrl.scala index 9add39b69e..460eba6ec5 100644 --- a/thehive/app/org/thp/thehive/controllers/v0/LogCtrl.scala +++ b/thehive/app/org/thp/thehive/controllers/v0/LogCtrl.scala @@ -2,8 +2,8 @@ package org.thp.thehive.controllers.v0 import javax.inject.{Inject, Named, Singleton} import org.thp.scalligraph.controllers.{Entrypoint, FieldsParser} -import org.thp.scalligraph.models.Database -import org.thp.scalligraph.query.{ParamQuery, PropertyUpdater, PublicProperty, Query} +import org.thp.scalligraph.models.{Database, IdMapping, UMapping} +import org.thp.scalligraph.query._ import org.thp.scalligraph.traversal.TraversalOps._ import org.thp.scalligraph.traversal.{IteratorOutput, Traversal} import org.thp.thehive.controllers.v0.Conversion._ @@ -14,35 +14,17 @@ import org.thp.thehive.services.OrganisationOps._ import org.thp.thehive.services.ShareOps._ import org.thp.thehive.services.TaskOps._ import org.thp.thehive.services.{LogSrv, OrganisationSrv, TaskSrv} -import play.api.Logger import play.api.mvc.{Action, AnyContent, Results} @Singleton class LogCtrl @Inject() ( - entrypoint: Entrypoint, - @Named("with-thehive-schema") db: Database, - properties: Properties, + override val entrypoint: Entrypoint, + @Named("with-thehive-schema") override val db: Database, logSrv: LogSrv, taskSrv: TaskSrv, - organisationSrv: OrganisationSrv -) extends QueryableCtrl { - - lazy val logger: Logger = Logger(getClass) - override val entityName: String = "log" - override val publicProperties: List[PublicProperty[_, _]] = properties.log - override val initialQuery: Query = - Query.init[Traversal.V[Log]]("listLog", (graph, authContext) => organisationSrv.get(authContext.organisation)(graph).shares.tasks.logs) - override val getQuery: ParamQuery[IdOrName] = Query.initWithParam[IdOrName, Traversal.V[Log]]( - "getLog", - FieldsParser[IdOrName], - (param, graph, authContext) => logSrv.get(param.idOrName)(graph).visible(authContext) - ) - override val pageQuery: ParamQuery[OutputParam] = Query.withParam[OutputParam, Traversal.V[Log], IteratorOutput]( - "page", - FieldsParser[OutputParam], - (range, logSteps, _) => logSteps.richPage(range.from, range.to, withTotal = true)(_.richLog) - ) - override val outputQuery: Query = Query.output[RichLog, Traversal.V[Log]](_.richLog) + override val queryExecutor: QueryExecutor, + override val publicData: PublicLog +) extends QueryCtrl { def create(taskId: String): Action[AnyContent] = entrypoint("create log") @@ -50,10 +32,11 @@ class LogCtrl @Inject() ( .authTransaction(db) { implicit request => implicit graph => val inputLog: InputLog = request.body("log") for { - task <- taskSrv - .getByIds(taskId) - .can(Permissions.manageTask) - .getOrFail("Task") + task <- + taskSrv + .getByIds(taskId) + .can(Permissions.manageTask) + .getOrFail("Task") createdLog <- logSrv.create(inputLog.toLog, task) attachment <- inputLog.attachment.map(logSrv.addAttachment(createdLog, _)).flip richLog = RichLog(createdLog, attachment.toList) @@ -62,7 +45,7 @@ class LogCtrl @Inject() ( def update(logId: String): Action[AnyContent] = entrypoint("update log") - .extract("log", FieldsParser.update("log", properties.log)) + .extract("log", FieldsParser.update("log", publicData.publicProperties)) .authTransaction(db) { implicit request => implicit graph => val propertyUpdaters: Seq[PropertyUpdater] = request.body("log") logSrv @@ -83,3 +66,29 @@ class LogCtrl @Inject() ( } yield Results.NoContent } } + +@Singleton +class PublicLog @Inject() (logSrv: LogSrv, organisationSrv: OrganisationSrv) extends PublicData { + override val entityName: String = "log" + override val initialQuery: Query = + Query.init[Traversal.V[Log]]("listLog", (graph, authContext) => organisationSrv.get(authContext.organisation)(graph).shares.tasks.logs) + override val getQuery: ParamQuery[IdOrName] = Query.initWithParam[IdOrName, Traversal.V[Log]]( + "getLog", + FieldsParser[IdOrName], + (param, graph, authContext) => logSrv.get(param.idOrName)(graph).visible(authContext) + ) + override val pageQuery: ParamQuery[OutputParam] = Query.withParam[OutputParam, Traversal.V[Log], IteratorOutput]( + "page", + FieldsParser[OutputParam], + (range, logSteps, _) => logSteps.richPage(range.from, range.to, withTotal = true)(_.richLog) + ) + override val outputQuery: Query = Query.output[RichLog, Traversal.V[Log]](_.richLog) + override val publicProperties: PublicProperties = + PublicPropertyListBuilder[Log] + .property("message", UMapping.string)(_.field.updatable) + .property("deleted", UMapping.boolean)(_.field.updatable) + .property("startDate", UMapping.date)(_.rename("date").readonly) + .property("status", UMapping.string)(_.select(_.constant("Ok")).readonly) + .property("attachment", IdMapping)(_.select(_.attachments._id).readonly) + .build +} diff --git a/thehive/app/org/thp/thehive/controllers/v0/ObservableCtrl.scala b/thehive/app/org/thp/thehive/controllers/v0/ObservableCtrl.scala index cf8dc42997..9aab18652b 100644 --- a/thehive/app/org/thp/thehive/controllers/v0/ObservableCtrl.scala +++ b/thehive/app/org/thp/thehive/controllers/v0/ObservableCtrl.scala @@ -3,8 +3,8 @@ package org.thp.thehive.controllers.v0 import javax.inject.{Inject, Named, Singleton} import org.thp.scalligraph._ import org.thp.scalligraph.controllers._ -import org.thp.scalligraph.models.Database -import org.thp.scalligraph.query.{ParamQuery, PropertyUpdater, PublicProperty, Query} +import org.thp.scalligraph.models.{Database, UMapping} +import org.thp.scalligraph.query._ import org.thp.scalligraph.traversal.TraversalOps._ import org.thp.scalligraph.traversal.{IteratorOutput, Traversal} import org.thp.thehive.controllers.v0.Conversion._ @@ -14,82 +14,45 @@ import org.thp.thehive.services.CaseOps._ import org.thp.thehive.services.ObservableOps._ import org.thp.thehive.services.OrganisationOps._ import org.thp.thehive.services.ShareOps._ +import org.thp.thehive.services.TagOps._ import org.thp.thehive.services._ -import play.api.Logger -import play.api.libs.json.JsObject +import play.api.libs.json.{JsObject, Json} import play.api.mvc.{Action, AnyContent, Results} import scala.util.Success @Singleton class ObservableCtrl @Inject() ( - entrypoint: Entrypoint, - @Named("with-thehive-schema") db: Database, - properties: Properties, + override val entrypoint: Entrypoint, + @Named("with-thehive-schema") override val db: Database, observableSrv: ObservableSrv, observableTypeSrv: ObservableTypeSrv, caseSrv: CaseSrv, - organisationSrv: OrganisationSrv -) extends QueryableCtrl - with ObservableRenderer { - - lazy val logger: Logger = Logger(getClass) - override val entityName: String = "observable" - override val publicProperties: List[PublicProperty[_, _]] = properties.observable - override val initialQuery: Query = - Query.init[Traversal.V[Observable]]( - "listObservable", - (graph, authContext) => organisationSrv.get(authContext.organisation)(graph).shares.observables - ) - override val getQuery: ParamQuery[IdOrName] = Query.initWithParam[IdOrName, Traversal.V[Observable]]( - "getObservable", - FieldsParser[IdOrName], - (param, graph, authContext) => observableSrv.get(param.idOrName)(graph).visible(authContext) - ) - override val pageQuery: ParamQuery[OutputParam] = - Query.withParam[OutputParam, Traversal.V[Observable], IteratorOutput]( - "page", - FieldsParser[OutputParam], { - case (OutputParam(from, to, withStats, 0), observableSteps, authContext) => - observableSteps - .richPage(from, to, withTotal = true) { - case o if withStats => - o.richObservableWithCustomRenderer(observableStatsRenderer(authContext))(authContext) - .domainMap(ros => (ros._1, ros._2, None: Option[RichCase])) - case o => - o.richObservable.domainMap(ro => (ro, JsObject.empty, None)) - } - case (OutputParam(from, to, _, _), observableSteps, authContext) => - observableSteps.richPage(from, to, withTotal = true)( - _.richObservableWithCustomRenderer(o => o.`case`.richCase(authContext))(authContext).domainMap(roc => - (roc._1, JsObject.empty, Some(roc._2): Option[RichCase]) - ) - ) - } - ) - override val outputQuery: Query = Query.output[RichObservable, Traversal.V[Observable]](_.richObservable) - override val extraQueries: Seq[ParamQuery[_]] = Seq( -// Query.output[(RichObservable, JsObject, Option[RichCase])] - ) - + override val queryExecutor: QueryExecutor, + override val publicData: PublicObservable +) extends ObservableRenderer + with QueryCtrl { def create(caseId: String): Action[AnyContent] = entrypoint("create artifact") .extract("artifact", FieldsParser[InputObservable]) .authTransaction(db) { implicit request => implicit graph => val inputObservable: InputObservable = request.body("artifact") for { - case0 <- caseSrv - .get(caseId) - .can(Permissions.manageObservable) - .orFail(AuthorizationError("Operation not permitted")) + case0 <- + caseSrv + .get(caseId) + .can(Permissions.manageObservable) + .orFail(AuthorizationError("Operation not permitted")) observableType <- observableTypeSrv.getOrFail(inputObservable.dataType) - observablesWithData <- inputObservable - .data - .toTry(d => observableSrv.create(inputObservable.toObservable, observableType, d, inputObservable.tags, Nil)) - observableWithAttachment <- inputObservable - .attachment - .map(a => observableSrv.create(inputObservable.toObservable, observableType, a, inputObservable.tags, Nil)) - .flip + observablesWithData <- + inputObservable + .data + .toTry(d => observableSrv.create(inputObservable.toObservable, observableType, d, inputObservable.tags, Nil)) + observableWithAttachment <- + inputObservable + .attachment + .map(a => observableSrv.create(inputObservable.toObservable, observableType, a, inputObservable.tags, Nil)) + .flip createdObservables <- (observablesWithData ++ observableWithAttachment).toTry { richObservables => caseSrv .addObservable(case0, richObservables) @@ -113,7 +76,7 @@ class ObservableCtrl @Inject() ( def update(observableId: String): Action[AnyContent] = entrypoint("update observable") - .extract("observable", FieldsParser.update("observable", publicProperties)) + .extract("observable", FieldsParser.update("observable", publicData.publicProperties)) .authTransaction(db) { implicit request => implicit graph => val propertyUpdaters: Seq[PropertyUpdater] = request.body("observable") observableSrv @@ -140,7 +103,7 @@ class ObservableCtrl @Inject() ( def bulkUpdate: Action[AnyContent] = entrypoint("bulk update") - .extract("input", FieldsParser.update("observable", publicProperties)) + .extract("input", FieldsParser.update("observable", publicData.publicProperties)) .extract("ids", FieldsParser.seq[String].on("ids")) .authTransaction(db) { implicit request => implicit graph => val properties: Seq[PropertyUpdater] = request.body("input") @@ -157,11 +120,81 @@ class ObservableCtrl @Inject() ( entrypoint("delete") .authTransaction(db) { implicit request => implicit graph => for { - observable <- observableSrv - .getByIds(obsId) - .can(Permissions.manageObservable) - .getOrFail("Observable") + observable <- + observableSrv + .getByIds(obsId) + .can(Permissions.manageObservable) + .getOrFail("Observable") _ <- observableSrv.remove(observable) } yield Results.NoContent } } + +@Singleton +class PublicObservable @Inject() ( + observableSrv: ObservableSrv, + organisationSrv: OrganisationSrv +) extends PublicData + with ObservableRenderer { + override val entityName: String = "observable" + override val initialQuery: Query = + Query.init[Traversal.V[Observable]]( + "listObservable", + (graph, authContext) => organisationSrv.get(authContext.organisation)(graph).shares.observables + ) + override val getQuery: ParamQuery[IdOrName] = Query.initWithParam[IdOrName, Traversal.V[Observable]]( + "getObservable", + FieldsParser[IdOrName], + (param, graph, authContext) => observableSrv.get(param.idOrName)(graph).visible(authContext) + ) + override val pageQuery: ParamQuery[OutputParam] = + Query.withParam[OutputParam, Traversal.V[Observable], IteratorOutput]( + "page", + FieldsParser[OutputParam], + { + case (OutputParam(from, to, withStats, 0), observableSteps, authContext) => + observableSteps + .richPage(from, to, withTotal = true) { + case o if withStats => + o.richObservableWithCustomRenderer(observableStatsRenderer(authContext))(authContext) + .domainMap(ros => (ros._1, ros._2, None: Option[RichCase])) + case o => + o.richObservable.domainMap(ro => (ro, JsObject.empty, None)) + } + case (OutputParam(from, to, _, _), observableSteps, authContext) => + observableSteps.richPage(from, to, withTotal = true)( + _.richObservableWithCustomRenderer(o => o.`case`.richCase(authContext))(authContext).domainMap(roc => + (roc._1, JsObject.empty, Some(roc._2): Option[RichCase]) + ) + ) + } + ) + override val outputQuery: Query = Query.output[RichObservable, Traversal.V[Observable]](_.richObservable) + override val extraQueries: Seq[ParamQuery[_]] = Seq( + // Query.output[(RichObservable, JsObject, Option[RichCase])] + ) + override val publicProperties: PublicProperties = PublicPropertyListBuilder[Observable] + .property("status", UMapping.string)(_.select(_.constant("Ok")).readonly) + .property("startDate", UMapping.date)(_.select(_._createdAt).readonly) + .property("ioc", UMapping.boolean)(_.field.updatable) + .property("sighted", UMapping.boolean)(_.field.updatable) + .property("tags", UMapping.string.set)( + _.select(_.tags.displayName) + .custom { (_, value, vertex, _, graph, authContext) => + observableSrv + .getByIds(vertex.id.toString)(graph) + .getOrFail("Observable") + .flatMap(observable => observableSrv.updateTagNames(observable, value)(graph, authContext)) + .map(_ => Json.obj("tags" -> value)) + } + ) + .property("message", UMapping.string)(_.field.updatable) + .property("tlp", UMapping.int)(_.field.updatable) + .property("dataType", UMapping.string)(_.select(_.observableType.value(_.name)).readonly) + .property("data", UMapping.string.optional)(_.select(_.data.value(_.data)).readonly) + .property("attachment.name", UMapping.string.optional)(_.select(_.attachments.value(_.name)).readonly) + .property("attachment.size", UMapping.long.optional)(_.select(_.attachments.value(_.size)).readonly) + .property("attachment.contentType", UMapping.string.optional)(_.select(_.attachments.value(_.contentType)).readonly) + .property("attachment.hashes", UMapping.hash)(_.select(_.attachments.value(_.hashes)).readonly) + .build +} diff --git a/thehive/app/org/thp/thehive/controllers/v0/ObservableTypeCtrl.scala b/thehive/app/org/thp/thehive/controllers/v0/ObservableTypeCtrl.scala index 6e634fb989..a6408e0000 100644 --- a/thehive/app/org/thp/thehive/controllers/v0/ObservableTypeCtrl.scala +++ b/thehive/app/org/thp/thehive/controllers/v0/ObservableTypeCtrl.scala @@ -2,8 +2,8 @@ package org.thp.thehive.controllers.v0 import javax.inject.{Inject, Named, Singleton} import org.thp.scalligraph.controllers.{Entrypoint, FieldsParser} -import org.thp.scalligraph.models.{Database, Entity} -import org.thp.scalligraph.query.{ParamQuery, PublicProperty, Query} +import org.thp.scalligraph.models.{Database, Entity, UMapping} +import org.thp.scalligraph.query._ import org.thp.scalligraph.traversal.TraversalOps._ import org.thp.scalligraph.traversal.{IteratorOutput, Traversal} import org.thp.thehive.controllers.v0.Conversion._ @@ -14,35 +14,19 @@ import play.api.mvc.{Action, AnyContent, Results} @Singleton class ObservableTypeCtrl @Inject() ( - entrypoint: Entrypoint, - @Named("with-thehive-schema") db: Database, - properties: Properties, - observableTypeSrv: ObservableTypeSrv -) extends QueryableCtrl { - - override val entityName: String = "ObjservableType" - override val publicProperties: List[PublicProperty[_, _]] = properties.observableType - override val initialQuery: Query = - Query.init[Traversal.V[ObservableType]]("listObservableType", (graph, _) => observableTypeSrv.startTraversal(graph)) - override val pageQuery: ParamQuery[OutputParam] = - Query.withParam[OutputParam, Traversal.V[ObservableType], IteratorOutput]( - "page", - FieldsParser[OutputParam], - (range, observableTypeSteps, _) => observableTypeSteps.richPage(range.from, range.to, withTotal = true)(identity) - ) - override val outputQuery: Query = Query.output[ObservableType with Entity] - override val getQuery: ParamQuery[IdOrName] = Query.initWithParam[IdOrName, Traversal.V[ObservableType]]( - "getObservableType", - FieldsParser[IdOrName], - (param, graph, _) => observableTypeSrv.get(param.idOrName)(graph) - ) - - def get(idOrName: String): Action[AnyContent] = entrypoint("get observable type").authRoTransaction(db) { _ => implicit graph => - observableTypeSrv - .get(idOrName) - .getOrFail("Observable") - .map(ot => Results.Ok(ot.toJson)) - } + override val entrypoint: Entrypoint, + @Named("with-thehive-schema") override val db: Database, + observableTypeSrv: ObservableTypeSrv, + override val queryExecutor: QueryExecutor, + override val publicData: PublicObservableType +) extends QueryCtrl { + def get(idOrName: String): Action[AnyContent] = + entrypoint("get observable type").authRoTransaction(db) { _ => implicit graph => + observableTypeSrv + .get(idOrName) + .getOrFail("Observable") + .map(ot => Results.Ok(ot.toJson)) + } def create: Action[AnyContent] = entrypoint("create observable type") @@ -60,3 +44,26 @@ class ObservableTypeCtrl @Inject() ( observableTypeSrv.remove(idOrName).map(_ => Results.NoContent) } } + +@Singleton +class PublicObservableType @Inject() (observableTypeSrv: ObservableTypeSrv) extends PublicData { + override val entityName: String = "ObservableType" + override val initialQuery: Query = + Query.init[Traversal.V[ObservableType]]("listObservableType", (graph, _) => observableTypeSrv.startTraversal(graph)) + override val pageQuery: ParamQuery[OutputParam] = + Query.withParam[OutputParam, Traversal.V[ObservableType], IteratorOutput]( + "page", + FieldsParser[OutputParam], + (range, observableTypeSteps, _) => observableTypeSteps.richPage(range.from, range.to, withTotal = true)(identity) + ) + override val outputQuery: Query = Query.output[ObservableType with Entity] + override val getQuery: ParamQuery[IdOrName] = Query.initWithParam[IdOrName, Traversal.V[ObservableType]]( + "getObservableType", + FieldsParser[IdOrName], + (param, graph, _) => observableTypeSrv.get(param.idOrName)(graph) + ) + override val publicProperties: PublicProperties = PublicPropertyListBuilder[ObservableType] + .property("name", UMapping.string)(_.field.readonly) + .property("isAttachment", UMapping.boolean)(_.field.readonly) + .build +} diff --git a/thehive/app/org/thp/thehive/controllers/v0/OrganisationCtrl.scala b/thehive/app/org/thp/thehive/controllers/v0/OrganisationCtrl.scala index 1a9d97c5e6..25d250717c 100644 --- a/thehive/app/org/thp/thehive/controllers/v0/OrganisationCtrl.scala +++ b/thehive/app/org/thp/thehive/controllers/v0/OrganisationCtrl.scala @@ -3,8 +3,8 @@ package org.thp.thehive.controllers.v0 import javax.inject.{Inject, Named, Singleton} import org.thp.scalligraph.NotFoundError import org.thp.scalligraph.controllers.{Entrypoint, FieldsParser} -import org.thp.scalligraph.models.{Database, Entity} -import org.thp.scalligraph.query.{ParamQuery, PropertyUpdater, PublicProperty, Query} +import org.thp.scalligraph.models.{Database, Entity, UMapping} +import org.thp.scalligraph.query._ import org.thp.scalligraph.traversal.TraversalOps._ import org.thp.scalligraph.traversal.{IteratorOutput, Traversal} import org.thp.thehive.controllers.v0.Conversion._ @@ -19,34 +19,13 @@ import scala.util.{Failure, Success} @Singleton class OrganisationCtrl @Inject() ( - entrypoint: Entrypoint, - properties: Properties, + override val entrypoint: Entrypoint, organisationSrv: OrganisationSrv, userSrv: UserSrv, - @Named("with-thehive-schema") implicit val db: Database -) extends QueryableCtrl { - - override val entityName: String = "organisation" - override val publicProperties: List[PublicProperty[_, _]] = properties.organisation - override val initialQuery: Query = - Query.init[Traversal.V[Organisation]]("listOrganisation", (graph, authContext) => organisationSrv.startTraversal(graph).visible(authContext)) - override val pageQuery: ParamQuery[OutputParam] = Query.withParam[OutputParam, Traversal.V[Organisation], IteratorOutput]( - "page", - FieldsParser[OutputParam], - (range, organisationSteps, _) => organisationSteps.page(range.from, range.to, withTotal = true) - ) - override val outputQuery: Query = Query.output[Organisation with Entity] - override val getQuery: ParamQuery[IdOrName] = Query.initWithParam[IdOrName, Traversal.V[Organisation]]( - "getOrganisation", - FieldsParser[IdOrName], - (param, graph, authContext) => organisationSrv.get(param.idOrName)(graph).visible(authContext) - ) - override val extraQueries: Seq[ParamQuery[_]] = Seq( - Query[Traversal.V[Organisation], Traversal.V[Organisation]]("visible", (organisationSteps, _) => organisationSteps.visibleOrganisationsFrom), - Query[Traversal.V[Organisation], Traversal.V[User]]("users", (organisationSteps, _) => organisationSteps.users), - Query[Traversal.V[Organisation], Traversal.V[CaseTemplate]]("caseTemplates", (organisationSteps, _) => organisationSteps.caseTemplates) - ) - + @Named("with-thehive-schema") implicit override val db: Database, + override val queryExecutor: QueryExecutor, + override val publicData: PublicOrganisation +) extends QueryCtrl { def create: Action[AnyContent] = entrypoint("create organisation") .extract("organisation", FieldsParser[InputOrganisation]) @@ -84,7 +63,7 @@ class OrganisationCtrl @Inject() ( def update(organisationId: String): Action[AnyContent] = entrypoint("update organisation") - .extract("organisation", FieldsParser.update("organisation", properties.organisation)) + .extract("organisation", FieldsParser.update("organisation", publicData.publicProperties)) .authPermittedTransaction(db, Permissions.manageOrganisation) { implicit request => implicit graph => val propertyUpdaters: Seq[PropertyUpdater] = request.body("organisation") @@ -122,8 +101,9 @@ class OrganisationCtrl @Inject() ( for { fromOrg <- organisationSrv.getOrFail(fromOrganisationId) toOrg <- organisationSrv.getOrFail(toOrganisationId) - _ <- if (organisationSrv.linkExists(fromOrg, toOrg)) Success(organisationSrv.doubleUnlink(fromOrg, toOrg)) - else Failure(NotFoundError(s"Organisation $fromOrganisationId is not linked to $toOrganisationId")) + _ <- + if (organisationSrv.linkExists(fromOrg, toOrg)) Success(organisationSrv.doubleUnlink(fromOrg, toOrg)) + else Failure(NotFoundError(s"Organisation $fromOrganisationId is not linked to $toOrganisationId")) } yield Results.NoContent } @@ -144,3 +124,31 @@ class OrganisationCtrl @Inject() ( Success(Results.Ok(organisations.toJson)) } } + +@Singleton +class PublicOrganisation @Inject() (organisationSrv: OrganisationSrv) extends PublicData { + override val entityName: String = "organisation" + + override val initialQuery: Query = + Query.init[Traversal.V[Organisation]]("listOrganisation", (graph, authContext) => organisationSrv.startTraversal(graph).visible(authContext)) + override val pageQuery: ParamQuery[OutputParam] = Query.withParam[OutputParam, Traversal.V[Organisation], IteratorOutput]( + "page", + FieldsParser[OutputParam], + (range, organisationSteps, _) => organisationSteps.page(range.from, range.to, withTotal = true) + ) + override val outputQuery: Query = Query.output[Organisation with Entity] + override val getQuery: ParamQuery[IdOrName] = Query.initWithParam[IdOrName, Traversal.V[Organisation]]( + "getOrganisation", + FieldsParser[IdOrName], + (param, graph, authContext) => organisationSrv.get(param.idOrName)(graph).visible(authContext) + ) + override val extraQueries: Seq[ParamQuery[_]] = Seq( + Query[Traversal.V[Organisation], Traversal.V[Organisation]]("visible", (organisationSteps, _) => organisationSteps.visibleOrganisationsFrom), + Query[Traversal.V[Organisation], Traversal.V[User]]("users", (organisationSteps, _) => organisationSteps.users), + Query[Traversal.V[Organisation], Traversal.V[CaseTemplate]]("caseTemplates", (organisationSteps, _) => organisationSteps.caseTemplates) + ) + override val publicProperties: PublicProperties = PublicPropertyListBuilder[Organisation] + .property("name", UMapping.string)(_.field.updatable) + .property("description", UMapping.string)(_.field.updatable) + .build +} diff --git a/thehive/app/org/thp/thehive/controllers/v0/PageCtrl.scala b/thehive/app/org/thp/thehive/controllers/v0/PageCtrl.scala index 3b793214dd..089addc30a 100644 --- a/thehive/app/org/thp/thehive/controllers/v0/PageCtrl.scala +++ b/thehive/app/org/thp/thehive/controllers/v0/PageCtrl.scala @@ -2,8 +2,8 @@ package org.thp.thehive.controllers.v0 import javax.inject.{Inject, Named, Singleton} import org.thp.scalligraph.controllers.{Entrypoint, FieldsParser} -import org.thp.scalligraph.models.{Database, Entity} -import org.thp.scalligraph.query.{ParamQuery, PropertyUpdater, PublicProperty, Query} +import org.thp.scalligraph.models.{Database, Entity, UMapping} +import org.thp.scalligraph.query._ import org.thp.scalligraph.traversal.TraversalOps._ import org.thp.scalligraph.traversal.{IteratorOutput, Traversal} import org.thp.thehive.controllers.v0.Conversion._ @@ -16,29 +16,12 @@ import play.api.mvc._ @Singleton class PageCtrl @Inject() ( - entrypoint: Entrypoint, + override val entrypoint: Entrypoint, pageSrv: PageSrv, - @Named("with-thehive-schema") db: Database, - properties: Properties, - organisationSrv: OrganisationSrv -) extends QueryableCtrl { - - override val entityName: String = "page" - override val publicProperties: List[PublicProperty[_, _]] = properties.page - override val initialQuery: Query = - Query.init[Traversal.V[Page]]("listPage", (graph, authContext) => organisationSrv.get(authContext.organisation)(graph).pages) - override val getQuery: ParamQuery[IdOrName] = Query.initWithParam[IdOrName, Traversal.V[Page]]( - "getPage", - FieldsParser[IdOrName], - (param, graph, authContext) => pageSrv.get(param.idOrName)(graph).visible(authContext) - ) - val pageQuery: ParamQuery[OutputParam] = Query.withParam[OutputParam, Traversal.V[Page], IteratorOutput]( - "page", - FieldsParser[OutputParam], - (range, pageSteps, _) => pageSteps.page(range.from, range.to, withTotal = true) - ) - override val outputQuery: Query = Query.output[Page with Entity] - + @Named("with-thehive-schema") override val db: Database, + override val queryExecutor: QueryExecutor, + override val publicData: PublicPage +) extends QueryCtrl { def get(idOrTitle: String): Action[AnyContent] = entrypoint("get a page") .authRoTransaction(db) { implicit request => implicit graph => @@ -62,7 +45,7 @@ class PageCtrl @Inject() ( def update(idOrTitle: String): Action[AnyContent] = entrypoint("update a page") - .extract("page", FieldsParser.update("page", properties.page)) + .extract("page", FieldsParser.update("page", publicData.publicProperties)) .authPermittedTransaction(db, Permissions.managePage) { implicit request => implicit graph => val propertyUpdaters: Seq[PropertyUpdater] = request.body("page") @@ -81,3 +64,26 @@ class PageCtrl @Inject() ( } yield Results.NoContent } } + +@Singleton +class PublicPage @Inject() (pageSrv: PageSrv, organisationSrv: OrganisationSrv) extends PublicData { + override val entityName: String = "page" + override val initialQuery: Query = + Query.init[Traversal.V[Page]]("listPage", (graph, authContext) => organisationSrv.get(authContext.organisation)(graph).pages) + override val getQuery: ParamQuery[IdOrName] = Query.initWithParam[IdOrName, Traversal.V[Page]]( + "getPage", + FieldsParser[IdOrName], + (param, graph, authContext) => pageSrv.get(param.idOrName)(graph).visible(authContext) + ) + val pageQuery: ParamQuery[OutputParam] = Query.withParam[OutputParam, Traversal.V[Page], IteratorOutput]( + "page", + FieldsParser[OutputParam], + (range, pageSteps, _) => pageSteps.page(range.from, range.to, withTotal = true) + ) + override val outputQuery: Query = Query.output[Page with Entity] + override val publicProperties: PublicProperties = PublicPropertyListBuilder[Page] + .property("title", UMapping.string)(_.field.updatable) + .property("content", UMapping.string.set)(_.field.updatable) + .build + +} diff --git a/thehive/app/org/thp/thehive/controllers/v0/ProfileCtrl.scala b/thehive/app/org/thp/thehive/controllers/v0/ProfileCtrl.scala index fb61f38e0e..b5fe127c70 100644 --- a/thehive/app/org/thp/thehive/controllers/v0/ProfileCtrl.scala +++ b/thehive/app/org/thp/thehive/controllers/v0/ProfileCtrl.scala @@ -3,8 +3,8 @@ package org.thp.thehive.controllers.v0 import javax.inject.{Inject, Named, Singleton} import org.thp.scalligraph.AuthorizationError import org.thp.scalligraph.controllers.{Entrypoint, FieldsParser} -import org.thp.scalligraph.models.{Database, Entity} -import org.thp.scalligraph.query.{ParamQuery, PropertyUpdater, PublicProperty, Query} +import org.thp.scalligraph.models.{Database, Entity, UMapping} +import org.thp.scalligraph.query._ import org.thp.scalligraph.traversal.TraversalOps._ import org.thp.scalligraph.traversal.{IteratorOutput, Traversal} import org.thp.thehive.controllers.v0.Conversion._ @@ -18,38 +18,20 @@ import scala.util.Failure @Singleton class ProfileCtrl @Inject() ( - entrypoint: Entrypoint, - properties: Properties, + override val entrypoint: Entrypoint, profileSrv: ProfileSrv, - @Named("with-thehive-schema") implicit val db: Database -) extends QueryableCtrl { - - override val getQuery: ParamQuery[IdOrName] = Query.initWithParam[IdOrName, Traversal.V[Profile]]( - "getProfile", - FieldsParser[IdOrName], - (param, graph, _) => profileSrv.get(param.idOrName)(graph) - ) - val entityName: String = "profile" - val publicProperties: List[PublicProperty[_, _]] = properties.profile - - val initialQuery: Query = - Query.init[Traversal.V[Profile]]("listProfile", (graph, _) => profileSrv.startTraversal(graph)) - - val pageQuery: ParamQuery[OutputParam] = Query.withParam[OutputParam, Traversal.V[Profile], IteratorOutput]( - "page", - FieldsParser[OutputParam], - (range, profileSteps, _) => profileSteps.page(range.from, range.to, withTotal = true) - ) - override val outputQuery: Query = Query.output[Profile with Entity] - + override val publicData: PublicProfile, + @Named("with-thehive-schema") implicit val db: Database, + override val queryExecutor: QueryExecutor +) extends QueryCtrl { def create: Action[AnyContent] = entrypoint("create profile") .extract("profile", FieldsParser[InputProfile]) .authTransaction(db) { implicit request => implicit graph => val profile: InputProfile = request.body("profile") - if (request.isPermitted(Permissions.manageProfile)) { + if (request.isPermitted(Permissions.manageProfile)) profileSrv.create(profile.toProfile).map(createdProfile => Results.Created(createdProfile.toJson)) - } else + else Failure(AuthorizationError("You don't have permission to create profiles")) } @@ -65,15 +47,15 @@ class ProfileCtrl @Inject() ( def update(profileId: String): Action[AnyContent] = entrypoint("update profile") - .extract("profile", FieldsParser.update("profile", properties.profile)) + .extract("profile", FieldsParser.update("profile", publicData.publicProperties)) .authTransaction(db) { implicit request => implicit graph => val propertyUpdaters: Seq[PropertyUpdater] = request.body("profile") - if (request.isPermitted(Permissions.manageProfile)) { + if (request.isPermitted(Permissions.manageProfile)) profileSrv .update(_.get(profileId), propertyUpdaters) .flatMap { case (profileSteps, _) => profileSteps.getOrFail("Profile") } .map(profile => Results.Ok(profile.toJson)) - } else + else Failure(AuthorizationError("You don't have permission to update profiles")) } @@ -86,3 +68,27 @@ class ProfileCtrl @Inject() ( .map(_ => Results.NoContent) } } + +@Singleton +class PublicProfile @Inject() (profileSrv: ProfileSrv) extends PublicData { + val entityName: String = "profile" + + override val getQuery: ParamQuery[IdOrName] = Query.initWithParam[IdOrName, Traversal.V[Profile]]( + "getProfile", + FieldsParser[IdOrName], + (param, graph, _) => profileSrv.get(param.idOrName)(graph) + ) + val initialQuery: Query = + Query.init[Traversal.V[Profile]]("listProfile", (graph, _) => profileSrv.startTraversal(graph)) + + val pageQuery: ParamQuery[OutputParam] = Query.withParam[OutputParam, Traversal.V[Profile], IteratorOutput]( + "page", + FieldsParser[OutputParam], + (range, profileSteps, _) => profileSteps.page(range.from, range.to, withTotal = true) + ) + override val outputQuery: Query = Query.output[Profile with Entity] + val publicProperties: PublicProperties = PublicPropertyListBuilder[Profile] + .property("name", UMapping.string)(_.field.updatable) + .property("permissions", UMapping.string.set)(_.field.updatable) + .build +} diff --git a/thehive/app/org/thp/thehive/controllers/v0/Properties.scala b/thehive/app/org/thp/thehive/controllers/v0/Properties.scala deleted file mode 100644 index d04c59b454..0000000000 --- a/thehive/app/org/thp/thehive/controllers/v0/Properties.scala +++ /dev/null @@ -1,486 +0,0 @@ -package org.thp.thehive.controllers.v0 - -import java.lang.{Long => JLong} -import java.util.Date - -import javax.inject.{Inject, Singleton} -import org.apache.tinkerpop.gremlin.process.traversal.P -import org.scalactic.Accumulation._ -import org.thp.scalligraph.controllers._ -import org.thp.scalligraph.models.{Database, IdMapping, UMapping} -import org.thp.scalligraph.query.{PublicProperty, PublicPropertyListBuilder} -import org.thp.scalligraph.traversal.Converter -import org.thp.scalligraph.traversal.TraversalOps._ -import org.thp.scalligraph.{AttributeCheckingError, AuthorizationError, BadRequestError, InvalidFormatAttributeError, RichSeq} -import org.thp.thehive.controllers.v0.Conversion._ -import org.thp.thehive.dto.v0.InputTask -import org.thp.thehive.models._ -import org.thp.thehive.services.AlertOps._ -import org.thp.thehive.services.AuditOps._ -import org.thp.thehive.services.CaseOps._ -import org.thp.thehive.services.CaseTemplateOps._ -import org.thp.thehive.services.CustomFieldOps._ -import org.thp.thehive.services.DashboardOps._ -import org.thp.thehive.services.LogOps._ -import org.thp.thehive.services.ObservableOps._ -import org.thp.thehive.services.OrganisationOps._ -import org.thp.thehive.services.TagOps._ -import org.thp.thehive.services.TaskOps._ -import org.thp.thehive.services.UserOps._ -import org.thp.thehive.services._ -import play.api.libs.json.{JsObject, JsValue, Json} - -import scala.util.{Failure, Success, Try} - -@Singleton -class Properties @Inject() ( - caseSrv: CaseSrv, - userSrv: UserSrv, - alertSrv: AlertSrv, - dashboardSrv: DashboardSrv, - observableSrv: ObservableSrv, - caseTemplateSrv: CaseTemplateSrv, - taskSrv: TaskSrv, - customFieldSrv: CustomFieldSrv, - db: Database -) { - - lazy val alert: List[PublicProperty[_, _]] = - PublicPropertyListBuilder[Alert] - .property("type", UMapping.string)(_.field.updatable) - .property("source", UMapping.string)(_.field.updatable) - .property("sourceRef", UMapping.string)(_.field.updatable) - .property("title", UMapping.string)(_.field.updatable) - .property("description", UMapping.string)(_.field.updatable) - .property("severity", UMapping.int)(_.field.updatable) - .property("date", UMapping.date)(_.field.updatable) - .property("lastSyncDate", UMapping.date.optional)(_.field.updatable) - .property("tags", UMapping.string.set)( - _.select(_.tags.displayName) - .custom { (_, value, vertex, _, graph, authContext) => - alertSrv - .get(vertex)(graph) - .getOrFail("Alert") - .flatMap(alert => alertSrv.updateTagNames(alert, value)(graph, authContext)) - .map(_ => Json.obj("tags" -> value)) - } - ) - .property("flag", UMapping.boolean)(_.field.updatable) - .property("tlp", UMapping.int)(_.field.updatable) - .property("pap", UMapping.int)(_.field.updatable) - .property("read", UMapping.boolean)(_.field.updatable) - .property("follow", UMapping.boolean)(_.field.updatable) - .property("status", UMapping.string)( - _.select( - _.project( - _.byValue(_.read) - .by(_.`case`.limit(1).count) - ).domainMap { - case (false, caseCount) if caseCount == 0L => "New" - case (false, _) => "Updated" - case (true, caseCount) if caseCount == 0L => "Ignored" - case (true, _) => "Imported" - } - ).readonly - ) - .property("summary", UMapping.string.optional)(_.field.updatable) - .property("user", UMapping.string)(_.field.updatable) - .property("customFields", UMapping.jsonNative)(_.subSelect { - case (FPathElem(_, FPathElem(name, _)), alertSteps) => - alertSteps.customFields(name).jsonValue - case (_, alertSteps) => alertSteps.customFields.nameJsonValue.fold.domainMap(JsObject(_)) - }.custom { - case (FPathElem(_, FPathElem(name, _)), value, vertex, _, graph, authContext) => - for { - c <- alertSrv.getByIds(vertex.id.toString)(graph).getOrFail("Alert") - _ <- alertSrv.setOrCreateCustomField(c, name, Some(value))(graph, authContext) - } yield Json.obj(s"customField.$name" -> value) - case (FPathElem(_, FPathEmpty), values: JsObject, vertex, _, graph, authContext) => - for { - c <- alertSrv.get(vertex)(graph).getOrFail("Alert") - cfv <- values.fields.toTry { case (n, v) => customFieldSrv.getOrFail(n)(graph).map(_ -> v) } - _ <- alertSrv.updateCustomField(c, cfv)(graph, authContext) - } yield Json.obj("customFields" -> values) - - case _ => Failure(BadRequestError("Invalid custom fields format")) - }) - .property("case", IdMapping)(_.select(_.`case`._id).readonly) - .build - - lazy val audit: List[PublicProperty[_, _]] = - PublicPropertyListBuilder[Audit] - .property("operation", UMapping.string)(_.rename("action").readonly) - .property("details", UMapping.string)(_.field.readonly) - .property("objectType", UMapping.string.optional)(_.field.readonly) - .property("objectId", UMapping.string.optional)(_.field.readonly) - .property("base", UMapping.boolean)(_.rename("mainAction").readonly) - .property("startDate", UMapping.date)(_.rename("_createdAt").readonly) - .property("requestId", UMapping.string)(_.field.readonly) - .property("rootId", IdMapping)(_.select(_.context._id).readonly) - .build - - lazy val `case`: List[PublicProperty[_, _]] = - PublicPropertyListBuilder[Case] - .property("caseId", UMapping.int)(_.rename("number").readonly) - .property("title", UMapping.string)(_.field.updatable) - .property("description", UMapping.string)(_.field.updatable) - .property("severity", UMapping.int)(_.field.updatable) - .property("startDate", UMapping.date)(_.field.updatable) - .property("endDate", UMapping.date.optional)(_.field.updatable) - .property("tags", UMapping.string.set)( - _.select(_.tags.displayName) - .custom { (_, value, vertex, _, graph, authContext) => - caseSrv - .get(vertex)(graph) - .getOrFail("Case") - .flatMap(`case` => caseSrv.updateTagNames(`case`, value)(graph, authContext)) - .map(_ => Json.obj("tags" -> value)) - } - ) - .property("flag", UMapping.boolean)(_.field.updatable) - .property("tlp", UMapping.int)(_.field.updatable) - .property("pap", UMapping.int)(_.field.updatable) - .property("status", UMapping.enum[CaseStatus.type])(_.field.updatable) - .property("summary", UMapping.string.optional)(_.field.updatable) - .property("owner", UMapping.string.optional)(_.select(_.user.value(_.login)).custom { (_, login, vertex, _, graph, authContext) => - for { - c <- caseSrv.get(vertex)(graph).getOrFail("Case") - user <- login.map(userSrv.get(_)(graph).getOrFail("User")).flip - _ <- user match { - case Some(u) => caseSrv.assign(c, u)(graph, authContext) - case None => caseSrv.unassign(c)(graph, authContext) - } - } yield Json.obj("owner" -> user.map(_.login)) - }) - .property("resolutionStatus", UMapping.string.optional)(_.select(_.resolutionStatus.value(_.value)).custom { - (_, resolutionStatus, vertex, _, graph, authContext) => - for { - c <- caseSrv.get(vertex)(graph).getOrFail("Case") - _ <- resolutionStatus match { - case Some(s) => caseSrv.setResolutionStatus(c, s)(graph, authContext) - case None => caseSrv.unsetResolutionStatus(c)(graph, authContext) - } - } yield Json.obj("resolutionStatus" -> resolutionStatus) - }) - .property("impactStatus", UMapping.string.optional)(_.select(_.impactStatus.value(_.value)).custom { - (_, impactStatus, vertex, _, graph, authContext) => - for { - c <- caseSrv.getByIds(vertex.id.toString)(graph).getOrFail("Case") - _ <- impactStatus match { - case Some(s) => caseSrv.setImpactStatus(c, s)(graph, authContext) - case None => caseSrv.unsetImpactStatus(c)(graph, authContext) - } - } yield Json.obj("impactStatus" -> impactStatus) - }) - .property("customFields", UMapping.jsonNative)(_.subSelect { - case (FPathElem(_, FPathElem(name, _)), caseSteps) => - caseSteps - .customFields(name) - .jsonValue - case (_, caseSteps) => caseSteps.customFields.nameJsonValue.fold.domainMap(JsObject(_)) - } - .filter { - case (FPathElem(_, FPathElem(name, _)), caseTraversal) => - db - .roTransaction(implicit graph => customFieldSrv.get(name).value(_.`type`).getOrFail("CustomField")) - .map { - case CustomFieldType.boolean => caseTraversal.customFields(name).value(_.booleanValue) - case CustomFieldType.date => caseTraversal.customFields(name).value(_.dateValue) - case CustomFieldType.float => caseTraversal.customFields(name).value(_.floatValue) - case CustomFieldType.integer => caseTraversal.customFields(name).value(_.integerValue) - case CustomFieldType.string => caseTraversal.customFields(name).value(_.stringValue) - } - .getOrElse(caseTraversal.constant2(null)) - case (_, caseTraversal) => caseTraversal.constant2(null) - } - .converter { - case FPathElem(_, FPathElem(name, _)) => - db - .roTransaction { implicit graph => - customFieldSrv.get(name).value(_.`type`).getOrFail("CustomField") - } - .map { - case CustomFieldType.boolean => new Converter[Any, JsValue] { def apply(x: JsValue): Any = x.as[Boolean] } - case CustomFieldType.date => new Converter[Any, JsValue] { def apply(x: JsValue): Any = x.as[Date] } - case CustomFieldType.float => new Converter[Any, JsValue] { def apply(x: JsValue): Any = x.as[Double] } - 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 }) - case _ => (x: JsValue) => x - } - .custom { - case (FPathElem(_, FPathElem(name, _)), value, vertex, _, graph, authContext) => - for { - c <- caseSrv.getByIds(vertex.id.toString)(graph).getOrFail("Case") - _ <- caseSrv.setOrCreateCustomField(c, name, Some(value), None)(graph, authContext) - } yield Json.obj(s"customField.$name" -> value) - case (FPathElem(_, FPathEmpty), values: JsObject, vertex, _, graph, authContext) => - for { - c <- caseSrv.get(vertex)(graph).getOrFail("Case") - cfv <- values.fields.toTry { case (n, v) => customFieldSrv.getOrFail(n)(graph).map(cf => (cf, v, None)) } - _ <- caseSrv.updateCustomField(c, cfv)(graph, authContext) - } yield Json.obj("customFields" -> values) - case _ => Failure(BadRequestError("Invalid custom fields format")) - }) - .property("computed.handlingDurationInHours", UMapping.long)( - _.select( - _.coalesce( - _.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 - ) - .build - - lazy val caseTemplate: List[PublicProperty[_, _]] = - PublicPropertyListBuilder[CaseTemplate] - .property("name", UMapping.string)(_.field.updatable) - .property("displayName", UMapping.string)(_.field.updatable) - .property("titlePrefix", UMapping.string.optional)(_.field.updatable) - .property("description", UMapping.string.optional)(_.field.updatable) - .property("severity", UMapping.int.optional)(_.field.updatable) - .property("tags", UMapping.string.set)( - _.select(_.tags.displayName) - .custom { (_, value, vertex, _, graph, authContext) => - caseTemplateSrv - .get(vertex)(graph) - .getOrFail("CaseTemplate") - .flatMap(caseTemplate => caseTemplateSrv.updateTagNames(caseTemplate, value)(graph, authContext)) - .map(_ => Json.obj("tags" -> value)) - } - ) - .property("flag", UMapping.boolean)(_.field.updatable) - .property("tlp", UMapping.int.optional)(_.field.updatable) - .property("pap", UMapping.int.optional)(_.field.updatable) - .property("summary", UMapping.string.optional)(_.field.updatable) - .property("user", UMapping.string)(_.field.updatable) - .property("customFields", UMapping.jsonNative)(_.subSelect { - case (FPathElem(_, FPathElem(name, _)), caseTemplateSteps) => caseTemplateSteps.customFields(name).jsonValue - case (_, caseTemplateSteps) => caseTemplateSteps.customFields.nameJsonValue.fold.domainMap(JsObject(_)) - }.custom { - case (FPathElem(_, FPathElem(name, _)), value, vertex, _, graph, authContext) => - for { - c <- caseTemplateSrv.getByIds(vertex.id.toString)(graph).getOrFail("CaseTemplate") - _ <- caseTemplateSrv.setOrCreateCustomField(c, name, Some(value), None)(graph, authContext) - } yield Json.obj(s"customFields.$name" -> value) - case (FPathElem(_, FPathEmpty), values: JsObject, vertex, _, graph, authContext) => - for { - c <- caseTemplateSrv.get(vertex)(graph).getOrFail("CaseTemplate") - cfv <- values.fields.toTry { case (n, v) => customFieldSrv.getOrFail(n)(graph).map(_ -> v) } - _ <- caseTemplateSrv.updateCustomField(c, cfv)(graph, authContext) - } yield Json.obj("customFields" -> values) - case _ => Failure(BadRequestError("Invalid custom fields format")) - }) - .property("tasks", UMapping.jsonNative.sequence)(_.select(_.tasks.richTask.domainMap(_.toJson)).custom { // FIXME select the correct mapping - (_, value, vertex, _, graph, authContext) => - val fp = FieldsParser[InputTask] - - caseTemplateSrv.get(vertex)(graph).tasks.remove() - for { - caseTemplate <- caseTemplateSrv.getByIds(vertex.id.toString)(graph).getOrFail("CaseTemplate") - tasks <- value.validatedBy(t => fp(Field(t))).badMap(AttributeCheckingError(_)).toTry - createdTasks <- - tasks - .toTry(t => t.owner.map(userSrv.getOrFail(_)(graph)).flip.flatMap(owner => taskSrv.create(t.toTask, owner)(graph, authContext))) - _ <- createdTasks.toTry(t => caseTemplateSrv.addTask(caseTemplate, t.task)(graph, authContext)) - } yield Json.obj("tasks" -> createdTasks.map(_.toJson)) - }) - .build - - lazy val customField: List[PublicProperty[_, _]] = - PublicPropertyListBuilder[CustomField] - .property("name", UMapping.string)(_.rename("displayName").updatable) - .property("description", UMapping.string)(_.field.updatable) - .property("reference", UMapping.string)(_.rename("name").readonly) - .property("mandatory", UMapping.boolean)(_.field.updatable) - .property("type", UMapping.string)(_.field.readonly) - .property("options", UMapping.json.sequence)(_.field.updatable) - .build - - lazy val dashboard: List[PublicProperty[_, _]] = - PublicPropertyListBuilder[Dashboard] - .property("title", UMapping.string)(_.field.updatable) - .property("description", UMapping.string)(_.field.updatable) - .property("definition", UMapping.string)(_.field.updatable) - .property("status", UMapping.string)( - _.select(_.organisation.fold.domainMap(d => if (d.isEmpty) "Private" else "Shared")).custom { // TODO replace by choose step - case (_, "Shared", vertex, _, graph, authContext) => - for { - dashboard <- dashboardSrv.get(vertex)(graph).filter(_.user.current(authContext)).getOrFail("Dashboard") - _ <- dashboardSrv.share(dashboard, authContext.organisation, writable = false)(graph, authContext) - } yield Json.obj("status" -> "Shared") - - case (_, "Private", vertex, _, graph, authContext) => - for { - d <- dashboardSrv.get(vertex)(graph).filter(_.user.current(authContext)).getOrFail("Dashboard") - _ <- dashboardSrv.unshare(d, authContext.organisation)(graph, authContext) - } yield Json.obj("status" -> "Private") - - case (_, "Deleted", vertex, _, graph, authContext) => - for { - d <- dashboardSrv.get(vertex)(graph).filter(_.user.current(authContext)).getOrFail("Dashboard") - _ <- dashboardSrv.remove(d)(graph, authContext) - } yield Json.obj("status" -> "Deleted") - - case (_, status, _, _, _, _) => - Failure(InvalidFormatAttributeError("status", "String", Set("Shared", "Private", "Deleted"), FString(status))) - } - ) - .build - - lazy val log: List[PublicProperty[_, _]] = - PublicPropertyListBuilder[Log] - .property("message", UMapping.string)(_.field.updatable) - .property("deleted", UMapping.boolean)(_.field.updatable) - .property("startDate", UMapping.date)(_.rename("date").readonly) - .property("status", UMapping.string)(_.select(_.constant("Ok")).readonly) - .property("attachment", IdMapping)(_.select(_.attachments._id).readonly) - .build - - lazy val observable: List[PublicProperty[_, _]] = - PublicPropertyListBuilder[Observable] - .property("status", UMapping.string)(_.select(_.constant("Ok")).readonly) - .property("startDate", UMapping.date)(_.select(_._createdAt).readonly) - .property("ioc", UMapping.boolean)(_.field.updatable) - .property("sighted", UMapping.boolean)(_.field.updatable) - .property("tags", UMapping.string.set)( - _.select(_.tags.displayName) - .custom { (_, value, vertex, _, graph, authContext) => - observableSrv - .getByIds(vertex.id.toString)(graph) - .getOrFail("Observable") - .flatMap(observable => observableSrv.updateTagNames(observable, value)(graph, authContext)) - .map(_ => Json.obj("tags" -> value)) - } - ) - .property("message", UMapping.string)(_.field.updatable) - .property("tlp", UMapping.int)(_.field.updatable) - .property("dataType", UMapping.string)(_.select(_.observableType.value(_.name)).readonly) - .property("data", UMapping.string.optional)(_.select(_.data.value(_.data)).readonly) - .property("attachment.name", UMapping.string.optional)(_.select(_.attachments.value(_.name)).readonly) - .property("attachment.size", UMapping.long.optional)(_.select(_.attachments.value(_.size)).readonly) - .property("attachment.contentType", UMapping.string.optional)(_.select(_.attachments.value(_.contentType)).readonly) - .property("attachment.hashes", UMapping.hash)(_.select(_.attachments.value(_.hashes)).readonly) - .build - - lazy val organisation: List[PublicProperty[_, _]] = - PublicPropertyListBuilder[Organisation] - .property("name", UMapping.string)(_.field.updatable) - .property("description", UMapping.string)(_.field.updatable) - .build - - lazy val page: List[PublicProperty[_, _]] = - PublicPropertyListBuilder[Page] - .property("title", UMapping.string)(_.field.updatable) - .property("content", UMapping.string.set)(_.field.updatable) - .build - - lazy val profile: List[PublicProperty[_, _]] = - PublicPropertyListBuilder[Profile] - .property("name", UMapping.string)(_.field.updatable) - .property("permissions", UMapping.string.set)(_.field.updatable) - .build - - lazy val tag: List[PublicProperty[_, _]] = PublicPropertyListBuilder[Tag] - .property("namespace", UMapping.string)(_.field.readonly) - .property("predicate", UMapping.string)(_.field.readonly) - .property("value", UMapping.string.optional)(_.field.readonly) - .property("description", UMapping.string.optional)(_.field.readonly) - .property("text", UMapping.string)(_.select(_.displayName).readonly) - .build - - lazy val task: List[PublicProperty[_, _]] = - PublicPropertyListBuilder[Task] - .property("title", UMapping.string)(_.field.updatable) - .property("description", UMapping.string.optional)(_.field.updatable) - .property("status", UMapping.enum[TaskStatus.type])(_.field.custom { (_, value, vertex, _, graph, authContext) => - for { - task <- taskSrv.get(vertex)(graph).getOrFail("Task") - user <- - userSrv - .current(graph, authContext) - .getOrFail("User") - _ <- taskSrv.updateStatus(task, user, value)(graph, authContext) - } yield Json.obj("status" -> value) - }) - .property("flag", UMapping.boolean)(_.field.updatable) - .property("startDate", UMapping.date.optional)(_.field.updatable) - .property("endDate", UMapping.date.optional)(_.field.updatable) - .property("order", UMapping.int)(_.field.updatable) - .property("dueDate", UMapping.date.optional)(_.field.updatable) - .property("group", UMapping.string)(_.field.updatable) - .property("owner", UMapping.string.optional)( - _.select(_.assignee.value(_.login)) - .custom { (_, login: Option[String], vertex, _, graph, authContext) => - for { - task <- taskSrv.get(vertex)(graph).getOrFail("Task") - user <- login.map(userSrv.getOrFail(_)(graph)).flip - _ <- user match { - case Some(u) => taskSrv.assign(task, u)(graph, authContext) - case None => taskSrv.unassign(task)(graph, authContext) - } - } yield Json.obj("owner" -> user.map(_.login)) - } - ) - .build - - lazy val user: List[PublicProperty[_, _]] = - PublicPropertyListBuilder[User] - .property("login", UMapping.string)(_.field.readonly) - .property("name", UMapping.string)(_.field.custom { (_, value, vertex, db, graph, authContext) => - def isCurrentUser: Try[Unit] = - userSrv - .current(graph, authContext) - .getByIds(vertex.id.toString) - .existsOrFail - - def isUserAdmin: Try[Unit] = - userSrv - .current(graph, authContext) - .organisations(Permissions.manageUser)(db) - .users - .getByIds(vertex.id.toString) - .existsOrFail - - isCurrentUser - .orElse(isUserAdmin) - .map { _ => - UMapping.string.setProperty(vertex, "name", value) - Json.obj("name" -> value) - } - }) - .property("status", UMapping.string)( - _.select(_.choose(predicate = _.value(_.locked).is(P.eq(true)), onTrue = _.constant("Locked"), onFalse = _.constant("Ok"))) - .custom { (_, value, vertex, _, graph, authContext) => - userSrv - .current(graph, authContext) - .organisations(Permissions.manageUser)(db) - .users - .getByIds(vertex.id.toString) - .orFail(AuthorizationError("Operation not permitted")) - .flatMap { - case user if value == "Ok" => - userSrv.unlock(user)(graph, authContext) - Success(Json.obj("status" -> value)) - case user if value == "Locked" => - userSrv.lock(user)(graph, authContext) - Success(Json.obj("status" -> value)) - case _ => Failure(InvalidFormatAttributeError("status", "UserStatus", Set("Ok", "Locked"), FString(value))) - } - } - ) - .build - - lazy val observableType: List[PublicProperty[_, _]] = PublicPropertyListBuilder[ObservableType] - .property("name", UMapping.string)(_.field.readonly) - .property("isAttachment", UMapping.boolean)(_.field.readonly) - .build -} diff --git a/thehive/app/org/thp/thehive/controllers/v0/QueryCtrl.scala b/thehive/app/org/thp/thehive/controllers/v0/QueryCtrl.scala index 7d309f1e73..aac496b522 100644 --- a/thehive/app/org/thp/thehive/controllers/v0/QueryCtrl.scala +++ b/thehive/app/org/thp/thehive/controllers/v0/QueryCtrl.scala @@ -1,6 +1,5 @@ package org.thp.thehive.controllers.v0 -import javax.inject.{Inject, Named, Singleton} import org.apache.tinkerpop.gremlin.process.traversal.Order import org.apache.tinkerpop.gremlin.structure.Graph import org.scalactic.Accumulation._ @@ -19,25 +18,27 @@ import scala.util.Try case class IdOrName(idOrName: String) -trait QueryableCtrl { +trait PublicData { val entityName: String - val publicProperties: List[PublicProperty[_, _]] + val publicProperties: PublicProperties val initialQuery: Query val pageQuery: ParamQuery[OutputParam] val outputQuery: Query val getQuery: ParamQuery[IdOrName] val extraQueries: Seq[ParamQuery[_]] = Nil } - -class QueryCtrl(entrypoint: Entrypoint, @Named("with-thehive-schema") db: Database, ctrl: QueryableCtrl, queryExecutor: QueryExecutor) { +trait QueryCtrl { lazy val logger: Logger = Logger(getClass) - val publicProperties: List[PublicProperty[_, _]] = queryExecutor.publicProperties - val filterQuery: FilterQuery = queryExecutor.filterQuery - val queryType: ru.Type = ctrl.initialQuery.toType(ru.typeOf[Graph]) + val publicData: PublicData + val entrypoint: Entrypoint + val queryExecutor: QueryExecutor + val db: Database + + val filterQuery: FilterQuery = queryExecutor.filterQuery + val queryType: ru.Type = publicData.initialQuery.toType(ru.typeOf[Graph]) - val inputFilterParser: FieldsParser[InputQuery[Unk, Unk]] = queryExecutor - .filterQuery + val inputFilterParser: FieldsParser[InputQuery[Unk, Unk]] = filterQuery .paramParser(queryType) val aggregationParser: FieldsParser[Aggregation] = @@ -87,9 +88,9 @@ class QueryCtrl(entrypoint: Entrypoint, @Named("with-thehive-schema") db: Databa filteredQuery = maybeInputFilter .map(inputFilter => filterQuery.toQuery(inputFilter)) - .fold(ctrl.initialQuery)(ctrl.initialQuery.andThen) + .fold(publicData.initialQuery)(publicData.initialQuery.andThen) aggs <- aggregationParser.sequence(field.get("stats")) - } yield aggs.map(a => filteredQuery andThen new AggregationQuery(db, publicProperties, queryExecutor.filterQuery).toQuery(a)) + } yield aggs.map(a => filteredQuery andThen new AggregationQuery(db, queryExecutor.publicProperties, filterQuery).toQuery(a)) } val searchParser: FieldsParser[Query] = FieldsParser[Query]("search") { @@ -99,16 +100,16 @@ class QueryCtrl(entrypoint: Entrypoint, @Named("with-thehive-schema") db: Databa filteredQuery = maybeInputFilter .map(inputFilter => filterQuery.toQuery(inputFilter)) - .fold(ctrl.initialQuery)(ctrl.initialQuery.andThen) + .fold(publicData.initialQuery)(publicData.initialQuery.andThen) inputSort <- sortParser(field.get("sort")) - sortedQuery = filteredQuery andThen new SortQuery(db, publicProperties).toQuery(inputSort) + sortedQuery = filteredQuery andThen new SortQuery(db, queryExecutor.publicProperties).toQuery(inputSort) outputParam <- outputParamParser.optional(field).map(_.getOrElse(OutputParam(0, 10, withStats = false, withParents = 0))) - outputQuery = ctrl.pageQuery.toQuery(outputParam) + outputQuery = publicData.pageQuery.toQuery(outputParam) } yield sortedQuery andThen outputQuery } def search: Action[AnyContent] = - entrypoint(s"search ${ctrl.entityName}") + entrypoint(s"search ${publicData.entityName}") .extract("query", searchParser) .auth { implicit request => val query: Query = request.body("query") @@ -116,7 +117,7 @@ class QueryCtrl(entrypoint: Entrypoint, @Named("with-thehive-schema") db: Databa } def stats: Action[AnyContent] = - entrypoint(s"${ctrl.entityName} stats") + entrypoint(s"${publicData.entityName} stats") .extract("query", statsParser) .authRoTransaction(db) { implicit request => graph => val queries: Seq[Query] = request.body("query") @@ -133,10 +134,3 @@ class QueryCtrl(entrypoint: Entrypoint, @Named("with-thehive-schema") db: Databa } } } - -@Singleton -class QueryCtrlBuilder @Inject() (entrypoint: Entrypoint, @Named("with-thehive-schema") db: Database) { - - def apply(ctrl: QueryableCtrl, queryExecutor: QueryExecutor): QueryCtrl = - new QueryCtrl(entrypoint, db, ctrl, queryExecutor) -} diff --git a/thehive/app/org/thp/thehive/controllers/v0/Router.scala b/thehive/app/org/thp/thehive/controllers/v0/Router.scala index e1b5772d59..abed990804 100644 --- a/thehive/app/org/thp/thehive/controllers/v0/Router.scala +++ b/thehive/app/org/thp/thehive/controllers/v0/Router.scala @@ -29,7 +29,6 @@ class Router @Inject() ( profileCtrl: ProfileCtrl, shareCtrl: ShareCtrl, tagCtrl: TagCtrl, - queryExecutor: TheHiveQueryExecutor, pageCtrl: PageCtrl, permissionCtrl: PermissionCtrl, observableTypeCtrl: ObservableTypeCtrl @@ -58,66 +57,66 @@ class Router @Inject() ( case DELETE(p"/case/share/$shareId") => shareCtrl.removeShare(shareId) case PATCH(p"/case/share/$shareId") => shareCtrl.updateShare(shareId) - case GET(p"/case/task") => queryExecutor.task.search + case GET(p"/case/task") => taskCtrl.search case POST(p"/case/$caseId/task") => taskCtrl.create(caseId) // Audit ok case GET(p"/case/task/$taskId") => taskCtrl.get(taskId) case PATCH(p"/case/task/$taskId") => taskCtrl.update(taskId) // Audit ok - case POST(p"/case/task/_search") => queryExecutor.task.search + case POST(p"/case/task/_search") => taskCtrl.search + case POST(p"/case/task/_stats") => taskCtrl.stats //case POST(p"/case/$caseId/task/_search") => taskCtrl.search - case POST(p"/case/task/_stats") => queryExecutor.task.stats //case GET(p"/case/task/$taskId/log") => logCtrl.findInTask(taskId) //case POST(p"/case/task/$taskId/log/_search") => logCtrl.findInTask(taskId) - case POST(p"/case/task/log/_search") => queryExecutor.log.search - case POST(p"/case/task/log/_stats") => queryExecutor.log.stats + case POST(p"/case/task/log/_search") => logCtrl.search + case POST(p"/case/task/log/_stats") => logCtrl.stats case POST(p"/case/task/$taskId/log") => logCtrl.create(taskId) // Audit ok - case PATCH(p"/case/task/log/$logId") => logCtrl.update(logId) // Audit ok - case DELETE(p"/case/task/log/$logId") => logCtrl.delete(logId) // Audit ok, weird logs/silent errors though (stream related) + case PATCH(p"/case/task/log/$logId") => logCtrl.update(logId) // Audit ok + case DELETE(p"/case/task/log/$logId") => logCtrl.delete(logId) // Audit ok, weird logs/silent errors though (stream related) // case GET(p"/case/task/log/$logId") => logCtrl.get(logId) - case POST(p"/case/artifact/_search") => queryExecutor.observable.search + case POST(p"/case/artifact/_search") => observableCtrl.search // case POST(p"/case/:caseId/artifact/_search") ⇒ observableCtrl.findInCase(caseId) - case POST(p"/case/artifact/_stats") => queryExecutor.observable.stats - case POST(p"/case/$caseId/artifact") => observableCtrl.create(caseId) // Audit ok + case POST(p"/case/artifact/_stats") => observableCtrl.stats + case POST(p"/case/$caseId/artifact") => observableCtrl.create(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/_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 GET(p"/case") => queryExecutor.`case`.search - case POST(p"/case") => caseCtrl.create // Audit ok + case GET(p"/case") => caseCtrl.search + 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 DELETE(p"/case/$caseId") => caseCtrl.delete(caseId) // Not used by frontend - case POST(p"/case/_search") => queryExecutor.`case`.search - case POST(p"/case/_stats") => queryExecutor.`case`.stats + 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 DELETE(p"/case/$caseId") => caseCtrl.delete(caseId) // Not used by frontend + case POST(p"/case/_search") => caseCtrl.search + case POST(p"/case/_stats") => caseCtrl.stats case DELETE(p"/case/$caseId/force") => caseCtrl.realDelete(caseId) // Audit ok case GET(p"/case/$caseId/links") => caseCtrl.linkedCases(caseId) - case GET(p"/case/template") => queryExecutor.caseTemplate.search - case POST(p"/case/template") => caseTemplateCtrl.create // Audit ok + case GET(p"/case/template") => caseTemplateCtrl.search + case POST(p"/case/template") => caseTemplateCtrl.create // Audit ok case GET(p"/case/template/$caseTemplateId") => caseTemplateCtrl.get(caseTemplateId) case PATCH(p"/case/template/$caseTemplateId") => caseTemplateCtrl.update(caseTemplateId) // Audit ok - case POST(p"/case/template/_search") => queryExecutor.caseTemplate.search + case POST(p"/case/template/_search") => caseTemplateCtrl.search case DELETE(p"/case/template/$caseTemplateId") => caseTemplateCtrl.delete(caseTemplateId) // Audit ok - case GET(p"/user") => queryExecutor.user.search - case POST(p"/user") => userCtrl.create // Audit ok + case GET(p"/user") => userCtrl.search + case POST(p"/user") => userCtrl.create // Audit ok case GET(p"/user/current") => userCtrl.current case GET(p"/user/$userId") => userCtrl.get(userId) - case PATCH(p"/user/$userId") => userCtrl.update(userId) // Audit ok - case DELETE(p"/user/$userId") => userCtrl.lock(userId) // Audit ok - case DELETE(p"/user/$userId/force") => userCtrl.delete(userId) // Audit ok - case POST(p"/user/$userId/password/set") => userCtrl.setPassword(userId) // Audit ok + case PATCH(p"/user/$userId") => userCtrl.update(userId) // Audit ok + case DELETE(p"/user/$userId") => userCtrl.lock(userId) // Audit ok + case DELETE(p"/user/$userId/force") => userCtrl.delete(userId) // Audit ok + case POST(p"/user/$userId/password/set") => userCtrl.setPassword(userId) // Audit ok case POST(p"/user/$userId/password/change") => userCtrl.changePassword(userId) // Audit ok case GET(p"/user/$userId/key") => userCtrl.getKey(userId) - case DELETE(p"/user/$userId/key") => userCtrl.removeKey(userId) // Audit ok - case POST(p"/user/$userId/key/renew") => userCtrl.renewKey(userId) // Audit ok - case POST(p"/user/_search") => queryExecutor.user.search + case DELETE(p"/user/$userId/key") => userCtrl.removeKey(userId) // Audit ok + case POST(p"/user/$userId/key/renew") => userCtrl.renewKey(userId) // Audit ok + case POST(p"/user/_search") => userCtrl.search case GET(p"/list") => listCtrl.list case DELETE(p"/list/$itemId") => listCtrl.deleteItem(itemId) @@ -127,7 +126,7 @@ class Router @Inject() ( case POST(p"/list/$listName/_exists") => listCtrl.itemExists(listName) case GET(p"/organisation") => organisationCtrl.list - case POST(p"/organisation") => organisationCtrl.create // Audit ok + case POST(p"/organisation") => organisationCtrl.create // Audit ok case GET(p"/organisation/$organisationId") => organisationCtrl.get(organisationId) case GET(p"/organisation/$organisationId/links") => organisationCtrl.listLinks(organisationId) case PATCH(p"/organisation/$organisationId") => organisationCtrl.update(organisationId) // Audit ok @@ -142,36 +141,36 @@ class Router @Inject() ( case PATCH(p"/customField/$id") => customFieldCtrl.update(id) case GET(p"/customFields/$id/use") => customFieldCtrl.useCount(id) - case GET(p"/alert") => queryExecutor.alert.search - case POST(p"/alert") => alertCtrl.create // Audit ok + case GET(p"/alert") => alertCtrl.search + case POST(p"/alert") => alertCtrl.create // Audit ok case GET(p"/alert/$alertId") => alertCtrl.get(alertId) - case PATCH(p"/alert/$alertId") => alertCtrl.update(alertId) // Audit ok - case POST(p"/alert/$alertId/markAsRead") => alertCtrl.markAsRead(alertId) // Audit ok - case POST(p"/alert/$alertId/markAsUnread") => alertCtrl.markAsUnread(alertId) // Audit ok - case POST(p"/alert/$alertId/follow") => alertCtrl.followAlert(alertId) // Audit ok + case PATCH(p"/alert/$alertId") => alertCtrl.update(alertId) // Audit ok + case POST(p"/alert/$alertId/markAsRead") => alertCtrl.markAsRead(alertId) // Audit ok + case POST(p"/alert/$alertId/markAsUnread") => alertCtrl.markAsUnread(alertId) // Audit ok + case POST(p"/alert/$alertId/follow") => alertCtrl.followAlert(alertId) // Audit ok case POST(p"/alert/$alertId/unfollow") => alertCtrl.unfollowAlert(alertId) // Audit ok - case POST(p"/alert/$alertId/createCase") => alertCtrl.createCase(alertId) // Audit ok - case POST(p"/alert/_search") => queryExecutor.alert.search + case POST(p"/alert/$alertId/createCase") => alertCtrl.createCase(alertId) // Audit ok + case POST(p"/alert/_search") => alertCtrl.search // PATCH /alert/_bulk controllers.AlertCtrl.bulkUpdate case POST(p"/alert/delete/_bulk") => alertCtrl.bulkDelete - case POST(p"/alert/_stats") => queryExecutor.alert.stats - case DELETE(p"/alert/$alertId") => alertCtrl.delete(alertId) // Audit ok + case POST(p"/alert/_stats") => alertCtrl.stats + case DELETE(p"/alert/$alertId") => alertCtrl.delete(alertId) // Audit ok case POST(p"/alert/$alertId/merge/$caseId") => alertCtrl.mergeWithCase(alertId, caseId) // Audit ok case POST(p"/alert/merge/_bulk") => alertCtrl.bulkMergeWithCase - case GET(p"/dashboard") => queryExecutor.dashboard.search - case POST(p"/dashboard/_search") => queryExecutor.dashboard.search - case POST(p"/dashboard/_stats") => queryExecutor.dashboard.stats - case POST(p"/dashboard") => dashboardCtrl.create // Audit ok + case GET(p"/dashboard") => dashboardCtrl.search + case POST(p"/dashboard/_search") => dashboardCtrl.search + case POST(p"/dashboard/_stats") => dashboardCtrl.stats + case POST(p"/dashboard") => dashboardCtrl.create // Audit ok case GET(p"/dashboard/$dashboardId") => dashboardCtrl.get(dashboardId) case PATCH(p"/dashboard/$dashboardId") => dashboardCtrl.update(dashboardId) // Audit ok case DELETE(p"/dashboard/$dashboardId") => dashboardCtrl.delete(dashboardId) // Audit ok case GET(p"/audit") => auditCtrl.flow(None) case GET(p"/flow" ? q_o"rootId=$rootId") => auditCtrl.flow(rootId) - case GET(p"/audit") => queryExecutor.audit.search - case POST(p"/audit/_search") => queryExecutor.audit.search - case POST(p"/audit/_stats") => queryExecutor.audit.stats + case GET(p"/audit") => auditCtrl.search + case POST(p"/audit/_search") => auditCtrl.search + case POST(p"/audit/_stats") => auditCtrl.stats case POST(p"/stream") => streamCtrl.create case GET(p"/stream/status") => streamCtrl.status @@ -190,29 +189,29 @@ class Router @Inject() ( case GET(p"/config/organisation/$path") => configCtrl.organisationGet(path) case PUT(p"/config/organisation/$path") => configCtrl.organisationSet(path) - case GET(p"/profile") => queryExecutor.profile.search - case POST(p"/profile/_search") => queryExecutor.profile.search - case POST(p"/profile/_stats") => queryExecutor.profile.stats + case GET(p"/profile") => profileCtrl.search + case POST(p"/profile/_search") => profileCtrl.search + case POST(p"/profile/_stats") => profileCtrl.stats case POST(p"/profile") => profileCtrl.create case GET(p"/profile/$profileId") => profileCtrl.get(profileId) case PATCH(p"/profile/$profileId") => profileCtrl.update(profileId) case DELETE(p"/profile/$profileId") => profileCtrl.delete(profileId) + case POST(p"/tag/_search") => tagCtrl.search + case POST(p"/tag/_stats") => tagCtrl.stats case POST(p"/tag/_import") => tagCtrl.importTaxonomy case GET(p"/tag/$id") => tagCtrl.get(id) - case POST(p"/tag/_search") => queryExecutor.tag.search - case POST(p"/tag/_stats") => queryExecutor.tag.stats + case POST(p"/page/_search") => pageCtrl.search + case POST(p"/page/_stats") => pageCtrl.stats case GET(p"/page/$idOrTitle") => pageCtrl.get(idOrTitle) case POST(p"/page") => pageCtrl.create case PATCH(p"/page/$idOrTitle") => pageCtrl.update(idOrTitle) case DELETE(p"/page/$idOrTitle") => pageCtrl.delete(idOrTitle) - case POST(p"/page/_search") => queryExecutor.page.search - case POST(p"/page/_stats") => queryExecutor.page.stats case GET(p"/permission") => permissionCtrl.list - case GET(p"/observable/type") => queryExecutor.observableType.search + case GET(p"/observable/type") => observableTypeCtrl.search case GET(p"/observable/type/$idOrName") => observableTypeCtrl.get(idOrName) case POST(p"/observable/type") => observableTypeCtrl.create case DELETE(p"/observable/type/$idOrName") => observableTypeCtrl.delete(idOrName) diff --git a/thehive/app/org/thp/thehive/controllers/v0/StatsCtrl.scala b/thehive/app/org/thp/thehive/controllers/v0/StatsCtrl.scala index 902a5a6bde..be261c5bea 100644 --- a/thehive/app/org/thp/thehive/controllers/v0/StatsCtrl.scala +++ b/thehive/app/org/thp/thehive/controllers/v0/StatsCtrl.scala @@ -12,6 +12,19 @@ import play.api.mvc.{Action, AnyContent, Results} @Singleton class StatsCtrl @Inject() ( entrypoint: Entrypoint, + caseCtrl: CaseCtrl, + taskCtrl: TaskCtrl, + logCtrl: LogCtrl, + alertCtrl: AlertCtrl, + userCtrl: UserCtrl, + caseTemplateCtrl: CaseTemplateCtrl, + observableCtrl: ObservableCtrl, + dashboardCtrl: DashboardCtrl, + organisationCtrl: OrganisationCtrl, + auditCtrl: AuditCtrl, + profileCtrl: ProfileCtrl, + tagCtrl: TagCtrl, + pageCtrl: PageCtrl, queryExecutor: TheHiveQueryExecutor, @Named("with-thehive-schema") db: Database ) { @@ -26,20 +39,20 @@ class StatsCtrl @Inject() ( .validatedBy { s => for { model <- FieldsParser.string(s.get("model")) - queryCtrl = model match { - case "case" => queryExecutor.`case` - case "case_task" => queryExecutor.task - case "case_task_log" => queryExecutor.log - case "alert" => queryExecutor.alert - case "user" => queryExecutor.user - case "caseTemplate" => queryExecutor.caseTemplate - case "case_artifact" => queryExecutor.observable - case "dashboard" => queryExecutor.dashboard - case "organisation" => queryExecutor.organisation - case "audit" => queryExecutor.audit - case "profile" => queryExecutor.profile - case "tag" => queryExecutor.tag - case "page" => queryExecutor.page + queryCtrl: QueryCtrl = model match { + case "case" => caseCtrl + case "case_task" => taskCtrl + case "case_task_log" => logCtrl + case "alert" => alertCtrl + case "user" => userCtrl + case "caseTemplate" => caseTemplateCtrl + case "case_artifact" => observableCtrl + case "dashboard" => dashboardCtrl + case "organisation" => organisationCtrl + case "audit" => auditCtrl + case "profile" => profileCtrl + case "tag" => tagCtrl + case "page" => pageCtrl } queries <- queryCtrl.statsParser(s) } yield queries diff --git a/thehive/app/org/thp/thehive/controllers/v0/TagCtrl.scala b/thehive/app/org/thp/thehive/controllers/v0/TagCtrl.scala index 2b2de10a1b..bf5e7b8a69 100644 --- a/thehive/app/org/thp/thehive/controllers/v0/TagCtrl.scala +++ b/thehive/app/org/thp/thehive/controllers/v0/TagCtrl.scala @@ -2,12 +2,12 @@ package org.thp.thehive.controllers.v0 import java.nio.file.Files -import javax.inject.{Inject, Named} +import javax.inject.{Inject, Named, Singleton} import org.apache.tinkerpop.gremlin.structure.Vertex import org.thp.scalligraph.RichSeq import org.thp.scalligraph.controllers.{Entrypoint, FFile, FieldsParser, Renderer} -import org.thp.scalligraph.models.{Database, Entity} -import org.thp.scalligraph.query.{ParamQuery, PublicProperty, Query} +import org.thp.scalligraph.models.{Database, Entity, UMapping} +import org.thp.scalligraph.query._ import org.thp.scalligraph.traversal.TraversalOps._ import org.thp.scalligraph.traversal.{Converter, IteratorOutput, Traversal} import org.thp.thehive.controllers.v0.Conversion._ @@ -20,33 +20,12 @@ import play.api.mvc.{Action, AnyContent, Results} import scala.util.Try class TagCtrl @Inject() ( - entrypoint: Entrypoint, - @Named("with-thehive-schema") db: Database, - properties: Properties, - tagSrv: TagSrv -) extends QueryableCtrl { - override val entityName: String = "tag" - override val publicProperties: List[PublicProperty[_, _]] = properties.tag - override val initialQuery: Query = Query.init[Traversal.V[Tag]]("listTag", (graph, _) => tagSrv.startTraversal(graph)) - override val pageQuery: ParamQuery[OutputParam] = Query.withParam[OutputParam, Traversal.V[Tag], IteratorOutput]( - "page", - FieldsParser[OutputParam], - (range, tagSteps, _) => tagSteps.page(range.from, range.to, withTotal = true) - ) - override val outputQuery: Query = Query.output[Tag with Entity] - override val getQuery: ParamQuery[IdOrName] = Query.initWithParam[IdOrName, Traversal.V[Tag]]( - "getTag", - FieldsParser[IdOrName], - (param, graph, _) => tagSrv.get(param.idOrName)(graph) - ) - implicit val stringRenderer: Renderer[String] = Renderer.toJson[String, String](identity) - override val extraQueries: Seq[ParamQuery[_]] = Seq( - Query[Traversal.V[Tag], Traversal.V[Tag]]("fromCase", (tagSteps, _) => tagSteps.fromCase), - Query[Traversal.V[Tag], Traversal.V[Tag]]("fromObservable", (tagSteps, _) => tagSteps.fromObservable), - Query[Traversal.V[Tag], Traversal[String, Vertex, Converter[String, Vertex]]]("text", (tagSteps, _) => tagSteps.displayName), - Query.output[String, Traversal[String, Vertex, Converter[String, Vertex]]] - ) - + override val entrypoint: Entrypoint, + @Named("with-thehive-schema") override val db: Database, + tagSrv: TagSrv, + override val queryExecutor: QueryExecutor, + override val publicData: PublicTag +) extends QueryCtrl { def importTaxonomy: Action[AnyContent] = entrypoint("import taxonomy") .extract("file", FieldsParser.file.optional.on("file")) @@ -74,20 +53,23 @@ class TagCtrl @Inject() ( def parseValues(namespace: String, values: Seq[JsObject]): Seq[Tag] = for { - value <- values - .foldLeft((Seq.empty[JsObject], Seq.empty[String]))((acc, v) => distinct((v \ "predicate").asOpt[String], acc, v)) - ._1 + value <- + values + .foldLeft((Seq.empty[JsObject], Seq.empty[String]))((acc, v) => distinct((v \ "predicate").asOpt[String], acc, v)) + ._1 predicate <- (value \ "predicate").asOpt[String].toList - entry <- (value \ "entry") - .asOpt[Seq[JsObject]] - .getOrElse(Nil) - .foldLeft((Seq.empty[JsObject], Seq.empty[String]))((acc, v) => distinct((v \ "value").asOpt[String], acc, v)) - ._1 + entry <- + (value \ "entry") + .asOpt[Seq[JsObject]] + .getOrElse(Nil) + .foldLeft((Seq.empty[JsObject], Seq.empty[String]))((acc, v) => distinct((v \ "value").asOpt[String], acc, v)) + ._1 v <- (entry \ "value").asOpt[String] - colour = (entry \ "colour") - .asOpt[String] - .map(parseColour) - .getOrElse(0) // black + colour = + (entry \ "colour") + .asOpt[String] + .map(parseColour) + .getOrElse(0) // black e = (entry \ "description").asOpt[String] orElse (entry \ "expanded").asOpt[String] } yield Tag(namespace, predicate, Some(v), e, colour) @@ -99,15 +81,17 @@ class TagCtrl @Inject() ( def parsePredicates(namespace: String, predicates: Seq[JsObject]): Seq[Tag] = for { - predicate <- predicates - .foldLeft((Seq.empty[JsObject], Seq.empty[String]))((acc, v) => distinct((v \ "value").asOpt[String], acc, v)) - ._1 + predicate <- + predicates + .foldLeft((Seq.empty[JsObject], Seq.empty[String]))((acc, v) => distinct((v \ "value").asOpt[String], acc, v)) + ._1 v <- (predicate \ "value").asOpt[String] e = (predicate \ "expanded").asOpt[String] - colour = (predicate \ "colour") - .asOpt[String] - .map(parseColour) - .getOrElse(0) // black + colour = + (predicate \ "colour") + .asOpt[String] + .map(parseColour) + .getOrElse(0) // black } yield Tag(namespace, v, None, e, colour) def get(tagId: String): Action[AnyContent] = @@ -120,3 +104,34 @@ class TagCtrl @Inject() ( } } } + +@Singleton +class PublicTag @Inject() (tagSrv: TagSrv) extends PublicData { + override val entityName: String = "tag" + override val initialQuery: Query = Query.init[Traversal.V[Tag]]("listTag", (graph, _) => tagSrv.startTraversal(graph)) + override val pageQuery: ParamQuery[OutputParam] = Query.withParam[OutputParam, Traversal.V[Tag], IteratorOutput]( + "page", + FieldsParser[OutputParam], + (range, tagSteps, _) => tagSteps.page(range.from, range.to, withTotal = true) + ) + override val outputQuery: Query = Query.output[Tag with Entity] + override val getQuery: ParamQuery[IdOrName] = Query.initWithParam[IdOrName, Traversal.V[Tag]]( + "getTag", + FieldsParser[IdOrName], + (param, graph, _) => tagSrv.get(param.idOrName)(graph) + ) + implicit val stringRenderer: Renderer[String] = Renderer.toJson[String, String](identity) + override val extraQueries: Seq[ParamQuery[_]] = Seq( + Query[Traversal.V[Tag], Traversal.V[Tag]]("fromCase", (tagSteps, _) => tagSteps.fromCase), + Query[Traversal.V[Tag], Traversal.V[Tag]]("fromObservable", (tagSteps, _) => tagSteps.fromObservable), + Query[Traversal.V[Tag], Traversal[String, Vertex, Converter[String, Vertex]]]("text", (tagSteps, _) => tagSteps.displayName), + Query.output[String, Traversal[String, Vertex, Converter[String, Vertex]]] + ) + override val publicProperties: PublicProperties = PublicPropertyListBuilder[Tag] + .property("namespace", UMapping.string)(_.field.readonly) + .property("predicate", UMapping.string)(_.field.readonly) + .property("value", UMapping.string.optional)(_.field.readonly) + .property("description", UMapping.string.optional)(_.field.readonly) + .property("text", UMapping.string)(_.select(_.displayName).readonly) + .build +} diff --git a/thehive/app/org/thp/thehive/controllers/v0/TaskCtrl.scala b/thehive/app/org/thp/thehive/controllers/v0/TaskCtrl.scala index d07c92f6d0..0a3bd9465c 100644 --- a/thehive/app/org/thp/thehive/controllers/v0/TaskCtrl.scala +++ b/thehive/app/org/thp/thehive/controllers/v0/TaskCtrl.scala @@ -3,8 +3,8 @@ package org.thp.thehive.controllers.v0 import javax.inject.{Inject, Named, Singleton} import org.thp.scalligraph.RichOptionTry import org.thp.scalligraph.controllers._ -import org.thp.scalligraph.models.Database -import org.thp.scalligraph.query.{ParamQuery, PropertyUpdater, PublicProperty, Query} +import org.thp.scalligraph.models.{Database, UMapping} +import org.thp.scalligraph.query._ import org.thp.scalligraph.traversal.TraversalOps._ import org.thp.scalligraph.traversal.{IteratorOutput, Traversal} import org.thp.thehive.controllers.v0.Conversion._ @@ -15,47 +15,21 @@ import org.thp.thehive.services.OrganisationOps._ import org.thp.thehive.services.ShareOps._ import org.thp.thehive.services.TaskOps._ import org.thp.thehive.services._ -import play.api.Logger +import play.api.libs.json.Json import play.api.mvc.{Action, AnyContent, Results} @Singleton class TaskCtrl @Inject() ( - entrypoint: Entrypoint, - @Named("with-thehive-schema") db: Database, - properties: Properties, + override val entrypoint: Entrypoint, + @Named("with-thehive-schema") override val db: Database, taskSrv: TaskSrv, caseSrv: CaseSrv, userSrv: UserSrv, organisationSrv: OrganisationSrv, - shareSrv: ShareSrv -) extends QueryableCtrl { - - lazy val logger: Logger = Logger(getClass) - override val entityName: String = "task" - override val publicProperties: List[PublicProperty[_, _]] = properties.task - override val initialQuery: Query = - Query.init[Traversal.V[Task]]("listTask", (graph, authContext) => organisationSrv.get(authContext.organisation)(graph).shares.tasks) - override val pageQuery: ParamQuery[OutputParam] = Query.withParam[OutputParam, Traversal.V[Task], IteratorOutput]( - "page", - FieldsParser[OutputParam], { - case (OutputParam(from, to, _, 0), taskSteps, _) => - taskSteps.richPage(from, to, withTotal = true)(_.richTask.domainMap(_ -> (None: Option[RichCase]))) - case (OutputParam(from, to, _, _), taskSteps, authContext) => - taskSteps.richPage(from, to, withTotal = true)( - _.richTaskWithCustomRenderer(_.`case`.richCase(authContext).domainMap(c => Some(c): Option[RichCase])) - ) - } - ) - override val getQuery: ParamQuery[IdOrName] = Query.initWithParam[IdOrName, Traversal.V[Task]]( - "getTask", - FieldsParser[IdOrName], - (param, graph, authContext) => taskSrv.get(param.idOrName)(graph).visible(authContext) - ) - override val outputQuery: Query = Query.output[RichTask, Traversal.V[Task]](_.richTask) - override val extraQueries: Seq[ParamQuery[_]] = Seq( - Query.output[(RichTask, Option[RichCase])], - Query[Traversal.V[Task], Traversal.V[User]]("assignableUsers", (taskSteps, authContext) => taskSteps.assignableUsers(authContext)) - ) + shareSrv: ShareSrv, + override val queryExecutor: QueryExecutor, + override val publicData: PublicTask +) extends QueryCtrl { def create(caseId: String): Action[AnyContent] = entrypoint("create task") @@ -86,7 +60,7 @@ class TaskCtrl @Inject() ( def update(taskId: String): Action[AnyContent] = entrypoint("update task") - .extract("task", FieldsParser.update("task", publicProperties)) + .extract("task", FieldsParser.update("task", publicData.publicProperties)) .authTransaction(db) { implicit request => implicit graph => val propertyUpdaters: Seq[PropertyUpdater] = request.body("task") taskSrv @@ -103,4 +77,75 @@ class TaskCtrl @Inject() ( .map(richTask => Results.Ok(richTask.toJson)) } } + +// def searchInCase(caseId: String): Action[AnyContent] = +// entrypoint("search task in case") +// .extract("query", searchParser) +// .auth { implicit request => +// val query: Query = request.body("query") +// queryExecutor.execute(query, request) +// } +} + +@Singleton +class PublicTask @Inject() (taskSrv: TaskSrv, organisationSrv: OrganisationSrv, userSrv: UserSrv) extends PublicData { + override val entityName: String = "task" + override val initialQuery: Query = + Query.init[Traversal.V[Task]]("listTask", (graph, authContext) => organisationSrv.get(authContext.organisation)(graph).shares.tasks) + override val pageQuery: ParamQuery[OutputParam] = Query.withParam[OutputParam, Traversal.V[Task], IteratorOutput]( + "page", + FieldsParser[OutputParam], + { + case (OutputParam(from, to, _, 0), taskSteps, _) => + taskSteps.richPage(from, to, withTotal = true)(_.richTask.domainMap(_ -> (None: Option[RichCase]))) + case (OutputParam(from, to, _, _), taskSteps, authContext) => + taskSteps.richPage(from, to, withTotal = true)( + _.richTaskWithCustomRenderer(_.`case`.richCase(authContext).domainMap(c => Some(c): Option[RichCase])) + ) + } + ) + override val getQuery: ParamQuery[IdOrName] = Query.initWithParam[IdOrName, Traversal.V[Task]]( + "getTask", + FieldsParser[IdOrName], + (param, graph, authContext) => taskSrv.get(param.idOrName)(graph).visible(authContext) + ) + override val outputQuery: Query = Query.output[RichTask, Traversal.V[Task]](_.richTask) + override val extraQueries: Seq[ParamQuery[_]] = Seq( + Query.output[(RichTask, Option[RichCase])], + Query[Traversal.V[Task], Traversal.V[User]]("assignableUsers", (taskSteps, authContext) => taskSteps.assignableUsers(authContext)) + ) + override val publicProperties: PublicProperties = PublicPropertyListBuilder[Task] + .property("title", UMapping.string)(_.field.updatable) + .property("description", UMapping.string.optional)(_.field.updatable) + .property("status", UMapping.enum[TaskStatus.type])(_.field.custom { (_, value, vertex, _, graph, authContext) => + for { + task <- taskSrv.get(vertex)(graph).getOrFail("Task") + user <- + userSrv + .current(graph, authContext) + .getOrFail("User") + _ <- taskSrv.updateStatus(task, user, value)(graph, authContext) + } yield Json.obj("status" -> value) + }) + .property("flag", UMapping.boolean)(_.field.updatable) + .property("startDate", UMapping.date.optional)(_.field.updatable) + .property("endDate", UMapping.date.optional)(_.field.updatable) + .property("order", UMapping.int)(_.field.updatable) + .property("dueDate", UMapping.date.optional)(_.field.updatable) + .property("group", UMapping.string)(_.field.updatable) + .property("owner", UMapping.string.optional)( + _.select(_.assignee.value(_.login)) + .custom { (_, login: Option[String], vertex, _, graph, authContext) => + for { + task <- taskSrv.get(vertex)(graph).getOrFail("Task") + user <- login.map(userSrv.getOrFail(_)(graph)).flip + _ <- user match { + case Some(u) => taskSrv.assign(task, u)(graph, authContext) + case None => taskSrv.unassign(task)(graph, authContext) + } + } yield Json.obj("owner" -> user.map(_.login)) + } + ) + .build + } diff --git a/thehive/app/org/thp/thehive/controllers/v0/TheHiveQueryExecutor.scala b/thehive/app/org/thp/thehive/controllers/v0/TheHiveQueryExecutor.scala index 99eb5fabbb..592a84d0ed 100644 --- a/thehive/app/org/thp/thehive/controllers/v0/TheHiveQueryExecutor.scala +++ b/thehive/app/org/thp/thehive/controllers/v0/TheHiveQueryExecutor.scala @@ -35,35 +35,36 @@ object OutputParam { @Singleton class TheHiveQueryExecutor @Inject() ( @Named("with-thehive-schema") override val db: Database, - caseCtrl: CaseCtrl, - taskCtrl: TaskCtrl, - logCtrl: LogCtrl, - observableCtrl: ObservableCtrl, - alertCtrl: AlertCtrl, - userCtrl: UserCtrl, - caseTemplateCtrl: CaseTemplateCtrl, - dashboardCtrl: DashboardCtrl, - organisationCtrl: OrganisationCtrl, - auditCtrl: AuditCtrl, - profileCtrl: ProfileCtrl, - tagCtrl: TagCtrl, - pageCtrl: PageCtrl, - observableTypeCtrl: ObservableTypeCtrl, - queryCtrlBuilder: QueryCtrlBuilder + `case`: PublicCase, + task: PublicTask, + log: PublicLog, + observable: PublicObservable, + alert: PublicAlert, + user: PublicUser, + caseTemplate: PublicCaseTemplate, + dashboard: PublicDashboard, + organisation: PublicOrganisation, + audit: PublicAudit, + profile: PublicProfile, + tag: PublicTag, + page: PublicPage, + observableType: PublicObservableType, + customField: PublicCustomField ) extends QueryExecutor { - lazy val controllers: List[QueryableCtrl] = - caseCtrl :: taskCtrl :: logCtrl :: observableCtrl :: alertCtrl :: userCtrl :: caseTemplateCtrl :: dashboardCtrl :: organisationCtrl :: auditCtrl :: profileCtrl :: tagCtrl :: pageCtrl :: observableTypeCtrl :: Nil + lazy val publicDatas: List[PublicData] = + `case` :: task :: log :: observable :: alert :: user :: caseTemplate :: dashboard :: organisation :: audit :: profile :: tag :: page :: observableType :: customField :: Nil - def metaProperties: List[PublicProperty[_, _]] = - PublicPropertyListBuilder[Product] + def metaProperties: PublicProperties = + PublicPropertyListBuilder + .forType[Product](_ => true) .property("createdBy", UMapping.string)(_.rename("_createdBy").readonly) .property("createdAt", UMapping.date)(_.rename("_createdAt").readonly) .property("updatedBy", UMapping.string.optional)(_.rename("_updatedBy").readonly) .property("updatedAt", UMapping.date.optional)(_.rename("_updatedAt").readonly) .build - override lazy val publicProperties: List[PublicProperty[_, _]] = controllers.flatMap(_.publicProperties) ::: metaProperties + override lazy val publicProperties: PublicProperties = publicDatas.foldLeft(metaProperties)(_ ++ _.publicProperties) val childTypes: PartialFunction[(ru.Type, String), ru.Type] = { case (tpe, "case_task_log") if SubType(tpe, ru.typeOf[Traversal.V[Task]]) => ru.typeOf[Traversal.V[Log]] @@ -87,26 +88,12 @@ class TheHiveQueryExecutor @Inject() ( } override lazy val queries: Seq[ParamQuery[_]] = - controllers.map(_.initialQuery) ::: - controllers.map(_.getQuery) ::: - controllers.map(_.pageQuery) ::: - controllers.map(_.outputQuery) ::: - controllers.flatMap(_.extraQueries) + publicDatas.map(_.initialQuery) ::: + publicDatas.map(_.getQuery) ::: + publicDatas.map(_.pageQuery) ::: + publicDatas.map(_.outputQuery) ::: + publicDatas.flatMap(_.extraQueries) override val version: (Int, Int) = 0 -> 0 - val `case`: QueryCtrl = queryCtrlBuilder(caseCtrl, this) - val task: QueryCtrl = queryCtrlBuilder(taskCtrl, this) - val log: QueryCtrl = queryCtrlBuilder(logCtrl, this) - val alert: QueryCtrl = queryCtrlBuilder(alertCtrl, this) - val user: QueryCtrl = queryCtrlBuilder(userCtrl, this) - val caseTemplate: QueryCtrl = queryCtrlBuilder(caseTemplateCtrl, this) - val observable: QueryCtrl = queryCtrlBuilder(observableCtrl, this) - val observableType: QueryCtrl = queryCtrlBuilder(observableTypeCtrl, this) - val dashboard: QueryCtrl = queryCtrlBuilder(dashboardCtrl, this) - val organisation: QueryCtrl = queryCtrlBuilder(organisationCtrl, this) - val audit: QueryCtrl = queryCtrlBuilder(auditCtrl, this) - val profile: QueryCtrl = queryCtrlBuilder(profileCtrl, this) - val tag: QueryCtrl = queryCtrlBuilder(tagCtrl, this) - val page: QueryCtrl = queryCtrlBuilder(pageCtrl, this) } object ParentIdFilter { @@ -123,7 +110,7 @@ object ParentIdFilter { class ParentIdInputFilter(parentId: String) extends InputQuery[Traversal.Unk, Traversal.Unk] { override def apply( db: Database, - publicProperties: List[PublicProperty[_, _]], + publicProperties: PublicProperties, traversalType: ru.Type, traversal: Traversal.Unk, authContext: AuthContext @@ -154,7 +141,7 @@ object ParentQueryFilter { class ParentQueryInputFilter(parentFilter: InputQuery[Traversal.Unk, Traversal.Unk]) extends InputQuery[Traversal.Unk, Traversal.Unk] { override def apply( db: Database, - publicProperties: List[PublicProperty[_, _]], + publicProperties: PublicProperties, traversalType: ru.Type, traversal: Traversal.Unk, authContext: AuthContext @@ -194,7 +181,7 @@ class ChildQueryInputFilter(childType: String, childFilter: InputQuery[Traversal extends InputQuery[Traversal.Unk, Traversal.Unk] { override def apply( db: Database, - publicProperties: List[PublicProperty[_, _]], + publicProperties: PublicProperties, traversalType: ru.Type, traversal: Traversal.Unk, authContext: AuthContext diff --git a/thehive/app/org/thp/thehive/controllers/v0/UserCtrl.scala b/thehive/app/org/thp/thehive/controllers/v0/UserCtrl.scala index 101f918f96..96fac67f8d 100644 --- a/thehive/app/org/thp/thehive/controllers/v0/UserCtrl.scala +++ b/thehive/app/org/thp/thehive/controllers/v0/UserCtrl.scala @@ -1,59 +1,37 @@ package org.thp.thehive.controllers.v0 import javax.inject.{Inject, Named, Singleton} +import org.apache.tinkerpop.gremlin.process.traversal.P import org.thp.scalligraph.auth.AuthSrv -import org.thp.scalligraph.controllers.{Entrypoint, FieldsParser} -import org.thp.scalligraph.models.Database -import org.thp.scalligraph.query.{ParamQuery, PropertyUpdater, PublicProperty, Query} +import org.thp.scalligraph.controllers.{Entrypoint, FString, FieldsParser} +import org.thp.scalligraph.models.{Database, UMapping} +import org.thp.scalligraph.query._ import org.thp.scalligraph.traversal.TraversalOps._ import org.thp.scalligraph.traversal.{IteratorOutput, Traversal} -import org.thp.scalligraph.{AuthorizationError, RichOptionTry} +import org.thp.scalligraph.{AuthorizationError, InvalidFormatAttributeError, RichOptionTry} import org.thp.thehive.controllers.v0.Conversion._ import org.thp.thehive.dto.v0.InputUser import org.thp.thehive.models._ import org.thp.thehive.services.OrganisationOps._ import org.thp.thehive.services.UserOps._ import org.thp.thehive.services._ -import play.api.Logger import play.api.libs.json.Json import play.api.mvc.{Action, AnyContent, Results} -import scala.util.{Failure, Success} +import scala.util.{Failure, Success, Try} @Singleton class UserCtrl @Inject() ( - entrypoint: Entrypoint, - properties: Properties, + override val entrypoint: Entrypoint, userSrv: UserSrv, profileSrv: ProfileSrv, authSrv: AuthSrv, organisationSrv: OrganisationSrv, auditSrv: AuditSrv, - @Named("with-thehive-schema") implicit val db: Database -) extends QueryableCtrl { - lazy val logger: Logger = Logger(getClass) - override val entityName: String = "user" - override val publicProperties: List[PublicProperty[_, _]] = properties.user - - override val initialQuery: Query = - Query.init[Traversal.V[User]]("listUser", (graph, authContext) => organisationSrv.get(authContext.organisation)(graph).users) - - override val getQuery: ParamQuery[IdOrName] = Query.initWithParam[IdOrName, Traversal.V[User]]( - "getUser", - FieldsParser[IdOrName], - (param, graph, authContext) => userSrv.get(param.idOrName)(graph).visible(authContext) - ) - - override val pageQuery: ParamQuery[OutputParam] = Query.withParam[OutputParam, Traversal.V[User], IteratorOutput]( - "page", - FieldsParser[OutputParam], - (range, userSteps, authContext) => userSteps.richUser(authContext).page(range.from, range.to, withTotal = true) - ) - override val outputQuery: Query = - Query.outputWithContext[RichUser, Traversal.V[User]]((userSteps, authContext) => userSteps.richUser(authContext)) - - override val extraQueries: Seq[ParamQuery[_]] = Seq() - + @Named("with-thehive-schema") implicit override val db: Database, + override val queryExecutor: QueryExecutor, + override val publicData: PublicUser +) extends QueryCtrl { def current: Action[AnyContent] = entrypoint("current user") .authRoTransaction(db) { implicit request => implicit graph => @@ -130,7 +108,7 @@ class UserCtrl @Inject() ( def update(userId: String): Action[AnyContent] = entrypoint("update user") - .extract("user", FieldsParser.update("user", properties.user)) + .extract("user", FieldsParser.update("user", publicData.publicProperties)) .authTransaction(db) { implicit request => implicit graph => val propertyUpdaters: Seq[PropertyUpdater] = request.body("user") for { @@ -247,3 +225,69 @@ class UserCtrl @Inject() ( } yield Results.Ok(key) } } + +@Singleton +class PublicUser @Inject() (userSrv: UserSrv, organisationSrv: OrganisationSrv, @Named("with-thehive-schema") db: Database) extends PublicData { + override val entityName: String = "user" + override val initialQuery: Query = + Query.init[Traversal.V[User]]("listUser", (graph, authContext) => organisationSrv.get(authContext.organisation)(graph).users) + override val getQuery: ParamQuery[IdOrName] = Query.initWithParam[IdOrName, Traversal.V[User]]( + "getUser", + FieldsParser[IdOrName], + (param, graph, authContext) => userSrv.get(param.idOrName)(graph).visible(authContext) + ) + override val pageQuery: ParamQuery[OutputParam] = Query.withParam[OutputParam, Traversal.V[User], IteratorOutput]( + "page", + FieldsParser[OutputParam], + (range, userSteps, authContext) => userSteps.richUser(authContext).page(range.from, range.to, withTotal = true) + ) + override val outputQuery: Query = + Query.outputWithContext[RichUser, Traversal.V[User]]((userSteps, authContext) => userSteps.richUser(authContext)) + override val extraQueries: Seq[ParamQuery[_]] = Seq() + override val publicProperties: PublicProperties = PublicPropertyListBuilder[User] + .property("login", UMapping.string)(_.field.readonly) + .property("name", UMapping.string)(_.field.custom { (_, value, vertex, db, graph, authContext) => + def isCurrentUser: Try[Unit] = + userSrv + .current(graph, authContext) + .getByIds(vertex.id.toString) + .existsOrFail + + def isUserAdmin: Try[Unit] = + userSrv + .current(graph, authContext) + .organisations(Permissions.manageUser)(db) + .users + .getByIds(vertex.id.toString) + .existsOrFail + + isCurrentUser + .orElse(isUserAdmin) + .map { _ => + UMapping.string.setProperty(vertex, "name", value) + Json.obj("name" -> value) + } + }) + .property("status", UMapping.string)( + _.select(_.choose(predicate = _.value(_.locked).is(P.eq(true)), onTrue = _.constant("Locked"), onFalse = _.constant("Ok"))) + .custom { (_, value, vertex, _, graph, authContext) => + userSrv + .current(graph, authContext) + .organisations(Permissions.manageUser)(db) + .users + .getByIds(vertex.id.toString) + .orFail(AuthorizationError("Operation not permitted")) + .flatMap { + case user if value == "Ok" => + userSrv.unlock(user)(graph, authContext) + Success(Json.obj("status" -> value)) + case user if value == "Locked" => + userSrv.lock(user)(graph, authContext) + Success(Json.obj("status" -> value)) + case _ => Failure(InvalidFormatAttributeError("status", "UserStatus", Set("Ok", "Locked"), FString(value))) + } + } + ) + .build + +} diff --git a/thehive/app/org/thp/thehive/controllers/v1/AlertCtrl.scala b/thehive/app/org/thp/thehive/controllers/v1/AlertCtrl.scala index b37fa2f50b..9c1d1ef0cb 100644 --- a/thehive/app/org/thp/thehive/controllers/v1/AlertCtrl.scala +++ b/thehive/app/org/thp/thehive/controllers/v1/AlertCtrl.scala @@ -5,7 +5,7 @@ import java.util.{Map => JMap} import javax.inject.{Inject, Named, Singleton} import org.thp.scalligraph.controllers.{Entrypoint, FieldsParser} import org.thp.scalligraph.models.Database -import org.thp.scalligraph.query.{ParamQuery, PropertyUpdater, PublicProperty, Query} +import org.thp.scalligraph.query.{ParamQuery, PropertyUpdater, PublicProperties, Query} import org.thp.scalligraph.traversal.TraversalOps._ import org.thp.scalligraph.traversal.{Converter, IteratorOutput, Traversal} import org.thp.thehive.controllers.v1.Conversion._ @@ -31,8 +31,8 @@ class AlertCtrl @Inject() ( ) extends QueryableCtrl with AlertRenderer { - override val entityName: String = "alert" - override val publicProperties: List[PublicProperty[_, _]] = properties.alert + override val entityName: String = "alert" + override val publicProperties: PublicProperties = properties.alert override val initialQuery: Query = Query.init[Traversal.V[Alert]]("listAlert", (graph, authContext) => organisationSrv.get(authContext.organisation)(graph).alerts) override val getQuery: ParamQuery[IdOrName] = Query.initWithParam[IdOrName, Traversal.V[Alert]]( diff --git a/thehive/app/org/thp/thehive/controllers/v1/AuditCtrl.scala b/thehive/app/org/thp/thehive/controllers/v1/AuditCtrl.scala index bf9d210fcf..f98a424ae5 100644 --- a/thehive/app/org/thp/thehive/controllers/v1/AuditCtrl.scala +++ b/thehive/app/org/thp/thehive/controllers/v1/AuditCtrl.scala @@ -3,7 +3,7 @@ package org.thp.thehive.controllers.v1 import javax.inject.{Inject, Named, Singleton} import org.thp.scalligraph.controllers.{Entrypoint, FieldsParser} import org.thp.scalligraph.models.{Database, Schema} -import org.thp.scalligraph.query.{ParamQuery, PublicProperty, Query} +import org.thp.scalligraph.query.{ParamQuery, PublicProperties, Query} import org.thp.scalligraph.traversal.TraversalOps._ import org.thp.scalligraph.traversal.{IteratorOutput, Traversal} import org.thp.thehive.controllers.v1.Conversion._ @@ -27,7 +27,7 @@ class AuditCtrl @Inject() ( val initialQuery: Query = Query.init[Traversal.V[Audit]]("listAudit", (graph, authContext) => auditSrv.startTraversal(graph).visible(authContext)) - val publicProperties: List[PublicProperty[_, _]] = properties.audit + val publicProperties: PublicProperties = properties.audit override val getQuery: ParamQuery[IdOrName] = Query.initWithParam[IdOrName, Traversal.V[Audit]]( "getAudit", FieldsParser[IdOrName], diff --git a/thehive/app/org/thp/thehive/controllers/v1/CaseCtrl.scala b/thehive/app/org/thp/thehive/controllers/v1/CaseCtrl.scala index 555b6e69bb..21ef75e23f 100644 --- a/thehive/app/org/thp/thehive/controllers/v1/CaseCtrl.scala +++ b/thehive/app/org/thp/thehive/controllers/v1/CaseCtrl.scala @@ -3,17 +3,17 @@ package org.thp.thehive.controllers.v1 import javax.inject.{Inject, Named, Singleton} import org.thp.scalligraph.controllers.{Entrypoint, FieldsParser} import org.thp.scalligraph.models.{Database, Entity} -import org.thp.scalligraph.query.{ParamQuery, PropertyUpdater, PublicProperty, Query} +import org.thp.scalligraph.query.{ParamQuery, PropertyUpdater, PublicProperties, Query} import org.thp.scalligraph.traversal.TraversalOps._ import org.thp.scalligraph.traversal.{IteratorOutput, Traversal} import org.thp.scalligraph.{RichOptionTry, RichSeq} import org.thp.thehive.controllers.v1.Conversion._ import org.thp.thehive.dto.v1.{InputCase, InputTask} import org.thp.thehive.models._ +import org.thp.thehive.services.AlertOps._ import org.thp.thehive.services.CaseOps._ import org.thp.thehive.services.CaseTemplateOps._ import org.thp.thehive.services.OrganisationOps._ -import org.thp.thehive.services.AlertOps._ import org.thp.thehive.services.UserOps._ import org.thp.thehive.services._ import play.api.mvc.{Action, AnyContent, Results} @@ -33,8 +33,8 @@ class CaseCtrl @Inject() ( ) extends QueryableCtrl with CaseRenderer { - override val entityName: String = "case" - override val publicProperties: List[PublicProperty[_, _]] = properties.`case` + override val entityName: String = "case" + override val publicProperties: PublicProperties = properties.`case` override val initialQuery: Query = Query.init[Traversal.V[Case]]("listCase", (graph, authContext) => organisationSrv.get(authContext.organisation)(graph).cases) override val getQuery: ParamQuery[IdOrName] = Query.initWithParam[IdOrName, Traversal.V[Case]]( diff --git a/thehive/app/org/thp/thehive/controllers/v1/CaseTemplateCtrl.scala b/thehive/app/org/thp/thehive/controllers/v1/CaseTemplateCtrl.scala index 9e85589031..233fda6c3d 100644 --- a/thehive/app/org/thp/thehive/controllers/v1/CaseTemplateCtrl.scala +++ b/thehive/app/org/thp/thehive/controllers/v1/CaseTemplateCtrl.scala @@ -3,7 +3,7 @@ package org.thp.thehive.controllers.v1 import javax.inject.{Inject, Named, Singleton} import org.thp.scalligraph.controllers.{Entrypoint, FieldsParser} import org.thp.scalligraph.models.Database -import org.thp.scalligraph.query.{ParamQuery, PropertyUpdater, PublicProperty, Query} +import org.thp.scalligraph.query.{ParamQuery, PropertyUpdater, PublicProperties, Query} import org.thp.scalligraph.traversal.TraversalOps._ import org.thp.scalligraph.traversal.{IteratorOutput, Traversal} import org.thp.thehive.controllers.v1.Conversion._ @@ -25,8 +25,8 @@ class CaseTemplateCtrl @Inject() ( @Named("with-thehive-schema") implicit val db: Database ) extends QueryableCtrl { - override val entityName: String = "caseTemplate" - override val publicProperties: List[PublicProperty[_, _]] = properties.caseTemplate + override val entityName: String = "caseTemplate" + override val publicProperties: PublicProperties = properties.caseTemplate override val initialQuery: Query = Query .init[Traversal.V[CaseTemplate]]("listCaseTemplate", (graph, authContext) => organisationSrv.get(authContext.organisation)(graph).caseTemplates) diff --git a/thehive/app/org/thp/thehive/controllers/v1/DescribeCtrl.scala b/thehive/app/org/thp/thehive/controllers/v1/DescribeCtrl.scala index 25c45fd70b..1c5c2b6ec4 100644 --- a/thehive/app/org/thp/thehive/controllers/v1/DescribeCtrl.scala +++ b/thehive/app/org/thp/thehive/controllers/v1/DescribeCtrl.scala @@ -68,44 +68,49 @@ class DescribeCtrl @Inject() ( .instanceOf(getClass.getClassLoader.loadClass(s"$packageName.$className")) .asInstanceOf[QueryableCtrl] .publicProperties + .list .flatMap(propertyToJson(name, _)) ) ).toOption val entityDescriptions: Seq[EntityDescription] = Seq( - EntityDescription("case", caseCtrl.publicProperties.flatMap(propertyToJson("case", _))), - EntityDescription("case_task", taskCtrl.publicProperties.flatMap(propertyToJson("case_task", _))), - EntityDescription("alert", alertCtrl.publicProperties.flatMap(propertyToJson("alert", _))), - EntityDescription("case_artifact", observableCtrl.publicProperties.flatMap(propertyToJson("case_artifact", _))), - EntityDescription("user", userCtrl.publicProperties.flatMap(propertyToJson("user", _))), -// EntityDescription("case_task_log", logCtrl.publicProperties.flatMap(propertyToJson("case_task_log", _))), - EntityDescription("audit", auditCtrl.publicProperties.flatMap(propertyToJson("audit", _))) + EntityDescription("case", caseCtrl.publicProperties.list.flatMap(propertyToJson("case", _))), + EntityDescription("case_task", taskCtrl.publicProperties.list.flatMap(propertyToJson("case_task", _))), + EntityDescription("alert", alertCtrl.publicProperties.list.flatMap(propertyToJson("alert", _))), + EntityDescription("case_artifact", observableCtrl.publicProperties.list.flatMap(propertyToJson("case_artifact", _))), + EntityDescription("user", userCtrl.publicProperties.list.flatMap(propertyToJson("user", _))), +// EntityDescription("case_task_log", logCtrl.publicProperties.list.flatMap(propertyToJson("case_task_log", _))), + EntityDescription("audit", auditCtrl.publicProperties.list.flatMap(propertyToJson("audit", _))) ) ++ describeCortexEntity("case_artifact_job", "/connector/cortex/job", "JobCtrl") ++ describeCortexEntity("action", "/connector/cortex/action", "ActionCtrl") implicit val propertyDescriptionWrites: Writes[PropertyDescription] = Json.writes[PropertyDescription].transform((_: JsObject) + ("description" -> JsString(""))) - def customFields: Seq[PropertyDescription] = db.roTransaction { implicit graph => - customFieldSrv.startTraversal.toSeq.map(cf => PropertyDescription(s"customFields.${cf.name}", cf.`type`.toString)) - } + def customFields: Seq[PropertyDescription] = + db.roTransaction { implicit graph => + customFieldSrv.startTraversal.toSeq.map(cf => PropertyDescription(s"customFields.${cf.name}", cf.`type`.toString)) + } - def impactStatus: PropertyDescription = db.roTransaction { implicit graph => - PropertyDescription("impactStatus", "enumeration", impactStatusSrv.startTraversal.toSeq.map(s => JsString(s.value))) - } + def impactStatus: PropertyDescription = + db.roTransaction { implicit graph => + PropertyDescription("impactStatus", "enumeration", impactStatusSrv.startTraversal.toSeq.map(s => JsString(s.value))) + } - def resolutionStatus: PropertyDescription = db.roTransaction { implicit graph => - PropertyDescription("resolutionStatus", "enumeration", resolutionStatusSrv.startTraversal.toSeq.map(s => JsString(s.value))) - } + def resolutionStatus: PropertyDescription = + db.roTransaction { implicit graph => + PropertyDescription("resolutionStatus", "enumeration", resolutionStatusSrv.startTraversal.toSeq.map(s => JsString(s.value))) + } - def customDescription(model: String, propertyName: String): Option[Seq[PropertyDescription]] = (model, propertyName) match { - case (_, "assignee") => Some(Seq(PropertyDescription("assignee", "user"))) - case ("case", "status") => - Some( - Seq(PropertyDescription("status", "enumeration", Seq(JsString("Open"), JsString("Resolved"), JsString("Deleted"), JsString("Duplicated")))) - ) - case ("case", "impactStatus") => Some(Seq(impactStatus)) - case ("case", "resolutionStatus") => Some(Seq(resolutionStatus)) + def customDescription(model: String, propertyName: String): Option[Seq[PropertyDescription]] = + (model, propertyName) match { + case (_, "assignee") => Some(Seq(PropertyDescription("assignee", "user"))) + case ("case", "status") => + Some( + Seq(PropertyDescription("status", "enumeration", Seq(JsString("Open"), JsString("Resolved"), JsString("Deleted"), JsString("Duplicated")))) + ) + case ("case", "impactStatus") => Some(Seq(impactStatus)) + case ("case", "resolutionStatus") => Some(Seq(resolutionStatus)) // //case ("observable", "status") => // // Some(PropertyDescription("status", "enumeration", Seq(JsString("Ok")))) // //case ("observable", "dataType") => @@ -128,19 +133,28 @@ class DescribeCtrl @Inject() ( // ) // ) // ) - case (_, "tlp") => - Some(Seq(PropertyDescription("tlp", "number", Seq(JsNumber(0), JsNumber(1), JsNumber(2), JsNumber(3)), Seq("white", "green", "amber", "red")))) - case (_, "pap") => - Some(Seq(PropertyDescription("pap", "number", Seq(JsNumber(0), JsNumber(1), JsNumber(2), JsNumber(3)), Seq("white", "green", "amber", "red")))) - case (_, "severity") => - Some( - Seq( - PropertyDescription("severity", "number", Seq(JsNumber(1), JsNumber(2), JsNumber(3), JsNumber(4)), Seq("low", "medium", "high", "critical")) + case (_, "tlp") => + Some( + Seq(PropertyDescription("tlp", "number", Seq(JsNumber(0), JsNumber(1), JsNumber(2), JsNumber(3)), Seq("white", "green", "amber", "red"))) ) - ) - case (_, "_createdBy") => Some(Seq(PropertyDescription("_createdBy", "user"))) - case (_, "_updatedBy") => Some(Seq(PropertyDescription("_updatedBy", "user"))) - case (_, "customFields") => Some(customFields) + case (_, "pap") => + Some( + Seq(PropertyDescription("pap", "number", Seq(JsNumber(0), JsNumber(1), JsNumber(2), JsNumber(3)), Seq("white", "green", "amber", "red"))) + ) + case (_, "severity") => + Some( + Seq( + PropertyDescription( + "severity", + "number", + Seq(JsNumber(1), JsNumber(2), JsNumber(3), JsNumber(4)), + Seq("low", "medium", "high", "critical") + ) + ) + ) + case (_, "_createdBy") => Some(Seq(PropertyDescription("_createdBy", "user"))) + case (_, "_updatedBy") => Some(Seq(PropertyDescription("_updatedBy", "user"))) + case (_, "customFields") => Some(customFields) // case ("case_artifact_job" | "action", "status") => // Some( // Seq( @@ -151,8 +165,8 @@ class DescribeCtrl @Inject() ( // ) // ) // ) - case _ => None - } + case _ => None + } def propertyToJson(model: String, prop: PublicProperty[_, _]): Seq[PropertyDescription] = customDescription(model, prop.propertyName).getOrElse { diff --git a/thehive/app/org/thp/thehive/controllers/v1/LogCtrl.scala b/thehive/app/org/thp/thehive/controllers/v1/LogCtrl.scala index eee92d124b..ec3b4454fe 100644 --- a/thehive/app/org/thp/thehive/controllers/v1/LogCtrl.scala +++ b/thehive/app/org/thp/thehive/controllers/v1/LogCtrl.scala @@ -3,7 +3,7 @@ package org.thp.thehive.controllers.v1 import javax.inject.{Inject, Named, Singleton} import org.thp.scalligraph.controllers.{Entrypoint, FieldsParser} import org.thp.scalligraph.models.Database -import org.thp.scalligraph.query.{ParamQuery, PropertyUpdater, PublicProperty, Query} +import org.thp.scalligraph.query.{ParamQuery, PropertyUpdater, PublicProperties, Query} import org.thp.scalligraph.traversal.TraversalOps._ import org.thp.scalligraph.traversal.{IteratorOutput, Traversal} import org.thp.thehive.controllers.v1.Conversion._ @@ -27,9 +27,9 @@ class LogCtrl @Inject() ( organisationSrv: OrganisationSrv ) extends QueryableCtrl with LogRenderer { - lazy val logger: Logger = Logger(getClass) - override val entityName: String = "log" - override val publicProperties: List[PublicProperty[_, _]] = properties.log + lazy val logger: Logger = Logger(getClass) + override val entityName: String = "log" + override val publicProperties: PublicProperties = properties.log override val initialQuery: Query = Query.init[Traversal.V[Log]]("listLog", (graph, authContext) => organisationSrv.get(authContext.organisation)(graph).shares.tasks.logs) override val getQuery: ParamQuery[IdOrName] = Query.initWithParam[IdOrName, Traversal.V[Log]]( @@ -53,10 +53,11 @@ class LogCtrl @Inject() ( .authTransaction(db) { implicit request => implicit graph => val inputLog: InputLog = request.body("log") for { - task <- taskSrv - .getByIds(taskId) - .can(Permissions.manageTask) - .getOrFail("Task") + task <- + taskSrv + .getByIds(taskId) + .can(Permissions.manageTask) + .getOrFail("Task") createdLog <- logSrv.create(inputLog.toLog, task) attachment <- inputLog.attachment.map(logSrv.addAttachment(createdLog, _)).flip richLog = RichLog(createdLog, attachment.toList) diff --git a/thehive/app/org/thp/thehive/controllers/v1/ObservableCtrl.scala b/thehive/app/org/thp/thehive/controllers/v1/ObservableCtrl.scala index c224d99ec5..0c32f5b2ba 100644 --- a/thehive/app/org/thp/thehive/controllers/v1/ObservableCtrl.scala +++ b/thehive/app/org/thp/thehive/controllers/v1/ObservableCtrl.scala @@ -4,7 +4,7 @@ import javax.inject.{Inject, Named, Singleton} import org.thp.scalligraph._ import org.thp.scalligraph.controllers._ import org.thp.scalligraph.models.Database -import org.thp.scalligraph.query.{ParamQuery, PropertyUpdater, PublicProperty, Query} +import org.thp.scalligraph.query.{ParamQuery, PropertyUpdater, PublicProperties, Query} import org.thp.scalligraph.traversal.TraversalOps._ import org.thp.scalligraph.traversal.{IteratorOutput, Traversal} import org.thp.thehive.controllers.v1.Conversion._ @@ -30,9 +30,9 @@ class ObservableCtrl @Inject() ( ) extends QueryableCtrl with ObservableRenderer { - lazy val logger: Logger = Logger(getClass) - override val entityName: String = "observable" - override val publicProperties: List[PublicProperty[_, _]] = properties.observable + lazy val logger: Logger = Logger(getClass) + override val entityName: String = "observable" + override val publicProperties: PublicProperties = properties.observable override val initialQuery: Query = Query.init[Traversal.V[Observable]]( "listObservable", @@ -45,7 +45,8 @@ class ObservableCtrl @Inject() ( ) override val pageQuery: ParamQuery[OutputParam] = Query.withParam[OutputParam, Traversal.V[Observable], IteratorOutput]( "page", - FieldsParser[OutputParam], { + FieldsParser[OutputParam], + { case (OutputParam(from, to, extraData), observableSteps, authContext) => observableSteps.richPage(from, to, extraData.contains("total")) { _.richObservableWithCustomRenderer(observableStatsRenderer(extraData - "total")(authContext))(authContext) @@ -72,18 +73,21 @@ class ObservableCtrl @Inject() ( .authTransaction(db) { implicit request => implicit graph => val inputObservable: InputObservable = request.body("artifact") for { - case0 <- caseSrv - .get(caseId) - .can(Permissions.manageObservable) - .getOrFail("Case") + case0 <- + caseSrv + .get(caseId) + .can(Permissions.manageObservable) + .getOrFail("Case") observableType <- observableTypeSrv.getOrFail(inputObservable.dataType) - observablesWithData <- inputObservable - .data - .toTry(d => observableSrv.create(inputObservable.toObservable, observableType, d, inputObservable.tags, Nil)) - observableWithAttachment <- inputObservable - .attachment - .map(a => observableSrv.create(inputObservable.toObservable, observableType, a, inputObservable.tags, Nil)) - .flip + observablesWithData <- + inputObservable + .data + .toTry(d => observableSrv.create(inputObservable.toObservable, observableType, d, inputObservable.tags, Nil)) + observableWithAttachment <- + inputObservable + .attachment + .map(a => observableSrv.create(inputObservable.toObservable, observableType, a, inputObservable.tags, Nil)) + .flip createdObservables <- (observablesWithData ++ observableWithAttachment).toTry { richObservables => caseSrv .addObservable(case0, richObservables) @@ -137,10 +141,11 @@ class ObservableCtrl @Inject() ( entryPoint("delete") .authTransaction(db) { implicit request => implicit graph => for { - observable <- observableSrv - .getByIds(obsId) - .can(Permissions.manageObservable) - .getOrFail("Observable") + observable <- + observableSrv + .getByIds(obsId) + .can(Permissions.manageObservable) + .getOrFail("Observable") _ <- observableSrv.remove(observable) } yield Results.NoContent } diff --git a/thehive/app/org/thp/thehive/controllers/v1/OrganisationCtrl.scala b/thehive/app/org/thp/thehive/controllers/v1/OrganisationCtrl.scala index a227c96998..2b07398f95 100644 --- a/thehive/app/org/thp/thehive/controllers/v1/OrganisationCtrl.scala +++ b/thehive/app/org/thp/thehive/controllers/v1/OrganisationCtrl.scala @@ -3,7 +3,7 @@ package org.thp.thehive.controllers.v1 import javax.inject.{Inject, Named, Singleton} import org.thp.scalligraph.controllers.{Entrypoint, FieldsParser} import org.thp.scalligraph.models.Database -import org.thp.scalligraph.query.{ParamQuery, PropertyUpdater, PublicProperty, Query} +import org.thp.scalligraph.query.{ParamQuery, PropertyUpdater, PublicProperties, Query} import org.thp.scalligraph.traversal.TraversalOps._ import org.thp.scalligraph.traversal.{IteratorOutput, Traversal} import org.thp.thehive.controllers.v1.Conversion._ @@ -23,8 +23,8 @@ class OrganisationCtrl @Inject() ( @Named("with-thehive-schema") implicit val db: Database ) extends QueryableCtrl { - override val entityName: String = "organisation" - override val publicProperties: List[PublicProperty[_, _]] = properties.organisation + override val entityName: String = "organisation" + override val publicProperties: PublicProperties = properties.organisation override val initialQuery: Query = Query.init[Traversal.V[Organisation]]("listOrganisation", (graph, authContext) => organisationSrv.startTraversal(graph).visible(authContext)) override val pageQuery: ParamQuery[OutputParam] = Query.withParam[OutputParam, Traversal.V[Organisation], IteratorOutput]( diff --git a/thehive/app/org/thp/thehive/controllers/v1/ProfileCtrl.scala b/thehive/app/org/thp/thehive/controllers/v1/ProfileCtrl.scala index f169854c83..2be85dd50b 100644 --- a/thehive/app/org/thp/thehive/controllers/v1/ProfileCtrl.scala +++ b/thehive/app/org/thp/thehive/controllers/v1/ProfileCtrl.scala @@ -4,7 +4,7 @@ import javax.inject.{Inject, Named, Singleton} import org.thp.scalligraph.AuthorizationError import org.thp.scalligraph.controllers.{Entrypoint, FieldsParser} import org.thp.scalligraph.models.{Database, Entity} -import org.thp.scalligraph.query.{ParamQuery, PropertyUpdater, PublicProperty, Query} +import org.thp.scalligraph.query.{ParamQuery, PropertyUpdater, PublicProperties, Query} import org.thp.scalligraph.traversal.TraversalOps._ import org.thp.scalligraph.traversal.{IteratorOutput, Traversal} import org.thp.thehive.controllers.v0.Conversion._ @@ -29,8 +29,8 @@ class ProfileCtrl @Inject() ( FieldsParser[IdOrName], (param, graph, _) => profileSrv.get(param.idOrName)(graph) ) - val entityName: String = "profile" - val publicProperties: List[PublicProperty[_, _]] = properties.profile + val entityName: String = "profile" + val publicProperties: PublicProperties = properties.profile val initialQuery: Query = Query.init[Traversal.V[Profile]]("listProfile", (graph, _) => profileSrv.startTraversal(graph)) @@ -47,9 +47,9 @@ class ProfileCtrl @Inject() ( .extract("profile", FieldsParser[InputProfile]) .authTransaction(db) { implicit request => implicit graph => val profile: InputProfile = request.body("profile") - if (request.isPermitted(Permissions.manageProfile)) { + if (request.isPermitted(Permissions.manageProfile)) profileSrv.create(profile.toProfile).map(createdProfile => Results.Created(createdProfile.toJson)) - } else + else Failure(AuthorizationError("You don't have permission to create profiles")) } @@ -68,12 +68,12 @@ class ProfileCtrl @Inject() ( .extract("profile", FieldsParser.update("profile", properties.profile)) .authTransaction(db) { implicit request => implicit graph => val propertyUpdaters: Seq[PropertyUpdater] = request.body("profile") - if (request.isPermitted(Permissions.manageProfile)) { + if (request.isPermitted(Permissions.manageProfile)) profileSrv .update(_.get(profileId), propertyUpdaters) .flatMap { case (profileSteps, _) => profileSteps.getOrFail("Profile") } .map(profile => Results.Ok(profile.toJson)) - } else + else Failure(AuthorizationError("You don't have permission to update profiles")) } diff --git a/thehive/app/org/thp/thehive/controllers/v1/Properties.scala b/thehive/app/org/thp/thehive/controllers/v1/Properties.scala index 515106b843..fb7652d839 100644 --- a/thehive/app/org/thp/thehive/controllers/v1/Properties.scala +++ b/thehive/app/org/thp/thehive/controllers/v1/Properties.scala @@ -4,7 +4,7 @@ import javax.inject.{Inject, Singleton} import org.thp.scalligraph.BadRequestError import org.thp.scalligraph.controllers.FPathElem import org.thp.scalligraph.models.{IdMapping, UMapping} -import org.thp.scalligraph.query.{PublicProperty, PublicPropertyListBuilder} +import org.thp.scalligraph.query.{PublicProperties, PublicPropertyListBuilder} import org.thp.scalligraph.traversal.TraversalOps._ import org.thp.thehive.models._ import org.thp.thehive.services.AlertOps._ @@ -31,7 +31,7 @@ class Properties @Inject() ( observableSrv: ObservableSrv ) { - lazy val alert: List[PublicProperty[_, _]] = + lazy val alert: PublicProperties = PublicPropertyListBuilder[Alert] .property("type", UMapping.string)(_.field.updatable) .property("source", UMapping.string)(_.field.updatable) @@ -73,7 +73,7 @@ class Properties @Inject() ( }) .build - lazy val audit: List[PublicProperty[_, _]] = + lazy val audit: PublicProperties = PublicPropertyListBuilder[Audit] .property("operation", UMapping.string)(_.rename("action").readonly) .property("details", UMapping.string)(_.field.readonly) @@ -85,7 +85,7 @@ class Properties @Inject() ( .property("rootId", IdMapping)(_.select(_.context._id).readonly) .build - lazy val `case`: List[PublicProperty[_, _]] = + lazy val `case`: PublicProperties = PublicPropertyListBuilder[Case] .property("title", UMapping.string)(_.field.updatable) .property("description", UMapping.string)(_.field.updatable) @@ -139,7 +139,7 @@ class Properties @Inject() ( }) .build - lazy val caseTemplate: List[PublicProperty[_, _]] = + lazy val caseTemplate: PublicProperties = PublicPropertyListBuilder[CaseTemplate] .property("name", UMapping.string)(_.field.updatable) .property("displayName", UMapping.string)(_.field.updatable) @@ -174,19 +174,19 @@ class Properties @Inject() ( }) .build - lazy val organisation: List[PublicProperty[_, _]] = + lazy val organisation: PublicProperties = PublicPropertyListBuilder[Organisation] .property("name", UMapping.string)(_.field.updatable) .property("description", UMapping.string)(_.field.updatable) .build - lazy val profile: List[PublicProperty[_, _]] = + lazy val profile: PublicProperties = PublicPropertyListBuilder[Profile] .property("name", UMapping.string)(_.field.updatable) .property("permissions", UMapping.string.set)(_.field.updatable) .build - lazy val task: List[PublicProperty[_, _]] = + lazy val task: PublicProperties = PublicPropertyListBuilder[Task] .property("title", UMapping.string)(_.field.updatable) .property("description", UMapping.string.optional)(_.field.updatable) @@ -213,7 +213,7 @@ class Properties @Inject() ( }) .build - lazy val log: List[PublicProperty[_, _]] = + lazy val log: PublicProperties = PublicPropertyListBuilder[Log] .property("message", UMapping.string)(_.field.updatable) .property("deleted", UMapping.boolean)(_.field.updatable) @@ -221,7 +221,7 @@ class Properties @Inject() ( .property("attachment", IdMapping)(_.select(_.attachments._id).readonly) .build - lazy val user: List[PublicProperty[_, _]] = + lazy val user: PublicProperties = PublicPropertyListBuilder[User] .property("login", UMapping.string)(_.field.readonly) .property("name", UMapping.string)(_.field.readonly) @@ -229,7 +229,7 @@ class Properties @Inject() ( .property("avatar", UMapping.string.optional)(_.select(_.avatar.value(_.attachmentId).domainMap(id => s"/api/datastore/$id")).readonly) .build - lazy val observable: List[PublicProperty[_, _]] = + lazy val observable: PublicProperties = PublicPropertyListBuilder[Observable] .property("status", UMapping.string)(_.select(_.constant("Ok")).readonly) .property("startDate", UMapping.date)(_.select(_._createdAt).readonly) diff --git a/thehive/app/org/thp/thehive/controllers/v1/QueryCtrl.scala b/thehive/app/org/thp/thehive/controllers/v1/QueryCtrl.scala index fa00086918..5af9b32670 100644 --- a/thehive/app/org/thp/thehive/controllers/v1/QueryCtrl.scala +++ b/thehive/app/org/thp/thehive/controllers/v1/QueryCtrl.scala @@ -1,12 +1,12 @@ package org.thp.thehive.controllers.v1 -import org.thp.scalligraph.query.{ParamQuery, PublicProperty, Query} +import org.thp.scalligraph.query.{ParamQuery, PublicProperties, Query} case class IdOrName(idOrName: String) trait QueryableCtrl { val entityName: String - val publicProperties: List[PublicProperty[_, _]] + val publicProperties: PublicProperties val initialQuery: Query val pageQuery: ParamQuery[OutputParam] val outputQuery: Query diff --git a/thehive/app/org/thp/thehive/controllers/v1/TaskCtrl.scala b/thehive/app/org/thp/thehive/controllers/v1/TaskCtrl.scala index 1ebed30d9b..32470eb1d1 100644 --- a/thehive/app/org/thp/thehive/controllers/v1/TaskCtrl.scala +++ b/thehive/app/org/thp/thehive/controllers/v1/TaskCtrl.scala @@ -3,7 +3,7 @@ package org.thp.thehive.controllers.v1 import javax.inject.{Inject, Named, Singleton} import org.thp.scalligraph.controllers.{Entrypoint, FieldsParser} import org.thp.scalligraph.models.Database -import org.thp.scalligraph.query.{ParamQuery, PropertyUpdater, PublicProperty, Query} +import org.thp.scalligraph.query.{ParamQuery, PropertyUpdater, PublicProperties, Query} import org.thp.scalligraph.traversal.TraversalOps._ import org.thp.scalligraph.traversal.{IteratorOutput, Traversal} import org.thp.thehive.controllers.v1.Conversion._ @@ -29,8 +29,8 @@ class TaskCtrl @Inject() ( ) extends QueryableCtrl with TaskRenderer { - override val entityName: String = "task" - override val publicProperties: List[PublicProperty[_, _]] = properties.task + override val entityName: String = "task" + override val publicProperties: PublicProperties = properties.task override val initialQuery: Query = Query.init[Traversal.V[Task]]("listTask", (graph, authContext) => organisationSrv.get(authContext.organisation)(graph).shares.tasks) override val pageQuery: ParamQuery[OutputParam] = Query.withParam[OutputParam, Traversal.V[Task], IteratorOutput]( diff --git a/thehive/app/org/thp/thehive/controllers/v1/TaskRenderer.scala b/thehive/app/org/thp/thehive/controllers/v1/TaskRenderer.scala index a4a6232337..d8ed893823 100644 --- a/thehive/app/org/thp/thehive/controllers/v1/TaskRenderer.scala +++ b/thehive/app/org/thp/thehive/controllers/v1/TaskRenderer.scala @@ -1,7 +1,7 @@ package org.thp.thehive.controllers.v1 -import java.util.{List => JList, Map => JMap} import java.lang.{Long => JLong} +import java.util.{List => JList, Map => JMap} import org.apache.tinkerpop.gremlin.structure.Vertex import org.thp.scalligraph.auth.AuthContext @@ -12,7 +12,7 @@ import org.thp.thehive.models.Task import org.thp.thehive.services.CaseOps._ import org.thp.thehive.services.CaseTemplateOps._ import org.thp.thehive.services.TaskOps._ -import play.api.libs.json.{JsBoolean, JsNull, JsNumber, JsObject, JsString, JsValue} +import play.api.libs.json._ trait TaskRenderer { diff --git a/thehive/app/org/thp/thehive/controllers/v1/TheHiveQueryExecutor.scala b/thehive/app/org/thp/thehive/controllers/v1/TheHiveQueryExecutor.scala index 43e621a510..1f4fd9ae4e 100644 --- a/thehive/app/org/thp/thehive/controllers/v1/TheHiveQueryExecutor.scala +++ b/thehive/app/org/thp/thehive/controllers/v1/TheHiveQueryExecutor.scala @@ -37,7 +37,7 @@ class TheHiveQueryExecutor @Inject() ( caseCtrl :: taskCtrl :: alertCtrl :: userCtrl :: caseTemplateCtrl :: organisationCtrl :: auditCtrl :: observableCtrl :: logCtrl :: Nil override val version: (Int, Int) = 1 -> 1 - def metaProperties: List[PublicProperty[_, _]] = + def metaProperties: PublicProperties = PublicPropertyListBuilder[Product] .property("_createdBy", UMapping.string)(_.field.readonly) .property("_createdAt", UMapping.date)(_.field.readonly) @@ -45,7 +45,7 @@ class TheHiveQueryExecutor @Inject() ( .property("_updatedAt", UMapping.date.optional)(_.field.readonly) .build - override lazy val publicProperties: List[PublicProperty[_, _]] = controllers.flatMap(_.publicProperties) ::: metaProperties + override lazy val publicProperties: PublicProperties = controllers.foldLeft(metaProperties)(_ ++ _.publicProperties) override lazy val queries: Seq[ParamQuery[_]] = controllers.map(_.initialQuery) ::: diff --git a/thehive/app/org/thp/thehive/controllers/v1/UserCtrl.scala b/thehive/app/org/thp/thehive/controllers/v1/UserCtrl.scala index ddaef19e66..68c6f48dc0 100644 --- a/thehive/app/org/thp/thehive/controllers/v1/UserCtrl.scala +++ b/thehive/app/org/thp/thehive/controllers/v1/UserCtrl.scala @@ -6,7 +6,7 @@ import javax.inject.{Inject, Named, Singleton} import org.thp.scalligraph.auth.AuthSrv import org.thp.scalligraph.controllers.{Entrypoint, FieldsParser} import org.thp.scalligraph.models.Database -import org.thp.scalligraph.query.{ParamQuery, PublicProperty, Query} +import org.thp.scalligraph.query.{ParamQuery, PublicProperties, Query} import org.thp.scalligraph.traversal.TraversalOps._ import org.thp.scalligraph.traversal.{IteratorOutput, Traversal} import org.thp.scalligraph.{AuthorizationError, BadRequestError, NotFoundError, RichOptionTry} @@ -37,8 +37,8 @@ class UserCtrl @Inject() ( @Named("with-thehive-schema") implicit val db: Database ) extends QueryableCtrl { - override val entityName: String = "user" - override val publicProperties: List[PublicProperty[_, _]] = properties.user + override val entityName: String = "user" + override val publicProperties: PublicProperties = properties.user override val initialQuery: Query = Query.init[Traversal.V[User]]("listUser", (graph, authContext) => organisationSrv.get(authContext.organisation)(graph).users) diff --git a/thehive/app/org/thp/thehive/services/StreamSrv.scala b/thehive/app/org/thp/thehive/services/StreamSrv.scala index 10a06443bb..ba87268f76 100644 --- a/thehive/app/org/thp/thehive/services/StreamSrv.scala +++ b/thehive/app/org/thp/thehive/services/StreamSrv.scala @@ -76,9 +76,8 @@ class StreamActor( .toSeq .map(_._id) logger.debug(s"[$self] AuditStreamMessage $ids => $visibleIds") - if (visibleIds.nonEmpty) { + if (visibleIds.nonEmpty) context.become(receive(messages ++ visibleIds, keepAliveTimer)) - } } } @@ -125,9 +124,8 @@ class StreamActor( commitTimer.cancel() val newCommitTimer = context.system.scheduler.scheduleOnce(maxWait, self, Commit) context.become(receive(messages ++ visibleIds, requestActor, keepAliveTimer, newCommitTimer, Some(newGraceTimer))) - } else { + } else context.become(receive(messages ++ visibleIds, requestActor, keepAliveTimer, commitTimer, Some(newGraceTimer))) - } } } } diff --git a/thehive/app/org/thp/thehive/services/th3/Aggregation.scala b/thehive/app/org/thp/thehive/services/th3/Aggregation.scala index 8014d405d8..9842199ed2 100644 --- a/thehive/app/org/thp/thehive/services/th3/Aggregation.scala +++ b/thehive/app/org/thp/thehive/services/th3/Aggregation.scala @@ -7,13 +7,13 @@ import java.util.{Calendar, Date, List => JList} import org.apache.tinkerpop.gremlin.process.traversal.Order import org.scalactic.Accumulation._ import org.scalactic._ -import org.thp.scalligraph.InvalidFormatAttributeError import org.thp.scalligraph.auth.AuthContext import org.thp.scalligraph.controllers._ import org.thp.scalligraph.models.Database -import org.thp.scalligraph.query.{Aggregation, PublicProperty} +import org.thp.scalligraph.query.{Aggregation, PublicProperties} import org.thp.scalligraph.traversal.TraversalOps._ import org.thp.scalligraph.traversal._ +import org.thp.scalligraph.{BadRequestError, InvalidFormatAttributeError} import play.api.Logger import play.api.libs.json.{JsNull, JsNumber, JsObject, Json} @@ -149,16 +149,19 @@ object TH3Aggregation { case class AggSum(aggName: Option[String], fieldName: String) extends Aggregation(aggName.getOrElse(s"sum_$fieldName")) { override def getTraversal( db: Database, - publicProperties: List[PublicProperty[_, _]], + publicProperties: PublicProperties, traversalType: ru.Type, traversal: Traversal.Unk, authContext: AuthContext ): Traversal.Domain[Output[_]] = { - val property = PublicProperty.getProperty(publicProperties, traversalType, fieldName) + val fieldPath = FPath(fieldName) + val property = publicProperties + .get[Traversal.UnkD, Traversal.UnkDU](fieldPath, traversalType) + .getOrElse(throw BadRequestError(s"Property $fieldName for type $traversalType not found")) traversal.coalesce( t => property - .select(FPath(fieldName), t) + .select(fieldPath, t) .sum .domainMap(sum => Output(Json.obj(name -> JsNumber(BigDecimal(sum.toString))))) .castDomain[Output[_]], @@ -169,16 +172,19 @@ case class AggSum(aggName: Option[String], fieldName: String) extends Aggregatio case class AggAvg(aggName: Option[String], fieldName: String) extends Aggregation(aggName.getOrElse(s"sum_$fieldName")) { override def getTraversal( db: Database, - publicProperties: List[PublicProperty[_, _]], + publicProperties: PublicProperties, traversalType: ru.Type, traversal: Traversal.Unk, authContext: AuthContext ): Traversal.Domain[Output[_]] = { - val property = PublicProperty.getProperty(publicProperties, traversalType, fieldName) + val fieldPath = FPath(fieldName) + val property = publicProperties + .get[Traversal.UnkD, Traversal.UnkDU](fieldPath, traversalType) + .getOrElse(throw BadRequestError(s"Property $fieldName for type $traversalType not found")) traversal.coalesce( t => property - .select(FPath(fieldName), t) + .select(fieldPath, t) .mean .domainMap(avg => Output(Json.obj(name -> avg.asInstanceOf[Double]))), _.constant2(Output(Json.obj(name -> JsNull))) @@ -189,16 +195,19 @@ case class AggAvg(aggName: Option[String], fieldName: String) extends Aggregatio case class AggMin(aggName: Option[String], fieldName: String) extends Aggregation(aggName.getOrElse(s"min_$fieldName")) { override def getTraversal( db: Database, - publicProperties: List[PublicProperty[_, _]], + publicProperties: PublicProperties, traversalType: ru.Type, traversal: Traversal.Unk, authContext: AuthContext ): Traversal.Domain[Output[_]] = { - val property = PublicProperty.getProperty(publicProperties, traversalType, fieldName) + val fieldPath = FPath(fieldName) + val property = publicProperties + .get[Traversal.UnkD, Traversal.UnkDU](fieldPath, traversalType) + .getOrElse(throw BadRequestError(s"Property $fieldName for type $traversalType not found")) traversal.coalesce( t => property - .select(FPath(fieldName), t) + .select(fieldPath, t) .min .domainMap(min => Output(Json.obj(name -> property.mapping.selectRenderer.toJson(min)))), _.constant2(Output(Json.obj(name -> JsNull))) @@ -209,16 +218,19 @@ case class AggMin(aggName: Option[String], fieldName: String) extends Aggregatio case class AggMax(aggName: Option[String], fieldName: String) extends Aggregation(aggName.getOrElse(s"max_$fieldName")) { override def getTraversal( db: Database, - publicProperties: List[PublicProperty[_, _]], + publicProperties: PublicProperties, traversalType: ru.Type, traversal: Traversal.Unk, authContext: AuthContext ): Traversal.Domain[Output[_]] = { - val property = PublicProperty.getProperty(publicProperties, traversalType, fieldName) + val fieldPath = FPath(fieldName) + val property = publicProperties + .get[Traversal.UnkD, Traversal.UnkDU](fieldPath, traversalType) + .getOrElse(throw BadRequestError(s"Property $fieldName for type $traversalType not found")) traversal.coalesce( t => property - .select(FPath(fieldName), t) + .select(fieldPath, t) .max .domainMap(max => Output(Json.obj(name -> property.mapping.selectRenderer.toJson(max)))), _.constant2(Output(Json.obj(name -> JsNull))) @@ -229,7 +241,7 @@ case class AggMax(aggName: Option[String], fieldName: String) extends Aggregatio case class AggCount(aggName: Option[String]) extends Aggregation(aggName.getOrElse("count")) { override def getTraversal( db: Database, - publicProperties: List[PublicProperty[_, _]], + publicProperties: PublicProperties, traversalType: ru.Type, traversal: Traversal.Unk, authContext: AuthContext @@ -253,15 +265,17 @@ case class FieldAggregation( override def getTraversal( db: Database, - publicProperties: List[PublicProperty[_, _]], + publicProperties: PublicProperties, traversalType: ru.Type, traversal: Traversal.Unk, authContext: AuthContext ): Traversal.Domain[Output[_]] = { - val label = StepLabel[Traversal.UnkD, Traversal.UnkG, Converter[Traversal.UnkD, Traversal.UnkG]] - val property = PublicProperty.getProperty(publicProperties, traversalType, fieldName) - val groupedVertices = property.select(FPath(fieldName), traversal.as(label)).group(_.by, _.by(_.select(label).fold)).unfold - // val groupedVertices = traversal.group(_.by(t => property.select(FPath(fieldName), t).cast[Any, Any])).unfold + val label = StepLabel[Traversal.UnkD, Traversal.UnkG, Converter[Traversal.UnkD, Traversal.UnkG]] + val fieldPath = FPath(fieldName) + val property = publicProperties + .get[Traversal.UnkD, Traversal.UnkDU](fieldPath, traversalType) + .getOrElse(throw BadRequestError(s"Property $fieldName for type $traversalType not found")) + val groupedVertices = property.select(fieldPath, traversal.as(label)).group(_.by, _.by(_.select(label).fold)).unfold val sortedAndGroupedVertex = orders .map { case order if order.headOption.contains('-') => order.tail -> Order.desc @@ -352,15 +366,18 @@ case class TimeAggregation( override def getTraversal( db: Database, - publicProperties: List[PublicProperty[_, _]], + publicProperties: PublicProperties, traversalType: ru.Type, traversal: Traversal.Unk, authContext: AuthContext ): Traversal.Domain[Output[_]] = { - val property = PublicProperty.getProperty(publicProperties, traversalType, fieldName) - val label = StepLabel[Traversal.UnkD, Traversal.UnkG, Converter[Traversal.UnkD, Traversal.UnkG]] + val fieldPath = FPath(fieldName) + val property = publicProperties + .get[Traversal.UnkD, Traversal.UnkDU](fieldPath, traversalType) + .getOrElse(throw BadRequestError(s"Property $fieldName for type $traversalType not found")) + val label = StepLabel[Traversal.UnkD, Traversal.UnkG, Converter[Traversal.UnkD, Traversal.UnkG]] val groupedVertex = property - .select(FPath(fieldName), traversal.as(label)) + .select(fieldPath, traversal.as(label)) .cast[Date, Date] .graphMap[Long, JLong, Converter[Long, JLong]](dateToKey, Converter.long) .group(_.by, _.by(_.select(label).fold)) diff --git a/thehive/test/org/thp/thehive/TestAppBuilder.scala b/thehive/test/org/thp/thehive/TestAppBuilder.scala index 3c221efc07..a870878ad1 100644 --- a/thehive/test/org/thp/thehive/TestAppBuilder.scala +++ b/thehive/test/org/thp/thehive/TestAppBuilder.scala @@ -9,8 +9,10 @@ import javax.inject.{Inject, Provider, Singleton} import org.apache.commons.io.FileUtils import org.thp.scalligraph.auth._ import org.thp.scalligraph.models.{Database, Schema} +import org.thp.scalligraph.query.QueryExecutor import org.thp.scalligraph.services.{GenIntegrityCheckOps, LocalFileSystemStorageSrv, StorageSrv} import org.thp.scalligraph.{janus, AppBuilder} +import org.thp.thehive.controllers.v0.TheHiveQueryExecutor import org.thp.thehive.models.TheHiveSchemaDefinition import org.thp.thehive.services.notification.notifiers.{AppendToFileProvider, EmailerProvider, NotifierProvider} import org.thp.thehive.services.notification.triggers._ @@ -29,6 +31,7 @@ trait TestAppBuilder { .bind[UserSrv, LocalUserSrv] .bind[StorageSrv, LocalFileSystemStorageSrv] .bind[Schema, TheHiveSchemaDefinition] + .bind[QueryExecutor, TheHiveQueryExecutor] .multiBind[AuthSrvProvider](classOf[LocalPasswordAuthProvider], classOf[LocalKeyAuthProvider], classOf[HeaderAuthProvider]) .multiBind[NotifierProvider](classOf[AppendToFileProvider]) .multiBind[NotifierProvider](classOf[EmailerProvider]) diff --git a/thehive/test/org/thp/thehive/controllers/v0/CaseCtrlTest.scala b/thehive/test/org/thp/thehive/controllers/v0/CaseCtrlTest.scala index 3a550056d3..d5b4a91a86 100644 --- a/thehive/test/org/thp/thehive/controllers/v0/CaseCtrlTest.scala +++ b/thehive/test/org/thp/thehive/controllers/v0/CaseCtrlTest.scala @@ -146,7 +146,7 @@ class CaseCtrlTest extends PlaySpecification with TestAppBuilder { ) val requestList = FakeRequest("GET", "/api/case/task").withHeaders("user" -> "certuser@thehive.local") - val resultList = app[TheHiveQueryExecutor].task.search(requestList) + val resultList = app[TaskCtrl].search(requestList) status(resultList) must equalTo(200).updateMessage(s => s"$s\n${contentAsString(resultList)}") val tasksList = contentAsJson(resultList)(defaultAwaitTimeout, app[Materializer]).as[Seq[OutputTask]] @@ -249,7 +249,7 @@ class CaseCtrlTest extends PlaySpecification with TestAppBuilder { .withJsonBody( Json.parse("""{"query":{"severity":2}}""") ) - val result = app[TheHiveQueryExecutor].`case`.search()(request) + val result = app[CaseCtrl].search()(request) status(result) must equalTo(200).updateMessage(s => s"$s\n${contentAsString(result)}") header("X-Total", result) must beSome("2") val resultCases = contentAsJson(result)(defaultAwaitTimeout, app[Materializer]).as[Seq[OutputCase]] @@ -294,7 +294,7 @@ class CaseCtrlTest extends PlaySpecification with TestAppBuilder { .withJsonBody( Json.parse("""{"query":{"_and":[{"_field":"customFields.boolean1","_value":true},{"_not":{"status":"Deleted"}}]}}""") ) - val resultSearch = app[TheHiveQueryExecutor].`case`.search()(requestSearch) + val resultSearch = app[CaseCtrl].search()(requestSearch) status(resultSearch) must equalTo(200).updateMessage(s => s"$s\n${contentAsString(resultSearch)}") contentAsJson(resultSearch)(defaultAwaitTimeout, app[Materializer]).as[List[OutputCase]] must not(beEmpty) } @@ -322,7 +322,7 @@ class CaseCtrlTest extends PlaySpecification with TestAppBuilder { ] }""") ) - val result = app[TheHiveQueryExecutor].`case`.stats()(request) + val result = app[CaseCtrl].stats()(request) status(result) must_=== 200 val resultCase = contentAsJson(result) diff --git a/thehive/test/org/thp/thehive/controllers/v0/ObservableCtrlTest.scala b/thehive/test/org/thp/thehive/controllers/v0/ObservableCtrlTest.scala index da4928d437..9e09bce672 100644 --- a/thehive/test/org/thp/thehive/controllers/v0/ObservableCtrlTest.scala +++ b/thehive/test/org/thp/thehive/controllers/v0/ObservableCtrlTest.scala @@ -129,7 +129,7 @@ class ObservableCtrlTest extends PlaySpecification with TestAppBuilder { } } """.stripMargin)) - val resultSearch = app[TheHiveQueryExecutor].observable.search(requestSearch) + val resultSearch = app[ObservableCtrl].search(requestSearch) status(resultSearch) should equalTo(200).updateMessage(s => s"$s\n${contentAsString(resultSearch)}") diff --git a/thehive/test/org/thp/thehive/controllers/v0/QueryTest.scala b/thehive/test/org/thp/thehive/controllers/v0/QueryTest.scala index 5f01dd9117..b75d8b1e4a 100644 --- a/thehive/test/org/thp/thehive/controllers/v0/QueryTest.scala +++ b/thehive/test/org/thp/thehive/controllers/v0/QueryTest.scala @@ -3,44 +3,35 @@ package org.thp.thehive.controllers.v0 import org.specs2.mock.Mockito import org.thp.scalligraph.controllers.{Entrypoint, Field} import org.thp.scalligraph.models.Database -import org.thp.scalligraph.query.{ParamQuery, PublicProperty, QueryExecutor} +import org.thp.scalligraph.query.{ParamQuery, PublicProperties, QueryExecutor} import org.thp.thehive.services._ import play.api.libs.json.Json import play.api.test.PlaySpecification class QueryTest extends PlaySpecification with Mockito { - val properties = new Properties( - mock[CaseSrv], - mock[UserSrv], - mock[AlertSrv], - mock[DashboardSrv], - mock[ObservableSrv], - mock[CaseTemplateSrv], - mock[TaskSrv], - mock[CustomFieldSrv], - mock[Database] - ) + val publicTask = new PublicTask(mock[TaskSrv], mock[OrganisationSrv], mock[UserSrv]) + + val queryExecutor: QueryExecutor = new QueryExecutor { + override val db: Database = mock[Database] + override val version: (Int, Int) = 0 -> 0 + override lazy val queries: Seq[ParamQuery[_]] = + publicTask.initialQuery +: publicTask.getQuery +: publicTask.outputQuery +: publicTask.outputQuery +: publicTask.extraQueries + override lazy val publicProperties: PublicProperties = publicTask.publicProperties + } val taskCtrl = new TaskCtrl( mock[Entrypoint], mock[Database], - properties, mock[TaskSrv], mock[CaseSrv], mock[UserSrv], mock[OrganisationSrv], - mock[ShareSrv] + mock[ShareSrv], + queryExecutor, + publicTask ) - val queryExecutor: QueryExecutor = new QueryExecutor { - override val db: Database = mock[Database] - override val version: (Int, Int) = 0 -> 0 - override lazy val queries: Seq[ParamQuery[_]] = Seq(taskCtrl.initialQuery, taskCtrl.pageQuery, taskCtrl.outputQuery) - override lazy val publicProperties: List[PublicProperty[_, _]] = taskCtrl.publicProperties - } - val queryCtrl: QueryCtrl = new QueryCtrlBuilder(mock[Entrypoint], mock[Database]).apply(taskCtrl, queryExecutor) - "Controller" should { "parse stats query" in { val input = Json.parse(""" @@ -65,7 +56,7 @@ class QueryTest extends PlaySpecification with Mockito { | } """.stripMargin) - val queryOrError = queryCtrl.statsParser(Field(input)) + val queryOrError = taskCtrl.statsParser(Field(input)) queryOrError.isGood must beTrue.updateMessage(s => s"$s\n$queryOrError") queryOrError.get must not be empty } diff --git a/thehive/test/org/thp/thehive/controllers/v0/TagCtrlTest.scala b/thehive/test/org/thp/thehive/controllers/v0/TagCtrlTest.scala index 3874cdfbde..5c9eee3a40 100644 --- a/thehive/test/org/thp/thehive/controllers/v0/TagCtrlTest.scala +++ b/thehive/test/org/thp/thehive/controllers/v0/TagCtrlTest.scala @@ -162,7 +162,7 @@ class TagCtrlTest extends PlaySpecification with TestAppBuilder { val request = FakeRequest("POST", s"/api/tag/_search") .withHeaders("user" -> "certuser@thehive.local") .withJsonBody(json) - val result = app[TheHiveQueryExecutor].tag.search(request) + val result = app[TagCtrl].search(request) status(result) must equalTo(200).updateMessage(s => s"$s\n${contentAsString(result)}") diff --git a/thehive/test/org/thp/thehive/controllers/v0/TaskCtrlTest.scala b/thehive/test/org/thp/thehive/controllers/v0/TaskCtrlTest.scala index 6b640e2d22..2551beffa4 100644 --- a/thehive/test/org/thp/thehive/controllers/v0/TaskCtrlTest.scala +++ b/thehive/test/org/thp/thehive/controllers/v0/TaskCtrlTest.scala @@ -209,7 +209,7 @@ class TaskCtrlTest extends PlaySpecification with TestAppBuilder { }""".stripMargin ) ) - val result = app[Database].roTransaction(_ => app[TheHiveQueryExecutor].task.stats(request)) + val result = app[Database].roTransaction(_ => app[TaskCtrl].stats(request)) status(result) must equalTo(200) diff --git a/thehive/test/org/thp/thehive/controllers/v0/UserCtrlTest.scala b/thehive/test/org/thp/thehive/controllers/v0/UserCtrlTest.scala index 104d0e44c6..a9dc3a8c2d 100644 --- a/thehive/test/org/thp/thehive/controllers/v0/UserCtrlTest.scala +++ b/thehive/test/org/thp/thehive/controllers/v0/UserCtrlTest.scala @@ -29,7 +29,7 @@ class UserCtrlTest extends PlaySpecification with TestAppBuilder { ) .withHeaders("user" -> "socadmin@thehive.local") - val result = app[TheHiveQueryExecutor].user.search(request) + val result = app[UserCtrl].search(request) status(result) must_=== 200 val resultUsers = contentAsJson(result)(defaultAwaitTimeout, app[Materializer])