From f63d7377bfdb075de64f1ba9af8d6bc175bb9849 Mon Sep 17 00:00:00 2001 From: To-om Date: Fri, 18 Nov 2016 16:28:52 +0100 Subject: [PATCH] #14 Add merge information in stats ; add nstats parameter in get case api call --- thehive-backend/app/controllers/Case.scala | 49 +++++---- thehive-backend/app/models/Case.scala | 111 +++++++++++++++------ 2 files changed, 110 insertions(+), 50 deletions(-) diff --git a/thehive-backend/app/controllers/Case.scala b/thehive-backend/app/controllers/Case.scala index 16d993d1f2..abf5f30d94 100644 --- a/thehive-backend/app/controllers/Case.scala +++ b/thehive-backend/app/controllers/Case.scala @@ -41,46 +41,51 @@ class CaseCtrl @Inject() ( val log = Logger(getClass) @Timed - def create() = authenticated(Role.write).async(fieldsBodyParser) { implicit request => + def create() = authenticated(Role.write).async(fieldsBodyParser) { implicit request ⇒ caseSrv.create(request.body) - .map(caze => renderer.toOutput(CREATED, caze)) + .map(caze ⇒ renderer.toOutput(CREATED, caze)) } @Timed - def get(id: String) = authenticated(Role.read).async { implicit request => - caseSrv.get(id) - .map(caze => renderer.toOutput(OK, caze)) + def get(id: String) = authenticated(Role.read).async(fieldsBodyParser) { implicit request ⇒ + val nparent = request.body.getLong("nparent").getOrElse(0L).toInt + val withStats = request.body.getBoolean("nstats").getOrElse(false) + + for { + caze ← caseSrv.get(id) + casesWithStats ← auxSrv.apply(caze, nparent, withStats) + } yield renderer.toOutput(OK, casesWithStats) } @Timed - def update(id: String) = authenticated(Role.write).async(fieldsBodyParser) { implicit request => + def update(id: String) = authenticated(Role.write).async(fieldsBodyParser) { implicit request ⇒ val isCaseClosing = request.body.getString("status").filter(_ == CaseStatus.Resolved.toString).isDefined for { // Closing the case, so lets close the open tasks - caze <- caseSrv.update(id, request.body) - closedTasks <- if (isCaseClosing) taskSrv.closeTasksOfCase(id) else Future.successful(Nil) // FIXME log warning if closedTasks contains errors + caze ← caseSrv.update(id, request.body) + closedTasks ← if (isCaseClosing) taskSrv.closeTasksOfCase(id) else Future.successful(Nil) // FIXME log warning if closedTasks contains errors } yield renderer.toOutput(OK, caze) } @Timed - def bulkUpdate() = authenticated(Role.write).async(fieldsBodyParser) { implicit request => + def bulkUpdate() = authenticated(Role.write).async(fieldsBodyParser) { implicit request ⇒ val isCaseClosing = request.body.getString("status").filter(_ == CaseStatus.Resolved.toString).isDefined - request.body.getStrings("ids").fold(Future.successful(Ok(JsArray()))) { ids => + request.body.getStrings("ids").fold(Future.successful(Ok(JsArray()))) { ids ⇒ if (isCaseClosing) taskSrv.closeTasksOfCase(ids: _*) // FIXME log warning if closedTasks contains errors - caseSrv.bulkUpdate(ids, request.body.unset("ids")).map(multiResult => renderer.toMultiOutput(OK, multiResult)) + caseSrv.bulkUpdate(ids, request.body.unset("ids")).map(multiResult ⇒ renderer.toMultiOutput(OK, multiResult)) } } @Timed - def delete(id: String) = authenticated(Role.write).async { implicit request => + def delete(id: String) = authenticated(Role.write).async { implicit request ⇒ caseSrv.delete(id) - .map(_ => NoContent) + .map(_ ⇒ NoContent) } @Timed - def find() = authenticated(Role.read).async(fieldsBodyParser) { implicit request => + def find() = authenticated(Role.read).async(fieldsBodyParser) { implicit request ⇒ val query = request.body.getValue("query").fold[QueryDef](QueryDSL.any)(_.as[QueryDef]) val range = request.body.getString("range") val sort = request.body.getStrings("sort").getOrElse(Nil) @@ -93,21 +98,21 @@ class CaseCtrl @Inject() ( } @Timed - def stats() = authenticated(Role.read).async(fieldsBodyParser) { implicit request => + def stats() = authenticated(Role.read).async(fieldsBodyParser) { implicit request ⇒ val query = request.body.getValue("query").fold[QueryDef](QueryDSL.any)(_.as[QueryDef]) val aggs = request.body.getValue("stats").getOrElse(throw BadRequestError("Parameter \"stats\" is missing")).as[Seq[Agg]] - caseSrv.stats(query, aggs).map(s => Ok(s)) + caseSrv.stats(query, aggs).map(s ⇒ Ok(s)) } @Timed - def linkedCases(id: String) = authenticated(Role.read).async { implicit request => + def linkedCases(id: String) = authenticated(Role.read).async { implicit request ⇒ caseSrv.linkedCases(id) .runWith(Sink.seq) - .map { cases => + .map { cases ⇒ val casesList = cases.sortWith { - case ((c1, _), (c2, _)) => c1.startDate().after(c2.startDate()) + case ((c1, _), (c2, _)) ⇒ c1.startDate().after(c2.startDate()) }.map { - case (caze, artifacts) => + case (caze, artifacts) ⇒ Json.toJson(caze).as[JsObject] - "description" + ("linkedWith" -> Json.toJson(artifacts)) + ("linksCount" -> Json.toJson(artifacts.size)) @@ -117,8 +122,8 @@ class CaseCtrl @Inject() ( } @Timed - def merge(caseId1: String, caseId2: String) = authenticated(Role.read).async { implicit request => - caseMergeSrv.merge(caseId1, caseId2).map { caze => + def merge(caseId1: String, caseId2: String) = authenticated(Role.read).async { implicit request ⇒ + caseMergeSrv.merge(caseId1, caseId2).map { caze ⇒ renderer.toOutput(OK, caze) } } diff --git a/thehive-backend/app/models/Case.scala b/thehive-backend/app/models/Case.scala index 91afb67620..dcc69e8634 100644 --- a/thehive-backend/app/models/Case.scala +++ b/thehive-backend/app/models/Case.scala @@ -13,11 +13,13 @@ import play.api.libs.json.Json import play.api.libs.json.Json.toJsFieldJsValueWrapper import org.elastic4play.JsonFormat.dateFormat -import org.elastic4play.models.{ AttributeDef, AttributeFormat => F, AttributeOption => O, BaseEntity, EntityDef, HiveEnumeration, ModelDef } +import org.elastic4play.models.{ AttributeDef, AttributeFormat ⇒ F, AttributeOption ⇒ O, BaseEntity, EntityDef, HiveEnumeration, ModelDef } import org.elastic4play.services.{ FindSrv, SequenceSrv } import JsonFormat.{ caseImpactStatusFormat, caseResolutionStatusFormat, caseStatusFormat } import services.AuditedModel +import services.CaseSrv +import play.api.Logger object CaseStatus extends Enumeration with HiveEnumeration { type Type = Value @@ -34,7 +36,7 @@ object CaseImpactStatus extends Enumeration with HiveEnumeration { val NoImpact, WithImpact, NotApplicable = Value } -trait CaseAttributes { _: AttributeDef => +trait CaseAttributes { _: AttributeDef ⇒ val caseId = attribute("caseId", F.numberFmt, "Id of the case (auto-generated)", O.model) val title = attribute("title", F.textFmt, "Title of the case") val description = attribute("description", F.textFmt, "Description of the case") @@ -50,58 +52,111 @@ trait CaseAttributes { _: AttributeDef => val resolutionStatus = optionalAttribute("resolutionStatus", F.enumFmt(CaseResolutionStatus), "Resolution status of the case") val impactStatus = optionalAttribute("impactStatus", F.enumFmt(CaseImpactStatus), "Impact status of the case") val summary = optionalAttribute("summary", F.textFmt, "Summary of the case, to be provided when closing a case") - val mergeInto = optionalAttribute("mergeInto",F.stringFmt, "Id of the case created by the merge") - val mergeFrom = multiAttribute("mergeFrom",F.stringFmt, "Id of the cases merged") + val mergeInto = optionalAttribute("mergeInto", F.stringFmt, "Id of the case created by the merge") + val mergeFrom = multiAttribute("mergeFrom", F.stringFmt, "Id of the cases merged") } @Singleton class CaseModel @Inject() ( artifactModel: Provider[ArtifactModel], taskModel: Provider[TaskModel], + caseSrv: Provider[CaseSrv], sequenceSrv: SequenceSrv, findSrv: FindSrv, - implicit val ec: ExecutionContext) extends ModelDef[CaseModel, Case]("case") with CaseAttributes with AuditedModel { caseModel => + implicit val ec: ExecutionContext) extends ModelDef[CaseModel, Case]("case") with CaseAttributes with AuditedModel { caseModel ⇒ + + lazy val logger = Logger(getClass) override val defaultSortBy = Seq("-startDate") override val removeAttribute = Json.obj("status" -> CaseStatus.Deleted) override def creationHook(parent: Option[BaseEntity], attrs: JsObject) = { - sequenceSrv("case").map { caseId => + sequenceSrv("case").map { caseId ⇒ attrs + ("caseId" -> JsNumber(caseId)) } } override def updateHook(entity: BaseEntity, updateAttrs: JsObject): Future[JsObject] = Future.successful { (updateAttrs \ "status").asOpt[CaseStatus.Type] match { - case Some(CaseStatus.Resolved) if !updateAttrs.keys.contains("endDate") => + case Some(CaseStatus.Resolved) if !updateAttrs.keys.contains("endDate") ⇒ updateAttrs + ("endDate" -> Json.toJson(new Date)) - case Some(CaseStatus.Open) => + case Some(CaseStatus.Open) ⇒ updateAttrs + ("endDate" -> JsArray(Nil)) - case _ => + case _ ⇒ updateAttrs } } - override def getStats(entity: BaseEntity): Future[JsObject] = { + private[models] def buildArtifactStats(caze: Case): Future[JsObject] = { import org.elastic4play.services.QueryDSL._ - for { - taskStatsJson <- findSrv( - taskModel.get, - and( - "_parent" ~= entity.id, - "status" in ("Waiting", "InProgress", "Completed")), - groupByField("status", selectCount)) - (taskCount, taskStats) = taskStatsJson.value.foldLeft((0L, JsObject(Nil))) { - case ((total, s), (key, value)) => - val count = (value \ "count").as[Long] - (total + count, s + (key -> JsNumber(count))) + findSrv( + artifactModel.get, + and( + parent("case", withId(caze.id)), + "status" ~= "Ok"), + selectCount) + .map { artifactStats ⇒ + Json.obj("artifacts" -> artifactStats) + } + } + + private[models] def buildTaskStats(caze: Case): Future[JsObject] = { + import org.elastic4play.services.QueryDSL._ + findSrv( + taskModel.get, + and( + parent("case", withId(caze.id)), + "status" in ("Waiting", "InProgress", "Completed")), + groupByField("status", selectCount)) + .map { taskStatsJson ⇒ + val (taskCount, taskStats) = taskStatsJson.value.foldLeft((0L, JsObject(Nil))) { + case ((total, s), (key, value)) ⇒ + val count = (value \ "count").as[Long] + (total + count, s + (key -> JsNumber(count))) + } + Json.obj("tasks" -> (taskStats + ("total" -> JsNumber(taskCount)))) + } + } + + private[models] def buildMergeIntoStats(caze: Case): Future[JsObject] = { + caze.mergeInto() + .fold(Future.successful(Json.obj())) { mergeCaseId ⇒ + caseSrv.get.get(mergeCaseId).map { c ⇒ + Json.obj("mergeInto" -> Json.obj( + "caseId" -> c.caseId(), + "title" -> c.title())) + } } - artifactStats <- findSrv( - artifactModel.get, - and( - "_parent" ~= entity.id, - "status" ~= "Ok"), - selectCount) - } yield Json.obj("tasks" -> (taskStats + ("total" -> JsNumber(taskCount))), "artifacts" -> artifactStats) + } + + private[models] def buildMergeFromStats(caze: Case): Future[JsObject] = { + Future + .traverse(caze.mergeFrom()) { id ⇒ + caseSrv.get.get(id).map { c ⇒ + Json.obj( + "caseId" -> c.caseId(), + "title" -> c.title()) + } + } + .map { + case mf if !mf.isEmpty ⇒ Json.obj("mergeFrom" -> mf) + case _ ⇒ Json.obj() + } + } + override def getStats(entity: BaseEntity): Future[JsObject] = { + + + entity match { + case caze: Case ⇒ + for { + taskStats <- buildTaskStats(caze) + artifactStats <- buildArtifactStats(caze) + mergeIntoStats <- buildMergeIntoStats(caze) + mergeFromStats <- buildMergeFromStats(caze) + } yield taskStats ++ artifactStats ++ mergeIntoStats ++ mergeFromStats + case other ⇒ + logger.warn(s"Request caseStats from a non-case entity ?! ${other.getClass}:$other") + Future.successful(Json.obj()) + } } override val computedMetrics = Map(