@clear-capabilities/agentic-security-scanner 0.171.0 → 0.172.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 +6 -0
- package/dist/1336.index.js +891 -719
- package/dist/550.index.js +548 -0
- package/dist/7405.index.js +868 -0
- package/dist/9382.index.js +4220 -0
- package/dist/9579.index.js +1197 -0
- package/dist/agentic-security.mjs +2 -2
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +3 -3
- package/src/context-intelligence/CLAUDE.md +21 -0
- package/src/context-intelligence/adapters/existing-ir.js +14 -0
- package/src/context-intelligence/adapters/haskell-hie.js +138 -0
- package/src/context-intelligence/build.js +68 -0
- package/src/context-intelligence/candidates.js +115 -0
- package/src/context-intelligence/canonical.js +95 -0
- package/src/context-intelligence/capabilities.js +33 -0
- package/src/context-intelligence/cli.js +422 -0
- package/src/context-intelligence/config.js +90 -0
- package/src/context-intelligence/controls.js +194 -0
- package/src/context-intelligence/diff.js +97 -0
- package/src/context-intelligence/entrypoints.js +336 -0
- package/src/context-intelligence/evaluation/index.js +228 -0
- package/src/context-intelligence/evaluation/runner.js +139 -0
- package/src/context-intelligence/evaluation/scripted-reader.js +279 -0
- package/src/context-intelligence/files.js +80 -0
- package/src/context-intelligence/graph-schema.js +227 -0
- package/src/context-intelligence/imports.js +123 -0
- package/src/context-intelligence/manifest.js +99 -0
- package/src/context-intelligence/packer.js +224 -0
- package/src/context-intelligence/persist.js +40 -0
- package/src/context-intelligence/prompts/packs.js +82 -0
- package/src/context-intelligence/queue.js +355 -0
- package/src/context-intelligence/reachability.js +182 -0
- package/src/context-intelligence/repo-info.js +16 -0
- package/src/context-intelligence/review.js +321 -0
- package/src/context-intelligence/spans.js +147 -0
- package/src/context-intelligence/store.js +203 -0
- package/src/posture/artifact-registry.js +2 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
edcc9e08f076b2bc50946c512557c15038e337fa568bc490586fc882ca1c11bc agentic-security.mjs
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clear-capabilities/agentic-security-scanner",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.172.0",
|
|
4
4
|
"description": "Scanner engine for the agentic-security Claude Code plugin: SAST, SCA with reachability and CISA KEV, secrets, IaC, Haskell and Nix/NixOS, prompt-injection, MCP/agent-tool audit, authZ analysis, SBOM, compliance attestation, and more.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"optionalDependencies": {
|
|
50
50
|
"tree-sitter-wasms": "0.1.13",
|
|
51
51
|
"web-tree-sitter": "0.20.8",
|
|
52
|
-
"z3-solver": "^5.
|
|
52
|
+
"z3-solver": "^5.3.0"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"@types/node": "^26.6.5",
|
|
@@ -121,7 +121,7 @@
|
|
|
121
121
|
"verification:replay-example": "node ../scripts/verification-replay-example.mjs",
|
|
122
122
|
"test:lifecycle": "node --test test/ci-hang-protection.test.js test/ci-test-shards.test.js test/pre-bash-guard.test.js test/lsp-protocol-smoke.test.js test/ide-surfaces.test.js test/detector-liveness.test.js test/bench-watchdog.test.js test/ci-parity.test.js test/dead-code.test.js test/no-dead-modules.test.js test/no-orphan-scripts.test.js test/no-stray-state.test.js test/tree-integrity.test.js test/stop-hook.test.js test/plugin-self-check.test.js test/skills-registry.test.js test/bodyguard.test.js test/cache-invalidator-guard.test.js test/dispatch-pre-tool.test.js test/dispatch-user-prompt.test.js test/session-start-model-capture.test.js test/sandbox.test.js test/sandbox-escape.test.js test/trust-boundary.test.js test/ci-templates.test.js test/agent-mcp-tools.test.js test/triage-command.test.js test/no-orphan-tests.test.js test/supply-command.test.js test/posture-command.test.js test/labs-command.test.js test/compliance-command.test.js test/check-doc-drift.test.js test/dist-chunks-tracked.test.js && node ../scripts/lint-command-descriptions.mjs test/workflow-confinement.test.js test/check-bundle-source.test.js test/ship.test.js",
|
|
123
123
|
"test:eval": "node --test test/independent-eval.test.js test/realworld-recall.test.js",
|
|
124
|
-
"test:discovery": "node --test test/discovery-partition.test.js test/discovery-lenses.test.js test/discovery-hunter.test.js test/discovery-confirm.test.js test/discovery-disprove.test.js test/discovery-judge.test.js test/discovery-run.test.js test/discovery-wiring.test.js test/discovery-llm-invoke.test.js test/local-only-mode.test.js",
|
|
124
|
+
"test:discovery": "node --test test/discovery-partition.test.js test/discovery-lenses.test.js test/discovery-hunter.test.js test/discovery-confirm.test.js test/discovery-disprove.test.js test/discovery-judge.test.js test/discovery-run.test.js test/discovery-wiring.test.js test/discovery-llm-invoke.test.js test/local-only-mode.test.js test/context-intelligence/build-js.test.js test/context-intelligence/build-python-haskell.test.js test/context-intelligence/hie-adapter.test.js test/context-intelligence/store.test.js test/context-intelligence/review-queue.test.js test/context-intelligence/packer-egress.test.js test/context-intelligence/evaluation.test.js test/context-intelligence/queue-diff.test.js test/context-intelligence/cli.test.js test/context-intelligence/resilience.test.js test/context-intelligence/frameworks.test.js test/context-intelligence/skew-git-rls.test.js test/context-intelligence/git-diff-candidates.test.js",
|
|
125
125
|
"eval:independent": "node ../bench/independent-eval/runner.mjs",
|
|
126
126
|
"eval:independent:gate": "node ../bench/independent-eval/runner.mjs --gate default",
|
|
127
127
|
"smoke": "node bin/agentic-security.js scan test/fixtures/vulnerable-js",
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# context-intelligence/
|
|
2
|
+
|
|
3
|
+
Deterministic Context Intelligence (the `context` command). Opt-in and additive: nothing in the default scan imports this directory. Guide: `docs/guides/context-intelligence.md`; architecture: `docs/architecture/context-intelligence.md`.
|
|
4
|
+
|
|
5
|
+
## Invariants (break one and a test fails)
|
|
6
|
+
|
|
7
|
+
- `build` is deterministic and makes no model or network call. Same snapshot and config give byte-identical manifests.
|
|
8
|
+
- Unresolved is never dropped. A material unresolved edge, a budget omission, a cap or a split prevents `complete`.
|
|
9
|
+
- A control that runs after the sensitive operation is `evaluated_and_ineffective`, never protection.
|
|
10
|
+
- Model output cannot set severity. A citation outside the bundle invalidates the review. Severity comes from the existing confirmation tier.
|
|
11
|
+
- Store writes are atomic and refuse links and unsafe paths. Stored packages are re-checked against their hashes.
|
|
12
|
+
- A job with no verified result is missing coverage, never a clean result.
|
|
13
|
+
- Evaluation is synthetic. Scorecards must say so and must not state an accuracy claim. Holdout use is ledgered.
|
|
14
|
+
|
|
15
|
+
## Where things are
|
|
16
|
+
|
|
17
|
+
See the module table in the architecture doc. Tests: `test/context-intelligence/` (in the `test:discovery` scope). The capability matrix in `capabilities.js` cites test files; `build-js.test.js` checks they exist.
|
|
18
|
+
|
|
19
|
+
## Reuse, do not fork
|
|
20
|
+
|
|
21
|
+
Model calls go through `discovery/llm-invoke.js` and the egress policy. Confirmation, refutation and judging are `discovery/{confirm,disprove,judge}.js`. Grouped intervals come from `posture/evaluation/interval.js`. State paths come from `posture/state-dir.js`.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Adapter: shared IR (`buildProjectIR`) -> symbol-graph/1.
|
|
3
|
+
//
|
|
4
|
+
// The IR is REUSED, not copied. All languages the IR lowers (JS/TS, Python, Java, C#, Kotlin, Go, PHP, Ruby, Rust,
|
|
5
|
+
// C/C++, and Haskell through the shared call graph) arrive through the one call graph, so this adapter has no
|
|
6
|
+
// per-language logic; per-language capability is a matter for the capability matrix, not for branching here.
|
|
7
|
+
import { buildProjectIR } from '../../ir/index.js';
|
|
8
|
+
import { buildSymbolGraph } from '../graph-schema.js';
|
|
9
|
+
|
|
10
|
+
export function graphFromSource(fileContents) {
|
|
11
|
+
const { perFile, callGraph } = buildProjectIR(fileContents);
|
|
12
|
+
const graph = buildSymbolGraph({ fileContents, functions: callGraph.functions, edges: callGraph.edges });
|
|
13
|
+
return { graph, perFile, callGraph };
|
|
14
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Optional compiler-semantic metadata: a READ-ONLY importer for a Haskell declaration/reference export (`hie-export/1`).
|
|
3
|
+
//
|
|
4
|
+
// What this is and is not:
|
|
5
|
+
// * It imports an export that already exists. It never runs GHC, `hiedb`, Cabal or Stack, never builds, and never
|
|
6
|
+
// executes anything from the repository (no Template Haskell, no custom Setup, no plugins). A later explicit
|
|
7
|
+
// metadata-build command would need a sandbox proof that does not exist in this build, so there is none.
|
|
8
|
+
// * `hie-export/1` is the interchange format DCI defines for the data GHC's `-fwrite-ide-info` / `hiedb` hold
|
|
9
|
+
// (declarations with exact spans, and references between them). Producing it from `.hie` files is a separate,
|
|
10
|
+
// explicit, operator step documented in docs/guides/context-intelligence.md; the binary `.hie` files themselves
|
|
11
|
+
// are not parsed here.
|
|
12
|
+
// * The export is untrusted input. It must name the compiler and tool, and it must carry the SHA-256 of every source
|
|
13
|
+
// file it describes; a file whose current hash differs makes the whole adapter DEGRADE (the static graph is kept and
|
|
14
|
+
// the mismatch is reported). Paths must be repository-relative and may not escape; sizes are capped.
|
|
15
|
+
// * Compiler facts are merged as additional, provenance-tagged evidence. A static edge is never silently overwritten:
|
|
16
|
+
// an unresolved static call that the compiler resolves is marked superseded (and counted as closed by the compiler);
|
|
17
|
+
// a static edge that DISAGREES with the compiler stays, and both are flagged as a conflict, which keeps the bundle
|
|
18
|
+
// partial.
|
|
19
|
+
import * as fs from 'node:fs';
|
|
20
|
+
import * as path from 'node:path';
|
|
21
|
+
import { hashOf, safeRelPath, sha256 } from '../canonical.js';
|
|
22
|
+
|
|
23
|
+
export const HIE_EXPORT_SCHEMA = 'hie-export/1';
|
|
24
|
+
export const HIE_EXPORT_FILE = 'hie-export.json';
|
|
25
|
+
const LIMITS = Object.freeze({ maxBytes: 16 * 1024 * 1024, maxDecls: 200000, maxRefs: 1000000, maxName: 300 });
|
|
26
|
+
const SUPPORTED_COMPILER = /^ghc$/i;
|
|
27
|
+
|
|
28
|
+
function degrade(reason, detail) { return { status: 'degraded', reason, detail: detail || null, declarations: [], references: [] }; }
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Load and validate an export. Returns { status: 'ok'|'degraded'|'off', ... }. Never throws, never reads outside the
|
|
32
|
+
* repository, and treats ANY validation failure as a degradation of the adapter, not of the scan.
|
|
33
|
+
*/
|
|
34
|
+
export function loadHieMetadata(rootReal, hieDir, { fileContents }) {
|
|
35
|
+
if (!hieDir) return { status: 'off', reason: 'no-directory-configured', declarations: [], references: [] };
|
|
36
|
+
const dirRel = safeRelPath(hieDir);
|
|
37
|
+
if (!dirRel) return degrade('unsafe-directory');
|
|
38
|
+
const abs = path.join(rootReal, dirRel, HIE_EXPORT_FILE);
|
|
39
|
+
let text;
|
|
40
|
+
try {
|
|
41
|
+
const st = fs.lstatSync(abs);
|
|
42
|
+
if (st.isSymbolicLink() || !st.isFile()) return degrade('not-a-regular-file');
|
|
43
|
+
if (st.size > LIMITS.maxBytes) return degrade('too-large');
|
|
44
|
+
const real = fs.realpathSync(abs);
|
|
45
|
+
if (real !== rootReal && !real.startsWith(rootReal + path.sep)) return degrade('escapes-root');
|
|
46
|
+
text = fs.readFileSync(abs, 'utf8');
|
|
47
|
+
} catch (e) {
|
|
48
|
+
return degrade(e && e.code === 'ENOENT' ? 'export-not-found' : 'unreadable');
|
|
49
|
+
}
|
|
50
|
+
let doc;
|
|
51
|
+
try { doc = JSON.parse(text); } catch { return degrade('invalid-json'); }
|
|
52
|
+
if (!doc || doc.schemaVersion !== HIE_EXPORT_SCHEMA) return degrade('unsupported-schema-version', String(doc && doc.schemaVersion));
|
|
53
|
+
if (!doc.compiler || !SUPPORTED_COMPILER.test(String(doc.compiler.name || '')) || typeof doc.compiler.version !== 'string') return degrade('compiler-not-identified');
|
|
54
|
+
if (!doc.tool || typeof doc.tool.name !== 'string' || typeof doc.tool.version !== 'string') return degrade('tool-not-identified');
|
|
55
|
+
if (!Array.isArray(doc.files) || !Array.isArray(doc.declarations) || !Array.isArray(doc.references)) return degrade('malformed');
|
|
56
|
+
if (doc.declarations.length > LIMITS.maxDecls || doc.references.length > LIMITS.maxRefs) return degrade('over-size-limit');
|
|
57
|
+
|
|
58
|
+
// source snapshot match: every file the export describes must be byte-identical to what is being analysed
|
|
59
|
+
const stale = [];
|
|
60
|
+
const described = new Set();
|
|
61
|
+
for (const f of doc.files) {
|
|
62
|
+
const rel = safeRelPath(String(f && f.path));
|
|
63
|
+
if (!rel || rel !== f.path) return degrade('unsafe-path-in-export', String(f && f.path).slice(0, 80));
|
|
64
|
+
described.add(rel);
|
|
65
|
+
if (typeof fileContents[rel] !== 'string') { stale.push(`${rel}:missing`); continue; }
|
|
66
|
+
if (sha256(fileContents[rel]) !== f.sha256) stale.push(`${rel}:hash-differs`);
|
|
67
|
+
}
|
|
68
|
+
if (stale.length) return degrade('source-snapshot-mismatch', stale.slice(0, 10).join(', '));
|
|
69
|
+
|
|
70
|
+
const declarations = [];
|
|
71
|
+
for (const d of doc.declarations) {
|
|
72
|
+
const rel = safeRelPath(String(d && d.file));
|
|
73
|
+
if (!rel || !described.has(rel) || typeof d.name !== 'string' || d.name.length > LIMITS.maxName) return degrade('malformed-declaration');
|
|
74
|
+
const total = fileContents[rel].split('\n').length;
|
|
75
|
+
if (![d.startLine, d.endLine].every(Number.isInteger) || d.startLine < 1 || d.endLine < d.startLine || d.endLine > total) return degrade('declaration-span-out-of-range', `${rel}:${d.name}`);
|
|
76
|
+
declarations.push({ name: d.name, file: rel, startLine: d.startLine, endLine: d.endLine });
|
|
77
|
+
}
|
|
78
|
+
const references = [];
|
|
79
|
+
for (const r of doc.references) {
|
|
80
|
+
if (!r || typeof r.from !== 'string' || typeof r.to !== 'string' || r.from.length > LIMITS.maxName || r.to.length > LIMITS.maxName || !Number.isInteger(r.line)) return degrade('malformed-reference');
|
|
81
|
+
references.push({ from: r.from, to: r.to, line: r.line });
|
|
82
|
+
}
|
|
83
|
+
declarations.sort((a, b) => (a.file < b.file ? -1 : a.file > b.file ? 1 : 0) || a.startLine - b.startLine || (a.name < b.name ? -1 : 1));
|
|
84
|
+
references.sort((a, b) => (a.from < b.from ? -1 : a.from > b.from ? 1 : 0) || a.line - b.line || (a.to < b.to ? -1 : 1));
|
|
85
|
+
return { status: 'ok', tool: doc.tool, compiler: doc.compiler, declarations, references, exportHash: hashOf({ declarations, references }) };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Merge an imported export into the static graph. Returns { graph, semantic } where `semantic` carries the provenance
|
|
90
|
+
* and the static-versus-compiler difference report.
|
|
91
|
+
*/
|
|
92
|
+
export function mergeHieIntoGraph(graph, meta, fileContents) {
|
|
93
|
+
void fileContents;
|
|
94
|
+
if (meta.status !== 'ok') {
|
|
95
|
+
return { graph, semantic: { source: ['static-ir'], status: meta.status, reason: meta.reason || null, detail: meta.detail || null, diff: null } };
|
|
96
|
+
}
|
|
97
|
+
const idsByName = new Map();
|
|
98
|
+
for (const s of graph.symbols) {
|
|
99
|
+
if (s.kind !== 'function' || s.id.includes('::<anon>@')) continue;
|
|
100
|
+
if (!idsByName.has(s.name)) idsByName.set(s.name, []);
|
|
101
|
+
idsByName.get(s.name).push(s.id);
|
|
102
|
+
}
|
|
103
|
+
const edges = graph.edges.map(e => ({ ...e }));
|
|
104
|
+
const diff = { newSymbols: [], newEdges: 0, resolvedUnknowns: 0, conflicts: [], remainingMaterialUnknowns: 0, unmatchedReferences: 0 };
|
|
105
|
+
|
|
106
|
+
// declarations the static pass never saw are reported (not added: DCI has no source-hash-verified span for them
|
|
107
|
+
// beyond what the export says, and an export-only symbol would be an unverified span)
|
|
108
|
+
const known = new Set(graph.symbols.map(s => `${s.file}\0${s.name}`));
|
|
109
|
+
for (const d of meta.declarations) if (!known.has(`${d.file}\0${d.name}`)) diff.newSymbols.push(`${d.file}::${d.name}`);
|
|
110
|
+
|
|
111
|
+
for (const r of meta.references) {
|
|
112
|
+
const from = (idsByName.get(r.from) || [])[0];
|
|
113
|
+
const to = (idsByName.get(r.to) || [])[0];
|
|
114
|
+
if (!from || !to || (idsByName.get(r.from) || []).length !== 1 || (idsByName.get(r.to) || []).length !== 1) { diff.unmatchedReferences++; continue; }
|
|
115
|
+
const same = edges.filter(e => e.from === from && e.line === r.line);
|
|
116
|
+
const agree = same.find(e => e.to === to);
|
|
117
|
+
if (agree) continue; // static already has it: nothing to add
|
|
118
|
+
const conflicting = same.find(e => e.to && e.to !== to && e.calleeName.split(/[.:]/).pop() === r.to.split(/[.:]/).pop());
|
|
119
|
+
if (conflicting) {
|
|
120
|
+
conflicting.conflict = true;
|
|
121
|
+
diff.conflicts.push({ from, line: r.line, static: conflicting.to, compiler: to });
|
|
122
|
+
edges.push({ from, to, line: r.line, calleeName: r.to, kind: 'call', resolver: 'compiler-enriched', conditionality: 'unknown', resolution: 'compiler', confidence: 'high', unresolvedReason: null, candidates: [], conflict: true });
|
|
123
|
+
diff.newEdges++;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const unresolved = same.find(e => !e.to && (e.calleeName.split(/[.:]/).pop() === r.to.split(/[.:]/).pop()));
|
|
127
|
+
if (unresolved) { unresolved.superseded = true; diff.resolvedUnknowns++; }
|
|
128
|
+
edges.push({ from, to, line: r.line, calleeName: r.to, kind: 'call', resolver: 'compiler-enriched', conditionality: unresolved ? unresolved.conditionality : 'unknown', resolution: 'compiler', confidence: 'high', unresolvedReason: null, candidates: [], conflict: false });
|
|
129
|
+
diff.newEdges++;
|
|
130
|
+
}
|
|
131
|
+
edges.sort((a, b) => (a.from < b.from ? -1 : a.from > b.from ? 1 : 0) || (a.line - b.line) || (a.calleeName < b.calleeName ? -1 : a.calleeName > b.calleeName ? 1 : 0) || ((a.to || '') < (b.to || '') ? -1 : 1));
|
|
132
|
+
diff.remainingMaterialUnknowns = edges.filter(e => !e.to && !e.superseded && e.confidence === 'low').length;
|
|
133
|
+
const body = { schemaVersion: graph.schemaVersion, snapshotHash: graph.snapshotHash, symbols: graph.symbols, edges, imports: graph.imports, semantic: { exportHash: meta.exportHash, compiler: meta.compiler, tool: meta.tool } };
|
|
134
|
+
return {
|
|
135
|
+
graph: { ...body, hash: hashOf(body) },
|
|
136
|
+
semantic: { source: ['static-ir', 'hie-import'], status: 'ok', compiler: meta.compiler, tool: meta.tool, exportHash: meta.exportHash, diff },
|
|
137
|
+
};
|
|
138
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Orchestration: source snapshot -> symbol graph -> entry points -> per-entry-point reachability -> bundles + manifests.
|
|
3
|
+
// Pure with respect to the repository (read-only); persistence is store.js's job. No model, no network.
|
|
4
|
+
import { graphFromSource } from './adapters/existing-ir.js';
|
|
5
|
+
import { validateGraph } from './graph-schema.js';
|
|
6
|
+
import { collectEntrypoints, countDispositions } from './entrypoints.js';
|
|
7
|
+
import { reachFrom } from './reachability.js';
|
|
8
|
+
import { packContext } from './packer.js';
|
|
9
|
+
import { buildManifest } from './manifest.js';
|
|
10
|
+
import { readSnapshot } from './files.js';
|
|
11
|
+
import { repositoryInfo } from './repo-info.js';
|
|
12
|
+
import { loadHieMetadata, mergeHieIntoGraph } from './adapters/haskell-hie.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Build every bundle for a project.
|
|
16
|
+
* opts.config normalised DCI config
|
|
17
|
+
* opts.route optional filter: a label substring or an entrypoint id
|
|
18
|
+
* opts.onEvent optional progress callback (JSON-serialisable events)
|
|
19
|
+
*/
|
|
20
|
+
export async function buildContext(root, opts = {}) {
|
|
21
|
+
const config = opts.config;
|
|
22
|
+
const emit = typeof opts.onEvent === 'function' ? opts.onEvent : () => {};
|
|
23
|
+
const snap = opts.snapshot || await readSnapshot(root, { maxFiles: config.maxFiles });
|
|
24
|
+
if (!snap.ok) return { ok: false, reason: snap.reason, detail: snap };
|
|
25
|
+
const { fileContents } = snap;
|
|
26
|
+
emit({ event: 'snapshot', files: Object.keys(fileContents).length, skipped: snap.skipped.length });
|
|
27
|
+
|
|
28
|
+
let { graph, perFile, callGraph } = graphFromSource(fileContents);
|
|
29
|
+
let semantic = { source: ['static-ir'], status: 'off', diff: null };
|
|
30
|
+
if (config.semanticMetadata.mode === 'import') {
|
|
31
|
+
const meta = loadHieMetadata(snap.rootReal, config.semanticMetadata.haskellHieDir, { fileContents });
|
|
32
|
+
const merged = mergeHieIntoGraph(graph, meta, fileContents);
|
|
33
|
+
graph = merged.graph;
|
|
34
|
+
semantic = merged.semantic;
|
|
35
|
+
}
|
|
36
|
+
const problems = validateGraph(graph, fileContents);
|
|
37
|
+
if (problems.length) return { ok: false, reason: 'graph-invalid', detail: problems.slice(0, 20) };
|
|
38
|
+
emit({ event: 'graph', symbols: graph.symbols.length, edges: graph.edges.length, hash: graph.hash });
|
|
39
|
+
|
|
40
|
+
let entrypoints = await collectEntrypoints({ fileContents, graph, config });
|
|
41
|
+
const counts = countDispositions(entrypoints);
|
|
42
|
+
emit({ event: 'entrypoints', ...counts });
|
|
43
|
+
if (opts.route) entrypoints = entrypoints.filter(e => e.id === opts.route || e.label.includes(opts.route));
|
|
44
|
+
|
|
45
|
+
const repository = snap.commit ? { commit: snap.commit, dirty: false, snapshotHash: snap.snapshotHash } : repositoryInfo(snap.rootReal, snap.snapshotHash);
|
|
46
|
+
const bundles = [];
|
|
47
|
+
for (const ep of entrypoints) {
|
|
48
|
+
if (ep.disposition === 'excluded_by_policy') { bundles.push({ entrypoint: ep, manifest: null, pack: null, reach: null }); continue; }
|
|
49
|
+
const reach = reachFrom({ graph, fileContents, entrypoint: ep, limits: { maxSymbols: config.context.maxSymbols, maxDepth: config.context.maxDepth } });
|
|
50
|
+
const pack = packContext({ entrypoint: ep, reach, graph, fileContents, config: { profile: config.context.profile, maxTokens: config.context.maxTokens, pruneLowValue: config.context.pruneLowValue, tokenizer: opts.tokenizer } });
|
|
51
|
+
const manifest = buildManifest({ entrypoint: ep, reach, pack, graph, fileContents, repository, semanticSource: semantic.source });
|
|
52
|
+
bundles.push({ entrypoint: ep, manifest, pack, reach });
|
|
53
|
+
emit({ event: 'bundle', entrypoint: ep.id, integrity: pack.integrity, packages: pack.packages.length });
|
|
54
|
+
}
|
|
55
|
+
const built = bundles.filter(b => b.manifest);
|
|
56
|
+
return {
|
|
57
|
+
ok: true, root: snap.rootReal, repository, graph, semantic, ir: { perFile, callGraph }, snapshotHash: snap.snapshotHash, entrypoints, bundles, fileContents, skipped: snap.skipped,
|
|
58
|
+
summary: {
|
|
59
|
+
entrypointsFound: counts.total, resolved: counts.resolved, partial: counts.partially_resolved, unresolved: counts.unresolved, excluded: counts.excluded_by_policy,
|
|
60
|
+
bundlesBuilt: built.length,
|
|
61
|
+
complete: built.filter(b => b.manifest.context.integrity === 'complete').length,
|
|
62
|
+
partialBundles: built.filter(b => b.manifest.context.integrity === 'partial').length,
|
|
63
|
+
unavailable: built.filter(b => b.manifest.context.integrity === 'unavailable').length,
|
|
64
|
+
unresolvedEdges: built.reduce((n, b) => n + b.manifest.unknowns.filter(u => u.material).length, 0),
|
|
65
|
+
tokens: built.reduce((n, b) => n + b.pack.packages.reduce((m, p) => m + p.tokensEstimated, 0), 0),
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Review results -> existing discovery candidates -> existing adjudication.
|
|
3
|
+
//
|
|
4
|
+
// A DCI review is a hypothesis. This module normalises an accepted `potential_issue` into the SAME candidate schema the
|
|
5
|
+
// `hunt` pipeline uses, so the existing machinery decides what happens next and nothing about it is reimplemented here:
|
|
6
|
+
//
|
|
7
|
+
// confirm.js routes the candidate back through the taint engine. A novel authorization or business-logic
|
|
8
|
+
// hypothesis the taint engine cannot prove stays `unconfirmed`; it is neither rejected nor promoted.
|
|
9
|
+
// disprove.js the only stage that may remove a candidate, and only by an argued majority.
|
|
10
|
+
// judge.js shapes the finding. Severity comes from the CONFIRMATION TIER (taint-confirmed high, sink-adjacent
|
|
11
|
+
// medium, unconfirmed low). A model's confidence, likelihood or adjectives are never an input, and
|
|
12
|
+
// nothing here can produce `critical`.
|
|
13
|
+
//
|
|
14
|
+
// One shared defect reached through many routes is ONE candidate with many witnesses, not many top-level findings.
|
|
15
|
+
import { sha256 } from './canonical.js';
|
|
16
|
+
import { confirmAll } from '../discovery/confirm.js';
|
|
17
|
+
import { disprovePanel } from '../discovery/disprove.js';
|
|
18
|
+
import { judgeCandidates } from '../discovery/judge.js';
|
|
19
|
+
|
|
20
|
+
const SINK_ROLES = ['data-access', 'database-read', 'database-write', 'state-change', 'sink', 'side-effect'];
|
|
21
|
+
|
|
22
|
+
function normalizeInvariant(s) { return String(s || '').toLowerCase().replace(/[^a-z0-9 ]+/g, ' ').replace(/\s+/g, ' ').trim(); }
|
|
23
|
+
|
|
24
|
+
function primaryEvidence(evidence) {
|
|
25
|
+
for (const role of SINK_ROLES) {
|
|
26
|
+
const hit = evidence.find(e => e.role === role);
|
|
27
|
+
if (hit) return hit;
|
|
28
|
+
}
|
|
29
|
+
return evidence[0];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Build candidates from verified reviews.
|
|
34
|
+
* records [{ review, entrypoints: [{id,label}], pack }] (only status 'completed' + 'potential_issue' qualifies)
|
|
35
|
+
*/
|
|
36
|
+
export function reviewsToCandidates(records) {
|
|
37
|
+
const groups = new Map();
|
|
38
|
+
for (const r of records) {
|
|
39
|
+
const rv = r.review;
|
|
40
|
+
if (!rv || rv.status !== 'completed' || rv.conclusion !== 'potential_issue' || !rv.evidence.length) continue;
|
|
41
|
+
const ev = primaryEvidence(rv.evidence);
|
|
42
|
+
const key = sha256(`${r.pack.id}|${ev.file}|${ev.startLine}|${normalizeInvariant(rv.invariant)}`).slice(0, 12);
|
|
43
|
+
if (!groups.has(key)) {
|
|
44
|
+
groups.set(key, {
|
|
45
|
+
id: key,
|
|
46
|
+
focusAreaId: String(rv.contextHash).slice(0, 12),
|
|
47
|
+
lens: r.pack.lens,
|
|
48
|
+
title: `${r.pack.title}: ${r.entrypoints[0].label}`,
|
|
49
|
+
file: ev.file,
|
|
50
|
+
line: ev.startLine,
|
|
51
|
+
family: r.pack.family,
|
|
52
|
+
cwe: r.pack.cwe,
|
|
53
|
+
rationale: rv.narrative || rv.invariant,
|
|
54
|
+
entryPoint: r.entrypoints.map(e => e.label).join('; '),
|
|
55
|
+
sink: ev.claim || ev.role,
|
|
56
|
+
origin: 'context-intelligence',
|
|
57
|
+
questionPack: rv.questionPack,
|
|
58
|
+
advisoryOnly: rv.advisoryOnly === true,
|
|
59
|
+
witnesses: [],
|
|
60
|
+
sourceSpanEvidence: rv.evidence.map(e => ({ role: e.role, file: e.file, startLine: e.startLine, endLine: e.endLine })),
|
|
61
|
+
scopeIntegrity: { contextIntegrity: rv.contextIntegrity, standalone: rv.standalone, deterministicAgreement: rv.deterministicAgreement, deterministicProtection: rv.deterministicProtection || null },
|
|
62
|
+
reviewStatus: rv.status,
|
|
63
|
+
uncertainties: rv.uncertainties,
|
|
64
|
+
recommendedValidation: rv.recommendedValidation,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
const g = groups.get(key);
|
|
68
|
+
for (const e of r.entrypoints) {
|
|
69
|
+
if (!g.witnesses.some(w => w.entrypointId === e.id)) g.witnesses.push({ entrypointId: e.id, label: e.label, contextHash: rv.contextHash, questionPack: rv.questionPack });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return [...groups.values()].sort((a, b) => (a.id < b.id ? -1 : 1));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Run the existing adjudication. All stages are optional and injected:
|
|
77
|
+
* taintProbe -> confirm.js refute -> disprove.js (needs a governed invoker: `{ llmInvoke | endpoint policy }`)
|
|
78
|
+
* Returns { findings, duplicates, suppressed, refuted, advisory }. `findings` carry a `contextIntelligence` block and
|
|
79
|
+
* `parser: 'DISCOVERY'`, so the report layer keeps them advisory exactly as it does for `hunt`.
|
|
80
|
+
*/
|
|
81
|
+
export async function adjudicate(candidates, opts = {}) {
|
|
82
|
+
const advisory = candidates.filter(c => c.advisoryOnly);
|
|
83
|
+
const real = candidates.filter(c => !c.advisoryOnly);
|
|
84
|
+
const confirmed = await confirmAll(real, { taintProbe: opts.taintProbe });
|
|
85
|
+
let survivors = confirmed;
|
|
86
|
+
let refuted = [];
|
|
87
|
+
if (opts.refute) {
|
|
88
|
+
const panel = await disprovePanel(confirmed, { ...opts.refute, scanRoot: opts.scanRoot });
|
|
89
|
+
survivors = panel.survivors;
|
|
90
|
+
refuted = panel.refuted;
|
|
91
|
+
}
|
|
92
|
+
const byId = new Map(candidates.map(c => [c.id, c]));
|
|
93
|
+
const judged = judgeCandidates(survivors, opts.priorScan, opts.triageFeedback);
|
|
94
|
+
const decorate = (f) => {
|
|
95
|
+
const id = String(f.id).split('-').pop();
|
|
96
|
+
const c = byId.get(id) || survivors.find(s => f.id.endsWith(s.id)) || null;
|
|
97
|
+
if (!c) return f;
|
|
98
|
+
return {
|
|
99
|
+
...f,
|
|
100
|
+
contextIntelligence: {
|
|
101
|
+
origin: 'context-intelligence', questionPack: c.questionPack, witnesses: c.witnesses, witnessCount: c.witnesses.length,
|
|
102
|
+
sourceSpanEvidence: c.sourceSpanEvidence, scopeIntegrity: c.scopeIntegrity, reviewStatus: c.reviewStatus,
|
|
103
|
+
uncertainties: c.uncertainties, recommendedValidation: c.recommendedValidation,
|
|
104
|
+
note: 'A model hypothesis checked for citations, not a proof. Severity comes from the confirmation tier only.',
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
return {
|
|
109
|
+
findings: judged.fresh.map(decorate),
|
|
110
|
+
duplicates: judged.duplicates.map(decorate),
|
|
111
|
+
suppressed: judged.suppressed.map(decorate),
|
|
112
|
+
refuted: refuted.map(c => ({ id: c.id, title: c.title, file: c.file, line: c.line, refutation: c.refutation })),
|
|
113
|
+
advisory: advisory.map(c => ({ id: c.id, title: c.title, file: c.file, line: c.line, questionPack: c.questionPack, witnesses: c.witnesses, rationale: c.rationale })),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Canonical serialization, hashing and path/snapshot safety for Deterministic Context Intelligence.
|
|
3
|
+
//
|
|
4
|
+
// Everything DCI stores or compares goes through this module so that "same checkout + same config => same bytes" is
|
|
5
|
+
// a property of one place. No clock, no randomness, no host path ever enters a hashed value.
|
|
6
|
+
import * as crypto from 'node:crypto';
|
|
7
|
+
import * as fs from 'node:fs';
|
|
8
|
+
import * as path from 'node:path';
|
|
9
|
+
|
|
10
|
+
/** Bump when the canonical encoding below changes; it is mixed into every hash. */
|
|
11
|
+
export const HASH_ALGORITHM = 'sha256/canon-1';
|
|
12
|
+
|
|
13
|
+
export function sha256(text) {
|
|
14
|
+
return crypto.createHash('sha256').update(text).digest('hex');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** CRLF/CR -> LF so a Windows checkout and a Unix one produce the same spans and hashes. */
|
|
18
|
+
export function normalizeText(text) {
|
|
19
|
+
return String(text).replace(/\r\n?/g, '\n');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* JSON with object keys sorted at every depth. Arrays keep their order (callers sort them where order is not
|
|
24
|
+
* meaningful). `undefined` members are dropped, exactly as JSON.stringify does, so a missing and an undefined field hash alike.
|
|
25
|
+
*/
|
|
26
|
+
export function canonicalJson(value) {
|
|
27
|
+
return JSON.stringify(sortKeys(value));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function sortKeys(v) {
|
|
31
|
+
if (Array.isArray(v)) return v.map(sortKeys);
|
|
32
|
+
if (v && typeof v === 'object') {
|
|
33
|
+
const out = {};
|
|
34
|
+
for (const k of Object.keys(v).sort()) {
|
|
35
|
+
if (v[k] !== undefined) out[k] = sortKeys(v[k]);
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
return v;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function hashOf(value) {
|
|
43
|
+
return sha256(`${HASH_ALGORITHM}\n${canonicalJson(value)}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* A repository-relative path in canonical form, or null. Rejects absolute paths, drive letters, NUL, backslash
|
|
48
|
+
* tricks and any `..` segment: a manifest can never name a file outside the repository.
|
|
49
|
+
*/
|
|
50
|
+
export function safeRelPath(p) {
|
|
51
|
+
if (typeof p !== 'string' || p === '' || p.includes('\0')) return null;
|
|
52
|
+
const n = p.replace(/\\/g, '/');
|
|
53
|
+
if (n.startsWith('/') || /^[A-Za-z]:/.test(n)) return null;
|
|
54
|
+
const parts = n.split('/');
|
|
55
|
+
const out = [];
|
|
56
|
+
for (const seg of parts) {
|
|
57
|
+
if (seg === '' || seg === '.') continue;
|
|
58
|
+
if (seg === '..') return null;
|
|
59
|
+
out.push(seg);
|
|
60
|
+
}
|
|
61
|
+
return out.length ? out.join('/') : null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Read one repository file for analysis. Refuses a symlink and anything whose real path leaves the repository root,
|
|
66
|
+
* and refuses non-regular files. Returns { ok, text } or { ok:false, reason }; never throws.
|
|
67
|
+
*/
|
|
68
|
+
export function readRepoFile(rootReal, rel, { maxBytes = 2 * 1024 * 1024 } = {}) {
|
|
69
|
+
const safe = safeRelPath(rel);
|
|
70
|
+
if (!safe) return { ok: false, reason: 'unsafe-path' };
|
|
71
|
+
const abs = path.join(rootReal, safe);
|
|
72
|
+
try {
|
|
73
|
+
const st = fs.lstatSync(abs);
|
|
74
|
+
if (st.isSymbolicLink()) return { ok: false, reason: 'symlink' };
|
|
75
|
+
if (!st.isFile()) return { ok: false, reason: 'not-a-regular-file' };
|
|
76
|
+
if (st.size > maxBytes) return { ok: false, reason: 'too-large' };
|
|
77
|
+
const real = fs.realpathSync(abs);
|
|
78
|
+
if (real !== rootReal && !real.startsWith(rootReal + path.sep)) return { ok: false, reason: 'escapes-root' };
|
|
79
|
+
return { ok: true, text: normalizeText(fs.readFileSync(abs, 'utf8')) };
|
|
80
|
+
} catch (err) {
|
|
81
|
+
return { ok: false, reason: err && err.code ? String(err.code) : 'unreadable' };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Hash of the analysed file set: sorted [path, content hash] pairs. Order of discovery never matters. */
|
|
86
|
+
export function snapshotHashOf(fileContents) {
|
|
87
|
+
const rows = Object.keys(fileContents).sort().map(f => [f, sha256(fileContents[f])]);
|
|
88
|
+
return hashOf(rows);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** 1-based inclusive line slice of already-normalised text. */
|
|
92
|
+
export function sliceLines(text, startLine, endLine) {
|
|
93
|
+
const lines = text.split('\n');
|
|
94
|
+
return lines.slice(Math.max(0, startLine - 1), Math.max(0, endLine)).join('\n');
|
|
95
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Capability matrix: what DCI supports, per language, entry-point kind and question pack, with the evidence behind each
|
|
3
|
+
// claim and its known limits. The words are used strictly:
|
|
4
|
+
//
|
|
5
|
+
// verified covered by reproducible fixture tests in this repository, for the BUILD half (resolution, bundling,
|
|
6
|
+
// integrity). It says nothing about how good a model's review is.
|
|
7
|
+
// experimental works on fixtures, narrow or unmeasured; no accuracy claim
|
|
8
|
+
// unsupported not implemented
|
|
9
|
+
// blocked cannot be done here, for a stated reason
|
|
10
|
+
//
|
|
11
|
+
// Review quality is `experimental` everywhere: there is no real-world adjudicated holdout in this repository, so no
|
|
12
|
+
// accuracy statement is made (see evaluation/ and docs/guides/context-intelligence.md).
|
|
13
|
+
const T = (f) => `test/context-intelligence/${f}`;
|
|
14
|
+
|
|
15
|
+
const BUILD = [
|
|
16
|
+
{ language: 'javascript/typescript', kind: 'http', frameworks: 'Express-style call registration (get/post/put/patch/delete/all, router.use, app.use)', status: 'verified', evidence: [T('build-js.test.js')], limits: 'Router mounts in another file and middleware factories are not followed (reported as router-mount-not-resolved); destructured and named imports are followed, re-exports and tsconfig path aliases are not.' },
|
|
17
|
+
{ language: 'python', kind: 'http', frameworks: 'decorator registration (Flask/FastAPI-style @app.route / @router.get), FastAPI Depends()/Security() guards', status: 'experimental', evidence: [T('build-python-haskell.test.js'), T('frameworks.test.js')], limits: 'Decorator guards are read from the decorator lines and Depends() guards from the signature (only dependencies named like an auth or user provider bind the principal); framework-level before_request hooks and Django URL conf are not modelled.' },
|
|
18
|
+
{ language: 'haskell', kind: 'http', frameworks: 'Scotty, WAI, Servant, Yesod (through the existing web model)', status: 'verified', evidence: [T('build-python-haskell.test.js'), T('frameworks.test.js')], limits: 'Static source analysis; the existing web model decides what a route is. Dynamic routes are reported unresolved.' },
|
|
19
|
+
{ language: 'java/kotlin/c#/go/php/ruby/rust', kind: 'http', frameworks: 'whatever the engine route scan recognises', status: 'unsupported', evidence: [], limits: 'No fixture proves this, so it is listed as unsupported even though some routes may resolve: Handlers are resolved only for decorator/annotation-style registration; no dedicated fixtures here.' },
|
|
20
|
+
{ language: 'any with a recognised surface', kind: 'webhook/queue/cron/upload/cli', frameworks: 'existing entry-point inventory', status: 'experimental', evidence: [T('build-js.test.js')], limits: 'Handler is the enclosing declaration of the surface; no framework wiring beyond that.' },
|
|
21
|
+
{ language: 'haskell', kind: 'compiler metadata import', frameworks: 'hie-export/1 JSON export of GHC declarations and references', status: 'experimental', evidence: [T('hie-adapter.test.js')], limits: 'Reads an export; never runs GHC or hiedb. The export is validated against the source snapshot. No export produced by a real GHC run is checked in.' },
|
|
22
|
+
{ language: 'any', kind: 'compiler metadata build', frameworks: 'automatic metadata generation', status: 'blocked', evidence: [], limits: 'Running a compiler on untrusted source needs a sandbox proof this build does not have (Template Haskell, custom Setup and plugins run code).' },
|
|
23
|
+
{ language: 'other semantic sources (TypeScript compiler API, Roslyn, go/packages, language servers)', kind: 'compiler metadata import', frameworks: '-', status: 'unsupported', evidence: [], limits: 'Extension point only: the graph schema carries a resolver field (source-static | compiler-enriched).' },
|
|
24
|
+
{ language: 'any', kind: 'provider batch API', frameworks: 'provider-hosted batch submission', status: 'blocked', evidence: [], limits: 'No provider batch contract is verified on this checkout. The offline batch files (request export, result import with hash checks) are implemented and tested.' },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
export function capabilityMatrix() {
|
|
28
|
+
const reviews = ['authz-idor', 'authz-function', 'cross-route-consistency', 'transaction-integrity', 'external-failure', 'sensitive-audit', 'business-invariant'].map(p => ({
|
|
29
|
+
language: 'any bundle that builds', kind: `review: ${p}`, frameworks: 'governed single-turn model call over a frozen bundle', status: 'experimental',
|
|
30
|
+
evidence: [T('review-queue.test.js'), T('queue-diff.test.js')], limits: 'Mechanics are tested with a fake provider; no accuracy measurement exists. The synthetic evaluation suite is a mechanism check, not an estimate.',
|
|
31
|
+
}));
|
|
32
|
+
return [...BUILD, ...reviews];
|
|
33
|
+
}
|