@cyvest/cyvest-js 6.1.2 → 7.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +4 -4
- package/dist/index.cjs +2057 -2166
- package/dist/index.d.cts +738 -1437
- package/dist/index.d.ts +738 -1437
- package/dist/index.js +1941 -2026
- package/package.json +31 -8
package/dist/index.cjs
CHANGED
|
@@ -1,2253 +1,2144 @@
|
|
|
1
|
-
"
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region \0rolldown/runtime.js
|
|
2
3
|
var __create = Object.create;
|
|
3
4
|
var __defProp = Object.defineProperty;
|
|
4
5
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
6
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
7
|
var __getProtoOf = Object.getPrototypeOf;
|
|
7
8
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
-
var __export = (target, all) => {
|
|
9
|
-
for (var name in all)
|
|
10
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
-
};
|
|
12
9
|
var __copyProps = (to, from, except, desc) => {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
11
|
+
key = keys[i];
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
13
|
+
get: ((k) => from[k]).bind(null, key),
|
|
14
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
19
18
|
};
|
|
20
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
// src/index.ts
|
|
31
|
-
var index_exports = {};
|
|
32
|
-
__export(index_exports, {
|
|
33
|
-
LEVEL_COLORS: () => LEVEL_COLORS,
|
|
34
|
-
LEVEL_ORDER: () => LEVEL_ORDER,
|
|
35
|
-
LEVEL_VALUES: () => LEVEL_VALUES,
|
|
36
|
-
areConnected: () => areConnected,
|
|
37
|
-
compareLevels: () => compareLevels,
|
|
38
|
-
countRelationshipsByType: () => countRelationshipsByType,
|
|
39
|
-
findExternalObservables: () => findExternalObservables,
|
|
40
|
-
findFindingByName: () => findFindingByName,
|
|
41
|
-
findFindingsAtLeast: () => findFindingsAtLeast,
|
|
42
|
-
findFindingsByLevel: () => findFindingsByLevel,
|
|
43
|
-
findFindingsForObservable: () => findFindingsForObservable,
|
|
44
|
-
findFindingsForTag: () => findFindingsForTag,
|
|
45
|
-
findHighestScoringFindings: () => findHighestScoringFindings,
|
|
46
|
-
findHighestScoringObservables: () => findHighestScoringObservables,
|
|
47
|
-
findInternalObservables: () => findInternalObservables,
|
|
48
|
-
findLeafObservables: () => findLeafObservables,
|
|
49
|
-
findMaliciousFindings: () => findMaliciousFindings,
|
|
50
|
-
findMaliciousObservables: () => findMaliciousObservables,
|
|
51
|
-
findObservablesAtLeast: () => findObservablesAtLeast,
|
|
52
|
-
findObservablesByLevel: () => findObservablesByLevel,
|
|
53
|
-
findObservablesByType: () => findObservablesByType,
|
|
54
|
-
findObservablesByValue: () => findObservablesByValue,
|
|
55
|
-
findObservablesContaining: () => findObservablesContaining,
|
|
56
|
-
findObservablesForFinding: () => findObservablesForFinding,
|
|
57
|
-
findObservablesMatching: () => findObservablesMatching,
|
|
58
|
-
findObservablesWithThreatIntel: () => findObservablesWithThreatIntel,
|
|
59
|
-
findOrphanObservables: () => findOrphanObservables,
|
|
60
|
-
findPath: () => findPath,
|
|
61
|
-
findSourceObservables: () => findSourceObservables,
|
|
62
|
-
findSuspiciousFindings: () => findSuspiciousFindings,
|
|
63
|
-
findSuspiciousObservables: () => findSuspiciousObservables,
|
|
64
|
-
findTagsAtLeast: () => findTagsAtLeast,
|
|
65
|
-
findTagsByLevel: () => findTagsByLevel,
|
|
66
|
-
findTagsByNamePattern: () => findTagsByNamePattern,
|
|
67
|
-
findThreatIntelAtLeast: () => findThreatIntelAtLeast,
|
|
68
|
-
findThreatIntelByLevel: () => findThreatIntelByLevel,
|
|
69
|
-
findThreatIntelBySource: () => findThreatIntelBySource,
|
|
70
|
-
findThreatIntelsForObservable: () => findThreatIntelsForObservable,
|
|
71
|
-
findWhitelistedObservables: () => findWhitelistedObservables,
|
|
72
|
-
generateEnrichmentKey: () => generateEnrichmentKey,
|
|
73
|
-
generateFindingKey: () => generateFindingKey,
|
|
74
|
-
generateObservableKey: () => generateObservableKey,
|
|
75
|
-
generateTagKey: () => generateTagKey,
|
|
76
|
-
generateThreatIntelKey: () => generateThreatIntelKey,
|
|
77
|
-
getAllEnrichments: () => getAllEnrichments,
|
|
78
|
-
getAllEvidences: () => getAllEvidences,
|
|
79
|
-
getAllFindingKeys: () => getAllFindingKeys,
|
|
80
|
-
getAllFindings: () => getAllFindings,
|
|
81
|
-
getAllObservableTypes: () => getAllObservableTypes,
|
|
82
|
-
getAllObservables: () => getAllObservables,
|
|
83
|
-
getAllRelationshipTypes: () => getAllRelationshipTypes,
|
|
84
|
-
getAllTags: () => getAllTags,
|
|
85
|
-
getAllThreatIntelSources: () => getAllThreatIntelSources,
|
|
86
|
-
getAllThreatIntels: () => getAllThreatIntels,
|
|
87
|
-
getColorForLevel: () => getColorForLevel,
|
|
88
|
-
getColorForScore: () => getColorForScore,
|
|
89
|
-
getCounts: () => getCounts,
|
|
90
|
-
getDataExtraction: () => getDataExtraction,
|
|
91
|
-
getEnrichment: () => getEnrichment,
|
|
92
|
-
getEnrichmentByName: () => getEnrichmentByName,
|
|
93
|
-
getEntityLevel: () => getEntityLevel,
|
|
94
|
-
getEvidence: () => getEvidence,
|
|
95
|
-
getFinding: () => getFinding,
|
|
96
|
-
getFindingByName: () => getFindingByName,
|
|
97
|
-
getLevelFromScore: () => getLevelFromScore,
|
|
98
|
-
getObservable: () => getObservable,
|
|
99
|
-
getObservableByTypeValue: () => getObservableByTypeValue,
|
|
100
|
-
getObservableChildren: () => getObservableChildren,
|
|
101
|
-
getObservableGraph: () => getObservableGraph,
|
|
102
|
-
getObservableParents: () => getObservableParents,
|
|
103
|
-
getReachableObservables: () => getReachableObservables,
|
|
104
|
-
getRelatedObservables: () => getRelatedObservables,
|
|
105
|
-
getRelatedObservablesByDirection: () => getRelatedObservablesByDirection,
|
|
106
|
-
getRelatedObservablesByType: () => getRelatedObservablesByType,
|
|
107
|
-
getRelationshipsForObservable: () => getRelationshipsForObservable,
|
|
108
|
-
getRootObservable: () => getRootObservable,
|
|
109
|
-
getStartedAt: () => getStartedAt,
|
|
110
|
-
getStats: () => getStats,
|
|
111
|
-
getTag: () => getTag,
|
|
112
|
-
getTagAggregatedLevel: () => getTagAggregatedLevel,
|
|
113
|
-
getTagAggregatedScore: () => getTagAggregatedScore,
|
|
114
|
-
getTagAncestors: () => getTagAncestors,
|
|
115
|
-
getTagByName: () => getTagByName,
|
|
116
|
-
getTagChildren: () => getTagChildren,
|
|
117
|
-
getTagDescendants: () => getTagDescendants,
|
|
118
|
-
getThreatIntel: () => getThreatIntel,
|
|
119
|
-
getThreatIntelBySourceObservable: () => getThreatIntelBySourceObservable,
|
|
120
|
-
getWhitelists: () => getWhitelists,
|
|
121
|
-
hasLevel: () => hasLevel,
|
|
122
|
-
isCyvest: () => isCyvest,
|
|
123
|
-
isLevelAtLeast: () => isLevelAtLeast,
|
|
124
|
-
isLevelHigherThan: () => isLevelHigherThan,
|
|
125
|
-
isLevelLowerThan: () => isLevelLowerThan,
|
|
126
|
-
isTagChildOf: () => isTagChildOf,
|
|
127
|
-
isTagDescendantOf: () => isTagDescendantOf,
|
|
128
|
-
isValidLevel: () => isValidLevel,
|
|
129
|
-
maxLevel: () => maxLevel,
|
|
130
|
-
minLevel: () => minLevel,
|
|
131
|
-
normalizeLevel: () => normalizeLevel,
|
|
132
|
-
parseCyvest: () => parseCyvest,
|
|
133
|
-
parseFindingKey: () => parseFindingKey,
|
|
134
|
-
parseKeyType: () => parseKeyType,
|
|
135
|
-
parseObservableKey: () => parseObservableKey,
|
|
136
|
-
parseThreatIntelKey: () => parseThreatIntelKey,
|
|
137
|
-
sortFindingsByLevel: () => sortFindingsByLevel,
|
|
138
|
-
sortFindingsByScore: () => sortFindingsByScore,
|
|
139
|
-
sortObservablesByLevel: () => sortObservablesByLevel,
|
|
140
|
-
sortObservablesByScore: () => sortObservablesByScore,
|
|
141
|
-
validateKey: () => validateKey
|
|
142
|
-
});
|
|
143
|
-
module.exports = __toCommonJS(index_exports);
|
|
144
|
-
|
|
145
|
-
// src/helpers.ts
|
|
146
|
-
var import__ = __toESM(require("ajv/dist/2020.js"), 1);
|
|
147
|
-
var import_ajv_formats = __toESM(require("ajv-formats"), 1);
|
|
148
|
-
|
|
149
|
-
// ../../../schema/cyvest.schema.json
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
|
|
20
|
+
value: mod,
|
|
21
|
+
enumerable: true
|
|
22
|
+
}) : target, mod));
|
|
23
|
+
//#endregion
|
|
24
|
+
let ajv_dist_2020_js = require("ajv/dist/2020.js");
|
|
25
|
+
ajv_dist_2020_js = __toESM(ajv_dist_2020_js, 1);
|
|
26
|
+
let ajv_formats = require("ajv-formats");
|
|
27
|
+
ajv_formats = __toESM(ajv_formats, 1);
|
|
28
|
+
//#region ../../../schema/cyvest.schema.json
|
|
150
29
|
var cyvest_schema_default = {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
30
|
+
$defs: {
|
|
31
|
+
"Contribution": {
|
|
32
|
+
"description": "One named term that fed a result, kept so the report can explain itself.",
|
|
33
|
+
"properties": {
|
|
34
|
+
"source_key": {
|
|
35
|
+
"title": "Source Key",
|
|
36
|
+
"type": "string"
|
|
37
|
+
},
|
|
38
|
+
"label": {
|
|
39
|
+
"title": "Label",
|
|
40
|
+
"type": "string"
|
|
41
|
+
},
|
|
42
|
+
"value": {
|
|
43
|
+
"title": "Value",
|
|
44
|
+
"type": "number"
|
|
45
|
+
},
|
|
46
|
+
"retained": {
|
|
47
|
+
"default": true,
|
|
48
|
+
"title": "Retained",
|
|
49
|
+
"type": "boolean"
|
|
50
|
+
},
|
|
51
|
+
"detail": {
|
|
52
|
+
"default": "",
|
|
53
|
+
"title": "Detail",
|
|
54
|
+
"type": "string"
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"required": [
|
|
58
|
+
"source_key",
|
|
59
|
+
"label",
|
|
60
|
+
"value"
|
|
61
|
+
],
|
|
62
|
+
"title": "Contribution",
|
|
63
|
+
"type": "object"
|
|
64
|
+
},
|
|
65
|
+
"Decision": {
|
|
66
|
+
"additionalProperties": false,
|
|
67
|
+
"description": "A human (or automated) call that overrides the computed result for one target.\n\n``UPHOLD`` forces the target to the policy floor, ``REFUTE`` neutralises it, ``VACATED``\nwithdraws a previous stance and restores the computed value. How each is applied depends on\nthe family of the target — an observable is bounded, a claim is taken out of the count — but\nthat is the engine's dispatch, not a second axis of this model.\n\n``justification`` is required: an override whose reason is optional is an override that\ncannot be audited, which defeats the point of recording it as a fact at all.",
|
|
68
|
+
"properties": {
|
|
69
|
+
"key": {
|
|
70
|
+
"minLength": 1,
|
|
71
|
+
"title": "Key",
|
|
72
|
+
"type": "string"
|
|
73
|
+
},
|
|
74
|
+
"seq": {
|
|
75
|
+
"maxLength": 26,
|
|
76
|
+
"minLength": 26,
|
|
77
|
+
"title": "Seq",
|
|
78
|
+
"type": "string"
|
|
79
|
+
},
|
|
80
|
+
"asserted_at": {
|
|
81
|
+
"format": "date-time",
|
|
82
|
+
"title": "Asserted At",
|
|
83
|
+
"type": "string"
|
|
84
|
+
},
|
|
85
|
+
"occurred_at": {
|
|
86
|
+
"anyOf": [{
|
|
87
|
+
"format": "date-time",
|
|
88
|
+
"type": "string"
|
|
89
|
+
}, { "type": "null" }],
|
|
90
|
+
"default": null,
|
|
91
|
+
"title": "Occurred At"
|
|
92
|
+
},
|
|
93
|
+
"source": { "$ref": "#/$defs/SourceRef" },
|
|
94
|
+
"fragment_id": {
|
|
95
|
+
"title": "Fragment Id",
|
|
96
|
+
"type": "string"
|
|
97
|
+
},
|
|
98
|
+
"external_id": {
|
|
99
|
+
"anyOf": [{ "type": "string" }, { "type": "null" }],
|
|
100
|
+
"default": null,
|
|
101
|
+
"title": "External Id"
|
|
102
|
+
},
|
|
103
|
+
"evidence_keys": {
|
|
104
|
+
"default": [],
|
|
105
|
+
"items": { "type": "string" },
|
|
106
|
+
"title": "Evidence Keys",
|
|
107
|
+
"type": "array"
|
|
108
|
+
},
|
|
109
|
+
"target_key": {
|
|
110
|
+
"minLength": 1,
|
|
111
|
+
"title": "Target Key",
|
|
112
|
+
"type": "string"
|
|
113
|
+
},
|
|
114
|
+
"kind": { "$ref": "#/$defs/DecisionKind" },
|
|
115
|
+
"justification": {
|
|
116
|
+
"minLength": 1,
|
|
117
|
+
"title": "Justification",
|
|
118
|
+
"type": "string"
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
"required": [
|
|
122
|
+
"key",
|
|
123
|
+
"seq",
|
|
124
|
+
"asserted_at",
|
|
125
|
+
"source",
|
|
126
|
+
"fragment_id",
|
|
127
|
+
"target_key",
|
|
128
|
+
"kind",
|
|
129
|
+
"justification"
|
|
130
|
+
],
|
|
131
|
+
"title": "Decision",
|
|
132
|
+
"type": "object"
|
|
133
|
+
},
|
|
134
|
+
"DecisionKind": {
|
|
135
|
+
"description": "A named override of the computation.\n\nForcing a score has to be a declared act — never the side effect of an inflated weight.\n\nThe kind states the **intent**; the mechanism follows from the family of the target, which\nthe key already carries. An earlier design enumerated one value per ``(intent, family)``\npair — ``ALLOWLISTED``/``BLOCKLISTED``/``CONFIRMED``/``DISMISSED`` — and then needed a\nvalidator to forbid the half of that product which made no sense. An enum requiring a\nvalidator to reject half its combinations encodes one axis too many: the family is the\ntarget's business, not the decision's.\n\nThe domain vocabulary survives untouched on the façade (``allowlist``, ``blocklist``,\n``confirm``, ``dismiss``), where it belongs.",
|
|
136
|
+
"enum": [
|
|
137
|
+
"UPHOLD",
|
|
138
|
+
"REFUTE",
|
|
139
|
+
"VACATED"
|
|
140
|
+
],
|
|
141
|
+
"title": "DecisionKind",
|
|
142
|
+
"type": "string"
|
|
143
|
+
},
|
|
144
|
+
"Effect": {
|
|
145
|
+
"description": "How a finding enters the investigation total. :class:`Status` says *whether*, this says *how*.\n\n``ADDITIVE`` is every finding v6 ever had: a term of the sum.\n\n``FLOOR`` and ``CEILING`` are the two halves of a **conclusion** — typically an analysis that\nread the other findings. Neither is a term: a floor raises the total just enough to reach the\nverdict it asserts, a ceiling lowers it just enough. Both add nothing once the investigation\nis already there, so conclusions never compound and several analysers may conclude on the\nsame case without inflating — or deflating — each other.\n\nA ceiling is what states a **declared benign context**: an awareness campaign, a sanctioned\npentest window, an authorised scanner. Without it the model could force a case up but never\ndown, and the only way to say \"whatever the evidence, this is benign\" would be to guess a\nlarge negative weight — the v6 mistake.\n\nThe band a verdict maps to is ``basic-v1``'s convention, like every other number here — see\n:mod:`cyvest.evaluation.projection`.",
|
|
146
|
+
"enum": [
|
|
147
|
+
"ADDITIVE",
|
|
148
|
+
"FLOOR",
|
|
149
|
+
"CEILING"
|
|
150
|
+
],
|
|
151
|
+
"title": "Effect",
|
|
152
|
+
"type": "string"
|
|
153
|
+
},
|
|
154
|
+
"Evidence": {
|
|
155
|
+
"additionalProperties": false,
|
|
156
|
+
"description": "A captured artefact: an API response, a header dump, an enrichment payload.",
|
|
157
|
+
"properties": {
|
|
158
|
+
"key": {
|
|
159
|
+
"minLength": 1,
|
|
160
|
+
"title": "Key",
|
|
161
|
+
"type": "string"
|
|
162
|
+
},
|
|
163
|
+
"seq": {
|
|
164
|
+
"maxLength": 26,
|
|
165
|
+
"minLength": 26,
|
|
166
|
+
"title": "Seq",
|
|
167
|
+
"type": "string"
|
|
168
|
+
},
|
|
169
|
+
"asserted_at": {
|
|
170
|
+
"format": "date-time",
|
|
171
|
+
"title": "Asserted At",
|
|
172
|
+
"type": "string"
|
|
173
|
+
},
|
|
174
|
+
"occurred_at": {
|
|
175
|
+
"anyOf": [{
|
|
176
|
+
"format": "date-time",
|
|
177
|
+
"type": "string"
|
|
178
|
+
}, { "type": "null" }],
|
|
179
|
+
"default": null,
|
|
180
|
+
"title": "Occurred At"
|
|
181
|
+
},
|
|
182
|
+
"source": { "$ref": "#/$defs/SourceRef" },
|
|
183
|
+
"fragment_id": {
|
|
184
|
+
"title": "Fragment Id",
|
|
185
|
+
"type": "string"
|
|
186
|
+
},
|
|
187
|
+
"external_id": {
|
|
188
|
+
"anyOf": [{ "type": "string" }, { "type": "null" }],
|
|
189
|
+
"default": null,
|
|
190
|
+
"title": "External Id"
|
|
191
|
+
},
|
|
192
|
+
"evidence_keys": {
|
|
193
|
+
"default": [],
|
|
194
|
+
"items": { "type": "string" },
|
|
195
|
+
"title": "Evidence Keys",
|
|
196
|
+
"type": "array"
|
|
197
|
+
},
|
|
198
|
+
"evidence_type": {
|
|
199
|
+
"minLength": 1,
|
|
200
|
+
"title": "Evidence Type",
|
|
201
|
+
"type": "string"
|
|
202
|
+
},
|
|
203
|
+
"title": {
|
|
204
|
+
"default": "",
|
|
205
|
+
"title": "Title",
|
|
206
|
+
"type": "string"
|
|
207
|
+
},
|
|
208
|
+
"content": {
|
|
209
|
+
"default": null,
|
|
210
|
+
"title": "Content"
|
|
211
|
+
},
|
|
212
|
+
"uri": {
|
|
213
|
+
"anyOf": [{ "type": "string" }, { "type": "null" }],
|
|
214
|
+
"default": null,
|
|
215
|
+
"title": "Uri"
|
|
216
|
+
},
|
|
217
|
+
"captured_at": {
|
|
218
|
+
"anyOf": [{
|
|
219
|
+
"format": "date-time",
|
|
220
|
+
"type": "string"
|
|
221
|
+
}, { "type": "null" }],
|
|
222
|
+
"default": null,
|
|
223
|
+
"title": "Captured At"
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
"required": [
|
|
227
|
+
"key",
|
|
228
|
+
"seq",
|
|
229
|
+
"asserted_at",
|
|
230
|
+
"source",
|
|
231
|
+
"fragment_id",
|
|
232
|
+
"evidence_type"
|
|
233
|
+
],
|
|
234
|
+
"title": "Evidence",
|
|
235
|
+
"type": "object"
|
|
236
|
+
},
|
|
237
|
+
"FactsSchema": {
|
|
238
|
+
"additionalProperties": false,
|
|
239
|
+
"description": "The fact collections, each keyed by its semantic key.",
|
|
240
|
+
"properties": {
|
|
241
|
+
"observables": {
|
|
242
|
+
"additionalProperties": { "$ref": "#/$defs/Observable" },
|
|
243
|
+
"title": "Observables",
|
|
244
|
+
"type": "object"
|
|
245
|
+
},
|
|
246
|
+
"relations": {
|
|
247
|
+
"additionalProperties": { "$ref": "#/$defs/Relation" },
|
|
248
|
+
"title": "Relations",
|
|
249
|
+
"type": "object"
|
|
250
|
+
},
|
|
251
|
+
"signals": {
|
|
252
|
+
"additionalProperties": { "$ref": "#/$defs/ThreatIntel" },
|
|
253
|
+
"title": "Signals",
|
|
254
|
+
"type": "object"
|
|
255
|
+
},
|
|
256
|
+
"evidences": {
|
|
257
|
+
"additionalProperties": { "$ref": "#/$defs/Evidence" },
|
|
258
|
+
"title": "Evidences",
|
|
259
|
+
"type": "object"
|
|
260
|
+
},
|
|
261
|
+
"findings": {
|
|
262
|
+
"additionalProperties": { "$ref": "#/$defs/Finding" },
|
|
263
|
+
"title": "Findings",
|
|
264
|
+
"type": "object"
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
"title": "FactsSchema",
|
|
268
|
+
"type": "object"
|
|
269
|
+
},
|
|
270
|
+
"Finding": {
|
|
271
|
+
"additionalProperties": false,
|
|
272
|
+
"description": "A rule outcome. Identity is ``rule_id`` alone, plus ``external_id`` when one is given.\n\nA finding names no subject: what it is about is its ``observable_links``, which are also what\nit scores on. Use ``external_id`` when the same rule must yield several findings — typically\nonce per observable, ``external_id=url.key``.\n\nA finding that describes an activity is **dated** through the envelope's ``occurred_at`` —\nwhen the activity happened, as opposed to when the rule fired — and may name the ATT&CK\n``tactic`` it demonstrates. Both are what the timeline reads; neither enters the score.",
|
|
273
|
+
"properties": {
|
|
274
|
+
"verdict": {
|
|
275
|
+
"$ref": "#/$defs/Verdict",
|
|
276
|
+
"default": "INFO"
|
|
277
|
+
},
|
|
278
|
+
"confidence": {
|
|
279
|
+
"default": 1,
|
|
280
|
+
"exclusiveMinimum": 0,
|
|
281
|
+
"maximum": 1,
|
|
282
|
+
"title": "Confidence",
|
|
283
|
+
"type": "number"
|
|
284
|
+
},
|
|
285
|
+
"weight": {
|
|
286
|
+
"anyOf": [{
|
|
287
|
+
"minimum": 0,
|
|
288
|
+
"type": "number"
|
|
289
|
+
}, { "type": "null" }],
|
|
290
|
+
"default": null,
|
|
291
|
+
"title": "Weight"
|
|
292
|
+
},
|
|
293
|
+
"key": {
|
|
294
|
+
"minLength": 1,
|
|
295
|
+
"title": "Key",
|
|
296
|
+
"type": "string"
|
|
297
|
+
},
|
|
298
|
+
"seq": {
|
|
299
|
+
"maxLength": 26,
|
|
300
|
+
"minLength": 26,
|
|
301
|
+
"title": "Seq",
|
|
302
|
+
"type": "string"
|
|
303
|
+
},
|
|
304
|
+
"asserted_at": {
|
|
305
|
+
"format": "date-time",
|
|
306
|
+
"title": "Asserted At",
|
|
307
|
+
"type": "string"
|
|
308
|
+
},
|
|
309
|
+
"occurred_at": {
|
|
310
|
+
"anyOf": [{
|
|
311
|
+
"format": "date-time",
|
|
312
|
+
"type": "string"
|
|
313
|
+
}, { "type": "null" }],
|
|
314
|
+
"default": null,
|
|
315
|
+
"title": "Occurred At"
|
|
316
|
+
},
|
|
317
|
+
"source": { "$ref": "#/$defs/SourceRef" },
|
|
318
|
+
"fragment_id": {
|
|
319
|
+
"title": "Fragment Id",
|
|
320
|
+
"type": "string"
|
|
321
|
+
},
|
|
322
|
+
"external_id": {
|
|
323
|
+
"anyOf": [{ "type": "string" }, { "type": "null" }],
|
|
324
|
+
"default": null,
|
|
325
|
+
"title": "External Id"
|
|
326
|
+
},
|
|
327
|
+
"evidence_keys": {
|
|
328
|
+
"default": [],
|
|
329
|
+
"items": { "type": "string" },
|
|
330
|
+
"title": "Evidence Keys",
|
|
331
|
+
"type": "array"
|
|
332
|
+
},
|
|
333
|
+
"rule_id": {
|
|
334
|
+
"minLength": 1,
|
|
335
|
+
"title": "Rule Id",
|
|
336
|
+
"type": "string"
|
|
337
|
+
},
|
|
338
|
+
"rule_version": {
|
|
339
|
+
"default": "1",
|
|
340
|
+
"title": "Rule Version",
|
|
341
|
+
"type": "string"
|
|
342
|
+
},
|
|
343
|
+
"name": {
|
|
344
|
+
"default": "",
|
|
345
|
+
"title": "Name",
|
|
346
|
+
"type": "string"
|
|
347
|
+
},
|
|
348
|
+
"comment": {
|
|
349
|
+
"default": "",
|
|
350
|
+
"title": "Comment",
|
|
351
|
+
"type": "string"
|
|
352
|
+
},
|
|
353
|
+
"status": {
|
|
354
|
+
"$ref": "#/$defs/Status",
|
|
355
|
+
"default": "EVALUATED"
|
|
356
|
+
},
|
|
357
|
+
"effect": {
|
|
358
|
+
"$ref": "#/$defs/Effect",
|
|
359
|
+
"default": "ADDITIVE"
|
|
360
|
+
},
|
|
361
|
+
"observable_links": {
|
|
362
|
+
"default": [],
|
|
363
|
+
"items": { "$ref": "#/$defs/ObservableLink" },
|
|
364
|
+
"title": "Observable Links",
|
|
365
|
+
"type": "array"
|
|
366
|
+
},
|
|
367
|
+
"labels": {
|
|
368
|
+
"default": [],
|
|
369
|
+
"items": { "$ref": "#/$defs/Label" },
|
|
370
|
+
"title": "Labels",
|
|
371
|
+
"type": "array"
|
|
372
|
+
},
|
|
373
|
+
"tactic": {
|
|
374
|
+
"anyOf": [{ "$ref": "#/$defs/Tactic" }, { "type": "null" }],
|
|
375
|
+
"default": null
|
|
376
|
+
},
|
|
377
|
+
"extra": {
|
|
378
|
+
"additionalProperties": true,
|
|
379
|
+
"title": "Extra",
|
|
380
|
+
"type": "object"
|
|
381
|
+
}
|
|
382
|
+
},
|
|
383
|
+
"required": [
|
|
384
|
+
"key",
|
|
385
|
+
"seq",
|
|
386
|
+
"asserted_at",
|
|
387
|
+
"source",
|
|
388
|
+
"fragment_id",
|
|
389
|
+
"rule_id"
|
|
390
|
+
],
|
|
391
|
+
"title": "Finding",
|
|
392
|
+
"type": "object"
|
|
393
|
+
},
|
|
394
|
+
"FindingResult": {
|
|
395
|
+
"description": "A finding's verdict.\n\n``own_term_suppressed`` flags that the rule's own claim was overridden by a stronger link —\na contradiction worth surfacing rather than silently dropping.\n\nThree combinations of ``(counted, score)`` are meaningful, and a consumer must not conflate\nthe last two:\n\n- ``(True, float)`` — an additive finding, a term of the total;\n- ``(False, None)`` — dismissed or not evaluated: visible, but out of the evaluation;\n- ``(True, None)`` — a conclusion (``effect`` is ``FLOOR`` or ``CEILING``): it takes part, but\n it has no magnitude of its own. Its effect is a bound on the investigation total, reported\n as a contribution of :class:`InvestigationResult`.",
|
|
396
|
+
"properties": {
|
|
397
|
+
"key": {
|
|
398
|
+
"title": "Key",
|
|
399
|
+
"type": "string"
|
|
400
|
+
},
|
|
401
|
+
"verdict": {
|
|
402
|
+
"$ref": "#/$defs/Verdict",
|
|
403
|
+
"default": "INFO"
|
|
404
|
+
},
|
|
405
|
+
"confidence": {
|
|
406
|
+
"default": 1,
|
|
407
|
+
"title": "Confidence",
|
|
408
|
+
"type": "number"
|
|
409
|
+
},
|
|
410
|
+
"score": {
|
|
411
|
+
"anyOf": [{ "type": "number" }, { "type": "null" }],
|
|
412
|
+
"default": null,
|
|
413
|
+
"title": "Score"
|
|
414
|
+
},
|
|
415
|
+
"contributions": {
|
|
416
|
+
"default": [],
|
|
417
|
+
"items": { "$ref": "#/$defs/Contribution" },
|
|
418
|
+
"title": "Contributions",
|
|
419
|
+
"type": "array"
|
|
420
|
+
},
|
|
421
|
+
"suppressed_by_decision": {
|
|
422
|
+
"default": false,
|
|
423
|
+
"title": "Suppressed By Decision",
|
|
424
|
+
"type": "boolean"
|
|
425
|
+
},
|
|
426
|
+
"raw": {
|
|
427
|
+
"additionalProperties": true,
|
|
428
|
+
"title": "Raw",
|
|
429
|
+
"type": "object"
|
|
430
|
+
},
|
|
431
|
+
"status": {
|
|
432
|
+
"$ref": "#/$defs/Status",
|
|
433
|
+
"default": "EVALUATED"
|
|
434
|
+
},
|
|
435
|
+
"effect": {
|
|
436
|
+
"$ref": "#/$defs/Effect",
|
|
437
|
+
"default": "ADDITIVE"
|
|
438
|
+
},
|
|
439
|
+
"own_term_suppressed": {
|
|
440
|
+
"default": false,
|
|
441
|
+
"title": "Own Term Suppressed",
|
|
442
|
+
"type": "boolean"
|
|
443
|
+
},
|
|
444
|
+
"counted": {
|
|
445
|
+
"default": true,
|
|
446
|
+
"title": "Counted",
|
|
447
|
+
"type": "boolean"
|
|
448
|
+
}
|
|
449
|
+
},
|
|
450
|
+
"required": ["key"],
|
|
451
|
+
"title": "FindingResult",
|
|
452
|
+
"type": "object"
|
|
453
|
+
},
|
|
454
|
+
"InvestigationHeader": {
|
|
455
|
+
"description": "What used to be a ``Case`` fact: metadata about the store rather than a fact inside it.\n\n``engine_id`` is denormalized here so an investigation stays replayable identically years\nlater, even after a newer stable engine ships.",
|
|
456
|
+
"properties": {
|
|
457
|
+
"investigation_id": {
|
|
458
|
+
"title": "Investigation Id",
|
|
459
|
+
"type": "string"
|
|
460
|
+
},
|
|
461
|
+
"name": {
|
|
462
|
+
"default": "",
|
|
463
|
+
"title": "Name",
|
|
464
|
+
"type": "string"
|
|
465
|
+
},
|
|
466
|
+
"root_key": {
|
|
467
|
+
"anyOf": [{ "type": "string" }, { "type": "null" }],
|
|
468
|
+
"default": null,
|
|
469
|
+
"title": "Root Key"
|
|
470
|
+
},
|
|
471
|
+
"opened_at": {
|
|
472
|
+
"format": "date-time",
|
|
473
|
+
"title": "Opened At",
|
|
474
|
+
"type": "string"
|
|
475
|
+
},
|
|
476
|
+
"policy_version": {
|
|
477
|
+
"default": "default-v1",
|
|
478
|
+
"title": "Policy Version",
|
|
479
|
+
"type": "string"
|
|
480
|
+
},
|
|
481
|
+
"engine_id": {
|
|
482
|
+
"default": "basic-v1",
|
|
483
|
+
"title": "Engine Id",
|
|
484
|
+
"type": "string"
|
|
485
|
+
},
|
|
486
|
+
"fragment_ids": {
|
|
487
|
+
"default": [],
|
|
488
|
+
"items": { "type": "string" },
|
|
489
|
+
"title": "Fragment Ids",
|
|
490
|
+
"type": "array"
|
|
491
|
+
}
|
|
492
|
+
},
|
|
493
|
+
"required": ["investigation_id"],
|
|
494
|
+
"title": "InvestigationHeader",
|
|
495
|
+
"type": "object"
|
|
496
|
+
},
|
|
497
|
+
"InvestigationResult": {
|
|
498
|
+
"description": "The investigation-level verdict.",
|
|
499
|
+
"properties": {
|
|
500
|
+
"key": {
|
|
501
|
+
"title": "Key",
|
|
502
|
+
"type": "string"
|
|
503
|
+
},
|
|
504
|
+
"verdict": {
|
|
505
|
+
"$ref": "#/$defs/Verdict",
|
|
506
|
+
"default": "INFO"
|
|
507
|
+
},
|
|
508
|
+
"confidence": {
|
|
509
|
+
"default": 1,
|
|
510
|
+
"title": "Confidence",
|
|
511
|
+
"type": "number"
|
|
512
|
+
},
|
|
513
|
+
"score": {
|
|
514
|
+
"anyOf": [{ "type": "number" }, { "type": "null" }],
|
|
515
|
+
"default": null,
|
|
516
|
+
"title": "Score"
|
|
517
|
+
},
|
|
518
|
+
"contributions": {
|
|
519
|
+
"default": [],
|
|
520
|
+
"items": { "$ref": "#/$defs/Contribution" },
|
|
521
|
+
"title": "Contributions",
|
|
522
|
+
"type": "array"
|
|
523
|
+
},
|
|
524
|
+
"suppressed_by_decision": {
|
|
525
|
+
"default": false,
|
|
526
|
+
"title": "Suppressed By Decision",
|
|
527
|
+
"type": "boolean"
|
|
528
|
+
},
|
|
529
|
+
"raw": {
|
|
530
|
+
"additionalProperties": true,
|
|
531
|
+
"title": "Raw",
|
|
532
|
+
"type": "object"
|
|
533
|
+
}
|
|
534
|
+
},
|
|
535
|
+
"required": ["key"],
|
|
536
|
+
"title": "InvestigationResult",
|
|
537
|
+
"type": "object"
|
|
538
|
+
},
|
|
539
|
+
"Label": {
|
|
540
|
+
"description": "A typed tag on a fact: ``axis`` says what kind of statement ``value`` makes.",
|
|
541
|
+
"properties": {
|
|
542
|
+
"axis": {
|
|
543
|
+
"minLength": 1,
|
|
544
|
+
"title": "Axis",
|
|
545
|
+
"type": "string"
|
|
546
|
+
},
|
|
547
|
+
"value": {
|
|
548
|
+
"minLength": 1,
|
|
549
|
+
"title": "Value",
|
|
550
|
+
"type": "string"
|
|
551
|
+
}
|
|
552
|
+
},
|
|
553
|
+
"required": ["axis", "value"],
|
|
554
|
+
"title": "Label",
|
|
555
|
+
"type": "object"
|
|
556
|
+
},
|
|
557
|
+
"LinkBasis": {
|
|
558
|
+
"description": "What a Finding→Observable link scores on.\n\nOne question, three answers, none of which depends on how the run was threaded:\n\n- ``OBSERVABLE`` — the observable as it stands, whoever contributed to it;\n- ``SIGNALS`` — the signals the link names, and nothing else, which is how a finding that\n fetched its own threat intel holds that value while the observable keeps accumulating;\n- ``NONE`` — nothing: the edge is kept for the graph and the narrative, but it is inert.\n\nv7.0 briefly carried a fourth, ``FRAGMENT``, gating the observable on the fragment that wrote\neach fact. It was dropped before release: it damped a merged total but never a local one, so\nthe same rules scored differently depending on whether enrichment ran in its own worker.\n``SIGNALS`` states that intent directly, and ``NONE`` covers the inert link it was standing in\nfor when migrating v6 documents.",
|
|
559
|
+
"enum": [
|
|
560
|
+
"OBSERVABLE",
|
|
561
|
+
"SIGNALS",
|
|
562
|
+
"NONE"
|
|
563
|
+
],
|
|
564
|
+
"title": "LinkBasis",
|
|
565
|
+
"type": "string"
|
|
566
|
+
},
|
|
567
|
+
"Observable": {
|
|
568
|
+
"additionalProperties": false,
|
|
569
|
+
"description": "A cyber observable. Identity is ``(type, subtype, namespace, value)``, nothing else.",
|
|
570
|
+
"properties": {
|
|
571
|
+
"key": {
|
|
572
|
+
"minLength": 1,
|
|
573
|
+
"title": "Key",
|
|
574
|
+
"type": "string"
|
|
575
|
+
},
|
|
576
|
+
"seq": {
|
|
577
|
+
"maxLength": 26,
|
|
578
|
+
"minLength": 26,
|
|
579
|
+
"title": "Seq",
|
|
580
|
+
"type": "string"
|
|
581
|
+
},
|
|
582
|
+
"asserted_at": {
|
|
583
|
+
"format": "date-time",
|
|
584
|
+
"title": "Asserted At",
|
|
585
|
+
"type": "string"
|
|
586
|
+
},
|
|
587
|
+
"occurred_at": {
|
|
588
|
+
"anyOf": [{
|
|
589
|
+
"format": "date-time",
|
|
590
|
+
"type": "string"
|
|
591
|
+
}, { "type": "null" }],
|
|
592
|
+
"default": null,
|
|
593
|
+
"title": "Occurred At"
|
|
594
|
+
},
|
|
595
|
+
"source": { "$ref": "#/$defs/SourceRef" },
|
|
596
|
+
"fragment_id": {
|
|
597
|
+
"title": "Fragment Id",
|
|
598
|
+
"type": "string"
|
|
599
|
+
},
|
|
600
|
+
"external_id": {
|
|
601
|
+
"anyOf": [{ "type": "string" }, { "type": "null" }],
|
|
602
|
+
"default": null,
|
|
603
|
+
"title": "External Id"
|
|
604
|
+
},
|
|
605
|
+
"evidence_keys": {
|
|
606
|
+
"default": [],
|
|
607
|
+
"items": { "type": "string" },
|
|
608
|
+
"title": "Evidence Keys",
|
|
609
|
+
"type": "array"
|
|
610
|
+
},
|
|
611
|
+
"type": {
|
|
612
|
+
"title": "Type",
|
|
613
|
+
"type": "string"
|
|
614
|
+
},
|
|
615
|
+
"subtype": {
|
|
616
|
+
"anyOf": [{ "type": "string" }, { "type": "null" }],
|
|
617
|
+
"title": "Subtype"
|
|
618
|
+
},
|
|
619
|
+
"namespace": {
|
|
620
|
+
"anyOf": [{ "type": "string" }, { "type": "null" }],
|
|
621
|
+
"default": null,
|
|
622
|
+
"title": "Namespace"
|
|
623
|
+
},
|
|
624
|
+
"value": {
|
|
625
|
+
"minLength": 1,
|
|
626
|
+
"title": "Value",
|
|
627
|
+
"type": "string"
|
|
628
|
+
},
|
|
629
|
+
"internal": {
|
|
630
|
+
"default": true,
|
|
631
|
+
"title": "Internal",
|
|
632
|
+
"type": "boolean"
|
|
633
|
+
},
|
|
634
|
+
"comment": {
|
|
635
|
+
"default": "",
|
|
636
|
+
"title": "Comment",
|
|
637
|
+
"type": "string"
|
|
638
|
+
},
|
|
639
|
+
"extra": {
|
|
640
|
+
"additionalProperties": true,
|
|
641
|
+
"title": "Extra",
|
|
642
|
+
"type": "object"
|
|
643
|
+
},
|
|
644
|
+
"aliases": {
|
|
645
|
+
"default": [],
|
|
646
|
+
"items": { "$ref": "#/$defs/ObservableAlias" },
|
|
647
|
+
"title": "Aliases",
|
|
648
|
+
"type": "array"
|
|
649
|
+
},
|
|
650
|
+
"occurrences": {
|
|
651
|
+
"additionalProperties": { "type": "integer" },
|
|
652
|
+
"title": "Occurrences",
|
|
653
|
+
"type": "object"
|
|
654
|
+
}
|
|
655
|
+
},
|
|
656
|
+
"required": [
|
|
657
|
+
"key",
|
|
658
|
+
"seq",
|
|
659
|
+
"asserted_at",
|
|
660
|
+
"source",
|
|
661
|
+
"fragment_id",
|
|
662
|
+
"type",
|
|
663
|
+
"value"
|
|
664
|
+
],
|
|
665
|
+
"title": "Observable",
|
|
666
|
+
"type": "object"
|
|
667
|
+
},
|
|
668
|
+
"ObservableAlias": {
|
|
669
|
+
"description": "A source identity that resolved to a canonical observable.",
|
|
670
|
+
"properties": {
|
|
671
|
+
"type": {
|
|
672
|
+
"title": "Type",
|
|
673
|
+
"type": "string"
|
|
674
|
+
},
|
|
675
|
+
"subtype": {
|
|
676
|
+
"anyOf": [{ "type": "string" }, { "type": "null" }],
|
|
677
|
+
"title": "Subtype"
|
|
678
|
+
},
|
|
679
|
+
"namespace": {
|
|
680
|
+
"anyOf": [{ "type": "string" }, { "type": "null" }],
|
|
681
|
+
"default": null,
|
|
682
|
+
"title": "Namespace"
|
|
683
|
+
},
|
|
684
|
+
"value": {
|
|
685
|
+
"minLength": 1,
|
|
686
|
+
"title": "Value",
|
|
687
|
+
"type": "string"
|
|
688
|
+
},
|
|
689
|
+
"counts": {
|
|
690
|
+
"additionalProperties": { "type": "integer" },
|
|
691
|
+
"title": "Counts",
|
|
692
|
+
"type": "object"
|
|
693
|
+
}
|
|
694
|
+
},
|
|
695
|
+
"required": ["type", "value"],
|
|
696
|
+
"title": "ObservableAlias",
|
|
697
|
+
"type": "object"
|
|
698
|
+
},
|
|
699
|
+
"ObservableLink": {
|
|
700
|
+
"description": "A link from a finding to one of its observables, with the basis it is evaluated on.\n\nBasis is **per link**, exactly like v6's ``propagation_mode``: a finding may mix bases, and\nmay even link the same observable twice under two of them. Deduplication is on the triple\n``(observable_key, basis, signal_keys)``.\n\n``signal_keys`` is sorted and deduplicated so that two links naming the same signals in a\ndifferent order are the same link, and merging stays idempotent.",
|
|
701
|
+
"properties": {
|
|
702
|
+
"observable_key": {
|
|
703
|
+
"minLength": 1,
|
|
704
|
+
"title": "Observable Key",
|
|
705
|
+
"type": "string"
|
|
706
|
+
},
|
|
707
|
+
"basis": {
|
|
708
|
+
"$ref": "#/$defs/LinkBasis",
|
|
709
|
+
"default": "OBSERVABLE"
|
|
710
|
+
},
|
|
711
|
+
"signal_keys": {
|
|
712
|
+
"default": [],
|
|
713
|
+
"items": { "type": "string" },
|
|
714
|
+
"title": "Signal Keys",
|
|
715
|
+
"type": "array"
|
|
716
|
+
}
|
|
717
|
+
},
|
|
718
|
+
"required": ["observable_key"],
|
|
719
|
+
"title": "ObservableLink",
|
|
720
|
+
"type": "object"
|
|
721
|
+
},
|
|
722
|
+
"ObservableResult": {
|
|
723
|
+
"description": "An observable's verdict. One per observable: the graph holds every fact anyone contributed.",
|
|
724
|
+
"properties": {
|
|
725
|
+
"key": {
|
|
726
|
+
"title": "Key",
|
|
727
|
+
"type": "string"
|
|
728
|
+
},
|
|
729
|
+
"verdict": {
|
|
730
|
+
"$ref": "#/$defs/Verdict",
|
|
731
|
+
"default": "INFO"
|
|
732
|
+
},
|
|
733
|
+
"confidence": {
|
|
734
|
+
"default": 1,
|
|
735
|
+
"title": "Confidence",
|
|
736
|
+
"type": "number"
|
|
737
|
+
},
|
|
738
|
+
"score": {
|
|
739
|
+
"anyOf": [{ "type": "number" }, { "type": "null" }],
|
|
740
|
+
"default": null,
|
|
741
|
+
"title": "Score"
|
|
742
|
+
},
|
|
743
|
+
"contributions": {
|
|
744
|
+
"default": [],
|
|
745
|
+
"items": { "$ref": "#/$defs/Contribution" },
|
|
746
|
+
"title": "Contributions",
|
|
747
|
+
"type": "array"
|
|
748
|
+
},
|
|
749
|
+
"suppressed_by_decision": {
|
|
750
|
+
"default": false,
|
|
751
|
+
"title": "Suppressed By Decision",
|
|
752
|
+
"type": "boolean"
|
|
753
|
+
},
|
|
754
|
+
"raw": {
|
|
755
|
+
"additionalProperties": true,
|
|
756
|
+
"title": "Raw",
|
|
757
|
+
"type": "object"
|
|
758
|
+
}
|
|
759
|
+
},
|
|
760
|
+
"required": ["key"],
|
|
761
|
+
"title": "ObservableResult",
|
|
762
|
+
"type": "object"
|
|
763
|
+
},
|
|
764
|
+
"Relation": {
|
|
765
|
+
"additionalProperties": false,
|
|
766
|
+
"description": "A directed edge between two observables, labelled by the analyst pivot that produced it.",
|
|
767
|
+
"properties": {
|
|
768
|
+
"key": {
|
|
769
|
+
"minLength": 1,
|
|
770
|
+
"title": "Key",
|
|
771
|
+
"type": "string"
|
|
772
|
+
},
|
|
773
|
+
"seq": {
|
|
774
|
+
"maxLength": 26,
|
|
775
|
+
"minLength": 26,
|
|
776
|
+
"title": "Seq",
|
|
777
|
+
"type": "string"
|
|
778
|
+
},
|
|
779
|
+
"asserted_at": {
|
|
780
|
+
"format": "date-time",
|
|
781
|
+
"title": "Asserted At",
|
|
782
|
+
"type": "string"
|
|
783
|
+
},
|
|
784
|
+
"occurred_at": {
|
|
785
|
+
"anyOf": [{
|
|
786
|
+
"format": "date-time",
|
|
787
|
+
"type": "string"
|
|
788
|
+
}, { "type": "null" }],
|
|
789
|
+
"default": null,
|
|
790
|
+
"title": "Occurred At"
|
|
791
|
+
},
|
|
792
|
+
"source": { "$ref": "#/$defs/SourceRef" },
|
|
793
|
+
"fragment_id": {
|
|
794
|
+
"title": "Fragment Id",
|
|
795
|
+
"type": "string"
|
|
796
|
+
},
|
|
797
|
+
"external_id": {
|
|
798
|
+
"anyOf": [{ "type": "string" }, { "type": "null" }],
|
|
799
|
+
"default": null,
|
|
800
|
+
"title": "External Id"
|
|
801
|
+
},
|
|
802
|
+
"evidence_keys": {
|
|
803
|
+
"default": [],
|
|
804
|
+
"items": { "type": "string" },
|
|
805
|
+
"title": "Evidence Keys",
|
|
806
|
+
"type": "array"
|
|
807
|
+
},
|
|
808
|
+
"source_key": {
|
|
809
|
+
"minLength": 1,
|
|
810
|
+
"title": "Source Key",
|
|
811
|
+
"type": "string"
|
|
812
|
+
},
|
|
813
|
+
"target_key": {
|
|
814
|
+
"minLength": 1,
|
|
815
|
+
"title": "Target Key",
|
|
816
|
+
"type": "string"
|
|
817
|
+
},
|
|
818
|
+
"kind": {
|
|
819
|
+
"$ref": "#/$defs/RelationKind",
|
|
820
|
+
"default": "related-to"
|
|
821
|
+
},
|
|
822
|
+
"observed_at": {
|
|
823
|
+
"anyOf": [{
|
|
824
|
+
"format": "date-time",
|
|
825
|
+
"type": "string"
|
|
826
|
+
}, { "type": "null" }],
|
|
827
|
+
"default": null,
|
|
828
|
+
"title": "Observed At"
|
|
829
|
+
},
|
|
830
|
+
"confidence": {
|
|
831
|
+
"default": 1,
|
|
832
|
+
"exclusiveMinimum": 0,
|
|
833
|
+
"maximum": 1,
|
|
834
|
+
"title": "Confidence",
|
|
835
|
+
"type": "number"
|
|
836
|
+
},
|
|
837
|
+
"comment": {
|
|
838
|
+
"default": "",
|
|
839
|
+
"title": "Comment",
|
|
840
|
+
"type": "string"
|
|
841
|
+
}
|
|
842
|
+
},
|
|
843
|
+
"required": [
|
|
844
|
+
"key",
|
|
845
|
+
"seq",
|
|
846
|
+
"asserted_at",
|
|
847
|
+
"source",
|
|
848
|
+
"fragment_id",
|
|
849
|
+
"source_key",
|
|
850
|
+
"target_key"
|
|
851
|
+
],
|
|
852
|
+
"title": "Relation",
|
|
853
|
+
"type": "object"
|
|
854
|
+
},
|
|
855
|
+
"RelationKind": {
|
|
856
|
+
"description": "The analyst pivot that produced the target from the source.\n\nDirection is implied: ``source_key`` is the parent, ``target_key`` the child. ``RELATED_TO``\nis symmetric and excluded from propagation, which makes v6's ``EXTRACTION`` +\n``BIDIRECTIONAL`` combination inexpressible.",
|
|
857
|
+
"enum": [
|
|
858
|
+
"extraction",
|
|
859
|
+
"pivot",
|
|
860
|
+
"related-to"
|
|
861
|
+
],
|
|
862
|
+
"title": "RelationKind",
|
|
863
|
+
"type": "string"
|
|
864
|
+
},
|
|
865
|
+
"Report": {
|
|
866
|
+
"description": "A full evaluation. Derived, never stored on the facts, recomputed from them.",
|
|
867
|
+
"properties": {
|
|
868
|
+
"engine_id": {
|
|
869
|
+
"title": "Engine Id",
|
|
870
|
+
"type": "string"
|
|
871
|
+
},
|
|
872
|
+
"policy_version": {
|
|
873
|
+
"title": "Policy Version",
|
|
874
|
+
"type": "string"
|
|
875
|
+
},
|
|
876
|
+
"investigation": { "$ref": "#/$defs/InvestigationResult" },
|
|
877
|
+
"findings": {
|
|
878
|
+
"additionalProperties": { "$ref": "#/$defs/FindingResult" },
|
|
879
|
+
"title": "Findings",
|
|
880
|
+
"type": "object"
|
|
881
|
+
},
|
|
882
|
+
"observables": {
|
|
883
|
+
"additionalProperties": { "$ref": "#/$defs/ObservableResult" },
|
|
884
|
+
"title": "Observables",
|
|
885
|
+
"type": "object"
|
|
886
|
+
}
|
|
887
|
+
},
|
|
888
|
+
"required": [
|
|
889
|
+
"engine_id",
|
|
890
|
+
"policy_version",
|
|
891
|
+
"investigation"
|
|
892
|
+
],
|
|
893
|
+
"title": "Report",
|
|
894
|
+
"type": "object"
|
|
895
|
+
},
|
|
896
|
+
"SourceClass": {
|
|
897
|
+
"description": "Family a source belongs to, used by the policy to weigh its reliability.",
|
|
898
|
+
"enum": [
|
|
899
|
+
"vendor_feed",
|
|
900
|
+
"sandbox",
|
|
901
|
+
"osint",
|
|
902
|
+
"internal_tool",
|
|
903
|
+
"org_analyst",
|
|
904
|
+
"org_policy",
|
|
905
|
+
"unknown"
|
|
906
|
+
],
|
|
907
|
+
"title": "SourceClass",
|
|
908
|
+
"type": "string"
|
|
909
|
+
},
|
|
910
|
+
"SourceRef": {
|
|
911
|
+
"description": "Who or what asserted a fact.",
|
|
912
|
+
"properties": {
|
|
913
|
+
"name": {
|
|
914
|
+
"minLength": 1,
|
|
915
|
+
"title": "Name",
|
|
916
|
+
"type": "string"
|
|
917
|
+
},
|
|
918
|
+
"source_class": {
|
|
919
|
+
"$ref": "#/$defs/SourceClass",
|
|
920
|
+
"default": "unknown"
|
|
921
|
+
}
|
|
922
|
+
},
|
|
923
|
+
"required": ["name"],
|
|
924
|
+
"title": "SourceRef",
|
|
925
|
+
"type": "object"
|
|
926
|
+
},
|
|
927
|
+
"Status": {
|
|
928
|
+
"description": "Whether a finding takes part in the evaluation at all.\n\nAnything other than ``EVALUATED`` is excluded from the score *and* from aggregation\ndenominators, while staying visible in the report.",
|
|
929
|
+
"enum": [
|
|
930
|
+
"NOT_APPLICABLE",
|
|
931
|
+
"PENDING",
|
|
932
|
+
"EVALUATED"
|
|
933
|
+
],
|
|
934
|
+
"title": "Status",
|
|
935
|
+
"type": "string"
|
|
936
|
+
},
|
|
937
|
+
"Tactic": {
|
|
938
|
+
"description": "A MITRE ATT&CK Enterprise tactic — the phase of the kill chain a finding demonstrates.\n\nKebab-case values, in kill-chain order. A tactic is a classification the timeline displays,\nnever a term the engine scores: whether the activity is malicious is the verdict's job.",
|
|
939
|
+
"enum": [
|
|
940
|
+
"reconnaissance",
|
|
941
|
+
"resource-development",
|
|
942
|
+
"initial-access",
|
|
943
|
+
"execution",
|
|
944
|
+
"persistence",
|
|
945
|
+
"privilege-escalation",
|
|
946
|
+
"defense-evasion",
|
|
947
|
+
"credential-access",
|
|
948
|
+
"discovery",
|
|
949
|
+
"lateral-movement",
|
|
950
|
+
"collection",
|
|
951
|
+
"command-and-control",
|
|
952
|
+
"exfiltration",
|
|
953
|
+
"impact"
|
|
954
|
+
],
|
|
955
|
+
"title": "Tactic",
|
|
956
|
+
"type": "string"
|
|
957
|
+
},
|
|
958
|
+
"Tag": {
|
|
959
|
+
"additionalProperties": false,
|
|
960
|
+
"description": "A label grouping findings. Merging two tags unions their finding keys.",
|
|
961
|
+
"properties": {
|
|
962
|
+
"key": {
|
|
963
|
+
"minLength": 1,
|
|
964
|
+
"title": "Key",
|
|
965
|
+
"type": "string"
|
|
966
|
+
},
|
|
967
|
+
"seq": {
|
|
968
|
+
"maxLength": 26,
|
|
969
|
+
"minLength": 26,
|
|
970
|
+
"title": "Seq",
|
|
971
|
+
"type": "string"
|
|
972
|
+
},
|
|
973
|
+
"asserted_at": {
|
|
974
|
+
"format": "date-time",
|
|
975
|
+
"title": "Asserted At",
|
|
976
|
+
"type": "string"
|
|
977
|
+
},
|
|
978
|
+
"occurred_at": {
|
|
979
|
+
"anyOf": [{
|
|
980
|
+
"format": "date-time",
|
|
981
|
+
"type": "string"
|
|
982
|
+
}, { "type": "null" }],
|
|
983
|
+
"default": null,
|
|
984
|
+
"title": "Occurred At"
|
|
985
|
+
},
|
|
986
|
+
"source": { "$ref": "#/$defs/SourceRef" },
|
|
987
|
+
"fragment_id": {
|
|
988
|
+
"title": "Fragment Id",
|
|
989
|
+
"type": "string"
|
|
990
|
+
},
|
|
991
|
+
"external_id": {
|
|
992
|
+
"anyOf": [{ "type": "string" }, { "type": "null" }],
|
|
993
|
+
"default": null,
|
|
994
|
+
"title": "External Id"
|
|
995
|
+
},
|
|
996
|
+
"evidence_keys": {
|
|
997
|
+
"default": [],
|
|
998
|
+
"items": { "type": "string" },
|
|
999
|
+
"title": "Evidence Keys",
|
|
1000
|
+
"type": "array"
|
|
1001
|
+
},
|
|
1002
|
+
"name": {
|
|
1003
|
+
"minLength": 1,
|
|
1004
|
+
"title": "Name",
|
|
1005
|
+
"type": "string"
|
|
1006
|
+
},
|
|
1007
|
+
"description": {
|
|
1008
|
+
"default": "",
|
|
1009
|
+
"title": "Description",
|
|
1010
|
+
"type": "string"
|
|
1011
|
+
},
|
|
1012
|
+
"finding_keys": {
|
|
1013
|
+
"default": [],
|
|
1014
|
+
"items": { "type": "string" },
|
|
1015
|
+
"title": "Finding Keys",
|
|
1016
|
+
"type": "array"
|
|
1017
|
+
}
|
|
1018
|
+
},
|
|
1019
|
+
"required": [
|
|
1020
|
+
"key",
|
|
1021
|
+
"seq",
|
|
1022
|
+
"asserted_at",
|
|
1023
|
+
"source",
|
|
1024
|
+
"fragment_id",
|
|
1025
|
+
"name"
|
|
1026
|
+
],
|
|
1027
|
+
"title": "Tag",
|
|
1028
|
+
"type": "object"
|
|
1029
|
+
},
|
|
1030
|
+
"Taxonomy": {
|
|
1031
|
+
"additionalProperties": false,
|
|
1032
|
+
"description": "A named value with a display verdict. No weight, confidence or scoring effect.",
|
|
1033
|
+
"properties": {
|
|
1034
|
+
"name": {
|
|
1035
|
+
"title": "Name",
|
|
1036
|
+
"type": "string"
|
|
1037
|
+
},
|
|
1038
|
+
"value": {
|
|
1039
|
+
"title": "Value",
|
|
1040
|
+
"type": "string"
|
|
1041
|
+
},
|
|
1042
|
+
"verdict": {
|
|
1043
|
+
"$ref": "#/$defs/Verdict",
|
|
1044
|
+
"default": "INFO",
|
|
1045
|
+
"description": "Descriptive only; never affects scoring"
|
|
1046
|
+
}
|
|
1047
|
+
},
|
|
1048
|
+
"required": ["name", "value"],
|
|
1049
|
+
"title": "Taxonomy",
|
|
1050
|
+
"type": "object"
|
|
1051
|
+
},
|
|
1052
|
+
"ThreatIntel": {
|
|
1053
|
+
"additionalProperties": false,
|
|
1054
|
+
"description": "A verdict from a threat-intelligence source.\n\nIdentity is ``(source, subject_key)``, so a source re-asserting the same observable updates\nin place instead of piling up duplicates. Pass ``external_id`` to keep history on purpose.",
|
|
1055
|
+
"properties": {
|
|
1056
|
+
"verdict": {
|
|
1057
|
+
"$ref": "#/$defs/Verdict",
|
|
1058
|
+
"default": "INFO"
|
|
1059
|
+
},
|
|
1060
|
+
"confidence": {
|
|
1061
|
+
"default": 1,
|
|
1062
|
+
"exclusiveMinimum": 0,
|
|
1063
|
+
"maximum": 1,
|
|
1064
|
+
"title": "Confidence",
|
|
1065
|
+
"type": "number"
|
|
1066
|
+
},
|
|
1067
|
+
"weight": {
|
|
1068
|
+
"anyOf": [{
|
|
1069
|
+
"minimum": 0,
|
|
1070
|
+
"type": "number"
|
|
1071
|
+
}, { "type": "null" }],
|
|
1072
|
+
"default": null,
|
|
1073
|
+
"title": "Weight"
|
|
1074
|
+
},
|
|
1075
|
+
"key": {
|
|
1076
|
+
"minLength": 1,
|
|
1077
|
+
"title": "Key",
|
|
1078
|
+
"type": "string"
|
|
1079
|
+
},
|
|
1080
|
+
"seq": {
|
|
1081
|
+
"maxLength": 26,
|
|
1082
|
+
"minLength": 26,
|
|
1083
|
+
"title": "Seq",
|
|
1084
|
+
"type": "string"
|
|
1085
|
+
},
|
|
1086
|
+
"asserted_at": {
|
|
1087
|
+
"format": "date-time",
|
|
1088
|
+
"title": "Asserted At",
|
|
1089
|
+
"type": "string"
|
|
1090
|
+
},
|
|
1091
|
+
"occurred_at": {
|
|
1092
|
+
"anyOf": [{
|
|
1093
|
+
"format": "date-time",
|
|
1094
|
+
"type": "string"
|
|
1095
|
+
}, { "type": "null" }],
|
|
1096
|
+
"default": null,
|
|
1097
|
+
"title": "Occurred At"
|
|
1098
|
+
},
|
|
1099
|
+
"source": { "$ref": "#/$defs/SourceRef" },
|
|
1100
|
+
"fragment_id": {
|
|
1101
|
+
"title": "Fragment Id",
|
|
1102
|
+
"type": "string"
|
|
1103
|
+
},
|
|
1104
|
+
"external_id": {
|
|
1105
|
+
"anyOf": [{ "type": "string" }, { "type": "null" }],
|
|
1106
|
+
"default": null,
|
|
1107
|
+
"title": "External Id"
|
|
1108
|
+
},
|
|
1109
|
+
"evidence_keys": {
|
|
1110
|
+
"default": [],
|
|
1111
|
+
"items": { "type": "string" },
|
|
1112
|
+
"title": "Evidence Keys",
|
|
1113
|
+
"type": "array"
|
|
1114
|
+
},
|
|
1115
|
+
"subject_key": {
|
|
1116
|
+
"minLength": 1,
|
|
1117
|
+
"title": "Subject Key",
|
|
1118
|
+
"type": "string"
|
|
1119
|
+
},
|
|
1120
|
+
"kind": {
|
|
1121
|
+
"const": "threat_intel",
|
|
1122
|
+
"default": "threat_intel",
|
|
1123
|
+
"title": "Kind",
|
|
1124
|
+
"type": "string"
|
|
1125
|
+
},
|
|
1126
|
+
"observed_at": {
|
|
1127
|
+
"anyOf": [{
|
|
1128
|
+
"format": "date-time",
|
|
1129
|
+
"type": "string"
|
|
1130
|
+
}, { "type": "null" }],
|
|
1131
|
+
"default": null,
|
|
1132
|
+
"title": "Observed At"
|
|
1133
|
+
},
|
|
1134
|
+
"labels": {
|
|
1135
|
+
"default": [],
|
|
1136
|
+
"items": { "$ref": "#/$defs/Label" },
|
|
1137
|
+
"title": "Labels",
|
|
1138
|
+
"type": "array"
|
|
1139
|
+
},
|
|
1140
|
+
"payload": {
|
|
1141
|
+
"additionalProperties": true,
|
|
1142
|
+
"title": "Payload",
|
|
1143
|
+
"type": "object"
|
|
1144
|
+
},
|
|
1145
|
+
"source_class": {
|
|
1146
|
+
"$ref": "#/$defs/SourceClass",
|
|
1147
|
+
"default": "vendor_feed"
|
|
1148
|
+
},
|
|
1149
|
+
"taxonomies": {
|
|
1150
|
+
"default": [],
|
|
1151
|
+
"items": { "$ref": "#/$defs/Taxonomy" },
|
|
1152
|
+
"title": "Taxonomies",
|
|
1153
|
+
"type": "array"
|
|
1154
|
+
},
|
|
1155
|
+
"comment": {
|
|
1156
|
+
"default": "",
|
|
1157
|
+
"title": "Comment",
|
|
1158
|
+
"type": "string"
|
|
1159
|
+
}
|
|
1160
|
+
},
|
|
1161
|
+
"required": [
|
|
1162
|
+
"key",
|
|
1163
|
+
"seq",
|
|
1164
|
+
"asserted_at",
|
|
1165
|
+
"source",
|
|
1166
|
+
"fragment_id",
|
|
1167
|
+
"subject_key"
|
|
1168
|
+
],
|
|
1169
|
+
"title": "ThreatIntel",
|
|
1170
|
+
"type": "object"
|
|
1171
|
+
},
|
|
1172
|
+
"Verdict": {
|
|
1173
|
+
"description": "Direction of a judgment, and the displayed level — they are the same thing.\n\nv7 merges the former ``Level`` into ``Verdict``. The five values line up one-for-one with\nthe score bands ``basic-v1`` inherits from v6 (``< 0``, ``= 0``, ``]0,3[``, ``[3,5[``,\n``>= 5``), which makes a verdict/level divergence structurally impossible.\n\nThose bands are ``basic-v1``'s convention, **not** part of the enum's contract: a\nprobabilistic engine maps its own posterior thresholds onto the same labels.\n\nTwo v6 levels deliberately left this axis: ``NONE`` became :class:`Status` and ``TRUSTED``\nbecame a :class:`DecisionKind` (or plain ``SAFE`` when it was merely a negative score).",
|
|
1174
|
+
"enum": [
|
|
1175
|
+
"SAFE",
|
|
1176
|
+
"INFO",
|
|
1177
|
+
"NOTABLE",
|
|
1178
|
+
"SUSPICIOUS",
|
|
1179
|
+
"MALICIOUS"
|
|
1180
|
+
],
|
|
1181
|
+
"title": "Verdict",
|
|
1182
|
+
"type": "string"
|
|
1183
|
+
}
|
|
1184
|
+
},
|
|
1185
|
+
$id: "https://cyvest.io/schema/investigation-7.json",
|
|
1186
|
+
additionalProperties: false,
|
|
1187
|
+
description: "A complete serialized investigation.",
|
|
1188
|
+
properties: {
|
|
1189
|
+
"schema_version": {
|
|
1190
|
+
"default": "7.1.0",
|
|
1191
|
+
"pattern": "^7\\.\\d+\\.\\d+$",
|
|
1192
|
+
"title": "Schema Version",
|
|
1193
|
+
"type": "string"
|
|
1194
|
+
},
|
|
1195
|
+
"header": { "$ref": "#/$defs/InvestigationHeader" },
|
|
1196
|
+
"policy_version": {
|
|
1197
|
+
"default": "default-v1",
|
|
1198
|
+
"title": "Policy Version",
|
|
1199
|
+
"type": "string"
|
|
1200
|
+
},
|
|
1201
|
+
"engine_id": {
|
|
1202
|
+
"default": "basic-v1",
|
|
1203
|
+
"title": "Engine Id",
|
|
1204
|
+
"type": "string"
|
|
1205
|
+
},
|
|
1206
|
+
"facts": { "$ref": "#/$defs/FactsSchema" },
|
|
1207
|
+
"decisions": {
|
|
1208
|
+
"additionalProperties": { "$ref": "#/$defs/Decision" },
|
|
1209
|
+
"title": "Decisions",
|
|
1210
|
+
"type": "object"
|
|
1211
|
+
},
|
|
1212
|
+
"tags": {
|
|
1213
|
+
"additionalProperties": { "$ref": "#/$defs/Tag" },
|
|
1214
|
+
"title": "Tags",
|
|
1215
|
+
"type": "object"
|
|
1216
|
+
},
|
|
1217
|
+
"report": { "$ref": "#/$defs/Report" }
|
|
1218
|
+
},
|
|
1219
|
+
required: ["header", "report"],
|
|
1220
|
+
title: "InvestigationSchema",
|
|
1221
|
+
type: "object"
|
|
1175
1222
|
};
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1223
|
+
//#endregion
|
|
1224
|
+
//#region src/helpers.ts
|
|
1225
|
+
/**
|
|
1226
|
+
* Parsing and validation.
|
|
1227
|
+
*
|
|
1228
|
+
* ajv checks the shape; the version check enforces the rule ajv cannot see — a 7.0 SDK must
|
|
1229
|
+
* refuse a 7.1 document rather than silently ignore fields it does not know about. Python
|
|
1230
|
+
* applies exactly the same rule.
|
|
1231
|
+
*
|
|
1232
|
+
* Order matters. The schema constrains `schema_version` to the current major, so the minor
|
|
1233
|
+
* window is the SDK's job alone; reading and judging the version first also keeps the error
|
|
1234
|
+
* message explicit instead of an opaque ajv shape error.
|
|
1235
|
+
*/
|
|
1236
|
+
const SCHEMA_VERSION = "7.1.0";
|
|
1237
|
+
const SEMVER = /^\d+\.\d+\.\d+$/;
|
|
1238
|
+
const ajv = new ajv_dist_2020_js.default({ allErrors: true });
|
|
1239
|
+
(0, ajv_formats.default)(ajv);
|
|
1240
|
+
let validateFn = null;
|
|
1181
1241
|
function getValidator() {
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1242
|
+
if (!validateFn) validateFn = ajv.compile(cyvest_schema_default);
|
|
1243
|
+
return validateFn;
|
|
1244
|
+
}
|
|
1245
|
+
function versionTuple(version) {
|
|
1246
|
+
const [major, minor] = version.split(".");
|
|
1247
|
+
return [Number(major), Number(minor)];
|
|
1248
|
+
}
|
|
1249
|
+
/**
|
|
1250
|
+
* Read the declared schema version, falling back to `"5"` for anything unversioned.
|
|
1251
|
+
*
|
|
1252
|
+
* A document with no `schema_version` is a pre-v7 document, not a current one: `schema_version`
|
|
1253
|
+
* is not in the schema's `required` list, so defaulting it to `SCHEMA_VERSION` would wave every
|
|
1254
|
+
* legacy payload straight through. Python's `detect_schema_version` makes the same choice.
|
|
1255
|
+
*/
|
|
1256
|
+
function detectSchemaVersion(json) {
|
|
1257
|
+
const raw = json?.schema_version;
|
|
1258
|
+
const version = typeof raw === "string" ? raw.trim() : "";
|
|
1259
|
+
return SEMVER.test(version) ? version : "5";
|
|
1260
|
+
}
|
|
1261
|
+
/** Upward compatibility only: read older documents, never newer ones. */
|
|
1262
|
+
function assertReadableVersion(version) {
|
|
1263
|
+
const [docMajor, docMinor] = versionTuple(version);
|
|
1264
|
+
const [libMajor, libMinor] = versionTuple(SCHEMA_VERSION);
|
|
1265
|
+
if (docMajor > libMajor || docMajor === libMajor && docMinor > libMinor) throw new Error(`Document schema ${version} is newer than this SDK (${SCHEMA_VERSION}); upgrade @cyvest/cyvest-js.`);
|
|
1266
|
+
if (docMajor < libMajor) throw new Error(`Document schema ${version} predates this SDK (${SCHEMA_VERSION}); run 'cyvest migrate'.`);
|
|
1267
|
+
}
|
|
1268
|
+
function isRecord(value) {
|
|
1269
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1270
|
+
}
|
|
1271
|
+
/** Normalize 7.0 free-text taxonomies once at the input boundary, without mutating the caller. */
|
|
1272
|
+
function normalizeTaxonomies(json) {
|
|
1273
|
+
if (!isRecord(json) || !isRecord(json.facts) || !isRecord(json.facts.signals)) return json;
|
|
1274
|
+
const signals = Object.fromEntries(Object.entries(json.facts.signals).map(([key, signal]) => [key, isRecord(signal) && Array.isArray(signal.taxonomies) ? {
|
|
1275
|
+
...signal,
|
|
1276
|
+
taxonomies: signal.taxonomies.map((entry) => typeof entry === "string" ? {
|
|
1277
|
+
name: entry,
|
|
1278
|
+
value: "",
|
|
1279
|
+
verdict: "INFO"
|
|
1280
|
+
} : entry)
|
|
1281
|
+
} : signal]));
|
|
1282
|
+
return {
|
|
1283
|
+
...json,
|
|
1284
|
+
facts: {
|
|
1285
|
+
...json.facts,
|
|
1286
|
+
signals
|
|
1287
|
+
}
|
|
1288
|
+
};
|
|
1186
1289
|
}
|
|
1187
1290
|
function parseCyvest(json) {
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
return json;
|
|
1291
|
+
assertReadableVersion(detectSchemaVersion(json));
|
|
1292
|
+
const normalized = normalizeTaxonomies(json);
|
|
1293
|
+
const validate = getValidator();
|
|
1294
|
+
if (!validate(normalized)) throw new Error(`Invalid Cyvest payload: ${ajv.errorsText(validate.errors || [])}`);
|
|
1295
|
+
return normalized;
|
|
1194
1296
|
}
|
|
1297
|
+
/** A type guard cannot normalize its argument: legacy strings need parseCyvest first. */
|
|
1195
1298
|
function isCyvest(json) {
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1299
|
+
try {
|
|
1300
|
+
assertReadableVersion(detectSchemaVersion(json));
|
|
1301
|
+
return !!getValidator()(json);
|
|
1302
|
+
} catch {
|
|
1303
|
+
return false;
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
//#endregion
|
|
1307
|
+
//#region src/keys.ts
|
|
1308
|
+
/** Every prefix a v7 document can carry. `enr:` is gone — enrichments are evidence now. */
|
|
1309
|
+
const KEY_PREFIXES = [
|
|
1310
|
+
"obs",
|
|
1311
|
+
"fnd",
|
|
1312
|
+
"evd",
|
|
1313
|
+
"sig",
|
|
1314
|
+
"rel",
|
|
1315
|
+
"dec",
|
|
1316
|
+
"tag"
|
|
1317
|
+
];
|
|
1318
|
+
/**
|
|
1319
|
+
* Normalize a string value for consistent key generation.
|
|
1320
|
+
*/
|
|
1201
1321
|
function normalizeValue(value) {
|
|
1202
|
-
|
|
1322
|
+
return value.trim().toLowerCase();
|
|
1203
1323
|
}
|
|
1204
1324
|
function normalizeObservableValue(obsType, value, subtype) {
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
if (!/^[+-]?\d+$/.test(stripped)) {
|
|
1217
|
-
throw new Error(`${normalizedSubtype} observable values must be base-10 integers`);
|
|
1218
|
-
}
|
|
1219
|
-
return BigInt(stripped).toString(10);
|
|
1220
|
-
}
|
|
1221
|
-
return stripped;
|
|
1325
|
+
const normalizedType = normalizeValue(obsType);
|
|
1326
|
+
const normalizedSubtype = subtype ? normalizeValue(subtype) : void 0;
|
|
1327
|
+
const stripped = value.trim();
|
|
1328
|
+
if (normalizedType === "command_line") return stripped;
|
|
1329
|
+
if (normalizedType === "email" || normalizedType === "host") return stripped.toLowerCase();
|
|
1330
|
+
if (normalizedType === "user" && (normalizedSubtype === "email" || normalizedSubtype === "upn")) return stripped.toLowerCase();
|
|
1331
|
+
if (normalizedSubtype === "uid" || normalizedSubtype === "pid") {
|
|
1332
|
+
if (!/^[+-]?\d+$/.test(stripped)) throw new Error(`${normalizedSubtype} observable values must be base-10 integers`);
|
|
1333
|
+
return BigInt(stripped).toString(10);
|
|
1334
|
+
}
|
|
1335
|
+
return stripped;
|
|
1222
1336
|
}
|
|
1223
1337
|
function sha256(content) {
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
return h.map((value) => value.toString(16).padStart(8, "0")).join("");
|
|
1338
|
+
const bytes = new TextEncoder().encode(content);
|
|
1339
|
+
const words = [];
|
|
1340
|
+
const bitLength = bytes.length * 8;
|
|
1341
|
+
for (const byte of bytes) words.push(byte);
|
|
1342
|
+
words.push(128);
|
|
1343
|
+
while (words.length % 64 !== 56) words.push(0);
|
|
1344
|
+
for (let i = 7; i >= 0; i--) words.push(Math.floor(bitLength / 2 ** (i * 8)) & 255);
|
|
1345
|
+
const h = [
|
|
1346
|
+
1779033703,
|
|
1347
|
+
3144134277,
|
|
1348
|
+
1013904242,
|
|
1349
|
+
2773480762,
|
|
1350
|
+
1359893119,
|
|
1351
|
+
2600822924,
|
|
1352
|
+
528734635,
|
|
1353
|
+
1541459225
|
|
1354
|
+
];
|
|
1355
|
+
const k = Array.from({ length: 64 }, (_, index) => {
|
|
1356
|
+
let primeCount = 0;
|
|
1357
|
+
let candidate = 2;
|
|
1358
|
+
while (true) {
|
|
1359
|
+
let prime = true;
|
|
1360
|
+
for (let divisor = 2; divisor * divisor <= candidate; divisor++) if (candidate % divisor === 0) {
|
|
1361
|
+
prime = false;
|
|
1362
|
+
break;
|
|
1363
|
+
}
|
|
1364
|
+
if (prime && primeCount++ === index) return Math.floor(Math.cbrt(candidate) % 1 * 2 ** 32) >>> 0;
|
|
1365
|
+
candidate++;
|
|
1366
|
+
}
|
|
1367
|
+
});
|
|
1368
|
+
const rotateRight = (value, amount) => value >>> amount | value << 32 - amount;
|
|
1369
|
+
for (let offset = 0; offset < words.length; offset += 64) {
|
|
1370
|
+
const schedule = new Array(64);
|
|
1371
|
+
for (let i = 0; i < 16; i++) {
|
|
1372
|
+
const base = offset + i * 4;
|
|
1373
|
+
schedule[i] = (words[base] << 24 | words[base + 1] << 16 | words[base + 2] << 8 | words[base + 3]) >>> 0;
|
|
1374
|
+
}
|
|
1375
|
+
for (let i = 16; i < 64; i++) {
|
|
1376
|
+
const s0 = rotateRight(schedule[i - 15], 7) ^ rotateRight(schedule[i - 15], 18) ^ schedule[i - 15] >>> 3;
|
|
1377
|
+
const s1 = rotateRight(schedule[i - 2], 17) ^ rotateRight(schedule[i - 2], 19) ^ schedule[i - 2] >>> 10;
|
|
1378
|
+
schedule[i] = schedule[i - 16] + s0 + schedule[i - 7] + s1 >>> 0;
|
|
1379
|
+
}
|
|
1380
|
+
let [a, b, c, d, e, f, g, hh] = h;
|
|
1381
|
+
for (let i = 0; i < 64; i++) {
|
|
1382
|
+
const s1 = rotateRight(e, 6) ^ rotateRight(e, 11) ^ rotateRight(e, 25);
|
|
1383
|
+
const choice = e & f ^ ~e & g;
|
|
1384
|
+
const temp1 = hh + s1 + choice + k[i] + schedule[i] >>> 0;
|
|
1385
|
+
const temp2 = (rotateRight(a, 2) ^ rotateRight(a, 13) ^ rotateRight(a, 22)) + (a & b ^ a & c ^ b & c) >>> 0;
|
|
1386
|
+
hh = g;
|
|
1387
|
+
g = f;
|
|
1388
|
+
f = e;
|
|
1389
|
+
e = d + temp1 >>> 0;
|
|
1390
|
+
d = c;
|
|
1391
|
+
c = b;
|
|
1392
|
+
b = a;
|
|
1393
|
+
a = temp1 + temp2 >>> 0;
|
|
1394
|
+
}
|
|
1395
|
+
[
|
|
1396
|
+
a,
|
|
1397
|
+
b,
|
|
1398
|
+
c,
|
|
1399
|
+
d,
|
|
1400
|
+
e,
|
|
1401
|
+
f,
|
|
1402
|
+
g,
|
|
1403
|
+
hh
|
|
1404
|
+
].forEach((value, index) => {
|
|
1405
|
+
h[index] = h[index] + value >>> 0;
|
|
1406
|
+
});
|
|
1407
|
+
}
|
|
1408
|
+
return h.map((value) => value.toString(16).padStart(8, "0")).join("");
|
|
1296
1409
|
}
|
|
1297
1410
|
function encodeKeyPart(value, keepSlash = false) {
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1411
|
+
let encoded = encodeURIComponent(value).replace(/[!'()*]/g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
1412
|
+
encoded = encoded.replace(/%40/gi, "@");
|
|
1413
|
+
if (keepSlash) encoded = encoded.replace(/%2F/gi, "/").replace(/%5C/gi, "\\");
|
|
1414
|
+
return encoded;
|
|
1415
|
+
}
|
|
1416
|
+
/**
|
|
1417
|
+
* Generate a unique key for an observable.
|
|
1418
|
+
*
|
|
1419
|
+
* Format: obs:{type}:{normalized_value}
|
|
1420
|
+
*
|
|
1421
|
+
* @param obsType - Type of observable (ip, url, domain, hash, etc.)
|
|
1422
|
+
* @param value - Value of the observable
|
|
1423
|
+
* @returns Unique observable key
|
|
1424
|
+
*
|
|
1425
|
+
* @example
|
|
1426
|
+
* ```ts
|
|
1427
|
+
* generateObservableKey("ipv4", "192.168.1.1")
|
|
1428
|
+
* // => "obs:ipv4:192.168.1.1"
|
|
1429
|
+
* ```
|
|
1430
|
+
*/
|
|
1315
1431
|
function generateObservableKey(obsType, value, subtype, namespace) {
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
}
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
}
|
|
1432
|
+
const normalizedType = normalizeValue(obsType);
|
|
1433
|
+
const normalizedSubtype = subtype ? normalizeValue(subtype) : void 0;
|
|
1434
|
+
const normalizedNamespace = namespace?.trim().toLowerCase() || void 0;
|
|
1435
|
+
const normalizedValue = normalizeObservableValue(normalizedType, value, normalizedSubtype);
|
|
1436
|
+
const identity = JSON.stringify({
|
|
1437
|
+
namespace: normalizedNamespace ?? null,
|
|
1438
|
+
subtype: normalizedSubtype ?? null,
|
|
1439
|
+
type: normalizedType,
|
|
1440
|
+
value: normalizedValue
|
|
1441
|
+
});
|
|
1442
|
+
if (normalizedType === "command_line" || new TextEncoder().encode(identity).length > 128) return `obs:${normalizedType}:sha256:${sha256(identity)}`;
|
|
1443
|
+
if (!normalizedSubtype && !normalizedNamespace) return `obs:${normalizedType}:${normalizedValue.toLowerCase()}`;
|
|
1444
|
+
const parts = [
|
|
1445
|
+
"obs",
|
|
1446
|
+
encodeKeyPart(normalizedType),
|
|
1447
|
+
normalizedSubtype ? encodeKeyPart(normalizedSubtype) : "_"
|
|
1448
|
+
];
|
|
1449
|
+
if (normalizedNamespace) parts.push(encodeKeyPart(normalizedNamespace));
|
|
1450
|
+
parts.push(encodeKeyPart(normalizedValue, true));
|
|
1451
|
+
return parts.join(":");
|
|
1452
|
+
}
|
|
1453
|
+
/**
|
|
1454
|
+
* Generate a finding key.
|
|
1455
|
+
*
|
|
1456
|
+
* Format: `fnd:{rule_id}`, or `fnd:{rule_id}:{external_id}`.
|
|
1457
|
+
*
|
|
1458
|
+
* A finding is identified by the rule that produced it. Running the same rule on several
|
|
1459
|
+
* observables means several findings, so pass an `externalId` to keep them apart — usually the
|
|
1460
|
+
* observable key.
|
|
1461
|
+
*
|
|
1462
|
+
* @example
|
|
1463
|
+
* ```ts
|
|
1464
|
+
* generateFindingKey("url_in_body")
|
|
1465
|
+
* // => "fnd:url_in_body"
|
|
1466
|
+
* ```
|
|
1467
|
+
*/
|
|
1468
|
+
function generateFindingKey(ruleId, externalId) {
|
|
1469
|
+
const normalizedRule = normalizeValue(ruleId);
|
|
1470
|
+
if (externalId) return `fnd:${normalizedRule}:${externalId.trim()}`;
|
|
1471
|
+
return `fnd:${normalizedRule}`;
|
|
1472
|
+
}
|
|
1473
|
+
/**
|
|
1474
|
+
* Generate an observable-signal key.
|
|
1475
|
+
*
|
|
1476
|
+
* Format: `sig:{source}:{subject_key}`. The prefix names the *family*, not its first member:
|
|
1477
|
+
* v6's `ti:` would have locked every future signal kind behind a threat-intel name.
|
|
1478
|
+
*
|
|
1479
|
+
* @example
|
|
1480
|
+
* ```ts
|
|
1481
|
+
* generateSignalKey("virustotal", "obs:ipv4:192.168.1.1")
|
|
1482
|
+
* // => "sig:virustotal:obs:ipv4:192.168.1.1"
|
|
1483
|
+
* ```
|
|
1484
|
+
*/
|
|
1485
|
+
function generateSignalKey(source, subjectKey, externalId) {
|
|
1486
|
+
const normalizedSource = normalizeValue(source);
|
|
1487
|
+
if (externalId) return `sig:${normalizedSource}:${externalId.trim()}:${subjectKey}`;
|
|
1488
|
+
return `sig:${normalizedSource}:${subjectKey}`;
|
|
1489
|
+
}
|
|
1490
|
+
/** Kept under its v6 name; new code calls {@link generateSignalKey}. */
|
|
1491
|
+
const generateThreatIntelKey = generateSignalKey;
|
|
1492
|
+
/**
|
|
1493
|
+
* Generate a relation key.
|
|
1494
|
+
*
|
|
1495
|
+
* Format: `rel:{kind}:{source_key}>{target_key}` — direction lives in the key itself, so no
|
|
1496
|
+
* `direction` field is needed to disambiguate.
|
|
1497
|
+
*/
|
|
1498
|
+
function generateRelationKey(sourceKey, targetKey, kind, externalId) {
|
|
1499
|
+
const normalizedKind = normalizeValue(kind);
|
|
1500
|
+
if (externalId) return `rel:${normalizedKind}:${externalId.trim()}:${sourceKey}>${targetKey}`;
|
|
1501
|
+
return `rel:${normalizedKind}:${sourceKey}>${targetKey}`;
|
|
1502
|
+
}
|
|
1503
|
+
/**
|
|
1504
|
+
* Generate a decision key.
|
|
1505
|
+
*
|
|
1506
|
+
* Format: `dec:{target_key}` — one decision per target, whatever it says. The kind is content,
|
|
1507
|
+
* not identity: a target holds a single current stance, and changing one's mind is a
|
|
1508
|
+
* re-assertion the merge law settles by freshness.
|
|
1509
|
+
*/
|
|
1510
|
+
function generateDecisionKey(targetKey) {
|
|
1511
|
+
return `dec:${targetKey}`;
|
|
1512
|
+
}
|
|
1513
|
+
/**
|
|
1514
|
+
* Generate a unique key for a tag.
|
|
1515
|
+
*
|
|
1516
|
+
* Format: tag:{normalized_name}
|
|
1517
|
+
*
|
|
1518
|
+
* @param name - Name of the tag (can use : as hierarchy delimiter)
|
|
1519
|
+
* @returns Unique tag key
|
|
1520
|
+
*
|
|
1521
|
+
* @example
|
|
1522
|
+
* ```ts
|
|
1523
|
+
* generateTagKey("header:auth:dkim")
|
|
1524
|
+
* // => "tag:header:auth:dkim"
|
|
1525
|
+
* ```
|
|
1526
|
+
*/
|
|
1357
1527
|
function generateTagKey(name) {
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1528
|
+
return `tag:${normalizeValue(name)}`;
|
|
1529
|
+
}
|
|
1530
|
+
/**
|
|
1531
|
+
* Get all ancestor tag names from a hierarchical tag name.
|
|
1532
|
+
*
|
|
1533
|
+
* @param name - Tag name with : delimiter
|
|
1534
|
+
* @returns Array of ancestor tag names
|
|
1535
|
+
*
|
|
1536
|
+
* @example
|
|
1537
|
+
* ```ts
|
|
1538
|
+
* getTagAncestors("header:auth:dkim")
|
|
1539
|
+
* // => ["header", "header:auth"]
|
|
1540
|
+
* ```
|
|
1541
|
+
*/
|
|
1361
1542
|
function getTagAncestors(name) {
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1543
|
+
const parts = name.split(":");
|
|
1544
|
+
const ancestors = [];
|
|
1545
|
+
for (let i = 0; i < parts.length - 1; i++) ancestors.push(parts.slice(0, i + 1).join(":"));
|
|
1546
|
+
return ancestors;
|
|
1547
|
+
}
|
|
1548
|
+
/**
|
|
1549
|
+
* Check if a tag is a direct child of another tag.
|
|
1550
|
+
*
|
|
1551
|
+
* @param childName - Potential child tag name
|
|
1552
|
+
* @param parentName - Potential parent tag name
|
|
1553
|
+
* @returns True if childName is a direct child of parentName
|
|
1554
|
+
*
|
|
1555
|
+
* @example
|
|
1556
|
+
* ```ts
|
|
1557
|
+
* isTagChildOf("header:auth", "header") // => true
|
|
1558
|
+
* isTagChildOf("header:auth:dkim", "header") // => false (grandchild)
|
|
1559
|
+
* ```
|
|
1560
|
+
*/
|
|
1369
1561
|
function isTagChildOf(childName, parentName) {
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1562
|
+
if (!childName.startsWith(parentName + ":")) return false;
|
|
1563
|
+
return !childName.slice(parentName.length + 1).includes(":");
|
|
1564
|
+
}
|
|
1565
|
+
/**
|
|
1566
|
+
* Check if a tag is a descendant of another tag (any depth).
|
|
1567
|
+
*
|
|
1568
|
+
* @param descendantName - Potential descendant tag name
|
|
1569
|
+
* @param ancestorName - Potential ancestor tag name
|
|
1570
|
+
* @returns True if descendantName is a descendant of ancestorName
|
|
1571
|
+
*
|
|
1572
|
+
* @example
|
|
1573
|
+
* ```ts
|
|
1574
|
+
* isTagDescendantOf("header:auth:dkim", "header") // => true
|
|
1575
|
+
* isTagDescendantOf("header", "header") // => false (same)
|
|
1576
|
+
* ```
|
|
1577
|
+
*/
|
|
1376
1578
|
function isTagDescendantOf(descendantName, ancestorName) {
|
|
1377
|
-
|
|
1378
|
-
}
|
|
1579
|
+
return descendantName.startsWith(ancestorName + ":");
|
|
1580
|
+
}
|
|
1581
|
+
/**
|
|
1582
|
+
* Extract the type prefix from a key.
|
|
1583
|
+
*
|
|
1584
|
+
* @param key - The key to parse
|
|
1585
|
+
* @returns Type prefix (see {@link KEY_PREFIXES}) or null if invalid
|
|
1586
|
+
*
|
|
1587
|
+
* @example
|
|
1588
|
+
* ```ts
|
|
1589
|
+
* parseKeyType("obs:ipv4:192.168.1.1") // => "obs"
|
|
1590
|
+
* parseKeyType("invalid") // => null
|
|
1591
|
+
* ```
|
|
1592
|
+
*/
|
|
1379
1593
|
function parseKeyType(key) {
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1594
|
+
if (key.includes(":")) {
|
|
1595
|
+
const prefix = key.split(":", 1)[0];
|
|
1596
|
+
if (KEY_PREFIXES.includes(prefix)) return prefix;
|
|
1597
|
+
}
|
|
1598
|
+
return null;
|
|
1599
|
+
}
|
|
1600
|
+
/**
|
|
1601
|
+
* Validate a key format and optionally finding its type.
|
|
1602
|
+
*
|
|
1603
|
+
* @param key - The key to validate
|
|
1604
|
+
* @param expectedType - Optional expected type prefix
|
|
1605
|
+
* @returns True if valid, false otherwise
|
|
1606
|
+
*
|
|
1607
|
+
* @example
|
|
1608
|
+
* ```ts
|
|
1609
|
+
* validateKey("obs:ipv4:192.168.1.1") // => true
|
|
1610
|
+
* validateKey("obs:ipv4:192.168.1.1", "obs") // => true
|
|
1611
|
+
* validateKey("obs:ipv4:192.168.1.1", "chk") // => false
|
|
1612
|
+
* validateKey("invalid") // => false
|
|
1613
|
+
* ```
|
|
1614
|
+
*/
|
|
1388
1615
|
function validateKey(key, expectedType) {
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1616
|
+
if (!key || !key.includes(":")) return false;
|
|
1617
|
+
const keyType = parseKeyType(key);
|
|
1618
|
+
if (!keyType) return false;
|
|
1619
|
+
if (expectedType && keyType !== expectedType) return false;
|
|
1620
|
+
return true;
|
|
1621
|
+
}
|
|
1622
|
+
/**
|
|
1623
|
+
* Extract components from an observable key.
|
|
1624
|
+
*
|
|
1625
|
+
* Only reliable for the simple form `obs:{type}:{value}`. A key carrying a subtype or a
|
|
1626
|
+
* namespace, and a `command_line` or oversized key folded into `obs:{type}:sha256:{digest}`,
|
|
1627
|
+
* both return the remaining segments verbatim as `value` — a key is an identity token, not a
|
|
1628
|
+
* record. Read the observable from the document instead of parsing its key.
|
|
1629
|
+
*
|
|
1630
|
+
* @param key - Observable key to parse
|
|
1631
|
+
* @returns Object with type and value, or null if invalid
|
|
1632
|
+
*
|
|
1633
|
+
* @example
|
|
1634
|
+
* ```ts
|
|
1635
|
+
* parseObservableKey("obs:ipv4:192.168.1.1")
|
|
1636
|
+
* // => { type: "ipv4", value: "192.168.1.1" }
|
|
1637
|
+
* ```
|
|
1638
|
+
*/
|
|
1401
1639
|
function parseObservableKey(key) {
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
const parts = key.split(":");
|
|
1420
|
-
if (parts.length >= 2) {
|
|
1421
|
-
return {
|
|
1422
|
-
findingName: parts.slice(1).join(":")
|
|
1423
|
-
};
|
|
1424
|
-
}
|
|
1425
|
-
return null;
|
|
1426
|
-
}
|
|
1427
|
-
function parseThreatIntelKey(key) {
|
|
1428
|
-
if (!validateKey(key, "ti")) {
|
|
1429
|
-
return null;
|
|
1430
|
-
}
|
|
1431
|
-
const parts = key.split(":");
|
|
1432
|
-
if (parts.length >= 3) {
|
|
1433
|
-
return {
|
|
1434
|
-
source: parts[1],
|
|
1435
|
-
observableKey: parts.slice(2).join(":")
|
|
1436
|
-
};
|
|
1437
|
-
}
|
|
1438
|
-
return null;
|
|
1439
|
-
}
|
|
1440
|
-
|
|
1441
|
-
// src/levels.ts
|
|
1442
|
-
var LEVEL_ORDER = [
|
|
1443
|
-
"NONE",
|
|
1444
|
-
"TRUSTED",
|
|
1445
|
-
"INFO",
|
|
1446
|
-
"SAFE",
|
|
1447
|
-
"NOTABLE",
|
|
1448
|
-
"SUSPICIOUS",
|
|
1449
|
-
"MALICIOUS"
|
|
1640
|
+
if (!validateKey(key, "obs")) return null;
|
|
1641
|
+
const parts = key.split(":");
|
|
1642
|
+
if (parts.length >= 3) return {
|
|
1643
|
+
type: parts[1],
|
|
1644
|
+
value: parts.slice(2).join(":")
|
|
1645
|
+
};
|
|
1646
|
+
return null;
|
|
1647
|
+
}
|
|
1648
|
+
//#endregion
|
|
1649
|
+
//#region src/verdicts.ts
|
|
1650
|
+
/** Ordered from most exculpatory to most inculpatory. */
|
|
1651
|
+
const VERDICT_ORDER = [
|
|
1652
|
+
"SAFE",
|
|
1653
|
+
"INFO",
|
|
1654
|
+
"NOTABLE",
|
|
1655
|
+
"SUSPICIOUS",
|
|
1656
|
+
"MALICIOUS"
|
|
1450
1657
|
];
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1658
|
+
const VERDICT_RANK = {
|
|
1659
|
+
SAFE: 0,
|
|
1660
|
+
INFO: 1,
|
|
1661
|
+
NOTABLE: 2,
|
|
1662
|
+
SUSPICIOUS: 3,
|
|
1663
|
+
MALICIOUS: 4
|
|
1664
|
+
};
|
|
1665
|
+
/**
|
|
1666
|
+
* Rich style names, mirroring the Python terminal renderer.
|
|
1667
|
+
*
|
|
1668
|
+
* These are *not* CSS colours: a browser cannot draw `orange3`. Use {@link VERDICT_HEX_COLORS}
|
|
1669
|
+
* for anything rendered on screen.
|
|
1670
|
+
*/
|
|
1671
|
+
const VERDICT_TERMINAL_STYLES = {
|
|
1672
|
+
SAFE: "bright_green",
|
|
1673
|
+
INFO: "cyan",
|
|
1674
|
+
NOTABLE: "yellow",
|
|
1675
|
+
SUSPICIOUS: "orange3",
|
|
1676
|
+
MALICIOUS: "red"
|
|
1459
1677
|
};
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
SAFE: "#4ade80",
|
|
1468
|
-
// bright green
|
|
1469
|
-
NOTABLE: "#eab308",
|
|
1470
|
-
// yellow
|
|
1471
|
-
SUSPICIOUS: "#f97316",
|
|
1472
|
-
// orange
|
|
1473
|
-
MALICIOUS: "#ef4444"
|
|
1474
|
-
// red
|
|
1678
|
+
/** The web palette: muted enough that a wall of NOTABLE nodes stays readable. */
|
|
1679
|
+
const VERDICT_HEX_COLORS = {
|
|
1680
|
+
SAFE: "#648b79",
|
|
1681
|
+
INFO: "#94a3b8",
|
|
1682
|
+
NOTABLE: "#aa8958",
|
|
1683
|
+
SUSPICIOUS: "#ad704b",
|
|
1684
|
+
MALICIOUS: "#ad5555"
|
|
1475
1685
|
};
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
}
|
|
1501
|
-
function
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
return LEVEL_VALUES[a] < LEVEL_VALUES[b];
|
|
1513
|
-
}
|
|
1514
|
-
function isLevelAtLeast(a, minLevel2) {
|
|
1515
|
-
return LEVEL_VALUES[a] >= LEVEL_VALUES[minLevel2];
|
|
1516
|
-
}
|
|
1517
|
-
function maxLevel(levels) {
|
|
1518
|
-
if (levels.length === 0) return "NONE";
|
|
1519
|
-
return levels.reduce(
|
|
1520
|
-
(max, level) => isLevelHigherThan(level, max) ? level : max
|
|
1521
|
-
);
|
|
1522
|
-
}
|
|
1523
|
-
function minLevel(levels) {
|
|
1524
|
-
if (levels.length === 0) return "MALICIOUS";
|
|
1525
|
-
return levels.reduce(
|
|
1526
|
-
(min, level) => isLevelLowerThan(level, min) ? level : min
|
|
1527
|
-
);
|
|
1528
|
-
}
|
|
1529
|
-
function getColorForLevel(level) {
|
|
1530
|
-
return LEVEL_COLORS[level];
|
|
1531
|
-
}
|
|
1532
|
-
function getColorForScore(score) {
|
|
1533
|
-
return getColorForLevel(getLevelFromScore(score));
|
|
1534
|
-
}
|
|
1535
|
-
function hasLevel(obj) {
|
|
1536
|
-
return typeof obj === "object" && obj !== null && "level" in obj && typeof obj.level === "string" && isValidLevel(obj.level);
|
|
1537
|
-
}
|
|
1538
|
-
function getEntityLevel(entity) {
|
|
1539
|
-
if ("direct_level" in entity) {
|
|
1540
|
-
const directLevel = entity.direct_level;
|
|
1541
|
-
if (typeof directLevel === "string" && isValidLevel(directLevel)) {
|
|
1542
|
-
return directLevel;
|
|
1543
|
-
}
|
|
1544
|
-
}
|
|
1545
|
-
if ("level" in entity && isValidLevel(entity.level)) {
|
|
1546
|
-
return entity.level;
|
|
1547
|
-
}
|
|
1548
|
-
throw new Error("Entity does not have a valid level.");
|
|
1549
|
-
}
|
|
1550
|
-
|
|
1551
|
-
// src/getters.ts
|
|
1686
|
+
/** Direction the judgment pushes: -1 exculpatory, 0 neutral, +1 inculpatory. */
|
|
1687
|
+
function verdictPolarity(verdict) {
|
|
1688
|
+
if (verdict === "SAFE") return -1;
|
|
1689
|
+
if (verdict === "INFO") return 0;
|
|
1690
|
+
return 1;
|
|
1691
|
+
}
|
|
1692
|
+
function isValidVerdict(value) {
|
|
1693
|
+
return typeof value === "string" && value in VERDICT_RANK;
|
|
1694
|
+
}
|
|
1695
|
+
function normalizeVerdict(value) {
|
|
1696
|
+
const upper = typeof value === "string" ? value.toUpperCase() : "";
|
|
1697
|
+
return isValidVerdict(upper) ? upper : "INFO";
|
|
1698
|
+
}
|
|
1699
|
+
function compareVerdicts(a, b) {
|
|
1700
|
+
return VERDICT_RANK[a] - VERDICT_RANK[b];
|
|
1701
|
+
}
|
|
1702
|
+
function isVerdictAtLeast(verdict, floor) {
|
|
1703
|
+
return VERDICT_RANK[verdict] >= VERDICT_RANK[floor];
|
|
1704
|
+
}
|
|
1705
|
+
function maxVerdict(verdicts) {
|
|
1706
|
+
return verdicts.reduce((best, current) => compareVerdicts(current, best) > 0 ? current : best, "SAFE");
|
|
1707
|
+
}
|
|
1708
|
+
function minVerdict(verdicts) {
|
|
1709
|
+
return verdicts.reduce((best, current) => compareVerdicts(current, best) < 0 ? current : best, "MALICIOUS");
|
|
1710
|
+
}
|
|
1711
|
+
function getColorForVerdict(verdict) {
|
|
1712
|
+
return VERDICT_HEX_COLORS[verdict] ?? VERDICT_HEX_COLORS.INFO;
|
|
1713
|
+
}
|
|
1714
|
+
/** Coarse bands for display; the engine works in floats. */
|
|
1715
|
+
function confidenceBand(confidence) {
|
|
1716
|
+
if (confidence < .5) return "low";
|
|
1717
|
+
if (confidence < .85) return "medium";
|
|
1718
|
+
return "high";
|
|
1719
|
+
}
|
|
1720
|
+
//#endregion
|
|
1721
|
+
//#region src/getters.ts
|
|
1552
1722
|
function getObservable(inv, key) {
|
|
1553
|
-
|
|
1723
|
+
return inv.facts?.observables?.[key];
|
|
1554
1724
|
}
|
|
1555
|
-
function
|
|
1556
|
-
|
|
1725
|
+
function getAllObservables(inv) {
|
|
1726
|
+
return inv.facts?.observables ?? {};
|
|
1557
1727
|
}
|
|
1558
1728
|
function getRootObservable(inv) {
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
return void 0;
|
|
1562
|
-
}
|
|
1563
|
-
const rootKey = generateObservableKey(rootType, "root");
|
|
1564
|
-
return inv.observables[rootKey];
|
|
1729
|
+
const rootKey = inv.header?.root_key;
|
|
1730
|
+
return rootKey ? getObservable(inv, rootKey) : void 0;
|
|
1565
1731
|
}
|
|
1566
|
-
function
|
|
1567
|
-
|
|
1732
|
+
function getRelation(inv, key) {
|
|
1733
|
+
return inv.facts?.relations?.[key];
|
|
1568
1734
|
}
|
|
1569
|
-
function
|
|
1570
|
-
|
|
1571
|
-
|
|
1735
|
+
function getAllRelations(inv) {
|
|
1736
|
+
return inv.facts?.relations ?? {};
|
|
1737
|
+
}
|
|
1738
|
+
function getThreatIntel(inv, key) {
|
|
1739
|
+
return inv.facts?.signals?.[key];
|
|
1740
|
+
}
|
|
1741
|
+
function getAllThreatIntels(inv) {
|
|
1742
|
+
return inv.facts?.signals ?? {};
|
|
1743
|
+
}
|
|
1744
|
+
/** Signals attached to one observable. */
|
|
1745
|
+
function getThreatIntelsFor(inv, observableKey) {
|
|
1746
|
+
return Object.values(getAllThreatIntels(inv)).filter((signal) => signal.subject_key === observableKey);
|
|
1747
|
+
}
|
|
1748
|
+
function getFinding(inv, key) {
|
|
1749
|
+
return inv.facts?.findings?.[key];
|
|
1572
1750
|
}
|
|
1573
1751
|
function getAllFindings(inv) {
|
|
1574
|
-
|
|
1752
|
+
return inv.facts?.findings ?? {};
|
|
1575
1753
|
}
|
|
1576
1754
|
function getEvidence(inv, key) {
|
|
1577
|
-
|
|
1755
|
+
return inv.facts?.evidences?.[key];
|
|
1578
1756
|
}
|
|
1579
1757
|
function getAllEvidences(inv) {
|
|
1580
|
-
|
|
1758
|
+
return inv.facts?.evidences ?? {};
|
|
1581
1759
|
}
|
|
1582
|
-
|
|
1583
|
-
|
|
1760
|
+
/** The v6 `Enrichment` is just an evidence type; filter instead of a dedicated accessor. */
|
|
1761
|
+
function getEvidencesByType(inv, evidenceType) {
|
|
1762
|
+
return Object.values(getAllEvidences(inv)).filter((evidence) => evidence.evidence_type === evidenceType);
|
|
1584
1763
|
}
|
|
1585
|
-
function
|
|
1586
|
-
|
|
1587
|
-
for (const ti of Object.values(inv.threat_intels)) {
|
|
1588
|
-
if (ti.source.toLowerCase() === normalizedSource && ti.observable_key === observableKey) {
|
|
1589
|
-
return ti;
|
|
1590
|
-
}
|
|
1591
|
-
}
|
|
1592
|
-
return void 0;
|
|
1764
|
+
function getDecision(inv, key) {
|
|
1765
|
+
return inv.decisions?.[key];
|
|
1593
1766
|
}
|
|
1594
|
-
function
|
|
1595
|
-
|
|
1767
|
+
function getAllDecisions(inv) {
|
|
1768
|
+
return inv.decisions ?? {};
|
|
1769
|
+
}
|
|
1770
|
+
/** The single stance standing on a target — the key carries the target alone. */
|
|
1771
|
+
function getDecisionFor(inv, targetKey) {
|
|
1772
|
+
return getDecision(inv, generateDecisionKey(targetKey));
|
|
1773
|
+
}
|
|
1774
|
+
function hasKind(inv, targetKey, kind) {
|
|
1775
|
+
return getDecisionFor(inv, targetKey)?.kind === kind;
|
|
1776
|
+
}
|
|
1777
|
+
function isAllowlisted(inv, observableKey) {
|
|
1778
|
+
return hasKind(inv, observableKey, "REFUTE");
|
|
1596
1779
|
}
|
|
1597
|
-
function
|
|
1598
|
-
|
|
1780
|
+
function isBlocklisted(inv, observableKey) {
|
|
1781
|
+
return hasKind(inv, observableKey, "UPHOLD");
|
|
1599
1782
|
}
|
|
1600
|
-
function
|
|
1601
|
-
|
|
1602
|
-
for (const enr of Object.values(inv.enrichments)) {
|
|
1603
|
-
if (enr.name.toLowerCase() === normalizedName) {
|
|
1604
|
-
return enr;
|
|
1605
|
-
}
|
|
1606
|
-
}
|
|
1607
|
-
return void 0;
|
|
1783
|
+
function isConfirmed(inv, findingKey) {
|
|
1784
|
+
return hasKind(inv, findingKey, "UPHOLD");
|
|
1608
1785
|
}
|
|
1609
|
-
function
|
|
1610
|
-
|
|
1786
|
+
function isDismissed(inv, findingKey) {
|
|
1787
|
+
return hasKind(inv, findingKey, "REFUTE");
|
|
1788
|
+
}
|
|
1789
|
+
/** True when a stance was withdrawn and the computed value applies again. */
|
|
1790
|
+
function isVacated(inv, targetKey) {
|
|
1791
|
+
return hasKind(inv, targetKey, "VACATED");
|
|
1792
|
+
}
|
|
1793
|
+
/**
|
|
1794
|
+
* The analyst's word for a stance, rebuilt from the intent and the family of its target.
|
|
1795
|
+
*
|
|
1796
|
+
* The model carries one axis on purpose; the vocabulary that reads naturally carries two, and
|
|
1797
|
+
* the display layer is the right place to pay for that.
|
|
1798
|
+
*/
|
|
1799
|
+
function decisionLabel(decision) {
|
|
1800
|
+
if (decision.kind === "VACATED") return "VACATED";
|
|
1801
|
+
const observable = decision.target_key.startsWith("obs:");
|
|
1802
|
+
if (decision.kind === "UPHOLD") return observable ? "BLOCKLISTED" : "CONFIRMED";
|
|
1803
|
+
return observable ? "ALLOWLISTED" : "DISMISSED";
|
|
1611
1804
|
}
|
|
1612
1805
|
function getTag(inv, key) {
|
|
1613
|
-
|
|
1806
|
+
return inv.tags?.[key];
|
|
1614
1807
|
}
|
|
1615
1808
|
function getTagByName(inv, name) {
|
|
1616
|
-
|
|
1617
|
-
return inv.tags[key];
|
|
1809
|
+
return getTag(inv, `tag:${name.trim().toLowerCase()}`);
|
|
1618
1810
|
}
|
|
1619
1811
|
function getAllTags(inv) {
|
|
1620
|
-
|
|
1812
|
+
return inv.tags ?? {};
|
|
1621
1813
|
}
|
|
1622
|
-
function
|
|
1623
|
-
|
|
1814
|
+
function getInvestigationResult(inv) {
|
|
1815
|
+
return inv.report.investigation;
|
|
1624
1816
|
}
|
|
1625
|
-
function
|
|
1626
|
-
|
|
1817
|
+
function getObservableResult(inv, observableKey) {
|
|
1818
|
+
return inv.report.observables?.[observableKey];
|
|
1627
1819
|
}
|
|
1628
|
-
function
|
|
1629
|
-
|
|
1820
|
+
function getFindingResult(inv, findingKey) {
|
|
1821
|
+
return inv.report.findings?.[findingKey];
|
|
1630
1822
|
}
|
|
1631
|
-
function
|
|
1632
|
-
|
|
1823
|
+
function getObservableScore(inv, observableKey) {
|
|
1824
|
+
return getObservableResult(inv, observableKey)?.score ?? 0;
|
|
1633
1825
|
}
|
|
1634
|
-
function
|
|
1635
|
-
|
|
1636
|
-
observables: Object.keys(inv.observables).length,
|
|
1637
|
-
findings: getAllFindings(inv).length,
|
|
1638
|
-
evidences: getAllEvidences(inv).length,
|
|
1639
|
-
threatIntels: Object.keys(inv.threat_intels).length,
|
|
1640
|
-
enrichments: Object.keys(inv.enrichments).length,
|
|
1641
|
-
tags: getAllTags(inv).length,
|
|
1642
|
-
whitelists: inv.whitelists.length
|
|
1643
|
-
};
|
|
1644
|
-
}
|
|
1645
|
-
function getStartedAt(inv) {
|
|
1646
|
-
const event = inv.audit_log?.find(
|
|
1647
|
-
(e) => e.event_type === "INVESTIGATION_STARTED"
|
|
1648
|
-
);
|
|
1649
|
-
return event?.timestamp;
|
|
1650
|
-
}
|
|
1651
|
-
function getTagChildren(inv, tagName) {
|
|
1652
|
-
return Object.values(inv.tags).filter((tag) => isTagChildOf(tag.name, tagName));
|
|
1653
|
-
}
|
|
1654
|
-
function getTagDescendants(inv, tagName) {
|
|
1655
|
-
const prefix = tagName + ":";
|
|
1656
|
-
return Object.values(inv.tags).filter((tag) => tag.name.startsWith(prefix));
|
|
1826
|
+
function getObservableVerdict(inv, observableKey) {
|
|
1827
|
+
return getObservableResult(inv, observableKey)?.verdict ?? "INFO";
|
|
1657
1828
|
}
|
|
1829
|
+
function getFindingScore(inv, findingKey) {
|
|
1830
|
+
return getFindingResult(inv, findingKey)?.score ?? 0;
|
|
1831
|
+
}
|
|
1832
|
+
function getFindingVerdict(inv, findingKey) {
|
|
1833
|
+
return getFindingResult(inv, findingKey)?.verdict ?? "INFO";
|
|
1834
|
+
}
|
|
1835
|
+
function getGlobalScore(inv) {
|
|
1836
|
+
return inv.report.investigation.score ?? 0;
|
|
1837
|
+
}
|
|
1838
|
+
function getGlobalVerdict(inv) {
|
|
1839
|
+
return inv.report.investigation.verdict ?? "INFO";
|
|
1840
|
+
}
|
|
1841
|
+
/** True when a decision or a stronger link overrode the computed value. */
|
|
1842
|
+
function wasSuppressed(inv, key) {
|
|
1843
|
+
const finding = getFindingResult(inv, key);
|
|
1844
|
+
if (finding) return Boolean(finding.suppressed_by_decision || finding.own_term_suppressed);
|
|
1845
|
+
return Boolean(getObservableResult(inv, key)?.suppressed_by_decision);
|
|
1846
|
+
}
|
|
1847
|
+
/** Findings a tag points at, plus those of every descendant tag. */
|
|
1848
|
+
function getTagFindingKeys(inv, tagName) {
|
|
1849
|
+
const prefix = `${tagName}:`;
|
|
1850
|
+
const keys = /* @__PURE__ */ new Set();
|
|
1851
|
+
for (const tag of Object.values(getAllTags(inv))) if (tag.name === tagName || tag.name.startsWith(prefix)) for (const key of tag.finding_keys ?? []) keys.add(key);
|
|
1852
|
+
return [...keys];
|
|
1853
|
+
}
|
|
1854
|
+
/**
|
|
1855
|
+
* A tag's aggregated score.
|
|
1856
|
+
*
|
|
1857
|
+
* This sums values the engine produced; it does not re-derive them. Uncounted findings — not
|
|
1858
|
+
* applicable, pending or dismissed — are absent from the sum *and* from any ratio built on it.
|
|
1859
|
+
*/
|
|
1658
1860
|
function getTagAggregatedScore(inv, tagName) {
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1861
|
+
return getTagFindingKeys(inv, tagName).reduce((total, key) => {
|
|
1862
|
+
const result = getFindingResult(inv, key);
|
|
1863
|
+
return result?.counted ? total + (result.score ?? 0) : total;
|
|
1864
|
+
}, 0);
|
|
1865
|
+
}
|
|
1866
|
+
function getCounts(inv) {
|
|
1867
|
+
return {
|
|
1868
|
+
observables: Object.keys(getAllObservables(inv)).length,
|
|
1869
|
+
relations: Object.keys(getAllRelations(inv)).length,
|
|
1870
|
+
signals: Object.keys(getAllThreatIntels(inv)).length,
|
|
1871
|
+
evidences: Object.keys(getAllEvidences(inv)).length,
|
|
1872
|
+
findings: Object.keys(getAllFindings(inv)).length,
|
|
1873
|
+
evaluatedFindings: Object.values(inv.report.findings ?? {}).filter((result) => result.counted).length,
|
|
1874
|
+
decisions: Object.keys(getAllDecisions(inv)).length,
|
|
1875
|
+
tags: Object.keys(getAllTags(inv)).length
|
|
1876
|
+
};
|
|
1877
|
+
}
|
|
1878
|
+
//#endregion
|
|
1879
|
+
//#region src/finders.ts
|
|
1880
|
+
/**
|
|
1881
|
+
* Query helpers.
|
|
1882
|
+
*
|
|
1883
|
+
* Filters on computed values go through the report; filters on stated values go through the
|
|
1884
|
+
* facts. Keeping the two apart is what stops a UI from quietly inventing its own scoring.
|
|
1885
|
+
*/
|
|
1675
1886
|
function findObservablesByType(inv, type) {
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
}
|
|
1681
|
-
function
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
function findObservablesAtLeast(inv, minLevel2) {
|
|
1685
|
-
return Object.values(inv.observables).filter(
|
|
1686
|
-
(obs) => isLevelAtLeast(obs.level, minLevel2)
|
|
1687
|
-
);
|
|
1688
|
-
}
|
|
1689
|
-
function findObservablesByValue(inv, value, caseSensitive = false) {
|
|
1690
|
-
const searchValue = caseSensitive ? value : value.toLowerCase();
|
|
1691
|
-
return Object.values(inv.observables).filter((obs) => {
|
|
1692
|
-
const obsValue = caseSensitive ? obs.value : obs.value.toLowerCase();
|
|
1693
|
-
return obsValue === searchValue;
|
|
1694
|
-
});
|
|
1695
|
-
}
|
|
1696
|
-
function findObservablesContaining(inv, substring, caseSensitive = false) {
|
|
1697
|
-
const searchStr = caseSensitive ? substring : substring.toLowerCase();
|
|
1698
|
-
return Object.values(inv.observables).filter((obs) => {
|
|
1699
|
-
const obsValue = caseSensitive ? obs.value : obs.value.toLowerCase();
|
|
1700
|
-
return obsValue.includes(searchStr);
|
|
1701
|
-
});
|
|
1702
|
-
}
|
|
1703
|
-
function findObservablesMatching(inv, pattern) {
|
|
1704
|
-
return Object.values(inv.observables).filter((obs) => pattern.test(obs.value));
|
|
1887
|
+
return Object.values(getAllObservables(inv)).filter((observable) => observable.type === type);
|
|
1888
|
+
}
|
|
1889
|
+
function findObservablesByValue(inv, value) {
|
|
1890
|
+
return Object.values(getAllObservables(inv)).filter((observable) => observable.value === value);
|
|
1891
|
+
}
|
|
1892
|
+
function findObservablesContaining(inv, fragment) {
|
|
1893
|
+
const needle = fragment.toLowerCase();
|
|
1894
|
+
return Object.values(getAllObservables(inv)).filter((observable) => observable.value.toLowerCase().includes(needle));
|
|
1705
1895
|
}
|
|
1706
1896
|
function findInternalObservables(inv) {
|
|
1707
|
-
|
|
1897
|
+
return Object.values(getAllObservables(inv)).filter((observable) => observable.internal === true);
|
|
1708
1898
|
}
|
|
1709
1899
|
function findExternalObservables(inv) {
|
|
1710
|
-
|
|
1900
|
+
return Object.values(getAllObservables(inv)).filter((observable) => observable.internal === false);
|
|
1711
1901
|
}
|
|
1712
|
-
function
|
|
1713
|
-
|
|
1902
|
+
function findObservablesByVerdict(inv, verdict) {
|
|
1903
|
+
return Object.values(getAllObservables(inv)).filter((observable) => getObservableResult(inv, observable.key)?.verdict === verdict);
|
|
1904
|
+
}
|
|
1905
|
+
function findObservablesAtLeast(inv, floor) {
|
|
1906
|
+
return Object.values(getAllObservables(inv)).filter((observable) => {
|
|
1907
|
+
const result = getObservableResult(inv, observable.key);
|
|
1908
|
+
return result ? isVerdictAtLeast(result.verdict, floor) : false;
|
|
1909
|
+
});
|
|
1714
1910
|
}
|
|
1715
1911
|
function findObservablesWithThreatIntel(inv) {
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1912
|
+
const subjects = new Set(Object.values(getAllThreatIntels(inv)).map((signal) => signal.subject_key));
|
|
1913
|
+
return Object.values(getAllObservables(inv)).filter((observable) => subjects.has(observable.key));
|
|
1914
|
+
}
|
|
1915
|
+
function findFindingsByVerdict(inv, verdict) {
|
|
1916
|
+
return Object.values(getAllFindings(inv)).filter((finding) => getFindingResult(inv, finding.key)?.verdict === verdict);
|
|
1719
1917
|
}
|
|
1720
|
-
function
|
|
1721
|
-
|
|
1918
|
+
function findFindingsAtLeast(inv, floor) {
|
|
1919
|
+
return Object.values(getAllFindings(inv)).filter((finding) => {
|
|
1920
|
+
const result = getFindingResult(inv, finding.key);
|
|
1921
|
+
return result?.counted ? isVerdictAtLeast(result.verdict, floor) : false;
|
|
1922
|
+
});
|
|
1722
1923
|
}
|
|
1723
|
-
function
|
|
1724
|
-
|
|
1725
|
-
(finding) => isLevelAtLeast(finding.level, minLevel2)
|
|
1726
|
-
);
|
|
1924
|
+
function findFindingsByRule(inv, ruleId) {
|
|
1925
|
+
return Object.values(getAllFindings(inv)).filter((finding) => finding.rule_id === ruleId);
|
|
1727
1926
|
}
|
|
1728
|
-
function
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1927
|
+
function findFindingsByConfidence(inv, band) {
|
|
1928
|
+
return Object.values(getAllFindings(inv)).filter((finding) => {
|
|
1929
|
+
const result = getFindingResult(inv, finding.key);
|
|
1930
|
+
return result ? confidenceBand(result.confidence ?? 1) === band : false;
|
|
1931
|
+
});
|
|
1932
|
+
}
|
|
1933
|
+
/** Findings excluded from the score but still worth showing, with their reason. */
|
|
1934
|
+
function findUncountedFindings(inv) {
|
|
1935
|
+
return Object.values(getAllFindings(inv)).filter((finding) => getFindingResult(inv, finding.key)?.counted === false);
|
|
1936
|
+
}
|
|
1937
|
+
/** Findings whose own claim was outweighed by one of their observables — worth surfacing. */
|
|
1938
|
+
function findContradictedFindings(inv) {
|
|
1939
|
+
return Object.values(getAllFindings(inv)).filter((finding) => getFindingResult(inv, finding.key)?.own_term_suppressed === true);
|
|
1733
1940
|
}
|
|
1734
1941
|
function findThreatIntelBySource(inv, source) {
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
}
|
|
1740
|
-
function
|
|
1741
|
-
|
|
1742
|
-
}
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
function
|
|
1757
|
-
|
|
1758
|
-
}
|
|
1759
|
-
|
|
1760
|
-
const result = [];
|
|
1761
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1762
|
-
const observable = inv.observables[observableKey];
|
|
1763
|
-
if (observable) {
|
|
1764
|
-
for (const findingKey of observable.finding_links) {
|
|
1765
|
-
const finding = inv.findings[findingKey];
|
|
1766
|
-
if (finding && !seen.has(finding.key)) {
|
|
1767
|
-
result.push(finding);
|
|
1768
|
-
seen.add(finding.key);
|
|
1769
|
-
}
|
|
1770
|
-
}
|
|
1771
|
-
}
|
|
1772
|
-
for (const finding of Object.values(inv.findings)) {
|
|
1773
|
-
if (seen.has(finding.key)) {
|
|
1774
|
-
continue;
|
|
1775
|
-
}
|
|
1776
|
-
if (finding.observable_links.some((link) => link.observable_key === observableKey)) {
|
|
1777
|
-
result.push(finding);
|
|
1778
|
-
seen.add(finding.key);
|
|
1779
|
-
}
|
|
1780
|
-
}
|
|
1781
|
-
return result;
|
|
1782
|
-
}
|
|
1783
|
-
function findThreatIntelsForObservable(inv, observableKey) {
|
|
1784
|
-
const observable = inv.observables[observableKey];
|
|
1785
|
-
if (observable) {
|
|
1786
|
-
return observable.threat_intels.map((tiKey) => inv.threat_intels[tiKey]).filter((ti) => ti !== void 0);
|
|
1787
|
-
}
|
|
1788
|
-
return Object.values(inv.threat_intels).filter(
|
|
1789
|
-
(ti) => ti.observable_key === observableKey
|
|
1790
|
-
);
|
|
1791
|
-
}
|
|
1792
|
-
function findObservablesForFinding(inv, findingKey) {
|
|
1793
|
-
const finding = inv.findings[findingKey];
|
|
1794
|
-
if (finding) {
|
|
1795
|
-
const keys = /* @__PURE__ */ new Set();
|
|
1796
|
-
for (const link of finding.observable_links) {
|
|
1797
|
-
keys.add(link.observable_key);
|
|
1798
|
-
}
|
|
1799
|
-
return Array.from(keys).map((obsKey) => inv.observables[obsKey]).filter((obs) => obs !== void 0);
|
|
1800
|
-
}
|
|
1801
|
-
return [];
|
|
1802
|
-
}
|
|
1803
|
-
function findFindingsForTag(inv, tagKey, recursive = false) {
|
|
1804
|
-
const result = [];
|
|
1805
|
-
const tag = inv.tags[tagKey];
|
|
1806
|
-
if (!tag) {
|
|
1807
|
-
return result;
|
|
1808
|
-
}
|
|
1809
|
-
for (const findingKey of tag.findings) {
|
|
1810
|
-
const finding = inv.findings[findingKey];
|
|
1811
|
-
if (finding) {
|
|
1812
|
-
result.push(finding);
|
|
1813
|
-
}
|
|
1814
|
-
}
|
|
1815
|
-
if (recursive) {
|
|
1816
|
-
const prefix = tag.name + ":";
|
|
1817
|
-
for (const otherTag of Object.values(inv.tags)) {
|
|
1818
|
-
if (otherTag.name.startsWith(prefix)) {
|
|
1819
|
-
for (const findingKey of otherTag.findings) {
|
|
1820
|
-
const finding = inv.findings[findingKey];
|
|
1821
|
-
if (finding) {
|
|
1822
|
-
result.push(finding);
|
|
1823
|
-
}
|
|
1824
|
-
}
|
|
1825
|
-
}
|
|
1826
|
-
}
|
|
1827
|
-
}
|
|
1828
|
-
return result;
|
|
1829
|
-
}
|
|
1830
|
-
function sortObservablesByScore(observables) {
|
|
1831
|
-
return [...observables].sort((a, b) => b.score - a.score);
|
|
1832
|
-
}
|
|
1833
|
-
function sortFindingsByScore(findings) {
|
|
1834
|
-
return [...findings].sort((a, b) => b.score - a.score);
|
|
1835
|
-
}
|
|
1836
|
-
function sortObservablesByLevel(observables) {
|
|
1837
|
-
return [...observables].sort(
|
|
1838
|
-
(a, b) => LEVEL_VALUES[b.level] - LEVEL_VALUES[a.level]
|
|
1839
|
-
);
|
|
1840
|
-
}
|
|
1841
|
-
function sortFindingsByLevel(findings) {
|
|
1842
|
-
return [...findings].sort(
|
|
1843
|
-
(a, b) => LEVEL_VALUES[b.level] - LEVEL_VALUES[a.level]
|
|
1844
|
-
);
|
|
1845
|
-
}
|
|
1846
|
-
function findHighestScoringObservables(inv, n = 10) {
|
|
1847
|
-
return sortObservablesByScore(Object.values(inv.observables)).slice(0, n);
|
|
1848
|
-
}
|
|
1849
|
-
function findHighestScoringFindings(inv, n = 10) {
|
|
1850
|
-
return sortFindingsByScore(Object.values(inv.findings)).slice(0, n);
|
|
1851
|
-
}
|
|
1852
|
-
function findMaliciousObservables(inv) {
|
|
1853
|
-
return findObservablesByLevel(inv, "MALICIOUS");
|
|
1854
|
-
}
|
|
1855
|
-
function findSuspiciousObservables(inv) {
|
|
1856
|
-
return findObservablesByLevel(inv, "SUSPICIOUS");
|
|
1857
|
-
}
|
|
1858
|
-
function findMaliciousFindings(inv) {
|
|
1859
|
-
return findFindingsByLevel(inv, "MALICIOUS");
|
|
1860
|
-
}
|
|
1861
|
-
function findSuspiciousFindings(inv) {
|
|
1862
|
-
return findFindingsByLevel(inv, "SUSPICIOUS");
|
|
1863
|
-
}
|
|
1864
|
-
function getAllFindingKeys(inv) {
|
|
1865
|
-
return Object.keys(inv.findings);
|
|
1866
|
-
}
|
|
1867
|
-
function getAllObservableTypes(inv) {
|
|
1868
|
-
const types = /* @__PURE__ */ new Set();
|
|
1869
|
-
for (const obs of Object.values(inv.observables)) {
|
|
1870
|
-
types.add(obs.type);
|
|
1871
|
-
}
|
|
1872
|
-
return Array.from(types);
|
|
1873
|
-
}
|
|
1874
|
-
function getAllThreatIntelSources(inv) {
|
|
1875
|
-
const sources = /* @__PURE__ */ new Set();
|
|
1876
|
-
for (const ti of Object.values(inv.threat_intels)) {
|
|
1877
|
-
sources.add(ti.source);
|
|
1878
|
-
}
|
|
1879
|
-
return Array.from(sources);
|
|
1880
|
-
}
|
|
1881
|
-
|
|
1882
|
-
// src/graph.ts
|
|
1883
|
-
function getRelatedObservables(inv, observableKey) {
|
|
1884
|
-
const observable = inv.observables[observableKey];
|
|
1885
|
-
if (!observable) {
|
|
1886
|
-
return [];
|
|
1887
|
-
}
|
|
1888
|
-
const relatedKeys = /* @__PURE__ */ new Set();
|
|
1889
|
-
for (const rel of observable.relationships) {
|
|
1890
|
-
relatedKeys.add(rel.target_key);
|
|
1891
|
-
}
|
|
1892
|
-
for (const [key, obs] of Object.entries(inv.observables)) {
|
|
1893
|
-
if (key === observableKey) continue;
|
|
1894
|
-
for (const rel of obs.relationships) {
|
|
1895
|
-
if (rel.target_key === observableKey) {
|
|
1896
|
-
relatedKeys.add(key);
|
|
1897
|
-
break;
|
|
1898
|
-
}
|
|
1899
|
-
}
|
|
1900
|
-
}
|
|
1901
|
-
return Array.from(relatedKeys).map((key) => inv.observables[key]).filter((obs) => obs !== void 0);
|
|
1942
|
+
return Object.values(getAllThreatIntels(inv)).filter((signal) => signal.source.name === source);
|
|
1943
|
+
}
|
|
1944
|
+
function findThreatIntelByVerdict(inv, verdict) {
|
|
1945
|
+
return Object.values(getAllThreatIntels(inv)).filter((signal) => signal.verdict === verdict);
|
|
1946
|
+
}
|
|
1947
|
+
function findThreatIntelForObservable(inv, observableKey) {
|
|
1948
|
+
return Object.values(getAllThreatIntels(inv)).filter((signal) => signal.subject_key === observableKey);
|
|
1949
|
+
}
|
|
1950
|
+
//#endregion
|
|
1951
|
+
//#region src/graph.ts
|
|
1952
|
+
/**
|
|
1953
|
+
* Graph traversal.
|
|
1954
|
+
*
|
|
1955
|
+
* Relations are standalone facts in v7, each carrying both keys, so there is no per-observable
|
|
1956
|
+
* adjacency list to walk and no `direction` field to interpret: `source_key` is the parent,
|
|
1957
|
+
* `target_key` the child, and the kind implies the rest.
|
|
1958
|
+
*/
|
|
1959
|
+
function relationsOf(inv) {
|
|
1960
|
+
return Object.values(getAllRelations(inv));
|
|
1961
|
+
}
|
|
1962
|
+
/** Keys of relations the report credits with an actual contribution. */
|
|
1963
|
+
function scoringRelationKeys(inv) {
|
|
1964
|
+
const keys = /* @__PURE__ */ new Set();
|
|
1965
|
+
for (const result of Object.values(inv.report.observables ?? {})) for (const contribution of result.contributions ?? []) if (contribution.retained !== false && contribution.source_key.startsWith("rel:")) keys.add(contribution.source_key);
|
|
1966
|
+
return keys;
|
|
1902
1967
|
}
|
|
1903
1968
|
function getObservableChildren(inv, observableKey) {
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
return [];
|
|
1907
|
-
}
|
|
1908
|
-
return observable.relationships.filter((rel) => rel.direction === "outbound" || rel.direction === "bidirectional").map((rel) => inv.observables[rel.target_key]).filter((obs) => obs !== void 0);
|
|
1969
|
+
const observables = getAllObservables(inv);
|
|
1970
|
+
return relationsOf(inv).filter((relation) => relation.source_key === observableKey).map((relation) => observables[relation.target_key]).filter((observable) => observable !== void 0);
|
|
1909
1971
|
}
|
|
1910
1972
|
function getObservableParents(inv, observableKey) {
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
if (key === observableKey) continue;
|
|
1914
|
-
for (const rel of obs.relationships) {
|
|
1915
|
-
if (rel.target_key === observableKey && (rel.direction === "outbound" || rel.direction === "bidirectional")) {
|
|
1916
|
-
parents.push(obs);
|
|
1917
|
-
break;
|
|
1918
|
-
}
|
|
1919
|
-
}
|
|
1920
|
-
}
|
|
1921
|
-
return parents;
|
|
1922
|
-
}
|
|
1923
|
-
function getRelatedObservablesByType(inv, observableKey, relationshipType) {
|
|
1924
|
-
const observable = inv.observables[observableKey];
|
|
1925
|
-
if (!observable) {
|
|
1926
|
-
return [];
|
|
1927
|
-
}
|
|
1928
|
-
const normalizedType = relationshipType.toLowerCase();
|
|
1929
|
-
const relatedKeys = /* @__PURE__ */ new Set();
|
|
1930
|
-
for (const rel of observable.relationships) {
|
|
1931
|
-
if (rel.relationship_type.toLowerCase() === normalizedType) {
|
|
1932
|
-
relatedKeys.add(rel.target_key);
|
|
1933
|
-
}
|
|
1934
|
-
}
|
|
1935
|
-
for (const [key, obs] of Object.entries(inv.observables)) {
|
|
1936
|
-
if (key === observableKey) continue;
|
|
1937
|
-
for (const rel of obs.relationships) {
|
|
1938
|
-
if (rel.target_key === observableKey && rel.relationship_type.toLowerCase() === normalizedType) {
|
|
1939
|
-
relatedKeys.add(key);
|
|
1940
|
-
break;
|
|
1941
|
-
}
|
|
1942
|
-
}
|
|
1943
|
-
}
|
|
1944
|
-
return Array.from(relatedKeys).map((key) => inv.observables[key]).filter((obs) => obs !== void 0);
|
|
1945
|
-
}
|
|
1946
|
-
function getRelatedObservablesByDirection(inv, observableKey, direction) {
|
|
1947
|
-
const observable = inv.observables[observableKey];
|
|
1948
|
-
if (!observable) {
|
|
1949
|
-
return [];
|
|
1950
|
-
}
|
|
1951
|
-
const relatedKeys = /* @__PURE__ */ new Set();
|
|
1952
|
-
if (direction === "outbound" || direction === "bidirectional") {
|
|
1953
|
-
for (const rel of observable.relationships) {
|
|
1954
|
-
if (rel.direction === direction || rel.direction === "bidirectional") {
|
|
1955
|
-
relatedKeys.add(rel.target_key);
|
|
1956
|
-
}
|
|
1957
|
-
}
|
|
1958
|
-
}
|
|
1959
|
-
if (direction === "inbound" || direction === "bidirectional") {
|
|
1960
|
-
for (const [key, obs] of Object.entries(inv.observables)) {
|
|
1961
|
-
if (key === observableKey) continue;
|
|
1962
|
-
for (const rel of obs.relationships) {
|
|
1963
|
-
if (rel.target_key === observableKey && (rel.direction === "outbound" || rel.direction === "bidirectional")) {
|
|
1964
|
-
relatedKeys.add(key);
|
|
1965
|
-
break;
|
|
1966
|
-
}
|
|
1967
|
-
}
|
|
1968
|
-
}
|
|
1969
|
-
}
|
|
1970
|
-
return Array.from(relatedKeys).map((key) => inv.observables[key]).filter((obs) => obs !== void 0);
|
|
1973
|
+
const observables = getAllObservables(inv);
|
|
1974
|
+
return relationsOf(inv).filter((relation) => relation.target_key === observableKey).map((relation) => observables[relation.source_key]).filter((observable) => observable !== void 0);
|
|
1971
1975
|
}
|
|
1972
|
-
function
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
for (const [key, obs] of Object.entries(inv.observables)) {
|
|
1977
|
-
nodes.push({
|
|
1978
|
-
id: key,
|
|
1979
|
-
type: obs.type,
|
|
1980
|
-
value: obs.value,
|
|
1981
|
-
level: obs.level,
|
|
1982
|
-
score: obs.score,
|
|
1983
|
-
internal: obs.internal,
|
|
1984
|
-
whitelisted: obs.whitelisted
|
|
1985
|
-
});
|
|
1986
|
-
for (const rel of obs.relationships) {
|
|
1987
|
-
const edgeKey = rel.direction === "bidirectional" ? [key, rel.target_key].sort().join("--") : `${key}--${rel.target_key}`;
|
|
1988
|
-
if (!seenEdges.has(edgeKey)) {
|
|
1989
|
-
seenEdges.add(edgeKey);
|
|
1990
|
-
edges.push({
|
|
1991
|
-
source: key,
|
|
1992
|
-
target: rel.target_key,
|
|
1993
|
-
type: rel.relationship_type,
|
|
1994
|
-
direction: rel.direction
|
|
1995
|
-
});
|
|
1996
|
-
}
|
|
1997
|
-
}
|
|
1998
|
-
}
|
|
1999
|
-
return { nodes, edges };
|
|
1976
|
+
function getRelatedObservables(inv, observableKey) {
|
|
1977
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1978
|
+
for (const observable of [...getObservableChildren(inv, observableKey), ...getObservableParents(inv, observableKey)]) seen.set(observable.key, observable);
|
|
1979
|
+
return [...seen.values()];
|
|
2000
1980
|
}
|
|
2001
|
-
function
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
1981
|
+
function getRelationsForObservable(inv, observableKey) {
|
|
1982
|
+
return relationsOf(inv).filter((relation) => relation.source_key === observableKey || relation.target_key === observableKey);
|
|
1983
|
+
}
|
|
1984
|
+
function countRelationsByKind(inv) {
|
|
1985
|
+
const counts = {};
|
|
1986
|
+
for (const relation of relationsOf(inv)) counts[relation.kind ?? "related-to"] = (counts[relation.kind ?? "related-to"] ?? 0) + 1;
|
|
1987
|
+
return counts;
|
|
1988
|
+
}
|
|
1989
|
+
function getObservableGraph(inv) {
|
|
1990
|
+
const scoring = scoringRelationKeys(inv);
|
|
1991
|
+
return {
|
|
1992
|
+
nodes: Object.values(getAllObservables(inv)).map((observable) => {
|
|
1993
|
+
const result = getObservableResult(inv, observable.key);
|
|
1994
|
+
return {
|
|
1995
|
+
key: observable.key,
|
|
1996
|
+
observable,
|
|
1997
|
+
score: result?.score ?? 0,
|
|
1998
|
+
verdict: result?.verdict ?? "INFO"
|
|
1999
|
+
};
|
|
2000
|
+
}),
|
|
2001
|
+
edges: relationsOf(inv).map((relation) => ({
|
|
2002
|
+
key: relation.key,
|
|
2003
|
+
source: relation.source_key,
|
|
2004
|
+
target: relation.target_key,
|
|
2005
|
+
kind: relation.kind ?? "related-to",
|
|
2006
|
+
confidence: relation.confidence ?? 1,
|
|
2007
|
+
carriedScore: scoring.has(relation.key)
|
|
2008
|
+
}))
|
|
2009
|
+
};
|
|
2010
|
+
}
|
|
2011
|
+
function areConnected(inv, a, b) {
|
|
2012
|
+
return getReachableObservables(inv, a).has(b);
|
|
2013
|
+
}
|
|
2014
|
+
/** Undirected reachability — the same walk `finalize_relationships` uses to spot orphans. */
|
|
2015
|
+
function getReachableObservables(inv, start) {
|
|
2016
|
+
const relations = relationsOf(inv);
|
|
2017
|
+
const reached = /* @__PURE__ */ new Set([start]);
|
|
2018
|
+
const queue = [start];
|
|
2019
|
+
while (queue.length > 0) {
|
|
2020
|
+
const current = queue.shift();
|
|
2021
|
+
for (const relation of relations) {
|
|
2022
|
+
const neighbour = relation.source_key === current ? relation.target_key : relation.target_key === current ? relation.source_key : void 0;
|
|
2023
|
+
if (neighbour && !reached.has(neighbour)) {
|
|
2024
|
+
reached.add(neighbour);
|
|
2025
|
+
queue.push(neighbour);
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
return reached;
|
|
2013
2030
|
}
|
|
2014
2031
|
function findOrphanObservables(inv) {
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
}
|
|
2022
|
-
}
|
|
2023
|
-
}
|
|
2024
|
-
return Object.values(inv.observables).filter(
|
|
2025
|
-
(obs) => !connectedKeys.has(obs.key)
|
|
2026
|
-
);
|
|
2032
|
+
const linked = /* @__PURE__ */ new Set();
|
|
2033
|
+
for (const relation of relationsOf(inv)) {
|
|
2034
|
+
linked.add(relation.source_key);
|
|
2035
|
+
linked.add(relation.target_key);
|
|
2036
|
+
}
|
|
2037
|
+
return Object.values(getAllObservables(inv)).filter((observable) => !linked.has(observable.key));
|
|
2027
2038
|
}
|
|
2028
2039
|
function findLeafObservables(inv) {
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
outbound,
|
|
2135
|
-
inbound,
|
|
2136
|
-
all: [
|
|
2137
|
-
...outbound,
|
|
2138
|
-
...inbound
|
|
2139
|
-
]
|
|
2140
|
-
};
|
|
2141
|
-
}
|
|
2142
|
-
// Annotate the CommonJS export names for ESM import in node:
|
|
2143
|
-
0 && (module.exports = {
|
|
2144
|
-
LEVEL_COLORS,
|
|
2145
|
-
LEVEL_ORDER,
|
|
2146
|
-
LEVEL_VALUES,
|
|
2147
|
-
areConnected,
|
|
2148
|
-
compareLevels,
|
|
2149
|
-
countRelationshipsByType,
|
|
2150
|
-
findExternalObservables,
|
|
2151
|
-
findFindingByName,
|
|
2152
|
-
findFindingsAtLeast,
|
|
2153
|
-
findFindingsByLevel,
|
|
2154
|
-
findFindingsForObservable,
|
|
2155
|
-
findFindingsForTag,
|
|
2156
|
-
findHighestScoringFindings,
|
|
2157
|
-
findHighestScoringObservables,
|
|
2158
|
-
findInternalObservables,
|
|
2159
|
-
findLeafObservables,
|
|
2160
|
-
findMaliciousFindings,
|
|
2161
|
-
findMaliciousObservables,
|
|
2162
|
-
findObservablesAtLeast,
|
|
2163
|
-
findObservablesByLevel,
|
|
2164
|
-
findObservablesByType,
|
|
2165
|
-
findObservablesByValue,
|
|
2166
|
-
findObservablesContaining,
|
|
2167
|
-
findObservablesForFinding,
|
|
2168
|
-
findObservablesMatching,
|
|
2169
|
-
findObservablesWithThreatIntel,
|
|
2170
|
-
findOrphanObservables,
|
|
2171
|
-
findPath,
|
|
2172
|
-
findSourceObservables,
|
|
2173
|
-
findSuspiciousFindings,
|
|
2174
|
-
findSuspiciousObservables,
|
|
2175
|
-
findTagsAtLeast,
|
|
2176
|
-
findTagsByLevel,
|
|
2177
|
-
findTagsByNamePattern,
|
|
2178
|
-
findThreatIntelAtLeast,
|
|
2179
|
-
findThreatIntelByLevel,
|
|
2180
|
-
findThreatIntelBySource,
|
|
2181
|
-
findThreatIntelsForObservable,
|
|
2182
|
-
findWhitelistedObservables,
|
|
2183
|
-
generateEnrichmentKey,
|
|
2184
|
-
generateFindingKey,
|
|
2185
|
-
generateObservableKey,
|
|
2186
|
-
generateTagKey,
|
|
2187
|
-
generateThreatIntelKey,
|
|
2188
|
-
getAllEnrichments,
|
|
2189
|
-
getAllEvidences,
|
|
2190
|
-
getAllFindingKeys,
|
|
2191
|
-
getAllFindings,
|
|
2192
|
-
getAllObservableTypes,
|
|
2193
|
-
getAllObservables,
|
|
2194
|
-
getAllRelationshipTypes,
|
|
2195
|
-
getAllTags,
|
|
2196
|
-
getAllThreatIntelSources,
|
|
2197
|
-
getAllThreatIntels,
|
|
2198
|
-
getColorForLevel,
|
|
2199
|
-
getColorForScore,
|
|
2200
|
-
getCounts,
|
|
2201
|
-
getDataExtraction,
|
|
2202
|
-
getEnrichment,
|
|
2203
|
-
getEnrichmentByName,
|
|
2204
|
-
getEntityLevel,
|
|
2205
|
-
getEvidence,
|
|
2206
|
-
getFinding,
|
|
2207
|
-
getFindingByName,
|
|
2208
|
-
getLevelFromScore,
|
|
2209
|
-
getObservable,
|
|
2210
|
-
getObservableByTypeValue,
|
|
2211
|
-
getObservableChildren,
|
|
2212
|
-
getObservableGraph,
|
|
2213
|
-
getObservableParents,
|
|
2214
|
-
getReachableObservables,
|
|
2215
|
-
getRelatedObservables,
|
|
2216
|
-
getRelatedObservablesByDirection,
|
|
2217
|
-
getRelatedObservablesByType,
|
|
2218
|
-
getRelationshipsForObservable,
|
|
2219
|
-
getRootObservable,
|
|
2220
|
-
getStartedAt,
|
|
2221
|
-
getStats,
|
|
2222
|
-
getTag,
|
|
2223
|
-
getTagAggregatedLevel,
|
|
2224
|
-
getTagAggregatedScore,
|
|
2225
|
-
getTagAncestors,
|
|
2226
|
-
getTagByName,
|
|
2227
|
-
getTagChildren,
|
|
2228
|
-
getTagDescendants,
|
|
2229
|
-
getThreatIntel,
|
|
2230
|
-
getThreatIntelBySourceObservable,
|
|
2231
|
-
getWhitelists,
|
|
2232
|
-
hasLevel,
|
|
2233
|
-
isCyvest,
|
|
2234
|
-
isLevelAtLeast,
|
|
2235
|
-
isLevelHigherThan,
|
|
2236
|
-
isLevelLowerThan,
|
|
2237
|
-
isTagChildOf,
|
|
2238
|
-
isTagDescendantOf,
|
|
2239
|
-
isValidLevel,
|
|
2240
|
-
maxLevel,
|
|
2241
|
-
minLevel,
|
|
2242
|
-
normalizeLevel,
|
|
2243
|
-
parseCyvest,
|
|
2244
|
-
parseFindingKey,
|
|
2245
|
-
parseKeyType,
|
|
2246
|
-
parseObservableKey,
|
|
2247
|
-
parseThreatIntelKey,
|
|
2248
|
-
sortFindingsByLevel,
|
|
2249
|
-
sortFindingsByScore,
|
|
2250
|
-
sortObservablesByLevel,
|
|
2251
|
-
sortObservablesByScore,
|
|
2252
|
-
validateKey
|
|
2253
|
-
});
|
|
2040
|
+
const parents = new Set(relationsOf(inv).map((relation) => relation.source_key));
|
|
2041
|
+
return Object.values(getAllObservables(inv)).filter((observable) => !parents.has(observable.key));
|
|
2042
|
+
}
|
|
2043
|
+
function findSourceObservables(inv) {
|
|
2044
|
+
const children = new Set(relationsOf(inv).map((relation) => relation.target_key));
|
|
2045
|
+
return Object.values(getAllObservables(inv)).filter((observable) => !children.has(observable.key));
|
|
2046
|
+
}
|
|
2047
|
+
//#endregion
|
|
2048
|
+
exports.KEY_PREFIXES = KEY_PREFIXES;
|
|
2049
|
+
exports.SCHEMA_VERSION = SCHEMA_VERSION;
|
|
2050
|
+
exports.VERDICT_HEX_COLORS = VERDICT_HEX_COLORS;
|
|
2051
|
+
exports.VERDICT_ORDER = VERDICT_ORDER;
|
|
2052
|
+
exports.VERDICT_TERMINAL_STYLES = VERDICT_TERMINAL_STYLES;
|
|
2053
|
+
exports.areConnected = areConnected;
|
|
2054
|
+
exports.assertReadableVersion = assertReadableVersion;
|
|
2055
|
+
exports.compareVerdicts = compareVerdicts;
|
|
2056
|
+
exports.confidenceBand = confidenceBand;
|
|
2057
|
+
exports.countRelationsByKind = countRelationsByKind;
|
|
2058
|
+
exports.decisionLabel = decisionLabel;
|
|
2059
|
+
exports.detectSchemaVersion = detectSchemaVersion;
|
|
2060
|
+
exports.findContradictedFindings = findContradictedFindings;
|
|
2061
|
+
exports.findExternalObservables = findExternalObservables;
|
|
2062
|
+
exports.findFindingsAtLeast = findFindingsAtLeast;
|
|
2063
|
+
exports.findFindingsByConfidence = findFindingsByConfidence;
|
|
2064
|
+
exports.findFindingsByRule = findFindingsByRule;
|
|
2065
|
+
exports.findFindingsByVerdict = findFindingsByVerdict;
|
|
2066
|
+
exports.findInternalObservables = findInternalObservables;
|
|
2067
|
+
exports.findLeafObservables = findLeafObservables;
|
|
2068
|
+
exports.findObservablesAtLeast = findObservablesAtLeast;
|
|
2069
|
+
exports.findObservablesByType = findObservablesByType;
|
|
2070
|
+
exports.findObservablesByValue = findObservablesByValue;
|
|
2071
|
+
exports.findObservablesByVerdict = findObservablesByVerdict;
|
|
2072
|
+
exports.findObservablesContaining = findObservablesContaining;
|
|
2073
|
+
exports.findObservablesWithThreatIntel = findObservablesWithThreatIntel;
|
|
2074
|
+
exports.findOrphanObservables = findOrphanObservables;
|
|
2075
|
+
exports.findSourceObservables = findSourceObservables;
|
|
2076
|
+
exports.findThreatIntelBySource = findThreatIntelBySource;
|
|
2077
|
+
exports.findThreatIntelByVerdict = findThreatIntelByVerdict;
|
|
2078
|
+
exports.findThreatIntelForObservable = findThreatIntelForObservable;
|
|
2079
|
+
exports.findUncountedFindings = findUncountedFindings;
|
|
2080
|
+
exports.generateDecisionKey = generateDecisionKey;
|
|
2081
|
+
exports.generateFindingKey = generateFindingKey;
|
|
2082
|
+
exports.generateObservableKey = generateObservableKey;
|
|
2083
|
+
exports.generateRelationKey = generateRelationKey;
|
|
2084
|
+
exports.generateSignalKey = generateSignalKey;
|
|
2085
|
+
exports.generateTagKey = generateTagKey;
|
|
2086
|
+
exports.generateThreatIntelKey = generateThreatIntelKey;
|
|
2087
|
+
exports.getAllDecisions = getAllDecisions;
|
|
2088
|
+
exports.getAllEvidences = getAllEvidences;
|
|
2089
|
+
exports.getAllFindings = getAllFindings;
|
|
2090
|
+
exports.getAllObservables = getAllObservables;
|
|
2091
|
+
exports.getAllRelations = getAllRelations;
|
|
2092
|
+
exports.getAllTags = getAllTags;
|
|
2093
|
+
exports.getAllThreatIntels = getAllThreatIntels;
|
|
2094
|
+
exports.getColorForVerdict = getColorForVerdict;
|
|
2095
|
+
exports.getCounts = getCounts;
|
|
2096
|
+
exports.getDecision = getDecision;
|
|
2097
|
+
exports.getDecisionFor = getDecisionFor;
|
|
2098
|
+
exports.getEvidence = getEvidence;
|
|
2099
|
+
exports.getEvidencesByType = getEvidencesByType;
|
|
2100
|
+
exports.getFinding = getFinding;
|
|
2101
|
+
exports.getFindingResult = getFindingResult;
|
|
2102
|
+
exports.getFindingScore = getFindingScore;
|
|
2103
|
+
exports.getFindingVerdict = getFindingVerdict;
|
|
2104
|
+
exports.getGlobalScore = getGlobalScore;
|
|
2105
|
+
exports.getGlobalVerdict = getGlobalVerdict;
|
|
2106
|
+
exports.getInvestigationResult = getInvestigationResult;
|
|
2107
|
+
exports.getObservable = getObservable;
|
|
2108
|
+
exports.getObservableChildren = getObservableChildren;
|
|
2109
|
+
exports.getObservableGraph = getObservableGraph;
|
|
2110
|
+
exports.getObservableParents = getObservableParents;
|
|
2111
|
+
exports.getObservableResult = getObservableResult;
|
|
2112
|
+
exports.getObservableScore = getObservableScore;
|
|
2113
|
+
exports.getObservableVerdict = getObservableVerdict;
|
|
2114
|
+
exports.getReachableObservables = getReachableObservables;
|
|
2115
|
+
exports.getRelatedObservables = getRelatedObservables;
|
|
2116
|
+
exports.getRelation = getRelation;
|
|
2117
|
+
exports.getRelationsForObservable = getRelationsForObservable;
|
|
2118
|
+
exports.getRootObservable = getRootObservable;
|
|
2119
|
+
exports.getTag = getTag;
|
|
2120
|
+
exports.getTagAggregatedScore = getTagAggregatedScore;
|
|
2121
|
+
exports.getTagAncestors = getTagAncestors;
|
|
2122
|
+
exports.getTagByName = getTagByName;
|
|
2123
|
+
exports.getTagFindingKeys = getTagFindingKeys;
|
|
2124
|
+
exports.getThreatIntel = getThreatIntel;
|
|
2125
|
+
exports.getThreatIntelsFor = getThreatIntelsFor;
|
|
2126
|
+
exports.isAllowlisted = isAllowlisted;
|
|
2127
|
+
exports.isBlocklisted = isBlocklisted;
|
|
2128
|
+
exports.isConfirmed = isConfirmed;
|
|
2129
|
+
exports.isCyvest = isCyvest;
|
|
2130
|
+
exports.isDismissed = isDismissed;
|
|
2131
|
+
exports.isTagChildOf = isTagChildOf;
|
|
2132
|
+
exports.isTagDescendantOf = isTagDescendantOf;
|
|
2133
|
+
exports.isVacated = isVacated;
|
|
2134
|
+
exports.isValidVerdict = isValidVerdict;
|
|
2135
|
+
exports.isVerdictAtLeast = isVerdictAtLeast;
|
|
2136
|
+
exports.maxVerdict = maxVerdict;
|
|
2137
|
+
exports.minVerdict = minVerdict;
|
|
2138
|
+
exports.normalizeVerdict = normalizeVerdict;
|
|
2139
|
+
exports.parseCyvest = parseCyvest;
|
|
2140
|
+
exports.parseKeyType = parseKeyType;
|
|
2141
|
+
exports.parseObservableKey = parseObservableKey;
|
|
2142
|
+
exports.validateKey = validateKey;
|
|
2143
|
+
exports.verdictPolarity = verdictPolarity;
|
|
2144
|
+
exports.wasSuppressed = wasSuppressed;
|