Skip to content

Commit

Permalink
#1670 Make freetags updatable
Browse files Browse the repository at this point in the history
  • Loading branch information
To-om committed Mar 3, 2021
1 parent 6dfcb2a commit e192c53
Show file tree
Hide file tree
Showing 10 changed files with 129 additions and 235 deletions.
1 change: 0 additions & 1 deletion thehive/app/org/thp/thehive/controllers/v0/Router.scala
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,6 @@ class Router @Inject() (
case GET(p"/tag") => tagCtrl.search
case POST(p"/tag/_search") => tagCtrl.search
case POST(p"/tag/_stats") => tagCtrl.stats
case POST(p"/tag/_import") => tagCtrl.importTaxonomy
case GET(p"/tag/$id") => tagCtrl.get(id)

case GET(p"/user") => userCtrl.search
Expand Down
73 changes: 4 additions & 69 deletions thehive/app/org/thp/thehive/controllers/v0/TagCtrl.scala
Original file line number Diff line number Diff line change
@@ -1,21 +1,19 @@
package org.thp.thehive.controllers.v0

import org.apache.tinkerpop.gremlin.structure.Vertex
import org.thp.scalligraph.controllers.{Entrypoint, FFile, FieldsParser, Renderer}
import org.thp.scalligraph.EntityIdOrName
import org.thp.scalligraph.controllers.{Entrypoint, Renderer}
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.models.Tag
import org.thp.thehive.services.TagOps._
import org.thp.thehive.services.{OrganisationSrv, TagSrv}
import play.api.libs.json.{JsNumber, JsObject, JsValue, Json}
import play.api.mvc.{Action, AnyContent, Results}

import java.nio.file.Files
import javax.inject.{Inject, Named, Singleton}

class TagCtrl @Inject() (
Expand All @@ -25,69 +23,6 @@ class TagCtrl @Inject() (
@Named("v0") override val queryExecutor: QueryExecutor,
override val publicData: PublicTag
) extends QueryCtrl {
def importTaxonomy: Action[AnyContent] =
entrypoint("import taxonomy")
.extract("file", FieldsParser.file.optional.on("file"))
.extract("content", FieldsParser.jsObject.optional.on("content"))
.authPermittedTransaction(db, Permissions.manageTag) { implicit request => implicit graph =>
val file: Option[FFile] = request.body("file")
val content: Option[JsObject] = request.body("content")
val tags = file
.fold(Seq.empty[Tag])(ffile => parseTaxonomy(Json.parse(Files.newInputStream(ffile.filepath)))) ++
content.fold(Seq.empty[Tag])(parseTaxonomy)

tags
.filterNot(tagSrv.startTraversal.getTag(_).exists)
.toTry(tagSrv.create)
.map(ts => Results.Ok(JsNumber(ts.size)))
}

def parseTaxonomy(taxonomy: JsValue): Seq[Tag] =
(taxonomy \ "namespace").asOpt[String].fold(Seq.empty[Tag]) { namespace =>
(taxonomy \ "values").asOpt[Seq[JsObject]].filter(_.nonEmpty) match {
case Some(values) => parseValues(namespace, values)
case _ => (taxonomy \ "predicates").asOpt[Seq[JsObject]].fold(Seq.empty[Tag])(parsePredicates(namespace, _))
}
}

def parseValues(namespace: String, values: Seq[JsObject]): Seq[Tag] =
for {
value <-
values
.foldLeft((Seq.empty[JsObject], Seq.empty[String]))((acc, v) => distinct((v \ "predicate").asOpt[String], acc, v))
._1
predicate <- (value \ "predicate").asOpt[String].toList
entry <-
(value \ "entry")
.asOpt[Seq[JsObject]]
.getOrElse(Nil)
.foldLeft((Seq.empty[JsObject], Seq.empty[String]))((acc, v) => distinct((v \ "value").asOpt[String], acc, v))
._1
v <- (entry \ "value").asOpt[String]
colour =
(entry \ "colour")
.asOpt[String]
.getOrElse("#000000")
e = (entry \ "description").asOpt[String] orElse (entry \ "expanded").asOpt[String]
} yield Tag(namespace, predicate, Some(v), e, colour)

private def distinct(valueOpt: Option[String], acc: (Seq[JsObject], Seq[String]), v: JsObject): (Seq[JsObject], Seq[String]) =
if (valueOpt.isDefined && acc._2.contains(valueOpt.get)) acc
else (acc._1 :+ v, valueOpt.fold(acc._2)(acc._2 :+ _))

def parsePredicates(namespace: String, predicates: Seq[JsObject]): Seq[Tag] =
for {
predicate <-
predicates
.foldLeft((Seq.empty[JsObject], Seq.empty[String]))((acc, v) => distinct((v \ "value").asOpt[String], acc, v))
._1
v <- (predicate \ "value").asOpt[String]
e = (predicate \ "expanded").asOpt[String]
colour =
(predicate \ "colour")
.asOpt[String]
.getOrElse("#000000")
} yield Tag(namespace, v, None, e, colour)

def get(tagId: String): Action[AnyContent] =
entrypoint("get tag")
Expand Down Expand Up @@ -122,7 +57,7 @@ class PublicTag @Inject() (tagSrv: TagSrv, organisationSrv: OrganisationSrv) ext
Query[Traversal.V[Tag], Traversal.V[Tag]]("fromObservable", (tagSteps, _) => tagSteps.fromObservable),
Query[Traversal.V[Tag], Traversal.V[Tag]]("fromAlert", (tagSteps, _) => tagSteps.fromAlert),
Query.initWithParam[TagHint, Traversal[String, Vertex, Converter[String, Vertex]]](
"autoComplete",
"TagAutoComplete",
(tagHint, graph, authContext) =>
tagHint
.freeTag
Expand Down
8 changes: 5 additions & 3 deletions thehive/app/org/thp/thehive/controllers/v1/Conversion.scala
Original file line number Diff line number Diff line change
Expand Up @@ -299,9 +299,11 @@ object Conversion {
.transform
}

implicit val tagOutput: Renderer.Aux[Tag, OutputTag] =
Renderer.toJson[Tag, OutputTag](
_.into[OutputTag]
implicit val tagOutput: Renderer.Aux[Tag with Entity, OutputTag] =
Renderer.toJson[Tag with Entity, OutputTag](tag =>
tag
.asInstanceOf[Tag]
.into[OutputTag]
.withFieldComputed(_.namespace, t => if (t.isFreeTag) "_freetags_" else t.namespace)
.transform
)
Expand Down
21 changes: 21 additions & 0 deletions thehive/app/org/thp/thehive/controllers/v1/Properties.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import org.thp.thehive.services.OrganisationOps._
import org.thp.thehive.services.PatternOps._
import org.thp.thehive.services.ProcedureOps._
import org.thp.thehive.services.ShareOps._
import org.thp.thehive.services.TagOps._
import org.thp.thehive.services.TaskOps._
import org.thp.thehive.services.TaxonomyOps._
import org.thp.thehive.services.UserOps._
Expand All @@ -36,6 +37,7 @@ class Properties @Inject() (
caseTemplateSrv: CaseTemplateSrv,
observableSrv: ObservableSrv,
customFieldSrv: CustomFieldSrv,
organisationSrv: OrganisationSrv,
db: Database
) {

Expand Down Expand Up @@ -407,4 +409,23 @@ class Properties @Inject() (
.property("version", UMapping.int)(_.field.readonly)
.property("enabled", UMapping.boolean)(_.select(_.enabled).readonly)
.build

lazy val tag: PublicProperties =
PublicPropertyListBuilder[Tag]
.property("namespace", UMapping.string)(_.field.readonly)
.property("predicate", UMapping.string)(_.field.updatable)
.property("value", UMapping.string.optional)(_.field.readonly)
.property("description", UMapping.string.optional)(_.field.updatable)
.property("colour", UMapping.string)(_.field.updatable)
.property("text", UMapping.string)(
_.select(_.displayName)
.filter[String] {
case (_, tags, authContext, Right(predicate)) => tags.freetags(organisationSrv)(authContext).has(_.predicate, predicate)
case (_, tags, _, Left(true)) => tags
case (_, tags, _, Left(false)) => tags.empty
}
.readonly
)
.build

}
5 changes: 4 additions & 1 deletion thehive/app/org/thp/thehive/controllers/v1/Router.scala
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,14 @@ class Router @Inject() (
patternCtrl: PatternCtrl,
procedureCtrl: ProcedureCtrl,
profileCtrl: ProfileCtrl,
tagCtrl: TagCtrl,
taskCtrl: TaskCtrl,
shareCtrl: ShareCtrl,
taxonomyCtrl: TaxonomyCtrl,
// shareCtrl: ShareCtrl,
userCtrl: UserCtrl,
statusCtrl: StatusCtrl
// streamCtrl: StreamCtrl,
// tagCtrl: TagCtrl,
) extends SimpleRouter {

override def routes: Routes = {
Expand Down Expand Up @@ -165,6 +165,9 @@ class Router @Inject() (
case PATCH(p"/profile/$profileId") => profileCtrl.update(profileId)
case DELETE(p"/profile/$profileId") => profileCtrl.delete(profileId)

case GET(p"/tag/$id") => tagCtrl.get(id)
case PATCH(p"/tag/$id") => tagCtrl.update(id)

case GET(p"/describe/_all") => describeCtrl.describeAll
case GET(p"/describe/$modelName") => describeCtrl.describe(modelName)

Expand Down
75 changes: 75 additions & 0 deletions thehive/app/org/thp/thehive/controllers/v1/TagCtrl.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package org.thp.thehive.controllers.v1

import org.apache.tinkerpop.gremlin.structure.Vertex
import org.thp.scalligraph.EntityIdOrName
import org.thp.scalligraph.controllers.{Entrypoint, FieldsParser}
import org.thp.scalligraph.models.{Database, Entity}
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.thehive.controllers.v1.Conversion._
import org.thp.thehive.models.{Permissions, Tag}
import org.thp.thehive.services.TagOps._
import org.thp.thehive.services.{OrganisationSrv, TagSrv}
import play.api.mvc.{Action, AnyContent, Results}

import javax.inject.Inject

case class TagHint(freeTag: Option[String], namespace: Option[String], predicate: Option[String], value: Option[String], limit: Option[Long])

class TagCtrl @Inject() (
entrypoint: Entrypoint,
db: Database,
tagSrv: TagSrv,
organisationSrv: OrganisationSrv,
properties: Properties
) extends QueryableCtrl {
override val entityName: String = "Tag"
override val publicProperties: PublicProperties = properties.tag
override val initialQuery: Query =
Query.init[Traversal.V[Tag]]("listTag", (graph, authContext) => tagSrv.startTraversal(graph).visible(authContext))
override val pageQuery: ParamQuery[OutputParam] = Query.withParam[OutputParam, Traversal.V[Tag], IteratorOutput](
"page",
(range, tagSteps, _) => tagSteps.page(range.from, range.to, withTotal = true)
)
override val outputQuery: Query = Query.output[Tag with Entity]
override val getQuery: ParamQuery[EntityIdOrName] = Query.initWithParam[EntityIdOrName, Traversal.V[Tag]](
"getTag",
(idOrName, graph, _) => tagSrv.get(idOrName)(graph)
)
override val extraQueries: Seq[ParamQuery[_]] = Seq(
Query[Traversal.V[Tag], Traversal.V[Tag]]("freetags", (tagSteps, authContext) => tagSteps.freetags(organisationSrv)(authContext)),
Query.initWithParam[TagHint, Traversal[String, Vertex, Converter[String, Vertex]]](
"TagAutoComplete",
(tagHint, graph, authContext) =>
tagHint
.freeTag
.fold(tagSrv.startTraversal(graph).autoComplete(tagHint.namespace, tagHint.predicate, tagHint.value)(authContext).visible(authContext))(
tagSrv.startTraversal(graph).autoComplete(organisationSrv, _)(authContext)
)
.merge(tagHint.limit)(_.limit(_))
.displayName
),
Query[Traversal.V[Tag], Traversal[String, Vertex, Converter[String, Vertex]]]("text", (tagSteps, _) => tagSteps.displayName),
Query.output[String, Traversal[String, Vertex, Converter[String, Vertex]]]
)

def get(tagId: String): Action[AnyContent] =
entrypoint("get tag")
.authRoTransaction(db) { _ => implicit graph =>
tagSrv
.getOrFail(EntityIdOrName(tagId))
.map(tag => Results.Ok(tag.toJson))
}

def update(tagId: String): Action[AnyContent] =
entrypoint("update tag")
.extract("tag", FieldsParser.update("tag", publicProperties))
.authPermittedTransaction(db, Permissions.manageTag) { implicit request => implicit graph =>
val propertyUpdaters: Seq[PropertyUpdater] = request.body("tag")
tagSrv
.update(_.getFreetag(organisationSrv, EntityIdOrName(tagId)), propertyUpdaters)
.map(_ => Results.NoContent)
}
}
2 changes: 1 addition & 1 deletion thehive/app/org/thp/thehive/models/Taxonomy.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ case class TaxonomyTag()

case class RichTaxonomy(
taxonomy: Taxonomy with Entity,
tags: Seq[Tag]
tags: Seq[Tag with Entity]
) {
def _id: EntityId = taxonomy._id
def _createdBy: String = taxonomy._createdBy
Expand Down
4 changes: 4 additions & 0 deletions thehive/app/org/thp/thehive/services/TagSrv.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,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.EntityIdOrName
import org.thp.scalligraph.auth.AuthContext
import org.thp.scalligraph.models.{Database, Entity}
import org.thp.scalligraph.services.config.{ApplicationConfig, ConfigItem}
Expand Down Expand Up @@ -119,6 +120,9 @@ object TagOps {
.has(_.namespace, freeTagNamespace)
}

def getFreetag(organisationSrv: OrganisationSrv, idOrName: EntityIdOrName)(implicit authContext: AuthContext): Traversal.V[Tag] =
idOrName.fold(traversal.getByIds(_), traversal.has(_.predicate, _)).freetags(organisationSrv)

def autoComplete(organisationSrv: OrganisationSrv, freeTag: String)(implicit authContext: AuthContext): Traversal.V[Tag] =
freetags(organisationSrv)
.has(_.predicate, TextP.containing(freeTag))
Expand Down
Loading

0 comments on commit e192c53

Please sign in to comment.