@clear-capabilities/agentic-security-scanner 0.136.2 → 0.137.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 +880 -0
- package/bin/agentic-security.js +189 -37
- package/dist/113.index.js +13 -4
- package/dist/178.index.js +1 -1
- package/dist/207.index.js +5 -4
- package/dist/238.index.js +1 -1
- package/dist/317.index.js +36 -6
- package/dist/384.index.js +1 -1
- package/dist/435.index.js +192 -15
- package/dist/444.index.js +20 -11
- package/dist/449.index.js +8 -1
- package/dist/526.index.js +3 -3
- package/dist/637.index.js +1 -1
- package/dist/agentic-security.mjs +15 -15
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/dist/compliance-frameworks/nist-privacy-1-1.json +2 -2
- package/dist/compliance-frameworks/owasp-asvs-5.json +1 -1
- package/package.json +21 -13
- package/src/dataflow/CLAUDE.md +12 -4
- package/src/dataflow/builtin-summaries.js +1 -1
- package/src/dataflow/catalog-expanded.js +1 -0
- package/src/dataflow/catalog.js +157 -31
- package/src/dataflow/engine.js +639 -112
- package/src/dataflow/implicit-flow.js +68 -36
- package/src/dataflow/incremental.js +18 -3
- package/src/dataflow/index.js +17 -1
- package/src/dataflow/points-to.js +19 -6
- 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/engine.js +430 -196
- package/src/ir/CLAUDE.md +16 -2
- package/src/ir/balanced-call.js +55 -0
- package/src/ir/class-hierarchy.js +57 -11
- package/src/ir/index.js +14 -2
- package/src/ir/parser-cs.js +513 -40
- package/src/ir/parser-go.js +29 -11
- package/src/ir/parser-java.js +300 -20
- package/src/ir/parser-js.js +300 -22
- package/src/ir/parser-kt.js +436 -18
- package/src/ir/parser-php.js +631 -38
- package/src/ir/parser-py.helper.py +32 -2
- package/src/ir/parser-py.js +31 -4
- package/src/ir/parser-rb.js +161 -26
- package/src/ir/ssa.js +6 -1
- package/src/lsp/server.js +35 -3
- package/src/mcp/CLAUDE.md +9 -2
- package/src/mcp/redact.js +26 -0
- package/src/mcp/tools.js +164 -15
- package/src/posture/CLAUDE.md +19 -7
- package/src/posture/accuracy-scorecard.js +9 -1
- package/src/posture/aibom.js +12 -8
- package/src/posture/auditor-walkthrough.js +102 -3
- 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/clustering.js +12 -1
- package/src/posture/compliance-frameworks/nist-privacy-1-1.json +2 -2
- package/src/posture/compliance-frameworks/owasp-asvs-5.json +1 -1
- package/src/posture/compliance-policy.js +33 -1
- 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/deploy-platform.js +4 -1
- package/src/posture/drift.js +7 -1
- package/src/posture/epss.js +13 -1
- package/src/posture/evidence-bundle.js +36 -6
- package/src/posture/exploitability-probability.js +13 -1
- package/src/posture/falsification.js +23 -2
- package/src/posture/fix-metrics.js +1 -1
- package/src/posture/fix-verify-loop.js +10 -1
- package/src/posture/iac-reachability.js +14 -8
- package/src/posture/integrity.js +25 -7
- package/src/posture/model-rescan.js +65 -0
- package/src/posture/mttr.js +5 -0
- package/src/posture/poc-inprocess.js +27 -8
- package/src/posture/regression-test-gen.js +23 -8
- package/src/posture/reverse-blast-radius.js +5 -1
- package/src/posture/risk-dollars.js +18 -1
- package/src/posture/sbom.js +2 -2
- 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/threat-model-grounding.js +2 -2
- package/src/posture/validator-metrics.js +10 -3
- package/src/posture/verifier.js +32 -57
- 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/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 +7 -0
- package/src/sca/sarif-ingest.js +0 -187
package/dist/435.index.js
CHANGED
|
@@ -124,6 +124,32 @@ const PATTERNS = [
|
|
|
124
124
|
[/rk_(?:live|test)_[A-Za-z0-9]{20,}/g, 'stripe-restricted-key'],
|
|
125
125
|
[/SG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}/g, 'sendgrid-key'],
|
|
126
126
|
[/AIza[0-9A-Za-z_-]{35}/g, 'google-api-key'],
|
|
127
|
+
// Stage 4 correctness audit (coverage breadth, AI security): this list
|
|
128
|
+
// only covered a small subset of what the scanner's OWN credential
|
|
129
|
+
// detector (engine.js's CREDENTIAL_PATTERNS, 40+ provider shapes) finds
|
|
130
|
+
// — a Shopify/Telegram/Twilio/Discord-webhook/Square/Google-OAuth/JDBC
|
|
131
|
+
// secret detected and reported by a scan reached explain_finding's
|
|
132
|
+
// output completely unredacted, because none of those shapes were in
|
|
133
|
+
// THIS separate, narrower list. Reusing the same regex bodies as
|
|
134
|
+
// engine.js's CREDENTIAL_PATTERNS for the shapes verified to leak
|
|
135
|
+
// (rather than importing engine.js itself, which would pull its entire
|
|
136
|
+
// multi-thousand-line module graph into the MCP server's dependency
|
|
137
|
+
// surface for a handful of consts).
|
|
138
|
+
[/ya29\.[0-9A-Za-z_-]{20,}/g, 'google-oauth-token'],
|
|
139
|
+
[/shp(?:at|ss|ca|pa)_[a-fA-F0-9]{32}/g, 'shopify-token'],
|
|
140
|
+
[/(?<![0-9])[0-9]{8,10}:AA[0-9A-Za-z_-]{33}(?![A-Za-z0-9_])/g, 'telegram-bot-token'],
|
|
141
|
+
[/twilio.{0,20}SK[0-9a-fA-F]{32}/gi, 'twilio-api-key'],
|
|
142
|
+
[/sq0atp-[0-9A-Za-z_-]{22}/g, 'square-access-token'],
|
|
143
|
+
[/sq0csp-[0-9A-Za-z_-]{43}/g, 'square-oauth-secret'],
|
|
144
|
+
[/access_token\$production\$[0-9a-z]{16}\$[0-9a-f]{32}/g, 'paypal-braintree-token'],
|
|
145
|
+
[/https:\/\/(?:discordapp|discord)\.com\/api\/webhooks\/[0-9]+\/[A-Za-z0-9_-]+/g, 'discord-webhook'],
|
|
146
|
+
[/https:\/\/hooks\.slack\.com\/services\/T[a-zA-Z0-9_]{8}\/B[a-zA-Z0-9_]{8,12}\/[a-zA-Z0-9_]{24}/g, 'slack-webhook'],
|
|
147
|
+
[/https:\/\/outlook\.office\.com\/webhook\/[A-Za-z0-9\-@]+\/IncomingWebhook\/[A-Za-z0-9-]+\/[A-Za-z0-9-]+/g, 'teams-webhook'],
|
|
148
|
+
[/https:\/\/(?:www\.)?hooks\.zapier\.com\/hooks\/catch\/[A-Za-z0-9]+\/[A-Za-z0-9]+\//g, 'zapier-webhook'],
|
|
149
|
+
// JDBC connection string carrying a password: only redact when password
|
|
150
|
+
// evidence is actually on the line (matches engine.js's own ctx gate),
|
|
151
|
+
// so a credential-free JDBC URL in docs isn't needlessly mangled.
|
|
152
|
+
[/jdbc:[a-z:]+:\/\/[A-Za-z0-9.\-_:;=/@?,&]*(?:@|password=|passwd=|pwd=)[A-Za-z0-9.\-_:;=/@?,&]*/gi, 'jdbc-connection-string'],
|
|
127
153
|
// JWT — three dot-separated b64url segments starting with eyJ
|
|
128
154
|
[/eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g, 'jwt'],
|
|
129
155
|
// PEM-encoded private keys
|
|
@@ -171,6 +197,8 @@ function redactArgsBlob(s) {
|
|
|
171
197
|
return redactString(s);
|
|
172
198
|
}
|
|
173
199
|
|
|
200
|
+
// EXTERNAL MODULE: ./src/report/index.js + 2 modules
|
|
201
|
+
var report = __webpack_require__(3550);
|
|
174
202
|
// EXTERNAL MODULE: ./src/posture/state-dir.js
|
|
175
203
|
var state_dir = __webpack_require__(1174);
|
|
176
204
|
;// CONCATENATED MODULE: ./src/posture/agents-memory.js
|
|
@@ -470,6 +498,7 @@ const cve_lookup_internals = { CACHE_DIR, CVE_RE, _stalenessTier };
|
|
|
470
498
|
|
|
471
499
|
|
|
472
500
|
|
|
501
|
+
|
|
473
502
|
// Lazy-loaded: these transitively pull in npm packages (@babel/core and
|
|
474
503
|
// friends) that aren't available in the plugin-cache install path
|
|
475
504
|
// (no node_modules). Deferring keeps the MCP server bootable everywhere;
|
|
@@ -600,8 +629,13 @@ function _validateScratchpadPath(relPath) {
|
|
|
600
629
|
return { ok: true, agent, session, fileParts };
|
|
601
630
|
}
|
|
602
631
|
|
|
632
|
+
// Routes through the same lstat+realpath confinement every other write/
|
|
633
|
+
// path-taking tool uses (OWASP MCP05) — a lexical prefix/charset check
|
|
634
|
+
// alone doesn't stop a pre-planted symlink at any path component from
|
|
635
|
+
// relocating the write/read outside the session root. Throws on escape;
|
|
636
|
+
// callers must catch (see append_scratchpad / read_scratchpad).
|
|
603
637
|
function _scratchpadAbs(sessionRoot, relPath) {
|
|
604
|
-
return
|
|
638
|
+
return _confine(sessionRoot, relPath.replace(/\\/g, '/'), 'scratchpad path');
|
|
605
639
|
}
|
|
606
640
|
|
|
607
641
|
function _scratchpadTotalBytes(sessionRoot) {
|
|
@@ -780,17 +814,35 @@ const scan_diff = {
|
|
|
780
814
|
fileContents[rel] = content;
|
|
781
815
|
}
|
|
782
816
|
|
|
817
|
+
// PRD R1 (docs/DETECTION_GAP_REMEDIATION_PRD.md): deep mode is default-on
|
|
818
|
+
// for the interactive CLI scan but was never requested here, so an
|
|
819
|
+
// agent's pre-write self-correction scan was regex/AST-only — blind to
|
|
820
|
+
// any bug whose source and sink are connected only through a call
|
|
821
|
+
// (`fileContents` scopes the deep engine's IR to exactly the files
|
|
822
|
+
// passed in, same bound this tool already enforces via MAX_FILES_PER_SCAN
|
|
823
|
+
// / MAX_TOTAL_SCAN_BYTES, so this does not turn scan_diff into a
|
|
824
|
+
// full-project deep scan).
|
|
783
825
|
const runScan = await getRunScan();
|
|
784
|
-
const result = await runScan(sessionRoot, { network: false, fileContents });
|
|
826
|
+
const result = await runScan(sessionRoot, { network: false, fileContents, deep: true, deepInCi: true });
|
|
785
827
|
const wantSet = new Set(Object.keys(fileContents));
|
|
786
828
|
const sevRank = { info: 0, low: 1, medium: 2, high: 3, critical: 4 };
|
|
787
829
|
const min = sevRank[severity] ?? 0;
|
|
788
|
-
|
|
830
|
+
// Stage 6 correctness audit: this only ever read result.scan.findings
|
|
831
|
+
// (the SAST channel) — scan.secrets and scan.logicVulns are separate
|
|
832
|
+
// arrays on the raw runScan() result (report/index.js's normalizeFindings
|
|
833
|
+
// is what merges all four channels, and that merge hasn't run yet here).
|
|
834
|
+
// A file containing a bare hardcoded credential reported findingCount: 0
|
|
835
|
+
// through a tool whose own description promises "Use BEFORE writing a
|
|
836
|
+
// Write/Edit to disk so the agent can self-correct". Also reused
|
|
837
|
+
// _remediationOf so a fix-string detector (the majority of engine.js's
|
|
838
|
+
// own, ~127 call sites) doesn't silently report an empty `description`
|
|
839
|
+
// the way reading only `.remediation` did.
|
|
840
|
+
const findings = [...(result.scan.findings || []), ...(result.scan.secrets || []), ...(result.scan.logicVulns || [])]
|
|
789
841
|
.filter(f => wantSet.has(String(f.file || '').replace(/\\/g, '/')) && (sevRank[f.severity] ?? 0) >= min)
|
|
790
842
|
.map(f => redactFinding({
|
|
791
843
|
id: f.id, severity: f.severity, file: f.file, line: f.line,
|
|
792
844
|
title: f.title || f.vuln, cwe: f.cwe,
|
|
793
|
-
description: f.description, remediation: f
|
|
845
|
+
description: f.description, remediation: (0,report/* _remediationOf */.uV)(f),
|
|
794
846
|
}));
|
|
795
847
|
// Harness-anatomy #1: offload when the result exceeds OFFLOAD_THRESHOLD.
|
|
796
848
|
// The agent gets a head+tail preview plus a path it can page through;
|
|
@@ -953,10 +1005,40 @@ const apply_fix = {
|
|
|
953
1005
|
additionalProperties: { type: 'string', maxLength: 500_000 },
|
|
954
1006
|
minProperties: 1, maxProperties: 8,
|
|
955
1007
|
},
|
|
1008
|
+
// Stage 6 correctness audit: same gap and same fix as verify_fix — the
|
|
1009
|
+
// honesty gate is reachable but was never wired to any real caller.
|
|
1010
|
+
// Here it's stronger than advisory: the inline re-verify below already
|
|
1011
|
+
// gates the WRITE on `verdict.ok`, and verifyFixCore's own `ok`
|
|
1012
|
+
// formula already folds in `honesty.ok` when fixMeta is supplied — so
|
|
1013
|
+
// passing it through here makes a dishonest fixMeta (hand-wave
|
|
1014
|
+
// residual, uncited false-positive verdict) block the write itself,
|
|
1015
|
+
// not just report a verdict.
|
|
1016
|
+
fixMeta: {
|
|
1017
|
+
type: 'object',
|
|
1018
|
+
additionalProperties: false,
|
|
1019
|
+
properties: {
|
|
1020
|
+
residual: { type: 'string', maxLength: 2000 },
|
|
1021
|
+
verdict: { type: 'string', maxLength: 64 },
|
|
1022
|
+
evidence: { type: 'array', items: { type: 'string', maxLength: 500 }, maxItems: 20 },
|
|
1023
|
+
signals: {
|
|
1024
|
+
type: 'object',
|
|
1025
|
+
additionalProperties: false,
|
|
1026
|
+
properties: {
|
|
1027
|
+
sinkSignatureChanged: { type: 'boolean' },
|
|
1028
|
+
allCallersRouted: { type: 'boolean' },
|
|
1029
|
+
testDiscriminates: { type: 'boolean' },
|
|
1030
|
+
rateLimitOnly: { type: 'boolean' },
|
|
1031
|
+
docsOnly: { type: 'boolean' },
|
|
1032
|
+
logOnlyNoReject: { type: 'boolean' },
|
|
1033
|
+
partialSanitization: { type: 'boolean' },
|
|
1034
|
+
},
|
|
1035
|
+
},
|
|
1036
|
+
},
|
|
1037
|
+
},
|
|
956
1038
|
},
|
|
957
1039
|
required: ['finding_id', 'confirm'],
|
|
958
1040
|
},
|
|
959
|
-
async handler({ finding_id, confirm, dry_run = false, patch = null }, ctx) {
|
|
1041
|
+
async handler({ finding_id, confirm, dry_run = false, patch = null, fixMeta = null }, ctx) {
|
|
960
1042
|
if (confirm !== true) {
|
|
961
1043
|
return { _meta: META, applied: false, reason: 'apply_fix requires confirm: true.' };
|
|
962
1044
|
}
|
|
@@ -1012,6 +1094,7 @@ const apply_fix = {
|
|
|
1012
1094
|
scanRoot: ctx.sessionRoot,
|
|
1013
1095
|
originalFindingStableId: f.stableId,
|
|
1014
1096
|
files: _files,
|
|
1097
|
+
fixMeta,
|
|
1015
1098
|
});
|
|
1016
1099
|
}
|
|
1017
1100
|
} catch (e) {
|
|
@@ -1021,7 +1104,7 @@ const apply_fix = {
|
|
|
1021
1104
|
return {
|
|
1022
1105
|
_meta: META, applied: false,
|
|
1023
1106
|
reason: `patch rejected by verifier: ${verdict.summary || verdict.rescan?.reason || 'did not verify'}`,
|
|
1024
|
-
verify: { rescan: verdict.rescan, lint: { runner: verdict.lint?.runner, ok: verdict.lint?.ok } },
|
|
1107
|
+
verify: { rescan: verdict.rescan, lint: { runner: verdict.lint?.runner, ok: verdict.lint?.ok }, honesty: verdict.honesty || null },
|
|
1025
1108
|
};
|
|
1026
1109
|
}
|
|
1027
1110
|
if (dry_run) {
|
|
@@ -1033,7 +1116,7 @@ const apply_fix = {
|
|
|
1033
1116
|
const originalContent = external_node_fs_.existsSync(v.abs) ? await promises_.readFile(v.abs, 'utf8') : '';
|
|
1034
1117
|
const entry = await (0,fix_history/* applyFix */.oM)({
|
|
1035
1118
|
scanRoot: ctx.sessionRoot, file: rel, originalContent, newContent: v.content,
|
|
1036
|
-
findingId: f.id, stableId: f.stableId, ruleId: f.
|
|
1119
|
+
findingId: f.id, stableId: f.stableId, ruleId: f.ruleId || f.cwe || f.family || null, vuln: f.vuln || f.title || null,
|
|
1037
1120
|
});
|
|
1038
1121
|
written.push({ file: rel, historyId: entry.id, backupPath: entry.backupPath });
|
|
1039
1122
|
}
|
|
@@ -1091,7 +1174,7 @@ const apply_fix = {
|
|
|
1091
1174
|
newContent: f.fix.replacement,
|
|
1092
1175
|
findingId: f.id,
|
|
1093
1176
|
stableId: f.stableId || null, // premortem 4R-8
|
|
1094
|
-
ruleId: f.
|
|
1177
|
+
ruleId: f.ruleId || f.cwe || f.family || null,
|
|
1095
1178
|
vuln: f.vuln || f.title || null,
|
|
1096
1179
|
});
|
|
1097
1180
|
} catch (e) {
|
|
@@ -1128,7 +1211,7 @@ const apply_fix = {
|
|
|
1128
1211
|
// proceed with apply_fix.
|
|
1129
1212
|
const verify_fix = {
|
|
1130
1213
|
name: 'verify_fix',
|
|
1131
|
-
description: 'Verify a proposed patch before applying. Re-scans the patched files in memory
|
|
1214
|
+
description: 'Verify a proposed patch before applying. Re-scans the patched files in memory, runs the project linter, runs the project test suite, checks fix honesty (FULL/MITIGATION/WORKAROUND) when fixMeta is supplied, and re-runs the PoC when one exists. Returns { ok, rescan, lint, tests, honesty, poc, summary }. Does not write to the target project’s own files, but DOES append one record per attempt to .agentic-security/fix-metrics.jsonl for the measured fix-loop.',
|
|
1132
1215
|
inputSchema: {
|
|
1133
1216
|
type: 'object',
|
|
1134
1217
|
additionalProperties: false,
|
|
@@ -1140,10 +1223,41 @@ const verify_fix = {
|
|
|
1140
1223
|
minProperties: 1,
|
|
1141
1224
|
maxProperties: 8,
|
|
1142
1225
|
},
|
|
1226
|
+
// Stage 6 correctness audit: posture/fix-honesty-gate.js's deterministic
|
|
1227
|
+
// honesty checks (vague-assurance residual prose, unbacked false-
|
|
1228
|
+
// positive verdicts, tier/residual consistency) were fully built and
|
|
1229
|
+
// fix-verify.js already consulted them when given a `fixMeta` — but
|
|
1230
|
+
// this schema never had a `fixMeta` property, so no call through the
|
|
1231
|
+
// MCP surface could ever supply one. The gate can only run against
|
|
1232
|
+
// claims the AGENT self-reports (residual risk, verdict, evidence,
|
|
1233
|
+
// completeness signals) — nothing here is server-computable — so
|
|
1234
|
+
// fixing this meant exposing the property, not inventing a lookup.
|
|
1235
|
+
fixMeta: {
|
|
1236
|
+
type: 'object',
|
|
1237
|
+
additionalProperties: false,
|
|
1238
|
+
properties: {
|
|
1239
|
+
residual: { type: 'string', maxLength: 2000 },
|
|
1240
|
+
verdict: { type: 'string', maxLength: 64 },
|
|
1241
|
+
evidence: { type: 'array', items: { type: 'string', maxLength: 500 }, maxItems: 20 },
|
|
1242
|
+
signals: {
|
|
1243
|
+
type: 'object',
|
|
1244
|
+
additionalProperties: false,
|
|
1245
|
+
properties: {
|
|
1246
|
+
sinkSignatureChanged: { type: 'boolean' },
|
|
1247
|
+
allCallersRouted: { type: 'boolean' },
|
|
1248
|
+
testDiscriminates: { type: 'boolean' },
|
|
1249
|
+
rateLimitOnly: { type: 'boolean' },
|
|
1250
|
+
docsOnly: { type: 'boolean' },
|
|
1251
|
+
logOnlyNoReject: { type: 'boolean' },
|
|
1252
|
+
partialSanitization: { type: 'boolean' },
|
|
1253
|
+
},
|
|
1254
|
+
},
|
|
1255
|
+
},
|
|
1256
|
+
},
|
|
1143
1257
|
},
|
|
1144
1258
|
required: ['stable_id', 'files'],
|
|
1145
1259
|
},
|
|
1146
|
-
async handler({ stable_id, files }, ctx) {
|
|
1260
|
+
async handler({ stable_id, files, fixMeta }, ctx) {
|
|
1147
1261
|
// Confine every file path before passing to the verifier.
|
|
1148
1262
|
const confined = {};
|
|
1149
1263
|
for (const [relPath, content] of Object.entries(files || {})) {
|
|
@@ -1155,17 +1269,48 @@ const verify_fix = {
|
|
|
1155
1269
|
confined[relPath] = String(content);
|
|
1156
1270
|
}
|
|
1157
1271
|
try {
|
|
1272
|
+
// The PoC-re-check leg (verifyFixCore's `pocLeg`) needs a `poc` param
|
|
1273
|
+
// to do anything — until now nothing supplied one, so it always
|
|
1274
|
+
// reported {status:'not-requested'} through this surface (see
|
|
1275
|
+
// posture/CLAUDE.md's disclosure). Rather than widening inputSchema
|
|
1276
|
+
// to make the CALLER pass PoC data back, look it up server-side: the
|
|
1277
|
+
// scan pipeline already attaches an HTTP-shaped f.poc to matching
|
|
1278
|
+
// findings by default (engine.js's annotatePocs), and last-scan.json
|
|
1279
|
+
// already carries it under the same stableId this handler receives.
|
|
1280
|
+
// Best-effort: a missing/unsigned/tampered scan just means no PoC is
|
|
1281
|
+
// available to re-check, not a verify_fix failure — the rescan/lint/
|
|
1282
|
+
// tests legs below are independent of this and still apply.
|
|
1283
|
+
let poc = null;
|
|
1284
|
+
try {
|
|
1285
|
+
const { scan: lastScan } = _readLastScanVerified(ctx.sessionRoot, { allowUnsigned: true });
|
|
1286
|
+
const orig = lastScan && (lastScan.findings || []).find(f => f.stableId === stable_id);
|
|
1287
|
+
if (orig && orig.poc && orig.poc.code) poc = { ...orig.poc, finding: orig };
|
|
1288
|
+
} catch { /* best-effort lookup; poc stays null */ }
|
|
1289
|
+
|
|
1158
1290
|
const verifyFixCore = await getVerifyFixCore();
|
|
1159
1291
|
const r = await verifyFixCore({
|
|
1160
1292
|
scanRoot: ctx.sessionRoot,
|
|
1161
1293
|
originalFindingStableId: stable_id,
|
|
1162
1294
|
files: confined,
|
|
1295
|
+
poc,
|
|
1296
|
+
fixMeta,
|
|
1163
1297
|
});
|
|
1164
1298
|
return {
|
|
1165
1299
|
_meta: META,
|
|
1166
1300
|
ok: r.ok,
|
|
1167
1301
|
rescan: { ok: r.rescan.ok, reason: r.rescan.reason, introduced: r.rescan.introduced || [] },
|
|
1168
1302
|
lint: { runner: r.lint.runner, ok: r.lint.ok, skipped: r.lint.skipped || false, output: redactString(r.lint.output || '').slice(0, 1500) },
|
|
1303
|
+
// verifyFix computes five legs, not two — tests/honesty/poc were
|
|
1304
|
+
// being silently dropped here, leaving an agent with no structured
|
|
1305
|
+
// way to see WHY verification failed when the failure was in one
|
|
1306
|
+
// of those three (only the free-text summary carried it).
|
|
1307
|
+
// test-runner.js's runProjectTests never returns raw stdout/stderr,
|
|
1308
|
+
// so no redaction is needed there; honesty.violations are static,
|
|
1309
|
+
// code-generated strings; poc.reason is redacted defensively since
|
|
1310
|
+
// it can echo proof-harness detail derived from scanned source.
|
|
1311
|
+
tests: r.tests,
|
|
1312
|
+
honesty: r.honesty,
|
|
1313
|
+
poc: r.poc ? { ...r.poc, reason: r.poc.reason ? redactString(r.poc.reason) : r.poc.reason } : r.poc,
|
|
1169
1314
|
summary: r.summary,
|
|
1170
1315
|
};
|
|
1171
1316
|
} catch (e) {
|
|
@@ -1249,7 +1394,13 @@ const synthesize_fix = {
|
|
|
1249
1394
|
regression_test: f.regression_test || null,
|
|
1250
1395
|
remediation: typeof fix.description === 'string' ? fix.description : (typeof fix === 'string' ? fix : null),
|
|
1251
1396
|
patchBounds: { touchedFiles, locDelta, oversized },
|
|
1252
|
-
|
|
1397
|
+
// oversized can only be true when hasReplacement is true (locDelta is
|
|
1398
|
+
// only computed in that branch, and touchedFiles never varies) — a
|
|
1399
|
+
// `!hasReplacement` conjunct here was a structural contradiction that
|
|
1400
|
+
// made this permanently false. The correct signal: the stored
|
|
1401
|
+
// replacement itself is too big to trust auto-applying, and there's
|
|
1402
|
+
// no safer deterministic alternative.
|
|
1403
|
+
recommendsFixPlan: oversized && !autofix,
|
|
1253
1404
|
};
|
|
1254
1405
|
},
|
|
1255
1406
|
};
|
|
@@ -1381,7 +1532,9 @@ const append_scratchpad = {
|
|
|
1381
1532
|
async handler({ path: relPath, content }, ctx) {
|
|
1382
1533
|
const v = _validateScratchpadPath(relPath);
|
|
1383
1534
|
if (!v.ok) return { _meta: META, ok: false, reason: v.reason };
|
|
1384
|
-
|
|
1535
|
+
let abs;
|
|
1536
|
+
try { abs = _scratchpadAbs(ctx.sessionRoot, relPath); }
|
|
1537
|
+
catch (e) { return { _meta: META, ok: false, reason: `path-escape refused: ${e.message}` }; }
|
|
1385
1538
|
const total = _scratchpadTotalBytes(ctx.sessionRoot);
|
|
1386
1539
|
if (total + content.length > SCRATCHPAD_MAX_TOTAL_BYTES) {
|
|
1387
1540
|
return {
|
|
@@ -1427,7 +1580,9 @@ const read_scratchpad = {
|
|
|
1427
1580
|
async handler({ path: relPath, offset, limit }, ctx) {
|
|
1428
1581
|
const v = _validateScratchpadPath(relPath);
|
|
1429
1582
|
if (!v.ok) return { _meta: META, ok: false, reason: v.reason };
|
|
1430
|
-
|
|
1583
|
+
let abs;
|
|
1584
|
+
try { abs = _scratchpadAbs(ctx.sessionRoot, relPath); }
|
|
1585
|
+
catch (e) { return { _meta: META, ok: false, reason: `path-escape refused: ${e.message}` }; }
|
|
1431
1586
|
if (!external_node_fs_.existsSync(abs)) return { _meta: META, ok: false, reason: 'not-found' };
|
|
1432
1587
|
let stat;
|
|
1433
1588
|
try { stat = external_node_fs_.statSync(abs); } catch (e) { return { _meta: META, ok: false, reason: `stat-failed: ${e.message}` }; }
|
|
@@ -1523,7 +1678,20 @@ const query_triage_memory = {
|
|
|
1523
1678
|
},
|
|
1524
1679
|
async handler({ query }, ctx) {
|
|
1525
1680
|
const { queryMemory } = await Promise.resolve(/* import() */).then(__webpack_require__.bind(__webpack_require__, 1905));
|
|
1526
|
-
const
|
|
1681
|
+
const raw = queryMemory(ctx.sessionRoot, query || '');
|
|
1682
|
+
// Stage 6 correctness audit: this returned queryMemory's output
|
|
1683
|
+
// verbatim, with no redaction pass — every other tool that echoes
|
|
1684
|
+
// scanned-source-derived text redacts it (mcp/CLAUDE.md's "Adding a new
|
|
1685
|
+
// tool" step 3). Round-trip through redactString the same way
|
|
1686
|
+
// redactFinding already does for its own opaque `.trace` field: results
|
|
1687
|
+
// here mix shapes (a triage decision's free-text `reason`, a finding's
|
|
1688
|
+
// `vuln`/`family`/file path), so scrubbing the whole serialized
|
|
1689
|
+
// structure catches secret-shaped substrings regardless of which field
|
|
1690
|
+
// they landed in, rather than hardcoding a field allowlist that could
|
|
1691
|
+
// miss one.
|
|
1692
|
+
let results;
|
|
1693
|
+
try { results = JSON.parse(redactString(JSON.stringify(raw))); }
|
|
1694
|
+
catch { results = raw; }
|
|
1527
1695
|
return {
|
|
1528
1696
|
_meta: META,
|
|
1529
1697
|
count: results.length,
|
|
@@ -1551,7 +1719,16 @@ const query_findings_memory = {
|
|
|
1551
1719
|
},
|
|
1552
1720
|
async handler({ query }, ctx) {
|
|
1553
1721
|
const { queryFindingsMemory } = await __webpack_require__.e(/* import() */ 839).then(__webpack_require__.bind(__webpack_require__, 3839));
|
|
1554
|
-
|
|
1722
|
+
const raw = queryFindingsMemory(ctx.sessionRoot, query || '');
|
|
1723
|
+
// Stage 6 correctness audit — same redaction gap and same fix as
|
|
1724
|
+
// query_triage_memory just above: this mixes four differently-shaped
|
|
1725
|
+
// result kinds (finding / triage / history / AGENTS.md text), so a
|
|
1726
|
+
// whole-structure redactString round-trip is applied rather than a
|
|
1727
|
+
// per-field allowlist that could miss one of the four shapes.
|
|
1728
|
+
let body;
|
|
1729
|
+
try { body = JSON.parse(redactString(JSON.stringify(raw))); }
|
|
1730
|
+
catch { body = raw; }
|
|
1731
|
+
return { _meta: META, ...body };
|
|
1555
1732
|
},
|
|
1556
1733
|
};
|
|
1557
1734
|
|
package/dist/444.index.js
CHANGED
|
@@ -43,17 +43,26 @@ function scanHistoryDiff(diffText, commit, detectFn) {
|
|
|
43
43
|
if (!added.trim()) return [];
|
|
44
44
|
let findings = [];
|
|
45
45
|
try { findings = detectFn(`git-history@${commit}`, added) || []; } catch { return []; }
|
|
46
|
-
return findings.map((f) =>
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
46
|
+
return findings.map((f) => {
|
|
47
|
+
const remediation = 'Rotate the credential now, then purge it from history (git filter-repo / BFG) and move it to a secrets manager. Removing it from HEAD alone is insufficient.';
|
|
48
|
+
return {
|
|
49
|
+
...f,
|
|
50
|
+
id: `secret-history:${commit}:${f.id || f.vuln || 'secret'}`,
|
|
51
|
+
file: `git-history@${commit}`,
|
|
52
|
+
line: 0,
|
|
53
|
+
commit,
|
|
54
|
+
_historical: true,
|
|
55
|
+
vuln: `${f.vuln || 'Hardcoded Secret'} (in git history)`,
|
|
56
|
+
description: `${f.description || 'A credential was committed.'} Found in commit ${commit}; even if removed from HEAD it remains recoverable from git and must be rotated.`,
|
|
57
|
+
remediation,
|
|
58
|
+
// report/index.js's _remediationOf checks `.fix` before `.remediation`
|
|
59
|
+
// — the underlying detector already set `.fix` to a generic "remove
|
|
60
|
+
// the line" string, which would otherwise silently shadow this
|
|
61
|
+
// history-specific instruction ("removing it from HEAD alone is
|
|
62
|
+
// insufficient") in every report format.
|
|
63
|
+
fix: remediation,
|
|
64
|
+
};
|
|
65
|
+
});
|
|
57
66
|
}
|
|
58
67
|
|
|
59
68
|
/**
|
package/dist/449.index.js
CHANGED
|
@@ -7,10 +7,12 @@ export const modules = {
|
|
|
7
7
|
|
|
8
8
|
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
9
9
|
/* harmony export */ buildBaselineMap: () => (/* binding */ buildBaselineMap),
|
|
10
|
+
/* harmony export */ computeMTTR: () => (/* binding */ computeMTTR),
|
|
11
|
+
/* harmony export */ fingerprintFinding: () => (/* binding */ fingerprintFinding),
|
|
10
12
|
/* harmony export */ renderSlaSummary: () => (/* binding */ renderSlaSummary),
|
|
11
13
|
/* harmony export */ stampFindingTimestamps: () => (/* binding */ stampFindingTimestamps)
|
|
12
14
|
/* harmony export */ });
|
|
13
|
-
/* unused harmony
|
|
15
|
+
/* unused harmony export findingsExceedingSLA */
|
|
14
16
|
/* harmony import */ var node_crypto__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(7598);
|
|
15
17
|
// 0.8.0 Feat-11: MTTR / finding-age tracking — per-finding firstSeenAt/lastSeenAt with SLA breach detection.
|
|
16
18
|
//
|
|
@@ -24,6 +26,11 @@ export const modules = {
|
|
|
24
26
|
|
|
25
27
|
|
|
26
28
|
// Stable fingerprint for cross-scan finding identity. Mirrors the dedupe key.
|
|
29
|
+
// Exported so a caller can compute the "removed since baseline" (i.e. fixed)
|
|
30
|
+
// set that computeMTTR needs, using the exact same identity function
|
|
31
|
+
// buildBaselineMap uses internally — a caller-side reimplementation would
|
|
32
|
+
// risk drifting from this one and silently under/over-counting fixes.
|
|
33
|
+
function fingerprintFinding(f) { return _fingerprint(f); }
|
|
27
34
|
function _fingerprint(f) {
|
|
28
35
|
const file = (f.file || '').split(' -> ').pop();
|
|
29
36
|
const line = f.line || f.source?.line || f.sink?.line || 0;
|
package/dist/526.index.js
CHANGED
|
@@ -78,7 +78,7 @@ function recordFixAttempt(scanRoot, record) {
|
|
|
78
78
|
try {
|
|
79
79
|
const dir = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__/* .stateDir */ .Pn)(scanRoot);
|
|
80
80
|
if (!(0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__.isSafeStateDir)(dir)) return false;
|
|
81
|
-
if (!(0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__.stateWritesEnabled)()) return;
|
|
81
|
+
if (!(0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__.stateWritesEnabled)()) return false;
|
|
82
82
|
node_fs__WEBPACK_IMPORTED_MODULE_0__.mkdirSync(dir, { recursive: true });
|
|
83
83
|
// One writeSync of one newline-terminated line: a concurrent reader sees
|
|
84
84
|
// whole records or nothing, and a torn tail is dropped on read.
|
|
@@ -234,8 +234,8 @@ var external_node_child_process_ = __webpack_require__(1421);
|
|
|
234
234
|
var external_node_fs_ = __webpack_require__(3024);
|
|
235
235
|
// EXTERNAL MODULE: external "node:path"
|
|
236
236
|
var external_node_path_ = __webpack_require__(6760);
|
|
237
|
-
// EXTERNAL MODULE: ./src/engine.js +
|
|
238
|
-
var engine = __webpack_require__(
|
|
237
|
+
// EXTERNAL MODULE: ./src/engine.js + 196 modules
|
|
238
|
+
var engine = __webpack_require__(2739);
|
|
239
239
|
;// CONCATENATED MODULE: ./src/posture/fix-honesty-gate.js
|
|
240
240
|
// Deterministic honesty gates on fix / finding output (#7).
|
|
241
241
|
//
|
package/dist/637.index.js
CHANGED
|
@@ -11,7 +11,7 @@ export const modules = {
|
|
|
11
11
|
/* harmony export */ renderPrDeltaText: () => (/* binding */ renderPrDeltaText)
|
|
12
12
|
/* harmony export */ });
|
|
13
13
|
/* harmony import */ var node_child_process__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1421);
|
|
14
|
-
/* harmony import */ var _engine_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(
|
|
14
|
+
/* harmony import */ var _engine_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(2739);
|
|
15
15
|
// Shadowscan / security-DELTA on PR (v0.72).
|
|
16
16
|
//
|
|
17
17
|
// Most SAST PR-comment integrations show absolute counts — "12 findings
|