@claude-flow/security 3.0.0-alpha.1 → 3.0.0-alpha.10

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.
Files changed (115) hide show
  1. package/README.md +292 -234
  2. package/__tests__/acceptance/security-compliance.test.ts +674 -674
  3. package/__tests__/authorization-propagator.test.ts +155 -0
  4. package/__tests__/credential-generator.test.ts +310 -310
  5. package/__tests__/fixtures/configurations.ts +425 -419
  6. package/__tests__/fixtures/index.ts +21 -21
  7. package/__tests__/helpers/create-mock.ts +469 -469
  8. package/__tests__/helpers/index.ts +32 -32
  9. package/__tests__/input-validator.test.ts +382 -381
  10. package/__tests__/integration/security-flow.test.ts +606 -606
  11. package/__tests__/password-hasher.test.ts +239 -239
  12. package/__tests__/path-validator.test.ts +302 -302
  13. package/__tests__/plugin-integrity-verifier.test.ts +199 -0
  14. package/__tests__/safe-executor.test.ts +292 -292
  15. package/__tests__/token-generator.test.ts +371 -371
  16. package/__tests__/tool-output-guardrail.test.ts +256 -0
  17. package/__tests__/unit/credential-generator.test.ts +182 -182
  18. package/__tests__/unit/password-hasher.test.ts +359 -359
  19. package/__tests__/unit/path-validator.test.ts +509 -509
  20. package/__tests__/unit/safe-executor.test.ts +667 -667
  21. package/__tests__/unit/token-generator.test.ts +310 -310
  22. package/dist/authorization/propagator.d.ts +148 -0
  23. package/dist/authorization/propagator.d.ts.map +1 -0
  24. package/dist/authorization/propagator.js +182 -0
  25. package/dist/authorization/propagator.js.map +1 -0
  26. package/dist/credential-generator.js +9 -9
  27. package/dist/domain/services/security-domain-service.js +1 -1
  28. package/dist/domain/services/security-domain-service.js.map +1 -1
  29. package/dist/index.d.ts +4 -1
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +11 -1
  32. package/dist/index.js.map +1 -1
  33. package/dist/password-hasher.d.ts.map +1 -1
  34. package/dist/password-hasher.js +8 -1
  35. package/dist/password-hasher.js.map +1 -1
  36. package/dist/plugins/integrity-verifier.d.ts +125 -0
  37. package/dist/plugins/integrity-verifier.d.ts.map +1 -0
  38. package/dist/plugins/integrity-verifier.js +186 -0
  39. package/dist/plugins/integrity-verifier.js.map +1 -0
  40. package/dist/safe-executor.d.ts +7 -0
  41. package/dist/safe-executor.d.ts.map +1 -1
  42. package/dist/safe-executor.js +21 -1
  43. package/dist/safe-executor.js.map +1 -1
  44. package/dist/tool-output-guardrail.d.ts +126 -0
  45. package/dist/tool-output-guardrail.d.ts.map +1 -0
  46. package/dist/tool-output-guardrail.js +257 -0
  47. package/dist/tool-output-guardrail.js.map +1 -0
  48. package/package.json +28 -28
  49. package/src/CVE-REMEDIATION.ts +251 -251
  50. package/src/application/index.ts +10 -10
  51. package/src/application/services/security-application-service.ts +193 -193
  52. package/src/authorization/propagator.ts +279 -0
  53. package/src/credential-generator.ts +368 -368
  54. package/src/domain/entities/security-context.ts +173 -173
  55. package/src/domain/index.ts +17 -17
  56. package/src/domain/services/security-domain-service.ts +296 -296
  57. package/src/index.ts +314 -271
  58. package/src/input-validator.ts +466 -466
  59. package/src/password-hasher.ts +277 -270
  60. package/src/path-validator.ts +525 -525
  61. package/src/plugins/integrity-verifier.ts +265 -0
  62. package/src/safe-executor.ts +546 -525
  63. package/src/token-generator.ts +463 -463
  64. package/src/tool-output-guardrail.ts +360 -0
  65. package/tmp.json +0 -0
  66. package/tsconfig.json +9 -9
  67. package/tsconfig.tsbuildinfo +1 -1
  68. package/vitest.config.ts +20 -0
  69. package/.agentic-flow/intelligence.json +0 -16
  70. package/__tests__/coverage/base.css +0 -224
  71. package/__tests__/coverage/block-navigation.js +0 -87
  72. package/__tests__/coverage/coverage-final.json +0 -11
  73. package/__tests__/coverage/favicon.png +0 -0
  74. package/__tests__/coverage/index.html +0 -161
  75. package/__tests__/coverage/lcov-report/base.css +0 -224
  76. package/__tests__/coverage/lcov-report/block-navigation.js +0 -87
  77. package/__tests__/coverage/lcov-report/favicon.png +0 -0
  78. package/__tests__/coverage/lcov-report/index.html +0 -161
  79. package/__tests__/coverage/lcov-report/prettify.css +0 -1
  80. package/__tests__/coverage/lcov-report/prettify.js +0 -2
  81. package/__tests__/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  82. package/__tests__/coverage/lcov-report/sorter.js +0 -210
  83. package/__tests__/coverage/lcov-report/src/CVE-REMEDIATION.ts.html +0 -838
  84. package/__tests__/coverage/lcov-report/src/application/services/index.html +0 -116
  85. package/__tests__/coverage/lcov-report/src/application/services/security-application-service.ts.html +0 -664
  86. package/__tests__/coverage/lcov-report/src/credential-generator.ts.html +0 -1189
  87. package/__tests__/coverage/lcov-report/src/domain/entities/index.html +0 -116
  88. package/__tests__/coverage/lcov-report/src/domain/entities/security-context.ts.html +0 -604
  89. package/__tests__/coverage/lcov-report/src/domain/services/index.html +0 -116
  90. package/__tests__/coverage/lcov-report/src/domain/services/security-domain-service.ts.html +0 -973
  91. package/__tests__/coverage/lcov-report/src/index.html +0 -206
  92. package/__tests__/coverage/lcov-report/src/input-validator.ts.html +0 -1483
  93. package/__tests__/coverage/lcov-report/src/password-hasher.ts.html +0 -895
  94. package/__tests__/coverage/lcov-report/src/path-validator.ts.html +0 -1660
  95. package/__tests__/coverage/lcov-report/src/safe-executor.ts.html +0 -1660
  96. package/__tests__/coverage/lcov-report/src/token-generator.ts.html +0 -1474
  97. package/__tests__/coverage/lcov.info +0 -3650
  98. package/__tests__/coverage/prettify.css +0 -1
  99. package/__tests__/coverage/prettify.js +0 -2
  100. package/__tests__/coverage/sort-arrow-sprite.png +0 -0
  101. package/__tests__/coverage/sorter.js +0 -210
  102. package/__tests__/coverage/src/CVE-REMEDIATION.ts.html +0 -838
  103. package/__tests__/coverage/src/application/services/index.html +0 -116
  104. package/__tests__/coverage/src/application/services/security-application-service.ts.html +0 -664
  105. package/__tests__/coverage/src/credential-generator.ts.html +0 -1189
  106. package/__tests__/coverage/src/domain/entities/index.html +0 -116
  107. package/__tests__/coverage/src/domain/entities/security-context.ts.html +0 -604
  108. package/__tests__/coverage/src/domain/services/index.html +0 -116
  109. package/__tests__/coverage/src/domain/services/security-domain-service.ts.html +0 -973
  110. package/__tests__/coverage/src/index.html +0 -206
  111. package/__tests__/coverage/src/input-validator.ts.html +0 -1483
  112. package/__tests__/coverage/src/password-hasher.ts.html +0 -895
  113. package/__tests__/coverage/src/path-validator.ts.html +0 -1660
  114. package/__tests__/coverage/src/safe-executor.ts.html +0 -1660
  115. package/__tests__/coverage/src/token-generator.ts.html +0 -1474
@@ -0,0 +1,360 @@
1
+ /**
2
+ * ToolOutputGuardrail — semantic screening of content returned by MCP tool
3
+ * calls, memory reads, and external API responses before that content enters
4
+ * agent reasoning. Closes the OWASP ASI01 (Agent Goal Hijacking) gap
5
+ * identified in ruvnet/ruflo#2149 / ADR-131.
6
+ *
7
+ * Threat model
8
+ * ------------
9
+ * Attackers embed malicious instructions in content the agent retrieves
10
+ * autonomously (web page, MCP tool response, memory entry). An LLM cannot
11
+ * reliably distinguish instructions from data once both are in the prompt.
12
+ * System-level per-boundary guardrails are the only category with
13
+ * sub-millisecond latency and no model dependency (arXiv:2601.17548,
14
+ * Jan 2026 systematic review).
15
+ *
16
+ * Scope
17
+ * -----
18
+ * - Detection only — does NOT modify the running prompt; callers decide
19
+ * what action to take (allow, flag, redact, reject) via `scanAndEnforce`.
20
+ * - Synchronous pattern match — designed for <1ms p99 on typical tool
21
+ * responses (≤32KB). Large content is capped at `maxScanBytes` and the
22
+ * truncation itself is reported as a low-severity finding.
23
+ * - Pure-function shape — no I/O, no async, no model calls. Safe to invoke
24
+ * in hot paths (every MCP tool result, every memory read).
25
+ *
26
+ * Non-goals
27
+ * ---------
28
+ * - Not a replacement for input validation at HTTP/CLI boundaries
29
+ * (InputValidator handles that). This is the *content* boundary.
30
+ * - Not a model-based classifier. False-positive rate is bounded by
31
+ * pattern specificity; tune via `customPatterns` and `policy`.
32
+ *
33
+ * Reference: ADR-131, OpenAI Agents SDK ToolGuardrail API (March 2025).
34
+ */
35
+
36
+ export type InjectionSeverity = 'low' | 'medium' | 'high' | 'critical';
37
+
38
+ export type InjectionCategory =
39
+ | 'instruction-override'
40
+ | 'role-hijack'
41
+ | 'exfiltration'
42
+ | 'jailbreak'
43
+ | 'hidden-unicode'
44
+ | 'embedded-system'
45
+ | 'tool-spoofing'
46
+ | 'truncation';
47
+
48
+ export interface InjectionFinding {
49
+ /** Short label for the matched pattern (stable identifier for telemetry). */
50
+ pattern: string;
51
+ /** Risk weight assigned to this pattern. */
52
+ severity: InjectionSeverity;
53
+ /** Category for downstream classification + OWASP mapping. */
54
+ category: InjectionCategory;
55
+ /** Char offset of the first match in the scanned content. */
56
+ position: number;
57
+ /** Up to 80 chars of surrounding context for human triage (redacted in `sanitized`). */
58
+ context: string;
59
+ }
60
+
61
+ export interface GuardrailResult {
62
+ /** True iff no findings, or all findings under the `flag` policy threshold. */
63
+ safe: boolean;
64
+ /** Findings in the order they were detected. */
65
+ findings: InjectionFinding[];
66
+ /** Highest severity observed; `none` if findings is empty. */
67
+ highest: InjectionSeverity | 'none';
68
+ }
69
+
70
+ export type GuardrailAction = 'allow' | 'flag' | 'redact' | 'reject';
71
+
72
+ export interface GuardrailConfig {
73
+ /**
74
+ * Per-severity action.
75
+ * Defaults:
76
+ * low → allow
77
+ * medium → flag
78
+ * high → redact
79
+ * critical → reject
80
+ *
81
+ * `allow` — content passes through unchanged, no logging required.
82
+ * `flag` — content passes through; caller SHOULD log + monitor.
83
+ * `redact` — matched substrings replaced with `[REDACTED:<pattern>]`.
84
+ * `reject` — caller MUST drop the content and treat the tool call
85
+ * as failed (signal the agent that the tool returned an
86
+ * unsafe payload rather than letting the payload through).
87
+ */
88
+ policy?: Partial<Record<InjectionSeverity, GuardrailAction>>;
89
+
90
+ /** Add domain-specific patterns without subclassing. */
91
+ customPatterns?: Array<{
92
+ label: string;
93
+ regex: RegExp;
94
+ severity: InjectionSeverity;
95
+ category: InjectionCategory;
96
+ }>;
97
+
98
+ /**
99
+ * Maximum bytes of content to scan. Beyond this, the tail is ignored and
100
+ * a `truncation` finding is added at `medium`. Default: 1 MiB.
101
+ * Set to 0 to disable truncation (scan everything; slower on huge blobs).
102
+ */
103
+ maxScanBytes?: number;
104
+ }
105
+
106
+ const DEFAULT_POLICY: Required<NonNullable<GuardrailConfig['policy']>> = {
107
+ low: 'allow',
108
+ medium: 'flag',
109
+ high: 'redact',
110
+ critical: 'reject',
111
+ };
112
+
113
+ const DEFAULT_MAX_SCAN_BYTES = 1024 * 1024;
114
+
115
+ /**
116
+ * Built-in pattern library. Ordered by severity (critical first) so the
117
+ * `highest` field reflects the worst category. Patterns are intentionally
118
+ * conservative — they target the explicit instruction-override / role-hijack
119
+ * shapes that show up in published indirect-injection corpora rather than
120
+ * general "suspicious looking" text.
121
+ */
122
+ const BUILTIN_PATTERNS: ReadonlyArray<{
123
+ label: string;
124
+ regex: RegExp;
125
+ severity: InjectionSeverity;
126
+ category: InjectionCategory;
127
+ }> = [
128
+ // ── critical: instruction override + embedded system frames ──
129
+ {
130
+ label: 'ignore-previous-instructions',
131
+ // Allow optional filler words between the verb and the temporal keyword
132
+ // ("ignore the above", "disregard your earlier", "forget all prior").
133
+ regex: /\b(?:ignore|disregard|forget)\s+(?:all\s+|any\s+|the\s+|my\s+|your\s+|these\s+|those\s+)?(?:previous|prior|above|earlier|preceding|aforementioned)\s+(?:instructions?|prompts?|rules?|directives?)\b/gi,
134
+ severity: 'critical',
135
+ category: 'instruction-override',
136
+ },
137
+ {
138
+ label: 'chatml-frame',
139
+ regex: /<\|(?:im_start|im_end|system|assistant|user|endoftext)\|>/gi,
140
+ severity: 'critical',
141
+ category: 'embedded-system',
142
+ },
143
+ {
144
+ label: 'llama-inst-frame',
145
+ regex: /\[\/?INST\]/g,
146
+ severity: 'critical',
147
+ category: 'embedded-system',
148
+ },
149
+ {
150
+ label: 'exfiltrate-secret',
151
+ regex: /\b(?:exfiltrate|leak|send|post|upload|transmit)\b[^.\n]{0,80}\b(?:secret|token|api[-_\s]?keys?|password|credential|env\s+vars?)\b/gi,
152
+ severity: 'critical',
153
+ category: 'exfiltration',
154
+ },
155
+
156
+ // ── high: role hijack + jailbreak + system tags + bidi unicode ──
157
+ {
158
+ label: 'system-tag-injection',
159
+ regex: /<\/?system>/gi,
160
+ severity: 'high',
161
+ category: 'embedded-system',
162
+ },
163
+ {
164
+ label: 'role-hijack-you-are-now',
165
+ regex: /\byou\s+are\s+(?:now|actually|secretly)\s+(?:a|an|the)\s+/gi,
166
+ severity: 'high',
167
+ category: 'role-hijack',
168
+ },
169
+ {
170
+ label: 'jailbreak-dan',
171
+ regex: /\b(?:DAN(?:\s+mode)?|developer\s+mode|jailbreak\s+mode|do\s+anything\s+now)\b/gi,
172
+ severity: 'high',
173
+ category: 'jailbreak',
174
+ },
175
+ {
176
+ label: 'new-instructions-directive',
177
+ regex: /\b(?:new|updated|revised|additional)\s+(?:instructions?|task|directives?|objectives?)\s*[:.]/gi,
178
+ severity: 'high',
179
+ category: 'instruction-override',
180
+ },
181
+ {
182
+ label: 'bidi-override',
183
+ regex: /[‪-‮⁦-⁩]/g,
184
+ severity: 'high',
185
+ category: 'hidden-unicode',
186
+ },
187
+
188
+ // ── medium: softer role manipulation + tool spoofing ──
189
+ {
190
+ label: 'role-hijack-act-as',
191
+ regex: /\b(?:act|behave|pretend|role[-\s]?play)\s+as\s+(?:if\s+)?(?:a|an|the)\s+/gi,
192
+ severity: 'medium',
193
+ category: 'role-hijack',
194
+ },
195
+ {
196
+ label: 'tool-call-spoof',
197
+ regex: /\b(?:tool_call|function_call|mcp_tool)\s*[:=]\s*["'{]/gi,
198
+ severity: 'medium',
199
+ category: 'tool-spoofing',
200
+ },
201
+
202
+ // ── low: zero-width whitespace (common in copy-paste attacks) ──
203
+ {
204
+ label: 'zero-width-char',
205
+ regex: /[​-‍]/g,
206
+ severity: 'low',
207
+ category: 'hidden-unicode',
208
+ },
209
+ ];
210
+
211
+ const SEVERITY_ORDER: Record<InjectionSeverity | 'none', number> = {
212
+ none: 0,
213
+ low: 1,
214
+ medium: 2,
215
+ high: 3,
216
+ critical: 4,
217
+ };
218
+
219
+ function snippet(text: string, idx: number, len: number): string {
220
+ const start = Math.max(0, idx - 24);
221
+ const end = Math.min(text.length, idx + len + 24);
222
+ const head = start > 0 ? '…' : '';
223
+ const tail = end < text.length ? '…' : '';
224
+ return head + text.slice(start, end).replace(/\s+/g, ' ') + tail;
225
+ }
226
+
227
+ function maxSeverity(a: InjectionSeverity | 'none', b: InjectionSeverity): InjectionSeverity {
228
+ return SEVERITY_ORDER[a] >= SEVERITY_ORDER[b]
229
+ ? (a === 'none' ? b : a)
230
+ : b;
231
+ }
232
+
233
+ export class ToolOutputGuardrail {
234
+ private readonly patterns: ReadonlyArray<typeof BUILTIN_PATTERNS[number]>;
235
+ private readonly policy: Required<NonNullable<GuardrailConfig['policy']>>;
236
+ private readonly maxScanBytes: number;
237
+
238
+ constructor(config: GuardrailConfig = {}) {
239
+ this.patterns = [...BUILTIN_PATTERNS, ...(config.customPatterns ?? [])];
240
+ this.policy = { ...DEFAULT_POLICY, ...(config.policy ?? {}) };
241
+ this.maxScanBytes = config.maxScanBytes ?? DEFAULT_MAX_SCAN_BYTES;
242
+ }
243
+
244
+ /**
245
+ * Pure scan — no side effects, no content modification. Useful when the
246
+ * caller wants to log findings but cannot drop the content (e.g. read-only
247
+ * audit mode).
248
+ */
249
+ scan(content: string): GuardrailResult {
250
+ if (typeof content !== 'string') {
251
+ return { safe: true, findings: [], highest: 'none' };
252
+ }
253
+
254
+ let scanned = content;
255
+ const findings: InjectionFinding[] = [];
256
+
257
+ if (this.maxScanBytes > 0 && content.length > this.maxScanBytes) {
258
+ scanned = content.slice(0, this.maxScanBytes);
259
+ findings.push({
260
+ pattern: 'content-truncated',
261
+ severity: 'medium',
262
+ category: 'truncation',
263
+ position: this.maxScanBytes,
264
+ context: `(content truncated at ${this.maxScanBytes} bytes; ${content.length - this.maxScanBytes} bytes unscanned)`,
265
+ });
266
+ }
267
+
268
+ for (const { label, regex, severity, category } of this.patterns) {
269
+ // Clone the RegExp so that the global-flag lastIndex state doesn't
270
+ // leak between calls. Built-in patterns are immutable; user-supplied
271
+ // patterns are also defensively cloned.
272
+ const re = new RegExp(regex.source, regex.flags.includes('g') ? regex.flags : regex.flags + 'g');
273
+ let match: RegExpExecArray | null;
274
+ while ((match = re.exec(scanned)) !== null) {
275
+ findings.push({
276
+ pattern: label,
277
+ severity,
278
+ category,
279
+ position: match.index,
280
+ context: snippet(scanned, match.index, match[0].length),
281
+ });
282
+ // Defensive: zero-length match would loop forever
283
+ if (match[0].length === 0) re.lastIndex++;
284
+ }
285
+ }
286
+
287
+ let highest: InjectionSeverity | 'none' = 'none';
288
+ for (const f of findings) highest = maxSeverity(highest, f.severity);
289
+
290
+ const action = highest === 'none' ? 'allow' : this.policy[highest];
291
+ const safe = action === 'allow' || action === 'flag';
292
+
293
+ return { safe, findings, highest };
294
+ }
295
+
296
+ /**
297
+ * Scan + enforce policy. Returns the content to pass forward (possibly
298
+ * redacted or empty) plus the scan result and the action that was taken.
299
+ * Callers should treat `reject` as "drop the tool result and signal an
300
+ * error" — do NOT silently substitute empty content.
301
+ */
302
+ scanAndEnforce(content: string): {
303
+ content: string;
304
+ result: GuardrailResult;
305
+ action: GuardrailAction;
306
+ } {
307
+ const result = this.scan(content);
308
+
309
+ const action: GuardrailAction =
310
+ result.highest === 'none' ? 'allow' : this.policy[result.highest];
311
+
312
+ let outgoing = content;
313
+ if (action === 'redact') {
314
+ outgoing = this.redact(content, result.findings);
315
+ } else if (action === 'reject') {
316
+ outgoing = '';
317
+ }
318
+
319
+ return { content: outgoing, result, action };
320
+ }
321
+
322
+ /**
323
+ * Replace each non-truncation finding's matched substring with
324
+ * `[REDACTED:<pattern>]`. Truncation findings have no substring to redact
325
+ * (their `position` is a length, not an offset into the content) and are
326
+ * skipped. Idempotent for already-redacted strings.
327
+ */
328
+ private redact(content: string, findings: InjectionFinding[]): string {
329
+ if (findings.length === 0) return content;
330
+
331
+ // Build a single combined regex per pattern label to avoid replacing
332
+ // overlapping matches multiple times.
333
+ const labels = new Set(
334
+ findings
335
+ .filter((f) => f.category !== 'truncation')
336
+ .map((f) => f.pattern)
337
+ );
338
+
339
+ let out = content;
340
+ for (const { label, regex } of this.patterns) {
341
+ if (!labels.has(label)) continue;
342
+ const re = new RegExp(regex.source, regex.flags.includes('g') ? regex.flags : regex.flags + 'g');
343
+ out = out.replace(re, `[REDACTED:${label}]`);
344
+ }
345
+ return out;
346
+ }
347
+ }
348
+
349
+ /** Convenience factory that returns a guardrail with the default policy. */
350
+ export function createToolOutputGuardrail(config?: GuardrailConfig): ToolOutputGuardrail {
351
+ return new ToolOutputGuardrail(config);
352
+ }
353
+
354
+ /**
355
+ * One-shot helper for callers that just want a yes/no answer without
356
+ * constructing a guardrail. Uses the default policy.
357
+ */
358
+ export function isToolOutputSafe(content: string): boolean {
359
+ return createToolOutputGuardrail().scan(content).safe;
360
+ }
package/tmp.json ADDED
File without changes
package/tsconfig.json CHANGED
@@ -1,9 +1,9 @@
1
- {
2
- "extends": "../../tsconfig.base.json",
3
- "compilerOptions": {
4
- "outDir": "./dist",
5
- "rootDir": "./src"
6
- },
7
- "include": ["src/**/*"],
8
- "exclude": ["node_modules", "dist"]
9
- }
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src"
6
+ },
7
+ "include": ["src/**/*"],
8
+ "exclude": ["node_modules", "dist"]
9
+ }