@clear-capabilities/agentic-security-scanner 0.74.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.
Files changed (331) hide show
  1. package/CHANGELOG.md +1580 -0
  2. package/bin/.agentic-security/findings.json +1577 -0
  3. package/bin/.agentic-security/last-scan.json +1577 -0
  4. package/bin/.agentic-security/last-scan.json.sig +1 -0
  5. package/bin/.agentic-security/scan-history.json +465 -0
  6. package/bin/.agentic-security/streak.json +25 -0
  7. package/bin/agentic-security-audit.js +198 -0
  8. package/bin/agentic-security-consistency.js +80 -0
  9. package/bin/agentic-security-diff.js +136 -0
  10. package/bin/agentic-security-lsp.js +12 -0
  11. package/bin/agentic-security-mcp.js +40 -0
  12. package/bin/agentic-security-rule.js +153 -0
  13. package/bin/agentic-security.js +1683 -0
  14. package/dist/117.index.js +207 -0
  15. package/dist/178.index.js +250 -0
  16. package/dist/218.index.js +793 -0
  17. package/dist/227.index.js +192 -0
  18. package/dist/301.index.js +167 -0
  19. package/dist/384.index.js +18 -0
  20. package/dist/476.index.js +126 -0
  21. package/dist/513.index.js +373 -0
  22. package/dist/520.index.js +13 -0
  23. package/dist/601.index.js +1038 -0
  24. package/dist/634.index.js +1892 -0
  25. package/dist/637.index.js +216 -0
  26. package/dist/660.index.js +131 -0
  27. package/dist/675.index.js +451 -0
  28. package/dist/826.index.js +188 -0
  29. package/dist/830.index.js +133 -0
  30. package/dist/agentic-security.mjs +272 -0
  31. package/dist/agentic-security.mjs.sha256 +1 -0
  32. package/dist/calibration-seed.json +27 -0
  33. package/package.json +77 -0
  34. package/src/.agentic-security/findings.json +80844 -0
  35. package/src/.agentic-security/last-scan.json +80844 -0
  36. package/src/.agentic-security/last-scan.json.sig +1 -0
  37. package/src/.agentic-security/scan-history.json +8408 -0
  38. package/src/.agentic-security/streak.json +26 -0
  39. package/src/badge.js +188 -0
  40. package/src/compare.js +203 -0
  41. package/src/dataflow/.agentic-security/findings.json +3487 -0
  42. package/src/dataflow/.agentic-security/last-scan.json +3487 -0
  43. package/src/dataflow/.agentic-security/last-scan.json.sig +1 -0
  44. package/src/dataflow/.agentic-security/scan-history.json +735 -0
  45. package/src/dataflow/.agentic-security/streak.json +24 -0
  46. package/src/dataflow/CLAUDE.md +38 -0
  47. package/src/dataflow/access-paths.js +172 -0
  48. package/src/dataflow/async-sequencing.js +177 -0
  49. package/src/dataflow/backward.js +201 -0
  50. package/src/dataflow/catalog-expanded.js +485 -0
  51. package/src/dataflow/catalog.js +659 -0
  52. package/src/dataflow/cross-repo.js +219 -0
  53. package/src/dataflow/engine.js +588 -0
  54. package/src/dataflow/exception-flow.js +116 -0
  55. package/src/dataflow/exploit-prover.js +187 -0
  56. package/src/dataflow/higher-order.js +221 -0
  57. package/src/dataflow/ifds.js +347 -0
  58. package/src/dataflow/implicit-flow.js +129 -0
  59. package/src/dataflow/incremental.js +229 -0
  60. package/src/dataflow/index.js +181 -0
  61. package/src/dataflow/numeric-domain.js +192 -0
  62. package/src/dataflow/path-feasibility.js +114 -0
  63. package/src/dataflow/points-to.js +337 -0
  64. package/src/dataflow/polyglot.js +190 -0
  65. package/src/dataflow/proven-clean.js +159 -0
  66. package/src/dataflow/receiver-context.js +76 -0
  67. package/src/dataflow/sanitizer-proof.js +154 -0
  68. package/src/dataflow/soft-taint.js +140 -0
  69. package/src/dataflow/string-domain.js +234 -0
  70. package/src/dataflow/stub-aware-filter.js +100 -0
  71. package/src/dataflow/summaries.js +132 -0
  72. package/src/dataflow/symbolic-exec.js +238 -0
  73. package/src/dataflow/tabulation.js +135 -0
  74. package/src/engine.js +7763 -0
  75. package/src/history-scan.js +229 -0
  76. package/src/index.js +3 -0
  77. package/src/integrations/.agentic-security/findings.json +1504 -0
  78. package/src/integrations/.agentic-security/last-scan.json +1504 -0
  79. package/src/integrations/.agentic-security/scan-history.json +40 -0
  80. package/src/integrations/.agentic-security/streak.json +21 -0
  81. package/src/integrations/index.js +321 -0
  82. package/src/integrations/tickets.js +200 -0
  83. package/src/ir/.agentic-security/findings.json +3036 -0
  84. package/src/ir/.agentic-security/last-scan.json +3036 -0
  85. package/src/ir/.agentic-security/last-scan.json.sig +1 -0
  86. package/src/ir/.agentic-security/scan-history.json +364 -0
  87. package/src/ir/.agentic-security/streak.json +23 -0
  88. package/src/ir/CLAUDE.md +172 -0
  89. package/src/ir/callgraph.js +73 -0
  90. package/src/ir/class-hierarchy.js +195 -0
  91. package/src/ir/index.js +152 -0
  92. package/src/ir/parser-cs.js +260 -0
  93. package/src/ir/parser-java.js +286 -0
  94. package/src/ir/parser-js.js +413 -0
  95. package/src/ir/parser-kt.js +258 -0
  96. package/src/ir/parser-py-cst.js +136 -0
  97. package/src/ir/parser-py.helper.py +501 -0
  98. package/src/ir/parser-py.js +312 -0
  99. package/src/ir/ssa.js +315 -0
  100. package/src/ir/type-stubs.js +288 -0
  101. package/src/leaderboard.js +152 -0
  102. package/src/llm-validator/.agentic-security/findings.json +1891 -0
  103. package/src/llm-validator/.agentic-security/last-scan.json +1891 -0
  104. package/src/llm-validator/.agentic-security/last-scan.json.sig +1 -0
  105. package/src/llm-validator/.agentic-security/scan-history.json +168 -0
  106. package/src/llm-validator/.agentic-security/streak.json +20 -0
  107. package/src/llm-validator/consistency.js +141 -0
  108. package/src/llm-validator/index.js +437 -0
  109. package/src/lsp/.agentic-security/findings.json +28 -0
  110. package/src/lsp/.agentic-security/last-scan.json +28 -0
  111. package/src/lsp/.agentic-security/scan-history.json +79 -0
  112. package/src/lsp/.agentic-security/streak.json +22 -0
  113. package/src/lsp/server.js +275 -0
  114. package/src/mcp/.agentic-security/findings.json +8358 -0
  115. package/src/mcp/.agentic-security/last-scan.json +8358 -0
  116. package/src/mcp/.agentic-security/last-scan.json.sig +1 -0
  117. package/src/mcp/.agentic-security/scan-history.json +1125 -0
  118. package/src/mcp/.agentic-security/streak.json +22 -0
  119. package/src/mcp/CLAUDE.md +54 -0
  120. package/src/mcp/audit.js +136 -0
  121. package/src/mcp/redact.js +75 -0
  122. package/src/mcp/server.js +158 -0
  123. package/src/mcp/stdio.js +83 -0
  124. package/src/mcp/tools.js +940 -0
  125. package/src/mcp/validate.js +49 -0
  126. package/src/personality.js +164 -0
  127. package/src/poc-video.js +239 -0
  128. package/src/posture/.agentic-security/findings.json +51239 -0
  129. package/src/posture/.agentic-security/last-scan.json +51239 -0
  130. package/src/posture/.agentic-security/last-scan.json.sig +1 -0
  131. package/src/posture/.agentic-security/scan-history.json +5557 -0
  132. package/src/posture/.agentic-security/streak.json +24 -0
  133. package/src/posture/CLAUDE.md +42 -0
  134. package/src/posture/adversarial-self-test.js +114 -0
  135. package/src/posture/adversary-agent.js +204 -0
  136. package/src/posture/agents-memory.js +135 -0
  137. package/src/posture/ai-code-fingerprint.js +171 -0
  138. package/src/posture/aibom.js +284 -0
  139. package/src/posture/api-inventory.js +96 -0
  140. package/src/posture/attack-playbooks.js +305 -0
  141. package/src/posture/auditor-agent.js +115 -0
  142. package/src/posture/auth-posture-import.js +135 -0
  143. package/src/posture/baseline-compare.js +114 -0
  144. package/src/posture/blast-radius.js +836 -0
  145. package/src/posture/bounty-prediction.js +141 -0
  146. package/src/posture/business-logic.js +239 -0
  147. package/src/posture/calibration-drift.js +93 -0
  148. package/src/posture/calibration-seed.json +27 -0
  149. package/src/posture/calibration.js +204 -0
  150. package/src/posture/clustering.js +75 -0
  151. package/src/posture/concurrency-checker.js +265 -0
  152. package/src/posture/confidence.js +65 -0
  153. package/src/posture/container-runtime.js +149 -0
  154. package/src/posture/counterfactual.js +109 -0
  155. package/src/posture/cross-lang-graphql.js +165 -0
  156. package/src/posture/cross-lang-grpc.js +166 -0
  157. package/src/posture/cross-lang-meta.js +101 -0
  158. package/src/posture/cross-lang-openapi.js +187 -0
  159. package/src/posture/cross-lang-orm.js +153 -0
  160. package/src/posture/cross-lang-queues.js +210 -0
  161. package/src/posture/crown-jewels.js +110 -0
  162. package/src/posture/custom-rules.js +361 -0
  163. package/src/posture/cve-alert-daemon.js +433 -0
  164. package/src/posture/cve-lookup.js +129 -0
  165. package/src/posture/dead-code.js +430 -0
  166. package/src/posture/defender-agent.js +158 -0
  167. package/src/posture/deploy-platform.js +204 -0
  168. package/src/posture/detector-fuzz.js +61 -0
  169. package/src/posture/deterministic.js +99 -0
  170. package/src/posture/drift.js +165 -0
  171. package/src/posture/epss.js +156 -0
  172. package/src/posture/exploitability-probability.js +212 -0
  173. package/src/posture/exploitability.js +121 -0
  174. package/src/posture/feature-flags.js +110 -0
  175. package/src/posture/finding-defaults.js +132 -0
  176. package/src/posture/fix-history.js +411 -0
  177. package/src/posture/fix-plan.js +121 -0
  178. package/src/posture/fix-verify-loop.js +157 -0
  179. package/src/posture/fix-verify.js +130 -0
  180. package/src/posture/flow-narration.js +105 -0
  181. package/src/posture/grader-calibration.js +156 -0
  182. package/src/posture/harness-discovery.js +113 -0
  183. package/src/posture/holdout-eval.js +144 -0
  184. package/src/posture/iac-reachability.js +163 -0
  185. package/src/posture/iam-policy.js +128 -0
  186. package/src/posture/integrity.js +97 -0
  187. package/src/posture/learning.js +166 -0
  188. package/src/posture/license-policy.js +109 -0
  189. package/src/posture/llm-redteam-prompts.js +418 -0
  190. package/src/posture/llm-redteam.js +303 -0
  191. package/src/posture/material-change.js +163 -0
  192. package/src/posture/mitigation-composite.js +55 -0
  193. package/src/posture/mttr.js +91 -0
  194. package/src/posture/network-policy-import.js +126 -0
  195. package/src/posture/path-predicates.js +99 -0
  196. package/src/posture/persona-prioritization.js +153 -0
  197. package/src/posture/poc-cwe-map.js +51 -0
  198. package/src/posture/poc-generator.js +500 -0
  199. package/src/posture/policy-gate.js +174 -0
  200. package/src/posture/pre-incident-archaeology.js +110 -0
  201. package/src/posture/profile.js +93 -0
  202. package/src/posture/reachability-filter.js +42 -0
  203. package/src/posture/regression-test-gen.js +200 -0
  204. package/src/posture/reverse-blast-radius.js +110 -0
  205. package/src/posture/router.js +109 -0
  206. package/src/posture/rule-overrides.js +198 -0
  207. package/src/posture/rule-pack-signing.js +209 -0
  208. package/src/posture/rule-packs.js +143 -0
  209. package/src/posture/rule-synthesis.js +108 -0
  210. package/src/posture/ruleset-version.js +71 -0
  211. package/src/posture/sbom.js +129 -0
  212. package/src/posture/schema-aware-bridge.js +207 -0
  213. package/src/posture/security-trend.js +87 -0
  214. package/src/posture/semantic-clone.js +114 -0
  215. package/src/posture/specification-mining.js +170 -0
  216. package/src/posture/stable-id.js +75 -0
  217. package/src/posture/stack-playbook.js +229 -0
  218. package/src/posture/streak.js +249 -0
  219. package/src/posture/suppressions.js +135 -0
  220. package/src/posture/telemetry-ingest.js +112 -0
  221. package/src/posture/threat-model.js +145 -0
  222. package/src/posture/three-agent-pipeline.js +74 -0
  223. package/src/posture/triage.js +146 -0
  224. package/src/posture/trust-boundary-diagram.js +115 -0
  225. package/src/posture/type-narrowing.js +129 -0
  226. package/src/posture/validator-metrics.js +179 -0
  227. package/src/posture/verifier-ephemeral.js +118 -0
  228. package/src/posture/verifier-target.js +147 -0
  229. package/src/posture/verifier.js +257 -0
  230. package/src/posture/version.js +75 -0
  231. package/src/posture/waf-ingest.js +200 -0
  232. package/src/posture/why-fired.js +141 -0
  233. package/src/pr-comment.js +172 -0
  234. package/src/pr-delta.js +198 -0
  235. package/src/report/.agentic-security/findings.json +79 -0
  236. package/src/report/.agentic-security/last-scan.json +79 -0
  237. package/src/report/.agentic-security/last-scan.json.sig +1 -0
  238. package/src/report/.agentic-security/scan-history.json +332 -0
  239. package/src/report/.agentic-security/streak.json +23 -0
  240. package/src/report/index.js +1136 -0
  241. package/src/report/mascot.js +42 -0
  242. package/src/runScan.js +141 -0
  243. package/src/sast/.agentic-security/findings.json +5051 -0
  244. package/src/sast/.agentic-security/last-scan.json +5051 -0
  245. package/src/sast/.agentic-security/last-scan.json.sig +1 -0
  246. package/src/sast/.agentic-security/scan-history.json +788 -0
  247. package/src/sast/.agentic-security/streak.json +23 -0
  248. package/src/sast/CLAUDE.md +39 -0
  249. package/src/sast/_comment-strip.js +46 -0
  250. package/src/sast/agent-tool-escalation.js +131 -0
  251. package/src/sast/auth-provider.js +171 -0
  252. package/src/sast/authz.js +236 -0
  253. package/src/sast/bench-shape/.agentic-security/findings.json +28 -0
  254. package/src/sast/bench-shape/.agentic-security/last-scan.json +28 -0
  255. package/src/sast/bench-shape/.agentic-security/scan-history.json +24 -0
  256. package/src/sast/bench-shape/.agentic-security/streak.json +22 -0
  257. package/src/sast/bench-shape/index.js +62 -0
  258. package/src/sast/claude-hook-injection.js +199 -0
  259. package/src/sast/claude-md-prompt-injection.js +170 -0
  260. package/src/sast/claude-settings.js +165 -0
  261. package/src/sast/client-side.js +149 -0
  262. package/src/sast/cpp-bench-extras.js +122 -0
  263. package/src/sast/cpp-dataflow.js +430 -0
  264. package/src/sast/cpp.js +248 -0
  265. package/src/sast/csharp.js +152 -0
  266. package/src/sast/csrf.js +82 -0
  267. package/src/sast/dart-flutter.js +173 -0
  268. package/src/sast/db-rls.js +147 -0
  269. package/src/sast/db-taint.js +215 -0
  270. package/src/sast/defi-deep.js +242 -0
  271. package/src/sast/deserialization-gadgets.js +113 -0
  272. package/src/sast/django-hardening.js +230 -0
  273. package/src/sast/env-hygiene.js +125 -0
  274. package/src/sast/fastapi-hardening.js +145 -0
  275. package/src/sast/go-extended.js +84 -0
  276. package/src/sast/host-header.js +106 -0
  277. package/src/sast/index.js +17 -0
  278. package/src/sast/java-ast-folding.js +561 -0
  279. package/src/sast/java-bench-extras.js +708 -0
  280. package/src/sast/java-collection-passthrough.js +178 -0
  281. package/src/sast/java-constant-fold.js +244 -0
  282. package/src/sast/java-deserialization.js +125 -0
  283. package/src/sast/jndi.js +104 -0
  284. package/src/sast/juliet-shape.js +324 -0
  285. package/src/sast/jwt-exp.js +104 -0
  286. package/src/sast/kotlin.js +82 -0
  287. package/src/sast/laravel-hardening.js +198 -0
  288. package/src/sast/ldap-injection.js +100 -0
  289. package/src/sast/llm-owasp.js +465 -0
  290. package/src/sast/llm-stored-prompt.js +103 -0
  291. package/src/sast/llm-trading-agent.js +161 -0
  292. package/src/sast/llm.js +308 -0
  293. package/src/sast/logic.js +140 -0
  294. package/src/sast/mass-assignment.js +101 -0
  295. package/src/sast/mcp-audit.js +242 -0
  296. package/src/sast/mobile-manifest.js +195 -0
  297. package/src/sast/model-load.js +164 -0
  298. package/src/sast/mutation-xss.js +87 -0
  299. package/src/sast/nosql-injection.js +82 -0
  300. package/src/sast/open-redirect.js +119 -0
  301. package/src/sast/php.js +91 -0
  302. package/src/sast/pipeline.js +122 -0
  303. package/src/sast/primary-cwe-java.js +155 -0
  304. package/src/sast/prompt-firewall.js +151 -0
  305. package/src/sast/prompt-template.js +157 -0
  306. package/src/sast/prototype-pollution.js +112 -0
  307. package/src/sast/python-sinks.js +195 -0
  308. package/src/sast/quarkus-hardening.js +102 -0
  309. package/src/sast/rag-poisoning.js +118 -0
  310. package/src/sast/rate-limit.js +128 -0
  311. package/src/sast/response-splitting.js +138 -0
  312. package/src/sast/ruby.js +108 -0
  313. package/src/sast/rust.js +105 -0
  314. package/src/sast/solidity.js +167 -0
  315. package/src/sast/springboot-hardening.js +186 -0
  316. package/src/sast/ssrf-cloud-metadata.js +80 -0
  317. package/src/sast/ssti.js +116 -0
  318. package/src/sast/swift.js +162 -0
  319. package/src/sast/toctou.js +95 -0
  320. package/src/sast/webhook.js +101 -0
  321. package/src/sast/xpath-injection.js +51 -0
  322. package/src/sast/xxe.js +140 -0
  323. package/src/sast/zip-slip.js +200 -0
  324. package/src/sca/base-images.json +45 -0
  325. package/src/sca/container.js +107 -0
  326. package/src/sca/dep-confusion.js +134 -0
  327. package/src/sca/index.js +6 -0
  328. package/src/sca/popular-packages.json +41 -0
  329. package/src/sca/sarif-ingest.js +187 -0
  330. package/src/sca/vuln-function-hints.json +89 -0
  331. package/src/secrets/index.js +4 -0
@@ -0,0 +1,161 @@
1
+ // LLM autonomous-trading-agent security audit.
2
+ //
3
+ // Targets code that gives an LLM agent on-chain transaction authority. These
4
+ // patterns aren't generic LLM prompt-injection issues — they're financial-
5
+ // loss-shaped, so a missed check means real money.
6
+ //
7
+ // Coverage:
8
+ // 1. send_raw_transaction / signTransaction / sendTransaction without a
9
+ // prior eth_call simulation.
10
+ // 2. Trading agent code with no MAX_SINGLE_TX or MAX_DAILY_SPEND constant
11
+ // (when an LLM-call API is present in the same file).
12
+ // 3. RPC response / token name / pair label concatenated into an LLM
13
+ // prompt without sanitization (prompt-injection-via-onchain-data).
14
+ // 4. Autonomous trading loop without a circuit breaker
15
+ // (consecutive-loss counter or hourly-loss-pct halt).
16
+ // 5. Raw hex private key in source: PRIVATE_KEY = "0x..."
17
+ //
18
+ // Fires on .py / .js / .ts / .mjs / .cjs files. The agent ↔ wallet pattern
19
+ // usually lives in Python or JS today; we cover both.
20
+
21
+ const _SUPPORTED_EXT = /\.(?:py|js|jsx|ts|tsx|mjs|cjs)$/i;
22
+
23
+ function _line(raw, idx) {
24
+ return raw.slice(0, idx).split('\n').length;
25
+ }
26
+
27
+ function _looksLikeTradingAgent(raw) {
28
+ // Heuristic: file imports web3 / ethers / viem / solana AND mentions an LLM
29
+ // SDK (anthropic / openai / langchain / together / groq) OR an agent loop.
30
+ const onchain = /\b(?:from\s+web3\b|import\s+\{?[^}]*\}?\s+from\s+['"]ethers|import\s+\{?[^}]*\}?\s+from\s+['"]viem|from\s+solders|from\s+anchorpy)\b/.test(raw);
31
+ const llmSdk = /\b(?:anthropic|openai|langchain|together|groq|@anthropic-ai|@openai)\b/i.test(raw);
32
+ const agentLoop = /\b(?:while\s+True|while\s*\(\s*true|asyncio\.run|setInterval|setTimeout.*?ms\s*\)|trading_loop|run_agent)\b/i.test(raw);
33
+ return onchain && (llmSdk || agentLoop);
34
+ }
35
+
36
+ const _SIGN_OR_SEND_RE = /\b(?:send_raw_transaction|sendRawTransaction|signTransaction|sign_transaction|sendTransaction)\s*\(/;
37
+ const _ETH_CALL_RE = /\b(?:eth_call|eth\.call|w3\.eth\.call|client\.call|estimateGas|estimate_gas|simulate(?:_transaction)?|callStatic)\b/;
38
+ const _SPEND_LIMIT_RE = /\b(?:MAX_SINGLE_TX|MAX_DAILY_SPEND|MAX_TX_VALUE|DAILY_LIMIT|SPEND_LIMIT|TX_BUDGET|TRADE_BUDGET)\b/;
39
+ const _CIRCUIT_BREAKER_RE = /\b(?:circuit_breaker|CircuitBreaker|consecutive_losses|max_hourly_loss|HOURLY_LOSS|MAX_DRAWDOWN|halt(?:_trading|_loop)?)\b/i;
40
+
41
+ const _PROMPT_INJECTION_PATTERNS = [
42
+ /ignore\s+(?:previous|all)\s+instructions/i,
43
+ /system\s+prompt/i,
44
+ /transfer\s+.{0,50}\s+to\b/i,
45
+ /approve\s+.{0,50}\s+for\b/i,
46
+ /\bsend\s+.{0,50}\s+to\s+0x[0-9a-fA-F]{40}\b/i,
47
+ ];
48
+
49
+ const _ONCHAIN_DATA_SOURCES = [
50
+ 'token_name', 'pair_name', 'pair_label', 'token_symbol', 'tokenName',
51
+ 'pairName', 'pairLabel', 'tokenSymbol', 'pool_name', 'event_log',
52
+ 'metadata', 'rpc_response', 'event_data',
53
+ ];
54
+
55
+ const _PROMPT_SINKS = /\b(?:client\.messages\.create|chat\.completions\.create|generate|invoke|stream|completion)\s*\(/;
56
+
57
+ export function scanLlmTradingAgent(file, raw) {
58
+ if (!file || !raw || typeof raw !== 'string') return [];
59
+ if (!_SUPPORTED_EXT.test(file)) return [];
60
+ if (raw.length > 200_000) return [];
61
+ if (!_looksLikeTradingAgent(raw)) return [];
62
+
63
+ const findings = [];
64
+
65
+ // 1. send_raw_transaction without prior eth_call simulation.
66
+ for (const m of raw.matchAll(new RegExp(_SIGN_OR_SEND_RE.source, 'g'))) {
67
+ // Look ±50 lines for any simulation call.
68
+ const lineNum = _line(raw, m.index);
69
+ const lines = raw.split('\n');
70
+ const window = lines.slice(Math.max(0, lineNum - 50), lineNum + 5).join('\n');
71
+ if (_ETH_CALL_RE.test(window)) continue;
72
+ findings.push({
73
+ id: `llm-trading:no-simulation:${file}:${lineNum}`,
74
+ file, line: lineNum,
75
+ vuln: 'Trading agent sends transaction without prior simulation (eth_call / estimateGas)',
76
+ severity: 'high',
77
+ family: 'llm-trading-no-simulation',
78
+ cwe: 'CWE-754',
79
+ confidence: 0.7,
80
+ description: 'The agent signs and sends a transaction without first calling eth_call / estimateGas / callStatic to simulate the outcome. A bug, prompt-injection, or oracle manipulation that produces a bad tx silently lands on-chain — and reverts there cost gas.',
81
+ remediation: 'Add a pre-send simulation: const result = await w3.eth.call(tx); decode the expected output; require it matches an expected_min_out before signTransaction + sendRawTransaction.',
82
+ });
83
+ }
84
+
85
+ // 2. No spend-limit constant
86
+ if (!_SPEND_LIMIT_RE.test(raw)) {
87
+ // Only flag when the file actually signs / sends.
88
+ if (_SIGN_OR_SEND_RE.test(raw)) {
89
+ const m = _SIGN_OR_SEND_RE.exec(raw);
90
+ findings.push({
91
+ id: `llm-trading:no-spend-limit:${file}:${_line(raw, m.index)}`,
92
+ file, line: _line(raw, m.index),
93
+ vuln: 'LLM trading agent has no per-tx / daily spend limit constant',
94
+ severity: 'critical',
95
+ family: 'llm-trading-no-spend-limit',
96
+ cwe: 'CWE-770',
97
+ confidence: 0.7,
98
+ description: 'Code can sign and send transactions but no MAX_SINGLE_TX / MAX_DAILY_SPEND / TX_BUDGET constant is visible. A prompt-injection (or a routine LLM hallucination) that asks the agent to "send all funds" has no upper bound.',
99
+ remediation: 'Define MAX_SINGLE_TX_USD and MAX_DAILY_SPEND_USD as Decimal constants. Add a SpendLimitGuard that checks both before every transaction; persist daily spend in disk-state so restart doesn\'t reset it.',
100
+ });
101
+ }
102
+ }
103
+
104
+ // 3. Onchain data concatenated into LLM prompt
105
+ if (_PROMPT_SINKS.test(raw)) {
106
+ for (const src of _ONCHAIN_DATA_SOURCES) {
107
+ // Look for f-string / template literal / + concat with the source name.
108
+ const fstr = new RegExp(`f["'][^"']*?\\{\\s*${src}\\s*\\}|\\$\\{\\s*${src}\\s*\\}|['"][^'"]*?['"]\\s*\\+\\s*${src}\\b`);
109
+ const m = fstr.exec(raw);
110
+ if (!m) continue;
111
+ // Only fire if there's no obvious sanitizer call near it.
112
+ const nearby = raw.slice(Math.max(0, m.index - 600), m.index + 200);
113
+ if (/\b(?:sanitize|validate|allow_list|allowlist|whitelist|escape|redact)/i.test(nearby)) continue;
114
+ findings.push({
115
+ id: `llm-trading:onchain-prompt-injection:${file}:${_line(raw, m.index)}`,
116
+ file, line: _line(raw, m.index),
117
+ vuln: `On-chain data (${src}) concatenated into an LLM prompt without sanitization`,
118
+ severity: 'high',
119
+ family: 'llm-trading-prompt-injection',
120
+ cwe: 'CWE-77',
121
+ confidence: 0.7,
122
+ description: `Token names, pair labels, pool metadata, and event-log payloads are attacker-controlled (anyone can deploy a token with any name). When concatenated into an execution-capable LLM prompt, the attacker can issue arbitrary trading instructions.`,
123
+ remediation: `Strip / allow-list the on-chain string before it enters the prompt. Verify it against known injection shapes: "ignore previous instructions", "transfer to 0x...", "approve unlimited".`,
124
+ });
125
+ break; // one per source is enough
126
+ }
127
+ }
128
+
129
+ // 4. Trading loop without circuit breaker
130
+ if (/while\s+True|while\s*\(\s*true|setInterval/.test(raw) && _SIGN_OR_SEND_RE.test(raw) && !_CIRCUIT_BREAKER_RE.test(raw)) {
131
+ const m = /while\s+True|while\s*\(\s*true|setInterval/.exec(raw);
132
+ findings.push({
133
+ id: `llm-trading:no-circuit-breaker:${file}:${_line(raw, m.index)}`,
134
+ file, line: _line(raw, m.index),
135
+ vuln: 'Autonomous trading loop with no circuit breaker',
136
+ severity: 'high',
137
+ family: 'llm-trading-no-circuit-breaker',
138
+ cwe: 'CWE-754',
139
+ confidence: 0.7,
140
+ description: 'Loop signs and sends transactions in a continuous cycle with no halt condition. A stuck oracle, an exchange bug, or a market crash causes the agent to keep trading the loss.',
141
+ remediation: 'Add a circuit breaker that halts after MAX_CONSECUTIVE_LOSSES or after a portfolio-value drop of MAX_HOURLY_LOSS_PCT. Require human re-arm to resume.',
142
+ });
143
+ }
144
+
145
+ // 5. Raw hex private key — PRIVATE_KEY = "0x..." (64 hex chars)
146
+ for (const m of raw.matchAll(/\b(?:PRIVATE_KEY|WALLET_KEY|SIGNING_KEY|priv_key|privKey)\s*[:=]\s*['"]0x[0-9a-fA-F]{64}['"]/g)) {
147
+ findings.push({
148
+ id: `llm-trading:hardcoded-private-key:${file}:${_line(raw, m.index)}`,
149
+ file, line: _line(raw, m.index),
150
+ vuln: 'Hardcoded 32-byte raw private key in source',
151
+ severity: 'critical',
152
+ family: 'llm-trading-hardcoded-key',
153
+ cwe: 'CWE-798',
154
+ confidence: 0.98,
155
+ description: 'A literal 64-hex-char private key in source is catastrophic — anyone who reads the file can sign transactions as the wallet owner. Bots scrape GitHub for this pattern in real-time.',
156
+ remediation: 'Move to a secure vault (AWS KMS / HashiCorp Vault / GCP Secret Manager) or a hardware wallet. Rotate the key immediately by sweeping funds to a new wallet from a clean machine.',
157
+ });
158
+ }
159
+
160
+ return findings;
161
+ }
@@ -0,0 +1,308 @@
1
+ // Prompt-injection / LLM-app security detector.
2
+ //
3
+ // F1 strategy:
4
+ // Recall — broad SDK coverage (Anthropic, OpenAI, Vercel AI, LangChain,
5
+ // Google, Mistral, Cohere, Groq, Together; JS + Python).
6
+ // Precision — only fire when concrete evidence ties a tainted source to an
7
+ // LLM sink, or a tool definition exposes a known-dangerous capability.
8
+ // Pure-literal prompts and non-prod paths are suppressed.
9
+
10
+ const _NONPROD_PATH_RE = /(?:^|\/)(?:tests?|__tests__|spec|fixtures?|examples?|docs?|stories|codefixes|node_modules)\//i;
11
+ const _SCANNABLE_EXT_RE = /\.(?:js|jsx|ts|tsx|mjs|cjs|py)$/i;
12
+
13
+ // LLM SDK call sites — one per family; line-anchored, escape-safe.
14
+ const LLM_SINK_PATTERNS = [
15
+ // Anthropic (TS/JS + Python share the same dotted shape)
16
+ /\b(?:anthropic|client|claude)\.(?:messages|completions)\.create\s*\(/,
17
+ // OpenAI SDK v4+ (chat/responses/completions)
18
+ /\b(?:openai|client|oai)\.(?:chat\.)?completions\.create\s*\(/,
19
+ /\b(?:openai|client|oai)\.responses\.create\s*\(/,
20
+ // Vercel AI SDK
21
+ /\b(?:generateText|streamText|generateObject|streamObject)\s*\(/,
22
+ // LangChain JS / Python
23
+ /\b(?:llm|model|chain|chat|agent|executor)\.(?:invoke|call|run|predict|stream|batch|ainvoke|astream)\s*\(/,
24
+ /\bChatPromptTemplate\.from(?:Messages|Template)\s*\(/,
25
+ // Google Generative AI
26
+ /\b(?:model|genAI|chat)\.generateContent(?:Stream)?\s*\(/,
27
+ // Mistral / Cohere / Groq / Together (same dotted-create shape)
28
+ /\b(?:mistral|cohere|groq|together)\.(?:chat\.complete|chat\.completions\.create|generate)\s*\(/,
29
+ // Ollama / OpenAI-compatible HTTP endpoints via any client (requests/httpx/aiohttp/fetch).
30
+ // Matches a quoted URL pointing at a known LLM completion path.
31
+ /["'`][^"'`]*\/(?:api\/(?:generate|chat|embeddings)|v1\/(?:chat\/completions|completions|messages|embeddings))(?:["'`?]|$)/,
32
+ ];
33
+
34
+ // Lower-precision LLM-call shape used only as a corroborator (require import).
35
+ const LLM_FUZZY_CALL_RE = /\b(?:complete|generate|chat|invoke|predict)\s*\(/;
36
+
37
+ // Imports indicate an LLM-using file even when call shape is non-standard.
38
+ const LLM_IMPORT_RE = /(?:from\s+["']?(?:anthropic|openai|@ai-sdk|ai|@anthropic-ai|@google\/generative-ai|cohere-ai|@mistralai|groq-sdk|together-ai|langchain|ollama|gpt4all|llama_index|llama-index)["']?|require\s*\(\s*["'](?:@anthropic-ai|@ai-sdk|openai|anthropic|cohere-ai|@mistralai|langchain|groq-sdk|together-ai|ollama)[^"']*["']\s*\)|\bimport\s+(?:OpenAI|Anthropic|GoogleGenerativeAI|Mistral|Cohere|Groq|Ollama))/;
39
+
40
+ // Environment / config signals that a file is talking to an LLM endpoint
41
+ // even when no SDK is imported (Ollama-style direct HTTP).
42
+ const LLM_ENDPOINT_SIGNAL_RE = /(?:\b(?:OLLAMA_(?:BASE_URL|HOST)|ollama_url|openai_url|llm_url|LITELLM_|HUGGINGFACE_(?:HUB_)?TOKEN|HF_TOKEN|GROQ_API|TOGETHER_API|MISTRAL_API|ANTHROPIC_API|OPENAI_API)\b|\/api\/(?:generate|chat|embeddings)\b|\/v1\/(?:chat\/completions|completions|messages|embeddings)\b)/;
43
+
44
+ // HTTP-tainted source (JS/TS + Python frameworks)
45
+ const HTTP_TAINT_RHS_RE = /\b(?:req|request|ctx|c)\.(?:body|query|params|headers|cookies|files|url|originalUrl|rawBody)\b|\bsearchParams\b|\bawait\s+(?:request|req)\.(?:json|text|formData)\b/;
46
+ const PY_HTTP_TAINT_RHS_RE = /\b(?:flask\.)?request\.(?:args|form|json|values|files|headers|data|cookies|get_json)\b|\brequest\.GET\b|\brequest\.POST\b|\bbody\s*=\s*await\s+request/;
47
+
48
+ // External / indirect taint (file, network, db) — for indirect prompt injection
49
+ const EXT_TAINT_RHS_RE = /\b(?:fetch|axios\.(?:get|post|request)|https?\.get|fs\.readFileSync|fs\.readFile|fs\.promises\.readFile|fs\.createReadStream|readFileSync|open\s*\(.*['"][^'"]+['"]\s*,\s*['"]r)/;
50
+ const PY_EXT_TAINT_RHS_RE = /\b(?:requests\.(?:get|post)|urlopen|httpx\.(?:get|post)|open\s*\(.*['"](?:r|rb)['"]\)\.read)/;
51
+
52
+ // Dangerous LLM tool names (LLM-callable functions with strong side-effects)
53
+ const DANGEROUS_TOOL_NAME_RE = /\bname\s*[:=]\s*["'](shell|bash|exec|execute|execute_shell|run_command|run_shell|run_code|sandbox_exec|eval|eval_python|python_exec|sql|sql_query|execute_sql|raw_query|query_db|read_file|write_file|delete_file|file_write|file_delete|edit_file|fetch_url|http_request|web_request|browse_url|navigate|delete|drop_table|admin|sudo|root|kubectl|docker_exec)["']/i;
54
+
55
+ // Output-rendering sinks that turn LLM text into HTML / response body
56
+ const UNSAFE_HTML_SINK_RE = /(?:\.innerHTML\s*=|dangerouslySetInnerHTML\s*=|document\.write\s*\(|\.outerHTML\s*=|v-html\s*=|\$\{\s*[A-Za-z_$][\w$]*\s*\}\s*<\/)/;
57
+
58
+ // Variables likely holding an LLM response (LHS-side patterns)
59
+ const LLM_OUTPUT_LHS_RE = [
60
+ /\b(?:const|let|var)\s+(\w+)\s*=\s*[^;]*?\.(?:content\s*\[\s*0\s*\]\.text|completion|message\.content|choices\s*\[\s*0\s*\]\.message\.content|generated_text|output_text|text\s*\(\s*\))\b/,
61
+ /\b(?:const|let|var)\s+\{\s*(?:text|content|completion|message)\s*:\s*(\w+)\b/,
62
+ /\b(?:const|let|var)\s+(\w+)\s*=\s*await\s+(?:anthropic|openai|client|llm|model|chain|agent|generateText|streamText|generateObject|streamObject)\b/,
63
+ // Python: reply = response.choices[0].message.content
64
+ /^\s*(\w+)\s*=\s*(?:[A-Za-z_]\w*)\.choices\s*\[\s*0\s*\]\.message\.content\b/m,
65
+ /^\s*(\w+)\s*=\s*(?:response|completion|reply)\.content\s*\[\s*0\s*\]\.text\b/m,
66
+ ];
67
+
68
+ // System-prompt leakage via response body / log (PI-5)
69
+ const SYSTEM_PROMPT_LEAK_RE = /(?:res\.(?:json|send)\s*\([^)]*?\b(?:messages|systemPrompt|system_prompt|systemMessage)\b|console\.log\s*\([^)]*?\b(?:systemPrompt|system_prompt|messages)\b)/;
70
+
71
+ const TOOL_CONTEXT_RE = /\b(?:tools?\s*[:=]|tool_choice|tool_calls?|input_schema|function_declarations|parameters\s*:\s*\{|strict\s*:\s*true)\b/;
72
+
73
+ function _hasLLMSink(line) {
74
+ return LLM_SINK_PATTERNS.some(re => re.test(line));
75
+ }
76
+
77
+ function _collectLHSAssignments(lines, regexList) {
78
+ const map = new Map();
79
+ for (let i = 0; i < lines.length; i++) {
80
+ const ln = lines[i];
81
+ for (const re of regexList) {
82
+ const m = ln.match(re);
83
+ if (m && m[1]) map.set(m[1], { line: i + 1, kind: 'llm-output' });
84
+ }
85
+ }
86
+ return map;
87
+ }
88
+
89
+ function _collectTaintedVars(lines) {
90
+ const vars = new Map();
91
+ for (let i = 0; i < lines.length; i++) {
92
+ const ln = lines[i];
93
+ // JS: const x = req.body.foo
94
+ let m = ln.match(/\b(?:const|let|var)\s+(\w+)\s*=[^;]*?(?:req|request|ctx|c)\.(?:body|query|params|headers|cookies)\b/);
95
+ if (m) vars.set(m[1], { line: i + 1, kind: 'http' });
96
+ // JS: destructured const { foo, bar } = req.body
97
+ m = ln.match(/\b(?:const|let|var)\s+\{([^}]+)\}\s*=[^;]*?(?:req|request|ctx|c)\.(?:body|query|params|headers|cookies)\b/);
98
+ if (m) for (const n of m[1].split(',')) {
99
+ const id = n.split(':').pop().trim().replace(/[^A-Za-z0-9_$]/g, '');
100
+ if (id) vars.set(id, { line: i + 1, kind: 'http' });
101
+ }
102
+ // JS: external/indirect (fetch, fs, db)
103
+ m = ln.match(/\b(?:const|let|var)\s+(\w+)\s*=[^;]*?(?:fetch|axios|fs\.readFile|fs\.readFileSync|\.query\s*\(|\.findOne\s*\()/);
104
+ if (m && /await|\.then|\.text\(|\.json\(|fs\.read/i.test(ln)) vars.set(m[1], { line: i + 1, kind: 'external' });
105
+ // Python: x = request.args.get(...) / request.json
106
+ m = ln.match(/^\s*(\w+)\s*=\s*(?:flask\.)?request\.(?:args|form|json|values|files|headers|data)\b/);
107
+ if (m) vars.set(m[1], { line: i + 1, kind: 'http' });
108
+ // Python: x = requests.get(...).text / open(...).read()
109
+ m = ln.match(/^\s*(\w+)\s*=\s*(?:requests\.(?:get|post)|urlopen|open\s*\()/);
110
+ if (m) vars.set(m[1], { line: i + 1, kind: 'external' });
111
+ }
112
+ return vars;
113
+ }
114
+
115
+ function _ctxWindow(lines, startLine, span) {
116
+ const start = Math.max(0, startLine);
117
+ const end = Math.min(lines.length, startLine + span);
118
+ return { text: lines.slice(start, end).join('\n'), start, end };
119
+ }
120
+
121
+ export function scanLLM(fp, raw) {
122
+ if (!_SCANNABLE_EXT_RE.test(fp)) return [];
123
+ const fpNorm = fp.replace(/\\/g, '/');
124
+ if (_NONPROD_PATH_RE.test(fpNorm)) return [];
125
+ if (!raw || raw.length > 500_000) return [];
126
+
127
+ const hasImport = LLM_IMPORT_RE.test(raw);
128
+ const hasAnyStrictSink = LLM_SINK_PATTERNS.some(re => re.test(raw));
129
+ const hasEndpointSignal = LLM_ENDPOINT_SIGNAL_RE.test(raw);
130
+ if (!hasImport && !hasAnyStrictSink && !hasEndpointSignal) return [];
131
+
132
+ const lines = raw.split('\n');
133
+ const findings = [];
134
+ const taintedVars = _collectTaintedVars(lines);
135
+ const llmOutVars = _collectLHSAssignments(lines, LLM_OUTPUT_LHS_RE);
136
+ const seen = new Set();
137
+
138
+ // Classify the *position* a tainted variable occupies inside the LLM call window.
139
+ // Returns one of: 'unsafe' (system/instruction position or interpolation),
140
+ // 'safe-user-role' (pure user-role content slot),
141
+ // 'unknown' (referenced but position unclear).
142
+ function _positionForVar(ctxLines, vn) {
143
+ const wordRe = new RegExp(`\\b${vn}\\b`);
144
+ const tplRe = new RegExp(`\\$\\{[^}]*\\b${vn}\\b[^}]*\\}`);
145
+ const concatRe = new RegExp(`['"]\\s*\\+\\s*${vn}\\b|\\b${vn}\\s*\\+\\s*['"]`);
146
+ const sysFieldRe = new RegExp(`\\b(?:system|instructions|preamble)\\s*:\\s*${vn}\\b`);
147
+ let referenced = false, looksUnsafe = false, anySafeUser = false;
148
+ for (let i = 0; i < ctxLines.length; i++) {
149
+ const ln = ctxLines[i];
150
+ if (!wordRe.test(ln)) continue;
151
+ referenced = true;
152
+ // Interpolation or string concat anywhere = injection-prone
153
+ if (tplRe.test(ln) || concatRe.test(ln)) { looksUnsafe = true; break; }
154
+ // Direct assignment into `system:` / `instructions:` field
155
+ if (sysFieldRe.test(ln)) { looksUnsafe = true; break; }
156
+ // `content: <var>` immediately following a `role: 'user'` line/clause
157
+ const looksContent = new RegExp(`\\bcontent\\s*:\\s*${vn}\\b`).test(ln);
158
+ if (looksContent) {
159
+ const within = (ctxLines[i - 1] || '') + ' ' + ln;
160
+ if (/role\s*:\s*['"]user['"]/.test(within)) anySafeUser = true;
161
+ }
162
+ // Surrounding system-role marker on adjacent lines
163
+ const win = (ctxLines[i - 1] || '') + ' ' + ln;
164
+ if (/role\s*:\s*['"]system['"]/.test(win) && new RegExp(`\\bcontent\\s*:\\s*${vn}\\b`).test(ln)) {
165
+ looksUnsafe = true; break;
166
+ }
167
+ }
168
+ if (!referenced) return 'absent';
169
+ if (looksUnsafe) return 'unsafe';
170
+ if (anySafeUser) return 'safe-user-role';
171
+ return 'unknown';
172
+ }
173
+
174
+ // Pass 1 — direct & indirect prompt injection at LLM sinks.
175
+ for (let li = 0; li < lines.length; li++) {
176
+ const line = lines[li];
177
+ const strictSink = _hasLLMSink(line);
178
+ const fuzzySink = !strictSink && (hasImport || hasEndpointSignal) && LLM_FUZZY_CALL_RE.test(line);
179
+ if (!strictSink && !fuzzySink) continue;
180
+
181
+ const { text: ctx } = _ctxWindow(lines, li, 18);
182
+ const ctxLines = ctx.split('\n');
183
+
184
+ // PI-1a: tainted variable referenced inside the call window.
185
+ let matched = false;
186
+ for (const [vn, info] of taintedVars) {
187
+ if (info.line > li + 18) continue; // forward refs we don't trust
188
+ const pos = _positionForVar(ctxLines, vn);
189
+ if (pos === 'absent' || pos === 'safe-user-role') continue;
190
+ // 'unknown' is a precision risk — only flag if the variable is HTTP-tainted
191
+ // AND the call uses fuzzy-sink shape; otherwise skip.
192
+ if (pos === 'unknown' && (info.kind !== 'http' || !strictSink)) continue;
193
+ const id = `llm-pi:${fp}:${li + 1}:${vn}:${info.kind}`;
194
+ if (seen.has(id)) continue;
195
+ seen.add(id);
196
+ const severity = info.kind === 'http' ? 'high' : 'medium';
197
+ const vuln = info.kind === 'http'
198
+ ? 'Prompt Injection (HTTP user input in LLM call)'
199
+ : 'Indirect Prompt Injection (external content in LLM call)';
200
+ findings.push({
201
+ id, kind: 'sast', severity, vuln,
202
+ cwe: 'CWE-1427', stride: 'Tampering',
203
+ file: fp, line: li + 1, snippet: line.trim(),
204
+ chain: [
205
+ { type: 'source', label: `Tainted (${info.kind}): ${vn}`, line: info.line },
206
+ { type: 'sink', label: 'LLM call (' + (pos === 'unsafe' ? 'system/instruction position' : 'unknown position') + ')', line: li + 1, snippet: line.trim() },
207
+ ],
208
+ fix: info.kind === 'http'
209
+ ? 'Pass user input only as a user-role message. Never interpolate it into the system prompt or instructions string.'
210
+ : 'Sanitize and bound external content before embedding in a prompt; consider tagging it with explicit "untrusted data" delimiters and instruction-defense system messages.',
211
+ confidence: pos === 'unsafe' ? (strictSink ? 0.92 : 0.7) : 0.55,
212
+ });
213
+ matched = true;
214
+ }
215
+
216
+ // PI-1b: template-literal user input directly in the call (no intermediate var)
217
+ if (!matched && /\$\{[^}]*\b(?:req|request|ctx|c)\.(?:body|query|params|headers|cookies)\b/.test(ctx)) {
218
+ const id = `llm-pi:${fp}:${li + 1}:template-http`;
219
+ if (!seen.has(id)) {
220
+ seen.add(id);
221
+ findings.push({
222
+ id, kind: 'sast', severity: 'high',
223
+ vuln: 'Prompt Injection (template-literal user input)',
224
+ cwe: 'CWE-1427', stride: 'Tampering',
225
+ file: fp, line: li + 1, snippet: line.trim(),
226
+ chain: [
227
+ { type: 'source', label: 'HTTP user input (inline ${...})', line: li + 1 },
228
+ { type: 'sink', label: 'LLM call (template literal)', line: li + 1, snippet: line.trim() },
229
+ ],
230
+ fix: 'Move user input out of the system prompt. Keep instructions separate from data; pass user content as a discrete user-role message.',
231
+ confidence: 0.88,
232
+ });
233
+ }
234
+ }
235
+ }
236
+
237
+ // Pass 2 — dangerous LLM tool definitions.
238
+ for (let li = 0; li < lines.length; li++) {
239
+ const line = lines[li];
240
+ if (!DANGEROUS_TOOL_NAME_RE.test(line)) continue;
241
+ // Require nearby tool-context evidence to avoid false matches on plain identifiers
242
+ const { text: wider } = _ctxWindow(lines, Math.max(0, li - 6), 14);
243
+ if (!TOOL_CONTEXT_RE.test(wider)) continue;
244
+ const m = line.match(DANGEROUS_TOOL_NAME_RE);
245
+ const toolName = m ? m[1] : 'dangerous';
246
+ const id = `llm-pi:${fp}:${li + 1}:tool:${toolName}`;
247
+ if (seen.has(id)) continue;
248
+ seen.add(id);
249
+ findings.push({
250
+ id, kind: 'sast', severity: 'high',
251
+ vuln: `Insecure LLM Tool Definition: ${toolName}`,
252
+ cwe: 'CWE-77', stride: 'Elevation of Privilege',
253
+ file: fp, line: li + 1, snippet: line.trim(),
254
+ fix: `Restrict the "${toolName}" tool to an explicit allowlist. Validate every argument against a schema; never let the LLM pass arbitrary input to shell, SQL, filesystem, or network primitives.`,
255
+ confidence: 0.8,
256
+ });
257
+ }
258
+
259
+ // Pass 3 — LLM output rendered to an unsafe HTML sink (XSS via LLM).
260
+ for (let li = 0; li < lines.length; li++) {
261
+ if (!UNSAFE_HTML_SINK_RE.test(lines[li])) continue;
262
+ for (const [vn, info] of llmOutVars) {
263
+ if (new RegExp(`\\b${vn}\\b`).test(lines[li])) {
264
+ const id = `llm-pi:${fp}:${li + 1}:${vn}:output-xss`;
265
+ if (seen.has(id)) continue;
266
+ seen.add(id);
267
+ findings.push({
268
+ id, kind: 'sast', severity: 'high',
269
+ vuln: 'Unsanitized LLM Output Rendered as HTML',
270
+ cwe: 'CWE-79', stride: 'Tampering',
271
+ file: fp, line: li + 1, snippet: lines[li].trim(),
272
+ chain: [
273
+ { type: 'source', label: `LLM response: ${vn}`, line: info.line },
274
+ { type: 'sink', label: 'HTML / DOM sink', line: li + 1, snippet: lines[li].trim() },
275
+ ],
276
+ fix: 'Render LLM output as text. If HTML is required, sanitize with DOMPurify or a server-side sanitizer with a strict allowlist.',
277
+ confidence: 0.85,
278
+ });
279
+ }
280
+ }
281
+ }
282
+
283
+ // Pass 4 — system-prompt leakage to user response or logs.
284
+ for (let li = 0; li < lines.length; li++) {
285
+ if (SYSTEM_PROMPT_LEAK_RE.test(lines[li])) {
286
+ const id = `llm-pi:${fp}:${li + 1}:sys-prompt-leak`;
287
+ if (seen.has(id)) continue;
288
+ seen.add(id);
289
+ findings.push({
290
+ id, kind: 'sast', severity: 'medium',
291
+ vuln: 'System Prompt / Message History Disclosure',
292
+ cwe: 'CWE-200', stride: 'Information Disclosure',
293
+ file: fp, line: li + 1, snippet: lines[li].trim(),
294
+ fix: 'Do not return system prompts, tool schemas, or full message arrays to clients. Strip these fields before responding.',
295
+ confidence: 0.7,
296
+ });
297
+ }
298
+ }
299
+
300
+ return findings;
301
+ }
302
+
303
+ export const _LLM_INTERNAL = {
304
+ LLM_SINK_PATTERNS, LLM_IMPORT_RE, LLM_ENDPOINT_SIGNAL_RE,
305
+ HTTP_TAINT_RHS_RE, PY_HTTP_TAINT_RHS_RE,
306
+ EXT_TAINT_RHS_RE, PY_EXT_TAINT_RHS_RE,
307
+ DANGEROUS_TOOL_NAME_RE, UNSAFE_HTML_SINK_RE, SYSTEM_PROMPT_LEAK_RE,
308
+ };
@@ -0,0 +1,140 @@
1
+ // Business-logic flaw detector — high-precision pattern layer.
2
+ //
3
+ // Logic flaws normally require semantic reasoning, but a handful of canonical
4
+ // anti-patterns are unambiguous in source: an admin check that uses `||` so it
5
+ // always passes, a TOCTOU `existsSync → readFile` sequence, a price field
6
+ // trusted from the request body in a DB write. We cover those here so the
7
+ // engine emits findings on them without needing an agent. Deeper logic review
8
+ // (intent vs. implementation) is delegated to the security-logic-reviewer agent.
9
+
10
+ const _NONPROD_PATH_RE = /(?:^|\/)(?:tests?|__tests__|spec|fixtures?|examples?|docs?|stories|codefixes|node_modules)\//i;
11
+ const _SCAN_EXT_RE = /\.(?:js|jsx|ts|tsx|mjs|cjs|py|rb|go|java|php)$/i;
12
+
13
+ // PATTERN A — always-true authorization clause.
14
+ // Examples: if (user.isAdmin || user.id) -> user.id is always truthy
15
+ // if (req.user.role === 'admin' || true) -> obvious
16
+ // if (isAdmin || isUser) -> isUser is always truthy in an authed context
17
+ const ALWAYS_TRUE_AUTH_RE = /\bif\s*\(\s*(?:[A-Za-z_$][\w$.]*\.(?:isAdmin|admin|isOwner|hasRole|role)\b[^)]*?)\|\|\s*(?:true\b|[A-Za-z_$][\w$.]*\.(?:id|userId|uid|email|user)\b\s*\)|true\b)/;
18
+
19
+ // PATTERN B — TOCTOU on filesystem. existsSync → readFile / open / unlink at a related path.
20
+ const TOCTOU_EXISTS_THEN_OP_RE = /\bfs\.existsSync\s*\(\s*([^)]+?)\s*\)[\s\S]{1,300}?\bfs\.(?:readFile(?:Sync)?|writeFile(?:Sync)?|unlink(?:Sync)?|open(?:Sync)?|createReadStream|createWriteStream)\s*\(\s*\1/;
21
+
22
+ // PATTERN C — client-controlled monetary field flowing into a DB write.
23
+ const CLIENT_AMOUNT_TO_DB_RE = /(?:price|amount|total|subtotal|cost|fee|charge|payment|sum)\s*[:=]\s*(?:req|request)\.body\b/;
24
+
25
+ // PATTERN D — admin / role / isAdmin set from request body (mass-assignment of privilege).
26
+ const PRIV_FROM_BODY_RE = /\b(?:isAdmin|is_admin|admin|role|roles|permissions|scopes|tier|isOwner|is_owner)\s*[:=]\s*(?:req|request)\.body\.[A-Za-z_]\w*/;
27
+
28
+ // PATTERN E — state transition without prior-state guard.
29
+ // Heuristic: `<obj>.status = 'completed' / 'paid' / 'shipped' / 'approved'` immediately
30
+ // after a fetch with no surrounding `if (... .status === ...)` check in the same function block.
31
+ const STATE_TERMINAL_SET_RE = /\b(\w+)\.(?:status|state|stage)\s*=\s*['"](?:completed|complete|paid|shipped|approved|active|published|verified|confirmed)['"]/;
32
+
33
+ // PATTERN F — coupon / discount applied without server-side lookup.
34
+ // Match either `discount = req.body...` (assignment) or `req.body.discount` (direct read).
35
+ const CLIENT_DISCOUNT_RE = /(?:discount|coupon|promo|voucher)\s*[:=]\s*(?:req|request)\.body\b|(?:req|request)\.body\.(?:discount|coupon|promo|voucher)\b/i;
36
+
37
+ // PATTERN G — duplicate-create / missing idempotency.
38
+ // Fires when a POST handler does an INSERT/.create()/.save() with no upstream
39
+ // SELECT-or-find-by-key check inside the same handler.
40
+ //
41
+ // Implemented in the per-route pass below.
42
+
43
+ function _ctxFn(lines, atIdx, span = 25) {
44
+ const start = Math.max(0, atIdx - Math.floor(span / 2));
45
+ const end = Math.min(lines.length, atIdx + Math.ceil(span / 2));
46
+ return lines.slice(start, end).join('\n');
47
+ }
48
+
49
+ function _emit(fp, vuln, line, snippet, severity, cwe, fix, confidence) {
50
+ return {
51
+ id: `logic:${fp}:${line}:${vuln.replace(/\s/g, '_').slice(0, 60)}`,
52
+ kind: 'logic', severity, vuln,
53
+ cwe: cwe || null, stride: 'Tampering',
54
+ file: fp, line, snippet: snippet.trim(),
55
+ fix, confidence,
56
+ };
57
+ }
58
+
59
+ export function scanBusinessLogic(fp, raw) {
60
+ if (!_SCAN_EXT_RE.test(fp)) return [];
61
+ const fpNorm = fp.replace(/\\/g, '/');
62
+ if (_NONPROD_PATH_RE.test(fpNorm)) return [];
63
+ if (!raw || raw.length > 500_000) return [];
64
+
65
+ const lines = raw.split('\n');
66
+ const findings = [];
67
+ const seen = new Set();
68
+
69
+ // Single-line patterns
70
+ for (let li = 0; li < lines.length; li++) {
71
+ const line = lines[li];
72
+
73
+ // PATTERN A
74
+ if (ALWAYS_TRUE_AUTH_RE.test(line)) {
75
+ const f = _emit(fp, 'Always-True Authorization Clause',
76
+ li + 1, line, 'high', 'CWE-285',
77
+ 'The `||` short-circuit makes this check pass for any authenticated user. Use `&&` to require BOTH conditions, or split into explicit role checks.',
78
+ 0.9);
79
+ if (!seen.has(f.id)) { seen.add(f.id); findings.push(f); }
80
+ }
81
+
82
+ // PATTERN C — client-controlled money
83
+ if (CLIENT_AMOUNT_TO_DB_RE.test(line)) {
84
+ const f = _emit(fp, 'Client-Controlled Monetary Field',
85
+ li + 1, line, 'high', 'CWE-841',
86
+ 'Recompute price/amount on the server from authoritative records (catalog, cart, subscription tier). Never accept the final amount from the request body.',
87
+ 0.85);
88
+ if (!seen.has(f.id)) { seen.add(f.id); findings.push(f); }
89
+ }
90
+
91
+ // PATTERN D — privilege from body
92
+ if (PRIV_FROM_BODY_RE.test(line)) {
93
+ const f = _emit(fp, 'Privilege Field Set from Request Body',
94
+ li + 1, line, 'critical', 'CWE-915',
95
+ 'Strip privilege fields from the request body before assignment, or use an explicit allowlist of mutable fields. An attacker can post `isAdmin:true` and elevate.',
96
+ 0.9);
97
+ if (!seen.has(f.id)) { seen.add(f.id); findings.push(f); }
98
+ }
99
+
100
+ // PATTERN F — client-controlled discount/coupon
101
+ if (CLIENT_DISCOUNT_RE.test(line)) {
102
+ const f = _emit(fp, 'Client-Controlled Discount/Coupon',
103
+ li + 1, line, 'medium', 'CWE-840',
104
+ 'Look up the coupon by code on the server and validate redemption status, expiry, and per-user use-count. Do not trust the discount value sent by the client.',
105
+ 0.8);
106
+ if (!seen.has(f.id)) { seen.add(f.id); findings.push(f); }
107
+ }
108
+
109
+ // PATTERN E — terminal state set without prior-state guard.
110
+ // Heuristic: look upward in a 25-line window for an `if (... .status === ...)` guard;
111
+ // suppress when one is present.
112
+ const stateM = line.match(STATE_TERMINAL_SET_RE);
113
+ if (stateM) {
114
+ const objName = stateM[1];
115
+ const ctxAbove = lines.slice(Math.max(0, li - 25), li).join('\n');
116
+ const guardRe = new RegExp(`if\\s*\\([^)]*\\b${objName}\\.(?:status|state|stage)\\s*(?:===|==|!==|!=)\\s*['"]`, 'i');
117
+ if (!guardRe.test(ctxAbove)) {
118
+ const f = _emit(fp, 'Terminal State Set Without Prior-State Guard',
119
+ li + 1, line, 'medium', 'CWE-840',
120
+ 'Verify the current state before transitioning to a terminal state (e.g., assert order.status === "pending" before setting to "paid"). Without this guard an attacker can replay or skip required steps.',
121
+ 0.7);
122
+ if (!seen.has(f.id)) { seen.add(f.id); findings.push(f); }
123
+ }
124
+ }
125
+ }
126
+
127
+ // Multi-line: TOCTOU
128
+ let m;
129
+ const toctouRe = new RegExp(TOCTOU_EXISTS_THEN_OP_RE.source, 'g');
130
+ while ((m = toctouRe.exec(raw))) {
131
+ const line = raw.substring(0, m.index).split('\n').length;
132
+ const f = _emit(fp, 'TOCTOU: existsSync followed by file op',
133
+ line, lines[line - 1] || '', 'medium', 'CWE-367',
134
+ 'Replace the check-then-act sequence with a single atomic operation (e.g., `fs.open` with appropriate flags). Between `existsSync` and the file op the file can be replaced by a symlink or removed.',
135
+ 0.85);
136
+ if (!seen.has(f.id)) { seen.add(f.id); findings.push(f); }
137
+ }
138
+
139
+ return findings;
140
+ }