@opena2a/oasb 0.1.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/LICENSE +98 -0
- package/README.md +287 -0
- package/config/arp-lab-default.yaml +54 -0
- package/config/dvaa-targets.ts +97 -0
- package/dist/harness/arp-wrapper.d.ts +28 -0
- package/dist/harness/arp-wrapper.js +133 -0
- package/dist/harness/dvaa-client.d.ts +45 -0
- package/dist/harness/dvaa-client.js +97 -0
- package/dist/harness/dvaa-manager.d.ts +16 -0
- package/dist/harness/dvaa-manager.js +131 -0
- package/dist/harness/event-collector.d.ts +32 -0
- package/dist/harness/event-collector.js +85 -0
- package/dist/harness/metrics.d.ts +13 -0
- package/dist/harness/metrics.js +55 -0
- package/dist/harness/mock-llm-adapter.d.ts +33 -0
- package/dist/harness/mock-llm-adapter.js +68 -0
- package/dist/harness/types.d.ts +73 -0
- package/dist/harness/types.js +2 -0
- package/package.json +39 -0
- package/src/atomic/enforcement/AT-ENF-001.log-action.test.ts +89 -0
- package/src/atomic/enforcement/AT-ENF-002.alert-callback.test.ts +120 -0
- package/src/atomic/enforcement/AT-ENF-003.pause-sigstop.test.ts +104 -0
- package/src/atomic/enforcement/AT-ENF-004.kill-sigterm.test.ts +153 -0
- package/src/atomic/enforcement/AT-ENF-005.resume-sigcont.test.ts +164 -0
- package/src/atomic/filesystem/AT-FS-001.sensitive-path.test.ts +118 -0
- package/src/atomic/filesystem/AT-FS-002.outside-allowed.test.ts +122 -0
- package/src/atomic/filesystem/AT-FS-003.credential-file.test.ts +115 -0
- package/src/atomic/filesystem/AT-FS-004.mass-file-creation.test.ts +137 -0
- package/src/atomic/filesystem/AT-FS-005.dotfile-write.test.ts +154 -0
- package/src/atomic/intelligence/AT-INT-001.l0-rule-match.test.ts +107 -0
- package/src/atomic/intelligence/AT-INT-002.l1-anomaly-score.test.ts +94 -0
- package/src/atomic/intelligence/AT-INT-003.l2-escalation.test.ts +124 -0
- package/src/atomic/intelligence/AT-INT-004.budget-exhaustion.test.ts +108 -0
- package/src/atomic/intelligence/AT-INT-005.baseline-learning.test.ts +121 -0
- package/src/atomic/network/AT-NET-001.new-outbound.test.ts +103 -0
- package/src/atomic/network/AT-NET-002.suspicious-host.test.ts +82 -0
- package/src/atomic/network/AT-NET-003.connection-burst.test.ts +91 -0
- package/src/atomic/network/AT-NET-004.allowed-host-bypass.test.ts +129 -0
- package/src/atomic/network/AT-NET-005.exfil-destination.test.ts +117 -0
- package/src/atomic/process/AT-PROC-001.spawn-child.test.ts +148 -0
- package/src/atomic/process/AT-PROC-002.suspicious-binary.test.ts +123 -0
- package/src/atomic/process/AT-PROC-003.high-cpu.test.ts +120 -0
- package/src/atomic/process/AT-PROC-004.privilege-escalation.test.ts +114 -0
- package/src/atomic/process/AT-PROC-005.process-terminated.test.ts +150 -0
- package/src/baseline/BL-001.normal-agent-profile.test.ts +140 -0
- package/src/baseline/BL-002.anomaly-injection.test.ts +134 -0
- package/src/baseline/BL-003.baseline-persistence.test.ts +130 -0
- package/src/e2e/E2E-001.live-filesystem-detection.test.ts +129 -0
- package/src/e2e/E2E-002.live-process-detection.test.ts +106 -0
- package/src/e2e/E2E-003.live-network-detection.test.ts +114 -0
- package/src/e2e/E2E-004.interceptor-process.test.ts +125 -0
- package/src/e2e/E2E-005.interceptor-network.test.ts +134 -0
- package/src/e2e/E2E-006.interceptor-filesystem.test.ts +140 -0
- package/src/harness/arp-wrapper.ts +121 -0
- package/src/harness/dvaa-client.ts +130 -0
- package/src/harness/dvaa-manager.ts +106 -0
- package/src/harness/event-collector.ts +100 -0
- package/src/harness/metrics.ts +64 -0
- package/src/harness/mock-llm-adapter.ts +90 -0
- package/src/harness/types.ts +77 -0
- package/src/integration/INT-001.data-exfil-detection.test.ts +228 -0
- package/src/integration/INT-002.mcp-tool-abuse.test.ts +236 -0
- package/src/integration/INT-003.prompt-injection-response.test.ts +238 -0
- package/src/integration/INT-004.a2a-trust-exploitation.test.ts +280 -0
- package/src/integration/INT-005.baseline-then-attack.test.ts +239 -0
- package/src/integration/INT-006.multi-monitor-correlation.test.ts +265 -0
- package/src/integration/INT-007.budget-exhaustion-attack.test.ts +249 -0
- package/src/integration/INT-008.kill-switch-recovery.test.ts +314 -0
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
// INT-008: Kill Switch and Recovery
|
|
2
|
+
// ATLAS: AML.TA0006 (ML Attack Lifecycle)
|
|
3
|
+
// OWASP: A04 (Excessive Agency)
|
|
4
|
+
// Scenario: Critical threat triggers kill, verify process stops, then recovery
|
|
5
|
+
//
|
|
6
|
+
// This test spawns a real child process, uses ARP's enforcement engine
|
|
7
|
+
// to kill it, and verifies:
|
|
8
|
+
// 1. The kill action succeeds and the process terminates
|
|
9
|
+
// 2. ARP itself remains running and can process new events after the kill
|
|
10
|
+
// 3. The enforcement result contains correct metadata
|
|
11
|
+
|
|
12
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
13
|
+
import { spawn, type ChildProcess } from 'child_process';
|
|
14
|
+
import { ArpWrapper } from '../harness/arp-wrapper';
|
|
15
|
+
import type { ARPEvent, AlertRule } from '@opena2a/arp';
|
|
16
|
+
|
|
17
|
+
describe('INT-008: Kill Switch and Recovery', () => {
|
|
18
|
+
let arp: ArpWrapper;
|
|
19
|
+
let childProcess: ChildProcess | null = null;
|
|
20
|
+
|
|
21
|
+
const killRules: AlertRule[] = [
|
|
22
|
+
{
|
|
23
|
+
name: 'critical-threat-kill',
|
|
24
|
+
condition: { category: 'threat', minSeverity: 'critical' },
|
|
25
|
+
action: 'kill',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: 'high-violation-alert',
|
|
29
|
+
condition: { category: 'violation', minSeverity: 'high' },
|
|
30
|
+
action: 'alert',
|
|
31
|
+
},
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
beforeEach(async () => {
|
|
35
|
+
arp = new ArpWrapper({
|
|
36
|
+
monitors: { process: false, network: false, filesystem: false },
|
|
37
|
+
rules: killRules,
|
|
38
|
+
});
|
|
39
|
+
await arp.start();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
afterEach(async () => {
|
|
43
|
+
// Clean up child process if still running
|
|
44
|
+
if (childProcess && childProcess.pid) {
|
|
45
|
+
try {
|
|
46
|
+
process.kill(childProcess.pid, 0); // Check if alive
|
|
47
|
+
childProcess.kill('SIGKILL');
|
|
48
|
+
} catch {
|
|
49
|
+
// Already dead
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
childProcess = null;
|
|
53
|
+
await arp.stop();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Spawn a simple long-running child process for testing.
|
|
58
|
+
* Uses 'sleep' on unix which blocks indefinitely.
|
|
59
|
+
*/
|
|
60
|
+
function spawnTarget(): ChildProcess {
|
|
61
|
+
const child = spawn('sleep', ['300'], {
|
|
62
|
+
stdio: 'ignore',
|
|
63
|
+
detached: false,
|
|
64
|
+
});
|
|
65
|
+
childProcess = child;
|
|
66
|
+
return child;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Check if a process with the given PID is alive.
|
|
71
|
+
*/
|
|
72
|
+
function isProcessAlive(pid: number): boolean {
|
|
73
|
+
try {
|
|
74
|
+
process.kill(pid, 0);
|
|
75
|
+
return true;
|
|
76
|
+
} catch {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Wait for a process to terminate (with timeout).
|
|
83
|
+
*/
|
|
84
|
+
function waitForProcessExit(pid: number, timeoutMs: number = 5000): Promise<boolean> {
|
|
85
|
+
return new Promise((resolve) => {
|
|
86
|
+
const deadline = Date.now() + timeoutMs;
|
|
87
|
+
const check = () => {
|
|
88
|
+
if (!isProcessAlive(pid)) {
|
|
89
|
+
resolve(true);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (Date.now() > deadline) {
|
|
93
|
+
resolve(false);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
setTimeout(check, 100);
|
|
97
|
+
};
|
|
98
|
+
check();
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
it('should kill a real child process via enforcement engine', async () => {
|
|
103
|
+
const child = spawnTarget();
|
|
104
|
+
const pid = child.pid!;
|
|
105
|
+
|
|
106
|
+
// Verify child is alive
|
|
107
|
+
expect(isProcessAlive(pid)).toBe(true);
|
|
108
|
+
|
|
109
|
+
// Create a mock event referencing the child PID
|
|
110
|
+
const mockEvent: ARPEvent = {
|
|
111
|
+
id: 'kill-test-001',
|
|
112
|
+
timestamp: new Date().toISOString(),
|
|
113
|
+
source: 'process',
|
|
114
|
+
category: 'threat',
|
|
115
|
+
severity: 'critical',
|
|
116
|
+
description: 'Critical threat: malicious process detected',
|
|
117
|
+
data: { pid },
|
|
118
|
+
classifiedBy: 'L0-rules',
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// Execute kill action via enforcement engine
|
|
122
|
+
const enforcement = arp.getEnforcement();
|
|
123
|
+
const result = await enforcement.execute('kill', mockEvent, pid);
|
|
124
|
+
|
|
125
|
+
expect(result.action).toBe('kill');
|
|
126
|
+
expect(result.success).toBe(true);
|
|
127
|
+
expect(result.targetPid).toBe(pid);
|
|
128
|
+
expect(result.reason).toContain(`Killed PID ${pid}`);
|
|
129
|
+
|
|
130
|
+
// Wait for process to actually terminate
|
|
131
|
+
const exited = await waitForProcessExit(pid);
|
|
132
|
+
expect(exited).toBe(true);
|
|
133
|
+
|
|
134
|
+
// Process should be dead now
|
|
135
|
+
expect(isProcessAlive(pid)).toBe(false);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should report failure when trying to kill a non-existent PID', async () => {
|
|
139
|
+
// Use a PID that almost certainly does not exist
|
|
140
|
+
const fakePid = 999999;
|
|
141
|
+
|
|
142
|
+
const mockEvent: ARPEvent = {
|
|
143
|
+
id: 'kill-test-002',
|
|
144
|
+
timestamp: new Date().toISOString(),
|
|
145
|
+
source: 'process',
|
|
146
|
+
category: 'threat',
|
|
147
|
+
severity: 'critical',
|
|
148
|
+
description: 'Kill attempt on non-existent process',
|
|
149
|
+
data: { pid: fakePid },
|
|
150
|
+
classifiedBy: 'L0-rules',
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const enforcement = arp.getEnforcement();
|
|
154
|
+
const result = await enforcement.execute('kill', mockEvent, fakePid);
|
|
155
|
+
|
|
156
|
+
expect(result.action).toBe('kill');
|
|
157
|
+
expect(result.success).toBe(false);
|
|
158
|
+
expect(result.targetPid).toBe(fakePid);
|
|
159
|
+
expect(result.reason).toContain('Failed to kill');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should report failure when no PID is provided for kill', async () => {
|
|
163
|
+
const mockEvent: ARPEvent = {
|
|
164
|
+
id: 'kill-test-003',
|
|
165
|
+
timestamp: new Date().toISOString(),
|
|
166
|
+
source: 'process',
|
|
167
|
+
category: 'threat',
|
|
168
|
+
severity: 'critical',
|
|
169
|
+
description: 'Kill attempt without PID',
|
|
170
|
+
data: {},
|
|
171
|
+
classifiedBy: 'L0-rules',
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const enforcement = arp.getEnforcement();
|
|
175
|
+
const result = await enforcement.execute('kill', mockEvent);
|
|
176
|
+
|
|
177
|
+
expect(result.action).toBe('kill');
|
|
178
|
+
expect(result.success).toBe(false);
|
|
179
|
+
expect(result.reason).toContain('No PID');
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('should continue processing events after killing a process', async () => {
|
|
183
|
+
const child = spawnTarget();
|
|
184
|
+
const pid = child.pid!;
|
|
185
|
+
|
|
186
|
+
// Kill the child
|
|
187
|
+
const mockEvent: ARPEvent = {
|
|
188
|
+
id: 'kill-test-004',
|
|
189
|
+
timestamp: new Date().toISOString(),
|
|
190
|
+
source: 'process',
|
|
191
|
+
category: 'threat',
|
|
192
|
+
severity: 'critical',
|
|
193
|
+
description: 'Kill target process',
|
|
194
|
+
data: { pid },
|
|
195
|
+
classifiedBy: 'L0-rules',
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const enforcement = arp.getEnforcement();
|
|
199
|
+
const killResult = await enforcement.execute('kill', mockEvent, pid);
|
|
200
|
+
expect(killResult.success).toBe(true);
|
|
201
|
+
|
|
202
|
+
// Wait for termination
|
|
203
|
+
await waitForProcessExit(pid);
|
|
204
|
+
|
|
205
|
+
// ARP should still be operational — inject new events
|
|
206
|
+
await arp.injectEvent({
|
|
207
|
+
source: 'network',
|
|
208
|
+
category: 'normal',
|
|
209
|
+
severity: 'info',
|
|
210
|
+
description: 'Post-kill normal event: ARP still running',
|
|
211
|
+
data: { phase: 'recovery', eventAfterKill: true },
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
await arp.injectEvent({
|
|
215
|
+
source: 'process',
|
|
216
|
+
category: 'violation',
|
|
217
|
+
severity: 'high',
|
|
218
|
+
description: 'Post-kill violation: new threat detected',
|
|
219
|
+
data: { phase: 'recovery', newThreat: true },
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// Verify events were captured after the kill
|
|
223
|
+
const allEvents = arp.collector.getEvents();
|
|
224
|
+
expect(allEvents.length).toBe(2);
|
|
225
|
+
|
|
226
|
+
const normalEvents = arp.collector.eventsByCategory('normal');
|
|
227
|
+
expect(normalEvents.length).toBe(1);
|
|
228
|
+
expect(normalEvents[0].data.eventAfterKill).toBe(true);
|
|
229
|
+
|
|
230
|
+
const violations = arp.collector.eventsByCategory('violation');
|
|
231
|
+
expect(violations.length).toBe(1);
|
|
232
|
+
expect(violations[0].data.newThreat).toBe(true);
|
|
233
|
+
|
|
234
|
+
// Enforcement still works after kill
|
|
235
|
+
const alertActions = arp.collector.enforcementsByAction('alert');
|
|
236
|
+
expect(alertActions.length).toBe(1);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('should handle kill triggered by L0 rule via event injection', async () => {
|
|
240
|
+
const child = spawnTarget();
|
|
241
|
+
const pid = child.pid!;
|
|
242
|
+
|
|
243
|
+
expect(isProcessAlive(pid)).toBe(true);
|
|
244
|
+
|
|
245
|
+
// Inject a critical threat event with the child PID in data
|
|
246
|
+
// The kill rule should fire, but note: L0 rule enforcement
|
|
247
|
+
// calls execute() which looks for data.pid
|
|
248
|
+
await arp.injectEvent({
|
|
249
|
+
source: 'process',
|
|
250
|
+
category: 'threat',
|
|
251
|
+
severity: 'critical',
|
|
252
|
+
description: 'Critical process threat detected',
|
|
253
|
+
data: { pid },
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// Verify the threat was captured
|
|
257
|
+
const threats = arp.collector.eventsByCategory('threat');
|
|
258
|
+
expect(threats.length).toBe(1);
|
|
259
|
+
|
|
260
|
+
// Verify kill enforcement was triggered by the rule
|
|
261
|
+
const killActions = arp.collector.enforcementsByAction('kill');
|
|
262
|
+
expect(killActions.length).toBe(1);
|
|
263
|
+
expect(killActions[0].reason).toContain('critical-threat-kill');
|
|
264
|
+
|
|
265
|
+
// Wait for the process to terminate (enforcement engine sends SIGTERM)
|
|
266
|
+
const exited = await waitForProcessExit(pid, 8000);
|
|
267
|
+
expect(exited).toBe(true);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('should verify ARP instance itself survives process kill', async () => {
|
|
271
|
+
const child = spawnTarget();
|
|
272
|
+
const pid = child.pid!;
|
|
273
|
+
|
|
274
|
+
// Kill the child via enforcement
|
|
275
|
+
const mockEvent: ARPEvent = {
|
|
276
|
+
id: 'kill-test-006',
|
|
277
|
+
timestamp: new Date().toISOString(),
|
|
278
|
+
source: 'process',
|
|
279
|
+
category: 'threat',
|
|
280
|
+
severity: 'critical',
|
|
281
|
+
description: 'Verify ARP survives kill',
|
|
282
|
+
data: { pid },
|
|
283
|
+
classifiedBy: 'L0-rules',
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const enforcement = arp.getEnforcement();
|
|
287
|
+
await enforcement.execute('kill', mockEvent, pid);
|
|
288
|
+
await waitForProcessExit(pid);
|
|
289
|
+
|
|
290
|
+
// ARP's underlying instance should still be accessible
|
|
291
|
+
const instance = arp.getInstance();
|
|
292
|
+
expect(instance).toBeDefined();
|
|
293
|
+
expect(instance.isRunning()).toBe(true);
|
|
294
|
+
|
|
295
|
+
// Engine should still accept events
|
|
296
|
+
const engine = arp.getEngine();
|
|
297
|
+
const newEvent = await engine.emit({
|
|
298
|
+
source: 'heartbeat',
|
|
299
|
+
category: 'normal',
|
|
300
|
+
severity: 'info',
|
|
301
|
+
description: 'ARP heartbeat after kill — system operational',
|
|
302
|
+
data: { systemCheck: true },
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
expect(newEvent.id).toBeDefined();
|
|
306
|
+
expect(newEvent.timestamp).toBeDefined();
|
|
307
|
+
expect(newEvent.classifiedBy).toBe('L0-rules');
|
|
308
|
+
|
|
309
|
+
// Collector should capture the heartbeat
|
|
310
|
+
const heartbeats = arp.collector.eventsBySource('heartbeat');
|
|
311
|
+
expect(heartbeats.length).toBe(1);
|
|
312
|
+
expect(heartbeats[0].data.systemCheck).toBe(true);
|
|
313
|
+
});
|
|
314
|
+
});
|