@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.
- package/CHANGELOG.md +15 -0
- package/bin/agentic-security.js +87 -0
- package/dist/{dad58d1113a12c23a142.json → 0b301d45b231f144a1b4.json} +26 -9
- package/dist/1128.index.js +908 -0
- package/dist/1336.index.js +261 -112
- package/dist/1665.index.js +828 -0
- package/dist/9579.index.js +1 -1
- package/dist/agentic-security.mjs +15 -15
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/dist/frontend/src/shell.js +12 -2
- package/package.json +26 -9
- package/src/dataflow/CLAUDE.md +43 -1
- package/src/dataflow/backward.js +42 -7
- package/src/dataflow/cross-service-taint.js +83 -1
- package/src/dataflow/demand-query.js +929 -0
- package/src/dataflow/dep-fingerprint.js +718 -0
- package/src/dataflow/engine.js +48 -6
- package/src/dataflow/ifds-precise.js +22 -0
- package/src/dataflow/incremental-replay.js +118 -0
- package/src/dataflow/incremental-session.js +257 -0
- package/src/dataflow/incremental.js +237 -35
- package/src/dataflow/index.js +25 -94
- package/src/dataflow/summaries.js +72 -6
- package/src/dataflow/tabulation.js +80 -24
- package/src/engine.js +51 -3
- package/src/graph/CLAUDE.md +31 -0
- package/src/graph/adapters/artifact.js +81 -0
- package/src/graph/adapters/crossrepo.js +75 -0
- package/src/graph/adapters/dataflow.js +76 -0
- package/src/graph/adapters/provenance.js +92 -0
- package/src/graph/adapters/semantic.js +89 -0
- package/src/graph/crosswalk-builders.js +152 -0
- package/src/graph/crosswalk.js +146 -0
- package/src/graph/index.js +15 -0
- package/src/graph/observation.js +57 -0
- package/src/graph/refs.js +105 -0
- package/src/graph/registry.js +129 -0
- package/src/graph/relations.js +81 -0
- package/src/graph/scc.js +241 -0
- package/src/graph/view.js +254 -0
- package/src/lineage/CLAUDE.md +71 -8
- package/src/lineage/DESIGN_GRAPH_BUILDER.md +1 -1
- package/src/lineage/DESIGN_PATH_PROVENANCE.md +6 -0
- package/src/lineage/boundary-contract.js +273 -0
- package/src/lineage/completion.js +205 -0
- package/src/lineage/continuation.js +140 -0
- package/src/lineage/control-credit.js +313 -0
- package/src/lineage/correlation.js +199 -0
- package/src/lineage/coverage.js +8 -1
- package/src/lineage/cross-repo-link.js +22 -0
- package/src/lineage/cut-planner.js +548 -0
- package/src/lineage/driver.js +219 -66
- package/src/lineage/engine.js +166 -21
- package/src/lineage/export-json.js +5 -0
- package/src/lineage/graph-builder.js +84 -7
- package/src/lineage/haskell-view.js +59 -0
- package/src/lineage/index.js +3 -0
- package/src/lineage/nix-view.js +35 -0
- package/src/lineage/owned-build.js +77 -0
- package/src/lineage/owned-worker-entry.js +22 -0
- package/src/lineage/owned-worker.js +95 -0
- package/src/lineage/path-evidence.js +123 -0
- package/src/lineage/path-predicates.js +529 -0
- package/src/lineage/path-query.js +189 -20
- package/src/lineage/path-store.js +191 -17
- package/src/lineage/scc-solver.js +186 -0
- package/src/lineage/smt-orchestrator.js +138 -0
- package/src/lineage/source-seeding.js +36 -2
- package/src/lineage/summaries.js +66 -4
- package/src/lineage/value-versions.js +130 -0
- package/src/lineage/witness-selection.js +95 -0
- package/src/pipeline/scan-health.js +18 -0
- package/src/posture/CLAUDE.md +13 -0
- package/src/posture/evidence-bundle.js +1 -1
- package/src/posture/witness-bundle.js +573 -0
- package/src/posture/witness-runtime.js +276 -0
- 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,
|
package/src/posture/CLAUDE.md
CHANGED
|
@@ -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 },
|