@opena2a/oasb 0.2.0 → 0.3.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 +57 -16
- package/dist/harness/adapter.d.ts +187 -0
- package/dist/harness/adapter.js +18 -0
- package/dist/harness/arp-wrapper.d.ts +24 -20
- package/dist/harness/arp-wrapper.js +114 -28
- package/dist/harness/create-adapter.d.ts +16 -0
- package/dist/harness/create-adapter.js +36 -0
- package/dist/harness/event-collector.d.ts +1 -1
- package/dist/harness/llm-guard-wrapper.d.ts +31 -0
- package/dist/harness/llm-guard-wrapper.js +315 -0
- package/dist/harness/mock-llm-adapter.d.ts +2 -2
- package/dist/harness/mock-llm-adapter.js +6 -5
- package/dist/harness/types.d.ts +4 -38
- package/package.json +15 -7
- package/src/atomic/ai-layer/AT-AI-001.prompt-input-scan.test.ts +18 -42
- package/src/atomic/ai-layer/AT-AI-002.prompt-output-scan.test.ts +13 -32
- package/src/atomic/ai-layer/AT-AI-003.mcp-tool-scan.test.ts +18 -42
- package/src/atomic/ai-layer/AT-AI-004.a2a-message-scan.test.ts +14 -36
- package/src/atomic/ai-layer/AT-AI-005.pattern-coverage.test.ts +11 -5
- package/src/atomic/enforcement/AT-ENF-001.log-action.test.ts +4 -4
- package/src/atomic/enforcement/AT-ENF-002.alert-callback.test.ts +5 -5
- package/src/atomic/enforcement/AT-ENF-003.pause-sigstop.test.ts +4 -4
- package/src/atomic/enforcement/AT-ENF-004.kill-sigterm.test.ts +5 -5
- package/src/atomic/enforcement/AT-ENF-005.resume-sigcont.test.ts +4 -4
- package/src/atomic/intelligence/AT-INT-001.l0-rule-match.test.ts +1 -1
- package/src/atomic/intelligence/AT-INT-002.l1-anomaly-score.test.ts +10 -8
- package/src/atomic/intelligence/AT-INT-003.l2-escalation.test.ts +1 -1
- package/src/atomic/intelligence/AT-INT-004.budget-exhaustion.test.ts +8 -6
- package/src/atomic/intelligence/AT-INT-005.baseline-learning.test.ts +9 -9
- package/src/baseline/BL-002.anomaly-injection.test.ts +6 -6
- package/src/baseline/BL-003.baseline-persistence.test.ts +9 -9
- package/src/harness/adapter.ts +222 -0
- package/src/harness/arp-wrapper.ts +150 -42
- package/src/harness/create-adapter.ts +49 -0
- package/src/harness/event-collector.ts +1 -1
- package/src/harness/llm-guard-wrapper.ts +333 -0
- package/src/harness/mock-llm-adapter.ts +7 -6
- package/src/harness/types.ts +31 -39
- package/src/integration/INT-001.data-exfil-detection.test.ts +1 -1
- package/src/integration/INT-002.mcp-tool-abuse.test.ts +1 -1
- package/src/integration/INT-003.prompt-injection-response.test.ts +1 -1
- package/src/integration/INT-004.a2a-trust-exploitation.test.ts +1 -1
- package/src/integration/INT-005.baseline-then-attack.test.ts +1 -1
- package/src/integration/INT-006.multi-monitor-correlation.test.ts +1 -1
- package/src/integration/INT-007.budget-exhaustion-attack.test.ts +8 -8
- package/src/integration/INT-008.kill-switch-recovery.test.ts +6 -6
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// marks the event for LLM review rather than executing immediate enforcement.
|
|
9
9
|
|
|
10
10
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
11
|
-
import type { AlertRule } from '
|
|
11
|
+
import type { AlertRule } from '../../harness/adapter';
|
|
12
12
|
import { ArpWrapper } from '../../harness/arp-wrapper';
|
|
13
13
|
|
|
14
14
|
describe('AT-INT-003: L2 LLM Escalation', () => {
|
|
@@ -10,10 +10,12 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
|
10
10
|
import * as fs from 'fs';
|
|
11
11
|
import * as os from 'os';
|
|
12
12
|
import * as path from 'path';
|
|
13
|
-
import {
|
|
13
|
+
import { createAdapter } from '../../harness/create-adapter';
|
|
14
|
+
import type { BudgetManager } from '../../harness/adapter';
|
|
14
15
|
|
|
15
16
|
describe('AT-INT-004: Budget Exhaustion', () => {
|
|
16
17
|
let dataDir: string;
|
|
18
|
+
const adapter = createAdapter();
|
|
17
19
|
|
|
18
20
|
beforeEach(() => {
|
|
19
21
|
dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'arp-budget-test-'));
|
|
@@ -28,7 +30,7 @@ describe('AT-INT-004: Budget Exhaustion', () => {
|
|
|
28
30
|
});
|
|
29
31
|
|
|
30
32
|
it('should allow spending when budget is available', () => {
|
|
31
|
-
const budget =
|
|
33
|
+
const budget = adapter.createBudgetManager(dataDir, {
|
|
32
34
|
budgetUsd: 0.01,
|
|
33
35
|
maxCallsPerHour: 5,
|
|
34
36
|
});
|
|
@@ -37,7 +39,7 @@ describe('AT-INT-004: Budget Exhaustion', () => {
|
|
|
37
39
|
});
|
|
38
40
|
|
|
39
41
|
it('should deny spending after budget is exhausted', () => {
|
|
40
|
-
const budget =
|
|
42
|
+
const budget = adapter.createBudgetManager(dataDir, {
|
|
41
43
|
budgetUsd: 0.01,
|
|
42
44
|
maxCallsPerHour: 100, // High limit so we hit budget cap first
|
|
43
45
|
});
|
|
@@ -53,7 +55,7 @@ describe('AT-INT-004: Budget Exhaustion', () => {
|
|
|
53
55
|
});
|
|
54
56
|
|
|
55
57
|
it('should deny spending after hourly call limit is reached', () => {
|
|
56
|
-
const budget =
|
|
58
|
+
const budget = adapter.createBudgetManager(dataDir, {
|
|
57
59
|
budgetUsd: 100, // Large budget so we hit call cap first
|
|
58
60
|
maxCallsPerHour: 5,
|
|
59
61
|
});
|
|
@@ -68,7 +70,7 @@ describe('AT-INT-004: Budget Exhaustion', () => {
|
|
|
68
70
|
});
|
|
69
71
|
|
|
70
72
|
it('should report correct totals in getStatus()', () => {
|
|
71
|
-
const budget =
|
|
73
|
+
const budget = adapter.createBudgetManager(dataDir, {
|
|
72
74
|
budgetUsd: 1.0,
|
|
73
75
|
maxCallsPerHour: 20,
|
|
74
76
|
});
|
|
@@ -88,7 +90,7 @@ describe('AT-INT-004: Budget Exhaustion', () => {
|
|
|
88
90
|
});
|
|
89
91
|
|
|
90
92
|
it('should allow spending again after reset', () => {
|
|
91
|
-
const budget =
|
|
93
|
+
const budget = adapter.createBudgetManager(dataDir, {
|
|
92
94
|
budgetUsd: 0.01,
|
|
93
95
|
maxCallsPerHour: 5,
|
|
94
96
|
});
|
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
// so the detector returns to its initial state.
|
|
9
9
|
|
|
10
10
|
import { describe, it, expect } from 'vitest';
|
|
11
|
-
import {
|
|
12
|
-
import type {
|
|
11
|
+
import { createAdapter } from '../../harness/create-adapter';
|
|
12
|
+
import type { SecurityEvent, AnomalyScorer } from '../../harness/adapter';
|
|
13
13
|
|
|
14
|
-
/** Create a minimal
|
|
15
|
-
function makeEvent(source:
|
|
14
|
+
/** Create a minimal SecurityEvent for the given source. */
|
|
15
|
+
function makeEvent(source: SecurityEvent['source']): SecurityEvent {
|
|
16
16
|
return {
|
|
17
17
|
id: crypto.randomUUID(),
|
|
18
18
|
timestamp: new Date().toISOString(),
|
|
@@ -27,7 +27,7 @@ function makeEvent(source: ARPEvent['source']): ARPEvent {
|
|
|
27
27
|
|
|
28
28
|
describe('AT-INT-005: Baseline Learning', () => {
|
|
29
29
|
it('should establish a baseline after recording many events', () => {
|
|
30
|
-
const detector =
|
|
30
|
+
const detector = createAdapter().createAnomalyScorer();
|
|
31
31
|
|
|
32
32
|
// Feed 50 observations to build a solid baseline for the 'network' source
|
|
33
33
|
for (let i = 0; i < 50; i++) {
|
|
@@ -41,7 +41,7 @@ describe('AT-INT-005: Baseline Learning', () => {
|
|
|
41
41
|
});
|
|
42
42
|
|
|
43
43
|
it('should return low anomaly score for events matching the baseline', () => {
|
|
44
|
-
const detector =
|
|
44
|
+
const detector = createAdapter().createAnomalyScorer();
|
|
45
45
|
|
|
46
46
|
// Build baseline with consistent frequency
|
|
47
47
|
for (let i = 0; i < 50; i++) {
|
|
@@ -54,7 +54,7 @@ describe('AT-INT-005: Baseline Learning', () => {
|
|
|
54
54
|
});
|
|
55
55
|
|
|
56
56
|
it('should track baselines independently per source', () => {
|
|
57
|
-
const detector =
|
|
57
|
+
const detector = createAdapter().createAnomalyScorer();
|
|
58
58
|
|
|
59
59
|
// Build baseline for 'network' only
|
|
60
60
|
for (let i = 0; i < 50; i++) {
|
|
@@ -71,7 +71,7 @@ describe('AT-INT-005: Baseline Learning', () => {
|
|
|
71
71
|
});
|
|
72
72
|
|
|
73
73
|
it('should clear all baselines on reset and return score 0', () => {
|
|
74
|
-
const detector =
|
|
74
|
+
const detector = createAdapter().createAnomalyScorer();
|
|
75
75
|
|
|
76
76
|
// Build baselines for two sources
|
|
77
77
|
for (let i = 0; i < 50; i++) {
|
|
@@ -94,7 +94,7 @@ describe('AT-INT-005: Baseline Learning', () => {
|
|
|
94
94
|
});
|
|
95
95
|
|
|
96
96
|
it('should differentiate between sources with different baselines', () => {
|
|
97
|
-
const detector =
|
|
97
|
+
const detector = createAdapter().createAnomalyScorer();
|
|
98
98
|
|
|
99
99
|
// Build baseline for 'network' with many events
|
|
100
100
|
for (let i = 0; i < 50; i++) {
|
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
// the detection threshold.
|
|
8
8
|
|
|
9
9
|
import { describe, it, expect, beforeEach } from 'vitest';
|
|
10
|
-
import {
|
|
11
|
-
import type {
|
|
10
|
+
import { createAdapter } from '../harness/create-adapter';
|
|
11
|
+
import type { SecurityEvent, AnomalyScorer } from '../harness/adapter';
|
|
12
12
|
|
|
13
|
-
/** Helper: create a minimal
|
|
14
|
-
function makeEvent(source: 'process' | 'network' | 'filesystem', index: number):
|
|
13
|
+
/** Helper: create a minimal SecurityEvent for a given source */
|
|
14
|
+
function makeEvent(source: 'process' | 'network' | 'filesystem', index: number): SecurityEvent {
|
|
15
15
|
return {
|
|
16
16
|
id: `bl002-${source}-${index}`,
|
|
17
17
|
timestamp: new Date().toISOString(),
|
|
@@ -25,10 +25,10 @@ function makeEvent(source: 'process' | 'network' | 'filesystem', index: number):
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
describe('BL-002: Controlled Anomaly Injection', () => {
|
|
28
|
-
let detector:
|
|
28
|
+
let detector: AnomalyScorer;
|
|
29
29
|
|
|
30
30
|
beforeEach(() => {
|
|
31
|
-
detector =
|
|
31
|
+
detector = createAdapter().createAnomalyScorer();
|
|
32
32
|
});
|
|
33
33
|
|
|
34
34
|
it('should return z-score 0 before baseline is established', () => {
|
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
// would need to serialize baselines to disk or a database to survive restarts.
|
|
8
8
|
|
|
9
9
|
import { describe, it, expect, beforeEach } from 'vitest';
|
|
10
|
-
import {
|
|
11
|
-
import type {
|
|
10
|
+
import { createAdapter } from '../harness/create-adapter';
|
|
11
|
+
import type { SecurityEvent, AnomalyScorer } from '../harness/adapter';
|
|
12
12
|
|
|
13
|
-
/** Helper: create a minimal
|
|
14
|
-
function makeEvent(source: 'process' | 'network' | 'filesystem', index: number):
|
|
13
|
+
/** Helper: create a minimal SecurityEvent for a given source */
|
|
14
|
+
function makeEvent(source: 'process' | 'network' | 'filesystem', index: number): SecurityEvent {
|
|
15
15
|
return {
|
|
16
16
|
id: `bl003-${source}-${index}`,
|
|
17
17
|
timestamp: new Date().toISOString(),
|
|
@@ -25,10 +25,10 @@ function makeEvent(source: 'process' | 'network' | 'filesystem', index: number):
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
describe('BL-003: Baseline Persistence Across Restarts', () => {
|
|
28
|
-
let detector:
|
|
28
|
+
let detector: AnomalyScorer;
|
|
29
29
|
|
|
30
30
|
beforeEach(() => {
|
|
31
|
-
detector =
|
|
31
|
+
detector = createAdapter().createAnomalyScorer();
|
|
32
32
|
});
|
|
33
33
|
|
|
34
34
|
it('should accumulate baseline data during a session', () => {
|
|
@@ -53,7 +53,7 @@ describe('BL-003: Baseline Persistence Across Restarts', () => {
|
|
|
53
53
|
expect(baselineBefore).not.toBeNull();
|
|
54
54
|
|
|
55
55
|
// Simulate restart: create a new AnomalyDetector instance
|
|
56
|
-
const restartedDetector =
|
|
56
|
+
const restartedDetector = createAdapter().createAnomalyScorer();
|
|
57
57
|
|
|
58
58
|
// KNOWN GAP: baseline is lost after restart
|
|
59
59
|
const baselineAfter = restartedDetector.getBaseline('process');
|
|
@@ -88,7 +88,7 @@ describe('BL-003: Baseline Persistence Across Restarts', () => {
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
// Simulate restart
|
|
91
|
-
const restartedDetector =
|
|
91
|
+
const restartedDetector = createAdapter().createAnomalyScorer();
|
|
92
92
|
|
|
93
93
|
// KNOWN GAP: all baselines lost
|
|
94
94
|
for (const source of sources) {
|
|
@@ -107,7 +107,7 @@ describe('BL-003: Baseline Persistence Across Restarts', () => {
|
|
|
107
107
|
const originalMean = originalBaseline!.mean;
|
|
108
108
|
|
|
109
109
|
// Simulate restart
|
|
110
|
-
const restartedDetector =
|
|
110
|
+
const restartedDetector = createAdapter().createAnomalyScorer();
|
|
111
111
|
|
|
112
112
|
// Feed the same number of events to the restarted detector
|
|
113
113
|
for (let i = 0; i < 50; i++) {
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OASB Security Product Adapter Interface
|
|
3
|
+
*
|
|
4
|
+
* Implement this interface to evaluate your security product against OASB.
|
|
5
|
+
* The reference implementation (ARP adapter) is in arp-wrapper.ts.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* // Vendor implements the adapter for their product:
|
|
9
|
+
* class MyProductAdapter implements SecurityProductAdapter { ... }
|
|
10
|
+
*
|
|
11
|
+
* // OASB tests use the adapter, not your product directly:
|
|
12
|
+
* const adapter = createAdapter(); // returns configured adapter
|
|
13
|
+
* await adapter.start();
|
|
14
|
+
* await adapter.injectEvent({ ... });
|
|
15
|
+
* const threats = adapter.getEventsByCategory('threat');
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
// ─── Core Event Types ───────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
export type EventCategory = 'normal' | 'activity' | 'threat' | 'violation';
|
|
21
|
+
export type EventSeverity = 'info' | 'low' | 'medium' | 'high' | 'critical';
|
|
22
|
+
export type MonitorSource = 'process' | 'network' | 'filesystem' | 'prompt' | 'mcp-protocol' | 'a2a-protocol' | string;
|
|
23
|
+
export type EnforcementAction = 'log' | 'alert' | 'pause' | 'kill' | 'resume';
|
|
24
|
+
|
|
25
|
+
export interface SecurityEvent {
|
|
26
|
+
id?: string;
|
|
27
|
+
timestamp?: string;
|
|
28
|
+
source: MonitorSource;
|
|
29
|
+
category: EventCategory;
|
|
30
|
+
severity: EventSeverity;
|
|
31
|
+
description: string;
|
|
32
|
+
data?: Record<string, unknown>;
|
|
33
|
+
classifiedBy?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface EnforcementResult {
|
|
37
|
+
action: EnforcementAction;
|
|
38
|
+
success: boolean;
|
|
39
|
+
reason: string;
|
|
40
|
+
event: SecurityEvent;
|
|
41
|
+
pid?: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface AlertRule {
|
|
45
|
+
name: string;
|
|
46
|
+
condition: AlertCondition;
|
|
47
|
+
action: EnforcementAction;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface AlertCondition {
|
|
51
|
+
source?: MonitorSource;
|
|
52
|
+
category?: EventCategory;
|
|
53
|
+
minSeverity?: EventSeverity;
|
|
54
|
+
descriptionContains?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ─── Scanner Types ──────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
export interface ScanResult {
|
|
60
|
+
detected: boolean;
|
|
61
|
+
matches: ScanMatch[];
|
|
62
|
+
truncated?: boolean;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface ScanMatch {
|
|
66
|
+
pattern: ThreatPattern;
|
|
67
|
+
matchedText: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface ThreatPattern {
|
|
71
|
+
id: string;
|
|
72
|
+
category: string;
|
|
73
|
+
description: string;
|
|
74
|
+
pattern: RegExp;
|
|
75
|
+
severity: 'medium' | 'high' | 'critical';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ─── Scanner Interfaces ─────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
export interface PromptScanner {
|
|
81
|
+
start(): Promise<void>;
|
|
82
|
+
stop(): Promise<void>;
|
|
83
|
+
scanInput(text: string): ScanResult;
|
|
84
|
+
scanOutput(text: string): ScanResult;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface MCPScanner {
|
|
88
|
+
start(): Promise<void>;
|
|
89
|
+
stop(): Promise<void>;
|
|
90
|
+
scanToolCall(toolName: string, params: Record<string, unknown>): ScanResult;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface A2AScanner {
|
|
94
|
+
start(): Promise<void>;
|
|
95
|
+
stop(): Promise<void>;
|
|
96
|
+
scanMessage(from: string, to: string, content: string): ScanResult;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface PatternScanner {
|
|
100
|
+
scanText(text: string, patterns: readonly ThreatPattern[]): ScanResult;
|
|
101
|
+
getAllPatterns(): readonly ThreatPattern[];
|
|
102
|
+
getPatternSets(): Record<string, readonly ThreatPattern[]>;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ─── Intelligence Interfaces ────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
export interface BudgetStatus {
|
|
108
|
+
spent: number;
|
|
109
|
+
budget: number;
|
|
110
|
+
remaining: number;
|
|
111
|
+
percentUsed: number;
|
|
112
|
+
callsThisHour: number;
|
|
113
|
+
maxCallsPerHour: number;
|
|
114
|
+
totalCalls: number;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface BudgetManager {
|
|
118
|
+
canAfford(estimatedCostUsd: number): boolean;
|
|
119
|
+
record(costUsd: number, tokens: number): void;
|
|
120
|
+
getStatus(): BudgetStatus;
|
|
121
|
+
reset(): void;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface AnomalyScorer {
|
|
125
|
+
score(event: SecurityEvent): number;
|
|
126
|
+
record(event: SecurityEvent): void;
|
|
127
|
+
getBaseline(source: string): { mean: number; stddev: number; count: number } | null;
|
|
128
|
+
reset(): void;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ─── LLM Adapter (for mock testing) ────────────────────────────────
|
|
132
|
+
|
|
133
|
+
export interface LLMAdapter {
|
|
134
|
+
name: string;
|
|
135
|
+
assess(prompt: string): Promise<LLMResponse>;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface LLMResponse {
|
|
139
|
+
content: string;
|
|
140
|
+
usage?: { inputTokens: number; outputTokens: number };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ─── Event Engine Interface ─────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
export interface EventEngine {
|
|
146
|
+
emit(event: Omit<SecurityEvent, 'id' | 'timestamp' | 'classifiedBy'>): SecurityEvent;
|
|
147
|
+
onEvent(handler: (event: SecurityEvent) => void | Promise<void>): void;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ─── Enforcement Interface ──────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
export interface EnforcementEngine {
|
|
153
|
+
execute(action: EnforcementAction, event: SecurityEvent): Promise<EnforcementResult>;
|
|
154
|
+
pause(pid: number): boolean;
|
|
155
|
+
resume(pid: number): boolean;
|
|
156
|
+
kill(pid: number, signal?: string): boolean;
|
|
157
|
+
getPausedPids(): number[];
|
|
158
|
+
setAlertCallback(callback: (event: SecurityEvent, rule: AlertRule) => void): void;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ─── Main Adapter Interface ─────────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
export interface SecurityProductAdapter {
|
|
164
|
+
/** Start the security product */
|
|
165
|
+
start(): Promise<void>;
|
|
166
|
+
/** Stop the security product */
|
|
167
|
+
stop(): Promise<void>;
|
|
168
|
+
|
|
169
|
+
/** Inject a synthetic event for testing */
|
|
170
|
+
injectEvent(event: Omit<SecurityEvent, 'id' | 'timestamp' | 'classifiedBy'>): Promise<SecurityEvent>;
|
|
171
|
+
|
|
172
|
+
/** Wait for an event matching a predicate */
|
|
173
|
+
waitForEvent(predicate: (event: SecurityEvent) => boolean, timeoutMs?: number): Promise<SecurityEvent>;
|
|
174
|
+
|
|
175
|
+
/** Get collected events */
|
|
176
|
+
getEvents(): SecurityEvent[];
|
|
177
|
+
getEventsByCategory(category: EventCategory): SecurityEvent[];
|
|
178
|
+
getEnforcements(): EnforcementResult[];
|
|
179
|
+
getEnforcementsByAction(action: EnforcementAction): EnforcementResult[];
|
|
180
|
+
|
|
181
|
+
/** Reset collected events */
|
|
182
|
+
resetCollector(): void;
|
|
183
|
+
|
|
184
|
+
/** Access sub-components (for tests that need direct access) */
|
|
185
|
+
getEventEngine(): EventEngine;
|
|
186
|
+
getEnforcementEngine(): EnforcementEngine;
|
|
187
|
+
|
|
188
|
+
/** Factory methods for component-level testing */
|
|
189
|
+
createPromptScanner(): PromptScanner;
|
|
190
|
+
createMCPScanner(allowedTools?: string[]): MCPScanner;
|
|
191
|
+
createA2AScanner(trustedAgents?: string[]): A2AScanner;
|
|
192
|
+
createPatternScanner(): PatternScanner;
|
|
193
|
+
createBudgetManager(dataDir: string, config?: { budgetUsd?: number; maxCallsPerHour?: number }): BudgetManager;
|
|
194
|
+
createAnomalyScorer(): AnomalyScorer;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ─── Lab Config ─────────────────────────────────────────────────────
|
|
198
|
+
|
|
199
|
+
export interface LabConfig {
|
|
200
|
+
monitors?: {
|
|
201
|
+
process?: boolean;
|
|
202
|
+
network?: boolean;
|
|
203
|
+
filesystem?: boolean;
|
|
204
|
+
};
|
|
205
|
+
rules?: AlertRule[];
|
|
206
|
+
intelligence?: {
|
|
207
|
+
enabled?: boolean;
|
|
208
|
+
};
|
|
209
|
+
dataDir?: string;
|
|
210
|
+
filesystemWatchPaths?: string[];
|
|
211
|
+
filesystemAllowedPaths?: string[];
|
|
212
|
+
networkAllowedHosts?: string[];
|
|
213
|
+
processIntervalMs?: number;
|
|
214
|
+
networkIntervalMs?: number;
|
|
215
|
+
interceptors?: {
|
|
216
|
+
process?: boolean;
|
|
217
|
+
network?: boolean;
|
|
218
|
+
filesystem?: boolean;
|
|
219
|
+
};
|
|
220
|
+
interceptorNetworkAllowedHosts?: string[];
|
|
221
|
+
interceptorFilesystemAllowedPaths?: string[];
|
|
222
|
+
}
|
|
@@ -1,30 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ARP Adapter — Reference implementation of SecurityProductAdapter
|
|
3
|
+
*
|
|
4
|
+
* Wraps HackMyAgent's ARP (Agent Runtime Protection) for OASB evaluation.
|
|
5
|
+
* Other vendors implement their own adapter against the same interface.
|
|
6
|
+
*
|
|
7
|
+
* Uses lazy require() for arp-guard so the module is only loaded when
|
|
8
|
+
* this adapter is actually selected. Tests that use a different adapter
|
|
9
|
+
* never trigger the arp-guard import.
|
|
10
|
+
*/
|
|
1
11
|
import * as fs from 'fs';
|
|
2
12
|
import * as os from 'os';
|
|
3
13
|
import * as path from 'path';
|
|
4
|
-
import {
|
|
5
|
-
AgentRuntimeProtection,
|
|
6
|
-
EventEngine,
|
|
7
|
-
EnforcementEngine,
|
|
8
|
-
type ARPConfig,
|
|
9
|
-
type ARPEvent,
|
|
10
|
-
} from '@opena2a/arp';
|
|
11
14
|
import { EventCollector } from './event-collector';
|
|
12
|
-
import type {
|
|
15
|
+
import type {
|
|
16
|
+
SecurityProductAdapter,
|
|
17
|
+
SecurityEvent,
|
|
18
|
+
EnforcementResult,
|
|
19
|
+
LabConfig,
|
|
20
|
+
PromptScanner,
|
|
21
|
+
MCPScanner,
|
|
22
|
+
A2AScanner,
|
|
23
|
+
PatternScanner,
|
|
24
|
+
BudgetManager,
|
|
25
|
+
AnomalyScorer,
|
|
26
|
+
EventEngine,
|
|
27
|
+
EnforcementEngine as EnforcementEngineInterface,
|
|
28
|
+
ScanResult,
|
|
29
|
+
ThreatPattern,
|
|
30
|
+
} from './adapter';
|
|
13
31
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
32
|
+
// Lazy-loaded arp-guard module
|
|
33
|
+
let _arp: any;
|
|
34
|
+
function arp(): any {
|
|
35
|
+
if (!_arp) {
|
|
36
|
+
_arp = require('arp-guard');
|
|
37
|
+
}
|
|
38
|
+
return _arp;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export class ArpWrapper implements SecurityProductAdapter {
|
|
42
|
+
private _arpInstance: any;
|
|
21
43
|
private _dataDir: string;
|
|
22
44
|
readonly collector: EventCollector;
|
|
23
45
|
|
|
24
46
|
constructor(labConfig?: LabConfig) {
|
|
25
47
|
this._dataDir = labConfig?.dataDir ?? fs.mkdtempSync(path.join(os.tmpdir(), 'arp-lab-'));
|
|
26
48
|
|
|
27
|
-
const
|
|
49
|
+
const { AgentRuntimeProtection } = arp();
|
|
50
|
+
|
|
51
|
+
const config = {
|
|
28
52
|
agentName: 'arp-lab-target',
|
|
29
53
|
agentDescription: 'Test target for ARP security lab',
|
|
30
54
|
declaredCapabilities: ['file read/write', 'HTTP requests'],
|
|
@@ -63,22 +87,20 @@ export class ArpWrapper {
|
|
|
63
87
|
},
|
|
64
88
|
};
|
|
65
89
|
|
|
66
|
-
this.
|
|
90
|
+
this._arpInstance = new AgentRuntimeProtection(config);
|
|
67
91
|
this.collector = new EventCollector();
|
|
68
92
|
|
|
69
|
-
|
|
70
|
-
this.
|
|
71
|
-
this.arp.onEnforcement(this.collector.enforcementHandler);
|
|
93
|
+
this._arpInstance.onEvent(this.collector.eventHandler);
|
|
94
|
+
this._arpInstance.onEnforcement(this.collector.enforcementHandler);
|
|
72
95
|
}
|
|
73
96
|
|
|
74
97
|
async start(): Promise<void> {
|
|
75
|
-
await this.
|
|
98
|
+
await this._arpInstance.start();
|
|
76
99
|
}
|
|
77
100
|
|
|
78
101
|
async stop(): Promise<void> {
|
|
79
|
-
await this.
|
|
102
|
+
await this._arpInstance.stop();
|
|
80
103
|
this.collector.reset();
|
|
81
|
-
// Clean up temp dir
|
|
82
104
|
try {
|
|
83
105
|
fs.rmSync(this._dataDir, { recursive: true, force: true });
|
|
84
106
|
} catch {
|
|
@@ -86,36 +108,122 @@ export class ArpWrapper {
|
|
|
86
108
|
}
|
|
87
109
|
}
|
|
88
110
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
111
|
+
async injectEvent(event: Omit<SecurityEvent, 'id' | 'timestamp' | 'classifiedBy'>): Promise<SecurityEvent> {
|
|
112
|
+
return this.getEngine().emit(event);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
waitForEvent(predicate: (event: SecurityEvent) => boolean, timeoutMs: number = 10000): Promise<SecurityEvent> {
|
|
116
|
+
return this.collector.waitForEvent(predicate, timeoutMs);
|
|
92
117
|
}
|
|
93
118
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
return this.arp.getEngine();
|
|
119
|
+
getEvents(): SecurityEvent[] {
|
|
120
|
+
return this.collector.getEvents();
|
|
97
121
|
}
|
|
98
122
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
return this.arp.getEnforcement();
|
|
123
|
+
getEventsByCategory(category: string): SecurityEvent[] {
|
|
124
|
+
return this.collector.eventsByCategory(category);
|
|
102
125
|
}
|
|
103
126
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
return this.getEngine().emit(event);
|
|
127
|
+
getEnforcements(): EnforcementResult[] {
|
|
128
|
+
return this.collector.getEnforcements() as EnforcementResult[];
|
|
107
129
|
}
|
|
108
130
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
):
|
|
114
|
-
|
|
131
|
+
getEnforcementsByAction(action: string): EnforcementResult[] {
|
|
132
|
+
return this.collector.enforcementsByAction(action) as EnforcementResult[];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
resetCollector(): void {
|
|
136
|
+
this.collector.reset();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
getInstance(): any {
|
|
140
|
+
return this._arpInstance;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
getEventEngine(): EventEngine {
|
|
144
|
+
return this._arpInstance.getEngine() as unknown as EventEngine;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
getEnforcementEngine(): EnforcementEngineInterface {
|
|
148
|
+
return this._arpInstance.getEnforcement() as unknown as EnforcementEngineInterface;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
getEngine(): any {
|
|
152
|
+
return this._arpInstance.getEngine();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
getEnforcement(): any {
|
|
156
|
+
return this._arpInstance.getEnforcement();
|
|
115
157
|
}
|
|
116
158
|
|
|
117
|
-
/** Get the data directory */
|
|
118
159
|
get dataDir(): string {
|
|
119
160
|
return this._dataDir;
|
|
120
161
|
}
|
|
162
|
+
|
|
163
|
+
// ─── Factory Methods ────────────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
createPromptScanner(): PromptScanner {
|
|
166
|
+
const { EventEngine, PromptInterceptor } = arp();
|
|
167
|
+
const engine = new EventEngine({ agentName: 'oasb-prompt-test' });
|
|
168
|
+
const interceptor = new PromptInterceptor(engine);
|
|
169
|
+
return {
|
|
170
|
+
start: () => interceptor.start(),
|
|
171
|
+
stop: () => interceptor.stop(),
|
|
172
|
+
scanInput: (text: string) => interceptor.scanInput(text),
|
|
173
|
+
scanOutput: (text: string) => interceptor.scanOutput(text),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
createMCPScanner(allowedTools?: string[]): MCPScanner {
|
|
178
|
+
const { EventEngine, MCPProtocolInterceptor } = arp();
|
|
179
|
+
const engine = new EventEngine({ agentName: 'oasb-mcp-test' });
|
|
180
|
+
const interceptor = new MCPProtocolInterceptor(engine, allowedTools);
|
|
181
|
+
return {
|
|
182
|
+
start: () => interceptor.start(),
|
|
183
|
+
stop: () => interceptor.stop(),
|
|
184
|
+
scanToolCall: (toolName: string, params: Record<string, unknown>) => interceptor.scanToolCall(toolName, params),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
createA2AScanner(trustedAgents?: string[]): A2AScanner {
|
|
189
|
+
const { EventEngine, A2AProtocolInterceptor } = arp();
|
|
190
|
+
const engine = new EventEngine({ agentName: 'oasb-a2a-test' });
|
|
191
|
+
const interceptor = new A2AProtocolInterceptor(engine, trustedAgents);
|
|
192
|
+
return {
|
|
193
|
+
start: () => interceptor.start(),
|
|
194
|
+
stop: () => interceptor.stop(),
|
|
195
|
+
scanMessage: (from: string, to: string, content: string) => interceptor.scanMessage(from, to, content),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
createPatternScanner(): PatternScanner {
|
|
200
|
+
const { scanText: _scanText, ALL_PATTERNS: _allPatterns, PATTERN_SETS: _patternSets } = arp();
|
|
201
|
+
return {
|
|
202
|
+
scanText: (text: string, patterns: readonly ThreatPattern[]) => _scanText(text, patterns) as ScanResult,
|
|
203
|
+
getAllPatterns: () => _allPatterns as unknown as readonly ThreatPattern[],
|
|
204
|
+
getPatternSets: () => _patternSets as unknown as Record<string, readonly ThreatPattern[]>,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
createBudgetManager(dataDir: string, config?: { budgetUsd?: number; maxCallsPerHour?: number }): BudgetManager {
|
|
209
|
+
const { BudgetController } = arp();
|
|
210
|
+
const controller = new BudgetController(dataDir, config);
|
|
211
|
+
return {
|
|
212
|
+
canAfford: (cost: number) => controller.canAfford(cost),
|
|
213
|
+
record: (cost: number, tokens: number) => controller.record(cost, tokens),
|
|
214
|
+
getStatus: () => controller.getStatus(),
|
|
215
|
+
reset: () => controller.reset(),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
createAnomalyScorer(): AnomalyScorer {
|
|
220
|
+
const { AnomalyDetector } = arp();
|
|
221
|
+
const detector = new AnomalyDetector();
|
|
222
|
+
return {
|
|
223
|
+
score: (event: SecurityEvent) => detector.score(event),
|
|
224
|
+
record: (event: SecurityEvent) => detector.record(event),
|
|
225
|
+
getBaseline: (source: string) => detector.getBaseline(source),
|
|
226
|
+
reset: () => detector.reset(),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
121
229
|
}
|