Skip to content

Commit d3dc391

Browse files
committed
#234 Check mandatory attributes on nested object
1 parent 6ef63fd commit d3dc391

File tree

4 files changed

+67
-17
lines changed

4 files changed

+67
-17
lines changed

thehive-backend/app/models/Alert.scala

+29-13
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import java.util.Date
44
import javax.inject.{ Inject, Singleton }
55

66
import models.JsonFormat.alertStatusFormat
7+
import org.elastic4play.controllers.JsonInputValue
8+
import org.elastic4play.{ AttributeCheckingError, InvalidFormatAttributeError }
79
import org.elastic4play.models.{ Attribute, AttributeDef, BaseEntity, EntityDef, HiveEnumeration, ModelDef, AttributeFormat F, AttributeOption O }
810
import org.elastic4play.utils.Hasher
911
import play.api.Logger
@@ -50,21 +52,35 @@ class AlertModel @Inject() (artifactModel: ArtifactModel)
5052
override val defaultSortBy: Seq[String] = Seq("-date")
5153
override val removeAttribute: JsObject = Json.obj("status" AlertStatus.Ignored)
5254

53-
override def artifactAttributes: Seq[Attribute[_]] = artifactModel.attributes
55+
override def artifactAttributes: Seq[Attribute[_]] = artifactModel
56+
.attributes
57+
.filter(_.isForm)
5458

5559
override def creationHook(parent: Option[BaseEntity], attrs: JsObject): Future[JsObject] = {
56-
Future.successful {
57-
if (attrs.keys.contains("_id"))
58-
attrs
59-
else {
60-
val hasher = Hasher("MD5")
61-
val tpe = (attrs \ "tpe").asOpt[String].getOrElse("<null>")
62-
val source = (attrs \ "source").asOpt[String].getOrElse("<null>")
63-
val sourceRef = (attrs \ "sourceRef").asOpt[String].getOrElse("<null>")
64-
val _id = hasher.fromString(s"$tpe|$source|$sourceRef").head.toString()
65-
attrs + ("_id" JsString(_id))
66-
} - "lastSyncDate" - "case" - "status" - "follow"
67-
}
60+
// check if data attribute is present on all artifacts
61+
val missingDataErrors = (attrs \ "artifacts")
62+
.asOpt[Seq[JsValue]]
63+
.getOrElse(Nil)
64+
.filter { a
65+
(a \ "data").toOption.isEmpty ||
66+
((a \ "tags").toOption.isEmpty && (a \ "message").toOption.isEmpty)
67+
}
68+
.map(v InvalidFormatAttributeError("artifacts", "artifact", JsonInputValue(v)))
69+
if (missingDataErrors.nonEmpty)
70+
Future.failed(AttributeCheckingError("alert", missingDataErrors))
71+
else
72+
Future.successful {
73+
if (attrs.keys.contains("_id"))
74+
attrs
75+
else {
76+
val hasher = Hasher("MD5")
77+
val tpe = (attrs \ "tpe").asOpt[String].getOrElse("<null>")
78+
val source = (attrs \ "source").asOpt[String].getOrElse("<null>")
79+
val sourceRef = (attrs \ "sourceRef").asOpt[String].getOrElse("<null>")
80+
val _id = hasher.fromString(s"$tpe|$source|$sourceRef").head.toString()
81+
attrs + ("_id" JsString(_id))
82+
} - "lastSyncDate" - "case" - "status" - "follow"
83+
}
6884
}
6985
}
7086

thehive-backend/app/models/Artifact.scala

+27-2
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ trait ArtifactAttributes { _: AttributeDef ⇒
2828
val artifactId: A[String] = attribute("_id", F.stringFmt, "Artifact id", O.model)
2929
val data: A[Option[String]] = optionalAttribute("data", F.stringFmt, "Content of the artifact", O.readonly)
3030
val dataType: A[String] = attribute("dataType", F.listEnumFmt("artifactDataType")(dblists), "Type of the artifact", O.readonly)
31-
val message: A[String] = attribute("message", F.textFmt, "Description of the artifact in the context of the case")
31+
val message: A[Option[String]] = optionalAttribute("message", F.textFmt, "Description of the artifact in the context of the case")
3232
val startDate: A[Date] = attribute("startDate", F.dateFmt, "Creation date", new Date)
3333
val attachment: A[Option[Attachment]] = optionalAttribute("attachment", F.attachmentFmt, "Artifact file content", O.readonly)
3434
val tlp: A[Long] = attribute("tlp", F.numberFmt, "TLP level", 2L)
@@ -47,21 +47,46 @@ class ArtifactModel @Inject() (
4747
implicit val ec: ExecutionContext) extends ChildModelDef[ArtifactModel, Artifact, CaseModel, Case](caseModel, "case_artifact") with ArtifactAttributes with AuditedModel {
4848
override val removeAttribute: JsObject = Json.obj("status" ArtifactStatus.Deleted)
4949

50-
override def apply(attributes: JsObject) = {
50+
override def apply(attributes: JsObject): Artifact = {
5151
val tags = (attributes \ "tags").asOpt[Seq[JsString]].getOrElse(Nil).distinct
5252
new Artifact(this, attributes + ("tags" JsArray(tags)))
5353
}
5454

5555
// this method modify request in order to hash artifact and manager file upload
5656
override def creationHook(parent: Option[BaseEntity], attrs: JsObject): Future[JsObject] = {
5757
val keys = attrs.keys
58+
println(s"keys=$keys")
59+
println(s"attrs=$attrs")
60+
if (!keys.contains("message") && (attrs \ "tags").asOpt[Seq[JsValue]].forall(_.isEmpty))
61+
throw BadRequestError(s"Artifact must contain a message or on ore more tags")
5862
if (keys.contains("data") == keys.contains("attachment"))
5963
throw BadRequestError(s"Artifact must contain data or attachment (but not both)")
6064
computeId(parent, attrs).map { id
6165
attrs + ("_id" JsString(id))
6266
}
6367
}
6468

69+
override def updateHook(entity: BaseEntity, updateAttrs: JsObject): Future[JsObject] = {
70+
entity match {
71+
case artifact: Artifact
72+
val removeMessage = (updateAttrs \ "message").toOption.exists {
73+
case JsNull true
74+
case JsArray(Nil) true
75+
case _ false
76+
}
77+
val removeTags = (updateAttrs \ "tags").toOption.exists {
78+
case JsNull true
79+
case JsArray(Nil) true
80+
case _ false
81+
}
82+
if ((removeMessage && removeTags) ||
83+
(removeMessage && artifact.tags().isEmpty) ||
84+
(removeTags && artifact.message().isEmpty))
85+
Future.failed(BadRequestError(s"Artifact must contain a message or on ore more tags"))
86+
else
87+
Future.successful(updateAttrs)
88+
}
89+
}
6590
def computeId(parent: Option[BaseEntity], attrs: JsObject): Future[String] = {
6691
// in order to make sure that there is no duplicated artifact, calculate its id from its content (dataType, data, attachment and parent)
6792
val mm = new MultiHash("MD5")

thehive-backend/app/models/Audit.scala

+6-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import javax.inject.{ Inject, Singleton }
66
import scala.collection.immutable
77
import play.api.{ Configuration, Logger }
88
import play.api.libs.json.JsObject
9-
import org.elastic4play.models.{ Attribute, AttributeDef, EntityDef, EnumerationAttributeFormat, ModelDef, ObjectAttributeFormat, StringAttributeFormat, AttributeFormat F, AttributeOption O }
9+
import org.elastic4play.models.{ Attribute, AttributeDef, EntityDef, EnumerationAttributeFormat, ModelDef, MultiAttributeFormat, ObjectAttributeFormat, OptionalAttributeFormat, StringAttributeFormat, AttributeFormat F, AttributeOption O }
1010
import org.elastic4play.services.AuditableAction
1111
import org.elastic4play.services.JsonFormat.auditableActionFormat
1212
import services.AuditedModel
@@ -48,6 +48,11 @@ class AuditModel(
4848
case attr Seq(attr)
4949
}
5050
.filter(_.isModel)
51+
.map {
52+
case attr @ Attribute(_, _, OptionalAttributeFormat(_), _, _, _) attr
53+
case attr @ Attribute(_, _, MultiAttributeFormat(_), _, _, _) attr
54+
case attr attr.toOptional
55+
}
5156
.groupBy(_.name)
5257
.flatMap {
5358
// if only one attribute is found for a name, get it

thehive-backend/app/services/CaseMergeSrv.scala

+5-1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ class CaseMergeSrv @Inject() (
3434

3535
import org.elastic4play.services.QueryDSL._
3636

37+
private[services] def concatOpt[E](entities: Seq[E], sep: String, getId: E Long, getStr: E Option[String]) = {
38+
JsString(entities.flatMap(e getStr(e).map(s s"#${getId(e)}:$s")).mkString(sep))
39+
}
40+
3741
private[services] def concat[E](entities: Seq[E], sep: String, getId: E Long, getStr: E String) = {
3842
JsString(entities.map(e s"#${getId(e)}:${getStr(e)}").mkString(sep))
3943
}
@@ -212,7 +216,7 @@ class CaseMergeSrv @Inject() (
212216
}
213217
.set("data", firstArtifact.data().map(JsString))
214218
.set("dataType", firstArtifact.dataType())
215-
.set("message", concat[Artifact](sameArtifacts, "\n \n", a caseMap(a.parentId.get).caseId(), _.message()))
219+
.set("message", concatOpt[Artifact](sameArtifacts, "\n \n", a caseMap(a.parentId.get).caseId(), _.message()))
216220
.set("startDate", firstDate(sameArtifacts.map(_.startDate())))
217221
.set("tlp", JsNumber(sameArtifacts.map(_.tlp()).min))
218222
.set("tags", JsArray(sameArtifacts.flatMap(_.tags()).distinct.map(JsString)))

0 commit comments

Comments
 (0)