Skip to content
This repository was archived by the owner on Feb 12, 2025. It is now read-only.

Commit 5ad9b05

Browse files
committed
TheHive-Project#2384 Add support of operations in analyzer reports
1 parent 6cdcada commit 5ad9b05

File tree

8 files changed

+122
-58
lines changed

8 files changed

+122
-58
lines changed

cortex/connector/src/main/scala/org/thp/thehive/connector/cortex/controllers/v0/Conversion.scala

+2
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ object Conversion {
4747
.withFieldComputed(_.id, _._id.toString)
4848
.withFieldConst(_._type, "case_artifact_job")
4949
.withFieldConst(_.case_artifact, None)
50+
.withFieldComputed(_.operations, a => JsArray(a.operations).toString)
5051
.enableMethodAccessors
5152
.transform
5253
)
@@ -80,6 +81,7 @@ object Conversion {
8081
Some(observableWithExtraOutput.toValue((richObservable, JsObject.empty, Some(Left(richCase)))))
8182
}
8283
)
84+
.withFieldComputed(_.operations, a => JsArray(a.operations).toString)
8385
.enableMethodAccessors
8486
.transform
8587
}

cortex/connector/src/main/scala/org/thp/thehive/connector/cortex/models/Job.scala

+3-2
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ case class Job(
2929
endDate: Date, // end date of the job or if it is not finished date of the last check
3030
report: Option[JsObject],
3131
cortexId: String,
32-
cortexJobId: String
32+
cortexJobId: String,
33+
operations: Seq[JsObject]
3334
)
3435

3536
case class RichJob(
@@ -50,5 +51,5 @@ case class RichJob(
5051
def report: Option[JsObject] = job.report
5152
def cortexId: String = job.cortexId
5253
def cortexJobId: String = job.cortexJobId
53-
54+
def operations: Seq[JsObject] = job.operations
5455
}

cortex/connector/src/main/scala/org/thp/thehive/connector/cortex/services/ActionSrv.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ class ActionSrv @Inject() (
9696
job.report.flatMap(_.full),
9797
client.name,
9898
job.id,
99-
job.report.fold[Seq[JsObject]](Nil)(_.operations)
99+
Nil
100100
)
101101
createdAction <- Future.fromTry {
102102
db.tryTransaction { implicit graph =>

cortex/connector/src/main/scala/org/thp/thehive/connector/cortex/services/JobSrv.scala

+29-4
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ class JobSrv @Inject() (
4242
observableTypeSrv: ObservableTypeSrv,
4343
attachmentSrv: AttachmentSrv,
4444
reportTagSrv: ReportTagSrv,
45+
actionOperationSrv: ActionOperationSrv,
4546
serviceHelper: ServiceHelper,
4647
auditSrv: CortexAuditSrv,
4748
organisationSrv: OrganisationSrv,
@@ -130,6 +131,7 @@ class JobSrv @Inject() (
130131
.withFieldConst(_.report, None)
131132
.withFieldConst(_.cortexId, "tbd")
132133
.withFieldComputed(_.cortexJobId, _.id)
134+
.withFieldConst(_.operations, Nil)
133135
.transform
134136

135137
/**
@@ -167,11 +169,31 @@ class JobSrv @Inject() (
167169
.availableCortexClients(connector.clients, authContext.organisation)
168170
.find(_.name == cortexId)
169171
.fold[Future[CortexClient]](Future.failed(NotFoundError(s"Cortex $cortexId not found")))(Future.successful)
170-
job <- Future.fromTry(updateJobStatus(jobId, cortexJob))
171-
_ <- importCortexArtifacts(job, cortexJob, cortexClient)
172-
_ <- Future.fromTry(importAnalyzerTags(job, cortexJob))
172+
operations <- Future.fromTry(executeOperations(jobId, cortexJob))
173+
job <- Future.fromTry(updateJobStatus(jobId, cortexJob, operations))
174+
_ <- importCortexArtifacts(job, cortexJob, cortexClient)
175+
_ <- Future.fromTry(importAnalyzerTags(job, cortexJob))
173176
} yield job
174177

178+
def executeOperations(jobId: EntityId, cortexJob: CortexJob)(implicit authContext: AuthContext): Try[Seq[ActionOperationStatus]] =
179+
db.tryTransaction { implicit graph =>
180+
get(jobId)
181+
.observable
182+
.project(_.by.by(_.`case`.option))
183+
.getOrFail("Observable")
184+
.map {
185+
case (relatedObservable, relatedCase) =>
186+
cortexJob
187+
.report
188+
.fold[Seq[ActionOperation]](Nil)(_.operations.map(_.as[ActionOperation]))
189+
.map { operation =>
190+
actionOperationSrv
191+
.execute(relatedObservable, operation, relatedCase, None)
192+
.fold(t => ActionOperationStatus(operation, success = false, t.getMessage), identity)
193+
}
194+
}
195+
}
196+
175197
/**
176198
* Update job status, set the endDate and remove artifacts from report
177199
*
@@ -180,7 +202,9 @@ class JobSrv @Inject() (
180202
* @param authContext the authentication context
181203
* @return the updated job
182204
*/
183-
private def updateJobStatus(jobId: EntityId, cortexJob: CortexJob)(implicit authContext: AuthContext): Try[Job with Entity] =
205+
private def updateJobStatus(jobId: EntityId, cortexJob: CortexJob, operations: Seq[ActionOperationStatus])(implicit
206+
authContext: AuthContext
207+
): Try[Job with Entity] =
184208
db.tryTransaction { implicit graph =>
185209
getOrFail(jobId).flatMap { job =>
186210
val report = cortexJob.report.flatMap(r => r.full orElse r.errorMessage.map(m => Json.obj("errorMessage" -> m)))
@@ -193,6 +217,7 @@ class JobSrv @Inject() (
193217
.update(_.endDate, endDate)
194218
.update(_._updatedAt, Some(new Date))
195219
.update(_._updatedBy, Some(authContext.userId))
220+
.update(_.operations, operations.map(o => Json.toJsObject(o)))
196221
.getOrFail("Job")
197222
observable <- get(job).observable.getOrFail("Observable")
198223
_ <-

cortex/connector/src/test/resources/cortex-jobs.json

+10-1
Original file line numberDiff line numberDiff line change
@@ -87,12 +87,21 @@
8787
{
8888
"data": "192.168.1.1",
8989
"message": "myIp",
90-
"tags": [],
90+
"tags": ["tag-test"],
9191
"tlp": 2,
9292
"dataType": "ip"
9393
}
9494
],
9595
"operations": [
96+
{
97+
"type": "AddArtifactToCase",
98+
"data": "myData",
99+
"dataType": "other",
100+
"message": "test-operation",
101+
"tlp": 3,
102+
"ignoreSimilarity": false,
103+
"tags": ["tag1", "tag2"]
104+
}
96105
]
97106
},
98107
"tlp": 2,

cortex/connector/src/test/scala/org/thp/thehive/connector/cortex/services/JobSrvTest.scala

+73-48
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ import org.thp.scalligraph.models.{Database, DummyUserSrv, Schema}
77
import org.thp.scalligraph.traversal.TraversalOps._
88
import org.thp.scalligraph.{AppBuilder, EntityName}
99
import org.thp.thehive.TestAppBuilder
10-
import org.thp.thehive.connector.cortex.models.{Job, JobStatus, TheHiveCortexSchemaProvider}
10+
import org.thp.thehive.connector.cortex.models.{ActionOperationStatus, Job, JobStatus, TheHiveCortexSchemaProvider}
1111
import org.thp.thehive.connector.cortex.services.JobOps._
1212
import org.thp.thehive.models.Permissions
13+
import org.thp.thehive.services.CaseOps._
1314
import org.thp.thehive.services.ObservableOps._
1415
import org.thp.thehive.services.UserOps._
1516
import org.thp.thehive.services._
@@ -23,7 +24,8 @@ import scala.concurrent.duration.DurationInt
2324
import scala.io.Source
2425

2526
class JobSrvTest extends PlaySpecification with TestAppBuilder {
26-
implicit val authContext: AuthContext = DummyUserSrv(userId = "[email protected]", permissions = Permissions.all).authContext
27+
implicit val authContext: AuthContext =
28+
DummyUserSrv(userId = "[email protected]", organisation = "cert", permissions = Permissions.all).authContext
2729
override def appConfigure: AppBuilder =
2830
super
2931
.appConfigure
@@ -33,53 +35,76 @@ class JobSrvTest extends PlaySpecification with TestAppBuilder {
3335
.`override`(_.bindToProvider[Schema, TheHiveCortexSchemaProvider])
3436

3537
"job service" should {
38+
val cortexOutputJob = {
39+
val dataSource = Source.fromResource("cortex-jobs.json")
40+
val data = dataSource.mkString
41+
dataSource.close()
42+
Json.parse(data).as[List[OutputJob]].find(_.id == "ZWu85Q1OCVNx03hXK4df").get
43+
}
44+
3645
"handle creation and then finished job" in testApp { app =>
37-
// val job = Job(
38-
// workerId = "anaTest2",
39-
// workerName = "anaTest2",
40-
// workerDefinition = "test2",
41-
// status = JobStatus.Waiting,
42-
// startDate = new Date(1561625908856L),
43-
// endDate = new Date(1561625908856L),
44-
// report = None,
45-
// cortexId = "test",
46-
// cortexJobId = "LVyYKFstq3Rtrdc9DFmL"
47-
// )
48-
//
49-
// val cortexOutputJob = {
50-
// val dataSource = Source.fromResource("cortex-jobs.json")
51-
// val data = dataSource.mkString
52-
// dataSource.close()
53-
// Json.parse(data).as[List[OutputJob]].find(_.id == "ZWu85Q1OCVNx03hXK4df").get
54-
// }
55-
//
56-
// val createdJobTry = app[Database].tryTransaction { implicit graph =>
57-
// for {
58-
// observable <- app[ObservableSrv].startTraversal.has(_.message, "hello world").getOrFail("Observable")
59-
// createdJob <- app[JobSrv].create(job, observable)
60-
// } yield createdJob
61-
// }
62-
// createdJobTry.map { createdJob =>
63-
// Await.result(app[JobSrv].finished(app[CortexClient].name, createdJob._id, cortexOutputJob), 20.seconds)
64-
// } must beASuccessfulTry.which { updatedJob =>
65-
// updatedJob.status shouldEqual JobStatus.Success
66-
// updatedJob.report must beSome
67-
// (updatedJob.report.get \ "data").as[String] shouldEqual "imageedit_2_3904987689.jpg"
68-
//
69-
// app[Database].roTransaction { implicit graph =>
70-
// app[JobSrv].get(updatedJob).observable.has(_.message, "hello world").exists must beTrue
71-
// app[JobSrv].get(updatedJob).reportObservables.toList.length must equalTo(2).updateMessage { s =>
72-
// s"$s\nreport observables are : ${app[JobSrv].get(updatedJob).reportObservables.richObservable.toList.mkString("\n")}"
73-
// }
74-
//
75-
// for {
76-
// audit <- app[AuditSrv].startTraversal.has(_.objectId, updatedJob._id.toString).getOrFail("Audit")
77-
// organisation <- app[OrganisationSrv].getByName("cert").getOrFail("Organisation")
78-
// user <- app[UserSrv].startTraversal.getByName("[email protected]").getOrFail("User")
79-
// } yield new JobFinished().filter(audit, Some(updatedJob), organisation, Some(user))
80-
// } must beASuccessfulTry(true)
81-
// }
82-
pending("flaky test")
46+
val job = Job(
47+
workerId = "anaTest2",
48+
workerName = "anaTest2",
49+
workerDefinition = "test2",
50+
status = JobStatus.Waiting,
51+
startDate = new Date(1561625908856L),
52+
endDate = new Date(1561625908856L),
53+
report = None,
54+
cortexId = "test",
55+
cortexJobId = "LVyYKFstq3Rtrdc9DFmL",
56+
operations = Nil
57+
)
58+
59+
val createdJobTry = app[Database].tryTransaction { implicit graph =>
60+
for {
61+
observable <- app[ObservableSrv].startTraversal.has(_.message, "hello world").getOrFail("Observable")
62+
createdJob <- app[JobSrv].create(job, observable)
63+
} yield createdJob
64+
}
65+
val finishedJobTry = createdJobTry.map { createdJob =>
66+
Await.result(app[JobSrv].finished(app[CortexClient].name, createdJob._id, cortexOutputJob), 20.seconds)
67+
}
68+
finishedJobTry must beASuccessfulTry
69+
val updatedJob = finishedJobTry.get
70+
updatedJob.status shouldEqual JobStatus.Success
71+
updatedJob.report must beSome
72+
(updatedJob.report.get \ "data").as[String] shouldEqual "imageedit_2_3904987689.jpg"
73+
updatedJob.operations must haveSize(1)
74+
updatedJob.operations.map(o => (o \ "status").as[String]) must contain(beEqualTo("Success"))
75+
.forall
76+
.updateMessage(s => s"$s\nOperation has failed: ${updatedJob.operations.map("\n -" + _).mkString}")
77+
78+
app[Database].roTransaction { implicit graph =>
79+
app[JobSrv].get(updatedJob).observable.has(_.message, "hello world").exists must beTrue
80+
val reportObservables = app[JobSrv].get(updatedJob).reportObservables.toSeq
81+
reportObservables.length must equalTo(2).updateMessage { s =>
82+
s"$s\nreport observables are : ${app[JobSrv].get(updatedJob).reportObservables.richObservable.toList.mkString("\n")}"
83+
}
84+
val ipObservable = reportObservables.find(_.dataType == "ip").get
85+
ipObservable.data must beSome("192.168.1.1")
86+
ipObservable.message must beSome("myIp")
87+
ipObservable.tags must contain(exactly("tag-test"))
88+
ipObservable.tlp must beEqualTo(2)
89+
90+
val operationObservableMaybe = app[JobSrv]
91+
.get(updatedJob)
92+
.observable
93+
.`case`
94+
.observables
95+
.has(_.message, "test-operation")
96+
.headOption
97+
operationObservableMaybe must beSome.which { operationObservable =>
98+
operationObservable.data must beSome("myData")
99+
operationObservable.tlp must beEqualTo(3)
100+
operationObservable.tags must contain(exactly("tag1", "tag2"))
101+
}
102+
for {
103+
audit <- app[AuditSrv].startTraversal.has(_.objectId, updatedJob._id.toString).getOrFail("Audit")
104+
organisation <- app[OrganisationSrv].getByName("cert").getOrFail("Organisation")
105+
user <- app[UserSrv].startTraversal.getByName("[email protected]").getOrFail("User")
106+
} yield JobFinished.filter(audit, Some(updatedJob), organisation, Some(user))
107+
} must beASuccessfulTry(true).setMessage("The audit doesn't match the expected criteria")
83108
}
84109

85110
"submit a job" in testApp { app =>

dto/src/main/scala/org/thp/thehive/connector/cortex/dto/v0/Job.scala

+2-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ case class OutputJob(
1717
cortexId: String,
1818
cortexJobId: String,
1919
id: String,
20-
case_artifact: Option[OutputObservable]
20+
case_artifact: Option[OutputObservable],
21+
operations: String
2122
)
2223

2324
object OutputJob {

migration/src/main/scala/org/thp/thehive/migration/th3/Conversion.scala

+2-1
Original file line numberDiff line numberDiff line change
@@ -479,7 +479,8 @@ trait Conversion {
479479
endDate,
480480
report,
481481
cortexId,
482-
cortexJobId
482+
cortexJobId,
483+
Nil
483484
)
484485
)
485486
}

0 commit comments

Comments
 (0)