@clear-capabilities/agentic-security-scanner 0.161.0 → 0.162.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.
Files changed (57) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/bin/agentic-security.js +11 -2
  3. package/dist/4113.index.js +59 -8
  4. package/dist/5613.index.js +500 -0
  5. package/dist/6239.index.js +469 -0
  6. package/dist/7838.index.js +38 -2
  7. package/dist/8798.index.js +1 -1
  8. package/dist/9207.index.js +5 -0
  9. package/dist/9560.index.js +36 -22
  10. package/dist/{1905.index.js → 9999.index.js} +265 -8
  11. package/dist/agentic-security.mjs +2 -2
  12. package/dist/agentic-security.mjs.sha256 +1 -1
  13. package/package.json +9 -4
  14. package/src/discovery/CLAUDE.md +9 -0
  15. package/src/discovery/index.js +28 -0
  16. package/src/discovery/memory.js +7 -5
  17. package/src/fix/apply-fix-service.js +33 -3
  18. package/src/mcp/CLAUDE.md +9 -1
  19. package/src/mcp/tools.js +17 -4
  20. package/src/posture/CLAUDE.md +174 -0
  21. package/src/posture/assurance/baseline-inventory.js +218 -0
  22. package/src/posture/assurance/baseline.js +322 -0
  23. package/src/posture/assurance/bounded-io.js +145 -0
  24. package/src/posture/assurance/config.js +299 -0
  25. package/src/posture/assurance/contracts.js +315 -0
  26. package/src/posture/assurance/identity.js +62 -0
  27. package/src/posture/assurance/migrations.js +259 -0
  28. package/src/posture/assurance/schema-kit.js +111 -0
  29. package/src/posture/assurance/verification-record.js +258 -0
  30. package/src/posture/attestation.js +5 -0
  31. package/src/posture/autopilot.js +28 -1
  32. package/src/posture/evidence-bundle.js +93 -7
  33. package/src/posture/execution-proof.js +11 -1
  34. package/src/posture/fix-verify-loop.js +20 -6
  35. package/src/posture/fix-verify.js +37 -2
  36. package/src/posture/oracles/adapters.js +360 -0
  37. package/src/posture/oracles/conformance.js +275 -0
  38. package/src/posture/oracles/oracle.js +433 -0
  39. package/src/posture/oracles/registry.js +76 -0
  40. package/src/posture/oracles/scenario-classes.js +223 -0
  41. package/src/posture/replay/replay.js +249 -0
  42. package/src/posture/verification/advisory-state.js +71 -0
  43. package/src/posture/verification/emit.js +257 -0
  44. package/src/posture/verification/hypothesis-promotion.js +211 -0
  45. package/src/posture/verification/patch-negative.js +214 -0
  46. package/src/posture/verification/patch-promotion.js +105 -0
  47. package/src/posture/verification/projection.js +183 -0
  48. package/src/posture/verification/repair-records.js +124 -0
  49. package/src/posture/verifier.js +16 -0
  50. package/src/report/index.js +49 -0
  51. package/src/sandbox/CLAUDE.md +41 -0
  52. package/src/sandbox/backend-namespace.js +7 -0
  53. package/src/sandbox/backend-userspace.js +68 -29
  54. package/src/sandbox/control-probes.js +215 -0
  55. package/src/sandbox/supervise.js +201 -0
  56. package/src/sandbox/trust-boundary.js +113 -0
  57. package/src/sandbox/trust-domains.js +171 -0
package/CHANGELOG.md CHANGED
@@ -9,6 +9,26 @@
9
9
  > make the history less accurate, not more.
10
10
 
11
11
 
12
+ ## 0.162.0 - first slice of the differentiation programme: versioned evidence contracts, an enforced trust boundary, one verification record, trusted oracles, patch-negative verification, and a conformance gate
13
+
14
+ This is the foundation and verification slice of the Agentic Security Differentiation PRD (CORE-001 to 004, CORE-003 trust boundary, LOOP-001, X-201 to X-208). It is **not** the whole programme: the evaluation work (QA-001 to 008, which holds the accuracy gates), the deployment graph, tenant invariants, capability enforcement, model routing and portfolio assurance are unstarted, and no accuracy number is claimed or changed by this release. The new capabilities that execute anything are off by default.
15
+
16
+ **Evidence contracts (CORE-001, 002).** Versioned schemas, validators, stable identities and legacy migration adapters for verification records, observation bindings, capability decisions, routing labels and release evidence. A model verdict can only ever be an inference: `confirmed` needs trusted runtime proof or independent adjudication, and the six states (not-run, unsupported, inconclusive, error, refuted, confirmed) stay distinct through every round trip. A baseline capture tool (`npm run baseline:capture`) records source and bundle digests, dirty paths and an inventory of 18 existing capabilities, each checked against real paths and symbols; it invalidates only the capabilities whose files changed and never modifies source.
17
+
18
+ **Trust boundary (CORE-003).** Four trust domains (target, worker, verifier, signer) with a default-deny policy. `runInBoundary` runs a target only after active probes prove the controls it needs, and returns `blocked` (target never run) otherwise; target output is untrusted and never sets a status. Process trees are killed on timeout or cancel, with a descendant sweep and an orphan check. Signed evidence bundles now carry an `issuance` block that labels them `self-issued-local-key`, never independently certified. **Compatibility:** bundles signed before this release still verify and report no declared trust basis, but a verifier older than this release rejects newly signed bundles (strict unknown-key rule).
19
+
20
+ **One verification record, oracles, replay (X-201 to X-203).** Every verification surface emits the same record. Seven oracle adapters (injection execution, authorization decision, state transition, side-effect reachability, parser/resource behaviour, replay/idempotency, functional regression) each have positive, negative and inconclusive fixtures; an unavailable prerequisite is `unsupported`, never a pass. Replay manifests pin commit, patch, fixture, toolchain, oracle and budgets and reject any that fail validation.
21
+
22
+ **Patch-negative verification and non-taint classes (X-204, X-205).** A fix reaches `verified-fix` only with an exploit-positive original, an exploit-negative patched revision and a passing functional check; the specific incomplete step is recorded otherwise, and promotion requires receipts bound to the exact diff and revision. Tenant authorization, privileged action, workflow order, replay/idempotency and bounded resource exhaustion have executable oracles; taint absence never refutes them.
23
+
24
+ **Parity, gating and conformance (X-206 to X-208).** CLI JSON, MCP, reports and the autopilot response now share one projection (`verificationView`, additive; previous output fields are pinned by fixtures; migration notes in `docs/guides/verification-schema-migration.md`). Advisory hunt hypotheses cannot touch authoritative scan state and are promoted only with a verified receipt and an explicit policy, never on confidence or model agreement. A conformance suite over all oracle adapters runs as a new release check (`verification-conformance-gate`).
25
+
26
+ **Loop registration (LOOP-001).** `assurance-differentiation` profile for the supervised controller: the unmodified importer parses all 70 requirements, 269 weight and 210 criteria. Preflight on the real profile refuses launch, by design, until the other suites have runnable wrappers. Several limits the PRD names (heartbeat setting, subprocess wall time, network limits, provider envelope) are not enforced by the controller and are listed as unenforced.
27
+
28
+ **Known issue, not fixed here (present since at least 0.161.0).** The taint engine reports a regular expression's `.exec()` called on request data as a high-severity command injection (`child_process.exec`), for example `SEMVER.exec(req.query.v)`. This is a false positive that will appear in ordinary JavaScript. Fixing the sink model touches the corpus and recall gates, so it is left for its own change. The self-scan baseline records the two such findings in this repository as reviewed false positives.
29
+
30
+ **Not verified here.** Everything that executes targets was exercised on macOS only; Linux enforcement is declared unverified in every manifest, and on a host where the boundary cannot run the conformance gate reports execution as not-run, never passed. Receipts are unsigned and valid only within the verifier's process. The patch-negative ledger and the promotion audit record are returned, not persisted.
31
+
12
32
  ## 0.161.0 - the first honest out-of-sample numbers, a ReDoS in the scanner itself fixed, large Haskell projects now finish, multi-file fixes over MCP, auth-guard order proved structurally, sturdier tests and a sharded CI
13
33
 
14
34
  **Measured, and not flattering.** The unseen shape set the engine had been tuned against (v2) was retired and a fresh one (v3: 13 Haskell and 12 Nix families, 300 cases, written without reading the engine's rules) was measured exactly once, with nothing tuned afterwards. On it: Haskell SAST precision 75.0% / recall 50.0% / F1 60.0% (24 TP, 8 FP, 24 FN); Haskell taint 84.6% / 73.3% / 78.6%; Nix config-SAST 100% / 87.9% / 93.5%; Nix config-taint precision 60.0% (all four false positives are duplicate findings on scripts the new set's author wrote with two sinks each, an authoring artefact, not an engine fault). The previous set had scored the Haskell rows 91.7% and 93.3% after being tuned against. All four generalization rows are now below target and the published support tables say so. The misses are concrete and listed in `docs/language-support.md` (for example session cookies set without Secure, missing ownership checks in Yesod and Servant handlers, `randomIO` as a CSRF token, `printf` logging a secret, some HDBC, Scotty `file` and `Network.HTTP.Req` shapes). Closing them is a tuning round that consumes this set; it was deliberately not done here, so the numbers stand as measured. The retired set is kept as a development set (`shape-dev-2`), which now scores 96% to 100%: that is a tuned-on set and is not a generalization number.
@@ -2290,7 +2290,7 @@ async function cmdVerify(args) {
2290
2290
  process.env.AGENTIC_SECURITY_VERIFY_LIVE = '1';
2291
2291
  process.env.AGENTIC_SECURITY_VERIFY_TARGET = targetFlag;
2292
2292
  }
2293
- const { annotateVerifierVerdicts, verifierCoverageSummary, recordVerifierRun } = await import('../src/posture/verifier.js');
2293
+ const { annotateVerifierVerdicts, verifierCoverageSummary, recordVerifierRun, verifierVerificationRecords } = await import('../src/posture/verifier.js');
2294
2294
  const filter = args.flags.finding ? findings.filter(f => f.id === args.flags.finding || f.stableId === args.flags.finding) : findings;
2295
2295
  if (!filter.length) {
2296
2296
  console.error(`No matching findings (use --finding <id>).`);
@@ -2319,7 +2319,13 @@ async function cmdVerify(args) {
2319
2319
  // Adversarial premortem Q1: a durable record that a security assessment of
2320
2320
  // findings actually happened, so module:verifier has something real to
2321
2321
  // point at (see verifier.js's recordVerifierRun header comment).
2322
- recordVerifierRun(scanRoot, { findingCount: filter.length, live: liveFlag, target: targetFlag || null, summary: sum });
2322
+ // X-201: one version-1 verification record per verified finding, in the durable run record (never on the findings,
2323
+ // which are written back to last-scan.json below).
2324
+ const { headCommit } = await import('../src/posture/verification/emit.js');
2325
+ recordVerifierRun(scanRoot, {
2326
+ findingCount: filter.length, live: liveFlag, target: targetFlag || null, summary: sum,
2327
+ verificationRecords: verifierVerificationRecords(filter, { commit: headCommit(scanRoot) }),
2328
+ });
2323
2329
  if (args.flags.verbose || args.flags.finding) {
2324
2330
  for (const f of filter) {
2325
2331
  console.log(` ${f.file}:${f.line} ${f.vuln}`);
@@ -3289,6 +3295,9 @@ async function cmdVerifyAttestation(args) {
3289
3295
  console.log('');
3290
3296
  console.log(` proves: ${bundle.proves}`);
3291
3297
  console.log(` does NOT prove: ${bundle.doesNotProve}`);
3298
+ // CORE-003.AC03: who vouches. Informational only; the exit code is unchanged.
3299
+ console.log(` trust basis: ${r.trustBasis}${r.issuer ? ` (issuer ${r.issuer.id})` : ''}`);
3300
+ console.log(` ${r.trustNote}`);
3292
3301
  return 0;
3293
3302
  }
3294
3303
 
@@ -531,6 +531,7 @@ __webpack_require__.a(__webpack_module__, async (__webpack_handle_async_dependen
531
531
  /* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(73024);
532
532
  /* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(76760);
533
533
  /* harmony import */ var _fix_verify_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(27838);
534
+ /* harmony import */ var _verification_emit_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(85578);
534
535
  var __webpack_async_dependencies__ = __webpack_handle_async_dependencies__([_fix_verify_js__WEBPACK_IMPORTED_MODULE_3__]);
535
536
  _fix_verify_js__WEBPACK_IMPORTED_MODULE_3__ = (__webpack_async_dependencies__.then ? (await __webpack_async_dependencies__)() : __webpack_async_dependencies__)[0];
536
537
  // Closed-loop fix verification (v0.68).
@@ -559,6 +560,7 @@ _fix_verify_js__WEBPACK_IMPORTED_MODULE_3__ = (__webpack_async_dependencies__.th
559
560
 
560
561
 
561
562
 
563
+
562
564
  const DEFAULT_TIMEOUT_MS = 120_000;
563
565
 
564
566
  // Test-runner discovery. Each entry: a sentinel-file check + a command +
@@ -666,20 +668,32 @@ async function verifyFixWithTests({
666
668
  runTests = true,
667
669
  testRunnerOverride,
668
670
  testTimeoutMs,
671
+ patchNegative,
672
+ assuranceConfig,
669
673
  } = {}) {
670
- const scanLint = await (0,_fix_verify_js__WEBPACK_IMPORTED_MODULE_3__.verifyFix)({ scanRoot, originalFindingStableId, files, depFileContents });
674
+ const scanLint = await (0,_fix_verify_js__WEBPACK_IMPORTED_MODULE_3__.verifyFix)({ scanRoot, originalFindingStableId, files, depFileContents, patchNegative, assuranceConfig });
671
675
  const legs = {
672
676
  scan: { ok: scanLint.rescan?.ok ?? scanLint.ok, detail: scanLint.rescan ?? scanLint },
673
677
  lint: { ok: scanLint.lint?.ok ?? true, detail: scanLint.lint ?? null },
674
678
  tests: { ok: true, detail: null, skipped: true, reason: 'not-run' },
675
679
  };
676
- if (!legs.scan.ok || !legs.lint.ok) {
677
- return {
680
+ // X-201: the closed loop's own record (additive). It runs the detector, linter and project tests, never an exploit
681
+ // oracle, so it can say `not-run` or `error` and nothing stronger.
682
+ const withRecord = (res) => {
683
+ const e = (0,_verification_emit_js__WEBPACK_IMPORTED_MODULE_4__/* .emitVerification */ .T)('fix-verify-loop', res, { originalFindingStableId, files, scanRoot });
684
+ return { ...res, verificationRecord: e.ok ? e.record : null };
685
+ };
686
+ // X-204: the patch-negative leg is present only when it was requested and its feature is on. A patch the oracle did not verify
687
+ // is never `verified-clean`, whatever the other legs say.
688
+ const pn = scanLint.patchNegative && scanLint.patchNegative.status !== 'disabled' ? scanLint.patchNegative : null;
689
+ if (pn) legs.patchNegative = { ok: pn.verifiedFix === true, detail: { status: pn.status, incompleteStep: pn.incompleteStep, failureCode: pn.failureCode, reason: pn.reason } };
690
+ if (!legs.scan.ok || !legs.lint.ok || (pn && !legs.patchNegative.ok)) {
691
+ return withRecord({
678
692
  ok: false,
679
693
  verdict: 'verification-failed',
680
694
  legs,
681
695
  summary: _summarize(legs, 'verification-failed'),
682
- };
696
+ });
683
697
  }
684
698
  if (runTests) {
685
699
  const tests = runProjectTests(scanRoot, { runnerOverride: testRunnerOverride, timeoutMs: testTimeoutMs });
@@ -688,8 +702,8 @@ async function verifyFixWithTests({
688
702
  const allOk = legs.scan.ok && legs.lint.ok && legs.tests.ok;
689
703
  const verdict = !allOk
690
704
  ? 'verification-failed'
691
- : (legs.tests.skipped ? 'untested-but-passes' : 'verified-clean');
692
- return { ok: allOk, verdict, legs, summary: _summarize(legs, verdict) };
705
+ : pn ? 'verified-fix' : (legs.tests.skipped ? 'untested-but-passes' : 'verified-clean');
706
+ return withRecord({ ok: allOk, verdict, legs, summary: _summarize(legs, verdict) });
693
707
  }
694
708
 
695
709
  function _summarize(legs, verdict) {
@@ -697,6 +711,7 @@ function _summarize(legs, verdict) {
697
711
  bits.push(`scan: ${legs.scan.ok ? 'pass' : 'fail'}`);
698
712
  bits.push(`lint: ${legs.lint.skipped ? 'skip' : legs.lint.ok ? 'pass' : 'fail'}`);
699
713
  bits.push(`tests: ${legs.tests.skipped ? 'skip' : legs.tests.ok ? 'pass' : 'fail'}`);
714
+ if (legs.patchNegative) bits.push(`patch-negative: ${legs.patchNegative.ok ? 'pass' : `fail (${legs.patchNegative.detail.incompleteStep})`}`);
700
715
  return `${verdict} (${bits.join(' · ')})`;
701
716
  }
702
717
 
@@ -722,6 +737,7 @@ __webpack_require__.r(__webpack_exports__);
722
737
  /* harmony import */ var _fix_honesty_gate_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(27785);
723
738
  /* harmony import */ var _test_runner_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(35136);
724
739
  /* harmony import */ var _fix_metrics_js__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(32238);
740
+ /* harmony import */ var _verification_emit_js__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(85578);
725
741
  var __webpack_async_dependencies__ = __webpack_handle_async_dependencies__([_engine_js__WEBPACK_IMPORTED_MODULE_3__]);
726
742
  _engine_js__WEBPACK_IMPORTED_MODULE_3__ = (__webpack_async_dependencies__.then ? (await __webpack_async_dependencies__)() : __webpack_async_dependencies__)[0];
727
743
  // Closed-loop /fix verification (Sentinel-parity FR-L4-4, FR-L4-5).
@@ -749,6 +765,7 @@ _engine_js__WEBPACK_IMPORTED_MODULE_3__ = (__webpack_async_dependencies__.then ?
749
765
 
750
766
 
751
767
 
768
+
752
769
  const SEVERITY_RANK = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
753
770
 
754
771
  // Run a focused re-scan over just the patched file(s) using the in-memory
@@ -913,6 +930,8 @@ async function verifyFix({
913
930
  testTimeoutMs,
914
931
  recordMetrics = true,
915
932
  poc,
933
+ patchNegative,
934
+ assuranceConfig,
916
935
  } = {}) {
917
936
  // R5 (reporting half) — time each stage as it runs. Measured here rather
918
937
  // than inside each stage because only this function knows the boundaries of
@@ -995,7 +1014,26 @@ async function verifyFix({
995
1014
  }
996
1015
  _lap('honesty');
997
1016
 
998
- const ok = rescan.ok && (lint.ok || lint.skipped) && testsOk && pocOk && (honesty ? honesty.ok : true);
1017
+ // X-204: patch-negative verification, only when the caller asked for it AND the `patch-negative-verification` feature is
1018
+ // on. `patchNegative` is the requester's exploit scenario and functional cases ({ original, functional, commit?, ... }); the
1019
+ // candidate `files` are the proposed diff. With the flag off, or no request, nothing here runs and every field below is
1020
+ // exactly what it was before.
1021
+ let pn = null;
1022
+ if (patchNegative && typeof patchNegative === 'object') {
1023
+ try {
1024
+ const { verifyPatchNegative } = await Promise.all(/* import() */[__webpack_require__.e(6239), __webpack_require__.e(5613)]).then(__webpack_require__.bind(__webpack_require__, 95613));
1025
+ pn = await verifyPatchNegative({
1026
+ hypothesisId: originalFindingStableId, ...patchNegative, patch: { files },
1027
+ commit: patchNegative.commit ?? (0,_verification_emit_js__WEBPACK_IMPORTED_MODULE_7__.headCommit)(scanRoot),
1028
+ }, { config: assuranceConfig });
1029
+ } catch (e) {
1030
+ pn = { status: 'error', verifiedFix: false, incompleteStep: 'patch-negative', failureCode: 'harness-error', reason: `patch-negative verification failed to run: ${String(e?.message || e).slice(0, 200)}`, summary: '' };
1031
+ }
1032
+ }
1033
+ const pnActive = pn !== null && pn.status !== 'disabled';
1034
+ _lap('patchNegative');
1035
+
1036
+ const ok = rescan.ok && (lint.ok || lint.skipped) && testsOk && pocOk && (honesty ? honesty.ok : true) && (pnActive ? pn.verifiedFix === true : true);
999
1037
 
1000
1038
  // FR-305 (assurance-hardening PRD): `ok` alone conflates "every leg
1001
1039
  // genuinely ran and passed" with "passed, but a required leg was skipped
@@ -1037,6 +1075,7 @@ async function verifyFix({
1037
1075
  : pocLeg.status === 'no-longer-proven' ? 'poc: PASS (ran against the patch and no longer demonstrates the vulnerability)'
1038
1076
  : `poc: inconclusive — not counted either way (${pocLeg.reason || 'no detail reported'})`,
1039
1077
  // FR-305: never let a degraded pass read the same as a full one.
1078
+ pnActive ? `patch-negative: ${pn.verifiedFix ? 'PASS (verified-fix)' : `FAIL - not verified-fix, incomplete step '${pn.incompleteStep}' (${pn.failureCode}): ${pn.reason}`}` : null,
1040
1079
  ok && !verifiedFull ? `NOTE: PASSED, but NOT fully verified — ${degradedLegs.join('; ')}` : null,
1041
1080
  ].filter(Boolean).join('\n');
1042
1081
  // Persist the attempt so the distribution can be reported from real runs.
@@ -1063,7 +1102,19 @@ async function verifyFix({
1063
1102
  });
1064
1103
  }
1065
1104
 
1066
- return { ok, verifiedFull, degradedLegs, rescan, lint, tests, testedPrePatch: _testedPrePatch, honesty, poc: pocLeg, durations, summary };
1105
+ // X-201: the one verification record, ADDITIVE. `ok` and every field above stay exactly as they were; the record
1106
+ // says what was actually established (a static re-scan is not an exploit oracle, so without a PoC leg it is
1107
+ // `not-run`, and nothing here can reach `confirmed`/`refuted`: that takes a trusted oracle adapter).
1108
+ const emitted = (0,_verification_emit_js__WEBPACK_IMPORTED_MODULE_7__/* .emitVerification */ .T)('fix-verify', { rescan, lint, tests, poc: pocLeg }, {
1109
+ originalFindingStableId, files, scanRoot, finding: poc?.finding,
1110
+ });
1111
+ const verificationRecord = emitted.ok ? emitted.record : null;
1112
+
1113
+ return {
1114
+ ok, verifiedFull, degradedLegs, rescan, lint, tests, testedPrePatch: _testedPrePatch, honesty, poc: pocLeg, durations, summary, verificationRecord,
1115
+ // additive, present only when patch-negative verification was requested (X-204)
1116
+ ...(pn ? { patchNegative: pn, ...(pnActive ? { fixStatus: pn.verifiedFix ? 'verified-fix' : 'not-verified-fix' } : {}) } : {}),
1117
+ };
1067
1118
  }
1068
1119
 
1069
1120
  __webpack_async_result__();