Skip to content

Add the ability to create and export reports as PDF #834

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 18 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions thehive-backend/app/controllers/CaseReportCtrl.scala
Original file line number Diff line number Diff line change
@@ -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)
}
}
16 changes: 16 additions & 0 deletions thehive-backend/app/models/CaseReport.scala
Original file line number Diff line number Diff line change
@@ -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
16 changes: 15 additions & 1 deletion thehive-backend/app/models/Migration.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -35,6 +35,7 @@ class Migration(
dblists: DBLists,
eventSrv: EventSrv,
dashboardSrv: DashboardSrv,
caseReportSrv: CaseReportSrv,
userSrv: UserSrv,
environment: Environment,
implicit val ec: ExecutionContext,
Expand All @@ -44,6 +45,7 @@ class Migration(
dblists: DBLists,
eventSrv: EventSrv,
dashboardSrv: DashboardSrv,
caseReportSrv: CaseReportSrv,
userSrv: UserSrv,
environment: Environment,
ec: ExecutionContext,
Expand All @@ -56,6 +58,7 @@ class Migration(
dblists,
eventSrv,
dashboardSrv,
caseReportSrv,
userSrv,
environment,
ec, materializer)
Expand Down Expand Up @@ -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"))
Expand Down Expand Up @@ -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 = {
Expand Down
2 changes: 1 addition & 1 deletion thehive-backend/app/models/package.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@


package object models {
val modelVersion = 14
val modelVersion = 15
}
43 changes: 43 additions & 0 deletions thehive-backend/app/services/CaseReportSrv.scala
Original file line number Diff line number Diff line change
@@ -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)
}
}
3 changes: 3 additions & 0 deletions thehive-backend/conf/routes
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 4 additions & 0 deletions ui/app/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@
<script src="bower_components/angular-drag-and-drop-lists/angular-drag-and-drop-lists.js"></script>
<script src="bower_components/angular-bootstrap-colorpicker/js/bootstrap-colorpicker-module.js"></script>
<script src="bower_components/file-saver/FileSaver.js"></script>
<script src="bower_components/jspdf/dist/jspdf.debug.js"></script>
<script src="bower_components/js-url/url.js"></script>
<!-- endbower -->

Expand Down Expand Up @@ -139,6 +140,7 @@
<script src="scripts/controllers/SearchCtrl.js"></script>
<script src="scripts/controllers/SettingsCtrl.js"></script>
<script src="scripts/controllers/admin/AdminCaseTemplatesCtrl.js"></script>
<script src="scripts/controllers/admin/AdminCaseReportCtrl.js"></script>
<script src="scripts/controllers/admin/AdminCustomFieldDialogCtrl.js"></script>
<script src="scripts/controllers/admin/AdminCustomFieldsCtrl.js"></script>
<script src="scripts/controllers/admin/AdminMetricsCtrl.js"></script>
Expand All @@ -161,6 +163,7 @@
<script src="scripts/controllers/case/CaseObservablesCtrl.js"></script>
<script src="scripts/controllers/case/CaseObservablesExportCtrl.js"></script>
<script src="scripts/controllers/case/CaseObservablesItemCtrl.js"></script>
<script src="scripts/controllers/case/CaseReportModalCtrl.js"></script>
<script src="scripts/controllers/case/CaseReopenModalCtrl.js"></script>
<script src="scripts/controllers/case/CaseStatsCtrl.js"></script>
<script src="scripts/controllers/case/CaseTasksCtrl.js"></script>
Expand Down Expand Up @@ -235,6 +238,7 @@
<script src="scripts/services/AuditSrv.js"></script>
<script src="scripts/services/AuthenticationSrv.js"></script>
<script src="scripts/services/CaseArtifactSrv.js"></script>
<script src="scripts/services/CaseReportSrv.js"></script>
<script src="scripts/services/CaseSrv.js"></script>
<script src="scripts/services/CaseTabsSrv.js"></script>
<script src="scripts/services/CaseTaskSrv.js"></script>
Expand Down
7 changes: 7 additions & 0 deletions ui/app/scripts/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}',
Expand Down
48 changes: 48 additions & 0 deletions ui/app/scripts/controllers/admin/AdminCaseReportCtrl.js
Original file line number Diff line number Diff line change
@@ -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)
})
};
});
})();
16 changes: 15 additions & 1 deletion ui/app/scripts/controllers/case/CaseMainCtrl.js
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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
Expand Down
53 changes: 53 additions & 0 deletions ui/app/scripts/controllers/case/CaseReportModalCtrl.js
Original file line number Diff line number Diff line change
@@ -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();
};
}
})();
23 changes: 23 additions & 0 deletions ui/app/scripts/services/CaseReportSrv.js
Original file line number Diff line number Diff line change
@@ -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);
}
});
})();
Loading