@clear-capabilities/agentic-security-scanner 0.134.0 → 0.136.9
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 +432 -0
- package/bin/agentic-security-audit.js +2 -1
- package/bin/agentic-security-consistency.js +2 -1
- package/bin/agentic-security.js +448 -74
- package/dist/113.index.js +16 -7
- package/dist/117.index.js +3 -1
- package/dist/178.index.js +1 -1
- package/dist/207.index.js +5 -4
- package/dist/220.index.js +5 -3
- package/dist/238.index.js +4 -4
- package/dist/317.index.js +300 -0
- package/dist/384.index.js +1 -1
- package/dist/435.index.js +196 -21
- package/dist/444.index.js +20 -11
- package/dist/449.index.js +8 -1
- package/dist/513.index.js +7 -3
- package/dist/526.index.js +6 -6
- 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 +14 -14
- 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 +24 -12
- package/src/badge.js +2 -1
- package/src/dataflow/CLAUDE.md +10 -4
- package/src/dataflow/builtin-summaries.js +1 -1
- package/src/dataflow/cross-service-taint.js +2 -1
- package/src/dataflow/engine.js +324 -60
- package/src/dataflow/ifds-precise.js +6 -4
- package/src/dataflow/implicit-flow.js +68 -36
- package/src/dataflow/incremental.js +25 -8
- package/src/dataflow/index.js +2 -1
- package/src/dataflow/proven-clean.js +41 -0
- package/src/dataflow/sanitizer-gate.js +35 -9
- package/src/dataflow/sanitizer-proof.js +21 -3
- package/src/dataflow/stub-aware-filter.js +36 -13
- package/src/dataflow/summaries.js +21 -2
- package/src/discovery/CLAUDE.md +10 -0
- package/src/discovery/index.js +175 -3
- package/src/discovery/llm-invoke.js +90 -1
- package/src/discovery/memory.js +163 -0
- package/src/engine.js +247 -50
- package/src/integrations/tickets.js +7 -6
- package/src/ir/CLAUDE.md +4 -1
- package/src/ir/balanced-call.js +55 -0
- package/src/ir/ir-stats.js +1 -1
- package/src/ir/parser-cpp.js +1 -1
- package/src/ir/parser-cs.js +62 -9
- package/src/ir/parser-go.js +29 -11
- package/src/ir/parser-java.js +96 -19
- package/src/ir/parser-js.js +151 -20
- package/src/ir/parser-php.js +44 -9
- package/src/ir/parser-rb.js +37 -7
- package/src/ir/ssa.js +6 -1
- package/src/leaderboard.js +3 -2
- package/src/llm-validator/consistency.js +6 -2
- package/src/llm-validator/index.js +1 -2
- package/src/lsp/server.js +28 -2
- package/src/mcp/CLAUDE.md +9 -2
- package/src/mcp/audit.js +2 -1
- package/src/mcp/redact.js +26 -0
- package/src/mcp/tools.js +159 -17
- package/src/posture/CLAUDE.md +45 -8
- package/src/posture/accuracy-scorecard.js +67 -1
- package/src/posture/agents-memory.js +5 -3
- package/src/posture/aibom.js +12 -8
- package/src/posture/auditor-walkthrough.js +111 -10
- package/src/posture/auth-posture-import.js +5 -4
- package/src/posture/autopilot.js +8 -1
- package/src/posture/calibration-drift.js +11 -5
- package/src/posture/calibration.js +24 -2
- package/src/posture/compliance-frameworks/nist-privacy-1-1.json +846 -0
- package/src/posture/compliance-frameworks/owasp-asvs-5.json +1 -1
- package/src/posture/compliance-policy.js +40 -10
- package/src/posture/confidence.js +44 -10
- package/src/posture/corpus-enroll.js +9 -5
- package/src/posture/corpus-match.js +19 -0
- package/src/posture/csharp-analysis.js +62 -3
- 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/deploy-platform.js +4 -1
- package/src/posture/deterministic.js +3 -2
- package/src/posture/drift.js +7 -1
- package/src/posture/epss.js +13 -1
- package/src/posture/evidence-bundle.js +276 -0
- package/src/posture/exploitability-probability.js +15 -2
- package/src/posture/falsification.js +23 -2
- 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/fix-verify-loop.js +10 -1
- package/src/posture/grader-calibration.js +3 -4
- package/src/posture/iac-reachability.js +14 -8
- package/src/posture/integrity.js +25 -7
- 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/model-rescan.js +69 -3
- package/src/posture/mttr.js +5 -0
- package/src/posture/network-policy-import.js +3 -2
- package/src/posture/poc-inprocess.js +27 -8
- 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/regression-test-gen.js +23 -8
- package/src/posture/reverse-blast-radius.js +5 -1
- package/src/posture/risk-dollars.js +20 -3
- 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/secret-history.js +20 -11
- package/src/posture/security-trend.js +7 -1
- package/src/posture/stack-playbook.js +22 -1
- 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 +13 -3
- package/src/posture/time-to-fix.js +3 -2
- package/src/posture/triage-memory.js +3 -2
- package/src/posture/validator-metrics.js +10 -3
- package/src/posture/verifier.js +32 -57
- package/src/posture/waf-ingest.js +6 -5
- package/src/posture/watch-mode.js +4 -3
- package/src/report/index.js +183 -14
- package/src/runScan.js +1 -1
- package/src/sast/_comment-strip.js +15 -4
- package/src/sast/_secret-entropy.js +1 -1
- package/src/sast/authz.js +6 -4
- package/src/sast/bench-shape/index.js +2 -7
- package/src/sast/claude-md-prompt-injection.js +14 -3
- package/src/sast/cloud-iam.js +60 -7
- package/src/sast/code-injection-multilang.js +29 -0
- package/src/sast/cpp-bench-extras.js +1 -1
- package/src/sast/csrf.js +7 -5
- package/src/sast/env-hygiene.js +5 -2
- package/src/sast/iac-terraform.js +25 -0
- package/src/sast/java-bench-extras.js +1 -1
- package/src/sast/java-constant-fold.js +5 -5
- package/src/sast/llm-owasp.js +4 -2
- package/src/sast/mcp-audit.js +7 -0
- package/src/sast/pipeline.js +8 -0
- package/src/sast/prompt-template.js +8 -6
- package/src/sast/prototype-pollution.js +6 -2
- package/src/sast/redos-nfa.js +6 -6
- package/src/sast/secret-concat.js +13 -2
- package/src/sast/ssrf-cloud-metadata.js +6 -3
- package/src/sast/xss-reflected-multilang.js +1 -1
- package/src/sast/xxe.js +1 -1
- package/src/sca/CLAUDE.md +3 -4
- package/src/sca/container.js +35 -3
- package/src/sca/dep-confusion.js +9 -1
- package/src/sca/sarif-ingest.js +0 -187
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
//
|
|
9
9
|
// nist-csf-2 NIST Cybersecurity Framework 2.0
|
|
10
10
|
// nist-ai-600-1 NIST AI Risk Management Framework, GenAI profile
|
|
11
|
+
// nist-privacy-1-1 NIST Privacy Framework 1.1 (see posture/privacy-framework.js
|
|
12
|
+
// for the assessment + remediation layer over it)
|
|
11
13
|
// owasp-asvs-5 OWASP Application Security Verification Standard 5.0
|
|
12
14
|
// owasp-llm-top-10 OWASP Top 10 for LLM Applications 2025
|
|
13
15
|
// eu-ai-act EU AI Act (Regulation 2024/1689)
|
|
@@ -32,9 +34,8 @@
|
|
|
32
34
|
import * as fs from 'node:fs';
|
|
33
35
|
import * as path from 'node:path';
|
|
34
36
|
|
|
37
|
+
import { statePath, stateWritesEnabled } from './state-dir.js';
|
|
35
38
|
const BUNDLED_DIR = path.join(path.dirname(new URL(import.meta.url).pathname), 'compliance-frameworks');
|
|
36
|
-
const STATE = '.agentic-security';
|
|
37
|
-
|
|
38
39
|
function _readJson(fp) {
|
|
39
40
|
try { return JSON.parse(fs.readFileSync(fp, 'utf8')); } catch { return null; }
|
|
40
41
|
}
|
|
@@ -52,7 +53,7 @@ export function listFrameworks(scanRoot) {
|
|
|
52
53
|
}
|
|
53
54
|
} catch {}
|
|
54
55
|
if (scanRoot) {
|
|
55
|
-
const projDir =
|
|
56
|
+
const projDir = statePath(scanRoot, 'compliance');
|
|
56
57
|
if (fs.existsSync(projDir)) {
|
|
57
58
|
try {
|
|
58
59
|
for (const sub of fs.readdirSync(projDir)) {
|
|
@@ -73,7 +74,7 @@ export function listFrameworks(scanRoot) {
|
|
|
73
74
|
*/
|
|
74
75
|
export function loadFramework(scanRoot, id) {
|
|
75
76
|
if (scanRoot) {
|
|
76
|
-
const projFp =
|
|
77
|
+
const projFp = statePath(scanRoot, 'compliance', id, 'controls.json');
|
|
77
78
|
if (fs.existsSync(projFp)) return _readJson(projFp);
|
|
78
79
|
}
|
|
79
80
|
for (const fn of fs.readdirSync(BUNDLED_DIR)) {
|
|
@@ -97,11 +98,69 @@ export function loadFramework(scanRoot, id) {
|
|
|
97
98
|
* 'absent' — no signal / open critical findings on every mapsTo family
|
|
98
99
|
* 'manual' — control has no mapsTo (requires manual attestation)
|
|
99
100
|
*/
|
|
101
|
+
// Stage 6 correctness audit: several compliance frameworks map controls to
|
|
102
|
+
// `family:auth-missing` / `family:authz`, but no detector in this codebase
|
|
103
|
+
// ever emits those literal family strings — the real missing-auth/authz
|
|
104
|
+
// detectors use `broken-access-control` (generic), `fastapi-missing-auth`,
|
|
105
|
+
// `springboot-missing-authz`, `laravel-missing-auth`, `quarkus-missing-authz`
|
|
106
|
+
// (framework-specific). Without this alias, a control mapped to
|
|
107
|
+
// auth-missing/authz read "present" (vacuously — the family bucket was
|
|
108
|
+
// always empty) even with a critical, unauthenticated route open. Listed
|
|
109
|
+
// under both compliance-side names since `broken-access-control` covers
|
|
110
|
+
// both "nobody checked" (missing auth) and "checked wrong" (broken authz)
|
|
111
|
+
// and it is strictly safer to over-count a real finding against both than
|
|
112
|
+
// to keep silently excluding it from either.
|
|
113
|
+
// CMP-1 (Stage 6 follow-up): grown incrementally by cross-referencing every
|
|
114
|
+
// `family:` string the bundled compliance-frameworks/*.json files reference
|
|
115
|
+
// against what src/sast + src/posture actually emit (a small, closed
|
|
116
|
+
// problem — only the strings a control actually maps to, not a universal
|
|
117
|
+
// vocabulary registry). k8s-admission.js's real rule id is
|
|
118
|
+
// `k8s-pod-privileged`; `nist-csf-2.json`/`hipaa-security-rule.json` map to
|
|
119
|
+
// the compliance-side spelling `k8s-pod-security-privileged`, which no
|
|
120
|
+
// detector ever emitted.
|
|
121
|
+
const COMPLIANCE_FAMILY_ALIAS = {
|
|
122
|
+
'auth-missing': ['broken-access-control', 'fastapi-missing-auth', 'springboot-missing-authz', 'laravel-missing-auth', 'quarkus-missing-authz'],
|
|
123
|
+
'authz': ['broken-access-control', 'idor', 'springboot-missing-authz', 'quarkus-missing-authz'],
|
|
124
|
+
'k8s-pod-security-privileged': ['k8s-pod-privileged'],
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// CMP-1 audit trail: every `family:` string referenced by the bundled
|
|
128
|
+
// compliance-frameworks/*.json files was cross-checked against real
|
|
129
|
+
// detector output; entries above are the confirmed naming mismatches with a
|
|
130
|
+
// real detector to alias, and `mcp-audit.js`/`sca/dep-confusion.js` were
|
|
131
|
+
// fixed at the SOURCE (they now set `family` explicitly) rather than
|
|
132
|
+
// aliased, since the finding constructors themselves were the root cause.
|
|
133
|
+
// Four references have no matching detector at all — not a naming
|
|
134
|
+
// mismatch, a genuine coverage gap that would need a new rule, out of
|
|
135
|
+
// scope for an alias table: `crypto-tls-version` (crypto-protocol.js checks
|
|
136
|
+
// verify-disabled, not minimum-TLS-version — the file's own header comment
|
|
137
|
+
// even names the never-implemented `crypto-tls-min-version` rule id),
|
|
138
|
+
// `nosql-injection` (referenced by cross-lang-meta.js's chain-detection
|
|
139
|
+
// list, but no dedicated NoSQL-injection detector exists), `pii-exposure`
|
|
140
|
+
// and `data-exposure` (both referenced only by consumers — threat-model-
|
|
141
|
+
// auto.js's classifier and a Juliet-benchmark answer-key label
|
|
142
|
+
// respectively — with no producer). Every control mapped to one of these
|
|
143
|
+
// four reads `manual`/`engine-gap` rather than a false `present`, which is
|
|
144
|
+
// the safe failure mode this whole mechanism exists to guarantee.
|
|
100
145
|
export function evaluateFramework(scanRoot, fw, scan) {
|
|
146
|
+
// CMP-2: last-scan.json (what this is actually handed in production) carries
|
|
147
|
+
// findings across four separate channels — SAST (`findings`), secrets,
|
|
148
|
+
// business-logic, and SCA (`supplyChain`) — because report/index.js's
|
|
149
|
+
// normalizeFindings keeps them apart too. Reading only `scan.findings` made
|
|
150
|
+
// every secrets/logic/SCA finding invisible to every framework's family:
|
|
151
|
+
// mappings, e.g. a critical hardcoded secret never counted against a
|
|
152
|
+
// control mapped to family:hardcoded-secret. Each channel's family default
|
|
153
|
+
// mirrors normalizeFindings' own fallback for that channel so the two stay
|
|
154
|
+
// in agreement about what family an untagged finding belongs to.
|
|
101
155
|
const findings = (scan && Array.isArray(scan.findings)) ? scan.findings : [];
|
|
156
|
+
const secrets = ((scan && Array.isArray(scan.secrets)) ? scan.secrets : [])
|
|
157
|
+
.map(s => ({ ...s, family: s.family || 'hardcoded-secret' }));
|
|
158
|
+
const logicVulns = (scan && Array.isArray(scan.logicVulns)) ? scan.logicVulns : [];
|
|
159
|
+
const supplyChain = ((scan && Array.isArray(scan.supplyChain)) ? scan.supplyChain : [])
|
|
160
|
+
.map(sc => ({ ...sc, family: sc.family || 'vulnerable-dep' }));
|
|
102
161
|
const components = (scan && Array.isArray(scan.components)) ? scan.components : [];
|
|
103
162
|
const families = new Map();
|
|
104
|
-
for (const f of findings) {
|
|
163
|
+
for (const f of [...findings, ...secrets, ...logicVulns, ...supplyChain]) {
|
|
105
164
|
const k = f.family || 'unknown';
|
|
106
165
|
if (!families.has(k)) families.set(k, []);
|
|
107
166
|
families.get(k).push(f);
|
|
@@ -121,15 +180,43 @@ export function evaluateFramework(scanRoot, fw, scan) {
|
|
|
121
180
|
|
|
122
181
|
let allCleared = true;
|
|
123
182
|
let anySignal = false;
|
|
183
|
+
// Tracks whether ANY family:/module: mapping in this control actually
|
|
184
|
+
// passed (distinct from allCleared, which asks whether EVERY one did).
|
|
185
|
+
// Feeds the 'absent' vs 'partial' distinction below: a control where
|
|
186
|
+
// nothing at all checks out is a materially different auditor story
|
|
187
|
+
// ("no evidence") than one that's mostly clean with one open gap
|
|
188
|
+
// ("evidence with a gap") — both used to render as the same 'partial'.
|
|
189
|
+
let anyCleared = false;
|
|
190
|
+
// CMP-2: a rule: mapping's own observation says "verify manually" — it
|
|
191
|
+
// is deliberately not code-checked. It used to set anySignal=true and
|
|
192
|
+
// leave allCleared untouched, so a control whose ONLY mapping was rule:
|
|
193
|
+
// resolved to 'present' (fully evidenced), the same status as a control
|
|
194
|
+
// with real, checked evidence. Any rule: mapping present caps the
|
|
195
|
+
// control at 'partial' — never 'present' — regardless of what the
|
|
196
|
+
// family:/module: mappings in the same control found.
|
|
197
|
+
let hasUnverifiableMapping = false;
|
|
124
198
|
for (const m of maps) {
|
|
125
199
|
if (m.startsWith('family:')) {
|
|
126
|
-
|
|
127
|
-
|
|
200
|
+
// `family:X` and the subfamily-qualified `family:X:Y` (used by
|
|
201
|
+
// owasp-llm-top-10 for LLM02/LLM06/LLM07) both used to collapse to
|
|
202
|
+
// just `X` here — the `:Y` qualifier was parsed and then silently
|
|
203
|
+
// dropped, so any finding of family X counted against every control
|
|
204
|
+
// mapped to X regardless of which subfamily the control actually
|
|
205
|
+
// named (four LLM controls flagged off one credential-in-prompt
|
|
206
|
+
// finding). Detectors that emit a subfamily always set it, so
|
|
207
|
+
// filtering is safe; findings with no subfamily set still count
|
|
208
|
+
// (recall-preserving default — same precedent as relevance.js).
|
|
209
|
+
const [fam, subfam] = m.slice('family:'.length).split(':');
|
|
210
|
+
const aliasFams = COMPLIANCE_FAMILY_ALIAS[fam] || [];
|
|
211
|
+
const candidates = [fam, ...aliasFams].flatMap(k => families.get(k) || []);
|
|
212
|
+
const scoped = subfam ? candidates.filter(f => !f.subfamily || f.subfamily === subfam) : candidates;
|
|
213
|
+
const open = scoped.filter(f => !f.intentSuppressed && !f.pastDecision && (f.severity === 'critical' || f.severity === 'high'));
|
|
128
214
|
if (open.length) {
|
|
129
215
|
allCleared = false;
|
|
130
216
|
obs.push(`${open.length} open ${fam} finding(s) at high/critical.`);
|
|
131
217
|
} else {
|
|
132
218
|
obs.push(`✓ ${fam}: no open critical/high findings.`);
|
|
219
|
+
anyCleared = true;
|
|
133
220
|
}
|
|
134
221
|
anySignal = true;
|
|
135
222
|
} else if (m.startsWith('module:')) {
|
|
@@ -164,15 +251,16 @@ export function evaluateFramework(scanRoot, fw, scan) {
|
|
|
164
251
|
// A '.../' sentinel marks a source-relative artifact (project source,
|
|
165
252
|
// e.g. a hook or agent file) — resolve it against the scan root itself.
|
|
166
253
|
// Everything else is a runtime artifact under the STATE dir. Without
|
|
167
|
-
// this, `
|
|
254
|
+
// this, `statePath(scanRoot, '.../x')` never resolves and the
|
|
168
255
|
// control falsely reads "not present" for every project.
|
|
169
256
|
const resolved = !target ? null
|
|
170
257
|
: target.startsWith('.../') ? path.join(scanRoot, target.slice(4))
|
|
171
|
-
:
|
|
258
|
+
: statePath(scanRoot, target);
|
|
172
259
|
const label = target ? target.replace(/^\.\.\.\//, '') : '(unmapped)';
|
|
173
260
|
if (resolved && fs.existsSync(resolved)) {
|
|
174
261
|
obs.push(`✓ ${mod}: ${label} present.`);
|
|
175
262
|
anySignal = true;
|
|
263
|
+
anyCleared = true;
|
|
176
264
|
} else {
|
|
177
265
|
obs.push(`✗ ${mod}: expected ${label} not present.`);
|
|
178
266
|
allCleared = false;
|
|
@@ -181,11 +269,23 @@ export function evaluateFramework(scanRoot, fw, scan) {
|
|
|
181
269
|
// Could check whether a custom rule fires zero — leave a hint for now.
|
|
182
270
|
obs.push(`(rule mapping) ${m} — verify manually that the bodyguard rule is enabled.`);
|
|
183
271
|
anySignal = true;
|
|
272
|
+
hasUnverifiableMapping = true;
|
|
184
273
|
}
|
|
185
274
|
}
|
|
186
275
|
|
|
276
|
+
// 'absent' was documented (see the docstring above) as a fourth,
|
|
277
|
+
// distinct status — a control where NOTHING passed at all — but the
|
|
278
|
+
// assignment below never produced it; every non-present, non-manual
|
|
279
|
+
// control rendered as 'partial' regardless of whether it was "mostly
|
|
280
|
+
// clean with one gap" or "completely unevidenced". Only introduced for
|
|
281
|
+
// the fully-automated case (no rule: mapping) to avoid reclassifying
|
|
282
|
+
// any control that already reads 'partial' because of an inherently
|
|
283
|
+
// unverifiable rule: mapping — that precedent (rule: caps at 'partial',
|
|
284
|
+
// never reaching 'present' OR 'absent') is unchanged.
|
|
187
285
|
if (!anySignal) status = 'manual';
|
|
286
|
+
else if (hasUnverifiableMapping) status = 'partial';
|
|
188
287
|
else if (allCleared) status = 'present';
|
|
288
|
+
else if (!anyCleared) status = 'absent';
|
|
189
289
|
else status = 'partial';
|
|
190
290
|
|
|
191
291
|
results.push({ control: c, status, observations: obs });
|
|
@@ -251,7 +351,8 @@ export function renderWalkthrough(fw, evaluation, opts = {}) {
|
|
|
251
351
|
* Persist the walkthrough at .agentic-security/auditor-walkthroughs/<id>.md
|
|
252
352
|
*/
|
|
253
353
|
export function persistWalkthrough(scanRoot, fw, body) {
|
|
254
|
-
const dir =
|
|
354
|
+
const dir = statePath(scanRoot, 'auditor-walkthroughs');
|
|
355
|
+
if (!stateWritesEnabled()) return null;
|
|
255
356
|
try { fs.mkdirSync(dir, { recursive: true }); } catch {}
|
|
256
357
|
const fp = path.join(dir, `${fw.id}.md`);
|
|
257
358
|
try { fs.writeFileSync(fp, body); } catch {}
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
import * as fs from 'node:fs';
|
|
28
28
|
import * as path from 'node:path';
|
|
29
29
|
|
|
30
|
+
import { statePath } from './state-dir.js';
|
|
30
31
|
const KNOWN_GOOD = new Set([
|
|
31
32
|
'session+csrf', 'session+admin', 'session+claim', 'session',
|
|
32
33
|
'jwt+verify', 'oauth+pkce', 'mtls',
|
|
@@ -41,15 +42,15 @@ const FAMILIES_GATEABLE_BY_AUTH = new Set([
|
|
|
41
42
|
]);
|
|
42
43
|
|
|
43
44
|
const CANDIDATE_PATHS = [
|
|
44
|
-
'
|
|
45
|
-
'
|
|
46
|
-
'
|
|
45
|
+
'auth-posture.json',
|
|
46
|
+
'auth-posture.yml',
|
|
47
|
+
'auth-posture.yaml',
|
|
47
48
|
];
|
|
48
49
|
|
|
49
50
|
export function loadAuthPosture(scanRoot) {
|
|
50
51
|
const root = scanRoot || process.cwd();
|
|
51
52
|
for (const rel of CANDIDATE_PATHS) {
|
|
52
|
-
const fp =
|
|
53
|
+
const fp = statePath(root, rel);
|
|
53
54
|
if (!fs.existsSync(fp)) continue;
|
|
54
55
|
try {
|
|
55
56
|
const text = fs.readFileSync(fp, 'utf8');
|
package/src/posture/autopilot.js
CHANGED
|
@@ -110,7 +110,14 @@ export async function runAutopilot({
|
|
|
110
110
|
for (const f of inScope) {
|
|
111
111
|
const key = f.stableId || `${f.file}:${f.line}:${f.vuln}`;
|
|
112
112
|
const prior = resume ? state.findings[key] : null;
|
|
113
|
-
|
|
113
|
+
// A cached VERIFIED_FIXED whose patch was gated (not written) on a prior
|
|
114
|
+
// apply:false run is not a terminal outcome once the caller asks for
|
|
115
|
+
// apply:true — replaying it verbatim would silently never call
|
|
116
|
+
// applyFix on the exact two-step workflow (preview, then --apply) this
|
|
117
|
+
// gate exists to support. Everything else cached IS terminal and is
|
|
118
|
+
// replayed as before.
|
|
119
|
+
const gatedOnlyByApply = prior?.outcome === 'VERIFIED_FIXED' && prior.applied !== true && apply === true;
|
|
120
|
+
if (prior?.outcome && !gatedOnlyByApply) { results.push(prior); continue; }
|
|
114
121
|
|
|
115
122
|
const rec = { key, file: f.file, line: f.line, vuln: f.vuln, severity: f.severity };
|
|
116
123
|
|
|
@@ -57,12 +57,18 @@ export function computeDrift(scanRoot, opts = {}) {
|
|
|
57
57
|
const fb = loadTriageFeedback(scanRoot);
|
|
58
58
|
if (!fb.length) return { alarms: [], note: 'no-feedback-data' };
|
|
59
59
|
|
|
60
|
-
// Group by family. Each entry should carry: family, verdict ('tp'|'fp'|'
|
|
61
|
-
// reportedConfidence (0..1),
|
|
60
|
+
// Group by family. Each entry should carry: family, verdict ('tp'|'fp'|'wontfix'),
|
|
61
|
+
// reportedConfidence (0..1), at.
|
|
62
|
+
// Field names/verdict values here previously didn't match the real
|
|
63
|
+
// producer (commands/triage.md's Step 2): it writes `at`, not `ts`, and
|
|
64
|
+
// never wrote `reportedConfidence` at all (fixed there alongside this —
|
|
65
|
+
// Stage 2 measurement-completeness audit) or a `'wai'` verdict (it only
|
|
66
|
+
// ever writes 'tp'|'fp'|'wontfix'), so this alarm could never fire against
|
|
67
|
+
// real triage data regardless of how badly calibration had drifted.
|
|
62
68
|
const byFamily = new Map();
|
|
63
69
|
for (const e of fb) {
|
|
64
|
-
if (!e || !e.family || !inWindow(e.
|
|
65
|
-
if (!['tp', 'fp', '
|
|
70
|
+
if (!e || !e.family || !inWindow(e.at, window)) continue;
|
|
71
|
+
if (!['tp', 'fp', 'wontfix'].includes(e.verdict)) continue;
|
|
66
72
|
if (typeof e.reportedConfidence !== 'number') continue;
|
|
67
73
|
if (!byFamily.has(e.family)) byFamily.set(e.family, []);
|
|
68
74
|
byFamily.get(e.family).push(e);
|
|
@@ -75,7 +81,7 @@ export function computeDrift(scanRoot, opts = {}) {
|
|
|
75
81
|
const reportedAcc = entries.reduce((acc, e) => acc + e.reportedConfidence, 0) / entries.length;
|
|
76
82
|
const divergence = Math.abs(reportedAcc - realizedAcc);
|
|
77
83
|
if (divergence < threshold) continue;
|
|
78
|
-
const firstTs = entries.map(e => e.
|
|
84
|
+
const firstTs = entries.map(e => e.at).filter(Boolean).sort()[0];
|
|
79
85
|
alarms.push({
|
|
80
86
|
alarm: true,
|
|
81
87
|
since: firstTs,
|
|
@@ -121,15 +121,37 @@ export function loadCalibrationHistory(scanRoot) {
|
|
|
121
121
|
};
|
|
122
122
|
if (seed) merge(seed);
|
|
123
123
|
if (customer) merge(customer);
|
|
124
|
-
// Merge triage-derived TP/FP counts (auto-feedback loop)
|
|
124
|
+
// Merge triage-derived TP/FP counts (auto-feedback loop).
|
|
125
|
+
//
|
|
126
|
+
// EA-01: only a state a human actually chose is a real signal. triage.js's
|
|
127
|
+
// syncWithScan sets every new finding to 'open' with no human involved, and
|
|
128
|
+
// auto-closes any finding the scanner stops seeing to 'fixed' with
|
|
129
|
+
// `automatic:true` on the transition — neither reflects a judgment that the
|
|
130
|
+
// finding was real. Counting them as TP made the "true positive" column
|
|
131
|
+
// entirely machine-generated while the FP column required an explicit human
|
|
132
|
+
// false-positive mark, so calibrated confidence trended toward 1.0 for any
|
|
133
|
+
// family with enough scan volume regardless of actual accuracy. Only a
|
|
134
|
+
// manual 'fixed' transition (confirmed real, now resolved) counts as TP;
|
|
135
|
+
// only 'false-positive' counts as FP. 'open', 'in-progress', 'wont-fix',
|
|
136
|
+
// and an automatically-reached 'fixed' contribute to neither bucket.
|
|
125
137
|
try {
|
|
126
138
|
const triage = _readJsonMaybe(statePath(scanRoot, 'triage.json'));
|
|
127
139
|
if (triage && triage.findings) {
|
|
140
|
+
const lastFixedIsAutomatic = new Map();
|
|
141
|
+
for (const t of triage.transitions || []) {
|
|
142
|
+
if (t && t.to === 'fixed') lastFixedIsAutomatic.set(t.id, t.automatic === true);
|
|
143
|
+
}
|
|
128
144
|
const triageFams = {};
|
|
129
145
|
for (const f of Object.values(triage.findings)) {
|
|
130
146
|
const fam = f.family || 'unknown';
|
|
131
147
|
if (!triageFams[fam]) triageFams[fam] = { tp: 0, fp: 0 };
|
|
132
|
-
|
|
148
|
+
// Require POSITIVE proof of a manual transition (=== false), not
|
|
149
|
+
// merely the absence of proof of an automatic one (!== true) — the
|
|
150
|
+
// latter fails OPEN: a finding with no transition record at all
|
|
151
|
+
// (lastFixedIsAutomatic.get returns undefined) counted as a manual
|
|
152
|
+
// TP by default, exactly the machine-generated-signal failure mode
|
|
153
|
+
// this whole block's comment states it exists to prevent.
|
|
154
|
+
if (f.state === 'fixed' && lastFixedIsAutomatic.get(f.id) === false) triageFams[fam].tp++;
|
|
133
155
|
else if (f.state === 'false-positive') triageFams[fam].fp++;
|
|
134
156
|
}
|
|
135
157
|
merge({ families: triageFams });
|