@clear-capabilities/agentic-security-scanner 0.143.0 → 0.145.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 (135) hide show
  1. package/CHANGELOG.md +555 -0
  2. package/bin/agentic-security.js +770 -49
  3. package/dist/1.index.js +223 -0
  4. package/dist/113.index.js +117 -18
  5. package/dist/144.index.js +163 -0
  6. package/dist/178.index.js +24 -6
  7. package/dist/238.index.js +3 -2
  8. package/dist/265.index.js +191 -0
  9. package/dist/271.index.js +165 -0
  10. package/dist/384.index.js +1 -1
  11. package/dist/435.index.js +187 -52
  12. package/dist/444.index.js +11 -2
  13. package/dist/449.index.js +76 -12
  14. package/dist/526.index.js +117 -18
  15. package/dist/552.index.js +97 -0
  16. package/dist/637.index.js +27 -5
  17. package/dist/730.index.js +311 -0
  18. package/dist/736.index.js +301 -0
  19. package/dist/824.index.js +7 -0
  20. package/dist/905.index.js +88 -22
  21. package/dist/920.index.js +491 -0
  22. package/dist/970.index.js +173 -0
  23. package/dist/agentic-security.mjs +13 -13
  24. package/dist/agentic-security.mjs.sha256 +1 -1
  25. package/dist/calibration-seed.json +2 -0
  26. package/package.json +23 -9
  27. package/src/compare.js +6 -1
  28. package/src/dataflow/CLAUDE.md +1 -1
  29. package/src/dataflow/index.js +18 -0
  30. package/src/dataflow/privacy-catalog.js +290 -0
  31. package/src/dataflow/privacy-deep-walker.js +515 -0
  32. package/src/dataflow/privacy-governance.js +126 -0
  33. package/src/dataflow/privacy-inventory.js +154 -0
  34. package/src/dataflow/privacy-sink-policy.js +125 -0
  35. package/src/dataflow/privacy-taint.js +115 -54
  36. package/src/dataflow/privacy-taxonomy.js +233 -0
  37. package/src/discovery/disprove.js +7 -3
  38. package/src/discovery/hunter.js +9 -5
  39. package/src/discovery/index.js +2 -2
  40. package/src/discovery/llm-invoke.js +69 -13
  41. package/src/egress/audit.js +147 -0
  42. package/src/egress/policy.js +313 -0
  43. package/src/egress/redact.js +180 -0
  44. package/src/engine.js +1048 -302
  45. package/src/fix/apply-fix-service.js +404 -0
  46. package/src/fix/approver-registry.js +157 -0
  47. package/src/history-scan.js +22 -5
  48. package/src/ir/CLAUDE.md +1 -1
  49. package/src/llm-validator/index.js +86 -9
  50. package/src/llm-validator/model-status.js +66 -0
  51. package/src/lsp/server.js +49 -2
  52. package/src/mcp/tools.js +177 -50
  53. package/src/pipeline/analyzer-supervisor.js +93 -0
  54. package/src/pipeline/analyzer-worker.js +26 -0
  55. package/src/pipeline/annotator-runner.js +33 -0
  56. package/src/pipeline/assurance-mode.js +154 -0
  57. package/src/pipeline/cascade-worker-pool.js +172 -0
  58. package/src/pipeline/cascade-worker.js +43 -0
  59. package/src/pipeline/coverage-ledger.js +0 -0
  60. package/src/pipeline/detector-runner.js +51 -0
  61. package/src/pipeline/enrichment-completion.js +58 -0
  62. package/src/pipeline/evidence-provenance.js +91 -0
  63. package/src/pipeline/finding-schema.js +108 -0
  64. package/src/pipeline/legacy-compat.js +101 -0
  65. package/src/pipeline/producer-collector.js +48 -0
  66. package/src/pipeline/producer-registry.js +112 -0
  67. package/src/pipeline/scan-health.js +144 -0
  68. package/src/posture/CLAUDE.md +123 -0
  69. package/src/posture/accuracy-scorecard.js +156 -1
  70. package/src/posture/adversary-agent.js +15 -3
  71. package/src/posture/artifact-registry.js +241 -0
  72. package/src/posture/auditor-walkthrough.js +186 -21
  73. package/src/posture/calibration-feedback.js +201 -0
  74. package/src/posture/calibration-seed.json +2 -0
  75. package/src/posture/calibration.js +25 -0
  76. package/src/posture/compliance-evidence-signing.js +131 -0
  77. package/src/posture/compliance-policy.js +324 -17
  78. package/src/posture/cross-repo-memory.js +7 -2
  79. package/src/posture/custom-rules.js +36 -0
  80. package/src/posture/deterministic.js +8 -1
  81. package/src/posture/encryption-provider.js +205 -0
  82. package/src/posture/evidence-grade-wording.js +71 -0
  83. package/src/posture/fix-history.js +137 -20
  84. package/src/posture/fix-honesty-gate.js +47 -6
  85. package/src/posture/fix-verify.js +65 -8
  86. package/src/posture/fleet.js +0 -0
  87. package/src/posture/flow-narration.js +7 -2
  88. package/src/posture/git-history.js +13 -5
  89. package/src/posture/legal-hold.js +140 -0
  90. package/src/posture/llm-redteam.js +10 -1
  91. package/src/posture/material-change.js +111 -2
  92. package/src/posture/mttr.js +75 -12
  93. package/src/posture/policy-bundle.js +274 -0
  94. package/src/posture/pre-incident-archaeology.js +39 -7
  95. package/src/posture/privacy-framework.js +47 -6
  96. package/src/posture/production-feedback.js +179 -0
  97. package/src/posture/provenance/ai-authorship.js +68 -0
  98. package/src/posture/provenance/branch-entry.js +80 -0
  99. package/src/posture/provenance/cache.js +143 -0
  100. package/src/posture/provenance/confidence.js +36 -0
  101. package/src/posture/provenance/coordinator.js +786 -0
  102. package/src/posture/provenance/dag-walk.js +249 -0
  103. package/src/posture/provenance/evidence-attribution.js +59 -0
  104. package/src/posture/provenance/git-evidence.js +310 -0
  105. package/src/posture/provenance/lifecycle.js +208 -0
  106. package/src/posture/provenance/missing-control-resolver.js +137 -0
  107. package/src/posture/provenance/origin-resolver.js +342 -0
  108. package/src/posture/provenance/predicate-replay.js +133 -0
  109. package/src/posture/provenance/providers/config.js +39 -0
  110. package/src/posture/provenance/providers/github.js +62 -0
  111. package/src/posture/provenance/providers/gitlab.js +58 -0
  112. package/src/posture/provenance/repo-lineage.js +74 -0
  113. package/src/posture/provenance/sca-origin.js +139 -0
  114. package/src/posture/provenance/schema.js +255 -0
  115. package/src/posture/provenance/transitive-sca.js +147 -0
  116. package/src/posture/provenance/validate.js +30 -0
  117. package/src/posture/provenance-evidence-bundle.js +144 -0
  118. package/src/posture/retention-policy.js +132 -0
  119. package/src/posture/risk-dollars.js +216 -26
  120. package/src/posture/sbom-diff.js +15 -2
  121. package/src/posture/scan-checkpoint.js +176 -31
  122. package/src/posture/secret-history.js +10 -2
  123. package/src/posture/state-dir.js +64 -5
  124. package/src/posture/state-lifecycle-report.js +77 -0
  125. package/src/posture/suppressions.js +59 -3
  126. package/src/posture/vuln-archaeology.js +8 -2
  127. package/src/pr-delta.js +25 -4
  128. package/src/privacy/ir-adapter.js +380 -0
  129. package/src/report/index.js +248 -5
  130. package/src/report/oscal.js +7 -2
  131. package/src/runScan.js +34 -5
  132. package/src/sast/cpp.js +3 -14
  133. package/src/sast/rate-limit.js +33 -3
  134. package/src/sca/llm-function-extract.js +6 -0
  135. package/src/util/git-hardening.js +128 -0
package/dist/435.index.js CHANGED
@@ -23,6 +23,12 @@ var external_node_url_ = __webpack_require__(3136);
23
23
  var promises_ = __webpack_require__(1455);
24
24
  // EXTERNAL MODULE: ./src/posture/fix-history.js
25
25
  var fix_history = __webpack_require__(4407);
26
+ // EXTERNAL MODULE: ./src/fix/apply-fix-service.js
27
+ var apply_fix_service = __webpack_require__(7730);
28
+ // EXTERNAL MODULE: ./src/posture/material-change.js
29
+ var material_change = __webpack_require__(4629);
30
+ // EXTERNAL MODULE: ./src/fix/approver-registry.js
31
+ var approver_registry = __webpack_require__(437);
26
32
  ;// CONCATENATED MODULE: ./src/posture/deterministic-fix.js
27
33
  // Deterministic fix synthesis (#1) — for the narrow set of vulnerability classes
28
34
  // where a context-INDEPENDENT literal swap is a safe, correct fix, produce a
@@ -92,6 +98,8 @@ function synthesizeDeterministicPatch(finding, fileContent) {
92
98
 
93
99
  // EXTERNAL MODULE: ./src/posture/integrity.js
94
100
  var integrity = __webpack_require__(1130);
101
+ // EXTERNAL MODULE: ./src/posture/state-dir.js
102
+ var state_dir = __webpack_require__(1174);
95
103
  // EXTERNAL MODULE: ./src/posture/cache-economics.js
96
104
  var cache_economics = __webpack_require__(8752);
97
105
  ;// CONCATENATED MODULE: ./src/mcp/redact.js
@@ -197,10 +205,10 @@ function redactArgsBlob(s) {
197
205
  return redactString(s);
198
206
  }
199
207
 
200
- // EXTERNAL MODULE: ./src/report/index.js + 2 modules
201
- var report = __webpack_require__(3550);
202
- // EXTERNAL MODULE: ./src/posture/state-dir.js
203
- var state_dir = __webpack_require__(1174);
208
+ // EXTERNAL MODULE: ./src/report/index.js + 3 modules
209
+ var report = __webpack_require__(457);
210
+ // EXTERNAL MODULE: ./src/posture/provenance/schema.js
211
+ var schema = __webpack_require__(4594);
204
212
  ;// CONCATENATED MODULE: ./src/posture/agents-memory.js
205
213
  // AGENTS.md — writable continual-learning memory (harness-anatomy #2).
206
214
  //
@@ -499,6 +507,14 @@ const cve_lookup_internals = { CACHE_DIR, CVE_RE, _stalenessTier };
499
507
 
500
508
 
501
509
 
510
+
511
+
512
+
513
+ // Git-origin provenance (Finding Provenance M0/M1). Distinct from
514
+ // `finding.provenance` (AI-authorship) and from an SCA entry's `provenance`
515
+ // (Sigstore/SLSA attestation) — see report/index.js's import comment.
516
+
517
+
502
518
  // Lazy-loaded: these transitively pull in npm packages (@babel/core and
503
519
  // friends) that aren't available in the plugin-cache install path
504
520
  // (no node_modules). Deferring keeps the MCP server bootable everywhere;
@@ -863,26 +879,44 @@ const scan_diff = {
863
879
  // / MAX_TOTAL_SCAN_BYTES, so this does not turn scan_diff into a
864
880
  // full-project deep scan).
865
881
  const runScan = await getRunScan();
866
- const result = await runScan(sessionRoot, { network: false, fileContents, deep: true, deepInCi: true });
882
+ // FR-704 (assurance-hardening PRD): this tool's own description promises
883
+ // "runs scan in memory" — without this, runFullScan's own state writers
884
+ // (dpia.md, ropa.md, privacy-framework.json, threat-model.json, and
885
+ // others) fire unconditionally on every call, silently mutating the
886
+ // user's real project on every pre-write self-correction scan. Confirmed
887
+ // by direct execution before this fix (11 state artifacts written by a
888
+ // single scan_diff-shaped call).
889
+ const result = await (0,state_dir/* withStateWritesDisabled */.Ao)(() =>
890
+ runScan(sessionRoot, { network: false, fileContents, deep: true, deepInCi: true }));
867
891
  const wantSet = new Set(Object.keys(fileContents));
868
892
  const sevRank = { info: 0, low: 1, medium: 2, high: 3, critical: 4 };
869
893
  const min = sevRank[severity] ?? 0;
870
- // Stage 6 correctness audit: this only ever read result.scan.findings
871
- // (the SAST channel) — scan.secrets and scan.logicVulns are separate
872
- // arrays on the raw runScan() result (report/index.js's normalizeFindings
873
- // is what merges all four channels, and that merge hasn't run yet here).
874
- // A file containing a bare hardcoded credential reported findingCount: 0
875
- // through a tool whose own description promises "Use BEFORE writing a
876
- // Write/Edit to disk so the agent can self-correct". Also reused
877
- // _remediationOf so a fix-string detector (the majority of engine.js's
878
- // own, ~127 call sites) doesn't silently report an empty `description`
879
- // the way reading only `.remediation` did.
880
- const findings = [...(result.scan.findings || []), ...(result.scan.secrets || []), ...(result.scan.logicVulns || [])]
894
+ // Stage 6 correctness audit (historical): this used to only read
895
+ // result.scan.findings (the SAST channel) — scan.secrets and
896
+ // scan.logicVulns are separate arrays on the raw runScan() result, and a
897
+ // hand-rolled 3-channel concat here was a second, divergent copy of the
898
+ // merge report/index.js's normalizeFindings() already does (four
899
+ // channels, plus per-channel defaulting and remediation-string
900
+ // resolution the old concat re-implemented separately and could drift
901
+ // from). Assurance-hardening PRD FR-105 ("JSON, SARIF, HTML, CSV, JUnit,
902
+ // and MCP outputs derive from the same validated object"): route through
903
+ // the same canonical merge every other output format uses.
904
+ //
905
+ // This closes the field-mapping/dedup divergence, but does NOT make
906
+ // scan_diff surface SCA/supply-chain findings end to end: this handler
907
+ // never builds a `depFileContents` map (everything a caller passes in
908
+ // `files`, manifests included, lands in `fileContents`), and manifest-
909
+ // based supply-chain detection in engine.js reads only `depFileContents`
910
+ // — so `result.scan.supplyChain` is always empty for this tool today
911
+ // regardless of this fix. That is a separate, real limitation (scan_diff
912
+ // was designed for pre-write code self-correction, not manifest
913
+ // scanning), left as-is rather than silently claimed fixed here.
914
+ const findings = (0,report.normalizeFindings)(result.scan)
881
915
  .filter(f => wantSet.has(String(f.file || '').replace(/\\/g, '/')) && (sevRank[f.severity] ?? 0) >= min)
882
916
  .map(f => redactFinding({
883
917
  id: f.id, severity: f.severity, file: f.file, line: f.line,
884
- title: f.title || f.vuln, cwe: f.cwe,
885
- description: f.description, remediation: (0,report/* _remediationOf */.uV)(f),
918
+ title: f.vuln, cwe: f.cwe,
919
+ description: f.description, remediation: f.remediation,
886
920
  }));
887
921
  // Harness-anatomy #1: offload when the result exceeds OFFLOAD_THRESHOLD.
888
922
  // The agent gets a head+tail preview plus a path it can page through;
@@ -1025,6 +1059,21 @@ const explain_finding = {
1025
1059
  epssScore: typeof f.epssScore === 'number' ? f.epssScore : null,
1026
1060
  epssPercentile: typeof f.epssPercentile === 'number' ? f.epssPercentile : null,
1027
1061
  exploitedNow: !!f.exploitedNow,
1062
+ // Which commit introduced this finding. `includeEmail` stays at its
1063
+ // DEFAULT (false) unconditionally — unlike the JSON report there is no
1064
+ // operator-set env escape for it here, because the consumer is an
1065
+ // agent that has no business receiving a committer's email address.
1066
+ // `pseudonymize`, by contrast, IS read back from the same env var
1067
+ // report/index.js's `_normalizedProvenance` reads
1068
+ // (AGENTIC_SECURITY_PSEUDONYMIZE_AUTHORS=1 / --pseudonymize-authors) —
1069
+ // fix-round item 4: an operator who set that policy was still getting
1070
+ // raw committer names (and, via providerEnrichment, raw reviewer
1071
+ // logins/CODEOWNERS lines) through this MCP surface because this call
1072
+ // passed no options object at all, silently defeating their policy at
1073
+ // this one output boundary while report/index.js honoured it.
1074
+ findingProvenance: f.findingProvenance ? (0,schema/* redactFindingProvenance */.As)(f.findingProvenance, {
1075
+ pseudonymize: process.env.AGENTIC_SECURITY_PSEUDONYMIZE_AUTHORS === '1',
1076
+ }) : null,
1028
1077
  };
1029
1078
  },
1030
1079
  };
@@ -1032,7 +1081,7 @@ const explain_finding = {
1032
1081
  // ─── apply_fix ───────────────────────────────────────────────────────────────
1033
1082
  const apply_fix = {
1034
1083
  name: 'apply_fix',
1035
- description: 'Apply a fix for a finding. Two modes: (1) the stored fix.replacement, or (2) a caller-supplied `patch` (a files map) which is RE-VERIFIED inline (rescan-clean + no new ≥medium + lint) before any write — this unblocks findings that ship only a template or description. Refuses if last-scan.json fails its HMAC check, if the finding is shadow-marked, or if a path escapes the session root via lexical traversal OR a symlink. Requires confirm:true. Supports dry_run:true to preview without writing.',
1084
+ description: 'Apply a fix for a finding. Two modes: (1) the stored fix.replacement, or (2) a caller-supplied `patch` (a files map) which is RE-VERIFIED inline (rescan-clean + no new ≥medium + lint) before any write — this unblocks findings that ship only a template or description. Refuses if last-scan.json fails its HMAC check, if the finding is shadow-marked, or if a path escapes the session root via lexical traversal OR a symlink. Requires confirm:true. Supports dry_run:true to preview without writing. On success, `verified:true` means verification passed but `verifiedFull:true` is the honest signal that every required leg (lint when configured, tests when a runner exists) genuinely ran — a false `verifiedFull` with `verified:true` means the pass is real but degraded (see `verify.degradedLegs`), not a full verification.',
1036
1085
  inputSchema: {
1037
1086
  type: 'object',
1038
1087
  additionalProperties: false,
@@ -1073,6 +1122,27 @@ const apply_fix = {
1073
1122
  partialSanitization: { type: 'boolean' },
1074
1123
  },
1075
1124
  },
1125
+ // FR-307/FR-1002: this schema had `additionalProperties: false`
1126
+ // and never declared `approval` — the property apply-fix-
1127
+ // service.js's high-impact-change gate has required since FR-307
1128
+ // was built. A real MCP caller supplying fixMeta.approval was
1129
+ // rejected by validate.js at the schema layer before the handler
1130
+ // ever ran, silently making the approval gate (and FR-1002's
1131
+ // identity check layered on it) unreachable from this tool's
1132
+ // only real production entry point. See D-0024.
1133
+ approval: {
1134
+ type: 'object',
1135
+ additionalProperties: false,
1136
+ properties: {
1137
+ approvedBy: { type: 'string', minLength: 1, maxLength: 200 },
1138
+ reason: { type: 'string', minLength: 1, maxLength: 1000 },
1139
+ },
1140
+ },
1141
+ // FR-1003: separation-of-duties. Self-reported the same way
1142
+ // approvedBy is — this tool has no way to determine who actually
1143
+ // wrote a patch, so `author` is a claim, checked against a
1144
+ // configurable policy the same way `approval` is.
1145
+ author: { type: 'string', minLength: 1, maxLength: 200 },
1076
1146
  },
1077
1147
  },
1078
1148
  },
@@ -1147,20 +1217,79 @@ const apply_fix = {
1147
1217
  verify: { rescan: verdict.rescan, lint: { runner: verdict.lint?.runner, ok: verdict.lint?.ok }, honesty: verdict.honesty || null },
1148
1218
  };
1149
1219
  }
1220
+ // FR-307/FR-1002/D-0024: this caller-supplied-patch branch writes via
1221
+ // applyFixHistory() directly and never called applyVerifiedFix() — so
1222
+ // the high-impact-change approval gate (auth/authZ/crypto/PII/schema/
1223
+ // infra-privilege/public-API) built for the OTHER apply_fix branch
1224
+ // (stored fix.replacement) never ran here at all, for any input. Since
1225
+ // this is the branch the tool's own description calls the one that
1226
+ // covers "~100% of findings that ship only a template," that gap was
1227
+ // the larger of the two found this cycle. Same before/after content
1228
+ // shape `apply-fix-service.js` already uses — read-first-in-try/catch
1229
+ // (D-0012), never existsSync-then-readFileSync.
1230
+ const filesForMaterialClassification = {};
1231
+ for (const [rel, v] of Object.entries(confinedAbs)) {
1232
+ let before = '';
1233
+ try { before = await promises_.readFile(v.abs, 'utf8'); } catch { /* new file — before stays '' */ }
1234
+ filesForMaterialClassification[rel] = { before, after: v.content };
1235
+ }
1236
+ const materialClassification = (0,material_change/* classifyFixMaterialRisk */.kz)(filesForMaterialClassification);
1150
1237
  if (dry_run) {
1151
- return { _meta: META, applied: false, dryRun: true, verified: true, files: Object.keys(confinedAbs), summary: verdict.summary };
1238
+ return { _meta: META, applied: false, dryRun: true, verified: true, files: Object.keys(confinedAbs), summary: verdict.summary, materialClassification };
1239
+ }
1240
+ if (materialClassification.highImpactCategories.length) {
1241
+ const approval = fixMeta && typeof fixMeta === 'object' ? fixMeta.approval : null;
1242
+ const hasApprovalEvidence = !!(approval && typeof approval === 'object' &&
1243
+ typeof approval.approvedBy === 'string' && approval.approvedBy.trim().length > 0 &&
1244
+ typeof approval.reason === 'string' && approval.reason.trim().length > 0);
1245
+ if (!hasApprovalEvidence) {
1246
+ return {
1247
+ _meta: META, applied: false,
1248
+ reason: `high-impact change (${materialClassification.highImpactCategories.join(', ')}) requires approval evidence — pass fixMeta.approval: {approvedBy, reason} — before it can be applied`,
1249
+ materialClassification,
1250
+ };
1251
+ }
1252
+ const approverRegistry = (0,approver_registry/* loadApproverRegistry */.P4)(ctx.sessionRoot);
1253
+ const requiredRoles = (0,approver_registry/* requiredRolesFor */.Kq)(approverRegistry, materialClassification.highImpactCategories);
1254
+ const identityCheck = (0,approver_registry/* verifyApprover */.$N)(approverRegistry, approval.approvedBy, requiredRoles);
1255
+ if (!identityCheck.verified) {
1256
+ return {
1257
+ _meta: META, applied: false,
1258
+ reason: `high-impact change (${materialClassification.highImpactCategories.join(', ')}) approval rejected: ${identityCheck.reason}`,
1259
+ materialClassification,
1260
+ };
1261
+ }
1262
+ // FR-1003: separation-of-duties, same no-op-unless-configured gate
1263
+ // as apply-fix-service.js's own copy — see approver-registry.js.
1264
+ const sodCheck = (0,approver_registry/* checkSeparationOfDuties */.I0)(approverRegistry, fixMeta?.author, approval.approvedBy);
1265
+ if (!sodCheck.ok) {
1266
+ return {
1267
+ _meta: META, applied: false,
1268
+ reason: `high-impact change (${materialClassification.highImpactCategories.join(', ')}) approval rejected: ${sodCheck.reason}`,
1269
+ materialClassification,
1270
+ };
1271
+ }
1152
1272
  }
1153
1273
  const written = [];
1154
1274
  try {
1155
1275
  for (const [rel, v] of Object.entries(confinedAbs)) {
1156
- const originalContent = external_node_fs_.existsSync(v.abs) ? await promises_.readFile(v.abs, 'utf8') : '';
1276
+ const fileExisted = external_node_fs_.existsSync(v.abs);
1277
+ const originalContent = fileExisted ? await promises_.readFile(v.abs, 'utf8') : '';
1157
1278
  const entry = await (0,fix_history/* applyFix */.oM)({
1158
- scanRoot: ctx.sessionRoot, file: rel, originalContent, newContent: v.content,
1279
+ scanRoot: ctx.sessionRoot, file: rel, originalContent, newContent: v.content, fileExisted,
1159
1280
  findingId: f.id, stableId: f.stableId, ruleId: f.ruleId || f.cwe || f.family || null, vuln: f.vuln || f.title || null,
1281
+ findingProvenance: f.findingProvenance || null,
1160
1282
  });
1161
1283
  written.push({ file: rel, historyId: entry.id, backupPath: entry.backupPath });
1162
1284
  }
1163
1285
  } catch (e) {
1286
+ // FR-306: roll back every file THIS batch already wrote before the
1287
+ // failure — applyFixHistory already restored the one file that just
1288
+ // failed; this covers the rest, so a multi-file patch never leaves
1289
+ // some files patched and others not.
1290
+ for (const w of written) {
1291
+ try { await (0,fix_history/* revertEntryById */.rJ)(ctx.sessionRoot, w.historyId); } catch { /* best-effort; original error still propagates below */ }
1292
+ }
1164
1293
  if (e && e.name === 'FixAttemptBudgetExceededError') {
1165
1294
  return { _meta: META, applied: false, reason: `budget-exceeded: ${e.message}`, budgetExceeded: true, attempts: e.attempts, maxAttempts: e.max, key: e.key };
1166
1295
  }
@@ -1168,7 +1297,7 @@ const apply_fix = {
1168
1297
  }
1169
1298
  let acceptance = null;
1170
1299
  try { acceptance = (0,fix_history/* fixAcceptanceRate */.XR)(ctx.sessionRoot); } catch { /* best-effort */ }
1171
- return { _meta: META, applied: true, verified: true, patched: written, integrity: status, verify: { summary: verdict.summary }, acceptance };
1300
+ return { _meta: META, applied: true, verified: true, patched: written, integrity: status, verify: { summary: verdict.summary }, acceptance, materialClassification };
1172
1301
  }
1173
1302
 
1174
1303
  if (typeof f.fix?.replacement !== 'string') {
@@ -1205,41 +1334,47 @@ const apply_fix = {
1205
1334
  };
1206
1335
  }
1207
1336
 
1208
- let entry;
1209
- try {
1210
- entry = await (0,fix_history/* applyFix */.oM)({
1211
- scanRoot: ctx.sessionRoot,
1212
- file: f.file,
1213
- originalContent,
1214
- newContent: f.fix.replacement,
1215
- findingId: f.id,
1216
- stableId: f.stableId || null, // premortem 4R-8
1217
- ruleId: f.ruleId || f.cwe || f.family || null,
1218
- vuln: f.vuln || f.title || null,
1219
- });
1220
- } catch (e) {
1221
- // Harness-engineering: step-budget refusal (post-derived). The
1222
- // deterministic layer enforces at-most-N attempts per stableId. When
1223
- // exceeded, surface it as a structured `budget-exceeded` outcome the
1224
- // agent can recognize — not a generic error.
1225
- if (e && e.name === 'FixAttemptBudgetExceededError') {
1226
- return {
1227
- _meta: META,
1228
- applied: false,
1229
- reason: `budget-exceeded: ${e.message}`,
1230
- budgetExceeded: true,
1231
- attempts: e.attempts,
1232
- maxAttempts: e.max,
1233
- key: e.key,
1234
- };
1337
+ // FR-301/A-08 (assurance-hardening PRD): this branch used to write
1338
+ // f.fix.replacement straight to disk with NO fresh verification — no
1339
+ // rescan, no lint, nothing confirming the stored replacement actually
1340
+ // closes the finding it claims to fix. The caller-patch branch above
1341
+ // already required this; there is no reason a STORED fix should be
1342
+ // trusted more than a caller-supplied one just because it shipped with
1343
+ // the finding. Routed through the same applyVerifiedFix() service the
1344
+ // CLI's `fix --apply` now also uses (src/fix/apply-fix-service.js) —
1345
+ // confinement/reserved-path are re-checked there too (harmless
1346
+ // redundancy with the dry_run preview above, kept for that preview's
1347
+ // size-diff shape) but the load-bearing addition is the verification
1348
+ // gate before the write.
1349
+ if (!f.stableId) {
1350
+ return { _meta: META, applied: false, reason: 'finding has no stableId — cannot verify a stored fix against it' };
1351
+ }
1352
+ const result = await (0,apply_fix_service/* applyVerifiedFix */.On)({
1353
+ scanRoot: ctx.sessionRoot,
1354
+ finding: f,
1355
+ files: { [f.file]: f.fix.replacement },
1356
+ fixMeta,
1357
+ });
1358
+ if (!result.ok) {
1359
+ if (result.budgetExceeded) {
1360
+ return { _meta: META, applied: false, reason: result.reason, budgetExceeded: true, attempts: result.attempts, maxAttempts: result.maxAttempts, key: result.key };
1235
1361
  }
1236
- throw e;
1362
+ return { _meta: META, applied: false, reason: result.reason, verify: result.verify || null };
1237
1363
  }
1238
1364
  // R25 (PRD §5): surface the running auto-fix acceptance rate after each
1239
1365
  // applied fix, so the closed loop reports its own success metric.
1240
1366
  let acceptance = null;
1241
1367
  try { acceptance = (0,fix_history/* fixAcceptanceRate */.XR)(ctx.sessionRoot); } catch { /* metric is best-effort */ }
1242
- return { _meta: META, applied: true, historyId: entry.id, file: f.file, backupPath: entry.backupPath, integrity: status, attemptOrdinal: entry.attemptOrdinal, acceptance };
1368
+ const entry = result.written[0];
1369
+ return {
1370
+ // FR-305: verifiedFull distinguishes "every required leg (lint, tests)
1371
+ // genuinely ran and passed" from "passed, but a required leg was
1372
+ // skipped or unavailable" — verified:true alone conflates them.
1373
+ _meta: META, applied: true, verified: true, verifiedFull: result.verifiedFull,
1374
+ historyId: entry.historyId, file: entry.file, backupPath: entry.backupPath,
1375
+ integrity: status, attemptOrdinal: entry.attemptOrdinal, acceptance,
1376
+ verify: result.verify,
1377
+ };
1243
1378
  },
1244
1379
  };
1245
1380
 
package/dist/444.index.js CHANGED
@@ -10,6 +10,7 @@ export const modules = {
10
10
  /* harmony export */ });
11
11
  /* unused harmony exports extractAddedLines, scanHistoryDiff */
12
12
  /* harmony import */ var node_child_process__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1421);
13
+ /* harmony import */ var _util_git_hardening_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(8844);
13
14
  // R15 (PRD §5) — git-history secret sweep.
14
15
  //
15
16
  // A secret removed from HEAD but present in any past commit is still
@@ -25,6 +26,7 @@ export const modules = {
25
26
 
26
27
 
27
28
 
29
+
28
30
  // Pull the post-image (added) lines out of a unified diff: lines starting with
29
31
  // a single '+' (not the '+++' file header). Returns reconstructed text.
30
32
  function extractAddedLines(diffText) {
@@ -74,8 +76,15 @@ function sweepGitHistory(scanRoot, detectFn, { maxCommits = 50, timeoutMs = 2000
74
76
  if (!scanRoot || typeof detectFn !== 'function') return [];
75
77
  let out;
76
78
  try {
77
- out = (0,node_child_process__WEBPACK_IMPORTED_MODULE_0__.execFileSync)('git', ['-C', scanRoot, 'log', '-p', '-n', String(maxCommits), '--no-color', '--no-merges', '--no-textconv'],
78
- { encoding: 'utf8', maxBuffer: 96 * 1024 * 1024, timeout: timeoutMs, stdio: ['ignore', 'pipe', 'ignore'] });
79
+ // Second independent Finding Provenance PRD audit (FR-PROV-024): this
80
+ // scanRoot is a scanned repository, not this project's own trusted
81
+ // checkout. `--no-textconv` alone (the pre-existing hardening here) closes
82
+ // the .gitattributes textconv surface but NOT `core.fsmonitor` /
83
+ // `core.hooksPath` — this `log -p` call renders every historical commit's
84
+ // diff content, the same shape verified exploitable in
85
+ // provenance/git-evidence.js, so it gets the full hardening too.
86
+ out = (0,node_child_process__WEBPACK_IMPORTED_MODULE_0__.execFileSync)('git', (0,_util_git_hardening_js__WEBPACK_IMPORTED_MODULE_1__/* .hardenGitArgs */ .Ax)(['-C', scanRoot, 'log', '-p', '-n', String(maxCommits), '--no-color', '--no-merges', '--no-textconv']),
87
+ { encoding: 'utf8', maxBuffer: 96 * 1024 * 1024, timeout: timeoutMs, stdio: ['ignore', 'pipe', 'ignore'], env: (0,_util_git_hardening_js__WEBPACK_IMPORTED_MODULE_1__/* .hardenGitEnv */ .Si)() });
79
88
  } catch { return []; }
80
89
  const parts = out.split(/^commit ([0-9a-f]{7,40})/m); // [pre, sha, body, sha, body, ...]
81
90
  const findings = [];
package/dist/449.index.js CHANGED
@@ -14,6 +14,7 @@ export const modules = {
14
14
  /* harmony export */ });
15
15
  /* unused harmony export findingsExceedingSLA */
16
16
  /* harmony import */ var node_crypto__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(7598);
17
+ /* harmony import */ var _provenance_schema_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4594);
17
18
  // 0.8.0 Feat-11: MTTR / finding-age tracking — per-finding firstSeenAt/lastSeenAt with SLA breach detection.
18
19
  //
19
20
  // Stamps every finding with `firstSeenAt` (preserved from the baseline if the
@@ -25,6 +26,29 @@ export const modules = {
25
26
 
26
27
 
27
28
 
29
+
30
+ // FR-PROV-019: "reports never show an age without its basis and confidence."
31
+ // FINDING_ORIGIN is the only basis backed by a resolved git commit for the
32
+ // finding's actual introduction; EARLIEST_OBSERVABLE is also git-derived but
33
+ // partial (weaker claim, no exact introduction commit). UNCOMMITTED and
34
+ // FIRST_OBSERVED are both wall-clock fallbacks — the age is a first-seen
35
+ // timestamp, never resolved against git history — and must say so honestly
36
+ // rather than read like a proven date.
37
+ function _ageBasisLabel(ageBasis, confidence) {
38
+ const level = confidence?.level && confidence.level !== 'unknown' ? confidence.level.toUpperCase() : null;
39
+ switch (ageBasis) {
40
+ case _provenance_schema_js__WEBPACK_IMPORTED_MODULE_1__/* .AGE_BASIS */ .pI.FINDING_ORIGIN:
41
+ return `proven origin${level ? `, ${level} confidence` : ''}`;
42
+ case _provenance_schema_js__WEBPACK_IMPORTED_MODULE_1__/* .AGE_BASIS */ .pI.EARLIEST_OBSERVABLE:
43
+ return `earliest observable commit, partial history${level ? `, ${level} confidence` : ''}`;
44
+ case _provenance_schema_js__WEBPACK_IMPORTED_MODULE_1__/* .AGE_BASIS */ .pI.UNCOMMITTED:
45
+ return 'uncommitted — first-seen fallback, origin not proven';
46
+ case _provenance_schema_js__WEBPACK_IMPORTED_MODULE_1__/* .AGE_BASIS */ .pI.FIRST_OBSERVED:
47
+ default:
48
+ return 'first-seen fallback — origin not proven';
49
+ }
50
+ }
51
+
28
52
  // Stable fingerprint for cross-scan finding identity. Mirrors the dedupe key.
29
53
  // Exported so a caller can compute the "removed since baseline" (i.e. fixed)
30
54
  // set that computeMTTR needs, using the exact same identity function
@@ -53,6 +77,25 @@ function stampFindingTimestamps(findings, baselineMap = new Map(), now = Date.no
53
77
  f.lastSeenAt = nowIso;
54
78
  const firstMs = Date.parse(f.firstSeenAt);
55
79
  f.ageDays = Math.max(0, Math.floor((now - firstMs) / 86400000));
80
+ // FR-PROV-019: age/SLA basis. ageDays above stays pure wall-clock —
81
+ // every existing SLA/computeMTTR consumer keeps its current meaning.
82
+ // ageBasis + provenAgeDays are ADDITIVE: a report can show both and
83
+ // explain the discrepancy, never silently swap which number "age" means.
84
+ const status = f.findingProvenance?.status;
85
+ const origin = f.findingProvenance?.findingOrigin;
86
+ if (status === 'complete' && origin?.authorDate) {
87
+ f.ageBasis = _provenance_schema_js__WEBPACK_IMPORTED_MODULE_1__/* .AGE_BASIS */ .pI.FINDING_ORIGIN;
88
+ f.provenAgeDays = Math.max(0, Math.floor((now - Date.parse(origin.authorDate)) / 86400000));
89
+ } else if (status === 'partial' && origin?.authorDate) {
90
+ f.ageBasis = _provenance_schema_js__WEBPACK_IMPORTED_MODULE_1__/* .AGE_BASIS */ .pI.EARLIEST_OBSERVABLE;
91
+ f.provenAgeDays = Math.max(0, Math.floor((now - Date.parse(origin.authorDate)) / 86400000));
92
+ } else if (status === 'uncommitted') {
93
+ f.ageBasis = _provenance_schema_js__WEBPACK_IMPORTED_MODULE_1__/* .AGE_BASIS */ .pI.UNCOMMITTED;
94
+ f.provenAgeDays = f.ageDays;
95
+ } else {
96
+ f.ageBasis = _provenance_schema_js__WEBPACK_IMPORTED_MODULE_1__/* .AGE_BASIS */ .pI.FIRST_OBSERVED;
97
+ f.provenAgeDays = f.ageDays;
98
+ }
56
99
  }
57
100
  return findings;
58
101
  }
@@ -84,28 +127,49 @@ function findingsExceedingSLA(findings, slaDays = null) {
84
127
  });
85
128
  }
86
129
 
87
- // Median age (days) of the currently-open findings a single-scan proxy for
88
- // "how long has this debt been sitting". True MTTR (computeMTTR) needs the set
89
- // of findings that were FIXED; this reports the open backlog's median age so a
90
- // scan can show whether debt is getting older. Returns null on empty input.
91
- // Local surfaced only through renderSlaSummary (its sole consumer).
92
- function medianOpenAgeDays(findings) {
93
- const ages = (findings || []).map(f => f.ageDays || 0).sort((a, b) => a - b);
94
- if (!ages.length) return null;
95
- return ages[Math.floor(ages.length / 2)];
130
+ // Median age (days) of the currently-open findings, PLUS the ageBasis/
131
+ // confidence of whichever finding landed on that median a single-scan proxy
132
+ // for "how long has this debt been sitting". True MTTR (computeMTTR) needs the
133
+ // set of findings that were FIXED; this reports the open backlog's median age
134
+ // so a scan can show whether debt is getting older. Returns null on empty
135
+ // input. Local — surfaced only through renderSlaSummary (its sole consumer).
136
+ //
137
+ // FR-PROV-019: the days figure prefers `provenAgeDays` (git-derived when
138
+ // available) over the pure-wall-clock `ageDays`, and the ageBasis/confidence
139
+ // travel WITH the day count they describe — never a bare number with the
140
+ // basis looked up separately, which is how the original miss happened
141
+ // (ageBasis was stamped onto the finding but never reached the string that
142
+ // printed its age). Findings stamped by an older/test caller that never ran
143
+ // through stampFindingTimestamps (no ageBasis at all) degrade to the same
144
+ // honest "not proven" label FIRST_OBSERVED gets, never a false claim.
145
+ function medianOpenAge(findings) {
146
+ const entries = (findings || [])
147
+ .map(f => ({
148
+ days: f.provenAgeDays != null ? f.provenAgeDays : (f.ageDays || 0),
149
+ ageBasis: f.ageBasis || _provenance_schema_js__WEBPACK_IMPORTED_MODULE_1__/* .AGE_BASIS */ .pI.FIRST_OBSERVED,
150
+ confidence: f.findingProvenance?.confidence || null,
151
+ }))
152
+ .sort((a, b) => a.days - b.days);
153
+ if (!entries.length) return null;
154
+ return entries[Math.floor(entries.length / 2)];
96
155
  }
97
156
 
98
157
  // One-line SLA-breach summary for surfacing after a scan (#10). Returns null
99
- // when nothing is past its per-severity SLA. Pairs with medianOpenAgeDays for a
158
+ // when nothing is past its per-severity SLA. Pairs with medianOpenAge for a
100
159
  // "is my security debt aging" readout that the vibecoder can act on.
160
+ //
161
+ // FR-PROV-019: never prints the median age number without its basis and
162
+ // confidence alongside it — see medianOpenAge/_ageBasisLabel above.
101
163
  function renderSlaSummary(findings, slaDays = null) {
102
164
  const breached = findingsExceedingSLA(findings || [], slaDays);
103
165
  if (!breached.length) return null;
104
166
  const bySev = {};
105
167
  for (const f of breached) bySev[f.severity] = (bySev[f.severity] || 0) + 1;
106
168
  const parts = ['critical', 'high', 'medium', 'low', 'info'].filter(s => bySev[s]).map(s => `${bySev[s]} ${s}`);
107
- const median = medianOpenAgeDays(findings);
108
- const ageNote = median != null ? ` (median open age ${median}d)` : '';
169
+ const median = medianOpenAge(findings);
170
+ const ageNote = median != null
171
+ ? ` (median open age ${median.days}d, ${_ageBasisLabel(median.ageBasis, median.confidence)})`
172
+ : '';
109
173
  return `${breached.length} finding(s) past remediation SLA: ${parts.join(', ')}${ageNote}`;
110
174
  }
111
175