diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f8802cf3e..973161994d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,37 @@ # Change Log -## [3.0.9](https://github.com/TheHive-Project/TheHive/tree/3.0.9) +## [3.0.10](https://github.com/TheHive-Project/TheHive/tree/3.0.10) (2018-05-29) + +[Full Changelog](https://github.com/TheHive-Project/TheHive/compare/3.0.9...3.0.10) + +**Implemented enhancements:** + +- Rotate logs [\#579](https://github.com/TheHive-Project/TheHive/issues/579) +- Send caseId to Cortex analyzer [\#564](https://github.com/TheHive-Project/TheHive/issues/564) +- Poll for connectors status and display [\#563](https://github.com/TheHive-Project/TheHive/issues/563) +- Sort related cases by related artifacts amount [\#548](https://github.com/TheHive-Project/TheHive/issues/548) +- Time Calculation for individual tasks [\#546](https://github.com/TheHive-Project/TheHive/issues/546) + +**Fixed bugs:** + +- Wrong error message when creating a observable with invalid data [\#592](https://github.com/TheHive-Project/TheHive/issues/592) +- Analyzer name not reflected in modal view of mini-reports [\#586](https://github.com/TheHive-Project/TheHive/issues/586) +- Invalid searches lead to read error messages [\#584](https://github.com/TheHive-Project/TheHive/issues/584) +- Merge case by ID brings red error message if not a number in textfield [\#583](https://github.com/TheHive-Project/TheHive/issues/583) +- Open cases not listed after deletion of merged case in UI [\#557](https://github.com/TheHive-Project/TheHive/issues/557) +- Making dashboards private makes them "invisible" [\#555](https://github.com/TheHive-Project/TheHive/issues/555) +- MISP Synchronisation error [\#522](https://github.com/TheHive-Project/TheHive/issues/522) +- Short Report is not shown on observables \(3.0.8\) [\#512](https://github.com/TheHive-Project/TheHive/issues/512) +- Artifacts reports are not merged when merging cases [\#446](https://github.com/TheHive-Project/TheHive/issues/446) + +**Closed issues:** + +- Max Age Filter Not Working? [\#577](https://github.com/TheHive-Project/TheHive/issues/577) +- Support X-Pack authentication/encryption for elastic [\#570](https://github.com/TheHive-Project/TheHive/issues/570) +- Order the cases list by custom field \[Feature Request\] [\#567](https://github.com/TheHive-Project/TheHive/issues/567) +- Using Postman to test the API, getting "No CSRF token found in headers" [\#549](https://github.com/TheHive-Project/TheHive/issues/549) + +## [3.0.9](https://github.com/TheHive-Project/TheHive/tree/3.0.9) (2018-04-13) [Full Changelog](https://github.com/TheHive-Project/TheHive/compare/3.0.8...3.0.9) **Fixed bugs:** @@ -37,7 +68,7 @@ - Add ElasticSearch file descriptor limit to docker-compose.yml [\#505](https://github.com/TheHive-Project/TheHive/pull/505) ([flmsc](https://github.com/flmsc)) -## [3.0.7](https://github.com/TheHive-Project/TheHive/tree/3.0.7) (2018-03-29) +## [3.0.7](https://github.com/TheHive-Project/TheHive/tree/3.0.7) (2018-04-03) [Full Changelog](https://github.com/TheHive-Project/TheHive/compare/3.0.6...3.0.7) **Implemented enhancements:** @@ -65,8 +96,8 @@ **Fixed bugs:** - - Importing Template Button Non-Functional bug [\#404](https://github.com/TheHive-Project/TheHive/issues/404) - - No reports available for "domain" type bug [\#409](https://github.com/TheHive-Project/TheHive/issues/409) +- No reports available for "domain" type [\#469](https://github.com/TheHive-Project/TheHive/issues/469) +- Importing Template Button Non-Functional [\#404](https://github.com/TheHive-Project/TheHive/issues/404) ## [3.0.4](https://github.com/TheHive-Project/TheHive/tree/3.0.4) (2018-02-06) [Full Changelog](https://github.com/TheHive-Project/TheHive/compare/3.0.3...3.0.4) diff --git a/conf/logback.xml b/conf/logback.xml index 864f2b5f23..156c86a57e 100644 --- a/conf/logback.xml +++ b/conf/logback.xml @@ -1,39 +1,50 @@ - - - - ${application.home:-.}/logs/application.log - - %date [%level] from %logger in %thread - %message%n%xException - - - - - - %coloredLevel %logger{15} - %message%n%xException{10} - - - - - - - - - - - - - - - - - - - - - + + + + ${application.home:-.}/logs/application.log + + ${application.home:-.}/logs/application.%i.log.zip + 1 + 10 + + + 10MB + + + %date [%level] from %logger in %thread - %message%n%xException + + + + + + %coloredLevel %logger{15} - %message%n%xException{10} + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/package/logback.xml b/package/logback.xml index fc1a000d7f..2257a996c6 100644 --- a/package/logback.xml +++ b/package/logback.xml @@ -1,39 +1,50 @@ - - - - /var/log/thehive/application.log - - %date [%level] from %logger in %thread - %message%n%xException - - - - - - %coloredLevel %logger{15} - %message%n%xException{10} - - - - - - - - - - - - - - - - - - - - - + + + + /var/log/thehive/application.log + + ${application.home:-.}/logs/application.%i.log.zip + 1 + 10 + + + 10MB + + + %date [%level] from %logger in %thread - %message%n%xException + + + + + + %coloredLevel %logger{15} - %message%n%xException{10} + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/thehive-backend/app/controllers/SearchCtrl.scala b/thehive-backend/app/controllers/SearchCtrl.scala index 01e8e656bc..7ff0a3fa52 100644 --- a/thehive-backend/app/controllers/SearchCtrl.scala +++ b/thehive-backend/app/controllers/SearchCtrl.scala @@ -5,7 +5,7 @@ import javax.inject.{ Inject, Singleton } import scala.concurrent.{ ExecutionContext, Future } import play.api.http.Status -import play.api.libs.json.{ JsObject, Json } +import play.api.libs.json.JsObject import play.api.mvc.{ AbstractController, Action, ControllerComponents } import models.Roles @@ -56,7 +56,7 @@ class SearchCtrl @Inject() ( findSrv.apply(model, and(globalQuery ::: query), agg: _*) } .map { statsResults ⇒ - renderer.toOutput(OK, statsResults.reduceOption(_ deepMerge _).getOrElse(Json.obj())) + renderer.toOutput(OK, statsResults.reduceOption(_ deepMerge _).getOrElse(JsObject.empty)) } } } \ No newline at end of file diff --git a/thehive-backend/app/models/Alert.scala b/thehive-backend/app/models/Alert.scala index a7cd983b91..b77e210806 100644 --- a/thehive-backend/app/models/Alert.scala +++ b/thehive-backend/app/models/Alert.scala @@ -27,23 +27,23 @@ trait AlertAttributes { _: AttributeDef ⇒ val artifactAttributes: Seq[Attribute[_]] = { val remoteAttachmentAttributes = Seq( - Attribute("alert", "reference", F.stringFmt, Seq(O.readonly), None, ""), - Attribute("alert", "filename", OptionalAttributeFormat(F.stringFmt), Seq(O.readonly), None, ""), - Attribute("alert", "contentType", OptionalAttributeFormat(F.stringFmt), Seq(O.readonly), None, ""), - Attribute("alert", "size", OptionalAttributeFormat(F.numberFmt), Seq(O.readonly), None, ""), - Attribute("alert", "hash", MultiAttributeFormat(F.stringFmt), Seq(O.readonly), None, ""), - Attribute("alert", "type", OptionalAttributeFormat(F.stringFmt), Seq(O.readonly), None, "")) + Attribute("alert", "reference", F.stringFmt, Nil, None, ""), + Attribute("alert", "filename", OptionalAttributeFormat(F.stringFmt), Nil, None, ""), + Attribute("alert", "contentType", OptionalAttributeFormat(F.stringFmt), Nil, None, ""), + Attribute("alert", "size", OptionalAttributeFormat(F.numberFmt), Nil, None, ""), + Attribute("alert", "hash", MultiAttributeFormat(F.stringFmt), Nil, None, ""), + Attribute("alert", "type", OptionalAttributeFormat(F.stringFmt), Nil, None, "")) Seq( - Attribute("alert", "data", OptionalAttributeFormat(F.stringFmt), Seq(O.readonly), None, ""), - Attribute("alert", "dataType", F.stringFmt, Seq(O.readonly), None, ""), - Attribute("alert", "message", OptionalAttributeFormat(F.stringFmt), Seq(O.readonly), None, ""), - Attribute("alert", "startDate", OptionalAttributeFormat(F.dateFmt), Seq(O.readonly), None, ""), - Attribute("alert", "attachment", OptionalAttributeFormat(F.attachmentFmt), Seq(O.readonly), None, ""), - Attribute("alert", "remoteAttachment", OptionalAttributeFormat(F.objectFmt(remoteAttachmentAttributes)), Seq(O.readonly), None, ""), - Attribute("alert", "tlp", OptionalAttributeFormat(TlpAttributeFormat), Seq(O.readonly), None, ""), - Attribute("alert", "tags", MultiAttributeFormat(F.stringFmt), Seq(O.readonly), None, ""), - Attribute("alert", "ioc", OptionalAttributeFormat(F.booleanFmt), Seq(O.readonly), None, "")) + Attribute("alert", "data", OptionalAttributeFormat(F.stringFmt), Nil, None, ""), + Attribute("alert", "dataType", F.stringFmt, Nil, None, ""), + Attribute("alert", "message", OptionalAttributeFormat(F.stringFmt), Nil, None, ""), + Attribute("alert", "startDate", OptionalAttributeFormat(F.dateFmt), Nil, None, ""), + Attribute("alert", "attachment", OptionalAttributeFormat(F.attachmentFmt), Nil, None, ""), + Attribute("alert", "remoteAttachment", OptionalAttributeFormat(F.objectFmt(remoteAttachmentAttributes)), Nil, None, ""), + Attribute("alert", "tlp", OptionalAttributeFormat(TlpAttributeFormat), Nil, None, ""), + Attribute("alert", "tags", MultiAttributeFormat(F.stringFmt), Nil, None, ""), + Attribute("alert", "ioc", OptionalAttributeFormat(F.booleanFmt), Nil, None, "")) } val alertId: A[String] = attribute("_id", F.stringFmt, "Alert id", O.readonly) diff --git a/thehive-backend/app/models/Case.scala b/thehive-backend/app/models/Case.scala index 6780b6d1d7..907692374f 100644 --- a/thehive-backend/app/models/Case.scala +++ b/thehive-backend/app/models/Case.scala @@ -56,15 +56,20 @@ trait CaseAttributes { _: AttributeDef ⇒ @Singleton class CaseModel @Inject() ( - artifactModel: Provider[ArtifactModel], - taskModel: Provider[TaskModel], - caseSrv: Provider[CaseSrv], - alertModel: Provider[AlertModel], + artifactModelProvider: Provider[ArtifactModel], + taskModelProvider: Provider[TaskModel], + caseSrvProvider: Provider[CaseSrv], + alertModelProvider: Provider[AlertModel], sequenceSrv: SequenceSrv, findSrv: FindSrv, implicit val ec: ExecutionContext) extends ModelDef[CaseModel, Case]("case", "Case", "/case") with CaseAttributes with AuditedModel { caseModel ⇒ - private[CaseModel] lazy val logger = Logger(getClass) + private lazy val logger = Logger(getClass) + private lazy val artifactModel = artifactModelProvider.get + private lazy val taskModel = taskModelProvider.get + private lazy val caseSrv = caseSrvProvider.get + private lazy val alertModel = alertModelProvider.get + override val defaultSortBy = Seq("-startDate") override val removeAttribute: JsObject = Json.obj("status" → CaseStatus.Deleted) @@ -90,7 +95,7 @@ class CaseModel @Inject() ( private[models] def buildArtifactStats(caze: Case): Future[JsObject] = { import org.elastic4play.services.QueryDSL._ findSrv( - artifactModel.get, + artifactModel, and( parent("case", withId(caze.id)), "status" ~= "Ok"), @@ -103,7 +108,7 @@ class CaseModel @Inject() ( private[models] def buildTaskStats(caze: Case): Future[JsObject] = { import org.elastic4play.services.QueryDSL._ findSrv( - taskModel.get, + taskModel, and( parent("case", withId(caze.id)), "status" in ("Waiting", "InProgress", "Completed")), @@ -120,34 +125,44 @@ class CaseModel @Inject() ( private[models] def buildMergeIntoStats(caze: Case): Future[JsObject] = { caze.mergeInto() - .fold(Future.successful(Json.obj())) { mergeCaseId ⇒ - caseSrv.get.get(mergeCaseId).map { c ⇒ + .fold(Future.successful(JsObject.empty)) { mergeCaseId ⇒ + caseSrv.get(mergeCaseId).map { c ⇒ Json.obj("mergeInto" → Json.obj( "caseId" → c.caseId(), "title" → c.title())) } + .recover { + case _ ⇒ Json.obj("mergeInto" → Json.obj( + "caseId" → "", + "title" → "")) + } } } private[models] def buildMergeFromStats(caze: Case): Future[JsObject] = { Future .traverse(caze.mergeFrom()) { id ⇒ - caseSrv.get.get(id).map { c ⇒ + caseSrv.get(id).map { c ⇒ Json.obj( "caseId" → c.caseId(), "title" → c.title()) } + .recover { + case _ ⇒ Json.obj( + "caseId" → "", + "title" → "") + } } .map { case mf if mf.nonEmpty ⇒ Json.obj("mergeFrom" → mf) - case _ ⇒ Json.obj() + case _ ⇒ JsObject.empty } } private[models] def buildAlertStats(caze: Case): Future[JsObject] = { import org.elastic4play.services.QueryDSL._ findSrv( - alertModel.get, + alertModel, "case" ~= caze.id, groupByField("type", groupByField("source", selectCount))) .map { alertStatsJson ⇒ @@ -172,7 +187,7 @@ class CaseModel @Inject() ( } yield taskStats ++ artifactStats ++ alertStats ++ mergeIntoStats ++ mergeFromStats case other ⇒ logger.warn(s"Request caseStats from a non-case entity ?! ${other.getClass}:$other") - Future.successful(Json.obj()) + Future.successful(JsObject.empty) } } diff --git a/thehive-backend/app/services/ArtifactSrv.scala b/thehive-backend/app/services/ArtifactSrv.scala index f7006f123e..c1b5b2b7b1 100644 --- a/thehive-backend/app/services/ArtifactSrv.scala +++ b/thehive-backend/app/services/ArtifactSrv.scala @@ -47,7 +47,7 @@ class ArtifactSrv @Inject() ( def create(caze: Case, fields: Fields)(implicit authContext: AuthContext): Future[Artifact] = { createSrv[ArtifactModel, Artifact, Case](artifactModel, caze, fields) .recoverWith { - case _ ⇒ updateIfDeleted(caze, fields) // maybe the artifact already exists. If so, search it and update it + case _: ConflictError ⇒ updateIfDeleted(caze, fields) // if the artifact already exists, search it and update it } } diff --git a/thehive-cortex/app/connectors/cortex/services/CortexSrv.scala b/thehive-cortex/app/connectors/cortex/services/CortexSrv.scala index 43b158245c..3d29c57d40 100644 --- a/thehive-cortex/app/connectors/cortex/services/CortexSrv.scala +++ b/thehive-cortex/app/connectors/cortex/services/CortexSrv.scala @@ -2,30 +2,30 @@ package connectors.cortex.services import java.util.Date -import javax.inject.{ Inject, Singleton } +import scala.concurrent.duration.{ DurationInt, FiniteDuration } +import scala.concurrent.{ ExecutionContext, Future, Promise } +import scala.util.control.NonFatal +import scala.util.{ Failure, Success, Try } + +import play.api.libs.json.{ JsObject, Json } +import play.api.libs.ws.WSClient +import play.api.{ Configuration, Logger } + import akka.NotUsed -import akka.actor.Actor +import akka.actor.{ Actor, ActorSystem } import akka.stream.Materializer import akka.stream.scaladsl.{ Sink, Source } import connectors.cortex.models.JsonFormat._ import connectors.cortex.models._ +import javax.inject.{ Inject, Singleton } import models.Artifact +import services.{ ArtifactSrv, CaseSrv, CustomWSAPI, MergeArtifact, RemoveJobsOf } import org.elastic4play.controllers.Fields -import org.elastic4play.services._ +import org.elastic4play.database.{ DBRemove, ModifyConfig } import org.elastic4play.services.JsonFormat.attachmentFormat +import org.elastic4play.services._ import org.elastic4play.{ InternalError, NotFoundError } -import play.api.libs.json.{ JsObject, Json } -import play.api.libs.ws.WSClient -import play.api.{ Configuration, Logger } - -import services.{ ArtifactSrv, CustomWSAPI, MergeArtifact, RemoveJobsOf } -import scala.concurrent.duration.DurationInt -import scala.concurrent.{ ExecutionContext, Future } -import scala.util.control.NonFatal -import scala.util.{ Failure, Success, Try } - -import org.elastic4play.database.{ DBRemove, ModifyConfig } object CortexConfig { def getCortexClient(name: String, configuration: Configuration, ws: CustomWSAPI): Option[CortexClient] = { @@ -68,8 +68,11 @@ case class CortexConfig(instances: Seq[CortexClient]) { class JobReplicateActor @Inject() ( cortexSrv: CortexSrv, eventSrv: EventSrv, + implicit val ec: ExecutionContext, implicit val mat: Materializer) extends Actor { + private lazy val logger = Logger(getClass) + override def preStart(): Unit = { eventSrv.subscribe(self, classOf[MergeArtifact]) super.preStart() @@ -82,11 +85,14 @@ class JobReplicateActor @Inject() ( override def receive: Receive = { case MergeArtifact(newArtifact, artifacts, authContext) ⇒ + logger.info(s"Merging jobs from artifacts ${artifacts.map(_.id)} into artifact ${newArtifact.id}") import org.elastic4play.services.QueryDSL._ cortexSrv.find(and(parent("case_artifact", withId(artifacts.map(_.id): _*)), "status" ~= JobStatus.Success), Some("all"), Nil)._1 .mapAsyncUnordered(5) { job ⇒ - val baseFields = Fields(job.attributes - "_id" - "_routing" - "_parent" - "_type" - "createdBy" - "createdAt" - "updatedBy" - "updatedAt" - "user") - cortexSrv.create(newArtifact, baseFields)(authContext) + val baseFields = Fields(job.attributes - "_id" - "_routing" - "_parent" - "_type" - "_version" - "createdBy" - "createdAt" - "updatedBy" - "updatedAt" - "user") + val createdJob = cortexSrv.create(newArtifact, baseFields)(authContext) + createdJob.failed.foreach(error ⇒ logger.error(s"Fail to create job under artifact ${newArtifact.id}\n\tjob attributes: $baseFields", error)) + createdJob } .runWith(Sink.ignore) case RemoveJobsOf(artifactId) ⇒ @@ -101,6 +107,7 @@ class JobReplicateActor @Inject() ( class CortexSrv @Inject() ( cortexConfig: CortexConfig, jobModel: JobModel, + caseSrv: CaseSrv, artifactSrv: ArtifactSrv, attachmentSrv: AttachmentSrv, getSrv: GetSrv, @@ -109,6 +116,7 @@ class CortexSrv @Inject() ( findSrv: FindSrv, dbRemove: DBRemove, userSrv: UserSrv, + system: ActorSystem, implicit val ws: WSClient, implicit val ec: ExecutionContext, implicit val mat: Materializer) { @@ -219,9 +227,14 @@ class CortexSrv @Inject() ( getSrv[JobModel, Job](jobModel, jobId) } - def retryIf[A](f: Throwable ⇒ Boolean, maxRetry: Int)(body: ⇒ Future[A]): Future[A] = { + def retryOnError[A](cond: Throwable ⇒ Boolean = _ ⇒ true, maxRetry: Int = 5, initialDelay: FiniteDuration = 1.second)(body: ⇒ Future[A]): Future[A] = { body.recoverWith { - case e if maxRetry > 0 && f(e) ⇒ retryIf(f, maxRetry - 1)(body) + case e if maxRetry > 0 && cond(e) ⇒ + val resultPromise = Promise[A] + system.scheduler.scheduleOnce(initialDelay) { + resultPromise.completeWith(retryOnError(cond, maxRetry - 1, initialDelay * 2)(body)) + } + resultPromise.future } } @@ -248,12 +261,13 @@ class CortexSrv @Inject() ( .toOption .flatMap(r ⇒ (r \ "summary").asOpt[JsObject]) .getOrElse(JsObject.empty) - retryIf(_ ⇒ true, 5) { + retryOnError() { for { artifact ← artifactSrv.get(job.artifactId()) reports = Try(Json.parse(artifact.reports()).asOpt[JsObject]).toOption.flatten.getOrElse(JsObject.empty) newReports = reports + (job.analyzerDefinition().getOrElse(job.analyzerId()) → jobSummary) - } yield artifactSrv.update(job.artifactId(), Fields.empty.set("reports", newReports.toString), ModifyConfig(retryOnConflict = 0, version = Some(artifact.version))) + updatedArtifact ← artifactSrv.update(job.artifactId(), Fields.empty.set("reports", newReports.toString), ModifyConfig(retryOnConflict = 0, version = Some(artifact.version))) + } yield updatedArtifact } .recover { case NonFatal(t) ⇒ logger.warn(s"Unable to insert summary report in artifact", t) @@ -300,9 +314,11 @@ class CortexSrv @Inject() ( case (cortex, analyzer) ⇒ for { artifact ← artifactSrv.get(artifactId) + caze ← caseSrv.get(artifact.parentId.get) artifactAttributes = Json.obj( "tlp" → artifact.tlp(), - "dataType" → artifact.dataType()) + "dataType" → artifact.dataType(), + "message" → caze.caseId().toString) cortexArtifact = (artifact.data(), artifact.attachment()) match { case (Some(data), None) ⇒ DataArtifact(data, artifactAttributes) case (None, Some(attachment)) ⇒ FileArtifact(attachmentSrv.source(attachment.id), artifactAttributes + ("attachment" → Json.toJson(attachment))) diff --git a/thehive-misp/app/connectors/misp/MispSynchro.scala b/thehive-misp/app/connectors/misp/MispSynchro.scala index 55c04d4917..01238827ca 100644 --- a/thehive-misp/app/connectors/misp/MispSynchro.scala +++ b/thehive-misp/app/connectors/misp/MispSynchro.scala @@ -1,8 +1,8 @@ package connectors.misp import java.util.Date -import javax.inject.{ Inject, Provider, Singleton } +import javax.inject.{ Inject, Provider, Singleton } import scala.collection.immutable import scala.concurrent.{ ExecutionContext, Future } import scala.concurrent.duration._ @@ -23,6 +23,7 @@ import JsonFormat.mispAlertWrites import org.elastic4play.controllers.Fields import org.elastic4play.services.{ Attachment, AuthContext, MigrationSrv, TempSrv } +import org.elastic4play.utils.Collection @Singleton class MispSynchro @Inject() ( @@ -171,14 +172,19 @@ class MispSynchro @Inject() ( case _ ⇒ Future.successful(false) } .flatMap { updateStatus ⇒ - val artifacts = JsArray(alert.artifacts() ++ attrs.map(Json.toJson(_))) + val artifacts = Collection.distinctBy(alert.artifacts() ++ attrs.map(Json.toJson(_))) { a ⇒ + (a \ "data").getOrElse(JsNull).toString + + (a \ "dataType").getOrElse(JsNull).toString + + (a \ "attachment").getOrElse(JsNull).toString + + (a \ "remoteAttachment").getOrElse(JsNull).toString + } val alertJson = Json.toJson(event).as[JsObject] - "type" - "source" - "sourceRef" - "caseTemplate" - "date" + - ("artifacts" → artifacts) + + ("artifacts" → JsArray(artifacts)) + ("status" → (if (!updateStatus) Json.toJson(alert.status()) else alert.status() match { case AlertStatus.New ⇒ Json.toJson(AlertStatus.New) diff --git a/ui/app/scripts/controllers/RootCtrl.js b/ui/app/scripts/controllers/RootCtrl.js index 83e9e76423..19d344dee1 100644 --- a/ui/app/scripts/controllers/RootCtrl.js +++ b/ui/app/scripts/controllers/RootCtrl.js @@ -2,7 +2,7 @@ * Controller for main page */ angular.module('theHiveControllers').controller('RootCtrl', - function($scope, $rootScope, $uibModal, $location, $state, AuthenticationSrv, AlertingSrv, StreamSrv, StreamStatSrv, CaseTemplateSrv, CustomFieldsCacheSrv, MetricsCacheSrv, NotificationSrv, AppLayoutSrv, currentUser, appConfig) { + function($scope, $rootScope, $uibModal, $location, $state, AuthenticationSrv, AlertingSrv, StreamSrv, StreamStatSrv, CaseTemplateSrv, CustomFieldsCacheSrv, MetricsCacheSrv, NotificationSrv, AppLayoutSrv, VersionSrv, currentUser, appConfig) { 'use strict'; if(currentUser === 520) { @@ -22,9 +22,31 @@ angular.module('theHiveControllers').controller('RootCtrl', }; $scope.mispEnabled = false; $scope.customFieldsCache = []; + $scope.currentUser = currentUser; StreamSrv.init(); - $scope.currentUser = currentUser; + VersionSrv.startMonitoring(function(conf) { + var connectors = ['misp', 'cortex']; + + _.each(connectors, function(connector) { + var currentStatus = $scope.appConfig.connectors[connector]; + var newStatus = conf.connectors[connector]; + if(currentStatus.enabled === newStatus.enabled && + newStatus.enabled === true && + currentStatus.status !== newStatus.status) { + + if(newStatus.status === 'OK') { + NotificationSrv.log('The configured ' + connector.toUpperCase() + ' connections are now up.', 'success'); + } else if(newStatus.status === 'WARNING') { + NotificationSrv.log('Some of the configured ' + connector.toUpperCase() + ' connections have errors. Please check your configuration.', 'warning'); + } else { + NotificationSrv.log('The configured ' + connector.toUpperCase() + ' connections have errors. Please check your configuration.', 'error'); + } + } + }); + + $scope.appConfig = conf; + }); CaseTemplateSrv.list().then(function(templates) { $scope.templates = templates; diff --git a/ui/app/scripts/controllers/SearchCtrl.js b/ui/app/scripts/controllers/SearchCtrl.js index a1a41a46ce..ff05377e0e 100644 --- a/ui/app/scripts/controllers/SearchCtrl.js +++ b/ui/app/scripts/controllers/SearchCtrl.js @@ -43,7 +43,7 @@ $scope.getUserInfo = UserInfoSrv; $scope.searchResults = PSearchSrv(undefined, 'any', { 'filter': angular.fromJson(Base64.decode($stateParams.q)), - 'baseFilter': {_string: '!_type:audit AND !_type:data AND !_type:user AND !_type:analyzer AND !_type:case_artifact_job_log AND !status:Deleted'}, + 'baseFilter': {_string: '!_type:dashboard AND !_type:audit AND !_type:data AND !_type:user AND !_type:analyzer AND !_type:case_artifact_job_log AND !status:Deleted'}, 'nparent': 10, skipStream: true }); diff --git a/ui/app/scripts/controllers/case/CaseLinksCtrl.js b/ui/app/scripts/controllers/case/CaseLinksCtrl.js index e551918974..b7fcdd8052 100644 --- a/ui/app/scripts/controllers/case/CaseLinksCtrl.js +++ b/ui/app/scripts/controllers/case/CaseLinksCtrl.js @@ -5,7 +5,10 @@ $scope.caseId = $stateParams.caseId; $scope.linkStats = []; $scope.currentFilter = ''; - $scope.filtering = {} + $scope.filtering = {}; + $scope.sorting = { + field: '-startDate' + } var tabName = 'links-' + $scope.caseId; // Add tab @@ -66,6 +69,14 @@ } }; + $scope.sortBy = function(field) { + if($scope.sorting.field.substr(1) !== field) { + $scope.sorting.field = '+' + field; + } else { + $scope.sorting.field = ($scope.sorting.field === '+' + field) ? '-'+field : '+'+field; + } + }; + $scope.$watch('links', function(data){ $scope.linkStats = $scope.initStats(data); }); diff --git a/ui/app/scripts/controllers/case/CaseMainCtrl.js b/ui/app/scripts/controllers/case/CaseMainCtrl.js index 3fcd30253e..96c256ee53 100644 --- a/ui/app/scripts/controllers/case/CaseMainCtrl.js +++ b/ui/app/scripts/controllers/case/CaseMainCtrl.js @@ -51,7 +51,11 @@ CaseSrv.links({ caseId: $scope.caseId }, function(data) { - $scope.links = data; + $scope.links = _.map(data, function(item){ + item.linksCount = item.linkedWith.length || 0; + + return item; + }); if (data.length > 0) { $scope.newestLink = data[0]; diff --git a/ui/app/scripts/controllers/case/CaseObservablesCtrl.js b/ui/app/scripts/controllers/case/CaseObservablesCtrl.js index a086b03384..f9efba2965 100644 --- a/ui/app/scripts/controllers/case/CaseObservablesCtrl.js +++ b/ui/app/scripts/controllers/case/CaseObservablesCtrl.js @@ -69,6 +69,16 @@ $scope.uiSrv.setPageSize(newValue); }); + $scope.sortBy = function(field) { + if($scope.artifacts.sort.substr(1) !== field) { + $scope.artifacts.sort = '+' + field; + } else { + $scope.artifacts.sort = ($scope.artifacts.sort === '+' + field) ? '-'+field : '+'+field; + } + + $scope.artifacts.update(); + } + $scope.keys = function(obj) { return _.keys(obj || {}); }; diff --git a/ui/app/scripts/services/SearchSrv.js b/ui/app/scripts/services/SearchSrv.js index d154c3fecf..dae947fe09 100644 --- a/ui/app/scripts/services/SearchSrv.js +++ b/ui/app/scripts/services/SearchSrv.js @@ -34,7 +34,7 @@ }).success(function(data, status, headers) { cb(data, parseInt(headers('X-Total'))); }).error(function(data, status) { - NotificationSrv.error('SearchSrv', data, status); + NotificationSrv.error('SearchSrv', data.type || data.message, status); }); }; }); diff --git a/ui/app/scripts/services/VersionSrv.js b/ui/app/scripts/services/VersionSrv.js index 112bb029d4..bbd8d303c0 100644 --- a/ui/app/scripts/services/VersionSrv.js +++ b/ui/app/scripts/services/VersionSrv.js @@ -1,14 +1,24 @@ (function() { 'use strict'; angular.module('theHiveServices') - .factory('VersionSrv', function($http, $q) { + .factory('VersionSrv', function($http, $q, $interval) { var cache = null; var factory = { - get: function() { + startMonitoring: function(callback) { + $interval(function() { + factory.get(true) + .then(function(appConfig) { + if(callback) { + callback(appConfig); + } + }); + }, 60000); + }, + get: function(force) { var deferred = $q.defer(); - if(cache !== null) { + if(!force && cache !== null) { deferred.resolve(cache); } else { $http.get('./api/status').then(function(response) { diff --git a/ui/app/styles/main.css b/ui/app/styles/main.css index b6967c4f01..1ffc518d81 100644 --- a/ui/app/styles/main.css +++ b/ui/app/styles/main.css @@ -81,6 +81,10 @@ body { padding-top: 50px } +.text-default { + color: #333; +} + .text-20 { font-size: 20px; } diff --git a/ui/app/views/components/app-container.component.html b/ui/app/views/components/app-container.component.html index ca3e599c36..77cd80c977 100644 --- a/ui/app/views/components/app-container.component.html +++ b/ui/app/views/components/app-container.component.html @@ -26,7 +26,7 @@
- TheHive Project 2016-2017, AGPL-V3 + TheHive Project 2016-2018, AGPL-V3
diff --git a/ui/app/views/components/header.component.html b/ui/app/views/components/header.component.html index 66c086aaea..284063dd50 100644 --- a/ui/app/views/components/header.component.html +++ b/ui/app/views/components/header.component.html @@ -151,15 +151,21 @@ diff --git a/ui/app/views/partials/about.html b/ui/app/views/partials/about.html index c91674895a..b3a68519d6 100644 --- a/ui/app/views/partials/about.html +++ b/ui/app/views/partials/about.html @@ -19,7 +19,8 @@
- Copyright (C) 2014-2017 Thomas Franco, Saâd Kadhi, Jérôme Leonard + Copyright (C) 2016-2018 Thomas Franco, Saâd Kadhi, Jérôme Leonard
+ Copyright (C) 2017-2018 Nabil Adouani
diff --git a/ui/app/views/partials/case/case.links.html b/ui/app/views/partials/case/case.links.html index efb7f436bb..7bffcd719b 100644 --- a/ui/app/views/partials/case/case.links.html +++ b/ui/app/views/partials/case/case.links.html @@ -4,7 +4,7 @@ -