@clear-capabilities/agentic-security-scanner 0.183.1 → 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 (85) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/bin/agentic-security.js +88 -0
  3. package/dist/{2a8456e0800aeee871b6.json → 0b301d45b231f144a1b4.json} +27 -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/2923.index.js +17 -9
  8. package/dist/9382.index.js +23 -2
  9. package/dist/9579.index.js +11 -4
  10. package/dist/agentic-security.mjs +15 -15
  11. package/dist/agentic-security.mjs.sha256 +1 -1
  12. package/dist/frontend/src/shell.js +12 -2
  13. package/package.json +27 -9
  14. package/src/context-intelligence/controls.js +15 -0
  15. package/src/context-intelligence/reachability.js +8 -2
  16. package/src/dataflow/CLAUDE.md +43 -1
  17. package/src/dataflow/backward.js +42 -7
  18. package/src/dataflow/cross-service-taint.js +83 -1
  19. package/src/dataflow/demand-query.js +929 -0
  20. package/src/dataflow/dep-fingerprint.js +718 -0
  21. package/src/dataflow/engine.js +48 -6
  22. package/src/dataflow/ifds-precise.js +22 -0
  23. package/src/dataflow/incremental-replay.js +118 -0
  24. package/src/dataflow/incremental-session.js +257 -0
  25. package/src/dataflow/incremental.js +237 -35
  26. package/src/dataflow/index.js +25 -94
  27. package/src/dataflow/summaries.js +72 -6
  28. package/src/dataflow/tabulation.js +80 -24
  29. package/src/discovery/hunter.js +6 -2
  30. package/src/discovery/index.js +5 -2
  31. package/src/engine.js +63 -3
  32. package/src/graph/CLAUDE.md +31 -0
  33. package/src/graph/adapters/artifact.js +81 -0
  34. package/src/graph/adapters/crossrepo.js +75 -0
  35. package/src/graph/adapters/dataflow.js +76 -0
  36. package/src/graph/adapters/provenance.js +92 -0
  37. package/src/graph/adapters/semantic.js +89 -0
  38. package/src/graph/crosswalk-builders.js +152 -0
  39. package/src/graph/crosswalk.js +146 -0
  40. package/src/graph/index.js +15 -0
  41. package/src/graph/observation.js +57 -0
  42. package/src/graph/refs.js +105 -0
  43. package/src/graph/registry.js +129 -0
  44. package/src/graph/relations.js +81 -0
  45. package/src/graph/scc.js +241 -0
  46. package/src/graph/view.js +254 -0
  47. package/src/language/haskell-disclosure.js +13 -1
  48. package/src/lineage/CLAUDE.md +71 -8
  49. package/src/lineage/DESIGN_GRAPH_BUILDER.md +1 -1
  50. package/src/lineage/DESIGN_PATH_PROVENANCE.md +6 -0
  51. package/src/lineage/boundary-contract.js +273 -0
  52. package/src/lineage/completion.js +205 -0
  53. package/src/lineage/continuation.js +140 -0
  54. package/src/lineage/control-credit.js +313 -0
  55. package/src/lineage/correlation.js +199 -0
  56. package/src/lineage/coverage.js +8 -1
  57. package/src/lineage/cross-repo-link.js +22 -0
  58. package/src/lineage/cut-planner.js +548 -0
  59. package/src/lineage/driver.js +219 -66
  60. package/src/lineage/engine.js +166 -21
  61. package/src/lineage/export-json.js +5 -0
  62. package/src/lineage/graph-builder.js +84 -7
  63. package/src/lineage/haskell-view.js +59 -0
  64. package/src/lineage/index.js +3 -0
  65. package/src/lineage/nix-view.js +35 -0
  66. package/src/lineage/observation-adapters.js +17 -9
  67. package/src/lineage/owned-build.js +77 -0
  68. package/src/lineage/owned-worker-entry.js +22 -0
  69. package/src/lineage/owned-worker.js +95 -0
  70. package/src/lineage/path-evidence.js +123 -0
  71. package/src/lineage/path-predicates.js +529 -0
  72. package/src/lineage/path-query.js +189 -20
  73. package/src/lineage/path-store.js +191 -17
  74. package/src/lineage/scc-solver.js +186 -0
  75. package/src/lineage/smt-orchestrator.js +138 -0
  76. package/src/lineage/source-seeding.js +36 -2
  77. package/src/lineage/summaries.js +66 -4
  78. package/src/lineage/value-versions.js +130 -0
  79. package/src/lineage/witness-selection.js +95 -0
  80. package/src/pipeline/scan-health.js +19 -0
  81. package/src/posture/CLAUDE.md +13 -0
  82. package/src/posture/evidence-bundle.js +1 -1
  83. package/src/posture/witness-bundle.js +573 -0
  84. package/src/posture/witness-runtime.js +276 -0
  85. package/src/report/index.js +2 -0
@@ -0,0 +1,95 @@
1
+ // witness-selection.js: which paths to DISPLAY, and what may be claimed from
2
+ // them (DAG-07 R07.8).
3
+ //
4
+ // Two jobs that must never be confused:
5
+ // 1. rankWitnesses: choose a small, diverse sample of the enumerated paths
6
+ // to show. Diversity is over source, sink, data class, trust boundary,
7
+ // control state and ambiguity. This is presentation only.
8
+ // 2. assessAllPathsProtected: decide whether "every path is protected" may
9
+ // be said. It looks at EVERY enumerated path, displayed or not, and it
10
+ // refuses outright when the enumeration itself was truncated, so a
11
+ // sample that happens to contain only protected paths can never produce
12
+ // the claim. A hidden unprotected path refutes the claim.
13
+ //
14
+ // Pure: no store access, no engine imports.
15
+
16
+ export const DIVERSITY_DIMENSIONS = Object.freeze(['source', 'sink', 'dataClass', 'trustBoundary', 'controlState', 'ambiguity']);
17
+
18
+ /**
19
+ * Attribute values for one path. `attrsOf(path)` may supply `dataClass` and
20
+ * `controlState` ('protected' | 'unprotected' | 'unknown'); source, sink,
21
+ * boundary and ambiguity come from the path itself.
22
+ */
23
+ export function witnessAttributes(path, attrsOf) {
24
+ const extra = typeof attrsOf === 'function' ? (attrsOf(path) ?? {}) : {};
25
+ return {
26
+ source: path.sourceNodeId ?? null,
27
+ sink: path.sinkNodeId ?? null,
28
+ dataClass: extra.dataClass ?? path.dataElementId ?? 'unknown',
29
+ trustBoundary: (path.crossScopeCount ?? 0) > 0 ? 'crosses' : 'local',
30
+ controlState: extra.controlState ?? 'unknown',
31
+ ambiguity: (path.ambiguousHopCount ?? 0) > 0 ? 'ambiguous' : 'correlated',
32
+ };
33
+ }
34
+
35
+ /**
36
+ * Greedy diversity ranking. Each step picks the path adding the most
37
+ * not-yet-shown values across the six dimensions; ties go to the path id so
38
+ * the order is deterministic. Never drops or alters a path: `ranked` is a
39
+ * permutation of the input and `displayed` is its prefix.
40
+ */
41
+ export function rankWitnesses(paths, { limit = 10, attrsOf } = {}) {
42
+ const list = Array.isArray(paths) ? paths : [];
43
+ const cap = Number.isInteger(limit) && limit >= 0 ? limit : 10;
44
+ const seen = Object.fromEntries(DIVERSITY_DIMENSIONS.map((d) => [d, new Set()]));
45
+ const pool = list.map((p) => ({ p, a: witnessAttributes(p, attrsOf) }));
46
+ const ranked = [];
47
+ while (pool.length) {
48
+ let best = -1;
49
+ let bestGain = -1;
50
+ for (let i = 0; i < pool.length; i++) {
51
+ let gain = 0;
52
+ for (const d of DIVERSITY_DIMENSIONS) if (!seen[d].has(pool[i].a[d])) gain += 1;
53
+ if (gain > bestGain || (gain === bestGain && String(pool[i].p.id) < String(pool[best].p.id))) { best = i; bestGain = gain; }
54
+ }
55
+ const [chosen] = pool.splice(best, 1);
56
+ for (const d of DIVERSITY_DIMENSIONS) seen[d].add(chosen.a[d]);
57
+ ranked.push(chosen.p);
58
+ }
59
+ const displayed = ranked.slice(0, cap);
60
+ const coverage = Object.fromEntries(DIVERSITY_DIMENSIONS.map((d) => [d, new Set(displayed.map((p) => witnessAttributes(p, attrsOf)[d])).size]));
61
+ return { ranked, displayed, hiddenCount: ranked.length - displayed.length, coverage };
62
+ }
63
+
64
+ /**
65
+ * May "all paths are protected" be asserted?
66
+ *
67
+ * `enumerated` is every path the query produced (before any display cap);
68
+ * `result` is the reconstructPaths result they came from. The claim is
69
+ * allowed only when the query was exhaustive (status complete, nothing
70
+ * dropped or truncated, at least one path) and every path is protected.
71
+ * `holds` is false when ANY enumerated path, hidden or not, is unprotected
72
+ * (a refutation needs no exhaustiveness), and null when it is merely not
73
+ * established.
74
+ */
75
+ export function assessAllPathsProtected({ result, enumerated, attrsOf }) {
76
+ const paths = Array.isArray(enumerated) ? enumerated : [];
77
+ const stateOf = (p) => (typeof attrsOf === 'function' ? (attrsOf(p)?.controlState ?? 'unknown') : 'unknown');
78
+ const unprotected = paths.filter((p) => stateOf(p) === 'unprotected');
79
+ const unknown = paths.filter((p) => stateOf(p) !== 'protected' && stateOf(p) !== 'unprotected');
80
+ const reasons = [];
81
+ if (!result || result.status !== 'complete') reasons.push('query-not-complete');
82
+ if (result && (result.truncated || result.droppedPathCount > 0 || result.analysisTruncated)) reasons.push('truncated-sample');
83
+ if (result && result.resumed) reasons.push('resumed-page');
84
+ if (paths.length === 0) reasons.push('no-paths');
85
+ if (unknown.length) reasons.push('control-state-unknown');
86
+ if (unprotected.length) {
87
+ return {
88
+ claim: 'all-paths-protected', allowed: false, holds: false,
89
+ reasons: ['refuted-by-unprotected-path'],
90
+ refutingPathId: unprotected[0].id ?? null, unprotectedCount: unprotected.length,
91
+ };
92
+ }
93
+ const allowed = reasons.length === 0;
94
+ return { claim: 'all-paths-protected', allowed, holds: allowed ? true : null, reasons, unprotectedCount: 0 };
95
+ }
@@ -84,6 +84,19 @@ export function computeScanHealth({ scanMeta = null, annotatorErrors = [], engin
84
84
  if (lineageStatus?.failure) {
85
85
  conditions.push(`lineage graph build threw and was skipped: ${lineageStatus.failure}`);
86
86
  }
87
+ if (Array.isArray(lineageStatus?.incomplete) && lineageStatus.incomplete.length) {
88
+ conditions.push(`lineage field analysis stopped early in ${lineageStatus.incomplete.length} function(s) (iteration budget); a missing flow from those functions is not evidence of safety`);
89
+ }
90
+ if (lineageStatus?.completion && lineageStatus.completion.status !== 'complete') {
91
+ // DAG-07 R07.2: construction limits, cancellation, seeding limits and a
92
+ // zero-flow result from incomplete analysis all land here, in terms of
93
+ // the shared envelope, so none of them can be silent.
94
+ const c = lineageStatus.completion;
95
+ conditions.push(`lineage analysis is ${c.status} (${(c.reasons || []).join(', ') || 'no reason recorded'}); absence of a flow is not evidence that none exists`);
96
+ }
97
+ if (Array.isArray(lineageStatus?.degraded) && lineageStatus.degraded.length) {
98
+ conditions.push(`lineage analysis used a coarser summary for ${lineageStatus.degraded.length} call context(s) (context cap or unconverged recursion)`);
99
+ }
87
100
  // FR-203: a detector that threw on at least one file (captured via
88
101
  // FR-201's runDetector isolation) is a real analysis gap distinct from
89
102
  // an ANNOTATOR error above — annotators run post-detection over the
@@ -136,6 +149,8 @@ export function computeScanHealth({ scanMeta = null, annotatorErrors = [], engin
136
149
  reason: deepStatus.reason ?? null,
137
150
  failure: deepStatus.failure ?? null,
138
151
  ...(Array.isArray(deepStatus.incomplete) && deepStatus.incomplete.length ? { incomplete: deepStatus.incomplete } : {}),
152
+ // DAG-05 R05.7: present only when the cross-scan cache was requested; says what was reused, what was recomputed, and why a run was cold.
153
+ ...(deepStatus.incremental ? { incremental: deepStatus.incremental } : {}),
139
154
  }
140
155
  : null,
141
156
  lineageAnalysis: lineageStatus
@@ -144,6 +159,9 @@ export function computeScanHealth({ scanMeta = null, annotatorErrors = [], engin
144
159
  enabled: !!lineageStatus.enabled,
145
160
  reason: lineageStatus.reason ?? null,
146
161
  failure: lineageStatus.failure ?? null,
162
+ ...(lineageStatus.completion ? { completion: lineageStatus.completion } : {}),
163
+ ...(Array.isArray(lineageStatus.incomplete) && lineageStatus.incomplete.length ? { incomplete: lineageStatus.incomplete } : {}),
164
+ ...(Array.isArray(lineageStatus.degraded) && lineageStatus.degraded.length ? { degraded: lineageStatus.degraded } : {}),
147
165
  }
148
166
  : null,
149
167
  annotatorErrorCount: safeAnnotatorErrors.length,
@@ -172,6 +190,7 @@ const FRESHNESS_CONDITION_BUILDERS = {
172
190
  epss: (f) => (f?.stale === true) ? `EPSS exploit-probability data is stale (${f.ageDays} day(s) old)` : null,
173
191
  calibration: (f) => (f?.stale === true) ? `calibration data is stale (${f.ageDays} day(s) old${f.generatedAt ? `, last generated ${f.generatedAt}` : ''})` : null,
174
192
  customRules: (f) => (f?.stale === true) ? `${f.staleFiles.length} custom rule file(s) exceed their configured review interval` : null,
193
+ advisories: (f) => (Number(f?.unanswered) > 0) ? `${f.unanswered} of ${f.componentsChecked} dependencies were not checked against advisory data (${f.source}) — the absence of a vulnerable dependency finding is not evidence that the dependency is safe` : null,
175
194
  compliance: (f) => (Number(f?.stale) > 0) ? `${f.stale} compliance control(s) have stale evidence` : null,
176
195
  };
177
196
 
@@ -1019,6 +1019,19 @@ Measures a deterministic baseline and model arms, and routes tasks from the meas
1019
1019
  * **The router is the policy layer, `routing/decide.js` is the calibrated one.** `policy-router.js` selects from measured scoreboard entries (quality, median cost, median latency, run count) under four policies; `decide.js` keeps quality LOWER bounds on held-out outcomes per stratum. They do not call each other. The router refuses to guess: too few runs, unknown cost under a cost policy, an egress denial or a dataset mismatch excludes a model with the reason, and `threat-model` is blocked because it has no evaluation task.
1020
1020
  * **Digest-checked state.** Runs and the scoreboard live under `.agentic-security/eval-lab/` (registered in `artifact-registry.js`); a record that no longer matches its digest is reported and never used for routing.
1021
1021
 
1022
+ ## Replayable path witnesses and scoped runtime support: `witness-bundle.js`, `witness-runtime.js` (DAG-10)
1023
+
1024
+ A sibling of `evidence-bundle.js` for one source-to-sink PATH rather than one finding. Same Ed25519 key, same self-issued trust basis (`buildLocalIssuance`, newly exported, is the only edit to `evidence-bundle.js`). Pure of the scan path: nothing here is wired into `engine.js`; the one production caller is `verify-attestation` in `bin/agentic-security.js`. Suite: `test/witness-bundle.test.js` (in `test:posture`).
1025
+
1026
+ - **Artifacts, not trust.** `exportProvenanceArtifact({hops, incomplete})` is the provenance store export: the normalized 14-field hop stream plus the truncation marks that never travel as hops (`analysisCompletion.incomplete`). `buildWitnessBundle` records graph digest (`computeGraphDigest`), provenance digest, source blob sha256s, the analysis configuration digest, transfer-model fingerprints, ordered edges (full derivation fields plus call/return binding), sink/source/field/context references and completion records. `replayWitness(bundle, {graph, provenance, sources, config})` is offline and bounded (`WITNESS_BOUNDS`): it rebuilds a `PathStore` from the hop stream and requires every recorded edge to be the edge the store derives, the edges to chain, each call/return binding to match both the store and a carrying hop, the terminal reason and truncation state to re-derive, and every digest to match.
1027
+ - **A valid signature proves integrity only.** The bundle carries `proves`/`doesNotProve` inside the signed bytes and the verifier rejects a bundle that drops or edits them. Transfer-model fingerprints are computed by RUNNING the half-edge classifiers on fixed probe hops (a minified bundle and the source tree agree); replay under a different fingerprint fails rather than pretending the old model still holds. Replay does not validate the model.
1028
+ - **Four statuses, never merged** (`witness-runtime.js`): `modeledPath`, `feasibility` (the nullable DAG-04 `controlEvidence` slot; `not_assessed` until it exists), `runtimeSupport`, `exploitProof`. `evidence` holds exactly these four keys, each `{status}`; any other key (a score, a confidence) fails replay.
1029
+ - **Runtime support rules.** `path_supported` needs ONE causal group covering every path edge with compatible environment, deployment, time window, identity scope, order and unambiguous mappings; edges seen in unrelated traces are `edge_partial`. Low-confidence mappings stay `candidate_only`; `no_observation_in_window` and `not_consulted` are different and neither is evidence of safety; a denied observation is `blocked_attempt` and proves only that attempt. The closed-world `RuntimeObservation` contract has no trace or session field, so causal groups are caller-supplied: from the observation store alone the strongest answer is `edge_partial`.
1030
+ - **Proof binding.** An external proof reference must name this bundle's sink node, data element, graph digest and configuration digest; a stale or mismatched one is listed under `exploit.rejected` with reasons and is never re-attached.
1031
+ - **Partial stays partial.** Completion fields are required (a missing one is invalid, never "complete"), the bundle is JSON-normalized at build so serialization cannot change it, and replay re-derives the path terminal, the truncation marks and `overall`.
1032
+ - **Redaction.** Identities become `idt_` tokens (`lineage/identity-token.js`), trace/session ids become `sref_` references, free text goes through `mcp/redact.js`, unknown input fields are dropped and only counted.
1033
+ - **Limits.** Building needs the hop stream, which a scan does not persist; there is no `attest --witness` yet. Source files are bound only when the caller supplies their text (the rest are listed `unboundSourceFiles` and replay warns). The graph-edge to provenance-edge relation is the flow binding (`graphBinding.flowId`), checked for existence, not re-derived. Cross-service replay depends on DAG-08.
1034
+
1022
1035
  ## Gotchas
1023
1036
 
1024
1037
  - The seed `calibration-seed.json` is small (n < 30 for several families). Don't treat it as a held-out set — that's `holdout-eval.js`'s job, against an externally-supplied JSONL.
@@ -77,7 +77,7 @@ export function keyFingerprint(publicKeyPem) {
77
77
  }
78
78
 
79
79
  /** Issuance block for a locally generated key. */
80
- function buildLocalIssuance(publicKeyPem) {
80
+ export function buildLocalIssuance(publicKeyPem) {
81
81
  const fp = keyFingerprint(publicKeyPem);
82
82
  return {
83
83
  issuer: { id: `local-install:${fp.slice(0, 16)}`, kind: 'local-install', keyFingerprint: fp },