@elizaos/skills 2.0.0-alpha.3

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 (371) hide show
  1. package/README.md +126 -0
  2. package/package.json +53 -0
  3. package/skills/1password/SKILL.md +70 -0
  4. package/skills/1password/references/cli-examples.md +29 -0
  5. package/skills/1password/references/get-started.md +17 -0
  6. package/skills/apple-notes/SKILL.md +77 -0
  7. package/skills/apple-reminders/SKILL.md +96 -0
  8. package/skills/bear-notes/SKILL.md +107 -0
  9. package/skills/bird/SKILL.md +224 -0
  10. package/skills/blogwatcher/SKILL.md +69 -0
  11. package/skills/blucli/SKILL.md +47 -0
  12. package/skills/bluebubbles/SKILL.md +131 -0
  13. package/skills/camsnap/SKILL.md +45 -0
  14. package/skills/canvas/SKILL.md +203 -0
  15. package/skills/clawhub/SKILL.md +77 -0
  16. package/skills/coding-agent/SKILL.md +284 -0
  17. package/skills/discord/SKILL.md +578 -0
  18. package/skills/eightctl/SKILL.md +50 -0
  19. package/skills/food-order/SKILL.md +48 -0
  20. package/skills/gemini/SKILL.md +43 -0
  21. package/skills/gifgrep/SKILL.md +79 -0
  22. package/skills/github/SKILL.md +77 -0
  23. package/skills/gog/SKILL.md +116 -0
  24. package/skills/goplaces/SKILL.md +52 -0
  25. package/skills/healthcheck/SKILL.md +245 -0
  26. package/skills/himalaya/SKILL.md +257 -0
  27. package/skills/himalaya/references/configuration.md +184 -0
  28. package/skills/himalaya/references/message-composition.md +199 -0
  29. package/skills/imsg/SKILL.md +74 -0
  30. package/skills/local-places/SERVER_README.md +101 -0
  31. package/skills/local-places/SKILL.md +102 -0
  32. package/skills/local-places/pyproject.toml +21 -0
  33. package/skills/local-places/src/local_places/__init__.py +2 -0
  34. package/skills/local-places/src/local_places/google_places.py +314 -0
  35. package/skills/local-places/src/local_places/main.py +65 -0
  36. package/skills/local-places/src/local_places/schemas.py +107 -0
  37. package/skills/mcporter/SKILL.md +61 -0
  38. package/skills/model-usage/SKILL.md +69 -0
  39. package/skills/model-usage/references/codexbar-cli.md +33 -0
  40. package/skills/model-usage/scripts/model_usage.py +310 -0
  41. package/skills/nano-banana-pro/SKILL.md +58 -0
  42. package/skills/nano-banana-pro/scripts/generate_image.py +184 -0
  43. package/skills/nano-pdf/SKILL.md +38 -0
  44. package/skills/notion/SKILL.md +172 -0
  45. package/skills/obsidian/SKILL.md +81 -0
  46. package/skills/openai-image-gen/SKILL.md +89 -0
  47. package/skills/openai-image-gen/scripts/gen.py +240 -0
  48. package/skills/openai-whisper/SKILL.md +38 -0
  49. package/skills/openai-whisper-api/SKILL.md +52 -0
  50. package/skills/openai-whisper-api/scripts/transcribe.sh +85 -0
  51. package/skills/openhue/SKILL.md +51 -0
  52. package/skills/oracle/SKILL.md +125 -0
  53. package/skills/ordercli/SKILL.md +78 -0
  54. package/skills/peekaboo/SKILL.md +190 -0
  55. package/skills/sag/SKILL.md +87 -0
  56. package/skills/security-ask-questions-if-underspecified/.claude-plugin/plugin.json +10 -0
  57. package/skills/security-ask-questions-if-underspecified/README.md +24 -0
  58. package/skills/security-ask-questions-if-underspecified/skills/ask-questions-if-underspecified/SKILL.md +85 -0
  59. package/skills/security-audit-context-building/.claude-plugin/plugin.json +10 -0
  60. package/skills/security-audit-context-building/README.md +58 -0
  61. package/skills/security-audit-context-building/commands/audit-context.md +21 -0
  62. package/skills/security-audit-context-building/skills/audit-context-building/SKILL.md +297 -0
  63. package/skills/security-audit-context-building/skills/audit-context-building/resources/COMPLETENESS_CHECKLIST.md +47 -0
  64. package/skills/security-audit-context-building/skills/audit-context-building/resources/FUNCTION_MICRO_ANALYSIS_EXAMPLE.md +355 -0
  65. package/skills/security-audit-context-building/skills/audit-context-building/resources/OUTPUT_REQUIREMENTS.md +71 -0
  66. package/skills/security-building-secure-contracts/.claude-plugin/plugin.json +10 -0
  67. package/skills/security-building-secure-contracts/README.md +241 -0
  68. package/skills/security-building-secure-contracts/skills/algorand-vulnerability-scanner/SKILL.md +284 -0
  69. package/skills/security-building-secure-contracts/skills/algorand-vulnerability-scanner/resources/VULNERABILITY_PATTERNS.md +405 -0
  70. package/skills/security-building-secure-contracts/skills/audit-prep-assistant/SKILL.md +409 -0
  71. package/skills/security-building-secure-contracts/skills/cairo-vulnerability-scanner/SKILL.md +329 -0
  72. package/skills/security-building-secure-contracts/skills/cairo-vulnerability-scanner/resources/VULNERABILITY_PATTERNS.md +722 -0
  73. package/skills/security-building-secure-contracts/skills/code-maturity-assessor/SKILL.md +218 -0
  74. package/skills/security-building-secure-contracts/skills/code-maturity-assessor/resources/ASSESSMENT_CRITERIA.md +355 -0
  75. package/skills/security-building-secure-contracts/skills/code-maturity-assessor/resources/EXAMPLE_REPORT.md +248 -0
  76. package/skills/security-building-secure-contracts/skills/code-maturity-assessor/resources/REPORT_FORMAT.md +33 -0
  77. package/skills/security-building-secure-contracts/skills/cosmos-vulnerability-scanner/SKILL.md +334 -0
  78. package/skills/security-building-secure-contracts/skills/cosmos-vulnerability-scanner/resources/VULNERABILITY_PATTERNS.md +740 -0
  79. package/skills/security-building-secure-contracts/skills/guidelines-advisor/SKILL.md +252 -0
  80. package/skills/security-building-secure-contracts/skills/guidelines-advisor/resources/ASSESSMENT_AREAS.md +329 -0
  81. package/skills/security-building-secure-contracts/skills/guidelines-advisor/resources/DELIVERABLES.md +118 -0
  82. package/skills/security-building-secure-contracts/skills/guidelines-advisor/resources/EXAMPLE_REPORT.md +298 -0
  83. package/skills/security-building-secure-contracts/skills/secure-workflow-guide/SKILL.md +161 -0
  84. package/skills/security-building-secure-contracts/skills/secure-workflow-guide/resources/EXAMPLE_REPORT.md +279 -0
  85. package/skills/security-building-secure-contracts/skills/secure-workflow-guide/resources/WORKFLOW_STEPS.md +132 -0
  86. package/skills/security-building-secure-contracts/skills/solana-vulnerability-scanner/SKILL.md +389 -0
  87. package/skills/security-building-secure-contracts/skills/solana-vulnerability-scanner/resources/VULNERABILITY_PATTERNS.md +669 -0
  88. package/skills/security-building-secure-contracts/skills/substrate-vulnerability-scanner/SKILL.md +298 -0
  89. package/skills/security-building-secure-contracts/skills/substrate-vulnerability-scanner/resources/VULNERABILITY_PATTERNS.md +791 -0
  90. package/skills/security-building-secure-contracts/skills/token-integration-analyzer/SKILL.md +362 -0
  91. package/skills/security-building-secure-contracts/skills/token-integration-analyzer/resources/ASSESSMENT_CATEGORIES.md +571 -0
  92. package/skills/security-building-secure-contracts/skills/token-integration-analyzer/resources/REPORT_TEMPLATES.md +141 -0
  93. package/skills/security-building-secure-contracts/skills/ton-vulnerability-scanner/SKILL.md +388 -0
  94. package/skills/security-building-secure-contracts/skills/ton-vulnerability-scanner/resources/VULNERABILITY_PATTERNS.md +595 -0
  95. package/skills/security-burpsuite-project-parser/.claude-plugin/plugin.json +10 -0
  96. package/skills/security-burpsuite-project-parser/README.md +103 -0
  97. package/skills/security-burpsuite-project-parser/commands/burp-search.md +18 -0
  98. package/skills/security-burpsuite-project-parser/skills/SKILL.md +358 -0
  99. package/skills/security-burpsuite-project-parser/skills/scripts/burp-search.sh +99 -0
  100. package/skills/security-claude-in-chrome-troubleshooting/.claude-plugin/plugin.json +8 -0
  101. package/skills/security-claude-in-chrome-troubleshooting/README.md +31 -0
  102. package/skills/security-claude-in-chrome-troubleshooting/skills/claude-in-chrome-troubleshooting/SKILL.md +251 -0
  103. package/skills/security-constant-time-analysis/.claude-plugin/plugin.json +9 -0
  104. package/skills/security-constant-time-analysis/README.md +381 -0
  105. package/skills/security-constant-time-analysis/commands/ct-check.md +20 -0
  106. package/skills/security-constant-time-analysis/ct_analyzer/__init__.py +49 -0
  107. package/skills/security-constant-time-analysis/ct_analyzer/analyzer.py +1284 -0
  108. package/skills/security-constant-time-analysis/ct_analyzer/script_analyzers.py +3081 -0
  109. package/skills/security-constant-time-analysis/ct_analyzer/tests/__init__.py +1 -0
  110. package/skills/security-constant-time-analysis/ct_analyzer/tests/test_analyzer.py +1397 -0
  111. package/skills/security-constant-time-analysis/ct_analyzer/tests/test_samples/bn_excerpt.js +205 -0
  112. package/skills/security-constant-time-analysis/ct_analyzer/tests/test_samples/decompose_constant_time.c +181 -0
  113. package/skills/security-constant-time-analysis/ct_analyzer/tests/test_samples/decompose_vulnerable.c +74 -0
  114. package/skills/security-constant-time-analysis/ct_analyzer/tests/test_samples/decompose_vulnerable.go +78 -0
  115. package/skills/security-constant-time-analysis/ct_analyzer/tests/test_samples/decompose_vulnerable.rs +92 -0
  116. package/skills/security-constant-time-analysis/ct_analyzer/tests/test_samples/vulnerable.cs +174 -0
  117. package/skills/security-constant-time-analysis/ct_analyzer/tests/test_samples/vulnerable.java +161 -0
  118. package/skills/security-constant-time-analysis/ct_analyzer/tests/test_samples/vulnerable.kt +181 -0
  119. package/skills/security-constant-time-analysis/ct_analyzer/tests/test_samples/vulnerable.php +140 -0
  120. package/skills/security-constant-time-analysis/ct_analyzer/tests/test_samples/vulnerable.py +252 -0
  121. package/skills/security-constant-time-analysis/ct_analyzer/tests/test_samples/vulnerable.rb +188 -0
  122. package/skills/security-constant-time-analysis/ct_analyzer/tests/test_samples/vulnerable.swift +199 -0
  123. package/skills/security-constant-time-analysis/ct_analyzer/tests/test_samples/vulnerable.ts +154 -0
  124. package/skills/security-constant-time-analysis/pyproject.toml +52 -0
  125. package/skills/security-constant-time-analysis/skills/constant-time-analysis/README.md +90 -0
  126. package/skills/security-constant-time-analysis/skills/constant-time-analysis/SKILL.md +219 -0
  127. package/skills/security-constant-time-analysis/skills/constant-time-analysis/references/compiled.md +129 -0
  128. package/skills/security-constant-time-analysis/skills/constant-time-analysis/references/javascript.md +136 -0
  129. package/skills/security-constant-time-analysis/skills/constant-time-analysis/references/kotlin.md +252 -0
  130. package/skills/security-constant-time-analysis/skills/constant-time-analysis/references/php.md +172 -0
  131. package/skills/security-constant-time-analysis/skills/constant-time-analysis/references/python.md +179 -0
  132. package/skills/security-constant-time-analysis/skills/constant-time-analysis/references/ruby.md +198 -0
  133. package/skills/security-constant-time-analysis/skills/constant-time-analysis/references/swift.md +288 -0
  134. package/skills/security-constant-time-analysis/skills/constant-time-analysis/references/vm-compiled.md +354 -0
  135. package/skills/security-constant-time-analysis/uv.lock +8 -0
  136. package/skills/security-culture-index/.claude-plugin/plugin.json +8 -0
  137. package/skills/security-culture-index/README.md +79 -0
  138. package/skills/security-culture-index/skills/interpreting-culture-index/SKILL.md +293 -0
  139. package/skills/security-culture-index/skills/interpreting-culture-index/references/anti-patterns.md +255 -0
  140. package/skills/security-culture-index/skills/interpreting-culture-index/references/conversation-starters.md +408 -0
  141. package/skills/security-culture-index/skills/interpreting-culture-index/references/interview-trait-signals.md +253 -0
  142. package/skills/security-culture-index/skills/interpreting-culture-index/references/motivators.md +158 -0
  143. package/skills/security-culture-index/skills/interpreting-culture-index/references/patterns-archetypes.md +147 -0
  144. package/skills/security-culture-index/skills/interpreting-culture-index/references/primary-traits.md +307 -0
  145. package/skills/security-culture-index/skills/interpreting-culture-index/references/secondary-traits.md +228 -0
  146. package/skills/security-culture-index/skills/interpreting-culture-index/references/team-composition.md +148 -0
  147. package/skills/security-culture-index/skills/interpreting-culture-index/scripts/check_deps.py +108 -0
  148. package/skills/security-culture-index/skills/interpreting-culture-index/scripts/culture_index/__init__.py +20 -0
  149. package/skills/security-culture-index/skills/interpreting-culture-index/scripts/culture_index/constants.py +122 -0
  150. package/skills/security-culture-index/skills/interpreting-culture-index/scripts/culture_index/extract.py +187 -0
  151. package/skills/security-culture-index/skills/interpreting-culture-index/scripts/culture_index/models.py +16 -0
  152. package/skills/security-culture-index/skills/interpreting-culture-index/scripts/culture_index/opencv_extractor.py +520 -0
  153. package/skills/security-culture-index/skills/interpreting-culture-index/scripts/extract_pdf.py +237 -0
  154. package/skills/security-culture-index/skills/interpreting-culture-index/scripts/pyproject.toml +18 -0
  155. package/skills/security-culture-index/skills/interpreting-culture-index/templates/burnout-report.md +113 -0
  156. package/skills/security-culture-index/skills/interpreting-culture-index/templates/comparison-report.md +103 -0
  157. package/skills/security-culture-index/skills/interpreting-culture-index/templates/hiring-profile.md +127 -0
  158. package/skills/security-culture-index/skills/interpreting-culture-index/templates/individual-report.md +85 -0
  159. package/skills/security-culture-index/skills/interpreting-culture-index/templates/predicted-profile.md +165 -0
  160. package/skills/security-culture-index/skills/interpreting-culture-index/templates/team-report.md +109 -0
  161. package/skills/security-culture-index/skills/interpreting-culture-index/workflows/analyze-team.md +188 -0
  162. package/skills/security-culture-index/skills/interpreting-culture-index/workflows/coach-manager.md +267 -0
  163. package/skills/security-culture-index/skills/interpreting-culture-index/workflows/compare-profiles.md +188 -0
  164. package/skills/security-culture-index/skills/interpreting-culture-index/workflows/define-hiring-profile.md +220 -0
  165. package/skills/security-culture-index/skills/interpreting-culture-index/workflows/detect-burnout.md +206 -0
  166. package/skills/security-culture-index/skills/interpreting-culture-index/workflows/extract-from-pdf.md +121 -0
  167. package/skills/security-culture-index/skills/interpreting-culture-index/workflows/interpret-individual.md +183 -0
  168. package/skills/security-culture-index/skills/interpreting-culture-index/workflows/interview-debrief.md +234 -0
  169. package/skills/security-culture-index/skills/interpreting-culture-index/workflows/mediate-conflict.md +306 -0
  170. package/skills/security-culture-index/skills/interpreting-culture-index/workflows/plan-onboarding.md +322 -0
  171. package/skills/security-culture-index/skills/interpreting-culture-index/workflows/predict-from-interview.md +250 -0
  172. package/skills/security-differential-review/.claude-plugin/plugin.json +10 -0
  173. package/skills/security-differential-review/README.md +109 -0
  174. package/skills/security-differential-review/commands/diff-review.md +21 -0
  175. package/skills/security-differential-review/skills/differential-review/SKILL.md +220 -0
  176. package/skills/security-differential-review/skills/differential-review/adversarial.md +203 -0
  177. package/skills/security-differential-review/skills/differential-review/methodology.md +234 -0
  178. package/skills/security-differential-review/skills/differential-review/patterns.md +300 -0
  179. package/skills/security-differential-review/skills/differential-review/reporting.md +369 -0
  180. package/skills/security-dwarf-expert/.claude-plugin/plugin.json +10 -0
  181. package/skills/security-dwarf-expert/README.md +38 -0
  182. package/skills/security-dwarf-expert/skills/dwarf-expert/SKILL.md +93 -0
  183. package/skills/security-dwarf-expert/skills/dwarf-expert/reference/coding.md +31 -0
  184. package/skills/security-dwarf-expert/skills/dwarf-expert/reference/dwarfdump.md +50 -0
  185. package/skills/security-dwarf-expert/skills/dwarf-expert/reference/readelf.md +8 -0
  186. package/skills/security-entry-point-analyzer/.claude-plugin/plugin.json +10 -0
  187. package/skills/security-entry-point-analyzer/README.md +74 -0
  188. package/skills/security-entry-point-analyzer/commands/entry-points.md +18 -0
  189. package/skills/security-entry-point-analyzer/skills/entry-point-analyzer/SKILL.md +251 -0
  190. package/skills/security-entry-point-analyzer/skills/entry-point-analyzer/references/cosmwasm.md +182 -0
  191. package/skills/security-entry-point-analyzer/skills/entry-point-analyzer/references/move-aptos.md +107 -0
  192. package/skills/security-entry-point-analyzer/skills/entry-point-analyzer/references/move-sui.md +87 -0
  193. package/skills/security-entry-point-analyzer/skills/entry-point-analyzer/references/solana.md +155 -0
  194. package/skills/security-entry-point-analyzer/skills/entry-point-analyzer/references/solidity.md +135 -0
  195. package/skills/security-entry-point-analyzer/skills/entry-point-analyzer/references/ton.md +185 -0
  196. package/skills/security-entry-point-analyzer/skills/entry-point-analyzer/references/vyper.md +141 -0
  197. package/skills/security-firebase-apk-scanner/.claude-plugin/plugin.json +10 -0
  198. package/skills/security-firebase-apk-scanner/README.md +85 -0
  199. package/skills/security-firebase-apk-scanner/commands/scan-apk.md +18 -0
  200. package/skills/security-firebase-apk-scanner/scanner.sh +1408 -0
  201. package/skills/security-firebase-apk-scanner/skills/firebase-apk-scanner/SKILL.md +197 -0
  202. package/skills/security-firebase-apk-scanner/skills/firebase-apk-scanner/references/vulnerabilities.md +803 -0
  203. package/skills/security-fix-review/.claude-plugin/plugin.json +13 -0
  204. package/skills/security-fix-review/README.md +118 -0
  205. package/skills/security-fix-review/commands/fix-review.md +24 -0
  206. package/skills/security-fix-review/skills/fix-review/SKILL.md +264 -0
  207. package/skills/security-fix-review/skills/fix-review/references/bug-detection.md +408 -0
  208. package/skills/security-fix-review/skills/fix-review/references/finding-matching.md +298 -0
  209. package/skills/security-fix-review/skills/fix-review/references/report-parsing.md +398 -0
  210. package/skills/security-insecure-defaults/.claude-plugin/plugin.json +10 -0
  211. package/skills/security-insecure-defaults/README.md +45 -0
  212. package/skills/security-insecure-defaults/skills/insecure-defaults/SKILL.md +117 -0
  213. package/skills/security-insecure-defaults/skills/insecure-defaults/references/examples.md +409 -0
  214. package/skills/security-modern-python/.claude-plugin/plugin.json +10 -0
  215. package/skills/security-modern-python/README.md +58 -0
  216. package/skills/security-modern-python/hooks/hooks.json +16 -0
  217. package/skills/security-modern-python/hooks/intercept-legacy-python.bats +388 -0
  218. package/skills/security-modern-python/hooks/intercept-legacy-python.sh +109 -0
  219. package/skills/security-modern-python/hooks/test_helper.bash +75 -0
  220. package/skills/security-modern-python/skills/modern-python/SKILL.md +333 -0
  221. package/skills/security-modern-python/skills/modern-python/references/dependabot.md +43 -0
  222. package/skills/security-modern-python/skills/modern-python/references/migration-checklist.md +141 -0
  223. package/skills/security-modern-python/skills/modern-python/references/pep723-scripts.md +259 -0
  224. package/skills/security-modern-python/skills/modern-python/references/prek.md +211 -0
  225. package/skills/security-modern-python/skills/modern-python/references/pyproject.md +254 -0
  226. package/skills/security-modern-python/skills/modern-python/references/ruff-config.md +240 -0
  227. package/skills/security-modern-python/skills/modern-python/references/security-setup.md +255 -0
  228. package/skills/security-modern-python/skills/modern-python/references/testing.md +284 -0
  229. package/skills/security-modern-python/skills/modern-python/references/uv-commands.md +200 -0
  230. package/skills/security-modern-python/skills/modern-python/templates/dependabot.yml +36 -0
  231. package/skills/security-modern-python/skills/modern-python/templates/pre-commit-config.yaml +66 -0
  232. package/skills/security-property-based-testing/.claude-plugin/plugin.json +9 -0
  233. package/skills/security-property-based-testing/README.md +47 -0
  234. package/skills/security-property-based-testing/skills/property-based-testing/README.md +88 -0
  235. package/skills/security-property-based-testing/skills/property-based-testing/SKILL.md +109 -0
  236. package/skills/security-property-based-testing/skills/property-based-testing/references/design.md +191 -0
  237. package/skills/security-property-based-testing/skills/property-based-testing/references/generating.md +200 -0
  238. package/skills/security-property-based-testing/skills/property-based-testing/references/libraries.md +130 -0
  239. package/skills/security-property-based-testing/skills/property-based-testing/references/refactoring.md +181 -0
  240. package/skills/security-property-based-testing/skills/property-based-testing/references/reviewing.md +209 -0
  241. package/skills/security-property-based-testing/skills/property-based-testing/references/strategies.md +124 -0
  242. package/skills/semgrep-rule-creator/.claude-plugin/plugin.json +8 -0
  243. package/skills/semgrep-rule-creator/README.md +43 -0
  244. package/skills/semgrep-rule-creator/commands/semgrep-rule.md +26 -0
  245. package/skills/semgrep-rule-creator/skills/semgrep-rule-creator/SKILL.md +168 -0
  246. package/skills/semgrep-rule-creator/skills/semgrep-rule-creator/references/quick-reference.md +203 -0
  247. package/skills/semgrep-rule-creator/skills/semgrep-rule-creator/references/workflow.md +240 -0
  248. package/skills/semgrep-rule-variant-creator/.claude-plugin/plugin.json +9 -0
  249. package/skills/semgrep-rule-variant-creator/README.md +86 -0
  250. package/skills/semgrep-rule-variant-creator/skills/semgrep-rule-variant-creator/SKILL.md +205 -0
  251. package/skills/semgrep-rule-variant-creator/skills/semgrep-rule-variant-creator/references/applicability-analysis.md +250 -0
  252. package/skills/semgrep-rule-variant-creator/skills/semgrep-rule-variant-creator/references/language-syntax-guide.md +324 -0
  253. package/skills/semgrep-rule-variant-creator/skills/semgrep-rule-variant-creator/references/workflow.md +518 -0
  254. package/skills/session-logs/SKILL.md +115 -0
  255. package/skills/sharp-edges/.claude-plugin/plugin.json +10 -0
  256. package/skills/sharp-edges/README.md +48 -0
  257. package/skills/sharp-edges/skills/sharp-edges/SKILL.md +292 -0
  258. package/skills/sharp-edges/skills/sharp-edges/references/auth-patterns.md +252 -0
  259. package/skills/sharp-edges/skills/sharp-edges/references/case-studies.md +274 -0
  260. package/skills/sharp-edges/skills/sharp-edges/references/config-patterns.md +333 -0
  261. package/skills/sharp-edges/skills/sharp-edges/references/crypto-apis.md +190 -0
  262. package/skills/sharp-edges/skills/sharp-edges/references/lang-c.md +205 -0
  263. package/skills/sharp-edges/skills/sharp-edges/references/lang-csharp.md +285 -0
  264. package/skills/sharp-edges/skills/sharp-edges/references/lang-go.md +270 -0
  265. package/skills/sharp-edges/skills/sharp-edges/references/lang-java.md +263 -0
  266. package/skills/sharp-edges/skills/sharp-edges/references/lang-javascript.md +269 -0
  267. package/skills/sharp-edges/skills/sharp-edges/references/lang-kotlin.md +265 -0
  268. package/skills/sharp-edges/skills/sharp-edges/references/lang-php.md +245 -0
  269. package/skills/sharp-edges/skills/sharp-edges/references/lang-python.md +274 -0
  270. package/skills/sharp-edges/skills/sharp-edges/references/lang-ruby.md +273 -0
  271. package/skills/sharp-edges/skills/sharp-edges/references/lang-rust.md +272 -0
  272. package/skills/sharp-edges/skills/sharp-edges/references/lang-swift.md +287 -0
  273. package/skills/sharp-edges/skills/sharp-edges/references/language-specific.md +588 -0
  274. package/skills/sherpa-onnx-tts/SKILL.md +103 -0
  275. package/skills/sherpa-onnx-tts/bin/sherpa-onnx-tts +178 -0
  276. package/skills/skill-creator/SKILL.md +370 -0
  277. package/skills/skill-creator/license.txt +202 -0
  278. package/skills/skill-creator/scripts/init_skill.py +378 -0
  279. package/skills/skill-creator/scripts/package_skill.py +111 -0
  280. package/skills/skill-creator/scripts/quick_validate.py +101 -0
  281. package/skills/slack/SKILL.md +144 -0
  282. package/skills/songsee/SKILL.md +49 -0
  283. package/skills/sonoscli/SKILL.md +46 -0
  284. package/skills/spec-to-code-compliance/.claude-plugin/plugin.json +10 -0
  285. package/skills/spec-to-code-compliance/README.md +67 -0
  286. package/skills/spec-to-code-compliance/commands/spec-compliance.md +22 -0
  287. package/skills/spec-to-code-compliance/skills/spec-to-code-compliance/SKILL.md +349 -0
  288. package/skills/spec-to-code-compliance/skills/spec-to-code-compliance/resources/COMPLETENESS_CHECKLIST.md +69 -0
  289. package/skills/spec-to-code-compliance/skills/spec-to-code-compliance/resources/IR_EXAMPLES.md +417 -0
  290. package/skills/spec-to-code-compliance/skills/spec-to-code-compliance/resources/OUTPUT_REQUIREMENTS.md +105 -0
  291. package/skills/spotify-player/SKILL.md +64 -0
  292. package/skills/static-analysis/.claude-plugin/plugin.json +8 -0
  293. package/skills/static-analysis/README.md +59 -0
  294. package/skills/static-analysis/skills/codeql/SKILL.md +315 -0
  295. package/skills/static-analysis/skills/sarif-parsing/SKILL.md +479 -0
  296. package/skills/static-analysis/skills/sarif-parsing/resources/jq-queries.md +162 -0
  297. package/skills/static-analysis/skills/sarif-parsing/resources/sarif_helpers.py +331 -0
  298. package/skills/static-analysis/skills/semgrep/SKILL.md +337 -0
  299. package/skills/summarize/SKILL.md +87 -0
  300. package/skills/testing-handbook-skills/.claude-plugin/plugin.json +8 -0
  301. package/skills/testing-handbook-skills/README.md +241 -0
  302. package/skills/testing-handbook-skills/scripts/pyproject.toml +8 -0
  303. package/skills/testing-handbook-skills/scripts/validate-skills.py +657 -0
  304. package/skills/testing-handbook-skills/skills/address-sanitizer/SKILL.md +341 -0
  305. package/skills/testing-handbook-skills/skills/aflpp/SKILL.md +640 -0
  306. package/skills/testing-handbook-skills/skills/atheris/SKILL.md +515 -0
  307. package/skills/testing-handbook-skills/skills/cargo-fuzz/SKILL.md +454 -0
  308. package/skills/testing-handbook-skills/skills/codeql/SKILL.md +549 -0
  309. package/skills/testing-handbook-skills/skills/constant-time-testing/SKILL.md +507 -0
  310. package/skills/testing-handbook-skills/skills/coverage-analysis/SKILL.md +607 -0
  311. package/skills/testing-handbook-skills/skills/fuzzing-dictionary/SKILL.md +297 -0
  312. package/skills/testing-handbook-skills/skills/fuzzing-obstacles/SKILL.md +426 -0
  313. package/skills/testing-handbook-skills/skills/harness-writing/SKILL.md +614 -0
  314. package/skills/testing-handbook-skills/skills/libafl/SKILL.md +625 -0
  315. package/skills/testing-handbook-skills/skills/libfuzzer/SKILL.md +795 -0
  316. package/skills/testing-handbook-skills/skills/ossfuzz/SKILL.md +426 -0
  317. package/skills/testing-handbook-skills/skills/ruzzy/SKILL.md +443 -0
  318. package/skills/testing-handbook-skills/skills/semgrep/SKILL.md +601 -0
  319. package/skills/testing-handbook-skills/skills/testing-handbook-generator/SKILL.md +372 -0
  320. package/skills/testing-handbook-skills/skills/testing-handbook-generator/agent-prompt.md +280 -0
  321. package/skills/testing-handbook-skills/skills/testing-handbook-generator/discovery.md +452 -0
  322. package/skills/testing-handbook-skills/skills/testing-handbook-generator/templates/domain-skill.md +504 -0
  323. package/skills/testing-handbook-skills/skills/testing-handbook-generator/templates/fuzzer-skill.md +454 -0
  324. package/skills/testing-handbook-skills/skills/testing-handbook-generator/templates/technique-skill.md +527 -0
  325. package/skills/testing-handbook-skills/skills/testing-handbook-generator/templates/tool-skill.md +366 -0
  326. package/skills/testing-handbook-skills/skills/testing-handbook-generator/testing.md +482 -0
  327. package/skills/testing-handbook-skills/skills/wycheproof/SKILL.md +533 -0
  328. package/skills/things-mac/SKILL.md +86 -0
  329. package/skills/tmux/SKILL.md +135 -0
  330. package/skills/tmux/scripts/find-sessions.sh +112 -0
  331. package/skills/tmux/scripts/wait-for-text.sh +83 -0
  332. package/skills/trello/SKILL.md +95 -0
  333. package/skills/variant-analysis/.claude-plugin/plugin.json +8 -0
  334. package/skills/variant-analysis/README.md +41 -0
  335. package/skills/variant-analysis/commands/variants.md +23 -0
  336. package/skills/variant-analysis/skills/variant-analysis/METHODOLOGY.md +327 -0
  337. package/skills/variant-analysis/skills/variant-analysis/SKILL.md +142 -0
  338. package/skills/variant-analysis/skills/variant-analysis/resources/codeql/cpp.ql +119 -0
  339. package/skills/variant-analysis/skills/variant-analysis/resources/codeql/go.ql +69 -0
  340. package/skills/variant-analysis/skills/variant-analysis/resources/codeql/java.ql +71 -0
  341. package/skills/variant-analysis/skills/variant-analysis/resources/codeql/javascript.ql +63 -0
  342. package/skills/variant-analysis/skills/variant-analysis/resources/codeql/python.ql +80 -0
  343. package/skills/variant-analysis/skills/variant-analysis/resources/semgrep/cpp.yaml +98 -0
  344. package/skills/variant-analysis/skills/variant-analysis/resources/semgrep/go.yaml +63 -0
  345. package/skills/variant-analysis/skills/variant-analysis/resources/semgrep/java.yaml +61 -0
  346. package/skills/variant-analysis/skills/variant-analysis/resources/semgrep/javascript.yaml +60 -0
  347. package/skills/variant-analysis/skills/variant-analysis/resources/semgrep/python.yaml +72 -0
  348. package/skills/variant-analysis/skills/variant-analysis/resources/variant-report-template.md +75 -0
  349. package/skills/video-frames/SKILL.md +46 -0
  350. package/skills/video-frames/scripts/frame.sh +81 -0
  351. package/skills/voice-call/SKILL.md +45 -0
  352. package/skills/wacli/SKILL.md +72 -0
  353. package/skills/weather/SKILL.md +54 -0
  354. package/skills/yara-authoring/.claude-plugin/plugin.json +9 -0
  355. package/skills/yara-authoring/README.md +131 -0
  356. package/skills/yara-authoring/skills/yara-rule-authoring/SKILL.md +645 -0
  357. package/skills/yara-authoring/skills/yara-rule-authoring/examples/MAL_Mac_ProtonRAT_Jan25.yar +99 -0
  358. package/skills/yara-authoring/skills/yara-rule-authoring/examples/MAL_NPM_SupplyChain_Jan25.yar +170 -0
  359. package/skills/yara-authoring/skills/yara-rule-authoring/examples/MAL_Win_Remcos_Jan25.yar +103 -0
  360. package/skills/yara-authoring/skills/yara-rule-authoring/examples/SUSP_CRX_SuspiciousPermissions.yar +134 -0
  361. package/skills/yara-authoring/skills/yara-rule-authoring/examples/SUSP_JS_Obfuscation_Jan25.yar +185 -0
  362. package/skills/yara-authoring/skills/yara-rule-authoring/references/crx-module.md +214 -0
  363. package/skills/yara-authoring/skills/yara-rule-authoring/references/dex-module.md +383 -0
  364. package/skills/yara-authoring/skills/yara-rule-authoring/references/performance.md +333 -0
  365. package/skills/yara-authoring/skills/yara-rule-authoring/references/strings.md +433 -0
  366. package/skills/yara-authoring/skills/yara-rule-authoring/references/style-guide.md +257 -0
  367. package/skills/yara-authoring/skills/yara-rule-authoring/references/testing.md +399 -0
  368. package/skills/yara-authoring/skills/yara-rule-authoring/scripts/atom_analyzer.py +526 -0
  369. package/skills/yara-authoring/skills/yara-rule-authoring/scripts/pyproject.toml +25 -0
  370. package/skills/yara-authoring/skills/yara-rule-authoring/scripts/yara_lint.py +631 -0
  371. package/skills/yara-authoring/skills/yara-rule-authoring/workflows/rule-development.md +493 -0
@@ -0,0 +1,3081 @@
1
+ #!/usr/bin/env python3
2
+ # /// script
3
+ # requires-python = ">=3.10"
4
+ # ///
5
+ """
6
+ Script language analyzers for constant-time analysis.
7
+
8
+ This module provides analyzers for scripting languages (PHP, JavaScript/TypeScript)
9
+ that work at the bytecode/opcode level rather than native assembly.
10
+ """
11
+
12
+ import os
13
+ import re
14
+ import subprocess
15
+ import sys
16
+ import tempfile
17
+ from abc import ABC, abstractmethod
18
+ from pathlib import Path
19
+
20
+ # Import shared types from main analyzer
21
+ try:
22
+ from .analyzer import AnalysisReport, Severity, Violation
23
+ except ImportError:
24
+ from analyzer import AnalysisReport, Severity, Violation
25
+
26
+
27
+ # =============================================================================
28
+ # PHP Dangerous Operations
29
+ # =============================================================================
30
+
31
+ DANGEROUS_PHP_OPCODES = {
32
+ "errors": {
33
+ # Variable-time arithmetic
34
+ "zend_div": "DIV opcode has variable-time execution based on operand values",
35
+ "div": "DIV opcode has variable-time execution based on operand values",
36
+ "zend_mod": "MOD opcode has variable-time execution based on operand values",
37
+ "mod": "MOD opcode has variable-time execution based on operand values",
38
+ "zend_pow": "POW opcode has variable-time execution",
39
+ "pow": "POW opcode has variable-time execution",
40
+ },
41
+ "warnings": {
42
+ # Comparisons that may early-terminate
43
+ "zend_is_equal": "Equality comparison may early-terminate on secret data",
44
+ "is_equal": "Equality comparison may early-terminate on secret data",
45
+ "zend_is_identical": "Identity comparison may early-terminate on secret data",
46
+ "is_identical": "Identity comparison may early-terminate on secret data",
47
+ "zend_is_not_equal": "Inequality comparison may early-terminate on secret data",
48
+ "is_not_equal": "Inequality comparison may early-terminate on secret data",
49
+ "zend_is_not_identical": "Non-identity comparison may early-terminate on secret data",
50
+ "is_not_identical": "Non-identity comparison may early-terminate on secret data",
51
+ # Table lookups (cache timing via secret-indexed array access)
52
+ "fetch_dim_r": "Array access may leak timing via cache if index depends on secrets",
53
+ "zend_fetch_dim_r": "Array access may leak timing via cache if index depends on secrets",
54
+ "fetch_dim_w": "Array access may leak timing via cache if index depends on secrets",
55
+ "zend_fetch_dim_w": "Array access may leak timing via cache if index depends on secrets",
56
+ # Bit shift operations (may leak via timing if shift amount is secret)
57
+ "zend_sl": "Left shift may leak timing if shift amount depends on secrets",
58
+ "sl": "Left shift may leak timing if shift amount depends on secrets",
59
+ "zend_sr": "Right shift may leak timing if shift amount depends on secrets",
60
+ "sr": "Right shift may leak timing if shift amount depends on secrets",
61
+ },
62
+ }
63
+
64
+ # Functions with timing side-channels (based on Paragonie research)
65
+ DANGEROUS_PHP_FUNCTIONS = {
66
+ "errors": {
67
+ # Cache-timing side-channels via table lookups
68
+ "chr": "chr() uses table lookup indexed by secret data; use pack('C', $int) instead",
69
+ "ord": "ord() uses table lookup indexed by secret data; use unpack('C', $char)[1] instead",
70
+ "bin2hex": "bin2hex() uses table lookups indexed on secret data",
71
+ "hex2bin": "hex2bin() uses table lookups indexed on secret data",
72
+ "base64_encode": "base64_encode() uses table lookups indexed on secret data",
73
+ "base64_decode": "base64_decode() uses table lookups indexed on secret data",
74
+ # Predictable randomness (not cryptographically secure)
75
+ "rand": "rand() is predictable; use random_int() for cryptographic purposes",
76
+ "mt_rand": "mt_rand() is predictable; use random_int() for cryptographic purposes",
77
+ "array_rand": "array_rand() uses mt_rand internally; use random_int() instead",
78
+ "uniqid": "uniqid() is predictable; use random_bytes() for cryptographic purposes",
79
+ "lcg_value": "lcg_value() is predictable; use random_int() for cryptographic purposes",
80
+ "str_shuffle": "str_shuffle() uses mt_rand internally",
81
+ "shuffle": "shuffle() uses mt_rand internally; use a Fisher-Yates with random_int()",
82
+ },
83
+ "warnings": {
84
+ # Variable-time string comparisons
85
+ "strcmp": "strcmp() has variable-time execution; use hash_equals() for secrets",
86
+ "strcasecmp": "strcasecmp() has variable-time execution; use hash_equals() for secrets",
87
+ "strncmp": "strncmp() has variable-time execution; use hash_equals() for secrets",
88
+ "strncasecmp": "strncasecmp() has variable-time execution; use hash_equals() for secrets",
89
+ "substr_compare": "substr_compare() has variable-time execution; use hash_equals()",
90
+ # String operations that may indicate unsafe comparison patterns
91
+ "substr": "substr() in comparisons may indicate timing-unsafe pattern",
92
+ # Variable-length encoding (may leak data length via timing)
93
+ "pack": "pack() may leak data length via timing; ensure fixed-length output",
94
+ "unpack": "unpack() may leak data length via timing; ensure fixed-length input",
95
+ "serialize": "serialize() produces variable-length output that may leak information",
96
+ "json_encode": "json_encode() produces variable-length output that may leak information",
97
+ },
98
+ }
99
+
100
+
101
+ # =============================================================================
102
+ # JavaScript/TypeScript Dangerous Operations
103
+ # =============================================================================
104
+
105
+ DANGEROUS_JS_BYTECODES = {
106
+ "errors": {
107
+ # Variable-time arithmetic
108
+ "div": "Div bytecode has variable-time execution based on operand values",
109
+ "mod": "Mod bytecode has variable-time execution based on operand values",
110
+ "divsmi": "DivSmi (division by small integer) has variable-time execution",
111
+ "modsmi": "ModSmi (modulo by small integer) has variable-time execution",
112
+ },
113
+ "warnings": {
114
+ # Conditional jumps (may leak timing if condition depends on secrets)
115
+ "jumpiftrue": "Conditional jump may leak timing if condition depends on secret data",
116
+ "jumpiffalse": "Conditional jump may leak timing if condition depends on secret data",
117
+ "jumpiftobooleanfalse": "Conditional jump may leak timing if condition depends on secret data",
118
+ "jumpiftobooleantrue": "Conditional jump may leak timing if condition depends on secret data",
119
+ "jumpifundefined": "Conditional jump may leak timing if condition depends on secret data",
120
+ "jumpifnull": "Conditional jump may leak timing if condition depends on secret data",
121
+ # Comparison operations
122
+ "testequal": "Equality test may early-terminate on secret data",
123
+ "testequalstrict": "Strict equality test may early-terminate on secret data",
124
+ # Table lookups (cache timing via secret-indexed array access)
125
+ "ldakeyedproperty": "Array/property access may leak timing via cache if index depends on secrets",
126
+ "stakeyedproperty": "Array/property access may leak timing via cache if index depends on secrets",
127
+ "ldanamedproperty": "Property access may leak timing via cache if key depends on secrets",
128
+ "getkeyed": "Array access may leak timing via cache if index depends on secrets",
129
+ "setkeyed": "Array access may leak timing via cache if index depends on secrets",
130
+ # Bit shift operations (may leak via timing if shift amount is secret)
131
+ "shiftleft": "Left shift may leak timing if shift amount depends on secrets",
132
+ "shiftright": "Right shift may leak timing if shift amount depends on secrets",
133
+ "shiftrightsmi": "Right shift by constant may still leak timing in some contexts",
134
+ "shiftleftsmi": "Left shift by constant may still leak timing in some contexts",
135
+ "bitwiseand": "Bitwise AND timing may vary based on operands",
136
+ "bitwiseor": "Bitwise OR timing may vary based on operands",
137
+ "bitwisexor": "Bitwise XOR timing may vary based on operands",
138
+ },
139
+ }
140
+
141
+ DANGEROUS_JS_FUNCTIONS = {
142
+ "errors": {
143
+ # Variable latency math operations
144
+ "math.sqrt": "Math.sqrt() has variable latency based on operand values",
145
+ "math.pow": "Math.pow() has variable latency based on operand values",
146
+ # Unpredictable timing
147
+ "eval": "eval() has unpredictable timing characteristics",
148
+ # Predictable randomness
149
+ "math.random": "Math.random() is predictable; use crypto.getRandomValues() instead",
150
+ },
151
+ "warnings": {
152
+ # Variable-time string operations
153
+ "localecompare": "localeCompare() has variable-time execution",
154
+ "indexof": "indexOf() has early-terminating behavior",
155
+ "includes": "includes() has early-terminating behavior",
156
+ "startswith": "startsWith() has early-terminating behavior",
157
+ "endswith": "endsWith() has early-terminating behavior",
158
+ "search": "search() has variable-time execution",
159
+ "match": "match() has variable-time execution",
160
+ # Variable-length encoding (may leak data length via timing)
161
+ "textencoder": "TextEncoder may leak data length via timing; ensure fixed-length output",
162
+ "textdecoder": "TextDecoder may leak data length via timing; ensure fixed-length input",
163
+ "json.stringify": "JSON.stringify() produces variable-length output that may leak information",
164
+ "json.parse": "JSON.parse() timing may vary based on input length/structure",
165
+ "btoa": "btoa() produces variable-length output based on input",
166
+ "atob": "atob() timing may vary based on input length",
167
+ "encodeuricomponent": "encodeURIComponent() produces variable-length output",
168
+ "decodeuricomponent": "decodeURIComponent() timing may vary based on input",
169
+ },
170
+ }
171
+
172
+
173
+ # =============================================================================
174
+ # Python Dangerous Operations
175
+ # =============================================================================
176
+
177
+ DANGEROUS_PYTHON_BYTECODES = {
178
+ "errors": {
179
+ # Python < 3.11 division/modulo operations
180
+ "binary_true_divide": "BINARY_TRUE_DIVIDE has variable-time execution",
181
+ "binary_floor_divide": "BINARY_FLOOR_DIVIDE has variable-time execution",
182
+ "binary_modulo": "BINARY_MODULO has variable-time execution",
183
+ "inplace_true_divide": "INPLACE_TRUE_DIVIDE has variable-time execution",
184
+ "inplace_floor_divide": "INPLACE_FLOOR_DIVIDE has variable-time execution",
185
+ "inplace_modulo": "INPLACE_MODULO has variable-time execution",
186
+ # Python 3.11+ uses BINARY_OP with oparg for these
187
+ # We detect these specially in the parser
188
+ },
189
+ "warnings": {
190
+ # Comparison operations
191
+ "compare_op": "COMPARE_OP may early-terminate on secret data",
192
+ "contains_op": "CONTAINS_OP has early-terminating behavior",
193
+ # Table lookups (cache timing via secret-indexed access)
194
+ "binary_subscr": "Subscript access may leak timing via cache if index depends on secrets",
195
+ "store_subscr": "Subscript store may leak timing via cache if index depends on secrets",
196
+ # Bit shift operations (may leak via timing if shift amount is secret)
197
+ "binary_lshift": "Left shift may leak timing if shift amount depends on secrets",
198
+ "binary_rshift": "Right shift may leak timing if shift amount depends on secrets",
199
+ "inplace_lshift": "Inplace left shift may leak timing if shift amount depends on secrets",
200
+ "inplace_rshift": "Inplace right shift may leak timing if shift amount depends on secrets",
201
+ },
202
+ }
203
+
204
+ DANGEROUS_PYTHON_FUNCTIONS = {
205
+ "errors": {
206
+ # Predictable randomness (not cryptographically secure)
207
+ "random.random": "random.random() is predictable; use secrets.token_bytes() instead",
208
+ "random.randint": "random.randint() is predictable; use secrets.randbelow() instead",
209
+ "random.randrange": "random.randrange() is predictable; use secrets.randbelow() instead",
210
+ "random.choice": "random.choice() is predictable; use secrets.choice() instead",
211
+ "random.shuffle": "random.shuffle() is predictable; use secrets module instead",
212
+ "random.sample": "random.sample() is predictable; use secrets module instead",
213
+ # Variable latency math operations
214
+ "math.sqrt": "math.sqrt() has variable latency based on operand values",
215
+ "math.pow": "math.pow() has variable latency based on operand values",
216
+ # Dangerous eval
217
+ "eval": "eval() has unpredictable timing characteristics",
218
+ "exec": "exec() has unpredictable timing characteristics",
219
+ },
220
+ "warnings": {
221
+ # Variable-time string operations
222
+ "str.find": "str.find() has early-terminating behavior",
223
+ "str.index": "str.index() has early-terminating behavior",
224
+ "str.startswith": "str.startswith() has early-terminating behavior",
225
+ "str.endswith": "str.endswith() has early-terminating behavior",
226
+ # in operator on strings (detected via CONTAINS_OP)
227
+ # Variable-length encoding (may leak data length via timing)
228
+ "int.to_bytes": "int.to_bytes() output length may leak information about the integer",
229
+ "int.from_bytes": "int.from_bytes() timing may vary based on input length",
230
+ "struct.pack": "struct.pack() may leak data length via timing; ensure fixed-length output",
231
+ "struct.unpack": "struct.unpack() may leak data length via timing; ensure fixed-length input",
232
+ "json.dumps": "json.dumps() produces variable-length output that may leak information",
233
+ "json.loads": "json.loads() timing may vary based on input length/structure",
234
+ "pickle.dumps": "pickle.dumps() produces variable-length output that may leak information",
235
+ "pickle.loads": "pickle.loads() timing varies based on input; also a security risk",
236
+ "base64.b64encode": "base64.b64encode() produces variable-length output",
237
+ "base64.b64decode": "base64.b64decode() timing may vary based on input length",
238
+ },
239
+ }
240
+
241
+
242
+ # =============================================================================
243
+ # Ruby Dangerous Operations
244
+ # =============================================================================
245
+
246
+ DANGEROUS_RUBY_BYTECODES = {
247
+ "errors": {
248
+ # Division and modulo operations
249
+ "opt_div": "opt_div has variable-time execution based on operand values",
250
+ "opt_mod": "opt_mod has variable-time execution based on operand values",
251
+ },
252
+ "warnings": {
253
+ # Comparison and equality operations
254
+ "opt_eq": "opt_eq may early-terminate on secret data",
255
+ "opt_neq": "opt_neq may early-terminate on secret data",
256
+ "opt_lt": "opt_lt comparison may leak timing information",
257
+ "opt_le": "opt_le comparison may leak timing information",
258
+ "opt_gt": "opt_gt comparison may leak timing information",
259
+ "opt_ge": "opt_ge comparison may leak timing information",
260
+ "branchif": "Conditional branch may leak timing if condition depends on secrets",
261
+ "branchunless": "Conditional branch may leak timing if condition depends on secrets",
262
+ # Table lookups (cache timing via secret-indexed access)
263
+ "opt_aref": "Array access may leak timing via cache if index depends on secrets",
264
+ "opt_aset": "Array store may leak timing via cache if index depends on secrets",
265
+ # Bit shift operations (may leak via timing if shift amount is secret)
266
+ "opt_lshift": "Left shift may leak timing if shift amount depends on secrets",
267
+ "opt_rshift": "Right shift may leak timing if shift amount depends on secrets",
268
+ "opt_and": "Bitwise AND timing may vary based on operands",
269
+ "opt_or": "Bitwise OR timing may vary based on operands",
270
+ },
271
+ }
272
+
273
+ DANGEROUS_RUBY_FUNCTIONS = {
274
+ "errors": {
275
+ # Predictable randomness
276
+ "rand": "rand() is predictable; use SecureRandom instead",
277
+ "random": "Random is predictable; use SecureRandom instead",
278
+ "srand": "srand() sets predictable seed; use SecureRandom instead",
279
+ # Variable latency math operations
280
+ "math.sqrt": "Math.sqrt() has variable latency based on operand values",
281
+ },
282
+ "warnings": {
283
+ # Variable-time string operations
284
+ "include?": "include?() has early-terminating behavior",
285
+ "index": "index() has early-terminating behavior",
286
+ "start_with?": "start_with?() has early-terminating behavior",
287
+ "end_with?": "end_with?() has early-terminating behavior",
288
+ "match": "match() has variable-time execution",
289
+ "=~": "=~ regex match has variable-time execution",
290
+ # Variable-length encoding (may leak data length via timing)
291
+ "pack": "Array#pack() may leak data length via timing; ensure fixed-length output",
292
+ "unpack": "String#unpack() may leak data length via timing; ensure fixed-length input",
293
+ "to_json": "to_json() produces variable-length output that may leak information",
294
+ "json.parse": "JSON.parse() timing may vary based on input length/structure",
295
+ "marshal.dump": "Marshal.dump() produces variable-length output that may leak information",
296
+ "marshal.load": "Marshal.load() timing varies based on input; also a security risk",
297
+ "base64.encode64": "Base64.encode64() produces variable-length output",
298
+ "base64.decode64": "Base64.decode64() timing may vary based on input length",
299
+ },
300
+ }
301
+
302
+
303
+ # =============================================================================
304
+ # Java (JVM) Dangerous Operations
305
+ # =============================================================================
306
+
307
+ DANGEROUS_JAVA_BYTECODES = {
308
+ "errors": {
309
+ # Integer division - variable time based on operand values
310
+ "idiv": "IDIV has variable-time execution based on operand values",
311
+ "ldiv": "LDIV has variable-time execution based on operand values",
312
+ "irem": "IREM has variable-time execution based on operand values",
313
+ "lrem": "LREM has variable-time execution based on operand values",
314
+ # Floating-point division - variable latency
315
+ "fdiv": "FDIV has variable latency based on operand values",
316
+ "ddiv": "DDIV has variable latency based on operand values",
317
+ "frem": "FREM has variable latency based on operand values",
318
+ "drem": "DREM has variable latency based on operand values",
319
+ },
320
+ "warnings": {
321
+ # Conditional branches - may leak timing if condition depends on secrets
322
+ "ifeq": "conditional branch may leak timing if condition depends on secret data",
323
+ "ifne": "conditional branch may leak timing if condition depends on secret data",
324
+ "iflt": "conditional branch may leak timing if condition depends on secret data",
325
+ "ifge": "conditional branch may leak timing if condition depends on secret data",
326
+ "ifgt": "conditional branch may leak timing if condition depends on secret data",
327
+ "ifle": "conditional branch may leak timing if condition depends on secret data",
328
+ "if_icmpeq": "conditional branch may leak timing if condition depends on secret data",
329
+ "if_icmpne": "conditional branch may leak timing if condition depends on secret data",
330
+ "if_icmplt": "conditional branch may leak timing if condition depends on secret data",
331
+ "if_icmpge": "conditional branch may leak timing if condition depends on secret data",
332
+ "if_icmpgt": "conditional branch may leak timing if condition depends on secret data",
333
+ "if_icmple": "conditional branch may leak timing if condition depends on secret data",
334
+ "if_acmpeq": "conditional branch may leak timing if condition depends on secret data",
335
+ "if_acmpne": "conditional branch may leak timing if condition depends on secret data",
336
+ "ifnull": "conditional branch may leak timing if condition depends on secret data",
337
+ "ifnonnull": "conditional branch may leak timing if condition depends on secret data",
338
+ # Table lookups - cache timing if index depends on secrets
339
+ "iaload": "array access may leak timing via cache if index depends on secrets",
340
+ "laload": "array access may leak timing via cache if index depends on secrets",
341
+ "faload": "array access may leak timing via cache if index depends on secrets",
342
+ "daload": "array access may leak timing via cache if index depends on secrets",
343
+ "aaload": "array access may leak timing via cache if index depends on secrets",
344
+ "baload": "array access may leak timing via cache if index depends on secrets",
345
+ "caload": "array access may leak timing via cache if index depends on secrets",
346
+ "saload": "array access may leak timing via cache if index depends on secrets",
347
+ "iastore": "array store may leak timing via cache if index depends on secrets",
348
+ "lastore": "array store may leak timing via cache if index depends on secrets",
349
+ "fastore": "array store may leak timing via cache if index depends on secrets",
350
+ "dastore": "array store may leak timing via cache if index depends on secrets",
351
+ "aastore": "array store may leak timing via cache if index depends on secrets",
352
+ "bastore": "array store may leak timing via cache if index depends on secrets",
353
+ "castore": "array store may leak timing via cache if index depends on secrets",
354
+ "sastore": "array store may leak timing via cache if index depends on secrets",
355
+ "tableswitch": "switch statement may leak timing based on case value",
356
+ "lookupswitch": "switch statement may leak timing based on case value",
357
+ },
358
+ }
359
+
360
+ DANGEROUS_JAVA_FUNCTIONS = {
361
+ "errors": {
362
+ # Predictable randomness
363
+ "java.util.random": "java.util.Random is predictable; use SecureRandom instead",
364
+ "math.random": "Math.random() is predictable; use SecureRandom instead",
365
+ # Variable latency math
366
+ "math.sqrt": "Math.sqrt() has variable latency based on operand values",
367
+ "math.pow": "Math.pow() has variable latency based on operand values",
368
+ },
369
+ "warnings": {
370
+ # Variable-time comparisons
371
+ "arrays.equals": "Arrays.equals() may early-terminate; use MessageDigest.isEqual()",
372
+ "string.equals": "String.equals() may early-terminate on secret data",
373
+ "string.compareto": "String.compareTo() has variable-time execution",
374
+ "string.contentequals": "String.contentEquals() may early-terminate",
375
+ # Variable-length encoding
376
+ "base64.getencoder": "Base64 encoding produces variable-length output",
377
+ "base64.getdecoder": "Base64 decoding timing may vary based on input",
378
+ },
379
+ }
380
+
381
+
382
+ # =============================================================================
383
+ # Kotlin (JVM) Dangerous Operations
384
+ # =============================================================================
385
+
386
+ # Kotlin compiles to JVM bytecode, so it uses the same dangerous bytecodes as Java
387
+ DANGEROUS_KOTLIN_BYTECODES = DANGEROUS_JAVA_BYTECODES
388
+
389
+ DANGEROUS_KOTLIN_FUNCTIONS = {
390
+ "errors": {
391
+ # Predictable randomness (Kotlin stdlib)
392
+ "random.nextint": "Random.nextInt() is predictable; use SecureRandom instead",
393
+ "random.nextlong": "Random.nextLong() is predictable; use SecureRandom instead",
394
+ "random.nextdouble": "Random.nextDouble() is predictable; use SecureRandom instead",
395
+ "random.nextfloat": "Random.nextFloat() is predictable; use SecureRandom instead",
396
+ "random.nextbytes": "Random.nextBytes() is predictable; use SecureRandom instead",
397
+ "random.default": "Random.Default is predictable; use SecureRandom instead",
398
+ # Java interop (same as Java)
399
+ "java.util.random": "java.util.Random is predictable; use SecureRandom instead",
400
+ "math.random": "Math.random() is predictable; use SecureRandom instead",
401
+ # Variable latency math
402
+ "kotlin.math.sqrt": "sqrt() has variable latency based on operand values",
403
+ "kotlin.math.pow": "pow() has variable latency based on operand values",
404
+ "math.sqrt": "Math.sqrt() has variable latency based on operand values",
405
+ "math.pow": "Math.pow() has variable latency based on operand values",
406
+ },
407
+ "warnings": {
408
+ # Variable-time comparisons (Kotlin-specific)
409
+ "contentequals": "contentEquals() may early-terminate on secret data",
410
+ "equals": "equals() may early-terminate on secret data",
411
+ "compareto": "compareTo() has variable-time execution",
412
+ # Arrays
413
+ "arrays.equals": "Arrays.equals() may early-terminate; use MessageDigest.isEqual()",
414
+ "arrays.contentequals": "contentEquals() may early-terminate on array comparison",
415
+ # String operations
416
+ "string.equals": "String.equals() may early-terminate on secret data",
417
+ "string.compareto": "String.compareTo() has variable-time execution",
418
+ # Variable-length encoding
419
+ "base64.getencoder": "Base64 encoding produces variable-length output",
420
+ "base64.getdecoder": "Base64 decoding timing may vary based on input",
421
+ "encodetobytearray": "encodeToByteArray() produces variable-length output",
422
+ "decodetostring": "decodeToString() timing may vary based on input",
423
+ },
424
+ }
425
+
426
+
427
+ # =============================================================================
428
+ # C# (CIL/.NET) Dangerous Operations
429
+ # =============================================================================
430
+
431
+ DANGEROUS_CSHARP_BYTECODES = {
432
+ "errors": {
433
+ # Integer division - variable time based on operand values
434
+ "div": "DIV has variable-time execution based on operand values",
435
+ "div.un": "DIV.UN has variable-time execution based on operand values",
436
+ "rem": "REM has variable-time execution based on operand values",
437
+ "rem.un": "REM.UN has variable-time execution based on operand values",
438
+ },
439
+ "warnings": {
440
+ # Conditional branches - may leak timing if condition depends on secrets
441
+ "beq": "conditional branch may leak timing if condition depends on secret data",
442
+ "beq.s": "conditional branch may leak timing if condition depends on secret data",
443
+ "bne": "conditional branch may leak timing if condition depends on secret data",
444
+ "bne.un": "conditional branch may leak timing if condition depends on secret data",
445
+ "bne.un.s": "conditional branch may leak timing if condition depends on secret data",
446
+ "blt": "conditional branch may leak timing if condition depends on secret data",
447
+ "blt.s": "conditional branch may leak timing if condition depends on secret data",
448
+ "blt.un": "conditional branch may leak timing if condition depends on secret data",
449
+ "blt.un.s": "conditional branch may leak timing if condition depends on secret data",
450
+ "bgt": "conditional branch may leak timing if condition depends on secret data",
451
+ "bgt.s": "conditional branch may leak timing if condition depends on secret data",
452
+ "bgt.un": "conditional branch may leak timing if condition depends on secret data",
453
+ "bgt.un.s": "conditional branch may leak timing if condition depends on secret data",
454
+ "ble": "conditional branch may leak timing if condition depends on secret data",
455
+ "ble.s": "conditional branch may leak timing if condition depends on secret data",
456
+ "ble.un": "conditional branch may leak timing if condition depends on secret data",
457
+ "ble.un.s": "conditional branch may leak timing if condition depends on secret data",
458
+ "bge": "conditional branch may leak timing if condition depends on secret data",
459
+ "bge.s": "conditional branch may leak timing if condition depends on secret data",
460
+ "bge.un": "conditional branch may leak timing if condition depends on secret data",
461
+ "bge.un.s": "conditional branch may leak timing if condition depends on secret data",
462
+ "brfalse": "conditional branch may leak timing if condition depends on secret data",
463
+ "brfalse.s": "conditional branch may leak timing if condition depends on secret data",
464
+ "brtrue": "conditional branch may leak timing if condition depends on secret data",
465
+ "brtrue.s": "conditional branch may leak timing if condition depends on secret data",
466
+ # Table lookups - cache timing if index depends on secrets
467
+ "ldelem": "array access may leak timing via cache if index depends on secrets",
468
+ "ldelem.i": "array access may leak timing via cache if index depends on secrets",
469
+ "ldelem.i1": "array access may leak timing via cache if index depends on secrets",
470
+ "ldelem.i2": "array access may leak timing via cache if index depends on secrets",
471
+ "ldelem.i4": "array access may leak timing via cache if index depends on secrets",
472
+ "ldelem.i8": "array access may leak timing via cache if index depends on secrets",
473
+ "ldelem.u1": "array access may leak timing via cache if index depends on secrets",
474
+ "ldelem.u2": "array access may leak timing via cache if index depends on secrets",
475
+ "ldelem.u4": "array access may leak timing via cache if index depends on secrets",
476
+ "ldelem.r4": "array access may leak timing via cache if index depends on secrets",
477
+ "ldelem.r8": "array access may leak timing via cache if index depends on secrets",
478
+ "ldelem.ref": "array access may leak timing via cache if index depends on secrets",
479
+ "stelem": "array store may leak timing via cache if index depends on secrets",
480
+ "stelem.i": "array store may leak timing via cache if index depends on secrets",
481
+ "stelem.i1": "array store may leak timing via cache if index depends on secrets",
482
+ "stelem.i2": "array store may leak timing via cache if index depends on secrets",
483
+ "stelem.i4": "array store may leak timing via cache if index depends on secrets",
484
+ "stelem.i8": "array store may leak timing via cache if index depends on secrets",
485
+ "stelem.r4": "array store may leak timing via cache if index depends on secrets",
486
+ "stelem.r8": "array store may leak timing via cache if index depends on secrets",
487
+ "stelem.ref": "array store may leak timing via cache if index depends on secrets",
488
+ "switch": "switch statement may leak timing based on case value",
489
+ },
490
+ }
491
+
492
+ DANGEROUS_CSHARP_FUNCTIONS = {
493
+ "errors": {
494
+ # Predictable randomness
495
+ "system.random": "System.Random is predictable; use RandomNumberGenerator instead",
496
+ # Variable latency math
497
+ "math.sqrt": "Math.Sqrt() has variable latency based on operand values",
498
+ "math.pow": "Math.Pow() has variable latency based on operand values",
499
+ },
500
+ "warnings": {
501
+ # Variable-time comparisons
502
+ "sequenceequal": "SequenceEqual() may early-terminate; use FixedTimeEquals()",
503
+ "string.equals": "String.Equals() may early-terminate on secret data",
504
+ "string.compare": "String.Compare() has variable-time execution",
505
+ "array.equals": "Array comparison may early-terminate",
506
+ # Variable-length encoding
507
+ "convert.tobase64string": "Base64 encoding produces variable-length output",
508
+ "convert.frombase64string": "Base64 decoding timing may vary based on input",
509
+ },
510
+ }
511
+
512
+
513
+ # =============================================================================
514
+ # ScriptAnalyzer Base Class
515
+ # =============================================================================
516
+
517
+
518
+ class ScriptAnalyzer(ABC):
519
+ """Base class for scripting language analyzers."""
520
+
521
+ name: str = "unknown"
522
+
523
+ @abstractmethod
524
+ def is_available(self) -> bool:
525
+ """Check if the analyzer's runtime is available."""
526
+ raise NotImplementedError
527
+
528
+ @abstractmethod
529
+ def analyze(
530
+ self,
531
+ source_file: str,
532
+ include_warnings: bool = False,
533
+ function_filter: str | None = None,
534
+ ) -> AnalysisReport:
535
+ """
536
+ Analyze source for timing violations.
537
+
538
+ Args:
539
+ source_file: Path to the source file to analyze
540
+ include_warnings: Include warning-level violations
541
+ function_filter: Regex pattern to filter functions
542
+
543
+ Returns:
544
+ AnalysisReport with results
545
+ """
546
+ raise NotImplementedError
547
+
548
+
549
+ # =============================================================================
550
+ # PHP Analyzer
551
+ # =============================================================================
552
+
553
+
554
+ class PHPAnalyzer(ScriptAnalyzer):
555
+ """
556
+ Analyzer for PHP scripts using VLD extension or OPcache debug output.
557
+
558
+ Detects timing-unsafe opcodes and function calls in PHP code.
559
+ """
560
+
561
+ name = "php"
562
+
563
+ def __init__(self, php_path: str | None = None):
564
+ self.php_path = php_path or "php"
565
+ self._vld_available: bool | None = None
566
+
567
+ def is_available(self) -> bool:
568
+ """Check if PHP is available."""
569
+ try:
570
+ result = subprocess.run(
571
+ [self.php_path, "--version"],
572
+ capture_output=True,
573
+ text=True,
574
+ )
575
+ return result.returncode == 0
576
+ except FileNotFoundError:
577
+ return False
578
+
579
+ def _check_vld_available(self) -> bool:
580
+ """Check if VLD extension is available."""
581
+ if self._vld_available is not None:
582
+ return self._vld_available
583
+
584
+ try:
585
+ result = subprocess.run(
586
+ [self.php_path, "-m"],
587
+ capture_output=True,
588
+ text=True,
589
+ )
590
+ self._vld_available = "vld" in result.stdout.lower()
591
+ except FileNotFoundError:
592
+ self._vld_available = False
593
+
594
+ return self._vld_available
595
+
596
+ def _get_vld_output(self, source_file: str) -> tuple[bool, str]:
597
+ """Get VLD opcode dump for a PHP file."""
598
+ cmd = [
599
+ self.php_path,
600
+ "-d",
601
+ "vld.active=1",
602
+ "-d",
603
+ "vld.execute=0",
604
+ "-d",
605
+ "vld.verbosity=1",
606
+ source_file,
607
+ ]
608
+
609
+ try:
610
+ result = subprocess.run(cmd, capture_output=True, text=True)
611
+ # VLD outputs to stderr
612
+ return True, result.stderr
613
+ except FileNotFoundError:
614
+ return False, f"PHP not found: {self.php_path}"
615
+
616
+ def _get_opcache_output(self, source_file: str) -> tuple[bool, str]:
617
+ """Get OPcache debug output for a PHP file (fallback)."""
618
+ cmd = [
619
+ self.php_path,
620
+ "-d",
621
+ "opcache.enable_cli=1",
622
+ "-d",
623
+ "opcache.opt_debug_level=0x10000",
624
+ source_file,
625
+ ]
626
+
627
+ try:
628
+ result = subprocess.run(cmd, capture_output=True, text=True)
629
+ # OPcache debug outputs to stderr
630
+ return True, result.stderr
631
+ except FileNotFoundError:
632
+ return False, f"PHP not found: {self.php_path}"
633
+
634
+ def _parse_vld_output(
635
+ self,
636
+ output: str,
637
+ include_warnings: bool = False,
638
+ function_filter: str | None = None,
639
+ ) -> tuple[list[dict], list[Violation]]:
640
+ """
641
+ Parse VLD output for dangerous opcodes and function calls.
642
+
643
+ VLD output format example:
644
+ Finding entry points
645
+ Branch analysis from position: 0
646
+ ...
647
+ filename: /path/to/file.php
648
+ function name: vulnerable_function
649
+ ...
650
+ line #* E I O op fetch ext return operands
651
+ -------------------------------------------------------------------------------------
652
+ 5 0 E > ASSIGN !0, 10
653
+ 6 1 ASSIGN !1, 3
654
+ 7 2 DIV ~4 !0, !1
655
+ """
656
+ functions = []
657
+ violations = []
658
+
659
+ current_function = None
660
+ current_file = None
661
+ in_opcode_section = False
662
+ filter_pattern = re.compile(function_filter) if function_filter else None
663
+
664
+ # Track function calls for detection
665
+ pending_fcall: str | None = None
666
+
667
+ for line in output.split("\n"):
668
+ line_stripped = line.strip()
669
+
670
+ # Detect function name
671
+ func_match = re.match(r"function name:\s*(.+)", line_stripped, re.IGNORECASE)
672
+ if func_match:
673
+ func_name = func_match.group(1).strip()
674
+ if func_name and func_name != "(null)":
675
+ current_function = func_name
676
+ functions.append({"name": current_function, "instructions": 0})
677
+ continue
678
+
679
+ # Detect filename
680
+ file_match = re.match(r"filename:\s*(.+)", line_stripped, re.IGNORECASE)
681
+ if file_match:
682
+ current_file = file_match.group(1).strip()
683
+ continue
684
+
685
+ # Detect start of opcode listing
686
+ # VLD format has header line with "line" and "op", then "---" separator
687
+ if "---" in line_stripped:
688
+ in_opcode_section = True
689
+ continue
690
+
691
+ # Also detect header line format
692
+ if line_stripped.startswith("line") and "op" in line_stripped.lower():
693
+ continue # Skip header line, section starts after ---
694
+
695
+ if not in_opcode_section:
696
+ continue
697
+
698
+ # Parse opcode line
699
+ # Format: line# index flags opcode [fetch] [ext] [return] operands
700
+ # Line number is optional (continuation lines don't have it)
701
+ # Examples:
702
+ # 5 0 E > RECV !0
703
+ # 1 RECV !1 (no line number)
704
+ opcode_match = re.match(r"(?:(\d+)\s+)?(\d+)\s+[E>*\s]*([A-Z_]+)\s*(.*)", line_stripped)
705
+
706
+ if not opcode_match:
707
+ # Check for end of section
708
+ if line_stripped.startswith("---") or not line_stripped:
709
+ in_opcode_section = False
710
+ continue
711
+
712
+ line_num = int(opcode_match.group(1)) if opcode_match.group(1) else None
713
+ # group(2) is the index, group(3) is the opcode, group(4) is operands
714
+ opcode = opcode_match.group(3).strip()
715
+ operands = opcode_match.group(4).strip() if opcode_match.group(4) else ""
716
+
717
+ # Update instruction count
718
+ if functions:
719
+ functions[-1]["instructions"] += 1
720
+
721
+ # Check if we should skip this function
722
+ if filter_pattern and current_function:
723
+ if not filter_pattern.search(current_function):
724
+ continue
725
+
726
+ opcode_lower = opcode.lower()
727
+
728
+ # Track function calls (INIT_FCALL followed by DO_FCALL)
729
+ if opcode in ("INIT_FCALL", "INIT_FCALL_BY_NAME", "INIT_NS_FCALL_BY_NAME"):
730
+ # Extract function name from operands
731
+ # Format varies: 'function_name' or "function_name"
732
+ fname_match = re.search(r"['\"]([^'\"]+)['\"]", operands)
733
+ if fname_match:
734
+ pending_fcall = fname_match.group(1).lower()
735
+
736
+ elif opcode in ("DO_FCALL", "DO_ICALL", "DO_FCALL_BY_NAME"):
737
+ if pending_fcall:
738
+ # Check if this function is dangerous
739
+ if pending_fcall in DANGEROUS_PHP_FUNCTIONS["errors"]:
740
+ violations.append(
741
+ Violation(
742
+ function=current_function or "<main>",
743
+ file=current_file or "",
744
+ line=line_num,
745
+ address="",
746
+ instruction=f"{opcode} {pending_fcall}",
747
+ mnemonic=pending_fcall.upper(),
748
+ reason=DANGEROUS_PHP_FUNCTIONS["errors"][pending_fcall],
749
+ severity=Severity.ERROR,
750
+ )
751
+ )
752
+ elif include_warnings and pending_fcall in DANGEROUS_PHP_FUNCTIONS["warnings"]:
753
+ violations.append(
754
+ Violation(
755
+ function=current_function or "<main>",
756
+ file=current_file or "",
757
+ line=line_num,
758
+ address="",
759
+ instruction=f"{opcode} {pending_fcall}",
760
+ mnemonic=pending_fcall.upper(),
761
+ reason=DANGEROUS_PHP_FUNCTIONS["warnings"][pending_fcall],
762
+ severity=Severity.WARNING,
763
+ )
764
+ )
765
+ pending_fcall = None
766
+
767
+ # Check for dangerous opcodes
768
+ if opcode_lower in DANGEROUS_PHP_OPCODES["errors"]:
769
+ violations.append(
770
+ Violation(
771
+ function=current_function or "<main>",
772
+ file=current_file or "",
773
+ line=line_num,
774
+ address="",
775
+ instruction=f"{opcode} {operands}".strip(),
776
+ mnemonic=opcode.upper(),
777
+ reason=DANGEROUS_PHP_OPCODES["errors"][opcode_lower],
778
+ severity=Severity.ERROR,
779
+ )
780
+ )
781
+ elif include_warnings and opcode_lower in DANGEROUS_PHP_OPCODES["warnings"]:
782
+ violations.append(
783
+ Violation(
784
+ function=current_function or "<main>",
785
+ file=current_file or "",
786
+ line=line_num,
787
+ address="",
788
+ instruction=f"{opcode} {operands}".strip(),
789
+ mnemonic=opcode.upper(),
790
+ reason=DANGEROUS_PHP_OPCODES["warnings"][opcode_lower],
791
+ severity=Severity.WARNING,
792
+ )
793
+ )
794
+
795
+ return functions, violations
796
+
797
+ def _parse_opcache_output(
798
+ self,
799
+ output: str,
800
+ include_warnings: bool = False,
801
+ function_filter: str | None = None,
802
+ ) -> tuple[list[dict], list[Violation]]:
803
+ """
804
+ Parse OPcache debug output for dangerous opcodes.
805
+
806
+ OPcache output format is similar but not identical to VLD.
807
+ """
808
+ # OPcache format is similar enough that we can reuse the VLD parser
809
+ # with some adjustments
810
+ return self._parse_vld_output(output, include_warnings, function_filter)
811
+
812
+ def analyze(
813
+ self,
814
+ source_file: str,
815
+ include_warnings: bool = False,
816
+ function_filter: str | None = None,
817
+ ) -> AnalysisReport:
818
+ """Analyze a PHP file for constant-time violations."""
819
+ source_path = Path(source_file)
820
+ if not source_path.exists():
821
+ raise FileNotFoundError(f"Source file not found: {source_file}")
822
+
823
+ # Try VLD first, fall back to OPcache
824
+ use_vld = self._check_vld_available()
825
+
826
+ if use_vld:
827
+ success, output = self._get_vld_output(str(source_path.absolute()))
828
+ backend = "vld"
829
+ else:
830
+ success, output = self._get_opcache_output(str(source_path.absolute()))
831
+ backend = "opcache"
832
+ print("Note: VLD extension not available, using OPcache debug output", file=sys.stderr)
833
+
834
+ if not success:
835
+ raise RuntimeError(f"Failed to get opcodes: {output}")
836
+
837
+ functions, violations = self._parse_vld_output(
838
+ output,
839
+ include_warnings,
840
+ function_filter,
841
+ )
842
+
843
+ return AnalysisReport(
844
+ architecture="zend", # PHP's Zend Engine
845
+ compiler=f"php/{backend}",
846
+ optimization="default",
847
+ source_file=str(source_file),
848
+ total_functions=len(functions),
849
+ total_instructions=sum(f["instructions"] for f in functions),
850
+ violations=violations,
851
+ )
852
+
853
+
854
+ # =============================================================================
855
+ # JavaScript/TypeScript Analyzer
856
+ # =============================================================================
857
+
858
+
859
+ class JavaScriptAnalyzer(ScriptAnalyzer):
860
+ """
861
+ Analyzer for JavaScript/TypeScript using V8 bytecode output.
862
+
863
+ For TypeScript files, transpiles to JavaScript first using tsc.
864
+ """
865
+
866
+ name = "javascript"
867
+
868
+ def __init__(self, node_path: str | None = None, tsc_path: str | None = None):
869
+ self.node_path = node_path or "node"
870
+ self.tsc_path = tsc_path or "tsc"
871
+
872
+ def is_available(self) -> bool:
873
+ """Check if Node.js is available."""
874
+ try:
875
+ result = subprocess.run(
876
+ [self.node_path, "--version"],
877
+ capture_output=True,
878
+ text=True,
879
+ )
880
+ return result.returncode == 0
881
+ except FileNotFoundError:
882
+ return False
883
+
884
+ def _is_tsc_available(self) -> bool:
885
+ """Check if TypeScript compiler is available."""
886
+ try:
887
+ result = subprocess.run(
888
+ [self.tsc_path, "--version"],
889
+ capture_output=True,
890
+ text=True,
891
+ )
892
+ return result.returncode == 0
893
+ except FileNotFoundError:
894
+ # Try npx tsc
895
+ try:
896
+ result = subprocess.run(
897
+ ["npx", "tsc", "--version"],
898
+ capture_output=True,
899
+ text=True,
900
+ )
901
+ if result.returncode == 0:
902
+ self.tsc_path = "npx tsc"
903
+ return True
904
+ except FileNotFoundError:
905
+ pass
906
+ return False
907
+
908
+ def _transpile_typescript(self, source_file: str, output_dir: str) -> tuple[bool, str]:
909
+ """Transpile TypeScript to JavaScript."""
910
+ source_path = Path(source_file)
911
+
912
+ # Check for tsconfig.json in the same directory or parent directories
913
+ tsconfig = None
914
+ check_dir = source_path.parent
915
+ for _ in range(5): # Check up to 5 parent directories
916
+ possible_config = check_dir / "tsconfig.json"
917
+ if possible_config.exists():
918
+ tsconfig = str(possible_config)
919
+ break
920
+ if check_dir.parent == check_dir:
921
+ break
922
+ check_dir = check_dir.parent
923
+
924
+ output_file = Path(output_dir) / source_path.with_suffix(".js").name
925
+
926
+ if self.tsc_path.startswith("npx"):
927
+ cmd = ["npx", "tsc"]
928
+ else:
929
+ cmd = [self.tsc_path]
930
+
931
+ cmd.extend(
932
+ [
933
+ "--outDir",
934
+ output_dir,
935
+ "--target",
936
+ "ES2020",
937
+ "--module",
938
+ "commonjs",
939
+ "--skipLibCheck",
940
+ "--noEmit",
941
+ "false",
942
+ ]
943
+ )
944
+
945
+ if tsconfig:
946
+ cmd.extend(["--project", tsconfig])
947
+
948
+ cmd.append(str(source_path.absolute()))
949
+
950
+ try:
951
+ result = subprocess.run(cmd, capture_output=True, text=True)
952
+ if result.returncode != 0:
953
+ return False, result.stderr or result.stdout
954
+ return True, str(output_file)
955
+ except FileNotFoundError:
956
+ return False, "TypeScript compiler not found"
957
+
958
+ def _get_v8_bytecode(
959
+ self, source_file: str, function_filter: str | None = None
960
+ ) -> tuple[bool, str]:
961
+ """Get V8 bytecode output for a JavaScript file."""
962
+ cmd = [self.node_path, "--print-bytecode"]
963
+
964
+ if function_filter:
965
+ cmd.extend(["--print-bytecode-filter", function_filter])
966
+
967
+ cmd.append(source_file)
968
+
969
+ try:
970
+ result = subprocess.run(cmd, capture_output=True, text=True)
971
+ # V8 bytecode goes to stdout
972
+ return True, result.stdout
973
+ except FileNotFoundError:
974
+ return False, f"Node.js not found: {self.node_path}"
975
+
976
+ def _parse_v8_bytecode(
977
+ self,
978
+ output: str,
979
+ source_file: str,
980
+ include_warnings: bool = False,
981
+ function_filter: str | None = None,
982
+ ) -> tuple[list[dict], list[Violation]]:
983
+ """
984
+ Parse V8 bytecode output for dangerous operations.
985
+
986
+ V8 bytecode format example:
987
+ [generated bytecode for function: vulnerableFunction (0x...)]
988
+ Bytecode length: 42
989
+ Parameter count 2
990
+ Register count 3
991
+ Frame size 24
992
+ 0 : LdaSmi [10]
993
+ 2 : Star0
994
+ 3 : LdaSmi [3]
995
+ 5 : Star1
996
+ 6 : Ldar r0
997
+ 8 : Div r1
998
+ 10 : Return
999
+ """
1000
+ functions = []
1001
+ violations = []
1002
+
1003
+ current_function = None
1004
+ current_file = source_file
1005
+ in_bytecode_section = False
1006
+ filter_pattern = re.compile(function_filter) if function_filter else None
1007
+
1008
+ # Track function calls
1009
+ pending_call: str | None = None
1010
+
1011
+ for line in output.split("\n"):
1012
+ line_stripped = line.strip()
1013
+
1014
+ # Detect function start
1015
+ # Format: [generated bytecode for function: functionName (0x...)]
1016
+ func_match = re.match(r"\[generated bytecode for function:\s*([^\s(]+)", line_stripped)
1017
+ if func_match:
1018
+ func_name = func_match.group(1).strip()
1019
+ # Skip internal Node.js functions
1020
+ if func_name and not func_name.startswith("__"):
1021
+ current_function = func_name
1022
+ functions.append({"name": current_function, "instructions": 0})
1023
+ in_bytecode_section = True
1024
+ continue
1025
+
1026
+ # Detect end of bytecode section
1027
+ if line_stripped.startswith("[") and "bytecode" in line_stripped.lower():
1028
+ in_bytecode_section = False
1029
+ continue
1030
+
1031
+ if not in_bytecode_section:
1032
+ continue
1033
+
1034
+ # Skip metadata lines
1035
+ if any(
1036
+ line_stripped.startswith(x)
1037
+ for x in [
1038
+ "Bytecode length:",
1039
+ "Parameter count",
1040
+ "Register count",
1041
+ "Frame size",
1042
+ "Constant pool",
1043
+ "Handler Table",
1044
+ ]
1045
+ ):
1046
+ continue
1047
+
1048
+ # Parse bytecode instruction
1049
+ # Format: offset : Instruction [operands]
1050
+ bytecode_match = re.match(
1051
+ r"\s*(\d+)\s*:\s*([A-Za-z][A-Za-z0-9]*)\s*(.*)", line_stripped
1052
+ )
1053
+
1054
+ if not bytecode_match:
1055
+ continue
1056
+
1057
+ offset = bytecode_match.group(1)
1058
+ instruction = bytecode_match.group(2)
1059
+ operands = bytecode_match.group(3).strip()
1060
+
1061
+ # Update instruction count
1062
+ if functions:
1063
+ functions[-1]["instructions"] += 1
1064
+
1065
+ # Check if we should skip this function
1066
+ if filter_pattern and current_function:
1067
+ if not filter_pattern.search(current_function):
1068
+ continue
1069
+
1070
+ instruction_lower = instruction.lower()
1071
+
1072
+ # Track function calls
1073
+ if instruction in (
1074
+ "CallRuntime",
1075
+ "CallUndefinedReceiver0",
1076
+ "CallUndefinedReceiver1",
1077
+ "CallUndefinedReceiver2",
1078
+ "CallProperty0",
1079
+ "CallProperty1",
1080
+ "CallProperty2",
1081
+ ):
1082
+ # Try to extract function name from operands
1083
+ # This is approximate since V8 bytecode uses indices
1084
+ pending_call = operands.lower()
1085
+
1086
+ # Check for dangerous bytecodes
1087
+ if instruction_lower in DANGEROUS_JS_BYTECODES["errors"]:
1088
+ violations.append(
1089
+ Violation(
1090
+ function=current_function or "<anonymous>",
1091
+ file=current_file,
1092
+ line=None,
1093
+ address=offset,
1094
+ instruction=f"{instruction} {operands}".strip(),
1095
+ mnemonic=instruction.upper(),
1096
+ reason=DANGEROUS_JS_BYTECODES["errors"][instruction_lower],
1097
+ severity=Severity.ERROR,
1098
+ )
1099
+ )
1100
+ elif include_warnings and instruction_lower in DANGEROUS_JS_BYTECODES["warnings"]:
1101
+ violations.append(
1102
+ Violation(
1103
+ function=current_function or "<anonymous>",
1104
+ file=current_file,
1105
+ line=None,
1106
+ address=offset,
1107
+ instruction=f"{instruction} {operands}".strip(),
1108
+ mnemonic=instruction.upper(),
1109
+ reason=DANGEROUS_JS_BYTECODES["warnings"][instruction_lower],
1110
+ severity=Severity.WARNING,
1111
+ )
1112
+ )
1113
+
1114
+ return functions, violations
1115
+
1116
+ def _detect_dangerous_function_calls(
1117
+ self,
1118
+ source_file: str,
1119
+ include_warnings: bool = False,
1120
+ ) -> list[Violation]:
1121
+ """
1122
+ Detect dangerous function calls and operators via static analysis of source.
1123
+
1124
+ This complements bytecode analysis since function names aren't
1125
+ always clear in V8 bytecode output.
1126
+ """
1127
+ violations = []
1128
+
1129
+ try:
1130
+ with open(source_file) as f:
1131
+ source = f.read()
1132
+ except OSError:
1133
+ return violations
1134
+
1135
+ # Simple regex-based detection for common patterns
1136
+ for func_name, reason in DANGEROUS_JS_FUNCTIONS["errors"].items():
1137
+ # Match function calls like Math.sqrt() or standalone sqrt()
1138
+ pattern = rf"\b{re.escape(func_name)}\s*\("
1139
+ for match in re.finditer(pattern, source, re.IGNORECASE):
1140
+ # Get line number
1141
+ line_num = source[: match.start()].count("\n") + 1
1142
+ violations.append(
1143
+ Violation(
1144
+ function="<source>",
1145
+ file=source_file,
1146
+ line=line_num,
1147
+ address="",
1148
+ instruction=match.group(0),
1149
+ mnemonic=func_name.upper().replace(".", "_"),
1150
+ reason=reason,
1151
+ severity=Severity.ERROR,
1152
+ )
1153
+ )
1154
+
1155
+ # Detect division and modulo operators in source
1156
+ # Pattern matches: a / b, a % b (but not // comments or /= assignment)
1157
+ div_pattern = r"[^/]\s*/\s*[^/=*]"
1158
+ for match in re.finditer(div_pattern, source):
1159
+ # Skip if inside a comment or regex
1160
+ line_start = source.rfind("\n", 0, match.start()) + 1
1161
+ line_end = source.find("\n", match.start())
1162
+ if line_end == -1:
1163
+ line_end = len(source)
1164
+ line = source[line_start:line_end]
1165
+ # Skip comment lines
1166
+ if line.strip().startswith("//") or line.strip().startswith("*"):
1167
+ continue
1168
+ line_num = source[: match.start()].count("\n") + 1
1169
+ violations.append(
1170
+ Violation(
1171
+ function="<source>",
1172
+ file=source_file,
1173
+ line=line_num,
1174
+ address="",
1175
+ instruction="/",
1176
+ mnemonic="DIV_OP",
1177
+ reason="Division operator has variable-time execution",
1178
+ severity=Severity.ERROR,
1179
+ )
1180
+ )
1181
+
1182
+ mod_pattern = r"\s%\s*[^=]"
1183
+ for match in re.finditer(mod_pattern, source):
1184
+ line_start = source.rfind("\n", 0, match.start()) + 1
1185
+ line_end = source.find("\n", match.start())
1186
+ if line_end == -1:
1187
+ line_end = len(source)
1188
+ line = source[line_start:line_end]
1189
+ if line.strip().startswith("//") or line.strip().startswith("*"):
1190
+ continue
1191
+ line_num = source[: match.start()].count("\n") + 1
1192
+ violations.append(
1193
+ Violation(
1194
+ function="<source>",
1195
+ file=source_file,
1196
+ line=line_num,
1197
+ address="",
1198
+ instruction="%",
1199
+ mnemonic="MOD_OP",
1200
+ reason="Modulo operator has variable-time execution",
1201
+ severity=Severity.ERROR,
1202
+ )
1203
+ )
1204
+
1205
+ if include_warnings:
1206
+ for func_name, reason in DANGEROUS_JS_FUNCTIONS["warnings"].items():
1207
+ pattern = rf"\.{re.escape(func_name)}\s*\("
1208
+ for match in re.finditer(pattern, source, re.IGNORECASE):
1209
+ line_num = source[: match.start()].count("\n") + 1
1210
+ violations.append(
1211
+ Violation(
1212
+ function="<source>",
1213
+ file=source_file,
1214
+ line=line_num,
1215
+ address="",
1216
+ instruction=match.group(0),
1217
+ mnemonic=func_name.upper(),
1218
+ reason=reason,
1219
+ severity=Severity.WARNING,
1220
+ )
1221
+ )
1222
+
1223
+ return violations
1224
+
1225
+ def analyze(
1226
+ self,
1227
+ source_file: str,
1228
+ include_warnings: bool = False,
1229
+ function_filter: str | None = None,
1230
+ ) -> AnalysisReport:
1231
+ """Analyze a JavaScript or TypeScript file for constant-time violations."""
1232
+ source_path = Path(source_file)
1233
+ if not source_path.exists():
1234
+ raise FileNotFoundError(f"Source file not found: {source_file}")
1235
+
1236
+ js_file = source_file
1237
+ is_typescript = source_path.suffix.lower() in (".ts", ".tsx")
1238
+
1239
+ # Handle TypeScript
1240
+ if is_typescript:
1241
+ if not self._is_tsc_available():
1242
+ raise RuntimeError(
1243
+ "TypeScript compiler not found. Install with: npm install -g typescript"
1244
+ )
1245
+
1246
+ with tempfile.TemporaryDirectory() as tmpdir:
1247
+ success, result = self._transpile_typescript(source_file, tmpdir)
1248
+ if not success:
1249
+ raise RuntimeError(f"TypeScript compilation failed: {result}")
1250
+ js_file = result
1251
+
1252
+ # Analyze the transpiled JS
1253
+ return self._analyze_js(
1254
+ js_file,
1255
+ source_file, # Report against original TS file
1256
+ include_warnings,
1257
+ function_filter,
1258
+ )
1259
+ else:
1260
+ return self._analyze_js(
1261
+ js_file,
1262
+ source_file,
1263
+ include_warnings,
1264
+ function_filter,
1265
+ )
1266
+
1267
+ def _analyze_js(
1268
+ self,
1269
+ js_file: str,
1270
+ report_file: str,
1271
+ include_warnings: bool = False,
1272
+ function_filter: str | None = None,
1273
+ ) -> AnalysisReport:
1274
+ """Analyze a JavaScript file."""
1275
+ success, output = self._get_v8_bytecode(js_file, function_filter)
1276
+ if not success:
1277
+ raise RuntimeError(f"Failed to get V8 bytecode: {output}")
1278
+
1279
+ functions, violations = self._parse_v8_bytecode(
1280
+ output,
1281
+ report_file,
1282
+ include_warnings,
1283
+ function_filter,
1284
+ )
1285
+
1286
+ # Also check for dangerous function calls in source
1287
+ source_violations = self._detect_dangerous_function_calls(
1288
+ report_file if Path(report_file).exists() else js_file,
1289
+ include_warnings,
1290
+ )
1291
+
1292
+ # Merge violations, avoiding duplicates
1293
+ existing = {(v.line, v.mnemonic) for v in violations}
1294
+ for v in source_violations:
1295
+ if (v.line, v.mnemonic) not in existing:
1296
+ violations.append(v)
1297
+
1298
+ return AnalysisReport(
1299
+ architecture="v8",
1300
+ compiler="node",
1301
+ optimization="default",
1302
+ source_file=report_file,
1303
+ total_functions=len(functions),
1304
+ total_instructions=sum(f["instructions"] for f in functions),
1305
+ violations=violations,
1306
+ )
1307
+
1308
+
1309
+ # =============================================================================
1310
+ # Python Analyzer
1311
+ # =============================================================================
1312
+
1313
+
1314
+ class PythonAnalyzer(ScriptAnalyzer):
1315
+ """
1316
+ Analyzer for Python scripts using the dis module for bytecode disassembly.
1317
+
1318
+ Detects timing-unsafe bytecodes and function calls in Python code.
1319
+ """
1320
+
1321
+ name = "python"
1322
+
1323
+ # Python 3.11+ BINARY_OP opargs for division/modulo
1324
+ # See: https://docs.python.org/3.11/library/dis.html#opcode-BINARY_OP
1325
+ BINARY_OP_DIV_OPARGS = {
1326
+ 11: "BINARY_OP_TRUEDIV", # /
1327
+ 12: "BINARY_OP_FLOORDIV", # //
1328
+ 6: "BINARY_OP_MODULO", # %
1329
+ # Inplace variants
1330
+ 24: "BINARY_OP_INPLACE_TRUEDIV", # /=
1331
+ 25: "BINARY_OP_INPLACE_FLOORDIV", # //=
1332
+ 19: "BINARY_OP_INPLACE_MODULO", # %=
1333
+ }
1334
+
1335
+ def __init__(self, python_path: str | None = None):
1336
+ self.python_path = python_path or "python3"
1337
+
1338
+ def is_available(self) -> bool:
1339
+ """Check if Python is available."""
1340
+ try:
1341
+ result = subprocess.run(
1342
+ [self.python_path, "--version"],
1343
+ capture_output=True,
1344
+ text=True,
1345
+ )
1346
+ return result.returncode == 0
1347
+ except FileNotFoundError:
1348
+ return False
1349
+
1350
+ def _get_dis_output(self, source_file: str) -> tuple[bool, str]:
1351
+ """Get Python dis module output for bytecode disassembly."""
1352
+ cmd = [
1353
+ self.python_path,
1354
+ "-m",
1355
+ "dis",
1356
+ source_file,
1357
+ ]
1358
+
1359
+ try:
1360
+ result = subprocess.run(cmd, capture_output=True, text=True)
1361
+ # dis outputs to stdout
1362
+ if result.returncode != 0:
1363
+ return False, result.stderr or result.stdout
1364
+ return True, result.stdout
1365
+ except FileNotFoundError:
1366
+ return False, f"Python not found: {self.python_path}"
1367
+
1368
+ def _parse_dis_output(
1369
+ self,
1370
+ output: str,
1371
+ source_file: str,
1372
+ include_warnings: bool = False,
1373
+ function_filter: str | None = None,
1374
+ ) -> tuple[list[dict], list[Violation]]:
1375
+ """
1376
+ Parse Python dis output for dangerous bytecodes.
1377
+
1378
+ dis output format example:
1379
+ Disassembly of <code object vulnerable_function at 0x...>:
1380
+ 3 0 LOAD_FAST 0 (value)
1381
+ 2 LOAD_FAST 1 (modulus)
1382
+ 4 BINARY_TRUE_DIVIDE
1383
+ 6 STORE_FAST 2 (result)
1384
+ ...
1385
+
1386
+ Python 3.11+ format:
1387
+ Disassembly of <code object vulnerable_function at 0x...>:
1388
+ 3 0 RESUME 0
1389
+
1390
+ 4 2 LOAD_FAST 0 (value)
1391
+ 4 LOAD_FAST 1 (modulus)
1392
+ 6 BINARY_OP 11 (/)
1393
+ 8 STORE_FAST 2 (result)
1394
+ """
1395
+ functions = []
1396
+ violations = []
1397
+
1398
+ current_function = None
1399
+ filter_pattern = re.compile(function_filter) if function_filter else None
1400
+
1401
+ for line in output.split("\n"):
1402
+ line_stripped = line.strip()
1403
+
1404
+ # Detect function/code object start
1405
+ # Format: Disassembly of <code object functionName at 0x...>:
1406
+ func_match = re.match(r"Disassembly of <code object\s+([^\s>]+)", line_stripped)
1407
+ if func_match:
1408
+ func_name = func_match.group(1).strip()
1409
+ current_function = func_name
1410
+ functions.append({"name": current_function, "instructions": 0})
1411
+ continue
1412
+
1413
+ # Also detect module-level code
1414
+ if line_stripped.startswith("Disassembly of") and "<module>" in line_stripped:
1415
+ current_function = "<module>"
1416
+ functions.append({"name": current_function, "instructions": 0})
1417
+ continue
1418
+
1419
+ # Parse bytecode instruction
1420
+ # Format: line_num? offset INSTRUCTION oparg? (argval?)
1421
+ # Examples:
1422
+ # 3 0 LOAD_FAST 0 (value)
1423
+ # 2 LOAD_FAST 1 (modulus)
1424
+ # 4 BINARY_TRUE_DIVIDE
1425
+ # 6 BINARY_OP 11 (/)
1426
+ bytecode_match = re.match(r"(?:(\d+)\s+)?(\d+)\s+([A-Z_]+)\s*(.*)", line_stripped)
1427
+
1428
+ if not bytecode_match:
1429
+ continue
1430
+
1431
+ line_num = int(bytecode_match.group(1)) if bytecode_match.group(1) else None
1432
+ offset = bytecode_match.group(2)
1433
+ instruction = bytecode_match.group(3).strip()
1434
+ operands = bytecode_match.group(4).strip() if bytecode_match.group(4) else ""
1435
+
1436
+ # Update instruction count
1437
+ if functions:
1438
+ functions[-1]["instructions"] += 1
1439
+
1440
+ # Check if we should skip this function
1441
+ if filter_pattern and current_function:
1442
+ if not filter_pattern.search(current_function):
1443
+ continue
1444
+
1445
+ instruction_lower = instruction.lower()
1446
+
1447
+ # Handle Python 3.11+ BINARY_OP with oparg
1448
+ if instruction == "BINARY_OP":
1449
+ # Extract oparg number from operands
1450
+ oparg_match = re.match(r"(\d+)", operands)
1451
+ if oparg_match:
1452
+ oparg = int(oparg_match.group(1))
1453
+ if oparg in self.BINARY_OP_DIV_OPARGS:
1454
+ op_name = self.BINARY_OP_DIV_OPARGS[oparg]
1455
+ violations.append(
1456
+ Violation(
1457
+ function=current_function or "<module>",
1458
+ file=source_file,
1459
+ line=line_num,
1460
+ address=offset,
1461
+ instruction=f"{instruction} {operands}".strip(),
1462
+ mnemonic=op_name,
1463
+ reason=f"{op_name} has variable-time execution",
1464
+ severity=Severity.ERROR,
1465
+ )
1466
+ )
1467
+ continue
1468
+
1469
+ # Check for dangerous bytecodes (Python < 3.11)
1470
+ if instruction_lower in DANGEROUS_PYTHON_BYTECODES["errors"]:
1471
+ violations.append(
1472
+ Violation(
1473
+ function=current_function or "<module>",
1474
+ file=source_file,
1475
+ line=line_num,
1476
+ address=offset,
1477
+ instruction=f"{instruction} {operands}".strip(),
1478
+ mnemonic=instruction.upper(),
1479
+ reason=DANGEROUS_PYTHON_BYTECODES["errors"][instruction_lower],
1480
+ severity=Severity.ERROR,
1481
+ )
1482
+ )
1483
+ elif include_warnings and instruction_lower in DANGEROUS_PYTHON_BYTECODES["warnings"]:
1484
+ violations.append(
1485
+ Violation(
1486
+ function=current_function or "<module>",
1487
+ file=source_file,
1488
+ line=line_num,
1489
+ address=offset,
1490
+ instruction=f"{instruction} {operands}".strip(),
1491
+ mnemonic=instruction.upper(),
1492
+ reason=DANGEROUS_PYTHON_BYTECODES["warnings"][instruction_lower],
1493
+ severity=Severity.WARNING,
1494
+ )
1495
+ )
1496
+
1497
+ return functions, violations
1498
+
1499
+ def _detect_dangerous_function_calls(
1500
+ self,
1501
+ source_file: str,
1502
+ include_warnings: bool = False,
1503
+ ) -> list[Violation]:
1504
+ """
1505
+ Detect dangerous function calls via static analysis of source.
1506
+ """
1507
+ violations = []
1508
+
1509
+ try:
1510
+ with open(source_file) as f:
1511
+ source = f.read()
1512
+ except OSError:
1513
+ return violations
1514
+
1515
+ # Detect dangerous function calls
1516
+ for func_name, reason in DANGEROUS_PYTHON_FUNCTIONS["errors"].items():
1517
+ # Match function calls like random.random() or math.sqrt()
1518
+ pattern = rf"\b{re.escape(func_name)}\s*\("
1519
+ for match in re.finditer(pattern, source, re.IGNORECASE):
1520
+ line_num = source[: match.start()].count("\n") + 1
1521
+ violations.append(
1522
+ Violation(
1523
+ function="<source>",
1524
+ file=source_file,
1525
+ line=line_num,
1526
+ address="",
1527
+ instruction=match.group(0),
1528
+ mnemonic=func_name.upper().replace(".", "_"),
1529
+ reason=reason,
1530
+ severity=Severity.ERROR,
1531
+ )
1532
+ )
1533
+
1534
+ if include_warnings:
1535
+ for func_name, reason in DANGEROUS_PYTHON_FUNCTIONS["warnings"].items():
1536
+ # Match method calls like .find(), .startswith()
1537
+ method_name = func_name.split(".")[-1] if "." in func_name else func_name
1538
+ pattern = rf"\.{re.escape(method_name)}\s*\("
1539
+ for match in re.finditer(pattern, source, re.IGNORECASE):
1540
+ line_num = source[: match.start()].count("\n") + 1
1541
+ violations.append(
1542
+ Violation(
1543
+ function="<source>",
1544
+ file=source_file,
1545
+ line=line_num,
1546
+ address="",
1547
+ instruction=match.group(0),
1548
+ mnemonic=method_name.upper(),
1549
+ reason=reason,
1550
+ severity=Severity.WARNING,
1551
+ )
1552
+ )
1553
+
1554
+ return violations
1555
+
1556
+ def analyze(
1557
+ self,
1558
+ source_file: str,
1559
+ include_warnings: bool = False,
1560
+ function_filter: str | None = None,
1561
+ ) -> AnalysisReport:
1562
+ """Analyze a Python file for constant-time violations."""
1563
+ source_path = Path(source_file)
1564
+ if not source_path.exists():
1565
+ raise FileNotFoundError(f"Source file not found: {source_file}")
1566
+
1567
+ success, output = self._get_dis_output(str(source_path.absolute()))
1568
+ if not success:
1569
+ raise RuntimeError(f"Failed to get Python bytecode: {output}")
1570
+
1571
+ functions, violations = self._parse_dis_output(
1572
+ output,
1573
+ source_file,
1574
+ include_warnings,
1575
+ function_filter,
1576
+ )
1577
+
1578
+ # Also check for dangerous function calls in source
1579
+ source_violations = self._detect_dangerous_function_calls(
1580
+ source_file,
1581
+ include_warnings,
1582
+ )
1583
+
1584
+ # Merge violations, avoiding duplicates
1585
+ existing = {(v.line, v.mnemonic) for v in violations}
1586
+ for v in source_violations:
1587
+ if (v.line, v.mnemonic) not in existing:
1588
+ violations.append(v)
1589
+
1590
+ return AnalysisReport(
1591
+ architecture="cpython",
1592
+ compiler="python3",
1593
+ optimization="default",
1594
+ source_file=str(source_file),
1595
+ total_functions=len(functions),
1596
+ total_instructions=sum(f["instructions"] for f in functions),
1597
+ violations=violations,
1598
+ )
1599
+
1600
+
1601
+ # =============================================================================
1602
+ # Ruby Analyzer
1603
+ # =============================================================================
1604
+
1605
+
1606
+ class RubyAnalyzer(ScriptAnalyzer):
1607
+ """
1608
+ Analyzer for Ruby scripts using YARV instruction sequence dump.
1609
+
1610
+ Detects timing-unsafe bytecodes and function calls in Ruby code.
1611
+ """
1612
+
1613
+ name = "ruby"
1614
+
1615
+ def __init__(self, ruby_path: str | None = None):
1616
+ self.ruby_path = ruby_path or "ruby"
1617
+
1618
+ def is_available(self) -> bool:
1619
+ """Check if Ruby is available."""
1620
+ try:
1621
+ result = subprocess.run(
1622
+ [self.ruby_path, "--version"],
1623
+ capture_output=True,
1624
+ text=True,
1625
+ )
1626
+ return result.returncode == 0
1627
+ except FileNotFoundError:
1628
+ return False
1629
+
1630
+ def _get_yarv_output(self, source_file: str) -> tuple[bool, str]:
1631
+ """Get Ruby YARV instruction sequence dump."""
1632
+ # Use --dump=insns to get instruction sequence
1633
+ cmd = [
1634
+ self.ruby_path,
1635
+ "--dump=insns",
1636
+ source_file,
1637
+ ]
1638
+
1639
+ try:
1640
+ result = subprocess.run(cmd, capture_output=True, text=True)
1641
+ # Ruby dumps to stdout
1642
+ if result.returncode != 0:
1643
+ return False, result.stderr or result.stdout
1644
+ return True, result.stdout
1645
+ except FileNotFoundError:
1646
+ return False, f"Ruby not found: {self.ruby_path}"
1647
+
1648
+ def _parse_yarv_output(
1649
+ self,
1650
+ output: str,
1651
+ source_file: str,
1652
+ include_warnings: bool = False,
1653
+ function_filter: str | None = None,
1654
+ ) -> tuple[list[dict], list[Violation]]:
1655
+ """
1656
+ Parse Ruby YARV instruction sequence output.
1657
+
1658
+ YARV output format example:
1659
+ == disasm: #<ISeq:<main>@test.rb:1 (1,0)-(10,3)>
1660
+ 0000 putobject 10
1661
+ 0002 putobject 3
1662
+ 0004 opt_div <calldata!...>
1663
+ 0006 leave
1664
+
1665
+ == disasm: #<ISeq:vulnerable_function@test.rb:1 (1,0)-(5,3)>
1666
+ local table (size: 2, argc: 2 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
1667
+ [ 2] value@0 [ 1] modulus@1
1668
+ 0000 getlocal_WC_0 value@0
1669
+ 0002 getlocal_WC_0 modulus@1
1670
+ 0004 opt_div <calldata!mid:/, argc:1, ARGS_SIMPLE>
1671
+ 0006 leave
1672
+ """
1673
+ functions = []
1674
+ violations = []
1675
+
1676
+ current_function = None
1677
+ current_line = None
1678
+ filter_pattern = re.compile(function_filter) if function_filter else None
1679
+
1680
+ for line in output.split("\n"):
1681
+ line_stripped = line.strip()
1682
+
1683
+ # Detect function/instruction sequence start
1684
+ # Format: == disasm: #<ISeq:functionName@file.rb:line ...>
1685
+ func_match = re.match(r"==\s*disasm:\s*#<ISeq:([^@]+)@([^:]+):(\d+)", line_stripped)
1686
+ if func_match:
1687
+ func_name = func_match.group(1).strip()
1688
+ # file_name = func_match.group(2)
1689
+ start_line = int(func_match.group(3))
1690
+ current_function = func_name
1691
+ current_line = start_line
1692
+ functions.append({"name": current_function, "instructions": 0})
1693
+ continue
1694
+
1695
+ # Skip local table and other metadata lines
1696
+ if line_stripped.startswith("local table") or line_stripped.startswith("["):
1697
+ continue
1698
+
1699
+ # Parse YARV instruction
1700
+ # Format: offset instruction operands
1701
+ # Examples:
1702
+ # 0000 putobject 10
1703
+ # 0004 opt_div <calldata!...>
1704
+ yarv_match = re.match(r"(\d{4})\s+([a-z_]+[a-z0-9_]*)\s*(.*)", line_stripped)
1705
+
1706
+ if not yarv_match:
1707
+ continue
1708
+
1709
+ offset = yarv_match.group(1)
1710
+ instruction = yarv_match.group(2).strip()
1711
+ operands = yarv_match.group(3).strip() if yarv_match.group(3) else ""
1712
+
1713
+ # Update instruction count
1714
+ if functions:
1715
+ functions[-1]["instructions"] += 1
1716
+
1717
+ # Check if we should skip this function
1718
+ if filter_pattern and current_function:
1719
+ if not filter_pattern.search(current_function):
1720
+ continue
1721
+
1722
+ instruction_lower = instruction.lower()
1723
+
1724
+ # Check for dangerous bytecodes
1725
+ if instruction_lower in DANGEROUS_RUBY_BYTECODES["errors"]:
1726
+ violations.append(
1727
+ Violation(
1728
+ function=current_function or "<main>",
1729
+ file=source_file,
1730
+ line=current_line,
1731
+ address=offset,
1732
+ instruction=f"{instruction} {operands}".strip(),
1733
+ mnemonic=instruction.upper(),
1734
+ reason=DANGEROUS_RUBY_BYTECODES["errors"][instruction_lower],
1735
+ severity=Severity.ERROR,
1736
+ )
1737
+ )
1738
+ elif include_warnings and instruction_lower in DANGEROUS_RUBY_BYTECODES["warnings"]:
1739
+ violations.append(
1740
+ Violation(
1741
+ function=current_function or "<main>",
1742
+ file=source_file,
1743
+ line=current_line,
1744
+ address=offset,
1745
+ instruction=f"{instruction} {operands}".strip(),
1746
+ mnemonic=instruction.upper(),
1747
+ reason=DANGEROUS_RUBY_BYTECODES["warnings"][instruction_lower],
1748
+ severity=Severity.WARNING,
1749
+ )
1750
+ )
1751
+
1752
+ return functions, violations
1753
+
1754
+ def _detect_dangerous_function_calls(
1755
+ self,
1756
+ source_file: str,
1757
+ include_warnings: bool = False,
1758
+ ) -> list[Violation]:
1759
+ """
1760
+ Detect dangerous function calls via static analysis of source.
1761
+ """
1762
+ violations = []
1763
+
1764
+ try:
1765
+ with open(source_file) as f:
1766
+ source = f.read()
1767
+ except OSError:
1768
+ return violations
1769
+
1770
+ # Detect dangerous function calls
1771
+ for func_name, reason in DANGEROUS_RUBY_FUNCTIONS["errors"].items():
1772
+ # Match function calls like rand() or Random.new
1773
+ if func_name == "random":
1774
+ # Match Random.new or Random.rand
1775
+ pattern = r"\bRandom\.(new|rand|bytes)\s*[(\[]?"
1776
+ elif func_name == "math.sqrt":
1777
+ pattern = r"\bMath\.sqrt\s*\("
1778
+ else:
1779
+ pattern = rf"\b{re.escape(func_name)}\s*[(\[]?"
1780
+ for match in re.finditer(pattern, source, re.IGNORECASE):
1781
+ line_num = source[: match.start()].count("\n") + 1
1782
+ violations.append(
1783
+ Violation(
1784
+ function="<source>",
1785
+ file=source_file,
1786
+ line=line_num,
1787
+ address="",
1788
+ instruction=match.group(0),
1789
+ mnemonic=func_name.upper().replace(".", "_").replace("?", ""),
1790
+ reason=reason,
1791
+ severity=Severity.ERROR,
1792
+ )
1793
+ )
1794
+
1795
+ if include_warnings:
1796
+ for func_name, reason in DANGEROUS_RUBY_FUNCTIONS["warnings"].items():
1797
+ # Match method calls like .include?(), .start_with?()
1798
+ if func_name == "=~":
1799
+ pattern = r"\s=~\s"
1800
+ else:
1801
+ pattern = rf"\.{re.escape(func_name)}\s*[(\[]?"
1802
+ for match in re.finditer(pattern, source):
1803
+ line_num = source[: match.start()].count("\n") + 1
1804
+ violations.append(
1805
+ Violation(
1806
+ function="<source>",
1807
+ file=source_file,
1808
+ line=line_num,
1809
+ address="",
1810
+ instruction=match.group(0),
1811
+ mnemonic=func_name.upper().replace("?", ""),
1812
+ reason=reason,
1813
+ severity=Severity.WARNING,
1814
+ )
1815
+ )
1816
+
1817
+ return violations
1818
+
1819
+ def analyze(
1820
+ self,
1821
+ source_file: str,
1822
+ include_warnings: bool = False,
1823
+ function_filter: str | None = None,
1824
+ ) -> AnalysisReport:
1825
+ """Analyze a Ruby file for constant-time violations."""
1826
+ source_path = Path(source_file)
1827
+ if not source_path.exists():
1828
+ raise FileNotFoundError(f"Source file not found: {source_file}")
1829
+
1830
+ success, output = self._get_yarv_output(str(source_path.absolute()))
1831
+ if not success:
1832
+ raise RuntimeError(f"Failed to get Ruby bytecode: {output}")
1833
+
1834
+ functions, violations = self._parse_yarv_output(
1835
+ output,
1836
+ source_file,
1837
+ include_warnings,
1838
+ function_filter,
1839
+ )
1840
+
1841
+ # Also check for dangerous function calls in source
1842
+ source_violations = self._detect_dangerous_function_calls(
1843
+ source_file,
1844
+ include_warnings,
1845
+ )
1846
+
1847
+ # Merge violations, avoiding duplicates
1848
+ existing = {(v.line, v.mnemonic) for v in violations}
1849
+ for v in source_violations:
1850
+ if (v.line, v.mnemonic) not in existing:
1851
+ violations.append(v)
1852
+
1853
+ return AnalysisReport(
1854
+ architecture="yarv",
1855
+ compiler="ruby",
1856
+ optimization="default",
1857
+ source_file=str(source_file),
1858
+ total_functions=len(functions),
1859
+ total_instructions=sum(f["instructions"] for f in functions),
1860
+ violations=violations,
1861
+ )
1862
+
1863
+
1864
+ # =============================================================================
1865
+ # Java Analyzer
1866
+ # =============================================================================
1867
+
1868
+
1869
+ class JavaAnalyzer(ScriptAnalyzer):
1870
+ """
1871
+ Analyzer for Java source files using javap for bytecode disassembly.
1872
+
1873
+ Compiles Java source to bytecode and analyzes for timing-unsafe operations.
1874
+ """
1875
+
1876
+ name = "java"
1877
+
1878
+ def __init__(self, javac_path: str | None = None, javap_path: str | None = None):
1879
+ self.javac_path = javac_path or "javac"
1880
+ self.javap_path = javap_path or "javap"
1881
+
1882
+ def is_available(self) -> bool:
1883
+ """Check if Java compiler and disassembler are available."""
1884
+ try:
1885
+ result = subprocess.run(
1886
+ [self.javac_path, "-version"],
1887
+ capture_output=True,
1888
+ text=True,
1889
+ )
1890
+ if result.returncode != 0:
1891
+ return False
1892
+ result = subprocess.run(
1893
+ [self.javap_path, "-version"],
1894
+ capture_output=True,
1895
+ text=True,
1896
+ )
1897
+ return result.returncode == 0
1898
+ except FileNotFoundError:
1899
+ return False
1900
+
1901
+ def _compile_java(self, source_file: str, output_dir: str) -> tuple[bool, str]:
1902
+ """Compile Java source to class files."""
1903
+ cmd = [
1904
+ self.javac_path,
1905
+ "-d",
1906
+ output_dir,
1907
+ source_file,
1908
+ ]
1909
+
1910
+ try:
1911
+ result = subprocess.run(cmd, capture_output=True, text=True)
1912
+ if result.returncode != 0:
1913
+ return False, result.stderr or result.stdout
1914
+ return True, output_dir
1915
+ except FileNotFoundError:
1916
+ return False, f"Java compiler not found: {self.javac_path}"
1917
+
1918
+ def _get_bytecode_output(self, class_file: str) -> tuple[bool, str]:
1919
+ """Get javap bytecode disassembly for a class file."""
1920
+ cmd = [
1921
+ self.javap_path,
1922
+ "-c", # Disassemble code
1923
+ "-p", # Show private members
1924
+ "-v", # Verbose (includes line numbers)
1925
+ class_file,
1926
+ ]
1927
+
1928
+ try:
1929
+ result = subprocess.run(cmd, capture_output=True, text=True)
1930
+ if result.returncode != 0:
1931
+ return False, result.stderr or result.stdout
1932
+ return True, result.stdout
1933
+ except FileNotFoundError:
1934
+ return False, f"Java disassembler not found: {self.javap_path}"
1935
+
1936
+ def _parse_javap_output(
1937
+ self,
1938
+ output: str,
1939
+ source_file: str,
1940
+ include_warnings: bool = False,
1941
+ function_filter: str | None = None,
1942
+ ) -> tuple[list[dict], list[Violation]]:
1943
+ """
1944
+ Parse javap bytecode output for dangerous operations.
1945
+
1946
+ javap output format example:
1947
+ public class CryptoUtils {
1948
+ public int vulnerable(int, int);
1949
+ Code:
1950
+ 0: iload_1
1951
+ 1: iload_2
1952
+ 2: idiv
1953
+ 3: ireturn
1954
+ LineNumberTable:
1955
+ line 5: 0
1956
+ """
1957
+ functions = []
1958
+ violations = []
1959
+
1960
+ current_method = None
1961
+ current_class = None
1962
+ current_line = None
1963
+ in_code_section = False
1964
+ filter_pattern = re.compile(function_filter) if function_filter else None
1965
+
1966
+ # Line number table mapping: bytecode offset -> source line
1967
+ line_number_table: dict[int, int] = {}
1968
+ in_line_number_table = False
1969
+
1970
+ for line in output.split("\n"):
1971
+ line_stripped = line.strip()
1972
+
1973
+ # Detect class declaration
1974
+ class_match = re.match(
1975
+ r"(?:public\s+|private\s+|protected\s+)?(?:final\s+)?class\s+(\S+)", line_stripped
1976
+ )
1977
+ if class_match:
1978
+ current_class = class_match.group(1)
1979
+ continue
1980
+
1981
+ # Detect method declaration
1982
+ method_match = re.match(
1983
+ r"(?:public|private|protected|static|\s)+\S+\s+(\w+)\s*\(", line_stripped
1984
+ )
1985
+ if method_match and not line_stripped.startswith("//"):
1986
+ method_name = method_match.group(1)
1987
+ if current_class:
1988
+ current_method = f"{current_class}.{method_name}"
1989
+ else:
1990
+ current_method = method_name
1991
+ functions.append({"name": current_method, "instructions": 0})
1992
+ in_code_section = False
1993
+ in_line_number_table = False
1994
+ line_number_table = {}
1995
+ continue
1996
+
1997
+ # Detect Code section start
1998
+ if line_stripped == "Code:":
1999
+ in_code_section = True
2000
+ in_line_number_table = False
2001
+ continue
2002
+
2003
+ # Detect LineNumberTable start
2004
+ if line_stripped == "LineNumberTable:":
2005
+ in_line_number_table = True
2006
+ in_code_section = False
2007
+ continue
2008
+
2009
+ # Parse line number table entries
2010
+ if in_line_number_table:
2011
+ lnt_match = re.match(r"line\s+(\d+):\s+(\d+)", line_stripped)
2012
+ if lnt_match:
2013
+ source_line = int(lnt_match.group(1))
2014
+ bytecode_offset = int(lnt_match.group(2))
2015
+ line_number_table[bytecode_offset] = source_line
2016
+ elif line_stripped and not line_stripped.startswith("line"):
2017
+ in_line_number_table = False
2018
+ continue
2019
+
2020
+ if not in_code_section:
2021
+ continue
2022
+
2023
+ # Parse bytecode instruction
2024
+ # Format: offset: instruction [operands]
2025
+ bytecode_match = re.match(r"(\d+):\s+(\w+)\s*(.*)", line_stripped)
2026
+
2027
+ if not bytecode_match:
2028
+ # End of code section - look for markers that indicate we've left the code
2029
+ # Skip metadata lines like "stack=3, locals=4, args_size=2"
2030
+ if line_stripped.startswith(("stack=", "locals", "args_size")):
2031
+ continue
2032
+ # Other non-digit lines end the code section
2033
+ if line_stripped and not line_stripped[0].isdigit():
2034
+ in_code_section = False
2035
+ continue
2036
+
2037
+ offset = int(bytecode_match.group(1))
2038
+ instruction = bytecode_match.group(2).strip()
2039
+ operands = bytecode_match.group(3).strip() if bytecode_match.group(3) else ""
2040
+
2041
+ # Look up source line from line number table
2042
+ current_line = line_number_table.get(offset)
2043
+
2044
+ # Update instruction count
2045
+ if functions:
2046
+ functions[-1]["instructions"] += 1
2047
+
2048
+ # Check if we should skip this method
2049
+ if filter_pattern and current_method:
2050
+ if not filter_pattern.search(current_method):
2051
+ continue
2052
+
2053
+ instruction_lower = instruction.lower()
2054
+
2055
+ # Check for dangerous bytecodes
2056
+ if instruction_lower in DANGEROUS_JAVA_BYTECODES["errors"]:
2057
+ violations.append(
2058
+ Violation(
2059
+ function=current_method or "<unknown>",
2060
+ file=source_file,
2061
+ line=current_line,
2062
+ address=str(offset),
2063
+ instruction=f"{instruction} {operands}".strip(),
2064
+ mnemonic=instruction.upper(),
2065
+ reason=DANGEROUS_JAVA_BYTECODES["errors"][instruction_lower],
2066
+ severity=Severity.ERROR,
2067
+ )
2068
+ )
2069
+ elif include_warnings and instruction_lower in DANGEROUS_JAVA_BYTECODES["warnings"]:
2070
+ violations.append(
2071
+ Violation(
2072
+ function=current_method or "<unknown>",
2073
+ file=source_file,
2074
+ line=current_line,
2075
+ address=str(offset),
2076
+ instruction=f"{instruction} {operands}".strip(),
2077
+ mnemonic=instruction.upper(),
2078
+ reason=DANGEROUS_JAVA_BYTECODES["warnings"][instruction_lower],
2079
+ severity=Severity.WARNING,
2080
+ )
2081
+ )
2082
+
2083
+ return functions, violations
2084
+
2085
+ def _detect_dangerous_function_calls(
2086
+ self,
2087
+ source_file: str,
2088
+ include_warnings: bool = False,
2089
+ ) -> list[Violation]:
2090
+ """Detect dangerous function calls via static analysis of source."""
2091
+ violations = []
2092
+
2093
+ try:
2094
+ with open(source_file) as f:
2095
+ source = f.read()
2096
+ except OSError:
2097
+ return violations
2098
+
2099
+ # Detect dangerous function calls
2100
+ for func_name, reason in DANGEROUS_JAVA_FUNCTIONS["errors"].items():
2101
+ if func_name == "java.util.random":
2102
+ pattern = r"\bnew\s+Random\s*\("
2103
+ elif func_name == "math.random":
2104
+ pattern = r"\bMath\.random\s*\("
2105
+ elif func_name == "math.sqrt":
2106
+ pattern = r"\bMath\.sqrt\s*\("
2107
+ elif func_name == "math.pow":
2108
+ pattern = r"\bMath\.pow\s*\("
2109
+ else:
2110
+ continue
2111
+ for match in re.finditer(pattern, source):
2112
+ line_num = source[: match.start()].count("\n") + 1
2113
+ violations.append(
2114
+ Violation(
2115
+ function="<source>",
2116
+ file=source_file,
2117
+ line=line_num,
2118
+ address="",
2119
+ instruction=match.group(0),
2120
+ mnemonic=func_name.upper().replace(".", "_"),
2121
+ reason=reason,
2122
+ severity=Severity.ERROR,
2123
+ )
2124
+ )
2125
+
2126
+ if include_warnings:
2127
+ for func_name, reason in DANGEROUS_JAVA_FUNCTIONS["warnings"].items():
2128
+ if func_name == "arrays.equals":
2129
+ pattern = r"\bArrays\.equals\s*\("
2130
+ elif func_name == "string.equals":
2131
+ pattern = r"\.equals\s*\("
2132
+ elif func_name == "string.compareto":
2133
+ pattern = r"\.compareTo\s*\("
2134
+ else:
2135
+ continue
2136
+ for match in re.finditer(pattern, source):
2137
+ line_num = source[: match.start()].count("\n") + 1
2138
+ violations.append(
2139
+ Violation(
2140
+ function="<source>",
2141
+ file=source_file,
2142
+ line=line_num,
2143
+ address="",
2144
+ instruction=match.group(0),
2145
+ mnemonic=func_name.upper().replace(".", "_"),
2146
+ reason=reason,
2147
+ severity=Severity.WARNING,
2148
+ )
2149
+ )
2150
+
2151
+ return violations
2152
+
2153
+ def analyze(
2154
+ self,
2155
+ source_file: str,
2156
+ include_warnings: bool = False,
2157
+ function_filter: str | None = None,
2158
+ ) -> AnalysisReport:
2159
+ """Analyze a Java file for constant-time violations."""
2160
+ source_path = Path(source_file)
2161
+ if not source_path.exists():
2162
+ raise FileNotFoundError(f"Source file not found: {source_file}")
2163
+
2164
+ with tempfile.TemporaryDirectory() as tmpdir:
2165
+ # Compile Java source
2166
+ success, result = self._compile_java(str(source_path.absolute()), tmpdir)
2167
+ if not success:
2168
+ raise RuntimeError(f"Java compilation failed: {result}")
2169
+
2170
+ # Find compiled class files
2171
+ class_files = list(Path(tmpdir).glob("**/*.class"))
2172
+ if not class_files:
2173
+ raise RuntimeError("No class files generated from compilation")
2174
+
2175
+ all_functions = []
2176
+ all_violations = []
2177
+
2178
+ # Analyze each class file
2179
+ for class_file in class_files:
2180
+ success, output = self._get_bytecode_output(str(class_file))
2181
+ if not success:
2182
+ continue
2183
+
2184
+ functions, violations = self._parse_javap_output(
2185
+ output,
2186
+ source_file,
2187
+ include_warnings,
2188
+ function_filter,
2189
+ )
2190
+ all_functions.extend(functions)
2191
+ all_violations.extend(violations)
2192
+
2193
+ # Also check for dangerous function calls in source
2194
+ source_violations = self._detect_dangerous_function_calls(
2195
+ source_file,
2196
+ include_warnings,
2197
+ )
2198
+
2199
+ # Merge violations, avoiding duplicates
2200
+ existing = {(v.line, v.mnemonic) for v in all_violations}
2201
+ for v in source_violations:
2202
+ if (v.line, v.mnemonic) not in existing:
2203
+ all_violations.append(v)
2204
+
2205
+ return AnalysisReport(
2206
+ architecture="jvm",
2207
+ compiler="javac",
2208
+ optimization="default",
2209
+ source_file=str(source_file),
2210
+ total_functions=len(all_functions),
2211
+ total_instructions=sum(f["instructions"] for f in all_functions),
2212
+ violations=all_violations,
2213
+ )
2214
+
2215
+
2216
+ # =============================================================================
2217
+ # Kotlin Analyzer
2218
+ # =============================================================================
2219
+
2220
+
2221
+ class KotlinAnalyzer(ScriptAnalyzer):
2222
+ """
2223
+ Analyzer for Kotlin source files using kotlinc and javap for bytecode disassembly.
2224
+
2225
+ Compiles Kotlin source to JVM bytecode and analyzes for timing-unsafe operations.
2226
+ Kotlin targets Android and JVM platforms, compiling to the same bytecode as Java.
2227
+ """
2228
+
2229
+ name = "kotlin"
2230
+
2231
+ def __init__(self, kotlinc_path: str | None = None, javap_path: str | None = None):
2232
+ self.kotlinc_path = kotlinc_path or "kotlinc"
2233
+ self.javap_path = javap_path or "javap"
2234
+
2235
+ def is_available(self) -> bool:
2236
+ """Check if Kotlin compiler and Java disassembler are available."""
2237
+ try:
2238
+ result = subprocess.run(
2239
+ [self.kotlinc_path, "-version"],
2240
+ capture_output=True,
2241
+ text=True,
2242
+ )
2243
+ if result.returncode != 0:
2244
+ return False
2245
+ result = subprocess.run(
2246
+ [self.javap_path, "-version"],
2247
+ capture_output=True,
2248
+ text=True,
2249
+ )
2250
+ return result.returncode == 0
2251
+ except FileNotFoundError:
2252
+ return False
2253
+
2254
+ def _compile_kotlin(self, source_file: str, output_dir: str) -> tuple[bool, str]:
2255
+ """Compile Kotlin source to class files."""
2256
+ cmd = [
2257
+ self.kotlinc_path,
2258
+ "-d",
2259
+ output_dir,
2260
+ source_file,
2261
+ ]
2262
+
2263
+ try:
2264
+ result = subprocess.run(cmd, capture_output=True, text=True)
2265
+ if result.returncode != 0:
2266
+ return False, result.stderr or result.stdout
2267
+ return True, output_dir
2268
+ except FileNotFoundError:
2269
+ return False, f"Kotlin compiler not found: {self.kotlinc_path}"
2270
+
2271
+ def _get_bytecode_output(self, class_file: str) -> tuple[bool, str]:
2272
+ """Get javap bytecode disassembly for a class file."""
2273
+ cmd = [
2274
+ self.javap_path,
2275
+ "-c", # Disassemble code
2276
+ "-p", # Show private members
2277
+ "-v", # Verbose (includes line numbers)
2278
+ class_file,
2279
+ ]
2280
+
2281
+ try:
2282
+ result = subprocess.run(cmd, capture_output=True, text=True)
2283
+ if result.returncode != 0:
2284
+ return False, result.stderr or result.stdout
2285
+ return True, result.stdout
2286
+ except FileNotFoundError:
2287
+ return False, f"Java disassembler not found: {self.javap_path}"
2288
+
2289
+ def _parse_javap_output(
2290
+ self,
2291
+ output: str,
2292
+ source_file: str,
2293
+ include_warnings: bool = False,
2294
+ function_filter: str | None = None,
2295
+ ) -> tuple[list[dict], list[Violation]]:
2296
+ """Parse javap bytecode output for dangerous operations (same as Java)."""
2297
+ functions = []
2298
+ violations = []
2299
+
2300
+ current_method = None
2301
+ current_class = None
2302
+ current_line = None
2303
+ in_code_section = False
2304
+ filter_pattern = re.compile(function_filter) if function_filter else None
2305
+
2306
+ line_number_table: dict[int, int] = {}
2307
+ in_line_number_table = False
2308
+
2309
+ for line in output.split("\n"):
2310
+ line_stripped = line.strip()
2311
+
2312
+ # Detect class declaration (including Kotlin's Kt suffix for file classes)
2313
+ class_match = re.match(
2314
+ r"(?:public\s+|private\s+|protected\s+)?(?:final\s+)?class\s+(\S+)", line_stripped
2315
+ )
2316
+ if class_match:
2317
+ current_class = class_match.group(1)
2318
+ continue
2319
+
2320
+ # Detect method declaration
2321
+ method_match = re.match(
2322
+ r"(?:public|private|protected|static|final|\s)+\S+\s+(\w+)\s*\(", line_stripped
2323
+ )
2324
+ if method_match and not line_stripped.startswith("//"):
2325
+ method_name = method_match.group(1)
2326
+ if current_class:
2327
+ current_method = f"{current_class}.{method_name}"
2328
+ else:
2329
+ current_method = method_name
2330
+ functions.append({"name": current_method, "instructions": 0})
2331
+ in_code_section = False
2332
+ in_line_number_table = False
2333
+ line_number_table = {}
2334
+ continue
2335
+
2336
+ # Detect Code section start
2337
+ if line_stripped == "Code:":
2338
+ in_code_section = True
2339
+ in_line_number_table = False
2340
+ continue
2341
+
2342
+ # Detect LineNumberTable start
2343
+ if line_stripped == "LineNumberTable:":
2344
+ in_line_number_table = True
2345
+ in_code_section = False
2346
+ continue
2347
+
2348
+ # Parse line number table entries
2349
+ if in_line_number_table:
2350
+ lnt_match = re.match(r"line\s+(\d+):\s+(\d+)", line_stripped)
2351
+ if lnt_match:
2352
+ source_line = int(lnt_match.group(1))
2353
+ bytecode_offset = int(lnt_match.group(2))
2354
+ line_number_table[bytecode_offset] = source_line
2355
+ elif line_stripped and not line_stripped.startswith("line"):
2356
+ in_line_number_table = False
2357
+ continue
2358
+
2359
+ if not in_code_section:
2360
+ continue
2361
+
2362
+ # Parse bytecode instruction
2363
+ bytecode_match = re.match(r"(\d+):\s+(\w+)\s*(.*)", line_stripped)
2364
+
2365
+ if not bytecode_match:
2366
+ if line_stripped.startswith(("stack=", "locals", "args_size")):
2367
+ continue
2368
+ if line_stripped and not line_stripped[0].isdigit():
2369
+ in_code_section = False
2370
+ continue
2371
+
2372
+ offset = int(bytecode_match.group(1))
2373
+ instruction = bytecode_match.group(2).strip()
2374
+ operands = bytecode_match.group(3).strip() if bytecode_match.group(3) else ""
2375
+
2376
+ current_line = line_number_table.get(offset)
2377
+
2378
+ if functions:
2379
+ functions[-1]["instructions"] += 1
2380
+
2381
+ if filter_pattern and current_method:
2382
+ if not filter_pattern.search(current_method):
2383
+ continue
2384
+
2385
+ instruction_lower = instruction.lower()
2386
+
2387
+ # Check for dangerous bytecodes (same as Java since Kotlin compiles to JVM)
2388
+ if instruction_lower in DANGEROUS_KOTLIN_BYTECODES["errors"]:
2389
+ violations.append(
2390
+ Violation(
2391
+ function=current_method or "<unknown>",
2392
+ file=source_file,
2393
+ line=current_line,
2394
+ address=str(offset),
2395
+ instruction=f"{instruction} {operands}".strip(),
2396
+ mnemonic=instruction.upper(),
2397
+ reason=DANGEROUS_KOTLIN_BYTECODES["errors"][instruction_lower],
2398
+ severity=Severity.ERROR,
2399
+ )
2400
+ )
2401
+ elif include_warnings and instruction_lower in DANGEROUS_KOTLIN_BYTECODES["warnings"]:
2402
+ violations.append(
2403
+ Violation(
2404
+ function=current_method or "<unknown>",
2405
+ file=source_file,
2406
+ line=current_line,
2407
+ address=str(offset),
2408
+ instruction=f"{instruction} {operands}".strip(),
2409
+ mnemonic=instruction.upper(),
2410
+ reason=DANGEROUS_KOTLIN_BYTECODES["warnings"][instruction_lower],
2411
+ severity=Severity.WARNING,
2412
+ )
2413
+ )
2414
+
2415
+ return functions, violations
2416
+
2417
+ def _detect_dangerous_function_calls(
2418
+ self,
2419
+ source_file: str,
2420
+ include_warnings: bool = False,
2421
+ ) -> list[Violation]:
2422
+ """Detect dangerous function calls via static analysis of Kotlin source."""
2423
+ violations = []
2424
+
2425
+ try:
2426
+ with open(source_file) as f:
2427
+ source = f.read()
2428
+ except OSError:
2429
+ return violations
2430
+
2431
+ # Detect dangerous function calls (Kotlin-specific patterns)
2432
+ for func_name, reason in DANGEROUS_KOTLIN_FUNCTIONS["errors"].items():
2433
+ pattern = None
2434
+ if func_name == "random.nextint":
2435
+ pattern = r"\bRandom\.nextInt\s*\("
2436
+ elif func_name == "random.nextlong":
2437
+ pattern = r"\bRandom\.nextLong\s*\("
2438
+ elif func_name == "random.nextdouble":
2439
+ pattern = r"\bRandom\.nextDouble\s*\("
2440
+ elif func_name == "random.nextfloat":
2441
+ pattern = r"\bRandom\.nextFloat\s*\("
2442
+ elif func_name == "random.nextbytes":
2443
+ pattern = r"\bRandom\.nextBytes\s*\("
2444
+ elif func_name == "random.default":
2445
+ pattern = r"\bRandom\.Default\b"
2446
+ elif func_name == "java.util.random":
2447
+ pattern = r"\bjava\.util\.Random\s*\("
2448
+ elif func_name == "math.random":
2449
+ pattern = r"\bMath\.random\s*\("
2450
+ elif func_name in ("kotlin.math.sqrt", "math.sqrt"):
2451
+ pattern = r"\b(?:kotlin\.math\.)?sqrt\s*\(|\bMath\.sqrt\s*\("
2452
+ elif func_name in ("kotlin.math.pow", "math.pow"):
2453
+ pattern = r"\b(?:kotlin\.math\.)?pow\s*\(|\bMath\.pow\s*\("
2454
+
2455
+ if pattern:
2456
+ for match in re.finditer(pattern, source, re.IGNORECASE):
2457
+ line_num = source[: match.start()].count("\n") + 1
2458
+ violations.append(
2459
+ Violation(
2460
+ function="<source>",
2461
+ file=source_file,
2462
+ line=line_num,
2463
+ address="",
2464
+ instruction=match.group(0),
2465
+ mnemonic=func_name.upper().replace(".", "_"),
2466
+ reason=reason,
2467
+ severity=Severity.ERROR,
2468
+ )
2469
+ )
2470
+
2471
+ if include_warnings:
2472
+ for func_name, reason in DANGEROUS_KOTLIN_FUNCTIONS["warnings"].items():
2473
+ pattern = None
2474
+ if func_name == "contentequals":
2475
+ pattern = r"\.contentEquals\s*\("
2476
+ elif func_name == "equals":
2477
+ pattern = r"\.equals\s*\("
2478
+ elif func_name == "compareto":
2479
+ pattern = r"\.compareTo\s*\("
2480
+ elif func_name == "arrays.equals":
2481
+ pattern = r"\bArrays\.equals\s*\("
2482
+
2483
+ if pattern:
2484
+ for match in re.finditer(pattern, source):
2485
+ line_num = source[: match.start()].count("\n") + 1
2486
+ violations.append(
2487
+ Violation(
2488
+ function="<source>",
2489
+ file=source_file,
2490
+ line=line_num,
2491
+ address="",
2492
+ instruction=match.group(0),
2493
+ mnemonic=func_name.upper().replace(".", "_"),
2494
+ reason=reason,
2495
+ severity=Severity.WARNING,
2496
+ )
2497
+ )
2498
+
2499
+ return violations
2500
+
2501
+ def analyze(
2502
+ self,
2503
+ source_file: str,
2504
+ include_warnings: bool = False,
2505
+ function_filter: str | None = None,
2506
+ ) -> AnalysisReport:
2507
+ """Analyze a Kotlin file for constant-time violations."""
2508
+ source_path = Path(source_file)
2509
+ if not source_path.exists():
2510
+ raise FileNotFoundError(f"Source file not found: {source_file}")
2511
+
2512
+ with tempfile.TemporaryDirectory() as tmpdir:
2513
+ # Compile Kotlin source
2514
+ success, result = self._compile_kotlin(str(source_path.absolute()), tmpdir)
2515
+ if not success:
2516
+ raise RuntimeError(f"Kotlin compilation failed: {result}")
2517
+
2518
+ # Find compiled class files
2519
+ class_files = list(Path(tmpdir).glob("**/*.class"))
2520
+ if not class_files:
2521
+ raise RuntimeError("No class files generated from compilation")
2522
+
2523
+ all_functions = []
2524
+ all_violations = []
2525
+
2526
+ # Analyze each class file
2527
+ for class_file in class_files:
2528
+ success, output = self._get_bytecode_output(str(class_file))
2529
+ if not success:
2530
+ continue
2531
+
2532
+ functions, violations = self._parse_javap_output(
2533
+ output,
2534
+ source_file,
2535
+ include_warnings,
2536
+ function_filter,
2537
+ )
2538
+ all_functions.extend(functions)
2539
+ all_violations.extend(violations)
2540
+
2541
+ # Also check for dangerous function calls in source
2542
+ source_violations = self._detect_dangerous_function_calls(
2543
+ source_file,
2544
+ include_warnings,
2545
+ )
2546
+
2547
+ # Merge violations, avoiding duplicates
2548
+ existing = {(v.line, v.mnemonic) for v in all_violations}
2549
+ for v in source_violations:
2550
+ if (v.line, v.mnemonic) not in existing:
2551
+ all_violations.append(v)
2552
+
2553
+ return AnalysisReport(
2554
+ architecture="jvm",
2555
+ compiler="kotlinc",
2556
+ optimization="default",
2557
+ source_file=str(source_file),
2558
+ total_functions=len(all_functions),
2559
+ total_instructions=sum(f["instructions"] for f in all_functions),
2560
+ violations=all_violations,
2561
+ )
2562
+
2563
+
2564
+ # =============================================================================
2565
+ # C# Analyzer
2566
+ # =============================================================================
2567
+
2568
+
2569
+ class CSharpAnalyzer(ScriptAnalyzer):
2570
+ """
2571
+ Analyzer for C# source files using .NET SDK for compilation and IL disassembly.
2572
+
2573
+ Compiles C# source to IL and analyzes for timing-unsafe operations.
2574
+ """
2575
+
2576
+ name = "csharp"
2577
+
2578
+ def __init__(self, dotnet_path: str | None = None):
2579
+ self.dotnet_path = dotnet_path or "dotnet"
2580
+
2581
+ def is_available(self) -> bool:
2582
+ """Check if .NET SDK is available."""
2583
+ try:
2584
+ result = subprocess.run(
2585
+ [self.dotnet_path, "--version"],
2586
+ capture_output=True,
2587
+ text=True,
2588
+ )
2589
+ return result.returncode == 0
2590
+ except FileNotFoundError:
2591
+ return False
2592
+
2593
+ def _compile_csharp(self, source_file: str, output_dir: str) -> tuple[bool, str]:
2594
+ """Compile C# source to DLL using dotnet build."""
2595
+ source_path = Path(source_file)
2596
+ output_dll = Path(output_dir) / f"{source_path.stem}.dll"
2597
+
2598
+ # Create a minimal project file for compilation
2599
+ proj_content = f"""<Project Sdk="Microsoft.NET.Sdk">
2600
+ <PropertyGroup>
2601
+ <TargetFramework>net8.0</TargetFramework>
2602
+ <OutputType>Library</OutputType>
2603
+ <OutputPath>{output_dir}</OutputPath>
2604
+ <EnableDefaultCompileItems>false</EnableDefaultCompileItems>
2605
+ </PropertyGroup>
2606
+ <ItemGroup>
2607
+ <Compile Include="{source_path.absolute()}" />
2608
+ </ItemGroup>
2609
+ </Project>
2610
+ """
2611
+ proj_file = Path(output_dir) / "temp.csproj"
2612
+ proj_file.write_text(proj_content)
2613
+
2614
+ cmd = [
2615
+ self.dotnet_path,
2616
+ "build",
2617
+ str(proj_file),
2618
+ "-c",
2619
+ "Release",
2620
+ "--nologo",
2621
+ "-v",
2622
+ "q",
2623
+ ]
2624
+
2625
+ try:
2626
+ result = subprocess.run(cmd, capture_output=True, text=True, cwd=output_dir)
2627
+ if result.returncode != 0:
2628
+ return False, result.stderr or result.stdout
2629
+
2630
+ # Find the output DLL
2631
+ dll_files = list(Path(output_dir).glob("**/*.dll"))
2632
+ if not dll_files:
2633
+ return False, "No DLL files generated"
2634
+
2635
+ return True, str(dll_files[0])
2636
+ except FileNotFoundError:
2637
+ return False, f".NET SDK not found: {self.dotnet_path}"
2638
+
2639
+ def _get_il_output(self, dll_file: str) -> tuple[bool, str]:
2640
+ """Get IL disassembly for a .NET assembly."""
2641
+ # First try ilspycmd directly (globally installed and in PATH)
2642
+ try:
2643
+ result = subprocess.run(
2644
+ ["ilspycmd", "-il", dll_file],
2645
+ capture_output=True,
2646
+ text=True,
2647
+ )
2648
+ if result.returncode == 0:
2649
+ return True, result.stdout
2650
+ except FileNotFoundError:
2651
+ pass # ilspycmd not in PATH, try other methods
2652
+
2653
+ # Try as local tool
2654
+ try:
2655
+ result = subprocess.run(
2656
+ [self.dotnet_path, "tool", "run", "ilspycmd", "-il", dll_file],
2657
+ capture_output=True,
2658
+ text=True,
2659
+ )
2660
+ if result.returncode == 0:
2661
+ return True, result.stdout
2662
+ except FileNotFoundError:
2663
+ pass # dotnet not found or local tool not installed
2664
+
2665
+ # Try running ilspycmd via .NET 8.0 from Homebrew (macOS)
2666
+ # This handles the case where ilspycmd targets .NET 8.0 but the system
2667
+ # has a newer .NET version installed
2668
+ dotnet8_paths = [
2669
+ "/opt/homebrew/opt/dotnet@8/libexec/dotnet", # Apple Silicon
2670
+ "/usr/local/opt/dotnet@8/libexec/dotnet", # Intel Mac
2671
+ ]
2672
+ ilspycmd_dll = Path.home() / ".dotnet/tools/.store/ilspycmd"
2673
+ if ilspycmd_dll.exists():
2674
+ # Find the ilspycmd.dll in the store
2675
+ for dll_path in ilspycmd_dll.glob("*/ilspycmd/*/tools/net8.0/any/ilspycmd.dll"):
2676
+ for dotnet8 in dotnet8_paths:
2677
+ if Path(dotnet8).exists():
2678
+ try:
2679
+ env = os.environ.copy()
2680
+ env["DOTNET_ROOT"] = str(Path(dotnet8).parent)
2681
+ result = subprocess.run(
2682
+ [dotnet8, str(dll_path), "-il", dll_file],
2683
+ capture_output=True,
2684
+ text=True,
2685
+ env=env,
2686
+ )
2687
+ if result.returncode == 0:
2688
+ return True, result.stdout
2689
+ except FileNotFoundError:
2690
+ pass
2691
+ break # Only try the first matching dll
2692
+
2693
+ # Try monodis (available on Linux/macOS with Mono)
2694
+ try:
2695
+ result = subprocess.run(
2696
+ ["monodis", "--method", dll_file],
2697
+ capture_output=True,
2698
+ text=True,
2699
+ )
2700
+ if result.returncode == 0:
2701
+ return True, result.stdout
2702
+ except FileNotFoundError:
2703
+ pass # monodis not available
2704
+
2705
+ # If nothing works, return helpful error
2706
+ return False, (
2707
+ "IL disassembly tools not available. Install ILSpy CLI: "
2708
+ "`dotnet tool install -g ilspycmd`"
2709
+ )
2710
+
2711
+ def _parse_il_output(
2712
+ self,
2713
+ output: str,
2714
+ source_file: str,
2715
+ include_warnings: bool = False,
2716
+ function_filter: str | None = None,
2717
+ ) -> tuple[list[dict], list[Violation]]:
2718
+ """
2719
+ Parse CIL/IL output for dangerous operations.
2720
+
2721
+ IL output format from ILSpy spans multiple lines:
2722
+ .method public hidebysig static
2723
+ int32 VulnerableModReduce (
2724
+ int32 'value',
2725
+ int32 modulus
2726
+ ) cil managed
2727
+ {
2728
+ .maxstack 2
2729
+ IL_0000: ldarg.1
2730
+ IL_0001: ldarg.2
2731
+ IL_0002: div
2732
+ IL_0003: ret
2733
+ }
2734
+ """
2735
+ functions = []
2736
+ violations = []
2737
+
2738
+ current_method = None
2739
+ in_method = False
2740
+ in_method_decl = False # Between .method and {
2741
+ filter_pattern = re.compile(function_filter) if function_filter else None
2742
+
2743
+ for line in output.split("\n"):
2744
+ line_stripped = line.strip()
2745
+
2746
+ # Detect start of method declaration
2747
+ if line_stripped.startswith(".method "):
2748
+ in_method_decl = True
2749
+ current_method = None
2750
+ # Try to extract method name from this line (single-line format)
2751
+ name_match = re.search(r"(\w+)\s*\(", line_stripped)
2752
+ if name_match:
2753
+ current_method = name_match.group(1)
2754
+ # Check if brace is on same line (rare but possible)
2755
+ if "{" in line_stripped:
2756
+ in_method_decl = False
2757
+ in_method = True
2758
+ if current_method:
2759
+ functions.append({"name": current_method, "instructions": 0})
2760
+ continue
2761
+
2762
+ # During method declaration, look for method name (before opening paren)
2763
+ if in_method_decl and not in_method:
2764
+ # Look for pattern: name ( - the method name is right before (
2765
+ name_match = re.search(r"(\w+)\s*\(", line_stripped)
2766
+ if name_match and current_method is None:
2767
+ current_method = name_match.group(1)
2768
+
2769
+ # Opening brace starts method body
2770
+ if "{" in line_stripped:
2771
+ in_method_decl = False
2772
+ in_method = True
2773
+ if current_method:
2774
+ functions.append({"name": current_method, "instructions": 0})
2775
+ continue
2776
+
2777
+ # Detect end of method
2778
+ if line_stripped.startswith("}") and in_method:
2779
+ in_method = False
2780
+ continue
2781
+
2782
+ if not in_method:
2783
+ continue
2784
+
2785
+ # Parse IL instruction
2786
+ # Format: IL_xxxx: instruction [operands]
2787
+ il_match = re.match(r"IL_([0-9a-fA-F]+):\s+(\S+)\s*(.*)", line_stripped)
2788
+
2789
+ if not il_match:
2790
+ continue
2791
+
2792
+ offset = il_match.group(1)
2793
+ instruction = il_match.group(2).strip()
2794
+ operands = il_match.group(3).strip() if il_match.group(3) else ""
2795
+
2796
+ # Update instruction count
2797
+ if functions:
2798
+ functions[-1]["instructions"] += 1
2799
+
2800
+ # Check if we should skip this method
2801
+ if filter_pattern and current_method:
2802
+ if not filter_pattern.search(current_method):
2803
+ continue
2804
+
2805
+ instruction_lower = instruction.lower()
2806
+
2807
+ # Check for dangerous bytecodes
2808
+ if instruction_lower in DANGEROUS_CSHARP_BYTECODES["errors"]:
2809
+ violations.append(
2810
+ Violation(
2811
+ function=current_method or "<unknown>",
2812
+ file=source_file,
2813
+ line=None,
2814
+ address=f"IL_{offset}",
2815
+ instruction=f"{instruction} {operands}".strip(),
2816
+ mnemonic=instruction.upper(),
2817
+ reason=DANGEROUS_CSHARP_BYTECODES["errors"][instruction_lower],
2818
+ severity=Severity.ERROR,
2819
+ )
2820
+ )
2821
+ elif include_warnings and instruction_lower in DANGEROUS_CSHARP_BYTECODES["warnings"]:
2822
+ violations.append(
2823
+ Violation(
2824
+ function=current_method or "<unknown>",
2825
+ file=source_file,
2826
+ line=None,
2827
+ address=f"IL_{offset}",
2828
+ instruction=f"{instruction} {operands}".strip(),
2829
+ mnemonic=instruction.upper(),
2830
+ reason=DANGEROUS_CSHARP_BYTECODES["warnings"][instruction_lower],
2831
+ severity=Severity.WARNING,
2832
+ )
2833
+ )
2834
+
2835
+ return functions, violations
2836
+
2837
+ def _detect_dangerous_function_calls(
2838
+ self,
2839
+ source_file: str,
2840
+ include_warnings: bool = False,
2841
+ ) -> list[Violation]:
2842
+ """Detect dangerous function calls via static analysis of source."""
2843
+ violations = []
2844
+
2845
+ try:
2846
+ with open(source_file) as f:
2847
+ source = f.read()
2848
+ except OSError:
2849
+ return violations
2850
+
2851
+ # Detect dangerous function calls
2852
+ for func_name, reason in DANGEROUS_CSHARP_FUNCTIONS["errors"].items():
2853
+ if func_name == "system.random":
2854
+ pattern = r"\bnew\s+Random\s*\("
2855
+ elif func_name == "math.sqrt":
2856
+ pattern = r"\bMath\.Sqrt\s*\("
2857
+ elif func_name == "math.pow":
2858
+ pattern = r"\bMath\.Pow\s*\("
2859
+ else:
2860
+ continue
2861
+ for match in re.finditer(pattern, source):
2862
+ line_num = source[: match.start()].count("\n") + 1
2863
+ violations.append(
2864
+ Violation(
2865
+ function="<source>",
2866
+ file=source_file,
2867
+ line=line_num,
2868
+ address="",
2869
+ instruction=match.group(0),
2870
+ mnemonic=func_name.upper().replace(".", "_"),
2871
+ reason=reason,
2872
+ severity=Severity.ERROR,
2873
+ )
2874
+ )
2875
+
2876
+ if include_warnings:
2877
+ for func_name, reason in DANGEROUS_CSHARP_FUNCTIONS["warnings"].items():
2878
+ if func_name == "sequenceequal":
2879
+ pattern = r"\.SequenceEqual\s*\("
2880
+ elif func_name == "string.equals":
2881
+ pattern = r"\.Equals\s*\("
2882
+ elif func_name == "string.compare":
2883
+ pattern = r"String\.Compare\s*\("
2884
+ else:
2885
+ continue
2886
+ for match in re.finditer(pattern, source):
2887
+ line_num = source[: match.start()].count("\n") + 1
2888
+ violations.append(
2889
+ Violation(
2890
+ function="<source>",
2891
+ file=source_file,
2892
+ line=line_num,
2893
+ address="",
2894
+ instruction=match.group(0),
2895
+ mnemonic=func_name.upper().replace(".", "_"),
2896
+ reason=reason,
2897
+ severity=Severity.WARNING,
2898
+ )
2899
+ )
2900
+
2901
+ return violations
2902
+
2903
+ def _analyze_source_only(
2904
+ self,
2905
+ source_file: str,
2906
+ include_warnings: bool = False,
2907
+ ) -> AnalysisReport:
2908
+ """Fallback analysis using source-level pattern matching only."""
2909
+ violations = self._detect_dangerous_function_calls(source_file, include_warnings)
2910
+
2911
+ # Also detect division/modulo operators in source
2912
+ try:
2913
+ with open(source_file) as f:
2914
+ source = f.read()
2915
+ except OSError:
2916
+ source = ""
2917
+
2918
+ # Detect division operator
2919
+ div_pattern = r"[^/]\s*/\s*[^/=*]"
2920
+ for match in re.finditer(div_pattern, source):
2921
+ line_start = source.rfind("\n", 0, match.start()) + 1
2922
+ line_end = source.find("\n", match.start())
2923
+ if line_end == -1:
2924
+ line_end = len(source)
2925
+ line = source[line_start:line_end]
2926
+ if line.strip().startswith("//"):
2927
+ continue
2928
+ line_num = source[: match.start()].count("\n") + 1
2929
+ violations.append(
2930
+ Violation(
2931
+ function="<source>",
2932
+ file=source_file,
2933
+ line=line_num,
2934
+ address="",
2935
+ instruction="/",
2936
+ mnemonic="DIV_OP",
2937
+ reason="Division operator may have variable-time execution",
2938
+ severity=Severity.ERROR,
2939
+ )
2940
+ )
2941
+
2942
+ # Detect modulo operator
2943
+ mod_pattern = r"\s%\s*[^=]"
2944
+ for match in re.finditer(mod_pattern, source):
2945
+ line_start = source.rfind("\n", 0, match.start()) + 1
2946
+ line_end = source.find("\n", match.start())
2947
+ if line_end == -1:
2948
+ line_end = len(source)
2949
+ line = source[line_start:line_end]
2950
+ if line.strip().startswith("//"):
2951
+ continue
2952
+ line_num = source[: match.start()].count("\n") + 1
2953
+ violations.append(
2954
+ Violation(
2955
+ function="<source>",
2956
+ file=source_file,
2957
+ line=line_num,
2958
+ address="",
2959
+ instruction="%",
2960
+ mnemonic="REM_OP",
2961
+ reason="Modulo operator may have variable-time execution",
2962
+ severity=Severity.ERROR,
2963
+ )
2964
+ )
2965
+
2966
+ return AnalysisReport(
2967
+ architecture="cil",
2968
+ compiler="source-analysis",
2969
+ optimization="default",
2970
+ source_file=str(source_file),
2971
+ total_functions=0,
2972
+ total_instructions=0,
2973
+ violations=violations,
2974
+ )
2975
+
2976
+ def analyze(
2977
+ self,
2978
+ source_file: str,
2979
+ include_warnings: bool = False,
2980
+ function_filter: str | None = None,
2981
+ ) -> AnalysisReport:
2982
+ """Analyze a C# file for constant-time violations."""
2983
+ source_path = Path(source_file)
2984
+ if not source_path.exists():
2985
+ raise FileNotFoundError(f"Source file not found: {source_file}")
2986
+
2987
+ with tempfile.TemporaryDirectory() as tmpdir:
2988
+ # Try to compile C# source
2989
+ success, result = self._compile_csharp(str(source_path.absolute()), tmpdir)
2990
+ if not success:
2991
+ # Fall back to source-only analysis
2992
+ print(
2993
+ f"Note: C# compilation failed ({result}), using source analysis only",
2994
+ file=sys.stderr,
2995
+ )
2996
+ return self._analyze_source_only(source_file, include_warnings)
2997
+
2998
+ # Get IL disassembly
2999
+ success, output = self._get_il_output(result)
3000
+ if not success:
3001
+ # Fall back to source-only analysis
3002
+ print(
3003
+ f"Note: IL disassembly failed ({output}), using source analysis only",
3004
+ file=sys.stderr,
3005
+ )
3006
+ return self._analyze_source_only(source_file, include_warnings)
3007
+
3008
+ functions, violations = self._parse_il_output(
3009
+ output,
3010
+ source_file,
3011
+ include_warnings,
3012
+ function_filter,
3013
+ )
3014
+
3015
+ # Also check for dangerous function calls in source
3016
+ source_violations = self._detect_dangerous_function_calls(
3017
+ source_file,
3018
+ include_warnings,
3019
+ )
3020
+
3021
+ # Merge violations, avoiding duplicates
3022
+ existing = {(v.line, v.mnemonic) for v in violations}
3023
+ for v in source_violations:
3024
+ if (v.line, v.mnemonic) not in existing:
3025
+ violations.append(v)
3026
+
3027
+ return AnalysisReport(
3028
+ architecture="cil",
3029
+ compiler="dotnet",
3030
+ optimization="Release",
3031
+ source_file=str(source_file),
3032
+ total_functions=len(functions),
3033
+ total_instructions=sum(f["instructions"] for f in functions),
3034
+ violations=violations,
3035
+ )
3036
+
3037
+
3038
+ # =============================================================================
3039
+ # Helper Functions
3040
+ # =============================================================================
3041
+
3042
+
3043
+ def get_script_analyzer(language: str) -> ScriptAnalyzer | None:
3044
+ """
3045
+ Get the appropriate analyzer for a bytecode-analyzed language.
3046
+
3047
+ Args:
3048
+ language: The language identifier
3049
+
3050
+ Returns:
3051
+ ScriptAnalyzer instance or None if not supported
3052
+ """
3053
+ analyzers = {
3054
+ "php": PHPAnalyzer,
3055
+ "javascript": JavaScriptAnalyzer,
3056
+ "typescript": JavaScriptAnalyzer,
3057
+ "python": PythonAnalyzer,
3058
+ "ruby": RubyAnalyzer,
3059
+ "java": JavaAnalyzer,
3060
+ "kotlin": KotlinAnalyzer,
3061
+ "csharp": CSharpAnalyzer,
3062
+ }
3063
+
3064
+ analyzer_class = analyzers.get(language.lower())
3065
+ if analyzer_class:
3066
+ return analyzer_class()
3067
+ return None
3068
+
3069
+
3070
+ def is_script_language(language: str) -> bool:
3071
+ """Check if a language is handled by bytecode analysis in this module."""
3072
+ return language.lower() in (
3073
+ "php",
3074
+ "javascript",
3075
+ "typescript",
3076
+ "python",
3077
+ "ruby",
3078
+ "java",
3079
+ "kotlin",
3080
+ "csharp",
3081
+ )