From bb1fa57908a29908d09030f28dc8063c871f76b3 Mon Sep 17 00:00:00 2001 From: Robin Riclet Date: Wed, 10 Feb 2021 18:14:06 +0100 Subject: [PATCH] #1670 Importing exisiting taxonomies updates them --- .../thehive/controllers/v1/TaxonomyCtrl.scala | 10 ++- .../org/thp/thehive/services/PatternSrv.scala | 2 +- .../app/org/thp/thehive/services/TagSrv.scala | 17 ++++- .../thp/thehive/services/TaxonomySrv.scala | 31 +++++++++- .../controllers/v1/TaxonomyCtrlTest.scala | 62 ++++++++++--------- 5 files changed, 89 insertions(+), 33 deletions(-) diff --git a/thehive/app/org/thp/thehive/controllers/v1/TaxonomyCtrl.scala b/thehive/app/org/thp/thehive/controllers/v1/TaxonomyCtrl.scala index c491fdf7aa..86a2776d87 100644 --- a/thehive/app/org/thp/thehive/controllers/v1/TaxonomyCtrl.scala +++ b/thehive/app/org/thp/thehive/controllers/v1/TaxonomyCtrl.scala @@ -88,7 +88,7 @@ class TaxonomyCtrl @Inject() ( case Failure(e) => Json.obj("namespace" -> taxo.namespace, "status" -> "Failure", "message" -> e.getMessage) case Success(t) => - Json.obj("namespace" -> t.namespace, "status" -> "Success", "tagsImported" -> t.tags.size) + Json.obj("namespace" -> t.namespace, "status" -> "Success", "numberOfTags" -> t.tags.size) } array :+ res } @@ -126,8 +126,14 @@ class TaxonomyCtrl @Inject() ( else if (inputTaxo.namespace.startsWith("_freetags")) Failure(BadRequestError(s"Namespace _freetags is restricted for TheHive")) else if (taxonomySrv.startTraversal.alreadyImported(inputTaxo.namespace)) - Failure(BadRequestError(s"A taxonomy with namespace '${inputTaxo.namespace}' already exists in this organisation")) + // Update the taxonomy, update exisiting tags & create others + for { + _ <- allTags.toTry(t => taxonomySrv.updateOrCreateTag(inputTaxo.namespace, t)) + taxonomy <- taxonomySrv.get(EntityIdOrName(inputTaxo.namespace)).getOrFail("Taxonomy") + updatedTaxo <- taxonomySrv.update(taxonomy, inputTaxo.toTaxonomy) + } yield updatedTaxo else + // Create the taxonomy and all its tags for { tagsEntities <- allTags.toTry(t => tagSrv.create(t)) richTaxonomy <- taxonomySrv.create(inputTaxo.toTaxonomy, tagsEntities) diff --git a/thehive/app/org/thp/thehive/services/PatternSrv.scala b/thehive/app/org/thp/thehive/services/PatternSrv.scala index 52b1121652..a9b18400ff 100644 --- a/thehive/app/org/thp/thehive/services/PatternSrv.scala +++ b/thehive/app/org/thp/thehive/services/PatternSrv.scala @@ -46,7 +46,7 @@ class PatternSrv @Inject() ( def update( pattern: Pattern with Entity, input: Pattern - )(implicit graph: Graph, authContext: AuthContext): Try[Pattern with Entity] = + )(implicit graph: Graph): Try[Pattern with Entity] = for { updatedPattern <- get(pattern) .when(pattern.patternId != input.patternId)(_.update(_.patternId, input.patternId)) diff --git a/thehive/app/org/thp/thehive/services/TagSrv.scala b/thehive/app/org/thp/thehive/services/TagSrv.scala index 2272f01c5f..49f2db7a1a 100644 --- a/thehive/app/org/thp/thehive/services/TagSrv.scala +++ b/thehive/app/org/thp/thehive/services/TagSrv.scala @@ -1,7 +1,6 @@ package org.thp.thehive.services import akka.actor.ActorRef -import javax.inject.{Inject, Named, Singleton} import org.apache.tinkerpop.gremlin.structure.{Graph, Vertex} import org.thp.scalligraph.auth.AuthContext import org.thp.scalligraph.models.{Database, Entity} @@ -9,9 +8,11 @@ import org.thp.scalligraph.services.config.{ApplicationConfig, ConfigItem} import org.thp.scalligraph.services.{IntegrityCheckOps, VertexSrv} import org.thp.scalligraph.traversal.TraversalOps._ import org.thp.scalligraph.traversal.{Converter, Traversal} +import org.thp.scalligraph.utils.FunctionalCondition.When import org.thp.thehive.models.{AlertTag, CaseTag, ObservableTag, Tag} import org.thp.thehive.services.TagOps._ +import javax.inject.{Inject, Named, Singleton} import scala.util.{Success, Try} @Singleton @@ -46,6 +47,9 @@ class TagSrv @Inject() (appConfig: ApplicationConfig, @Named("integrity-check-ac } } + def getOrCreate(tag: Tag)(implicit graph: Graph, authContext: AuthContext): Try[Tag with Entity] = + getTag(tag).getOrFail("Tag").recoverWith { case _ => create(tag) } + override def createEntity(e: Tag)(implicit graph: Graph, authContext: AuthContext): Try[Tag with Entity] = { integrityCheckActor ! EntityAdded("Tag") super.createEntity(e) @@ -54,6 +58,17 @@ class TagSrv @Inject() (appConfig: ApplicationConfig, @Named("integrity-check-ac def create(tag: Tag)(implicit graph: Graph, authContext: AuthContext): Try[Tag with Entity] = createEntity(tag) override def exists(e: Tag)(implicit graph: Graph): Boolean = startTraversal.getByName(e.namespace, e.predicate, e.value).exists + + def update( + tag: Tag with Entity, + input: Tag + )(implicit graph: Graph): Try[Tag with Entity] = + for { + updatedTag <- get(tag) + .when(tag.description != input.description)(_.update(_.description, input.description)) + .when(tag.colour != input.colour)(_.update(_.colour, input.colour)) + .getOrFail("Tag") + } yield updatedTag } object TagOps { diff --git a/thehive/app/org/thp/thehive/services/TaxonomySrv.scala b/thehive/app/org/thp/thehive/services/TaxonomySrv.scala index 01dd9f071e..e0c20f629a 100644 --- a/thehive/app/org/thp/thehive/services/TaxonomySrv.scala +++ b/thehive/app/org/thp/thehive/services/TaxonomySrv.scala @@ -11,16 +11,19 @@ import org.thp.scalligraph.services.{EdgeSrv, VertexSrv} import org.thp.scalligraph.traversal.Converter.Identity import org.thp.scalligraph.traversal.TraversalOps.TraversalOpsDefs import org.thp.scalligraph.traversal.{Converter, Traversal} +import org.thp.scalligraph.utils.FunctionalCondition.When import org.thp.scalligraph.{BadRequestError, EntityId, EntityIdOrName, RichSeq} import org.thp.thehive.models._ import org.thp.thehive.services.OrganisationOps._ import org.thp.thehive.services.TaxonomyOps._ +import org.thp.thehive.services.TagOps._ import scala.util.{Failure, Success, Try} @Singleton class TaxonomySrv @Inject() ( - organisationSrv: OrganisationSrv + organisationSrv: OrganisationSrv, + tagSrv: TagSrv )(implicit @Named("with-thehive-schema") db: Database) extends VertexSrv[Taxonomy] { @@ -46,6 +49,30 @@ class TaxonomySrv @Inject() ( override def getByName(name: String)(implicit graph: Graph): Traversal.V[Taxonomy] = Try(startTraversal.getByNamespace(name)).getOrElse(startTraversal.limit(0)) + def update(taxonomy: Taxonomy with Entity, input: Taxonomy)(implicit graph: Graph): Try[RichTaxonomy] = + for { + updatedTaxonomy <- + get(taxonomy) + .when(taxonomy.namespace != input.namespace)(_.update(_.namespace, input.namespace)) + .when(taxonomy.description != input.description)(_.update(_.description, input.description)) + .when(taxonomy.version != input.version)(_.update(_.version, input.version)) + .richTaxonomy + .getOrFail("Taxonomy") + } yield updatedTaxonomy + + def updateOrCreateTag(namespace: String, t: Tag)(implicit graph: Graph, authContext: AuthContext): Try[Tag with Entity] = + if (getByName(namespace).doesTagExists(t)) + for { + tag <- tagSrv.getTag(t).getOrFail("Tag") + updatedTag <- tagSrv.update(tag, t) + } yield updatedTag + else + for { + tag <- tagSrv.create(t) + taxo <- getByName(namespace).getOrFail("Taxonomy") + _ <- taxonomyTagSrv.create(TaxonomyTag(), taxo, tag) + } yield tag + def activate(taxonomyId: EntityIdOrName)(implicit graph: Graph, authContext: AuthContext): Try[Unit] = for { taxo <- get(taxonomyId).getOrFail("Taxonomy") @@ -97,6 +124,8 @@ object TaxonomyOps { def tags: Traversal.V[Tag] = traversal.out[TaxonomyTag].v[Tag] + def doesTagExists(tag: Tag): Boolean = traversal.tags.getTag(tag).exists + def richTaxonomy: Traversal[RichTaxonomy, JMap[String, Any], Converter[RichTaxonomy, JMap[String, Any]]] = traversal .project( diff --git a/thehive/test/org/thp/thehive/controllers/v1/TaxonomyCtrlTest.scala b/thehive/test/org/thp/thehive/controllers/v1/TaxonomyCtrlTest.scala index a1664d8abc..6086e0df71 100644 --- a/thehive/test/org/thp/thehive/controllers/v1/TaxonomyCtrlTest.scala +++ b/thehive/test/org/thp/thehive/controllers/v1/TaxonomyCtrlTest.scala @@ -13,7 +13,7 @@ case class TestTaxonomy( namespace: String, description: String, version: Int, - tags: List[OutputTag] + tags: Set[OutputTag] ) object TestTaxonomy { @@ -22,7 +22,7 @@ object TestTaxonomy { outputTaxonomy.namespace, outputTaxonomy.description, outputTaxonomy.version, - outputTaxonomy.tags.toList + outputTaxonomy.tags.toSet ) } @@ -50,6 +50,15 @@ class TaxonomyCtrlTest extends PlaySpecification with TestAppBuilder { ) ) + val updateTaxo = InputTaxonomy( + "taxonomy1", + "Updated The taxonomy 1", + 2, + None, + List(InputPredicate("pred1", None, None, None, None)), + List(InputValue("pred1", List(InputEntry("value2", None, Some("#fba800"), None, None)))) + ) + "create a valid taxonomy" in testApp { app => val request = FakeRequest("POST", "/api/v1/taxonomy") .withJsonBody(Json.toJson(inputTaxo)) @@ -64,7 +73,7 @@ class TaxonomyCtrlTest extends PlaySpecification with TestAppBuilder { "test-taxo", "A test taxonomy", 1, - List( + Set( OutputTag("test-taxo", "pred1", Some("entry1"), None, "#ffa800"), OutputTag("test-taxo", "pred2", Some("entry2"), None, "#00ad1c"), OutputTag("test-taxo", "pred2", Some("entry21"), None, "#00ad1c") @@ -82,19 +91,6 @@ class TaxonomyCtrlTest extends PlaySpecification with TestAppBuilder { (contentAsJson(result) \ "type").as[String] must beEqualTo("AuthorizationError") } - "return error if namespace is present in database" in testApp { app => - val alreadyInDatabase = inputTaxo.copy(namespace = "taxonomy1") - - val request = FakeRequest("POST", "/api/v1/taxonomy") - .withJsonBody(Json.toJson(alreadyInDatabase)) - .withHeaders("user" -> "admin@thehive.local") - - val result = app[TaxonomyCtrl].create(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("already exists") - } - "return error if namespace is empty" in testApp { app => val emptyNamespace = inputTaxo.copy(namespace = "") @@ -120,7 +116,7 @@ class TaxonomyCtrlTest extends PlaySpecification with TestAppBuilder { "taxonomy1", "The taxonomy 1", 1, - List(OutputTag("taxonomy1", "pred1", Some("value1"), None, "#00f300")) + Set(OutputTag("taxonomy1", "pred1", Some("value1"), None, "#00f300")) ) } @@ -169,17 +165,6 @@ class TaxonomyCtrlTest extends PlaySpecification with TestAppBuilder { contentAsJson(result).as[JsArray].value.size must beEqualTo(1) } - "return error if zip file contains an already present taxonomy" in testApp { app => - val request = FakeRequest("POST", "/api/v1/taxonomy/import-zip") - .withHeaders("user" -> "admin@thehive.local") - .withBody(AnyContentAsMultipartFormData(multipartZipFile("machinetag-present.zip"))) - - val result = app[TaxonomyCtrl].importZip(request) - status(result) must beEqualTo(201).updateMessage(s => s"$s\n${contentAsString(result)}") - contentAsString(result) must contain("Failure") - contentAsJson(result).as[JsArray].value.size must beEqualTo(2) - } - "return error if zip file contains a bad formatted taxonomy" in testApp { app => val request = FakeRequest("POST", "/api/v1/taxonomy/import-zip") .withHeaders("user" -> "admin@thehive.local") @@ -191,6 +176,27 @@ class TaxonomyCtrlTest extends PlaySpecification with TestAppBuilder { (contentAsJson(result) \ "message").as[String] must contain("formatting") } + "update a taxonomies and their tags" in testApp { app => + val request = FakeRequest("POST", "/api/v1/taxonomy") + .withJsonBody(Json.toJson(updateTaxo)) + .withHeaders("user" -> "admin@thehive.local") + + val result = app[TaxonomyCtrl].create(request) + status(result) must beEqualTo(201).updateMessage(s => s"$s\n${contentAsString(result)}") + + val resultTaxo = contentAsJson(result).as[OutputTaxonomy] + + TestTaxonomy(resultTaxo) must_=== TestTaxonomy( + "taxonomy1", + "Updated The taxonomy 1", + 2, + Set( + OutputTag("taxonomy1", "pred1", Some("value2"), None, "#fba800"), + OutputTag("taxonomy1", "pred1", Some("value1"), None, "#00f300") + ) + ) + } + "activate a taxonomy" in testApp { app => val request1 = FakeRequest("GET", "/api/v1/taxonomy/taxonomy2") .withHeaders("user" -> "certuser@thehive.local")