@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.
@@ -0,0 +1,398 @@
1
+ import { describe, it, expect, beforeEach } from "vitest";
2
+ import type { CyvestInvestigation } from "../src";
3
+ import {
4
+ getRelatedObservables,
5
+ getObservableChildren,
6
+ getObservableParents,
7
+ getRelatedObservablesByType,
8
+ getObservableGraph,
9
+ findRootObservables,
10
+ findOrphanObservables,
11
+ findLeafObservables,
12
+ areConnected,
13
+ findPath,
14
+ getReachableObservables,
15
+ getAllRelationshipTypes,
16
+ countRelationshipsByType,
17
+ getRelationshipsForObservable,
18
+ } from "../src";
19
+
20
+ // Test fixture with relationships
21
+ function createGraphTestInvestigation(): CyvestInvestigation {
22
+ return {
23
+ score: 5,
24
+ level: "MALICIOUS",
25
+ whitelisted: false,
26
+ whitelists: [],
27
+ observables: {
28
+ "obs:email-message:msg1": {
29
+ key: "obs:email-message:msg1",
30
+ type: "email-message",
31
+ value: "msg1",
32
+ internal: false,
33
+ whitelisted: false,
34
+ comment: "",
35
+ extra: null,
36
+ score: 0,
37
+ level: "INFO",
38
+ relationships: [
39
+ {
40
+ target_key: "obs:email-addr:sender@example.com",
41
+ relationship_type: "from",
42
+ direction: "outbound",
43
+ },
44
+ {
45
+ target_key: "obs:ipv4-addr:192.168.1.1",
46
+ relationship_type: "originated-from",
47
+ direction: "outbound",
48
+ },
49
+ ],
50
+ threat_intels: [],
51
+ generated_by_checks: [],
52
+ },
53
+ "obs:email-addr:sender@example.com": {
54
+ key: "obs:email-addr:sender@example.com",
55
+ type: "email-addr",
56
+ value: "sender@example.com",
57
+ internal: false,
58
+ whitelisted: false,
59
+ comment: "",
60
+ extra: null,
61
+ score: 0,
62
+ level: "INFO",
63
+ relationships: [
64
+ {
65
+ target_key: "obs:domain-name:example.com",
66
+ relationship_type: "related-to",
67
+ direction: "outbound",
68
+ },
69
+ ],
70
+ threat_intels: [],
71
+ generated_by_checks: [],
72
+ },
73
+ "obs:ipv4-addr:192.168.1.1": {
74
+ key: "obs:ipv4-addr:192.168.1.1",
75
+ type: "ipv4-addr",
76
+ value: "192.168.1.1",
77
+ internal: true,
78
+ whitelisted: false,
79
+ comment: "",
80
+ extra: null,
81
+ score: 0,
82
+ level: "INFO",
83
+ relationships: [],
84
+ threat_intels: [],
85
+ generated_by_checks: [],
86
+ },
87
+ "obs:domain-name:example.com": {
88
+ key: "obs:domain-name:example.com",
89
+ type: "domain-name",
90
+ value: "example.com",
91
+ internal: false,
92
+ whitelisted: false,
93
+ comment: "",
94
+ extra: null,
95
+ score: 5,
96
+ level: "MALICIOUS",
97
+ relationships: [],
98
+ threat_intels: [],
99
+ generated_by_checks: [],
100
+ },
101
+ "obs:file-hash:abc123": {
102
+ key: "obs:file-hash:abc123",
103
+ type: "file-hash",
104
+ value: "abc123",
105
+ internal: false,
106
+ whitelisted: false,
107
+ comment: "",
108
+ extra: null,
109
+ score: 3,
110
+ level: "SUSPICIOUS",
111
+ relationships: [],
112
+ threat_intels: [],
113
+ generated_by_checks: [],
114
+ },
115
+ },
116
+ checks: {},
117
+ checks_by_level: {},
118
+ threat_intels: {},
119
+ enrichments: {},
120
+ containers: {},
121
+ stats: {
122
+ total_observables: 5,
123
+ internal_observables: 1,
124
+ external_observables: 4,
125
+ whitelisted_observables: 0,
126
+ observables_by_type: {},
127
+ observables_by_level: {},
128
+ observables_by_type_and_level: {},
129
+ total_checks: 0,
130
+ applied_checks: 0,
131
+ checks_by_scope: {},
132
+ checks_by_level: {},
133
+ total_threat_intel: 0,
134
+ threat_intel_by_source: {},
135
+ threat_intel_by_level: {},
136
+ total_containers: 0,
137
+ },
138
+ stats_checks: {
139
+ checks: 0,
140
+ applied: 0,
141
+ },
142
+ data_extraction: {
143
+ root_type: "email-message",
144
+ score_mode: "max",
145
+ },
146
+ };
147
+ }
148
+
149
+ describe("Graph Traversal", () => {
150
+ let inv: CyvestInvestigation;
151
+
152
+ beforeEach(() => {
153
+ inv = createGraphTestInvestigation();
154
+ });
155
+
156
+ describe("getRelatedObservables", () => {
157
+ it("returns all directly connected observables", () => {
158
+ const related = getRelatedObservables(inv, "obs:email-message:msg1");
159
+ expect(related).toHaveLength(2);
160
+ const values = related.map((o) => o.value);
161
+ expect(values).toContain("sender@example.com");
162
+ expect(values).toContain("192.168.1.1");
163
+ });
164
+
165
+ it("returns empty array for non-existent observable", () => {
166
+ expect(getRelatedObservables(inv, "obs:missing:key")).toEqual([]);
167
+ });
168
+
169
+ it("includes inbound relationships", () => {
170
+ const related = getRelatedObservables(
171
+ inv,
172
+ "obs:email-addr:sender@example.com"
173
+ );
174
+ // Has outbound to domain and inbound from email message
175
+ expect(related.length).toBeGreaterThanOrEqual(2);
176
+ });
177
+ });
178
+
179
+ describe("getObservableChildren", () => {
180
+ it("returns outbound related observables", () => {
181
+ const children = getObservableChildren(inv, "obs:email-message:msg1");
182
+ expect(children).toHaveLength(2);
183
+ });
184
+
185
+ it("returns empty for leaf nodes", () => {
186
+ const children = getObservableChildren(inv, "obs:ipv4-addr:192.168.1.1");
187
+ expect(children).toHaveLength(0);
188
+ });
189
+ });
190
+
191
+ describe("getObservableParents", () => {
192
+ it("returns observables pointing to this one", () => {
193
+ const parents = getObservableParents(
194
+ inv,
195
+ "obs:email-addr:sender@example.com"
196
+ );
197
+ expect(parents).toHaveLength(1);
198
+ expect(parents[0].value).toBe("msg1");
199
+ });
200
+
201
+ it("returns empty for root nodes", () => {
202
+ const parents = getObservableParents(inv, "obs:email-message:msg1");
203
+ expect(parents).toHaveLength(0);
204
+ });
205
+ });
206
+
207
+ describe("getRelatedObservablesByType", () => {
208
+ it("filters by relationship type", () => {
209
+ const fromRelated = getRelatedObservablesByType(
210
+ inv,
211
+ "obs:email-message:msg1",
212
+ "from"
213
+ );
214
+ expect(fromRelated).toHaveLength(1);
215
+ expect(fromRelated[0].value).toBe("sender@example.com");
216
+ });
217
+ });
218
+
219
+ describe("getObservableGraph", () => {
220
+ it("returns correct node count", () => {
221
+ const graph = getObservableGraph(inv);
222
+ expect(graph.nodes).toHaveLength(5);
223
+ });
224
+
225
+ it("returns correct edge count", () => {
226
+ const graph = getObservableGraph(inv);
227
+ expect(graph.edges).toHaveLength(3);
228
+ });
229
+
230
+ it("nodes have correct structure", () => {
231
+ const graph = getObservableGraph(inv);
232
+ const emailNode = graph.nodes.find(
233
+ (n) => n.id === "obs:email-message:msg1"
234
+ );
235
+ expect(emailNode).toBeDefined();
236
+ expect(emailNode?.type).toBe("email-message");
237
+ expect(emailNode?.value).toBe("msg1");
238
+ expect(emailNode?.level).toBe("INFO");
239
+ });
240
+
241
+ it("edges have correct structure", () => {
242
+ const graph = getObservableGraph(inv);
243
+ const fromEdge = graph.edges.find((e) => e.type === "from");
244
+ expect(fromEdge).toBeDefined();
245
+ expect(fromEdge?.source).toBe("obs:email-message:msg1");
246
+ expect(fromEdge?.target).toBe("obs:email-addr:sender@example.com");
247
+ });
248
+ });
249
+
250
+ describe("findRootObservables", () => {
251
+ it("finds observables with no incoming relationships", () => {
252
+ const roots = findRootObservables(inv);
253
+ // email-message and file-hash are roots
254
+ expect(roots.length).toBeGreaterThanOrEqual(2);
255
+ const values = roots.map((o) => o.value);
256
+ expect(values).toContain("msg1");
257
+ expect(values).toContain("abc123");
258
+ });
259
+ });
260
+
261
+ describe("findOrphanObservables", () => {
262
+ it("finds observables with no relationships", () => {
263
+ const orphans = findOrphanObservables(inv);
264
+ // file-hash has no relationships
265
+ expect(orphans).toHaveLength(1);
266
+ expect(orphans[0].value).toBe("abc123");
267
+ });
268
+ });
269
+
270
+ describe("findLeafObservables", () => {
271
+ it("finds observables that are targets but have no outbound", () => {
272
+ const leaves = findLeafObservables(inv);
273
+ const values = leaves.map((o) => o.value);
274
+ expect(values).toContain("192.168.1.1");
275
+ expect(values).toContain("example.com");
276
+ });
277
+ });
278
+
279
+ describe("areConnected", () => {
280
+ it("returns true for directly connected", () => {
281
+ expect(
282
+ areConnected(inv, "obs:email-message:msg1", "obs:ipv4-addr:192.168.1.1")
283
+ ).toBe(true);
284
+ });
285
+
286
+ it("returns true for transitively connected", () => {
287
+ expect(
288
+ areConnected(
289
+ inv,
290
+ "obs:email-message:msg1",
291
+ "obs:domain-name:example.com"
292
+ )
293
+ ).toBe(true);
294
+ });
295
+
296
+ it("returns false for disconnected", () => {
297
+ expect(
298
+ areConnected(inv, "obs:file-hash:abc123", "obs:email-message:msg1")
299
+ ).toBe(false);
300
+ });
301
+
302
+ it("returns true for same node", () => {
303
+ expect(
304
+ areConnected(inv, "obs:email-message:msg1", "obs:email-message:msg1")
305
+ ).toBe(true);
306
+ });
307
+ });
308
+
309
+ describe("findPath", () => {
310
+ it("finds direct path", () => {
311
+ const path = findPath(
312
+ inv,
313
+ "obs:email-message:msg1",
314
+ "obs:ipv4-addr:192.168.1.1"
315
+ );
316
+ expect(path).toEqual([
317
+ "obs:email-message:msg1",
318
+ "obs:ipv4-addr:192.168.1.1",
319
+ ]);
320
+ });
321
+
322
+ it("finds transitive path", () => {
323
+ const path = findPath(
324
+ inv,
325
+ "obs:email-message:msg1",
326
+ "obs:domain-name:example.com"
327
+ );
328
+ expect(path).not.toBeNull();
329
+ expect(path?.length).toBe(3);
330
+ expect(path?.[0]).toBe("obs:email-message:msg1");
331
+ expect(path?.[path.length - 1]).toBe("obs:domain-name:example.com");
332
+ });
333
+
334
+ it("returns null for no path", () => {
335
+ const path = findPath(
336
+ inv,
337
+ "obs:file-hash:abc123",
338
+ "obs:email-message:msg1"
339
+ );
340
+ expect(path).toBeNull();
341
+ });
342
+
343
+ it("returns single node for same source/target", () => {
344
+ const path = findPath(
345
+ inv,
346
+ "obs:email-message:msg1",
347
+ "obs:email-message:msg1"
348
+ );
349
+ expect(path).toEqual(["obs:email-message:msg1"]);
350
+ });
351
+ });
352
+
353
+ describe("getReachableObservables", () => {
354
+ it("returns all reachable from start", () => {
355
+ const reachable = getReachableObservables(inv, "obs:email-message:msg1");
356
+ expect(reachable).toHaveLength(4); // msg1 + sender + ip + domain
357
+ });
358
+
359
+ it("respects max depth", () => {
360
+ const reachable = getReachableObservables(
361
+ inv,
362
+ "obs:email-message:msg1",
363
+ 1
364
+ );
365
+ expect(reachable).toHaveLength(3); // msg1 + sender + ip (depth 1)
366
+ });
367
+ });
368
+
369
+ describe("getAllRelationshipTypes", () => {
370
+ it("returns unique relationship types", () => {
371
+ const types = getAllRelationshipTypes(inv);
372
+ expect(types).toContain("from");
373
+ expect(types).toContain("originated-from");
374
+ expect(types).toContain("related-to");
375
+ });
376
+ });
377
+
378
+ describe("countRelationshipsByType", () => {
379
+ it("counts relationships by type", () => {
380
+ const counts = countRelationshipsByType(inv);
381
+ expect(counts["from"]).toBe(1);
382
+ expect(counts["originated-from"]).toBe(1);
383
+ expect(counts["related-to"]).toBe(1);
384
+ });
385
+ });
386
+
387
+ describe("getRelationshipsForObservable", () => {
388
+ it("returns outbound and inbound relationships", () => {
389
+ const rels = getRelationshipsForObservable(
390
+ inv,
391
+ "obs:email-addr:sender@example.com"
392
+ );
393
+ expect(rels.outbound).toHaveLength(1);
394
+ expect(rels.inbound).toHaveLength(1);
395
+ expect(rels.all.length).toBe(2);
396
+ });
397
+ });
398
+ });
@@ -0,0 +1,298 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ // Keys
4
+ generateObservableKey,
5
+ generateCheckKey,
6
+ generateThreatIntelKey,
7
+ generateEnrichmentKey,
8
+ generateContainerKey,
9
+ parseKeyType,
10
+ validateKey,
11
+ parseObservableKey,
12
+ parseCheckKey,
13
+ parseThreatIntelKey,
14
+ // Levels
15
+ normalizeLevel,
16
+ isValidLevel,
17
+ getLevelFromScore,
18
+ compareLevels,
19
+ isLevelHigherThan,
20
+ isLevelAtLeast,
21
+ maxLevel,
22
+ minLevel,
23
+ LEVEL_ORDER,
24
+ LEVEL_VALUES,
25
+ } from "../src";
26
+
27
+ describe("Key Generation", () => {
28
+ describe("generateObservableKey", () => {
29
+ it("generates correct observable key", () => {
30
+ expect(generateObservableKey("ipv4-addr", "192.168.1.1")).toBe(
31
+ "obs:ipv4-addr:192.168.1.1"
32
+ );
33
+ });
34
+
35
+ it("normalizes to lowercase", () => {
36
+ expect(generateObservableKey("IPV4-ADDR", "192.168.1.1")).toBe(
37
+ "obs:ipv4-addr:192.168.1.1"
38
+ );
39
+ });
40
+
41
+ it("trims whitespace", () => {
42
+ expect(generateObservableKey(" ipv4-addr ", " 192.168.1.1 ")).toBe(
43
+ "obs:ipv4-addr:192.168.1.1"
44
+ );
45
+ });
46
+ });
47
+
48
+ describe("generateCheckKey", () => {
49
+ it("generates correct check key", () => {
50
+ expect(generateCheckKey("sender_verification", "email_headers")).toBe(
51
+ "chk:sender_verification:email_headers"
52
+ );
53
+ });
54
+ });
55
+
56
+ describe("generateThreatIntelKey", () => {
57
+ it("generates correct threat intel key", () => {
58
+ expect(
59
+ generateThreatIntelKey("virustotal", "obs:ipv4-addr:192.168.1.1")
60
+ ).toBe("ti:virustotal:obs:ipv4-addr:192.168.1.1");
61
+ });
62
+ });
63
+
64
+ describe("generateEnrichmentKey", () => {
65
+ it("generates key without context", () => {
66
+ expect(generateEnrichmentKey("whois_data")).toBe("enr:whois_data");
67
+ });
68
+
69
+ it("generates key with context hash", () => {
70
+ const key = generateEnrichmentKey("whois_data", "example.com");
71
+ expect(key).toMatch(/^enr:whois_data:[a-f0-9]+$/);
72
+ });
73
+ });
74
+
75
+ describe("generateContainerKey", () => {
76
+ it("generates correct container key", () => {
77
+ expect(generateContainerKey("email/headers")).toBe("ctr:email/headers");
78
+ });
79
+
80
+ it("normalizes path separators", () => {
81
+ expect(generateContainerKey("email\\headers")).toBe("ctr:email/headers");
82
+ });
83
+
84
+ it("strips leading/trailing slashes", () => {
85
+ expect(generateContainerKey("/email/headers/")).toBe("ctr:email/headers");
86
+ });
87
+ });
88
+
89
+ describe("parseKeyType", () => {
90
+ it("parses observable key type", () => {
91
+ expect(parseKeyType("obs:ipv4-addr:192.168.1.1")).toBe("obs");
92
+ });
93
+
94
+ it("parses check key type", () => {
95
+ expect(parseKeyType("chk:sender:email")).toBe("chk");
96
+ });
97
+
98
+ it("parses threat intel key type", () => {
99
+ expect(parseKeyType("ti:vt:obs:ip:1.1.1.1")).toBe("ti");
100
+ });
101
+
102
+ it("parses enrichment key type", () => {
103
+ expect(parseKeyType("enr:whois")).toBe("enr");
104
+ });
105
+
106
+ it("parses container key type", () => {
107
+ expect(parseKeyType("ctr:email/body")).toBe("ctr");
108
+ });
109
+
110
+ it("returns null for invalid key", () => {
111
+ expect(parseKeyType("invalid")).toBeNull();
112
+ expect(parseKeyType("foo:bar")).toBeNull();
113
+ });
114
+ });
115
+
116
+ describe("validateKey", () => {
117
+ it("validates correct keys", () => {
118
+ expect(validateKey("obs:ipv4-addr:192.168.1.1")).toBe(true);
119
+ expect(validateKey("chk:sender:email")).toBe(true);
120
+ });
121
+
122
+ it("validates key type", () => {
123
+ expect(validateKey("obs:ipv4-addr:192.168.1.1", "obs")).toBe(true);
124
+ expect(validateKey("obs:ipv4-addr:192.168.1.1", "chk")).toBe(false);
125
+ });
126
+
127
+ it("rejects invalid keys", () => {
128
+ expect(validateKey("")).toBe(false);
129
+ expect(validateKey("invalid")).toBe(false);
130
+ expect(validateKey("foo:bar")).toBe(false);
131
+ });
132
+ });
133
+
134
+ describe("parseObservableKey", () => {
135
+ it("parses observable key components", () => {
136
+ expect(parseObservableKey("obs:ipv4-addr:192.168.1.1")).toEqual({
137
+ type: "ipv4-addr",
138
+ value: "192.168.1.1",
139
+ });
140
+ });
141
+
142
+ it("handles values with colons", () => {
143
+ expect(parseObservableKey("obs:url:http://example.com:8080")).toEqual({
144
+ type: "url",
145
+ value: "http://example.com:8080",
146
+ });
147
+ });
148
+
149
+ it("returns null for invalid key", () => {
150
+ expect(parseObservableKey("chk:sender:email")).toBeNull();
151
+ });
152
+ });
153
+
154
+ describe("parseCheckKey", () => {
155
+ it("parses check key components", () => {
156
+ expect(parseCheckKey("chk:sender_verification:email_headers")).toEqual({
157
+ checkId: "sender_verification",
158
+ scope: "email_headers",
159
+ });
160
+ });
161
+ });
162
+
163
+ describe("parseThreatIntelKey", () => {
164
+ it("parses threat intel key components", () => {
165
+ expect(
166
+ parseThreatIntelKey("ti:virustotal:obs:ipv4-addr:192.168.1.1")
167
+ ).toEqual({
168
+ source: "virustotal",
169
+ observableKey: "obs:ipv4-addr:192.168.1.1",
170
+ });
171
+ });
172
+ });
173
+ });
174
+
175
+ describe("Levels", () => {
176
+ describe("LEVEL_ORDER", () => {
177
+ it("has correct order", () => {
178
+ expect(LEVEL_ORDER).toEqual([
179
+ "NONE",
180
+ "TRUSTED",
181
+ "INFO",
182
+ "SAFE",
183
+ "NOTABLE",
184
+ "SUSPICIOUS",
185
+ "MALICIOUS",
186
+ ]);
187
+ });
188
+ });
189
+
190
+ describe("normalizeLevel", () => {
191
+ it("normalizes lowercase input", () => {
192
+ expect(normalizeLevel("malicious")).toBe("MALICIOUS");
193
+ expect(normalizeLevel("info")).toBe("INFO");
194
+ });
195
+
196
+ it("accepts uppercase input", () => {
197
+ expect(normalizeLevel("MALICIOUS")).toBe("MALICIOUS");
198
+ });
199
+
200
+ it("throws on invalid input", () => {
201
+ expect(() => normalizeLevel("invalid")).toThrow("Invalid level name");
202
+ });
203
+ });
204
+
205
+ describe("isValidLevel", () => {
206
+ it("returns true for valid levels", () => {
207
+ expect(isValidLevel("MALICIOUS")).toBe(true);
208
+ expect(isValidLevel("info")).toBe(true);
209
+ });
210
+
211
+ it("returns false for invalid levels", () => {
212
+ expect(isValidLevel("invalid")).toBe(false);
213
+ });
214
+ });
215
+
216
+ describe("getLevelFromScore", () => {
217
+ it("returns TRUSTED for negative scores", () => {
218
+ expect(getLevelFromScore(-1)).toBe("TRUSTED");
219
+ expect(getLevelFromScore(-0.5)).toBe("TRUSTED");
220
+ });
221
+
222
+ it("returns INFO for zero", () => {
223
+ expect(getLevelFromScore(0)).toBe("INFO");
224
+ });
225
+
226
+ it("returns NOTABLE for scores < 3", () => {
227
+ expect(getLevelFromScore(0.1)).toBe("NOTABLE");
228
+ expect(getLevelFromScore(2.9)).toBe("NOTABLE");
229
+ });
230
+
231
+ it("returns SUSPICIOUS for scores < 5", () => {
232
+ expect(getLevelFromScore(3)).toBe("SUSPICIOUS");
233
+ expect(getLevelFromScore(4.9)).toBe("SUSPICIOUS");
234
+ });
235
+
236
+ it("returns MALICIOUS for scores >= 5", () => {
237
+ expect(getLevelFromScore(5)).toBe("MALICIOUS");
238
+ expect(getLevelFromScore(10)).toBe("MALICIOUS");
239
+ });
240
+ });
241
+
242
+ describe("compareLevels", () => {
243
+ it("returns -1 when a < b", () => {
244
+ expect(compareLevels("INFO", "MALICIOUS")).toBe(-1);
245
+ });
246
+
247
+ it("returns 1 when a > b", () => {
248
+ expect(compareLevels("MALICIOUS", "INFO")).toBe(1);
249
+ });
250
+
251
+ it("returns 0 when a === b", () => {
252
+ expect(compareLevels("INFO", "INFO")).toBe(0);
253
+ });
254
+ });
255
+
256
+ describe("isLevelHigherThan", () => {
257
+ it("returns true when a is more severe", () => {
258
+ expect(isLevelHigherThan("MALICIOUS", "SUSPICIOUS")).toBe(true);
259
+ expect(isLevelHigherThan("SUSPICIOUS", "INFO")).toBe(true);
260
+ });
261
+
262
+ it("returns false when a is less severe or equal", () => {
263
+ expect(isLevelHigherThan("INFO", "MALICIOUS")).toBe(false);
264
+ expect(isLevelHigherThan("INFO", "INFO")).toBe(false);
265
+ });
266
+ });
267
+
268
+ describe("isLevelAtLeast", () => {
269
+ it("returns true when a >= minLevel", () => {
270
+ expect(isLevelAtLeast("MALICIOUS", "SUSPICIOUS")).toBe(true);
271
+ expect(isLevelAtLeast("SUSPICIOUS", "SUSPICIOUS")).toBe(true);
272
+ });
273
+
274
+ it("returns false when a < minLevel", () => {
275
+ expect(isLevelAtLeast("INFO", "SUSPICIOUS")).toBe(false);
276
+ });
277
+ });
278
+
279
+ describe("maxLevel", () => {
280
+ it("returns most severe level", () => {
281
+ expect(maxLevel(["INFO", "MALICIOUS", "SUSPICIOUS"])).toBe("MALICIOUS");
282
+ });
283
+
284
+ it("returns NONE for empty array", () => {
285
+ expect(maxLevel([])).toBe("NONE");
286
+ });
287
+ });
288
+
289
+ describe("minLevel", () => {
290
+ it("returns least severe level", () => {
291
+ expect(minLevel(["INFO", "MALICIOUS", "SUSPICIOUS"])).toBe("INFO");
292
+ });
293
+
294
+ it("returns MALICIOUS for empty array", () => {
295
+ expect(minLevel([])).toBe("MALICIOUS");
296
+ });
297
+ });
298
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,4 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "include": ["src"]
4
+ }