@clear-capabilities/agentic-security-scanner 0.136.2 → 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 +236 -0
- package/bin/agentic-security.js +186 -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 +183 -14
- 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 +14 -14
- 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 +18 -10
- package/src/dataflow/CLAUDE.md +10 -4
- package/src/dataflow/builtin-summaries.js +1 -1
- package/src/dataflow/engine.js +324 -60
- package/src/dataflow/implicit-flow.js +68 -36
- package/src/dataflow/incremental.js +18 -3
- 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/engine.js +202 -42
- package/src/ir/CLAUDE.md +4 -1
- package/src/ir/balanced-call.js +55 -0
- 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/lsp/server.js +28 -2
- package/src/mcp/CLAUDE.md +9 -2
- package/src/mcp/redact.js +26 -0
- package/src/mcp/tools.js +155 -14
- 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/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/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/207.index.js
CHANGED
|
@@ -6,9 +6,10 @@ export const modules = {
|
|
|
6
6
|
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
|
7
7
|
|
|
8
8
|
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
9
|
-
/* harmony export */ computeRunAttestation: () => (/* binding */ computeRunAttestation)
|
|
9
|
+
/* harmony export */ computeRunAttestation: () => (/* binding */ computeRunAttestation),
|
|
10
|
+
/* harmony export */ verifyRunAttestation: () => (/* binding */ verifyRunAttestation)
|
|
10
11
|
/* harmony export */ });
|
|
11
|
-
/* unused harmony
|
|
12
|
+
/* unused harmony export ATTESTATION_CANONICALISATION */
|
|
12
13
|
/* harmony import */ var node_crypto__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(7598);
|
|
13
14
|
/* harmony import */ var _integrity_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(1130);
|
|
14
15
|
// R4 — determinism as a contract.
|
|
@@ -202,11 +203,11 @@ function verifyRunAttestation(attestation, inputs = {}) {
|
|
|
202
203
|
}
|
|
203
204
|
if (attestation.signature) {
|
|
204
205
|
let expected;
|
|
205
|
-
try { expected = signLastScan(attestation.digest); }
|
|
206
|
+
try { expected = (0,_integrity_js__WEBPACK_IMPORTED_MODULE_1__/* .signLastScan */ .lU)(attestation.digest); }
|
|
206
207
|
catch { return { ok: false, reason: 'signature present but no key material available to check it' }; }
|
|
207
208
|
const a = Buffer.from(String(attestation.signature), 'hex');
|
|
208
209
|
const b = Buffer.from(expected, 'hex');
|
|
209
|
-
if (a.length !== b.length || !
|
|
210
|
+
if (a.length !== b.length || !node_crypto__WEBPACK_IMPORTED_MODULE_0__.timingSafeEqual(a, b)) {
|
|
210
211
|
return { ok: false, reason: 'signature does not verify under this install key' };
|
|
211
212
|
}
|
|
212
213
|
return { ok: true, reason: 'digest and per-install signature verify' };
|
package/dist/238.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.
|
package/dist/317.index.js
CHANGED
|
@@ -123,12 +123,25 @@ function ensureKeyPair(dir = keyDir()) {
|
|
|
123
123
|
return { privateKeyPem, publicKeyPem, created: true, ...p };
|
|
124
124
|
} catch (e) {
|
|
125
125
|
if (e.code !== 'EEXIST') throw e;
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
126
|
+
// We lost the race on the private key — someone else's 'wx' won.
|
|
127
|
+
// Their public-key write follows immediately after their private-key
|
|
128
|
+
// write but is not itself atomic with it, so it may not have landed
|
|
129
|
+
// yet: a bare read here would throw an uncaught ENOENT on a genuinely
|
|
130
|
+
// transient state, not a real error. Retry briefly rather than crash.
|
|
131
|
+
const sleepBuf = new Int32Array(new SharedArrayBuffer(4));
|
|
132
|
+
for (let attempt = 0; ; attempt++) {
|
|
133
|
+
try {
|
|
134
|
+
return {
|
|
135
|
+
privateKeyPem: node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(p.privateKey, 'utf8'),
|
|
136
|
+
publicKeyPem: node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(p.publicKey, 'utf8'),
|
|
137
|
+
created: false,
|
|
138
|
+
...p,
|
|
139
|
+
};
|
|
140
|
+
} catch (readErr) {
|
|
141
|
+
if (readErr.code !== 'ENOENT' || attempt >= 50) throw readErr;
|
|
142
|
+
Atomics.wait(sleepBuf, 0, 0, 10); // 10ms; ~500ms total budget
|
|
143
|
+
}
|
|
144
|
+
}
|
|
132
145
|
}
|
|
133
146
|
}
|
|
134
147
|
|
|
@@ -246,9 +259,26 @@ function signEvidenceBundle(bundle, privateKeyPem) {
|
|
|
246
259
|
* Returns {ok, reason}. Never throws — a malformed bundle from an untrusted
|
|
247
260
|
* source is an expected input, not an exceptional one.
|
|
248
261
|
*/
|
|
262
|
+
// The complete set of top-level keys a legitimately-built, signed bundle can
|
|
263
|
+
// carry — buildEvidenceBundle's six plus signEvidenceBundle's `signature`.
|
|
264
|
+
// EA-03 (Stage-0 audit, 2026): canonicalBytes SIGNS an allowlist of fields;
|
|
265
|
+
// verifyEvidenceBundle never checked for keys OUTSIDE that allowlist, so a
|
|
266
|
+
// bundle with a fabricated `verdict`/`proofLevel`/anything-else stapled on
|
|
267
|
+
// after signing verified as authentic — the signature simply never covered
|
|
268
|
+
// those bytes. Rejecting unknown keys here closes that; it must exactly match
|
|
269
|
+
// what buildEvidenceBundle+signEvidenceBundle actually produce, or a
|
|
270
|
+
// legitimate bundle would start failing verification.
|
|
271
|
+
const BUNDLE_TOP_LEVEL_KEYS = new Set([
|
|
272
|
+
'schema', 'finding', 'evidence', 'engine', 'proves', 'doesNotProve', 'signature',
|
|
273
|
+
]);
|
|
274
|
+
|
|
249
275
|
function verifyEvidenceBundle(bundle, publicKeyPem) {
|
|
250
276
|
if (!bundle || typeof bundle !== 'object') return { ok: false, reason: 'bundle is not an object' };
|
|
251
277
|
if (bundle.schema !== BUNDLE_SCHEMA) return { ok: false, reason: `unrecognised schema: ${bundle.schema}` };
|
|
278
|
+
const unknownKeys = Object.keys(bundle).filter(k => !BUNDLE_TOP_LEVEL_KEYS.has(k));
|
|
279
|
+
if (unknownKeys.length) {
|
|
280
|
+
return { ok: false, reason: `unrecognised top-level key(s) not covered by the signature: ${unknownKeys.join(', ')}` };
|
|
281
|
+
}
|
|
252
282
|
const sig = bundle.signature;
|
|
253
283
|
if (!sig?.value) return { ok: false, reason: 'bundle is unsigned' };
|
|
254
284
|
if (sig.algorithm !== 'ed25519') return { ok: false, reason: `unsupported algorithm: ${sig.algorithm}` };
|
package/dist/384.index.js
CHANGED
|
@@ -8,7 +8,7 @@ export const modules = {
|
|
|
8
8
|
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
9
9
|
/* harmony export */ scanCredentials: () => (/* reexport safe */ _engine_js__WEBPACK_IMPORTED_MODULE_0__.Sv)
|
|
10
10
|
/* harmony export */ });
|
|
11
|
-
/* harmony import */ var _engine_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(
|
|
11
|
+
/* harmony import */ var _engine_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2739);
|
|
12
12
|
// Secrets submodule view of the engine — credential + entropy + TODO scanning.
|
|
13
13
|
|
|
14
14
|
|
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) {
|
|
@@ -785,12 +819,22 @@ const scan_diff = {
|
|
|
785
819
|
const wantSet = new Set(Object.keys(fileContents));
|
|
786
820
|
const sevRank = { info: 0, low: 1, medium: 2, high: 3, critical: 4 };
|
|
787
821
|
const min = sevRank[severity] ?? 0;
|
|
788
|
-
|
|
822
|
+
// Stage 6 correctness audit: this only ever read result.scan.findings
|
|
823
|
+
// (the SAST channel) — scan.secrets and scan.logicVulns are separate
|
|
824
|
+
// arrays on the raw runScan() result (report/index.js's normalizeFindings
|
|
825
|
+
// is what merges all four channels, and that merge hasn't run yet here).
|
|
826
|
+
// A file containing a bare hardcoded credential reported findingCount: 0
|
|
827
|
+
// through a tool whose own description promises "Use BEFORE writing a
|
|
828
|
+
// Write/Edit to disk so the agent can self-correct". Also reused
|
|
829
|
+
// _remediationOf so a fix-string detector (the majority of engine.js's
|
|
830
|
+
// own, ~127 call sites) doesn't silently report an empty `description`
|
|
831
|
+
// the way reading only `.remediation` did.
|
|
832
|
+
const findings = [...(result.scan.findings || []), ...(result.scan.secrets || []), ...(result.scan.logicVulns || [])]
|
|
789
833
|
.filter(f => wantSet.has(String(f.file || '').replace(/\\/g, '/')) && (sevRank[f.severity] ?? 0) >= min)
|
|
790
834
|
.map(f => redactFinding({
|
|
791
835
|
id: f.id, severity: f.severity, file: f.file, line: f.line,
|
|
792
836
|
title: f.title || f.vuln, cwe: f.cwe,
|
|
793
|
-
description: f.description, remediation: f
|
|
837
|
+
description: f.description, remediation: (0,report/* _remediationOf */.uV)(f),
|
|
794
838
|
}));
|
|
795
839
|
// Harness-anatomy #1: offload when the result exceeds OFFLOAD_THRESHOLD.
|
|
796
840
|
// The agent gets a head+tail preview plus a path it can page through;
|
|
@@ -953,10 +997,40 @@ const apply_fix = {
|
|
|
953
997
|
additionalProperties: { type: 'string', maxLength: 500_000 },
|
|
954
998
|
minProperties: 1, maxProperties: 8,
|
|
955
999
|
},
|
|
1000
|
+
// Stage 6 correctness audit: same gap and same fix as verify_fix — the
|
|
1001
|
+
// honesty gate is reachable but was never wired to any real caller.
|
|
1002
|
+
// Here it's stronger than advisory: the inline re-verify below already
|
|
1003
|
+
// gates the WRITE on `verdict.ok`, and verifyFixCore's own `ok`
|
|
1004
|
+
// formula already folds in `honesty.ok` when fixMeta is supplied — so
|
|
1005
|
+
// passing it through here makes a dishonest fixMeta (hand-wave
|
|
1006
|
+
// residual, uncited false-positive verdict) block the write itself,
|
|
1007
|
+
// not just report a verdict.
|
|
1008
|
+
fixMeta: {
|
|
1009
|
+
type: 'object',
|
|
1010
|
+
additionalProperties: false,
|
|
1011
|
+
properties: {
|
|
1012
|
+
residual: { type: 'string', maxLength: 2000 },
|
|
1013
|
+
verdict: { type: 'string', maxLength: 64 },
|
|
1014
|
+
evidence: { type: 'array', items: { type: 'string', maxLength: 500 }, maxItems: 20 },
|
|
1015
|
+
signals: {
|
|
1016
|
+
type: 'object',
|
|
1017
|
+
additionalProperties: false,
|
|
1018
|
+
properties: {
|
|
1019
|
+
sinkSignatureChanged: { type: 'boolean' },
|
|
1020
|
+
allCallersRouted: { type: 'boolean' },
|
|
1021
|
+
testDiscriminates: { type: 'boolean' },
|
|
1022
|
+
rateLimitOnly: { type: 'boolean' },
|
|
1023
|
+
docsOnly: { type: 'boolean' },
|
|
1024
|
+
logOnlyNoReject: { type: 'boolean' },
|
|
1025
|
+
partialSanitization: { type: 'boolean' },
|
|
1026
|
+
},
|
|
1027
|
+
},
|
|
1028
|
+
},
|
|
1029
|
+
},
|
|
956
1030
|
},
|
|
957
1031
|
required: ['finding_id', 'confirm'],
|
|
958
1032
|
},
|
|
959
|
-
async handler({ finding_id, confirm, dry_run = false, patch = null }, ctx) {
|
|
1033
|
+
async handler({ finding_id, confirm, dry_run = false, patch = null, fixMeta = null }, ctx) {
|
|
960
1034
|
if (confirm !== true) {
|
|
961
1035
|
return { _meta: META, applied: false, reason: 'apply_fix requires confirm: true.' };
|
|
962
1036
|
}
|
|
@@ -1012,6 +1086,7 @@ const apply_fix = {
|
|
|
1012
1086
|
scanRoot: ctx.sessionRoot,
|
|
1013
1087
|
originalFindingStableId: f.stableId,
|
|
1014
1088
|
files: _files,
|
|
1089
|
+
fixMeta,
|
|
1015
1090
|
});
|
|
1016
1091
|
}
|
|
1017
1092
|
} catch (e) {
|
|
@@ -1021,7 +1096,7 @@ const apply_fix = {
|
|
|
1021
1096
|
return {
|
|
1022
1097
|
_meta: META, applied: false,
|
|
1023
1098
|
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 } },
|
|
1099
|
+
verify: { rescan: verdict.rescan, lint: { runner: verdict.lint?.runner, ok: verdict.lint?.ok }, honesty: verdict.honesty || null },
|
|
1025
1100
|
};
|
|
1026
1101
|
}
|
|
1027
1102
|
if (dry_run) {
|
|
@@ -1033,7 +1108,7 @@ const apply_fix = {
|
|
|
1033
1108
|
const originalContent = external_node_fs_.existsSync(v.abs) ? await promises_.readFile(v.abs, 'utf8') : '';
|
|
1034
1109
|
const entry = await (0,fix_history/* applyFix */.oM)({
|
|
1035
1110
|
scanRoot: ctx.sessionRoot, file: rel, originalContent, newContent: v.content,
|
|
1036
|
-
findingId: f.id, stableId: f.stableId, ruleId: f.
|
|
1111
|
+
findingId: f.id, stableId: f.stableId, ruleId: f.ruleId || f.cwe || f.family || null, vuln: f.vuln || f.title || null,
|
|
1037
1112
|
});
|
|
1038
1113
|
written.push({ file: rel, historyId: entry.id, backupPath: entry.backupPath });
|
|
1039
1114
|
}
|
|
@@ -1091,7 +1166,7 @@ const apply_fix = {
|
|
|
1091
1166
|
newContent: f.fix.replacement,
|
|
1092
1167
|
findingId: f.id,
|
|
1093
1168
|
stableId: f.stableId || null, // premortem 4R-8
|
|
1094
|
-
ruleId: f.
|
|
1169
|
+
ruleId: f.ruleId || f.cwe || f.family || null,
|
|
1095
1170
|
vuln: f.vuln || f.title || null,
|
|
1096
1171
|
});
|
|
1097
1172
|
} catch (e) {
|
|
@@ -1128,7 +1203,7 @@ const apply_fix = {
|
|
|
1128
1203
|
// proceed with apply_fix.
|
|
1129
1204
|
const verify_fix = {
|
|
1130
1205
|
name: 'verify_fix',
|
|
1131
|
-
description: 'Verify a proposed patch before applying. Re-scans the patched files in memory
|
|
1206
|
+
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
1207
|
inputSchema: {
|
|
1133
1208
|
type: 'object',
|
|
1134
1209
|
additionalProperties: false,
|
|
@@ -1140,10 +1215,41 @@ const verify_fix = {
|
|
|
1140
1215
|
minProperties: 1,
|
|
1141
1216
|
maxProperties: 8,
|
|
1142
1217
|
},
|
|
1218
|
+
// Stage 6 correctness audit: posture/fix-honesty-gate.js's deterministic
|
|
1219
|
+
// honesty checks (vague-assurance residual prose, unbacked false-
|
|
1220
|
+
// positive verdicts, tier/residual consistency) were fully built and
|
|
1221
|
+
// fix-verify.js already consulted them when given a `fixMeta` — but
|
|
1222
|
+
// this schema never had a `fixMeta` property, so no call through the
|
|
1223
|
+
// MCP surface could ever supply one. The gate can only run against
|
|
1224
|
+
// claims the AGENT self-reports (residual risk, verdict, evidence,
|
|
1225
|
+
// completeness signals) — nothing here is server-computable — so
|
|
1226
|
+
// fixing this meant exposing the property, not inventing a lookup.
|
|
1227
|
+
fixMeta: {
|
|
1228
|
+
type: 'object',
|
|
1229
|
+
additionalProperties: false,
|
|
1230
|
+
properties: {
|
|
1231
|
+
residual: { type: 'string', maxLength: 2000 },
|
|
1232
|
+
verdict: { type: 'string', maxLength: 64 },
|
|
1233
|
+
evidence: { type: 'array', items: { type: 'string', maxLength: 500 }, maxItems: 20 },
|
|
1234
|
+
signals: {
|
|
1235
|
+
type: 'object',
|
|
1236
|
+
additionalProperties: false,
|
|
1237
|
+
properties: {
|
|
1238
|
+
sinkSignatureChanged: { type: 'boolean' },
|
|
1239
|
+
allCallersRouted: { type: 'boolean' },
|
|
1240
|
+
testDiscriminates: { type: 'boolean' },
|
|
1241
|
+
rateLimitOnly: { type: 'boolean' },
|
|
1242
|
+
docsOnly: { type: 'boolean' },
|
|
1243
|
+
logOnlyNoReject: { type: 'boolean' },
|
|
1244
|
+
partialSanitization: { type: 'boolean' },
|
|
1245
|
+
},
|
|
1246
|
+
},
|
|
1247
|
+
},
|
|
1248
|
+
},
|
|
1143
1249
|
},
|
|
1144
1250
|
required: ['stable_id', 'files'],
|
|
1145
1251
|
},
|
|
1146
|
-
async handler({ stable_id, files }, ctx) {
|
|
1252
|
+
async handler({ stable_id, files, fixMeta }, ctx) {
|
|
1147
1253
|
// Confine every file path before passing to the verifier.
|
|
1148
1254
|
const confined = {};
|
|
1149
1255
|
for (const [relPath, content] of Object.entries(files || {})) {
|
|
@@ -1155,17 +1261,48 @@ const verify_fix = {
|
|
|
1155
1261
|
confined[relPath] = String(content);
|
|
1156
1262
|
}
|
|
1157
1263
|
try {
|
|
1264
|
+
// The PoC-re-check leg (verifyFixCore's `pocLeg`) needs a `poc` param
|
|
1265
|
+
// to do anything — until now nothing supplied one, so it always
|
|
1266
|
+
// reported {status:'not-requested'} through this surface (see
|
|
1267
|
+
// posture/CLAUDE.md's disclosure). Rather than widening inputSchema
|
|
1268
|
+
// to make the CALLER pass PoC data back, look it up server-side: the
|
|
1269
|
+
// scan pipeline already attaches an HTTP-shaped f.poc to matching
|
|
1270
|
+
// findings by default (engine.js's annotatePocs), and last-scan.json
|
|
1271
|
+
// already carries it under the same stableId this handler receives.
|
|
1272
|
+
// Best-effort: a missing/unsigned/tampered scan just means no PoC is
|
|
1273
|
+
// available to re-check, not a verify_fix failure — the rescan/lint/
|
|
1274
|
+
// tests legs below are independent of this and still apply.
|
|
1275
|
+
let poc = null;
|
|
1276
|
+
try {
|
|
1277
|
+
const { scan: lastScan } = _readLastScanVerified(ctx.sessionRoot, { allowUnsigned: true });
|
|
1278
|
+
const orig = lastScan && (lastScan.findings || []).find(f => f.stableId === stable_id);
|
|
1279
|
+
if (orig && orig.poc && orig.poc.code) poc = { ...orig.poc, finding: orig };
|
|
1280
|
+
} catch { /* best-effort lookup; poc stays null */ }
|
|
1281
|
+
|
|
1158
1282
|
const verifyFixCore = await getVerifyFixCore();
|
|
1159
1283
|
const r = await verifyFixCore({
|
|
1160
1284
|
scanRoot: ctx.sessionRoot,
|
|
1161
1285
|
originalFindingStableId: stable_id,
|
|
1162
1286
|
files: confined,
|
|
1287
|
+
poc,
|
|
1288
|
+
fixMeta,
|
|
1163
1289
|
});
|
|
1164
1290
|
return {
|
|
1165
1291
|
_meta: META,
|
|
1166
1292
|
ok: r.ok,
|
|
1167
1293
|
rescan: { ok: r.rescan.ok, reason: r.rescan.reason, introduced: r.rescan.introduced || [] },
|
|
1168
1294
|
lint: { runner: r.lint.runner, ok: r.lint.ok, skipped: r.lint.skipped || false, output: redactString(r.lint.output || '').slice(0, 1500) },
|
|
1295
|
+
// verifyFix computes five legs, not two — tests/honesty/poc were
|
|
1296
|
+
// being silently dropped here, leaving an agent with no structured
|
|
1297
|
+
// way to see WHY verification failed when the failure was in one
|
|
1298
|
+
// of those three (only the free-text summary carried it).
|
|
1299
|
+
// test-runner.js's runProjectTests never returns raw stdout/stderr,
|
|
1300
|
+
// so no redaction is needed there; honesty.violations are static,
|
|
1301
|
+
// code-generated strings; poc.reason is redacted defensively since
|
|
1302
|
+
// it can echo proof-harness detail derived from scanned source.
|
|
1303
|
+
tests: r.tests,
|
|
1304
|
+
honesty: r.honesty,
|
|
1305
|
+
poc: r.poc ? { ...r.poc, reason: r.poc.reason ? redactString(r.poc.reason) : r.poc.reason } : r.poc,
|
|
1169
1306
|
summary: r.summary,
|
|
1170
1307
|
};
|
|
1171
1308
|
} catch (e) {
|
|
@@ -1249,7 +1386,13 @@ const synthesize_fix = {
|
|
|
1249
1386
|
regression_test: f.regression_test || null,
|
|
1250
1387
|
remediation: typeof fix.description === 'string' ? fix.description : (typeof fix === 'string' ? fix : null),
|
|
1251
1388
|
patchBounds: { touchedFiles, locDelta, oversized },
|
|
1252
|
-
|
|
1389
|
+
// oversized can only be true when hasReplacement is true (locDelta is
|
|
1390
|
+
// only computed in that branch, and touchedFiles never varies) — a
|
|
1391
|
+
// `!hasReplacement` conjunct here was a structural contradiction that
|
|
1392
|
+
// made this permanently false. The correct signal: the stored
|
|
1393
|
+
// replacement itself is too big to trust auto-applying, and there's
|
|
1394
|
+
// no safer deterministic alternative.
|
|
1395
|
+
recommendsFixPlan: oversized && !autofix,
|
|
1253
1396
|
};
|
|
1254
1397
|
},
|
|
1255
1398
|
};
|
|
@@ -1381,7 +1524,9 @@ const append_scratchpad = {
|
|
|
1381
1524
|
async handler({ path: relPath, content }, ctx) {
|
|
1382
1525
|
const v = _validateScratchpadPath(relPath);
|
|
1383
1526
|
if (!v.ok) return { _meta: META, ok: false, reason: v.reason };
|
|
1384
|
-
|
|
1527
|
+
let abs;
|
|
1528
|
+
try { abs = _scratchpadAbs(ctx.sessionRoot, relPath); }
|
|
1529
|
+
catch (e) { return { _meta: META, ok: false, reason: `path-escape refused: ${e.message}` }; }
|
|
1385
1530
|
const total = _scratchpadTotalBytes(ctx.sessionRoot);
|
|
1386
1531
|
if (total + content.length > SCRATCHPAD_MAX_TOTAL_BYTES) {
|
|
1387
1532
|
return {
|
|
@@ -1427,7 +1572,9 @@ const read_scratchpad = {
|
|
|
1427
1572
|
async handler({ path: relPath, offset, limit }, ctx) {
|
|
1428
1573
|
const v = _validateScratchpadPath(relPath);
|
|
1429
1574
|
if (!v.ok) return { _meta: META, ok: false, reason: v.reason };
|
|
1430
|
-
|
|
1575
|
+
let abs;
|
|
1576
|
+
try { abs = _scratchpadAbs(ctx.sessionRoot, relPath); }
|
|
1577
|
+
catch (e) { return { _meta: META, ok: false, reason: `path-escape refused: ${e.message}` }; }
|
|
1431
1578
|
if (!external_node_fs_.existsSync(abs)) return { _meta: META, ok: false, reason: 'not-found' };
|
|
1432
1579
|
let stat;
|
|
1433
1580
|
try { stat = external_node_fs_.statSync(abs); } catch (e) { return { _meta: META, ok: false, reason: `stat-failed: ${e.message}` }; }
|
|
@@ -1523,7 +1670,20 @@ const query_triage_memory = {
|
|
|
1523
1670
|
},
|
|
1524
1671
|
async handler({ query }, ctx) {
|
|
1525
1672
|
const { queryMemory } = await Promise.resolve(/* import() */).then(__webpack_require__.bind(__webpack_require__, 1905));
|
|
1526
|
-
const
|
|
1673
|
+
const raw = queryMemory(ctx.sessionRoot, query || '');
|
|
1674
|
+
// Stage 6 correctness audit: this returned queryMemory's output
|
|
1675
|
+
// verbatim, with no redaction pass — every other tool that echoes
|
|
1676
|
+
// scanned-source-derived text redacts it (mcp/CLAUDE.md's "Adding a new
|
|
1677
|
+
// tool" step 3). Round-trip through redactString the same way
|
|
1678
|
+
// redactFinding already does for its own opaque `.trace` field: results
|
|
1679
|
+
// here mix shapes (a triage decision's free-text `reason`, a finding's
|
|
1680
|
+
// `vuln`/`family`/file path), so scrubbing the whole serialized
|
|
1681
|
+
// structure catches secret-shaped substrings regardless of which field
|
|
1682
|
+
// they landed in, rather than hardcoding a field allowlist that could
|
|
1683
|
+
// miss one.
|
|
1684
|
+
let results;
|
|
1685
|
+
try { results = JSON.parse(redactString(JSON.stringify(raw))); }
|
|
1686
|
+
catch { results = raw; }
|
|
1527
1687
|
return {
|
|
1528
1688
|
_meta: META,
|
|
1529
1689
|
count: results.length,
|
|
@@ -1551,7 +1711,16 @@ const query_findings_memory = {
|
|
|
1551
1711
|
},
|
|
1552
1712
|
async handler({ query }, ctx) {
|
|
1553
1713
|
const { queryFindingsMemory } = await __webpack_require__.e(/* import() */ 839).then(__webpack_require__.bind(__webpack_require__, 3839));
|
|
1554
|
-
|
|
1714
|
+
const raw = queryFindingsMemory(ctx.sessionRoot, query || '');
|
|
1715
|
+
// Stage 6 correctness audit — same redaction gap and same fix as
|
|
1716
|
+
// query_triage_memory just above: this mixes four differently-shaped
|
|
1717
|
+
// result kinds (finding / triage / history / AGENTS.md text), so a
|
|
1718
|
+
// whole-structure redactString round-trip is applied rather than a
|
|
1719
|
+
// per-field allowlist that could miss one of the four shapes.
|
|
1720
|
+
let body;
|
|
1721
|
+
try { body = JSON.parse(redactString(JSON.stringify(raw))); }
|
|
1722
|
+
catch { body = raw; }
|
|
1723
|
+
return { _meta: META, ...body };
|
|
1555
1724
|
},
|
|
1556
1725
|
};
|
|
1557
1726
|
|
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
|