@cyvest/cyvest-js 4.4.1 → 5.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.
@@ -81,7 +81,7 @@ export interface CyvestInvestigation {
81
81
  checks: Checks;
82
82
  threat_intels: ThreatIntels1;
83
83
  enrichments: Enrichments;
84
- containers: Containers;
84
+ tags: Tags;
85
85
  stats: StatisticsSchema;
86
86
  data_extraction: DataExtractionSchema;
87
87
  /**
@@ -157,10 +157,10 @@ export interface Relationship {
157
157
  [k: string]: unknown;
158
158
  }
159
159
  /**
160
- * Checks organized by scope.
160
+ * Checks keyed by their unique key.
161
161
  */
162
162
  export interface Checks {
163
- [k: string]: Check[];
163
+ [k: string]: Check;
164
164
  }
165
165
  /**
166
166
  * Represents a verification step in the investigation.
@@ -169,8 +169,7 @@ export interface Checks {
169
169
  * and contributes to the overall investigation score.
170
170
  */
171
171
  export interface Check {
172
- check_id: string;
173
- scope: string;
172
+ check_name: string;
174
173
  description: string;
175
174
  comment: string;
176
175
  extra: Extra1;
@@ -250,28 +249,34 @@ export interface Data {
250
249
  [k: string]: unknown;
251
250
  }
252
251
  /**
253
- * Containers keyed by their unique key.
252
+ * Tags keyed by their unique key.
254
253
  */
255
- export interface Containers {
256
- [k: string]: Container;
254
+ export interface Tags {
255
+ [k: string]: Tag;
257
256
  }
258
257
  /**
259
- * Groups checks and sub-containers for hierarchical organization.
258
+ * Groups checks for categorical organization.
260
259
  *
261
- * Containers allow structuring the investigation into logical sections
262
- * with aggregated scores and levels.
260
+ * Tags allow structuring the investigation into logical sections
261
+ * with aggregated scores and levels. Hierarchy is automatic based on
262
+ * the ":" delimiter in tag names (e.g., "header:auth:dkim").
263
263
  */
264
- export interface Container {
265
- path: string;
264
+ export interface Tag {
265
+ name: string;
266
266
  description?: string;
267
267
  checks: Checks1;
268
- sub_containers: SubContainers;
269
268
  key: string;
270
- aggregated_score: number;
271
- aggregated_level: Level;
272
- }
273
- export interface SubContainers {
274
- [k: string]: Container;
269
+ /**
270
+ * Calculate the score from direct checks only (no hierarchy).
271
+ *
272
+ * For hierarchical aggregation (including descendant tags), use
273
+ * Investigation.get_tag_aggregated_score() or TagProxy.get_aggregated_score().
274
+ *
275
+ * Returns:
276
+ * Total score from direct checks
277
+ */
278
+ direct_score: number;
279
+ direct_level: Level;
275
280
  }
276
281
  /**
277
282
  * Schema for investigation statistics.
@@ -288,12 +293,11 @@ export interface StatisticsSchema {
288
293
  observables_by_type_and_level?: ObservablesByTypeAndLevel;
289
294
  total_checks: number;
290
295
  applied_checks: number;
291
- checks_by_scope?: ChecksByScope;
292
296
  checks_by_level?: ChecksByLevel;
293
297
  total_threat_intel: number;
294
298
  threat_intel_by_source?: ThreatIntelBySource;
295
299
  threat_intel_by_level?: ThreatIntelByLevel;
296
- total_containers: number;
300
+ total_tags: number;
297
301
  }
298
302
  export interface ObservablesByType {
299
303
  [k: string]: number;
@@ -306,9 +310,6 @@ export interface ObservablesByTypeAndLevel {
306
310
  [k: string]: number;
307
311
  };
308
312
  }
309
- export interface ChecksByScope {
310
- [k: string]: string[];
311
- }
312
313
  export interface ChecksByLevel {
313
314
  [k: string]: string[];
314
315
  }
@@ -5,7 +5,7 @@ import {
5
5
  getObservable,
6
6
  getObservableByTypeValue,
7
7
  getCheck,
8
- getCheckByIdScope,
8
+ getCheckByName,
9
9
  getAllChecks,
10
10
  getThreatIntel,
11
11
  getThreatIntelBySourceObservable,
@@ -13,12 +13,16 @@ import {
13
13
  getEnrichment,
14
14
  getEnrichmentByName,
15
15
  getAllEnrichments,
16
- getContainer,
17
- getContainerByPath,
18
- getAllContainers,
16
+ getTag,
17
+ getTagByName,
18
+ getAllTags,
19
19
  getAllObservables,
20
20
  getCounts,
21
21
  getStartedAt,
22
+ getTagChildren,
23
+ getTagDescendants,
24
+ getTagAggregatedScore,
25
+ getTagAggregatedLevel,
22
26
  // Finders
23
27
  findObservablesByType,
24
28
  findObservablesByLevel,
@@ -26,16 +30,15 @@ import {
26
30
  findObservablesByValue,
27
31
  findInternalObservables,
28
32
  findWhitelistedObservables,
29
- findChecksByScope,
30
33
  findChecksByLevel,
31
34
  findChecksAtLeast,
32
35
  findThreatIntelBySource,
33
- getChecksForObservable,
34
- getThreatIntelsForObservable,
35
- getObservablesForCheck,
36
- getHighestScoringObservables,
37
- getMaliciousObservables,
38
- getAllScopes,
36
+ findChecksForObservable,
37
+ findThreatIntelsForObservable,
38
+ findObservablesForCheck,
39
+ findHighestScoringObservables,
40
+ findMaliciousObservables,
41
+ getAllCheckKeys,
39
42
  getAllObservableTypes,
40
43
  } from "../src";
41
44
 
@@ -133,57 +136,50 @@ function createTestInvestigation(): CyvestInvestigation {
133
136
  },
134
137
  },
135
138
  checks: {
136
- network: [
137
- {
138
- key: "chk:ip_check:network",
139
- check_id: "ip_check",
140
- scope: "network",
141
- description: "IP address check",
142
- comment: "",
143
- extra: {},
144
- score: 0,
145
- score_display: "0.00",
146
- level: "INFO",
147
- origin_investigation_id: "01HXYZTESTINVESTIGATION",
148
- observable_links: [
149
- {
150
- observable_key: "obs:ipv4-addr:192.168.1.1",
151
- },
152
- ],
153
- },
154
- ],
155
- dns: [
156
- {
157
- key: "chk:domain_check:dns",
158
- check_id: "domain_check",
159
- scope: "dns",
160
- description: "Domain reputation check",
161
- comment: "",
162
- extra: {},
163
- score: 5,
164
- score_display: "5.00",
165
- level: "MALICIOUS",
166
- origin_investigation_id: "01HXYZTESTINVESTIGATION",
167
- observable_links: [
168
- {
169
- observable_key: "obs:domain-name:example.com",
170
- },
171
- ],
172
- },
173
- {
174
- key: "chk:dns_lookup:dns",
175
- check_id: "dns_lookup",
176
- scope: "dns",
177
- description: "DNS lookup",
178
- comment: "",
179
- extra: {},
180
- score: 0,
181
- score_display: "0.00",
182
- level: "INFO",
183
- origin_investigation_id: "01HXYZTESTINVESTIGATION",
184
- observable_links: [],
185
- },
186
- ],
139
+ "chk:ip_check": {
140
+ key: "chk:ip_check",
141
+ check_name: "ip_check",
142
+ description: "IP address check",
143
+ comment: "",
144
+ extra: {},
145
+ score: 0,
146
+ score_display: "0.00",
147
+ level: "INFO",
148
+ origin_investigation_id: "01HXYZTESTINVESTIGATION",
149
+ observable_links: [
150
+ {
151
+ observable_key: "obs:ipv4-addr:192.168.1.1",
152
+ },
153
+ ],
154
+ },
155
+ "chk:domain_check": {
156
+ key: "chk:domain_check",
157
+ check_name: "domain_check",
158
+ description: "Domain reputation check",
159
+ comment: "",
160
+ extra: {},
161
+ score: 5,
162
+ score_display: "5.00",
163
+ level: "MALICIOUS",
164
+ origin_investigation_id: "01HXYZTESTINVESTIGATION",
165
+ observable_links: [
166
+ {
167
+ observable_key: "obs:domain-name:example.com",
168
+ },
169
+ ],
170
+ },
171
+ "chk:dns_lookup": {
172
+ key: "chk:dns_lookup",
173
+ check_name: "dns_lookup",
174
+ description: "DNS lookup",
175
+ comment: "",
176
+ extra: {},
177
+ score: 0,
178
+ score_display: "0.00",
179
+ level: "INFO",
180
+ origin_investigation_id: "01HXYZTESTINVESTIGATION",
181
+ observable_links: [],
182
+ },
187
183
  },
188
184
  threat_intels: {
189
185
  "ti:virustotal:obs:domain-name:example.com": {
@@ -206,25 +202,38 @@ function createTestInvestigation(): CyvestInvestigation {
206
202
  context: "example.com",
207
203
  },
208
204
  },
209
- containers: {
210
- "ctr:email": {
211
- key: "ctr:email",
212
- path: "email",
213
- description: "Email container",
205
+ tags: {
206
+ "tag:email": {
207
+ key: "tag:email",
208
+ name: "email",
209
+ description: "Email tag",
214
210
  checks: [],
215
- sub_containers: {
216
- "ctr:email/headers": {
217
- key: "ctr:email/headers",
218
- path: "email/headers",
219
- description: "Email headers",
220
- checks: ["chk:ip_check:network"],
221
- sub_containers: {},
222
- aggregated_score: 0,
223
- aggregated_level: "INFO",
224
- },
225
- },
226
- aggregated_score: 0,
227
- aggregated_level: "INFO",
211
+ direct_score: 1.5,
212
+ direct_level: "NOTABLE",
213
+ },
214
+ "tag:email:headers": {
215
+ key: "tag:email:headers",
216
+ name: "email:headers",
217
+ description: "Email headers",
218
+ checks: ["chk:ip_check"],
219
+ direct_score: 2.0,
220
+ direct_level: "NOTABLE",
221
+ },
222
+ "tag:email:headers:auth": {
223
+ key: "tag:email:headers:auth",
224
+ name: "email:headers:auth",
225
+ description: "Auth headers",
226
+ checks: [],
227
+ direct_score: 3.5,
228
+ direct_level: "SUSPICIOUS",
229
+ },
230
+ "tag:email:body": {
231
+ key: "tag:email:body",
232
+ name: "email:body",
233
+ description: "Email body",
234
+ checks: [],
235
+ direct_score: 1.0,
236
+ direct_level: "NOTABLE",
228
237
  },
229
238
  },
230
239
  stats: {
@@ -237,12 +246,11 @@ function createTestInvestigation(): CyvestInvestigation {
237
246
  observables_by_type_and_level: {},
238
247
  total_checks: 3,
239
248
  applied_checks: 2,
240
- checks_by_scope: { network: ["chk:ip_check:network"], dns: ["chk:domain_check:dns", "chk:dns_lookup:dns"] },
241
- checks_by_level: { INFO: ["chk:ip_check:network", "chk:dns_lookup:dns"], MALICIOUS: ["chk:domain_check:dns"] },
249
+ checks_by_level: { INFO: ["chk:ip_check", "chk:dns_lookup"], MALICIOUS: ["chk:domain_check"] },
242
250
  total_threat_intel: 1,
243
251
  threat_intel_by_source: { virustotal: 1 },
244
252
  threat_intel_by_level: { MALICIOUS: 1 },
245
- total_containers: 2,
253
+ total_tags: 4,
246
254
  },
247
255
  data_extraction: {
248
256
  root_type: "file",
@@ -285,17 +293,17 @@ describe("Getters", () => {
285
293
 
286
294
  describe("getCheck", () => {
287
295
  it("returns check by key", () => {
288
- const check = getCheck(inv, "chk:domain_check:dns");
296
+ const check = getCheck(inv, "chk:domain_check");
289
297
  expect(check).toBeDefined();
290
- expect(check?.check_id).toBe("domain_check");
298
+ expect(check?.check_name).toBe("domain_check");
291
299
  });
292
300
  });
293
301
 
294
- describe("getCheckByIdScope", () => {
295
- it("finds check by id and scope", () => {
296
- const check = getCheckByIdScope(inv, "domain_check", "dns");
302
+ describe("getCheckByName", () => {
303
+ it("finds check by name", () => {
304
+ const check = getCheckByName(inv, "domain_check");
297
305
  expect(check).toBeDefined();
298
- expect(check?.key).toBe("chk:domain_check:dns");
306
+ expect(check?.key).toBe("chk:domain_check");
299
307
  });
300
308
  });
301
309
 
@@ -317,24 +325,24 @@ describe("Getters", () => {
317
325
  });
318
326
  });
319
327
 
320
- describe("getContainer", () => {
321
- it("returns top-level container", () => {
322
- const container = getContainer(inv, "ctr:email");
323
- expect(container).toBeDefined();
324
- expect(container?.path).toBe("email");
328
+ describe("getTag", () => {
329
+ it("returns tag by key", () => {
330
+ const tag = getTag(inv, "tag:email");
331
+ expect(tag).toBeDefined();
332
+ expect(tag?.name).toBe("email");
325
333
  });
326
334
 
327
- it("returns nested container", () => {
328
- const container = getContainer(inv, "ctr:email/headers");
329
- expect(container).toBeDefined();
330
- expect(container?.path).toBe("email/headers");
335
+ it("returns nested tag", () => {
336
+ const tag = getTag(inv, "tag:email:headers");
337
+ expect(tag).toBeDefined();
338
+ expect(tag?.name).toBe("email:headers");
331
339
  });
332
340
  });
333
341
 
334
- describe("getAllContainers", () => {
335
- it("returns all containers including nested", () => {
336
- const containers = getAllContainers(inv);
337
- expect(containers).toHaveLength(2);
342
+ describe("getAllTags", () => {
343
+ it("returns all tags", () => {
344
+ const tags = getAllTags(inv);
345
+ expect(tags).toHaveLength(4);
338
346
  });
339
347
  });
340
348
 
@@ -345,7 +353,7 @@ describe("Getters", () => {
345
353
  expect(counts.checks).toBe(3);
346
354
  expect(counts.threatIntels).toBe(1);
347
355
  expect(counts.enrichments).toBe(1);
348
- expect(counts.containers).toBe(2);
356
+ expect(counts.tags).toBe(4);
349
357
  expect(counts.whitelists).toBe(1);
350
358
  });
351
359
  });
@@ -419,13 +427,6 @@ describe("Finders", () => {
419
427
  });
420
428
  });
421
429
 
422
- describe("findChecksByScope", () => {
423
- it("finds checks in scope", () => {
424
- const dnsChecks = findChecksByScope(inv, "dns");
425
- expect(dnsChecks).toHaveLength(2);
426
- });
427
- });
428
-
429
430
  describe("findChecksByLevel", () => {
430
431
  it("finds checks at level", () => {
431
432
  const malicious = findChecksByLevel(inv, "MALICIOUS");
@@ -440,17 +441,17 @@ describe("Finders", () => {
440
441
  });
441
442
  });
442
443
 
443
- describe("getChecksForObservable", () => {
444
+ describe("findChecksForObservable", () => {
444
445
  it("finds checks that reference observable", () => {
445
- const checks = getChecksForObservable(inv, "obs:ipv4-addr:192.168.1.1");
446
+ const checks = findChecksForObservable(inv, "obs:ipv4-addr:192.168.1.1");
446
447
  expect(checks).toHaveLength(1);
447
- expect(checks[0].check_id).toBe("ip_check");
448
+ expect(checks[0].check_name).toBe("ip_check");
448
449
  });
449
450
  });
450
451
 
451
- describe("getThreatIntelsForObservable", () => {
452
+ describe("findThreatIntelsForObservable", () => {
452
453
  it("finds threat intel for observable", () => {
453
- const tis = getThreatIntelsForObservable(
454
+ const tis = findThreatIntelsForObservable(
454
455
  inv,
455
456
  "obs:domain-name:example.com"
456
457
  );
@@ -459,34 +460,34 @@ describe("Finders", () => {
459
460
  });
460
461
  });
461
462
 
462
- describe("getObservablesForCheck", () => {
463
+ describe("findObservablesForCheck", () => {
463
464
  it("finds observables referenced by check", () => {
464
- const obs = getObservablesForCheck(inv, "chk:ip_check:network");
465
+ const obs = findObservablesForCheck(inv, "chk:ip_check");
465
466
  expect(obs).toHaveLength(1);
466
467
  expect(obs[0].value).toBe("192.168.1.1");
467
468
  });
468
469
  });
469
470
 
470
- describe("getHighestScoringObservables", () => {
471
+ describe("findHighestScoringObservables", () => {
471
472
  it("returns top scoring observables", () => {
472
- const top = getHighestScoringObservables(inv, 2);
473
+ const top = findHighestScoringObservables(inv, 2);
473
474
  expect(top).toHaveLength(2);
474
475
  expect(top[0].score).toBeGreaterThanOrEqual(top[1].score);
475
476
  });
476
477
  });
477
478
 
478
- describe("getMaliciousObservables", () => {
479
+ describe("findMaliciousObservables", () => {
479
480
  it("returns malicious observables", () => {
480
- const mal = getMaliciousObservables(inv);
481
+ const mal = findMaliciousObservables(inv);
481
482
  expect(mal).toHaveLength(2);
482
483
  });
483
484
  });
484
485
 
485
- describe("getAllScopes", () => {
486
- it("returns all scopes", () => {
487
- const scopes = getAllScopes(inv);
488
- expect(scopes).toContain("network");
489
- expect(scopes).toContain("dns");
486
+ describe("getAllCheckKeys", () => {
487
+ it("returns all check keys", () => {
488
+ const keys = getAllCheckKeys(inv);
489
+ expect(keys).toContain("chk:ip_check");
490
+ expect(keys).toContain("chk:domain_check");
490
491
  });
491
492
  });
492
493
 
@@ -498,4 +499,102 @@ describe("Finders", () => {
498
499
  expect(types).toContain("url");
499
500
  });
500
501
  });
502
+
503
+ // Tag Aggregation Tests
504
+ describe("getTagChildren", () => {
505
+ it("returns direct children of a tag", () => {
506
+ const children = getTagChildren(inv, "email");
507
+ expect(children).toHaveLength(2);
508
+ const names = children.map((t) => t.name);
509
+ expect(names).toContain("email:headers");
510
+ expect(names).toContain("email:body");
511
+ });
512
+
513
+ it("does not return grandchildren", () => {
514
+ const children = getTagChildren(inv, "email");
515
+ const names = children.map((t) => t.name);
516
+ expect(names).not.toContain("email:headers:auth");
517
+ });
518
+
519
+ it("returns empty array for leaf tag", () => {
520
+ const children = getTagChildren(inv, "email:headers:auth");
521
+ expect(children).toHaveLength(0);
522
+ });
523
+
524
+ it("returns empty array for non-existent tag", () => {
525
+ const children = getTagChildren(inv, "nonexistent");
526
+ expect(children).toHaveLength(0);
527
+ });
528
+ });
529
+
530
+ describe("getTagDescendants", () => {
531
+ it("returns all descendants of a tag", () => {
532
+ const descendants = getTagDescendants(inv, "email");
533
+ expect(descendants).toHaveLength(3);
534
+ const names = descendants.map((t) => t.name);
535
+ expect(names).toContain("email:headers");
536
+ expect(names).toContain("email:headers:auth");
537
+ expect(names).toContain("email:body");
538
+ });
539
+
540
+ it("returns children and grandchildren", () => {
541
+ const descendants = getTagDescendants(inv, "email:headers");
542
+ expect(descendants).toHaveLength(1);
543
+ expect(descendants[0].name).toBe("email:headers:auth");
544
+ });
545
+
546
+ it("returns empty array for leaf tag", () => {
547
+ const descendants = getTagDescendants(inv, "email:headers:auth");
548
+ expect(descendants).toHaveLength(0);
549
+ });
550
+ });
551
+
552
+ describe("getTagAggregatedScore", () => {
553
+ it("returns aggregated score including all descendants", () => {
554
+ // email (1.5) + email:headers (2.0) + email:headers:auth (3.5) + email:body (1.0) = 8.0
555
+ const score = getTagAggregatedScore(inv, "email");
556
+ expect(score).toBe(8.0);
557
+ });
558
+
559
+ it("returns aggregated score for intermediate tag", () => {
560
+ // email:headers (2.0) + email:headers:auth (3.5) = 5.5
561
+ const score = getTagAggregatedScore(inv, "email:headers");
562
+ expect(score).toBe(5.5);
563
+ });
564
+
565
+ it("returns direct score for leaf tag", () => {
566
+ const score = getTagAggregatedScore(inv, "email:headers:auth");
567
+ expect(score).toBe(3.5);
568
+ });
569
+
570
+ it("returns 0 for non-existent tag", () => {
571
+ const score = getTagAggregatedScore(inv, "nonexistent");
572
+ expect(score).toBe(0);
573
+ });
574
+ });
575
+
576
+ describe("getTagAggregatedLevel", () => {
577
+ it("returns level based on aggregated score", () => {
578
+ // email aggregated score = 8.0 -> MALICIOUS (>= 5)
579
+ const level = getTagAggregatedLevel(inv, "email");
580
+ expect(level).toBe("MALICIOUS");
581
+ });
582
+
583
+ it("returns level for intermediate tag", () => {
584
+ // email:headers aggregated score = 5.5 -> MALICIOUS (>= 5)
585
+ const level = getTagAggregatedLevel(inv, "email:headers");
586
+ expect(level).toBe("MALICIOUS");
587
+ });
588
+
589
+ it("returns level for leaf tag", () => {
590
+ // email:headers:auth direct score = 3.5 -> SUSPICIOUS (3 <= x < 5)
591
+ const level = getTagAggregatedLevel(inv, "email:headers:auth");
592
+ expect(level).toBe("SUSPICIOUS");
593
+ });
594
+
595
+ it("returns INFO for non-existent tag (score 0)", () => {
596
+ const level = getTagAggregatedLevel(inv, "nonexistent");
597
+ expect(level).toBe("INFO");
598
+ });
599
+ });
501
600
  });
@@ -6,7 +6,7 @@ import {
6
6
  getObservableParents,
7
7
  getRelatedObservablesByType,
8
8
  getObservableGraph,
9
- findRootObservables,
9
+ findSourceObservables,
10
10
  findOrphanObservables,
11
11
  findLeafObservables,
12
12
  areConnected,
@@ -144,7 +144,6 @@ function createGraphTestInvestigation(): CyvestInvestigation {
144
144
  observables_by_type_and_level: {},
145
145
  total_checks: 0,
146
146
  applied_checks: 0,
147
- checks_by_scope: {},
148
147
  checks_by_level: {},
149
148
  total_threat_intel: 0,
150
149
  threat_intel_by_source: {},
@@ -259,12 +258,12 @@ describe("Graph Traversal", () => {
259
258
  });
260
259
  });
261
260
 
262
- describe("findRootObservables", () => {
261
+ describe("findSourceObservables", () => {
263
262
  it("finds observables with no incoming relationships", () => {
264
- const roots = findRootObservables(inv);
265
- // email-message and file-hash are roots
266
- expect(roots.length).toBeGreaterThanOrEqual(2);
267
- const values = roots.map((o) => o.value);
263
+ const sources = findSourceObservables(inv);
264
+ // email-message and file-hash are sources
265
+ expect(sources.length).toBeGreaterThanOrEqual(2);
266
+ const values = sources.map((o) => o.value);
268
267
  expect(values).toContain("msg1");
269
268
  expect(values).toContain("abc123");
270
269
  });