@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
@@ -18,6 +18,7 @@ import * as ids from './ids.js';
18
18
  import { classifyDataElementName } from './classification.js';
19
19
  import { analyzeNixSecrets } from '../language/nix-secrets.js';
20
20
  import { analyzeNixPersonalData, destinationOf } from '../language/nix-privacy.js';
21
+ import { analyzeNixScripts } from '../language/nix-script-taint.js';
21
22
 
22
23
  export const NIX_VIEW_VERSION = 'nix-lineage/1';
23
24
 
@@ -196,3 +197,37 @@ export function contributeNixConfigLineage(graph, files, opts = {}) {
196
197
  }
197
198
  return result;
198
199
  }
200
+
201
+ // ── DAG-08: configuration and generated-script scope carried through links ─────────────────────────────────────────────────
202
+ // A Nix value that reaches a generated script is STATIC CONFIGURATION evidence. It carries both the original Nix location and the
203
+ // generated-script location, and it stays that way when a cross-repo link or a boundary contract refers to it. It does not become
204
+ // an application-runtime value by name, by shared attribute or by being declared: only an explicit binding that a verifier has
205
+ // checked (a named verifier and an application site) lets a record say the value enters application runtime.
206
+ export const NIX_SCOPE = Object.freeze({ language: 'nix', kind: 'configuration', scope: 'static-configuration', runtime: false });
207
+
208
+ /** Script-level Nix findings as scope records that keep the original and the generated location side by side. */
209
+ export function nixGeneratedScriptScopes(files) {
210
+ const r = analyzeNixScripts({ files });
211
+ return (r.findings || []).filter((f) => f.originalLocation).map((f) => ({
212
+ rule: f.rule, attrPath: f.attrPath || null, file: f.file,
213
+ originalLocation: { file: f.file, line: f.originalLocation.startLine, column: f.originalLocation.startColumn },
214
+ generatedLocation: f.generatedLocation ? { generated: true, line: f.generatedLocation.line, column: f.generatedLocation.startColumn } : null,
215
+ origins: (f.origins || []).map((o) => ({ kind: o.kind, detail: o.detail })),
216
+ scope: NIX_SCOPE, application: { entersRuntime: false, binding: null, observed: false },
217
+ }));
218
+ }
219
+
220
+ /**
221
+ * Decide whether a Nix scope record may claim to enter application runtime. It may only with an explicit binding that names the
222
+ * application site (file, function) and a verifier (`verified: true` plus `verifiedBy`). Anything less keeps the static-configuration
223
+ * scope and says why. A matching name is never a binding, and even a verified binding is a declaration, never an observation.
224
+ */
225
+ export function bindNixValueToApplication(record, binding) {
226
+ const base = { ...record, scope: NIX_SCOPE };
227
+ const ok = binding && binding.verified === true && typeof binding.verifiedBy === 'string' && binding.verifiedBy.length > 0
228
+ && binding.site && typeof binding.site.file === 'string' && typeof binding.site.function === 'string';
229
+ if (!ok) {
230
+ return { ...base, application: { entersRuntime: false, binding: null, observed: false, reason: binding ? 'binding is not explicit and verified (needs verified: true, verifiedBy and an application site)' : 'no binding declared; the value is static configuration only' } };
231
+ }
232
+ return { ...base, application: { entersRuntime: true, binding: { site: { file: binding.site.file, function: binding.site.function }, verifiedBy: binding.verifiedBy }, observed: false, reason: 'explicit verified binding; this is a declaration, not an observed runtime flow' } };
233
+ }
@@ -0,0 +1,77 @@
1
+ // owned-build.js: run the lineage graph build under an owned worker with a
2
+ // deadline and a watchdog (DAG-07 R07.7).
3
+ //
4
+ // The in-process build (index.js buildLineageGraph) is unchanged and remains
5
+ // the default. This module is the opt-in path: it sends the file contents to a
6
+ // worker thread this module owns, which rebuilds the IR and call graph and runs
7
+ // the same buildLineageGraph with a cooperative stop signal. Two outcomes:
8
+ // - the field analysis polls the stop flag, finishes the function it is in and
9
+ // returns a graph whose scanHealth.status is 'cancelled' (cooperative);
10
+ // - a worker that never polls (or is stuck in a phase that cannot be stopped,
11
+ // such as parsing) is terminated by the watchdog `ackGraceMs` after the
12
+ // deadline, and the result is {status:'cancelled', terminatedByWatchdog}.
13
+ // Either way the caller is released and the worker thread is gone
14
+ // (`exited` resolves when it is).
15
+ //
16
+ // Enabled by opts.deadlineMs or AGENTIC_SECURITY_LINEAGE_DEADLINE_MS. Not set
17
+ // means not used, and nothing about the default path changes.
18
+
19
+ import { fileURLToPath } from 'node:url';
20
+ import { runOwnedWorker } from './owned-worker.js';
21
+ import { buildLineageGraph } from './index.js';
22
+ import { buildProjectIR, buildProjectIRAsync } from '../ir/index.js';
23
+
24
+ const SELF = fileURLToPath(import.meta.url);
25
+
26
+ /** Resolve the configured deadline in ms, or null when the owned path is off. */
27
+ export function resolveLineageDeadlineMs(opts = {}, env = process.env) {
28
+ const raw = opts.deadlineMs !== undefined ? opts.deadlineMs : env.AGENTIC_SECURITY_LINEAGE_DEADLINE_MS;
29
+ const n = Number(raw);
30
+ return Number.isFinite(n) && n > 0 ? n : null;
31
+ }
32
+
33
+ /**
34
+ * Worker body. Called as fn(spec, { shouldStop }) by owned-worker-entry.js.
35
+ * `spec.opts` must be structured-cloneable (no functions).
36
+ */
37
+ export async function lineageWorkerBody(spec, ctl) {
38
+ const fc = spec.fileContents || {};
39
+ const hasJava = Object.keys(fc).some((f) => /\.java$/i.test(f));
40
+ const { perFile, callGraph } = hasJava ? await buildProjectIRAsync(fc) : buildProjectIR(fc);
41
+ const signal = { get aborted() { return !!(ctl && ctl.shouldStop && ctl.shouldStop()); } };
42
+ return buildLineageGraph(callGraph, { ...(spec.opts || {}), perFile, fileContents: fc, signal });
43
+ }
44
+
45
+ /**
46
+ * @param {Object<string,string>} fileContents
47
+ * @param {object} [opts] buildLineageGraph options that survive structured clone,
48
+ * plus deadlineMs, ackGraceMs, signal (AbortSignal), and the test seam `worker`
49
+ * ({modulePath, exportName}) which replaces the worker body.
50
+ * @returns {{done:Promise<object>, exited:Promise<void>}} done resolves to the
51
+ * buildLineageGraph result shape, plus `cancelled` and, when the watchdog ended
52
+ * the worker, `terminatedByWatchdog` with status 'cancelled' and graph null.
53
+ */
54
+ export function buildLineageGraphOwned(fileContents, opts = {}) {
55
+ const t0 = Date.now();
56
+ // onProgress and perFile cannot cross the thread boundary; the worker rebuilds the IR.
57
+ // eslint-disable-next-line no-unused-vars
58
+ const { deadlineMs, ackGraceMs, signal, worker, onProgress, perFile, ...rest } = opts;
59
+ const timeoutMs = resolveLineageDeadlineMs({ deadlineMs });
60
+ const spec = worker || { modulePath: SELF, exportName: 'lineageWorkerBody' };
61
+ const run = runOwnedWorker(
62
+ { ...spec, args: [{ fileContents, opts: rest }] },
63
+ { signal, timeoutMs: timeoutMs || undefined, ackGraceMs },
64
+ );
65
+ const done = run.done.then((r) => {
66
+ if (r.ok) return { ...r.result, cancelled: !!r.cancelled, elapsedMs: Date.now() - t0 };
67
+ if (r.status === 'cancelled') {
68
+ return {
69
+ status: 'cancelled', graph: null, transitEvidence: new Map(),
70
+ failure: `lineage build cancelled (${r.reason}); worker terminated by watchdog`,
71
+ cancelled: true, terminatedByWatchdog: true, ackMs: r.ackMs, elapsedMs: Date.now() - t0,
72
+ };
73
+ }
74
+ return { status: 'failed', graph: null, transitEvidence: new Map(), failure: r.error, cancelled: false, elapsedMs: Date.now() - t0 };
75
+ });
76
+ return { done, exited: run.exited };
77
+ }
@@ -0,0 +1,22 @@
1
+ // Worker entry for owned-worker.js (DAG-07 R07.7). Same shape as
2
+ // pipeline/analyzer-worker.js, plus a shared cancellation flag the work can
3
+ // poll even from inside a synchronous loop (a posted message could not be
4
+ // delivered to a busy thread). The export is called as
5
+ // fn(...args, { shouldStop }).
6
+ import { parentPort, workerData } from 'node:worker_threads';
7
+
8
+ async function run() {
9
+ const flag = new Int32Array(workerData.flag);
10
+ const shouldStop = () => Atomics.load(flag, 0) === 1;
11
+ try {
12
+ const { modulePath, exportName, args } = workerData;
13
+ const mod = await import(modulePath);
14
+ const fn = mod[exportName];
15
+ if (typeof fn !== 'function') throw new Error(`export "${exportName}" is not a function in ${modulePath}`);
16
+ const result = await fn(...(args || []), { shouldStop });
17
+ parentPort.postMessage({ ok: true, cancelled: shouldStop(), result });
18
+ } catch (e) {
19
+ parentPort.postMessage({ ok: false, error: String((e && e.message) || e) });
20
+ }
21
+ }
22
+ run();
@@ -0,0 +1,95 @@
1
+ // owned-worker.js: run bounded work in a worker thread this module owns, with
2
+ // cooperative cancellation and a watchdog (DAG-07 R07.7).
3
+ //
4
+ // Follows the existing worker convention (pipeline/analyzer-supervisor.js:
5
+ // worker_threads, Worker.terminate() as the only preemptive tool) and adds
6
+ // the two things that module does not have:
7
+ // - cancellation on an AbortSignal, acknowledged through a shared flag the
8
+ // work polls (works from inside a synchronous loop);
9
+ // - a watchdog: if the worker has not acknowledged within `ackGraceMs` of
10
+ // the abort, it is terminated, so a worker that never polls cannot hold
11
+ // the caller past that bound.
12
+ // The returned promise always settles with a structured result, and every
13
+ // resource this function created (worker, timers, abort listener) is released
14
+ // before it settles; `exited` resolves when the worker thread is really gone.
15
+
16
+ import { Worker } from 'node:worker_threads';
17
+ import * as path from 'node:path';
18
+ import { fileURLToPath } from 'node:url';
19
+
20
+ const ENTRY = path.join(path.dirname(fileURLToPath(import.meta.url)), 'owned-worker-entry.js');
21
+
22
+ export const DEFAULT_ACK_GRACE_MS = 500;
23
+
24
+ /**
25
+ * @param {{modulePath:string, exportName:string, args?:Array}} spec
26
+ * @param {{signal?:AbortSignal, timeoutMs?:number, ackGraceMs?:number}} [opts]
27
+ * @returns {{done:Promise<object>, exited:Promise<void>}}
28
+ * done resolves to one of:
29
+ * {ok:true, result, cancelled:false}
30
+ * {ok:true, result, cancelled:true, ackMs} cooperative stop
31
+ * {ok:false, status:'cancelled', terminatedByWatchdog:true, ackMs}
32
+ * {ok:false, error}
33
+ */
34
+ export function runOwnedWorker(spec, opts = {}) {
35
+ const ackGraceMs = Number.isFinite(opts.ackGraceMs) && opts.ackGraceMs > 0 ? opts.ackGraceMs : DEFAULT_ACK_GRACE_MS;
36
+ let resolveExited;
37
+ const exited = new Promise((r) => { resolveExited = r; });
38
+ const done = new Promise((resolve) => {
39
+ if (!spec || !spec.modulePath || !spec.exportName) {
40
+ resolveExited();
41
+ return resolve({ ok: false, error: 'runOwnedWorker requires spec.modulePath and spec.exportName' });
42
+ }
43
+ const flag = new Int32Array(new SharedArrayBuffer(4));
44
+ const worker = new Worker(ENTRY, { workerData: { modulePath: spec.modulePath, exportName: spec.exportName, args: spec.args || [], flag: flag.buffer } });
45
+ worker.once('exit', () => resolveExited());
46
+ let settled = false;
47
+ let cancelAt = null;
48
+ let reason = null;
49
+ let watchdog = null;
50
+ let deadlineTimer = null;
51
+
52
+ const release = () => {
53
+ clearTimeout(watchdog);
54
+ clearTimeout(deadlineTimer);
55
+ if (opts.signal) opts.signal.removeEventListener('abort', onAbort);
56
+ worker.terminate().catch(() => {});
57
+ };
58
+ const settle = (value) => {
59
+ if (settled) return;
60
+ settled = true;
61
+ release();
62
+ resolve(value);
63
+ };
64
+ const requestCancel = (why) => {
65
+ if (settled || cancelAt !== null) return;
66
+ cancelAt = Date.now();
67
+ reason = why;
68
+ Atomics.store(flag, 0, 1);
69
+ watchdog = setTimeout(() => {
70
+ settle({ ok: false, status: 'cancelled', reason: why, terminatedByWatchdog: true, ackMs: Date.now() - cancelAt });
71
+ }, ackGraceMs);
72
+ };
73
+ function onAbort() { requestCancel('aborted'); }
74
+
75
+ worker.once('message', (msg) => {
76
+ if (msg && msg.ok) {
77
+ const wasCancelled = !!msg.cancelled || cancelAt !== null;
78
+ settle(wasCancelled
79
+ ? { ok: true, result: msg.result, cancelled: true, reason, ackMs: cancelAt === null ? 0 : Date.now() - cancelAt }
80
+ : { ok: true, result: msg.result, cancelled: false });
81
+ } else {
82
+ settle({ ok: false, error: (msg && msg.error) || 'worker failed' });
83
+ }
84
+ });
85
+ worker.once('error', (e) => settle({ ok: false, error: String((e && e.message) || e) }));
86
+
87
+ if (opts.signal) {
88
+ if (opts.signal.aborted) onAbort(); else opts.signal.addEventListener('abort', onAbort, { once: true });
89
+ }
90
+ if (Number.isFinite(opts.timeoutMs) && opts.timeoutMs > 0) {
91
+ deadlineTimer = setTimeout(() => requestCancel('deadline'), opts.timeoutMs);
92
+ }
93
+ });
94
+ return { done, exited };
95
+ }
@@ -0,0 +1,123 @@
1
+ //
2
+ // path-evidence.js: the DAG-04 evidence for witnesses, assembled (R04.1 to R04.6).
3
+ //
4
+ // One entry point per use:
5
+ // * evaluatePathEvidence one reconstructed path: conditions + feasibility + control state.
6
+ // * summarizeFlowEvidence a group of paths that share one graph flow: alternatives stay distinct,
7
+ // infeasible-under-model paths are listed (never silently dropped), and the
8
+ // all-paths claim goes through assessAllPathsProtectedWithControls.
9
+ // * buildControlEvidence the `controlEvidence` slot of a witness bundle (posture/witness-bundle.js).
10
+ //
11
+ // Everything here is opt-in. Nothing runs, and no field is added to any output, unless a caller (or the
12
+ // graph builder with `pathEvidence: true` / AGENTIC_SECURITY_LINEAGE_PATH_PREDICATES=1) asks for it.
13
+
14
+ import crypto from 'node:crypto';
15
+ import { collectWitnessConditions, evaluateWitnessConditions, PATH_PREDICATE_VERSION, SATISFIABILITY_NOTE } from './path-predicates.js';
16
+ import { evaluateWitnessConditionsAsync } from './smt-orchestrator.js';
17
+ import { creditPath, assessAllPathsProtectedWithControls, CONTROL_CREDIT_VERSION } from './control-credit.js';
18
+
19
+ export const PATH_EVIDENCE_VERSION = 'path-evidence/1';
20
+
21
+ export function pathEvidenceEnabled(opts) {
22
+ if (opts && opts.pathEvidence !== undefined) return Boolean(opts.pathEvidence);
23
+ return process.env.AGENTIC_SECURITY_LINEAGE_PATH_PREDICATES === '1';
24
+ }
25
+
26
+ const sha12 = (s) => crypto.createHash('sha256').update(s).digest('hex').slice(0, 12);
27
+
28
+ function assemble(path, conditions, feasibility, credit) {
29
+ const refsByEdge = conditions.edgeGuards;
30
+ return {
31
+ version: PATH_EVIDENCE_VERSION,
32
+ pathId: path.id ?? null,
33
+ conditions, feasibility, credit,
34
+ controlState: credit.controlState,
35
+ edgeGuards: refsByEdge,
36
+ controlRefs: credit.credits.map((c) => ({ edgeId: c.edgeId, ref: `ctl:${sha12(`${c.scope}|${c.nodeId}|${c.callee}|${c.sinkFamily}`)}` })),
37
+ };
38
+ }
39
+
40
+ /** Synchronous evidence for one reconstructed path (abstract domains only). */
41
+ export function evaluatePathEvidence({ path, nodeOf, fnOf, sinkFamily = null, predicateOpts } = {}) {
42
+ const hops = path?.hops ?? [];
43
+ const conditions = collectWitnessConditions(hops, { fnOf });
44
+ const feasibility = evaluateWitnessConditions(conditions, predicateOpts);
45
+ const credit = creditPath({ hops, nodeOf, fnOf, sinkFamily, truncated: path?.complete === false || path?.analysisTruncated === true });
46
+ return assemble(path ?? {}, conditions, feasibility, credit);
47
+ }
48
+
49
+ /** As above, but an `unknown` abstract answer may be sent to an optional solver (awaited, time-bounded, cancellable). */
50
+ export async function evaluatePathEvidenceAsync({ path, nodeOf, fnOf, sinkFamily = null, predicateOpts, solverOpts } = {}) {
51
+ const hops = path?.hops ?? [];
52
+ const conditions = collectWitnessConditions(hops, { fnOf });
53
+ const feasibility = await evaluateWitnessConditionsAsync(conditions, { ...(predicateOpts ?? {}), ...(solverOpts ?? {}) });
54
+ const credit = creditPath({ hops, nodeOf, fnOf, sinkFamily, truncated: path?.complete === false || path?.analysisTruncated === true });
55
+ return assemble(path ?? {}, conditions, feasibility, credit);
56
+ }
57
+
58
+ const STATE_RANK = { unprotected: 0, unknown: 1, protected: 2 };
59
+
60
+ /**
61
+ * Summarize every path of one flow group. `evidences` is the per-path output of evaluatePathEvidence.
62
+ * The aggregate control state is the WORST over relevant paths, never the best, and alternatives are
63
+ * listed one by one with their own conjunction of conditions.
64
+ */
65
+ export function summarizeFlowEvidence({ result, paths, evidences, proof = null }) {
66
+ const byId = new Map(evidences.map((e, i) => [paths[i].id, e]));
67
+ const assessed = assessAllPathsProtectedWithControls({ result, enumerated: paths, evidenceOf: (p) => byId.get(p.id), proof });
68
+ const relevant = evidences.filter((e) => e.feasibility.status !== 'infeasible-under-model');
69
+ let worst = null;
70
+ for (const e of relevant) if (worst === null || STATE_RANK[e.controlState] < STATE_RANK[worst]) worst = e.controlState;
71
+ const feasCounts = {};
72
+ for (const e of evidences) feasCounts[e.feasibility.status] = (feasCounts[e.feasibility.status] ?? 0) + 1;
73
+ return {
74
+ version: PATH_EVIDENCE_VERSION,
75
+ semanticsVersion: PATH_PREDICATE_VERSION,
76
+ controlSemanticsVersion: CONTROL_CREDIT_VERSION,
77
+ pathCount: paths.length,
78
+ feasibility: feasCounts,
79
+ controlState: worst ?? 'unknown',
80
+ allPathsProtected: assessed,
81
+ alternatives: evidences.map((e, i) => ({
82
+ pathId: paths[i].id,
83
+ feasibility: e.feasibility.status,
84
+ feasibilityReasons: e.feasibility.reasons,
85
+ controlState: e.controlState,
86
+ predicateRefs: e.conditions.clauses.map((c) => ({ ref: c.ref, polarity: c.polarity, branchNodeId: c.branchNodeId, scope: c.scope, text: c.text, versions: c.versions })),
87
+ })),
88
+ note: SATISFIABILITY_NOTE,
89
+ };
90
+ }
91
+
92
+ /**
93
+ * The `controlEvidence` slot of a witness bundle (posture/witness-bundle.js validates its shape).
94
+ * Maps the five-valued feasibility result onto the slot's three statuses and keeps the precise result in
95
+ * `detail`, so `timeout` and `unsupported` stay visible as `unknown` rather than becoming a clean bill.
96
+ */
97
+ export function buildControlEvidence(evidence, { pathEdgeIds }) {
98
+ const f = evidence.feasibility;
99
+ const status = f.status === 'feasible-model' ? 'feasible' : f.status === 'infeasible-under-model' ? 'infeasible' : 'unknown';
100
+ const onPath = new Set(pathEdgeIds);
101
+ const predicateRefs = [];
102
+ for (const c of evidence.conditions.clauses) for (const edgeId of c.atEdgeIds) if (onPath.has(edgeId)) predicateRefs.push({ edgeId, ref: c.ref });
103
+ const controlRefs = evidence.controlRefs.filter((r) => onPath.has(r.edgeId));
104
+ return {
105
+ status,
106
+ predicateRefs: predicateRefs.sort((a, b) => (a.edgeId + a.ref < b.edgeId + b.ref ? -1 : 1)),
107
+ controlRefs,
108
+ detail: {
109
+ semanticsVersion: PATH_PREDICATE_VERSION,
110
+ feasibility: f.status,
111
+ reasons: f.reasons,
112
+ contradiction: f.contradiction ?? null,
113
+ unmodeledPredicates: f.unmodeledPredicates ?? [],
114
+ solver: f.solver ?? null,
115
+ limitations: f.limitations,
116
+ doesNotImply: SATISFIABILITY_NOTE,
117
+ controlState: evidence.controlState,
118
+ controlSemanticsVersion: CONTROL_CREDIT_VERSION,
119
+ controlsRejected: evidence.credit.rejected.map((r) => ({ edgeId: r.edgeId, callee: r.callee, reason: r.reason })),
120
+ controlLimitations: evidence.credit.limitations,
121
+ },
122
+ };
123
+ }