From 677018797d532c8c3f17175ee5ddfac3eaa80bb1 Mon Sep 17 00:00:00 2001 From: To-om Date: Fri, 26 Jun 2020 14:57:28 +0200 Subject: [PATCH] #1410 Add describe API --- .../thehive/controllers/v1/DescribeCtrl.scala | 177 ++++++++++++++++++ .../thp/thehive/controllers/v1/Router.scala | 6 +- 2 files changed, 182 insertions(+), 1 deletion(-) create mode 100644 thehive/app/org/thp/thehive/controllers/v1/DescribeCtrl.scala diff --git a/thehive/app/org/thp/thehive/controllers/v1/DescribeCtrl.scala b/thehive/app/org/thp/thehive/controllers/v1/DescribeCtrl.scala new file mode 100644 index 0000000000..b5be68bb0c --- /dev/null +++ b/thehive/app/org/thp/thehive/controllers/v1/DescribeCtrl.scala @@ -0,0 +1,177 @@ +package org.thp.thehive.controllers.v1 + +import java.lang.{Boolean => JBoolean} +import java.util.Date + +import javax.inject.{Inject, Named, Singleton} +import org.thp.scalligraph.NotFoundError +import org.thp.scalligraph.controllers.Entrypoint +import org.thp.scalligraph.models.Database +import org.thp.scalligraph.query.PublicProperty +import org.thp.scalligraph.services.config.ApplicationConfig.durationFormat +import org.thp.scalligraph.services.config.{ApplicationConfig, ConfigItem} +import org.thp.scalligraph.steps.StepsOps._ +import org.thp.scalligraph.utils.Hash +import org.thp.thehive.services.CustomFieldSrv +import play.api.Logger +import play.api.cache.SyncCacheApi +import play.api.inject.Injector +import play.api.libs.json._ +import play.api.mvc.{Action, AnyContent, Results} + +import scala.concurrent.duration.Duration +import scala.util.{Failure, Success, Try} + +@Singleton +class DescribeCtrl @Inject() ( + cacheApi: SyncCacheApi, + entrypoint: Entrypoint, + caseCtrl: CaseCtrl, + taskCtrl: TaskCtrl, + alertCtrl: AlertCtrl, + observableCtrl: ObservableCtrl, + userCtrl: UserCtrl, +// logCtrl: LogCtrl, + auditCtrl: AuditCtrl, + customFieldSrv: CustomFieldSrv, + injector: Injector, + @Named("with-thehive-schema") db: Database, + applicationConfig: ApplicationConfig +) { + + case class PropertyDescription(name: String, `type`: String, values: Seq[JsValue] = Nil, labels: Seq[String] = Nil) + case class EntityDescription(label: String, attributes: Seq[PropertyDescription]) { + def toJson: JsObject = + Json.obj( + "label" -> label, + "attributes" -> attributes + ) + } + + lazy val logger: Logger = Logger(getClass) + + val cacheExpireConfig: ConfigItem[Duration, Duration] = + applicationConfig.item[Duration]("describe.cache.expire", "Custom fields refresh in describe") + def cacheExpire: Duration = cacheExpireConfig.get + + def describeCortexEntity( + name: String, + className: String, + packageName: String = "org.thp.thehive.connector.cortex.controllers.v1" + ): Option[EntityDescription] = + Try( + EntityDescription( + name, + injector + .instanceOf(getClass.getClassLoader.loadClass(s"$packageName.$className")) + .asInstanceOf[QueryableCtrl] + .publicProperties + .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", _))) + ) ++ 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: List[PropertyDescription] = db.roTransaction { implicit graph => + customFieldSrv.initSteps.toList.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")) +// ) +// ) +// ) +// 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 (_, "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 { + prop.mapping.domainTypeClass match { + case c if c == classOf[Boolean] || c == classOf[JBoolean] => Seq(PropertyDescription(prop.propertyName, "boolean")) + case c if c == classOf[Date] => Seq(PropertyDescription(prop.propertyName, "date")) + case c if c == classOf[Hash] => Seq(PropertyDescription(prop.propertyName, "hash")) + case c if classOf[Number].isAssignableFrom(c) => Seq(PropertyDescription(prop.propertyName, "number")) + case c if c == classOf[String] => Seq(PropertyDescription(prop.propertyName, "string")) + case _ => + logger.warn(s"Unrecognized property $prop. Add a custom description") + Seq(PropertyDescription(prop.propertyName, "unknown")) + } + } + + def describe(modelName: String): Action[AnyContent] = + entrypoint("describe model") + .auth { _ => + entityDescriptions + .collectFirst { + case desc if desc.label == modelName => Success(Results.Ok(cacheApi.getOrElseUpdate(s"describe.$modelName", cacheExpire)(desc.toJson))) + } + .getOrElse(Failure(NotFoundError(s"Model $modelName not found"))) + } + + def describeAll: Action[AnyContent] = + entrypoint("describe all models") + .auth { _ => + val descriptors = entityDescriptions.map { desc => + desc.label -> cacheApi.getOrElseUpdate(s"describe.${desc.label}", cacheExpire)(desc.toJson) + } + Success(Results.Ok(JsObject(descriptors))) + } +} diff --git a/thehive/app/org/thp/thehive/controllers/v1/Router.scala b/thehive/app/org/thp/thehive/controllers/v1/Router.scala index 70e4477a87..984871bf24 100644 --- a/thehive/app/org/thp/thehive/controllers/v1/Router.scala +++ b/thehive/app/org/thp/thehive/controllers/v1/Router.scala @@ -16,7 +16,8 @@ class Router @Inject() ( alertCtrl: AlertCtrl, auditCtrl: AuditCtrl, statusCtrl: StatusCtrl, - authenticationCtrl: AuthenticationCtrl + authenticationCtrl: AuthenticationCtrl, + describeCtrl: DescribeCtrl ) extends SimpleRouter { override def routes: Routes = { @@ -94,5 +95,8 @@ class Router @Inject() ( // POST /audit/_search controllers.AuditCtrl.find() // POST /audit/_stats controllers.AuditCtrl.stats() + case GET(p"/describe/_all") => describeCtrl.describeAll + case GET(p"/describe/$modelName") => describeCtrl.describe(modelName) + } }