@clear-capabilities/agentic-security-scanner 0.128.1 → 0.130.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 +101 -0
- package/bin/agentic-security.js +33 -0
- package/dist/11.index.js +2 -2
- package/dist/113.index.js +209 -7
- package/dist/178.index.js +1 -1
- package/dist/207.index.js +217 -0
- package/dist/384.index.js +1 -1
- package/dist/415.index.js +1 -1
- package/dist/435.index.js +2 -2
- package/dist/526.index.js +555 -0
- package/dist/637.index.js +1 -1
- package/dist/830.index.js +1 -1
- package/dist/agentic-security.mjs +113 -162
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +22 -14
- package/src/dataflow/CLAUDE.md +4 -1
- package/src/dataflow/async-sequencing.js +8 -3
- package/src/dataflow/catalog.js +278 -11
- package/src/dataflow/cross-repo.js +1 -1
- package/src/dataflow/cross-service-taint.js +1 -1
- package/src/dataflow/engine.js +182 -61
- package/src/dataflow/ifds.js +10 -5
- package/src/dataflow/index.js +15 -3
- package/src/dataflow/points-to.js +8 -2
- package/src/dataflow/proof-gate.js +7 -0
- package/src/dataflow/sanitizer-gate.js +89 -0
- package/src/dataflow/tabulation.js +14 -3
- package/src/engine.js +154 -7
- package/src/integrations/index.js +1 -1
- package/src/ir/CLAUDE.md +49 -4
- package/src/ir/call-sites.js +66 -0
- package/src/ir/callgraph.js +174 -7
- package/src/ir/class-hierarchy.js +22 -2
- package/src/ir/index.js +138 -51
- package/src/ir/ir-stats.js +126 -0
- package/src/ir/parser-cpp.js +829 -0
- package/src/ir/parser-cs.js +4 -1
- package/src/ir/parser-go.js +4 -1
- package/src/ir/parser-js.js +5 -1
- package/src/ir/parser-kt.js +4 -1
- package/src/ir/parser-php.js +10 -3
- package/src/ir/parser-py-cst.js +62 -10
- package/src/ir/tree-sitter-loader.js +13 -1
- package/src/llm-validator/index.js +9 -2
- package/src/llm-validator/redact.js +157 -0
- package/src/posture/CLAUDE.md +115 -0
- package/src/posture/accuracy-scorecard.js +317 -0
- package/src/posture/api-contract.js +1 -1
- package/src/posture/attestation.js +199 -0
- package/src/posture/auditor-walkthrough.js +12 -3
- package/src/posture/compliance-policy.js +1 -1
- package/src/posture/cross-lang-openapi.js +1 -1
- package/src/posture/custom-rules.js +1 -1
- package/src/posture/execution-proof.js +52 -0
- package/src/posture/exploitability-probability.js +1 -1
- package/src/posture/falsification.js +45 -1
- package/src/posture/fix-verify.js +55 -2
- package/src/posture/license-policy.js +1 -1
- package/src/posture/profile.js +1 -1
- package/src/posture/proof-tier.js +33 -0
- package/src/posture/relevance.js +379 -0
- package/src/posture/rule-overrides.js +1 -1
- package/src/posture/sca-policy.js +1 -1
- package/src/posture/scan-checkpoint.js +277 -0
- package/src/posture/suppressions.js +1 -1
- package/src/posture/test-runner.js +147 -0
- package/src/posture/verification-separation.js +131 -0
- package/src/report/index.js +11 -0
- package/src/runScan.js +3 -1
- package/src/sandbox/CLAUDE.md +218 -0
- package/src/sandbox/backend-disabled.js +14 -0
- package/src/sandbox/backend-namespace.js +83 -0
- package/src/sandbox/backend-userspace.js +100 -0
- package/src/sandbox/capabilities.js +53 -0
- package/src/sandbox/index.js +30 -0
- package/src/sandbox/limits.js +42 -0
- package/src/sandbox/result.js +104 -0
- package/src/sca/dep-confusion.js +1 -1
- package/src/util/yaml.js +24 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,106 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.130.0 — the roadmap's first ten: provable security over orchestration parity
|
|
4
|
+
|
|
5
|
+
A capability roadmap (`docs/ROADMAP.md`) plus its first ten items, derived from a
|
|
6
|
+
survey of the current agentic security-review field. The strategic call: that
|
|
7
|
+
field cannot state a false-positive rate or prove it did not regress, because its
|
|
8
|
+
core is a model call. This project's core is a deterministic engine behind three
|
|
9
|
+
gates, so the work doubles down on provable, measurable, reproducible.
|
|
10
|
+
|
|
11
|
+
- **Execution sandbox** (`src/sandbox/`) — fail-closed confined execution. Verified
|
|
12
|
+
by execution: writes outside the sandbox root blocked, network blocked, wall-clock
|
|
13
|
+
overrun terminated, benign work still succeeds. With no confinement primitive
|
|
14
|
+
available, execution is REFUSED, never run unconfined.
|
|
15
|
+
- **Execution-verified findings** (`posture/execution-proof.js`) — a finding can be
|
|
16
|
+
promoted to `execution-proven` by running its proof-of-concept in the sandbox.
|
|
17
|
+
Proof is a marker file, never an exit code, because the sandbox cannot reliably
|
|
18
|
+
distinguish a denied run from a clean exit. `proof-failed` is a triage signal,
|
|
19
|
+
NOT a false-positive verdict.
|
|
20
|
+
- **Published accuracy scorecard** (`npm run scorecard`) — detection and correct-
|
|
21
|
+
silence rates sliced by language and CWE, every figure with its denominator,
|
|
22
|
+
regeneration-stable. F1 deliberately omitted: no labelled real-world population
|
|
23
|
+
exists to measure precision over, and the document says so.
|
|
24
|
+
- **Determinism attestation** (`posture/attestation.js`) — an order-independent
|
|
25
|
+
signed digest. Cross-machine reproducibility is explicitly NOT claimed.
|
|
26
|
+
- **Fixes must pass your tests** (`posture/test-runner.js`) — `verifyFix` previously
|
|
27
|
+
proved only that the finding disappeared, which deleting the feature also
|
|
28
|
+
achieves. It now runs the project's own suite, and says so when the suite ran
|
|
29
|
+
against unpatched code.
|
|
30
|
+
- **Relevance ranking** (`posture/relevance.js`) — re-ranks by entry-point
|
|
31
|
+
reachability. Recall-preserving: nothing deleted, severity never touched,
|
|
32
|
+
`unreachable` only on positive evidence.
|
|
33
|
+
- **Enforced verification separation** (`posture/verification-separation.js`) — a
|
|
34
|
+
verifier cannot rubber-stamp its own finding.
|
|
35
|
+
- **Resumable scans** (`posture/scan-checkpoint.js`) — opt-in via
|
|
36
|
+
`AGENTIC_SECURITY_RESUME=1`, crash-safe, conservatively invalidated.
|
|
37
|
+
- **Secret redaction** (`llm-validator/redact.js`) — credentials removed before any
|
|
38
|
+
code leaves the machine; ordinary code passes through byte-unchanged.
|
|
39
|
+
|
|
40
|
+
Also: dependencies updated to latest (Babel 7→8 with the removed preset options
|
|
41
|
+
migrated, js-yaml 4→5 with an empty-input shim). One dependency deliberately held
|
|
42
|
+
back: bumping the grammar runtime silently drops all six long-tail language
|
|
43
|
+
grammars, so it stays pinned and the reason is documented.
|
|
44
|
+
|
|
45
|
+
`npm test` 1989/0; cve-replay 199/199; self-scan no drift.
|
|
46
|
+
|
|
47
|
+
## 0.129.0 — closing two taint-engine recall gaps
|
|
48
|
+
|
|
49
|
+
Two defects were found by execution on the merged tree, each silencing real findings across
|
|
50
|
+
every supported language. Both are now fixed, measured, and gated.
|
|
51
|
+
|
|
52
|
+
- **Sinks are matched on assignment right-hand sides** (`dataflow/engine.js`). The engine only
|
|
53
|
+
ever sink-matched in statement position, so `db.query(tainted)` was reported while
|
|
54
|
+
`const rows = db.query(tainted)` was silent — in every language. The sink-matching logic is
|
|
55
|
+
now extracted into shared helpers called from both `case 'call'` and `case 'assign'` rather
|
|
56
|
+
than duplicated, and the pre-existing statement-position path is unchanged (measured control:
|
|
57
|
+
`1/1` before and after; assignment position `0/0 -> 1/1`).
|
|
58
|
+
- **`match.type:'global'` catalog entries are indexed and reachable** (`dataflow/catalog.js`).
|
|
59
|
+
All 10 global entries were unreachable from `matchSource()` — including `$_GET`/`$_POST`/
|
|
60
|
+
`$_REQUEST`, the canonical PHP taint sources, in a language that already had interprocedural
|
|
61
|
+
analysis. A new `GLOBAL_INDEX`, plus lookup-side sigil normalization (`_globalKey()`) so PHP's
|
|
62
|
+
`$` prefix matches sigil-free catalog keys, takes catalog reachability `0/10 -> 10/10` with
|
|
63
|
+
language scoping preserved.
|
|
64
|
+
- **Seven self-scan false positives eliminated at source.** Raising sink recall exposed
|
|
65
|
+
pre-existing catalog imprecision: `py-yaml-load`/`py-pickle-load` matched bare callee `load`
|
|
66
|
+
with no receiver constraint, so ordinary `json.load(fh)` was flagged as unsafe deserialization.
|
|
67
|
+
Each finding was inspected individually and all seven were false positives; the entries are now
|
|
68
|
+
pinned to their receiver. **Nothing was baselined** — `bench/self-scan/BASELINE.json` is
|
|
69
|
+
unchanged and the gate is green on the source fix.
|
|
70
|
+
- **New**: `bench/engine-recall` before/after harness (`npm run bench:engine-recall`) and
|
|
71
|
+
`bench/engine-recall/RESULTS.md`, the full measurement record including what the fixes cost.
|
|
72
|
+
- **Corpus 197 -> 199**: two deep-tier entries, each verified missed-before / found-after against
|
|
73
|
+
its specific fix.
|
|
74
|
+
|
|
75
|
+
Known trade, recorded rather than papered over: pinning the receiver drops `import yaml as y;
|
|
76
|
+
y.load(f)` and `from yaml import load; load(x)`, which are now covered at no layer. A corpus guard
|
|
77
|
+
was attempted and deliberately withheld because it would score `pre:TN`; closing it needs
|
|
78
|
+
import-alias resolution in the Python IR. Separately, `10/10` is catalog reachability, not
|
|
79
|
+
end-to-end recall — only PHP is proven end to end; Ruby's deep engine does not complete those
|
|
80
|
+
flows (pre-existing). Both are documented in `RESULTS.md` §3 and §8.
|
|
81
|
+
|
|
82
|
+
`npm test` 1854/0; cve-replay 199/199; self-scan no drift.
|
|
83
|
+
|
|
84
|
+
## 0.128.2 — compliance attestation accuracy + quieter self-scans
|
|
85
|
+
|
|
86
|
+
Two fixes surfaced while dogfooding the compliance flow on this repo:
|
|
87
|
+
|
|
88
|
+
- **Fixed a compliance-attestation path bug** (`posture/auditor-walkthrough.js`). Three
|
|
89
|
+
evidence checks (`mcp-tools`, `security-fixer`, `pre-edit-bodyguard`) carried a literal
|
|
90
|
+
`.../` placeholder path that `path.join(scanRoot, STATE, '.../x')` could never resolve, so
|
|
91
|
+
they read **"not present" for every project** — falsely dragging OWASP LLM08/LLM09 (and any
|
|
92
|
+
framework mapping to those modules) to "manual/not-present". A `.../` sentinel now resolves
|
|
93
|
+
against the scan root itself. On a self-attestation this flips LLM09 → satisfied and makes
|
|
94
|
+
LLM08 honestly partial.
|
|
95
|
+
- **Repo `ignorePaths` for meaningful self-scans** (`.agentic-security/rules.yml`). Added
|
|
96
|
+
`bench/**` and `scanner/test/fixtures/**` so a repo-root `/scan` no longer counts the ~600
|
|
97
|
+
intentionally-vulnerable benchmark corpora and test fixtures as findings. Safe: `rules.yml`
|
|
98
|
+
is loaded from the exact scan root only, so the cve-replay runner (which scans each
|
|
99
|
+
pre/post fixture as its own root) and the unit tests are unaffected — corpus gate stays
|
|
100
|
+
185/185.
|
|
101
|
+
|
|
102
|
+
`npm test` 1695/0; cve-replay 185/185.
|
|
103
|
+
|
|
3
104
|
## 0.128.1 — patch dependency vulnerabilities (11 Dependabot alerts → 0)
|
|
4
105
|
|
|
5
106
|
Security maintenance. Cleared all 11 open Dependabot alerts by updating the two lockfiles to
|
package/bin/agentic-security.js
CHANGED
|
@@ -274,6 +274,21 @@ function renderV3Blocks(scan, flags) {
|
|
|
274
274
|
}
|
|
275
275
|
|
|
276
276
|
// Always-on machine output (R2). Vibecoder gets JSON only; pro gets JSON+SARIF+CSV.
|
|
277
|
+
// SHA-256 of the running bundle, read from the sidecar `npm run build` emits
|
|
278
|
+
// NEXT TO the bundle. Running from source (bin/ + src/) yields 'unavailable'
|
|
279
|
+
// rather than the checkout's dist hash: src and a previously-built dist can
|
|
280
|
+
// disagree, and attesting a bundle that did not produce this run would be a
|
|
281
|
+
// false claim.
|
|
282
|
+
function _bundleSha() {
|
|
283
|
+
const here = path.dirname(new URL(import.meta.url).pathname);
|
|
284
|
+
try {
|
|
285
|
+
const raw = fs.readFileSync(path.join(here, 'agentic-security.mjs.sha256'), 'utf8').trim();
|
|
286
|
+
const m = /^([0-9a-f]{64})\b/.exec(raw);
|
|
287
|
+
if (m) return m[1];
|
|
288
|
+
} catch { /* not running from the bundle */ }
|
|
289
|
+
return 'unavailable';
|
|
290
|
+
}
|
|
291
|
+
|
|
277
292
|
async function writeMachineOutput(targetAbs, scan, meta, profile) {
|
|
278
293
|
const stateDir = path.join(targetAbs, '.agentic-security');
|
|
279
294
|
const { isSafeStateDir: _isSafe } = await import('../src/posture/state-dir.js');
|
|
@@ -553,6 +568,24 @@ async function cmdScan(args) {
|
|
|
553
568
|
// Deterministic post-process: stable-sort findings + zero out timing.
|
|
554
569
|
if (isDeterministic()) makeDeterministic(scan, meta);
|
|
555
570
|
|
|
571
|
+
// R4 — determinism as a contract. Bind the PUBLISHED finding set (the same
|
|
572
|
+
// normalization every report format emits) to the engine version, ruleset
|
|
573
|
+
// version and bundle hash that produced it, via an order-independent digest.
|
|
574
|
+
// Runs after every filter above so it attests what actually ships. Metadata
|
|
575
|
+
// only — a failure here must never fail a scan.
|
|
576
|
+
try {
|
|
577
|
+
const { computeRunAttestation } = await import('../src/posture/attestation.js');
|
|
578
|
+
const { effectiveVersion } = await import('../src/posture/ruleset-version.js');
|
|
579
|
+
scan.attestation = computeRunAttestation({
|
|
580
|
+
findings: normalizeFindings(scan),
|
|
581
|
+
engineVersion: PKG_VERSION,
|
|
582
|
+
rulesetVersion: effectiveVersion(targetAbs).version,
|
|
583
|
+
bundleSha: _bundleSha(),
|
|
584
|
+
root: targetAbs,
|
|
585
|
+
sign: true,
|
|
586
|
+
});
|
|
587
|
+
} catch { /* attestation is metadata; never fail a scan over it */ }
|
|
588
|
+
|
|
556
589
|
// R2: Always emit machine-readable artifacts to .agentic-security/.
|
|
557
590
|
await writeMachineOutput(targetAbs, scan, meta, profile);
|
|
558
591
|
|
package/dist/11.index.js
CHANGED
|
@@ -21,8 +21,8 @@ var external_node_child_process_ = __webpack_require__(1421);
|
|
|
21
21
|
var external_node_fs_ = __webpack_require__(3024);
|
|
22
22
|
// EXTERNAL MODULE: external "node:path"
|
|
23
23
|
var external_node_path_ = __webpack_require__(6760);
|
|
24
|
-
// EXTERNAL MODULE: ./src/engine.js +
|
|
25
|
-
var engine = __webpack_require__(
|
|
24
|
+
// EXTERNAL MODULE: ./src/engine.js + 592 modules
|
|
25
|
+
var engine = __webpack_require__(9408);
|
|
26
26
|
;// CONCATENATED MODULE: ./src/posture/fix-honesty-gate.js
|
|
27
27
|
// Deterministic honesty gates on fix / finding output (#7).
|
|
28
28
|
//
|
package/dist/113.index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const id = 113;
|
|
2
|
-
export const ids = [113,
|
|
2
|
+
export const ids = [113,526];
|
|
3
3
|
export const modules = {
|
|
4
4
|
|
|
5
5
|
/***/ 4113:
|
|
@@ -12,7 +12,7 @@ export const modules = {
|
|
|
12
12
|
/* harmony import */ var node_child_process__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1421);
|
|
13
13
|
/* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(3024);
|
|
14
14
|
/* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(6760);
|
|
15
|
-
/* harmony import */ var _fix_verify_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(
|
|
15
|
+
/* harmony import */ var _fix_verify_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(3526);
|
|
16
16
|
// Closed-loop fix verification (v0.68).
|
|
17
17
|
//
|
|
18
18
|
// Existing `fix-verify.js` does scan + lint. This module adds the third
|
|
@@ -174,7 +174,7 @@ function _summarize(legs, verdict) {
|
|
|
174
174
|
|
|
175
175
|
/***/ }),
|
|
176
176
|
|
|
177
|
-
/***/
|
|
177
|
+
/***/ 3526:
|
|
178
178
|
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
|
179
179
|
|
|
180
180
|
// ESM COMPAT FLAG
|
|
@@ -193,8 +193,8 @@ var external_node_child_process_ = __webpack_require__(1421);
|
|
|
193
193
|
var external_node_fs_ = __webpack_require__(3024);
|
|
194
194
|
// EXTERNAL MODULE: external "node:path"
|
|
195
195
|
var external_node_path_ = __webpack_require__(6760);
|
|
196
|
-
// EXTERNAL MODULE: ./src/engine.js +
|
|
197
|
-
var engine = __webpack_require__(
|
|
196
|
+
// EXTERNAL MODULE: ./src/engine.js + 595 modules
|
|
197
|
+
var engine = __webpack_require__(4660);
|
|
198
198
|
;// CONCATENATED MODULE: ./src/posture/fix-honesty-gate.js
|
|
199
199
|
// Deterministic honesty gates on fix / finding output (#7).
|
|
200
200
|
//
|
|
@@ -372,6 +372,155 @@ function gateFixOutput({ residual, verdict, evidence, signals } = {}) {
|
|
|
372
372
|
|
|
373
373
|
const _internals = Object.freeze({ BANNED_RESIDUAL_PHRASES, CITATION_RE, FP_VERDICTS });
|
|
374
374
|
|
|
375
|
+
;// CONCATENATED MODULE: ./src/posture/test-runner.js
|
|
376
|
+
// R5 (partial, roadmap) — the project's own test suite as a verification
|
|
377
|
+
// stage for `verifyFix()` (see `fix-verify.js`). Closes the gap where a
|
|
378
|
+
// "verified" fix only proved a finding's stableId stopped firing — a patch
|
|
379
|
+
// that deletes the feature entirely would satisfy that just as well as a
|
|
380
|
+
// real fix. Running the project's own tests is the cheapest available check
|
|
381
|
+
// that the application still works.
|
|
382
|
+
//
|
|
383
|
+
// Execution-safety note: this spawns the TARGET PROJECT's own test command
|
|
384
|
+
// in the target project's own directory. That is deliberately NOT routed
|
|
385
|
+
// through the R1 confinement sandbox (`../sandbox/`). That sandbox exists to
|
|
386
|
+
// contain untrusted proof-of-concept exploit code the scanner itself
|
|
387
|
+
// synthesizes — code nobody has vetted, being run for the first time. A
|
|
388
|
+
// project's pre-existing test suite is the opposite case: it is the
|
|
389
|
+
// project's own trusted source, already sitting on disk, and running it is
|
|
390
|
+
// exactly what a human developer does by hand before trusting a fix.
|
|
391
|
+
// Wrapping "npm test" / "pytest" / "go test" in the PoC sandbox's
|
|
392
|
+
// syscall/network/filesystem restrictions would break the large majority of
|
|
393
|
+
// real test suites (they bind local ports, spawn child processes, write temp
|
|
394
|
+
// fixtures, etc.) for no corresponding security benefit.
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
const DEFAULT_TIMEOUT_MS = 300_000;
|
|
401
|
+
const NPM_PLACEHOLDER = /Error: no test specified/i;
|
|
402
|
+
|
|
403
|
+
function _exists(scanRoot, rel) {
|
|
404
|
+
try { return external_node_fs_.existsSync(external_node_path_.join(scanRoot, rel)); } catch { return false; }
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function _isDir(scanRoot, rel) {
|
|
408
|
+
try { return external_node_fs_.statSync(external_node_path_.join(scanRoot, rel)).isDirectory(); } catch { return false; }
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function _binaryAvailable(cmd) {
|
|
412
|
+
try {
|
|
413
|
+
const r = (0,external_node_child_process_.spawnSync)(cmd, ['--version'], { timeout: 5_000, stdio: 'ignore' });
|
|
414
|
+
return !(r.error && r.error.code === 'ENOENT');
|
|
415
|
+
} catch {
|
|
416
|
+
return false;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Detect the project's test command. Read-only — never spawns the actual
|
|
421
|
+
// test run, only (optionally) a cheap `--version` probe to confirm a tool
|
|
422
|
+
// like `pytest` is actually installed before committing to it. Returns
|
|
423
|
+
// `null` when nothing detectable is found — most scanned repos will hit
|
|
424
|
+
// this path, and that must not be treated as a failure by callers.
|
|
425
|
+
function detectTestCommand(scanRoot) {
|
|
426
|
+
if (!scanRoot) return null;
|
|
427
|
+
|
|
428
|
+
// JS/TS — package.json with a real (non-placeholder) `scripts.test`.
|
|
429
|
+
let pkg = null;
|
|
430
|
+
try { pkg = JSON.parse(external_node_fs_.readFileSync(external_node_path_.join(scanRoot, 'package.json'), 'utf8')); } catch { pkg = null; }
|
|
431
|
+
const testScript = pkg && pkg.scripts && pkg.scripts.test;
|
|
432
|
+
if (testScript && !NPM_PLACEHOLDER.test(String(testScript))) {
|
|
433
|
+
if (_exists(scanRoot, 'pnpm-lock.yaml')) return { cmd: 'pnpm', args: ['test'], kind: 'pnpm' };
|
|
434
|
+
if (_exists(scanRoot, 'yarn.lock')) return { cmd: 'yarn', args: ['test'], kind: 'yarn' };
|
|
435
|
+
if (_exists(scanRoot, 'bun.lockb') || _exists(scanRoot, 'bun.lock')) return { cmd: 'bun', args: ['test'], kind: 'bun' };
|
|
436
|
+
return { cmd: 'npm', args: ['test', '--silent'], kind: 'npm' };
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Python — pytest.ini / pyproject.toml / tox.ini / a tests/ dir, and the
|
|
440
|
+
// `pytest` binary actually available. If pytest isn't installed we do NOT
|
|
441
|
+
// report a python test command — falling through lets a later language
|
|
442
|
+
// marker (e.g. go.mod in a polyglot repo) still be detected.
|
|
443
|
+
const pyMarker = _exists(scanRoot, 'pytest.ini') || _exists(scanRoot, 'pyproject.toml') ||
|
|
444
|
+
_exists(scanRoot, 'tox.ini') || _isDir(scanRoot, 'tests');
|
|
445
|
+
if (pyMarker && _binaryAvailable('pytest')) {
|
|
446
|
+
return { cmd: 'pytest', args: ['-q'], kind: 'pytest' };
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Go
|
|
450
|
+
if (_exists(scanRoot, 'go.mod')) {
|
|
451
|
+
return { cmd: 'go', args: ['test', './...'], kind: 'go' };
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return null;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Run the detected test command with a walltime budget. Always returns a
|
|
458
|
+
// result object — never throws. Distinguishes four outcomes:
|
|
459
|
+
// - no detectable/runnable command -> status: 'skipped' (does NOT fail)
|
|
460
|
+
// - ran and exited 0 -> status: 'passed'
|
|
461
|
+
// - ran and exited non-zero -> status: 'failed'
|
|
462
|
+
// - ran past the timeout budget -> status: 'failed', timedOut: true
|
|
463
|
+
function runProjectTests(scanRoot, { timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
464
|
+
const startedAt = Date.now();
|
|
465
|
+
const command = detectTestCommand(scanRoot);
|
|
466
|
+
if (!command) {
|
|
467
|
+
return {
|
|
468
|
+
status: 'skipped', passed: null, skipped: true,
|
|
469
|
+
reason: 'no-test-command-detected', exitCode: null, timedOut: false,
|
|
470
|
+
durationMs: Date.now() - startedAt,
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
let r;
|
|
475
|
+
try {
|
|
476
|
+
r = (0,external_node_child_process_.spawnSync)(command.cmd, command.args, {
|
|
477
|
+
cwd: scanRoot,
|
|
478
|
+
encoding: 'utf8',
|
|
479
|
+
timeout: timeoutMs,
|
|
480
|
+
env: { ...process.env, CI: '1' },
|
|
481
|
+
});
|
|
482
|
+
} catch (e) {
|
|
483
|
+
// The spawn call itself threw (rare — e.g. cwd vanished). Treat as
|
|
484
|
+
// "could not run", not "ran and failed".
|
|
485
|
+
return {
|
|
486
|
+
status: 'skipped', passed: null, skipped: true,
|
|
487
|
+
reason: `spawn-error: ${e.message}`, exitCode: null, timedOut: false,
|
|
488
|
+
durationMs: Date.now() - startedAt,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
const durationMs = Date.now() - startedAt;
|
|
492
|
+
|
|
493
|
+
if (r.error && r.error.code === 'ENOENT') {
|
|
494
|
+
// The detected tool isn't actually installed on this machine. Not a
|
|
495
|
+
// test failure — the suite never ran.
|
|
496
|
+
return {
|
|
497
|
+
status: 'skipped', passed: null, skipped: true,
|
|
498
|
+
reason: `${command.kind}-not-installed`, exitCode: null, timedOut: false, durationMs,
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (r.status === null) {
|
|
503
|
+
// spawnSync sets status:null both on timeout-kill and on being killed by
|
|
504
|
+
// another signal; either way the run did not complete, which is a
|
|
505
|
+
// verification failure, never a skip — we asked for a result and the
|
|
506
|
+
// process was terminated before producing one.
|
|
507
|
+
return {
|
|
508
|
+
status: 'failed', passed: false, skipped: false,
|
|
509
|
+
reason: 'timed-out', exitCode: null, timedOut: true, durationMs,
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return {
|
|
514
|
+
status: r.status === 0 ? 'passed' : 'failed',
|
|
515
|
+
passed: r.status === 0,
|
|
516
|
+
skipped: false,
|
|
517
|
+
reason: r.status === 0 ? null : 'test-failures',
|
|
518
|
+
exitCode: r.status,
|
|
519
|
+
timedOut: false,
|
|
520
|
+
durationMs,
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
|
|
375
524
|
;// CONCATENATED MODULE: ./src/posture/fix-verify.js
|
|
376
525
|
// Closed-loop /fix verification (Sentinel-parity FR-L4-4, FR-L4-5).
|
|
377
526
|
//
|
|
@@ -381,6 +530,10 @@ const _internals = Object.freeze({ BANNED_RESIDUAL_PHRASES, CITATION_RE, FP_VERD
|
|
|
381
530
|
// 1. The original finding's stableId no longer fires on the patched file.
|
|
382
531
|
// 2. No new findings at severity ≥ medium were introduced by the patch.
|
|
383
532
|
// 3. The project's existing linter (when present) passes on the patched file.
|
|
533
|
+
// 4. The project's own test suite (when detectable) still passes. This is
|
|
534
|
+
// the R5 gap-closer: a patch that silently deletes the feature would
|
|
535
|
+
// satisfy (1) and (2) just as well as a real fix — only running the
|
|
536
|
+
// tests catches that. See `test-runner.js` for detection + execution.
|
|
384
537
|
//
|
|
385
538
|
// If any of those fail, the caller is expected to NOT apply the patch and
|
|
386
539
|
// instead surface a "fix plan" — a numbered list of steps the engineer can
|
|
@@ -392,6 +545,7 @@ const _internals = Object.freeze({ BANNED_RESIDUAL_PHRASES, CITATION_RE, FP_VERD
|
|
|
392
545
|
|
|
393
546
|
|
|
394
547
|
|
|
548
|
+
|
|
395
549
|
const SEVERITY_RANK = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
|
|
396
550
|
|
|
397
551
|
// Run a focused re-scan over just the patched file(s) using the in-memory
|
|
@@ -494,29 +648,77 @@ function runLinter(cwd, cmd, args) {
|
|
|
494
648
|
// dishonest or over-claiming fix fails the gate. When `fixMeta` is absent
|
|
495
649
|
// (the deterministic MCP write path, which has no claims to check) the honesty
|
|
496
650
|
// gate is skipped and behavior is unchanged.
|
|
651
|
+
// R5 (partial) — the test-suite stage. Runs the target project's own tests,
|
|
652
|
+
// in the target project's own directory, against whatever is currently on
|
|
653
|
+
// disk there. See `test-runner.js`'s header comment for why that run is
|
|
654
|
+
// deliberately NOT routed through the R1 PoC-confinement sandbox: this is
|
|
655
|
+
// the project's own already-trusted suite, not untrusted synthesized code.
|
|
656
|
+
//
|
|
657
|
+
// Caveat that matters for callers: `verifyPatch` above re-scans the
|
|
658
|
+
// candidate patch purely in memory (no write to disk), but a test runner
|
|
659
|
+
// needs real files — there is no cheap way to hand a runner an in-memory
|
|
660
|
+
// overlay. So this leg reports on the CURRENT on-disk tree, not the
|
|
661
|
+
// candidate `files` map, when `verifyFix` is used as a pre-write preview
|
|
662
|
+
// (e.g. the `verify_fix` MCP tool). Callers that apply the patch first and
|
|
663
|
+
// then re-verify get the strongest signal; that ordering is not enforced
|
|
664
|
+
// here — it's the caller's responsibility, same as it already is for the
|
|
665
|
+
// closed-loop `fix-verify-loop.js` path.
|
|
666
|
+
// Does the caller's candidate patch differ from what is on disk right now?
|
|
667
|
+
// If so, any test run necessarily exercised the pre-patch tree. Compared by
|
|
668
|
+
// content so a patch that happens to match disk (already applied) is correctly
|
|
669
|
+
// treated as NOT pre-patch.
|
|
670
|
+
function _candidateDiffersFromDisk(scanRoot, files) {
|
|
671
|
+
if (!files || typeof files !== 'object') return false;
|
|
672
|
+
for (const [rel, content] of Object.entries(files)) {
|
|
673
|
+
if (typeof content !== 'string') continue;
|
|
674
|
+
try {
|
|
675
|
+
const abs = external_node_path_.resolve(scanRoot, rel);
|
|
676
|
+
if (external_node_fs_.readFileSync(abs, 'utf8') !== content) return true;
|
|
677
|
+
} catch {
|
|
678
|
+
return true; // candidate file absent on disk -> definitely not applied
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
return false;
|
|
682
|
+
}
|
|
683
|
+
|
|
497
684
|
async function verifyFix({
|
|
498
685
|
scanRoot,
|
|
499
686
|
originalFindingStableId,
|
|
500
687
|
files,
|
|
501
688
|
depFileContents,
|
|
502
689
|
fixMeta,
|
|
690
|
+
testTimeoutMs,
|
|
503
691
|
} = {}) {
|
|
504
692
|
const rescan = await verifyPatch({ scanRoot, originalFindingStableId, files, depFileContents });
|
|
505
693
|
const lint = runProjectLinter(scanRoot, Object.keys(files || {}));
|
|
694
|
+
const tests = runProjectTests(scanRoot, testTimeoutMs != null ? { timeoutMs: testTimeoutMs } : {});
|
|
695
|
+
// True when a candidate patch was supplied but has not been written, so the
|
|
696
|
+
// suite necessarily ran against the pre-patch tree. Surfaced in the summary
|
|
697
|
+
// and on the result so a caller cannot mistake it for a verified patch.
|
|
698
|
+
const _testedPrePatch = !tests.skipped && _candidateDiffersFromDisk(scanRoot, files);
|
|
699
|
+
const testsOk = tests.skipped ? true : tests.passed === true;
|
|
506
700
|
let honesty = null;
|
|
507
701
|
if (fixMeta && typeof fixMeta === 'object') {
|
|
508
702
|
try { honesty = gateFixOutput(fixMeta); } catch { honesty = null; }
|
|
509
703
|
}
|
|
510
|
-
const ok = rescan.ok && (lint.ok || lint.skipped) && (honesty ? honesty.ok : true);
|
|
704
|
+
const ok = rescan.ok && (lint.ok || lint.skipped) && testsOk && (honesty ? honesty.ok : true);
|
|
511
705
|
const summary = [
|
|
512
706
|
`re-scan: ${rescan.ok ? 'PASS' : 'FAIL — ' + rescan.reason}`,
|
|
513
707
|
`linter: ${lint.runner === 'none' ? 'skipped (no linter config)'
|
|
514
708
|
: lint.skipped ? `${lint.runner} not installed`
|
|
515
709
|
: lint.ok ? `${lint.runner} PASS`
|
|
516
710
|
: `${lint.runner} FAIL (exit ${lint.exitCode})`}`,
|
|
711
|
+
// Say which tree the suite actually ran against. `files` is a candidate
|
|
712
|
+
// patch held in memory; the runner needs real files, so it sees whatever is
|
|
713
|
+
// on disk. Reporting a bare "PASS" here would let a caller believe the
|
|
714
|
+
// PATCH passed the tests when the suite may have run on unpatched code.
|
|
715
|
+
`tests: ${tests.skipped ? `skipped (${tests.reason})`
|
|
716
|
+
: tests.timedOut ? 'FAIL (timed out)'
|
|
717
|
+
: tests.passed ? `PASS${_testedPrePatch ? ' — on the CURRENT on-disk tree, NOT the candidate patch' : ''}`
|
|
718
|
+
: `FAIL (exit ${tests.exitCode})`}`,
|
|
517
719
|
honesty ? `honesty: ${honesty.ok ? `PASS (${honesty.tier})` : 'FAIL — ' + honesty.violations.join('; ')}` : null,
|
|
518
720
|
].filter(Boolean).join('\n');
|
|
519
|
-
return { ok, rescan, lint, honesty, summary };
|
|
721
|
+
return { ok, rescan, lint, tests, testedPrePatch: _testedPrePatch, honesty, summary };
|
|
520
722
|
}
|
|
521
723
|
|
|
522
724
|
|
package/dist/178.index.js
CHANGED
|
@@ -13,7 +13,7 @@ export const modules = {
|
|
|
13
13
|
/* harmony import */ var node_child_process__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1421);
|
|
14
14
|
/* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(3024);
|
|
15
15
|
/* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(6760);
|
|
16
|
-
/* harmony import */ var _engine_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(
|
|
16
|
+
/* harmony import */ var _engine_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(4660);
|
|
17
17
|
// Time-travel + counterfactual scanning (v0.68).
|
|
18
18
|
//
|
|
19
19
|
// Two new modes that exploit the pure-input shape of runFullScan:
|