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,118 @@
|
|
|
1
|
+
"""Adapter: crypto_fix — replace weak crypto with modern algorithms."""
|
|
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.crypto_fix")
|
|
14
|
+
|
|
15
|
+
# Known bad → good replacements (in order of specificity)
|
|
16
|
+
_CRYPTO_REPLACEMENTS: list[tuple[str, str]] = [
|
|
17
|
+
("hashlib.md5(", "hashlib.sha256("),
|
|
18
|
+
("hashlib.sha1(", "hashlib.sha256("),
|
|
19
|
+
(
|
|
20
|
+
"from cryptography.hazmat.primitives.ciphers.algorithms import ARC4",
|
|
21
|
+
"from cryptography.hazmat.primitives.ciphers.algorithms import ChaCha20",
|
|
22
|
+
),
|
|
23
|
+
("ARC4.new(", "ChaCha20.new("),
|
|
24
|
+
("DES.new(", "AES.new("),
|
|
25
|
+
("DES3.new(", "AES.new("),
|
|
26
|
+
("RSA.generate(1024", "RSA.generate(2048"),
|
|
27
|
+
("RSA.generate(512", "RSA.generate(2048"),
|
|
28
|
+
("DSA.generate(512", "RSA.generate(2048"),
|
|
29
|
+
("DSA.generate(1024", "RSA.generate(2048"),
|
|
30
|
+
("PBKDF2", "PBKDF2HMAC"),
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class CryptoFixAdapter(BaseAdapter):
|
|
35
|
+
"""Replace weak crypto with modern algorithms."""
|
|
36
|
+
|
|
37
|
+
action_type = "crypto_fix"
|
|
38
|
+
|
|
39
|
+
def execute(self, action: DefenseAction) -> DefendResult:
|
|
40
|
+
if not action.target or not Path(action.target).exists():
|
|
41
|
+
return DefendResult(
|
|
42
|
+
action="skipped",
|
|
43
|
+
reason=f"File does not exist: {action.target}",
|
|
44
|
+
defense_action=action,
|
|
45
|
+
)
|
|
46
|
+
fpath = Path(action.target)
|
|
47
|
+
try:
|
|
48
|
+
content = fpath.read_text(encoding="utf-8", errors="ignore")
|
|
49
|
+
except OSError as e:
|
|
50
|
+
return DefendResult(action="blocked", reason=f"Read error: {e}", defense_action=action)
|
|
51
|
+
|
|
52
|
+
changes = []
|
|
53
|
+
for old, new in _CRYPTO_REPLACEMENTS:
|
|
54
|
+
if old in content:
|
|
55
|
+
lines = content.splitlines()
|
|
56
|
+
for _i, line in enumerate(lines):
|
|
57
|
+
if old in line:
|
|
58
|
+
change = FileChange(
|
|
59
|
+
path=str(fpath),
|
|
60
|
+
original=line,
|
|
61
|
+
proposed=line.replace(old, new),
|
|
62
|
+
)
|
|
63
|
+
changes.append(change)
|
|
64
|
+
break
|
|
65
|
+
|
|
66
|
+
if not changes:
|
|
67
|
+
return DefendResult(
|
|
68
|
+
action="skipped",
|
|
69
|
+
reason="No weak crypto patterns found in file",
|
|
70
|
+
defense_action=action,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
patch = Patch(
|
|
74
|
+
id=uuid.uuid4().hex[:12],
|
|
75
|
+
agent=action.finding.agent,
|
|
76
|
+
finding_id=action.finding.type,
|
|
77
|
+
patch_type="SECURITY",
|
|
78
|
+
changes=changes,
|
|
79
|
+
description="Replace weak cryptography with modern algorithms",
|
|
80
|
+
risk_score=20,
|
|
81
|
+
confidence=90,
|
|
82
|
+
blast_radius=1,
|
|
83
|
+
)
|
|
84
|
+
gate_result = self.risk_gate.evaluate(patch)
|
|
85
|
+
if gate_result.is_auto:
|
|
86
|
+
try:
|
|
87
|
+
patch.apply()
|
|
88
|
+
return DefendResult(
|
|
89
|
+
action="applied",
|
|
90
|
+
reason="Crypto fix auto-applied",
|
|
91
|
+
defense_action=action,
|
|
92
|
+
patch=patch,
|
|
93
|
+
gate_result=gate_result,
|
|
94
|
+
)
|
|
95
|
+
except Exception as e:
|
|
96
|
+
return DefendResult(
|
|
97
|
+
action="blocked",
|
|
98
|
+
reason=f"Apply failed: {e}",
|
|
99
|
+
defense_action=action,
|
|
100
|
+
patch=patch,
|
|
101
|
+
gate_result=gate_result,
|
|
102
|
+
)
|
|
103
|
+
elif gate_result.is_blocked:
|
|
104
|
+
return DefendResult(
|
|
105
|
+
action="blocked",
|
|
106
|
+
reason=gate_result.reason,
|
|
107
|
+
defense_action=action,
|
|
108
|
+
patch=patch,
|
|
109
|
+
gate_result=gate_result,
|
|
110
|
+
)
|
|
111
|
+
else:
|
|
112
|
+
return DefendResult(
|
|
113
|
+
action="queued",
|
|
114
|
+
reason=gate_result.reason,
|
|
115
|
+
defense_action=action,
|
|
116
|
+
patch=patch,
|
|
117
|
+
gate_result=gate_result,
|
|
118
|
+
)
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Adapter: enforce_rate_limit — add rate limiting middleware."""
|
|
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.enforce_rate_limit")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class EnforceRateLimitAdapter(BaseAdapter):
|
|
17
|
+
"""Enforce rate limiting on an endpoint by adding middleware."""
|
|
18
|
+
|
|
19
|
+
action_type = "enforce_rate_limit"
|
|
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():
|
|
36
|
+
if "from flask_limiter import Limiter" not in content:
|
|
37
|
+
changes.append(
|
|
38
|
+
FileChange(
|
|
39
|
+
path=str(fpath),
|
|
40
|
+
original=content,
|
|
41
|
+
proposed=(
|
|
42
|
+
"from flask_limiter import Limiter\n"
|
|
43
|
+
"from flask_limiter.util import get_remote_address\n\n"
|
|
44
|
+
"limiter = Limiter(key_func=get_remote_address)\n" + content
|
|
45
|
+
),
|
|
46
|
+
)
|
|
47
|
+
)
|
|
48
|
+
elif "fastapi" in content.lower():
|
|
49
|
+
if (
|
|
50
|
+
"from slowapi import Limiter" not in content
|
|
51
|
+
and "from fastapi import Depends" in content
|
|
52
|
+
):
|
|
53
|
+
changes.append(
|
|
54
|
+
FileChange(
|
|
55
|
+
path=str(fpath),
|
|
56
|
+
original=content,
|
|
57
|
+
proposed=(
|
|
58
|
+
"from slowapi import Limiter, _rate_limit_exceeded_handler\n"
|
|
59
|
+
"from slowapi.util import get_remote_address\n" + content
|
|
60
|
+
),
|
|
61
|
+
)
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
if not changes:
|
|
65
|
+
return DefendResult(
|
|
66
|
+
action="queued",
|
|
67
|
+
reason="Add rate limit middleware manually — no framework template matched",
|
|
68
|
+
defense_action=action,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
patch = Patch(
|
|
72
|
+
id=uuid.uuid4().hex[:12],
|
|
73
|
+
agent=action.finding.agent,
|
|
74
|
+
finding_id=action.finding.type,
|
|
75
|
+
patch_type="CONFIG",
|
|
76
|
+
changes=changes,
|
|
77
|
+
description="Add rate limiting middleware",
|
|
78
|
+
risk_score=15,
|
|
79
|
+
confidence=65,
|
|
80
|
+
blast_radius=1,
|
|
81
|
+
)
|
|
82
|
+
gate_result = self.risk_gate.evaluate(patch)
|
|
83
|
+
if gate_result.is_auto:
|
|
84
|
+
try:
|
|
85
|
+
patch.apply()
|
|
86
|
+
return DefendResult(
|
|
87
|
+
action="applied",
|
|
88
|
+
reason="Rate limit middleware template added",
|
|
89
|
+
defense_action=action,
|
|
90
|
+
patch=patch,
|
|
91
|
+
gate_result=gate_result,
|
|
92
|
+
)
|
|
93
|
+
except Exception as e:
|
|
94
|
+
return DefendResult(
|
|
95
|
+
action="blocked",
|
|
96
|
+
reason=f"Apply failed: {e}",
|
|
97
|
+
defense_action=action,
|
|
98
|
+
patch=patch,
|
|
99
|
+
gate_result=gate_result,
|
|
100
|
+
)
|
|
101
|
+
else:
|
|
102
|
+
return DefendResult(
|
|
103
|
+
action="queued" if not gate_result.is_blocked else "blocked",
|
|
104
|
+
reason=gate_result.reason,
|
|
105
|
+
defense_action=action,
|
|
106
|
+
patch=patch,
|
|
107
|
+
gate_result=gate_result,
|
|
108
|
+
)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Adapter: fix_code — create Patch, pass through RiskGate, apply if allowed."""
|
|
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.fix_code")
|
|
14
|
+
|
|
15
|
+
# Human-readable labels for each action type (shared across adapters)
|
|
16
|
+
ACTION_LABELS: dict[str, str] = {
|
|
17
|
+
"fix_code": "Fix code vulnerability",
|
|
18
|
+
"update_dependency": "Update vulnerable dependency",
|
|
19
|
+
"block_ip": "Block malicious IP",
|
|
20
|
+
"rotate_secret": "Rotate exposed secret",
|
|
21
|
+
"patch_config": "Patch security configuration",
|
|
22
|
+
"crypto_fix": "Replace weak crypto with modern algorithms",
|
|
23
|
+
"auth_middleware": "Add authentication middleware",
|
|
24
|
+
"invalidate_session": "Invalidate active sessions",
|
|
25
|
+
"enforce_rate_limit": "Enforce rate limiting middleware",
|
|
26
|
+
"suspend_account": "Suspend compromised account",
|
|
27
|
+
"block_ws_origin": "Block WebSocket origin",
|
|
28
|
+
"escalate": "Escalate for human review",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class FixCodeAdapter(BaseAdapter):
|
|
33
|
+
"""Create a Patch and pass through RiskGate for mode-based decision."""
|
|
34
|
+
|
|
35
|
+
action_type = "fix_code"
|
|
36
|
+
|
|
37
|
+
def execute(self, action: DefenseAction) -> DefendResult:
|
|
38
|
+
if not action.target or not Path(action.target).exists():
|
|
39
|
+
return DefendResult(
|
|
40
|
+
action="skipped",
|
|
41
|
+
reason=f"File does not exist: {action.target}",
|
|
42
|
+
defense_action=action,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
change = FileChange(path=action.target, original="", proposed=action.fix_code)
|
|
46
|
+
patch = Patch(
|
|
47
|
+
id=uuid.uuid4().hex[:12],
|
|
48
|
+
agent=action.finding.agent,
|
|
49
|
+
finding_id=action.finding.type,
|
|
50
|
+
patch_type="SECURITY",
|
|
51
|
+
changes=[change],
|
|
52
|
+
description=ACTION_LABELS.get(action.type, "Security fix"),
|
|
53
|
+
risk_score=self._estimate_risk(action),
|
|
54
|
+
confidence=90,
|
|
55
|
+
blast_radius=0,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
gate_result = self.risk_gate.evaluate(patch)
|
|
59
|
+
|
|
60
|
+
if gate_result.is_auto:
|
|
61
|
+
try:
|
|
62
|
+
patch.apply()
|
|
63
|
+
return DefendResult(
|
|
64
|
+
action="applied",
|
|
65
|
+
reason=f"Auto-applied: {gate_result.reason}",
|
|
66
|
+
defense_action=action,
|
|
67
|
+
patch=patch,
|
|
68
|
+
gate_result=gate_result,
|
|
69
|
+
)
|
|
70
|
+
except Exception as e:
|
|
71
|
+
return DefendResult(
|
|
72
|
+
action="blocked",
|
|
73
|
+
reason=f"Apply failed: {e}",
|
|
74
|
+
defense_action=action,
|
|
75
|
+
patch=patch,
|
|
76
|
+
gate_result=gate_result,
|
|
77
|
+
)
|
|
78
|
+
elif gate_result.is_blocked:
|
|
79
|
+
return DefendResult(
|
|
80
|
+
action="blocked",
|
|
81
|
+
reason=gate_result.reason,
|
|
82
|
+
defense_action=action,
|
|
83
|
+
patch=patch,
|
|
84
|
+
gate_result=gate_result,
|
|
85
|
+
)
|
|
86
|
+
else:
|
|
87
|
+
return DefendResult(
|
|
88
|
+
action="queued",
|
|
89
|
+
reason=gate_result.reason,
|
|
90
|
+
defense_action=action,
|
|
91
|
+
patch=patch,
|
|
92
|
+
gate_result=gate_result,
|
|
93
|
+
)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Adapter: invalidate_session — queue session invalidation for compromised users."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import time
|
|
8
|
+
|
|
9
|
+
from .base import BaseAdapter, DefendResult, DefenseAction
|
|
10
|
+
|
|
11
|
+
_log = logging.getLogger("patchi.security.defenders.invalidate_session")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class InvalidateSessionAdapter(BaseAdapter):
|
|
15
|
+
"""Invalidate all active sessions for a user."""
|
|
16
|
+
|
|
17
|
+
action_type = "invalidate_session"
|
|
18
|
+
|
|
19
|
+
def execute(self, action: DefenseAction) -> DefendResult:
|
|
20
|
+
inval_path = self.root / ".patchi" / "invalidate_sessions.json"
|
|
21
|
+
target = action.target or "unknown_user"
|
|
22
|
+
pending: dict = {}
|
|
23
|
+
if inval_path.exists():
|
|
24
|
+
try:
|
|
25
|
+
pending = json.loads(inval_path.read_text(encoding="utf-8"))
|
|
26
|
+
except Exception as e:
|
|
27
|
+
_log.warning("InvalidateSessionAdapter read failed: %s", e)
|
|
28
|
+
pending = {}
|
|
29
|
+
pending[target] = {
|
|
30
|
+
"timestamp": time.time(),
|
|
31
|
+
"reason": action.finding.message or "Session compromise detected",
|
|
32
|
+
"finding_type": action.finding.type,
|
|
33
|
+
}
|
|
34
|
+
inval_path.parent.mkdir(parents=True, exist_ok=True)
|
|
35
|
+
inval_path.write_text(json.dumps(pending, indent=2), encoding="utf-8")
|
|
36
|
+
return DefendResult(
|
|
37
|
+
action="applied",
|
|
38
|
+
reason=f"Queued session invalidation for {target} — written to .patchi/invalidate_sessions.json",
|
|
39
|
+
defense_action=action,
|
|
40
|
+
)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Adapter: patch_config — edit config file with known-good values."""
|
|
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
|
+
from .fix_code import ACTION_LABELS
|
|
13
|
+
|
|
14
|
+
_log = logging.getLogger("patchi.security.defenders.patch_config")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PatchConfigAdapter(BaseAdapter):
|
|
18
|
+
"""Patch a configuration file with known-good values."""
|
|
19
|
+
|
|
20
|
+
action_type = "patch_config"
|
|
21
|
+
|
|
22
|
+
def execute(self, action: DefenseAction) -> DefendResult:
|
|
23
|
+
if not action.target or not Path(action.target).exists():
|
|
24
|
+
return DefendResult(
|
|
25
|
+
action="skipped",
|
|
26
|
+
reason=f"Config file does not exist: {action.target}",
|
|
27
|
+
defense_action=action,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
change = FileChange(path=action.target, original="", proposed=action.fix_code)
|
|
31
|
+
patch = Patch(
|
|
32
|
+
id=uuid.uuid4().hex[:12],
|
|
33
|
+
agent=action.finding.agent,
|
|
34
|
+
finding_id=action.finding.type,
|
|
35
|
+
patch_type="CONFIG",
|
|
36
|
+
changes=[change],
|
|
37
|
+
description=ACTION_LABELS.get(action.type, "Config patch"),
|
|
38
|
+
risk_score=self._estimate_risk(action),
|
|
39
|
+
confidence=85,
|
|
40
|
+
blast_radius=0,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
gate_result = self.risk_gate.evaluate(patch)
|
|
44
|
+
|
|
45
|
+
if gate_result.is_auto:
|
|
46
|
+
try:
|
|
47
|
+
patch.apply()
|
|
48
|
+
return DefendResult(
|
|
49
|
+
action="applied",
|
|
50
|
+
reason=f"Config auto-patched: {gate_result.reason}",
|
|
51
|
+
defense_action=action,
|
|
52
|
+
patch=patch,
|
|
53
|
+
gate_result=gate_result,
|
|
54
|
+
)
|
|
55
|
+
except Exception as e:
|
|
56
|
+
return DefendResult(
|
|
57
|
+
action="blocked",
|
|
58
|
+
reason=f"Config patch failed: {e}",
|
|
59
|
+
defense_action=action,
|
|
60
|
+
patch=patch,
|
|
61
|
+
gate_result=gate_result,
|
|
62
|
+
)
|
|
63
|
+
else:
|
|
64
|
+
return DefendResult(
|
|
65
|
+
action="queued" if not gate_result.is_blocked else "blocked",
|
|
66
|
+
reason=gate_result.reason,
|
|
67
|
+
defense_action=action,
|
|
68
|
+
patch=patch,
|
|
69
|
+
gate_result=gate_result,
|
|
70
|
+
)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Adapter: rotate_secret — generate new secret and update .env."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import re
|
|
7
|
+
import secrets as sec
|
|
8
|
+
|
|
9
|
+
from .base import BaseAdapter, DefendResult, DefenseAction
|
|
10
|
+
|
|
11
|
+
_log = logging.getLogger("patchi.security.defenders.rotate_secret")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class RotateSecretAdapter(BaseAdapter):
|
|
15
|
+
"""Generate a new secret and update the code."""
|
|
16
|
+
|
|
17
|
+
action_type = "rotate_secret"
|
|
18
|
+
|
|
19
|
+
def execute(self, action: DefenseAction) -> DefendResult:
|
|
20
|
+
new_secret = sec.token_urlsafe(32)
|
|
21
|
+
env_path = self.root / ".env"
|
|
22
|
+
if env_path.exists():
|
|
23
|
+
try:
|
|
24
|
+
content = env_path.read_text(encoding="utf-8")
|
|
25
|
+
var_name = (
|
|
26
|
+
action.target if action.target and "=" not in action.target else "SECRET_KEY"
|
|
27
|
+
)
|
|
28
|
+
if var_name in content:
|
|
29
|
+
content = re.sub(
|
|
30
|
+
rf"^{re.escape(var_name)}=.*$",
|
|
31
|
+
f"{var_name}={new_secret}",
|
|
32
|
+
content,
|
|
33
|
+
count=1,
|
|
34
|
+
flags=re.MULTILINE,
|
|
35
|
+
)
|
|
36
|
+
else:
|
|
37
|
+
content = f"{content.rstrip()}\n{var_name}={new_secret}\n"
|
|
38
|
+
env_path.write_text(content, encoding="utf-8")
|
|
39
|
+
return DefendResult(
|
|
40
|
+
action="applied",
|
|
41
|
+
reason=f"Rotated {var_name} in .env — new 256-bit secret generated",
|
|
42
|
+
defense_action=action,
|
|
43
|
+
)
|
|
44
|
+
except Exception as e:
|
|
45
|
+
return DefendResult(
|
|
46
|
+
action="blocked",
|
|
47
|
+
reason=f"Failed to rotate secret: {e}",
|
|
48
|
+
defense_action=action,
|
|
49
|
+
)
|
|
50
|
+
return DefendResult(
|
|
51
|
+
action="queued",
|
|
52
|
+
reason="No .env file found — create one and add the rotated secret manually",
|
|
53
|
+
defense_action=action,
|
|
54
|
+
)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Adapter: suspend_account — suspend a compromised account (hosted mode only)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import time
|
|
8
|
+
|
|
9
|
+
from .base import BaseAdapter, DefendResult, DefenseAction
|
|
10
|
+
|
|
11
|
+
_log = logging.getLogger("patchi.security.defenders.suspend_account")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SuspendAccountAdapter(BaseAdapter):
|
|
15
|
+
"""Suspend a compromised account (hosted mode only)."""
|
|
16
|
+
|
|
17
|
+
action_type = "suspend_account"
|
|
18
|
+
|
|
19
|
+
def execute(self, action: DefenseAction) -> DefendResult:
|
|
20
|
+
susp_path = self.root / ".patchi" / "suspend_accounts.json"
|
|
21
|
+
target = action.target or "unknown_account"
|
|
22
|
+
pending: dict = {}
|
|
23
|
+
if susp_path.exists():
|
|
24
|
+
try:
|
|
25
|
+
pending = json.loads(susp_path.read_text(encoding="utf-8"))
|
|
26
|
+
except Exception as e:
|
|
27
|
+
_log.warning("SuspendAccountAdapter read failed: %s", e)
|
|
28
|
+
pending = {}
|
|
29
|
+
pending[target] = {
|
|
30
|
+
"timestamp": time.time(),
|
|
31
|
+
"reason": action.finding.message or "Account compromise detected",
|
|
32
|
+
"finding_type": action.finding.type,
|
|
33
|
+
"severity": action.severity,
|
|
34
|
+
}
|
|
35
|
+
susp_path.parent.mkdir(parents=True, exist_ok=True)
|
|
36
|
+
susp_path.write_text(json.dumps(pending, indent=2), encoding="utf-8")
|
|
37
|
+
return DefendResult(
|
|
38
|
+
action="applied",
|
|
39
|
+
reason=f"Queued account suspension for {target} — written to .patchi/suspend_accounts.json",
|
|
40
|
+
defense_action=action,
|
|
41
|
+
)
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""Adapter: update_dependency — detect package manager, update vulnerable package."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import subprocess
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from .base import BaseAdapter, DefendResult, DefenseAction
|
|
10
|
+
|
|
11
|
+
_log = logging.getLogger("patchi.security.defenders.update_dep")
|
|
12
|
+
|
|
13
|
+
# Package managers and their update commands
|
|
14
|
+
_PACKAGE_MANAGERS: list[dict] = [
|
|
15
|
+
{
|
|
16
|
+
"name": "pip",
|
|
17
|
+
"check": ["pip", "--version"],
|
|
18
|
+
"update": ["pip", "install", "--upgrade"],
|
|
19
|
+
"files": ["requirements.txt", "Pipfile", "pyproject.toml"],
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"name": "npm",
|
|
23
|
+
"check": ["npm", "--version"],
|
|
24
|
+
"update": ["npm", "update"],
|
|
25
|
+
"files": ["package.json"],
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"name": "yarn",
|
|
29
|
+
"check": ["yarn", "--version"],
|
|
30
|
+
"update": ["yarn", "upgrade"],
|
|
31
|
+
"files": ["package.json"],
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"name": "cargo",
|
|
35
|
+
"check": ["cargo", "--version"],
|
|
36
|
+
"update": ["cargo", "update"],
|
|
37
|
+
"files": ["Cargo.toml"],
|
|
38
|
+
},
|
|
39
|
+
{"name": "go", "check": ["go", "version"], "update": ["go", "get", "-u"], "files": ["go.mod"]},
|
|
40
|
+
{
|
|
41
|
+
"name": "gem",
|
|
42
|
+
"check": ["gem", "--version"],
|
|
43
|
+
"update": ["gem", "update"],
|
|
44
|
+
"files": ["Gemfile"],
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"name": "nuget",
|
|
48
|
+
"check": ["dotnet", "--version"],
|
|
49
|
+
"update": ["dotnet", "package", "update"],
|
|
50
|
+
"files": ["*.csproj", "packages.config"],
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"name": "composer",
|
|
54
|
+
"check": ["composer", "--version"],
|
|
55
|
+
"update": ["composer", "update"],
|
|
56
|
+
"files": ["composer.json"],
|
|
57
|
+
},
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def detect_package_manager(root: Path) -> str | None:
|
|
62
|
+
"""Detect which package manager is available in the project."""
|
|
63
|
+
for pm in _PACKAGE_MANAGERS:
|
|
64
|
+
try:
|
|
65
|
+
r = subprocess.run(pm["check"], capture_output=True, text=True, timeout=5)
|
|
66
|
+
if r.returncode == 0:
|
|
67
|
+
for f in pm["files"]:
|
|
68
|
+
matches = list(root.glob(f))
|
|
69
|
+
if matches:
|
|
70
|
+
return pm["name"]
|
|
71
|
+
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
72
|
+
continue
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def detect_manifest(root: Path) -> Path | None:
|
|
77
|
+
"""Detect project manifest file."""
|
|
78
|
+
for pattern in [
|
|
79
|
+
"pyproject.toml",
|
|
80
|
+
"package.json",
|
|
81
|
+
"Cargo.toml",
|
|
82
|
+
"go.mod",
|
|
83
|
+
"Gemfile",
|
|
84
|
+
"build.gradle",
|
|
85
|
+
"pom.xml",
|
|
86
|
+
]:
|
|
87
|
+
matches = list(root.glob(pattern))
|
|
88
|
+
if matches:
|
|
89
|
+
return matches[0]
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class UpdateDependencyAdapter(BaseAdapter):
|
|
94
|
+
"""Update a vulnerable dependency via detected package manager."""
|
|
95
|
+
|
|
96
|
+
action_type = "update_dependency"
|
|
97
|
+
|
|
98
|
+
def execute(self, action: DefenseAction) -> DefendResult:
|
|
99
|
+
pkg = Path(action.target).name if action.target else ""
|
|
100
|
+
if not pkg:
|
|
101
|
+
return DefendResult(
|
|
102
|
+
action="skipped",
|
|
103
|
+
reason="No package specified for update",
|
|
104
|
+
defense_action=action,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
pm_name = detect_package_manager(self.root)
|
|
108
|
+
if not pm_name:
|
|
109
|
+
return DefendResult(
|
|
110
|
+
action="queued",
|
|
111
|
+
reason="No supported package manager detected (tried pip, npm, yarn, cargo, go, gem, nuget, composer)",
|
|
112
|
+
defense_action=action,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
pm_config = next((p for p in _PACKAGE_MANAGERS if p["name"] == pm_name), None)
|
|
116
|
+
if not pm_config:
|
|
117
|
+
return DefendResult(
|
|
118
|
+
action="blocked",
|
|
119
|
+
reason=f"Package manager '{pm_name}' config not found",
|
|
120
|
+
defense_action=action,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
cmd = pm_config["update"] + [pkg]
|
|
125
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
|
126
|
+
if result.returncode == 0:
|
|
127
|
+
manifest = detect_manifest(self.root)
|
|
128
|
+
if manifest and manifest.exists():
|
|
129
|
+
try:
|
|
130
|
+
text = manifest.read_text(encoding="utf-8")
|
|
131
|
+
text = text.replace(
|
|
132
|
+
f"{pkg}==",
|
|
133
|
+
f"{pkg}== # updated by Patchi",
|
|
134
|
+
)
|
|
135
|
+
manifest.write_text(text, encoding="utf-8")
|
|
136
|
+
except Exception as e:
|
|
137
|
+
_log.warning("UpdateDependencyAdapter failed to annotate manifest: %s", e)
|
|
138
|
+
return DefendResult(
|
|
139
|
+
action="applied",
|
|
140
|
+
reason=f"Updated {pkg} via {pm_name} successfully",
|
|
141
|
+
defense_action=action,
|
|
142
|
+
)
|
|
143
|
+
else:
|
|
144
|
+
return DefendResult(
|
|
145
|
+
action="blocked",
|
|
146
|
+
reason=f"{pm_name} update failed: {result.stderr[:200]}",
|
|
147
|
+
defense_action=action,
|
|
148
|
+
)
|
|
149
|
+
except FileNotFoundError:
|
|
150
|
+
return DefendResult(
|
|
151
|
+
action="skipped",
|
|
152
|
+
reason=f"{pm_name} not found — cannot update dependencies",
|
|
153
|
+
defense_action=action,
|
|
154
|
+
)
|
|
155
|
+
except subprocess.TimeoutExpired:
|
|
156
|
+
return DefendResult(
|
|
157
|
+
action="blocked",
|
|
158
|
+
reason=f"{pm_name} update timed out",
|
|
159
|
+
defense_action=action,
|
|
160
|
+
)
|