@clear-capabilities/agentic-security-scanner 0.162.0 → 0.163.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.
- package/CHANGELOG.md +34 -0
- package/bin/agentic-security.js +87 -0
- package/dist/1340.index.js +1871 -0
- package/dist/3477.index.js +727 -0
- package/dist/3499.index.js +23 -3
- package/dist/4113.index.js +1 -1
- package/dist/5014.index.js +833 -0
- package/dist/5756.index.js +2 -2
- package/dist/6257.index.js +1 -1
- package/dist/6907.index.js +248 -0
- package/dist/6994.index.js +1 -1
- package/dist/7033.index.js +33 -5
- package/dist/7439.index.js +1822 -0
- package/dist/7838.index.js +1 -1
- package/dist/9560.index.js +2318 -23
- package/dist/957.index.js +1 -1
- package/dist/9999.index.js +43 -7
- package/dist/agentic-security.mjs +12 -12
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +33 -6
- package/src/capabilities/CLAUDE.md +114 -0
- package/src/capabilities/address.js +73 -0
- package/src/capabilities/attack-coverage.js +93 -0
- package/src/capabilities/decide.js +256 -0
- package/src/capabilities/delegate.js +69 -0
- package/src/capabilities/egress.js +20 -0
- package/src/capabilities/hook-advice.js +50 -0
- package/src/capabilities/index.js +32 -0
- package/src/capabilities/manifest.js +344 -0
- package/src/capabilities/outbound.js +189 -0
- package/src/capabilities/paths.js +57 -0
- package/src/capabilities/probes.js +211 -0
- package/src/capabilities/proxy.js +182 -0
- package/src/capabilities/reasons.js +67 -0
- package/src/capabilities/receipts.js +356 -0
- package/src/capabilities/records.js +58 -0
- package/src/capabilities/recovery.js +367 -0
- package/src/capabilities/report.js +81 -0
- package/src/capabilities/runner.js +236 -0
- package/src/capabilities/secrets.js +26 -0
- package/src/capabilities/tool-gate.js +119 -0
- package/src/capabilities/tool-registry.js +60 -0
- package/src/dataflow/CLAUDE.md +30 -0
- package/src/dataflow/catalog.js +42 -1
- package/src/dataflow/engine.js +67 -2
- package/src/dataflow/guard-dominance.js +352 -0
- package/src/discovery/CLAUDE.md +9 -0
- package/src/discovery/index.js +15 -1
- package/src/discovery/lenses.js +18 -0
- package/src/engine.js +80 -15
- package/src/integrations/index.js +5 -2
- package/src/ir/CLAUDE.md +14 -0
- package/src/ir/implicit-handler-params.js +74 -0
- package/src/ir/parser-cs.js +20 -0
- package/src/ir/parser-go.js +30 -13
- package/src/ir/parser-java.js +11 -0
- package/src/language/nix-eval-isolation.js +33 -5
- package/src/lineage/CLAUDE.md +31 -0
- package/src/lineage/deployment/adapter-cloud.js +202 -0
- package/src/lineage/deployment/adapter-compose.js +90 -0
- package/src/lineage/deployment/adapter-kit.js +142 -0
- package/src/lineage/deployment/adapter-kubernetes.js +247 -0
- package/src/lineage/deployment/attack-paths.js +443 -0
- package/src/lineage/deployment/boundaries-run.js +223 -0
- package/src/lineage/deployment/boundary-graph.js +567 -0
- package/src/lineage/deployment/cross-repo.js +253 -0
- package/src/lineage/deployment/drift.js +383 -0
- package/src/lineage/deployment/ingest.js +221 -0
- package/src/lineage/deployment/projection.js +244 -0
- package/src/lineage/deployment/trace-correlation.js +541 -0
- package/src/lineage/source-registry.js +19 -0
- package/src/mcp/CLAUDE.md +5 -2
- package/src/mcp/invariant-tools.js +64 -0
- package/src/mcp/portfolio-tools.js +79 -0
- package/src/mcp/redact.js +15 -0
- package/src/mcp/server.js +24 -3
- package/src/mcp/tools.js +13 -1
- package/src/posture/CLAUDE.md +332 -1
- package/src/posture/accuracy-scorecard.js +95 -4
- package/src/posture/assurance/rollback.js +214 -0
- package/src/posture/assurance/rollout.js +200 -0
- package/src/posture/business-logic.js +26 -4
- package/src/posture/evaluation/bakeoff.js +148 -0
- package/src/posture/evaluation/custody.js +200 -0
- package/src/posture/evaluation/deployment-ablation.js +416 -0
- package/src/posture/evaluation/dev-cases.js +65 -0
- package/src/posture/evaluation/economics.js +85 -0
- package/src/posture/evaluation/gates.js +161 -0
- package/src/posture/evaluation/grouping.js +101 -0
- package/src/posture/evaluation/interval.js +132 -0
- package/src/posture/evaluation/invariant-ablation.js +325 -0
- package/src/posture/evaluation/labels.js +200 -0
- package/src/posture/evaluation/protocol.js +308 -0
- package/src/posture/evaluation/release-gate.js +209 -0
- package/src/posture/evaluation/report.js +158 -0
- package/src/posture/evaluation/runner.js +273 -0
- package/src/posture/evaluation/scan-child.js +80 -0
- package/src/posture/evaluation/score.js +174 -0
- package/src/posture/evaluation/synthetic.js +90 -0
- package/src/posture/evaluation/why-missed.js +219 -0
- package/src/posture/fleet.js +11 -2
- package/src/posture/invariants/cli.js +95 -0
- package/src/posture/invariants/coverage.js +164 -0
- package/src/posture/invariants/export.js +178 -0
- package/src/posture/invariants/expressions.js +46 -0
- package/src/posture/invariants/lifecycle.js +289 -0
- package/src/posture/invariants/project-input.js +55 -0
- package/src/posture/invariants/repair.js +340 -0
- package/src/posture/invariants/run.js +45 -0
- package/src/posture/invariants/scenarios.js +311 -0
- package/src/posture/invariants/schema.js +167 -0
- package/src/posture/invariants/shrink.js +407 -0
- package/src/posture/invariants/state-assertions.js +165 -0
- package/src/posture/logic-claims.js +12 -0
- package/src/posture/model-routing.js +40 -0
- package/src/posture/oracles/adapters.js +172 -1
- package/src/posture/oracles/conformance.js +3 -2
- package/src/posture/oracles/oracle.js +23 -3
- package/src/posture/oracles/registry.js +1 -1
- package/src/posture/portfolio/backend.js +279 -0
- package/src/posture/portfolio/bundle.js +314 -0
- package/src/posture/portfolio/cli.js +142 -0
- package/src/posture/portfolio/manifest.js +263 -0
- package/src/posture/portfolio/progress.js +267 -0
- package/src/posture/portfolio/resume.js +130 -0
- package/src/posture/portfolio/retention.js +207 -0
- package/src/posture/portfolio/scheduler.js +488 -0
- package/src/posture/portfolio/signing.js +155 -0
- package/src/posture/portfolio/wording.js +59 -0
- package/src/posture/portfolio/work-units.js +405 -0
- package/src/posture/replay/replay.js +1 -0
- package/src/posture/routing/adapter.js +140 -0
- package/src/posture/routing/calibration.js +278 -0
- package/src/posture/routing/canary.js +141 -0
- package/src/posture/routing/control.js +35 -0
- package/src/posture/routing/decide.js +211 -0
- package/src/posture/routing/drift.js +137 -0
- package/src/posture/routing/economics.js +185 -0
- package/src/posture/routing/feedback.js +207 -0
- package/src/posture/routing/index.js +31 -0
- package/src/posture/routing/outcomes.js +281 -0
- package/src/posture/routing/promotion.js +291 -0
- package/src/posture/routing/receipts.js +59 -0
- package/src/posture/routing/report.js +142 -0
- package/src/posture/routing/shadow.js +53 -0
- package/src/posture/specification-mining.js +26 -0
- package/src/posture/verification/patch-negative.js +16 -4
- package/src/posture/verification/patch-promotion.js +10 -2
- package/src/report/index.js +51 -1
- package/src/sandbox/CLAUDE.md +10 -0
- package/src/sandbox/backend-userspace.js +91 -13
- package/dist/5613.index.js +0 -500
- package/dist/6239.index.js +0 -469
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// boundaries-run.js: the operator entry point that builds a boundary context for scanned findings (X-308 wiring).
|
|
2
|
+
//
|
|
3
|
+
// Nothing in the default scan builds a boundary context. This is the one place that does, and it runs only when an operator
|
|
4
|
+
// asks for it (`agentic-security boundaries`) AND the `deployment-boundaries` feature is on (off by default). Rules it keeps:
|
|
5
|
+
// - Local files only. It reads the directory it is pointed at, bounded, with no symlink followed, and optionally one scan
|
|
6
|
+
// result file. It opens no socket, spawns no process, reads no credential and executes no configuration.
|
|
7
|
+
// - Read-only with respect to the project. The only write is the report file the operator names with `out`, refused when the
|
|
8
|
+
// target is a symlink.
|
|
9
|
+
// - Off means off. With the feature off nothing is read: the result is a typed `disabled` and no file is opened.
|
|
10
|
+
// - Everything that was not established is carried through: ingest gaps, unsupported syntax, files not read, trace sampling
|
|
11
|
+
// and staleness, findings with no service binding. A result never says safe.
|
|
12
|
+
//
|
|
13
|
+
// Reserved names inside the directory: `service-bindings.json` (finding path prefix to service), `identities.json` (policy file
|
|
14
|
+
// to the identity it belongs to) and `traces.jsonl` / `traces.ndjson` (sanitized runtime trace lines). They are inputs, never
|
|
15
|
+
// ingested as configuration.
|
|
16
|
+
|
|
17
|
+
import * as fs from 'node:fs';
|
|
18
|
+
import * as path from 'node:path';
|
|
19
|
+
import { featureStatus, limitValues, typed } from '../../posture/assurance/config.js';
|
|
20
|
+
import { readFileBounded } from '../../posture/assurance/bounded-io.js';
|
|
21
|
+
import { STATE_DIR_NAME } from '../../posture/state-dir.js';
|
|
22
|
+
import { ingestDeploymentFiles, MAX_INGEST_FILES, DEPLOYMENT_FEATURE } from './ingest.js';
|
|
23
|
+
import { importTraceLines, correlateObservations, applyCorrelation } from './trace-correlation.js';
|
|
24
|
+
import { attachBoundaryContext, boundaryFields, boundaryCoverage } from './projection.js';
|
|
25
|
+
|
|
26
|
+
export const BOUNDARIES_REPORT_SCHEMA = 'agentic-security/boundaries-report';
|
|
27
|
+
export const BOUNDARIES_REPORT_VERSION = '1.0.0';
|
|
28
|
+
export const BINDINGS_FILE = 'service-bindings.json';
|
|
29
|
+
export const IDENTITIES_FILE = 'identities.json';
|
|
30
|
+
export const TRACE_FILES = Object.freeze(['traces.jsonl', 'traces.ndjson']);
|
|
31
|
+
export const MAX_FINDINGS = 5000;
|
|
32
|
+
|
|
33
|
+
const RESERVED = new Set([BINDINGS_FILE, IDENTITIES_FILE, ...TRACE_FILES]);
|
|
34
|
+
const CONFIG_EXTENSIONS = new Set(['.yaml', '.yml', '.json', '.tf', '.hcl', '.tfvars', '.conf', '.toml', '.xml']);
|
|
35
|
+
const SKIP_DIRS = new Set(['.git', 'node_modules', STATE_DIR_NAME]);
|
|
36
|
+
const MAX_WALK_ENTRIES = 5000;
|
|
37
|
+
|
|
38
|
+
/** List candidate configuration files under `dir`, relative and sorted. Symlinks are never followed or listed. */
|
|
39
|
+
export function listConfigFiles(dir) {
|
|
40
|
+
const found = [];
|
|
41
|
+
const skipped = { symlinks: 0, overLimit: false };
|
|
42
|
+
let seen = 0;
|
|
43
|
+
const walk = (d, rel) => {
|
|
44
|
+
let entries;
|
|
45
|
+
try { entries = fs.readdirSync(d, { withFileTypes: true }); } catch { return; }
|
|
46
|
+
for (const e of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
47
|
+
if (++seen > MAX_WALK_ENTRIES) { skipped.overLimit = true; return; }
|
|
48
|
+
const r = rel ? `${rel}/${e.name}` : e.name;
|
|
49
|
+
if (e.isSymbolicLink()) { skipped.symlinks += 1; continue; }
|
|
50
|
+
if (e.isDirectory()) { if (!SKIP_DIRS.has(e.name)) walk(path.join(d, e.name), r); continue; }
|
|
51
|
+
if (!e.isFile()) continue;
|
|
52
|
+
if (!rel && RESERVED.has(e.name)) continue;
|
|
53
|
+
if (CONFIG_EXTENSIONS.has(path.extname(e.name).toLowerCase())) found.push(r);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
walk(dir, '');
|
|
57
|
+
return { files: found.sort(), skipped };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function readJsonFile(file, maxBytes, what) {
|
|
61
|
+
const r = readFileBounded(file, maxBytes);
|
|
62
|
+
if (r.status !== 'ok') return { ok: false, reason: `${what}: ${r.reason}` };
|
|
63
|
+
try { return { ok: true, value: JSON.parse(r.text) }; } catch (e) { return { ok: false, reason: `${what} is not valid JSON (${String(e.message).split('\n')[0]})` }; }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Read the findings of a scan result file. A sibling `.sig` that exists must verify; a result with no signature is read and
|
|
68
|
+
* labelled unverified.
|
|
69
|
+
*/
|
|
70
|
+
export function readScanFindings(file, { maxBytes, verify } = {}) {
|
|
71
|
+
const r = readFileBounded(file, maxBytes);
|
|
72
|
+
if (r.status !== 'ok') return { ok: false, reason: `scan result: ${r.reason}`, findings: [], integrity: 'unreadable' };
|
|
73
|
+
let integrity = 'unsigned';
|
|
74
|
+
if (typeof verify === 'function') {
|
|
75
|
+
const v = verify(r.text, `${file}.sig`);
|
|
76
|
+
if (v === false) return { ok: false, reason: 'the scan result has a signature that does not verify; it is not used', findings: [], integrity: 'signature-mismatch' };
|
|
77
|
+
if (v === true) integrity = 'verified';
|
|
78
|
+
}
|
|
79
|
+
let doc;
|
|
80
|
+
try { doc = JSON.parse(r.text); } catch (e) { return { ok: false, reason: `scan result is not valid JSON (${String(e.message).split('\n')[0]})`, findings: [], integrity }; }
|
|
81
|
+
const list = Array.isArray(doc) ? doc : doc?.findings;
|
|
82
|
+
if (!Array.isArray(list)) return { ok: false, reason: 'scan result has no findings array', findings: [], integrity };
|
|
83
|
+
return { ok: true, reason: null, findings: list.filter((f) => f && typeof f === 'object' && !Array.isArray(f)), integrity };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Build a boundary graph from a directory of deployment sources and attach a context to each finding.
|
|
88
|
+
*
|
|
89
|
+
* @param {object} o
|
|
90
|
+
* @param {object} o.config resolveAssuranceConfig() result (the feature gate)
|
|
91
|
+
* @param {string} o.from directory with configuration, optional traces, bindings and identities
|
|
92
|
+
* @param {string} [o.environment] default 'prod'
|
|
93
|
+
* @param {string|null} [o.repository]
|
|
94
|
+
* @param {string|null} [o.revision] exact 40/64 hex commit the files were read at
|
|
95
|
+
* @param {string|null} [o.findingsFile] a scan result (last-scan.json)
|
|
96
|
+
* @param {object[]|null} [o.findings] findings already in memory (an evaluation passes these instead of a file)
|
|
97
|
+
* @param {Function} [o.verifySignature] (body, sigFile) => true | false | null
|
|
98
|
+
* @param {string} [o.now] ISO time used to judge trace staleness; defaults to the current time
|
|
99
|
+
* @returns {object} typed result; `status` is ok | disabled | blocked | unsupported | error. Never throws.
|
|
100
|
+
*/
|
|
101
|
+
export function runBoundaries({ config, from, environment = 'prod', repository = null, revision = null, findingsFile = null, findings: suppliedFindings = null, verifySignature, now } = {}) {
|
|
102
|
+
const gate = featureStatus(config, DEPLOYMENT_FEATURE);
|
|
103
|
+
if (gate.status !== 'ok') return { ...gate, report: null };
|
|
104
|
+
const limits = limitValues(config);
|
|
105
|
+
const fail = (code, reason) => typed('error', code, reason, { feature: DEPLOYMENT_FEATURE, report: null });
|
|
106
|
+
|
|
107
|
+
if (typeof from !== 'string' || !from) return fail('invalid-input', '--from names the directory with deployment configuration');
|
|
108
|
+
let root;
|
|
109
|
+
try { root = fs.realpathSync(from); if (!fs.statSync(root).isDirectory()) throw new Error('not a directory'); } catch { return fail('invalid-input', `--from '${from}' is not a readable directory`); }
|
|
110
|
+
|
|
111
|
+
const clock = now ?? new Date().toISOString();
|
|
112
|
+
const notes = [];
|
|
113
|
+
const listing = listConfigFiles(root);
|
|
114
|
+
if (listing.skipped.symlinks) notes.push(`${listing.skipped.symlinks} symbolic link(s) were not followed`);
|
|
115
|
+
if (listing.skipped.overLimit) notes.push(`the directory walk stopped at ${MAX_WALK_ENTRIES} entries; later files were not listed`);
|
|
116
|
+
let paths = listing.files;
|
|
117
|
+
if (paths.length > MAX_INGEST_FILES) { notes.push(`${paths.length - MAX_INGEST_FILES} configuration file(s) beyond the ${MAX_INGEST_FILES} file limit were not read`); paths = paths.slice(0, MAX_INGEST_FILES); }
|
|
118
|
+
if (!paths.length) return fail('invalid-input', `no configuration files (${[...CONFIG_EXTENSIONS].join(', ')}) were found under '${from}'`);
|
|
119
|
+
|
|
120
|
+
let identities = {};
|
|
121
|
+
const idFile = path.join(root, IDENTITIES_FILE);
|
|
122
|
+
if (fs.existsSync(idFile)) {
|
|
123
|
+
const j = readJsonFile(idFile, limits.maxFileBytes, IDENTITIES_FILE);
|
|
124
|
+
if (!j.ok) return fail('invalid-input', j.reason);
|
|
125
|
+
if (!j.value || typeof j.value !== 'object' || Array.isArray(j.value)) return fail('invalid-input', `${IDENTITIES_FILE} must be an object mapping a file path to an identity`);
|
|
126
|
+
identities = Object.fromEntries(Object.entries(j.value).filter(([, v]) => typeof v === 'string'));
|
|
127
|
+
}
|
|
128
|
+
let bindings = [];
|
|
129
|
+
const bindFile = path.join(root, BINDINGS_FILE);
|
|
130
|
+
if (fs.existsSync(bindFile)) {
|
|
131
|
+
const j = readJsonFile(bindFile, limits.maxFileBytes, BINDINGS_FILE);
|
|
132
|
+
if (!j.ok) return fail('invalid-input', j.reason);
|
|
133
|
+
if (!Array.isArray(j.value)) return fail('invalid-input', `${BINDINGS_FILE} must be an array of { pathPrefix, service } objects`);
|
|
134
|
+
bindings = j.value;
|
|
135
|
+
} else notes.push(`no ${BINDINGS_FILE}: no finding can be tied to a service, so every finding reports exposure as not assessed`);
|
|
136
|
+
|
|
137
|
+
const ingested = ingestDeploymentFiles({ config, root, paths, environment, repository, revision, identities });
|
|
138
|
+
if (ingested.status !== 'ok') return fail(ingested.code ?? 'invalid-config', ingested.reason ?? 'ingest failed');
|
|
139
|
+
let graph = ingested.graph;
|
|
140
|
+
|
|
141
|
+
const observation = { supplied: false, accepted: 0, rejected: 0, status: 'not-supplied', coverage: null };
|
|
142
|
+
const traceName = TRACE_FILES.find((n) => fs.existsSync(path.join(root, n)));
|
|
143
|
+
if (traceName) {
|
|
144
|
+
observation.supplied = true;
|
|
145
|
+
const t = readFileBounded(path.join(root, traceName), limits.maxFileBytes);
|
|
146
|
+
if (t.status !== 'ok') { observation.status = 'unreadable'; notes.push(`${traceName} was not read: ${t.reason}`); }
|
|
147
|
+
else {
|
|
148
|
+
const imp = importTraceLines(t.text, { environment });
|
|
149
|
+
if (imp.status !== 'ok') { observation.status = imp.status; notes.push(`${traceName} was not imported: ${imp.reason}`); }
|
|
150
|
+
else {
|
|
151
|
+
observation.accepted = imp.set.stats.accepted; observation.rejected = imp.set.stats.rejected;
|
|
152
|
+
const corr = correlateObservations(graph, imp.set, { now: clock });
|
|
153
|
+
if (corr.status !== 'ok') { observation.status = corr.status; notes.push(`traces were not correlated: ${corr.reason}`); }
|
|
154
|
+
else {
|
|
155
|
+
const applied = applyCorrelation(graph, corr);
|
|
156
|
+
if (applied.ok) { graph = applied.graph; observation.status = 'correlated'; observation.coverage = corr.coverage; }
|
|
157
|
+
else { observation.status = 'invalid-graph'; notes.push('correlated traces produced an invalid graph and were not applied'); }
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let findings = [];
|
|
164
|
+
let integrity = 'not-supplied';
|
|
165
|
+
if (Array.isArray(suppliedFindings)) {
|
|
166
|
+
integrity = 'in-memory';
|
|
167
|
+
findings = suppliedFindings.filter((f) => f && typeof f === 'object' && !Array.isArray(f));
|
|
168
|
+
if (findings.length > MAX_FINDINGS) { notes.push(`${findings.length - MAX_FINDINGS} finding(s) beyond the ${MAX_FINDINGS} limit were not given a context`); findings = findings.slice(0, MAX_FINDINGS); }
|
|
169
|
+
} else if (findingsFile) {
|
|
170
|
+
const s = readScanFindings(findingsFile, { maxBytes: limits.maxFileBytes, verify: verifySignature });
|
|
171
|
+
if (!s.ok) return fail('invalid-input', s.reason);
|
|
172
|
+
integrity = s.integrity;
|
|
173
|
+
findings = s.findings;
|
|
174
|
+
if (findings.length > MAX_FINDINGS) { notes.push(`${findings.length - MAX_FINDINGS} finding(s) beyond the ${MAX_FINDINGS} limit were not given a context`); findings = findings.slice(0, MAX_FINDINGS); }
|
|
175
|
+
if (integrity === 'unsigned') notes.push('the scan result carries no signature, so its integrity was not verified');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const attached = attachBoundaryContext({ config, findings, graph, bindings, now: clock });
|
|
179
|
+
if (attached.status !== 'ok') return { ...attached, report: null };
|
|
180
|
+
const contexts = attached.findings.map((f) => f.boundaryContext).filter(Boolean);
|
|
181
|
+
const rows = attached.findings.map((f) => ({
|
|
182
|
+
id: f.id ?? null, stableId: f.stableId ?? null, file: f.file ?? null, line: Number.isInteger(f.line) ? f.line : null, vuln: f.vuln ?? null,
|
|
183
|
+
...(f.boundaryContext ? boundaryFields(f.boundaryContext) : { boundaryContext: null, boundaryView: null, notAnalyzed: 'the context could not be computed for this finding' }),
|
|
184
|
+
}));
|
|
185
|
+
const report = {
|
|
186
|
+
schema: BOUNDARIES_REPORT_SCHEMA, schemaVersion: BOUNDARIES_REPORT_VERSION,
|
|
187
|
+
feature: DEPLOYMENT_FEATURE, environment, repository, revision,
|
|
188
|
+
graph: {
|
|
189
|
+
digest: graph.digest, nodeCount: graph.nodes.length, edgeCount: graph.edges.length, gapCount: graph.gaps.length,
|
|
190
|
+
unresolvedNodeCount: graph.nodes.filter((n) => !n.resolved).length,
|
|
191
|
+
gaps: graph.gaps.map((g) => ({ code: g.code, subject: g.subject, file: g.file ?? null, message: g.message })),
|
|
192
|
+
files: ingested.files, sourceCount: graph.sources.length,
|
|
193
|
+
},
|
|
194
|
+
observation,
|
|
195
|
+
scanResult: { supplied: Boolean(findingsFile) || integrity === 'in-memory', integrity, findingCount: findings.length },
|
|
196
|
+
coverage: boundaryCoverage(contexts),
|
|
197
|
+
findings: rows,
|
|
198
|
+
errors: attached.errors,
|
|
199
|
+
notes,
|
|
200
|
+
statement: 'Configured relationships were not exercised and runtime traffic coverage is not established; a path that is not listed has not been shown to be absent.',
|
|
201
|
+
};
|
|
202
|
+
return typed('ok', null, 'boundary contexts built from local files', { feature: DEPLOYMENT_FEATURE, report, graph });
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Text form of a report. Reuses each context's own projected text; adds nothing about safety. */
|
|
206
|
+
export function renderBoundariesText(report) {
|
|
207
|
+
const L = [];
|
|
208
|
+
L.push(`Deployment boundaries (environment ${report.environment}${report.repository ? `, repository ${report.repository}` : ''})`);
|
|
209
|
+
L.push(` Graph: ${report.graph.nodeCount} node(s), ${report.graph.edgeCount} edge(s), ${report.graph.gapCount} gap(s), ${report.graph.unresolvedNodeCount} unresolved; digest ${report.graph.digest.slice(0, 12)}`);
|
|
210
|
+
for (const f of report.graph.files) L.push(` ${f.file}: ${f.adapter ?? 'not read'} (${f.status})`);
|
|
211
|
+
for (const g of report.graph.gaps.slice(0, 20)) L.push(` gap ${g.code}: ${g.subject}`);
|
|
212
|
+
if (report.graph.gaps.length > 20) L.push(` and ${report.graph.gaps.length - 20} more gap(s)`);
|
|
213
|
+
L.push(` Traces: ${report.observation.status}${report.observation.supplied ? ` (${report.observation.accepted} accepted, ${report.observation.rejected} rejected)` : ''}`);
|
|
214
|
+
L.push(` Scan result: ${report.scanResult.supplied ? `${report.scanResult.findingCount} finding(s), integrity ${report.scanResult.integrity}` : 'not supplied, so no finding was analyzed'}`);
|
|
215
|
+
for (const f of report.findings) {
|
|
216
|
+
L.push(` ${f.id ?? f.stableId ?? 'finding'} ${f.file ?? ''}${f.line ? `:${f.line}` : ''} ${f.vuln ?? ''}`.trimEnd());
|
|
217
|
+
for (const t of f.boundaryView?.text ?? [f.notAnalyzed ?? 'not analyzed']) L.push(` ${t}`);
|
|
218
|
+
}
|
|
219
|
+
for (const line of report.coverage.lines) L.push(line);
|
|
220
|
+
for (const n of report.notes) L.push(` Note: ${n}`);
|
|
221
|
+
L.push(` ${report.statement}`);
|
|
222
|
+
return `${L.join('\n')}\n`;
|
|
223
|
+
}
|