@clear-capabilities/agentic-security-scanner 0.130.0 → 0.133.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 +247 -0
- package/bin/agentic-security.js +39 -3
- package/dist/113.index.js +294 -5
- package/dist/178.index.js +1 -1
- package/dist/207.index.js +7 -4
- package/dist/238.index.js +218 -0
- package/dist/259.index.js +975 -0
- package/dist/384.index.js +1 -1
- package/dist/435.index.js +2 -2
- package/dist/526.index.js +294 -5
- package/dist/637.index.js +1 -1
- package/dist/agentic-security.mjs +18 -57
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +19 -10
- package/src/engine.js +48 -1
- package/src/ir/parser-js.js +8 -0
- package/src/llm-validator/cost-ceiling.js +199 -0
- package/src/llm-validator/index.js +241 -12
- package/src/llm-validator/local-endpoint.js +90 -0
- package/src/mcp/tools.js +2 -2
- package/src/posture/CLAUDE.md +83 -6
- package/src/posture/accuracy-scorecard.js +37 -6
- package/src/posture/attestation.js +7 -4
- package/src/posture/corpus-enroll.js +303 -0
- package/src/posture/corpus-match.js +67 -0
- package/src/posture/custom-rules.js +2 -2
- package/src/posture/execution-proof.js +44 -4
- package/src/posture/fix-metrics.js +197 -0
- package/src/posture/fix-verify.js +76 -2
- package/src/posture/integrity.js +42 -9
- package/src/posture/learning.js +8 -1
- package/src/posture/model-routing.js +26 -0
- package/src/posture/model-trust.js +174 -0
- package/src/posture/poc-inprocess.js +165 -0
- package/src/posture/prove-findings.js +148 -0
- package/src/posture/root-cause-sweep.js +0 -0
- package/src/posture/rule-overrides.js +64 -3
- package/src/posture/state-dir.js +25 -0
- package/src/posture/vuln-archaeology.js +231 -0
- package/src/report/index.js +7 -0
- package/src/runScan.js +2 -6
- package/src/sandbox/CLAUDE.md +190 -46
- package/src/sandbox/backend-namespace.js +328 -48
- package/src/sandbox/backend-userspace.js +6 -19
- package/src/sandbox/capabilities.js +132 -4
- package/src/sandbox/limits.js +21 -0
- package/src/sandbox/result.js +1 -1
- package/src/sast/CLAUDE.md +4 -0
- package/src/sast/crypto-specialist.js +247 -0
- package/src/util/glob.js +173 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
a868fa3988608a5a5c0867774d870781a52d79924daf0956652c62fa1384d782 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.133.0",
|
|
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",
|
|
@@ -38,9 +38,8 @@
|
|
|
38
38
|
"@babel/core": "^8.0.1",
|
|
39
39
|
"@babel/preset-react": "^8.0.1",
|
|
40
40
|
"@babel/preset-typescript": "^8.0.1",
|
|
41
|
-
"fast-glob": "^3.3.3",
|
|
42
41
|
"java-parser": "^3.0.1",
|
|
43
|
-
"js-yaml": "^5.2.
|
|
42
|
+
"js-yaml": "^5.2.3",
|
|
44
43
|
"safe-regex": "^2.1.1"
|
|
45
44
|
},
|
|
46
45
|
"optionalDependencies": {
|
|
@@ -48,7 +47,7 @@
|
|
|
48
47
|
"web-tree-sitter": "0.20.8"
|
|
49
48
|
},
|
|
50
49
|
"devDependencies": {
|
|
51
|
-
"@types/node": "^26.
|
|
50
|
+
"@types/node": "^26.2.0",
|
|
52
51
|
"@vercel/ncc": "^0.44.1"
|
|
53
52
|
},
|
|
54
53
|
"overrides": {
|
|
@@ -57,12 +56,14 @@
|
|
|
57
56
|
},
|
|
58
57
|
"scripts": {
|
|
59
58
|
"build": "ncc build bin/agentic-security.js -o dist --minify -e web-tree-sitter -e tree-sitter-wasms && mv dist/index.js dist/agentic-security.mjs && rm -f dist/package.json && chmod +x dist/agentic-security.mjs && node -e \"const fs=require('fs');const p='dist/agentic-security.mjs';const c=fs.readFileSync(p,'utf8');if(!c.startsWith('#!'))fs.writeFileSync(p,'#!/usr/bin/env node\\n'+c);\" && node -e \"const fs=require('fs');const c=require('crypto');const h=c.createHash('sha256').update(fs.readFileSync('dist/agentic-security.mjs')).digest('hex');fs.writeFileSync('dist/agentic-security.mjs.sha256',h+' agentic-security.mjs\\n');\"",
|
|
60
|
-
"
|
|
61
|
-
"
|
|
59
|
+
"prepare": "node ../scripts/pre-push-gate.mjs --install-hook",
|
|
60
|
+
"prepublishOnly": "npm run build && node ../scripts/sync-scanner-changelog.mjs && node ../scripts/release-check.mjs",
|
|
61
|
+
"test": "npm run test:smoke && npm run test:glob && 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:
|
|
64
|
-
"test:
|
|
65
|
-
"test:
|
|
63
|
+
"test:glob": "node --test test/glob-compat.test.js",
|
|
64
|
+
"test:sast": "node --test test/crypto-specialist.test.js 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",
|
|
65
|
+
"test:posture": "node --test test/material-change.test.js test/drift.test.js test/scorecard.test.js test/accuracy-scorecard.test.js test/scorecard-gate.test.js test/release-check.test.js test/pre-push-gate.test.js test/dependency-currency.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/verification-separation.test.js test/attestation.test.js test/determinism-cross-machine.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/relevance.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-verify-tests.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 test/execution-proof.test.js test/fix-metrics.test.js test/corpus-enroll.test.js test/prove-findings.test.js test/corpus-match.test.js test/corpus-provenance.test.js test/learning-quorum.test.js test/llm-cache-integrity.test.js test/integrity-legacy-key.test.js test/suppression-visibility.test.js test/cost-ceiling.test.js test/local-endpoint.test.js test/model-trust.test.js test/vuln-archaeology.test.js test/scan-checkpoint.test.js test/llm-redact.test.js",
|
|
66
|
+
"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 test/ir-stats.test.js test/parser-cpp.test.js test/parser-js-decorators.test.js test/cpp-integration.test.js test/engine-reconnect.test.js test/phase2-scoping.test.js test/engine-recall.test.js",
|
|
66
67
|
"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
68
|
"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
69
|
"test:bench-modules": "node --test test/phase4-harness.test.js test/pipeline.test.js test/proof-corpus-lib.test.js test/proof-corpus-runner.test.js",
|
|
@@ -95,7 +96,15 @@
|
|
|
95
96
|
"bench:self-scan:check": "node ../bench/self-scan/check.mjs",
|
|
96
97
|
"bench:self-scan:update-baseline": "node ../bench/self-scan/check.mjs --update-baseline",
|
|
97
98
|
"bench:engine-recall": "node ../bench/engine-recall/measure.mjs",
|
|
98
|
-
"scorecard": "node ../scripts/scorecard.mjs"
|
|
99
|
+
"scorecard": "node ../scripts/scorecard.mjs",
|
|
100
|
+
"corpus:enroll": "node ../scripts/enroll-proven-finding.mjs",
|
|
101
|
+
"corpus:provenance": "node ../scripts/corpus-provenance-check.mjs",
|
|
102
|
+
"determinism:attest": "node ../scripts/attest-fixture.mjs",
|
|
103
|
+
"scorecard:check": "node ../scripts/scorecard-check.mjs",
|
|
104
|
+
"release:check": "node ../scripts/release-check.mjs",
|
|
105
|
+
"release:check:fast": "node ../scripts/release-check.mjs --fast",
|
|
106
|
+
"gate:prepush": "node ../scripts/pre-push-gate.mjs",
|
|
107
|
+
"gate:prepush:install": "node ../scripts/pre-push-gate.mjs --install-hook"
|
|
99
108
|
},
|
|
100
109
|
"author": "Ross Young <ross@clearcapabilities.com>",
|
|
101
110
|
"license": "PolyForm-Internal-Use-1.0.0"
|
package/src/engine.js
CHANGED
|
@@ -90,6 +90,7 @@ import { scanOpenRedirect } from './sast/open-redirect.js';
|
|
|
90
90
|
import { scanWrongContextSanitizer, scanSanitizerContextMismatch } from './sast/wrong-context-sanitizer.js';
|
|
91
91
|
import { scanFrontendHygiene } from './sast/frontend-hygiene.js';
|
|
92
92
|
import { scanCsvInjection } from './sast/csv-injection.js';
|
|
93
|
+
import { scanCryptoSpecialist } from './sast/crypto-specialist.js';
|
|
93
94
|
import { scanStoredTaint } from './sast/stored-taint.js';
|
|
94
95
|
import { scanTreeSitterSinks } from './sast/tree-sitter-sinks.js';
|
|
95
96
|
import { scanJavaStructural } from './sast/java-structural.js';
|
|
@@ -121,6 +122,8 @@ import { classifySecretCandidate as _entropyClassifySecret } from './sast/_secre
|
|
|
121
122
|
import { annotateConfidence } from './posture/confidence.js';
|
|
122
123
|
import { backfillFindingDefaults } from './posture/finding-defaults.js';
|
|
123
124
|
import { annotatePocs } from './posture/poc-generator.js';
|
|
125
|
+
import { annotateExecutionProofs } from './posture/prove-findings.js';
|
|
126
|
+
import { mineVulnHistory, annotateHistoricalRisk } from './posture/vuln-archaeology.js';
|
|
124
127
|
import { annotateVerifierVerdicts } from './posture/verifier.js';
|
|
125
128
|
import { annotateRegressionTests } from './posture/regression-test-gen.js';
|
|
126
129
|
import { annotateCalibratedConfidence } from './posture/calibration.js';
|
|
@@ -996,6 +999,14 @@ function performASTAnalysis(fp, code) {
|
|
|
996
999
|
babelTransformSync(code, {
|
|
997
1000
|
filename: fp,
|
|
998
1001
|
presets: [presetReact, [presetTypescript, { ignoreExtensions: true }]],
|
|
1002
|
+
// Decorators are SYNTAX we must accept, never transform — without them the
|
|
1003
|
+
// parser rejects the whole file and every finding in it silently disappears.
|
|
1004
|
+
// Measured on one real target: 201 JS files unparseable, all decorator-using
|
|
1005
|
+
// framework code. 'decorators-legacy' covers the framework and TypeScript
|
|
1006
|
+
// parameter forms; 'decoratorAutoAccessors' adds the modern `accessor` field.
|
|
1007
|
+
// The modern 'decorators' variant was rejected: it cannot parse TS parameter
|
|
1008
|
+
// decorators, so it would trade one blind spot for another.
|
|
1009
|
+
parserOpts: { plugins: ['decorators-legacy', 'decoratorAutoAccessors'] },
|
|
999
1010
|
plugins: [astTaintTrackerPlugin],
|
|
1000
1011
|
ast: false, code: false,
|
|
1001
1012
|
babelrc: false, configFile: false,
|
|
@@ -4646,6 +4657,14 @@ function _buildCallGraphAST(fp, code){
|
|
|
4646
4657
|
babelTransformSync(code, {
|
|
4647
4658
|
filename: fp,
|
|
4648
4659
|
presets: [presetReact, [presetTypescript, { ignoreExtensions: true }]],
|
|
4660
|
+
// Decorators are SYNTAX we must accept, never transform — without them the
|
|
4661
|
+
// parser rejects the whole file and every finding in it silently disappears.
|
|
4662
|
+
// Measured on one real target: 201 JS files unparseable, all decorator-using
|
|
4663
|
+
// framework code. 'decorators-legacy' covers the framework and TypeScript
|
|
4664
|
+
// parameter forms; 'decoratorAutoAccessors' adds the modern `accessor` field.
|
|
4665
|
+
// The modern 'decorators' variant was rejected: it cannot parse TS parameter
|
|
4666
|
+
// decorators, so it would trade one blind spot for another.
|
|
4667
|
+
parserOpts: { plugins: ['decorators-legacy', 'decoratorAutoAccessors'] },
|
|
4649
4668
|
plugins: [callTrackerPlugin],
|
|
4650
4669
|
ast: false, code: false,
|
|
4651
4670
|
babelrc: false, configFile: false,
|
|
@@ -7550,6 +7569,11 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null,
|
|
|
7550
7569
|
aF.push(...scanSanitizerContextMismatch(p,c));
|
|
7551
7570
|
aF.push(...scanFrontendHygiene(p,c));
|
|
7552
7571
|
aF.push(...scanCsvInjection(p,c));
|
|
7572
|
+
// R16 — specialist crypto-hygiene classes (constant-time comparison,
|
|
7573
|
+
// secret zeroization). Narrow by design: keyed on the secret-ness of the
|
|
7574
|
+
// identifier, and silent whenever the correct constant-time or
|
|
7575
|
+
// guaranteed-wipe API is already present.
|
|
7576
|
+
aF.push(...scanCryptoSpecialist(p,c));
|
|
7553
7577
|
aF.push(...scanStoredTaint(p,c));
|
|
7554
7578
|
aF.push(...scanJavaStructural(p,c));
|
|
7555
7579
|
aF.push(...scanCsharpStructural(p,c));
|
|
@@ -7970,6 +7994,7 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null,
|
|
|
7970
7994
|
// Every catch in this block writes into _annotatorErrors so the operator
|
|
7971
7995
|
// can tell "didn't run" from "ran cleanly." The array is surfaced as
|
|
7972
7996
|
// scan.annotatorErrors in the report; an empty array means clean.
|
|
7997
|
+
let _executionProofSummary = null, _vulnHistory = null;
|
|
7973
7998
|
const _annotatorErrors = [];
|
|
7974
7999
|
const _runAnnotator = (phase, fn) => {
|
|
7975
8000
|
try { return fn(); }
|
|
@@ -8179,6 +8204,28 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null,
|
|
|
8179
8204
|
_runAnnotator("annotatePocs", () => { annotatePocs(finalFindings, { routes: aR, fileContents: fc }); });
|
|
8180
8205
|
// FR-VER-3: regression-test generator (builds on the PoC artifact).
|
|
8181
8206
|
_runAnnotator("annotateRegressionTests", () => { annotateRegressionTests(finalFindings); });
|
|
8207
|
+
// R2 — execution proof. Synthesizes a SANDBOX-RUNNABLE PoC (the HTTP PoCs
|
|
8208
|
+
// above need a live server, so they can never be executed by the prover)
|
|
8209
|
+
// and lets R1's sandbox decide the tier. Opt-in via AGENTIC_SECURITY_PROVE=1
|
|
8210
|
+
// because it executes code derived from the scanned project; with the flag
|
|
8211
|
+
// unset, or with no confinement backend, nothing runs and no tier moves.
|
|
8212
|
+
// Awaited rather than fire-and-forget: a proof that lands after the report
|
|
8213
|
+
// is emitted is not evidence anyone sees.
|
|
8214
|
+
// R14 — vulnerability archaeology. Mines git history for where this team has
|
|
8215
|
+
// introduced security bugs before and attaches an ADVISORY per-file prior.
|
|
8216
|
+
// Never a finding and never a severity change: those bugs are fixed, and a
|
|
8217
|
+
// historical fix is not evidence of a present defect. Opt-in because it
|
|
8218
|
+
// shells out to git over up to 500 commits.
|
|
8219
|
+
_runAnnotator('annotateHistoricalRisk', () => {
|
|
8220
|
+
if (process.env.AGENTIC_SECURITY_ARCHAEOLOGY !== '1' || !scanRoot) return;
|
|
8221
|
+
_vulnHistory = mineVulnHistory(scanRoot);
|
|
8222
|
+
annotateHistoricalRisk(finalFindings, _vulnHistory);
|
|
8223
|
+
});
|
|
8224
|
+
try {
|
|
8225
|
+
_executionProofSummary = await annotateExecutionProofs(finalFindings, { fileContents: fc });
|
|
8226
|
+
} catch (e) {
|
|
8227
|
+
_annotatorErrors.push({ phase: 'annotateExecutionProofs', err: String((e && e.message) || e) });
|
|
8228
|
+
}
|
|
8182
8229
|
// Phase-1 next-gen P1.2 (FR-VER-3, FR-VER-6, FR-VER-7): per-finding
|
|
8183
8230
|
// verifier verdict — verified-exploit (live PoC ran), verified-by-llm,
|
|
8184
8231
|
// verified-sanitizer-absence, unverified-by-design, or cannot-verify.
|
|
@@ -8715,7 +8762,7 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null,
|
|
|
8715
8762
|
// Addition #3 — root-cause sweep: from confirmed findings, find sibling instances
|
|
8716
8763
|
// detectors missed, with total-count accounting. Confirmed-only (cheap by default).
|
|
8717
8764
|
let _rootCauseSweep = null; try { _rootCauseSweep = sweepRootCauses(finalFindings, fc); } catch { _rootCauseSweep = null; }
|
|
8718
|
-
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};}
|
|
8765
|
+
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,executionProof:_executionProofSummary,vulnHistory:_vulnHistory,threatModel:_threatModel,sbomDiff:_sbomDiff,complianceReport:_complianceReport,exploitBundles:_exploitBundles,pqcPlan:_pqcPlan,licenseGraph:_licenseGraph,attributions:_attributions,attackTaxonomy:_taxonomySummary};}
|
|
8719
8766
|
|
|
8720
8767
|
// Post-aggregation classification: every source becomes "unsafe"|"safe"; every sink becomes "confirmed"|"safe".
|
|
8721
8768
|
// Orphans (no finding linkage) are bucketed by file-local heuristic so the UI shows binary states only.
|
package/src/ir/parser-js.js
CHANGED
|
@@ -402,6 +402,14 @@ export function parseJsFile(file, code) {
|
|
|
402
402
|
// regardless of extension. JSX stays enabled via preset-react, so .js files
|
|
403
403
|
// containing JSX still parse — which .isTSX/.allExtensions guaranteed before.
|
|
404
404
|
presets: [presetReact, [presetTypescript, { ignoreExtensions: true }]],
|
|
405
|
+
// Decorators are SYNTAX we must accept, never transform — without them the
|
|
406
|
+
// parser rejects the whole file and every finding in it silently disappears.
|
|
407
|
+
// Measured on one real target: 201 JS files unparseable, all decorator-using
|
|
408
|
+
// framework code. 'decorators-legacy' covers the framework and TypeScript
|
|
409
|
+
// parameter forms; 'decoratorAutoAccessors' adds the modern `accessor` field.
|
|
410
|
+
// The modern 'decorators' variant was rejected: it cannot parse TS parameter
|
|
411
|
+
// decorators, so it would trade one blind spot for another.
|
|
412
|
+
parserOpts: { plugins: ['decorators-legacy', 'decoratorAutoAccessors'] },
|
|
405
413
|
plugins: [plugin],
|
|
406
414
|
ast: false, code: false, babelrc: false, configFile: false,
|
|
407
415
|
});
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
// R12 — a hard cost ceiling for the LLM validator tier.
|
|
2
|
+
//
|
|
3
|
+
// A cost *advisor* already exists (`hooks/model-cost-advisor.js`): it biases a
|
|
4
|
+
// quality/cost dial and warns as spend approaches a soft budget. What did not
|
|
5
|
+
// exist is a CAP — something that refuses to spend rather than advising about
|
|
6
|
+
// it. A soft budget you can sail past is not a ceiling, and "it warned you" is
|
|
7
|
+
// no comfort on an invoice.
|
|
8
|
+
//
|
|
9
|
+
// THE DISTINCTION THAT MATTERS: this never degrades quality to fit a budget.
|
|
10
|
+
// It stops. Silently switching to a cheaper model or a shorter prompt to stay
|
|
11
|
+
// under a cap would change what the scan MEANS while reporting the same shape,
|
|
12
|
+
// and a finding validated by a model the operator did not choose is a
|
|
13
|
+
// different claim than the one they asked for. When the cap binds, remaining
|
|
14
|
+
// findings are left explicitly `unvalidated` with a reason naming the cap.
|
|
15
|
+
//
|
|
16
|
+
// FAIL CLOSED ON UNKNOWN PRICING. A ceiling that cannot price a call cannot
|
|
17
|
+
// enforce anything. Rather than spending unmetered and reporting a $0.00
|
|
18
|
+
// ledger, an unpriceable model refuses every call. Operators override with
|
|
19
|
+
// `AGENTIC_SECURITY_LLM_PRICE_USD_PER_MTOK="<in>,<out>"`.
|
|
20
|
+
//
|
|
21
|
+
// PRICES ARE OPERATOR-SUPPLIED FACTS, NOT ENGINE FACTS. The built-in table is a
|
|
22
|
+
// convenience for the shipped preset, and list prices change. It is deliberately
|
|
23
|
+
// small, it is stamped with the date it was last checked, and anything not in it
|
|
24
|
+
// must be priced explicitly. Do not grow this table casually — a stale price
|
|
25
|
+
// here silently mis-enforces every ceiling built on it.
|
|
26
|
+
|
|
27
|
+
// USD per 1,000,000 tokens, {input, output}. Last checked: 2026-08-07.
|
|
28
|
+
const PRICES = Object.freeze({
|
|
29
|
+
'claude-haiku-4-5': { input: 1.00, output: 5.00 },
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export class CapExceeded extends Error {
|
|
33
|
+
constructor(msg) { super(msg); this.name = 'CapExceeded'; }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Parse the configured cap. Returns null when no cap is set (feature off). */
|
|
37
|
+
export function parseCapUsd(env = process.env) {
|
|
38
|
+
const raw = env.AGENTIC_SECURITY_LLM_MAX_USD;
|
|
39
|
+
if (raw == null || raw === '') return null;
|
|
40
|
+
const n = Number(raw);
|
|
41
|
+
// A malformed cap is refused rather than ignored: treating "abc" or a
|
|
42
|
+
// negative as "no cap" turns a typo into unlimited spend.
|
|
43
|
+
if (!Number.isFinite(n) || n < 0) {
|
|
44
|
+
throw new CapExceeded(`AGENTIC_SECURITY_LLM_MAX_USD is not a non-negative number: ${JSON.stringify(raw)}`);
|
|
45
|
+
}
|
|
46
|
+
return n;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Resolve {input, output} USD per 1M tokens for a model, or null if unknown. */
|
|
50
|
+
export function priceFor(model, env = process.env) {
|
|
51
|
+
const override = env.AGENTIC_SECURITY_LLM_PRICE_USD_PER_MTOK;
|
|
52
|
+
if (override) {
|
|
53
|
+
const parts = String(override).split(',').map(s => Number(s.trim()));
|
|
54
|
+
if (parts.length === 2 && parts.every(n => Number.isFinite(n) && n >= 0)) {
|
|
55
|
+
return { input: parts[0], output: parts[1], source: 'override' };
|
|
56
|
+
}
|
|
57
|
+
return null; // malformed override -> unpriceable, so fail closed
|
|
58
|
+
}
|
|
59
|
+
const p = PRICES[model];
|
|
60
|
+
return p ? { ...p, source: 'built-in' } : null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function costOf({ inputTokens = 0, outputTokens = 0 }, price) {
|
|
64
|
+
if (!price) return null;
|
|
65
|
+
return (inputTokens / 1e6) * price.input + (outputTokens / 1e6) * price.output;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* A ledger enforcing a hard ceiling.
|
|
70
|
+
*
|
|
71
|
+
* `capUsd === null` means no ceiling was configured: `canAfford` always allows
|
|
72
|
+
* and nothing is enforced. That is the default, and it is the ONLY state in
|
|
73
|
+
* which unpriceable models are permitted — without a cap there is nothing to
|
|
74
|
+
* enforce, so refusing would break existing behaviour for no benefit.
|
|
75
|
+
*/
|
|
76
|
+
export function createCostLedger({ capUsd = null, model = 'unknown', env = process.env } = {}) {
|
|
77
|
+
const price = priceFor(model, env);
|
|
78
|
+
let spentUsd = 0, calls = 0, refusals = 0;
|
|
79
|
+
// How much of `spentUsd` came from ESTIMATES rather than reported usage.
|
|
80
|
+
// Tracked separately because the two are not the same kind of number: the
|
|
81
|
+
// estimate charges the full permitted output length, which most replies
|
|
82
|
+
// never reach, so a ledger fed only estimates reports an upper bound. That
|
|
83
|
+
// is fine for ENFORCEMENT (it can only stop early, never late) and wrong for
|
|
84
|
+
// REPORTING. Callers get told which they are looking at.
|
|
85
|
+
let estimatedUsd = 0, estimatedCalls = 0;
|
|
86
|
+
|
|
87
|
+
const enforcing = capUsd != null;
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
enforcing,
|
|
91
|
+
capUsd,
|
|
92
|
+
model,
|
|
93
|
+
price,
|
|
94
|
+
spentUsd: () => spentUsd,
|
|
95
|
+
calls: () => calls,
|
|
96
|
+
refusals: () => refusals,
|
|
97
|
+
remainingUsd: () => (enforcing ? Math.max(0, capUsd - spentUsd) : Infinity),
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* May a call costing at most `estimate` tokens proceed?
|
|
101
|
+
* @returns {{ok:boolean, reason?:string}}
|
|
102
|
+
*/
|
|
103
|
+
canAfford({ inputTokens = 0, outputTokens = 0 } = {}) {
|
|
104
|
+
if (!enforcing) return { ok: true };
|
|
105
|
+
if (!price) {
|
|
106
|
+
refusals++;
|
|
107
|
+
return {
|
|
108
|
+
ok: false,
|
|
109
|
+
reason: `no price is known for model '${model}', so a spend ceiling cannot be enforced. `
|
|
110
|
+
+ 'Set AGENTIC_SECURITY_LLM_PRICE_USD_PER_MTOK="<input>,<output>" (USD per 1M tokens) '
|
|
111
|
+
+ 'or remove AGENTIC_SECURITY_LLM_MAX_USD. Refusing rather than spending unmetered.',
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
// Charge the ESTIMATE before the call, not the actual after it. Checking
|
|
115
|
+
// afterwards would let a single call blow through the cap and report the
|
|
116
|
+
// overrun as a fait accompli.
|
|
117
|
+
const projected = spentUsd + (costOf({ inputTokens, outputTokens }, price) || 0);
|
|
118
|
+
if (projected > capUsd) {
|
|
119
|
+
refusals++;
|
|
120
|
+
return {
|
|
121
|
+
ok: false,
|
|
122
|
+
reason: `cost ceiling reached: this call would bring spend to $${projected.toFixed(4)}, `
|
|
123
|
+
+ `over the $${capUsd.toFixed(4)} cap (AGENTIC_SECURITY_LLM_MAX_USD). `
|
|
124
|
+
+ 'Remaining findings are left unvalidated rather than validated by a cheaper substitute.',
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
return { ok: true };
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Record usage after a call.
|
|
132
|
+
* @param {object} usage {inputTokens, outputTokens}
|
|
133
|
+
* @param {object} [opts]
|
|
134
|
+
* @param {boolean} [opts.measured] true when the figures came from the
|
|
135
|
+
* provider's own usage report; false when they are our pre-call
|
|
136
|
+
* estimate. Defaults to false — the conservative reading, so a caller
|
|
137
|
+
* that forgets to say cannot accidentally upgrade an estimate into a
|
|
138
|
+
* measurement.
|
|
139
|
+
*/
|
|
140
|
+
record({ inputTokens = 0, outputTokens = 0 } = {}, { measured = false } = {}) {
|
|
141
|
+
calls++;
|
|
142
|
+
const c = costOf({ inputTokens, outputTokens }, price);
|
|
143
|
+
// Unpriceable usage is not free. With no cap it is simply not tracked;
|
|
144
|
+
// with a cap, `canAfford` already refused, so this branch cannot spend.
|
|
145
|
+
if (c != null) spentUsd += c;
|
|
146
|
+
if (!measured) {
|
|
147
|
+
estimatedCalls++;
|
|
148
|
+
if (c != null) estimatedUsd += c;
|
|
149
|
+
}
|
|
150
|
+
return spentUsd;
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
/** Reportable state. Always carries the cap so a figure cannot be read alone. */
|
|
154
|
+
state() {
|
|
155
|
+
return {
|
|
156
|
+
enforcing,
|
|
157
|
+
capUsd,
|
|
158
|
+
model,
|
|
159
|
+
priceSource: price?.source || null,
|
|
160
|
+
priceable: !!price,
|
|
161
|
+
spentUsd: Number(spentUsd.toFixed(6)),
|
|
162
|
+
remainingUsd: enforcing ? Number(Math.max(0, capUsd - spentUsd).toFixed(6)) : null,
|
|
163
|
+
calls,
|
|
164
|
+
refusals,
|
|
165
|
+
// Disclosure, not decoration. `spentUsd` is an UPPER BOUND to the
|
|
166
|
+
// extent these are non-zero, and a reader has no way to know that
|
|
167
|
+
// without being told.
|
|
168
|
+
estimatedCalls,
|
|
169
|
+
estimatedUsd: Number(estimatedUsd.toFixed(6)),
|
|
170
|
+
fullyMeasured: estimatedCalls === 0,
|
|
171
|
+
};
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** One-line summary; null when no ceiling is configured. */
|
|
177
|
+
export function renderCostCeiling(s) {
|
|
178
|
+
if (!s || !s.enforcing) return null;
|
|
179
|
+
if (!s.priceable) {
|
|
180
|
+
return `LLM cost ceiling: ENFORCED but model '${s.model}' is unpriceable — ${s.refusals} call(s) refused, nothing spent.`;
|
|
181
|
+
}
|
|
182
|
+
// Say "at most" whenever any part of the figure is an estimate. The word is
|
|
183
|
+
// the whole point: without it an upper bound reads as a measurement.
|
|
184
|
+
const qualifier = s.fullyMeasured ? '' : 'at most ';
|
|
185
|
+
const base = `LLM spend ${qualifier}$${s.spentUsd.toFixed(4)} of $${s.capUsd.toFixed(4)} cap across ${s.calls} call(s)`;
|
|
186
|
+
const parts = [base];
|
|
187
|
+
if (!s.fullyMeasured) {
|
|
188
|
+
parts.push(
|
|
189
|
+
`${s.estimatedCalls} of those call(s) reported no token usage, so their cost is ESTIMATED at the `
|
|
190
|
+
+ 'full permitted output length — the true spend is lower',
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
if (s.refusals) {
|
|
194
|
+
parts.push(`${s.refusals} call(s) REFUSED at the ceiling — those findings are unvalidated, not validated`);
|
|
195
|
+
}
|
|
196
|
+
return parts.join('; ') + '.';
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export const _internals = { PRICES };
|