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,238 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DefectDojoReporter — optional push of Patchi defense results to DefectDojo.
|
|
3
|
+
|
|
4
|
+
DefectDojo is an open-source vulnerability management platform.
|
|
5
|
+
https://github.com/DefectDojo/django-DefectDojo
|
|
6
|
+
|
|
7
|
+
Configuration (opt-in, disabled by default):
|
|
8
|
+
{
|
|
9
|
+
"defectdojo": {
|
|
10
|
+
"enabled": false,
|
|
11
|
+
"url": "https://demo.defectdojo.org",
|
|
12
|
+
"api_key": "",
|
|
13
|
+
"auto_create_product": true,
|
|
14
|
+
"product_name": "auto", // "auto" = use project directory name
|
|
15
|
+
"engagement_prefix": "scan-"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
Usage:
|
|
20
|
+
reporter = DefectDojoReporter(config)
|
|
21
|
+
reporter.push_gated_report(gated_report, project_root)
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import time
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Any
|
|
29
|
+
|
|
30
|
+
from loguru import logger
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class DefectDojoReporter:
|
|
34
|
+
"""
|
|
35
|
+
Push Patchi defense pipeline results to DefectDojo.
|
|
36
|
+
|
|
37
|
+
All operations are optional and fail gracefully — never block scans.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, config: dict | None = None):
|
|
41
|
+
self.config = config or {}
|
|
42
|
+
dd_cfg = self.config.get("defectdojo", {})
|
|
43
|
+
self.enabled = dd_cfg.get("enabled", False)
|
|
44
|
+
self.base_url = dd_cfg.get("url", "").rstrip("/")
|
|
45
|
+
self.api_key = dd_cfg.get("api_key", "")
|
|
46
|
+
self.auto_create_product = dd_cfg.get("auto_create_product", True)
|
|
47
|
+
self.product_name = dd_cfg.get("product_name", "auto")
|
|
48
|
+
self.engagement_prefix = dd_cfg.get("engagement_prefix", "scan-")
|
|
49
|
+
|
|
50
|
+
if self.enabled and not self.base_url:
|
|
51
|
+
logger.warning("DefectDojo enabled but no URL configured — disabling")
|
|
52
|
+
self.enabled = False
|
|
53
|
+
if self.enabled and not self.api_key:
|
|
54
|
+
logger.warning("DefectDojo enabled but no API key configured — disabling")
|
|
55
|
+
self.enabled = False
|
|
56
|
+
|
|
57
|
+
def push_gated_report(self, gated_report: Any, root: Path) -> dict:
|
|
58
|
+
"""
|
|
59
|
+
Push a GatedReport to DefectDojo.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
gated_report: GatedReport from DetectionPipeline.process()
|
|
63
|
+
root: Project root Path (used for product name)
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
dict with status and details
|
|
67
|
+
"""
|
|
68
|
+
if not self.enabled:
|
|
69
|
+
return {"status": "skipped", "reason": "DefectDojo not enabled"}
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
import httpx # noqa: F401 — used to check availability
|
|
73
|
+
except ImportError:
|
|
74
|
+
logger.warning("httpx not installed — cannot push to DefectDojo")
|
|
75
|
+
return {"status": "skipped", "reason": "httpx not installed"}
|
|
76
|
+
|
|
77
|
+
product_name = Path(root).name if self.product_name == "auto" else self.product_name
|
|
78
|
+
|
|
79
|
+
# Build findings payload
|
|
80
|
+
findings = []
|
|
81
|
+
for gf in getattr(gated_report, "findings", []):
|
|
82
|
+
f = getattr(gf, "finding", gf)
|
|
83
|
+
findings.append(self._finding_to_dd(f, gf))
|
|
84
|
+
|
|
85
|
+
if not findings:
|
|
86
|
+
return {"status": "skipped", "reason": "No findings to push"}
|
|
87
|
+
|
|
88
|
+
# Ensure product and engagement exist
|
|
89
|
+
product_id = self._get_or_create_product(product_name)
|
|
90
|
+
if not product_id:
|
|
91
|
+
return {"status": "error", "reason": "Failed to get/create product"}
|
|
92
|
+
|
|
93
|
+
engagement_id = self._create_engagement(product_id, product_name)
|
|
94
|
+
if not engagement_id:
|
|
95
|
+
return {"status": "error", "reason": "Failed to create engagement"}
|
|
96
|
+
|
|
97
|
+
# Push import scan
|
|
98
|
+
return self._import_scan(engagement_id, findings, product_name)
|
|
99
|
+
|
|
100
|
+
# ── Finding conversion ────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
def _finding_to_dd(self, finding: Any, gated_finding: Any = None) -> dict:
|
|
103
|
+
"""Convert a Patchi Finding to DefectDojo finding format."""
|
|
104
|
+
severity_map = {
|
|
105
|
+
"critical": "Critical",
|
|
106
|
+
"high": "High",
|
|
107
|
+
"medium": "Medium",
|
|
108
|
+
"low": "Low",
|
|
109
|
+
"info": "Info",
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
cwe = 0
|
|
113
|
+
if hasattr(finding, "cwe") and finding.cwe:
|
|
114
|
+
try:
|
|
115
|
+
cwe = int(str(finding.cwe).replace("CWE-", ""))
|
|
116
|
+
except ValueError:
|
|
117
|
+
cwe = 0
|
|
118
|
+
|
|
119
|
+
can_defend = False
|
|
120
|
+
if gated_finding is not None:
|
|
121
|
+
can_defend = getattr(gated_finding, "can_defend", False)
|
|
122
|
+
|
|
123
|
+
severity = severity_map.get(
|
|
124
|
+
getattr(finding, "severity", "info").value
|
|
125
|
+
if hasattr(getattr(finding, "severity", ""), "value")
|
|
126
|
+
else str(getattr(finding, "severity", "info")).lower(),
|
|
127
|
+
"Info",
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
"title": getattr(finding, "message", "Untitled finding")[:500],
|
|
132
|
+
"severity": severity,
|
|
133
|
+
"file_path": getattr(finding, "file", ""),
|
|
134
|
+
"line": getattr(finding, "line", 0),
|
|
135
|
+
"cwe": cwe,
|
|
136
|
+
"description": getattr(finding, "detail", "")[:2000],
|
|
137
|
+
"remediation": getattr(finding, "suggestion", "")[:1000],
|
|
138
|
+
"active": True,
|
|
139
|
+
"verified": can_defend,
|
|
140
|
+
"false_positive": False,
|
|
141
|
+
"duplicate": False,
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
# ── DefectDojo API calls ──────────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
def _api_call(self, method: str, endpoint: str, **kwargs) -> dict | None:
|
|
147
|
+
"""Make an HTTP call to DefectDojo API."""
|
|
148
|
+
try:
|
|
149
|
+
import httpx
|
|
150
|
+
except ImportError:
|
|
151
|
+
return None
|
|
152
|
+
|
|
153
|
+
url = f"{self.base_url}/api/v2/{endpoint}"
|
|
154
|
+
headers = {
|
|
155
|
+
"Authorization": f"Token {self.api_key}",
|
|
156
|
+
"Content-Type": "application/json",
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
try:
|
|
160
|
+
if method == "GET":
|
|
161
|
+
r = httpx.get(url, headers=headers, params=kwargs.get("params"), timeout=30)
|
|
162
|
+
elif method == "POST":
|
|
163
|
+
r = httpx.post(url, headers=headers, json=kwargs.get("json", {}), timeout=30)
|
|
164
|
+
else:
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
if r.status_code in (200, 201):
|
|
168
|
+
return r.json()
|
|
169
|
+
logger.debug(f"DefectDojo API error: {r.status_code} {r.text[:200]}")
|
|
170
|
+
return None
|
|
171
|
+
except Exception as e:
|
|
172
|
+
logger.debug(f"DefectDojo request failed: {e}")
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
def _get_or_create_product(self, product_name: str) -> int | None:
|
|
176
|
+
"""Get existing product or create new one. Returns product_id."""
|
|
177
|
+
# Try to find existing product
|
|
178
|
+
result = self._api_call("GET", "products", params={"limit": 100})
|
|
179
|
+
if result:
|
|
180
|
+
for p in result.get("results", []):
|
|
181
|
+
if p.get("name") == product_name:
|
|
182
|
+
return p["id"]
|
|
183
|
+
|
|
184
|
+
# Create if auto-create enabled
|
|
185
|
+
if not self.auto_create_product:
|
|
186
|
+
logger.warning(f"Product '{product_name}' not found and auto_create is false")
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
result = self._api_call(
|
|
190
|
+
"POST",
|
|
191
|
+
"products",
|
|
192
|
+
json={
|
|
193
|
+
"name": product_name,
|
|
194
|
+
"description": f"Patchi scan findings for {product_name}",
|
|
195
|
+
"prod_type": 1, # default product type
|
|
196
|
+
},
|
|
197
|
+
)
|
|
198
|
+
if result:
|
|
199
|
+
return result["id"]
|
|
200
|
+
return None
|
|
201
|
+
|
|
202
|
+
def _create_engagement(self, product_id: int, product_name: str) -> int | None:
|
|
203
|
+
"""Create a new engagement for a scan. Returns engagement_id."""
|
|
204
|
+
engagement_name = f"{self.engagement_prefix}{int(time.time())}"
|
|
205
|
+
result = self._api_call(
|
|
206
|
+
"POST",
|
|
207
|
+
"engagements",
|
|
208
|
+
json={
|
|
209
|
+
"name": engagement_name,
|
|
210
|
+
"product": product_id,
|
|
211
|
+
"target_start": time.strftime("%Y-%m-%d"),
|
|
212
|
+
"target_end": time.strftime("%Y-%m-%d"),
|
|
213
|
+
"engagement_type": "CI/CD",
|
|
214
|
+
"status": "In Progress",
|
|
215
|
+
"description": f"Patchi scan of {product_name}",
|
|
216
|
+
},
|
|
217
|
+
)
|
|
218
|
+
if result:
|
|
219
|
+
return result["id"]
|
|
220
|
+
return None
|
|
221
|
+
|
|
222
|
+
def _import_scan(self, engagement_id: int, findings: list[dict], product_name: str) -> dict:
|
|
223
|
+
"""Push findings as an import-scan."""
|
|
224
|
+
result = self._api_call(
|
|
225
|
+
"POST",
|
|
226
|
+
"import-scan/",
|
|
227
|
+
json={
|
|
228
|
+
"engagement": engagement_id,
|
|
229
|
+
"scan_type": "Patchi Scan",
|
|
230
|
+
"product_name": product_name,
|
|
231
|
+
"findings": findings,
|
|
232
|
+
},
|
|
233
|
+
)
|
|
234
|
+
if result:
|
|
235
|
+
count = len(findings)
|
|
236
|
+
logger.info(f"Pushed {count} findings to DefectDojo (engagement {engagement_id})")
|
|
237
|
+
return {"status": "success", "finding_count": count, "engagement_id": engagement_id}
|
|
238
|
+
return {"status": "error", "reason": "Import scan API call failed"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Defenders package — adapter registry for defense actions.
|
|
2
|
+
|
|
3
|
+
Usage::
|
|
4
|
+
|
|
5
|
+
from patchi.core.security.defenders import ADAPTER_REGISTRY, get_adapter
|
|
6
|
+
from patchi.core.security.defenders.base import DefenseAction
|
|
7
|
+
|
|
8
|
+
adapter = get_adapter("fix_code", root=path, risk_gate=gate)
|
|
9
|
+
result = adapter.execute(action)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from .auth_middleware import AuthMiddlewareAdapter
|
|
17
|
+
from .base import BaseAdapter, DefendResult, DefenseAction
|
|
18
|
+
from .block_ip import BlockIpAdapter
|
|
19
|
+
from .block_ws_origin import BlockWsOriginAdapter
|
|
20
|
+
from .crypto_fix import CryptoFixAdapter
|
|
21
|
+
from .enforce_rate_limit import EnforceRateLimitAdapter
|
|
22
|
+
from .fix_code import FixCodeAdapter
|
|
23
|
+
from .invalidate_session import InvalidateSessionAdapter
|
|
24
|
+
from .patch_config import PatchConfigAdapter
|
|
25
|
+
from .rotate_secret import RotateSecretAdapter
|
|
26
|
+
from .suspend_account import SuspendAccountAdapter
|
|
27
|
+
from .update_dependency import UpdateDependencyAdapter
|
|
28
|
+
|
|
29
|
+
# Registry: action_type string → adapter class
|
|
30
|
+
ADAPTER_REGISTRY: dict[str, type[BaseAdapter]] = {
|
|
31
|
+
"fix_code": FixCodeAdapter,
|
|
32
|
+
"update_dependency": UpdateDependencyAdapter,
|
|
33
|
+
"block_ip": BlockIpAdapter,
|
|
34
|
+
"rotate_secret": RotateSecretAdapter,
|
|
35
|
+
"patch_config": PatchConfigAdapter,
|
|
36
|
+
"crypto_fix": CryptoFixAdapter,
|
|
37
|
+
"auth_middleware": AuthMiddlewareAdapter,
|
|
38
|
+
"invalidate_session": InvalidateSessionAdapter,
|
|
39
|
+
"enforce_rate_limit": EnforceRateLimitAdapter,
|
|
40
|
+
"suspend_account": SuspendAccountAdapter,
|
|
41
|
+
"block_ws_origin": BlockWsOriginAdapter,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
__all__ = [
|
|
45
|
+
"ADAPTER_REGISTRY",
|
|
46
|
+
"BaseAdapter",
|
|
47
|
+
"DefenseAction",
|
|
48
|
+
"DefendResult",
|
|
49
|
+
"get_adapter",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_adapter(action_type: str, root: Path, risk_gate) -> BaseAdapter:
|
|
54
|
+
"""Get an adapter instance for the given action type.
|
|
55
|
+
|
|
56
|
+
Falls back to a no-op adapter if the type is unknown.
|
|
57
|
+
"""
|
|
58
|
+
cls = ADAPTER_REGISTRY.get(action_type)
|
|
59
|
+
if cls is not None:
|
|
60
|
+
return cls(root=root, risk_gate=risk_gate)
|
|
61
|
+
|
|
62
|
+
# Fallback: return a stub that queues for human review
|
|
63
|
+
class _FallbackAdapter(BaseAdapter):
|
|
64
|
+
action_type = "" # overridden below
|
|
65
|
+
|
|
66
|
+
def execute(self, _action: DefenseAction) -> DefendResult:
|
|
67
|
+
return DefendResult(
|
|
68
|
+
action="queued",
|
|
69
|
+
reason=f"No auto-defense available for '{_action.type}' — queued for human review",
|
|
70
|
+
defense_action=_action,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
adapter = _FallbackAdapter(root=root, risk_gate=risk_gate)
|
|
74
|
+
adapter.action_type = action_type
|
|
75
|
+
return adapter
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Adapter: auth_middleware — add authentication middleware / decorator to an endpoint."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import uuid
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from patchi.core.fix.patch import FileChange, Patch
|
|
10
|
+
|
|
11
|
+
from .base import BaseAdapter, DefendResult, DefenseAction
|
|
12
|
+
|
|
13
|
+
_log = logging.getLogger("patchi.security.defenders.auth_middleware")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AuthMiddlewareAdapter(BaseAdapter):
|
|
17
|
+
"""Add authentication middleware / decorator to an endpoint."""
|
|
18
|
+
|
|
19
|
+
action_type = "auth_middleware"
|
|
20
|
+
|
|
21
|
+
def execute(self, action: DefenseAction) -> DefendResult:
|
|
22
|
+
if not action.target or not Path(action.target).exists():
|
|
23
|
+
return DefendResult(
|
|
24
|
+
action="skipped",
|
|
25
|
+
reason=f"File does not exist: {action.target}",
|
|
26
|
+
defense_action=action,
|
|
27
|
+
)
|
|
28
|
+
fpath = Path(action.target)
|
|
29
|
+
try:
|
|
30
|
+
content = fpath.read_text(encoding="utf-8", errors="ignore")
|
|
31
|
+
except OSError as e:
|
|
32
|
+
return DefendResult(action="blocked", reason=f"Read error: {e}", defense_action=action)
|
|
33
|
+
|
|
34
|
+
changes = []
|
|
35
|
+
if "flask" in content.lower() or "from flask" in content:
|
|
36
|
+
if (
|
|
37
|
+
"from flask_login import login_required" not in content
|
|
38
|
+
and "from flask_login import" not in content
|
|
39
|
+
):
|
|
40
|
+
lines = content.splitlines()
|
|
41
|
+
insert_at = 0
|
|
42
|
+
for i, line in enumerate(lines):
|
|
43
|
+
if line.startswith("#!") or line.startswith("# -*-"):
|
|
44
|
+
insert_at = i + 1
|
|
45
|
+
else:
|
|
46
|
+
break
|
|
47
|
+
lines.insert(insert_at, "from flask_login import login_required")
|
|
48
|
+
modified = "\n".join(lines)
|
|
49
|
+
changes.append(FileChange(path=str(fpath), original=content, proposed=modified))
|
|
50
|
+
elif "django" in content.lower() or "from django" in content:
|
|
51
|
+
if "from django.contrib.auth.decorators import login_required" not in content:
|
|
52
|
+
changes.append(
|
|
53
|
+
FileChange(
|
|
54
|
+
path=str(fpath),
|
|
55
|
+
original=content,
|
|
56
|
+
proposed="from django.contrib.auth.decorators import login_required\n"
|
|
57
|
+
+ content,
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
elif "fastapi" in content.lower() or "from fastapi" in content:
|
|
61
|
+
if "Depends" not in content and "get_current_user" not in content:
|
|
62
|
+
changes.append(
|
|
63
|
+
FileChange(
|
|
64
|
+
path=str(fpath),
|
|
65
|
+
original=content,
|
|
66
|
+
proposed="from fastapi import Depends\nfrom .auth import get_current_user\n"
|
|
67
|
+
+ content,
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
if not changes:
|
|
72
|
+
return DefendResult(
|
|
73
|
+
action="skipped",
|
|
74
|
+
reason="No recognizable framework found for auth middleware insertion",
|
|
75
|
+
defense_action=action,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
patch = Patch(
|
|
79
|
+
id=uuid.uuid4().hex[:12],
|
|
80
|
+
agent=action.finding.agent,
|
|
81
|
+
finding_id=action.finding.type,
|
|
82
|
+
patch_type="SECURITY",
|
|
83
|
+
changes=changes,
|
|
84
|
+
description="Add authentication middleware",
|
|
85
|
+
risk_score=30,
|
|
86
|
+
confidence=70,
|
|
87
|
+
blast_radius=2,
|
|
88
|
+
)
|
|
89
|
+
gate_result = self.risk_gate.evaluate(patch)
|
|
90
|
+
if gate_result.is_auto:
|
|
91
|
+
try:
|
|
92
|
+
patch.apply()
|
|
93
|
+
return DefendResult(
|
|
94
|
+
action="applied",
|
|
95
|
+
reason="Auth middleware template added",
|
|
96
|
+
defense_action=action,
|
|
97
|
+
patch=patch,
|
|
98
|
+
gate_result=gate_result,
|
|
99
|
+
)
|
|
100
|
+
except Exception as e:
|
|
101
|
+
return DefendResult(
|
|
102
|
+
action="blocked",
|
|
103
|
+
reason=f"Apply failed: {e}",
|
|
104
|
+
defense_action=action,
|
|
105
|
+
patch=patch,
|
|
106
|
+
gate_result=gate_result,
|
|
107
|
+
)
|
|
108
|
+
else:
|
|
109
|
+
return DefendResult(
|
|
110
|
+
action="queued" if not gate_result.is_blocked else "blocked",
|
|
111
|
+
reason=gate_result.reason,
|
|
112
|
+
defense_action=action,
|
|
113
|
+
patch=patch,
|
|
114
|
+
gate_result=gate_result,
|
|
115
|
+
)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Base adapter for defense actions.
|
|
2
|
+
|
|
3
|
+
Each concrete adapter handles one defense action type
|
|
4
|
+
(fix_code, block_ip, rotate_secret, etc.) and implements
|
|
5
|
+
``execute(action) -> DefendResult``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import abc
|
|
11
|
+
import time
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from patchi.core.agents.base import Finding
|
|
16
|
+
from patchi.core.fix.patch import Patch
|
|
17
|
+
from patchi.core.fix.risk_gate import GateResult
|
|
18
|
+
|
|
19
|
+
_log_name = "patchi.security.defenders"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class DefenseAction:
|
|
24
|
+
"""A concrete action to defend against a confirmed finding."""
|
|
25
|
+
|
|
26
|
+
type: str
|
|
27
|
+
target: str
|
|
28
|
+
finding: Finding
|
|
29
|
+
severity: str
|
|
30
|
+
fix_code: str = ""
|
|
31
|
+
|
|
32
|
+
def to_dict(self) -> dict:
|
|
33
|
+
return {
|
|
34
|
+
"type": self.type,
|
|
35
|
+
"target": self.target,
|
|
36
|
+
"severity": self.severity,
|
|
37
|
+
"finding_id": self.finding.type,
|
|
38
|
+
"finding_file": self.finding.file,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class DefendResult:
|
|
44
|
+
"""Result of executing a defense action."""
|
|
45
|
+
|
|
46
|
+
action: str # "applied" | "queued" | "blocked" | "skipped"
|
|
47
|
+
reason: str
|
|
48
|
+
defense_action: DefenseAction | None = None
|
|
49
|
+
patch: Patch | None = None
|
|
50
|
+
gate_result: GateResult | None = None
|
|
51
|
+
timestamp: float = field(default_factory=time.time)
|
|
52
|
+
|
|
53
|
+
def to_dict(self) -> dict:
|
|
54
|
+
d = {
|
|
55
|
+
"action": self.action,
|
|
56
|
+
"reason": self.reason,
|
|
57
|
+
"timestamp": self.timestamp,
|
|
58
|
+
}
|
|
59
|
+
if self.defense_action:
|
|
60
|
+
d["defense_action"] = self.defense_action.to_dict()
|
|
61
|
+
if self.patch:
|
|
62
|
+
d["patch_id"] = self.patch.id
|
|
63
|
+
return d
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class BaseAdapter(abc.ABC):
|
|
67
|
+
"""All defense adapters must subclass this."""
|
|
68
|
+
|
|
69
|
+
action_type: str # must be set by subclass
|
|
70
|
+
|
|
71
|
+
def __init__(self, root: Path, risk_gate):
|
|
72
|
+
self.root = root
|
|
73
|
+
self.risk_gate = risk_gate
|
|
74
|
+
|
|
75
|
+
@abc.abstractmethod
|
|
76
|
+
def execute(self, action: DefenseAction) -> DefendResult:
|
|
77
|
+
"""Execute the defense action and return a result."""
|
|
78
|
+
|
|
79
|
+
def _estimate_risk(self, action: DefenseAction) -> int:
|
|
80
|
+
"""Estimate risk score for a defense action (0-100)."""
|
|
81
|
+
sev_scores = {"critical": 80, "high": 60, "medium": 40, "low": 20, "info": 10}
|
|
82
|
+
base = sev_scores.get(action.severity, 30)
|
|
83
|
+
action_risk = {
|
|
84
|
+
"fix_code": 0,
|
|
85
|
+
"update_dependency": 10,
|
|
86
|
+
"block_ip": 5,
|
|
87
|
+
"rotate_secret": 15,
|
|
88
|
+
"patch_config": 20,
|
|
89
|
+
"crypto_fix": 5,
|
|
90
|
+
"auth_middleware": 15,
|
|
91
|
+
"invalidate_session": 25,
|
|
92
|
+
"enforce_rate_limit": 10,
|
|
93
|
+
"suspend_account": 40,
|
|
94
|
+
"block_ws_origin": 10,
|
|
95
|
+
"escalate": 0,
|
|
96
|
+
}
|
|
97
|
+
return min(100, base + action_risk.get(action.type, 0))
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Adapter: block_ip — block IP via OS firewall and persist to .patchi/blocked_ips.json."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import subprocess
|
|
8
|
+
|
|
9
|
+
from .base import BaseAdapter, DefendResult, DefenseAction
|
|
10
|
+
|
|
11
|
+
_log = logging.getLogger("patchi.security.defenders.block_ip")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BlockIpAdapter(BaseAdapter):
|
|
15
|
+
"""Block an IP address via OS firewall (hosted mode only)."""
|
|
16
|
+
|
|
17
|
+
action_type = "block_ip"
|
|
18
|
+
|
|
19
|
+
def execute(self, action: DefenseAction) -> DefendResult:
|
|
20
|
+
ip = action.target
|
|
21
|
+
if not ip:
|
|
22
|
+
return DefendResult(
|
|
23
|
+
action="skipped",
|
|
24
|
+
reason="No IP address to block",
|
|
25
|
+
defense_action=action,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
blocked_path = self.root / ".patchi" / "blocked_ips.json"
|
|
29
|
+
blocked: list = []
|
|
30
|
+
if blocked_path.exists():
|
|
31
|
+
try:
|
|
32
|
+
blocked = json.loads(blocked_path.read_text(encoding="utf-8"))
|
|
33
|
+
except Exception as e:
|
|
34
|
+
_log.warning("BlockIpAdapter read failed: %s", e)
|
|
35
|
+
blocked = []
|
|
36
|
+
if ip not in blocked:
|
|
37
|
+
blocked.append(ip)
|
|
38
|
+
blocked_path.parent.mkdir(parents=True, exist_ok=True)
|
|
39
|
+
blocked_path.write_text(json.dumps(blocked, indent=2), encoding="utf-8")
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
result = subprocess.run(
|
|
43
|
+
["iptables", "-A", "INPUT", "-s", ip, "-j", "DROP"],
|
|
44
|
+
capture_output=True,
|
|
45
|
+
text=True,
|
|
46
|
+
timeout=10,
|
|
47
|
+
)
|
|
48
|
+
if result.returncode == 0:
|
|
49
|
+
return DefendResult(
|
|
50
|
+
action="applied",
|
|
51
|
+
reason=f"Blocked IP {ip} via iptables (persisted to .patchi/blocked_ips.json)",
|
|
52
|
+
defense_action=action,
|
|
53
|
+
)
|
|
54
|
+
else:
|
|
55
|
+
return DefendResult(
|
|
56
|
+
action="applied",
|
|
57
|
+
reason=f"Blocked IP {ip} persisted to .patchi/blocked_ips.json (iptables unavailable: {result.stderr[:100]})",
|
|
58
|
+
defense_action=action,
|
|
59
|
+
)
|
|
60
|
+
except FileNotFoundError:
|
|
61
|
+
return DefendResult(
|
|
62
|
+
action="applied",
|
|
63
|
+
reason=f"Blocked IP {ip} persisted to .patchi/blocked_ips.json (iptables not available)",
|
|
64
|
+
defense_action=action,
|
|
65
|
+
)
|
|
66
|
+
except subprocess.TimeoutExpired:
|
|
67
|
+
return DefendResult(
|
|
68
|
+
action="applied",
|
|
69
|
+
reason=f"Blocked IP {ip} persisted to .patchi/blocked_ips.json (iptables timed out)",
|
|
70
|
+
defense_action=action,
|
|
71
|
+
)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Adapter: block_ws_origin — block a WebSocket origin via config patch."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
|
|
8
|
+
from .base import BaseAdapter, DefendResult, DefenseAction
|
|
9
|
+
|
|
10
|
+
_log = logging.getLogger("patchi.security.defenders.block_ws_origin")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BlockWsOriginAdapter(BaseAdapter):
|
|
14
|
+
"""Block a WebSocket origin via config patch."""
|
|
15
|
+
|
|
16
|
+
action_type = "block_ws_origin"
|
|
17
|
+
|
|
18
|
+
def execute(self, action: DefenseAction) -> DefendResult:
|
|
19
|
+
ws_path = self.root / ".patchi" / "blocked_ws_origins.json"
|
|
20
|
+
target = action.target or "unknown_origin"
|
|
21
|
+
blocked: list = []
|
|
22
|
+
if ws_path.exists():
|
|
23
|
+
try:
|
|
24
|
+
blocked = json.loads(ws_path.read_text(encoding="utf-8"))
|
|
25
|
+
except Exception as e:
|
|
26
|
+
_log.warning("BlockWsOriginAdapter read failed: %s", e)
|
|
27
|
+
blocked = []
|
|
28
|
+
if target not in blocked:
|
|
29
|
+
blocked.append(target)
|
|
30
|
+
ws_path.parent.mkdir(parents=True, exist_ok=True)
|
|
31
|
+
ws_path.write_text(json.dumps(blocked, indent=2), encoding="utf-8")
|
|
32
|
+
return DefendResult(
|
|
33
|
+
action="applied",
|
|
34
|
+
reason=f"Blocked WebSocket origin: {target} — written to .patchi/blocked_ws_origins.json",
|
|
35
|
+
defense_action=action,
|
|
36
|
+
)
|