@cyvest/cyvest-js 2.0.1

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/src/finders.ts ADDED
@@ -0,0 +1,712 @@
1
+ /**
2
+ * Finder utilities for querying and filtering Cyvest Investigation data.
3
+ *
4
+ * These functions provide filtering, searching, and cross-referencing
5
+ * capabilities for observables, checks, and threat intel.
6
+ */
7
+
8
+ import type {
9
+ CyvestInvestigation,
10
+ Observable,
11
+ Check,
12
+ ThreatIntel,
13
+ Container,
14
+ Level,
15
+ } from "./types.generated";
16
+ import { isLevelAtLeast, isLevelHigherThan, LEVEL_VALUES } from "./levels";
17
+
18
+ // ============================================================================
19
+ // Observable Finders
20
+ // ============================================================================
21
+
22
+ /**
23
+ * Find all observables of a specific type.
24
+ *
25
+ * @param inv - The investigation to search
26
+ * @param type - Observable type (e.g., "ipv4-addr", "url", "domain-name")
27
+ * @returns Array of matching observables
28
+ *
29
+ * @example
30
+ * ```ts
31
+ * const ips = findObservablesByType(investigation, "ipv4-addr");
32
+ * const urls = findObservablesByType(investigation, "url");
33
+ * ```
34
+ */
35
+ export function findObservablesByType(
36
+ inv: CyvestInvestigation,
37
+ type: string
38
+ ): Observable[] {
39
+ const normalizedType = type.trim().toLowerCase();
40
+ return Object.values(inv.observables).filter(
41
+ (obs) => obs.type.toLowerCase() === normalizedType
42
+ );
43
+ }
44
+
45
+ /**
46
+ * Find all observables at a specific level.
47
+ *
48
+ * @param inv - The investigation to search
49
+ * @param level - Security level to filter by
50
+ * @returns Array of matching observables
51
+ *
52
+ * @example
53
+ * ```ts
54
+ * const malicious = findObservablesByLevel(investigation, "MALICIOUS");
55
+ * ```
56
+ */
57
+ export function findObservablesByLevel(
58
+ inv: CyvestInvestigation,
59
+ level: Level
60
+ ): Observable[] {
61
+ return Object.values(inv.observables).filter((obs) => obs.level === level);
62
+ }
63
+
64
+ /**
65
+ * Find all observables at or above a minimum level.
66
+ *
67
+ * @param inv - The investigation to search
68
+ * @param minLevel - Minimum security level
69
+ * @returns Array of matching observables
70
+ *
71
+ * @example
72
+ * ```ts
73
+ * const suspicious = findObservablesAtLeast(investigation, "SUSPICIOUS");
74
+ * // Returns SUSPICIOUS and MALICIOUS observables
75
+ * ```
76
+ */
77
+ export function findObservablesAtLeast(
78
+ inv: CyvestInvestigation,
79
+ minLevel: Level
80
+ ): Observable[] {
81
+ return Object.values(inv.observables).filter((obs) =>
82
+ isLevelAtLeast(obs.level, minLevel)
83
+ );
84
+ }
85
+
86
+ /**
87
+ * Find observables by exact value match.
88
+ *
89
+ * @param inv - The investigation to search
90
+ * @param value - Value to search for
91
+ * @param caseSensitive - Whether to perform case-sensitive match (default: false)
92
+ * @returns Array of matching observables
93
+ */
94
+ export function findObservablesByValue(
95
+ inv: CyvestInvestigation,
96
+ value: string,
97
+ caseSensitive = false
98
+ ): Observable[] {
99
+ const searchValue = caseSensitive ? value : value.toLowerCase();
100
+ return Object.values(inv.observables).filter((obs) => {
101
+ const obsValue = caseSensitive ? obs.value : obs.value.toLowerCase();
102
+ return obsValue === searchValue;
103
+ });
104
+ }
105
+
106
+ /**
107
+ * Find observables containing a substring in their value.
108
+ *
109
+ * @param inv - The investigation to search
110
+ * @param substring - Substring to search for
111
+ * @param caseSensitive - Whether to perform case-sensitive match (default: false)
112
+ * @returns Array of matching observables
113
+ */
114
+ export function findObservablesContaining(
115
+ inv: CyvestInvestigation,
116
+ substring: string,
117
+ caseSensitive = false
118
+ ): Observable[] {
119
+ const searchStr = caseSensitive ? substring : substring.toLowerCase();
120
+ return Object.values(inv.observables).filter((obs) => {
121
+ const obsValue = caseSensitive ? obs.value : obs.value.toLowerCase();
122
+ return obsValue.includes(searchStr);
123
+ });
124
+ }
125
+
126
+ /**
127
+ * Find observables matching a regular expression.
128
+ *
129
+ * @param inv - The investigation to search
130
+ * @param pattern - Regular expression pattern
131
+ * @returns Array of matching observables
132
+ */
133
+ export function findObservablesMatching(
134
+ inv: CyvestInvestigation,
135
+ pattern: RegExp
136
+ ): Observable[] {
137
+ return Object.values(inv.observables).filter((obs) => pattern.test(obs.value));
138
+ }
139
+
140
+ /**
141
+ * Find internal observables.
142
+ *
143
+ * @param inv - The investigation to search
144
+ * @returns Array of internal observables
145
+ */
146
+ export function findInternalObservables(inv: CyvestInvestigation): Observable[] {
147
+ return Object.values(inv.observables).filter((obs) => obs.internal);
148
+ }
149
+
150
+ /**
151
+ * Find external (non-internal) observables.
152
+ *
153
+ * @param inv - The investigation to search
154
+ * @returns Array of external observables
155
+ */
156
+ export function findExternalObservables(inv: CyvestInvestigation): Observable[] {
157
+ return Object.values(inv.observables).filter((obs) => !obs.internal);
158
+ }
159
+
160
+ /**
161
+ * Find whitelisted observables.
162
+ *
163
+ * @param inv - The investigation to search
164
+ * @returns Array of whitelisted observables
165
+ */
166
+ export function findWhitelistedObservables(
167
+ inv: CyvestInvestigation
168
+ ): Observable[] {
169
+ return Object.values(inv.observables).filter((obs) => obs.whitelisted);
170
+ }
171
+
172
+ /**
173
+ * Find observables with threat intelligence data.
174
+ *
175
+ * @param inv - The investigation to search
176
+ * @returns Array of observables that have associated threat intel
177
+ */
178
+ export function findObservablesWithThreatIntel(
179
+ inv: CyvestInvestigation
180
+ ): Observable[] {
181
+ return Object.values(inv.observables).filter(
182
+ (obs) => obs.threat_intels.length > 0
183
+ );
184
+ }
185
+
186
+ // ============================================================================
187
+ // Check Finders
188
+ // ============================================================================
189
+
190
+ /**
191
+ * Find all checks in a specific scope.
192
+ *
193
+ * @param inv - The investigation to search
194
+ * @param scope - Check scope
195
+ * @returns Array of checks in the scope
196
+ *
197
+ * @example
198
+ * ```ts
199
+ * const emailChecks = findChecksByScope(investigation, "email_headers");
200
+ * ```
201
+ */
202
+ export function findChecksByScope(
203
+ inv: CyvestInvestigation,
204
+ scope: string
205
+ ): Check[] {
206
+ const normalizedScope = scope.trim().toLowerCase();
207
+
208
+ // Try direct lookup first
209
+ if (inv.checks[scope]) {
210
+ return inv.checks[scope];
211
+ }
212
+
213
+ // Fallback to normalized search
214
+ for (const [key, checks] of Object.entries(inv.checks)) {
215
+ if (key.toLowerCase() === normalizedScope) {
216
+ return checks;
217
+ }
218
+ }
219
+
220
+ return [];
221
+ }
222
+
223
+ /**
224
+ * Find all checks at a specific level.
225
+ *
226
+ * @param inv - The investigation to search
227
+ * @param level - Security level to filter by
228
+ * @returns Array of matching checks
229
+ */
230
+ export function findChecksByLevel(
231
+ inv: CyvestInvestigation,
232
+ level: Level
233
+ ): Check[] {
234
+ const result: Check[] = [];
235
+ for (const checks of Object.values(inv.checks)) {
236
+ for (const check of checks) {
237
+ if (check.level === level) {
238
+ result.push(check);
239
+ }
240
+ }
241
+ }
242
+ return result;
243
+ }
244
+
245
+ /**
246
+ * Find all checks at or above a minimum level.
247
+ *
248
+ * @param inv - The investigation to search
249
+ * @param minLevel - Minimum security level
250
+ * @returns Array of matching checks
251
+ */
252
+ export function findChecksAtLeast(
253
+ inv: CyvestInvestigation,
254
+ minLevel: Level
255
+ ): Check[] {
256
+ const result: Check[] = [];
257
+ for (const checks of Object.values(inv.checks)) {
258
+ for (const check of checks) {
259
+ if (isLevelAtLeast(check.level, minLevel)) {
260
+ result.push(check);
261
+ }
262
+ }
263
+ }
264
+ return result;
265
+ }
266
+
267
+ /**
268
+ * Find checks by check ID (across all scopes).
269
+ *
270
+ * @param inv - The investigation to search
271
+ * @param checkId - Check identifier to search for
272
+ * @returns Array of matching checks
273
+ */
274
+ export function findChecksByCheckId(
275
+ inv: CyvestInvestigation,
276
+ checkId: string
277
+ ): Check[] {
278
+ const normalizedId = checkId.trim().toLowerCase();
279
+ const result: Check[] = [];
280
+
281
+ for (const checks of Object.values(inv.checks)) {
282
+ for (const check of checks) {
283
+ if (check.check_id.toLowerCase() === normalizedId) {
284
+ result.push(check);
285
+ }
286
+ }
287
+ }
288
+ return result;
289
+ }
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
+ // ============================================================================
310
+ // Threat Intel Finders
311
+ // ============================================================================
312
+
313
+ /**
314
+ * Find all threat intel from a specific source.
315
+ *
316
+ * @param inv - The investigation to search
317
+ * @param source - Source name (e.g., "virustotal", "otx")
318
+ * @returns Array of threat intel from the source
319
+ */
320
+ export function findThreatIntelBySource(
321
+ inv: CyvestInvestigation,
322
+ source: string
323
+ ): ThreatIntel[] {
324
+ const normalizedSource = source.trim().toLowerCase();
325
+ return Object.values(inv.threat_intels).filter(
326
+ (ti) => ti.source.toLowerCase() === normalizedSource
327
+ );
328
+ }
329
+
330
+ /**
331
+ * Find all threat intel at a specific level.
332
+ *
333
+ * @param inv - The investigation to search
334
+ * @param level - Security level to filter by
335
+ * @returns Array of matching threat intel
336
+ */
337
+ export function findThreatIntelByLevel(
338
+ inv: CyvestInvestigation,
339
+ level: Level
340
+ ): ThreatIntel[] {
341
+ return Object.values(inv.threat_intels).filter((ti) => ti.level === level);
342
+ }
343
+
344
+ /**
345
+ * Find all threat intel at or above a minimum level.
346
+ *
347
+ * @param inv - The investigation to search
348
+ * @param minLevel - Minimum security level
349
+ * @returns Array of matching threat intel
350
+ */
351
+ export function findThreatIntelAtLeast(
352
+ inv: CyvestInvestigation,
353
+ minLevel: Level
354
+ ): ThreatIntel[] {
355
+ return Object.values(inv.threat_intels).filter((ti) =>
356
+ isLevelAtLeast(ti.level, minLevel)
357
+ );
358
+ }
359
+
360
+ // ============================================================================
361
+ // Container Finders
362
+ // ============================================================================
363
+
364
+ /**
365
+ * Find containers at a specific aggregated level.
366
+ *
367
+ * @param inv - The investigation to search
368
+ * @param level - Aggregated level to filter by
369
+ * @returns Array of matching containers
370
+ */
371
+ export function findContainersByLevel(
372
+ inv: CyvestInvestigation,
373
+ level: Level
374
+ ): Container[] {
375
+ const result: Container[] = [];
376
+
377
+ function searchContainers(containers: Record<string, Container>): void {
378
+ for (const container of Object.values(containers)) {
379
+ if (container.aggregated_level === level) {
380
+ result.push(container);
381
+ }
382
+ searchContainers(container.sub_containers);
383
+ }
384
+ }
385
+
386
+ searchContainers(inv.containers);
387
+ return result;
388
+ }
389
+
390
+ /**
391
+ * Find containers at or above a minimum aggregated level.
392
+ *
393
+ * @param inv - The investigation to search
394
+ * @param minLevel - Minimum aggregated level
395
+ * @returns Array of matching containers
396
+ */
397
+ export function findContainersAtLeast(
398
+ inv: CyvestInvestigation,
399
+ minLevel: Level
400
+ ): Container[] {
401
+ const result: Container[] = [];
402
+
403
+ function searchContainers(containers: Record<string, Container>): void {
404
+ for (const container of Object.values(containers)) {
405
+ if (isLevelAtLeast(container.aggregated_level, minLevel)) {
406
+ result.push(container);
407
+ }
408
+ searchContainers(container.sub_containers);
409
+ }
410
+ }
411
+
412
+ searchContainers(inv.containers);
413
+ return result;
414
+ }
415
+
416
+ // ============================================================================
417
+ // Cross-Reference Finders
418
+ // ============================================================================
419
+
420
+ /**
421
+ * Get all checks that generated or reference a specific observable.
422
+ *
423
+ * @param inv - The investigation to search
424
+ * @param observableKey - Key of the observable
425
+ * @returns Array of checks that reference this observable
426
+ *
427
+ * @example
428
+ * ```ts
429
+ * const checks = getChecksForObservable(investigation, "obs:ipv4-addr:192.168.1.1");
430
+ * ```
431
+ */
432
+ export function getChecksForObservable(
433
+ inv: CyvestInvestigation,
434
+ observableKey: string
435
+ ): Check[] {
436
+ const result: Check[] = [];
437
+
438
+ for (const checks of Object.values(inv.checks)) {
439
+ for (const check of checks) {
440
+ if (check.observables.includes(observableKey)) {
441
+ result.push(check);
442
+ }
443
+ }
444
+ }
445
+
446
+ return result;
447
+ }
448
+
449
+ /**
450
+ * Get all threat intel entries for a specific observable.
451
+ *
452
+ * @param inv - The investigation to search
453
+ * @param observableKey - Key of the observable
454
+ * @returns Array of threat intel for this observable
455
+ */
456
+ export function getThreatIntelsForObservable(
457
+ inv: CyvestInvestigation,
458
+ observableKey: string
459
+ ): ThreatIntel[] {
460
+ // First try using the observable's threat_intels array
461
+ const observable = inv.observables[observableKey];
462
+ if (observable) {
463
+ return observable.threat_intels
464
+ .map((tiKey) => inv.threat_intels[tiKey])
465
+ .filter((ti): ti is ThreatIntel => ti !== undefined);
466
+ }
467
+
468
+ // Fallback: search all threat intel
469
+ return Object.values(inv.threat_intels).filter(
470
+ (ti) => ti.observable_key === observableKey
471
+ );
472
+ }
473
+
474
+ /**
475
+ * Get all observables referenced by a specific check.
476
+ *
477
+ * @param inv - The investigation to search
478
+ * @param checkKey - Key of the check
479
+ * @returns Array of observables referenced by this check
480
+ */
481
+ export function getObservablesForCheck(
482
+ inv: CyvestInvestigation,
483
+ checkKey: string
484
+ ): Observable[] {
485
+ // Find the check
486
+ for (const checks of Object.values(inv.checks)) {
487
+ for (const check of checks) {
488
+ if (check.key === checkKey) {
489
+ return check.observables
490
+ .map((obsKey) => inv.observables[obsKey])
491
+ .filter((obs): obs is Observable => obs !== undefined);
492
+ }
493
+ }
494
+ }
495
+ return [];
496
+ }
497
+
498
+ /**
499
+ * Get all checks for a specific container.
500
+ *
501
+ * @param inv - The investigation to search
502
+ * @param containerKey - Key of the container
503
+ * @param recursive - Include checks from sub-containers (default: false)
504
+ * @returns Array of checks in the container
505
+ */
506
+ export function getChecksForContainer(
507
+ inv: CyvestInvestigation,
508
+ containerKey: string,
509
+ recursive = false
510
+ ): Check[] {
511
+ const result: Check[] = [];
512
+
513
+ function findContainer(
514
+ containers: Record<string, Container>
515
+ ): Container | undefined {
516
+ for (const container of Object.values(containers)) {
517
+ if (container.key === containerKey) {
518
+ return container;
519
+ }
520
+ const found = findContainer(container.sub_containers);
521
+ if (found) return found;
522
+ }
523
+ return undefined;
524
+ }
525
+
526
+ function collectChecks(container: Container): void {
527
+ for (const checkKey of container.checks) {
528
+ for (const checks of Object.values(inv.checks)) {
529
+ for (const check of checks) {
530
+ if (check.key === checkKey) {
531
+ result.push(check);
532
+ }
533
+ }
534
+ }
535
+ }
536
+
537
+ if (recursive) {
538
+ for (const subContainer of Object.values(container.sub_containers)) {
539
+ collectChecks(subContainer);
540
+ }
541
+ }
542
+ }
543
+
544
+ const container = findContainer(inv.containers);
545
+ if (container) {
546
+ collectChecks(container);
547
+ }
548
+
549
+ return result;
550
+ }
551
+
552
+ // ============================================================================
553
+ // Sorting Utilities
554
+ // ============================================================================
555
+
556
+ /**
557
+ * Sort observables by score (descending - highest first).
558
+ *
559
+ * @param observables - Array of observables to sort
560
+ * @returns Sorted array (new array, doesn't mutate input)
561
+ */
562
+ export function sortObservablesByScore(observables: Observable[]): Observable[] {
563
+ return [...observables].sort((a, b) => b.score - a.score);
564
+ }
565
+
566
+ /**
567
+ * Sort checks by score (descending - highest first).
568
+ *
569
+ * @param checks - Array of checks to sort
570
+ * @returns Sorted array (new array, doesn't mutate input)
571
+ */
572
+ export function sortChecksByScore(checks: Check[]): Check[] {
573
+ return [...checks].sort((a, b) => b.score - a.score);
574
+ }
575
+
576
+ /**
577
+ * Sort observables by level (descending - most severe first).
578
+ *
579
+ * @param observables - Array of observables to sort
580
+ * @returns Sorted array (new array, doesn't mutate input)
581
+ */
582
+ export function sortObservablesByLevel(observables: Observable[]): Observable[] {
583
+ return [...observables].sort(
584
+ (a, b) => LEVEL_VALUES[b.level] - LEVEL_VALUES[a.level]
585
+ );
586
+ }
587
+
588
+ /**
589
+ * Sort checks by level (descending - most severe first).
590
+ *
591
+ * @param checks - Array of checks to sort
592
+ * @returns Sorted array (new array, doesn't mutate input)
593
+ */
594
+ export function sortChecksByLevel(checks: Check[]): Check[] {
595
+ return [...checks].sort(
596
+ (a, b) => LEVEL_VALUES[b.level] - LEVEL_VALUES[a.level]
597
+ );
598
+ }
599
+
600
+ // ============================================================================
601
+ // Aggregation Utilities
602
+ // ============================================================================
603
+
604
+ /**
605
+ * Get the highest scoring observables.
606
+ *
607
+ * @param inv - The investigation to search
608
+ * @param n - Number of results to return (default: 10)
609
+ * @returns Array of highest scoring observables
610
+ */
611
+ export function getHighestScoringObservables(
612
+ inv: CyvestInvestigation,
613
+ n = 10
614
+ ): Observable[] {
615
+ return sortObservablesByScore(Object.values(inv.observables)).slice(0, n);
616
+ }
617
+
618
+ /**
619
+ * Get the highest scoring checks.
620
+ *
621
+ * @param inv - The investigation to search
622
+ * @param n - Number of results to return (default: 10)
623
+ * @returns Array of highest scoring checks
624
+ */
625
+ export function getHighestScoringChecks(
626
+ inv: CyvestInvestigation,
627
+ n = 10
628
+ ): Check[] {
629
+ const allChecks: Check[] = [];
630
+ for (const checks of Object.values(inv.checks)) {
631
+ allChecks.push(...checks);
632
+ }
633
+ return sortChecksByScore(allChecks).slice(0, n);
634
+ }
635
+
636
+ /**
637
+ * Get all malicious observables (convenience function).
638
+ *
639
+ * @param inv - The investigation to search
640
+ * @returns Array of malicious observables
641
+ */
642
+ export function getMaliciousObservables(inv: CyvestInvestigation): Observable[] {
643
+ return findObservablesByLevel(inv, "MALICIOUS");
644
+ }
645
+
646
+ /**
647
+ * Get all suspicious observables (convenience function).
648
+ *
649
+ * @param inv - The investigation to search
650
+ * @returns Array of suspicious observables
651
+ */
652
+ export function getSuspiciousObservables(inv: CyvestInvestigation): Observable[] {
653
+ return findObservablesByLevel(inv, "SUSPICIOUS");
654
+ }
655
+
656
+ /**
657
+ * Get all malicious checks (convenience function).
658
+ *
659
+ * @param inv - The investigation to search
660
+ * @returns Array of malicious checks
661
+ */
662
+ export function getMaliciousChecks(inv: CyvestInvestigation): Check[] {
663
+ return findChecksByLevel(inv, "MALICIOUS");
664
+ }
665
+
666
+ /**
667
+ * Get all suspicious checks (convenience function).
668
+ *
669
+ * @param inv - The investigation to search
670
+ * @returns Array of suspicious checks
671
+ */
672
+ export function getSuspiciousChecks(inv: CyvestInvestigation): Check[] {
673
+ return findChecksByLevel(inv, "SUSPICIOUS");
674
+ }
675
+
676
+ /**
677
+ * Get all scopes that have checks.
678
+ *
679
+ * @param inv - The investigation
680
+ * @returns Array of scope names
681
+ */
682
+ export function getAllScopes(inv: CyvestInvestigation): string[] {
683
+ return Object.keys(inv.checks);
684
+ }
685
+
686
+ /**
687
+ * Get all observable types present in the investigation.
688
+ *
689
+ * @param inv - The investigation
690
+ * @returns Array of unique observable types
691
+ */
692
+ export function getAllObservableTypes(inv: CyvestInvestigation): string[] {
693
+ const types = new Set<string>();
694
+ for (const obs of Object.values(inv.observables)) {
695
+ types.add(obs.type);
696
+ }
697
+ return Array.from(types);
698
+ }
699
+
700
+ /**
701
+ * Get all threat intel sources present in the investigation.
702
+ *
703
+ * @param inv - The investigation
704
+ * @returns Array of unique source names
705
+ */
706
+ export function getAllThreatIntelSources(inv: CyvestInvestigation): string[] {
707
+ const sources = new Set<string>();
708
+ for (const ti of Object.values(inv.threat_intels)) {
709
+ sources.add(ti.source);
710
+ }
711
+ return Array.from(sources);
712
+ }