@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,138 @@
1
+ // HTTP Response Splitting / CRLF Injection (CWE-113).
2
+ //
3
+ // Pattern: a header value or `Location:` body is set from user input
4
+ // without stripping CR/LF. The attacker injects `\r\n\r\n<html>…` and
5
+ // splits the response, turning a single response into two — the second
6
+ // one fully attacker-controlled. Same root mechanism as open-redirect
7
+ // when the location is concatenated, but the consequence is broader
8
+ // (cache poisoning, XSS via injected body, session fixation).
9
+ //
10
+ // We catch:
11
+ // - JS/Node: res.setHeader('X-…', userValue)
12
+ // res.set('X-…', userValue)
13
+ // ctx.response.set('X-…', userValue)
14
+ // - Java: response.setHeader(…, name)
15
+ // response.addHeader(…, name)
16
+ // - Python: response['X-…'] = userValue (Django)
17
+ // response.headers['X-…'] = userValue (Flask)
18
+ // - PHP: header("X-…: " . $_GET[...]) (also caught by open-redirect
19
+ // when Location:; this rule catches non-Location headers)
20
+ //
21
+ // Suppression: if the user value passes through a CRLF strip / regex
22
+ // filter / a known sanitizer (`escape`, `URLEncoder.encode`, `quote_plus`)
23
+ // in the preceding 30 lines, no finding.
24
+
25
+ import { blankComments } from './_comment-strip.js';
26
+
27
+ const PATTERNS = [
28
+ // res.setHeader('name', val) | res.set('name', val) | ctx.response.set(…)
29
+ ['js', /\b(?:res|reply|response|ctx\.response)\s*\.\s*(?:setHeader|set|append)\s*\(\s*[`"'][^`"']+[`"']\s*,\s*([^)]+?)\s*\)/g, 'res.setHeader'],
30
+ // response.setHeader / response.addHeader (Java Servlet API). Accept any
31
+ // identifier as the receiver — `resp`, `response`, `httpResponse`, etc.
32
+ ['java', /\b(?:[A-Za-z_][\w]*)\s*\.\s*(?:setHeader|addHeader)\s*\(\s*"[^"]+"\s*,\s*([^)]+?)\s*\)/g, 'setHeader'],
33
+ // Django/Flask response.headers['X-…'] = userValue
34
+ ['py', /\b(?:response|resp|r)\s*(?:\.\s*headers)?\s*\[\s*['"][^'"]+['"]\s*\]\s*=\s*([^\n;]+)/g, 'response.headers[…] = …'],
35
+ // PHP header("X-Foo: " . $_GET[…]) (Location: variants handled by open-redirect)
36
+ ['php', /\bheader\s*\(\s*['"](?!Location)([^'"]+):\s*['"]\s*\.\s*(\$\w[\w\[\]'"]*)/g, 'PHP header()'],
37
+ ];
38
+
39
+ const TAINT_HINT_RE =
40
+ /\b(?:req\.|request\.|params\.|query\.|body\.|ctx\.query|ctx\.request|reply\.query|c\.Query|r\.URL\.Query|_GET|_POST|_REQUEST|getParameter|getHeader)\b/;
41
+
42
+ const SANITIZER_PATTERNS = [
43
+ /\.replace\s*\(\s*\/\\r\?\\?n/i, // .replace(/\r?\n/, '')
44
+ /\.replace\s*\(\s*\/[\[]\\r\\n[\]]/i, // .replace(/[\r\n]/, '')
45
+ /\.replaceAll\s*\(\s*['"]\\r?\\?n?['"]/,
46
+ /\bstripNewlines\b/i,
47
+ /\bsanitizeHeader\b/i,
48
+ /\bescapeCRLF\b/i,
49
+ /\bURLEncoder\s*\.\s*encode\b/,
50
+ /\bquote_plus\s*\(/,
51
+ /\bencodeURIComponent\s*\(/,
52
+ /\bquote\s*\(/,
53
+ // Explicit CRLF reject patterns — any reference to \r\n inside a
54
+ // .matches / regex check, paired with throw/return earlier in the file.
55
+ /\.matches\s*\([^)]*\\\\?r\\\\?n/, // .matches(... \r\n ...)
56
+ /\babort\s*\(\s*4\d\d/, // abort(4xx) earlier
57
+ /\bres\s*\.\s*status\s*\(\s*4\d\d\b/,
58
+ /\bthrow\s+new\s+\w+Exception\s*\([^)]*(?:header|crlf|newline)/i,
59
+ ];
60
+
61
+ function _lineOf(raw, idx) { return raw.substring(0, idx).split('\n').length; }
62
+ function _lang(fp) {
63
+ if (/\.(?:js|jsx|ts|tsx|mjs|cjs)$/i.test(fp)) return 'js';
64
+ if (/\.py$/i.test(fp)) return 'py';
65
+ if (/\.java$/i.test(fp)) return 'java';
66
+ if (/\.php$/i.test(fp)) return 'php';
67
+ return null;
68
+ }
69
+
70
+ function _looksSanitizedAbove(raw, callLine) {
71
+ const lines = raw.split('\n');
72
+ const lo = Math.max(0, callLine - 30);
73
+ const before = lines.slice(lo, callLine).join('\n');
74
+ for (const p of SANITIZER_PATTERNS) if (p.test(before)) return true;
75
+ return false;
76
+ }
77
+
78
+ export function scanResponseSplitting(fp, raw) {
79
+ if (!raw || raw.length > 500_000) return [];
80
+ const lang = _lang(fp);
81
+ if (!lang) return [];
82
+ const code = blankComments(raw, lang === 'py' ? 'py' : undefined);
83
+ if (!/\b(?:setHeader|addHeader|response\s*\.\s*headers|response\s*\[|reply\s*\.\s*set|\bheader\s*\()/i.test(code)) return [];
84
+ const findings = [];
85
+ const seen = new Set();
86
+ for (const [plang, pat, label] of PATTERNS) {
87
+ if (plang !== lang) continue;
88
+ const re = new RegExp(pat.source, pat.flags);
89
+ let m;
90
+ while ((m = re.exec(code))) {
91
+ const value = (m[1] || '').trim();
92
+ let tainted = TAINT_HINT_RE.test(value);
93
+ // Java-handler-context heuristic: if the receiver looks like an
94
+ // HttpServletResponse and `value` is a plain ident that appears in
95
+ // the enclosing method's param list, treat it as user-derived. Same
96
+ // signature pattern as a real SAST tool's "request-scope param" rule.
97
+ if (!tainted && plang === 'java' && /^[A-Za-z_][\w]*$/.test(value)) {
98
+ const callIdx = m.index;
99
+ // Find the start of the enclosing method by walking back to the
100
+ // most recent `(...)`-style signature followed by `{`.
101
+ const before = code.slice(0, callIdx);
102
+ const sigRe = /\b(?:public|private|protected|static)[^;{]*\(([^)]*)\)\s*(?:throws[^{]+)?\{/g;
103
+ let sig = null;
104
+ let sm;
105
+ while ((sm = sigRe.exec(before)) !== null) sig = sm;
106
+ if (sig) {
107
+ const params = sig[1].split(',').map(p => (p.trim().split(/\s+/).pop() || '').trim());
108
+ if (params.includes(value)) tainted = true;
109
+ }
110
+ }
111
+ if (!tainted) continue;
112
+ const line = _lineOf(raw, m.index);
113
+ if (_looksSanitizedAbove(raw, line)) continue;
114
+ const id = `response-splitting:${fp}:${line}:${label}`;
115
+ if (seen.has(id)) continue;
116
+ seen.add(id);
117
+ findings.push({
118
+ id,
119
+ file: fp, line,
120
+ vuln: `HTTP Response Splitting / CRLF Injection (${label})`,
121
+ severity: 'high',
122
+ cwe: 'CWE-113',
123
+ family: 'response-splitting',
124
+ stride: 'Tampering',
125
+ snippet: (raw.split('\n')[line - 1] || '').trim().slice(0, 200),
126
+ remediation:
127
+ 'Strip or reject CR/LF characters before writing the value into a response header. ' +
128
+ 'Better: validate the value against a strict pattern (alnum + a small set of punctuation) and reject anything else. ' +
129
+ 'Java Servlet: prefer `response.setHeader(name, URLEncoder.encode(value, "UTF-8"))` or normalize via a helper. ' +
130
+ 'Node: `value.replace(/[\\r\\n]/g, "")` before `res.setHeader`. ' +
131
+ 'Django/Flask: assign through a setter that sanitizes; never raw `response[".."] = userInput`.',
132
+ parser: 'RESPONSE-SPLITTING',
133
+ confidence: 0.85,
134
+ });
135
+ }
136
+ }
137
+ return findings;
138
+ }
@@ -0,0 +1,108 @@
1
+ import { blankComments } from './_comment-strip.js';
2
+ // Ruby-specific detectors. Targets Rails idioms and the eval-family methods
3
+ // that make Ruby code easy to compromise when fed untrusted input.
4
+ //
5
+ // - User input into eval / instance_eval / class_eval / module_eval
6
+ // - send / public_send with a user-controlled method name
7
+ // - Marshal.load on user input
8
+ // - YAML.load (not safe_load) on user input
9
+ // - ERB.new(...).result on user input
10
+ // - Open / `` (backtick) with user input (command injection)
11
+ // - Rails: attributes = params (without strong_params)
12
+ // - Open::URI.open(user_url) — SSRF
13
+ // - File.read(params[...]) — path traversal
14
+
15
+ const RE = {
16
+ evalFamily: /\b(?:eval|instance_eval|class_eval|module_eval)\s*\(?\s*(?:params|request|@\w+\.params|cookies|session)\b/g,
17
+ send: /\.(?:send|public_send)\s*\(\s*(?:params|request|@\w+\.params)\b/g,
18
+ marshalLoad: /\bMarshal\s*\.\s*load\s*\(\s*(?:params|request|@\w+\.params|cookies|session)\b/g,
19
+ yamlUnsafe: /\bYAML\s*\.\s*load\s*\(\s*(?:params|request|@\w+\.params|cookies|session)\b/g,
20
+ erbResult: /\bERB\.new\s*\(\s*(?:params|request|@\w+\.params)\b[^)]*\)\s*\.\s*result/g,
21
+ backtick: /`[^`]*#\{[^}]*\b(?:params|request|@\w+\.params)\b[^}]*\}/g,
22
+ systemUser: /\b(?:system|exec|Open3\.capture\d*|IO\.popen)\s*\(\s*[^)]*\b(?:params|request|@\w+\.params)\b/g,
23
+ attributesEq: /\.\s*attributes\s*=\s*params\b(?!\s*\.permit)/g,
24
+ openSsrf: /\b(?:open|URI\.open|URI\.parse\([^)]*\)\.read)\s*\(\s*(?:params|request|@\w+\.params)\b/g,
25
+ fileRead: /\bFile\s*\.\s*(?:read|open|new|readlines)\s*\(\s*params\s*\[/g,
26
+ };
27
+
28
+ function lineOf(raw, idx) { return raw.substring(0, idx).split('\n').length; }
29
+
30
+ export function scanRuby(fp, raw) {
31
+ if (!/\.rb$/i.test(fp)) return [];
32
+ if (!raw || raw.length > 500_000) return [];
33
+ // Ruby's comment char is # (same as Python) — use the py stripper.
34
+ const code = blankComments(raw, 'py');
35
+ const findings = [];
36
+ const seen = new Set();
37
+ const push = (f) => { if (!seen.has(f.id)) { seen.add(f.id); findings.push(f); } };
38
+
39
+ for (const [key, re] of Object.entries(RE)) {
40
+ const r = new RegExp(re.source, re.flags);
41
+ let m;
42
+ while ((m = r.exec(code))) {
43
+ const line = lineOf(raw, m.index);
44
+ const meta = {
45
+ evalFamily: {
46
+ vuln: 'Code Injection: eval/instance_eval/class_eval on user-controlled input',
47
+ severity: 'critical', cwe: 'CWE-94',
48
+ remediation: 'Never call eval on user input — there is no safe sanitization. Replace with explicit branching on enumerated values, a method dispatch table (whitelist), or a parser for a constrained DSL.',
49
+ },
50
+ send: {
51
+ vuln: 'Method Reflection: send/public_send with user-controlled method name',
52
+ severity: 'high', cwe: 'CWE-470',
53
+ remediation: 'Validate the method name against an explicit whitelist before sending. `params[:action]` straight into `send` lets the client invoke any method on the receiver, including private ones with `send`.',
54
+ },
55
+ marshalLoad: {
56
+ vuln: 'Insecure Deserialization: Marshal.load on user input',
57
+ severity: 'critical', cwe: 'CWE-502',
58
+ remediation: 'Marshal is unsafe by design — never use it on data crossing a trust boundary. Replace with JSON or msgpack with an explicit schema.',
59
+ },
60
+ yamlUnsafe: {
61
+ vuln: 'Insecure Deserialization: YAML.load on user input',
62
+ severity: 'critical', cwe: 'CWE-502',
63
+ remediation: 'Replace `YAML.load(input)` with `YAML.safe_load(input, permitted_classes: [Symbol], aliases: true)` — the default `load` will instantiate arbitrary Ruby classes from a crafted document (same risk class as Marshal).',
64
+ },
65
+ erbResult: {
66
+ vuln: 'Server-Side Template Injection: ERB.new(user_template).result',
67
+ severity: 'critical', cwe: 'CWE-94',
68
+ remediation: 'Never feed a user-supplied string into ERB. Predefine templates server-side; the client may pass *values*, never the template body.',
69
+ },
70
+ backtick: {
71
+ vuln: 'Command Injection: backtick command interpolates request data',
72
+ severity: 'critical', cwe: 'CWE-78',
73
+ remediation: 'Use `Open3.capture2(["cmd", arg1, arg2])` with an array form so the shell does not parse anything. Backticks and `system("cmd #{params[...]}")` are pure shell injection.',
74
+ },
75
+ systemUser: {
76
+ vuln: 'Command Injection: system/exec/Open3 with user-controlled input',
77
+ severity: 'critical', cwe: 'CWE-78',
78
+ remediation: 'Use the array form: `system(["cmd", arg])`. The string form lets the shell parse — any quoting trick wins.',
79
+ },
80
+ attributesEq: {
81
+ vuln: 'Mass Assignment: model.attributes = params (no strong_params)',
82
+ severity: 'high', cwe: 'CWE-915',
83
+ remediation: 'Use `params.require(:user).permit(:name, :email)` — explicit allow-list. Assigning raw `params` lets the client set fields the controller never intended (admin: true, role: ...).',
84
+ },
85
+ openSsrf: {
86
+ vuln: 'SSRF: open/URI.open with user-controlled URL',
87
+ severity: 'high', cwe: 'CWE-918',
88
+ remediation: 'Resolve and validate the host against an allow-list before fetching. `open(params[:url])` is also a path-traversal vector under older Ruby (open-uri inherits Kernel#open semantics).',
89
+ },
90
+ fileRead: {
91
+ vuln: 'Path Traversal: File.read/open with user-controlled path',
92
+ severity: 'high', cwe: 'CWE-22',
93
+ remediation: 'Canonicalize the path and verify it stays under an allowed base: `path = File.expand_path(File.join(base, name)); raise unless path.start_with?(base)`.',
94
+ },
95
+ }[key];
96
+ push({
97
+ id: `ruby-${key}:${fp}:${line}`,
98
+ file: fp, line,
99
+ vuln: meta.vuln, severity: meta.severity, cwe: meta.cwe,
100
+ snippet: (raw.split('\n')[line - 1] || '').trim().slice(0, 200),
101
+ remediation: meta.remediation,
102
+ parser: 'RUBY',
103
+ confidence: 0.80,
104
+ });
105
+ }
106
+ }
107
+ return findings;
108
+ }
@@ -0,0 +1,105 @@
1
+ // Rust SAST module.
2
+ //
3
+ // Rust is memory-safe by default; the interesting attacks are:
4
+ // - sqlx::query(&format!(...)) SQL injection via format! macro
5
+ // (sqlx::query! is the safe macro)
6
+ // - Command::new("sh").arg("-c").arg(user) shell-form command injection
7
+ // - serde_json::from_str::<Value>(...) type-confusion when downstream code
8
+ // unwraps without validation
9
+ // - unsafe { ... } blocks informational — counts and reports
10
+ // location; not a finding by itself
11
+ // - hardcoded crypto seed Rng::from_seed([0; 32]) or similar
12
+ //
13
+ // Patterns are syntactically distinctive and don't false-match in other languages.
14
+
15
+ import { blankComments } from './_comment-strip.js';
16
+
17
+ const FINDINGS = [
18
+ {
19
+ id: 'rust-sqlx-format', severity: 'high', cwe: 'CWE-89', family: 'sql-injection',
20
+ // sqlx::query(&format!("...{}...", user)) — the format! macro disables
21
+ // the compile-time placeholder check that sqlx::query! / query_as! enforce.
22
+ re: /\bsqlx::(?:query|query_as|query_scalar)(?:_unchecked)?\s*\(\s*&?\s*format!\s*\(/g,
23
+ vuln: 'SQL Injection — sqlx::query with format! macro',
24
+ remediation: 'Use the compile-time-checked macros instead: `sqlx::query!("SELECT … WHERE id = $1", id)` (note the `!` and `$1` placeholder). `query(&format!(…))` interpolates user data into the SQL string at runtime, defeating sqlx\'s static SQL guarantee.',
25
+ },
26
+ {
27
+ id: 'rust-cmd-shell', severity: 'critical', cwe: 'CWE-78', family: 'command-injection',
28
+ // Command::new("sh").arg("-c").arg(user_input) — restrict to a single
29
+ // statement by forbidding `;` between the segments.
30
+ re: /\bCommand::new\s*\(\s*"(?:sh|bash|zsh|cmd|cmd\.exe|powershell)"\s*\)[^;]{0,200}\.\s*arg\s*\(\s*"-c"\s*\)[^;]{0,200}\.\s*arg\s*\(\s*(?!"[^"]*"\s*\))/g,
31
+ vuln: 'Command Injection — Command::new("sh").arg("-c").arg(<dynamic>)',
32
+ remediation: 'Drop the shell. Pass the program and arguments to Command::new directly: `Command::new("ls").arg("-l").arg(&user_dir)`. The shell-form (`sh -c "..."`) interprets metacharacters in user input.',
33
+ },
34
+ {
35
+ id: 'rust-cmd-arg-format', severity: 'high', cwe: 'CWE-78', family: 'command-injection',
36
+ // Command::new(...).arg(format!("--flag={}", user)) — restrict to a
37
+ // single statement (no `;` between the call and the .arg).
38
+ re: /\bCommand::new\s*\([^)]+\)[^;]{0,200}\.\s*arg\s*\(\s*format!\s*\(/g,
39
+ vuln: 'Command Injection — Command::arg(format!(...)) interpolates user input',
40
+ remediation: 'Pass each value as its own .arg(): `cmd.arg("--user").arg(&name)`. Building one argument with format!("…{user}…") loses the argv boundary and can be split by spaces / shell metachars depending on the program.',
41
+ },
42
+ {
43
+ id: 'rust-rng-zero-seed', severity: 'high', cwe: 'CWE-338', family: 'weak-rng',
44
+ // ChaCha20Rng::from_seed([0; 32]) or seed: [0u8; 32]
45
+ re: /\b(?:ChaCha\d+Rng|StdRng|SmallRng|Hc128Rng|Pcg\d+|Isaac\d+Rng)::from_seed\s*\(\s*\[\s*0\s*[;u]/g,
46
+ vuln: 'Weak randomness — RNG seeded with constant zeros',
47
+ remediation: 'Seed from the OS CSPRNG: `let rng = ChaCha20Rng::from_entropy()` or `OsRng.fill_bytes(&mut seed)`. A constant seed makes the RNG output fully predictable.',
48
+ },
49
+ {
50
+ id: 'rust-unsafe-block', severity: 'info', cwe: 'CWE-758', family: 'unsafe-block',
51
+ // unsafe { ... } — informational, but counted so reviewers can flag densities
52
+ re: /\bunsafe\s*\{/g,
53
+ vuln: 'unsafe block — review for memory-safety invariants',
54
+ remediation: 'Each `unsafe` block bypasses Rust\'s memory-safety guarantees. Verify the invariants documented for the unsafe operation hold (typically: aliasing rules, bounds, lifetime extension). Audit dense unsafe regions for buffer overflows, use-after-free, and data races.',
55
+ infoOnly: true,
56
+ },
57
+ {
58
+ id: 'rust-actix-extract-string', severity: 'medium', cwe: 'CWE-20', family: 'input-validation',
59
+ // web::Path<String> / web::Query<String> — accepts arbitrary user-controlled
60
+ // strings without a typed extractor. Often a precursor to SQL/path/cmd-injection.
61
+ re: /\bweb::(?:Path|Query)<\s*String\s*>/g,
62
+ vuln: 'Untyped Actix extractor — web::Path<String>/web::Query<String> accepts any input',
63
+ remediation: 'Define a typed struct with serde for the extractor: `#[derive(Deserialize)] struct UserPath { id: i64 }`. Typed extractors reject malformed input at the framework boundary instead of bubbling raw strings into your handlers.',
64
+ infoOnly: true,
65
+ },
66
+ ];
67
+
68
+ function lineOf(raw, idx) { return raw.substring(0, idx).split('\n').length; }
69
+
70
+ export function scanRust(fp, raw) {
71
+ if (!/\.rs$/i.test(fp)) return [];
72
+ if (!raw || raw.length > 500_000) return [];
73
+ const code = blankComments(raw);
74
+ const out = [];
75
+ const seen = new Set();
76
+ for (const rule of FINDINGS) {
77
+ const re = new RegExp(rule.re.source, rule.re.flags);
78
+ let m;
79
+ let count = 0;
80
+ while ((m = re.exec(code))) {
81
+ const line = lineOf(raw, m.index);
82
+ const id = `${rule.id}:${fp}:${line}`;
83
+ if (seen.has(id)) continue;
84
+ seen.add(id);
85
+ // For informational rules, only emit the first 3 hits per file
86
+ // (avoids drowning the output with low-signal `unsafe` blocks).
87
+ if (rule.infoOnly && ++count > 3) break;
88
+ out.push({
89
+ id, file: fp, line,
90
+ vuln: rule.vuln,
91
+ severity: rule.severity,
92
+ cwe: rule.cwe,
93
+ stride: rule.family === 'sql-injection' ? 'Tampering'
94
+ : rule.family === 'command-injection' ? 'Elevation of Privilege'
95
+ : rule.family === 'weak-rng' ? 'Spoofing'
96
+ : 'Information Disclosure',
97
+ snippet: (raw.split('\n')[line - 1] || '').trim().slice(0, 200),
98
+ remediation: rule.remediation,
99
+ confidence: 0.85,
100
+ parser: 'RUST',
101
+ });
102
+ }
103
+ }
104
+ return out;
105
+ }
@@ -0,0 +1,167 @@
1
+ // Solidity smart-contract SAST module.
2
+ //
3
+ // Covers the highest-impact, lowest-FP-risk smart-contract patterns:
4
+ // - reentrancy external call (.call.value / .send / .transfer) followed
5
+ // by a state-changing statement in the same function
6
+ // - tx-origin `tx.origin == owner` / `require(tx.origin == ...)`
7
+ // — vulnerable to phishing-style proxy attacks
8
+ // - integer-overflow pragma solidity ^0.7.x or older without SafeMath import
9
+ // — Solidity 0.8+ has built-in overflow checks
10
+ // - block-timestamp-rng block.timestamp / block.difficulty used as randomness
11
+ // - unprotected-selfdestruct selfdestruct() in a function without owner check
12
+ // - delegatecall-user delegatecall with user-controlled target
13
+ // - low-level-call-unchecked unchecked return value from .call / .send
14
+ //
15
+ // Solidity syntax is distinctive enough that these patterns won't false-match
16
+ // in any other language.
17
+
18
+ import { blankComments } from './_comment-strip.js';
19
+
20
+ const FINDINGS = [
21
+ {
22
+ id: 'sol-reentrancy', severity: 'critical', cwe: 'CWE-841', family: 'reentrancy',
23
+ // .call{value: x}("") OR .call.value(x)() OR .send(x) OR .transfer(x)
24
+ // followed within ~6 lines by a state-changing statement (assignment to
25
+ // a non-local var, balances[...] = ..., totalSupply -=, etc.)
26
+ re: /\b(?:[a-zA-Z_]\w*)\s*\.\s*call\s*(?:\{[^}]*value\s*:[^}]*\}|\.\s*value\s*\([^)]+\))\s*\([\s\S]{0,400}?[a-zA-Z_]\w*\s*\[[^\]]+\]\s*[-+]?=/g,
27
+ vuln: 'Reentrancy — external call before state update (Check-Effects-Interactions violated)',
28
+ remediation: 'Update internal state BEFORE making the external call (Check-Effects-Interactions pattern). Or use OpenZeppelin\'s ReentrancyGuard modifier. The DAO hack ($150M, 2016) and many later incidents follow this exact shape: balance is read, .call.value() pays out, and only after does the balance get zeroed — the callee re-enters and drains.',
29
+ },
30
+ {
31
+ id: 'sol-tx-origin', severity: 'high', cwe: 'CWE-345', family: 'tx-origin-auth',
32
+ re: /\b(?:require|assert|if)\s*\(\s*[^)]*\btx\.origin\s*(?:==|!=)/g,
33
+ vuln: 'Authentication using tx.origin (phishing-vulnerable)',
34
+ remediation: 'Use `msg.sender` for the authenticated caller, never `tx.origin`. tx.origin is the EOA at the start of the call chain — if a user is tricked into calling an attacker contract that then calls yours, tx.origin is still the user but msg.sender is the attacker.',
35
+ },
36
+ {
37
+ id: 'sol-overflow-old-pragma', severity: 'high', cwe: 'CWE-190', family: 'integer-overflow',
38
+ // pragma solidity ^0.x.y where x < 8 AND no SafeMath import in file
39
+ re: /pragma\s+solidity\s+[\^>=~]*\s*0\.[0-7]\b/g,
40
+ vuln: 'Integer overflow risk — pragma <0.8 without SafeMath',
41
+ remediation: 'Either upgrade the pragma to `pragma solidity ^0.8.0;` (built-in checked arithmetic) or import OpenZeppelin SafeMath: `using SafeMath for uint256;`. Pre-0.8 silently wraps on overflow — a classic gateway to fund-drain bugs.',
42
+ fileSafe: /\b(?:SafeMath|using\s+SafeMath)\b|pragma\s+solidity\s+[\^>=~]*\s*0\.[89]|pragma\s+solidity\s+[\^>=~]*\s*[1-9]\d*\b/,
43
+ },
44
+ {
45
+ id: 'sol-block-timestamp-rng', severity: 'medium', cwe: 'CWE-330', family: 'weak-rng',
46
+ re: /\b(?:keccak256|sha3|abi\.encodePacked)\s*\([^)]*\bblock\.(?:timestamp|difficulty|number|prevrandao|coinbase)/g,
47
+ vuln: 'Predictable randomness — block.timestamp / block.difficulty as RNG seed',
48
+ remediation: 'Block proposers can influence (or fully control) block.timestamp / block.difficulty within a small range. For any randomness with funds at stake, use Chainlink VRF or a commit-reveal scheme.',
49
+ },
50
+ {
51
+ id: 'sol-selfdestruct-unprotected', severity: 'critical', cwe: 'CWE-284', family: 'unprotected-selfdestruct',
52
+ // selfdestruct(...) appears in a function. Suppress if the same function body has an owner check.
53
+ re: /\bselfdestruct\s*\(/g,
54
+ vuln: 'selfdestruct() without owner-only modifier (contract destruction risk)',
55
+ remediation: 'Restrict selfdestruct() to an `onlyOwner` modifier (OpenZeppelin Ownable) or guard with `require(msg.sender == owner)`. The Parity Multisig wallet ($150M, 2017) was bricked when an unprotected init function was called by an attacker who then triggered selfdestruct.',
56
+ },
57
+ {
58
+ id: 'sol-delegatecall-user', severity: 'critical', cwe: 'CWE-94', family: 'delegatecall-untrusted',
59
+ re: /\b\w+\s*\.\s*delegatecall\s*\(/g,
60
+ vuln: 'delegatecall — verify the target address is fixed and trusted',
61
+ remediation: 'delegatecall executes the target contract\'s code in YOUR contract\'s storage. If the target is user-controlled, an attacker can write arbitrary slots — including the owner field. Restrict delegatecall to a hardcoded, audited implementation address.',
62
+ // OpenZeppelin Address.sol and Proxy.sol implement audited library
63
+ // delegatecall helpers (functionDelegateCall, _delegate). They're the
64
+ // canonical "trusted" wrappers — firing on them is noise. Skip files
65
+ // whose basename is a known OZ utility.
66
+ skipBasename: /^(?:Address|Proxy|TransparentUpgradeableProxy|ERC1967Proxy|UpgradeableProxy|BeaconProxy|StorageSlot|Multicall)\.sol$/i,
67
+ },
68
+ {
69
+ id: 'sol-call-unchecked', severity: 'medium', cwe: 'CWE-252', family: 'unchecked-low-level-call',
70
+ // .call{...}(...) or .send(...) whose return value isn't captured to a bool
71
+ re: /(?:[a-zA-Z_]\w*)\s*\.\s*(?:call|send)\s*(?:\{[^}]*\})?\s*\([^;]+\);/g,
72
+ vuln: 'Unchecked low-level call — return value not inspected',
73
+ remediation: 'Capture the bool from `.call(...)` / `.send(...)` and revert on false: `(bool ok, ) = addr.call{value: amt}(""); require(ok, "call failed");`. Silent failure leaves the contract in an inconsistent state.',
74
+ // Suppress when the pattern is consumed by `(bool ok,) = …`
75
+ isSafeMatch: (matched) => /\b(?:bool\s+\w+\s*,?)?\s*\)?\s*=/.test(matched) || /^require\s*\(/.test(matched),
76
+ },
77
+ ];
78
+
79
+ function lineOf(raw, idx) { return raw.substring(0, idx).split('\n').length; }
80
+
81
+ // Check if the line where the selfdestruct sits is inside a function body
82
+ // that also has an owner-check require.
83
+ function _selfdestructHasOwnerCheck(lines, sinkLine) {
84
+ // Walk back to find the enclosing `function` declaration; walk forward to
85
+ // matching `}`. Search for an owner check within that range.
86
+ let openBraceLine = -1;
87
+ let depth = 0;
88
+ for (let i = sinkLine - 1; i >= 0; i--) {
89
+ const ln = lines[i] || '';
90
+ for (let k = ln.length - 1; k >= 0; k--) {
91
+ if (ln[k] === '}') depth++;
92
+ else if (ln[k] === '{') { if (depth === 0) { openBraceLine = i; } else depth--; }
93
+ }
94
+ if (openBraceLine >= 0) break;
95
+ }
96
+ if (openBraceLine < 0) return false;
97
+ // Look at the line above the brace for a `function ...` decl with `onlyOwner`
98
+ const declLine = lines[openBraceLine] || '';
99
+ const prevLine = lines[Math.max(0, openBraceLine - 1)] || '';
100
+ if (/\bonlyOwner\b/.test(declLine) || /\bonlyOwner\b/.test(prevLine)) return true;
101
+ // Scan body for `require(msg.sender == owner)` or `require(msg.sender == ...Owner)`
102
+ let d = 0;
103
+ for (let i = openBraceLine; i < lines.length; i++) {
104
+ const ln = lines[i] || '';
105
+ for (const c of ln) {
106
+ if (c === '{') d++;
107
+ else if (c === '}') { d--; if (d === 0) return false; }
108
+ }
109
+ if (/\brequire\s*\(\s*msg\.sender\s*==\s*\w*[Oo]wner\w*\s*[,)]/.test(ln)) return true;
110
+ if (/\brequire\s*\(\s*msg\.sender\s*==\s*admin/.test(ln)) return true;
111
+ }
112
+ return false;
113
+ }
114
+
115
+ export function scanSolidity(fp, raw) {
116
+ if (!/\.sol$/i.test(fp)) return [];
117
+ if (!raw || raw.length > 500_000) return [];
118
+ const code = blankComments(raw);
119
+ const lines = raw.split('\n');
120
+ const out = [];
121
+ const seen = new Set();
122
+ for (const rule of FINDINGS) {
123
+ if (rule.fileSafe && rule.fileSafe.test(code)) continue;
124
+ if (rule.skipBasename) {
125
+ const base = fp.replace(/\\/g, '/').split('/').pop() || '';
126
+ if (rule.skipBasename.test(base)) continue;
127
+ }
128
+ const re = new RegExp(rule.re.source, rule.re.flags);
129
+ let m;
130
+ while ((m = re.exec(code))) {
131
+ // Suppress self-destruct findings where the enclosing function has an
132
+ // owner check (modifier or require).
133
+ if (rule.id === 'sol-selfdestruct-unprotected') {
134
+ const ln = lineOf(raw, m.index);
135
+ if (_selfdestructHasOwnerCheck(lines, ln)) continue;
136
+ }
137
+ // Skip low-level-call when the call's return is captured.
138
+ if (rule.id === 'sol-call-unchecked') {
139
+ const ln = lineOf(raw, m.index);
140
+ const lineText = lines[ln - 1] || '';
141
+ // If the line begins with `(bool` or contains ` = ` before the call,
142
+ // the return is being captured — suppress.
143
+ if (/\(\s*bool\s+\w+\s*,/.test(lineText) || /=\s*[a-zA-Z_]\w*\s*\.\s*(?:call|send)/.test(lineText)) continue;
144
+ }
145
+ const line = lineOf(raw, m.index);
146
+ const id = `${rule.id}:${fp}:${line}`;
147
+ if (seen.has(id)) continue;
148
+ seen.add(id);
149
+ out.push({
150
+ id, file: fp, line,
151
+ vuln: rule.vuln,
152
+ severity: rule.severity,
153
+ cwe: rule.cwe,
154
+ stride: rule.family === 'reentrancy' || rule.family === 'unchecked-low-level-call' ? 'Tampering'
155
+ : rule.family === 'tx-origin-auth' ? 'Spoofing'
156
+ : rule.family === 'integer-overflow' ? 'Tampering'
157
+ : rule.family === 'weak-rng' ? 'Spoofing'
158
+ : 'Elevation of Privilege',
159
+ snippet: (lines[line - 1] || '').trim().slice(0, 200),
160
+ remediation: rule.remediation,
161
+ confidence: 0.85,
162
+ parser: 'SOL',
163
+ });
164
+ }
165
+ }
166
+ return out;
167
+ }