@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
@@ -0,0 +1,912 @@
1
+ export const id = 1128;
2
+ export const ids = [1128,3196];
3
+ export const modules = {
4
+
5
+ /***/ 33196:
6
+ /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
7
+
8
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
9
+ /* harmony export */ makeIdentityTokenizer: () => (/* binding */ makeIdentityTokenizer)
10
+ /* harmony export */ });
11
+ /* harmony import */ var _posture_integrity_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(71130);
12
+ // A keyed-hash token for an authenticated identity (PRD 2 section 1, "authenticated identity"). The observation contract is closed-world
13
+ // and refuses raw identities, so what is stored is `idt_` + 16 hex of an HMAC under the per-install key:
14
+ // * the same identity always yields the same token on this install, so "the same identity used these paths" can be answered;
15
+ // * without the install key the token cannot be reversed or confirmed by guessing usernames (an unkeyed hash could);
16
+ // * tokens from two installs do not match, which is deliberate: they are not a portable identifier.
17
+ // It is a pseudonym, not anonymity: someone who holds the install key and a candidate username can confirm a match.
18
+
19
+
20
+ function makeIdentityTokenizer() {
21
+ return (raw) => (typeof raw === 'string' && raw.length > 0 ? `idt_${(0,_posture_integrity_js__WEBPACK_IMPORTED_MODULE_0__/* .signLastScan */ .lU)(`identity-token:v1:${raw}`).slice(0, 16)}` : null);
22
+ }
23
+
24
+
25
+ /***/ }),
26
+
27
+ /***/ 91128:
28
+ /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
29
+
30
+
31
+ // EXPORTS
32
+ __webpack_require__.d(__webpack_exports__, {
33
+ WITNESS_SCHEMA: () => (/* binding */ WITNESS_SCHEMA),
34
+ buildWitnessBundle: () => (/* binding */ buildWitnessBundle),
35
+ exportProvenanceArtifact: () => (/* binding */ exportProvenanceArtifact),
36
+ parseWitnessBundle: () => (/* binding */ parseWitnessBundle),
37
+ signWitnessBundle: () => (/* binding */ signWitnessBundle),
38
+ verifyWitness: () => (/* binding */ verifyWitness)
39
+ });
40
+
41
+ // UNUSED EXPORTS: replayWitness, transferModelFingerprints, verifyWitnessSignature
42
+
43
+ // EXTERNAL MODULE: external "node:crypto"
44
+ var external_node_crypto_ = __webpack_require__(77598);
45
+ // EXTERNAL MODULE: ./src/lineage/path-store.js
46
+ var path_store = __webpack_require__(22013);
47
+ // EXTERNAL MODULE: ./src/lineage/ids.js
48
+ var ids = __webpack_require__(45034);
49
+ // EXTERNAL MODULE: ./src/lineage/export-json.js
50
+ var export_json = __webpack_require__(40859);
51
+ // EXTERNAL MODULE: ./src/lineage/path-query.js
52
+ var path_query = __webpack_require__(444);
53
+ // EXTERNAL MODULE: ./src/lineage/identity-token.js
54
+ var identity_token = __webpack_require__(33196);
55
+ // EXTERNAL MODULE: ./src/mcp/redact.js
56
+ var redact = __webpack_require__(83468);
57
+ // EXTERNAL MODULE: ./src/posture/evidence-bundle.js
58
+ var evidence_bundle = __webpack_require__(98317);
59
+ ;// CONCATENATED MODULE: ./src/posture/witness-runtime.js
60
+ // Runtime support, feasibility and exploit-proof evidence for a path witness (DAG-10, R10.3 to R10.7).
61
+ //
62
+ // Pure: no fs, no network, never throws, never mutates its inputs. It READS the per-edge states that
63
+ // `lineage/runtime-edge-states.js` already derives and adds the one thing those states cannot say: whether
64
+ // the observed edges of a path belong to ONE execution.
65
+ //
66
+ // FOUR EVIDENCE STATUSES, NEVER MERGED
67
+ // ------------------------------------
68
+ // modeledPath the static analysis connected source to sink (complete or partial)
69
+ // feasibility a predicate / control check on the path (a nullable slot until DAG-04 supplies it)
70
+ // runtimeSupport what telemetry says about the path (this file)
71
+ // exploitProof explicit, execution-backed proof bound to this sink, value and configuration
72
+ // They are reported side by side. There is deliberately no function here (and the verifier rejects any bundle
73
+ // field) that combines them into a score, a level or a single confidence.
74
+ //
75
+ // WHAT RUNTIME SUPPORT MAY CLAIM
76
+ // ------------------------------
77
+ // * Several edges observed does NOT prove one end-to-end execution. `path_supported` needs ONE causal group
78
+ // (a trace or session the caller can name) that covers EVERY edge, in a compatible environment,
79
+ // deployment, time window and identity scope, with unambiguous edge mappings. Edges seen in unrelated
80
+ // traces are `edge_partial`, however many there are.
81
+ // * Low-confidence and ambiguous mappings stay CANDIDATES (`candidate_only`); they never count as observed.
82
+ // * No telemetry is not evidence of safety: `no_observation_in_window` and `not_consulted` are different
83
+ // answers and neither says "the path is not exercised".
84
+ // * A denied observation proves only that attempt was refused (`blocked_attempt`), not every execution.
85
+ //
86
+ // KNOWN LIMIT: the closed-world RuntimeObservation contract carries no trace or session token (by design:
87
+ // it is metadata-only). Causal groups are therefore supplied by the caller from a source that has one, and
88
+ // without them the strongest honest answer from the observation store alone is `edge_partial`.
89
+
90
+
91
+
92
+
93
+
94
+ const RUNTIME_SUPPORT_STATUSES = Object.freeze([
95
+ 'not_consulted', 'no_observation_in_window', 'candidate_only', 'edge_partial', 'path_supported', 'blocked_attempt',
96
+ ]);
97
+ const EXPLOIT_PROOF_STATUSES = Object.freeze(['none', 'bound', 'rejected']);
98
+ const FEASIBILITY_STATUSES = Object.freeze(['not_assessed', 'feasible', 'infeasible', 'unknown']);
99
+ const MODELED_PATH_STATUSES = Object.freeze(['connected_complete', 'connected_partial']);
100
+ const EVIDENCE_STATUS_KEYS = Object.freeze(['modeledPath', 'feasibility', 'runtimeSupport', 'exploitProof']);
101
+
102
+ const DEFAULT_MAX_WINDOW_MS = 5 * 60 * 1000;
103
+ const TRUSTED_CONFIDENCE = new Set(['high', 'medium']);
104
+ const REF_RE = /^[A-Za-z0-9._:@/-]{1,160}$/;
105
+ const isObj = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
106
+ const arr = (v) => (Array.isArray(v) ? v : []);
107
+ const sha = (s) => external_node_crypto_.createHash('sha256').update(String(s)).digest('hex');
108
+
109
+ /** A caller trace/session id becomes an opaque reference: comparable, never the raw value. */
110
+ function pseudonymizeSessionRef(raw) {
111
+ return typeof raw === 'string' && raw.length > 0 ? `sref_${sha(`session-ref:v1:${raw}`).slice(0, 16)}` : null;
112
+ }
113
+
114
+ function iso(v) {
115
+ if (typeof v !== 'string') return null;
116
+ const t = Date.parse(v);
117
+ return Number.isFinite(t) ? t : null;
118
+ }
119
+
120
+ /**
121
+ * Reduce caller-supplied causal groups to the closed set of fields the assessment reads. Unknown fields are
122
+ * dropped and only COUNTED (their names could themselves carry a value). Identities are tokenized with the
123
+ * existing keyed-hash convention; a raw identity with no tokenizer is dropped, never kept.
124
+ */
125
+ function sanitizeCausalGroups(groups, { tokenize = (0,identity_token.makeIdentityTokenizer)() } = {}) {
126
+ const out = []; const rejected = []; let droppedFields = 0;
127
+ const GROUP_KEYS = new Set(['id', 'sessionRef', 'sessionId', 'traceId', 'environment', 'deploymentRef', 'deploymentConfigDigest',
128
+ 'identity', 'identityToken', 'firstObservedAt', 'lastObservedAt', 'edgeObservations']);
129
+ const OBS_KEYS = new Set(['edgeId', 'firstObservedAt', 'lastObservedAt', 'matchConfidence', 'matchedEdgeIds', 'observationIds']);
130
+ arr(groups).forEach((g, i) => {
131
+ if (!isObj(g)) { rejected.push({ index: i, reason: 'not-an-object' }); return; }
132
+ droppedFields += Object.keys(g).filter((k) => !GROUP_KEYS.has(k)).length;
133
+ // Idempotent: an already-opaque reference is kept, so re-sanitizing a recorded group (replay) changes nothing.
134
+ const ref = typeof g.sessionRef === 'string' && /^sref_[0-9a-f]{16}$/.test(g.sessionRef)
135
+ ? g.sessionRef : pseudonymizeSessionRef(g.sessionRef ?? g.sessionId ?? g.traceId);
136
+ const token = typeof g.identityToken === 'string' && /^idt_[0-9a-f]{16}$/.test(g.identityToken)
137
+ ? g.identityToken : (typeof g.identity === 'string' ? tokenize(g.identity) : null);
138
+ const ident = (v) => (typeof v === 'string' && REF_RE.test(v) ? v : null);
139
+ const edgeObservations = [];
140
+ for (const o of arr(g.edgeObservations)) {
141
+ if (!isObj(o) || typeof o.edgeId !== 'string' || !REF_RE.test(o.edgeId)) continue;
142
+ droppedFields += Object.keys(o).filter((k) => !OBS_KEYS.has(k)).length;
143
+ edgeObservations.push({
144
+ edgeId: o.edgeId,
145
+ firstObservedAt: iso(o.firstObservedAt) === null ? null : o.firstObservedAt,
146
+ lastObservedAt: iso(o.lastObservedAt) === null ? null : o.lastObservedAt,
147
+ matchConfidence: ['high', 'medium', 'low', 'ambiguous'].includes(o.matchConfidence) ? o.matchConfidence : 'ambiguous',
148
+ matchedEdgeIds: arr(o.matchedEdgeIds).filter((x) => typeof x === 'string' && REF_RE.test(x)).sort(),
149
+ observationIds: arr(o.observationIds).filter((x) => typeof x === 'string' && REF_RE.test(x)).sort().slice(0, 8),
150
+ });
151
+ }
152
+ out.push({
153
+ id: ident(g.id) ?? `group-${i}`,
154
+ sessionRef: ref,
155
+ environment: ident(g.environment),
156
+ deploymentRef: ident(g.deploymentRef),
157
+ deploymentConfigDigest: ident(g.deploymentConfigDigest),
158
+ identityToken: token ?? null,
159
+ firstObservedAt: iso(g.firstObservedAt) === null ? null : g.firstObservedAt,
160
+ lastObservedAt: iso(g.lastObservedAt) === null ? null : g.lastObservedAt,
161
+ edgeObservations: edgeObservations.sort((a, b) => (a.edgeId < b.edgeId ? -1 : a.edgeId > b.edgeId ? 1 : 0)),
162
+ });
163
+ });
164
+ return { groups: out, rejected, droppedFields };
165
+ }
166
+
167
+ /** Why a single causal group cannot support the WHOLE path (empty list means it can). */
168
+ function groupIncompatibilities(group, pathEdgeIds, expected, maxWindowMs) {
169
+ const why = [];
170
+ if (!group.sessionRef) why.push('no-session-or-trace-reference');
171
+ const covered = new Map(group.edgeObservations.map((o) => [o.edgeId, o]));
172
+ const missing = pathEdgeIds.filter((id) => !covered.has(id));
173
+ if (missing.length) why.push(`edges-not-in-group:${missing.length}`);
174
+ if (!expected.environment) why.push('environment-unspecified');
175
+ else if (group.environment !== expected.environment) why.push('environment-mismatch');
176
+ if (!expected.deploymentRef) why.push('deployment-unspecified');
177
+ else if (group.deploymentRef !== expected.deploymentRef) why.push('deployment-mismatch');
178
+ if (expected.deploymentConfigDigest && group.deploymentConfigDigest && expected.deploymentConfigDigest !== group.deploymentConfigDigest) {
179
+ why.push('deployment-configuration-mismatch');
180
+ }
181
+ if (expected.identityToken && group.identityToken && expected.identityToken !== group.identityToken) why.push('identity-scope-mismatch');
182
+ const first = iso(group.firstObservedAt); const last = iso(group.lastObservedAt);
183
+ if (first === null || last === null) why.push('time-window-missing');
184
+ else {
185
+ if (last < first) why.push('time-window-inverted');
186
+ if (last - first > maxWindowMs) why.push('time-window-too-wide');
187
+ const w = expected.window;
188
+ if (w && (iso(w.start) !== null && first < iso(w.start) || iso(w.end) !== null && last > iso(w.end))) why.push('outside-requested-window');
189
+ }
190
+ // Edge-level mapping and order: each edge's own interval is inside the group window and the path order
191
+ // is not contradicted (an edge cannot have been last seen before its predecessor was first seen).
192
+ let prevFirst = null;
193
+ for (const id of pathEdgeIds) {
194
+ const o = covered.get(id);
195
+ if (!o) continue;
196
+ if (!TRUSTED_CONFIDENCE.has(o.matchConfidence) || o.matchedEdgeIds.length > 1) { why.push('ambiguous-edge-mapping'); break; }
197
+ const f = iso(o.firstObservedAt); const l = iso(o.lastObservedAt);
198
+ if (f === null || l === null) { why.push('edge-time-missing'); break; }
199
+ if (first !== null && last !== null && (f < first || l > last)) { why.push('edge-outside-group-window'); break; }
200
+ if (prevFirst !== null && l < prevFirst) { why.push('causal-order-violated'); break; }
201
+ prevFirst = f;
202
+ }
203
+ return why;
204
+ }
205
+
206
+ /**
207
+ * Assess what telemetry says about ONE path.
208
+ *
209
+ * @param {object} p
210
+ * @param {string[]} p.pathEdgeIds DataFlowGraph edge ids of the path, source first.
211
+ * @param {object|null} p.edgeStates the result of `deriveEdgeStates` (null or evaluated:false means no telemetry was consulted)
212
+ * @param {object[]} [p.causalGroups]
213
+ * @param {object} [p.expected] { environment, deploymentRef, deploymentConfigDigest, identityToken, window, maxWindowMs }
214
+ */
215
+ function assessRuntimeSupport({ pathEdgeIds, edgeStates, causalGroups = [], expected = {}, tokenize } = {}) {
216
+ const ids = arr(pathEdgeIds).filter((x) => typeof x === 'string');
217
+ const base = {
218
+ pathEdgeCount: ids.length, observedEdgeIds: [], unobservedEdgeIds: [], candidateEdgeIds: [], blockedEdgeIds: [],
219
+ wholePath: { supported: false, groupId: null, reasons: [] }, groupsConsidered: 0, rejectedGroups: 0, droppedFields: 0,
220
+ statements: [],
221
+ };
222
+ if (!ids.length || !isObj(edgeStates) || !edgeStates.evaluated) {
223
+ return { ...base, status: 'not_consulted', statements: ['No telemetry was consulted for this path. That is not an observation of absence.'] };
224
+ }
225
+ const sanitized = sanitizeCausalGroups(causalGroups, { tokenize });
226
+ base.groupsConsidered = sanitized.groups.length; base.rejectedGroups = sanitized.rejected.length; base.droppedFields = sanitized.droppedFields;
227
+
228
+ for (const id of ids) {
229
+ const s = edgeStates.byEdge?.[id];
230
+ const states = arr(s?.states);
231
+ if (states.includes('RUNTIME_OBSERVED')) base.observedEdgeIds.push(id);
232
+ else if (states.includes('RUNTIME_BLOCKED')) base.blockedEdgeIds.push(id);
233
+ else {
234
+ base.unobservedEdgeIds.push(id);
235
+ if ((s?.uncertainObservations ?? 0) > 0) base.candidateEdgeIds.push(id);
236
+ }
237
+ // An edge observed AND denied keeps both facts.
238
+ if (states.includes('RUNTIME_OBSERVED') && states.includes('RUNTIME_BLOCKED')) base.blockedEdgeIds.push(id);
239
+ }
240
+
241
+ const allObserved = base.observedEdgeIds.length === ids.length;
242
+ if (allObserved) {
243
+ const maxWindowMs = Number.isFinite(expected.maxWindowMs) && expected.maxWindowMs > 0 ? expected.maxWindowMs : DEFAULT_MAX_WINDOW_MS;
244
+ let best = null;
245
+ for (const g of sanitized.groups) {
246
+ const why = groupIncompatibilities(g, ids, expected, maxWindowMs);
247
+ if (!why.length) { best = { g, why: [] }; break; }
248
+ if (!best || why.length < best.why.length) best = { g, why };
249
+ }
250
+ if (best && !best.why.length) {
251
+ base.wholePath = { supported: true, groupId: best.g.id, reasons: [] };
252
+ return { ...base, status: 'path_supported', statements: [
253
+ 'Every edge was observed inside ONE compatible trace or session. This supports that this path executed in that session; it does not prove the data was sensitive or that the path is exploitable.',
254
+ ] };
255
+ }
256
+ base.wholePath.reasons = best ? best.why : ['no-causal-group-supplied'];
257
+ } else {
258
+ base.wholePath.reasons = ['not-every-edge-observed'];
259
+ }
260
+
261
+ if (base.blockedEdgeIds.length && base.blockedEdgeIds.some((id) => !base.observedEdgeIds.includes(id))) {
262
+ return { ...base, status: 'blocked_attempt', statements: [
263
+ 'A denied observation proves only that this attempt was refused. It does not show every execution of this path is blocked.',
264
+ ] };
265
+ }
266
+ if (base.observedEdgeIds.length) {
267
+ return { ...base, status: 'edge_partial', statements: [
268
+ 'Some edges were observed, but not within one compatible trace or session. Independently observed edges do not establish an end-to-end execution.',
269
+ ] };
270
+ }
271
+ if (base.candidateEdgeIds.length) {
272
+ return { ...base, status: 'candidate_only', statements: [
273
+ 'Only low-confidence or ambiguous mappings were seen. They remain candidates and are not counted as observed.',
274
+ ] };
275
+ }
276
+ return { ...base, status: 'no_observation_in_window', statements: [
277
+ 'Telemetry was consulted and nothing mapped to this path in the window. That is not evidence that the path is unreachable or safe.',
278
+ ] };
279
+ }
280
+
281
+ /**
282
+ * R10.6: external proof references are bound to the sink, the value and the configuration. A reference that is
283
+ * stale or mismatched is REJECTED with its reasons; it is never re-attached to the changed graph.
284
+ */
285
+ function bindExploitProofs(proofs, expected) {
286
+ const bound = []; const rejected = [];
287
+ for (const [i, p] of arr(proofs).entries()) {
288
+ const id = isObj(p) && typeof p.id === 'string' && REF_RE.test(p.id) ? p.id : `proof-${i}`;
289
+ if (!isObj(p)) { rejected.push({ id, reasons: ['not-an-object'] }); continue; }
290
+ const reasons = [];
291
+ if (!['proof', 'dnr-hunt', 'incident'].includes(p.kind)) reasons.push('unknown-kind');
292
+ if (!p.sinkRef || !p.dataElementId || !p.graphDigest || !p.analysisConfigDigest) reasons.push('unbound');
293
+ else {
294
+ if (p.sinkRef !== expected.sinkRef) reasons.push('sink-mismatch');
295
+ if (p.dataElementId !== expected.dataElementId) reasons.push('value-mismatch');
296
+ if (p.graphDigest !== expected.graphDigest) reasons.push('stale-graph');
297
+ if (p.analysisConfigDigest !== expected.analysisConfigDigest) reasons.push('configuration-mismatch');
298
+ }
299
+ if (reasons.length) rejected.push({ id, reasons });
300
+ else bound.push({ id, kind: p.kind, executionBacked: p.kind === 'proof', note: typeof p.note === 'string' ? (0,redact/* redactString */.rd)(p.note).slice(0, 200) : null });
301
+ }
302
+ bound.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
303
+ rejected.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
304
+ // Rejected references stay listed with their reasons whatever the status; they are never silently dropped.
305
+ return { status: bound.length ? 'bound' : rejected.length ? 'rejected' : 'none', bound, rejected };
306
+ }
307
+
308
+ /** Validate the nullable DAG-04 slot. Returns { ok, errors }. */
309
+ function validateControlEvidence(ce, pathEdgeIds) {
310
+ if (ce === null) return { ok: true, errors: [] };
311
+ const errors = [];
312
+ if (!isObj(ce)) return { ok: false, errors: ['control evidence must be an object or null'] };
313
+ if (!FEASIBILITY_STATUSES.includes(ce.status) || ce.status === 'not_assessed') errors.push('control evidence status must be feasible, infeasible or unknown');
314
+ const set = new Set(pathEdgeIds);
315
+ for (const k of ['predicateRefs', 'controlRefs']) {
316
+ if (!Array.isArray(ce[k])) { errors.push(`${k} must be an array`); continue; }
317
+ for (const r of ce[k]) {
318
+ if (!isObj(r) || !set.has(r.edgeId) || typeof r.ref !== 'string' || !REF_RE.test(r.ref)) errors.push(`${k} entry must name an edge on this path and an identifier-shaped ref`);
319
+ }
320
+ }
321
+ return { ok: errors.length === 0, errors };
322
+ }
323
+
324
+ /**
325
+ * The four statuses, side by side. Returns exactly the four keys in EVIDENCE_STATUS_KEYS and nothing that
326
+ * ranks, scores or combines them.
327
+ */
328
+ function buildEvidenceStatuses({ pathComplete, controlEvidence, runtime, exploit }) {
329
+ return {
330
+ modeledPath: { status: pathComplete ? 'connected_complete' : 'connected_partial' },
331
+ feasibility: { status: controlEvidence ? controlEvidence.status : 'not_assessed' },
332
+ runtimeSupport: { status: runtime.status },
333
+ exploitProof: { status: exploit.status },
334
+ };
335
+ }
336
+
337
+ ;// CONCATENATED MODULE: ./src/posture/witness-bundle.js
338
+ // Replayable path witness bundles (DAG-10, R10.1 to R10.7).
339
+ //
340
+ // WHAT THIS ADDS
341
+ // --------------
342
+ // `evidence-bundle.js` signs a per-FINDING claim. A path witness is the evidence for one source-to-sink
343
+ // PATH, built so that someone holding only the referenced artifacts can re-derive it offline:
344
+ //
345
+ // * the DataFlowGraph (bound by `computeGraphDigest`),
346
+ // * the provenance store export (the raw hop stream plus the out-of-band truncation marks, bound by digest),
347
+ // * the source blobs the path's scopes name (bound by sha256),
348
+ // * the analysis configuration and the transfer-model fingerprints (bound by digest).
349
+ //
350
+ // A bundle records every edge in order with its full derivation inputs and its call/return binding. The
351
+ // replay verifier rebuilds a `PathStore` from the hop stream, requires every recorded edge to be the edge the
352
+ // store derives (same id, same endpoints, same discriminator fields, same truncation mark), requires the edges
353
+ // to chain, and re-checks every call/return binding against the hops that carry it. Altering an edge, a
354
+ // source blob, a call-site binding or the configuration digest makes replay fail.
355
+ //
356
+ // WHAT A VALID SIGNATURE PROVES
357
+ // -----------------------------
358
+ // Integrity only: the bundle is the one the signer produced. It does not say the transfer model is correct,
359
+ // the finding is real or the path is exploitable. Replay adds: the recorded path re-derives from the
360
+ // referenced artifacts under the recorded model fingerprints. It still does not validate the model itself.
361
+ //
362
+ // FOUR EVIDENCE STATUSES, KEPT APART
363
+ // ----------------------------------
364
+ // `evidence` carries exactly modeledPath, feasibility, runtimeSupport and exploitProof, each a status and
365
+ // nothing else. The verifier rejects any other key there, so a score or a merged confidence cannot be added
366
+ // without failing the check. See witness-runtime.js for the rules each one follows.
367
+ //
368
+ // PARTIAL STAYS PARTIAL
369
+ // ---------------------
370
+ // Completion is recorded as explicit fields (path, query, analysis, store) plus a derived `overall`. Every
371
+ // required field must be present after parsing (a missing field is invalid, never "complete"), and replay
372
+ // RE-DERIVES the path's terminal reason and the truncation marks from the artifacts, so a bundle edited to
373
+ // claim complete, or a partial one whose fields were dropped in transit, fails.
374
+ //
375
+ // REDACTION
376
+ // ---------
377
+ // The bundle holds ids, hashes, kinds, scopes and access paths, never payloads. Free text is passed through
378
+ // the existing secret redactor; identities are keyed-hash tokens (`identity-token.js`); a trace or session id
379
+ // becomes an opaque reference. Digests and ids are kept, so an authorized replay still works.
380
+
381
+
382
+
383
+
384
+
385
+
386
+
387
+
388
+
389
+
390
+
391
+ const WITNESS_SCHEMA = 'agentic-security/path-witness@1';
392
+ const PROVENANCE_ARTIFACT_SCHEMA = 'agentic-security/provenance-hops@1';
393
+
394
+ const WITNESS_BOUNDS = Object.freeze({ maxPathEdges: 5000, maxHops: 2_000_000, maxSources: 2000, maxSourceBytes: 64 * 1024 * 1024 });
395
+
396
+ const PROVES = 'The contents of this bundle are exactly what the signer attested, unmodified.';
397
+ const DOES_NOT_PROVE =
398
+ 'A signature is an integrity claim only. It does not prove the finding is real or exploitable, and it does not ' +
399
+ 'prove any transfer model is correct. Replay shows the recorded path re-derives from the referenced artifacts under ' +
400
+ 'the recorded model fingerprints; it does not validate those models. Modeled path, feasibility, runtime support and ' +
401
+ 'exploit proof are separate statuses and are never combined into one confidence.';
402
+
403
+ const WITNESS_UNSIGNED_KEYS = [
404
+ 'schema', 'graphRef', 'query', 'references', 'path', 'artifacts', 'sources', 'config', 'transferModels',
405
+ 'controlEvidence', 'completion', 'evidence', 'runtime', 'exploit', 'graphBinding', 'redaction', 'engine',
406
+ 'proves', 'doesNotProve',
407
+ ];
408
+ const WITNESS_TOP_KEYS = new Set([...WITNESS_UNSIGNED_KEYS, 'issuance', 'signature']);
409
+ const OVERALL = ['complete', 'partial', 'cancelled', 'unavailable', 'invalid'];
410
+
411
+ const witness_bundle_sha = (text) => `sha256:${external_node_crypto_.createHash('sha256').update(text).digest('hex')}`;
412
+ const witness_bundle_isObj = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
413
+ const witness_bundle_arr = (v) => (Array.isArray(v) ? v : []);
414
+ const jsonSafe = (v) => JSON.parse(JSON.stringify(v));
415
+ const same = (a, b) => (0,evidence_bundle/* canonicalJson */.dj)(a) === (0,evidence_bundle/* canonicalJson */.dj)(b);
416
+ const fileOfScope = (scope) => (typeof scope === 'string' ? scope.split('::')[0] : null);
417
+
418
+ // ---- artifacts -------------------------------------------------------------------------------------
419
+
420
+ /**
421
+ * The provenance store export: the normalized hop stream plus the truncation marks that never travel as hops.
422
+ * `incomplete` is `analysisCompletion.incomplete` from the graph builder (entries `{qid, context, reasons}`).
423
+ */
424
+ function exportProvenanceArtifact({ hops, incomplete = [] }) {
425
+ const seen = new Map();
426
+ for (const h of witness_bundle_arr(hops)) {
427
+ const norm = Object.fromEntries(path_store/* HOP_FIELDS */.KO.map((f) => [f, h?.[f] ?? null]));
428
+ seen.set((0,evidence_bundle/* canonicalJson */.dj)(norm), norm);
429
+ }
430
+ const hopList = [...seen.keys()].sort().map((k) => seen.get(k));
431
+ const truncations = witness_bundle_arr(incomplete)
432
+ .map((u) => ({ scope: u.qid ?? u.scope ?? null, context: u.context ?? null, reason: u.reasons?.[0] ?? u.reason ?? 'analysis-incomplete' }))
433
+ .sort((a, b) => ((0,evidence_bundle/* canonicalJson */.dj)(a) < (0,evidence_bundle/* canonicalJson */.dj)(b) ? -1 : 1));
434
+ return { schema: PROVENANCE_ARTIFACT_SCHEMA, hops: hopList, truncations, digest: witness_bundle_sha((0,evidence_bundle/* canonicalJson */.dj)({ hops: hopList, truncations })) };
435
+ }
436
+
437
+ function artifactDigest(a) {
438
+ return witness_bundle_sha((0,evidence_bundle/* canonicalJson */.dj)({ hops: a.hops, truncations: a.truncations }));
439
+ }
440
+
441
+ function rebuildStore(artifact, storeLimits) {
442
+ const store = new path_store/* PathStore */.Xw(storeLimits);
443
+ store.addHops(artifact.hops);
444
+ for (const t of witness_bundle_arr(artifact.truncations)) store.markTruncated(t.scope, t.context, t.reason);
445
+ return store;
446
+ }
447
+
448
+ /**
449
+ * Fingerprints of the behaviour replay relies on. Computed by RUNNING the classifiers on fixed probe hops, not
450
+ * by hashing source text, so a minified bundle and the source tree agree and a behavioural change is caught.
451
+ */
452
+ function transferModelFingerprints() {
453
+ const base = { kind: 'production', subKind: 'ident', scope: 's', dataElementId: 'd', fromPath: null, toPath: null, syntacticPath: null, nodeId: 'n', line: 1, widenReason: null, lossReason: null, context: 'c', peerScope: null, peerContext: null };
454
+ const probes = [
455
+ { ...base, fromPath: 'a' }, { ...base, peerScope: 'p', peerContext: 'pc' }, { ...base, peerScope: 'p', peerContext: 'pc', lossReason: 'context-cap-degraded' }, base,
456
+ { ...base, kind: 'write-out', subKind: 'assign', toPath: 'b' }, { ...base, kind: 'write-out', subKind: 'assign', toPath: 'b', peerScope: 'p', peerContext: 'pc' },
457
+ { ...base, kind: 'write-out', subKind: 'return' }, { ...base, kind: 'write-out', subKind: 'call-arg' }, { ...base, kind: 'write-out', subKind: 'x', lossReason: 'unsupported-target' },
458
+ ];
459
+ const probeResults = probes.map((p) => (p.kind === 'write-out' ? (0,path_store/* classifyOut */.BC)(p) : (0,path_store/* classifyIn */.as)(p)));
460
+ const edgeProbe = (0,ids/* provenanceEdgeId */.OF)({ fromNodeId: 'a', toNodeId: 'b', dataElementId: 'd', scope: 's', context: 'c', siteNodeId: 'n', inKind: 'production', inSubKind: 'ident', outKind: 'write-out', outSubKind: 'assign', widenReasons: ['w'], lossReasons: [] });
461
+ const nodeProbe = (0,ids/* provenanceNodeId */.Lg)({ kind: 'path', scope: 's', context: 'c', path: 'p', siteNodeId: null, dataElementId: 'd' });
462
+ return {
463
+ hopRecord: witness_bundle_sha((0,evidence_bundle/* canonicalJson */.dj)(path_store/* HOP_FIELDS */.KO)),
464
+ halfEdgeClassification: witness_bundle_sha((0,evidence_bundle/* canonicalJson */.dj)(probeResults)),
465
+ identity: witness_bundle_sha((0,evidence_bundle/* canonicalJson */.dj)([edgeProbe, nodeProbe])),
466
+ pathQueryDefaults: witness_bundle_sha((0,evidence_bundle/* canonicalJson */.dj)(path_query/* DEFAULTS */.zY)),
467
+ };
468
+ }
469
+
470
+ // ---- records ---------------------------------------------------------------------------------------
471
+
472
+ function bindingOf(store, edge) {
473
+ const from = store.getNode(edge.fromNodeId); const to = store.getNode(edge.toNodeId);
474
+ const fromPeer = !!from && (from.scope !== edge.scope || from.context !== edge.context);
475
+ const toPeer = !!to && (to.scope !== edge.scope || to.context !== edge.context);
476
+ let direction = 'local'; let callee = null;
477
+ if (fromPeer && toPeer) direction = 'invalid';
478
+ else if (fromPeer) { direction = 'return'; callee = from; } else if (toPeer) { direction = 'call'; callee = to; }
479
+ return {
480
+ siteNodeId: edge.siteNodeId, callerScope: edge.scope, callerContext: edge.context, direction,
481
+ calleeScope: callee ? callee.scope : null, calleeContext: callee ? callee.context : null, line: edge.line ?? null,
482
+ };
483
+ }
484
+
485
+ function edgeRecordOf(store, edge) {
486
+ return {
487
+ edgeId: edge.id, fromNodeId: edge.fromNodeId, toNodeId: edge.toNodeId, dataElementId: edge.dataElementId,
488
+ scope: edge.scope, context: edge.context, siteNodeId: edge.siteNodeId,
489
+ inKind: edge.inKind, inSubKind: edge.inSubKind, outKind: edge.outKind, outSubKind: edge.outSubKind,
490
+ widenReasons: [...edge.widenReasons], lossReasons: [...edge.lossReasons],
491
+ crossScope: !!edge.crossScope, ambiguousCorrelation: !!edge.ambiguousCorrelation, originated: !!edge.originated,
492
+ truncated: !!edge.truncated, line: edge.line ?? null, binding: bindingOf(store, edge),
493
+ };
494
+ }
495
+
496
+ function nodeRef(n) { return n ? { nodeId: n.id, kind: n.kind, scope: n.scope, context: n.context } : null; }
497
+
498
+ function deriveOverall(c) {
499
+ if (c.query.status === 'invalid') return 'invalid';
500
+ if (c.analysis.status === 'cancelled') return 'cancelled';
501
+ if (c.analysis.status === 'unavailable' || c.query.status === 'unavailable') return 'unavailable';
502
+ if (c.query.status === 'cancelled') return 'cancelled';
503
+ if (c.path.complete !== true || c.query.status !== 'complete' || c.analysis.status !== 'complete' || c.store.limitHit !== false) return 'partial';
504
+ return 'complete';
505
+ }
506
+
507
+ function terminalReasonFor(store, path, maxDepth) {
508
+ const first = path.nodeIds[0];
509
+ const incoming = store.edgesTo(first);
510
+ if (incoming.length === 0) {
511
+ return store.diagnostics().orphanedPeerSources.some((o) => o.nodeId === first) ? 'incomplete-record' : 'origin';
512
+ }
513
+ // The walk checks depth before it looks for a cycle, so a node at the depth limit is a depth-limit stop.
514
+ if (Number.isInteger(maxDepth) && path.nodeIds.length - 1 >= maxDepth) return 'depth-limit';
515
+ const onPath = new Set(path.nodeIds);
516
+ return incoming.every((e) => onPath.has(e.fromNodeId)) ? 'cycle' : 'depth-limit';
517
+ }
518
+
519
+ function compactEdgeStates(edgeStates, graphEdgeIds) {
520
+ if (!witness_bundle_isObj(edgeStates) || !edgeStates.evaluated) return { evaluated: false, byEdge: {} };
521
+ const byEdge = {};
522
+ for (const id of graphEdgeIds) {
523
+ const s = edgeStates.byEdge?.[id];
524
+ byEdge[id] = { states: witness_bundle_arr(s?.states).filter((x) => typeof x === 'string').sort(), uncertainObservations: Number.isInteger(s?.uncertainObservations) ? s.uncertainObservations : 0 };
525
+ }
526
+ return { evaluated: true, byEdge };
527
+ }
528
+
529
+ // ---- build -----------------------------------------------------------------------------------------
530
+
531
+ /**
532
+ * Build an unsigned witness bundle for one reconstructed path.
533
+ *
534
+ * @param {object} o
535
+ * @param {object} o.result the `reconstructPaths` result the path came from
536
+ * @param {object} [o.path] one of `result.paths` (default: the first, or the one named by `o.pathId`)
537
+ * @param {object} o.store the PathStore the result was computed over
538
+ * @param {object} o.graph the DataFlowGraph the store's analysis produced
539
+ * @param {object} o.provenance `exportProvenanceArtifact(...)` of the same analysis
540
+ * @param {object} [o.analysisConfig] extra configuration to bind (budgets already recorded)
541
+ * @param {object} [o.sourceBlobs] { file: text } the path's scopes name
542
+ * @param {object} [o.graphBinding] { flowId } to bind the path to a DataFlowGraph flow (required for runtime evidence)
543
+ * @param {object|null} [o.controlEvidence] the DAG-04 slot; null until it exists
544
+ * @param {object} [o.runtime] { edgeStates, causalGroups, expected }
545
+ * @param {object[]} [o.exploitProofs]
546
+ * @returns {{ok:true, bundle:object}|{ok:false, reason:string}}
547
+ */
548
+ function buildWitnessBundle(o = {}) {
549
+ const { result, store, graph, provenance } = o;
550
+ if (!witness_bundle_isObj(result) || !store || !witness_bundle_isObj(graph) || !witness_bundle_isObj(provenance)) return { ok: false, reason: 'result, store, graph and provenance are required' };
551
+ if (provenance.schema !== PROVENANCE_ARTIFACT_SCHEMA || artifactDigest(provenance) !== provenance.digest) return { ok: false, reason: 'provenance artifact is malformed or its digest does not match' };
552
+ const path = o.path ?? (o.pathId ? result.paths.find((p) => p.id === o.pathId) : result.paths[0]);
553
+ if (!path || !witness_bundle_arr(path.edgeIds).length) return { ok: false, reason: 'no path to witness (an empty or unknown result has nothing to attest)' };
554
+ if (path.edgeIds.length > WITNESS_BOUNDS.maxPathEdges) return { ok: false, reason: `path exceeds ${WITNESS_BOUNDS.maxPathEdges} edges` };
555
+
556
+ const edges = [];
557
+ for (const id of path.edgeIds) {
558
+ const e = store.getEdge(id);
559
+ if (!e) return { ok: false, reason: `edge ${id} is not in the supplied store` };
560
+ edges.push(edgeRecordOf(store, e));
561
+ }
562
+ const srcNode = store.getNode(edges[0].fromNodeId); const sinkNode = store.getNode(edges[edges.length - 1].toNodeId);
563
+ if (!srcNode || !sinkNode) return { ok: false, reason: 'path endpoints are not in the supplied store' };
564
+
565
+ const storeLimits = store.limitStatus();
566
+ // The store handed in must be the one the provenance export describes: rebuild from the export and require
567
+ // identical edges. Otherwise the bundle could not be replayed, or a truncation mark could be missing from it.
568
+ const check = rebuildStore(provenance, storeLimits.limits);
569
+ for (const rec of edges) {
570
+ const e = check.getEdge(rec.edgeId);
571
+ if (!e || !same(rec, edgeRecordOf(check, e))) return { ok: false, reason: `the supplied store and the provenance export disagree on edge ${rec.edgeId}` };
572
+ }
573
+ const budget = { ...result.budget }; delete budget.expansionsUsed;
574
+ const config = { pathQuery: budget, storeLimits: storeLimits.limits, ...(witness_bundle_isObj(o.analysisConfig) ? { extra: o.analysisConfig } : {}) };
575
+ const analysisConfigDigest = witness_bundle_sha((0,evidence_bundle/* canonicalJson */.dj)(config));
576
+ const graphDigest = (0,export_json.computeGraphDigest)(graph);
577
+
578
+ const files = [...new Set([srcNode, sinkNode, ...edges.flatMap((e) => [store.getNode(e.fromNodeId), store.getNode(e.toNodeId)])].map((n) => fileOfScope(n?.scope)).filter(Boolean))].sort();
579
+ const sources = []; const unboundSourceFiles = [];
580
+ for (const f of files) {
581
+ const text = o.sourceBlobs?.[f];
582
+ if (typeof text === 'string' || Buffer.isBuffer(text)) {
583
+ const buf = Buffer.isBuffer(text) ? text : Buffer.from(text, 'utf8');
584
+ sources.push({ file: f, sha256: witness_bundle_sha(buf), bytes: buf.length });
585
+ } else unboundSourceFiles.push(f);
586
+ }
587
+
588
+ const aCompletion = graph.coverage?.analysisCompletion ?? { status: 'complete' };
589
+ const completion = {
590
+ path: { complete: path.complete === true, terminalReason: path.terminal?.reason ?? null, analysisTruncated: path.analysisTruncated === true, ambiguousHopCount: path.ambiguousHopCount ?? 0 },
591
+ query: {
592
+ status: result.status ?? 'partial', queryExhausted: result.status !== 'invalid' && result.truncated === false,
593
+ truncated: result.truncated === true, truncationReasons: witness_bundle_arr(result.truncationReasons),
594
+ modelCoverage: (aCompletion.status === 'complete' && !storeLimits.hit && path.analysisTruncated !== true) ? 'complete' : 'partial',
595
+ negativeClaimAllowed: false, droppedPathCount: result.droppedPathCount ?? 0, enumeratedPathCount: result.enumeratedPathCount ?? 0, cyclesClipped: result.cyclesClipped ?? 0,
596
+ },
597
+ analysis: { status: aCompletion.status ?? 'complete', incompleteUnits: witness_bundle_arr(aCompletion.incomplete).length, reasons: [...new Set(witness_bundle_arr(aCompletion.incomplete).flatMap((u) => witness_bundle_arr(u.reasons)))].sort() },
598
+ store: { limitHit: storeLimits.hit === true, hopsRefused: storeLimits.hopsRefused, groupsSkipped: storeLimits.groupsSkipped.length, edgesRefused: storeLimits.edgesRefused },
599
+ };
600
+ completion.overall = deriveOverall(completion);
601
+
602
+ // ---- graph binding, runtime evidence, proofs ----
603
+ let graphBinding = null; let pathGraphEdgeIds = [];
604
+ if (o.graphBinding) {
605
+ const flow = witness_bundle_arr(graph.flows).find((f) => f.id === o.graphBinding.flowId);
606
+ if (!flow) return { ok: false, reason: `flow ${o.graphBinding.flowId} is not in the graph` };
607
+ pathGraphEdgeIds = witness_bundle_arr(flow.edgeIds).slice();
608
+ graphBinding = { flowId: flow.id, edgeIds: pathGraphEdgeIds, sourceNodeId: flow.source ?? flow.sourceNodeId ?? null, sinkNodeId: flow.sink ?? flow.sinkNodeId ?? null };
609
+ } else if (o.runtime) return { ok: false, reason: 'runtime evidence needs graphBinding.flowId: telemetry maps to DataFlowGraph edges' };
610
+
611
+ const ce = o.controlEvidence ?? null;
612
+ const ceCheck = validateControlEvidence(ce, edges.map((e) => e.edgeId));
613
+ if (!ceCheck.ok) return { ok: false, reason: `control evidence: ${ceCheck.errors.join('; ')}` };
614
+
615
+ const tokenize = o.tokenize ?? (0,identity_token.makeIdentityTokenizer)();
616
+ let runtime;
617
+ if (o.runtime) {
618
+ const groups = sanitizeCausalGroups(o.runtime.causalGroups, { tokenize });
619
+ const expected = sanitizeExpected(o.runtime.expected, tokenize);
620
+ const edgeStates = compactEdgeStates(o.runtime.edgeStates, pathGraphEdgeIds);
621
+ runtime = { consulted: edgeStates.evaluated, inputs: { edgeStates, causalGroups: groups.groups, expected }, droppedFields: groups.droppedFields, rejectedGroups: groups.rejected.length };
622
+ } else runtime = { consulted: false, inputs: null, droppedFields: 0, rejectedGroups: 0 };
623
+ runtime.assessment = runtimeAssessment(runtime, pathGraphEdgeIds);
624
+
625
+ const sinkRef = sinkNode.id;
626
+ const exploitInputs = witness_bundle_arr(o.exploitProofs).map((p) => sanitizeProof(p));
627
+ const exploit = { inputs: exploitInputs, ...bindExploitProofs(exploitInputs, { sinkRef, dataElementId: edges[0].dataElementId, graphDigest, analysisConfigDigest }) };
628
+
629
+ const sourcePath = (0,redact/* redactString */.rd)(srcNode.path ?? ''); const sinkPath = (0,redact/* redactString */.rd)(sinkNode.path ?? '');
630
+ const redactions = (srcNode.path && sourcePath !== srcNode.path ? 1 : 0) + (sinkNode.path && sinkPath !== sinkNode.path ? 1 : 0);
631
+
632
+ const bundle = jsonSafe({
633
+ schema: WITNESS_SCHEMA,
634
+ graphRef: { id: graph.graphId ?? null, digest: graphDigest, analysisConfigDigest },
635
+ query: { direction: 'backward', sinkRef, argumentIndex: null, accessPath: sinkPath || null, contextRef: sinkNode.context ?? null },
636
+ references: {
637
+ source: nodeRef(srcNode), sink: nodeRef(sinkNode),
638
+ field: { dataElementId: edges[0].dataElementId, sourcePath: sourcePath || null, sinkPath: sinkPath || null },
639
+ unboundSourceFiles,
640
+ },
641
+ path: {
642
+ pathId: path.id, nodeIds: witness_bundle_arr(path.nodeIds), hopCount: edges.length, edges,
643
+ terminal: { nodeId: path.terminal?.nodeId ?? null, reason: path.terminal?.reason ?? null, kind: path.terminal?.kind ?? null },
644
+ },
645
+ artifacts: { provenance: { schema: provenance.schema, digest: provenance.digest, hopCount: provenance.hops.length, truncationCount: provenance.truncations.length } },
646
+ sources,
647
+ config,
648
+ transferModels: transferModelFingerprints(),
649
+ controlEvidence: ce,
650
+ completion,
651
+ evidence: buildEvidenceStatuses({ pathComplete: completion.path.complete && completion.overall === 'complete', controlEvidence: ce, runtime: runtime.assessment, exploit }),
652
+ runtime,
653
+ exploit,
654
+ graphBinding,
655
+ redaction: { redactions, identityTokens: 'keyed-hash (idt_)', sessionRefs: 'opaque (sref_)', payloads: 'never recorded' },
656
+ engine: { engineVersion: o.engine?.engineVersion ?? null, bundleSha: o.engine?.bundleSha ?? null, commit: o.engine?.commit ?? null },
657
+ proves: PROVES,
658
+ doesNotProve: DOES_NOT_PROVE,
659
+ });
660
+ return { ok: true, bundle };
661
+ }
662
+
663
+ function sanitizeExpected(e, tokenize) {
664
+ const x = witness_bundle_isObj(e) ? e : {};
665
+ const ref = (v) => (typeof v === 'string' && /^[A-Za-z0-9._:@/-]{1,160}$/.test(v) ? v : null);
666
+ const w = witness_bundle_isObj(x.window) ? { start: typeof x.window.start === 'string' ? x.window.start : null, end: typeof x.window.end === 'string' ? x.window.end : null } : null;
667
+ const tok = typeof x.identityToken === 'string' && /^idt_[0-9a-f]{16}$/.test(x.identityToken) ? x.identityToken : (typeof x.identity === 'string' ? tokenize(x.identity) : null);
668
+ return { environment: ref(x.environment), deploymentRef: ref(x.deploymentRef), deploymentConfigDigest: ref(x.deploymentConfigDigest), identityToken: tok, window: w, maxWindowMs: Number.isFinite(x.maxWindowMs) ? x.maxWindowMs : null };
669
+ }
670
+
671
+ function sanitizeProof(p) {
672
+ if (!witness_bundle_isObj(p)) return { id: null, kind: null };
673
+ const ref = (v) => (typeof v === 'string' && /^[A-Za-z0-9._:@/-]{1,160}$/.test(v) ? v : null);
674
+ return {
675
+ id: ref(p.id), kind: ['proof', 'dnr-hunt', 'incident'].includes(p.kind) ? p.kind : null, sinkRef: ref(p.sinkRef), dataElementId: ref(p.dataElementId),
676
+ graphDigest: ref(p.graphDigest), analysisConfigDigest: ref(p.analysisConfigDigest), note: typeof p.note === 'string' ? (0,redact/* redactString */.rd)(p.note).slice(0, 200) : null,
677
+ };
678
+ }
679
+
680
+ function runtimeAssessment(runtime, pathGraphEdgeIds) {
681
+ if (!runtime.inputs) return assessRuntimeSupport({ pathEdgeIds: pathGraphEdgeIds, edgeStates: null });
682
+ const x = runtime.inputs;
683
+ return assessRuntimeSupport({
684
+ pathEdgeIds: pathGraphEdgeIds, edgeStates: x.edgeStates,
685
+ causalGroups: x.causalGroups, expected: { ...x.expected, maxWindowMs: x.expected.maxWindowMs ?? undefined },
686
+ });
687
+ }
688
+
689
+ // ---- signing ---------------------------------------------------------------------------------------
690
+
691
+ function signedBytes(bundle) {
692
+ const signed = {};
693
+ for (const k of WITNESS_UNSIGNED_KEYS) signed[k] = bundle[k];
694
+ if (bundle.issuance !== undefined) signed.issuance = bundle.issuance;
695
+ return Buffer.from((0,evidence_bundle/* canonicalJson */.dj)(signed), 'utf8');
696
+ }
697
+
698
+ /** Sign with the same Ed25519 key and self-issued trust basis as finding evidence bundles. */
699
+ function signWitnessBundle(bundle, privateKeyPem) {
700
+ const publicKeyPem = external_node_crypto_.createPublicKey(privateKeyPem).export({ type: 'spki', format: 'pem' });
701
+ const withIssuance = { ...bundle, issuance: (0,evidence_bundle/* buildLocalIssuance */.hc)(publicKeyPem) };
702
+ const sig = external_node_crypto_.sign(null, signedBytes(withIssuance), privateKeyPem);
703
+ return { ...withIssuance, signature: { algorithm: 'ed25519', canonicalisation: WITNESS_SCHEMA, value: sig.toString('base64') } };
704
+ }
705
+
706
+ /** Integrity check with a PUBLIC key. Never throws. */
707
+ function verifyWitnessSignature(bundle, publicKeyPem) {
708
+ if (!witness_bundle_isObj(bundle)) return { ok: false, reason: 'bundle is not an object' };
709
+ if (bundle.schema !== WITNESS_SCHEMA) return { ok: false, reason: `unrecognised schema: ${bundle.schema}` };
710
+ const unknown = Object.keys(bundle).filter((k) => !WITNESS_TOP_KEYS.has(k));
711
+ if (unknown.length) return { ok: false, reason: `unrecognised top-level key(s) not covered by the signature: ${unknown.join(', ')}` };
712
+ const missing = WITNESS_UNSIGNED_KEYS.filter((k) => !Object.prototype.hasOwnProperty.call(bundle, k));
713
+ if (missing.length) return { ok: false, reason: `required field(s) missing (a missing field is never read as complete): ${missing.join(', ')}` };
714
+ const sig = bundle.signature;
715
+ if (!sig?.value) return { ok: false, reason: 'bundle is unsigned' };
716
+ if (sig.algorithm !== 'ed25519') return { ok: false, reason: `unsupported algorithm: ${sig.algorithm}` };
717
+ if (!publicKeyPem) return { ok: false, reason: 'no public key supplied' };
718
+ let ok = false;
719
+ try { ok = external_node_crypto_.verify(null, signedBytes(bundle), publicKeyPem, Buffer.from(sig.value, 'base64')); } catch (e) { return { ok: false, reason: `verification error: ${e.message}` }; }
720
+ if (!ok) return { ok: false, reason: 'signature does not match the bundle contents: it was modified after signing' };
721
+ const iss = bundle.issuance;
722
+ if (iss === undefined) return { ok: true, reason: null, trustBasis: 'none-declared', independentlyCertified: false, issuer: null, trustNote: 'No trust basis is declared. Treat it as unattested provenance.' };
723
+ const basis = witness_bundle_isObj(iss) ? evidence_bundle/* TRUST_BASES */.oR[iss.trustBasis] : undefined;
724
+ if (!basis) return { ok: false, reason: `unsupported trust basis: ${JSON.stringify(iss?.trustBasis ?? null)}` };
725
+ if (iss.independentlyCertified !== false) return { ok: false, reason: 'a self-issued bundle may not claim independent certification' };
726
+ let fp; try { fp = (0,evidence_bundle/* keyFingerprint */.yy)(publicKeyPem); } catch (e) { return { ok: false, reason: `verification error: ${e.message}` }; }
727
+ if (iss.issuer?.keyFingerprint !== fp) return { ok: false, reason: 'the declared issuer key does not match the key that verified the signature' };
728
+ return { ok: true, reason: null, trustBasis: iss.trustBasis, independentlyCertified: false, issuer: iss.issuer, trustNote: basis.statement };
729
+ }
730
+
731
+ /** Strict parse: JSON, then the same required-field gate verification uses. */
732
+ function parseWitnessBundle(text) {
733
+ let b; try { b = JSON.parse(text); } catch (e) { return { ok: false, reason: `not JSON: ${e.message}` }; }
734
+ if (!witness_bundle_isObj(b) || b.schema !== WITNESS_SCHEMA) return { ok: false, reason: 'not a path witness bundle' };
735
+ const shape = shapeFailures(b);
736
+ return shape.length ? { ok: false, reason: shape[0].detail } : { ok: true, bundle: b };
737
+ }
738
+
739
+ // ---- replay ----------------------------------------------------------------------------------------
740
+
741
+ function shapeFailures(b) {
742
+ const f = (detail) => [{ code: 'schema', detail }];
743
+ const missing = WITNESS_UNSIGNED_KEYS.filter((k) => !Object.prototype.hasOwnProperty.call(b, k));
744
+ if (missing.length) return f(`required field(s) missing: ${missing.join(', ')}`);
745
+ const c = b.completion;
746
+ if (!witness_bundle_isObj(c) || !witness_bundle_isObj(c.path) || !witness_bundle_isObj(c.query) || !witness_bundle_isObj(c.analysis) || !witness_bundle_isObj(c.store)) return f('completion record is incomplete');
747
+ if (typeof c.path.complete !== 'boolean' || typeof c.query.truncated !== 'boolean' || typeof c.store.limitHit !== 'boolean') return f('completion flags must be booleans');
748
+ if (!['complete', 'partial', 'cancelled', 'unavailable', 'invalid'].includes(c.query.status) || typeof c.analysis.status !== 'string') return f('completion status is missing');
749
+ if (!OVERALL.includes(c.overall)) return f('completion.overall is missing or unknown');
750
+ if (!witness_bundle_isObj(b.path) || !Array.isArray(b.path.edges) || !Array.isArray(b.path.nodeIds)) return f('path record is malformed');
751
+ if (!witness_bundle_isObj(b.evidence) || Object.keys(b.evidence).sort().join() !== [...EVIDENCE_STATUS_KEYS].sort().join()) return f('evidence must hold exactly the four separate statuses and nothing else (no merged score or confidence)');
752
+ const legal = { modeledPath: MODELED_PATH_STATUSES, feasibility: FEASIBILITY_STATUSES, runtimeSupport: RUNTIME_SUPPORT_STATUSES, exploitProof: EXPLOIT_PROOF_STATUSES };
753
+ for (const k of EVIDENCE_STATUS_KEYS) {
754
+ if (!witness_bundle_isObj(b.evidence[k]) || Object.keys(b.evidence[k]).join() !== 'status') return f(`evidence.${k} must be a status only`);
755
+ if (!legal[k].includes(b.evidence[k].status)) return f(`evidence.${k} has an unknown status`);
756
+ }
757
+ return [];
758
+ }
759
+
760
+ /**
761
+ * Offline, bounded replay of a bundle against the artifact set it names.
762
+ *
763
+ * @param {object} bundle
764
+ * @param {{graph:object, provenance:object, sources?:Object<string,string>, config?:object}} artifacts
765
+ * @returns {{ok:boolean, failures:{code:string,detail:string}[], warnings:string[], derived:object}}
766
+ */
767
+ function replayWitness(bundle, artifacts, opts = {}) {
768
+ const bounds = { ...WITNESS_BOUNDS, ...opts.bounds };
769
+ const failures = []; const warnings = []; const derived = {};
770
+ const fail = (code, detail) => { if (failures.length < 50) failures.push({ code, detail }); };
771
+ const early = witness_bundle_isObj(bundle) ? shapeFailures(bundle) : [{ code: 'schema', detail: 'bundle is not an object' }];
772
+ if (early.length) return { ok: false, failures: early, warnings, derived };
773
+ if (bundle.schema !== WITNESS_SCHEMA) return { ok: false, failures: [{ code: 'schema', detail: `unrecognised schema ${bundle.schema}` }], warnings, derived };
774
+ const unknown = Object.keys(bundle).filter((k) => !WITNESS_TOP_KEYS.has(k));
775
+ if (unknown.length) fail('schema', `unrecognised top-level key(s): ${unknown.join(', ')}`);
776
+ if (bundle.proves !== PROVES || bundle.doesNotProve !== DOES_NOT_PROVE) fail('schema', 'the proves / doesNotProve statements were altered or dropped');
777
+ if (!witness_bundle_isObj(artifacts) || !witness_bundle_isObj(artifacts.graph) || !witness_bundle_isObj(artifacts.provenance)) return { ok: false, failures: [...failures, { code: 'artifacts', detail: 'graph and provenance artifacts are required to replay' }], warnings, derived };
778
+ if (bundle.path.edges.length === 0 || bundle.path.edges.length > bounds.maxPathEdges) fail('bounds', `path has ${bundle.path.edges.length} edges (allowed 1 to ${bounds.maxPathEdges})`);
779
+ if (witness_bundle_arr(bundle.sources).length > bounds.maxSources) fail('bounds', 'too many source entries');
780
+ const prov = artifacts.provenance;
781
+ if (prov.schema !== PROVENANCE_ARTIFACT_SCHEMA || !Array.isArray(prov.hops) || !Array.isArray(prov.truncations)) {
782
+ fail('artifacts', 'provenance artifact is malformed');
783
+ return { ok: false, failures, warnings, derived };
784
+ }
785
+ if (prov.hops.length > bounds.maxHops) { fail('bounds', 'provenance artifact exceeds the replay hop bound'); return { ok: false, failures, warnings, derived }; }
786
+ if (failures.some((x) => x.code === 'bounds')) return { ok: false, failures, warnings, derived };
787
+
788
+ // 1. artifact and configuration digests
789
+ if ((0,export_json.computeGraphDigest)(artifacts.graph) !== bundle.graphRef.digest) fail('graph-digest', 'the graph artifact does not match the digest the bundle was built against');
790
+ if ((artifacts.graph.graphId ?? null) !== bundle.graphRef.id) fail('graph-id', 'the graph artifact is a different graph than the bundle names');
791
+ if (artifactDigest(prov) !== bundle.artifacts.provenance.digest) fail('provenance-digest', 'the provenance store export does not match the digest the bundle was built against');
792
+ if (witness_bundle_sha((0,evidence_bundle/* canonicalJson */.dj)(bundle.config)) !== bundle.graphRef.analysisConfigDigest) fail('config-digest', 'the recorded configuration does not match its digest');
793
+ if (witness_bundle_isObj(artifacts.config) && witness_bundle_sha((0,evidence_bundle/* canonicalJson */.dj)(artifacts.config)) !== bundle.graphRef.analysisConfigDigest) fail('config-digest', 'the supplied configuration does not match the bundle configuration digest');
794
+ if (!same(bundle.transferModels, transferModelFingerprints())) fail('transfer-model', 'the transfer-model fingerprints differ from this verifier; replay under a different model proves nothing');
795
+
796
+ // 2. source blobs
797
+ let totalBytes = 0;
798
+ for (const s of witness_bundle_arr(bundle.sources)) {
799
+ const text = artifacts.sources?.[s.file];
800
+ if (typeof text !== 'string' && !Buffer.isBuffer(text)) { fail('source-blob-missing', `source blob ${s.file} was not supplied`); continue; }
801
+ const buf = Buffer.isBuffer(text) ? text : Buffer.from(text, 'utf8');
802
+ totalBytes += buf.length;
803
+ if (totalBytes > bounds.maxSourceBytes) { fail('bounds', 'source blobs exceed the replay byte bound'); break; }
804
+ if (witness_bundle_sha(buf) !== s.sha256) fail('source-blob-altered', `source blob ${s.file} does not match its recorded digest`);
805
+ }
806
+ if (witness_bundle_arr(bundle.references.unboundSourceFiles).length) warnings.push(`${bundle.references.unboundSourceFiles.length} referenced source file(s) were not bound by digest`);
807
+
808
+ // 3. rebuild the store and re-derive every edge
809
+ const store = rebuildStore(prov, bundle.config?.storeLimits);
810
+ const lim = store.limitStatus();
811
+ derived.storeLimitHit = lim.hit;
812
+ if (lim.hit !== bundle.completion.store.limitHit) fail('completeness', 'the recorded store-limit state differs from the replayed store');
813
+ const hopIndex = new Map();
814
+ for (const h of prov.hops) {
815
+ if (h.peerScope == null) continue;
816
+ const k = (0,evidence_bundle/* canonicalJson */.dj)([h.scope, h.nodeId, h.dataElementId, h.context, h.peerScope, h.peerContext]);
817
+ hopIndex.set(k, true);
818
+ }
819
+ const edges = bundle.path.edges; const nodeIds = [];
820
+ edges.forEach((rec, i) => {
821
+ if (!witness_bundle_isObj(rec)) { fail('edge-altered', `edge ${i} is not an object`); return; }
822
+ const recomputed = (0,ids/* provenanceEdgeId */.OF)({ ...rec, widenReasons: witness_bundle_arr(rec.widenReasons), lossReasons: witness_bundle_arr(rec.lossReasons) });
823
+ if (recomputed !== rec.edgeId) fail('edge-derivation', `edge ${i} (${rec.edgeId}) does not derive from its recorded fields`);
824
+ const e = store.getEdge(rec.edgeId);
825
+ if (!e) { fail('edge-missing', `edge ${i} (${rec.edgeId}) is not derived by the provenance store export`); return; }
826
+ const truth = edgeRecordOf(store, e);
827
+ if (!same(rec, truth)) fail('edge-altered', `edge ${i} (${rec.edgeId}) differs from the edge the store derives`);
828
+ // call/return binding, against the model and against the carrying hops
829
+ const b = truth.binding;
830
+ if (b.direction === 'invalid') fail('binding', `edge ${i} has both endpoints in a callee namespace`);
831
+ if (truth.crossScope !== (b.direction === 'call' || b.direction === 'return')) fail('binding', `edge ${i} crossScope disagrees with its binding direction`);
832
+ if (b.direction === 'call' || b.direction === 'return') {
833
+ const k = (0,evidence_bundle/* canonicalJson */.dj)([b.callerScope, b.siteNodeId, truth.dataElementId, b.callerContext, b.calleeScope, b.calleeContext]);
834
+ if (!hopIndex.has(k)) fail('binding', `edge ${i}: no recorded hop carries the ${b.direction} binding to ${b.calleeScope}`);
835
+ }
836
+ if (!same(rec.binding, b)) fail('binding', `edge ${i} call-site binding was altered`);
837
+ if (i === 0) nodeIds.push(rec.fromNodeId);
838
+ if (i > 0 && edges[i - 1].toNodeId !== rec.fromNodeId) fail('path-chain', `edge ${i} does not continue from edge ${i - 1}`);
839
+ nodeIds.push(rec.toNodeId);
840
+ });
841
+ if (!same(nodeIds, bundle.path.nodeIds)) fail('path-chain', 'recorded node sequence does not follow from the edge sequence');
842
+ const last = nodeIds[nodeIds.length - 1];
843
+ if ((0,ids/* pathId */.lH)({ startNodeId: last, edgeIds: edges.map((e) => e.edgeId) }) !== bundle.path.pathId) fail('path-chain', 'path id does not derive from the edge sequence');
844
+
845
+ // 4. references
846
+ const src = store.getNode(nodeIds[0]); const sink = store.getNode(last);
847
+ if (!same(nodeRef(src), bundle.references.source) || !same(nodeRef(sink), bundle.references.sink)) fail('references', 'source or sink reference does not match the replayed store');
848
+ if (bundle.query.sinkRef !== last || bundle.query.contextRef !== (sink?.context ?? null)) fail('references', 'query sink or context reference does not match the replayed path');
849
+ if (bundle.references.field.dataElementId !== edges[0]?.dataElementId) fail('references', 'field reference does not match the path');
850
+ if (src && bundle.references.field.sourcePath !== ((0,redact/* redactString */.rd)(src.path ?? '') || null)) fail('references', 'source access path does not match the replayed store');
851
+ if (sink && bundle.references.field.sinkPath !== ((0,redact/* redactString */.rd)(sink.path ?? '') || null)) fail('references', 'sink access path does not match the replayed store');
852
+ const files = new Set([src, sink, ...nodeIds.map((id) => store.getNode(id))].map((n) => fileOfScope(n?.scope)).filter(Boolean));
853
+ const covered = new Set([...witness_bundle_arr(bundle.sources).map((s) => s.file), ...witness_bundle_arr(bundle.references.unboundSourceFiles)]);
854
+ if (!same([...files].sort(), [...covered].sort())) fail('source-blob-missing', 'the source files named by the path scopes are not all bound or declared unbound');
855
+
856
+ // 5. completion, re-derived
857
+ if (src) {
858
+ const reason = terminalReasonFor(store, { nodeIds }, bundle.config?.pathQuery?.maxDepth);
859
+ derived.terminalReason = reason;
860
+ if (bundle.path.terminal.reason !== reason || bundle.completion.path.terminalReason !== reason) fail('completeness', `terminal reason is ${reason} on replay, not ${bundle.path.terminal.reason}`);
861
+ const pathComplete = reason === 'origin';
862
+ if (bundle.completion.path.complete !== pathComplete) fail('completeness', 'path completeness was altered: the path ends at a non-origin node');
863
+ const anyTruncated = edges.some((_, i) => store.getEdge(edges[i].edgeId)?.truncated) || nodeIds.some((id) => store.getNode(id)?.truncated);
864
+ if (anyTruncated && bundle.completion.analysis.status === 'complete') fail('completeness', 'a node or edge on the path is marked truncated but the analysis is recorded complete');
865
+ if (prov.truncations.length > 0 && bundle.completion.analysis.status === 'complete') fail('completeness', 'the provenance export records incomplete analysis units but the bundle claims complete');
866
+ const overall = deriveOverall({ ...bundle.completion, path: { ...bundle.completion.path, complete: pathComplete && bundle.completion.path.complete } });
867
+ derived.overall = overall;
868
+ if (overall !== bundle.completion.overall) fail('completeness', `overall completeness is ${overall} on replay, not ${bundle.completion.overall}`);
869
+ if (bundle.completion.query.negativeClaimAllowed !== false) fail('completeness', 'a witness cannot carry a negative claim');
870
+ }
871
+
872
+ // 6. graph binding, control evidence, runtime, proofs, evidence statuses
873
+ const gb = bundle.graphBinding;
874
+ if (gb) {
875
+ const flow = witness_bundle_arr(artifacts.graph.flows).find((f) => f.id === gb.flowId);
876
+ if (!flow) fail('graph-binding', `flow ${gb.flowId} is not in the graph artifact`);
877
+ else if (!same(witness_bundle_arr(flow.edgeIds), gb.edgeIds)) fail('graph-binding', 'the flow edge ids differ from the graph artifact');
878
+ const gEdges = new Set(witness_bundle_arr(artifacts.graph.edges).map((e) => e.id));
879
+ if (gb.edgeIds.some((id) => !gEdges.has(id))) fail('graph-binding', 'a bound graph edge is not in the graph artifact');
880
+ }
881
+ const ceCheck = validateControlEvidence(bundle.controlEvidence, edges.map((e) => e.edgeId));
882
+ if (!ceCheck.ok) fail('control-evidence', ceCheck.errors.join('; '));
883
+ const rt = bundle.runtime;
884
+ const reAssessed = witness_bundle_isObj(rt) ? runtimeAssessment(rt, gb ? gb.edgeIds : []) : null;
885
+ if (!reAssessed || !same(reAssessed, rt.assessment)) fail('runtime-inconsistent', 'the recorded runtime assessment does not follow from its recorded inputs');
886
+ if (rt?.inputs && !gb) fail('runtime-inconsistent', 'runtime inputs without a graph binding');
887
+ const reBound = bindExploitProofs(witness_bundle_arr(bundle.exploit?.inputs), { sinkRef: last, dataElementId: edges[0]?.dataElementId, graphDigest: bundle.graphRef.digest, analysisConfigDigest: bundle.graphRef.analysisConfigDigest });
888
+ if (!same({ status: reBound.status, bound: reBound.bound, rejected: reBound.rejected }, { status: bundle.exploit?.status, bound: bundle.exploit?.bound, rejected: bundle.exploit?.rejected })) fail('proof-binding', 'the recorded proof binding does not follow from the sink, value and configuration it is bound to');
889
+ if (reAssessed && bundle.completion) {
890
+ const ev = buildEvidenceStatuses({ pathComplete: bundle.completion.path.complete && derived.overall === 'complete', controlEvidence: bundle.controlEvidence, runtime: reAssessed, exploit: reBound });
891
+ if (!same(ev, bundle.evidence)) fail('evidence-statuses', 'the four evidence statuses do not follow from their inputs');
892
+ }
893
+ return { ok: failures.length === 0, failures, warnings, derived };
894
+ }
895
+
896
+ /**
897
+ * Integrity (public key) plus, when artifacts are supplied, offline replay.
898
+ * `verdict`: 'replayed' (both hold), 'integrity-only' (signature valid, no replay attempted), 'invalid'.
899
+ */
900
+ function verifyWitness(bundle, publicKeyPem, artifacts, opts = {}) {
901
+ const signature = verifyWitnessSignature(bundle, publicKeyPem);
902
+ const statement = { proves: PROVES, doesNotProve: DOES_NOT_PROVE };
903
+ if (!signature.ok) return { ok: false, verdict: 'invalid', signature, replay: { performed: false, ok: false, failures: [], warnings: [] }, statement };
904
+ if (!artifacts) return { ok: true, verdict: 'integrity-only', signature, replay: { performed: false, ok: false, failures: [], warnings: [] }, statement };
905
+ const replay = replayWitness(bundle, artifacts, opts);
906
+ return { ok: replay.ok, verdict: replay.ok ? 'replayed' : 'invalid', signature, replay: { performed: true, ...replay }, statement };
907
+ }
908
+
909
+
910
+ /***/ })
911
+
912
+ };