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