@@ -22,6 +22,11 @@ import org.elastic4play.services.JsonFormat.log
22
22
import org .elastic4play .services .QueryDSL
23
23
24
24
import models .{ Artifact , ArtifactStatus , Case , CaseImpactStatus , CaseResolutionStatus , CaseStatus , JobStatus , Task }
25
+ import play .api .Logger
26
+ import scala .util .Success
27
+ import scala .util .Failure
28
+ import models .TaskStatus
29
+ import models .LogStatus
25
30
26
31
@ Singleton
27
32
class CaseMergeSrv @ Inject () (caseSrv : CaseSrv ,
@@ -32,9 +37,20 @@ class CaseMergeSrv @Inject() (caseSrv: CaseSrv,
32
37
implicit val ec : ExecutionContext ,
33
38
implicit val mat : Materializer ) {
34
39
40
+ lazy val logger = Logger (getClass)
41
+
35
42
import QueryDSL ._
36
- private [services] def concat [E ](entities : Seq [E ], sep : String , getId : E => Long , getStr : E => String ) = {
37
- JsString (entities.map(e => s " # ${getId(e)}: ${getStr(e)}" ).mkString(sep))
43
+ private [services] def concat [E ](entities : Seq [E ], sep : String , getId : E ⇒ Long , getStr : E ⇒ String ) = {
44
+ JsString (entities.map(e ⇒ s " # ${getId(e)}: ${getStr(e)}" ).mkString(sep))
45
+ }
46
+
47
+ private [services] def concatCaseDescription (cases : Seq [Case ]) = {
48
+ val str = cases
49
+ .map { caze ⇒
50
+ s " #### ${caze.title()} ([# ${caze.caseId()}](#/case/ ${caze.id}/details)) \n\n ${caze.description()}"
51
+ }
52
+ .mkString(" \n \n " )
53
+ JsString (str)
38
54
}
39
55
40
56
private [services] def firstDate (dates : Seq [Date ]) = Json .toJson(dates.min)
@@ -43,39 +59,39 @@ class CaseMergeSrv @Inject() (caseSrv: CaseSrv,
43
59
val resolutionStatus = cases
44
60
.map(_.resolutionStatus())
45
61
.reduce[Option [CaseResolutionStatus .Type ]] {
46
- case (None , s) => s
47
- case (s, None ) => s
48
- case (Some (CaseResolutionStatus .Other ), s) => s
49
- case (s, Some (CaseResolutionStatus .Other )) => s
50
- case (Some (CaseResolutionStatus .FalsePositive ), s) => s
51
- case (s, Some (CaseResolutionStatus .FalsePositive )) => s
52
- case (Some (CaseResolutionStatus .Indeterminate ), s) => s
53
- case (s, Some (CaseResolutionStatus .Indeterminate )) => s
54
- case (s, _) => s // TruePositive
62
+ case (None , s) ⇒ s
63
+ case (s, None ) ⇒ s
64
+ case (Some (CaseResolutionStatus .Other ), s) ⇒ s
65
+ case (s, Some (CaseResolutionStatus .Other )) ⇒ s
66
+ case (Some (CaseResolutionStatus .FalsePositive ), s) ⇒ s
67
+ case (s, Some (CaseResolutionStatus .FalsePositive )) ⇒ s
68
+ case (Some (CaseResolutionStatus .Indeterminate ), s) ⇒ s
69
+ case (s, Some (CaseResolutionStatus .Indeterminate )) ⇒ s
70
+ case (s, _) ⇒ s // TruePositive
55
71
}
56
- resolutionStatus.map(s => JsString (s.toString))
72
+ resolutionStatus.map(s ⇒ JsString (s.toString))
57
73
}
58
74
59
75
private [services] def mergeImpactStatus (cases : Seq [Case ]) = {
60
76
val impactStatus = cases
61
77
.map(_.impactStatus())
62
78
.reduce[Option [CaseImpactStatus .Type ]] {
63
- case (None , s) => s
64
- case (s, None ) => s
65
- case (Some (CaseImpactStatus .NotApplicable ), s) => s
66
- case (s, Some (CaseImpactStatus .NotApplicable )) => s
67
- case (Some (CaseImpactStatus .NoImpact ), s) => s
68
- case (s, Some (CaseImpactStatus .NoImpact )) => s
69
- case (s, _) => s // WithImpact
79
+ case (None , s) ⇒ s
80
+ case (s, None ) ⇒ s
81
+ case (Some (CaseImpactStatus .NotApplicable ), s) ⇒ s
82
+ case (s, Some (CaseImpactStatus .NotApplicable )) ⇒ s
83
+ case (Some (CaseImpactStatus .NoImpact ), s) ⇒ s
84
+ case (s, Some (CaseImpactStatus .NoImpact )) ⇒ s
85
+ case (s, _) ⇒ s // WithImpact
70
86
}
71
- impactStatus.map(s => JsString (s.toString))
87
+ impactStatus.map(s ⇒ JsString (s.toString))
72
88
}
73
89
74
90
private [services] def mergeSummary (cases : Seq [Case ]) = {
75
91
val summary = cases
76
- .flatMap(c => c.summary().map(_ -> c.caseId()))
92
+ .flatMap(c ⇒ c.summary().map(_ -> c.caseId()))
77
93
.map {
78
- case (summary, caseId) => s " # $caseId: $summary"
94
+ case (summary, caseId) ⇒ s " # $caseId: $summary"
79
95
}
80
96
if (summary.isEmpty)
81
97
None
@@ -85,13 +101,13 @@ class CaseMergeSrv @Inject() (caseSrv: CaseSrv,
85
101
86
102
private [services] def mergeMetrics (cases : Seq [Case ]): JsObject = {
87
103
val metrics = for {
88
- caze <- cases
89
- metrics <- caze.metrics()
90
- metricsObject <- metrics.asOpt[JsObject ]
104
+ caze ← cases
105
+ metrics ← caze.metrics()
106
+ metricsObject ← metrics.asOpt[JsObject ]
91
107
} yield metricsObject
92
108
93
- val mergedMetrics : Seq [(String , JsValue )] = metrics.flatMap(_.keys).distinct.map { key =>
94
- val metricValues = metrics.flatMap(m => (m \ key).asOpt[BigDecimal ])
109
+ val mergedMetrics : Seq [(String , JsValue )] = metrics.flatMap(_.keys).distinct.map { key ⇒
110
+ val metricValues = metrics.flatMap(m ⇒ (m \ key).asOpt[BigDecimal ])
95
111
if (metricValues.size != 1 )
96
112
key -> JsNull
97
113
else
@@ -105,24 +121,30 @@ class CaseMergeSrv @Inject() (caseSrv: CaseSrv,
105
121
106
122
private [services] def mergeLogs (oldTask : Task , newTask : Task )(implicit authContext : AuthContext ): Future [Done ] = {
107
123
logSrv.find(" _parent" ~= oldTask.id, Some (" all" ), Nil )._1
108
- .mapAsyncUnordered(5 ) { log =>
124
+ .mapAsyncUnordered(5 ) { log ⇒
109
125
logSrv.create(newTask, baseFields(log))
110
126
}
111
127
.runWith(Sink .ignore)
112
128
}
113
129
114
130
private [services] def mergeTasksAndLogs (newCase : Case , cases : Seq [Case ])(implicit authContext : AuthContext ): Future [Done ] = {
115
- taskSrv.find(or( cases.map(" _parent " ~= _.id)) , Some (" all" ), Nil )._1
116
- .mapAsyncUnordered( 5 ) { task =>
117
- taskSrv.create(newCase, baseFields(task)).map(task -> _)
118
- }
131
+ val (tasks, futureTaskCount) = taskSrv.find(and(parent( " case " , withId( cases.map(_.id) : _* )), " status " ~!= TaskStatus . Cancel ) , Some (" all" ), Nil )
132
+ futureTaskCount.foreach(count ⇒ logger.info( s " Creating $count task(s): " ))
133
+ tasks
134
+ .mapAsyncUnordered( 5 ) { task ⇒ taskSrv.create(newCase, baseFields(task)).map(task -> _) }
119
135
.flatMapConcat {
120
- case (oldTask, newTask) =>
121
- logSrv.find(" _parent" ~= oldTask.id, Some (" all" ), Nil )._1
122
- .map(_ -> newTask)
136
+ case (oldTask, newTask) ⇒
137
+ logger.info(s " \t task : ${oldTask.id} -> ${newTask.id} : ${newTask.title()}" )
138
+ val (logs, futureLogCount) = logSrv.find(and(parent(" case_task" , withId(oldTask.id)), " status" ~!= LogStatus .Deleted ), Some (" all" ), Nil )
139
+ futureLogCount.foreach { count ⇒ logger.info(s " Creating $count log(s) in task ${newTask.id}" ) }
140
+ logs.map(_ -> newTask)
123
141
}
124
142
.mapAsyncUnordered(5 ) {
125
- case (log, task) => logSrv.create(task, baseFields(log))
143
+ case (log, task) ⇒
144
+ val fields = log.attachment().fold(baseFields(log)) { a ⇒
145
+ baseFields(log).set(" attachment" , AttachmentInputValue (a.name, a.hashes, a.size, a.contentType, a.id))
146
+ }
147
+ logSrv.create(task, fields)
126
148
}
127
149
.runWith(Sink .ignore)
128
150
}
@@ -131,71 +153,86 @@ class CaseMergeSrv @Inject() (caseSrv: CaseSrv,
131
153
val status = artifacts
132
154
.map(_.status())
133
155
.reduce[ArtifactStatus .Type ] {
134
- case (ArtifactStatus .Deleted , s) => s
135
- case (s, _) => s
156
+ case (ArtifactStatus .Deleted , s) ⇒ s
157
+ case (s, _) ⇒ s
136
158
}
137
159
.toString
138
160
JsString (status)
139
161
}
140
162
141
163
private [services] def mergeJobs (newArtifact : Artifact , artifacts : Seq [Artifact ])(implicit authContext : AuthContext ): Future [Done ] = {
142
- jobSrv.find(and(or( artifacts.map(" _parent " ~= _.id)), " status" ~= JobStatus .Success ), Some (" all" ), Nil )._1
143
- .mapAsyncUnordered(5 ) { job =>
164
+ jobSrv.find(and(parent( " case_artifact " , withId( artifacts.map(_.id) : _* )), " status" ~= JobStatus .Success ), Some (" all" ), Nil )._1
165
+ .mapAsyncUnordered(5 ) { job ⇒
144
166
jobSrv.create(newArtifact, baseFields(job))
145
167
}
146
168
.runWith(Sink .ignore)
147
169
}
148
170
149
171
private [services] def mergeArtifactsAndJobs (newCase : Case , cases : Seq [Case ])(implicit authContext : AuthContext ): Future [Done ] = {
150
- val caseMap = cases.map(c => c.id -> c).toMap
151
- val caseFilter = or( cases.map(" _parent " ~= _.id) )
172
+ val caseMap = cases.map(c ⇒ c.id -> c).toMap
173
+ val caseFilter = and(parent( " case " , withId( cases.map(_.id) : _* )), " status " ~= " Ok " )
152
174
// Find artifacts hold by cases
153
- artifactSrv.find(caseFilter, Some (" all" ), Nil )._1
154
- .map { artifact =>
175
+ val (artifacts, futureArtifactCount) = artifactSrv.find(caseFilter, Some (" all" ), Nil )
176
+ futureArtifactCount.foreach { count ⇒ log.info(s " Found $count artifact(s) in merging cases " ) }
177
+ artifacts
178
+ .mapAsyncUnordered(5 ) { artifact ⇒
155
179
// For each artifact find similar artifacts
156
180
val dataFilter = artifact.data().map(" data" ~= _) orElse artifact.attachment().map(" attachment.id" ~= _.id)
157
181
val filter = and(caseFilter,
158
182
" status" ~= " Ok" ,
159
183
" dataType" ~= artifact.dataType(),
160
184
dataFilter.get)
161
- artifactSrv.find(filter, Some (" all" ), Nil )._1
162
- .runWith(Sink .seq)
163
- .flatMap { sameArtifacts =>
164
- // Same artifacts are merged
165
- val firstArtifact = sameArtifacts.head
166
- val fields = firstArtifact.attachment().fold(Fields .empty) { a =>
167
- Fields .empty.set(" attachment" , AttachmentInputValue (a.name, a.hashes, a.size, a.contentType, a.id))
168
- }
169
- .set(" data" , firstArtifact.data().map(JsString ))
170
- .set(" dataType" , firstArtifact.dataType())
171
- .set(" message" , concat[Artifact ](sameArtifacts, " \n \n " , a => caseMap(a.parentId.get).caseId(), _.message()))
172
- .set(" startDate" , firstDate(sameArtifacts.map(_.startDate())))
173
- .set(" tlp" , JsNumber (sameArtifacts.map(_.tlp()).min))
174
- .set(" tags" , JsArray (sameArtifacts.flatMap(_.tags()).map(JsString )))
175
- .set(" ioc" , JsBoolean (sameArtifacts.map(_.ioc()).reduce(_ || _)))
176
- .set(" status" , mergeArtifactStatus(sameArtifacts))
177
- // Merged artifact is created under new case
178
- artifactSrv
179
- .create(newCase, fields)
180
- // Then jobs are imported
181
- .flatMap { newArtifact =>
182
- mergeJobs(newArtifact, sameArtifacts)
183
- }
184
- // Errors are logged and ignored (probably document already exists)
185
- .recover {
186
- case error =>
187
- log.warn(" Artifact creation fail" , error)
188
- Done
189
- }
185
+
186
+ val (artifacts, futureArtifactCount) = artifactSrv.find(filter, Some (" all" ), Nil )
187
+ futureArtifactCount.foreach { count ⇒
188
+ logger.debug(s " ${count} identical artifact(s) found ( ${artifact.dataType()}): ${(artifact.data() orElse artifact.attachment().map(_.name)).get}" )
189
+ }
190
+ artifacts.runWith(Sink .seq)
191
+ }
192
+ .mapAsync(5 ) { sameArtifacts ⇒
193
+ // Same artifacts are merged
194
+ val firstArtifact = sameArtifacts.head
195
+ val fields = firstArtifact.attachment().fold(Fields .empty) { a ⇒
196
+ Fields .empty.set(" attachment" , AttachmentInputValue (a.name, a.hashes, a.size, a.contentType, a.id))
197
+ }
198
+ .set(" data" , firstArtifact.data().map(JsString ))
199
+ .set(" dataType" , firstArtifact.dataType())
200
+ .set(" message" , concat[Artifact ](sameArtifacts, " \n \n " , a ⇒ caseMap(a.parentId.get).caseId(), _.message()))
201
+ .set(" startDate" , firstDate(sameArtifacts.map(_.startDate())))
202
+ .set(" tlp" , JsNumber (sameArtifacts.map(_.tlp()).min))
203
+ .set(" tags" , JsArray (sameArtifacts.flatMap(_.tags()).map(JsString )))
204
+ .set(" ioc" , JsBoolean (sameArtifacts.map(_.ioc()).reduce(_ || _)))
205
+ .set(" status" , mergeArtifactStatus(sameArtifacts))
206
+ // Merged artifact is created under new case
207
+ artifactSrv
208
+ .create(newCase, fields)
209
+ .map(a ⇒ List (a -> sameArtifacts))
210
+ // Errors are logged and ignored (probably document already exists)
211
+ .recover {
212
+ case e ⇒
213
+ logger.warn(" Artifact creation fail" , e)
214
+ Nil
190
215
}
191
216
}
217
+ .mapConcat(identity)
218
+ .mapAsyncUnordered(5 ) {
219
+ case (newArtifact, sameArtifacts) ⇒
220
+ // Then jobs are imported
221
+ mergeJobs(newArtifact, sameArtifacts)
222
+ .recover {
223
+ case error ⇒
224
+ logger.error(" Log creation fail" , error)
225
+ Done
226
+ }
227
+ }
192
228
.runWith(Sink .ignore)
193
229
}
194
230
195
231
private [services] def mergeCases (cases : Seq [Case ])(implicit authContext : AuthContext ): Future [Case ] = {
232
+ logger.info(" Merging cases: " + cases.map(c ⇒ s " # ${c.caseId()}: ${c.title()}" ).mkString(" / " ))
196
233
val fields = Fields .empty
197
234
.set(" title" , concat[Case ](cases, " / " , _.caseId(), _.title()))
198
- .set(" description" , concat[ Case ] (cases, " \n \n " , _.caseId(), _.description() ))
235
+ .set(" description" , concatCaseDescription (cases))
199
236
.set(" severity" , JsNumber (cases.map(_.severity()).max))
200
237
.set(" startDate" , firstDate(cases.map(_.startDate())))
201
238
.set(" tags" , JsArray (cases.flatMap(_.tags()).distinct.map(JsString )))
@@ -207,15 +244,30 @@ class CaseMergeSrv @Inject() (caseSrv: CaseSrv,
207
244
.set(" resolutionStatus" , mergeResolutionStatus(cases))
208
245
.set(" impactStatus" , mergeImpactStatus(cases))
209
246
.set(" summary" , mergeSummary(cases))
247
+ .set(" mergeFrom" , JsArray (cases.map(c ⇒ JsString (c.id))))
210
248
caseSrv.create(fields)
211
249
}
212
250
251
+ def markCaseAsDuplicated (caseIds : Seq [String ], mergeCaseId : String )(implicit authContext : AuthContext ): Future [Unit ] = {
252
+ caseSrv.bulkUpdate(caseIds, Fields .empty
253
+ .set(" mergeInto" , mergeCaseId)
254
+ .set(" status" , CaseStatus .Resolved .toString)
255
+ .set(" resolutionStatus" , CaseResolutionStatus .Duplicated .toString))
256
+ .map(_.foreach {
257
+ case Success (_) ⇒ Done
258
+ case Failure (error) ⇒
259
+ log.error(" Case update fail" , error)
260
+ Done
261
+ })
262
+ }
263
+
213
264
def merge (caseIds : String * )(implicit authContext : AuthContext ): Future [Case ] = {
214
265
for {
215
- cases <- Future .sequence(caseIds.map(caseSrv.get))
216
- newCase <- mergeCases(cases)
217
- _ <- mergeTasksAndLogs(newCase, cases)
218
- _ <- mergeArtifactsAndJobs(newCase, cases)
266
+ cases ← Future .sequence(caseIds.map(caseSrv.get))
267
+ newCase ← mergeCases(cases)
268
+ _ ← mergeTasksAndLogs(newCase, cases)
269
+ _ ← mergeArtifactsAndJobs(newCase, cases)
270
+ _ ← markCaseAsDuplicated(caseIds, newCase.id)
219
271
} yield newCase
220
272
}
221
273
}
0 commit comments