@cccsaurora/howler-ui 2.19.0-cases.1122 → 2.19.0-cases.1131
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/components/elements/display/QueryResultText.d.ts +1 -0
- package/components/elements/display/QueryResultText.js +2 -2
- package/components/routes/cases/detail/CaseObservables.d.ts +0 -5
- package/components/routes/cases/detail/CaseObservables.js +101 -63
- package/components/routes/cases/detail/CaseObservables.test.js +86 -81
- package/components/routes/cases/detail/CaseRules.js +5 -7
- package/components/routes/cases/detail/CaseRules.test.js +18 -1
- package/components/routes/cases/detail/CreateRuleDialog.js +37 -55
- package/components/routes/cases/detail/CreateRuleDialog.test.js +47 -22
- package/components/routes/cases/detail/observables/Observable.d.ts +2 -8
- package/components/routes/cases/detail/observables/Observable.js +2 -2
- package/components/routes/cases/detail/observables/Observable.test.js +8 -8
- package/components/routes/cases/detail/observables/ObservableTable.d.ts +7 -0
- package/components/routes/cases/detail/observables/ObservableTable.js +61 -0
- package/components/routes/cases/detail/types.d.ts +23 -0
- package/components/routes/cases/detail/utils.d.ts +29 -0
- package/components/routes/cases/detail/utils.js +114 -0
- package/components/routes/cases/detail/utils.test.d.ts +1 -0
- package/components/routes/cases/detail/utils.test.js +144 -0
- package/locales/en/translation.json +33 -13
- package/locales/fr/translation.json +33 -13
- package/models/entities/generated/Rule.d.ts +3 -1
- package/package.json +1 -1
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { createMockCase, createMockEvent, createMockHit } from '@cccsaurora/howler-ui/tests/utils';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { buildObservableEntries, classifyRole } from './utils';
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Pure logic tests — no React needed
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
describe('buildObservableEntries', () => {
|
|
8
|
+
it('returns an empty array for records with no related field', () => {
|
|
9
|
+
expect(buildObservableEntries([createMockHit({ howler: { id: 'h1' } })])).toEqual([]);
|
|
10
|
+
});
|
|
11
|
+
it('extracts a single IP from a hit', () => {
|
|
12
|
+
const result = buildObservableEntries([createMockHit({ howler: { id: 'h1' }, related: { ip: ['1.2.3.4'] } })]);
|
|
13
|
+
expect(result).toHaveLength(1);
|
|
14
|
+
expect(result[0]).toEqual({ type: 'ip', value: '1.2.3.4', seenIn: ['h1'] });
|
|
15
|
+
});
|
|
16
|
+
it('extracts multiple fields from a single record', () => {
|
|
17
|
+
const result = buildObservableEntries([
|
|
18
|
+
createMockHit({ howler: { id: 'h1' }, related: { ip: ['1.2.3.4'], user: ['alice'] } })
|
|
19
|
+
]);
|
|
20
|
+
const types = result.map(a => a.type).sort();
|
|
21
|
+
expect(types).toEqual(['ip', 'user']);
|
|
22
|
+
});
|
|
23
|
+
it('deduplicates the same observable value across multiple records', () => {
|
|
24
|
+
const result = buildObservableEntries([
|
|
25
|
+
createMockHit({ howler: { id: 'h1' }, related: { ip: ['1.2.3.4'] } }),
|
|
26
|
+
createMockEvent({ howler: { id: 'obs1' }, related: { ip: ['1.2.3.4'] } })
|
|
27
|
+
]);
|
|
28
|
+
expect(result).toHaveLength(1);
|
|
29
|
+
expect(result[0].seenIn).toEqual(['h1', 'obs1']);
|
|
30
|
+
});
|
|
31
|
+
it('keeps distinct observable values as separate entries', () => {
|
|
32
|
+
const result = buildObservableEntries([
|
|
33
|
+
createMockHit({ howler: { id: 'h1' }, related: { ip: ['1.2.3.4'] } }),
|
|
34
|
+
createMockHit({ howler: { id: 'h2' }, related: { ip: ['5.6.7.8'] } })
|
|
35
|
+
]);
|
|
36
|
+
expect(result).toHaveLength(2);
|
|
37
|
+
});
|
|
38
|
+
it('does not duplicate seenIn ids when the same record appears twice for the same observable', () => {
|
|
39
|
+
const result = buildObservableEntries([
|
|
40
|
+
createMockHit({ howler: { id: 'h1' }, related: { ip: ['1.2.3.4'] } }),
|
|
41
|
+
createMockHit({ howler: { id: 'h1' }, related: { ip: ['1.2.3.4'] } })
|
|
42
|
+
]);
|
|
43
|
+
expect(result[0].seenIn).toEqual(['h1']);
|
|
44
|
+
});
|
|
45
|
+
it('skips records with no howler.id', () => {
|
|
46
|
+
const noId = { related: { ip: ['1.2.3.4'] } };
|
|
47
|
+
expect(buildObservableEntries([noId])).toEqual([]);
|
|
48
|
+
});
|
|
49
|
+
it('handles the scalar `id` field on Related', () => {
|
|
50
|
+
const result = buildObservableEntries([createMockHit({ howler: { id: 'h1' }, related: { id: 'some-id' } })]);
|
|
51
|
+
expect(result).toHaveLength(1);
|
|
52
|
+
expect(result[0]).toEqual({ type: 'id', value: 'some-id', seenIn: ['h1'] });
|
|
53
|
+
});
|
|
54
|
+
it('handles array fields like hash, hosts, user, ids, uri, signature', () => {
|
|
55
|
+
const related = {
|
|
56
|
+
hash: ['abc123'],
|
|
57
|
+
hosts: ['host.example.com'],
|
|
58
|
+
user: ['bob'],
|
|
59
|
+
ids: ['guid-1'],
|
|
60
|
+
uri: ['https://example.com'],
|
|
61
|
+
signature: ['rule-X']
|
|
62
|
+
};
|
|
63
|
+
const result = buildObservableEntries([createMockHit({ howler: { id: 'h1' }, related })]);
|
|
64
|
+
const types = result.map(a => a.type).sort();
|
|
65
|
+
expect(types).toEqual(['hash', 'hosts', 'ids', 'signature', 'uri', 'user']);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// classifyRole tests
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
describe('classifyRole', () => {
|
|
72
|
+
const emptyCase = createMockCase();
|
|
73
|
+
it('returns "threat" when value is in case.threats', () => {
|
|
74
|
+
const _case = createMockCase({ threats: ['malicious.exe'] });
|
|
75
|
+
const records = [createMockHit({ howler: { id: 'h1' } })];
|
|
76
|
+
expect(classifyRole('malicious.exe', _case, records)).toBe('threat');
|
|
77
|
+
});
|
|
78
|
+
it('returns "target" when value is in case.targets', () => {
|
|
79
|
+
const _case = createMockCase({ targets: ['server-01'] });
|
|
80
|
+
const records = [createMockHit({ howler: { id: 'h1' } })];
|
|
81
|
+
expect(classifyRole('server-01', _case, records)).toBe('target');
|
|
82
|
+
});
|
|
83
|
+
it('returns "indicator" when value is in case.indicators', () => {
|
|
84
|
+
const _case = createMockCase({ indicators: ['1.2.3.4'] });
|
|
85
|
+
const records = [createMockHit({ howler: { id: 'h1' } })];
|
|
86
|
+
expect(classifyRole('1.2.3.4', _case, records)).toBe('indicator');
|
|
87
|
+
});
|
|
88
|
+
it('returns "threat" when value matches howler.outline.threat', () => {
|
|
89
|
+
const records = [createMockHit({ howler: { id: 'h1', outline: { threat: 'malicious.exe' } } })];
|
|
90
|
+
expect(classifyRole('malicious.exe', emptyCase, records)).toBe('threat');
|
|
91
|
+
});
|
|
92
|
+
it('returns "threat" with case-insensitive matching', () => {
|
|
93
|
+
const records = [createMockHit({ howler: { id: 'h1', outline: { threat: 'Malicious.EXE' } } })];
|
|
94
|
+
expect(classifyRole('malicious.exe', emptyCase, records)).toBe('threat');
|
|
95
|
+
});
|
|
96
|
+
it('does not match threat by substring — only exact match', () => {
|
|
97
|
+
const records = [createMockHit({ howler: { id: 'h1', outline: { threat: 'evil.com dropped payload' } } })];
|
|
98
|
+
// No substring match, defaults to indicator
|
|
99
|
+
expect(classifyRole('evil.com', emptyCase, records)).toBe('indicator');
|
|
100
|
+
});
|
|
101
|
+
it('classifies as indicator when value is in outline.indicators but not in threat/target', () => {
|
|
102
|
+
const records = [
|
|
103
|
+
createMockHit({
|
|
104
|
+
howler: { id: 'h1', outline: { threat: 'actor-x', target: 'server-01', indicators: ['1.2.3.4'] } }
|
|
105
|
+
})
|
|
106
|
+
];
|
|
107
|
+
expect(classifyRole('1.2.3.4', emptyCase, records)).toBe('indicator');
|
|
108
|
+
});
|
|
109
|
+
it('returns "target" when value matches howler.outline.target', () => {
|
|
110
|
+
const records = [createMockHit({ howler: { id: 'h1', outline: { target: 'server-01' } } })];
|
|
111
|
+
expect(classifyRole('server-01', emptyCase, records)).toBe('target');
|
|
112
|
+
});
|
|
113
|
+
it('returns "indicator" when value matches threat.indicator.ip', () => {
|
|
114
|
+
const records = [
|
|
115
|
+
createMockHit({
|
|
116
|
+
howler: { id: 'h1' },
|
|
117
|
+
threat: { indicator: { ip: '10.0.0.1' } }
|
|
118
|
+
})
|
|
119
|
+
];
|
|
120
|
+
expect(classifyRole('10.0.0.1', emptyCase, records)).toBe('indicator');
|
|
121
|
+
});
|
|
122
|
+
it('returns "indicator" when value matches threat.indicator.description', () => {
|
|
123
|
+
const records = [
|
|
124
|
+
createMockHit({
|
|
125
|
+
howler: { id: 'h1' },
|
|
126
|
+
threat: { indicator: { description: 'suspicious-hash-abc123' } }
|
|
127
|
+
})
|
|
128
|
+
];
|
|
129
|
+
expect(classifyRole('suspicious-hash-abc123', emptyCase, records)).toBe('indicator');
|
|
130
|
+
});
|
|
131
|
+
it('defaults to "indicator" when value has no specific classification', () => {
|
|
132
|
+
const records = [createMockHit({ howler: { id: 'h1' }, related: { ip: ['5.6.7.8'] } })];
|
|
133
|
+
expect(classifyRole('5.6.7.8', emptyCase, records)).toBe('indicator');
|
|
134
|
+
});
|
|
135
|
+
it('case-level threats take priority over per-record outline.target', () => {
|
|
136
|
+
const _case = createMockCase({ threats: ['dual-use.exe'] });
|
|
137
|
+
const records = [createMockHit({ howler: { id: 'h1', outline: { target: 'dual-use.exe' } } })];
|
|
138
|
+
expect(classifyRole('dual-use.exe', _case, records)).toBe('threat');
|
|
139
|
+
});
|
|
140
|
+
it('trims whitespace before comparing', () => {
|
|
141
|
+
const records = [createMockHit({ howler: { id: 'h1', outline: { threat: ' evil.com ' } } })];
|
|
142
|
+
expect(classifyRole('evil.com', emptyCase, records)).toBe('threat');
|
|
143
|
+
});
|
|
144
|
+
});
|
|
@@ -391,18 +391,6 @@
|
|
|
391
391
|
"page.403.title": "403: Access Forbidden",
|
|
392
392
|
"page.404.description": "The page you are looking for cannot be found...",
|
|
393
393
|
"page.404.title": "404: Not found",
|
|
394
|
-
"page.cases.observables": "Observables",
|
|
395
|
-
"page.cases.observables.empty": "No observables found.",
|
|
396
|
-
"page.cases.observables.filter_by_type": "Filter by type:",
|
|
397
|
-
"page.cases.observables.seen_in": "Seen in",
|
|
398
|
-
"page.cases.observables.type.hash": "Hash",
|
|
399
|
-
"page.cases.observables.type.hosts": "Host",
|
|
400
|
-
"page.cases.observables.type.id": "ID",
|
|
401
|
-
"page.cases.observables.type.ids": "ID",
|
|
402
|
-
"page.cases.observables.type.ip": "IP",
|
|
403
|
-
"page.cases.observables.type.signature": "Signature",
|
|
404
|
-
"page.cases.observables.type.uri": "URI",
|
|
405
|
-
"page.cases.observables.type.user": "User",
|
|
406
394
|
"page.cases.created": "Created",
|
|
407
395
|
"page.cases.dashboard": "Dashboard",
|
|
408
396
|
"page.cases.dashboard.alerts": "Alerts",
|
|
@@ -418,13 +406,40 @@
|
|
|
418
406
|
"page.cases.dashboard.tasks.hide_child": "Hide child case tasks",
|
|
419
407
|
"page.cases.dashboard.tasks.show_child": "Show child case tasks",
|
|
420
408
|
"page.cases.dashboard.threat": "Threats",
|
|
409
|
+
"page.cases.detail.escalation": "Escalation",
|
|
421
410
|
"page.cases.detail.participants": "Participants",
|
|
422
411
|
"page.cases.detail.properties": "Properties",
|
|
423
412
|
"page.cases.detail.status": "Status",
|
|
424
|
-
"page.cases.detail.escalation": "Escalation",
|
|
425
413
|
"page.cases.detail.viewers": "Active Viewers",
|
|
426
414
|
"page.cases.escalation": "Escalation",
|
|
427
415
|
"page.cases.folder.drop.root": "Place here to move to root",
|
|
416
|
+
"page.cases.observables": "Observables",
|
|
417
|
+
"page.cases.observables.columns.escalation": "Escalation",
|
|
418
|
+
"page.cases.observables.columns.role": "Role",
|
|
419
|
+
"page.cases.observables.columns.seen_in": "Seen In",
|
|
420
|
+
"page.cases.observables.columns.sources": "Sources",
|
|
421
|
+
"page.cases.observables.columns.type": "Type",
|
|
422
|
+
"page.cases.observables.columns.value": "Value",
|
|
423
|
+
"page.cases.observables.empty": "No observables found.",
|
|
424
|
+
"page.cases.observables.filter_by_escalation": "Escalation",
|
|
425
|
+
"page.cases.observables.filter_by_origin": "Origin",
|
|
426
|
+
"page.cases.observables.filter_by_role": "Role",
|
|
427
|
+
"page.cases.observables.filter_by_type": "Filter by type:",
|
|
428
|
+
"page.cases.observables.origin.event": "Event",
|
|
429
|
+
"page.cases.observables.origin.hit": "Hit",
|
|
430
|
+
"page.cases.observables.role.indicator": "Indicator",
|
|
431
|
+
"page.cases.observables.role.target": "Target",
|
|
432
|
+
"page.cases.observables.role.threat": "Threat",
|
|
433
|
+
"page.cases.observables.search": "Search observables...",
|
|
434
|
+
"page.cases.observables.seen_in": "Seen in",
|
|
435
|
+
"page.cases.observables.type.hash": "Hash",
|
|
436
|
+
"page.cases.observables.type.hosts": "Host",
|
|
437
|
+
"page.cases.observables.type.id": "ID",
|
|
438
|
+
"page.cases.observables.type.ids": "ID",
|
|
439
|
+
"page.cases.observables.type.ip": "IP",
|
|
440
|
+
"page.cases.observables.type.signature": "Signature",
|
|
441
|
+
"page.cases.observables.type.uri": "URI",
|
|
442
|
+
"page.cases.observables.type.user": "User",
|
|
428
443
|
"page.cases.rules": "Rules",
|
|
429
444
|
"page.cases.rules.author": "Author",
|
|
430
445
|
"page.cases.rules.create": "Create Rule",
|
|
@@ -432,9 +447,14 @@
|
|
|
432
447
|
"page.cases.rules.destination": "Alert Destination Path",
|
|
433
448
|
"page.cases.rules.destination.help": "Handlebars template for the case item path (e.g. alerts/{{howler.analytic}})",
|
|
434
449
|
"page.cases.rules.empty": "No correlation rules configured.",
|
|
450
|
+
"page.cases.rules.expire_after_resolved": "Start expiry after case is resolved",
|
|
451
|
+
"page.cases.rules.indexes": "Indexes",
|
|
435
452
|
"page.cases.rules.no_expiry": "No expiry",
|
|
436
453
|
"page.cases.rules.query": "Match Query",
|
|
437
454
|
"page.cases.rules.timeframe": "Rule Expiry",
|
|
455
|
+
"page.cases.rules.timeframe.after_resolved": "{{days}} day(s) after resolved",
|
|
456
|
+
"page.cases.rules.timeframe.days": "{{days}} day(s)",
|
|
457
|
+
"page.cases.rules.timeframe.help": "Number of days before the rule expires",
|
|
438
458
|
"page.cases.search": "Search",
|
|
439
459
|
"page.cases.sidebar.folder.remove": "Remove folder",
|
|
440
460
|
"page.cases.sidebar.item.open": "Open item",
|
|
@@ -391,18 +391,6 @@
|
|
|
391
391
|
"page.403.title": "403: Accès interdit",
|
|
392
392
|
"page.404.description": "La page que vous recherchez est introuvable ...",
|
|
393
393
|
"page.404.title": "404: Introuvable",
|
|
394
|
-
"page.cases.observables": "Observables",
|
|
395
|
-
"page.cases.observables.empty": "Aucun observable trouvé.",
|
|
396
|
-
"page.cases.observables.filter_by_type": "Filtrer par type :",
|
|
397
|
-
"page.cases.observables.seen_in": "Vu dans",
|
|
398
|
-
"page.cases.observables.type.hash": "Hachage",
|
|
399
|
-
"page.cases.observables.type.hosts": "Hôte",
|
|
400
|
-
"page.cases.observables.type.id": "Identifiant",
|
|
401
|
-
"page.cases.observables.type.ids": "Identifiant",
|
|
402
|
-
"page.cases.observables.type.ip": "IP",
|
|
403
|
-
"page.cases.observables.type.signature": "Signature",
|
|
404
|
-
"page.cases.observables.type.uri": "URI",
|
|
405
|
-
"page.cases.observables.type.user": "Utilisateur",
|
|
406
394
|
"page.cases.created": "Créé",
|
|
407
395
|
"page.cases.dashboard": "Tableau de bord",
|
|
408
396
|
"page.cases.dashboard.alerts": "Alertes",
|
|
@@ -418,13 +406,40 @@
|
|
|
418
406
|
"page.cases.dashboard.tasks.hide_child": "Masquer les tâches des cas enfants",
|
|
419
407
|
"page.cases.dashboard.tasks.show_child": "Afficher les tâches des cas enfants",
|
|
420
408
|
"page.cases.dashboard.threat": "Menaces",
|
|
409
|
+
"page.cases.detail.escalation": "Escalade",
|
|
421
410
|
"page.cases.detail.participants": "Participants",
|
|
422
411
|
"page.cases.detail.properties": "Propriétés",
|
|
423
412
|
"page.cases.detail.status": "Statut",
|
|
424
|
-
"page.cases.detail.escalation": "Escalade",
|
|
425
413
|
"page.cases.detail.viewers": "Spectateurs actifs",
|
|
426
414
|
"page.cases.escalation": "Escalade",
|
|
427
415
|
"page.cases.folder.drop.root": "Déposer ici pour déplacer à la racine",
|
|
416
|
+
"page.cases.observables": "Observables",
|
|
417
|
+
"page.cases.observables.columns.escalation": "Escalade",
|
|
418
|
+
"page.cases.observables.columns.role": "Rôle",
|
|
419
|
+
"page.cases.observables.columns.seen_in": "Vu dans",
|
|
420
|
+
"page.cases.observables.columns.sources": "Sources",
|
|
421
|
+
"page.cases.observables.columns.type": "Type",
|
|
422
|
+
"page.cases.observables.columns.value": "Valeur",
|
|
423
|
+
"page.cases.observables.empty": "Aucun observable trouvé.",
|
|
424
|
+
"page.cases.observables.filter_by_escalation": "Escalade",
|
|
425
|
+
"page.cases.observables.filter_by_origin": "Origine",
|
|
426
|
+
"page.cases.observables.filter_by_role": "Rôle",
|
|
427
|
+
"page.cases.observables.filter_by_type": "Filtrer par type :",
|
|
428
|
+
"page.cases.observables.origin.event": "Événement",
|
|
429
|
+
"page.cases.observables.origin.hit": "Alerte",
|
|
430
|
+
"page.cases.observables.role.indicator": "Indicateur",
|
|
431
|
+
"page.cases.observables.role.target": "Cible",
|
|
432
|
+
"page.cases.observables.role.threat": "Menace",
|
|
433
|
+
"page.cases.observables.search": "Rechercher des observables...",
|
|
434
|
+
"page.cases.observables.seen_in": "Vu dans",
|
|
435
|
+
"page.cases.observables.type.hash": "Hachage",
|
|
436
|
+
"page.cases.observables.type.hosts": "Hôte",
|
|
437
|
+
"page.cases.observables.type.id": "Identifiant",
|
|
438
|
+
"page.cases.observables.type.ids": "Identifiant",
|
|
439
|
+
"page.cases.observables.type.ip": "IP",
|
|
440
|
+
"page.cases.observables.type.signature": "Signature",
|
|
441
|
+
"page.cases.observables.type.uri": "URI",
|
|
442
|
+
"page.cases.observables.type.user": "Utilisateur",
|
|
428
443
|
"page.cases.rules": "Règles",
|
|
429
444
|
"page.cases.rules.author": "Auteur",
|
|
430
445
|
"page.cases.rules.create": "Créer une règle",
|
|
@@ -432,9 +447,14 @@
|
|
|
432
447
|
"page.cases.rules.destination": "Chemin de destination des alertes",
|
|
433
448
|
"page.cases.rules.destination.help": "Modèle Handlebars pour le chemin de l'élément de cas (ex. alerts/{{howler.analytic}})",
|
|
434
449
|
"page.cases.rules.empty": "Aucune règle de corrélation configurée.",
|
|
450
|
+
"page.cases.rules.expire_after_resolved": "Démarrer l'expiration après la résolution du cas",
|
|
451
|
+
"page.cases.rules.indexes": "Index",
|
|
435
452
|
"page.cases.rules.no_expiry": "Sans expiration",
|
|
436
453
|
"page.cases.rules.query": "Requête de correspondance",
|
|
437
454
|
"page.cases.rules.timeframe": "Expiration de la règle",
|
|
455
|
+
"page.cases.rules.timeframe.after_resolved": "{{days}} jour(s) après résolution",
|
|
456
|
+
"page.cases.rules.timeframe.days": "{{days}} jour(s)",
|
|
457
|
+
"page.cases.rules.timeframe.help": "Nombre de jours avant l'expiration de la règle",
|
|
438
458
|
"page.cases.search": "Rechercher",
|
|
439
459
|
"page.cases.sidebar.folder.remove": "Supprimer le dossier",
|
|
440
460
|
"page.cases.sidebar.item.open": "Ouvrir l'élément",
|