Skip to content

Commit

Permalink
#130 Include server version and status for MISP and Cortex connectors…
Browse files Browse the repository at this point in the history
… in status page
  • Loading branch information
To-om committed Oct 20, 2017
1 parent 195bc6f commit 2f82d50
Show file tree
Hide file tree
Showing 10 changed files with 133 additions and 39 deletions.
3 changes: 2 additions & 1 deletion thehive-backend/app/connectors/Connectors.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package connectors
import javax.inject.{ Inject, Singleton }

import scala.collection.immutable
import scala.concurrent.Future

import play.api.libs.json.{ JsObject, Json }
import play.api.mvc._
Expand All @@ -15,7 +16,7 @@ import net.codingwell.scalaguice.{ ScalaModule, ScalaMultibinder }
trait Connector {
val name: String
val router: Router
val status: JsObject = Json.obj("enabled" true)
def status: Future[JsObject] = Future.successful(Json.obj("enabled" true))
}

@Singleton
Expand Down
46 changes: 27 additions & 19 deletions thehive-backend/app/controllers/StatusCtrl.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,44 +3,52 @@ package controllers
import javax.inject.{ Inject, Singleton }

import scala.collection.immutable
import scala.concurrent.{ ExecutionContext, Future }

import play.api.Configuration
import play.api.libs.json.{ JsObject, JsString, Json }
import play.api.libs.json.Json.toJsFieldJsValueWrapper
import play.api.mvc.{ AbstractController, ControllerComponents }
import play.api.mvc.{ AbstractController, Action, AnyContent, ControllerComponents }

import com.sksamuel.elastic4s.ElasticDsl
import connectors.Connector

import org.elastic4play.Timed
import org.elastic4play.database.DBIndex
import org.elastic4play.services.AuthSrv
import org.elastic4play.services.auth.MultiAuthSrv

@Singleton
class StatusCtrl @Inject() (
connectors: immutable.Set[Connector],
configuration: Configuration,
dBIndex: DBIndex,
authSrv: AuthSrv,
components: ControllerComponents) extends AbstractController(components) {
components: ControllerComponents,
implicit val ec: ExecutionContext) extends AbstractController(components) {

private[controllers] def getVersion(c: Class[_]) = Option(c.getPackage.getImplementationVersion).getOrElse("SNAPSHOT")

@Timed("controllers.StatusCtrl.get")
def get = Action {
Ok(Json.obj(
"versions" Json.obj(
"TheHive" getVersion(classOf[models.Case]),
"Elastic4Play" getVersion(classOf[Timed]),
"Play" getVersion(classOf[AbstractController]),
"Elastic4s" getVersion(classOf[ElasticDsl]),
"ElasticSearch" getVersion(classOf[org.elasticsearch.Build])),
"connectors" JsObject(connectors.map(c c.name c.status).toSeq),
"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)))))
def get: Action[AnyContent] = Action.async {
Future.traverse(connectors)(c c.status.map(c.name _))
.map { connectorStatus
Ok(Json.obj(
"versions" Json.obj(
"TheHive" getVersion(classOf[models.Case]),
"Elastic4Play" getVersion(classOf[Timed]),
"Play" getVersion(classOf[AbstractController]),
"Elastic4s" getVersion(classOf[ElasticDsl]),
"ElasticSearch" getVersion(classOf[org.elasticsearch.Build])),
"connectors" JsObject(connectorStatus.toSeq),
"health" Json.obj("elasticsearch" dBIndex.clusterStatusName),
"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)))))
}
}
}
}
21 changes: 18 additions & 3 deletions thehive-cortex/app/connectors/cortex/controllers/CortexCtrl.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package connectors.cortex.controllers

import javax.inject.{ Inject, Singleton }

import scala.concurrent.ExecutionContext
import scala.concurrent.{ ExecutionContext, Future }

import play.api.Logger
import play.api.http.Status
Expand All @@ -11,6 +11,8 @@ import play.api.mvc._
import play.api.routing.SimpleRouter
import play.api.routing.sird.{ DELETE, GET, PATCH, POST, UrlContext }

import akka.actor.ActorSystem

import org.elastic4play.{ BadRequestError, NotFoundError, Timed }
import org.elastic4play.controllers.{ Authenticated, Fields, FieldsBodyParser, Renderer }
import org.elastic4play.models.JsonFormat.baseModelEntityWrites
Expand All @@ -31,12 +33,25 @@ class CortexCtrl @Inject() (
fieldsBodyParser: FieldsBodyParser,
renderer: Renderer,
components: ControllerComponents,
implicit val ec: ExecutionContext) extends AbstractController(components) with Connector with Status {
implicit val ec: ExecutionContext,
implicit val system: ActorSystem) extends AbstractController(components) with Connector with Status {

val name = "cortex"
private[CortexCtrl] lazy val logger = Logger(getClass)

override val status: JsObject = Json.obj("enabled" true, "servers" cortexConfig.instances.map(_.name))
override def status: Future[JsObject] =
Future.traverse(cortexConfig.instances)(instance instance.status())
.map { statusDetails
val distinctStatus = statusDetails.map(s (s \ "status").as[String]).toSet
val healthStatus = if (distinctStatus.contains("OK")) {
if (distinctStatus.size > 1) "WARNING" else "OK"
}
else "ERROR"
Json.obj(
"enabled" true,
"servers" statusDetails,
"status" healthStatus)
}

val router = SimpleRouter {
case POST(p"/job") createJob
Expand Down
40 changes: 31 additions & 9 deletions thehive-cortex/app/connectors/cortex/services/CortexClient.scala
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
package connectors.cortex.services

import akka.stream.scaladsl.Source
import connectors.cortex.models.JsonFormat._
import connectors.cortex.models.{ Analyzer, CortexArtifact, DataArtifact, FileArtifact }
import scala.concurrent.duration._
import scala.concurrent.{ ExecutionContext, Future }

import play.api.Logger
import play.api.libs.json.{ JsObject, JsValue, Json }
import play.api.libs.ws.{ WSAuthScheme, WSRequest, WSResponse }
import play.api.libs.ws.WSBodyWritables.writeableOf_JsValue
import play.api.mvc.MultipartFormData.{ DataPart, FilePart }

import akka.actor.ActorSystem
import akka.stream.scaladsl.Source
import connectors.cortex.models.JsonFormat._
import connectors.cortex.models.{ Analyzer, CortexArtifact, DataArtifact, FileArtifact }
import services.CustomWSAPI

import scala.concurrent.duration.Duration
import scala.concurrent.{ ExecutionContext, Future }
import org.elastic4play.utils.RichFuture

case class CortexError(status: Int, requestUrl: String, message: String) extends Exception(s"Cortex error on $requestUrl ($status) \n$message")
class CortexClient(val name: String, baseUrl: String, key: String, authentication: Option[(String, String)], ws: CustomWSAPI) {
Expand Down Expand Up @@ -67,10 +70,29 @@ class CortexClient(val name: String, baseUrl: String, key: String, authenticatio
}

def report(jobId: String)(implicit ec: ExecutionContext): Future[JsObject] = {
request(s"api/job/$jobId/report", _.get, r r.json.as[JsObject])
request(s"api/job/$jobId/report", _.get, _.json.as[JsObject])
}

def waitReport(jobId: String, atMost: Duration)(implicit ec: ExecutionContext): Future[JsObject] = {
request(s"api/job/$jobId/waitreport", _.withQueryStringParameters("atMost" atMost.toString).get, r r.json.as[JsObject])
request(s"api/job/$jobId/waitreport", _.withQueryStringParameters("atMost" atMost.toString).get, _.json.as[JsObject])
}
}

def status()(implicit system: ActorSystem, ec: ExecutionContext): Future[JsObject] =
request("api/status", _.get, identity)
.map {
case resp if resp.status / 100 == 2 (resp.json \ "versions" \ "Cortex").asOpt[String]
case _ None
}
.recover { case _ None }
.withTimeout(1.seconds, None)
.map {
case Some(version) Json.obj(
"name" name,
"version" version,
"status" "OK")
case None Json.obj(
"name" name,
"version" "",
"status" "ERROR")
}
}
28 changes: 27 additions & 1 deletion thehive-misp/app/connectors/misp/MispConnection.scala
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
package connectors.misp

import scala.concurrent.duration._
import scala.concurrent.{ ExecutionContext, Future }

import play.api.Logger
import play.api.libs.json.{ JsObject, Json }
import play.api.libs.ws.WSRequest

import akka.actor.ActorSystem
import services.CustomWSAPI

import org.elastic4play.utils.RichFuture

case class MispConnection(
name: String,
baseUrl: String,
Expand All @@ -21,9 +29,27 @@ case class MispConnection(
|\tcase template: ${caseTemplate.getOrElse("<not set>")}
|\tartifact tags: ${artifactTags.mkString}""".stripMargin)

private[misp] def apply(url: String) =
private[misp] def apply(url: String): WSRequest =
ws.url(s"$baseUrl/$url")
.withHttpHeaders(
"Authorization" key,
"Accept" "application/json")

def status()(implicit system: ActorSystem, ec: ExecutionContext): Future[JsObject] = apply("servers/getVersion").get
.map {
case resp if resp.status / 100 == 2 (resp.json \ "version").asOpt[String]
case _ None
}
.recover { case _ None }
.withTimeout(1.seconds, None)
.map {
case Some(version) Json.obj(
"name" name,
"version" version,
"status" "OK")
case None Json.obj(
"name" name,
"version" "",
"status" "ERROR")
}
}
18 changes: 16 additions & 2 deletions thehive-misp/app/connectors/misp/MispCtrl.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import play.api.mvc._
import play.api.routing.SimpleRouter
import play.api.routing.sird.{ GET, POST, UrlContext }

import akka.actor.ActorSystem
import connectors.Connector
import models.{ Alert, Case, Roles, UpdateMispAlertArtifact }
import services.{ AlertTransformer, CaseSrv }
Expand All @@ -32,11 +33,24 @@ class MispCtrl @Inject() (
renderer: Renderer,
eventSrv: EventSrv,
components: ControllerComponents,
implicit val ec: ExecutionContext) extends AbstractController(components) with Connector with Status with AlertTransformer {
implicit val ec: ExecutionContext,
implicit val system: ActorSystem) extends AbstractController(components) with Connector with Status with AlertTransformer {

override val name: String = "misp"

override val status: JsObject = Json.obj("enabled" true, "servers" mispConfig.connections.map(_.name))
override def status: Future[JsObject] =
Future.traverse(mispConfig.connections)(_.status())
.map { statusDetails
val distinctStatus = statusDetails.map(s (s \ "status").as[String]).toSet
val healthStatus = if (distinctStatus.contains("OK")) {
if (distinctStatus.size > 1) "WARNING" else "OK"
}
else "ERROR"
Json.obj(
"enabled" true,
"servers" statusDetails,
"status" healthStatus)
}

private[MispCtrl] lazy val logger = Logger(getClass)
val router = SimpleRouter {
Expand Down
2 changes: 1 addition & 1 deletion ui/app/scripts/controllers/case/CaseExportDialogCtrl.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

this.caze = caze;
this.mode = '';
this.servers = config.servers;
this.servers = config.servers; // TODO Nabil
this.failures = [];

this.existingExports = {};
Expand Down
4 changes: 2 additions & 2 deletions ui/app/scripts/services/VersionSrv.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@
hasCortex: function() {
try {
var service = cache.connectors.cortex;
return service.enabled && service.servers.length;

return service.enabled && _.pluck(service.servers, 'status').indexOf('OK') !== -1;
} catch (err) {
return false;
}
Expand Down
8 changes: 8 additions & 0 deletions ui/app/views/components/app-container.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,17 @@
<span>
<strong>Version</strong>: {{appConfig.versions.TheHive}}
</span>
<!--
show appConfig.connectors.cortex.status (OK, WARNING or ERROR)
and in tooltip the list of appConfig.connectors.cortex.server (property name, status and version)
-->
<span ng-if="appConfig.connectors.cortex.enabled" uib-tooltip="Cortex integration enabled">
<img class="footer-logo" src="images/cortex-logo.svg"/>
</span>
<!--
show appConfig.connectors.misp.status (OK, WARNING or ERROR)
and in tooltip the list of appConfig.connectors.misp.server (property name, status and version)
-->
<span ng-if="appConfig.connectors.misp.enabled" uib-tooltip="MISP integration enabled">
<img class="footer-logo" src="images/misp-logo.svg"/>
</span>
Expand Down
2 changes: 1 addition & 1 deletion ui/app/views/partials/misp/case.export.confirm.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ <h3 class="modal-title">MISP Export</h3>
</p>

<table class="mt-m table table-striped valigned">
<tbody class="pv-xs" ng-repeat="server in dialog.servers">
<tbody class="pv-xs" ng-repeat="server in dialog.servers"> <!-- TODO Nabil -->
<tr>
<td><h4>{{server}}</h4></td>
<td width="150">
Expand Down

0 comments on commit 2f82d50

Please sign in to comment.