@cyvest/cyvest-js 4.4.1 → 5.0.2
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/README.md +5 -4
- package/dist/index.cjs +248 -333
- package/dist/index.d.cts +255 -121
- package/dist/index.d.ts +255 -121
- package/dist/index.js +219 -312
- package/package.json +1 -1
- package/src/finders.ts +101 -186
- package/src/getters.ts +176 -104
- package/src/graph.ts +4 -4
- package/src/keys.ts +84 -30
- package/src/levels.ts +7 -7
- package/src/types.generated.ts +25 -24
- package/tests/getters-finders.test.ts +225 -126
- package/tests/graph.test.ts +6 -7
- package/tests/keys-levels.test.ts +14 -15
package/src/getters.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Getter utilities for retrieving entities from a Cyvest Investigation.
|
|
3
3
|
*
|
|
4
4
|
* These functions provide type-safe access to observables, checks, threat intel,
|
|
5
|
-
* enrichments, and
|
|
5
|
+
* enrichments, and tags by their keys.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type {
|
|
@@ -11,8 +11,11 @@ import type {
|
|
|
11
11
|
Check,
|
|
12
12
|
ThreatIntel,
|
|
13
13
|
Enrichment,
|
|
14
|
-
|
|
14
|
+
Tag,
|
|
15
|
+
Level,
|
|
15
16
|
} from "./types.generated";
|
|
17
|
+
import { generateObservableKey, isTagChildOf } from "./keys";
|
|
18
|
+
import { getLevelFromScore } from "./levels";
|
|
16
19
|
|
|
17
20
|
/**
|
|
18
21
|
* Get an observable by its key.
|
|
@@ -68,6 +71,32 @@ export function getObservableByTypeValue(
|
|
|
68
71
|
return undefined;
|
|
69
72
|
}
|
|
70
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Get the root observable of the investigation.
|
|
76
|
+
*
|
|
77
|
+
* The root observable is identified using the `root_type` from data extraction
|
|
78
|
+
* metadata combined with value="root".
|
|
79
|
+
*
|
|
80
|
+
* @param inv - The investigation
|
|
81
|
+
* @returns The root observable, or undefined if not found
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```ts
|
|
85
|
+
* const root = getRootObservable(investigation);
|
|
86
|
+
* if (root) {
|
|
87
|
+
* console.log(`Root: ${root.type} = ${root.value}`);
|
|
88
|
+
* }
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
export function getRootObservable(inv: CyvestInvestigation): Observable | undefined {
|
|
92
|
+
const rootType = inv.data_extraction.root_type;
|
|
93
|
+
if (!rootType) {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
const rootKey = generateObservableKey(rootType, "root");
|
|
97
|
+
return inv.observables[rootKey];
|
|
98
|
+
}
|
|
99
|
+
|
|
71
100
|
/**
|
|
72
101
|
* Get a check by its key.
|
|
73
102
|
*
|
|
@@ -84,60 +113,32 @@ export function getCheck(
|
|
|
84
113
|
inv: CyvestInvestigation,
|
|
85
114
|
key: string
|
|
86
115
|
): Check | undefined {
|
|
87
|
-
|
|
88
|
-
for (const check of checks) {
|
|
89
|
-
if (check.key === key) {
|
|
90
|
-
return check;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
return undefined;
|
|
116
|
+
return inv.checks[key];
|
|
95
117
|
}
|
|
96
118
|
|
|
97
119
|
/**
|
|
98
|
-
* Get a check by its
|
|
120
|
+
* Get a check by its name.
|
|
99
121
|
*
|
|
100
122
|
* @param inv - The investigation to search
|
|
101
|
-
* @param
|
|
102
|
-
* @param scope - Check scope
|
|
123
|
+
* @param checkName - Check name
|
|
103
124
|
* @returns The check or undefined if not found
|
|
104
125
|
*
|
|
105
126
|
* @example
|
|
106
127
|
* ```ts
|
|
107
|
-
* const check =
|
|
128
|
+
* const check = getCheckByName(investigation, "sender_verification");
|
|
108
129
|
* ```
|
|
109
130
|
*/
|
|
110
|
-
export function
|
|
131
|
+
export function getCheckByName(
|
|
111
132
|
inv: CyvestInvestigation,
|
|
112
|
-
|
|
113
|
-
scope: string
|
|
133
|
+
checkName: string
|
|
114
134
|
): Check | undefined {
|
|
115
|
-
const
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
const scopeChecks = inv.checks[normalizedScope] || inv.checks[scope];
|
|
119
|
-
if (scopeChecks) {
|
|
120
|
-
return scopeChecks.find(
|
|
121
|
-
(c) => c.check_id.toLowerCase() === normalizedId
|
|
122
|
-
);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Fallback: search all scopes
|
|
126
|
-
for (const checks of Object.values(inv.checks)) {
|
|
127
|
-
for (const check of checks) {
|
|
128
|
-
if (
|
|
129
|
-
check.check_id.toLowerCase() === normalizedId &&
|
|
130
|
-
check.scope.toLowerCase() === normalizedScope
|
|
131
|
-
) {
|
|
132
|
-
return check;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
return undefined;
|
|
135
|
+
const normalizedName = checkName.trim().toLowerCase();
|
|
136
|
+
const key = `chk:${normalizedName}`;
|
|
137
|
+
return inv.checks[key];
|
|
137
138
|
}
|
|
138
139
|
|
|
139
140
|
/**
|
|
140
|
-
* Get all checks as
|
|
141
|
+
* Get all checks as an array.
|
|
141
142
|
*
|
|
142
143
|
* @param inv - The investigation
|
|
143
144
|
* @returns Array of all checks
|
|
@@ -149,11 +150,7 @@ export function getCheckByIdScope(
|
|
|
149
150
|
* ```
|
|
150
151
|
*/
|
|
151
152
|
export function getAllChecks(inv: CyvestInvestigation): Check[] {
|
|
152
|
-
|
|
153
|
-
for (const checks of Object.values(inv.checks)) {
|
|
154
|
-
result.push(...checks);
|
|
155
|
-
}
|
|
156
|
-
return result;
|
|
153
|
+
return Object.values(inv.checks);
|
|
157
154
|
}
|
|
158
155
|
|
|
159
156
|
/**
|
|
@@ -252,81 +249,62 @@ export function getAllEnrichments(inv: CyvestInvestigation): Enrichment[] {
|
|
|
252
249
|
}
|
|
253
250
|
|
|
254
251
|
/**
|
|
255
|
-
* Get a
|
|
252
|
+
* Get a tag by its key.
|
|
256
253
|
*
|
|
257
254
|
* @param inv - The investigation to search
|
|
258
|
-
* @param key -
|
|
259
|
-
* @returns The
|
|
255
|
+
* @param key - Tag key (e.g., "tag:header:auth")
|
|
256
|
+
* @returns The tag or undefined if not found
|
|
257
|
+
*
|
|
258
|
+
* @example
|
|
259
|
+
* ```ts
|
|
260
|
+
* const tag = getTag(investigation, "tag:header:auth");
|
|
261
|
+
* if (tag) {
|
|
262
|
+
* console.log(tag.name, tag.direct_level);
|
|
263
|
+
* }
|
|
264
|
+
* ```
|
|
260
265
|
*/
|
|
261
|
-
export function
|
|
266
|
+
export function getTag(
|
|
262
267
|
inv: CyvestInvestigation,
|
|
263
268
|
key: string
|
|
264
|
-
):
|
|
265
|
-
|
|
266
|
-
if (inv.containers[key]) {
|
|
267
|
-
return inv.containers[key];
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// Search recursively in sub-containers
|
|
271
|
-
function searchSubContainers(containers: Record<string, Container>): Container | undefined {
|
|
272
|
-
for (const container of Object.values(containers)) {
|
|
273
|
-
if (container.key === key) {
|
|
274
|
-
return container;
|
|
275
|
-
}
|
|
276
|
-
const found = searchSubContainers(container.sub_containers);
|
|
277
|
-
if (found) return found;
|
|
278
|
-
}
|
|
279
|
-
return undefined;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
return searchSubContainers(inv.containers);
|
|
269
|
+
): Tag | undefined {
|
|
270
|
+
return inv.tags[key];
|
|
283
271
|
}
|
|
284
272
|
|
|
285
273
|
/**
|
|
286
|
-
* Get a
|
|
274
|
+
* Get a tag by its name.
|
|
287
275
|
*
|
|
288
276
|
* @param inv - The investigation to search
|
|
289
|
-
* @param
|
|
290
|
-
* @returns The
|
|
277
|
+
* @param name - Tag name (e.g., "header:auth:dkim")
|
|
278
|
+
* @returns The tag or undefined if not found
|
|
279
|
+
*
|
|
280
|
+
* @example
|
|
281
|
+
* ```ts
|
|
282
|
+
* const tag = getTagByName(investigation, "header:auth:dkim");
|
|
283
|
+
* ```
|
|
291
284
|
*/
|
|
292
|
-
export function
|
|
285
|
+
export function getTagByName(
|
|
293
286
|
inv: CyvestInvestigation,
|
|
294
|
-
|
|
295
|
-
):
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
for (const container of Object.values(containers)) {
|
|
300
|
-
if (container.path.toLowerCase() === normalizedPath) {
|
|
301
|
-
return container;
|
|
302
|
-
}
|
|
303
|
-
const found = searchContainers(container.sub_containers);
|
|
304
|
-
if (found) return found;
|
|
305
|
-
}
|
|
306
|
-
return undefined;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
return searchContainers(inv.containers);
|
|
287
|
+
name: string
|
|
288
|
+
): Tag | undefined {
|
|
289
|
+
const normalizedName = name.trim().toLowerCase();
|
|
290
|
+
const key = `tag:${normalizedName}`;
|
|
291
|
+
return inv.tags[key];
|
|
310
292
|
}
|
|
311
293
|
|
|
312
294
|
/**
|
|
313
|
-
* Get all
|
|
295
|
+
* Get all tags as an array.
|
|
314
296
|
*
|
|
315
297
|
* @param inv - The investigation
|
|
316
|
-
* @returns Array of all
|
|
298
|
+
* @returns Array of all tags
|
|
299
|
+
*
|
|
300
|
+
* @example
|
|
301
|
+
* ```ts
|
|
302
|
+
* const allTags = getAllTags(investigation);
|
|
303
|
+
* console.log(`Total tags: ${allTags.length}`);
|
|
304
|
+
* ```
|
|
317
305
|
*/
|
|
318
|
-
export function
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
function collectContainers(containers: Record<string, Container>): void {
|
|
322
|
-
for (const container of Object.values(containers)) {
|
|
323
|
-
result.push(container);
|
|
324
|
-
collectContainers(container.sub_containers);
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
collectContainers(inv.containers);
|
|
329
|
-
return result;
|
|
306
|
+
export function getAllTags(inv: CyvestInvestigation): Tag[] {
|
|
307
|
+
return Object.values(inv.tags);
|
|
330
308
|
}
|
|
331
309
|
|
|
332
310
|
/**
|
|
@@ -377,7 +355,7 @@ export interface InvestigationCounts {
|
|
|
377
355
|
checks: number;
|
|
378
356
|
threatIntels: number;
|
|
379
357
|
enrichments: number;
|
|
380
|
-
|
|
358
|
+
tags: number;
|
|
381
359
|
whitelists: number;
|
|
382
360
|
}
|
|
383
361
|
|
|
@@ -393,7 +371,7 @@ export function getCounts(inv: CyvestInvestigation): InvestigationCounts {
|
|
|
393
371
|
checks: getAllChecks(inv).length,
|
|
394
372
|
threatIntels: Object.keys(inv.threat_intels).length,
|
|
395
373
|
enrichments: Object.keys(inv.enrichments).length,
|
|
396
|
-
|
|
374
|
+
tags: getAllTags(inv).length,
|
|
397
375
|
whitelists: inv.whitelists.length,
|
|
398
376
|
};
|
|
399
377
|
}
|
|
@@ -420,3 +398,97 @@ export function getStartedAt(inv: CyvestInvestigation): string | undefined {
|
|
|
420
398
|
);
|
|
421
399
|
return event?.timestamp;
|
|
422
400
|
}
|
|
401
|
+
|
|
402
|
+
// ============================================================================
|
|
403
|
+
// Tag Aggregation
|
|
404
|
+
// ============================================================================
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Get direct child tags of a given tag.
|
|
408
|
+
*
|
|
409
|
+
* @param inv - The investigation
|
|
410
|
+
* @param tagName - Parent tag name
|
|
411
|
+
* @returns Array of direct child tags
|
|
412
|
+
*
|
|
413
|
+
* @example
|
|
414
|
+
* ```ts
|
|
415
|
+
* const children = getTagChildren(investigation, "bodies");
|
|
416
|
+
* // Returns tags like "bodies:urls", "bodies:domains" (but not "bodies:urls:something")
|
|
417
|
+
* ```
|
|
418
|
+
*/
|
|
419
|
+
export function getTagChildren(inv: CyvestInvestigation, tagName: string): Tag[] {
|
|
420
|
+
return Object.values(inv.tags).filter((tag) => isTagChildOf(tag.name, tagName));
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Get all descendant tags of a given tag (any depth).
|
|
425
|
+
*
|
|
426
|
+
* @param inv - The investigation
|
|
427
|
+
* @param tagName - Ancestor tag name
|
|
428
|
+
* @returns Array of all descendant tags
|
|
429
|
+
*
|
|
430
|
+
* @example
|
|
431
|
+
* ```ts
|
|
432
|
+
* const descendants = getTagDescendants(investigation, "bodies");
|
|
433
|
+
* // Returns all tags starting with "bodies:"
|
|
434
|
+
* ```
|
|
435
|
+
*/
|
|
436
|
+
export function getTagDescendants(inv: CyvestInvestigation, tagName: string): Tag[] {
|
|
437
|
+
const prefix = tagName + ":";
|
|
438
|
+
return Object.values(inv.tags).filter((tag) => tag.name.startsWith(prefix));
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Get the aggregated score for a tag including all descendant tags.
|
|
443
|
+
*
|
|
444
|
+
* The aggregated score includes:
|
|
445
|
+
* - The tag's direct_score (from its direct checks)
|
|
446
|
+
* - Recursively, the aggregated scores of all child tags
|
|
447
|
+
*
|
|
448
|
+
* @param inv - The investigation
|
|
449
|
+
* @param tagName - Name of the tag
|
|
450
|
+
* @returns Total aggregated score, or 0 if tag not found
|
|
451
|
+
*
|
|
452
|
+
* @example
|
|
453
|
+
* ```ts
|
|
454
|
+
* const score = getTagAggregatedScore(investigation, "bodies");
|
|
455
|
+
* // Includes scores from bodies, bodies:urls, bodies:domains, etc.
|
|
456
|
+
* ```
|
|
457
|
+
*/
|
|
458
|
+
export function getTagAggregatedScore(inv: CyvestInvestigation, tagName: string): number {
|
|
459
|
+
const tag = getTagByName(inv, tagName);
|
|
460
|
+
if (!tag) {
|
|
461
|
+
return 0;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Start with direct score
|
|
465
|
+
let total = tag.direct_score;
|
|
466
|
+
|
|
467
|
+
// Add scores from direct children (they will recursively add their children)
|
|
468
|
+
const children = getTagChildren(inv, tagName);
|
|
469
|
+
for (const child of children) {
|
|
470
|
+
total += getTagAggregatedScore(inv, child.name);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return total;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Get the aggregated level for a tag including all descendant tags.
|
|
478
|
+
*
|
|
479
|
+
* The level is calculated from the aggregated score using the standard
|
|
480
|
+
* score-to-level mapping.
|
|
481
|
+
*
|
|
482
|
+
* @param inv - The investigation
|
|
483
|
+
* @param tagName - Name of the tag
|
|
484
|
+
* @returns Level based on aggregated score
|
|
485
|
+
*
|
|
486
|
+
* @example
|
|
487
|
+
* ```ts
|
|
488
|
+
* const level = getTagAggregatedLevel(investigation, "bodies");
|
|
489
|
+
* // Returns "MALICIOUS" if aggregated score >= 5, etc.
|
|
490
|
+
* ```
|
|
491
|
+
*/
|
|
492
|
+
export function getTagAggregatedLevel(inv: CyvestInvestigation, tagName: string): Level {
|
|
493
|
+
return getLevelFromScore(getTagAggregatedScore(inv, tagName));
|
|
494
|
+
}
|
package/src/graph.ts
CHANGED
|
@@ -325,15 +325,15 @@ export function getObservableGraph(inv: CyvestInvestigation): InvestigationGraph
|
|
|
325
325
|
// ============================================================================
|
|
326
326
|
|
|
327
327
|
/**
|
|
328
|
-
* Find
|
|
328
|
+
* Find source observables in the investigation graph.
|
|
329
329
|
*
|
|
330
|
-
*
|
|
330
|
+
* Source observables are those that have no incoming relationships
|
|
331
331
|
* (nothing points to them as a target).
|
|
332
332
|
*
|
|
333
333
|
* @param inv - The investigation
|
|
334
|
-
* @returns Array of
|
|
334
|
+
* @returns Array of source observables
|
|
335
335
|
*/
|
|
336
|
-
export function
|
|
336
|
+
export function findSourceObservables(inv: CyvestInvestigation): Observable[] {
|
|
337
337
|
const targetKeys = new Set<string>();
|
|
338
338
|
|
|
339
339
|
// Collect all target keys from relationships
|
package/src/keys.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
/**
|
|
9
9
|
* Key type prefixes used in Cyvest.
|
|
10
10
|
*/
|
|
11
|
-
export type KeyType = "obs" | "chk" | "ti" | "enr" | "
|
|
11
|
+
export type KeyType = "obs" | "chk" | "ti" | "enr" | "tag";
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Normalize a string value for consistent key generation.
|
|
@@ -58,22 +58,20 @@ export function generateObservableKey(obsType: string, value: string): string {
|
|
|
58
58
|
/**
|
|
59
59
|
* Generate a unique key for a check.
|
|
60
60
|
*
|
|
61
|
-
* Format: chk:{
|
|
61
|
+
* Format: chk:{check_name}
|
|
62
62
|
*
|
|
63
|
-
* @param
|
|
64
|
-
* @param scope - Scope of the check
|
|
63
|
+
* @param checkName - Name of the check
|
|
65
64
|
* @returns Unique check key
|
|
66
65
|
*
|
|
67
66
|
* @example
|
|
68
67
|
* ```ts
|
|
69
|
-
* generateCheckKey("sender_verification"
|
|
70
|
-
* // => "chk:sender_verification
|
|
68
|
+
* generateCheckKey("sender_verification")
|
|
69
|
+
* // => "chk:sender_verification"
|
|
71
70
|
* ```
|
|
72
71
|
*/
|
|
73
|
-
export function generateCheckKey(
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
return `chk:${normalizedId}:${normalizedScope}`;
|
|
72
|
+
export function generateCheckKey(checkName: string): string {
|
|
73
|
+
const normalizedName = normalizeValue(checkName);
|
|
74
|
+
return `chk:${normalizedName}`;
|
|
77
75
|
}
|
|
78
76
|
|
|
79
77
|
/**
|
|
@@ -127,31 +125,88 @@ export function generateEnrichmentKey(name: string, context?: string): string {
|
|
|
127
125
|
}
|
|
128
126
|
|
|
129
127
|
/**
|
|
130
|
-
* Generate a unique key for a
|
|
128
|
+
* Generate a unique key for a tag.
|
|
131
129
|
*
|
|
132
|
-
* Format:
|
|
130
|
+
* Format: tag:{normalized_name}
|
|
133
131
|
*
|
|
134
|
-
* @param
|
|
135
|
-
* @returns Unique
|
|
132
|
+
* @param name - Name of the tag (can use : as hierarchy delimiter)
|
|
133
|
+
* @returns Unique tag key
|
|
136
134
|
*
|
|
137
135
|
* @example
|
|
138
136
|
* ```ts
|
|
139
|
-
*
|
|
140
|
-
* // => "
|
|
137
|
+
* generateTagKey("header:auth:dkim")
|
|
138
|
+
* // => "tag:header:auth:dkim"
|
|
141
139
|
* ```
|
|
142
140
|
*/
|
|
143
|
-
export function
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
141
|
+
export function generateTagKey(name: string): string {
|
|
142
|
+
const normalizedName = normalizeValue(name);
|
|
143
|
+
return `tag:${normalizedName}`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Get all ancestor tag names from a hierarchical tag name.
|
|
148
|
+
*
|
|
149
|
+
* @param name - Tag name with : delimiter
|
|
150
|
+
* @returns Array of ancestor tag names
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* ```ts
|
|
154
|
+
* getTagAncestors("header:auth:dkim")
|
|
155
|
+
* // => ["header", "header:auth"]
|
|
156
|
+
* ```
|
|
157
|
+
*/
|
|
158
|
+
export function getTagAncestors(name: string): string[] {
|
|
159
|
+
const parts = name.split(":");
|
|
160
|
+
const ancestors: string[] = [];
|
|
161
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
162
|
+
ancestors.push(parts.slice(0, i + 1).join(":"));
|
|
163
|
+
}
|
|
164
|
+
return ancestors;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Check if a tag is a direct child of another tag.
|
|
169
|
+
*
|
|
170
|
+
* @param childName - Potential child tag name
|
|
171
|
+
* @param parentName - Potential parent tag name
|
|
172
|
+
* @returns True if childName is a direct child of parentName
|
|
173
|
+
*
|
|
174
|
+
* @example
|
|
175
|
+
* ```ts
|
|
176
|
+
* isTagChildOf("header:auth", "header") // => true
|
|
177
|
+
* isTagChildOf("header:auth:dkim", "header") // => false (grandchild)
|
|
178
|
+
* ```
|
|
179
|
+
*/
|
|
180
|
+
export function isTagChildOf(childName: string, parentName: string): boolean {
|
|
181
|
+
if (!childName.startsWith(parentName + ":")) {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
const remaining = childName.slice(parentName.length + 1);
|
|
185
|
+
return !remaining.includes(":");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Check if a tag is a descendant of another tag (any depth).
|
|
190
|
+
*
|
|
191
|
+
* @param descendantName - Potential descendant tag name
|
|
192
|
+
* @param ancestorName - Potential ancestor tag name
|
|
193
|
+
* @returns True if descendantName is a descendant of ancestorName
|
|
194
|
+
*
|
|
195
|
+
* @example
|
|
196
|
+
* ```ts
|
|
197
|
+
* isTagDescendantOf("header:auth:dkim", "header") // => true
|
|
198
|
+
* isTagDescendantOf("header", "header") // => false (same)
|
|
199
|
+
* ```
|
|
200
|
+
*/
|
|
201
|
+
export function isTagDescendantOf(descendantName: string, ancestorName: string): boolean {
|
|
202
|
+
return descendantName.startsWith(ancestorName + ":");
|
|
148
203
|
}
|
|
149
204
|
|
|
150
205
|
/**
|
|
151
206
|
* Extract the type prefix from a key.
|
|
152
207
|
*
|
|
153
208
|
* @param key - The key to parse
|
|
154
|
-
* @returns Type prefix (obs, chk, ti, enr,
|
|
209
|
+
* @returns Type prefix (obs, chk, ti, enr, tag) or null if invalid
|
|
155
210
|
*
|
|
156
211
|
* @example
|
|
157
212
|
* ```ts
|
|
@@ -162,7 +217,7 @@ export function generateContainerKey(path: string): string {
|
|
|
162
217
|
export function parseKeyType(key: string): KeyType | null {
|
|
163
218
|
if (key.includes(":")) {
|
|
164
219
|
const prefix = key.split(":", 1)[0] as KeyType;
|
|
165
|
-
if (["obs", "chk", "ti", "enr", "
|
|
220
|
+
if (["obs", "chk", "ti", "enr", "tag"].includes(prefix)) {
|
|
166
221
|
return prefix;
|
|
167
222
|
}
|
|
168
223
|
}
|
|
@@ -233,25 +288,24 @@ export function parseObservableKey(
|
|
|
233
288
|
* Extract components from a check key.
|
|
234
289
|
*
|
|
235
290
|
* @param key - Check key to parse
|
|
236
|
-
* @returns Object with
|
|
291
|
+
* @returns Object with checkName, or null if invalid
|
|
237
292
|
*
|
|
238
293
|
* @example
|
|
239
294
|
* ```ts
|
|
240
|
-
* parseCheckKey("chk:sender_verification
|
|
241
|
-
* // => {
|
|
295
|
+
* parseCheckKey("chk:sender_verification")
|
|
296
|
+
* // => { checkName: "sender_verification" }
|
|
242
297
|
* ```
|
|
243
298
|
*/
|
|
244
299
|
export function parseCheckKey(
|
|
245
300
|
key: string
|
|
246
|
-
): {
|
|
301
|
+
): { checkName: string } | null {
|
|
247
302
|
if (!validateKey(key, "chk")) {
|
|
248
303
|
return null;
|
|
249
304
|
}
|
|
250
305
|
const parts = key.split(":");
|
|
251
|
-
if (parts.length >=
|
|
306
|
+
if (parts.length >= 2) {
|
|
252
307
|
return {
|
|
253
|
-
|
|
254
|
-
scope: parts.slice(2).join(":"),
|
|
308
|
+
checkName: parts.slice(1).join(":"),
|
|
255
309
|
};
|
|
256
310
|
}
|
|
257
311
|
return null;
|
package/src/levels.ts
CHANGED
|
@@ -9,7 +9,7 @@ import type {
|
|
|
9
9
|
Observable,
|
|
10
10
|
Check,
|
|
11
11
|
ThreatIntel,
|
|
12
|
-
|
|
12
|
+
Tag,
|
|
13
13
|
Level,
|
|
14
14
|
} from "./types.generated";
|
|
15
15
|
|
|
@@ -254,15 +254,15 @@ export function hasLevel(obj: unknown): obj is { level: Level } {
|
|
|
254
254
|
}
|
|
255
255
|
|
|
256
256
|
/**
|
|
257
|
-
* Extract level from an entity (Observable, Check, ThreatIntel,
|
|
257
|
+
* Extract level from an entity (Observable, Check, ThreatIntel, Tag).
|
|
258
258
|
*/
|
|
259
259
|
export function getEntityLevel(
|
|
260
|
-
entity: Observable | Check | ThreatIntel |
|
|
260
|
+
entity: Observable | Check | ThreatIntel | Tag
|
|
261
261
|
): Level {
|
|
262
|
-
if ("
|
|
263
|
-
const
|
|
264
|
-
if (typeof
|
|
265
|
-
return
|
|
262
|
+
if ("direct_level" in entity) {
|
|
263
|
+
const directLevel = entity.direct_level;
|
|
264
|
+
if (typeof directLevel === "string" && isValidLevel(directLevel)) {
|
|
265
|
+
return directLevel;
|
|
266
266
|
}
|
|
267
267
|
}
|
|
268
268
|
if ("level" in entity && isValidLevel(entity.level)) {
|