agentshield-sdk 7.0.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/CHANGELOG.md +191 -0
- package/LICENSE +21 -0
- package/README.md +975 -0
- package/bin/agent-shield.js +680 -0
- package/package.json +118 -0
- package/src/adaptive.js +330 -0
- package/src/agent-protocol.js +998 -0
- package/src/alert-tuning.js +480 -0
- package/src/allowlist.js +603 -0
- package/src/audit-immutable.js +914 -0
- package/src/audit-streaming.js +469 -0
- package/src/badges.js +196 -0
- package/src/behavior-profiling.js +289 -0
- package/src/benchmark-harness.js +804 -0
- package/src/canary.js +271 -0
- package/src/certification.js +563 -0
- package/src/circuit-breaker.js +321 -0
- package/src/compliance.js +617 -0
- package/src/confidence-tuning.js +324 -0
- package/src/confused-deputy.js +624 -0
- package/src/context-scoring.js +360 -0
- package/src/conversation.js +494 -0
- package/src/cost-optimizer.js +1024 -0
- package/src/ctf.js +462 -0
- package/src/detector-core.js +1999 -0
- package/src/distributed.js +359 -0
- package/src/document-scanner.js +795 -0
- package/src/embedding.js +307 -0
- package/src/encoding.js +429 -0
- package/src/enterprise.js +405 -0
- package/src/errors.js +100 -0
- package/src/eu-ai-act.js +523 -0
- package/src/fuzzer.js +764 -0
- package/src/honeypot.js +328 -0
- package/src/i18n-patterns.js +523 -0
- package/src/index.js +430 -0
- package/src/integrations.js +528 -0
- package/src/llm-redteam.js +670 -0
- package/src/main.js +741 -0
- package/src/main.mjs +38 -0
- package/src/mcp-bridge.js +542 -0
- package/src/mcp-certification.js +846 -0
- package/src/mcp-sdk-integration.js +355 -0
- package/src/mcp-security-runtime.js +741 -0
- package/src/mcp-server.js +740 -0
- package/src/middleware.js +208 -0
- package/src/model-finetuning.js +884 -0
- package/src/model-fingerprint.js +1042 -0
- package/src/multi-agent-trust.js +453 -0
- package/src/multi-agent.js +404 -0
- package/src/multimodal.js +296 -0
- package/src/nist-mapping.js +505 -0
- package/src/observability.js +330 -0
- package/src/openclaw.js +450 -0
- package/src/otel.js +544 -0
- package/src/owasp-2025.js +483 -0
- package/src/pii.js +390 -0
- package/src/plugin-marketplace.js +628 -0
- package/src/plugin-system.js +349 -0
- package/src/policy-dsl.js +775 -0
- package/src/policy-extended.js +635 -0
- package/src/policy.js +443 -0
- package/src/presets.js +409 -0
- package/src/production.js +557 -0
- package/src/prompt-leakage.js +321 -0
- package/src/rag-vulnerability.js +579 -0
- package/src/redteam.js +475 -0
- package/src/response-handler.js +429 -0
- package/src/scanners.js +357 -0
- package/src/self-healing.js +363 -0
- package/src/semantic.js +339 -0
- package/src/shield-score.js +250 -0
- package/src/sso-saml.js +897 -0
- package/src/stream-scanner.js +806 -0
- package/src/testing.js +505 -0
- package/src/threat-encyclopedia.js +629 -0
- package/src/threat-intel-network.js +1017 -0
- package/src/token-analysis.js +467 -0
- package/src/tool-guard.js +412 -0
- package/src/tool-output-validator.js +354 -0
- package/src/utils.js +83 -0
- package/src/watermark.js +235 -0
- package/src/worker-scanner.js +601 -0
- package/types/index.d.ts +2088 -0
|
@@ -0,0 +1,775 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Agent Shield — Policy-as-Code DSL
|
|
5
|
+
*
|
|
6
|
+
* A domain-specific language for writing shield policies.
|
|
7
|
+
* Like Rego for OPA, but for prompt injection rules.
|
|
8
|
+
*
|
|
9
|
+
* Example:
|
|
10
|
+
* policy "strict" {
|
|
11
|
+
* severity minimum "high"
|
|
12
|
+
* rule "no-injection" {
|
|
13
|
+
* when input matches "ignore.*previous.*instructions"
|
|
14
|
+
* then block with severity "critical"
|
|
15
|
+
* }
|
|
16
|
+
* }
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
// =========================================================================
|
|
20
|
+
// BUILTIN FUNCTIONS
|
|
21
|
+
// =========================================================================
|
|
22
|
+
|
|
23
|
+
/** @type {Object<string, function>} */
|
|
24
|
+
const BUILTIN_FUNCTIONS = {
|
|
25
|
+
matches: (() => {
|
|
26
|
+
const cache = new Map();
|
|
27
|
+
return (text, pattern) => {
|
|
28
|
+
let re = cache.get(pattern);
|
|
29
|
+
if (!re) { re = new RegExp(pattern, 'i'); cache.set(pattern, re); if (cache.size > 200) cache.clear(); }
|
|
30
|
+
return re.test(text);
|
|
31
|
+
};
|
|
32
|
+
})(),
|
|
33
|
+
contains: (text, substr) => typeof text === 'string' && text.toLowerCase().includes(substr.toLowerCase()),
|
|
34
|
+
starts_with: (text, prefix) => typeof text === 'string' && text.toLowerCase().startsWith(prefix.toLowerCase()),
|
|
35
|
+
ends_with: (text, suffix) => typeof text === 'string' && text.toLowerCase().endsWith(suffix.toLowerCase()),
|
|
36
|
+
length: (text) => typeof text === 'string' ? text.length : 0,
|
|
37
|
+
lower: (text) => typeof text === 'string' ? text.toLowerCase() : '',
|
|
38
|
+
upper: (text) => typeof text === 'string' ? text.toUpperCase() : '',
|
|
39
|
+
hash: (text) => {
|
|
40
|
+
let h = 0;
|
|
41
|
+
for (let i = 0; i < text.length; i++) {
|
|
42
|
+
h = ((h << 5) - h + text.charCodeAt(i)) | 0;
|
|
43
|
+
}
|
|
44
|
+
return h.toString(16);
|
|
45
|
+
},
|
|
46
|
+
now: () => Date.now(),
|
|
47
|
+
severity_gte: (a, b) => {
|
|
48
|
+
const order = { low: 1, medium: 2, high: 3, critical: 4 };
|
|
49
|
+
return (order[a] || 0) >= (order[b] || 0);
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// =========================================================================
|
|
54
|
+
// TOKEN TYPES
|
|
55
|
+
// =========================================================================
|
|
56
|
+
|
|
57
|
+
const TOKEN_TYPES = {
|
|
58
|
+
KEYWORD: 'keyword',
|
|
59
|
+
STRING: 'string',
|
|
60
|
+
NUMBER: 'number',
|
|
61
|
+
IDENTIFIER: 'identifier',
|
|
62
|
+
BLOCK_OPEN: 'block_open',
|
|
63
|
+
BLOCK_CLOSE: 'block_close',
|
|
64
|
+
OPERATOR: 'operator',
|
|
65
|
+
NEWLINE: 'newline',
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const KEYWORDS = new Set([
|
|
69
|
+
'policy', 'rule', 'when', 'then', 'and', 'or', 'not',
|
|
70
|
+
'allow', 'block', 'warn', 'with', 'severity', 'minimum',
|
|
71
|
+
'message', 'rate_limit', 'per', 'scan_mode', 'on_threat',
|
|
72
|
+
'input', 'source', 'metadata', 'is', 'matches', 'contains',
|
|
73
|
+
'starts_with', 'ends_with', 'greater_than', 'less_than', 'true', 'false',
|
|
74
|
+
]);
|
|
75
|
+
|
|
76
|
+
// =========================================================================
|
|
77
|
+
// POLICY PARSER
|
|
78
|
+
// =========================================================================
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Tokenizer and recursive descent parser for the policy DSL.
|
|
82
|
+
*/
|
|
83
|
+
class PolicyParser {
|
|
84
|
+
constructor() {
|
|
85
|
+
this._tokens = [];
|
|
86
|
+
this._pos = 0;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Tokenize DSL source into tokens.
|
|
91
|
+
* @param {string} source
|
|
92
|
+
* @returns {Array<{type: string, value: string, line: number, col: number}>}
|
|
93
|
+
*/
|
|
94
|
+
tokenize(source) {
|
|
95
|
+
const tokens = [];
|
|
96
|
+
const lines = source.split('\n');
|
|
97
|
+
|
|
98
|
+
for (let lineNum = 0; lineNum < lines.length; lineNum++) {
|
|
99
|
+
let line = lines[lineNum];
|
|
100
|
+
// Strip comments
|
|
101
|
+
const commentIdx = line.indexOf('#');
|
|
102
|
+
if (commentIdx >= 0) line = line.substring(0, commentIdx);
|
|
103
|
+
line = line.trim();
|
|
104
|
+
if (!line) continue;
|
|
105
|
+
|
|
106
|
+
let col = 0;
|
|
107
|
+
let i = 0;
|
|
108
|
+
while (i < line.length) {
|
|
109
|
+
// Skip whitespace
|
|
110
|
+
if (/\s/.test(line[i])) { i++; col++; continue; }
|
|
111
|
+
|
|
112
|
+
// Block delimiters
|
|
113
|
+
if (line[i] === '{') {
|
|
114
|
+
tokens.push({ type: TOKEN_TYPES.BLOCK_OPEN, value: '{', line: lineNum + 1, col });
|
|
115
|
+
i++; col++; continue;
|
|
116
|
+
}
|
|
117
|
+
if (line[i] === '}') {
|
|
118
|
+
tokens.push({ type: TOKEN_TYPES.BLOCK_CLOSE, value: '}', line: lineNum + 1, col });
|
|
119
|
+
i++; col++; continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Strings
|
|
123
|
+
if (line[i] === '"') {
|
|
124
|
+
let str = '';
|
|
125
|
+
i++; col++;
|
|
126
|
+
while (i < line.length && line[i] !== '"') {
|
|
127
|
+
if (line[i] === '\\' && i + 1 < line.length) { str += line[i + 1]; i += 2; col += 2; }
|
|
128
|
+
else { str += line[i]; i++; col++; }
|
|
129
|
+
}
|
|
130
|
+
i++; col++; // closing quote
|
|
131
|
+
tokens.push({ type: TOKEN_TYPES.STRING, value: str, line: lineNum + 1, col });
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Numbers
|
|
136
|
+
if (/\d/.test(line[i])) {
|
|
137
|
+
let num = '';
|
|
138
|
+
while (i < line.length && /[\d.]/.test(line[i])) { num += line[i]; i++; col++; }
|
|
139
|
+
tokens.push({ type: TOKEN_TYPES.NUMBER, value: num, line: lineNum + 1, col });
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Dot operator
|
|
144
|
+
if (line[i] === '.') {
|
|
145
|
+
tokens.push({ type: TOKEN_TYPES.OPERATOR, value: '.', line: lineNum + 1, col });
|
|
146
|
+
i++; col++; continue;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Identifiers / keywords
|
|
150
|
+
if (/[a-zA-Z_]/.test(line[i])) {
|
|
151
|
+
let word = '';
|
|
152
|
+
while (i < line.length && /[a-zA-Z0-9_]/.test(line[i])) { word += line[i]; i++; col++; }
|
|
153
|
+
const type = KEYWORDS.has(word) ? TOKEN_TYPES.KEYWORD : TOKEN_TYPES.IDENTIFIER;
|
|
154
|
+
tokens.push({ type, value: word, line: lineNum + 1, col });
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Skip unknown
|
|
159
|
+
i++; col++;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return tokens;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Parse tokens into an AST.
|
|
168
|
+
* @param {Array} tokens
|
|
169
|
+
* @returns {{type: string, policies: Array}}
|
|
170
|
+
*/
|
|
171
|
+
parse(tokens) {
|
|
172
|
+
this._tokens = tokens;
|
|
173
|
+
this._pos = 0;
|
|
174
|
+
const policies = [];
|
|
175
|
+
|
|
176
|
+
while (this._pos < this._tokens.length) {
|
|
177
|
+
if (this._peek('policy')) {
|
|
178
|
+
policies.push(this._parsePolicy());
|
|
179
|
+
} else {
|
|
180
|
+
this._pos++;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return { type: 'Program', policies };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** @private */
|
|
188
|
+
_peek(value) {
|
|
189
|
+
return this._pos < this._tokens.length && this._tokens[this._pos].value === value;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** @private */
|
|
193
|
+
_consume(value) {
|
|
194
|
+
if (this._pos >= this._tokens.length) throw new Error(`Unexpected end of input, expected '${value}'`);
|
|
195
|
+
const token = this._tokens[this._pos];
|
|
196
|
+
if (value && token.value !== value) {
|
|
197
|
+
throw new Error(`Expected '${value}' at line ${token.line}, got '${token.value}'`);
|
|
198
|
+
}
|
|
199
|
+
this._pos++;
|
|
200
|
+
return token;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** @private */
|
|
204
|
+
_consumeType(type) {
|
|
205
|
+
if (this._pos >= this._tokens.length) throw new Error(`Unexpected end of input, expected ${type}`);
|
|
206
|
+
const token = this._tokens[this._pos];
|
|
207
|
+
if (token.type !== type) throw new Error(`Expected ${type} at line ${token.line}, got ${token.type}`);
|
|
208
|
+
this._pos++;
|
|
209
|
+
return token;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** @private */
|
|
213
|
+
_parsePolicy() {
|
|
214
|
+
this._consume('policy');
|
|
215
|
+
const name = this._consumeType(TOKEN_TYPES.STRING);
|
|
216
|
+
this._consume('{');
|
|
217
|
+
|
|
218
|
+
const rules = [];
|
|
219
|
+
const allows = [];
|
|
220
|
+
const config = {};
|
|
221
|
+
|
|
222
|
+
while (!this._peek('}') && this._pos < this._tokens.length) {
|
|
223
|
+
if (this._peek('rule')) {
|
|
224
|
+
rules.push(this._parseRule());
|
|
225
|
+
} else if (this._peek('allow')) {
|
|
226
|
+
allows.push(this._parseAllow());
|
|
227
|
+
} else if (this._peek('severity')) {
|
|
228
|
+
this._consume('severity');
|
|
229
|
+
if (this._peek('minimum')) this._consume('minimum');
|
|
230
|
+
config.minSeverity = this._consumeType(TOKEN_TYPES.STRING).value;
|
|
231
|
+
} else if (this._peek('block')) {
|
|
232
|
+
this._consume('block');
|
|
233
|
+
this._consume('on_threat');
|
|
234
|
+
config.blockOnThreat = this._tokens[this._pos].value === 'true';
|
|
235
|
+
this._pos++;
|
|
236
|
+
} else if (this._peek('rate_limit')) {
|
|
237
|
+
this._consume('rate_limit');
|
|
238
|
+
config.rateLimit = parseInt(this._consumeType(TOKEN_TYPES.NUMBER).value);
|
|
239
|
+
this._consume('per');
|
|
240
|
+
config.rateLimitPeriod = this._consumeType(TOKEN_TYPES.STRING).value;
|
|
241
|
+
} else if (this._peek('scan_mode')) {
|
|
242
|
+
this._consume('scan_mode');
|
|
243
|
+
config.scanMode = this._consumeType(TOKEN_TYPES.STRING).value;
|
|
244
|
+
} else {
|
|
245
|
+
this._pos++;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
this._consume('}');
|
|
250
|
+
return { type: 'Policy', name: name.value, rules, allows, config };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** @private */
|
|
254
|
+
_parseRule() {
|
|
255
|
+
this._consume('rule');
|
|
256
|
+
const name = this._consumeType(TOKEN_TYPES.STRING);
|
|
257
|
+
this._consume('{');
|
|
258
|
+
|
|
259
|
+
const conditions = [];
|
|
260
|
+
let action = 'block';
|
|
261
|
+
let severity = 'high';
|
|
262
|
+
let message = '';
|
|
263
|
+
|
|
264
|
+
while (!this._peek('}') && this._pos < this._tokens.length) {
|
|
265
|
+
if (this._peek('when')) {
|
|
266
|
+
this._consume('when');
|
|
267
|
+
conditions.push(this._parseCondition());
|
|
268
|
+
} else if (this._peek('and')) {
|
|
269
|
+
this._consume('and');
|
|
270
|
+
conditions.push(this._parseCondition());
|
|
271
|
+
} else if (this._peek('then')) {
|
|
272
|
+
this._consume('then');
|
|
273
|
+
action = this._tokens[this._pos].value;
|
|
274
|
+
this._pos++;
|
|
275
|
+
if (this._peek('with')) {
|
|
276
|
+
this._consume('with');
|
|
277
|
+
this._consume('severity');
|
|
278
|
+
severity = this._consumeType(TOKEN_TYPES.STRING).value;
|
|
279
|
+
}
|
|
280
|
+
} else if (this._peek('message')) {
|
|
281
|
+
this._consume('message');
|
|
282
|
+
message = this._consumeType(TOKEN_TYPES.STRING).value;
|
|
283
|
+
} else {
|
|
284
|
+
this._pos++;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
this._consume('}');
|
|
289
|
+
return { type: 'Rule', name: name.value, conditions, action, severity, message };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** @private */
|
|
293
|
+
_parseCondition() {
|
|
294
|
+
const subject = this._tokens[this._pos].value;
|
|
295
|
+
this._pos++;
|
|
296
|
+
|
|
297
|
+
// Handle property access (e.g., input.length)
|
|
298
|
+
let property = null;
|
|
299
|
+
if (this._peek('.')) {
|
|
300
|
+
this._consume('.');
|
|
301
|
+
property = this._tokens[this._pos].value;
|
|
302
|
+
this._pos++;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const operator = this._tokens[this._pos].value;
|
|
306
|
+
this._pos++;
|
|
307
|
+
|
|
308
|
+
const value = this._tokens[this._pos];
|
|
309
|
+
this._pos++;
|
|
310
|
+
|
|
311
|
+
return {
|
|
312
|
+
type: 'Condition',
|
|
313
|
+
subject,
|
|
314
|
+
property,
|
|
315
|
+
operator,
|
|
316
|
+
value: value.value,
|
|
317
|
+
valueType: value.type,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** @private */
|
|
322
|
+
_parseAllow() {
|
|
323
|
+
this._consume('allow');
|
|
324
|
+
this._consume('{');
|
|
325
|
+
|
|
326
|
+
const conditions = [];
|
|
327
|
+
const logic = []; // 'and' or 'or' between conditions
|
|
328
|
+
|
|
329
|
+
while (!this._peek('}') && this._pos < this._tokens.length) {
|
|
330
|
+
if (this._peek('when')) {
|
|
331
|
+
this._consume('when');
|
|
332
|
+
conditions.push(this._parseCondition());
|
|
333
|
+
} else if (this._peek('and')) {
|
|
334
|
+
this._consume('and');
|
|
335
|
+
logic.push('and');
|
|
336
|
+
conditions.push(this._parseCondition());
|
|
337
|
+
} else if (this._peek('or')) {
|
|
338
|
+
this._consume('or');
|
|
339
|
+
logic.push('or');
|
|
340
|
+
conditions.push(this._parseCondition());
|
|
341
|
+
} else {
|
|
342
|
+
this._pos++;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
this._consume('}');
|
|
347
|
+
return { type: 'Allow', conditions, logic };
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// =========================================================================
|
|
352
|
+
// POLICY COMPILER
|
|
353
|
+
// =========================================================================
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Compiles an AST into executable policy functions.
|
|
357
|
+
*/
|
|
358
|
+
class PolicyCompiler {
|
|
359
|
+
constructor() {}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Compile an AST into a CompiledPolicy.
|
|
363
|
+
* @param {{type: string, policies: Array}} ast
|
|
364
|
+
* @returns {Array<{name: string, rules: Array, allows: Array, config: object}>}
|
|
365
|
+
*/
|
|
366
|
+
compile(ast) {
|
|
367
|
+
return ast.policies.map(p => this._compilePolicy(p));
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** @private */
|
|
371
|
+
_compilePolicy(policyNode) {
|
|
372
|
+
const rules = policyNode.rules.map(r => this._compileRule(r));
|
|
373
|
+
const allows = policyNode.allows.map(a => this._compileAllow(a));
|
|
374
|
+
return {
|
|
375
|
+
name: policyNode.name,
|
|
376
|
+
config: policyNode.config,
|
|
377
|
+
rules,
|
|
378
|
+
allows,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** @private */
|
|
383
|
+
_compileRule(ruleNode) {
|
|
384
|
+
const predicates = ruleNode.conditions.map(c => this._compileCondition(c));
|
|
385
|
+
return {
|
|
386
|
+
name: ruleNode.name,
|
|
387
|
+
action: ruleNode.action,
|
|
388
|
+
severity: ruleNode.severity,
|
|
389
|
+
message: ruleNode.message,
|
|
390
|
+
test: (context) => predicates.every(pred => pred(context)),
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** @private */
|
|
395
|
+
_compileAllow(allowNode) {
|
|
396
|
+
const predicates = allowNode.conditions.map(c => this._compileCondition(c));
|
|
397
|
+
const logic = allowNode.logic;
|
|
398
|
+
|
|
399
|
+
return {
|
|
400
|
+
test: (context) => {
|
|
401
|
+
if (predicates.length === 0) return true;
|
|
402
|
+
let result = predicates[0](context);
|
|
403
|
+
for (let i = 1; i < predicates.length; i++) {
|
|
404
|
+
const op = logic[i - 1] || 'and';
|
|
405
|
+
if (op === 'or') result = result || predicates[i](context);
|
|
406
|
+
else result = result && predicates[i](context);
|
|
407
|
+
}
|
|
408
|
+
return result;
|
|
409
|
+
},
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/** @private */
|
|
414
|
+
_compileCondition(condNode) {
|
|
415
|
+
const { subject, property, operator, value, valueType } = condNode;
|
|
416
|
+
|
|
417
|
+
return (context) => {
|
|
418
|
+
let subjectValue;
|
|
419
|
+
if (subject === 'input') {
|
|
420
|
+
subjectValue = property ? this._getProperty(context.input, property) : context.input;
|
|
421
|
+
} else if (subject === 'source') {
|
|
422
|
+
subjectValue = context.source || '';
|
|
423
|
+
} else if (subject === 'metadata') {
|
|
424
|
+
subjectValue = property ? (context.metadata || {})[property] : context.metadata;
|
|
425
|
+
} else {
|
|
426
|
+
subjectValue = context[subject];
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const compareValue = valueType === TOKEN_TYPES.NUMBER ? parseFloat(value)
|
|
430
|
+
: value === 'true' ? true
|
|
431
|
+
: value === 'false' ? false
|
|
432
|
+
: value;
|
|
433
|
+
|
|
434
|
+
switch (operator) {
|
|
435
|
+
case 'matches': return BUILTIN_FUNCTIONS.matches(String(subjectValue), String(compareValue));
|
|
436
|
+
case 'contains': return BUILTIN_FUNCTIONS.contains(String(subjectValue), String(compareValue));
|
|
437
|
+
case 'is': return subjectValue === compareValue;
|
|
438
|
+
case 'starts_with': return BUILTIN_FUNCTIONS.starts_with(String(subjectValue), String(compareValue));
|
|
439
|
+
case 'ends_with': return BUILTIN_FUNCTIONS.ends_with(String(subjectValue), String(compareValue));
|
|
440
|
+
case 'greater_than': return Number(subjectValue) > Number(compareValue);
|
|
441
|
+
case 'less_than': return Number(subjectValue) < Number(compareValue);
|
|
442
|
+
default: return false;
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/** @private */
|
|
448
|
+
_getProperty(obj, prop) {
|
|
449
|
+
if (typeof obj === 'string') {
|
|
450
|
+
if (prop === 'length') return obj.length;
|
|
451
|
+
}
|
|
452
|
+
if (obj && typeof obj === 'object') return obj[prop];
|
|
453
|
+
return undefined;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// =========================================================================
|
|
458
|
+
// POLICY RUNTIME
|
|
459
|
+
// =========================================================================
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Executes compiled policies against scan contexts.
|
|
463
|
+
*/
|
|
464
|
+
class PolicyRuntime {
|
|
465
|
+
/**
|
|
466
|
+
* @param {object} [builtins]
|
|
467
|
+
*/
|
|
468
|
+
constructor(builtins) {
|
|
469
|
+
this._functions = { ...BUILTIN_FUNCTIONS, ...builtins };
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Register a custom function.
|
|
474
|
+
* @param {string} name
|
|
475
|
+
* @param {function} fn
|
|
476
|
+
*/
|
|
477
|
+
registerFunction(name, fn) {
|
|
478
|
+
this._functions[name] = fn;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Execute a compiled policy against a context.
|
|
483
|
+
* @param {object} compiledPolicy
|
|
484
|
+
* @param {{input: string, source?: string, metadata?: object}} context
|
|
485
|
+
* @returns {{action: string, reason: string, severity: string, matched_rules: string[]}}
|
|
486
|
+
*/
|
|
487
|
+
execute(compiledPolicy, context) {
|
|
488
|
+
// Check allow rules first
|
|
489
|
+
for (const allow of compiledPolicy.allows) {
|
|
490
|
+
if (allow.test(context)) {
|
|
491
|
+
return { action: 'allow', reason: 'Matched allow rule', severity: 'safe', matched_rules: [] };
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Check deny rules
|
|
496
|
+
const matchedRules = [];
|
|
497
|
+
let maxSeverity = 'safe';
|
|
498
|
+
let action = 'allow';
|
|
499
|
+
let reason = '';
|
|
500
|
+
const severityOrder = { safe: 0, low: 1, medium: 2, high: 3, critical: 4 };
|
|
501
|
+
|
|
502
|
+
for (const rule of compiledPolicy.rules) {
|
|
503
|
+
if (rule.test(context)) {
|
|
504
|
+
matchedRules.push(rule.name);
|
|
505
|
+
if (severityOrder[rule.severity] > severityOrder[maxSeverity]) {
|
|
506
|
+
maxSeverity = rule.severity;
|
|
507
|
+
}
|
|
508
|
+
action = rule.action;
|
|
509
|
+
reason = rule.message || `Matched rule: ${rule.name}`;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (matchedRules.length === 0) {
|
|
514
|
+
return { action: 'allow', reason: 'No rules matched', severity: 'safe', matched_rules: [] };
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
return { action, reason, severity: maxSeverity, matched_rules: matchedRules };
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// =========================================================================
|
|
522
|
+
// POLICY VALIDATOR
|
|
523
|
+
// =========================================================================
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Validates policy DSL syntax and semantics.
|
|
527
|
+
*/
|
|
528
|
+
class PolicyValidator {
|
|
529
|
+
constructor() {}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Validate DSL source.
|
|
533
|
+
* @param {string} source
|
|
534
|
+
* @returns {{valid: boolean, errors: Array<{message: string, line?: number}>, warnings: Array}}
|
|
535
|
+
*/
|
|
536
|
+
validate(source) {
|
|
537
|
+
const errors = [];
|
|
538
|
+
const warnings = [];
|
|
539
|
+
const parser = new PolicyParser();
|
|
540
|
+
|
|
541
|
+
if (!source || typeof source !== 'string') {
|
|
542
|
+
return { valid: false, errors: [{ message: 'Source must be a non-empty string' }], warnings: [] };
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Check braces
|
|
546
|
+
let braceCount = 0;
|
|
547
|
+
const lines = source.split('\n');
|
|
548
|
+
for (let i = 0; i < lines.length; i++) {
|
|
549
|
+
for (const ch of lines[i]) {
|
|
550
|
+
if (ch === '{') braceCount++;
|
|
551
|
+
if (ch === '}') braceCount--;
|
|
552
|
+
if (braceCount < 0) {
|
|
553
|
+
errors.push({ message: `Unexpected '}' at line ${i + 1}`, line: i + 1 });
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
if (braceCount > 0) {
|
|
558
|
+
errors.push({ message: `Unclosed block: ${braceCount} missing '}'` });
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Try tokenizing
|
|
562
|
+
let tokens;
|
|
563
|
+
try {
|
|
564
|
+
tokens = parser.tokenize(source);
|
|
565
|
+
} catch (e) {
|
|
566
|
+
errors.push({ message: `Tokenization error: ${e.message}` });
|
|
567
|
+
return { valid: false, errors, warnings };
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Check for policy keyword
|
|
571
|
+
const hasPolicyKeyword = tokens.some(t => t.value === 'policy');
|
|
572
|
+
if (!hasPolicyKeyword) {
|
|
573
|
+
errors.push({ message: 'No policy block found. Source must contain at least one "policy" block.' });
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Check severity values
|
|
577
|
+
const validSeverities = new Set(['low', 'medium', 'high', 'critical']);
|
|
578
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
579
|
+
if (tokens[i].value === 'severity' && i + 1 < tokens.length) {
|
|
580
|
+
const next = tokens[i + 1].value === 'minimum' ? tokens[i + 2] : tokens[i + 1];
|
|
581
|
+
if (next && next.type === TOKEN_TYPES.STRING && !validSeverities.has(next.value)) {
|
|
582
|
+
warnings.push({ message: `Unknown severity '${next.value}' at line ${next.line}. Valid: low, medium, high, critical`, line: next.line });
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Try parsing
|
|
588
|
+
try {
|
|
589
|
+
parser.parse(tokens);
|
|
590
|
+
} catch (e) {
|
|
591
|
+
errors.push({ message: `Parse error: ${e.message}` });
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Check for duplicate rule names
|
|
595
|
+
const ruleNames = new Set();
|
|
596
|
+
for (const token of tokens) {
|
|
597
|
+
if (token.value === 'rule') {
|
|
598
|
+
const idx = tokens.indexOf(token);
|
|
599
|
+
if (idx + 1 < tokens.length && tokens[idx + 1].type === TOKEN_TYPES.STRING) {
|
|
600
|
+
const name = tokens[idx + 1].value;
|
|
601
|
+
if (ruleNames.has(name)) {
|
|
602
|
+
warnings.push({ message: `Duplicate rule name '${name}' at line ${tokens[idx + 1].line}`, line: tokens[idx + 1].line });
|
|
603
|
+
}
|
|
604
|
+
ruleNames.add(name);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// =========================================================================
|
|
614
|
+
// POLICY DSL (MAIN ENTRY)
|
|
615
|
+
// =========================================================================
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Main entry point for the Policy DSL.
|
|
619
|
+
*/
|
|
620
|
+
class PolicyDSL {
|
|
621
|
+
constructor() {
|
|
622
|
+
this._parser = new PolicyParser();
|
|
623
|
+
this._compiler = new PolicyCompiler();
|
|
624
|
+
this._runtime = new PolicyRuntime();
|
|
625
|
+
this._validator = new PolicyValidator();
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Parse DSL source into AST.
|
|
630
|
+
* @param {string} source
|
|
631
|
+
* @returns {object}
|
|
632
|
+
*/
|
|
633
|
+
parse(source) {
|
|
634
|
+
const tokens = this._parser.tokenize(source);
|
|
635
|
+
return this._parser.parse(tokens);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Compile AST into executable policies.
|
|
640
|
+
* @param {object} ast
|
|
641
|
+
* @returns {Array}
|
|
642
|
+
*/
|
|
643
|
+
compile(ast) {
|
|
644
|
+
return this._compiler.compile(ast);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Evaluate a compiled policy against a context.
|
|
649
|
+
* @param {object} policy
|
|
650
|
+
* @param {{input: string, source?: string, metadata?: object}} context
|
|
651
|
+
* @returns {{action: string, reason: string, severity: string, matched_rules: string[]}}
|
|
652
|
+
*/
|
|
653
|
+
evaluate(policy, context) {
|
|
654
|
+
const ctx = context && typeof context === 'object' ? context : { input: '' };
|
|
655
|
+
if (ctx.input === undefined || ctx.input === null) ctx.input = '';
|
|
656
|
+
if (typeof ctx.input !== 'string') ctx.input = String(ctx.input);
|
|
657
|
+
return this._runtime.execute(policy, ctx);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* Parse + compile in one step.
|
|
662
|
+
* @param {string} source
|
|
663
|
+
* @returns {Array}
|
|
664
|
+
*/
|
|
665
|
+
loadFile(source) {
|
|
666
|
+
const ast = this.parse(source);
|
|
667
|
+
return this.compile(ast);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Validate DSL source.
|
|
672
|
+
* @param {string} source
|
|
673
|
+
* @returns {{valid: boolean, errors: Array, warnings: Array}}
|
|
674
|
+
*/
|
|
675
|
+
validate(source) {
|
|
676
|
+
return this._validator.validate(source);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// =========================================================================
|
|
681
|
+
// EXAMPLE POLICIES
|
|
682
|
+
// =========================================================================
|
|
683
|
+
|
|
684
|
+
const EXAMPLE_STRICT_POLICY = `
|
|
685
|
+
policy "strict" {
|
|
686
|
+
severity minimum "low"
|
|
687
|
+
block on_threat true
|
|
688
|
+
scan_mode "deep"
|
|
689
|
+
|
|
690
|
+
rule "no-injection" {
|
|
691
|
+
when input matches "ignore.*previous.*instructions"
|
|
692
|
+
then block with severity "critical"
|
|
693
|
+
message "Instruction override attempt detected"
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
rule "no-role-hijack" {
|
|
697
|
+
when input matches "you are now.*unrestricted"
|
|
698
|
+
then block with severity "critical"
|
|
699
|
+
message "Role hijacking attempt detected"
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
rule "no-exfiltration" {
|
|
703
|
+
when input contains "send data to"
|
|
704
|
+
then block with severity "high"
|
|
705
|
+
message "Data exfiltration attempt detected"
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
rate_limit 100 per "minute"
|
|
709
|
+
}
|
|
710
|
+
`;
|
|
711
|
+
|
|
712
|
+
const EXAMPLE_PERMISSIVE_POLICY = `
|
|
713
|
+
policy "permissive" {
|
|
714
|
+
severity minimum "high"
|
|
715
|
+
block on_threat false
|
|
716
|
+
scan_mode "fast"
|
|
717
|
+
|
|
718
|
+
rule "critical-only" {
|
|
719
|
+
when input matches "override.*system.*safety"
|
|
720
|
+
then warn with severity "critical"
|
|
721
|
+
message "Critical safety override attempt"
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
allow {
|
|
725
|
+
when input.length less_than 50
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
rate_limit 10000 per "minute"
|
|
729
|
+
}
|
|
730
|
+
`;
|
|
731
|
+
|
|
732
|
+
const EXAMPLE_CUSTOM_RULES_POLICY = `
|
|
733
|
+
# Custom rules for a financial services agent
|
|
734
|
+
policy "financial-security" {
|
|
735
|
+
severity minimum "medium"
|
|
736
|
+
block on_threat true
|
|
737
|
+
|
|
738
|
+
rule "no-account-exfil" {
|
|
739
|
+
when input matches "account.*number|credit.*card|ssn|social.*security"
|
|
740
|
+
then block with severity "critical"
|
|
741
|
+
message "PII exfiltration attempt in financial context"
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
rule "no-auth-bypass" {
|
|
745
|
+
when input contains "bypass authentication"
|
|
746
|
+
then block with severity "critical"
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
rule "no-transaction-manipulation" {
|
|
750
|
+
when input matches "transfer.*funds|modify.*balance|override.*limit"
|
|
751
|
+
then block with severity "high"
|
|
752
|
+
message "Transaction manipulation attempt"
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
allow {
|
|
756
|
+
when source is "internal-audit"
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
`;
|
|
760
|
+
|
|
761
|
+
// =========================================================================
|
|
762
|
+
// EXPORTS
|
|
763
|
+
// =========================================================================
|
|
764
|
+
|
|
765
|
+
module.exports = {
|
|
766
|
+
PolicyDSL,
|
|
767
|
+
PolicyParser,
|
|
768
|
+
PolicyCompiler,
|
|
769
|
+
PolicyRuntime,
|
|
770
|
+
PolicyValidator,
|
|
771
|
+
BUILTIN_FUNCTIONS,
|
|
772
|
+
EXAMPLE_STRICT_POLICY,
|
|
773
|
+
EXAMPLE_PERMISSIVE_POLICY,
|
|
774
|
+
EXAMPLE_CUSTOM_RULES_POLICY,
|
|
775
|
+
};
|