Skip to content

Commit

Permalink
#426 Make obsolete worker still available and notify user to update them
Browse files Browse the repository at this point in the history
  • Loading branch information
To-om committed Sep 21, 2022
1 parent 4001272 commit 6b80b4e
Show file tree
Hide file tree
Showing 13 changed files with 214 additions and 93 deletions.
72 changes: 43 additions & 29 deletions app/org/thp/cortex/controllers/StatusCtrl.scala
Original file line number Diff line number Diff line change
@@ -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
}
54 changes: 27 additions & 27 deletions app/org/thp/cortex/services/WorkerSrv.scala
Original file line number Diff line number Diff line change
@@ -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() (
Expand Down Expand Up @@ -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] = {
Expand Down
1 change: 1 addition & 0 deletions conf/routes
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
17 changes: 12 additions & 5 deletions www/src/app/components/header/header.component.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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() {
Expand All @@ -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'
);
}
});
Expand Down
5 changes: 3 additions & 2 deletions www/src/app/components/header/header.html
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@
</li>
<li ui-sref-active="active" require-roles="orgadmin">
<a ui-sref="main.organization({id: $ctrl.main.currentUser.organization})">
<i class="fa fa-file-text"></i>
<i class="fa fa-file-text" ng-if="$ctrl.AlertService.isEmpty()"></i>
<i class="fa fa-file-text notif-badge" ng-if="$ctrl.AlertService.nonEmpty()"></i>
<strong>Organization</strong>
</a>
</li>
Expand Down Expand Up @@ -116,4 +117,4 @@
</ul>
</div>
</div>
</nav>
</nav>
19 changes: 18 additions & 1 deletion www/src/app/components/header/header.scss
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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%;
}
4 changes: 3 additions & 1 deletion www/src/app/core/core.module.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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);
Expand Down
52 changes: 52 additions & 0 deletions www/src/app/core/services/common/AlertService.js
Original file line number Diff line number Diff line change
@@ -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, 1000, 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;
},
rejection => {
self.alerts = [];
}
);
}

contains(alertType) {
return _.find(this.alerts, { type: alertType });
}

nonEmpty() {
return this.alerts.length > 0;
}

isEmpty() {
return this.alerts.length == 0;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
<section>

<div class="mb-s" ng-if="$ctrl.invalidAnalyzers.length > 0">
<div class="mb-s" ng-if="$ctrl.obsoleteAnalyzers.length > 0">
<div class="callout callout-warning">
<h4>You have {{$ctrl.invalidAnalyzers.length}} invalid <ng-pluralize count="$ctrl.invalidAnalyzers.length"
<h4>You have {{$ctrl.obsoleteAnalyzers.length}} obsolete <ng-pluralize count="$ctrl.obsoleteAnalyzers.length"
when="{'1': 'analyzer', 'other': 'analyzers'}"></ng-pluralize>
</h4>
<p>Invalid analyzers have no definition and cannot be run on any observable. You have to remove them.</p>
<p>Obsolete analyzers have been removed from the catalog. They have most likely been updated. You have to remove
them and enable the new version.</p>
</div>
<div class="row">
<div class="col-sm-12 flex-table">
<div class="flex-row media" ng-repeat="a in $ctrl.invalidAnalyzers">
<div class="flex-row media" ng-repeat="a in $ctrl.obsoleteAnalyzers">
<div class="flex-col flex-1">
<h4 class="media-heading">
<span class="mr-m text-primary">{{a.name}}</span>
Expand Down
Loading

0 comments on commit 6b80b4e

Please sign in to comment.