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,245 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SessionManagementAgent — Session management vulnerability detection.
|
|
3
|
+
|
|
4
|
+
Detects:
|
|
5
|
+
- Session fixation (no regeneration on login)
|
|
6
|
+
- Weak session ID generation (user-controlled or predictable)
|
|
7
|
+
- Missing/insecure cookie flags (HttpOnly, Secure, SameSite)
|
|
8
|
+
- Session timeout not configured
|
|
9
|
+
- Concurrent session control missing
|
|
10
|
+
- Session in URL parameters
|
|
11
|
+
- Missing session invalidation on logout
|
|
12
|
+
- Weak session storage (localStorage instead of httpOnly cookies)
|
|
13
|
+
|
|
14
|
+
Uses tree-sitter AST for call/identifier detection where beneficial, with
|
|
15
|
+
high-quality literal-pattern checks for configuration flags. NOT AI.
|
|
16
|
+
Does NOT write to disk. Does NOT touch the queue.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import os
|
|
22
|
+
import re
|
|
23
|
+
|
|
24
|
+
from ..agents.base import (
|
|
25
|
+
AgentDomain,
|
|
26
|
+
AgentGroup,
|
|
27
|
+
AgentInput,
|
|
28
|
+
AgentResult,
|
|
29
|
+
BaseAgent,
|
|
30
|
+
Finding,
|
|
31
|
+
Severity,
|
|
32
|
+
register,
|
|
33
|
+
safe_rglob,
|
|
34
|
+
)
|
|
35
|
+
from ..brain.ast_utils import find_calls
|
|
36
|
+
from ..brain.languages import EXTENSION_MAP, Lang
|
|
37
|
+
|
|
38
|
+
# Call names that set a session cookie (used for cookie-flag checks).
|
|
39
|
+
_COOKIE_SETTER_CALLS = {
|
|
40
|
+
"set_cookie",
|
|
41
|
+
"setcookie",
|
|
42
|
+
"cookie.set",
|
|
43
|
+
"cookies.set",
|
|
44
|
+
"session.cookie",
|
|
45
|
+
"response.set_cookie",
|
|
46
|
+
"res.cookie",
|
|
47
|
+
"ctx.cookie",
|
|
48
|
+
}
|
|
49
|
+
# Literal header / config forms that also set cookies.
|
|
50
|
+
_COOKIE_SETTER_LITERALS = re.compile(
|
|
51
|
+
r"(?i)\b(?:Set-Cookie|set_cookie|session\.cookie|cookie\.set)\b"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@register
|
|
56
|
+
class SessionManagementAgent(BaseAgent):
|
|
57
|
+
"""Detects session management vulnerabilities."""
|
|
58
|
+
|
|
59
|
+
group = AgentGroup.SECURITY
|
|
60
|
+
domain = AgentDomain.SECURITY
|
|
61
|
+
name = "SessionManagementAgent"
|
|
62
|
+
description = (
|
|
63
|
+
"Session fixation, weak session IDs, missing cookie flags, insecure session storage"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def _run(self, inp: AgentInput, result: AgentResult) -> None:
|
|
67
|
+
patterns = ["*.py", "*.js", "*.ts", "*.jsx", "*.tsx", "*.java", "*.rb", "*.php", "*.go"]
|
|
68
|
+
|
|
69
|
+
for pat in patterns:
|
|
70
|
+
for fpath in safe_rglob(inp.root, pat):
|
|
71
|
+
if not fpath.is_file():
|
|
72
|
+
continue
|
|
73
|
+
rel = fpath.relative_to(inp.root).as_posix()
|
|
74
|
+
try:
|
|
75
|
+
content = fpath.read_text(encoding="utf-8", errors="ignore")
|
|
76
|
+
except OSError:
|
|
77
|
+
continue
|
|
78
|
+
result.files_scanned += 1
|
|
79
|
+
ext = os.path.splitext(rel)[1].lower()
|
|
80
|
+
lang = EXTENSION_MAP.get(ext)
|
|
81
|
+
self._scan_session(content, rel, result, lang)
|
|
82
|
+
|
|
83
|
+
# ── Helpers ─────────────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
def _calls(self, content: str, lang: Lang | None, names: set[str]) -> list[dict]:
|
|
86
|
+
if lang is None:
|
|
87
|
+
return []
|
|
88
|
+
return find_calls(content, lang, names)
|
|
89
|
+
|
|
90
|
+
def _offset_of_line(self, content: str, line: int) -> int:
|
|
91
|
+
lines = content.splitlines()
|
|
92
|
+
offset = 0
|
|
93
|
+
for ln in lines[: max(0, line - 1)]:
|
|
94
|
+
offset += len(ln) + 1
|
|
95
|
+
return offset
|
|
96
|
+
|
|
97
|
+
# ── Scan ────────────────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
def _scan_session(self, content: str, rel: str, result: AgentResult, lang: Lang | None) -> None:
|
|
100
|
+
# ── Session fixation — no regeneration on login ──────────────────────
|
|
101
|
+
login_match = re.search(r"(?i)(?:login|signin|authenticate)\s*\(", content)
|
|
102
|
+
if login_match:
|
|
103
|
+
login_line = content[: login_match.start()].count("\n") + 1
|
|
104
|
+
surrounding = content[login_match.start() : login_match.start() + 800]
|
|
105
|
+
if re.search(r'(?i)\bsession\[["\']?(?:user_id|user|username|email)\b', surrounding):
|
|
106
|
+
if not re.search(
|
|
107
|
+
r"(?i)(?:regenerate|rotate|regenerate_id|session\.new)", surrounding
|
|
108
|
+
):
|
|
109
|
+
result.add_finding(
|
|
110
|
+
Finding(
|
|
111
|
+
agent=self.name,
|
|
112
|
+
type="session_fixation",
|
|
113
|
+
severity=Severity.HIGH,
|
|
114
|
+
file=rel,
|
|
115
|
+
line=login_line,
|
|
116
|
+
message="Session not regenerated on login — vulnerable to session fixation",
|
|
117
|
+
suggestion="Call session.regenerate() or rotate session ID after successful login",
|
|
118
|
+
cwe="CWE-384",
|
|
119
|
+
extra={"skill": "owasp-top10-web.skill"},
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# ── Cookie flags missing / insecure ──────────────────────────────────
|
|
124
|
+
has_cookie_setter = bool(self._calls(content, lang, _COOKIE_SETTER_CALLS)) or bool(
|
|
125
|
+
_COOKIE_SETTER_LITERALS.search(content)
|
|
126
|
+
)
|
|
127
|
+
if has_cookie_setter:
|
|
128
|
+
if not re.search(r"(?i)\b(?:HttpOnly|httponly|http_only)\b", content):
|
|
129
|
+
result.add_finding(
|
|
130
|
+
Finding(
|
|
131
|
+
agent=self.name,
|
|
132
|
+
type="missing_httponly",
|
|
133
|
+
severity=Severity.MEDIUM,
|
|
134
|
+
file=rel,
|
|
135
|
+
line=0,
|
|
136
|
+
message="HTTP-only cookie flag not set — session ID accessible to JavaScript",
|
|
137
|
+
suggestion="Set HttpOnly flag on session cookies",
|
|
138
|
+
cwe="CWE-1004",
|
|
139
|
+
extra={"skill": "owasp-top10-web.skill"},
|
|
140
|
+
)
|
|
141
|
+
)
|
|
142
|
+
if not re.search(r"\bSecure\b", content, re.IGNORECASE):
|
|
143
|
+
result.add_finding(
|
|
144
|
+
Finding(
|
|
145
|
+
agent=self.name,
|
|
146
|
+
type="missing_secure_flag",
|
|
147
|
+
severity=Severity.MEDIUM,
|
|
148
|
+
file=rel,
|
|
149
|
+
line=0,
|
|
150
|
+
message="Secure cookie flag not set — session cookies sent over HTTP",
|
|
151
|
+
suggestion="Set Secure flag on session cookies for HTTPS-only transmission",
|
|
152
|
+
cwe="CWE-614",
|
|
153
|
+
extra={"skill": "owasp-top10-web.skill"},
|
|
154
|
+
)
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# ── Session in URL parameter ────────────────────────────────────────
|
|
158
|
+
for m in re.finditer(r"(?i)\b(?:session|sid|session_id|token)\s*=\s*\{?\w+", content):
|
|
159
|
+
line = content[: m.start()].count("\n") + 1
|
|
160
|
+
if m.group(0).startswith(("session=", "sid=", "session_id=", "token=")):
|
|
161
|
+
result.add_finding(
|
|
162
|
+
Finding(
|
|
163
|
+
agent=self.name,
|
|
164
|
+
type="session_in_url",
|
|
165
|
+
severity=Severity.HIGH,
|
|
166
|
+
file=rel,
|
|
167
|
+
line=line,
|
|
168
|
+
message="Session identifier in URL parameter — exposed in referrer headers and logs",
|
|
169
|
+
suggestion="Move session ID to httpOnly cookies, never in URL",
|
|
170
|
+
cwe="CWE-598",
|
|
171
|
+
extra={"skill": "owasp-top10-web.skill"},
|
|
172
|
+
)
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
# ── Weak session storage (localStorage) ────────────────────────────
|
|
176
|
+
for call in self._calls(content, lang, {"setItem"}):
|
|
177
|
+
full = call.get("full_text", "")
|
|
178
|
+
if re.search(r"(?i)(?:token|session|jwt|access_token)", full):
|
|
179
|
+
result.add_finding(
|
|
180
|
+
Finding(
|
|
181
|
+
agent=self.name,
|
|
182
|
+
type="insecure_session_storage",
|
|
183
|
+
severity=Severity.CRITICAL,
|
|
184
|
+
file=rel,
|
|
185
|
+
line=call.get("line", 0),
|
|
186
|
+
message="Session/token stored in localStorage — accessible to XSS attacks",
|
|
187
|
+
suggestion="Use httpOnly cookies for session tokens instead of localStorage",
|
|
188
|
+
cwe="CWE-312",
|
|
189
|
+
extra={"skill": "owasp-top10-web.skill"},
|
|
190
|
+
)
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
# ── No session timeout / expiry ─────────────────────────────────────
|
|
194
|
+
if re.search(
|
|
195
|
+
r"(?i)\b(?:session|token)\b.*\b(?:expire|timeout|ttl|max_age|maxAge)\b", content
|
|
196
|
+
):
|
|
197
|
+
pass # OK — has expiry configuration
|
|
198
|
+
elif re.search(r"(?i)\bsession\.(?:start|init|create|new)\s*\(", content):
|
|
199
|
+
result.add_finding(
|
|
200
|
+
Finding(
|
|
201
|
+
agent=self.name,
|
|
202
|
+
type="missing_session_timeout",
|
|
203
|
+
severity=Severity.LOW,
|
|
204
|
+
file=rel,
|
|
205
|
+
line=0,
|
|
206
|
+
message="Session created without explicit timeout/expiry configuration",
|
|
207
|
+
suggestion="Configure session TTL / max-age (recommended: 30-60 minutes)",
|
|
208
|
+
cwe="CWE-613",
|
|
209
|
+
extra={"skill": "owasp-top10-web.skill"},
|
|
210
|
+
)
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
# ── Missing logout / invalidation ───────────────────────────────────
|
|
214
|
+
if re.search(r"(?i)\bsession\.(?:destroy|invalidate|clear|delete)\b", content):
|
|
215
|
+
pass # OK — logout properly invalidates
|
|
216
|
+
elif re.search(r"(?i)(?:def\s+logout|function\s+logout|logout\s*=\s*function)", content):
|
|
217
|
+
result.add_finding(
|
|
218
|
+
Finding(
|
|
219
|
+
agent=self.name,
|
|
220
|
+
type="missing_session_invalidation",
|
|
221
|
+
severity=Severity.MEDIUM,
|
|
222
|
+
file=rel,
|
|
223
|
+
line=0,
|
|
224
|
+
message="Logout function present but session invalidation may be missing",
|
|
225
|
+
suggestion="Call session.destroy() or session.clear() on logout",
|
|
226
|
+
cwe="CWE-613",
|
|
227
|
+
extra={"skill": "owasp-top10-web.skill"},
|
|
228
|
+
)
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
# ── Weak session ID via user-controlled input ──────────────────────
|
|
232
|
+
for m in re.finditer(r"(?i)\bsession_id\s*=\s*(?:request\.|req\.|params\.|input)", content):
|
|
233
|
+
result.add_finding(
|
|
234
|
+
Finding(
|
|
235
|
+
agent=self.name,
|
|
236
|
+
type="user_controlled_session_id",
|
|
237
|
+
severity=Severity.CRITICAL,
|
|
238
|
+
file=rel,
|
|
239
|
+
line=content[: m.start()].count("\n") + 1,
|
|
240
|
+
message="Session ID derived from user-controlled input — session hijacking risk",
|
|
241
|
+
suggestion="Use server-generated cryptographically random session IDs",
|
|
242
|
+
cwe="CWE-384",
|
|
243
|
+
extra={"skill": "owasp-top10-web.skill"},
|
|
244
|
+
)
|
|
245
|
+
)
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SSRFProtectionAgent — Server-Side Request Forgery protection analysis.
|
|
3
|
+
|
|
4
|
+
Scans for:
|
|
5
|
+
- Unvalidated URL/user-input passed to HTTP requests
|
|
6
|
+
- HTTP client calls with user-controlled URLs
|
|
7
|
+
- SSRF via redirect following
|
|
8
|
+
- Internal network access from user input
|
|
9
|
+
- File:// and other protocol handlers
|
|
10
|
+
- Missing URL validation/sanitization
|
|
11
|
+
- DNS rebinding potential
|
|
12
|
+
- Cloud metadata endpoint access (169.254.169.254)
|
|
13
|
+
|
|
14
|
+
Uses AST via ast_utils for HTTP client detection;
|
|
15
|
+
regex for literal text patterns (IPs, protocols, URL construction).
|
|
16
|
+
Does NOT call AI. Does NOT write to disk. Does NOT touch the queue.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import ipaddress
|
|
22
|
+
import logging
|
|
23
|
+
import os
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from urllib.parse import urlsplit
|
|
26
|
+
|
|
27
|
+
from ..agents.base import (
|
|
28
|
+
AgentDomain,
|
|
29
|
+
AgentGroup,
|
|
30
|
+
AgentInput,
|
|
31
|
+
AgentResult,
|
|
32
|
+
AgentStatus,
|
|
33
|
+
BaseAgent,
|
|
34
|
+
Finding,
|
|
35
|
+
Severity,
|
|
36
|
+
make_finding,
|
|
37
|
+
register,
|
|
38
|
+
)
|
|
39
|
+
from ..brain.ast_utils import HTTP_CLIENTS, find_assignments, find_calls, get_call_arg
|
|
40
|
+
from ..brain.code_query import string_literals
|
|
41
|
+
from ..brain.languages import DEFAULT_IGNORE_DIRS, Lang, detect_language
|
|
42
|
+
from ..brain.trace_log import trace_agent
|
|
43
|
+
from .pattern_loader import (
|
|
44
|
+
get_ssrf_internal_network_patterns,
|
|
45
|
+
get_ssrf_metadata_patterns,
|
|
46
|
+
get_ssrf_protocol_patterns,
|
|
47
|
+
load_patterns,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# Load YAML patterns once at module level
|
|
51
|
+
_SSRF_PATTERNS = load_patterns().get("ssrf", {})
|
|
52
|
+
_YAML_PROTOCOL = get_ssrf_protocol_patterns()
|
|
53
|
+
_YAML_METADATA = get_ssrf_metadata_patterns()
|
|
54
|
+
_YAML_INTERNAL = get_ssrf_internal_network_patterns()
|
|
55
|
+
_YAML_URL_VARS = set(_SSRF_PATTERNS.get("url_var_names", []))
|
|
56
|
+
_YAML_USER_INPUT = set(_SSRF_PATTERNS.get("user_input_markers", []))
|
|
57
|
+
_YAML_REDIRECT_SINKS = _SSRF_PATTERNS.get("redirect_sinks", {})
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
_log = logging.getLogger("patchi.security.ssrf_agent")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@register
|
|
64
|
+
class SSRFProtectionAgent(BaseAgent):
|
|
65
|
+
"""Agent for detecting SSRF vulnerabilities."""
|
|
66
|
+
|
|
67
|
+
group = AgentGroup.SECURITY
|
|
68
|
+
domain = AgentDomain.SECURITY
|
|
69
|
+
name = "SSRFProtectionAgent"
|
|
70
|
+
description = (
|
|
71
|
+
"SSRF protection: unvalidated URLs, internal network access, metadata endpoint exposure"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# ── Source patterns for multi-language scanning ─────────────────────────
|
|
75
|
+
_SOURCE_PATTERNS = [
|
|
76
|
+
"*.py",
|
|
77
|
+
"*.js",
|
|
78
|
+
"*.jsx",
|
|
79
|
+
"*.ts",
|
|
80
|
+
"*.tsx",
|
|
81
|
+
"*.java",
|
|
82
|
+
"*.php",
|
|
83
|
+
"*.rb",
|
|
84
|
+
"*.go",
|
|
85
|
+
"*.rs",
|
|
86
|
+
"*.cs",
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
# ── Dangerous protocol handlers (merged from YAML + defaults) ────────────
|
|
90
|
+
# Dangerous schemes / hosts, matched against parsed string literals
|
|
91
|
+
# (urllib.parse + ipaddress — comments and formatting can't misfire).
|
|
92
|
+
# User-supplied YAML customs stay pattern-based (see _YAML_* below).
|
|
93
|
+
_DANGEROUS_SCHEMES = {
|
|
94
|
+
"file": (
|
|
95
|
+
Severity.HIGH,
|
|
96
|
+
"file:// protocol handler — potential local file access via SSRF",
|
|
97
|
+
),
|
|
98
|
+
"gopher": (
|
|
99
|
+
Severity.HIGH,
|
|
100
|
+
"gopher:// protocol handler — potential SSRF vector",
|
|
101
|
+
),
|
|
102
|
+
"dict": (
|
|
103
|
+
Severity.MEDIUM,
|
|
104
|
+
"dict:// protocol handler — potential SSRF vector",
|
|
105
|
+
),
|
|
106
|
+
"ldap": (
|
|
107
|
+
Severity.MEDIUM,
|
|
108
|
+
"ldap:// protocol handler — potential SSRF vector",
|
|
109
|
+
),
|
|
110
|
+
}
|
|
111
|
+
_METADATA_HOSTS = frozenset({"169.254.169.254", "metadata.google.internal"})
|
|
112
|
+
|
|
113
|
+
# ── Cloud metadata endpoints (merged from YAML + defaults) ──────────────
|
|
114
|
+
_METADATA_MESSAGES = {
|
|
115
|
+
"169.254.169.254": "AWS/GCP cloud metadata endpoint access — potential SSRF to cloud credentials",
|
|
116
|
+
"metadata.google.internal": "GCP metadata endpoint access — potential SSRF to cloud credentials",
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
# ── Internal network patterns (merged from YAML + defaults) ─────────────
|
|
120
|
+
_LOCALHOST_MESSAGE = "Localhost URL — verify this isn't user-controlled"
|
|
121
|
+
_INTERNAL_MESSAGE = "Internal network IP — potential SSRF to internal services"
|
|
122
|
+
|
|
123
|
+
# ── URL construction patterns (where user input flows to URL) ───────────
|
|
124
|
+
_URL_VAR_NAMES = {
|
|
125
|
+
"url",
|
|
126
|
+
"endpoint",
|
|
127
|
+
"target",
|
|
128
|
+
"host",
|
|
129
|
+
"base_url",
|
|
130
|
+
"baseUri",
|
|
131
|
+
"requestUrl",
|
|
132
|
+
"fetchUrl",
|
|
133
|
+
"request_url",
|
|
134
|
+
} | _YAML_URL_VARS
|
|
135
|
+
|
|
136
|
+
_USER_INPUT_MARKERS = {
|
|
137
|
+
"request.",
|
|
138
|
+
"req.",
|
|
139
|
+
"params.",
|
|
140
|
+
"args.",
|
|
141
|
+
"input",
|
|
142
|
+
"data[",
|
|
143
|
+
"form[",
|
|
144
|
+
"query.",
|
|
145
|
+
"body.",
|
|
146
|
+
"headers.",
|
|
147
|
+
"c.Query",
|
|
148
|
+
"r.URL",
|
|
149
|
+
} | _YAML_USER_INPUT
|
|
150
|
+
|
|
151
|
+
# ── Redirect/forward patterns (merged from YAML + defaults) ─────────────
|
|
152
|
+
_REDIRECT_CALL_SINKS = {
|
|
153
|
+
Lang.PYTHON: {"redirect", "redirect_url"},
|
|
154
|
+
Lang.JAVASCRIPT: {"res.redirect", "response.redirect"},
|
|
155
|
+
Lang.TYPESCRIPT: {"res.redirect", "response.redirect"},
|
|
156
|
+
Lang.JAVA: {"sendRedirect"},
|
|
157
|
+
Lang.PHP: {"header", "redirect"},
|
|
158
|
+
Lang.RUBY: {"redirect_to"},
|
|
159
|
+
Lang.GO: {"http.Redirect"},
|
|
160
|
+
Lang.C_SHARP: {"Redirect"},
|
|
161
|
+
}
|
|
162
|
+
for _lk, _funcs in _YAML_REDIRECT_SINKS.items():
|
|
163
|
+
try:
|
|
164
|
+
_lang = Lang(_lk)
|
|
165
|
+
except ValueError:
|
|
166
|
+
continue
|
|
167
|
+
_REDIRECT_CALL_SINKS.setdefault(_lang, set()).update(_funcs)
|
|
168
|
+
|
|
169
|
+
_SKIP_DIRS = DEFAULT_IGNORE_DIRS | {"test", "tests", "vendor"}
|
|
170
|
+
|
|
171
|
+
def _run(self, inp: AgentInput, result: AgentResult) -> None:
|
|
172
|
+
findings = []
|
|
173
|
+
files_scanned = 0
|
|
174
|
+
|
|
175
|
+
with trace_agent(self.name, inp.root) as trace:
|
|
176
|
+
for pattern in self._SOURCE_PATTERNS:
|
|
177
|
+
for p in self._safe_rglob(inp.root, pattern):
|
|
178
|
+
rel = str(p.relative_to(inp.root))
|
|
179
|
+
if self._should_skip(rel):
|
|
180
|
+
continue
|
|
181
|
+
files_scanned += 1
|
|
182
|
+
try:
|
|
183
|
+
content = p.read_text(encoding="utf-8", errors="ignore")
|
|
184
|
+
except Exception as e:
|
|
185
|
+
_log.warning("SSRFProtectionAgent._run failed: %s", e)
|
|
186
|
+
continue
|
|
187
|
+
|
|
188
|
+
lang = detect_language(p)
|
|
189
|
+
findings.extend(self._check_http_clients(content, lang, rel))
|
|
190
|
+
findings.extend(self._check_url_literals(content, lang, rel))
|
|
191
|
+
findings.extend(self._check_custom_patterns(content, rel))
|
|
192
|
+
findings.extend(self._check_url_construction(content, lang, rel))
|
|
193
|
+
findings.extend(self._check_redirects(content, lang, rel))
|
|
194
|
+
# Dedupe: structural checks and YAML customs can flag the same
|
|
195
|
+
# literal with slightly different messages — same place+type is one issue
|
|
196
|
+
seen: set[tuple] = set()
|
|
197
|
+
unique: list = []
|
|
198
|
+
for finding in findings:
|
|
199
|
+
key = (finding.file, finding.type, finding.line)
|
|
200
|
+
if key not in seen:
|
|
201
|
+
seen.add(key)
|
|
202
|
+
unique.append(finding)
|
|
203
|
+
findings = unique
|
|
204
|
+
trace.findings = len(findings)
|
|
205
|
+
trace.files_scanned = files_scanned
|
|
206
|
+
|
|
207
|
+
result.status = AgentStatus.DONE
|
|
208
|
+
result.findings = findings
|
|
209
|
+
result.files_scanned = files_scanned
|
|
210
|
+
result.data.update({"files_scanned": files_scanned, "finding_count": len(findings)})
|
|
211
|
+
return
|
|
212
|
+
|
|
213
|
+
def _check_http_clients(self, content: str, lang: Lang, rel_path: str) -> list[Finding]:
|
|
214
|
+
findings = []
|
|
215
|
+
for call in find_calls(content, lang, HTTP_CLIENTS):
|
|
216
|
+
findings.append(
|
|
217
|
+
make_finding(
|
|
218
|
+
self.name,
|
|
219
|
+
"ssrf_http_client",
|
|
220
|
+
Severity.MEDIUM,
|
|
221
|
+
rel_path,
|
|
222
|
+
f"HTTP client call ({call['name']}) — verify URL is not user-controlled",
|
|
223
|
+
line=call["line"],
|
|
224
|
+
code_snippet=call["full_text"][:120],
|
|
225
|
+
suggestion="Validate and whitelist URLs before making HTTP requests. Use URL parsing and allowlist.",
|
|
226
|
+
)
|
|
227
|
+
)
|
|
228
|
+
return findings
|
|
229
|
+
|
|
230
|
+
@staticmethod
|
|
231
|
+
def _split_host(value: str) -> tuple[str, str]:
|
|
232
|
+
"""(scheme, host) from a literal; bare hostnames work without scheme."""
|
|
233
|
+
text = value.strip()
|
|
234
|
+
if "://" in text:
|
|
235
|
+
try:
|
|
236
|
+
parsed = urlsplit(text)
|
|
237
|
+
return parsed.scheme.lower(), parsed.hostname or ""
|
|
238
|
+
except ValueError:
|
|
239
|
+
return "", ""
|
|
240
|
+
return "", text
|
|
241
|
+
|
|
242
|
+
def _check_url_literals(self, content: str, lang: Lang, rel_path: str) -> list[Finding]:
|
|
243
|
+
"""Dangerous URLs via parsed string literals (urllib.parse + ipaddress)."""
|
|
244
|
+
findings = []
|
|
245
|
+
lines = content.splitlines()
|
|
246
|
+
|
|
247
|
+
def _snippet(line_num: int) -> str:
|
|
248
|
+
if 0 < line_num <= len(lines):
|
|
249
|
+
return lines[line_num - 1].strip()[:120]
|
|
250
|
+
return ""
|
|
251
|
+
|
|
252
|
+
for value, line_num in string_literals(content, lang):
|
|
253
|
+
if "://" not in value and "." not in value and ":" not in value:
|
|
254
|
+
continue
|
|
255
|
+
scheme, host = self._split_host(value)
|
|
256
|
+
if scheme in self._DANGEROUS_SCHEMES:
|
|
257
|
+
severity, message = self._DANGEROUS_SCHEMES[scheme]
|
|
258
|
+
findings.append(
|
|
259
|
+
make_finding(
|
|
260
|
+
self.name,
|
|
261
|
+
"ssrf_dangerous_protocol",
|
|
262
|
+
severity,
|
|
263
|
+
rel_path,
|
|
264
|
+
message,
|
|
265
|
+
line=line_num,
|
|
266
|
+
code_snippet=_snippet(line_num),
|
|
267
|
+
suggestion="Block non-HTTP/HTTPS protocols in URL handling.",
|
|
268
|
+
)
|
|
269
|
+
)
|
|
270
|
+
continue
|
|
271
|
+
host = (host or "").lower().split("@")[-1].split(":")[0].strip("[]")
|
|
272
|
+
if not host:
|
|
273
|
+
continue
|
|
274
|
+
if host in self._METADATA_MESSAGES:
|
|
275
|
+
findings.append(
|
|
276
|
+
make_finding(
|
|
277
|
+
self.name,
|
|
278
|
+
"ssrf_metadata_access",
|
|
279
|
+
Severity.CRITICAL,
|
|
280
|
+
rel_path,
|
|
281
|
+
self._METADATA_MESSAGES[host],
|
|
282
|
+
line=line_num,
|
|
283
|
+
code_snippet=_snippet(line_num),
|
|
284
|
+
suggestion="Block access to cloud metadata endpoints. Use IMDSv2 if metadata access is required.",
|
|
285
|
+
)
|
|
286
|
+
)
|
|
287
|
+
continue
|
|
288
|
+
if host == "localhost":
|
|
289
|
+
findings.append(
|
|
290
|
+
make_finding(
|
|
291
|
+
self.name,
|
|
292
|
+
"ssrf_internal_network",
|
|
293
|
+
Severity.LOW,
|
|
294
|
+
rel_path,
|
|
295
|
+
self._LOCALHOST_MESSAGE,
|
|
296
|
+
line=line_num,
|
|
297
|
+
code_snippet=_snippet(line_num),
|
|
298
|
+
suggestion="Restrict outbound HTTP requests to prevent access to internal services.",
|
|
299
|
+
)
|
|
300
|
+
)
|
|
301
|
+
continue
|
|
302
|
+
try:
|
|
303
|
+
addr = ipaddress.ip_address(host)
|
|
304
|
+
except ValueError:
|
|
305
|
+
continue
|
|
306
|
+
if addr.is_private or addr.is_loopback or addr.is_link_local or addr.is_reserved:
|
|
307
|
+
if addr.is_loopback:
|
|
308
|
+
severity, message = Severity.LOW, self._LOCALHOST_MESSAGE
|
|
309
|
+
else:
|
|
310
|
+
severity, message = Severity.MEDIUM, self._INTERNAL_MESSAGE
|
|
311
|
+
findings.append(
|
|
312
|
+
make_finding(
|
|
313
|
+
self.name,
|
|
314
|
+
"ssrf_internal_network",
|
|
315
|
+
severity,
|
|
316
|
+
rel_path,
|
|
317
|
+
message,
|
|
318
|
+
line=line_num,
|
|
319
|
+
code_snippet=_snippet(line_num),
|
|
320
|
+
suggestion="Restrict outbound HTTP requests to prevent access to internal services.",
|
|
321
|
+
)
|
|
322
|
+
)
|
|
323
|
+
return findings
|
|
324
|
+
|
|
325
|
+
@staticmethod
|
|
326
|
+
def _as_severity(value) -> Severity:
|
|
327
|
+
if isinstance(value, Severity):
|
|
328
|
+
return value
|
|
329
|
+
try:
|
|
330
|
+
return Severity(str(value).lower())
|
|
331
|
+
except ValueError:
|
|
332
|
+
return Severity.MEDIUM
|
|
333
|
+
|
|
334
|
+
def _check_custom_patterns(self, content: str, rel_path: str) -> list[Finding]:
|
|
335
|
+
"""User-supplied YAML customs stay pattern-based (configuration, not logic)."""
|
|
336
|
+
findings = []
|
|
337
|
+
customs: list[tuple] = []
|
|
338
|
+
customs.extend((p, s, m, "ssrf_dangerous_protocol") for p, s, m in _YAML_PROTOCOL)
|
|
339
|
+
customs.extend((p, s, m, "ssrf_metadata_access") for p, s, m in _YAML_METADATA)
|
|
340
|
+
customs.extend((p, s, m, "ssrf_internal_network") for p, s, m in _YAML_INTERNAL)
|
|
341
|
+
if not customs:
|
|
342
|
+
return findings
|
|
343
|
+
for line_num, line in enumerate(content.splitlines(), 1):
|
|
344
|
+
stripped = line.strip()
|
|
345
|
+
if stripped.startswith("#"):
|
|
346
|
+
continue
|
|
347
|
+
for pattern, severity, message, finding_type in customs:
|
|
348
|
+
try:
|
|
349
|
+
matched = pattern.search(line)
|
|
350
|
+
except Exception:
|
|
351
|
+
continue
|
|
352
|
+
if matched:
|
|
353
|
+
findings.append(
|
|
354
|
+
make_finding(
|
|
355
|
+
self.name,
|
|
356
|
+
finding_type,
|
|
357
|
+
self._as_severity(severity),
|
|
358
|
+
rel_path,
|
|
359
|
+
message,
|
|
360
|
+
line=line_num,
|
|
361
|
+
code_snippet=stripped[:120],
|
|
362
|
+
suggestion="Review against SSRF guidance.",
|
|
363
|
+
)
|
|
364
|
+
)
|
|
365
|
+
return findings
|
|
366
|
+
|
|
367
|
+
def _check_url_construction(self, content: str, lang: Lang, rel_path: str) -> list[Finding]:
|
|
368
|
+
findings = []
|
|
369
|
+
for assign in find_assignments(content, lang):
|
|
370
|
+
target = assign["target"].lower()
|
|
371
|
+
if not any(v in target for v in self._URL_VAR_NAMES):
|
|
372
|
+
continue
|
|
373
|
+
value = assign["value"]
|
|
374
|
+
if not any(marker in value.lower() for marker in self._USER_INPUT_MARKERS):
|
|
375
|
+
continue
|
|
376
|
+
findings.append(
|
|
377
|
+
make_finding(
|
|
378
|
+
self.name,
|
|
379
|
+
"ssrf_url_construction",
|
|
380
|
+
Severity.HIGH,
|
|
381
|
+
rel_path,
|
|
382
|
+
f"URL variable '{assign['target']}' constructed from user input — validate and sanitize before use",
|
|
383
|
+
line=assign["line"],
|
|
384
|
+
code_snippet=assign["full_text"][:120],
|
|
385
|
+
suggestion="Parse URLs with urllib.parse and validate against an allowlist before use.",
|
|
386
|
+
)
|
|
387
|
+
)
|
|
388
|
+
return findings
|
|
389
|
+
|
|
390
|
+
def _check_redirects(self, content: str, lang: Lang, rel_path: str) -> list[Finding]:
|
|
391
|
+
findings = []
|
|
392
|
+
sinks = self._REDIRECT_CALL_SINKS.get(lang, set())
|
|
393
|
+
if sinks:
|
|
394
|
+
for call in find_calls(content, lang, sinks):
|
|
395
|
+
arg = get_call_arg(call["full_text"])
|
|
396
|
+
if arg and any(marker in arg.lower() for marker in self._USER_INPUT_MARKERS):
|
|
397
|
+
findings.append(
|
|
398
|
+
make_finding(
|
|
399
|
+
self.name,
|
|
400
|
+
"ssrf_redirect",
|
|
401
|
+
Severity.HIGH,
|
|
402
|
+
rel_path,
|
|
403
|
+
"Redirect with user-controlled URL — potential open redirect/SSRF",
|
|
404
|
+
line=call["line"],
|
|
405
|
+
code_snippet=call["full_text"][:120],
|
|
406
|
+
suggestion="Validate redirect targets against an allowlist. Never redirect to user-supplied URLs.",
|
|
407
|
+
)
|
|
408
|
+
)
|
|
409
|
+
return findings
|
|
410
|
+
|
|
411
|
+
def _should_skip(self, rel_path: str) -> bool:
|
|
412
|
+
parts = rel_path.replace("\\", "/").split("/")
|
|
413
|
+
return any(p in self._SKIP_DIRS for p in parts)
|
|
414
|
+
|
|
415
|
+
def _safe_rglob(self, root: Path, pattern: str):
|
|
416
|
+
"""Directory-walking with pruning."""
|
|
417
|
+
suffix = pattern.replace("*.", ".")
|
|
418
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
419
|
+
dirnames[:] = [d for d in dirnames if d not in self._SKIP_DIRS]
|
|
420
|
+
for fn in filenames:
|
|
421
|
+
if fn.endswith(suffix):
|
|
422
|
+
yield Path(dirpath) / fn
|