diff --git a/app/org/thp/cortex/controllers/StatusCtrl.scala b/app/org/thp/cortex/controllers/StatusCtrl.scala index 2e8900231..83b25b4b9 100644 --- a/app/org/thp/cortex/controllers/StatusCtrl.scala +++ b/app/org/thp/cortex/controllers/StatusCtrl.scala @@ -1,57 +1,71 @@ package org.thp.cortex.controllers -import scala.concurrent.ExecutionContext - +import scala.concurrent.{ExecutionContext, Future} import play.api.Configuration import play.api.http.Status import play.api.libs.json.Json.toJsFieldJsValueWrapper -import play.api.libs.json.{JsBoolean, JsString, Json} +import play.api.libs.json.{JsBoolean, JsNull, JsString, Json} import play.api.mvc.{AbstractController, Action, AnyContent, ControllerComponents} - import com.sksamuel.elastic4s.ElasticDsl +import org.elastic4play.controllers.Authenticated + import javax.inject.{Inject, Singleton} import org.elasticsearch.client.Node -import org.thp.cortex.models.Worker - +import org.thp.cortex.models.{Roles, Worker, WorkerType} import org.elastic4play.services.AuthSrv import org.elastic4play.services.auth.MultiAuthSrv +import org.thp.cortex.services.WorkerSrv @Singleton class StatusCtrl @Inject() ( configuration: Configuration, authSrv: AuthSrv, + workerSrv: WorkerSrv, components: ControllerComponents, + authenticated: Authenticated, implicit val ec: ExecutionContext ) extends AbstractController(components) with Status { private[controllers] def getVersion(c: Class[_]) = Option(c.getPackage.getImplementationVersion).getOrElse("SNAPSHOT") - def get: Action[AnyContent] = Action { - Ok( - Json.obj( - "versions" -> Json.obj( - "Cortex" -> getVersion(classOf[Worker]), - "Elastic4Play" -> getVersion(classOf[AuthSrv]), - "Play" -> getVersion(classOf[AbstractController]), - "Elastic4s" -> getVersion(classOf[ElasticDsl]), - "ElasticSearch client" -> getVersion(classOf[Node]) - ), - "config" -> Json.obj( - "protectDownloadsWith" -> configuration.get[String]("datastore.attachment.password"), - "authType" -> (authSrv match { - case multiAuthSrv: MultiAuthSrv => - multiAuthSrv.authProviders.map { a => - JsString(a.name) - } - case _ => JsString(authSrv.name) - }), - "capabilities" -> authSrv.capabilities.map(c => JsString(c.toString)), - "ssoAutoLogin" -> JsBoolean(configuration.getOptional[Boolean]("auth.sso.autologin").getOrElse(false)) + def get: Action[AnyContent] = + Action { + Ok( + Json.obj( + "versions" -> Json.obj( + "Cortex" -> getVersion(classOf[Worker]), + "Elastic4Play" -> getVersion(classOf[AuthSrv]), + "Play" -> getVersion(classOf[AbstractController]), + "Elastic4s" -> getVersion(classOf[ElasticDsl]), + "ElasticSearch client" -> getVersion(classOf[Node]) + ), + "config" -> Json.obj( + "protectDownloadsWith" -> configuration.get[String]("datastore.attachment.password"), + "authType" -> (authSrv match { + case multiAuthSrv: MultiAuthSrv => + multiAuthSrv.authProviders.map { a => + JsString(a.name) + } + case _ => JsString(authSrv.name) + }), + "capabilities" -> authSrv.capabilities.map(c => JsString(c.toString)), + "ssoAutoLogin" -> JsBoolean(configuration.getOptional[Boolean]("auth.sso.autologin").getOrElse(false)) + ) ) ) - ) - } + } + + def getAlerts: Action[AnyContent] = + authenticated(Roles.read).async { implicit request => + workerSrv.obsoleteWorkersForUser(request.userId).map { obsoleteWorkers => + val (obsoleteAnalyzers, obsoleteResponders) = obsoleteWorkers.partition(_.tpe() == WorkerType.analyzer) + val alerts = + (if (obsoleteAnalyzers.nonEmpty) List("ObsoleteAnalyzers") else Nil) ::: + (if (obsoleteResponders.nonEmpty) List("ObsoleteResponders") else Nil) + Ok(Json.toJson(alerts)) + } + } def health: Action[AnyContent] = TODO } diff --git a/app/org/thp/cortex/models/WorkerDefinition.scala b/app/org/thp/cortex/models/WorkerDefinition.scala index 7d0300f10..10406b447 100644 --- a/app/org/thp/cortex/models/WorkerDefinition.scala +++ b/app/org/thp/cortex/models/WorkerDefinition.scala @@ -110,7 +110,23 @@ object WorkerDefinition { def reads(workerType: WorkerType.Type): Reads[List[WorkerDefinition]] = { val reads = singleReads(workerType) - reads.map(List(_)) orElse Reads.list(reads) + implicitly[Reads[JsArray]] + reads.map(List(_)) orElse JsArrayReads.map(array => + array + .value + .toList + .flatMap { js => + reads + .reads(js) + .fold( + invalid => { + logger.warn(s"The catalog contains an invalid entry\n entry:$js\n $invalid") + Seq.empty + }, + Seq(_) + ) + } + ) } implicit val writes: Writes[WorkerDefinition] = Writes[WorkerDefinition] { workerDefinition => diff --git a/app/org/thp/cortex/services/DockerJobRunnerSrv.scala b/app/org/thp/cortex/services/DockerJobRunnerSrv.scala index 830e1e62b..298e4f167 100644 --- a/app/org/thp/cortex/services/DockerJobRunnerSrv.scala +++ b/app/org/thp/cortex/services/DockerJobRunnerSrv.scala @@ -62,7 +62,7 @@ class DockerJobRunnerSrv( ec: ExecutionContext ): Try[Unit] = { import scala.collection.JavaConverters._ - if (autoUpdate) client.pull(dockerImage) + if (autoUpdate) Try(client.pull(dockerImage)) // ContainerConfig.builder().addVolume() val hostConfigBuilder = HostConfig.builder() config.getOptional[Seq[String]]("docker.container.capAdd").map(_.asJava).foreach(hostConfigBuilder.capAdd) diff --git a/app/org/thp/cortex/services/JobRunnerSrv.scala b/app/org/thp/cortex/services/JobRunnerSrv.scala index 1731eec4a..2760714d7 100644 --- a/app/org/thp/cortex/services/JobRunnerSrv.scala +++ b/app/org/thp/cortex/services/JobRunnerSrv.scala @@ -129,7 +129,7 @@ class JobRunnerSrv @Inject() ( .deepMerge(worker.config) .deepMerge(proxy_http) .deepMerge(proxy_https) - (worker.config \ "cacerts").asOpt[String].foreach { cacerts => + (worker.config \ "cacerts").asOpt[String].filterNot(_.trim.isEmpty).foreach { cacerts => val cacertsFile = jobFolder.resolve("input").resolve("cacerts") Files.write(cacertsFile, cacerts.getBytes) } diff --git a/app/org/thp/cortex/services/WorkerSrv.scala b/app/org/thp/cortex/services/WorkerSrv.scala index 8b4a37cdd..9de59b0bd 100644 --- a/app/org/thp/cortex/services/WorkerSrv.scala +++ b/app/org/thp/cortex/services/WorkerSrv.scala @@ -1,28 +1,26 @@ package org.thp.cortex.services -import java.net.URL -import java.nio.file.{Files, Path, Paths} - -import scala.collection.JavaConverters._ -import scala.concurrent.{ExecutionContext, Future} -import scala.io.Codec -import scala.util.{Failure, Success, Try} - -import play.api.libs.json.{JsArray, JsObject, JsString, Json} -import play.api.{Configuration, Logger} - import akka.NotUsed import akka.stream.Materializer import akka.stream.scaladsl.{Sink, Source} -import javax.inject.{Inject, Provider, Singleton} -import org.scalactic.Accumulation._ -import org.scalactic._ -import org.thp.cortex.models._ - import org.elastic4play._ import org.elastic4play.controllers.{Fields, StringInputValue} import org.elastic4play.database.ModifyConfig +import org.elastic4play.services.QueryDSL.any import org.elastic4play.services._ +import org.scalactic.Accumulation._ +import org.scalactic._ +import org.thp.cortex.models._ +import play.api.libs.json.{JsObject, JsString, Json} +import play.api.{Configuration, Logger} + +import java.net.URL +import java.nio.file.{Files, Path, Paths} +import javax.inject.{Inject, Provider, Singleton} +import scala.collection.JavaConverters._ +import scala.concurrent.{ExecutionContext, Future} +import scala.io.Codec +import scala.util.{Failure, Success, Try} @Singleton class WorkerSrv @Inject() ( @@ -128,21 +126,23 @@ class WorkerSrv @Inject() ( private def find(queryDef: QueryDef, range: Option[String], sortBy: Seq[String]): (Source[Worker, NotUsed], Future[Long]) = findSrv[WorkerModel, Worker](workerModel, queryDef, range, sortBy) - def rescan(): Unit = { - import org.elastic4play.services.QueryDSL._ + def rescan(): Unit = scan( analyzersURLs.map(_ -> WorkerType.analyzer) ++ respondersURLs.map(_ -> WorkerType.responder) - ).onComplete { _ => - userSrv.inInitAuthContext { implicit authContext => - find(any, Some("all"), Nil)._1.runForeach { worker => - workerMap.get(worker.workerDefinitionId()) match { - case Some(wd) => update(worker, Fields.empty.set("dataTypeList", Json.toJson(wd.dataTypeList))) - case None => update(worker, Fields.empty.set("dataTypeList", JsArray.empty)) - } - } - } + ) + + def obsoleteWorkersForUser(userId: String): Future[Seq[Worker]] = + userSrv.get(userId).flatMap { user => + obsoleteWorkersForOrganization(user.organization()) } + + def obsoleteWorkersForOrganization(organizationId: String): Future[Seq[Worker]] = { + import org.elastic4play.services.QueryDSL._ + find(withParent("organization", organizationId), Some("all"), Nil) + ._1 + .filterNot(worker => workerMap.contains(worker.workerDefinitionId())) + .runWith(Sink.seq) } def scan(workerUrls: Seq[(String, WorkerType.Type)]): Future[Unit] = { @@ -250,7 +250,7 @@ class WorkerSrv @Inject() ( .set("command", workerDefinition.command.map(p => JsString(p.toString))) .set("url", workerDefinition.url) .set("license", workerDefinition.license) - .set("baseConfig", workerDefinition.baseConfiguration.map(JsString.apply)) + .set("baseConfig", workerDefinition.baseConfiguration.fold(JsString(workerDefinition.name))(JsString.apply)) .set("configuration", cfg.toString) .set("type", workerDefinition.tpe.toString) .addIfAbsent("dataTypeList", StringInputValue(workerDefinition.dataTypeList)) diff --git a/conf/routes b/conf/routes index 20f7519fd..5cbab37dc 100644 --- a/conf/routes +++ b/conf/routes @@ -13,6 +13,7 @@ POST /api/ssoLogin org.thp.cort ################### # API used by TheHive GET /api/status org.thp.cortex.controllers.StatusCtrl.get +GET /api/alert org.thp.cortex.controllers.StatusCtrl.getAlerts GET /api/analyzer org.thp.cortex.controllers.AnalyzerCtrl.find POST /api/analyzer/_search org.thp.cortex.controllers.AnalyzerCtrl.find GET /api/analyzer/:id org.thp.cortex.controllers.AnalyzerCtrl.get(id) diff --git a/version.sbt b/version.sbt index 283b9e4e0..72cb9276a 100644 --- a/version.sbt +++ b/version.sbt @@ -1 +1 @@ -ThisBuild / version := "3.1.6-1" +ThisBuild / version := "3.1.7-1" diff --git a/www/.nvmrc b/www/.nvmrc new file mode 100644 index 000000000..52589560b --- /dev/null +++ b/www/.nvmrc @@ -0,0 +1 @@ +v12.18 diff --git a/www/package.json b/www/package.json index 27994f827..d38b1847a 100755 --- a/www/package.json +++ b/www/package.json @@ -1,6 +1,6 @@ { "name": "cortex", - "version": "3.1.6", + "version": "3.1.7", "description": "A powerfull observable analysis engine", "license": "AGPL-3.0-or-later", "homepage": "https://github.com/TheHive-Project/Cortex", diff --git a/www/src/app/components/header/header.component.js b/www/src/app/components/header/header.component.js index 4f6d18ab4..b1e85eb02 100644 --- a/www/src/app/components/header/header.component.js +++ b/www/src/app/components/header/header.component.js @@ -13,22 +13,24 @@ class HeaderController { constructor( $state, $log, - $q, $uibModal, + $scope, AuthService, AnalyzerService, - NotificationService + NotificationService, + AlertService ) { 'ngInject'; this.$state = $state; this.$log = $log; this.$uibModal = $uibModal; - this.$q = $q; + this.$scope = $scope; this.AuthService = AuthService; this.AnalyzerService = AnalyzerService; this.NotificationService = NotificationService; + this.AlertService = AlertService; } logout() { @@ -44,6 +46,11 @@ class HeaderController { $onInit() { this.isOrgAdmin = this.AuthService.isOrgAdmin(this.main.currentUser); this.isSuperAdmin = this.AuthService.isSuperAdmin(this.main.currentUser); + + this.AlertService.startUpdate(); + this.$scope.$on('$destroy', () => { + this.AlertService.stopUpdate(); + }); } newAnalysis() { @@ -67,8 +74,8 @@ class HeaderController { if (!_.isString(err)) { this.NotificationService.error( err.data.message || - `An error occurred: ${err.statusText}` || - 'An unexpected error occurred' + `An error occurred: ${err.statusText}` || + 'An unexpected error occurred' ); } }); diff --git a/www/src/app/components/header/header.html b/www/src/app/components/header/header.html index cfba5224d..903569376 100644 --- a/www/src/app/components/header/header.html +++ b/www/src/app/components/header/header.html @@ -47,7 +47,8 @@
  • - + + Organization
  • @@ -116,4 +117,4 @@ - + \ No newline at end of file diff --git a/www/src/app/components/header/header.scss b/www/src/app/components/header/header.scss index b1ef8855b..413a7fa10 100644 --- a/www/src/app/components/header/header.scss +++ b/www/src/app/components/header/header.scss @@ -1,14 +1,16 @@ .navbar { .avatar.avatar-xs { line-height: 20px; + span { line-height: 20px; font-weight: bold; } + .avatar-icon { margin-top: -5px; } - } + } } .profile-dropdown { @@ -18,4 +20,19 @@ right: 0; } } +} + +.notif-badge { + position: relative; +} + +.notif-badge:after { + position: absolute; + content: ""; + width: 10px; + height: 10px; + left: -5px; + top: -5px; + background-color: rgb(240, 43, 43); + border-radius: 50%; } \ No newline at end of file diff --git a/www/src/app/core/core.module.js b/www/src/app/core/core.module.js index 32f072132..55bd52a2d 100755 --- a/www/src/app/core/core.module.js +++ b/www/src/app/core/core.module.js @@ -16,6 +16,7 @@ import constants from './services/constants'; import notificationService from './services/common/NotificationService'; import streamService from './services/common/StreamService'; import versionService from './services/common/VersionService'; +import AlertService from './services/common/AlertService'; import utilsService from './services/common/UtilsService'; import fangFilter from './filters/fang'; @@ -32,7 +33,8 @@ core .service('HtmlSanitizer', HtmlSanitizer) .service('SearchService', SearchService) .service('UserService', UserService) - .service('ModalService', ModalService); + .service('ModalService', ModalService) + .service('AlertService', AlertService); fixedHeightDirective(core); fileChooserDirective(core); diff --git a/www/src/app/core/services/common/AlertService.js b/www/src/app/core/services/common/AlertService.js new file mode 100644 index 000000000..9444a25ae --- /dev/null +++ b/www/src/app/core/services/common/AlertService.js @@ -0,0 +1,52 @@ +'user strict'; + +import _ from "lodash"; + +export default class AlertService { + constructor($http, $interval) { + 'ngInject'; + + this.$http = $http; + this.$interval = $interval; + this.update = 0; + this.alerts = []; + } + + startUpdate() { + this.update += 1; + if (!this.timer) { + this.timer = this.$interval(this.updateAlerts, 10000, 0, true, this); + } + } + + stopUpdate() { + this.update -= 1; + if (this.update <= 0 && this.$interval) { + this.$interval.cancel(this.timer); + delete this.$interval; + } + } + + updateAlerts(self) { + self.$http.get('./api/alert').then( + response => { + self.alerts = response.data; + }, + () => { + self.alerts = []; + } + ); + } + + contains(alertType) { + return _.find(this.alerts, { type: alertType }); + } + + nonEmpty() { + return this.alerts.length > 0; + } + + isEmpty() { + return this.alerts.length === 0; + } +} \ No newline at end of file diff --git a/www/src/app/pages/admin/organizations/components/analyzers/analyzers-list.controller.js b/www/src/app/pages/admin/organizations/components/analyzers/analyzers-list.controller.js index 5d262e895..6cf0eec01 100644 --- a/www/src/app/pages/admin/organizations/components/analyzers/analyzers-list.controller.js +++ b/www/src/app/pages/admin/organizations/components/analyzers/analyzers-list.controller.js @@ -33,9 +33,7 @@ export default class OrganizationAnalyzersController { $onInit() { this.activeAnalyzers = _.keyBy(this.analyzers, 'analyzerDefinitionId'); this.definitionsIds = _.keys(this.analyzerDefinitions).sort(); - this.invalidAnalyzers = _.filter(this.analyzers, a => - _.isEmpty(a.dataTypeList) - ); + this.obsoleteAnalyzers = _.filter(this.analyzers, a => !this.definitionsIds.includes(a.workerDefinitionId)); } openModal(mode, definition, analyzer) { diff --git a/www/src/app/pages/admin/organizations/components/analyzers/analyzers-list.html b/www/src/app/pages/admin/organizations/components/analyzers/analyzers-list.html index cc7177a42..e300573b1 100644 --- a/www/src/app/pages/admin/organizations/components/analyzers/analyzers-list.html +++ b/www/src/app/pages/admin/organizations/components/analyzers/analyzers-list.html @@ -1,15 +1,16 @@
    -
    +
    -

    You have {{$ctrl.invalidAnalyzers.length}} invalid You have {{$ctrl.obsoleteAnalyzers.length}} obsolete

    -

    Invalid analyzers have no definition and cannot be run on any observable. You have to remove them.

    +

    Obsolete analyzers have been removed from the catalog. They have most likely been updated. You have to remove + them and enable the new version.

    -
    +

    {{a.name}} diff --git a/www/src/app/pages/admin/organizations/components/responders/responders-list.html b/www/src/app/pages/admin/organizations/components/responders/responders-list.html index 5701d6a1b..9f3899244 100644 --- a/www/src/app/pages/admin/organizations/components/responders/responders-list.html +++ b/www/src/app/pages/admin/organizations/components/responders/responders-list.html @@ -1,16 +1,17 @@
    -
    +
    -

    You have {{$ctrl.invalidResponders.length}} invalid - +

    You have {{$ctrl.obsoleteResponders.length}} Obsolete +

    -

    Invalid responders have no definition and cannot be run on any observable. You have to remove them.

    +

    Obsolete responders have been removed from the catalog. They have most likely been updated. You have to remove + them and enable the new version.

    -
    +

    {{r.name}} diff --git a/www/src/app/pages/admin/organizations/details/organization.page.controller.js b/www/src/app/pages/admin/organizations/details/organization.page.controller.js index f66ad1969..084603181 100644 --- a/www/src/app/pages/admin/organizations/details/organization.page.controller.js +++ b/www/src/app/pages/admin/organizations/details/organization.page.controller.js @@ -4,18 +4,31 @@ export default class OrganizationPageController { constructor( $log, $stateParams, + $scope, AnalyzerService, ResponderService, OrganizationService, - AuthService + AuthService, + AlertService ) { 'ngInject'; this.$log = $log; + this.$scope = $scope; this.orgId = $stateParams.id; this.AnalyzerService = AnalyzerService; this.ResponderService = ResponderService; this.OrganizationService = OrganizationService; this.AuthService = AuthService; + this.AlertService = AlertService; + } + + + $onInit() { + this.AlertService.startUpdate(); + + this.$scope.$on('$destroy', () => { + this.AlertService.stopUpdte(); + }); } } \ No newline at end of file diff --git a/www/src/app/pages/admin/organizations/details/organization.page.html b/www/src/app/pages/admin/organizations/details/organization.page.html index 94ab1fe96..2f2c79bb3 100644 --- a/www/src/app/pages/admin/organizations/details/organization.page.html +++ b/www/src/app/pages/admin/organizations/details/organization.page.html @@ -1,39 +1,53 @@
    -

    Organization: +

    Organization: {{$ctrl.organization.id}} - Back to list + Back to list

    - + - Users - + Users + + - Analyzers Config - + Analyzers Config + + + - Analyzers - + + + Analyzers + + - Responders Config - + Responders Config + + - Responders - + + + Responders + + - + -
    +

    \ No newline at end of file diff --git a/www/src/app/pages/jobs/job.page.html b/www/src/app/pages/jobs/job.page.html index b8ceea0b8..cb369d592 100644 --- a/www/src/app/pages/jobs/job.page.html +++ b/www/src/app/pages/jobs/job.page.html @@ -18,6 +18,9 @@

    Job details

    Job report

    +
    Parameters
    +
    {{$ctrl.job.parameters | json}}
    +
    Input details
    {{$ctrl.job.data | json}}