patchi 0.7.5__py3-none-any.whl
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.
- patchi/__init__.py +1 -0
- patchi/__main__.py +13 -0
- patchi/cli/__init__.py +0 -0
- patchi/cli/command_families.py +235 -0
- patchi/cli/commands/__init__.py +0 -0
- patchi/cli/commands/access_cmd.py +65 -0
- patchi/cli/commands/agent_stats_cmd.py +273 -0
- patchi/cli/commands/agents_cmd.py +187 -0
- patchi/cli/commands/ai_cmd.py +308 -0
- patchi/cli/commands/assure_cmd.py +538 -0
- patchi/cli/commands/audit_cmd.py +286 -0
- patchi/cli/commands/auto_cmd.py +110 -0
- patchi/cli/commands/blame_cmd.py +53 -0
- patchi/cli/commands/chains_cmd.py +327 -0
- patchi/cli/commands/charter_cmd.py +173 -0
- patchi/cli/commands/chat_cmd.py +597 -0
- patchi/cli/commands/check_cmd.py +232 -0
- patchi/cli/commands/cleanup_cmd.py +328 -0
- patchi/cli/commands/cockpit_cmd.py +208 -0
- patchi/cli/commands/commands_cmd.py +160 -0
- patchi/cli/commands/cross_repo_cmd.py +113 -0
- patchi/cli/commands/deps_cmd.py +110 -0
- patchi/cli/commands/dev_check_cmd.py +342 -0
- patchi/cli/commands/dev_cmd.py +445 -0
- patchi/cli/commands/doctor_cmd.py +457 -0
- patchi/cli/commands/findings_cmd.py +187 -0
- patchi/cli/commands/fix_cmd.py +648 -0
- patchi/cli/commands/fix_review_cmd.py +273 -0
- patchi/cli/commands/heatmap_cmd.py +62 -0
- patchi/cli/commands/help_cmd.py +159 -0
- patchi/cli/commands/hosted_cmd.py +980 -0
- patchi/cli/commands/init.py +456 -0
- patchi/cli/commands/key_cmd.py +571 -0
- patchi/cli/commands/learn_cmd.py +140 -0
- patchi/cli/commands/link_cmd.py +104 -0
- patchi/cli/commands/log_cmd.py +57 -0
- patchi/cli/commands/memory_cmd.py +215 -0
- patchi/cli/commands/model_cmd.py +207 -0
- patchi/cli/commands/notify_cmd.py +213 -0
- patchi/cli/commands/notify_prompts.py +29 -0
- patchi/cli/commands/patch_cmd.py +231 -0
- patchi/cli/commands/plan_cmd.py +128 -0
- patchi/cli/commands/plugins_cmd.py +307 -0
- patchi/cli/commands/queue_cmd.py +152 -0
- patchi/cli/commands/quick_cmd.py +159 -0
- patchi/cli/commands/ready_cmd.py +292 -0
- patchi/cli/commands/report_cmd.py +416 -0
- patchi/cli/commands/restrict_cmd.py +127 -0
- patchi/cli/commands/review_cmd.py +225 -0
- patchi/cli/commands/rules_cmd.py +233 -0
- patchi/cli/commands/scan_cmd.py +1908 -0
- patchi/cli/commands/settings_cmd.py +45 -0
- patchi/cli/commands/status_cmd.py +679 -0
- patchi/cli/commands/test_cmd.py +1052 -0
- patchi/cli/commands/trend_cmd.py +296 -0
- patchi/cli/commands/undo_cmd.py +235 -0
- patchi/cli/commands/update_cmd.py +345 -0
- patchi/cli/commands/verify_cmd.py +56 -0
- patchi/cli/commands/vr_cmd.py +440 -0
- patchi/cli/commands/watch_cmd.py +304 -0
- patchi/cli/commands/web_cmd.py +114 -0
- patchi/cli/console.py +101 -0
- patchi/cli/display/__init__.py +1 -0
- patchi/cli/display/live_audit.py +269 -0
- patchi/cli/display/live_progress.py +169 -0
- patchi/cli/framework.py +241 -0
- patchi/cli/logo.py +128 -0
- patchi/cli/main.py +222 -0
- patchi/cli/registry.py +1171 -0
- patchi/cli/style.py +68 -0
- patchi/cli/themes.py +187 -0
- patchi/cli/ux.py +444 -0
- patchi/core/__init__.py +0 -0
- patchi/core/agents/__init__.py +0 -0
- patchi/core/agents/api_fuzzer_agent.py +141 -0
- patchi/core/agents/attack_agent.py +242 -0
- patchi/core/agents/base.py +1012 -0
- patchi/core/agents/bug_predictor_agent.py +58 -0
- patchi/core/agents/build_tool_validator.py +150 -0
- patchi/core/agents/cache.py +192 -0
- patchi/core/agents/chaos_agent.py +133 -0
- patchi/core/agents/cicd_generator.py +240 -0
- patchi/core/agents/comment_scanner.py +280 -0
- patchi/core/agents/console_logging_agent.py +156 -0
- patchi/core/agents/coordinator.py +858 -0
- patchi/core/agents/core_scanner.py +269 -0
- patchi/core/agents/coverage_prioritizer.py +181 -0
- patchi/core/agents/dead_code_hygiene.py +296 -0
- patchi/core/agents/dead_code_scanner.py +692 -0
- patchi/core/agents/dependency_scanner.py +725 -0
- patchi/core/agents/doc_claim_agent.py +573 -0
- patchi/core/agents/duplicate_scanner.py +568 -0
- patchi/core/agents/env_scanner.py +410 -0
- patchi/core/agents/feature_flag_agent.py +89 -0
- patchi/core/agents/frontend_framework_agent.py +133 -0
- patchi/core/agents/goroutine_agent.py +82 -0
- patchi/core/agents/governor.py +1972 -0
- patchi/core/agents/i18n_agent.py +95 -0
- patchi/core/agents/license_compliance.py +247 -0
- patchi/core/agents/linking_agent.py +251 -0
- patchi/core/agents/memory_profiler_agent.py +102 -0
- patchi/core/agents/modernization_agent.py +84 -0
- patchi/core/agents/mutation_agent.py +174 -0
- patchi/core/agents/nl_query_agent.py +74 -0
- patchi/core/agents/promise_rejection_agent.py +100 -0
- patchi/core/agents/refactoring_agent.py +286 -0
- patchi/core/agents/resource_leak_agent.py +91 -0
- patchi/core/agents/route_graph_scanner.py +951 -0
- patchi/core/agents/run_agent.py +106 -0
- patchi/core/agents/rust_unwrap_agent.py +100 -0
- patchi/core/agents/sbom_generator.py +284 -0
- patchi/core/agents/scanners.py +67 -0
- patchi/core/agents/side/__init__.py +1 -0
- patchi/core/agents/side/build_agent.py +65 -0
- patchi/core/agents/side/format_agent.py +50 -0
- patchi/core/agents/side/install_agent.py +63 -0
- patchi/core/agents/side_file_scanner.py +549 -0
- patchi/core/agents/snapshot_drift_detector.py +146 -0
- patchi/core/agents/spa_route_inventory.py +203 -0
- patchi/core/agents/test_scanner.py +442 -0
- patchi/core/agents/tool_health.py +109 -0
- patchi/core/agents/tool_runner.py +162 -0
- patchi/core/agents/type_scanner.py +168 -0
- patchi/core/agents/ui_scanner.py +558 -0
- patchi/core/ai/__init__.py +20 -0
- patchi/core/ai/agent_audit.py +153 -0
- patchi/core/ai/agent_profiler.py +277 -0
- patchi/core/ai/client.py +499 -0
- patchi/core/ai/cost_tracker.py +107 -0
- patchi/core/ai/model_router.py +318 -0
- patchi/core/ai/orchestrator.py +625 -0
- patchi/core/ai/prompts.py +681 -0
- patchi/core/ai/smart.py +302 -0
- patchi/core/ai/tool_executor.py +24 -0
- patchi/core/ai/tools/__init__.py +10 -0
- patchi/core/ai/tools/executor.py +621 -0
- patchi/core/ai/tools/realize.py +1160 -0
- patchi/core/ai/tools/registry.py +1577 -0
- patchi/core/assurance/__init__.py +33 -0
- patchi/core/assurance/builder.py +211 -0
- patchi/core/assurance/graph.py +342 -0
- patchi/core/assurance/invariants.py +168 -0
- patchi/core/atomic.py +61 -0
- patchi/core/attackers/__init__.py +17 -0
- patchi/core/attackers/api_abuse.py +89 -0
- patchi/core/attackers/auth_bypass.py +73 -0
- patchi/core/attackers/base.py +91 -0
- patchi/core/attackers/business_logic.py +88 -0
- patchi/core/attackers/planner.py +108 -0
- patchi/core/attackers/priv_escalation.py +88 -0
- patchi/core/attackers/recon.py +87 -0
- patchi/core/brain/__init__.py +0 -0
- patchi/core/brain/ast_utils/__init__.py +81 -0
- patchi/core/brain/ast_utils/assignments.py +219 -0
- patchi/core/brain/ast_utils/calls.py +139 -0
- patchi/core/brain/ast_utils/config.py +132 -0
- patchi/core/brain/ast_utils/control_flow.py +74 -0
- patchi/core/brain/ast_utils/dead_symbols.py +205 -0
- patchi/core/brain/ast_utils/decorators.py +108 -0
- patchi/core/brain/ast_utils/helpers.py +144 -0
- patchi/core/brain/ast_utils/imports.py +169 -0
- patchi/core/brain/ast_utils/scan.py +143 -0
- patchi/core/brain/ast_utils/taint.py +310 -0
- patchi/core/brain/audit.py +379 -0
- patchi/core/brain/baseline.py +195 -0
- patchi/core/brain/blast_radius.py +312 -0
- patchi/core/brain/body_tags.py +144 -0
- patchi/core/brain/brain.py +1161 -0
- patchi/core/brain/brain_context.py +393 -0
- patchi/core/brain/brain_watcher.py +340 -0
- patchi/core/brain/charter.py +473 -0
- patchi/core/brain/classifier.py +214 -0
- patchi/core/brain/cockpit.py +350 -0
- patchi/core/brain/code_query.py +710 -0
- patchi/core/brain/contract.py +889 -0
- patchi/core/brain/contract_diff.py +155 -0
- patchi/core/brain/council.py +800 -0
- patchi/core/brain/cross_repo.py +104 -0
- patchi/core/brain/dead_code_tools.py +157 -0
- patchi/core/brain/doc_validator.py +197 -0
- patchi/core/brain/domain_activator.py +2183 -0
- patchi/core/brain/enriched_context.py +220 -0
- patchi/core/brain/file_corpus.py +220 -0
- patchi/core/brain/framework.py +953 -0
- patchi/core/brain/freshness.py +273 -0
- patchi/core/brain/git_aware.py +206 -0
- patchi/core/brain/heatmap.py +104 -0
- patchi/core/brain/ignore_parser.py +153 -0
- patchi/core/brain/import_graph.py +486 -0
- patchi/core/brain/languages.py +397 -0
- patchi/core/brain/layered_brain.py +419 -0
- patchi/core/brain/learning.py +229 -0
- patchi/core/brain/orphaned_endpoints.py +313 -0
- patchi/core/brain/personas/__init__.py +648 -0
- patchi/core/brain/personas/bad_user.py +165 -0
- patchi/core/brain/personas/base.py +349 -0
- patchi/core/brain/proactive.py +807 -0
- patchi/core/brain/project_context.py +121 -0
- patchi/core/brain/project_reader.py +521 -0
- patchi/core/brain/reasoning.py +269 -0
- patchi/core/brain/route_detector/__init__.py +15 -0
- patchi/core/brain/route_detector/base.py +31 -0
- patchi/core/brain/route_detector/csharp.py +209 -0
- patchi/core/brain/route_detector/file_based.py +81 -0
- patchi/core/brain/route_detector/go.py +152 -0
- patchi/core/brain/route_detector/java.py +195 -0
- patchi/core/brain/route_detector/javascript.py +197 -0
- patchi/core/brain/route_detector/php.py +158 -0
- patchi/core/brain/route_detector/python.py +193 -0
- patchi/core/brain/route_detector/registry.py +25 -0
- patchi/core/brain/route_detector/ruby.py +134 -0
- patchi/core/brain/route_detector/rust.py +216 -0
- patchi/core/brain/route_detector/swift.py +118 -0
- patchi/core/brain/route_mapper.py +265 -0
- patchi/core/brain/scanner.py +2136 -0
- patchi/core/brain/secrets.py +280 -0
- patchi/core/brain/symbol_graph.py +1955 -0
- patchi/core/brain/team.py +28 -0
- patchi/core/brain/trace_log.py +132 -0
- patchi/core/brain/type_checker/__init__.py +65 -0
- patchi/core/brain/type_checker/base.py +60 -0
- patchi/core/brain/type_checker/csharp.py +59 -0
- patchi/core/brain/type_checker/dart.py +59 -0
- patchi/core/brain/type_checker/go.py +81 -0
- patchi/core/brain/type_checker/java.py +75 -0
- patchi/core/brain/type_checker/kotlin.py +75 -0
- patchi/core/brain/type_checker/php.py +88 -0
- patchi/core/brain/type_checker/python.py +87 -0
- patchi/core/brain/type_checker/rust.py +81 -0
- patchi/core/brain/type_checker/swift.py +59 -0
- patchi/core/brain/type_checker/typescript.py +298 -0
- patchi/core/brain/understander.py +130 -0
- patchi/core/brain/verify.py +379 -0
- patchi/core/campaigns/__init__.py +14 -0
- patchi/core/campaigns/base.py +62 -0
- patchi/core/campaigns/data_flow_abuse.py +153 -0
- patchi/core/campaigns/orchestrator.py +140 -0
- patchi/core/campaigns/state_transitions.py +178 -0
- patchi/core/ci_bundle.py +104 -0
- patchi/core/config.py +311 -0
- patchi/core/constants.py +245 -0
- patchi/core/debug/__init__.py +25 -0
- patchi/core/debug/adapters/__init__.py +11 -0
- patchi/core/debug/adapters/codelldb.py +379 -0
- patchi/core/debug/adapters/node.py +222 -0
- patchi/core/debug/adapters/powershell.py +210 -0
- patchi/core/debug/adapters/python.py +244 -0
- patchi/core/debug/capture.py +210 -0
- patchi/core/debug/dap_client.py +268 -0
- patchi/core/detector/__init__.py +0 -0
- patchi/core/detector/audit.py +194 -0
- patchi/core/detector/bus.py +226 -0
- patchi/core/detector/dispatcher.py +275 -0
- patchi/core/detector/event.py +254 -0
- patchi/core/detector/sigma_engine.py +378 -0
- patchi/core/detector/triage.py +289 -0
- patchi/core/export/sarif.py +113 -0
- patchi/core/fix/__init__.py +17 -0
- patchi/core/fix/applier.py +574 -0
- patchi/core/fix/base.py +510 -0
- patchi/core/fix/code_fixer.py +169 -0
- patchi/core/fix/dead_code_remover.py +131 -0
- patchi/core/fix/fix_agents.py +775 -0
- patchi/core/fix/patch.py +444 -0
- patchi/core/fix/risk_gate.py +383 -0
- patchi/core/fix/security_fixer.py +93 -0
- patchi/core/fix/verify_loop.py +204 -0
- patchi/core/fuzz/__init__.py +8 -0
- patchi/core/fuzz/corpus.py +137 -0
- patchi/core/fuzz/input_fuzzer.py +256 -0
- patchi/core/fuzz/sequence_fuzzer.py +162 -0
- patchi/core/fuzz/state_fuzzer.py +236 -0
- patchi/core/health.py +407 -0
- patchi/core/hosted/__init__.py +1 -0
- patchi/core/hosted/anomaly.py +298 -0
- patchi/core/hosted/audit_log.py +102 -0
- patchi/core/hosted/compliance_report.py +148 -0
- patchi/core/hosted/ip_reputation.py +169 -0
- patchi/core/hosted/log_parsers.py +251 -0
- patchi/core/hosted/tokens.py +127 -0
- patchi/core/hosted/watchlist.py +164 -0
- patchi/core/hosted/webhooks.py +156 -0
- patchi/core/memory.py +437 -0
- patchi/core/notifications/__init__.py +18 -0
- patchi/core/notifications/channels.py +192 -0
- patchi/core/notifications/digest.py +141 -0
- patchi/core/notifications/escalation.py +158 -0
- patchi/core/notifications/notifier.py +182 -0
- patchi/core/notifications/quiet_hours.py +69 -0
- patchi/core/plugins/__init__.py +48 -0
- patchi/core/plugins/analyzer.py +326 -0
- patchi/core/plugins/analyzers/__init__.py +6 -0
- patchi/core/plugins/analyzers/dead_code_analyzer.py +120 -0
- patchi/core/plugins/analyzers/secret_scanner.py +158 -0
- patchi/core/plugins/analyzers/todo_scanner.py +115 -0
- patchi/core/plugins/registry.py +335 -0
- patchi/core/queue.py +375 -0
- patchi/core/reliability/__init__.py +13 -0
- patchi/core/reliability/chaos.py +163 -0
- patchi/core/reliability/fault_injector.py +172 -0
- patchi/core/reliability/idempotency.py +128 -0
- patchi/core/reliability/recovery.py +147 -0
- patchi/core/runtime/__init__.py +5 -0
- patchi/core/runtime/tracer.py +230 -0
- patchi/core/scan_bus.py +156 -0
- patchi/core/security/__init__.py +1 -0
- patchi/core/security/ai_validator.py +462 -0
- patchi/core/security/app_mapper.py +216 -0
- patchi/core/security/app_profile.py +464 -0
- patchi/core/security/attack_feedback.py +141 -0
- patchi/core/security/attack_scenarios/__init__.py +1 -0
- patchi/core/security/attack_tree.py +176 -0
- patchi/core/security/auth_audit_agent.py +344 -0
- patchi/core/security/authz_agent.py +602 -0
- patchi/core/security/auto_fix_proactive.py +620 -0
- patchi/core/security/auto_fixer.py +660 -0
- patchi/core/security/auto_ticket.py +53 -0
- patchi/core/security/bandit_agent.py +152 -0
- patchi/core/security/blast_radius.py +226 -0
- patchi/core/security/browser_tester.py +333 -0
- patchi/core/security/business_logic_agent.py +263 -0
- patchi/core/security/catch_block_auditor.py +395 -0
- patchi/core/security/cdn_cache_agent.py +357 -0
- patchi/core/security/chain_analyzer.py +321 -0
- patchi/core/security/chain_to_assurance.py +168 -0
- patchi/core/security/charter.py +510 -0
- patchi/core/security/cloud_waf_detector.py +295 -0
- patchi/core/security/codeql_agent.py +233 -0
- patchi/core/security/compliance_agent.py +348 -0
- patchi/core/security/confidence_gate.py +423 -0
- patchi/core/security/container_scanner.py +282 -0
- patchi/core/security/crypto_agent.py +378 -0
- patchi/core/security/cve_monitor.py +262 -0
- patchi/core/security/dast_agent.py +865 -0
- patchi/core/security/defectdojo.py +238 -0
- patchi/core/security/defenders/__init__.py +75 -0
- patchi/core/security/defenders/auth_middleware.py +115 -0
- patchi/core/security/defenders/base.py +97 -0
- patchi/core/security/defenders/block_ip.py +71 -0
- patchi/core/security/defenders/block_ws_origin.py +36 -0
- patchi/core/security/defenders/crypto_fix.py +118 -0
- patchi/core/security/defenders/enforce_rate_limit.py +108 -0
- patchi/core/security/defenders/fix_code.py +93 -0
- patchi/core/security/defenders/invalidate_session.py +40 -0
- patchi/core/security/defenders/patch_config.py +70 -0
- patchi/core/security/defenders/rotate_secret.py +54 -0
- patchi/core/security/defenders/suspend_account.py +41 -0
- patchi/core/security/defenders/update_dependency.py +160 -0
- patchi/core/security/defense_layer.py +217 -0
- patchi/core/security/dependency_vulnerability_agent.py +616 -0
- patchi/core/security/detection_pipeline.py +289 -0
- patchi/core/security/dns_security_agent.py +497 -0
- patchi/core/security/domain_activator_v2.py +913 -0
- patchi/core/security/domain_loader.py +1004 -0
- patchi/core/security/domains/__init__.py +0 -0
- patchi/core/security/email_authentication_agent.py +417 -0
- patchi/core/security/env_var_validator.py +304 -0
- patchi/core/security/evidence.py +100 -0
- patchi/core/security/falco_runtime_agent.py +387 -0
- patchi/core/security/fix-playbooks/__init__.py +0 -0
- patchi/core/security/gated_finding.py +85 -0
- patchi/core/security/git_diff_activator.py +367 -0
- patchi/core/security/governance.py +208 -0
- patchi/core/security/gradual_gate.py +34 -0
- patchi/core/security/history.py +315 -0
- patchi/core/security/iac_scanner.py +250 -0
- patchi/core/security/ignore_expiry.py +111 -0
- patchi/core/security/ignore_learner.py +416 -0
- patchi/core/security/injection_agent.py +319 -0
- patchi/core/security/insecure_randomness_agent.py +288 -0
- patchi/core/security/intent_analyzer.py +326 -0
- patchi/core/security/jwt_agent.py +453 -0
- patchi/core/security/kubernetes_agent.py +638 -0
- patchi/core/security/layer2_orchestrator.py +341 -0
- patchi/core/security/llm_security_agent.py +252 -0
- patchi/core/security/misconfig_agent.py +600 -0
- patchi/core/security/mobile_security_agent.py +474 -0
- patchi/core/security/network_agent.py +376 -0
- patchi/core/security/noise_filter.py +274 -0
- patchi/core/security/orchestrator.py +313 -0
- patchi/core/security/pattern_context.py +125 -0
- patchi/core/security/pattern_loader.py +101 -0
- patchi/core/security/pentest/__init__.py +4 -0
- patchi/core/security/pentest/base.py +58 -0
- patchi/core/security/pentest/dalfox_adapter.py +40 -0
- patchi/core/security/pentest/ffuf_adapter.py +48 -0
- patchi/core/security/pentest/nuclei_adapter.py +50 -0
- patchi/core/security/pentest/registry.py +80 -0
- patchi/core/security/pentest/shannon_adapter.py +107 -0
- patchi/core/security/pentest/sqlmap_adapter.py +56 -0
- patchi/core/security/pentest/zap_adapter.py +52 -0
- patchi/core/security/plan_auditor.py +276 -0
- patchi/core/security/policy_engine.py +272 -0
- patchi/core/security/prechecks.py +172 -0
- patchi/core/security/precommit_hook.py +247 -0
- patchi/core/security/privacy_agent.py +361 -0
- patchi/core/security/push_notification_agent.py +417 -0
- patchi/core/security/pysa_agent.py +143 -0
- patchi/core/security/reasoning.py +442 -0
- patchi/core/security/red_team_agent.py +184 -0
- patchi/core/security/red_team_engine.py +1142 -0
- patchi/core/security/remediation.py +360 -0
- patchi/core/security/request_interceptor.py +294 -0
- patchi/core/security/runtime_validator.py +245 -0
- patchi/core/security/saml_sso_agent.py +401 -0
- patchi/core/security/sast_agent.py +213 -0
- patchi/core/security/sast_gate.py +95 -0
- patchi/core/security/scheduler.py +287 -0
- patchi/core/security/secrets_runtime_agent.py +476 -0
- patchi/core/security/security_agents.py +175 -0
- patchi/core/security/security_config.py +551 -0
- patchi/core/security/security_probe.py +762 -0
- patchi/core/security/security_taint.py +1005 -0
- patchi/core/security/sensitive_data_agent.py +271 -0
- patchi/core/security/service_mesh_agent.py +412 -0
- patchi/core/security/session_management_agent.py +245 -0
- patchi/core/security/ssrf_agent.py +422 -0
- patchi/core/security/supply_chain.py +449 -0
- patchi/core/security/threat_model_generator.py +378 -0
- patchi/core/security/threat_model_updater.py +196 -0
- patchi/core/security/tool_adapters.py +135 -0
- patchi/core/security/tool_verify.py +173 -0
- patchi/core/security/websocket_security_agent.py +188 -0
- patchi/core/snapshot.py +242 -0
- patchi/core/tenant.py +386 -0
- patchi/core/testing/__init__.py +17 -0
- patchi/core/testing/_browser.py +147 -0
- patchi/core/testing/ads_agent.py +141 -0
- patchi/core/testing/api_contract_agent.py +450 -0
- patchi/core/testing/app_discovery_agent.py +641 -0
- patchi/core/testing/app_launcher.py +184 -0
- patchi/core/testing/browser_test_agent.py +516 -0
- patchi/core/testing/e2e_flow_agent.py +354 -0
- patchi/core/testing/flake_detector_agent.py +292 -0
- patchi/core/testing/gate.py +31 -0
- patchi/core/testing/live_test_runner.py +304 -0
- patchi/core/testing/live_v2/__init__.py +1 -0
- patchi/core/testing/live_v2/browser_pool.py +349 -0
- patchi/core/testing/live_v2/browser_test_runner.py +313 -0
- patchi/core/testing/live_v2/runner.py +626 -0
- patchi/core/testing/live_v2/screenshot_manager.py +474 -0
- patchi/core/testing/live_v2/stress_orchestrator.py +523 -0
- patchi/core/testing/live_v2/video_recorder.py +303 -0
- patchi/core/testing/regression_agent.py +302 -0
- patchi/core/testing/security_test_agent.py +253 -0
- patchi/core/testing/stress_test_agent.py +203 -0
- patchi/core/testing/test_agents.py +50 -0
- patchi/core/testing/test_config.py +194 -0
- patchi/core/testing/ui_accessibility_agent.py +324 -0
- patchi/core/testing/ui_button_agent.py +294 -0
- patchi/core/testing/ui_layout_agent.py +330 -0
- patchi/core/testing/unit_test_agent.py +1149 -0
- patchi/core/testing/visual_regression_agent.py +260 -0
- patchi/web/__init__.py +1 -0
- patchi/web/api/__init__.py +1 -0
- patchi/web/api/brain_map.py +114 -0
- patchi/web/api/charts.py +47 -0
- patchi/web/api/chat.py +224 -0
- patchi/web/api/cicd.py +576 -0
- patchi/web/api/dast_evidence.py +49 -0
- patchi/web/api/dev_check.py +320 -0
- patchi/web/api/fix.py +134 -0
- patchi/web/api/guard.py +65 -0
- patchi/web/api/hosted.py +128 -0
- patchi/web/api/hosted_v2.py +234 -0
- patchi/web/api/live_testing.py +699 -0
- patchi/web/api/scan.py +582 -0
- patchi/web/api/smart.py +111 -0
- patchi/web/api/tenant.py +117 -0
- patchi/web/api/tools.py +85 -0
- patchi/web/api_legacy.py +2401 -0
- patchi/web/app.py +310 -0
- patchi/web/events.py +483 -0
- patchi/web/routes/__init__.py +1 -0
- patchi/web/routes/assurance.py +541 -0
- patchi/web/routes/brain.py +36 -0
- patchi/web/routes/charter.py +227 -0
- patchi/web/routes/chat.py +23 -0
- patchi/web/routes/dashboard.py +558 -0
- patchi/web/routes/dashboard_v2.py +402 -0
- patchi/web/routes/findings.py +330 -0
- patchi/web/routes/guard.py +35 -0
- patchi/web/routes/history.py +126 -0
- patchi/web/routes/landing.py +52 -0
- patchi/web/routes/live_testing.py +24 -0
- patchi/web/routes/review.py +57 -0
- patchi/web/routes/self_improvement.py +110 -0
- patchi/web/routes/settings.py +49 -0
- patchi/web/routes/tokens.py +29 -0
- patchi/web/spawn.py +139 -0
- patchi/web/ws.py +141 -0
- patchi-0.7.5.dist-info/METADATA +315 -0
- patchi-0.7.5.dist-info/RECORD +496 -0
- patchi-0.7.5.dist-info/WHEEL +5 -0
- patchi-0.7.5.dist-info/licenses/LICENSE +60 -0
- patchi-0.7.5.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,951 @@
|
|
|
1
|
+
"""
|
|
2
|
+
RouteGraphScanner — framework-aware route extraction.
|
|
3
|
+
|
|
4
|
+
Detects and extracts routes from various frameworks:
|
|
5
|
+
- Express.js: app.get/post/put/delete/use() calls
|
|
6
|
+
- FastAPI: @app.get/post/put/delete() decorators
|
|
7
|
+
- Flask: @app.route() decorators
|
|
8
|
+
- Django: urlpatterns list entries
|
|
9
|
+
- Next.js: pages/ and app/ directory structure
|
|
10
|
+
- Spring Boot: @GetMapping/@PostMapping annotations
|
|
11
|
+
- ASP.NET Core: [HttpGet]/[HttpPost] attributes
|
|
12
|
+
- Ruby on Rails: routes.rb definitions
|
|
13
|
+
|
|
14
|
+
Extracts:
|
|
15
|
+
- HTTP method
|
|
16
|
+
- Route path
|
|
17
|
+
- Handler function name
|
|
18
|
+
- Middleware chain
|
|
19
|
+
- Authentication status
|
|
20
|
+
|
|
21
|
+
Does NOT call AI.
|
|
22
|
+
Does NOT write to disk.
|
|
23
|
+
Does NOT touch the queue.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import ast as py_ast
|
|
29
|
+
import logging
|
|
30
|
+
import re
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
from typing import Any
|
|
33
|
+
|
|
34
|
+
from ..brain.languages import Lang, detect_language, get_parser, parse_source
|
|
35
|
+
from .base import (
|
|
36
|
+
_SKIP_DIRS,
|
|
37
|
+
_SKIP_FILES,
|
|
38
|
+
AgentGroup,
|
|
39
|
+
AgentInput,
|
|
40
|
+
AgentResult,
|
|
41
|
+
AgentStatus,
|
|
42
|
+
BaseAgent,
|
|
43
|
+
Finding,
|
|
44
|
+
Severity,
|
|
45
|
+
make_finding,
|
|
46
|
+
register,
|
|
47
|
+
safe_rglob,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
_log = logging.getLogger("patchi.agents.route_graph_scanner")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@register
|
|
54
|
+
class RouteGraphScanner(BaseAgent):
|
|
55
|
+
"""Scanner for web framework routes."""
|
|
56
|
+
|
|
57
|
+
group = AgentGroup.SCANNER
|
|
58
|
+
name = "RouteGraphScanner"
|
|
59
|
+
description = "Framework-aware route extraction"
|
|
60
|
+
|
|
61
|
+
def _run(self, inp: AgentInput, result: AgentResult) -> None:
|
|
62
|
+
"""Scan for routes in web framework files."""
|
|
63
|
+
findings = []
|
|
64
|
+
|
|
65
|
+
# Define route file patterns
|
|
66
|
+
route_patterns = [
|
|
67
|
+
# JavaScript/TypeScript (Express, Next.js, etc.)
|
|
68
|
+
"*.js",
|
|
69
|
+
"*.jsx",
|
|
70
|
+
"*.ts",
|
|
71
|
+
"*.tsx",
|
|
72
|
+
"**/routes/**",
|
|
73
|
+
"**/api/**",
|
|
74
|
+
"**/pages/**",
|
|
75
|
+
"**/app/**",
|
|
76
|
+
# Python (FastAPI, Flask, Django)
|
|
77
|
+
"*.py",
|
|
78
|
+
"**/urls.py",
|
|
79
|
+
"**/views.py",
|
|
80
|
+
"**/routes.py",
|
|
81
|
+
"**/api/**",
|
|
82
|
+
# Java (Spring Boot)
|
|
83
|
+
"*.java",
|
|
84
|
+
# Ruby (Rails)
|
|
85
|
+
"*.rb",
|
|
86
|
+
"routes.rb",
|
|
87
|
+
# PHP (Laravel, Symfony)
|
|
88
|
+
"*.php",
|
|
89
|
+
# C# (ASP.NET)
|
|
90
|
+
"*.cs",
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
# Search for route files and avoid scanning the same file twice
|
|
94
|
+
scanned_files = set()
|
|
95
|
+
# ponytail: rglob traverses node_modules (30k+ files). Walk with pruning.
|
|
96
|
+
import fnmatch
|
|
97
|
+
import os
|
|
98
|
+
|
|
99
|
+
for dirpath, dirnames, filenames in os.walk(inp.root):
|
|
100
|
+
dirnames[:] = [d for d in dirnames if d not in _SKIP_DIRS]
|
|
101
|
+
rel_dir = Path(dirpath).relative_to(inp.root).as_posix()
|
|
102
|
+
for fname in filenames:
|
|
103
|
+
if fname in _SKIP_FILES:
|
|
104
|
+
continue
|
|
105
|
+
for pattern in route_patterns:
|
|
106
|
+
if "**" in pattern:
|
|
107
|
+
parts = pattern.split("/")
|
|
108
|
+
if len(parts) == 1:
|
|
109
|
+
if fnmatch.fnmatch(fname, parts[0]):
|
|
110
|
+
break
|
|
111
|
+
else:
|
|
112
|
+
mid = parts[1] if len(parts) > 2 else None
|
|
113
|
+
if mid and mid in rel_dir.split("/"):
|
|
114
|
+
break
|
|
115
|
+
else:
|
|
116
|
+
if fnmatch.fnmatch(fname, pattern):
|
|
117
|
+
break
|
|
118
|
+
else:
|
|
119
|
+
continue
|
|
120
|
+
file_path = Path(dirpath) / fname
|
|
121
|
+
rel_path = f"{rel_dir}/{fname}" if rel_dir != "." else fname
|
|
122
|
+
if rel_path not in scanned_files:
|
|
123
|
+
scanned_files.add(rel_path)
|
|
124
|
+
if not self._should_skip_file(rel_path, inp):
|
|
125
|
+
findings.extend(self._scan_route_file(file_path, rel_path))
|
|
126
|
+
|
|
127
|
+
# Get only the route findings
|
|
128
|
+
route_findings = [f for f in findings if f.type.startswith("route_")]
|
|
129
|
+
|
|
130
|
+
# Guard at top: if no routes found, return early
|
|
131
|
+
if not route_findings:
|
|
132
|
+
result.status = AgentStatus.SUCCEEDED
|
|
133
|
+
result.findings = findings
|
|
134
|
+
result.data.update(
|
|
135
|
+
{
|
|
136
|
+
"routes_found": 0,
|
|
137
|
+
"route_map": {},
|
|
138
|
+
"framework_detected": self._detect_framework(inp),
|
|
139
|
+
"needs_ai": False,
|
|
140
|
+
}
|
|
141
|
+
)
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
findings.extend(self._detect_sensitive_routes(route_findings))
|
|
145
|
+
route_map = self._build_route_map(route_findings)
|
|
146
|
+
|
|
147
|
+
result.status = AgentStatus.SUCCEEDED
|
|
148
|
+
result.findings = findings
|
|
149
|
+
result.data.update(
|
|
150
|
+
{
|
|
151
|
+
"routes_found": len(route_findings),
|
|
152
|
+
"route_map": route_map,
|
|
153
|
+
"framework_detected": self._detect_framework(inp),
|
|
154
|
+
"needs_ai": False,
|
|
155
|
+
}
|
|
156
|
+
)
|
|
157
|
+
return
|
|
158
|
+
|
|
159
|
+
def _should_skip_file(self, file_path: str, inp: AgentInput) -> bool:
|
|
160
|
+
"""Check if file should be skipped based on restrictions."""
|
|
161
|
+
from pathlib import PurePosixPath
|
|
162
|
+
|
|
163
|
+
p = PurePosixPath(file_path)
|
|
164
|
+
if any(s in p.parts for s in _SKIP_DIRS):
|
|
165
|
+
return True
|
|
166
|
+
if p.name in _SKIP_FILES:
|
|
167
|
+
return True
|
|
168
|
+
|
|
169
|
+
# Check restrictions
|
|
170
|
+
restrictions = inp.config.get("restrictions", [])
|
|
171
|
+
for r in restrictions:
|
|
172
|
+
if r.get("enabled", True):
|
|
173
|
+
path = r["path"]
|
|
174
|
+
if file_path.startswith(path) or PurePosixPath(file_path).match(path):
|
|
175
|
+
if r["type"] == "NO_TOUCH":
|
|
176
|
+
return True
|
|
177
|
+
elif r["type"] == "SCAN_ONLY" and self.__class__.__name__ == "FixAgent":
|
|
178
|
+
return True
|
|
179
|
+
return False
|
|
180
|
+
|
|
181
|
+
def _scan_route_file(self, file_path: Path, rel_path: str) -> list[Finding]:
|
|
182
|
+
"""Scan a file for routes."""
|
|
183
|
+
findings = []
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
content = file_path.read_text(encoding="utf-8")
|
|
187
|
+
lang = detect_language(file_path)
|
|
188
|
+
|
|
189
|
+
if lang in [Lang.JAVASCRIPT, Lang.TYPESCRIPT]:
|
|
190
|
+
findings.extend(self._scan_js_routes(content, rel_path))
|
|
191
|
+
elif lang == Lang.PYTHON:
|
|
192
|
+
findings.extend(self._scan_py_routes(content, rel_path))
|
|
193
|
+
elif lang == Lang.JAVA:
|
|
194
|
+
findings.extend(self._scan_java_routes(content, rel_path))
|
|
195
|
+
elif lang == Lang.RUBY:
|
|
196
|
+
findings.extend(self._scan_ruby_routes(content, rel_path))
|
|
197
|
+
elif lang == Lang.PHP:
|
|
198
|
+
findings.extend(self._scan_php_routes(content, rel_path))
|
|
199
|
+
elif lang == Lang.C_SHARP:
|
|
200
|
+
findings.extend(self._scan_cs_routes(content, rel_path))
|
|
201
|
+
|
|
202
|
+
except Exception as e:
|
|
203
|
+
findings.append(
|
|
204
|
+
make_finding(
|
|
205
|
+
severity=Severity.LOW,
|
|
206
|
+
file=rel_path,
|
|
207
|
+
line_start=0,
|
|
208
|
+
title="Route file read error",
|
|
209
|
+
description=f"Could not read route file {file_path.name}: {str(e)}",
|
|
210
|
+
evidence=str(e),
|
|
211
|
+
)
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
return findings
|
|
215
|
+
|
|
216
|
+
def _scan_py_routes(self, content: str, rel_path: str) -> list[Finding]:
|
|
217
|
+
"""Scan Python files for routes (Flask, FastAPI, Django) using AST parsing."""
|
|
218
|
+
findings = []
|
|
219
|
+
|
|
220
|
+
try:
|
|
221
|
+
tree = py_ast.parse(content)
|
|
222
|
+
except SyntaxError:
|
|
223
|
+
# If AST parsing fails, fall back to basic pattern matching
|
|
224
|
+
return self._scan_py_routes_fallback(content, rel_path)
|
|
225
|
+
|
|
226
|
+
for node in py_ast.walk(tree):
|
|
227
|
+
# FastAPI patterns - look for decorator calls like @app.get("/path")
|
|
228
|
+
if isinstance(node, py_ast.FunctionDef) and node.decorator_list:
|
|
229
|
+
for decorator in node.decorator_list:
|
|
230
|
+
if isinstance(decorator, py_ast.Call) and isinstance(
|
|
231
|
+
decorator.func, py_ast.Attribute
|
|
232
|
+
):
|
|
233
|
+
func_attr = decorator.func.attr
|
|
234
|
+
if func_attr in [
|
|
235
|
+
"get",
|
|
236
|
+
"post",
|
|
237
|
+
"put",
|
|
238
|
+
"delete",
|
|
239
|
+
"patch",
|
|
240
|
+
"options",
|
|
241
|
+
"head",
|
|
242
|
+
"trace",
|
|
243
|
+
]:
|
|
244
|
+
# Check if this is a FastAPI app method
|
|
245
|
+
if isinstance(
|
|
246
|
+
decorator.func.value, py_ast.Name
|
|
247
|
+
) and decorator.func.value.id in ["app", "router"]:
|
|
248
|
+
# Extract the route path from the decorator arguments
|
|
249
|
+
path_arg = None
|
|
250
|
+
if decorator.args:
|
|
251
|
+
# First argument should be the path
|
|
252
|
+
first_arg = decorator.args[0]
|
|
253
|
+
if isinstance(first_arg, py_ast.Constant):
|
|
254
|
+
path_arg = first_arg.value
|
|
255
|
+
elif isinstance(first_arg, py_ast.Str): # Python < 3.8
|
|
256
|
+
path_arg = first_arg.s
|
|
257
|
+
|
|
258
|
+
if path_arg:
|
|
259
|
+
findings.append(
|
|
260
|
+
make_finding(
|
|
261
|
+
severity=Severity.INFO,
|
|
262
|
+
file=rel_path,
|
|
263
|
+
line_start=node.lineno,
|
|
264
|
+
title=f"Route: {func_attr.upper()} {path_arg} (FastAPI)",
|
|
265
|
+
description=f"FastAPI HTTP {func_attr.upper()} route: {path_arg}",
|
|
266
|
+
evidence=f"@app.{func_attr}('{path_arg}')",
|
|
267
|
+
)
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
# Flask patterns - look for @app.route decorator
|
|
271
|
+
elif isinstance(node, py_ast.FunctionDef) and node.decorator_list:
|
|
272
|
+
for decorator in node.decorator_list:
|
|
273
|
+
if (
|
|
274
|
+
isinstance(decorator, py_ast.Call)
|
|
275
|
+
and isinstance(decorator.func, py_ast.Attribute)
|
|
276
|
+
and isinstance(decorator.func.value, py_ast.Name)
|
|
277
|
+
and decorator.func.value.id == "app"
|
|
278
|
+
and decorator.func.attr == "route"
|
|
279
|
+
):
|
|
280
|
+
# Extract the route path from the decorator arguments
|
|
281
|
+
path_arg = None
|
|
282
|
+
if decorator.args:
|
|
283
|
+
first_arg = decorator.args[0]
|
|
284
|
+
if isinstance(first_arg, py_ast.Constant):
|
|
285
|
+
path_arg = first_arg.value
|
|
286
|
+
elif isinstance(first_arg, py_ast.Str): # Python < 3.8
|
|
287
|
+
path_arg = first_arg.s
|
|
288
|
+
|
|
289
|
+
if path_arg:
|
|
290
|
+
# Check for methods in keyword arguments
|
|
291
|
+
methods = ["GET"] # Default for Flask
|
|
292
|
+
for keyword in decorator.keywords:
|
|
293
|
+
if keyword.arg == "methods" and isinstance(
|
|
294
|
+
keyword.value, py_ast.List
|
|
295
|
+
):
|
|
296
|
+
for elt in keyword.value.elts:
|
|
297
|
+
if isinstance(elt, py_ast.Constant):
|
|
298
|
+
methods.append(elt.value)
|
|
299
|
+
elif isinstance(elt, py_ast.Str): # Python < 3.8
|
|
300
|
+
methods.append(elt.s)
|
|
301
|
+
|
|
302
|
+
method_str = ", ".join(methods)
|
|
303
|
+
findings.append(
|
|
304
|
+
make_finding(
|
|
305
|
+
severity=Severity.INFO,
|
|
306
|
+
file=rel_path,
|
|
307
|
+
line_start=node.lineno,
|
|
308
|
+
title=f"Route: {method_str} {path_arg} (Flask)",
|
|
309
|
+
description=f"Flask HTTP {method_str} route: {path_arg}",
|
|
310
|
+
evidence=f"@app.route('{path_arg}')",
|
|
311
|
+
)
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
return findings
|
|
315
|
+
|
|
316
|
+
def _scan_py_routes_fallback(self, content: str, rel_path: str) -> list[Finding]:
|
|
317
|
+
"""Fallback regex-based scanning for Python routes."""
|
|
318
|
+
findings = []
|
|
319
|
+
|
|
320
|
+
# FastAPI patterns
|
|
321
|
+
fastapi_methods = ["get", "post", "put", "delete", "patch", "options", "head", "trace"]
|
|
322
|
+
for method in fastapi_methods:
|
|
323
|
+
pattern = rf'@app\.{method}\s*\(\s*["\']([^"\']*)["\']'
|
|
324
|
+
matches = re.finditer(pattern, content, re.MULTILINE)
|
|
325
|
+
for match in matches:
|
|
326
|
+
path = match.group(1)
|
|
327
|
+
line_start = content[: match.start()].count("\n") + 1
|
|
328
|
+
|
|
329
|
+
findings.append(
|
|
330
|
+
make_finding(
|
|
331
|
+
severity=Severity.INFO,
|
|
332
|
+
file=rel_path,
|
|
333
|
+
line_start=line_start,
|
|
334
|
+
title=f"Route: {method.upper()} {path} (FastAPI)",
|
|
335
|
+
description=f"FastAPI HTTP {method.upper()} route: {path}",
|
|
336
|
+
evidence=match.group(0)[:100],
|
|
337
|
+
)
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
# Flask patterns
|
|
341
|
+
flask_route_pattern = r'@app\.route\s*\(\s*["\']([^"\']*)["\']'
|
|
342
|
+
flask_matches = re.finditer(flask_route_pattern, content, re.MULTILINE)
|
|
343
|
+
for match in flask_matches:
|
|
344
|
+
path = match.group(1)
|
|
345
|
+
line_start = content[: match.start()].count("\n") + 1
|
|
346
|
+
|
|
347
|
+
# Check if there are method restrictions
|
|
348
|
+
methods = ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD", "TRACE"]
|
|
349
|
+
method_matches = []
|
|
350
|
+
for method in methods:
|
|
351
|
+
if (
|
|
352
|
+
f"methods=['{method}']" in content[match.start() : match.start() + 200]
|
|
353
|
+
or f'methods=["{method}"]' in content[match.start() : match.start() + 200]
|
|
354
|
+
):
|
|
355
|
+
method_matches.append(method)
|
|
356
|
+
|
|
357
|
+
if method_matches:
|
|
358
|
+
method_str = ", ".join(method_matches)
|
|
359
|
+
else:
|
|
360
|
+
method_str = "GET" # Default for Flask routes
|
|
361
|
+
|
|
362
|
+
findings.append(
|
|
363
|
+
make_finding(
|
|
364
|
+
severity=Severity.INFO,
|
|
365
|
+
file=rel_path,
|
|
366
|
+
line_start=line_start,
|
|
367
|
+
title=f"Route: {method_str} {path} (Flask)",
|
|
368
|
+
description=f"Flask HTTP {method_str} route: {path}",
|
|
369
|
+
evidence=match.group(0)[:100],
|
|
370
|
+
)
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
# Django patterns - look for url patterns
|
|
374
|
+
django_patterns = [
|
|
375
|
+
r"url\s*\(\s*['\"]([^'\"]*?)['\"]\s*,\s*([a-zA-Z_][a-zA-Z0-9_]*[.])*([a-zA-Z_][a-zA-Z0-9_]*)",
|
|
376
|
+
r"path\s*\(\s*['\"]([^'\"]*?)['\"]\s*,\s*([a-zA-Z_][a-zA-Z0-9_]*[.])*([a-zA-Z_][a-zA-Z0-9_]*)",
|
|
377
|
+
]
|
|
378
|
+
|
|
379
|
+
for pattern in django_patterns:
|
|
380
|
+
matches = re.finditer(pattern, content, re.MULTILINE)
|
|
381
|
+
for match in matches:
|
|
382
|
+
path = match.group(1) if len(match.groups()) >= 1 else "dynamic"
|
|
383
|
+
handler = match.group(3) if len(match.groups()) >= 3 else "unknown"
|
|
384
|
+
line_start = content[: match.start()].count("\n") + 1
|
|
385
|
+
|
|
386
|
+
findings.append(
|
|
387
|
+
make_finding(
|
|
388
|
+
severity=Severity.INFO,
|
|
389
|
+
file=rel_path,
|
|
390
|
+
line_start=line_start,
|
|
391
|
+
title=f"Route: * {path} (Django) -> {handler}",
|
|
392
|
+
description=f"Django route: {path} handled by {handler}",
|
|
393
|
+
evidence=match.group(0)[:100],
|
|
394
|
+
)
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
return findings
|
|
398
|
+
|
|
399
|
+
def _detect_sensitive_routes(self, route_findings: list[Finding]) -> list[Finding]:
|
|
400
|
+
"""Detect unprotected sensitive routes like login/auth endpoints."""
|
|
401
|
+
findings: list[Finding] = []
|
|
402
|
+
|
|
403
|
+
# Only check actual route findings (not all findings)
|
|
404
|
+
for finding in route_findings:
|
|
405
|
+
# Extract route path from the finding title or description
|
|
406
|
+
# Look for patterns like "Route: METHOD /path (Framework)"
|
|
407
|
+
route_match = re.search(
|
|
408
|
+
r"Route:\s*(?:([A-Z]+(?:,\s*[A-Z]+)*)\s+)?(/[-_a-zA-Z0-9/{}:.*\[\]]+)",
|
|
409
|
+
finding.title,
|
|
410
|
+
re.IGNORECASE,
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
if route_match:
|
|
414
|
+
methods_part = route_match.group(1)
|
|
415
|
+
path = route_match.group(2).lower()
|
|
416
|
+
|
|
417
|
+
# Determine methods if not specified
|
|
418
|
+
methods = [m.strip() for m in methods_part.split(",")] if methods_part else ["GET"]
|
|
419
|
+
|
|
420
|
+
# Check if the path contains sensitive keywords
|
|
421
|
+
sensitive_keywords = [
|
|
422
|
+
"/auth",
|
|
423
|
+
"/login",
|
|
424
|
+
"/signin",
|
|
425
|
+
"/signup",
|
|
426
|
+
"/register",
|
|
427
|
+
"/password",
|
|
428
|
+
"/admin",
|
|
429
|
+
"/dashboard",
|
|
430
|
+
]
|
|
431
|
+
|
|
432
|
+
for keyword in sensitive_keywords:
|
|
433
|
+
if keyword in path:
|
|
434
|
+
for method in methods:
|
|
435
|
+
method_upper = method.strip().upper()
|
|
436
|
+
# Flag sensitive routes that use unsafe methods
|
|
437
|
+
if method_upper in {"POST", "PUT", "PATCH", "DELETE"}:
|
|
438
|
+
findings.append(
|
|
439
|
+
make_finding(
|
|
440
|
+
severity=Severity.MEDIUM,
|
|
441
|
+
finding_type="unprotected_sensitive_route",
|
|
442
|
+
file=finding.file,
|
|
443
|
+
line_start=finding.line,
|
|
444
|
+
title=f"Unprotected sensitive route: {method_upper} {path}",
|
|
445
|
+
description=f"Sensitive route {path} with potentially unsafe HTTP method {method_upper} appears unprotected",
|
|
446
|
+
evidence=finding.title,
|
|
447
|
+
)
|
|
448
|
+
)
|
|
449
|
+
break # Break to avoid duplicate checks for the same path
|
|
450
|
+
|
|
451
|
+
return findings
|
|
452
|
+
|
|
453
|
+
def _build_route_map(self, route_findings: list[Finding]) -> dict:
|
|
454
|
+
"""Build a map of routes for analysis."""
|
|
455
|
+
route_map = {}
|
|
456
|
+
for finding in route_findings:
|
|
457
|
+
route_match = re.search(
|
|
458
|
+
r"Route:\s*(?:([A-Z]+(?:,\s*[A-Z]+)*)\s+)?(/[-_a-zA-Z0-9/{}:.*\[\]]+)",
|
|
459
|
+
finding.title,
|
|
460
|
+
re.IGNORECASE,
|
|
461
|
+
)
|
|
462
|
+
if route_match:
|
|
463
|
+
methods_part = route_match.group(1)
|
|
464
|
+
path = route_match.group(2)
|
|
465
|
+
methods = [m.strip() for m in methods_part.split(",")] if methods_part else ["GET"]
|
|
466
|
+
|
|
467
|
+
route_map[path] = {
|
|
468
|
+
"methods": methods,
|
|
469
|
+
"file": finding.file,
|
|
470
|
+
"line": finding.line,
|
|
471
|
+
"framework": finding.title.split("(")[-1].split(")")[0]
|
|
472
|
+
if "(" in finding.title
|
|
473
|
+
else "unknown",
|
|
474
|
+
}
|
|
475
|
+
return route_map
|
|
476
|
+
|
|
477
|
+
def _detect_framework(self, inp: AgentInput) -> str:
|
|
478
|
+
"""Detect the web framework used in the project."""
|
|
479
|
+
# Look for framework-specific files
|
|
480
|
+
framework_indicators = {
|
|
481
|
+
"FastAPI": ["main.py", "app.py"],
|
|
482
|
+
"Flask": ["app.py", "application.py"],
|
|
483
|
+
"Django": ["manage.py", "settings.py"],
|
|
484
|
+
"Express.js": ["server.js", "app.js"],
|
|
485
|
+
"Next.js": ["next.config.js", "pages/", "app/"],
|
|
486
|
+
"Spring Boot": ["pom.xml", "build.gradle", "Application.java"],
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
for framework, indicators in framework_indicators.items():
|
|
490
|
+
for indicator in indicators:
|
|
491
|
+
if (inp.root / indicator).exists() or any(safe_rglob(inp.root, indicator)):
|
|
492
|
+
return framework
|
|
493
|
+
|
|
494
|
+
return "unknown"
|
|
495
|
+
|
|
496
|
+
def _scan_js_routes(self, content: str, rel_path: str) -> list[Finding]:
|
|
497
|
+
"""Scan JavaScript/TypeScript files for routes using tree-sitter AST."""
|
|
498
|
+
findings: list[Finding] = []
|
|
499
|
+
|
|
500
|
+
parser = get_parser(Lang.JAVASCRIPT)
|
|
501
|
+
if parser is None:
|
|
502
|
+
return self._scan_js_routes_regex(content, rel_path)
|
|
503
|
+
|
|
504
|
+
try:
|
|
505
|
+
tree = parse_source(Lang.JAVASCRIPT, content)
|
|
506
|
+
buf = content.encode("utf-8")
|
|
507
|
+
self._walk_js_route_node(tree.root_node, buf, content, rel_path, findings, None)
|
|
508
|
+
except Exception as e:
|
|
509
|
+
_log.warning("RouteGraphScanner._scan_js_routes failed: %s", e)
|
|
510
|
+
return self._scan_js_routes_regex(content, rel_path)
|
|
511
|
+
|
|
512
|
+
return findings
|
|
513
|
+
|
|
514
|
+
def _scan_js_routes_regex(self, content: str, rel_path: str) -> list[Finding]:
|
|
515
|
+
"""Regex fallback for JS/TS routes."""
|
|
516
|
+
findings = []
|
|
517
|
+
for pattern, framework in [
|
|
518
|
+
(
|
|
519
|
+
r'app\.(get|post|put|delete|patch|options|head)\s*\(\s*["\']([^"\']*)["\']',
|
|
520
|
+
"Express.js",
|
|
521
|
+
),
|
|
522
|
+
(
|
|
523
|
+
r'router\.(get|post|put|delete|patch|options|head)\s*\(\s*["\']([^"\']*)["\']',
|
|
524
|
+
"Express.js Router",
|
|
525
|
+
),
|
|
526
|
+
(
|
|
527
|
+
r'fastify\.(get|post|put|delete|patch|options|head)\s*\(\s*["\']([^"\']*)["\']',
|
|
528
|
+
"Fastify",
|
|
529
|
+
),
|
|
530
|
+
]:
|
|
531
|
+
for m in re.finditer(pattern, content, re.MULTILINE):
|
|
532
|
+
findings.append(
|
|
533
|
+
self._make_route_finding(
|
|
534
|
+
rel_path, content, m, m.group(1).upper(), m.group(2), framework
|
|
535
|
+
)
|
|
536
|
+
)
|
|
537
|
+
return findings
|
|
538
|
+
|
|
539
|
+
def _walk_js_route_node(
|
|
540
|
+
self,
|
|
541
|
+
node: Any,
|
|
542
|
+
buf: bytes,
|
|
543
|
+
content: str,
|
|
544
|
+
rel_path: str,
|
|
545
|
+
findings: list[Finding],
|
|
546
|
+
current_obj: str | None,
|
|
547
|
+
) -> None:
|
|
548
|
+
ntype = node.type
|
|
549
|
+
|
|
550
|
+
if ntype == "call_expression":
|
|
551
|
+
method = ""
|
|
552
|
+
path = ""
|
|
553
|
+
obj = ""
|
|
554
|
+
|
|
555
|
+
for child in node.children:
|
|
556
|
+
if child.type == "member_expression":
|
|
557
|
+
for sub in child.children:
|
|
558
|
+
if sub.type == "identifier":
|
|
559
|
+
obj = (
|
|
560
|
+
buf[sub.start_byte : sub.end_byte].decode()
|
|
561
|
+
if hasattr(sub, "start_byte")
|
|
562
|
+
else ""
|
|
563
|
+
)
|
|
564
|
+
elif sub.type == "property_identifier":
|
|
565
|
+
method = (
|
|
566
|
+
buf[sub.start_byte : sub.end_byte].decode()
|
|
567
|
+
if hasattr(sub, "start_byte")
|
|
568
|
+
else ""
|
|
569
|
+
)
|
|
570
|
+
elif child.type == "arguments":
|
|
571
|
+
for arg in child.children:
|
|
572
|
+
if arg.type == "string":
|
|
573
|
+
path = (
|
|
574
|
+
buf[arg.start_byte : arg.end_byte].decode()
|
|
575
|
+
if hasattr(arg, "start_byte")
|
|
576
|
+
else ""
|
|
577
|
+
)
|
|
578
|
+
path = path.strip("'\"")
|
|
579
|
+
break
|
|
580
|
+
|
|
581
|
+
if method.lower() in ("get", "post", "put", "delete", "patch", "options", "head"):
|
|
582
|
+
if obj in ("app", "router", "fastify"):
|
|
583
|
+
framework = {
|
|
584
|
+
"app": "Express.js",
|
|
585
|
+
"router": "Express.js Router",
|
|
586
|
+
"fastify": "Fastify",
|
|
587
|
+
}.get(obj, "")
|
|
588
|
+
findings.append(
|
|
589
|
+
self._make_route_finding(
|
|
590
|
+
rel_path,
|
|
591
|
+
content,
|
|
592
|
+
None,
|
|
593
|
+
method.upper(),
|
|
594
|
+
path,
|
|
595
|
+
framework,
|
|
596
|
+
line=node.start_point[0] + 1 if node.start_point else 0,
|
|
597
|
+
)
|
|
598
|
+
)
|
|
599
|
+
elif method.lower() == "get" and obj:
|
|
600
|
+
# Could be a route declaration
|
|
601
|
+
pass
|
|
602
|
+
|
|
603
|
+
for child in node.children:
|
|
604
|
+
self._walk_js_route_node(child, buf, content, rel_path, findings, current_obj)
|
|
605
|
+
|
|
606
|
+
def _make_route_finding(
|
|
607
|
+
self,
|
|
608
|
+
rel_path: str,
|
|
609
|
+
content: str,
|
|
610
|
+
match: Any | None,
|
|
611
|
+
method: str,
|
|
612
|
+
path: str,
|
|
613
|
+
framework: str,
|
|
614
|
+
line: int = 0,
|
|
615
|
+
) -> Finding:
|
|
616
|
+
if match:
|
|
617
|
+
line = content[: match.start()].count("\n") + 1
|
|
618
|
+
evidence = f"{method} {path}"
|
|
619
|
+
return make_finding(
|
|
620
|
+
severity=Severity.INFO,
|
|
621
|
+
file=rel_path,
|
|
622
|
+
line_start=line,
|
|
623
|
+
title=f"Route: {method} {path} ({framework})",
|
|
624
|
+
description=f"HTTP {method} route: {path}",
|
|
625
|
+
evidence=evidence,
|
|
626
|
+
)
|
|
627
|
+
|
|
628
|
+
def _scan_java_routes(self, content: str, rel_path: str) -> list[Finding]:
|
|
629
|
+
"""Scan Java files for routes (Spring Boot) using tree-sitter AST."""
|
|
630
|
+
findings: list[Finding] = []
|
|
631
|
+
|
|
632
|
+
parser = get_parser(Lang.JAVA)
|
|
633
|
+
if parser is None:
|
|
634
|
+
return self._scan_java_routes_regex(content, rel_path)
|
|
635
|
+
|
|
636
|
+
try:
|
|
637
|
+
tree = parse_source(Lang.JAVA, content)
|
|
638
|
+
buf = content.encode("utf-8")
|
|
639
|
+
self._walk_java_route_node(tree.root_node, buf, rel_path, findings)
|
|
640
|
+
except Exception as e:
|
|
641
|
+
_log.warning("RouteGraphScanner._scan_java_routes failed: %s", e)
|
|
642
|
+
return self._scan_java_routes_regex(content, rel_path)
|
|
643
|
+
|
|
644
|
+
return findings
|
|
645
|
+
|
|
646
|
+
def _scan_java_routes_regex(self, content: str, rel_path: str) -> list[Finding]:
|
|
647
|
+
"""Regex fallback for Java routes."""
|
|
648
|
+
findings = []
|
|
649
|
+
for m in re.finditer(
|
|
650
|
+
r'@(GetMapping|PostMapping|PutMapping|DeleteMapping|PatchMapping|RequestMapping)\s*\(\s*["\']([^"\']*)["\']',
|
|
651
|
+
content,
|
|
652
|
+
):
|
|
653
|
+
annotation = m.group(1).replace("Mapping", "").upper()
|
|
654
|
+
if annotation == "REQUEST":
|
|
655
|
+
annotation = "GET"
|
|
656
|
+
findings.append(
|
|
657
|
+
self._make_route_finding(
|
|
658
|
+
rel_path, content, m, annotation, m.group(2), "Spring Boot"
|
|
659
|
+
)
|
|
660
|
+
)
|
|
661
|
+
return findings
|
|
662
|
+
|
|
663
|
+
def _walk_java_route_node(
|
|
664
|
+
self, node: Any, buf: bytes, rel_path: str, findings: list[Finding]
|
|
665
|
+
) -> None:
|
|
666
|
+
ntype = node.type
|
|
667
|
+
|
|
668
|
+
if ntype == "marker_annotation":
|
|
669
|
+
ann_text = _node_text_buf(node, buf)
|
|
670
|
+
route_methods = {
|
|
671
|
+
"GetMapping": "GET",
|
|
672
|
+
"PostMapping": "POST",
|
|
673
|
+
"PutMapping": "PUT",
|
|
674
|
+
"DeleteMapping": "DELETE",
|
|
675
|
+
"PatchMapping": "PATCH",
|
|
676
|
+
}
|
|
677
|
+
for ann_name, method in route_methods.items():
|
|
678
|
+
if ann_text.startswith("@" + ann_name):
|
|
679
|
+
path = ""
|
|
680
|
+
for child in node.children:
|
|
681
|
+
if hasattr(child, "type") and child.type == "annotation_argument_list":
|
|
682
|
+
_node_text_buf(child, buf)
|
|
683
|
+
for arg in child.children:
|
|
684
|
+
if arg.type == "string":
|
|
685
|
+
p = _node_text_buf(arg, buf).strip("\"'")
|
|
686
|
+
if p:
|
|
687
|
+
path = p
|
|
688
|
+
break
|
|
689
|
+
findings.append(
|
|
690
|
+
self._make_route_finding(
|
|
691
|
+
rel_path,
|
|
692
|
+
"",
|
|
693
|
+
None,
|
|
694
|
+
method,
|
|
695
|
+
path or "/",
|
|
696
|
+
"Spring Boot",
|
|
697
|
+
line=node.start_point[0] + 1,
|
|
698
|
+
)
|
|
699
|
+
)
|
|
700
|
+
return
|
|
701
|
+
# Handle @RequestMapping with value=
|
|
702
|
+
if ann_text.startswith("@RequestMapping"):
|
|
703
|
+
path = ""
|
|
704
|
+
for child in node.children:
|
|
705
|
+
if hasattr(child, "type") and child.type == "annotation_argument_list":
|
|
706
|
+
for arg in child.children:
|
|
707
|
+
if arg.type == "string":
|
|
708
|
+
p = _node_text_buf(arg, buf).strip("\"'")
|
|
709
|
+
if p:
|
|
710
|
+
path = p
|
|
711
|
+
break
|
|
712
|
+
findings.append(
|
|
713
|
+
self._make_route_finding(
|
|
714
|
+
rel_path,
|
|
715
|
+
"",
|
|
716
|
+
None,
|
|
717
|
+
"GET",
|
|
718
|
+
path or "/",
|
|
719
|
+
"Spring Boot",
|
|
720
|
+
line=node.start_point[0] + 1,
|
|
721
|
+
)
|
|
722
|
+
)
|
|
723
|
+
|
|
724
|
+
for child in node.children if hasattr(node, "children") else []:
|
|
725
|
+
self._walk_java_route_node(child, buf, rel_path, findings)
|
|
726
|
+
|
|
727
|
+
def _scan_ruby_routes(self, content: str, rel_path: str) -> list[Finding]:
|
|
728
|
+
"""Scan Ruby files for routes (Rails) using tree-sitter AST."""
|
|
729
|
+
findings: list[Finding] = []
|
|
730
|
+
|
|
731
|
+
parser = get_parser(Lang.RUBY)
|
|
732
|
+
if parser is None:
|
|
733
|
+
return self._scan_ruby_routes_regex(content, rel_path)
|
|
734
|
+
|
|
735
|
+
try:
|
|
736
|
+
tree = parse_source(Lang.RUBY, content)
|
|
737
|
+
buf = content.encode("utf-8")
|
|
738
|
+
self._walk_ruby_route_node(tree.root_node, buf, rel_path, findings)
|
|
739
|
+
except Exception as e:
|
|
740
|
+
_log.warning("RouteGraphScanner._scan_ruby_routes failed: %s", e)
|
|
741
|
+
return self._scan_ruby_routes_regex(content, rel_path)
|
|
742
|
+
|
|
743
|
+
return findings
|
|
744
|
+
|
|
745
|
+
def _scan_ruby_routes_regex(self, content: str, rel_path: str) -> list[Finding]:
|
|
746
|
+
"""Regex fallback for Ruby routes."""
|
|
747
|
+
findings = []
|
|
748
|
+
for m in re.finditer(r'(get|post|put|patch|delete)\s+["\']([^"\']*)["\']', content):
|
|
749
|
+
findings.append(
|
|
750
|
+
self._make_route_finding(
|
|
751
|
+
rel_path, content, m, m.group(1).upper(), m.group(2), "Rails"
|
|
752
|
+
)
|
|
753
|
+
)
|
|
754
|
+
for m in re.finditer(r"resources\s+:(\w+)", content):
|
|
755
|
+
findings.append(
|
|
756
|
+
self._make_route_finding(rel_path, content, m, "RESOURCE", m.group(1), "Rails")
|
|
757
|
+
)
|
|
758
|
+
return findings
|
|
759
|
+
|
|
760
|
+
def _walk_ruby_route_node(
|
|
761
|
+
self, node: Any, buf: bytes, rel_path: str, findings: list[Finding]
|
|
762
|
+
) -> None:
|
|
763
|
+
ntype = node.type
|
|
764
|
+
if ntype == "call":
|
|
765
|
+
method_name = ""
|
|
766
|
+
for child in node.children:
|
|
767
|
+
if child.type == "identifier":
|
|
768
|
+
method_name = _node_text_buf(child, buf)
|
|
769
|
+
break
|
|
770
|
+
route_methods = {"get", "post", "put", "patch", "delete", "resources"}
|
|
771
|
+
if method_name.lower() in route_methods:
|
|
772
|
+
path = ""
|
|
773
|
+
for child in node.children:
|
|
774
|
+
if child.type == "argument_list":
|
|
775
|
+
for arg in child.children:
|
|
776
|
+
if arg.type == "simple_symbol":
|
|
777
|
+
path = _node_text_buf(arg, buf)
|
|
778
|
+
if path.startswith(":"):
|
|
779
|
+
path = path[1:]
|
|
780
|
+
break
|
|
781
|
+
elif arg.type == "string":
|
|
782
|
+
path = _node_text_buf(arg, buf).strip("\"'")
|
|
783
|
+
break
|
|
784
|
+
if path:
|
|
785
|
+
method_display = (
|
|
786
|
+
method_name.upper() if method_name.lower() != "resources" else "RESOURCE"
|
|
787
|
+
)
|
|
788
|
+
findings.append(
|
|
789
|
+
self._make_route_finding(
|
|
790
|
+
rel_path,
|
|
791
|
+
"",
|
|
792
|
+
None,
|
|
793
|
+
method_display,
|
|
794
|
+
path,
|
|
795
|
+
"Rails",
|
|
796
|
+
line=node.start_point[0] + 1,
|
|
797
|
+
)
|
|
798
|
+
)
|
|
799
|
+
|
|
800
|
+
for child in node.children if hasattr(node, "children") else []:
|
|
801
|
+
self._walk_ruby_route_node(child, buf, rel_path, findings)
|
|
802
|
+
|
|
803
|
+
def _scan_php_routes(self, content: str, rel_path: str) -> list[Finding]:
|
|
804
|
+
"""Scan PHP files for routes (Laravel) using tree-sitter AST."""
|
|
805
|
+
findings: list[Finding] = []
|
|
806
|
+
|
|
807
|
+
parser = get_parser(Lang.PHP)
|
|
808
|
+
if parser is None:
|
|
809
|
+
return self._scan_php_routes_regex(content, rel_path)
|
|
810
|
+
|
|
811
|
+
try:
|
|
812
|
+
tree = parse_source(Lang.PHP, content)
|
|
813
|
+
buf = content.encode("utf-8")
|
|
814
|
+
self._walk_php_route_node(tree.root_node, buf, rel_path, findings)
|
|
815
|
+
except Exception as e:
|
|
816
|
+
_log.warning("RouteGraphScanner._scan_php_routes failed: %s", e)
|
|
817
|
+
return self._scan_php_routes_regex(content, rel_path)
|
|
818
|
+
|
|
819
|
+
return findings
|
|
820
|
+
|
|
821
|
+
def _scan_php_routes_regex(self, content: str, rel_path: str) -> list[Finding]:
|
|
822
|
+
"""Regex fallback for PHP routes."""
|
|
823
|
+
findings = []
|
|
824
|
+
for m in re.finditer(
|
|
825
|
+
r"Route::(get|post|put|patch|delete)\s*\(\s*['\"]([^'\"]*)['\"]\s*,", content
|
|
826
|
+
):
|
|
827
|
+
findings.append(
|
|
828
|
+
self._make_route_finding(
|
|
829
|
+
rel_path, content, m, m.group(1).upper(), m.group(2), "Laravel"
|
|
830
|
+
)
|
|
831
|
+
)
|
|
832
|
+
return findings
|
|
833
|
+
|
|
834
|
+
def _walk_php_route_node(
|
|
835
|
+
self, node: Any, buf: bytes, rel_path: str, findings: list[Finding]
|
|
836
|
+
) -> None:
|
|
837
|
+
ntype = node.type
|
|
838
|
+
if ntype == "function_call_expression" or ntype == "scoped_call_expression":
|
|
839
|
+
call_text = _node_text_buf(node, buf)
|
|
840
|
+
route_match = __import__("re").match(
|
|
841
|
+
r"Route::(get|post|put|patch|delete)\s*\(", call_text
|
|
842
|
+
)
|
|
843
|
+
if route_match:
|
|
844
|
+
method = route_match.group(1).upper()
|
|
845
|
+
path = ""
|
|
846
|
+
for child in node.children if hasattr(node, "children") else []:
|
|
847
|
+
if child.type == "arguments" or child.type == "argument_list":
|
|
848
|
+
for arg in child.children if hasattr(child, "children") else []:
|
|
849
|
+
if arg.type in ("encapsed_string", "string"):
|
|
850
|
+
path = _node_text_buf(arg, buf).strip("'\"")
|
|
851
|
+
break
|
|
852
|
+
break
|
|
853
|
+
if path:
|
|
854
|
+
findings.append(
|
|
855
|
+
self._make_route_finding(
|
|
856
|
+
rel_path,
|
|
857
|
+
"",
|
|
858
|
+
None,
|
|
859
|
+
method,
|
|
860
|
+
path,
|
|
861
|
+
"Laravel",
|
|
862
|
+
line=node.start_point[0] + 1,
|
|
863
|
+
)
|
|
864
|
+
)
|
|
865
|
+
|
|
866
|
+
for child in node.children if hasattr(node, "children") else []:
|
|
867
|
+
self._walk_php_route_node(child, buf, rel_path, findings)
|
|
868
|
+
|
|
869
|
+
def _scan_cs_routes(self, content: str, rel_path: str) -> list[Finding]:
|
|
870
|
+
"""Scan C# files for routes (ASP.NET Core) using tree-sitter AST."""
|
|
871
|
+
findings: list[Finding] = []
|
|
872
|
+
|
|
873
|
+
parser = get_parser(Lang.C_SHARP)
|
|
874
|
+
if parser is None:
|
|
875
|
+
return self._scan_cs_routes_regex(content, rel_path)
|
|
876
|
+
|
|
877
|
+
try:
|
|
878
|
+
tree = parse_source(Lang.C_SHARP, content)
|
|
879
|
+
buf = content.encode("utf-8")
|
|
880
|
+
self._walk_cs_route_node(tree.root_node, buf, rel_path, findings)
|
|
881
|
+
except Exception as e:
|
|
882
|
+
_log.warning("RouteGraphScanner._scan_cs_routes failed: %s", e)
|
|
883
|
+
return self._scan_cs_routes_regex(content, rel_path)
|
|
884
|
+
|
|
885
|
+
return findings
|
|
886
|
+
|
|
887
|
+
def _scan_cs_routes_regex(self, content: str, rel_path: str) -> list[Finding]:
|
|
888
|
+
"""Regex fallback for C# routes."""
|
|
889
|
+
findings = []
|
|
890
|
+
for pattern, method in [
|
|
891
|
+
(r'\[HttpGet\(["\']([^"\']*)["\']\s*\)\]', "GET"),
|
|
892
|
+
(r'\[HttpPost\(["\']([^"\']*)["\']\s*\)\]', "POST"),
|
|
893
|
+
(r'\[HttpPut\(["\']([^"\']*)["\']\s*\)\]', "PUT"),
|
|
894
|
+
(r'\[HttpDelete\(["\']([^"\']*)["\']\s*\)\]', "DELETE"),
|
|
895
|
+
(r'\[Route\(["\']([^"\']*)["\']\s*\)\]', "ROUTE"),
|
|
896
|
+
]:
|
|
897
|
+
for m in re.finditer(pattern, content):
|
|
898
|
+
findings.append(
|
|
899
|
+
self._make_route_finding(
|
|
900
|
+
rel_path, content, m, method, m.group(1), "ASP.NET Core"
|
|
901
|
+
)
|
|
902
|
+
)
|
|
903
|
+
return findings
|
|
904
|
+
|
|
905
|
+
def _walk_cs_route_node(
|
|
906
|
+
self, node: Any, buf: bytes, rel_path: str, findings: list[Finding]
|
|
907
|
+
) -> None:
|
|
908
|
+
ntype = node.type
|
|
909
|
+
if ntype == "attribute":
|
|
910
|
+
attr_text = _node_text_buf(node, buf)
|
|
911
|
+
route_methods = {
|
|
912
|
+
"HttpGet": "GET",
|
|
913
|
+
"HttpPost": "POST",
|
|
914
|
+
"HttpPut": "PUT",
|
|
915
|
+
"HttpDelete": "DELETE",
|
|
916
|
+
"Route": "ROUTE",
|
|
917
|
+
}
|
|
918
|
+
for attr_name, method in route_methods.items():
|
|
919
|
+
if attr_text.startswith("[" + attr_name) or attr_text.startswith(attr_name):
|
|
920
|
+
path = ""
|
|
921
|
+
for child in node.children if hasattr(node, "children") else []:
|
|
922
|
+
if child.type == "attribute_argument_list":
|
|
923
|
+
for arg in child.children if hasattr(child, "children") else []:
|
|
924
|
+
if arg.type == "string_literal":
|
|
925
|
+
path = _node_text_buf(arg, buf).strip("\"'")
|
|
926
|
+
break
|
|
927
|
+
break
|
|
928
|
+
findings.append(
|
|
929
|
+
self._make_route_finding(
|
|
930
|
+
rel_path,
|
|
931
|
+
"",
|
|
932
|
+
None,
|
|
933
|
+
method,
|
|
934
|
+
path or "/",
|
|
935
|
+
"ASP.NET Core",
|
|
936
|
+
line=node.start_point[0] + 1,
|
|
937
|
+
)
|
|
938
|
+
)
|
|
939
|
+
return
|
|
940
|
+
|
|
941
|
+
for child in node.children if hasattr(node, "children") else []:
|
|
942
|
+
self._walk_cs_route_node(child, buf, rel_path, findings)
|
|
943
|
+
|
|
944
|
+
|
|
945
|
+
def _node_text_buf(node: Any, buf: bytes) -> str:
|
|
946
|
+
"""Extract text from a tree-sitter node using byte buffer."""
|
|
947
|
+
try:
|
|
948
|
+
return buf[node.start_byte : node.end_byte].decode("utf-8", errors="replace")
|
|
949
|
+
except Exception as e:
|
|
950
|
+
_log.debug("_node_text_buf failed: %s", e)
|
|
951
|
+
return ""
|