@clear-capabilities/agentic-security-scanner 0.118.2 → 0.119.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 +256 -0
- package/bin/agentic-security.js +78 -1
- package/dist/178.index.js +1 -1
- package/dist/384.index.js +1 -1
- package/dist/415.index.js +90 -0
- package/dist/444.index.js +89 -0
- package/dist/503.index.js +288 -0
- package/dist/637.index.js +15 -1
- package/dist/838.index.js +1 -1
- package/dist/985.index.js +5 -1
- package/dist/agentic-security.mjs +18 -18
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +13 -8
- package/src/dataflow/CLAUDE.md +1 -1
- package/src/dataflow/catalog.js +50 -11
- package/src/dataflow/engine.js +76 -1
- package/src/dataflow/implicit-flow.js +4 -2
- package/src/dataflow/index.js +9 -4
- package/src/dataflow/proof-gate.js +5 -0
- package/src/dataflow/summaries.js +19 -1
- package/src/engine.js +95 -7
- package/src/ir/parser-go.js +8 -0
- package/src/mcp/tools.js +6 -2
- package/src/posture/fix-history.js +24 -0
- package/src/posture/provenance.js +39 -0
- package/src/posture/sca-verdict.js +130 -0
- package/src/posture/secret-history.js +72 -0
- package/src/pr-delta.js +13 -0
- package/src/report/index.js +50 -0
- package/src/sast/agent-untrusted-flow.js +107 -0
- package/src/sast/api-authz.js +71 -0
- package/src/sast/cross-service.js +98 -0
- package/src/sast/event-entrypoint.js +90 -0
- package/src/sast/iac-terraform.js +107 -0
- package/src/sast/rbac-consistency.js +113 -0
- package/src/sast/wrong-context-sanitizer.js +55 -0
- package/src/sca/image-packages.js +74 -0
- package/src/sca/import-reachability.js +271 -0
- package/src/sca/install-script-analysis.js +82 -0
- package/bin/.agentic-security/findings.json +0 -1907
- package/bin/.agentic-security/last-scan.json +0 -1907
- package/bin/.agentic-security/last-scan.json.sig +0 -1
- package/bin/.agentic-security/scan-history.json +0 -166
- package/bin/.agentic-security/streak.json +0 -20
- package/src/.agentic-security/dpia.md +0 -26
- package/src/.agentic-security/findings.json +0 -117732
- package/src/.agentic-security/last-scan.json +0 -117732
- package/src/.agentic-security/last-scan.json.sig +0 -1
- package/src/.agentic-security/pqc-migration-plan.json +0 -80
- package/src/.agentic-security/pqc-migration-plan.md +0 -32
- package/src/.agentic-security/sbom-history/0775d96ba4cf814520d02760cfb3010760c7684f.json +0 -6
- package/src/.agentic-security/sbom-history/29300441d3460832137a8e51a92b74068cad2f15.json +0 -6
- package/src/.agentic-security/sbom-history/3852ad2aa0e50951bed0c930c29d1861aa13d048.json +0 -6
- package/src/.agentic-security/sbom-history/3d96e2daec97e3226c9679bac6e64a89ca10bfe9.json +0 -6
- package/src/.agentic-security/sbom-history/5f0b136622fce09b469a1a6137c04d5b18652ea4.json +0 -6
- package/src/.agentic-security/sbom-history/64e30595d447b62344d0d9200760269df2095332.json +0 -6
- package/src/.agentic-security/sbom-history/71f3241902c9e7437764e3caa046ba8a3bcdd32a.json +0 -6
- package/src/.agentic-security/sbom-history/99c372cdc33ecc9dde92960b4ffadf7775860e61.json +0 -6
- package/src/.agentic-security/sbom-history/9c93b15e8cf3ff2ddb8cdae92371b82e178b463c.json +0 -6
- package/src/.agentic-security/sbom-history/a3544abf1863d71776b61302ca74d9ead2b26a9f.json +0 -6
- package/src/.agentic-security/sbom-history/dca15abdec43c508aa342db86dc5db8d7f01fbbe.json +0 -6
- package/src/.agentic-security/scan-history.json +0 -12946
- package/src/.agentic-security/streak.json +0 -21
- package/src/.agentic-security/threat-model.json +0 -3702
- package/src/.agentic-security/threat-model.md +0 -118
- package/src/dataflow/.agentic-security/dpia.md +0 -26
- package/src/dataflow/.agentic-security/findings.json +0 -6086
- package/src/dataflow/.agentic-security/last-scan.json +0 -6086
- package/src/dataflow/.agentic-security/last-scan.json.sig +0 -1
- package/src/dataflow/.agentic-security/sbom-history/15d7c4d20f9d968ebec29445c9e9b3fc6f74e6a1.json +0 -6
- package/src/dataflow/.agentic-security/sbom-history/3852ad2aa0e50951bed0c930c29d1861aa13d048.json +0 -6
- package/src/dataflow/.agentic-security/sbom-history/73166872888d9ddcbe98645af818ecc7d8917b07.json +0 -6
- package/src/dataflow/.agentic-security/sbom-history/8869fe64035bec0152d92ece961e9369d8637076.json +0 -6
- package/src/dataflow/.agentic-security/sbom-history/9c93b15e8cf3ff2ddb8cdae92371b82e178b463c.json +0 -6
- package/src/dataflow/.agentic-security/sbom-history/b7ac2dada4a9216ccfe8969fc57f988c83f46975.json +0 -6
- package/src/dataflow/.agentic-security/scan-history.json +0 -250
- package/src/dataflow/.agentic-security/streak.json +0 -21
- package/src/dataflow/.agentic-security/threat-model.json +0 -161
- package/src/dataflow/.agentic-security/threat-model.md +0 -43
- package/src/ir/.agentic-security/dpia.md +0 -26
- package/src/ir/.agentic-security/exploit-bundles.json +0 -77
- package/src/ir/.agentic-security/findings.json +0 -4011
- package/src/ir/.agentic-security/last-scan.json +0 -4011
- package/src/ir/.agentic-security/last-scan.json.sig +0 -1
- package/src/ir/.agentic-security/sbom-history/73166872888d9ddcbe98645af818ecc7d8917b07.json +0 -6
- package/src/ir/.agentic-security/sbom-history/b5f468a158f6a145aedd8680841c106b9809f16b.json +0 -6
- package/src/ir/.agentic-security/scan-history.json +0 -193
- package/src/ir/.agentic-security/streak.json +0 -20
- package/src/ir/.agentic-security/threat-model.json +0 -143
- package/src/ir/.agentic-security/threat-model.md +0 -39
- package/src/mcp/.agentic-security/findings.json +0 -8632
- package/src/mcp/.agentic-security/last-scan.json +0 -8632
- package/src/mcp/.agentic-security/last-scan.json.sig +0 -1
- package/src/mcp/.agentic-security/scan-history.json +0 -331
- package/src/mcp/.agentic-security/streak.json +0 -20
- package/src/posture/.agentic-security/dpia.md +0 -26
- package/src/posture/.agentic-security/findings.json +0 -77181
- package/src/posture/.agentic-security/last-scan.json +0 -77181
- package/src/posture/.agentic-security/last-scan.json.sig +0 -1
- package/src/posture/.agentic-security/pqc-migration-plan.json +0 -65
- package/src/posture/.agentic-security/pqc-migration-plan.md +0 -30
- package/src/posture/.agentic-security/sbom-history/0724c133d2943c87555695b5b2adcfcfc3373a2d.json +0 -6
- package/src/posture/.agentic-security/sbom-history/7d45b5e03804aac084b4a2b4dc8c6f10107d2005.json +0 -6
- package/src/posture/.agentic-security/sbom-history/99c372cdc33ecc9dde92960b4ffadf7775860e61.json +0 -6
- package/src/posture/.agentic-security/sbom-history/b7ac2dada4a9216ccfe8969fc57f988c83f46975.json +0 -6
- package/src/posture/.agentic-security/scan-history.json +0 -8904
- package/src/posture/.agentic-security/streak.json +0 -21
- package/src/posture/.agentic-security/threat-model.json +0 -2038
- package/src/posture/.agentic-security/threat-model.md +0 -73
- package/src/posture/compliance-frameworks/.agentic-security/findings.json +0 -80
- package/src/posture/compliance-frameworks/.agentic-security/last-scan.json +0 -80
- package/src/posture/compliance-frameworks/.agentic-security/last-scan.json.sig +0 -1
- package/src/posture/compliance-frameworks/.agentic-security/scan-history.json +0 -90
- package/src/posture/compliance-frameworks/.agentic-security/streak.json +0 -22
- package/src/report/.agentic-security/dpia.md +0 -26
- package/src/report/.agentic-security/sbom-history/7d45b5e03804aac084b4a2b4dc8c6f10107d2005.json +0 -6
- package/src/report/.agentic-security/sbom-history/f94be839aa6a1011f53da016416a561698b8f338.json +0 -6
- package/src/report/.agentic-security/threat-model.json +0 -7
- package/src/report/.agentic-security/threat-model.md +0 -22
- package/src/sast/.agentic-security/dpia.md +0 -26
- package/src/sast/.agentic-security/findings.json +0 -6154
- package/src/sast/.agentic-security/last-scan.json +0 -6154
- package/src/sast/.agentic-security/last-scan.json.sig +0 -1
- package/src/sast/.agentic-security/pqc-migration-plan.json +0 -65
- package/src/sast/.agentic-security/pqc-migration-plan.md +0 -30
- package/src/sast/.agentic-security/sbom-history/043a7b401c68f5a298c83d4c42e94f118ff2a89a.json +0 -6
- package/src/sast/.agentic-security/sbom-history/0775d96ba4cf814520d02760cfb3010760c7684f.json +0 -6
- package/src/sast/.agentic-security/sbom-history/20d77ad7363a82777d506dedbbb1ecfed52f094a.json +0 -6
- package/src/sast/.agentic-security/sbom-history/29300441d3460832137a8e51a92b74068cad2f15.json +0 -6
- package/src/sast/.agentic-security/sbom-history/3852ad2aa0e50951bed0c930c29d1861aa13d048.json +0 -6
- package/src/sast/.agentic-security/sbom-history/3c6b3205f04e26f7654943d465281d00e0fa395e.json +0 -6
- package/src/sast/.agentic-security/sbom-history/3d96e2daec97e3226c9679bac6e64a89ca10bfe9.json +0 -6
- package/src/sast/.agentic-security/sbom-history/5282502e090013219eb006f7e02d8c983bc29c8e.json +0 -6
- package/src/sast/.agentic-security/sbom-history/5f0b136622fce09b469a1a6137c04d5b18652ea4.json +0 -6
- package/src/sast/.agentic-security/sbom-history/64e30595d447b62344d0d9200760269df2095332.json +0 -6
- package/src/sast/.agentic-security/sbom-history/71f3241902c9e7437764e3caa046ba8a3bcdd32a.json +0 -6
- package/src/sast/.agentic-security/sbom-history/73514b485d4770f3c5ee57d87338bccd1c61d8b7.json +0 -6
- package/src/sast/.agentic-security/sbom-history/96fa584fc3134512237d58aac63d6f3778b0cbcb.json +0 -6
- package/src/sast/.agentic-security/sbom-history/99c372cdc33ecc9dde92960b4ffadf7775860e61.json +0 -6
- package/src/sast/.agentic-security/sbom-history/a3544abf1863d71776b61302ca74d9ead2b26a9f.json +0 -6
- package/src/sast/.agentic-security/sbom-history/a4a34ab8b3d758de2adc8c3dc3e42d4cb0db00a3.json +0 -6
- package/src/sast/.agentic-security/sbom-history/ac236ec3602cfc8fb133f389ec3687e409084673.json +0 -6
- package/src/sast/.agentic-security/sbom-history/b57b3e1292f1f9f0de6c5a34a436e330c78008a3.json +0 -6
- package/src/sast/.agentic-security/sbom-history/dca15abdec43c508aa342db86dc5db8d7f01fbbe.json +0 -6
- package/src/sast/.agentic-security/sbom-history/dcbfd4935922dd165093c08a1003c338eed937c5.json +0 -6
- package/src/sast/.agentic-security/sbom-history/fa017f725615b0293814344c8ef7dfeb06775ca0.json +0 -6
- package/src/sast/.agentic-security/scan-history.json +0 -941
- package/src/sast/.agentic-security/streak.json +0 -22
- package/src/sast/.agentic-security/threat-model.json +0 -816
- package/src/sast/.agentic-security/threat-model.md +0 -91
- package/src/sca/.agentic-security/findings.json +0 -7460
- package/src/sca/.agentic-security/last-scan.json +0 -7460
- package/src/sca/.agentic-security/last-scan.json.sig +0 -1
- package/src/sca/.agentic-security/scan-history.json +0 -113
- package/src/sca/.agentic-security/streak.json +0 -21
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
40d6882dc7b5dbd62ac41f10cb01ec4e43fa42081dbcc0427553e0282748fedb 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.119.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",
|
|
@@ -27,7 +27,9 @@
|
|
|
27
27
|
"src/",
|
|
28
28
|
"bin/",
|
|
29
29
|
"dist/",
|
|
30
|
-
"CHANGELOG.md"
|
|
30
|
+
"CHANGELOG.md",
|
|
31
|
+
"!**/.agentic-security",
|
|
32
|
+
"!**/.agentic-security/**"
|
|
31
33
|
],
|
|
32
34
|
"engines": {
|
|
33
35
|
"node": ">=24.0.0"
|
|
@@ -55,16 +57,19 @@
|
|
|
55
57
|
},
|
|
56
58
|
"scripts": {
|
|
57
59
|
"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');\"",
|
|
58
|
-
"prepublishOnly": "npm run build && node
|
|
59
|
-
"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 && AGENTIC_SECURITY_CPP_DATAFLOW=1 node --test test/cpp-dataflow.test.js",
|
|
60
|
+
"prepublishOnly": "npm run build && node ../scripts/sync-scanner-changelog.mjs",
|
|
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",
|
|
60
62
|
"test:smoke": "node --test test/smoke.test.js",
|
|
61
|
-
"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/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",
|
|
62
|
-
"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/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/sca-route-reachable.test.js test/sca-policy.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/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/coverage-report.test.js test/corpus-status.test.js",
|
|
63
|
-
"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/kcfa-context.test.js test/callgraph-resolve.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/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",
|
|
64
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",
|
|
65
|
-
"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",
|
|
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",
|
|
66
68
|
"test:bench-modules": "node --test test/phase4-harness.test.js test/pipeline.test.js",
|
|
67
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 && node ../scripts/lint-command-descriptions.mjs",
|
|
70
|
+
"test:eval": "node --test test/independent-eval.test.js",
|
|
71
|
+
"eval:independent": "node ../bench/independent-eval/runner.mjs",
|
|
72
|
+
"eval:independent:gate": "node ../bench/independent-eval/runner.mjs --gate default",
|
|
68
73
|
"smoke": "node bin/agentic-security.js scan test/fixtures/vulnerable-js",
|
|
69
74
|
"prebench": "npm run build",
|
|
70
75
|
"bench": "node test/benchmark/bench.js",
|
package/src/dataflow/CLAUDE.md
CHANGED
|
@@ -28,7 +28,7 @@ Layer-2 taint engine. Walks the Layer-1 IR (`../ir/`) with field-sensitive forwa
|
|
|
28
28
|
|
|
29
29
|
## Entry points
|
|
30
30
|
|
|
31
|
-
- `runTaintEngine(perFileIR, callGraph, opts)` — the public entry.
|
|
31
|
+
- `runTaintEngine(perFileIR, callGraph, opts)` — the public entry. Runs from `engine.js` when `AGENTIC_SECURITY_DEEP=1`. **R1 (PRD §5):** the CLI entry (`bin/agentic-security.js#cmdScan`) sets that env var by default for local/interactive scans (not in CI, and not when `--no-deep`/`AGENTIC_SECURITY_DEEP=0`), so the default `/scan --all` runs deep. In-process callers (tests, the cve-replay corpus) invoke `runScan()` directly without the CLI default and therefore stay deep-off unless they set the env var themselves (e.g. `test/deep-taint.test.js`).
|
|
32
32
|
- `applyPathFeasibility` — constant-fold pass that runs before the worklist.
|
|
33
33
|
- `annotateBackwardSlices` — backward-slice annotation for already-emitted findings.
|
|
34
34
|
- `annotateProvenClean(findings, perFileIR)` (`proven-clean.js`) — proves a SQL sink is reached only through a parameterizer; sets `provenClean`. Wired by default in `runDeepAnalysis` (opt out: `AGENTIC_SECURITY_NO_PROOF_GATE=1`).
|
package/src/dataflow/catalog.js
CHANGED
|
@@ -166,6 +166,21 @@ export const CATALOG = [
|
|
|
166
166
|
{ kind: 'source', id: 'go-buffalo-request',language:'go', framework:'buffalo',match: { type: 'member', object: 'c', prop: 'Request' }, label: 'c.Request (buffalo)' },
|
|
167
167
|
{ kind: 'source', id: 'go-gorilla-vars', language: 'go', framework: 'gorilla',match: { type: 'call', callee: 'Vars' }, label: 'mux.Vars (gorilla)' },
|
|
168
168
|
|
|
169
|
+
// ─── SINKS (Go — database/sql) — R3 (PRD §5) ──────────────────────────────
|
|
170
|
+
// callee 'Query' is also a net/http SOURCE (r.URL.Query). The engine
|
|
171
|
+
// disambiguates by position — source at the assignment RHS, sink at a call
|
|
172
|
+
// with a tainted query argument — so coexistence is benign (a source call
|
|
173
|
+
// like r.URL.Query() has no tainted arg, so it never fires the sink).
|
|
174
|
+
{ kind: 'sink', id: 'go-sql-query', language: 'go', framework: 'database/sql', match: { type: 'call', callee: 'Query' }, argIndex: 0,
|
|
175
|
+
vuln: { name: 'SQL Injection (db.Query — Go)', severity: 'critical', cwe: 'CWE-89',
|
|
176
|
+
remediation: 'Use parameterized queries: db.Query("SELECT … WHERE id = $1", id). Never concatenate untrusted input into the SQL string.' } },
|
|
177
|
+
{ kind: 'sink', id: 'go-sql-queryrow', language: 'go', framework: 'database/sql', match: { type: 'call', callee: 'QueryRow' }, argIndex: 0,
|
|
178
|
+
vuln: { name: 'SQL Injection (db.QueryRow — Go)', severity: 'critical', cwe: 'CWE-89',
|
|
179
|
+
remediation: 'Use placeholders ($1 / ?) and pass args separately.' } },
|
|
180
|
+
{ kind: 'sink', id: 'go-sql-exec', language: 'go', framework: 'database/sql', match: { type: 'call', callee: 'Exec' }, argIndex: 0,
|
|
181
|
+
vuln: { name: 'SQL Injection (db.Exec — Go)', severity: 'critical', cwe: 'CWE-89',
|
|
182
|
+
remediation: 'Use parameterized statements: db.Exec("UPDATE t SET x=$1 WHERE id=$2", x, id).' } },
|
|
183
|
+
|
|
169
184
|
// ─── SOURCES (Ruby — Rails / Sinatra) ─────────────────────────────────────
|
|
170
185
|
{ kind: 'source', id: 'rb-rails-params', language: 'rb', framework: 'rails', match: { type: 'global', name: 'params' }, label: 'params (Rails)' },
|
|
171
186
|
{ kind: 'source', id: 'rb-rails-cookies', language: 'rb', framework: 'rails', match: { type: 'global', name: 'cookies' }, label: 'cookies (Rails)' },
|
|
@@ -747,22 +762,46 @@ function filterByProvenance(entries) {
|
|
|
747
762
|
return list;
|
|
748
763
|
}
|
|
749
764
|
|
|
750
|
-
export function matchSource(
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
if (
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
765
|
+
export function matchSource(expr) {
|
|
766
|
+
if (!expr) return null;
|
|
767
|
+
// Member sources (req.query): the original path — unchanged.
|
|
768
|
+
if (expr.kind === 'member' && expr.object?.kind === 'ident') {
|
|
769
|
+
const raw = MEMBER_INDEX.get(`${expr.object.name}.${expr.prop}`);
|
|
770
|
+
if (raw) {
|
|
771
|
+
const hits = filterByProvenance(raw);
|
|
772
|
+
const s = hits.find(h => h.kind === 'source');
|
|
773
|
+
if (s) return s;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
// R3 (PRD §5): CALL sources (r.FormValue(), r.URL.Query(), c.QueryParam()).
|
|
777
|
+
// matchSource previously handled only member reads, so Go's call-shaped
|
|
778
|
+
// sources were never recognized at the assignment RHS. Match by callee last
|
|
779
|
+
// segment (Go gives a dotted string; JS/Py a member/ident expr).
|
|
780
|
+
if (expr.kind === 'call') {
|
|
781
|
+
let cn = null;
|
|
782
|
+
if (typeof expr.callee === 'string') cn = expr.callee.includes('.') ? expr.callee.slice(expr.callee.lastIndexOf('.') + 1) : expr.callee;
|
|
783
|
+
else if (expr.callee && expr.callee.kind === 'member') cn = expr.callee.prop;
|
|
784
|
+
else if (expr.callee && expr.callee.kind === 'ident') cn = expr.callee.name;
|
|
785
|
+
if (cn) {
|
|
786
|
+
const raw = CALLEE_INDEX.get(cn);
|
|
787
|
+
if (raw) {
|
|
788
|
+
const hits = filterByProvenance(raw);
|
|
789
|
+
const s = hits.find(h => h.kind === 'source');
|
|
790
|
+
if (s) return s;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
return null;
|
|
760
795
|
}
|
|
761
796
|
|
|
762
797
|
export function matchSinkOrSanitizer(calleeExpr) {
|
|
763
798
|
if (!calleeExpr) return null;
|
|
764
799
|
let calleeName = null;
|
|
765
|
-
|
|
800
|
+
// R3 (PRD §5): the Go IR (and other string-callee parsers) represent a call
|
|
801
|
+
// target as a dotted STRING ("db.Query") rather than a member expr. Match on
|
|
802
|
+
// the last segment so those languages' sinks/sanitizers are recognized too.
|
|
803
|
+
if (typeof calleeExpr === 'string') calleeName = calleeExpr.includes('.') ? calleeExpr.slice(calleeExpr.lastIndexOf('.') + 1) : calleeExpr;
|
|
804
|
+
else if (calleeExpr.kind === 'ident') calleeName = calleeExpr.name;
|
|
766
805
|
else if (calleeExpr.kind === 'member') calleeName = calleeExpr.prop;
|
|
767
806
|
if (!calleeName) return null;
|
|
768
807
|
const raw = CALLEE_INDEX.get(calleeName);
|
package/src/dataflow/engine.js
CHANGED
|
@@ -39,6 +39,7 @@ import { aliasesForVar } from './points-to.js';
|
|
|
39
39
|
import { higherOrderTaintFlow } from './higher-order.js';
|
|
40
40
|
import { SummaryCache, entryStateFromCall } from './summaries.js';
|
|
41
41
|
import { lookupBuiltinSummary } from './builtin-summaries.js';
|
|
42
|
+
import { isImplicitFlowEnabled, buildImplicitContext, implicitAssignTarget, markImplicitTaint, createImplicitFinding } from './implicit-flow.js';
|
|
42
43
|
|
|
43
44
|
// v0.70 #2 — addPath that also taints every alias of the variable.
|
|
44
45
|
// When `target` is a dotted path like "a.x" and the root `a` has aliases
|
|
@@ -157,6 +158,13 @@ function exprIsSource(expr) {
|
|
|
157
158
|
const hit = matchSource(expr);
|
|
158
159
|
if (hit) return hit;
|
|
159
160
|
}
|
|
161
|
+
// R3 (PRD §5): call-shaped sources (r.FormValue(), r.URL.Query(), c.Query()).
|
|
162
|
+
// Previously only member reads were recognized, so Go's call-style sources
|
|
163
|
+
// never tainted the assignment target. matchSource now resolves call sources.
|
|
164
|
+
if (expr.kind === 'call') {
|
|
165
|
+
const hit = matchSource(expr);
|
|
166
|
+
if (hit) return hit;
|
|
167
|
+
}
|
|
160
168
|
if (expr.kind === 'member' && expr.object) {
|
|
161
169
|
return exprIsSource(expr.object);
|
|
162
170
|
}
|
|
@@ -383,6 +391,20 @@ function step(node, stateIn, callContext) {
|
|
|
383
391
|
});
|
|
384
392
|
}
|
|
385
393
|
}
|
|
394
|
+
// R4 (PRD §5): array-element taint. A mutating array method (push/unshift/
|
|
395
|
+
// splice/fill/copyWithin) called with a tainted argument taints the
|
|
396
|
+
// receiver array; an index read (a[0] → access path "a.0") is then covered
|
|
397
|
+
// by the receiver prefix. Object-property taint already flows via the
|
|
398
|
+
// access-path lattice — this closes the array case.
|
|
399
|
+
if (node.callee && node.callee.kind === 'member' && typeof node.callee.prop === 'string'
|
|
400
|
+
&& /^(?:push|unshift|splice|fill|copyWithin)$/.test(node.callee.prop)
|
|
401
|
+
&& Array.isArray(argTaints) && argTaints.some(Boolean)) {
|
|
402
|
+
// Mutate the state Set IN PLACE (the binding is const; the call case
|
|
403
|
+
// returns this same Set ref). Avoids touching the unrelated mutated-param
|
|
404
|
+
// paths in this case, keeping the blast radius to array-element taint only.
|
|
405
|
+
const _arrRecv = accessPathOf(node.callee.object);
|
|
406
|
+
if (_arrRecv) state.add(_arrRecv);
|
|
407
|
+
}
|
|
386
408
|
if (cat) {
|
|
387
409
|
for (const e of cat) {
|
|
388
410
|
if (e.kind === 'sink' && (
|
|
@@ -493,6 +515,11 @@ function step(node, stateIn, callContext) {
|
|
|
493
515
|
// control flow) can otherwise hold past the global timeout.
|
|
494
516
|
function analyzeFunction(fn, entryState, callContext) {
|
|
495
517
|
const nodes = fn.cfg.nodes;
|
|
518
|
+
// R2 (PRD §5): set the call-string caller context to THIS function while its
|
|
519
|
+
// worklist computes callee summaries (so a callee is keyed by its caller).
|
|
520
|
+
// No-op for the key unless AGENTIC_SECURITY_KCFA_CALLSTRING=1. Restored below.
|
|
521
|
+
const _prevCallerCtx = (callContext && callContext._summaryCache && callContext._summaryCache.setCallerContext)
|
|
522
|
+
? callContext._summaryCache.setCallerContext(fn.qid) : undefined;
|
|
496
523
|
const work = [];
|
|
497
524
|
const inStates = new Map();
|
|
498
525
|
const outStates = new Map();
|
|
@@ -535,6 +562,47 @@ function analyzeFunction(fn, entryState, callContext) {
|
|
|
535
562
|
}
|
|
536
563
|
}
|
|
537
564
|
|
|
565
|
+
// R4 (PRD §5) — implicit / control-dependence flow. OPT-IN (default OFF, see
|
|
566
|
+
// isImplicitFlowEnabled). Post-pass over the converged CFG: a sink reached
|
|
567
|
+
// INSIDE a tainted-condition branch can leak information even when its
|
|
568
|
+
// argument is constant or only implicitly tainted (a var assigned in that
|
|
569
|
+
// branch). Findings carry implicit:true + capped confidence.
|
|
570
|
+
if (isImplicitFlowEnabled() && fn.cfg) {
|
|
571
|
+
try {
|
|
572
|
+
const union = new Set();
|
|
573
|
+
for (const s of inStates.values()) for (const p of s) union.add(p);
|
|
574
|
+
const ictx = buildImplicitContext(fn.cfg, (expr) => exprTaint(expr, union));
|
|
575
|
+
// Mark vars assigned inside a tainted branch as implicit-tainted.
|
|
576
|
+
let implicitState = new Set();
|
|
577
|
+
for (const [nid, ctx] of ictx) {
|
|
578
|
+
const t = implicitAssignTarget(nodes[nid], ctx);
|
|
579
|
+
if (t) implicitState = markImplicitTaint(implicitState, t);
|
|
580
|
+
}
|
|
581
|
+
// A sink in a tainted branch whose arg is implicit-tainted (or constant)
|
|
582
|
+
// — and NOT already explicitly tainted (the normal pass covers that).
|
|
583
|
+
for (const [nid, ctx] of ictx) {
|
|
584
|
+
const node = nodes[nid];
|
|
585
|
+
if (!node || node.kind !== 'call') continue;
|
|
586
|
+
const cat = matchSinkOrSanitizer(node.callee);
|
|
587
|
+
const sink = cat && cat.find((e) => e.kind === 'sink');
|
|
588
|
+
if (!sink) continue;
|
|
589
|
+
const inS = inStates.get(nid) || new Set();
|
|
590
|
+
if ((node.args || []).some((a) => exprTaint(a, inS))) continue;
|
|
591
|
+
const argRefsImplicit = (node.args || []).some((a) => {
|
|
592
|
+
const ap = accessPathOf(a); return ap && isCoveredBy(implicitState, `implicit:${ap}`);
|
|
593
|
+
});
|
|
594
|
+
const allConst = (node.args || []).length > 0 && (node.args || []).every((a) => a && a.kind === 'literal');
|
|
595
|
+
if (argRefsImplicit || allConst) {
|
|
596
|
+
callContext._findings.push({
|
|
597
|
+
...createImplicitFinding(node, ctx.conditionLabel),
|
|
598
|
+
_funcQid: fn.qid, sinkId: sink.id,
|
|
599
|
+
cwe: (sink.vuln && sink.vuln.cwe) || 'CWE-200',
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
} catch { /* implicit flow is best-effort + opt-in */ }
|
|
604
|
+
}
|
|
605
|
+
|
|
538
606
|
const exit = outStates.get(fn.cfg.exit) || new Set();
|
|
539
607
|
// v0.66 — record which params are tainted at function exit so the
|
|
540
608
|
// caller's applyAtCallSite can propagate that mutated taint back. We
|
|
@@ -546,6 +614,10 @@ function analyzeFunction(fn, entryState, callContext) {
|
|
|
546
614
|
if (isCoveredBy(exit, p)) callContext._mutatedParamsOut.add(p);
|
|
547
615
|
}
|
|
548
616
|
}
|
|
617
|
+
// R2: restore the caller context for the enclosing function's analysis.
|
|
618
|
+
if (_prevCallerCtx !== undefined && callContext && callContext._summaryCache && callContext._summaryCache.setCallerContext) {
|
|
619
|
+
callContext._summaryCache.setCallerContext(_prevCallerCtx);
|
|
620
|
+
}
|
|
549
621
|
return exit;
|
|
550
622
|
}
|
|
551
623
|
|
|
@@ -749,7 +821,10 @@ export function runTaintEngine(perFileIR, callGraph, opts = {}) {
|
|
|
749
821
|
cwe: f.cwe,
|
|
750
822
|
remediation: f.remediation,
|
|
751
823
|
parser: 'IR-TAINT',
|
|
752
|
-
|
|
824
|
+
// R4 implicit-flow: preserve the implicit flag + its capped confidence
|
|
825
|
+
// (a control-dependence finding, not an explicit data-flow one).
|
|
826
|
+
confidence: (f.implicit && typeof f.confidence === 'number') ? f.confidence : 0.75,
|
|
827
|
+
...(f.implicit === true ? { implicit: true } : {}),
|
|
753
828
|
source: f.trace && f.trace.length ? {
|
|
754
829
|
file: fn.file,
|
|
755
830
|
line: f.trace[0].line,
|
|
@@ -34,8 +34,10 @@
|
|
|
34
34
|
import { addPath } from './access-paths.js';
|
|
35
35
|
|
|
36
36
|
export function isImplicitFlowEnabled() {
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
// OPT-IN (default OFF): implicit/control-dependence flow is famously noisy, so
|
|
38
|
+
// it must be explicitly requested. Findings carry implicit:true + capped
|
|
39
|
+
// confidence so the standard pipeline ranks them below explicit flows.
|
|
40
|
+
return process.env.AGENTIC_SECURITY_IMPLICIT_FLOW === '1';
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
/**
|
package/src/dataflow/index.js
CHANGED
|
@@ -48,12 +48,17 @@ export function runDeepAnalysis(perFileIR, callGraph, opts = {}) {
|
|
|
48
48
|
scanner: opts.scannerVersion || 'unknown',
|
|
49
49
|
rules: opts.rulesDigest || `catalog:${_catalogSize()}`,
|
|
50
50
|
};
|
|
51
|
+
// Compute current file hashes UNCONDITIONALLY — the first scan (and any
|
|
52
|
+
// scan after a version/rule invalidation) must persist a baseline so the
|
|
53
|
+
// NEXT scan has something to reuse. This previously lived inside the
|
|
54
|
+
// valid-state branch, so a cold cache never committed and incremental was
|
|
55
|
+
// a permanent no-op (the commit guard at the bottom keys on currentFileHashes).
|
|
56
|
+
currentFileHashes = {};
|
|
57
|
+
for (const [fp, content] of Object.entries(opts.fileContents)) {
|
|
58
|
+
currentFileHashes[fp] = hashFileContent(content);
|
|
59
|
+
}
|
|
51
60
|
const valid = validateIncrementalState(priorState, currentVersion);
|
|
52
61
|
if (valid.valid) {
|
|
53
|
-
currentFileHashes = {};
|
|
54
|
-
for (const [fp, content] of Object.entries(opts.fileContents)) {
|
|
55
|
-
currentFileHashes[fp] = hashFileContent(content);
|
|
56
|
-
}
|
|
57
62
|
const diff = diffFileHashes(priorState.files || {}, currentFileHashes);
|
|
58
63
|
const changedQids = new Set();
|
|
59
64
|
// Map a changed file to the qids it owns. perFileIR exposes file→fns.
|
|
@@ -76,6 +76,11 @@ export function annotateProofGate(findings, opts = {}) {
|
|
|
76
76
|
|
|
77
77
|
// proven-clean | proven-infeasible → recall-preserving demotion.
|
|
78
78
|
if (verdict === 'proven-clean') stats.provenClean++; else stats.provenInfeasible++;
|
|
79
|
+
// R13 (PRD §5): make "provably safe" a first-class, queryable verdict — not
|
|
80
|
+
// just a quiet confidence demotion. "We proved this can't fire" is a
|
|
81
|
+
// stronger, rarer claim than any confidence score, and lets the report
|
|
82
|
+
// optionally hide discharged findings (--hide-proven-safe).
|
|
83
|
+
f.provablySafe = true;
|
|
79
84
|
|
|
80
85
|
if (typeof f.confidence === 'number') {
|
|
81
86
|
f._confidenceBeforeProofGate = f.confidence;
|
|
@@ -64,12 +64,30 @@ export class SummaryCache {
|
|
|
64
64
|
const envCap = Number(process.env.AGENTIC_SECURITY_KCFA_MAX_CONTEXTS);
|
|
65
65
|
this._maxContextsPerFn = Number.isFinite(envCap) && envCap >= 0 ? envCap : 16;
|
|
66
66
|
this._contextCapHits = 0;
|
|
67
|
+
// R2 (PRD §5): bounded k=1 call-string sensitivity. OPT-IN (default OFF).
|
|
68
|
+
// When on, the cache key is extended with the immediate caller's qid, so two
|
|
69
|
+
// call paths reaching a helper with the SAME tainted-arg shape get DISTINCT
|
|
70
|
+
// summaries instead of sharing one (removes the over-merge that the pure
|
|
71
|
+
// value-context cache produces). Off by default → keys are byte-identical to
|
|
72
|
+
// before, so no behavior change for the shipped engine.
|
|
73
|
+
this._callString = process.env.AGENTIC_SECURITY_KCFA_CALLSTRING === '1';
|
|
74
|
+
this._callerCtx = null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Set the immediate-caller qid for call-string keying; returns the previous
|
|
78
|
+
// value so the engine can save/restore it around nested callee analysis.
|
|
79
|
+
setCallerContext(callerQid) {
|
|
80
|
+
const prev = this._callerCtx;
|
|
81
|
+
this._callerCtx = callerQid || null;
|
|
82
|
+
return prev;
|
|
67
83
|
}
|
|
68
84
|
|
|
69
85
|
_key(qid, taintedParams, receiverType) {
|
|
70
86
|
// P1.2: when a receiver type is provided, extend the cache key with
|
|
71
87
|
// its hash. Backward-compatible: no receiverType → same key as before.
|
|
72
|
-
|
|
88
|
+
let base = `${qid}::${_hashState(taintedParams)}`;
|
|
89
|
+
// R2: append the caller qid when call-string sensitivity is enabled.
|
|
90
|
+
if (this._callString && this._callerCtx) base = `${base}@${this._callerCtx}`;
|
|
73
91
|
if (!receiverType) return base;
|
|
74
92
|
return `${base}::${hashReceiverType(receiverType)}`;
|
|
75
93
|
}
|
package/src/engine.js
CHANGED
|
@@ -35,6 +35,10 @@ import { scanMobileManifest } from './sast/mobile-manifest.js';
|
|
|
35
35
|
import { scanQuarkusHardening } from './sast/quarkus-hardening.js';
|
|
36
36
|
import { scanFastapiHardening } from './sast/fastapi-hardening.js';
|
|
37
37
|
import { scanAuthZ } from './sast/authz.js';
|
|
38
|
+
import { scanApiBrokenAuthz } from './sast/api-authz.js';
|
|
39
|
+
import { scanTerraform } from './sast/iac-terraform.js';
|
|
40
|
+
import { scanCrossService } from './sast/cross-service.js';
|
|
41
|
+
import { scanRbacConsistency } from './sast/rbac-consistency.js';
|
|
38
42
|
import { scanModelLoad } from './sast/model-load.js';
|
|
39
43
|
import { scanPromptTemplate } from './sast/prompt-template.js';
|
|
40
44
|
import { scanXXE } from './sast/xxe.js';
|
|
@@ -81,7 +85,7 @@ import { scanLDAPInjection } from './sast/ldap-injection.js';
|
|
|
81
85
|
import { scanXPathInjection } from './sast/xpath-injection.js';
|
|
82
86
|
import { scanSSTI } from './sast/ssti.js';
|
|
83
87
|
import { scanOpenRedirect } from './sast/open-redirect.js';
|
|
84
|
-
import { scanWrongContextSanitizer } from './sast/wrong-context-sanitizer.js';
|
|
88
|
+
import { scanWrongContextSanitizer, scanSanitizerContextMismatch } from './sast/wrong-context-sanitizer.js';
|
|
85
89
|
import { scanFrontendHygiene } from './sast/frontend-hygiene.js';
|
|
86
90
|
import { scanCsvInjection } from './sast/csv-injection.js';
|
|
87
91
|
import { scanStoredTaint } from './sast/stored-taint.js';
|
|
@@ -99,6 +103,9 @@ import { scanResponseSplitting } from './sast/response-splitting.js';
|
|
|
99
103
|
import { scanStoredPromptInjection, scanStoredPromptInjectionCrossFile } from './sast/llm-stored-prompt.js';
|
|
100
104
|
import { scanRAGPoisoning } from './sast/rag-poisoning.js';
|
|
101
105
|
import { scanAgentToolEscalation } from './sast/agent-tool-escalation.js';
|
|
106
|
+
import { scanAgentUntrustedFlow } from './sast/agent-untrusted-flow.js';
|
|
107
|
+
import { scanInstallScripts } from './sca/install-script-analysis.js';
|
|
108
|
+
import { scanEventEntrypoints } from './sast/event-entrypoint.js';
|
|
102
109
|
import { scanDbTaint, scanDbTaintCrossFile } from './sast/db-taint.js';
|
|
103
110
|
import { scanSSRFCloudMetadata } from './sast/ssrf-cloud-metadata.js';
|
|
104
111
|
import { scanMutationXSS } from './sast/mutation-xss.js';
|
|
@@ -143,6 +150,8 @@ import { annotateFeatureFlagGating } from './posture/feature-flags.js';
|
|
|
143
150
|
import { annotatePersonaScores } from './posture/persona-prioritization.js';
|
|
144
151
|
import { annotateMitigationComposite } from './posture/mitigation-composite.js';
|
|
145
152
|
import { annotateCompositeRisk } from './posture/composite-risk.js';
|
|
153
|
+
import { annotateScaVerdicts } from './posture/sca-verdict.js';
|
|
154
|
+
import { annotateFindingProvenance } from './posture/provenance.js';
|
|
146
155
|
|
|
147
156
|
// ── Integration block: world-class scaffolded modules ──────────────────────
|
|
148
157
|
// Each module is opt-in via its own env var so partial adoption is safe.
|
|
@@ -6691,6 +6700,42 @@ function _parseMavenDependencyTree(text, filePath){
|
|
|
6691
6700
|
return out;
|
|
6692
6701
|
}
|
|
6693
6702
|
|
|
6703
|
+
// R10 (PRD §5): Gradle transitive graph. `gradle dependencies` (or
|
|
6704
|
+
// `gradle :app:dependencies`) prints the FULL resolved tree; build.gradle alone
|
|
6705
|
+
// only declares direct deps. Canonical committed path: `gradle-dependencies.txt`.
|
|
6706
|
+
// Format (tree-drawing prefixes we strip), version may be conflict-resolved:
|
|
6707
|
+
// +--- group:artifact:1.0
|
|
6708
|
+
// | \--- group:artifact:1.0 -> 1.2 (resolved to 1.2)
|
|
6709
|
+
// Config headers ("compileClasspath - …"), project deps, and (*)/(c) markers skipped.
|
|
6710
|
+
function _parseGradleDependencies(text, filePath) {
|
|
6711
|
+
const out = [];
|
|
6712
|
+
const seen = new Set();
|
|
6713
|
+
for (const rawLine of String(text).split('\n')) {
|
|
6714
|
+
if (!rawLine.trim()) continue;
|
|
6715
|
+
const indent = rawLine.length - rawLine.replace(/^[\s|+\\-]+/, '').length;
|
|
6716
|
+
const stripped = rawLine.replace(/^[\s|+\\-]+/, '').trim();
|
|
6717
|
+
if (!stripped || stripped.startsWith('project ') || /\bproject\s+[':]/.test(stripped)) continue;
|
|
6718
|
+
// group:artifact:version, with optional `-> resolved` and trailing (*)/(c)/(n).
|
|
6719
|
+
const m = stripped.match(/^([A-Za-z0-9._-]+):([A-Za-z0-9._-]+):([^\s(]+?)(?:\s*->\s*([^\s(]+))?\s*(?:\([*cn]\))?\s*$/);
|
|
6720
|
+
if (!m) continue;
|
|
6721
|
+
const group = m[1], artifact = m[2];
|
|
6722
|
+
let ver = (m[4] || m[3] || '').replace(/[{}]/g, '');
|
|
6723
|
+
ver = ver.replace(/^(?:strictly|require|prefer)\s+/, '');
|
|
6724
|
+
if (!group || !artifact || !ver) continue;
|
|
6725
|
+
const key = `${group}:${artifact}:${ver}`;
|
|
6726
|
+
if (seen.has(key)) continue;
|
|
6727
|
+
seen.add(key);
|
|
6728
|
+
out.push({
|
|
6729
|
+
name: `${group}:${artifact}`, version: ver, group, scope: 'required',
|
|
6730
|
+
purl: _makePurl('maven', artifact, ver, group), ecosystem: 'maven', filePath,
|
|
6731
|
+
isUnpinned: false,
|
|
6732
|
+
isTransitive: indent > 6,
|
|
6733
|
+
pomSource: 'gradle-dependencies',
|
|
6734
|
+
});
|
|
6735
|
+
}
|
|
6736
|
+
return out;
|
|
6737
|
+
}
|
|
6738
|
+
|
|
6694
6739
|
function _parseBuildGradle(text,filePath){
|
|
6695
6740
|
const out=[];
|
|
6696
6741
|
const pat=/(?:implementation|api|compile|runtimeOnly|testImplementation|testCompile|compileOnly|classpath)\s*(?:\(|['"])\s*['"]?([a-zA-Z0-9._\-]+):([a-zA-Z0-9._\-]+):([^'")\s,]+)/g;
|
|
@@ -6969,6 +7014,8 @@ function parseManifests(allFileContents){
|
|
|
6969
7014
|
// `target/dependency-tree.txt` (per `mvn dependency:tree -DoutputFile=...`)
|
|
6970
7015
|
// but users sometimes commit it as `dependency-tree.txt` at repo root.
|
|
6971
7016
|
'dependency-tree.txt':_parseMavenDependencyTree,
|
|
7017
|
+
// R10: Gradle resolved transitive graph — `gradle dependencies > gradle-dependencies.txt`.
|
|
7018
|
+
'gradle-dependencies.txt':_parseGradleDependencies,
|
|
6972
7019
|
};
|
|
6973
7020
|
const out=[],seen=new Set();
|
|
6974
7021
|
for(const[fp,content]of Object.entries(allFileContents)){
|
|
@@ -7346,12 +7393,13 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null},
|
|
|
7346
7393
|
const _perFileTimeoutMs = parseInt(process.env.AGENTIC_SECURITY_PER_FILE_TIMEOUT_MS || '10000', 10);
|
|
7347
7394
|
const _fileTimings = [];
|
|
7348
7395
|
let _filesSkipped = 0, _filesTimedOut = 0, _filesDenseSkipped = 0;
|
|
7349
|
-
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));
|
|
7396
|
+
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));
|
|
7350
7397
|
aF.push(...scanLLM(p,c));
|
|
7351
7398
|
aF.push(...scanLLMOwasp(p,c));
|
|
7352
7399
|
aLogic.push(...scanBusinessLogic(p,c));
|
|
7353
7400
|
aF.push(...scanPipeline(p,c));
|
|
7354
7401
|
aF.push(...scanContainer(p,c));
|
|
7402
|
+
aF.push(...scanInstallScripts(p,c));
|
|
7355
7403
|
aF.push(...scanMCP(p,c));
|
|
7356
7404
|
aF.push(...scanClaudeSettings(p,c));
|
|
7357
7405
|
aF.push(...scanClaudeMdPromptInjection(p,c));
|
|
@@ -7409,6 +7457,7 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null},
|
|
|
7409
7457
|
aF.push(...scanSSTI(p,c));
|
|
7410
7458
|
aF.push(...scanOpenRedirect(p,c));
|
|
7411
7459
|
aF.push(...scanWrongContextSanitizer(p,c));
|
|
7460
|
+
aF.push(...scanSanitizerContextMismatch(p,c));
|
|
7412
7461
|
aF.push(...scanFrontendHygiene(p,c));
|
|
7413
7462
|
aF.push(...scanCsvInjection(p,c));
|
|
7414
7463
|
aF.push(...scanStoredTaint(p,c));
|
|
@@ -7424,6 +7473,8 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null},
|
|
|
7424
7473
|
aF.push(...scanStoredPromptInjection(p,c));
|
|
7425
7474
|
aF.push(...scanRAGPoisoning(p,c));
|
|
7426
7475
|
aF.push(...scanAgentToolEscalation(p,c));
|
|
7476
|
+
aF.push(...scanAgentUntrustedFlow(p,c));
|
|
7477
|
+
aF.push(...scanEventEntrypoints(p,c));
|
|
7427
7478
|
aF.push(...scanDbTaint(p,c));
|
|
7428
7479
|
aF.push(...scanSSRFCloudMetadata(p,c));
|
|
7429
7480
|
aF.push(...scanMutationXSS(p,c));
|
|
@@ -7585,6 +7636,12 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null},
|
|
|
7585
7636
|
setProgress({current:i,total:files.length,file:"Stored taint...",phase:"Linking"});const storedRegistry=buildStoredTaintRegistry(fc);const stf=crossStoredTaint(fc,storedRegistry);aF.push(...stf);
|
|
7586
7637
|
setProgress({current:i,total:files.length,file:"Session taint...",phase:"Linking"});const sess=crossSessionTaint(fc);aF.push(...sess);
|
|
7587
7638
|
setProgress({current:i,total:files.length,file:"Call graph...",phase:"Linking"});const callGraph=buildCallGraph(fc);
|
|
7639
|
+
// R19 (PRD §5): cross-route BOLA/BFLA over the aggregated route inventory.
|
|
7640
|
+
try{aF.push(...scanApiBrokenAuthz(aR));}catch(_){}
|
|
7641
|
+
// R22 (PRD §5): cross-service edges inferred from code (client call → matched route).
|
|
7642
|
+
try{aF.push(...scanCrossService(aR,fc));}catch(_){}
|
|
7643
|
+
// R21 (PRD §5): RBAC role-tier consistency over the route inventory.
|
|
7644
|
+
try{aF.push(...scanRbacConsistency(aR,fc));}catch(_){}
|
|
7588
7645
|
setProgress({current:i,total:files.length,file:"Reachability + guards...",phase:"Linking"});annotateReachability(aF,aR,callGraph,fc);aF.forEach(f=>detectGuardsForFinding(f,fc));
|
|
7589
7646
|
setProgress({current:i,total:files.length,file:"Inferring sanitizers...",phase:"Linking"});const learned=inferSanitizers(fc);applyLearnedSanitizers(aF,learned,fc);
|
|
7590
7647
|
setProgress({current:i,total:files.length,file:"Sanitizer effectiveness...",phase:"Linking"});applySanitizerEffectiveness(aF);
|
|
@@ -7593,6 +7650,9 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null},
|
|
|
7593
7650
|
setProgress({current:i,total:files.length,file:"OSV vulnerability database...",phase:"SCA"});
|
|
7594
7651
|
const allFileContents={...fc, ...depFileContents};
|
|
7595
7652
|
const components=parseManifests(allFileContents);
|
|
7653
|
+
// R8 (PRD §5): OS packages from an extracted container image's package DBs
|
|
7654
|
+
// (dpkg/apk) — baked-in deps the Dockerfile never names. Feed the OSV/SBOM pipeline.
|
|
7655
|
+
try{const{extractImagePackages}=await import('./sca/image-packages.js');for(const ip of extractImagePackages(allFileContents)){if(!components.some(c=>c.ecosystem===ip.ecosystem&&c.name===ip.name&&c.version===ip.version))components.push(ip);}}catch(_){}
|
|
7596
7656
|
try{const{detectVendoredLibraries}=await import('./sca/vendor-detect.js');const vendored=detectVendoredLibraries(fc);for(const v of vendored){const key=`${v.ecosystem}:${v.name}:${v.version}`;if(!components.some(c=>`${c.ecosystem}:${c.name}:${c.version}`===key))components.push({...v,group:'',purl:`pkg:${v.ecosystem}/${v.name}@${v.version}`,filePath:v.file,isUnpinned:false,reachable:true});}}catch(_){}
|
|
7597
7657
|
const reach=buildReachabilitySet(fc);
|
|
7598
7658
|
const reachabilitySet=reach.imported;
|
|
@@ -7607,6 +7667,14 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null},
|
|
|
7607
7667
|
// 0.10.0: enrich SCA findings with CISA KEV (CISA KEV catalog)
|
|
7608
7668
|
try{supplyChain=await _enrichWithKEV(supplyChain);}catch(_){}
|
|
7609
7669
|
try{markUsedVulnFunctions(supplyChain,fc);}catch(_){}
|
|
7670
|
+
// R7 (PRD §5): import-aware call-site augmentation for npm/pypi deps. The regex
|
|
7671
|
+
// pass above misses aliased / namespace imports (import {merge as deepMerge};
|
|
7672
|
+
// deepMerge()) and over-matches bare names in files that don't import the
|
|
7673
|
+
// package. This parses each JS/TS + Python file's imports, resolves vuln-fn
|
|
7674
|
+
// calls THROUGH the alias map, and gates them to files that actually import the
|
|
7675
|
+
// package — then ADDS the recovered sites so the route-reachability classifier
|
|
7676
|
+
// below (which decides reachable/unreachable/unknown) sees them. Additive only.
|
|
7677
|
+
try{const{augmentReachabilityViaImports}=await import('./sca/import-reachability.js');augmentReachabilityViaImports(supplyChain,fc,VULN_FUNCTION_HINTS);}catch(_){}
|
|
7610
7678
|
// Python AST-based function validation (deep mode only)
|
|
7611
7679
|
if(process.env.AGENTIC_SECURITY_DEEP==='1'){try{const{validateOsvFunctionsExist}=await import('./sca/py-package-functions.js');for(const sc of supplyChain){if(sc.type!=='vulnerable_dep'||sc.ecosystem!=='pypi')continue;if(!sc.osvVulnFunctions||!sc.osvVulnFunctions.length)continue;const{validated,missing}=validateOsvFunctionsExist(sc.name,sc.osvVulnFunctions,scanRoot);if(validated.length)sc._pyAstValidated=validated;if(missing.length)sc._pyAstMissing=missing;}}catch(_){}}
|
|
7612
7680
|
// LLM-assisted function extraction for CVEs without hints (opt-in: AGENTIC_SECURITY_LLM_SCA=1)
|
|
@@ -8050,10 +8118,14 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null},
|
|
|
8050
8118
|
try { await annotateNarration(finalFindings); }
|
|
8051
8119
|
catch (e) { _annotatorErrors.push({ phase: 'annotateNarration', err: String((e && e.message) || e) }); }
|
|
8052
8120
|
// Phase 3 (Sentinel-parity FR-L1, FR-L2) — IR + interprocedural taint.
|
|
8053
|
-
//
|
|
8054
|
-
//
|
|
8055
|
-
//
|
|
8056
|
-
//
|
|
8121
|
+
// R1 (PRD §5): the CLI entry (bin/agentic-security.js#cmdScan) now sets
|
|
8122
|
+
// AGENTIC_SECURITY_DEEP=1 by default for local/interactive scans, so deep mode
|
|
8123
|
+
// runs on the default `/scan --all` path. This gate is the enforcement +
|
|
8124
|
+
// CI-safety point: it honors an explicit opt-out (DEEP=0) and keeps deep off in
|
|
8125
|
+
// CI unless DEEP_IN_CI=1. In-process callers (tests, the cve-replay corpus) invoke
|
|
8126
|
+
// runScan()/runFullScan() directly without the CLI default, so they stay deep-off
|
|
8127
|
+
// and remain deterministic regression gates. Findings ride through the standard
|
|
8128
|
+
// dedup/cluster/confidence pipeline below and the LLM-validator stage that follows.
|
|
8057
8129
|
//
|
|
8058
8130
|
// SAFETY: Deep mode is gated for CI safety:
|
|
8059
8131
|
// - Global timeout via AGENTIC_SECURITY_DEEP_TIMEOUT_MS (default 300_000 = 5 min)
|
|
@@ -8195,13 +8267,29 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null},
|
|
|
8195
8267
|
// apply accept-risk / SLA / major-version-freeze rules. supplyChain
|
|
8196
8268
|
// findings get suppressed/tagged in place.
|
|
8197
8269
|
try { const sp = loadScaPolicy(scanRoot); if (sp && !sp._error) applyScaPolicy(supplyChain, sp); } catch (_) {}
|
|
8270
|
+
// R12 (PRD §5): deterministic decision-first verdict per dep, derived from the
|
|
8271
|
+
// signals enriched above (composite risk × KEV × EPSS × reachability × policy).
|
|
8272
|
+
// testsDetected is read from the UNFILTERED fileContents because shouldScan()
|
|
8273
|
+
// strips test files out of `fc`.
|
|
8274
|
+
try {
|
|
8275
|
+
const _testsDetected = Object.keys(fileContents).some(f =>
|
|
8276
|
+
/(?:^|\/)(?:tests?|spec|__tests__)\//i.test(f) ||
|
|
8277
|
+
/\.(?:test|spec)\.[cm]?[jt]sx?$/i.test(f) ||
|
|
8278
|
+
/(?:^|\/)test_[^/]*\.py$/i.test(f) || /_test\.(?:py|go)$/i.test(f));
|
|
8279
|
+
annotateScaVerdicts(supplyChain, { testsDetected: _testsDetected });
|
|
8280
|
+
} catch (_) {}
|
|
8198
8281
|
// 0.9.0 Feat-15: dep confusion
|
|
8199
8282
|
try{const dc=detectDepConfusion(annotatedComponents,scanRoot);aF.push(...dc);}catch(_){}
|
|
8200
8283
|
// Deployment-platform security checklist
|
|
8201
8284
|
try{const dpf=scanDeployPlatform(scanRoot);aLogic.push(...dpf);}catch(_){}
|
|
8202
8285
|
// Stack-specific security playbook
|
|
8203
8286
|
try{const sp=runStackPlaybook(scanRoot);if(sp&&sp.findings)aLogic.push(...sp.findings);}catch(_){}
|
|
8204
|
-
|
|
8287
|
+
// R17 (PRD §5): annotate per-finding corroboration ("one issue, many signals")
|
|
8288
|
+
// after evidence + validator verdicts are populated, then use it as a final
|
|
8289
|
+
// tiebreaker so multi-signal findings rank above single-signal ones at equal
|
|
8290
|
+
// severity. Does not alter calibrated confidence.
|
|
8291
|
+
try { annotateFindingProvenance(finalFindings); } catch (_) {}
|
|
8292
|
+
finalFindings.sort((a,b)=>(b.toxicityScore||0)-(a.toxicityScore||0)||(b.triageScore||0)-(a.triageScore||0)||({critical:0,high:1,medium:2,low:3}[a.severity]??4)-({critical:0,high:1,medium:2,low:3}[b.severity]??4)||((b.corroborationCount||1)-(a.corroborationCount||1)));
|
|
8205
8293
|
// Auto-PoC filter: tag whether a concrete payload+test can be derived. When
|
|
8206
8294
|
// AGENTIC_SECURITY_POC=1, demote ≥medium findings that fail this check.
|
|
8207
8295
|
// Used as a precision lever for users who want to ship clean reports —
|
package/src/ir/parser-go.js
CHANGED
|
@@ -79,6 +79,14 @@ function _lowerExpr(text) {
|
|
|
79
79
|
const parts = _splitTopLevelCommas(inner).map(_lowerExpr);
|
|
80
80
|
return { kind: 'tpl', parts };
|
|
81
81
|
}
|
|
82
|
+
// R3 (PRD §5): string concat with `+` MUST be checked before the bare-literal
|
|
83
|
+
// rule below — otherwise `"SELECT " + q` (starts with `"`) is captured whole as
|
|
84
|
+
// a single literal and taint cannot flow through the concatenation. Only fires
|
|
85
|
+
// when there's a top-level `+` (a pure literal still falls through to literal).
|
|
86
|
+
if (/["'`]/.test(s) && s.includes('+')) {
|
|
87
|
+
const plusParts = _splitTopLevelPlus(s);
|
|
88
|
+
if (plusParts.length > 1) return { kind: 'tpl', parts: plusParts.map(_lowerExpr) };
|
|
89
|
+
}
|
|
82
90
|
if (/^"/.test(s) || /^`/.test(s)) return { kind: 'literal', value: s };
|
|
83
91
|
if (/^\d/.test(s)) return { kind: 'literal', value: s };
|
|
84
92
|
if (/^(true|false|nil)\b/.test(s)) return { kind: 'literal', value: s };
|
package/src/mcp/tools.js
CHANGED
|
@@ -16,7 +16,7 @@ import * as fs from 'node:fs';
|
|
|
16
16
|
import * as fsp from 'node:fs/promises';
|
|
17
17
|
import * as path from 'node:path';
|
|
18
18
|
import * as crypto from 'node:crypto';
|
|
19
|
-
import { applyFix as applyFixHistory } from '../posture/fix-history.js';
|
|
19
|
+
import { applyFix as applyFixHistory, fixAcceptanceRate } from '../posture/fix-history.js';
|
|
20
20
|
import { verifyLastScan } from '../posture/integrity.js';
|
|
21
21
|
import { redactString, redactFinding } from './redact.js';
|
|
22
22
|
|
|
@@ -578,7 +578,11 @@ export const apply_fix = {
|
|
|
578
578
|
}
|
|
579
579
|
throw e;
|
|
580
580
|
}
|
|
581
|
-
|
|
581
|
+
// R25 (PRD §5): surface the running auto-fix acceptance rate after each
|
|
582
|
+
// applied fix, so the closed loop reports its own success metric.
|
|
583
|
+
let acceptance = null;
|
|
584
|
+
try { acceptance = fixAcceptanceRate(ctx.sessionRoot); } catch { /* metric is best-effort */ }
|
|
585
|
+
return { _meta: META, applied: true, historyId: entry.id, file: f.file, backupPath: entry.backupPath, integrity: status, attemptOrdinal: entry.attemptOrdinal, acceptance };
|
|
582
586
|
},
|
|
583
587
|
};
|
|
584
588
|
|