From 14eb88109fe6abd0409e21c7b9c40bfbec9b244e Mon Sep 17 00:00:00 2001 From: To-om Date: Thu, 1 Apr 2021 10:19:58 +0200 Subject: [PATCH 01/63] #1921 Fix customField selection query --- ScalliGraph | 2 +- .../thp/thehive/controllers/v0/CaseCtrl.scala | 4 +- .../controllers/v0/CaseTemplateCtrl.scala | 42 ++++---- .../thehive/controllers/v1/Properties.scala | 44 ++++++--- .../org/thp/thehive/models/CustomField.scala | 17 ++++ .../org/thp/thehive/services/AlertSrv.scala | 8 ++ .../org/thp/thehive/services/CaseSrv.scala | 8 ++ .../thehive/services/CaseTemplateSrv.scala | 95 +++++++++++++++---- .../services/CaseTemplateSrvTest.scala | 5 +- 9 files changed, 174 insertions(+), 51 deletions(-) diff --git a/ScalliGraph b/ScalliGraph index 4aa2d6084e..e7cf9fc714 160000 --- a/ScalliGraph +++ b/ScalliGraph @@ -1 +1 @@ -Subproject commit 4aa2d6084eaca86a8dbc7e078a53a103f0d63c8c +Subproject commit e7cf9fc7141ab4d12b8c06b5db427fb009a4f19c diff --git a/thehive/app/org/thp/thehive/controllers/v0/CaseCtrl.scala b/thehive/app/org/thp/thehive/controllers/v0/CaseCtrl.scala index 35baed7a5a..5dd56482af 100644 --- a/thehive/app/org/thp/thehive/controllers/v0/CaseCtrl.scala +++ b/thehive/app/org/thp/thehive/controllers/v0/CaseCtrl.scala @@ -275,9 +275,7 @@ class PublicCase @Inject() ( }) .property("customFields", UMapping.jsonNative)(_.subSelect { case (FPathElem(_, FPathElem(idOrName, _)), caseSteps) => - caseSteps - .customFields(EntityIdOrName(idOrName)) - .jsonValue + caseSteps.customFieldJsonValue(customFieldSrv, EntityIdOrName(idOrName)) case (_, caseSteps) => caseSteps.customFields.nameJsonValue.fold.domainMap(JsObject(_)) } .filter[JsValue] { diff --git a/thehive/app/org/thp/thehive/controllers/v0/CaseTemplateCtrl.scala b/thehive/app/org/thp/thehive/controllers/v0/CaseTemplateCtrl.scala index a0c435e2d9..832b808f56 100644 --- a/thehive/app/org/thp/thehive/controllers/v0/CaseTemplateCtrl.scala +++ b/thehive/app/org/thp/thehive/controllers/v0/CaseTemplateCtrl.scala @@ -16,7 +16,7 @@ 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.libs.json.{JsObject, JsValue, Json} import play.api.mvc.{Action, AnyContent, Results} import javax.inject.{Inject, Named, Singleton} @@ -134,22 +134,32 @@ class PublicCaseTemplate @Inject() ( .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 (FPathElem(_, FPathElem(name, _)), caseTemplateSteps) => caseTemplateSteps.customFieldJsonValue(customFieldSrv, EntityIdOrName(name)) case (_, caseTemplateSteps) => caseTemplateSteps.customFields.nameJsonValue.fold.domainMap(JsObject(_)) - }.custom { - case (FPathElem(_, FPathElem(name, _)), value, vertex, graph, authContext) => - for { - c <- caseTemplateSrv.get(vertex)(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(EntityIdOrName(n))(graph).map(_ -> v) } - _ <- caseTemplateSrv.updateCustomField(c, cfv)(graph, authContext) - } yield Json.obj("customFields" -> values) - case _ => Failure(BadRequestError("Invalid custom fields format")) - }) + } + .filter[JsValue] { + case (FPathElem(_, FPathElem(name, _)), caseTemplateTraversal, _, predicate) => + predicate match { + case Right(predicate) => caseTemplateTraversal.customFieldFilter(customFieldSrv, EntityIdOrName(name), predicate) + case Left(true) => caseTemplateTraversal.hasCustomField(customFieldSrv, EntityIdOrName(name)) + case Left(false) => caseTemplateTraversal.hasNotCustomField(customFieldSrv, EntityIdOrName(name)) + } + case (_, caseTraversal, _, _) => caseTraversal.empty + } + .custom { + case (FPathElem(_, FPathElem(name, _)), value, vertex, graph, authContext) => + for { + c <- caseTemplateSrv.get(vertex)(graph).getOrFail("CaseTemplate") + _ <- caseTemplateSrv.setOrCreateCustomField(c, EntityIdOrName(name), Some(value), None)(graph, authContext) + } yield Json.obj(s"customField.$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(EntityIdOrName(n))(graph).map(cf => (cf, v, None)) } + _ <- 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.richTaskWithoutActionRequired.domainMap(_.toJson)).custom { // FIXME select the correct mapping (_, value, vertex, graph, authContext) => diff --git a/thehive/app/org/thp/thehive/controllers/v1/Properties.scala b/thehive/app/org/thp/thehive/controllers/v1/Properties.scala index 819844c133..5a82f5ad7a 100644 --- a/thehive/app/org/thp/thehive/controllers/v1/Properties.scala +++ b/thehive/app/org/thp/thehive/controllers/v1/Properties.scala @@ -106,8 +106,7 @@ class Properties @Inject() ( .property("customFields", UMapping.jsonNative)(_.subSelect { case (FPathElem(_, FPathElem(idOrName, _)), alerts) => alerts - .customFields(EntityIdOrName(idOrName)) - .jsonValue + .customFieldJsonValue(customFieldSrv, EntityIdOrName(idOrName)) case (_, alerts) => alerts.customFields.nameJsonValue.fold.domainMap(JsObject(_)) } .filter[JsValue] { @@ -209,8 +208,7 @@ class Properties @Inject() ( .property("customFields", UMapping.jsonNative)(_.subSelect { case (FPathElem(_, FPathElem(idOrName, _)), caseSteps) => caseSteps - .customFields(EntityIdOrName(idOrName)) - .jsonValue + .customFieldJsonValue(customFieldSrv, EntityIdOrName(idOrName)) case (_, caseSteps) => caseSteps.customFields.nameJsonValue.fold.domainMap(JsObject(_)) } .filter[JsValue] { @@ -273,16 +271,34 @@ class Properties @Inject() ( .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 <- caseTemplateSrv.getOrFail(vertex)(graph) - _ <- caseTemplateSrv.setOrCreateCustomField(c, name, Some(value), None)(graph, authContext) - } yield Json.obj(s"customField.$name" -> value) - case _ => Failure(BadRequestError("Invalid custom fields format")) - }) + case (FPathElem(_, FPathElem(idOrName, _)), caseTemplateSteps) => + caseTemplateSteps + .customFieldJsonValue(customFieldSrv, EntityIdOrName(idOrName)) + case (_, caseTemplateSteps) => caseTemplateSteps.customFields.nameJsonValue.fold.domainMap(JsObject(_)) + } + .filter[JsValue] { + case (FPathElem(_, FPathElem(name, _)), caseTemplateTraversal, _, predicate) => + predicate match { + case Right(predicate) => caseTemplateTraversal.customFieldFilter(customFieldSrv, EntityIdOrName(name), predicate) + case Left(true) => caseTemplateTraversal.hasCustomField(customFieldSrv, EntityIdOrName(name)) + case Left(false) => caseTemplateTraversal.hasNotCustomField(customFieldSrv, EntityIdOrName(name)) + } + case (_, caseTraversal, _, _) => caseTraversal.empty + } + .custom { + case (FPathElem(_, FPathElem(idOrName, _)), value, vertex, graph, authContext) => + for { + c <- caseTemplateSrv.get(vertex)(graph).getOrFail("CaseTemplate") + _ <- caseTemplateSrv.setOrCreateCustomField(c, EntityIdOrName(idOrName), Some(value), None)(graph, authContext) + } yield Json.obj(s"customField.$idOrName" -> 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(EntityIdOrName(n))(graph).map(cf => (cf, v, None)) } + _ <- caseTemplateSrv.updateCustomField(c, cfv)(graph, authContext) + } yield Json.obj("customFields" -> values) + case _ => Failure(BadRequestError("Invalid custom fields format")) + }) .build lazy val organisation: PublicProperties = diff --git a/thehive/app/org/thp/thehive/models/CustomField.scala b/thehive/app/org/thp/thehive/models/CustomField.scala index fb4edb5241..e30b8710c6 100644 --- a/thehive/app/org/thp/thehive/models/CustomField.scala +++ b/thehive/app/org/thp/thehive/models/CustomField.scala @@ -3,6 +3,9 @@ package org.thp.thehive.models import org.apache.tinkerpop.gremlin.structure.Edge import org.thp.scalligraph._ import org.thp.scalligraph.models._ +import org.thp.scalligraph.traversal.Traversal +import org.thp.scalligraph.traversal.Traversal.{Domain, E} +import org.thp.scalligraph.traversal.TraversalOps._ import play.api.libs.json._ import java.util.{Date, NoSuchElementException} @@ -91,6 +94,10 @@ sealed abstract class CustomFieldType[T] { def getJsonValue(ccf: CustomFieldValue[_]): JsValue = getValue(ccf).fold[JsValue](JsNull)(writes.writes) + def getValue[C <: CustomFieldValue[_]](traversal: Traversal.E[C]): Traversal.Domain[T] + + def getJsonValue[C <: CustomFieldValue[_]](traversal: Traversal.E[C]): Traversal.Domain[JsValue] = getValue(traversal).domainMap(writes.writes) + override def toString: String = name protected def setValueFailure(value: Any): Failure[Nothing] = @@ -117,6 +124,8 @@ object CustomFieldString extends CustomFieldType[String] { } override def getValue(ccf: CustomFieldValue[_]): Option[String] = ccf.stringValue + + override def getValue[C <: CustomFieldValue[_]](traversal: E[C]): Traversal.Domain[String] = traversal.value(_.stringValue).castDomain } object CustomFieldBoolean extends CustomFieldType[Boolean] { @@ -137,6 +146,8 @@ object CustomFieldBoolean extends CustomFieldType[Boolean] { } override def getValue(ccf: CustomFieldValue[_]): Option[Boolean] = ccf.booleanValue + + override def getValue[C <: CustomFieldValue[_]](traversal: E[C]): Domain[Boolean] = traversal.value(_.booleanValue).castDomain } object CustomFieldInteger extends CustomFieldType[Int] { @@ -158,6 +169,8 @@ object CustomFieldInteger extends CustomFieldType[Int] { } override def getValue(ccf: CustomFieldValue[_]): Option[Int] = ccf.integerValue + + override def getValue[C <: CustomFieldValue[_]](traversal: E[C]): Domain[Int] = traversal.value(_.integerValue).castDomain } object CustomFieldFloat extends CustomFieldType[Double] { @@ -178,6 +191,8 @@ object CustomFieldFloat extends CustomFieldType[Double] { } override def getValue(ccf: CustomFieldValue[_]): Option[Double] = ccf.floatValue + + override def getValue[C <: CustomFieldValue[_]](traversal: E[C]): Domain[Double] = traversal.value(_.floatValue).castDomain } object CustomFieldDate extends CustomFieldType[Date] { @@ -199,6 +214,8 @@ object CustomFieldDate extends CustomFieldType[Date] { } override def getValue(ccf: CustomFieldValue[_]): Option[Date] = ccf.dateValue + + override def getValue[C <: CustomFieldValue[_]](traversal: E[C]): Domain[Date] = traversal.value(_.dateValue).castDomain } @DefineIndex(IndexType.unique, "name") diff --git a/thehive/app/org/thp/thehive/services/AlertSrv.scala b/thehive/app/org/thp/thehive/services/AlertSrv.scala index c39e6387fd..eebc4a9d91 100644 --- a/thehive/app/org/thp/thehive/services/AlertSrv.scala +++ b/thehive/app/org/thp/thehive/services/AlertSrv.scala @@ -469,6 +469,14 @@ object AlertOps { def customFields: Traversal.E[AlertCustomField] = traversal.outE[AlertCustomField] + def customFieldJsonValue(customFieldSrv: CustomFieldSrv, customField: EntityIdOrName): Traversal.Domain[JsValue] = + customFieldSrv + .get(customField)(traversal.graph) + .value(_.`type`) + .headOption + .map(t => CustomFieldType.map(t).getJsonValue(traversal.customFields(customField))) + .getOrElse(traversal.empty.castDomain) + def richCustomFields: Traversal[RichCustomField, JMap[String, Any], Converter[RichCustomField, JMap[String, Any]]] = traversal .outE[AlertCustomField] diff --git a/thehive/app/org/thp/thehive/services/CaseSrv.scala b/thehive/app/org/thp/thehive/services/CaseSrv.scala index 094978ef53..0d7b4d5cd1 100644 --- a/thehive/app/org/thp/thehive/services/CaseSrv.scala +++ b/thehive/app/org/thp/thehive/services/CaseSrv.scala @@ -494,6 +494,14 @@ object CaseOps { name => customFields.filter(_.inV.v[CustomField].has(_.name, name)) ) + def customFieldJsonValue(customFieldSrv: CustomFieldSrv, customField: EntityIdOrName): Traversal.Domain[JsValue] = + customFieldSrv + .get(customField)(traversal.graph) + .value(_.`type`) + .headOption + .map(t => CustomFieldType.map(t).getJsonValue(traversal.customFields(customField))) + .getOrElse(traversal.empty.castDomain) + def richCustomFields: Traversal[RichCustomField, JMap[String, Any], Converter[RichCustomField, JMap[String, Any]]] = customFields .project(_.by.by(_.inV.v[CustomField])) diff --git a/thehive/app/org/thp/thehive/services/CaseTemplateSrv.scala b/thehive/app/org/thp/thehive/services/CaseTemplateSrv.scala index cda9c8d8e1..d5f208f698 100644 --- a/thehive/app/org/thp/thehive/services/CaseTemplateSrv.scala +++ b/thehive/app/org/thp/thehive/services/CaseTemplateSrv.scala @@ -4,6 +4,7 @@ import akka.actor.ActorRef import org.apache.tinkerpop.gremlin.process.traversal.P import org.thp.scalligraph.auth.{AuthContext, Permission} import org.thp.scalligraph.models.{Database, Entity} +import org.thp.scalligraph.query.PredicateOps.PredicateOpsDefs import org.thp.scalligraph.query.PropertyUpdater import org.thp.scalligraph.services._ import org.thp.scalligraph.traversal.TraversalOps._ @@ -16,9 +17,9 @@ import org.thp.thehive.services.CustomFieldOps._ import org.thp.thehive.services.OrganisationOps._ import org.thp.thehive.services.TaskOps._ import org.thp.thehive.services.UserOps._ -import play.api.libs.json.{JsObject, Json} +import play.api.libs.json.{JsObject, JsValue, Json} -import java.util.{Map => JMap} +import java.util.{Date, Map => JMap} import javax.inject.{Inject, Named} import scala.util.{Failure, Success, Try} @@ -61,7 +62,9 @@ class CaseTemplateSrv @Inject() ( _ <- caseTemplateOrganisationSrv.create(CaseTemplateOrganisation(), createdCaseTemplate, organisation) createdTasks <- tasks.toTry(createTask(createdCaseTemplate, _)) _ <- caseTemplate.tags.toTry(tagSrv.getOrCreate(_).flatMap(t => caseTemplateTagSrv.create(CaseTemplateTag(), createdCaseTemplate, t))) - cfs <- customFields.zipWithIndex.toTry { case ((name, value), order) => createCustomField(createdCaseTemplate, name, value, Some(order + 1)) } + cfs <- customFields.zipWithIndex.toTry { + case ((name, value), order) => createCustomField(createdCaseTemplate, EntityIdOrName(name), value, Some(order + 1)) + } richCaseTemplate = RichCaseTemplate(createdCaseTemplate, organisation.name, createdTasks, cfs) _ <- auditSrv.caseTemplate.create(createdCaseTemplate, richCaseTemplate.toJson) } yield richCaseTemplate @@ -101,12 +104,12 @@ class CaseTemplateSrv @Inject() ( def addTags(caseTemplate: CaseTemplate with Entity, tags: Set[String])(implicit graph: Graph, authContext: AuthContext): Try[Unit] = updateTags(caseTemplate, tags ++ caseTemplate.tags).map(_ => ()) - def getCustomField(caseTemplate: CaseTemplate with Entity, customFieldName: String)(implicit graph: Graph): Option[RichCustomField] = - get(caseTemplate).customFields(customFieldName).richCustomField.headOption + def getCustomField(caseTemplate: CaseTemplate with Entity, customFieldIdOrName: EntityIdOrName)(implicit graph: Graph): Option[RichCustomField] = + get(caseTemplate).customFields(customFieldIdOrName).richCustomField.headOption def updateCustomField( caseTemplate: CaseTemplate 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(caseTemplate) @@ -114,32 +117,32 @@ class CaseTemplateSrv @Inject() ( .richCustomField .toIterator .filterNot(rcf => customFieldNames.contains(rcf.name)) - .foreach(rcf => get(caseTemplate).customFields(rcf.name).remove()) + .foreach(rcf => get(caseTemplate).customFields(EntityName(rcf.name)).remove()) customFieldValues - .zipWithIndex - .toTry { case ((cf, v), o) => setOrCreateCustomField(caseTemplate, cf.name, Some(v), Some(o + 1)) } + .toTry { case (cf, v, o) => setOrCreateCustomField(caseTemplate, EntityName(cf.name), Some(v), o) } .map(_ => ()) } - def setOrCreateCustomField(caseTemplate: CaseTemplate with Entity, customFieldName: String, value: Option[Any], order: Option[Int])(implicit + def setOrCreateCustomField(caseTemplate: CaseTemplate with Entity, customFieldIdOrName: EntityIdOrName, value: Option[Any], order: Option[Int])( + implicit graph: Graph, authContext: AuthContext ): Try[Unit] = { - val cfv = get(caseTemplate).customFields(customFieldName) + val cfv = get(caseTemplate).customFields(customFieldIdOrName) if (cfv.clone().exists) cfv.setValue(value) else - createCustomField(caseTemplate, customFieldName, value, order).map(_ => ()) + createCustomField(caseTemplate, customFieldIdOrName, value, order).map(_ => ()) } def createCustomField( caseTemplate: CaseTemplate with Entity, - customFieldName: String, + customFieldIdOrName: EntityIdOrName, customFieldValue: Option[Any], order: Option[Int] )(implicit graph: Graph, authContext: AuthContext): Try[RichCustomField] = for { - cf <- customFieldSrv.getOrFail(EntityIdOrName(customFieldName)) + cf <- customFieldSrv.getOrFail(customFieldIdOrName) ccf <- CustomFieldType.map(cf.`type`).setValue(CaseTemplateCustomField(order = order), customFieldValue) ccfe <- caseTemplateCustomFieldSrv.create(ccf, caseTemplate, cf) } yield RichCustomField(cf, ccfe) @@ -197,11 +200,71 @@ object CaseTemplateOps { def tags: Traversal.V[Tag] = traversal.out[CaseTemplateTag].v[Tag] - def customFields(name: String): Traversal.E[CaseTemplateCustomField] = - traversal.outE[CaseTemplateCustomField].filter(_.inV.v[CustomField].has(_.name, name)) + def customFields(idOrName: EntityIdOrName): Traversal.E[CaseTemplateCustomField] = + idOrName + .fold( + id => customFields.filter(_.inV.getByIds(id)), + name => customFields.filter(_.inV.v[CustomField].has(_.name, name)) + ) def customFields: Traversal.E[CaseTemplateCustomField] = traversal.outE[CaseTemplateCustomField] + + def customFieldJsonValue(customFieldSrv: CustomFieldSrv, customField: EntityIdOrName): Traversal.Domain[JsValue] = + customFieldSrv + .get(customField)(traversal.graph) + .value(_.`type`) + .headOption + .map(t => CustomFieldType.map(t).getJsonValue(traversal.customFields(customField))) + .getOrElse(traversal.empty.castDomain) + + def customFieldFilter(customFieldSrv: CustomFieldSrv, customField: EntityIdOrName, predicate: P[JsValue]): Traversal.V[CaseTemplate] = + customFieldSrv + .get(customField)(traversal.graph) + .value(_.`type`) + .headOption + .map { + case CustomFieldType.boolean => traversal.filter(_.customFields(customField).has(_.booleanValue, predicate.mapValue(_.as[Boolean]))) + case CustomFieldType.date => traversal.filter(_.customFields(customField).has(_.dateValue, predicate.mapValue(_.as[Date]))) + case CustomFieldType.float => traversal.filter(_.customFields(customField).has(_.floatValue, predicate.mapValue(_.as[Double]))) + case CustomFieldType.integer => traversal.filter(_.customFields(customField).has(_.integerValue, predicate.mapValue(_.as[Int]))) + case CustomFieldType.string => traversal.filter(_.customFields(customField).has(_.stringValue, predicate.mapValue(_.as[String]))) + } + .getOrElse(traversal.empty) + + def hasCustomField(customFieldSrv: CustomFieldSrv, customField: EntityIdOrName): Traversal.V[CaseTemplate] = { + val cfFilter = (t: Traversal.V[CustomField]) => customField.fold(id => t.hasId(id), name => t.has(_.name, name)) + + customFieldSrv + .get(customField)(traversal.graph) + .value(_.`type`) + .headOption + .map { + case CustomFieldType.boolean => traversal.filter(t => cfFilter(t.outE[CaseTemplateCustomField].has(_.booleanValue).inV.v[CustomField])) + case CustomFieldType.date => traversal.filter(t => cfFilter(t.outE[CaseTemplateCustomField].has(_.dateValue).inV.v[CustomField])) + case CustomFieldType.float => traversal.filter(t => cfFilter(t.outE[CaseTemplateCustomField].has(_.floatValue).inV.v[CustomField])) + case CustomFieldType.integer => traversal.filter(t => cfFilter(t.outE[CaseTemplateCustomField].has(_.integerValue).inV.v[CustomField])) + case CustomFieldType.string => traversal.filter(t => cfFilter(t.outE[CaseTemplateCustomField].has(_.stringValue).inV.v[CustomField])) + } + .getOrElse(traversal.empty) + } + + def hasNotCustomField(customFieldSrv: CustomFieldSrv, customField: EntityIdOrName): Traversal.V[CaseTemplate] = { + val cfFilter = (t: Traversal.V[CustomField]) => customField.fold(id => t.hasId(id), name => t.has(_.name, name)) + + customFieldSrv + .get(customField)(traversal.graph) + .value(_.`type`) + .headOption + .map { + case CustomFieldType.boolean => traversal.filterNot(t => cfFilter(t.outE[CaseTemplateCustomField].has(_.booleanValue).inV.v[CustomField])) + case CustomFieldType.date => traversal.filterNot(t => cfFilter(t.outE[CaseTemplateCustomField].has(_.dateValue).inV.v[CustomField])) + case CustomFieldType.float => traversal.filterNot(t => cfFilter(t.outE[CaseTemplateCustomField].has(_.floatValue).inV.v[CustomField])) + case CustomFieldType.integer => traversal.filterNot(t => cfFilter(t.outE[CaseTemplateCustomField].has(_.integerValue).inV.v[CustomField])) + case CustomFieldType.string => traversal.filterNot(t => cfFilter(t.outE[CaseTemplateCustomField].has(_.stringValue).inV.v[CustomField])) + } + .getOrElse(traversal.empty) + } } implicit class CaseTemplateCustomFieldsOpsDefs(traversal: Traversal.E[CaseTemplateCustomField]) extends CustomFieldValueOpsDefs(traversal) diff --git a/thehive/test/org/thp/thehive/services/CaseTemplateSrvTest.scala b/thehive/test/org/thp/thehive/services/CaseTemplateSrvTest.scala index 63ef590903..a744266871 100644 --- a/thehive/test/org/thp/thehive/services/CaseTemplateSrvTest.scala +++ b/thehive/test/org/thp/thehive/services/CaseTemplateSrvTest.scala @@ -128,7 +128,10 @@ class CaseTemplateSrvTest extends PlaySpecification with TestAppBuilder { bool1 <- app[CustomFieldSrv].getOrFail(EntityName("boolean1")) integer1 <- app[CustomFieldSrv].getOrFail(EntityName("integer1")) caseTemplate <- app[CaseTemplateSrv].getOrFail(EntityName("spam")) - _ <- app[CaseTemplateSrv].updateCustomField(caseTemplate, Seq((string1, JsString("hate")), (bool1, JsTrue), (integer1, JsNumber(1)))) + _ <- app[CaseTemplateSrv].updateCustomField( + caseTemplate, + Seq((string1, JsString("hate"), None), (bool1, JsTrue, None), (integer1, JsNumber(1), None)) + ) } yield () } must beSuccessfulTry From 59a7b1b29444b4807603be1079bedc85e580fd3f Mon Sep 17 00:00:00 2001 From: To-om Date: Thu, 1 Apr 2021 17:19:10 +0200 Subject: [PATCH 02/63] #1923 Make the property "Imported" in alerts optimised for index --- .../org/thp/thehive/controllers/v0/AlertCtrl.scala | 13 ++++++++++++- .../org/thp/thehive/controllers/v1/Properties.scala | 1 - 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/thehive/app/org/thp/thehive/controllers/v0/AlertCtrl.scala b/thehive/app/org/thp/thehive/controllers/v0/AlertCtrl.scala index da88cd00dd..777fe6bd8f 100644 --- a/thehive/app/org/thp/thehive/controllers/v0/AlertCtrl.scala +++ b/thehive/app/org/thp/thehive/controllers/v0/AlertCtrl.scala @@ -497,7 +497,18 @@ class PublicAlert @Inject() ( case _ => Failure(BadRequestError("Invalid custom fields format")) }) .property("case", db.idMapping)(_.select(_.`case`._id).readonly) - .property("imported", UMapping.boolean)(_.select(_.imported).readonly) + .property("imported", UMapping.boolean)( + _.select(_.imported) + .filter[Boolean]((_, alertTraversal, _, predicate) => + predicate.fold( + b => if (b) alertTraversal else alertTraversal.empty, + p => + if (p.getValue) alertTraversal.has(_.caseId) + else alertTraversal.hasNot(_.caseId) + ) + ) + .readonly + ) .property("importDate", UMapping.date.optional)(_.select(_.importDate).readonly) .property("computed.handlingDuration", UMapping.long)(_.select(_.handlingDuration).readonly) .property("computed.handlingDurationInSeconds", UMapping.long)(_.select(_.handlingDuration.math("_ / 1000").domainMap(_.toLong)).readonly) diff --git a/thehive/app/org/thp/thehive/controllers/v1/Properties.scala b/thehive/app/org/thp/thehive/controllers/v1/Properties.scala index 5a82f5ad7a..05c37ca713 100644 --- a/thehive/app/org/thp/thehive/controllers/v1/Properties.scala +++ b/thehive/app/org/thp/thehive/controllers/v1/Properties.scala @@ -133,7 +133,6 @@ class Properties @Inject() ( case _ => Failure(BadRequestError("Invalid custom fields format")) }) .property("case", db.idMapping)(_.select(_.`case`._id).readonly) - .property("imported", UMapping.boolean)(_.select(_.imported).readonly) .property("importDate", UMapping.date.optional)(_.select(_.importDate).readonly) .property("computed.handlingDuration", UMapping.long)(_.select(_.handlingDuration).readonly) .property("computed.handlingDurationInSeconds", UMapping.long)(_.select(_.handlingDuration.math("_ / 1000").domainMap(_.toLong)).readonly) From f6acf12391507c76272f05d673960f8e4cdd56fa Mon Sep 17 00:00:00 2001 From: To-om Date: Thu, 1 Apr 2021 17:19:29 +0200 Subject: [PATCH 03/63] Remove useless warning messages --- conf/logback.xml | 1 + thehive/app/org/thp/thehive/controllers/v0/DescribeCtrl.scala | 1 + thehive/app/org/thp/thehive/controllers/v1/DescribeCtrl.scala | 1 + 3 files changed, 3 insertions(+) diff --git a/conf/logback.xml b/conf/logback.xml index 94caf258db..8d7af55449 100644 --- a/conf/logback.xml +++ b/conf/logback.xml @@ -38,6 +38,7 @@ + - + diff --git a/thehive/app/org/thp/thehive/controllers/v1/AdminCtrl.scala b/thehive/app/org/thp/thehive/controllers/v1/AdminCtrl.scala index f8edb65334..0bb548f6b2 100644 --- a/thehive/app/org/thp/thehive/controllers/v1/AdminCtrl.scala +++ b/thehive/app/org/thp/thehive/controllers/v1/AdminCtrl.scala @@ -17,6 +17,8 @@ import scala.collection.immutable import scala.concurrent.duration.DurationInt import scala.concurrent.{ExecutionContext, Future} import scala.util.Success +import ch.qos.logback.classic.{Level, LoggerContext} +import org.slf4j.LoggerFactory @Singleton class AdminCtrl @Inject() ( @@ -40,6 +42,25 @@ class AdminCtrl @Inject() ( } lazy val logger: Logger = Logger(getClass) + def setLogLevel(packageName: String, levelName: String): Action[AnyContent] = + entrypoint("Update log level") + .authPermitted(Permissions.managePlatform) { _ => + val level = levelName match { + case "ALL" => Level.ALL + case "DEBUG" => Level.DEBUG + case "INFO" => Level.INFO + case "WARN" => Level.WARN + case "ERROR" => Level.ERROR + case "OFF" => Level.OFF + case "TRACE" => Level.TRACE + case _ => Level.INFO + } + val loggerContext = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext] + val logger = loggerContext.getLogger(packageName) + logger.setLevel(level) + Success(Results.NoContent) + } + def triggerCheck(name: String): Action[AnyContent] = entrypoint("Trigger check") .authPermitted(Permissions.managePlatform) { _ => diff --git a/thehive/app/org/thp/thehive/controllers/v1/Router.scala b/thehive/app/org/thp/thehive/controllers/v1/Router.scala index 652fbdd85d..e5b3ca9fa9 100644 --- a/thehive/app/org/thp/thehive/controllers/v1/Router.scala +++ b/thehive/app/org/thp/thehive/controllers/v1/Router.scala @@ -43,10 +43,11 @@ class Router @Inject() ( case GET(p"/status") => statusCtrl.get // GET /health controllers.StatusCtrl.health - case GET(p"/admin/check/stats") => adminCtrl.checkStats - case GET(p"/admin/check/$name/trigger") => adminCtrl.triggerCheck(name) - case GET(p"/admin/index/status") => adminCtrl.indexStatus - case GET(p"/admin/index/$name/reindex") => adminCtrl.reindex(name) + case GET(p"/admin/check/stats") => adminCtrl.checkStats + case GET(p"/admin/check/$name/trigger") => adminCtrl.triggerCheck(name) + case GET(p"/admin/index/status") => adminCtrl.indexStatus + case GET(p"/admin/index/$name/reindex") => adminCtrl.reindex(name) + case GET(p"/admin/log/set/$packageName/$level") => adminCtrl.setLogLevel(packageName, level) // GET /logout controllers.AuthenticationCtrl.logout() case GET(p"/logout") => authenticationCtrl.logout diff --git a/thehive/app/org/thp/thehive/services/IntegrityCheckActor.scala b/thehive/app/org/thp/thehive/services/IntegrityCheckActor.scala index 4e26cf9682..1fc88d07c3 100644 --- a/thehive/app/org/thp/thehive/services/IntegrityCheckActor.scala +++ b/thehive/app/org/thp/thehive/services/IntegrityCheckActor.scala @@ -125,9 +125,9 @@ class IntegrityCheckActor() extends Actor { Success(integrityCheck.initialCheck()) } } - integrityCheckOps.foreach { integrityCheck => - self ! DuplicationCheck(integrityCheck.name) - } +// integrityCheckOps.foreach { integrityCheck => +// self ! DuplicationCheck(integrityCheck.name) +// } globalTimers = integrityCheckOps.map { integrityCheck => val interval = globalInterval(integrityCheck.name) val initialDelay = FiniteDuration((interval.toNanos * Random.nextDouble()).round, NANOSECONDS) From 767026931f16959d60365c7f47ff559971b4924f Mon Sep 17 00:00:00 2001 From: Nabil Adouani Date: Fri, 9 Apr 2021 06:27:18 +0200 Subject: [PATCH 16/63] #1948 Remove extra case and alert count in list pages --- .../controllers/alert/AlertListCtrl.js | 341 +++++++++--------- .../scripts/controllers/case/CaseListCtrl.js | 202 ++++------- frontend/app/views/partials/alert/list.html | 194 ++++++---- .../app/views/partials/case/case.list.html | 229 ++++++++---- .../app/views/partials/case/list/toolbar.html | 26 +- 5 files changed, 535 insertions(+), 457 deletions(-) diff --git a/frontend/app/scripts/controllers/alert/AlertListCtrl.js b/frontend/app/scripts/controllers/alert/AlertListCtrl.js index 13f80ce024..da5db5a052 100755 --- a/frontend/app/scripts/controllers/alert/AlertListCtrl.js +++ b/frontend/app/scripts/controllers/alert/AlertListCtrl.js @@ -1,8 +1,8 @@ -(function() { +(function () { 'use strict'; angular.module('theHiveControllers') - .controller('AlertListCtrl', function($rootScope, $scope, $q, $state, $uibModal, TagSrv, StreamQuerySrv, CaseTemplateSrv, ModalUtilsSrv, AlertingSrv, NotificationSrv, FilteringSrv, CortexSrv, Severity, VersionSrv) { + .controller('AlertListCtrl', function ($rootScope, $scope, $q, $state, $uibModal, TagSrv, StreamQuerySrv, CaseTemplateSrv, ModalUtilsSrv, AlertingSrv, NotificationSrv, FilteringSrv, CortexSrv, Severity, VersionSrv) { var self = this; self.urls = VersionSrv.mispUrls(); @@ -21,7 +21,7 @@ self.lastSearch = null; self.responders = null; - this.$onInit = function() { + this.$onInit = function () { self.filtering = new FilteringSrv('alert', 'alert.list', { version: 'v1', defaults: { @@ -37,33 +37,16 @@ }] }); self.filtering.initContext('list') - .then(function() { + .then(function () { self.load(); $scope.$watch('$vm.list.pageSize', function (newValue) { self.filtering.setPageSize(newValue); }); }); - - StreamQuerySrv('v1', [ - {_name: 'listAlert'}, - {_name: 'count'} - ], { - scope: $scope, - rootId: 'any', - objectType: 'alert', - query: { - params: { - name: 'alert-count' - } - }, - onUpdate: function(data) { - self.alertListCount = data; - } - }); }; - self.load = function() { + self.load = function () { var config = { scope: $scope, filter: this.filtering.buildQuery(), @@ -90,22 +73,22 @@ this.canMarkAsRead = AlertingSrv.canMarkAsRead; this.canMarkAsUnread = AlertingSrv.canMarkAsUnread; - this.markAsRead = function(event) { + this.markAsRead = function (event) { var fn = angular.noop; - if(this.canMarkAsRead(event)) { + if (this.canMarkAsRead(event)) { fn = AlertingSrv.markAsRead; } else { fn = AlertingSrv.markAsUnread; } - fn(event._id).then(function( /*data*/ ) { - }, function(response) { + fn(event._id).then(function ( /*data*/) { + }, function (response) { NotificationSrv.error('AlertListCtrl', response.data, response.status); }); }; - self.follow = function(event) { + self.follow = function (event) { var fn = angular.noop; if (event.follow === true) { @@ -114,13 +97,13 @@ fn = AlertingSrv.follow; } - fn(event._id).then(function( /*data*/ ) { - }, function(response) { + fn(event._id).then(function ( /*data*/) { + }, function (response) { NotificationSrv.error('AlertListCtrl', response.data, response.status); }); }; - self.bulkFollow = function(follow) { + self.bulkFollow = function (follow) { var ids = _.pluck(self.selection, 'id'); var fn = angular.noop; @@ -130,59 +113,59 @@ fn = AlertingSrv.unfollow; } - var promises = _.map(ids, function(id) { + var promises = _.map(ids, function (id) { return fn(id); }); - $q.all(promises).then(function( /*response*/ ) { + $q.all(promises).then(function ( /*response*/) { NotificationSrv.log('The selected events have been ' + (follow ? 'followed' : 'unfollowed'), 'success'); - }, function(response) { + }, function (response) { NotificationSrv.error('AlertListCtrl', response.data, response.status); }); }; - self.bulkMarkAsRead = function(markAsReadFlag) { + self.bulkMarkAsRead = function (markAsReadFlag) { var ids = _.pluck(self.selection, '_id'); var fn = angular.noop; var markAsRead = markAsReadFlag && this.canMarkAsRead(self.selection[0]); - if(markAsRead) { + if (markAsRead) { fn = AlertingSrv.markAsRead; } else { fn = AlertingSrv.markAsUnread; } - var promises = _.map(ids, function(id) { + var promises = _.map(ids, function (id) { return fn(id); }); - $q.all(promises).then(function( /*response*/ ) { + $q.all(promises).then(function ( /*response*/) { self.list.update(); NotificationSrv.log('The selected events have been ' + (markAsRead ? 'marked as read' : 'marked as unread'), 'success'); - }, function(response) { + }, function (response) { NotificationSrv.error('AlertListCtrl', response.data, response.status); }); }; - self.bulkDelete = function() { + self.bulkDelete = function () { - ModalUtilsSrv.confirm('Remove Alerts', 'Are you sure you want to delete the selected Alerts?', { - okText: 'Yes, remove them', - flavor: 'danger' - }).then(function() { - var ids = _.pluck(self.selection, '_id'); + ModalUtilsSrv.confirm('Remove Alerts', 'Are you sure you want to delete the selected Alerts?', { + okText: 'Yes, remove them', + flavor: 'danger' + }).then(function () { + var ids = _.pluck(self.selection, '_id'); - AlertingSrv.bulkRemove(ids) - .then(function(/*response*/) { - NotificationSrv.log('The selected events have been deleted', 'success'); - }) - .catch(function(response) { - NotificationSrv.error('AlertListCtrl', response.data, response.status); - }); - }); + AlertingSrv.bulkRemove(ids) + .then(function (/*response*/) { + NotificationSrv.log('The selected events have been deleted', 'success'); + }) + .catch(function (response) { + NotificationSrv.error('AlertListCtrl', response.data, response.status); + }); + }); }; - self.import = function(event) { + self.import = function (event) { var modalInstance = $uibModal.open({ templateUrl: 'views/partials/alert/event.dialog.html', controller: 'AlertEventCtrl', @@ -190,21 +173,21 @@ size: 'max', resolve: { event: event, - templates: function() { + templates: function () { return CaseTemplateSrv.list(); }, readonly: false } }); - modalInstance.result.catch(function(err) { - if(err && !_.isString(err)) { + modalInstance.result.catch(function (err) { + if (err && !_.isString(err)) { NotificationSrv.error('AlertListCtrl', err.data, err.status); } }); }; - self.resetSelection = function() { + self.resetSelection = function () { if (self.menu.selectAll) { self.selectAll(); } else { @@ -214,39 +197,39 @@ } }; - this.getResponders = function(event, force) { - if(!force && this.responders !== null) { - return; + this.getResponders = function (event, force) { + if (!force && this.responders !== null) { + return; } this.responders = null; CortexSrv.getResponders('alert', event._id) - .then(function(responders) { - self.responders = responders; - return CortexSrv.promntForResponder(responders); - }) - .then(function(response) { - if(response && _.isString(response)) { - NotificationSrv.log(response, 'warning'); - } else { - return CortexSrv.runResponder(response.id, response.name, 'alert', _.pick(event, '_id', 'tlp')); - } - }) - .then(function(response){ - NotificationSrv.log(['Responder', response.data.responderName, 'started successfully on alert', event.title].join(' '), 'success'); - }) - .catch(function(err) { - if(err && !_.isString(err)) { - NotificationSrv.error('AlertList', err.data, err.status); - } - }); - }; - - self.cancel = function() { + .then(function (responders) { + self.responders = responders; + return CortexSrv.promntForResponder(responders); + }) + .then(function (response) { + if (response && _.isString(response)) { + NotificationSrv.log(response, 'warning'); + } else { + return CortexSrv.runResponder(response.id, response.name, 'alert', _.pick(event, '_id', 'tlp')); + } + }) + .then(function (response) { + NotificationSrv.log(['Responder', response.data.responderName, 'started successfully on alert', event.title].join(' '), 'success'); + }) + .catch(function (err) { + if (err && !_.isString(err)) { + NotificationSrv.error('AlertList', err.data, err.status); + } + }); + }; + + self.cancel = function () { self.modalInstance.close(); }; - self.updateMenu = function() { + self.updateMenu = function () { var temp = _.uniq(_.pluck(self.selection, 'follow')); self.menu.unfollow = temp.length === 1 && temp[0] === true; @@ -267,11 +250,11 @@ self.menu.delete = temp.length === 0; }; - self.select = function(event) { + self.select = function (event) { if (event.selected) { self.selection.push(event); } else { - self.selection = _.reject(self.selection, function(item) { + self.selection = _.reject(self.selection, function (item) { return item._id === event._id; }); } @@ -280,9 +263,9 @@ }; - self.selectAll = function() { + self.selectAll = function () { var selected = self.menu.selectAll; - _.each(self.list.values, function(item) { + _.each(self.list.values, function (item) { item.selected = selected; }); @@ -296,109 +279,109 @@ }; - self.createNewCase = function() { + self.createNewCase = function () { var alertIds = _.pluck(self.selection, '_id'); CaseTemplateSrv.list() - .then(function(templates) { - - if(!templates || templates.length === 0) { - return $q.resolve(undefined); - } - - // Open template selection dialog - var modal = $uibModal.open({ - templateUrl: 'views/partials/case/case.templates.selector.html', - controller: 'CaseTemplatesDialogCtrl', - controllerAs: 'dialog', - size: 'lg', - resolve: { - templates: function(){ - return templates; - }, - uiSettings: ['UiSettingsSrv', function(UiSettingsSrv) { - return UiSettingsSrv.all(); - }] - } - }); - - return modal.result; - }) - .then(function(template) { - - // Open case creation dialog - var modal = $uibModal.open({ - templateUrl: 'views/partials/case/case.creation.html', - controller: 'CaseCreationCtrl', - size: 'lg', - resolve: { - template: template - } - }); - - return modal.result; - }) - .then(function(createdCase) { - // Bulk merge the selected alerts into the created case - NotificationSrv.log('New case has been created', 'success'); - - return AlertingSrv.bulkMergeInto(alertIds, createdCase.id); - }) - .then(function(response) { - if(alertIds.length === 1) { - NotificationSrv.log(alertIds.length + ' Alert has been merged into the newly created case.', 'success'); - } else { - NotificationSrv.log(alertIds.length + ' Alert(s) have been merged into the newly created case.', 'success'); - } - - $rootScope.$broadcast('alert:event-imported'); - - $state.go('app.case.details', { - caseId: response.data.id - }); - }) - .catch(function(err) { - if(err && !_.isString(err)) { - NotificationSrv.error('AlertEventCtrl', err.data, err.status); - } - }); - - }; - - self.mergeInCase = function() { + .then(function (templates) { + + if (!templates || templates.length === 0) { + return $q.resolve(undefined); + } + + // Open template selection dialog + var modal = $uibModal.open({ + templateUrl: 'views/partials/case/case.templates.selector.html', + controller: 'CaseTemplatesDialogCtrl', + controllerAs: 'dialog', + size: 'lg', + resolve: { + templates: function () { + return templates; + }, + uiSettings: ['UiSettingsSrv', function (UiSettingsSrv) { + return UiSettingsSrv.all(); + }] + } + }); + + return modal.result; + }) + .then(function (template) { + + // Open case creation dialog + var modal = $uibModal.open({ + templateUrl: 'views/partials/case/case.creation.html', + controller: 'CaseCreationCtrl', + size: 'lg', + resolve: { + template: template + } + }); + + return modal.result; + }) + .then(function (createdCase) { + // Bulk merge the selected alerts into the created case + NotificationSrv.log('New case has been created', 'success'); + + return AlertingSrv.bulkMergeInto(alertIds, createdCase.id); + }) + .then(function (response) { + if (alertIds.length === 1) { + NotificationSrv.log(alertIds.length + ' Alert has been merged into the newly created case.', 'success'); + } else { + NotificationSrv.log(alertIds.length + ' Alert(s) have been merged into the newly created case.', 'success'); + } + + $rootScope.$broadcast('alert:event-imported'); + + $state.go('app.case.details', { + caseId: response.data.id + }); + }) + .catch(function (err) { + if (err && !_.isString(err)) { + NotificationSrv.error('AlertEventCtrl', err.data, err.status); + } + }); + + }; + + self.mergeInCase = function () { var caseModal = $uibModal.open({ templateUrl: 'views/partials/case/case.merge.html', controller: 'CaseMergeModalCtrl', controllerAs: 'dialog', size: 'lg', resolve: { - source: function() { + source: function () { return self.event; }, - title: function() { + title: function () { return 'Merge selected Alert(s)'; }, - prompt: function() { + prompt: function () { return 'the ' + self.selection.length + ' selected Alert(s)'; } } }); - caseModal.result.then(function(selectedCase) { + caseModal.result.then(function (selectedCase) { return AlertingSrv.bulkMergeInto(_.pluck(self.selection, '_id'), selectedCase._id); }) - .then(function(response) { - $rootScope.$broadcast('alert:event-imported'); + .then(function (response) { + $rootScope.$broadcast('alert:event-imported'); - $state.go('app.case.details', { - caseId: response.data.id + $state.go('app.case.details', { + caseId: response.data.id + }); + }) + .catch(function (err) { + if (err && !_.isString(err)) { + NotificationSrv.error('AlertEventCtrl', err.data, err.status); + } }); - }) - .catch(function(err) { - if(err && !_.isString(err)) { - NotificationSrv.error('AlertEventCtrl', err.data, err.status); - } - }); }; this.filter = function () { @@ -428,47 +411,47 @@ this.search(); }; - this.filterByStatus = function(flag) { + this.filterByStatus = function (flag) { self.filtering.clearFilters() - .then(function(){ + .then(function () { self.addFilterValue('imported', flag); }); }; - this.filterByNewAndUpdated = function() { + this.filterByNewAndUpdated = function () { self.filtering.clearFilters() - .then(function(){ + .then(function () { // TODO nadouani: how to support updated alerts self.addFilterValue('imported', true); }); }; - this.filterBySeverity = function(numericSev) { + this.filterBySeverity = function (numericSev) { self.addFilterValue('severity', Severity.values[numericSev]); }; - this.filterBy = function(field, value) { + this.filterBy = function (field, value) { self.filtering.clearFilters() - .then(function(){ + .then(function () { self.addFilterValue(field, value); }); }; - this.sortBy = function(sort) { + this.sortBy = function (sort) { self.list.sort = sort; self.list.update(); self.filtering.setSort(sort); }; - this.sortByField = function(field) { + this.sortByField = function (field) { var context = this.filtering.context; var currentSort = Array.isArray(context.sort) ? context.sort[0] : context.sort; var sort = null; - if(currentSort.substr(1) !== field) { + if (currentSort.substr(1) !== field) { sort = ['+' + field]; } else { - sort = [(currentSort === '+' + field) ? '-'+field : '+'+field]; + sort = [(currentSort === '+' + field) ? '-' + field : '+' + field]; } self.list.sort = sort; diff --git a/frontend/app/scripts/controllers/case/CaseListCtrl.js b/frontend/app/scripts/controllers/case/CaseListCtrl.js index 1f1cea1a8f..16f3bdbcb3 100644 --- a/frontend/app/scripts/controllers/case/CaseListCtrl.js +++ b/frontend/app/scripts/controllers/case/CaseListCtrl.js @@ -1,4 +1,4 @@ -(function() { +(function () { 'use strict'; angular.module('theHiveControllers') .controller('CaseListCtrl', CaseListCtrl) @@ -19,7 +19,7 @@ selectAll: false }; - this.$onInit = function() { + this.$onInit = function () { self.filtering = new FilteringSrv('case', 'case.list', { version: 'v1', defaults: { @@ -41,7 +41,7 @@ }); self.filtering.initContext('list') - .then(function() { + .then(function () { self.load(); $scope.$watch('$vm.list.pageSize', function (newValue) { @@ -49,55 +49,9 @@ }); }); - - // Case stats to build quick filter menu - StreamQuerySrv('v1', [ - { - _name: 'listCase' - }, - { - _name: 'aggregation', - _agg: 'field', - _field: 'status', - _select: [ - {_agg: 'count'} - ] - } - ], { - scope: $scope, - rootId: 'any', - objectType: 'case', - query: { - params: { - name: 'case-status-stats' - } - }, - onUpdate: function(updates) { - self.caseStats = updates; - } - }); - - // Case total - StreamQuerySrv('v1', [ - {_name: 'listCase'}, - {_name: 'count'} - ], { - scope: $scope, - rootId: 'any', - objectType: 'case', - query: { - params: { - name: 'case-count-stats' - } - }, - onUpdate: function(updates) { - self.caseCount = updates; - } - }); - }; - this.load = function() { + this.load = function () { this.list = new PaginatedQuerySrv({ name: 'cases', @@ -110,16 +64,16 @@ pageSize: self.filtering.context.pageSize, filter: this.filtering.buildQuery(), operations: [ - {'_name': 'listCase'} + { '_name': 'listCase' } ], extraData: ['observableStats', 'taskStats', 'procedureCount', 'isOwner', 'shareCount', 'permissions', 'actionRequired'], - onUpdate: function() { + onUpdate: function () { self.resetSelection(); } }); }; - self.resetSelection = function() { + self.resetSelection = function () { if (self.menu.selectAll) { self.selectAll(); } else { @@ -129,7 +83,7 @@ } }; - self.updateMenu = function() { + self.updateMenu = function () { // Handle flag/unflag menu items var temp = _.uniq(_.pluck(self.selection, 'flag')); self.menu.unflag = temp.length === 1 && temp[0] === true; @@ -143,28 +97,28 @@ self.menu.delete = self.selection.length > 0; }; - self.select = function(caze) { + self.select = function (caze) { if (caze.selected) { self.selection.push(caze); } else { - self.selection = _.reject(self.selection, function(item) { + self.selection = _.reject(self.selection, function (item) { return item._id === caze._id; }); } self.updateMenu(); }; - self.selectAll = function() { + self.selectAll = function () { var selected = self.menu.selectAll; - _.each(self.list.values, function(item) { - if(SecuritySrv.checkPermissions(['manageCase'], item.extraData.permissions)) { + _.each(self.list.values, function (item) { + if (SecuritySrv.checkPermissions(['manageCase'], item.extraData.permissions)) { item.selected = selected; } }); if (selected) { - self.selection = _.filter(self.list.values, function(item) { + self.selection = _.filter(self.list.values, function (item) { return !!item.selected; }); } else { @@ -209,9 +163,9 @@ this.search(); }; - this.filterMyCases = function() { + this.filterMyCases = function () { this.filtering.clearFilters() - .then(function() { + .then(function () { var currentUser = AuthenticationSrv.currentUser; self.filtering.addFilter({ field: 'assignee', @@ -227,9 +181,9 @@ }); }; - this.filterMyOrgCases = function() { + this.filterMyOrgCases = function () { this.filtering.clearFilters() - .then(function() { + .then(function () { var currentUser = AuthenticationSrv.currentUser; self.filtering.addFilter({ field: 'owningOrganisation', @@ -246,9 +200,9 @@ }); }; - this.filterSharedWithMyOrg = function() { + this.filterSharedWithMyOrg = function () { this.filtering.clearFilters() - .then(function() { + .then(function () { var currentUser = AuthenticationSrv.currentUser; self.filtering.addFilter({ field: 'owningOrganisation', @@ -265,9 +219,9 @@ }); }; - this.filterMyOpenCases = function() { + this.filterMyOpenCases = function () { this.filtering.clearFilters() - .then(function() { + .then(function () { var currentUser = AuthenticationSrv.currentUser; self.filtering.addFilter({ field: 'assignee', @@ -283,36 +237,36 @@ }); }; - this.filterByStatus = function(status) { + this.filterByStatus = function (status) { this.filtering.clearFilters() - .then(function() { + .then(function () { self.addFilterValue('status', status); }); }; - this.filterByResolutionStatus = function(status) { + this.filterByResolutionStatus = function (status) { this.filtering.clearFilters() - .then(function() { + .then(function () { self.filtering.addFilterValue('resolutionStatus', status); self.addFilterValue('status', 'Resolved'); }); }; - this.sortBy = function(sort) { + this.sortBy = function (sort) { this.list.sort = sort; this.list.update(); this.filtering.setSort(sort); }; - this.sortByField = function(field) { + this.sortByField = function (field) { var context = this.filtering.context; var currentSort = Array.isArray(context.sort) ? _.without(context.sort, '-flag', '+flag')[0] : context.sort; var sort = null; - if(currentSort.substr(1) !== field) { + if (currentSort.substr(1) !== field) { sort = ['-flag', '+' + field]; } else { - sort = ['-flag', (currentSort === '+' + field) ? '-'+field : '+'+field]; + sort = ['-flag', (currentSort === '+' + field) ? '-' + field : '+' + field]; } self.list.sort = sort; @@ -320,20 +274,20 @@ self.filtering.setSort(sort); }; - this.bulkFlag = function(flag) { + this.bulkFlag = function (flag) { var ids = _.pluck(self.selection, '_id'); - return CaseSrv.bulkUpdate(ids, {flag: flag}) - .then(function(/*responses*/) { + return CaseSrv.bulkUpdate(ids, { flag: flag }) + .then(function (/*responses*/) { NotificationSrv.log('Selected cases have been updated successfully', 'success'); }) - .catch(function(err) { + .catch(function (err) { NotificationSrv.error('Bulk flag cases', err.data, err.status); }); } - this.bulkEdit = function() { + this.bulkEdit = function () { var modal = $uibModal.open({ animation: 'true', templateUrl: 'views/partials/case/case.update.html', @@ -341,22 +295,22 @@ controllerAs: '$dialog', size: 'lg', resolve: { - selection: function() { + selection: function () { return self.selection; } } }); - modal.result.then(function(operations) { - $q.all(_.map(operations, function(operation) { + modal.result.then(function (operations) { + $q.all(_.map(operations, function (operation) { return CaseSrv.bulkUpdate(operation.ids, operation.patch); - })).then(function(/*responses*/) { + })).then(function (/*responses*/) { NotificationSrv.log('Selected cases have been updated successfully', 'success'); }); }); }; - this.bulkRemove = function() { + this.bulkRemove = function () { var modal = $uibModal.open({ animation: 'true', templateUrl: 'views/partials/case/case.bulk.delete.confirm.html', @@ -364,49 +318,49 @@ controllerAs: '$dialog', size: 'lg', resolve: { - selection: function() { + selection: function () { return self.selection; } } }); - modal.result.catch(function(err) { - if(err && !_.isString(err)) { + modal.result.catch(function (err) { + if (err && !_.isString(err)) { NotificationSrv.error('Case Remove', err.data, err.status); } }) } - this.bulkReopen = function() { + this.bulkReopen = function () { return ModalUtilsSrv.confirm('Reopen cases', 'Are you sure you want to reopen the selected cases?', { okText: 'Yes, proceed' - }).then(function() { + }).then(function () { var ids = _.pluck(self.selection, '_id'); - return CaseSrv.bulkUpdate(ids, {status: 'Open'}) - .then(function(/*responses*/) { + return CaseSrv.bulkUpdate(ids, { status: 'Open' }) + .then(function (/*responses*/) { NotificationSrv.log('Selected cases have been reopened successfully', 'success'); }) - .catch(function(err) { + .catch(function (err) { NotificationSrv.error('Bulk reopen cases', err.data, err.status); }); }); } - this.closeCase = function(caze) { + this.closeCase = function (caze) { var scope = $rootScope.$new(); scope.CaseResolutionStatus = CaseResolutionStatus; scope.CaseImpactStatus = CaseImpactStatus; scope.caseId = caze._id; - scope.updateField = function(data) { + scope.updateField = function (data) { return CaseSrv.update({ caseId: caze._id }, data) - .$promise - .then(function(/*response*/) { - return caze; - }); + .$promise + .then(function (/*response*/) { + return caze; + }); }; var modal = $uibModal.open({ @@ -415,63 +369,63 @@ controller: 'CaseCloseModalCtrl', size: 'lg', resolve: { - caze: function() { + caze: function () { return angular.copy(caze); } } }) - return modal.result.catch(function(err){ - if(err && !_.isString(err)) { + return modal.result.catch(function (err) { + if (err && !_.isString(err)) { NotificationSrv.error('Case bulk close', err.data, err.status); } }); } - $scope.updateField = function(data) { + $scope.updateField = function (data) { return CaseSrv.update({ caseId: caseId }, data).$promise; }; - this.bulkClose = function() { - return ModalUtilsSrv.confirm('Close cases', 'Are you sure you want to close the selected ' + self.selection.length+' case(s)?', { + this.bulkClose = function () { + return ModalUtilsSrv.confirm('Close cases', 'Are you sure you want to close the selected ' + self.selection.length + ' case(s)?', { okText: 'Yes, proceed' - }).then(function() { - return self.selection.reduce(function(initialPromise, nextCase) { + }).then(function () { + return self.selection.reduce(function (initialPromise, nextCase) { return initialPromise .then(self.closeCase(nextCase)); }, $q.resolve()); - }).catch(function(err){ - if(err && !_.isString(err)) { + }).catch(function (err) { + if (err && !_.isString(err)) { NotificationSrv.error('Case bulk close', err.data, err.status); } }); } - this.getCaseResponders = function(caze, force) { + this.getCaseResponders = function (caze, force) { if (!force && this.caseResponders !== null) { return; } self.caseResponders = null; CortexSrv.getResponders('case', caze._id) - .then(function(responders){ + .then(function (responders) { self.caseResponders = responders; return CortexSrv.promntForResponder(responders); }) - .then(function(response) { - if(response && _.isString(response)) { + .then(function (response) { + if (response && _.isString(response)) { NotificationSrv.log(response, 'warning'); } else { return CortexSrv.runResponder(response.id, response.name, 'case', _.pick(caze, '_id', 'tlp', 'pap')); } }) - .then(function(response){ + .then(function (response) { NotificationSrv.log(['Responder', response.data.responderName, 'started successfully on case', caze.title].join(' '), 'success'); }) - .catch(function(err) { - if(err && !_.isString(err)) { + .catch(function (err) { + if (err && !_.isString(err)) { NotificationSrv.error('CaseList', err.data, err.status); } }); @@ -486,28 +440,28 @@ this.typedCount = undefined; this.loading = false; - this.ok = function() { + this.ok = function () { $uibModalInstance.close(); } - this.cancel = function() { + this.cancel = function () { $uibModalInstance.dismiss(); } - this.confirm = function() { + this.confirm = function () { self.loading = true; - var promises = _.map(self.selection, function(caze) { + var promises = _.map(self.selection, function (caze) { return CaseSrv.forceRemove({ caseId: caze._id }).$promise; }); $q.all(promises) - .then(function(responses) { + .then(function (responses) { self.loading = false; NotificationSrv.log('Cases have been deleted successfully: ' + responses.length, 'success'); $uibModalInstance.close(); }) - .catch(function(errors) { + .catch(function (errors) { self.loading = false; - _.each(errors, function(err) { + _.each(errors, function (err) { NotificationSrv.error('Bulk delete cases', err.data, err.status); }); }) diff --git a/frontend/app/views/partials/alert/list.html b/frontend/app/views/partials/alert/list.html index 7282826230..e66f05426d 100644 --- a/frontend/app/views/partials/alert/list.html +++ b/frontend/app/views/partials/alert/list.html @@ -2,20 +2,21 @@
-

List of alerts ({{$vm.list.total || 0}} of {{$vm.alertListCount}})

+

List of alerts ({{$vm.list.values.length || 0}} of {{$vm.list.total}})

-
+
-
+
-
@@ -33,23 +34,30 @@

List of alerts ({{$vm.list.total || 0}} of {{$vm.alertList - + - - Reference - - - - + + Reference + + + + - - Type - - - - + + Type + + + + Imported @@ -58,47 +66,65 @@

List of alerts ({{$vm.list.total || 0}} of {{$vm.alertList Read - Title + Title - - Source - - - - + + Source + + + + - - Severity - - - - + + Severity + + + + - Observables + Observables Dates - + O. - - - + + + - + C. - - - + + + - + U. - - - + + + @@ -115,7 +141,8 @@

List of alerts ({{$vm.list.total || 0}} of {{$vm.alertList {{::event.sourceRef}} - + @@ -123,68 +150,96 @@

List of alerts ({{$vm.list.total || 0}} of {{$vm.alertList - {{::event.type}} + {{::event.type}} - {{event.caseId ? 'Imported' : 'New'}} + {{event.caseId ? + 'Imported' : 'New'}} - {{event.read ? 'Read' : 'Unread'}} + {{event.read ? 'Read' : + 'Unread'}}
{{::event.title}} - + {{::event.title}}
- {{event.source}} + {{event.source}} + -
+
{{::event.observableCount || 0}} -
- O. {{event.date | shortDate}} + -
- C. {{event._createdAt | shortDate}} + -
- U. {{event._updatedAt | shortDate}} +
- +
@@ -195,7 +250,8 @@

List of alerts ({{$vm.list.total || 0}} of {{$vm.alertList
- None + None
@@ -205,7 +261,9 @@

List of alerts ({{$vm.list.total || 0}} of {{$vm.alertList - + + diff --git a/frontend/app/views/partials/case/case.list.html b/frontend/app/views/partials/case/case.list.html index 04dc3a3e6e..2ca39cecd8 100644 --- a/frontend/app/views/partials/case/case.list.html +++ b/frontend/app/views/partials/case/case.list.html @@ -2,7 +2,7 @@
-

List of cases ({{$vm.list.total || 0}} of {{$vm.caseCount}})

+

List of cases ({{$vm.list.values.length || 0}} of {{$vm.list.total}})

@@ -14,15 +14,16 @@

List of cases ({{$vm.list.total || 0}} of {{$vm.caseCount}
-
+
-
+
-
@@ -40,68 +41,101 @@

List of cases ({{$vm.list.total || 0}} of {{$vm.caseCount} - + - + Status - - - + + + - + # Number - - - + + + - + Title - - - + + + - + Severity - - - + + + Details - + Assignee - - - + + + Dates - + S. - - - + + + - + C. - - - + + + - + U. - - - + + + @@ -109,62 +143,81 @@

List of cases ({{$vm.list.total || 0}} of {{$vm.caseCount} - - + + - +
{{currentCase.status === 'Resolved' ? 'Closed' : currentCase.status}} + }[currentCase.status]" + ng-click="$vm.addFilterValue('status', currentCase.status)">{{currentCase.status + === 'Resolved' ? 'Closed' : currentCase.status}}
- - + +
- None - + None + +
- +
- (Closed at {{currentCase.endDate | shortDate}} as {{$vm.CaseResolutionStatus[currentCase.resolutionStatus]}}) + (Closed at {{currentCase.endDate | shortDate}} as {{$vm.CaseResolutionStatus[currentCase.resolutionStatus]}})
+

-
+
-
+ - + @@ -211,20 +273,31 @@

List of cases ({{$vm.list.total || 0}} of {{$vm.caseCount} -
- S. {{currentCase.startDate | shortDate}} + -
- C. {{currentCase._createdAt | shortDate}} + -
- U. {{currentCase._updatedAt | shortDate}} + - - + + diff --git a/frontend/app/views/partials/case/list/toolbar.html b/frontend/app/views/partials/case/list/toolbar.html index d4a04f62e1..0955d90856 100644 --- a/frontend/app/views/partials/case/list/toolbar.html +++ b/frontend/app/views/partials/case/list/toolbar.html @@ -3,8 +3,11 @@
-
-
-
From 58d5c47bf459c52b7851100e968c763e96267b10 Mon Sep 17 00:00:00 2001 From: Nabil Adouani Date: Fri, 9 Apr 2021 09:57:41 +0200 Subject: [PATCH 17/63] #1508 Add blank text to updatable-user component and use it in tasks section --- .../app/scripts/directives/updatableUser.js | 27 +-- .../app/views/directives/updatable-user.html | 3 +- .../app/views/partials/case/case.tasks.html | 195 +++++++++++------- .../views/partials/case/case.tasks.item.html | 86 ++++---- 4 files changed, 178 insertions(+), 133 deletions(-) diff --git a/frontend/app/scripts/directives/updatableUser.js b/frontend/app/scripts/directives/updatableUser.js index 1722831dd6..40a5e4c258 100644 --- a/frontend/app/scripts/directives/updatableUser.js +++ b/frontend/app/scripts/directives/updatableUser.js @@ -1,25 +1,25 @@ -(function() { +(function () { 'use strict'; angular.module('theHiveDirectives') - .directive('updatableUser', function(UserSrv, QuerySrv, UtilsSrv, AuthenticationSrv, NotificationSrv) { + .directive('updatableUser', function (UserSrv, QuerySrv, UtilsSrv, AuthenticationSrv, NotificationSrv) { return { restrict: 'E', - link: function(scope, element, attrs, ctrl, transclude) { + link: function (scope, element, attrs, ctrl, transclude) { var cached = false; UtilsSrv.updatableLink(scope, element, attrs, ctrl, transclude); - scope.setValue = function(value) { + scope.setValue = function (value) { scope.value = value; }; scope.getUserInfo = UserSrv.getCache; - scope.$watch('updatable.updating', function(value) { + scope.$watch('updatable.updating', function (value) { - if(value === true && !cached) { + if (value === true && !cached) { var assignableUsers = []; - if(_.isFunction(scope.query)) { + if (_.isFunction(scope.query)) { assignableUsers = scope.query.apply(this, scope.queryParams); } else { assignableUsers = scope.query; @@ -32,12 +32,12 @@ }, sort: ['+name'] }) - .then(function(users) { - scope.userList = users; - }) - .catch(function(err) { - NotificationSrv.error('Fetching users', err.data, err.status); - }); + .then(function (users) { + scope.userList = users; + }) + .catch(function (err) { + NotificationSrv.error('Fetching users', err.data, err.status); + }); cached = true; } @@ -47,6 +47,7 @@ scope: { value: '=?', query: '=', + blankText: '@', queryParams: '=', onUpdate: '&', active: '=?', diff --git a/frontend/app/views/directives/updatable-user.html b/frontend/app/views/directives/updatable-user.html index a64a013626..fd9d33a7c6 100644 --- a/frontend/app/views/directives/updatable-user.html +++ b/frontend/app/views/directives/updatable-user.html @@ -1,6 +1,7 @@
- + {{blankText || 'Not Specified'}} + diff --git a/frontend/app/views/partials/case/case.tasks.html b/frontend/app/views/partials/case/case.tasks.html index 5eb8fd57e3..220c573b4d 100755 --- a/frontend/app/views/partials/case/case.tasks.html +++ b/frontend/app/views/partials/case/case.tasks.html @@ -1,6 +1,7 @@
-
+
@@ -11,8 +12,7 @@

-
@@ -28,24 +28,25 @@

-
- -
+
+ +
-
- - - - - -
+
+ + + + + +
@@ -61,7 +62,8 @@

- + Group @@ -69,50 +71,57 @@

Date Assignee - Actions + Actions - + - + {{task.group}} -
- Closed after {{(task.endDate - task.startDate) | amDurationFormat : 'milliseconds'}} -
-
- Started -
+ + + + + {{task.title}} + +

+
+ Closed after {{(task.endDate - task.startDate) | amDurationFormat : + 'milliseconds'}} +
+
+ Started +
@@ -128,10 +137,12 @@

{{task.startDate | shortDate}} - + - + @@ -148,16 +159,21 @@

- + - + - + - + @@ -189,7 +205,8 @@

- + Group @@ -197,55 +214,65 @@

Date Assignee - Actions + Actions - + - + + - + {{task.group}} - -
- Closed after {{(task.endDate - task.startDate) | amDurationFormat : 'milliseconds'}} -
-
- Started -
+ + + + + + + + {{task.title}} + +

+
+ Closed after {{(task.endDate - task.startDate) | amDurationFormat : + 'milliseconds'}} +
+
+ Started +
- + ({{task.extraData.shareCount || 0}}) @@ -255,10 +282,14 @@

{{task.startDate | shortDate}} - + - + @@ -270,21 +301,27 @@

- + - + - + - + - + diff --git a/frontend/app/views/partials/case/case.tasks.item.html b/frontend/app/views/partials/case/case.tasks.item.html index 6190531c0e..ef8d9ad1ad 100644 --- a/frontend/app/views/partials/case/case.tasks.item.html +++ b/frontend/app/views/partials/case/case.tasks.item.html @@ -6,7 +6,8 @@

This task requires an action from your organisation. - +
@@ -55,7 +56,8 @@

- + | @@ -88,7 +90,7 @@

Title
- +
{{task.title}} @@ -98,7 +100,7 @@

Group
- +
{{task.group}} @@ -108,7 +110,8 @@

Assignee
- +
@@ -120,7 +123,7 @@

Start date
- +
@@ -162,14 +165,15 @@

-

Description

-
- -
-
-
- Not specified -
+

Description

+
+ +
+
+
+ Not specified +
@@ -206,17 +210,24 @@

Task logs

- -
+ +
- - + - -
-
-
- -
+

Task sharing

+ +
+ +
From 1d2d0dff798fa12c3d6d04fc1baaaacaaf83bc3d Mon Sep 17 00:00:00 2001 From: To-om Date: Fri, 9 Apr 2021 11:18:29 +0200 Subject: [PATCH 18/63] #1946 Fix stream --- .../org/thp/thehive/services/AuditSrv.scala | 92 +++++++++---------- 1 file changed, 44 insertions(+), 48 deletions(-) diff --git a/thehive/app/org/thp/thehive/services/AuditSrv.scala b/thehive/app/org/thp/thehive/services/AuditSrv.scala index a88bf5573f..39d89adc80 100644 --- a/thehive/app/org/thp/thehive/services/AuditSrv.scala +++ b/thehive/app/org/thp/thehive/services/AuditSrv.scala @@ -36,33 +36,33 @@ class AuditSrv @Inject() ( eventSrv: EventSrv, db: Database ) extends VertexSrv[Audit] { auditSrv => - lazy val userSrv: UserSrv = userSrvProvider.get - val auditUserSrv = new EdgeSrv[AuditUser, Audit, User] - val auditedSrv = new EdgeSrv[Audited, Audit, Product] - val auditContextSrv = new EdgeSrv[AuditContext, Audit, Product] - val `case` = new SelfContextObjectAudit[Case] - val task = new SelfContextObjectAudit[Task] - val observable = new SelfContextObjectAudit[Observable] - val log = new ObjectAudit[Log, Task] - val caseTemplate = new SelfContextObjectAudit[CaseTemplate] - val taskInTemplate = new ObjectAudit[Task, CaseTemplate] - val alert = new AlertAudit - val share = new ShareAudit - val observableInAlert = new ObjectAudit[Observable, Alert] - val user = new UserAudit - val dashboard = new SelfContextObjectAudit[Dashboard] - val organisation = new SelfContextObjectAudit[Organisation] - val profile = new SelfContextObjectAudit[Profile] - val pattern = new SelfContextObjectAudit[Pattern] - val procedure = new ObjectAudit[Procedure, Case] - val customField = new SelfContextObjectAudit[CustomField] - val page = new SelfContextObjectAudit[Page] - private val pendingAuditsLock = new Object - private val transactionAuditIdsLock = new Object - private val unauditedTransactionsLock = new Object - private var pendingAudits: Map[AnyRef, PendingAudit] = Map.empty - private var transactionAuditIds: List[(AnyRef, EntityId)] = Nil - private var unauditedTransactions: Set[AnyRef] = Set.empty + lazy val userSrv: UserSrv = userSrvProvider.get + val auditUserSrv = new EdgeSrv[AuditUser, Audit, User] + val auditedSrv = new EdgeSrv[Audited, Audit, Product] + val auditContextSrv = new EdgeSrv[AuditContext, Audit, Product] + val `case` = new SelfContextObjectAudit[Case] + val task = new SelfContextObjectAudit[Task] + val observable = new SelfContextObjectAudit[Observable] + val log = new ObjectAudit[Log, Task] + val caseTemplate = new SelfContextObjectAudit[CaseTemplate] + val taskInTemplate = new ObjectAudit[Task, CaseTemplate] + val alert = new AlertAudit + val share = new ShareAudit + val observableInAlert = new ObjectAudit[Observable, Alert] + val user = new UserAudit + val dashboard = new SelfContextObjectAudit[Dashboard] + val organisation = new SelfContextObjectAudit[Organisation] + val profile = new SelfContextObjectAudit[Profile] + val pattern = new SelfContextObjectAudit[Pattern] + val procedure = new ObjectAudit[Procedure, Case] + val customField = new SelfContextObjectAudit[CustomField] + val page = new SelfContextObjectAudit[Page] + private val pendingAuditsLock = new Object + private val transactionAuditIdsLock = new Object + private val unauditedTransactionsLock = new Object + private var pendingAudits: Map[Graph, PendingAudit] = Map.empty + private var transactionAuditIds: List[(Graph, EntityId)] = Nil + private var unauditedTransactions: Set[Graph] = Set.empty /** * Gets the main action Audits by ids sorted by date @@ -77,29 +77,26 @@ class AuditSrv @Inject() ( .sort(_.by("_createdAt", order)) def mergeAudits[R](body: => Try[R])(auditCreator: R => Try[Unit])(implicit graph: Graph): Try[R] = { - val tx = db.currentTransactionId(graph) unauditedTransactionsLock.synchronized { - unauditedTransactions = unauditedTransactions + tx + unauditedTransactions = unauditedTransactions + graph } val result = body unauditedTransactionsLock.synchronized { - unauditedTransactions = unauditedTransactions - tx + unauditedTransactions = unauditedTransactions - graph } result.flatMap { r => auditCreator(r).map(_ => r) } } - def flushPendingAudit()(implicit graph: Graph, authContext: AuthContext): Try[Unit] = flushPendingAudit(db.currentTransactionId(graph)) - - def flushPendingAudit(tx: AnyRef)(implicit graph: Graph, authContext: AuthContext): Try[Unit] = { + def flushPendingAudit()(implicit graph: Graph, authContext: AuthContext): Try[Unit] = { logger.debug("Store last audit") - pendingAudits.get(tx).fold[Try[Unit]](Success(())) { p => + pendingAudits.get(graph).fold[Try[Unit]](Success(())) { p => pendingAuditsLock.synchronized { - pendingAudits = pendingAudits - tx + pendingAudits = pendingAudits - graph } - createFromPending(tx, p.audit.copy(mainAction = true), p.context, p.`object`).map { _ => - val (ids, otherTxIds) = transactionAuditIds.partition(_._1 == tx) + createFromPending(p.audit.copy(mainAction = true), p.context, p.`object`).map { _ => + val (ids, otherTxIds) = transactionAuditIds.partition(_._1 == graph) transactionAuditIdsLock.synchronized { transactionAuditIds = otherTxIds } @@ -115,7 +112,7 @@ class AuditSrv @Inject() ( } } - private def createFromPending(tx: AnyRef, audit: Audit, context: Product with Entity, `object`: Option[Product with Entity])(implicit + private def createFromPending(audit: Audit, context: Product with Entity, `object`: Option[Product with Entity])(implicit graph: Graph, authContext: AuthContext ): Try[Unit] = { @@ -127,7 +124,7 @@ class AuditSrv @Inject() ( _ <- `object`.map(auditedSrv.create(Audited(), createdAudit, _)).flip _ = auditContextSrv.create(AuditContext(), createdAudit, context) // this could fail on delete (context doesn't exist) } yield transactionAuditIdsLock.synchronized { - transactionAuditIds = (tx -> createdAudit._id) :: transactionAuditIds + transactionAuditIds = (graph -> createdAudit._id) :: transactionAuditIds } } @@ -135,33 +132,32 @@ class AuditSrv @Inject() ( graph: Graph, authContext: AuthContext ): Try[Unit] = { - def setupCallbacks(tx: AnyRef): Try[Unit] = { + def setupCallbacks(): Try[Unit] = { logger.debug("Setup callbacks for the current transaction") db.addTransactionListener { case Status.ROLLBACK => pendingAuditsLock.synchronized { - pendingAudits = pendingAudits - tx + pendingAudits = pendingAudits - graph } transactionAuditIdsLock.synchronized { - transactionAuditIds = transactionAuditIds.filterNot(_._1 == tx) + transactionAuditIds = transactionAuditIds.filterNot(_._1 == graph) } case _ => } - db.addCallback(() => flushPendingAudit(tx)) + db.addCallback(() => flushPendingAudit()) Success(()) } - val tx = db.currentTransactionId(graph) - if (unauditedTransactions.contains(tx)) { + if (unauditedTransactions.contains(graph)) { logger.debug(s"Audit is disable to the current transaction, $audit ignored.") Success(()) } else { logger.debug(s"Hold $audit, store previous audit if any") - val p = pendingAudits.get(tx) + val p = pendingAudits.get(graph) pendingAuditsLock.synchronized { - pendingAudits = pendingAudits + (tx -> PendingAudit(audit, context, `object`)) + pendingAudits = pendingAudits + (graph -> PendingAudit(audit, context, `object`)) } - p.fold(setupCallbacks(tx))(p => createFromPending(tx, p.audit, p.context, p.`object`)) + p.fold(setupCallbacks())(p => createFromPending(p.audit, p.context, p.`object`)) } } From 18d297dd84a6ae8c0a3279fda48fe3e85409fcbc Mon Sep 17 00:00:00 2001 From: To-om Date: Fri, 9 Apr 2021 11:19:00 +0200 Subject: [PATCH 19/63] #1946 Improve log messages --- ScalliGraph | 2 +- conf/logback.xml | 19 +++++-------------- package/logback.xml | 4 ++++ 3 files changed, 10 insertions(+), 15 deletions(-) diff --git a/ScalliGraph b/ScalliGraph index e995731253..268150cf26 160000 --- a/ScalliGraph +++ b/ScalliGraph @@ -1 +1 @@ -Subproject commit e9957312530f3925a90de47d3e2fb407df805a6e +Subproject commit 268150cf261cd14f025f2c1dfc35caae38f02802 diff --git a/conf/logback.xml b/conf/logback.xml index 2d299bb9ce..e72d600cf0 100644 --- a/conf/logback.xml +++ b/conf/logback.xml @@ -34,21 +34,12 @@ - + + - - + + + diff --git a/package/logback.xml b/package/logback.xml index 4b0ef2a674..af4ead5d9d 100644 --- a/package/logback.xml +++ b/package/logback.xml @@ -34,6 +34,10 @@ + + + + From b685b77d3abb5978b64c2b478947700972577d04 Mon Sep 17 00:00:00 2001 From: To-om Date: Fri, 9 Apr 2021 12:06:48 +0200 Subject: [PATCH 20/63] #1946 Add limited count query --- ScalliGraph | 2 +- .../cortex/controllers/v0/CortexQueryExecutor.scala | 7 ++++++- .../thp/thehive/controllers/v0/TheHiveQueryExecutor.scala | 5 +++++ .../thp/thehive/controllers/v1/TheHiveQueryExecutor.scala | 5 +++++ thehive/conf/reference.conf | 2 ++ .../test/org/thp/thehive/controllers/v0/QueryTest.scala | 7 ++++--- 6 files changed, 23 insertions(+), 5 deletions(-) diff --git a/ScalliGraph b/ScalliGraph index 268150cf26..0fa2e37c38 160000 --- a/ScalliGraph +++ b/ScalliGraph @@ -1 +1 @@ -Subproject commit 268150cf261cd14f025f2c1dfc35caae38f02802 +Subproject commit 0fa2e37c388a599d918b7eda4711418e6c8640d3 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 96e8390af9..87318660a9 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 @@ -5,6 +5,7 @@ import org.thp.scalligraph.auth.AuthContext import org.thp.scalligraph.controllers.FieldsParser import org.thp.scalligraph.models._ import org.thp.scalligraph.query._ +import org.thp.scalligraph.services.config.{ApplicationConfig, ConfigItem} import org.thp.scalligraph.traversal.Traversal import org.thp.scalligraph.traversal.TraversalOps._ import org.thp.scalligraph.{BadRequestError, EntityIdOrName} @@ -18,7 +19,8 @@ import javax.inject.{Inject, Singleton} import scala.reflect.runtime.{universe => ru} @Singleton -class CortexQueryExecutor @Inject() (implicit +class CortexQueryExecutor @Inject() ( + appConfig: ApplicationConfig, override val db: Database, job: PublicJob, report: PublicAnalyzerTemplate, @@ -27,6 +29,9 @@ class CortexQueryExecutor @Inject() (implicit ) extends QueryExecutor { lazy val controllers: List[PublicData] = action :: report :: job :: analyzerTemplate :: Nil + val limitedCountThresholdConfig: ConfigItem[Long, Long] = appConfig.item[Long]("query.limitedCountThreshold", "Maximum number returned by a count") + override val limitedCountThreshold: Long = limitedCountThresholdConfig.get + override lazy val publicProperties: PublicProperties = controllers.map(_.publicProperties).reduce(_ ++ _) override lazy val queries: Seq[ParamQuery[_]] = diff --git a/thehive/app/org/thp/thehive/controllers/v0/TheHiveQueryExecutor.scala b/thehive/app/org/thp/thehive/controllers/v0/TheHiveQueryExecutor.scala index a4f81dbb79..5db1c5c90e 100644 --- a/thehive/app/org/thp/thehive/controllers/v0/TheHiveQueryExecutor.scala +++ b/thehive/app/org/thp/thehive/controllers/v0/TheHiveQueryExecutor.scala @@ -5,6 +5,7 @@ import org.thp.scalligraph.auth.AuthContext import org.thp.scalligraph.controllers.{FObject, Field, FieldsParser} import org.thp.scalligraph.models._ import org.thp.scalligraph.query._ +import org.thp.scalligraph.services.config.{ApplicationConfig, ConfigItem} import org.thp.scalligraph.traversal.Traversal import org.thp.scalligraph.traversal.TraversalOps._ import org.thp.scalligraph.utils.RichType @@ -37,6 +38,7 @@ object OutputParam { @Singleton class TheHiveQueryExecutor @Inject() ( override val db: Database, + appConfig: ApplicationConfig, alert: PublicAlert, audit: PublicAudit, `case`: PublicCase, @@ -57,6 +59,9 @@ class TheHiveQueryExecutor @Inject() ( lazy val publicDatas: Seq[PublicData] = Seq(alert, audit, `case`, caseTemplate, customField, dashboard, log, observable, observableType, organisation, page, profile, tag, task, user) + val limitedCountThresholdConfig: ConfigItem[Long, Long] = appConfig.item[Long]("query.limitedCountThreshold", "Maximum number returned by a count") + override val limitedCountThreshold: Long = limitedCountThresholdConfig.get + def metaProperties: PublicProperties = PublicPropertyListBuilder .forType[Product](_ => true) diff --git a/thehive/app/org/thp/thehive/controllers/v1/TheHiveQueryExecutor.scala b/thehive/app/org/thp/thehive/controllers/v1/TheHiveQueryExecutor.scala index b11fcace4a..f6c0567967 100644 --- a/thehive/app/org/thp/thehive/controllers/v1/TheHiveQueryExecutor.scala +++ b/thehive/app/org/thp/thehive/controllers/v1/TheHiveQueryExecutor.scala @@ -3,6 +3,7 @@ package org.thp.thehive.controllers.v1 import org.thp.scalligraph.controllers.{FObject, FieldsParser} import org.thp.scalligraph.models.Database import org.thp.scalligraph.query._ +import org.thp.scalligraph.services.config.{ApplicationConfig, ConfigItem} import javax.inject.{Inject, Singleton} @@ -21,6 +22,7 @@ object OutputParam { @Singleton class TheHiveQueryExecutor @Inject() ( + appConfig: ApplicationConfig, alertCtrl: AlertCtrl, auditCtrl: AuditCtrl, caseCtrl: CaseCtrl, @@ -66,6 +68,9 @@ class TheHiveQueryExecutor @Inject() ( taxonomyCtrl ) + val limitedCountThresholdConfig: ConfigItem[Long, Long] = appConfig.item[Long]("query.limitedCountThreshold", "Maximum number returned by a count") + override val limitedCountThreshold: Long = limitedCountThresholdConfig.get + override val version: (Int, Int) = 1 -> 1 override lazy val publicProperties: PublicProperties = controllers.foldLeft(properties.metaProperties)(_ ++ _.publicProperties) diff --git a/thehive/conf/reference.conf b/thehive/conf/reference.conf index fd90ce7b82..0bcf7561f6 100644 --- a/thehive/conf/reference.conf +++ b/thehive/conf/reference.conf @@ -12,6 +12,8 @@ db { initialisationTimeout: 1 hour } +query.limitedCountThreshold: 1000 + storage { provider: localfs localfs.directory: /opt/thp/thehive/files diff --git a/thehive/test/org/thp/thehive/controllers/v0/QueryTest.scala b/thehive/test/org/thp/thehive/controllers/v0/QueryTest.scala index ab415bebd3..14cbcca708 100644 --- a/thehive/test/org/thp/thehive/controllers/v0/QueryTest.scala +++ b/thehive/test/org/thp/thehive/controllers/v0/QueryTest.scala @@ -13,8 +13,9 @@ class QueryTest extends PlaySpecification with Mockito { 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 val limitedCountThreshold: Long = 1000 + 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 @@ -56,7 +57,7 @@ class QueryTest extends PlaySpecification with Mockito { val queryOrError = taskCtrl.statsParser(Field(input)) queryOrError.isGood must beTrue.updateMessage(s => s"$s\n$queryOrError") - queryOrError.get must not be empty + queryOrError.get must not be empty } } } From 4e1e25a0106f92a7909dec4d2e879202b9b39d17 Mon Sep 17 00:00:00 2001 From: Nabil Adouani Date: Fri, 9 Apr 2021 11:34:31 +0200 Subject: [PATCH 21/63] #1925 Sort case templates in case creation dialog --- .../case/case.templates.selector.html | 58 +++++++++---------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/frontend/app/views/partials/case/case.templates.selector.html b/frontend/app/views/partials/case/case.templates.selector.html index 839661d649..63830c19e7 100644 --- a/frontend/app/views/partials/case/case.templates.selector.html +++ b/frontend/app/views/partials/case/case.templates.selector.html @@ -3,42 +3,42 @@