Skip to content

Commit

Permalink
#306 Add a global health api
Browse files Browse the repository at this point in the history
  • Loading branch information
To-om committed Nov 13, 2017
1 parent 5b721ff commit 2ae13a7
Show file tree
Hide file tree
Showing 8 changed files with 107 additions and 23 deletions.
2 changes: 2 additions & 0 deletions thehive-backend/app/connectors/Connectors.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@ import play.api.routing.sird.UrlContext
import play.api.routing.{ Router, SimpleRouter }

import com.google.inject.AbstractModule
import models.HealthStatus
import net.codingwell.scalaguice.{ ScalaModule, ScalaMultibinder }

trait Connector {
val name: String
val router: Router
def status: Future[JsObject] = Future.successful(Json.obj("enabled" true))
def health: Future[HealthStatus.Type] = Future.successful(HealthStatus.Ok)
}

@Singleton
Expand Down
24 changes: 22 additions & 2 deletions thehive-backend/app/controllers/StatusCtrl.scala
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import play.api.mvc.{ AbstractController, Action, AnyContent, ControllerComponen

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

import org.elastic4play.Timed
import org.elastic4play.database.DBIndex
Expand All @@ -23,7 +24,7 @@ import org.elastic4play.services.auth.MultiAuthSrv
class StatusCtrl @Inject() (
connectors: immutable.Set[Connector],
configuration: Configuration,
dBIndex: DBIndex,
dbIndex: DBIndex,
authSrv: AuthSrv,
components: ControllerComponents,
implicit val ec: ExecutionContext) extends AbstractController(components) {
Expand All @@ -32,7 +33,7 @@ class StatusCtrl @Inject() (

@Timed("controllers.StatusCtrl.get")
def get: Action[AnyContent] = Action.async {
val clusterStatusName = Try(dBIndex.clusterStatusName).getOrElse("ERROR")
val clusterStatusName = Try(dbIndex.clusterStatusName).getOrElse("ERROR")
Future.traverse(connectors)(c c.status.map(c.name _))
.map { connectorStatus
Ok(Json.obj(
Expand All @@ -53,4 +54,23 @@ class StatusCtrl @Inject() (
"capabilities" authSrv.capabilities.map(c JsString(c.toString)))))
}
}

@Timed("controllers.StatusCtrl.health")
def health: Action[AnyContent] = Action.async {
for {
dbStatusInt dbIndex.getClusterStatus
dbStatus = dbStatusInt match {
case 0 HealthStatus.Ok
case 1 HealthStatus.Warning
case _ HealthStatus.Error
}
connectorStatus Future.traverse(connectors)(c c.health)
distinctStatus = connectorStatus + dbStatus
globalStatus = if (distinctStatus.contains(HealthStatus.Ok)) {
if (distinctStatus.size > 1) HealthStatus.Warning else HealthStatus.Ok
}
else if (distinctStatus.contains(HealthStatus.Error)) HealthStatus.Error
else HealthStatus.Warning
} yield Ok(globalStatus.toString)
}
}
8 changes: 8 additions & 0 deletions thehive-backend/app/models/HealthStatus.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package models

import org.elastic4play.models.HiveEnumeration

object HealthStatus extends Enumeration with HiveEnumeration {
type Type = Value
val Ok, Warning, Error = Value
}
1 change: 1 addition & 0 deletions thehive-backend/conf/routes
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

GET / controllers.Default.redirect(to = "/index.html")
GET /api/status controllers.StatusCtrl.get
GET /api/health controllers.StatusCtrl.health
GET /api/logout controllers.AuthenticationCtrl.logout()
POST /api/login controllers.AuthenticationCtrl.login()

Expand Down
17 changes: 15 additions & 2 deletions thehive-cortex/app/connectors/cortex/controllers/CortexCtrl.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@ import org.elastic4play.{ BadRequestError, NotFoundError, Timed }
import org.elastic4play.controllers.{ Authenticated, Fields, FieldsBodyParser, Renderer }
import org.elastic4play.models.JsonFormat.baseModelEntityWrites
import org.elastic4play.services.{ Agg, AuxSrv, QueryDSL, QueryDef }
import org.elastic4play.services.JsonFormat.{ queryReads, aggReads }
import org.elastic4play.services.JsonFormat.{ aggReads, queryReads }
import connectors.Connector
import connectors.cortex.models.JsonFormat.analyzerFormats
import connectors.cortex.services.{ CortexConfig, CortexSrv }
import models.Roles
import models.HealthStatus.Type
import models.{ HealthStatus, Roles }

@Singleton
class CortexCtrl @Inject() (
Expand Down Expand Up @@ -53,6 +54,18 @@ class CortexCtrl @Inject() (
"status" healthStatus)
}

override def health: Future[Type] = {
Future.traverse(cortexConfig.instances)(instance instance.health())
.map { healthStatus
val distinctStatus = healthStatus.toSet
if (distinctStatus.contains(HealthStatus.Ok)) {
if (distinctStatus.size > 1) HealthStatus.Warning else HealthStatus.Ok
}
else if (distinctStatus.contains(HealthStatus.Error)) HealthStatus.Error
else HealthStatus.Warning
}
}

val router = SimpleRouter {
case POST(p"/job") createJob
case GET(p"/job/$jobId<[^/]*>") getJob(jobId)
Expand Down
15 changes: 14 additions & 1 deletion thehive-cortex/app/connectors/cortex/services/CortexClient.scala
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import akka.actor.ActorSystem
import akka.stream.scaladsl.Source
import connectors.cortex.models.JsonFormat._
import connectors.cortex.models.{ Analyzer, CortexArtifact, DataArtifact, FileArtifact }
import models.HealthStatus
import services.CustomWSAPI

import org.elastic4play.utils.RichFuture
Expand Down Expand Up @@ -77,14 +78,18 @@ class CortexClient(val name: String, baseUrl: String, key: String, authenticatio
request(s"api/job/$jobId/waitreport", _.withQueryStringParameters("atMost" atMost.toString).get, _.json.as[JsObject])
}

def status()(implicit system: ActorSystem, ec: ExecutionContext): Future[JsObject] =
def getVersion()(implicit system: ActorSystem, ec: ExecutionContext): Future[Option[String]] = {
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)
}

def status()(implicit system: ActorSystem, ec: ExecutionContext): Future[JsObject] =
getVersion()
.map {
case Some(version) Json.obj(
"name" name,
Expand All @@ -95,4 +100,12 @@ class CortexClient(val name: String, baseUrl: String, key: String, authenticatio
"version" "",
"status" "ERROR")
}

def health()(implicit system: ActorSystem, ec: ExecutionContext): Future[HealthStatus.Type] = {
getVersion()
.map {
case None HealthStatus.Error
case _ HealthStatus.Ok
}
}
}
49 changes: 32 additions & 17 deletions thehive-misp/app/connectors/misp/MispConnection.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import play.api.libs.json.{ JsObject, Json }
import play.api.libs.ws.WSRequest

import akka.actor.ActorSystem
import models.HealthStatus
import services.CustomWSAPI

import org.elastic4play.utils.RichFuture
Expand Down Expand Up @@ -35,21 +36,35 @@ case class MispConnection(
"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")
}
def getVersion()(implicit system: ActorSystem, ec: ExecutionContext): Future[Option[String]] = {
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)
}

def status()(implicit system: ActorSystem, ec: ExecutionContext): Future[JsObject] = {
getVersion()
.map {
case Some(version) Json.obj(
"name" name,
"version" version,
"status" "OK")
case None Json.obj(
"name" name,
"version" "",
"status" "ERROR")
}
}

def healthStatus()(implicit system: ActorSystem, ec: ExecutionContext): Future[HealthStatus.Type] = {
getVersion()
.map {
case None HealthStatus.Error
case _ HealthStatus.Ok
}
}
}
14 changes: 13 additions & 1 deletion thehive-misp/app/connectors/misp/MispCtrl.scala
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import play.api.routing.sird.{ GET, POST, UrlContext }

import akka.actor.ActorSystem
import connectors.Connector
import models.{ Alert, Case, Roles, UpdateMispAlertArtifact }
import models._
import services.{ AlertTransformer, CaseSrv }

import org.elastic4play.JsonFormat.tryWrites
Expand Down Expand Up @@ -52,6 +52,18 @@ class MispCtrl @Inject() (
"status" healthStatus)
}

override def health: Future[HealthStatus.Type] = {
Future.traverse(mispConfig.connections)(_.healthStatus())
.map { healthStatus
val distinctStatus = healthStatus.toSet
if (distinctStatus.contains(HealthStatus.Ok)) {
if (distinctStatus.size > 1) HealthStatus.Warning else HealthStatus.Ok
}
else if (distinctStatus.contains(HealthStatus.Error)) HealthStatus.Error
else HealthStatus.Warning
}
}

private[MispCtrl] lazy val logger = Logger(getClass)
val router = SimpleRouter {
case GET(p"/_syncAlerts") syncAlerts
Expand Down

1 comment on commit 2ae13a7

@romans8
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like when I try Cortex it reads. TODO.

Please sign in to comment.