Skip to content

Commit

Permalink
#1264 WIP merge review & tests
Browse files Browse the repository at this point in the history
  • Loading branch information
rriclet committed Mar 10, 2021
1 parent 2f21d85 commit 68764f5
Show file tree
Hide file tree
Showing 21 changed files with 246 additions and 142 deletions.
28 changes: 1 addition & 27 deletions thehive/app/org/thp/thehive/controllers/v0/CaseCtrl.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@ package org.thp.thehive.controllers.v0

import org.apache.tinkerpop.gremlin.process.traversal.P
import org.thp.scalligraph._
import org.thp.scalligraph.traversal.Graph
import org.thp.scalligraph.controllers.{Entrypoint, FPathElem, FPathEmpty, FieldsParser}
import org.thp.scalligraph.models.{Database, Entity, UMapping}
import org.thp.scalligraph.query._
import org.thp.scalligraph.traversal.TraversalOps._
import org.thp.scalligraph.traversal.{IteratorOutput, Traversal}
import org.thp.scalligraph.traversal.{Graph, IteratorOutput, Traversal}
import org.thp.thehive.controllers.v0.Conversion._
import org.thp.thehive.dto.v0.{InputCase, InputTask}
import org.thp.thehive.dto.v1.InputCustomFieldValue
Expand All @@ -19,7 +18,6 @@ import org.thp.thehive.services.CustomFieldOps._
import org.thp.thehive.services.ObservableOps._
import org.thp.thehive.services.OrganisationOps._
import org.thp.thehive.services.ShareOps._
import org.thp.thehive.services.TagOps._
import org.thp.thehive.services.UserOps._
import org.thp.thehive.services._
import play.api.libs.json._
Expand Down Expand Up @@ -153,34 +151,10 @@ class CaseCtrl @Inject() (
for {
caze <- caseSrv.get(EntityIdOrName(caseId)).visible(organisationSrv).getOrFail("Case")
toMerge <- caseSrv.get(EntityIdOrName(caseToMerge)).visible(organisationSrv).getOrFail("Case")
_ <- sameOrga(Seq(caze, toMerge))
_ <- sameProfile(Seq(caze, toMerge))
merged <- caseSrv.merge(Seq(caze, toMerge))
} yield Results.Created(merged.toJson)
}

private def sameOrga(cases: Seq[Case with Entity])(implicit graph: Graph): Try[Seq[Case with Entity]] =
for {
orgas <- cases.toTry(c => caseSrv.get(c).organisations.getOrFail("Organisation"))
firstOrga <- orgas.headOption match {
case Some(o) => Success(o)
case None => Failure(BadRequestError("No organisations found"))
}
sameOrga = orgas.forall(_.name == firstOrga.name)
res <- if (sameOrga) Success(cases) else Failure(BadRequestError(s"Cases to merge have different organisations"))
} yield res

private def sameProfile(cases: Seq[Case with Entity])(implicit graph: Graph): Try[Seq[Case with Entity]] =
for {
profiles <- cases.toTry(c => caseSrv.get(c).shares.profile.getOrFail("Profile"))
firstProfile <- profiles.headOption match {
case Some(o) => Success(o)
case None => Failure(BadRequestError("No profiles found"))
}
sameProfile = profiles.forall(_.name == firstProfile.name)
res <- if (sameProfile) Success(cases) else Failure(BadRequestError(s"Cases to merge have different profiles"))
} yield res

def linkedCases(caseIdOrNumber: String): Action[AnyContent] =
entrypoint("case link")
.authRoTransaction(db) { implicit request => implicit graph =>
Expand Down
31 changes: 3 additions & 28 deletions thehive/app/org/thp/thehive/controllers/v1/CaseCtrl.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ package org.thp.thehive.controllers.v1

import org.apache.tinkerpop.gremlin.process.traversal.P
import org.thp.scalligraph.controllers.{Entrypoint, FieldsParser}
import org.thp.scalligraph.models.{Database, Entity}
import org.thp.scalligraph.models.Database
import org.thp.scalligraph.query.{ParamQuery, PropertyUpdater, PublicProperties, Query}
import org.thp.scalligraph.traversal.TraversalOps._
import org.thp.scalligraph.traversal.{Graph, IteratorOutput, Traversal}
import org.thp.scalligraph.{BadRequestError, EntityIdOrName, RichOptionTry, RichSeq}
import org.thp.scalligraph.traversal.{IteratorOutput, Traversal}
import org.thp.scalligraph.{EntityIdOrName, RichOptionTry, RichSeq}
import org.thp.thehive.controllers.v1.Conversion._
import org.thp.thehive.dto.v1.{InputCase, InputTask}
import org.thp.thehive.models._
Expand All @@ -22,7 +22,6 @@ import org.thp.thehive.services._
import play.api.mvc.{Action, AnyContent, Results}

import javax.inject.{Inject, Singleton}
import scala.util.{Failure, Success, Try}

@Singleton
class CaseCtrl @Inject() (
Expand Down Expand Up @@ -156,31 +155,7 @@ class CaseCtrl @Inject() (
.visible(organisationSrv)
.getOrFail("Case")
)
_ <- sameOrga(cases)
_ <- sameProfile(cases)
mergedCase <- caseSrv.merge(cases)
} yield Results.Created(mergedCase.toJson)
}

private def sameOrga(cases: Seq[Case with Entity])(implicit graph: Graph): Try[Seq[Case with Entity]] =
for {
orgas <- cases.toTry(c => caseSrv.get(c).organisations.getOrFail("Organisation"))
firstOrga <- orgas.headOption match {
case Some(o) => Success(o)
case None => Failure(BadRequestError("No organisations found"))
}
sameOrga = orgas.forall(_.name == firstOrga.name)
res <- if (sameOrga) Success(cases) else Failure(BadRequestError(s"Cases to merge have different organisations"))
} yield res

private def sameProfile(cases: Seq[Case with Entity])(implicit graph: Graph): Try[Seq[Case with Entity]] =
for {
profiles <- cases.toTry(c => caseSrv.get(c).shares.profile.getOrFail("Profile"))
firstProfile <- profiles.headOption match {
case Some(o) => Success(o)
case None => Failure(BadRequestError("No profiles found"))
}
sameProfile = profiles.forall(_.name == firstProfile.name)
res <- if (sameProfile) Success(cases) else Failure(BadRequestError(s"Cases to merge have different profiles"))
} yield res
}
159 changes: 106 additions & 53 deletions thehive/app/org/thp/thehive/services/CaseSrv.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import org.thp.scalligraph.query.PropertyUpdater
import org.thp.scalligraph.services._
import org.thp.scalligraph.traversal.TraversalOps._
import org.thp.scalligraph.traversal._
import org.thp.scalligraph.{EntityId, EntityIdOrName, EntityName, RichOptionTry, RichSeq}
import org.thp.scalligraph.{BadRequestError, EntityId, EntityIdOrName, EntityName, RichOptionTry, RichSeq}
import org.thp.thehive.controllers.v1.Conversion._
import org.thp.thehive.dto.v1.InputCustomFieldValue
import org.thp.thehive.models._
Expand All @@ -29,7 +29,7 @@ import play.api.libs.json.{JsNull, JsObject, JsValue, Json}
import java.lang.{Long => JLong}
import java.util.{Date, List => JList, Map => JMap}
import javax.inject.{Inject, Named, Provider, Singleton}
import scala.util.{Success, Try}
import scala.util.{Failure, Success, Try}

@Singleton
class CaseSrv @Inject() (
Expand Down Expand Up @@ -320,58 +320,111 @@ class CaseSrv @Inject() (
auditSrv.`case`.update(`case`, Json.obj("owner" -> JsNull))
}

def merge(cases: Seq[Case with Entity])(implicit graph: Graph, authContext: AuthContext): Try[RichCase] = {
val mergedCase = Case(
cases.map(_.title).mkString(" / "),
cases.map(_.description).mkString("\n\n"),
cases.map(_.severity).max,
cases.map(_.startDate).min,
None,
cases.exists(_.flag),
cases.map(_.tlp).max,
cases.map(_.pap).max,
CaseStatus.Open,
cases.map(_.summary).fold(None)((s1, s2) => (s1 ++ s2).reduceOption(_ + "\n\n" + _)),
cases.flatMap(_.tags).distinct
)
for {
user <- userSrv.get(EntityIdOrName(authContext.userId)).getOrFail("User")
orga <- organisationSrv.get(authContext.organisation).getOrFail("Organisation")
richCase <- create(mergedCase, Some(user), orga, Seq(), None, Seq())
_ <- cases.toTry { c =>
for {
_ <-
get(c)
.tasks
.richTask
.toList
.toTry(shareSrv.shareTask(_, richCase.`case`, orga._id))
_ <-
get(c)
.observables
.richObservable
.toList
.toTry(shareSrv.shareObservable(_, richCase.`case`, orga._id))
_ <-
get(c)
.alert
.toList
.toTry(alertSrv.alertCaseSrv.create(AlertCase(), _, richCase.`case`))
_ <-
get(c)
.procedure
.toList
.toTry(caseProcedureSrv.create(CaseProcedure(), richCase.`case`, _))
_ <-
get(c)
.richCustomFields
.toList
.toTry(c => createCustomField(richCase.`case`, EntityIdOrName(c.customField.name), c.value, c.order))
} yield Success(())
}
_ = cases.map(remove(_))
} yield richCase
def merge(cases: Seq[Case with Entity])(implicit graph: Graph, authContext: AuthContext): Try[RichCase] =
if (cases.size > 1 && canMerge(cases)) {
val mergedCase = Case(
cases.map(_.title).mkString(" / "),
cases.map(_.description).mkString("\n\n"),
cases.map(_.severity).max,
cases.map(_.startDate).min,
None,
cases.exists(_.flag),
cases.map(_.tlp).max,
cases.map(_.pap).max,
CaseStatus.Open,
cases.map(_.summary).fold(None)((s1, s2) => (s1 ++ s2).reduceOption(_ + "\n\n" + _)),
cases.flatMap(_.tags).distinct
)

val allProfilesOrgas = get(cases.head)
.shares
.project(_.by(_.profile).by(_.organisation))
.toSeq

for {
user <- userSrv.get(EntityIdOrName(authContext.userId)).getOrFail("User")
orga <- organisationSrv.current.getOrFail("Organisation")
richCase <- create(mergedCase, Some(user), orga, Seq(), None, Seq())
_ <- cases.toTry { c =>
for {
// Share case with all organisations except the one who created the merged case
_ <-
allProfilesOrgas
.filter(_._2._id != organisationSrv.currentId)
.toTry(profileOrg => shareSrv.shareCase(owner = false, richCase.`case`, profileOrg._2, profileOrg._1))

_ <- shareMergedCaseTasks(allProfilesOrgas.map(_._2), c, richCase.`case`)
_ <- shareMergedCaseObservables(allProfilesOrgas.map(_._2), c, richCase.`case`)
_ <-
get(c)
.alert
.toList
.toTry(alertSrv.alertCaseSrv.create(AlertCase(), _, richCase.`case`))
_ <-
get(c)
.procedure
.toList
.toTry(caseProcedureSrv.create(CaseProcedure(), richCase.`case`, _))
_ <-
get(c)
.richCustomFields
.toList
.toTry(c => createCustomField(richCase.`case`, EntityIdOrName(c.customField.name), c.value, c.order))
} yield Success(())
}
_ = cases.map(remove(_))
} yield richCase
} else
Failure(BadRequestError("To be able to merge, cases must have same organisation / profile pair and user must be org-admin"))

private def canMerge(cases: Seq[Case with Entity])(implicit graph: Graph, authContext: AuthContext): Boolean = {
val allOrgProfiles = getByIds(cases.map(_._id): _*)
.shares
.project(_.by(_.profile.value(_.name)).by(_.organisation._id))
.fold
.toSeq
.map(_.toSet)
.distinct

// All cases must have the same organisation / profile pair &&
// case organisation must match current organisation and be of org-admin profile
allOrgProfiles.size == 1 && allOrgProfiles
.head
.find(_._2 == organisationSrv.currentId)
.map(_._1)
.contains(Profile.orgAdmin.name)
}

private def shareMergedCaseTasks(orgs: Seq[Organisation with Entity], fromCase: Case with Entity, mergedCase: Case with Entity)(implicit
graph: Graph,
authContext: AuthContext
): Try[Unit] =
for {
_ <- orgs.toTry(org =>
get(fromCase)
.share(org._id)
.tasks
.richTask
.toList
.toTry(shareSrv.shareTask(_, mergedCase, org._id))
)
} yield Success()

private def shareMergedCaseObservables(orgs: Seq[Organisation with Entity], fromCase: Case with Entity, mergedCase: Case with Entity)(implicit
graph: Graph,
authContext: AuthContext
): Try[Unit] =
for {
_ <- orgs.toTry(org =>
get(fromCase)
.share(org._id)
.observables
.richObservable
.toList
.toTry(shareSrv.shareObservable(_, mergedCase, org._id))
)
} yield Success()

}

object CaseOps {
Expand Down
2 changes: 1 addition & 1 deletion thehive/test/org/thp/thehive/DatabaseBuilder.scala
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ class DatabaseBuilder @Inject() (
// createEdge(taskSrv.taskUserSrv, taskSrv, userSrv, FieldsParser[TaskUser], idMap)
createEdge(taskSrv.taskLogSrv, taskSrv, logSrv, FieldsParser[TaskLog], idMap)

createEdge(caseSrv.caseUserSrv, caseSrv, userSrv, FieldsParser[CaseUser], idMap)
// createEdge(caseSrv.caseUserSrv, caseSrv, userSrv, FieldsParser[CaseUser], idMap)
// createEdge(caseSrv.mergedFromSrv, caseSrv, caseSrv, FieldsParser[MergedFrom], idMap)
// createEdge(caseSrv.caseCaseTemplateSrv, caseSrv, caseTemplateSrv, FieldsParser[CaseCaseTemplate], idMap)
// createEdge(caseSrv.caseResolutionStatusSrv, caseSrv, resolutionStatusSrv, FieldsParser[CaseResolutionStatus], idMap)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ class CaseCtrlTest extends PlaySpecification with TestAppBuilder {
status(result) must_=== 200
val resultCase = contentAsJson(result)

(resultCase \ "count").asOpt[Int] must beSome(3)
(resultCase \ "count").asOpt[Int] must beSome(7)
(resultCase \ "t1" \ "count").asOpt[Int] must beSome(2)
(resultCase \ "t2" \ "count").asOpt[Int] must beSome(1)
(resultCase \ "t3" \ "count").asOpt[Int] must beSome(1)
Expand Down Expand Up @@ -418,7 +418,6 @@ class CaseCtrlTest extends PlaySpecification with TestAppBuilder {
val result = app[CaseCtrl].merge("21", "25")(request)
status(result) must beEqualTo(400).updateMessage(s => s"$s\n${contentAsString(result)}")
(contentAsJson(result) \ "type").as[String] must beEqualTo("BadRequest")
(contentAsJson(result) \ "message").as[String] must contain("different profiles")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,6 @@ class CaseCtrlTest extends PlaySpecification with TestAppBuilder {
val result = app[CaseCtrl].merge("21,25")(request)
status(result) must beEqualTo(400).updateMessage(s => s"$s\n${contentAsString(result)}")
(contentAsJson(result) \ "type").as[String] must beEqualTo("BadRequest")
(contentAsJson(result) \ "message").as[String] must contain("different profiles")
}
}
}
Loading

0 comments on commit 68764f5

Please sign in to comment.