ai-shield-core 0.1.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.
Files changed (76) hide show
  1. package/dist/audit/logger.d.ts.map +1 -1
  2. package/dist/audit/logger.js +13 -14
  3. package/dist/audit/types.js +1 -2
  4. package/dist/cache/lru.js +1 -5
  5. package/dist/canary/memory.d.ts +75 -0
  6. package/dist/canary/memory.d.ts.map +1 -0
  7. package/dist/canary/memory.js +194 -0
  8. package/dist/context/wrap-context.d.ts +169 -0
  9. package/dist/context/wrap-context.d.ts.map +1 -0
  10. package/dist/context/wrap-context.js +278 -0
  11. package/dist/cost/anomaly.js +1 -4
  12. package/dist/cost/pricing.d.ts.map +1 -1
  13. package/dist/cost/pricing.js +26 -19
  14. package/dist/cost/tracker.d.ts +19 -1
  15. package/dist/cost/tracker.d.ts.map +1 -1
  16. package/dist/cost/tracker.js +27 -10
  17. package/dist/index.d.ts +34 -3
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +55 -37
  20. package/dist/judge/async-judge.d.ts +85 -0
  21. package/dist/judge/async-judge.d.ts.map +1 -0
  22. package/dist/judge/async-judge.js +146 -0
  23. package/dist/policy/circuit-breaker.d.ts +70 -0
  24. package/dist/policy/circuit-breaker.d.ts.map +1 -0
  25. package/dist/policy/circuit-breaker.js +376 -0
  26. package/dist/policy/engine.js +1 -5
  27. package/dist/policy/tools.js +4 -8
  28. package/dist/scanner/canary.js +4 -8
  29. package/dist/scanner/chain.js +1 -5
  30. package/dist/scanner/heuristic.d.ts +27 -0
  31. package/dist/scanner/heuristic.d.ts.map +1 -1
  32. package/dist/scanner/heuristic.js +118 -7
  33. package/dist/scanner/ingestion.d.ts +147 -0
  34. package/dist/scanner/ingestion.d.ts.map +1 -0
  35. package/dist/scanner/ingestion.js +520 -0
  36. package/dist/scanner/output.d.ts +73 -0
  37. package/dist/scanner/output.d.ts.map +1 -0
  38. package/dist/scanner/output.js +297 -0
  39. package/dist/scanner/pii.d.ts.map +1 -1
  40. package/dist/scanner/pii.js +24 -12
  41. package/dist/shield.d.ts.map +1 -1
  42. package/dist/shield.js +34 -26
  43. package/dist/types.d.ts +156 -2
  44. package/dist/types.d.ts.map +1 -1
  45. package/dist/types.js +1 -2
  46. package/package.json +4 -3
  47. package/src/audit/logger.ts +6 -1
  48. package/src/canary/memory.ts +259 -0
  49. package/src/context/wrap-context.ts +475 -0
  50. package/src/cost/pricing.ts +21 -9
  51. package/src/cost/tracker.ts +35 -1
  52. package/src/index.ts +113 -2
  53. package/src/judge/async-judge.ts +254 -0
  54. package/src/policy/circuit-breaker.ts +449 -0
  55. package/src/scanner/heuristic.ts +125 -2
  56. package/src/scanner/ingestion.ts +624 -0
  57. package/src/scanner/output.ts +386 -0
  58. package/src/scanner/pii.ts +21 -7
  59. package/src/shield.ts +15 -2
  60. package/src/types.ts +194 -2
  61. package/tsconfig.json +2 -1
  62. package/dist/audit/logger.js.map +0 -1
  63. package/dist/audit/types.js.map +0 -1
  64. package/dist/cache/lru.js.map +0 -1
  65. package/dist/cost/anomaly.js.map +0 -1
  66. package/dist/cost/pricing.js.map +0 -1
  67. package/dist/cost/tracker.js.map +0 -1
  68. package/dist/index.js.map +0 -1
  69. package/dist/policy/engine.js.map +0 -1
  70. package/dist/policy/tools.js.map +0 -1
  71. package/dist/scanner/canary.js.map +0 -1
  72. package/dist/scanner/chain.js.map +0 -1
  73. package/dist/scanner/heuristic.js.map +0 -1
  74. package/dist/scanner/pii.js.map +0 -1
  75. package/dist/shield.js.map +0 -1
  76. package/dist/types.js.map +0 -1
@@ -0,0 +1,297 @@
1
+ import { PIIScanner } from "./pii.js";
2
+ import { normalizeForInjectionScan } from "./heuristic.js";
3
+ // ============================================================
4
+ // Output Scanner — OWASP LLM05 Improper Output Handling +
5
+ // LLM02 Sensitive Information Disclosure (output side)
6
+ //
7
+ // AI Shield's input scanners answer "is this prompt safe to send to the
8
+ // model?". This scanner answers the other half: "is this model OUTPUT
9
+ // safe to act on / show / forward downstream?".
10
+ //
11
+ // LLM output must never reach a SQL engine, a shell, an HTML sink, or a
12
+ // template renderer unfiltered — XSS, SSRF, SQLi and command injection
13
+ // sourced from model output are a documented 2026 attack class (OWASP
14
+ // LLM05). And a model can leak its own system prompt or a secret it was
15
+ // shown, which is LLM02 / LLM07.
16
+ //
17
+ // Five checks. Inputs are Unicode-normalized first (homoglyph / zero-width /
18
+ // fullwidth evasion defense). Secret + canary checks scan the FULL output
19
+ // (a leak can sit anywhere); the structural checks scan a length-capped copy
20
+ // (those payloads live in the first chunk):
21
+ // 1. secret_leak — API keys, tokens, private keys, DSNs (full output)
22
+ // 2. output_injection — SQL / shell / HTML-JS / template / md-exfil (capped)
23
+ // 3. system_prompt_leak — canary-token leak (exact, full) + heuristic phrasing
24
+ // 4. pii_detected — reuses the input-side PIIScanner
25
+ // 5. jailbreak_indicator— compliance-preamble / mode-switch acknowledgement
26
+ //
27
+ // Checks 1-3 are high-confidence and block. PII follows its configured
28
+ // action. Jailbreak is heuristic and only warns — a "sure, here's how"
29
+ // preamble is often legitimate.
30
+ // ============================================================
31
+ /** Hard cap on the bytes we pattern-scan. A 1 MB model response is not the
32
+ * threat model and unbounded regex over it pressures GC. Overridable. */
33
+ const DEFAULT_MAX_OUTPUT_BYTES = 256 * 1024;
34
+ /**
35
+ * High-confidence secret formats. Each is anchored on a provider-specific
36
+ * prefix so false positives on prose are near-zero. Patterns are linear
37
+ * (no nested quantifiers) — ReDoS-safe on large output.
38
+ */
39
+ const SECRET_PATTERNS = [
40
+ { id: "SEC-OPENAI", re: /\bsk-(?:proj-)?[A-Za-z0-9_-]{20,}\b/, label: "OpenAI API key" },
41
+ { id: "SEC-ANTHROPIC", re: /\bsk-ant-[A-Za-z0-9_-]{20,}\b/, label: "Anthropic API key" },
42
+ { id: "SEC-AWS-AKID", re: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/, label: "AWS access key id" },
43
+ { id: "SEC-GITHUB", re: /\bgh[pousr]_[A-Za-z0-9]{36,}\b/, label: "GitHub token" },
44
+ { id: "SEC-GOOGLE", re: /\bAIza[0-9A-Za-z_-]{35}\b/, label: "Google API key" },
45
+ { id: "SEC-GOOGLE-OAUTH", re: /\bGOCSPX-[A-Za-z0-9_-]{28}\b/, label: "Google OAuth client secret" },
46
+ { id: "SEC-GCP-SA", re: /"type"\s*:\s*"service_account"/, label: "GCP service-account JSON" },
47
+ { id: "SEC-HUGGINGFACE", re: /\bhf_[A-Za-z0-9]{30,}\b/, label: "HuggingFace token" },
48
+ { id: "SEC-NPM", re: /\bnpm_[A-Za-z0-9]{36}\b/, label: "npm publish token" },
49
+ { id: "SEC-SLACK", re: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/, label: "Slack token" },
50
+ { id: "SEC-STRIPE", re: /\b[rs]k_live_[A-Za-z0-9]{20,}\b/, label: "Stripe live key" },
51
+ { id: "SEC-JWT", re: /\beyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/, label: "JWT" },
52
+ { id: "SEC-PEM", re: /-----BEGIN (?:RSA |EC |DSA |OPENSSH |PGP )?PRIVATE KEY-----/, label: "PEM private key" },
53
+ // DSN: both credential segments are length-bounded so a long near-match
54
+ // without a trailing `@` can't drive O(n²) backtracking (review H1).
55
+ { id: "SEC-DSN", re: /\b(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis|amqps?):\/\/[^\s:/@]{1,64}:[^\s@]{3,80}@/, label: "connection string with credentials" },
56
+ ];
57
+ /**
58
+ * Output-injection payloads, grouped by downstream sink. Each pattern is
59
+ * deliberately conservative — flagging legitimate output that merely
60
+ * *mentions* SQL would be useless. They target syntax that only matters
61
+ * when the string is interpreted, not displayed.
62
+ */
63
+ const INJECTION_PATTERNS = [
64
+ // SQL
65
+ { id: "OUTI-SQL-1", sink: "sql", re: /\bunion\s+(?:all\s+)?select\b/i, label: "SQL UNION SELECT" },
66
+ { id: "OUTI-SQL-2", sink: "sql", re: /['"]\s*;\s*(?:drop|delete|update|insert|truncate|alter)\s+/i, label: "SQL statement break" },
67
+ { id: "OUTI-SQL-3", sink: "sql", re: /\bor\s+1\s*=\s*1\b|\bor\s+'1'\s*=\s*'1'/i, label: "SQL tautology" },
68
+ // Shell
69
+ { id: "OUTI-SH-1", sink: "shell", re: /\$\([^)]{1,200}\)|`[^`]{1,200}`/, label: "shell command substitution" },
70
+ { id: "OUTI-SH-2", sink: "shell", re: /[;&|]\s*(?:rm|curl|wget|nc|bash|sh|chmod|mkfifo|dd)\s+-?/i, label: "chained shell command" },
71
+ { id: "OUTI-SH-3", sink: "shell", re: /\|\s*(?:sh|bash|zsh|python[0-9.]*)\b/i, label: "pipe to interpreter" },
72
+ // HTML / JS (XSS)
73
+ { id: "OUTI-XSS-1", sink: "html", re: /<script[\s>]/i, label: "<script> tag" },
74
+ { id: "OUTI-XSS-2", sink: "html", re: /\bon(?:error|load|click|mouseover)\s*=\s*["']?[^"'>]{1,200}/i, label: "inline event handler" },
75
+ { id: "OUTI-XSS-3", sink: "html", re: /\bjavascript:\s*[^\s"']{1,200}/i, label: "javascript: URI" },
76
+ { id: "OUTI-XSS-4", sink: "html", re: /<iframe[\s>]|<img[^>]{0,200}\bsrc\s*=\s*["']?\s*(?:javascript|data):/i, label: "iframe / data-URI image" },
77
+ // Markdown-image data exfiltration: ![x](http://evil/log?data=…). When a
78
+ // renderer auto-loads the image the query string leaks whatever the model
79
+ // was told to embed. The most-overlooked LLM05 class (review: Research).
80
+ { id: "OUTI-MDEXF", sink: "html", re: /!\[[^\]]{0,200}\]\(\s*https?:\/\/[^)\s]{1,300}[?&][\w-]{1,40}=/i, label: "markdown-image data exfiltration" },
81
+ // Template / SSTI
82
+ { id: "OUTI-SSTI-1", sink: "template", re: /\{\{[^}]{0,200}(?:constructor|process|require|global|__proto__|self\.|cycler)[^}]{0,200}\}\}/i, label: "template-injection payload" },
83
+ { id: "OUTI-SSTI-2", sink: "template", re: /<%[^%]{0,200}(?:system|exec|require|eval)[^%]{0,200}%>/i, label: "ERB/EJS injection" },
84
+ ];
85
+ /**
86
+ * System-prompt-leak heuristics — used only when no canary token is
87
+ * available. Low-confidence by design (these phrasings occur in benign
88
+ * output too), so they warn rather than block.
89
+ */
90
+ const SYSTEM_LEAK_PATTERNS = [
91
+ /(?:my|the)\s+(?:system\s+)?(?:prompt|instructions?)\s+(?:is|are|say|states?|read)\b/i,
92
+ /i\s+(?:was|am|have\s+been)\s+(?:instructed|told|configured|programmed|designed)\s+to\b/i,
93
+ /here\s+(?:is|are)\s+my\s+(?:system\s+)?(?:prompt|instructions?|guidelines?|rules?)\b/i,
94
+ /you\s+are\s+(?:a|an)\s+[\w-]{2,30}\s+(?:assistant|agent|bot|model)\b.{0,40}\b(?:you\s+must|your\s+(?:rules?|guidelines?|instructions?))/i,
95
+ ];
96
+ /**
97
+ * Jailbreak-success indicators in the OUTPUT. Conservative + low weight:
98
+ * a generic "Sure, here's how" is not enough on its own — these target
99
+ * explicit mode-switch acknowledgements and self-declared rule-breaking.
100
+ */
101
+ const JAILBREAK_PATTERNS = [
102
+ /\bas\s+(?:DAN|an?\s+(?:unrestricted|unfiltered|jailbroken|uncensored))\b/i,
103
+ /i(?:'?ll|\s+will)\s+(?:now\s+)?(?:ignore|bypass|disregard|set\s+aside)\s+(?:my|the|all)\s+(?:guidelines?|restrictions?|rules?|safety|programming|filters?)/i,
104
+ /(?:jailbreak|developer\s+mode|dan\s+mode)\s+(?:enabled|activated|successful|engaged)/i,
105
+ /i\s+am\s+(?:now\s+)?(?:free\s+(?:from|of)|no\s+longer\s+bound\s+by)\s+(?:my\s+)?(?:restrictions?|guidelines?|rules?|programming)/i,
106
+ ];
107
+ const SECRET_REDACTION = "[REDACTED_SECRET]";
108
+ /**
109
+ * Scanner for LLM output. Stateless; safe to reuse across calls.
110
+ */
111
+ export class OutputScanner {
112
+ config;
113
+ pii;
114
+ constructor(config = {}) {
115
+ this.config = config;
116
+ this.pii =
117
+ config.pii === false
118
+ ? null
119
+ : new PIIScanner(config.pii ?? { action: "mask" });
120
+ }
121
+ async scan(output, context = {}) {
122
+ const start = performance.now();
123
+ const violations = [];
124
+ const checksRun = [];
125
+ const checks = this.config.checks ?? {};
126
+ const maxBytes = this.config.maxBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
127
+ const safeOutput = typeof output === "string" ? output : "";
128
+ // Capped copy for the *structural* checks (injection / leak-phrasing /
129
+ // jailbreak) — those payloads live in the first chunk and the regex over
130
+ // a 1 MB response would pressure GC. Normalized so homoglyph / zero-width
131
+ // / fullwidth evasion can't slip a payload past the patterns (review H6).
132
+ const cappedDetect = normalizeForInjectionScan(safeOutput.length > maxBytes ? safeOutput.slice(0, maxBytes) : safeOutput);
133
+ // Secrets and canaries can sit ANYWHERE in the output, and the secret
134
+ // patterns are anchored + linear — so they scan the FULL output, not the
135
+ // cap (review C1: a key padded past 256 KB must not slip through). Also
136
+ // normalized for the same evasion defense.
137
+ const fullDetect = normalizeForInjectionScan(safeOutput);
138
+ let sanitized = output;
139
+ let worst = "allow";
140
+ const bump = (d) => {
141
+ if (priority(d) > priority(worst))
142
+ worst = d;
143
+ };
144
+ // 1. Secret leak — high-confidence, always blocks. Redact in `sanitized`.
145
+ // Detection runs on the normalized full output; redaction is
146
+ // best-effort over the raw output (a key fragmented by zero-width
147
+ // chars is still flagged via `fullDetect` and blocks, but may resist
148
+ // clean redaction — callers MUST gate on `safe`/`decision` and never
149
+ // forward a blocked output regardless of `sanitized`).
150
+ if (checks.secrets !== false) {
151
+ checksRun.push("secrets");
152
+ for (const { id, re, label } of SECRET_PATTERNS) {
153
+ if (re.test(fullDetect)) {
154
+ violations.push({
155
+ type: "secret_leak",
156
+ scanner: "output",
157
+ score: 1.0,
158
+ threshold: 0.5,
159
+ message: `Output leaks a secret: ${label}`,
160
+ detail: `Rule ${id}`,
161
+ });
162
+ bump("block");
163
+ // Redact every occurrence in the full output (global copy of re).
164
+ sanitized = sanitized.replace(new RegExp(re.source, re.flags.includes("g") ? re.flags : re.flags + "g"), SECRET_REDACTION);
165
+ }
166
+ }
167
+ }
168
+ // 2. Output injection — payloads dangerous to a downstream sink.
169
+ if (checks.injection !== false) {
170
+ checksRun.push("injection");
171
+ const allowedSinks = this.config.sinks;
172
+ for (const { id, sink, re, label } of INJECTION_PATTERNS) {
173
+ if (allowedSinks && !allowedSinks.includes(sink))
174
+ continue;
175
+ if (re.test(cappedDetect)) {
176
+ violations.push({
177
+ type: "output_injection",
178
+ scanner: "output",
179
+ score: 0.85,
180
+ threshold: 0.5,
181
+ message: `Output carries a ${sink} injection payload: ${label}`,
182
+ detail: `Rule ${id} (sink=${sink})`,
183
+ });
184
+ bump("block");
185
+ }
186
+ }
187
+ }
188
+ // 3. System-prompt leak — canary first (exact, certain), then heuristics.
189
+ if (checks.systemPromptLeak !== false) {
190
+ checksRun.push("system_prompt_leak");
191
+ const tokens = normalizeTokens(this.config.canaryTokens);
192
+ let canaryHit = false;
193
+ for (const token of tokens) {
194
+ // Check the FULL output, not the capped copy — a leak past 256 KB is
195
+ // still a leak, and an exact substring search is cheap.
196
+ if (token.length >= 4 && output.includes(token)) {
197
+ canaryHit = true;
198
+ violations.push({
199
+ type: "system_prompt_leak",
200
+ scanner: "output",
201
+ score: 1.0,
202
+ threshold: 0.5,
203
+ message: "Output leaks a system-prompt canary token",
204
+ detail: "Canary match (exact)",
205
+ });
206
+ bump("block");
207
+ }
208
+ }
209
+ // Heuristic phrasing only when no canary was available/hit — avoids
210
+ // double-reporting and keeps the low-confidence signal subordinate.
211
+ if (!canaryHit && tokens.length === 0) {
212
+ for (const re of SYSTEM_LEAK_PATTERNS) {
213
+ if (re.test(cappedDetect)) {
214
+ violations.push({
215
+ type: "system_prompt_leak",
216
+ scanner: "output",
217
+ score: 0.4,
218
+ threshold: 0.5,
219
+ message: "Output may be echoing the system prompt",
220
+ detail: "Heuristic phrasing (no canary configured — pass canaryTokens for an exact check)",
221
+ });
222
+ bump("warn");
223
+ break; // one heuristic signal is enough
224
+ }
225
+ }
226
+ }
227
+ }
228
+ // 4. Jailbreak indicators — heuristic, warn only.
229
+ if (checks.jailbreak !== false) {
230
+ checksRun.push("jailbreak");
231
+ for (const re of JAILBREAK_PATTERNS) {
232
+ if (re.test(cappedDetect)) {
233
+ violations.push({
234
+ type: "jailbreak_indicator",
235
+ scanner: "output",
236
+ score: 0.3,
237
+ threshold: 0.5,
238
+ message: "Output shows a possible jailbreak success indicator",
239
+ detail: "Heuristic phrasing",
240
+ });
241
+ bump("warn");
242
+ break;
243
+ }
244
+ }
245
+ }
246
+ // 5. PII — reuse the input-side scanner; respects its configured action.
247
+ if (this.pii) {
248
+ checksRun.push("pii");
249
+ const piiResult = await this.pii.scan(sanitized, context);
250
+ for (const v of piiResult.violations) {
251
+ violations.push({ ...v, scanner: "output" });
252
+ }
253
+ if (piiResult.sanitized !== undefined)
254
+ sanitized = piiResult.sanitized;
255
+ bump(piiResult.decision);
256
+ }
257
+ return {
258
+ safe: worst === "allow",
259
+ decision: worst,
260
+ sanitized,
261
+ violations,
262
+ meta: {
263
+ scanDurationMs: performance.now() - start,
264
+ checksRun,
265
+ },
266
+ };
267
+ }
268
+ }
269
+ /**
270
+ * One-shot helper. Scan a model response before acting on it.
271
+ *
272
+ * @example
273
+ * ```ts
274
+ * import { scanOutput } from "ai-shield-core";
275
+ *
276
+ * const reply = await llm.generate(prompt);
277
+ * const r = await scanOutput(reply, { canaryTokens: canary, sinks: ["sql"] });
278
+ * if (!r.safe) {
279
+ * audit.warn("unsafe model output", r.violations);
280
+ * return genericFallback(); // do not run r.sanitized as SQL
281
+ * }
282
+ * showToUser(r.sanitized); // PII masked, secrets redacted
283
+ * ```
284
+ */
285
+ export async function scanOutput(output, config = {}, context = {}) {
286
+ return new OutputScanner(config).scan(output, context);
287
+ }
288
+ function normalizeTokens(tokens) {
289
+ if (!tokens)
290
+ return [];
291
+ const arr = Array.isArray(tokens) ? tokens : [tokens];
292
+ return arr.filter((t) => typeof t === "string" && t.length > 0);
293
+ }
294
+ function priority(d) {
295
+ return d === "block" ? 2 : d === "warn" ? 1 : 0;
296
+ }
297
+ //# sourceMappingURL=output.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"pii.d.ts","sourceRoot":"","sources":["../../src/scanner/pii.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,OAAO,EACP,aAAa,EACb,WAAW,EAEX,SAAS,EAGT,SAAS,EACV,MAAM,aAAa,CAAC;AAsKrB,qBAAa,UAAW,YAAW,OAAO;IACxC,QAAQ,CAAC,IAAI,SAAS;IACtB,OAAO,CAAC,QAAQ,CAAe;IAC/B,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,aAAa,CAAsC;IAC3D,OAAO,CAAC,YAAY,CAAe;gBAEvB,MAAM,GAAE,SAAc;IAO5B,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,WAAW,GAAG,OAAO,CAAC,aAAa,CAAC;IAoDxE,sCAAsC;IACtC,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,EAAE;IA8BjC,uFAAuF;IACvF,OAAO,CAAC,mBAAmB;IAuB3B,gCAAgC;IAChC,OAAO,CAAC,YAAY;CAerB"}
1
+ {"version":3,"file":"pii.d.ts","sourceRoot":"","sources":["../../src/scanner/pii.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,OAAO,EACP,aAAa,EACb,WAAW,EAEX,SAAS,EAGT,SAAS,EACV,MAAM,aAAa,CAAC;AAoLrB,qBAAa,UAAW,YAAW,OAAO;IACxC,QAAQ,CAAC,IAAI,SAAS;IACtB,OAAO,CAAC,QAAQ,CAAe;IAC/B,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,aAAa,CAAsC;IAC3D,OAAO,CAAC,YAAY,CAAe;gBAEvB,MAAM,GAAE,SAAc;IAO5B,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,WAAW,GAAG,OAAO,CAAC,aAAa,CAAC;IAoDxE,sCAAsC;IACtC,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,EAAE;IA8BjC,uFAAuF;IACvF,OAAO,CAAC,mBAAmB;IAuB3B,gCAAgC;IAChC,OAAO,CAAC,YAAY;CAerB"}
@@ -1,12 +1,12 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.PIIScanner = void 0;
4
1
  // --- German & International PII Patterns ---
5
2
  const PII_PATTERNS = [
6
- // IBAN: DE + 2 check digits + 18 digits (with optional spaces/dashes)
3
+ // IBAN: 2-letter ISO + 2 check digits + 11..30 alphanumerics (with optional spaces/dashes).
4
+ // Covers all 80+ IBAN countries: NO (15), BE (16), DE (22), FR (27), MT (31), SC (31).
5
+ // The validator runs mod-97 over the cleaned value and rejects anything that isn't a real IBAN.
6
+ // Pattern is linear (no nested quantifiers) — ReDoS-safe.
7
7
  {
8
8
  type: "iban",
9
- pattern: /\b[A-Z]{2}\s?\d{2}\s?\d{4}\s?\d{4}\s?\d{4}\s?\d{4}\s?\d{2,4}\b/g,
9
+ pattern: /\b[A-Z]{2}\d{2}[ -]?[A-Z0-9](?:[A-Z0-9 -]{9,36}[A-Z0-9])?\b/g,
10
10
  validator: validateIBAN,
11
11
  baseConfidence: 0.95,
12
12
  },
@@ -36,10 +36,15 @@ const PII_PATTERNS = [
36
36
  pattern: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g,
37
37
  baseConfidence: 0.95,
38
38
  },
39
- // Phone: German formats (+49, 0xxx) and international
39
+ // Phone: German formats (+49, 0xxx) and international.
40
+ // Previous pattern had nested optional quantifiers (`\s?[\s\-/]?` plus two
41
+ // `[\s\-/]?\d{0,5}` tails) which risks catastrophic backtracking on
42
+ // malformed inputs. Restructured so every separator group requires at least
43
+ // one char when present and the trailing digits group is a true non-optional
44
+ // extension (or absent entirely).
40
45
  {
41
46
  type: "phone",
42
- pattern: /(?<!\d)(?:\+\d{1,3}|00\d{1,3}|0)\s?[\s\-/]?\(?\d{2,5}\)?[\s\-/]?\d{3,8}[\s\-/]?\d{0,5}\b/g,
47
+ pattern: /(?<!\d)(?:\+\d{1,3}|00\d{1,3}|0)[\s\-/]?\(?\d{2,5}\)?[\s\-/]?\d{3,8}(?:[\s\-/]\d{1,5})?\b/g,
43
48
  validator: validatePhone,
44
49
  baseConfidence: 0.80,
45
50
  },
@@ -129,17 +134,25 @@ function maskValue(type, value) {
129
134
  return value[0] + "***@" + value.substring(atIdx + 1);
130
135
  }
131
136
  case "phone":
137
+ // Need room for 4-prefix + **** + 2-suffix without overlap.
138
+ if (value.length < 7)
139
+ return "[PHONE]";
132
140
  return value.substring(0, 4) + "****" + value.substring(value.length - 2);
133
141
  case "iban":
134
- return value.substring(0, 4) + " **** **** ****";
135
- case "credit_card":
136
- return "**** **** **** " + value.replace(/\D/g, "").substring(12);
142
+ // Keep country code + check digits, mask rest. Works for any IBAN length.
143
+ return value.length >= 4 ? value.substring(0, 4) + " **** **** ****" : "[IBAN]";
144
+ case "credit_card": {
145
+ const digits = value.replace(/\D/g, "");
146
+ if (digits.length < 13)
147
+ return "[CREDIT_CARD]";
148
+ return "**** **** **** " + digits.substring(digits.length - 4);
149
+ }
137
150
  default:
138
151
  return `[${type.toUpperCase()}]`;
139
152
  }
140
153
  }
141
154
  // --- PII Scanner Class ---
142
- class PIIScanner {
155
+ export class PIIScanner {
143
156
  name = "pii";
144
157
  patterns;
145
158
  action;
@@ -251,5 +264,4 @@ class PIIScanner {
251
264
  return masked;
252
265
  }
253
266
  }
254
- exports.PIIScanner = PIIScanner;
255
267
  //# sourceMappingURL=pii.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"shield.d.ts","sourceRoot":"","sources":["../src/shield.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,WAAW,EAAc,MAAM,YAAY,CAAC;AAKpF,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAUlD,qBAAa,QAAQ;IACnB,OAAO,CAAC,KAAK,CAAe;IAC5B,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,WAAW,CAAqB;IACxC,OAAO,CAAC,WAAW,CAAqB;IACxC,OAAO,CAAC,SAAS,CAAkC;IACnD,OAAO,CAAC,MAAM,CAAe;gBAEjB,MAAM,GAAE,YAAiB;IAyBrC,qCAAqC;IAC/B,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,UAAU,CAAC;IA+BzE,kDAAkD;IAC5C,WAAW,CACf,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,oBAAoB,EAAE,MAAM,EAC5B,qBAAqB,CAAC,EAAE,MAAM;IAahC,+CAA+C;IACzC,UAAU,CACd,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM;IAMtB,sCAAsC;IAChC,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAKxD,4BAA4B;IAC5B,SAAS,IAAI,YAAY;IAIzB,2BAA2B;IAC3B,UAAU,IAAI,IAAI;IAIlB,sBAAsB;IACtB,IAAI,SAAS,IAAI,MAAM,CAEtB;IAED,wBAAwB;IAClB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAS5B,OAAO,CAAC,aAAa;IAUrB,OAAO,CAAC,aAAa;IAkDrB,OAAO,CAAC,UAAU;CA2BnB"}
1
+ {"version":3,"file":"shield.d.ts","sourceRoot":"","sources":["../src/shield.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,WAAW,EAAc,MAAM,YAAY,CAAC;AAKpF,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAUlD,qBAAa,QAAQ;IACnB,OAAO,CAAC,KAAK,CAAe;IAC5B,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,WAAW,CAAqB;IACxC,OAAO,CAAC,WAAW,CAAqB;IACxC,OAAO,CAAC,SAAS,CAAkC;IACnD,OAAO,CAAC,MAAM,CAAe;gBAEjB,MAAM,GAAE,YAAiB;IAyBrC,qCAAqC;IAC/B,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,UAAU,CAAC;IA4CzE,kDAAkD;IAC5C,WAAW,CACf,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,oBAAoB,EAAE,MAAM,EAC5B,qBAAqB,CAAC,EAAE,MAAM;IAahC,+CAA+C;IACzC,UAAU,CACd,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM;IAMtB,sCAAsC;IAChC,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAKxD,4BAA4B;IAC5B,SAAS,IAAI,YAAY;IAIzB,2BAA2B;IAC3B,UAAU,IAAI,IAAI;IAIlB,sBAAsB;IACtB,IAAI,SAAS,IAAI,MAAM,CAEtB;IAED,wBAAwB;IAClB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAS5B,OAAO,CAAC,aAAa;IAUrB,OAAO,CAAC,aAAa;IAkDrB,OAAO,CAAC,UAAU;CA2BnB"}
package/dist/shield.js CHANGED
@@ -1,18 +1,15 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.AIShield = void 0;
4
- const chain_js_1 = require("./scanner/chain.js");
5
- const heuristic_js_1 = require("./scanner/heuristic.js");
6
- const pii_js_1 = require("./scanner/pii.js");
7
- const tools_js_1 = require("./policy/tools.js");
8
- const engine_js_1 = require("./policy/engine.js");
9
- const tracker_js_1 = require("./cost/tracker.js");
10
- const logger_js_1 = require("./audit/logger.js");
11
- const lru_js_1 = require("./cache/lru.js");
1
+ import { ScannerChain } from "./scanner/chain.js";
2
+ import { HeuristicScanner } from "./scanner/heuristic.js";
3
+ import { PIIScanner } from "./scanner/pii.js";
4
+ import { ToolPolicyScanner } from "./policy/tools.js";
5
+ import { PolicyEngine } from "./policy/engine.js";
6
+ import { CostTracker } from "./cost/tracker.js";
7
+ import { AuditLogger, ConsoleAuditStore } from "./audit/logger.js";
8
+ import { ScanLRUCache } from "./cache/lru.js";
12
9
  // ============================================================
13
10
  // AIShield — Main class, single entry point
14
11
  // ============================================================
15
- class AIShield {
12
+ export class AIShield {
16
13
  chain;
17
14
  policyEngine;
18
15
  costTracker;
@@ -21,19 +18,19 @@ class AIShield {
21
18
  config;
22
19
  constructor(config = {}) {
23
20
  this.config = config;
24
- this.policyEngine = new engine_js_1.PolicyEngine(config.preset ?? "public_website");
25
- this.chain = new chain_js_1.ScannerChain({ earlyExit: true });
21
+ this.policyEngine = new PolicyEngine(config.preset ?? "public_website");
22
+ this.chain = new ScannerChain({ earlyExit: true });
26
23
  // Build scanner chain based on config
27
24
  this.setupScanners(config);
28
25
  // Cost tracker (optional, needs Redis for distributed use)
29
26
  this.costTracker = config.cost?.enabled !== false && config.cost?.budgets
30
- ? new tracker_js_1.CostTracker(config.cost.budgets)
27
+ ? new CostTracker(config.cost.budgets)
31
28
  : null;
32
29
  // Audit logger (optional)
33
30
  this.auditLogger = this.setupAudit(config);
34
31
  // Scan cache (enabled when cache config is provided)
35
32
  this.scanCache = config.cache && config.cache.enabled !== false
36
- ? new lru_js_1.ScanLRUCache({
33
+ ? new ScanLRUCache({
37
34
  maxSize: config.cache.maxSize,
38
35
  ttlMs: config.cache.ttlMs,
39
36
  })
@@ -41,6 +38,15 @@ class AIShield {
41
38
  }
42
39
  /** Scan input text — the main API */
43
40
  async scan(input, context = {}) {
41
+ // Input-length guard — without this a user-supplied multi-MB prompt would
42
+ // trigger every regex scan on the full buffer, which is O(n) best-case
43
+ // but pathological under ReDoS-prone patterns. 256 KB handles every
44
+ // real chat/tool input we care about with plenty of headroom; override
45
+ // via AI_SHIELD_MAX_INPUT_BYTES when needed.
46
+ const maxInputBytes = Number(process.env.AI_SHIELD_MAX_INPUT_BYTES ?? 262_144);
47
+ if (input.length > maxInputBytes) {
48
+ input = input.slice(0, maxInputBytes);
49
+ }
44
50
  // Apply preset if not set in context
45
51
  if (!context.preset) {
46
52
  context.preset = this.config.preset ?? "public_website";
@@ -59,8 +65,11 @@ class AIShield {
59
65
  const cacheKey = this.buildCacheKey(input, context);
60
66
  this.scanCache.set(cacheKey, result);
61
67
  }
62
- // Log to audit if enabled
63
- if (this.auditLogger) {
68
+ // Log to audit if enabled — but never double-log the same input when
69
+ // a downstream caller re-scans cached content (result.meta.cached is set
70
+ // by the cache hit path above; here it is always false but we guard to
71
+ // be explicit and so subclasses extending scan() stay safe).
72
+ if (this.auditLogger && !result.meta.cached) {
64
73
  void this.auditLogger.log(input, result, context);
65
74
  }
66
75
  return result;
@@ -117,7 +126,7 @@ class AIShield {
117
126
  // 1. Heuristic injection scanner (always on unless explicitly disabled)
118
127
  if (config.injection?.enabled !== false) {
119
128
  const preset = this.policyEngine.getPreset();
120
- this.chain.add(new heuristic_js_1.HeuristicScanner({
129
+ this.chain.add(new HeuristicScanner({
121
130
  strictness: config.injection?.strictness ?? "medium",
122
131
  threshold: config.injection?.threshold ?? preset.injection.threshold,
123
132
  customPatterns: config.injection?.customPatterns?.map((pattern, i) => ({
@@ -131,7 +140,7 @@ class AIShield {
131
140
  }
132
141
  // 2. PII scanner
133
142
  if (config.pii?.enabled !== false) {
134
- this.chain.add(new pii_js_1.PIIScanner({
143
+ this.chain.add(new PIIScanner({
135
144
  action: config.pii?.action ?? this.policyEngine.getPIIAction(),
136
145
  locale: config.pii?.locale,
137
146
  types: config.pii?.types,
@@ -149,7 +158,7 @@ class AIShield {
149
158
  this.policyEngine.getMaxToolChainDepth(),
150
159
  },
151
160
  };
152
- this.chain.add(new tools_js_1.ToolPolicyScanner(toolPolicy, config.tools.manifestPins));
161
+ this.chain.add(new ToolPolicyScanner(toolPolicy, config.tools.manifestPins));
153
162
  }
154
163
  }
155
164
  setupAudit(config) {
@@ -158,27 +167,26 @@ class AIShield {
158
167
  let store;
159
168
  switch (config.audit?.store) {
160
169
  case "console":
161
- store = new logger_js_1.ConsoleAuditStore();
170
+ store = new ConsoleAuditStore();
162
171
  break;
163
172
  case "postgresql":
164
173
  // PostgreSQL store would be imported separately to keep core lightweight
165
174
  // For now, fall through to console
166
- store = new logger_js_1.ConsoleAuditStore();
175
+ store = new ConsoleAuditStore();
167
176
  break;
168
177
  case "memory":
169
178
  default:
170
179
  // If no store configured and audit not explicitly enabled, skip
171
180
  if (!config.audit?.store && config.audit?.enabled !== true)
172
181
  return null;
173
- store = new logger_js_1.ConsoleAuditStore();
182
+ store = new ConsoleAuditStore();
174
183
  break;
175
184
  }
176
- return new logger_js_1.AuditLogger({
185
+ return new AuditLogger({
177
186
  store,
178
187
  batchSize: config.audit?.batchSize,
179
188
  flushIntervalMs: config.audit?.flushIntervalMs,
180
189
  });
181
190
  }
182
191
  }
183
- exports.AIShield = AIShield;
184
192
  //# sourceMappingURL=shield.js.map