@clear-capabilities/agentic-security-scanner 0.144.0 → 0.147.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 (209) hide show
  1. package/CHANGELOG.md +390 -0
  2. package/bin/agentic-security.js +3813 -83
  3. package/dist/1122.index.js +702 -0
  4. package/dist/{301.index.js → 1301.index.js} +2 -2
  5. package/dist/1379.index.js +591 -0
  6. package/dist/{444.index.js → 1444.index.js} +13 -4
  7. package/dist/{660.index.js → 1660.index.js} +2 -2
  8. package/dist/{700.index.js → 1700.index.js} +2 -2
  9. package/dist/{905.index.js → 1905.index.js} +3 -3
  10. package/dist/{920.index.js → 1920.index.js} +3 -3
  11. package/dist/{238.index.js → 2238.index.js} +3 -3
  12. package/dist/2271.index.js +165 -0
  13. package/dist/{985.index.js → 2376.index.js} +1260 -340
  14. package/dist/2432.index.js +793 -0
  15. package/dist/2659.index.js +93 -0
  16. package/dist/{826.index.js → 2826.index.js} +2 -2
  17. package/dist/{830.index.js → 2830.index.js} +2 -2
  18. package/dist/2923.index.js +298 -0
  19. package/dist/{1.index.js → 3001.index.js} +5 -5
  20. package/dist/{117.index.js → 3117.index.js} +3 -3
  21. package/dist/3180.index.js +307 -0
  22. package/dist/3276.index.js +117 -0
  23. package/dist/{415.index.js → 3415.index.js} +2 -2
  24. package/dist/{499.index.js → 3499.index.js} +2 -2
  25. package/dist/3518.index.js +450 -0
  26. package/dist/{526.index.js → 3526.index.js} +14 -6
  27. package/dist/{736.index.js → 3736.index.js} +4 -4
  28. package/dist/{839.index.js → 3839.index.js} +4 -4
  29. package/dist/{113.index.js → 4113.index.js} +14 -6
  30. package/dist/{265.index.js → 4265.index.js} +2 -2
  31. package/dist/{384.index.js → 4384.index.js} +3 -3
  32. package/dist/4547.index.js +268 -0
  33. package/dist/4863.index.js +422 -0
  34. package/dist/{970.index.js → 4970.index.js} +67 -3
  35. package/dist/5051.index.js +770 -0
  36. package/dist/{144.index.js → 5144.index.js} +5 -5
  37. package/dist/{333.index.js → 5333.index.js} +3 -3
  38. package/dist/5343.index.js +185 -0
  39. package/dist/5350.index.js +866 -0
  40. package/dist/5561.index.js +436 -0
  41. package/dist/{637.index.js → 5637.index.js} +29 -7
  42. package/dist/{449.index.js → 5830.index.js} +78 -14
  43. package/dist/6626.index.js +532 -0
  44. package/dist/6662.index.js +297 -0
  45. package/dist/{675.index.js → 6675.index.js} +5 -5
  46. package/dist/{730.index.js → 6730.index.js} +6 -6
  47. package/dist/6829.index.js +225 -0
  48. package/dist/6944.index.js +130 -0
  49. package/dist/{178.index.js → 7178.index.js} +26 -8
  50. package/dist/{227.index.js → 7227.index.js} +2 -2
  51. package/dist/7310.index.js +520 -0
  52. package/dist/{552.index.js → 7552.index.js} +4 -4
  53. package/dist/7709.index.js +78 -0
  54. package/dist/8218.index.js +160 -0
  55. package/dist/{476.index.js → 8476.index.js} +4 -4
  56. package/dist/{513.index.js → 8513.index.js} +5 -5
  57. package/dist/{520.index.js → 8520.index.js} +2 -2
  58. package/dist/{718.index.js → 8718.index.js} +2 -2
  59. package/dist/{752.index.js → 8752.index.js} +2 -2
  60. package/dist/8846.index.js +100 -0
  61. package/dist/{435.index.js → 9091.index.js} +580 -187
  62. package/dist/{207.index.js → 9207.index.js} +2 -2
  63. package/dist/{220.index.js → 9220.index.js} +2 -2
  64. package/dist/9390.index.js +163 -0
  65. package/dist/{503.index.js → 9503.index.js} +2 -2
  66. package/dist/{801.index.js → 9801.index.js} +2 -2
  67. package/dist/{824.index.js → 9824.index.js} +2 -2
  68. package/dist/agentic-security.mjs +16 -16
  69. package/dist/agentic-security.mjs.sha256 +1 -1
  70. package/dist/compliance-frameworks/hipaa-security-rule.json +3 -2
  71. package/package.json +23 -10
  72. package/src/compare.js +6 -1
  73. package/src/dataflow/CLAUDE.md +2 -2
  74. package/src/dataflow/catalog.js +42 -0
  75. package/src/dataflow/orm-write-catalog.js +175 -0
  76. package/src/engine.js +580 -30
  77. package/src/fix/apply-fix-service.js +1 -0
  78. package/src/history-scan.js +22 -5
  79. package/src/ir/CLAUDE.md +2 -1
  80. package/src/ir/chrome-probe.mjs +150 -0
  81. package/src/ir/parser-js.js +94 -7
  82. package/src/lineage/CLAUDE.md +1203 -0
  83. package/src/lineage/DESIGN_DESTINATION_RESOLVER.md +156 -0
  84. package/src/lineage/DESIGN_GRAPH_BUILDER.md +938 -0
  85. package/src/lineage/DESIGN_HANDLING_ANALYZER.md +355 -0
  86. package/src/lineage/DESIGN_INTRAPROCEDURAL.md +628 -0
  87. package/src/lineage/DESIGN_PATH_PROVENANCE.md +3451 -0
  88. package/src/lineage/DESIGN_QUEUE_DETAIL.md +120 -0
  89. package/src/lineage/DESIGN_REGISTRIES.md +880 -0
  90. package/src/lineage/DESIGN_STORE_DETAIL.md +143 -0
  91. package/src/lineage/DESIGN_TRANSIT_PROTECTION.md +245 -0
  92. package/src/lineage/classification.js +56 -0
  93. package/src/lineage/coverage.js +658 -0
  94. package/src/lineage/cross-repo-link.js +107 -0
  95. package/src/lineage/dataflow-graph.schema.json +184 -0
  96. package/src/lineage/decision-story.js +206 -0
  97. package/src/lineage/drift-policy.js +279 -0
  98. package/src/lineage/driver.js +135 -0
  99. package/src/lineage/engine.js +992 -0
  100. package/src/lineage/export-briefing.js +628 -0
  101. package/src/lineage/export-csv.js +62 -0
  102. package/src/lineage/export-json.js +238 -0
  103. package/src/lineage/export-privacy.js +258 -0
  104. package/src/lineage/federation-loader.js +111 -0
  105. package/src/lineage/field-identity.js +78 -0
  106. package/src/lineage/fixtures/build-flagship-fixture.mjs +272 -0
  107. package/src/lineage/fixtures/flagship-graph.json +1453 -0
  108. package/src/lineage/flow-grade.js +221 -0
  109. package/src/lineage/governance-edit.js +169 -0
  110. package/src/lineage/graph-builder.js +1114 -0
  111. package/src/lineage/graph-diff.js +431 -0
  112. package/src/lineage/graph-snapshot.js +180 -0
  113. package/src/lineage/handling-analyzer.js +168 -0
  114. package/src/lineage/ids.js +349 -0
  115. package/src/lineage/impact-assessment.js +76 -0
  116. package/src/lineage/impact-engine.js +268 -0
  117. package/src/lineage/index.js +281 -0
  118. package/src/lineage/language-coverage-tiers.js +58 -0
  119. package/src/lineage/obligation-mapping.js +126 -0
  120. package/src/lineage/obligation-predicates.js +235 -0
  121. package/src/lineage/observation-adapters.js +282 -0
  122. package/src/lineage/observation-correlation.js +622 -0
  123. package/src/lineage/observation-store.js +497 -0
  124. package/src/lineage/path-query.js +410 -0
  125. package/src/lineage/path-store.js +400 -0
  126. package/src/lineage/protection.js +53 -0
  127. package/src/lineage/recipient-profile.js +192 -0
  128. package/src/lineage/recipient-registry.js +394 -0
  129. package/src/lineage/redact-graph.js +224 -0
  130. package/src/lineage/remediation.js +417 -0
  131. package/src/lineage/resolve-destination.js +91 -0
  132. package/src/lineage/runtime-observation.js +464 -0
  133. package/src/lineage/scenario-diff.js +84 -0
  134. package/src/lineage/scenario-engine.js +251 -0
  135. package/src/lineage/scenario.js +101 -0
  136. package/src/lineage/schema.js +167 -0
  137. package/src/lineage/sink-registry.js +427 -0
  138. package/src/lineage/source-registry.js +357 -0
  139. package/src/lineage/source-seeding.js +212 -0
  140. package/src/lineage/summaries.js +590 -0
  141. package/src/lineage/transform-catalog.js +397 -0
  142. package/src/lineage/transit-protection.js +150 -0
  143. package/src/lineage/validate.js +285 -0
  144. package/src/lsp/server.js +49 -2
  145. package/src/mcp/CLAUDE.md +7 -1
  146. package/src/mcp/dataflow-tools.js +160 -0
  147. package/src/mcp/server.js +1 -1
  148. package/src/mcp/tools.js +22 -1
  149. package/src/pipeline/assurance-mode.js +64 -1
  150. package/src/pipeline/finding-schema.js +8 -1
  151. package/src/pipeline/scan-health.js +19 -1
  152. package/src/posture/CLAUDE.md +140 -0
  153. package/src/posture/accuracy-scorecard.js +60 -0
  154. package/src/posture/artifact-registry.js +76 -0
  155. package/src/posture/auditor-walkthrough.js +192 -13
  156. package/src/posture/compliance-frameworks/hipaa-security-rule.json +3 -2
  157. package/src/posture/compliance-policy.js +12 -2
  158. package/src/posture/cross-repo-memory.js +7 -2
  159. package/src/posture/fix-history.js +25 -2
  160. package/src/posture/fix-verify.js +9 -1
  161. package/src/posture/fleet.js +0 -0
  162. package/src/posture/git-history.js +13 -5
  163. package/src/posture/material-change.js +21 -2
  164. package/src/posture/mttr.js +75 -12
  165. package/src/posture/obligation-evidence-pack.js +202 -0
  166. package/src/posture/pre-incident-archaeology.js +39 -7
  167. package/src/posture/privacy-framework.js +14 -0
  168. package/src/posture/provenance/ai-authorship.js +68 -0
  169. package/src/posture/provenance/branch-entry.js +80 -0
  170. package/src/posture/provenance/cache.js +143 -0
  171. package/src/posture/provenance/confidence.js +36 -0
  172. package/src/posture/provenance/coordinator.js +786 -0
  173. package/src/posture/provenance/dag-walk.js +249 -0
  174. package/src/posture/provenance/evidence-attribution.js +59 -0
  175. package/src/posture/provenance/git-evidence.js +310 -0
  176. package/src/posture/provenance/lifecycle.js +208 -0
  177. package/src/posture/provenance/missing-control-resolver.js +137 -0
  178. package/src/posture/provenance/origin-resolver.js +342 -0
  179. package/src/posture/provenance/predicate-replay.js +133 -0
  180. package/src/posture/provenance/providers/config.js +39 -0
  181. package/src/posture/provenance/providers/github.js +62 -0
  182. package/src/posture/provenance/providers/gitlab.js +58 -0
  183. package/src/posture/provenance/repo-lineage.js +74 -0
  184. package/src/posture/provenance/sca-origin.js +139 -0
  185. package/src/posture/provenance/schema.js +255 -0
  186. package/src/posture/provenance/transitive-sca.js +147 -0
  187. package/src/posture/provenance/validate.js +30 -0
  188. package/src/posture/provenance-evidence-bundle.js +144 -0
  189. package/src/posture/remediation-ledger.js +337 -0
  190. package/src/posture/sbom-diff.js +15 -2
  191. package/src/posture/secret-history.js +10 -2
  192. package/src/posture/state-dir.js +38 -14
  193. package/src/posture/vuln-archaeology.js +8 -2
  194. package/src/pr-delta.js +25 -4
  195. package/src/report/index.js +197 -3
  196. package/src/runScan.js +34 -5
  197. package/src/sast/rate-limit.js +33 -3
  198. package/src/server/CLAUDE.md +47 -0
  199. package/src/server/graph-loader.js +141 -0
  200. package/src/server/http-server.js +325 -0
  201. package/src/server/routes.js +129 -0
  202. package/src/server/security.js +111 -0
  203. package/src/server/static-assets.js +139 -0
  204. package/src/util/git-hardening.js +128 -0
  205. package/dist/11.index.js +0 -353
  206. package/dist/259.index.js +0 -975
  207. package/dist/317.index.js +0 -300
  208. package/dist/609.index.js +0 -741
  209. package/dist/838.index.js +0 -152
@@ -42,6 +42,8 @@
42
42
  // mode to behave differently from advisory, that is a deliberate, separate
43
43
  // decision -- not something to guess at here.
44
44
 
45
+ import { isProvenanceHealthy } from '../posture/provenance/schema.js';
46
+
45
47
  export const ASSURANCE_MODES = Object.freeze(['advisory', 'standard', 'strict']);
46
48
  export const DEFAULT_ASSURANCE_MODE = 'standard';
47
49
 
@@ -56,7 +58,7 @@ function _isValidMode(mode) {
56
58
  * ok:false only ever happens in strict mode; advisory/standard always ok:true
57
59
  * (they report, they do not gate).
58
60
  */
59
- export function evaluateAssuranceMode(mode, scanHealth) {
61
+ export function evaluateAssuranceMode(mode, scanHealth, findings = []) {
60
62
  const effectiveMode = _isValidMode(mode) ? mode : DEFAULT_ASSURANCE_MODE;
61
63
  const conditions = Array.isArray(scanHealth?.conditions) ? scanHealth.conditions : [];
62
64
 
@@ -85,6 +87,67 @@ export function evaluateAssuranceMode(mode, scanHealth) {
85
87
  conditions,
86
88
  };
87
89
  }
90
+
91
+ // M2 §2.5: strict cares about overall scan completeness, which now
92
+ // explicitly includes PROVENANCE completeness, not just detector/analyzer
93
+ // completeness. A finding whose findingProvenance status is outside
94
+ // ['complete','uncommitted'] — including a finding with NO
95
+ // findingProvenance at all, e.g. --no-provenance was used — means strict
96
+ // cannot vouch for this scan's provenance the same way it already refuses
97
+ // to vouch for a scan with a failed analyzer.
98
+ //
99
+ // KNOWN INTERACTION: scan.secrets/scan.logicVulns are unconditionally
100
+ // stamped not_available today (M0+M1 deliberately deferred real origin
101
+ // resolution for those two channels — see the M2/M3/M4 design spec's
102
+ // §2.6). Any real secret or logic finding therefore fails strict mode
103
+ // until that resolution work lands. This is the literal, intended
104
+ // consequence of "never false certainty" applied to strict's own
105
+ // definition, not an oversight — a strict-mode operator with secrets
106
+ // findings should expect this until M3+ closes that gap.
107
+ //
108
+ // This list is INCOMPLETE without scan.supplyChain, and the omission
109
+ // matters more than the secrets/logic one above because it hits nearly
110
+ // every real project. engine.js stamps every supplyChain entry
111
+ // not_available too (see the loop over `supplyChain` right after the
112
+ // `annotateGitProvenance` calls), and that bucket covers three distinct
113
+ // populations, not one:
114
+ //
115
+ // - transitive `vulnerable_dep` findings: a genuine, if currently
116
+ // unresolved, DEFERRAL — same shape as secrets/logicVulns above. The
117
+ // vulnerable version was never declared in this repo's own manifests,
118
+ // so there is no local commit to walk yet, but one could exist to
119
+ // resolve in a later phase.
120
+ // - `unpinned_dep` / `no_lockfile` findings: a CATEGORY ERROR, not a
121
+ // deferral. These describe an ABSENT state (a version range with no
122
+ // pin, a manifest with no lockfile) — there is no "commit that
123
+ // introduced a missing lockfile" for any future resolver to find,
124
+ // because the finding is about the absence of an event, not an event
125
+ // itself. No amount of future engineering work makes these resolvable.
126
+ //
127
+ // Direct `vulnerable_dep` findings DO go through real origin resolution
128
+ // (`resolveDirectSCAOrigin`, gated on `isDirect`) and are not part of this
129
+ // limitation.
130
+ //
131
+ // Net effect: because `unpinned_dep`/`no_lockfile` findings are a category
132
+ // error rather than a deferral, `--assurance strict` will fail on nearly
133
+ // any real project that has a `package.json` (or equivalent manifest)
134
+ // today — an unpinned or unlocked dependency is common, and this check has
135
+ // no way to ever resolve one. This is a known, disclosed limitation of the
136
+ // current implementation, not a bug, and it is not something this check
137
+ // should route around: exempting these finding types from the strict-mode
138
+ // gate was considered and deliberately deferred to a future milestone
139
+ // rather than done here, so strict mode keeps refusing to vouch for
140
+ // provenance it cannot actually speak to.
141
+ const badProvenance = (Array.isArray(findings) ? findings : []).filter((f) => !isProvenanceHealthy(f?.findingProvenance));
142
+ if (badProvenance.length > 0) {
143
+ return {
144
+ ok: false,
145
+ mode: 'strict',
146
+ reason: `strict mode requires complete finding provenance; ${badProvenance.length} finding(s) have status outside [complete, uncommitted]`,
147
+ conditions,
148
+ };
149
+ }
150
+
88
151
  return { ok: true, mode: 'strict', reason: null, conditions };
89
152
  }
90
153
 
@@ -46,7 +46,14 @@ export const FINDING_SCHEMA_VERSION = 1;
46
46
  // sets to a real (non-null) value today; `optional` fields are legitimately
47
47
  // null on many findings (e.g. a finding no annotator has enriched yet).
48
48
  export const FINDING_FIELD_GROUPS = {
49
- identity: { required: ['id', 'kind', 'vuln'], optional: ['stableId'] },
49
+ // `findingProvenance` is REQUIRED, not optional, and that is deliberate:
50
+ // posture/provenance/coordinator.js guarantees every finding it sees leaves
51
+ // with a TERMINAL provenance object, expressing every failure mode as a
52
+ // status ('not_available', 'uncommitted', 'budget_exhausted', 'error')
53
+ // rather than as an absent field. So a missing findingProvenance never means
54
+ // "provenance didn't apply here" — it means the finding escaped annotation
55
+ // entirely, which is exactly the condition this group exists to surface.
56
+ identity: { required: ['id', 'kind', 'vuln', 'findingProvenance'], optional: ['stableId'] },
50
57
  location: { required: ['file', 'line'], optional: ['snippet'] },
51
58
  classification: { required: ['severity'], optional: ['cwe', 'owaspLlm', 'family', 'parser', 'tags', 'description'] },
52
59
  confidence: { required: [], optional: ['confidence', 'confidenceTier', 'calibrated_confidence', 'calibration_reason'] },
@@ -33,6 +33,7 @@ export const SCAN_HEALTH_SCHEMA_VERSION = 1;
33
33
  * @param {Array<{phase:string, err:string}>} input.annotatorErrors
34
34
  * @param {object} [input.engineErrors] - e.g. { cppDataflowParseErrors }
35
35
  * @param {object} [input.deepStatus] - { requested, enabled, inCi, ciOverrideAllowed, reason, failure }
36
+ * @param {object} [input.lineageStatus] - { requested, enabled, reason, failure } — Sub-project E, increment 5's own status, kept separate from deepStatus (IR-taint's own).
36
37
  * @param {object} [input.analyzerCoverage] - coverage-ledger.js's
37
38
  * summarizeCoverageForScanHealth() output: {expected, completed, failed,
38
39
  * timedOut, skippedByPolicy}. Omitted (not just empty) is treated the
@@ -41,7 +42,7 @@ export const SCAN_HEALTH_SCHEMA_VERSION = 1;
41
42
  * never a fabricated all-zero summary.
42
43
  * @returns {object} scanHealth per PRD §10.3, additive fields only.
43
44
  */
44
- export function computeScanHealth({ scanMeta = null, annotatorErrors = [], engineErrors = null, deepStatus = null, analyzerCoverage = null } = {}) {
45
+ export function computeScanHealth({ scanMeta = null, annotatorErrors = [], engineErrors = null, deepStatus = null, analyzerCoverage = null, lineageStatus = null } = {}) {
45
46
  const conditions = [];
46
47
  const safeAnnotatorErrors = Array.isArray(annotatorErrors) ? annotatorErrors : [];
47
48
  const filesTimedOut = Number(scanMeta?.filesTimedOut) || 0;
@@ -63,6 +64,15 @@ export function computeScanHealth({ scanMeta = null, annotatorErrors = [], engin
63
64
  if (deepStatus?.requested && !deepStatus.enabled) {
64
65
  conditions.push(`deep analysis was requested but did not run: ${deepStatus.reason || 'unknown reason'}`);
65
66
  }
67
+ // Sub-project E, increment 5: lineage graph build status — a SEPARATE
68
+ // condition from deepStatus above, never folded into it. deepStatus is
69
+ // specifically IR-taint's own status; conflating the two would make
70
+ // `scanHealth.deepAnalysis.failure` ambiguous about which subsystem
71
+ // actually failed (the same "distinguish A from B" discipline this
72
+ // module already applies to analyzerCoverage vs. annotatorErrors).
73
+ if (lineageStatus?.failure) {
74
+ conditions.push(`lineage graph build threw and was skipped: ${lineageStatus.failure}`);
75
+ }
66
76
  // FR-203: a detector that threw on at least one file (captured via
67
77
  // FR-201's runDetector isolation) is a real analysis gap distinct from
68
78
  // an ANNOTATOR error above — annotators run post-detection over the
@@ -96,6 +106,14 @@ export function computeScanHealth({ scanMeta = null, annotatorErrors = [], engin
96
106
  failure: deepStatus.failure ?? null,
97
107
  }
98
108
  : null,
109
+ lineageAnalysis: lineageStatus
110
+ ? {
111
+ requested: !!lineageStatus.requested,
112
+ enabled: !!lineageStatus.enabled,
113
+ reason: lineageStatus.reason ?? null,
114
+ failure: lineageStatus.failure ?? null,
115
+ }
116
+ : null,
99
117
  annotatorErrorCount: safeAnnotatorErrors.length,
100
118
  freshness: null,
101
119
  conditions,
@@ -42,6 +42,23 @@ actually took — and are flagged `reliable:false` below n=10 rather than hidden
42
42
  or quoted as settled. Recording goes through `isSafeStateDir`, so it declines
43
43
  rather than creating a stray state dir outside a project.
44
44
 
45
+ **Remediation ledger (M5 deliverable #6)** — `remediation-ledger.js`. The
46
+ I/O half of Blast-Radius: Remediation Command Center (FR-507 + AC-31) —
47
+ locking, JSONL append, tolerant read, and a hash chain over
48
+ `.agentic-security/remediation/items.jsonl`/`items.lock`, matching
49
+ `fix-metrics.js`'s own append-and-tolerant-read shape above (one
50
+ newline-terminated record per `appendFileSync`, a torn tail dropped on
51
+ read, never a whole-file rewrite). The pure state machine it writes
52
+ through — `foldRemediationItem`/`foldRemediationLedger`/
53
+ `validateTransition` — lives at `../lineage/remediation.js`, making this
54
+ the SECOND `posture/` → `lineage/` import in the codebase
55
+ (`auditor-walkthrough.js`'s `graph:` branch was the first — see "First
56
+ `posture/` → `lineage/` import" below). `appendLedgerEvent` is the single
57
+ place `validateTransition` is enforced; no CLI command computes validity
58
+ for itself. Its `withLock` is a faithful local PORT of
59
+ `provenance/lifecycle.js`'s own `withLock` — not an import, since that
60
+ function is not exported.
61
+
45
62
  **Agentic verification** — `verifier.js`, `verifier-target.js`, `verifier-ephemeral.js`, `harness-discovery.js`, `adversary-agent.js`, `defender-agent.js`, `auditor-agent.js`, `three-agent-pipeline.js`.
46
63
 
47
64
  **Methodology additions (Agentic Methodology PRD, removed post-implementation)** — default-on annotators/artifacts that layer the agentic-hunter methodology on the deterministic engine:
@@ -83,6 +100,8 @@ Wired in `bin/agentic-security.js` after every filter and after `makeDeterminist
83
100
 
84
101
  **Rule lifecycle** — `custom-rules.js` (YAML pattern DSL), `rule-overrides.js` (`disable:` gated on signature), `rule-packs.js`, `rule-synthesis.js` (proposes suppressions from triage feedback), `ruleset-version.js`.
85
102
 
103
+ **First `posture/` → `lineage/` import.** `auditor-walkthrough.js`'s `graph:` mapping branch (Data Flow Explorer M4 sub-project 6b) imports `evaluateGraphFlowPredicate`/`buildObligationMappingFromGraphPredicate` from `../lineage/obligation-predicates.js` — the first time any module in this directory has reached into `lineage/` (previously a one-way boundary: `lineage/` never imports `posture/`, and nothing here imported it back). It mints a real `ObligationMapping` record (see `scanner/src/lineage/CLAUDE.md`) from `scan.lineageGraph` when present, purely additively — never touching the pre-existing `anySignal`/`allCleared`/`anyCleared`/`hasUnverifiableMapping` status machinery.
104
+
86
105
  **NIST Privacy Framework 1.1 (`privacy-framework.js`)** — assessment + remediation
87
106
  over the bundled `compliance-frameworks/nist-privacy-1-1.json` (all 104 controls).
88
107
  Sits on top of `auditor-walkthrough.js`'s evaluator and adds the half a narrative
@@ -351,6 +370,127 @@ Driver: `scripts/comparison.mjs`, over the CVE-replay corpus.
351
370
 
352
371
  **State artifact registry (assurance-hardening PRD FR-701/FR-703)** — `artifact-registry.js`. The registry `cmdReset` (bin/agentic-security.js) now iterates instead of two hardcoded WIPE/WIPE_DIRS Sets. Every known `.agentic-security/` artifact is classified `generated` (scanner-written, safe to delete on reset) or `operator-config` (hand- or agent-authored input, never deleted) — built from an audit of every `statePath()`/`stateDir()` call site, not guessed from filenames; several looked generated by name but turned out to be inputs (`.agentic-security/logic-claims.json`, `.agentic-security/exploit-history.jsonl`, `.agentic-security/cve-alerts.json`, `.agentic-security/network-policy.json`, `.agentic-security/current-intent.md` — see the module's own header for the evidence behind each). Guarded by a completeness test (`test/artifact-registry-completeness.test.js`) that scans for every `statePath()`/`stateDir()` literal and fails if one isn't registered — a `no-dead-modules.test.js`-style drift guard, not a snapshot.
353
372
 
373
+ ## Finding provenance — `provenance/` (20 modules)
374
+
375
+ The only SUBDIRECTORY under `posture/`, because it is a pipeline rather than an
376
+ annotator: twenty small modules that together answer "which commit introduced
377
+ this finding, and how sure are we?" Everything outside the subdirectory sees one
378
+ function, `annotateGitProvenance(findings, ctx)` from `coordinator.js`, wired in
379
+ `engine.js` after every finding has been appended.
380
+
381
+ **Read the naming rule before you touch anything here.** The exported function is
382
+ `annotateGitProvenance` — NOT `annotateProvenance` (taken by
383
+ `sca/sigstore-verify.js`, build attestations) and NOT `annotateFindingProvenance`
384
+ (taken by `posture/provenance.js`, parser-corroboration signals). `engine.js`
385
+ imports all three; either alternative name is a duplicate binding, and the second
386
+ takes a findings array as its first argument exactly like this one, so a wrong
387
+ import would RUN rather than fail. The field is `finding.findingProvenance`,
388
+ never bare `.provenance` — `finding.provenance` and `supplyChainEntry.provenance`
389
+ are both pre-existing unrelated fields.
390
+
391
+ **The pipeline**, in call order — all LIVE-WIRED into `engine.js`'s scan unless noted:
392
+
393
+ | Module | Answers |
394
+ |---|---|
395
+ | `coordinator.js` | the integration point — budget, cache, per-finding dispatch, the terminal-status guarantee |
396
+ | `git-evidence.js` | the only Git wrapper (`getRepoState`, `blameLine`, `candidateCommitsForLine`, `getBlobAtCommit`, `commitMeta`) |
397
+ | `origin-resolver.js` | which commit introduced a SAST finding |
398
+ | `dag-walk.js` | (M3 §3.1) non-first-parent DAG walk + revert/cherry-pick detection for `--provenance deep` |
399
+ | `predicate-replay.js` | was this finding's condition true at commit X (calls `runFullScan` on that commit's blobs) |
400
+ | `sca-origin.js` | which commit moved a directly-declared dependency version into an advisory's vulnerable range |
401
+ | `transitive-sca.js` | (M3 §3.2) the same question for a TRANSITIVE dependency, re-deriving lockfile ancestry per historical commit |
402
+ | `branch-entry.js` | which branch/PR merge brought the origin commit into the current branch |
403
+ | `evidence-attribution.js` | the path:line:commit triples for source / sink / manifest |
404
+ | `confidence.js` | HIGH / MEDIUM / LOW plus the reasons behind it |
405
+ | `lifecycle.js` | the introduce / remediate / reintroduce ledger |
406
+ | `cache.js` | per-(HEAD, stableId, ruleset, boundary, mode) memo under its own top-level `.agentic-security/provenance-cache/` (split out from `provenance/` so it can carry a `'cache'` retentionClass the permanent lifecycle ledger must not get — see artifact-registry.js) |
407
+ | `schema.js` | the status/method/role/confidence enums, `emptyProvenance`, `redactFindingProvenance`, `isProvenanceHealthy` |
408
+ | `validate.js` | shape assertion for tests |
409
+ | `missing-control-resolver.js` | (M3 §3.3, FR-PROV-017) when a previously-observed safeguard disappeared — **wired into `coordinator.js`**: `resolveMissingControlOrigin` calls `resolveMissingControl` for any finding with `missingControlCandidate:true` (today, `sast/rate-limit.js`'s findings) |
410
+ | `providers/config.js`, `providers/github.js`, `providers/gitlab.js` | (M3 §3.4, FR-PROV-022) GitHub/GitLab PR-metadata + CODEOWNERS fetch, config resolved from `.agentic-security/provenance-providers.yml` / token env vars — **wired into `coordinator.js`**: `resolveProviderConfig` is resolved once per scan in `annotateGitProvenance`, and `fetchPRMetadata`/`fetchCodeowners` are called per `complete`-status finding (capped, see `MAX_PROVIDER_ENRICHMENTS_PER_SCAN`), landing on `findingProvenance.providerEnrichment` |
411
+ | `repo-lineage.js` | (M4 §4.2) loads + fully verifies an operator-declared `.agentic-security/repo-lineage.json` cross-repo link (local clones only, no remote fetch) — used by `origin-resolver.js`'s root-commit case, not a standalone-unwired module |
412
+ | `ai-authorship.js` | (M4 §4.3) extensible AI-authorship verifier registry (`registerAIAuthorshipVerifier`/`resolveAIAuthorship`), defaults to `{status:'unknown', verifier:null}` with nothing registered (today's real state) — wired into `origin-resolver.js`'s `originFrom`, so every SAST `findingOrigin` carries `aiAuthorship`; scoped to SAST only, not direct/transitive SCA origins |
413
+
414
+ **Four invariants, each with a test that fails if you relax it:**
415
+
416
+ - **Terminal status, always.** After `annotateGitProvenance` returns, every
417
+ finding carries a `findingProvenance` with one of `complete` / `partial` /
418
+ `uncommitted` / `not_available` / `budget_exhausted` / `error`. There is no
419
+ path — missing git binary, malformed finding, downstream throw — that leaves
420
+ the field absent. `engine.js` additionally backstops every channel OUTSIDE
421
+ the `_runAnnotator` wrapper, because that wrapper swallows throws —
422
+ `findings` and `supplyChain` with a full not_available/error catch-all as
423
+ before; since Task 11, `secrets` and blameable `logicVulns` go through REAL
424
+ resolution (real stableIds backfilled, real `annotateGitProvenance` calls
425
+ made), so their outside-the-wrapper coverage narrowed to a defensive
426
+ catch-all for whatever the real call somehow didn't reach, plus the 3
427
+ synthetic-line `logicVulns` producers (`license-policy:`/`deploy-platform:`/
428
+ `stack-playbook:`), which stay on a permanent, principled not_available —
429
+ never routed through `resolveOrigin` at all, not merely deferred.
430
+ - **Never false certainty.** A shallow clone cannot reach `complete`; an
431
+ unverifiable parent boundary degrades to `partial` with its reason carried
432
+ through. `origin-resolver.js` decides this on the `shallow` flag of the
433
+ repoState object, and it must come from the REAL `getRepoState()` — pass it a
434
+ stub and the guarantee is gone.
435
+ - **The lifecycle ledger only closes findings on a COMPLETE scan.** `applyScan`'s
436
+ remediation pass turns absence into the claim "this was fixed," which is sound
437
+ only if the scan looked everywhere. `runScan.js` computes `completeScan` (false
438
+ for `--changed-since`/`--pr` and for caller-supplied `fileContents`) and threads
439
+ it through `runFullScan` to `updateLifecycle`. `updateLifecycle` is also gated on
440
+ the `scanRoot` being **a directory that exists** — not merely truthy.
441
+ `resolveProjectRoot` honours a caller-supplied scanRoot only when it resolves to
442
+ a real directory; for `null`, for a typo'd path, or for a file, it falls back to
443
+ walking up from the PROCESS CWD. Both doors led to the same corruption: a scan
444
+ that never looked at your project writing your project's ledger, and then —
445
+ finding nothing while still claiming `completeScan` — remediating every open
446
+ finding in it. `agentic-security scan ./typo` is the reachable form. This repo's
447
+ own checkout accumulated a 1.1 MB ledger of spurious events that way.
448
+ - **One budget for the whole scan.** `engine.js` computes ONE `deadlineAt` and
449
+ passes it to all five of its `annotateGitProvenance` calls (SAST findings,
450
+ direct SCA deps, transitive SCA deps per Task 7, then secrets and blameable
451
+ logicVulns per Task 11); a caller-supplied `deadlineAt`/`perFindingBudgetMs`
452
+ wins over the coordinator's own computation. Inside, each finding gets
453
+ `max(2s, remaining/count)` so one deep-history finding cannot starve the rest.
454
+ `budget_exhausted` is the one result that is **never cached** — it is a property
455
+ of the run, not the repository, and caching it would pin a timeout in place
456
+ until HEAD moved.
457
+
458
+ **Re-entrancy brake.** `predicate-replay.js` calls `runFullScan` back on historical
459
+ blobs, so every internal re-scan must pass `provenance:false` or the pass recurses
460
+ without bound. Present callers: `history-scan.js` (×3), `pr-delta.js`,
461
+ `fix-verify.js`, `compare.js`; `lsp/server.js` uses the wider
462
+ `withStateWritesDisabled`.
463
+
464
+ **Privacy.** Author emails are collected but redacted by `redactFindingProvenance`
465
+ at every output boundary (`report/index.js`, `mcp/tools.js`) unless
466
+ `AGENTIC_SECURITY_INCLUDE_AUTHOR_EMAIL=1` / `--include-author-email`. Separately,
467
+ `AGENTIC_SECURITY_PSEUDONYMIZE_AUTHORS=1` / `--pseudonymize-authors` (PRD Section 8)
468
+ replaces `authorName` with a stable `Contributor-XXXXXXXX` pseudonym instead of
469
+ withholding it — `redactFindingProvenance` applies the same treatment to
470
+ `providerEnrichment.reviewers`/`codeowners` (FR-PROV-022's PR-reviewer logins and
471
+ raw CODEOWNERS lines), not just `findingOrigin`. Both `report/index.js` and
472
+ `mcp/tools.js` read the env var per call to build the redaction options
473
+ (`mcp/tools.js` deliberately never reads `AGENTIC_SECURITY_INCLUDE_AUTHOR_EMAIL`
474
+ itself — an agent caller gets no raw email regardless of that flag); the
475
+ `auditor-walkthrough.js` narrative reads it too, for the one `earliestOrigin`
476
+ field that bypasses `redactFindingProvenance` entirely (see that module's own
477
+ comment on why).
478
+
479
+ **At rest, `provenance/cache.js` stores the UNREDACTED record, on purpose.**
480
+ Redaction is a read-time/output-time concern — the same cached record gets
481
+ replayed back out through `redactFindingProvenance` differently per output
482
+ call (default vs. `--include-author-email` vs. `--pseudonymize-authors`), which
483
+ only works if the cache holds one raw, policy-independent copy. Pre-redacting
484
+ at write time would freeze whichever policy was active when the entry was
485
+ cached, breaking that per-call flexibility for every later reader (second
486
+ independent Finding Provenance PRD audit). The accepted mitigation is a
487
+ permissions floor, not encryption: every `cacheSet` chmods the entry file to
488
+ `0600` and the `provenance-cache/` directory to `0700` (same posture as
489
+ `integrity.js`'s per-install HMAC key). This defeats other local users/processes
490
+ reading the cache; it does not defeat root or the same OS user. See
491
+ `cache.js`'s own header for the full tradeoff writeup, including why
492
+ encryption-at-rest was considered and deferred.
493
+
354
494
  ## Gotchas
355
495
 
356
496
  - The seed `calibration-seed.json` is small (n < 30 for several families). Don't treat it as a held-out set — that's `holdout-eval.js`'s job, against an externally-supplied JSONL.
@@ -123,6 +123,18 @@ export function aggregateCorpus(detail) {
123
123
  * — measured THIS run
124
124
  * committed { corpusBaseline, proofCorpus } — read from committed files,
125
125
  * labelled as such in the output, never used to derive a rate
126
+ * scan optional — a scan-shaped object (`{findings, secrets,
127
+ * supplyChain}`, trimmed to just those arrays) from a run over
128
+ * a full (non-shallow) Git clone, used ONLY to compute
129
+ * provenanceCoverage below. `scripts/scorecard.mjs` passes
130
+ * `selfScan.provenanceScan` — the self-scan harness
131
+ * (bench/self-scan/measure.mjs) already runs a real
132
+ * `runScan()` over this project's own full git clone with
133
+ * provenance resolution on by default, so this reuses that
134
+ * run's already-computed `findingProvenance` rather than
135
+ * performing a second scan. Still optional: a caller with no
136
+ * such scan renders "not measured this run" rather than a
137
+ * fabricated rate. See PRD Success Metrics.
126
138
  */
127
139
  export function buildScorecard(inputs) {
128
140
  const corpus = aggregateCorpus(inputs.corpusDetail);
@@ -163,6 +175,14 @@ export function buildScorecard(inputs) {
163
175
  byTier: corpus.byTier,
164
176
  },
165
177
  selfScan: { measuredThisRun: true, targets, polyglot: selfScan.polyglot || { total: 0, byLanguage: {} } },
178
+ // PRD Success Metrics: "Provenance coverage >=95% complete or uncommitted
179
+ // for P0-supported findings in full Git clones." `inputs.scan` is
180
+ // optional (see the JSDoc above) — absent when no caller yet supplies a
181
+ // real scan, in which case this reports "not measured" rather than a
182
+ // fabricated 0/0.
183
+ provenanceCoverage: inputs.scan
184
+ ? { measuredThisRun: true, ...computeProvenanceCoverage(inputs.scan) }
185
+ : { measuredThisRun: false },
166
186
  taintRecall: (() => {
167
187
  const lr = inputs.layerRecall;
168
188
  if (!lr) {
@@ -249,6 +269,28 @@ export function buildScorecard(inputs) {
249
269
  };
250
270
  }
251
271
 
272
+ // PRD Success Metrics: "Provenance coverage >=95% complete or uncommitted
273
+ // for P0-supported findings in full Git clones." P0-supported scope per
274
+ // the PRD's own Release Scope table: code (SAST), secrets, IaC/config,
275
+ // direct dependency findings. Secrets now get real origin resolution
276
+ // (Task 11 -- `engine.js` calls `annotateGitProvenance` on `scan.secrets`
277
+ // with a real per-pattern-backfilled stableId, the same as SAST findings),
278
+ // so this metric no longer has a structural reason to read lower for the
279
+ // secrets share of the denominator than for any other P0-scoped channel.
280
+ export function computeProvenanceCoverage(scan) {
281
+ const p0Findings = [
282
+ ...(scan.findings || []),
283
+ ...(scan.secrets || []),
284
+ ...(scan.supplyChain || []).filter((s) => s.type === 'vulnerable_dep' && s.isDirect),
285
+ ];
286
+ const d = p0Findings.length;
287
+ const n = p0Findings.filter((f) => {
288
+ const status = f.findingProvenance?.status;
289
+ return status === 'complete' || status === 'uncommitted';
290
+ }).length;
291
+ return { n, d };
292
+ }
293
+
252
294
  function rateRow(r) {
253
295
  return `| ${r.key} | ${r.entries} | ${formatRate(r.detection.n, r.detection.d)} | ${formatRate(r.silence.n, r.silence.d)} |`;
254
296
  }
@@ -470,6 +512,24 @@ export function renderScorecardMarkdown(m) {
470
512
  }
471
513
  L.push('Per-file counts are in `docs/scorecard.json`.');
472
514
  L.push('');
515
+ if (m.provenanceCoverage && m.provenanceCoverage.measuredThisRun) {
516
+ L.push('## Provenance coverage');
517
+ L.push('');
518
+ L.push('PRD Success Metric: **>=95% of P0-scoped findings (SAST + secrets + direct**');
519
+ L.push('**dependency findings) resolve to `complete` or `uncommitted` git provenance**');
520
+ L.push('in a full (non-shallow) clone. Transitive dependency findings are excluded —');
521
+ L.push('the PRD\'s Release Scope table names direct dependency findings only.');
522
+ L.push('');
523
+ L.push('| P0-scoped findings — complete/uncommitted provenance |');
524
+ L.push('| --- |');
525
+ L.push(`| ${formatRate(m.provenanceCoverage.n, m.provenanceCoverage.d)} |`);
526
+ L.push('');
527
+ L.push('Secrets, SAST, and direct-dependency findings all resolve through the same');
528
+ L.push('git-origin resolution pipeline, so a gap in this rate reflects the clone');
529
+ L.push('itself (shallow history, uncommitted lines the pipeline could not blame) —');
530
+ L.push('not a channel this measurement structurally cannot yet cover.');
531
+ L.push('');
532
+ }
473
533
  // PRD F12.6 — the honest scorecard publishes the LIMITS too, not only the
474
534
  // rates. Three claims this project makes are only meaningful with their
475
535
  // caveat attached, and each caveat was invisible before this section:
@@ -83,6 +83,31 @@ export const ARTIFACT_REGISTRY = [
83
83
  { name: 'scan-history.json', kind: 'file', classification: 'generated', retentionClass: 'scan' },
84
84
  { name: 'last-scan.json', kind: 'file', classification: 'generated', retentionClass: 'scan' },
85
85
  { name: 'last-scan.json.sig', kind: 'file', classification: 'generated', retentionClass: 'scan' },
86
+ // Sub-project E, increment 5: the Data Flow Explorer's DataFlowGraph v1
87
+ // document + signature, written by bin/agentic-security.js alongside
88
+ // last-scan.json whenever AGENTIC_SECURITY_LINEAGE_DEEP=1 produced a
89
+ // graph. NOT to be confused with the similarly-named, already-registered
90
+ // `repo-lineage.json` below — that one is an operator-authored
91
+ // cross-repo provenance declaration (classification 'operator-config',
92
+ // never written by the scanner); this one is scanner-generated scan
93
+ // output derived from the user's own source (dataElements[].dataClasses
94
+ // included), so it belongs in the 'generated'/'scan' bucket `reset`
95
+ // clears by default, same as last-scan.json itself.
96
+ { name: 'lineage-graph.json', kind: 'file', classification: 'generated', retentionClass: 'scan' },
97
+ { name: 'lineage-graph.json.sig', kind: 'file', classification: 'generated', retentionClass: 'scan' },
98
+ // M4 deliverable #8 (FR-503 §14, DFG-022, sub-project 8a): the "Data-Flow
99
+ // Time Machine" foundation — one commit-keyed GraphSnapshot per scan,
100
+ // written by src/lineage/graph-snapshot.js's persistGraphSnapshot(),
101
+ // additively alongside (never replacing) lineage-graph.json above. Same
102
+ // 'generated'/'scan' bucket for the same reason: fully scanner-derived,
103
+ // regenerable by re-scanning at that commit.
104
+ { name: 'lineage-snapshots', kind: 'dir', classification: 'generated', retentionClass: 'scan', source: 'src/lineage/graph-snapshot.js (persistGraphSnapshot)' },
105
+ // M5 deliverable #7 (FR-505 §7.12, AC-29): the Runtime-Corroborated
106
+ // Digital Twin's observation store — one immutable whole file per adapter
107
+ // import, written by src/lineage/observation-store.js's
108
+ // persistObservationImport(), mirroring lineage-snapshots/'s own
109
+ // directory-of-files shape re-keyed commit -> import.
110
+ { name: 'runtime-observations', kind: 'dir', classification: 'generated', retentionClass: 'evidence', confidential: true, source: 'src/lineage/observation-store.js (persistObservationImport)', note: 'FR-505 requires an observation store follow artifact encryption, RETENTION, RESET, access-control and no-egress rules. That is why this is `generated` (a plain `reset` MUST be able to delete it) rather than `operator-config`, and why it carries a real retentionClass — deliberately NOT the `remediation`/`legal-holds.json` no-retention call one section down, and deliberately NOT `provenance`\'s permanent-history call. This DOES stretch `generated`\'s usual definition — a rescan does not re-derive an import, the operator re-imports it — and that stretch is disclosed here rather than hidden: FR-505\'s explicit reset requirement breaks the tie. `confidential: true` is enforced by observation-store.js calling maybeEncryptForWrite/maybeDecryptForRead itself (posture/encryption-provider.js), the same per-writer opt-in compliance-evidence.json makes — the flag alone enforces nothing.' },
86
111
  { name: 'shadow-findings.json', kind: 'file', classification: 'generated', retentionClass: 'scan' },
87
112
  { name: 'mcp-audit.log', kind: 'file', classification: 'generated', retentionClass: 'evidence' },
88
113
  { name: 'egress-audit.log', kind: 'file', classification: 'generated', retentionClass: 'evidence', note: "FR-604 per-call egress audit log — hash-chained NDJSON written by egress/audit.js's recordEgressCall, never read as config" },
@@ -125,6 +150,28 @@ export const ARTIFACT_REGISTRY = [
125
150
  { name: 'sca-upgrade-history', kind: 'dir', classification: 'generated', retentionClass: 'scan' },
126
151
  { name: 'scan-baselines', kind: 'dir', classification: 'generated', retentionClass: 'scan', source: 'posture/pr-augment.js' },
127
152
  { name: 'agent-scratchpad', kind: 'dir', classification: 'generated', retentionClass: 'cache', source: 'mcp/tools.js (append_scratchpad)' },
153
+ // Finding Provenance (M0/M1, split per PRD Section 8 retention task). Used
154
+ // to be one directory with two writers sharing it, which meant they could
155
+ // not get different retention treatment — `cmdReset`/`findExpiredArtifacts`
156
+ // only ever operate on exact TOP-LEVEL `.agentic-security/` directory
157
+ // names, never on a `/`-qualified sub-path. Now physically split:
158
+ // - posture/provenance/cache.js writes provenance-cache/<hash>.json — a
159
+ // pure HEAD-keyed memo of resolved origins, safely regenerable, no
160
+ // correctness dependency on being preserved. Gets retentionClass:
161
+ // 'cache' (7-day default / 30-day max TTL, RETENTION_DEFAULTS.cache).
162
+ // - posture/provenance/lifecycle.js writes provenance/lifecycle.json +
163
+ // .lock — the introduce/remediate/reintroduce ledger. Deliberately NO
164
+ // retentionClass: this is permanent history, not a cache; auto-expiring
165
+ // it would silently lose lifecycle events a report may already have
166
+ // cited. `reset` (without `--expired`) still clears it, which is
167
+ // explicit operator action, unlike TTL-driven auto-expiry.
168
+ // Old-location cache files (`provenance/cache/*.json`, written before this
169
+ // split) are DELIBERATELY NOT migrated — see cache.js's own comment and the
170
+ // commit that introduced this split. They are simply orphaned: invisible to
171
+ // this registry, un-swept by reset/retention, silently ignored by the new
172
+ // code, and harmless to leave until a human deletes them by hand.
173
+ { name: 'provenance-cache', kind: 'dir', classification: 'generated', retentionClass: 'cache', source: 'posture/provenance/cache.js -- pure HEAD-keyed memo, safely regenerable, no correctness dependency on being preserved' },
174
+ { name: 'provenance', kind: 'dir', classification: 'generated', source: 'posture/provenance/lifecycle.js -- the introduce/remediate/reintroduce ledger. Deliberately NO retentionClass: this is permanent history, not a cache; auto-expiring it would silently lose lifecycle events a report may already have cited.' },
128
175
  { name: 'AGENTS.md', kind: 'file', classification: 'generated', source: 'posture/agents-memory.js' },
129
176
  { name: 'AGENTS.md.archive', kind: 'file', classification: 'generated', source: 'posture/agents-memory.js' },
130
177
  { name: 'baseline.json', kind: 'file', classification: 'generated', source: 'bin/agentic-security.js (--set-baseline)', note: 'operator-set intent, functionally closer to operator-config than scan output — no auto-expiry' },
@@ -146,6 +193,19 @@ export const ARTIFACT_REGISTRY = [
146
193
  { name: 'deletion-report.json', kind: 'file', classification: 'generated', retentionClass: 'evidence', source: 'posture/state-lifecycle-report.js (via bin/agentic-security.js cmdReset)' },
147
194
  { name: 'export-report.json', kind: 'file', classification: 'generated', retentionClass: 'evidence', source: 'posture/state-lifecycle-report.js (via bin/agentic-security.js cmdExport)' },
148
195
 
196
+ // M5 governance editing workflow final-review fix round 1 (I5): backups
197
+ // of recipient-profiles.json written by `governance propose-edit --yes`,
198
+ // mirroring the `fix-history` directory precedent above — one directory
199
+ // entry covers every timestamped .bak file inside it, since the registry
200
+ // only supports exact-name matches, never a per-file timestamped name.
201
+ { name: 'recipient-profiles-backups', kind: 'dir', classification: 'generated', retentionClass: 'backup', note: 'per-edit backups written by `governance propose-edit --yes`, mirrors the fix-history/ precedent — one directory entry covers every timestamped .bak file inside it' },
202
+
203
+ // M5 deliverable #8 (FR-304 "declared" half): backups of
204
+ // cross-repo-links.json written by `federate declare --yes`, mirrors
205
+ // the `recipient-profiles-backups` precedent immediately above
206
+ // exactly.
207
+ { name: 'cross-repo-links-backups', kind: 'dir', classification: 'generated', retentionClass: 'backup', note: 'per-declare backups written by `federate declare --yes`, mirrors the recipient-profiles-backups/ precedent — one directory entry covers every timestamped .bak file inside it' },
208
+
149
209
  // ── Operator-config: hand-authored (or agent-authored) input, never wiped ──
150
210
  { name: 'rules.yml', kind: 'file', classification: 'operator-config' },
151
211
  { name: 'rules', kind: 'dir', classification: 'operator-config' },
@@ -169,12 +229,28 @@ export const ARTIFACT_REGISTRY = [
169
229
  { name: 'policy-bundle-public-key.pem', kind: 'file', classification: 'operator-config', note: 'FR-1001 public key an operator installs to verify org-distributed policy bundles — read by posture/policy-bundle.js, never written by the scanner' },
170
230
  { name: 'retention-policy.yml', kind: 'file', classification: 'operator-config', note: 'FR-702 per-retention-class TTL overrides (clamped to a built-in per-class maximum) — read by posture/retention-policy.js, never written by the scanner' },
171
231
  { name: 'legal-holds.json', kind: 'file', classification: 'operator-config', note: 'FR-707 legal holds ({artifact, owner, reason, expires_at}) — read by posture/retention-policy.js and bin/agentic-security.js cmdReset; WRITTEN by the CLI (legal-hold add/remove), but classified operator-config (not generated) deliberately: a plain `reset` must never be able to delete the very record protecting other artifacts from deletion' },
232
+ { name: 'remediation', kind: 'dir', classification: 'operator-config', note: 'M5 deliverable #6 remediation work-item ledger (items.jsonl + items.lock) — an APPEND-ONLY record of human decisions: owner assignment, approvals, manual attestations, and accepted-risk exceptions with approver/reason/scope/expiration. WRITTEN by the CLI (`remediation open|update|verify|accept-risk|reopen-check --yes`) but classified operator-config, not generated, deliberately — the same call legal-holds.json makes one entry up: a plain `reset` must never be able to delete the audit trail AC-31 depends on, and nothing regenerates it from a rescan. Deliberately NOT the `provenance` entry\'s `generated` classification (see its own note above): that ledger is scan-derived history a rescan can rebuild; this one is human decisions that cannot be. No retentionClass for the same reason legal-holds.json has none — auto-expiry would silently delete an accepted-risk exception a report may already cite.' },
172
233
  { name: 'calibration-feedback.jsonl', kind: 'file', classification: 'operator-config', note: 'FR-806 opt-in calibration ground truth ({at, findingId, outcome: accept-risk|realized-incident, predicted*, note}) — WRITTEN by the CLI (calibration-feedback record), but classified operator-config like exploit-history.jsonl: real, hard-to-recreate customer-reported ground truth, never scanner-regenerable, so a routine reset must never delete it' },
173
234
  { name: 'encryption-policy.yml', kind: 'file', classification: 'operator-config', note: 'FR-705 encryption provider/required opt-in policy ({provider: local-key, required: true|false}) — read by posture/encryption-provider.js, never written by the scanner' },
235
+ { name: 'provenance-providers.yml', kind: 'file', classification: 'operator-config', note: 'Finding Provenance M3 §3.4 GitHub/GitLab provider enrichment opt-in ({token} or provider-scoped tokens) — read by posture/provenance/providers/config.js, never written by the scanner; env vars (AGENTIC_SECURITY_GITHUB_TOKEN/AGENTIC_SECURITY_GITLAB_TOKEN) take precedence when set' },
236
+ { name: 'repo-lineage.json', kind: 'file', classification: 'operator-config', note: 'Finding Provenance M4 §4.2 cross-repository lineage declaration ({linkedFrom: {path, atCommit}}) — read by posture/provenance/repo-lineage.js, never written by the scanner; the linked path is verified as a real local git repo before use, no remote fetch' },
237
+ { name: 'recipient-profiles.json', kind: 'file', classification: 'operator-config', note: 'FR-506 recipient/subprocessor governance profiles — hand- or agent-authored via `governance propose-edit`, never scanner-regenerable, so a routine reset must never delete it' },
238
+ { name: 'cross-repo-links.json', kind: 'file', classification: 'operator-config', note: 'M5 deliverable #8 (FR-304 "declared" half) — declared local<->remote node links between two independently-scanned repos, written via `federate declare --yes`, never scanner-regenerable, so a routine reset must never delete it' },
174
239
  { name: 'logic-claims.json', kind: 'file', classification: 'operator-config', note: 'authored by an external reviewing agent; engine.js only ever reads it (fs.readFileSync, never written)' },
175
240
  { name: 'current-intent.md', kind: 'file', classification: 'operator-config', note: 'developer-authored; no writer exists anywhere in src/ or bin/' },
176
241
  { name: 'exploit-history.jsonl', kind: 'file', classification: 'operator-config', note: 'own header comment: "operator-curated record of past confirmed exploits"' },
177
242
  { name: 'cve-alerts.json', kind: 'file', classification: 'operator-config', note: 'own header comment: "Configuration is read from"; state lives in the separate cve-alerts-state.json, which IS generated' },
243
+ // Pre-existing gap, found by M5 deliverable #7's own scoping investigation
244
+ // and fixed here rather than left: posture/runtime-correlation.js reads
245
+ // these three filenames via `statePath(scanRoot, n)` with a VARIABLE, so
246
+ // test/artifact-registry-completeness.test.js's own PATTERNS regexes (which
247
+ // require a string literal) never saw them and never demanded registration.
248
+ // An unregistered state artifact means `reset` does not know about it and
249
+ // retention cannot reach it. Operator-config, not generated: these are
250
+ // hand-supplied eBPF/APM trace exports the scanner only ever reads.
251
+ { name: 'runtime-trace.jsonl', kind: 'file', classification: 'operator-config', note: 'eBPF/APM runtime trace consumed by posture/runtime-correlation.js\'s own trace loader (deliberately not named by its literal export here — no-dead-modules.test.js scans note strings too, and naming it would make it look, wrongly, like a real call site) — operator-supplied, never scanner-written; $AGENTIC_SECURITY_RUNTIME_TRACE_PATH overrides the location entirely' },
252
+ { name: 'runtime.jsonl', kind: 'file', classification: 'operator-config', note: 'alternate filename for runtime-trace.jsonl — see posture/runtime-correlation.js\'s DEFAULT_TRACE_NAMES' },
253
+ { name: 'ebpf-trace.jsonl', kind: 'file', classification: 'operator-config', note: 'alternate filename for runtime-trace.jsonl — see posture/runtime-correlation.js\'s DEFAULT_TRACE_NAMES' },
178
254
  ];
179
255
 
180
256
  export function listGeneratedArtifacts() {