diff --git a/thehive-backend/app/controllers/CaseReportCtrl.scala b/thehive-backend/app/controllers/CaseReportCtrl.scala new file mode 100644 index 0000000000..60320f4f99 --- /dev/null +++ b/thehive-backend/app/controllers/CaseReportCtrl.scala @@ -0,0 +1,42 @@ +package controllers + +import javax.inject.{ Inject, Singleton } +import org.elastic4play.Timed +import org.elastic4play.controllers.{ Authenticated, Fields, FieldsBodyParser, Renderer } +import org.elastic4play.services.{ AuxSrv, QueryDSL } +import play.api.http.Status +import play.api.mvc._ +import models.Roles +import services.CaseReportSrv + +import scala.concurrent.ExecutionContext + +@Singleton +class CaseReportCtrl @Inject() ( + auxSrv: AuxSrv, + authenticated: Authenticated, + renderer: Renderer, + components: ControllerComponents, + fieldsBodyParser: FieldsBodyParser, + caseReportSrv: CaseReportSrv, + implicit val ec: ExecutionContext) extends AbstractController(components) with Status { + + def create(): Action[Fields] = authenticated(Roles.admin).async(fieldsBodyParser) { implicit request ⇒ + caseReportSrv.create(request.body).map(caseReport ⇒ renderer.toOutput(OK, caseReport.toJson)) + } + + @Timed + def update(caseId: String): Action[Fields] = authenticated(Roles.admin).async(fieldsBodyParser) { implicit request ⇒ + caseReportSrv.update(caseId, request.body).map(template ⇒ renderer.toOutput(OK, template.toJson)) + } + + @Timed + def find(): Action[Fields] = authenticated(Roles.read).async(fieldsBodyParser) { implicit request ⇒ + val query = QueryDSL.any + val range = request.body.getString("range") + val sort = request.body.getStrings("sort").getOrElse(Nil) + + val (reports, total) = caseReportSrv.find(query, range, sort) + renderer.toOutput(OK, reports.map(_.toJson), total) + } +} \ No newline at end of file diff --git a/thehive-backend/app/models/CaseReport.scala b/thehive-backend/app/models/CaseReport.scala new file mode 100644 index 0000000000..ed68d6f0c4 --- /dev/null +++ b/thehive-backend/app/models/CaseReport.scala @@ -0,0 +1,16 @@ +package models + +import play.api.libs.json.JsObject + +import org.elastic4play.models.{ AttributeDef, EntityDef, ModelDef, AttributeFormat ⇒ F } + +trait CaseReportAttributes { _: AttributeDef ⇒ + val templateName: A[String] = attribute("name", F.stringFmt, "Name of the template") + val content: A[String] = attribute("content", F.textFmt, "Content of the template") +} + +class CaseReportModel extends ModelDef[CaseReportModel, CaseReport]("case_report", "case_report", "/case_report") with CaseReportAttributes + +class CaseReport(model: CaseReportModel, attributes: JsObject) + extends EntityDef[CaseReportModel, CaseReport](model, attributes) + with CaseReportAttributes \ No newline at end of file diff --git a/thehive-backend/app/models/Migration.scala b/thehive-backend/app/models/Migration.scala index 6f35c01d8f..dd0c33edf9 100644 --- a/thehive-backend/app/models/Migration.scala +++ b/thehive-backend/app/models/Migration.scala @@ -16,7 +16,7 @@ import play.api.{ Configuration, Environment, Logger } import akka.NotUsed import akka.stream.Materializer import akka.stream.scaladsl.Source -import services.{ AlertSrv, DashboardSrv } +import services.{ AlertSrv, DashboardSrv, CaseReportSrv } import org.elastic4play.ConflictError import org.elastic4play.controllers.Fields @@ -35,6 +35,7 @@ class Migration( dblists: DBLists, eventSrv: EventSrv, dashboardSrv: DashboardSrv, + caseReportSrv: CaseReportSrv, userSrv: UserSrv, environment: Environment, implicit val ec: ExecutionContext, @@ -44,6 +45,7 @@ class Migration( dblists: DBLists, eventSrv: EventSrv, dashboardSrv: DashboardSrv, + caseReportSrv: CaseReportSrv, userSrv: UserSrv, environment: Environment, ec: ExecutionContext, @@ -56,6 +58,7 @@ class Migration( dblists, eventSrv, dashboardSrv, + caseReportSrv, userSrv, environment, ec, materializer) @@ -110,11 +113,19 @@ class Migration( } } + private def createDefaultReport(): Future[CaseReport] = { + userSrv.inInitAuthContext { implicit authContext ⇒ + caseReportSrv.create(Fields(Json.obj("name" -> "Default", "content" -> ""))) + } + } + override def endMigration(version: Int): Future[Unit] = { if (requireUpdateMispAlertArtifact) { logger.info("Retrieve MISP attribute to update alerts") eventSrv.publish(UpdateMispAlertArtifact()) } + logger.info("Creating the base case report") + createDefaultReport() logger.info("Updating observable data type list") addDataTypes(Seq("filename", "fqdn", "url", "user-agent", "domain", "ip", "mail_subject", "hash", "mail", "registry", "uri_path", "regexp", "other", "file", "autonomous-system")) @@ -341,6 +352,9 @@ class Migration( addAttribute("alert", "customFields" → JsObject.empty), addAttribute("case_task", "group" → JsString("default")), addAttribute("case", "pap" → JsNumber(2))) + + case DatabaseState(14) ⇒ + Seq() } private def generateAlertId(alert: JsObject): String = { diff --git a/thehive-backend/app/models/package.scala b/thehive-backend/app/models/package.scala index 9447b30217..1a279150fc 100644 --- a/thehive-backend/app/models/package.scala +++ b/thehive-backend/app/models/package.scala @@ -1,5 +1,5 @@ package object models { - val modelVersion = 14 + val modelVersion = 15 } \ No newline at end of file diff --git a/thehive-backend/app/services/CaseReportSrv.scala b/thehive-backend/app/services/CaseReportSrv.scala new file mode 100644 index 0000000000..51fd41e154 --- /dev/null +++ b/thehive-backend/app/services/CaseReportSrv.scala @@ -0,0 +1,43 @@ +package services + +import akka.NotUsed +import akka.stream.Materializer +import akka.stream.scaladsl.Source +import javax.inject.{ Inject, Singleton } +import models.{ CaseReportModel, CaseReport } +import org.elastic4play.controllers.Fields +import org.elastic4play.database.ModifyConfig +import org.elastic4play.services._ + +import scala.concurrent.{ ExecutionContext, Future } + +@Singleton +class CaseReportSrv @Inject() ( + caseReportModel: CaseReportModel, + createSrv: CreateSrv, + getSrv: GetSrv, + updateSrv: UpdateSrv, + deleteSrv: DeleteSrv, + findSrv: FindSrv, + implicit val ec: ExecutionContext, + implicit val mat: Materializer) { + + def create(fields: Fields)(implicit authContext: AuthContext): Future[CaseReport] = + createSrv[CaseReportModel, CaseReport](caseReportModel, fields) + + def get(id: String): Future[CaseReport] = + getSrv[CaseReportModel, CaseReport](caseReportModel, id) + + def update(id: String, fields: Fields)(implicit authContext: AuthContext): Future[CaseReport] = + update(id, fields, ModifyConfig.default) + + def update(id: String, fields: Fields, modifyConfig: ModifyConfig)(implicit authContext: AuthContext): Future[CaseReport] = + updateSrv[CaseReportModel, CaseReport](caseReportModel, id, fields, modifyConfig) + + def delete(id: String)(implicit authContext: AuthContext): Future[Unit] = + deleteSrv.realDelete[CaseReportModel, CaseReport](caseReportModel, id) + + def find(queryDef: QueryDef, range: Option[String], sortBy: Seq[String]): (Source[CaseReport, NotUsed], Future[Long]) = { + findSrv[CaseReportModel, CaseReport](caseReportModel, queryDef, range, sortBy) + } +} \ No newline at end of file diff --git a/thehive-backend/conf/routes b/thehive-backend/conf/routes index c6a152b2da..93e67692ba 100644 --- a/thehive-backend/conf/routes +++ b/thehive-backend/conf/routes @@ -24,6 +24,9 @@ DELETE /api/case/:caseId/force controllers.CaseCtrl.realDelet GET /api/case/:caseId/links controllers.CaseCtrl.linkedCases(caseId) POST /api/case/:caseId1/_merge/:caseId2 controllers.CaseCtrl.merge(caseId1, caseId2) +GET /api/case_report controllers.CaseReportCtrl.find() +PATCH /api/case_report/:templateId controllers.CaseReportCtrl.update(templateId) + POST /api/case/template/_search controllers.CaseTemplateCtrl.find() POST /api/case/template controllers.CaseTemplateCtrl.create() GET /api/case/template/:caseTemplateId controllers.CaseTemplateCtrl.get(caseTemplateId) diff --git a/ui/app/index.html b/ui/app/index.html index 0ffd85386e..ec9c98501c 100644 --- a/ui/app/index.html +++ b/ui/app/index.html @@ -112,6 +112,7 @@ + @@ -139,6 +140,7 @@ + @@ -161,6 +163,7 @@ + @@ -235,6 +238,7 @@ + diff --git a/ui/app/scripts/app.js b/ui/app/scripts/app.js index d496cd8d65..4f366d6085 100644 --- a/ui/app/scripts/app.js +++ b/ui/app/scripts/app.js @@ -215,6 +215,13 @@ angular.module('thehive', ['ngAnimate', 'ngMessages', 'ngSanitize', 'ui.bootstra controller: 'AdminObservablesCtrl', title: 'Observable administration' }) + .state('app.administration.case-report-templates',{ + url: '/case-report-templates', + templateUrl: 'views/partials/admin/case-report-templates.html', + controller: 'AdminCaseReportCtrl', + controllerAs: 'cr', + title: 'Case report template administration' + }) .state('app.case', { abstract: true, url: 'case/{caseId}', diff --git a/ui/app/scripts/controllers/admin/AdminCaseReportCtrl.js b/ui/app/scripts/controllers/admin/AdminCaseReportCtrl.js new file mode 100644 index 0000000000..084b561937 --- /dev/null +++ b/ui/app/scripts/controllers/admin/AdminCaseReportCtrl.js @@ -0,0 +1,48 @@ +(function () { + 'use strict'; + + angular.module('theHiveControllers').controller('AdminCaseReportCtrl', + function AdminCaseReportCtrl($scope, $q, CaseReportSrv, NotificationSrv){ + var self = this; + self.templates = [] + self.formData = {} + self.editorOptions = { + useWrapMode: true, + showGutter: true + }; + + this.load = function() { + $q.all([ + CaseReportSrv.list(), + ]).then(function (response) { + self.templates = response[0]; + self.formData.content = self.templates[0].content + self.formData.name = self.templates[0].name + self.formData.template_id = self.templates[0].id + return $q.resolve(self.templates); + }, function(rejection) { + NotificationSrv.error('CaseReport', rejection.data, rejection.status); + }) + }; + this.load(); + + $scope.updateReport = function(){ + $q.all([ + CaseReportSrv.update(self.formData.template_id, { + 'name': self.formData.name, + 'content': self.formData.content + }) + ]).then(function (response){ + var res = response[0] + if (res.status === 200){ + NotificationSrv.log('Template updated successfully', 'success'); + } + else{ + NotificationSrv.error('CaseReport', res.data, res.status) + } + }, function(rejection){ + NotificationSrv.error('CaseReport', rejection.data, rejection.status) + }) + }; + }); +})(); diff --git a/ui/app/scripts/controllers/case/CaseMainCtrl.js b/ui/app/scripts/controllers/case/CaseMainCtrl.js index 425994cc22..9aa51e64b9 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, MispSrv, StreamSrv, StreamStatSrv, NotificationSrv, UtilsSrv, CaseResolutionStatus, CaseImpactStatus, CortexSrv, caze) { + function($scope, $rootScope, $state, $stateParams, $q, $uibModal, CaseTabsSrv, CaseSrv, MetricsCacheSrv, UserInfoSrv, MispSrv, StreamSrv, StreamStatSrv, NotificationSrv, UtilsSrv, CaseResolutionStatus, CaseImpactStatus, CortexSrv, CaseReportSrv, caze) { $scope.CaseResolutionStatus = CaseResolutionStatus; $scope.CaseImpactStatus = CaseImpactStatus; $scope.caseResponders = null; @@ -343,6 +343,20 @@ }); }; + $scope.caseReport = function() { + $uibModal.open({ + scope: $scope, + controller: 'CaseReportModalCtrl', + templateUrl: 'views/partials/case/case.report.html', + size: 'lg', + resolve: { + caze: function() { + return $scope.caze; + }, + } + }); + }; + /** * A workaround filter to make sure the ngRepeat doesn't order the * object keys diff --git a/ui/app/scripts/controllers/case/CaseReportModalCtrl.js b/ui/app/scripts/controllers/case/CaseReportModalCtrl.js new file mode 100644 index 0000000000..415221ebd8 --- /dev/null +++ b/ui/app/scripts/controllers/case/CaseReportModalCtrl.js @@ -0,0 +1,53 @@ +(function () { + 'use strict'; + + angular.module('theHiveControllers') + .controller('CaseReportModalCtrl', CaseReportModalCtrl); + + function CaseReportModalCtrl($scope, $state, $uibModalInstance, $q, $compile, PSearchSrv,SearchSrv, CaseSrv, UserInfoSrv, NotificationSrv, caze, $http, CaseReportSrv) { + var self = this; + self.templates = []; + self.artifacts = []; + + $q.all([ + CaseReportSrv.list(), + PSearchSrv($scope.caseId, 'case_artifact', { + scope: $scope, + baseFilter: { + '_and': [{ + '_parent': { + "_type": "case", + "_query": { + "_id": $scope.caseId + } + } + },   { + 'status': 'Ok' + }] + }, + loadAll: true, + sort: '-startDate', + nstats: true + }), + ]).then(function (response) { + self.templates = response[0]; + caze.artifacts = response[1]; + console.log(caze.artifacts); + var template = self.templates[0].content; + $('#case-report-content').html($compile(template)($scope)); + return $q.resolve(self.template); + }, function(rejection) { + NotificationSrv.error('CaseReport', rejection.data, rejection.status); + }); + + $scope.createPDF = function(){ + var pdf = new jsPDF('p', 'pt', 'letter'); + pdf.fromHTML($('#case-report-content').get(0), 5, 5); + pdf.save(caze.title + '.pdf'); + } + + $scope.cancel = function () { + $uibModalInstance.dismiss(); + }; + } +})(); diff --git a/ui/app/scripts/services/CaseReportSrv.js b/ui/app/scripts/services/CaseReportSrv.js new file mode 100644 index 0000000000..a9c924b266 --- /dev/null +++ b/ui/app/scripts/services/CaseReportSrv.js @@ -0,0 +1,23 @@ +(function() { + 'use strict'; + angular.module('theHiveServices').service('CaseReportSrv', function($q, $http) { + this.list = function() { + var defer = $q.defer(); + $http.get('./api/case_report') + .then(function(response) { + defer.resolve(response.data); + }, function(err) { + defer.reject(err); + }); + return defer.promise; + }; + + this.create = function(template) { + return $http.post('./api/case_report/create', template); + } + + this.update = function(id, template) { + return $http.patch('./api/case_report/' + id, template); + } + }); +})(); diff --git a/ui/app/views/components/header.component.html b/ui/app/views/components/header.component.html index c9c66d4814..93bb6a9915 100644 --- a/ui/app/views/components/header.component.html +++ b/ui/app/views/components/header.component.html @@ -125,6 +125,12 @@ Report templates +
  • + + + Case report templates + +
  • diff --git a/ui/app/views/partials/admin/case-report-templates.html b/ui/app/views/partials/admin/case-report-templates.html new file mode 100644 index 0000000000..9c354e4ea4 --- /dev/null +++ b/ui/app/views/partials/admin/case-report-templates.html @@ -0,0 +1,31 @@ +
    +
    +

    Case report template management

    +
    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    +
    +
    Case report has not been created yet.
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    diff --git a/ui/app/views/partials/case/case.panelinfo.html b/ui/app/views/partials/case/case.panelinfo.html index 991c3a98f8..1ac38f1efe 100644 --- a/ui/app/views/partials/case/case.panelinfo.html +++ b/ui/app/views/partials/case/case.panelinfo.html @@ -61,8 +61,16 @@

    + + + + Report + + + | + Share ({{existingExports}}) @@ -107,7 +115,6 @@

    Reopen - diff --git a/ui/app/views/partials/case/case.report.html b/ui/app/views/partials/case/case.report.html new file mode 100644 index 0000000000..0f00567ffe --- /dev/null +++ b/ui/app/views/partials/case/case.report.html @@ -0,0 +1,14 @@ + + diff --git a/ui/bower.json b/ui/bower.json index 02944d5f43..2063f33fd0 100644 --- a/ui/bower.json +++ b/ui/bower.json @@ -51,6 +51,7 @@ "underscore.string": "^3.3.4", "angular-drag-and-drop-lists": "^2.1.0", "angular-bootstrap-colorpicker": "^3.0.31", + "jspdf": "^1.4.1", "file-saver": "1.3.4", "js-url": "^2.5.2" },