@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.
- package/package.json +1 -1
- package/src/agents/hook-command.mjs +1 -1
- package/src/artifacts/matchers.mjs +7 -0
- package/src/cli/flags.mjs +2 -2
- package/src/cli/help-sections.mjs +7 -0
- package/src/cli/help.mjs +1 -1
- package/src/commands/check.mjs +3 -11
- package/src/commands/gate.mjs +26 -4
- package/src/commands/git-hooks.mjs +2 -2
- package/src/commands/ledger.mjs +0 -1
- package/src/commands/mcp-add.mjs +2 -1
- package/src/commands/memory-scan.mjs +135 -47
- package/src/commands/pr.mjs +7 -10
- package/src/commands/provenance.mjs +8 -13
- package/src/commands/scan.mjs +7 -1
- package/src/commands/secrets.mjs +4 -5
- package/src/core/git-exec.mjs +79 -0
- package/src/core/yaml-lite.mjs +300 -0
- package/src/core/zip-lite.mjs +37 -0
- package/src/detect/local-redact.mjs +1 -3
- package/src/detect/sast/rules-config.mjs +1 -1
- package/src/detect/sast/scanner.mjs +1 -1
- package/src/detect/signals/agent-frameworks.mjs +231 -0
- package/src/detect/signals/agent-graph-surface.mjs +113 -0
- package/src/detect/signals/agentic-ci-surface.mjs +314 -0
- package/src/detect/signals/agentic-shim.mjs +82 -0
- package/src/detect/signals/artifacts.mjs +7 -35
- package/src/detect/signals/chat-template.mjs +211 -0
- package/src/detect/signals/ci-workflow.mjs +169 -0
- package/src/detect/signals/gate.mjs +77 -9
- package/src/detect/signals/guardrail-shape.mjs +564 -0
- package/src/detect/signals/guardrail-surface.mjs +221 -0
- package/src/detect/signals/injection.mjs +8 -0
- package/src/detect/signals/inspect-shim.mjs +7 -0
- package/src/detect/signals/instruction-paths.mjs +60 -0
- package/src/detect/signals/manifests.mjs +302 -0
- package/src/detect/signals/mcp-advisories.mjs +109 -0
- package/src/detect/signals/mcp-config.mjs +598 -0
- package/src/detect/signals/memory-directives.mjs +661 -0
- package/src/detect/signals/memory-locations.mjs +158 -0
- package/src/detect/signals/memory.mjs +47 -29
- package/src/detect/signals/model-config-rules.mjs +655 -0
- package/src/detect/signals/model-config.mjs +61 -0
- package/src/detect/signals/prose-context.mjs +6 -9
- package/src/detect/signals/scan.mjs +4 -4
- package/src/detect/signals/secret-scanner.mjs +241 -0
- package/src/detect/signals/secrets.mjs +1 -48
- package/src/detect/signals/shell.mjs +3 -3
- package/src/gate/advisories.mjs +16 -0
- package/src/gate/batch.mjs +10 -0
- package/src/gate/environment.mjs +8 -53
- package/src/guard/artifact-paths.mjs +107 -0
- package/src/guard/classify.mjs +165 -7
- package/src/guard/command-resolve.mjs +35 -5
- package/src/guard/memory-write.mjs +218 -0
- package/src/guard/prompt-guard.mjs +0 -1
- package/src/guard/tool-guard.mjs +52 -77
- package/src/inventory/agent-posture.mjs +236 -57
- package/src/inventory/artifacts/classify.mjs +10 -1
- package/src/inventory/artifacts/discover.mjs +113 -3
- package/src/inventory/artifacts/extensions.mjs +70 -0
- package/src/inventory/artifacts/hook-scripts.mjs +128 -0
- package/src/inventory/artifacts/limits.mjs +1 -1
- package/src/inventory/artifacts/plugins.mjs +105 -0
- package/src/inventory/artifacts/roots.mjs +40 -0
- package/src/inventory/discovery/ai-dependencies.mjs +39 -12
- package/src/inventory/discovery/all.mjs +4 -0
- package/src/inventory/discovery/cloud-clis.mjs +472 -0
- package/src/inventory/discovery/coding-agents.mjs +19 -4
- package/src/inventory/discovery/mcp-clients.mjs +16 -10
- package/src/inventory/discovery/mcp-servers.mjs +125 -35
- package/src/inventory/discovery/mcp-stores.mjs +207 -0
- package/src/inventory/env-redirect.mjs +148 -0
- package/src/inventory/grant-extract.mjs +463 -0
- package/src/inventory/project-roots.mjs +108 -0
- package/src/inventory/vscode-state.mjs +153 -0
- package/src/mcp/server-tools.mjs +1 -1
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { parseYaml } from '../../core/yaml-lite.mjs';
|
|
3
|
+
|
|
4
|
+
const PROMPT_FIELD_RE = /^(?:template|system_?message|system_?prompt|systemMessagePrompt|humanMessagePrompt|prompt|instructions?|agent_description)$/i;
|
|
5
|
+
const MAX_PROMPTS = 40;
|
|
6
|
+
|
|
7
|
+
function takePrompts(into , node , fields , read ) {
|
|
8
|
+
for (const [k, v] of Object.entries(fields).slice(0, 200)) {
|
|
9
|
+
if (into.prompts.length >= MAX_PROMPTS || !PROMPT_FIELD_RE.test(k)) continue;
|
|
10
|
+
const text = read(v);
|
|
11
|
+
if (typeof text === 'string' && text.trim().length > 8) into.prompts.push({ node, text: text.slice(0, 8000) });
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const MAX = 200;
|
|
16
|
+
const obj = (v ) => (v && typeof v === 'object' && !Array.isArray(v) ? (v ) : null);
|
|
17
|
+
const arr = (v ) => (Array.isArray(v) ? v.slice(0, MAX) : []);
|
|
18
|
+
const isTrue = (v ) => v === true || (typeof v === 'string' && /^true$/i.test(v.trim()));
|
|
19
|
+
const names = (v ) => arr(v).map((t) => (typeof t === 'string' ? t : String(obj(t)?.name ?? obj(t)?.id ?? obj(t)?.provider ?? ''))).filter(Boolean).slice(0, 40);
|
|
20
|
+
|
|
21
|
+
function parse(path , text ) {
|
|
22
|
+
const t = text.replace(/^/, '');
|
|
23
|
+
if (/\.json$/i.test(path)) { try { return obj(JSON.parse(t)); } catch { return null; } }
|
|
24
|
+
if (/\.ya?ml$/i.test(path)) return obj(parseYaml(t));
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const CREW_FIELDS = ['role', 'goal', 'backstory'];
|
|
29
|
+
|
|
30
|
+
function isCrewAgentMap(doc ) {
|
|
31
|
+
const vals = Object.values(doc).slice(0, 50);
|
|
32
|
+
return vals.length > 0 && vals.every((v) => obj(v)) && vals.some((v) => CREW_FIELDS.filter((k) => k in v).length >= 2);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function agentFrameworkOf(path , text , doc ) {
|
|
36
|
+
if (!text || !/\.(json|ya?ml)$/i.test(path)) return null;
|
|
37
|
+
if (!/agent_class|autogen_|"nodes"|nodes\s*:|kind\s*:\s*app|backstory|langgraph|graphs|code_execution/i.test(text)) return null;
|
|
38
|
+
const d = doc === undefined ? parse(path, text) : doc;
|
|
39
|
+
if (!d) return null;
|
|
40
|
+
if (typeof d.agent_class === 'string' || (typeof d.instruction === 'string' && typeof d.model === 'string' && (Array.isArray(d.sub_agents) || Array.isArray(d.tools)))) return 'adk';
|
|
41
|
+
if (typeof d.provider === 'string' && /^autogen_(?:agentchat|ext|core)\./.test(d.provider)) return 'autogen';
|
|
42
|
+
if (d.kind === 'app' && obj(d.app) && (obj(d.workflow) || obj(d.model_config))) return 'dify';
|
|
43
|
+
const data = obj(d.data);
|
|
44
|
+
if (data && Array.isArray(data.nodes) && arr(data.nodes).some((n) => typeof obj(obj(n)?.data)?.type === 'string')) return 'langflow';
|
|
45
|
+
if (Array.isArray(d.nodes) && Array.isArray(d.edges) && arr(d.nodes).some((n) => typeof obj(obj(n)?.data)?.category === 'string' && typeof obj(obj(n)?.data)?.name === 'string')) return 'flowise';
|
|
46
|
+
if (obj(d.graphs) && /langgraph/i.test(path + text)) return 'langgraph';
|
|
47
|
+
if (isCrewAgentMap(d) || Array.isArray(d.agents) || obj(d.agents)) return /crew|backstory/i.test(path + text) ? 'crewai' : 'agent-framework';
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function crewAgents(doc ) {
|
|
52
|
+
const entries = Array.isArray(doc.agents)
|
|
53
|
+
? arr(doc.agents).map((a, i) => [String(obj(a)?.name ?? obj(a)?.role ?? `agent ${i + 1}`), a])
|
|
54
|
+
: obj(doc.agents)
|
|
55
|
+
? Object.entries(doc.agents)
|
|
56
|
+
: Object.entries(doc).filter(([, v]) => obj(v) && CREW_FIELDS.some((k) => k in v));
|
|
57
|
+
return entries.slice(0, 40).map(([name, raw]) => {
|
|
58
|
+
const a = obj(raw) ?? {};
|
|
59
|
+
const exec = isTrue(a.allow_code_execution);
|
|
60
|
+
const unsafe = String(a.code_execution_mode ?? '').toLowerCase() === 'unsafe';
|
|
61
|
+
return {
|
|
62
|
+
name,
|
|
63
|
+
role: typeof a.role === 'string' ? a.role.trim().slice(0, 120) : null,
|
|
64
|
+
tools: names(a.tools),
|
|
65
|
+
delegates: isTrue(a.allow_delegation ?? a.allowDelegation),
|
|
66
|
+
codeExec: exec ? (unsafe ? 'host' : 'container') : null,
|
|
67
|
+
codeExecWhy: exec ? `allow_code_execution: true, code_execution_mode: ${unsafe ? 'unsafe' : a.code_execution_mode ?? 'safe (default)'}` : null,
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function adkAgents(doc ) {
|
|
73
|
+
const own = {
|
|
74
|
+
name: String(doc.name ?? 'root_agent'),
|
|
75
|
+
role: typeof doc.description === 'string' ? doc.description.slice(0, 120) : null,
|
|
76
|
+
tools: names(doc.tools),
|
|
77
|
+
delegates: arr(doc.sub_agents).length > 0 && !isTrue(doc.disallow_transfer_to_peers),
|
|
78
|
+
codeExec: null,
|
|
79
|
+
codeExecWhy: null,
|
|
80
|
+
};
|
|
81
|
+
return [own];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function autogenAgents(doc ) {
|
|
85
|
+
const agents = [];
|
|
86
|
+
const visit = (n , depth ) => {
|
|
87
|
+
if (depth > 16 || agents.length >= 40 || !n || typeof n !== 'object') return;
|
|
88
|
+
if (Array.isArray(n)) { for (const x of n.slice(0, MAX)) visit(x, depth + 1); return; }
|
|
89
|
+
const provider = typeof n.provider === 'string' ? n.provider : '';
|
|
90
|
+
const cfg = obj(n.config) ?? {};
|
|
91
|
+
if (/\.agents\./.test(provider)) {
|
|
92
|
+
const exec = executorOf(cfg);
|
|
93
|
+
agents.push({ name: String(cfg.name ?? provider.split('.').pop()), role: typeof cfg.description === 'string' ? cfg.description.slice(0, 120) : null, tools: names(cfg.tools), delegates: false, codeExec: exec.where, codeExecWhy: exec.why });
|
|
94
|
+
}
|
|
95
|
+
for (const v of Object.values(n)) visit(v, depth + 1);
|
|
96
|
+
};
|
|
97
|
+
visit(doc, 0);
|
|
98
|
+
return agents;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function executorOf(cfg ) {
|
|
102
|
+
const found = [];
|
|
103
|
+
const visit = (n , depth ) => {
|
|
104
|
+
if (depth > 10 || !n || typeof n !== 'object') return;
|
|
105
|
+
if (typeof n.provider === 'string' && /code_executors?\.|CodeExecution|Executor/i.test(n.provider)) found.push(n.provider);
|
|
106
|
+
for (const v of Object.values(n)) visit(v, depth + 1);
|
|
107
|
+
};
|
|
108
|
+
visit(cfg, 0);
|
|
109
|
+
const local = found.find((p) => /LocalCommandLineCodeExecutor|JupyterCodeExecutor|\.local\./.test(p));
|
|
110
|
+
if (local) return { where: 'host', why: `${local.split('.').pop()} runs generated code as the host process` };
|
|
111
|
+
const docker = found.find((p) => /Docker/i.test(p));
|
|
112
|
+
if (docker) return { where: 'container', why: `${docker.split('.').pop()}` };
|
|
113
|
+
if (found.length) return { where: 'sandbox', why: found[0].split('.').pop() ?? null };
|
|
114
|
+
return { where: null, why: null };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const LANGFLOW_INTERPRETER_RE = /^(?:PythonREPL(?:Component|Tool)?|PythonInterpreter|PythonCodeStructuredTool|CodeInterpreter)$/i;
|
|
118
|
+
const USER_INPUT_TYPES_RE = /^(?:ChatInput|TextInput|Webhook(?:Component)?|APIRequest|start|trigger-webhook|chatflow|chatInput)$/i;
|
|
119
|
+
|
|
120
|
+
function reachableFrom(starts , edges ) {
|
|
121
|
+
const next = new Map ();
|
|
122
|
+
for (const e of edges.slice(0, 5000)) next.set(e.source, [...(next.get(e.source) ?? []), e.target]);
|
|
123
|
+
const seen = new Set ();
|
|
124
|
+
const queue = [...starts];
|
|
125
|
+
while (queue.length && seen.size < 5000) {
|
|
126
|
+
const id = queue.shift() ;
|
|
127
|
+
for (const t of next.get(id) ?? []) if (!seen.has(t)) { seen.add(t); queue.push(t); }
|
|
128
|
+
}
|
|
129
|
+
return seen;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function langflow(doc , m ) {
|
|
133
|
+
const data = obj(doc.data) ?? {};
|
|
134
|
+
const nodes = arr(data.nodes).map(obj).filter(Boolean) ;
|
|
135
|
+
const edges = arr(data.edges).map((e) => ({ source: String(obj(e)?.source ?? ''), target: String(obj(e)?.target ?? '') }));
|
|
136
|
+
m.nodeCount = nodes.length;
|
|
137
|
+
const inputs = nodes.filter((n) => USER_INPUT_TYPES_RE.test(String(obj(n.data)?.type ?? ''))).map((n) => String(n.id));
|
|
138
|
+
const reach = reachableFrom(inputs, edges);
|
|
139
|
+
for (const n of nodes) {
|
|
140
|
+
const d = obj(n.data) ?? {};
|
|
141
|
+
const type = String(d.type ?? '');
|
|
142
|
+
const template = obj(obj(d.node)?.template) ?? {};
|
|
143
|
+
takePrompts(m, String(obj(d.node)?.display_name ?? type), template, (v) => obj(v)?.value);
|
|
144
|
+
const interpreter = LANGFLOW_INTERPRETER_RE.test(type);
|
|
145
|
+
const source = typeof obj(template.code)?.value === 'string' ? String(obj(template.code) .value) : '';
|
|
146
|
+
const extra = [obj(template.python_code)?.value, obj(template.tool_code)?.value].filter((x) => typeof x === 'string').join('\n');
|
|
147
|
+
if (!interpreter && !source && !extra) continue;
|
|
148
|
+
m.codeNodes.push({
|
|
149
|
+
name: String(obj(d.node)?.display_name ?? type), type, language: 'python', code: [source, extra].filter(Boolean).join('\n'),
|
|
150
|
+
interpreter, fromUserInput: reach.has(String(n.id)), sandboxed: false, ...langflowProvenance(d, type, source),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function langflowProvenance(d , type , source ) {
|
|
156
|
+
const node = obj(d.node) ?? {};
|
|
157
|
+
const declared = typeof obj(node.metadata)?.code_hash === 'string' ? String(obj(node.metadata) .code_hash) : null;
|
|
158
|
+
const actual = source ? createHash('sha256').update(source, 'utf8').digest('hex').slice(0, 12) : null;
|
|
159
|
+
const module = typeof obj(node.metadata)?.module === 'string' ? String(obj(node.metadata) .module) : null;
|
|
160
|
+
if (actual && declared && actual !== declared) return { provenance: 'altered', declaredHash: declared, actualHash: actual };
|
|
161
|
+
|
|
162
|
+
const custom = /^Custom/i.test(type) || isTrue(node.edited) || (module !== null && !/^(?:lfx|langflow)\.components\./.test(module));
|
|
163
|
+
return { provenance: custom ? 'custom' : 'claimed-stock', declaredHash: declared, actualHash: actual };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const FLOWISE_CODE_INPUTS = ['javascriptFunction', 'customToolFunc', 'ifFunction', 'elseFunction', 'customFunctionJavascriptFunction', 'code'];
|
|
167
|
+
|
|
168
|
+
function flowise(doc , m ) {
|
|
169
|
+
const nodes = arr(doc.nodes).map(obj).filter(Boolean) ;
|
|
170
|
+
const edges = arr(doc.edges).map((e) => ({ source: String(obj(e)?.source ?? ''), target: String(obj(e)?.target ?? '') }));
|
|
171
|
+
m.nodeCount = nodes.length;
|
|
172
|
+
const inputs = nodes.filter((n) => /chat|start|webhook/i.test(String(obj(n.data)?.name ?? ''))).map((n) => String(n.id));
|
|
173
|
+
const reach = reachableFrom(inputs, edges);
|
|
174
|
+
for (const n of nodes) {
|
|
175
|
+
const d = obj(n.data) ?? {};
|
|
176
|
+
const name = String(d.name ?? '');
|
|
177
|
+
const inp = obj(d.inputs) ?? {};
|
|
178
|
+
const code = FLOWISE_CODE_INPUTS.map((k) => inp[k]).filter((x) => typeof x === 'string' && x.trim()).join('\n');
|
|
179
|
+
takePrompts(m, String(d.label ?? name), inp, (v) => v);
|
|
180
|
+
const interpreter = /codeInterpreter|pythonInterpreter/i.test(name);
|
|
181
|
+
if (!code && !interpreter) {
|
|
182
|
+
if (/^(?:requestsGet|requestsPost|httpRequest)/i.test(name) && typeof inp.url === 'string') m.httpUrls.push(inp.url);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
m.codeNodes.push({ name: String(d.label ?? name), type: name, language: 'javascript', code, interpreter, fromUserInput: reach.has(String(n.id)), sandboxed: /E2B/i.test(name) });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function dify(doc , m ) {
|
|
190
|
+
const wf = obj(doc.workflow) ?? {};
|
|
191
|
+
const graph = obj(wf.graph) ?? {};
|
|
192
|
+
const nodes = arr(graph.nodes).map(obj).filter(Boolean) ;
|
|
193
|
+
const edges = arr(graph.edges).map((e) => ({ source: String(obj(e)?.source ?? ''), target: String(obj(e)?.target ?? '') }));
|
|
194
|
+
m.nodeCount = nodes.length;
|
|
195
|
+
const starts = nodes.filter((n) => USER_INPUT_TYPES_RE.test(String(obj(n.data)?.type ?? ''))).map((n) => String(n.id));
|
|
196
|
+
const reach = reachableFrom(starts, edges);
|
|
197
|
+
for (const n of nodes) {
|
|
198
|
+
const d = obj(n.data) ?? {};
|
|
199
|
+
const type = String(d.type ?? '');
|
|
200
|
+
if (type === 'code' && typeof d.code === 'string') {
|
|
201
|
+
m.codeNodes.push({ name: String(d.title ?? 'code'), type, language: /javascript/i.test(String(d.code_language)) ? 'javascript' : 'python', code: d.code, interpreter: false, fromUserInput: reach.has(String(n.id)), sandboxed: true });
|
|
202
|
+
}
|
|
203
|
+
if (type === 'http-request' && typeof d.url === 'string') m.httpUrls.push(d.url);
|
|
204
|
+
if (type === 'llm' || type === 'agent' || type === 'agent-v2') {
|
|
205
|
+
const tpl = Array.isArray(d.prompt_template) ? arr(d.prompt_template).map((p) => obj(p)?.text).filter((x) => typeof x === 'string').join('\n') : d.prompt_template;
|
|
206
|
+
takePrompts(m, String(d.title ?? type), { prompt: tpl, instruction: d.instruction ?? obj(obj(d.agent_parameters)?.instruction)?.value }, (v) => v);
|
|
207
|
+
}
|
|
208
|
+
if (type === 'agent' || type === 'agent-v2') m.agents.push({ name: String(d.title ?? 'agent'), role: null, tools: names(obj(d.agent_parameters)?.tools?.value ?? d.tools), delegates: false, codeExec: null, codeExecWhy: null });
|
|
209
|
+
}
|
|
210
|
+
for (const v of arr(wf.environment_variables).map(obj)) {
|
|
211
|
+
if (v && String(v.value_type) === 'secret' && typeof v.value === 'string' && v.value.trim()) m.exportedSecrets.push(String(v.name ?? 'secret'));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function agentFrameworkModel(path , text , fw ) {
|
|
216
|
+
const doc = parse(path, text);
|
|
217
|
+
const framework = fw ?? agentFrameworkOf(path, text, doc);
|
|
218
|
+
if (!doc || !framework) return null;
|
|
219
|
+
const m = { framework, agents: [], graphs: [], codeNodes: [], httpUrls: [], exportedSecrets: [], nodeCount: 0, prompts: [] };
|
|
220
|
+
switch (framework) {
|
|
221
|
+
case 'adk': m.agents = adkAgents(doc); break;
|
|
222
|
+
case 'autogen': m.agents = autogenAgents(doc); break;
|
|
223
|
+
case 'langflow': langflow(doc, m); break;
|
|
224
|
+
case 'flowise': flowise(doc, m); break;
|
|
225
|
+
case 'dify': dify(doc, m); break;
|
|
226
|
+
default: m.agents = crewAgents(doc); break;
|
|
227
|
+
}
|
|
228
|
+
if (obj(doc.graphs)) m.graphs = Object.keys(doc.graphs).slice(0, 20);
|
|
229
|
+
return m;
|
|
230
|
+
}
|
|
231
|
+
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { agentFrameworkModel, } from './agent-frameworks.mjs';
|
|
2
|
+
import { locate } from './agentic-shim.mjs';
|
|
3
|
+
import { codeFindings, describesAt, egressFindings, prohibitsAt, shellFindings, ssrfFindings, textFindings } from './agentic-shim.mjs';
|
|
4
|
+
|
|
5
|
+
const FRAMEWORK_LABEL = {
|
|
6
|
+
crewai: 'CrewAI', langgraph: 'LangGraph', autogen: 'AutoGen', adk: 'Google ADK', langflow: 'Langflow',
|
|
7
|
+
flowise: 'Flowise', dify: 'Dify', 'agent-framework': 'agent framework',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const STRUCTURAL_SIGNAL_RE = /directive tag|forced output|verdict coercion/i;
|
|
11
|
+
|
|
12
|
+
const CONDITIONAL_DEFENCE_RE =
|
|
13
|
+
/\b(?:if|when|whenever|should)\b[^.\n]{0,160}\b(?:attempts?|tries|try|asks?|tells?|instructs?|contains?|says?|claims?)\b|\b(?:prompt[\s-]injection|jailbreak(?:s|ing)?|confidentiality)\s*[:—-]|\b(?:never|do not|don't|must not|refuse to)\s+(?:[\w-]+,?\s+(?:or\s+)?){0,5}(?:reveal|share|disclose|repeat|recite|follow|comply|obey|execute|print|expose)\b/i;
|
|
14
|
+
function instructionLines(text ) {
|
|
15
|
+
return text.split('\n').filter((l) => !prohibitsAt(l) && !describesAt(l) && !CONDITIONAL_DEFENCE_RE.test(l)).join('\n');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function hostExecFindings(a , m , label ) {
|
|
19
|
+
const out = [];
|
|
20
|
+
const delegation = m.agents.some((g) => g.delegates);
|
|
21
|
+
for (const g of m.agents.filter((x) => x.codeExec === 'host' || x.codeExec === 'container').slice(0, 10)) {
|
|
22
|
+
const host = g.codeExec === 'host';
|
|
23
|
+
out.push({
|
|
24
|
+
class: host ? 'OVER_PERMISSIONED' : 'INSECURE_CONFIG',
|
|
25
|
+
severity: host ? 'CRITICAL' : 'LOW',
|
|
26
|
+
title: host ? `${label} agent "${g.name}" runs generated code on the host` : `${label} agent "${g.name}" executes generated code in a container`,
|
|
27
|
+
detail: host
|
|
28
|
+
? `${g.codeExecWhy}. Whatever the model writes - including code an injected document or page asked for - runs as the process that started the agents: same filesystem, same credentials, same network.${delegation ? ' Delegation is on in this configuration, so any agent can hand work to this one and reach its executor.' : ''}`
|
|
29
|
+
: `${g.codeExecWhy}. Container execution is the right boundary; confirm what the container mounts and what network it reaches, because generated code runs with exactly that.`,
|
|
30
|
+
remediationText: host
|
|
31
|
+
? 'Run generated code in an isolated sandbox (a container without host mounts or credentials, or a remote sandbox such as E2B / Modal), or remove code execution from the agent.'
|
|
32
|
+
: 'Keep the executor image minimal, mount only a scratch directory, and deny network egress it does not need.',
|
|
33
|
+
remediationTier: host ? 1 : 3,
|
|
34
|
+
evidence: { agent: g.name, codeExec: g.codeExec, why: g.codeExecWhy, framework: m.framework, path: a.path, ...locate(host ? /unsafe|LocalCommandLine|Jupyter/ : /allow_code_execution|Docker/, a.content) },
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const MAX_CODE_NODES = 200;
|
|
41
|
+
|
|
42
|
+
function codeNodeFindings(a , m , label , n ) {
|
|
43
|
+
const out = [];
|
|
44
|
+
if (n.interpreter) {
|
|
45
|
+
out.push({
|
|
46
|
+
class: 'OVER_PERMISSIONED',
|
|
47
|
+
severity: n.fromUserInput && !n.sandboxed ? 'CRITICAL' : n.sandboxed ? 'MEDIUM' : 'HIGH',
|
|
48
|
+
title: n.fromUserInput
|
|
49
|
+
? `${label} flow "${a.name}" wires user input into a code interpreter ("${n.name}")`
|
|
50
|
+
: `${label} flow "${a.name}" contains a code interpreter ("${n.name}")`,
|
|
51
|
+
detail:
|
|
52
|
+
`The ${n.type} node executes ${n.language === 'python' ? 'Python' : 'code'} it is handed at run time${n.sandboxed ? ' in a remote sandbox' : ' inside the flow server\'s own process'}.` +
|
|
53
|
+
(n.fromUserInput ? ' It is reachable from the flow\'s chat / API input, so anyone who can send the flow a message can make it run code - with the server\'s environment variables, stored credentials and network.' : ' Anything that can steer the model driving it can choose the code.'),
|
|
54
|
+
remediationText: 'Remove the interpreter from flows that serve untrusted users, or point it at an isolated sandbox with no credentials; never expose such a flow on an unauthenticated endpoint.',
|
|
55
|
+
remediationTier: 1,
|
|
56
|
+
evidence: { node: n.name, type: n.type, fromUserInput: n.fromUserInput, sandboxed: n.sandboxed, framework: m.framework, path: a.path, ...locate(n.type, a.content) },
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
if (n.provenance === 'altered') {
|
|
60
|
+
out.push({
|
|
61
|
+
class: 'INSECURE_CONFIG',
|
|
62
|
+
severity: 'LOW',
|
|
63
|
+
title: `${label} node "${n.name}" runs code that no longer matches its declared hash`,
|
|
64
|
+
detail: `The export declares code_hash ${n.declaredHash} but the code it carries hashes to ${n.actualHash}. Langflow gates custom code on that hash server-side, so this node is the flow's own code - edited in place, or altered after export - and is graded as such rather than as a shipped component.`,
|
|
65
|
+
remediationText: 'Review the node\'s code; if it was not deliberately customised, replace it with the stock component.',
|
|
66
|
+
remediationTier: 3,
|
|
67
|
+
evidence: { node: n.name, declaredHash: n.declaredHash, actualHash: n.actualHash, path: a.path, ...locate(n.declaredHash ?? n.name, a.content) },
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
if (!n.code.trim()) return out;
|
|
71
|
+
const where = `${n.type} node "${n.name}" of ${label} flow "${a.name}"`;
|
|
72
|
+
const virtualPath = `${a.path}#${n.name.replace(/[^\w.-]+/g, '_')}.${n.language === 'javascript' ? 'js' : 'py'}`;
|
|
73
|
+
const all = [...shellFindings(a, n.code, where), ...codeFindings(a, virtualPath, n.code, { context: 'agent' }), ...egressFindings(a, n.code, where)];
|
|
74
|
+
|
|
75
|
+
const graded = n.provenance === 'claimed-stock' ? all.filter((f) => f.severity === 'HIGH' || f.severity === 'CRITICAL') : all;
|
|
76
|
+
for (const f of graded.slice(0, 6)) {
|
|
77
|
+
const ev = (f.evidence ?? {}) ;
|
|
78
|
+
|
|
79
|
+
if (typeof ev.snippet === 'string' && !(a.content ?? '').includes(ev.snippet)) { delete ev.snippet; delete ev.snippetStartLine; delete ev.line; }
|
|
80
|
+
out.push({ ...f, severity: n.sandboxed && f.severity === 'CRITICAL' ? 'HIGH' : f.severity, evidence: { ...ev, node: n.name, sandboxed: n.sandboxed, provenance: n.provenance ?? 'custom', ...(ev.line == null ? locate(n.name, a.content) : {}) } });
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function frameworkFindings(a ) {
|
|
86
|
+
const m = agentFrameworkModel(a.path, a.content ?? '', (a.meta?.framework ) ?? null);
|
|
87
|
+
if (!m) return [];
|
|
88
|
+
const label = FRAMEWORK_LABEL[m.framework];
|
|
89
|
+
const out = hostExecFindings(a, m, label);
|
|
90
|
+
for (const n of m.codeNodes.slice(0, MAX_CODE_NODES)) out.push(...codeNodeFindings(a, m, label, n));
|
|
91
|
+
|
|
92
|
+
for (const p of m.prompts) {
|
|
93
|
+
for (const f of textFindings(a, instructionLines(p.text), 'PROMPT_INJECTION').filter((x) => x.class === 'PROMPT_INJECTION')) {
|
|
94
|
+
const signals = ((f.evidence?.signals ) ?? []).filter((s) => !STRUCTURAL_SIGNAL_RE.test(s));
|
|
95
|
+
if (!signals.length) continue;
|
|
96
|
+
out.push({ ...f, title: `Injected instruction in the "${p.node}" prompt of ${label} flow "${a.name}"`, evidence: { ...(f.evidence ?? {}), signals, node: p.node, path: a.path } });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
for (const url of [...new Set(m.httpUrls)].slice(0, 10)) out.push(...ssrfFindings(a, url, `HTTP node of ${label} flow "${a.name}"`));
|
|
100
|
+
if (m.exportedSecrets.length) {
|
|
101
|
+
out.push({
|
|
102
|
+
class: 'SECRET_EXPOSURE',
|
|
103
|
+
severity: 'HIGH',
|
|
104
|
+
title: `${label} export "${a.name}" contains secret variable values`,
|
|
105
|
+
detail: `${m.exportedSecrets.slice(0, 6).join(', ')} ${m.exportedSecrets.length === 1 ? 'is a' : 'are'} secret-typed environment variable${m.exportedSecrets.length === 1 ? '' : 's'} exported with ${m.exportedSecrets.length === 1 ? 'its' : 'their'} value. Anyone the DSL file is shared with, or any repository it is committed to, holds the credential.`,
|
|
106
|
+
remediationText: 'Rotate the credentials and re-export without secrets (Dify leaves secret values empty unless include_secret is set).',
|
|
107
|
+
remediationTier: 1,
|
|
108
|
+
evidence: { variables: m.exportedSecrets, path: a.path, ...locate(m.exportedSecrets[0], a.content) },
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
|