From f239da608b7aefeb0fcae21a6492f58578bd69ef Mon Sep 17 00:00:00 2001 From: Nabil Adouani Date: Tue, 25 Apr 2017 10:48:39 +0200 Subject: [PATCH 01/49] #52 Init misp export UI --- ui/app/index.html | 1 + ui/app/scripts/app.js | 11 +++ .../controllers/case/CaseExportCtrl.js | 63 +++++++++++++++ .../scripts/controllers/case/CaseMainCtrl.js | 6 ++ ui/app/scripts/services/MispSrv.js | 17 ++++ ui/app/views/partials/case/case.export.html | 79 +++++++++++++++++++ .../views/partials/case/case.panelinfo.html | 7 ++ 7 files changed, 184 insertions(+) create mode 100644 ui/app/scripts/controllers/case/CaseExportCtrl.js create mode 100644 ui/app/views/partials/case/case.export.html diff --git a/ui/app/index.html b/ui/app/index.html index cf277ef6a5..9ed00bc79e 100644 --- a/ui/app/index.html +++ b/ui/app/index.html @@ -147,6 +147,7 @@ + diff --git a/ui/app/scripts/app.js b/ui/app/scripts/app.js index 8dfb0b5132..f772e77ad9 100644 --- a/ui/app/scripts/app.js +++ b/ui/app/scripts/app.js @@ -239,6 +239,17 @@ angular.module('thehive', ['ngAnimate', 'ngMessages', 'ngSanitize', 'ui.bootstra templateUrl: 'views/partials/case/case.links.html', controller: 'CaseLinksCtrl' }) + .state('app.case.export', { + url: '/export', + templateUrl: 'views/partials/case/case.export.html', + controller: 'CaseExportCtrl', + controllerAs: '$vm', + resolve: { + categories: function(MispSrv) { + return MispSrv.categories(); + } + } + }) .state('app.case.tasks-item', { url: '/tasks/{itemId}', templateUrl: 'views/partials/case/case.tasks.item.html', diff --git a/ui/app/scripts/controllers/case/CaseExportCtrl.js b/ui/app/scripts/controllers/case/CaseExportCtrl.js new file mode 100644 index 0000000000..a153930ab7 --- /dev/null +++ b/ui/app/scripts/controllers/case/CaseExportCtrl.js @@ -0,0 +1,63 @@ +(function() { + 'use strict'; + angular.module('theHiveControllers').controller('CaseExportCtrl', + function($scope, $state, $stateParams, $timeout, PSearchSrv, CaseTabsSrv, categories) { + var self = this; + + this.caseId = $stateParams.caseId; + this.searchForm = {}; + var tabName = 'export-' + this.caseId; + + // MISP category/type map + this.categories = categories; + + // Add tab + CaseTabsSrv.addTab(tabName, { + name: tabName, + label: 'Export', + closable: true, + state: 'app.case.export', + params: {} + }); + + // Select tab + $timeout(function() { + CaseTabsSrv.activateTab(tabName); + }, 0); + + + this.artifacts = PSearchSrv(this.caseId, 'case_artifact', { + scope: $scope, + baseFilter: { + '_and': [{ + '_parent': { + "_type": "case", + "_query": { + "_id": $scope.caseId + } + } + }, { + 'ioc': true + }, { + 'status': 'Ok' + }] + }, + filter: this.searchForm.searchQuery !== '' ? { + _string: this.searchForm.searchQuery + } : '', + loadAll: true, + sort: '-startDate', + pageSize: 30, + onUpdate: function () { + self.enhanceArtifacts(); + }, + nstats: true + }); + + this.enhanceArtifacts = function(data) { + console.log(data); + } + + } + ); +})(); diff --git a/ui/app/scripts/controllers/case/CaseMainCtrl.js b/ui/app/scripts/controllers/case/CaseMainCtrl.js index f8ab031bcf..9003d9f2d2 100644 --- a/ui/app/scripts/controllers/case/CaseMainCtrl.js +++ b/ui/app/scripts/controllers/case/CaseMainCtrl.js @@ -198,6 +198,12 @@ }); }; + $scope.shareCase = function() { + $state.go('app.case.export', { + caseId: $scope.caseId + }); + }; + /** * A workaround filter to make sure the ngRepeat doesn't order the * object keys diff --git a/ui/app/scripts/services/MispSrv.js b/ui/app/scripts/services/MispSrv.js index 2c38651928..2e5a552834 100644 --- a/ui/app/scripts/services/MispSrv.js +++ b/ui/app/scripts/services/MispSrv.js @@ -115,6 +115,23 @@ defer.resolve(statuses); }); + return defer.promise; + }, + + categories: function() { + var defer = $q.defer(); + + $q.resolve({ + 'category1': [ + 'type1.1', 'type1.2', 'type1.3' + ], + 'category2': [ + 'type2.1', 'type2.2', 'type2.3' + ] + }).then(function(response) { + defer.resolve(response); + }); + return defer.promise; } }; diff --git a/ui/app/views/partials/case/case.export.html b/ui/app/views/partials/case/case.export.html new file mode 100644 index 0000000000..21267a5e1b --- /dev/null +++ b/ui/app/views/partials/case/case.export.html @@ -0,0 +1,79 @@ +
+
+
No records.
+
+
+ + +
+
+ + + +
+
+ + +
+
+ + +
+
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + TypeData/FilenameTagsMISP CategoryMISP Type 
+ + + + {{(artifact.data | fang) || (artifact.attachment.name | fang)}} + + + + + + + Not specified + Not specified + + Not specified + Not specified + + + +
+ +
+
diff --git a/ui/app/views/partials/case/case.panelinfo.html b/ui/app/views/partials/case/case.panelinfo.html index 88e4d41a60..4b76d8e6e4 100644 --- a/ui/app/views/partials/case/case.panelinfo.html +++ b/ui/app/views/partials/case/case.panelinfo.html @@ -37,6 +37,13 @@

+ + + + Share + + + From 5379580bac147a0dd54d56f1fe15a03f571e27ef Mon Sep 17 00:00:00 2001 From: To-om Date: Thu, 27 Jul 2017 09:18:36 +0200 Subject: [PATCH 02/49] #52 Export case into MISP event --- .../app/controllers/AlertCtrl.scala | 6 +- thehive-backend/app/models/Alert.scala | 2 +- .../app/connectors/misp/JsonFormat.scala | 9 + .../app/connectors/misp/MispCtrl.scala | 25 +- .../app/connectors/misp/MispSrv.scala | 224 ++++++++++++++++-- 5 files changed, 242 insertions(+), 24 deletions(-) diff --git a/thehive-backend/app/controllers/AlertCtrl.scala b/thehive-backend/app/controllers/AlertCtrl.scala index f90dff0be8..70f9526d15 100644 --- a/thehive-backend/app/controllers/AlertCtrl.scala +++ b/thehive-backend/app/controllers/AlertCtrl.scala @@ -33,7 +33,11 @@ class AlertCtrl @Inject() ( @Timed def create(): Action[Fields] = authenticated(Role.write).async(fieldsBodyParser) { implicit request ⇒ - alertSrv.create(request.body) + alertSrv.create(request.body + .unset("lastSyncDate") + .unset("case") + .unset("status") + .unset("follow")) .map(alert ⇒ renderer.toOutput(CREATED, alert)) } diff --git a/thehive-backend/app/models/Alert.scala b/thehive-backend/app/models/Alert.scala index b6cb770234..249a1ebb18 100644 --- a/thehive-backend/app/models/Alert.scala +++ b/thehive-backend/app/models/Alert.scala @@ -95,7 +95,7 @@ class AlertModel @Inject() (dblists: DBLists) val sourceRef = (attrs \ "sourceRef").asOpt[String].getOrElse("") val _id = hasher.fromString(s"$tpe|$source|$sourceRef").head.toString() attrs + ("_id" → JsString(_id)) - } - "lastSyncDate" - "case" - "status" - "follow" + } } } } diff --git a/thehive-misp/app/connectors/misp/JsonFormat.scala b/thehive-misp/app/connectors/misp/JsonFormat.scala index 5a27bb80d5..17dccc260e 100644 --- a/thehive-misp/app/connectors/misp/JsonFormat.scala +++ b/thehive-misp/app/connectors/misp/JsonFormat.scala @@ -65,4 +65,13 @@ object JsonFormat { comment, value, tags :+ s"MISP:category$category" :+ s"MISP:type=$tpe")) + + implicit val exportedAttributeWrites: Writes[ExportedMispAttribute] = Writes[ExportedMispAttribute] { attribute ⇒ + Json.obj( + "category" → attribute.category, + "type" → attribute.tpe, + "value" → attribute.value.fold[String](identity, _.name), + "comment" → attribute.comment, + "status" → attribute.status) + } } diff --git a/thehive-misp/app/connectors/misp/MispCtrl.scala b/thehive-misp/app/connectors/misp/MispCtrl.scala index b1f3c9179b..6c58b7ecf8 100644 --- a/thehive-misp/app/connectors/misp/MispCtrl.scala +++ b/thehive-misp/app/connectors/misp/MispCtrl.scala @@ -15,13 +15,15 @@ import play.api.libs.json.Json import play.api.mvc.{ Action, AnyContent, Controller } import play.api.routing.SimpleRouter import play.api.routing.sird.{ GET, UrlContext } -import services.AlertTransformer +import services.{ AlertTransformer, CaseSrv } +import connectors.misp.JsonFormat.exportedAttributeWrites import scala.concurrent.{ ExecutionContext, Future } @Singleton class MispCtrl @Inject() ( mispSrv: MispSrv, + caseSrv: CaseSrv, authenticated: Authenticated, eventSrv: EventSrv, implicit val ec: ExecutionContext) extends Controller with Connector with Status with AlertTransformer { @@ -30,10 +32,11 @@ class MispCtrl @Inject() ( private[MispCtrl] lazy val logger = Logger(getClass) val router = SimpleRouter { - case GET(p"/_syncAlerts") ⇒ syncAlerts - case GET(p"/_syncAllAlerts") ⇒ syncAllAlerts - case GET(p"/_syncArtifacts") ⇒ syncArtifacts - case r ⇒ throw NotFoundError(s"${r.uri} not found") + case GET(p"/_syncAlerts") ⇒ syncAlerts + case GET(p"/_syncAllAlerts") ⇒ syncAllAlerts + case GET(p"/_syncArtifacts") ⇒ syncArtifacts + case GET(p"/export/$caseId/to/$mispName") ⇒ exportCase(mispName, caseId) + case r ⇒ throw NotFoundError(s"${r.uri} not found") } @Timed @@ -54,6 +57,18 @@ class MispCtrl @Inject() ( Ok("") } + @Timed + def exportCase(mispName: String, caseId: String): Action[AnyContent] = authenticated(Role.write).async { implicit request ⇒ + caseSrv + .get(caseId) + .flatMap { caze ⇒ mispSrv.export(mispName, caze) } + .map { + case (eventId, exportedAttributes) ⇒ Ok(Json.obj( + "eventId" → eventId, + "attributes" → exportedAttributes)) + } + } + override def createCase(alert: Alert, customCaseTemplate: Option[String])(implicit authContext: AuthContext): Future[Case] = { mispSrv.createCase(alert, customCaseTemplate) } diff --git a/thehive-misp/app/connectors/misp/MispSrv.scala b/thehive-misp/app/connectors/misp/MispSrv.scala index 6a5f4636d4..37a25e8cd4 100644 --- a/thehive-misp/app/connectors/misp/MispSrv.scala +++ b/thehive-misp/app/connectors/misp/MispSrv.scala @@ -16,7 +16,7 @@ import net.lingala.zip4j.model.FileHeader import org.elastic4play.controllers.{ Fields, FileInputValue } import org.elastic4play.services.{ UserSrv ⇒ _, _ } import org.elastic4play.utils.RichJson -import org.elastic4play.{ InternalError, NotFoundError } +import org.elastic4play.{ ConflictError, InternalError, NotFoundError } import play.api.inject.ApplicationLifecycle import play.api.libs.json.JsLookupResult.jsLookupResultToJsLookup import play.api.libs.json.JsValue.jsValueToJsLookup @@ -24,6 +24,7 @@ import play.api.libs.json.Json.toJsFieldJsValueWrapper import play.api.libs.json._ import play.api.{ Configuration, Environment, Logger } import services._ +import java.text.SimpleDateFormat import scala.collection.immutable import scala.concurrent.duration.{ DurationInt, DurationLong, FiniteDuration } @@ -55,6 +56,8 @@ class MispConfig(val interval: FiniteDuration, val connections: Seq[MispConnecti configuration, configuration.getString("misp.caseTemplate"), httpSrv) + + def getConnection(name: String): Option[MispConnection] = connections.find(_.name == name) } case class MispConnection( @@ -82,6 +85,8 @@ case class MispConnection( } +case class ExportedMispAttribute(tpe: String, category: String, value: Either[String, Attachment], comment: Option[String], status: String) + @Singleton class MispSrv @Inject() ( mispConfig: MispConfig, @@ -170,22 +175,21 @@ class MispSrv @Inject() ( } .flatMapConcat { case (mispConnection, lastSyncDate) ⇒ - synchronize(mispConnection, lastSyncDate) + synchronize(mispConnection, Some(lastSyncDate)) } .runWith(Sink.seq) } def fullSynchronize()(implicit authContext: AuthContext): Future[immutable.Seq[Try[Alert]]] = { Source(mispConfig.connections.toList) - .flatMapConcat(mispConnection ⇒ synchronize(mispConnection, new Date(1))) + .flatMapConcat(mispConnection ⇒ synchronize(mispConnection, None)) .runWith(Sink.seq) } - def synchronize(mispConnection: MispConnection, lastSyncDate: Date)(implicit authContext: AuthContext): Source[Try[Alert], NotUsed] = { + def synchronize(mispConnection: MispConnection, lastSyncDate: Option[Date])(implicit authContext: AuthContext): Source[Try[Alert], NotUsed] = { logger.info(s"Synchronize MISP ${mispConnection.name} from $lastSyncDate") - val fullSynchro = if (lastSyncDate.getTime == 1) Some(lastSyncDate) else None // get events that have been published after the last synchronization - getEventsFromDate(mispConnection, lastSyncDate) + getEventsFromDate(mispConnection, lastSyncDate.getOrElse(new Date(0))) // get related alert .mapAsyncUnordered(1) { event ⇒ logger.trace(s"Looking for alert misp:${event.source}:${event.sourceRef}") @@ -196,7 +200,7 @@ class MispSrv @Inject() ( case (event, alert) ⇒ logger.trace(s"MISP synchro ${mispConnection.name}, event ${event.sourceRef}, alert ${alert.fold("no alert")(a ⇒ "alert " + a.alertId() + "last sync at " + a.lastSyncDate())}") logger.info(s"getting MISP event ${event.source}:${event.sourceRef}") - getAttributes(mispConnection, event.sourceRef, fullSynchro.orElse(alert.map(_.lastSyncDate()))) + getAttributes(mispConnection, event.sourceRef, lastSyncDate.flatMap(_ ⇒ alert.map(_.lastSyncDate()))) .map((event, alert, _)) } .mapAsyncUnordered(1) { @@ -211,8 +215,8 @@ class MispSrv @Inject() ( .map(Success(_)) .recover { case t ⇒ Failure(t) } - // if a related alert exists, update it - case (event, Some(alert), attrs) ⇒ + // if a related alert exists and we follow it or a fullSync, update it + case (event, Some(alert), attrs) if alert.follow() || lastSyncDate.isEmpty ⇒ logger.info(s"MISP event ${event.source}:${event.sourceRef} has related alert, update it with ${attrs.size} observable(s)") val alertJson = Json.toJson(event).as[JsObject] - "type" - @@ -221,8 +225,8 @@ class MispSrv @Inject() ( "caseTemplate" - "date" + ("artifacts" → JsArray(attrs)) + - // if this is a full synchronization, don't update alert status - ("status" → (if (!alert.follow() || fullSynchro.isDefined) Json.toJson(alert.status()) + // if this is a full synchronization, don't update alert status + ("status" → (if (lastSyncDate.isEmpty) Json.toJson(alert.status()) else alert.status() match { case AlertStatus.New ⇒ Json.toJson(AlertStatus.New) case _ ⇒ Json.toJson(AlertStatus.Updated) @@ -236,7 +240,7 @@ class MispSrv @Inject() ( for { a ← fAlert // if this is a full synchronization, don't update case status - caseFields = if (fullSynchro.isDefined) Fields(alert.toCaseJson).unset("status") + caseFields = if (lastSyncDate.isEmpty) Fields(alert.toCaseJson).unset("status") else Fields(alert.toCaseJson) _ ← caseSrv.update(caze, caseFields) _ ← artifactSrv.create(caze, attrs.map(Fields.apply)) @@ -244,6 +248,10 @@ class MispSrv @Inject() ( }) .map(Success(_)) .recover { case t ⇒ Failure(t) } + + // if the alert is not followed, do nothing + case (_, Some(alert), _) ⇒ + Future.successful(Success(alert)) } } @@ -510,26 +518,178 @@ class MispSrv @Inject() ( } } + def exportStatus(caseId: String): Future[Seq[(String, String)]] = { + import org.elastic4play.services.QueryDSL._ + alertSrv.find(and("type" ~= "misp", "case" ~= caseId), Some("all"), Nil) + ._1 + .map { alert ⇒ + alert.source() → alert.sourceRef() + } + .runWith(Sink.seq) + } + + def export(mispName: String, caze: Case)(implicit authContext: AuthContext): Future[(String, Seq[ExportedMispAttribute])] = { + val mispConnection = mispConfig.getConnection(mispName).getOrElse(sys.error("MISP instance not found")) + val dateFormat = new SimpleDateFormat("yy-MM-dd") + + def buildAttributeList(): Future[Seq[ExportedMispAttribute]] = { + import org.elastic4play.services.QueryDSL._ + artifactSrv + .find(parent("case", withId(caze.id)), Some("all"), Nil) + ._1 + .map { artifact ⇒ + val (category, tpe) = artifact2attribute(artifact.dataType(), artifact.data()) + val value = (artifact.data(), artifact.attachment()) match { + case (Some(data), None) ⇒ Left(data) + case (None, Some(attachment)) ⇒ Right(attachment) + case _ ⇒ sys.error("???") + } + ExportedMispAttribute(tpe, category, value, artifact.message(), "") + } + .runWith(Sink.seq) + } + + def extractDuplicateAttributes(attributes: Seq[ExportedMispAttribute]): (Seq[ExportedMispAttribute], Seq[ExportedMispAttribute]) = { + val attrIndex = attributes.zipWithIndex + + val (duplicateAttributes, uniqueAttributes) = attrIndex.partition { + case (ExportedMispAttribute(category, tpe, value, _, _), index) ⇒ attrIndex.exists { + case (ExportedMispAttribute(`category`, `tpe`, `value`, _, _), otherIndex) ⇒ otherIndex < index + case _ ⇒ false + } + } + (duplicateAttributes.map(_._1.copy(status = "Duplicate")), uniqueAttributes.map(_._1)) + } + + def updateStatus(response: JsValue, attributes: Seq[ExportedMispAttribute]): Seq[ExportedMispAttribute] = { + val messages = (response \ "errors" \ "Attribute") + .asOpt[JsObject] + .getOrElse(JsObject(Nil)) + .fields + .toMap + .mapValues { m ⇒ + (m \ "value") + .asOpt[Seq[String]] + .flatMap(_.headOption) + .getOrElse(s"Unexpected message format: $m") + } + + attributes.zipWithIndex.map { + case (attr, index) ⇒ attr.copy(status = messages.getOrElse(index.toString, "Ok")) + } + } + + def exportFileAttribute(eventId: String, attribute: ExportedMispAttribute): Future[ExportedMispAttribute] = attribute match { + case ExportedMispAttribute(_, _, Right(attachment), comment, _) ⇒ + attachmentSrv + .source(attachment.id) + .runReduce(_ ++ _) + .flatMap { data ⇒ + val b64data = java.util.Base64.getEncoder.encodeToString(data.toArray[Byte]) + mispConnection("events/upload_sample").post(Json.obj( + "request" → Json.obj( + "event_id" → eventId.toInt, // FIXME add checks + "category" → "Payload delivery", + "type" → "malware-sample", + "comment" → comment, + "files" → Json.arr( + Json.obj( + "filename" → attachment.name, + "data" → b64data))))) + } + .map { response ⇒ + val name = (response.json \ "name") + .asOpt[String] + val status = name + .filter(_ == "Success") + .map(_ ⇒ "Ok") + .orElse((response.json \ "message").asOpt[String]) + .orElse(name) + .getOrElse(s"Unexpected MISP response: ${response.status} ${response.statusText}\n${response.body}") + attribute.copy(status = status, value = Left(attachment.name)) + } + } + + for { + exportStatus ← exportStatus(caze.id) + _ = println(s"exporting ${caze.id} in $mispName") + _ = println(s"exportStatus=$exportStatus") + _ ← exportStatus.find(_._1 == mispName).fold(Future.successful(()))(es ⇒ Future.failed(ConflictError(s"Case ${caze.id} has already been exported in MISP ${es._1}#${es._2}", JsObject(Nil)))) + attributes ← buildAttributeList() + (duplicateAttributes, uniqueAttributes) = extractDuplicateAttributes(attributes) + (simpleAttributes, fileAttributes) = uniqueAttributes.partition(_.value.isLeft) + simpleAttributeJson = simpleAttributes.map { attribute ⇒ + Json.obj( + "type" → attribute.tpe, + "category" → attribute.category, + "value" → attribute.value.left.get, + "comment" → attribute.comment) + } + + mispEvent = Json.obj( + "Event" → Json.obj( + "distribution" → 0, + "threat_level_id" → caze.severity().toString, + "analysis" → 0, + "info" → caze.title(), + "date" → dateFormat.format(caze.startDate()), + "published" → false, + "Attribute" → simpleAttributeJson + // "Tag" → tags + // "orgc_id" + // "org_id" + // "sharing_group_id" + )) + simpleAttributeResponse ← mispConnection("events").post(mispEvent) + eventId ← (simpleAttributeResponse.json \ "Event" \ "id") + .asOpt[String] + .fold[Future[String]](Future.failed(InternalError(s"Unexpected MISP response: ${simpleAttributeResponse.status} ${simpleAttributeResponse.statusText}\n${simpleAttributeResponse.body}")))(Future.successful) + fileAttributeStatus ← Future.traverse(fileAttributes)(attr ⇒ exportFileAttribute(eventId, attr)) + _ ← alertSrv.create(Fields.empty + .set("type", "misp") + .set("source", mispName) + .set("sourceRef", eventId) + .set("date", Json.toJson(caze.startDate())) + .set("lastSyncDate", Json.toJson(new Date(0))) + .set("case", caze.id) + .set("title", caze.title()) + .set("description", "Case have been exported to MISP") + .set("severity", JsNumber(caze.severity())) + .set("tags", Json.toJson(caze.tags())) + .set("tlp", JsNumber(caze.tlp())) + .set("artifacts", JsArray()) + .set("status", "Imported") + .set("follow", JsBoolean(false))) + exportedAttributes = updateStatus(simpleAttributeResponse.json, simpleAttributes) ++ duplicateAttributes ++ fileAttributeStatus + } yield eventId → exportedAttributes + } + def convertAttribute(mispAttribute: MispAttribute): Seq[JsObject] = { - val dataType = typeLookup.getOrElse(mispAttribute.tpe, "other") + val dataType = attribute2artifact(mispAttribute.tpe) val fields = Json.obj( "data" → mispAttribute.value, "dataType" → dataType, "message" → mispAttribute.comment, "startDate" → mispAttribute.date, - "tags" → Json.arr(s"MISP:type=${mispAttribute.tpe}", s"MISP:category=${mispAttribute.category}")) + "tags" → Json.arr(s"MISP:type=${ + mispAttribute.tpe + }", s"MISP:category=${ + mispAttribute.category + }")) val types = mispAttribute.tpe.split('|').toSeq if (types.length > 1) { val values = mispAttribute.value.split('|').toSeq val typesValues = types.zipAll(values, "noType", "noValue") val additionnalMessage = typesValues - .map { case (t, v) ⇒ s"$t: $v" } + .map { + case (t, v) ⇒ s"$t: $v" + } .mkString("\n") typesValues.map { case (tpe, value) ⇒ fields + - ("dataType" → JsString(typeLookup.getOrElse(tpe, "other"))) + + ("dataType" → JsString(attribute2artifact(tpe))) + ("data" → JsString(value)) + ("message" → JsString(mispAttribute.comment + "\n" + additionnalMessage)) } @@ -539,7 +699,37 @@ class MispSrv @Inject() ( } } - private val typeLookup = Map( + private def artifact2attribute(dataType: String, data: Option[String]): (String, String) = { + dataType match { + case "filename" ⇒ "Payload delivery" → "filename" + case "fqdn" ⇒ "Network activity" → "hostname" + case "url" ⇒ "External analysis" → "url" + case "user-agent" ⇒ "Network activity" → "user-agent" + case "domain" ⇒ "Network activity" → "domain" + case "ip" ⇒ "Network activity" → "ip-src" + case "mail_subject" ⇒ "Payload delivery" → "email-subject" + case "hash" ⇒ data.fold(0)(_.length) match { + case 32 ⇒ "Payload delivery" → "md5" + case 40 ⇒ "Payload delivery" → "sha1" + case 64 ⇒ "Payload delivery" → "sha256" + case 56 ⇒ "Payload delivery" → "sha224" + case 71 ⇒ "Payload delivery" → "sha384" + case 128 ⇒ "Payload delivery" → "sha512" + case _ ⇒ "Payload delivery" → "other" + } + case "mail" ⇒ "Payload delivery" → "email-src" + case "registry" ⇒ "Persistence mechanism" → "regkey" + case "uri_path" ⇒ "Network activity" → "uri" + case "regexp" ⇒ "Other" → "other" + case "other" ⇒ "Other" → "other" + case "file" ⇒ "Payload delivery" → "malware-sample" + case _ ⇒ "Other" → "other" + } + } + + private def attribute2artifact(tpe: String): String = attribute2artifactLookup.getOrElse(tpe, "other") + + private lazy val attribute2artifactLookup = Map( "md5" → "hash", "sha1" → "hash", "sha256" → "hash", From 47bfae940b53ba12d337517894f9bef090e6335d Mon Sep 17 00:00:00 2001 From: To-om Date: Thu, 24 Aug 2017 09:38:46 +0200 Subject: [PATCH 03/49] #275 Add Support for Play 2.6.x and Elasticsearch 5.x --- project/BuildSettings.scala | 2 +- project/Dependencies.scala | 18 +-- project/build.properties | 2 +- project/plugins.sbt | 2 +- .../app/connectors/Connectors.scala | 18 ++- .../app/controllers/AlertCtrl.scala | 25 ++-- .../app/controllers/ArtifactCtrl.scala | 15 ++- thehive-backend/app/controllers/Asset.scala | 10 +- .../app/controllers/AttachmentCtrl.scala | 41 +++--- .../app/controllers/AuthenticationCtrl.scala | 15 ++- .../app/controllers/CaseCtrl.scala | 25 ++-- .../app/controllers/CaseTemplateCtrl.scala | 14 +- .../app/controllers/FlowCtrl.scala | 11 +- thehive-backend/app/controllers/LogCtrl.scala | 17 ++- .../app/controllers/SearchCtrl.scala | 10 +- .../app/controllers/StatusCtrl.scala | 22 ++-- .../app/controllers/StreamCtrl.scala | 48 +++---- .../app/controllers/TaskCtrl.scala | 16 ++- .../app/controllers/UserCtrl.scala | 28 ++-- thehive-backend/app/global/Filters.scala | 9 +- thehive-backend/app/global/TheHive.scala | 51 ++++---- thehive-backend/app/models/Alert.scala | 14 +- thehive-backend/app/models/Artifact.scala | 36 +++--- thehive-backend/app/models/Audit.scala | 13 +- thehive-backend/app/models/Case.scala | 16 ++- thehive-backend/app/models/CaseTemplate.scala | 4 +- thehive-backend/app/models/JsonFormat.scala | 3 +- thehive-backend/app/models/Log.scala | 7 +- thehive-backend/app/models/Migration.scala | 33 ++--- thehive-backend/app/models/Task.scala | 11 +- thehive-backend/app/models/User.scala | 11 +- thehive-backend/app/services/AlertSrv.scala | 23 ++-- .../app/services/ArtifactSrv.scala | 18 +-- thehive-backend/app/services/AuditSrv.scala | 70 +++++----- .../app/services/CaseMergeSrv.scala | 16 ++- thehive-backend/app/services/CaseSrv.scala | 16 ++- .../app/services/CaseTemplateSrv.scala | 7 +- .../app/services/CustomWSAPI.scala | 73 ++++++----- thehive-backend/app/services/FlowSrv.scala | 18 +-- .../app/services/LocalAuthSrv.scala | 7 +- thehive-backend/app/services/LogSrv.scala | 11 +- .../app/services/StreamMessage.scala | 13 +- thehive-backend/app/services/StreamSrv.scala | 50 ++++---- thehive-backend/app/services/TaskSrv.scala | 9 +- thehive-backend/app/services/UserSrv.scala | 10 +- thehive-backend/build.sbt | 4 +- .../connectors/cortex/CortexConnector.scala | 13 +- .../{CortextCtrl.scala => CortexCtrl.scala} | 16 ++- .../controllers/ReportTemplateCtrl.scala | 19 +-- .../cortex/models/ReportTemplate.scala | 1 - .../cortex/services/CortexClient.scala | 5 +- .../cortex/services/CortexSrv.scala | 66 ++++++---- .../cortex/services/ReportTemplateSrv.scala | 2 +- thehive-cortex/build.sbt | 2 + .../app/connectors/metrics/Influxdb.scala | 25 ++-- .../app/connectors/metrics/MetricsCtrl.scala | 11 +- .../connectors/metrics/MetricsModule.scala | 121 ++++++++++-------- .../app/connectors/misp/MispConnector.scala | 10 +- .../app/connectors/misp/MispCtrl.scala | 8 +- .../app/connectors/misp/MispSrv.scala | 114 ++++++++++------- thehive-misp/build.sbt | 2 + 61 files changed, 725 insertions(+), 582 deletions(-) rename thehive-cortex/app/connectors/cortex/controllers/{CortextCtrl.scala => CortexCtrl.scala} (92%) diff --git a/project/BuildSettings.scala b/project/BuildSettings.scala index 5fc2ab1da7..7629188722 100644 --- a/project/BuildSettings.scala +++ b/project/BuildSettings.scala @@ -13,7 +13,7 @@ object BasicSettings extends AutoPlugin { "-deprecation", // Emit warning and location for usages of deprecated APIs. "-feature", // Emit warning and location for usages of features that should be imported explicitly. "-unchecked", // Enable additional warnings where generated code depends on assumptions. - "-Xfatal-warnings", // Fail the compilation if there are any warnings. + //"-Xfatal-warnings", // Fail the compilation if there are any warnings. "-Xlint", // Enable recommended additional warnings. "-Ywarn-adapted-args", // Warn if an argument list is modified to match the receiver. "-Ywarn-dead-code", // Warn when dead code is identified. diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 129c4e93f0..3a615b1dcc 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -1,17 +1,19 @@ import sbt._ object Dependencies { - val scalaVersion = "2.11.8" + val scalaVersion = "2.12.3" object Library { object Play { val version = play.core.PlayVersion.current val ws = "com.typesafe.play" %% "play-ws" % version - val cache = "com.typesafe.play" %% "play-cache" % version + val ahc = "com.typesafe.play" %% "play-ahc-ws" % version + val cache = "com.typesafe.play" %% "play-ehcache" % version val test = "com.typesafe.play" %% "play-test" % version val specs2 = "com.typesafe.play" %% "play-specs2" % version val filters = "com.typesafe.play" %% "filters-helpers" % version + val guice = "com.typesafe.play" %% "play-guice" % version object Specs2 { private val version = "3.6.6" val matcherExtra = "org.specs2" %% "specs2-matcher-extra" % version @@ -20,16 +22,16 @@ object Dependencies { } object Specs2 { - private val version = "3.6.6" + private val version = "3.9.4" val core = "org.specs2" %% "specs2-core" % version val matcherExtra = "org.specs2" %% "specs2-matcher-extra" % version val mock = "org.specs2" %% "specs2-mock" % version } - val scalaGuice = "net.codingwell" %% "scala-guice" % "4.0.1" - val akkaTestkit = "com.typesafe.akka" %% "akka-testkit" % "2.4.7" - val reflections = "org.reflections" % "reflections" % "0.9.10" + val scalaGuice = "net.codingwell" %% "scala-guice" % "4.1.0" + val akkaTestkit = "com.typesafe.akka" %% "akka-testkit" % "2.5.4" + val reflections = "org.reflections" % "reflections" % "0.9.11" val zip4j = "net.lingala.zip4j" % "zip4j" % "1.3.2" - val akkaTest = "com.typesafe.akka" %% "akka-stream-testkit" % "2.4.4" - val elastic4play = "org.cert-bdf" %% "elastic4play" % "1.2.1" + val akkaTest = "com.typesafe.akka" %% "akka-stream-testkit" % "2.5.4" + val elastic4play = "org.cert-bdf" %% "elastic4play" % "1.3-SNAPSHOT" } } diff --git a/project/build.properties b/project/build.properties index 27e88aa115..c091b86ca4 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=0.13.13 +sbt.version=0.13.16 diff --git a/project/plugins.sbt b/project/plugins.sbt index bd976428ba..b0115921fe 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,7 +1,7 @@ // Comment to get more information during initialization logLevel := Level.Info -addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.5.14") +addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.3") addSbtPlugin("me.lessis" % "bintray-sbt" % "0.3.0") diff --git a/thehive-backend/app/connectors/Connectors.scala b/thehive-backend/app/connectors/Connectors.scala index 929fdf1269..df5e0ff218 100644 --- a/thehive-backend/app/connectors/Connectors.scala +++ b/thehive-backend/app/connectors/Connectors.scala @@ -1,15 +1,16 @@ package connectors -import javax.inject.Inject +import javax.inject.{ Inject, Singleton } + +import scala.collection.immutable -import com.google.inject.AbstractModule -import net.codingwell.scalaguice.{ ScalaModule, ScalaMultibinder } import play.api.libs.json.{ JsObject, Json } -import play.api.mvc.{ Action, Handler, RequestHeader, Results } +import play.api.mvc._ import play.api.routing.sird.UrlContext import play.api.routing.{ Router, SimpleRouter } -import scala.collection.immutable +import com.google.inject.AbstractModule +import net.codingwell.scalaguice.{ ScalaModule, ScalaMultibinder } trait Connector { val name: String @@ -17,14 +18,17 @@ trait Connector { val status: JsObject = Json.obj("enabled" → true) } -class ConnectorRouter @Inject() (connectors: immutable.Set[Connector]) extends SimpleRouter { +@Singleton +class ConnectorRouter @Inject() ( + connectors: immutable.Set[Connector], + actionBuilder: DefaultActionBuilder) extends SimpleRouter { def get(connectorName: String): Option[Connector] = connectors.find(_.name == connectorName) def routes: PartialFunction[RequestHeader, Handler] = { case request @ p"/$connector/$path<.*>" ⇒ get(connector) .flatMap(_.router.withPrefix(s"/$connector/").handlerFor(request)) - .getOrElse(Action { _ ⇒ Results.NotFound(s"connector $connector not found") }) + .getOrElse(actionBuilder { _ ⇒ Results.NotFound(s"connector $connector not found") }) } } diff --git a/thehive-backend/app/controllers/AlertCtrl.scala b/thehive-backend/app/controllers/AlertCtrl.scala index f90dff0be8..8783eed490 100644 --- a/thehive-backend/app/controllers/AlertCtrl.scala +++ b/thehive-backend/app/controllers/AlertCtrl.scala @@ -2,21 +2,23 @@ package controllers import javax.inject.{ Inject, Singleton } +import scala.concurrent.{ ExecutionContext, Future } +import scala.util.Try + +import play.api.Logger +import play.api.http.Status +import play.api.libs.json.{ JsArray, JsObject, Json } +import play.api.mvc._ + import akka.stream.Materializer +import services.JsonFormat.caseSimilarityWrites +import services.{ AlertSrv, CaseSrv } + import org.elastic4play.controllers.{ Authenticated, Fields, FieldsBodyParser, Renderer } import org.elastic4play.models.JsonFormat.baseModelEntityWrites import org.elastic4play.services.JsonFormat.{ aggReads, queryReads } import org.elastic4play.services._ import org.elastic4play.{ BadRequestError, Timed } -import play.api.Logger -import play.api.http.Status -import play.api.libs.json.{ JsArray, JsObject, Json } -import play.api.mvc.{ Action, AnyContent, Controller } -import services.{ AlertSrv, CaseSrv } -import services.JsonFormat.caseSimilarityWrites - -import scala.concurrent.{ ExecutionContext, Future } -import scala.util.Try @Singleton class AlertCtrl @Inject() ( @@ -25,11 +27,12 @@ class AlertCtrl @Inject() ( auxSrv: AuxSrv, authenticated: Authenticated, renderer: Renderer, + components: ControllerComponents, fieldsBodyParser: FieldsBodyParser, implicit val ec: ExecutionContext, - implicit val mat: Materializer) extends Controller with Status { + implicit val mat: Materializer) extends AbstractController(components) with Status { - val log = Logger(getClass) + private[AlertCtrl] lazy val logger = Logger(getClass) @Timed def create(): Action[Fields] = authenticated(Role.write).async(fieldsBodyParser) { implicit request ⇒ diff --git a/thehive-backend/app/controllers/ArtifactCtrl.scala b/thehive-backend/app/controllers/ArtifactCtrl.scala index f6ac842c85..9f8873a0e8 100644 --- a/thehive-backend/app/controllers/ArtifactCtrl.scala +++ b/thehive-backend/app/controllers/ArtifactCtrl.scala @@ -3,16 +3,18 @@ package controllers import javax.inject.{ Inject, Singleton } import scala.concurrent.{ ExecutionContext, Future } + import play.api.http.Status import play.api.libs.json.JsArray -import play.api.mvc.{ Action, AnyContent, Controller } -import org.elastic4play.{ BadRequestError, Timed } +import play.api.mvc._ + +import services.ArtifactSrv + import org.elastic4play.controllers.{ Authenticated, Fields, FieldsBodyParser, Renderer } import org.elastic4play.models.JsonFormat.baseModelEntityWrites -import org.elastic4play.services.{ Agg, AuxSrv } -import org.elastic4play.services.{ QueryDSL, QueryDef, Role } import org.elastic4play.services.JsonFormat.{ aggReads, queryReads } -import services.ArtifactSrv +import org.elastic4play.services._ +import org.elastic4play.{ BadRequestError, Timed } @Singleton class ArtifactCtrl @Inject() ( @@ -20,8 +22,9 @@ class ArtifactCtrl @Inject() ( auxSrv: AuxSrv, authenticated: Authenticated, renderer: Renderer, + components: ControllerComponents, fieldsBodyParser: FieldsBodyParser, - implicit val ec: ExecutionContext) extends Controller with Status { + implicit val ec: ExecutionContext) extends AbstractController(components) with Status { @Timed def create(caseId: String): Action[Fields] = authenticated(Role.write).async(fieldsBodyParser) { implicit request ⇒ diff --git a/thehive-backend/app/controllers/Asset.scala b/thehive-backend/app/controllers/Asset.scala index 5ca4ddcd35..f2bfb27616 100644 --- a/thehive-backend/app/controllers/Asset.scala +++ b/thehive-backend/app/controllers/Asset.scala @@ -2,21 +2,23 @@ package controllers import javax.inject.{ Inject, Singleton } +import scala.concurrent.ExecutionContext + import play.api.Environment -import play.api.http.HttpErrorHandler -import play.api.mvc.{ Action, AnyContent, Controller } +import play.api.http.{ FileMimeTypes, HttpErrorHandler } +import play.api.mvc.{ Action, AnyContent } trait AssetCtrl { def get(file: String): Action[AnyContent] } @Singleton -class AssetCtrlProd @Inject() (errorHandler: HttpErrorHandler) extends Assets(errorHandler) with AssetCtrl { +class AssetCtrlProd @Inject() (errorHandler: HttpErrorHandler, meta: AssetsMetadata) extends Assets(errorHandler, meta) with AssetCtrl { def get(file: String): Action[AnyContent] = at("/ui", file) } @Singleton -class AssetCtrlDev @Inject() (environment: Environment) extends ExternalAssets(environment) with AssetCtrl { +class AssetCtrlDev @Inject() (environment: Environment)(implicit ec: ExecutionContext, fileMimeTypes: FileMimeTypes) extends ExternalAssets(environment) with AssetCtrl { def get(file: String): Action[AnyContent] = { if (file.startsWith("bower_components/")) { at("ui", file) diff --git a/thehive-backend/app/controllers/AttachmentCtrl.scala b/thehive-backend/app/controllers/AttachmentCtrl.scala index 7633d6680a..2b9b9faae1 100644 --- a/thehive-backend/app/controllers/AttachmentCtrl.scala +++ b/thehive-backend/app/controllers/AttachmentCtrl.scala @@ -1,21 +1,22 @@ package controllers +import java.nio.file.Files import javax.inject.{ Inject, Singleton } -import akka.stream.scaladsl.FileIO -import play.api.Configuration import play.api.http.HttpEntity -import play.api.libs.Files.TemporaryFile +import play.api.libs.Files.DefaultTemporaryFileCreator import play.api.mvc._ +import play.api.{ Configuration, mvc } + +import akka.stream.scaladsl.FileIO import net.lingala.zip4j.core.ZipFile import net.lingala.zip4j.model.ZipParameters import net.lingala.zip4j.util.Zip4jConstants + import org.elastic4play.Timed -import org.elastic4play.services.{ AttachmentSrv, Role } -import org.elastic4play.models.AttachmentAttributeFormat +import org.elastic4play.controllers.{ Authenticated, Renderer } import org.elastic4play.models.AttachmentAttributeFormat -import org.elastic4play.controllers.Authenticated -import org.elastic4play.controllers.Renderer +import org.elastic4play.services.{ AttachmentSrv, Role } /** * Controller used to access stored attachments (plain or zipped) @@ -23,19 +24,25 @@ import org.elastic4play.controllers.Renderer @Singleton class AttachmentCtrl( password: String, + tempFileCreator: DefaultTemporaryFileCreator, attachmentSrv: AttachmentSrv, authenticated: Authenticated, - renderer: Renderer) extends Controller { + components: ControllerComponents, + renderer: Renderer) extends AbstractController(components) { @Inject() def this( configuration: Configuration, + tempFileCreator: DefaultTemporaryFileCreator, attachmentSrv: AttachmentSrv, authenticated: Authenticated, + components: ControllerComponents, renderer: Renderer) = this( - configuration.getString("datastore.attachment.password").get, + configuration.get[String]("datastore.attachment.password"), + tempFileCreator, attachmentSrv, authenticated, + components, renderer) /** @@ -47,8 +54,8 @@ class AttachmentCtrl( def download(hash: String, name: Option[String]): Action[AnyContent] = authenticated(Role.read) { implicit request ⇒ if (hash.startsWith("{{")) // angularjs hack NoContent - else if (!name.getOrElse("").intersect(AttachmentAttributeFormat.forbiddenChar).isEmpty()) - BadRequest("File name is invalid") + else if (!name.getOrElse("").intersect(AttachmentAttributeFormat.forbiddenChar).isEmpty) + mvc.Results.BadRequest("File name is invalid") else Result( header = ResponseHeader( @@ -66,12 +73,12 @@ class AttachmentCtrl( */ @Timed("controllers.AttachmentCtrl.downloadZip") def downloadZip(hash: String, name: Option[String]): Action[AnyContent] = authenticated(Role.read) { implicit request ⇒ - if (!name.getOrElse("").intersect(AttachmentAttributeFormat.forbiddenChar).isEmpty()) + if (!name.getOrElse("").intersect(AttachmentAttributeFormat.forbiddenChar).isEmpty) BadRequest("File name is invalid") else { - val f = TemporaryFile("zip", hash).file - f.delete() - val zipFile = new ZipFile(f) + val f = tempFileCreator.create("zip", hash).path + Files.delete(f) + val zipFile = new ZipFile(f.toFile) val zipParams = new ZipParameters zipParams.setCompressionLevel(Zip4jConstants.DEFLATE_LEVEL_FASTEST) zipParams.setEncryptFiles(true) @@ -88,8 +95,8 @@ class AttachmentCtrl( "Content-Disposition" → s"""attachment; filename="${name.getOrElse(hash)}.zip"""", "Content-Type" → "application/zip", "Content-Transfer-Encoding" → "binary", - "Content-Length" → f.length.toString)), - body = HttpEntity.Streamed(FileIO.fromPath(f.toPath), Some(f.length), Some("application/zip"))) + "Content-Length" → Files.size(f).toString)), + body = HttpEntity.Streamed(FileIO.fromPath(f), Some(Files.size(f)), Some("application/zip"))) } } } \ No newline at end of file diff --git a/thehive-backend/app/controllers/AuthenticationCtrl.scala b/thehive-backend/app/controllers/AuthenticationCtrl.scala index 7a0a360026..24c8261ac9 100644 --- a/thehive-backend/app/controllers/AuthenticationCtrl.scala +++ b/thehive-backend/app/controllers/AuthenticationCtrl.scala @@ -2,15 +2,17 @@ package controllers import javax.inject.{ Inject, Singleton } +import scala.concurrent.{ ExecutionContext, Future } + +import play.api.mvc._ + import models.UserStatus -import org.elastic4play.{ AuthorizationError, Timed } +import services.UserSrv + import org.elastic4play.controllers.{ Authenticated, Fields, FieldsBodyParser, Renderer } import org.elastic4play.database.DBIndex import org.elastic4play.services.AuthSrv -import play.api.mvc.{ Action, Controller, Results } -import services.UserSrv - -import scala.concurrent.{ ExecutionContext, Future } +import org.elastic4play.{ AuthorizationError, Timed } @Singleton class AuthenticationCtrl @Inject() ( @@ -19,8 +21,9 @@ class AuthenticationCtrl @Inject() ( authenticated: Authenticated, dbIndex: DBIndex, renderer: Renderer, + components: ControllerComponents, fieldsBodyParser: FieldsBodyParser, - implicit val ec: ExecutionContext) extends Controller { + implicit val ec: ExecutionContext) extends AbstractController(components) { @Timed def login: Action[Fields] = Action.async(fieldsBodyParser) { implicit request ⇒ diff --git a/thehive-backend/app/controllers/CaseCtrl.scala b/thehive-backend/app/controllers/CaseCtrl.scala index 818d3219be..791144947f 100644 --- a/thehive-backend/app/controllers/CaseCtrl.scala +++ b/thehive-backend/app/controllers/CaseCtrl.scala @@ -2,22 +2,24 @@ package controllers import javax.inject.{ Inject, Singleton } +import scala.concurrent.{ ExecutionContext, Future } +import scala.util.Try + +import play.api.Logger +import play.api.http.Status +import play.api.libs.json.{ JsArray, JsObject, Json } +import play.api.mvc._ + import akka.stream.Materializer import akka.stream.scaladsl.Sink import models.CaseStatus +import services.{ CaseMergeSrv, CaseSrv, CaseTemplateSrv, TaskSrv } + import org.elastic4play.controllers.{ Authenticated, Fields, FieldsBodyParser, Renderer } import org.elastic4play.models.JsonFormat.baseModelEntityWrites import org.elastic4play.services.JsonFormat.{ aggReads, queryReads } import org.elastic4play.services._ import org.elastic4play.{ BadRequestError, Timed } -import play.api.Logger -import play.api.http.Status -import play.api.libs.json.{ JsArray, JsObject, Json } -import play.api.mvc.{ Action, AnyContent, Controller } -import services.{ CaseMergeSrv, CaseSrv, CaseTemplateSrv, TaskSrv } - -import scala.concurrent.{ ExecutionContext, Future } -import scala.util.Try @Singleton class CaseCtrl @Inject() ( @@ -28,11 +30,12 @@ class CaseCtrl @Inject() ( auxSrv: AuxSrv, authenticated: Authenticated, renderer: Renderer, + components: ControllerComponents, fieldsBodyParser: FieldsBodyParser, implicit val ec: ExecutionContext, - implicit val mat: Materializer) extends Controller with Status { + implicit val mat: Materializer) extends AbstractController(components) with Status { - val log = Logger(getClass) + private[CaseCtrl] lazy val logger = Logger(getClass) @Timed def create(): Action[Fields] = authenticated(Role.write).async(fieldsBodyParser) { implicit request ⇒ @@ -70,7 +73,7 @@ class CaseCtrl @Inject() ( for { // Closing the case, so lets close the open tasks caze ← caseSrv.update(id, request.body) - closedTasks ← if (isCaseClosing) taskSrv.closeTasksOfCase(id) else Future.successful(Nil) // FIXME log warning if closedTasks contains errors + _ ← if (isCaseClosing) taskSrv.closeTasksOfCase(id) else Future.successful(Nil) // FIXME log warning if closedTasks contains errors } yield renderer.toOutput(OK, caze) } diff --git a/thehive-backend/app/controllers/CaseTemplateCtrl.scala b/thehive-backend/app/controllers/CaseTemplateCtrl.scala index 35f40abc46..13d8d83b82 100644 --- a/thehive-backend/app/controllers/CaseTemplateCtrl.scala +++ b/thehive-backend/app/controllers/CaseTemplateCtrl.scala @@ -3,16 +3,17 @@ package controllers import javax.inject.{ Inject, Singleton } import scala.concurrent.ExecutionContext -import scala.reflect.runtime.universe + import play.api.http.Status -import play.api.mvc.{ Action, AnyContent, Controller } +import play.api.mvc._ + +import services.CaseTemplateSrv + import org.elastic4play.Timed import org.elastic4play.controllers.{ Authenticated, Fields, FieldsBodyParser, Renderer } import org.elastic4play.models.JsonFormat.baseModelEntityWrites -import org.elastic4play.services.{ QueryDSL, QueryDef, Role } -import org.elastic4play.services.AuxSrv import org.elastic4play.services.JsonFormat.queryReads -import services.CaseTemplateSrv +import org.elastic4play.services.{ AuxSrv, QueryDSL, QueryDef, Role } @Singleton class CaseTemplateCtrl @Inject() ( @@ -20,8 +21,9 @@ class CaseTemplateCtrl @Inject() ( auxSrv: AuxSrv, authenticated: Authenticated, renderer: Renderer, + components: ControllerComponents, fieldsBodyParser: FieldsBodyParser, - implicit val ec: ExecutionContext) extends Controller with Status { + implicit val ec: ExecutionContext) extends AbstractController(components) with Status { @Timed def create: Action[Fields] = authenticated(Role.admin).async(fieldsBodyParser) { implicit request ⇒ diff --git a/thehive-backend/app/controllers/FlowCtrl.scala b/thehive-backend/app/controllers/FlowCtrl.scala index 350c2e991a..92d89ac164 100644 --- a/thehive-backend/app/controllers/FlowCtrl.scala +++ b/thehive-backend/app/controllers/FlowCtrl.scala @@ -2,14 +2,16 @@ package controllers import javax.inject.{ Inject, Singleton } -import scala.annotation.implicitNotFound import scala.concurrent.ExecutionContext + import play.api.http.Status -import play.api.mvc.{ Action, AnyContent, Controller } +import play.api.mvc._ + +import services.FlowSrv + import org.elastic4play.Timed import org.elastic4play.controllers.{ Authenticated, Renderer } import org.elastic4play.services.{ AuxSrv, Role } -import services.FlowSrv @Singleton class FlowCtrl @Inject() ( @@ -17,7 +19,8 @@ class FlowCtrl @Inject() ( auxSrv: AuxSrv, authenticated: Authenticated, renderer: Renderer, - implicit val ec: ExecutionContext) extends Controller with Status { + components: ControllerComponents, + implicit val ec: ExecutionContext) extends AbstractController(components) with Status { /** * Return audit logs. For each item, include ancestor entities diff --git a/thehive-backend/app/controllers/LogCtrl.scala b/thehive-backend/app/controllers/LogCtrl.scala index 6dfffbd7cf..3ddbbe3a7e 100644 --- a/thehive-backend/app/controllers/LogCtrl.scala +++ b/thehive-backend/app/controllers/LogCtrl.scala @@ -2,16 +2,18 @@ package controllers import javax.inject.{ Inject, Singleton } +import scala.concurrent.ExecutionContext + +import play.api.http.Status +import play.api.mvc._ + +import services.LogSrv + import org.elastic4play.Timed import org.elastic4play.controllers.{ Authenticated, Fields, FieldsBodyParser, Renderer } +import org.elastic4play.models.JsonFormat.baseModelEntityWrites import org.elastic4play.services.JsonFormat.queryReads import org.elastic4play.services.{ QueryDSL, QueryDef, Role } -import org.elastic4play.models.JsonFormat.baseModelEntityWrites -import play.api.http.Status -import play.api.mvc.{ Action, AnyContent, Controller } -import services.LogSrv - -import scala.concurrent.ExecutionContext @Singleton class LogCtrl @Inject() ( @@ -19,7 +21,8 @@ class LogCtrl @Inject() ( authenticated: Authenticated, renderer: Renderer, fieldsBodyParser: FieldsBodyParser, - implicit val ec: ExecutionContext) extends Controller with Status { + components: ControllerComponents, + implicit val ec: ExecutionContext) extends AbstractController(components) with Status { @Timed def create(taskId: String): Action[Fields] = authenticated(Role.read).async(fieldsBodyParser) { implicit request ⇒ diff --git a/thehive-backend/app/controllers/SearchCtrl.scala b/thehive-backend/app/controllers/SearchCtrl.scala index dc84d7cb38..539fa90400 100644 --- a/thehive-backend/app/controllers/SearchCtrl.scala +++ b/thehive-backend/app/controllers/SearchCtrl.scala @@ -3,13 +3,14 @@ package controllers import javax.inject.{ Inject, Singleton } import scala.concurrent.ExecutionContext + import play.api.http.Status -import play.api.mvc.{ Action, Controller } +import play.api.mvc.{ AbstractController, Action, ControllerComponents } + import org.elastic4play.Timed import org.elastic4play.controllers.{ Authenticated, Fields, FieldsBodyParser, Renderer } -import org.elastic4play.services.{ AuxSrv, FindSrv } -import org.elastic4play.services.{ QueryDSL, QueryDef, Role } import org.elastic4play.services.JsonFormat.queryReads +import org.elastic4play.services._ @Singleton class SearchCtrl @Inject() ( @@ -17,8 +18,9 @@ class SearchCtrl @Inject() ( auxSrv: AuxSrv, authenticated: Authenticated, renderer: Renderer, + components: ControllerComponents, fieldsBodyParser: FieldsBodyParser, - implicit val ec: ExecutionContext) extends Controller with Status { + implicit val ec: ExecutionContext) extends AbstractController(components) with Status { @Timed def find(): Action[Fields] = authenticated(Role.read).async(fieldsBodyParser) { implicit request ⇒ diff --git a/thehive-backend/app/controllers/StatusCtrl.scala b/thehive-backend/app/controllers/StatusCtrl.scala index 6b2c6874b4..a5cbbc8ab8 100644 --- a/thehive-backend/app/controllers/StatusCtrl.scala +++ b/thehive-backend/app/controllers/StatusCtrl.scala @@ -5,27 +5,23 @@ import javax.inject.{ Inject, Singleton } import scala.collection.immutable import play.api.Configuration -import play.api.libs.json.Json +import play.api.libs.json.{ JsObject, JsString, Json } import play.api.libs.json.Json.toJsFieldJsValueWrapper -import play.api.mvc.{ Action, Controller } - -import org.elastic4play.Timed -import org.elasticsearch.Build +import play.api.mvc.{ AbstractController, ControllerComponents } import com.sksamuel.elastic4s.ElasticDsl - import connectors.Connector -import models.Case -import play.api.libs.json.JsObject -import org.elastic4play.services.auth.MultiAuthSrv -import play.api.libs.json.JsString + +import org.elastic4play.Timed import org.elastic4play.services.AuthSrv +import org.elastic4play.services.auth.MultiAuthSrv @Singleton class StatusCtrl @Inject() ( connectors: immutable.Set[Connector], configuration: Configuration, - authSrv: AuthSrv) extends Controller { + authSrv: AuthSrv, + components: ControllerComponents) extends AbstractController(components) { private[controllers] def getVersion(c: Class[_]) = Option(c.getPackage.getImplementationVersion).getOrElse("SNAPSHOT") @@ -35,12 +31,12 @@ class StatusCtrl @Inject() ( "versions" → Json.obj( "TheHive" → getVersion(classOf[models.Case]), "Elastic4Play" → getVersion(classOf[Timed]), - "Play" → getVersion(classOf[Controller]), + "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.getString("datastore.attachment.password").get, + "protectDownloadsWith" → configuration.get[String]("datastore.attachment.password"), "authType" → (authSrv match { case multiAuthSrv: MultiAuthSrv ⇒ multiAuthSrv.authProviders.map { a ⇒ JsString(a.name) } case _ ⇒ JsString(authSrv.name) diff --git a/thehive-backend/app/controllers/StreamCtrl.scala b/thehive-backend/app/controllers/StreamCtrl.scala index aaa6b06ca1..3ba62fa597 100644 --- a/thehive-backend/app/controllers/StreamCtrl.scala +++ b/thehive-backend/app/controllers/StreamCtrl.scala @@ -2,29 +2,26 @@ package controllers import javax.inject.{ Inject, Singleton } -import scala.annotation.implicitNotFound -import scala.concurrent.ExecutionContext +import scala.collection.immutable +import scala.concurrent.{ ExecutionContext, Future } import scala.concurrent.duration.{ DurationLong, FiniteDuration } -import scala.reflect.runtime.universe import scala.util.Random -import akka.actor.{ ActorSystem, Props } -import akka.pattern.ask -import akka.util.Timeout -import play.api.{ Configuration, Logger } + import play.api.http.Status import play.api.libs.json.Json import play.api.libs.json.Json.toJsFieldJsValueWrapper -import play.api.mvc.{ Action, AnyContent, Controller } -import org.elastic4play.{ AuthenticationError, Timed } -import org.elastic4play.controllers.{ Authenticated, ExpirationError, ExpirationOk, ExpirationWarning, Renderer } -import org.elastic4play.services.{ AuxSrv, EventSrv, Role } +import play.api.mvc._ +import play.api.{ Configuration, Logger } + +import akka.actor.{ ActorSystem, Props } +import akka.pattern.ask +import akka.util.Timeout import services.StreamActor import services.StreamActor.StreamMessages -import akka.actor.ActorPath -import org.elastic4play.services.MigrationSrv -import scala.collection.immutable -import scala.concurrent.Future +import org.elastic4play.controllers._ +import org.elastic4play.services.{ AuxSrv, EventSrv, MigrationSrv, Role } +import org.elastic4play.{ AuthenticationError, Timed } @Singleton class StreamCtrl( @@ -37,8 +34,9 @@ class StreamCtrl( eventSrv: EventSrv, auxSrv: AuxSrv, migrationSrv: MigrationSrv, + components: ControllerComponents, implicit val system: ActorSystem, - implicit val ec: ExecutionContext) extends Controller with Status { + implicit val ec: ExecutionContext) extends AbstractController(components) with Status { @Inject() def this( configuration: Configuration, @@ -47,21 +45,23 @@ class StreamCtrl( eventSrv: EventSrv, auxSrv: AuxSrv, migrationSrv: MigrationSrv, + components: ControllerComponents, system: ActorSystem, ec: ExecutionContext) = this( - configuration.getMilliseconds("stream.longpolling.cache").get.millis, - configuration.getMilliseconds("stream.longpolling.refresh").get.millis, - configuration.getMilliseconds("stream.longpolling.nextItemMaxWait").get.millis, - configuration.getMilliseconds("stream.longpolling.globalMaxWait").get.millis, + configuration.getMillis("stream.longpolling.cache").millis, + configuration.getMillis("stream.longpolling.refresh").millis, + configuration.getMillis("stream.longpolling.nextItemMaxWait").millis, + configuration.getMillis("stream.longpolling.globalMaxWait").millis, authenticated, renderer, eventSrv, auxSrv, migrationSrv, + components, system, ec) - val log = Logger(getClass) + private[StreamCtrl] lazy val logger = Logger(getClass) /** * Create a new stream entry with the event head @@ -69,7 +69,7 @@ class StreamCtrl( @Timed("controllers.StreamCtrl.create") def create: Action[AnyContent] = authenticated(Role.read) { val id = generateStreamId() - val aref = system.actorOf(Props( + system.actorOf(Props( classOf[StreamActor], cacheExpiration, refresh, @@ -80,7 +80,7 @@ class StreamCtrl( Ok(id) } - val alphanumeric: immutable.IndexedSeq[Char] = (('a' to 'z') ++ ('A' to 'Z') ++ ('0' to '9')) + val alphanumeric: immutable.IndexedSeq[Char] = ('a' to 'z') ++ ('A' to 'Z') ++ ('0' to '9') private[controllers] def generateStreamId() = Seq.fill(10)(alphanumeric(Random.nextInt(alphanumeric.size))).mkString private[controllers] def isValidStreamId(streamId: String): Boolean = { streamId.length == 10 && streamId.forall(alphanumeric.contains) @@ -92,7 +92,7 @@ class StreamCtrl( */ @Timed("controllers.StreamCtrl.get") def get(id: String): Action[AnyContent] = Action.async { implicit request ⇒ - implicit val timeout = Timeout(refresh + globalMaxWait + 1.second) + implicit val timeout: Timeout = Timeout(refresh + globalMaxWait + 1.second) if (!isValidStreamId(id)) { Future.successful(BadRequest("Invalid stream id")) diff --git a/thehive-backend/app/controllers/TaskCtrl.scala b/thehive-backend/app/controllers/TaskCtrl.scala index 6f00e4beb9..6362db045c 100644 --- a/thehive-backend/app/controllers/TaskCtrl.scala +++ b/thehive-backend/app/controllers/TaskCtrl.scala @@ -3,16 +3,17 @@ package controllers import javax.inject.{ Inject, Singleton } import scala.concurrent.ExecutionContext -import scala.reflect.runtime.universe + import play.api.http.Status -import play.api.mvc.{ Action, AnyContent, Controller } -import org.elastic4play.{ BadRequestError, Timed } +import play.api.mvc._ + +import services.TaskSrv + import org.elastic4play.controllers.{ Authenticated, Fields, FieldsBodyParser, Renderer } import org.elastic4play.models.JsonFormat.baseModelEntityWrites -import org.elastic4play.services.{ Agg, AuxSrv } -import org.elastic4play.services.{ QueryDSL, QueryDef, Role } import org.elastic4play.services.JsonFormat.{ aggReads, queryReads } -import services.TaskSrv +import org.elastic4play.services._ +import org.elastic4play.{ BadRequestError, Timed } @Singleton class TaskCtrl @Inject() ( @@ -21,7 +22,8 @@ class TaskCtrl @Inject() ( authenticated: Authenticated, renderer: Renderer, fieldsBodyParser: FieldsBodyParser, - implicit val ec: ExecutionContext) extends Controller with Status { + components: ControllerComponents, + implicit val ec: ExecutionContext) extends AbstractController(components) with Status { @Timed def create(caseId: String): Action[Fields] = authenticated(Role.write).async(fieldsBodyParser) { implicit request ⇒ diff --git a/thehive-backend/app/controllers/UserCtrl.scala b/thehive-backend/app/controllers/UserCtrl.scala index e6f4f82730..98e833757d 100644 --- a/thehive-backend/app/controllers/UserCtrl.scala +++ b/thehive-backend/app/controllers/UserCtrl.scala @@ -2,22 +2,21 @@ package controllers import javax.inject.{ Inject, Singleton } -import scala.annotation.implicitNotFound import scala.concurrent.{ ExecutionContext, Future } +import scala.util.Try + import play.api.Logger import play.api.http.Status -import play.api.mvc.{ Action, AnyContent, Controller, Result } -import org.elastic4play.{ AuthorizationError, MissingAttributeError, Timed } -import org.elastic4play.controllers.{ Authenticated, Fields, FieldsBodyParser, Renderer } -import org.elastic4play.models.JsonFormat.baseModelEntityWrites -import org.elastic4play.services.{ QueryDSL, QueryDef, Role } -import org.elastic4play.services.AuthSrv -import org.elastic4play.services.JsonFormat.{ authContextWrites, queryReads } +import play.api.libs.json.{ JsObject, Json } +import play.api.mvc._ + import services.UserSrv -import play.api.libs.json.Json -import scala.util.Try -import play.api.libs.json.JsObject +import org.elastic4play.controllers.{ Authenticated, Fields, FieldsBodyParser, Renderer } +import org.elastic4play.models.JsonFormat.baseModelEntityWrites +import org.elastic4play.services.JsonFormat.queryReads +import org.elastic4play.services.{ AuthSrv, QueryDSL, QueryDef, Role } +import org.elastic4play.{ AuthorizationError, MissingAttributeError, Timed } @Singleton class UserCtrl @Inject() ( @@ -26,9 +25,10 @@ class UserCtrl @Inject() ( authenticated: Authenticated, renderer: Renderer, fieldsBodyParser: FieldsBodyParser, - implicit val ec: ExecutionContext) extends Controller with Status { + components: ControllerComponents, + implicit val ec: ExecutionContext) extends AbstractController(components) with Status { - lazy val logger = Logger(getClass) + private[UserCtrl] lazy val logger = Logger(getClass) @Timed def create: Action[Fields] = authenticated(Role.admin).async(fieldsBodyParser) { implicit request ⇒ @@ -95,7 +95,7 @@ class UserCtrl @Inject() ( user ← userSrv.get(authContext.userId) preferences = Try(Json.parse(user.preferences())) .recover { - case error ⇒ + case _ ⇒ logger.warn(s"User ${authContext.userId} has invalid preference format: ${user.preferences()}") JsObject(Nil) } diff --git a/thehive-backend/app/global/Filters.scala b/thehive-backend/app/global/Filters.scala index 180f6d3020..b319722e37 100644 --- a/thehive-backend/app/global/Filters.scala +++ b/thehive-backend/app/global/Filters.scala @@ -2,15 +2,16 @@ package global import javax.inject.{ Inject, Provider, Singleton } -import akka.stream.Materializer +import scala.collection.immutable + import play.api.Logger -import play.api.http.HttpFilters +import play.api.http.{ HttpFilters, SessionConfiguration } import play.api.libs.crypto.CSRFTokenSigner import play.api.mvc.{ EssentialFilter, RequestHeader } import play.filters.csrf.CSRF.{ ErrorHandler, TokenProvider } import play.filters.csrf.CSRFConfig -import scala.collection.immutable +import akka.stream.Materializer @Singleton class TheHiveFilters @Inject() (injectedFilters: immutable.Set[EssentialFilter]) extends HttpFilters { @@ -35,10 +36,12 @@ object CSRFFilter { class CSRFFilter @Inject() ( config: Provider[CSRFConfig], tokenSignerProvider: Provider[CSRFTokenSigner], + sessionConfiguration: SessionConfiguration, tokenProvider: TokenProvider, errorHandler: ErrorHandler)(mat: Materializer) extends play.filters.csrf.CSRFFilter( config.get.copy(shouldProtect = CSRFFilter.shouldProtect), tokenSignerProvider.get, + sessionConfiguration, tokenProvider, errorHandler)(mat) \ No newline at end of file diff --git a/thehive-backend/app/global/TheHive.scala b/thehive-backend/app/global/TheHive.scala index f9f0f6f19d..c6a2400d33 100644 --- a/thehive-backend/app/global/TheHive.scala +++ b/thehive-backend/app/global/TheHive.scala @@ -1,6 +1,10 @@ package global -import java.net.{ URL, URLClassLoader } +import scala.collection.JavaConverters._ + +import play.api.mvc.EssentialFilter +import play.api.{ Configuration, Environment, Logger, Mode } +import play.api.libs.concurrent.AkkaGuiceSupport import com.google.inject.AbstractModule import com.google.inject.name.Names @@ -8,16 +12,14 @@ import connectors.Connector import controllers.{ AssetCtrl, AssetCtrlDev, AssetCtrlProd } import models.Migration import net.codingwell.scalaguice.{ ScalaModule, ScalaMultibinder } +import org.reflections.Reflections +import org.reflections.scanners.SubTypesScanner +import org.reflections.util.ConfigurationBuilder +import services._ + import org.elastic4play.models.BaseModelDef import org.elastic4play.services.auth.MultiAuthSrv import org.elastic4play.services.{ AuthSrv, AuthSrvFactory, MigrationOperations, TempFilter } -import org.reflections.Reflections -import play.api.libs.concurrent.AkkaGuiceSupport -import play.api.mvc.EssentialFilter -import play.api.{ Configuration, Environment, Logger, Mode } -import services.{ AuditSrv, AuditedModel, StreamFilter, StreamMonitor } - -import scala.collection.JavaConversions.asScalaSet class TheHive( environment: Environment, @@ -33,15 +35,19 @@ class TheHive( val authBindings = ScalaMultibinder.newSetBinder[AuthSrv](binder) val authFactoryBindings = ScalaMultibinder.newSetBinder[AuthSrvFactory](binder) - val packageUrls = Seq(getClass.getClassLoader, classOf[org.elastic4play.Timed].getClassLoader).flatMap { - case ucl: URLClassLoader ⇒ ucl.getURLs - case _ ⇒ Array.empty[URL] - } + val reflectionClasses = new Reflections(new ConfigurationBuilder() + .forPackages("org.elastic4play") + .forPackages("connectors.cortex") + .forPackages("connectors.misp") + .forPackages("connectors.metrics") + .addClassLoader(getClass.getClassLoader) + .addClassLoader(environment.getClass.getClassLoader) + .setExpandSuperTypes(false) + .setScanners(new SubTypesScanner(false))) - new Reflections(new org.reflections.util.ConfigurationBuilder() - .addUrls(packageUrls: _*) - .setScanners(new org.reflections.scanners.SubTypesScanner(false))) + reflectionClasses .getSubTypesOf(classOf[BaseModelDef]) + .asScala .filterNot(c ⇒ java.lang.reflect.Modifier.isAbstract(c.getModifiers)) .foreach { modelClass ⇒ logger.info(s"Loading model $modelClass") @@ -51,20 +57,18 @@ class TheHive( } } - new Reflections(new org.reflections.util.ConfigurationBuilder() - .addUrls(packageUrls: _*) - .setScanners(new org.reflections.scanners.SubTypesScanner(false))) + reflectionClasses .getSubTypesOf(classOf[AuthSrv]) + .asScala .filterNot(c ⇒ java.lang.reflect.Modifier.isAbstract(c.getModifiers) || c.isMemberClass) .filterNot(_ == classOf[MultiAuthSrv]) .foreach { modelClass ⇒ authBindings.addBinding.to(modelClass) } - new Reflections(new org.reflections.util.ConfigurationBuilder() - .addUrls(packageUrls: _*) - .setScanners(new org.reflections.scanners.SubTypesScanner(false))) + reflectionClasses .getSubTypesOf(classOf[AuthSrvFactory]) + .asScala .filterNot(c ⇒ java.lang.reflect.Modifier.isAbstract(c.getModifiers)) .foreach { modelClass ⇒ authFactoryBindings.addBinding.to(modelClass) @@ -77,8 +81,9 @@ class TheHive( bind[MigrationOperations].to[Migration] bind[AuthSrv].to[MultiAuthSrv] - bind[StreamMonitor].asEagerSingleton() - bind[AuditSrv].asEagerSingleton() + + bindActor[AuditActor]("AuditActor") + bindActor[DeadLetterMonitoringActor]("DeadLetterMonitoringActor") if (environment.mode == Mode.Prod) bind[AssetCtrl].to[AssetCtrlProd] diff --git a/thehive-backend/app/models/Alert.scala b/thehive-backend/app/models/Alert.scala index b6cb770234..caca397506 100644 --- a/thehive-backend/app/models/Alert.scala +++ b/thehive-backend/app/models/Alert.scala @@ -3,18 +3,20 @@ package models import java.util.Date import javax.inject.{ Inject, Singleton } +import scala.concurrent.Future +import scala.util.Try + +import play.api.Logger +import play.api.libs.json._ + import models.JsonFormat.alertStatusFormat +import services.AuditedModel + import org.elastic4play.controllers.JsonInputValue import org.elastic4play.models.{ Attribute, AttributeDef, BaseEntity, EntityDef, HiveEnumeration, ModelDef, MultiAttributeFormat, OptionalAttributeFormat, AttributeFormat ⇒ F, AttributeOption ⇒ O } import org.elastic4play.services.DBLists import org.elastic4play.utils.Hasher import org.elastic4play.{ AttributeCheckingError, InvalidFormatAttributeError } -import play.api.Logger -import play.api.libs.json._ -import services.AuditedModel - -import scala.concurrent.Future -import scala.util.Try object AlertStatus extends Enumeration with HiveEnumeration { type Type = Value diff --git a/thehive-backend/app/models/Artifact.scala b/thehive-backend/app/models/Artifact.scala index 379c2ec931..401c83542b 100644 --- a/thehive-backend/app/models/Artifact.scala +++ b/thehive-backend/app/models/Artifact.scala @@ -3,25 +3,24 @@ package models import java.util.Date import javax.inject.{ Inject, Provider, Singleton } -import akka.{ Done, NotUsed } - import scala.concurrent.{ ExecutionContext, Future } -import scala.language.postfixOps -import akka.stream.{ IOResult, Materializer } -import play.api.libs.json.{ JsArray, JsNull, JsObject, JsString, JsValue } +import scala.util.Success + +import play.api.Logger import play.api.libs.json.JsLookupResult.jsLookupResultToJsLookup import play.api.libs.json.JsValue.jsValueToJsLookup -import play.api.libs.json.Json import play.api.libs.json.Json.toJsFieldJsValueWrapper -import org.elastic4play.{ BadRequestError, InternalError } -import org.elastic4play.models.{ AttributeDef, BaseEntity, ChildModelDef, EntityDef, HiveEnumeration, AttributeFormat ⇒ F, AttributeOption ⇒ O } -import org.elastic4play.services.{ Attachment, AttachmentSrv, DBLists } -import org.elastic4play.utils.MultiHash +import play.api.libs.json._ + +import akka.stream.{ IOResult, Materializer } +import akka.{ Done, NotUsed } import models.JsonFormat.artifactStatusFormat -import play.api.Logger import services.{ ArtifactSrv, AuditedModel } -import scala.util.Success +import org.elastic4play.models.{ AttributeDef, BaseEntity, ChildModelDef, EntityDef, HiveEnumeration, AttributeFormat ⇒ F, AttributeOption ⇒ O } +import org.elastic4play.services.{ Attachment, AttachmentSrv, DBLists } +import org.elastic4play.utils.MultiHash +import org.elastic4play.{ BadRequestError, InternalError } object ArtifactStatus extends Enumeration with HiveEnumeration { type Type = Value @@ -75,14 +74,14 @@ class ArtifactModel @Inject() ( entity match { case artifact: Artifact ⇒ val removeMessage = (updateAttrs \ "message").toOption.exists { - case JsNull ⇒ true - case JsArray(Nil) ⇒ true - case _ ⇒ false + case JsNull ⇒ true + case JsArray(Seq()) ⇒ true + case _ ⇒ false } val removeTags = (updateAttrs \ "tags").toOption.exists { - case JsNull ⇒ true - case JsArray(Nil) ⇒ true - case _ ⇒ false + case JsNull ⇒ true + case JsArray(Seq()) ⇒ true + case _ ⇒ false } if ((removeMessage && removeTags) || (removeMessage && artifact.tags().isEmpty) || @@ -115,6 +114,7 @@ class ArtifactModel @Inject() ( entity match { case artifact: Artifact ⇒ val (_, total) = artifactSrv.get.findSimilar(artifact, Some("0-0"), Nil) + total.failed.foreach(t ⇒ logger.error("Artifact.getStats error", t)) total.map { t ⇒ Json.obj("seen" → t) } case _ ⇒ Future.successful(JsObject(Nil)) } diff --git a/thehive-backend/app/models/Audit.scala b/thehive-backend/app/models/Audit.scala index 3bb152c059..5c9d1c8860 100644 --- a/thehive-backend/app/models/Audit.scala +++ b/thehive-backend/app/models/Audit.scala @@ -4,12 +4,15 @@ import java.util.Date import javax.inject.{ Inject, Singleton } import scala.collection.immutable -import play.api.{ Configuration, Logger } + import play.api.libs.json.JsObject -import org.elastic4play.models.{ Attribute, AttributeFormat, AttributeDef, EntityDef, EnumerationAttributeFormat, ListEnumerationAttributeFormat, ModelDef, MultiAttributeFormat, ObjectAttributeFormat, OptionalAttributeFormat, StringAttributeFormat, AttributeOption ⇒ O } +import play.api.{ Configuration, Logger } + +import services.AuditedModel + +import org.elastic4play.models.{ Attribute, AttributeDef, AttributeFormat, EntityDef, EnumerationAttributeFormat, ListEnumerationAttributeFormat, ModelDef, MultiAttributeFormat, ObjectAttributeFormat, OptionalAttributeFormat, StringAttributeFormat, AttributeOption ⇒ O } import org.elastic4play.services.AuditableAction import org.elastic4play.services.JsonFormat.auditableActionFormat -import services.AuditedModel trait AuditAttributes { _: AttributeDef ⇒ def detailsAttributes: Seq[Attribute[_]] @@ -34,10 +37,10 @@ class AuditModel( configuration: Configuration, auditedModels: immutable.Set[AuditedModel]) = this( - configuration.getString("audit.name").get, + configuration.get[String]("audit.name"), auditedModels) - lazy val logger = Logger(getClass) + private[AuditModel] lazy val logger = Logger(getClass) def mergeAttributeFormat(context: String, format1: AttributeFormat[_], format2: AttributeFormat[_]): Option[AttributeFormat[_]] = { (format1, format2) match { diff --git a/thehive-backend/app/models/Case.scala b/thehive-backend/app/models/Case.scala index 8fe9d83d55..802a295071 100644 --- a/thehive-backend/app/models/Case.scala +++ b/thehive-backend/app/models/Case.scala @@ -3,18 +3,20 @@ package models import java.util.Date import javax.inject.{ Inject, Provider, Singleton } -import models.JsonFormat.{ caseImpactStatusFormat, caseResolutionStatusFormat, caseStatusFormat } -import org.elastic4play.JsonFormat.dateFormat -import org.elastic4play.models.{ AttributeDef, BaseEntity, EntityDef, HiveEnumeration, ModelDef, AttributeFormat ⇒ F, AttributeOption ⇒ O } -import org.elastic4play.services.{ FindSrv, SequenceSrv } +import scala.concurrent.{ ExecutionContext, Future } +import scala.math.BigDecimal.{ int2bigDecimal, long2bigDecimal } + import play.api.Logger import play.api.libs.json.JsValue.jsValueToJsLookup import play.api.libs.json.Json.toJsFieldJsValueWrapper import play.api.libs.json._ + +import models.JsonFormat.{ caseImpactStatusFormat, caseResolutionStatusFormat, caseStatusFormat } import services.{ AuditedModel, CaseSrv } -import scala.concurrent.{ ExecutionContext, Future } -import scala.math.BigDecimal.{ int2bigDecimal, long2bigDecimal } +import org.elastic4play.JsonFormat.dateFormat +import org.elastic4play.models.{ AttributeDef, BaseEntity, EntityDef, HiveEnumeration, ModelDef, AttributeFormat ⇒ F, AttributeOption ⇒ O } +import org.elastic4play.services.{ FindSrv, SequenceSrv } object CaseStatus extends Enumeration with HiveEnumeration { type Type = Value @@ -61,7 +63,7 @@ class CaseModel @Inject() ( findSrv: FindSrv, implicit val ec: ExecutionContext) extends ModelDef[CaseModel, Case]("case") with CaseAttributes with AuditedModel { caseModel ⇒ - lazy val logger = Logger(getClass) + private[CaseModel] lazy val logger = Logger(getClass) override val defaultSortBy = Seq("-startDate") override val removeAttribute: JsObject = Json.obj("status" → CaseStatus.Deleted) diff --git a/thehive-backend/app/models/CaseTemplate.scala b/thehive-backend/app/models/CaseTemplate.scala index 833f58dcd2..f6775f64a9 100644 --- a/thehive-backend/app/models/CaseTemplate.scala +++ b/thehive-backend/app/models/CaseTemplate.scala @@ -3,9 +3,11 @@ package models import javax.inject.{ Inject, Singleton } import play.api.libs.json.{ JsObject, JsValue } -import org.elastic4play.models.{ Attribute, AttributeDef, EntityDef, HiveEnumeration, ModelDef, AttributeFormat ⇒ F } + import models.JsonFormat.caseTemplateStatusFormat +import org.elastic4play.models.{ Attribute, AttributeDef, EntityDef, HiveEnumeration, ModelDef, AttributeFormat ⇒ F } + object CaseTemplateStatus extends Enumeration with HiveEnumeration { type Type = Value val Ok, Deleted = Value diff --git a/thehive-backend/app/models/JsonFormat.scala b/thehive-backend/app/models/JsonFormat.scala index 824976b89d..6c46292e5c 100644 --- a/thehive-backend/app/models/JsonFormat.scala +++ b/thehive-backend/app/models/JsonFormat.scala @@ -2,9 +2,10 @@ package models import java.nio.file.Path -import org.elastic4play.models.JsonFormat.enumFormat import play.api.libs.json.{ Format, JsString, Writes } +import org.elastic4play.models.JsonFormat.enumFormat + object JsonFormat { implicit val userStatusFormat: Format[UserStatus.Type] = enumFormat(UserStatus) implicit val caseStatusFormat: Format[CaseStatus.Type] = enumFormat(CaseStatus) diff --git a/thehive-backend/app/models/Log.scala b/thehive-backend/app/models/Log.scala index ea49125cd0..cf07277b62 100644 --- a/thehive-backend/app/models/Log.scala +++ b/thehive-backend/app/models/Log.scala @@ -1,17 +1,16 @@ package models import java.util.Date - import javax.inject.{ Inject, Singleton } -import play.api.libs.json.{ JsObject, Json } import play.api.libs.json.Json.toJsFieldJsValueWrapper - -import org.elastic4play.models.{ AttributeDef, AttributeFormat ⇒ F, AttributeOption ⇒ O, ChildModelDef, EntityDef, HiveEnumeration } +import play.api.libs.json.{ JsObject, Json } import models.JsonFormat.logStatusFormat import services.AuditedModel +import org.elastic4play.models.{ AttributeDef, ChildModelDef, EntityDef, HiveEnumeration, AttributeFormat ⇒ F, AttributeOption ⇒ O } + object LogStatus extends Enumeration with HiveEnumeration { type Type = Value val Ok, Deleted = Value diff --git a/thehive-backend/app/models/Migration.scala b/thehive-backend/app/models/Migration.scala index 80ec8ae3e5..d5467b7138 100644 --- a/thehive-backend/app/models/Migration.scala +++ b/thehive-backend/app/models/Migration.scala @@ -1,28 +1,31 @@ package models import java.util.Date -import javax.inject.Inject +import javax.inject.{ Inject, Singleton } + +import scala.collection.immutable.{ Set ⇒ ISet } +import scala.concurrent.{ ExecutionContext, Future } +import scala.math.BigDecimal.int2bigDecimal +import scala.util.Try + +import play.api.libs.json.JsValue.jsValueToJsLookup +import play.api.libs.json._ +import play.api.{ Configuration, Logger } import akka.NotUsed import akka.stream.Materializer import akka.stream.scaladsl.Source +import services.AlertSrv + import org.elastic4play.models.BaseModelDef import org.elastic4play.services.JsonFormat.attachmentFormat import org.elastic4play.services._ import org.elastic4play.utils import org.elastic4play.utils.{ Hasher, RichJson } -import play.api.libs.json.JsValue.jsValueToJsLookup -import play.api.libs.json._ -import play.api.{ Configuration, Logger } -import services.AlertSrv - -import scala.collection.immutable.{ Set ⇒ ISet } -import scala.concurrent.{ ExecutionContext, Future } -import scala.math.BigDecimal.int2bigDecimal -import scala.util.Try case class UpdateMispAlertArtifact() extends EventMessage +@Singleton class Migration( mispCaseTemplate: Option[String], mainHash: String, @@ -41,17 +44,17 @@ class Migration( ec: ExecutionContext, materializer: Materializer) = { this( - configuration.getString("misp.caseTemplate"), - configuration.getString("datastore.hash.main").get, - configuration.getStringSeq("datastore.hash.extra").get, - configuration.getString("datastore.name").get, + configuration.getOptional[String]("misp.caseTemplate"), + configuration.get[String]("datastore.hash.main"), + configuration.get[Seq[String]]("datastore.hash.extra"), + configuration.get[String]("datastore.name"), models, dblists, eventSrv, ec, materializer) } import org.elastic4play.services.Operation._ - val logger = Logger(getClass) + private[Migration] lazy val logger = Logger(getClass) private var requireUpdateMispAlertArtifact = false override def beginMigration(version: Int): Future[Unit] = Future.successful(()) diff --git a/thehive-backend/app/models/Task.scala b/thehive-backend/app/models/Task.scala index 4efa4b2433..0b99e25417 100644 --- a/thehive-backend/app/models/Task.scala +++ b/thehive-backend/app/models/Task.scala @@ -1,21 +1,20 @@ package models import java.util.Date - import javax.inject.{ Inject, Singleton } import scala.concurrent.Future -import play.api.libs.json.{ JsBoolean, JsObject } import play.api.libs.json.JsValue.jsValueToJsLookup - -import org.elastic4play.JsonFormat.dateFormat -import org.elastic4play.models.{ AttributeDef, AttributeFormat ⇒ F, BaseEntity, ChildModelDef, EntityDef, HiveEnumeration } -import org.elastic4play.utils.RichJson +import play.api.libs.json.{ JsBoolean, JsObject } import models.JsonFormat.taskStatusFormat import services.AuditedModel +import org.elastic4play.JsonFormat.dateFormat +import org.elastic4play.models.{ AttributeDef, BaseEntity, ChildModelDef, EntityDef, HiveEnumeration, AttributeFormat ⇒ F } +import org.elastic4play.utils.RichJson + object TaskStatus extends Enumeration with HiveEnumeration { type Type = Value val Waiting, InProgress, Completed, Cancel = Value diff --git a/thehive-backend/app/models/User.scala b/thehive-backend/app/models/User.scala index 673b3d02f9..9df4658c4d 100644 --- a/thehive-backend/app/models/User.scala +++ b/thehive-backend/app/models/User.scala @@ -3,18 +3,17 @@ package models import java.util.UUID import scala.concurrent.Future -import scala.language.postfixOps -import play.api.libs.json.{ JsBoolean, JsObject, JsString, JsUndefined } import play.api.libs.json.JsValue.jsValueToJsLookup - -import org.elastic4play.models.{ AttributeDef, AttributeFormat ⇒ F, AttributeOption ⇒ O, BaseEntity, EntityDef, HiveEnumeration, ModelDef } -import org.elastic4play.services.JsonFormat.roleFormat -import org.elastic4play.services.Role +import play.api.libs.json.{ JsBoolean, JsObject, JsString, JsUndefined } import models.JsonFormat.userStatusFormat import services.AuditedModel +import org.elastic4play.models.{ AttributeDef, BaseEntity, EntityDef, HiveEnumeration, ModelDef, AttributeFormat ⇒ F, AttributeOption ⇒ O } +import org.elastic4play.services.JsonFormat.roleFormat +import org.elastic4play.services.Role + object UserStatus extends Enumeration with HiveEnumeration { type Type = Value val Ok, Locked = Value diff --git a/thehive-backend/app/services/AlertSrv.scala b/thehive-backend/app/services/AlertSrv.scala index 2ee217216a..d67cc97d7a 100644 --- a/thehive-backend/app/services/AlertSrv.scala +++ b/thehive-backend/app/services/AlertSrv.scala @@ -1,25 +1,27 @@ package services import java.nio.file.Files -import javax.inject.Inject +import javax.inject.{ Inject, Singleton } + +import scala.collection.immutable +import scala.concurrent.{ ExecutionContext, Future } +import scala.util.matching.Regex +import scala.util.{ Failure, Try } + +import play.api.libs.json._ +import play.api.{ Configuration, Logger } import akka.NotUsed import akka.stream.Materializer import akka.stream.scaladsl.{ Sink, Source } import connectors.ConnectorRouter import models._ + import org.elastic4play.InternalError import org.elastic4play.controllers.{ Fields, FileInputValue } import org.elastic4play.services.JsonFormat.attachmentFormat import org.elastic4play.services.QueryDSL.{ groupByField, parent, selectCount, withId } import org.elastic4play.services._ -import play.api.libs.json._ -import play.api.{ Configuration, Logger } - -import scala.collection.immutable -import scala.concurrent.{ ExecutionContext, Future } -import scala.util.matching.Regex -import scala.util.{ Failure, Try } trait AlertTransformer { def createCase(alert: Alert, customCaseTemplate: Option[String])(implicit authContext: AuthContext): Future[Case] @@ -33,6 +35,7 @@ object AlertSrv { val dataExtractor: Regex = "^(.*);(.*);(.*)".r } +@Singleton class AlertSrv( templates: Map[String, String], alertModel: AlertModel, @@ -77,7 +80,7 @@ class AlertSrv( caseTemplateSrv, attachmentSrv, connectors, - (configuration.getString("datastore.hash.main").get +: configuration.getStringSeq("datastore.hash.extra").get).distinct, + (configuration.get[String]("datastore.hash.main") +: configuration.get[Seq[String]]("datastore.hash.extra")).distinct, ec, mat) @@ -300,7 +303,7 @@ class AlertSrv( caseSrv.get(caseId).map((_, similarIOCCount, similarArtifactCount)) } .filter { - case (caze, _, _) ⇒ caze.status() != CaseStatus.Deleted && caze.resolutionStatus != CaseResolutionStatus.Duplicated + case (caze, _, _) ⇒ caze.status() != CaseStatus.Deleted && !caze.resolutionStatus().contains(CaseResolutionStatus.Duplicated) } .mapAsyncUnordered(5) { case (caze, similarIOCCount, similarArtifactCount) ⇒ diff --git a/thehive-backend/app/services/ArtifactSrv.scala b/thehive-backend/app/services/ArtifactSrv.scala index e37daab702..9f957004ab 100644 --- a/thehive-backend/app/services/ArtifactSrv.scala +++ b/thehive-backend/app/services/ArtifactSrv.scala @@ -2,19 +2,21 @@ package services import javax.inject.{ Inject, Singleton } +import scala.concurrent.{ ExecutionContext, Future } +import scala.util.{ Failure, Try } + +import play.api.Logger +import play.api.libs.json.JsObject +import play.api.libs.json.JsValue.jsValueToJsLookup + import akka.NotUsed import akka.stream.scaladsl.Source import models.{ CaseResolutionStatus, CaseStatus, _ } + import org.elastic4play.ConflictError import org.elastic4play.controllers.Fields import org.elastic4play.services._ import org.elastic4play.utils.{ RichFuture, RichOr } -import play.api.Logger -import play.api.libs.json.JsObject -import play.api.libs.json.JsValue.jsValueToJsLookup - -import scala.concurrent.{ ExecutionContext, Future } -import scala.util.{ Failure, Try } @Singleton class ArtifactSrv @Inject() ( @@ -37,7 +39,7 @@ class ArtifactSrv @Inject() ( def create(caze: Case, fields: Fields)(implicit authContext: AuthContext): Future[Artifact] = { createSrv[ArtifactModel, Artifact, Case](artifactModel, caze, fields) .recoverWith { - case error ⇒ updateIfDeleted(caze, fields) // maybe the artifact already exists. If so, search it and update it + case _ ⇒ updateIfDeleted(caze, fields) // maybe the artifact already exists. If so, search it and update it } } @@ -71,7 +73,7 @@ class ArtifactSrv @Inject() ( case t ⇒ Future.successful(t) } - def get(id: String)(implicit authContext: AuthContext): Future[Artifact] = { + def get(id: String): Future[Artifact] = { getSrv[ArtifactModel, Artifact](artifactModel, id) } diff --git a/thehive-backend/app/services/AuditSrv.scala b/thehive-backend/app/services/AuditSrv.scala index ad3fb04555..3e62d3aff8 100644 --- a/thehive-backend/app/services/AuditSrv.scala +++ b/thehive-backend/app/services/AuditSrv.scala @@ -2,17 +2,18 @@ package services import javax.inject.{ Inject, Singleton } -import akka.actor.ActorDSL.{ Act, actor } -import akka.actor.{ ActorRef, ActorSystem } +import scala.concurrent.ExecutionContext + +import play.api.Logger +import play.api.libs.json.{ JsBoolean, JsObject, Json } + +import akka.actor.Actor import models.{ Audit, AuditModel } + import org.elastic4play.controllers.Fields import org.elastic4play.models.{ Attribute, BaseEntity, BaseModelDef } import org.elastic4play.services._ import org.elastic4play.utils.Instance -import play.api.Logger -import play.api.libs.json.{ JsBoolean, JsObject, Json } - -import scala.concurrent.ExecutionContext trait AuditedModel { self: BaseModelDef ⇒ def attributes: Seq[Attribute[_]] @@ -34,39 +35,42 @@ trait AuditedModel { self: BaseModelDef ⇒ } @Singleton -class AuditSrv @Inject() ( +class AuditActor @Inject() ( auditModel: AuditModel, - eventSrv: EventSrv, createSrv: CreateSrv, - implicit val ec: ExecutionContext, - implicit val system: ActorSystem) { - + eventSrv: EventSrv, + implicit val ec: ExecutionContext) extends Actor { object EntityExtractor { def unapply(e: BaseEntity) = Some((e.model, e.id, e.routing)) } + var currentRequestIds = Set.empty[String] + private[AuditActor] lazy val logger = Logger(getClass) - val auditActor: ActorRef = actor(new Act { + override def preStart(): Unit = { + eventSrv.subscribe(self, classOf[EventMessage]) + super.preStart() + } - lazy val log = Logger(getClass) - var currentRequestIds = Set.empty[String] + override def postStop(): Unit = { + eventSrv.unsubscribe(self) + super.postStop() + } - become { - case RequestProcessEnd(request, _) ⇒ - currentRequestIds = currentRequestIds - Instance.getRequestId(request) - case AuditOperation(EntityExtractor(model: AuditedModel, id, routing), action, details, authContext, date) ⇒ - val requestId = authContext.requestId - createSrv[AuditModel, Audit](auditModel, Fields.empty - .set("operation", action.toString) - .set("details", model.selectAuditedAttributes(details)) - .set("objectType", model.name) - .set("objectId", id) - .set("base", JsBoolean(!currentRequestIds.contains(requestId))) - .set("startDate", Json.toJson(date)) - .set("rootId", routing) - .set("requestId", requestId))(authContext) - .onFailure { case t ⇒ log.error("Audit error", t) } - currentRequestIds = currentRequestIds + requestId - } - }) - eventSrv.subscribe(auditActor, classOf[EventMessage]) // need to unsubsribe ? + override def receive: Receive = { + case RequestProcessEnd(request, _) ⇒ + currentRequestIds = currentRequestIds - Instance.getRequestId(request) + case AuditOperation(EntityExtractor(model: AuditedModel, id, routing), action, details, authContext, date) ⇒ + val requestId = authContext.requestId + createSrv[AuditModel, Audit](auditModel, Fields.empty + .set("operation", action.toString) + .set("details", model.selectAuditedAttributes(details)) + .set("objectType", model.name) + .set("objectId", id) + .set("base", JsBoolean(!currentRequestIds.contains(requestId))) + .set("startDate", Json.toJson(date)) + .set("rootId", routing) + .set("requestId", requestId))(authContext) + .failed.foreach(t ⇒ logger.error("Audit error", t)) + currentRequestIds = currentRequestIds + requestId + } } \ No newline at end of file diff --git a/thehive-backend/app/services/CaseMergeSrv.scala b/thehive-backend/app/services/CaseMergeSrv.scala index 6e7fecb009..9bd9b7788d 100644 --- a/thehive-backend/app/services/CaseMergeSrv.scala +++ b/thehive-backend/app/services/CaseMergeSrv.scala @@ -3,20 +3,22 @@ package services import java.util.Date import javax.inject.{ Inject, Singleton } +import scala.concurrent.{ ExecutionContext, Future } +import scala.math.BigDecimal.long2bigDecimal +import scala.util.Failure + +import play.api.Logger +import play.api.libs.json.JsValue.jsValueToJsLookup +import play.api.libs.json._ + import akka.Done import akka.stream.Materializer import akka.stream.scaladsl.Sink import models._ + import org.elastic4play.controllers.{ AttachmentInputValue, Fields } import org.elastic4play.models.BaseEntity import org.elastic4play.services.{ AuthContext, EventMessage, EventSrv } -import play.api.Logger -import play.api.libs.json.JsValue.jsValueToJsLookup -import play.api.libs.json._ - -import scala.concurrent.{ ExecutionContext, Future } -import scala.math.BigDecimal.long2bigDecimal -import scala.util.Failure case class MergeArtifact(newArtifact: Artifact, artifacts: Seq[Artifact], authContext: AuthContext) extends EventMessage diff --git a/thehive-backend/app/services/CaseSrv.scala b/thehive-backend/app/services/CaseSrv.scala index 93db6a64e4..b75b0edd7d 100644 --- a/thehive-backend/app/services/CaseSrv.scala +++ b/thehive-backend/app/services/CaseSrv.scala @@ -2,18 +2,20 @@ package services import javax.inject.{ Inject, Singleton } +import scala.concurrent.{ ExecutionContext, Future } +import scala.util.Try + +import play.api.Logger +import play.api.libs.json.Json.toJsFieldJsValueWrapper +import play.api.libs.json._ + import akka.NotUsed import akka.stream.scaladsl.Source import models._ + import org.elastic4play.InternalError import org.elastic4play.controllers.Fields import org.elastic4play.services._ -import play.api.Logger -import play.api.libs.json.Json.toJsFieldJsValueWrapper -import play.api.libs.json._ - -import scala.concurrent.{ ExecutionContext, Future } -import scala.util.Try @Singleton class CaseSrv @Inject() ( @@ -28,7 +30,7 @@ class CaseSrv @Inject() ( findSrv: FindSrv, implicit val ec: ExecutionContext) { - lazy val log = Logger(getClass) + private[CaseSrv] lazy val logger = Logger(getClass) def applyTemplate(template: CaseTemplate, originalFields: Fields): Fields = { def getJsObjectOrEmpty(value: Option[JsValue]) = value.fold(JsObject(Nil)) { diff --git a/thehive-backend/app/services/CaseTemplateSrv.scala b/thehive-backend/app/services/CaseTemplateSrv.scala index 62a08f0fe9..5a3405a155 100644 --- a/thehive-backend/app/services/CaseTemplateSrv.scala +++ b/thehive-backend/app/services/CaseTemplateSrv.scala @@ -7,12 +7,11 @@ import scala.concurrent.{ ExecutionContext, Future } import akka.NotUsed import akka.stream.Materializer import akka.stream.scaladsl.{ Sink, Source } +import models.{ CaseTemplate, CaseTemplateModel } import org.elastic4play.NotFoundError import org.elastic4play.controllers.Fields -import org.elastic4play.services.{ AuthContext, CreateSrv, DeleteSrv, FindSrv, GetSrv, QueryDSL, QueryDef, UpdateSrv } - -import models.{ CaseTemplate, CaseTemplateModel } +import org.elastic4play.services._ @Singleton class CaseTemplateSrv @Inject() ( @@ -28,7 +27,7 @@ class CaseTemplateSrv @Inject() ( def create(fields: Fields)(implicit authContext: AuthContext): Future[CaseTemplate] = createSrv[CaseTemplateModel, CaseTemplate](caseTemplateModel, fields) - def get(id: String)(implicit Context: AuthContext): Future[CaseTemplate] = + def get(id: String): Future[CaseTemplate] = getSrv[CaseTemplateModel, CaseTemplate](caseTemplateModel, id) def getByName(name: String): Future[CaseTemplate] = { diff --git a/thehive-backend/app/services/CustomWSAPI.scala b/thehive-backend/app/services/CustomWSAPI.scala index a35e237c68..64154b285f 100644 --- a/thehive-backend/app/services/CustomWSAPI.scala +++ b/thehive-backend/app/services/CustomWSAPI.scala @@ -1,39 +1,41 @@ package services -import javax.inject.Inject +import javax.inject.{ Inject, Singleton } -import akka.stream.Materializer import play.api.inject.ApplicationLifecycle -import play.api.libs.ws.ahc.{ AhcWSAPI, AhcWSClient, AhcWSClientConfig, AhcWSClientConfigParser } -import play.api.libs.ws.ssl.TrustStoreConfig import play.api.libs.ws._ +import play.api.libs.ws.ahc.{ AhcWSClient, AhcWSClientConfig, AhcWSClientConfigParser } import play.api.{ Configuration, Environment, Logger } +import akka.stream.Materializer +import com.typesafe.sslconfig.ssl.TrustStoreConfig + object CustomWSAPI { private[CustomWSAPI] lazy val logger = Logger(getClass) - def parseWSConfig(config: Configuration, environment: Environment): AhcWSClientConfig = { + def parseWSConfig(config: Configuration): AhcWSClientConfig = { new AhcWSClientConfigParser( - new WSConfigParser(config, environment).parse(), - config, - environment).parse() + new WSConfigParser(config.underlying, getClass.getClassLoader).parse(), + config.underlying, + getClass.getClassLoader).parse() } - def parseProxyConfig(config: Configuration): Option[WSProxyServer] = for { - proxyConfig ← config.getConfig("play.ws.proxy") - proxyHost ← proxyConfig.getString("host") - proxyPort ← proxyConfig.getInt("port") - proxyProtocol = proxyConfig.getString("protocol") - proxyPrincipal = proxyConfig.getString("user") - proxyPassword = proxyConfig.getString("password") - proxyNtlmDomain = proxyConfig.getString("ntlmDomain") - proxyEncoding = proxyConfig.getString("encoding") - proxyNonProxyHosts = proxyConfig.getStringSeq("nonProxyHosts") - } yield DefaultWSProxyServer(proxyHost, proxyPort, proxyProtocol, proxyPrincipal, proxyPassword, proxyNtlmDomain, proxyEncoding, proxyNonProxyHosts) + def parseProxyConfig(config: Configuration): Option[WSProxyServer] = + config.getOptional[Configuration]("play.ws.proxy").map { proxyConfig ⇒ + DefaultWSProxyServer( + proxyConfig.get[String]("host"), + proxyConfig.get[Int]("port"), + proxyConfig.getOptional[String]("protocol"), + proxyConfig.getOptional[String]("user"), + proxyConfig.getOptional[String]("password"), + proxyConfig.getOptional[String]("ntlmDomain"), + proxyConfig.getOptional[String]("encoding"), + proxyConfig.getOptional[Seq[String]]("nonProxyHosts")) + } - def getWS(config: Configuration, environment: Environment, lifecycle: ApplicationLifecycle, mat: Materializer): AhcWSAPI = { - val clientConfig = parseWSConfig(config, environment) - val clientConfigWithTruststore = config.getString("play.cert") match { + def getWS(config: Configuration)(implicit mat: Materializer): AhcWSClient = { + val clientConfig = parseWSConfig(config) + val clientConfigWithTruststore = config.getOptional[String]("play.cert") match { case Some(p) ⇒ logger.warn( """Use of "cert" parameter in configuration file is deprecated. Please use: @@ -48,36 +50,45 @@ object CustomWSAPI { """.stripMargin) clientConfig.copy( wsClientConfig = clientConfig.wsClientConfig.copy( - ssl = clientConfig.wsClientConfig.ssl.copy( - trustManagerConfig = clientConfig.wsClientConfig.ssl.trustManagerConfig.copy( - trustStoreConfigs = clientConfig.wsClientConfig.ssl.trustManagerConfig.trustStoreConfigs :+ TrustStoreConfig(filePath = Some(p.toString), data = None))))) + ssl = clientConfig.wsClientConfig.ssl.withTrustManagerConfig( + clientConfig.wsClientConfig.ssl.trustManagerConfig.withTrustStoreConfigs( + clientConfig.wsClientConfig.ssl.trustManagerConfig.trustStoreConfigs :+ TrustStoreConfig(filePath = Some(p.toString), data = None))))) case None ⇒ clientConfig } - new AhcWSAPI(environment, clientConfigWithTruststore, lifecycle)(mat) + AhcWSClient(clientConfigWithTruststore, None) } def getConfig(config: Configuration, path: String): Configuration = { Configuration( - config.getConfig(s"play.$path").getOrElse(Configuration.empty).underlying.withFallback( - config.getConfig(path).getOrElse(Configuration.empty).underlying)) + config.getOptional[Configuration](s"play.$path").getOrElse(Configuration.empty).underlying.withFallback( + config.getOptional[Configuration](path).getOrElse(Configuration.empty).underlying)) } } -class CustomWSAPI(ws: AhcWSAPI, val proxy: Option[WSProxyServer], config: Configuration, environment: Environment, lifecycle: ApplicationLifecycle, mat: Materializer) extends WSAPI { +@Singleton +class CustomWSAPI( + ws: AhcWSClient, + val proxy: Option[WSProxyServer], + config: Configuration, + environment: Environment, + lifecycle: ApplicationLifecycle, + mat: Materializer) extends WSClient { private[CustomWSAPI] lazy val logger = Logger(getClass) @Inject() def this(config: Configuration, environment: Environment, lifecycle: ApplicationLifecycle, mat: Materializer) = this( - CustomWSAPI.getWS(config, environment, lifecycle, mat), + CustomWSAPI.getWS(config)(mat), CustomWSAPI.parseProxyConfig(config), config, environment, lifecycle, mat) + override def close(): Unit = ws.close() + override def url(url: String): WSRequest = { val req = ws.url(url) proxy.fold(req)(req.withProxyServer) } - override def client: AhcWSClient = ws.client + override def underlying[T]: T = ws.underlying[T] def withConfig(subConfig: Configuration): CustomWSAPI = { logger.debug(s"Override WS configuration using $subConfig") diff --git a/thehive-backend/app/services/FlowSrv.scala b/thehive-backend/app/services/FlowSrv.scala index 635a9f6411..ed4eb7ed84 100644 --- a/thehive-backend/app/services/FlowSrv.scala +++ b/thehive-backend/app/services/FlowSrv.scala @@ -2,28 +2,28 @@ package services import javax.inject.{ Inject, Singleton } -import scala.annotation.implicitNotFound import scala.concurrent.{ ExecutionContext, Future } +import play.api.Logger +import play.api.libs.json.JsValue.jsValueToJsLookup +import play.api.libs.json.{ JsObject, JsValue } + import akka.NotUsed import akka.stream.scaladsl.Source +import models.{ Audit, AuditModel } -import play.api.libs.json.{ JsObject, JsValue } -import play.api.libs.json.JsValue.jsValueToJsLookup - -import org.elastic4play.services.{ AuxSrv, FindSrv, ModelSrv, QueryDSL } +import org.elastic4play.services.{ AuxSrv, FindSrv, ModelSrv } import org.elastic4play.utils.RichJson -import models.{ Audit, AuditModel } -import play.api.Logger - +@Singleton class FlowSrv @Inject() ( auditModel: AuditModel, modelSrv: ModelSrv, auxSrv: AuxSrv, findSrv: FindSrv, implicit val ec: ExecutionContext) { - lazy val log = Logger(getClass) + + private[FlowSrv] lazy val logger = Logger(getClass) def apply(rootId: Option[String], count: Int): (Source[JsObject, NotUsed], Future[Long]) = { import org.elastic4play.services.QueryDSL._ diff --git a/thehive-backend/app/services/LocalAuthSrv.scala b/thehive-backend/app/services/LocalAuthSrv.scala index ad0641d3f3..5ff83fe9ee 100644 --- a/thehive-backend/app/services/LocalAuthSrv.scala +++ b/thehive-backend/app/services/LocalAuthSrv.scala @@ -2,19 +2,18 @@ package services import javax.inject.{ Inject, Singleton } -import scala.annotation.implicitNotFound import scala.concurrent.{ ExecutionContext, Future } import scala.util.Random import play.api.libs.json.{ JsObject, JsString } import play.api.mvc.RequestHeader -import org.elastic4play.{ AuthenticationError, AuthorizationError } +import models.{ User, UserModel } + import org.elastic4play.controllers.Fields import org.elastic4play.services.{ AuthCapability, AuthContext, AuthSrv, UpdateSrv } import org.elastic4play.utils.Hasher - -import models.{ User, UserModel } +import org.elastic4play.{ AuthenticationError, AuthorizationError } @Singleton class LocalAuthSrv @Inject() ( diff --git a/thehive-backend/app/services/LogSrv.scala b/thehive-backend/app/services/LogSrv.scala index 2d080edaaf..15a5c5636f 100644 --- a/thehive-backend/app/services/LogSrv.scala +++ b/thehive-backend/app/services/LogSrv.scala @@ -3,12 +3,15 @@ package services import javax.inject.{ Inject, Singleton } import scala.concurrent.{ ExecutionContext, Future } + +import play.api.libs.json.JsObject + import akka.NotUsed import akka.stream.scaladsl.Source -import org.elastic4play.controllers.Fields -import org.elastic4play.services.{ Agg, AuthContext, CreateSrv, DeleteSrv, FindSrv, GetSrv, QueryDef, UpdateSrv } import models.{ Log, LogModel, Task, TaskModel } -import play.api.libs.json.JsObject + +import org.elastic4play.controllers.Fields +import org.elastic4play.services._ @Singleton class LogSrv @Inject() ( @@ -28,7 +31,7 @@ class LogSrv @Inject() ( def create(task: Task, fields: Fields)(implicit authContext: AuthContext): Future[Log] = createSrv[LogModel, Log, Task](logModel, task, fields) - def get(id: String)(implicit Context: AuthContext): Future[Log] = + def get(id: String): Future[Log] = getSrv[LogModel, Log](logModel, id) def update(id: String, fields: Fields)(implicit Context: AuthContext): Future[Log] = diff --git a/thehive-backend/app/services/StreamMessage.scala b/thehive-backend/app/services/StreamMessage.scala index a210e6f1dc..b8dfb7de77 100644 --- a/thehive-backend/app/services/StreamMessage.scala +++ b/thehive-backend/app/services/StreamMessage.scala @@ -1,17 +1,12 @@ package services -import javax.inject.Singleton - -import scala.annotation.elidable -import scala.annotation.elidable.ASSERTION -import scala.annotation.implicitNotFound import scala.concurrent.{ ExecutionContext, Future } -import play.api.libs.json.{ JsObject, Json } +import play.api.Logger import play.api.libs.json.Json.toJsFieldJsValueWrapper +import play.api.libs.json.{ JsObject, Json } import org.elastic4play.services.{ AuditOperation, AuxSrv, MigrationEvent } -import play.api.Logger trait StreamMessageGroup[M] { def :+(message: M): StreamMessageGroup[M] @@ -54,7 +49,7 @@ case class AuditOperationGroup( } object AuditOperationGroup { - lazy val log = Logger(getClass) + private[AuditOperationGroup] lazy val logger = Logger(classOf[AuditOperationGroup]) def apply(auxSrv: AuxSrv, operation: AuditOperation)(implicit ec: ExecutionContext): AuditOperationGroup = { val auditedAttributes = JsObject { @@ -69,7 +64,7 @@ object AuditOperationGroup { val obj = auxSrv(operation.entity, 10, withStats = false, removeUnaudited = true) .recover { case error ⇒ - log.error("auxSrv fails", error) + logger.error("auxSrv fails", error) JsObject(Nil) } new AuditOperationGroup( diff --git a/thehive-backend/app/services/StreamSrv.scala b/thehive-backend/app/services/StreamSrv.scala index 61706ce738..d19f3f9a01 100644 --- a/thehive-backend/app/services/StreamSrv.scala +++ b/thehive-backend/app/services/StreamSrv.scala @@ -2,37 +2,43 @@ package services import javax.inject.{ Inject, Singleton } -import scala.concurrent.{ ExecutionContext, Future } import scala.concurrent.duration.FiniteDuration - -import akka.actor.{ ActorLogging, ActorRef, ActorSystem, Cancellable, DeadLetter, PoisonPill, actorRef2Scala } -import akka.actor.Actor -import akka.actor.ActorDSL.{ Act, actor } -import akka.stream.Materializer +import scala.concurrent.{ ExecutionContext, Future } import play.api.Logger import play.api.libs.json.JsObject import play.api.mvc.{ Filter, RequestHeader, Result } -import org.elastic4play.services.{ AuditOperation, AuxSrv, EndOfMigrationEvent, EventMessage, EventSrv, MigrationEvent } +import akka.actor.{ Actor, ActorLogging, ActorRef, ActorSystem, Cancellable, DeadLetter, PoisonPill, actorRef2Scala } +import akka.stream.Materializer + +import org.elastic4play.services._ import org.elastic4play.utils.Instance /** * This actor monitors dead messages and log them */ @Singleton -class StreamMonitor @Inject() (implicit val system: ActorSystem) { - lazy val logger = Logger(getClass) - val monitorActor: ActorRef = actor(new Act { - become { - case DeadLetter(StreamActor.GetOperations, sender, recipient) ⇒ - logger.warn(s"receive dead GetOperations message, $sender -> $recipient") - sender ! StreamActor.StreamNotFound - case other ⇒ - logger.error(s"receive dead message : $other") - } - }) - system.eventStream.subscribe(monitorActor, classOf[DeadLetter]) +class DeadLetterMonitoringActor @Inject() (system: ActorSystem) extends Actor { + private[DeadLetterMonitoringActor] lazy val logger = Logger(getClass) + + override def preStart(): Unit = { + system.eventStream.subscribe(self, classOf[DeadLetter]) + super.preStart() + } + + override def postStop(): Unit = { + system.eventStream.unsubscribe(self) + super.postStop() + } + + override def receive: Receive = { + case DeadLetter(StreamActor.GetOperations, sender, recipient) ⇒ + logger.warn(s"receive dead GetOperations message, $sender -> $recipient") + sender ! StreamActor.StreamNotFound + case other ⇒ + logger.error(s"receive dead message : $other") + } } object StreamActor { @@ -56,10 +62,10 @@ class StreamActor( globalMaxWait: FiniteDuration, eventSrv: EventSrv, auxSrv: AuxSrv) extends Actor with ActorLogging { - import services.StreamActor._ import context.dispatcher + import services.StreamActor._ - lazy val logger = Logger(getClass) + private[StreamActor] lazy val logger = Logger(getClass) private object FakeCancellable extends Cancellable { def cancel() = true @@ -204,7 +210,7 @@ class StreamFilter @Inject() ( implicit val mat: Materializer, implicit val ec: ExecutionContext) extends Filter { - val log = Logger(getClass) + private[StreamFilter] lazy val logger = Logger(getClass) def apply(nextFilter: RequestHeader ⇒ Future[Result])(requestHeader: RequestHeader): Future[Result] = { val requestId = Instance.getRequestId(requestHeader) eventSrv.publish(StreamActor.Initialize(requestId)) diff --git a/thehive-backend/app/services/TaskSrv.scala b/thehive-backend/app/services/TaskSrv.scala index b22120fd01..79be66344f 100644 --- a/thehive-backend/app/services/TaskSrv.scala +++ b/thehive-backend/app/services/TaskSrv.scala @@ -5,16 +5,15 @@ import javax.inject.{ Inject, Singleton } import scala.concurrent.{ ExecutionContext, Future } import scala.util.Try +import play.api.libs.json.{ JsBoolean, JsObject } + import akka.NotUsed import akka.stream.Materializer import akka.stream.scaladsl.{ Sink, Source } - -import play.api.libs.json.{ JsBoolean, JsObject } +import models._ import org.elastic4play.controllers.Fields -import org.elastic4play.services.{ Agg, AuthContext, CreateSrv, DeleteSrv, FindSrv, GetSrv, QueryDef, UpdateSrv } - -import models.{ Case, CaseModel, Task, TaskModel, TaskStatus } +import org.elastic4play.services._ @Singleton class TaskSrv @Inject() ( diff --git a/thehive-backend/app/services/UserSrv.scala b/thehive-backend/app/services/UserSrv.scala index ec759bcb37..4eb5cee75f 100644 --- a/thehive-backend/app/services/UserSrv.scala +++ b/thehive-backend/app/services/UserSrv.scala @@ -2,17 +2,19 @@ package services import javax.inject.{ Inject, Provider, Singleton } +import scala.concurrent.{ ExecutionContext, Future } + +import play.api.mvc.RequestHeader + import akka.NotUsed import akka.stream.scaladsl.Source import models.{ User, UserModel, UserStatus } + import org.elastic4play.controllers.Fields import org.elastic4play.database.DBIndex import org.elastic4play.services._ import org.elastic4play.utils.Instance import org.elastic4play.{ AuthenticationError, AuthorizationError } -import play.api.mvc.RequestHeader - -import scala.concurrent.{ ExecutionContext, Future } @Singleton class UserSrv @Inject() ( @@ -44,7 +46,7 @@ class UserSrv @Inject() ( override def getInitialUser(request: RequestHeader): Future[AuthContext] = dbIndex.getSize(userModel.name).map { - case size if size > 0 ⇒ throw AuthenticationError(s"Not authenticated") + case size if size > 0 ⇒ throw AuthenticationError(s"Use of initial user is forbidden because users exist in database") case _ ⇒ AuthContextImpl("init", "", Instance.getRequestId(request), Seq(Role.admin, Role.read)) } diff --git a/thehive-backend/build.sbt b/thehive-backend/build.sbt index 88036d9d43..7c0b5a551e 100644 --- a/thehive-backend/build.sbt +++ b/thehive-backend/build.sbt @@ -3,11 +3,13 @@ import Dependencies._ libraryDependencies ++= Seq( Library.Play.cache, Library.Play.ws, + Library.Play.ahc, Library.Play.filters, + Library.Play.guice, Library.scalaGuice, Library.elastic4play, Library.zip4j, - "org.reflections" % "reflections" % "0.9.10" + Library.reflections ) enablePlugins(PlayScala) diff --git a/thehive-cortex/app/connectors/cortex/CortexConnector.scala b/thehive-cortex/app/connectors/cortex/CortexConnector.scala index a1b0385578..9bf2cb6691 100644 --- a/thehive-cortex/app/connectors/cortex/CortexConnector.scala +++ b/thehive-cortex/app/connectors/cortex/CortexConnector.scala @@ -1,21 +1,24 @@ package connectors.cortex +import play.api.libs.concurrent.AkkaGuiceSupport import play.api.{ Configuration, Environment, Logger } import connectors.ConnectorModule -import connectors.cortex.controllers.CortextCtrl +import connectors.cortex.controllers.CortexCtrl +import connectors.cortex.services.JobReplicateActor class CortexConnector( environment: Environment, - configuration: Configuration) extends ConnectorModule { - val log = Logger(getClass) + configuration: Configuration) extends ConnectorModule with AkkaGuiceSupport { + private[CortexConnector] lazy val logger = Logger(getClass) def configure() { try { - registerController[CortextCtrl] + registerController[CortexCtrl] + bindActor[JobReplicateActor]("JobReplicateActor") } catch { - case t: Throwable ⇒ log.error("Corte connector is disabled because its configuration is invalid", t) + case t: Throwable ⇒ logger.error("Cortex connector is disabled because its configuration is invalid", t) } } } diff --git a/thehive-cortex/app/connectors/cortex/controllers/CortextCtrl.scala b/thehive-cortex/app/connectors/cortex/controllers/CortexCtrl.scala similarity index 92% rename from thehive-cortex/app/connectors/cortex/controllers/CortextCtrl.scala rename to thehive-cortex/app/connectors/cortex/controllers/CortexCtrl.scala index 5afb1708fb..96e8037309 100644 --- a/thehive-cortex/app/connectors/cortex/controllers/CortextCtrl.scala +++ b/thehive-cortex/app/connectors/cortex/controllers/CortexCtrl.scala @@ -3,23 +3,25 @@ package connectors.cortex.controllers import javax.inject.{ Inject, Singleton } import scala.concurrent.ExecutionContext + import play.api.Logger import play.api.http.Status import play.api.libs.json.{ JsObject, Json } -import play.api.mvc.{ Action, AnyContent, Controller } +import play.api.mvc._ import play.api.routing.SimpleRouter import play.api.routing.sird.{ DELETE, GET, PATCH, POST, UrlContext } + import org.elastic4play.{ BadRequestError, NotFoundError, Timed } import org.elastic4play.controllers.{ Authenticated, Fields, FieldsBodyParser, Renderer } import org.elastic4play.models.JsonFormat.baseModelEntityWrites import org.elastic4play.services.{ AuxSrv, QueryDSL, QueryDef, Role } import org.elastic4play.services.JsonFormat.queryReads import connectors.Connector -import connectors.cortex.models.JsonFormat.{ analyzerFormats, cortexJobFormat } +import connectors.cortex.models.JsonFormat.analyzerFormats import connectors.cortex.services.{ CortexConfig, CortexSrv } @Singleton -class CortextCtrl @Inject() ( +class CortexCtrl @Inject() ( reportTemplateCtrl: ReportTemplateCtrl, cortexConfig: CortexConfig, cortexSrv: CortexSrv, @@ -27,10 +29,14 @@ class CortextCtrl @Inject() ( authenticated: Authenticated, fieldsBodyParser: FieldsBodyParser, renderer: Renderer, - implicit val ec: ExecutionContext) extends Controller with Connector with Status { + components: ControllerComponents, + implicit val ec: ExecutionContext) extends AbstractController(components) with Connector with Status { + val name = "cortex" - val log = Logger(getClass) + private[CortexCtrl] lazy val logger = Logger(getClass) + override val status: JsObject = Json.obj("enabled" → true, "servers" → cortexConfig.instances.map(_.name)) + val router = SimpleRouter { case POST(p"/job") ⇒ createJob case GET(p"/job/$jobId<[^/]*>") ⇒ getJob(jobId) diff --git a/thehive-cortex/app/connectors/cortex/controllers/ReportTemplateCtrl.scala b/thehive-cortex/app/connectors/cortex/controllers/ReportTemplateCtrl.scala index 144968d4b0..90797523fc 100644 --- a/thehive-cortex/app/connectors/cortex/controllers/ReportTemplateCtrl.scala +++ b/thehive-cortex/app/connectors/cortex/controllers/ReportTemplateCtrl.scala @@ -2,18 +2,20 @@ package connectors.cortex.controllers import javax.inject.{ Inject, Singleton } -import scala.collection.JavaConversions.asScalaBuffer +import scala.collection.JavaConverters._ import scala.concurrent.{ ExecutionContext, Future } import scala.io.Source import scala.util.control.NonFatal + import akka.stream.Materializer import akka.stream.scaladsl.Sink import play.api.Logger import play.api.http.Status import play.api.libs.json.{ JsBoolean, JsObject } -import play.api.mvc.{ Action, AnyContent, Controller } +import play.api.mvc._ + import org.elastic4play.{ BadRequestError, Timed } -import org.elastic4play.controllers.{ Authenticated, Fields, FieldsBodyParser, FileInputValue, Renderer } +import org.elastic4play.controllers._ import org.elastic4play.models.JsonFormat.baseModelEntityWrites import org.elastic4play.services.{ QueryDSL, QueryDef, Role } import org.elastic4play.services.AuxSrv @@ -29,10 +31,11 @@ class ReportTemplateCtrl @Inject() ( authenticated: Authenticated, renderer: Renderer, fieldsBodyParser: FieldsBodyParser, + components: ControllerComponents, implicit val ec: ExecutionContext, - implicit val mat: Materializer) extends Controller with Status { + implicit val mat: Materializer) extends AbstractController(components) with Status { - lazy val logger = Logger(getClass) + private[ReportTemplateCtrl] lazy val logger = Logger(getClass) @Timed def create: Action[Fields] = authenticated(Role.admin).async(fieldsBodyParser) { implicit request ⇒ @@ -89,10 +92,10 @@ class ReportTemplateCtrl @Inject() ( @Timed def importTemplatePackage: Action[Fields] = authenticated(Role.write).async(fieldsBodyParser) { implicit request ⇒ val zipFile = request.body.get("templates") match { - case Some(FileInputValue(name, filepath, contentType)) ⇒ new ZipFile(filepath.toFile) - case _ ⇒ throw BadRequestError("") + case Some(FileInputValue(_, filepath, _)) ⇒ new ZipFile(filepath.toFile) + case _ ⇒ throw BadRequestError("") } - val importedReportTemplates: Seq[Future[(String, JsBoolean)]] = zipFile.getFileHeaders.toSeq.filter(_ != null).collect { + val importedReportTemplates: Seq[Future[(String, JsBoolean)]] = zipFile.getFileHeaders.asScala.filter(_ != null).collect { case fileHeader: FileHeader if !fileHeader.isDirectory ⇒ val Array(analyzerId, reportTypeHtml, _*) = (fileHeader.getFileName + "/").split("/", 3) val inputStream = zipFile.getInputStream(fileHeader) diff --git a/thehive-cortex/app/connectors/cortex/models/ReportTemplate.scala b/thehive-cortex/app/connectors/cortex/models/ReportTemplate.scala index b2cdf1b170..b2422a86f8 100644 --- a/thehive-cortex/app/connectors/cortex/models/ReportTemplate.scala +++ b/thehive-cortex/app/connectors/cortex/models/ReportTemplate.scala @@ -5,7 +5,6 @@ import javax.inject.{ Inject, Singleton } import play.api.libs.json.JsObject import org.elastic4play.models.{ AttributeDef, AttributeFormat ⇒ F, AttributeOption ⇒ O, EntityDef, ModelDef } -import org.elastic4play.BadRequestError import org.elastic4play.models.BaseEntity import play.api.libs.json.JsString import scala.concurrent.Future diff --git a/thehive-cortex/app/connectors/cortex/services/CortexClient.scala b/thehive-cortex/app/connectors/cortex/services/CortexClient.scala index a3ca683cc7..9a74b8943f 100644 --- a/thehive-cortex/app/connectors/cortex/services/CortexClient.scala +++ b/thehive-cortex/app/connectors/cortex/services/CortexClient.scala @@ -6,6 +6,7 @@ import connectors.cortex.models.{ Analyzer, CortexArtifact, DataArtifact, FileAr 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 services.CustomWSAPI @@ -19,7 +20,7 @@ class CortexClient(val name: String, baseUrl: String, key: String, authenticatio logger.info(s"new Cortex($name, $baseUrl, $key) Basic Auth enabled: ${authentication.isDefined}") def request[A](uri: String, f: WSRequest ⇒ Future[WSResponse], t: WSResponse ⇒ A)(implicit ec: ExecutionContext): Future[A] = { - val requestBuilder = ws.url(s"$baseUrl/$uri").withHeaders("auth" → key) + val requestBuilder = ws.url(s"$baseUrl/$uri").withHttpHeaders("auth" → key) val authenticatedRequestBuilder = authentication.fold(requestBuilder) { case (username, password) ⇒ requestBuilder.withAuth(username, password, WSAuthScheme.BASIC) } @@ -70,6 +71,6 @@ class CortexClient(val name: String, baseUrl: String, key: String, authenticatio } def waitReport(jobId: String, atMost: Duration)(implicit ec: ExecutionContext): Future[JsObject] = { - request(s"api/job/$jobId/waitreport", _.withQueryString("atMost" → atMost.toString).get, r ⇒ r.json.as[JsObject]) + request(s"api/job/$jobId/waitreport", _.withQueryStringParameters("atMost" → atMost.toString).get, r ⇒ r.json.as[JsObject]) } } diff --git a/thehive-cortex/app/connectors/cortex/services/CortexSrv.scala b/thehive-cortex/app/connectors/cortex/services/CortexSrv.scala index 96b2e0e7e1..4d25208792 100644 --- a/thehive-cortex/app/connectors/cortex/services/CortexSrv.scala +++ b/thehive-cortex/app/connectors/cortex/services/CortexSrv.scala @@ -4,13 +4,13 @@ import java.util.Date import javax.inject.{ Inject, Singleton } import akka.NotUsed -import akka.actor.ActorDSL.{ Act, actor } -import akka.actor.ActorSystem +import akka.actor.Actor import akka.stream.Materializer import akka.stream.scaladsl.{ Sink, Source } import connectors.cortex.models.JsonFormat._ import connectors.cortex.models._ import models.Artifact + import org.elastic4play.controllers.Fields import org.elastic4play.services._ import org.elastic4play.services.JsonFormat.attachmentFormat @@ -18,33 +18,32 @@ 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 } +import services.{ ArtifactSrv, CustomWSAPI, MergeArtifact } import scala.concurrent.duration.DurationInt import scala.concurrent.{ ExecutionContext, Future } -import scala.language.implicitConversions import scala.util.{ Failure, Success, Try } object CortexConfig { def getCortexClient(name: String, configuration: Configuration, ws: CustomWSAPI): Option[CortexClient] = { - val url = configuration.getString("url").getOrElse(sys.error("url is missing")).replaceFirst("/*$", "") + val url = configuration.getOptional[String]("url").getOrElse(sys.error("url is missing")).replaceFirst("/*$", "") val key = "" // configuration.getString("key").getOrElse(sys.error("key is missing")) val authentication = for { - basicEnabled ← configuration.getBoolean("basicAuth") + basicEnabled ← configuration.getOptional[Boolean]("basicAuth") if basicEnabled - username ← configuration.getString("username") - password ← configuration.getString("password") + username ← configuration.getOptional[String]("username") + password ← configuration.getOptional[String]("password") } yield username → password Some(new CortexClient(name, url, key, authentication, ws)) } def getInstances(configuration: Configuration, globalWS: CustomWSAPI): Seq[CortexClient] = { for { - cfg ← configuration.getConfig("cortex").toSeq + cfg ← configuration.getOptional[Configuration]("cortex").toSeq cortexWS = globalWS.withConfig(cfg) key ← cfg.subKeys if key != "ws" - c ← cfg.getConfig(key) + c ← cfg.getOptional[Configuration](key) instanceWS = cortexWS.withConfig(c) cic ← getCortexClient(key, c, instanceWS) } yield cic @@ -59,6 +58,34 @@ case class CortexConfig(instances: Seq[CortexClient]) { CortexConfig.getInstances(configuration, globalWS)) } +@Singleton +class JobReplicateActor @Inject() ( + cortexSrv: CortexSrv, + eventSrv: EventSrv, + implicit val mat: Materializer) extends Actor { + + override def preStart(): Unit = { + eventSrv.subscribe(self, classOf[MergeArtifact]) + super.preStart() + } + + override def postStop(): Unit = { + eventSrv.unsubscribe(self) + super.postStop() + } + + override def receive: Receive = { + case MergeArtifact(newArtifact, artifacts, authContext) ⇒ + 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) + } + .runWith(Sink.ignore) + } +} + @Singleton class CortexSrv @Inject() ( cortexConfig: CortexConfig, @@ -70,10 +97,8 @@ class CortexSrv @Inject() ( updateSrv: UpdateSrv, findSrv: FindSrv, userSrv: UserSrv, - eventSrv: EventSrv, implicit val ws: WSClient, implicit val ec: ExecutionContext, - implicit val system: ActorSystem, implicit val mat: Materializer) { private[CortexSrv] lazy val logger = Logger(getClass) @@ -99,22 +124,7 @@ class CortexSrv @Inject() ( } } - private[CortexSrv] val mergeActor = actor(new Act { - become { - case MergeArtifact(newArtifact, artifacts, authContext) ⇒ - import org.elastic4play.services.QueryDSL._ - 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") - create(newArtifact, baseFields)(authContext) - } - .runWith(Sink.ignore) - } - }) - - eventSrv.subscribe(mergeActor, classOf[MergeArtifact]) // need to unsubsribe ? - - private[CortexSrv] def create(artifact: Artifact, fields: Fields)(implicit authContext: AuthContext): Future[Job] = { + private[services] def create(artifact: Artifact, fields: Fields)(implicit authContext: AuthContext): Future[Job] = { createSrv[JobModel, Job, Artifact](jobModel, artifact, fields.set("artifactId", artifact.id)) } diff --git a/thehive-cortex/app/connectors/cortex/services/ReportTemplateSrv.scala b/thehive-cortex/app/connectors/cortex/services/ReportTemplateSrv.scala index 16886a03c9..ee056b3cf0 100644 --- a/thehive-cortex/app/connectors/cortex/services/ReportTemplateSrv.scala +++ b/thehive-cortex/app/connectors/cortex/services/ReportTemplateSrv.scala @@ -28,7 +28,7 @@ class ReportTemplateSrv @Inject() ( findSrv: FindSrv, implicit val ec: ExecutionContext) { - lazy val log = Logger(getClass) + private[ReportTemplateSrv] lazy val logger = Logger(getClass) def create(fields: Fields)(implicit authContext: AuthContext): Future[ReportTemplate] = { createSrv[ReportTemplateModel, ReportTemplate](reportTemplateModel, fields) diff --git a/thehive-cortex/build.sbt b/thehive-cortex/build.sbt index d0bc335d4f..a0b122b542 100644 --- a/thehive-cortex/build.sbt +++ b/thehive-cortex/build.sbt @@ -2,6 +2,8 @@ import Dependencies._ libraryDependencies ++= Seq( Library.Play.ws, + Library.Play.guice, + Library.Play.ahc, Library.elastic4play, Library.zip4j ) diff --git a/thehive-metrics/app/connectors/metrics/Influxdb.scala b/thehive-metrics/app/connectors/metrics/Influxdb.scala index acf98c6015..ad57ed0426 100644 --- a/thehive-metrics/app/connectors/metrics/Influxdb.scala +++ b/thehive-metrics/app/connectors/metrics/Influxdb.scala @@ -1,11 +1,10 @@ package connectors.metrics import java.util -import java.util.SortedMap import java.util.concurrent.TimeUnit import javax.inject.{ Inject, Singleton } -import scala.collection.JavaConversions.{ iterableAsScalaIterable, mapAsScalaMap } +import scala.collection.JavaConverters._ import scala.concurrent.ExecutionContext import play.api.Logger import play.api.libs.ws.WSClient @@ -52,21 +51,21 @@ trait InfluxDBAPI { class InfluxDBFactory @Inject() ( ws: WSClient, implicit val ec: ExecutionContext) { - val log = Logger("InfluxDB") + private[InfluxDBFactory] lazy val logger = Logger(classOf[InfluxDB]) case class InfluxDB(url: String, user: String, password: String, database: String, retentionPolicy: String) extends InfluxDBAPI { def send(points: InfluxPoint*): Unit = { - val x = ws + ws .url(url.stripSuffix("/") + "/write") - .withQueryString( + .withQueryStringParameters( "u" → user, "p" → password, "db" → database, "rp" → retentionPolicy) - .withHeaders("Content-Type" → "text/plain") + .withHttpHeaders("Content-Type" → "text/plain") .post(points.map(_.lineProtocol).mkString("\n")) .map { response ⇒ if ((response.status / 100) != 2) - log.warn(s"Send metrics to InfluxDB error : ${response.body}") + logger.warn(s"Send metrics to InfluxDB error : ${response.body}") } () } @@ -90,11 +89,11 @@ class InfluxDBReporter( val now = System.currentTimeMillis() * 1000000 - val points = gauges.map { case (name, gauge) ⇒ pointGauge(now, name, gauge) } ++ - counters.map { case (name, counter) ⇒ pointCounter(now, tags, name, counter) } ++ - histograms.map { case (name, histogram) ⇒ pointHistogram(now, tags, name, histogram) } ++ - meters.map { case (name, meter) ⇒ pointMeter(now, tags, name, meter) } ++ - timers.map { case (name, timer) ⇒ pointTimer(now, tags, name, timer) } + val points = gauges.asScala.map { case (name, gauge) ⇒ pointGauge(now, name, gauge) } ++ + counters.asScala.map { case (name, counter) ⇒ pointCounter(now, tags, name, counter) } ++ + histograms.asScala.map { case (name, histogram) ⇒ pointHistogram(now, tags, name, histogram) } ++ + meters.asScala.map { case (name, meter) ⇒ pointMeter(now, tags, name, meter) } ++ + timers.asScala.map { case (name, timer) ⇒ pointTimer(now, tags, name, timer) } influxdb.send(points.toSeq: _*) } @@ -108,7 +107,7 @@ class InfluxDBReporter( def pointGauge(now: Long, name: String, gauge: Gauge[_]): InfluxPoint = { val value = gauge.getValue match { case s: String ⇒ InfluxString(s) - case i: java.lang.Iterable[_] ⇒ InfluxString(i.mkString(",")) + case i: java.lang.Iterable[_] ⇒ InfluxString(i.asScala.mkString(",")) case d: Double ⇒ InfluxFloat(d) case f: Float ⇒ InfluxFloat(f.toDouble) case l: Long ⇒ InfluxLong(l) diff --git a/thehive-metrics/app/connectors/metrics/MetricsCtrl.scala b/thehive-metrics/app/connectors/metrics/MetricsCtrl.scala index e7448e8b09..1fd0ac365d 100644 --- a/thehive-metrics/app/connectors/metrics/MetricsCtrl.scala +++ b/thehive-metrics/app/connectors/metrics/MetricsCtrl.scala @@ -1,19 +1,20 @@ package connectors.metrics import java.io.StringWriter - import javax.inject.{ Inject, Singleton } -import play.api.mvc.{ Action, Controller } +import play.api.mvc.{ AbstractController, ControllerComponents, DefaultActionBuilder } import play.api.routing.SimpleRouter import play.api.routing.sird.{ GET, UrlContext } import org.elastic4play.Timed - import connectors.Connector @Singleton -class MetricsCtrl @Inject() (metricsModule: Metrics) extends Controller with Connector { +class MetricsCtrl @Inject() ( + metricsModule: Metrics, + actionBuilder: DefaultActionBuilder, + components: ControllerComponents) extends AbstractController(components) with Connector { val name = "metrics" @@ -22,7 +23,7 @@ class MetricsCtrl @Inject() (metricsModule: Metrics) extends Controller with Con } @Timed("controllers.MetricsCtrl.stats") - def stats = Action { + def stats = actionBuilder { val writer = metricsModule.mapper.writerWithDefaultPrettyPrinter val stringWriter = new StringWriter() writer.writeValue(stringWriter, metricsModule.registry) diff --git a/thehive-metrics/app/connectors/metrics/MetricsModule.scala b/thehive-metrics/app/connectors/metrics/MetricsModule.scala index a1609e0028..82ac8b0314 100644 --- a/thehive-metrics/app/connectors/metrics/MetricsModule.scala +++ b/thehive-metrics/app/connectors/metrics/MetricsModule.scala @@ -3,15 +3,14 @@ package connectors.metrics import java.util.concurrent.TimeUnit import javax.inject.{ Inject, Provider, Singleton } -import akka.util.ByteString - -import scala.concurrent.Future +import scala.concurrent.{ ExecutionContext, Future } import scala.concurrent.duration.{ DurationLong, FiniteDuration } import scala.language.implicitConversions + import play.api.{ Configuration, Environment, Logger } import play.api.inject.ApplicationLifecycle -import play.api.libs.concurrent.Execution.Implicits.defaultContext import play.api.mvc._ + import org.aopalliance.intercept.{ MethodInterceptor, MethodInvocation } import net.codingwell.scalaguice.ScalaMultibinder import com.codahale.metrics._ @@ -21,26 +20,26 @@ import com.codahale.metrics.jvm.{ GarbageCollectorMetricSet, MemoryUsageGaugeSet import com.codahale.metrics.logback.InstrumentedAppender import com.fasterxml.jackson.databind.ObjectMapper import com.google.inject.matcher.Matchers + import org.elastic4play.Timed import ch.qos.logback.classic import connectors.ConnectorModule import info.ganglia.gmetric4j.gmetric.GMetric -import play.api.libs.streams.Accumulator trait UnitConverter { - val validUnits = Some(TimeUnit.values.map(_.toString).toSet) + val validUnits: Set[Option[String]] = TimeUnit.values.map(v ⇒ Some(v.toString)).toSet + None implicit def stringToTimeUnit(s: String): TimeUnit = TimeUnit.valueOf(s) } case class GraphiteMetricConfig(host: String, port: Int, prefix: String, rateUnit: TimeUnit, durationUnit: TimeUnit, interval: FiniteDuration) object GraphiteMetricConfig extends UnitConverter { def apply(configuration: Configuration): GraphiteMetricConfig = { - val host = configuration.getString("metrics.graphite.host").getOrElse("127.0.0.1") - val port = configuration.getInt("metrics.graphite.port").getOrElse(2003) - val prefix = configuration.getString("metrics.graphite.prefix").getOrElse("thehive") - val rateUnit = configuration.getString("metrics.graphite.rateUnit", validUnits).getOrElse("SECONDS") - val durationUnit = configuration.getString("metrics.graphite.durationUnit", validUnits).getOrElse("MILLISECONDS") - val interval = configuration.getMilliseconds("metrics.graphite.period").getOrElse(10000L).millis // 10 seconds + val host = configuration.getOptional[String]("metrics.graphite.host").getOrElse("127.0.0.1") + val port = configuration.getOptional[Int]("metrics.graphite.port").getOrElse(2003) + val prefix = configuration.getOptional[String]("metrics.graphite.prefix").getOrElse("thehive") + val rateUnit = configuration.getAndValidate[Option[String]]("metrics.graphite.rateUnit", validUnits).getOrElse("SECONDS") + val durationUnit = configuration.getAndValidate[Option[String]]("metrics.graphite.durationUnit", validUnits).getOrElse("MILLISECONDS") + val interval = configuration.getOptional[FiniteDuration]("metrics.graphite.period").getOrElse(10.seconds) GraphiteMetricConfig(host, port, prefix, rateUnit, durationUnit, interval) } } @@ -48,18 +47,18 @@ object GraphiteMetricConfig extends UnitConverter { case class GangliaMetricConfig(host: String, port: Int, prefix: String, mode: String, ttl: Int, version: Boolean, rateUnit: TimeUnit, durationUnit: TimeUnit, tMax: Int, dMax: Int, interval: FiniteDuration) object GangliaMetricConfig extends UnitConverter { def apply(configuration: Configuration): GangliaMetricConfig = { - val host = configuration.getString("metrics.ganglia.host").getOrElse("127.0.0.1") - val port = configuration.getInt("metrics.ganglia.port").getOrElse(8649) - val prefix = configuration.getString("metrics.ganglia.prefix").getOrElse("thehive") - val validMode = Some(GMetric.UDPAddressingMode.values().map(_.toString).toSet) - val mode = configuration.getString("metrics.ganglia.mode", validMode).getOrElse("UNICAST") - val ttl = configuration.getInt("metrics.ganglia.ttl").getOrElse(1) - val version = configuration.getString("metrics.ganglia.version", Some(Set("3.0", "3.1"))).forall(_ == 3.1) - val rateUnit = configuration.getString("metrics.ganglia.rateUnit", validUnits).getOrElse("SECONDS") - val durationUnit = configuration.getString("metrics.ganglia.durationUnit", validUnits).getOrElse("MILLISECONDS") - val tMax = configuration.getInt("metrics.ganglia.tmax").getOrElse(60) - val dMax = configuration.getInt("metrics.ganglia.dmax").getOrElse(0) - val interval = configuration.getMilliseconds("metrics.ganglia.period").getOrElse(10000L).millis // 10 seconds + val host = configuration.getOptional[String]("metrics.ganglia.host").getOrElse("127.0.0.1") + val port = configuration.getOptional[Int]("metrics.ganglia.port").getOrElse(8649) + val prefix = configuration.getOptional[String]("metrics.ganglia.prefix").getOrElse("thehive") + val validMode = GMetric.UDPAddressingMode.values().map(v ⇒ Some(v.toString)).toSet + None + val mode = configuration.getAndValidate[Option[String]]("metrics.ganglia.mode", validMode).getOrElse("UNICAST") + val ttl = configuration.getOptional[Int]("metrics.ganglia.ttl").getOrElse(1) + val version = configuration.getAndValidate[Option[String]]("metrics.ganglia.version", Set(Some("3.0"), Some("3.1"), None)).forall(_ == 3.1) + val rateUnit = configuration.getAndValidate[Option[String]]("metrics.ganglia.rateUnit", validUnits).getOrElse("SECONDS") + val durationUnit = configuration.getAndValidate[Option[String]]("metrics.ganglia.durationUnit", validUnits).getOrElse("MILLISECONDS") + val tMax = configuration.getOptional[Int]("metrics.ganglia.tmax").getOrElse(60) + val dMax = configuration.getOptional[Int]("metrics.ganglia.dmax").getOrElse(0) + val interval = configuration.getOptional[FiniteDuration]("metrics.ganglia.period").getOrElse(10.seconds) GangliaMetricConfig(host, port, prefix, mode, ttl, version, rateUnit, durationUnit, tMax, dMax, interval) } } @@ -67,15 +66,15 @@ object GangliaMetricConfig extends UnitConverter { case class InfluxMetricConfig(url: String, user: String, password: String, database: String, retention: String, tags: Map[String, String], interval: FiniteDuration) object InfluxMetricConfig { def apply(configuration: Configuration): InfluxMetricConfig = { - val url = configuration.getString("metrics.influx.url").getOrElse("http://127.0.0.1:8086") - val user = configuration.getString("meorg.aopalliance.intercept.MethodInterceptortrics.influx.user").getOrElse("root") - val password = configuration.getString("metrics.influx.password").getOrElse("root") - val database = configuration.getString("metrics.influx.database").getOrElse("thehive") - val retention = configuration.getString("metrics.influx.retention").getOrElse("default") - val tags = configuration.getConfig("metrics.influx.tags").fold(Map.empty[String, String]) { cfg ⇒ + val url = configuration.getOptional[String]("metrics.influx.url").getOrElse("http://127.0.0.1:8086") + val user = configuration.getOptional[String]("meorg.aopalliance.intercept.MethodInterceptortrics.influx.user").getOrElse("root") + val password = configuration.getOptional[String]("metrics.influx.password").getOrElse("root") + val database = configuration.getOptional[String]("metrics.influx.database").getOrElse("thehive") + val retention = configuration.getOptional[String]("metrics.influx.retention").getOrElse("default") + val tags = configuration.getOptional[Configuration]("metrics.influx.tags").fold(Map.empty[String, String]) { cfg ⇒ cfg.entrySet.toMap.mapValues(_.render) } - val interval = configuration.getMilliseconds("metrics.influx.period").getOrElse(10000L).millis // 10 seconds + val interval = configuration.getOptional[FiniteDuration]("metrics.influx.period").getOrElse(10.seconds) InfluxMetricConfig(url, user, password, database, retention, tags, interval) } } @@ -84,20 +83,26 @@ case class MetricConfig(registryName: String, rateUnit: TimeUnit, durationUnit: object MetricConfig extends UnitConverter { def apply(configuration: Configuration): MetricConfig = { - val registryName = configuration.getString("metrics.name").getOrElse("default") - val rateUnit = configuration.getString("metrics.rateUnit", validUnits).getOrElse("SECONDS") - val durationUnit = configuration.getString("metrics.durationUnit", validUnits).getOrElse("SECONDS") - val jvm = configuration.getBoolean("metrics.jvm").getOrElse(true) - val logback = configuration.getBoolean("metrics.logback").getOrElse(true) - - val graphiteMetricConfig = configuration.getBoolean("metrics.graphite.enabled").filter(identity).map(_ ⇒ GraphiteMetricConfig(configuration)) - val gangliaMetricConfig = configuration.getBoolean("metrics.ganglia.enabled").filter(identity).map(_ ⇒ GangliaMetricConfig(configuration)) - val influxMetricConfig = configuration.getBoolean("metrics.influx.enabled").filter(identity).map(_ ⇒ InfluxMetricConfig(configuration)) + val registryName = configuration.getOptional[String]("metrics.name").getOrElse("default") + val rateUnit = configuration.getAndValidate[Option[String]]("metrics.rateUnit", validUnits).getOrElse("SECONDS") + val durationUnit = configuration.getAndValidate[Option[String]]("metrics.durationUnit", validUnits).getOrElse("SECONDS") + val jvm = configuration.getOptional[Boolean]("metrics.jvm").getOrElse(true) + val logback = configuration.getOptional[Boolean]("metrics.logback").getOrElse(true) + + val graphiteMetricConfig = configuration.getOptional[Boolean]("metrics.graphite.enabled").filter(identity).map(_ ⇒ GraphiteMetricConfig(configuration)) + val gangliaMetricConfig = configuration.getOptional[Boolean]("metrics.ganglia.enabled").filter(identity).map(_ ⇒ GangliaMetricConfig(configuration)) + val influxMetricConfig = configuration.getOptional[Boolean]("metrics.influx.enabled").filter(identity).map(_ ⇒ InfluxMetricConfig(configuration)) MetricConfig(registryName, rateUnit, durationUnit, jvm, logback, graphiteMetricConfig, gangliaMetricConfig, influxMetricConfig) } } -class TimedInterceptor @Inject() (metricsProvider: Provider[Metrics]) extends MethodInterceptor { +@Singleton +class TimedInterceptor @Inject() ( + actionBuilderProvider: Provider[DefaultActionBuilder], + metricsProvider: Provider[Metrics], + ecProvider: Provider[ExecutionContext]) extends MethodInterceptor { + implicit lazy val ec = ecProvider.get + lazy val actionBuilder = actionBuilderProvider.get override def invoke(invocation: MethodInvocation): AnyRef = { val timerName = invocation.getStaticPart.getAnnotation(classOf[Timed]).value match { case "" ⇒ @@ -110,14 +115,12 @@ class TimedInterceptor @Inject() (metricsProvider: Provider[Metrics]) extends Me case f: Future[_] ⇒ f.onComplete { _ ⇒ timer.stop() } f - case action: Action[x] ⇒ new Action[x] { - def apply(request: Request[x]) = { - val result = action.apply(request) - result.onComplete { _ ⇒ timer.stop() } - result - } - lazy val parser = action.parser + case action: Action[x] ⇒ actionBuilder.async(action.parser) { (request: Request[x]) ⇒ + val result: Future[Result] = action.apply(request) + result.onComplete { _ ⇒ timer.stop() } + result } + case o ⇒ timer.stop() o @@ -129,13 +132,15 @@ class MetricsModule( environment: Environment, configuration: Configuration) extends ConnectorModule { def configure(): Unit = { - if (configuration.getBoolean("metrics.enabled").getOrElse(false)) { + if (configuration.getOptional[Boolean]("metrics.enabled").getOrElse(false)) { bind[MetricConfig].toInstance(MetricConfig(configuration)) bind[Metrics].asEagerSingleton() val filterBindings = ScalaMultibinder.newSetBinder[Filter](binder) filterBindings.addBinding.to[MetricsFilterImpl] - - bindInterceptor(Matchers.any, Matchers.annotatedWith(classOf[org.elastic4play.Timed]), new TimedInterceptor(getProvider[Metrics])) + bindInterceptor( + Matchers.any, + Matchers.annotatedWith(classOf[org.elastic4play.Timed]), + new TimedInterceptor(getProvider[DefaultActionBuilder], getProvider[Metrics], getProvider[ExecutionContext])) registerController[MetricsCtrl] } () @@ -158,10 +163,16 @@ class Metrics @Inject() (configuration: Configuration, metricConfig: MetricConfi if (metricConfig.logback) { val appender: InstrumentedAppender = new InstrumentedAppender(registry) - val logger = Logger.logger.asInstanceOf[classic.Logger] - appender.setContext(logger.getLoggerContext) - appender.start() - logger.addAppender(appender) + val logger = Logger(getClass) + + logger.underlyingLogger match { + case cl: classic.Logger ⇒ + appender.setContext(cl.getLoggerContext) + appender.start() + cl.addAppender(appender) + case l ⇒ + logger.error(s"Can't initialize logger metrics. Logger (${l.getClass} is not a logback classic logger).") + } } mapper.registerModule(new json.MetricsModule(metricConfig.rateUnit, metricConfig.durationUnit, false)) diff --git a/thehive-misp/app/connectors/misp/MispConnector.scala b/thehive-misp/app/connectors/misp/MispConnector.scala index bab80ac1b3..d560f51a19 100644 --- a/thehive-misp/app/connectors/misp/MispConnector.scala +++ b/thehive-misp/app/connectors/misp/MispConnector.scala @@ -2,6 +2,7 @@ package connectors.misp import javax.inject.Singleton +import play.api.libs.concurrent.AkkaGuiceSupport import play.api.{ Configuration, Environment, Logger } import connectors.ConnectorModule @@ -9,18 +10,17 @@ import connectors.ConnectorModule @Singleton class MispConnector( environment: Environment, - configuration: Configuration) extends ConnectorModule { - val log = Logger(getClass) + configuration: Configuration) extends ConnectorModule with AkkaGuiceSupport { + private[MispConnector] lazy val logger = Logger(getClass) def configure() { try { - // val mispConfig = MispConfig(configuration) - // bind[MispConfig].toInstance(mispConfig) bind[MispSrv].asEagerSingleton() + bindActor[UpdateMispAlertArtifactActor]("UpdateMispAlertArtifactActor") registerController[MispCtrl] } catch { - case t: Throwable ⇒ log.error("MISP connector is disabled because its configuration is invalid", t) + case t: Throwable ⇒ logger.error("MISP connector is disabled because its configuration is invalid", t) } } } \ No newline at end of file diff --git a/thehive-misp/app/connectors/misp/MispCtrl.scala b/thehive-misp/app/connectors/misp/MispCtrl.scala index b1f3c9179b..405f22db23 100644 --- a/thehive-misp/app/connectors/misp/MispCtrl.scala +++ b/thehive-misp/app/connectors/misp/MispCtrl.scala @@ -4,6 +4,7 @@ import javax.inject.{ Inject, Singleton } import connectors.Connector import models.{ Alert, Case, UpdateMispAlertArtifact } + import org.elastic4play.JsonFormat.tryWrites import org.elastic4play.controllers.Authenticated import org.elastic4play.models.JsonFormat.baseModelEntityWrites @@ -12,11 +13,11 @@ import org.elastic4play.{ NotFoundError, Timed } import play.api.Logger import play.api.http.Status import play.api.libs.json.Json -import play.api.mvc.{ Action, AnyContent, Controller } +import play.api.mvc._ import play.api.routing.SimpleRouter import play.api.routing.sird.{ GET, UrlContext } -import services.AlertTransformer +import services.AlertTransformer import scala.concurrent.{ ExecutionContext, Future } @Singleton @@ -24,7 +25,8 @@ class MispCtrl @Inject() ( mispSrv: MispSrv, authenticated: Authenticated, eventSrv: EventSrv, - implicit val ec: ExecutionContext) extends Controller with Connector with Status with AlertTransformer { + components: ControllerComponents, + implicit val ec: ExecutionContext) extends AbstractController(components) with Connector with Status with AlertTransformer { override val name: String = "misp" diff --git a/thehive-misp/app/connectors/misp/MispSrv.scala b/thehive-misp/app/connectors/misp/MispSrv.scala index 6a5f4636d4..eb50c27564 100644 --- a/thehive-misp/app/connectors/misp/MispSrv.scala +++ b/thehive-misp/app/connectors/misp/MispSrv.scala @@ -4,8 +4,7 @@ import java.util.Date import javax.inject.{ Inject, Provider, Singleton } import akka.NotUsed -import akka.actor.ActorDSL._ -import akka.actor.ActorSystem +import akka.actor.{ Actor, ActorSystem } import akka.stream.Materializer import akka.stream.scaladsl.{ FileIO, Sink, Source } import connectors.misp.JsonFormat._ @@ -13,6 +12,7 @@ import models._ import net.lingala.zip4j.core.ZipFile import net.lingala.zip4j.exception.ZipException import net.lingala.zip4j.model.FileHeader + import org.elastic4play.controllers.{ Fields, FileInputValue } import org.elastic4play.services.{ UserSrv ⇒ _, _ } import org.elastic4play.utils.RichJson @@ -23,37 +23,38 @@ import play.api.libs.json.JsValue.jsValueToJsLookup import play.api.libs.json.Json.toJsFieldJsValueWrapper import play.api.libs.json._ import play.api.{ Configuration, Environment, Logger } -import services._ +import play.api.libs.ws.WSBodyWritables.writeableOf_JsValue +import services._ import scala.collection.immutable -import scala.concurrent.duration.{ DurationInt, DurationLong, FiniteDuration } +import scala.concurrent.duration.{ DurationInt, FiniteDuration } import scala.concurrent.{ ExecutionContext, Future } import scala.util.{ Failure, Success, Try } class MispConfig(val interval: FiniteDuration, val connections: Seq[MispConnection]) { def this(configuration: Configuration, defaultCaseTemplate: Option[String], globalWS: CustomWSAPI) = this( - configuration.getMilliseconds("misp.interval").fold(1.hour)(_.millis), + configuration.getOptional[FiniteDuration]("misp.interval").getOrElse(1.hour), for { - cfg ← configuration.getConfig("misp").toSeq + cfg ← configuration.getOptional[Configuration]("misp").toSeq mispWS = globalWS.withConfig(cfg) - defaultArtifactTags = cfg.getStringSeq("tags").getOrElse(Nil) + defaultArtifactTags = cfg.getOptional[Seq[String]]("tags").getOrElse(Nil) name ← cfg.subKeys - mispConnectionConfig ← Try(cfg.getConfig(name)).toOption.flatten.toSeq - url ← mispConnectionConfig.getString("url") - key ← mispConnectionConfig.getString("key") + mispConnectionConfig ← Try(cfg.get[Configuration](name)).toOption.toSeq + url ← mispConnectionConfig.getOptional[String]("url") + key ← mispConnectionConfig.getOptional[String]("key") instanceWS = mispWS.withConfig(mispConnectionConfig) - artifactTags = mispConnectionConfig.getStringSeq("tags").getOrElse(defaultArtifactTags) - caseTemplate = mispConnectionConfig.getString("caseTemplate").orElse(defaultCaseTemplate) + artifactTags = mispConnectionConfig.getOptional[Seq[String]]("tags").getOrElse(defaultArtifactTags) + caseTemplate = mispConnectionConfig.getOptional[String]("caseTemplate").orElse(defaultCaseTemplate) } yield MispConnection(name, url, key, instanceWS, caseTemplate, artifactTags)) @Inject def this(configuration: Configuration, httpSrv: CustomWSAPI) = this( configuration, - configuration.getString("misp.caseTemplate"), + configuration.getOptional[String]("misp.caseTemplate"), httpSrv) } @@ -76,12 +77,54 @@ case class MispConnection( private[misp] def apply(url: String) = ws.url(s"$baseUrl/$url") - .withHeaders( + .withHttpHeaders( "Authorization" → key, "Accept" → "application/json") } +/** + * This actor listens message from migration (message UpdateMispAlertArtifact) which indicates that artifacts in + * MISP event must be retrieved in inserted in alerts. + * + * @param eventSrv event bus used to receive migration message + * @param userSrv user service used to do operations on database without real user request + * @param mispSrv misp service to invoke artifact update action + * @param ec execution context + */ +class UpdateMispAlertArtifactActor @Inject() ( + eventSrv: EventSrv, + userSrv: UserSrv, + mispSrv: MispSrv, + implicit val ec: ExecutionContext) extends Actor { + + private[UpdateMispAlertArtifactActor] lazy val logger = Logger(getClass) + override def preStart(): Unit = { + eventSrv.subscribe(self, classOf[UpdateMispAlertArtifact]) + super.preStart() + } + + override def postStop(): Unit = { + eventSrv.unsubscribe(self) + super.postStop() + } + + override def receive: Receive = { + case UpdateMispAlertArtifact() ⇒ + logger.info("UpdateMispAlertArtifact") + userSrv + .inInitAuthContext { implicit authContext ⇒ + mispSrv.updateMispAlertArtifact() + } + .onComplete { + case Success(_) ⇒ logger.info("Artifacts in MISP alerts updated") + case Failure(error) ⇒ logger.error("Update MISP alert artifacts error :", error) + } + () + case msg ⇒ + logger.info(s"Receiving unexpected message: $msg (${msg.getClass})") + } +} @Singleton class MispSrv @Inject() ( mispConfig: MispConfig, @@ -100,7 +143,7 @@ class MispSrv @Inject() ( implicit val materializer: Materializer, implicit val ec: ExecutionContext) { - private[misp] val logger = Logger(getClass) + private[misp] lazy val logger = Logger(getClass) private[misp] lazy val alertSrv = alertSrvProvider.get private[misp] def getInstanceConfig(name: String): Future[MispConnection] = mispConfig.connections @@ -109,7 +152,7 @@ class MispSrv @Inject() ( Future.successful(instanceConfig) } - private[misp] def initScheduler() = { + private[misp] def initScheduler(): Unit = { val task = system.scheduler.schedule(0.seconds, mispConfig.interval) { if (migrationSrv.isReady) { logger.info("Update of MISP events is starting ...") @@ -138,24 +181,6 @@ class MispSrv @Inject() ( } initScheduler() - eventSrv.subscribe(actor(new Act { - become { - case UpdateMispAlertArtifact() ⇒ - logger.info("UpdateMispAlertArtifact") - userSrv - .inInitAuthContext { implicit authContext ⇒ - updateMispAlertArtifact() - } - .onComplete { - case Success(_) ⇒ logger.info("Artifacts in MISP alerts updated") - case Failure(error) ⇒ logger.error("Update MISP alert artifacts error :", error) - } - () - case msg ⇒ - logger.info(s"Receiving unexpected message: $msg (${msg.getClass})") - } - }), classOf[UpdateMispAlertArtifact]) - logger.info("subscribe actor") def synchronize()(implicit authContext: AuthContext): Future[Seq[Try[Alert]]] = { import org.elastic4play.services.QueryDSL._ @@ -435,7 +460,7 @@ class MispSrv @Inject() ( } def extractMalwareAttachment(file: FileInputValue)(implicit authContext: AuthContext): FileInputValue = { - import scala.collection.JavaConversions._ + import scala.collection.JavaConverters._ try { val zipFile = new ZipFile(file.filepath.toFile) @@ -443,7 +468,7 @@ class MispSrv @Inject() ( zipFile.setPassword("infected") // Get the list of file headers from the zip file - val fileHeaders = zipFile.getFileHeaders.toList.asInstanceOf[List[FileHeader]] + val fileHeaders = zipFile.getFileHeaders.asScala.toList.asInstanceOf[List[FileHeader]] val (fileNameHeaders, contentFileHeaders) = fileHeaders.partition { fileHeader ⇒ fileHeader.getFileName.endsWith(".filename.txt") } @@ -486,22 +511,19 @@ class MispSrv @Inject() ( .withMethod("GET") .stream() .flatMap { - case response if response.headers.status != 200 ⇒ - val status = response.headers.status - response.body.runWith(Sink.headOption).flatMap { body ⇒ - val message = body.fold("")(_.decodeString("UTF-8")) - logger.warn(s"MISP attachment $attachmentId can't be downloaded (status $status) : $message") - Future.failed(InternalError(s"MISP attachment $attachmentId can't be downloaded (status $status)")) - } + case response if response.status != 200 ⇒ + val status = response.status + logger.warn(s"MISP attachment $attachmentId can't be downloaded (status $status) : ${response.body}") + Future.failed(InternalError(s"MISP attachment $attachmentId can't be downloaded (status $status)")) case response ⇒ val tempFile = tempSrv.newTemporaryFile("misp_attachment", attachmentId) - response.body + response.bodyAsSource .runWith(FileIO.toPath(tempFile)) .map { ioResult ⇒ if (!ioResult.wasSuccessful) // throw an exception if transfer failed throw ioResult.getError - val contentType = response.headers.headers.getOrElse("Content-Type", Seq("application/octet-stream")).head - val filename = response.headers.headers + val contentType = response.headers.getOrElse("Content-Type", Seq("application/octet-stream")).head + val filename = response.headers .get("Content-Disposition") .flatMap(_.collectFirst { case fileNameExtractor(name) ⇒ name }) .getOrElse("noname") diff --git a/thehive-misp/build.sbt b/thehive-misp/build.sbt index f7064e3fc1..59c831777f 100644 --- a/thehive-misp/build.sbt +++ b/thehive-misp/build.sbt @@ -2,6 +2,8 @@ import Dependencies._ libraryDependencies ++= Seq( Library.Play.ws, + Library.Play.guice, + Library.Play.ahc, Library.zip4j, Library.elastic4play ) From fae95cbea28fe60b258f5add83abbd937592be7c Mon Sep 17 00:00:00 2001 From: To-om Date: Thu, 24 Aug 2017 11:01:22 +0200 Subject: [PATCH 04/49] #291 Add basic authentication to Stream API --- .../app/controllers/StreamCtrl.scala | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/thehive-backend/app/controllers/StreamCtrl.scala b/thehive-backend/app/controllers/StreamCtrl.scala index 3ba62fa597..b2a109244c 100644 --- a/thehive-backend/app/controllers/StreamCtrl.scala +++ b/thehive-backend/app/controllers/StreamCtrl.scala @@ -21,7 +21,7 @@ import services.StreamActor.StreamMessages import org.elastic4play.controllers._ import org.elastic4play.services.{ AuxSrv, EventSrv, MigrationSrv, Role } -import org.elastic4play.{ AuthenticationError, Timed } +import org.elastic4play.Timed @Singleton class StreamCtrl( @@ -98,16 +98,17 @@ class StreamCtrl( Future.successful(BadRequest("Invalid stream id")) } else { - val status = authenticated.expirationStatus(request) match { - case ExpirationError if !migrationSrv.isMigrating ⇒ throw AuthenticationError("Not authenticated") - case _: ExpirationWarning ⇒ 220 - case _ ⇒ OK - + val futureStatus = authenticated.expirationStatus(request) match { + case ExpirationError if !migrationSrv.isMigrating ⇒ authenticated.getFromApiKey(request).map(_ ⇒ OK) + case _: ExpirationWarning ⇒ Future.successful(220) + case _ ⇒ Future.successful(OK) } - (system.actorSelection(s"/user/stream-$id") ? StreamActor.GetOperations) map { - case StreamMessages(operations) ⇒ renderer.toOutput(status, operations) - case m ⇒ InternalServerError(s"Unexpected message : $m (${m.getClass})") + futureStatus.flatMap { status ⇒ + (system.actorSelection(s"/user/stream-$id") ? StreamActor.GetOperations) map { + case StreamMessages(operations) ⇒ renderer.toOutput(status, operations) + case m ⇒ InternalServerError(s"Unexpected message : $m (${m.getClass})") + } } } } From b55e9420d7251196f5e11a1ca742c83b54bc9325 Mon Sep 17 00:00:00 2001 From: To-om Date: Thu, 24 Aug 2017 13:41:28 +0200 Subject: [PATCH 05/49] #20 Add initial webhook feature Audit trail elements are sent in JSON format to an URL using http POST. A new configuration section "webhooks" has been added. The format is: webhooks { webhook-name-1 { url = "http://my.webhook.url" } webhook-name-2 { url = "http://my.other.webhook.url" } } HTTP client configuration (timeout, proxy, SSL, ...) can be configured in each level like MISP and Cortex configuration. --- thehive-backend/app/services/AuditSrv.scala | 26 ++++++++------ thehive-backend/app/services/WebHook.scala | 40 +++++++++++++++++++++ 2 files changed, 56 insertions(+), 10 deletions(-) create mode 100644 thehive-backend/app/services/WebHook.scala diff --git a/thehive-backend/app/services/AuditSrv.scala b/thehive-backend/app/services/AuditSrv.scala index 3e62d3aff8..4603969a3c 100644 --- a/thehive-backend/app/services/AuditSrv.scala +++ b/thehive-backend/app/services/AuditSrv.scala @@ -5,7 +5,7 @@ import javax.inject.{ Inject, Singleton } import scala.concurrent.ExecutionContext import play.api.Logger -import play.api.libs.json.{ JsBoolean, JsObject, Json } +import play.api.libs.json.{ JsObject, Json } import akka.actor.Actor import models.{ Audit, AuditModel } @@ -39,7 +39,9 @@ class AuditActor @Inject() ( auditModel: AuditModel, createSrv: CreateSrv, eventSrv: EventSrv, + webHooks: WebHooks, implicit val ec: ExecutionContext) extends Actor { + object EntityExtractor { def unapply(e: BaseEntity) = Some((e.model, e.id, e.routing)) } @@ -61,16 +63,20 @@ class AuditActor @Inject() ( currentRequestIds = currentRequestIds - Instance.getRequestId(request) case AuditOperation(EntityExtractor(model: AuditedModel, id, routing), action, details, authContext, date) ⇒ val requestId = authContext.requestId - createSrv[AuditModel, Audit](auditModel, Fields.empty - .set("operation", action.toString) - .set("details", model.selectAuditedAttributes(details)) - .set("objectType", model.name) - .set("objectId", id) - .set("base", JsBoolean(!currentRequestIds.contains(requestId))) - .set("startDate", Json.toJson(date)) - .set("rootId", routing) - .set("requestId", requestId))(authContext) + val audit = Json.obj( + "operation" → action, + "details" → model.selectAuditedAttributes(details), + "objectType" → model.name, + "objectId" → id, + "base" → !currentRequestIds.contains(requestId), + "startDate" → date, + "rootId" → routing, + "requestId" → requestId) + + createSrv[AuditModel, Audit](auditModel, Fields(audit))(authContext) .failed.foreach(t ⇒ logger.error("Audit error", t)) currentRequestIds = currentRequestIds + requestId + + webHooks.send(audit) } } \ No newline at end of file diff --git a/thehive-backend/app/services/WebHook.scala b/thehive-backend/app/services/WebHook.scala new file mode 100644 index 0000000000..3ef78d89fb --- /dev/null +++ b/thehive-backend/app/services/WebHook.scala @@ -0,0 +1,40 @@ +package services + +import javax.inject.Inject + +import scala.concurrent.ExecutionContext +import scala.util.{ Failure, Success, Try } + +import play.api.{ Configuration, Logger } +import play.api.libs.json.JsObject +import play.api.libs.ws.WSRequest + +case class WebHook(name: String, ws: WSRequest)(implicit ec: ExecutionContext) { + private[WebHook] lazy val logger = Logger(getClass.getName + "." + name) + + def send(obj: JsObject) = ws.post(obj).onComplete { + case Success(resp) if resp.status / 100 != 2 ⇒ logger.error(s"WebHook returns status ${resp.status} ${resp.statusText}") + case Failure(error) ⇒ logger.error("WebHook call error", error) + case _ ⇒ + } +} + +class WebHooks( + webhooks: Seq[WebHook]) { + @Inject() def this( + configuration: Configuration, + globalWS: CustomWSAPI, + ec: ExecutionContext) = { + this( + for { + cfg ← configuration.getOptional[Configuration]("webhooks").toSeq + whWS = globalWS.withConfig(cfg) + name ← cfg.subKeys + whConfig ← Try(cfg.get[Configuration](name)).toOption + url ← whConfig.getOptional[String]("url") + instanceWS = whWS.withConfig(whConfig).url(url) + } yield WebHook(name, instanceWS)(ec)) + } + + def send(obj: JsObject): Unit = webhooks.foreach(_.send(obj)) +} From 7696dedc97c520c5a1c5ab73cbdad6c1a507363b Mon Sep 17 00:00:00 2001 From: To-om Date: Mon, 28 Aug 2017 14:16:16 +0200 Subject: [PATCH 06/49] #52 Add export status in case stats. Add MISP server in TheHive status --- thehive-backend/app/models/Case.scala | 23 +- .../app/connectors/misp/JsonFormat.scala | 3 +- .../app/connectors/misp/MispCtrl.scala | 16 +- .../app/connectors/misp/MispModel.scala | 17 +- .../app/connectors/misp/MispSrv.scala | 253 ++++++++++-------- 5 files changed, 182 insertions(+), 130 deletions(-) diff --git a/thehive-backend/app/models/Case.scala b/thehive-backend/app/models/Case.scala index 8fe9d83d55..f4df086725 100644 --- a/thehive-backend/app/models/Case.scala +++ b/thehive-backend/app/models/Case.scala @@ -4,6 +4,7 @@ import java.util.Date import javax.inject.{ Inject, Provider, Singleton } import models.JsonFormat.{ caseImpactStatusFormat, caseResolutionStatusFormat, caseStatusFormat } + import org.elastic4play.JsonFormat.dateFormat import org.elastic4play.models.{ AttributeDef, BaseEntity, EntityDef, HiveEnumeration, ModelDef, AttributeFormat ⇒ F, AttributeOption ⇒ O } import org.elastic4play.services.{ FindSrv, SequenceSrv } @@ -11,8 +12,8 @@ import play.api.Logger import play.api.libs.json.JsValue.jsValueToJsLookup import play.api.libs.json.Json.toJsFieldJsValueWrapper import play.api.libs.json._ -import services.{ AuditedModel, CaseSrv } +import services.{ AuditedModel, CaseSrv } import scala.concurrent.{ ExecutionContext, Future } import scala.math.BigDecimal.{ int2bigDecimal, long2bigDecimal } @@ -57,6 +58,7 @@ class CaseModel @Inject() ( artifactModel: Provider[ArtifactModel], taskModel: Provider[TaskModel], caseSrv: Provider[CaseSrv], + alertModel: Provider[AlertModel], sequenceSrv: SequenceSrv, findSrv: FindSrv, implicit val ec: ExecutionContext) extends ModelDef[CaseModel, Case]("case") with CaseAttributes with AuditedModel { caseModel ⇒ @@ -140,6 +142,22 @@ class CaseModel @Inject() ( case _ ⇒ Json.obj() } } + + private[models] def buildAlertStats(caze: Case): Future[JsObject] = { + import org.elastic4play.services.QueryDSL._ + findSrv( + alertModel.get, + "case" ~= caze.id, + groupByField("type", groupByField("source", selectCount))) + .map { alertStatsJson ⇒ + val alertStats = for { + (tpe, JsObject(srcStats)) ← alertStatsJson.value + src ← srcStats.keys + } yield Json.obj("type" → tpe, "source" → src) + Json.obj("alerts" → alertStats) + } + } + override def getStats(entity: BaseEntity): Future[JsObject] = { entity match { @@ -147,9 +165,10 @@ class CaseModel @Inject() ( for { taskStats ← buildTaskStats(caze) artifactStats ← buildArtifactStats(caze) + alertStats ← buildAlertStats(caze) mergeIntoStats ← buildMergeIntoStats(caze) mergeFromStats ← buildMergeFromStats(caze) - } yield taskStats ++ artifactStats ++ mergeIntoStats ++ mergeFromStats + } 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()) diff --git a/thehive-misp/app/connectors/misp/JsonFormat.scala b/thehive-misp/app/connectors/misp/JsonFormat.scala index 17dccc260e..cd5a3e4379 100644 --- a/thehive-misp/app/connectors/misp/JsonFormat.scala +++ b/thehive-misp/app/connectors/misp/JsonFormat.scala @@ -71,7 +71,6 @@ object JsonFormat { "category" → attribute.category, "type" → attribute.tpe, "value" → attribute.value.fold[String](identity, _.name), - "comment" → attribute.comment, - "status" → attribute.status) + "comment" → attribute.comment) } } diff --git a/thehive-misp/app/connectors/misp/MispCtrl.scala b/thehive-misp/app/connectors/misp/MispCtrl.scala index 6c58b7ecf8..65641ccb4c 100644 --- a/thehive-misp/app/connectors/misp/MispCtrl.scala +++ b/thehive-misp/app/connectors/misp/MispCtrl.scala @@ -4,32 +4,37 @@ import javax.inject.{ Inject, Singleton } import connectors.Connector import models.{ Alert, Case, UpdateMispAlertArtifact } + import org.elastic4play.JsonFormat.tryWrites -import org.elastic4play.controllers.Authenticated +import org.elastic4play.controllers.{ Authenticated, Renderer } import org.elastic4play.models.JsonFormat.baseModelEntityWrites import org.elastic4play.services._ import org.elastic4play.{ NotFoundError, Timed } import play.api.Logger import play.api.http.Status -import play.api.libs.json.Json +import play.api.libs.json.{ JsObject, Json } import play.api.mvc.{ Action, AnyContent, Controller } import play.api.routing.SimpleRouter import play.api.routing.sird.{ GET, UrlContext } + import services.{ AlertTransformer, CaseSrv } import connectors.misp.JsonFormat.exportedAttributeWrites - import scala.concurrent.{ ExecutionContext, Future } @Singleton class MispCtrl @Inject() ( mispSrv: MispSrv, + mispConfig: MispConfig, caseSrv: CaseSrv, authenticated: Authenticated, + renderer: Renderer, eventSrv: EventSrv, implicit val ec: ExecutionContext) extends Controller with Connector with Status with AlertTransformer { override val name: String = "misp" + override val status: JsObject = Json.obj("enabled" → true, "servers" → mispConfig.connections.map(_.name)) + private[MispCtrl] lazy val logger = Logger(getClass) val router = SimpleRouter { case GET(p"/_syncAlerts") ⇒ syncAlerts @@ -63,9 +68,8 @@ class MispCtrl @Inject() ( .get(caseId) .flatMap { caze ⇒ mispSrv.export(mispName, caze) } .map { - case (eventId, exportedAttributes) ⇒ Ok(Json.obj( - "eventId" → eventId, - "attributes" → exportedAttributes)) + case (_, exportedAttributes) ⇒ + renderer.toMultiOutput(CREATED, exportedAttributes) } } diff --git a/thehive-misp/app/connectors/misp/MispModel.scala b/thehive-misp/app/connectors/misp/MispModel.scala index c7d472eeab..40ec7a1110 100644 --- a/thehive-misp/app/connectors/misp/MispModel.scala +++ b/thehive-misp/app/connectors/misp/MispModel.scala @@ -2,6 +2,12 @@ package connectors.misp import java.util.Date +import play.api.libs.json.JsObject + +import models.Artifact + +import org.elastic4play.services.Attachment + case class MispAlert( source: String, sourceRef: String, @@ -22,4 +28,13 @@ case class MispAttribute( date: Date, comment: String, value: String, - tags: Seq[String]) \ No newline at end of file + tags: Seq[String]) + +case class ExportedMispAttribute( + artifact: Artifact, + tpe: String, + category: String, + value: Either[String, Attachment], + comment: Option[String]) + +case class MispExportError(message: String, artifact: Artifact) extends Exception(message) \ No newline at end of file diff --git a/thehive-misp/app/connectors/misp/MispSrv.scala b/thehive-misp/app/connectors/misp/MispSrv.scala index 37a25e8cd4..110950bd7f 100644 --- a/thehive-misp/app/connectors/misp/MispSrv.scala +++ b/thehive-misp/app/connectors/misp/MispSrv.scala @@ -1,8 +1,21 @@ package connectors.misp +import java.text.SimpleDateFormat import java.util.Date import javax.inject.{ Inject, Provider, Singleton } +import scala.collection.immutable +import scala.concurrent.duration.{ DurationInt, DurationLong, FiniteDuration } +import scala.concurrent.{ ExecutionContext, Future } +import scala.util.{ Failure, Success, Try } + +import play.api.inject.ApplicationLifecycle +import play.api.libs.json.JsLookupResult.jsLookupResultToJsLookup +import play.api.libs.json.JsValue.jsValueToJsLookup +import play.api.libs.json.Json.toJsFieldJsValueWrapper +import play.api.libs.json._ +import play.api.{ Configuration, Environment, Logger } + import akka.NotUsed import akka.actor.ActorDSL._ import akka.actor.ActorSystem @@ -13,23 +26,13 @@ import models._ import net.lingala.zip4j.core.ZipFile import net.lingala.zip4j.exception.ZipException import net.lingala.zip4j.model.FileHeader -import org.elastic4play.controllers.{ Fields, FileInputValue } -import org.elastic4play.services.{ UserSrv ⇒ _, _ } -import org.elastic4play.utils.RichJson -import org.elastic4play.{ ConflictError, InternalError, NotFoundError } -import play.api.inject.ApplicationLifecycle -import play.api.libs.json.JsLookupResult.jsLookupResultToJsLookup -import play.api.libs.json.JsValue.jsValueToJsLookup -import play.api.libs.json.Json.toJsFieldJsValueWrapper -import play.api.libs.json._ -import play.api.{ Configuration, Environment, Logger } import services._ -import java.text.SimpleDateFormat -import scala.collection.immutable -import scala.concurrent.duration.{ DurationInt, DurationLong, FiniteDuration } -import scala.concurrent.{ ExecutionContext, Future } -import scala.util.{ Failure, Success, Try } +import org.elastic4play.controllers.{ Fields, FileInputValue } +import org.elastic4play.models.JsonFormat.baseModelEntityWrites +import org.elastic4play.services.{ UserSrv ⇒ _, _ } +import org.elastic4play.utils.{ RichFuture, RichJson } +import org.elastic4play.{ InternalError, NotFoundError } class MispConfig(val interval: FiniteDuration, val connections: Seq[MispConnection]) { @@ -85,8 +88,6 @@ case class MispConnection( } -case class ExportedMispAttribute(tpe: String, category: String, value: Either[String, Attachment], comment: Option[String], status: String) - @Singleton class MispSrv @Inject() ( mispConfig: MispConfig, @@ -114,7 +115,7 @@ class MispSrv @Inject() ( Future.successful(instanceConfig) } - private[misp] def initScheduler() = { + private[misp] def initScheduler(): Unit = { val task = system.scheduler.schedule(0.seconds, mispConfig.interval) { if (migrationSrv.isReady) { logger.info("Update of MISP events is starting ...") @@ -528,7 +529,16 @@ class MispSrv @Inject() ( .runWith(Sink.seq) } - def export(mispName: String, caze: Case)(implicit authContext: AuthContext): Future[(String, Seq[ExportedMispAttribute])] = { + def relatedMispEvent(mispName: String, caseId: String): Future[(Option[String], Option[String])] = { + import org.elastic4play.services.QueryDSL._ + alertSrv.find(and("type" ~= "misp", "case" ~= caseId, "source" ~= mispName), Some("0-1"), Nil) + ._1 + .map { alert ⇒ alert.id → alert.sourceRef() } + .runWith(Sink.headOption) + .map(alertIdSource ⇒ alertIdSource.map(_._1) → alertIdSource.map(_._2)) + } + + def export(mispName: String, caze: Case)(implicit authContext: AuthContext): Future[(String, Seq[Try[Artifact]])] = { val mispConnection = mispConfig.getConnection(mispName).getOrElse(sys.error("MISP instance not found")) val dateFormat = new SimpleDateFormat("yy-MM-dd") @@ -544,124 +554,129 @@ class MispSrv @Inject() ( case (None, Some(attachment)) ⇒ Right(attachment) case _ ⇒ sys.error("???") } - ExportedMispAttribute(tpe, category, value, artifact.message(), "") + ExportedMispAttribute(artifact, tpe, category, value, artifact.message()) } .runWith(Sink.seq) } - def extractDuplicateAttributes(attributes: Seq[ExportedMispAttribute]): (Seq[ExportedMispAttribute], Seq[ExportedMispAttribute]) = { + def removeDuplicateAttributes(attributes: Seq[ExportedMispAttribute]): Seq[ExportedMispAttribute] = { val attrIndex = attributes.zipWithIndex - val (duplicateAttributes, uniqueAttributes) = attrIndex.partition { - case (ExportedMispAttribute(category, tpe, value, _, _), index) ⇒ attrIndex.exists { - case (ExportedMispAttribute(`category`, `tpe`, `value`, _, _), otherIndex) ⇒ otherIndex < index - case _ ⇒ false + attrIndex + .filter { + case (ExportedMispAttribute(_, category, tpe, value, _), index) ⇒ attrIndex.exists { + case (ExportedMispAttribute(_, `category`, `tpe`, `value`, _), otherIndex) ⇒ otherIndex >= index + case _ ⇒ true + } } - } - (duplicateAttributes.map(_._1.copy(status = "Duplicate")), uniqueAttributes.map(_._1)) + .map(_._1) } - def updateStatus(response: JsValue, attributes: Seq[ExportedMispAttribute]): Seq[ExportedMispAttribute] = { - val messages = (response \ "errors" \ "Attribute") - .asOpt[JsObject] - .getOrElse(JsObject(Nil)) - .fields - .toMap - .mapValues { m ⇒ - (m \ "value") - .asOpt[Seq[String]] - .flatMap(_.headOption) - .getOrElse(s"Unexpected message format: $m") + def createEvent(title: String, severity: String, date: Date, attributes: Seq[ExportedMispAttribute]): Future[(String, Seq[ExportedMispAttribute])] = { + val mispEvent = Json.obj( + "Event" → Json.obj( + "distribution" → 0, + "threat_level_id" → caze.severity().toString, + "analysis" → 0, + "info" → caze.title(), + "date" → dateFormat.format(caze.startDate()), + "published" → false, + "Attribute" → attributes)) + mispConnection("events") + .post(mispEvent) + .map { mispResponse ⇒ + val eventId = (mispResponse.json \ "Event" \ "id") + .asOpt[String] + .getOrElse(throw InternalError(s"Unexpected MISP response: ${mispResponse.status} ${mispResponse.statusText}\n${mispResponse.body}")) + val messages = (mispResponse.json \ "errors" \ "Attribute") + .asOpt[JsObject] + .getOrElse(JsObject(Nil)) + .fields + .toMap + .mapValues { m ⇒ + (m \ "value") + .asOpt[Seq[String]] + .flatMap(_.headOption) + .getOrElse(s"Unexpected message format: $m") + } + val exportedAttributes = attributes.zipWithIndex.collect { + case (attr, index) if !messages.contains(index.toString) ⇒ attr + } + eventId → exportedAttributes } + } + + def exportAttribute(eventId: String, attribute: ExportedMispAttribute): Future[Artifact] = { + val mispResponse = attribute match { + case ExportedMispAttribute(_, _, _, Right(attachment), comment) ⇒ + attachmentSrv + .source(attachment.id) + .runReduce(_ ++ _) + .flatMap { data ⇒ + val b64data = java.util.Base64.getEncoder.encodeToString(data.toArray[Byte]) + val body = Json.obj( + "request" → Json.obj( + "event_id" → eventId.toInt, + "category" → "Payload delivery", + "type" → "malware-sample", + "comment" → comment, + "files" → Json.arr( + Json.obj( + "filename" → attachment.name, + "data" → b64data)))) + mispConnection("events/upload_sample").post(body) + } + case attr ⇒ mispConnection(s"attributes/add/$eventId").post(Json.toJson(attr)) - attributes.zipWithIndex.map { - case (attr, index) ⇒ attr.copy(status = messages.getOrElse(index.toString, "Ok")) } - } - def exportFileAttribute(eventId: String, attribute: ExportedMispAttribute): Future[ExportedMispAttribute] = attribute match { - case ExportedMispAttribute(_, _, Right(attachment), comment, _) ⇒ - attachmentSrv - .source(attachment.id) - .runReduce(_ ++ _) - .flatMap { data ⇒ - val b64data = java.util.Base64.getEncoder.encodeToString(data.toArray[Byte]) - mispConnection("events/upload_sample").post(Json.obj( - "request" → Json.obj( - "event_id" → eventId.toInt, // FIXME add checks - "category" → "Payload delivery", - "type" → "malware-sample", - "comment" → comment, - "files" → Json.arr( - Json.obj( - "filename" → attachment.name, - "data" → b64data))))) - } - .map { response ⇒ - val name = (response.json \ "name") - .asOpt[String] - val status = name - .filter(_ == "Success") - .map(_ ⇒ "Ok") - .orElse((response.json \ "message").asOpt[String]) - .orElse(name) - .getOrElse(s"Unexpected MISP response: ${response.status} ${response.statusText}\n${response.body}") - attribute.copy(status = status, value = Left(attachment.name)) - } + mispResponse.map { + case response if response.status / 100 == 2 ⇒ attribute.artifact + case response ⇒ + val json = response.json + val message = (json \ "message").asOpt[String] + val error = (json \ "errors" \ "value").head.asOpt[String] + val errorMessage = for (m ← message; e ← error) yield s"$m $e" + throw MispExportError(errorMessage orElse message orElse error getOrElse s"Unexpected MISP response: ${response.status} ${response.statusText}\n${response.body}", attribute.artifact) + } } for { - exportStatus ← exportStatus(caze.id) - _ = println(s"exporting ${caze.id} in $mispName") - _ = println(s"exportStatus=$exportStatus") - _ ← exportStatus.find(_._1 == mispName).fold(Future.successful(()))(es ⇒ Future.failed(ConflictError(s"Case ${caze.id} has already been exported in MISP ${es._1}#${es._2}", JsObject(Nil)))) + (maybeAlertId, maybeEventId) ← relatedMispEvent(mispName, caze.id) attributes ← buildAttributeList() - (duplicateAttributes, uniqueAttributes) = extractDuplicateAttributes(attributes) - (simpleAttributes, fileAttributes) = uniqueAttributes.partition(_.value.isLeft) - simpleAttributeJson = simpleAttributes.map { attribute ⇒ - Json.obj( - "type" → attribute.tpe, - "category" → attribute.category, - "value" → attribute.value.left.get, - "comment" → attribute.comment) + uniqueAttributes = removeDuplicateAttributes(attributes) + simpleAttributes = uniqueAttributes.filter(_.value.isLeft) // FIXME used only if event doesn't exist + (eventId, existingAttributes) ← maybeEventId.fold { + // if no event is associated to this case, create a new one + createEvent(caze.title(), caze.severity().toString, caze.startDate(), simpleAttributes).map { + case (eventId, exportedAttributes) ⇒ eventId → exportedAttributes.map(_.value.left.get) + } + } { eventId ⇒ // if an event already exists, retrieve its attributes in order to export only new one + getAttributes(mispConnection, eventId, None).map { attributes ⇒ + eventId → attributes.map { attribute ⇒ + (attribute \ "data").asOpt[String].getOrElse((attribute \ "remoteAttachment" \ "filename").as[String]) + } + } } - - mispEvent = Json.obj( - "Event" → Json.obj( - "distribution" → 0, - "threat_level_id" → caze.severity().toString, - "analysis" → 0, - "info" → caze.title(), - "date" → dateFormat.format(caze.startDate()), - "published" → false, - "Attribute" → simpleAttributeJson - // "Tag" → tags - // "orgc_id" - // "org_id" - // "sharing_group_id" - )) - simpleAttributeResponse ← mispConnection("events").post(mispEvent) - eventId ← (simpleAttributeResponse.json \ "Event" \ "id") - .asOpt[String] - .fold[Future[String]](Future.failed(InternalError(s"Unexpected MISP response: ${simpleAttributeResponse.status} ${simpleAttributeResponse.statusText}\n${simpleAttributeResponse.body}")))(Future.successful) - fileAttributeStatus ← Future.traverse(fileAttributes)(attr ⇒ exportFileAttribute(eventId, attr)) - _ ← alertSrv.create(Fields.empty - .set("type", "misp") - .set("source", mispName) - .set("sourceRef", eventId) - .set("date", Json.toJson(caze.startDate())) - .set("lastSyncDate", Json.toJson(new Date(0))) - .set("case", caze.id) - .set("title", caze.title()) - .set("description", "Case have been exported to MISP") - .set("severity", JsNumber(caze.severity())) - .set("tags", Json.toJson(caze.tags())) - .set("tlp", JsNumber(caze.tlp())) - .set("artifacts", JsArray()) - .set("status", "Imported") - .set("follow", JsBoolean(false))) - exportedAttributes = updateStatus(simpleAttributeResponse.json, simpleAttributes) ++ duplicateAttributes ++ fileAttributeStatus - } yield eventId → exportedAttributes + newAttributes = uniqueAttributes.filterNot(attr ⇒ existingAttributes.contains(attr.value.fold(identity, _.name))) + exportedArtifact ← Future.traverse(newAttributes)(attr ⇒ exportAttribute(eventId, attr).toTry) + alertFields = Fields(Json.obj( + "type" → "misp", + "source" → mispName, + "sourceRef" → eventId, + "date" → caze.startDate(), + "lastSyncDate" → new Date(0), + "case" → caze.id, + "title" → caze.title(), + "description" → "Case have been exported to MISP", + "severity" → caze.severity(), + "tags" → caze.tags(), + "tlp" → caze.tlp(), + "artifacts" → uniqueAttributes.map(_.artifact), + "status" → "Imported", + "follow" → false)) + alert ← maybeAlertId.fold(alertSrv.create(alertFields))(alertId ⇒ alertSrv.update(alertId, alertFields)) + } yield alert.id → exportedArtifact } def convertAttribute(mispAttribute: MispAttribute): Seq[JsObject] = { From bb6e4d08019e649acfdaa29ec399672c69449b1b Mon Sep 17 00:00:00 2001 From: To-om Date: Mon, 28 Aug 2017 15:42:53 +0200 Subject: [PATCH 07/49] #52 Split MISP service in several files --- .../app/connectors/misp/MispConfig.scala | 39 ++ .../app/connectors/misp/MispConnection.scala | 30 + .../app/connectors/misp/MispConverter.scala | 205 ++++++ .../app/connectors/misp/MispCtrl.scala | 8 +- .../app/connectors/misp/MispExport.scala | 183 ++++++ .../app/connectors/misp/MispSrv.scala | 617 +----------------- .../app/connectors/misp/MispSynchro.scala | 164 +++++ .../misp/UpdateMispAlertArtifactActor.scala | 58 ++ 8 files changed, 694 insertions(+), 610 deletions(-) create mode 100644 thehive-misp/app/connectors/misp/MispConfig.scala create mode 100644 thehive-misp/app/connectors/misp/MispConnection.scala create mode 100644 thehive-misp/app/connectors/misp/MispConverter.scala create mode 100644 thehive-misp/app/connectors/misp/MispExport.scala create mode 100644 thehive-misp/app/connectors/misp/MispSynchro.scala create mode 100644 thehive-misp/app/connectors/misp/UpdateMispAlertArtifactActor.scala diff --git a/thehive-misp/app/connectors/misp/MispConfig.scala b/thehive-misp/app/connectors/misp/MispConfig.scala new file mode 100644 index 0000000000..2048cabf0a --- /dev/null +++ b/thehive-misp/app/connectors/misp/MispConfig.scala @@ -0,0 +1,39 @@ +package connectors.misp + +import javax.inject.Inject + +import scala.concurrent.duration.{ DurationInt, FiniteDuration } +import scala.util.Try + +import play.api.Configuration + +import services.CustomWSAPI + +class MispConfig(val interval: FiniteDuration, val connections: Seq[MispConnection]) { + + def this(configuration: Configuration, defaultCaseTemplate: Option[String], globalWS: CustomWSAPI) = this( + configuration.getOptional[FiniteDuration]("misp.interval").getOrElse(1.hour), + + for { + cfg ← configuration.getOptional[Configuration]("misp").toSeq + mispWS = globalWS.withConfig(cfg) + + defaultArtifactTags = cfg.getOptional[Seq[String]]("tags").getOrElse(Nil) + name ← cfg.subKeys + + mispConnectionConfig ← Try(cfg.get[Configuration](name)).toOption.toSeq + url ← mispConnectionConfig.getOptional[String]("url") + key ← mispConnectionConfig.getOptional[String]("key") + instanceWS = mispWS.withConfig(mispConnectionConfig) + artifactTags = mispConnectionConfig.getOptional[Seq[String]]("tags").getOrElse(defaultArtifactTags) + caseTemplate = mispConnectionConfig.getOptional[String]("caseTemplate").orElse(defaultCaseTemplate) + } yield MispConnection(name, url, key, instanceWS, caseTemplate, artifactTags)) + + @Inject def this(configuration: Configuration, httpSrv: CustomWSAPI) = + this( + configuration, + configuration.getOptional[String]("misp.caseTemplate"), + httpSrv) + + def getConnection(name: String): Option[MispConnection] = connections.find(_.name == name) +} diff --git a/thehive-misp/app/connectors/misp/MispConnection.scala b/thehive-misp/app/connectors/misp/MispConnection.scala new file mode 100644 index 0000000000..4bc4db9c21 --- /dev/null +++ b/thehive-misp/app/connectors/misp/MispConnection.scala @@ -0,0 +1,30 @@ +package connectors.misp + +import play.api.Logger + +import services.CustomWSAPI + +case class MispConnection( + name: String, + baseUrl: String, + key: String, + ws: CustomWSAPI, + caseTemplate: Option[String], + artifactTags: Seq[String]) { + + private[MispConnection] lazy val logger = Logger(getClass) + + logger.info( + s"""Add MISP connection $name + |\turl: $baseUrl + |\tproxy: ${ws.proxy} + |\tcase template: ${caseTemplate.getOrElse("")} + |\tartifact tags: ${artifactTags.mkString}""".stripMargin) + + private[misp] def apply(url: String) = + ws.url(s"$baseUrl/$url") + .withHttpHeaders( + "Authorization" → key, + "Accept" → "application/json") + +} \ No newline at end of file diff --git a/thehive-misp/app/connectors/misp/MispConverter.scala b/thehive-misp/app/connectors/misp/MispConverter.scala new file mode 100644 index 0000000000..db0209cd4a --- /dev/null +++ b/thehive-misp/app/connectors/misp/MispConverter.scala @@ -0,0 +1,205 @@ +package connectors.misp + +import play.api.libs.json.{ JsObject, JsString, Json } + +trait MispConverter { + def convertAttribute(mispAttribute: MispAttribute): Seq[JsObject] = { + val dataType = toArtifact(mispAttribute.tpe) + val tags = Seq(s"MISP:type=${mispAttribute.tpe}", s"MISP:category=${mispAttribute.category}") + val fields = Json.obj( + "data" → mispAttribute.value, + "dataType" → dataType, + "message" → mispAttribute.comment, + "startDate" → mispAttribute.date, + "tags" → tags) + + val types = mispAttribute.tpe.split('|').toSeq + if (types.length > 1) { + val values = mispAttribute.value.split('|').toSeq + val typesValues = types.zipAll(values, "noType", "noValue") + val additionnalMessage = typesValues + .map { + case (t, v) ⇒ s"$t: $v" + } + .mkString("\n") + typesValues.map { + case (tpe, value) ⇒ + fields + + ("dataType" → JsString(toArtifact(tpe))) + + ("data" → JsString(value)) + + ("message" → JsString(mispAttribute.comment + "\n" + additionnalMessage)) + } + } + else { + Seq(fields) + } + } + + def fromArtifact(dataType: String, data: Option[String]): (String, String) = { + dataType match { + case "filename" ⇒ "Payload delivery" → "filename" + case "fqdn" ⇒ "Network activity" → "hostname" + case "url" ⇒ "External analysis" → "url" + case "user-agent" ⇒ "Network activity" → "user-agent" + case "domain" ⇒ "Network activity" → "domain" + case "ip" ⇒ "Network activity" → "ip-src" + case "mail_subject" ⇒ "Payload delivery" → "email-subject" + case "hash" ⇒ data.fold(0)(_.length) match { + case 32 ⇒ "Payload delivery" → "md5" + case 40 ⇒ "Payload delivery" → "sha1" + case 64 ⇒ "Payload delivery" → "sha256" + case 56 ⇒ "Payload delivery" → "sha224" + case 71 ⇒ "Payload delivery" → "sha384" + case 128 ⇒ "Payload delivery" → "sha512" + case _ ⇒ "Payload delivery" → "other" + } + case "mail" ⇒ "Payload delivery" → "email-src" + case "registry" ⇒ "Persistence mechanism" → "regkey" + case "uri_path" ⇒ "Network activity" → "uri" + case "regexp" ⇒ "Other" → "other" + case "other" ⇒ "Other" → "other" + case "file" ⇒ "Payload delivery" → "malware-sample" + case _ ⇒ "Other" → "other" + } + } + + def toArtifact(tpe: String): String = attribute2artifactLookup.getOrElse(tpe, "other") + + private lazy val attribute2artifactLookup = Map( + "md5" → "hash", + "sha1" → "hash", + "sha256" → "hash", + "filename" → "filename", + "pdb" → "other", + "filename|md5" → "other", + "filename|sha1" → "other", + "filename|sha256" → "other", + "ip-src" → "ip", + "ip-dst" → "ip", + "hostname" → "fqdn", + "domain" → "domain", + "domain|ip" → "other", + "email-src" → "mail", + "email-dst" → "mail", + "email-subject" → "mail_subject", + "email-attachment" → "other", + "float" → "other", + "url" → "url", + "http-method" → "other", + "user-agent" → "user-agent", + "regkey" → "registry", + "regkey|value" → "registry", + "AS" → "other", + "snort" → "other", + "pattern-in-file" → "other", + "pattern-in-traffic" → "other", + "pattern-in-memory" → "other", + "yara" → "other", + "sigma" → "other", + "vulnerability" → "other", + "attachment" → "file", + "malware-sample" → "file", + "link" → "other", + "comment" → "other", + "text" → "other", + "hex" → "other", + "other" → "other", + "named" → "other", + "mutex" → "other", + "target-user" → "other", + "target-email" → "mail", + "target-machine" → "fqdn", + "target-org" → "other", + "target-location" → "other", + "target-external" → "other", + "btc" → "other", + "iban" → "other", + "bic" → "other", + "bank-account-nr" → "other", + "aba-rtn" → "other", + "bin" → "other", + "cc-number" → "other", + "prtn" → "other", + "threat-actor" → "other", + "campaign-name" → "other", + "campaign-id" → "other", + "malware-type" → "other", + "uri" → "uri_path", + "authentihash" → "other", + "ssdeep" → "hash", + "imphash" → "hash", + "pehash" → "hash", + "impfuzzy" → "hash", + "sha224" → "hash", + "sha384" → "hash", + "sha512" → "hash", + "sha512/224" → "hash", + "sha512/256" → "hash", + "tlsh" → "other", + "filename|authentihash" → "other", + "filename|ssdeep" → "other", + "filename|imphash" → "other", + "filename|impfuzzy" → "other", + "filename|pehash" → "other", + "filename|sha224" → "other", + "filename|sha384" → "other", + "filename|sha512" → "other", + "filename|sha512/224" → "other", + "filename|sha512/256" → "other", + "filename|tlsh" → "other", + "windows-scheduled-task" → "other", + "windows-service-name" → "other", + "windows-service-displayname" → "other", + "whois-registrant-email" → "mail", + "whois-registrant-phone" → "other", + "whois-registrant-name" → "other", + "whois-registrar" → "other", + "whois-creation-date" → "other", + "x509-fingerprint-sha1" → "other", + "dns-soa-email" → "other", + "size-in-bytes" → "other", + "counter" → "other", + "datetime" → "other", + "cpe" → "other", + "port" → "other", + "ip-dst|port" → "other", + "ip-src|port" → "other", + "hostname|port" → "other", + "email-dst-display-name" → "other", + "email-src-display-name" → "other", + "email-header" → "other", + "email-reply-to" → "other", + "email-x-mailer" → "other", + "email-mime-boundary" → "other", + "email-thread-index" → "other", + "email-message-id" → "other", + "github-username" → "other", + "github-repository" → "other", + "github-organisation" → "other", + "jabber-id" → "other", + "twitter-id" → "other", + "first-name" → "other", + "middle-name" → "other", + "last-name" → "other", + "date-of-birth" → "other", + "place-of-birth" → "other", + "gender" → "other", + "passport-number" → "other", + "passport-country" → "other", + "passport-expiration" → "other", + "redress-number" → "other", + "nationality" → "other", + "visa-number" → "other", + "issue-date-of-the-visa" → "other", + "primary-residence" → "other", + "country-of-residence" → "other", + "special-service-request" → "other", + "frequent-flyer-number" → "other", + "travel-details" → "other", + "payment-details" → "other", + "place-port-of-original-embarkation" → "other", + "place-port-of-clearance" → "other", + "place-port-of-onward-foreign-destination" → "other", + "passenger-name-record-locator-number" → "other", + "mobile-application-id" → "other") +} \ No newline at end of file diff --git a/thehive-misp/app/connectors/misp/MispCtrl.scala b/thehive-misp/app/connectors/misp/MispCtrl.scala index 5e2c0d5331..e1d624ce3f 100644 --- a/thehive-misp/app/connectors/misp/MispCtrl.scala +++ b/thehive-misp/app/connectors/misp/MispCtrl.scala @@ -23,7 +23,9 @@ import org.elastic4play.{ NotFoundError, Timed } @Singleton class MispCtrl @Inject() ( + mispSynchro: MispSynchro, mispSrv: MispSrv, + mispExport: MispExport, mispConfig: MispConfig, caseSrv: CaseSrv, authenticated: Authenticated, @@ -47,13 +49,13 @@ class MispCtrl @Inject() ( @Timed def syncAlerts: Action[AnyContent] = authenticated(Role.admin).async { implicit request ⇒ - mispSrv.synchronize() + mispSynchro.synchronize() .map { m ⇒ Ok(Json.toJson(m)) } } @Timed def syncAllAlerts: Action[AnyContent] = authenticated(Role.admin).async { implicit request ⇒ - mispSrv.fullSynchronize() + mispSynchro.fullSynchronize() .map { m ⇒ Ok(Json.toJson(m)) } } @@ -67,7 +69,7 @@ class MispCtrl @Inject() ( def exportCase(mispName: String, caseId: String): Action[AnyContent] = authenticated(Role.write).async { implicit request ⇒ caseSrv .get(caseId) - .flatMap { caze ⇒ mispSrv.export(mispName, caze) } + .flatMap { caze ⇒ mispExport.export(mispName, caze) } .map { case (_, exportedAttributes) ⇒ renderer.toMultiOutput(CREATED, exportedAttributes) diff --git a/thehive-misp/app/connectors/misp/MispExport.scala b/thehive-misp/app/connectors/misp/MispExport.scala new file mode 100644 index 0000000000..d8e99a23fb --- /dev/null +++ b/thehive-misp/app/connectors/misp/MispExport.scala @@ -0,0 +1,183 @@ +package connectors.misp + +import java.text.SimpleDateFormat +import java.util.Date +import javax.inject.Inject + +import scala.concurrent.{ ExecutionContext, Future } +import scala.util.Try + +import play.api.libs.json.{ JsObject, Json } + +import akka.stream.scaladsl.Sink +import models.{ Artifact, Case } +import services.{ AlertSrv, ArtifactSrv } +import JsonFormat.exportedAttributeWrites +import akka.stream.Materializer + +import org.elastic4play.InternalError +import org.elastic4play.controllers.Fields +import org.elastic4play.models.JsonFormat.baseModelEntityWrites +import org.elastic4play.services.{ AttachmentSrv, AuthContext } +import org.elastic4play.utils.RichFuture + +class MispExport @Inject() ( + mispConfig: MispConfig, + mispSrv: MispSrv, + artifactSrv: ArtifactSrv, + alertSrv: AlertSrv, + attachmentSrv: AttachmentSrv, + implicit val ec: ExecutionContext, + implicit val mat: Materializer) extends MispConverter { + + lazy val dateFormat = new SimpleDateFormat("yy-MM-dd") + + def relatedMispEvent(mispName: String, caseId: String): Future[(Option[String], Option[String])] = { + import org.elastic4play.services.QueryDSL._ + alertSrv.find(and("type" ~= "misp", "case" ~= caseId, "source" ~= mispName), Some("0-1"), Nil) + ._1 + .map { alert ⇒ alert.id → alert.sourceRef() } + .runWith(Sink.headOption) + .map(alertIdSource ⇒ alertIdSource.map(_._1) → alertIdSource.map(_._2)) + } + + def buildAttributeList(caze: Case): Future[Seq[ExportedMispAttribute]] = { + import org.elastic4play.services.QueryDSL._ + artifactSrv + .find(parent("case", withId(caze.id)), Some("all"), Nil) + ._1 + .map { artifact ⇒ + val (category, tpe) = fromArtifact(artifact.dataType(), artifact.data()) + val value = (artifact.data(), artifact.attachment()) match { + case (Some(data), None) ⇒ Left(data) + case (None, Some(attachment)) ⇒ Right(attachment) + case _ ⇒ sys.error("???") + } + ExportedMispAttribute(artifact, tpe, category, value, artifact.message()) + } + .runWith(Sink.seq) + } + + def removeDuplicateAttributes(attributes: Seq[ExportedMispAttribute]): Seq[ExportedMispAttribute] = { + val attrIndex = attributes.zipWithIndex + + attrIndex + .filter { + case (ExportedMispAttribute(_, category, tpe, value, _), index) ⇒ attrIndex.exists { + case (ExportedMispAttribute(_, `category`, `tpe`, `value`, _), otherIndex) ⇒ otherIndex >= index + case _ ⇒ true + } + } + .map(_._1) + } + + def createEvent(mispConnection: MispConnection, title: String, severity: String, date: Date, attributes: Seq[ExportedMispAttribute]): Future[(String, Seq[ExportedMispAttribute])] = { + val mispEvent = Json.obj( + "Event" → Json.obj( + "distribution" → 0, + "threat_level_id" → severity, + "analysis" → 0, + "info" → title, + "date" → dateFormat.format(date), + "published" → false, + "Attribute" → attributes)) + mispConnection("events") + .post(mispEvent) + .map { mispResponse ⇒ + val eventId = (mispResponse.json \ "Event" \ "id") + .asOpt[String] + .getOrElse(throw InternalError(s"Unexpected MISP response: ${mispResponse.status} ${mispResponse.statusText}\n${mispResponse.body}")) + val messages = (mispResponse.json \ "errors" \ "Attribute") + .asOpt[JsObject] + .getOrElse(JsObject(Nil)) + .fields + .toMap + .mapValues { m ⇒ + (m \ "value") + .asOpt[Seq[String]] + .flatMap(_.headOption) + .getOrElse(s"Unexpected message format: $m") + } + val exportedAttributes = attributes.zipWithIndex.collect { + case (attr, index) if !messages.contains(index.toString) ⇒ attr + } + eventId → exportedAttributes + } + } + + def exportAttribute(mispConnection: MispConnection, eventId: String, attribute: ExportedMispAttribute): Future[Artifact] = { + val mispResponse = attribute match { + case ExportedMispAttribute(_, _, _, Right(attachment), comment) ⇒ + attachmentSrv + .source(attachment.id) + .runReduce(_ ++ _) + .flatMap { data ⇒ + val b64data = java.util.Base64.getEncoder.encodeToString(data.toArray[Byte]) + val body = Json.obj( + "request" → Json.obj( + "event_id" → eventId.toInt, + "category" → "Payload delivery", + "type" → "malware-sample", + "comment" → comment, + "files" → Json.arr( + Json.obj( + "filename" → attachment.name, + "data" → b64data)))) + mispConnection("events/upload_sample").post(body) + } + case attr ⇒ mispConnection(s"attributes/add/$eventId").post(Json.toJson(attr)) + + } + + mispResponse.map { + case response if response.status / 100 == 2 ⇒ attribute.artifact + case response ⇒ + val json = response.json + val message = (json \ "message").asOpt[String] + val error = (json \ "errors" \ "value").head.asOpt[String] + val errorMessage = for (m ← message; e ← error) yield s"$m $e" + throw MispExportError(errorMessage orElse message orElse error getOrElse s"Unexpected MISP response: ${response.status} ${response.statusText}\n${response.body}", attribute.artifact) + } + } + + def export(mispName: String, caze: Case)(implicit authContext: AuthContext): Future[(String, Seq[Try[Artifact]])] = { + val mispConnection = mispConfig.getConnection(mispName).getOrElse(sys.error("MISP instance not found")) + + for { + (maybeAlertId, maybeEventId) ← relatedMispEvent(mispName, caze.id) + attributes ← buildAttributeList(caze) + uniqueAttributes = removeDuplicateAttributes(attributes) + simpleAttributes = uniqueAttributes.filter(_.value.isLeft) // FIXME used only if event doesn't exist + (eventId, existingAttributes) ← maybeEventId.fold { + // if no event is associated to this case, create a new one + createEvent(mispConnection, caze.title(), caze.severity().toString, caze.startDate(), simpleAttributes).map { + case (eventId, exportedAttributes) ⇒ eventId → exportedAttributes.map(_.value.left.get) + } + } { eventId ⇒ // if an event already exists, retrieve its attributes in order to export only new one + mispSrv.getAttributes(mispConnection, eventId, None).map { attributes ⇒ + eventId → attributes.map { attribute ⇒ + (attribute \ "data").asOpt[String].getOrElse((attribute \ "remoteAttachment" \ "filename").as[String]) + } + } + } + newAttributes = uniqueAttributes.filterNot(attr ⇒ existingAttributes.contains(attr.value.fold(identity, _.name))) + exportedArtifact ← Future.traverse(newAttributes)(attr ⇒ exportAttribute(mispConnection, eventId, attr).toTry) + alertFields = Fields(Json.obj( + "type" → "misp", + "source" → mispName, + "sourceRef" → eventId, + "date" → caze.startDate(), + "lastSyncDate" → new Date(0), + "case" → caze.id, + "title" → caze.title(), + "description" → "Case have been exported to MISP", + "severity" → caze.severity(), + "tags" → caze.tags(), + "tlp" → caze.tlp(), + "artifacts" → uniqueAttributes.map(_.artifact), + "status" → "Imported", + "follow" → false)) + alert ← maybeAlertId.fold(alertSrv.create(alertFields))(alertId ⇒ alertSrv.update(alertId, alertFields)) + } yield alert.id → exportedArtifact + } +} diff --git a/thehive-misp/app/connectors/misp/MispSrv.scala b/thehive-misp/app/connectors/misp/MispSrv.scala index 334ef2fab4..8f7097b5aa 100644 --- a/thehive-misp/app/connectors/misp/MispSrv.scala +++ b/thehive-misp/app/connectors/misp/MispSrv.scala @@ -1,24 +1,18 @@ package connectors.misp -import java.text.SimpleDateFormat import java.util.Date import javax.inject.{ Inject, Provider, Singleton } -import scala.collection.immutable -import scala.concurrent.duration.{ DurationInt, FiniteDuration } import scala.concurrent.{ ExecutionContext, Future } -import scala.util.{ Failure, Success, Try } -import play.api.inject.ApplicationLifecycle +import play.api.Logger import play.api.libs.json.JsLookupResult.jsLookupResultToJsLookup import play.api.libs.json.JsValue.jsValueToJsLookup import play.api.libs.json.Json.toJsFieldJsValueWrapper import play.api.libs.json._ import play.api.libs.ws.WSBodyWritables.writeableOf_JsValue -import play.api.{ Configuration, Environment, Logger } import akka.NotUsed -import akka.actor.{ Actor, ActorSystem } import akka.stream.Materializer import akka.stream.scaladsl.{ FileIO, Sink, Source } import connectors.misp.JsonFormat._ @@ -29,124 +23,19 @@ import net.lingala.zip4j.model.FileHeader import services._ import org.elastic4play.controllers.{ Fields, FileInputValue } -import org.elastic4play.models.JsonFormat.baseModelEntityWrites -import org.elastic4play.services.{ UserSrv ⇒ _, _ } -import org.elastic4play.utils.{ RichFuture, RichJson } +import org.elastic4play.services.{ AuthContext, TempSrv } +import org.elastic4play.utils.RichJson import org.elastic4play.{ InternalError, NotFoundError } -class MispConfig(val interval: FiniteDuration, val connections: Seq[MispConnection]) { - - def this(configuration: Configuration, defaultCaseTemplate: Option[String], globalWS: CustomWSAPI) = this( - configuration.getOptional[FiniteDuration]("misp.interval").getOrElse(1.hour), - - for { - cfg ← configuration.getOptional[Configuration]("misp").toSeq - mispWS = globalWS.withConfig(cfg) - - defaultArtifactTags = cfg.getOptional[Seq[String]]("tags").getOrElse(Nil) - name ← cfg.subKeys - - mispConnectionConfig ← Try(cfg.get[Configuration](name)).toOption.toSeq - url ← mispConnectionConfig.getOptional[String]("url") - key ← mispConnectionConfig.getOptional[String]("key") - instanceWS = mispWS.withConfig(mispConnectionConfig) - artifactTags = mispConnectionConfig.getOptional[Seq[String]]("tags").getOrElse(defaultArtifactTags) - caseTemplate = mispConnectionConfig.getOptional[String]("caseTemplate").orElse(defaultCaseTemplate) - } yield MispConnection(name, url, key, instanceWS, caseTemplate, artifactTags)) - - @Inject def this(configuration: Configuration, httpSrv: CustomWSAPI) = - this( - configuration, - configuration.getOptional[String]("misp.caseTemplate"), - httpSrv) - - def getConnection(name: String): Option[MispConnection] = connections.find(_.name == name) -} - -case class MispConnection( - name: String, - baseUrl: String, - key: String, - ws: CustomWSAPI, - caseTemplate: Option[String], - artifactTags: Seq[String]) { - - private[MispConnection] lazy val logger = Logger(getClass) - - logger.info( - s"""Add MISP connection $name - |\turl: $baseUrl - |\tproxy: ${ws.proxy} - |\tcase template: ${caseTemplate.getOrElse("")} - |\tartifact tags: ${artifactTags.mkString}""".stripMargin) - - private[misp] def apply(url: String) = - ws.url(s"$baseUrl/$url") - .withHttpHeaders( - "Authorization" → key, - "Accept" → "application/json") - -} - -/** - * This actor listens message from migration (message UpdateMispAlertArtifact) which indicates that artifacts in - * MISP event must be retrieved in inserted in alerts. - * - * @param eventSrv event bus used to receive migration message - * @param userSrv user service used to do operations on database without real user request - * @param mispSrv misp service to invoke artifact update action - * @param ec execution context - */ -class UpdateMispAlertArtifactActor @Inject() ( - eventSrv: EventSrv, - userSrv: UserSrv, - mispSrv: MispSrv, - implicit val ec: ExecutionContext) extends Actor { - - private[UpdateMispAlertArtifactActor] lazy val logger = Logger(getClass) - override def preStart(): Unit = { - eventSrv.subscribe(self, classOf[UpdateMispAlertArtifact]) - super.preStart() - } - - override def postStop(): Unit = { - eventSrv.unsubscribe(self) - super.postStop() - } - - override def receive: Receive = { - case UpdateMispAlertArtifact() ⇒ - logger.info("UpdateMispAlertArtifact") - userSrv - .inInitAuthContext { implicit authContext ⇒ - mispSrv.updateMispAlertArtifact() - } - .onComplete { - case Success(_) ⇒ logger.info("Artifacts in MISP alerts updated") - case Failure(error) ⇒ logger.error("Update MISP alert artifacts error :", error) - } - () - case msg ⇒ - logger.info(s"Receiving unexpected message: $msg (${msg.getClass})") - } -} @Singleton class MispSrv @Inject() ( mispConfig: MispConfig, alertSrvProvider: Provider[AlertSrv], caseSrv: CaseSrv, artifactSrv: ArtifactSrv, - userSrv: UserSrv, - attachmentSrv: AttachmentSrv, tempSrv: TempSrv, - eventSrv: EventSrv, - migrationSrv: MigrationSrv, - httpSrv: CustomWSAPI, - environment: Environment, - lifecycle: ApplicationLifecycle, - implicit val system: ActorSystem, - implicit val materializer: Materializer, - implicit val ec: ExecutionContext) { + implicit val ec: ExecutionContext, + implicit val mat: Materializer) extends MispConverter { private[misp] lazy val logger = Logger(getClass) private[misp] lazy val alertSrv = alertSrvProvider.get @@ -157,129 +46,6 @@ class MispSrv @Inject() ( Future.successful(instanceConfig) } - private[misp] def initScheduler(): Unit = { - val task = system.scheduler.schedule(0.seconds, mispConfig.interval) { - if (migrationSrv.isReady) { - logger.info("Update of MISP events is starting ...") - userSrv - .inInitAuthContext { implicit authContext ⇒ - synchronize().andThen { case _ ⇒ tempSrv.releaseTemporaryFiles() } - } - .onComplete { - case Success(a) ⇒ - logger.info("Misp synchronization completed") - a.collect { - case Failure(t) ⇒ logger.warn(s"Update MISP error", t) - } - case Failure(t) ⇒ logger.info("Misp synchronization failed", t) - } - } - else { - logger.info("MISP synchronization cancel, database is not ready") - } - } - lifecycle.addStopHook { () ⇒ - logger.info("Stopping MISP fetching ...") - task.cancel() - Future.successful(()) - } - } - - initScheduler() - - def synchronize()(implicit authContext: AuthContext): Future[Seq[Try[Alert]]] = { - import org.elastic4play.services.QueryDSL._ - - // for each MISP server - Source(mispConfig.connections.toList) - // get last synchronization - .mapAsyncUnordered(1) { mispConnection ⇒ - alertSrv.stats(and("type" ~= "misp", "source" ~= mispConnection.name), Seq(selectMax("lastSyncDate"))) - .map { maxLastSyncDate ⇒ mispConnection → new Date((maxLastSyncDate \ "max_lastSyncDate").as[Long]) } - .recover { case _ ⇒ mispConnection → new Date(0) } - } - .flatMapConcat { - case (mispConnection, lastSyncDate) ⇒ - synchronize(mispConnection, Some(lastSyncDate)) - } - .runWith(Sink.seq) - } - - def fullSynchronize()(implicit authContext: AuthContext): Future[immutable.Seq[Try[Alert]]] = { - Source(mispConfig.connections.toList) - .flatMapConcat(mispConnection ⇒ synchronize(mispConnection, None)) - .runWith(Sink.seq) - } - - def synchronize(mispConnection: MispConnection, lastSyncDate: Option[Date])(implicit authContext: AuthContext): Source[Try[Alert], NotUsed] = { - logger.info(s"Synchronize MISP ${mispConnection.name} from $lastSyncDate") - // get events that have been published after the last synchronization - getEventsFromDate(mispConnection, lastSyncDate.getOrElse(new Date(0))) - // get related alert - .mapAsyncUnordered(1) { event ⇒ - logger.trace(s"Looking for alert misp:${event.source}:${event.sourceRef}") - alertSrv.get("misp", event.source, event.sourceRef) - .map((event, _)) - } - .mapAsyncUnordered(1) { - case (event, alert) ⇒ - logger.trace(s"MISP synchro ${mispConnection.name}, event ${event.sourceRef}, alert ${alert.fold("no alert")(a ⇒ "alert " + a.alertId() + "last sync at " + a.lastSyncDate())}") - logger.info(s"getting MISP event ${event.source}:${event.sourceRef}") - getAttributes(mispConnection, event.sourceRef, lastSyncDate.flatMap(_ ⇒ alert.map(_.lastSyncDate()))) - .map((event, alert, _)) - } - .mapAsyncUnordered(1) { - // if there is no related alert, create a new one - case (event, None, attrs) ⇒ - logger.info(s"MISP event ${event.source}:${event.sourceRef} has no related alert, create it with ${attrs.size} observable(s)") - val alertJson = Json.toJson(event).as[JsObject] + - ("type" → JsString("misp")) + - ("caseTemplate" → mispConnection.caseTemplate.fold[JsValue](JsNull)(JsString)) + - ("artifacts" → JsArray(attrs)) - alertSrv.create(Fields(alertJson)) - .map(Success(_)) - .recover { case t ⇒ Failure(t) } - - // if a related alert exists and we follow it or a fullSync, update it - case (event, Some(alert), attrs) if alert.follow() || lastSyncDate.isEmpty ⇒ - logger.info(s"MISP event ${event.source}:${event.sourceRef} has related alert, update it with ${attrs.size} observable(s)") - val alertJson = Json.toJson(event).as[JsObject] - - "type" - - "source" - - "sourceRef" - - "caseTemplate" - - "date" + - ("artifacts" → JsArray(attrs)) + - // if this is a full synchronization, don't update alert status - ("status" → (if (lastSyncDate.isEmpty) Json.toJson(alert.status()) - else alert.status() match { - case AlertStatus.New ⇒ Json.toJson(AlertStatus.New) - case _ ⇒ Json.toJson(AlertStatus.Updated) - })) - logger.debug(s"Update alert ${alert.id} with\n$alertJson") - val fAlert = alertSrv.update(alert.id, Fields(alertJson)) - // if a case have been created, update it - (alert.caze() match { - case None ⇒ fAlert - case Some(caze) ⇒ - for { - a ← fAlert - // if this is a full synchronization, don't update case status - caseFields = if (lastSyncDate.isEmpty) Fields(alert.toCaseJson).unset("status") - else Fields(alert.toCaseJson) - _ ← caseSrv.update(caze, caseFields) - _ ← artifactSrv.create(caze, attrs.map(Fields.apply)) - } yield a - }) - .map(Success(_)) - .recover { case t ⇒ Failure(t) } - - // if the alert is not followed, do nothing - case (_, Some(alert), _) ⇒ - Future.successful(Success(alert)) - } - } - def getEventsFromDate(mispConnection: MispConnection, fromDate: Date): Source[MispAlert, NotUsed] = { val date = fromDate.getTime / 1000 Source @@ -354,8 +120,8 @@ class MispSrv @Inject() ( def attributeToArtifact( mispConnection: MispConnection, - alert: Alert, - attr: JsObject)(implicit authContext: AuthContext): Option[Future[Fields]] = { + attr: JsObject, + defaultTlp: Long)(implicit authContext: AuthContext): Option[Future[Fields]] = { (for { dataType ← (attr \ "dataType").validate[String] data ← (attr \ "data").validateOpt[String] @@ -380,7 +146,7 @@ class MispSrv @Inject() ( case "tlp:amber" ⇒ JsNumber(2) case "tlp:red" ⇒ JsNumber(3) } - .getOrElse(JsNumber(alert.tlp())) + .getOrElse(JsNumber(defaultTlp)) fields = Fields.empty .set("dataType", dataType) .set("message", message) @@ -396,7 +162,7 @@ class MispSrv @Inject() ( })) match { case JsSuccess(r, _) ⇒ Some(r) case e: JsError ⇒ - logger.warn(s"Invalid attribute in alert ${alert.id}: $e\n$attr") + logger.warn(s"Invalid attribute format: $e\n$attr") None } } @@ -416,7 +182,7 @@ class MispSrv @Inject() ( def importArtifacts(alert: Alert, caze: Case)(implicit authContext: AuthContext): Future[Case] = { for { instanceConfig ← getInstanceConfig(alert.source()) - artifacts ← Future.sequence(alert.artifacts().flatMap(attributeToArtifact(instanceConfig, alert, _))) + artifacts ← Future.sequence(alert.artifacts().flatMap(attributeToArtifact(instanceConfig, _, alert.tlp()))) _ ← artifactSrv.create(caze, artifacts) } yield caze } @@ -539,367 +305,4 @@ class MispSrv @Inject() ( } } } - - def exportStatus(caseId: String): Future[Seq[(String, String)]] = { - import org.elastic4play.services.QueryDSL._ - alertSrv.find(and("type" ~= "misp", "case" ~= caseId), Some("all"), Nil) - ._1 - .map { alert ⇒ - alert.source() → alert.sourceRef() - } - .runWith(Sink.seq) - } - - def relatedMispEvent(mispName: String, caseId: String): Future[(Option[String], Option[String])] = { - import org.elastic4play.services.QueryDSL._ - alertSrv.find(and("type" ~= "misp", "case" ~= caseId, "source" ~= mispName), Some("0-1"), Nil) - ._1 - .map { alert ⇒ alert.id → alert.sourceRef() } - .runWith(Sink.headOption) - .map(alertIdSource ⇒ alertIdSource.map(_._1) → alertIdSource.map(_._2)) - } - - def export(mispName: String, caze: Case)(implicit authContext: AuthContext): Future[(String, Seq[Try[Artifact]])] = { - val mispConnection = mispConfig.getConnection(mispName).getOrElse(sys.error("MISP instance not found")) - val dateFormat = new SimpleDateFormat("yy-MM-dd") - - def buildAttributeList(): Future[Seq[ExportedMispAttribute]] = { - import org.elastic4play.services.QueryDSL._ - artifactSrv - .find(parent("case", withId(caze.id)), Some("all"), Nil) - ._1 - .map { artifact ⇒ - val (category, tpe) = artifact2attribute(artifact.dataType(), artifact.data()) - val value = (artifact.data(), artifact.attachment()) match { - case (Some(data), None) ⇒ Left(data) - case (None, Some(attachment)) ⇒ Right(attachment) - case _ ⇒ sys.error("???") - } - ExportedMispAttribute(artifact, tpe, category, value, artifact.message()) - } - .runWith(Sink.seq) - } - - def removeDuplicateAttributes(attributes: Seq[ExportedMispAttribute]): Seq[ExportedMispAttribute] = { - val attrIndex = attributes.zipWithIndex - - attrIndex - .filter { - case (ExportedMispAttribute(_, category, tpe, value, _), index) ⇒ attrIndex.exists { - case (ExportedMispAttribute(_, `category`, `tpe`, `value`, _), otherIndex) ⇒ otherIndex >= index - case _ ⇒ true - } - } - .map(_._1) - } - - def createEvent(title: String, severity: String, date: Date, attributes: Seq[ExportedMispAttribute]): Future[(String, Seq[ExportedMispAttribute])] = { - val mispEvent = Json.obj( - "Event" → Json.obj( - "distribution" → 0, - "threat_level_id" → caze.severity().toString, - "analysis" → 0, - "info" → caze.title(), - "date" → dateFormat.format(caze.startDate()), - "published" → false, - "Attribute" → attributes)) - mispConnection("events") - .post(mispEvent) - .map { mispResponse ⇒ - val eventId = (mispResponse.json \ "Event" \ "id") - .asOpt[String] - .getOrElse(throw InternalError(s"Unexpected MISP response: ${mispResponse.status} ${mispResponse.statusText}\n${mispResponse.body}")) - val messages = (mispResponse.json \ "errors" \ "Attribute") - .asOpt[JsObject] - .getOrElse(JsObject(Nil)) - .fields - .toMap - .mapValues { m ⇒ - (m \ "value") - .asOpt[Seq[String]] - .flatMap(_.headOption) - .getOrElse(s"Unexpected message format: $m") - } - val exportedAttributes = attributes.zipWithIndex.collect { - case (attr, index) if !messages.contains(index.toString) ⇒ attr - } - eventId → exportedAttributes - } - } - - def exportAttribute(eventId: String, attribute: ExportedMispAttribute): Future[Artifact] = { - val mispResponse = attribute match { - case ExportedMispAttribute(_, _, _, Right(attachment), comment) ⇒ - attachmentSrv - .source(attachment.id) - .runReduce(_ ++ _) - .flatMap { data ⇒ - val b64data = java.util.Base64.getEncoder.encodeToString(data.toArray[Byte]) - val body = Json.obj( - "request" → Json.obj( - "event_id" → eventId.toInt, - "category" → "Payload delivery", - "type" → "malware-sample", - "comment" → comment, - "files" → Json.arr( - Json.obj( - "filename" → attachment.name, - "data" → b64data)))) - mispConnection("events/upload_sample").post(body) - } - case attr ⇒ mispConnection(s"attributes/add/$eventId").post(Json.toJson(attr)) - - } - - mispResponse.map { - case response if response.status / 100 == 2 ⇒ attribute.artifact - case response ⇒ - val json = response.json - val message = (json \ "message").asOpt[String] - val error = (json \ "errors" \ "value").head.asOpt[String] - val errorMessage = for (m ← message; e ← error) yield s"$m $e" - throw MispExportError(errorMessage orElse message orElse error getOrElse s"Unexpected MISP response: ${response.status} ${response.statusText}\n${response.body}", attribute.artifact) - } - } - - for { - (maybeAlertId, maybeEventId) ← relatedMispEvent(mispName, caze.id) - attributes ← buildAttributeList() - uniqueAttributes = removeDuplicateAttributes(attributes) - simpleAttributes = uniqueAttributes.filter(_.value.isLeft) // FIXME used only if event doesn't exist - (eventId, existingAttributes) ← maybeEventId.fold { - // if no event is associated to this case, create a new one - createEvent(caze.title(), caze.severity().toString, caze.startDate(), simpleAttributes).map { - case (eventId, exportedAttributes) ⇒ eventId → exportedAttributes.map(_.value.left.get) - } - } { eventId ⇒ // if an event already exists, retrieve its attributes in order to export only new one - getAttributes(mispConnection, eventId, None).map { attributes ⇒ - eventId → attributes.map { attribute ⇒ - (attribute \ "data").asOpt[String].getOrElse((attribute \ "remoteAttachment" \ "filename").as[String]) - } - } - } - newAttributes = uniqueAttributes.filterNot(attr ⇒ existingAttributes.contains(attr.value.fold(identity, _.name))) - exportedArtifact ← Future.traverse(newAttributes)(attr ⇒ exportAttribute(eventId, attr).toTry) - alertFields = Fields(Json.obj( - "type" → "misp", - "source" → mispName, - "sourceRef" → eventId, - "date" → caze.startDate(), - "lastSyncDate" → new Date(0), - "case" → caze.id, - "title" → caze.title(), - "description" → "Case have been exported to MISP", - "severity" → caze.severity(), - "tags" → caze.tags(), - "tlp" → caze.tlp(), - "artifacts" → uniqueAttributes.map(_.artifact), - "status" → "Imported", - "follow" → false)) - alert ← maybeAlertId.fold(alertSrv.create(alertFields))(alertId ⇒ alertSrv.update(alertId, alertFields)) - } yield alert.id → exportedArtifact - } - - def convertAttribute(mispAttribute: MispAttribute): Seq[JsObject] = { - val dataType = attribute2artifact(mispAttribute.tpe) - val fields = Json.obj( - "data" → mispAttribute.value, - "dataType" → dataType, - "message" → mispAttribute.comment, - "startDate" → mispAttribute.date, - "tags" → Json.arr(s"MISP:type=${ - mispAttribute.tpe - }", s"MISP:category=${ - mispAttribute.category - }")) - - val types = mispAttribute.tpe.split('|').toSeq - if (types.length > 1) { - val values = mispAttribute.value.split('|').toSeq - val typesValues = types.zipAll(values, "noType", "noValue") - val additionnalMessage = typesValues - .map { - case (t, v) ⇒ s"$t: $v" - } - .mkString("\n") - typesValues.map { - case (tpe, value) ⇒ - fields + - ("dataType" → JsString(attribute2artifact(tpe))) + - ("data" → JsString(value)) + - ("message" → JsString(mispAttribute.comment + "\n" + additionnalMessage)) - } - } - else { - Seq(fields) - } - } - - private def artifact2attribute(dataType: String, data: Option[String]): (String, String) = { - dataType match { - case "filename" ⇒ "Payload delivery" → "filename" - case "fqdn" ⇒ "Network activity" → "hostname" - case "url" ⇒ "External analysis" → "url" - case "user-agent" ⇒ "Network activity" → "user-agent" - case "domain" ⇒ "Network activity" → "domain" - case "ip" ⇒ "Network activity" → "ip-src" - case "mail_subject" ⇒ "Payload delivery" → "email-subject" - case "hash" ⇒ data.fold(0)(_.length) match { - case 32 ⇒ "Payload delivery" → "md5" - case 40 ⇒ "Payload delivery" → "sha1" - case 64 ⇒ "Payload delivery" → "sha256" - case 56 ⇒ "Payload delivery" → "sha224" - case 71 ⇒ "Payload delivery" → "sha384" - case 128 ⇒ "Payload delivery" → "sha512" - case _ ⇒ "Payload delivery" → "other" - } - case "mail" ⇒ "Payload delivery" → "email-src" - case "registry" ⇒ "Persistence mechanism" → "regkey" - case "uri_path" ⇒ "Network activity" → "uri" - case "regexp" ⇒ "Other" → "other" - case "other" ⇒ "Other" → "other" - case "file" ⇒ "Payload delivery" → "malware-sample" - case _ ⇒ "Other" → "other" - } - } - - private def attribute2artifact(tpe: String): String = attribute2artifactLookup.getOrElse(tpe, "other") - - private lazy val attribute2artifactLookup = Map( - "md5" → "hash", - "sha1" → "hash", - "sha256" → "hash", - "filename" → "filename", - "pdb" → "other", - "filename|md5" → "other", - "filename|sha1" → "other", - "filename|sha256" → "other", - "ip-src" → "ip", - "ip-dst" → "ip", - "hostname" → "fqdn", - "domain" → "domain", - "domain|ip" → "other", - "email-src" → "mail", - "email-dst" → "mail", - "email-subject" → "mail_subject", - "email-attachment" → "other", - "float" → "other", - "url" → "url", - "http-method" → "other", - "user-agent" → "user-agent", - "regkey" → "registry", - "regkey|value" → "registry", - "AS" → "other", - "snort" → "other", - "pattern-in-file" → "other", - "pattern-in-traffic" → "other", - "pattern-in-memory" → "other", - "yara" → "other", - "sigma" → "other", - "vulnerability" → "other", - "attachment" → "file", - "malware-sample" → "file", - "link" → "other", - "comment" → "other", - "text" → "other", - "hex" → "other", - "other" → "other", - "named" → "other", - "mutex" → "other", - "target-user" → "other", - "target-email" → "mail", - "target-machine" → "fqdn", - "target-org" → "other", - "target-location" → "other", - "target-external" → "other", - "btc" → "other", - "iban" → "other", - "bic" → "other", - "bank-account-nr" → "other", - "aba-rtn" → "other", - "bin" → "other", - "cc-number" → "other", - "prtn" → "other", - "threat-actor" → "other", - "campaign-name" → "other", - "campaign-id" → "other", - "malware-type" → "other", - "uri" → "uri_path", - "authentihash" → "other", - "ssdeep" → "hash", - "imphash" → "hash", - "pehash" → "hash", - "impfuzzy" → "hash", - "sha224" → "hash", - "sha384" → "hash", - "sha512" → "hash", - "sha512/224" → "hash", - "sha512/256" → "hash", - "tlsh" → "other", - "filename|authentihash" → "other", - "filename|ssdeep" → "other", - "filename|imphash" → "other", - "filename|impfuzzy" → "other", - "filename|pehash" → "other", - "filename|sha224" → "other", - "filename|sha384" → "other", - "filename|sha512" → "other", - "filename|sha512/224" → "other", - "filename|sha512/256" → "other", - "filename|tlsh" → "other", - "windows-scheduled-task" → "other", - "windows-service-name" → "other", - "windows-service-displayname" → "other", - "whois-registrant-email" → "mail", - "whois-registrant-phone" → "other", - "whois-registrant-name" → "other", - "whois-registrar" → "other", - "whois-creation-date" → "other", - "x509-fingerprint-sha1" → "other", - "dns-soa-email" → "other", - "size-in-bytes" → "other", - "counter" → "other", - "datetime" → "other", - "cpe" → "other", - "port" → "other", - "ip-dst|port" → "other", - "ip-src|port" → "other", - "hostname|port" → "other", - "email-dst-display-name" → "other", - "email-src-display-name" → "other", - "email-header" → "other", - "email-reply-to" → "other", - "email-x-mailer" → "other", - "email-mime-boundary" → "other", - "email-thread-index" → "other", - "email-message-id" → "other", - "github-username" → "other", - "github-repository" → "other", - "github-organisation" → "other", - "jabber-id" → "other", - "twitter-id" → "other", - "first-name" → "other", - "middle-name" → "other", - "last-name" → "other", - "date-of-birth" → "other", - "place-of-birth" → "other", - "gender" → "other", - "passport-number" → "other", - "passport-country" → "other", - "passport-expiration" → "other", - "redress-number" → "other", - "nationality" → "other", - "visa-number" → "other", - "issue-date-of-the-visa" → "other", - "primary-residence" → "other", - "country-of-residence" → "other", - "special-service-request" → "other", - "frequent-flyer-number" → "other", - "travel-details" → "other", - "payment-details" → "other", - "place-port-of-original-embarkation" → "other", - "place-port-of-clearance" → "other", - "place-port-of-onward-foreign-destination" → "other", - "passenger-name-record-locator-number" → "other", - "mobile-application-id" → "other") } diff --git a/thehive-misp/app/connectors/misp/MispSynchro.scala b/thehive-misp/app/connectors/misp/MispSynchro.scala new file mode 100644 index 0000000000..de2912794a --- /dev/null +++ b/thehive-misp/app/connectors/misp/MispSynchro.scala @@ -0,0 +1,164 @@ +package connectors.misp + +import java.util.Date +import javax.inject.Inject + +import scala.collection.immutable +import scala.concurrent.{ ExecutionContext, Future } +import scala.concurrent.duration._ +import scala.util.{ Failure, Success, Try } + +import play.api.Logger +import play.api.inject.ApplicationLifecycle +import play.api.libs.json._ + +import akka.NotUsed +import akka.actor.ActorSystem +import akka.stream.Materializer +import akka.stream.scaladsl.{ Sink, Source } +import models.{ Alert, AlertStatus } +import services.{ AlertSrv, ArtifactSrv, CaseSrv, UserSrv } +import JsonFormat.mispAlertWrites + +import org.elastic4play.controllers.Fields +import org.elastic4play.services.{ AuthContext, MigrationSrv, TempSrv } + +class MispSynchro @Inject() ( + mispConfig: MispConfig, + migrationSrv: MigrationSrv, + mispSrv: MispSrv, + caseSrv: CaseSrv, + artifactSrv: ArtifactSrv, + alertSrv: AlertSrv, + userSrv: UserSrv, + tempSrv: TempSrv, + lifecycle: ApplicationLifecycle, + system: ActorSystem, + implicit val ec: ExecutionContext, + implicit val mat: Materializer) { + + private[misp] lazy val logger = Logger(getClass) + + private[misp] def initScheduler(): Unit = { + val task = system.scheduler.schedule(0.seconds, mispConfig.interval) { + if (migrationSrv.isReady) { + logger.info("Update of MISP events is starting ...") + userSrv + .inInitAuthContext { implicit authContext ⇒ + synchronize().andThen { case _ ⇒ tempSrv.releaseTemporaryFiles() } + } + .onComplete { + case Success(a) ⇒ + logger.info("Misp synchronization completed") + a.collect { + case Failure(t) ⇒ logger.warn(s"Update MISP error", t) + } + case Failure(t) ⇒ logger.info("Misp synchronization failed", t) + } + } + else { + logger.info("MISP synchronization cancel, database is not ready") + } + } + lifecycle.addStopHook { () ⇒ + logger.info("Stopping MISP fetching ...") + task.cancel() + Future.successful(()) + } + } + + initScheduler() + + def synchronize()(implicit authContext: AuthContext): Future[Seq[Try[Alert]]] = { + import org.elastic4play.services.QueryDSL._ + + // for each MISP server + Source(mispConfig.connections.toList) + // get last synchronization + .mapAsyncUnordered(1) { mispConnection ⇒ + alertSrv.stats(and("type" ~= "misp", "source" ~= mispConnection.name), Seq(selectMax("lastSyncDate"))) + .map { maxLastSyncDate ⇒ mispConnection → new Date((maxLastSyncDate \ "max_lastSyncDate").as[Long]) } + .recover { case _ ⇒ mispConnection → new Date(0) } + } + .flatMapConcat { + case (mispConnection, lastSyncDate) ⇒ + synchronize(mispConnection, Some(lastSyncDate)) + } + .runWith(Sink.seq) + } + + def fullSynchronize()(implicit authContext: AuthContext): Future[immutable.Seq[Try[Alert]]] = { + Source(mispConfig.connections.toList) + .flatMapConcat(mispConnection ⇒ synchronize(mispConnection, None)) + .runWith(Sink.seq) + } + + def synchronize(mispConnection: MispConnection, lastSyncDate: Option[Date])(implicit authContext: AuthContext): Source[Try[Alert], NotUsed] = { + logger.info(s"Synchronize MISP ${mispConnection.name} from $lastSyncDate") + // get events that have been published after the last synchronization + mispSrv.getEventsFromDate(mispConnection, lastSyncDate.getOrElse(new Date(0))) + // get related alert + .mapAsyncUnordered(1) { event ⇒ + logger.trace(s"Looking for alert misp:${event.source}:${event.sourceRef}") + alertSrv.get("misp", event.source, event.sourceRef) + .map((event, _)) + } + .mapAsyncUnordered(1) { + case (event, alert) ⇒ + logger.trace(s"MISP synchro ${mispConnection.name}, event ${event.sourceRef}, alert ${alert.fold("no alert")(a ⇒ "alert " + a.alertId() + "last sync at " + a.lastSyncDate())}") + logger.info(s"getting MISP event ${event.source}:${event.sourceRef}") + mispSrv.getAttributes(mispConnection, event.sourceRef, lastSyncDate.flatMap(_ ⇒ alert.map(_.lastSyncDate()))) + .map((event, alert, _)) + } + .mapAsyncUnordered(1) { + // if there is no related alert, create a new one + case (event, None, attrs) ⇒ + logger.info(s"MISP event ${event.source}:${event.sourceRef} has no related alert, create it with ${attrs.size} observable(s)") + val alertJson = Json.toJson(event).as[JsObject] + + ("type" → JsString("misp")) + + ("caseTemplate" → mispConnection.caseTemplate.fold[JsValue](JsNull)(JsString)) + + ("artifacts" → JsArray(attrs)) + alertSrv.create(Fields(alertJson)) + .map(Success(_)) + .recover { case t ⇒ Failure(t) } + + // if a related alert exists and we follow it or a fullSync, update it + case (event, Some(alert), attrs) if alert.follow() || lastSyncDate.isEmpty ⇒ + logger.info(s"MISP event ${event.source}:${event.sourceRef} has related alert, update it with ${attrs.size} observable(s)") + val alertJson = Json.toJson(event).as[JsObject] - + "type" - + "source" - + "sourceRef" - + "caseTemplate" - + "date" + + ("artifacts" → JsArray(attrs)) + + // if this is a full synchronization, don't update alert status + ("status" → (if (lastSyncDate.isEmpty) Json.toJson(alert.status()) + else alert.status() match { + case AlertStatus.New ⇒ Json.toJson(AlertStatus.New) + case _ ⇒ Json.toJson(AlertStatus.Updated) + })) + logger.debug(s"Update alert ${alert.id} with\n$alertJson") + val fAlert = alertSrv.update(alert.id, Fields(alertJson)) + // if a case have been created, update it + (alert.caze() match { + case None ⇒ fAlert + case Some(caze) ⇒ + for { + a ← fAlert + // if this is a full synchronization, don't update case status + caseFields = if (lastSyncDate.isEmpty) Fields(alert.toCaseJson).unset("status") + else Fields(alert.toCaseJson) + _ ← caseSrv.update(caze, caseFields) + _ ← artifactSrv.create(caze, attrs.map(Fields.apply)) + } yield a + }) + .map(Success(_)) + .recover { case t ⇒ Failure(t) } + + // if the alert is not followed, do nothing + case (_, Some(alert), _) ⇒ + Future.successful(Success(alert)) + } + } +} diff --git a/thehive-misp/app/connectors/misp/UpdateMispAlertArtifactActor.scala b/thehive-misp/app/connectors/misp/UpdateMispAlertArtifactActor.scala new file mode 100644 index 0000000000..ba6989cb15 --- /dev/null +++ b/thehive-misp/app/connectors/misp/UpdateMispAlertArtifactActor.scala @@ -0,0 +1,58 @@ +package connectors.misp + +import javax.inject.{ Inject, Singleton } + +import scala.concurrent.ExecutionContext +import scala.util.{ Failure, Success } + +import play.api.Logger + +import akka.actor.Actor +import models.UpdateMispAlertArtifact +import services.UserSrv + +import org.elastic4play.services.EventSrv + +/** + * This actor listens message from migration (message UpdateMispAlertArtifact) which indicates that artifacts in + * MISP event must be retrieved in inserted in alerts. + * + * @param eventSrv event bus used to receive migration message + * @param userSrv user service used to do operations on database without real user request + * @param mispSrv misp service to invoke artifact update action + * @param ec execution context + */ +@Singleton +class UpdateMispAlertArtifactActor @Inject() ( + eventSrv: EventSrv, + userSrv: UserSrv, + mispSrv: MispSrv, + implicit val ec: ExecutionContext) extends Actor { + + private[UpdateMispAlertArtifactActor] lazy val logger = Logger(getClass) + override def preStart(): Unit = { + eventSrv.subscribe(self, classOf[UpdateMispAlertArtifact]) + super.preStart() + } + + override def postStop(): Unit = { + eventSrv.unsubscribe(self) + super.postStop() + } + + override def receive: Receive = { + case UpdateMispAlertArtifact() ⇒ + logger.info("UpdateMispAlertArtifact") + userSrv + .inInitAuthContext { implicit authContext ⇒ + mispSrv.updateMispAlertArtifact() + } + .onComplete { + case Success(_) ⇒ logger.info("Artifacts in MISP alerts updated") + case Failure(error) ⇒ logger.error("Update MISP alert artifacts error :", error) + } + () + case msg ⇒ + logger.info(s"Receiving unexpected message: $msg (${msg.getClass})") + } +} \ No newline at end of file From 830fef3dec240464e42ba1a163d17750e664dff4 Mon Sep 17 00:00:00 2001 From: To-om Date: Mon, 28 Aug 2017 15:50:07 +0200 Subject: [PATCH 08/49] #293 hide stacktrace if connection to web hook fails --- thehive-backend/app/services/WebHook.scala | 2 ++ 1 file changed, 2 insertions(+) diff --git a/thehive-backend/app/services/WebHook.scala b/thehive-backend/app/services/WebHook.scala index 3ef78d89fb..cd0ca5dbc0 100644 --- a/thehive-backend/app/services/WebHook.scala +++ b/thehive-backend/app/services/WebHook.scala @@ -1,5 +1,6 @@ package services +import java.net.ConnectException import javax.inject.Inject import scala.concurrent.ExecutionContext @@ -14,6 +15,7 @@ case class WebHook(name: String, ws: WSRequest)(implicit ec: ExecutionContext) { def send(obj: JsObject) = ws.post(obj).onComplete { case Success(resp) if resp.status / 100 != 2 ⇒ logger.error(s"WebHook returns status ${resp.status} ${resp.statusText}") + case Failure(ce: ConnectException) ⇒ logger.error(s"Connection to WebHook $name error", ce) case Failure(error) ⇒ logger.error("WebHook call error", error) case _ ⇒ } From 6c4489daf1413e1835ce3e50cde69119ade88069 Mon Sep 17 00:00:00 2001 From: To-om Date: Mon, 28 Aug 2017 15:58:17 +0200 Subject: [PATCH 09/49] #292 Fix MISP threat level and Thehive severity convertion --- thehive-misp/app/connectors/misp/JsonFormat.scala | 2 +- thehive-misp/app/connectors/misp/MispExport.scala | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/thehive-misp/app/connectors/misp/JsonFormat.scala b/thehive-misp/app/connectors/misp/JsonFormat.scala index cd5a3e4379..63fc4617ee 100644 --- a/thehive-misp/app/connectors/misp/JsonFormat.scala +++ b/thehive-misp/app/connectors/misp/JsonFormat.scala @@ -39,7 +39,7 @@ object JsonFormat { isPublished, s"#$eventId ${info.trim}", s"Imported from MISP Event #$eventId, created at $date", - threatLevel.toLong, + 4 - threatLevel.toLong, alertTags, tlp, "") diff --git a/thehive-misp/app/connectors/misp/MispExport.scala b/thehive-misp/app/connectors/misp/MispExport.scala index d8e99a23fb..6909f7bad3 100644 --- a/thehive-misp/app/connectors/misp/MispExport.scala +++ b/thehive-misp/app/connectors/misp/MispExport.scala @@ -71,11 +71,11 @@ class MispExport @Inject() ( .map(_._1) } - def createEvent(mispConnection: MispConnection, title: String, severity: String, date: Date, attributes: Seq[ExportedMispAttribute]): Future[(String, Seq[ExportedMispAttribute])] = { + def createEvent(mispConnection: MispConnection, title: String, severity: Long, date: Date, attributes: Seq[ExportedMispAttribute]): Future[(String, Seq[ExportedMispAttribute])] = { val mispEvent = Json.obj( "Event" → Json.obj( "distribution" → 0, - "threat_level_id" → severity, + "threat_level_id" → (4 - severity), "analysis" → 0, "info" → title, "date" → dateFormat.format(date), @@ -150,7 +150,7 @@ class MispExport @Inject() ( simpleAttributes = uniqueAttributes.filter(_.value.isLeft) // FIXME used only if event doesn't exist (eventId, existingAttributes) ← maybeEventId.fold { // if no event is associated to this case, create a new one - createEvent(mispConnection, caze.title(), caze.severity().toString, caze.startDate(), simpleAttributes).map { + createEvent(mispConnection, caze.title(), caze.severity(), caze.startDate(), simpleAttributes).map { case (eventId, exportedAttributes) ⇒ eventId → exportedAttributes.map(_.value.left.get) } } { eventId ⇒ // if an event already exists, retrieve its attributes in order to export only new one From 2ad04fb54cb8d1550ea77245c60f0d531402e494 Mon Sep 17 00:00:00 2001 From: To-om Date: Mon, 28 Aug 2017 16:08:29 +0200 Subject: [PATCH 10/49] #52 Fix circular dependency error --- thehive-misp/app/connectors/misp/MispConfig.scala | 3 ++- thehive-misp/app/connectors/misp/MispExport.scala | 6 ++++-- thehive-misp/app/connectors/misp/MispSynchro.scala | 6 ++++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/thehive-misp/app/connectors/misp/MispConfig.scala b/thehive-misp/app/connectors/misp/MispConfig.scala index 2048cabf0a..28350d045e 100644 --- a/thehive-misp/app/connectors/misp/MispConfig.scala +++ b/thehive-misp/app/connectors/misp/MispConfig.scala @@ -1,6 +1,6 @@ package connectors.misp -import javax.inject.Inject +import javax.inject.{ Inject, Singleton } import scala.concurrent.duration.{ DurationInt, FiniteDuration } import scala.util.Try @@ -9,6 +9,7 @@ import play.api.Configuration import services.CustomWSAPI +@Singleton class MispConfig(val interval: FiniteDuration, val connections: Seq[MispConnection]) { def this(configuration: Configuration, defaultCaseTemplate: Option[String], globalWS: CustomWSAPI) = this( diff --git a/thehive-misp/app/connectors/misp/MispExport.scala b/thehive-misp/app/connectors/misp/MispExport.scala index 6909f7bad3..76fd1dbdca 100644 --- a/thehive-misp/app/connectors/misp/MispExport.scala +++ b/thehive-misp/app/connectors/misp/MispExport.scala @@ -2,7 +2,7 @@ package connectors.misp import java.text.SimpleDateFormat import java.util.Date -import javax.inject.Inject +import javax.inject.{ Inject, Provider, Singleton } import scala.concurrent.{ ExecutionContext, Future } import scala.util.Try @@ -21,16 +21,18 @@ import org.elastic4play.models.JsonFormat.baseModelEntityWrites import org.elastic4play.services.{ AttachmentSrv, AuthContext } import org.elastic4play.utils.RichFuture +@Singleton class MispExport @Inject() ( mispConfig: MispConfig, mispSrv: MispSrv, artifactSrv: ArtifactSrv, - alertSrv: AlertSrv, + alertSrvProvider: Provider[AlertSrv], attachmentSrv: AttachmentSrv, implicit val ec: ExecutionContext, implicit val mat: Materializer) extends MispConverter { lazy val dateFormat = new SimpleDateFormat("yy-MM-dd") + private[misp] lazy val alertSrv = alertSrvProvider.get def relatedMispEvent(mispName: String, caseId: String): Future[(Option[String], Option[String])] = { import org.elastic4play.services.QueryDSL._ diff --git a/thehive-misp/app/connectors/misp/MispSynchro.scala b/thehive-misp/app/connectors/misp/MispSynchro.scala index de2912794a..98e4e2dd41 100644 --- a/thehive-misp/app/connectors/misp/MispSynchro.scala +++ b/thehive-misp/app/connectors/misp/MispSynchro.scala @@ -1,7 +1,7 @@ package connectors.misp import java.util.Date -import javax.inject.Inject +import javax.inject.{ Inject, Provider, Singleton } import scala.collection.immutable import scala.concurrent.{ ExecutionContext, Future } @@ -23,13 +23,14 @@ import JsonFormat.mispAlertWrites import org.elastic4play.controllers.Fields import org.elastic4play.services.{ AuthContext, MigrationSrv, TempSrv } +@Singleton class MispSynchro @Inject() ( mispConfig: MispConfig, migrationSrv: MigrationSrv, mispSrv: MispSrv, caseSrv: CaseSrv, artifactSrv: ArtifactSrv, - alertSrv: AlertSrv, + alertSrvProvider: Provider[AlertSrv], userSrv: UserSrv, tempSrv: TempSrv, lifecycle: ApplicationLifecycle, @@ -38,6 +39,7 @@ class MispSynchro @Inject() ( implicit val mat: Materializer) { private[misp] lazy val logger = Logger(getClass) + private[misp] lazy val alertSrv = alertSrvProvider.get private[misp] def initScheduler(): Unit = { val task = system.scheduler.schedule(0.seconds, mispConfig.interval) { From 4fd91e20a9dc57260c1c00567549afa7e6ba9903 Mon Sep 17 00:00:00 2001 From: Nabil Adouani Date: Mon, 28 Aug 2017 16:23:59 +0200 Subject: [PATCH 11/49] #295 Display file name in observable conflict dialog --- ui/app/scripts/controllers/case/ObservableCreationCtrl.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/scripts/controllers/case/ObservableCreationCtrl.js b/ui/app/scripts/controllers/case/ObservableCreationCtrl.js index 3fc95327e4..41665bfecc 100644 --- a/ui/app/scripts/controllers/case/ObservableCreationCtrl.js +++ b/ui/app/scripts/controllers/case/ObservableCreationCtrl.js @@ -102,7 +102,7 @@ return _.map(failures, function(observable) { return { - data: observable.object.data, + data: observable.object.dataType === 'file' ? observable.object.attachment.name : observable.object.data, type: observable.type }; }); From 1b49540b33fd86aaff889cb983371880fba417be Mon Sep 17 00:00:00 2001 From: To-om Date: Tue, 29 Aug 2017 15:17:27 +0200 Subject: [PATCH 12/49] #52 Support bidirectional synchronization with MISP --- .../app/connectors/misp/JsonFormat.scala | 19 +++- .../app/connectors/misp/MispConverter.scala | 66 +++++++------ .../app/connectors/misp/MispExport.scala | 91 ++++++++++-------- .../app/connectors/misp/MispModel.scala | 23 ++++- .../app/connectors/misp/MispSrv.scala | 66 ++++++++----- .../app/connectors/misp/MispSynchro.scala | 94 +++++++++++-------- 6 files changed, 226 insertions(+), 133 deletions(-) diff --git a/thehive-misp/app/connectors/misp/JsonFormat.scala b/thehive-misp/app/connectors/misp/JsonFormat.scala index 63fc4617ee..52ee070c91 100644 --- a/thehive-misp/app/connectors/misp/JsonFormat.scala +++ b/thehive-misp/app/connectors/misp/JsonFormat.scala @@ -6,6 +6,8 @@ import play.api.libs.json.JsLookupResult.jsLookupResultToJsLookup import play.api.libs.json.JsValue.jsValueToJsLookup import play.api.libs.json._ +import org.elastic4play.services.JsonFormat.attachmentFormat + object JsonFormat { implicit val mispAlertReads: Reads[MispAlert] = Reads[MispAlert] { json ⇒ @@ -64,7 +66,7 @@ object JsonFormat { date, comment, value, - tags :+ s"MISP:category$category" :+ s"MISP:type=$tpe")) + tags)) implicit val exportedAttributeWrites: Writes[ExportedMispAttribute] = Writes[ExportedMispAttribute] { attribute ⇒ Json.obj( @@ -73,4 +75,17 @@ object JsonFormat { "value" → attribute.value.fold[String](identity, _.name), "comment" → attribute.comment) } -} + + implicit val mispArtifactWrites: Writes[MispArtifact] = OWrites[MispArtifact] { artifact ⇒ + Json.obj( + "dataType" → artifact.dataType, + "message" → artifact.message, + "tlp" → artifact.tlp, + "tags" → artifact.tags, + "startDate" → artifact.startDate) + (artifact.value match { + case SimpleArtifactData(data) ⇒ "data" → JsString(data) + case RemoteAttachmentArtifact(filename, reference, tpe) ⇒ "remoteAttachment" → Json.obj("filename" → filename, "reference" → reference, "type" → tpe) + case AttachmentArtifact(attachment) ⇒ "attachment" → Json.toJson(attachment) + }) + } +} \ No newline at end of file diff --git a/thehive-misp/app/connectors/misp/MispConverter.scala b/thehive-misp/app/connectors/misp/MispConverter.scala index db0209cd4a..c4e5446bde 100644 --- a/thehive-misp/app/connectors/misp/MispConverter.scala +++ b/thehive-misp/app/connectors/misp/MispConverter.scala @@ -1,37 +1,49 @@ package connectors.misp -import play.api.libs.json.{ JsObject, JsString, Json } - trait MispConverter { - def convertAttribute(mispAttribute: MispAttribute): Seq[JsObject] = { - val dataType = toArtifact(mispAttribute.tpe) + def convertAttribute(mispAttribute: MispAttribute): Seq[MispArtifact] = { val tags = Seq(s"MISP:type=${mispAttribute.tpe}", s"MISP:category=${mispAttribute.category}") - val fields = Json.obj( - "data" → mispAttribute.value, - "dataType" → dataType, - "message" → mispAttribute.comment, - "startDate" → mispAttribute.date, - "tags" → tags) + if (mispAttribute.tpe == "attachment" || mispAttribute.tpe == "malware-sample") { + Seq( + MispArtifact( + value = RemoteAttachmentArtifact(mispAttribute.value.split("\\|").head, mispAttribute.id, mispAttribute.tpe), + dataType = "file", + message = mispAttribute.comment, + tlp = 0, + tags = tags ++ mispAttribute.tags, + startDate = mispAttribute.date)) + } + else { + val dataType = toArtifact(mispAttribute.tpe) + val artifact = + MispArtifact( + value = SimpleArtifactData(mispAttribute.value), + dataType = dataType, + message = mispAttribute.comment, + tlp = 0, + tags = tags ++ mispAttribute.tags, + startDate = mispAttribute.date) - val types = mispAttribute.tpe.split('|').toSeq - if (types.length > 1) { - val values = mispAttribute.value.split('|').toSeq - val typesValues = types.zipAll(values, "noType", "noValue") - val additionnalMessage = typesValues - .map { - case (t, v) ⇒ s"$t: $v" + val types = mispAttribute.tpe.split('|').toSeq + if (types.length > 1) { + val values = mispAttribute.value.split('|').toSeq + val typesValues = types.zipAll(values, "noType", "noValue") + val additionnalMessage = typesValues + .map { + case (t, v) ⇒ s"$t: $v" + } + .mkString("\n") + typesValues.map { + case (tpe, value) ⇒ + artifact.copy( + dataType = toArtifact(tpe), + value = SimpleArtifactData(value), + message = mispAttribute.comment + "\n" + additionnalMessage) } - .mkString("\n") - typesValues.map { - case (tpe, value) ⇒ - fields + - ("dataType" → JsString(toArtifact(tpe))) + - ("data" → JsString(value)) + - ("message" → JsString(mispAttribute.comment + "\n" + additionnalMessage)) } - } - else { - Seq(fields) + else { + Seq(artifact) + } } } diff --git a/thehive-misp/app/connectors/misp/MispExport.scala b/thehive-misp/app/connectors/misp/MispExport.scala index 76fd1dbdca..d80b912151 100644 --- a/thehive-misp/app/connectors/misp/MispExport.scala +++ b/thehive-misp/app/connectors/misp/MispExport.scala @@ -18,7 +18,8 @@ import akka.stream.Materializer import org.elastic4play.InternalError import org.elastic4play.controllers.Fields import org.elastic4play.models.JsonFormat.baseModelEntityWrites -import org.elastic4play.services.{ AttachmentSrv, AuthContext } +import org.elastic4play.services.{ Attachment, AttachmentSrv, AuthContext } +import org.elastic4play.services.JsonFormat.attachmentFormat import org.elastic4play.utils.RichFuture @Singleton @@ -43,23 +44,6 @@ class MispExport @Inject() ( .map(alertIdSource ⇒ alertIdSource.map(_._1) → alertIdSource.map(_._2)) } - def buildAttributeList(caze: Case): Future[Seq[ExportedMispAttribute]] = { - import org.elastic4play.services.QueryDSL._ - artifactSrv - .find(parent("case", withId(caze.id)), Some("all"), Nil) - ._1 - .map { artifact ⇒ - val (category, tpe) = fromArtifact(artifact.dataType(), artifact.data()) - val value = (artifact.data(), artifact.attachment()) match { - case (Some(data), None) ⇒ Left(data) - case (None, Some(attachment)) ⇒ Right(attachment) - case _ ⇒ sys.error("???") - } - ExportedMispAttribute(artifact, tpe, category, value, artifact.message()) - } - .runWith(Sink.seq) - } - def removeDuplicateAttributes(attributes: Seq[ExportedMispAttribute]): Seq[ExportedMispAttribute] = { val attrIndex = attributes.zipWithIndex @@ -147,39 +131,64 @@ class MispExport @Inject() ( for { (maybeAlertId, maybeEventId) ← relatedMispEvent(mispName, caze.id) - attributes ← buildAttributeList(caze) + _ = println(s"[0] $maybeAlertId $maybeEventId") + attributes ← mispSrv.getAttributesFromCase(caze) + _ = println(s"[1] $attributes") uniqueAttributes = removeDuplicateAttributes(attributes) - simpleAttributes = uniqueAttributes.filter(_.value.isLeft) // FIXME used only if event doesn't exist + _ = println(s"[2] $uniqueAttributes") (eventId, existingAttributes) ← maybeEventId.fold { + val simpleAttributes = uniqueAttributes.filter(_.value.isLeft) + println(s"[3] $simpleAttributes") // if no event is associated to this case, create a new one createEvent(mispConnection, caze.title(), caze.severity(), caze.startDate(), simpleAttributes).map { - case (eventId, exportedAttributes) ⇒ eventId → exportedAttributes.map(_.value.left.get) + case (eventId, exportedAttributes) ⇒ eventId → exportedAttributes.map(_.value.map(_.name)) } } { eventId ⇒ // if an event already exists, retrieve its attributes in order to export only new one - mispSrv.getAttributes(mispConnection, eventId, None).map { attributes ⇒ - eventId → attributes.map { attribute ⇒ - (attribute \ "data").asOpt[String].getOrElse((attribute \ "remoteAttachment" \ "filename").as[String]) + mispSrv.getAttributesFromMisp(mispConnection, eventId, None).map { attributes ⇒ + eventId → attributes.map { + case MispArtifact(SimpleArtifactData(data), _, _, _, _, _) ⇒ Left(data) + case MispArtifact(RemoteAttachmentArtifact(filename, _, _), _, _, _, _, _) ⇒ Right(filename) + case MispArtifact(AttachmentArtifact(Attachment(filename, _, _, _, _)), _, _, _, _, _) ⇒ Right(filename) } } } - newAttributes = uniqueAttributes.filterNot(attr ⇒ existingAttributes.contains(attr.value.fold(identity, _.name))) + _ = println(s"[4] $existingAttributes") + newAttributes = uniqueAttributes.filterNot(attr ⇒ existingAttributes.contains(attr.value.map(_.name))) + _ = println(s"[5] $newAttributes") exportedArtifact ← Future.traverse(newAttributes)(attr ⇒ exportAttribute(mispConnection, eventId, attr).toTry) - alertFields = Fields(Json.obj( - "type" → "misp", - "source" → mispName, - "sourceRef" → eventId, - "date" → caze.startDate(), - "lastSyncDate" → new Date(0), - "case" → caze.id, - "title" → caze.title(), - "description" → "Case have been exported to MISP", - "severity" → caze.severity(), - "tags" → caze.tags(), - "tlp" → caze.tlp(), - "artifacts" → uniqueAttributes.map(_.artifact), - "status" → "Imported", - "follow" → false)) - alert ← maybeAlertId.fold(alertSrv.create(alertFields))(alertId ⇒ alertSrv.update(alertId, alertFields)) + _ = println(s"[6] $exportedArtifact") + alert ← maybeAlertId.fold { + alertSrv.create(Fields(Json.obj( + "type" → "misp", + "source" → mispName, + "sourceRef" → eventId, + "date" → caze.startDate(), + "lastSyncDate" → new Date(0), + "case" → caze.id, + "title" → caze.title(), + "description" → "Case have been exported to MISP", + "severity" → caze.severity(), + "tags" → caze.tags(), + "tlp" → caze.tlp(), + "artifacts" → uniqueAttributes.map(_.artifact), + "status" → "Imported", + "follow" → false))) + } { alertId ⇒ + val artifacts = uniqueAttributes.map { exportedArtifact ⇒ + Json.obj( + "data" → exportedArtifact.artifact.data(), + "dataType" → exportedArtifact.artifact.dataType(), + "message" → exportedArtifact.artifact.message(), + "startDate" → exportedArtifact.artifact.startDate(), + "attachment" → exportedArtifact.artifact.attachment(), + "tlp" → exportedArtifact.artifact.tlp(), + "tags" → exportedArtifact.artifact.tags(), + "ioc" → exportedArtifact.artifact.ioc()) + } + alertSrv.update(alertId, Fields(Json.obj( + "artifacts" → artifacts, + "status" → "Imported"))) + } } yield alert.id → exportedArtifact } } diff --git a/thehive-misp/app/connectors/misp/MispModel.scala b/thehive-misp/app/connectors/misp/MispModel.scala index b90a4c9acc..9b83fcc05a 100644 --- a/thehive-misp/app/connectors/misp/MispModel.scala +++ b/thehive-misp/app/connectors/misp/MispModel.scala @@ -5,6 +5,18 @@ import java.util.Date import models.Artifact import org.elastic4play.services.Attachment +import org.elastic4play.utils.Hash + +sealed trait ArtifactData +case class SimpleArtifactData(data: String) extends ArtifactData +case class AttachmentArtifact(attachment: Attachment) extends ArtifactData { + def name: String = attachment.name + def hashes: Seq[Hash] = attachment.hashes + def size: Long = attachment.size + def contentType: String = attachment.contentType + def id: String = attachment.id +} +case class RemoteAttachmentArtifact(filename: String, reference: String, tpe: String) extends ArtifactData case class MispAlert( source: String, @@ -35,4 +47,13 @@ case class ExportedMispAttribute( value: Either[String, Attachment], comment: Option[String]) -case class MispExportError(message: String, artifact: Artifact) extends Exception(message) \ No newline at end of file +case class MispArtifact( + value: ArtifactData, + dataType: String, + message: String, + tlp: Long, + tags: Seq[String], + startDate: Date) + +case class MispExportError(message: String, artifact: Artifact) extends Exception(message) + diff --git a/thehive-misp/app/connectors/misp/MispSrv.scala b/thehive-misp/app/connectors/misp/MispSrv.scala index 8f7097b5aa..d55ef869dc 100644 --- a/thehive-misp/app/connectors/misp/MispSrv.scala +++ b/thehive-misp/app/connectors/misp/MispSrv.scala @@ -23,8 +23,7 @@ import net.lingala.zip4j.model.FileHeader import services._ import org.elastic4play.controllers.{ Fields, FileInputValue } -import org.elastic4play.services.{ AuthContext, TempSrv } -import org.elastic4play.utils.RichJson +import org.elastic4play.services.{ Attachment, AuthContext, TempSrv } import org.elastic4play.{ InternalError, NotFoundError } @Singleton @@ -74,14 +73,35 @@ class MispSrv @Inject() ( val eventsSize = events.size if (eventJsonSize != eventsSize) logger.warn(s"MISP returns $eventJsonSize events but only $eventsSize contain valid data") - events.toList + events.filter(_.lastSyncDate after fromDate).toList } } - def getAttributes( + def getAttributesFromCase(caze: Case): Future[Seq[ExportedMispAttribute]] = { + import org.elastic4play.services.QueryDSL._ + val (artifacts, totalArtifacts) = artifactSrv + .find(and(withParent(caze), "status" ~= "Ok"), Some("all"), Nil) + totalArtifacts.foreach(t ⇒ println(s"Case ${caze.id} has $t artifact(s)")) + artifacts + .map { artifact ⇒ + println(s"[-] $artifact") + val (category, tpe) = fromArtifact(artifact.dataType(), artifact.data()) + val value = (artifact.data(), artifact.attachment()) match { + case (Some(data), None) ⇒ Left(data) + case (None, Some(attachment)) ⇒ Right(attachment) + case _ ⇒ + logger.error(s"Artifact $artifact has neither data nor attachment") + sys.error("???") + } + ExportedMispAttribute(artifact, tpe, category, value, artifact.message()) + } + .runWith(Sink.seq) + } + + def getAttributesFromMisp( mispConnection: MispConnection, eventId: String, - fromDate: Option[Date]): Future[Seq[JsObject]] = { + fromDate: Option[Date]): Future[Seq[MispArtifact]] = { val date = fromDate.fold(0L)(_.getTime / 1000) @@ -94,27 +114,23 @@ class MispSrv @Inject() ( // add ("deleted" → "only") to see only deleted attributes .map { response ⇒ val refDate = fromDate.getOrElse(new Date(0)) - val artifactTags = JsString(s"src:${mispConnection.name}") +: JsArray(mispConnection.artifactTags.map(JsString)) + val artifactTags = s"src:${mispConnection.name}" +: mispConnection.artifactTags (Json.parse(response.body) \ "response" \\ "Attribute") .flatMap(_.as[Seq[MispAttribute]]) .filter(_.date after refDate) - .flatMap { - case a if a.tpe == "attachment" || a.tpe == "malware-sample" ⇒ - Seq( - Json.obj( - "dataType" → "file", - "message" → a.comment, - "tags" → (artifactTags.value ++ a.tags.map(JsString)), - "remoteAttachment" → Json.obj( - "filename" → a.value, - "reference" → a.id, - "type" → a.tpe), - "startDate" → a.date)) - case a ⇒ convertAttribute(a).map { j ⇒ - val tags = artifactTags ++ (j \ "tags").asOpt[JsArray].getOrElse(JsArray(Nil)) - j.setIfAbsent("tlp", 2L) + ("tags" → tags) - } + .flatMap(convertAttribute) + .groupBy { + case MispArtifact(SimpleArtifactData(data), dataType, _, _, _, _) ⇒ dataType → Right(data) + case MispArtifact(RemoteAttachmentArtifact(filename, _, _), dataType, _, _, _, _) ⇒ dataType → Left(filename) + case MispArtifact(AttachmentArtifact(Attachment(filename, _, _, _, _)), dataType, _, _, _, _) ⇒ dataType → Left(filename) + } + .values + .map { mispArtifact ⇒ + mispArtifact.head.copy( + tags = (mispArtifact.head.tags ++ artifactTags).distinct, + tlp = 2L) } + .toSeq } } @@ -207,7 +223,7 @@ class MispSrv @Inject() ( else { getInstanceConfig(alert.source()) .flatMap { mcfg ⇒ - getAttributes(mcfg, alert.sourceRef(), None) + getAttributesFromMisp(mcfg, alert.sourceRef(), None) } .map(alert → _) .recover { @@ -224,7 +240,7 @@ class MispSrv @Inject() ( .mapAsyncUnordered(5) { case (alert, artifacts) ⇒ logger.info(s"Updating alert ${alert.id}") - alertSrv.update(alert.id, Fields.empty.set("artifacts", JsArray(artifacts))) + alertSrv.update(alert.id, Fields.empty.set("artifacts", Json.toJson(artifacts))) .recover { case t ⇒ logger.error(s"Update alert ${alert.id} fail", t) } @@ -276,10 +292,10 @@ class MispSrv @Inject() ( } } + private[MispSrv] val fileNameExtractor = """attachment; filename="(.*)"""".r def downloadAttachment( mispConnection: MispConnection, attachmentId: String)(implicit authContext: AuthContext): Future[FileInputValue] = { - val fileNameExtractor = """attachment; filename="(.*)"""".r mispConnection(s"attributes/download/$attachmentId") .withMethod("GET") diff --git a/thehive-misp/app/connectors/misp/MispSynchro.scala b/thehive-misp/app/connectors/misp/MispSynchro.scala index 98e4e2dd41..d9d4139774 100644 --- a/thehive-misp/app/connectors/misp/MispSynchro.scala +++ b/thehive-misp/app/connectors/misp/MispSynchro.scala @@ -16,12 +16,13 @@ import akka.NotUsed import akka.actor.ActorSystem import akka.stream.Materializer import akka.stream.scaladsl.{ Sink, Source } -import models.{ Alert, AlertStatus } +import connectors.misp.JsonFormat.mispArtifactWrites +import models.{ Alert, AlertStatus, Artifact, CaseStatus } import services.{ AlertSrv, ArtifactSrv, CaseSrv, UserSrv } import JsonFormat.mispAlertWrites import org.elastic4play.controllers.Fields -import org.elastic4play.services.{ AuthContext, MigrationSrv, TempSrv } +import org.elastic4play.services.{ Attachment, AuthContext, MigrationSrv, TempSrv } @Singleton class MispSynchro @Inject() ( @@ -95,6 +96,31 @@ class MispSynchro @Inject() ( .runWith(Sink.seq) } + def updateArtifacts(mispConnection: MispConnection, caseId: String, mispArtifacts: Seq[MispArtifact])(implicit authContext: AuthContext): Future[Seq[Try[Artifact]]] = { + import org.elastic4play.services.QueryDSL._ + + for { + // Either data or filename + existingArtifacts: Seq[Either[String, String]] ← artifactSrv.find(and(withParent("case", caseId), "status" ~= "Ok"), Some("all"), Nil)._1.map { artifact ⇒ + artifact.data().map(Left.apply).getOrElse(Right(artifact.attachment().get.name)) + } + .runWith(Sink.seq) + newAttributes ← Future.traverse(mispArtifacts) { + case artifact @ MispArtifact(SimpleArtifactData(data), _, _, _, _, _) if !existingArtifacts.contains(Right(data)) ⇒ Future.successful(Fields(Json.toJson(artifact).as[JsObject])) + case artifact @ MispArtifact(AttachmentArtifact(Attachment(filename, _, _, _, _)), _, _, _, _, _) if !existingArtifacts.contains(Left(filename)) ⇒ Future.successful(Fields(Json.toJson(artifact).as[JsObject])) + case artifact @ MispArtifact(RemoteAttachmentArtifact(filename, reference, tpe), _, _, _, _, _) if !existingArtifacts.contains(Left(filename)) ⇒ + mispSrv.downloadAttachment(mispConnection, reference) + .map { + case fiv if tpe == "malware-sample" ⇒ mispSrv.extractMalwareAttachment(fiv) + case fiv ⇒ fiv + } + .map(fiv ⇒ Fields(Json.toJson(artifact).as[JsObject]).unset("remoteAttachment").set("attachment", fiv)) + case _ ⇒ Future.successful(Fields.empty) + } + createdArtifacts ← artifactSrv.create(caseId, newAttributes.filterNot(_.isEmpty)) + } yield createdArtifacts + } + def synchronize(mispConnection: MispConnection, lastSyncDate: Option[Date])(implicit authContext: AuthContext): Source[Try[Alert], NotUsed] = { logger.info(s"Synchronize MISP ${mispConnection.name} from $lastSyncDate") // get events that have been published after the last synchronization @@ -109,7 +135,7 @@ class MispSynchro @Inject() ( case (event, alert) ⇒ logger.trace(s"MISP synchro ${mispConnection.name}, event ${event.sourceRef}, alert ${alert.fold("no alert")(a ⇒ "alert " + a.alertId() + "last sync at " + a.lastSyncDate())}") logger.info(s"getting MISP event ${event.source}:${event.sourceRef}") - mispSrv.getAttributes(mispConnection, event.sourceRef, lastSyncDate.flatMap(_ ⇒ alert.map(_.lastSyncDate()))) + mispSrv.getAttributesFromMisp(mispConnection, event.sourceRef, lastSyncDate.flatMap(_ ⇒ alert.map(_.lastSyncDate()))) .map((event, alert, _)) } .mapAsyncUnordered(1) { @@ -119,48 +145,42 @@ class MispSynchro @Inject() ( val alertJson = Json.toJson(event).as[JsObject] + ("type" → JsString("misp")) + ("caseTemplate" → mispConnection.caseTemplate.fold[JsValue](JsNull)(JsString)) + - ("artifacts" → JsArray(attrs)) + ("artifacts" → Json.toJson(attrs)) alertSrv.create(Fields(alertJson)) .map(Success(_)) .recover { case t ⇒ Failure(t) } - // if a related alert exists and we follow it or a fullSync, update it - case (event, Some(alert), attrs) if alert.follow() || lastSyncDate.isEmpty ⇒ + case (event, Some(alert), attrs) ⇒ logger.info(s"MISP event ${event.source}:${event.sourceRef} has related alert, update it with ${attrs.size} observable(s)") - val alertJson = Json.toJson(event).as[JsObject] - - "type" - - "source" - - "sourceRef" - - "caseTemplate" - - "date" + - ("artifacts" → JsArray(attrs)) + - // if this is a full synchronization, don't update alert status - ("status" → (if (lastSyncDate.isEmpty) Json.toJson(alert.status()) - else alert.status() match { - case AlertStatus.New ⇒ Json.toJson(AlertStatus.New) - case _ ⇒ Json.toJson(AlertStatus.Updated) - })) - logger.debug(s"Update alert ${alert.id} with\n$alertJson") - val fAlert = alertSrv.update(alert.id, Fields(alertJson)) - // if a case have been created, update it - (alert.caze() match { - case None ⇒ fAlert - case Some(caze) ⇒ + + alert.caze().fold[Future[Boolean]](Future.successful(lastSyncDate.isDefined && attrs.nonEmpty && alert.follow())) { + case caze if alert.follow() ⇒ for { - a ← fAlert - // if this is a full synchronization, don't update case status - caseFields = if (lastSyncDate.isEmpty) Fields(alert.toCaseJson).unset("status") - else Fields(alert.toCaseJson) - _ ← caseSrv.update(caze, caseFields) - _ ← artifactSrv.create(caze, attrs.map(Fields.apply)) - } yield a - }) + addedArtifacts ← updateArtifacts(mispConnection, caze, attrs) + updateStatus = lastSyncDate.nonEmpty && addedArtifacts.exists(_.isSuccess) + _ ← if (updateStatus) caseSrv.update(caze, Fields.empty.set("status", CaseStatus.Open.toString)) else Future.successful(()) + } yield updateStatus + case _ ⇒ Future.successful(false) + } + .flatMap { updateStatus ⇒ + val artifacts = JsArray(alert.artifacts() ++ attrs.map(Json.toJson(_))) + val alertJson = Json.toJson(event).as[JsObject] - + "type" - + "source" - + "sourceRef" - + "caseTemplate" - + "date" + + ("artifacts" → artifacts) + + ("status" → (if (!updateStatus) Json.toJson(alert.status()) + else alert.status() match { + case AlertStatus.New ⇒ Json.toJson(AlertStatus.New) + case _ ⇒ Json.toJson(AlertStatus.Updated) + })) + logger.debug(s"Update alert ${alert.id} with\n$alertJson") + alertSrv.update(alert.id, Fields(alertJson)) + } .map(Success(_)) .recover { case t ⇒ Failure(t) } - - // if the alert is not followed, do nothing - case (_, Some(alert), _) ⇒ - Future.successful(Success(alert)) } } } From 3d8ad5804a03c38e749c1caf5bd6f477d74242da Mon Sep 17 00:00:00 2001 From: To-om Date: Thu, 31 Aug 2017 15:48:45 +0200 Subject: [PATCH 13/49] #52 remove debug --- thehive-misp/app/connectors/misp/MispExport.scala | 8 +------- thehive-misp/app/connectors/misp/MispSrv.scala | 6 ++---- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/thehive-misp/app/connectors/misp/MispExport.scala b/thehive-misp/app/connectors/misp/MispExport.scala index d80b912151..eb6b4ce1c7 100644 --- a/thehive-misp/app/connectors/misp/MispExport.scala +++ b/thehive-misp/app/connectors/misp/MispExport.scala @@ -131,14 +131,10 @@ class MispExport @Inject() ( for { (maybeAlertId, maybeEventId) ← relatedMispEvent(mispName, caze.id) - _ = println(s"[0] $maybeAlertId $maybeEventId") attributes ← mispSrv.getAttributesFromCase(caze) - _ = println(s"[1] $attributes") uniqueAttributes = removeDuplicateAttributes(attributes) - _ = println(s"[2] $uniqueAttributes") (eventId, existingAttributes) ← maybeEventId.fold { val simpleAttributes = uniqueAttributes.filter(_.value.isLeft) - println(s"[3] $simpleAttributes") // if no event is associated to this case, create a new one createEvent(mispConnection, caze.title(), caze.severity(), caze.startDate(), simpleAttributes).map { case (eventId, exportedAttributes) ⇒ eventId → exportedAttributes.map(_.value.map(_.name)) @@ -152,11 +148,9 @@ class MispExport @Inject() ( } } } - _ = println(s"[4] $existingAttributes") newAttributes = uniqueAttributes.filterNot(attr ⇒ existingAttributes.contains(attr.value.map(_.name))) - _ = println(s"[5] $newAttributes") exportedArtifact ← Future.traverse(newAttributes)(attr ⇒ exportAttribute(mispConnection, eventId, attr).toTry) - _ = println(s"[6] $exportedArtifact") + alert ← maybeAlertId.fold { alert ← maybeAlertId.fold { alertSrv.create(Fields(Json.obj( "type" → "misp", diff --git a/thehive-misp/app/connectors/misp/MispSrv.scala b/thehive-misp/app/connectors/misp/MispSrv.scala index d55ef869dc..078d8336f2 100644 --- a/thehive-misp/app/connectors/misp/MispSrv.scala +++ b/thehive-misp/app/connectors/misp/MispSrv.scala @@ -79,12 +79,10 @@ class MispSrv @Inject() ( def getAttributesFromCase(caze: Case): Future[Seq[ExportedMispAttribute]] = { import org.elastic4play.services.QueryDSL._ - val (artifacts, totalArtifacts) = artifactSrv + artifactSrv .find(and(withParent(caze), "status" ~= "Ok"), Some("all"), Nil) - totalArtifacts.foreach(t ⇒ println(s"Case ${caze.id} has $t artifact(s)")) - artifacts + ._1 .map { artifact ⇒ - println(s"[-] $artifact") val (category, tpe) = fromArtifact(artifact.dataType(), artifact.data()) val value = (artifact.data(), artifact.attachment()) match { case (Some(data), None) ⇒ Left(data) From d9d24a814f7db0b50416ee52989f1da7710e95e7 Mon Sep 17 00:00:00 2001 From: To-om Date: Thu, 31 Aug 2017 16:18:56 +0200 Subject: [PATCH 14/49] #11 Fix typo --- thehive-misp/app/connectors/misp/MispExport.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/thehive-misp/app/connectors/misp/MispExport.scala b/thehive-misp/app/connectors/misp/MispExport.scala index eb6b4ce1c7..84c701b4df 100644 --- a/thehive-misp/app/connectors/misp/MispExport.scala +++ b/thehive-misp/app/connectors/misp/MispExport.scala @@ -150,7 +150,6 @@ class MispExport @Inject() ( } newAttributes = uniqueAttributes.filterNot(attr ⇒ existingAttributes.contains(attr.value.map(_.name))) exportedArtifact ← Future.traverse(newAttributes)(attr ⇒ exportAttribute(mispConnection, eventId, attr).toTry) - alert ← maybeAlertId.fold { alert ← maybeAlertId.fold { alertSrv.create(Fields(Json.obj( "type" → "misp", From ef673d52f5185db29ec6273c3131e38d93742a65 Mon Sep 17 00:00:00 2001 From: To-om Date: Fri, 1 Sep 2017 11:23:07 +0200 Subject: [PATCH 15/49] #52 Fix error handler --- .../app/connectors/misp/MispExport.scala | 38 +++++++++---------- .../app/connectors/misp/MispModel.scala | 4 +- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/thehive-misp/app/connectors/misp/MispExport.scala b/thehive-misp/app/connectors/misp/MispExport.scala index 84c701b4df..44bcc7508f 100644 --- a/thehive-misp/app/connectors/misp/MispExport.scala +++ b/thehive-misp/app/connectors/misp/MispExport.scala @@ -5,7 +5,7 @@ import java.util.Date import javax.inject.{ Inject, Provider, Singleton } import scala.concurrent.{ ExecutionContext, Future } -import scala.util.Try +import scala.util.{ Success, Try } import play.api.libs.json.{ JsObject, Json } @@ -133,23 +133,34 @@ class MispExport @Inject() ( (maybeAlertId, maybeEventId) ← relatedMispEvent(mispName, caze.id) attributes ← mispSrv.getAttributesFromCase(caze) uniqueAttributes = removeDuplicateAttributes(attributes) - (eventId, existingAttributes) ← maybeEventId.fold { + (eventId, initialExportesArtifacts, existingAttributes) ← maybeEventId.fold { val simpleAttributes = uniqueAttributes.filter(_.value.isLeft) // if no event is associated to this case, create a new one createEvent(mispConnection, caze.title(), caze.severity(), caze.startDate(), simpleAttributes).map { - case (eventId, exportedAttributes) ⇒ eventId → exportedAttributes.map(_.value.map(_.name)) + case (eventId, exportedAttributes) ⇒ (eventId, exportedAttributes.map(a ⇒ Success(a.artifact)), exportedAttributes.map(_.value.map(_.name))) } } { eventId ⇒ // if an event already exists, retrieve its attributes in order to export only new one mispSrv.getAttributesFromMisp(mispConnection, eventId, None).map { attributes ⇒ - eventId → attributes.map { + (eventId, Nil, attributes.map { case MispArtifact(SimpleArtifactData(data), _, _, _, _, _) ⇒ Left(data) case MispArtifact(RemoteAttachmentArtifact(filename, _, _), _, _, _, _, _) ⇒ Right(filename) case MispArtifact(AttachmentArtifact(Attachment(filename, _, _, _, _)), _, _, _, _, _) ⇒ Right(filename) - } + }) } } newAttributes = uniqueAttributes.filterNot(attr ⇒ existingAttributes.contains(attr.value.map(_.name))) exportedArtifact ← Future.traverse(newAttributes)(attr ⇒ exportAttribute(mispConnection, eventId, attr).toTry) + artifacts = uniqueAttributes.map { a ⇒ + Json.obj( + "data" → a.artifact.data(), + "dataType" → a.artifact.dataType(), + "message" → a.artifact.message(), + "startDate" → a.artifact.startDate(), + "attachment" → a.artifact.attachment(), + "tlp" → a.artifact.tlp(), + "tags" → a.artifact.tags(), + "ioc" → a.artifact.ioc()) + } alert ← maybeAlertId.fold { alertSrv.create(Fields(Json.obj( "type" → "misp", @@ -163,25 +174,14 @@ class MispExport @Inject() ( "severity" → caze.severity(), "tags" → caze.tags(), "tlp" → caze.tlp(), - "artifacts" → uniqueAttributes.map(_.artifact), + "artifacts" → artifacts, "status" → "Imported", - "follow" → false))) + "follow" → true))) } { alertId ⇒ - val artifacts = uniqueAttributes.map { exportedArtifact ⇒ - Json.obj( - "data" → exportedArtifact.artifact.data(), - "dataType" → exportedArtifact.artifact.dataType(), - "message" → exportedArtifact.artifact.message(), - "startDate" → exportedArtifact.artifact.startDate(), - "attachment" → exportedArtifact.artifact.attachment(), - "tlp" → exportedArtifact.artifact.tlp(), - "tags" → exportedArtifact.artifact.tags(), - "ioc" → exportedArtifact.artifact.ioc()) - } alertSrv.update(alertId, Fields(Json.obj( "artifacts" → artifacts, "status" → "Imported"))) } - } yield alert.id → exportedArtifact + } yield alert.id → (initialExportesArtifacts ++ exportedArtifact) } } diff --git a/thehive-misp/app/connectors/misp/MispModel.scala b/thehive-misp/app/connectors/misp/MispModel.scala index 9b83fcc05a..37f6c8dc2a 100644 --- a/thehive-misp/app/connectors/misp/MispModel.scala +++ b/thehive-misp/app/connectors/misp/MispModel.scala @@ -4,6 +4,7 @@ import java.util.Date import models.Artifact +import org.elastic4play.ErrorWithObject import org.elastic4play.services.Attachment import org.elastic4play.utils.Hash @@ -55,5 +56,4 @@ case class MispArtifact( tags: Seq[String], startDate: Date) -case class MispExportError(message: String, artifact: Artifact) extends Exception(message) - +case class MispExportError(message: String, artifact: Artifact) extends ErrorWithObject(message, artifact.attributes) \ No newline at end of file From 0ab9537a108b0d0125fabbf0495eb7ee33e13008 Mon Sep 17 00:00:00 2001 From: To-om Date: Thu, 24 Aug 2017 16:52:49 +0200 Subject: [PATCH 16/49] wip --- .../app/controllers/DBListCtrl.scala | 72 +++++++++++++++++++ thehive-backend/app/services/UserSrv.scala | 16 +++-- 2 files changed, 83 insertions(+), 5 deletions(-) create mode 100644 thehive-backend/app/controllers/DBListCtrl.scala diff --git a/thehive-backend/app/controllers/DBListCtrl.scala b/thehive-backend/app/controllers/DBListCtrl.scala new file mode 100644 index 0000000000..45597537ce --- /dev/null +++ b/thehive-backend/app/controllers/DBListCtrl.scala @@ -0,0 +1,72 @@ +package org.elastic4play.controllers + +import javax.inject.{ Inject, Singleton } + +import scala.concurrent.{ ExecutionContext, Future } + +import play.api.libs.json.{ JsValue, Json } +import play.api.mvc._ + +import org.elastic4play.services.{ DBLists, Role } +import org.elastic4play.{ MissingAttributeError, Timed } + +@Singleton +class DBListCtrl @Inject() ( + dblists: DBLists, + authenticated: Authenticated, + renderer: Renderer, + components: ControllerComponents, + fieldsBodyParser: FieldsBodyParser, + implicit val ec: ExecutionContext) extends AbstractController(components) { + + @Timed("controllers.DBListCtrl.list") + def list: Action[AnyContent] = authenticated(Role.read).async { implicit request ⇒ + dblists.listAll.map { listNames ⇒ + renderer.toOutput(OK, listNames) + } + } + + @Timed("controllers.DBListCtrl.listItems") + def listItems(listName: String): Action[AnyContent] = authenticated(Role.read) { implicit request ⇒ + val (src, _) = dblists(listName).getItems[JsValue] + val items = src.map { case (id, value) ⇒ s""""$id":$value""" } + .intersperse("{", ",", "}") + Ok.chunked(items).as("application/json") + } + + @Timed("controllers.DBListCtrl.addItem") + def addItem(listName: String): Action[Fields] = authenticated(Role.admin).async(fieldsBodyParser) { implicit request ⇒ + request.body.getValue("value").fold(Future.successful(NoContent)) { value ⇒ + dblists(listName).addItem(value).map { item ⇒ + renderer.toOutput(OK, item.id) + } + } + } + + @Timed("controllers.DBListCtrl.deleteItem") + def deleteItem(itemId: String): Action[AnyContent] = authenticated(Role.admin).async { implicit request ⇒ + dblists.deleteItem(itemId).map { _ ⇒ + NoContent + } + } + + @Timed("controllers.DBListCtrl.udpateItem") + def updateItem(itemId: String): Action[Fields] = authenticated(Role.admin).async(fieldsBodyParser) { implicit request ⇒ + request.body.getValue("value") + .map { value ⇒ + for { + item ← dblists.getItem(itemId) + _ ← dblists.deleteItem(item) + newItem ← dblists(item.dblist).addItem(value) + } yield renderer.toOutput(OK, newItem.id) + } + .getOrElse(Future.failed(MissingAttributeError("value"))) + } + + @Timed("controllers.DBListCtrl.itemExists") + def itemExists(listName: String): Action[Fields] = authenticated(Role.read).async(fieldsBodyParser) { implicit request ⇒ + val itemKey = request.body.getString("key").getOrElse(throw MissingAttributeError("Parameter key is missing")) + val itemValue = request.body.getValue("value").getOrElse(throw MissingAttributeError("Parameter value is missing")) + dblists(listName).exists(itemKey, itemValue).map(r ⇒ Ok(Json.obj("found" → r))) + } +} \ No newline at end of file diff --git a/thehive-backend/app/services/UserSrv.scala b/thehive-backend/app/services/UserSrv.scala index 4eb5cee75f..02c2b2f270 100644 --- a/thehive-backend/app/services/UserSrv.scala +++ b/thehive-backend/app/services/UserSrv.scala @@ -1,20 +1,26 @@ package services -import javax.inject.{ Inject, Provider, Singleton } +import javax.inject.{Inject, Provider, Singleton} -import scala.concurrent.{ ExecutionContext, Future } +import scala.concurrent.{ExecutionContext, Future} import play.api.mvc.RequestHeader import akka.NotUsed import akka.stream.scaladsl.Source -import models.{ User, UserModel, UserStatus } +import models.{User, UserModel, UserStatus} import org.elastic4play.controllers.Fields import org.elastic4play.database.DBIndex import org.elastic4play.services._ import org.elastic4play.utils.Instance -import org.elastic4play.{ AuthenticationError, AuthorizationError } +import org.elastic4play.{AuthenticationError, AuthorizationError} + +object Roles { + object read extends Role + object write extends Role + object admin extends Role +} @Singleton class UserSrv @Inject() ( @@ -29,7 +35,7 @@ class UserSrv @Inject() ( dbIndex: DBIndex, implicit val ec: ExecutionContext) extends org.elastic4play.services.UserSrv { - private case class AuthContextImpl(userId: String, userName: String, requestId: String, roles: Seq[Role.Type]) extends AuthContext + private case class AuthContextImpl(userId: String, userName: String, requestId: String, roles: Seq[Role]) extends AuthContext override def getFromId(request: RequestHeader, userId: String): Future[AuthContext] = { getSrv[UserModel, User](userModel, userId) From cfcd7f585455020b4d4e4a399963beaec9db0019 Mon Sep 17 00:00:00 2001 From: To-om Date: Fri, 1 Sep 2017 16:34:19 +0200 Subject: [PATCH 17/49] #263 Add "alert" user role --- .../app/controllers/AlertCtrl.scala | 29 ++++++------ .../app/controllers/ArtifactCtrl.scala | 19 ++++---- .../app/controllers/AttachmentCtrl.scala | 7 +-- .../app/controllers/CaseCtrl.scala | 20 ++++---- .../app/controllers/CaseTemplateCtrl.scala | 13 ++--- .../app/controllers/DBListCtrl.scala | 16 ++++--- .../app/controllers/FlowCtrl.scala | 5 +- thehive-backend/app/controllers/LogCtrl.scala | 15 +++--- .../app/controllers/SearchCtrl.scala | 4 +- .../app/controllers/StreamCtrl.scala | 5 +- .../app/controllers/TaskCtrl.scala | 15 +++--- .../app/controllers/UserCtrl.scala | 21 +++++---- thehive-backend/app/models/JsonFormat.scala | 10 +++- thehive-backend/app/models/Roles.scala | 47 +++++++++++++++++++ thehive-backend/app/models/User.scala | 4 +- thehive-backend/app/services/UserSrv.scala | 18 +++---- .../cortex/controllers/CortexCtrl.scala | 15 +++--- .../controllers/ReportTemplateCtrl.scala | 17 +++---- .../app/connectors/misp/MispCtrl.scala | 10 ++-- .../app/connectors/misp/MispExport.scala | 1 - 20 files changed, 176 insertions(+), 115 deletions(-) create mode 100644 thehive-backend/app/models/Roles.scala diff --git a/thehive-backend/app/controllers/AlertCtrl.scala b/thehive-backend/app/controllers/AlertCtrl.scala index e29e67fc1c..322f90f3dd 100644 --- a/thehive-backend/app/controllers/AlertCtrl.scala +++ b/thehive-backend/app/controllers/AlertCtrl.scala @@ -11,6 +11,7 @@ import play.api.libs.json.{ JsArray, JsObject, Json } import play.api.mvc._ import akka.stream.Materializer +import models.Roles import services.JsonFormat.caseSimilarityWrites import services.{ AlertSrv, CaseSrv } @@ -35,7 +36,7 @@ class AlertCtrl @Inject() ( private[AlertCtrl] lazy val logger = Logger(getClass) @Timed - def create(): Action[Fields] = authenticated(Role.write).async(fieldsBodyParser) { implicit request ⇒ + def create(): Action[Fields] = authenticated(Roles.alert).async(fieldsBodyParser) { implicit request ⇒ alertSrv.create(request.body .unset("lastSyncDate") .unset("case") @@ -45,7 +46,7 @@ class AlertCtrl @Inject() ( } @Timed - def mergeWithCase(alertId: String, caseId: String): Action[Fields] = authenticated(Role.write).async(fieldsBodyParser) { implicit request ⇒ + def mergeWithCase(alertId: String, caseId: String): Action[Fields] = authenticated(Roles.write).async(fieldsBodyParser) { implicit request ⇒ for { alert ← alertSrv.get(alertId) caze ← caseSrv.get(caseId) @@ -54,7 +55,7 @@ class AlertCtrl @Inject() ( } @Timed - def get(id: String): Action[AnyContent] = authenticated(Role.read).async { implicit request ⇒ + def get(id: String): Action[AnyContent] = authenticated(Roles.read).async { implicit request ⇒ val withStats = request .queryString .get("nstats") @@ -80,26 +81,26 @@ class AlertCtrl @Inject() ( } @Timed - def update(id: String): Action[Fields] = authenticated(Role.write).async(fieldsBodyParser) { implicit request ⇒ + def update(id: String): Action[Fields] = authenticated(Roles.write).async(fieldsBodyParser) { implicit request ⇒ alertSrv.update(id, request.body) .map { alert ⇒ renderer.toOutput(OK, alert) } } @Timed - def bulkUpdate(): Action[Fields] = authenticated(Role.write).async(fieldsBodyParser) { implicit request ⇒ + def bulkUpdate(): Action[Fields] = authenticated(Roles.write).async(fieldsBodyParser) { implicit request ⇒ request.body.getStrings("ids").fold(Future.successful(Ok(JsArray()))) { ids ⇒ alertSrv.bulkUpdate(ids, request.body.unset("ids")).map(multiResult ⇒ renderer.toMultiOutput(OK, multiResult)) } } @Timed - def delete(id: String): Action[AnyContent] = authenticated(Role.write).async { implicit request ⇒ + def delete(id: String): Action[AnyContent] = authenticated(Roles.write).async { implicit request ⇒ alertSrv.delete(id) .map(_ ⇒ NoContent) } @Timed - def find(): Action[Fields] = authenticated(Role.read).async(fieldsBodyParser) { implicit request ⇒ + def find(): Action[Fields] = authenticated(Roles.read).async(fieldsBodyParser) { implicit request ⇒ val query = request.body.getValue("query").fold[QueryDef](QueryDSL.any)(_.as[QueryDef]) val range = request.body.getString("range") val sort = request.body.getStrings("sort").getOrElse(Nil) @@ -112,7 +113,7 @@ class AlertCtrl @Inject() ( } @Timed - def stats(): Action[Fields] = authenticated(Role.read).async(fieldsBodyParser) { implicit request ⇒ + def stats(): Action[Fields] = authenticated(Roles.read).async(fieldsBodyParser) { implicit request ⇒ val query = request.body.getValue("query") .fold[QueryDef](QueryDSL.any)(_.as[QueryDef]) val aggs = request.body.getValue("stats") @@ -121,7 +122,7 @@ class AlertCtrl @Inject() ( } @Timed - def markAsRead(id: String): Action[AnyContent] = authenticated(Role.write).async { implicit request ⇒ + def markAsRead(id: String): Action[AnyContent] = authenticated(Roles.write).async { implicit request ⇒ for { alert ← alertSrv.get(id) updatedAlert ← alertSrv.markAsRead(alert) @@ -129,7 +130,7 @@ class AlertCtrl @Inject() ( } @Timed - def markAsUnread(id: String): Action[AnyContent] = authenticated(Role.write).async { implicit request ⇒ + def markAsUnread(id: String): Action[AnyContent] = authenticated(Roles.write).async { implicit request ⇒ for { alert ← alertSrv.get(id) updatedAlert ← alertSrv.markAsUnread(alert) @@ -137,7 +138,7 @@ class AlertCtrl @Inject() ( } @Timed - def createCase(id: String): Action[Fields] = authenticated(Role.write).async(fieldsBodyParser) { implicit request ⇒ + def createCase(id: String): Action[Fields] = authenticated(Roles.write).async(fieldsBodyParser) { implicit request ⇒ for { alert ← alertSrv.get(id) customCaseTemplate = request.body.getString("caseTemplate") @@ -146,19 +147,19 @@ class AlertCtrl @Inject() ( } @Timed - def followAlert(id: String): Action[AnyContent] = authenticated(Role.write).async { implicit request ⇒ + def followAlert(id: String): Action[AnyContent] = authenticated(Roles.write).async { implicit request ⇒ alertSrv.setFollowAlert(id, follow = true) .map { alert ⇒ renderer.toOutput(OK, alert) } } @Timed - def unfollowAlert(id: String): Action[AnyContent] = authenticated(Role.write).async { implicit request ⇒ + def unfollowAlert(id: String): Action[AnyContent] = authenticated(Roles.write).async { implicit request ⇒ alertSrv.setFollowAlert(id, follow = false) .map { alert ⇒ renderer.toOutput(OK, alert) } } @Timed - def fixStatus(): Action[AnyContent] = authenticated(Role.admin).async { implicit request ⇒ + def fixStatus(): Action[AnyContent] = authenticated(Roles.admin).async { implicit request ⇒ alertSrv.fixStatus() .map(_ ⇒ NoContent) } diff --git a/thehive-backend/app/controllers/ArtifactCtrl.scala b/thehive-backend/app/controllers/ArtifactCtrl.scala index 9f8873a0e8..7d6908a84a 100644 --- a/thehive-backend/app/controllers/ArtifactCtrl.scala +++ b/thehive-backend/app/controllers/ArtifactCtrl.scala @@ -8,6 +8,7 @@ import play.api.http.Status import play.api.libs.json.JsArray import play.api.mvc._ +import models.Roles import services.ArtifactSrv import org.elastic4play.controllers.{ Authenticated, Fields, FieldsBodyParser, Renderer } @@ -27,7 +28,7 @@ class ArtifactCtrl @Inject() ( implicit val ec: ExecutionContext) extends AbstractController(components) with Status { @Timed - def create(caseId: String): Action[Fields] = authenticated(Role.write).async(fieldsBodyParser) { implicit request ⇒ + def create(caseId: String): Action[Fields] = authenticated(Roles.write).async(fieldsBodyParser) { implicit request ⇒ val fields = request.body val data = fields.getStrings("data") .getOrElse(fields.getString("data").toSeq) @@ -50,32 +51,32 @@ class ArtifactCtrl @Inject() ( } @Timed - def get(id: String): Action[Fields] = authenticated(Role.read).async(fieldsBodyParser) { implicit request ⇒ + def get(id: String): Action[Fields] = authenticated(Roles.read).async(fieldsBodyParser) { implicit request ⇒ artifactSrv.get(id) .map(artifact ⇒ renderer.toOutput(OK, artifact)) } @Timed - def update(id: String): Action[Fields] = authenticated(Role.write).async(fieldsBodyParser) { implicit request ⇒ + def update(id: String): Action[Fields] = authenticated(Roles.write).async(fieldsBodyParser) { implicit request ⇒ artifactSrv.update(id, request.body) .map(artifact ⇒ renderer.toOutput(OK, artifact)) } @Timed - def bulkUpdate(): Action[Fields] = authenticated(Role.write).async(fieldsBodyParser) { implicit request ⇒ + def bulkUpdate(): Action[Fields] = authenticated(Roles.write).async(fieldsBodyParser) { implicit request ⇒ request.body.getStrings("ids").fold(Future.successful(Ok(JsArray()))) { ids ⇒ artifactSrv.bulkUpdate(ids, request.body.unset("ids")).map(multiResult ⇒ renderer.toMultiOutput(OK, multiResult)) } } @Timed - def delete(id: String): Action[AnyContent] = authenticated(Role.write).async { implicit request ⇒ + def delete(id: String): Action[AnyContent] = authenticated(Roles.write).async { implicit request ⇒ artifactSrv.delete(id) .map(_ ⇒ NoContent) } @Timed - def findInCase(caseId: String): Action[Fields] = authenticated(Role.read).async(fieldsBodyParser) { implicit request ⇒ + def findInCase(caseId: String): Action[Fields] = authenticated(Roles.read).async(fieldsBodyParser) { implicit request ⇒ import org.elastic4play.services.QueryDSL._ val childQuery = request.body.getValue("query").fold[QueryDef](QueryDSL.any)(_.as[QueryDef]) val query = and(childQuery, "_parent" ~= caseId) @@ -87,7 +88,7 @@ class ArtifactCtrl @Inject() ( } @Timed - def find(): Action[Fields] = authenticated(Role.read).async(fieldsBodyParser) { implicit request ⇒ + def find(): Action[Fields] = authenticated(Roles.read).async(fieldsBodyParser) { implicit request ⇒ val query = request.body.getValue("query").fold[QueryDef](QueryDSL.any)(_.as[QueryDef]) val range = request.body.getString("range") val sort = request.body.getStrings("sort").getOrElse(Nil) @@ -100,7 +101,7 @@ class ArtifactCtrl @Inject() ( } @Timed - def findSimilar(artifactId: String): Action[Fields] = authenticated(Role.read).async(fieldsBodyParser) { implicit request ⇒ + def findSimilar(artifactId: String): Action[Fields] = authenticated(Roles.read).async(fieldsBodyParser) { implicit request ⇒ artifactSrv.get(artifactId).flatMap { artifact ⇒ val range = request.body.getString("range") val sort = request.body.getStrings("sort").getOrElse(Nil) @@ -112,7 +113,7 @@ class ArtifactCtrl @Inject() ( } @Timed - def stats(): Action[Fields] = authenticated(Role.read).async(fieldsBodyParser) { implicit request ⇒ + def stats(): Action[Fields] = authenticated(Roles.read).async(fieldsBodyParser) { implicit request ⇒ val query = request.body.getValue("query").fold[QueryDef](QueryDSL.any)(_.as[QueryDef]) val aggs = request.body.getValue("stats").getOrElse(throw BadRequestError("Parameter \"stats\" is missing")).as[Seq[Agg]] artifactSrv.stats(query, aggs).map(s ⇒ Ok(s)) diff --git a/thehive-backend/app/controllers/AttachmentCtrl.scala b/thehive-backend/app/controllers/AttachmentCtrl.scala index 2b9b9faae1..5f21a41696 100644 --- a/thehive-backend/app/controllers/AttachmentCtrl.scala +++ b/thehive-backend/app/controllers/AttachmentCtrl.scala @@ -12,11 +12,12 @@ import akka.stream.scaladsl.FileIO import net.lingala.zip4j.core.ZipFile import net.lingala.zip4j.model.ZipParameters import net.lingala.zip4j.util.Zip4jConstants +import models.Roles import org.elastic4play.Timed import org.elastic4play.controllers.{ Authenticated, Renderer } import org.elastic4play.models.AttachmentAttributeFormat -import org.elastic4play.services.{ AttachmentSrv, Role } +import org.elastic4play.services.AttachmentSrv /** * Controller used to access stored attachments (plain or zipped) @@ -51,7 +52,7 @@ class AttachmentCtrl( * open the document directly. It must be used only for safe file */ @Timed("controllers.AttachmentCtrl.download") - def download(hash: String, name: Option[String]): Action[AnyContent] = authenticated(Role.read) { implicit request ⇒ + def download(hash: String, name: Option[String]): Action[AnyContent] = authenticated(Roles.read) { implicit request ⇒ if (hash.startsWith("{{")) // angularjs hack NoContent else if (!name.getOrElse("").intersect(AttachmentAttributeFormat.forbiddenChar).isEmpty) @@ -72,7 +73,7 @@ class AttachmentCtrl( * File name can be specified (zip extension is append) */ @Timed("controllers.AttachmentCtrl.downloadZip") - def downloadZip(hash: String, name: Option[String]): Action[AnyContent] = authenticated(Role.read) { implicit request ⇒ + def downloadZip(hash: String, name: Option[String]): Action[AnyContent] = authenticated(Roles.read) { implicit request ⇒ if (!name.getOrElse("").intersect(AttachmentAttributeFormat.forbiddenChar).isEmpty) BadRequest("File name is invalid") else { diff --git a/thehive-backend/app/controllers/CaseCtrl.scala b/thehive-backend/app/controllers/CaseCtrl.scala index 791144947f..5ba40b741c 100644 --- a/thehive-backend/app/controllers/CaseCtrl.scala +++ b/thehive-backend/app/controllers/CaseCtrl.scala @@ -12,7 +12,7 @@ import play.api.mvc._ import akka.stream.Materializer import akka.stream.scaladsl.Sink -import models.CaseStatus +import models.{ CaseStatus, Roles } import services.{ CaseMergeSrv, CaseSrv, CaseTemplateSrv, TaskSrv } import org.elastic4play.controllers.{ Authenticated, Fields, FieldsBodyParser, Renderer } @@ -38,7 +38,7 @@ class CaseCtrl @Inject() ( private[CaseCtrl] lazy val logger = Logger(getClass) @Timed - def create(): Action[Fields] = authenticated(Role.write).async(fieldsBodyParser) { implicit request ⇒ + def create(): Action[Fields] = authenticated(Roles.write).async(fieldsBodyParser) { implicit request ⇒ request.body .getString("template") .map { templateName ⇒ @@ -54,7 +54,7 @@ class CaseCtrl @Inject() ( } @Timed - def get(id: String): Action[AnyContent] = authenticated(Role.read).async { implicit request ⇒ + def get(id: String): Action[AnyContent] = authenticated(Roles.read).async { implicit request ⇒ val withStats = for { statsValues ← request.queryString.get("nstats") firstValue ← statsValues.headOption @@ -67,7 +67,7 @@ class CaseCtrl @Inject() ( } @Timed - def update(id: String): Action[Fields] = authenticated(Role.write).async(fieldsBodyParser) { implicit request ⇒ + def update(id: String): Action[Fields] = authenticated(Roles.write).async(fieldsBodyParser) { implicit request ⇒ val isCaseClosing = request.body.getString("status").contains(CaseStatus.Resolved.toString) for { @@ -78,7 +78,7 @@ class CaseCtrl @Inject() ( } @Timed - def bulkUpdate(): Action[Fields] = authenticated(Role.write).async(fieldsBodyParser) { implicit request ⇒ + def bulkUpdate(): Action[Fields] = authenticated(Roles.write).async(fieldsBodyParser) { implicit request ⇒ val isCaseClosing = request.body.getString("status").contains(CaseStatus.Resolved.toString) request.body.getStrings("ids").fold(Future.successful(Ok(JsArray()))) { ids ⇒ @@ -88,13 +88,13 @@ class CaseCtrl @Inject() ( } @Timed - def delete(id: String): Action[AnyContent] = authenticated(Role.write).async { implicit request ⇒ + def delete(id: String): Action[AnyContent] = authenticated(Roles.write).async { implicit request ⇒ caseSrv.delete(id) .map(_ ⇒ NoContent) } @Timed - def find(): Action[Fields] = authenticated(Role.read).async(fieldsBodyParser) { implicit request ⇒ + def find(): Action[Fields] = authenticated(Roles.read).async(fieldsBodyParser) { implicit request ⇒ val query = request.body.getValue("query").fold[QueryDef](QueryDSL.any)(_.as[QueryDef]) val range = request.body.getString("range") val sort = request.body.getStrings("sort").getOrElse(Nil) @@ -107,14 +107,14 @@ class CaseCtrl @Inject() ( } @Timed - def stats(): Action[Fields] = authenticated(Role.read).async(fieldsBodyParser) { implicit request ⇒ + def stats(): Action[Fields] = authenticated(Roles.read).async(fieldsBodyParser) { implicit request ⇒ val query = request.body.getValue("query").fold[QueryDef](QueryDSL.any)(_.as[QueryDef]) val aggs = request.body.getValue("stats").getOrElse(throw BadRequestError("Parameter \"stats\" is missing")).as[Seq[Agg]] caseSrv.stats(query, aggs).map(s ⇒ Ok(s)) } @Timed - def linkedCases(id: String): Action[AnyContent] = authenticated(Role.read).async { implicit request ⇒ + def linkedCases(id: String): Action[AnyContent] = authenticated(Roles.read).async { implicit request ⇒ caseSrv.linkedCases(id) .runWith(Sink.seq) .map { cases ⇒ @@ -131,7 +131,7 @@ class CaseCtrl @Inject() ( } @Timed - def merge(caseId1: String, caseId2: String): Action[AnyContent] = authenticated(Role.read).async { implicit request ⇒ + def merge(caseId1: String, caseId2: String): Action[AnyContent] = authenticated(Roles.read).async { implicit request ⇒ caseMergeSrv.merge(caseId1, caseId2).map { caze ⇒ renderer.toOutput(OK, caze) } diff --git a/thehive-backend/app/controllers/CaseTemplateCtrl.scala b/thehive-backend/app/controllers/CaseTemplateCtrl.scala index 13d8d83b82..662e76d8fc 100644 --- a/thehive-backend/app/controllers/CaseTemplateCtrl.scala +++ b/thehive-backend/app/controllers/CaseTemplateCtrl.scala @@ -7,13 +7,14 @@ import scala.concurrent.ExecutionContext import play.api.http.Status import play.api.mvc._ +import models.Roles import services.CaseTemplateSrv import org.elastic4play.Timed import org.elastic4play.controllers.{ Authenticated, Fields, FieldsBodyParser, Renderer } import org.elastic4play.models.JsonFormat.baseModelEntityWrites import org.elastic4play.services.JsonFormat.queryReads -import org.elastic4play.services.{ AuxSrv, QueryDSL, QueryDef, Role } +import org.elastic4play.services.{ AuxSrv, QueryDSL, QueryDef } @Singleton class CaseTemplateCtrl @Inject() ( @@ -26,31 +27,31 @@ class CaseTemplateCtrl @Inject() ( implicit val ec: ExecutionContext) extends AbstractController(components) with Status { @Timed - def create: Action[Fields] = authenticated(Role.admin).async(fieldsBodyParser) { implicit request ⇒ + def create: Action[Fields] = authenticated(Roles.admin).async(fieldsBodyParser) { implicit request ⇒ caseTemplateSrv.create(request.body) .map(caze ⇒ renderer.toOutput(CREATED, caze)) } @Timed - def get(id: String): Action[AnyContent] = authenticated(Role.read).async { implicit request ⇒ + def get(id: String): Action[AnyContent] = authenticated(Roles.read).async { implicit request ⇒ caseTemplateSrv.get(id) .map(caze ⇒ renderer.toOutput(OK, caze)) } @Timed - def update(id: String): Action[Fields] = authenticated(Role.admin).async(fieldsBodyParser) { implicit request ⇒ + def update(id: String): Action[Fields] = authenticated(Roles.admin).async(fieldsBodyParser) { implicit request ⇒ caseTemplateSrv.update(id, request.body) .map(caze ⇒ renderer.toOutput(OK, caze)) } @Timed - def delete(id: String): Action[AnyContent] = authenticated(Role.admin).async { implicit request ⇒ + def delete(id: String): Action[AnyContent] = authenticated(Roles.admin).async { implicit request ⇒ caseTemplateSrv.delete(id) .map(_ ⇒ NoContent) } @Timed - def find: Action[Fields] = authenticated(Role.read).async(fieldsBodyParser) { implicit request ⇒ + def find: Action[Fields] = authenticated(Roles.read).async(fieldsBodyParser) { implicit request ⇒ val query = request.body.getValue("query").fold[QueryDef](QueryDSL.any)(_.as[QueryDef]) val range = request.body.getString("range") val sort = request.body.getStrings("sort").getOrElse(Nil) diff --git a/thehive-backend/app/controllers/DBListCtrl.scala b/thehive-backend/app/controllers/DBListCtrl.scala index 45597537ce..870ce43f05 100644 --- a/thehive-backend/app/controllers/DBListCtrl.scala +++ b/thehive-backend/app/controllers/DBListCtrl.scala @@ -7,7 +7,9 @@ import scala.concurrent.{ ExecutionContext, Future } import play.api.libs.json.{ JsValue, Json } import play.api.mvc._ -import org.elastic4play.services.{ DBLists, Role } +import models.Roles + +import org.elastic4play.services.DBLists import org.elastic4play.{ MissingAttributeError, Timed } @Singleton @@ -20,14 +22,14 @@ class DBListCtrl @Inject() ( implicit val ec: ExecutionContext) extends AbstractController(components) { @Timed("controllers.DBListCtrl.list") - def list: Action[AnyContent] = authenticated(Role.read).async { implicit request ⇒ + def list: Action[AnyContent] = authenticated(Roles.read).async { implicit request ⇒ dblists.listAll.map { listNames ⇒ renderer.toOutput(OK, listNames) } } @Timed("controllers.DBListCtrl.listItems") - def listItems(listName: String): Action[AnyContent] = authenticated(Role.read) { implicit request ⇒ + def listItems(listName: String): Action[AnyContent] = authenticated(Roles.read) { implicit request ⇒ val (src, _) = dblists(listName).getItems[JsValue] val items = src.map { case (id, value) ⇒ s""""$id":$value""" } .intersperse("{", ",", "}") @@ -35,7 +37,7 @@ class DBListCtrl @Inject() ( } @Timed("controllers.DBListCtrl.addItem") - def addItem(listName: String): Action[Fields] = authenticated(Role.admin).async(fieldsBodyParser) { implicit request ⇒ + def addItem(listName: String): Action[Fields] = authenticated(Roles.admin).async(fieldsBodyParser) { implicit request ⇒ request.body.getValue("value").fold(Future.successful(NoContent)) { value ⇒ dblists(listName).addItem(value).map { item ⇒ renderer.toOutput(OK, item.id) @@ -44,14 +46,14 @@ class DBListCtrl @Inject() ( } @Timed("controllers.DBListCtrl.deleteItem") - def deleteItem(itemId: String): Action[AnyContent] = authenticated(Role.admin).async { implicit request ⇒ + def deleteItem(itemId: String): Action[AnyContent] = authenticated(Roles.admin).async { implicit request ⇒ dblists.deleteItem(itemId).map { _ ⇒ NoContent } } @Timed("controllers.DBListCtrl.udpateItem") - def updateItem(itemId: String): Action[Fields] = authenticated(Role.admin).async(fieldsBodyParser) { implicit request ⇒ + def updateItem(itemId: String): Action[Fields] = authenticated(Roles.admin).async(fieldsBodyParser) { implicit request ⇒ request.body.getValue("value") .map { value ⇒ for { @@ -64,7 +66,7 @@ class DBListCtrl @Inject() ( } @Timed("controllers.DBListCtrl.itemExists") - def itemExists(listName: String): Action[Fields] = authenticated(Role.read).async(fieldsBodyParser) { implicit request ⇒ + def itemExists(listName: String): Action[Fields] = authenticated(Roles.read).async(fieldsBodyParser) { implicit request ⇒ val itemKey = request.body.getString("key").getOrElse(throw MissingAttributeError("Parameter key is missing")) val itemValue = request.body.getValue("value").getOrElse(throw MissingAttributeError("Parameter value is missing")) dblists(listName).exists(itemKey, itemValue).map(r ⇒ Ok(Json.obj("found" → r))) diff --git a/thehive-backend/app/controllers/FlowCtrl.scala b/thehive-backend/app/controllers/FlowCtrl.scala index 92d89ac164..315ce1f452 100644 --- a/thehive-backend/app/controllers/FlowCtrl.scala +++ b/thehive-backend/app/controllers/FlowCtrl.scala @@ -7,11 +7,12 @@ import scala.concurrent.ExecutionContext import play.api.http.Status import play.api.mvc._ +import models.Roles import services.FlowSrv import org.elastic4play.Timed import org.elastic4play.controllers.{ Authenticated, Renderer } -import org.elastic4play.services.{ AuxSrv, Role } +import org.elastic4play.services.AuxSrv @Singleton class FlowCtrl @Inject() ( @@ -26,7 +27,7 @@ class FlowCtrl @Inject() ( * Return audit logs. For each item, include ancestor entities */ @Timed - def flow(rootId: Option[String], count: Option[Int]): Action[AnyContent] = authenticated(Role.read).async { implicit request ⇒ + def flow(rootId: Option[String], count: Option[Int]): Action[AnyContent] = authenticated(Roles.read).async { implicit request ⇒ val (audits, total) = flowSrv(rootId.filterNot(_ == "any"), count.getOrElse(10)) renderer.toOutput(OK, audits, total) } diff --git a/thehive-backend/app/controllers/LogCtrl.scala b/thehive-backend/app/controllers/LogCtrl.scala index 3ddbbe3a7e..038224eaa5 100644 --- a/thehive-backend/app/controllers/LogCtrl.scala +++ b/thehive-backend/app/controllers/LogCtrl.scala @@ -7,13 +7,14 @@ import scala.concurrent.ExecutionContext import play.api.http.Status import play.api.mvc._ +import models.Roles import services.LogSrv import org.elastic4play.Timed import org.elastic4play.controllers.{ Authenticated, Fields, FieldsBodyParser, Renderer } import org.elastic4play.models.JsonFormat.baseModelEntityWrites import org.elastic4play.services.JsonFormat.queryReads -import org.elastic4play.services.{ QueryDSL, QueryDef, Role } +import org.elastic4play.services.{ QueryDSL, QueryDef } @Singleton class LogCtrl @Inject() ( @@ -25,31 +26,31 @@ class LogCtrl @Inject() ( implicit val ec: ExecutionContext) extends AbstractController(components) with Status { @Timed - def create(taskId: String): Action[Fields] = authenticated(Role.read).async(fieldsBodyParser) { implicit request ⇒ + def create(taskId: String): Action[Fields] = authenticated(Roles.read).async(fieldsBodyParser) { implicit request ⇒ logSrv.create(taskId, request.body) .map(log ⇒ renderer.toOutput(CREATED, log)) } @Timed - def get(id: String): Action[AnyContent] = authenticated(Role.read).async { implicit request ⇒ + def get(id: String): Action[AnyContent] = authenticated(Roles.read).async { implicit request ⇒ logSrv.get(id) .map(log ⇒ renderer.toOutput(OK, log)) } @Timed - def update(id: String): Action[Fields] = authenticated(Role.write).async(fieldsBodyParser) { implicit request ⇒ + def update(id: String): Action[Fields] = authenticated(Roles.write).async(fieldsBodyParser) { implicit request ⇒ logSrv.update(id, request.body) .map(log ⇒ renderer.toOutput(OK, log)) } @Timed - def delete(id: String): Action[AnyContent] = authenticated(Role.write).async { implicit request ⇒ + def delete(id: String): Action[AnyContent] = authenticated(Roles.write).async { implicit request ⇒ logSrv.delete(id) .map(_ ⇒ Ok("")) } @Timed - def findInTask(taskId: String): Action[Fields] = authenticated(Role.read).async(fieldsBodyParser) { implicit request ⇒ + def findInTask(taskId: String): Action[Fields] = authenticated(Roles.read).async(fieldsBodyParser) { implicit request ⇒ import org.elastic4play.services.QueryDSL._ val childQuery = request.body.getValue("query").fold[QueryDef](QueryDSL.any)(_.as[QueryDef]) val query = and(childQuery, parent("case_task", withId(taskId))) @@ -61,7 +62,7 @@ class LogCtrl @Inject() ( } @Timed - def find: Action[Fields] = authenticated(Role.read).async(fieldsBodyParser) { implicit request ⇒ + def find: Action[Fields] = authenticated(Roles.read).async(fieldsBodyParser) { implicit request ⇒ val query = request.body.getValue("query").fold[QueryDef](QueryDSL.any)(_.as[QueryDef]) val range = request.body.getString("range") val sort = request.body.getStrings("sort").getOrElse(Nil) diff --git a/thehive-backend/app/controllers/SearchCtrl.scala b/thehive-backend/app/controllers/SearchCtrl.scala index 539fa90400..96322b23e6 100644 --- a/thehive-backend/app/controllers/SearchCtrl.scala +++ b/thehive-backend/app/controllers/SearchCtrl.scala @@ -7,6 +7,8 @@ import scala.concurrent.ExecutionContext import play.api.http.Status import play.api.mvc.{ AbstractController, Action, ControllerComponents } +import models.Roles + import org.elastic4play.Timed import org.elastic4play.controllers.{ Authenticated, Fields, FieldsBodyParser, Renderer } import org.elastic4play.services.JsonFormat.queryReads @@ -23,7 +25,7 @@ class SearchCtrl @Inject() ( implicit val ec: ExecutionContext) extends AbstractController(components) with Status { @Timed - def find(): Action[Fields] = authenticated(Role.read).async(fieldsBodyParser) { implicit request ⇒ + def find(): Action[Fields] = authenticated(Roles.read).async(fieldsBodyParser) { implicit request ⇒ import org.elastic4play.services.QueryDSL._ val query = request.body.getValue("query").fold[QueryDef](QueryDSL.any)(_.as[QueryDef]) val range = request.body.getString("range") diff --git a/thehive-backend/app/controllers/StreamCtrl.scala b/thehive-backend/app/controllers/StreamCtrl.scala index b2a109244c..28a19c7695 100644 --- a/thehive-backend/app/controllers/StreamCtrl.scala +++ b/thehive-backend/app/controllers/StreamCtrl.scala @@ -16,11 +16,12 @@ import play.api.{ Configuration, Logger } import akka.actor.{ ActorSystem, Props } import akka.pattern.ask import akka.util.Timeout +import models.Roles import services.StreamActor import services.StreamActor.StreamMessages import org.elastic4play.controllers._ -import org.elastic4play.services.{ AuxSrv, EventSrv, MigrationSrv, Role } +import org.elastic4play.services.{ AuxSrv, EventSrv, MigrationSrv } import org.elastic4play.Timed @Singleton @@ -67,7 +68,7 @@ class StreamCtrl( * Create a new stream entry with the event head */ @Timed("controllers.StreamCtrl.create") - def create: Action[AnyContent] = authenticated(Role.read) { + def create: Action[AnyContent] = authenticated(Roles.read) { val id = generateStreamId() system.actorOf(Props( classOf[StreamActor], diff --git a/thehive-backend/app/controllers/TaskCtrl.scala b/thehive-backend/app/controllers/TaskCtrl.scala index 6362db045c..ddadd22ffe 100644 --- a/thehive-backend/app/controllers/TaskCtrl.scala +++ b/thehive-backend/app/controllers/TaskCtrl.scala @@ -7,6 +7,7 @@ import scala.concurrent.ExecutionContext import play.api.http.Status import play.api.mvc._ +import models.Roles import services.TaskSrv import org.elastic4play.controllers.{ Authenticated, Fields, FieldsBodyParser, Renderer } @@ -26,31 +27,31 @@ class TaskCtrl @Inject() ( implicit val ec: ExecutionContext) extends AbstractController(components) with Status { @Timed - def create(caseId: String): Action[Fields] = authenticated(Role.write).async(fieldsBodyParser) { implicit request ⇒ + def create(caseId: String): Action[Fields] = authenticated(Roles.write).async(fieldsBodyParser) { implicit request ⇒ taskSrv.create(caseId, request.body) .map(task ⇒ renderer.toOutput(CREATED, task)) } @Timed - def get(id: String): Action[AnyContent] = authenticated(Role.read).async { implicit request ⇒ + def get(id: String): Action[AnyContent] = authenticated(Roles.read).async { implicit request ⇒ taskSrv.get(id) .map(task ⇒ renderer.toOutput(OK, task)) } @Timed - def update(id: String): Action[Fields] = authenticated(Role.write).async(fieldsBodyParser) { implicit request ⇒ + def update(id: String): Action[Fields] = authenticated(Roles.write).async(fieldsBodyParser) { implicit request ⇒ taskSrv.update(id, request.body) .map(task ⇒ renderer.toOutput(OK, task)) } @Timed - def delete(id: String): Action[AnyContent] = authenticated(Role.write).async { implicit request ⇒ + def delete(id: String): Action[AnyContent] = authenticated(Roles.write).async { implicit request ⇒ taskSrv.delete(id) .map(_ ⇒ NoContent) } @Timed - def findInCase(caseId: String): Action[Fields] = authenticated(Role.read).async(fieldsBodyParser) { implicit request ⇒ + def findInCase(caseId: String): Action[Fields] = authenticated(Roles.read).async(fieldsBodyParser) { implicit request ⇒ import org.elastic4play.services.QueryDSL._ val childQuery = request.body.getValue("query").fold[QueryDef](QueryDSL.any)(_.as[QueryDef]) val query = and(childQuery, "_parent" ~= caseId) @@ -62,7 +63,7 @@ class TaskCtrl @Inject() ( } @Timed - def find: Action[Fields] = authenticated(Role.read).async(fieldsBodyParser) { implicit request ⇒ + def find: Action[Fields] = authenticated(Roles.read).async(fieldsBodyParser) { implicit request ⇒ val query = request.body.getValue("query").fold[QueryDef](QueryDSL.any)(_.as[QueryDef]) val range = request.body.getString("range") val sort = request.body.getStrings("sort").getOrElse(Nil) @@ -75,7 +76,7 @@ class TaskCtrl @Inject() ( } @Timed - def stats(): Action[Fields] = authenticated(Role.read).async(fieldsBodyParser) { implicit request ⇒ + def stats(): Action[Fields] = authenticated(Roles.read).async(fieldsBodyParser) { implicit request ⇒ val query = request.body.getValue("query").fold[QueryDef](QueryDSL.any)(_.as[QueryDef]) val aggs = request.body.getValue("stats").getOrElse(throw BadRequestError("Parameter \"stats\" is missing")).as[Seq[Agg]] taskSrv.stats(query, aggs).map(s ⇒ Ok(s)) diff --git a/thehive-backend/app/controllers/UserCtrl.scala b/thehive-backend/app/controllers/UserCtrl.scala index 98e833757d..63d99fc4c8 100644 --- a/thehive-backend/app/controllers/UserCtrl.scala +++ b/thehive-backend/app/controllers/UserCtrl.scala @@ -10,12 +10,13 @@ import play.api.http.Status import play.api.libs.json.{ JsObject, Json } import play.api.mvc._ +import models.Roles import services.UserSrv import org.elastic4play.controllers.{ Authenticated, Fields, FieldsBodyParser, Renderer } import org.elastic4play.models.JsonFormat.baseModelEntityWrites import org.elastic4play.services.JsonFormat.queryReads -import org.elastic4play.services.{ AuthSrv, QueryDSL, QueryDef, Role } +import org.elastic4play.services.{ AuthSrv, QueryDSL, QueryDef } import org.elastic4play.{ AuthorizationError, MissingAttributeError, Timed } @Singleton @@ -31,23 +32,23 @@ class UserCtrl @Inject() ( private[UserCtrl] lazy val logger = Logger(getClass) @Timed - def create: Action[Fields] = authenticated(Role.admin).async(fieldsBodyParser) { implicit request ⇒ + def create: Action[Fields] = authenticated(Roles.admin).async(fieldsBodyParser) { implicit request ⇒ userSrv.create(request.body) .map(user ⇒ renderer.toOutput(CREATED, user)) } @Timed - def get(id: String): Action[AnyContent] = authenticated(Role.read).async { implicit request ⇒ + def get(id: String): Action[AnyContent] = authenticated(Roles.read).async { implicit request ⇒ userSrv.get(id) .map { user ⇒ - val json = if (request.roles.contains(Role.admin)) user.toAdminJson else user.toJson + val json = if (request.roles.contains(Roles.admin)) user.toAdminJson else user.toJson renderer.toOutput(OK, json) } } @Timed - def update(id: String): Action[Fields] = authenticated(Role.read).async(fieldsBodyParser) { implicit request ⇒ - if (id == request.authContext.userId || request.authContext.roles.contains(Role.admin)) { + def update(id: String): Action[Fields] = authenticated(Roles.read).async(fieldsBodyParser) { implicit request ⇒ + if (id == request.authContext.userId || request.authContext.roles.contains(Roles.admin)) { if (request.body.contains("password")) logger.warn("Change password attribute using update operation is deprecated. Please use dedicated API (setPassword and changePassword)") userSrv.update(id, request.body.unset("password")).map { user ⇒ @@ -60,7 +61,7 @@ class UserCtrl @Inject() ( } @Timed - def setPassword(login: String): Action[Fields] = authenticated(Role.admin).async(fieldsBodyParser) { implicit request ⇒ + def setPassword(login: String): Action[Fields] = authenticated(Roles.admin).async(fieldsBodyParser) { implicit request ⇒ request.body.getString("password") .fold(Future.failed[Result](MissingAttributeError("password"))) { password ⇒ authSrv.setPassword(login, password).map(_ ⇒ NoContent) @@ -68,7 +69,7 @@ class UserCtrl @Inject() ( } @Timed - def changePassword(login: String): Action[Fields] = authenticated(Role.read).async(fieldsBodyParser) { implicit request ⇒ + def changePassword(login: String): Action[Fields] = authenticated(Roles.read).async(fieldsBodyParser) { implicit request ⇒ if (login == request.authContext.userId) { val fields = request.body fields.getString("password").fold(Future.failed[Result](MissingAttributeError("password"))) { password ⇒ @@ -83,7 +84,7 @@ class UserCtrl @Inject() ( } @Timed - def delete(id: String): Action[AnyContent] = authenticated(Role.admin).async { implicit request ⇒ + def delete(id: String): Action[AnyContent] = authenticated(Roles.admin).async { implicit request ⇒ userSrv.delete(id) .map(_ ⇒ NoContent) } @@ -105,7 +106,7 @@ class UserCtrl @Inject() ( } @Timed - def find: Action[Fields] = authenticated(Role.read).async(fieldsBodyParser) { implicit request ⇒ + def find: Action[Fields] = authenticated(Roles.read).async(fieldsBodyParser) { implicit request ⇒ val query = request.body.getValue("query").fold[QueryDef](QueryDSL.any)(_.as[QueryDef]) val range = request.body.getString("range") val sort = request.body.getStrings("sort").getOrElse(Nil) diff --git a/thehive-backend/app/models/JsonFormat.scala b/thehive-backend/app/models/JsonFormat.scala index 6c46292e5c..12d753ec51 100644 --- a/thehive-backend/app/models/JsonFormat.scala +++ b/thehive-backend/app/models/JsonFormat.scala @@ -2,9 +2,10 @@ package models import java.nio.file.Path -import play.api.libs.json.{ Format, JsString, Writes } +import play.api.libs.json._ import org.elastic4play.models.JsonFormat.enumFormat +import org.elastic4play.services.Role object JsonFormat { implicit val userStatusFormat: Format[UserStatus.Type] = enumFormat(UserStatus) @@ -18,4 +19,11 @@ object JsonFormat { implicit val alertStatusFormat: Format[AlertStatus.Type] = enumFormat(AlertStatus) implicit val pathWrites: Writes[Path] = Writes((value: Path) ⇒ JsString(value.toString)) + + private val roleWrites: Writes[Role] = Writes((role: Role) ⇒ JsString(role.name)) + private val roleReads: Reads[Role] = Reads { + case JsString(s) if Roles.isValid(s) ⇒ JsSuccess(Roles.withName(s).get) + case _ ⇒ JsError(Seq(JsPath → Seq(JsonValidationError(s"error.expected.role(${Roles.roleNames}")))) + } + implicit val roleFormat: Format[Role] = Format[Role](roleReads, roleWrites) } \ No newline at end of file diff --git a/thehive-backend/app/models/Roles.scala b/thehive-backend/app/models/Roles.scala new file mode 100644 index 0000000000..f613b11f93 --- /dev/null +++ b/thehive-backend/app/models/Roles.scala @@ -0,0 +1,47 @@ +package models + +import play.api.libs.json.{ JsString, JsValue } + +import com.sksamuel.elastic4s.ElasticDsl.keywordField +import com.sksamuel.elastic4s.mappings.KeywordFieldDefinition +import org.scalactic.{ Every, Good, One, Or } +import models.JsonFormat.roleFormat + +import org.elastic4play.{ AttributeError, InvalidFormatAttributeError } +import org.elastic4play.controllers.{ InputValue, JsonInputValue, StringInputValue } +import org.elastic4play.models.AttributeFormat +import org.elastic4play.services.Role + +object Roles { + object read extends Role("read") + object write extends Role("write") + object admin extends Role("admin") + object alert extends Role("alert") + val roles = read :: write :: admin :: alert :: Nil + + val roleNames = roles.map(_.name) + def isValid(roleName: String) = roleNames.contains(roleName) + def withName(roleName: String) = roles.find(_.name == roleName) +} + +object RoleAttributeFormat extends AttributeFormat[Role]("role") { + + override def checkJson(subNames: Seq[String], value: JsValue): Or[JsValue, One[InvalidFormatAttributeError]] = value match { + case JsString(v) if subNames.isEmpty && Roles.isValid(v) ⇒ Good(value) + case _ ⇒ formatError(JsonInputValue(value)) + } + + override def fromInputValue(subNames: Seq[String], value: InputValue): Role Or Every[AttributeError] = { + if (subNames.nonEmpty) + formatError(value) + else + (value match { + case StringInputValue(Seq(v)) ⇒ Good(v) + case JsonInputValue(JsString(v)) ⇒ Good(v) + case _ ⇒ formatError(value) + }).flatMap(v ⇒ Roles.withName(v).fold[Role Or Every[AttributeError]](formatError(value))(role ⇒ Good(role))) + + } + + override def elasticType(attributeName: String): KeywordFieldDefinition = keywordField(attributeName) +} \ No newline at end of file diff --git a/thehive-backend/app/models/User.scala b/thehive-backend/app/models/User.scala index 9df4658c4d..635b35c19f 100644 --- a/thehive-backend/app/models/User.scala +++ b/thehive-backend/app/models/User.scala @@ -11,8 +11,6 @@ import models.JsonFormat.userStatusFormat import services.AuditedModel import org.elastic4play.models.{ AttributeDef, BaseEntity, EntityDef, HiveEnumeration, ModelDef, AttributeFormat ⇒ F, AttributeOption ⇒ O } -import org.elastic4play.services.JsonFormat.roleFormat -import org.elastic4play.services.Role object UserStatus extends Enumeration with HiveEnumeration { type Type = Value @@ -25,7 +23,7 @@ trait UserAttributes { _: AttributeDef ⇒ val withKey = optionalAttribute("with-key", F.booleanFmt, "Generate an API key", O.form) val key = optionalAttribute("key", F.uuidFmt, "API key", O.model, O.sensitive, O.unaudited) val userName = attribute("name", F.stringFmt, "Full name (Firstname Lastname)") - val roles = multiAttribute("roles", F.enumFmt(Role), "Comma separated role list (READ, WRITE and ADMIN)") + val roles = multiAttribute("roles", RoleAttributeFormat, "Comma separated role list (READ, WRITE and ADMIN)") val status = attribute("status", F.enumFmt(UserStatus), "Status of the user", UserStatus.Ok) val password = optionalAttribute("password", F.stringFmt, "Password", O.sensitive, O.unaudited) val avatar = optionalAttribute("avatar", F.stringFmt, "Base64 representation of user avatar image", O.unaudited) diff --git a/thehive-backend/app/services/UserSrv.scala b/thehive-backend/app/services/UserSrv.scala index 02c2b2f270..cad528765f 100644 --- a/thehive-backend/app/services/UserSrv.scala +++ b/thehive-backend/app/services/UserSrv.scala @@ -1,26 +1,20 @@ package services -import javax.inject.{Inject, Provider, Singleton} +import javax.inject.{ Inject, Provider, Singleton } -import scala.concurrent.{ExecutionContext, Future} +import scala.concurrent.{ ExecutionContext, Future } import play.api.mvc.RequestHeader import akka.NotUsed import akka.stream.scaladsl.Source -import models.{User, UserModel, UserStatus} +import models.{ Roles, User, UserModel, UserStatus } import org.elastic4play.controllers.Fields import org.elastic4play.database.DBIndex import org.elastic4play.services._ import org.elastic4play.utils.Instance -import org.elastic4play.{AuthenticationError, AuthorizationError} - -object Roles { - object read extends Role - object write extends Role - object admin extends Role -} +import org.elastic4play.{ AuthenticationError, AuthorizationError } @Singleton class UserSrv @Inject() ( @@ -53,11 +47,11 @@ class UserSrv @Inject() ( override def getInitialUser(request: RequestHeader): Future[AuthContext] = dbIndex.getSize(userModel.name).map { case size if size > 0 ⇒ throw AuthenticationError(s"Use of initial user is forbidden because users exist in database") - case _ ⇒ AuthContextImpl("init", "", Instance.getRequestId(request), Seq(Role.admin, Role.read)) + case _ ⇒ AuthContextImpl("init", "", Instance.getRequestId(request), Seq(Roles.admin, Roles.read, Roles.alert)) } override def inInitAuthContext[A](block: AuthContext ⇒ Future[A]): Future[A] = { - val authContext = AuthContextImpl("init", "", Instance.getInternalId, Seq(Role.admin, Role.read)) + val authContext = AuthContextImpl("init", "", Instance.getInternalId, Seq(Roles.admin, Roles.read, Roles.alert)) eventSrv.publish(StreamActor.Initialize(authContext.requestId)) block(authContext).andThen { case _ ⇒ eventSrv.publish(StreamActor.Commit(authContext.requestId)) diff --git a/thehive-cortex/app/connectors/cortex/controllers/CortexCtrl.scala b/thehive-cortex/app/connectors/cortex/controllers/CortexCtrl.scala index 96e8037309..a11bb6e7a7 100644 --- a/thehive-cortex/app/connectors/cortex/controllers/CortexCtrl.scala +++ b/thehive-cortex/app/connectors/cortex/controllers/CortexCtrl.scala @@ -14,11 +14,12 @@ import play.api.routing.sird.{ DELETE, GET, PATCH, POST, UrlContext } import org.elastic4play.{ BadRequestError, NotFoundError, Timed } import org.elastic4play.controllers.{ Authenticated, Fields, FieldsBodyParser, Renderer } import org.elastic4play.models.JsonFormat.baseModelEntityWrites -import org.elastic4play.services.{ AuxSrv, QueryDSL, QueryDef, Role } +import org.elastic4play.services.{ AuxSrv, QueryDSL, QueryDef } import org.elastic4play.services.JsonFormat.queryReads import connectors.Connector import connectors.cortex.models.JsonFormat.analyzerFormats import connectors.cortex.services.{ CortexConfig, CortexSrv } +import models.Roles @Singleton class CortexCtrl @Inject() ( @@ -55,7 +56,7 @@ class CortexCtrl @Inject() ( } @Timed - def createJob: Action[Fields] = authenticated(Role.write).async(fieldsBodyParser) { implicit request ⇒ + def createJob: Action[Fields] = authenticated(Roles.write).async(fieldsBodyParser) { implicit request ⇒ val analyzerId = request.body.getString("analyzerId").getOrElse(throw BadRequestError(s"analyzerId is missing")) val artifactId = request.body.getString("artifactId").getOrElse(throw BadRequestError(s"artifactId is missing")) val cortexId = request.body.getString("cortexId") @@ -65,14 +66,14 @@ class CortexCtrl @Inject() ( } @Timed - def getJob(jobId: String): Action[AnyContent] = authenticated(Role.read).async { implicit request ⇒ + def getJob(jobId: String): Action[AnyContent] = authenticated(Roles.read).async { implicit request ⇒ cortexSrv.getJob(jobId).map { job ⇒ renderer.toOutput(OK, job) } } @Timed - def findJob: Action[Fields] = authenticated(Role.read).async(fieldsBodyParser) { implicit request ⇒ + def findJob: Action[Fields] = authenticated(Roles.read).async(fieldsBodyParser) { implicit request ⇒ val query = request.body.getValue("query").fold[QueryDef](QueryDSL.any)(_.as[QueryDef]) val range = request.body.getString("range") val sort = request.body.getStrings("sort").getOrElse(Nil) @@ -83,21 +84,21 @@ class CortexCtrl @Inject() ( } @Timed - def getAnalyzer(analyzerId: String): Action[AnyContent] = authenticated(Role.read).async { implicit request ⇒ + def getAnalyzer(analyzerId: String): Action[AnyContent] = authenticated(Roles.read).async { implicit request ⇒ cortexSrv.getAnalyzer(analyzerId).map { analyzer ⇒ renderer.toOutput(OK, analyzer) } } @Timed - def getAnalyzerFor(dataType: String): Action[AnyContent] = authenticated(Role.read).async { implicit request ⇒ + def getAnalyzerFor(dataType: String): Action[AnyContent] = authenticated(Roles.read).async { implicit request ⇒ cortexSrv.getAnalyzersFor(dataType).map { analyzers ⇒ renderer.toOutput(OK, analyzers) } } @Timed - def listAnalyzer: Action[AnyContent] = authenticated(Role.read).async { implicit request ⇒ + def listAnalyzer: Action[AnyContent] = authenticated(Roles.read).async { implicit request ⇒ cortexSrv.listAnalyzer.map { analyzers ⇒ renderer.toOutput(OK, analyzers) } diff --git a/thehive-cortex/app/connectors/cortex/controllers/ReportTemplateCtrl.scala b/thehive-cortex/app/connectors/cortex/controllers/ReportTemplateCtrl.scala index 90797523fc..c669933998 100644 --- a/thehive-cortex/app/connectors/cortex/controllers/ReportTemplateCtrl.scala +++ b/thehive-cortex/app/connectors/cortex/controllers/ReportTemplateCtrl.scala @@ -17,10 +17,11 @@ import play.api.mvc._ import org.elastic4play.{ BadRequestError, Timed } import org.elastic4play.controllers._ import org.elastic4play.models.JsonFormat.baseModelEntityWrites -import org.elastic4play.services.{ QueryDSL, QueryDef, Role } +import org.elastic4play.services.{ QueryDSL, QueryDef } import org.elastic4play.services.AuxSrv import org.elastic4play.services.JsonFormat.queryReads import connectors.cortex.services.ReportTemplateSrv +import models.Roles import net.lingala.zip4j.core.ZipFile import net.lingala.zip4j.model.FileHeader @@ -38,19 +39,19 @@ class ReportTemplateCtrl @Inject() ( private[ReportTemplateCtrl] lazy val logger = Logger(getClass) @Timed - def create: Action[Fields] = authenticated(Role.admin).async(fieldsBodyParser) { implicit request ⇒ + def create: Action[Fields] = authenticated(Roles.admin).async(fieldsBodyParser) { implicit request ⇒ reportTemplateSrv.create(request.body) .map(reportTemplate ⇒ renderer.toOutput(CREATED, reportTemplate)) } @Timed - def get(id: String): Action[AnyContent] = authenticated(Role.read).async { implicit request ⇒ + def get(id: String): Action[AnyContent] = authenticated(Roles.read).async { implicit request ⇒ reportTemplateSrv.get(id) .map(reportTemplate ⇒ renderer.toOutput(OK, reportTemplate)) } @Timed - def getContent(analyzerId: String, reportType: String): Action[AnyContent] = authenticated(Role.read).async { implicit request ⇒ + def getContent(analyzerId: String, reportType: String): Action[AnyContent] = authenticated(Roles.read).async { implicit request ⇒ import org.elastic4play.services.QueryDSL._ val (reportTemplates, total) = reportTemplateSrv.find(and("analyzerId" ~= analyzerId, "reportType" ~= reportType), Some("0-1"), Nil) total.foreach { t ⇒ @@ -65,19 +66,19 @@ class ReportTemplateCtrl @Inject() ( } @Timed - def update(id: String): Action[Fields] = authenticated(Role.admin).async(fieldsBodyParser) { implicit request ⇒ + def update(id: String): Action[Fields] = authenticated(Roles.admin).async(fieldsBodyParser) { implicit request ⇒ reportTemplateSrv.update(id, request.body) .map(reportTemplate ⇒ renderer.toOutput(OK, reportTemplate)) } @Timed - def delete(id: String): Action[AnyContent] = authenticated(Role.admin).async { implicit request ⇒ + def delete(id: String): Action[AnyContent] = authenticated(Roles.admin).async { implicit request ⇒ reportTemplateSrv.delete(id) .map(_ ⇒ NoContent) } @Timed - def find: Action[Fields] = authenticated(Role.read).async(fieldsBodyParser) { implicit request ⇒ + def find: Action[Fields] = authenticated(Roles.read).async(fieldsBodyParser) { implicit request ⇒ val query = request.body.getValue("query").fold[QueryDef](QueryDSL.any)(_.as[QueryDef]) val range = request.body.getString("range") val sort = request.body.getStrings("sort").getOrElse(Nil) @@ -90,7 +91,7 @@ class ReportTemplateCtrl @Inject() ( } @Timed - def importTemplatePackage: Action[Fields] = authenticated(Role.write).async(fieldsBodyParser) { implicit request ⇒ + def importTemplatePackage: Action[Fields] = authenticated(Roles.write).async(fieldsBodyParser) { implicit request ⇒ val zipFile = request.body.get("templates") match { case Some(FileInputValue(_, filepath, _)) ⇒ new ZipFile(filepath.toFile) case _ ⇒ throw BadRequestError("") diff --git a/thehive-misp/app/connectors/misp/MispCtrl.scala b/thehive-misp/app/connectors/misp/MispCtrl.scala index e1d624ce3f..060749e0d5 100644 --- a/thehive-misp/app/connectors/misp/MispCtrl.scala +++ b/thehive-misp/app/connectors/misp/MispCtrl.scala @@ -12,7 +12,7 @@ import play.api.routing.SimpleRouter import play.api.routing.sird.{ GET, UrlContext } import connectors.Connector -import models.{ Alert, Case, UpdateMispAlertArtifact } +import models.{ Alert, Case, Roles, UpdateMispAlertArtifact } import services.{ AlertTransformer, CaseSrv } import org.elastic4play.JsonFormat.tryWrites @@ -48,25 +48,25 @@ class MispCtrl @Inject() ( } @Timed - def syncAlerts: Action[AnyContent] = authenticated(Role.admin).async { implicit request ⇒ + def syncAlerts: Action[AnyContent] = authenticated(Roles.admin).async { implicit request ⇒ mispSynchro.synchronize() .map { m ⇒ Ok(Json.toJson(m)) } } @Timed - def syncAllAlerts: Action[AnyContent] = authenticated(Role.admin).async { implicit request ⇒ + def syncAllAlerts: Action[AnyContent] = authenticated(Roles.admin).async { implicit request ⇒ mispSynchro.fullSynchronize() .map { m ⇒ Ok(Json.toJson(m)) } } @Timed - def syncArtifacts: Action[AnyContent] = authenticated(Role.admin) { + def syncArtifacts: Action[AnyContent] = authenticated(Roles.admin) { eventSrv.publish(UpdateMispAlertArtifact()) Ok("") } @Timed - def exportCase(mispName: String, caseId: String): Action[AnyContent] = authenticated(Role.write).async { implicit request ⇒ + def exportCase(mispName: String, caseId: String): Action[AnyContent] = authenticated(Roles.write).async { implicit request ⇒ caseSrv .get(caseId) .flatMap { caze ⇒ mispExport.export(mispName, caze) } diff --git a/thehive-misp/app/connectors/misp/MispExport.scala b/thehive-misp/app/connectors/misp/MispExport.scala index 44bcc7508f..d6d4060cfe 100644 --- a/thehive-misp/app/connectors/misp/MispExport.scala +++ b/thehive-misp/app/connectors/misp/MispExport.scala @@ -17,7 +17,6 @@ import akka.stream.Materializer import org.elastic4play.InternalError import org.elastic4play.controllers.Fields -import org.elastic4play.models.JsonFormat.baseModelEntityWrites import org.elastic4play.services.{ Attachment, AttachmentSrv, AuthContext } import org.elastic4play.services.JsonFormat.attachmentFormat import org.elastic4play.utils.RichFuture From 213ea6c71127e7356711cbc945bc88f2a5ae7f3e Mon Sep 17 00:00:00 2001 From: To-om Date: Fri, 1 Sep 2017 16:34:48 +0200 Subject: [PATCH 18/49] #263 Add API key authentication service --- thehive-backend/app/services/ApiKeyAuth.scala | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 thehive-backend/app/services/ApiKeyAuth.scala diff --git a/thehive-backend/app/services/ApiKeyAuth.scala b/thehive-backend/app/services/ApiKeyAuth.scala new file mode 100644 index 0000000000..701a6bedd1 --- /dev/null +++ b/thehive-backend/app/services/ApiKeyAuth.scala @@ -0,0 +1,45 @@ +package services + +import javax.inject.{Inject, Singleton} + +import scala.concurrent.{ExecutionContext, Future} + +import play.api.mvc.RequestHeader + +import akka.stream.scaladsl.Sink +import models.UserModel + +import org.elastic4play.{AuthenticationError, AuthorizationError} +import org.elastic4play.controllers.Fields +import org.elastic4play.services._ + +@Singleton +class ApiKeyAuth @Inject() ( + userModel: UserModel, + userSrv: UserSrv, + updateSrv: UpdateSrv, + implicit val ec: ExecutionContext) extends AuthSrv { + val name = "apikey" + + def capabilities = Set(AuthCapability.setPassword) + + def authenticate(username: String, password: String)(implicit request: RequestHeader): Future[AuthContext] = + Future.failed(AuthorizationError("apikey authenticator doesn't support login/password authentication"))) + + def authenticate(key: String)(implicit request: RequestHeader): Future[AuthContext] = { + import org.elastic4play.services.QueryDSL._ + userSrv.find(and("status" ~!= "Ok", "key" ~!= key), Some("0-1"), Nil) + ._1 + .runWith(Sink.headOption) + .flatMap { + case Some(user) => userSrv.getFromUser(request, user) + case None => Future.failed(AuthenticationError("Authentication failure")) + } + + } + def changePassword(username: String, oldPassword: String, newPassword: String)(implicit authContext: AuthContext): Future[Unit] = + Future.failed(AuthorizationError("Operation not supported")) + + def setPassword(username: String, newPassword: String)(implicit authContext: AuthContext): Future[Unit] = + userSrv.update(username, Fields.empty.set("key", newPassword)).map(_ => ()) +} From 555320085fd8ef2961d8df8829d73f9e9d86700b Mon Sep 17 00:00:00 2001 From: To-om Date: Mon, 4 Sep 2017 17:56:24 +0200 Subject: [PATCH 19/49] #263 Add API key authentication --- .../app/controllers/UserCtrl.scala | 15 ++++-- thehive-backend/app/models/User.scala | 22 ++------- thehive-backend/app/services/ApiKeyAuth.scala | 45 ------------------ .../app/services/LocalAuthSrv.scala | 46 +++++++++++++------ thehive-backend/app/services/UserSrv.scala | 4 ++ thehive-backend/conf/routes | 3 ++ 6 files changed, 55 insertions(+), 80 deletions(-) delete mode 100644 thehive-backend/app/services/ApiKeyAuth.scala diff --git a/thehive-backend/app/controllers/UserCtrl.scala b/thehive-backend/app/controllers/UserCtrl.scala index 63d99fc4c8..b7d268c69f 100644 --- a/thehive-backend/app/controllers/UserCtrl.scala +++ b/thehive-backend/app/controllers/UserCtrl.scala @@ -40,10 +40,7 @@ class UserCtrl @Inject() ( @Timed def get(id: String): Action[AnyContent] = authenticated(Roles.read).async { implicit request ⇒ userSrv.get(id) - .map { user ⇒ - val json = if (request.roles.contains(Roles.admin)) user.toAdminJson else user.toJson - renderer.toOutput(OK, json) - } + .map { user ⇒ renderer.toOutput(OK, user) } } @Timed @@ -113,4 +110,14 @@ class UserCtrl @Inject() ( val (users, total) = userSrv.find(query, range, sort) renderer.toOutput(OK, users, total) } + + @Timed + def getKey(id: String): Action[AnyContent] = authenticated(Roles.admin).async { implicit request => + authSrv.getKey(id).map(Ok(_)) + } + + @Timed + def renewKey(id: String): Action[AnyContent] = authenticated(Roles.admin).async { implicit request => + authSrv.renewKey(id).map(Ok(_)) + } } \ No newline at end of file diff --git a/thehive-backend/app/models/User.scala b/thehive-backend/app/models/User.scala index 635b35c19f..b536b5d43a 100644 --- a/thehive-backend/app/models/User.scala +++ b/thehive-backend/app/models/User.scala @@ -1,11 +1,9 @@ package models -import java.util.UUID - import scala.concurrent.Future import play.api.libs.json.JsValue.jsValueToJsLookup -import play.api.libs.json.{ JsBoolean, JsObject, JsString, JsUndefined } +import play.api.libs.json.{ JsObject, JsString } import models.JsonFormat.userStatusFormat import services.AuditedModel @@ -21,7 +19,7 @@ trait UserAttributes { _: AttributeDef ⇒ val login = attribute("login", F.stringFmt, "Login of the user", O.form) val userId = attribute("_id", F.stringFmt, "User id (login)", O.model) val withKey = optionalAttribute("with-key", F.booleanFmt, "Generate an API key", O.form) - val key = optionalAttribute("key", F.uuidFmt, "API key", O.model, O.sensitive, O.unaudited) + val key = optionalAttribute("key", F.stringFmt, "API key", O.model, O.sensitive, O.unaudited) val userName = attribute("name", F.stringFmt, "Full name (Firstname Lastname)") val roles = multiAttribute("roles", RoleAttributeFormat, "Comma separated role list (READ, WRITE and ADMIN)") val status = attribute("status", F.enumFmt(UserStatus), "Status of the user", UserStatus.Ok) @@ -32,26 +30,14 @@ trait UserAttributes { _: AttributeDef ⇒ class UserModel extends ModelDef[UserModel, User]("user") with UserAttributes with AuditedModel { - private def addKey = (attrs: JsObject) ⇒ attrs \ "with-key" match { - case _: JsUndefined ⇒ attrs - case _ ⇒ attrs + ("key" → JsString(UUID.randomUUID.toString)) - "with-key" - } - - private def setUserId = (attrs: JsObject) ⇒ (attrs \ "login").asOpt[JsString].fold(attrs) { login ⇒ + private def setUserId(attrs: JsObject) = (attrs \ "login").asOpt[JsString].fold(attrs) { login ⇒ attrs - "login" + ("_id" → login) } - override def creationHook(parent: Option[BaseEntity], attrs: JsObject) = Future.successful(addKey.andThen(setUserId)(attrs)) - - override def updateHook(user: BaseEntity, updateAttrs: JsObject): Future[JsObject] = Future.successful(addKey(updateAttrs)) + override def creationHook(parent: Option[BaseEntity], attrs: JsObject): Future[JsObject] = Future.successful(setUserId(attrs)) } class User(model: UserModel, attributes: JsObject) extends EntityDef[UserModel, User](model, attributes) with UserAttributes with org.elastic4play.services.User { - override def toJson = super.toJson + - ("has-key" → JsBoolean(key().isDefined)) - - def toAdminJson = key().fold(toJson) { k ⇒ toJson + ("key" → JsString(k.toString)) } - override def getUserName = userName() override def getRoles = roles() } \ No newline at end of file diff --git a/thehive-backend/app/services/ApiKeyAuth.scala b/thehive-backend/app/services/ApiKeyAuth.scala deleted file mode 100644 index 701a6bedd1..0000000000 --- a/thehive-backend/app/services/ApiKeyAuth.scala +++ /dev/null @@ -1,45 +0,0 @@ -package services - -import javax.inject.{Inject, Singleton} - -import scala.concurrent.{ExecutionContext, Future} - -import play.api.mvc.RequestHeader - -import akka.stream.scaladsl.Sink -import models.UserModel - -import org.elastic4play.{AuthenticationError, AuthorizationError} -import org.elastic4play.controllers.Fields -import org.elastic4play.services._ - -@Singleton -class ApiKeyAuth @Inject() ( - userModel: UserModel, - userSrv: UserSrv, - updateSrv: UpdateSrv, - implicit val ec: ExecutionContext) extends AuthSrv { - val name = "apikey" - - def capabilities = Set(AuthCapability.setPassword) - - def authenticate(username: String, password: String)(implicit request: RequestHeader): Future[AuthContext] = - Future.failed(AuthorizationError("apikey authenticator doesn't support login/password authentication"))) - - def authenticate(key: String)(implicit request: RequestHeader): Future[AuthContext] = { - import org.elastic4play.services.QueryDSL._ - userSrv.find(and("status" ~!= "Ok", "key" ~!= key), Some("0-1"), Nil) - ._1 - .runWith(Sink.headOption) - .flatMap { - case Some(user) => userSrv.getFromUser(request, user) - case None => Future.failed(AuthenticationError("Authentication failure")) - } - - } - def changePassword(username: String, oldPassword: String, newPassword: String)(implicit authContext: AuthContext): Future[Unit] = - Future.failed(AuthorizationError("Operation not supported")) - - def setPassword(username: String, newPassword: String)(implicit authContext: AuthContext): Future[Unit] = - userSrv.update(username, Fields.empty.set("key", newPassword)).map(_ => ()) -} diff --git a/thehive-backend/app/services/LocalAuthSrv.scala b/thehive-backend/app/services/LocalAuthSrv.scala index 5ff83fe9ee..734d2e47b9 100644 --- a/thehive-backend/app/services/LocalAuthSrv.scala +++ b/thehive-backend/app/services/LocalAuthSrv.scala @@ -5,25 +5,25 @@ import javax.inject.{ Inject, Singleton } import scala.concurrent.{ ExecutionContext, Future } import scala.util.Random -import play.api.libs.json.{ JsObject, JsString } import play.api.mvc.RequestHeader -import models.{ User, UserModel } +import akka.stream.Materializer +import akka.stream.scaladsl.Sink +import models.User import org.elastic4play.controllers.Fields -import org.elastic4play.services.{ AuthCapability, AuthContext, AuthSrv, UpdateSrv } +import org.elastic4play.services.{ AuthCapability, AuthContext, AuthSrv } import org.elastic4play.utils.Hasher -import org.elastic4play.{ AuthenticationError, AuthorizationError } +import org.elastic4play.{ AuthenticationError, AuthorizationError, BadRequestError } @Singleton class LocalAuthSrv @Inject() ( - userModel: UserModel, userSrv: UserSrv, - updateSrv: UpdateSrv, - implicit val ec: ExecutionContext) extends AuthSrv { + implicit val ec: ExecutionContext, + implicit val mat: Materializer) extends AuthSrv { val name = "local" - def capabilities = Set(AuthCapability.changePassword, AuthCapability.setPassword) + override val capabilities = Set(AuthCapability.changePassword, AuthCapability.setPassword, AuthCapability.renewKey) private[services] def doAuthenticate(user: User, password: String): Boolean = { user.password().map(_.split(",", 2)).fold(false) { @@ -34,24 +34,44 @@ class LocalAuthSrv @Inject() ( } } - def authenticate(username: String, password: String)(implicit request: RequestHeader): Future[AuthContext] = { + override def authenticate(username: String, password: String)(implicit request: RequestHeader): Future[AuthContext] = { userSrv.get(username).flatMap { user ⇒ if (doAuthenticate(user, password)) userSrv.getFromUser(request, user) else Future.failed(AuthenticationError("Authentication failure")) } } - def changePassword(username: String, oldPassword: String, newPassword: String)(implicit authContext: AuthContext): Future[Unit] = { + override def authenticate(key: String)(implicit request: RequestHeader): Future[AuthContext] = { + import org.elastic4play.services.QueryDSL._ + userSrv.find(and("status" ~= "Ok", "key" ~= key), Some("0-1"), Nil) + ._1 + .runWith(Sink.headOption) + .flatMap { + case Some(user) ⇒ userSrv.getFromUser(request, user) + case None ⇒ Future.failed(AuthenticationError("Authentication failure")) + } + } + + override def changePassword(username: String, oldPassword: String, newPassword: String)(implicit authContext: AuthContext): Future[Unit] = { userSrv.get(username).flatMap { user ⇒ if (doAuthenticate(user, oldPassword)) setPassword(username, newPassword) else Future.failed(AuthorizationError("Authentication failure")) } } - def setPassword(username: String, newPassword: String)(implicit authContext: AuthContext): Future[Unit] = { + override def setPassword(username: String, newPassword: String)(implicit authContext: AuthContext): Future[Unit] = { val seed = Random.nextString(10).replace(',', '!') val newHash = seed + "," + Hasher("SHA-256").fromString(seed + newPassword).head.toString - userSrv.update(username, Fields(JsObject(Seq("password" → JsString(newHash))))) - .map(_ ⇒ ()) + userSrv.update(username, Fields.empty.set("password", newHash)).map(_ ⇒ ()) + } + + override def renewKey(username: String)(implicit authContext: AuthContext): Future[String] = { + val newKey = generateKey() + userSrv.update(username, Fields.empty.set("key", newKey)).map(_ ⇒ newKey) } + + override def getKey(username: String)(implicit authContext: AuthContext): Future[String] = { + userSrv.get(username).map(_.key().getOrElse(throw BadRequestError(s"User $username hasn't key"))) + } + } \ No newline at end of file diff --git a/thehive-backend/app/services/UserSrv.scala b/thehive-backend/app/services/UserSrv.scala index cad528765f..d9da66539c 100644 --- a/thehive-backend/app/services/UserSrv.scala +++ b/thehive-backend/app/services/UserSrv.scala @@ -73,6 +73,10 @@ class UserSrv @Inject() ( updateSrv[UserModel, User](userModel, id, fields) } + def update(user: User, fields: Fields)(implicit Context: AuthContext): Future[User] = { + updateSrv(user, fields) + } + def delete(id: String)(implicit Context: AuthContext): Future[User] = deleteSrv[UserModel, User](userModel, id) diff --git a/thehive-backend/conf/routes b/thehive-backend/conf/routes index 884d0dc7da..fcaba89198 100644 --- a/thehive-backend/conf/routes +++ b/thehive-backend/conf/routes @@ -91,6 +91,9 @@ DELETE /api/user/:userId controllers.UserCtrl.delete(us PATCH /api/user/:userId controllers.UserCtrl.update(userId) POST /api/user/:userId/password/set controllers.UserCtrl.setPassword(userId) POST /api/user/:userId/password/change controllers.UserCtrl.changePassword(userId) +GET /api/user/:userId/key controllers.UserCtrl.getKey(userId) +POST /api/user/:userId/key/renew controllers.UserCtrl.renewKey(userId) + POST /api/stream controllers.StreamCtrl.create() GET /api/stream/status controllers.StreamCtrl.status From 106842fc7877dfa3f1a0a2ae5b55ad21e7754d6a Mon Sep 17 00:00:00 2001 From: To-om Date: Tue, 5 Sep 2017 09:28:53 +0200 Subject: [PATCH 20/49] #263 fix role name --- thehive-backend/app/controllers/UserCtrl.scala | 4 ++-- thehive-backend/app/models/Roles.scala | 18 +++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/thehive-backend/app/controllers/UserCtrl.scala b/thehive-backend/app/controllers/UserCtrl.scala index b7d268c69f..4df774ce92 100644 --- a/thehive-backend/app/controllers/UserCtrl.scala +++ b/thehive-backend/app/controllers/UserCtrl.scala @@ -112,12 +112,12 @@ class UserCtrl @Inject() ( } @Timed - def getKey(id: String): Action[AnyContent] = authenticated(Roles.admin).async { implicit request => + def getKey(id: String): Action[AnyContent] = authenticated(Roles.admin).async { implicit request ⇒ authSrv.getKey(id).map(Ok(_)) } @Timed - def renewKey(id: String): Action[AnyContent] = authenticated(Roles.admin).async { implicit request => + def renewKey(id: String): Action[AnyContent] = authenticated(Roles.admin).async { implicit request ⇒ authSrv.renewKey(id).map(Ok(_)) } } \ No newline at end of file diff --git a/thehive-backend/app/models/Roles.scala b/thehive-backend/app/models/Roles.scala index f613b11f93..2b86eb7e01 100644 --- a/thehive-backend/app/models/Roles.scala +++ b/thehive-backend/app/models/Roles.scala @@ -13,15 +13,15 @@ import org.elastic4play.models.AttributeFormat import org.elastic4play.services.Role object Roles { - object read extends Role("read") - object write extends Role("write") - object admin extends Role("admin") - object alert extends Role("alert") - val roles = read :: write :: admin :: alert :: Nil - - val roleNames = roles.map(_.name) - def isValid(roleName: String) = roleNames.contains(roleName) - def withName(roleName: String) = roles.find(_.name == roleName) + object read extends Role("READ") + object write extends Role("WRITE") + object admin extends Role("ADMIN") + object alert extends Role("ALERT") + val roles: List[Role] = read :: write :: admin :: alert :: Nil + + val roleNames: List[String] = roles.map(_.name) + def isValid(roleName: String): Boolean = roleNames.contains(roleName) + def withName(roleName: String): Option[Role] = roles.find(_.name == roleName) } object RoleAttributeFormat extends AttributeFormat[Role]("role") { From 008a59a315e42602276437f8285d410a73f96a0a Mon Sep 17 00:00:00 2001 From: To-om Date: Tue, 5 Sep 2017 10:36:06 +0200 Subject: [PATCH 21/49] #263 Don't delegate user key search to ES as key is not indexed --- thehive-backend/app/controllers/UserCtrl.scala | 6 +++--- thehive-backend/app/models/User.scala | 3 +-- thehive-backend/app/services/LocalAuthSrv.scala | 4 +++- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/thehive-backend/app/controllers/UserCtrl.scala b/thehive-backend/app/controllers/UserCtrl.scala index 4df774ce92..a8a7395be6 100644 --- a/thehive-backend/app/controllers/UserCtrl.scala +++ b/thehive-backend/app/controllers/UserCtrl.scala @@ -46,9 +46,9 @@ class UserCtrl @Inject() ( @Timed def update(id: String): Action[Fields] = authenticated(Roles.read).async(fieldsBodyParser) { implicit request ⇒ if (id == request.authContext.userId || request.authContext.roles.contains(Roles.admin)) { - if (request.body.contains("password")) - logger.warn("Change password attribute using update operation is deprecated. Please use dedicated API (setPassword and changePassword)") - userSrv.update(id, request.body.unset("password")).map { user ⇒ + if (request.body.contains("password") || request.body.contains("key")) + logger.warn("Change password or key using update operation is deprecated. Please use dedicated API (setPassword, changePassword or renewKey)") + userSrv.update(id, request.body.unset("password").unset("key")).map { user ⇒ renderer.toOutput(OK, user) } } diff --git a/thehive-backend/app/models/User.scala b/thehive-backend/app/models/User.scala index b536b5d43a..505429d1ef 100644 --- a/thehive-backend/app/models/User.scala +++ b/thehive-backend/app/models/User.scala @@ -18,8 +18,7 @@ object UserStatus extends Enumeration with HiveEnumeration { trait UserAttributes { _: AttributeDef ⇒ val login = attribute("login", F.stringFmt, "Login of the user", O.form) val userId = attribute("_id", F.stringFmt, "User id (login)", O.model) - val withKey = optionalAttribute("with-key", F.booleanFmt, "Generate an API key", O.form) - val key = optionalAttribute("key", F.stringFmt, "API key", O.model, O.sensitive, O.unaudited) + val key = optionalAttribute("key", F.stringFmt, "API key", O.sensitive, O.unaudited) val userName = attribute("name", F.stringFmt, "Full name (Firstname Lastname)") val roles = multiAttribute("roles", RoleAttributeFormat, "Comma separated role list (READ, WRITE and ADMIN)") val status = attribute("status", F.enumFmt(UserStatus), "Status of the user", UserStatus.Ok) diff --git a/thehive-backend/app/services/LocalAuthSrv.scala b/thehive-backend/app/services/LocalAuthSrv.scala index 734d2e47b9..334ff1d737 100644 --- a/thehive-backend/app/services/LocalAuthSrv.scala +++ b/thehive-backend/app/services/LocalAuthSrv.scala @@ -43,8 +43,10 @@ class LocalAuthSrv @Inject() ( override def authenticate(key: String)(implicit request: RequestHeader): Future[AuthContext] = { import org.elastic4play.services.QueryDSL._ - userSrv.find(and("status" ~= "Ok", "key" ~= key), Some("0-1"), Nil) + // key attribute is sensitive so it is not possible to search on that field + userSrv.find("status" ~= "Ok", Some("all"), Nil) ._1 + .filter(_.key().contains(key)) .runWith(Sink.headOption) .flatMap { case Some(user) ⇒ userSrv.getFromUser(request, user) From cef3e18d1e491615e44a24a7a2baec3e7efd6d12 Mon Sep 17 00:00:00 2001 From: To-om Date: Tue, 5 Sep 2017 11:21:13 +0200 Subject: [PATCH 22/49] #293 include object in webhook body --- thehive-backend/app/services/WebHook.scala | 26 +++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/thehive-backend/app/services/WebHook.scala b/thehive-backend/app/services/WebHook.scala index cd0ca5dbc0..3783197a3c 100644 --- a/thehive-backend/app/services/WebHook.scala +++ b/thehive-backend/app/services/WebHook.scala @@ -3,17 +3,19 @@ package services import java.net.ConnectException import javax.inject.Inject -import scala.concurrent.ExecutionContext +import scala.concurrent.{ ExecutionContext, Future } import scala.util.{ Failure, Success, Try } import play.api.{ Configuration, Logger } import play.api.libs.json.JsObject import play.api.libs.ws.WSRequest +import org.elastic4play.services.AuxSrv + case class WebHook(name: String, ws: WSRequest)(implicit ec: ExecutionContext) { private[WebHook] lazy val logger = Logger(getClass.getName + "." + name) - def send(obj: JsObject) = ws.post(obj).onComplete { + def send(obj: JsObject): Unit = ws.post(obj).onComplete { case Success(resp) if resp.status / 100 != 2 ⇒ logger.error(s"WebHook returns status ${resp.status} ${resp.statusText}") case Failure(ce: ConnectException) ⇒ logger.error(s"Connection to WebHook $name error", ce) case Failure(error) ⇒ logger.error("WebHook call error", error) @@ -22,10 +24,13 @@ case class WebHook(name: String, ws: WSRequest)(implicit ec: ExecutionContext) { } class WebHooks( - webhooks: Seq[WebHook]) { + webhooks: Seq[WebHook], + auxSrv: AuxSrv, + implicit val ec: ExecutionContext) { @Inject() def this( configuration: Configuration, globalWS: CustomWSAPI, + auxSrv: AuxSrv, ec: ExecutionContext) = { this( for { @@ -35,8 +40,19 @@ class WebHooks( whConfig ← Try(cfg.get[Configuration](name)).toOption url ← whConfig.getOptional[String]("url") instanceWS = whWS.withConfig(whConfig).url(url) - } yield WebHook(name, instanceWS)(ec)) + } yield WebHook(name, instanceWS)(ec), + auxSrv, + ec) } - def send(obj: JsObject): Unit = webhooks.foreach(_.send(obj)) + def send(obj: JsObject): Unit = { + (for { + objectType ← (obj \ "objectType").asOpt[String] + objectId ← (obj \ "objectId").asOpt[String] + } yield auxSrv(objectType, objectId, nparent = 0, withStats = false, removeUnaudited = false)) + .getOrElse(Future.successful(JsObject(Nil))) + .map(o ⇒ obj + ("object" → o)) + .fallbackTo(Future.successful(obj)) + .map(o ⇒ webhooks.foreach(_.send(o))) + } } From b7d2320178016af9868aeefd32b960790da54317 Mon Sep 17 00:00:00 2001 From: To-om Date: Tue, 5 Sep 2017 11:31:38 +0200 Subject: [PATCH 23/49] #263 make role case insensitive --- thehive-backend/app/models/JsonFormat.scala | 2 +- thehive-backend/app/models/Roles.scala | 15 +++++++++------ thehive-backend/app/models/User.scala | 4 +++- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/thehive-backend/app/models/JsonFormat.scala b/thehive-backend/app/models/JsonFormat.scala index 12d753ec51..3f79ed3ef5 100644 --- a/thehive-backend/app/models/JsonFormat.scala +++ b/thehive-backend/app/models/JsonFormat.scala @@ -20,7 +20,7 @@ object JsonFormat { implicit val pathWrites: Writes[Path] = Writes((value: Path) ⇒ JsString(value.toString)) - private val roleWrites: Writes[Role] = Writes((role: Role) ⇒ JsString(role.name)) + private val roleWrites: Writes[Role] = Writes((role: Role) ⇒ JsString(role.name.toLowerCase())) private val roleReads: Reads[Role] = Reads { case JsString(s) if Roles.isValid(s) ⇒ JsSuccess(Roles.withName(s).get) case _ ⇒ JsError(Seq(JsPath → Seq(JsonValidationError(s"error.expected.role(${Roles.roleNames}")))) diff --git a/thehive-backend/app/models/Roles.scala b/thehive-backend/app/models/Roles.scala index 2b86eb7e01..927b57e60f 100644 --- a/thehive-backend/app/models/Roles.scala +++ b/thehive-backend/app/models/Roles.scala @@ -13,15 +13,18 @@ import org.elastic4play.models.AttributeFormat import org.elastic4play.services.Role object Roles { - object read extends Role("READ") - object write extends Role("WRITE") - object admin extends Role("ADMIN") - object alert extends Role("ALERT") + object read extends Role("read") + object write extends Role("write") + object admin extends Role("admin") + object alert extends Role("alert") val roles: List[Role] = read :: write :: admin :: alert :: Nil val roleNames: List[String] = roles.map(_.name) - def isValid(roleName: String): Boolean = roleNames.contains(roleName) - def withName(roleName: String): Option[Role] = roles.find(_.name == roleName) + def isValid(roleName: String): Boolean = roleNames.contains(roleName.toLowerCase()) + def withName(roleName: String): Option[Role] = { + val lowerCaseRole = roleName.toLowerCase() + roles.find(_.name == lowerCaseRole) + } } object RoleAttributeFormat extends AttributeFormat[Role]("role") { diff --git a/thehive-backend/app/models/User.scala b/thehive-backend/app/models/User.scala index 505429d1ef..f36bfd88ba 100644 --- a/thehive-backend/app/models/User.scala +++ b/thehive-backend/app/models/User.scala @@ -3,7 +3,7 @@ package models import scala.concurrent.Future import play.api.libs.json.JsValue.jsValueToJsLookup -import play.api.libs.json.{ JsObject, JsString } +import play.api.libs.json.{ JsArray, JsObject, JsString } import models.JsonFormat.userStatusFormat import services.AuditedModel @@ -39,4 +39,6 @@ class UserModel extends ModelDef[UserModel, User]("user") with UserAttributes wi class User(model: UserModel, attributes: JsObject) extends EntityDef[UserModel, User](model, attributes) with UserAttributes with org.elastic4play.services.User { override def getUserName = userName() override def getRoles = roles() + + override def toJson: JsObject = super.toJson + ("roles" → JsArray(roles().map(r ⇒ JsString(r.name.toLowerCase())))) } \ No newline at end of file From a5d9feda7409bff5d629f10bcc165b7ab3d1b4f2 Mon Sep 17 00:00:00 2001 From: To-om Date: Tue, 5 Sep 2017 11:45:51 +0200 Subject: [PATCH 24/49] #280 Make number of maximum similar cases configurable under key "maxSimilarCases" (default=100) --- thehive-backend/app/services/AlertSrv.scala | 2 ++ thehive-backend/app/services/CaseSrv.scala | 31 +++++++++++++++++++-- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/thehive-backend/app/services/AlertSrv.scala b/thehive-backend/app/services/AlertSrv.scala index d67cc97d7a..2a90ab3384 100644 --- a/thehive-backend/app/services/AlertSrv.scala +++ b/thehive-backend/app/services/AlertSrv.scala @@ -37,6 +37,7 @@ object AlertSrv { @Singleton class AlertSrv( + maxSimilarCases: Int, templates: Map[String, String], alertModel: AlertModel, createSrv: CreateSrv, @@ -68,6 +69,7 @@ class AlertSrv( connectors: ConnectorRouter, ec: ExecutionContext, mat: Materializer) = this( + configuration.getOptional[Int]("maxSimilarCases").getOrElse(100), Map.empty[String, String], alertModel: AlertModel, createSrv, diff --git a/thehive-backend/app/services/CaseSrv.scala b/thehive-backend/app/services/CaseSrv.scala index b75b0edd7d..e802d77cab 100644 --- a/thehive-backend/app/services/CaseSrv.scala +++ b/thehive-backend/app/services/CaseSrv.scala @@ -5,7 +5,7 @@ import javax.inject.{ Inject, Singleton } import scala.concurrent.{ ExecutionContext, Future } import scala.util.Try -import play.api.Logger +import play.api.{ Configuration, Logger } import play.api.libs.json.Json.toJsFieldJsValueWrapper import play.api.libs.json._ @@ -18,7 +18,8 @@ import org.elastic4play.controllers.Fields import org.elastic4play.services._ @Singleton -class CaseSrv @Inject() ( +class CaseSrv( + maxSimilarCases: Int, caseModel: CaseModel, artifactModel: ArtifactModel, taskModel: TaskModel, @@ -30,6 +31,30 @@ class CaseSrv @Inject() ( findSrv: FindSrv, implicit val ec: ExecutionContext) { + @Inject() def this( + configuration: Configuration, + caseModel: CaseModel, + artifactModel: ArtifactModel, + taskModel: TaskModel, + createSrv: CreateSrv, + artifactSrv: ArtifactSrv, + getSrv: GetSrv, + updateSrv: UpdateSrv, + deleteSrv: DeleteSrv, + findSrv: FindSrv, + ec: ExecutionContext) = this( + configuration.getOptional[Int]("maxSimilarCases").getOrElse(100), + caseModel, + artifactModel, + taskModel, + createSrv, + artifactSrv, + getSrv, + updateSrv, + deleteSrv, + findSrv, + ec) + private[CaseSrv] lazy val logger = Logger(getClass) def applyTemplate(template: CaseTemplate, originalFields: Fields): Fields = { @@ -120,7 +145,7 @@ class CaseSrv @Inject() ( "status" ~= "Ok"), Some("all"), Nil) ._1 .flatMapConcat { artifact ⇒ artifactSrv.findSimilar(artifact, Some("all"), Nil)._1 } - .groupBy(100, _.parentId) + .groupBy(maxSimilarCases, _.parentId) .map { a ⇒ (a.parentId, Seq(a)) } .reduce((l, r) ⇒ (l._1, r._2 ++ l._2)) .mergeSubstreams From bf1c866cab3a6b6addaebe82d7a0f604c32132f8 Mon Sep 17 00:00:00 2001 From: To-om Date: Tue, 5 Sep 2017 11:54:51 +0200 Subject: [PATCH 25/49] #300 Set severity to medium if threat level is out of range --- thehive-misp/app/connectors/misp/JsonFormat.scala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/thehive-misp/app/connectors/misp/JsonFormat.scala b/thehive-misp/app/connectors/misp/JsonFormat.scala index 52ee070c91..2b7234e013 100644 --- a/thehive-misp/app/connectors/misp/JsonFormat.scala +++ b/thehive-misp/app/connectors/misp/JsonFormat.scala @@ -31,7 +31,8 @@ object JsonFormat { date = new Date(timestamp.toLong * 1000) publishTimestamp ← (json \ "publish_timestamp").validate[String] publishDate = new Date(publishTimestamp.toLong * 1000) - threatLevel ← (json \ "threat_level_id").validate[String] + threatLevelString ← (json \ "threat_level_id").validate[String] + threatLevel = threatLevelString.toLong isPublished ← (json \ "published").validate[Boolean] } yield MispAlert( org, @@ -41,7 +42,7 @@ object JsonFormat { isPublished, s"#$eventId ${info.trim}", s"Imported from MISP Event #$eventId, created at $date", - 4 - threatLevel.toLong, + if (0 < threatLevel && threatLevel < 4) 4 - threatLevel else 2, alertTags, tlp, "") From dbaff4e760395c8398726cff26324552e37f53fa Mon Sep 17 00:00:00 2001 From: To-om Date: Tue, 5 Sep 2017 13:18:46 +0200 Subject: [PATCH 26/49] #302 Encode filename when downloading attachment --- thehive-backend/app/controllers/AttachmentCtrl.scala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/thehive-backend/app/controllers/AttachmentCtrl.scala b/thehive-backend/app/controllers/AttachmentCtrl.scala index 5f21a41696..9d61cbf23e 100644 --- a/thehive-backend/app/controllers/AttachmentCtrl.scala +++ b/thehive-backend/app/controllers/AttachmentCtrl.scala @@ -1,5 +1,6 @@ package controllers +import java.net.URLEncoder import java.nio.file.Files import javax.inject.{ Inject, Singleton } @@ -62,7 +63,7 @@ class AttachmentCtrl( header = ResponseHeader( 200, Map( - "Content-Disposition" → s"""attachment; filename="${name.getOrElse(hash)}"""", + "Content-Disposition" → s"""attachment; filename="${URLEncoder.encode(name.getOrElse(hash), "utf-8")}"""", "Content-Transfer-Encoding" → "binary")), body = HttpEntity.Streamed(attachmentSrv.source(hash), None, None)) } @@ -93,7 +94,7 @@ class AttachmentCtrl( header = ResponseHeader( 200, Map( - "Content-Disposition" → s"""attachment; filename="${name.getOrElse(hash)}.zip"""", + "Content-Disposition" → s"""attachment; filename="${URLEncoder.encode(name.getOrElse(hash), "utf-8")}.zip"""", "Content-Type" → "application/zip", "Content-Transfer-Encoding" → "binary", "Content-Length" → Files.size(f).toString)), From deb0e2c5b98978bc8e7e9ce54ac15f4f5bac9c44 Mon Sep 17 00:00:00 2001 From: To-om Date: Wed, 6 Sep 2017 10:44:05 +0200 Subject: [PATCH 27/49] #263 Add separate authSrv for authentication by key. Add removeKey API. Add hasKey to user json output Enable authentication by key in module initialization (can't be disabled) --- .../app/controllers/UserCtrl.scala | 5 ++ thehive-backend/app/global/TheHive.scala | 21 ++----- thehive-backend/app/models/User.scala | 6 +- thehive-backend/app/services/KeyAuthSrv.scala | 59 +++++++++++++++++++ .../app/services/LocalAuthSrv.scala | 28 +-------- .../app/services/TheHiveAuthSrv.scala | 50 ++++++++++++++++ thehive-backend/conf/routes | 1 + 7 files changed, 127 insertions(+), 43 deletions(-) create mode 100644 thehive-backend/app/services/KeyAuthSrv.scala create mode 100644 thehive-backend/app/services/TheHiveAuthSrv.scala diff --git a/thehive-backend/app/controllers/UserCtrl.scala b/thehive-backend/app/controllers/UserCtrl.scala index a8a7395be6..d7922402c9 100644 --- a/thehive-backend/app/controllers/UserCtrl.scala +++ b/thehive-backend/app/controllers/UserCtrl.scala @@ -116,6 +116,11 @@ class UserCtrl @Inject() ( authSrv.getKey(id).map(Ok(_)) } + @Timed + def removeKey(id: String): Action[AnyContent] = authenticated(Roles.admin).async { implicit request ⇒ + authSrv.removeKey(id).map(_ ⇒ Ok) + } + @Timed def renewKey(id: String): Action[AnyContent] = authenticated(Roles.admin).async { implicit request ⇒ authSrv.renewKey(id).map(Ok(_)) diff --git a/thehive-backend/app/global/TheHive.scala b/thehive-backend/app/global/TheHive.scala index c6a2400d33..a7d4b0c67a 100644 --- a/thehive-backend/app/global/TheHive.scala +++ b/thehive-backend/app/global/TheHive.scala @@ -2,9 +2,9 @@ package global import scala.collection.JavaConverters._ +import play.api.libs.concurrent.AkkaGuiceSupport import play.api.mvc.EssentialFilter import play.api.{ Configuration, Environment, Logger, Mode } -import play.api.libs.concurrent.AkkaGuiceSupport import com.google.inject.AbstractModule import com.google.inject.name.Names @@ -19,7 +19,7 @@ import services._ import org.elastic4play.models.BaseModelDef import org.elastic4play.services.auth.MultiAuthSrv -import org.elastic4play.services.{ AuthSrv, AuthSrvFactory, MigrationOperations, TempFilter } +import org.elastic4play.services.{ AuthSrv, MigrationOperations, TempFilter } class TheHive( environment: Environment, @@ -33,7 +33,6 @@ class TheHive( val modelBindings = ScalaMultibinder.newSetBinder[BaseModelDef](binder) val auditedModelBindings = ScalaMultibinder.newSetBinder[AuditedModel](binder) val authBindings = ScalaMultibinder.newSetBinder[AuthSrv](binder) - val authFactoryBindings = ScalaMultibinder.newSetBinder[AuthSrvFactory](binder) val reflectionClasses = new Reflections(new ConfigurationBuilder() .forPackages("org.elastic4play") @@ -61,17 +60,9 @@ class TheHive( .getSubTypesOf(classOf[AuthSrv]) .asScala .filterNot(c ⇒ java.lang.reflect.Modifier.isAbstract(c.getModifiers) || c.isMemberClass) - .filterNot(_ == classOf[MultiAuthSrv]) - .foreach { modelClass ⇒ - authBindings.addBinding.to(modelClass) - } - - reflectionClasses - .getSubTypesOf(classOf[AuthSrvFactory]) - .asScala - .filterNot(c ⇒ java.lang.reflect.Modifier.isAbstract(c.getModifiers)) - .foreach { modelClass ⇒ - authFactoryBindings.addBinding.to(modelClass) + .filterNot(c ⇒ c == classOf[MultiAuthSrv] || c == classOf[TheHiveAuthSrv]) + .foreach { authSrvClass ⇒ + authBindings.addBinding.to(authSrvClass) } val filterBindings = ScalaMultibinder.newSetBinder[EssentialFilter](binder) @@ -80,7 +71,7 @@ class TheHive( filterBindings.addBinding.to[CSRFFilter] bind[MigrationOperations].to[Migration] - bind[AuthSrv].to[MultiAuthSrv] + bind[AuthSrv].to[TheHiveAuthSrv] bindActor[AuditActor]("AuditActor") bindActor[DeadLetterMonitoringActor]("DeadLetterMonitoringActor") diff --git a/thehive-backend/app/models/User.scala b/thehive-backend/app/models/User.scala index f36bfd88ba..f092b0801e 100644 --- a/thehive-backend/app/models/User.scala +++ b/thehive-backend/app/models/User.scala @@ -3,7 +3,7 @@ package models import scala.concurrent.Future import play.api.libs.json.JsValue.jsValueToJsLookup -import play.api.libs.json.{ JsArray, JsObject, JsString } +import play.api.libs.json.{ JsArray, JsBoolean, JsObject, JsString } import models.JsonFormat.userStatusFormat import services.AuditedModel @@ -40,5 +40,7 @@ class User(model: UserModel, attributes: JsObject) extends EntityDef[UserModel, override def getUserName = userName() override def getRoles = roles() - override def toJson: JsObject = super.toJson + ("roles" → JsArray(roles().map(r ⇒ JsString(r.name.toLowerCase())))) + override def toJson: JsObject = super.toJson + + ("roles" → JsArray(roles().map(r ⇒ JsString(r.name.toLowerCase())))) + + ("hasKey" → JsBoolean(key().isDefined)) } \ No newline at end of file diff --git a/thehive-backend/app/services/KeyAuthSrv.scala b/thehive-backend/app/services/KeyAuthSrv.scala new file mode 100644 index 0000000000..2bcc301aba --- /dev/null +++ b/thehive-backend/app/services/KeyAuthSrv.scala @@ -0,0 +1,59 @@ +package services + +import java.util.Base64 +import javax.inject.{ Inject, Singleton } + +import scala.concurrent.{ ExecutionContext, Future } +import scala.util.Random + +import play.api.libs.json.JsArray +import play.api.mvc.RequestHeader + +import akka.stream.Materializer +import akka.stream.scaladsl.Sink + +import org.elastic4play.controllers.Fields +import org.elastic4play.services.{ AuthCapability, AuthContext, AuthSrv } +import org.elastic4play.{ AuthenticationError, BadRequestError } + +@Singleton +class KeyAuthSrv @Inject() ( + userSrv: UserSrv, + implicit val ec: ExecutionContext, + implicit val mat: Materializer) extends AuthSrv { + override val name = "key" + + protected final def generateKey(): String = { + val bytes = Array.ofDim[Byte](24) + Random.nextBytes(bytes) + Base64.getEncoder.encodeToString(bytes) + } + + override val capabilities = Set(AuthCapability.authByKey) + + override def authenticate(key: String)(implicit request: RequestHeader): Future[AuthContext] = { + import org.elastic4play.services.QueryDSL._ + // key attribute is sensitive so it is not possible to search on that field + userSrv.find("status" ~= "Ok", Some("all"), Nil) + ._1 + .filter(_.key().contains(key)) + .runWith(Sink.headOption) + .flatMap { + case Some(user) ⇒ userSrv.getFromUser(request, user) + case None ⇒ Future.failed(AuthenticationError("Authentication failure")) + } + } + + override def renewKey(username: String)(implicit authContext: AuthContext): Future[String] = { + val newKey = generateKey() + userSrv.update(username, Fields.empty.set("key", newKey)).map(_ ⇒ newKey) + } + + override def getKey(username: String)(implicit authContext: AuthContext): Future[String] = { + userSrv.get(username).map(_.key().getOrElse(throw BadRequestError(s"User $username hasn't key"))) + } + + override def removeKey(username: String)(implicit authContext: AuthContext): Future[Unit] = { + userSrv.update(username, Fields.empty.set("key", JsArray())).map(_ ⇒ ()) + } +} diff --git a/thehive-backend/app/services/LocalAuthSrv.scala b/thehive-backend/app/services/LocalAuthSrv.scala index 334ff1d737..fe96e383e5 100644 --- a/thehive-backend/app/services/LocalAuthSrv.scala +++ b/thehive-backend/app/services/LocalAuthSrv.scala @@ -8,13 +8,12 @@ import scala.util.Random import play.api.mvc.RequestHeader import akka.stream.Materializer -import akka.stream.scaladsl.Sink import models.User import org.elastic4play.controllers.Fields import org.elastic4play.services.{ AuthCapability, AuthContext, AuthSrv } import org.elastic4play.utils.Hasher -import org.elastic4play.{ AuthenticationError, AuthorizationError, BadRequestError } +import org.elastic4play.{ AuthenticationError, AuthorizationError } @Singleton class LocalAuthSrv @Inject() ( @@ -23,7 +22,7 @@ class LocalAuthSrv @Inject() ( implicit val mat: Materializer) extends AuthSrv { val name = "local" - override val capabilities = Set(AuthCapability.changePassword, AuthCapability.setPassword, AuthCapability.renewKey) + override val capabilities = Set(AuthCapability.changePassword, AuthCapability.setPassword) private[services] def doAuthenticate(user: User, password: String): Boolean = { user.password().map(_.split(",", 2)).fold(false) { @@ -41,19 +40,6 @@ class LocalAuthSrv @Inject() ( } } - override def authenticate(key: String)(implicit request: RequestHeader): Future[AuthContext] = { - import org.elastic4play.services.QueryDSL._ - // key attribute is sensitive so it is not possible to search on that field - userSrv.find("status" ~= "Ok", Some("all"), Nil) - ._1 - .filter(_.key().contains(key)) - .runWith(Sink.headOption) - .flatMap { - case Some(user) ⇒ userSrv.getFromUser(request, user) - case None ⇒ Future.failed(AuthenticationError("Authentication failure")) - } - } - override def changePassword(username: String, oldPassword: String, newPassword: String)(implicit authContext: AuthContext): Future[Unit] = { userSrv.get(username).flatMap { user ⇒ if (doAuthenticate(user, oldPassword)) setPassword(username, newPassword) @@ -66,14 +52,4 @@ class LocalAuthSrv @Inject() ( val newHash = seed + "," + Hasher("SHA-256").fromString(seed + newPassword).head.toString userSrv.update(username, Fields.empty.set("password", newHash)).map(_ ⇒ ()) } - - override def renewKey(username: String)(implicit authContext: AuthContext): Future[String] = { - val newKey = generateKey() - userSrv.update(username, Fields.empty.set("key", newKey)).map(_ ⇒ newKey) - } - - override def getKey(username: String)(implicit authContext: AuthContext): Future[String] = { - userSrv.get(username).map(_.key().getOrElse(throw BadRequestError(s"User $username hasn't key"))) - } - } \ No newline at end of file diff --git a/thehive-backend/app/services/TheHiveAuthSrv.scala b/thehive-backend/app/services/TheHiveAuthSrv.scala new file mode 100644 index 0000000000..dfc27cadc7 --- /dev/null +++ b/thehive-backend/app/services/TheHiveAuthSrv.scala @@ -0,0 +1,50 @@ +package services + +import javax.inject.{ Inject, Singleton } + +import scala.collection.immutable +import scala.concurrent.{ ExecutionContext, Future } +import scala.util.{ Failure, Success } + +import play.api.mvc.RequestHeader +import play.api.{ Configuration, Logger } + +import org.elastic4play.AuthenticationError +import org.elastic4play.services.{ AuthContext, AuthSrv } +import org.elastic4play.services.auth.MultiAuthSrv + +object TheHiveAuthSrv { + private[TheHiveAuthSrv] lazy val logger = Logger(getClass) + + def getAuthSrv(authTypes: Seq[String], authModules: immutable.Set[AuthSrv]): Seq[AuthSrv] = { + ("key" +: authTypes.filterNot(_ == "key")) + .flatMap { authType ⇒ + authModules.find(_.name == authType) + .orElse { + logger.error(s"Authentication module $authType not found") + None + } + } + } +} + +@Singleton +class TheHiveAuthSrv @Inject() ( + configuration: Configuration, + authModules: immutable.Set[AuthSrv], + userSrv: UserSrv, + override implicit val ec: ExecutionContext) extends MultiAuthSrv( + TheHiveAuthSrv.getAuthSrv( + configuration.getOptional[Seq[String]]("auth.type").getOrElse(Seq("local")), + authModules), + ec) { + + // Uncomment the following lines if you want to prevent user with key to use password to authenticate + // override def authenticate(username: String, password: String)(implicit request: RequestHeader): Future[AuthContext] = + // userSrv.get(username) + // .transformWith { + // case Success(user) if user.key().isDefined ⇒ Future.failed(AuthenticationError("Authentication by password is not permitted for user with key")) + // case _: Success[_] ⇒ super.authenticate(username, password) + // case _: Failure[_] ⇒ Future.failed(AuthenticationError("Authentication failure")) + // } +} \ No newline at end of file diff --git a/thehive-backend/conf/routes b/thehive-backend/conf/routes index fcaba89198..7ce66025ca 100644 --- a/thehive-backend/conf/routes +++ b/thehive-backend/conf/routes @@ -92,6 +92,7 @@ PATCH /api/user/:userId controllers.UserCtrl.update(us POST /api/user/:userId/password/set controllers.UserCtrl.setPassword(userId) POST /api/user/:userId/password/change controllers.UserCtrl.changePassword(userId) GET /api/user/:userId/key controllers.UserCtrl.getKey(userId) +DELETE /api/user/:userId/key controllers.UserCtrl.removeKey(userId) POST /api/user/:userId/key/renew controllers.UserCtrl.renewKey(userId) From a4915030ba33437cf5c10e741a0fc3dee98a510e Mon Sep 17 00:00:00 2001 From: Nabil Adouani Date: Tue, 5 Sep 2017 14:29:15 +0200 Subject: [PATCH 28/49] #52 Add a share button on case details page to export to misp --- .../app/connectors/misp/MispCtrl.scala | 14 ++--- ui/app/index.html | 3 +- ui/app/scripts/app.js | 11 ---- .../controllers/case/CaseExportCtrl.js | 63 ------------------- .../scripts/controllers/case/CaseMainCtrl.js | 15 +++-- .../misc/ServerInstanceDialogCtrl.js | 20 ++++++ ui/app/scripts/services/CortexSrv.js | 2 +- ui/app/scripts/services/MispSrv.js | 44 ++++++++----- .../partials/misp/choose-instance-dialog.html | 22 +++++++ ui/app/views/partials/misp/error-modal.html | 28 +++++++++ 10 files changed, 120 insertions(+), 102 deletions(-) delete mode 100644 ui/app/scripts/controllers/case/CaseExportCtrl.js create mode 100644 ui/app/scripts/controllers/misc/ServerInstanceDialogCtrl.js create mode 100644 ui/app/views/partials/misp/choose-instance-dialog.html create mode 100644 ui/app/views/partials/misp/error-modal.html diff --git a/thehive-misp/app/connectors/misp/MispCtrl.scala b/thehive-misp/app/connectors/misp/MispCtrl.scala index 060749e0d5..315d79995c 100644 --- a/thehive-misp/app/connectors/misp/MispCtrl.scala +++ b/thehive-misp/app/connectors/misp/MispCtrl.scala @@ -9,7 +9,7 @@ import play.api.http.Status import play.api.libs.json.{ JsObject, Json } import play.api.mvc._ import play.api.routing.SimpleRouter -import play.api.routing.sird.{ GET, UrlContext } +import play.api.routing.sird.{ GET, POST, UrlContext } import connectors.Connector import models.{ Alert, Case, Roles, UpdateMispAlertArtifact } @@ -40,11 +40,11 @@ class MispCtrl @Inject() ( private[MispCtrl] lazy val logger = Logger(getClass) val router = SimpleRouter { - case GET(p"/_syncAlerts") ⇒ syncAlerts - case GET(p"/_syncAllAlerts") ⇒ syncAllAlerts - case GET(p"/_syncArtifacts") ⇒ syncArtifacts - case GET(p"/export/$caseId/to/$mispName") ⇒ exportCase(mispName, caseId) - case r ⇒ throw NotFoundError(s"${r.uri} not found") + case GET(p"/_syncAlerts") ⇒ syncAlerts + case GET(p"/_syncAllAlerts") ⇒ syncAllAlerts + case GET(p"/_syncArtifacts") ⇒ syncArtifacts + case POST(p"/export/$caseId/$mispName") ⇒ exportCase(mispName, caseId) + case r ⇒ throw NotFoundError(s"${r.uri} not found") } @Timed @@ -83,4 +83,4 @@ class MispCtrl @Inject() ( override def mergeWithCase(alert: Alert, caze: Case)(implicit authContext: AuthContext): Future[Case] = { mispSrv.mergeWithCase(alert, caze) } -} \ No newline at end of file +} diff --git a/ui/app/index.html b/ui/app/index.html index 9ed00bc79e..db38fa9395 100644 --- a/ui/app/index.html +++ b/ui/app/index.html @@ -147,7 +147,6 @@ - @@ -161,7 +160,7 @@ - + diff --git a/ui/app/scripts/app.js b/ui/app/scripts/app.js index f772e77ad9..8dfb0b5132 100644 --- a/ui/app/scripts/app.js +++ b/ui/app/scripts/app.js @@ -239,17 +239,6 @@ angular.module('thehive', ['ngAnimate', 'ngMessages', 'ngSanitize', 'ui.bootstra templateUrl: 'views/partials/case/case.links.html', controller: 'CaseLinksCtrl' }) - .state('app.case.export', { - url: '/export', - templateUrl: 'views/partials/case/case.export.html', - controller: 'CaseExportCtrl', - controllerAs: '$vm', - resolve: { - categories: function(MispSrv) { - return MispSrv.categories(); - } - } - }) .state('app.case.tasks-item', { url: '/tasks/{itemId}', templateUrl: 'views/partials/case/case.tasks.item.html', diff --git a/ui/app/scripts/controllers/case/CaseExportCtrl.js b/ui/app/scripts/controllers/case/CaseExportCtrl.js deleted file mode 100644 index a153930ab7..0000000000 --- a/ui/app/scripts/controllers/case/CaseExportCtrl.js +++ /dev/null @@ -1,63 +0,0 @@ -(function() { - 'use strict'; - angular.module('theHiveControllers').controller('CaseExportCtrl', - function($scope, $state, $stateParams, $timeout, PSearchSrv, CaseTabsSrv, categories) { - var self = this; - - this.caseId = $stateParams.caseId; - this.searchForm = {}; - var tabName = 'export-' + this.caseId; - - // MISP category/type map - this.categories = categories; - - // Add tab - CaseTabsSrv.addTab(tabName, { - name: tabName, - label: 'Export', - closable: true, - state: 'app.case.export', - params: {} - }); - - // Select tab - $timeout(function() { - CaseTabsSrv.activateTab(tabName); - }, 0); - - - this.artifacts = PSearchSrv(this.caseId, 'case_artifact', { - scope: $scope, - baseFilter: { - '_and': [{ - '_parent': { - "_type": "case", - "_query": { - "_id": $scope.caseId - } - } - }, { - 'ioc': true - }, { - 'status': 'Ok' - }] - }, - filter: this.searchForm.searchQuery !== '' ? { - _string: this.searchForm.searchQuery - } : '', - loadAll: true, - sort: '-startDate', - pageSize: 30, - onUpdate: function () { - self.enhanceArtifacts(); - }, - nstats: true - }); - - this.enhanceArtifacts = function(data) { - console.log(data); - } - - } - ); -})(); diff --git a/ui/app/scripts/controllers/case/CaseMainCtrl.js b/ui/app/scripts/controllers/case/CaseMainCtrl.js index 9003d9f2d2..4fb028b5bc 100644 --- a/ui/app/scripts/controllers/case/CaseMainCtrl.js +++ b/ui/app/scripts/controllers/case/CaseMainCtrl.js @@ -1,7 +1,7 @@ (function() { 'use strict'; angular.module('theHiveControllers').controller('CaseMainCtrl', - function($scope, $rootScope, $state, $stateParams, $q, $uibModal, CaseTabsSrv, CaseSrv, MetricsCacheSrv, UserInfoSrv, StreamStatSrv, NotificationSrv, UtilsSrv, CaseResolutionStatus, CaseImpactStatus, caze) { + function($scope, $rootScope, $state, $stateParams, $q, $uibModal, CaseTabsSrv, CaseSrv, MetricsCacheSrv, UserInfoSrv, MispSrv, StreamStatSrv, NotificationSrv, UtilsSrv, CaseResolutionStatus, CaseImpactStatus, caze) { $scope.CaseResolutionStatus = CaseResolutionStatus; $scope.CaseImpactStatus = CaseImpactStatus; @@ -199,9 +199,16 @@ }; $scope.shareCase = function() { - $state.go('app.case.export', { - caseId: $scope.caseId - }); + var mispConfig = $scope.appConfig.connectors.misp; + MispSrv.getServer(mispConfig) + .then(function(server) { + return MispSrv.export($scope.caseId, server); + }) + .then(function(response){ + console.log(response); + }, function(err) { + console.log(err); + }); }; /** diff --git a/ui/app/scripts/controllers/misc/ServerInstanceDialogCtrl.js b/ui/app/scripts/controllers/misc/ServerInstanceDialogCtrl.js new file mode 100644 index 0000000000..1e86095a85 --- /dev/null +++ b/ui/app/scripts/controllers/misc/ServerInstanceDialogCtrl.js @@ -0,0 +1,20 @@ +(function() { + 'use strict'; + angular.module('theHiveControllers') + .controller('ServerInstanceDialogCtrl', ServerInstanceDialogCtrl); + + function ServerInstanceDialogCtrl($uibModalInstance, servers) { + var self = this; + + this.servers = servers; + this.selected = null; + + this.ok = function() { + $uibModalInstance.close(this.selected); + }; + + this.cancel = function() { + $uibModalInstance.dismiss(); + }; + } +})(); diff --git a/ui/app/scripts/services/CortexSrv.js b/ui/app/scripts/services/CortexSrv.js index 0dd722c419..71b5108c4a 100644 --- a/ui/app/scripts/services/CortexSrv.js +++ b/ui/app/scripts/services/CortexSrv.js @@ -66,7 +66,7 @@ promptForInstance: function(servers) { var modalInstance = $uibModal.open({ templateUrl: 'views/partials/cortex/choose-instance-dialog.html', - controller: 'CortexInstanceDialogCtrl', + controller: 'ServerInstanceDialogCtrl', controllerAs: 'vm', size: '', resolve: { diff --git a/ui/app/scripts/services/MispSrv.js b/ui/app/scripts/services/MispSrv.js index 2e5a552834..d252cfcf0c 100644 --- a/ui/app/scripts/services/MispSrv.js +++ b/ui/app/scripts/services/MispSrv.js @@ -1,7 +1,7 @@ (function() { 'use strict'; angular.module('theHiveServices') - .factory('MispSrv', function($q, $http, $rootScope, StatSrv, StreamSrv, PSearchSrv) { + .factory('MispSrv', function($q, $http, $rootScope, $uibModal, StatSrv, StreamSrv, PSearchSrv) { var baseUrl = './api/connector/misp'; @@ -118,21 +118,37 @@ return defer.promise; }, - categories: function() { - var defer = $q.defer(); - - $q.resolve({ - 'category1': [ - 'type1.1', 'type1.2', 'type1.3' - ], - 'category2': [ - 'type2.1', 'type2.2', 'type2.3' - ] - }).then(function(response) { - defer.resolve(response); + promptForInstance: function(servers) { + var modalInstance = $uibModal.open({ + templateUrl: 'views/partials/misp/choose-instance-dialog.html', + controller: 'ServerInstanceDialogCtrl', + controllerAs: 'vm', + size: '', + resolve: { + servers: function() { + return servers; + } + } }); - return defer.promise; + return modalInstance.result; + }, + + getServer: function(mispConfig) { + if(!mispConfig || !mispConfig.enabled || !mispConfig.servers) { + return $q.reject(); + } + + var servers = mispConfig.servers; + if (servers.length === 1) { + return $q.resolve(servers[0]); + } else { + return factory.promptForInstance(servers); + } + }, + + export: function(caseId, server) { + return $http.post(baseUrl + '/export/' + caseId + '/' + server, {}); } }; diff --git a/ui/app/views/partials/misp/choose-instance-dialog.html b/ui/app/views/partials/misp/choose-instance-dialog.html new file mode 100644 index 0000000000..1315ad0a04 --- /dev/null +++ b/ui/app/views/partials/misp/choose-instance-dialog.html @@ -0,0 +1,22 @@ +
+ + + +
diff --git a/ui/app/views/partials/misp/error-modal.html b/ui/app/views/partials/misp/error-modal.html new file mode 100644 index 0000000000..4794776206 --- /dev/null +++ b/ui/app/views/partials/misp/error-modal.html @@ -0,0 +1,28 @@ + + + + + From a9c0980c7d7d493c714e567e4cd9f2cc3eb40c24 Mon Sep 17 00:00:00 2001 From: Nabil Adouani Date: Thu, 7 Sep 2017 16:42:12 +0200 Subject: [PATCH 29/49] #263 Update the user creation UI --- ui/app/index.html | 2 + .../controllers/admin/AdminUserDialogCtrl.js | 65 +++++++++ .../controllers/admin/AdminUsersCtrl.js | 54 ++++++- ui/app/scripts/services/UserSrv.js | 23 +++ ui/app/views/partials/admin/user-dialog.html | 75 ++++++++++ ui/app/views/partials/admin/users.html | 132 +++++++----------- 6 files changed, 263 insertions(+), 88 deletions(-) create mode 100644 ui/app/scripts/controllers/admin/AdminUserDialogCtrl.js create mode 100644 ui/app/views/partials/admin/user-dialog.html diff --git a/ui/app/index.html b/ui/app/index.html index db38fa9395..b65392d5ff 100644 --- a/ui/app/index.html +++ b/ui/app/index.html @@ -140,6 +140,7 @@ + @@ -160,6 +161,7 @@ + diff --git a/ui/app/scripts/controllers/admin/AdminUserDialogCtrl.js b/ui/app/scripts/controllers/admin/AdminUserDialogCtrl.js new file mode 100644 index 0000000000..597ddb00ce --- /dev/null +++ b/ui/app/scripts/controllers/admin/AdminUserDialogCtrl.js @@ -0,0 +1,65 @@ +(function() { + 'use strict'; + + angular.module('theHiveControllers').controller('AdminUserDialogCtrl', function($scope, $uibModalInstance, UserSrv, NotificationSrv, user) { + var self = this; + + self.user = user; + self.isEdit = user.id; + + self.formData = _.defaults(_.pick(self.user, 'id', 'name', 'roles'), { + id: null, + name: null, + roles: [], + alert: false + }); + self.formData.alert = self.formData.roles.indexOf('alert') !== -1; + + var onSuccess = function(data) { + $uibModalInstance.close(data); + }; + + var onFailure = function(response) { + NotificationSrv.error('AdminUserDialogCtrl', response.data, response.status); + }; + + var buildRoles = function(roles, alert) { + var result = angular.copy(roles) || []; + + if(alert && roles.indexOf('alert') === -1) { + result.push('alert'); + } else if (!alert && roles.indexOf('alert') !== -1) { + result = _.omit(result, 'alert'); + } + + return result; + }; + + self.saveUser = function(form) { + if (!form.$valid) { + return; + } + + var postData = {}; + + if (self.user.id) { + postData = { + name: self.formData.name, + roles: buildRoles(self.formData.roles, self.formData.alert) + }; + UserSrv.update({'userId': self.user.id}, postData, onSuccess, onFailure); + } else { + postData = { + login: angular.lowercase(self.formData.id), + name: self.formData.name, + roles: buildRoles(self.formData.roles, self.formData.alert) + }; + UserSrv.save(postData, onSuccess, onFailure); + } + }; + + self.cancel = function() { + $uibModalInstance.dismiss(); + } + }); +})(); diff --git a/ui/app/scripts/controllers/admin/AdminUsersCtrl.js b/ui/app/scripts/controllers/admin/AdminUsersCtrl.js index 26773bf493..1abda270b2 100644 --- a/ui/app/scripts/controllers/admin/AdminUsersCtrl.js +++ b/ui/app/scripts/controllers/admin/AdminUsersCtrl.js @@ -2,7 +2,7 @@ 'use strict'; angular.module('theHiveControllers').controller('AdminUsersCtrl', - function($scope, PSearchSrv, UserSrv, NotificationSrv, appConfig) { + function($scope, $uibModal, PSearchSrv, UserSrv, NotificationSrv, clipboard, appConfig) { $scope.appConfig = appConfig; $scope.canSetPass = appConfig.config.capabilities.indexOf('setPassword') !== -1; $scope.newUser = { @@ -48,15 +48,35 @@ $scope.usrKey = {}; $scope.getKey = function(user) { - UserSrv.get({ + UserSrv.getKey(user.id) + .then(function(key) { + $scope.usrKey[user.id] = key; + }); + + }; + $scope.createKey = function(user) { + UserSrv.setKey({ userId: user.id - }, function(usr) { - $scope.usrKey[user.id] = usr.key; + },{}, function() { + delete $scope.usrKey[user.id]; + }, function(response) { + NotificationSrv.error('AdminUsersCtrl', response.data, response.status); }); + }; + $scope.revokeKey = function(user) { + UserSrv.revokeKey({ + userId: user.id + },{}, function() { + delete $scope.usrKey[user.id]; + }, function(response) { + NotificationSrv.error('AdminUsersCtrl', response.data, response.status); + }); }; - $scope.createKey = function(user) { - $scope.updateField(user, 'with-key', true); + + $scope.copyKey = function(user) { + clipboard.copyText($scope.usrKey[user.id]); + delete $scope.usrKey[user.id]; }; $scope.updateField = function(user, fieldName, newValue) { @@ -86,6 +106,28 @@ }); }; + $scope.copyPwd = function(password) { + clipboard.copyText(password); + }; + + $scope.showUserDialog = function(user) { + var modalInstance = $uibModal.open({ + templateUrl: 'views/partials/admin/user-dialog.html', + controller: 'AdminUserDialogCtrl', + controllerAs: '$vm', + size: 'lg', + resolve: { + user: angular.copy(user) || {} + } + }); + + modalInstance.result.then(function(data) { + //self.initCustomfields(); + //CustomFieldsCacheSrv.clearCache(); + //$scope.$emit('custom-fields:refresh'); + }); + } + }); })(); diff --git a/ui/app/scripts/services/UserSrv.js b/ui/app/scripts/services/UserSrv.js index 3e2296b0e2..e5ce4b6fb0 100644 --- a/ui/app/scripts/services/UserSrv.js +++ b/ui/app/scripts/services/UserSrv.js @@ -17,6 +17,18 @@ angular.module('theHiveServices') setPass: { method: 'POST', url: './api/user/:userId/password/set' + }, + // getKey: { + // method: 'GET', + // url: './api/user/:userId/key' + // }, + setKey: { + method: 'POST', + url: './api/user/:userId/key/renew' + }, + revokeKey: { + method: 'DELETE', + url: './api/user/:userId/key' } }); /** @@ -70,6 +82,17 @@ angular.module('theHiveServices') return defer.promise; }; + res.getKey = function(userId) { + var defer = $q.defer(); + + $http.get('./api/user/'+userId+'/key') + .then(function(response) { + defer.resolve(response.data); + }); + + return defer.promise; + }; + res.list = function(query) { var defer = $q.defer(); diff --git a/ui/app/views/partials/admin/user-dialog.html b/ui/app/views/partials/admin/user-dialog.html new file mode 100644 index 0000000000..9cc4f2a303 --- /dev/null +++ b/ui/app/views/partials/admin/user-dialog.html @@ -0,0 +1,75 @@ +
+ + + +
diff --git a/ui/app/views/partials/admin/users.html b/ui/app/views/partials/admin/users.html index 0eabd04488..c2480f64af 100644 --- a/ui/app/views/partials/admin/users.html +++ b/ui/app/views/partials/admin/users.html @@ -3,116 +3,84 @@

User management

-
-
-
-
- -
-
- -
-
- - API key -
-
- -
-
- Roles -
- - -
-
-
-
-
- Add user -
-
-
-
+ + + +
- +
- - - - - + + + + + + - - - - + + - From f36b141a469a8d009ec1d75967232128885fd31d Mon Sep 17 00:00:00 2001 From: Nabil Adouani Date: Thu, 7 Sep 2017 17:11:00 +0200 Subject: [PATCH 30/49] #263 Remoe copy password button --- ui/app/scripts/controllers/admin/AdminUsersCtrl.js | 4 ---- ui/app/views/partials/admin/users.html | 5 +---- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/ui/app/scripts/controllers/admin/AdminUsersCtrl.js b/ui/app/scripts/controllers/admin/AdminUsersCtrl.js index 1abda270b2..3c0a0e8182 100644 --- a/ui/app/scripts/controllers/admin/AdminUsersCtrl.js +++ b/ui/app/scripts/controllers/admin/AdminUsersCtrl.js @@ -106,10 +106,6 @@ }); }; - $scope.copyPwd = function(password) { - clipboard.copyText(password); - }; - $scope.showUserDialog = function(user) { var modalInstance = $uibModal.open({ templateUrl: 'views/partials/admin/user-dialog.html', diff --git a/ui/app/views/partials/admin/users.html b/ui/app/views/partials/admin/users.html index c2480f64af..db5108602c 100644 --- a/ui/app/views/partials/admin/users.html +++ b/ui/app/views/partials/admin/users.html @@ -38,10 +38,7 @@

User management

- - + From c3636091e70558b155c45f2f53f884e82095aa41 Mon Sep 17 00:00:00 2001 From: To-om Date: Fri, 8 Sep 2017 10:59:02 +0200 Subject: [PATCH 31/49] #275 Disable log4j2 to use slf4j --- build.sbt | 9 +++++++++ thehive-backend/app/services/TheHiveAuthSrv.scala | 7 ++----- thehive-backend/build.sbt | 2 +- thehive-cortex/build.sbt | 1 - thehive-metrics/build.sbt | 2 -- thehive-misp/build.sbt | 4 +--- 6 files changed, 13 insertions(+), 12 deletions(-) diff --git a/build.sbt b/build.sbt index 96d2bfee54..cf66919cfc 100644 --- a/build.sbt +++ b/build.sbt @@ -1,17 +1,21 @@ name := "TheHive" lazy val thehiveBackend = (project in file("thehive-backend")) + .enablePlugins(PlayScala) .settings(publish := {}) lazy val thehiveMetrics = (project in file("thehive-metrics")) + .enablePlugins(PlayScala) .dependsOn(thehiveBackend) .settings(publish := {}) lazy val thehiveMisp = (project in file("thehive-misp")) + .enablePlugins(PlayScala) .dependsOn(thehiveBackend) .settings(publish := {}) lazy val thehiveCortex = (project in file("thehive-cortex")) + .enablePlugins(PlayScala) .dependsOn(thehiveBackend) .settings(publish := {}) .settings(SbtScalariform.scalariformSettings: _*) @@ -26,6 +30,11 @@ lazy val thehive = (project in file(".")) .settings(PublishToBinTray.settings: _*) .settings(Release.settings: _*) + +// Redirect logs from ElasticSearch (which uses log4j2) to slf4j +libraryDependencies += "org.apache.logging.log4j" % "log4j-to-slf4j" % "2.9.0" +excludeDependencies += "org.apache.logging.log4j" % "log4j-core" + lazy val rpmPackageRelease = (project in file("package/rpm-release")) .enablePlugins(RpmPlugin) .settings( diff --git a/thehive-backend/app/services/TheHiveAuthSrv.scala b/thehive-backend/app/services/TheHiveAuthSrv.scala index dfc27cadc7..0273dcb545 100644 --- a/thehive-backend/app/services/TheHiveAuthSrv.scala +++ b/thehive-backend/app/services/TheHiveAuthSrv.scala @@ -3,14 +3,11 @@ package services import javax.inject.{ Inject, Singleton } import scala.collection.immutable -import scala.concurrent.{ ExecutionContext, Future } -import scala.util.{ Failure, Success } +import scala.concurrent.ExecutionContext -import play.api.mvc.RequestHeader import play.api.{ Configuration, Logger } -import org.elastic4play.AuthenticationError -import org.elastic4play.services.{ AuthContext, AuthSrv } +import org.elastic4play.services.AuthSrv import org.elastic4play.services.auth.MultiAuthSrv object TheHiveAuthSrv { diff --git a/thehive-backend/build.sbt b/thehive-backend/build.sbt index 7c0b5a551e..a2fd132475 100644 --- a/thehive-backend/build.sbt +++ b/thehive-backend/build.sbt @@ -12,4 +12,4 @@ libraryDependencies ++= Seq( Library.reflections ) -enablePlugins(PlayScala) +play.sbt.routes.RoutesKeys.routesImport -= "controllers.Assets.Asset" \ No newline at end of file diff --git a/thehive-cortex/build.sbt b/thehive-cortex/build.sbt index a0b122b542..e6dc20cc15 100644 --- a/thehive-cortex/build.sbt +++ b/thehive-cortex/build.sbt @@ -8,4 +8,3 @@ libraryDependencies ++= Seq( Library.zip4j ) -enablePlugins(PlayScala) diff --git a/thehive-metrics/build.sbt b/thehive-metrics/build.sbt index e696b189f4..445fca1da2 100644 --- a/thehive-metrics/build.sbt +++ b/thehive-metrics/build.sbt @@ -14,5 +14,3 @@ libraryDependencies ++= Seq( "io.dropwizard.metrics" % "metrics-ganglia" % "3.1.2", "info.ganglia.gmetric4j" % "gmetric4j" % "1.0.10" ) - -enablePlugins(PlayScala) diff --git a/thehive-misp/build.sbt b/thehive-misp/build.sbt index 59c831777f..969ef9384d 100644 --- a/thehive-misp/build.sbt +++ b/thehive-misp/build.sbt @@ -6,6 +6,4 @@ libraryDependencies ++= Seq( Library.Play.ahc, Library.zip4j, Library.elastic4play -) - -enablePlugins(PlayScala) +) \ No newline at end of file From 65bb93b71c497e93f62859333824a982381e2c6a Mon Sep 17 00:00:00 2001 From: Nabil Adouani Date: Fri, 8 Sep 2017 15:52:12 +0200 Subject: [PATCH 32/49] #304 update alert flow template --- ui/app/views/directives/flow/alert.html | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/ui/app/views/directives/flow/alert.html b/ui/app/views/directives/flow/alert.html index d7c477572e..daffaa1c24 100644 --- a/ui/app/views/directives/flow/alert.html +++ b/ui/app/views/directives/flow/alert.html @@ -2,7 +2,7 @@
Alert updates - {{base.object.title}} + [{{base.object.source}}] {{base.object.title}}
@@ -18,14 +18,31 @@
- +
{{k}}: {{v.length}}
+
+ {{k}}: +
+
+ {{k}}: + + {{tag}} + + + None + +
+
+ {{k}}: +
{{k}}: - {{v}} + {{v | limitTo: 250}}
From d0fe5bf5e6f559599ba875636ac3c811424dcf57 Mon Sep 17 00:00:00 2001 From: To-om Date: Fri, 8 Sep 2017 16:10:44 +0200 Subject: [PATCH 33/49] Rename authentication type by authentication provider Related to CERT-BDF/elastic4play#29 --- thehive-backend/app/services/TheHiveAuthSrv.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/thehive-backend/app/services/TheHiveAuthSrv.scala b/thehive-backend/app/services/TheHiveAuthSrv.scala index 0273dcb545..77f33cf868 100644 --- a/thehive-backend/app/services/TheHiveAuthSrv.scala +++ b/thehive-backend/app/services/TheHiveAuthSrv.scala @@ -32,7 +32,7 @@ class TheHiveAuthSrv @Inject() ( userSrv: UserSrv, override implicit val ec: ExecutionContext) extends MultiAuthSrv( TheHiveAuthSrv.getAuthSrv( - configuration.getOptional[Seq[String]]("auth.type").getOrElse(Seq("local")), + configuration.getDeprecated[Option[Seq[String]]]("auth.provider", "auth.type").getOrElse(Seq("local")), authModules), ec) { From cfef53ea24a7dcbd9a5e44cf038567125b0c6e7c Mon Sep 17 00:00:00 2001 From: To-om Date: Fri, 8 Sep 2017 16:13:27 +0200 Subject: [PATCH 34/49] #293 really hide stacktrace if connection to web hook fails --- thehive-backend/app/services/WebHook.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/thehive-backend/app/services/WebHook.scala b/thehive-backend/app/services/WebHook.scala index 3783197a3c..a96d2726f6 100644 --- a/thehive-backend/app/services/WebHook.scala +++ b/thehive-backend/app/services/WebHook.scala @@ -17,7 +17,7 @@ case class WebHook(name: String, ws: WSRequest)(implicit ec: ExecutionContext) { def send(obj: JsObject): Unit = ws.post(obj).onComplete { case Success(resp) if resp.status / 100 != 2 ⇒ logger.error(s"WebHook returns status ${resp.status} ${resp.statusText}") - case Failure(ce: ConnectException) ⇒ logger.error(s"Connection to WebHook $name error", ce) + case Failure(_: ConnectException) ⇒ logger.error(s"Connection to WebHook $name error") case Failure(error) ⇒ logger.error("WebHook call error", error) case _ ⇒ } From 7c443742bf9c3d79db6bb37333cc9aa0e142e23f Mon Sep 17 00:00:00 2001 From: To-om Date: Fri, 8 Sep 2017 17:08:36 +0200 Subject: [PATCH 35/49] Remove alert sourceRef from case title (except for MISP alert) --- thehive-backend/app/services/AlertSrv.scala | 2 +- thehive-misp/app/connectors/misp/MispSynchro.scala | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/thehive-backend/app/services/AlertSrv.scala b/thehive-backend/app/services/AlertSrv.scala index 2a90ab3384..1cf512019e 100644 --- a/thehive-backend/app/services/AlertSrv.scala +++ b/thehive-backend/app/services/AlertSrv.scala @@ -176,7 +176,7 @@ class AlertSrv( caseTemplate ← getCaseTemplate(alert, customCaseTemplate) caze ← caseSrv.create( Fields.empty - .set("title", s"#${alert.sourceRef()} " + alert.title()) + .set("title", alert.title()) .set("description", alert.description()) .set("severity", JsNumber(alert.severity())) .set("tags", JsArray(alert.tags().map(JsString))) diff --git a/thehive-misp/app/connectors/misp/MispSynchro.scala b/thehive-misp/app/connectors/misp/MispSynchro.scala index d9d4139774..96759499fd 100644 --- a/thehive-misp/app/connectors/misp/MispSynchro.scala +++ b/thehive-misp/app/connectors/misp/MispSynchro.scala @@ -143,6 +143,7 @@ class MispSynchro @Inject() ( case (event, None, attrs) ⇒ logger.info(s"MISP event ${event.source}:${event.sourceRef} has no related alert, create it with ${attrs.size} observable(s)") val alertJson = Json.toJson(event).as[JsObject] + + ("title" → JsString(s"#${event.sourceRef} ${event.title}")) + ("type" → JsString("misp")) + ("caseTemplate" → mispConnection.caseTemplate.fold[JsValue](JsNull)(JsString)) + ("artifacts" → Json.toJson(attrs)) From 78b4fc515ff01b498c0e79d08afa3cfd291eb034 Mon Sep 17 00:00:00 2001 From: Nabil Adouani Date: Fri, 8 Sep 2017 17:19:49 +0200 Subject: [PATCH 36/49] #52 Add a confirmation dialog before misp export operation --- .../scripts/controllers/case/CaseMainCtrl.js | 103 ++++++++++++++++-- .../partials/misp/case.export.confirm.html | 12 ++ .../{error-modal.html => error.dialog.html} | 6 +- 3 files changed, 108 insertions(+), 13 deletions(-) create mode 100644 ui/app/views/partials/misp/case.export.confirm.html rename ui/app/views/partials/misp/{error-modal.html => error.dialog.html} (66%) diff --git a/ui/app/scripts/controllers/case/CaseMainCtrl.js b/ui/app/scripts/controllers/case/CaseMainCtrl.js index 4fb028b5bc..d804e39531 100644 --- a/ui/app/scripts/controllers/case/CaseMainCtrl.js +++ b/ui/app/scripts/controllers/case/CaseMainCtrl.js @@ -198,17 +198,100 @@ }); }; + + var extractExportErrors = function (errors) { + var result = []; + + console.log(errors); + result = errors.map(function(item) { + return { + data: item.object.dataType === 'file' ? item.object.attachment.name : item.object.data, + message: item.message + }; + }); + + return result; + } + + var showExportErrors = function(errors) { + $uibModal.open({ + templateUrl: 'views/partials/misp/error.dialog.html', + controller: function(clipboard, $uibModalInstance, failures) { + this.failures = failures; + this.cancel = function() { + $uibModalInstance.dismiss(); + } + + this.copyToClipboard = function() { + clipboard.copyText(_.pluck(failures, 'data').join('\n')); + $uibModalInstance.dismiss(); + } + }, + controllerAs: 'dialog', + size: 'lg', + resolve: { + failures: function() { + return errors; + } + } + }) + } + $scope.shareCase = function() { - var mispConfig = $scope.appConfig.connectors.misp; - MispSrv.getServer(mispConfig) - .then(function(server) { - return MispSrv.export($scope.caseId, server); - }) - .then(function(response){ - console.log(response); - }, function(err) { - console.log(err); - }); + var modalInstance = $uibModal.open({ + templateUrl: 'views/partials/misp/case.export.confirm.html', + controller: function($uibModalInstance, data) { + this.caze = data; + this.cancel = function() { + $uibModalInstance.dismiss(); + } + + this.confirm = function() { + $uibModalInstance.close(); + } + }, + controllerAs: 'dialog', + resolve: { + data: function() { + return $scope.caze; + } + } + }); + + modalInstance.result.then(function() { + var mispConfig = $scope.appConfig.connectors.misp; + return MispSrv.getServer(mispConfig) + }).then(function(server) { + return MispSrv.export($scope.caseId, server); + }) + .then(function(response){ + var success = 0, + failure = 0; + + if (response.status === 207) { + success = response.data.success.length; + failure = response.data.failure.length; + + showExportErrors(extractExportErrors(response.data.failure)); + + NotificationSrv.log('The case has been successfully exported, but '+ failure +' observable(s) failed', 'warning'); + } else { + success = angular.isObject(response.data) ? 1 : response.data.length; + NotificationSrv.log('The case has been successfully exported with ' + success+ ' observable(s)', 'success'); + } + + }, function(err) { + if(!err) { + return; + } + + if (err.status === 400) { + showExportErrors(extractExportErrors(err.data)); + } else { + NotificationSrv.error('CaseExportCtrl', 'An unexpected error occurred while exporting case', response.status); + } + }); + }; /** diff --git a/ui/app/views/partials/misp/case.export.confirm.html b/ui/app/views/partials/misp/case.export.confirm.html new file mode 100644 index 0000000000..d1c9bb933d --- /dev/null +++ b/ui/app/views/partials/misp/case.export.confirm.html @@ -0,0 +1,12 @@ + + + diff --git a/ui/app/views/partials/misp/error-modal.html b/ui/app/views/partials/misp/error.dialog.html similarity index 66% rename from ui/app/views/partials/misp/error-modal.html rename to ui/app/views/partials/misp/error.dialog.html index 4794776206..97ecd3b9a9 100644 --- a/ui/app/views/partials/misp/error-modal.html +++ b/ui/app/views/partials/misp/error.dialog.html @@ -12,7 +12,7 @@
- + @@ -23,6 +23,6 @@ From a48e67b9dbdc952cd6baa45a70bf582e7ee8b901 Mon Sep 17 00:00:00 2001 From: Nabil Adouani Date: Fri, 8 Sep 2017 17:32:00 +0200 Subject: [PATCH 37/49] #52 Clean up --- ui/app/scripts/controllers/case/CaseMainCtrl.js | 1 - 1 file changed, 1 deletion(-) diff --git a/ui/app/scripts/controllers/case/CaseMainCtrl.js b/ui/app/scripts/controllers/case/CaseMainCtrl.js index d804e39531..5f4c34b80f 100644 --- a/ui/app/scripts/controllers/case/CaseMainCtrl.js +++ b/ui/app/scripts/controllers/case/CaseMainCtrl.js @@ -202,7 +202,6 @@ var extractExportErrors = function (errors) { var result = []; - console.log(errors); result = errors.map(function(item) { return { data: item.object.dataType === 'file' ? item.object.attachment.name : item.object.data, From 4e795d0cea832278c819fe52a5b8cc94f63975d6 Mon Sep 17 00:00:00 2001 From: To-om Date: Mon, 11 Sep 2017 11:07:43 +0200 Subject: [PATCH 38/49] #52 Don't export to MISP observables which are not IOC --- thehive-misp/app/connectors/misp/MispSrv.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/thehive-misp/app/connectors/misp/MispSrv.scala b/thehive-misp/app/connectors/misp/MispSrv.scala index 078d8336f2..5eba30ed0b 100644 --- a/thehive-misp/app/connectors/misp/MispSrv.scala +++ b/thehive-misp/app/connectors/misp/MispSrv.scala @@ -80,7 +80,7 @@ class MispSrv @Inject() ( def getAttributesFromCase(caze: Case): Future[Seq[ExportedMispAttribute]] = { import org.elastic4play.services.QueryDSL._ artifactSrv - .find(and(withParent(caze), "status" ~= "Ok"), Some("all"), Nil) + .find(and(withParent(caze), "status" ~= "Ok", "ioc" ~= true), Some("all"), Nil) ._1 .map { artifact ⇒ val (category, tpe) = fromArtifact(artifact.dataType(), artifact.data()) From 594e4f217e139bd941387b14caabeca8a5136f38 Mon Sep 17 00:00:00 2001 From: Nabil Adouani Date: Mon, 11 Sep 2017 16:17:44 +0200 Subject: [PATCH 39/49] #52 Refactor the MISP export dialog to merge confirmation and target selection --- ui/app/index.html | 1 + .../controllers/case/CaseExportDialogCtrl.js | 88 +++++++++++++++ .../scripts/controllers/case/CaseMainCtrl.js | 101 ++++-------------- ui/app/scripts/services/MispSrv.js | 29 ----- .../views/partials/case/case.panelinfo.html | 2 +- .../partials/misp/case.export.confirm.html | 44 +++++++- ui/app/views/partials/misp/error.dialog.html | 28 ----- 7 files changed, 150 insertions(+), 143 deletions(-) create mode 100644 ui/app/scripts/controllers/case/CaseExportDialogCtrl.js delete mode 100644 ui/app/views/partials/misp/error.dialog.html diff --git a/ui/app/index.html b/ui/app/index.html index b65392d5ff..74a0f6a452 100644 --- a/ui/app/index.html +++ b/ui/app/index.html @@ -148,6 +148,7 @@ + diff --git a/ui/app/scripts/controllers/case/CaseExportDialogCtrl.js b/ui/app/scripts/controllers/case/CaseExportDialogCtrl.js new file mode 100644 index 0000000000..eeaaa4c4e6 --- /dev/null +++ b/ui/app/scripts/controllers/case/CaseExportDialogCtrl.js @@ -0,0 +1,88 @@ +(function() { + 'use strict'; + + angular + .module('theHiveControllers') + .controller('CaseExportDialogCtrl', function(MispSrv, NotificationSrv, clipboard, $uibModalInstance, caze, config) { + var self = this; + + this.caze = caze; + this.mode = ''; + this.servers = config.servers; + this.failures = []; + + this.existingExports = {}; + this.loading = false; + + _.each(_.filter(this.caze.stats.alerts || [], function(item) { + return item.type === 'misp'; + }), function(item) { + self.existingExports[item.source] = true; + }); + + var extractExportErrors = function (errors) { + var result = []; + + result = errors.map(function(item) { + return { + data: item.object.dataType === 'file' ? item.object.attachment.name : item.object.data, + message: item.message + }; + }); + + return result; + } + + this.copyToClipboard = function() { + clipboard.copyText(_.pluck(self.failures, 'data').join('\n')); + $uibModalInstance.dismiss(); + } + + this.cancel = function() { + $uibModalInstance.dismiss(); + }; + + this.confirm = function() { + $uibModalInstance.close(); + }; + + this.export = function(server) { + self.loading = true; + self.failures = []; + + MispSrv.export(self.caze.id, server) + .then(function(response){ + var success = 0, + failure = 0; + + if (response.status === 207) { + success = response.data.success.length; + failure = response.data.failure.length; + + self.mode = 'error'; + self.failures = extractExportErrors(response.data.failure); + + NotificationSrv.log('The case has been successfully exported, but '+ failure +' observable(s) failed', 'warning'); + } else { + success = angular.isObject(response.data) ? 1 : response.data.length; + NotificationSrv.log('The case has been successfully exported with ' + success+ ' observable(s)', 'success'); + $uibModalInstance.close(); + } + self.loading = false; + + }, function(err) { + if(!err) { + return; + } + + if (err.status === 400) { + self.mode = 'error'; + self.failures = extractExportErrors(err.data); + } else { + NotificationSrv.error('CaseExportCtrl', 'An unexpected error occurred while exporting case', err.status); + } + self.loading = false; + }); + } + }); +})(); diff --git a/ui/app/scripts/controllers/case/CaseMainCtrl.js b/ui/app/scripts/controllers/case/CaseMainCtrl.js index 5f4c34b80f..dc97fff5a8 100644 --- a/ui/app/scripts/controllers/case/CaseMainCtrl.js +++ b/ui/app/scripts/controllers/case/CaseMainCtrl.js @@ -26,6 +26,13 @@ $scope.caze = caze; $rootScope.title = 'Case #' + caze.caseId + ': ' + caze.title; + $scope.initExports = function() { + $scope.existingExports = _.filter($scope.caze.stats.alerts || [], function(item) { + return item.type === 'misp'; + }).length; + }; + $scope.initExports(); + $scope.updateMetricsList = function() { MetricsCacheSrv.all().then(function(metrics) { $scope.allMetrics = _.omit(metrics, _.keys($scope.caze.metrics)); @@ -198,99 +205,31 @@ }); }; - - var extractExportErrors = function (errors) { - var result = []; - - result = errors.map(function(item) { - return { - data: item.object.dataType === 'file' ? item.object.attachment.name : item.object.data, - message: item.message - }; - }); - - return result; - } - - var showExportErrors = function(errors) { - $uibModal.open({ - templateUrl: 'views/partials/misp/error.dialog.html', - controller: function(clipboard, $uibModalInstance, failures) { - this.failures = failures; - this.cancel = function() { - $uibModalInstance.dismiss(); - } - - this.copyToClipboard = function() { - clipboard.copyText(_.pluck(failures, 'data').join('\n')); - $uibModalInstance.dismiss(); - } - }, - controllerAs: 'dialog', - size: 'lg', - resolve: { - failures: function() { - return errors; - } - } - }) - } - $scope.shareCase = function() { var modalInstance = $uibModal.open({ templateUrl: 'views/partials/misp/case.export.confirm.html', - controller: function($uibModalInstance, data) { - this.caze = data; - this.cancel = function() { - $uibModalInstance.dismiss(); - } - - this.confirm = function() { - $uibModalInstance.close(); - } - }, + controller: 'CaseExportDialogCtrl', controllerAs: 'dialog', + size: 'lg', resolve: { - data: function() { + caze: function() { return $scope.caze; + }, + config: function() { + return $scope.appConfig.connectors.misp; } } }); modalInstance.result.then(function() { - var mispConfig = $scope.appConfig.connectors.misp; - return MispSrv.getServer(mispConfig) - }).then(function(server) { - return MispSrv.export($scope.caseId, server); + return CaseSrv.get({ + 'caseId': $scope.caseId, + 'nstats': true + }).$promise; + }).then(function(data) { + $scope.caze = data.toJSON(); + $scope.initExports(); }) - .then(function(response){ - var success = 0, - failure = 0; - - if (response.status === 207) { - success = response.data.success.length; - failure = response.data.failure.length; - - showExportErrors(extractExportErrors(response.data.failure)); - - NotificationSrv.log('The case has been successfully exported, but '+ failure +' observable(s) failed', 'warning'); - } else { - success = angular.isObject(response.data) ? 1 : response.data.length; - NotificationSrv.log('The case has been successfully exported with ' + success+ ' observable(s)', 'success'); - } - - }, function(err) { - if(!err) { - return; - } - - if (err.status === 400) { - showExportErrors(extractExportErrors(err.data)); - } else { - NotificationSrv.error('CaseExportCtrl', 'An unexpected error occurred while exporting case', response.status); - } - }); - }; /** diff --git a/ui/app/scripts/services/MispSrv.js b/ui/app/scripts/services/MispSrv.js index d252cfcf0c..1779e99cc5 100644 --- a/ui/app/scripts/services/MispSrv.js +++ b/ui/app/scripts/services/MispSrv.js @@ -118,35 +118,6 @@ return defer.promise; }, - promptForInstance: function(servers) { - var modalInstance = $uibModal.open({ - templateUrl: 'views/partials/misp/choose-instance-dialog.html', - controller: 'ServerInstanceDialogCtrl', - controllerAs: 'vm', - size: '', - resolve: { - servers: function() { - return servers; - } - } - }); - - return modalInstance.result; - }, - - getServer: function(mispConfig) { - if(!mispConfig || !mispConfig.enabled || !mispConfig.servers) { - return $q.reject(); - } - - var servers = mispConfig.servers; - if (servers.length === 1) { - return $q.resolve(servers[0]); - } else { - return factory.promptForInstance(servers); - } - }, - export: function(caseId, server) { return $http.post(baseUrl + '/export/' + caseId + '/' + server, {}); } diff --git a/ui/app/views/partials/case/case.panelinfo.html b/ui/app/views/partials/case/case.panelinfo.html index 4b76d8e6e4..46d815771d 100644 --- a/ui/app/views/partials/case/case.panelinfo.html +++ b/ui/app/views/partials/case/case.panelinfo.html @@ -40,7 +40,7 @@

- Share + Share ({{existingExports}}) diff --git a/ui/app/views/partials/misp/case.export.confirm.html b/ui/app/views/partials/misp/case.export.confirm.html index d1c9bb933d..b971b586c7 100644 --- a/ui/app/views/partials/misp/case.export.confirm.html +++ b/ui/app/views/partials/misp/case.export.confirm.html @@ -2,11 +2,47 @@

LoginFull NameRolesPassword / API keyLockLoginFull NameRolesPasswordAPI keyLock
- {{user.id}} + + {{user.id}} + -
- - -
-
- -
+
+ + New password
- +
- - Create API Key - Show API Key - {{usrKey[user.id]}} +
+ + Create API Key + + +
+ + Renew + Revoke + Reveal + + + + + +
+ +
-
- - - - - -
+
+ + + + +
Reason
{{o.data | fang}} {{o.message}}
+ + + + + + +

{{server}}

+ +
+
+ +
+
+ Failed to export the following observables +
+ + + + + + + + + + + +
ObservableReason
{{o.data | fang}}{{o.message}}
+
diff --git a/ui/app/views/partials/misp/error.dialog.html b/ui/app/views/partials/misp/error.dialog.html deleted file mode 100644 index 97ecd3b9a9..0000000000 --- a/ui/app/views/partials/misp/error.dialog.html +++ /dev/null @@ -1,28 +0,0 @@ - - - - - From e6c2b9da39ad1c8b1fec9bb57064fb10279fdd73 Mon Sep 17 00:00:00 2001 From: Nabil Adouani Date: Mon, 11 Sep 2017 16:49:30 +0200 Subject: [PATCH 40/49] #263 Refine the user edit dialog --- .../controllers/admin/AdminUserDialogCtrl.js | 7 +++++-- ui/app/views/partials/admin/users.html | 20 +++++++++---------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/ui/app/scripts/controllers/admin/AdminUserDialogCtrl.js b/ui/app/scripts/controllers/admin/AdminUserDialogCtrl.js index 597ddb00ce..f4c234e0d2 100644 --- a/ui/app/scripts/controllers/admin/AdminUserDialogCtrl.js +++ b/ui/app/scripts/controllers/admin/AdminUserDialogCtrl.js @@ -7,13 +7,16 @@ self.user = user; self.isEdit = user.id; - self.formData = _.defaults(_.pick(self.user, 'id', 'name', 'roles'), { + var formData = _.defaults(_.pick(self.user, 'id', 'name', 'roles'), { id: null, name: null, roles: [], alert: false }); - self.formData.alert = self.formData.roles.indexOf('alert') !== -1; + formData.alert = formData.roles.indexOf('alert') !== -1; + formData.roles = _.without(formData.roles, 'alert'); + + self.formData = formData; var onSuccess = function(data) { $uibModalInstance.close(data); diff --git a/ui/app/views/partials/admin/users.html b/ui/app/views/partials/admin/users.html index db5108602c..547badb5c6 100644 --- a/ui/app/views/partials/admin/users.html +++ b/ui/app/views/partials/admin/users.html @@ -16,7 +16,7 @@

User management

Roles Password API key - Lock + Actions @@ -38,7 +38,7 @@

User management

- + @@ -54,7 +54,6 @@

User management

-
Renew @@ -71,13 +70,14 @@

User management

- - - - - - - + + + + From 8a0000576cbf31c8d8bf6be63f7c94ccc0115029 Mon Sep 17 00:00:00 2001 From: Nabil Adouani Date: Tue, 12 Sep 2017 16:01:30 +0200 Subject: [PATCH 41/49] Fix layout issues in alert list and dialog --- ui/app/views/partials/alert/event.dialog.html | 2 +- ui/app/views/partials/alert/list.html | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ui/app/views/partials/alert/event.dialog.html b/ui/app/views/partials/alert/event.dialog.html index 587256bdde..d3ba65256a 100644 --- a/ui/app/views/partials/alert/event.dialog.html +++ b/ui/app/views/partials/alert/event.dialog.html @@ -35,7 +35,7 @@

- None + None {{tag}}
diff --git a/ui/app/views/partials/alert/list.html b/ui/app/views/partials/alert/list.html index e083799f1e..6864d17e71 100644 --- a/ui/app/views/partials/alert/list.html +++ b/ui/app/views/partials/alert/list.html @@ -44,7 +44,7 @@

List of alerts ({{$vm.list.total || 0}} of {{alertEvents.c - Reference + Reference Type Status Title @@ -61,7 +61,7 @@

List of alerts ({{$vm.list.total || 0}} of {{alertEvents.c - + {{::event.sourceRef}} From c3fe6895dec6bb7b39cac3d2b4c3bd84cee5428a Mon Sep 17 00:00:00 2001 From: Nabil Adouani Date: Tue, 12 Sep 2017 17:20:02 +0200 Subject: [PATCH 42/49] #241 Prevent users from filtering alerts and cases by unavailable values --- .../controllers/alert/AlertListCtrl.js | 13 +++++- .../scripts/controllers/case/CaseListCtrl.js | 9 +++- ui/app/scripts/services/TagSrv.js | 41 ++++++++++++------- ui/app/views/partials/alert/list/filters.html | 15 ++++--- ui/app/views/partials/case/list/filters.html | 11 +++-- 5 files changed, 61 insertions(+), 28 deletions(-) diff --git a/ui/app/scripts/controllers/alert/AlertListCtrl.js b/ui/app/scripts/controllers/alert/AlertListCtrl.js index bb047aa2fa..843b956fbc 100644 --- a/ui/app/scripts/controllers/alert/AlertListCtrl.js +++ b/ui/app/scripts/controllers/alert/AlertListCtrl.js @@ -1,7 +1,7 @@ (function() { 'use strict'; angular.module('theHiveControllers') - .controller('AlertListCtrl', function($scope, $q, $state, $uibModal, TemplateSrv, AlertingSrv, NotificationSrv, FilteringSrv, Severity) { + .controller('AlertListCtrl', function($scope, $q, $state, $uibModal, TagSrv, TemplateSrv, AlertingSrv, NotificationSrv, FilteringSrv, Severity) { var self = this; self.list = []; @@ -93,6 +93,7 @@ self.searchForm = { searchQuery: self.filtering.buildQuery() || '' }; + self.lastSearch = null; $scope.$watch('$vm.list.pageSize', function (newValue) { self.filtering.setPageSize(newValue); @@ -275,7 +276,11 @@ this.applyFilters = function () { self.searchForm.searchQuery = self.filtering.buildQuery(); - self.search(); + + if(self.lastSearch !== self.searchForm.searchQuery) { + self.lastSearch = self.searchForm.searchQuery; + self.search(); + } }; this.clearFilters = function () { @@ -372,6 +377,10 @@ return AlertingSrv.sources(query); }; + this.getTags = function(query) { + return TagSrv.fromAlerts(query); + }; + self.load(); }); })(); diff --git a/ui/app/scripts/controllers/case/CaseListCtrl.js b/ui/app/scripts/controllers/case/CaseListCtrl.js index f1d46a3bfa..6c5c1879de 100644 --- a/ui/app/scripts/controllers/case/CaseListCtrl.js +++ b/ui/app/scripts/controllers/case/CaseListCtrl.js @@ -15,6 +15,7 @@ this.searchForm = { searchQuery: this.uiSrv.buildQuery() || '' }; + this.lastQuery = null; this.list = PSearchSrv(undefined, 'case', { scope: $scope, @@ -36,7 +37,6 @@ field: 'status' }); - $scope.$watch('$vm.list.pageSize', function (newValue) { self.uiSrv.setPageSize(newValue); }); @@ -55,7 +55,12 @@ this.applyFilters = function () { self.searchForm.searchQuery = self.uiSrv.buildQuery(); - self.search(); + + if(self.lastQuery !== self.searchForm.searchQuery) { + self.lastQuery = self.searchForm.searchQuery; + self.search(); + } + }; this.clearFilters = function () { diff --git a/ui/app/scripts/services/TagSrv.js b/ui/app/scripts/services/TagSrv.js index 2572c3097e..2d60d2959f 100644 --- a/ui/app/scripts/services/TagSrv.js +++ b/ui/app/scripts/services/TagSrv.js @@ -3,27 +3,40 @@ angular.module('theHiveServices') .service('TagSrv', function(StatSrv, $q) { - this.fromCases = function(query) { - var defer = $q.defer(); - - StatSrv.getPromise({ - objectType: 'case', + var getPromiseFor = function(objectType) { + return StatSrv.getPromise({ + objectType: objectType, field: 'tags', limit: 1000 - }).then(function(response) { - var tags = []; + }); + }; + + var mapTags = function(collection, term) { + return _.map(_.filter(_.keys(collection), function(tag) { + var regex = new RegExp(term, 'gi'); + return regex.test(tag); + }), function(tag) { + return {text: tag}; + }); + }; - tags = _.map(_.filter(_.keys(response.data), function(tag) { - var regex = new RegExp(query, 'gi'); - return regex.test(tag); - }), function(tag) { - return {text: tag}; - }); + var getTags = function(objectType, term) { + var defer = $q.defer(); - defer.resolve(tags); + getPromiseFor(objectType).then(function(response) { + defer.resolve(mapTags(response.data, term) || []); }); return defer.promise; + } + + + this.fromCases = function(term) { + return getTags('case', term); + }; + + this.fromAlerts = function(term) { + return getTags('alert', term); }; }); diff --git a/ui/app/views/partials/alert/list/filters.html b/ui/app/views/partials/alert/list/filters.html index 44a5bba987..b087c197d2 100644 --- a/ui/app/views/partials/alert/list/filters.html +++ b/ui/app/views/partials/alert/list/filters.html @@ -31,8 +31,9 @@

Filters

min-length="2" ng-model="$vm.filtering.activeFilters.status.value" placeholder="ex: New, Updated, Ignored, Imported" - replace-spaces-with-dashes="false"> - + replace-spaces-with-dashes="false" + add-from-autocomplete-only="true"> +

@@ -55,8 +56,9 @@

Filters

min-length="2" ng-model="$vm.filtering.activeFilters.source.value" placeholder="ex: CIRCL, OSINT" - replace-spaces-with-dashes="false"> - + replace-spaces-with-dashes="false" + add-from-autocomplete-only="true"> +
@@ -69,8 +71,9 @@

Filters

min-length="2" ng-model="$vm.filtering.activeFilters.severity.value" placeholder="ex: High, Medium, Low" - replace-spaces-with-dashes="false"> - + replace-spaces-with-dashes="false" + add-from-autocomplete-only="true"> + diff --git a/ui/app/views/partials/case/list/filters.html b/ui/app/views/partials/case/list/filters.html index 2751cc7cde..d1db14b3b1 100644 --- a/ui/app/views/partials/case/list/filters.html +++ b/ui/app/views/partials/case/list/filters.html @@ -31,8 +31,9 @@

Filters

min-length="2" ng-model="$vm.uiSrv.activeFilters.status.value" placeholder="ex: Open, Resolved" - replace-spaces-with-dashes="false"> - + replace-spaces-with-dashes="false" + add-from-autocomplete-only="true"> + @@ -56,6 +57,7 @@

Filters

ng-model="$vm.uiSrv.activeFilters.owner.value" placeholder="ex: Firstname Lastname" replace-spaces-with-dashes="false" + add-from-autocomplete-only="true" display-property="label"> @@ -70,8 +72,9 @@

Filters

min-length="2" ng-model="$vm.uiSrv.activeFilters.severity.value" placeholder="ex: High, Medium, Low" - replace-spaces-with-dashes="false"> - + replace-spaces-with-dashes="false" + add-from-autocomplete-only="true"> + From 3f512a2b7d53ded4e56d22327bc42f68fd5313cb Mon Sep 17 00:00:00 2001 From: To-om Date: Wed, 13 Sep 2017 08:37:13 +0200 Subject: [PATCH 43/49] #263 Fix deprecated setting (auth.type -> auth.provider) in default configuration file --- thehive-backend/conf/reference.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/thehive-backend/conf/reference.conf b/thehive-backend/conf/reference.conf index a31791c084..f316ca5782 100644 --- a/thehive-backend/conf/reference.conf +++ b/thehive-backend/conf/reference.conf @@ -56,7 +56,7 @@ auth { # services.LocalAuthSrv : passwords are stored in user entity (in ElasticSearch). No configuration are required. # ad : use ActiveDirectory to authenticate users. Configuration is under "auth.ad" key # ldap : use LDAP to authenticate users. Configuration is under "auth.ldap" key - type = [local] + provider = [local] ad { # Domain Windows name using DNS format. This parameter is required. From 88bef1907dc94832809df413622484716e2cec12 Mon Sep 17 00:00:00 2001 From: To-om Date: Wed, 13 Sep 2017 08:38:05 +0200 Subject: [PATCH 44/49] Bump version --- build.sbt | 2 +- ui/bower.json | 2 +- ui/package.json | 2 +- version.sbt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build.sbt b/build.sbt index cf66919cfc..0beff8c08d 100644 --- a/build.sbt +++ b/build.sbt @@ -122,7 +122,7 @@ packageBin := { } // DEB // linuxPackageMappings in Debian += packageMapping(file("LICENSE") -> "/usr/share/doc/thehive/copyright").withPerms("644") -version in Debian := version.value + "-1" +version in Debian := version.value + "-0" debianPackageRecommends := Seq("elasticsearch") debianPackageDependencies += "openjdk-8-jre-headless" maintainerScripts in Debian := maintainerScriptsFromDirectory( diff --git a/ui/bower.json b/ui/bower.json index f5b2636fcb..747d0c8017 100644 --- a/ui/bower.json +++ b/ui/bower.json @@ -1,6 +1,6 @@ { "name": "thehive", - "version": "2.12.1", + "version": "2.13.0", "license": "AGPL-3.0", "dependencies": { "angular": "1.5.8", diff --git a/ui/package.json b/ui/package.json index f32f835bb9..5d500c67fc 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,6 +1,6 @@ { "name": "thehive", - "version": "2.12.1", + "version": "2.13.0", "license": "AGPL-3.0", "repository": { "type": "git", diff --git a/version.sbt b/version.sbt index e0fc553286..b6764391ec 100644 --- a/version.sbt +++ b/version.sbt @@ -1 +1 @@ -version in ThisBuild := "2.12.1" +version in ThisBuild := "2.13.0" From 89413ff74a9bdc8d9da0f0f42d18e8128bef392f Mon Sep 17 00:00:00 2001 From: To-om Date: Wed, 13 Sep 2017 17:45:25 +0200 Subject: [PATCH 45/49] #52 Prefix alert title with MISP event id only once --- thehive-misp/app/connectors/misp/MispSynchro.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/thehive-misp/app/connectors/misp/MispSynchro.scala b/thehive-misp/app/connectors/misp/MispSynchro.scala index 96759499fd..d9d4139774 100644 --- a/thehive-misp/app/connectors/misp/MispSynchro.scala +++ b/thehive-misp/app/connectors/misp/MispSynchro.scala @@ -143,7 +143,6 @@ class MispSynchro @Inject() ( case (event, None, attrs) ⇒ logger.info(s"MISP event ${event.source}:${event.sourceRef} has no related alert, create it with ${attrs.size} observable(s)") val alertJson = Json.toJson(event).as[JsObject] + - ("title" → JsString(s"#${event.sourceRef} ${event.title}")) + ("type" → JsString("misp")) + ("caseTemplate" → mispConnection.caseTemplate.fold[JsValue](JsNull)(JsString)) + ("artifacts" → Json.toJson(attrs)) From b92908d1435d2cb1d83603e660c5574cf12646c7 Mon Sep 17 00:00:00 2001 From: To-om Date: Fri, 15 Sep 2017 14:33:45 +0200 Subject: [PATCH 46/49] #263 Fix deprecated setting (auth.type -> auth.provider) in docker entrypoint --- package/docker/entrypoint | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/docker/entrypoint b/package/docker/entrypoint index 07db120a82..8f3ca03306 100755 --- a/package/docker/entrypoint +++ b/package/docker/entrypoint @@ -59,7 +59,7 @@ then SECRET=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 64 | head -n 1) fi echo Using secret: $SECRET - echo play.crypto.secret=\"$SECRET\" >> $CONFIG_FILE + echo play.http.secret.key=\"$SECRET\" >> $CONFIG_FILE fi if test $CONFIG_ES = 1 From 11426331919736c9f09589d3d6e683c68e8a3547 Mon Sep 17 00:00:00 2001 From: To-om Date: Fri, 15 Sep 2017 14:55:36 +0200 Subject: [PATCH 47/49] #307 Fix file ownership to run docker image in Openshift --- build.sbt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index 0beff8c08d..37a774d9df 100644 --- a/build.sbt +++ b/build.sbt @@ -172,12 +172,18 @@ mappings in Docker ~= (_.filterNot { case (_, filepath) => filepath == "/opt/thehive/conf/application.conf" }) dockerCommands ~= { dc => - val (dockerInitCmds, dockerTailCmds) = dc.splitAt(4) + val (dockerInitCmds, dockerTailCmds) = dc + .collect { + case ExecCmd("RUN", "chown", _*) => ExecCmd("RUN", "chown", "-R", "daemon:root", ".") + case other => other + } + .splitAt(4) dockerInitCmds ++ Seq( Cmd("ADD", "var", "/var"), Cmd("ADD", "etc", "/etc"), - ExecCmd("RUN", "chown", "-R", "daemon:daemon", "/var/log/thehive")) ++ + ExecCmd("RUN", "chown", "-R", "daemon:root", "/var/log/thehive"), + ExecCmd("RUN", "chmod", "+x", "/opt/thehive/bin/thehive", "/opt/thehive/entrypoint")) ++ dockerTailCmds } From 94b31d31f23c30b02ed04d040685043c57bbf02e Mon Sep 17 00:00:00 2001 From: To-om Date: Fri, 15 Sep 2017 15:03:25 +0200 Subject: [PATCH 48/49] Set version and add changelog --- CHANGELOG.md | 31 +++++++++++++++++++++++++++++-- build.sbt | 2 +- project/Dependencies.scala | 2 +- 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cbce21785e..382d661da3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,34 @@ # Change Log -## [2.12.1](https://github.com/CERT-BDF/TheHive/tree/2.12.1) (2017-08-01) +## [2.13](https://github.com/CERT-BDF/TheHive/tree/2.13) (2017-09-15) + +[Full Changelog](https://github.com/CERT-BDF/TheHive/compare/2.12.1...2.13) + +**Implemented enhancements:** + +- Group ownership in Docker image prevents running on OpenShift [\#307](https://github.com/CERT-BDF/TheHive/issues/307) +- Improve the content of alert flow items [\#304](https://github.com/CERT-BDF/TheHive/issues/304) +- Add a basic support for webhooks [\#293](https://github.com/CERT-BDF/TheHive/issues/293) +- Add basic authentication to Stream API [\#291](https://github.com/CERT-BDF/TheHive/issues/291) +- Add Support for Play 2.6.x and Elasticsearch 5.x [\#275](https://github.com/CERT-BDF/TheHive/issues/275) +- Fine grained user permissions for API access [\#263](https://github.com/CERT-BDF/TheHive/issues/263) +- Alert Pane: Catch Incorrect Keywords [\#241](https://github.com/CERT-BDF/TheHive/issues/241) +- Specify multiple AD servers in TheHive configuration [\#231](https://github.com/CERT-BDF/TheHive/issues/231) +- Export cases in MISP events [\#52](https://github.com/CERT-BDF/TheHive/issues/52) + +**Fixed bugs:** +- Download attachment with non-latin filename [\#302](https://github.com/CERT-BDF/TheHive/issues/302) +- Undefined threat level from MISP events becomes severity "4" [\#300](https://github.com/CERT-BDF/TheHive/issues/300) +- File name is not displayed in observable conflict dialog [\#295](https://github.com/CERT-BDF/TheHive/issues/295) +- A colon punctuation mark in a search query results in 500 [\#285](https://github.com/CERT-BDF/TheHive/issues/285) +- Previewing alerts fails with "too many substreams open" due to case similarity process [\#280](https://github.com/CERT-BDF/TheHive/issues/280) + +**Closed issues:** + +- Threat level/severity code inverted between The Hive and MISP [\#292](https://github.com/CERT-BDF/TheHive/issues/292) + +## [2.12.1](https://github.com/CERT-BDF/TheHive/tree/2.12.1) (2017-08-01) [Full Changelog](https://github.com/CERT-BDF/TheHive/compare/2.12.0...2.12.1) **Implemented enhancements:** @@ -11,12 +38,12 @@ **Fixed bugs:** +- Cortex Connector Not Found [\#256](https://github.com/CERT-BDF/TheHive/issues/256) - Case similarity reports merged cases [\#272](https://github.com/CERT-BDF/TheHive/issues/272) - Closing a case with an open task does not dismiss task in "My tasks" [\#269](https://github.com/CERT-BDF/TheHive/issues/269) - API: cannot create alert if one alert artifact contains the IOC field set [\#268](https://github.com/CERT-BDF/TheHive/issues/268) - Can't get logs of a task via API [\#259](https://github.com/CERT-BDF/TheHive/issues/259) - Add multiple attachments in a single task log doesn't work [\#257](https://github.com/CERT-BDF/TheHive/issues/257) -- Cortex Connector Not Found [\#256](https://github.com/CERT-BDF/TheHive/issues/256) - TheHive doesn't send the file name to Cortex [\#254](https://github.com/CERT-BDF/TheHive/issues/254) - Renaming of users does not work [\#249](https://github.com/CERT-BDF/TheHive/issues/249) diff --git a/build.sbt b/build.sbt index 37a774d9df..476fe57fa4 100644 --- a/build.sbt +++ b/build.sbt @@ -122,7 +122,7 @@ packageBin := { } // DEB // linuxPackageMappings in Debian += packageMapping(file("LICENSE") -> "/usr/share/doc/thehive/copyright").withPerms("644") -version in Debian := version.value + "-0" +version in Debian := version.value + "-1" debianPackageRecommends := Seq("elasticsearch") debianPackageDependencies += "openjdk-8-jre-headless" maintainerScripts in Debian := maintainerScriptsFromDirectory( diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 3a615b1dcc..92cf4717af 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -32,6 +32,6 @@ object Dependencies { val reflections = "org.reflections" % "reflections" % "0.9.11" val zip4j = "net.lingala.zip4j" % "zip4j" % "1.3.2" val akkaTest = "com.typesafe.akka" %% "akka-stream-testkit" % "2.5.4" - val elastic4play = "org.cert-bdf" %% "elastic4play" % "1.3-SNAPSHOT" + val elastic4play = "org.cert-bdf" %% "elastic4play" % "1.3.0" } } From 34a8e60f2dd39df2fec60e7e9eda9aa8c25bb941 Mon Sep 17 00:00:00 2001 From: To-om Date: Fri, 15 Sep 2017 15:23:49 +0200 Subject: [PATCH 49/49] Fix rpmVendor context --- build.sbt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build.sbt b/build.sbt index 476fe57fa4..1d05af0bde 100644 --- a/build.sbt +++ b/build.sbt @@ -42,7 +42,7 @@ lazy val rpmPackageRelease = (project in file("package/rpm-release")) maintainer := "TheHive Project ", version := "1.0.0", rpmRelease := "3", - rpmVendor in Rpm := "TheHive Project", + rpmVendor := "TheHive Project", rpmUrl := Some("http://thehive-project.org/"), rpmLicense := Some("AGPL"), maintainerScripts in Rpm := Map.empty, @@ -134,7 +134,7 @@ linuxMakeStartScript in Debian := None // RPM // rpmRelease := "1" -rpmVendor in Rpm := "TheHive Project" +rpmVendor := "TheHive Project" rpmUrl := Some("http://thehive-project.org/") rpmLicense := Some("AGPL") rpmRequirements += "java-1.8.0-openjdk-headless" @@ -180,7 +180,7 @@ dockerCommands ~= { dc => .splitAt(4) dockerInitCmds ++ Seq( - Cmd("ADD", "var", "/var"), + Cmd("ADD", "var", "/var"), Cmd("ADD", "etc", "/etc"), ExecCmd("RUN", "chown", "-R", "daemon:root", "/var/log/thehive"), ExecCmd("RUN", "chmod", "+x", "/opt/thehive/bin/thehive", "/opt/thehive/entrypoint")) ++