@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.
Files changed (174) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/bin/agentic-security.js +66 -18
  3. package/dist/1128.index.js +25 -21
  4. package/dist/{5817.index.js → 1602.index.js} +378 -29
  5. package/dist/2584.index.js +1 -1
  6. package/dist/4171.index.js +2 -2
  7. package/dist/{0b301d45b231f144a1b4.json → 4d06c8944ae121d7085c.json} +8 -8
  8. package/dist/5752.index.js +466 -0
  9. package/dist/7405.index.js +1 -1
  10. package/dist/8158.index.js +1713 -0
  11. package/dist/8798.index.js +1 -1
  12. package/dist/9382.index.js +2 -2
  13. package/dist/9560.index.js +1 -1
  14. package/dist/9579.index.js +1 -1
  15. package/dist/agentic-security.mjs +29 -29
  16. package/dist/agentic-security.mjs.sha256 +1 -1
  17. package/package.json +8 -8
  18. package/src/dataflow/CLAUDE.md +6 -3
  19. package/src/dataflow/access-paths.js +12 -2
  20. package/src/dataflow/demand-query.js +279 -40
  21. package/src/dataflow/dep-fingerprint.js +1 -1
  22. package/src/dataflow/exploit-prover.js +6 -2
  23. package/src/dataflow/ir-cache.js +268 -0
  24. package/src/dataflow/privacy-taint.js +7 -0
  25. package/src/engine.js +62 -5
  26. package/src/ir/CLAUDE.md +4 -0
  27. package/src/ir/callgraph.js +36 -8
  28. package/src/ir/index.js +18 -4
  29. package/src/ir/parser-js.js +10 -0
  30. package/src/language/CLAUDE.md +13 -0
  31. package/src/language/nix-option-types.js +170 -0
  32. package/src/language/nix-script-taint.js +46 -3
  33. package/src/language/nix-secrets.js +20 -2
  34. package/src/lineage/CLAUDE.md +2 -1
  35. package/src/lineage/smt-orchestrator.js +47 -11
  36. package/src/lineage/smt-z3-backend.js +294 -0
  37. package/src/posture/CLAUDE.md +5 -1
  38. package/src/posture/business-logic.js +16 -7
  39. package/src/posture/dag-commands.js +442 -0
  40. package/src/posture/finding-defaults.js +19 -0
  41. package/src/posture/git-history.js +91 -1
  42. package/src/posture/proof-fabric/cli.js +1 -1
  43. package/src/posture/proof-fabric/container.js +2 -2
  44. package/src/posture/proof-fabric/generate-haskell.js +205 -0
  45. package/src/posture/proof-fabric/generate.js +6 -3
  46. package/src/posture/proof-fabric/reproducer.js +4 -2
  47. package/src/posture/proof-fabric/runner.js +4 -4
  48. package/src/sast/CLAUDE.md +2 -0
  49. package/src/sast/_call-args.js +70 -0
  50. package/src/sast/init-scope.js +87 -0
  51. package/src/sast/listen-bind-address.js +49 -0
  52. package/src/sast/redirect-target.js +106 -0
  53. package/src/sast/reflected-input.js +76 -0
  54. package/src/sast/send-file-root.js +27 -0
  55. package/src/sast/sensitive-data-logging.js +38 -1
  56. package/dist/119.index.js +0 -197
  57. package/dist/128.index.js +0 -129
  58. package/dist/1293.index.js +0 -5689
  59. package/dist/1336.index.js +0 -16331
  60. package/dist/1351.index.js +0 -133
  61. package/dist/1387.index.js +0 -271
  62. package/dist/1437.index.js +0 -23
  63. package/dist/1445.index.js +0 -110
  64. package/dist/1505.index.js +0 -151
  65. package/dist/1557.index.js +0 -293
  66. package/dist/1621.index.js +0 -311
  67. package/dist/1751.index.js +0 -160
  68. package/dist/1765.index.js +0 -727
  69. package/dist/1861.index.js +0 -690
  70. package/dist/1906.index.js +0 -433
  71. package/dist/208.index.js +0 -1631
  72. package/dist/2223.index.js +0 -159
  73. package/dist/2238.index.js +0 -295
  74. package/dist/2530.index.js +0 -152
  75. package/dist/2565.index.js +0 -116
  76. package/dist/2574.index.js +0 -127
  77. package/dist/2620.index.js +0 -345
  78. package/dist/2773.index.js +0 -98
  79. package/dist/2775.index.js +0 -32
  80. package/dist/2966.index.js +0 -171
  81. package/dist/2972.index.js +0 -87
  82. package/dist/2991.index.js +0 -131
  83. package/dist/3063.index.js +0 -833
  84. package/dist/3075.index.js +0 -227
  85. package/dist/3121.index.js +0 -297
  86. package/dist/3314.index.js +0 -263
  87. package/dist/3480.index.js +0 -1822
  88. package/dist/3481.index.js +0 -24
  89. package/dist/3506.index.js +0 -268
  90. package/dist/3533.index.js +0 -591
  91. package/dist/3653.index.js +0 -450
  92. package/dist/3740.index.js +0 -722
  93. package/dist/3822.index.js +0 -131
  94. package/dist/3868.index.js +0 -292
  95. package/dist/3918.index.js +0 -192
  96. package/dist/4090.index.js +0 -288
  97. package/dist/4113.index.js +0 -1286
  98. package/dist/4121.index.js +0 -1190
  99. package/dist/4142.index.js +0 -690
  100. package/dist/4168.index.js +0 -509
  101. package/dist/4281.index.js +0 -993
  102. package/dist/4336.index.js +0 -436
  103. package/dist/4387.index.js +0 -532
  104. package/dist/4413.index.js +0 -302
  105. package/dist/4476.index.js +0 -4510
  106. package/dist/4491.index.js +0 -133
  107. package/dist/4538.index.js +0 -185
  108. package/dist/4684.index.js +0 -319
  109. package/dist/4740.index.js +0 -165
  110. package/dist/4780.index.js +0 -209
  111. package/dist/4802.index.js +0 -225
  112. package/dist/4812.index.js +0 -379
  113. package/dist/4835.index.js +0 -126
  114. package/dist/494.index.js +0 -226
  115. package/dist/4991.index.js +0 -549
  116. package/dist/5199.index.js +0 -305
  117. package/dist/5216.index.js +0 -109
  118. package/dist/5439.index.js +0 -301
  119. package/dist/5443.index.js +0 -97
  120. package/dist/5496.index.js +0 -377
  121. package/dist/5738.index.js +0 -240
  122. package/dist/5801.index.js +0 -588
  123. package/dist/5858.index.js +0 -223
  124. package/dist/5933.index.js +0 -143
  125. package/dist/601.index.js +0 -23
  126. package/dist/6173.index.js +0 -273
  127. package/dist/6291.index.js +0 -1871
  128. package/dist/6312.index.js +0 -666
  129. package/dist/634.index.js +0 -16182
  130. package/dist/644.index.js +0 -87
  131. package/dist/6508.index.js +0 -283
  132. package/dist/6560.index.js +0 -645
  133. package/dist/6576.index.js +0 -422
  134. package/dist/6579.index.js +0 -271
  135. package/dist/6655.index.js +0 -3221
  136. package/dist/6745.index.js +0 -305
  137. package/dist/6818.index.js +0 -21642
  138. package/dist/6932.index.js +0 -1190
  139. package/dist/6972.index.js +0 -167
  140. package/dist/6995.index.js +0 -163
  141. package/dist/7249.index.js +0 -191
  142. package/dist/7372.index.js +0 -1197
  143. package/dist/7546.index.js +0 -541
  144. package/dist/76.index.js +0 -448
  145. package/dist/7879.index.js +0 -163
  146. package/dist/8025.index.js +0 -130
  147. package/dist/8054.index.js +0 -248
  148. package/dist/808.index.js +0 -1330
  149. package/dist/8097.index.js +0 -201
  150. package/dist/8126.index.js +0 -191
  151. package/dist/8178.index.js +0 -78
  152. package/dist/8382.index.js +0 -93
  153. package/dist/8408.index.js +0 -261
  154. package/dist/8540.index.js +0 -174
  155. package/dist/8635.index.js +0 -802
  156. package/dist/8688.index.js +0 -410
  157. package/dist/8699.index.js +0 -779
  158. package/dist/8891.index.js +0 -457
  159. package/dist/8916.index.js +0 -885
  160. package/dist/9146.index.js +0 -170
  161. package/dist/9168.index.js +0 -841
  162. package/dist/9292.index.js +0 -369
  163. package/dist/9422.index.js +0 -866
  164. package/dist/9440.index.js +0 -453
  165. package/dist/9479.index.js +0 -152
  166. package/dist/9481.index.js +0 -220
  167. package/dist/9539.index.js +0 -100
  168. package/dist/9547.index.js +0 -103
  169. package/dist/9692.index.js +0 -90
  170. package/dist/9800.index.js +0 -462
  171. package/dist/9899.index.js +0 -138
  172. package/dist/9981.index.js +0 -206
  173. package/dist/999.index.js +0 -117
  174. package/dist/9999.index.js +0 -1698
@@ -1,8 +1,8 @@
1
- export const id = 5817;
2
- export const ids = [5817,2199];
1
+ export const id = 1602;
2
+ export const ids = [1602,2199];
3
3
  export const modules = {
4
4
 
5
- /***/ 95817:
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 only, and only code the proof environment can actually load:
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 MAX_SOURCE = 256 * 1024;
74
- const MAX_FILES = 8;
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 fail = (reason, extra = {}) => ({ ok: false, reason, ...extra });
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 > MAX_SOURCE) return null;
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 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)`);
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
- if (!/\.(?:js|mjs|cjs)$/i.test(rel)) return fail(`no harness builder for ${external_node_path_.extname(rel) || 'this file type'}; JavaScript only`);
177
- if (!Number.isInteger(finding?.line)) return fail('the finding has no line number');
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 fail('the flagged file is missing, outside the repository, or too large');
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 fail('the flagged file could not be parsed');
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 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)`);
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 fail(`the relative import "${spec}" was not found inside the repository`);
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 fail(`"${spec}" lives outside the flagged file's directory, which a proof workspace cannot mirror`);
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 >= MAX_FILES) return fail(`the file pulls in more than ${MAX_FILES} repository files`);
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 fail(`"${spec}" is too large or unreadable`);
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 fail(`"${spec}" could not be parsed`);
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 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)] });
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 ? fail(`the generated reproducer is invalid: ${errs[0]}`) : { ok: true, reproducer, basis: { shape: 'script', class: cls.name, entry: rel, line: finding.line, assumptions } };
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 fail('the flagged line is not inside a function, and the file does not read its payload from process.argv[2]');
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 fail('the function around the flagged line is not exported, so nothing outside the file can call it');
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 fail('the function around the flagged line takes no parameters, so the payload has no way in');
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 fail('none of the function\'s parameters appears in the flagged code, so there is no tainted parameter to fill');
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 ? 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 } };
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})`}`);
@@ -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__, 28149));
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) {
@@ -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__(11793);
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