@clear-capabilities/agentic-security-scanner 0.124.1 → 0.128.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +206 -0
- package/bin/agentic-security.js +75 -2
- package/dist/11.index.js +353 -0
- package/dist/113.index.js +525 -0
- package/dist/178.index.js +1 -1
- package/dist/220.index.js +193 -0
- package/dist/384.index.js +1 -1
- package/dist/435.index.js +2406 -0
- package/dist/449.index.js +135 -0
- package/dist/637.index.js +1 -1
- package/dist/752.index.js +7 -4
- package/dist/801.index.js +87 -0
- package/dist/826.index.js +4 -1
- package/dist/838.index.js +1 -1
- package/dist/agentic-security.mjs +1 -2
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +6 -6
- package/src/engine.js +31 -1
- package/src/integrations/tickets.js +9 -3
- package/src/ir/CLAUDE.md +22 -17
- package/src/llm-validator/index.js +47 -12
- package/src/mcp/tools.js +108 -3
- package/src/posture/CLAUDE.md +10 -1
- package/src/posture/cache-economics.js +7 -4
- package/src/posture/deterministic-fix.js +65 -0
- package/src/posture/entrypoint-inventory.js +248 -0
- package/src/posture/falsification.js +121 -0
- package/src/posture/fix-honesty-gate.js +175 -0
- package/src/posture/fix-verify.js +18 -3
- package/src/posture/model-routing.js +126 -0
- package/src/posture/mttr.js +25 -0
- package/src/posture/provider-catalog.js +108 -0
- package/src/posture/root-cause-sweep.js +262 -0
- package/src/posture/secret-live-check.js +71 -0
- package/src/pr-comment.js +3 -1
- package/src/sast/CLAUDE.md +1 -1
- package/src/sast/api-authz.js +36 -0
- package/src/sast/file-upload.js +118 -0
- package/src/sast/llm-cost-advisor.js +88 -0
- package/src/util/untrusted.js +148 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
2d630902bbb9bebbd5557bce1c7db106cd81fdf975cf18c5273b1e46716103a7 agentic-security.mjs
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clear-capabilities/agentic-security-scanner",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.128.1",
|
|
4
4
|
"description": "Scanner engine for the agentic-security Claude Code plugin — SAST, SCA (function-level reachability + CISA KEV), secrets, IaC, prompt-injection, MCP/agent-tool audit, auth/authZ deep analysis, attack chains, PoC generation, business logic, toxic-combinations scoring, SBOM, SARIF ingest, pipeline integrity, compliance attestation, and more.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -60,14 +60,14 @@
|
|
|
60
60
|
"prepublishOnly": "npm run build && node ../scripts/sync-scanner-changelog.mjs",
|
|
61
61
|
"test": "npm run test:smoke && npm run test:sast && npm run test:posture && npm run test:dataflow && npm run test:mcp && npm run test:report && npm run test:bench-modules && npm run test:lifecycle && npm run test:eval && AGENTIC_SECURITY_CPP_DATAFLOW=1 node --test test/cpp-dataflow.test.js",
|
|
62
62
|
"test:smoke": "node --test test/smoke.test.js",
|
|
63
|
-
"test:sast": "node --test test/llm.test.js test/llm-owasp.test.js test/logic.test.js test/authz.test.js test/model-load.test.js test/prompt-template.test.js test/business-logic.test.js test/python-sinks.test.js test/phase1-detectors.test.js test/phase2-detectors.test.js test/phase3-v3.test.js test/phase7-extensions.test.js test/phase8-extensions.test.js test/new-cwe-detectors.test.js test/llmsecops-detectors.test.js test/db-taint.test.js test/dart-swift.test.js test/redos-nfa.test.js test/weak-randomness.test.js test/csharp-pipeline.test.js test/post-quantum-crypto.test.js test/web3-advanced.test.js test/cloud-iam-k8s.test.js test/crypto-protocol.test.js test/ml-supply-chain.test.js test/wrong-context-sanitizer.test.js test/sanitizer-context.test.js test/frontend-hygiene.test.js test/csv-injection.test.js test/stored-taint.test.js test/tree-sitter-sinks.test.js test/kotlin-structural.test.js test/ruby-php-structural.test.js test/java-csharp-structural.test.js test/guard-recognition.test.js test/js-python-framework-structural.test.js test/go-structural.test.js test/secret-concat.test.js test/xss-reflected-multilang.test.js test/code-injection-multilang.test.js test/xxe-multilang.test.js test/xpath-injection-multilang.test.js test/gapfill-batch13.test.js test/agent-untrusted-flow.test.js test/api-authz.test.js test/event-entrypoint.test.js test/iac-terraform.test.js test/cross-service.test.js test/rbac-consistency.test.js",
|
|
64
|
-
"test:posture": "node --test test/material-change.test.js test/drift.test.js test/scorecard.test.js test/mttr.test.js test/license-policy.test.js test/aibom.test.js test/sbom.test.js test/api-inventory.test.js test/iam-policy.test.js test/container.test.js test/container-runtime.test.js test/image-packages.test.js test/kev.test.js test/dep-confusion.test.js test/sca-deprecated.test.js test/sca-batch.test.js test/composite-risk.test.js test/sca-coverage.test.js test/gradle-deps.test.js test/sca-route-reachable.test.js test/sca-policy.test.js test/sca-verdict.test.js test/install-script.test.js test/sca-linked-findings.test.js test/packs.test.js test/flow-narration.test.js test/regression-test-gen.test.js test/rule-synthesis.test.js test/policy-gate.test.js test/agents-memory.test.js test/cve-lookup.test.js test/cve-alert-daemon.test.js test/fix-verify-loop.test.js test/fix-acceptance.test.js test/exploitability-probability.test.js test/history-scan.test.js test/viral-features.test.js test/viral-v074.test.js test/state-dir.test.js test/license-graph.test.js test/attack-taxonomy.test.js test/triage-memory.test.js test/pr-augment.test.js test/chat-batch2.test.js test/chat-batch3.test.js test/chat-batch4.test.js test/chat-batch5.test.js test/chat-batch6.test.js test/router.test.js test/legacy-alias-redirect.test.js test/cache-economics.test.js test/coverage-report.test.js test/corpus-status.test.js test/provenance.test.js test/secret-history.test.js",
|
|
65
|
-
"test:dataflow": "node --test test/fn-reach.test.js test/deep-taint.test.js test/calibration.test.js test/holdout-eval.test.js test/cross-lang-meta.test.js test/cross-lang-queues.test.js test/phase5-xlang.test.js test/phase5-coverage.test.js test/phase6-taint.test.js test/llm-validator-consistency.test.js test/llm-validator-default-on.test.js test/parser-py-cst.test.js test/parser-cs-kt.test.js test/parser-go.test.js test/parser-php-rb.test.js test/interproc-k2.test.js test/proven-clean.test.js test/backward-default.test.js test/incremental-cache.test.js test/string-regex-lattice.test.js test/closure-capture.test.js test/points-to.test.js test/type-stubs.test.js test/soft-taint.test.js test/ifds.test.js test/symbolic-exec-proof.test.js test/ifds-summary-edges.test.js test/stub-aware-filter.test.js test/cross-repo.test.js test/proof-gate.test.js test/proof-safe.test.js test/collection-taint.test.js test/kcfa-context.test.js test/kcfa-callstring.test.js test/flow-parity.test.js test/callgraph-resolve.test.js test/import-reachability.test.js",
|
|
63
|
+
"test:sast": "node --test test/llm.test.js test/llm-cost-advisor.test.js test/llm-owasp.test.js test/logic.test.js test/authz.test.js test/model-load.test.js test/prompt-template.test.js test/business-logic.test.js test/python-sinks.test.js test/phase1-detectors.test.js test/phase2-detectors.test.js test/phase3-v3.test.js test/phase7-extensions.test.js test/phase8-extensions.test.js test/new-cwe-detectors.test.js test/file-upload.test.js test/llmsecops-detectors.test.js test/db-taint.test.js test/dart-swift.test.js test/redos-nfa.test.js test/weak-randomness.test.js test/csharp-pipeline.test.js test/post-quantum-crypto.test.js test/web3-advanced.test.js test/cloud-iam-k8s.test.js test/crypto-protocol.test.js test/ml-supply-chain.test.js test/wrong-context-sanitizer.test.js test/sanitizer-context.test.js test/frontend-hygiene.test.js test/csv-injection.test.js test/stored-taint.test.js test/tree-sitter-sinks.test.js test/kotlin-structural.test.js test/ruby-php-structural.test.js test/java-csharp-structural.test.js test/guard-recognition.test.js test/js-python-framework-structural.test.js test/go-structural.test.js test/secret-concat.test.js test/xss-reflected-multilang.test.js test/code-injection-multilang.test.js test/xxe-multilang.test.js test/xpath-injection-multilang.test.js test/gapfill-batch13.test.js test/agent-untrusted-flow.test.js test/api-authz.test.js test/event-entrypoint.test.js test/iac-terraform.test.js test/cross-service.test.js test/rbac-consistency.test.js",
|
|
64
|
+
"test:posture": "node --test test/material-change.test.js test/drift.test.js test/scorecard.test.js test/mttr.test.js test/license-policy.test.js test/aibom.test.js test/sbom.test.js test/api-inventory.test.js test/iam-policy.test.js test/container.test.js test/container-runtime.test.js test/image-packages.test.js test/kev.test.js test/dep-confusion.test.js test/sca-deprecated.test.js test/sca-batch.test.js test/composite-risk.test.js test/sca-coverage.test.js test/gradle-deps.test.js test/sca-route-reachable.test.js test/sca-policy.test.js test/sca-verdict.test.js test/install-script.test.js test/sca-linked-findings.test.js test/packs.test.js test/flow-narration.test.js test/regression-test-gen.test.js test/deterministic-fix.test.js test/falsification.test.js test/fix-honesty-gate.test.js test/model-routing.test.js test/root-cause-sweep.test.js test/entrypoint-inventory.test.js test/untrusted.test.js test/agent-hardening.test.js test/rule-synthesis.test.js test/policy-gate.test.js test/agents-memory.test.js test/cve-lookup.test.js test/cve-alert-daemon.test.js test/fix-verify-loop.test.js test/fix-acceptance.test.js test/exploitability-probability.test.js test/history-scan.test.js test/viral-features.test.js test/viral-v074.test.js test/state-dir.test.js test/license-graph.test.js test/secret-live-check.test.js test/attack-taxonomy.test.js test/triage-memory.test.js test/pr-augment.test.js test/chat-batch2.test.js test/chat-batch3.test.js test/chat-batch4.test.js test/chat-batch5.test.js test/chat-batch6.test.js test/router.test.js test/legacy-alias-redirect.test.js test/cache-economics.test.js test/coverage-report.test.js test/corpus-status.test.js test/provenance.test.js test/secret-history.test.js",
|
|
65
|
+
"test:dataflow": "node --test test/fn-reach.test.js test/deep-taint.test.js test/calibration.test.js test/holdout-eval.test.js test/cross-lang-meta.test.js test/cross-lang-queues.test.js test/phase5-xlang.test.js test/phase5-coverage.test.js test/phase6-taint.test.js test/llm-validator-consistency.test.js test/llm-validator-default-on.test.js test/llm-validator-preset.test.js test/parser-py-cst.test.js test/parser-cs-kt.test.js test/parser-go.test.js test/parser-php-rb.test.js test/interproc-k2.test.js test/proven-clean.test.js test/backward-default.test.js test/incremental-cache.test.js test/string-regex-lattice.test.js test/closure-capture.test.js test/points-to.test.js test/type-stubs.test.js test/soft-taint.test.js test/ifds.test.js test/symbolic-exec-proof.test.js test/ifds-summary-edges.test.js test/stub-aware-filter.test.js test/cross-repo.test.js test/proof-gate.test.js test/proof-safe.test.js test/collection-taint.test.js test/kcfa-context.test.js test/kcfa-callstring.test.js test/flow-parity.test.js test/callgraph-resolve.test.js test/import-reachability.test.js",
|
|
66
66
|
"test:mcp": "node --test test/mcp.test.js test/mcp-audit.test.js test/audit-cli.test.js test/mcp-scratchpad.test.js test/mcp-offload.test.js test/sca-upgrade.test.js",
|
|
67
67
|
"test:report": "node --test test/sarif-ingest.test.js test/junit.test.js test/ci.test.js test/poc-generator.test.js test/verifier.test.js test/verifier-target.test.js test/annotator-errors.test.js test/grader-calibration.test.js test/pr-delta-gate.test.js test/vex.test.js test/report-render.test.js",
|
|
68
68
|
"test:bench-modules": "node --test test/phase4-harness.test.js test/pipeline.test.js",
|
|
69
|
-
"test:lifecycle": "node --test test/dead-code.test.js test/no-dead-modules.test.js test/stop-hook.test.js test/plugin-self-check.test.js test/skills-registry.test.js test/bodyguard.test.js test/cache-invalidator-guard.test.js && node ../scripts/lint-command-descriptions.mjs",
|
|
70
|
-
"test:eval": "node --test test/independent-eval.test.js",
|
|
69
|
+
"test:lifecycle": "node --test test/dead-code.test.js test/no-dead-modules.test.js test/stop-hook.test.js test/plugin-self-check.test.js test/skills-registry.test.js test/bodyguard.test.js test/cache-invalidator-guard.test.js test/dispatch-pre-tool.test.js test/dispatch-user-prompt.test.js test/session-start-model-capture.test.js && node ../scripts/lint-command-descriptions.mjs",
|
|
70
|
+
"test:eval": "node --test test/independent-eval.test.js test/realworld-recall.test.js",
|
|
71
71
|
"eval:independent": "node ../bench/independent-eval/runner.mjs",
|
|
72
72
|
"eval:independent:gate": "node ../bench/independent-eval/runner.mjs --gate default",
|
|
73
73
|
"smoke": "node bin/agentic-security.js scan test/fixtures/vulnerable-js",
|
package/src/engine.js
CHANGED
|
@@ -11,6 +11,7 @@ import { createRequire } from 'node:module';
|
|
|
11
11
|
const _require = createRequire(import.meta.url);
|
|
12
12
|
import { scanLLM } from './sast/llm.js';
|
|
13
13
|
import { scanLLMOwasp } from './sast/llm-owasp.js';
|
|
14
|
+
import { scanLlmCost } from './sast/llm-cost-advisor.js';
|
|
14
15
|
import { scanBusinessLogic } from './sast/logic.js';
|
|
15
16
|
import { scanPipeline } from './sast/pipeline.js';
|
|
16
17
|
import { scanMCP } from './sast/mcp-audit.js';
|
|
@@ -52,6 +53,7 @@ import { deadBranchRanges as _deadBranchRanges, isLineInDeadRange as _isLineInDe
|
|
|
52
53
|
import { scanJavaDeserialization } from './sast/java-deserialization.js';
|
|
53
54
|
import { scanJwtExp } from './sast/jwt-exp.js';
|
|
54
55
|
import { scanZipSlip } from './sast/zip-slip.js';
|
|
56
|
+
import { scanFileUpload } from './sast/file-upload.js';
|
|
55
57
|
import { scanHostHeader } from './sast/host-header.js';
|
|
56
58
|
import { scanPythonSinks } from './sast/python-sinks.js';
|
|
57
59
|
import { scanCSharp } from './sast/csharp.js';
|
|
@@ -172,6 +174,10 @@ import { applyLearnedCalibration } from './posture/triage-learning.js';
|
|
|
172
174
|
import { annotateFormalVerification } from './dataflow/formal-verify.js';
|
|
173
175
|
import { annotatePathFeasibility } from './dataflow/smt-feasibility.js';
|
|
174
176
|
import { annotateProofGate } from './dataflow/proof-gate.js';
|
|
177
|
+
import { annotateFalsification } from './posture/falsification.js';
|
|
178
|
+
import { routeModelForFinding } from './posture/model-routing.js';
|
|
179
|
+
import { buildEntrypointInventory } from './posture/entrypoint-inventory.js';
|
|
180
|
+
import { sweepRootCauses } from './posture/root-cause-sweep.js';
|
|
175
181
|
import { computeAnalysisTiers, countUnmodeledSinkCandidates } from './posture/coverage-report.js';
|
|
176
182
|
import { annotatePrivacyTaint, emitDpiaArtifact } from './dataflow/privacy-taint.js';
|
|
177
183
|
import { buildThreatModel as buildAutoThreatModel, persistThreatModel as persistAutoThreatModel } from './posture/threat-model-auto.js';
|
|
@@ -7396,6 +7402,7 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null},
|
|
|
7396
7402
|
const files=Object.keys(fileContents).filter(f=>shouldScan(f) && !_isPathIgnored(f));const fc={},pfr={};const aR=[],aF=[],aSrc=[],aSink=[],aSan=[],aLogic=[],aSupply=[],aSecrets=[],aCiphersRest=[],aCiphersTransit=[];let i=0;for(const p of files){i++;const _ft0=Date.now();setProgress({current:i,total:files.length,file:p.split("/").pop(),phase:"Scanning"});try{const c=fileContents[p];if(!c||c.length>500000){_filesSkipped++;continue;}const _avgLine=c.length/Math.max(c.split('\n').length,1);if(_avgLine>400&&c.length>10000){_filesDenseSkipped++;continue;}fc[p]=c;aR.push(...scanRoutes(p,c));const ta=performAnalysis(p,c);pfr[p]=ta;aF.push(...ta.findings);aSrc.push(...ta.sources);aSink.push(...ta.sinks);aSan.push(...ta.sanitizers);aLogic.push(...scanLogicVulns(p,c));aSecrets.push(...scanCredentials(p,c));aF.push(...scanStructuralVulns(p,c));aF.push(...scanExtraStructural(p,c));aF.push(...scanAliasedSinks(p,c));aF.push(...scanJavaSAST(p,c));aF.push(...scanJavaBenchExtras(p,c));aLogic.push(...scanMiddlewareOrdering(p,c));aLogic.push(...scanReDoS(p,c));if(/\.(?:java|cs|kt|py|php|phtml)$/i.test(p)){try{aLogic.push(...scanRegexReDoS(p,c));}catch(_){}}aLogic.push(...scanTodosNearSecurity(p,c));aSecrets.push(...scanEntropySecrets(p,c));const cp=scanCiphers(p,c);aCiphersRest.push(...cp.atRest);aCiphersTransit.push(...cp.inTransit);if(/\.(graphql|gql)$/i.test(p))aF.push(...scanGraphQL(p,c));aF.push(...scanIaC(p,c));aF.push(...scanTerraform(p,c));
|
|
7397
7403
|
aF.push(...scanLLM(p,c));
|
|
7398
7404
|
aF.push(...scanLLMOwasp(p,c));
|
|
7405
|
+
aF.push(...scanLlmCost(p,c));
|
|
7399
7406
|
aLogic.push(...scanBusinessLogic(p,c));
|
|
7400
7407
|
aF.push(...scanPipeline(p,c));
|
|
7401
7408
|
aF.push(...scanContainer(p,c));
|
|
@@ -7429,6 +7436,7 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null},
|
|
|
7429
7436
|
aF.push(...scanJavaDeserialization(p,c));
|
|
7430
7437
|
aF.push(...scanJwtExp(p,c));
|
|
7431
7438
|
aF.push(...scanZipSlip(p,c));
|
|
7439
|
+
aF.push(...scanFileUpload(p,c));
|
|
7432
7440
|
aF.push(...scanHostHeader(p,c));
|
|
7433
7441
|
aF.push(...scanPythonSinks(p,c));
|
|
7434
7442
|
aF.push(...scanCSharp(p,c));
|
|
@@ -7908,6 +7916,23 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null},
|
|
|
7908
7916
|
if (process.env.AGENTIC_SECURITY_NO_PROOF_GATE !== '1') {
|
|
7909
7917
|
_runAnnotator("annotateProofGate", () => { annotateProofGate(finalFindings); });
|
|
7910
7918
|
}
|
|
7919
|
+
// Addition #1 — default falsification pass. Actively tries to DISPROVE each
|
|
7920
|
+
// taint-style finding by locating a context-matched control on the path, and
|
|
7921
|
+
// demotes + quarantines the ones it can block. Recall-preserving (never
|
|
7922
|
+
// removes a finding, never touches severity — like the proof gate). Runs
|
|
7923
|
+
// AFTER proof-gate so it layers on the same demotion channel. Deterministic by
|
|
7924
|
+
// default; the LLM tier is only wired when an endpoint is configured. Opt out
|
|
7925
|
+
// with AGENTIC_SECURITY_NO_FALSIFICATION=1.
|
|
7926
|
+
if (process.env.AGENTIC_SECURITY_NO_FALSIFICATION !== '1') {
|
|
7927
|
+
_runAnnotator("annotateFalsification", () => { annotateFalsification(finalFindings, fc); });
|
|
7928
|
+
}
|
|
7929
|
+
// Addition #5 — capability-based model routing. Stamp each finding with the
|
|
7930
|
+
// model tier a cost-sensitive fixer/triager/PoC subagent should be dispatched
|
|
7931
|
+
// on for THIS vuln class (crypto/auth/critical → strongest; injection → mid;
|
|
7932
|
+
// low-sev hardening → cheapest). Advisory metadata consumed at dispatch time.
|
|
7933
|
+
_runAnnotator("annotateDispatchModel", () => {
|
|
7934
|
+
for (const f of finalFindings) { try { f.dispatchModel = routeModelForFinding(f).model; } catch { /* advisory only */ } }
|
|
7935
|
+
});
|
|
7911
7936
|
// v3 next-gen: production-aware context ingest (Pillar 9). Must run BEFORE
|
|
7912
7937
|
// the mitigation composite, persona prioritization, and final why-fired
|
|
7913
7938
|
// record so those see the demotion signals.
|
|
@@ -8538,7 +8563,12 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null},
|
|
|
8538
8563
|
try { _analysisTier = computeAnalysisTiers(Object.keys(fc)); } catch {}
|
|
8539
8564
|
try { _unmodeledSinks = countUnmodeledSinkCandidates(fc, finalFindings); } catch {}
|
|
8540
8565
|
const _scanMeta={filesScanned:files.length,filesSkipped:_filesSkipped,filesDenseSkipped:_filesDenseSkipped,filesTimedOut:_filesTimedOut,analysisTier:_analysisTier,unmodeledSinkCandidates:_unmodeledSinks,fileTimings:_fileTimings.sort((a,b)=>b.ms-a.ms).slice(0,20),findingsBySeverity:{critical:finalFindings.filter(f=>f.severity==='critical').length,high:finalFindings.filter(f=>f.severity==='high').length,medium:finalFindings.filter(f=>f.severity==='medium').length,low:finalFindings.filter(f=>f.severity==='low').length,info:finalFindings.filter(f=>f.severity==='info').length}};
|
|
8541
|
-
|
|
8566
|
+
// Addition #2 — attack-surface completeness inventory (entry points → dispositions).
|
|
8567
|
+
let _entrypointInventory = {}; try { _entrypointInventory = buildEntrypointInventory(fc, { routes: aR, findings: finalFindings }); } catch { _entrypointInventory = {}; }
|
|
8568
|
+
// Addition #3 — root-cause sweep: from confirmed findings, find sibling instances
|
|
8569
|
+
// detectors missed, with total-count accounting. Confirmed-only (cheap by default).
|
|
8570
|
+
let _rootCauseSweep = null; try { _rootCauseSweep = sweepRootCauses(finalFindings, fc); } catch { _rootCauseSweep = null; }
|
|
8571
|
+
return{entrypointInventory:_entrypointInventory,rootCauseSweep:_rootCauseSweep,routes:dd(aR,r=>`${r.method}:${r.path}:${r.file}:${r.line}`),findings:finalFindings,sources:aSrc,sinks:aSink,sanitizers:aSan,filesScanned:files.length,crossFileCount:cf.length,logicVulns:aLogic,supplyChain,components:annotatedComponents,secrets:aSecrets,ciphers:{atRest:aCiphersRest,inTransit:aCiphersTransit},pfr,fc,suppressions:_getSuppressions(),_v3,_scanMeta,_engineErrors:{cppDataflowParseErrors:_cppDataflowParseErrors.value},annotatorErrors:_annotatorErrors,threatModel:_threatModel,sbomDiff:_sbomDiff,complianceReport:_complianceReport,exploitBundles:_exploitBundles,pqcPlan:_pqcPlan,licenseGraph:_licenseGraph,attributions:_attributions,attackTaxonomy:_taxonomySummary};}
|
|
8542
8572
|
|
|
8543
8573
|
// Post-aggregation classification: every source becomes "unsafe"|"safe"; every sink becomes "confirmed"|"safe".
|
|
8544
8574
|
// Orphans (no finding linkage) are bucketed by file-local heuristic so the UI shows binary states only.
|
|
@@ -17,6 +17,7 @@ import * as fs from 'node:fs';
|
|
|
17
17
|
import * as path from 'node:path';
|
|
18
18
|
import * as cp from 'node:child_process';
|
|
19
19
|
import { buildJiraIssue } from './index.js';
|
|
20
|
+
import { escapeMarkdown } from '../util/untrusted.js';
|
|
20
21
|
|
|
21
22
|
function statePath(scanRoot) {
|
|
22
23
|
return path.join(scanRoot, '.agentic-security', 'tickets.json');
|
|
@@ -32,7 +33,10 @@ function writeState(scanRoot, state) {
|
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
function findingTitle(f) {
|
|
35
|
-
|
|
36
|
+
// vuln/title are lifted from the (untrusted) scanned code — escape before
|
|
37
|
+
// they land in an issue title. See docs/AGENT_THREAT_MODEL.md path #1.
|
|
38
|
+
const label = escapeMarkdown(f.vuln) || escapeMarkdown(f.title) || 'security finding';
|
|
39
|
+
return `[${(f.severity || 'medium').toUpperCase()}] ${label} at ${f.file}:${f.line}`;
|
|
36
40
|
}
|
|
37
41
|
function findingBody(f) {
|
|
38
42
|
const br = f.blastRadius;
|
|
@@ -43,9 +47,11 @@ function findingBody(f) {
|
|
|
43
47
|
f.cwe ? `**CWE:** ${f.cwe}` : null,
|
|
44
48
|
f.epss != null ? `**EPSS:** ${f.epss.toFixed(4)} (percentile ${(f.epssPercentile * 100).toFixed(1)}%)` : null,
|
|
45
49
|
exploited,
|
|
46
|
-
f.description ? `\n${f.description}` : null,
|
|
50
|
+
f.description ? `\n${escapeMarkdown(f.description)}` : null,
|
|
47
51
|
br?.narrative ? `\n**Blast radius:** ${br.narrative}` : null,
|
|
48
|
-
|
|
52
|
+
// snippet is attacker-authored code; escape so a crafted ``` fence or
|
|
53
|
+
// <img>/[x](url) inside it cannot break out of the code block.
|
|
54
|
+
f.snippet ? `\n\`\`\`\n${escapeMarkdown(f.snippet)}\n\`\`\`` : null,
|
|
49
55
|
f.remediation ? `\n**Remediation:** ${f.remediation}` : null,
|
|
50
56
|
`\n---\n_Surfaced by agentic-security · finding id: ${f.id}_`,
|
|
51
57
|
].filter(Boolean).join('\n');
|
package/src/ir/CLAUDE.md
CHANGED
|
@@ -76,23 +76,28 @@ topLevel}`) as the regex parser. The CFG is built from the real AST, so:
|
|
|
76
76
|
Each of these is a real failure mode; the regex fallback keeps the scan
|
|
77
77
|
producing findings instead of returning empty.
|
|
78
78
|
|
|
79
|
-
### What CST
|
|
80
|
-
|
|
81
|
-
The helper
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
79
|
+
### What CST models (and the one remaining limit)
|
|
80
|
+
|
|
81
|
+
The helper now lowers — and the dataflow engine propagates taint through — all
|
|
82
|
+
of the constructs this section once listed as unmodeled. Verified end-to-end in
|
|
83
|
+
`test/parser-py-cst.test.js` (`#16` flow tests):
|
|
84
|
+
|
|
85
|
+
- `match` case bodies — each `case` arm lowers to an `if` (the pattern) plus its
|
|
86
|
+
body block; a capture pattern (`case Foo(x)`) emits an assign for the binding.
|
|
87
|
+
Taint flows source → through a case body → sink.
|
|
88
|
+
- walrus `:=` — the named binding is tracked as its own assign, both at statement
|
|
89
|
+
position and inside `if`/`while` tests (`_emit_walrus_assigns`).
|
|
90
|
+
- comprehension generators — the loop-var assign from the iter AND the
|
|
91
|
+
generator's own `if` filters are emitted (`for x in iter if cond`).
|
|
92
|
+
- destructuring assignment (`a, b = expr`) — one assign per target, sourced from
|
|
93
|
+
the element (`member[]`) of the RHS.
|
|
94
|
+
|
|
95
|
+
**Remaining limit (a deep-engine collection-element trait, NOT a dropped CFG
|
|
96
|
+
node):** taint carried through the *element* of a destructured tuple or a
|
|
97
|
+
comprehension result — `a, b = src1, src2; sink(a)` or `xs = [src…]; sink(xs[0])`
|
|
98
|
+
— does not always reach a finding. That's the collection-element-taint limitation
|
|
99
|
+
tracked in `../dataflow/CLAUDE.md`; the CFG nodes are present, the element-level
|
|
100
|
+
propagation is the open item.
|
|
96
101
|
|
|
97
102
|
## IR shape contract
|
|
98
103
|
|
|
@@ -92,10 +92,45 @@ Reply now with the JSON object on the last line of your response. Nothing else a
|
|
|
92
92
|
`;
|
|
93
93
|
|
|
94
94
|
function endpointConfig() {
|
|
95
|
+
// Explicit BYO endpoint always wins (unchanged behaviour).
|
|
95
96
|
const endpoint = process.env.AGENTIC_SECURITY_LLM_ENDPOINT;
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
if (endpoint) {
|
|
98
|
+
return { endpoint, apiKey: process.env.AGENTIC_SECURITY_LLM_API_KEY, model: process.env.AGENTIC_SECURITY_LLM_MODEL || 'unknown', preset: null };
|
|
99
|
+
}
|
|
100
|
+
// #18 — first-class Anthropic preset. Opt-in via AGENTIC_SECURITY_LLM_PRESET=anthropic
|
|
101
|
+
// + a key (AGENTIC_SECURITY_LLM_API_KEY or ANTHROPIC_API_KEY): makes the FP-suppression
|
|
102
|
+
// validator reachable with just a key — no BYO endpoint URL or request-shape wrangling.
|
|
103
|
+
// Offline-degrading: no key → null (validator no-ops; no runtime cloud call by default).
|
|
104
|
+
if ((process.env.AGENTIC_SECURITY_LLM_PRESET || '').toLowerCase() === 'anthropic') {
|
|
105
|
+
const apiKey = process.env.AGENTIC_SECURITY_LLM_API_KEY || process.env.ANTHROPIC_API_KEY;
|
|
106
|
+
if (!apiKey) return null;
|
|
107
|
+
return {
|
|
108
|
+
endpoint: 'https://api.anthropic.com/v1/messages',
|
|
109
|
+
apiKey,
|
|
110
|
+
model: process.env.AGENTIC_SECURITY_LLM_MODEL || 'claude-haiku-4-5',
|
|
111
|
+
preset: 'anthropic',
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Shape the request for the target: the Anthropic Messages API needs an
|
|
118
|
+
// x-api-key header (added by the caller), an anthropic-version header, and a
|
|
119
|
+
// {model, max_tokens, messages:[…]} body with the reply in content[].text. The
|
|
120
|
+
// generic path posts {prompt, model} with a Bearer header. Pure — no I/O.
|
|
121
|
+
function buildRequest(model, prompt, preset) {
|
|
122
|
+
if (preset === 'anthropic') {
|
|
123
|
+
return {
|
|
124
|
+
headers: { 'Content-Type': 'application/json', 'anthropic-version': '2023-06-01' },
|
|
125
|
+
body: { model, max_tokens: 512, messages: [{ role: 'user', content: prompt }] },
|
|
126
|
+
extractText: (j) => (Array.isArray(j?.content) ? j.content.filter(b => b?.type === 'text').map(b => b.text || '').join('') : ''),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
headers: { 'Content-Type': 'application/json' },
|
|
131
|
+
body: { prompt, model },
|
|
132
|
+
extractText: (j) => (j && (j.response || j.text || j.content || j.output || j.choices?.[0]?.message?.content || j.message?.content)) || '',
|
|
133
|
+
};
|
|
99
134
|
}
|
|
100
135
|
|
|
101
136
|
function ensureCacheDir(scanRoot) {
|
|
@@ -179,17 +214,17 @@ function renderPrompt(finding, fileContents, challenge, nonce) {
|
|
|
179
214
|
.replace('{{context}}', sterileContext || '(no surrounding code available)');
|
|
180
215
|
}
|
|
181
216
|
|
|
182
|
-
async function callEndpoint(endpoint, apiKey, model, prompt) {
|
|
183
|
-
const headers =
|
|
184
|
-
if (apiKey)
|
|
185
|
-
|
|
217
|
+
async function callEndpoint(endpoint, apiKey, model, prompt, preset = null) {
|
|
218
|
+
const { headers, body, extractText } = buildRequest(model, prompt, preset);
|
|
219
|
+
if (apiKey) {
|
|
220
|
+
if (preset === 'anthropic') headers['x-api-key'] = apiKey;
|
|
221
|
+
else headers['Authorization'] = `Bearer ${apiKey}`;
|
|
222
|
+
}
|
|
186
223
|
try {
|
|
187
224
|
const r = await fetch(endpoint, { method: 'POST', headers, body: JSON.stringify(body) });
|
|
188
225
|
if (!r.ok) return { ok: false, error: `HTTP ${r.status}` };
|
|
189
226
|
const j = await r.json().catch(() => null);
|
|
190
|
-
|
|
191
|
-
j.choices?.[0]?.message?.content || j.message?.content)) || '';
|
|
192
|
-
return { ok: true, text: String(text) };
|
|
227
|
+
return { ok: true, text: String(extractText(j) || '') };
|
|
193
228
|
} catch (e) {
|
|
194
229
|
return { ok: false, error: e.message };
|
|
195
230
|
}
|
|
@@ -329,7 +364,7 @@ export async function validateOne(finding, fileContents, scanRoot) {
|
|
|
329
364
|
const challenge = crypto.randomBytes(8).toString('hex');
|
|
330
365
|
const nonce = crypto.randomBytes(8).toString('hex');
|
|
331
366
|
const prompt = renderPrompt(finding, fileContents, challenge, nonce);
|
|
332
|
-
const resp = await callEndpoint(cfg.endpoint, cfg.apiKey, cfg.model, prompt);
|
|
367
|
+
const resp = await callEndpoint(cfg.endpoint, cfg.apiKey, cfg.model, prompt, cfg.preset);
|
|
333
368
|
if (!resp.ok) {
|
|
334
369
|
finding.validator_verdict = 'unvalidated';
|
|
335
370
|
finding.unvalidated = true;
|
|
@@ -436,4 +471,4 @@ export function applyValidatorVerdicts(findings) {
|
|
|
436
471
|
return { kept, dropped };
|
|
437
472
|
}
|
|
438
473
|
|
|
439
|
-
export const _internal = { PROMPT_VERSION, renderPrompt, parseLastJsonObject, validateResponse, sanitizeReasoning, cacheKey };
|
|
474
|
+
export const _internal = { PROMPT_VERSION, renderPrompt, parseLastJsonObject, validateResponse, sanitizeReasoning, cacheKey, endpointConfig, buildRequest };
|
package/src/mcp/tools.js
CHANGED
|
@@ -17,6 +17,7 @@ import * as fsp from 'node:fs/promises';
|
|
|
17
17
|
import * as path from 'node:path';
|
|
18
18
|
import * as crypto from 'node:crypto';
|
|
19
19
|
import { applyFix as applyFixHistory, fixAcceptanceRate } from '../posture/fix-history.js';
|
|
20
|
+
import { synthesizeDeterministicPatch } from '../posture/deterministic-fix.js';
|
|
20
21
|
import { verifyLastScan } from '../posture/integrity.js';
|
|
21
22
|
import { analyzeTranscript, formatCacheReport, renderCacheStatusLine } from '../posture/cache-economics.js';
|
|
22
23
|
import { redactString, redactFinding } from './redact.js';
|
|
@@ -491,7 +492,7 @@ export const explain_finding = {
|
|
|
491
492
|
// ─── apply_fix ───────────────────────────────────────────────────────────────
|
|
492
493
|
export const apply_fix = {
|
|
493
494
|
name: 'apply_fix',
|
|
494
|
-
description: 'Apply the stored replacement
|
|
495
|
+
description: 'Apply a fix for a finding. Two modes: (1) the stored fix.replacement, or (2) a caller-supplied `patch` (a files map) which is RE-VERIFIED inline (rescan-clean + no new ≥medium + lint) before any write — this unblocks findings that ship only a template or description. Refuses if last-scan.json fails its HMAC check, if the finding is shadow-marked, or if a path escapes the session root via lexical traversal OR a symlink. Requires confirm:true. Supports dry_run:true to preview without writing.',
|
|
495
496
|
inputSchema: {
|
|
496
497
|
type: 'object',
|
|
497
498
|
additionalProperties: false,
|
|
@@ -499,10 +500,15 @@ export const apply_fix = {
|
|
|
499
500
|
finding_id: { type: 'string', minLength: 1, maxLength: 256 },
|
|
500
501
|
confirm: { type: 'boolean' },
|
|
501
502
|
dry_run: { type: 'boolean' },
|
|
503
|
+
patch: {
|
|
504
|
+
type: 'object',
|
|
505
|
+
additionalProperties: { type: 'string', maxLength: 500_000 },
|
|
506
|
+
minProperties: 1, maxProperties: 8,
|
|
507
|
+
},
|
|
502
508
|
},
|
|
503
509
|
required: ['finding_id', 'confirm'],
|
|
504
510
|
},
|
|
505
|
-
async handler({ finding_id, confirm, dry_run = false }, ctx) {
|
|
511
|
+
async handler({ finding_id, confirm, dry_run = false, patch = null }, ctx) {
|
|
506
512
|
if (confirm !== true) {
|
|
507
513
|
return { _meta: META, applied: false, reason: 'apply_fix requires confirm: true.' };
|
|
508
514
|
}
|
|
@@ -515,6 +521,85 @@ export const apply_fix = {
|
|
|
515
521
|
if (f._shadow === true) {
|
|
516
522
|
return { _meta: META, applied: false, reason: 'shadow findings cannot be auto-applied' };
|
|
517
523
|
}
|
|
524
|
+
|
|
525
|
+
// #3 — verifier-approved patch path. When the caller supplies `patch` (a
|
|
526
|
+
// files map, same shape as verify_fix), apply_fix re-runs the verifier
|
|
527
|
+
// INLINE and writes only if it passes: the original finding's stableId is
|
|
528
|
+
// gone, no new ≥medium finding was introduced, and lint is clean. This lets
|
|
529
|
+
// a deterministic OR LLM-synthesized patch be applied for the ~100% of
|
|
530
|
+
// findings that ship only a template/description (no stored fix.replacement).
|
|
531
|
+
// Security: all existing gates hold (confirm, last-scan HMAC, reserved
|
|
532
|
+
// paths, confinement, fix-history backup + attempt budget); the write is
|
|
533
|
+
// additionally gated on a FRESH verification, so a stale/forged patch can't
|
|
534
|
+
// slip through — there is no token to replay, the verify runs here and now.
|
|
535
|
+
if (patch && typeof patch === 'object' && Object.keys(patch).length) {
|
|
536
|
+
if (!f.stableId) {
|
|
537
|
+
return { _meta: META, applied: false, reason: 'finding has no stableId — cannot verify a patch against it' };
|
|
538
|
+
}
|
|
539
|
+
const confinedAbs = {};
|
|
540
|
+
for (const [rel, content] of Object.entries(patch)) {
|
|
541
|
+
let abs;
|
|
542
|
+
try { abs = _confine(ctx.sessionRoot, rel, 'patch key'); }
|
|
543
|
+
catch (e) { return { _meta: META, applied: false, reason: `path-escape refused: ${e.message}` }; }
|
|
544
|
+
if (_isReservedWritePath(ctx.sessionRoot, abs)) {
|
|
545
|
+
return { _meta: META, applied: false, reason: `reserved path refused: ${rel}` };
|
|
546
|
+
}
|
|
547
|
+
confinedAbs[rel] = { abs, content: String(content) };
|
|
548
|
+
}
|
|
549
|
+
// Inline re-verify — the load-bearing gate. Must pass to write.
|
|
550
|
+
let verdict;
|
|
551
|
+
try {
|
|
552
|
+
const _files = Object.fromEntries(Object.entries(confinedAbs).map(([rel, v]) => [rel, v.content]));
|
|
553
|
+
if (process.env.AGENTIC_SECURITY_FIX_RUN_TESTS === '1') {
|
|
554
|
+
// Addition #7 — connect the closed-loop verifier: add the project test
|
|
555
|
+
// suite as a fourth verification leg (scan + lint + tests). Opt-in
|
|
556
|
+
// because many repos have no runner and we must not fail-closed by
|
|
557
|
+
// default. Normalized to the scan+lint verdict shape used below.
|
|
558
|
+
const { verifyFixWithTests } = await import('../posture/fix-verify-loop.js');
|
|
559
|
+
const t = await verifyFixWithTests({ scanRoot: ctx.sessionRoot, originalFindingStableId: f.stableId, files: _files });
|
|
560
|
+
verdict = { ok: t.ok, summary: t.summary, rescan: t.legs?.scan?.detail, lint: t.legs?.lint?.detail, tests: t.legs?.tests, testVerdict: t.verdict };
|
|
561
|
+
} else {
|
|
562
|
+
const verifyFixCore = await getVerifyFixCore();
|
|
563
|
+
verdict = await verifyFixCore({
|
|
564
|
+
scanRoot: ctx.sessionRoot,
|
|
565
|
+
originalFindingStableId: f.stableId,
|
|
566
|
+
files: _files,
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
} catch (e) {
|
|
570
|
+
return { _meta: META, applied: false, reason: `patch verification failed: ${e.message}` };
|
|
571
|
+
}
|
|
572
|
+
if (!verdict.ok) {
|
|
573
|
+
return {
|
|
574
|
+
_meta: META, applied: false,
|
|
575
|
+
reason: `patch rejected by verifier: ${verdict.summary || verdict.rescan?.reason || 'did not verify'}`,
|
|
576
|
+
verify: { rescan: verdict.rescan, lint: { runner: verdict.lint?.runner, ok: verdict.lint?.ok } },
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
if (dry_run) {
|
|
580
|
+
return { _meta: META, applied: false, dryRun: true, verified: true, files: Object.keys(confinedAbs), summary: verdict.summary };
|
|
581
|
+
}
|
|
582
|
+
const written = [];
|
|
583
|
+
try {
|
|
584
|
+
for (const [rel, v] of Object.entries(confinedAbs)) {
|
|
585
|
+
const originalContent = fs.existsSync(v.abs) ? await fsp.readFile(v.abs, 'utf8') : '';
|
|
586
|
+
const entry = await applyFixHistory({
|
|
587
|
+
scanRoot: ctx.sessionRoot, file: rel, originalContent, newContent: v.content,
|
|
588
|
+
findingId: f.id, stableId: f.stableId, ruleId: f.rule || null, vuln: f.vuln || f.title || null,
|
|
589
|
+
});
|
|
590
|
+
written.push({ file: rel, historyId: entry.id, backupPath: entry.backupPath });
|
|
591
|
+
}
|
|
592
|
+
} catch (e) {
|
|
593
|
+
if (e && e.name === 'FixAttemptBudgetExceededError') {
|
|
594
|
+
return { _meta: META, applied: false, reason: `budget-exceeded: ${e.message}`, budgetExceeded: true, attempts: e.attempts, maxAttempts: e.max, key: e.key };
|
|
595
|
+
}
|
|
596
|
+
throw e;
|
|
597
|
+
}
|
|
598
|
+
let acceptance = null;
|
|
599
|
+
try { acceptance = fixAcceptanceRate(ctx.sessionRoot); } catch { /* best-effort */ }
|
|
600
|
+
return { _meta: META, applied: true, verified: true, patched: written, integrity: status, verify: { summary: verdict.summary }, acceptance };
|
|
601
|
+
}
|
|
602
|
+
|
|
518
603
|
if (typeof f.fix?.replacement !== 'string') {
|
|
519
604
|
// Premortem #2: templates are patch-shaped text. Same reasoning as
|
|
520
605
|
// the replacement path — do NOT pass through redactString here.
|
|
@@ -678,6 +763,20 @@ export const synthesize_fix = {
|
|
|
678
763
|
locDelta = Math.abs(fix.replacement.split('\n').length - orig.split('\n').length);
|
|
679
764
|
}
|
|
680
765
|
const oversized = touchedFiles > 3 || locDelta > 100;
|
|
766
|
+
// #1 — deterministic autofix: for classes with a safe context-independent
|
|
767
|
+
// swap (weak hash, TLS verify-off), materialize a full-file patch from the
|
|
768
|
+
// live file. The agent passes `autofix.patch` straight to apply_fix, which
|
|
769
|
+
// re-verifies it (rescan-clean + no new ≥medium + lint) before writing — so
|
|
770
|
+
// even a mis-attributed swap can't land a bad edit. No stored replacement,
|
|
771
|
+
// no per-finding bloat in last-scan.json.
|
|
772
|
+
let autofix = null;
|
|
773
|
+
if (!hasReplacement) {
|
|
774
|
+
try {
|
|
775
|
+
const abs = _confine(ctx.sessionRoot, f.file, 'finding.file');
|
|
776
|
+
const det = synthesizeDeterministicPatch(f, fs.readFileSync(abs, 'utf8'));
|
|
777
|
+
if (det) autofix = { deterministic: true, ruleId: det.ruleId, patch: det.patch };
|
|
778
|
+
} catch { /* best-effort — no file / no rule → no autofix */ }
|
|
779
|
+
}
|
|
681
780
|
// Premortem #2: `replacement` is a *patch* (the code we'll write to disk),
|
|
682
781
|
// not a finding excerpt. Running it through redactString silently corrupts
|
|
683
782
|
// valid patches whose content happens to match a secret-shape (e.g. a
|
|
@@ -694,9 +793,15 @@ export const synthesize_fix = {
|
|
|
694
793
|
hasReplacement,
|
|
695
794
|
replacement: hasReplacement ? fix.replacement : null,
|
|
696
795
|
template: fix.code || null,
|
|
796
|
+
autofix,
|
|
797
|
+
// #15 — the regression test the scan annotator already generated for this
|
|
798
|
+
// finding (present when a PoC was built). Surfaced here so the fix flow
|
|
799
|
+
// writes the test alongside the patch; fix-verify-loop then runs it, so an
|
|
800
|
+
// applied fix ships with a test that fails pre-fix and passes post-fix.
|
|
801
|
+
regression_test: f.regression_test || null,
|
|
697
802
|
remediation: typeof fix.description === 'string' ? fix.description : (typeof fix === 'string' ? fix : null),
|
|
698
803
|
patchBounds: { touchedFiles, locDelta, oversized },
|
|
699
|
-
recommendsFixPlan: oversized && !hasReplacement,
|
|
804
|
+
recommendsFixPlan: oversized && !hasReplacement && !autofix,
|
|
700
805
|
};
|
|
701
806
|
},
|
|
702
807
|
};
|
package/src/posture/CLAUDE.md
CHANGED
|
@@ -13,12 +13,21 @@ Annotators that run **after** every detector has emitted, plus state stores read
|
|
|
13
13
|
|
|
14
14
|
**Risk amplification** — `epss.js`, `kev` (in `version.js`), `blast-radius.js`, `crown-jewels.js`, `exploitability.js`, `bounty-prediction.js`, `risk-in-dollars` (lives in `scripts/`, not here).
|
|
15
15
|
|
|
16
|
+
**Secrets lifecycle** — `secret-history.js` (git-history blob sweep for committed-then-removed secrets, behind `--secret-history`), `secret-live-check.js` (opt-in, offline-degrading live/dead/unknown labeling via a read-only provider "whoami"; behind `--validate-secrets`).
|
|
17
|
+
|
|
16
18
|
**Production-posture ingest** — `auth-posture-import.js`, `network-policy-import.js`, `telemetry-ingest.js`, `waf-ingest.js`, `feature-flags.js`. These read customer-side YAML and convert to mitigation flags consumed by `mitigation-composite.js`.
|
|
17
19
|
|
|
18
|
-
**Fix lifecycle** — `fix-history.js` (apply + backup + recover), `fix-verify.js` (closed-loop re-scan + lint), `fix-plan.js` (oversized-patch fallback), `regression-test-gen.js
|
|
20
|
+
**Fix lifecycle** — `fix-history.js` (apply + backup + recover), `fix-verify.js` (closed-loop re-scan + lint), `fix-plan.js` (oversized-patch fallback), `regression-test-gen.js`, `deterministic-fix.js` (safe context-independent literal-swap patch synthesis — md5/sha1→sha256, TLS verify-off→on — materialized on demand by `mcp/synthesize_fix`; every patch still passes through `apply_fix`'s inline verify before it lands).
|
|
19
21
|
|
|
20
22
|
**Agentic verification** — `verifier.js`, `verifier-target.js`, `verifier-ephemeral.js`, `harness-discovery.js`, `adversary-agent.js`, `defender-agent.js`, `auditor-agent.js`, `three-agent-pipeline.js`.
|
|
21
23
|
|
|
24
|
+
**Methodology additions (`docs/AGENTIC_METHODOLOGY_PRD.md`)** — default-on annotators/artifacts that layer the agentic-hunter methodology on the deterministic engine:
|
|
25
|
+
- `falsification.js` — default falsification pass. For each taint-style finding, tries to DISPROVE it (locate a context-matched control on the path, reusing `dataflow/sanitizer-proof.js`'s shape rules read-only); a blocked finding is demoted + `quarantined`, never removed and never severity-touched (recall-preserving, like `proof-gate`). Wired after `annotateProofGate`. Opt out: `AGENTIC_SECURITY_NO_FALSIFICATION=1`. Optional LLM tier over survivors when an endpoint is configured.
|
|
26
|
+
- `entrypoint-inventory.js` — attack-surface completeness ledger. Enumerates every entry point (HTTP/queue/cron/CLI/env/upload/webhook) with a disposition each; on `scan.entrypointInventory`.
|
|
27
|
+
- `root-cause-sweep.js` — from confirmed findings, finds sibling instances detectors missed with total-count accounting (`found === candidates + mitigated`); on `scan.rootCauseSweep`.
|
|
28
|
+
- `model-routing.js` — capability-based CWE/severity→model policy; stamps `finding.dispatchModel` (strongest for crypto/auth/critical, mid for injection, cheapest for low-sev hardening) for cost-sensitive subagent dispatch.
|
|
29
|
+
- `fix-honesty-gate.js` — deterministic honesty gates on fix output: a residual-risk hand-wave guard, a cited-file:line requirement for any FP/safe verdict, and FULL/MITIGATION/WORKAROUND completeness tiers. Consumed by `fix-verify.js` when the caller supplies fix metadata; the closed-loop test leg (`fix-verify-loop.js`) is wired into `mcp/apply_fix` behind `AGENTIC_SECURITY_FIX_RUN_TESTS=1`.
|
|
30
|
+
|
|
22
31
|
**Integrity + signing** — `integrity.js` (per-install HMAC for `last-scan.json`), `rule-pack-signing.js`. The HMAC key lives at `$XDG_CONFIG_HOME/agentic-security/scan-key`; override via `$AGENTIC_SECURITY_HMAC_KEY`. Premortem-derived; do not regress to hostname-derived.
|
|
23
32
|
|
|
24
33
|
**Rule lifecycle** — `custom-rules.js` (YAML pattern DSL), `rule-overrides.js` (`disable:` gated on signature), `rule-packs.js`, `rule-synthesis.js` (proposes suppressions from triage feedback), `ruleset-version.js`.
|
|
@@ -26,9 +26,11 @@ function money(n) {
|
|
|
26
26
|
|
|
27
27
|
// Per-1M-token rates (input / output). Mirror hooks/model-cost-advisor.js MODELS.
|
|
28
28
|
const MODEL_RATES = {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
fable: { label: 'Fable 5', in: 10, out: 50 },
|
|
30
|
+
opus: { label: 'Opus 4.8', in: 5, out: 25 },
|
|
31
|
+
sonnet5: { label: 'Sonnet 5', in: 3, out: 15 },
|
|
32
|
+
sonnet: { label: 'Sonnet 4.6', in: 3, out: 15 },
|
|
33
|
+
haiku: { label: 'Haiku 4.5', in: 1, out: 5 },
|
|
32
34
|
};
|
|
33
35
|
const CACHE_READ_MULT = 0.1; // cache read ≈ 0.1× input
|
|
34
36
|
const CACHE_WRITE_MULT = 1.25; // 5-minute cache write ≈ 1.25× input
|
|
@@ -40,8 +42,9 @@ const TTL_MS = 5 * 60 * 1000;
|
|
|
40
42
|
function rateFor(model) {
|
|
41
43
|
if (typeof model !== 'string') return null;
|
|
42
44
|
const s = model.toLowerCase();
|
|
45
|
+
if (s.includes('fable') || s.includes('mythos')) return MODEL_RATES.fable;
|
|
43
46
|
if (s.includes('haiku')) return MODEL_RATES.haiku;
|
|
44
|
-
if (s.includes('sonnet')) return MODEL_RATES.sonnet;
|
|
47
|
+
if (s.includes('sonnet')) return (s.includes('sonnet-5') || s.includes('sonnet 5')) ? MODEL_RATES.sonnet5 : MODEL_RATES.sonnet;
|
|
45
48
|
if (s.includes('opus')) return MODEL_RATES.opus;
|
|
46
49
|
return null;
|
|
47
50
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Deterministic fix synthesis (#1) — for the narrow set of vulnerability classes
|
|
2
|
+
// where a context-INDEPENDENT literal swap is a safe, correct fix, produce a
|
|
3
|
+
// full-file replacement from the current file content. No LLM, no guessing, no
|
|
4
|
+
// per-finding bloat in last-scan.json (the patch is materialized on demand by
|
|
5
|
+
// synthesize_fix from the live file, not stored on every finding).
|
|
6
|
+
//
|
|
7
|
+
// Safety: every patch this produces is still gated by verify_fix before apply_fix
|
|
8
|
+
// writes it (original finding gone + no new ≥medium + lint clean). So a swap that
|
|
9
|
+
// a rule mis-attributed simply fails verification instead of landing a bad edit —
|
|
10
|
+
// this module widens the deterministic-fix surface without weakening the gate.
|
|
11
|
+
//
|
|
12
|
+
// Returns { patch: { [relFile]: newContent }, ruleId } or null when no
|
|
13
|
+
// deterministic fix applies to the finding.
|
|
14
|
+
|
|
15
|
+
const JS_EXT = /\.(?:js|jsx|ts|tsx|mjs|cjs)$/i;
|
|
16
|
+
const PY_EXT = /\.py$/i;
|
|
17
|
+
|
|
18
|
+
// Each rule gates on the finding's cwe/family, then rewrites the whole-file
|
|
19
|
+
// content. transform() returns the new content, or null when nothing changed
|
|
20
|
+
// (e.g. the vulnerable token isn't literally present — then we don't claim a fix).
|
|
21
|
+
const RULES = [
|
|
22
|
+
{
|
|
23
|
+
id: 'weak-hash-sha256',
|
|
24
|
+
// md5 / sha1 → sha256. Every occurrence in the file is a weak hash, so
|
|
25
|
+
// swapping them all is safe; the verifier confirms the weak-hash finding is
|
|
26
|
+
// gone and nothing worse appeared.
|
|
27
|
+
applies: (f) => /CWE-(?:327|328|916)/.test(f.cwe || '') || /weak.?hash/i.test(f.family || ''),
|
|
28
|
+
transform: (content, file) => {
|
|
29
|
+
let out = content;
|
|
30
|
+
if (JS_EXT.test(file)) {
|
|
31
|
+
out = out.replace(/(\bcreateHash\s*\(\s*['"`])(?:md5|sha1)(['"`])/gi, '$1sha256$2');
|
|
32
|
+
} else if (PY_EXT.test(file)) {
|
|
33
|
+
out = out.replace(/\bhashlib\.(?:md5|sha1)\s*\(/g, 'hashlib.sha256(');
|
|
34
|
+
}
|
|
35
|
+
return out !== content ? out : null;
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: 'tls-verify-on',
|
|
40
|
+
// Disabled TLS verification → enabled. rejectUnauthorized:false → true (JS),
|
|
41
|
+
// verify=False → verify=True (Python requests).
|
|
42
|
+
applies: (f) => /CWE-295/.test(f.cwe || '') || /tls.?no.?verify|cert.?(?:none|verify)/i.test(f.family || ''),
|
|
43
|
+
transform: (content, file) => {
|
|
44
|
+
let out = content;
|
|
45
|
+
if (JS_EXT.test(file)) {
|
|
46
|
+
out = out.replace(/(\brejectUnauthorized\s*:\s*)false\b/g, '$1true');
|
|
47
|
+
} else if (PY_EXT.test(file)) {
|
|
48
|
+
out = out.replace(/(\bverify\s*=\s*)False\b/g, '$1True');
|
|
49
|
+
}
|
|
50
|
+
return out !== content ? out : null;
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
export function synthesizeDeterministicPatch(finding, fileContent) {
|
|
56
|
+
if (!finding || typeof fileContent !== 'string' || !finding.file) return null;
|
|
57
|
+
for (const rule of RULES) {
|
|
58
|
+
try {
|
|
59
|
+
if (!rule.applies(finding)) continue;
|
|
60
|
+
const next = rule.transform(fileContent, finding.file);
|
|
61
|
+
if (next && next !== fileContent) return { patch: { [finding.file]: next }, ruleId: rule.id };
|
|
62
|
+
} catch { /* a single rule failing must never break synthesis */ }
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|