@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.
- package/README.md +126 -0
- package/package.json +53 -0
- package/skills/1password/SKILL.md +70 -0
- package/skills/1password/references/cli-examples.md +29 -0
- package/skills/1password/references/get-started.md +17 -0
- package/skills/apple-notes/SKILL.md +77 -0
- package/skills/apple-reminders/SKILL.md +96 -0
- package/skills/bear-notes/SKILL.md +107 -0
- package/skills/bird/SKILL.md +224 -0
- package/skills/blogwatcher/SKILL.md +69 -0
- package/skills/blucli/SKILL.md +47 -0
- package/skills/bluebubbles/SKILL.md +131 -0
- package/skills/camsnap/SKILL.md +45 -0
- package/skills/canvas/SKILL.md +203 -0
- package/skills/clawhub/SKILL.md +77 -0
- package/skills/coding-agent/SKILL.md +284 -0
- package/skills/discord/SKILL.md +578 -0
- package/skills/eightctl/SKILL.md +50 -0
- package/skills/food-order/SKILL.md +48 -0
- package/skills/gemini/SKILL.md +43 -0
- package/skills/gifgrep/SKILL.md +79 -0
- package/skills/github/SKILL.md +77 -0
- package/skills/gog/SKILL.md +116 -0
- package/skills/goplaces/SKILL.md +52 -0
- package/skills/healthcheck/SKILL.md +245 -0
- package/skills/himalaya/SKILL.md +257 -0
- package/skills/himalaya/references/configuration.md +184 -0
- package/skills/himalaya/references/message-composition.md +199 -0
- package/skills/imsg/SKILL.md +74 -0
- package/skills/local-places/SERVER_README.md +101 -0
- package/skills/local-places/SKILL.md +102 -0
- package/skills/local-places/pyproject.toml +21 -0
- package/skills/local-places/src/local_places/__init__.py +2 -0
- package/skills/local-places/src/local_places/google_places.py +314 -0
- package/skills/local-places/src/local_places/main.py +65 -0
- package/skills/local-places/src/local_places/schemas.py +107 -0
- package/skills/mcporter/SKILL.md +61 -0
- package/skills/model-usage/SKILL.md +69 -0
- package/skills/model-usage/references/codexbar-cli.md +33 -0
- package/skills/model-usage/scripts/model_usage.py +310 -0
- package/skills/nano-banana-pro/SKILL.md +58 -0
- package/skills/nano-banana-pro/scripts/generate_image.py +184 -0
- package/skills/nano-pdf/SKILL.md +38 -0
- package/skills/notion/SKILL.md +172 -0
- package/skills/obsidian/SKILL.md +81 -0
- package/skills/openai-image-gen/SKILL.md +89 -0
- package/skills/openai-image-gen/scripts/gen.py +240 -0
- package/skills/openai-whisper/SKILL.md +38 -0
- package/skills/openai-whisper-api/SKILL.md +52 -0
- package/skills/openai-whisper-api/scripts/transcribe.sh +85 -0
- package/skills/openhue/SKILL.md +51 -0
- package/skills/oracle/SKILL.md +125 -0
- package/skills/ordercli/SKILL.md +78 -0
- package/skills/peekaboo/SKILL.md +190 -0
- package/skills/sag/SKILL.md +87 -0
- package/skills/security-ask-questions-if-underspecified/.claude-plugin/plugin.json +10 -0
- package/skills/security-ask-questions-if-underspecified/README.md +24 -0
- package/skills/security-ask-questions-if-underspecified/skills/ask-questions-if-underspecified/SKILL.md +85 -0
- package/skills/security-audit-context-building/.claude-plugin/plugin.json +10 -0
- package/skills/security-audit-context-building/README.md +58 -0
- package/skills/security-audit-context-building/commands/audit-context.md +21 -0
- package/skills/security-audit-context-building/skills/audit-context-building/SKILL.md +297 -0
- package/skills/security-audit-context-building/skills/audit-context-building/resources/COMPLETENESS_CHECKLIST.md +47 -0
- package/skills/security-audit-context-building/skills/audit-context-building/resources/FUNCTION_MICRO_ANALYSIS_EXAMPLE.md +355 -0
- package/skills/security-audit-context-building/skills/audit-context-building/resources/OUTPUT_REQUIREMENTS.md +71 -0
- package/skills/security-building-secure-contracts/.claude-plugin/plugin.json +10 -0
- package/skills/security-building-secure-contracts/README.md +241 -0
- package/skills/security-building-secure-contracts/skills/algorand-vulnerability-scanner/SKILL.md +284 -0
- package/skills/security-building-secure-contracts/skills/algorand-vulnerability-scanner/resources/VULNERABILITY_PATTERNS.md +405 -0
- package/skills/security-building-secure-contracts/skills/audit-prep-assistant/SKILL.md +409 -0
- package/skills/security-building-secure-contracts/skills/cairo-vulnerability-scanner/SKILL.md +329 -0
- package/skills/security-building-secure-contracts/skills/cairo-vulnerability-scanner/resources/VULNERABILITY_PATTERNS.md +722 -0
- package/skills/security-building-secure-contracts/skills/code-maturity-assessor/SKILL.md +218 -0
- package/skills/security-building-secure-contracts/skills/code-maturity-assessor/resources/ASSESSMENT_CRITERIA.md +355 -0
- package/skills/security-building-secure-contracts/skills/code-maturity-assessor/resources/EXAMPLE_REPORT.md +248 -0
- package/skills/security-building-secure-contracts/skills/code-maturity-assessor/resources/REPORT_FORMAT.md +33 -0
- package/skills/security-building-secure-contracts/skills/cosmos-vulnerability-scanner/SKILL.md +334 -0
- package/skills/security-building-secure-contracts/skills/cosmos-vulnerability-scanner/resources/VULNERABILITY_PATTERNS.md +740 -0
- package/skills/security-building-secure-contracts/skills/guidelines-advisor/SKILL.md +252 -0
- package/skills/security-building-secure-contracts/skills/guidelines-advisor/resources/ASSESSMENT_AREAS.md +329 -0
- package/skills/security-building-secure-contracts/skills/guidelines-advisor/resources/DELIVERABLES.md +118 -0
- package/skills/security-building-secure-contracts/skills/guidelines-advisor/resources/EXAMPLE_REPORT.md +298 -0
- package/skills/security-building-secure-contracts/skills/secure-workflow-guide/SKILL.md +161 -0
- package/skills/security-building-secure-contracts/skills/secure-workflow-guide/resources/EXAMPLE_REPORT.md +279 -0
- package/skills/security-building-secure-contracts/skills/secure-workflow-guide/resources/WORKFLOW_STEPS.md +132 -0
- package/skills/security-building-secure-contracts/skills/solana-vulnerability-scanner/SKILL.md +389 -0
- package/skills/security-building-secure-contracts/skills/solana-vulnerability-scanner/resources/VULNERABILITY_PATTERNS.md +669 -0
- package/skills/security-building-secure-contracts/skills/substrate-vulnerability-scanner/SKILL.md +298 -0
- package/skills/security-building-secure-contracts/skills/substrate-vulnerability-scanner/resources/VULNERABILITY_PATTERNS.md +791 -0
- package/skills/security-building-secure-contracts/skills/token-integration-analyzer/SKILL.md +362 -0
- package/skills/security-building-secure-contracts/skills/token-integration-analyzer/resources/ASSESSMENT_CATEGORIES.md +571 -0
- package/skills/security-building-secure-contracts/skills/token-integration-analyzer/resources/REPORT_TEMPLATES.md +141 -0
- package/skills/security-building-secure-contracts/skills/ton-vulnerability-scanner/SKILL.md +388 -0
- package/skills/security-building-secure-contracts/skills/ton-vulnerability-scanner/resources/VULNERABILITY_PATTERNS.md +595 -0
- package/skills/security-burpsuite-project-parser/.claude-plugin/plugin.json +10 -0
- package/skills/security-burpsuite-project-parser/README.md +103 -0
- package/skills/security-burpsuite-project-parser/commands/burp-search.md +18 -0
- package/skills/security-burpsuite-project-parser/skills/SKILL.md +358 -0
- package/skills/security-burpsuite-project-parser/skills/scripts/burp-search.sh +99 -0
- package/skills/security-claude-in-chrome-troubleshooting/.claude-plugin/plugin.json +8 -0
- package/skills/security-claude-in-chrome-troubleshooting/README.md +31 -0
- package/skills/security-claude-in-chrome-troubleshooting/skills/claude-in-chrome-troubleshooting/SKILL.md +251 -0
- package/skills/security-constant-time-analysis/.claude-plugin/plugin.json +9 -0
- package/skills/security-constant-time-analysis/README.md +381 -0
- package/skills/security-constant-time-analysis/commands/ct-check.md +20 -0
- package/skills/security-constant-time-analysis/ct_analyzer/__init__.py +49 -0
- package/skills/security-constant-time-analysis/ct_analyzer/analyzer.py +1284 -0
- package/skills/security-constant-time-analysis/ct_analyzer/script_analyzers.py +3081 -0
- package/skills/security-constant-time-analysis/ct_analyzer/tests/__init__.py +1 -0
- package/skills/security-constant-time-analysis/ct_analyzer/tests/test_analyzer.py +1397 -0
- package/skills/security-constant-time-analysis/ct_analyzer/tests/test_samples/bn_excerpt.js +205 -0
- package/skills/security-constant-time-analysis/ct_analyzer/tests/test_samples/decompose_constant_time.c +181 -0
- package/skills/security-constant-time-analysis/ct_analyzer/tests/test_samples/decompose_vulnerable.c +74 -0
- package/skills/security-constant-time-analysis/ct_analyzer/tests/test_samples/decompose_vulnerable.go +78 -0
- package/skills/security-constant-time-analysis/ct_analyzer/tests/test_samples/decompose_vulnerable.rs +92 -0
- package/skills/security-constant-time-analysis/ct_analyzer/tests/test_samples/vulnerable.cs +174 -0
- package/skills/security-constant-time-analysis/ct_analyzer/tests/test_samples/vulnerable.java +161 -0
- package/skills/security-constant-time-analysis/ct_analyzer/tests/test_samples/vulnerable.kt +181 -0
- package/skills/security-constant-time-analysis/ct_analyzer/tests/test_samples/vulnerable.php +140 -0
- package/skills/security-constant-time-analysis/ct_analyzer/tests/test_samples/vulnerable.py +252 -0
- package/skills/security-constant-time-analysis/ct_analyzer/tests/test_samples/vulnerable.rb +188 -0
- package/skills/security-constant-time-analysis/ct_analyzer/tests/test_samples/vulnerable.swift +199 -0
- package/skills/security-constant-time-analysis/ct_analyzer/tests/test_samples/vulnerable.ts +154 -0
- package/skills/security-constant-time-analysis/pyproject.toml +52 -0
- package/skills/security-constant-time-analysis/skills/constant-time-analysis/README.md +90 -0
- package/skills/security-constant-time-analysis/skills/constant-time-analysis/SKILL.md +219 -0
- package/skills/security-constant-time-analysis/skills/constant-time-analysis/references/compiled.md +129 -0
- package/skills/security-constant-time-analysis/skills/constant-time-analysis/references/javascript.md +136 -0
- package/skills/security-constant-time-analysis/skills/constant-time-analysis/references/kotlin.md +252 -0
- package/skills/security-constant-time-analysis/skills/constant-time-analysis/references/php.md +172 -0
- package/skills/security-constant-time-analysis/skills/constant-time-analysis/references/python.md +179 -0
- package/skills/security-constant-time-analysis/skills/constant-time-analysis/references/ruby.md +198 -0
- package/skills/security-constant-time-analysis/skills/constant-time-analysis/references/swift.md +288 -0
- package/skills/security-constant-time-analysis/skills/constant-time-analysis/references/vm-compiled.md +354 -0
- package/skills/security-constant-time-analysis/uv.lock +8 -0
- package/skills/security-culture-index/.claude-plugin/plugin.json +8 -0
- package/skills/security-culture-index/README.md +79 -0
- package/skills/security-culture-index/skills/interpreting-culture-index/SKILL.md +293 -0
- package/skills/security-culture-index/skills/interpreting-culture-index/references/anti-patterns.md +255 -0
- package/skills/security-culture-index/skills/interpreting-culture-index/references/conversation-starters.md +408 -0
- package/skills/security-culture-index/skills/interpreting-culture-index/references/interview-trait-signals.md +253 -0
- package/skills/security-culture-index/skills/interpreting-culture-index/references/motivators.md +158 -0
- package/skills/security-culture-index/skills/interpreting-culture-index/references/patterns-archetypes.md +147 -0
- package/skills/security-culture-index/skills/interpreting-culture-index/references/primary-traits.md +307 -0
- package/skills/security-culture-index/skills/interpreting-culture-index/references/secondary-traits.md +228 -0
- package/skills/security-culture-index/skills/interpreting-culture-index/references/team-composition.md +148 -0
- package/skills/security-culture-index/skills/interpreting-culture-index/scripts/check_deps.py +108 -0
- package/skills/security-culture-index/skills/interpreting-culture-index/scripts/culture_index/__init__.py +20 -0
- package/skills/security-culture-index/skills/interpreting-culture-index/scripts/culture_index/constants.py +122 -0
- package/skills/security-culture-index/skills/interpreting-culture-index/scripts/culture_index/extract.py +187 -0
- package/skills/security-culture-index/skills/interpreting-culture-index/scripts/culture_index/models.py +16 -0
- package/skills/security-culture-index/skills/interpreting-culture-index/scripts/culture_index/opencv_extractor.py +520 -0
- package/skills/security-culture-index/skills/interpreting-culture-index/scripts/extract_pdf.py +237 -0
- package/skills/security-culture-index/skills/interpreting-culture-index/scripts/pyproject.toml +18 -0
- package/skills/security-culture-index/skills/interpreting-culture-index/templates/burnout-report.md +113 -0
- package/skills/security-culture-index/skills/interpreting-culture-index/templates/comparison-report.md +103 -0
- package/skills/security-culture-index/skills/interpreting-culture-index/templates/hiring-profile.md +127 -0
- package/skills/security-culture-index/skills/interpreting-culture-index/templates/individual-report.md +85 -0
- package/skills/security-culture-index/skills/interpreting-culture-index/templates/predicted-profile.md +165 -0
- package/skills/security-culture-index/skills/interpreting-culture-index/templates/team-report.md +109 -0
- package/skills/security-culture-index/skills/interpreting-culture-index/workflows/analyze-team.md +188 -0
- package/skills/security-culture-index/skills/interpreting-culture-index/workflows/coach-manager.md +267 -0
- package/skills/security-culture-index/skills/interpreting-culture-index/workflows/compare-profiles.md +188 -0
- package/skills/security-culture-index/skills/interpreting-culture-index/workflows/define-hiring-profile.md +220 -0
- package/skills/security-culture-index/skills/interpreting-culture-index/workflows/detect-burnout.md +206 -0
- package/skills/security-culture-index/skills/interpreting-culture-index/workflows/extract-from-pdf.md +121 -0
- package/skills/security-culture-index/skills/interpreting-culture-index/workflows/interpret-individual.md +183 -0
- package/skills/security-culture-index/skills/interpreting-culture-index/workflows/interview-debrief.md +234 -0
- package/skills/security-culture-index/skills/interpreting-culture-index/workflows/mediate-conflict.md +306 -0
- package/skills/security-culture-index/skills/interpreting-culture-index/workflows/plan-onboarding.md +322 -0
- package/skills/security-culture-index/skills/interpreting-culture-index/workflows/predict-from-interview.md +250 -0
- package/skills/security-differential-review/.claude-plugin/plugin.json +10 -0
- package/skills/security-differential-review/README.md +109 -0
- package/skills/security-differential-review/commands/diff-review.md +21 -0
- package/skills/security-differential-review/skills/differential-review/SKILL.md +220 -0
- package/skills/security-differential-review/skills/differential-review/adversarial.md +203 -0
- package/skills/security-differential-review/skills/differential-review/methodology.md +234 -0
- package/skills/security-differential-review/skills/differential-review/patterns.md +300 -0
- package/skills/security-differential-review/skills/differential-review/reporting.md +369 -0
- package/skills/security-dwarf-expert/.claude-plugin/plugin.json +10 -0
- package/skills/security-dwarf-expert/README.md +38 -0
- package/skills/security-dwarf-expert/skills/dwarf-expert/SKILL.md +93 -0
- package/skills/security-dwarf-expert/skills/dwarf-expert/reference/coding.md +31 -0
- package/skills/security-dwarf-expert/skills/dwarf-expert/reference/dwarfdump.md +50 -0
- package/skills/security-dwarf-expert/skills/dwarf-expert/reference/readelf.md +8 -0
- package/skills/security-entry-point-analyzer/.claude-plugin/plugin.json +10 -0
- package/skills/security-entry-point-analyzer/README.md +74 -0
- package/skills/security-entry-point-analyzer/commands/entry-points.md +18 -0
- package/skills/security-entry-point-analyzer/skills/entry-point-analyzer/SKILL.md +251 -0
- package/skills/security-entry-point-analyzer/skills/entry-point-analyzer/references/cosmwasm.md +182 -0
- package/skills/security-entry-point-analyzer/skills/entry-point-analyzer/references/move-aptos.md +107 -0
- package/skills/security-entry-point-analyzer/skills/entry-point-analyzer/references/move-sui.md +87 -0
- package/skills/security-entry-point-analyzer/skills/entry-point-analyzer/references/solana.md +155 -0
- package/skills/security-entry-point-analyzer/skills/entry-point-analyzer/references/solidity.md +135 -0
- package/skills/security-entry-point-analyzer/skills/entry-point-analyzer/references/ton.md +185 -0
- package/skills/security-entry-point-analyzer/skills/entry-point-analyzer/references/vyper.md +141 -0
- package/skills/security-firebase-apk-scanner/.claude-plugin/plugin.json +10 -0
- package/skills/security-firebase-apk-scanner/README.md +85 -0
- package/skills/security-firebase-apk-scanner/commands/scan-apk.md +18 -0
- package/skills/security-firebase-apk-scanner/scanner.sh +1408 -0
- package/skills/security-firebase-apk-scanner/skills/firebase-apk-scanner/SKILL.md +197 -0
- package/skills/security-firebase-apk-scanner/skills/firebase-apk-scanner/references/vulnerabilities.md +803 -0
- package/skills/security-fix-review/.claude-plugin/plugin.json +13 -0
- package/skills/security-fix-review/README.md +118 -0
- package/skills/security-fix-review/commands/fix-review.md +24 -0
- package/skills/security-fix-review/skills/fix-review/SKILL.md +264 -0
- package/skills/security-fix-review/skills/fix-review/references/bug-detection.md +408 -0
- package/skills/security-fix-review/skills/fix-review/references/finding-matching.md +298 -0
- package/skills/security-fix-review/skills/fix-review/references/report-parsing.md +398 -0
- package/skills/security-insecure-defaults/.claude-plugin/plugin.json +10 -0
- package/skills/security-insecure-defaults/README.md +45 -0
- package/skills/security-insecure-defaults/skills/insecure-defaults/SKILL.md +117 -0
- package/skills/security-insecure-defaults/skills/insecure-defaults/references/examples.md +409 -0
- package/skills/security-modern-python/.claude-plugin/plugin.json +10 -0
- package/skills/security-modern-python/README.md +58 -0
- package/skills/security-modern-python/hooks/hooks.json +16 -0
- package/skills/security-modern-python/hooks/intercept-legacy-python.bats +388 -0
- package/skills/security-modern-python/hooks/intercept-legacy-python.sh +109 -0
- package/skills/security-modern-python/hooks/test_helper.bash +75 -0
- package/skills/security-modern-python/skills/modern-python/SKILL.md +333 -0
- package/skills/security-modern-python/skills/modern-python/references/dependabot.md +43 -0
- package/skills/security-modern-python/skills/modern-python/references/migration-checklist.md +141 -0
- package/skills/security-modern-python/skills/modern-python/references/pep723-scripts.md +259 -0
- package/skills/security-modern-python/skills/modern-python/references/prek.md +211 -0
- package/skills/security-modern-python/skills/modern-python/references/pyproject.md +254 -0
- package/skills/security-modern-python/skills/modern-python/references/ruff-config.md +240 -0
- package/skills/security-modern-python/skills/modern-python/references/security-setup.md +255 -0
- package/skills/security-modern-python/skills/modern-python/references/testing.md +284 -0
- package/skills/security-modern-python/skills/modern-python/references/uv-commands.md +200 -0
- package/skills/security-modern-python/skills/modern-python/templates/dependabot.yml +36 -0
- package/skills/security-modern-python/skills/modern-python/templates/pre-commit-config.yaml +66 -0
- package/skills/security-property-based-testing/.claude-plugin/plugin.json +9 -0
- package/skills/security-property-based-testing/README.md +47 -0
- package/skills/security-property-based-testing/skills/property-based-testing/README.md +88 -0
- package/skills/security-property-based-testing/skills/property-based-testing/SKILL.md +109 -0
- package/skills/security-property-based-testing/skills/property-based-testing/references/design.md +191 -0
- package/skills/security-property-based-testing/skills/property-based-testing/references/generating.md +200 -0
- package/skills/security-property-based-testing/skills/property-based-testing/references/libraries.md +130 -0
- package/skills/security-property-based-testing/skills/property-based-testing/references/refactoring.md +181 -0
- package/skills/security-property-based-testing/skills/property-based-testing/references/reviewing.md +209 -0
- package/skills/security-property-based-testing/skills/property-based-testing/references/strategies.md +124 -0
- package/skills/semgrep-rule-creator/.claude-plugin/plugin.json +8 -0
- package/skills/semgrep-rule-creator/README.md +43 -0
- package/skills/semgrep-rule-creator/commands/semgrep-rule.md +26 -0
- package/skills/semgrep-rule-creator/skills/semgrep-rule-creator/SKILL.md +168 -0
- package/skills/semgrep-rule-creator/skills/semgrep-rule-creator/references/quick-reference.md +203 -0
- package/skills/semgrep-rule-creator/skills/semgrep-rule-creator/references/workflow.md +240 -0
- package/skills/semgrep-rule-variant-creator/.claude-plugin/plugin.json +9 -0
- package/skills/semgrep-rule-variant-creator/README.md +86 -0
- package/skills/semgrep-rule-variant-creator/skills/semgrep-rule-variant-creator/SKILL.md +205 -0
- package/skills/semgrep-rule-variant-creator/skills/semgrep-rule-variant-creator/references/applicability-analysis.md +250 -0
- package/skills/semgrep-rule-variant-creator/skills/semgrep-rule-variant-creator/references/language-syntax-guide.md +324 -0
- package/skills/semgrep-rule-variant-creator/skills/semgrep-rule-variant-creator/references/workflow.md +518 -0
- package/skills/session-logs/SKILL.md +115 -0
- package/skills/sharp-edges/.claude-plugin/plugin.json +10 -0
- package/skills/sharp-edges/README.md +48 -0
- package/skills/sharp-edges/skills/sharp-edges/SKILL.md +292 -0
- package/skills/sharp-edges/skills/sharp-edges/references/auth-patterns.md +252 -0
- package/skills/sharp-edges/skills/sharp-edges/references/case-studies.md +274 -0
- package/skills/sharp-edges/skills/sharp-edges/references/config-patterns.md +333 -0
- package/skills/sharp-edges/skills/sharp-edges/references/crypto-apis.md +190 -0
- package/skills/sharp-edges/skills/sharp-edges/references/lang-c.md +205 -0
- package/skills/sharp-edges/skills/sharp-edges/references/lang-csharp.md +285 -0
- package/skills/sharp-edges/skills/sharp-edges/references/lang-go.md +270 -0
- package/skills/sharp-edges/skills/sharp-edges/references/lang-java.md +263 -0
- package/skills/sharp-edges/skills/sharp-edges/references/lang-javascript.md +269 -0
- package/skills/sharp-edges/skills/sharp-edges/references/lang-kotlin.md +265 -0
- package/skills/sharp-edges/skills/sharp-edges/references/lang-php.md +245 -0
- package/skills/sharp-edges/skills/sharp-edges/references/lang-python.md +274 -0
- package/skills/sharp-edges/skills/sharp-edges/references/lang-ruby.md +273 -0
- package/skills/sharp-edges/skills/sharp-edges/references/lang-rust.md +272 -0
- package/skills/sharp-edges/skills/sharp-edges/references/lang-swift.md +287 -0
- package/skills/sharp-edges/skills/sharp-edges/references/language-specific.md +588 -0
- package/skills/sherpa-onnx-tts/SKILL.md +103 -0
- package/skills/sherpa-onnx-tts/bin/sherpa-onnx-tts +178 -0
- package/skills/skill-creator/SKILL.md +370 -0
- package/skills/skill-creator/license.txt +202 -0
- package/skills/skill-creator/scripts/init_skill.py +378 -0
- package/skills/skill-creator/scripts/package_skill.py +111 -0
- package/skills/skill-creator/scripts/quick_validate.py +101 -0
- package/skills/slack/SKILL.md +144 -0
- package/skills/songsee/SKILL.md +49 -0
- package/skills/sonoscli/SKILL.md +46 -0
- package/skills/spec-to-code-compliance/.claude-plugin/plugin.json +10 -0
- package/skills/spec-to-code-compliance/README.md +67 -0
- package/skills/spec-to-code-compliance/commands/spec-compliance.md +22 -0
- package/skills/spec-to-code-compliance/skills/spec-to-code-compliance/SKILL.md +349 -0
- package/skills/spec-to-code-compliance/skills/spec-to-code-compliance/resources/COMPLETENESS_CHECKLIST.md +69 -0
- package/skills/spec-to-code-compliance/skills/spec-to-code-compliance/resources/IR_EXAMPLES.md +417 -0
- package/skills/spec-to-code-compliance/skills/spec-to-code-compliance/resources/OUTPUT_REQUIREMENTS.md +105 -0
- package/skills/spotify-player/SKILL.md +64 -0
- package/skills/static-analysis/.claude-plugin/plugin.json +8 -0
- package/skills/static-analysis/README.md +59 -0
- package/skills/static-analysis/skills/codeql/SKILL.md +315 -0
- package/skills/static-analysis/skills/sarif-parsing/SKILL.md +479 -0
- package/skills/static-analysis/skills/sarif-parsing/resources/jq-queries.md +162 -0
- package/skills/static-analysis/skills/sarif-parsing/resources/sarif_helpers.py +331 -0
- package/skills/static-analysis/skills/semgrep/SKILL.md +337 -0
- package/skills/summarize/SKILL.md +87 -0
- package/skills/testing-handbook-skills/.claude-plugin/plugin.json +8 -0
- package/skills/testing-handbook-skills/README.md +241 -0
- package/skills/testing-handbook-skills/scripts/pyproject.toml +8 -0
- package/skills/testing-handbook-skills/scripts/validate-skills.py +657 -0
- package/skills/testing-handbook-skills/skills/address-sanitizer/SKILL.md +341 -0
- package/skills/testing-handbook-skills/skills/aflpp/SKILL.md +640 -0
- package/skills/testing-handbook-skills/skills/atheris/SKILL.md +515 -0
- package/skills/testing-handbook-skills/skills/cargo-fuzz/SKILL.md +454 -0
- package/skills/testing-handbook-skills/skills/codeql/SKILL.md +549 -0
- package/skills/testing-handbook-skills/skills/constant-time-testing/SKILL.md +507 -0
- package/skills/testing-handbook-skills/skills/coverage-analysis/SKILL.md +607 -0
- package/skills/testing-handbook-skills/skills/fuzzing-dictionary/SKILL.md +297 -0
- package/skills/testing-handbook-skills/skills/fuzzing-obstacles/SKILL.md +426 -0
- package/skills/testing-handbook-skills/skills/harness-writing/SKILL.md +614 -0
- package/skills/testing-handbook-skills/skills/libafl/SKILL.md +625 -0
- package/skills/testing-handbook-skills/skills/libfuzzer/SKILL.md +795 -0
- package/skills/testing-handbook-skills/skills/ossfuzz/SKILL.md +426 -0
- package/skills/testing-handbook-skills/skills/ruzzy/SKILL.md +443 -0
- package/skills/testing-handbook-skills/skills/semgrep/SKILL.md +601 -0
- package/skills/testing-handbook-skills/skills/testing-handbook-generator/SKILL.md +372 -0
- package/skills/testing-handbook-skills/skills/testing-handbook-generator/agent-prompt.md +280 -0
- package/skills/testing-handbook-skills/skills/testing-handbook-generator/discovery.md +452 -0
- package/skills/testing-handbook-skills/skills/testing-handbook-generator/templates/domain-skill.md +504 -0
- package/skills/testing-handbook-skills/skills/testing-handbook-generator/templates/fuzzer-skill.md +454 -0
- package/skills/testing-handbook-skills/skills/testing-handbook-generator/templates/technique-skill.md +527 -0
- package/skills/testing-handbook-skills/skills/testing-handbook-generator/templates/tool-skill.md +366 -0
- package/skills/testing-handbook-skills/skills/testing-handbook-generator/testing.md +482 -0
- package/skills/testing-handbook-skills/skills/wycheproof/SKILL.md +533 -0
- package/skills/things-mac/SKILL.md +86 -0
- package/skills/tmux/SKILL.md +135 -0
- package/skills/tmux/scripts/find-sessions.sh +112 -0
- package/skills/tmux/scripts/wait-for-text.sh +83 -0
- package/skills/trello/SKILL.md +95 -0
- package/skills/variant-analysis/.claude-plugin/plugin.json +8 -0
- package/skills/variant-analysis/README.md +41 -0
- package/skills/variant-analysis/commands/variants.md +23 -0
- package/skills/variant-analysis/skills/variant-analysis/METHODOLOGY.md +327 -0
- package/skills/variant-analysis/skills/variant-analysis/SKILL.md +142 -0
- package/skills/variant-analysis/skills/variant-analysis/resources/codeql/cpp.ql +119 -0
- package/skills/variant-analysis/skills/variant-analysis/resources/codeql/go.ql +69 -0
- package/skills/variant-analysis/skills/variant-analysis/resources/codeql/java.ql +71 -0
- package/skills/variant-analysis/skills/variant-analysis/resources/codeql/javascript.ql +63 -0
- package/skills/variant-analysis/skills/variant-analysis/resources/codeql/python.ql +80 -0
- package/skills/variant-analysis/skills/variant-analysis/resources/semgrep/cpp.yaml +98 -0
- package/skills/variant-analysis/skills/variant-analysis/resources/semgrep/go.yaml +63 -0
- package/skills/variant-analysis/skills/variant-analysis/resources/semgrep/java.yaml +61 -0
- package/skills/variant-analysis/skills/variant-analysis/resources/semgrep/javascript.yaml +60 -0
- package/skills/variant-analysis/skills/variant-analysis/resources/semgrep/python.yaml +72 -0
- package/skills/variant-analysis/skills/variant-analysis/resources/variant-report-template.md +75 -0
- package/skills/video-frames/SKILL.md +46 -0
- package/skills/video-frames/scripts/frame.sh +81 -0
- package/skills/voice-call/SKILL.md +45 -0
- package/skills/wacli/SKILL.md +72 -0
- package/skills/weather/SKILL.md +54 -0
- package/skills/yara-authoring/.claude-plugin/plugin.json +9 -0
- package/skills/yara-authoring/README.md +131 -0
- package/skills/yara-authoring/skills/yara-rule-authoring/SKILL.md +645 -0
- package/skills/yara-authoring/skills/yara-rule-authoring/examples/MAL_Mac_ProtonRAT_Jan25.yar +99 -0
- package/skills/yara-authoring/skills/yara-rule-authoring/examples/MAL_NPM_SupplyChain_Jan25.yar +170 -0
- package/skills/yara-authoring/skills/yara-rule-authoring/examples/MAL_Win_Remcos_Jan25.yar +103 -0
- package/skills/yara-authoring/skills/yara-rule-authoring/examples/SUSP_CRX_SuspiciousPermissions.yar +134 -0
- package/skills/yara-authoring/skills/yara-rule-authoring/examples/SUSP_JS_Obfuscation_Jan25.yar +185 -0
- package/skills/yara-authoring/skills/yara-rule-authoring/references/crx-module.md +214 -0
- package/skills/yara-authoring/skills/yara-rule-authoring/references/dex-module.md +383 -0
- package/skills/yara-authoring/skills/yara-rule-authoring/references/performance.md +333 -0
- package/skills/yara-authoring/skills/yara-rule-authoring/references/strings.md +433 -0
- package/skills/yara-authoring/skills/yara-rule-authoring/references/style-guide.md +257 -0
- package/skills/yara-authoring/skills/yara-rule-authoring/references/testing.md +399 -0
- package/skills/yara-authoring/skills/yara-rule-authoring/scripts/atom_analyzer.py +526 -0
- package/skills/yara-authoring/skills/yara-rule-authoring/scripts/pyproject.toml +25 -0
- package/skills/yara-authoring/skills/yara-rule-authoring/scripts/yara_lint.py +631 -0
- 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
|
+
)
|