@shomra/agent 0.3.29 → 0.3.30

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 (77) hide show
  1. package/package.json +1 -1
  2. package/src/agents/hook-command.mjs +1 -1
  3. package/src/artifacts/matchers.mjs +7 -0
  4. package/src/cli/flags.mjs +2 -2
  5. package/src/cli/help-sections.mjs +7 -0
  6. package/src/cli/help.mjs +1 -1
  7. package/src/commands/check.mjs +3 -11
  8. package/src/commands/gate.mjs +26 -4
  9. package/src/commands/git-hooks.mjs +2 -2
  10. package/src/commands/ledger.mjs +0 -1
  11. package/src/commands/mcp-add.mjs +2 -1
  12. package/src/commands/memory-scan.mjs +135 -47
  13. package/src/commands/pr.mjs +7 -10
  14. package/src/commands/provenance.mjs +8 -13
  15. package/src/commands/scan.mjs +7 -1
  16. package/src/commands/secrets.mjs +4 -5
  17. package/src/core/git-exec.mjs +79 -0
  18. package/src/core/yaml-lite.mjs +300 -0
  19. package/src/core/zip-lite.mjs +37 -0
  20. package/src/detect/local-redact.mjs +1 -3
  21. package/src/detect/sast/rules-config.mjs +1 -1
  22. package/src/detect/sast/scanner.mjs +1 -1
  23. package/src/detect/signals/agent-frameworks.mjs +231 -0
  24. package/src/detect/signals/agent-graph-surface.mjs +113 -0
  25. package/src/detect/signals/agentic-ci-surface.mjs +314 -0
  26. package/src/detect/signals/agentic-shim.mjs +82 -0
  27. package/src/detect/signals/artifacts.mjs +7 -35
  28. package/src/detect/signals/chat-template.mjs +211 -0
  29. package/src/detect/signals/ci-workflow.mjs +169 -0
  30. package/src/detect/signals/gate.mjs +77 -9
  31. package/src/detect/signals/guardrail-shape.mjs +564 -0
  32. package/src/detect/signals/guardrail-surface.mjs +221 -0
  33. package/src/detect/signals/injection.mjs +8 -0
  34. package/src/detect/signals/inspect-shim.mjs +7 -0
  35. package/src/detect/signals/instruction-paths.mjs +60 -0
  36. package/src/detect/signals/manifests.mjs +302 -0
  37. package/src/detect/signals/mcp-advisories.mjs +109 -0
  38. package/src/detect/signals/mcp-config.mjs +598 -0
  39. package/src/detect/signals/memory-directives.mjs +661 -0
  40. package/src/detect/signals/memory-locations.mjs +158 -0
  41. package/src/detect/signals/memory.mjs +47 -29
  42. package/src/detect/signals/model-config-rules.mjs +655 -0
  43. package/src/detect/signals/model-config.mjs +61 -0
  44. package/src/detect/signals/prose-context.mjs +6 -9
  45. package/src/detect/signals/scan.mjs +4 -4
  46. package/src/detect/signals/secret-scanner.mjs +241 -0
  47. package/src/detect/signals/secrets.mjs +1 -48
  48. package/src/detect/signals/shell.mjs +3 -3
  49. package/src/gate/advisories.mjs +16 -0
  50. package/src/gate/batch.mjs +10 -0
  51. package/src/gate/environment.mjs +8 -53
  52. package/src/guard/artifact-paths.mjs +107 -0
  53. package/src/guard/classify.mjs +165 -7
  54. package/src/guard/command-resolve.mjs +35 -5
  55. package/src/guard/memory-write.mjs +218 -0
  56. package/src/guard/prompt-guard.mjs +0 -1
  57. package/src/guard/tool-guard.mjs +52 -77
  58. package/src/inventory/agent-posture.mjs +236 -57
  59. package/src/inventory/artifacts/classify.mjs +10 -1
  60. package/src/inventory/artifacts/discover.mjs +113 -3
  61. package/src/inventory/artifacts/extensions.mjs +70 -0
  62. package/src/inventory/artifacts/hook-scripts.mjs +128 -0
  63. package/src/inventory/artifacts/limits.mjs +1 -1
  64. package/src/inventory/artifacts/plugins.mjs +105 -0
  65. package/src/inventory/artifacts/roots.mjs +40 -0
  66. package/src/inventory/discovery/ai-dependencies.mjs +39 -12
  67. package/src/inventory/discovery/all.mjs +4 -0
  68. package/src/inventory/discovery/cloud-clis.mjs +472 -0
  69. package/src/inventory/discovery/coding-agents.mjs +19 -4
  70. package/src/inventory/discovery/mcp-clients.mjs +16 -10
  71. package/src/inventory/discovery/mcp-servers.mjs +125 -35
  72. package/src/inventory/discovery/mcp-stores.mjs +207 -0
  73. package/src/inventory/env-redirect.mjs +148 -0
  74. package/src/inventory/grant-extract.mjs +463 -0
  75. package/src/inventory/project-roots.mjs +108 -0
  76. package/src/inventory/vscode-state.mjs +153 -0
  77. package/src/mcp/server-tools.mjs +1 -1
@@ -0,0 +1,564 @@
1
+ import { parseYaml } from '../../core/yaml-lite.mjs';
2
+
3
+ const MAX_ITEMS = 200;
4
+ const INJECTION_NAME_RE = /prompt[\s_-]*attack|jailbreak|injection|self[\s_]check[\s_]input|prompt[\s_-]*(?:guard|shield)|indirect[\s_-]*attack|llama[\s_]guard[\s_]check[\s_]input|content[\s_]safety[\s_]check[\s_]input|lakera|promptguard|model_armor|azure\/prompt_shield|vigil|rebuff/i;
5
+
6
+ function empty(framework , source ) {
7
+ return {
8
+ framework, source, controls: [], declaredRails: [], emptyRails: [], judgePrompts: [], missingPrompts: [],
9
+ streamedBeforeCheck: null, optIn: [], failOpen: [], blindSpots: [], endpoints: [], nonStoppingRefusals: [],
10
+ unauthenticatedService: false,
11
+ };
12
+ }
13
+
14
+ const obj = (v ) => (v && typeof v === 'object' && !Array.isArray(v) ? (v ) : null);
15
+ const arr = (v ) => (Array.isArray(v) ? v.slice(0, MAX_ITEMS) : []);
16
+ const str = (v ) => (typeof v === 'string' ? v : typeof v === 'number' || typeof v === 'boolean' ? String(v) : null);
17
+ const isFalse = (v ) => v === false || (typeof v === 'string' && /^false$/i.test(v.trim()));
18
+ const isTrue = (v ) => v === true || (typeof v === 'string' && /^true$/i.test(v.trim()));
19
+ const num = (v ) => {
20
+ const n = typeof v === 'number' ? v : typeof v === 'string' && v.trim() ? Number(v) : NaN;
21
+ return Number.isFinite(n) ? n : null;
22
+ };
23
+
24
+ const norm = (k ) => k.toLowerCase().replace(/[\s_-]/g, '');
25
+ function pick(o , ...keys ) {
26
+ if (!o) return undefined;
27
+ const want = new Set(keys.map(norm));
28
+ for (const [k, v] of Object.entries(o)) if (want.has(norm(k))) return v;
29
+ return undefined;
30
+ }
31
+
32
+ const LOCAL_HOST_RE = /^(?:localhost|0\.0\.0\.0|\[?::1\]?|127(?:\.\d+){3}|10(?:\.\d+){3}|192\.168(?:\.\d+){2}|172\.(?:1[6-9]|2\d|3[01])(?:\.\d+){2}|[\w-]+|[\w.-]+\.(?:local|internal|svc|lan|localhost))$/i;
33
+
34
+ export function isCleartextRemote(url ) {
35
+ const m = /^http:\/\/(?:[^@/]*@)?(\[[^\]]+\]|[^/:?#]+)/i.exec(url.trim());
36
+ return !!m && !LOCAL_HOST_RE.test(m[1]);
37
+ }
38
+
39
+ function parseDoc(path , text ) {
40
+ const t = text.replace(/^/, '');
41
+ if (/\.json$/i.test(path) || /^\s*[{[]/.test(t)) {
42
+ try { return JSON.parse(t); } catch { }
43
+ }
44
+ if (/\.(ya?ml|json)$/i.test(path)) return parseYaml(t);
45
+ return null;
46
+ }
47
+
48
+ const COLANG_RE = /^\s*(?:define\s+(?:user|bot|flow|subflow)\b|flow\s+[\w $="]+$|import\s+(?:guardrails|core|llm|nemoguardrails)\b|activate\s+[\w ]+)/m;
49
+ const NEMO_RAIL_KEYS = ['input', 'output', 'retrieval', 'dialog', 'config', 'tool_input', 'tool_output', 'actions'];
50
+ const KONG_GUARD_PLUGINS = /^(ai-prompt-guard|ai-semantic-prompt-guard|ai-aws-guardrails|ai-azure-content-safety)$/;
51
+
52
+ export function guardrailFramework(path , text ) {
53
+ if (!text) return null;
54
+ const lower = path.toLowerCase();
55
+ if (lower.endsWith('.rail')) return 'guardrails-ai';
56
+ if (lower.endsWith('.co')) return COLANG_RE.test(text) ? 'colang' : null;
57
+ if (lower.endsWith('.tf')) {
58
+ if (/resource\s+"aws_bedrock_guardrail"/.test(text)) return 'bedrock';
59
+ if (/resource\s+"azurerm_cognitive_account_rai_policy"/.test(text)) return 'azure-rai';
60
+ return null;
61
+ }
62
+ if (lower.endsWith('.bicep')) return /Microsoft\.CognitiveServices\/accounts\/raiPolicies/i.test(text) ? 'azure-rai' : null;
63
+ if (!/\.(ya?ml|json)$/.test(lower)) return null;
64
+ if (/AWS::Bedrock::Guardrail\b/.test(text)) return 'bedrock';
65
+ if (/Microsoft\.CognitiveServices\/accounts\/raiPolicies/i.test(text)) return 'azure-rai';
66
+ if (!/rails|guardrail|hooks|scanners|plugins|filtersConfig|filters_config|contentPolicy|validators|guards/i.test(text)) return null;
67
+ const doc = obj(parseDoc(path, text));
68
+ if (!doc) return null;
69
+ const rails = obj(doc.rails);
70
+ if (rails && Object.keys(rails).some((k) => NEMO_RAIL_KEYS.includes(k))) return 'nemo';
71
+ if (arr(doc.guardrails).some((g) => obj(g)?.litellm_params || obj(g)?.guardrail_name)) return 'litellm';
72
+ if (arr(obj(doc.litellm_settings)?.guardrails).length) return 'litellm';
73
+ if (['before_request_hooks', 'after_request_hooks', 'input_guardrails', 'output_guardrails'].some((k) => Array.isArray(doc[k]))) return 'portkey';
74
+ if (Array.isArray(doc.input_scanners) || Array.isArray(doc.output_scanners)) return 'llm-guard';
75
+ if (kongGuardPlugins(doc).length) return 'kong';
76
+ if (pick(doc, 'contentPolicyConfig', 'contentPolicy') && /filters/i.test(text)) return 'bedrock';
77
+ if (arr(doc.guards).some((g) => Array.isArray(obj(g)?.validators)) || (Array.isArray(doc.validators) && arr(doc.validators).some((v) => obj(v)?.id))) return 'guardrails-ai';
78
+ return null;
79
+ }
80
+
81
+ const NEMO_PROMPT_FOR = [
82
+ { flow: /^self check input(?:\s+\$variant\s*=\s*"?([\w-]+)"?)?$/i, task: (m) => m[1] ?? 'self_check_input', vars: ['user_input'] },
83
+ { flow: /^self check output(?:\s+\$variant\s*=\s*"?([\w-]+)"?)?$/i, task: (m) => m[1] ?? 'self_check_output', vars: ['bot_response'] },
84
+ { flow: /^self check facts(?:\s+\$variant\s*=\s*"?([\w-]+)"?)?$/i, task: (m) => m[1] ?? 'self_check_facts', vars: ['response'] },
85
+ { flow: /^llama guard check input$/i, task: () => 'llama_guard_check_input', vars: ['user_input'] },
86
+ { flow: /^llama guard check output$/i, task: () => 'llama_guard_check_output', vars: ['bot_response'] },
87
+ { flow: /^content safety check (input|output) \$model\s*=\s*"?([\w.-]+)"?$/i, task: (m) => `content_safety_check_${m[1].toLowerCase()} $model=${m[2]}`, vars: [] },
88
+ ];
89
+ const VARS_FOR_TASK = {
90
+ self_check_input: ['user_input'],
91
+ self_check_output: ['bot_response'],
92
+ self_check_facts: ['response'],
93
+ llama_guard_check_input: ['user_input'],
94
+ llama_guard_check_output: ['bot_response'],
95
+ };
96
+ const JAILBREAK_DEFAULTS = { length_per_perplexity_threshold: 89.79, prefix_suffix_perplexity_threshold: 1845.65 };
97
+
98
+ function nemoDirection(rail ) {
99
+ if (rail === 'input' || rail === 'output' || rail === 'retrieval') return rail;
100
+ if (rail === 'tool_input' || rail === 'tool_output') return 'tool';
101
+ return 'unknown';
102
+ }
103
+
104
+ function readNemo(doc , source , siblingTasks ) {
105
+ const m = empty('nemo', source);
106
+ const rails = obj(doc.rails) ?? {};
107
+ const prompts = arr(doc.prompts).map(obj).filter(Boolean) ;
108
+ const tasks = new Set([...prompts.map((p) => String(p.task ?? '')), ...siblingTasks]);
109
+
110
+ for (const rail of ['input', 'output', 'retrieval', 'tool_input', 'tool_output']) {
111
+ const block = obj(rails[rail]);
112
+ if (!block || !Array.isArray(block.flows)) continue;
113
+ const flows = arr(block.flows).map((f) => String(f).trim()).filter(Boolean);
114
+ (flows.length ? m.declaredRails : m.emptyRails).push(rail);
115
+ for (const flow of flows) {
116
+ m.controls.push({ name: flow, direction: nemoDirection(rail), injection: INJECTION_NAME_RE.test(flow), state: 'enforcing', why: null, anchor: flow });
117
+ for (const need of NEMO_PROMPT_FOR) {
118
+ const hit = need.flow.exec(flow);
119
+ if (hit && !tasks.has(need.task(hit))) m.missingPrompts.push(need.task(hit));
120
+ }
121
+ }
122
+ }
123
+
124
+ for (const p of prompts) {
125
+ const task = String(p.task ?? '');
126
+ const content = str(p.content) ?? arr(p.messages).map((x) => str(obj(x)?.content) ?? '').join('\n');
127
+ if (!task || !content) continue;
128
+ const vars = VARS_FOR_TASK[task.replace(/\s.*$/, '')] ?? [];
129
+ m.judgePrompts.push({ task, content, missingVars: vars.filter((v) => !new RegExp(`\\{\\{\\s*${v}\\b`).test(content)) });
130
+ }
131
+
132
+ const streaming = obj(obj(rails.output)?.streaming);
133
+ if (streaming && isTrue(streaming.enabled) && !isFalse(streaming.stream_first) && m.declaredRails.includes('output')) {
134
+ m.streamedBeforeCheck = {
135
+ why: streaming.stream_first === undefined ? 'streaming is enabled and stream_first defaults to true' : 'stream_first: true',
136
+ anchor: 'stream_first' in streaming ? 'stream_first' : 'streaming',
137
+ };
138
+ }
139
+
140
+ const cfg = obj(rails.config) ?? {};
141
+ const jb = obj(cfg.jailbreak_detection);
142
+ if (jb) {
143
+ for (const [key, dflt] of Object.entries(JAILBREAK_DEFAULTS)) {
144
+ const v = num(jb[key]);
145
+ if (v != null && v >= dflt * 5) {
146
+ m.controls.push({ name: `jailbreak detection (${key})`, direction: 'input', injection: true, state: 'weak', why: `${key}: ${v} is ${Math.round(v / dflt)}x the default ${dflt}`, anchor: key });
147
+ }
148
+ }
149
+ for (const key of ['server_endpoint', 'nim_base_url']) if (typeof jb[key] === 'string') m.endpoints.push({ url: jb[key], anchor: key });
150
+ }
151
+ const sdd = obj(cfg.sensitive_data_detection);
152
+ for (const dir of ['input', 'output', 'retrieval'] ) {
153
+ const enabled = m.controls.some((c) => /(?:detect|mask) sensitive data on/i.test(c.name) && c.direction === dir);
154
+ const block = obj(sdd?.[dir]);
155
+ if (enabled && (!block || !arr(block.entities).length)) {
156
+ m.controls.push({ name: `sensitive data detection on ${dir}`, direction: dir, injection: false, state: 'disabled', why: 'the rail is enabled but lists no entities to detect', anchor: 'sensitive_data_detection' });
157
+ }
158
+ }
159
+ if (typeof doc.actions_server_url === 'string') m.endpoints.push({ url: doc.actions_server_url, anchor: 'actions_server_url' });
160
+ return m;
161
+ }
162
+
163
+ const REFUSE_RE = /^\s*bot\s+(?:refuse|inform\s+(?:cannot|answer\s+unknown))|^\s*bot\s+say\s+"?(?:I\s+(?:can(?:no|')t|won't|am\s+not\s+able))/i;
164
+ const STOP_RE = /^\s*(?:stop|abort)\b/i;
165
+ const CHECK_RE = /\bexecute\s+\w+|\$\w+\s*=\s*(?:execute|await)\b/i;
166
+
167
+ function underBranch(lines , i ) {
168
+ const indent = lines[i].search(/\S/);
169
+ for (let j = i - 1, seen = 0; j >= 0 && seen < 30; j--) {
170
+ if (!lines[j].trim()) continue;
171
+ seen++;
172
+ if (lines[j].search(/\S/) < indent) return /^\s*(?:if|else|elif|when|or\s+when)\b/i.test(lines[j]);
173
+ }
174
+ return false;
175
+ }
176
+
177
+ function readColang(text , source ) {
178
+ const m = empty('colang', source);
179
+ const lines = text.split(/\r?\n/).slice(0, 20_000);
180
+ let flow = null;
181
+ let flowChecks = false;
182
+ for (let i = 0; i < lines.length; i++) {
183
+ const head = /^(?:define\s+(?:sub)?flow|flow)\s+(.+?)\s*$/i.exec(lines[i]);
184
+ if (head) { flow = head[1]; flowChecks = false; m.controls.push({ name: flow, direction: /output/i.test(flow) ? 'output' : 'input', injection: INJECTION_NAME_RE.test(flow), state: 'enforcing', why: null, anchor: lines[i].trim() }); continue; }
185
+ if (CHECK_RE.test(lines[i])) flowChecks = true;
186
+
187
+ if (!flow || !flowChecks || !REFUSE_RE.test(lines[i]) || !underBranch(lines, i)) continue;
188
+ const indent = lines[i].search(/\S/);
189
+ let stopped = false;
190
+
191
+ for (let j = i + 1, seen = 0; j < lines.length && seen < 12; j++) {
192
+ if (!lines[j].trim()) continue;
193
+ seen++;
194
+ if (lines[j].search(/\S/) < indent || REFUSE_RE.test(lines[j])) break;
195
+ if (STOP_RE.test(lines[j])) { stopped = true; break; }
196
+ }
197
+ if (m.nonStoppingRefusals.length >= 50) break;
198
+
199
+ if (!stopped) m.nonStoppingRefusals.push({ flow, anchor: lines[i].trim() });
200
+ }
201
+ return m;
202
+ }
203
+
204
+ function readRail(text , source ) {
205
+ const m = empty('guardrails-ai', source);
206
+ const tags = text.match(/<[a-z][\w-]*\b[^>]*\b(?:format|validators)\s*=\s*"[^"]*"[^>]*>/gi) ?? [];
207
+ for (const tag of tags.slice(0, MAX_ITEMS)) {
208
+ const spec = /\b(?:format|validators)\s*=\s*"([^"]*)"/i.exec(tag)?.[1] ?? '';
209
+ const onFail = new Map([...tag.matchAll(/\bon-fail-([\w-]+)\s*=\s*"([^"]*)"/gi)].map((x) => [x[1].toLowerCase(), x[2].trim().toLowerCase()]));
210
+ for (const raw of spec.split(';').map((s) => s.trim()).filter(Boolean)) {
211
+ const name = raw.split(/[\s:]/)[0];
212
+ const alias = name.replace(/\//g, '_').toLowerCase();
213
+ const action = onFail.get(alias) ?? onFail.get(alias.replace(/_/g, '-')) ?? null;
214
+ const passes = action == null || action === 'noop';
215
+ m.controls.push({
216
+ name, direction: /<input\b|<prompt\b/i.test(tag) ? 'input' : 'output', injection: INJECTION_NAME_RE.test(name),
217
+ state: passes ? 'observe-only' : 'enforcing', validator: true,
218
+ why: action == null ? `no on-fail-${alias} attribute, so a failure defaults to noop` : action === 'noop' ? `on-fail-${alias}="noop"` : null,
219
+ anchor: action == null ? name : `on-fail-${alias}`,
220
+ });
221
+ }
222
+ }
223
+ return m;
224
+ }
225
+
226
+ function readGuardJson(doc , source ) {
227
+ const m = empty('guardrails-ai', source);
228
+ const guards = Array.isArray(doc.guards) ? arr(doc.guards) : [doc];
229
+ for (const g of guards) {
230
+ for (const v of arr(obj(g)?.validators)) {
231
+ const o = obj(v);
232
+ if (!o) continue;
233
+ const name = String(o.id ?? o.name ?? 'validator');
234
+ const action = str(pick(o, 'onFail', 'on_fail'))?.toLowerCase() ?? null;
235
+ const passes = action == null || action === 'noop';
236
+ m.controls.push({ name, direction: 'unknown', injection: INJECTION_NAME_RE.test(name), state: passes ? 'observe-only' : 'enforcing', validator: true, why: passes ? (action ? 'onFail: noop' : 'no onFail, so a failure defaults to noop') : null, anchor: name });
237
+ }
238
+ }
239
+ return m;
240
+ }
241
+
242
+ function bedrockFilter(f , into , anchorPrefix = '') {
243
+ const type = String(pick(f, 'type') ?? '').toUpperCase();
244
+ if (!type) return;
245
+ const injection = type === 'PROMPT_ATTACK';
246
+ for (const dir of ['input', 'output'] ) {
247
+ if (injection && dir === 'output') continue;
248
+ const strength = String(pick(f, `${dir}Strength`) ?? '').toUpperCase();
249
+ const action = String(pick(f, `${dir}Action`) ?? '').toUpperCase();
250
+ const enabled = pick(f, `${dir}Enabled`);
251
+ let state = 'enforcing';
252
+ let why = null;
253
+ if (isFalse(enabled)) { state = 'disabled'; why = `${dir}Enabled: false`; }
254
+ else if (strength === 'NONE') { state = 'disabled'; why = `${dir}Strength: NONE`; }
255
+ else if (action === 'NONE') { state = 'observe-only'; why = `${dir}Action: NONE (detect and log, never block)`; }
256
+ else if (strength === 'LOW') { state = 'weak'; why = `${dir}Strength: LOW - only high-confidence matches are blocked`; }
257
+ if (state === 'enforcing' && !(injection || strength)) continue;
258
+ into.controls.push({ name: `${type} filter`, direction: dir, injection, state, why, anchor: anchorPrefix || type });
259
+ }
260
+ }
261
+
262
+ function readBedrockDoc(props , into ) {
263
+ const content = obj(pick(props, 'contentPolicyConfig', 'contentPolicy'));
264
+ const filters = arr(pick(content, 'filtersConfig', 'filters')).map(obj).filter(Boolean) ;
265
+ for (const f of filters) bedrockFilter(f, into);
266
+ if (content && !filters.some((f) => String(pick(f, 'type')).toUpperCase() === 'PROMPT_ATTACK')) {
267
+ into.controls.push({ name: 'PROMPT_ATTACK filter', direction: 'input', injection: true, state: 'disabled', why: 'the content policy has no PROMPT_ATTACK filter', anchor: 'FiltersConfig' });
268
+ }
269
+ const pii = obj(pick(props, 'sensitiveInformationPolicyConfig', 'sensitiveInformationPolicy'));
270
+ for (const e of arr(pick(pii, 'piiEntitiesConfig', 'piiEntities')).map(obj)) {
271
+ if (e && String(pick(e, 'action')).toUpperCase() === 'NONE') {
272
+ into.controls.push({ name: `${pick(e, 'type') ?? 'PII'} entity`, direction: 'both', injection: false, state: 'observe-only', why: 'Action: NONE', anchor: String(pick(e, 'type') ?? 'Action') });
273
+ }
274
+ }
275
+ const grounding = obj(pick(props, 'contextualGroundingPolicyConfig', 'contextualGroundingPolicy'));
276
+ for (const g of arr(pick(grounding, 'filtersConfig', 'filters')).map(obj)) {
277
+ if (!g) continue;
278
+ const t = num(pick(g, 'threshold'));
279
+ if (t === 0) into.controls.push({ name: `${pick(g, 'type') ?? 'GROUNDING'} check`, direction: 'output', injection: false, state: 'disabled', why: 'Threshold: 0 - no response scores below it, so nothing is blocked', anchor: 'Threshold' });
280
+ else if (String(pick(g, 'action') ?? '').toUpperCase() === 'NONE') into.controls.push({ name: `${pick(g, 'type') ?? 'GROUNDING'} check`, direction: 'output', injection: false, state: 'observe-only', why: 'Action: NONE', anchor: 'Action' });
281
+ }
282
+ }
283
+
284
+ function readBedrock(path , text , source ) {
285
+ const m = empty('bedrock', source);
286
+ if (/\.tf$/i.test(path)) {
287
+ for (const res of hclBlocks(text, /resource\s+"aws_bedrock_guardrail"\s+"[^"]+"/g)) {
288
+ for (const f of hclBlocks(res, /\bfilters_config\b/g)) {
289
+ if (/\bthreshold\s*=/.test(f)) continue;
290
+ bedrockFilter(hclAttrs(f), m, 'filters_config');
291
+ }
292
+ const content = hclBlocks(res, /\bcontent_policy_config\b/g)[0];
293
+ if (content && !/"PROMPT_ATTACK"/.test(content)) m.controls.push({ name: 'PROMPT_ATTACK filter', direction: 'input', injection: true, state: 'disabled', why: 'content_policy_config has no PROMPT_ATTACK filter', anchor: 'content_policy_config' });
294
+ for (const g of hclBlocks(res, /\bcontextual_grounding_policy_config\b/g).flatMap((b) => hclBlocks(b, /\bfilters_config\b/g))) {
295
+ if (num(hclAttrs(g).threshold) === 0) m.controls.push({ name: 'GROUNDING check', direction: 'output', injection: false, state: 'disabled', why: 'threshold = 0', anchor: 'threshold' });
296
+ }
297
+ }
298
+ return m;
299
+ }
300
+ const doc = obj(parseDoc(path, text));
301
+ if (!doc) return m;
302
+ const resources = obj(doc.Resources);
303
+ const guardrails = resources
304
+ ? Object.values(resources).map(obj).filter((r) => r && r.Type === 'AWS::Bedrock::Guardrail').map((r) => obj(r .Properties) ?? {})
305
+ : [doc];
306
+ for (const props of guardrails.slice(0, 50)) readBedrockDoc(props, m);
307
+ return m;
308
+ }
309
+
310
+ const AZURE_INJECTION_FILTER_RE = /^(jailbreak|indirect[\s_-]*attacks?|prompt[\s_-]*shields?)$/i;
311
+
312
+ function azureFilter(name , f , into ) {
313
+ const injection = AZURE_INJECTION_FILTER_RE.test(name.trim());
314
+ const src = String(f.source ?? '').toLowerCase();
315
+ const direction = /prompt|pre/.test(src) ? 'input' : /completion|post/.test(src) ? 'output' : /tool/.test(src) ? 'tool' : injection ? 'input' : 'unknown';
316
+ const action = String(f.action ?? '').toUpperCase();
317
+ let state = 'enforcing';
318
+ let why = null;
319
+ if (isFalse(f.enabled)) { state = 'disabled'; why = 'enabled: false'; }
320
+ else if (isFalse(f.blocking) || action === 'ANNOTATING' || action === 'NONE') { state = 'observe-only'; why = isFalse(f.blocking) ? 'blocking: false (annotate only)' : `action: ${action}`; }
321
+ else if (/^high$/i.test(String(f.severity ?? ''))) { state = 'weak'; why = 'severityThreshold: High - only the most severe content is blocked'; }
322
+ if (state === 'enforcing') return;
323
+ into.controls.push({ name: `${name} filter`, direction, injection, state, why, anchor: name });
324
+ }
325
+
326
+ function readAzureRai(path , text , source ) {
327
+ const m = empty('azure-rai', source);
328
+ const streamed = (mode , anchor ) => {
329
+ if (/^(asynchronous_filter|deferred)$/i.test(mode)) m.streamedBeforeCheck = { why: `mode: ${mode} - completion tokens stream immediately and the filter catches up about 1,000 characters later`, anchor };
330
+ };
331
+ if (/\.tf$/i.test(path)) {
332
+ for (const res of hclBlocks(text, /resource\s+"azurerm_cognitive_account_rai_policy"\s+"[^"]+"/g)) {
333
+ const a = hclAttrs(res);
334
+ if (a.mode) streamed(String(a.mode), 'mode');
335
+ for (const cf of hclBlocks(res, /\bcontent_filter\b/g)) {
336
+ const c = hclAttrs(cf);
337
+ azureFilter(String(c.name ?? 'content'), { enabled: c.filter_enabled, blocking: c.block_enabled, severity: c.severity_threshold, action: null, source: c.source }, m);
338
+ }
339
+ }
340
+ return m;
341
+ }
342
+ if (/\.bicep$/i.test(path)) {
343
+ const mode = /\bmode\s*:\s*'([^']+)'/.exec(text);
344
+ if (mode) streamed(mode[1], 'mode');
345
+ for (const item of text.match(/\{[^{}]*\bname\s*:\s*'[^']+'[^{}]*\}/g) ?? []) {
346
+ if (!/\b(blocking|enabled|severityThreshold)\s*:/.test(item)) continue;
347
+ const get = (k ) => new RegExp(`\\b${k}\\s*:\\s*'?([\\w -]+?)'?\\s*(?:[,}\\n])`).exec(item)?.[1];
348
+ azureFilter(get('name') ?? 'content', { enabled: get('enabled'), blocking: get('blocking'), severity: get('severityThreshold'), action: get('action'), source: get('source') }, m);
349
+ }
350
+ return m;
351
+ }
352
+ const doc = parseDoc(path, text);
353
+ const policies = [];
354
+ const visit = (n , depth = 0) => {
355
+ if (depth > 12 || !n || typeof n !== 'object' || policies.length > 50) return;
356
+ const p = obj(n.properties);
357
+ if (p && (Array.isArray(p.contentFilters) || typeof p.basePolicyName === 'string')) policies.push(p);
358
+ for (const v of Object.values(n)) visit(v, depth + 1);
359
+ };
360
+ visit(doc);
361
+ for (const p of policies) {
362
+ if (typeof p.mode === 'string') streamed(p.mode, 'mode');
363
+ for (const f of arr(p.contentFilters).map(obj)) {
364
+ if (f) azureFilter(String(f.name ?? 'content'), { enabled: f.enabled, blocking: f.blocking, severity: f.severityThreshold, action: f.action, source: f.source }, m);
365
+ }
366
+ }
367
+ return m;
368
+ }
369
+
370
+ function litellmModes(mode ) {
371
+ if (typeof mode === 'string') return [mode];
372
+ if (Array.isArray(mode)) return mode.map(String);
373
+ const o = obj(mode);
374
+ if (!o) return [];
375
+ return [...Object.values(obj(o.tags) ?? {}).map(String), ...(o.default ? [String(o.default)] : [])];
376
+ }
377
+
378
+ function readLitellm(doc , source ) {
379
+ const m = empty('litellm', source);
380
+ for (const g of arr(doc.guardrails).map(obj)) {
381
+ if (!g) continue;
382
+ const p = obj(g.litellm_params) ?? {};
383
+ const name = String(g.guardrail_name ?? p.guardrail ?? 'guardrail');
384
+ const modes = litellmModes(p.mode);
385
+ const tagged = obj(obj(p.mode)?.tags);
386
+ const loggingOnly = modes.length > 0 && modes.every((x) => x === 'logging_only');
387
+ const direction = modes.some((x) => /pre|during/.test(x)) && modes.some((x) => /post/.test(x)) ? 'both' : modes.some((x) => /post/.test(x)) ? 'output' : modes.some((x) => /pre|during/.test(x)) ? 'input' : 'unknown';
388
+ const injection = INJECTION_NAME_RE.test(`${name} ${p.guardrail ?? ''}`);
389
+ m.controls.push({ name, direction, injection, state: loggingOnly ? 'observe-only' : 'enforcing', why: loggingOnly ? 'mode: logging_only' : null, anchor: loggingOnly ? 'logging_only' : name });
390
+ if (tagged && Object.values(tagged).some((v) => String(v) === 'logging_only')) {
391
+ const tag = Object.entries(tagged).find(([, v]) => String(v) === 'logging_only') [0];
392
+ m.blindSpots.push({ name, why: `a request carrying the tag "${tag}" is only logged - the caller picks the header, so the caller picks whether the guardrail blocks`, anchor: tag.split(':')[0] });
393
+ }
394
+ if (!isTrue(p.default_on)) m.optIn.push({ name, anchor: 'default_on' in p ? 'default_on' : name });
395
+ if (isFalse(p.fail_on_error)) m.failOpen.push({ name, why: 'fail_on_error: false - a guardrail error lets the request through', anchor: 'fail_on_error' });
396
+ if (String(p.unreachable_fallback ?? '') === 'fail_open') m.failOpen.push({ name, why: 'unreachable_fallback: fail_open', anchor: 'unreachable_fallback' });
397
+ if (isTrue(p.experimental_use_latest_role_message_only)) m.blindSpots.push({ name, why: 'only the latest message is screened, so an instruction placed in an earlier turn is never seen', anchor: 'experimental_use_latest_role_message_only' });
398
+ if (isTrue(p.skip_tool_message_in_guardrail)) m.blindSpots.push({ name, why: 'tool results are skipped - the channel indirect injection arrives on', anchor: 'skip_tool_message_in_guardrail' });
399
+ if (typeof p.api_base === 'string') m.endpoints.push({ url: p.api_base, anchor: 'api_base' });
400
+ }
401
+ for (const g of arr(obj(doc.litellm_settings)?.guardrails).map(obj)) {
402
+ for (const [name, v] of Object.entries(g ?? {})) {
403
+ const o = obj(v);
404
+ if (!o) continue;
405
+ m.controls.push({ name, direction: 'unknown', injection: INJECTION_NAME_RE.test(`${name} ${arr(o.callbacks).join(' ')}`), state: isTrue(o.logging_only) ? 'observe-only' : 'enforcing', why: isTrue(o.logging_only) ? 'logging_only: true' : null, anchor: name });
406
+ if (!isTrue(o.default_on)) m.optIn.push({ name, anchor: 'default_on' in o ? 'default_on' : name });
407
+ }
408
+ }
409
+ return m;
410
+ }
411
+
412
+ function readPortkey(doc , source ) {
413
+ const m = empty('portkey', source);
414
+ const hooks = [['before_request_hooks', 'input'], ['after_request_hooks', 'output']];
415
+ for (const [key, direction] of hooks) {
416
+ for (const h of arr(doc[key]).map(obj)) {
417
+ if (!h) continue;
418
+ const name = String(h.id ?? 'guardrail');
419
+ const injection = INJECTION_NAME_RE.test(`${name} ${arr(h.checks).map((c) => obj(c)?.id).join(' ')}`);
420
+ const denies = isTrue(h.deny);
421
+ const sync = isFalse(h.async);
422
+ const why = !sync ? `async ${h.async === undefined ? 'defaults to true' : 'is true'} - the check runs beside the request and only logs` : !denies ? `deny ${h.deny === undefined ? 'defaults to false' : 'is false'} - a failed check returns 246 and the request is still processed` : null;
423
+ m.controls.push({ name, direction, injection, state: why ? 'observe-only' : 'enforcing', why, anchor: !sync ? (h.async === undefined ? name : 'async') : !denies ? (h.deny === undefined ? name : 'deny') : name });
424
+ for (const c of arr(h.checks).map(obj)) {
425
+ if (c && isFalse(c.is_enabled)) m.controls.push({ name: String(c.id ?? 'check'), direction, injection: INJECTION_NAME_RE.test(String(c.id)), state: 'disabled', why: 'is_enabled: false', anchor: 'is_enabled' });
426
+ }
427
+ }
428
+ }
429
+ return m;
430
+ }
431
+
432
+ function readLlmGuard(doc , source ) {
433
+ const m = empty('llm-guard', source);
434
+ const lists = [['input_scanners', 'input'], ['output_scanners', 'output']];
435
+ for (const [key, direction] of lists) {
436
+ for (const s of arr(doc[key]).map(obj)) {
437
+ if (!s) continue;
438
+ const name = String(s.type ?? 'scanner');
439
+ const threshold = num(obj(s.params)?.threshold);
440
+ const injection = /PromptInjection|Jailbreak|BanSubstrings/i.test(name) && direction === 'input';
441
+
442
+ const state = threshold != null && threshold >= 1 ? 'disabled' : threshold != null && threshold >= 0.99 ? 'weak' : 'enforcing';
443
+ m.controls.push({ name, direction, injection, state, why: state === 'disabled' ? `threshold: ${threshold} - a score can never exceed it` : state === 'weak' ? `threshold: ${threshold} - only near-certain detections fail` : null, anchor: state === 'enforcing' ? name : 'threshold' });
444
+ }
445
+ }
446
+ m.unauthenticatedService = !obj(doc.auth) && !!obj(doc.app);
447
+ return m;
448
+ }
449
+
450
+ function kongGuardPlugins(doc ) {
451
+ const out = [];
452
+ const take = (list ) => { for (const p of arr(list).map(obj)) if (p && KONG_GUARD_PLUGINS.test(String(p.name ?? ''))) out.push(p); };
453
+ take(doc.plugins);
454
+ for (const scope of ['services', 'routes', 'consumers']) {
455
+ for (const s of arr(doc[scope]).map(obj)) { take(s?.plugins); for (const r of arr(s?.routes).map(obj)) take(r?.plugins); }
456
+ }
457
+ return out.slice(0, MAX_ITEMS);
458
+ }
459
+
460
+ function readKong(doc , source ) {
461
+ const m = empty('kong', source);
462
+ for (const p of kongGuardPlugins(doc)) {
463
+ const name = String(p.name);
464
+ const c = obj(p.config) ?? {};
465
+ const rules = obj(c.rules) ?? {};
466
+ if (isFalse(p.enabled)) { m.controls.push({ name, direction: 'input', injection: true, state: 'disabled', why: 'enabled: false', anchor: name }); continue; }
467
+ const mode = String(c.guarding_mode ?? '').toUpperCase();
468
+ m.controls.push({ name, direction: mode === 'BOTH' ? 'both' : mode === 'OUTPUT' ? 'output' : 'input', injection: /prompt-guard|guardrails|content-safety/.test(name), state: 'enforcing', why: null, anchor: name });
469
+ if (name === 'ai-prompt-guard' && isTrue(c.allow_all_conversation_history)) m.blindSpots.push({ name, why: 'allow_all_conversation_history: true - earlier turns are ignored, so an instruction planted before the last message is never matched', anchor: 'allow_all_conversation_history' });
470
+ if (/prompt-guard/.test(name) && !isTrue(c.match_all_roles ?? rules.match_all_roles)) m.blindSpots.push({ name, why: 'match_all_roles is off - only user-role messages are matched, and the caller writes the assistant and system turns too', anchor: 'match_all_roles' in c || 'match_all_roles' in rules ? 'match_all_roles' : name });
471
+ if (isFalse(c.stop_on_error)) m.failOpen.push({ name, why: 'stop_on_error: false - an unreachable guardrail service lets the request through', anchor: 'stop_on_error' });
472
+ if (typeof c.content_safety_url === 'string') m.endpoints.push({ url: c.content_safety_url, anchor: 'content_safety_url' });
473
+ }
474
+ return m;
475
+ }
476
+
477
+ export function hclBlocks(text , header ) {
478
+ const out = [];
479
+ const re = new RegExp(header.source, header.flags.includes('g') ? header.flags : header.flags + 'g');
480
+ let m ;
481
+ while ((m = re.exec(text)) && out.length < MAX_ITEMS) {
482
+ const open = text.indexOf('{', m.index + m[0].length);
483
+ if (open < 0 || /\S/.test(text.slice(m.index + m[0].length, open).replace(/=/, ''))) continue;
484
+ let depth = 0;
485
+ let quote = false;
486
+ for (let i = open; i < text.length; i++) {
487
+ const ch = text[i];
488
+ if (quote) { if (ch === '\\') i++; else if (ch === '"') quote = false; continue; }
489
+ if (ch === '"') quote = true;
490
+ else if (ch === '{') depth++;
491
+ else if (ch === '}' && --depth === 0) { out.push(text.slice(open + 1, i)); re.lastIndex = i; break; }
492
+ }
493
+ }
494
+ return out;
495
+ }
496
+
497
+ export function hclAttrs(body ) {
498
+ const out = {};
499
+ let depth = 0;
500
+ for (const line of body.split(/\r?\n/)) {
501
+ if (depth === 0) {
502
+ const a = /^\s*([A-Za-z_][\w-]*)\s*=\s*(?:"((?:[^"\\]|\\.)*)"|([^\s#/{[]+))/.exec(line);
503
+ if (a) out[a[1]] = a[2] ?? a[3];
504
+ }
505
+ depth += (line.match(/\{/g) ?? []).length - (line.match(/\}/g) ?? []).length;
506
+ if (depth < 0) depth = 0;
507
+ }
508
+ return out;
509
+ }
510
+
511
+ const PASS_THROUGH_RE = /^(?:noop|nofix|none|pass|log|reask_?none)$/i;
512
+
513
+ function passThroughValidators(doc , into ) {
514
+ let seen = 0;
515
+ const visit = (n , depth ) => {
516
+ if (depth > 12 || seen > MAX_ITEMS || !n || typeof n !== 'object') return;
517
+ if (Array.isArray(n)) { for (const x of n.slice(0, MAX_ITEMS)) visit(x, depth + 1); return; }
518
+ seen++;
519
+ const action = str(pick(n, 'on_fail', 'onFail', 'on-fail'));
520
+ if (action && PASS_THROUGH_RE.test(action.trim())) {
521
+ const name = String(n.name ?? n.id ?? n.type ?? 'validator');
522
+ if (!into.controls.some((c) => c.validator && c.name === name)) {
523
+ into.controls.push({ name, direction: 'unknown', injection: INJECTION_NAME_RE.test(name), state: 'observe-only', validator: true, why: `on_fail: ${action.trim()}`, anchor: 'on_fail' in n ? 'on_fail' : 'onFail' in n ? 'onFail' : name });
524
+ }
525
+ }
526
+ for (const v of Object.values(n)) visit(v, depth + 1);
527
+ };
528
+ visit(doc, 0);
529
+ }
530
+
531
+ export function guardrailModel(path , text , framework , siblingTasks = []) {
532
+ const source = path;
533
+ switch (framework) {
534
+ case 'colang': return readColang(text, source);
535
+ case 'bedrock': return readBedrock(path, text, source);
536
+ case 'azure-rai': return readAzureRai(path, text, source);
537
+ default: break;
538
+ }
539
+ if (framework === 'guardrails-ai' && /\.rail$/i.test(path)) return readRail(text, source);
540
+ const doc = obj(parseDoc(path, text));
541
+ if (!doc) return null;
542
+ const m = readDoc(framework, doc, source, siblingTasks);
543
+ if (m) passThroughValidators(doc, m);
544
+ return m;
545
+ }
546
+
547
+ function readDoc(framework , doc , source , siblingTasks ) {
548
+ switch (framework) {
549
+ case 'nemo': return readNemo(doc, source, siblingTasks);
550
+ case 'guardrails-ai': return readGuardJson(doc, source);
551
+ case 'litellm': return readLitellm(doc, source);
552
+ case 'portkey': return readPortkey(doc, source);
553
+ case 'llm-guard': return readLlmGuard(doc, source);
554
+ case 'kong': return readKong(doc, source);
555
+ default: return null;
556
+ }
557
+ }
558
+
559
+ export function nemoPromptTasks(text , path ) {
560
+ if (!text || !/\bprompts\s*:/.test(text)) return [];
561
+ const doc = obj(parseDoc(path, text));
562
+ return arr(doc?.prompts).map((p) => String(obj(p)?.task ?? '')).filter(Boolean);
563
+ }
564
+