diff --git a/cortex/connector/src/main/scala/org/thp/thehive/connector/cortex/services/ActionOperationSrv.scala b/cortex/connector/src/main/scala/org/thp/thehive/connector/cortex/services/ActionOperationSrv.scala index e080801a32..22230c7b5b 100644 --- a/cortex/connector/src/main/scala/org/thp/thehive/connector/cortex/services/ActionOperationSrv.scala +++ b/cortex/connector/src/main/scala/org/thp/thehive/connector/cortex/services/ActionOperationSrv.scala @@ -71,7 +71,7 @@ class ActionOperationSrv @Inject() ( case AddCustomFields(name, _, value) => for { c <- relatedCase.fold[Try[Case with Entity]](Failure(InternalError("Unable to apply action AddCustomFields without case")))(Success(_)) - _ <- caseSrv.setOrCreateCustomField(c, name, Some(value)) + _ <- caseSrv.setOrCreateCustomField(c, name, Some(value), None) } yield updateOperation(operation) case CloseTask() => diff --git a/dto/src/main/scala/org/thp/thehive/dto/v1/CustomFieldValue.scala b/dto/src/main/scala/org/thp/thehive/dto/v1/CustomFieldValue.scala index 09f18634f4..7f0112345e 100644 --- a/dto/src/main/scala/org/thp/thehive/dto/v1/CustomFieldValue.scala +++ b/dto/src/main/scala/org/thp/thehive/dto/v1/CustomFieldValue.scala @@ -20,7 +20,7 @@ object OutputCustomField { implicit val format: OFormat[OutputCustomField] = Json.format[OutputCustomField] } -case class InputCustomFieldValue(name: String, value: Option[Any]) +case class InputCustomFieldValue(name: String, value: Option[Any], order: Option[Int]) object InputCustomFieldValue { @@ -29,11 +29,11 @@ object InputCustomFieldValue { fields .toSeq .validatedBy { - case (name, FString(value)) => Good(InputCustomFieldValue(name, Some(value))) - case (name, FNumber(value)) => Good(InputCustomFieldValue(name, Some(value))) - case (name, FBoolean(value)) => Good(InputCustomFieldValue(name, Some(value))) - case (name, FAny(value :: _)) => Good(InputCustomFieldValue(name, Some(value))) - case (name, FNull) => Good(InputCustomFieldValue(name, None)) + case (name, FString(value)) => Good(InputCustomFieldValue(name, Some(value), None)) + case (name, FNumber(value)) => Good(InputCustomFieldValue(name, Some(value), None)) + case (name, FBoolean(value)) => Good(InputCustomFieldValue(name, Some(value), None)) + case (name, FAny(value :: _)) => Good(InputCustomFieldValue(name, Some(value), None)) + case (name, FNull) => Good(InputCustomFieldValue(name, None, None)) case (name, other) => Bad( One( @@ -46,13 +46,13 @@ object InputCustomFieldValue { } implicit val writes: Writes[Seq[InputCustomFieldValue]] = Writes[Seq[InputCustomFieldValue]] { icfv => val fields = icfv.map { - case InputCustomFieldValue(name, Some(s: String)) => name -> JsString(s) - case InputCustomFieldValue(name, Some(l: Long)) => name -> JsNumber(l) - case InputCustomFieldValue(name, Some(d: Double)) => name -> JsNumber(d) - case InputCustomFieldValue(name, Some(b: Boolean)) => name -> JsBoolean(b) - case InputCustomFieldValue(name, Some(d: Date)) => name -> JsNumber(d.getTime) - case InputCustomFieldValue(name, None) => name -> JsNull - case InputCustomFieldValue(name, other) => sys.error(s"The custom field $name has invalid value: $other (${other.getClass})") + case InputCustomFieldValue(name, Some(s: String), _) => name -> JsString(s) + case InputCustomFieldValue(name, Some(l: Long), _) => name -> JsNumber(l) + case InputCustomFieldValue(name, Some(d: Double), _) => name -> JsNumber(d) + case InputCustomFieldValue(name, Some(b: Boolean), _) => name -> JsBoolean(b) + case InputCustomFieldValue(name, Some(d: Date), _) => name -> JsNumber(d.getTime) + case InputCustomFieldValue(name, None, _) => name -> JsNull + case InputCustomFieldValue(name, other, _) => sys.error(s"The custom field $name has invalid value: $other (${other.getClass})") } JsObject(fields) } diff --git a/thehive/app/org/thp/thehive/controllers/v0/CaseCtrl.scala b/thehive/app/org/thp/thehive/controllers/v0/CaseCtrl.scala index 5aa02514bb..8492b74626 100644 --- a/thehive/app/org/thp/thehive/controllers/v0/CaseCtrl.scala +++ b/thehive/app/org/thp/thehive/controllers/v0/CaseCtrl.scala @@ -68,7 +68,7 @@ class CaseCtrl @Inject() ( val caseTemplateName: Option[String] = request.body("caseTemplate") val inputCase: InputCase = request.body("case") val inputTasks: Seq[InputTask] = request.body("tasks") - val customFields = inputCase.customFields.map(c => c.name -> c.value).toMap + val customFields = inputCase.customFields.map(c => (c.name, c.value, c.order)) for { organisation <- userSrv .current diff --git a/thehive/app/org/thp/thehive/controllers/v0/Properties.scala b/thehive/app/org/thp/thehive/controllers/v0/Properties.scala index c9095be753..8e025b2451 100644 --- a/thehive/app/org/thp/thehive/controllers/v0/Properties.scala +++ b/thehive/app/org/thp/thehive/controllers/v0/Properties.scala @@ -196,12 +196,12 @@ class Properties @Inject() ( for { // v <- UniMapping.jsonNative.toGraphOpt(value).fold[Try[Any]](???)(Success.apply) c <- caseSrv.getOrFail(vertex)(graph) - _ <- caseSrv.setOrCreateCustomField(c, name, Some(value))(graph, authContext) + _ <- 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(_ -> v) } + 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")) diff --git a/thehive/app/org/thp/thehive/controllers/v1/CaseCtrl.scala b/thehive/app/org/thp/thehive/controllers/v1/CaseCtrl.scala index 9d7dfdd813..3f34f255a6 100644 --- a/thehive/app/org/thp/thehive/controllers/v1/CaseCtrl.scala +++ b/thehive/app/org/thp/thehive/controllers/v1/CaseCtrl.scala @@ -66,7 +66,7 @@ class CaseCtrl @Inject() ( val inputTasks: Seq[InputTask] = request.body("tasks") for { caseTemplate <- caseTemplateName.map(caseTemplateSrv.get(_).visible.richCaseTemplate.getOrFail("CaseTemplate")).flip - customFields = inputCase.customFieldValue.map(cf => cf.name -> cf.value).toMap + customFields = inputCase.customFieldValue.map(cf => (cf.name, cf.value, cf.order)) organisation <- userSrv.current.organisations(Permissions.manageCase).get(request.organisation).getOrFail("Organisation") user <- inputCase.user.fold[Try[Option[User with Entity]]](Success(None))(u => userSrv.getOrFail(u).map(Some.apply)) tags <- inputCase.tags.toTry(tagSrv.getOrCreate) diff --git a/thehive/app/org/thp/thehive/services/AlertSrv.scala b/thehive/app/org/thp/thehive/services/AlertSrv.scala index 16bb3f0a3b..2f0a318c99 100644 --- a/thehive/app/org/thp/thehive/services/AlertSrv.scala +++ b/thehive/app/org/thp/thehive/services/AlertSrv.scala @@ -231,7 +231,7 @@ class AlertSrv @Inject() ( .caseTemplate .map(caseTemplateSrv.get(_).richCaseTemplate.getOrFail()) .flip - customField = alert.customFields.map(f => f.name -> f.value).toMap + customField = alert.customFields.map(f => (f.name, f.value, f.order)) case0 = Case( number = 0, title = caseTemplate.flatMap(_.titlePrefix).getOrElse("") + alert.title, diff --git a/thehive/app/org/thp/thehive/services/CaseSrv.scala b/thehive/app/org/thp/thehive/services/CaseSrv.scala index b6aae42ec5..255d2233ae 100644 --- a/thehive/app/org/thp/thehive/services/CaseSrv.scala +++ b/thehive/app/org/thp/thehive/services/CaseSrv.scala @@ -57,7 +57,7 @@ class CaseSrv @Inject() ( user: Option[User with Entity], organisation: Organisation with Entity, tags: Set[Tag with Entity], - customFields: Map[String, Option[Any]], + customFields: Seq[(String, Option[Any], Option[Int])], caseTemplate: Option[RichCaseTemplate], additionalTasks: Seq[(Task, Option[User with Entity])] )(implicit graph: Graph, authContext: AuthContext): Try[RichCase] = @@ -73,8 +73,8 @@ class CaseSrv @Inject() ( _ <- createdTasks.toTry(t => shareSrv.shareTask(t, createdCase, organisation)) caseTemplateCustomFields = caseTemplate .fold[Seq[RichCustomField]](Nil)(_.customFields) - .map(cf => cf.name -> cf.value) - cfs <- (caseTemplateCustomFields.toMap ++ customFields).toTry { case (name, value) => createCustomField(createdCase, name, value) } + .map(cf => (cf.name, cf.value, cf.order)) + cfs <- (caseTemplateCustomFields ++ customFields).toTry { case (name, value, order) => createCustomField(createdCase, name, value, order) } caseTemplateTags = caseTemplate.fold[Seq[Tag with Entity]](Nil)(_.tags) allTags = tags ++ caseTemplateTags _ <- allTags.toTry(t => caseTagSrv.create(CaseTag(), createdCase, t)) @@ -190,7 +190,7 @@ class CaseSrv @Inject() ( def updateCustomField( `case`: Case with Entity, - customFieldValues: Seq[(CustomField, Any)] + customFieldValues: Seq[(CustomField, Any, Option[Int])] )(implicit graph: Graph, authContext: AuthContext): Try[Unit] = { val customFieldNames = customFieldValues.map(_._1.name) get(`case`) @@ -200,11 +200,11 @@ class CaseSrv @Inject() ( .filterNot(rcf => customFieldNames.contains(rcf.name)) .foreach(rcf => get(`case`).customFields(rcf.name).remove()) customFieldValues - .toTry { case (cf, v) => setOrCreateCustomField(`case`, cf.name, Some(v)) } + .toTry { case (cf, v, o) => setOrCreateCustomField(`case`, cf.name, Some(v), o) } .map(_ => ()) } - def setOrCreateCustomField(`case`: Case with Entity, customFieldName: String, value: Option[Any])( + def setOrCreateCustomField(`case`: Case with Entity, customFieldName: String, value: Option[Any], order: Option[Int])( implicit graph: Graph, authContext: AuthContext ): Try[Unit] = { @@ -212,17 +212,18 @@ class CaseSrv @Inject() ( if (cfv.newInstance().exists()) cfv.setValue(value) else - createCustomField(`case`, customFieldName, value).map(_ => ()) + createCustomField(`case`, customFieldName, value, order).map(_ => ()) } def createCustomField( `case`: Case with Entity, customFieldName: String, - customFieldValue: Option[Any] + customFieldValue: Option[Any], + order: Option[Int] )(implicit graph: Graph, authContext: AuthContext): Try[RichCustomField] = for { cf <- customFieldSrv.getOrFail(customFieldName) - ccf <- CustomFieldType.map(cf.`type`).setValue(CaseCustomField(), customFieldValue) + ccf <- CustomFieldType.map(cf.`type`).setValue(CaseCustomField(), customFieldValue).map(_.order_=(order)) ccfe <- caseCustomFieldSrv.create(ccf, `case`, cf) } yield RichCustomField(cf, ccfe) diff --git a/thehive/test/org/thp/thehive/controllers/v0/AuditCtrlTest.scala b/thehive/test/org/thp/thehive/controllers/v0/AuditCtrlTest.scala index 25cf315e6e..260ef0599e 100644 --- a/thehive/test/org/thp/thehive/controllers/v0/AuditCtrlTest.scala +++ b/thehive/test/org/thp/thehive/controllers/v0/AuditCtrlTest.scala @@ -32,7 +32,7 @@ class AuditCtrlTest extends PlaySpecification with TestAppBuilder { None, app[OrganisationSrv].getOrFail("admin").get, Set.empty, - Map.empty, + Seq.empty, None, Nil )(graph, authContext) diff --git a/thehive/test/org/thp/thehive/controllers/v0/StreamCtrlTest.scala b/thehive/test/org/thp/thehive/controllers/v0/StreamCtrlTest.scala index fa82604aa2..ed4350db4f 100644 --- a/thehive/test/org/thp/thehive/controllers/v0/StreamCtrlTest.scala +++ b/thehive/test/org/thp/thehive/controllers/v0/StreamCtrlTest.scala @@ -37,7 +37,7 @@ class StreamCtrlTest extends PlaySpecification with TestAppBuilder { None, app[OrganisationSrv].getOrFail("cert").get, Set.empty, - Map.empty, + Seq.empty, None, Nil ) diff --git a/thehive/test/org/thp/thehive/services/AuditSrvTest.scala b/thehive/test/org/thp/thehive/services/AuditSrvTest.scala index 8a1a4ac9d7..1403f3413b 100644 --- a/thehive/test/org/thp/thehive/services/AuditSrvTest.scala +++ b/thehive/test/org/thp/thehive/services/AuditSrvTest.scala @@ -25,7 +25,7 @@ class AuditSrvTest extends PlaySpecification with TestAppBuilder { None, orgAdmin, Set.empty, - Map.empty, + Seq.empty, None, Nil ) diff --git a/thehive/test/org/thp/thehive/services/CaseSrvTest.scala b/thehive/test/org/thp/thehive/services/CaseSrvTest.scala index 977b8b7057..ee60050a81 100644 --- a/thehive/test/org/thp/thehive/services/CaseSrvTest.scala +++ b/thehive/test/org/thp/thehive/services/CaseSrvTest.scala @@ -169,7 +169,7 @@ class CaseSrvTest extends PlaySpecification with TestAppBuilder { "add custom field with wrong type" in testApp { app => app[Database].transaction { implicit graph => app[CaseSrv].getOrFail("#3") must beSuccessfulTry.which { `case`: Case with Entity => - app[CaseSrv].setOrCreateCustomField(`case`, "boolean1", Some("plop")) must beFailedTry + app[CaseSrv].setOrCreateCustomField(`case`, "boolean1", Some("plop"), None) must beFailedTry } } } @@ -177,7 +177,7 @@ class CaseSrvTest extends PlaySpecification with TestAppBuilder { "add custom field" in testApp { app => app[Database].transaction { implicit graph => app[CaseSrv].getOrFail("#3") must beSuccessfulTry.which { `case`: Case with Entity => - app[CaseSrv].setOrCreateCustomField(`case`, "boolean1", Some(true)) must beSuccessfulTry + app[CaseSrv].setOrCreateCustomField(`case`, "boolean1", Some(true), None) must beSuccessfulTry app[CaseSrv].getCustomField(`case`, "boolean1").flatMap(_.value) must beSome.which(_ == true) } } @@ -186,7 +186,7 @@ class CaseSrvTest extends PlaySpecification with TestAppBuilder { "update custom field" in testApp { app => app[Database].transaction { implicit graph => app[CaseSrv].getOrFail("#3") must beSuccessfulTry.which { `case`: Case with Entity => - app[CaseSrv].setOrCreateCustomField(`case`, "boolean1", Some(false)) must beSuccessfulTry + app[CaseSrv].setOrCreateCustomField(`case`, "boolean1", Some(false), None) must beSuccessfulTry app[CaseSrv].getCustomField(`case`, "boolean1").flatMap(_.value) must beSome.which(_ == false) } } @@ -242,7 +242,7 @@ class CaseSrvTest extends PlaySpecification with TestAppBuilder { None, app[OrganisationSrv].getOrFail("cert").get, app[TagSrv].initSteps.toList.toSet, - Map.empty, + Seq.empty, None, Nil ) @@ -299,7 +299,7 @@ class CaseSrvTest extends PlaySpecification with TestAppBuilder { None, app[OrganisationSrv].getOrFail("cert").get, Set[Tag with Entity](), - Map.empty, + Seq.empty, None, Nil ) @@ -321,7 +321,7 @@ class CaseSrvTest extends PlaySpecification with TestAppBuilder { None, app[OrganisationSrv].getOrFail("cert").get, app[TagSrv].initSteps.toList.toSet, - Map.empty, + Seq.empty, None, Nil ) @@ -344,7 +344,7 @@ class CaseSrvTest extends PlaySpecification with TestAppBuilder { None, app[OrganisationSrv].getOrFail("cert").get, app[TagSrv].initSteps.toList.toSet, - Map.empty, + Seq.empty, None, Nil ) @@ -367,7 +367,7 @@ class CaseSrvTest extends PlaySpecification with TestAppBuilder { Some(app[UserSrv].get("certuser@thehive.local").getOrFail().get), app[OrganisationSrv].getOrFail("cert").get, app[TagSrv].initSteps.toList.toSet, - Map.empty, + Seq.empty, None, Nil )