ai-shield-core 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/dist/audit/logger.d.ts +40 -0
- package/dist/audit/logger.d.ts.map +1 -0
- package/dist/audit/logger.js +100 -0
- package/dist/audit/logger.js.map +1 -0
- package/dist/audit/types.d.ts +12 -0
- package/dist/audit/types.d.ts.map +1 -0
- package/dist/audit/types.js +3 -0
- package/dist/audit/types.js.map +1 -0
- package/dist/cache/lru.d.ts +27 -0
- package/dist/cache/lru.d.ts.map +1 -0
- package/dist/cache/lru.js +74 -0
- package/dist/cache/lru.js.map +1 -0
- package/dist/cost/anomaly.d.ts +10 -0
- package/dist/cost/anomaly.d.ts.map +1 -0
- package/dist/cost/anomaly.js +42 -0
- package/dist/cost/anomaly.js.map +1 -0
- package/dist/cost/pricing.d.ts +7 -0
- package/dist/cost/pricing.d.ts.map +1 -0
- package/dist/cost/pricing.js +51 -0
- package/dist/cost/pricing.js.map +1 -0
- package/dist/cost/tracker.d.ts +24 -0
- package/dist/cost/tracker.d.ts.map +1 -0
- package/dist/cost/tracker.js +136 -0
- package/dist/cost/tracker.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +59 -0
- package/dist/index.js.map +1 -0
- package/dist/policy/engine.d.ts +36 -0
- package/dist/policy/engine.d.ts.map +1 -0
- package/dist/policy/engine.js +127 -0
- package/dist/policy/engine.js.map +1 -0
- package/dist/policy/tools.d.ts +25 -0
- package/dist/policy/tools.d.ts.map +1 -0
- package/dist/policy/tools.js +158 -0
- package/dist/policy/tools.js.map +1 -0
- package/dist/scanner/canary.d.ts +9 -0
- package/dist/scanner/canary.d.ts.map +1 -0
- package/dist/scanner/canary.js +19 -0
- package/dist/scanner/canary.js.map +1 -0
- package/dist/scanner/chain.d.ts +17 -0
- package/dist/scanner/chain.d.ts.map +1 -0
- package/dist/scanner/chain.js +69 -0
- package/dist/scanner/chain.js.map +1 -0
- package/dist/scanner/heuristic.d.ts +28 -0
- package/dist/scanner/heuristic.d.ts.map +1 -0
- package/dist/scanner/heuristic.js +375 -0
- package/dist/scanner/heuristic.js.map +1 -0
- package/dist/scanner/pii.d.ts +17 -0
- package/dist/scanner/pii.d.ts.map +1 -0
- package/dist/scanner/pii.js +255 -0
- package/dist/scanner/pii.js.map +1 -0
- package/dist/shield.d.ts +31 -0
- package/dist/shield.d.ts.map +1 -0
- package/dist/shield.js +184 -0
- package/dist/shield.js.map +1 -0
- package/dist/types.d.ts +182 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/package.json +27 -0
- package/src/audit/logger.ts +135 -0
- package/src/audit/schema.sql +51 -0
- package/src/audit/types.ts +16 -0
- package/src/cache/lru.ts +93 -0
- package/src/cost/anomaly.ts +57 -0
- package/src/cost/pricing.ts +58 -0
- package/src/cost/tracker.ts +182 -0
- package/src/index.ts +91 -0
- package/src/policy/engine.ts +163 -0
- package/src/policy/tools.ts +189 -0
- package/src/scanner/canary.ts +30 -0
- package/src/scanner/chain.ts +88 -0
- package/src/scanner/heuristic.ts +427 -0
- package/src/scanner/pii.ts +313 -0
- package/src/shield.ts +228 -0
- package/src/types.ts +242 -0
- package/tsconfig.json +8 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Scanner,
|
|
3
|
+
ScannerResult,
|
|
4
|
+
ScanContext,
|
|
5
|
+
Violation,
|
|
6
|
+
PIIEntity,
|
|
7
|
+
PIIType,
|
|
8
|
+
PIIAction,
|
|
9
|
+
PIIConfig,
|
|
10
|
+
} from "../types.js";
|
|
11
|
+
|
|
12
|
+
// ============================================================
|
|
13
|
+
// PII Scanner — German/EU-first PII Detection
|
|
14
|
+
// Supports: IBAN, Steuernr, Kreditkarte, Email, Phone, IP
|
|
15
|
+
// Modes: block / mask / tokenize
|
|
16
|
+
// ============================================================
|
|
17
|
+
|
|
18
|
+
interface PIIPattern {
|
|
19
|
+
type: PIIType;
|
|
20
|
+
pattern: RegExp;
|
|
21
|
+
validator?: (value: string) => boolean;
|
|
22
|
+
baseConfidence: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// --- German & International PII Patterns ---
|
|
26
|
+
const PII_PATTERNS: PIIPattern[] = [
|
|
27
|
+
// IBAN: DE + 2 check digits + 18 digits (with optional spaces/dashes)
|
|
28
|
+
{
|
|
29
|
+
type: "iban",
|
|
30
|
+
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,
|
|
31
|
+
validator: validateIBAN,
|
|
32
|
+
baseConfidence: 0.95,
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
// Credit card: 4 groups of 4 digits (Luhn-validated)
|
|
36
|
+
{
|
|
37
|
+
type: "credit_card",
|
|
38
|
+
pattern: /\b(?:\d{4}[\s-]?){3}\d{4}\b/g,
|
|
39
|
+
validator: validateLuhn,
|
|
40
|
+
baseConfidence: 0.95,
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
// German tax ID (Steuerliche Identifikationsnummer): 11 digits
|
|
44
|
+
{
|
|
45
|
+
type: "german_tax_id",
|
|
46
|
+
pattern: /\b\d{2}\s?\d{3}\s?\d{3}\s?\d{3}\b/g,
|
|
47
|
+
validator: validateGermanTaxId,
|
|
48
|
+
baseConfidence: 0.70,
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
// German social security number: 2 digits + 6 digits + letter + 3 digits
|
|
52
|
+
{
|
|
53
|
+
type: "german_social_security",
|
|
54
|
+
pattern: /\b\d{2}\s?\d{6}\s?[A-Z]\s?\d{3}\b/g,
|
|
55
|
+
baseConfidence: 0.75,
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
// Email
|
|
59
|
+
{
|
|
60
|
+
type: "email",
|
|
61
|
+
pattern: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g,
|
|
62
|
+
baseConfidence: 0.95,
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
// Phone: German formats (+49, 0xxx) and international
|
|
66
|
+
{
|
|
67
|
+
type: "phone",
|
|
68
|
+
pattern:
|
|
69
|
+
/(?<!\d)(?:\+\d{1,3}|00\d{1,3}|0)\s?[\s\-/]?\(?\d{2,5}\)?[\s\-/]?\d{3,8}[\s\-/]?\d{0,5}\b/g,
|
|
70
|
+
validator: validatePhone,
|
|
71
|
+
baseConfidence: 0.80,
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
// IP addresses (v4)
|
|
75
|
+
{
|
|
76
|
+
type: "ip_address",
|
|
77
|
+
pattern:
|
|
78
|
+
/\b(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\b/g,
|
|
79
|
+
validator: validateIPNotPrivate,
|
|
80
|
+
baseConfidence: 0.85,
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
// URLs with embedded credentials
|
|
84
|
+
{
|
|
85
|
+
type: "url_with_credentials",
|
|
86
|
+
pattern: /https?:\/\/[^:\s]+:[^@\s]+@[^\s]+/g,
|
|
87
|
+
baseConfidence: 0.95,
|
|
88
|
+
},
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
// --- Validators ---
|
|
92
|
+
|
|
93
|
+
function validateIBAN(raw: string): boolean {
|
|
94
|
+
const cleaned = raw.replace(/\s|-/g, "");
|
|
95
|
+
if (cleaned.length < 15 || cleaned.length > 34) return false;
|
|
96
|
+
|
|
97
|
+
// Move first 4 chars to end, convert letters to numbers
|
|
98
|
+
const rearranged = cleaned.substring(4) + cleaned.substring(0, 4);
|
|
99
|
+
const numeric = rearranged.replace(/[A-Z]/g, (c) =>
|
|
100
|
+
String(c.charCodeAt(0) - 55),
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
// Modulo 97 check (handle large numbers via chunking)
|
|
104
|
+
let remainder = numeric;
|
|
105
|
+
while (remainder.length > 2) {
|
|
106
|
+
const chunk = remainder.substring(0, 9);
|
|
107
|
+
remainder =
|
|
108
|
+
String(parseInt(chunk, 10) % 97) + remainder.substring(chunk.length);
|
|
109
|
+
}
|
|
110
|
+
return parseInt(remainder, 10) % 97 === 1;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function validateLuhn(raw: string): boolean {
|
|
114
|
+
const digits = raw.replace(/\D/g, "");
|
|
115
|
+
if (digits.length < 13 || digits.length > 19) return false;
|
|
116
|
+
|
|
117
|
+
let sum = 0;
|
|
118
|
+
for (let i = digits.length - 1; i >= 0; i--) {
|
|
119
|
+
let digit = parseInt(digits[i]!, 10);
|
|
120
|
+
if ((digits.length - i) % 2 === 0) {
|
|
121
|
+
digit *= 2;
|
|
122
|
+
if (digit > 9) digit -= 9;
|
|
123
|
+
}
|
|
124
|
+
sum += digit;
|
|
125
|
+
}
|
|
126
|
+
return sum % 10 === 0;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function validateGermanTaxId(raw: string): boolean {
|
|
130
|
+
const digits = raw.replace(/\s/g, "");
|
|
131
|
+
if (digits.length !== 11) return false;
|
|
132
|
+
if (!/^\d+$/.test(digits)) return false;
|
|
133
|
+
// First digit cannot be 0
|
|
134
|
+
if (digits[0] === "0") return false;
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function validatePhone(raw: string): boolean {
|
|
139
|
+
const digits = raw.replace(/\D/g, "");
|
|
140
|
+
// Must be at least 7 digits, max 15
|
|
141
|
+
return digits.length >= 7 && digits.length <= 15;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function validateIPNotPrivate(raw: string): boolean {
|
|
145
|
+
const parts = raw.split(".").map(Number);
|
|
146
|
+
// Skip private/loopback ranges (not really PII)
|
|
147
|
+
if (parts[0] === 10) return false;
|
|
148
|
+
if (parts[0] === 172 && parts[1]! >= 16 && parts[1]! <= 31) return false;
|
|
149
|
+
if (parts[0] === 192 && parts[1] === 168) return false;
|
|
150
|
+
if (parts[0] === 127) return false;
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// --- Masking ---
|
|
155
|
+
|
|
156
|
+
function maskValue(type: PIIType, value: string): string {
|
|
157
|
+
switch (type) {
|
|
158
|
+
case "email": {
|
|
159
|
+
const atIdx = value.indexOf("@");
|
|
160
|
+
if (atIdx <= 1) return "[EMAIL]";
|
|
161
|
+
return value[0] + "***@" + value.substring(atIdx + 1);
|
|
162
|
+
}
|
|
163
|
+
case "phone":
|
|
164
|
+
return value.substring(0, 4) + "****" + value.substring(value.length - 2);
|
|
165
|
+
case "iban":
|
|
166
|
+
return value.substring(0, 4) + " **** **** ****";
|
|
167
|
+
case "credit_card":
|
|
168
|
+
return "**** **** **** " + value.replace(/\D/g, "").substring(12);
|
|
169
|
+
default:
|
|
170
|
+
return `[${type.toUpperCase()}]`;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// --- PII Scanner Class ---
|
|
175
|
+
|
|
176
|
+
export class PIIScanner implements Scanner {
|
|
177
|
+
readonly name = "pii";
|
|
178
|
+
private patterns: PIIPattern[];
|
|
179
|
+
private action: PIIAction;
|
|
180
|
+
private typeOverrides: Partial<Record<PIIType, PIIAction>>;
|
|
181
|
+
private allowedTypes: Set<PIIType>;
|
|
182
|
+
|
|
183
|
+
constructor(config: PIIConfig = {}) {
|
|
184
|
+
this.patterns = PII_PATTERNS;
|
|
185
|
+
this.action = config.action ?? "mask";
|
|
186
|
+
this.typeOverrides = config.types ?? {};
|
|
187
|
+
this.allowedTypes = new Set(config.allowedTypes ?? []);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async scan(input: string, _context: ScanContext): Promise<ScannerResult> {
|
|
191
|
+
const start = performance.now();
|
|
192
|
+
const entities = this.detect(input);
|
|
193
|
+
const violations: Violation[] = [];
|
|
194
|
+
|
|
195
|
+
// Filter out allowed types
|
|
196
|
+
const activeEntities = entities.filter(
|
|
197
|
+
(e) => !this.allowedTypes.has(e.type),
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
if (activeEntities.length === 0) {
|
|
201
|
+
return {
|
|
202
|
+
decision: "allow",
|
|
203
|
+
violations: [],
|
|
204
|
+
sanitized: input,
|
|
205
|
+
durationMs: performance.now() - start,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Build violations
|
|
210
|
+
let shouldBlock = false;
|
|
211
|
+
for (const entity of activeEntities) {
|
|
212
|
+
const action = this.typeOverrides[entity.type] ?? this.action;
|
|
213
|
+
if (action === "block") shouldBlock = true;
|
|
214
|
+
|
|
215
|
+
violations.push({
|
|
216
|
+
type: "pii_detected",
|
|
217
|
+
scanner: this.name,
|
|
218
|
+
score: entity.confidence,
|
|
219
|
+
threshold: 0,
|
|
220
|
+
message: `${entity.type} detected`,
|
|
221
|
+
detail: `Found ${entity.type} at position ${entity.start}-${entity.end} (action: ${action})`,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Apply masking if needed
|
|
226
|
+
let sanitized = input;
|
|
227
|
+
const effectiveAction = shouldBlock ? "block" : this.action;
|
|
228
|
+
if (effectiveAction === "mask" || effectiveAction === "tokenize") {
|
|
229
|
+
sanitized = this.applyMasking(input, activeEntities);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const decision = shouldBlock ? "block" : "warn";
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
decision,
|
|
236
|
+
violations,
|
|
237
|
+
sanitized,
|
|
238
|
+
durationMs: performance.now() - start,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Detect all PII entities in text */
|
|
243
|
+
detect(text: string): PIIEntity[] {
|
|
244
|
+
const raw: PIIEntity[] = [];
|
|
245
|
+
|
|
246
|
+
for (const piiPattern of this.patterns) {
|
|
247
|
+
// Create fresh regex for each scan (stateful with /g flag)
|
|
248
|
+
const regex = new RegExp(piiPattern.pattern.source, piiPattern.pattern.flags);
|
|
249
|
+
let match: RegExpExecArray | null;
|
|
250
|
+
|
|
251
|
+
while ((match = regex.exec(text)) !== null) {
|
|
252
|
+
const value = match[0];
|
|
253
|
+
const cleaned = value.replace(/[\s-]/g, "");
|
|
254
|
+
|
|
255
|
+
// Run validator if present
|
|
256
|
+
if (piiPattern.validator && !piiPattern.validator(cleaned)) {
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
raw.push({
|
|
261
|
+
type: piiPattern.type,
|
|
262
|
+
value,
|
|
263
|
+
start: match.index,
|
|
264
|
+
end: match.index + value.length,
|
|
265
|
+
confidence: piiPattern.baseConfidence,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return this.deduplicateOverlaps(raw);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** Remove overlapping detections — keep the more specific (higher confidence) match */
|
|
274
|
+
private deduplicateOverlaps(entities: PIIEntity[]): PIIEntity[] {
|
|
275
|
+
if (entities.length <= 1) return entities;
|
|
276
|
+
|
|
277
|
+
// Sort by start position, then by span length descending (longer = more specific)
|
|
278
|
+
const sorted = [...entities].sort((a, b) =>
|
|
279
|
+
a.start !== b.start ? a.start - b.start : (b.end - b.start) - (a.end - a.start),
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
const kept: PIIEntity[] = [];
|
|
283
|
+
for (const entity of sorted) {
|
|
284
|
+
// Check if this entity overlaps with any already-kept entity
|
|
285
|
+
const overlaps = kept.some(
|
|
286
|
+
(k) => entity.start < k.end && entity.end > k.start,
|
|
287
|
+
);
|
|
288
|
+
if (!overlaps) {
|
|
289
|
+
kept.push(entity);
|
|
290
|
+
}
|
|
291
|
+
// If it overlaps, the already-kept entity wins (it appeared first in pattern order = more specific)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return kept;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** Mask detected PII in text */
|
|
298
|
+
private applyMasking(text: string, entities: PIIEntity[]): string {
|
|
299
|
+
// Sort by position descending to preserve offsets
|
|
300
|
+
const sorted = [...entities].sort((a, b) => b.start - a.start);
|
|
301
|
+
let masked = text;
|
|
302
|
+
|
|
303
|
+
for (const entity of sorted) {
|
|
304
|
+
const replacement = maskValue(entity.type, entity.value);
|
|
305
|
+
masked =
|
|
306
|
+
masked.substring(0, entity.start) +
|
|
307
|
+
replacement +
|
|
308
|
+
masked.substring(entity.end);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return masked;
|
|
312
|
+
}
|
|
313
|
+
}
|
package/src/shield.ts
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import type { ShieldConfig, ScanResult, ScanContext, ToolPolicy } from "./types.js";
|
|
2
|
+
import { ScannerChain } from "./scanner/chain.js";
|
|
3
|
+
import { HeuristicScanner } from "./scanner/heuristic.js";
|
|
4
|
+
import { PIIScanner } from "./scanner/pii.js";
|
|
5
|
+
import { ToolPolicyScanner } from "./policy/tools.js";
|
|
6
|
+
import { PolicyEngine } from "./policy/engine.js";
|
|
7
|
+
import { CostTracker } from "./cost/tracker.js";
|
|
8
|
+
import { AuditLogger, ConsoleAuditStore } from "./audit/logger.js";
|
|
9
|
+
import type { AuditStore } from "./audit/types.js";
|
|
10
|
+
import { ScanLRUCache } from "./cache/lru.js";
|
|
11
|
+
|
|
12
|
+
// ============================================================
|
|
13
|
+
// AIShield — Main class, single entry point
|
|
14
|
+
// ============================================================
|
|
15
|
+
|
|
16
|
+
export class AIShield {
|
|
17
|
+
private chain: ScannerChain;
|
|
18
|
+
private policyEngine: PolicyEngine;
|
|
19
|
+
private costTracker: CostTracker | null;
|
|
20
|
+
private auditLogger: AuditLogger | null;
|
|
21
|
+
private scanCache: ScanLRUCache<ScanResult> | null;
|
|
22
|
+
private config: ShieldConfig;
|
|
23
|
+
|
|
24
|
+
constructor(config: ShieldConfig = {}) {
|
|
25
|
+
this.config = config;
|
|
26
|
+
this.policyEngine = new PolicyEngine(config.preset ?? "public_website");
|
|
27
|
+
this.chain = new ScannerChain({ earlyExit: true });
|
|
28
|
+
|
|
29
|
+
// Build scanner chain based on config
|
|
30
|
+
this.setupScanners(config);
|
|
31
|
+
|
|
32
|
+
// Cost tracker (optional, needs Redis for distributed use)
|
|
33
|
+
this.costTracker = config.cost?.enabled !== false && config.cost?.budgets
|
|
34
|
+
? new CostTracker(config.cost.budgets)
|
|
35
|
+
: null;
|
|
36
|
+
|
|
37
|
+
// Audit logger (optional)
|
|
38
|
+
this.auditLogger = this.setupAudit(config);
|
|
39
|
+
|
|
40
|
+
// Scan cache (enabled when cache config is provided)
|
|
41
|
+
this.scanCache = config.cache && config.cache.enabled !== false
|
|
42
|
+
? new ScanLRUCache<ScanResult>({
|
|
43
|
+
maxSize: config.cache.maxSize,
|
|
44
|
+
ttlMs: config.cache.ttlMs,
|
|
45
|
+
})
|
|
46
|
+
: null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Scan input text — the main API */
|
|
50
|
+
async scan(input: string, context: ScanContext = {}): Promise<ScanResult> {
|
|
51
|
+
// Apply preset if not set in context
|
|
52
|
+
if (!context.preset) {
|
|
53
|
+
context.preset = this.config.preset ?? "public_website";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Check cache
|
|
57
|
+
if (this.scanCache) {
|
|
58
|
+
const cacheKey = this.buildCacheKey(input, context);
|
|
59
|
+
const cached = this.scanCache.get(cacheKey);
|
|
60
|
+
if (cached) {
|
|
61
|
+
return { ...cached, meta: { ...cached.meta, cached: true } };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const result = await this.chain.run(input, context);
|
|
66
|
+
|
|
67
|
+
// Store in cache
|
|
68
|
+
if (this.scanCache) {
|
|
69
|
+
const cacheKey = this.buildCacheKey(input, context);
|
|
70
|
+
this.scanCache.set(cacheKey, result);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Log to audit if enabled
|
|
74
|
+
if (this.auditLogger) {
|
|
75
|
+
void this.auditLogger.log(input, result, context);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Check cost budget before making an LLM call */
|
|
82
|
+
async checkBudget(
|
|
83
|
+
entityId: string,
|
|
84
|
+
model: string,
|
|
85
|
+
estimatedInputTokens: number,
|
|
86
|
+
estimatedOutputTokens?: number,
|
|
87
|
+
) {
|
|
88
|
+
if (!this.costTracker) {
|
|
89
|
+
return { allowed: true, currentSpend: 0, remainingBudget: Infinity };
|
|
90
|
+
}
|
|
91
|
+
return this.costTracker.checkBudget(
|
|
92
|
+
entityId,
|
|
93
|
+
model,
|
|
94
|
+
estimatedInputTokens,
|
|
95
|
+
estimatedOutputTokens,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Record cost after receiving LLM response */
|
|
100
|
+
async recordCost(
|
|
101
|
+
entityId: string,
|
|
102
|
+
model: string,
|
|
103
|
+
inputTokens: number,
|
|
104
|
+
outputTokens: number,
|
|
105
|
+
) {
|
|
106
|
+
if (!this.costTracker) return null;
|
|
107
|
+
return this.costTracker.recordCost(entityId, model, inputTokens, outputTokens);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Get current spend for an entity */
|
|
111
|
+
async getCurrentSpend(entityId: string): Promise<number> {
|
|
112
|
+
if (!this.costTracker) return 0;
|
|
113
|
+
return this.costTracker.getCurrentSpend(entityId);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Get the policy engine */
|
|
117
|
+
getPolicy(): PolicyEngine {
|
|
118
|
+
return this.policyEngine;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Clear the scan cache */
|
|
122
|
+
clearCache(): void {
|
|
123
|
+
this.scanCache?.clear();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Get cache stats */
|
|
127
|
+
get cacheSize(): number {
|
|
128
|
+
return this.scanCache?.size ?? 0;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Graceful shutdown */
|
|
132
|
+
async close(): Promise<void> {
|
|
133
|
+
this.scanCache?.clear();
|
|
134
|
+
if (this.auditLogger) {
|
|
135
|
+
await this.auditLogger.close();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// --- Private setup ---
|
|
140
|
+
|
|
141
|
+
private buildCacheKey(input: string, context: ScanContext): string {
|
|
142
|
+
// Include preset + tool names in key since they affect scan results
|
|
143
|
+
const parts = [context.preset ?? "default"];
|
|
144
|
+
if (context.tools?.length) {
|
|
145
|
+
parts.push(context.tools.map((t) => t.name).sort().join(","));
|
|
146
|
+
}
|
|
147
|
+
parts.push(input);
|
|
148
|
+
return parts.join("\x00");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private setupScanners(config: ShieldConfig): void {
|
|
152
|
+
// 1. Heuristic injection scanner (always on unless explicitly disabled)
|
|
153
|
+
if (config.injection?.enabled !== false) {
|
|
154
|
+
const preset = this.policyEngine.getPreset();
|
|
155
|
+
this.chain.add(
|
|
156
|
+
new HeuristicScanner({
|
|
157
|
+
strictness: config.injection?.strictness ?? "medium",
|
|
158
|
+
threshold: config.injection?.threshold ?? preset.injection.threshold,
|
|
159
|
+
customPatterns: config.injection?.customPatterns?.map((pattern, i) => ({
|
|
160
|
+
id: `CUSTOM-${i + 1}`,
|
|
161
|
+
category: "instruction_override" as const,
|
|
162
|
+
pattern,
|
|
163
|
+
weight: 0.25,
|
|
164
|
+
description: `Custom pattern #${i + 1}`,
|
|
165
|
+
})),
|
|
166
|
+
}),
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// 2. PII scanner
|
|
171
|
+
if (config.pii?.enabled !== false) {
|
|
172
|
+
this.chain.add(
|
|
173
|
+
new PIIScanner({
|
|
174
|
+
action: config.pii?.action ?? this.policyEngine.getPIIAction(),
|
|
175
|
+
locale: config.pii?.locale,
|
|
176
|
+
types: config.pii?.types,
|
|
177
|
+
allowedTypes: config.pii?.allowedTypes,
|
|
178
|
+
}),
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// 3. Tool policy scanner
|
|
183
|
+
if (config.tools?.enabled !== false && config.tools?.policies) {
|
|
184
|
+
const toolPolicy: ToolPolicy = {
|
|
185
|
+
permissions: config.tools.policies,
|
|
186
|
+
global: {
|
|
187
|
+
dangerousPatterns:
|
|
188
|
+
config.tools.globalDangerousPatterns ??
|
|
189
|
+
this.policyEngine.getDangerousToolPatterns(),
|
|
190
|
+
maxToolChainDepth:
|
|
191
|
+
config.tools.maxToolChainDepth ??
|
|
192
|
+
this.policyEngine.getMaxToolChainDepth(),
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
this.chain.add(
|
|
196
|
+
new ToolPolicyScanner(toolPolicy, config.tools.manifestPins),
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private setupAudit(config: ShieldConfig): AuditLogger | null {
|
|
202
|
+
if (config.audit?.enabled === false) return null;
|
|
203
|
+
|
|
204
|
+
let store: AuditStore;
|
|
205
|
+
switch (config.audit?.store) {
|
|
206
|
+
case "console":
|
|
207
|
+
store = new ConsoleAuditStore();
|
|
208
|
+
break;
|
|
209
|
+
case "postgresql":
|
|
210
|
+
// PostgreSQL store would be imported separately to keep core lightweight
|
|
211
|
+
// For now, fall through to console
|
|
212
|
+
store = new ConsoleAuditStore();
|
|
213
|
+
break;
|
|
214
|
+
case "memory":
|
|
215
|
+
default:
|
|
216
|
+
// If no store configured and audit not explicitly enabled, skip
|
|
217
|
+
if (!config.audit?.store && config.audit?.enabled !== true) return null;
|
|
218
|
+
store = new ConsoleAuditStore();
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return new AuditLogger({
|
|
223
|
+
store,
|
|
224
|
+
batchSize: config.audit?.batchSize,
|
|
225
|
+
flushIntervalMs: config.audit?.flushIntervalMs,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|