@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.
- 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/types.generated.ts
CHANGED
|
@@ -81,7 +81,7 @@ export interface CyvestInvestigation {
|
|
|
81
81
|
checks: Checks;
|
|
82
82
|
threat_intels: ThreatIntels1;
|
|
83
83
|
enrichments: Enrichments;
|
|
84
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
*
|
|
252
|
+
* Tags keyed by their unique key.
|
|
254
253
|
*/
|
|
255
|
-
export interface
|
|
256
|
-
[k: string]:
|
|
254
|
+
export interface Tags {
|
|
255
|
+
[k: string]: Tag;
|
|
257
256
|
}
|
|
258
257
|
/**
|
|
259
|
-
* Groups checks
|
|
258
|
+
* Groups checks for categorical organization.
|
|
260
259
|
*
|
|
261
|
-
*
|
|
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
|
|
265
|
-
|
|
264
|
+
export interface Tag {
|
|
265
|
+
name: string;
|
|
266
266
|
description?: string;
|
|
267
267
|
checks: Checks1;
|
|
268
|
-
sub_containers: SubContainers;
|
|
269
268
|
key: string;
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
{
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
210
|
-
"
|
|
211
|
-
key: "
|
|
212
|
-
|
|
213
|
-
description: "Email
|
|
205
|
+
tags: {
|
|
206
|
+
"tag:email": {
|
|
207
|
+
key: "tag:email",
|
|
208
|
+
name: "email",
|
|
209
|
+
description: "Email tag",
|
|
214
210
|
checks: [],
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
296
|
+
const check = getCheck(inv, "chk:domain_check");
|
|
289
297
|
expect(check).toBeDefined();
|
|
290
|
-
expect(check?.
|
|
298
|
+
expect(check?.check_name).toBe("domain_check");
|
|
291
299
|
});
|
|
292
300
|
});
|
|
293
301
|
|
|
294
|
-
describe("
|
|
295
|
-
it("finds check by
|
|
296
|
-
const check =
|
|
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
|
|
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("
|
|
321
|
-
it("returns
|
|
322
|
-
const
|
|
323
|
-
expect(
|
|
324
|
-
expect(
|
|
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
|
|
328
|
-
const
|
|
329
|
-
expect(
|
|
330
|
-
expect(
|
|
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("
|
|
335
|
-
it("returns all
|
|
336
|
-
const
|
|
337
|
-
expect(
|
|
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.
|
|
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("
|
|
444
|
+
describe("findChecksForObservable", () => {
|
|
444
445
|
it("finds checks that reference observable", () => {
|
|
445
|
-
const checks =
|
|
446
|
+
const checks = findChecksForObservable(inv, "obs:ipv4-addr:192.168.1.1");
|
|
446
447
|
expect(checks).toHaveLength(1);
|
|
447
|
-
expect(checks[0].
|
|
448
|
+
expect(checks[0].check_name).toBe("ip_check");
|
|
448
449
|
});
|
|
449
450
|
});
|
|
450
451
|
|
|
451
|
-
describe("
|
|
452
|
+
describe("findThreatIntelsForObservable", () => {
|
|
452
453
|
it("finds threat intel for observable", () => {
|
|
453
|
-
const tis =
|
|
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("
|
|
463
|
+
describe("findObservablesForCheck", () => {
|
|
463
464
|
it("finds observables referenced by check", () => {
|
|
464
|
-
const obs =
|
|
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("
|
|
471
|
+
describe("findHighestScoringObservables", () => {
|
|
471
472
|
it("returns top scoring observables", () => {
|
|
472
|
-
const top =
|
|
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("
|
|
479
|
+
describe("findMaliciousObservables", () => {
|
|
479
480
|
it("returns malicious observables", () => {
|
|
480
|
-
const mal =
|
|
481
|
+
const mal = findMaliciousObservables(inv);
|
|
481
482
|
expect(mal).toHaveLength(2);
|
|
482
483
|
});
|
|
483
484
|
});
|
|
484
485
|
|
|
485
|
-
describe("
|
|
486
|
-
it("returns all
|
|
487
|
-
const
|
|
488
|
-
expect(
|
|
489
|
-
expect(
|
|
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
|
});
|
package/tests/graph.test.ts
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
getObservableParents,
|
|
7
7
|
getRelatedObservablesByType,
|
|
8
8
|
getObservableGraph,
|
|
9
|
-
|
|
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("
|
|
261
|
+
describe("findSourceObservables", () => {
|
|
263
262
|
it("finds observables with no incoming relationships", () => {
|
|
264
|
-
const
|
|
265
|
-
// email-message and file-hash are
|
|
266
|
-
expect(
|
|
267
|
-
const values =
|
|
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
|
});
|