@clear-capabilities/agentic-security-scanner 0.133.0 → 0.136.2
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 +299 -0
- package/bin/agentic-security-audit.js +2 -1
- package/bin/agentic-security-consistency.js +2 -1
- package/bin/agentic-security.js +348 -40
- package/dist/113.index.js +6 -6
- package/dist/117.index.js +3 -1
- package/dist/178.index.js +1 -1
- package/dist/220.index.js +5 -3
- package/dist/238.index.js +4 -4
- package/dist/317.index.js +270 -0
- package/dist/384.index.js +1 -1
- package/dist/435.index.js +13 -7
- package/dist/499.index.js +86 -0
- package/dist/513.index.js +7 -3
- package/dist/526.index.js +6 -6
- package/dist/609.index.js +741 -0
- package/dist/637.index.js +1 -1
- package/dist/675.index.js +7 -5
- package/dist/839.index.js +4 -3
- package/dist/905.index.js +1173 -0
- package/dist/agentic-security.mjs +56 -56
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/dist/compliance-frameworks/ccpa.json +32 -0
- package/dist/compliance-frameworks/eu-ai-act.json +51 -0
- package/dist/compliance-frameworks/gdpr.json +45 -0
- package/dist/compliance-frameworks/hipaa-security-rule.json +56 -0
- package/dist/compliance-frameworks/nist-ai-600-1.json +51 -0
- package/dist/compliance-frameworks/nist-csf-2.json +73 -0
- package/dist/compliance-frameworks/nist-privacy-1-1.json +846 -0
- package/dist/compliance-frameworks/owasp-asvs-5.json +79 -0
- package/dist/compliance-frameworks/owasp-llm-top-10.json +69 -0
- package/package.json +14 -6
- package/src/badge.js +2 -1
- package/src/dataflow/cross-service-taint.js +2 -1
- package/src/dataflow/ifds-precise.js +6 -4
- package/src/dataflow/incremental.js +7 -5
- package/src/discovery/CLAUDE.md +48 -0
- package/src/discovery/confirm.js +47 -0
- package/src/discovery/disprove.js +79 -0
- package/src/discovery/hunter.js +116 -0
- package/src/discovery/index.js +331 -0
- package/src/discovery/judge.js +97 -0
- package/src/discovery/lenses.js +69 -0
- package/src/discovery/llm-invoke.js +120 -0
- package/src/discovery/memory.js +163 -0
- package/src/discovery/partition.js +92 -0
- package/src/engine.js +163 -7
- package/src/integrations/tickets.js +7 -6
- package/src/ir/ir-stats.js +1 -1
- package/src/ir/parser-cpp.js +1 -1
- package/src/leaderboard.js +3 -2
- package/src/llm-validator/consistency.js +6 -2
- package/src/llm-validator/index.js +30 -41
- package/src/llm-validator/providers.js +227 -0
- package/src/mcp/audit.js +2 -1
- package/src/mcp/tools.js +4 -3
- package/src/posture/CLAUDE.md +102 -1
- package/src/posture/accuracy-scorecard.js +59 -1
- package/src/posture/agents-memory.js +5 -3
- package/src/posture/auditor-walkthrough.js +9 -7
- package/src/posture/auth-posture-import.js +5 -4
- package/src/posture/autopilot.js +225 -0
- package/src/posture/comparison.js +181 -0
- package/src/posture/compliance-frameworks/nist-privacy-1-1.json +846 -0
- package/src/posture/compliance-policy.js +7 -9
- package/src/posture/custom-rules.js +7 -5
- package/src/posture/cve-alert-daemon.js +6 -5
- package/src/posture/dep-add-guard.js +2 -1
- package/src/posture/deterministic.js +3 -2
- package/src/posture/evidence-bundle.js +246 -0
- package/src/posture/execution-proof.js +25 -1
- package/src/posture/exploitability-probability.js +2 -1
- package/src/posture/feature-flags.js +3 -2
- package/src/posture/findings-memory.js +3 -3
- package/src/posture/fix-history.js +5 -2
- package/src/posture/fix-metrics.js +5 -5
- package/src/posture/fix-plan.js +2 -1
- package/src/posture/fleet.js +0 -0
- package/src/posture/grader-calibration.js +3 -4
- package/src/posture/intent-context.js +2 -1
- package/src/posture/learning.js +4 -3
- package/src/posture/license-attributions.js +5 -7
- package/src/posture/license-graph.js +2 -1
- package/src/posture/license-policy.js +2 -1
- package/src/posture/logic-claims.js +266 -0
- package/src/posture/model-rescan.js +4 -3
- package/src/posture/network-policy-import.js +3 -2
- package/src/posture/poc-inprocess.js +404 -2
- package/src/posture/pqc-migration-plan.js +7 -5
- package/src/posture/pr-augment.js +8 -5
- package/src/posture/privacy-framework.js +262 -0
- package/src/posture/proof-artifact.js +101 -0
- package/src/posture/prove-findings.js +28 -4
- package/src/posture/risk-dollars.js +2 -2
- package/src/posture/router.js +5 -4
- package/src/posture/ruleset-version.js +2 -2
- package/src/posture/runtime-correlation.js +2 -1
- package/src/posture/sbom-diff.js +12 -3
- package/src/posture/sca-policy.js +7 -4
- package/src/posture/scan-checkpoint.js +15 -0
- package/src/posture/state-dir.js +34 -0
- package/src/posture/telemetry-ingest.js +4 -3
- package/src/posture/threat-model-auto.js +4 -1
- package/src/posture/threat-model-grounding.js +11 -1
- package/src/posture/time-to-fix.js +3 -2
- package/src/posture/triage-memory.js +3 -2
- package/src/posture/waf-ingest.js +6 -5
- package/src/posture/watch-mode.js +4 -3
- package/src/report/index.js +9 -0
- package/src/sast/code-injection-multilang.js +29 -0
- package/src/sca/dep-confusion.js +2 -1
|
@@ -11,8 +11,7 @@
|
|
|
11
11
|
// The emitter is deterministic — sorts by ecosystem, name, version —
|
|
12
12
|
// so commits don't churn between scans.
|
|
13
13
|
|
|
14
|
-
import
|
|
15
|
-
import * as path from 'node:path';
|
|
14
|
+
import { statePath, safeWriteState } from './state-dir.js';
|
|
16
15
|
|
|
17
16
|
function _sortKey(c) {
|
|
18
17
|
return `${c.ecosystem || 'zz'}:${c.name || ''}:${c.version || ''}`;
|
|
@@ -83,11 +82,10 @@ export function generateAttributions(components, options) {
|
|
|
83
82
|
|
|
84
83
|
export function persistAttributions(scanRoot, result) {
|
|
85
84
|
if (!result || !result.markdown) return null;
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
85
|
+
// Through the seam — see the note in pqc-migration-plan.js. The result is
|
|
86
|
+
// still returned when writing is off; only the artifact is withheld.
|
|
87
|
+
safeWriteState(statePath(scanRoot, 'ATTRIBUTIONS.md'), result.markdown);
|
|
88
|
+
if (result.notice) safeWriteState(statePath(scanRoot, 'NOTICE'), result.notice);
|
|
91
89
|
return result;
|
|
92
90
|
}
|
|
93
91
|
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
import * as fs from 'node:fs';
|
|
38
38
|
import * as path from 'node:path';
|
|
39
39
|
|
|
40
|
+
import { statePath } from './state-dir.js';
|
|
40
41
|
// ── License taxonomy ───────────────────────────────────────────────────────
|
|
41
42
|
|
|
42
43
|
const LICENSE_FAMILIES = {
|
|
@@ -223,7 +224,7 @@ export function analyzeLicenseGraph(components, options) {
|
|
|
223
224
|
|
|
224
225
|
export function loadLicenseGraphPolicy(scanRoot) {
|
|
225
226
|
if (!scanRoot) return { distributionMode: DEFAULT_DIST_MODE };
|
|
226
|
-
const fp =
|
|
227
|
+
const fp = statePath(scanRoot, 'license-policy.yml');
|
|
227
228
|
if (!fs.existsSync(fp)) return { distributionMode: DEFAULT_DIST_MODE };
|
|
228
229
|
try {
|
|
229
230
|
const raw = fs.readFileSync(fp, 'utf8');
|
|
@@ -16,6 +16,7 @@ import * as fs from 'node:fs';
|
|
|
16
16
|
import * as path from 'node:path';
|
|
17
17
|
import * as yaml from '../util/yaml.js';
|
|
18
18
|
|
|
19
|
+
import { statePath } from './state-dir.js';
|
|
19
20
|
const DEFAULT_POLICY = {
|
|
20
21
|
allow: [],
|
|
21
22
|
deny: [],
|
|
@@ -26,7 +27,7 @@ const DEFAULT_POLICY = {
|
|
|
26
27
|
export function loadLicensePolicy(scanRoot) {
|
|
27
28
|
if (!scanRoot) return null;
|
|
28
29
|
for (const name of ['license-policy.yml', 'license-policy.yaml', 'license-policy.json']) {
|
|
29
|
-
const p =
|
|
30
|
+
const p = statePath(scanRoot, name);
|
|
30
31
|
if (!fs.existsSync(p)) continue;
|
|
31
32
|
try {
|
|
32
33
|
const raw = fs.readFileSync(p, 'utf8');
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
// PRD Epic 6 — the business-logic tier's missing half.
|
|
2
|
+
//
|
|
3
|
+
// The deterministic side of business logic already exists and is wired:
|
|
4
|
+
// `sast/logic.js` carries the canonical anti-patterns, `posture/business-logic.js`
|
|
5
|
+
// builds the per-route authZ matrix, extracts state machines and finds
|
|
6
|
+
// negative-test gaps. What did NOT exist was any handling of the OTHER
|
|
7
|
+
// producer — the reviewing agent, which is the only party that can read intent
|
|
8
|
+
// and is therefore the only one that can find the flaws patterns cannot.
|
|
9
|
+
//
|
|
10
|
+
// THE PROBLEM WITH THAT PRODUCER. Everything else in this engine can be
|
|
11
|
+
// checked: a taint finding has a path, an execution-proven finding has a marker
|
|
12
|
+
// file, an SCA finding has a version range. A logic claim is prose. It arrives
|
|
13
|
+
// asserting that a handler lets one user act on another's resource, and there
|
|
14
|
+
// is nothing in the finding that a second party could disagree with. An
|
|
15
|
+
// unrefutable claim is the weakest thing this engine emits, and it was the only
|
|
16
|
+
// tier with no way to be wrong.
|
|
17
|
+
//
|
|
18
|
+
// WHAT THIS MODULE DOES. It takes claims from a reviewing agent and puts them
|
|
19
|
+
// through deterministic lenses that can REFUTE them — cheaply, offline, and
|
|
20
|
+
// without asking a model to grade its own homework:
|
|
21
|
+
//
|
|
22
|
+
// citation — the file exists and the cited line is inside it. A claim about
|
|
23
|
+
// `routes/orders.js:214` in a 90-line file is refuted on the
|
|
24
|
+
// spot; that is the signature of a fabricated location.
|
|
25
|
+
// quotation — the snippet the claim quotes actually appears at the cited
|
|
26
|
+
// line (± a small window). A claim that misquotes the code it is
|
|
27
|
+
// about was not written from the code.
|
|
28
|
+
// corroboration — for the claim kinds that MAKE a checkable assertion about
|
|
29
|
+
// the source ("this route has no authentication"), check it.
|
|
30
|
+
// A route that plainly does authenticate refutes it.
|
|
31
|
+
//
|
|
32
|
+
// RECALL-PRESERVING, same precedent as `falsification.js` and `proof-gate.js`.
|
|
33
|
+
// A refuted claim is marked and kept, never deleted and never severity-touched.
|
|
34
|
+
// Refutation here means "no second party could corroborate this", which is a
|
|
35
|
+
// triage signal, not proof the reviewer was wrong.
|
|
36
|
+
//
|
|
37
|
+
// SEPARATION IS ENFORCED, NOT ASSUMED. The agent is stamped as producer and
|
|
38
|
+
// these lenses record under their own verifier ids, so `assertSeparation`
|
|
39
|
+
// refuses if anything ever tries to verify its own claim. That is why the
|
|
40
|
+
// lenses live here in deterministic code rather than in the agent's prompt: a
|
|
41
|
+
// reviewer asked to double-check itself is the same party voting twice.
|
|
42
|
+
|
|
43
|
+
import { recordProducer, recordVerdict, consensusOf } from './verification-separation.js';
|
|
44
|
+
|
|
45
|
+
export const PRODUCER = 'agent:logic-reviewer';
|
|
46
|
+
|
|
47
|
+
export const VERIFIER_CITATION = 'verifier:citation';
|
|
48
|
+
const VERIFIER_QUOTATION = 'verifier:quotation';
|
|
49
|
+
export const VERIFIER_CORROBORATION = 'verifier:logic-corroboration';
|
|
50
|
+
|
|
51
|
+
// Claim kinds that assert something checkable about the source. Anything else
|
|
52
|
+
// is accepted as unverifiable-but-recorded rather than silently upheld.
|
|
53
|
+
const CLAIM_KINDS = Object.freeze([
|
|
54
|
+
'missing-authentication',
|
|
55
|
+
'missing-authorization',
|
|
56
|
+
'missing-ownership-check',
|
|
57
|
+
'state-transition-bypass',
|
|
58
|
+
'race-condition',
|
|
59
|
+
'other',
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
// Reused deliberately from the same vocabulary the authZ matrix uses, so a
|
|
63
|
+
// corroboration verdict and a matrix finding cannot disagree about what
|
|
64
|
+
// "authenticated" means in this codebase.
|
|
65
|
+
const AUTH_HINTS = [
|
|
66
|
+
/\breq\.user\b/, /\breq\.auth\b/, /\brequest\.user\b/,
|
|
67
|
+
/requireAuth|isAuthenticated|@login_required|@requires_auth|@jwt_required/,
|
|
68
|
+
/authorize|authMiddleware|verifyJWT|jwt\.verify\b/, /\bpassport\b/,
|
|
69
|
+
/\bgetSession\b|\bcurrentUser\b/,
|
|
70
|
+
];
|
|
71
|
+
const OWNERSHIP_HINTS = [
|
|
72
|
+
/\bowner(?:Id)?\b/i, /\buser_?id\s*[=:]/i,
|
|
73
|
+
/\.userId\s*===\s*req\.user/, /\.owner\s*===\s*req\.user/,
|
|
74
|
+
/where\s*:\s*\{[^}]*user/i,
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
// How far from the cited line a quoted snippet may appear before the citation
|
|
78
|
+
// is treated as not corroborated. Small on purpose: an agent reading the file
|
|
79
|
+
// is off by a line or two, not by twenty.
|
|
80
|
+
const QUOTE_WINDOW = 3;
|
|
81
|
+
|
|
82
|
+
function _lines(content) { return String(content).split('\n'); }
|
|
83
|
+
|
|
84
|
+
function _normalize(s) {
|
|
85
|
+
return String(s).replace(/\s+/g, ' ').trim().toLowerCase();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The enclosing handler body around a line, bounded by blank-line-separated
|
|
90
|
+
* top-level blocks. Deliberately crude: a corroboration lens that guessed at
|
|
91
|
+
* scope precisely would be a parser, and a wrong guess here REFUTES a real
|
|
92
|
+
* finding. So the window is generous — it errs toward finding the auth check
|
|
93
|
+
* and therefore toward refusing to refute.
|
|
94
|
+
*/
|
|
95
|
+
function _enclosingBlock(content, line) {
|
|
96
|
+
const ls = _lines(content);
|
|
97
|
+
const idx = Math.max(0, Math.min(ls.length - 1, line - 1));
|
|
98
|
+
let start = idx, end = idx;
|
|
99
|
+
while (start > 0 && !/^\s*$/.test(ls[start - 1])) start--;
|
|
100
|
+
while (end < ls.length - 1 && !/^\s*$/.test(ls[end + 1])) end++;
|
|
101
|
+
// Widen by a few lines either side: middleware often sits on the route line
|
|
102
|
+
// above the block the flaw is in.
|
|
103
|
+
start = Math.max(0, start - 5);
|
|
104
|
+
end = Math.min(ls.length - 1, end + 5);
|
|
105
|
+
return ls.slice(start, end + 1).join('\n');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Put one claim through the deterministic lenses.
|
|
110
|
+
*
|
|
111
|
+
* @param {object} claim {file, line, vuln, kind, description, snippet?, severity?}
|
|
112
|
+
* @param {object|Map} fileContents file -> source
|
|
113
|
+
* @returns {object} the claim as a finding, carrying `verification`
|
|
114
|
+
*/
|
|
115
|
+
export function verifyLogicClaim(claim, fileContents) {
|
|
116
|
+
const finding = {
|
|
117
|
+
...claim,
|
|
118
|
+
parser: 'LOGIC-AGENT',
|
|
119
|
+
family: claim.family || 'business-logic',
|
|
120
|
+
kind: CLAIM_KINDS.includes(claim.kind) ? claim.kind : 'other',
|
|
121
|
+
};
|
|
122
|
+
recordProducer(finding, claim.producer || PRODUCER);
|
|
123
|
+
|
|
124
|
+
const read = (f) => {
|
|
125
|
+
if (!fileContents) return null;
|
|
126
|
+
if (typeof fileContents.get === 'function') return fileContents.get(f) ?? null;
|
|
127
|
+
return fileContents[f] ?? null;
|
|
128
|
+
};
|
|
129
|
+
const content = claim.file ? read(claim.file) : null;
|
|
130
|
+
|
|
131
|
+
// ── citation ──────────────────────────────────────────────────────────────
|
|
132
|
+
if (content === null) {
|
|
133
|
+
recordVerdict(finding, {
|
|
134
|
+
verifierId: VERIFIER_CITATION, lens: 'citation', verdict: 'refuted',
|
|
135
|
+
reason: `no file '${claim.file}' was scanned, so the cited location does not exist`,
|
|
136
|
+
});
|
|
137
|
+
finding.consensus = consensusOf(finding);
|
|
138
|
+
finding.quarantined = true;
|
|
139
|
+
return finding;
|
|
140
|
+
}
|
|
141
|
+
const total = _lines(content).length;
|
|
142
|
+
const line = Number(claim.line);
|
|
143
|
+
if (!Number.isInteger(line) || line < 1 || line > total) {
|
|
144
|
+
recordVerdict(finding, {
|
|
145
|
+
verifierId: VERIFIER_CITATION, lens: 'citation', verdict: 'refuted',
|
|
146
|
+
reason: `cited line ${claim.line} is outside ${claim.file} (${total} lines)`,
|
|
147
|
+
});
|
|
148
|
+
} else {
|
|
149
|
+
recordVerdict(finding, {
|
|
150
|
+
verifierId: VERIFIER_CITATION, lens: 'citation', verdict: 'upheld',
|
|
151
|
+
reason: `${claim.file}:${line} exists`,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ── quotation ─────────────────────────────────────────────────────────────
|
|
156
|
+
// Only a lens when the claim actually quotes something. A claim with no
|
|
157
|
+
// snippet is UNDECIDED here, not upheld — silence is not corroboration.
|
|
158
|
+
if (!claim.snippet || !String(claim.snippet).trim()) {
|
|
159
|
+
recordVerdict(finding, {
|
|
160
|
+
verifierId: VERIFIER_QUOTATION, lens: 'quotation', verdict: 'undecided',
|
|
161
|
+
reason: 'the claim quotes no source, so there is nothing to check it against',
|
|
162
|
+
});
|
|
163
|
+
} else {
|
|
164
|
+
const want = _normalize(claim.snippet);
|
|
165
|
+
const ls = _lines(content);
|
|
166
|
+
const lo = Math.max(0, (line || 1) - 1 - QUOTE_WINDOW);
|
|
167
|
+
const hi = Math.min(ls.length, (line || 1) + QUOTE_WINDOW);
|
|
168
|
+
const window = _normalize(ls.slice(lo, hi).join(' '));
|
|
169
|
+
const anywhere = _normalize(content);
|
|
170
|
+
if (window.includes(want)) {
|
|
171
|
+
recordVerdict(finding, {
|
|
172
|
+
verifierId: VERIFIER_QUOTATION, lens: 'quotation', verdict: 'upheld',
|
|
173
|
+
reason: 'the quoted source appears at the cited line',
|
|
174
|
+
});
|
|
175
|
+
} else if (anywhere.includes(want)) {
|
|
176
|
+
// Right file, wrong line. Not a fabrication, but the location is not
|
|
177
|
+
// usable as-is, so it is not corroboration either.
|
|
178
|
+
recordVerdict(finding, {
|
|
179
|
+
verifierId: VERIFIER_QUOTATION, lens: 'quotation', verdict: 'undecided',
|
|
180
|
+
reason: 'the quoted source is in the file but not at the cited line',
|
|
181
|
+
});
|
|
182
|
+
} else {
|
|
183
|
+
recordVerdict(finding, {
|
|
184
|
+
verifierId: VERIFIER_QUOTATION, lens: 'quotation', verdict: 'refuted',
|
|
185
|
+
reason: 'the quoted source does not appear in the cited file',
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ── corroboration ─────────────────────────────────────────────────────────
|
|
191
|
+
const block = _enclosingBlock(content, line || 1);
|
|
192
|
+
if (finding.kind === 'missing-authentication') {
|
|
193
|
+
const hit = AUTH_HINTS.find((re) => re.test(block));
|
|
194
|
+
recordVerdict(finding, hit
|
|
195
|
+
? { verifierId: VERIFIER_CORROBORATION, lens: 'authz', verdict: 'refuted',
|
|
196
|
+
reason: `the handler around this line does authenticate (${hit.source})` }
|
|
197
|
+
: { verifierId: VERIFIER_CORROBORATION, lens: 'authz', verdict: 'upheld',
|
|
198
|
+
reason: 'no authentication marker anywhere in the enclosing handler' });
|
|
199
|
+
} else if (finding.kind === 'missing-authorization' || finding.kind === 'missing-ownership-check') {
|
|
200
|
+
const hit = OWNERSHIP_HINTS.find((re) => re.test(block));
|
|
201
|
+
recordVerdict(finding, hit
|
|
202
|
+
? { verifierId: VERIFIER_CORROBORATION, lens: 'authz', verdict: 'refuted',
|
|
203
|
+
reason: `the handler around this line does scope the record to a user (${hit.source})` }
|
|
204
|
+
: { verifierId: VERIFIER_CORROBORATION, lens: 'authz', verdict: 'upheld',
|
|
205
|
+
reason: 'no ownership scoping in the enclosing handler' });
|
|
206
|
+
} else {
|
|
207
|
+
// No deterministic lens exists for this kind. Said out loud rather than
|
|
208
|
+
// counted as agreement — an unchecked claim and a corroborated one must
|
|
209
|
+
// not read the same in the consensus.
|
|
210
|
+
recordVerdict(finding, {
|
|
211
|
+
verifierId: VERIFIER_CORROBORATION, lens: 'authz', verdict: 'undecided',
|
|
212
|
+
reason: `no deterministic lens covers claim kind '${finding.kind}'`,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
finding.consensus = consensusOf(finding);
|
|
217
|
+
// Quarantine, not deletion — the same contract falsification uses.
|
|
218
|
+
finding.quarantined = finding.consensus.verdict === 'refuted';
|
|
219
|
+
return finding;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Verify a batch. Nothing is dropped: the returned list is the same length as
|
|
224
|
+
* the input, in the same order.
|
|
225
|
+
*/
|
|
226
|
+
export function ingestLogicClaims(claims, { fileContents = null } = {}) {
|
|
227
|
+
const list = Array.isArray(claims) ? claims : [];
|
|
228
|
+
const out = list.map((c) => {
|
|
229
|
+
try { return verifyLogicClaim(c, fileContents); }
|
|
230
|
+
catch (e) {
|
|
231
|
+
// A lens that throws must not swallow the claim.
|
|
232
|
+
const f = { ...c, parser: 'LOGIC-AGENT', family: 'business-logic', quarantined: false };
|
|
233
|
+
f.consensus = { verdict: 'undecided', upheld: 0, refuted: 0, undecided: 0, lenses: [] };
|
|
234
|
+
f.verificationError = String(e?.message || e);
|
|
235
|
+
return f;
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
return { claims: out, summary: summarizeLogicClaims(out) };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function summarizeLogicClaims(claims) {
|
|
242
|
+
const s = { total: claims.length, corroborated: 0, refuted: 0, unverifiable: 0 };
|
|
243
|
+
for (const c of claims) {
|
|
244
|
+
const v = c.consensus?.verdict;
|
|
245
|
+
if (v === 'upheld') s.corroborated++;
|
|
246
|
+
else if (v === 'refuted') s.refuted++;
|
|
247
|
+
else s.unverifiable++;
|
|
248
|
+
}
|
|
249
|
+
return s;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** One line. Leads with what could not be corroborated. */
|
|
253
|
+
export function renderLogicClaimSummary(s) {
|
|
254
|
+
if (!s || !s.total) return null;
|
|
255
|
+
const bits = [`${s.total} business-logic claim(s)`];
|
|
256
|
+
if (s.refuted) bits.push(`${s.refuted} REFUTED by a deterministic lens (quarantined, not deleted)`);
|
|
257
|
+
if (s.unverifiable) bits.push(`${s.unverifiable} unverifiable — no lens could agree or disagree`);
|
|
258
|
+
if (s.corroborated) bits.push(`${s.corroborated} corroborated`);
|
|
259
|
+
return bits.join('; ') + '.';
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Not exported: the quotation verifier id, the claim-kind vocabulary and the
|
|
263
|
+
// batch summariser have no consumer outside this module. Kept internal rather
|
|
264
|
+
// than exported-and-unused — an export with no call site is how dead code gets
|
|
265
|
+
// shipped and then trusted.
|
|
266
|
+
export const _internals = { AUTH_HINTS, OWNERSHIP_HINTS, _enclosingBlock, QUOTE_WINDOW, CLAIM_KINDS, VERIFIER_QUOTATION, summarizeLogicClaims };
|
|
@@ -15,10 +15,10 @@
|
|
|
15
15
|
import * as fs from 'node:fs';
|
|
16
16
|
import * as path from 'node:path';
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
import { statePath, stateWritesEnabled } from './state-dir.js';
|
|
19
19
|
|
|
20
20
|
function _readJson(scanRoot, name) {
|
|
21
|
-
try { return JSON.parse(fs.readFileSync(
|
|
21
|
+
try { return JSON.parse(fs.readFileSync(statePath(scanRoot, name), 'utf8')); } catch { return null; }
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
/**
|
|
@@ -50,7 +50,8 @@ export function diffValidatorRuns(runA, runB) {
|
|
|
50
50
|
* Persist a model-rescan report. Returns the file path.
|
|
51
51
|
*/
|
|
52
52
|
export function persistRescanReport(scanRoot, from, to, changed) {
|
|
53
|
-
const dir =
|
|
53
|
+
const dir = statePath(scanRoot, 'model-rescan');
|
|
54
|
+
if (!stateWritesEnabled()) return null;
|
|
54
55
|
try { fs.mkdirSync(dir, { recursive: true }); } catch {}
|
|
55
56
|
const safe = (s) => String(s || 'unknown').replace(/[^\w.-]/g, '-');
|
|
56
57
|
const fp = path.join(dir, `${safe(from)}-vs-${safe(to)}.json`);
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
import * as fs from 'node:fs';
|
|
28
28
|
import * as path from 'node:path';
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
|
|
31
|
+
import { statePath } from './state-dir.js';
|
|
31
32
|
const K8S_DIRS = ['k8s', 'infra/k8s', 'deploy/k8s', 'kubernetes', 'manifests'];
|
|
32
33
|
|
|
33
34
|
const INTERNAL_CIDRS = [
|
|
@@ -81,7 +82,7 @@ function parseKubeManifests(scanRoot) {
|
|
|
81
82
|
|
|
82
83
|
export function loadNetworkPosture(scanRoot) {
|
|
83
84
|
const root = scanRoot || process.cwd();
|
|
84
|
-
const digestPath =
|
|
85
|
+
const digestPath = statePath(root, 'network-policy.json');
|
|
85
86
|
if (fs.existsSync(digestPath)) {
|
|
86
87
|
try {
|
|
87
88
|
const data = JSON.parse(fs.readFileSync(digestPath, 'utf8'));
|