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

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