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