diff --git a/CHANGELOG.md b/CHANGELOG.md index d10cd01815..b504a828a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,25 @@ # Change Log -## [3.4.0-4C1](https://github.com/TheHive-Project/TheHive/tree/3.4.0-RC1) (2019-07-09) +## [3.4.0](https://github.com/TheHive-Project/TheHive/tree/HEAD) (2019-09-05) +[Full Changelog](https://github.com/TheHive-Project/TheHive/compare/3.4.0-RC2...3.4.0) + +**Implemented enhancements:** + +- Removing custom fields [\#954](https://github.com/TheHive-Project/TheHive/issues/954) + +**Fixed bugs:** + +- Cosmetic Bug: wrong number of exported observables displayed [\#1071](https://github.com/TheHive-Project/TheHive/issues/1071) +- Update Database button does not appear in training appliance [\#1067](https://github.com/TheHive-Project/TheHive/issues/1067) +- bulk merge alerts into case lose description's alert [\#1065](https://github.com/TheHive-Project/TheHive/issues/1065) +- Incorrect number of related observables returned [\#1062](https://github.com/TheHive-Project/TheHive/issues/1062) +- Incorrect tag filter results when observables with tags are added then deleted [\#1061](https://github.com/TheHive-Project/TheHive/issues/1061) +- Cannot setup TheHive 3.4.0-RC2 using Docker [\#1051](https://github.com/TheHive-Project/TheHive/issues/1051) +- Case statistics dashboard loads with an error message and the case over time panel fails to display any data [\#1050](https://github.com/TheHive-Project/TheHive/issues/1050) +- Can't secure ElasticSearch connection [\#1046](https://github.com/TheHive-Project/TheHive/issues/1046) + +## [3.4.0-RC2](https://github.com/TheHive-Project/TheHive/tree/3.4.0-RC2) (2019-07-10) [Full Changelog](https://github.com/TheHive-Project/TheHive/compare/3.4.0-RC1...3.4.0-RC2) **Implemented enhancements:** @@ -25,7 +43,7 @@ - Cannot add custom fields to case template [\#1042](https://github.com/TheHive-Project/TheHive/issues/1042) - sample hive does not connect to cortex and prints no helpful error message [\#1028](https://github.com/TheHive-Project/TheHive/issues/1028) -## [3.4.0-4C1](https://github.com/TheHive-Project/TheHive/tree/HEAD) (2019-06-05) +## [3.4.0-RC1](https://github.com/TheHive-Project/TheHive/tree/HEAD) (2019-06-05) [Full Changelog](https://github.com/TheHive-Project/TheHive/compare/3.3.1...3.4.0-4C1) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 59861dee38..7be462cf0c 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -20,8 +20,8 @@ object Dependencies { val reflections = "org.reflections" % "reflections" % "0.9.11" val zip4j = "net.lingala.zip4j" % "zip4j" % "1.3.2" - val elastic4play = "org.thehive-project" %% "elastic4play" % "1.11.4" - val akkaCluster = "com.typesafe.akka" %% "akka-cluster" % "2.5.19" - val akkaClusterTools = "com.typesafe.akka" %% "akka-cluster-tools" % "2.5.19" + val elastic4play = "org.thehive-project" %% "elastic4play" % "1.11.5" + val akkaCluster = "com.typesafe.akka" %% "akka-cluster" % "2.5.21" + val akkaClusterTools = "com.typesafe.akka" %% "akka-cluster-tools" % "2.5.21" } } diff --git a/project/plugins.sbt b/project/plugins.sbt index c037cfab7e..c8ec87a869 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,6 +1,6 @@ // Comment to get more information during initialization logLevel := Level.Info -addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.22") +addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.23") addSbtPlugin("org.foundweekends" % "sbt-bintray" % "0.5.1") addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.0.0") diff --git a/thehive-backend/app/controllers/AlertCtrl.scala b/thehive-backend/app/controllers/AlertCtrl.scala index 38a1ee4af6..9e87c3fb0e 100644 --- a/thehive-backend/app/controllers/AlertCtrl.scala +++ b/thehive-backend/app/controllers/AlertCtrl.scala @@ -128,8 +128,9 @@ class AlertCtrl @Inject()( @Timed def bulkDelete(): Action[Fields] = authenticated(Roles.admin).async(fieldsBodyParser) { implicit request ⇒ request.body.getStrings("ids").fold(Future.successful(NoContent)) { ids ⇒ - Future.traverse(ids)(alertSrv.delete(_, request.body.getBoolean("force").getOrElse(false))) - .map(_ => NoContent) + Future + .traverse(ids)(alertSrv.delete(_, request.body.getBoolean("force").getOrElse(false))) + .map(_ ⇒ NoContent) } } @@ -180,7 +181,9 @@ class AlertCtrl @Inject()( def createCase(id: String): Action[Fields] = authenticated(Roles.write).async(fieldsBodyParser) { implicit request ⇒ for { alert ← alertSrv.get(id) - customCaseTemplate = request.body.getString("caseTemplate") + customCaseTemplate = request + .body + .getString("caseTemplate") .orElse(alert.caseTemplate()) caze ← alertSrv.createCase(alert, customCaseTemplate) } yield renderer.toOutput(CREATED, caze) diff --git a/thehive-backend/app/controllers/CustomFieldsCtrl.scala b/thehive-backend/app/controllers/CustomFieldsCtrl.scala new file mode 100644 index 0000000000..a5a7b38b35 --- /dev/null +++ b/thehive-backend/app/controllers/CustomFieldsCtrl.scala @@ -0,0 +1,56 @@ +package controllers + +import scala.concurrent.{ExecutionContext, Future} + +import play.api.http.Status +import play.api.libs.json.{JsNumber, JsObject, Json} +import play.api.mvc.{AbstractController, Action, AnyContent, ControllerComponents} + +import akka.stream.Materializer +import akka.stream.scaladsl.Sink +import com.sksamuel.elastic4s.http.ElasticDsl.{search, termsAggregation} +import javax.inject.{Inject, Singleton} +import models.Roles + +import org.elastic4play.NotFoundError +import org.elastic4play.controllers.Authenticated +import org.elastic4play.database.DBFind +import org.elastic4play.services.DBLists +import org.elastic4play.services.QueryDSL._ + +@Singleton +class CustomFieldsCtrl @Inject()( + authenticated: Authenticated, + dbfind: DBFind, + dblists: DBLists, + components: ControllerComponents, + implicit val ec: ExecutionContext, + implicit val mat: Materializer +) extends AbstractController(components) + with Status { + + def useCount(customField: String): Action[AnyContent] = + authenticated(Roles.read) + .async { + dblists("custom_fields") + .getItems[JsObject] + ._1 + .collect { + case (_, value) if (value \ "reference").asOpt[String].contains(customField) ⇒ (value \ "type").as[String] + } + .runWith(Sink.head) + .recoverWith { case _ ⇒ Future.failed(NotFoundError(s"CustomField $customField not found")) } + .flatMap { customFieldType ⇒ + val filter = and("relations" in ("case", "alert", "caseTemplate"), contains(s"customFields.$customField.$customFieldType")) + dbfind( + indexName ⇒ search(indexName).query(filter.query).aggregations(termsAggregation("t").field("relations")) + ).map { searchResponse ⇒ + val buckets = searchResponse.aggregations.terms("t").buckets + val total = buckets.map(_.docCount).sum + val result = buckets.map(b ⇒ b.key → JsNumber(b.docCount)) :+ ("total" → JsNumber(total)) + Ok(JsObject(result)) + } + .recover { case _ ⇒ Ok(Json.obj("total" → 0)) } + } + } +} diff --git a/thehive-backend/app/controllers/DBListCtrl.scala b/thehive-backend/app/controllers/DBListCtrl.scala index f1b1dbbc03..d4c6a68df3 100644 --- a/thehive-backend/app/controllers/DBListCtrl.scala +++ b/thehive-backend/app/controllers/DBListCtrl.scala @@ -1,14 +1,14 @@ -package org.elastic4play.controllers - -import javax.inject.{Inject, Singleton} +package controllers import scala.concurrent.{ExecutionContext, Future} import play.api.libs.json.{JsValue, Json} import play.api.mvc._ +import javax.inject.{Inject, Singleton} import models.Roles +import org.elastic4play.controllers.{Authenticated, Fields, FieldsBodyParser, Renderer} import org.elastic4play.services.DBLists import org.elastic4play.{MissingAttributeError, Timed} diff --git a/thehive-backend/app/controllers/StatusCtrl.scala b/thehive-backend/app/controllers/StatusCtrl.scala index 2f9f698798..08ee2f7145 100644 --- a/thehive-backend/app/controllers/StatusCtrl.scala +++ b/thehive-backend/app/controllers/StatusCtrl.scala @@ -1,40 +1,48 @@ package controllers +import akka.actor.ActorSystem + import scala.collection.immutable import scala.concurrent.ExecutionContext import scala.util.Try - import play.api.Configuration import play.api.libs.json.Json.toJsFieldJsValueWrapper import play.api.libs.json.{JsBoolean, JsObject, JsString, Json} import play.api.mvc.{AbstractController, Action, AnyContent, ControllerComponents} - import com.sksamuel.elastic4s.http.ElasticDsl import connectors.Connector import javax.inject.{Inject, Singleton} import models.HealthStatus import org.elasticsearch.client.Node - import org.elastic4play.Timed import org.elastic4play.database.DBIndex import org.elastic4play.services.AuthSrv import org.elastic4play.services.auth.MultiAuthSrv +import scala.concurrent.duration.{DurationInt, FiniteDuration} + @Singleton class StatusCtrl @Inject()( connectors: immutable.Set[Connector], configuration: Configuration, dbIndex: DBIndex, authSrv: AuthSrv, + system: ActorSystem, components: ControllerComponents, implicit val ec: ExecutionContext ) extends AbstractController(components) { private[controllers] def getVersion(c: Class[_]) = Option(c.getPackage.getImplementationVersion).getOrElse("SNAPSHOT") + private var clusterStatusName: String = "Init" + val checkStatusInterval: FiniteDuration = configuration.getOptional[FiniteDuration]("statusCheckInterval").getOrElse(1.minute) + private def updateStatus(): Unit = { + clusterStatusName = Try(dbIndex.clusterStatusName).getOrElse("ERROR") + system.scheduler.scheduleOnce(checkStatusInterval)(updateStatus()) + } + updateStatus() @Timed("controllers.StatusCtrl.get") def get: Action[AnyContent] = Action { - val clusterStatusName = Try(dbIndex.clusterStatusName).getOrElse("ERROR") Ok( Json.obj( "versions" → Json.obj( diff --git a/thehive-backend/app/controllers/StreamCtrl.scala b/thehive-backend/app/controllers/StreamCtrl.scala index d37c55830d..7f3ca03a27 100644 --- a/thehive-backend/app/controllers/StreamCtrl.scala +++ b/thehive-backend/app/controllers/StreamCtrl.scala @@ -23,7 +23,7 @@ import services.StreamActor import services.StreamActor.StreamMessages import org.elastic4play.controllers._ -import org.elastic4play.services.{AuxSrv, EventSrv, MigrationSrv} +import org.elastic4play.services.{AuxSrv, EventSrv, MigrationSrv, UserSrv} import org.elastic4play.Timed @Singleton @@ -33,6 +33,7 @@ class StreamCtrl( authenticated: Authenticated, renderer: Renderer, eventSrv: EventSrv, + userSrv: UserSrv, auxSrv: AuxSrv, migrationSrv: MigrationSrv, components: ControllerComponents, @@ -46,6 +47,7 @@ class StreamCtrl( authenticated: Authenticated, renderer: Renderer, eventSrv: EventSrv, + userSrv: UserSrv, auxSrv: AuxSrv, migrationSrv: MigrationSrv, components: ControllerComponents, @@ -58,6 +60,7 @@ class StreamCtrl( authenticated, renderer, eventSrv, + userSrv, auxSrv, migrationSrv, components, @@ -96,9 +99,10 @@ class StreamCtrl( Future.successful(BadRequest("Invalid stream id")) } else { val futureStatus = authenticated.expirationStatus(request) match { - case ExpirationError if !migrationSrv.isMigrating ⇒ authenticated.getFromApiKey(request).map(_ ⇒ OK) - case _: ExpirationWarning ⇒ Future.successful(220) - case _ ⇒ Future.successful(OK) + case ExpirationError if !migrationSrv.isMigrating ⇒ + userSrv.getInitialUser(request).recoverWith { case _ ⇒ authenticated.getFromApiKey(request) }.map(_ ⇒ OK) + case _: ExpirationWarning ⇒ Future.successful(220) + case _ ⇒ Future.successful(OK) } // Check if stream actor exists diff --git a/thehive-backend/app/services/AlertSrv.scala b/thehive-backend/app/services/AlertSrv.scala index 6a6ea09102..4b367c3ca0 100644 --- a/thehive-backend/app/services/AlertSrv.scala +++ b/thehive-backend/app/services/AlertSrv.scala @@ -3,27 +3,27 @@ package services import java.nio.file.Files import scala.collection.immutable -import scala.concurrent.{ ExecutionContext, Future } +import scala.concurrent.{ExecutionContext, Future} import scala.util.matching.Regex -import scala.util.{ Failure, Success, Try } +import scala.util.{Failure, Success, Try} import play.api.libs.json._ -import play.api.{ Configuration, Logger } +import play.api.{Configuration, Logger} import akka.NotUsed import akka.stream.Materializer -import akka.stream.scaladsl.{ Sink, Source } +import akka.stream.scaladsl.{Sink, Source} import connectors.ConnectorRouter -import javax.inject.{ Inject, Singleton } +import javax.inject.{Inject, Singleton} import models._ -import org.elastic4play.controllers.{ Fields, FileInputValue } +import org.elastic4play.controllers.{Fields, FileInputValue} import org.elastic4play.database.ModifyConfig import org.elastic4play.services.JsonFormat.attachmentFormat -import org.elastic4play.services.QueryDSL.{ groupByField, parent, selectCount, withId } +import org.elastic4play.services.QueryDSL.{groupByField, parent, selectCount, withId} import org.elastic4play.services._ import org.elastic4play.utils.Collection -import org.elastic4play.{ ConflictError, InternalError } +import org.elastic4play.{ConflictError, InternalError} trait AlertTransformer { def createCase(alert: Alert, customCaseTemplate: Option[String])(implicit authContext: AuthContext): Future[Case] @@ -312,7 +312,7 @@ class AlertSrv( .create(caze, artifactsFields) .flatMap { artifacts ⇒ Future.traverse(artifacts) { - case Success(_) => Future.successful(()) + case Success(_) ⇒ Future.successful(()) case Failure(ConflictError(_, attributes)) ⇒ // if it already exists, add tags from alert import org.elastic4play.services.QueryDSL._ (for { @@ -340,7 +340,7 @@ class AlertSrv( Future.successful(()) } } - .map(_ => caze) + .map(_ ⇒ caze) updatedCase.onComplete { _ ⇒ // remove temporary files artifactsFields diff --git a/thehive-backend/conf/routes b/thehive-backend/conf/routes index 6d7bae50d6..f0acbc4687 100644 --- a/thehive-backend/conf/routes +++ b/thehive-backend/conf/routes @@ -1,126 +1,126 @@ -# Routes -# This file defines all application routes (Higher priority routes first) -# ~~~~ - -GET / controllers.Home.redirect -GET /api/status controllers.StatusCtrl.get -GET /api/health controllers.StatusCtrl.health -GET /api/logout controllers.AuthenticationCtrl.logout() -POST /api/login controllers.AuthenticationCtrl.login() -POST /api/ssoLogin controllers.AuthenticationCtrl.ssoLogin() - -POST /api/_search controllers.SearchCtrl.find() -POST /api/_stats controllers.SearchCtrl.stats() - -GET /api/case controllers.CaseCtrl.find() -POST /api/case/_search controllers.CaseCtrl.find() -PATCH /api/case/_bulk controllers.CaseCtrl.bulkUpdate() -POST /api/case/_stats controllers.CaseCtrl.stats() -POST /api/case controllers.CaseCtrl.create() -GET /api/case/:caseId controllers.CaseCtrl.get(caseId) -PATCH /api/case/:caseId controllers.CaseCtrl.update(caseId) -DELETE /api/case/:caseId controllers.CaseCtrl.delete(caseId) -DELETE /api/case/:caseId/force controllers.CaseCtrl.realDelete(caseId) -GET /api/case/:caseId/links controllers.CaseCtrl.linkedCases(caseId) -POST /api/case/:caseId1/_merge/:caseId2 controllers.CaseCtrl.merge(caseId1, caseId2) - -POST /api/case/template/_search controllers.CaseTemplateCtrl.find() -POST /api/case/template controllers.CaseTemplateCtrl.create() -GET /api/case/template/:caseTemplateId controllers.CaseTemplateCtrl.get(caseTemplateId) -PATCH /api/case/template/:caseTemplateId controllers.CaseTemplateCtrl.update(caseTemplateId) -DELETE /api/case/template/:caseTemplateId controllers.CaseTemplateCtrl.delete(caseTemplateId) - -POST /api/case/artifact/_search controllers.ArtifactCtrl.find() -POST /api/case/:caseId/artifact/_search controllers.ArtifactCtrl.findInCase(caseId) -POST /api/case/artifact/_stats controllers.ArtifactCtrl.stats() -POST /api/case/:caseId/artifact controllers.ArtifactCtrl.create(caseId) -GET /api/case/artifact/:artifactId controllers.ArtifactCtrl.get(artifactId) -DELETE /api/case/artifact/:artifactId controllers.ArtifactCtrl.delete(artifactId) -PATCH /api/case/artifact/_bulk controllers.ArtifactCtrl.bulkUpdate() -PATCH /api/case/artifact/:artifactId controllers.ArtifactCtrl.update(artifactId) -GET /api/case/artifact/:artifactId/similar controllers.ArtifactCtrl.findSimilar(artifactId) - -POST /api/case/:caseId/task/_search controllers.TaskCtrl.findInCase(caseId) -POST /api/case/task/_search controllers.TaskCtrl.find() -POST /api/case/task/_stats controllers.TaskCtrl.stats() -GET /api/case/task/:taskId controllers.TaskCtrl.get(taskId) -PATCH /api/case/task/:taskId controllers.TaskCtrl.update(taskId) -POST /api/case/:caseId/task controllers.TaskCtrl.create(caseId) - -GET /api/case/task/:taskId/log controllers.LogCtrl.findInTask(taskId) -POST /api/case/task/:taskId/log/_search controllers.LogCtrl.findInTask(taskId) -POST /api/case/task/log/_search controllers.LogCtrl.find() -POST /api/case/task/log/_stats controllers.LogCtrl.stats() -POST /api/case/task/:taskId/log controllers.LogCtrl.create(taskId) -PATCH /api/case/task/log/:logId controllers.LogCtrl.update(logId) -DELETE /api/case/task/log/:logId controllers.LogCtrl.delete(logId) -GET /api/case/task/log/:logId controllers.LogCtrl.get(logId) - -GET /api/alert controllers.AlertCtrl.find() -POST /api/alert/_search controllers.AlertCtrl.find() -PATCH /api/alert/_bulk controllers.AlertCtrl.bulkUpdate() -POST /api/alert/_stats controllers.AlertCtrl.stats() -GET /api/alert/_fixStatus controllers.AlertCtrl.fixStatus() -POST /api/alert controllers.AlertCtrl.create() -GET /api/alert/:alertId controllers.AlertCtrl.get(alertId) -PATCH /api/alert/:alertId controllers.AlertCtrl.update(alertId) -DELETE /api/alert/:alertId controllers.AlertCtrl.delete(alertId, force: Option[Boolean]) -POST /api/alert/:alertId/markAsRead controllers.AlertCtrl.markAsRead(alertId) -POST /api/alert/:alertId/markAsUnread controllers.AlertCtrl.markAsUnread(alertId) -POST /api/alert/:alertId/createCase controllers.AlertCtrl.createCase(alertId) -POST /api/alert/:alertId/follow controllers.AlertCtrl.followAlert(alertId) -POST /api/alert/:alertId/unfollow controllers.AlertCtrl.unfollowAlert(alertId) -POST /api/alert/:alertId/merge/:caseId controllers.AlertCtrl.mergeWithCase(alertId, caseId) -POST /api/alert/merge/_bulk controllers.AlertCtrl.bulkMergeWithCase() -POST /api/alert/delete/_bulk controllers.AlertCtrl.bulkDelete() - -GET /api/flow controllers.AuditCtrl.flow(rootId: Option[String], count: Option[Int]) -GET /api/audit controllers.AuditCtrl.find() -POST /api/audit/_search controllers.AuditCtrl.find() -POST /api/audit/_stats controllers.AuditCtrl.stats() - -GET /api/datastore/:hash controllers.AttachmentCtrl.download(hash, name: Option[String]) -GET /api/datastorezip/:hash controllers.AttachmentCtrl.downloadZip(hash, name: Option[String]) - -POST /api/maintenance/migrate org.elastic4play.controllers.MigrationCtrl.migrate -#POST /api/maintenance/rehash controllers.MaintenanceCtrl.reHash - -GET /api/list org.elastic4play.controllers.DBListCtrl.list() -DELETE /api/list/:itemId org.elastic4play.controllers.DBListCtrl.deleteItem(itemId) -PATCH /api/list/:itemId org.elastic4play.controllers.DBListCtrl.updateItem(itemId) -POST /api/list/:listName org.elastic4play.controllers.DBListCtrl.addItem(listName) -GET /api/list/:listName org.elastic4play.controllers.DBListCtrl.listItems(listName) -POST /api/list/:listName/_exists org.elastic4play.controllers.DBListCtrl.itemExists(listName) - - -GET /api/user/current controllers.UserCtrl.currentUser() -POST /api/user/_search controllers.UserCtrl.find() -POST /api/user controllers.UserCtrl.create() -GET /api/user/:userId controllers.UserCtrl.get(userId) -DELETE /api/user/:userId controllers.UserCtrl.delete(userId) -PATCH /api/user/:userId controllers.UserCtrl.update(userId) -POST /api/user/:userId/password/set controllers.UserCtrl.setPassword(userId) -POST /api/user/:userId/password/change controllers.UserCtrl.changePassword(userId) -GET /api/user/:userId/key controllers.UserCtrl.getKey(userId) -DELETE /api/user/:userId/key controllers.UserCtrl.removeKey(userId) -POST /api/user/:userId/key/renew controllers.UserCtrl.renewKey(userId) - - -POST /api/stream controllers.StreamCtrl.create() -GET /api/stream/status controllers.StreamCtrl.status -GET /api/stream/:streamId controllers.StreamCtrl.get(streamId) - -GET /api/describe/_all controllers.DescribeCtrl.describeAll -GET /api/describe/:modelName controllers.DescribeCtrl.describe(modelName) - -GET /api/dashboard controllers.DashboardCtrl.find() -POST /api/dashboard/_search controllers.DashboardCtrl.find() -POST /api/dashboard/_stats controllers.DashboardCtrl.stats() -POST /api/dashboard controllers.DashboardCtrl.create() -GET /api/dashboard/:dashboardId controllers.DashboardCtrl.get(dashboardId) -PATCH /api/dashboard/:dashboardId controllers.DashboardCtrl.update(dashboardId) -DELETE /api/dashboard/:dashboardId controllers.DashboardCtrl.delete(dashboardId) - --> /api/connector connectors.ConnectorRouter - -GET /*file controllers.AssetCtrl.get(file) +# Routes +# This file defines all application routes (Higher priority routes first) +# ~~~~ + +GET / controllers.Home.redirect +GET /api/status controllers.StatusCtrl.get +GET /api/health controllers.StatusCtrl.health +GET /api/logout controllers.AuthenticationCtrl.logout() +POST /api/login controllers.AuthenticationCtrl.login() +POST /api/ssoLogin controllers.AuthenticationCtrl.ssoLogin() + +POST /api/_search controllers.SearchCtrl.find() +POST /api/_stats controllers.SearchCtrl.stats() + +GET /api/case controllers.CaseCtrl.find() +POST /api/case/_search controllers.CaseCtrl.find() +PATCH /api/case/_bulk controllers.CaseCtrl.bulkUpdate() +POST /api/case/_stats controllers.CaseCtrl.stats() +POST /api/case controllers.CaseCtrl.create() +GET /api/case/:caseId controllers.CaseCtrl.get(caseId) +PATCH /api/case/:caseId controllers.CaseCtrl.update(caseId) +DELETE /api/case/:caseId controllers.CaseCtrl.delete(caseId) +DELETE /api/case/:caseId/force controllers.CaseCtrl.realDelete(caseId) +GET /api/case/:caseId/links controllers.CaseCtrl.linkedCases(caseId) +POST /api/case/:caseId1/_merge/:caseId2 controllers.CaseCtrl.merge(caseId1, caseId2) + +POST /api/case/template/_search controllers.CaseTemplateCtrl.find() +POST /api/case/template controllers.CaseTemplateCtrl.create() +GET /api/case/template/:caseTemplateId controllers.CaseTemplateCtrl.get(caseTemplateId) +PATCH /api/case/template/:caseTemplateId controllers.CaseTemplateCtrl.update(caseTemplateId) +DELETE /api/case/template/:caseTemplateId controllers.CaseTemplateCtrl.delete(caseTemplateId) + +POST /api/case/artifact/_search controllers.ArtifactCtrl.find() +POST /api/case/:caseId/artifact/_search controllers.ArtifactCtrl.findInCase(caseId) +POST /api/case/artifact/_stats controllers.ArtifactCtrl.stats() +POST /api/case/:caseId/artifact controllers.ArtifactCtrl.create(caseId) +GET /api/case/artifact/:artifactId controllers.ArtifactCtrl.get(artifactId) +DELETE /api/case/artifact/:artifactId controllers.ArtifactCtrl.delete(artifactId) +PATCH /api/case/artifact/_bulk controllers.ArtifactCtrl.bulkUpdate() +PATCH /api/case/artifact/:artifactId controllers.ArtifactCtrl.update(artifactId) +GET /api/case/artifact/:artifactId/similar controllers.ArtifactCtrl.findSimilar(artifactId) + +POST /api/case/:caseId/task/_search controllers.TaskCtrl.findInCase(caseId) +POST /api/case/task/_search controllers.TaskCtrl.find() +POST /api/case/task/_stats controllers.TaskCtrl.stats() +GET /api/case/task/:taskId controllers.TaskCtrl.get(taskId) +PATCH /api/case/task/:taskId controllers.TaskCtrl.update(taskId) +POST /api/case/:caseId/task controllers.TaskCtrl.create(caseId) + +GET /api/case/task/:taskId/log controllers.LogCtrl.findInTask(taskId) +POST /api/case/task/:taskId/log/_search controllers.LogCtrl.findInTask(taskId) +POST /api/case/task/log/_search controllers.LogCtrl.find() +POST /api/case/task/log/_stats controllers.LogCtrl.stats() +POST /api/case/task/:taskId/log controllers.LogCtrl.create(taskId) +PATCH /api/case/task/log/:logId controllers.LogCtrl.update(logId) +DELETE /api/case/task/log/:logId controllers.LogCtrl.delete(logId) +GET /api/case/task/log/:logId controllers.LogCtrl.get(logId) + +GET /api/alert controllers.AlertCtrl.find() +POST /api/alert/_search controllers.AlertCtrl.find() +PATCH /api/alert/_bulk controllers.AlertCtrl.bulkUpdate() +POST /api/alert/_stats controllers.AlertCtrl.stats() +GET /api/alert/_fixStatus controllers.AlertCtrl.fixStatus() +POST /api/alert controllers.AlertCtrl.create() +GET /api/alert/:alertId controllers.AlertCtrl.get(alertId) +PATCH /api/alert/:alertId controllers.AlertCtrl.update(alertId) +DELETE /api/alert/:alertId controllers.AlertCtrl.delete(alertId, force: Option[Boolean]) +POST /api/alert/:alertId/markAsRead controllers.AlertCtrl.markAsRead(alertId) +POST /api/alert/:alertId/markAsUnread controllers.AlertCtrl.markAsUnread(alertId) +POST /api/alert/:alertId/createCase controllers.AlertCtrl.createCase(alertId) +POST /api/alert/:alertId/follow controllers.AlertCtrl.followAlert(alertId) +POST /api/alert/:alertId/unfollow controllers.AlertCtrl.unfollowAlert(alertId) +POST /api/alert/:alertId/merge/:caseId controllers.AlertCtrl.mergeWithCase(alertId, caseId) +POST /api/alert/merge/_bulk controllers.AlertCtrl.bulkMergeWithCase() +POST /api/alert/delete/_bulk controllers.AlertCtrl.bulkDelete() + +GET /api/flow controllers.AuditCtrl.flow(rootId: Option[String], count: Option[Int]) +GET /api/audit controllers.AuditCtrl.find() +POST /api/audit/_search controllers.AuditCtrl.find() +POST /api/audit/_stats controllers.AuditCtrl.stats() + +GET /api/datastore/:hash controllers.AttachmentCtrl.download(hash, name: Option[String]) +GET /api/datastorezip/:hash controllers.AttachmentCtrl.downloadZip(hash, name: Option[String]) + +POST /api/maintenance/migrate org.elastic4play.controllers.MigrationCtrl.migrate +#POST /api/maintenance/rehash controllers.MaintenanceCtrl.reHash + +GET /api/list controllers.DBListCtrl.list() +DELETE /api/list/:itemId controllers.DBListCtrl.deleteItem(itemId) +PATCH /api/list/:itemId controllers.DBListCtrl.updateItem(itemId) +POST /api/list/:listName controllers.DBListCtrl.addItem(listName) +GET /api/list/:listName controllers.DBListCtrl.listItems(listName) +POST /api/list/:listName/_exists controllers.DBListCtrl.itemExists(listName) +GET /api/customFields/:name controllers.CustomFieldsCtrl.useCount(name) + +GET /api/user/current controllers.UserCtrl.currentUser() +POST /api/user/_search controllers.UserCtrl.find() +POST /api/user controllers.UserCtrl.create() +GET /api/user/:userId controllers.UserCtrl.get(userId) +DELETE /api/user/:userId controllers.UserCtrl.delete(userId) +PATCH /api/user/:userId controllers.UserCtrl.update(userId) +POST /api/user/:userId/password/set controllers.UserCtrl.setPassword(userId) +POST /api/user/:userId/password/change controllers.UserCtrl.changePassword(userId) +GET /api/user/:userId/key controllers.UserCtrl.getKey(userId) +DELETE /api/user/:userId/key controllers.UserCtrl.removeKey(userId) +POST /api/user/:userId/key/renew controllers.UserCtrl.renewKey(userId) + + +POST /api/stream controllers.StreamCtrl.create() +GET /api/stream/status controllers.StreamCtrl.status +GET /api/stream/:streamId controllers.StreamCtrl.get(streamId) + +GET /api/describe/_all controllers.DescribeCtrl.describeAll +GET /api/describe/:modelName controllers.DescribeCtrl.describe(modelName) + +GET /api/dashboard controllers.DashboardCtrl.find() +POST /api/dashboard/_search controllers.DashboardCtrl.find() +POST /api/dashboard/_stats controllers.DashboardCtrl.stats() +POST /api/dashboard controllers.DashboardCtrl.create() +GET /api/dashboard/:dashboardId controllers.DashboardCtrl.get(dashboardId) +PATCH /api/dashboard/:dashboardId controllers.DashboardCtrl.update(dashboardId) +DELETE /api/dashboard/:dashboardId controllers.DashboardCtrl.delete(dashboardId) + +-> /api/connector connectors.ConnectorRouter + +GET /*file controllers.AssetCtrl.get(file) diff --git a/thehive-misp/app/connectors/misp/MispExport.scala b/thehive-misp/app/connectors/misp/MispExport.scala index 4e0fe2d44c..3104c06f76 100644 --- a/thehive-misp/app/connectors/misp/MispExport.scala +++ b/thehive-misp/app/connectors/misp/MispExport.scala @@ -51,17 +51,16 @@ class MispExport @Inject()( } def removeDuplicateAttributes(attributes: Seq[ExportedMispAttribute]): Seq[ExportedMispAttribute] = { - val attrIndex = attributes.zipWithIndex - - attrIndex - .filter { - case (ExportedMispAttribute(_, category, tpe, _, value, _), index) ⇒ - attrIndex.exists { - case (ExportedMispAttribute(_, `category`, `tpe`, _, `value`, _), otherIndex) ⇒ otherIndex >= index - case _ ⇒ true - } + var attrSet = Set.empty[(String, String, String)] + val builder = Seq.newBuilder[ExportedMispAttribute] + attributes.foreach { attr ⇒ + val tuple = (attr.category, attr.tpe, attr.value.fold(identity, _.name)) + if (!attrSet.contains(tuple)) { + builder += attr + attrSet += tuple } - .map(_._1) + } + builder.result() } def createEvent( diff --git a/ui/app/index.html b/ui/app/index.html index f4a81a5a5b..276ad53014 100644 --- a/ui/app/index.html +++ b/ui/app/index.html @@ -246,6 +246,7 @@ + diff --git a/ui/app/scripts/app.js b/ui/app/scripts/app.js index e2873c001d..5628903a9c 100644 --- a/ui/app/scripts/app.js +++ b/ui/app/scripts/app.js @@ -40,7 +40,7 @@ angular.module('thehive', [ }) .config(function($compileProvider) { 'use strict'; - $compileProvider.debugInfoEnabled(false); + $compileProvider.debugInfoEnabled(false); }) .config(function($stateProvider, $urlRouterProvider) { 'use strict'; @@ -80,7 +80,7 @@ angular.module('thehive', [ templateUrl: 'views/app.html', controller: 'RootCtrl', resolve: { - currentUser: function($q, $state, AuthenticationSrv) { + currentUser: function($q, $state, AuthenticationSrv, NotificationSrv) { var deferred = $q.defer(); AuthenticationSrv.current() @@ -88,7 +88,9 @@ angular.module('thehive', [ return deferred.resolve(userData); }) .catch( function(err) { - return deferred.resolve(err.status === 520 ? err.status : null); + NotificationSrv.error('App', err.data, err.status); + return deferred.reject(err); + //return deferred.resolve(err.status === 520 ? err.status : null); }); return deferred.promise; @@ -148,19 +150,20 @@ angular.module('thehive', [ controller: 'SettingsCtrl', title: 'Personal settings', resolve: { - currentUser: function($q, $state, $timeout, AuthenticationSrv) { + currentUser: function($q, $state, $timeout, AuthenticationSrv, NotificationSrv) { var deferred = $q.defer(); AuthenticationSrv.current() .then(function(userData) { return deferred.resolve(userData); }) - .catch( function(/*err*/) { - $timeout(function() { - $state.go('login'); - }); + .catch( function(err) { + NotificationSrv.error('SettingsCtrl', err.data, err.status); + // $timeout(function() { + // $state.go('login'); + // }); - return deferred.reject(); + return deferred.reject(err); }); return deferred.promise; diff --git a/ui/app/scripts/controllers/SearchCtrl.js b/ui/app/scripts/controllers/SearchCtrl.js index 4daf9305e7..27f760936b 100644 --- a/ui/app/scripts/controllers/SearchCtrl.js +++ b/ui/app/scripts/controllers/SearchCtrl.js @@ -51,7 +51,9 @@ event: event, templates: function() { return CaseTemplateSrv.list(); - } + }, + readonly: true, + isAdmin: false } }).result.then(function(/*response*/) { $scope.searchResults.update(); diff --git a/ui/app/scripts/controllers/admin/AdminCustomFieldsCtrl.js b/ui/app/scripts/controllers/admin/AdminCustomFieldsCtrl.js index 9b4a631bd3..04b56cac48 100644 --- a/ui/app/scripts/controllers/admin/AdminCustomFieldsCtrl.js +++ b/ui/app/scripts/controllers/admin/AdminCustomFieldsCtrl.js @@ -2,7 +2,7 @@ 'use strict'; angular.module('theHiveControllers').controller('AdminCustomFieldsCtrl', - function($scope, $uibModal, ListSrv, CustomFieldsCacheSrv, NotificationSrv) { + function($scope, $uibModal, ListSrv, CustomFieldsCacheSrv, NotificationSrv, ModalUtilsSrv, CustomFieldsSrv) { var self = this; self.reference = { @@ -66,6 +66,66 @@ }); }; + self.deleteField = function(customField) { + CustomFieldsSrv.usage(customField) + .then(function(response) { + var usage = response.data, + message, + isHtml = false; + + + if (usage.total === 0) { + message = 'Are you sure you want to delete this custom field?'; + } else { + var segs = [ + 'Are you sure you want to delete this custom field?', + '
', + '
', + 'This custom field is used by:', + ''); + + message = segs.join(''); + isHtml = true; + } + + return ModalUtilsSrv.confirm('Remove custom field', message, { + okText: 'Yes, remove it', + flavor: 'danger', + isHtml: isHtml + }); + }) + .then(function(/*response*/) { + return CustomFieldsSrv.removeField(customField); + }) + .then(function() { + NotificationSrv.log('The custom field has been removed successfully', 'success'); + + self.initCustomfields(); + CustomFieldsCacheSrv.clearCache(); + $scope.$emit('custom-fields:refresh'); + }) + .catch(function(err) { + if(err && !_.isString(err)) { + NotificationSrv.error('AdminCustomFields', err.data, err.status); + } + }); + }; + self.initCustomfields(); }); })(); diff --git a/ui/app/scripts/controllers/alert/AlertEventCtrl.js b/ui/app/scripts/controllers/alert/AlertEventCtrl.js index cde6a84015..265440d64b 100644 --- a/ui/app/scripts/controllers/alert/AlertEventCtrl.js +++ b/ui/app/scripts/controllers/alert/AlertEventCtrl.js @@ -1,10 +1,11 @@ (function() { 'use strict'; angular.module('theHiveControllers') - .controller('AlertEventCtrl', function($scope, $rootScope, $state, $uibModal, $uibModalInstance, ModalUtilsSrv, CustomFieldsCacheSrv, CaseResolutionStatus, AlertingSrv, NotificationSrv, UiSettingsSrv, clipboard, event, templates, isAdmin) { + .controller('AlertEventCtrl', function($scope, $rootScope, $state, $uibModal, $uibModalInstance, ModalUtilsSrv, CustomFieldsCacheSrv, CaseResolutionStatus, AlertingSrv, NotificationSrv, UiSettingsSrv, clipboard, event, templates, isAdmin, readonly) { var self = this; var eventId = event.id; + self.readonly = readonly; self.isAdmin = isAdmin; self.templates = _.pluck(templates, 'name'); self.CaseResolutionStatus = CaseResolutionStatus; diff --git a/ui/app/scripts/controllers/alert/AlertListCtrl.js b/ui/app/scripts/controllers/alert/AlertListCtrl.js index 9d2e4836c3..83740c9e86 100755 --- a/ui/app/scripts/controllers/alert/AlertListCtrl.js +++ b/ui/app/scripts/controllers/alert/AlertListCtrl.js @@ -225,7 +225,8 @@ templates: function() { return CaseTemplateSrv.list(); }, - isAdmin: self.isAdmin + isAdmin: self.isAdmin, + readonly: false } }); }; diff --git a/ui/app/scripts/controllers/case/CaseAlertsCtrl.js b/ui/app/scripts/controllers/case/CaseAlertsCtrl.js index c379fbfc43..12aa928dfe 100644 --- a/ui/app/scripts/controllers/case/CaseAlertsCtrl.js +++ b/ui/app/scripts/controllers/case/CaseAlertsCtrl.js @@ -73,6 +73,24 @@ } }; + $scope.previewEvent = function(event) { + $uibModal.open({ + templateUrl: 'views/partials/alert/event.dialog.html', + controller: 'AlertEventCtrl', + controllerAs: 'dialog', + size: 'max', + resolve: { + event: event, + templates: function() { + //return CaseTemplateSrv.list(); + return []; + }, + isAdmin: false, + readonly: true + } + }); + }; + $scope.alertStats = $scope.initStats($scope.alerts); } ); diff --git a/ui/app/scripts/controllers/case/CaseCloseModalCtrl.js b/ui/app/scripts/controllers/case/CaseCloseModalCtrl.js index d52daf3d79..9d481674a7 100644 --- a/ui/app/scripts/controllers/case/CaseCloseModalCtrl.js +++ b/ui/app/scripts/controllers/case/CaseCloseModalCtrl.js @@ -43,15 +43,20 @@ }), 'name'); return result; - } + }; $scope.initialize = function() { CustomFieldsCacheSrv.all().then(function(fields) { $scope.orderedFields = getTemplateCustomFields($scope.caze.customFields); - $scope.allCustomFields = fields; + $scope.allCustomFields = fields; $scope.mandatoryFields = _.without(_.map($scope.orderedFields, function(cf) { var fieldDef = fields[cf]; + + if(!fieldDef) { + return; + } + var fieldValue = $scope.caze.customFields[cf][cf.type]; if((fieldValue === undefined || fieldValue === null) && fieldDef.mandatory === true) { diff --git a/ui/app/scripts/controllers/case/CaseExportDialogCtrl.js b/ui/app/scripts/controllers/case/CaseExportDialogCtrl.js index b86458f7f9..e6de0c147d 100644 --- a/ui/app/scripts/controllers/case/CaseExportDialogCtrl.js +++ b/ui/app/scripts/controllers/case/CaseExportDialogCtrl.js @@ -66,7 +66,7 @@ NotificationSrv.log('The case has been successfully exported, but '+ failure +' observable(s) failed', 'warning'); } else { - success = angular.isObject(response.data) ? 1 : response.data.length; + success = angular.isArray(response.data) ? response.data.length : 1 ; NotificationSrv.log('The case has been successfully exported with ' + success+ ' observable(s)', 'success'); $uibModalInstance.close(); } @@ -85,6 +85,6 @@ } self.loading = false; }); - } + }; }); })(); diff --git a/ui/app/scripts/controllers/case/CaseListCtrl.js b/ui/app/scripts/controllers/case/CaseListCtrl.js index 0997c18634..933bb964f0 100644 --- a/ui/app/scripts/controllers/case/CaseListCtrl.js +++ b/ui/app/scripts/controllers/case/CaseListCtrl.js @@ -246,8 +246,8 @@ self.caseResponders = responders; }) .catch(function(err) { - NotificationSrv.error('CaseList', response.data, response.status); - }) + NotificationSrv.error('CaseList', err.data, err.status); + }); }; this.runResponder = function(responderId, responderName, caze) { diff --git a/ui/app/scripts/controllers/case/CaseObservablesItemCtrl.js b/ui/app/scripts/controllers/case/CaseObservablesItemCtrl.js index 12955fe013..26a0e3dc17 100644 --- a/ui/app/scripts/controllers/case/CaseObservablesItemCtrl.js +++ b/ui/app/scripts/controllers/case/CaseObservablesItemCtrl.js @@ -23,6 +23,7 @@ $scope.analysisEnabled = VersionSrv.hasCortex(); $scope.cortexServers = $scope.analysisEnabled && appConfig.connectors.cortex.servers; $scope.protectDownloadsWith = appConfig.config.protectDownloadsWith; + $scope.similarArtifactsLimit = 10; $scope.editorOptions = { lineNumbers: true, @@ -125,6 +126,10 @@ } }; + $scope.showMoreSimilar = function() { + $scope.similarArtifactsLimit = $scope.similarArtifactsLimit + 10; + }; + $scope.showReport = function (jobId) { $scope.report = {}; @@ -155,7 +160,9 @@ }; $scope.similarArtifacts = CaseArtifactSrv.api().similar({ - 'artifactId': observableId + artifactId: observableId, + range: 'all', + sort: ['-startDate'] }); diff --git a/ui/app/scripts/services/CasesUISrv.js b/ui/app/scripts/services/CasesUISrv.js index 20080a716f..cae968fdfc 100644 --- a/ui/app/scripts/services/CasesUISrv.js +++ b/ui/app/scripts/services/CasesUISrv.js @@ -165,10 +165,10 @@ // Prepare the filter value if (field === 'keyword') { - query = value; + query = value.replace(/"/gi, '\\"'); } else if (angular.isArray(value) && value.length > 0) { query = _.map(value, function(val) { - return field + ':"' + convertFn(val.text) + '"'; + return field + ':"' + convertFn(val.text.replace(/"/gi, '\\"')) + '"'; }).join(' OR '); query = '(' + query + ')'; } else if (filterDef.type === 'date') { @@ -178,7 +178,7 @@ query = field + ':[ ' + fromDate + ' TO ' + toDate + ' ]'; } else { - query = field + ':' + convertFn(value); + query = field + ':' + convertFn(value.replace(/"/gi, '\\"')); } factory.filters[field] = { diff --git a/ui/app/scripts/services/CustomFieldsSrv.js b/ui/app/scripts/services/CustomFieldsSrv.js new file mode 100644 index 0000000000..e6ac051a2b --- /dev/null +++ b/ui/app/scripts/services/CustomFieldsSrv.js @@ -0,0 +1,17 @@ +(function () { + 'use strict'; + angular.module('theHiveServices') + .factory('CustomFieldsSrv', function ($http) { + + var factory = { + removeField: function (field) { + return $http.delete('./api/list/' + field.id); + }, + usage: function(field) { + return $http.get('./api/customFields/' + field.reference); + } + }; + + return factory; + }); +})(); diff --git a/ui/app/views/partials/admin/case-template/custom-fields.html b/ui/app/views/partials/admin/case-template/custom-fields.html index b1a0ef0531..d85d6440b0 100644 --- a/ui/app/views/partials/admin/case-template/custom-fields.html +++ b/ui/app/views/partials/admin/case-template/custom-fields.html @@ -20,7 +20,7 @@

Delete
- +