@clear-capabilities/agentic-security-scanner 0.185.0 → 0.186.2
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 +34 -0
- package/bin/agentic-security.js +66 -18
- package/dist/1128.index.js +25 -21
- package/dist/{5817.index.js → 1602.index.js} +378 -29
- package/dist/2584.index.js +1 -1
- package/dist/4171.index.js +2 -2
- package/dist/{0b301d45b231f144a1b4.json → 4d06c8944ae121d7085c.json} +8 -8
- package/dist/5752.index.js +466 -0
- package/dist/7405.index.js +1 -1
- package/dist/8158.index.js +1713 -0
- package/dist/8798.index.js +1 -1
- package/dist/9382.index.js +2 -2
- package/dist/9560.index.js +1 -1
- package/dist/9579.index.js +1 -1
- package/dist/agentic-security.mjs +29 -29
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +8 -8
- package/src/dataflow/CLAUDE.md +6 -3
- package/src/dataflow/access-paths.js +12 -2
- package/src/dataflow/demand-query.js +279 -40
- package/src/dataflow/dep-fingerprint.js +1 -1
- package/src/dataflow/exploit-prover.js +6 -2
- package/src/dataflow/ir-cache.js +268 -0
- package/src/dataflow/privacy-taint.js +7 -0
- package/src/engine.js +62 -5
- package/src/ir/CLAUDE.md +4 -0
- package/src/ir/callgraph.js +36 -8
- package/src/ir/index.js +18 -4
- package/src/ir/parser-js.js +10 -0
- package/src/language/CLAUDE.md +13 -0
- package/src/language/nix-option-types.js +170 -0
- package/src/language/nix-script-taint.js +46 -3
- package/src/language/nix-secrets.js +20 -2
- package/src/lineage/CLAUDE.md +2 -1
- package/src/lineage/smt-orchestrator.js +47 -11
- package/src/lineage/smt-z3-backend.js +294 -0
- package/src/posture/CLAUDE.md +5 -1
- package/src/posture/business-logic.js +16 -7
- package/src/posture/dag-commands.js +442 -0
- package/src/posture/finding-defaults.js +19 -0
- package/src/posture/git-history.js +91 -1
- package/src/posture/proof-fabric/cli.js +1 -1
- package/src/posture/proof-fabric/container.js +2 -2
- package/src/posture/proof-fabric/generate-haskell.js +205 -0
- package/src/posture/proof-fabric/generate.js +6 -3
- package/src/posture/proof-fabric/reproducer.js +4 -2
- package/src/posture/proof-fabric/runner.js +4 -4
- package/src/sast/CLAUDE.md +2 -0
- package/src/sast/_call-args.js +70 -0
- package/src/sast/init-scope.js +87 -0
- package/src/sast/listen-bind-address.js +49 -0
- package/src/sast/redirect-target.js +106 -0
- package/src/sast/reflected-input.js +76 -0
- package/src/sast/send-file-root.js +27 -0
- package/src/sast/sensitive-data-logging.js +38 -1
- package/dist/119.index.js +0 -197
- package/dist/128.index.js +0 -129
- package/dist/1293.index.js +0 -5689
- package/dist/1336.index.js +0 -16331
- package/dist/1351.index.js +0 -133
- package/dist/1387.index.js +0 -271
- package/dist/1437.index.js +0 -23
- package/dist/1445.index.js +0 -110
- package/dist/1505.index.js +0 -151
- package/dist/1557.index.js +0 -293
- package/dist/1621.index.js +0 -311
- package/dist/1751.index.js +0 -160
- package/dist/1765.index.js +0 -727
- package/dist/1861.index.js +0 -690
- package/dist/1906.index.js +0 -433
- package/dist/208.index.js +0 -1631
- package/dist/2223.index.js +0 -159
- package/dist/2238.index.js +0 -295
- package/dist/2530.index.js +0 -152
- package/dist/2565.index.js +0 -116
- package/dist/2574.index.js +0 -127
- package/dist/2620.index.js +0 -345
- package/dist/2773.index.js +0 -98
- package/dist/2775.index.js +0 -32
- package/dist/2966.index.js +0 -171
- package/dist/2972.index.js +0 -87
- package/dist/2991.index.js +0 -131
- package/dist/3063.index.js +0 -833
- package/dist/3075.index.js +0 -227
- package/dist/3121.index.js +0 -297
- package/dist/3314.index.js +0 -263
- package/dist/3480.index.js +0 -1822
- package/dist/3481.index.js +0 -24
- package/dist/3506.index.js +0 -268
- package/dist/3533.index.js +0 -591
- package/dist/3653.index.js +0 -450
- package/dist/3740.index.js +0 -722
- package/dist/3822.index.js +0 -131
- package/dist/3868.index.js +0 -292
- package/dist/3918.index.js +0 -192
- package/dist/4090.index.js +0 -288
- package/dist/4113.index.js +0 -1286
- package/dist/4121.index.js +0 -1190
- package/dist/4142.index.js +0 -690
- package/dist/4168.index.js +0 -509
- package/dist/4281.index.js +0 -993
- package/dist/4336.index.js +0 -436
- package/dist/4387.index.js +0 -532
- package/dist/4413.index.js +0 -302
- package/dist/4476.index.js +0 -4510
- package/dist/4491.index.js +0 -133
- package/dist/4538.index.js +0 -185
- package/dist/4684.index.js +0 -319
- package/dist/4740.index.js +0 -165
- package/dist/4780.index.js +0 -209
- package/dist/4802.index.js +0 -225
- package/dist/4812.index.js +0 -379
- package/dist/4835.index.js +0 -126
- package/dist/494.index.js +0 -226
- package/dist/4991.index.js +0 -549
- package/dist/5199.index.js +0 -305
- package/dist/5216.index.js +0 -109
- package/dist/5439.index.js +0 -301
- package/dist/5443.index.js +0 -97
- package/dist/5496.index.js +0 -377
- package/dist/5738.index.js +0 -240
- package/dist/5801.index.js +0 -588
- package/dist/5858.index.js +0 -223
- package/dist/5933.index.js +0 -143
- package/dist/601.index.js +0 -23
- package/dist/6173.index.js +0 -273
- package/dist/6291.index.js +0 -1871
- package/dist/6312.index.js +0 -666
- package/dist/634.index.js +0 -16182
- package/dist/644.index.js +0 -87
- package/dist/6508.index.js +0 -283
- package/dist/6560.index.js +0 -645
- package/dist/6576.index.js +0 -422
- package/dist/6579.index.js +0 -271
- package/dist/6655.index.js +0 -3221
- package/dist/6745.index.js +0 -305
- package/dist/6818.index.js +0 -21642
- package/dist/6932.index.js +0 -1190
- package/dist/6972.index.js +0 -167
- package/dist/6995.index.js +0 -163
- package/dist/7249.index.js +0 -191
- package/dist/7372.index.js +0 -1197
- package/dist/7546.index.js +0 -541
- package/dist/76.index.js +0 -448
- package/dist/7879.index.js +0 -163
- package/dist/8025.index.js +0 -130
- package/dist/8054.index.js +0 -248
- package/dist/808.index.js +0 -1330
- package/dist/8097.index.js +0 -201
- package/dist/8126.index.js +0 -191
- package/dist/8178.index.js +0 -78
- package/dist/8382.index.js +0 -93
- package/dist/8408.index.js +0 -261
- package/dist/8540.index.js +0 -174
- package/dist/8635.index.js +0 -802
- package/dist/8688.index.js +0 -410
- package/dist/8699.index.js +0 -779
- package/dist/8891.index.js +0 -457
- package/dist/8916.index.js +0 -885
- package/dist/9146.index.js +0 -170
- package/dist/9168.index.js +0 -841
- package/dist/9292.index.js +0 -369
- package/dist/9422.index.js +0 -866
- package/dist/9440.index.js +0 -453
- package/dist/9479.index.js +0 -152
- package/dist/9481.index.js +0 -220
- package/dist/9539.index.js +0 -100
- package/dist/9547.index.js +0 -103
- package/dist/9692.index.js +0 -90
- package/dist/9800.index.js +0 -462
- package/dist/9899.index.js +0 -138
- package/dist/9981.index.js +0 -206
- package/dist/999.index.js +0 -117
- package/dist/9999.index.js +0 -1698
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
export const id =
|
|
2
|
-
export const ids = [
|
|
1
|
+
export const id = 1602;
|
|
2
|
+
export const ids = [1602,2199];
|
|
3
3
|
export const modules = {
|
|
4
4
|
|
|
5
|
-
/***/
|
|
5
|
+
/***/ 31602:
|
|
6
6
|
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
|
7
7
|
|
|
8
8
|
// ESM COMPAT FLAG
|
|
@@ -35,6 +35,348 @@ var proof_fabric_reproducer = __webpack_require__(37055);
|
|
|
35
35
|
var external_node_module_ = __webpack_require__(98995);
|
|
36
36
|
// EXTERNAL MODULE: ./node_modules/@babel/parser/lib/index.js
|
|
37
37
|
var lib = __webpack_require__(46411);
|
|
38
|
+
// EXTERNAL MODULE: external "node:child_process"
|
|
39
|
+
var external_node_child_process_ = __webpack_require__(31421);
|
|
40
|
+
;// CONCATENATED MODULE: ./src/posture/proof-fabric/generate-python.js
|
|
41
|
+
// Reproducer generation for Python (PRD 1), the second language of `generate.js`. Same contract: a DETERMINISTIC proposal built
|
|
42
|
+
// from the flagged file alone, then run through the ordinary runner, oracle, 3-of-3 repetition and independent replay. A generated
|
|
43
|
+
// reproducer is a proposal that execution either confirms or does not.
|
|
44
|
+
//
|
|
45
|
+
// Python is parsed by Python itself (`python3 -I -c`, the standard `ast` module, source on stdin, JSON on stdout) with a timeout, so
|
|
46
|
+
// the shape rules follow the real grammar. Two shapes, as in JavaScript:
|
|
47
|
+
// script the file reads `sys.argv[1]`: it is run with the payload as its argument (whether the flagged line is reached is then
|
|
48
|
+
// decided by execution, not assumed);
|
|
49
|
+
// function a module-level `def` around the flagged line: a generated harness imports the file (never as `__main__`, so its main
|
|
50
|
+
// guard does not run) and calls the function with the payload in every parameter.
|
|
51
|
+
// Only the standard library and sibling `.py` files in the flagged file's directory can be satisfied; any other import is refused,
|
|
52
|
+
// with the package named. Anything outside returns `{ ok: false }` with the reason, and nothing is executed.
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
const MAX_SOURCE = 256 * 1024;
|
|
59
|
+
const MAX_FILES = 8;
|
|
60
|
+
const fail = (reason, extra = {}) => ({ ok: false, reason, ...extra });
|
|
61
|
+
|
|
62
|
+
const ANALYZER = `
|
|
63
|
+
import ast, json, sys
|
|
64
|
+
src = sys.stdin.read()
|
|
65
|
+
try:
|
|
66
|
+
tree = ast.parse(src)
|
|
67
|
+
except SyntaxError as e:
|
|
68
|
+
print(json.dumps({"error": "syntax"})); sys.exit(0)
|
|
69
|
+
line = int(sys.argv[1])
|
|
70
|
+
imports = []
|
|
71
|
+
for n in ast.walk(tree):
|
|
72
|
+
if isinstance(n, ast.Import):
|
|
73
|
+
for a in n.names: imports.append({"module": a.name, "level": 0})
|
|
74
|
+
elif isinstance(n, ast.ImportFrom):
|
|
75
|
+
imports.append({"module": n.module or "", "level": n.level or 0, "names": [a.name for a in n.names]})
|
|
76
|
+
fns = []
|
|
77
|
+
for n in ast.walk(tree):
|
|
78
|
+
if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef)) and n.lineno <= line <= (n.end_lineno or n.lineno):
|
|
79
|
+
args = [a.arg for a in n.args.posonlyargs + n.args.args + n.args.kwonlyargs]
|
|
80
|
+
fns.append({"name": n.name, "start": n.lineno, "end": n.end_lineno, "args": args, "async": isinstance(n, ast.AsyncFunctionDef), "decorated": bool(n.decorator_list), "toplevel": n in tree.body, "defaults": len(n.args.defaults)})
|
|
81
|
+
fns.sort(key=lambda f: f["end"] - f["start"])
|
|
82
|
+
main_guard = any(isinstance(n, ast.If) and "__name__" in ast.dump(n.test) for n in tree.body)
|
|
83
|
+
print(json.dumps({"imports": imports, "functions": fns, "mainGuard": main_guard, "stdlib": sorted(sys.stdlib_module_names) if hasattr(sys, "stdlib_module_names") else None}))
|
|
84
|
+
`;
|
|
85
|
+
|
|
86
|
+
function analyze(text, line) {
|
|
87
|
+
const r = (0,external_node_child_process_.spawnSync)('python3', ['-I', '-c', ANALYZER, String(line)], { input: text, encoding: 'utf8', timeout: 10_000, maxBuffer: 4 * 1024 * 1024 });
|
|
88
|
+
if (r.error || r.status !== 0) return null;
|
|
89
|
+
try { return JSON.parse(r.stdout); } catch { return null; }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function readInside(root, rel) {
|
|
93
|
+
let real; try { real = external_node_fs_.realpathSync(external_node_path_.resolve(root, rel)); } catch { return null; }
|
|
94
|
+
const base = external_node_fs_.realpathSync(root);
|
|
95
|
+
if (!(real === base || real.startsWith(base + external_node_path_.sep))) return null;
|
|
96
|
+
const st = external_node_fs_.statSync(real);
|
|
97
|
+
return st.isFile() && st.size <= MAX_SOURCE ? { real, text: external_node_fs_.readFileSync(real, 'utf8') } : null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const HARNESS = (entry, name, args) => `# Generated by the proof fabric from the finding's own evidence. It imports the flagged module (never as __main__) and calls the function.
|
|
101
|
+
import importlib.util, sys, asyncio, inspect
|
|
102
|
+
payload = sys.argv[1] if len(sys.argv) > 1 else ""
|
|
103
|
+
spec = importlib.util.spec_from_file_location("target", ${JSON.stringify(entry)})
|
|
104
|
+
mod = importlib.util.module_from_spec(spec)
|
|
105
|
+
sys.modules["target"] = mod
|
|
106
|
+
spec.loader.exec_module(mod)
|
|
107
|
+
fn = getattr(mod, ${JSON.stringify(name)}, None)
|
|
108
|
+
if not callable(fn):
|
|
109
|
+
print("the function is not defined at module level", file=sys.stderr); sys.exit(3)
|
|
110
|
+
try:
|
|
111
|
+
res = fn(${args.join(', ')})
|
|
112
|
+
if inspect.isawaitable(res): asyncio.run(asyncio.wait_for(res, 5))
|
|
113
|
+
except Exception as e:
|
|
114
|
+
print("target raised:", str(e)[:200], file=sys.stderr)
|
|
115
|
+
`;
|
|
116
|
+
|
|
117
|
+
/** @param {{finding: object, root: string, cls: {name: string, payload: string, oracle: object, network?: string, watch?: object, needsPy?: RegExp}}} o */
|
|
118
|
+
function generatePythonReproducer({ finding, root, cls }) {
|
|
119
|
+
const rel = String(finding.file);
|
|
120
|
+
const src = readInside(root, rel);
|
|
121
|
+
if (!src) return fail('the flagged file is missing, outside the repository, or too large');
|
|
122
|
+
const info = analyze(src.text, finding.line);
|
|
123
|
+
if (!info || info.error) return fail('the flagged file could not be parsed by Python (is python3 installed and is the file valid Python 3?)');
|
|
124
|
+
const lines = src.text.split('\n');
|
|
125
|
+
const sinkLine = lines[finding.line - 1] ?? '';
|
|
126
|
+
const around = lines.slice(Math.max(0, finding.line - 3), finding.line + 2).join('\n');
|
|
127
|
+
if (cls.needsPy && !cls.needsPy.test(sinkLine) && !cls.needsPy.test(around)) return fail(`${cls.name}: the flagged code is not a write, and this class is only proven for write sinks`);
|
|
128
|
+
|
|
129
|
+
const stdlib = new Set(info.stdlib ?? []);
|
|
130
|
+
if (!info.stdlib) return fail('this Python is too old to list its standard library (3.10 or newer is needed), so imports cannot be classified');
|
|
131
|
+
const dir = external_node_path_.dirname(src.real); const rootReal = external_node_fs_.realpathSync(root);
|
|
132
|
+
const sources = {}; const external = [];
|
|
133
|
+
for (const imp of info.imports) {
|
|
134
|
+
const top = imp.module.split('.')[0];
|
|
135
|
+
if (imp.level === 0 && stdlib.has(top)) continue;
|
|
136
|
+
const sibling = imp.level <= 1 ? external_node_path_.join(dir, `${top || (imp.names?.[0] ?? '')}.py`) : null;
|
|
137
|
+
if (sibling && external_node_fs_.existsSync(sibling)) {
|
|
138
|
+
const dep = readInside(root, external_node_path_.relative(rootReal, external_node_fs_.realpathSync(sibling)));
|
|
139
|
+
if (!dep) return fail(`"${top}" is too large or outside the repository`);
|
|
140
|
+
const destRel = external_node_path_.relative(dir, dep.real);
|
|
141
|
+
if (!sources[destRel]) { if (Object.keys(sources).length >= MAX_FILES) return fail(`the file pulls in more than ${MAX_FILES} repository files`); sources[destRel] = external_node_path_.relative(rootReal, dep.real); }
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
external.push(top || '.'.repeat(imp.level));
|
|
145
|
+
}
|
|
146
|
+
if (external.length) return fail(`the flagged code needs ${[...new Set(external)].slice(0, 4).join(', ')}, which is not available in the proof environment; only the standard library and sibling files in the same directory can be satisfied`);
|
|
147
|
+
|
|
148
|
+
const entry = 'target.py';
|
|
149
|
+
sources[entry] = rel;
|
|
150
|
+
const assumptions = [];
|
|
151
|
+
const base = { schema: 'agentic-security/reproducer', version: 1, findingId: String(finding.stableId ?? finding.id ?? 'generated'), lang: 'python', oracle: cls.oracle, network: cls.network ?? 'none', ...(cls.watch ? { watch: cls.watch } : {}) };
|
|
152
|
+
if (cls.watch) assumptions.push('the write target is assumed to be one directory below the working directory (the payload is ../escaped.txt); if the code uses another base, the proof will not confirm it');
|
|
153
|
+
|
|
154
|
+
if (/sys\.argv\s*\[\s*1\s*\]/.test(src.text)) {
|
|
155
|
+
const reproducer = { ...base, sources, run: ['{{PYTHON}}', entry, cls.payload] };
|
|
156
|
+
const errs = (0,proof_fabric_reproducer/* validateReproducer */.mC)(reproducer);
|
|
157
|
+
return errs.length ? fail(`the generated reproducer is invalid: ${errs[0]}`) : { ok: true, reproducer, basis: { shape: 'script', class: cls.name, entry: rel, line: finding.line, assumptions } };
|
|
158
|
+
}
|
|
159
|
+
const fn = info.functions.find((f) => f.toplevel);
|
|
160
|
+
if (!fn) return fail('the flagged line is not inside a module-level function, and the file does not read sys.argv[1]');
|
|
161
|
+
if (fn.decorated) return fail(`${fn.name} is decorated (a web framework route, a cache, a wrapper): calling it directly would not match how it is reached, and the framework is not available`);
|
|
162
|
+
if (!fn.args.length) return fail('the function around the flagged line takes no parameters, so the payload has no way in');
|
|
163
|
+
if (fn.async) assumptions.push('the function is a coroutine; the harness awaits it for at most 5 seconds');
|
|
164
|
+
assumptions.push('the payload is passed in every parameter (a Python parameter has no request-like stand-in), which is an approximation of how the function is really called');
|
|
165
|
+
const harness = HARNESS(entry, fn.name, fn.args.map(() => 'payload'));
|
|
166
|
+
const reproducer = { ...base, sources, files: { 'harness.py': harness }, run: ['{{PYTHON}}', 'harness.py', cls.payload] };
|
|
167
|
+
const errs = (0,proof_fabric_reproducer/* validateReproducer */.mC)(reproducer);
|
|
168
|
+
return errs.length ? fail(`the generated reproducer is invalid: ${errs[0]}`) : { ok: true, reproducer, basis: { shape: 'function', class: cls.name, entry: rel, line: finding.line, function: `${fn.name}(${fn.args.join(', ')})`, assumptions } };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// EXTERNAL MODULE: ./src/language/haskell-parser.js + 1 modules
|
|
172
|
+
var haskell_parser = __webpack_require__(99864);
|
|
173
|
+
;// CONCATENATED MODULE: ./src/posture/proof-fabric/generate-haskell.js
|
|
174
|
+
// Reproducer generation for Haskell (PRD 1), the third language of `generate.js`. Same contract as JavaScript and Python: a
|
|
175
|
+
// DETERMINISTIC proposal built from the flagged file alone, then run through the ordinary runner, oracle, 3-of-3 repetition and
|
|
176
|
+
// independent replay. A generated reproducer is a proposal that execution either confirms or does not.
|
|
177
|
+
//
|
|
178
|
+
// The program is run by GHC's interpreter (`ghc -e`), not compiled. Why: it needs no C compiler and no linker (so no dependence on
|
|
179
|
+
// the platform's developer tools), it leaves no object files or binaries to account for, it starts in about half a second where a
|
|
180
|
+
// compile-then-run takes a second or more, and there is one process tree to confine instead of a build stage followed by a run
|
|
181
|
+
// stage. Interpreted bytecode calls the same installed libraries a compiled program would, so a shell string reaches the same
|
|
182
|
+
// `System.Process` either way. The tradeoff is stated, not hidden: a program whose behaviour depends on compiled code (FFI, an
|
|
183
|
+
// optimisation-sensitive race) is refused, and `-O` is never in play. A hand-written reproducer may still use `build`/`run` with
|
|
184
|
+
// `{{GHC}}` to compile; both paths run under the same sandbox, timeout and output cap.
|
|
185
|
+
//
|
|
186
|
+
// Two shapes, as in the other languages:
|
|
187
|
+
// script the file reads `getArgs`: it is run as `:main "<payload>"`, so the payload arrives as the first argument;
|
|
188
|
+
// function a top-level function around the flagged line that has a type signature of `String`/`FilePath` parameters and an `IO`
|
|
189
|
+
// result: `ghc -e 'fn "<payload>" ...'` loads the module with every top-level name in scope (the module need not export
|
|
190
|
+
// it) and calls it with the payload in every parameter.
|
|
191
|
+
//
|
|
192
|
+
// Only what the proof environment can load is accepted: the file's imports must be modules of the packages that ship with GHC (the
|
|
193
|
+
// global package database, read from the installed GHC) or other `.hs` files in the repository. A web framework, a database binding
|
|
194
|
+
// or any Hackage package is refused with the package named where it is known. Template Haskell, quasi-quotes, CPP, the foreign
|
|
195
|
+
// function interface and compiler-plugin options are refused too: they run code at load time, before the payload is involved.
|
|
196
|
+
//
|
|
197
|
+
// Classes: command injection (CWE-78) and path traversal by a write (CWE-22). SSRF (CWE-918) is refused for Haskell on purpose: no
|
|
198
|
+
// HTTP client ships with GHC, so a request-making program necessarily imports a package the stdlib-only rule already refuses, and
|
|
199
|
+
// nothing could be proven honestly. Nothing is executed on a refusal.
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
const generate_haskell_MAX_SOURCE = 256 * 1024;
|
|
208
|
+
const generate_haskell_MAX_FILES = 8;
|
|
209
|
+
const generate_haskell_fail = (reason, extra = {}) => ({ ok: false, reason, ...extra });
|
|
210
|
+
|
|
211
|
+
// Well-known packages that are NOT part of a GHC install, by module prefix, so a refusal can name the package to install.
|
|
212
|
+
const THIRD_PARTY = [
|
|
213
|
+
['Web.Scotty', 'scotty'], ['Network.Wai', 'wai'], ['Network.HTTP.Types', 'http-types'], ['Network.HTTP.Client', 'http-client'],
|
|
214
|
+
['Network.HTTP.Simple', 'http-conduit'], ['Network.HTTP.Conduit', 'http-conduit'], ['Network.Wreq', 'wreq'], ['Network.Socket', 'network'],
|
|
215
|
+
['Servant', 'servant'], ['Yesod', 'yesod'], ['Snap', 'snap'], ['Happstack', 'happstack-server'], ['Data.Aeson', 'aeson'],
|
|
216
|
+
['Database.SQLite.Simple', 'sqlite-simple'], ['Database.PostgreSQL.Simple', 'postgresql-simple'], ['Database.Persist', 'persistent'],
|
|
217
|
+
['Database.MySQL', 'mysql-simple'], ['System.Random', 'random'], ['Data.Vector', 'vector'], ['Data.Hashable', 'hashable'],
|
|
218
|
+
['Data.HashMap', 'unordered-containers'], ['Control.Lens', 'lens'], ['Test.', 'a test framework'], ['Turtle', 'turtle'],
|
|
219
|
+
['Shelly', 'shelly'], ['System.Process.Typed', 'typed-process'], ['Conduit', 'conduit'], ['Control.Monad.Logger', 'monad-logger'],
|
|
220
|
+
['Options.Applicative', 'optparse-applicative'], ['Data.Text.Lazy.Builder', null],
|
|
221
|
+
];
|
|
222
|
+
|
|
223
|
+
const libCache = new Map();
|
|
224
|
+
/** The modules the installed GHC exposes from its global package database (what a program can import with no extra packages). */
|
|
225
|
+
function bootModuleSet(ghc) {
|
|
226
|
+
if (libCache.has(ghc)) return libCache.get(ghc);
|
|
227
|
+
let set = null;
|
|
228
|
+
try {
|
|
229
|
+
const r = (0,external_node_child_process_.spawnSync)(ghc, ['--print-libdir'], { encoding: 'utf8', timeout: 10_000 });
|
|
230
|
+
const dir = r.status === 0 ? external_node_path_.join(r.stdout.trim(), 'package.conf.d') : null;
|
|
231
|
+
if (dir && external_node_fs_.existsSync(dir)) {
|
|
232
|
+
set = new Set();
|
|
233
|
+
for (const f of external_node_fs_.readdirSync(dir)) {
|
|
234
|
+
if (!f.endsWith('.conf')) continue;
|
|
235
|
+
const text = external_node_fs_.readFileSync(external_node_path_.join(dir, f), 'utf8');
|
|
236
|
+
if (/^exposed:\s*False/m.test(text)) continue;
|
|
237
|
+
const m = /^exposed-modules:\s*\n?([\s\S]*?)(?=^\S)/m.exec(text);
|
|
238
|
+
if (!m) continue;
|
|
239
|
+
for (const tok of m[1].split(/[\s,]+/)) if (/^[A-Z][\w']*(?:\.[A-Z][\w']*)*$/.test(tok)) set.add(tok);
|
|
240
|
+
}
|
|
241
|
+
if (!set.has('Prelude')) set = null; // a database that does not even list Prelude was not read correctly
|
|
242
|
+
}
|
|
243
|
+
} catch { set = null; }
|
|
244
|
+
libCache.set(ghc, set);
|
|
245
|
+
return set;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function generate_haskell_readInside(root, rel) {
|
|
249
|
+
let real; try { real = external_node_fs_.realpathSync(external_node_path_.resolve(root, rel)); } catch { return null; }
|
|
250
|
+
const base = external_node_fs_.realpathSync(root);
|
|
251
|
+
if (!(real === base || real.startsWith(base + external_node_path_.sep))) return null;
|
|
252
|
+
const st = external_node_fs_.statSync(real);
|
|
253
|
+
return st.isFile() && st.size <= generate_haskell_MAX_SOURCE ? { real, text: external_node_fs_.readFileSync(real, 'utf8') } : null;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Split a type on its top-level arrows (arrows inside parentheses or brackets belong to a function-typed argument). */
|
|
257
|
+
function splitArrows(type) {
|
|
258
|
+
const parts = []; let depth = 0; let cur = '';
|
|
259
|
+
for (let i = 0; i < type.length; i++) {
|
|
260
|
+
const c = type[i];
|
|
261
|
+
if ('([{'.includes(c)) depth++; else if (')]}'.includes(c)) depth--;
|
|
262
|
+
if (depth === 0 && c === '-' && type[i + 1] === '>') { parts.push(cur.trim()); cur = ''; i++; continue; }
|
|
263
|
+
cur += c;
|
|
264
|
+
}
|
|
265
|
+
parts.push(cur.trim());
|
|
266
|
+
return parts;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Compile-time code execution and options that change what runs before the payload is involved.
|
|
270
|
+
function loadTimeHazard(text) {
|
|
271
|
+
if (/\{-#\s*LANGUAGE[^#]*\b(?:TemplateHaskell|TemplateHaskellQuotes|QuasiQuotes|CPP|ForeignFunctionInterface|CApiFFI)\b/i.test(text)) return 'it enables Template Haskell, quasi-quotes, CPP or the foreign function interface, which run or require code at load time';
|
|
272
|
+
if (/\{-#\s*OPTIONS(?:_GHC)?\b[^#]*(?:-fplugin|-pgm|-opt[a-zA-Z]|-F\b|-cpp|-fobject-code)/i.test(text)) return 'its OPTIONS_GHC pragma names a plugin, a preprocessor or an external program';
|
|
273
|
+
if (/^\s*foreign\s+(?:import|export)\b/m.test(text)) return 'it uses the foreign function interface';
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function classify(mod, pkg, boot) {
|
|
278
|
+
if (pkg) return { ok: false, name: String(pkg).replace(/^"|"$/g, '') };
|
|
279
|
+
if (boot.has(mod)) return { ok: true };
|
|
280
|
+
for (const [prefix, name] of THIRD_PARTY) if (mod === prefix || mod.startsWith(prefix.endsWith('.') ? prefix : `${prefix}.`)) return { ok: false, name: name ?? mod };
|
|
281
|
+
return { ok: false, name: mod };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** @param {{finding: object, root: string, cls: {name: string, payload: string, oracle: object, network?: string, watch?: object, needsHs?: RegExp}, bootModules?: Set<string>}} o */
|
|
285
|
+
function generateHaskellReproducer({ finding, root, cls, bootModules }) {
|
|
286
|
+
const rel = String(finding.file);
|
|
287
|
+
if (cls.name === 'ssrf') return generate_haskell_fail('ssrf: no HTTP client ships with GHC, so any program that makes a request imports a package outside the proof environment; this class is not proven for Haskell');
|
|
288
|
+
const src = generate_haskell_readInside(root, rel);
|
|
289
|
+
if (!src) return generate_haskell_fail('the flagged file is missing, outside the repository, or too large');
|
|
290
|
+
const hazard = loadTimeHazard(src.text);
|
|
291
|
+
if (hazard) return generate_haskell_fail(`the flagged file is refused: ${hazard}`);
|
|
292
|
+
const info = (0,haskell_parser/* parseHaskell */.cX)(src.text);
|
|
293
|
+
if (!info || info.status !== 'parsed') return generate_haskell_fail('the flagged file could not be parsed as Haskell');
|
|
294
|
+
const lines = src.text.split('\n');
|
|
295
|
+
const sinkLine = lines[finding.line - 1] ?? '';
|
|
296
|
+
const around = lines.slice(Math.max(0, finding.line - 3), finding.line + 2).join('\n');
|
|
297
|
+
if (cls.needsHs && !cls.needsHs.test(sinkLine) && !cls.needsHs.test(around)) return generate_haskell_fail(`${cls.name}: the flagged code is not a write, and this class is only proven for write sinks`);
|
|
298
|
+
|
|
299
|
+
const tools = (0,runner/* hostTools */.qV)();
|
|
300
|
+
const boot = bootModules ?? (tools.ghc ? bootModuleSet(tools.ghc) : null);
|
|
301
|
+
if (!boot) return generate_haskell_fail('no working GHC was found to list the modules the proof environment can import (install ghc, or run `agentic-security proof doctor`)');
|
|
302
|
+
|
|
303
|
+
// the entry module is placed at its module path so that other modules can import it; Main goes to Main.hs
|
|
304
|
+
const modName = info.module?.name ?? 'Main';
|
|
305
|
+
const entry = `${modName.replaceAll('.', '/')}.hs`;
|
|
306
|
+
const rootReal = external_node_fs_.realpathSync(root);
|
|
307
|
+
const entryDir = external_node_path_.dirname(src.real);
|
|
308
|
+
const modRel = modName.replaceAll('.', external_node_path_.sep);
|
|
309
|
+
const srcRoot = src.real.endsWith(`${modRel}.hs`) ? src.real.slice(0, src.real.length - modRel.length - 3) : `${entryDir}${external_node_path_.sep}`;
|
|
310
|
+
const searchRoots = [...new Set([srcRoot.replace(/[\\/]$/, ''), ...['src', 'app', 'lib', ''].map((d) => external_node_path_.join(rootReal, d))])];
|
|
311
|
+
const sources = {}; const external = []; const seen = new Set([modName]);
|
|
312
|
+
let count = 0;
|
|
313
|
+
const visit = (imports) => {
|
|
314
|
+
for (const imp of imports) {
|
|
315
|
+
const mod = imp.module;
|
|
316
|
+
if (seen.has(mod)) continue;
|
|
317
|
+
seen.add(mod);
|
|
318
|
+
let local = null;
|
|
319
|
+
for (const base of searchRoots) { const cand = external_node_path_.join(base, `${mod.replaceAll('.', '/')}.hs`); const r = generate_haskell_readInside(root, external_node_path_.relative(rootReal, cand)); if (r) { local = r; break; } }
|
|
320
|
+
if (local && !imp.package) {
|
|
321
|
+
if (++count > generate_haskell_MAX_FILES) return generate_haskell_fail(`the file pulls in more than ${generate_haskell_MAX_FILES} repository modules`);
|
|
322
|
+
const dep = (0,haskell_parser/* parseHaskell */.cX)(local.text);
|
|
323
|
+
if (!dep || dep.status !== 'parsed') return generate_haskell_fail(`the module ${mod} could not be parsed as Haskell`);
|
|
324
|
+
const h = loadTimeHazard(local.text); if (h) return generate_haskell_fail(`the module ${mod} is refused: ${h}`);
|
|
325
|
+
sources[`${mod.replaceAll('.', '/')}.hs`] = external_node_path_.relative(rootReal, local.real);
|
|
326
|
+
const r = visit(dep.imports ?? []); if (r) return r;
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
const c = classify(mod, imp.package, boot);
|
|
330
|
+
if (!c.ok) external.push(c.name);
|
|
331
|
+
}
|
|
332
|
+
return null;
|
|
333
|
+
};
|
|
334
|
+
const bad = visit(info.imports ?? []);
|
|
335
|
+
if (bad) return bad;
|
|
336
|
+
if (external.length) return generate_haskell_fail(`the flagged code needs ${[...new Set(external)].slice(0, 4).join(', ')}, which is not available in the proof environment; only the packages that ship with GHC and files in the repository can be loaded`, { missing: [...new Set(external)] });
|
|
337
|
+
sources[entry] = rel;
|
|
338
|
+
|
|
339
|
+
const assumptions = [];
|
|
340
|
+
const base = { schema: 'agentic-security/reproducer', version: 1, findingId: String(finding.stableId ?? finding.id ?? 'generated'), lang: 'haskell', oracle: cls.oracle, network: cls.network ?? 'none', ...(cls.watch ? { watch: cls.watch } : {}) };
|
|
341
|
+
if (cls.watch) assumptions.push('the write target is assumed to be one directory below the working directory (the payload is ../escaped.txt); if the code uses another base, the proof will not confirm it');
|
|
342
|
+
const ghcRun = (expr) => ['{{GHC}}', '-ignore-dot-ghci', '-e', expr, entry];
|
|
343
|
+
const literal = (s) => JSON.stringify(s); // payloads are fixed strings with no quote or backslash; this is a plain Haskell string literal
|
|
344
|
+
|
|
345
|
+
// shape 1: a program that reads its payload from the command line
|
|
346
|
+
const hasMain = (info.functions ?? []).some((f) => f.scope === 'top' && f.name === 'main');
|
|
347
|
+
if (/\bgetArgs\b/.test(src.text) && hasMain) {
|
|
348
|
+
assumptions.push('the payload is passed as the first and only command-line argument; a program that expects other arguments will not reach the flagged line');
|
|
349
|
+
const reproducer = { ...base, sources, run: ghcRun(`:main ${literal(cls.payload)}`) };
|
|
350
|
+
const errs = (0,proof_fabric_reproducer/* validateReproducer */.mC)(reproducer);
|
|
351
|
+
return errs.length ? generate_haskell_fail(`the generated reproducer is invalid: ${errs[0]}`) : { ok: true, reproducer, basis: { shape: 'script', class: cls.name, entry: rel, line: finding.line, assumptions } };
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// shape 2: a top-level function around the flagged line
|
|
355
|
+
const fn = (info.functions ?? []).filter((f) => f.scope === 'top' && f.name !== 'main' && /^[a-z_][\w']*$/.test(f.name)).find((f) => {
|
|
356
|
+
const end = Math.max(f.span.endLine, ...(f.clauses ?? []).map((c) => c.span?.endLine ?? 0));
|
|
357
|
+
return f.span.startLine <= finding.line && end >= finding.line;
|
|
358
|
+
});
|
|
359
|
+
if (!fn) return generate_haskell_fail('the flagged line is not inside a top-level function, and the file does not read getArgs in a main');
|
|
360
|
+
const sig = (info.signatures ?? []).find((s) => s.scope === 'top' && s.names.includes(fn.name));
|
|
361
|
+
if (!sig) return generate_haskell_fail(`${fn.name} has no type signature, so the proof cannot tell what to pass it`);
|
|
362
|
+
const sigText = lines.slice(sig.span.startLine - 1, sig.span.endLine).join(' ');
|
|
363
|
+
const m = new RegExp(`${fn.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[^:]*::\\s*([\\s\\S]*)$`).exec(sigText);
|
|
364
|
+
if (!m) return generate_haskell_fail(`the type signature of ${fn.name} could not be read`);
|
|
365
|
+
const type = m[1].replace(/--.*$/, '').trim();
|
|
366
|
+
if (type.includes('=>') || /\bforall\b/.test(type)) return generate_haskell_fail(`${fn.name} is polymorphic or constrained (${type.slice(0, 60)}), so the proof cannot choose its arguments`);
|
|
367
|
+
const parts = splitArrows(type);
|
|
368
|
+
const result = parts[parts.length - 1]; const params = parts.slice(0, -1);
|
|
369
|
+
if (!/^IO\b/.test(result)) return generate_haskell_fail(`${fn.name} returns ${result.slice(0, 40)}, not IO, so calling it would not perform the flagged operation`);
|
|
370
|
+
if (!params.length) return generate_haskell_fail(`${fn.name} takes no parameters, so the payload has no way in`);
|
|
371
|
+
const bad2 = params.find((p) => !/^(?:String|FilePath|\[Char\])$/.test(p));
|
|
372
|
+
if (bad2) return generate_haskell_fail(`${fn.name} takes a parameter of type ${bad2.slice(0, 40)}; only String and FilePath can be filled with a payload`);
|
|
373
|
+
assumptions.push('the payload is passed in every parameter (a String has no request-like stand-in), which is an approximation of how the function is really called');
|
|
374
|
+
const call = `${fn.name} ${params.map(() => literal(cls.payload)).join(' ')} >>= \\_ -> return ()`;
|
|
375
|
+
const reproducer = { ...base, sources, run: ghcRun(call) };
|
|
376
|
+
const errs = (0,proof_fabric_reproducer/* validateReproducer */.mC)(reproducer);
|
|
377
|
+
return errs.length ? generate_haskell_fail(`the generated reproducer is invalid: ${errs[0]}`) : { ok: true, reproducer, basis: { shape: 'function', class: cls.name, entry: rel, line: finding.line, function: `${fn.name}(${params.length} String)`, assumptions } };
|
|
378
|
+
}
|
|
379
|
+
|
|
38
380
|
;// CONCATENATED MODULE: ./src/posture/proof-fabric/generate.js
|
|
39
381
|
// Reproducer generation from a finding's own evidence (PRD 1: `triage exploit --prove` with no hand-written reproducer).
|
|
40
382
|
//
|
|
@@ -44,7 +386,7 @@ var lib = __webpack_require__(46411);
|
|
|
44
386
|
// independent replay as any other. A generated reproducer is not a verdict; it is a proposal that is either confirmed by execution
|
|
45
387
|
// or not.
|
|
46
388
|
//
|
|
47
|
-
// What this is not: a general exploit generator. It handles JavaScript
|
|
389
|
+
// What this is not: a general exploit generator. It handles JavaScript (here), Python (`generate-python.js`) and Haskell (`generate-haskell.js`), and only code the proof environment can actually load:
|
|
48
390
|
// the file's imports must be Node built-ins or relative files inside the repository (a package from node_modules is not there in
|
|
49
391
|
// the sandbox, so a handler that needs `express` is refused, with the package named). Anything outside that returns `{ ok: false }`
|
|
50
392
|
// with the reason, and nothing is executed.
|
|
@@ -65,18 +407,20 @@ var lib = __webpack_require__(46411);
|
|
|
65
407
|
|
|
66
408
|
|
|
67
409
|
|
|
410
|
+
|
|
411
|
+
|
|
68
412
|
const CLASSES = {
|
|
69
413
|
'CWE-78': { name: 'command-injection', payload: 'x; touch {{CANARY:command}}', oracle: { type: 'command_execution', params: { canary: 'command' } } },
|
|
70
|
-
'CWE-22': { name: 'path-traversal', payload: '../escaped.txt', oracle: { type: 'filesystem_mutation', params: { canary: 'fs' } }, watch: { fs: 'escaped.txt' }, needs: /\b(?:writeFile|writeFileSync|appendFile|appendFileSync|createWriteStream)\b/ },
|
|
414
|
+
'CWE-22': { needsPy: /\bopen\s*\([^)]*['"][wax]b?\+?['"]|\.write_(?:text|bytes)\s*\(|\bshutil\.(?:copy|move)/, name: 'path-traversal', payload: '../escaped.txt', oracle: { type: 'filesystem_mutation', params: { canary: 'fs' } }, watch: { fs: 'escaped.txt' }, needsHs: /\b(?:writeFile|appendFile|copyFile|copyFileWithMetadata|renameFile|openBinaryFile)\b|\b(?:openFile|withFile|withBinaryFile)\b[^\n]*\b(?:WriteMode|AppendMode|ReadWriteMode)\b/, needs: /\b(?:writeFile|writeFileSync|appendFile|appendFileSync|createWriteStream)\b/ },
|
|
71
415
|
'CWE-918': { name: 'ssrf', payload: '{{CALLBACK_URL}}', oracle: { type: 'outbound_callback' }, network: 'callback' },
|
|
72
416
|
};
|
|
73
|
-
const
|
|
74
|
-
const
|
|
417
|
+
const generate_MAX_SOURCE = 256 * 1024;
|
|
418
|
+
const generate_MAX_FILES = 8;
|
|
75
419
|
const EXTS = ['', '.js', '.mjs', '.cjs', '/index.js'];
|
|
76
420
|
const BUILTINS = new Set(external_node_module_.builtinModules.flatMap((m) => [m, `node:${m}`]));
|
|
77
421
|
const MODULE_SYNTAX = /^\s*(?:import\s[^('"]|import\s*['"{*]|export\s)/m;
|
|
78
422
|
|
|
79
|
-
const
|
|
423
|
+
const generate_fail = (reason, extra = {}) => ({ ok: false, reason, ...extra });
|
|
80
424
|
const normCwe = (c) => { const m = /(\d{1,5})/.exec(String(c ?? '')); return m ? `CWE-${Number(m[1])}` : null; };
|
|
81
425
|
|
|
82
426
|
function inside(root, rel) {
|
|
@@ -91,7 +435,7 @@ function readSource(root, rel) {
|
|
|
91
435
|
const real = inside(root, rel);
|
|
92
436
|
if (!real) return null;
|
|
93
437
|
const st = external_node_fs_.statSync(real);
|
|
94
|
-
if (!st.isFile() || st.size >
|
|
438
|
+
if (!st.isFile() || st.size > generate_MAX_SOURCE) return null;
|
|
95
439
|
return { real, text: external_node_fs_.readFileSync(real, 'utf8') };
|
|
96
440
|
}
|
|
97
441
|
|
|
@@ -164,24 +508,29 @@ const P = new Proxy({}, { get: (_t, k) => (k === Symbol.toPrimitive ? () => payl
|
|
|
164
508
|
const res = new Proxy(function () {}, { get: () => res, apply: () => res });
|
|
165
509
|
const fn = ${pick};
|
|
166
510
|
if (typeof fn !== 'function') { console.error('the exported value is not a function'); process.exit(3); }
|
|
167
|
-
try { await Promise.race([Promise.resolve(fn(${args.join(', ')})), new Promise((r) => setTimeout(r, 5000))]); } catch (e) { console.error('target threw:', String(e && e.message).slice(0, 200)); }
|
|
511
|
+
let timer; try { await Promise.race([Promise.resolve(fn(${args.join(', ')})), new Promise((r) => { timer = setTimeout(r, 5000); })]); } catch (e) { console.error('target threw:', String(e && e.message).slice(0, 200)); }
|
|
512
|
+
clearTimeout(timer);
|
|
168
513
|
`;
|
|
169
514
|
}
|
|
170
515
|
|
|
171
516
|
function generateReproducer({ finding, root }) {
|
|
172
517
|
const cwe = normCwe(finding?.cwe);
|
|
173
518
|
const cls = CLASSES[cwe];
|
|
174
|
-
if (!cls) return
|
|
519
|
+
if (!cls) return generate_fail(`no payload and oracle exist for ${cwe ?? 'this finding'}; supported: ${Object.keys(CLASSES).join(', ')} (others need a database, a deserializer or a native harness)`);
|
|
175
520
|
const rel = String(finding?.file ?? '');
|
|
176
|
-
|
|
177
|
-
|
|
521
|
+
const isPy = /\.py$/i.test(rel);
|
|
522
|
+
const isHs = /\.hs$/i.test(rel);
|
|
523
|
+
if (!isPy && !isHs && !/\.(?:js|mjs|cjs)$/i.test(rel)) return generate_fail(`no harness builder for ${external_node_path_.extname(rel) || 'this file type'}; JavaScript, Python and Haskell only`);
|
|
524
|
+
if (!Number.isInteger(finding?.line)) return generate_fail('the finding has no line number');
|
|
525
|
+
if (isPy) return generatePythonReproducer({ finding, root, cls });
|
|
526
|
+
if (isHs) return generateHaskellReproducer({ finding, root, cls });
|
|
178
527
|
const src = readSource(root, rel);
|
|
179
|
-
if (!src) return
|
|
528
|
+
if (!src) return generate_fail('the flagged file is missing, outside the repository, or too large');
|
|
180
529
|
const ast = parseSource(src.text);
|
|
181
|
-
if (!ast) return
|
|
530
|
+
if (!ast) return generate_fail('the flagged file could not be parsed');
|
|
182
531
|
const lines = src.text.split('\n');
|
|
183
532
|
const sinkLine = lines[finding.line - 1] ?? '';
|
|
184
|
-
if (cls.needs && !cls.needs.test(sinkLine) && !cls.needs.test(lines.slice(Math.max(0, finding.line - 3), finding.line + 2).join('\n'))) return
|
|
533
|
+
if (cls.needs && !cls.needs.test(sinkLine) && !cls.needs.test(lines.slice(Math.max(0, finding.line - 3), finding.line + 2).join('\n'))) return generate_fail(`${cls.name}: the flagged code is not a write, and the oracle can only observe a write (a read leaves no trace unless the program prints it)`);
|
|
185
534
|
|
|
186
535
|
// dependencies the sandbox can satisfy
|
|
187
536
|
const sources = {}; const external = [];
|
|
@@ -193,23 +542,23 @@ function generateReproducer({ finding, root }) {
|
|
|
193
542
|
if (!spec.startsWith('.')) { external.push(spec); continue; }
|
|
194
543
|
let found = null;
|
|
195
544
|
for (const e of EXTS) { const cand = inside(root, external_node_path_.relative(rootReal, external_node_path_.resolve(external_node_path_.dirname(realFile), spec + e))); if (cand && external_node_fs_.statSync(cand).isFile()) { found = cand; break; } }
|
|
196
|
-
if (!found) return
|
|
545
|
+
if (!found) return generate_fail(`the relative import "${spec}" was not found inside the repository`);
|
|
197
546
|
const destRel = external_node_path_.relative(entryDir, found);
|
|
198
|
-
if (destRel.startsWith('..')) return
|
|
547
|
+
if (destRel.startsWith('..')) return generate_fail(`"${spec}" lives outside the flagged file's directory, which a proof workspace cannot mirror`);
|
|
199
548
|
if (sources[destRel] || found === src.real) continue;
|
|
200
|
-
if (Object.keys(sources).length >=
|
|
549
|
+
if (Object.keys(sources).length >= generate_MAX_FILES) return generate_fail(`the file pulls in more than ${generate_MAX_FILES} repository files`);
|
|
201
550
|
const dep = readSource(root, external_node_path_.relative(rootReal, found));
|
|
202
|
-
if (!dep) return
|
|
551
|
+
if (!dep) return generate_fail(`"${spec}" is too large or unreadable`);
|
|
203
552
|
sources[destRel] = external_node_path_.relative(rootReal, found);
|
|
204
553
|
const depAst = parseSource(dep.text);
|
|
205
|
-
if (!depAst) return
|
|
554
|
+
if (!depAst) return generate_fail(`"${spec}" could not be parsed`);
|
|
206
555
|
const r = collect(found, depAst, count + 1); if (r) return r;
|
|
207
556
|
}
|
|
208
557
|
return null;
|
|
209
558
|
};
|
|
210
559
|
const bad = collect(src.real, ast, 0);
|
|
211
560
|
if (bad) return bad;
|
|
212
|
-
if (external.length) return
|
|
561
|
+
if (external.length) return generate_fail(`the flagged code needs ${[...new Set(external)].slice(0, 4).join(', ')}, which is not installed in the proof environment; only Node built-ins and repository files can be loaded`, { missing: [...new Set(external)] });
|
|
213
562
|
|
|
214
563
|
const esm = MODULE_SYNTAX.test(src.text) || external_node_path_.extname(rel).toLowerCase() === '.mjs';
|
|
215
564
|
const entry = `target${esm ? '.mjs' : '.cjs'}`;
|
|
@@ -222,29 +571,29 @@ function generateReproducer({ finding, root }) {
|
|
|
222
571
|
if (/process\.argv\s*\[\s*2\s*\]|process\.argv\.slice\(\s*2\s*\)/.test(src.text) && !/\bmodule\.exports\b|^\s*export\s/m.test(src.text)) {
|
|
223
572
|
const reproducer = { ...base, sources, run: ['{{NODE}}', entry, cls.payload] };
|
|
224
573
|
const errs = (0,proof_fabric_reproducer/* validateReproducer */.mC)(reproducer);
|
|
225
|
-
return errs.length ?
|
|
574
|
+
return errs.length ? generate_fail(`the generated reproducer is invalid: ${errs[0]}`) : { ok: true, reproducer, basis: { shape: 'script', class: cls.name, entry: rel, line: finding.line, assumptions } };
|
|
226
575
|
}
|
|
227
576
|
|
|
228
577
|
// shape 2: an exported function around the flagged line
|
|
229
578
|
const candidates = functionsAround(ast, finding.line);
|
|
230
|
-
if (!candidates.length) return
|
|
579
|
+
if (!candidates.length) return generate_fail('the flagged line is not inside a function, and the file does not read its payload from process.argv[2]');
|
|
231
580
|
let around = null; let exp = null;
|
|
232
581
|
for (const c of candidates) { const e = exportName(ast, c); if (e) { around = c; exp = e; break; } }
|
|
233
|
-
if (!around) return
|
|
582
|
+
if (!around) return generate_fail('the function around the flagged line is not exported, so nothing outside the file can call it');
|
|
234
583
|
const names = paramNames(around.node);
|
|
235
|
-
if (!names.length) return
|
|
584
|
+
if (!names.length) return generate_fail('the function around the flagged line takes no parameters, so the payload has no way in');
|
|
236
585
|
const window = lines.slice(Math.max(0, around.node.loc.start.line - 1), Math.min(lines.length, finding.line + 1)).join('\n');
|
|
237
586
|
const requestLike = names.findIndex((n) => n && /^(?:req|request)$/.test(n) && new RegExp(`\\b${n}\\.(?:body|query|params)\\b`).test(sinkLine));
|
|
238
587
|
const direct = requestLike >= 0 ? -1 : names.findIndex((n) => n && new RegExp(`\\b${n}\\b`).test(sinkLine));
|
|
239
588
|
const fallback = direct < 0 && requestLike < 0 ? names.findIndex((n) => n && new RegExp(`\\b${n}\\b`).test(window)) : -1;
|
|
240
589
|
const idx = requestLike >= 0 ? requestLike : direct >= 0 ? direct : fallback;
|
|
241
|
-
if (idx < 0) return
|
|
590
|
+
if (idx < 0) return generate_fail('none of the function\'s parameters appears in the flagged code, so there is no tainted parameter to fill');
|
|
242
591
|
if (fallback >= 0) assumptions.push(`the tainted parameter "${names[idx]}" was chosen because it appears in the function body near the flagged line, not on the line itself`);
|
|
243
592
|
const args = names.map((_, i) => (i !== idx ? (/^(?:res|response)$/.test(names[i] ?? '') ? 'res' : 'undefined') : requestLike >= 0 ? '{ body: P, query: P, params: P, headers: P, cookies: P }' : 'payload'));
|
|
244
593
|
const files = { 'harness.mjs': harness({ esm, entry, exp, args }) };
|
|
245
594
|
const reproducer = { ...base, sources, files, run: ['{{NODE}}', 'harness.mjs', cls.payload] };
|
|
246
595
|
const errs = (0,proof_fabric_reproducer/* validateReproducer */.mC)(reproducer);
|
|
247
|
-
return errs.length ?
|
|
596
|
+
return errs.length ? generate_fail(`the generated reproducer is invalid: ${errs[0]}`) : { ok: true, reproducer, basis: { shape: 'function', class: cls.name, entry: rel, line: finding.line, function: names.length ? `${exp.kind === 'named' ? exp.name : exp.kind}(${names.join(', ')})` : exp.kind, taintedParameter: names[idx], requestLike: requestLike >= 0, assumptions } };
|
|
248
597
|
}
|
|
249
598
|
|
|
250
599
|
;// CONCATENATED MODULE: ./src/posture/proof-fabric/cli.js
|
|
@@ -382,7 +731,7 @@ async function cmdProof(args, root) {
|
|
|
382
731
|
function doctor(args) {
|
|
383
732
|
const tools = (0,runner/* hostTools */.qV)();
|
|
384
733
|
const rows = proof_fabric_reproducer/* LANGUAGES */.Yj.map((l) => ({ lang: l, ...(0,runner/* languageSupport */.ql)(l, tools) }));
|
|
385
|
-
const envs = ['javascript', 'python'].map((l) => ({ lang: l, ...(0,runner/* selectEnvironment */.aP)({ lang: l }) }));
|
|
734
|
+
const envs = ['javascript', 'python', 'haskell'].map((l) => ({ lang: l, ...(0,runner/* selectEnvironment */.aP)({ lang: l }) }));
|
|
386
735
|
if (args.flags.json) { console.log(JSON.stringify({ tools, languages: rows, environments: envs }, null, 2)); return 0; }
|
|
387
736
|
console.log('Proof fabric doctor');
|
|
388
737
|
for (const r of rows) console.log(` ${r.lang.padEnd(11)} ${r.supported ? 'supported' : `unsupported (${r.reason})`}`);
|
package/dist/2584.index.js
CHANGED
|
@@ -1430,7 +1430,7 @@ async function diagnosticsOf(scan) {
|
|
|
1430
1430
|
try { wanted = JSON.parse(process.env.AGENTIC_SECURITY_DIAG_FILES || '[]'); } catch { wanted = []; }
|
|
1431
1431
|
if (wanted.length) {
|
|
1432
1432
|
try {
|
|
1433
|
-
const irMod = await Promise.resolve(/* import() */).then(__webpack_require__.bind(__webpack_require__,
|
|
1433
|
+
const irMod = await Promise.resolve(/* import() */).then(__webpack_require__.bind(__webpack_require__, 98297));
|
|
1434
1434
|
irMod._resetIrParseFailures();
|
|
1435
1435
|
const ir = await irMod.buildProjectIRAsync(Object.fromEntries(wanted.filter((f) => scan.fc && typeof scan.fc[f] === 'string').map((f) => [f, scan.fc[f]])));
|
|
1436
1436
|
for (const f of wanted) {
|
package/dist/4171.index.js
CHANGED
|
@@ -22,8 +22,8 @@ var nix_build_trust = __webpack_require__(36668);
|
|
|
22
22
|
var discovery = __webpack_require__(30951);
|
|
23
23
|
// EXTERNAL MODULE: ./src/language/haskell-supply.js
|
|
24
24
|
var haskell_supply = __webpack_require__(86349);
|
|
25
|
-
// EXTERNAL MODULE: ./src/language/nix-script-taint.js
|
|
26
|
-
var nix_script_taint = __webpack_require__(
|
|
25
|
+
// EXTERNAL MODULE: ./src/language/nix-script-taint.js + 1 modules
|
|
26
|
+
var nix_script_taint = __webpack_require__(98078);
|
|
27
27
|
// EXTERNAL MODULE: ./src/language/nix-secrets.js
|
|
28
28
|
var nix_secrets = __webpack_require__(69466);
|
|
29
29
|
// EXTERNAL MODULE: ./src/language/nix-parser.js
|