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,1142 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Red Team Engine — Orchestrates attack simulations and auto-fix verification.
|
|
3
|
+
|
|
4
|
+
This is the core engine that:
|
|
5
|
+
1. Loads attack scenarios from YAML
|
|
6
|
+
2. Selects relevant scenarios based on project context
|
|
7
|
+
3. Executes attacks safely (with safe_mode)
|
|
8
|
+
4. Generates findings with exploit evidence
|
|
9
|
+
5. Triggers auto-fix generation
|
|
10
|
+
6. Verifies fixes by re-running attacks
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import asyncio
|
|
16
|
+
import logging
|
|
17
|
+
import time
|
|
18
|
+
import uuid
|
|
19
|
+
from collections.abc import Callable
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from datetime import UTC, datetime
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
import yaml
|
|
25
|
+
|
|
26
|
+
from patchi.core.agents.base import (
|
|
27
|
+
AgentGroup,
|
|
28
|
+
AgentInput,
|
|
29
|
+
AgentResult,
|
|
30
|
+
BaseAgent,
|
|
31
|
+
Finding,
|
|
32
|
+
Severity,
|
|
33
|
+
register,
|
|
34
|
+
)
|
|
35
|
+
from patchi.core.security.domain_loader import DomainLoader
|
|
36
|
+
|
|
37
|
+
_log = logging.getLogger("patchi.security.red_team_engine")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class AttackStepResult:
|
|
42
|
+
"""Result of a single attack step."""
|
|
43
|
+
|
|
44
|
+
step: int
|
|
45
|
+
action: str
|
|
46
|
+
success: bool
|
|
47
|
+
evidence: str = ""
|
|
48
|
+
response_data: dict = field(default_factory=dict)
|
|
49
|
+
error: str = ""
|
|
50
|
+
duration_ms: int = 0
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class ScenarioResult:
|
|
55
|
+
"""Result of running an attack scenario."""
|
|
56
|
+
|
|
57
|
+
scenario_id: str
|
|
58
|
+
scenario_name: str
|
|
59
|
+
status: str # "success", "failed", "blocked", "skipped"
|
|
60
|
+
steps_completed: int
|
|
61
|
+
total_steps: int
|
|
62
|
+
step_results: list[AttackStepResult] = field(default_factory=list)
|
|
63
|
+
findings: list[Finding] = field(default_factory=list)
|
|
64
|
+
exploit_evidence: dict = field(default_factory=dict)
|
|
65
|
+
started_at: str = field(default_factory=lambda: datetime.now(UTC).isoformat())
|
|
66
|
+
completed_at: str = ""
|
|
67
|
+
duration_ms: int = 0
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class RedTeamReport:
|
|
72
|
+
"""Complete red team assessment report."""
|
|
73
|
+
|
|
74
|
+
assessment_id: str
|
|
75
|
+
project_root: str
|
|
76
|
+
scope: str
|
|
77
|
+
intensity: str
|
|
78
|
+
scenarios_run: list[ScenarioResult] = field(default_factory=list)
|
|
79
|
+
total_findings: int = 0
|
|
80
|
+
by_severity: dict[str, int] = field(default_factory=dict)
|
|
81
|
+
attack_tree: dict = field(default_factory=dict)
|
|
82
|
+
remediation_playbooks: list[str] = field(default_factory=list)
|
|
83
|
+
started_at: str = field(default_factory=lambda: datetime.now(UTC).isoformat())
|
|
84
|
+
completed_at: str = ""
|
|
85
|
+
duration_ms: int = 0
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class AttackExecutor:
|
|
89
|
+
"""Executes individual attack steps with real Playwright/HTTP."""
|
|
90
|
+
|
|
91
|
+
def __init__(
|
|
92
|
+
self,
|
|
93
|
+
root: Path,
|
|
94
|
+
target_url: str = None,
|
|
95
|
+
safe_mode: bool = True,
|
|
96
|
+
on_progress: Callable[[str], None] = None,
|
|
97
|
+
evidence_dir: Path | None = None,
|
|
98
|
+
):
|
|
99
|
+
self.root = root
|
|
100
|
+
self.target_url = target_url or "http://127.0.0.1:1612"
|
|
101
|
+
self.safe_mode = safe_mode
|
|
102
|
+
self.on_progress = on_progress or (lambda _: None)
|
|
103
|
+
self.session = None # aiohttp session
|
|
104
|
+
self.browser = None # Playwright browser
|
|
105
|
+
self._playwright = None
|
|
106
|
+
self._evidence_dir = evidence_dir or (root / ".patchi" / "evidence")
|
|
107
|
+
self._evidence_dir.mkdir(parents=True, exist_ok=True)
|
|
108
|
+
self._evidence: list[dict] = []
|
|
109
|
+
self._screenshot_count = 0
|
|
110
|
+
|
|
111
|
+
async def initialize(self):
|
|
112
|
+
"""Initialize HTTP session and Playwright browser."""
|
|
113
|
+
# HTTP session
|
|
114
|
+
try:
|
|
115
|
+
import aiohttp
|
|
116
|
+
|
|
117
|
+
timeout = aiohttp.ClientTimeout(total=15)
|
|
118
|
+
self.session = aiohttp.ClientSession(timeout=timeout)
|
|
119
|
+
except ImportError:
|
|
120
|
+
_log.warning("aiohttp not available — HTTP attacks limited")
|
|
121
|
+
|
|
122
|
+
# Playwright browser
|
|
123
|
+
try:
|
|
124
|
+
from playwright.async_api import async_playwright
|
|
125
|
+
|
|
126
|
+
self._playwright = await async_playwright().start()
|
|
127
|
+
self.browser = await self._playwright.chromium.launch(
|
|
128
|
+
headless=True,
|
|
129
|
+
args=["--no-sandbox", "--disable-gpu", "--disable-dev-shm-usage"],
|
|
130
|
+
)
|
|
131
|
+
self.on_progress(" Browser initialized (headless Chromium)")
|
|
132
|
+
except ImportError:
|
|
133
|
+
_log.warning("Playwright not available — browser attacks limited")
|
|
134
|
+
except Exception as e:
|
|
135
|
+
_log.warning("Playwright launch failed: %s", e)
|
|
136
|
+
|
|
137
|
+
async def cleanup(self):
|
|
138
|
+
"""Clean up resources."""
|
|
139
|
+
if self.session:
|
|
140
|
+
await self.session.close()
|
|
141
|
+
if self.browser:
|
|
142
|
+
await self.browser.close()
|
|
143
|
+
if self._playwright:
|
|
144
|
+
await self._playwright.stop()
|
|
145
|
+
|
|
146
|
+
# ── Evidence capture ────────────────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
async def _capture_screenshot(self, page, label: str) -> str | None:
|
|
149
|
+
"""Take a screenshot and return its path."""
|
|
150
|
+
if not page:
|
|
151
|
+
return None
|
|
152
|
+
self._screenshot_count += 1
|
|
153
|
+
path = self._evidence_dir / f"{self._screenshot_count:04d}_{label}.png"
|
|
154
|
+
try:
|
|
155
|
+
await page.screenshot(path=str(path), full_page=False)
|
|
156
|
+
return str(path)
|
|
157
|
+
except Exception as e:
|
|
158
|
+
_log.warning("Screenshot failed: %s", e)
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
def _record_evidence(self, step_num: int, action: str, evidence: dict) -> None:
|
|
162
|
+
"""Record evidence for the report."""
|
|
163
|
+
evidence["step"] = step_num
|
|
164
|
+
evidence["action"] = action
|
|
165
|
+
evidence["timestamp"] = time.time()
|
|
166
|
+
self._evidence.append(evidence)
|
|
167
|
+
|
|
168
|
+
async def _run_pentest_tool(self, tool: str, step: dict, scenario: dict, context: dict) -> dict:
|
|
169
|
+
"""Delegate to real PentestRegistry engines (nuclei/sqlmap/shannon etc)."""
|
|
170
|
+
try:
|
|
171
|
+
from patchi.core.security.pentest.registry import PentestRegistry
|
|
172
|
+
|
|
173
|
+
reg = PentestRegistry()
|
|
174
|
+
target = step.get("target") or step.get("url") or self.target_url
|
|
175
|
+
safe = step.get("safe_mode", self.safe_mode)
|
|
176
|
+
# shannon needs repo_root
|
|
177
|
+
extra = {"repo_root": str(self.root), "templates": step.get("templates"), "wordlist": step.get("wordlist"), "data": step.get("data")}
|
|
178
|
+
# Run in thread to avoid blocking event loop (subprocess)
|
|
179
|
+
import asyncio as _aio
|
|
180
|
+
|
|
181
|
+
res = await _aio.to_thread(reg.run, tool, target, safe, self._evidence_dir, extra)
|
|
182
|
+
out: dict = {"success": bool(res.success and not res.error and (res.findings or res.evidence)), "evidence": res.evidence or res.error, "data": {"tool": res.tool, "findings": res.findings, "raw": res.raw_output[:2000], "duration_ms": res.duration_ms}, "error": res.error}
|
|
183
|
+
# Count as success even if 0 findings but no error (target clean)
|
|
184
|
+
if res.success and not res.error and not res.findings:
|
|
185
|
+
out["success"] = True
|
|
186
|
+
out["evidence"] = f"{tool}: no findings (target clean or not vulnerable)"
|
|
187
|
+
return out
|
|
188
|
+
except Exception as exc: # noqa: BLE001
|
|
189
|
+
return {"success": False, "error": f"pentest tool {tool} failed: {exc}"}
|
|
190
|
+
|
|
191
|
+
def get_evidence(self) -> list[dict]:
|
|
192
|
+
"""Return all captured evidence."""
|
|
193
|
+
return list(self._evidence)
|
|
194
|
+
|
|
195
|
+
# ── Execute step ────────────────────────────────────────────────────────
|
|
196
|
+
|
|
197
|
+
async def execute_step(
|
|
198
|
+
self,
|
|
199
|
+
step: dict,
|
|
200
|
+
scenario: dict,
|
|
201
|
+
context: dict,
|
|
202
|
+
) -> AttackStepResult:
|
|
203
|
+
"""Execute a single attack step."""
|
|
204
|
+
start = time.monotonic()
|
|
205
|
+
step_num = step.get("step", 0)
|
|
206
|
+
action = step.get("action", "")
|
|
207
|
+
tool = step.get("tool", "")
|
|
208
|
+
|
|
209
|
+
self.on_progress(f" Step {step_num}: {action} ({tool})")
|
|
210
|
+
|
|
211
|
+
try:
|
|
212
|
+
if tool == "fuzz_params":
|
|
213
|
+
result = await self._fuzz_params(step, scenario, context)
|
|
214
|
+
elif tool == "sql_payload":
|
|
215
|
+
result = await self._sql_payload(step, scenario, context)
|
|
216
|
+
elif tool == "http_request":
|
|
217
|
+
result = await self._http_request(step, scenario, context)
|
|
218
|
+
elif tool == "code_scan":
|
|
219
|
+
result = await self._code_scan(step, scenario, context)
|
|
220
|
+
elif tool == "browser_action":
|
|
221
|
+
result = await self._browser_action(step, scenario, context)
|
|
222
|
+
elif tool == "jwt_tool":
|
|
223
|
+
result = await self._jwt_tool(step, scenario, context)
|
|
224
|
+
elif tool in ("nuclei", "sqlmap", "dalfox", "ffuf", "zap", "shannon"):
|
|
225
|
+
result = await self._run_pentest_tool(tool, step, scenario, context)
|
|
226
|
+
else:
|
|
227
|
+
result = {"success": False, "error": f"Unknown tool: {tool}"}
|
|
228
|
+
|
|
229
|
+
self._record_evidence(
|
|
230
|
+
step_num,
|
|
231
|
+
action,
|
|
232
|
+
{
|
|
233
|
+
"tool": tool,
|
|
234
|
+
"success": result.get("success", False),
|
|
235
|
+
"evidence": result.get("evidence", ""),
|
|
236
|
+
"data": result.get("data", {}),
|
|
237
|
+
},
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
duration = int((time.monotonic() - start) * 1000)
|
|
241
|
+
return AttackStepResult(
|
|
242
|
+
step=step_num,
|
|
243
|
+
action=action,
|
|
244
|
+
success=result.get("success", False),
|
|
245
|
+
evidence=result.get("evidence", ""),
|
|
246
|
+
response_data=result.get("data", {}),
|
|
247
|
+
error=result.get("error", ""),
|
|
248
|
+
duration_ms=duration,
|
|
249
|
+
)
|
|
250
|
+
except Exception as e:
|
|
251
|
+
duration = int((time.monotonic() - start) * 1000)
|
|
252
|
+
return AttackStepResult(
|
|
253
|
+
step=step_num,
|
|
254
|
+
action=action,
|
|
255
|
+
success=False,
|
|
256
|
+
error=str(e),
|
|
257
|
+
duration_ms=duration,
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
# ── HTTP requests (real) ────────────────────────────────────────────────
|
|
261
|
+
|
|
262
|
+
async def _http_request(self, step: dict, scenario: dict, context: dict) -> dict:
|
|
263
|
+
"""Send real HTTP request with payload and capture response."""
|
|
264
|
+
if not self.session:
|
|
265
|
+
return {"success": False, "error": "No HTTP session"}
|
|
266
|
+
|
|
267
|
+
url = step.get("url", self.target_url)
|
|
268
|
+
method = step.get("method", "GET").upper()
|
|
269
|
+
payloads = step.get("payloads", [])
|
|
270
|
+
headers = step.get("headers", {})
|
|
271
|
+
data = step.get("data", {})
|
|
272
|
+
|
|
273
|
+
# Apply payloads to URL params or form data
|
|
274
|
+
if payloads:
|
|
275
|
+
for _i, payload in enumerate(payloads[:5]): # cap at 5
|
|
276
|
+
test_url = url
|
|
277
|
+
test_data = dict(data)
|
|
278
|
+
|
|
279
|
+
# Inject payload into URL params
|
|
280
|
+
if "?" in test_url:
|
|
281
|
+
test_url += f"&input={payload}"
|
|
282
|
+
else:
|
|
283
|
+
test_url += f"?input={payload}"
|
|
284
|
+
|
|
285
|
+
# Also inject into form data
|
|
286
|
+
if test_data:
|
|
287
|
+
for key in list(test_data.keys()):
|
|
288
|
+
test_data[key] = payload
|
|
289
|
+
|
|
290
|
+
try:
|
|
291
|
+
async with self.session.request(
|
|
292
|
+
method, test_url, headers=headers, json=test_data if test_data else None
|
|
293
|
+
) as resp:
|
|
294
|
+
body = await resp.text()
|
|
295
|
+
# Check for SQL error messages
|
|
296
|
+
error_indicators = [
|
|
297
|
+
"sql syntax",
|
|
298
|
+
"mysql",
|
|
299
|
+
"sqlite",
|
|
300
|
+
"postgresql",
|
|
301
|
+
"ora-",
|
|
302
|
+
"unquoted parameter",
|
|
303
|
+
"microsoft ole db",
|
|
304
|
+
"odbc",
|
|
305
|
+
"jdbc",
|
|
306
|
+
"syntax error",
|
|
307
|
+
]
|
|
308
|
+
has_error = any(ind in body.lower() for ind in error_indicators)
|
|
309
|
+
|
|
310
|
+
if has_error:
|
|
311
|
+
return {
|
|
312
|
+
"success": True,
|
|
313
|
+
"evidence": f"SQL error reflected with payload: {payload[:50]}",
|
|
314
|
+
"data": {
|
|
315
|
+
"payload": payload,
|
|
316
|
+
"status": resp.status,
|
|
317
|
+
"error_reflection": body[:200],
|
|
318
|
+
},
|
|
319
|
+
}
|
|
320
|
+
except Exception as e:
|
|
321
|
+
_log.debug("HTTP request failed: %s", e)
|
|
322
|
+
continue
|
|
323
|
+
|
|
324
|
+
# No payloads — just send a normal request
|
|
325
|
+
try:
|
|
326
|
+
async with self.session.request(method, url, headers=headers) as resp:
|
|
327
|
+
body = await resp.text()
|
|
328
|
+
return {
|
|
329
|
+
"success": resp.status < 400,
|
|
330
|
+
"evidence": f"HTTP {method} {url} → {resp.status} ({len(body)} bytes)",
|
|
331
|
+
"data": {
|
|
332
|
+
"status": resp.status,
|
|
333
|
+
"headers": dict(resp.headers),
|
|
334
|
+
"body_preview": body[:500],
|
|
335
|
+
},
|
|
336
|
+
}
|
|
337
|
+
except Exception as e:
|
|
338
|
+
return {"success": False, "error": str(e)}
|
|
339
|
+
|
|
340
|
+
async def _fuzz_params(self, step: dict, scenario: dict, context: dict) -> dict:
|
|
341
|
+
"""Fuzz parameters to find injection points."""
|
|
342
|
+
if not self.session:
|
|
343
|
+
return {"success": False, "error": "No HTTP session"}
|
|
344
|
+
|
|
345
|
+
payloads = step.get("payloads", [])
|
|
346
|
+
if self.safe_mode:
|
|
347
|
+
payloads = [
|
|
348
|
+
p
|
|
349
|
+
for p in payloads
|
|
350
|
+
if not any(
|
|
351
|
+
d in p.upper() for d in ["DROP", "DELETE", "UPDATE", "INSERT", "EXEC", "SYSTEM"]
|
|
352
|
+
)
|
|
353
|
+
]
|
|
354
|
+
|
|
355
|
+
url = step.get("url", self.target_url)
|
|
356
|
+
findings = []
|
|
357
|
+
|
|
358
|
+
for payload in payloads[:10]: # cap at 10
|
|
359
|
+
try:
|
|
360
|
+
test_url = f"{url}?fuzz={payload}"
|
|
361
|
+
async with self.session.get(test_url) as resp:
|
|
362
|
+
body = await resp.text()
|
|
363
|
+
# Check for error reflection, stack traces, or interesting responses
|
|
364
|
+
if resp.status >= 500:
|
|
365
|
+
findings.append(
|
|
366
|
+
{"payload": payload, "status": resp.status, "type": "server_error"}
|
|
367
|
+
)
|
|
368
|
+
elif any(
|
|
369
|
+
kw in body.lower() for kw in ["traceback", "exception", "stack trace"]
|
|
370
|
+
):
|
|
371
|
+
findings.append(
|
|
372
|
+
{"payload": payload, "status": resp.status, "type": "info_disclosure"}
|
|
373
|
+
)
|
|
374
|
+
elif payload in body: # payload reflected in response
|
|
375
|
+
findings.append(
|
|
376
|
+
{"payload": payload, "status": resp.status, "type": "reflected"}
|
|
377
|
+
)
|
|
378
|
+
except Exception:
|
|
379
|
+
continue
|
|
380
|
+
|
|
381
|
+
return {
|
|
382
|
+
"success": True,
|
|
383
|
+
"evidence": f"Fuzzed {len(payloads)} payloads, found {len(findings)} interesting responses",
|
|
384
|
+
"data": {"findings": findings, "tested": len(payloads)},
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
async def _sql_payload(self, step: dict, scenario: dict, context: dict) -> dict:
|
|
388
|
+
"""Execute SQL injection payload and check for error reflection."""
|
|
389
|
+
if not self.session:
|
|
390
|
+
return {"success": False, "error": "No HTTP session"}
|
|
391
|
+
|
|
392
|
+
url = step.get("url", self.target_url)
|
|
393
|
+
payloads = step.get(
|
|
394
|
+
"payloads",
|
|
395
|
+
[
|
|
396
|
+
"' OR '1'='1",
|
|
397
|
+
"' OR 1=1--",
|
|
398
|
+
"admin'--",
|
|
399
|
+
"' UNION SELECT NULL--",
|
|
400
|
+
"1; DROP TABLE users--",
|
|
401
|
+
],
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
sql_indicators = [
|
|
405
|
+
"sql syntax",
|
|
406
|
+
"mysql",
|
|
407
|
+
"sqlite",
|
|
408
|
+
"postgresql",
|
|
409
|
+
"ora-",
|
|
410
|
+
"unquoted",
|
|
411
|
+
"microsoft ole db",
|
|
412
|
+
"odbc",
|
|
413
|
+
"warning.*mysql",
|
|
414
|
+
"unclosed quotation mark",
|
|
415
|
+
]
|
|
416
|
+
|
|
417
|
+
results = []
|
|
418
|
+
for payload in payloads:
|
|
419
|
+
try:
|
|
420
|
+
test_url = f"{url}?id={payload}"
|
|
421
|
+
async with self.session.get(test_url) as resp:
|
|
422
|
+
body = await resp.text()
|
|
423
|
+
has_sql_error = any(ind in body.lower() for ind in sql_indicators)
|
|
424
|
+
results.append(
|
|
425
|
+
{
|
|
426
|
+
"payload": payload,
|
|
427
|
+
"status": resp.status,
|
|
428
|
+
"sql_error": has_sql_error,
|
|
429
|
+
"response_preview": body[:200] if has_sql_error else "",
|
|
430
|
+
}
|
|
431
|
+
)
|
|
432
|
+
if has_sql_error:
|
|
433
|
+
return {
|
|
434
|
+
"success": True,
|
|
435
|
+
"evidence": f"SQL injection confirmed: {payload} → SQL error reflected",
|
|
436
|
+
"data": {"results": results, "confirmed": True},
|
|
437
|
+
}
|
|
438
|
+
except Exception:
|
|
439
|
+
continue
|
|
440
|
+
|
|
441
|
+
return {
|
|
442
|
+
"success": True,
|
|
443
|
+
"evidence": f"Tested {len(payloads)} SQL payloads, no error reflection found",
|
|
444
|
+
"data": {"results": results, "confirmed": False},
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
# ── Browser automation (real Playwright) ────────────────────────────────
|
|
448
|
+
|
|
449
|
+
async def _browser_action(self, step: dict, scenario: dict, context: dict) -> dict:
|
|
450
|
+
"""Execute real browser automation with Playwright."""
|
|
451
|
+
if not self.browser:
|
|
452
|
+
return {"success": False, "error": "Browser not available"}
|
|
453
|
+
|
|
454
|
+
action_type = step.get("action_type", "navigate")
|
|
455
|
+
url = step.get("url", self.target_url)
|
|
456
|
+
selector = step.get("selector", "")
|
|
457
|
+
value = step.get("value", "")
|
|
458
|
+
|
|
459
|
+
page = await self.browser.new_page()
|
|
460
|
+
evidence = {"url": url, "action": action_type}
|
|
461
|
+
|
|
462
|
+
try:
|
|
463
|
+
if action_type == "navigate":
|
|
464
|
+
await page.goto(url, wait_until="domcontentloaded", timeout=10000)
|
|
465
|
+
evidence["title"] = await page.title()
|
|
466
|
+
evidence["screenshot"] = await self._capture_screenshot(
|
|
467
|
+
page, f"nav_{step.get('step', 0)}"
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
elif action_type == "click":
|
|
471
|
+
await page.goto(url, wait_until="domcontentloaded", timeout=10000)
|
|
472
|
+
if selector:
|
|
473
|
+
await page.click(selector)
|
|
474
|
+
await page.wait_for_load_state("domcontentloaded")
|
|
475
|
+
evidence["screenshot"] = await self._capture_screenshot(
|
|
476
|
+
page, f"click_{step.get('step', 0)}"
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
elif action_type == "fill":
|
|
480
|
+
await page.goto(url, wait_until="domcontentloaded", timeout=10000)
|
|
481
|
+
if selector and value:
|
|
482
|
+
await page.fill(selector, value)
|
|
483
|
+
evidence["screenshot"] = await self._capture_screenshot(
|
|
484
|
+
page, f"fill_{step.get('step', 0)}"
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
elif action_type == "fill_and_submit":
|
|
488
|
+
await page.goto(url, wait_until="domcontentloaded", timeout=10000)
|
|
489
|
+
if selector and value:
|
|
490
|
+
await page.fill(selector, value)
|
|
491
|
+
# Try to find and click submit button
|
|
492
|
+
submit = await page.query_selector(
|
|
493
|
+
"button[type=submit], input[type=submit], button:has-text('Login'), button:has-text('Submit')"
|
|
494
|
+
)
|
|
495
|
+
if submit:
|
|
496
|
+
await submit.click()
|
|
497
|
+
await page.wait_for_load_state("domcontentloaded")
|
|
498
|
+
evidence["screenshot"] = await self._capture_screenshot(
|
|
499
|
+
page, f"submit_{step.get('step', 0)}"
|
|
500
|
+
)
|
|
501
|
+
evidence["final_url"] = page.url
|
|
502
|
+
|
|
503
|
+
elif action_type == "check_auth_bypass":
|
|
504
|
+
await page.goto(url, wait_until="domcontentloaded", timeout=10000)
|
|
505
|
+
# Check if we can access protected resource without auth
|
|
506
|
+
content = await page.content()
|
|
507
|
+
has_login_form = bool(await page.query_selector("input[type=password]"))
|
|
508
|
+
has_dashboard = any(
|
|
509
|
+
kw in content.lower() for kw in ["dashboard", "welcome", "logout", "admin"]
|
|
510
|
+
)
|
|
511
|
+
evidence["has_login_form"] = has_login_form
|
|
512
|
+
evidence["has_dashboard"] = has_dashboard
|
|
513
|
+
evidence["bypassed"] = has_dashboard and not has_login_form
|
|
514
|
+
evidence["screenshot"] = await self._capture_screenshot(
|
|
515
|
+
page, f"bypass_{step.get('step', 0)}"
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
elif action_type == "screenshot":
|
|
519
|
+
await page.goto(url, wait_until="domcontentloaded", timeout=10000)
|
|
520
|
+
evidence["screenshot"] = await self._capture_screenshot(
|
|
521
|
+
page, f"capture_{step.get('step', 0)}"
|
|
522
|
+
)
|
|
523
|
+
evidence["title"] = await page.title()
|
|
524
|
+
|
|
525
|
+
elif action_type == "intercept_requests":
|
|
526
|
+
captured_requests = []
|
|
527
|
+
|
|
528
|
+
async def on_request(request):
|
|
529
|
+
captured_requests.append(
|
|
530
|
+
{
|
|
531
|
+
"method": request.method,
|
|
532
|
+
"url": request.url,
|
|
533
|
+
"headers": dict(request.headers),
|
|
534
|
+
}
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
page.on("request", on_request)
|
|
538
|
+
await page.goto(url, wait_until="domcontentloaded", timeout=10000)
|
|
539
|
+
await page.wait_for_timeout(2000) # let requests settle
|
|
540
|
+
evidence["captured_requests"] = captured_requests[:20]
|
|
541
|
+
evidence["request_count"] = len(captured_requests)
|
|
542
|
+
evidence["screenshot"] = await self._capture_screenshot(
|
|
543
|
+
page, f"intercept_{step.get('step', 0)}"
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
else:
|
|
547
|
+
evidence["error"] = f"Unknown browser action: {action_type}"
|
|
548
|
+
|
|
549
|
+
return {
|
|
550
|
+
"success": True,
|
|
551
|
+
"evidence": f"Browser {action_type} on {url}",
|
|
552
|
+
"data": evidence,
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
except Exception as e:
|
|
556
|
+
evidence["error"] = str(e)
|
|
557
|
+
return {
|
|
558
|
+
"success": False,
|
|
559
|
+
"evidence": f"Browser action failed: {e}",
|
|
560
|
+
"data": evidence,
|
|
561
|
+
}
|
|
562
|
+
finally:
|
|
563
|
+
await page.close()
|
|
564
|
+
|
|
565
|
+
# ── Code scan (static) ──────────────────────────────────────────────────
|
|
566
|
+
|
|
567
|
+
async def _code_scan(self, step: dict, scenario: dict, context: dict) -> dict:
|
|
568
|
+
"""Scan codebase for vulnerability patterns."""
|
|
569
|
+
patterns = step.get("patterns", [])
|
|
570
|
+
findings = []
|
|
571
|
+
|
|
572
|
+
for py_file in sorted(self.root.rglob("*.py")):
|
|
573
|
+
if ".patchi" in str(py_file) or "__pycache__" in str(py_file):
|
|
574
|
+
continue
|
|
575
|
+
try:
|
|
576
|
+
content = py_file.read_text(encoding="utf-8", errors="ignore")
|
|
577
|
+
except OSError:
|
|
578
|
+
continue
|
|
579
|
+
|
|
580
|
+
for pattern in patterns:
|
|
581
|
+
if pattern.lower() in content.lower():
|
|
582
|
+
# Find the line
|
|
583
|
+
for i, line in enumerate(content.splitlines(), 1):
|
|
584
|
+
if pattern.lower() in line.lower():
|
|
585
|
+
findings.append(
|
|
586
|
+
{
|
|
587
|
+
"file": str(py_file.relative_to(self.root)),
|
|
588
|
+
"line": i,
|
|
589
|
+
"pattern": pattern,
|
|
590
|
+
"context": line.strip()[:100],
|
|
591
|
+
}
|
|
592
|
+
)
|
|
593
|
+
break
|
|
594
|
+
|
|
595
|
+
return {
|
|
596
|
+
"success": True,
|
|
597
|
+
"evidence": f"Scanned for {len(patterns)} patterns, found {len(findings)} matches",
|
|
598
|
+
"data": {"findings": findings[:20]},
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
# ── JWT tool ────────────────────────────────────────────────────────────
|
|
602
|
+
|
|
603
|
+
async def _jwt_tool(self, step: dict, scenario: dict, context: dict) -> dict:
|
|
604
|
+
"""Manipulate JWT tokens for auth testing."""
|
|
605
|
+
action = step.get("jwt_action", "decode")
|
|
606
|
+
token = step.get("token", "")
|
|
607
|
+
|
|
608
|
+
try:
|
|
609
|
+
import base64
|
|
610
|
+
import json as _json
|
|
611
|
+
|
|
612
|
+
if action == "decode" and token:
|
|
613
|
+
# Decode JWT without verification
|
|
614
|
+
parts = token.split(".")
|
|
615
|
+
if len(parts) >= 2:
|
|
616
|
+
# Decode payload
|
|
617
|
+
payload = parts[1] + "=" * (4 - len(parts[1]) % 4)
|
|
618
|
+
decoded = _json.loads(base64.urlsafe_b64decode(payload))
|
|
619
|
+
return {
|
|
620
|
+
"success": True,
|
|
621
|
+
"evidence": f"JWT decoded: {list(decoded.keys())}",
|
|
622
|
+
"data": {
|
|
623
|
+
"header": _json.loads(base64.urlsafe_b64decode(parts[0] + "==")),
|
|
624
|
+
"payload": decoded,
|
|
625
|
+
},
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
elif action == "alg_none":
|
|
629
|
+
# Try to forge token with alg:none
|
|
630
|
+
if token:
|
|
631
|
+
parts = token.split(".")
|
|
632
|
+
if len(parts) >= 3:
|
|
633
|
+
forged = parts[0] + "." + parts[1] + "."
|
|
634
|
+
return {
|
|
635
|
+
"success": True,
|
|
636
|
+
"evidence": "Forged JWT with alg:none (test if server accepts)",
|
|
637
|
+
"data": {"forged_token": forged[:100]},
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
return {"success": False, "error": f"Unknown JWT action: {action}"}
|
|
641
|
+
|
|
642
|
+
except Exception as e:
|
|
643
|
+
return {"success": False, "error": str(e)}
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
class RedTeamEngine:
|
|
647
|
+
"""
|
|
648
|
+
Main Red Team Engine.
|
|
649
|
+
|
|
650
|
+
Orchestrates attack scenarios, manages execution, and produces reports.
|
|
651
|
+
"""
|
|
652
|
+
|
|
653
|
+
def __init__(
|
|
654
|
+
self,
|
|
655
|
+
root: Path,
|
|
656
|
+
target_url: str = None,
|
|
657
|
+
safe_mode: bool = True,
|
|
658
|
+
on_progress: Callable[[str], None] = None,
|
|
659
|
+
):
|
|
660
|
+
self.root = root
|
|
661
|
+
self.target_url = target_url
|
|
662
|
+
self.safe_mode = safe_mode
|
|
663
|
+
self.on_progress = on_progress or (lambda _: None)
|
|
664
|
+
self.scenarios_dir = root / "patchi" / "core" / "security" / "attack_scenarios"
|
|
665
|
+
self._scenarios_cache: dict[str, dict] = {}
|
|
666
|
+
self.domain_loader = DomainLoader(root)
|
|
667
|
+
|
|
668
|
+
def _load_scenarios(self) -> dict[str, dict]:
|
|
669
|
+
"""Load all attack scenarios from YAML files."""
|
|
670
|
+
if self._scenarios_cache:
|
|
671
|
+
return self._scenarios_cache
|
|
672
|
+
|
|
673
|
+
scenarios = {}
|
|
674
|
+
if self.scenarios_dir.exists():
|
|
675
|
+
for yaml_file in self.scenarios_dir.glob("*.yaml"):
|
|
676
|
+
try:
|
|
677
|
+
with open(yaml_file) as f:
|
|
678
|
+
data = yaml.safe_load(f)
|
|
679
|
+
if data and "scenarios" in data:
|
|
680
|
+
for scenario in data["scenarios"]:
|
|
681
|
+
scenarios[scenario["id"]] = scenario
|
|
682
|
+
except Exception as e:
|
|
683
|
+
_log.warning(f"Failed to load scenarios from {yaml_file}: {e}")
|
|
684
|
+
|
|
685
|
+
self._scenarios_cache = scenarios
|
|
686
|
+
return scenarios
|
|
687
|
+
|
|
688
|
+
def select_scenarios(
|
|
689
|
+
self,
|
|
690
|
+
project_context: dict,
|
|
691
|
+
scope: str = "full",
|
|
692
|
+
intensity: str = "active",
|
|
693
|
+
forced_scenarios: list[str] = None,
|
|
694
|
+
) -> list[dict]:
|
|
695
|
+
"""Select relevant attack scenarios based on project context."""
|
|
696
|
+
all_scenarios = self._load_scenarios()
|
|
697
|
+
forced_scenarios = forced_scenarios or []
|
|
698
|
+
|
|
699
|
+
# Get active security domains
|
|
700
|
+
project_context.get("active_security_domains", [])
|
|
701
|
+
frameworks = [f.get("name", "").lower() for f in project_context.get("frameworks", [])]
|
|
702
|
+
component_type = project_context.get("component_type", "")
|
|
703
|
+
|
|
704
|
+
# Score scenarios
|
|
705
|
+
# Active domains use compound ids ("injection-sql"); scenario categories
|
|
706
|
+
# are simple tokens ("injection", "sql"). Match on both the full id and
|
|
707
|
+
# its hyphen-split parts so a domain activates its whole family.
|
|
708
|
+
domain_tokens: set[str] = set()
|
|
709
|
+
for d in project_context.get("active_security_domains", []):
|
|
710
|
+
domain_tokens.update(str(d).lower().split("-"))
|
|
711
|
+
|
|
712
|
+
scored = []
|
|
713
|
+
for scenario_id, scenario in all_scenarios.items():
|
|
714
|
+
score = 0
|
|
715
|
+
|
|
716
|
+
# Domain match (full id beats individual token)
|
|
717
|
+
sid_lower = scenario_id.lower()
|
|
718
|
+
for d in project_context.get("active_security_domains", []):
|
|
719
|
+
if str(d).lower() == sid_lower:
|
|
720
|
+
score += 15
|
|
721
|
+
break
|
|
722
|
+
|
|
723
|
+
category = scenario.get("category", "")
|
|
724
|
+
subcategory = scenario.get("subcategory", "")
|
|
725
|
+
if category and category in domain_tokens:
|
|
726
|
+
score += 6
|
|
727
|
+
if subcategory and subcategory in domain_tokens:
|
|
728
|
+
score += 8 # subcategory is a precise match — weight it highest
|
|
729
|
+
|
|
730
|
+
# Framework match
|
|
731
|
+
for fw in frameworks:
|
|
732
|
+
if fw in scenario_id.lower() or fw in category:
|
|
733
|
+
score += 3
|
|
734
|
+
|
|
735
|
+
# Component type match
|
|
736
|
+
if component_type:
|
|
737
|
+
if "frontend" in component_type and "xss" in scenario_id:
|
|
738
|
+
score += 5
|
|
739
|
+
if "backend" in component_type and "sqli" in scenario_id:
|
|
740
|
+
score += 5
|
|
741
|
+
if "backend" in component_type and "sql" in subcategory:
|
|
742
|
+
score += 5
|
|
743
|
+
if "api" in component_type and "auth" in scenario_id:
|
|
744
|
+
score += 5
|
|
745
|
+
|
|
746
|
+
# Scope filter
|
|
747
|
+
if scope == "api" and "browser" in scenario_id:
|
|
748
|
+
score -= 10
|
|
749
|
+
if scope == "web" and "sql" in scenario_id:
|
|
750
|
+
score -= 5
|
|
751
|
+
|
|
752
|
+
# Intensity filter
|
|
753
|
+
if intensity == "passive" and scenario.get("severity") == "critical":
|
|
754
|
+
score -= 5
|
|
755
|
+
|
|
756
|
+
# Forced scenarios get high score
|
|
757
|
+
if scenario_id in forced_scenarios:
|
|
758
|
+
score += 100
|
|
759
|
+
|
|
760
|
+
if score > 0:
|
|
761
|
+
scored.append((score, scenario))
|
|
762
|
+
|
|
763
|
+
# Sort by score
|
|
764
|
+
scored.sort(key=lambda x: x[0], reverse=True)
|
|
765
|
+
|
|
766
|
+
# Limit based on intensity
|
|
767
|
+
max_scenarios = {"passive": 10, "active": 25, "aggressive": 50}.get(intensity, 25)
|
|
768
|
+
selected = [s for _, s in scored[:max_scenarios]]
|
|
769
|
+
|
|
770
|
+
_log.info(f"Selected {len(selected)} scenarios for {scope}/{intensity} assessment")
|
|
771
|
+
return selected
|
|
772
|
+
|
|
773
|
+
async def run_assessment(
|
|
774
|
+
self,
|
|
775
|
+
project_context: dict,
|
|
776
|
+
scope: str = "full",
|
|
777
|
+
intensity: str = "active",
|
|
778
|
+
forced_scenarios: list[str] = None,
|
|
779
|
+
max_scenarios: int = None,
|
|
780
|
+
) -> RedTeamReport:
|
|
781
|
+
"""Run a complete red team assessment."""
|
|
782
|
+
assessment_id = f"rt-{uuid.uuid4().hex[:8]}"
|
|
783
|
+
start_time = time.monotonic()
|
|
784
|
+
|
|
785
|
+
self.on_progress(f"🎯 Starting Red Team Assessment: {assessment_id}")
|
|
786
|
+
self.on_progress(
|
|
787
|
+
f" Scope: {scope} | Intensity: {intensity} | Safe Mode: {self.safe_mode}"
|
|
788
|
+
)
|
|
789
|
+
|
|
790
|
+
# Select scenarios
|
|
791
|
+
scenarios = self.select_scenarios(project_context, scope, intensity, forced_scenarios)
|
|
792
|
+
if max_scenarios:
|
|
793
|
+
scenarios = scenarios[:max_scenarios]
|
|
794
|
+
|
|
795
|
+
self.on_progress(f"📋 Selected {len(scenarios)} attack scenarios")
|
|
796
|
+
|
|
797
|
+
# Initialize executor
|
|
798
|
+
evidence_dir = self.root / ".patchi" / "evidence" / assessment_id
|
|
799
|
+
executor = AttackExecutor(
|
|
800
|
+
self.root,
|
|
801
|
+
target_url=self.target_url,
|
|
802
|
+
safe_mode=self.safe_mode,
|
|
803
|
+
on_progress=self.on_progress,
|
|
804
|
+
evidence_dir=evidence_dir,
|
|
805
|
+
)
|
|
806
|
+
await executor.initialize()
|
|
807
|
+
|
|
808
|
+
report = RedTeamReport(
|
|
809
|
+
assessment_id=assessment_id,
|
|
810
|
+
project_root=str(self.root),
|
|
811
|
+
scope=scope,
|
|
812
|
+
intensity=intensity,
|
|
813
|
+
)
|
|
814
|
+
|
|
815
|
+
try:
|
|
816
|
+
# Run scenarios
|
|
817
|
+
for i, scenario in enumerate(scenarios):
|
|
818
|
+
self.on_progress(f"⚔️ Scenario {i + 1}/{len(scenarios)}: {scenario['name']}")
|
|
819
|
+
|
|
820
|
+
result = await self._run_scenario(scenario, executor, project_context)
|
|
821
|
+
report.scenarios_run.append(result)
|
|
822
|
+
|
|
823
|
+
# Count findings
|
|
824
|
+
report.total_findings += len(result.findings)
|
|
825
|
+
for finding in result.findings:
|
|
826
|
+
sev = finding.severity.value
|
|
827
|
+
report.by_severity[sev] = report.by_severity.get(sev, 0) + 1
|
|
828
|
+
|
|
829
|
+
# Collect remediation playbooks
|
|
830
|
+
playbook = scenario.get("remediation_playbook")
|
|
831
|
+
if playbook and playbook not in report.remediation_playbooks:
|
|
832
|
+
report.remediation_playbooks.append(playbook)
|
|
833
|
+
|
|
834
|
+
# Capture evidence from executor
|
|
835
|
+
report.exploit_evidence = {
|
|
836
|
+
"screenshots": len(
|
|
837
|
+
[e for e in executor.get_evidence() if e.get("data", {}).get("screenshot")]
|
|
838
|
+
),
|
|
839
|
+
"total_steps": len(executor.get_evidence()),
|
|
840
|
+
"evidence_dir": str(executor._evidence_dir),
|
|
841
|
+
"evidence": executor.get_evidence()[:50],
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
finally:
|
|
845
|
+
await executor.cleanup()
|
|
846
|
+
|
|
847
|
+
report.completed_at = datetime.now(UTC).isoformat()
|
|
848
|
+
report.duration_ms = int((time.monotonic() - start_time) * 1000)
|
|
849
|
+
|
|
850
|
+
self.on_progress(
|
|
851
|
+
f"✅ Assessment complete: {report.total_findings} findings in {report.duration_ms}ms"
|
|
852
|
+
)
|
|
853
|
+
|
|
854
|
+
# Save report
|
|
855
|
+
await self._save_report(report)
|
|
856
|
+
|
|
857
|
+
return report
|
|
858
|
+
|
|
859
|
+
async def _run_scenario(
|
|
860
|
+
self,
|
|
861
|
+
scenario: dict,
|
|
862
|
+
executor: AttackExecutor,
|
|
863
|
+
project_context: dict,
|
|
864
|
+
) -> ScenarioResult:
|
|
865
|
+
"""Run a single attack scenario."""
|
|
866
|
+
scenario_id = scenario["id"]
|
|
867
|
+
start_time = time.monotonic()
|
|
868
|
+
|
|
869
|
+
result = ScenarioResult(
|
|
870
|
+
scenario_id=scenario_id,
|
|
871
|
+
scenario_name=scenario["name"],
|
|
872
|
+
status="running",
|
|
873
|
+
total_steps=len(scenario.get("attack_steps", [])),
|
|
874
|
+
)
|
|
875
|
+
|
|
876
|
+
steps = scenario.get("attack_steps", [])
|
|
877
|
+
context = {"scenario": scenario, "project_context": project_context}
|
|
878
|
+
|
|
879
|
+
for step in steps:
|
|
880
|
+
step_result = await executor.execute_step(step, scenario, context)
|
|
881
|
+
result.step_results.append(step_result)
|
|
882
|
+
result.steps_completed += 1
|
|
883
|
+
|
|
884
|
+
if not step_result.success and step.get("required", True):
|
|
885
|
+
result.status = "failed"
|
|
886
|
+
break
|
|
887
|
+
|
|
888
|
+
if result.status == "running":
|
|
889
|
+
result.status = "success"
|
|
890
|
+
|
|
891
|
+
# Generate findings from successful steps
|
|
892
|
+
result.findings = self._generate_findings(scenario, result)
|
|
893
|
+
|
|
894
|
+
# Collect exploit evidence
|
|
895
|
+
result.exploit_evidence = self._collect_evidence(scenario, result)
|
|
896
|
+
|
|
897
|
+
result.completed_at = datetime.now(UTC).isoformat()
|
|
898
|
+
result.duration_ms = int((time.monotonic() - start_time) * 1000)
|
|
899
|
+
|
|
900
|
+
return result
|
|
901
|
+
|
|
902
|
+
def _generate_findings(self, scenario: dict, result: ScenarioResult) -> list[Finding]:
|
|
903
|
+
"""Generate findings from scenario results."""
|
|
904
|
+
findings = []
|
|
905
|
+
|
|
906
|
+
if result.status != "success":
|
|
907
|
+
return findings
|
|
908
|
+
|
|
909
|
+
# Create finding based on scenario
|
|
910
|
+
finding = Finding(
|
|
911
|
+
agent="RedTeamEngine",
|
|
912
|
+
type=scenario.get("category", "attack"),
|
|
913
|
+
severity=Severity(scenario.get("severity", "high")),
|
|
914
|
+
file="",
|
|
915
|
+
line=0,
|
|
916
|
+
message=f"{scenario['name']}: {scenario.get('description', '')}",
|
|
917
|
+
detail=f"Attack scenario {scenario['id']} completed successfully. Steps: {result.steps_completed}/{result.total_steps}",
|
|
918
|
+
cwe=scenario.get("cwe", ""),
|
|
919
|
+
suggestion=f"Apply remediation playbook: {scenario.get('remediation_playbook', 'manual review')}",
|
|
920
|
+
extra={
|
|
921
|
+
"owasp": scenario.get("owasp", ""),
|
|
922
|
+
"remediation_playbook": scenario.get("remediation_playbook", ""),
|
|
923
|
+
"tags": scenario.get("tags", []),
|
|
924
|
+
"scenario_id": scenario["id"],
|
|
925
|
+
},
|
|
926
|
+
)
|
|
927
|
+
findings.append(finding)
|
|
928
|
+
|
|
929
|
+
# Add findings for each successful step with evidence
|
|
930
|
+
for step_result in result.step_results:
|
|
931
|
+
if step_result.success and step_result.evidence:
|
|
932
|
+
step_finding = Finding(
|
|
933
|
+
agent="RedTeamEngine",
|
|
934
|
+
type=f"{scenario.get('category', 'attack')}.step",
|
|
935
|
+
severity=Severity.MEDIUM,
|
|
936
|
+
file="",
|
|
937
|
+
line=0,
|
|
938
|
+
message=f"Step {step_result.step} ({step_result.action}): {step_result.evidence}",
|
|
939
|
+
detail=f"Attack step evidence: {step_result.evidence}",
|
|
940
|
+
tags=["evidence", "step"],
|
|
941
|
+
)
|
|
942
|
+
findings.append(step_finding)
|
|
943
|
+
|
|
944
|
+
return findings
|
|
945
|
+
|
|
946
|
+
def _collect_evidence(self, scenario: dict, result: ScenarioResult) -> dict:
|
|
947
|
+
"""Collect exploit evidence for reporting."""
|
|
948
|
+
return {
|
|
949
|
+
"scenario_id": scenario["id"],
|
|
950
|
+
"steps": [
|
|
951
|
+
{
|
|
952
|
+
"step": sr.step,
|
|
953
|
+
"action": sr.action,
|
|
954
|
+
"success": sr.success,
|
|
955
|
+
"evidence": sr.evidence,
|
|
956
|
+
"duration_ms": sr.duration_ms,
|
|
957
|
+
}
|
|
958
|
+
for sr in result.step_results
|
|
959
|
+
],
|
|
960
|
+
"detection_signatures": scenario.get("detection_signatures", []),
|
|
961
|
+
"verification_method": scenario.get("verification", []),
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
async def _save_report(self, report: RedTeamReport):
|
|
965
|
+
"""Save report to memory and file."""
|
|
966
|
+
from patchi.core import memory as mem
|
|
967
|
+
|
|
968
|
+
# Save to scan results
|
|
969
|
+
mem.save_scan_result(
|
|
970
|
+
"RedTeamEngine",
|
|
971
|
+
{
|
|
972
|
+
"assessment_id": report.assessment_id,
|
|
973
|
+
"findings": [f.to_dict() for sr in report.scenarios_run for f in sr.findings],
|
|
974
|
+
"summary": {
|
|
975
|
+
"total_findings": report.total_findings,
|
|
976
|
+
"by_severity": report.by_severity,
|
|
977
|
+
"scenarios_run": len(report.scenarios_run),
|
|
978
|
+
"duration_ms": report.duration_ms,
|
|
979
|
+
},
|
|
980
|
+
},
|
|
981
|
+
self.root,
|
|
982
|
+
)
|
|
983
|
+
|
|
984
|
+
# Save detailed report as JSON
|
|
985
|
+
report_path = self.root / ".patchi" / "reports" / f"redteam_{report.assessment_id}.json"
|
|
986
|
+
report_path.parent.mkdir(parents=True, exist_ok=True)
|
|
987
|
+
|
|
988
|
+
import json
|
|
989
|
+
|
|
990
|
+
report_data = {
|
|
991
|
+
"assessment_id": report.assessment_id,
|
|
992
|
+
"project_root": report.project_root,
|
|
993
|
+
"scope": report.scope,
|
|
994
|
+
"intensity": report.intensity,
|
|
995
|
+
"started_at": report.started_at,
|
|
996
|
+
"completed_at": report.completed_at,
|
|
997
|
+
"duration_ms": report.duration_ms,
|
|
998
|
+
"total_findings": report.total_findings,
|
|
999
|
+
"by_severity": report.by_severity,
|
|
1000
|
+
"scenarios": [
|
|
1001
|
+
{
|
|
1002
|
+
"id": sr.scenario_id,
|
|
1003
|
+
"name": sr.scenario_name,
|
|
1004
|
+
"status": sr.status,
|
|
1005
|
+
"steps_completed": sr.steps_completed,
|
|
1006
|
+
"total_steps": sr.total_steps,
|
|
1007
|
+
"findings": [f.to_dict() for f in sr.findings],
|
|
1008
|
+
"evidence": sr.exploit_evidence,
|
|
1009
|
+
"duration_ms": sr.duration_ms,
|
|
1010
|
+
}
|
|
1011
|
+
for sr in report.scenarios_run
|
|
1012
|
+
],
|
|
1013
|
+
"remediation_playbooks": report.remediation_playbooks,
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
try:
|
|
1017
|
+
report_path.write_text(json.dumps(report_data, indent=2))
|
|
1018
|
+
except Exception as e:
|
|
1019
|
+
_log.warning(f"Failed to save red team report: {e}")
|
|
1020
|
+
|
|
1021
|
+
async def verify_fixes(
|
|
1022
|
+
self,
|
|
1023
|
+
patch_ids: list[str],
|
|
1024
|
+
project_context: dict,
|
|
1025
|
+
) -> dict:
|
|
1026
|
+
"""Verify fixes by re-running relevant attack scenarios."""
|
|
1027
|
+
self.on_progress(f"🔍 Verifying {len(patch_ids)} fixes...")
|
|
1028
|
+
|
|
1029
|
+
# Get findings associated with patches
|
|
1030
|
+
from patchi.core.fix.patch import list_patches
|
|
1031
|
+
|
|
1032
|
+
patches = list_patches(self.root)
|
|
1033
|
+
|
|
1034
|
+
for patch_id in patch_ids:
|
|
1035
|
+
patch = next((p for p in patches if p.get("id") == patch_id), None)
|
|
1036
|
+
if patch:
|
|
1037
|
+
# Related findings wiring: deferred to post-assessment correlation
|
|
1038
|
+
_log.debug("patch %s found for re-assessment", patch_id)
|
|
1039
|
+
|
|
1040
|
+
# For now, run a focused assessment
|
|
1041
|
+
verification_report = await self.run_assessment(
|
|
1042
|
+
project_context,
|
|
1043
|
+
scope="targeted",
|
|
1044
|
+
intensity="active",
|
|
1045
|
+
max_scenarios=10,
|
|
1046
|
+
)
|
|
1047
|
+
|
|
1048
|
+
verified = 0
|
|
1049
|
+
for sr in verification_report.scenarios_run:
|
|
1050
|
+
if sr.status != "success":
|
|
1051
|
+
verified += 1
|
|
1052
|
+
|
|
1053
|
+
return {
|
|
1054
|
+
"verified_fixes": verified,
|
|
1055
|
+
"total_patches": len(patch_ids),
|
|
1056
|
+
"verification_report_id": verification_report.assessment_id,
|
|
1057
|
+
"remaining_vulnerabilities": verification_report.total_findings,
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
|
|
1061
|
+
# Convenience function for CLI
|
|
1062
|
+
async def run_red_team(
|
|
1063
|
+
root: Path,
|
|
1064
|
+
target_url: str = None,
|
|
1065
|
+
safe_mode: bool = True,
|
|
1066
|
+
scope: str = "full",
|
|
1067
|
+
intensity: str = "active",
|
|
1068
|
+
on_progress: Callable[[str], None] = None,
|
|
1069
|
+
) -> RedTeamReport:
|
|
1070
|
+
"""Run a red team assessment."""
|
|
1071
|
+
# Get project context from brain
|
|
1072
|
+
from patchi.core import memory as mem
|
|
1073
|
+
|
|
1074
|
+
brain = mem.get_brain(root)
|
|
1075
|
+
|
|
1076
|
+
project_context = {
|
|
1077
|
+
"project_purpose": brain.get("project_purpose", ""),
|
|
1078
|
+
"project_domain": brain.get("project_domain", ""),
|
|
1079
|
+
"frameworks": brain.get("frameworks", []),
|
|
1080
|
+
"active_security_domains": brain.get("active_security_domains", []),
|
|
1081
|
+
"component_type": brain.get("component_type", ""),
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
engine = RedTeamEngine(root, target_url, safe_mode, on_progress)
|
|
1085
|
+
return await engine.run_assessment(project_context, scope, intensity)
|
|
1086
|
+
|
|
1087
|
+
|
|
1088
|
+
# Agent wrapper for integration with Patchi's agent system
|
|
1089
|
+
@register
|
|
1090
|
+
class RedTeamEngineAgent(BaseAgent):
|
|
1091
|
+
"""Red Team Engine as a Patchi agent."""
|
|
1092
|
+
|
|
1093
|
+
name = "RedTeamEngineAgent"
|
|
1094
|
+
group = AgentGroup.SECURITY
|
|
1095
|
+
timeout = 600 # 10 minutes
|
|
1096
|
+
|
|
1097
|
+
def _run(self, inp: AgentInput, result: AgentResult) -> None:
|
|
1098
|
+
# Get project context from brain
|
|
1099
|
+
project_context = {
|
|
1100
|
+
"project_purpose": inp.brain.get("project_purpose", ""),
|
|
1101
|
+
"project_domain": inp.brain.get("project_domain", ""),
|
|
1102
|
+
"frameworks": inp.brain.get("frameworks", []),
|
|
1103
|
+
"active_security_domains": inp.brain.get("active_security_domains", []),
|
|
1104
|
+
"component_type": inp.brain.get("component_type", ""),
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
# Get config
|
|
1108
|
+
target_url = inp.extra.get("target_url")
|
|
1109
|
+
safe_mode = inp.extra.get("safe_mode", True)
|
|
1110
|
+
scope = inp.extra.get("scope", "full")
|
|
1111
|
+
intensity = inp.extra.get("intensity", "active")
|
|
1112
|
+
|
|
1113
|
+
# Run assessment
|
|
1114
|
+
async def run():
|
|
1115
|
+
engine = RedTeamEngine(
|
|
1116
|
+
inp.root,
|
|
1117
|
+
target_url=target_url,
|
|
1118
|
+
safe_mode=safe_mode,
|
|
1119
|
+
on_progress=lambda m: result.add_log(m),
|
|
1120
|
+
)
|
|
1121
|
+
return await engine.run_assessment(project_context, scope, intensity)
|
|
1122
|
+
|
|
1123
|
+
try:
|
|
1124
|
+
loop = asyncio.get_event_loop()
|
|
1125
|
+
except RuntimeError:
|
|
1126
|
+
loop = asyncio.new_event_loop()
|
|
1127
|
+
asyncio.set_event_loop(loop)
|
|
1128
|
+
|
|
1129
|
+
report = loop.run_until_complete(run())
|
|
1130
|
+
|
|
1131
|
+
# Add findings to result
|
|
1132
|
+
for sr in report.scenarios_run:
|
|
1133
|
+
for finding in sr.findings:
|
|
1134
|
+
result.add_finding(finding)
|
|
1135
|
+
|
|
1136
|
+
result.data["red_team_report"] = {
|
|
1137
|
+
"assessment_id": report.assessment_id,
|
|
1138
|
+
"total_findings": report.total_findings,
|
|
1139
|
+
"by_severity": report.by_severity,
|
|
1140
|
+
"scenarios_run": len(report.scenarios_run),
|
|
1141
|
+
"remediation_playbooks": report.remediation_playbooks,
|
|
1142
|
+
}
|