@clear-capabilities/agentic-security-scanner 0.184.0 → 0.186.1

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 (228) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/bin/agentic-security.js +111 -1
  3. package/dist/1128.index.js +912 -0
  4. package/dist/1665.index.js +828 -0
  5. package/dist/2584.index.js +1 -1
  6. package/dist/4171.index.js +2 -2
  7. package/dist/5752.index.js +466 -0
  8. package/dist/7405.index.js +1 -1
  9. package/dist/8158.index.js +1713 -0
  10. package/dist/8798.index.js +1 -1
  11. package/dist/9382.index.js +2 -2
  12. package/dist/9579.index.js +1 -1
  13. package/dist/agentic-security.mjs +29 -29
  14. package/dist/agentic-security.mjs.sha256 +1 -1
  15. package/dist/{dad58d1113a12c23a142.json → ca62ee93079be4c24eee.json} +29 -12
  16. package/dist/frontend/src/shell.js +12 -2
  17. package/package.json +29 -12
  18. package/src/dataflow/CLAUDE.md +46 -1
  19. package/src/dataflow/access-paths.js +12 -2
  20. package/src/dataflow/backward.js +42 -7
  21. package/src/dataflow/cross-service-taint.js +83 -1
  22. package/src/dataflow/demand-query.js +1168 -0
  23. package/src/dataflow/dep-fingerprint.js +718 -0
  24. package/src/dataflow/engine.js +48 -6
  25. package/src/dataflow/exploit-prover.js +6 -2
  26. package/src/dataflow/ifds-precise.js +22 -0
  27. package/src/dataflow/incremental-replay.js +118 -0
  28. package/src/dataflow/incremental-session.js +257 -0
  29. package/src/dataflow/incremental.js +237 -35
  30. package/src/dataflow/index.js +25 -94
  31. package/src/dataflow/ir-cache.js +268 -0
  32. package/src/dataflow/privacy-taint.js +7 -0
  33. package/src/dataflow/summaries.js +72 -6
  34. package/src/dataflow/tabulation.js +80 -24
  35. package/src/engine.js +113 -8
  36. package/src/graph/CLAUDE.md +31 -0
  37. package/src/graph/adapters/artifact.js +81 -0
  38. package/src/graph/adapters/crossrepo.js +75 -0
  39. package/src/graph/adapters/dataflow.js +76 -0
  40. package/src/graph/adapters/provenance.js +92 -0
  41. package/src/graph/adapters/semantic.js +89 -0
  42. package/src/graph/crosswalk-builders.js +152 -0
  43. package/src/graph/crosswalk.js +146 -0
  44. package/src/graph/index.js +15 -0
  45. package/src/graph/observation.js +57 -0
  46. package/src/graph/refs.js +105 -0
  47. package/src/graph/registry.js +129 -0
  48. package/src/graph/relations.js +81 -0
  49. package/src/graph/scc.js +241 -0
  50. package/src/graph/view.js +254 -0
  51. package/src/ir/CLAUDE.md +4 -0
  52. package/src/ir/callgraph.js +36 -8
  53. package/src/ir/index.js +18 -4
  54. package/src/ir/parser-js.js +10 -0
  55. package/src/language/CLAUDE.md +13 -0
  56. package/src/language/nix-option-types.js +170 -0
  57. package/src/language/nix-script-taint.js +46 -3
  58. package/src/language/nix-secrets.js +20 -2
  59. package/src/lineage/CLAUDE.md +72 -8
  60. package/src/lineage/DESIGN_GRAPH_BUILDER.md +1 -1
  61. package/src/lineage/DESIGN_PATH_PROVENANCE.md +6 -0
  62. package/src/lineage/boundary-contract.js +273 -0
  63. package/src/lineage/completion.js +205 -0
  64. package/src/lineage/continuation.js +140 -0
  65. package/src/lineage/control-credit.js +313 -0
  66. package/src/lineage/correlation.js +199 -0
  67. package/src/lineage/coverage.js +8 -1
  68. package/src/lineage/cross-repo-link.js +22 -0
  69. package/src/lineage/cut-planner.js +548 -0
  70. package/src/lineage/driver.js +219 -66
  71. package/src/lineage/engine.js +166 -21
  72. package/src/lineage/export-json.js +5 -0
  73. package/src/lineage/graph-builder.js +84 -7
  74. package/src/lineage/haskell-view.js +59 -0
  75. package/src/lineage/index.js +3 -0
  76. package/src/lineage/nix-view.js +35 -0
  77. package/src/lineage/owned-build.js +77 -0
  78. package/src/lineage/owned-worker-entry.js +22 -0
  79. package/src/lineage/owned-worker.js +95 -0
  80. package/src/lineage/path-evidence.js +123 -0
  81. package/src/lineage/path-predicates.js +529 -0
  82. package/src/lineage/path-query.js +189 -20
  83. package/src/lineage/path-store.js +191 -17
  84. package/src/lineage/scc-solver.js +186 -0
  85. package/src/lineage/smt-orchestrator.js +174 -0
  86. package/src/lineage/smt-z3-backend.js +294 -0
  87. package/src/lineage/source-seeding.js +36 -2
  88. package/src/lineage/summaries.js +66 -4
  89. package/src/lineage/value-versions.js +130 -0
  90. package/src/lineage/witness-selection.js +95 -0
  91. package/src/pipeline/scan-health.js +18 -0
  92. package/src/posture/CLAUDE.md +17 -0
  93. package/src/posture/business-logic.js +16 -7
  94. package/src/posture/dag-commands.js +442 -0
  95. package/src/posture/evidence-bundle.js +1 -1
  96. package/src/posture/finding-defaults.js +19 -0
  97. package/src/posture/git-history.js +91 -1
  98. package/src/posture/witness-bundle.js +573 -0
  99. package/src/posture/witness-runtime.js +276 -0
  100. package/src/report/index.js +2 -0
  101. package/src/sast/CLAUDE.md +2 -0
  102. package/src/sast/_call-args.js +70 -0
  103. package/src/sast/init-scope.js +87 -0
  104. package/src/sast/listen-bind-address.js +49 -0
  105. package/src/sast/redirect-target.js +106 -0
  106. package/src/sast/reflected-input.js +76 -0
  107. package/src/sast/send-file-root.js +27 -0
  108. package/src/sast/sensitive-data-logging.js +38 -1
  109. package/dist/119.index.js +0 -197
  110. package/dist/128.index.js +0 -129
  111. package/dist/1293.index.js +0 -5689
  112. package/dist/1336.index.js +0 -16182
  113. package/dist/1351.index.js +0 -133
  114. package/dist/1387.index.js +0 -271
  115. package/dist/1437.index.js +0 -23
  116. package/dist/1445.index.js +0 -110
  117. package/dist/1505.index.js +0 -151
  118. package/dist/1557.index.js +0 -293
  119. package/dist/1621.index.js +0 -311
  120. package/dist/1751.index.js +0 -160
  121. package/dist/1765.index.js +0 -727
  122. package/dist/1861.index.js +0 -690
  123. package/dist/1906.index.js +0 -433
  124. package/dist/208.index.js +0 -1631
  125. package/dist/2223.index.js +0 -159
  126. package/dist/2238.index.js +0 -295
  127. package/dist/2530.index.js +0 -152
  128. package/dist/2565.index.js +0 -116
  129. package/dist/2574.index.js +0 -127
  130. package/dist/2620.index.js +0 -345
  131. package/dist/2773.index.js +0 -98
  132. package/dist/2775.index.js +0 -32
  133. package/dist/2966.index.js +0 -171
  134. package/dist/2972.index.js +0 -87
  135. package/dist/2991.index.js +0 -131
  136. package/dist/3063.index.js +0 -833
  137. package/dist/3075.index.js +0 -227
  138. package/dist/3121.index.js +0 -297
  139. package/dist/3314.index.js +0 -263
  140. package/dist/3480.index.js +0 -1822
  141. package/dist/3481.index.js +0 -24
  142. package/dist/3506.index.js +0 -268
  143. package/dist/3533.index.js +0 -591
  144. package/dist/3653.index.js +0 -450
  145. package/dist/3740.index.js +0 -722
  146. package/dist/3822.index.js +0 -131
  147. package/dist/3868.index.js +0 -292
  148. package/dist/3918.index.js +0 -192
  149. package/dist/4090.index.js +0 -288
  150. package/dist/4113.index.js +0 -1286
  151. package/dist/4121.index.js +0 -1190
  152. package/dist/4142.index.js +0 -690
  153. package/dist/4168.index.js +0 -509
  154. package/dist/4281.index.js +0 -993
  155. package/dist/4336.index.js +0 -436
  156. package/dist/4387.index.js +0 -532
  157. package/dist/4413.index.js +0 -302
  158. package/dist/4476.index.js +0 -4510
  159. package/dist/4491.index.js +0 -133
  160. package/dist/4538.index.js +0 -185
  161. package/dist/4684.index.js +0 -319
  162. package/dist/4740.index.js +0 -165
  163. package/dist/4780.index.js +0 -209
  164. package/dist/4802.index.js +0 -225
  165. package/dist/4812.index.js +0 -379
  166. package/dist/4835.index.js +0 -126
  167. package/dist/494.index.js +0 -226
  168. package/dist/4991.index.js +0 -549
  169. package/dist/5199.index.js +0 -305
  170. package/dist/5216.index.js +0 -109
  171. package/dist/5439.index.js +0 -301
  172. package/dist/5443.index.js +0 -97
  173. package/dist/5496.index.js +0 -377
  174. package/dist/5738.index.js +0 -240
  175. package/dist/5801.index.js +0 -588
  176. package/dist/5817.index.js +0 -714
  177. package/dist/5858.index.js +0 -223
  178. package/dist/5933.index.js +0 -143
  179. package/dist/601.index.js +0 -23
  180. package/dist/6173.index.js +0 -273
  181. package/dist/6291.index.js +0 -1871
  182. package/dist/6312.index.js +0 -666
  183. package/dist/634.index.js +0 -16182
  184. package/dist/644.index.js +0 -87
  185. package/dist/6508.index.js +0 -283
  186. package/dist/6560.index.js +0 -645
  187. package/dist/6576.index.js +0 -422
  188. package/dist/6579.index.js +0 -271
  189. package/dist/6655.index.js +0 -3221
  190. package/dist/6745.index.js +0 -305
  191. package/dist/6818.index.js +0 -21642
  192. package/dist/6932.index.js +0 -1190
  193. package/dist/6972.index.js +0 -167
  194. package/dist/6995.index.js +0 -163
  195. package/dist/7249.index.js +0 -191
  196. package/dist/7372.index.js +0 -1197
  197. package/dist/7546.index.js +0 -541
  198. package/dist/76.index.js +0 -448
  199. package/dist/7879.index.js +0 -163
  200. package/dist/8025.index.js +0 -130
  201. package/dist/8054.index.js +0 -248
  202. package/dist/808.index.js +0 -1330
  203. package/dist/8097.index.js +0 -201
  204. package/dist/8126.index.js +0 -191
  205. package/dist/8178.index.js +0 -78
  206. package/dist/8382.index.js +0 -93
  207. package/dist/8408.index.js +0 -261
  208. package/dist/8540.index.js +0 -174
  209. package/dist/8635.index.js +0 -802
  210. package/dist/8688.index.js +0 -410
  211. package/dist/8699.index.js +0 -779
  212. package/dist/8891.index.js +0 -457
  213. package/dist/8916.index.js +0 -885
  214. package/dist/9146.index.js +0 -170
  215. package/dist/9168.index.js +0 -841
  216. package/dist/9292.index.js +0 -369
  217. package/dist/9422.index.js +0 -866
  218. package/dist/9440.index.js +0 -453
  219. package/dist/9479.index.js +0 -152
  220. package/dist/9481.index.js +0 -220
  221. package/dist/9539.index.js +0 -100
  222. package/dist/9547.index.js +0 -103
  223. package/dist/9692.index.js +0 -90
  224. package/dist/9800.index.js +0 -462
  225. package/dist/9899.index.js +0 -138
  226. package/dist/9981.index.js +0 -206
  227. package/dist/999.index.js +0 -117
  228. package/dist/9999.index.js +0 -1698
@@ -1,436 +0,0 @@
1
- export const id = 4336;
2
- export const ids = [4336];
3
- export const modules = {
4
-
5
- /***/ 94336:
6
- /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
7
-
8
- /* harmony export */ __webpack_require__.d(__webpack_exports__, {
9
- /* harmony export */ evaluateVerificationEvidence: () => (/* binding */ evaluateVerificationEvidence),
10
- /* harmony export */ foldRemediationLedger: () => (/* binding */ foldRemediationLedger),
11
- /* harmony export */ validateOpenPayload: () => (/* binding */ validateOpenPayload),
12
- /* harmony export */ validateTransition: () => (/* binding */ validateTransition)
13
- /* harmony export */ });
14
- /* unused harmony exports REMEDIATION_STATES, REMEDIATION_EVENT_TYPES, ACCEPTED_RISK_REQUIRED_FIELDS, foldRemediationItem */
15
- // remediation.js — M5 deliverable #6 (Blast-Radius: Remediation Command
16
- // Center, FR-507 + AC-31): the pure RemediationItem contract, its event
17
- // fold, its state machine, and the verification-evidence decision.
18
- //
19
- // Pure by contract — zero imports, no fs, no I/O of any kind, never
20
- // throws. The ledger layer (`posture/remediation-ledger.js`, Task 2 of
21
- // this same sub-project) owns every side effect: file locking, append,
22
- // tolerant read, hash chaining. This module mirrors `governance-edit.js`'s
23
- // own split (that module's own header makes the identical claim for its
24
- // own CLI-write boundary) and the "pure data + pure functions, boundary-
25
- // tested" precedent `flow-grade.js`/`obligation-mapping.js` already set
26
- // in this package.
27
- //
28
- // ── The `verified`-unreachable-from-`state_changed` rule (AC-31) ──────
29
- //
30
- // AC-31's own `then`-clause requires that marking remediation work
31
- // "done" can NEVER, by itself, mark a finding verified — only a real
32
- // scan re-confirming the flow is gone (`scan_verification`) or an
33
- // explicitly-permitted, explicitly-attributed manual attestation
34
- // (`manual_attestation`) may transition an item into `verified`. PRD
35
- // line 171 states the same rule as an explicit non-goal: this deliverable
36
- // must never let "I fixed it" (a human claim) substitute for "the scanner
37
- // re-observed the flow is gone" (an evidence-backed claim) without an
38
- // explicit, auditable manual-attestation opt-in. `validateTransition`
39
- // enforces this by rejecting `state_changed` with `state: 'verified'`
40
- // FIRST, before even looking at the item's current state — the rejection
41
- // is therefore genuinely unconditional, not just unreached from the
42
- // states this module happens to reach today.
43
- //
44
- // ── Deliberate scope narrowing in evaluateVerificationEvidence ────────
45
- //
46
- // A required-evidence flow id is treated as SATISFIED only when it
47
- // appears in `diff.removed.flows` with `causeClassification ===
48
- // 'application_change'` — i.e. the flow is genuinely gone from the graph
49
- // and its disappearance is attributable to an application change, not to
50
- // the scanner simply seeing less than it used to. A flow that is still
51
- // PRESENT in the graph but has merely become MORE PROTECTED (e.g. its
52
- // `protectionSummary` moved from `unprotected` to `protected` — visible
53
- // only as a `changed.flows` entry, never a `removed.flows` entry) is
54
- // explicitly OUT OF SCOPE for this first cut: recognizing an "improving
55
- // transition" as evidence of a fix needs its own deliberate, disclosed
56
- // heuristic (what counts as "protected enough", whether a policy verdict
57
- // change alone should count, etc.) that this module does not attempt.
58
- // A future increment could add it; until then, such a flow reports
59
- // `flows_still_present`, same as a flow with no change at all.
60
- //
61
- // `possible_coverage_regression` is computed DIFF-WIDE by
62
- // `computeGraphDiff` (see `graph-diff.js:328`'s own
63
- // `coverageRegressionReasons`/`flowRemovalCause` — every removed flow in
64
- // one diff shares the SAME cause once any completeness signal regressed)
65
- // — which is why a coverage-regression hit on any of the item's OWN
66
- // required-evidence flows refuses the WHOLE verification immediately (PRD
67
- // line 1975), rather than letting other, seemingly-clean required-
68
- // evidence flows in the same diff verify: an incomplete scan cannot be
69
- // trusted to have honestly seen everything it claims to have not seen.
70
- // (The loop below only ever inspects `diff.removed.flows` entries for
71
- // flows actually named in `requiredEvidenceFlowIds` — an unrelated
72
- // removed flow elsewhere in the same diff is never consulted.)
73
-
74
- const REMEDIATION_STATES = Object.freeze([
75
- 'open', 'in_progress', 'awaiting_verification', 'verified', 'accepted_risk', 'reopened',
76
- ]);
77
-
78
- const REMEDIATION_EVENT_TYPES = Object.freeze([
79
- 'opened', 'state_changed', 'scan_verification', 'manual_attestation', 'accepted_risk', 'reopened',
80
- ]);
81
-
82
- // PRD line 572's exact four-field list (scoping doc §4.3).
83
- const ACCEPTED_RISK_REQUIRED_FIELDS = Object.freeze(['approver', 'reason', 'scope', 'expiration']);
84
-
85
- function _isNonEmptyString(v) {
86
- return typeof v === 'string' && v.length > 0;
87
- }
88
-
89
- function _isPlainObject(v) {
90
- return v !== null && typeof v === 'object' && !Array.isArray(v);
91
- }
92
-
93
- // ── foldRemediationItem / foldRemediationLedger ────────────────────────
94
-
95
- /**
96
- * Folds an ordered event list for ONE remediation item into its current
97
- * shape, or `null` for an empty/malformed/non-`opened`-first list. Never
98
- * throws.
99
- */
100
- function foldRemediationItem(events) {
101
- if (!Array.isArray(events) || events.length === 0) return null;
102
- const openEvent = events[0];
103
- if (!openEvent || openEvent.type !== 'opened') return null;
104
-
105
- const item = {
106
- id: openEvent.id,
107
- state: 'open',
108
- owner: openEvent.owner,
109
- dueDate: openEvent.dueDate,
110
- recommendedControl: openEvent.recommendedControl,
111
- assessment: openEvent.assessment,
112
- affectedFlowIds: openEvent.affectedFlowIds,
113
- affectedNodeIds: openEvent.affectedNodeIds,
114
- affectedEdgeIds: openEvent.affectedEdgeIds,
115
- requiredEvidence: openEvent.requiredEvidence,
116
- manualAttestationPermitted: openEvent.manualAttestationPermitted,
117
- approvals: [],
118
- exceptions: [],
119
- verificationSnapshotId: null,
120
- history: events,
121
- };
122
-
123
- for (let i = 1; i < events.length; i++) {
124
- const ev = events[i];
125
- if (!ev || typeof ev !== 'object') continue;
126
- switch (ev.type) {
127
- case 'state_changed':
128
- item.state = ev.state;
129
- break;
130
- case 'scan_verification':
131
- if (ev.outcome === 'verified') {
132
- item.state = 'verified';
133
- item.verificationSnapshotId = ev.snapshotId;
134
- }
135
- // an 'unverifiable' outcome changes neither state nor snapshot.
136
- break;
137
- case 'manual_attestation':
138
- item.state = 'verified';
139
- item.approvals.push({
140
- approver: ev.approver, reason: ev.reason, at: ev.at, evidenceKind: 'manual',
141
- });
142
- // Records a real baseline (final-review fix round 1, Blocking-3):
143
- // without this, a manually-attested item keeps whatever STALE
144
- // `verificationSnapshotId` it happened to carry (or `null`,
145
- // falling back to an even less defensible baseline) forever, and
146
- // `reopen-check` keeps diffing from that stale anchor — reopening
147
- // a just-permitted attestation on the very next run even though
148
- // nothing changed. `ev.snapshotId` is optional (a manual
149
- // attestation before any lineage scan has ever run is legitimate).
150
- if (ev.snapshotId) item.verificationSnapshotId = ev.snapshotId;
151
- break;
152
- case 'accepted_risk':
153
- item.state = 'accepted_risk';
154
- item.exceptions.push({
155
- approver: ev.approver, reason: ev.reason, scope: ev.scope, expiration: ev.expiration, at: ev.at,
156
- });
157
- break;
158
- case 'reopened':
159
- item.state = 'reopened';
160
- // Retires the stale anchor (final-review fix round 1, Blocking-3)
161
- // so it cannot outlive the verification it belonged to — the next
162
- // verification (scan or manual) must establish its own baseline.
163
- item.verificationSnapshotId = null;
164
- break;
165
- default:
166
- // an unrecognized event type is ignored by the fold — validation
167
- // of proposed events is validateTransition's job, not this one's.
168
- break;
169
- }
170
- }
171
-
172
- return item;
173
- }
174
-
175
- /**
176
- * Groups a flat, interleaved event stream by item id (`ev.itemId`,
177
- * falling back to `ev.id` on an `opened` event that carries only `id`),
178
- * preserving per-group insertion order, and folds each group. Events with
179
- * no resolvable item id are skipped. Groups that fold to `null` are
180
- * dropped. Never throws.
181
- */
182
- function foldRemediationLedger(allEvents) {
183
- const result = {};
184
- if (!Array.isArray(allEvents)) return result;
185
-
186
- const groups = new Map();
187
- for (const ev of allEvents) {
188
- if (!ev || typeof ev !== 'object') continue;
189
- const itemId = ev.itemId ?? (ev.type === 'opened' ? ev.id : undefined);
190
- if (!_isNonEmptyString(itemId)) continue;
191
- if (!groups.has(itemId)) groups.set(itemId, []);
192
- groups.get(itemId).push(ev);
193
- }
194
-
195
- for (const [itemId, events] of groups) {
196
- const item = foldRemediationItem(events);
197
- if (item !== null) result[itemId] = item;
198
- }
199
-
200
- return result;
201
- }
202
-
203
- // ── validateOpenPayload ─────────────────────────────────────────────────
204
-
205
- const _REQUIRED_ASSESSMENT_FIELDS = [
206
- 'assessmentId', 'targetId', 'targetKind', 'traceKind', 'scope', 'graphId', 'graphDigest', 'snapshotId',
207
- ];
208
-
209
- const _DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
210
-
211
- /**
212
- * Validates an `opened`-event-shaped payload. Returns `{valid, errors}`,
213
- * `errors` as `[{field, message}]`. Never throws.
214
- */
215
- function validateOpenPayload(payload) {
216
- const errors = [];
217
- const err = (field, message) => errors.push({ field, message });
218
-
219
- if (!_isPlainObject(payload)) {
220
- err('(payload)', 'payload must be an object');
221
- return { valid: false, errors };
222
- }
223
-
224
- for (const field of ['id', 'owner', 'recommendedControl']) {
225
- if (!_isNonEmptyString(payload[field])) err(field, `${field} is required and must be a non-empty string`);
226
- }
227
-
228
- if (!_isNonEmptyString(payload.dueDate) || !_DATE_RE.test(payload.dueDate)) {
229
- err('dueDate', 'dueDate is required and must be a YYYY-MM-DD date');
230
- }
231
-
232
- if (!_isPlainObject(payload.assessment)) {
233
- err('assessment', 'assessment is required and must be an object');
234
- } else {
235
- for (const field of _REQUIRED_ASSESSMENT_FIELDS) {
236
- if (!_isNonEmptyString(payload.assessment[field])) {
237
- err(`assessment.${field}`, `assessment.${field} is required and must be a non-empty string`);
238
- }
239
- }
240
- // assessmentPath is the one OPTIONAL inline field — no check.
241
- }
242
-
243
- const evidence = payload.requiredEvidence;
244
- if (!Array.isArray(evidence) || evidence.length === 0 || !evidence.every(_isNonEmptyString)) {
245
- err('requiredEvidence', 'requiredEvidence is required and must be a non-empty array of non-empty strings');
246
- }
247
-
248
- return { valid: errors.length === 0, errors };
249
- }
250
-
251
- // ── validateTransition — AC-31's own state machine ──────────────────────
252
-
253
- /**
254
- * Validates a proposed event against a remediation item's current state
255
- * (or `null` for no item yet). Returns `{valid, errors}`, `errors` as
256
- * `[{field, message}]`. Never throws.
257
- */
258
- function validateTransition(item, proposedEvent) {
259
- const errors = [];
260
- const err = (field, message) => errors.push({ field, message });
261
-
262
- if (!_isPlainObject(proposedEvent)) {
263
- err('(event)', 'proposed event must be an object');
264
- return { valid: false, errors };
265
- }
266
-
267
- const type = proposedEvent.type;
268
-
269
- if (type === 'opened') {
270
- if (item !== null && item !== undefined) {
271
- err('type', 'a remediation item with this id already exists — opened is only valid when no item exists');
272
- return { valid: false, errors };
273
- }
274
- return { valid: true, errors: [] };
275
- }
276
-
277
- // AC-31's own load-bearing rule: state_changed can never reach
278
- // `verified`. Checked FIRST, before even the "item must exist" check
279
- // below, so the rejection is genuinely unconditional — reachable from
280
- // no state at all, including no item — and cannot be routed around by
281
- // any current state. See this file's own header.
282
- if (type === 'state_changed' && proposedEvent.state === 'verified') {
283
- err('state', 'state_changed can never reach verified — only scan_verification or manual_attestation may');
284
- return { valid: false, errors };
285
- }
286
-
287
- // Every other event type requires an existing item.
288
- if (!_isPlainObject(item)) {
289
- err('type', `${type} is not valid against no item — open the item first`);
290
- return { valid: false, errors };
291
- }
292
-
293
- switch (type) {
294
- case 'state_changed': {
295
- if (proposedEvent.state === 'in_progress') {
296
- if (item.state !== 'open' && item.state !== 'reopened') {
297
- err('state', `state_changed to in_progress is only valid from open or reopened, not ${item.state}`);
298
- }
299
- } else if (proposedEvent.state === 'awaiting_verification') {
300
- if (item.state !== 'in_progress') {
301
- err('state', `state_changed to awaiting_verification is only valid from in_progress, not ${item.state}`);
302
- }
303
- } else {
304
- err('state', `state_changed to ${JSON.stringify(proposedEvent.state)} is not a recognized transition`);
305
- }
306
- break;
307
- }
308
-
309
- case 'scan_verification': {
310
- if (item.state !== 'awaiting_verification') {
311
- err('type', `scan_verification is only valid from awaiting_verification, not ${item.state}`);
312
- break;
313
- }
314
- if (proposedEvent.outcome === 'verified') {
315
- if (!_isNonEmptyString(proposedEvent.snapshotId)) {
316
- err('snapshotId', 'a verified outcome requires a snapshotId');
317
- }
318
- } else if (proposedEvent.outcome === 'unverifiable') {
319
- // no further requirement beyond the outcome itself.
320
- } else {
321
- err('outcome', `outcome ${JSON.stringify(proposedEvent.outcome)} is not a recognized scan_verification outcome`);
322
- }
323
- break;
324
- }
325
-
326
- case 'manual_attestation': {
327
- if (item.state !== 'awaiting_verification') {
328
- err('type', `manual_attestation is only valid from awaiting_verification, not ${item.state}`);
329
- break;
330
- }
331
- if (!item.manualAttestationPermitted) {
332
- err('manualAttestationPermitted', 'manual attestation is not permitted for this item — it must be opened with --allow-manual-attestation to allow one (open a new item if this one predates that need)');
333
- break;
334
- }
335
- if (!_isNonEmptyString(proposedEvent.approver)) err('approver', 'manual_attestation requires a non-empty approver');
336
- if (!_isNonEmptyString(proposedEvent.reason)) err('reason', 'manual_attestation requires a non-empty reason');
337
- break;
338
- }
339
-
340
- case 'accepted_risk': {
341
- if (item.state === 'verified' || item.state === 'accepted_risk') {
342
- err('type', `accepted_risk is not valid from ${item.state}`);
343
- break;
344
- }
345
- for (const field of ACCEPTED_RISK_REQUIRED_FIELDS) {
346
- if (!_isNonEmptyString(proposedEvent[field])) {
347
- err(field, `accepted_risk requires a non-empty ${field}`);
348
- }
349
- }
350
- break;
351
- }
352
-
353
- case 'reopened': {
354
- if (item.state !== 'verified') {
355
- err('type', `reopened is only valid from verified, not ${item.state}`);
356
- break;
357
- }
358
- if (!_isNonEmptyString(proposedEvent.reason)) {
359
- err('reason', 'reopened must always carry a non-empty reason naming what triggered it');
360
- }
361
- break;
362
- }
363
-
364
- default:
365
- err('type', `${JSON.stringify(type)} is not a recognized remediation event type`);
366
- break;
367
- }
368
-
369
- return { valid: errors.length === 0, errors };
370
- }
371
-
372
- // ── evaluateVerificationEvidence ────────────────────────────────────────
373
-
374
- /**
375
- * Decides whether a GraphDiff's real, checkable evidence satisfies every
376
- * flow id a remediation item required evidence for. See this file's own
377
- * header for the deliberate scope narrowing and the coverage-regression
378
- * whole-verification-refusal rule. Never throws.
379
- */
380
- function evaluateVerificationEvidence(diff, requiredEvidenceFlowIds) {
381
- if (!Array.isArray(requiredEvidenceFlowIds) || requiredEvidenceFlowIds.length === 0) {
382
- return { outcome: 'unverifiable', reason: 'no_required_evidence' };
383
- }
384
-
385
- const removedFlows = diff?.removed?.flows;
386
- const removedById = new Map();
387
- if (Array.isArray(removedFlows)) {
388
- for (const entry of removedFlows) {
389
- if (entry && typeof entry.id === 'string') removedById.set(entry.id, entry);
390
- }
391
- }
392
-
393
- const unsatisfiedFlowIds = [];
394
-
395
- for (const flowId of requiredEvidenceFlowIds) {
396
- const entry = removedById.get(flowId);
397
- const cause = entry?.causeClassification;
398
-
399
- if (cause === 'possible_coverage_regression') {
400
- return {
401
- outcome: 'unverifiable',
402
- reason: 'possible_coverage_regression',
403
- flowId,
404
- coverageRegressionReasons: entry.coverageRegressionReasons,
405
- };
406
- }
407
-
408
- if (cause === 'reidentified') {
409
- return {
410
- outcome: 'unverifiable',
411
- reason: 'reidentified',
412
- flowId,
413
- reidentifiedTo: entry.reidentifiedTo,
414
- };
415
- }
416
-
417
- if (cause === 'application_change') {
418
- continue; // satisfied
419
- }
420
-
421
- // Absent from removed.flows entirely, or an unrecognized
422
- // classification — either way, not satisfied.
423
- unsatisfiedFlowIds.push(flowId);
424
- }
425
-
426
- if (unsatisfiedFlowIds.length > 0) {
427
- return { outcome: 'unverifiable', reason: 'flows_still_present', unsatisfiedFlowIds };
428
- }
429
-
430
- return { outcome: 'verified' };
431
- }
432
-
433
-
434
- /***/ })
435
-
436
- };