@cyvest/cyvest-js 3.2.0 → 4.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.mjs CHANGED
@@ -5,6 +5,97 @@ import addFormats from "ajv-formats";
5
5
  // ../../../schema/cyvest.schema.json
6
6
  var cyvest_schema_default = {
7
7
  $defs: {
8
+ AuditEvent: {
9
+ additionalProperties: true,
10
+ description: "Centralized audit event for investigation-level changes.",
11
+ properties: {
12
+ event_id: {
13
+ title: "Event Id",
14
+ type: "string"
15
+ },
16
+ timestamp: {
17
+ format: "date-time",
18
+ title: "Timestamp",
19
+ type: "string"
20
+ },
21
+ event_type: {
22
+ title: "Event Type",
23
+ type: "string"
24
+ },
25
+ actor: {
26
+ anyOf: [
27
+ {
28
+ type: "string"
29
+ },
30
+ {
31
+ type: "null"
32
+ }
33
+ ],
34
+ default: null,
35
+ title: "Actor"
36
+ },
37
+ reason: {
38
+ anyOf: [
39
+ {
40
+ type: "string"
41
+ },
42
+ {
43
+ type: "null"
44
+ }
45
+ ],
46
+ default: null,
47
+ title: "Reason"
48
+ },
49
+ tool: {
50
+ anyOf: [
51
+ {
52
+ type: "string"
53
+ },
54
+ {
55
+ type: "null"
56
+ }
57
+ ],
58
+ default: null,
59
+ title: "Tool"
60
+ },
61
+ object_type: {
62
+ anyOf: [
63
+ {
64
+ type: "string"
65
+ },
66
+ {
67
+ type: "null"
68
+ }
69
+ ],
70
+ default: null,
71
+ title: "Object Type"
72
+ },
73
+ object_key: {
74
+ anyOf: [
75
+ {
76
+ type: "string"
77
+ },
78
+ {
79
+ type: "null"
80
+ }
81
+ ],
82
+ default: null,
83
+ title: "Object Key"
84
+ },
85
+ details: {
86
+ additionalProperties: true,
87
+ title: "Details",
88
+ type: "object"
89
+ }
90
+ },
91
+ required: [
92
+ "event_id",
93
+ "timestamp",
94
+ "event_type"
95
+ ],
96
+ title: "AuditEvent",
97
+ type: "object"
98
+ },
8
99
  Check: {
9
100
  description: "Represents a verification step in the investigation.\n\nA check validates a specific aspect of the data under investigation\nand contributes to the overall investigation score.",
10
101
  properties: {
@@ -36,17 +127,17 @@ var cyvest_schema_default = {
36
127
  level: {
37
128
  $ref: "#/$defs/Level"
38
129
  },
39
- observables: {
130
+ origin_investigation_id: {
131
+ title: "Origin Investigation Id",
132
+ type: "string"
133
+ },
134
+ observable_links: {
40
135
  items: {
41
- type: "string"
136
+ $ref: "#/$defs/ObservableLink"
42
137
  },
43
- title: "Observables",
138
+ title: "Observable Links",
44
139
  type: "array"
45
140
  },
46
- score_policy: {
47
- $ref: "#/$defs/CheckScorePolicy",
48
- default: "auto"
49
- },
50
141
  key: {
51
142
  title: "Key",
52
143
  type: "string"
@@ -65,22 +156,14 @@ var cyvest_schema_default = {
65
156
  "extra",
66
157
  "score",
67
158
  "level",
68
- "observables",
159
+ "origin_investigation_id",
160
+ "observable_links",
69
161
  "key",
70
162
  "score_display"
71
163
  ],
72
164
  title: "Check",
73
165
  type: "object"
74
166
  },
75
- CheckScorePolicy: {
76
- description: "Controls how a check reacts to linked observables.",
77
- enum: [
78
- "auto",
79
- "manual"
80
- ],
81
- title: "CheckScorePolicy",
82
- type: "string"
83
- },
84
167
  Container: {
85
168
  additionalProperties: false,
86
169
  description: "Groups checks and sub-containers for hierarchical organization.\n\nContainers allow structuring the investigation into logical sections\nwith aggregated scores and levels.",
@@ -290,13 +373,13 @@ var cyvest_schema_default = {
290
373
  title: "Key",
291
374
  type: "string"
292
375
  },
293
- generated_by_checks: {
294
- description: "Checks that generated this observable.",
376
+ check_links: {
377
+ description: "Checks that currently link to this observable (navigation-only).",
295
378
  items: {
296
379
  type: "string"
297
380
  },
298
381
  readOnly: true,
299
- title: "Generated By Checks",
382
+ title: "Check Links",
300
383
  type: "array"
301
384
  },
302
385
  score_display: {
@@ -317,12 +400,40 @@ var cyvest_schema_default = {
317
400
  "threat_intels",
318
401
  "relationships",
319
402
  "key",
320
- "generated_by_checks",
403
+ "check_links",
321
404
  "score_display"
322
405
  ],
323
406
  title: "Observable",
324
407
  type: "object"
325
408
  },
409
+ ObservableLink: {
410
+ additionalProperties: false,
411
+ description: "Edge metadata for a Check\u2194Observable association.",
412
+ properties: {
413
+ observable_key: {
414
+ title: "Observable Key",
415
+ type: "string"
416
+ },
417
+ propagation_mode: {
418
+ $ref: "#/$defs/PropagationMode",
419
+ default: "LOCAL_ONLY"
420
+ }
421
+ },
422
+ required: [
423
+ "observable_key"
424
+ ],
425
+ title: "ObservableLink",
426
+ type: "object"
427
+ },
428
+ PropagationMode: {
429
+ description: "Controls how a Check\u2194Observable link propagates across merged investigations.",
430
+ enum: [
431
+ "LOCAL_ONLY",
432
+ "GLOBAL"
433
+ ],
434
+ title: "PropagationMode",
435
+ type: "string"
436
+ },
326
437
  Relationship: {
327
438
  description: "Represents a relationship between observables.",
328
439
  properties: {
@@ -569,6 +680,24 @@ var cyvest_schema_default = {
569
680
  additionalProperties: false,
570
681
  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.",
571
682
  properties: {
683
+ investigation_id: {
684
+ description: "Stable investigation identity (ULID).",
685
+ title: "Investigation Id",
686
+ type: "string"
687
+ },
688
+ investigation_name: {
689
+ anyOf: [
690
+ {
691
+ type: "string"
692
+ },
693
+ {
694
+ type: "null"
695
+ }
696
+ ],
697
+ default: null,
698
+ description: "Optional human-readable investigation name.",
699
+ title: "Investigation Name"
700
+ },
572
701
  started_at: {
573
702
  description: "Investigation start time (UTC).",
574
703
  format: "date-time",
@@ -597,6 +726,14 @@ var cyvest_schema_default = {
597
726
  title: "Whitelists",
598
727
  type: "array"
599
728
  },
729
+ event_log: {
730
+ description: "Append-only investigation audit log.",
731
+ items: {
732
+ $ref: "#/$defs/AuditEvent"
733
+ },
734
+ title: "Event Log",
735
+ type: "array"
736
+ },
600
737
  observables: {
601
738
  additionalProperties: {
602
739
  $ref: "#/$defs/Observable"
@@ -671,6 +808,7 @@ var cyvest_schema_default = {
671
808
  }
672
809
  },
673
810
  required: [
811
+ "investigation_id",
674
812
  "started_at",
675
813
  "score",
676
814
  "level",
@@ -1165,17 +1303,6 @@ function findChecksByCheckId(inv, checkId) {
1165
1303
  }
1166
1304
  return result;
1167
1305
  }
1168
- function findManuallyScored(inv) {
1169
- const result = [];
1170
- for (const checks of Object.values(inv.checks)) {
1171
- for (const check of checks) {
1172
- if (check.score_policy === "manual") {
1173
- result.push(check);
1174
- }
1175
- }
1176
- }
1177
- return result;
1178
- }
1179
1306
  function findThreatIntelBySource(inv, source) {
1180
1307
  const normalizedSource = source.trim().toLowerCase();
1181
1308
  return Object.values(inv.threat_intels).filter(
@@ -1218,13 +1345,32 @@ function findContainersAtLeast(inv, minLevel2) {
1218
1345
  }
1219
1346
  function getChecksForObservable(inv, observableKey) {
1220
1347
  const result = [];
1348
+ const seen = /* @__PURE__ */ new Set();
1349
+ const checkLookup = /* @__PURE__ */ new Map();
1221
1350
  for (const checks of Object.values(inv.checks)) {
1222
1351
  for (const check of checks) {
1223
- if (check.observables.includes(observableKey)) {
1352
+ checkLookup.set(check.key, check);
1353
+ }
1354
+ }
1355
+ const observable = inv.observables[observableKey];
1356
+ if (observable) {
1357
+ for (const checkKey of observable.check_links) {
1358
+ const check = checkLookup.get(checkKey);
1359
+ if (check && !seen.has(check.key)) {
1224
1360
  result.push(check);
1361
+ seen.add(check.key);
1225
1362
  }
1226
1363
  }
1227
1364
  }
1365
+ for (const check of checkLookup.values()) {
1366
+ if (seen.has(check.key)) {
1367
+ continue;
1368
+ }
1369
+ if (check.observable_links.some((link) => link.observable_key === observableKey)) {
1370
+ result.push(check);
1371
+ seen.add(check.key);
1372
+ }
1373
+ }
1228
1374
  return result;
1229
1375
  }
1230
1376
  function getThreatIntelsForObservable(inv, observableKey) {
@@ -1240,7 +1386,11 @@ function getObservablesForCheck(inv, checkKey) {
1240
1386
  for (const checks of Object.values(inv.checks)) {
1241
1387
  for (const check of checks) {
1242
1388
  if (check.key === checkKey) {
1243
- return check.observables.map((obsKey) => inv.observables[obsKey]).filter((obs) => obs !== void 0);
1389
+ const keys = /* @__PURE__ */ new Set();
1390
+ for (const link of check.observable_links) {
1391
+ keys.add(link.observable_key);
1392
+ }
1393
+ return Array.from(keys).map((obsKey) => inv.observables[obsKey]).filter((obs) => obs !== void 0);
1244
1394
  }
1245
1395
  }
1246
1396
  }
@@ -1612,7 +1762,6 @@ export {
1612
1762
  findExternalObservables,
1613
1763
  findInternalObservables,
1614
1764
  findLeafObservables,
1615
- findManuallyScored,
1616
1765
  findObservablesAtLeast,
1617
1766
  findObservablesByLevel,
1618
1767
  findObservablesByType,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyvest/cyvest-js",
3
- "version": "3.2.0",
3
+ "version": "4.0.0",
4
4
  "main": "dist/index.cjs",
5
5
  "module": "dist/index.mjs",
6
6
  "types": "dist/index.d.ts",
@@ -10,10 +10,10 @@
10
10
  "ajv-formats": "^3.0.1"
11
11
  },
12
12
  "devDependencies": {
13
- "json-schema-to-typescript": "^13.1.1",
14
- "tsup": "^8.0.0",
15
- "typescript": "^5.6.0",
16
- "vitest": "^2.0.0"
13
+ "json-schema-to-typescript": "^15.0.4",
14
+ "tsup": "^8.5.1",
15
+ "typescript": "^5.9.3",
16
+ "vitest": "^4.0.16"
17
17
  },
18
18
  "engines": {
19
19
  "node": ">=20"
package/src/finders.ts CHANGED
@@ -288,24 +288,6 @@ export function findChecksByCheckId(
288
288
  return result;
289
289
  }
290
290
 
291
- /**
292
- * Find checks with score policy set to manual.
293
- *
294
- * @param inv - The investigation to search
295
- * @returns Array of manually scored checks
296
- */
297
- export function findManuallyScored(inv: CyvestInvestigation): Check[] {
298
- const result: Check[] = [];
299
- for (const checks of Object.values(inv.checks)) {
300
- for (const check of checks) {
301
- if (check.score_policy === "manual") {
302
- result.push(check);
303
- }
304
- }
305
- }
306
- return result;
307
- }
308
-
309
291
  // ============================================================================
310
292
  // Threat Intel Finders
311
293
  // ============================================================================
@@ -434,15 +416,37 @@ export function getChecksForObservable(
434
416
  observableKey: string
435
417
  ): Check[] {
436
418
  const result: Check[] = [];
419
+ const seen = new Set<string>();
420
+ const checkLookup = new Map<string, Check>();
437
421
 
438
422
  for (const checks of Object.values(inv.checks)) {
439
423
  for (const check of checks) {
440
- if (check.observables.includes(observableKey)) {
424
+ checkLookup.set(check.key, check);
425
+ }
426
+ }
427
+
428
+ const observable = inv.observables[observableKey];
429
+ if (observable) {
430
+ for (const checkKey of observable.check_links) {
431
+ const check = checkLookup.get(checkKey);
432
+ if (check && !seen.has(check.key)) {
441
433
  result.push(check);
434
+ seen.add(check.key);
442
435
  }
443
436
  }
444
437
  }
445
438
 
439
+ for (const check of checkLookup.values()) {
440
+ if (seen.has(check.key)) {
441
+ continue;
442
+ }
443
+
444
+ if (check.observable_links.some((link) => link.observable_key === observableKey)) {
445
+ result.push(check);
446
+ seen.add(check.key);
447
+ }
448
+ }
449
+
446
450
  return result;
447
451
  }
448
452
 
@@ -486,7 +490,12 @@ export function getObservablesForCheck(
486
490
  for (const checks of Object.values(inv.checks)) {
487
491
  for (const check of checks) {
488
492
  if (check.key === checkKey) {
489
- return check.observables
493
+ const keys = new Set<string>();
494
+ for (const link of check.observable_links) {
495
+ keys.add(link.observable_key);
496
+ }
497
+
498
+ return Array.from(keys)
490
499
  .map((obsKey) => inv.observables[obsKey])
491
500
  .filter((obs): obs is Observable => obs !== undefined);
492
501
  }
@@ -1,5 +1,9 @@
1
1
  // AUTO-GENERATED FROM cyvest.schema.json — DO NOT EDIT
2
2
 
3
+ /**
4
+ * Optional human-readable investigation name.
5
+ */
6
+ export type InvestigationName = string | null;
3
7
  /**
4
8
  * Security level classification for checks, observables, and threat intelligence.
5
9
  *
@@ -11,6 +15,15 @@ export type Justification = string | null;
11
15
  * List of whitelist entries applied to this investigation.
12
16
  */
13
17
  export type Whitelists = InvestigationWhitelist[];
18
+ export type Actor = string | null;
19
+ export type Reason = string | null;
20
+ export type Tool = string | null;
21
+ export type ObjectType = string | null;
22
+ export type ObjectKey = string | null;
23
+ /**
24
+ * Append-only investigation audit log.
25
+ */
26
+ export type EventLog = AuditEvent[];
14
27
  export type ThreatIntels = string[];
15
28
  /**
16
29
  * Direction of a relationship between observables.
@@ -18,14 +31,14 @@ export type ThreatIntels = string[];
18
31
  export type RelationshipDirection = "outbound" | "inbound" | "bidirectional";
19
32
  export type Relationships = Relationship[];
20
33
  /**
21
- * Checks that generated this observable.
34
+ * Checks that currently link to this observable (navigation-only).
22
35
  */
23
- export type GeneratedByChecks = string[];
24
- export type Observables1 = string[];
36
+ export type CheckLinks = string[];
25
37
  /**
26
- * Controls how a check reacts to linked observables.
38
+ * Controls how a Check↔Observable link propagates across merged investigations.
27
39
  */
28
- export type CheckScorePolicy = "auto" | "manual";
40
+ export type PropagationMode = "LOCAL_ONLY" | "GLOBAL";
41
+ export type ObservableLinks = ObservableLink[];
29
42
  export type Taxonomies = {
30
43
  [k: string]: unknown;
31
44
  }[];
@@ -50,6 +63,11 @@ export type ScoreMode = "max" | "sum";
50
63
  * schemas matching the actual model_dump() output.
51
64
  */
52
65
  export interface CyvestInvestigation {
66
+ /**
67
+ * Stable investigation identity (ULID).
68
+ */
69
+ investigation_id: string;
70
+ investigation_name?: InvestigationName;
53
71
  /**
54
72
  * Investigation start time (UTC).
55
73
  */
@@ -64,6 +82,7 @@ export interface CyvestInvestigation {
64
82
  */
65
83
  whitelisted: boolean;
66
84
  whitelists: Whitelists;
85
+ event_log?: EventLog;
67
86
  observables: Observables;
68
87
  checks: Checks;
69
88
  checks_by_level: ChecksByLevel;
@@ -87,6 +106,24 @@ export interface InvestigationWhitelist {
87
106
  justification?: Justification;
88
107
  [k: string]: unknown;
89
108
  }
109
+ /**
110
+ * Centralized audit event for investigation-level changes.
111
+ */
112
+ export interface AuditEvent {
113
+ event_id: string;
114
+ timestamp: string;
115
+ event_type: string;
116
+ actor?: Actor;
117
+ reason?: Reason;
118
+ tool?: Tool;
119
+ object_type?: ObjectType;
120
+ object_key?: ObjectKey;
121
+ details?: Details;
122
+ [k: string]: unknown;
123
+ }
124
+ export interface Details {
125
+ [k: string]: unknown;
126
+ }
90
127
  /**
91
128
  * Observables keyed by their unique key.
92
129
  */
@@ -111,7 +148,7 @@ export interface Observable {
111
148
  threat_intels: ThreatIntels;
112
149
  relationships: Relationships;
113
150
  key: string;
114
- generated_by_checks: GeneratedByChecks;
151
+ check_links: CheckLinks;
115
152
  score_display: string;
116
153
  [k: string]: unknown;
117
154
  }
@@ -147,8 +184,8 @@ export interface Check {
147
184
  extra: Extra1;
148
185
  score: number;
149
186
  level: Level;
150
- observables: Observables1;
151
- score_policy?: CheckScorePolicy;
187
+ origin_investigation_id: string;
188
+ observable_links: ObservableLinks;
152
189
  key: string;
153
190
  score_display: string;
154
191
  [k: string]: unknown;
@@ -156,6 +193,13 @@ export interface Check {
156
193
  export interface Extra1 {
157
194
  [k: string]: unknown;
158
195
  }
196
+ /**
197
+ * Edge metadata for a Check↔Observable association.
198
+ */
199
+ export interface ObservableLink {
200
+ observable_key: string;
201
+ propagation_mode?: PropagationMode;
202
+ }
159
203
  /**
160
204
  * Check keys organized by level name.
161
205
  */
@@ -41,7 +41,11 @@ import {
41
41
  // Test fixture
42
42
  function createTestInvestigation(): CyvestInvestigation {
43
43
  return {
44
+ investigation_id: "01HXYZTESTINVESTIGATION",
45
+ investigation_name: "Test Investigation",
46
+ started_at: "2024-01-01T00:00:00Z",
44
47
  score: 7.5,
48
+ score_display: "7.50",
45
49
  level: "MALICIOUS",
46
50
  whitelisted: false,
47
51
  whitelists: [
@@ -59,8 +63,9 @@ function createTestInvestigation(): CyvestInvestigation {
59
63
  internal: true,
60
64
  whitelisted: false,
61
65
  comment: "",
62
- extra: null,
66
+ extra: {},
63
67
  score: 0,
68
+ score_display: "0.00",
64
69
  level: "INFO",
65
70
  relationships: [
66
71
  {
@@ -70,7 +75,7 @@ function createTestInvestigation(): CyvestInvestigation {
70
75
  },
71
76
  ],
72
77
  threat_intels: [],
73
- generated_by_checks: ["chk:ip_check:network"],
78
+ check_links: ["chk:ip_check:network"],
74
79
  },
75
80
  "obs:ipv4-addr:8.8.8.8": {
76
81
  key: "obs:ipv4-addr:8.8.8.8",
@@ -79,12 +84,13 @@ function createTestInvestigation(): CyvestInvestigation {
79
84
  internal: false,
80
85
  whitelisted: true,
81
86
  comment: "Google DNS",
82
- extra: null,
87
+ extra: {},
83
88
  score: -1,
89
+ score_display: "-1.00",
84
90
  level: "TRUSTED",
85
91
  relationships: [],
86
92
  threat_intels: [],
87
- generated_by_checks: [],
93
+ check_links: [],
88
94
  },
89
95
  "obs:domain-name:example.com": {
90
96
  key: "obs:domain-name:example.com",
@@ -93,12 +99,13 @@ function createTestInvestigation(): CyvestInvestigation {
93
99
  internal: false,
94
100
  whitelisted: false,
95
101
  comment: "",
96
- extra: null,
102
+ extra: {},
97
103
  score: 5,
104
+ score_display: "5.00",
98
105
  level: "MALICIOUS",
99
106
  relationships: [],
100
107
  threat_intels: ["ti:virustotal:obs:domain-name:example.com"],
101
- generated_by_checks: ["chk:domain_check:dns"],
108
+ check_links: ["chk:domain_check:dns"],
102
109
  },
103
110
  "obs:url:http://malware.com/bad": {
104
111
  key: "obs:url:http://malware.com/bad",
@@ -107,12 +114,13 @@ function createTestInvestigation(): CyvestInvestigation {
107
114
  internal: false,
108
115
  whitelisted: false,
109
116
  comment: "",
110
- extra: null,
117
+ extra: {},
111
118
  score: 7.5,
119
+ score_display: "7.50",
112
120
  level: "MALICIOUS",
113
121
  relationships: [],
114
122
  threat_intels: [],
115
- generated_by_checks: [],
123
+ check_links: [],
116
124
  },
117
125
  },
118
126
  checks: {
@@ -123,11 +131,16 @@ function createTestInvestigation(): CyvestInvestigation {
123
131
  scope: "network",
124
132
  description: "IP address check",
125
133
  comment: "",
126
- extra: null,
134
+ extra: {},
127
135
  score: 0,
136
+ score_display: "0.00",
128
137
  level: "INFO",
129
- score_policy: "auto",
130
- observables: ["obs:ipv4-addr:192.168.1.1"],
138
+ origin_investigation_id: "01HXYZTESTINVESTIGATION",
139
+ observable_links: [
140
+ {
141
+ observable_key: "obs:ipv4-addr:192.168.1.1",
142
+ },
143
+ ],
131
144
  },
132
145
  ],
133
146
  dns: [
@@ -137,11 +150,16 @@ function createTestInvestigation(): CyvestInvestigation {
137
150
  scope: "dns",
138
151
  description: "Domain reputation check",
139
152
  comment: "",
140
- extra: null,
153
+ extra: {},
141
154
  score: 5,
155
+ score_display: "5.00",
142
156
  level: "MALICIOUS",
143
- score_policy: "auto",
144
- observables: ["obs:domain-name:example.com"],
157
+ origin_investigation_id: "01HXYZTESTINVESTIGATION",
158
+ observable_links: [
159
+ {
160
+ observable_key: "obs:domain-name:example.com",
161
+ },
162
+ ],
145
163
  },
146
164
  {
147
165
  key: "chk:dns_lookup:dns",
@@ -149,11 +167,12 @@ function createTestInvestigation(): CyvestInvestigation {
149
167
  scope: "dns",
150
168
  description: "DNS lookup",
151
169
  comment: "",
152
- extra: null,
170
+ extra: {},
153
171
  score: 0,
172
+ score_display: "0.00",
154
173
  level: "INFO",
155
- score_policy: "manual",
156
- observables: [],
174
+ origin_investigation_id: "01HXYZTESTINVESTIGATION",
175
+ observable_links: [],
157
176
  },
158
177
  ],
159
178
  },
@@ -167,8 +186,9 @@ function createTestInvestigation(): CyvestInvestigation {
167
186
  source: "virustotal",
168
187
  observable_key: "obs:domain-name:example.com",
169
188
  comment: "",
170
- extra: null,
189
+ extra: {},
171
190
  score: 5,
191
+ score_display: "5.00",
172
192
  level: "MALICIOUS",
173
193
  taxonomies: [{ verdict: "malicious" }],
174
194
  },