@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,186 @@
1
+ // Spring Boot framework hardening.
2
+ //
3
+ // Extends the general Java SAST with Spring-specific annotation-level
4
+ // audits. Targets controllers, security configurations, and the
5
+ // application.properties / application.yml that ship with Spring Boot apps.
6
+ //
7
+ // Coverage:
8
+ // 1. @RestController / @Controller class with mutating endpoints
9
+ // (POST/PUT/PATCH/DELETE) missing @PreAuthorize / @Secured / @RolesAllowed
10
+ // 2. SecurityFilterChain with .permitAll() on /admin/** or /api/**
11
+ // 3. @CrossOrigin(origins = "*") (or allow-all credentials combo)
12
+ // 4. application.properties / application.yml with literal credentials
13
+ // 5. application.properties spring.security.user.password=
14
+ // 6. Missing @EnableMethodSecurity / @EnableGlobalMethodSecurity
15
+ // 7. JWT filter that skips signature verification (decodes only)
16
+ // 8. management.endpoints.web.exposure.include=* (Actuator exposed)
17
+
18
+ const _JAVA_RE = /\.java$/i;
19
+ const _SPRING_PROPS_RE = /(?:^|[\\/])application(?:[-.][\w-]+)?\.(?:properties|ya?ml)$/i;
20
+
21
+ const _SPRING_CTL_ANN_RE = /@(?:RestController|Controller)\b/;
22
+ const _SPRING_MAPPING_RE = /@(?:Get|Post|Put|Patch|Delete|Request)Mapping\b/;
23
+ const _MUTATING_RE = /@(?:Post|Put|Patch|Delete)Mapping\b/;
24
+ const _AUTHZ_ANN_RE = /@(?:PreAuthorize|PostAuthorize|Secured|RolesAllowed|PermitAll)\b/;
25
+
26
+ function _line(raw, idx) {
27
+ return raw.slice(0, idx).split('\n').length;
28
+ }
29
+
30
+ function _isSpringJavaFile(raw) {
31
+ return /\bimport\s+org\.springframework\b/.test(raw) || /\b@SpringBoot|@RestController|@Controller|@Service|@Repository\b/.test(raw);
32
+ }
33
+
34
+ export function scanSpringbootHardening(file, raw) {
35
+ if (!file || !raw || typeof raw !== 'string') return [];
36
+ if (raw.length > 300_000) return [];
37
+
38
+ const findings = [];
39
+
40
+ // ── application.properties / .yml checks ────────────────────────────────
41
+ if (_SPRING_PROPS_RE.test(file)) {
42
+ // spring.security.user.password= literal (default in-memory user with a real password)
43
+ for (const m of raw.matchAll(/^\s*spring\.security\.user\.password\s*[=:]\s*(?!(?:\$\{|---|''|""|\s*$))(\S+)/gm)) {
44
+ findings.push({
45
+ id: `springboot:default-user-password:${file}:${_line(raw, m.index)}`,
46
+ file, line: _line(raw, m.index),
47
+ vuln: 'Spring Boot application.properties: spring.security.user.password set to a literal',
48
+ severity: 'critical',
49
+ family: 'springboot-hardcoded-credential',
50
+ cwe: 'CWE-798',
51
+ confidence: 0.9,
52
+ description: 'Spring Security\'s in-memory default user is enabled with a hardcoded password. Anyone who reads the property file gets admin access in dev/staging — and these files frequently ship to production by mistake.',
53
+ remediation: 'Use spring.security.user.password=${ADMIN_PASSWORD} pulled from env. Better: replace the default user with a real UserDetailsService backed by your identity store.',
54
+ });
55
+ }
56
+ // OIDC client secret literal
57
+ for (const m of raw.matchAll(/^\s*(?:quarkus\.oidc\.credentials\.secret|spring\.security\.oauth2\.client\.registration\.\w+\.client-secret)\s*[=:]\s*(\S{12,})/gm)) {
58
+ const val = m[1];
59
+ if (val.startsWith('${') || val === '""' || val === "''") continue;
60
+ findings.push({
61
+ id: `springboot:oidc-secret-literal:${file}:${_line(raw, m.index)}`,
62
+ file, line: _line(raw, m.index),
63
+ vuln: 'OIDC client secret in plaintext config',
64
+ severity: 'critical',
65
+ family: 'springboot-hardcoded-credential',
66
+ cwe: 'CWE-798',
67
+ confidence: 0.9,
68
+ description: 'An OIDC / OAuth2 client secret is in a config file checked into source control. Anyone with repo read can impersonate the application against the IdP.',
69
+ remediation: 'Replace with ${OIDC_CLIENT_SECRET} env-var reference. Rotate the leaked secret immediately at the IdP.',
70
+ });
71
+ }
72
+ // Actuator endpoints exposed via wildcard
73
+ if (/^\s*management\.endpoints\.web\.exposure\.include\s*[=:]\s*['"]?\*/.test(raw)) {
74
+ const m = /^\s*management\.endpoints\.web\.exposure\.include\s*[=:]\s*['"]?\*/.exec(raw);
75
+ findings.push({
76
+ id: `springboot:actuator-exposed:${file}:${_line(raw, m.index)}`,
77
+ file, line: _line(raw, m.index),
78
+ vuln: 'Spring Actuator endpoints exposed via wildcard (management.endpoints.web.exposure.include=*)',
79
+ severity: 'high',
80
+ family: 'springboot-actuator-exposed',
81
+ cwe: 'CWE-200',
82
+ confidence: 0.95,
83
+ description: 'Wildcard Actuator exposure makes /actuator/env, /heapdump, /threaddump, /loggers reachable. /env leaks process environment variables (including database passwords); /heapdump dumps the JVM heap.',
84
+ remediation: 'Set management.endpoints.web.exposure.include=health,info (or whatever specific ones you need). Bind Actuator to a separate port behind a private network.',
85
+ });
86
+ }
87
+ return findings;
88
+ }
89
+
90
+ // ── Java source checks ──────────────────────────────────────────────────
91
+ if (!_JAVA_RE.test(file)) return [];
92
+ if (!_isSpringJavaFile(raw)) return [];
93
+
94
+ // 1. Controllers with mutating endpoints missing authz annotation
95
+ if (_SPRING_CTL_ANN_RE.test(raw) && _MUTATING_RE.test(raw)) {
96
+ // Find each method annotated with mutating mapping; check if @PreAuthorize / @Secured / @RolesAllowed sits above it.
97
+ const methodRe = /(@(?:Post|Put|Patch|Delete)Mapping\b[^\n]*\n(?:[^\n]*\n){0,4})((?:[^{}]|\{[^{}]*\})*?)public\s+\w[\w<>,\s\[\]?]*\s+(\w+)\s*\(/g;
98
+ let mm;
99
+ while ((mm = methodRe.exec(raw))) {
100
+ const block = mm[0];
101
+ const methodName = mm[3];
102
+ const lineIdx = _line(raw, mm.index);
103
+ // Search 6 lines upward for an authz annotation
104
+ const above = raw.slice(Math.max(0, mm.index - 400), mm.index);
105
+ if (_AUTHZ_ANN_RE.test(above)) continue;
106
+ // Skip GET-style "list" methods that might legitimately be public.
107
+ // (We only matched mutating verbs anyway, so this is just for safety.)
108
+ findings.push({
109
+ id: `springboot:no-authz:${file}:${lineIdx}:${methodName}`,
110
+ file, line: lineIdx,
111
+ vuln: `Mutating endpoint ${methodName}() has no @PreAuthorize / @Secured / @RolesAllowed`,
112
+ severity: 'high',
113
+ family: 'springboot-missing-authz',
114
+ cwe: 'CWE-862',
115
+ confidence: 0.75,
116
+ description: 'A POST/PUT/PATCH/DELETE handler is exposed without any authorization annotation. Unless the entire URL prefix is gated by SecurityFilterChain (and explicitly), this endpoint is callable by anyone who reaches it.',
117
+ remediation: 'Add @PreAuthorize("hasRole(\'ADMIN\')") or @Secured("ROLE_ADMIN") above the method; or rely on a SecurityFilterChain rule and prove the path is covered.',
118
+ });
119
+ void block;
120
+ }
121
+ }
122
+
123
+ // 2. SecurityFilterChain with permitAll() on /admin/** or /api/**
124
+ for (const m of raw.matchAll(/\.requestMatchers\s*\(\s*['"]([^'"]*\/(?:admin|api|internal)\/[^'"]*)['"][^)]*\)[^)]*\.permitAll\s*\(\s*\)/g)) {
125
+ findings.push({
126
+ id: `springboot:permitAll-admin:${file}:${_line(raw, m.index)}`,
127
+ file, line: _line(raw, m.index),
128
+ vuln: `Spring Security permitAll() on a sensitive path: ${m[1]}`,
129
+ severity: 'critical',
130
+ family: 'springboot-permitall-sensitive',
131
+ cwe: 'CWE-862',
132
+ confidence: 0.9,
133
+ description: 'A path matcher that looks like an administrative or API surface is being permitAll-ed in the security filter chain. Anyone reaching it bypasses auth entirely.',
134
+ remediation: 'Replace .permitAll() with .hasRole(\'ADMIN\') (or whatever role applies). If the path is genuinely public, narrow the matcher so it cannot match real admin URLs.',
135
+ });
136
+ }
137
+
138
+ // 3. @CrossOrigin(origins = "*")
139
+ for (const m of raw.matchAll(/@CrossOrigin\s*\(\s*[^)]*origins\s*=\s*['"]?\*['"]?[^)]*\)/g)) {
140
+ findings.push({
141
+ id: `springboot:cors-wildcard:${file}:${_line(raw, m.index)}`,
142
+ file, line: _line(raw, m.index),
143
+ vuln: '@CrossOrigin(origins = "*") allows any origin',
144
+ severity: 'high',
145
+ family: 'springboot-cors-wildcard',
146
+ cwe: 'CWE-942',
147
+ confidence: 0.9,
148
+ description: 'Wildcard CORS combined with credentialed requests means any origin can read authenticated responses. Even without credentials, it broadens the API\'s exposure to scraping and abuse.',
149
+ remediation: 'Replace with origins = {"https://app.example.com"}. If credentials are involved, the wildcard is rejected by browsers anyway, so be explicit.',
150
+ });
151
+ }
152
+
153
+ // 4. Missing @EnableMethodSecurity in @Configuration class that uses @PreAuthorize elsewhere
154
+ if (/@Configuration\b/.test(raw) && _AUTHZ_ANN_RE.test(raw) &&
155
+ !/@EnableMethodSecurity|@EnableGlobalMethodSecurity/.test(raw)) {
156
+ const m = /@Configuration\b/.exec(raw);
157
+ findings.push({
158
+ id: `springboot:method-security-disabled:${file}:${_line(raw, m.index)}`,
159
+ file, line: _line(raw, m.index),
160
+ vuln: 'Spring @Configuration uses @PreAuthorize but @EnableMethodSecurity is not declared',
161
+ severity: 'high',
162
+ family: 'springboot-method-security-disabled',
163
+ cwe: 'CWE-862',
164
+ confidence: 0.6,
165
+ description: 'Method-level authorization annotations (@PreAuthorize / @Secured / @RolesAllowed) are NO-OPS unless @EnableMethodSecurity (Spring Security 6+) or @EnableGlobalMethodSecurity (older) is on a @Configuration class.',
166
+ remediation: 'Add @EnableMethodSecurity (Spring Security 6+) on a @Configuration class.',
167
+ });
168
+ }
169
+
170
+ // 5. JWT decode without verify — JWT.decode(token) instead of JWT.require(...).build().verify(token)
171
+ for (const m of raw.matchAll(/\bJWT\s*\.\s*decode\s*\(/g)) {
172
+ findings.push({
173
+ id: `springboot:jwt-decode-only:${file}:${_line(raw, m.index)}`,
174
+ file, line: _line(raw, m.index),
175
+ vuln: 'JWT.decode() used — does NOT verify the signature',
176
+ severity: 'critical',
177
+ family: 'springboot-jwt-no-verify',
178
+ cwe: 'CWE-347',
179
+ confidence: 0.85,
180
+ description: 'JWT.decode() returns the decoded claims without verifying the signature. An attacker can craft a token with any claims they want — including elevated roles — and it will be accepted.',
181
+ remediation: 'Use JWT.require(Algorithm.HMAC256(secret)).build().verify(token), or Spring Security\'s JwtDecoder / JwtAuthenticationToken.',
182
+ });
183
+ }
184
+
185
+ return findings;
186
+ }
@@ -0,0 +1,80 @@
1
+ import { blankComments } from './_comment-strip.js';
2
+ // SSRF cloud-metadata awareness.
3
+ //
4
+ // Two layers:
5
+ // 1. Hardcoded fetches of cloud metadata endpoints (likely intentional, but
6
+ // flag for review — these endpoints leak instance credentials).
7
+ // 2. User-controlled URL into an HTTP client with no allow-list — separately
8
+ // flagged by the general SSRF rule. This module adds a +severity bump
9
+ // and a specific remediation note when the codebase shows no evidence of
10
+ // blocking 169.254.169.254 / fd00:ec2:: / metadata.google.internal /
11
+ // Azure IMDS in any allow-list / proxy / fetch wrapper.
12
+
13
+ const METADATA_LITERALS = [
14
+ /169\.254\.169\.254/, // AWS, Azure
15
+ /fd00:ec2::254/i, // AWS IPv6
16
+ /metadata\.google\.internal/i, // GCP
17
+ /metadata\.azure\.com/i, // Azure
18
+ ];
19
+
20
+ const SSRF_CLIENT_RE = /\b(?:fetch|axios\.\w+|requests\.\w+|http\.get|http\.request|urllib\.request\.urlopen|new\s+URL\s*\(|HttpClient\.\w+)\s*\(\s*[^)]*?(req|request|ctx\.request|input|userInput|url)\s*\.\s*(?:body|query|params|url|host)/g;
21
+
22
+ const METADATA_GUARD_RE = /(?:169\.254\.169\.254|169\.254\.|metadata\.google\.internal|metadata\.azure\.com|fd00:ec2|metadata\.aws\.amazon)/i;
23
+
24
+ function lineOf(raw, idx) { return raw.substring(0, idx).split('\n').length; }
25
+
26
+ export function scanSSRFCloudMetadata(fp, raw) {
27
+ if (!raw || raw.length > 500_000) return [];
28
+ const code = blankComments(raw);
29
+
30
+ const findings = [];
31
+ const seen = new Set();
32
+ const push = (f) => { if (!seen.has(f.id)) { seen.add(f.id); findings.push(f); } };
33
+
34
+ // 1. Hardcoded reference to a metadata endpoint.
35
+ for (const re of METADATA_LITERALS) {
36
+ const r = new RegExp(re.source, (re.flags || '') + 'g');
37
+ let m;
38
+ while ((m = r.exec(code))) {
39
+ const line = lineOf(raw, m.index);
40
+ push({
41
+ id: `ssrf-meta-hardcoded:${fp}:${line}`,
42
+ file: fp, line,
43
+ vuln: 'SSRF: explicit reference to cloud instance-metadata endpoint',
44
+ severity: 'medium',
45
+ cwe: 'CWE-918',
46
+ stride: 'Information Disclosure',
47
+ snippet: (raw.split('\n')[line - 1] || '').trim().slice(0, 200),
48
+ remediation: 'Cloud metadata endpoints (169.254.169.254 for AWS/Azure, metadata.google.internal for GCP) expose instance credentials and tokens. Calls *to* these endpoints are sometimes legitimate (the workload retrieving its own creds), but exposure *via* user-controlled requests is a primary cloud-takeover path. If this call is intentional, gate it behind a startup-only path. If not, drop it.',
49
+ parser: 'SSRF-METADATA',
50
+ confidence: 0.70,
51
+ });
52
+ }
53
+ }
54
+
55
+ // 2. User-controlled URL into an HTTP client, with no metadata guard nearby.
56
+ const fileHasMetadataGuard = METADATA_GUARD_RE.test(code);
57
+ const r = new RegExp(SSRF_CLIENT_RE.source, SSRF_CLIENT_RE.flags);
58
+ let m;
59
+ while ((m = r.exec(code))) {
60
+ const line = lineOf(raw, m.index);
61
+ // Skip if a guard appears in ±10 lines.
62
+ const lines = raw.split('\n');
63
+ const window = lines.slice(Math.max(0, line - 11), line + 10).join(' ');
64
+ if (METADATA_GUARD_RE.test(window) || fileHasMetadataGuard) continue;
65
+ push({
66
+ id: `ssrf-meta-usercontrolled:${fp}:${line}`,
67
+ file: fp, line,
68
+ vuln: 'SSRF (metadata-aware): user-controlled URL into HTTP client without metadata allow-deny',
69
+ severity: 'high',
70
+ cwe: 'CWE-918',
71
+ stride: 'Information Disclosure',
72
+ snippet: (raw.split('\n')[line - 1] || '').trim().slice(0, 200),
73
+ remediation: 'Reject hostnames that resolve into RFC1918 ranges (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`), loopback (`127.0.0.1`, `::1`), link-local (`169.254.0.0/16`), and cloud-metadata DNS names (`metadata.google.internal`, `metadata.azure.com`). Resolve DNS yourself and re-check after the resolution — DNS rebinding will swap a public IP for `169.254.169.254` between resolution and connect. Use a vetted SSRF guard like `ssrf-req-filter` or a per-call HTTP proxy that enforces the allow-list.',
74
+ parser: 'SSRF-METADATA',
75
+ confidence: 0.75,
76
+ });
77
+ }
78
+
79
+ return findings;
80
+ }
@@ -0,0 +1,116 @@
1
+ // Server-Side Template Injection (CWE-94 / OWASP A03).
2
+ //
3
+ // Pattern: user-controlled string fed to a template-engine compile or
4
+ // from_string. The attacker controls the template body, so they can
5
+ // invoke the language's expression-evaluation surface (often equivalent
6
+ // to code execution via Jinja2's `__class__.__bases__` / Handlebars's
7
+ // helpers / EJS's `<% %>`).
8
+ //
9
+ // We catch the high-confidence shapes — when the template body is built
10
+ // from a user-derived source (req.body, request.args, params.<x>, …) or
11
+ // the body has obvious concatenation/interpolation. We do NOT flag the
12
+ // pattern with a constant string body — that's the secure form.
13
+ //
14
+ // Engines covered (v1):
15
+ // - Jinja2 (Python): Environment.from_string(x), Template(x)
16
+ // - Handlebars (JS): Handlebars.compile(x)
17
+ // - EJS (JS): ejs.compile(x), ejs.render(x)
18
+ // - Mustache (JS): Mustache.render(x, …) when x is non-literal
19
+ // - Twig (PHP): $twig->createTemplate($x)
20
+ // - Pug (JS, no AST): pug.compile(x)
21
+ // - Velocity (Java): Velocity.evaluate(ctx, w, log, x) when x is non-literal
22
+ //
23
+ // Severity: critical (template-injection → RCE in most engines).
24
+
25
+ import { blankComments } from './_comment-strip.js';
26
+
27
+ const TAINT_HINT_RE =
28
+ /\b(?:req\.|request\.|params\.|query\.|body\.|ctx\.query|ctx\.request|c\.Query|r\.URL\.Query|_GET|_POST|_REQUEST|process\.argv|getenv|environ)\b|`[^`]*\$\{|"[^"]*"\s*\+\s*\w|'[^']*'\s*\+\s*\w|\bf['"]/;
29
+
30
+ // PATTERNS: [lang, regex, engine]. Each captures the call's first arg
31
+ // region in group 1 so we can test for taint hints.
32
+ const PATTERNS = [
33
+ // Jinja2 from_string — most common SSTI shape. Accept any `<ident>.from_string`
34
+ // because the Environment is often stored in a local (e.g. `env = Environment()`).
35
+ ['py', /\b(?:[A-Za-z_][\w]*|\w+\([^)]*\))\s*\.\s*from_string\s*\(\s*([^)]*?)\s*\)/g, 'Jinja2'],
36
+ // Plain `Template(x)` import — risky enough to flag with a taint hint.
37
+ ['py', /\bTemplate\s*\(\s*((?:request|params|body|query|f["']|[a-z_][\w]*\s*\+)[^)]*)\)/g, 'Jinja2/Template'],
38
+ // Handlebars.compile (Node).
39
+ ['js', /\bHandlebars\s*\.\s*compile\s*\(\s*([^)]*?)\s*\)/g, 'Handlebars'],
40
+ // EJS compile/render.
41
+ ['js', /\bejs\s*\.\s*(?:compile|render)\s*\(\s*([^)]*?)\s*\)/g, 'EJS'],
42
+ // Mustache render — only when first arg is dynamic.
43
+ ['js', /\bMustache\s*\.\s*render\s*\(\s*((?:req\.|request\.|params\.|query\.|body\.|ctx\.|`[^`]*\$\{)[^,)]*)/g, 'Mustache'],
44
+ // Pug compile.
45
+ ['js', /\bpug\s*\.\s*compile\s*\(\s*([^)]*?)\s*\)/g, 'Pug'],
46
+ // Twig dynamic template.
47
+ ['php', /->\s*createTemplate\s*\(\s*([^)]*?)\s*\)/g, 'Twig'],
48
+ // Velocity dynamic template evaluation.
49
+ ['java', /\bVelocity\s*\.\s*evaluate\s*\([^,]+,[^,]+,[^,]+,\s*([^)]+)\s*\)/g, 'Velocity'],
50
+ ];
51
+
52
+ function _lineOf(raw, idx) { return raw.substring(0, idx).split('\n').length; }
53
+
54
+ function _lang(fp) {
55
+ if (/\.(?:js|jsx|ts|tsx|mjs|cjs)$/i.test(fp)) return 'js';
56
+ if (/\.py$/i.test(fp)) return 'py';
57
+ if (/\.php$/i.test(fp)) return 'php';
58
+ if (/\.java$/i.test(fp)) return 'java';
59
+ return null;
60
+ }
61
+
62
+ export function scanSSTI(fp, raw) {
63
+ if (!raw || raw.length > 500_000) return [];
64
+ const lang = _lang(fp);
65
+ if (!lang) return [];
66
+ const code = blankComments(raw, lang === 'py' ? 'py' : undefined);
67
+ // Cheap pre-filter so we don't pay regex cost on files that don't
68
+ // mention any template engine.
69
+ if (!/\b(?:Jinja|jinja|Template|Handlebars|ejs|Mustache|pug|Twig|Velocity|createTemplate|from_string)\b/.test(code)) return [];
70
+ const findings = [];
71
+ const seen = new Set();
72
+ const rawLines = raw.split('\n');
73
+ for (const [plang, pat, engine] of PATTERNS) {
74
+ if (plang !== lang) continue;
75
+ const re = new RegExp(pat.source, pat.flags);
76
+ let m;
77
+ while ((m = re.exec(code))) {
78
+ const firstArg = (m[1] || '').trim();
79
+ const literalConst = /^\s*(['"`])(?:[^'"`\\]|\\.)*\1\s*$/.test(firstArg);
80
+ if (literalConst) continue; // const template body is safe
81
+ const line = _lineOf(raw, m.index);
82
+ // Direct taint hint inline? Or a hint in the preceding 10 lines that
83
+ // assigns to the same identifier name we're about to feed the engine?
84
+ let tainted = TAINT_HINT_RE.test(firstArg);
85
+ if (!tainted && /^[a-z_][\w]*$/i.test(firstArg)) {
86
+ const lo = Math.max(0, line - 11);
87
+ const before = rawLines.slice(lo, line - 1).join('\n');
88
+ const assignRe = new RegExp(`\\b${firstArg}\\s*=\\s*[^;\\n]*(?:${TAINT_HINT_RE.source})`);
89
+ if (assignRe.test(before)) tainted = true;
90
+ }
91
+ if (!tainted) continue;
92
+ const id = `ssti:${fp}:${line}:${engine}`;
93
+ if (seen.has(id)) continue;
94
+ seen.add(id);
95
+ findings.push({
96
+ id,
97
+ file: fp, line,
98
+ vuln: `Server-Side Template Injection (${engine})`,
99
+ severity: 'critical',
100
+ cwe: 'CWE-94',
101
+ family: 'ssti',
102
+ stride: 'Elevation of Privilege',
103
+ snippet: (raw.split('\n')[line - 1] || '').trim().slice(0, 200),
104
+ remediation:
105
+ 'Never compile a template from user input. Pre-register templates and pass user values as variables. ' +
106
+ 'Jinja2: load templates with `jinja2.FileSystemLoader` + `env.get_template("name.html")`, then `.render(name=user_value)`. ' +
107
+ 'Handlebars: pre-compile the template; pass user values via the context object. ' +
108
+ 'EJS: same — `ejs.render(template_string_from_disk, { name: user_value })`. ' +
109
+ 'Mustache: never pass user input as the template body argument.',
110
+ parser: 'SSTI',
111
+ confidence: 0.85,
112
+ });
113
+ }
114
+ }
115
+ return findings;
116
+ }
@@ -0,0 +1,162 @@
1
+ // Swift / iOS application security audit.
2
+ //
3
+ // Covers the canonical Swift/iOS bugs from the rules/swift/security.md
4
+ // guidance:
5
+ // 1. UserDefaults used for secret storage (should be Keychain)
6
+ // 2. App Transport Security (ATS) disabled — looking for the code-side
7
+ // `URLSession` patterns; the Info.plist bypass is caught by
8
+ // mobile-manifest.js.
9
+ // 3. Force-unwrap on URL(string:) / URL.init(string:) → crash + bypass
10
+ // 4. WKWebView with JavaScript enabled and no navigation delegate
11
+ // 5. Deep-link handling without scheme / host / path allow-list
12
+ // 6. Cleartext HTTP in URLRequest URL string
13
+ // 7. Hardcoded API key / token literal
14
+
15
+ const _SWIFT_RE = /\.swift$/i;
16
+
17
+ function _line(raw, idx) {
18
+ return raw.slice(0, idx).split('\n').length;
19
+ }
20
+
21
+ const _CRED_RE = [
22
+ { re: /\bsk-ant-[A-Za-z0-9_-]{20,}\b/, label: 'Anthropic API key' },
23
+ { re: /\bsk-[A-Za-z0-9]{32,}\b/, label: 'OpenAI-style key' },
24
+ { re: /\bghp_[A-Za-z0-9]{36}\b/, label: 'GitHub PAT' },
25
+ { re: /\bAKIA[0-9A-Z]{16}\b/, label: 'AWS access key' },
26
+ { re: /\bxox[abprs]-[A-Za-z0-9-]{10,}\b/, label: 'Slack token' },
27
+ ];
28
+
29
+ export function scanSwift(file, raw) {
30
+ if (!file || !raw || typeof raw !== 'string') return [];
31
+ if (!_SWIFT_RE.test(file)) return [];
32
+ if (raw.length > 200_000) return [];
33
+
34
+ const findings = [];
35
+
36
+ // 1. UserDefaults for secrets — the variable name on the LHS is the clue.
37
+ const udSecretRe = /\bUserDefaults(?:\.standard)?\.(?:set|setValue|setObject)\s*\([^)]*?\b(?:[tT]oken|[pP]assword|[aA]pi[kK]ey|apiKey|api_key|jwt|bearer|secret|credential|session[kK]ey)\b/g;
38
+ for (const m of raw.matchAll(udSecretRe)) {
39
+ findings.push({
40
+ id: `swift:userdefaults-secret:${file}:${_line(raw, m.index)}`,
41
+ file, line: _line(raw, m.index),
42
+ vuln: 'UserDefaults used to store a secret (token / password / key)',
43
+ severity: 'high',
44
+ family: 'swift-insecure-storage',
45
+ cwe: 'CWE-922',
46
+ confidence: 0.8,
47
+ description: 'UserDefaults stores values in plist files in the app sandbox; they\'re trivially extracted by anyone with the device or a backup. Keychain Services is the correct vault on iOS — encrypted-at-rest with hardware-backed access control on modern devices.',
48
+ remediation: 'Use Keychain Services (Security.framework) — e.g., KeychainAccess library or the SecItemAdd/SecItemCopyMatching APIs directly. Set kSecAttrAccessible to kSecAttrAccessibleWhenUnlockedThisDeviceOnly for tokens.',
49
+ snippet: m[0].slice(0, 80),
50
+ });
51
+ }
52
+
53
+ // 2. URL force-unwrap — URL(string: ...)! pattern
54
+ for (const m of raw.matchAll(/\bURL\s*\(\s*string\s*:\s*[^)]+\)\s*!/g)) {
55
+ findings.push({
56
+ id: `swift:url-force-unwrap:${file}:${_line(raw, m.index)}`,
57
+ file, line: _line(raw, m.index),
58
+ vuln: 'URL force-unwrap (URL(string:)! ) — crash or attacker-controlled URL',
59
+ severity: 'medium',
60
+ family: 'swift-force-unwrap',
61
+ cwe: 'CWE-755',
62
+ confidence: 0.75,
63
+ description: 'Force-unwrapping URL(string:) crashes the app on a malformed URL. If the input comes from a deep link, the attacker can either crash the app or — paired with downstream logic — supply a URL the developer assumed would be validated.',
64
+ remediation: 'Use guard let url = URL(string: input), url.scheme == "https" else { return } with explicit scheme/host validation.',
65
+ snippet: m[0],
66
+ });
67
+ }
68
+
69
+ // 3. WKWebView with JS enabled, no navigationDelegate visible
70
+ if (/\bWKWebView\b/.test(raw) || /\bWKWebViewConfiguration\b/.test(raw)) {
71
+ // Detect explicit `configuration.preferences.javaScriptEnabled = true` or
72
+ // `defaultWebpagePreferences.allowsContentJavaScript = true` without
73
+ // a corresponding navigationDelegate assignment in the file.
74
+ const jsEnabledRe = /\b(?:javaScriptEnabled\s*=\s*true|allowsContentJavaScript\s*=\s*true)\b/;
75
+ const hasNavDelegate = /\b(?:webView\.navigationDelegate\s*=|navigationDelegate\s*:\s*)/.test(raw);
76
+ if (jsEnabledRe.test(raw) && !hasNavDelegate) {
77
+ const m = jsEnabledRe.exec(raw);
78
+ findings.push({
79
+ id: `swift:webview-js-no-delegate:${file}:${_line(raw, m.index)}`,
80
+ file, line: _line(raw, m.index),
81
+ vuln: 'WKWebView with JavaScript enabled and no navigationDelegate to validate URLs',
82
+ severity: 'high',
83
+ family: 'swift-webview-unsafe',
84
+ cwe: 'CWE-829',
85
+ confidence: 0.7,
86
+ description: 'WKWebView with JS enabled and no navigationDelegate accepts navigation to any URL. Deep-link or message-passing attacks can pivot the WebView to attacker pages and execute JS that bridges back to native via WKScriptMessageHandler.',
87
+ remediation: 'Assign a navigationDelegate that vets webView(_:decidePolicyFor:decisionHandler:); allow-list the host and reject everything else with .cancel.',
88
+ });
89
+ }
90
+ }
91
+
92
+ // 4. Deep-link / universal-link handler without allow-list
93
+ // Look for application(_:open:options:) or scene(_:openURLContexts:) that doesn't validate
94
+ for (const m of raw.matchAll(/func\s+(?:application\s*\([^)]*open[^)]*\)|scene\s*\([^)]*openURLContexts[^)]*\))[^{]*\{([\s\S]{0,800}?)(?=\n\s*func\s|\}\n)/g)) {
95
+ const body = m[1];
96
+ if (!body) continue;
97
+ // Skip if body validates host/scheme/path against allow-list constants.
98
+ if (/\b(?:host\s*==|scheme\s*==|allowedHosts|allowedSchemes|allowedPaths)\b/.test(body)) continue;
99
+ findings.push({
100
+ id: `swift:deeplink-no-validate:${file}:${_line(raw, m.index)}`,
101
+ file, line: _line(raw, m.index),
102
+ vuln: 'Deep-link / universal-link handler does not validate URL scheme / host / path',
103
+ severity: 'high',
104
+ family: 'swift-deeplink-unsafe',
105
+ cwe: 'CWE-20',
106
+ confidence: 0.65,
107
+ description: 'An openURL handler accepts the incoming URL without checking scheme / host / path against an allow-list. Attackers can route the app to internal screens, trigger sensitive flows, or chain into XSS via WebView.',
108
+ remediation: 'Add guard let host = url.host, allowedHosts.contains(host), let path = ..., allowedPaths.contains(path) else { return false } at the top of the handler.',
109
+ });
110
+ }
111
+
112
+ // 5. Cleartext HTTP URL literal
113
+ for (const m of raw.matchAll(/\bURL\s*\(\s*string\s*:\s*['"]http:\/\/(?!localhost|127\.0\.0\.1)[^'"]+['"]/g)) {
114
+ findings.push({
115
+ id: `swift:cleartext-http:${file}:${_line(raw, m.index)}`,
116
+ file, line: _line(raw, m.index),
117
+ vuln: 'Cleartext HTTP URL literal in Swift source',
118
+ severity: 'medium',
119
+ family: 'swift-cleartext-http',
120
+ cwe: 'CWE-319',
121
+ confidence: 0.85,
122
+ description: 'A hard-coded http:// URL ships cleartext on the wire. Even if ATS is enabled, this is an explicit bypass.',
123
+ remediation: 'Use https://. If the endpoint genuinely lacks TLS, set up your own TLS-terminating proxy.',
124
+ snippet: m[0].slice(0, 80),
125
+ });
126
+ }
127
+
128
+ // 6. Hardcoded API keys
129
+ for (const { re, label } of _CRED_RE) {
130
+ const m = re.exec(raw);
131
+ if (!m) continue;
132
+ findings.push({
133
+ id: `swift:hardcoded-${label.toLowerCase().replace(/\s+/g, '-')}:${file}:${_line(raw, m.index)}`,
134
+ file, line: _line(raw, m.index),
135
+ vuln: `Hardcoded ${label} in Swift source`,
136
+ severity: 'critical',
137
+ family: 'swift-hardcoded-credential',
138
+ cwe: 'CWE-798',
139
+ confidence: 0.95,
140
+ description: 'Decompiling a Swift binary is trivial — `strings`, `class-dump`, Ghidra all extract literal credentials in seconds. Hardcoded keys in mobile apps are routinely scraped at scale.',
141
+ remediation: 'Read from ProcessInfo.processInfo.environment["API_KEY"] (with .xcconfig for build-time config). For runtime secrets, fetch from a backend service authenticated by the user\'s session.',
142
+ snippet: m[0].slice(0, 8) + '...' + m[0].slice(-4),
143
+ });
144
+ }
145
+
146
+ // 7. NSAllowsArbitraryLoads = true in code (Info.plist is handled in mobile-manifest)
147
+ for (const m of raw.matchAll(/NSAllowsArbitraryLoads\s*[:=]\s*true\b/g)) {
148
+ findings.push({
149
+ id: `swift:ats-bypass-code:${file}:${_line(raw, m.index)}`,
150
+ file, line: _line(raw, m.index),
151
+ vuln: 'NSAllowsArbitraryLoads = true — App Transport Security bypassed in code',
152
+ severity: 'high',
153
+ family: 'swift-ats-disabled',
154
+ cwe: 'CWE-319',
155
+ confidence: 0.95,
156
+ description: 'NSAllowsArbitraryLoads=true disables ATS, allowing the app to make plaintext HTTP calls to arbitrary hosts.',
157
+ remediation: 'Remove the bypass. If a specific domain needs HTTP, scope it via NSExceptionDomains in Info.plist; never use NSAllowsArbitraryLoads globally.',
158
+ });
159
+ }
160
+
161
+ return findings;
162
+ }
@@ -0,0 +1,95 @@
1
+ import { blankComments } from './_comment-strip.js';
2
+ // TOCTOU — time-of-check / time-of-use races.
3
+ //
4
+ // Two dominant shapes:
5
+ // 1. Filesystem race: fs.access(p) → fs.open(p)
6
+ // os.access(p) → open(p)
7
+ // os.path.exists(p) → open(p)
8
+ // The classic CWE-367. Whatever the check learned about `p` may not hold
9
+ // by the time the use happens; an attacker who can swap symlinks wins.
10
+ //
11
+ // 2. Auth race: if (!user.isAdmin) return; doSensitiveThing(user)
12
+ // if user.role != 'admin': return; do_sensitive(user)
13
+ // Less concrete — these are gated on the check happening *first* — but
14
+ // when the check value can be mutated by a concurrent request, we flag
15
+ // it. Heuristic: between the guard and the use, the code awaits
16
+ // something. The await is the race window.
17
+
18
+ const FS_CHECK_THEN_OPEN_JS = /\bfs\s*\.\s*(?:access|exists|stat|statSync|existsSync)\s*\(\s*([^,)]+?)\s*[,)][^]*?\bfs\s*\.\s*(?:open|openSync|readFile|readFileSync|writeFile|writeFileSync|createReadStream|createWriteStream)\s*\(\s*\1/g;
19
+
20
+ const FS_CHECK_THEN_OPEN_PY = /\b(?:os\.access|os\.path\.exists|os\.path\.isfile|os\.stat)\s*\(\s*([^,)]+?)\s*\)[^]*?\b(?:open|os\.open|shutil\.copy|shutil\.move)\s*\(\s*\1/g;
21
+
22
+ const AUTH_CHECK_AWAIT_USE_JS = /\bif\s*\(\s*!?\s*\w+\s*(?:\.|->)\s*(?:isAdmin|isOwner|hasRole|role)\b[^)]*\)\s*[^]*?\bawait\b[^]*?\b(?:save|update|create|destroy|delete|withdraw|transfer|approve|grant)\s*\(/g;
23
+
24
+ function lineOf(raw, idx) { return raw.substring(0, idx).split('\n').length; }
25
+
26
+ export function scanTOCTOU(fp, raw) {
27
+ if (!raw || raw.length > 500_000) return [];
28
+ const findings = [];
29
+ const seen = new Set();
30
+ const push = (f) => { if (!seen.has(f.id)) { seen.add(f.id); findings.push(f); } };
31
+
32
+ if (/\.(?:js|jsx|ts|tsx|mjs|cjs)$/i.test(fp)) {
33
+ const code = blankComments(raw);
34
+ let m;
35
+ const r = new RegExp(FS_CHECK_THEN_OPEN_JS.source, FS_CHECK_THEN_OPEN_JS.flags);
36
+ while ((m = r.exec(code))) {
37
+ const line = lineOf(raw, m.index);
38
+ push({
39
+ id: `toctou-fs:${fp}:${line}`,
40
+ file: fp, line,
41
+ vuln: 'TOCTOU: file existence/permission check before open',
42
+ severity: 'medium',
43
+ cwe: 'CWE-367',
44
+ stride: 'Tampering',
45
+ snippet: (raw.split('\n')[line - 1] || '').trim().slice(0, 200),
46
+ remediation: 'Drop the existence/permission check and rely on `open()` to fail atomically — handle the resulting error. The check-then-open pattern is a TOCTOU race: an attacker who can swap symlinks between the check and the open wins. If you need a permission test, do it on the `open()` result\'s `fstat`, not on the path before opening.',
47
+ parser: 'TOCTOU',
48
+ confidence: 0.70,
49
+ });
50
+ }
51
+ const ar = new RegExp(AUTH_CHECK_AWAIT_USE_JS.source, AUTH_CHECK_AWAIT_USE_JS.flags);
52
+ while ((m = ar.exec(code))) {
53
+ const block = m[0];
54
+ // Heuristic: only flag if the guard variable is potentially re-read after await.
55
+ // Approximation: guard uses `await` *between* check and side effect.
56
+ if (!/await/.test(block)) continue;
57
+ const line = lineOf(raw, m.index);
58
+ push({
59
+ id: `toctou-auth:${fp}:${line}`,
60
+ file: fp, line,
61
+ vuln: 'TOCTOU: auth check then await then sensitive action',
62
+ severity: 'medium',
63
+ cwe: 'CWE-367',
64
+ stride: 'Elevation of Privilege',
65
+ snippet: (raw.split('\n')[line - 1] || '').trim().slice(0, 200),
66
+ remediation: 'Re-fetch and re-check the authorization right before the sensitive action, ideally inside the same transaction. If the user\'s role can be revoked or downgraded mid-request, the check at the top of the handler is not load-bearing. Pattern: `BEGIN TX → SELECT FOR UPDATE → check role → side effect → COMMIT`.',
67
+ parser: 'TOCTOU',
68
+ confidence: 0.55,
69
+ });
70
+ }
71
+ }
72
+
73
+ if (/\.py$/i.test(fp)) {
74
+ const code = blankComments(raw, 'py');
75
+ let m;
76
+ const r = new RegExp(FS_CHECK_THEN_OPEN_PY.source, FS_CHECK_THEN_OPEN_PY.flags);
77
+ while ((m = r.exec(code))) {
78
+ const line = lineOf(raw, m.index);
79
+ push({
80
+ id: `toctou-fs:${fp}:${line}`,
81
+ file: fp, line,
82
+ vuln: 'TOCTOU: file existence/permission check before open',
83
+ severity: 'medium',
84
+ cwe: 'CWE-367',
85
+ stride: 'Tampering',
86
+ snippet: (raw.split('\n')[line - 1] || '').trim().slice(0, 200),
87
+ remediation: 'Use `try: open(...) except OSError` instead of the `if os.path.exists(...)` pre-check. The pre-check creates a TOCTOU window an attacker who can swap symlinks can exploit. For permission tests, use `os.open(path, os.O_RDONLY | os.O_NOFOLLOW)` then `os.fstat()`.',
88
+ parser: 'TOCTOU',
89
+ confidence: 0.70,
90
+ });
91
+ }
92
+ }
93
+
94
+ return findings;
95
+ }