@cyvest/cyvest-js 6.1.2 → 7.0.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 +2011 -2166
- package/dist/index.d.cts +731 -1439
- package/dist/index.d.ts +731 -1439
- package/dist/index.js +1895 -2026
- package/package.json +31 -8
package/dist/index.cjs
CHANGED
|
@@ -1,2253 +1,2098 @@
|
|
|
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
|
+
"ThreatIntel": {
|
|
1031
|
+
"additionalProperties": false,
|
|
1032
|
+
"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.",
|
|
1033
|
+
"properties": {
|
|
1034
|
+
"verdict": {
|
|
1035
|
+
"$ref": "#/$defs/Verdict",
|
|
1036
|
+
"default": "INFO"
|
|
1037
|
+
},
|
|
1038
|
+
"confidence": {
|
|
1039
|
+
"default": 1,
|
|
1040
|
+
"exclusiveMinimum": 0,
|
|
1041
|
+
"maximum": 1,
|
|
1042
|
+
"title": "Confidence",
|
|
1043
|
+
"type": "number"
|
|
1044
|
+
},
|
|
1045
|
+
"weight": {
|
|
1046
|
+
"anyOf": [{
|
|
1047
|
+
"minimum": 0,
|
|
1048
|
+
"type": "number"
|
|
1049
|
+
}, { "type": "null" }],
|
|
1050
|
+
"default": null,
|
|
1051
|
+
"title": "Weight"
|
|
1052
|
+
},
|
|
1053
|
+
"key": {
|
|
1054
|
+
"minLength": 1,
|
|
1055
|
+
"title": "Key",
|
|
1056
|
+
"type": "string"
|
|
1057
|
+
},
|
|
1058
|
+
"seq": {
|
|
1059
|
+
"maxLength": 26,
|
|
1060
|
+
"minLength": 26,
|
|
1061
|
+
"title": "Seq",
|
|
1062
|
+
"type": "string"
|
|
1063
|
+
},
|
|
1064
|
+
"asserted_at": {
|
|
1065
|
+
"format": "date-time",
|
|
1066
|
+
"title": "Asserted At",
|
|
1067
|
+
"type": "string"
|
|
1068
|
+
},
|
|
1069
|
+
"occurred_at": {
|
|
1070
|
+
"anyOf": [{
|
|
1071
|
+
"format": "date-time",
|
|
1072
|
+
"type": "string"
|
|
1073
|
+
}, { "type": "null" }],
|
|
1074
|
+
"default": null,
|
|
1075
|
+
"title": "Occurred At"
|
|
1076
|
+
},
|
|
1077
|
+
"source": { "$ref": "#/$defs/SourceRef" },
|
|
1078
|
+
"fragment_id": {
|
|
1079
|
+
"title": "Fragment Id",
|
|
1080
|
+
"type": "string"
|
|
1081
|
+
},
|
|
1082
|
+
"external_id": {
|
|
1083
|
+
"anyOf": [{ "type": "string" }, { "type": "null" }],
|
|
1084
|
+
"default": null,
|
|
1085
|
+
"title": "External Id"
|
|
1086
|
+
},
|
|
1087
|
+
"evidence_keys": {
|
|
1088
|
+
"default": [],
|
|
1089
|
+
"items": { "type": "string" },
|
|
1090
|
+
"title": "Evidence Keys",
|
|
1091
|
+
"type": "array"
|
|
1092
|
+
},
|
|
1093
|
+
"subject_key": {
|
|
1094
|
+
"minLength": 1,
|
|
1095
|
+
"title": "Subject Key",
|
|
1096
|
+
"type": "string"
|
|
1097
|
+
},
|
|
1098
|
+
"kind": {
|
|
1099
|
+
"const": "threat_intel",
|
|
1100
|
+
"default": "threat_intel",
|
|
1101
|
+
"title": "Kind",
|
|
1102
|
+
"type": "string"
|
|
1103
|
+
},
|
|
1104
|
+
"observed_at": {
|
|
1105
|
+
"anyOf": [{
|
|
1106
|
+
"format": "date-time",
|
|
1107
|
+
"type": "string"
|
|
1108
|
+
}, { "type": "null" }],
|
|
1109
|
+
"default": null,
|
|
1110
|
+
"title": "Observed At"
|
|
1111
|
+
},
|
|
1112
|
+
"labels": {
|
|
1113
|
+
"default": [],
|
|
1114
|
+
"items": { "$ref": "#/$defs/Label" },
|
|
1115
|
+
"title": "Labels",
|
|
1116
|
+
"type": "array"
|
|
1117
|
+
},
|
|
1118
|
+
"payload": {
|
|
1119
|
+
"additionalProperties": true,
|
|
1120
|
+
"title": "Payload",
|
|
1121
|
+
"type": "object"
|
|
1122
|
+
},
|
|
1123
|
+
"source_class": {
|
|
1124
|
+
"$ref": "#/$defs/SourceClass",
|
|
1125
|
+
"default": "vendor_feed"
|
|
1126
|
+
},
|
|
1127
|
+
"taxonomies": {
|
|
1128
|
+
"default": [],
|
|
1129
|
+
"items": { "type": "string" },
|
|
1130
|
+
"title": "Taxonomies",
|
|
1131
|
+
"type": "array"
|
|
1132
|
+
},
|
|
1133
|
+
"comment": {
|
|
1134
|
+
"default": "",
|
|
1135
|
+
"title": "Comment",
|
|
1136
|
+
"type": "string"
|
|
1137
|
+
}
|
|
1138
|
+
},
|
|
1139
|
+
"required": [
|
|
1140
|
+
"key",
|
|
1141
|
+
"seq",
|
|
1142
|
+
"asserted_at",
|
|
1143
|
+
"source",
|
|
1144
|
+
"fragment_id",
|
|
1145
|
+
"subject_key"
|
|
1146
|
+
],
|
|
1147
|
+
"title": "ThreatIntel",
|
|
1148
|
+
"type": "object"
|
|
1149
|
+
},
|
|
1150
|
+
"Verdict": {
|
|
1151
|
+
"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).",
|
|
1152
|
+
"enum": [
|
|
1153
|
+
"SAFE",
|
|
1154
|
+
"INFO",
|
|
1155
|
+
"NOTABLE",
|
|
1156
|
+
"SUSPICIOUS",
|
|
1157
|
+
"MALICIOUS"
|
|
1158
|
+
],
|
|
1159
|
+
"title": "Verdict",
|
|
1160
|
+
"type": "string"
|
|
1161
|
+
}
|
|
1162
|
+
},
|
|
1163
|
+
$id: "https://cyvest.io/schema/investigation-7.json",
|
|
1164
|
+
additionalProperties: false,
|
|
1165
|
+
description: "A complete serialized investigation.",
|
|
1166
|
+
properties: {
|
|
1167
|
+
"schema_version": {
|
|
1168
|
+
"default": "7.0.0",
|
|
1169
|
+
"pattern": "^7\\.\\d+\\.\\d+$",
|
|
1170
|
+
"title": "Schema Version",
|
|
1171
|
+
"type": "string"
|
|
1172
|
+
},
|
|
1173
|
+
"header": { "$ref": "#/$defs/InvestigationHeader" },
|
|
1174
|
+
"policy_version": {
|
|
1175
|
+
"default": "default-v1",
|
|
1176
|
+
"title": "Policy Version",
|
|
1177
|
+
"type": "string"
|
|
1178
|
+
},
|
|
1179
|
+
"engine_id": {
|
|
1180
|
+
"default": "basic-v1",
|
|
1181
|
+
"title": "Engine Id",
|
|
1182
|
+
"type": "string"
|
|
1183
|
+
},
|
|
1184
|
+
"facts": { "$ref": "#/$defs/FactsSchema" },
|
|
1185
|
+
"decisions": {
|
|
1186
|
+
"additionalProperties": { "$ref": "#/$defs/Decision" },
|
|
1187
|
+
"title": "Decisions",
|
|
1188
|
+
"type": "object"
|
|
1189
|
+
},
|
|
1190
|
+
"tags": {
|
|
1191
|
+
"additionalProperties": { "$ref": "#/$defs/Tag" },
|
|
1192
|
+
"title": "Tags",
|
|
1193
|
+
"type": "object"
|
|
1194
|
+
},
|
|
1195
|
+
"report": { "$ref": "#/$defs/Report" }
|
|
1196
|
+
},
|
|
1197
|
+
required: ["header", "report"],
|
|
1198
|
+
title: "InvestigationSchema",
|
|
1199
|
+
type: "object"
|
|
1175
1200
|
};
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1201
|
+
//#endregion
|
|
1202
|
+
//#region src/helpers.ts
|
|
1203
|
+
/**
|
|
1204
|
+
* Parsing and validation.
|
|
1205
|
+
*
|
|
1206
|
+
* ajv checks the shape; the version check enforces the rule ajv cannot see — a 7.0 SDK must
|
|
1207
|
+
* refuse a 7.1 document rather than silently ignore fields it does not know about. Python
|
|
1208
|
+
* applies exactly the same rule.
|
|
1209
|
+
*
|
|
1210
|
+
* Order matters. The schema constrains `schema_version` to the current major, so the minor
|
|
1211
|
+
* window is the SDK's job alone; reading and judging the version first also keeps the error
|
|
1212
|
+
* message explicit instead of an opaque ajv shape error.
|
|
1213
|
+
*/
|
|
1214
|
+
const SCHEMA_VERSION = "7.0.0";
|
|
1215
|
+
const SEMVER = /^\d+\.\d+\.\d+$/;
|
|
1216
|
+
const ajv = new ajv_dist_2020_js.default({ allErrors: true });
|
|
1217
|
+
(0, ajv_formats.default)(ajv);
|
|
1218
|
+
let validateFn = null;
|
|
1181
1219
|
function getValidator() {
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1220
|
+
if (!validateFn) validateFn = ajv.compile(cyvest_schema_default);
|
|
1221
|
+
return validateFn;
|
|
1222
|
+
}
|
|
1223
|
+
function versionTuple(version) {
|
|
1224
|
+
const [major, minor] = version.split(".");
|
|
1225
|
+
return [Number(major), Number(minor)];
|
|
1226
|
+
}
|
|
1227
|
+
/**
|
|
1228
|
+
* Read the declared schema version, falling back to `"5"` for anything unversioned.
|
|
1229
|
+
*
|
|
1230
|
+
* A document with no `schema_version` is a pre-v7 document, not a current one: `schema_version`
|
|
1231
|
+
* is not in the schema's `required` list, so defaulting it to `SCHEMA_VERSION` would wave every
|
|
1232
|
+
* legacy payload straight through. Python's `detect_schema_version` makes the same choice.
|
|
1233
|
+
*/
|
|
1234
|
+
function detectSchemaVersion(json) {
|
|
1235
|
+
const raw = json?.schema_version;
|
|
1236
|
+
const version = typeof raw === "string" ? raw.trim() : "";
|
|
1237
|
+
return SEMVER.test(version) ? version : "5";
|
|
1238
|
+
}
|
|
1239
|
+
/** Upward compatibility only: read older documents, never newer ones. */
|
|
1240
|
+
function assertReadableVersion(version) {
|
|
1241
|
+
const [docMajor, docMinor] = versionTuple(version);
|
|
1242
|
+
const [libMajor, libMinor] = versionTuple(SCHEMA_VERSION);
|
|
1243
|
+
if (docMajor > libMajor || docMajor === libMajor && docMinor > libMinor) throw new Error(`Document schema ${version} is newer than this SDK (${SCHEMA_VERSION}); upgrade @cyvest/cyvest-js.`);
|
|
1244
|
+
if (docMajor < libMajor) throw new Error(`Document schema ${version} predates this SDK (${SCHEMA_VERSION}); run 'cyvest migrate'.`);
|
|
1186
1245
|
}
|
|
1187
1246
|
function parseCyvest(json) {
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
}
|
|
1193
|
-
return json;
|
|
1247
|
+
assertReadableVersion(detectSchemaVersion(json));
|
|
1248
|
+
const validate = getValidator();
|
|
1249
|
+
if (!validate(json)) throw new Error(`Invalid Cyvest payload: ${ajv.errorsText(validate.errors || [])}`);
|
|
1250
|
+
return json;
|
|
1194
1251
|
}
|
|
1195
1252
|
function isCyvest(json) {
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1253
|
+
try {
|
|
1254
|
+
parseCyvest(json);
|
|
1255
|
+
return true;
|
|
1256
|
+
} catch {
|
|
1257
|
+
return false;
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
//#endregion
|
|
1261
|
+
//#region src/keys.ts
|
|
1262
|
+
/** Every prefix a v7 document can carry. `enr:` is gone — enrichments are evidence now. */
|
|
1263
|
+
const KEY_PREFIXES = [
|
|
1264
|
+
"obs",
|
|
1265
|
+
"fnd",
|
|
1266
|
+
"evd",
|
|
1267
|
+
"sig",
|
|
1268
|
+
"rel",
|
|
1269
|
+
"dec",
|
|
1270
|
+
"tag"
|
|
1271
|
+
];
|
|
1272
|
+
/**
|
|
1273
|
+
* Normalize a string value for consistent key generation.
|
|
1274
|
+
*/
|
|
1201
1275
|
function normalizeValue(value) {
|
|
1202
|
-
|
|
1276
|
+
return value.trim().toLowerCase();
|
|
1203
1277
|
}
|
|
1204
1278
|
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;
|
|
1279
|
+
const normalizedType = normalizeValue(obsType);
|
|
1280
|
+
const normalizedSubtype = subtype ? normalizeValue(subtype) : void 0;
|
|
1281
|
+
const stripped = value.trim();
|
|
1282
|
+
if (normalizedType === "command_line") return stripped;
|
|
1283
|
+
if (normalizedType === "email" || normalizedType === "host") return stripped.toLowerCase();
|
|
1284
|
+
if (normalizedType === "user" && (normalizedSubtype === "email" || normalizedSubtype === "upn")) return stripped.toLowerCase();
|
|
1285
|
+
if (normalizedSubtype === "uid" || normalizedSubtype === "pid") {
|
|
1286
|
+
if (!/^[+-]?\d+$/.test(stripped)) throw new Error(`${normalizedSubtype} observable values must be base-10 integers`);
|
|
1287
|
+
return BigInt(stripped).toString(10);
|
|
1288
|
+
}
|
|
1289
|
+
return stripped;
|
|
1222
1290
|
}
|
|
1223
1291
|
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("");
|
|
1292
|
+
const bytes = new TextEncoder().encode(content);
|
|
1293
|
+
const words = [];
|
|
1294
|
+
const bitLength = bytes.length * 8;
|
|
1295
|
+
for (const byte of bytes) words.push(byte);
|
|
1296
|
+
words.push(128);
|
|
1297
|
+
while (words.length % 64 !== 56) words.push(0);
|
|
1298
|
+
for (let i = 7; i >= 0; i--) words.push(Math.floor(bitLength / 2 ** (i * 8)) & 255);
|
|
1299
|
+
const h = [
|
|
1300
|
+
1779033703,
|
|
1301
|
+
3144134277,
|
|
1302
|
+
1013904242,
|
|
1303
|
+
2773480762,
|
|
1304
|
+
1359893119,
|
|
1305
|
+
2600822924,
|
|
1306
|
+
528734635,
|
|
1307
|
+
1541459225
|
|
1308
|
+
];
|
|
1309
|
+
const k = Array.from({ length: 64 }, (_, index) => {
|
|
1310
|
+
let primeCount = 0;
|
|
1311
|
+
let candidate = 2;
|
|
1312
|
+
while (true) {
|
|
1313
|
+
let prime = true;
|
|
1314
|
+
for (let divisor = 2; divisor * divisor <= candidate; divisor++) if (candidate % divisor === 0) {
|
|
1315
|
+
prime = false;
|
|
1316
|
+
break;
|
|
1317
|
+
}
|
|
1318
|
+
if (prime && primeCount++ === index) return Math.floor(Math.cbrt(candidate) % 1 * 2 ** 32) >>> 0;
|
|
1319
|
+
candidate++;
|
|
1320
|
+
}
|
|
1321
|
+
});
|
|
1322
|
+
const rotateRight = (value, amount) => value >>> amount | value << 32 - amount;
|
|
1323
|
+
for (let offset = 0; offset < words.length; offset += 64) {
|
|
1324
|
+
const schedule = new Array(64);
|
|
1325
|
+
for (let i = 0; i < 16; i++) {
|
|
1326
|
+
const base = offset + i * 4;
|
|
1327
|
+
schedule[i] = (words[base] << 24 | words[base + 1] << 16 | words[base + 2] << 8 | words[base + 3]) >>> 0;
|
|
1328
|
+
}
|
|
1329
|
+
for (let i = 16; i < 64; i++) {
|
|
1330
|
+
const s0 = rotateRight(schedule[i - 15], 7) ^ rotateRight(schedule[i - 15], 18) ^ schedule[i - 15] >>> 3;
|
|
1331
|
+
const s1 = rotateRight(schedule[i - 2], 17) ^ rotateRight(schedule[i - 2], 19) ^ schedule[i - 2] >>> 10;
|
|
1332
|
+
schedule[i] = schedule[i - 16] + s0 + schedule[i - 7] + s1 >>> 0;
|
|
1333
|
+
}
|
|
1334
|
+
let [a, b, c, d, e, f, g, hh] = h;
|
|
1335
|
+
for (let i = 0; i < 64; i++) {
|
|
1336
|
+
const s1 = rotateRight(e, 6) ^ rotateRight(e, 11) ^ rotateRight(e, 25);
|
|
1337
|
+
const choice = e & f ^ ~e & g;
|
|
1338
|
+
const temp1 = hh + s1 + choice + k[i] + schedule[i] >>> 0;
|
|
1339
|
+
const temp2 = (rotateRight(a, 2) ^ rotateRight(a, 13) ^ rotateRight(a, 22)) + (a & b ^ a & c ^ b & c) >>> 0;
|
|
1340
|
+
hh = g;
|
|
1341
|
+
g = f;
|
|
1342
|
+
f = e;
|
|
1343
|
+
e = d + temp1 >>> 0;
|
|
1344
|
+
d = c;
|
|
1345
|
+
c = b;
|
|
1346
|
+
b = a;
|
|
1347
|
+
a = temp1 + temp2 >>> 0;
|
|
1348
|
+
}
|
|
1349
|
+
[
|
|
1350
|
+
a,
|
|
1351
|
+
b,
|
|
1352
|
+
c,
|
|
1353
|
+
d,
|
|
1354
|
+
e,
|
|
1355
|
+
f,
|
|
1356
|
+
g,
|
|
1357
|
+
hh
|
|
1358
|
+
].forEach((value, index) => {
|
|
1359
|
+
h[index] = h[index] + value >>> 0;
|
|
1360
|
+
});
|
|
1361
|
+
}
|
|
1362
|
+
return h.map((value) => value.toString(16).padStart(8, "0")).join("");
|
|
1296
1363
|
}
|
|
1297
1364
|
function encodeKeyPart(value, keepSlash = false) {
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1365
|
+
let encoded = encodeURIComponent(value).replace(/[!'()*]/g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
1366
|
+
encoded = encoded.replace(/%40/gi, "@");
|
|
1367
|
+
if (keepSlash) encoded = encoded.replace(/%2F/gi, "/").replace(/%5C/gi, "\\");
|
|
1368
|
+
return encoded;
|
|
1369
|
+
}
|
|
1370
|
+
/**
|
|
1371
|
+
* Generate a unique key for an observable.
|
|
1372
|
+
*
|
|
1373
|
+
* Format: obs:{type}:{normalized_value}
|
|
1374
|
+
*
|
|
1375
|
+
* @param obsType - Type of observable (ip, url, domain, hash, etc.)
|
|
1376
|
+
* @param value - Value of the observable
|
|
1377
|
+
* @returns Unique observable key
|
|
1378
|
+
*
|
|
1379
|
+
* @example
|
|
1380
|
+
* ```ts
|
|
1381
|
+
* generateObservableKey("ipv4", "192.168.1.1")
|
|
1382
|
+
* // => "obs:ipv4:192.168.1.1"
|
|
1383
|
+
* ```
|
|
1384
|
+
*/
|
|
1315
1385
|
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
|
-
}
|
|
1386
|
+
const normalizedType = normalizeValue(obsType);
|
|
1387
|
+
const normalizedSubtype = subtype ? normalizeValue(subtype) : void 0;
|
|
1388
|
+
const normalizedNamespace = namespace?.trim().toLowerCase() || void 0;
|
|
1389
|
+
const normalizedValue = normalizeObservableValue(normalizedType, value, normalizedSubtype);
|
|
1390
|
+
const identity = JSON.stringify({
|
|
1391
|
+
namespace: normalizedNamespace ?? null,
|
|
1392
|
+
subtype: normalizedSubtype ?? null,
|
|
1393
|
+
type: normalizedType,
|
|
1394
|
+
value: normalizedValue
|
|
1395
|
+
});
|
|
1396
|
+
if (normalizedType === "command_line" || new TextEncoder().encode(identity).length > 128) return `obs:${normalizedType}:sha256:${sha256(identity)}`;
|
|
1397
|
+
if (!normalizedSubtype && !normalizedNamespace) return `obs:${normalizedType}:${normalizedValue.toLowerCase()}`;
|
|
1398
|
+
const parts = [
|
|
1399
|
+
"obs",
|
|
1400
|
+
encodeKeyPart(normalizedType),
|
|
1401
|
+
normalizedSubtype ? encodeKeyPart(normalizedSubtype) : "_"
|
|
1402
|
+
];
|
|
1403
|
+
if (normalizedNamespace) parts.push(encodeKeyPart(normalizedNamespace));
|
|
1404
|
+
parts.push(encodeKeyPart(normalizedValue, true));
|
|
1405
|
+
return parts.join(":");
|
|
1406
|
+
}
|
|
1407
|
+
/**
|
|
1408
|
+
* Generate a finding key.
|
|
1409
|
+
*
|
|
1410
|
+
* Format: `fnd:{rule_id}`, or `fnd:{rule_id}:{external_id}`.
|
|
1411
|
+
*
|
|
1412
|
+
* A finding is identified by the rule that produced it. Running the same rule on several
|
|
1413
|
+
* observables means several findings, so pass an `externalId` to keep them apart — usually the
|
|
1414
|
+
* observable key.
|
|
1415
|
+
*
|
|
1416
|
+
* @example
|
|
1417
|
+
* ```ts
|
|
1418
|
+
* generateFindingKey("url_in_body")
|
|
1419
|
+
* // => "fnd:url_in_body"
|
|
1420
|
+
* ```
|
|
1421
|
+
*/
|
|
1422
|
+
function generateFindingKey(ruleId, externalId) {
|
|
1423
|
+
const normalizedRule = normalizeValue(ruleId);
|
|
1424
|
+
if (externalId) return `fnd:${normalizedRule}:${externalId.trim()}`;
|
|
1425
|
+
return `fnd:${normalizedRule}`;
|
|
1426
|
+
}
|
|
1427
|
+
/**
|
|
1428
|
+
* Generate an observable-signal key.
|
|
1429
|
+
*
|
|
1430
|
+
* Format: `sig:{source}:{subject_key}`. The prefix names the *family*, not its first member:
|
|
1431
|
+
* v6's `ti:` would have locked every future signal kind behind a threat-intel name.
|
|
1432
|
+
*
|
|
1433
|
+
* @example
|
|
1434
|
+
* ```ts
|
|
1435
|
+
* generateSignalKey("virustotal", "obs:ipv4:192.168.1.1")
|
|
1436
|
+
* // => "sig:virustotal:obs:ipv4:192.168.1.1"
|
|
1437
|
+
* ```
|
|
1438
|
+
*/
|
|
1439
|
+
function generateSignalKey(source, subjectKey, externalId) {
|
|
1440
|
+
const normalizedSource = normalizeValue(source);
|
|
1441
|
+
if (externalId) return `sig:${normalizedSource}:${externalId.trim()}:${subjectKey}`;
|
|
1442
|
+
return `sig:${normalizedSource}:${subjectKey}`;
|
|
1443
|
+
}
|
|
1444
|
+
/** Kept under its v6 name; new code calls {@link generateSignalKey}. */
|
|
1445
|
+
const generateThreatIntelKey = generateSignalKey;
|
|
1446
|
+
/**
|
|
1447
|
+
* Generate a relation key.
|
|
1448
|
+
*
|
|
1449
|
+
* Format: `rel:{kind}:{source_key}>{target_key}` — direction lives in the key itself, so no
|
|
1450
|
+
* `direction` field is needed to disambiguate.
|
|
1451
|
+
*/
|
|
1452
|
+
function generateRelationKey(sourceKey, targetKey, kind, externalId) {
|
|
1453
|
+
const normalizedKind = normalizeValue(kind);
|
|
1454
|
+
if (externalId) return `rel:${normalizedKind}:${externalId.trim()}:${sourceKey}>${targetKey}`;
|
|
1455
|
+
return `rel:${normalizedKind}:${sourceKey}>${targetKey}`;
|
|
1456
|
+
}
|
|
1457
|
+
/**
|
|
1458
|
+
* Generate a decision key.
|
|
1459
|
+
*
|
|
1460
|
+
* Format: `dec:{target_key}` — one decision per target, whatever it says. The kind is content,
|
|
1461
|
+
* not identity: a target holds a single current stance, and changing one's mind is a
|
|
1462
|
+
* re-assertion the merge law settles by freshness.
|
|
1463
|
+
*/
|
|
1464
|
+
function generateDecisionKey(targetKey) {
|
|
1465
|
+
return `dec:${targetKey}`;
|
|
1466
|
+
}
|
|
1467
|
+
/**
|
|
1468
|
+
* Generate a unique key for a tag.
|
|
1469
|
+
*
|
|
1470
|
+
* Format: tag:{normalized_name}
|
|
1471
|
+
*
|
|
1472
|
+
* @param name - Name of the tag (can use : as hierarchy delimiter)
|
|
1473
|
+
* @returns Unique tag key
|
|
1474
|
+
*
|
|
1475
|
+
* @example
|
|
1476
|
+
* ```ts
|
|
1477
|
+
* generateTagKey("header:auth:dkim")
|
|
1478
|
+
* // => "tag:header:auth:dkim"
|
|
1479
|
+
* ```
|
|
1480
|
+
*/
|
|
1357
1481
|
function generateTagKey(name) {
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1482
|
+
return `tag:${normalizeValue(name)}`;
|
|
1483
|
+
}
|
|
1484
|
+
/**
|
|
1485
|
+
* Get all ancestor tag names from a hierarchical tag name.
|
|
1486
|
+
*
|
|
1487
|
+
* @param name - Tag name with : delimiter
|
|
1488
|
+
* @returns Array of ancestor tag names
|
|
1489
|
+
*
|
|
1490
|
+
* @example
|
|
1491
|
+
* ```ts
|
|
1492
|
+
* getTagAncestors("header:auth:dkim")
|
|
1493
|
+
* // => ["header", "header:auth"]
|
|
1494
|
+
* ```
|
|
1495
|
+
*/
|
|
1361
1496
|
function getTagAncestors(name) {
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1497
|
+
const parts = name.split(":");
|
|
1498
|
+
const ancestors = [];
|
|
1499
|
+
for (let i = 0; i < parts.length - 1; i++) ancestors.push(parts.slice(0, i + 1).join(":"));
|
|
1500
|
+
return ancestors;
|
|
1501
|
+
}
|
|
1502
|
+
/**
|
|
1503
|
+
* Check if a tag is a direct child of another tag.
|
|
1504
|
+
*
|
|
1505
|
+
* @param childName - Potential child tag name
|
|
1506
|
+
* @param parentName - Potential parent tag name
|
|
1507
|
+
* @returns True if childName is a direct child of parentName
|
|
1508
|
+
*
|
|
1509
|
+
* @example
|
|
1510
|
+
* ```ts
|
|
1511
|
+
* isTagChildOf("header:auth", "header") // => true
|
|
1512
|
+
* isTagChildOf("header:auth:dkim", "header") // => false (grandchild)
|
|
1513
|
+
* ```
|
|
1514
|
+
*/
|
|
1369
1515
|
function isTagChildOf(childName, parentName) {
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1516
|
+
if (!childName.startsWith(parentName + ":")) return false;
|
|
1517
|
+
return !childName.slice(parentName.length + 1).includes(":");
|
|
1518
|
+
}
|
|
1519
|
+
/**
|
|
1520
|
+
* Check if a tag is a descendant of another tag (any depth).
|
|
1521
|
+
*
|
|
1522
|
+
* @param descendantName - Potential descendant tag name
|
|
1523
|
+
* @param ancestorName - Potential ancestor tag name
|
|
1524
|
+
* @returns True if descendantName is a descendant of ancestorName
|
|
1525
|
+
*
|
|
1526
|
+
* @example
|
|
1527
|
+
* ```ts
|
|
1528
|
+
* isTagDescendantOf("header:auth:dkim", "header") // => true
|
|
1529
|
+
* isTagDescendantOf("header", "header") // => false (same)
|
|
1530
|
+
* ```
|
|
1531
|
+
*/
|
|
1376
1532
|
function isTagDescendantOf(descendantName, ancestorName) {
|
|
1377
|
-
|
|
1378
|
-
}
|
|
1533
|
+
return descendantName.startsWith(ancestorName + ":");
|
|
1534
|
+
}
|
|
1535
|
+
/**
|
|
1536
|
+
* Extract the type prefix from a key.
|
|
1537
|
+
*
|
|
1538
|
+
* @param key - The key to parse
|
|
1539
|
+
* @returns Type prefix (see {@link KEY_PREFIXES}) or null if invalid
|
|
1540
|
+
*
|
|
1541
|
+
* @example
|
|
1542
|
+
* ```ts
|
|
1543
|
+
* parseKeyType("obs:ipv4:192.168.1.1") // => "obs"
|
|
1544
|
+
* parseKeyType("invalid") // => null
|
|
1545
|
+
* ```
|
|
1546
|
+
*/
|
|
1379
1547
|
function parseKeyType(key) {
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1548
|
+
if (key.includes(":")) {
|
|
1549
|
+
const prefix = key.split(":", 1)[0];
|
|
1550
|
+
if (KEY_PREFIXES.includes(prefix)) return prefix;
|
|
1551
|
+
}
|
|
1552
|
+
return null;
|
|
1553
|
+
}
|
|
1554
|
+
/**
|
|
1555
|
+
* Validate a key format and optionally finding its type.
|
|
1556
|
+
*
|
|
1557
|
+
* @param key - The key to validate
|
|
1558
|
+
* @param expectedType - Optional expected type prefix
|
|
1559
|
+
* @returns True if valid, false otherwise
|
|
1560
|
+
*
|
|
1561
|
+
* @example
|
|
1562
|
+
* ```ts
|
|
1563
|
+
* validateKey("obs:ipv4:192.168.1.1") // => true
|
|
1564
|
+
* validateKey("obs:ipv4:192.168.1.1", "obs") // => true
|
|
1565
|
+
* validateKey("obs:ipv4:192.168.1.1", "chk") // => false
|
|
1566
|
+
* validateKey("invalid") // => false
|
|
1567
|
+
* ```
|
|
1568
|
+
*/
|
|
1388
1569
|
function validateKey(key, expectedType) {
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1570
|
+
if (!key || !key.includes(":")) return false;
|
|
1571
|
+
const keyType = parseKeyType(key);
|
|
1572
|
+
if (!keyType) return false;
|
|
1573
|
+
if (expectedType && keyType !== expectedType) return false;
|
|
1574
|
+
return true;
|
|
1575
|
+
}
|
|
1576
|
+
/**
|
|
1577
|
+
* Extract components from an observable key.
|
|
1578
|
+
*
|
|
1579
|
+
* Only reliable for the simple form `obs:{type}:{value}`. A key carrying a subtype or a
|
|
1580
|
+
* namespace, and a `command_line` or oversized key folded into `obs:{type}:sha256:{digest}`,
|
|
1581
|
+
* both return the remaining segments verbatim as `value` — a key is an identity token, not a
|
|
1582
|
+
* record. Read the observable from the document instead of parsing its key.
|
|
1583
|
+
*
|
|
1584
|
+
* @param key - Observable key to parse
|
|
1585
|
+
* @returns Object with type and value, or null if invalid
|
|
1586
|
+
*
|
|
1587
|
+
* @example
|
|
1588
|
+
* ```ts
|
|
1589
|
+
* parseObservableKey("obs:ipv4:192.168.1.1")
|
|
1590
|
+
* // => { type: "ipv4", value: "192.168.1.1" }
|
|
1591
|
+
* ```
|
|
1592
|
+
*/
|
|
1401
1593
|
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"
|
|
1594
|
+
if (!validateKey(key, "obs")) return null;
|
|
1595
|
+
const parts = key.split(":");
|
|
1596
|
+
if (parts.length >= 3) return {
|
|
1597
|
+
type: parts[1],
|
|
1598
|
+
value: parts.slice(2).join(":")
|
|
1599
|
+
};
|
|
1600
|
+
return null;
|
|
1601
|
+
}
|
|
1602
|
+
//#endregion
|
|
1603
|
+
//#region src/verdicts.ts
|
|
1604
|
+
/** Ordered from most exculpatory to most inculpatory. */
|
|
1605
|
+
const VERDICT_ORDER = [
|
|
1606
|
+
"SAFE",
|
|
1607
|
+
"INFO",
|
|
1608
|
+
"NOTABLE",
|
|
1609
|
+
"SUSPICIOUS",
|
|
1610
|
+
"MALICIOUS"
|
|
1450
1611
|
];
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1612
|
+
const VERDICT_RANK = {
|
|
1613
|
+
SAFE: 0,
|
|
1614
|
+
INFO: 1,
|
|
1615
|
+
NOTABLE: 2,
|
|
1616
|
+
SUSPICIOUS: 3,
|
|
1617
|
+
MALICIOUS: 4
|
|
1618
|
+
};
|
|
1619
|
+
/**
|
|
1620
|
+
* Rich style names, mirroring the Python terminal renderer.
|
|
1621
|
+
*
|
|
1622
|
+
* These are *not* CSS colours: a browser cannot draw `orange3`. Use {@link VERDICT_HEX_COLORS}
|
|
1623
|
+
* for anything rendered on screen.
|
|
1624
|
+
*/
|
|
1625
|
+
const VERDICT_TERMINAL_STYLES = {
|
|
1626
|
+
SAFE: "bright_green",
|
|
1627
|
+
INFO: "cyan",
|
|
1628
|
+
NOTABLE: "yellow",
|
|
1629
|
+
SUSPICIOUS: "orange3",
|
|
1630
|
+
MALICIOUS: "red"
|
|
1459
1631
|
};
|
|
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
|
|
1632
|
+
/** The web palette: muted enough that a wall of NOTABLE nodes stays readable. */
|
|
1633
|
+
const VERDICT_HEX_COLORS = {
|
|
1634
|
+
SAFE: "#648b79",
|
|
1635
|
+
INFO: "#94a3b8",
|
|
1636
|
+
NOTABLE: "#aa8958",
|
|
1637
|
+
SUSPICIOUS: "#ad704b",
|
|
1638
|
+
MALICIOUS: "#ad5555"
|
|
1475
1639
|
};
|
|
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
|
|
1640
|
+
/** Direction the judgment pushes: -1 exculpatory, 0 neutral, +1 inculpatory. */
|
|
1641
|
+
function verdictPolarity(verdict) {
|
|
1642
|
+
if (verdict === "SAFE") return -1;
|
|
1643
|
+
if (verdict === "INFO") return 0;
|
|
1644
|
+
return 1;
|
|
1645
|
+
}
|
|
1646
|
+
function isValidVerdict(value) {
|
|
1647
|
+
return typeof value === "string" && value in VERDICT_RANK;
|
|
1648
|
+
}
|
|
1649
|
+
function normalizeVerdict(value) {
|
|
1650
|
+
const upper = typeof value === "string" ? value.toUpperCase() : "";
|
|
1651
|
+
return isValidVerdict(upper) ? upper : "INFO";
|
|
1652
|
+
}
|
|
1653
|
+
function compareVerdicts(a, b) {
|
|
1654
|
+
return VERDICT_RANK[a] - VERDICT_RANK[b];
|
|
1655
|
+
}
|
|
1656
|
+
function isVerdictAtLeast(verdict, floor) {
|
|
1657
|
+
return VERDICT_RANK[verdict] >= VERDICT_RANK[floor];
|
|
1658
|
+
}
|
|
1659
|
+
function maxVerdict(verdicts) {
|
|
1660
|
+
return verdicts.reduce((best, current) => compareVerdicts(current, best) > 0 ? current : best, "SAFE");
|
|
1661
|
+
}
|
|
1662
|
+
function minVerdict(verdicts) {
|
|
1663
|
+
return verdicts.reduce((best, current) => compareVerdicts(current, best) < 0 ? current : best, "MALICIOUS");
|
|
1664
|
+
}
|
|
1665
|
+
function getColorForVerdict(verdict) {
|
|
1666
|
+
return VERDICT_HEX_COLORS[verdict] ?? VERDICT_HEX_COLORS.INFO;
|
|
1667
|
+
}
|
|
1668
|
+
/** Coarse bands for display; the engine works in floats. */
|
|
1669
|
+
function confidenceBand(confidence) {
|
|
1670
|
+
if (confidence < .5) return "low";
|
|
1671
|
+
if (confidence < .85) return "medium";
|
|
1672
|
+
return "high";
|
|
1673
|
+
}
|
|
1674
|
+
//#endregion
|
|
1675
|
+
//#region src/getters.ts
|
|
1552
1676
|
function getObservable(inv, key) {
|
|
1553
|
-
|
|
1677
|
+
return inv.facts?.observables?.[key];
|
|
1554
1678
|
}
|
|
1555
|
-
function
|
|
1556
|
-
|
|
1679
|
+
function getAllObservables(inv) {
|
|
1680
|
+
return inv.facts?.observables ?? {};
|
|
1557
1681
|
}
|
|
1558
1682
|
function getRootObservable(inv) {
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
return void 0;
|
|
1562
|
-
}
|
|
1563
|
-
const rootKey = generateObservableKey(rootType, "root");
|
|
1564
|
-
return inv.observables[rootKey];
|
|
1683
|
+
const rootKey = inv.header?.root_key;
|
|
1684
|
+
return rootKey ? getObservable(inv, rootKey) : void 0;
|
|
1565
1685
|
}
|
|
1566
|
-
function
|
|
1567
|
-
|
|
1686
|
+
function getRelation(inv, key) {
|
|
1687
|
+
return inv.facts?.relations?.[key];
|
|
1568
1688
|
}
|
|
1569
|
-
function
|
|
1570
|
-
|
|
1571
|
-
|
|
1689
|
+
function getAllRelations(inv) {
|
|
1690
|
+
return inv.facts?.relations ?? {};
|
|
1691
|
+
}
|
|
1692
|
+
function getThreatIntel(inv, key) {
|
|
1693
|
+
return inv.facts?.signals?.[key];
|
|
1694
|
+
}
|
|
1695
|
+
function getAllThreatIntels(inv) {
|
|
1696
|
+
return inv.facts?.signals ?? {};
|
|
1697
|
+
}
|
|
1698
|
+
/** Signals attached to one observable. */
|
|
1699
|
+
function getThreatIntelsFor(inv, observableKey) {
|
|
1700
|
+
return Object.values(getAllThreatIntels(inv)).filter((signal) => signal.subject_key === observableKey);
|
|
1701
|
+
}
|
|
1702
|
+
function getFinding(inv, key) {
|
|
1703
|
+
return inv.facts?.findings?.[key];
|
|
1572
1704
|
}
|
|
1573
1705
|
function getAllFindings(inv) {
|
|
1574
|
-
|
|
1706
|
+
return inv.facts?.findings ?? {};
|
|
1575
1707
|
}
|
|
1576
1708
|
function getEvidence(inv, key) {
|
|
1577
|
-
|
|
1709
|
+
return inv.facts?.evidences?.[key];
|
|
1578
1710
|
}
|
|
1579
1711
|
function getAllEvidences(inv) {
|
|
1580
|
-
|
|
1712
|
+
return inv.facts?.evidences ?? {};
|
|
1581
1713
|
}
|
|
1582
|
-
|
|
1583
|
-
|
|
1714
|
+
/** The v6 `Enrichment` is just an evidence type; filter instead of a dedicated accessor. */
|
|
1715
|
+
function getEvidencesByType(inv, evidenceType) {
|
|
1716
|
+
return Object.values(getAllEvidences(inv)).filter((evidence) => evidence.evidence_type === evidenceType);
|
|
1584
1717
|
}
|
|
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;
|
|
1718
|
+
function getDecision(inv, key) {
|
|
1719
|
+
return inv.decisions?.[key];
|
|
1593
1720
|
}
|
|
1594
|
-
function
|
|
1595
|
-
|
|
1721
|
+
function getAllDecisions(inv) {
|
|
1722
|
+
return inv.decisions ?? {};
|
|
1723
|
+
}
|
|
1724
|
+
/** The single stance standing on a target — the key carries the target alone. */
|
|
1725
|
+
function getDecisionFor(inv, targetKey) {
|
|
1726
|
+
return getDecision(inv, generateDecisionKey(targetKey));
|
|
1727
|
+
}
|
|
1728
|
+
function hasKind(inv, targetKey, kind) {
|
|
1729
|
+
return getDecisionFor(inv, targetKey)?.kind === kind;
|
|
1730
|
+
}
|
|
1731
|
+
function isAllowlisted(inv, observableKey) {
|
|
1732
|
+
return hasKind(inv, observableKey, "REFUTE");
|
|
1596
1733
|
}
|
|
1597
|
-
function
|
|
1598
|
-
|
|
1734
|
+
function isBlocklisted(inv, observableKey) {
|
|
1735
|
+
return hasKind(inv, observableKey, "UPHOLD");
|
|
1599
1736
|
}
|
|
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;
|
|
1737
|
+
function isConfirmed(inv, findingKey) {
|
|
1738
|
+
return hasKind(inv, findingKey, "UPHOLD");
|
|
1608
1739
|
}
|
|
1609
|
-
function
|
|
1610
|
-
|
|
1740
|
+
function isDismissed(inv, findingKey) {
|
|
1741
|
+
return hasKind(inv, findingKey, "REFUTE");
|
|
1742
|
+
}
|
|
1743
|
+
/** True when a stance was withdrawn and the computed value applies again. */
|
|
1744
|
+
function isVacated(inv, targetKey) {
|
|
1745
|
+
return hasKind(inv, targetKey, "VACATED");
|
|
1746
|
+
}
|
|
1747
|
+
/**
|
|
1748
|
+
* The analyst's word for a stance, rebuilt from the intent and the family of its target.
|
|
1749
|
+
*
|
|
1750
|
+
* The model carries one axis on purpose; the vocabulary that reads naturally carries two, and
|
|
1751
|
+
* the display layer is the right place to pay for that.
|
|
1752
|
+
*/
|
|
1753
|
+
function decisionLabel(decision) {
|
|
1754
|
+
if (decision.kind === "VACATED") return "VACATED";
|
|
1755
|
+
const observable = decision.target_key.startsWith("obs:");
|
|
1756
|
+
if (decision.kind === "UPHOLD") return observable ? "BLOCKLISTED" : "CONFIRMED";
|
|
1757
|
+
return observable ? "ALLOWLISTED" : "DISMISSED";
|
|
1611
1758
|
}
|
|
1612
1759
|
function getTag(inv, key) {
|
|
1613
|
-
|
|
1760
|
+
return inv.tags?.[key];
|
|
1614
1761
|
}
|
|
1615
1762
|
function getTagByName(inv, name) {
|
|
1616
|
-
|
|
1617
|
-
return inv.tags[key];
|
|
1763
|
+
return getTag(inv, `tag:${name.trim().toLowerCase()}`);
|
|
1618
1764
|
}
|
|
1619
1765
|
function getAllTags(inv) {
|
|
1620
|
-
|
|
1766
|
+
return inv.tags ?? {};
|
|
1621
1767
|
}
|
|
1622
|
-
function
|
|
1623
|
-
|
|
1768
|
+
function getInvestigationResult(inv) {
|
|
1769
|
+
return inv.report.investigation;
|
|
1624
1770
|
}
|
|
1625
|
-
function
|
|
1626
|
-
|
|
1771
|
+
function getObservableResult(inv, observableKey) {
|
|
1772
|
+
return inv.report.observables?.[observableKey];
|
|
1627
1773
|
}
|
|
1628
|
-
function
|
|
1629
|
-
|
|
1774
|
+
function getFindingResult(inv, findingKey) {
|
|
1775
|
+
return inv.report.findings?.[findingKey];
|
|
1630
1776
|
}
|
|
1631
|
-
function
|
|
1632
|
-
|
|
1777
|
+
function getObservableScore(inv, observableKey) {
|
|
1778
|
+
return getObservableResult(inv, observableKey)?.score ?? 0;
|
|
1633
1779
|
}
|
|
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));
|
|
1780
|
+
function getObservableVerdict(inv, observableKey) {
|
|
1781
|
+
return getObservableResult(inv, observableKey)?.verdict ?? "INFO";
|
|
1657
1782
|
}
|
|
1783
|
+
function getFindingScore(inv, findingKey) {
|
|
1784
|
+
return getFindingResult(inv, findingKey)?.score ?? 0;
|
|
1785
|
+
}
|
|
1786
|
+
function getFindingVerdict(inv, findingKey) {
|
|
1787
|
+
return getFindingResult(inv, findingKey)?.verdict ?? "INFO";
|
|
1788
|
+
}
|
|
1789
|
+
function getGlobalScore(inv) {
|
|
1790
|
+
return inv.report.investigation.score ?? 0;
|
|
1791
|
+
}
|
|
1792
|
+
function getGlobalVerdict(inv) {
|
|
1793
|
+
return inv.report.investigation.verdict ?? "INFO";
|
|
1794
|
+
}
|
|
1795
|
+
/** True when a decision or a stronger link overrode the computed value. */
|
|
1796
|
+
function wasSuppressed(inv, key) {
|
|
1797
|
+
const finding = getFindingResult(inv, key);
|
|
1798
|
+
if (finding) return Boolean(finding.suppressed_by_decision || finding.own_term_suppressed);
|
|
1799
|
+
return Boolean(getObservableResult(inv, key)?.suppressed_by_decision);
|
|
1800
|
+
}
|
|
1801
|
+
/** Findings a tag points at, plus those of every descendant tag. */
|
|
1802
|
+
function getTagFindingKeys(inv, tagName) {
|
|
1803
|
+
const prefix = `${tagName}:`;
|
|
1804
|
+
const keys = /* @__PURE__ */ new Set();
|
|
1805
|
+
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);
|
|
1806
|
+
return [...keys];
|
|
1807
|
+
}
|
|
1808
|
+
/**
|
|
1809
|
+
* A tag's aggregated score.
|
|
1810
|
+
*
|
|
1811
|
+
* This sums values the engine produced; it does not re-derive them. Uncounted findings — not
|
|
1812
|
+
* applicable, pending or dismissed — are absent from the sum *and* from any ratio built on it.
|
|
1813
|
+
*/
|
|
1658
1814
|
function getTagAggregatedScore(inv, tagName) {
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1815
|
+
return getTagFindingKeys(inv, tagName).reduce((total, key) => {
|
|
1816
|
+
const result = getFindingResult(inv, key);
|
|
1817
|
+
return result?.counted ? total + (result.score ?? 0) : total;
|
|
1818
|
+
}, 0);
|
|
1819
|
+
}
|
|
1820
|
+
function getCounts(inv) {
|
|
1821
|
+
return {
|
|
1822
|
+
observables: Object.keys(getAllObservables(inv)).length,
|
|
1823
|
+
relations: Object.keys(getAllRelations(inv)).length,
|
|
1824
|
+
signals: Object.keys(getAllThreatIntels(inv)).length,
|
|
1825
|
+
evidences: Object.keys(getAllEvidences(inv)).length,
|
|
1826
|
+
findings: Object.keys(getAllFindings(inv)).length,
|
|
1827
|
+
evaluatedFindings: Object.values(inv.report.findings ?? {}).filter((result) => result.counted).length,
|
|
1828
|
+
decisions: Object.keys(getAllDecisions(inv)).length,
|
|
1829
|
+
tags: Object.keys(getAllTags(inv)).length
|
|
1830
|
+
};
|
|
1831
|
+
}
|
|
1832
|
+
//#endregion
|
|
1833
|
+
//#region src/finders.ts
|
|
1834
|
+
/**
|
|
1835
|
+
* Query helpers.
|
|
1836
|
+
*
|
|
1837
|
+
* Filters on computed values go through the report; filters on stated values go through the
|
|
1838
|
+
* facts. Keeping the two apart is what stops a UI from quietly inventing its own scoring.
|
|
1839
|
+
*/
|
|
1675
1840
|
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));
|
|
1841
|
+
return Object.values(getAllObservables(inv)).filter((observable) => observable.type === type);
|
|
1842
|
+
}
|
|
1843
|
+
function findObservablesByValue(inv, value) {
|
|
1844
|
+
return Object.values(getAllObservables(inv)).filter((observable) => observable.value === value);
|
|
1845
|
+
}
|
|
1846
|
+
function findObservablesContaining(inv, fragment) {
|
|
1847
|
+
const needle = fragment.toLowerCase();
|
|
1848
|
+
return Object.values(getAllObservables(inv)).filter((observable) => observable.value.toLowerCase().includes(needle));
|
|
1705
1849
|
}
|
|
1706
1850
|
function findInternalObservables(inv) {
|
|
1707
|
-
|
|
1851
|
+
return Object.values(getAllObservables(inv)).filter((observable) => observable.internal === true);
|
|
1708
1852
|
}
|
|
1709
1853
|
function findExternalObservables(inv) {
|
|
1710
|
-
|
|
1854
|
+
return Object.values(getAllObservables(inv)).filter((observable) => observable.internal === false);
|
|
1711
1855
|
}
|
|
1712
|
-
function
|
|
1713
|
-
|
|
1856
|
+
function findObservablesByVerdict(inv, verdict) {
|
|
1857
|
+
return Object.values(getAllObservables(inv)).filter((observable) => getObservableResult(inv, observable.key)?.verdict === verdict);
|
|
1858
|
+
}
|
|
1859
|
+
function findObservablesAtLeast(inv, floor) {
|
|
1860
|
+
return Object.values(getAllObservables(inv)).filter((observable) => {
|
|
1861
|
+
const result = getObservableResult(inv, observable.key);
|
|
1862
|
+
return result ? isVerdictAtLeast(result.verdict, floor) : false;
|
|
1863
|
+
});
|
|
1714
1864
|
}
|
|
1715
1865
|
function findObservablesWithThreatIntel(inv) {
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1866
|
+
const subjects = new Set(Object.values(getAllThreatIntels(inv)).map((signal) => signal.subject_key));
|
|
1867
|
+
return Object.values(getAllObservables(inv)).filter((observable) => subjects.has(observable.key));
|
|
1868
|
+
}
|
|
1869
|
+
function findFindingsByVerdict(inv, verdict) {
|
|
1870
|
+
return Object.values(getAllFindings(inv)).filter((finding) => getFindingResult(inv, finding.key)?.verdict === verdict);
|
|
1719
1871
|
}
|
|
1720
|
-
function
|
|
1721
|
-
|
|
1872
|
+
function findFindingsAtLeast(inv, floor) {
|
|
1873
|
+
return Object.values(getAllFindings(inv)).filter((finding) => {
|
|
1874
|
+
const result = getFindingResult(inv, finding.key);
|
|
1875
|
+
return result?.counted ? isVerdictAtLeast(result.verdict, floor) : false;
|
|
1876
|
+
});
|
|
1722
1877
|
}
|
|
1723
|
-
function
|
|
1724
|
-
|
|
1725
|
-
(finding) => isLevelAtLeast(finding.level, minLevel2)
|
|
1726
|
-
);
|
|
1878
|
+
function findFindingsByRule(inv, ruleId) {
|
|
1879
|
+
return Object.values(getAllFindings(inv)).filter((finding) => finding.rule_id === ruleId);
|
|
1727
1880
|
}
|
|
1728
|
-
function
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1881
|
+
function findFindingsByConfidence(inv, band) {
|
|
1882
|
+
return Object.values(getAllFindings(inv)).filter((finding) => {
|
|
1883
|
+
const result = getFindingResult(inv, finding.key);
|
|
1884
|
+
return result ? confidenceBand(result.confidence ?? 1) === band : false;
|
|
1885
|
+
});
|
|
1886
|
+
}
|
|
1887
|
+
/** Findings excluded from the score but still worth showing, with their reason. */
|
|
1888
|
+
function findUncountedFindings(inv) {
|
|
1889
|
+
return Object.values(getAllFindings(inv)).filter((finding) => getFindingResult(inv, finding.key)?.counted === false);
|
|
1890
|
+
}
|
|
1891
|
+
/** Findings whose own claim was outweighed by one of their observables — worth surfacing. */
|
|
1892
|
+
function findContradictedFindings(inv) {
|
|
1893
|
+
return Object.values(getAllFindings(inv)).filter((finding) => getFindingResult(inv, finding.key)?.own_term_suppressed === true);
|
|
1733
1894
|
}
|
|
1734
1895
|
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);
|
|
1896
|
+
return Object.values(getAllThreatIntels(inv)).filter((signal) => signal.source.name === source);
|
|
1897
|
+
}
|
|
1898
|
+
function findThreatIntelByVerdict(inv, verdict) {
|
|
1899
|
+
return Object.values(getAllThreatIntels(inv)).filter((signal) => signal.verdict === verdict);
|
|
1900
|
+
}
|
|
1901
|
+
function findThreatIntelForObservable(inv, observableKey) {
|
|
1902
|
+
return Object.values(getAllThreatIntels(inv)).filter((signal) => signal.subject_key === observableKey);
|
|
1903
|
+
}
|
|
1904
|
+
//#endregion
|
|
1905
|
+
//#region src/graph.ts
|
|
1906
|
+
/**
|
|
1907
|
+
* Graph traversal.
|
|
1908
|
+
*
|
|
1909
|
+
* Relations are standalone facts in v7, each carrying both keys, so there is no per-observable
|
|
1910
|
+
* adjacency list to walk and no `direction` field to interpret: `source_key` is the parent,
|
|
1911
|
+
* `target_key` the child, and the kind implies the rest.
|
|
1912
|
+
*/
|
|
1913
|
+
function relationsOf(inv) {
|
|
1914
|
+
return Object.values(getAllRelations(inv));
|
|
1915
|
+
}
|
|
1916
|
+
/** Keys of relations the report credits with an actual contribution. */
|
|
1917
|
+
function scoringRelationKeys(inv) {
|
|
1918
|
+
const keys = /* @__PURE__ */ new Set();
|
|
1919
|
+
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);
|
|
1920
|
+
return keys;
|
|
1902
1921
|
}
|
|
1903
1922
|
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);
|
|
1923
|
+
const observables = getAllObservables(inv);
|
|
1924
|
+
return relationsOf(inv).filter((relation) => relation.source_key === observableKey).map((relation) => observables[relation.target_key]).filter((observable) => observable !== void 0);
|
|
1909
1925
|
}
|
|
1910
1926
|
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);
|
|
1927
|
+
const observables = getAllObservables(inv);
|
|
1928
|
+
return relationsOf(inv).filter((relation) => relation.target_key === observableKey).map((relation) => observables[relation.source_key]).filter((observable) => observable !== void 0);
|
|
1971
1929
|
}
|
|
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 };
|
|
1930
|
+
function getRelatedObservables(inv, observableKey) {
|
|
1931
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1932
|
+
for (const observable of [...getObservableChildren(inv, observableKey), ...getObservableParents(inv, observableKey)]) seen.set(observable.key, observable);
|
|
1933
|
+
return [...seen.values()];
|
|
2000
1934
|
}
|
|
2001
|
-
function
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
1935
|
+
function getRelationsForObservable(inv, observableKey) {
|
|
1936
|
+
return relationsOf(inv).filter((relation) => relation.source_key === observableKey || relation.target_key === observableKey);
|
|
1937
|
+
}
|
|
1938
|
+
function countRelationsByKind(inv) {
|
|
1939
|
+
const counts = {};
|
|
1940
|
+
for (const relation of relationsOf(inv)) counts[relation.kind ?? "related-to"] = (counts[relation.kind ?? "related-to"] ?? 0) + 1;
|
|
1941
|
+
return counts;
|
|
1942
|
+
}
|
|
1943
|
+
function getObservableGraph(inv) {
|
|
1944
|
+
const scoring = scoringRelationKeys(inv);
|
|
1945
|
+
return {
|
|
1946
|
+
nodes: Object.values(getAllObservables(inv)).map((observable) => {
|
|
1947
|
+
const result = getObservableResult(inv, observable.key);
|
|
1948
|
+
return {
|
|
1949
|
+
key: observable.key,
|
|
1950
|
+
observable,
|
|
1951
|
+
score: result?.score ?? 0,
|
|
1952
|
+
verdict: result?.verdict ?? "INFO"
|
|
1953
|
+
};
|
|
1954
|
+
}),
|
|
1955
|
+
edges: relationsOf(inv).map((relation) => ({
|
|
1956
|
+
key: relation.key,
|
|
1957
|
+
source: relation.source_key,
|
|
1958
|
+
target: relation.target_key,
|
|
1959
|
+
kind: relation.kind ?? "related-to",
|
|
1960
|
+
confidence: relation.confidence ?? 1,
|
|
1961
|
+
carriedScore: scoring.has(relation.key)
|
|
1962
|
+
}))
|
|
1963
|
+
};
|
|
1964
|
+
}
|
|
1965
|
+
function areConnected(inv, a, b) {
|
|
1966
|
+
return getReachableObservables(inv, a).has(b);
|
|
1967
|
+
}
|
|
1968
|
+
/** Undirected reachability — the same walk `finalize_relationships` uses to spot orphans. */
|
|
1969
|
+
function getReachableObservables(inv, start) {
|
|
1970
|
+
const relations = relationsOf(inv);
|
|
1971
|
+
const reached = /* @__PURE__ */ new Set([start]);
|
|
1972
|
+
const queue = [start];
|
|
1973
|
+
while (queue.length > 0) {
|
|
1974
|
+
const current = queue.shift();
|
|
1975
|
+
for (const relation of relations) {
|
|
1976
|
+
const neighbour = relation.source_key === current ? relation.target_key : relation.target_key === current ? relation.source_key : void 0;
|
|
1977
|
+
if (neighbour && !reached.has(neighbour)) {
|
|
1978
|
+
reached.add(neighbour);
|
|
1979
|
+
queue.push(neighbour);
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
return reached;
|
|
2013
1984
|
}
|
|
2014
1985
|
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
|
-
);
|
|
1986
|
+
const linked = /* @__PURE__ */ new Set();
|
|
1987
|
+
for (const relation of relationsOf(inv)) {
|
|
1988
|
+
linked.add(relation.source_key);
|
|
1989
|
+
linked.add(relation.target_key);
|
|
1990
|
+
}
|
|
1991
|
+
return Object.values(getAllObservables(inv)).filter((observable) => !linked.has(observable.key));
|
|
2027
1992
|
}
|
|
2028
1993
|
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
|
-
});
|
|
1994
|
+
const parents = new Set(relationsOf(inv).map((relation) => relation.source_key));
|
|
1995
|
+
return Object.values(getAllObservables(inv)).filter((observable) => !parents.has(observable.key));
|
|
1996
|
+
}
|
|
1997
|
+
function findSourceObservables(inv) {
|
|
1998
|
+
const children = new Set(relationsOf(inv).map((relation) => relation.target_key));
|
|
1999
|
+
return Object.values(getAllObservables(inv)).filter((observable) => !children.has(observable.key));
|
|
2000
|
+
}
|
|
2001
|
+
//#endregion
|
|
2002
|
+
exports.KEY_PREFIXES = KEY_PREFIXES;
|
|
2003
|
+
exports.SCHEMA_VERSION = SCHEMA_VERSION;
|
|
2004
|
+
exports.VERDICT_HEX_COLORS = VERDICT_HEX_COLORS;
|
|
2005
|
+
exports.VERDICT_ORDER = VERDICT_ORDER;
|
|
2006
|
+
exports.VERDICT_TERMINAL_STYLES = VERDICT_TERMINAL_STYLES;
|
|
2007
|
+
exports.areConnected = areConnected;
|
|
2008
|
+
exports.assertReadableVersion = assertReadableVersion;
|
|
2009
|
+
exports.compareVerdicts = compareVerdicts;
|
|
2010
|
+
exports.confidenceBand = confidenceBand;
|
|
2011
|
+
exports.countRelationsByKind = countRelationsByKind;
|
|
2012
|
+
exports.decisionLabel = decisionLabel;
|
|
2013
|
+
exports.detectSchemaVersion = detectSchemaVersion;
|
|
2014
|
+
exports.findContradictedFindings = findContradictedFindings;
|
|
2015
|
+
exports.findExternalObservables = findExternalObservables;
|
|
2016
|
+
exports.findFindingsAtLeast = findFindingsAtLeast;
|
|
2017
|
+
exports.findFindingsByConfidence = findFindingsByConfidence;
|
|
2018
|
+
exports.findFindingsByRule = findFindingsByRule;
|
|
2019
|
+
exports.findFindingsByVerdict = findFindingsByVerdict;
|
|
2020
|
+
exports.findInternalObservables = findInternalObservables;
|
|
2021
|
+
exports.findLeafObservables = findLeafObservables;
|
|
2022
|
+
exports.findObservablesAtLeast = findObservablesAtLeast;
|
|
2023
|
+
exports.findObservablesByType = findObservablesByType;
|
|
2024
|
+
exports.findObservablesByValue = findObservablesByValue;
|
|
2025
|
+
exports.findObservablesByVerdict = findObservablesByVerdict;
|
|
2026
|
+
exports.findObservablesContaining = findObservablesContaining;
|
|
2027
|
+
exports.findObservablesWithThreatIntel = findObservablesWithThreatIntel;
|
|
2028
|
+
exports.findOrphanObservables = findOrphanObservables;
|
|
2029
|
+
exports.findSourceObservables = findSourceObservables;
|
|
2030
|
+
exports.findThreatIntelBySource = findThreatIntelBySource;
|
|
2031
|
+
exports.findThreatIntelByVerdict = findThreatIntelByVerdict;
|
|
2032
|
+
exports.findThreatIntelForObservable = findThreatIntelForObservable;
|
|
2033
|
+
exports.findUncountedFindings = findUncountedFindings;
|
|
2034
|
+
exports.generateDecisionKey = generateDecisionKey;
|
|
2035
|
+
exports.generateFindingKey = generateFindingKey;
|
|
2036
|
+
exports.generateObservableKey = generateObservableKey;
|
|
2037
|
+
exports.generateRelationKey = generateRelationKey;
|
|
2038
|
+
exports.generateSignalKey = generateSignalKey;
|
|
2039
|
+
exports.generateTagKey = generateTagKey;
|
|
2040
|
+
exports.generateThreatIntelKey = generateThreatIntelKey;
|
|
2041
|
+
exports.getAllDecisions = getAllDecisions;
|
|
2042
|
+
exports.getAllEvidences = getAllEvidences;
|
|
2043
|
+
exports.getAllFindings = getAllFindings;
|
|
2044
|
+
exports.getAllObservables = getAllObservables;
|
|
2045
|
+
exports.getAllRelations = getAllRelations;
|
|
2046
|
+
exports.getAllTags = getAllTags;
|
|
2047
|
+
exports.getAllThreatIntels = getAllThreatIntels;
|
|
2048
|
+
exports.getColorForVerdict = getColorForVerdict;
|
|
2049
|
+
exports.getCounts = getCounts;
|
|
2050
|
+
exports.getDecision = getDecision;
|
|
2051
|
+
exports.getDecisionFor = getDecisionFor;
|
|
2052
|
+
exports.getEvidence = getEvidence;
|
|
2053
|
+
exports.getEvidencesByType = getEvidencesByType;
|
|
2054
|
+
exports.getFinding = getFinding;
|
|
2055
|
+
exports.getFindingResult = getFindingResult;
|
|
2056
|
+
exports.getFindingScore = getFindingScore;
|
|
2057
|
+
exports.getFindingVerdict = getFindingVerdict;
|
|
2058
|
+
exports.getGlobalScore = getGlobalScore;
|
|
2059
|
+
exports.getGlobalVerdict = getGlobalVerdict;
|
|
2060
|
+
exports.getInvestigationResult = getInvestigationResult;
|
|
2061
|
+
exports.getObservable = getObservable;
|
|
2062
|
+
exports.getObservableChildren = getObservableChildren;
|
|
2063
|
+
exports.getObservableGraph = getObservableGraph;
|
|
2064
|
+
exports.getObservableParents = getObservableParents;
|
|
2065
|
+
exports.getObservableResult = getObservableResult;
|
|
2066
|
+
exports.getObservableScore = getObservableScore;
|
|
2067
|
+
exports.getObservableVerdict = getObservableVerdict;
|
|
2068
|
+
exports.getReachableObservables = getReachableObservables;
|
|
2069
|
+
exports.getRelatedObservables = getRelatedObservables;
|
|
2070
|
+
exports.getRelation = getRelation;
|
|
2071
|
+
exports.getRelationsForObservable = getRelationsForObservable;
|
|
2072
|
+
exports.getRootObservable = getRootObservable;
|
|
2073
|
+
exports.getTag = getTag;
|
|
2074
|
+
exports.getTagAggregatedScore = getTagAggregatedScore;
|
|
2075
|
+
exports.getTagAncestors = getTagAncestors;
|
|
2076
|
+
exports.getTagByName = getTagByName;
|
|
2077
|
+
exports.getTagFindingKeys = getTagFindingKeys;
|
|
2078
|
+
exports.getThreatIntel = getThreatIntel;
|
|
2079
|
+
exports.getThreatIntelsFor = getThreatIntelsFor;
|
|
2080
|
+
exports.isAllowlisted = isAllowlisted;
|
|
2081
|
+
exports.isBlocklisted = isBlocklisted;
|
|
2082
|
+
exports.isConfirmed = isConfirmed;
|
|
2083
|
+
exports.isCyvest = isCyvest;
|
|
2084
|
+
exports.isDismissed = isDismissed;
|
|
2085
|
+
exports.isTagChildOf = isTagChildOf;
|
|
2086
|
+
exports.isTagDescendantOf = isTagDescendantOf;
|
|
2087
|
+
exports.isVacated = isVacated;
|
|
2088
|
+
exports.isValidVerdict = isValidVerdict;
|
|
2089
|
+
exports.isVerdictAtLeast = isVerdictAtLeast;
|
|
2090
|
+
exports.maxVerdict = maxVerdict;
|
|
2091
|
+
exports.minVerdict = minVerdict;
|
|
2092
|
+
exports.normalizeVerdict = normalizeVerdict;
|
|
2093
|
+
exports.parseCyvest = parseCyvest;
|
|
2094
|
+
exports.parseKeyType = parseKeyType;
|
|
2095
|
+
exports.parseObservableKey = parseObservableKey;
|
|
2096
|
+
exports.validateKey = validateKey;
|
|
2097
|
+
exports.verdictPolarity = verdictPolarity;
|
|
2098
|
+
exports.wasSuppressed = wasSuppressed;
|