diff --git a/ScalliGraph b/ScalliGraph index 6450d6fefe..5f1f6a4c10 160000 --- a/ScalliGraph +++ b/ScalliGraph @@ -1 +1 @@ -Subproject commit 6450d6fefe9d9981f27e754366d77bcdf3e1c4e1 +Subproject commit 5f1f6a4c101074d3eb595632dde7ff9abcdf7c9f diff --git a/frontend/app/scripts/services/api/TagSrv.js b/frontend/app/scripts/services/api/TagSrv.js index ce9035f11f..44044433a1 100644 --- a/frontend/app/scripts/services/api/TagSrv.js +++ b/frontend/app/scripts/services/api/TagSrv.js @@ -5,28 +5,13 @@ var self = this; - var getTags = function(objectType, term) { + var getTags = function(objectType, term) { // TODO remove objectType parameter (not used anymore) var defer = $q.defer(); - var operations = [ - { _name: 'listTag' } - ]; - - if(objectType) { - operations.push({ _name: objectType }); - } - - operations.push({ - _name: 'filter', - _like: { - _field: 'text', - _value: '*' + term + '*' - } - }); - - operations.push({ _name: 'text' }); - - // Get the list - QuerySrv.call('v0', operations, {name: 'tags-auto-complete'}) + QuerySrv.call('v0', [ + { _name: 'listTag' }, + { _name: 'autoComplete', freeTag: term, limit: 10 }, + { _name: 'text' } + ], {name: 'tags-auto-complete'}) .then(function(data) { defer.resolve(_.map(_.unique(data), function(tag) { return {text: tag}; diff --git a/migration/src/main/scala/org/thp/thehive/migration/th4/Output.scala b/migration/src/main/scala/org/thp/thehive/migration/th4/Output.scala index 7706330f2d..3cc893a635 100644 --- a/migration/src/main/scala/org/thp/thehive/migration/th4/Output.scala +++ b/migration/src/main/scala/org/thp/thehive/migration/th4/Output.scala @@ -281,8 +281,10 @@ class Output @Inject() ( body(graph)(getAuthContext(userId)) } - def getTag(tagName: String)(implicit graph: Graph, authContext: AuthContext): Try[Tag with Entity] = - cache.getOrElseUpdate(s"tag-$tagName")(tagSrv.createEntity(Tag.fromString(tagName, tagSrv.defaultNamespace, tagSrv.defaultColour))) + def getTag(tagName: String, organisationId: String)(implicit graph: Graph, authContext: AuthContext): Try[Tag with Entity] = + cache.getOrElseUpdate(s"tag-$tagName")( + tagSrv.createEntity(Tag(s"_freetags_$organisationId", tagName, None, None, tagSrv.freeTagColour)) + ) override def organisationExists(inputOrganisation: InputOrganisation): Boolean = organisations.contains(inputOrganisation.organisation.name) @@ -540,7 +542,7 @@ class Output @Inject() ( .logFailure(s"Unable to set case template ${ct.name} to case #${createdCase.number}") } inputCase.`case`.tags.foreach { tagName => - getTag(tagName) + getTag(tagName, organisationIds.head.value) .flatMap(tag => caseSrv.caseTagSrv.create(CaseTag(), createdCase, tag)) .logFailure(s"Unable to add tag $tagName to case #${createdCase.number}") } @@ -667,7 +669,7 @@ class Output @Inject() ( _ = updateMetaData(observable, inputObservable.metaData) _ <- observableSrv.observableObservableType.create(ObservableObservableType(), observable, observableType) _ = inputObservable.observable.tags.foreach { tagName => - getTag(tagName) + getTag(tagName, organisationIds.head.value) .foreach(tag => observableSrv.observableTagSrv.create(ObservableTag(), observable, tag)) } } yield observable @@ -711,11 +713,11 @@ class Output @Inject() ( authTransaction(inputAlert.metaData.createdBy) { implicit graph => implicit authContext => logger.debug(s"Create alert ${inputAlert.alert.`type`}:${inputAlert.alert.source}:${inputAlert.alert.sourceRef}") val `case` = inputAlert.caseId.flatMap(c => getCase(EntityId.read(c)).toOption) - val tags = inputAlert.alert.tags.flatMap(getTag(_).toOption) for { organisation <- getOrganisation(inputAlert.organisation) createdAlert <- alertSrv.createEntity(inputAlert.alert.copy(organisationId = organisation._id, caseId = `case`.map(_._id))) - _ = updateMetaData(createdAlert, inputAlert.metaData) + tags = inputAlert.alert.tags.flatMap(getTag(_, organisation._id.value).toOption) + _ = updateMetaData(createdAlert, inputAlert.metaData) _ <- alertSrv.alertOrganisationSrv.create(AlertOrganisation(), createdAlert, organisation) _ <- inputAlert diff --git a/thehive/app/org/thp/thehive/controllers/v0/ObservableCtrl.scala b/thehive/app/org/thp/thehive/controllers/v0/ObservableCtrl.scala index c1b21d0240..8e6b82c4ef 100644 --- a/thehive/app/org/thp/thehive/controllers/v0/ObservableCtrl.scala +++ b/thehive/app/org/thp/thehive/controllers/v0/ObservableCtrl.scala @@ -374,7 +374,6 @@ class PublicObservable @Inject() ( override val pageQuery: ParamQuery[OutputParam] = Query.withParam[OutputParam, Traversal.V[Observable], IteratorOutput]( "page", - FieldsParser[OutputParam], { case (OutputParam(from, to, withStats, 0), observableSteps, authContext) => observableSteps @@ -418,7 +417,7 @@ class PublicObservable @Inject() ( observableSrv .get(vertex)(graph) .getOrFail("Observable") - .flatMap(observable => observableSrv.updateTagNames(observable, value)(graph, authContext)) + .flatMap(observable => observableSrv.updateTags(observable, value)(graph, authContext)) .map(_ => Json.obj("tags" -> value)) } ) diff --git a/thehive/app/org/thp/thehive/controllers/v0/TagCtrl.scala b/thehive/app/org/thp/thehive/controllers/v0/TagCtrl.scala index e90c8f623a..f291927063 100644 --- a/thehive/app/org/thp/thehive/controllers/v0/TagCtrl.scala +++ b/thehive/app/org/thp/thehive/controllers/v0/TagCtrl.scala @@ -6,11 +6,12 @@ import org.thp.scalligraph.models.{Database, Entity, UMapping} import org.thp.scalligraph.query._ import org.thp.scalligraph.traversal.TraversalOps._ import org.thp.scalligraph.traversal.{Converter, IteratorOutput, Traversal} +import org.thp.scalligraph.utils.FunctionalCondition.When import org.thp.scalligraph.{EntityIdOrName, RichSeq} import org.thp.thehive.controllers.v0.Conversion._ import org.thp.thehive.models.{Permissions, Tag} import org.thp.thehive.services.TagOps._ -import org.thp.thehive.services.TagSrv +import org.thp.thehive.services.{OrganisationSrv, TagSrv} import play.api.libs.json.{JsNumber, JsObject, JsValue, Json} import play.api.mvc.{Action, AnyContent, Results} @@ -99,8 +100,10 @@ class TagCtrl @Inject() ( } } +case class TagHint(freeTag: Option[String], namespace: Option[String], predicate: Option[String], value: Option[String], limit: Option[Long]) + @Singleton -class PublicTag @Inject() (tagSrv: TagSrv) extends PublicData { +class PublicTag @Inject() (tagSrv: TagSrv, organisationSrv: OrganisationSrv) extends PublicData { override val entityName: String = "tag" override val initialQuery: Query = Query.init[Traversal.V[Tag]]("listTag", (graph, _) => tagSrv.startTraversal(graph)) override val pageQuery: ParamQuery[OutputParam] = Query.withParam[OutputParam, Traversal.V[Tag], IteratorOutput]( @@ -118,6 +121,16 @@ class PublicTag @Inject() (tagSrv: TagSrv) extends PublicData { Query[Traversal.V[Tag], Traversal.V[Tag]]("fromCase", (tagSteps, _) => tagSteps.fromCase), Query[Traversal.V[Tag], Traversal.V[Tag]]("fromObservable", (tagSteps, _) => tagSteps.fromObservable), Query[Traversal.V[Tag], Traversal.V[Tag]]("fromAlert", (tagSteps, _) => tagSteps.fromAlert), + Query.withParam[TagHint, Traversal.V[Tag], Traversal.V[Tag]]( + "autoComplete", + (tagHint, tags, authContext) => + tagHint + .freeTag + .fold(tags.autoComplete(tagHint.namespace, tagHint.predicate, tagHint.value)(authContext))( + tags.autoComplete(organisationSrv, _)(authContext) + ) + .merge(tagHint.limit)(_.limit(_)) + ), Query[Traversal.V[Tag], Traversal[String, Vertex, Converter[String, Vertex]]]("text", (tagSteps, _) => tagSteps.displayName), Query.output[String, Traversal[String, Vertex, Converter[String, Vertex]]] ) diff --git a/thehive/app/org/thp/thehive/controllers/v1/Properties.scala b/thehive/app/org/thp/thehive/controllers/v1/Properties.scala index 43c9a1bfce..65e443d3e6 100644 --- a/thehive/app/org/thp/thehive/controllers/v1/Properties.scala +++ b/thehive/app/org/thp/thehive/controllers/v1/Properties.scala @@ -371,7 +371,7 @@ class Properties @Inject() ( _.field.custom { (_, value, vertex, graph, authContext) => observableSrv .getOrFail(vertex)(graph) - .flatMap(observable => observableSrv.updateTagNames(observable, value)(graph, authContext)) + .flatMap(observable => observableSrv.updateTags(observable, value)(graph, authContext)) .map(_ => Json.obj("tags" -> value)) } ) @@ -393,12 +393,4 @@ class Properties @Inject() ( .property("version", UMapping.int)(_.field.readonly) .property("enabled", UMapping.boolean)(_.select(_.enabled).readonly) .build - - private def vertexToTag: Vertex => String = { v => - val namespace = UMapping.string.getProperty(v, "namespace") - val predicate = UMapping.string.getProperty(v, "predicate") - val value = UMapping.string.optional.getProperty(v, "value") - Tag(namespace, predicate, value, None, "#000000").toString - } - } diff --git a/thehive/app/org/thp/thehive/controllers/v1/TaxonomyCtrl.scala b/thehive/app/org/thp/thehive/controllers/v1/TaxonomyCtrl.scala index 743b778a10..dfb4179b7a 100644 --- a/thehive/app/org/thp/thehive/controllers/v1/TaxonomyCtrl.scala +++ b/thehive/app/org/thp/thehive/controllers/v1/TaxonomyCtrl.scala @@ -43,7 +43,6 @@ class TaxonomyCtrl @Inject() ( override val pageQuery: ParamQuery[OutputParam] = Query.withParam[OutputParam, Traversal.V[Taxonomy], IteratorOutput]( "page", - FieldsParser[OutputParam], { case (OutputParam(from, to, extraData), taxoSteps, authContext) => taxoSteps.richPage(from, to, extraData.contains("total")) { @@ -105,12 +104,12 @@ class TaxonomyCtrl @Inject() ( // Create tags val tagValues = inputTaxo.values.getOrElse(Seq()) val tags = tagValues.flatMap { value => - value.entry.map(e => Tag(inputTaxo.namespace, value.predicate, Some(e.value), e.expanded, e.colour.getOrElse(tagSrv.defaultColour))) + value.entry.map(e => Tag(inputTaxo.namespace, value.predicate, Some(e.value), e.expanded, e.colour.getOrElse(tagSrv.freeTagColour))) } // Create a tag for predicates with no tags associated val predicateWithNoTags = inputTaxo.predicates.map(_.value).diff(tagValues.map(_.predicate)) - val allTags = tags ++ predicateWithNoTags.map(p => Tag(inputTaxo.namespace, p, None, None, tagSrv.defaultColour)) + val allTags = tags ++ predicateWithNoTags.map(p => Tag(inputTaxo.namespace, p, None, None, tagSrv.freeTagColour)) if (inputTaxo.namespace.isEmpty) Failure(BadRequestError(s"A taxonomy with no namespace cannot be imported")) diff --git a/thehive/app/org/thp/thehive/models/Tag.scala b/thehive/app/org/thp/thehive/models/Tag.scala index ee1264f61e..ab928bff34 100644 --- a/thehive/app/org/thp/thehive/models/Tag.scala +++ b/thehive/app/org/thp/thehive/models/Tag.scala @@ -7,6 +7,7 @@ import play.api.Logger import scala.util.matching.Regex @DefineIndex(IndexType.unique, "namespace", "predicate", "value") +@DefineIndex(IndexType.fulltext, "namespace", "predicate", "value", "description") @BuildVertexEntity case class Tag( namespace: String, @@ -17,10 +18,13 @@ case class Tag( ) { override def hashCode(): Int = 31 * (31 * value.## + predicate.##) + namespace.## - override def equals(obj: Any): Boolean = obj match { - case Tag(n, p, v, _, _) => n == namespace && p == predicate && v == value - case _ => false - } + override def equals(obj: Any): Boolean = + obj match { + case Tag(n, p, v, _, _) => n == namespace && p == predicate && v == value + case _ => false + } + + lazy val isFreeTag: Boolean = namespace.startsWith("_freetags_") override def canEqual(that: Any): Boolean = that.isInstanceOf[Tag] @@ -35,8 +39,8 @@ object Tag { val tagColour: Regex = "(.*)(#\\p{XDigit}{6})".r val namespacePredicateValue: Regex = "([^\".:=]+)[.:]([^\".=]+)=\"?([^\"]+)\"?".r val namespacePredicate: Regex = "([^\".:=]+)[.]([^\".=]+)".r - val PredicateValue: Regex = "([^\".:=]+)[=:]\"?([^\"]+)\"?".r - val predicate: Regex = "([^\".:=]+)".r +// val PredicateValue: Regex = "([^\".:=]+)[=:]\"?([^\"]+)\"?".r + val predicate: Regex = "([^\".:=]+)".r def fromString(tagName: String, defaultNamespace: String, defaultColour: String = "#000000"): Tag = { val (name, colour) = tagName match { @@ -47,9 +51,9 @@ object Tag { case namespacePredicateValue(namespace, predicate, value) if value.exists(_ != '=') => Tag(namespace.trim, predicate.trim, Some(value.trim), None, colour) case namespacePredicate(namespace, predicate) => Tag(namespace.trim, predicate.trim, None, None, colour) - case PredicateValue(predicate, value) => Tag(defaultNamespace, predicate.trim, Some(value.trim), None, colour) - case predicate(predicate) => Tag(defaultNamespace, predicate.trim, None, None, colour) - case _ => Tag(defaultNamespace, name, None, None, colour) +// case PredicateValue(predicate, value) => Tag(defaultNamespace, predicate.trim, Some(value.trim), None, colour) + case predicate(predicate) => Tag(defaultNamespace, predicate.trim, None, None, colour) + case _ => Tag(defaultNamespace, name, None, None, colour) } } } diff --git a/thehive/app/org/thp/thehive/models/TheHiveSchemaDefinition.scala b/thehive/app/org/thp/thehive/models/TheHiveSchemaDefinition.scala index d33a3dcf3c..b93c1c69f9 100644 --- a/thehive/app/org/thp/thehive/models/TheHiveSchemaDefinition.scala +++ b/thehive/app/org/thp/thehive/models/TheHiveSchemaDefinition.scala @@ -1,18 +1,18 @@ package org.thp.thehive.models -import org.apache.tinkerpop.gremlin.process.traversal.P +import org.apache.tinkerpop.gremlin.process.traversal.{Order, P, TextP} import org.apache.tinkerpop.gremlin.structure.VertexProperty.Cardinality import org.janusgraph.core.schema.ConsistencyModifier import org.janusgraph.graphdb.types.TypeDefinitionCategory import org.reflections.Reflections import org.reflections.scanners.SubTypesScanner import org.reflections.util.ConfigurationBuilder +import org.thp.scalligraph.EntityId import org.thp.scalligraph.auth.AuthContext import org.thp.scalligraph.janus.JanusDatabase import org.thp.scalligraph.models._ import org.thp.scalligraph.traversal.TraversalOps._ import org.thp.scalligraph.traversal.{Converter, Graph, Traversal} -import org.thp.scalligraph.{EntityId, RichSeq} import org.thp.thehive.services.LocalUserSrv import play.api.Logger @@ -94,59 +94,70 @@ class TheHiveSchemaDefinition @Inject() extends Schema with UpdatableSchema { Success(()) } //=====[release 4.0.3]===== + //=====[release 4.0.4]===== // Taxonomies - .addVertexModel[String]("Taxonomy", Seq("namespace")) + .addVertexModel[String]("Taxonomy") + .addProperty[String]("Taxonomy", "namespace") + .addProperty[String]("Taxonomy", "description") + .addProperty[Int]("Taxonomy", "version") .dbOperation[Database]("Add Custom taxonomy vertex for each Organisation") { db => db.tryTransaction { implicit g => // For each organisation, if there is no custom taxonomy, create it - db.labelFilter("Organisation", Traversal.V()).unsafeHas("name", P.neq("admin")).toIterator.toTry { o => - Traversal.V(EntityId(o.id)).out[OrganisationTaxonomy].v[Taxonomy].unsafeHas("namespace", s"_freetags_${o.id()}").headOption match { - case None => - val taxoVertex = g.addVertex("Taxonomy") - taxoVertex.property("_label", "Taxonomy") - taxoVertex.property("_createdBy", "system@thehive.local") - taxoVertex.property("_createdAt", new Date()) - taxoVertex.property("namespace", s"_freetags_${o.id()}") - taxoVertex.property("description", "Custom taxonomy") - taxoVertex.property("version", 1) - o.addEdge("OrganisationTaxonomy", taxoVertex) - Success(()) - case _ => Success(()) + db.labelFilter("Organisation", Traversal.V()).unsafeHas("name", P.neq("admin")).foreach { o => + val hasFreetagsTaxonomy = Traversal + .V(EntityId(o.id)) + .out[OrganisationTaxonomy] + .v[Taxonomy] + .unsafeHas("namespace", s"_freetags_${o.id()}") + .exists + if (!hasFreetagsTaxonomy) { + val taxoVertex = g.addVertex("Taxonomy") + taxoVertex.property("_label", "Taxonomy") + taxoVertex.property("_createdBy", "system@thehive.local") + taxoVertex.property("_createdAt", new Date()) + taxoVertex.property("namespace", s"_freetags_${o.id()}") + taxoVertex.property("description", "Custom taxonomy") + taxoVertex.property("version", 1) + o.addEdge("OrganisationTaxonomy", taxoVertex) } } - }.map(_ => ()) + Success(()) + } } - .dbOperation[Database]("Add each tag to its Organisation's Custom taxonomy") { db => - db.tryTransaction { implicit g => - db.labelFilter("Organisation", Traversal.V()).unsafeHas("name", P.neq("admin")).toIterator.toTry { o => - val customTaxo = Traversal.V(EntityId(o.id())).out("OrganisationTaxonomy").unsafeHas("namespace", s"_freetags_${o.id()}").head - Traversal - .V(EntityId(o.id())) - .unionFlat( - _.out("OrganisationShare").out("ShareCase").out("CaseTag"), - _.out("OrganisationShare").out("ShareObservable").out("ObservableTag"), - _.in("AlertOrganisation").out("AlertTag"), - _.in("CaseTemplateOrganisation").out("CaseTemplateTag") + .updateGraph("Add each tag to its Organisation's FreeTags taxonomy", "Tag") { tags => + tags + .project( + _.by.by( + _.unionFlat( + _.in("CaseTag").in("ShareCase").in("OrganisationShare"), + _.in("ObservableTag").unionFlat(_.in("ShareObservable").in("OrganisationShare"), _.in("AlertObservable").out("AlertOrganisation")), + _.in("AlertTag").out("AlertOrganisation"), + _.in("CaseTemplateTag").out("CaseTemplateOrganisation") ) - .toSeq - .foreach { tag => - // Create a freetext tag and store it into predicate - val tagStr = tagString( - tag.property("namespace").value().toString, - tag.property("predicate").value().toString, - tag.property("value").orElse("") - ) - tag.property("namespace", s"_freetags_${o.id()}") - tag.property("predicate", tagStr) - tag.property("value").remove() - customTaxo.addEdge("TaxonomyTag", tag) - } - Success(()) + .dedup + .sort(_.by("_createdAt", Order.desc)) + .limit(1) + .out("OrganisationTaxonomy") + .unsafeHas("namespace", TextP.startingWith("_freetags_")) + .option + ) + ) + .foreach { + case (tag, Some(freeTagsTaxo)) => + val tagStr = tagString( + tag.property[String]("namespace").value(), + tag.property[String]("predicate").value(), + tag.property[String]("value").orElse("") + ) + tag.property("namespace", freeTagsTaxo.property[String]("namespace").value) + tag.property("predicate", tagStr) + tag.property("value").remove() + freeTagsTaxo.addEdge("TaxonomyTag", tag) } - }.map(_ => ()) + Success(()) } .updateGraph("Add manageTaxonomy to admin profile", "Profile") { traversal => - Try(traversal.unsafeHas("name", "admin").raw.property("permissions", "manageTaxonomy").iterate()) + traversal.unsafeHas("name", "admin").raw.property("permissions", "manageTaxonomy").iterate() Success(()) } .updateGraph("Remove colour property for Tags", "Tag") { traversal => @@ -159,6 +170,7 @@ class TheHiveSchemaDefinition @Inject() extends Schema with UpdatableSchema { traversal.raw.property("colour", "#000000").iterate() Success(()) } + // Patterns .updateGraph("Add managePattern permission to admin profile", "Profile") { traversal => traversal.unsafeHas("name", "admin").raw.property("permissions", "managePattern").iterate() Success(()) @@ -171,7 +183,7 @@ class TheHiveSchemaDefinition @Inject() extends Schema with UpdatableSchema { .iterate() Success(()) } - //=====[release 4.0.3]===== + // Index backend /* Alert index */ .addProperty[Seq[String]]("Alert", "tags") .addProperty[EntityId]("Alert", "organisationId") diff --git a/thehive/app/org/thp/thehive/services/AlertSrv.scala b/thehive/app/org/thp/thehive/services/AlertSrv.scala index 0b687cc09d..ee94e58718 100644 --- a/thehive/app/org/thp/thehive/services/AlertSrv.scala +++ b/thehive/app/org/thp/thehive/services/AlertSrv.scala @@ -175,7 +175,7 @@ class AlertSrv @Inject() ( } { existingObservable => val tags = (existingObservable.tags ++ richObservable.tags).toSet if ((tags -- existingObservable.tags).nonEmpty) - observableSrv.updateTagNames(existingObservable.observable, tags) + observableSrv.updateTags(existingObservable.observable, tags) Success(()) } } @@ -344,7 +344,7 @@ class AlertSrv @Inject() ( .headOption .foreach { observable => val newTags = (observable.tags ++ richObservable.tags).toSet - observableSrv.updateTagNames(observable, newTags) + observableSrv.updateTags(observable, newTags) } } } diff --git a/thehive/app/org/thp/thehive/services/CaseSrv.scala b/thehive/app/org/thp/thehive/services/CaseSrv.scala index 1d247a888b..5c8e3d91f7 100644 --- a/thehive/app/org/thp/thehive/services/CaseSrv.scala +++ b/thehive/app/org/thp/thehive/services/CaseSrv.scala @@ -159,8 +159,8 @@ class CaseSrv @Inject() ( tagsToAdd <- (tags -- `case`.tags).toTry(tagSrv.getOrCreate) tagsToRemove <- (`case`.tags.toSet -- tags).toTry(tagSrv.getOrCreate) _ <- tagsToAdd.toTry(caseTagSrv.create(CaseTag(), `case`, _)) - _ = if (tags.nonEmpty) get(`case`).outE[AlertTag].filter(_.otherV.hasId(tagsToRemove.map(_._id): _*)).remove() - _ <- get(`case`).update(_.tags, tags).getOrFail("Alert") + _ = if (tags.nonEmpty) get(`case`).outE[CaseTag].filter(_.otherV.hasId(tagsToRemove.map(_._id): _*)).remove() + _ <- get(`case`).update(_.tags, tags.toSeq).getOrFail("Case") _ <- auditSrv.`case`.update(`case`, Json.obj("tags" -> tags)) } yield (tagsToAdd, tagsToRemove) diff --git a/thehive/app/org/thp/thehive/services/CaseTemplateSrv.scala b/thehive/app/org/thp/thehive/services/CaseTemplateSrv.scala index e99df05864..67ab6f9dad 100644 --- a/thehive/app/org/thp/thehive/services/CaseTemplateSrv.scala +++ b/thehive/app/org/thp/thehive/services/CaseTemplateSrv.scala @@ -93,8 +93,8 @@ class CaseTemplateSrv @Inject() ( tagsToAdd <- (tags -- caseTemplate.tags).toTry(tagSrv.getOrCreate) tagsToRemove <- (caseTemplate.tags.toSet -- tags).toTry(tagSrv.getOrCreate) _ <- tagsToAdd.toTry(caseTemplateTagSrv.create(CaseTemplateTag(), caseTemplate, _)) - _ = if (tags.nonEmpty) get(caseTemplate).outE[AlertTag].filter(_.otherV.hasId(tagsToRemove.map(_._id): _*)).remove() - _ <- get(caseTemplate).update(_.tags, tags).getOrFail("Alert") + _ = if (tags.nonEmpty) get(caseTemplate).outE[CaseTemplateTag].filter(_.otherV.hasId(tagsToRemove.map(_._id): _*)).remove() + _ <- get(caseTemplate).update(_.tags, tags.toSeq).getOrFail("CaseTemplate") _ <- auditSrv.caseTemplate.update(caseTemplate, Json.obj("tags" -> tags)) } yield (tagsToAdd, tagsToRemove) diff --git a/thehive/app/org/thp/thehive/services/ObservableSrv.scala b/thehive/app/org/thp/thehive/services/ObservableSrv.scala index 6395072d8c..a92e3daa25 100644 --- a/thehive/app/org/thp/thehive/services/ObservableSrv.scala +++ b/thehive/app/org/thp/thehive/services/ObservableSrv.scala @@ -16,7 +16,7 @@ import org.thp.thehive.services.AlertOps._ import org.thp.thehive.services.ObservableOps._ import org.thp.thehive.services.OrganisationOps._ import org.thp.thehive.services.ShareOps._ -import play.api.libs.json.JsObject +import play.api.libs.json.{JsObject, Json} import java.util.{Map => JMap} import javax.inject.{Inject, Provider, Singleton} @@ -118,23 +118,18 @@ class ObservableSrv @Inject() ( } yield createdTags } - def updateTagNames(observable: Observable with Entity, tags: Set[String])(implicit graph: Graph, authContext: AuthContext): Try[Unit] = - tags.toTry(tagSrv.getOrCreate).flatMap(t => updateTags(observable, t.toSet)) - - def updateTags(observable: Observable with Entity, tags: Set[Tag with Entity])(implicit graph: Graph, authContext: AuthContext): Try[Unit] = { - val (tagsToAdd, tagsToRemove) = get(observable) - .tags - .toIterator - .foldLeft((tags, Set.empty[Tag with Entity])) { - case ((toAdd, toRemove), t) if toAdd.contains(t) => (toAdd - t, toRemove) - case ((toAdd, toRemove), t) => (toAdd, toRemove + t) - } + def updateTags(observable: Observable with Entity, tags: Set[String])(implicit + graph: Graph, + authContext: AuthContext + ): Try[(Seq[Tag with Entity], Seq[Tag with Entity])] = for { - _ <- tagsToAdd.toTry(observableTagSrv.create(ObservableTag(), observable, _)) - _ = get(observable).removeTags(tagsToRemove) - // _ <- auditSrv.observable.update(observable, Json.obj("tags" -> tags)) TODO add context (case or alert ?) - } yield () - } + tagsToAdd <- (tags -- observable.tags).toTry(tagSrv.getOrCreate) + tagsToRemove <- (observable.tags.toSet -- tags).toTry(tagSrv.getOrCreate) + _ <- tagsToAdd.toTry(observableTagSrv.create(ObservableTag(), observable, _)) + _ = if (tags.nonEmpty) get(observable).outE[ObservableTag].filter(_.otherV.hasId(tagsToRemove.map(_._id): _*)).remove() + _ <- get(observable).update(_.tags, tags.toSeq).getOrFail("Observable") + _ <- auditSrv.observable.update(observable, Json.obj("tags" -> tags)) + } yield (tagsToAdd, tagsToRemove) def remove(observable: Observable with Entity)(implicit graph: Graph, authContext: AuthContext): Try[Unit] = get(observable).alert.headOption match { diff --git a/thehive/app/org/thp/thehive/services/TagSrv.scala b/thehive/app/org/thp/thehive/services/TagSrv.scala index 850f72a452..6c33f0816d 100644 --- a/thehive/app/org/thp/thehive/services/TagSrv.scala +++ b/thehive/app/org/thp/thehive/services/TagSrv.scala @@ -1,6 +1,7 @@ package org.thp.thehive.services import akka.actor.ActorRef +import org.apache.tinkerpop.gremlin.process.traversal.TextP import org.apache.tinkerpop.gremlin.structure.Vertex import org.thp.scalligraph.auth.AuthContext import org.thp.scalligraph.models.{Database, Entity} @@ -8,40 +9,38 @@ 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, Graph, Traversal} -import org.thp.thehive.models.{AlertTag, CaseTag, ObservableTag, Tag} +import org.thp.scalligraph.utils.FunctionalCondition.When +import org.thp.thehive.models.{AlertTag, CaseTag, ObservableTag, Organisation, OrganisationTaxonomy, Tag, Taxonomy, TaxonomyTag} import org.thp.thehive.services.TagOps._ +import org.thp.thehive.services.OrganisationOps._ import javax.inject.{Inject, Named, Singleton} import scala.util.{Success, Try} @Singleton -class TagSrv @Inject() (appConfig: ApplicationConfig, @Named("integrity-check-actor") integrityCheckActor: ActorRef) extends VertexSrv[Tag] { +class TagSrv @Inject() (organisationSrv: OrganisationSrv, appConfig: ApplicationConfig, @Named("integrity-check-actor") integrityCheckActor: ActorRef) + extends VertexSrv[Tag] { - private val autoCreateConfig: ConfigItem[Boolean, Boolean] = - appConfig.item[Boolean]("tags.autocreate", "If true, create automatically tag if it doesn't exist") + private val freeTagColourConfig: ConfigItem[String, String] = + appConfig.item[String]("tags.freeTagColour", "Default colour for free tags") - def autoCreate: Boolean = autoCreateConfig.get + def freeTagColour: String = freeTagColourConfig.get - private val defaultNamespaceConfig: ConfigItem[String, String] = - appConfig.item[String]("tags.defaultNamespace", "Default namespace of the automatically created tags") + def freeTagNamespace(implicit graph: Graph, authContext: AuthContext): String = + s"_freetags_${organisationSrv.currentId(graph, authContext).value}" - def defaultNamespace: String = defaultNamespaceConfig.get - - private val defaultColourConfig: ConfigItem[String, String] = - appConfig.item[String]("tags.defaultColour", "Default colour of the automatically created tags") - - def defaultColour: String = defaultColourConfig.get - - def parseString(tagName: String): Tag = - Tag.fromString(tagName, defaultNamespace, defaultColour) + def parseString(tagName: String)(implicit graph: Graph, authContext: AuthContext): Tag = { + val ns = freeTagNamespace + val tag = Tag.fromString(tagName, ns, freeTagColour) + if (tag.isFreeTag) Tag(ns, tagName, None, None, freeTagColour) + else tag + } def getTag(tag: Tag)(implicit graph: Graph): Traversal.V[Tag] = startTraversal.getTag(tag) def getOrCreate(tagName: String)(implicit graph: Graph, authContext: AuthContext): Try[Tag with Entity] = { val tag = parseString(tagName) - getTag(tag).getOrFail("Tag").recoverWith { - case _ if autoCreate => create(tag) - } + getTag(tag).headOption.fold(create(tag))(Success(_)) } override def createEntity(e: Tag)(implicit graph: Graph, authContext: AuthContext): Try[Tag with Entity] = { @@ -67,6 +66,9 @@ object TagOps { value.fold(t.hasNot(_.value))(v => t.has(_.value, v)) } + def taxonomy: Traversal.V[Taxonomy] = traversal.in[TaxonomyTag].v[Taxonomy] + + def organisation: Traversal.V[Organisation] = traversal.in[TaxonomyTag].in[OrganisationTaxonomy].v[Organisation] def displayName: Traversal[String, Vertex, Converter[String, Vertex]] = traversal.domainMap(_.toString) def fromCase: Traversal.V[Tag] = traversal.filter(_.in[CaseTag]) @@ -74,8 +76,27 @@ object TagOps { def fromObservable: Traversal.V[Tag] = traversal.filter(_.in[ObservableTag]) def fromAlert: Traversal.V[Tag] = traversal.filter(_.in[AlertTag]) - } + def autoComplete(organisationSrv: OrganisationSrv, freeTag: String)(implicit authContext: AuthContext): Traversal.V[Tag] = { + val freeTagNamespace: String = s"_freetags_${organisationSrv.currentId(traversal.graph, authContext).value}" + traversal + .has(_.namespace, freeTagNamespace) + .has(_.predicate, TextP.containing(freeTag)) + } + def autoComplete(namespace: Option[String], predicate: Option[String], value: Option[String])(implicit + authContext: AuthContext + ): Traversal.V[Tag] = { + traversal.graph.db.mapPredicate(TextP.containing("")) + traversal + .merge(namespace)((t, ns) => t.has(_.namespace, TextP.containing(ns))) + .merge(predicate)((t, p) => t.has(_.predicate, TextP.containing(p))) + .merge(value)((t, v) => t.has(_.value, TextP.containing(v))) + .visible + } + + def visible(implicit authContext: AuthContext): Traversal.V[Tag] = + traversal.filter(_.organisation.current) + } } class TagIntegrityCheckOps @Inject() (val db: Database, val service: TagSrv) extends IntegrityCheckOps[Tag] { diff --git a/thehive/app/org/thp/thehive/services/TaxonomySrv.scala b/thehive/app/org/thp/thehive/services/TaxonomySrv.scala index f510103426..4166e2f897 100644 --- a/thehive/app/org/thp/thehive/services/TaxonomySrv.scala +++ b/thehive/app/org/thp/thehive/services/TaxonomySrv.scala @@ -31,7 +31,7 @@ class TaxonomySrv @Inject() (organisationSrvProvider: Provider[OrganisationSrv]) } yield richTaxonomy def createFreetag(organisation: Organisation with Entity)(implicit graph: Graph, authContext: AuthContext): Try[RichTaxonomy] = { - val customTaxo = Taxonomy(s"_freetags_${organisation._id}", "Custom taxonomy", 1) + val customTaxo = Taxonomy(s"_freetags_${organisation._id.value}", "Custom taxonomy", 1) for { taxonomy <- createEntity(customTaxo) richTaxonomy <- Try(RichTaxonomy(taxonomy, Seq())) diff --git a/thehive/conf/reference.conf b/thehive/conf/reference.conf index 523fb7d7d1..fba4e92834 100644 --- a/thehive/conf/reference.conf +++ b/thehive/conf/reference.conf @@ -44,11 +44,7 @@ stream.longPolling { pollingDuration: 1 second } -tags { - autocreate: true - defaultNamespace: "_autocreate" - defaultColour: "#000000" -} +tags.freeTagColour: "#000000" user { defaults { diff --git a/thehive/test/org/thp/thehive/controllers/v0/ConfigCtrlTest.scala b/thehive/test/org/thp/thehive/controllers/v0/ConfigCtrlTest.scala index 17e88bb138..b3a550642b 100644 --- a/thehive/test/org/thp/thehive/controllers/v0/ConfigCtrlTest.scala +++ b/thehive/test/org/thp/thehive/controllers/v0/ConfigCtrlTest.scala @@ -39,7 +39,7 @@ class ConfigCtrlTest extends PlaySpecification with TestAppBuilder { status(result) must equalTo(204).updateMessage(s => s"$s\n${contentAsString(result)}") - app[TagSrv].defaultColour must beEqualTo("#00FF00") + app[TagSrv].freeTagColour must beEqualTo("#00FF00") } // TODO leave unused tests ? //