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,865 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DAST Agent — Dynamic Application Security Testing with Playwright.
|
|
3
|
+
|
|
4
|
+
Performs live security testing against running applications:
|
|
5
|
+
- XSS (Cross-Site Scripting) injection
|
|
6
|
+
- SQL Injection testing
|
|
7
|
+
- CSRF vulnerability detection
|
|
8
|
+
- Clickjacking checks
|
|
9
|
+
- Security header validation
|
|
10
|
+
- Information disclosure detection
|
|
11
|
+
- Screenshot capture for evidence
|
|
12
|
+
|
|
13
|
+
Uses BrowserPool for parallel testing and ScreenshotManager for evidence.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import asyncio
|
|
19
|
+
import logging
|
|
20
|
+
import time
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
from patchi.core.agents.base import (
|
|
25
|
+
AgentDomain,
|
|
26
|
+
AgentGroup,
|
|
27
|
+
AgentInput,
|
|
28
|
+
AgentResult,
|
|
29
|
+
BaseAgent,
|
|
30
|
+
Finding,
|
|
31
|
+
Severity,
|
|
32
|
+
register,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
_log = logging.getLogger("patchi.security.dast")
|
|
36
|
+
|
|
37
|
+
# Additional XSS payloads from DASTScanner
|
|
38
|
+
ADDITIONAL_XSS_PAYLOADS = [
|
|
39
|
+
"<img src=x onerror=alert(1)>",
|
|
40
|
+
"<svg onload=alert(1)>",
|
|
41
|
+
"'-alert(1)-'",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
# Additional security headers from DASTScanner
|
|
45
|
+
ADDITIONAL_HEADER_CHECKS = [
|
|
46
|
+
(
|
|
47
|
+
"x-content-type-options",
|
|
48
|
+
"nosniff",
|
|
49
|
+
"Missing X-Content-Type-Options header",
|
|
50
|
+
"medium",
|
|
51
|
+
),
|
|
52
|
+
("x-xss-protection", None, "Missing X-XSS-Protection header", "low"),
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
# Auth bypass keywords from DASTScanner
|
|
56
|
+
AUTH_BYPASS_KEYWORDS = [
|
|
57
|
+
"dashboard",
|
|
58
|
+
"admin",
|
|
59
|
+
"settings",
|
|
60
|
+
"profile",
|
|
61
|
+
"logout",
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
# Info disclosure indicators from DASTScanner
|
|
65
|
+
INFO_DISCLOSURE_INDICATORS = [
|
|
66
|
+
"traceback",
|
|
67
|
+
"stack trace",
|
|
68
|
+
"exception in",
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
# Open redirect payloads from DASTScanner
|
|
72
|
+
OPEN_REDIRECT_PAYLOADS = [
|
|
73
|
+
"https://evil.com",
|
|
74
|
+
"//evil.com",
|
|
75
|
+
"/\\evil.com",
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
# Common endpoint paths from DASTScanner for discovery
|
|
79
|
+
COMMON_ENDPOINT_PATHS = [
|
|
80
|
+
"/",
|
|
81
|
+
"/login",
|
|
82
|
+
"/admin",
|
|
83
|
+
"/api",
|
|
84
|
+
"/dashboard",
|
|
85
|
+
"/settings",
|
|
86
|
+
"/profile",
|
|
87
|
+
"/users",
|
|
88
|
+
"/health",
|
|
89
|
+
"/api/health",
|
|
90
|
+
"/api/status",
|
|
91
|
+
"/static/",
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass
|
|
96
|
+
class DASTTest:
|
|
97
|
+
"""A single DAST test case."""
|
|
98
|
+
|
|
99
|
+
name: str
|
|
100
|
+
description: str
|
|
101
|
+
category: str # xss, sqli, csrf, header, info_disclosure, clickjacking
|
|
102
|
+
severity: Severity
|
|
103
|
+
test_fn: str # Method name to call
|
|
104
|
+
tags: list[str] | None = None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# ── Test Registry ────────────────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
DAST_TESTS: list[DASTTest] = [
|
|
110
|
+
# XSS Tests
|
|
111
|
+
DASTTest(
|
|
112
|
+
name="xss_reflected_basic",
|
|
113
|
+
description="Test for reflected XSS via basic script injection",
|
|
114
|
+
category="xss",
|
|
115
|
+
severity=Severity.HIGH,
|
|
116
|
+
test_fn="test_xss_reflected",
|
|
117
|
+
tags=["xss", "injection", "reflected"],
|
|
118
|
+
),
|
|
119
|
+
DASTTest(
|
|
120
|
+
name="xss_stored_svg",
|
|
121
|
+
description="Test for stored XSS via SVG injection",
|
|
122
|
+
category="xss",
|
|
123
|
+
severity=Severity.HIGH,
|
|
124
|
+
test_fn="test_xss_svg",
|
|
125
|
+
tags=["xss", "injection", "stored"],
|
|
126
|
+
),
|
|
127
|
+
# SQL Injection Tests
|
|
128
|
+
DASTTest(
|
|
129
|
+
name="sqli_error_based",
|
|
130
|
+
description="Test for SQL injection via error-based extraction",
|
|
131
|
+
category="sqli",
|
|
132
|
+
severity=Severity.CRITICAL,
|
|
133
|
+
test_fn="test_sqli_error",
|
|
134
|
+
tags=["sqli", "injection", "database"],
|
|
135
|
+
),
|
|
136
|
+
DASTTest(
|
|
137
|
+
name="sqli_union_select",
|
|
138
|
+
description="Test for SQL injection via UNION SELECT",
|
|
139
|
+
category="sqli",
|
|
140
|
+
severity=Severity.CRITICAL,
|
|
141
|
+
test_fn="test_sqli_union",
|
|
142
|
+
tags=["sqli", "injection", "database"],
|
|
143
|
+
),
|
|
144
|
+
# Security Headers
|
|
145
|
+
DASTTest(
|
|
146
|
+
name="missing_content_security_policy",
|
|
147
|
+
description="Check for missing Content-Security-Policy header",
|
|
148
|
+
category="header",
|
|
149
|
+
severity=Severity.MEDIUM,
|
|
150
|
+
test_fn="test_csp_header",
|
|
151
|
+
tags=["headers", "csp"],
|
|
152
|
+
),
|
|
153
|
+
DASTTest(
|
|
154
|
+
name="missing_x_frame_options",
|
|
155
|
+
description="Check for missing X-Frame-Options header (clickjacking)",
|
|
156
|
+
category="clickjacking",
|
|
157
|
+
severity=Severity.MEDIUM,
|
|
158
|
+
test_fn="test_x_frame_options",
|
|
159
|
+
tags=["headers", "clickjacking"],
|
|
160
|
+
),
|
|
161
|
+
DASTTest(
|
|
162
|
+
name="missing_hsts",
|
|
163
|
+
description="Check for missing Strict-Transport-Security header",
|
|
164
|
+
category="header",
|
|
165
|
+
severity=Severity.LOW,
|
|
166
|
+
test_fn="test_hsts_header",
|
|
167
|
+
tags=["headers", "hsts"],
|
|
168
|
+
),
|
|
169
|
+
# Information Disclosure
|
|
170
|
+
DASTTest(
|
|
171
|
+
name="server_version_disclosure",
|
|
172
|
+
description="Check for server version disclosure in headers",
|
|
173
|
+
category="info_disclosure",
|
|
174
|
+
severity=Severity.LOW,
|
|
175
|
+
test_fn="test_server_version",
|
|
176
|
+
tags=["info", "server"],
|
|
177
|
+
),
|
|
178
|
+
DASTTest(
|
|
179
|
+
name="error_page_disclosure",
|
|
180
|
+
description="Check if error pages reveal stack traces",
|
|
181
|
+
category="info_disclosure",
|
|
182
|
+
severity=Severity.MEDIUM,
|
|
183
|
+
test_fn="test_error_disclosure",
|
|
184
|
+
tags=["info", "errors"],
|
|
185
|
+
),
|
|
186
|
+
# Directory Traversal
|
|
187
|
+
DASTTest(
|
|
188
|
+
name="directory_traversal",
|
|
189
|
+
description="Test for directory traversal via path manipulation",
|
|
190
|
+
category="path_traversal",
|
|
191
|
+
severity=Severity.HIGH,
|
|
192
|
+
test_fn="test_directory_traversal",
|
|
193
|
+
tags=["traversal", "directory"],
|
|
194
|
+
),
|
|
195
|
+
# CSRF
|
|
196
|
+
DASTTest(
|
|
197
|
+
name="csrf_token_missing",
|
|
198
|
+
description="Check for missing CSRF tokens in forms",
|
|
199
|
+
category="csrf",
|
|
200
|
+
severity=Severity.MEDIUM,
|
|
201
|
+
test_fn="test_csrf_token",
|
|
202
|
+
tags=["csrf", "forms"],
|
|
203
|
+
),
|
|
204
|
+
# Auth Bypass (from DASTScanner)
|
|
205
|
+
DASTTest(
|
|
206
|
+
name="auth_bypass",
|
|
207
|
+
description="Test for authentication bypass on protected pages",
|
|
208
|
+
category="auth_bypass",
|
|
209
|
+
severity=Severity.CRITICAL,
|
|
210
|
+
test_fn="test_auth_bypass",
|
|
211
|
+
tags=["auth", "bypass"],
|
|
212
|
+
),
|
|
213
|
+
# Open Redirect (from DASTScanner)
|
|
214
|
+
DASTTest(
|
|
215
|
+
name="open_redirect",
|
|
216
|
+
description="Test for open redirect vulnerabilities",
|
|
217
|
+
category="open_redirect",
|
|
218
|
+
severity=Severity.HIGH,
|
|
219
|
+
test_fn="test_open_redirect",
|
|
220
|
+
tags=["redirect", "open_redirect"],
|
|
221
|
+
),
|
|
222
|
+
# Additional Security Headers (from DASTScanner)
|
|
223
|
+
DASTTest(
|
|
224
|
+
name="missing_x_content_type_options",
|
|
225
|
+
description="Check for missing X-Content-Type-Options header",
|
|
226
|
+
category="header",
|
|
227
|
+
severity=Severity.MEDIUM,
|
|
228
|
+
test_fn="test_additional_headers",
|
|
229
|
+
tags=["headers", "x-content-type-options"],
|
|
230
|
+
),
|
|
231
|
+
DASTTest(
|
|
232
|
+
name="missing_x_xss_protection",
|
|
233
|
+
description="Check for missing X-XSS-Protection header",
|
|
234
|
+
category="header",
|
|
235
|
+
severity=Severity.LOW,
|
|
236
|
+
test_fn="test_additional_headers",
|
|
237
|
+
tags=["headers", "x-xss-protection"],
|
|
238
|
+
),
|
|
239
|
+
# Stack Trace Disclosure (from DASTScanner)
|
|
240
|
+
DASTTest(
|
|
241
|
+
name="stack_trace_disclosure",
|
|
242
|
+
description="Check if error pages reveal stack traces",
|
|
243
|
+
category="info_disclosure",
|
|
244
|
+
severity=Severity.HIGH,
|
|
245
|
+
test_fn="test_stack_trace_disclosure",
|
|
246
|
+
tags=["info", "stack_trace"],
|
|
247
|
+
),
|
|
248
|
+
]
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class DASTAgent(BaseAgent):
|
|
252
|
+
"""
|
|
253
|
+
Dynamic Application Security Testing agent.
|
|
254
|
+
|
|
255
|
+
Runs live security tests against a running web application using Playwright.
|
|
256
|
+
Captures screenshots as evidence and reports findings.
|
|
257
|
+
"""
|
|
258
|
+
|
|
259
|
+
name = "DASTAgent"
|
|
260
|
+
group = AgentGroup.SECURITY
|
|
261
|
+
domain = AgentDomain.SECURITY
|
|
262
|
+
timeout = 300 # 5 minutes for browser tests
|
|
263
|
+
|
|
264
|
+
def _run(self, inp: AgentInput, result: AgentResult) -> None:
|
|
265
|
+
"""Run DAST tests against the target application."""
|
|
266
|
+
target_url = self._detect_target(inp.root)
|
|
267
|
+
if not target_url:
|
|
268
|
+
result.add_finding(Finding(
|
|
269
|
+
agent=self.name,
|
|
270
|
+
type="dast_config",
|
|
271
|
+
severity=Severity.INFO,
|
|
272
|
+
message="No running web application detected. Start the web server first.",
|
|
273
|
+
))
|
|
274
|
+
return
|
|
275
|
+
|
|
276
|
+
_log.info("DAST: Testing %s", target_url)
|
|
277
|
+
|
|
278
|
+
# Run tests synchronously (browser tests)
|
|
279
|
+
test_results = asyncio.run(self._run_tests(target_url, inp))
|
|
280
|
+
|
|
281
|
+
# Add findings to result
|
|
282
|
+
for finding in test_results:
|
|
283
|
+
result.add_finding(finding)
|
|
284
|
+
|
|
285
|
+
result.files_scanned = 1
|
|
286
|
+
result.data["target_url"] = target_url
|
|
287
|
+
result.data["tests_run"] = len(DAST_TESTS)
|
|
288
|
+
result.data["findings_count"] = len(test_results)
|
|
289
|
+
|
|
290
|
+
async def _run_tests(self, target_url: str, inp: AgentInput) -> list[Finding]:
|
|
291
|
+
"""Run all DAST tests asynchronously with video recording."""
|
|
292
|
+
findings: list[Finding] = []
|
|
293
|
+
|
|
294
|
+
try:
|
|
295
|
+
from patchi.core.testing.live_v2.screenshot_manager import ScreenshotManager
|
|
296
|
+
from patchi.core.testing.live_v2.video_recorder import (
|
|
297
|
+
RecordingConfig,
|
|
298
|
+
VideoRecorder,
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
screenshot_mgr = ScreenshotManager(
|
|
302
|
+
baseline_dir=inp.root / ".patchi" / "evidence" / "dast"
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
# Initialize video recorder — outputs to evidence/video
|
|
306
|
+
video_dir = inp.root / ".patchi" / "evidence" / "video"
|
|
307
|
+
video_dir.mkdir(parents=True, exist_ok=True)
|
|
308
|
+
recorder = VideoRecorder(RecordingConfig(output_dir=video_dir))
|
|
309
|
+
|
|
310
|
+
# Run each test with video recording
|
|
311
|
+
for test in DAST_TESTS:
|
|
312
|
+
recording_id = None
|
|
313
|
+
try:
|
|
314
|
+
finding, recording_id = await self._run_test_recorded(
|
|
315
|
+
recorder, screenshot_mgr, test, target_url
|
|
316
|
+
)
|
|
317
|
+
if finding:
|
|
318
|
+
findings.append(finding)
|
|
319
|
+
except Exception as e:
|
|
320
|
+
_log.warning("DAST test %s failed: %s", test.name, e)
|
|
321
|
+
finally:
|
|
322
|
+
# Ensure recording is stopped even on error
|
|
323
|
+
if recording_id and recording_id in recorder._active_recordings:
|
|
324
|
+
try:
|
|
325
|
+
await recorder.stop_recording(recording_id)
|
|
326
|
+
except Exception as _exc:
|
|
327
|
+
_log.warning('_run_tests failed: %s', _exc)
|
|
328
|
+
|
|
329
|
+
except ImportError:
|
|
330
|
+
_log.warning("Playwright not installed. Install with: pip install playwright")
|
|
331
|
+
except Exception as e:
|
|
332
|
+
_log.error("DAST agent failed: %s", e)
|
|
333
|
+
|
|
334
|
+
return findings
|
|
335
|
+
|
|
336
|
+
async def _run_test_recorded(
|
|
337
|
+
self,
|
|
338
|
+
recorder,
|
|
339
|
+
screenshot_mgr,
|
|
340
|
+
test: DASTTest,
|
|
341
|
+
target_url: str,
|
|
342
|
+
) -> tuple[Finding | None, str | None]:
|
|
343
|
+
"""Run a single DAST test with video recording.
|
|
344
|
+
|
|
345
|
+
Returns (finding, recording_id) so the caller can stop the recording.
|
|
346
|
+
"""
|
|
347
|
+
# Start a recorded browser context for this test
|
|
348
|
+
test_name = f"dast_{test.name}_{int(time.time())}"
|
|
349
|
+
context, browser = await recorder.start_recording(test_name)
|
|
350
|
+
recording_id = f"{test_name}-{int(time.time())}"
|
|
351
|
+
|
|
352
|
+
page = await context.new_page()
|
|
353
|
+
|
|
354
|
+
try:
|
|
355
|
+
# Get the test function
|
|
356
|
+
test_fn = getattr(self, test.test_fn, None)
|
|
357
|
+
if not test_fn:
|
|
358
|
+
_log.warning("DAST test function not found: %s", test.test_fn)
|
|
359
|
+
return None, recording_id
|
|
360
|
+
|
|
361
|
+
# Run the test
|
|
362
|
+
vuln_found, evidence = await test_fn(page, target_url, screenshot_mgr)
|
|
363
|
+
|
|
364
|
+
if vuln_found:
|
|
365
|
+
# Capture screenshot as evidence
|
|
366
|
+
try:
|
|
367
|
+
await screenshot_mgr.capture(
|
|
368
|
+
page, target_url, name=f"dast_{test.name}"
|
|
369
|
+
)
|
|
370
|
+
except Exception as e:
|
|
371
|
+
_log.debug("Screenshot capture failed: %s", e)
|
|
372
|
+
|
|
373
|
+
return Finding(
|
|
374
|
+
agent=self.name,
|
|
375
|
+
type=f"dast_{test.category}",
|
|
376
|
+
severity=test.severity,
|
|
377
|
+
message=test.description,
|
|
378
|
+
file=target_url,
|
|
379
|
+
suggestion=f"Fix {test.category} vulnerability: {evidence}",
|
|
380
|
+
code_snippet=evidence[:500] if evidence else "",
|
|
381
|
+
), recording_id
|
|
382
|
+
|
|
383
|
+
return None, recording_id
|
|
384
|
+
|
|
385
|
+
finally:
|
|
386
|
+
# Close page and stop recording
|
|
387
|
+
try:
|
|
388
|
+
await page.close()
|
|
389
|
+
except Exception as _exc:
|
|
390
|
+
_log.warning('_run_test_recorded failed: %s', _exc)
|
|
391
|
+
# Stop recording and save video
|
|
392
|
+
try:
|
|
393
|
+
if recording_id in recorder._active_recordings:
|
|
394
|
+
recording = await recorder.stop_recording(recording_id)
|
|
395
|
+
_log.info(
|
|
396
|
+
"DAST video recorded: %s (%.1fs)",
|
|
397
|
+
test.name,
|
|
398
|
+
recording.total_duration_seconds,
|
|
399
|
+
)
|
|
400
|
+
except Exception as e:
|
|
401
|
+
_log.debug("Video recording stop failed: %s", e)
|
|
402
|
+
try:
|
|
403
|
+
await browser.close()
|
|
404
|
+
except Exception as _exc:
|
|
405
|
+
_log.warning('_run_test_recorded failed: %s', _exc)
|
|
406
|
+
|
|
407
|
+
# ── XSS Tests ──────────────────────────────────────────────────────────
|
|
408
|
+
|
|
409
|
+
async def test_xss_reflected(
|
|
410
|
+
self, page: Any, base_url: str, screenshot_mgr: Any
|
|
411
|
+
) -> tuple[bool, str]:
|
|
412
|
+
"""Test for reflected XSS via basic script injection."""
|
|
413
|
+
payloads = [
|
|
414
|
+
"<script>alert('XSS')</script>",
|
|
415
|
+
"<img src=x onerror=alert(1)>",
|
|
416
|
+
"<svg onload=alert(1)>",
|
|
417
|
+
]
|
|
418
|
+
|
|
419
|
+
for payload in payloads:
|
|
420
|
+
try:
|
|
421
|
+
# Try common injection points
|
|
422
|
+
test_urls = [
|
|
423
|
+
f"{base_url}?q={payload}",
|
|
424
|
+
f"{base_url}/search?q={payload}",
|
|
425
|
+
f"{base_url}/?error={payload}",
|
|
426
|
+
]
|
|
427
|
+
|
|
428
|
+
for url in test_urls:
|
|
429
|
+
await page.goto(url, wait_until="domcontentloaded", timeout=10000)
|
|
430
|
+
content = await page.content()
|
|
431
|
+
|
|
432
|
+
# Check if payload is reflected unescaped
|
|
433
|
+
if payload in content and "<script>" in payload:
|
|
434
|
+
return True, f"Reflected XSS at {url} with payload: {payload}"
|
|
435
|
+
|
|
436
|
+
except Exception:
|
|
437
|
+
continue
|
|
438
|
+
|
|
439
|
+
return False, ""
|
|
440
|
+
|
|
441
|
+
async def test_xss_svg(
|
|
442
|
+
self, page: Any, base_url: str, screenshot_mgr: Any
|
|
443
|
+
) -> tuple[bool, str]:
|
|
444
|
+
"""Test for stored XSS via SVG injection."""
|
|
445
|
+
|
|
446
|
+
try:
|
|
447
|
+
# Try common upload/comment endpoints
|
|
448
|
+
endpoints = ["/upload", "/comment", "/profile", "/bio"]
|
|
449
|
+
|
|
450
|
+
for endpoint in endpoints:
|
|
451
|
+
url = f"{base_url}{endpoint}"
|
|
452
|
+
await page.goto(url, wait_until="domcontentloaded", timeout=10000)
|
|
453
|
+
|
|
454
|
+
# Look for file upload inputs
|
|
455
|
+
file_inputs = await page.query_selector_all('input[type="file"]')
|
|
456
|
+
if file_inputs:
|
|
457
|
+
# Note: actual file upload testing would require creating a temp file
|
|
458
|
+
return True, f"Potential XSS via SVG upload at {url}"
|
|
459
|
+
|
|
460
|
+
except Exception as _exc:
|
|
461
|
+
_log.warning('test_xss_svg failed: %s', _exc)
|
|
462
|
+
|
|
463
|
+
return False, ""
|
|
464
|
+
|
|
465
|
+
# ── SQL Injection Tests ────────────────────────────────────────────────
|
|
466
|
+
|
|
467
|
+
async def test_sqli_error(
|
|
468
|
+
self, page: Any, base_url: str, screenshot_mgr: Any
|
|
469
|
+
) -> tuple[bool, str]:
|
|
470
|
+
"""Test for SQL injection via error-based extraction."""
|
|
471
|
+
payloads = [
|
|
472
|
+
"'",
|
|
473
|
+
"1' OR '1'='1",
|
|
474
|
+
"1' UNION SELECT NULL--",
|
|
475
|
+
]
|
|
476
|
+
|
|
477
|
+
sql_errors = [
|
|
478
|
+
"SQL syntax",
|
|
479
|
+
"mysql_fetch",
|
|
480
|
+
"ORA-01756",
|
|
481
|
+
"PostgreSQL",
|
|
482
|
+
"SQLite3::",
|
|
483
|
+
"Microsoft OLE DB",
|
|
484
|
+
"ODBC SQL Server",
|
|
485
|
+
]
|
|
486
|
+
|
|
487
|
+
for payload in payloads:
|
|
488
|
+
try:
|
|
489
|
+
test_urls = [
|
|
490
|
+
f"{base_url}?id={payload}",
|
|
491
|
+
f"{base_url}/user?id={payload}",
|
|
492
|
+
f"{base_url}/api/data?id={payload}",
|
|
493
|
+
]
|
|
494
|
+
|
|
495
|
+
for url in test_urls:
|
|
496
|
+
await page.goto(url, wait_until="domcontentloaded", timeout=10000)
|
|
497
|
+
content = await page.content()
|
|
498
|
+
|
|
499
|
+
for error in sql_errors:
|
|
500
|
+
if error.lower() in content.lower():
|
|
501
|
+
return True, f"SQL injection error at {url}: {error}"
|
|
502
|
+
|
|
503
|
+
except Exception:
|
|
504
|
+
continue
|
|
505
|
+
|
|
506
|
+
return False, ""
|
|
507
|
+
|
|
508
|
+
async def test_sqli_union(
|
|
509
|
+
self, page: Any, base_url: str, screenshot_mgr: Any
|
|
510
|
+
) -> tuple[bool, str]:
|
|
511
|
+
"""Test for SQL injection via UNION SELECT."""
|
|
512
|
+
# Simplified check - look for SQL error patterns
|
|
513
|
+
return await self.test_sqli_error(page, base_url, screenshot_mgr)
|
|
514
|
+
|
|
515
|
+
# ── Header Tests ───────────────────────────────────────────────────────
|
|
516
|
+
|
|
517
|
+
async def test_csp_header(
|
|
518
|
+
self, page: Any, base_url: str, screenshot_mgr: Any
|
|
519
|
+
) -> tuple[bool, str]:
|
|
520
|
+
"""Check for missing Content-Security-Policy header."""
|
|
521
|
+
try:
|
|
522
|
+
response = await page.goto(base_url, wait_until="domcontentloaded")
|
|
523
|
+
csp = response.headers.get("content-security-policy", "")
|
|
524
|
+
|
|
525
|
+
if not csp:
|
|
526
|
+
return True, "Missing Content-Security-Policy header"
|
|
527
|
+
|
|
528
|
+
except Exception as _exc:
|
|
529
|
+
_log.warning('test_csp_header failed: %s', _exc)
|
|
530
|
+
|
|
531
|
+
return False, ""
|
|
532
|
+
|
|
533
|
+
async def test_x_frame_options(
|
|
534
|
+
self, page: Any, base_url: str, screenshot_mgr: Any
|
|
535
|
+
) -> tuple[bool, str]:
|
|
536
|
+
"""Check for missing X-Frame-Options header."""
|
|
537
|
+
try:
|
|
538
|
+
response = await page.goto(base_url, wait_until="domcontentloaded")
|
|
539
|
+
xfo = response.headers.get("x-frame-options", "")
|
|
540
|
+
csp = response.headers.get("content-security-policy", "")
|
|
541
|
+
|
|
542
|
+
if not xfo and "frame-ancestors" not in csp:
|
|
543
|
+
return True, "Missing X-Frame-Options header (clickjacking risk)"
|
|
544
|
+
|
|
545
|
+
except Exception as _exc:
|
|
546
|
+
_log.warning('test_x_frame_options failed: %s', _exc)
|
|
547
|
+
|
|
548
|
+
return False, ""
|
|
549
|
+
|
|
550
|
+
async def test_hsts_header(
|
|
551
|
+
self, page: Any, base_url: str, screenshot_mgr: Any
|
|
552
|
+
) -> tuple[bool, str]:
|
|
553
|
+
"""Check for missing Strict-Transport-Security header."""
|
|
554
|
+
if not base_url.startswith("https"):
|
|
555
|
+
return False, ""
|
|
556
|
+
|
|
557
|
+
try:
|
|
558
|
+
response = await page.goto(base_url, wait_until="domcontentloaded")
|
|
559
|
+
hsts = response.headers.get("strict-transport-security", "")
|
|
560
|
+
|
|
561
|
+
if not hsts:
|
|
562
|
+
return True, "Missing Strict-Transport-Security header"
|
|
563
|
+
|
|
564
|
+
except Exception as _exc:
|
|
565
|
+
_log.warning('test_hsts_header failed: %s', _exc)
|
|
566
|
+
|
|
567
|
+
return False, ""
|
|
568
|
+
|
|
569
|
+
# ── Information Disclosure ─────────────────────────────────────────────
|
|
570
|
+
|
|
571
|
+
async def test_server_version(
|
|
572
|
+
self, page: Any, base_url: str, screenshot_mgr: Any
|
|
573
|
+
) -> tuple[bool, str]:
|
|
574
|
+
"""Check for server version disclosure in headers."""
|
|
575
|
+
try:
|
|
576
|
+
response = await page.goto(base_url, wait_until="domcontentloaded")
|
|
577
|
+
server = response.headers.get("server", "")
|
|
578
|
+
x_powered = response.headers.get("x-powered-by", "")
|
|
579
|
+
|
|
580
|
+
if server and any(v in server.lower() for v in ["apache", "nginx", "iis"]):
|
|
581
|
+
return True, f"Server version disclosed: {server}"
|
|
582
|
+
|
|
583
|
+
if x_powered:
|
|
584
|
+
return True, f"X-Powered-By disclosed: {x_powered}"
|
|
585
|
+
|
|
586
|
+
except Exception as _exc:
|
|
587
|
+
_log.warning('test_server_version failed: %s', _exc)
|
|
588
|
+
|
|
589
|
+
return False, ""
|
|
590
|
+
|
|
591
|
+
async def test_error_disclosure(
|
|
592
|
+
self, page: Any, base_url: str, screenshot_mgr: Any
|
|
593
|
+
) -> tuple[bool, str]:
|
|
594
|
+
"""Check if error pages reveal stack traces."""
|
|
595
|
+
error_indicators = [
|
|
596
|
+
"Traceback (most recent call last)",
|
|
597
|
+
"Stack Trace:",
|
|
598
|
+
"at line",
|
|
599
|
+
"Exception in thread",
|
|
600
|
+
"Fatal error",
|
|
601
|
+
]
|
|
602
|
+
|
|
603
|
+
try:
|
|
604
|
+
# Try to trigger error pages
|
|
605
|
+
error_urls = [
|
|
606
|
+
f"{base_url}/nonexistent-page-12345",
|
|
607
|
+
f"{base_url}/api/nonexistent-endpoint",
|
|
608
|
+
f"{base_url}/?error=trigger",
|
|
609
|
+
]
|
|
610
|
+
|
|
611
|
+
for url in error_urls:
|
|
612
|
+
await page.goto(url, wait_until="domcontentloaded")
|
|
613
|
+
content = await page.content()
|
|
614
|
+
|
|
615
|
+
for indicator in error_indicators:
|
|
616
|
+
if indicator.lower() in content.lower():
|
|
617
|
+
return True, f"Error disclosure at {url}: {indicator}"
|
|
618
|
+
|
|
619
|
+
except Exception as _exc:
|
|
620
|
+
_log.warning('test_error_disclosure failed: %s', _exc)
|
|
621
|
+
|
|
622
|
+
return False, ""
|
|
623
|
+
|
|
624
|
+
# ── Directory Traversal ────────────────────────────────────────────────
|
|
625
|
+
|
|
626
|
+
async def test_directory_traversal(
|
|
627
|
+
self, page: Any, base_url: str, screenshot_mgr: Any
|
|
628
|
+
) -> tuple[bool, str]:
|
|
629
|
+
"""Test for directory traversal via path manipulation."""
|
|
630
|
+
payloads = [
|
|
631
|
+
"../../../etc/passwd",
|
|
632
|
+
"....//....//etc/passwd",
|
|
633
|
+
"%2e%2e%2f%2e%2e%2fetc/passwd",
|
|
634
|
+
]
|
|
635
|
+
|
|
636
|
+
traversal_indicators = [
|
|
637
|
+
"root:x:0:0",
|
|
638
|
+
"/bin/bash",
|
|
639
|
+
"/bin/sh",
|
|
640
|
+
"[boot loader]",
|
|
641
|
+
]
|
|
642
|
+
|
|
643
|
+
for payload in payloads:
|
|
644
|
+
try:
|
|
645
|
+
test_urls = [
|
|
646
|
+
f"{base_url}/file?path={payload}",
|
|
647
|
+
f"{base_url}/download?file={payload}",
|
|
648
|
+
f"{base_url}/view?path={payload}",
|
|
649
|
+
]
|
|
650
|
+
|
|
651
|
+
for url in test_urls:
|
|
652
|
+
await page.goto(url, wait_until="domcontentloaded", timeout=10000)
|
|
653
|
+
content = await page.content()
|
|
654
|
+
|
|
655
|
+
for indicator in traversal_indicators:
|
|
656
|
+
if indicator in content:
|
|
657
|
+
return True, f"Directory traversal at {url}: {indicator}"
|
|
658
|
+
|
|
659
|
+
except Exception:
|
|
660
|
+
continue
|
|
661
|
+
|
|
662
|
+
return False, ""
|
|
663
|
+
|
|
664
|
+
# ── CSRF Tests ─────────────────────────────────────────────────────────
|
|
665
|
+
|
|
666
|
+
async def test_csrf_token(
|
|
667
|
+
self, page: Any, base_url: str, screenshot_mgr: Any
|
|
668
|
+
) -> tuple[bool, str]:
|
|
669
|
+
"""Check for missing CSRF tokens in forms."""
|
|
670
|
+
try:
|
|
671
|
+
await page.goto(base_url, wait_until="domcontentloaded")
|
|
672
|
+
|
|
673
|
+
# Find all forms
|
|
674
|
+
forms = await page.query_selector_all("form")
|
|
675
|
+
|
|
676
|
+
for _i, form in enumerate(forms):
|
|
677
|
+
# Check for CSRF token fields
|
|
678
|
+
csrf_inputs = await form.query_selector_all(
|
|
679
|
+
'input[name*="csrf"], input[name*="token"], input[name="_token"]'
|
|
680
|
+
)
|
|
681
|
+
|
|
682
|
+
method = await form.get_attribute("method") or "GET"
|
|
683
|
+
if method.upper() == "POST" and not csrf_inputs:
|
|
684
|
+
action = await form.get_attribute("action") or "unknown"
|
|
685
|
+
return True, f"Form at {action} missing CSRF token"
|
|
686
|
+
|
|
687
|
+
except Exception as _exc:
|
|
688
|
+
_log.warning('test_csrf_token failed: %s', _exc)
|
|
689
|
+
|
|
690
|
+
return False, ""
|
|
691
|
+
|
|
692
|
+
# ── Endpoint Discovery (from DASTScanner) ──────────────────────────────
|
|
693
|
+
|
|
694
|
+
async def _discover_endpoints(self, base_url: str) -> list[str]:
|
|
695
|
+
"""Discover endpoints by crawling the app (from DASTScanner)."""
|
|
696
|
+
import socket
|
|
697
|
+
endpoints = set()
|
|
698
|
+
endpoints.add(base_url)
|
|
699
|
+
|
|
700
|
+
for path in COMMON_ENDPOINT_PATHS:
|
|
701
|
+
url = f"{base_url}{path}"
|
|
702
|
+
try:
|
|
703
|
+
# Quick TCP check first
|
|
704
|
+
try:
|
|
705
|
+
with socket.create_connection(("127.0.0.1", 80), timeout=1):
|
|
706
|
+
pass
|
|
707
|
+
except Exception:
|
|
708
|
+
pass # Can't check easily without page context
|
|
709
|
+
|
|
710
|
+
# Use page context for actual check
|
|
711
|
+
async with self._temp_page() as page:
|
|
712
|
+
resp = await page.goto(url, wait_until="domcontentloaded", timeout=5000)
|
|
713
|
+
if resp and resp.status < 400:
|
|
714
|
+
endpoints.add(url)
|
|
715
|
+
# Extract links from page
|
|
716
|
+
links = await page.eval_on_selector_all(
|
|
717
|
+
"a[href]",
|
|
718
|
+
"els => els.map(e => e.href)",
|
|
719
|
+
)
|
|
720
|
+
for link in links:
|
|
721
|
+
if link.startswith(base_url):
|
|
722
|
+
endpoints.add(link)
|
|
723
|
+
except Exception as _exc:
|
|
724
|
+
_log.debug('_discover_endpoints failed: %s', _exc)
|
|
725
|
+
|
|
726
|
+
return list(endpoints)[:50] # cap at 50 endpoints
|
|
727
|
+
|
|
728
|
+
async def _temp_page(self):
|
|
729
|
+
"""Create a temporary page for endpoint discovery."""
|
|
730
|
+
from patchi.core.testing.live_v2.browser_pool import BrowserPool
|
|
731
|
+
pool = BrowserPool()
|
|
732
|
+
await pool.initialize()
|
|
733
|
+
page = await pool.get_page()
|
|
734
|
+
return page
|
|
735
|
+
|
|
736
|
+
# ── Auth Bypass Tests (from DASTScanner) ──────────────────────────────
|
|
737
|
+
|
|
738
|
+
async def test_auth_bypass(
|
|
739
|
+
self, page: Any, base_url: str, screenshot_mgr: Any
|
|
740
|
+
) -> tuple[bool, str]:
|
|
741
|
+
"""Test for authentication bypass on protected pages."""
|
|
742
|
+
try:
|
|
743
|
+
resp = await page.goto(base_url, wait_until="domcontentloaded", timeout=5000)
|
|
744
|
+
if not resp:
|
|
745
|
+
return False, ""
|
|
746
|
+
|
|
747
|
+
content = await page.content()
|
|
748
|
+
has_login = bool(await page.query_selector("input[type=password]"))
|
|
749
|
+
has_protected = any(
|
|
750
|
+
kw in content.lower()
|
|
751
|
+
for kw in AUTH_BYPASS_KEYWORDS
|
|
752
|
+
)
|
|
753
|
+
|
|
754
|
+
if has_protected and not has_login:
|
|
755
|
+
# Page has protected content but no login form — possible bypass
|
|
756
|
+
return True, f"Auth bypass: Protected content accessible without authentication at {base_url}"
|
|
757
|
+
|
|
758
|
+
except Exception as e:
|
|
759
|
+
_log.debug("Auth bypass test failed: %s", e)
|
|
760
|
+
|
|
761
|
+
return False, ""
|
|
762
|
+
|
|
763
|
+
# ── Open Redirect Tests (from DASTScanner) ──────────────────────────────
|
|
764
|
+
|
|
765
|
+
async def test_open_redirect(
|
|
766
|
+
self, page: Any, base_url: str, screenshot_mgr: Any
|
|
767
|
+
) -> tuple[bool, str]:
|
|
768
|
+
"""Test for open redirect vulnerabilities."""
|
|
769
|
+
for payload in OPEN_REDIRECT_PAYLOADS:
|
|
770
|
+
try:
|
|
771
|
+
test_url = f"{base_url}?redirect={payload}"
|
|
772
|
+
resp = await page.goto(test_url, wait_until="domcontentloaded", timeout=5000)
|
|
773
|
+
if resp and page.url.startswith("http"):
|
|
774
|
+
current = page.url
|
|
775
|
+
if "evil.com" in current:
|
|
776
|
+
return True, f"Open redirect at {test_url}: Redirected to {current}"
|
|
777
|
+
|
|
778
|
+
except Exception:
|
|
779
|
+
continue
|
|
780
|
+
|
|
781
|
+
return False, ""
|
|
782
|
+
|
|
783
|
+
# ── Additional Security Headers (from DASTScanner) ──────────────────────
|
|
784
|
+
|
|
785
|
+
async def test_additional_headers(
|
|
786
|
+
self, page: Any, base_url: str, screenshot_mgr: Any
|
|
787
|
+
) -> tuple[bool, str]:
|
|
788
|
+
"""Check for additional missing security headers."""
|
|
789
|
+
try:
|
|
790
|
+
resp = await page.goto(base_url, wait_until="domcontentloaded")
|
|
791
|
+
if not resp:
|
|
792
|
+
return False, ""
|
|
793
|
+
|
|
794
|
+
headers = {k.lower(): v for k, v in resp.headers.items()}
|
|
795
|
+
|
|
796
|
+
for header, expected_value, message, severity in ADDITIONAL_HEADER_CHECKS:
|
|
797
|
+
if header not in headers:
|
|
798
|
+
return True, message
|
|
799
|
+
elif expected_value and headers[header] != expected_value:
|
|
800
|
+
return True, f"{header} = {headers[header]} (expected {expected_value})"
|
|
801
|
+
|
|
802
|
+
except Exception as e:
|
|
803
|
+
_log.debug("Additional headers test failed: %s", e)
|
|
804
|
+
|
|
805
|
+
return False, ""
|
|
806
|
+
|
|
807
|
+
# ── Info Disclosure Stack Trace (from DASTScanner) ──────────────────────
|
|
808
|
+
|
|
809
|
+
async def test_stack_trace_disclosure(
|
|
810
|
+
self, page: Any, base_url: str, screenshot_mgr: Any
|
|
811
|
+
) -> tuple[bool, str]:
|
|
812
|
+
"""Check if error pages reveal stack traces."""
|
|
813
|
+
try:
|
|
814
|
+
error_urls = [
|
|
815
|
+
f"{base_url}/nonexistent-page-12345",
|
|
816
|
+
f"{base_url}/api/nonexistent-endpoint",
|
|
817
|
+
f"{base_url}/?error=trigger",
|
|
818
|
+
]
|
|
819
|
+
|
|
820
|
+
for url in error_urls:
|
|
821
|
+
await page.goto(url, wait_until="domcontentloaded")
|
|
822
|
+
content = await page.content()
|
|
823
|
+
|
|
824
|
+
for indicator in INFO_DISCLOSURE_INDICATORS:
|
|
825
|
+
if indicator.lower() in content.lower():
|
|
826
|
+
return True, f"Stack trace disclosure at {url}: {indicator}"
|
|
827
|
+
|
|
828
|
+
except Exception as _exc:
|
|
829
|
+
_log.warning('test_stack_trace_disclosure failed: %s', _exc)
|
|
830
|
+
|
|
831
|
+
return False, ""
|
|
832
|
+
|
|
833
|
+
# ── Helper Methods ─────────────────────────────────────────────────────
|
|
834
|
+
|
|
835
|
+
def _detect_target(self, root) -> str | None:
|
|
836
|
+
"""Detect the target URL for testing."""
|
|
837
|
+
import socket
|
|
838
|
+
|
|
839
|
+
# Try common ports
|
|
840
|
+
ports = [1612, 8000, 3000, 8080, 80]
|
|
841
|
+
|
|
842
|
+
for port in ports:
|
|
843
|
+
try:
|
|
844
|
+
with socket.create_connection(("127.0.0.1", port), timeout=1):
|
|
845
|
+
return f"http://127.0.0.1:{port}"
|
|
846
|
+
except (ConnectionRefusedError, OSError):
|
|
847
|
+
continue
|
|
848
|
+
|
|
849
|
+
# Check if there's a web server in the config
|
|
850
|
+
try:
|
|
851
|
+
from patchi.core import config as cfg
|
|
852
|
+
|
|
853
|
+
config = cfg.load(root)
|
|
854
|
+
web_config = config.get("web", {})
|
|
855
|
+
host = web_config.get("host", "127.0.0.1")
|
|
856
|
+
port = web_config.get("port", 1612)
|
|
857
|
+
return f"http://{host}:{port}"
|
|
858
|
+
except Exception as _exc:
|
|
859
|
+
_log.warning('_detect_target failed: %s', _exc)
|
|
860
|
+
|
|
861
|
+
return None
|
|
862
|
+
|
|
863
|
+
|
|
864
|
+
# Register the agent
|
|
865
|
+
register(DASTAgent)
|