@clear-capabilities/agentic-security-scanner 0.188.0 → 0.190.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/bin/agentic-security.js +18 -1
- package/dist/{1602.index.js → 2066.index.js} +7 -463
- package/dist/{92f78fd3e2dbe30fb58d.json → 2d84fed7f6258f8552e1.json} +6 -3
- package/dist/8798.index.js +100 -0
- package/dist/9560.index.js +2 -2
- package/dist/9926.index.js +477 -0
- package/dist/agentic-security.mjs +2 -2
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +6 -3
- package/src/dataflow/engine.js +2 -1
- package/src/engine.js +24 -4
- package/src/ir/index.js +6 -5
- package/src/language/CLAUDE.md +10 -3
- package/src/language/context.js +32 -5
- package/src/language/haskell-fix.js +495 -28
- package/src/language/haskell-ir.js +311 -16
- package/src/language/haskell-manifests.js +88 -5
- package/src/language/haskell-parser.js +18 -4
- package/src/language/haskell-reachability.js +84 -0
- package/src/language/haskell-security-rules.js +23 -3
- package/src/language/haskell-syntax.js +2 -1
- package/src/language/haskell-trust.js +24 -6
- package/src/language/haskell-web.js +3 -3
- package/src/language/witness.js +96 -0
- package/src/mcp/tools.js +1 -1
- package/src/posture/proof-fabric/container.js +13 -2
- package/src/posture/relevance.js +21 -1
package/dist/8798.index.js
CHANGED
|
@@ -11,10 +11,13 @@ __webpack_require__.r(__webpack_exports__);
|
|
|
11
11
|
/* harmony export */ WITNESS_STATUS: () => (/* binding */ WITNESS_STATUS),
|
|
12
12
|
/* harmony export */ WITNESS_VERSION: () => (/* binding */ WITNESS_VERSION),
|
|
13
13
|
/* harmony export */ buildNixScriptWitness: () => (/* binding */ buildNixScriptWitness),
|
|
14
|
+
/* harmony export */ haskellFixWitness: () => (/* binding */ haskellFixWitness),
|
|
15
|
+
/* harmony export */ haskellFunctionAt: () => (/* binding */ haskellFunctionAt),
|
|
14
16
|
/* harmony export */ haskellRunner: () => (/* binding */ haskellRunner),
|
|
15
17
|
/* harmony export */ judgeFixWitness: () => (/* binding */ judgeFixWitness),
|
|
16
18
|
/* harmony export */ nixFixWitness: () => (/* binding */ nixFixWitness),
|
|
17
19
|
/* harmony export */ nixFlowTarget: () => (/* binding */ nixFlowTarget),
|
|
20
|
+
/* harmony export */ runHaskellEval: () => (/* binding */ runHaskellEval),
|
|
18
21
|
/* harmony export */ runHaskellWitness: () => (/* binding */ runHaskellWitness),
|
|
19
22
|
/* harmony export */ runNixScriptWitness: () => (/* binding */ runNixScriptWitness),
|
|
20
23
|
/* harmony export */ witnessNixFlow: () => (/* binding */ witnessNixFlow),
|
|
@@ -26,6 +29,7 @@ __webpack_require__.r(__webpack_exports__);
|
|
|
26
29
|
/* harmony import */ var node_child_process__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(31421);
|
|
27
30
|
/* harmony import */ var _sandbox_index_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(75778);
|
|
28
31
|
/* harmony import */ var _nix_script_taint_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(98078);
|
|
32
|
+
/* harmony import */ var _posture_proof_fabric_generate_haskell_js__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(29926);
|
|
29
33
|
// Controlled proof witnesses for Haskell and Nix findings (X-009).
|
|
30
34
|
//
|
|
31
35
|
// A witness is a small, bounded program that DEMONSTRATES the predicted effect inside the confinement sandbox
|
|
@@ -55,6 +59,7 @@ __webpack_require__.r(__webpack_exports__);
|
|
|
55
59
|
|
|
56
60
|
|
|
57
61
|
|
|
62
|
+
|
|
58
63
|
const WITNESS_VERSION = 'language-witness/1';
|
|
59
64
|
const PROOF_MARKER = 'PROVEN';
|
|
60
65
|
const WITNESS_STATUS = Object.freeze(['reproduced', 'not-reproduced', 'invalid', 'unavailable', 'unsupported']);
|
|
@@ -214,6 +219,101 @@ function nixFixWitness(finding, opts = {}) {
|
|
|
214
219
|
};
|
|
215
220
|
}
|
|
216
221
|
|
|
222
|
+
// ── Haskell: the fix witness ──────────────────────────────────────────────────
|
|
223
|
+
// `haskellFixWitness(finding)` is the `witness` hook of `runFixLifecycle` for a Haskell fix, the counterpart of `nixFixWitness`. It runs the ORIGINAL
|
|
224
|
+
// and the PATCHED file under GHC's interpreter inside the confinement sandbox (`ghc -e ':load *File' -e 'fn "<payload>"'`, every top-level name in
|
|
225
|
+
// scope, so the function need not be exported) and judges them with `judgeFixWitness`: verified only when the original reproduced the effect and the
|
|
226
|
+
// patched code ran without reproducing it.
|
|
227
|
+
//
|
|
228
|
+
// Where it can honestly be built: a command-injection fix (`behavior.kind === 'process'`) in a top-level function whose parameters are all
|
|
229
|
+
// String/FilePath and whose result is IO, in a file whose imports are all modules of the packages that ship with GHC (`bootModuleSet`, the same
|
|
230
|
+
// decision the proof fabric makes before it runs anything). The payload makes the shell create `PROVEN`; the marker is the only thing that proves.
|
|
231
|
+
// Every other class is REFUSED with the reason (never faked): the effect is not observable by a program GHC can run alone (a TLS peer, a response
|
|
232
|
+
// header of a web framework, a database), not an exploit at all (a digest, a log line), or a property of a distribution (randomness).
|
|
233
|
+
const WITNESS_REFUSALS = Object.freeze({
|
|
234
|
+
tls: 'certificate validation is observable only against a TLS server presenting an untrusted certificate, and the confinement environment denies the network',
|
|
235
|
+
cookie: 'a cookie attribute is a property of a response header produced by a web framework (wai, scotty, servant) that GHC does not ship, so no program can run it here',
|
|
236
|
+
random: 'unpredictability is a property of a distribution; no single run reproduces or refutes it',
|
|
237
|
+
'body-limit': 'the effect needs a wai Request with a streamed body, and wai is not a package GHC ships',
|
|
238
|
+
sql: 'the effect is observable only in a database driver, which GHC does not ship',
|
|
239
|
+
html: 'the effect is observable only in the rendering library, which GHC does not ship',
|
|
240
|
+
hash: 'a weaker digest is not an exploitable effect a program can demonstrate',
|
|
241
|
+
log: 'the effect is a value appearing in a log, which depends on the logging setup the file does not state',
|
|
242
|
+
path: 'the base directory is the program\'s own and is not present in the sandbox, so a traversal out of it cannot be planted and observed',
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
/** The top-level function around `line`: {name, params:[{name,type}], result} from its signature and first clause, or null. */
|
|
246
|
+
function haskellFunctionAt(text, line) {
|
|
247
|
+
const lines = String(text).split('\n');
|
|
248
|
+
let clause = -1;
|
|
249
|
+
for (let i = Math.min(line - 1, lines.length - 1); i >= 0; i--) if (/^[a-z_][\w']*\b[^:\n]*=/.test(lines[i]) && !/^[a-z_][\w']*\s*::/.test(lines[i])) { clause = i; break; }
|
|
250
|
+
if (clause < 0) return null;
|
|
251
|
+
const m = /^([a-z_][\w']*)([^=]*)=/.exec(lines[clause]);
|
|
252
|
+
if (!m) return null;
|
|
253
|
+
const params = m[2].trim() ? m[2].trim().split(/\s+/) : [];
|
|
254
|
+
if (params.some((p) => !/^[a-z_][\w']*$/.test(p))) return null;
|
|
255
|
+
const sigLine = lines.findIndex((l) => new RegExp(`^${m[1]}\\s*::`).test(l));
|
|
256
|
+
if (sigLine < 0) return null;
|
|
257
|
+
let sig = lines[sigLine].replace(/^[^:]*::/, '');
|
|
258
|
+
for (let j = sigLine + 1; j < lines.length && /^\s+\S/.test(lines[j]) && !/^[a-z_][\w']*\s*(::|=)/.test(lines[j]); j++) sig += ` ${lines[j].trim()}`;
|
|
259
|
+
if (/=>/.test(sig)) return null; // a constrained signature: the argument types are not concrete
|
|
260
|
+
const types = []; let depth = 0; let cur = '';
|
|
261
|
+
for (let i = 0; i < sig.length; i++) {
|
|
262
|
+
const c = sig[i];
|
|
263
|
+
if (c === '(' || c === '[') depth++; else if (c === ')' || c === ']') depth--;
|
|
264
|
+
if (depth === 0 && sig.startsWith('->', i)) { types.push(cur.trim()); cur = ''; i++; continue; }
|
|
265
|
+
cur += c;
|
|
266
|
+
}
|
|
267
|
+
types.push(cur.trim());
|
|
268
|
+
if (types.length !== params.length + 1) return null;
|
|
269
|
+
return { name: m[1], params: params.map((p, i) => ({ name: p, type: types[i] })), result: types[types.length - 1] };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** Runs `expr` in the interpreter with `file` loaded (all its top-level names in scope), confined. Never throws. */
|
|
273
|
+
function runHaskellEval(tree, file, expr, { timeoutMs = 60_000, force } = {}) {
|
|
274
|
+
const base = { witness: WITNESS_VERSION, lang: 'haskell', ran: false, backend: null, marker: PROOF_MARKER };
|
|
275
|
+
const ghc = which('ghc');
|
|
276
|
+
if (!ghc) return { ...base, status: 'unavailable', reason: 'no GHC on PATH; the Haskell witness cannot run here' };
|
|
277
|
+
if (!(0,_sandbox_index_js__WEBPACK_IMPORTED_MODULE_4__/* .sandboxAvailable */ .$G)()) return { ...base, status: 'invalid', reason: 'no confinement primitive available; refusing to execute' };
|
|
278
|
+
let root;
|
|
279
|
+
try {
|
|
280
|
+
root = node_fs__WEBPACK_IMPORTED_MODULE_0__.realpathSync(node_fs__WEBPACK_IMPORTED_MODULE_0__.mkdtempSync(node_path__WEBPACK_IMPORTED_MODULE_2__.join(node_os__WEBPACK_IMPORTED_MODULE_1__.tmpdir(), 'witness-hs-')));
|
|
281
|
+
for (const [f, t] of Object.entries(tree)) if (/\.l?hs$/i.test(f) && typeof t === 'string') { node_fs__WEBPACK_IMPORTED_MODULE_0__.mkdirSync(node_path__WEBPACK_IMPORTED_MODULE_2__.dirname(node_path__WEBPACK_IMPORTED_MODULE_2__.join(root, f)), { recursive: true }); node_fs__WEBPACK_IMPORTED_MODULE_0__.writeFileSync(node_path__WEBPACK_IMPORTED_MODULE_2__.join(root, f), t); }
|
|
282
|
+
const r = (0,_sandbox_index_js__WEBPACK_IMPORTED_MODULE_4__/* .runConfined */ .id)([ghc, '-isrc', '-i.', '-e', `:load *${file}`, '-e', expr], { root, timeoutMs, force });
|
|
283
|
+
if (r.status === 'disabled' || r.status === 'error') return { ...base, backend: r.backend, status: 'invalid', reason: `the sandbox could not execute it: ${String(r.stderr || '').trim() || r.status}` };
|
|
284
|
+
if (r.timedOut) return { ...base, backend: r.backend, status: 'invalid', reason: 'the witness exceeded its time budget', timedOut: true };
|
|
285
|
+
const proven = node_fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(node_path__WEBPACK_IMPORTED_MODULE_2__.join(root, PROOF_MARKER));
|
|
286
|
+
if (!proven && /\[GHC-\d+\]|Could not find module|parse error|error:\s/.test(String(r.stderr || ''))) return { ...base, backend: r.backend, status: 'invalid', reason: `the module did not load or run: ${String(r.stderr).trim().split('\n').find((l) => l.trim()) || 'GHC rejected it'}` };
|
|
287
|
+
return { ...base, ran: true, backend: r.backend, status: proven ? 'reproduced' : 'not-reproduced', exitCode: r.exitCode, reason: proven ? null : 'the function ran and the marker was not created' };
|
|
288
|
+
} catch (e) {
|
|
289
|
+
return { ...base, status: 'invalid', reason: `witness harness error: ${String((e && e.message) || e)}` };
|
|
290
|
+
} finally { if (root) node_fs__WEBPACK_IMPORTED_MODULE_0__.rmSync(root, { recursive: true, force: true }); }
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const importsOf = (text) => [...String(text).matchAll(/^import\s+(?:safe\s+)?(?:qualified\s+)?([A-Z][\w.']*)/gm)].map((m) => m[1]);
|
|
294
|
+
|
|
295
|
+
/** A `witness` hook for runFixLifecycle over a Haskell fix. See the block comment above for what is and is not supported. */
|
|
296
|
+
function haskellFixWitness(finding, opts = {}) {
|
|
297
|
+
return (plan, files, patched) => {
|
|
298
|
+
const kind = plan && plan.behavior && plan.behavior.kind;
|
|
299
|
+
const no = (reason) => ({ ok: false, verified: false, reason });
|
|
300
|
+
if (kind !== 'process') return no(`no controlled witness can be built for ${kind || 'this'} fixes: ${WITNESS_REFUSALS[kind] || 'the class has no observable effect a program GHC runs alone could produce'}`);
|
|
301
|
+
const fn = haskellFunctionAt(plan.before, plan.fixedAtLine || finding.line);
|
|
302
|
+
if (!fn || !fn.params.length || !/^IO\b/.test(fn.result) || fn.params.some((p) => !/^(?:String|FilePath)$/.test(p.type))) return no('the flagged call is not in a top-level function of String/FilePath parameters returning IO, so there is no argument to put a payload in');
|
|
303
|
+
const ghc = opts.ghc || which('ghc');
|
|
304
|
+
if (!ghc) return { ok: false, verified: false, status: 'unavailable', reason: 'no GHC on PATH; the Haskell witness is unavailable here' };
|
|
305
|
+
const boot = (0,_posture_proof_fabric_generate_haskell_js__WEBPACK_IMPORTED_MODULE_6__/* .bootModuleSet */ .hp)(ghc);
|
|
306
|
+
if (!boot) return no('the modules the installed GHC provides could not be listed, so whether the file can be loaded is unknown');
|
|
307
|
+
const external = [...new Set([...importsOf(plan.before), ...importsOf(plan.after)])].filter((m) => !boot.has(m));
|
|
308
|
+
if (external.length) return no(`the file imports ${external.slice(0, 4).join(', ')}, which GHC's boot packages do not provide, so it cannot be loaded for a witness`);
|
|
309
|
+
if (/\{-#\s*LANGUAGE[^#]*\b(?:TemplateHaskell|QuasiQuotes|CPP)\b/.test(plan.before)) return no('the file uses Template Haskell, quasi-quotes or CPP, which run code at load time');
|
|
310
|
+
const payload = `x; touch ${PROOF_MARKER}`;
|
|
311
|
+
const expr = `${fn.name} ${fn.params.map(() => JSON.stringify(payload)).join(' ')}`;
|
|
312
|
+
const only = (t) => Object.fromEntries(Object.entries(t).filter(([p, x]) => p === plan.file && typeof x === 'string'));
|
|
313
|
+
return judgeFixWitness(runHaskellEval(only(files), plan.file, expr, opts), runHaskellEval(only(patched), plan.file, expr, opts));
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
217
317
|
|
|
218
318
|
/***/ })
|
|
219
319
|
|
package/dist/9560.index.js
CHANGED
|
@@ -2626,7 +2626,7 @@ __webpack_require__.a(__webpack_module__, async (__webpack_handle_async_dependen
|
|
|
2626
2626
|
/* harmony export */ });
|
|
2627
2627
|
/* harmony import */ var _posture_proof_fabric_orchestrator_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(3740);
|
|
2628
2628
|
/* harmony import */ var _posture_proof_fabric_fix_proof_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(818);
|
|
2629
|
-
/* harmony import */ var _posture_proof_fabric_cli_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(
|
|
2629
|
+
/* harmony import */ var _posture_proof_fabric_cli_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(92066);
|
|
2630
2630
|
/* harmony import */ var _posture_proof_fabric_store_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(62199);
|
|
2631
2631
|
/* harmony import */ var _posture_proof_fabric_reproducer_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(37055);
|
|
2632
2632
|
/* harmony import */ var _posture_proof_fabric_siblings_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(25099);
|
|
@@ -4328,7 +4328,7 @@ const synthesize_fix = {
|
|
|
4328
4328
|
if (lc.languageOfFinding(f)) {
|
|
4329
4329
|
const proj = lc.loadLanguageProject(ctx.sessionRoot);
|
|
4330
4330
|
const prev = await lc.languageFixPreview(f, proj.files);
|
|
4331
|
-
languageFix = { status: prev.status, ok: prev.ok, label: prev.label || null, tier: prev.tier || null, reason: prev.reason || null, diff: prev.diff || null, explanation: prev.explanation || null, consequences: prev.consequences || [] };
|
|
4331
|
+
languageFix = { status: prev.status, ok: prev.ok, label: prev.label || null, tier: prev.tier || null, reason: prev.reason || null, decision: prev.decision || null, diff: prev.diff || null, explanation: prev.explanation || null, consequences: prev.consequences || [] };
|
|
4332
4332
|
// A fix that spans several files is applied with apply_fix + plan_digest (atomic, one history group); see _applyMultiFilePlan.
|
|
4333
4333
|
if (prev.ok && prev.edits) {
|
|
4334
4334
|
languageFix.multiFile = prev.edits.map((e) => e.file);
|