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,660 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Auto-Fixer — Generates and verifies fixes for security findings.
|
|
3
|
+
|
|
4
|
+
Integrates with:
|
|
5
|
+
- Red Team Engine (attack scenarios -> findings -> fixes)
|
|
6
|
+
- Security Orchestrator (correlated findings -> fixes)
|
|
7
|
+
- Domain Loader (fix playbooks)
|
|
8
|
+
- Patch System (apply, verify, rollback)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import asyncio
|
|
14
|
+
import logging
|
|
15
|
+
import uuid
|
|
16
|
+
from collections.abc import Callable
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from datetime import UTC, datetime
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
from patchi.core.agents.base import Finding
|
|
22
|
+
from patchi.core.fix.applier import PatchApplier
|
|
23
|
+
from patchi.core.fix.base import generate_fix as base_generate_fix
|
|
24
|
+
from patchi.core.fix.patch import Patch, PatchState, save_patch_state
|
|
25
|
+
from patchi.core.security.domain_loader import DomainLoader, FixPlaybook
|
|
26
|
+
from patchi.core.security.orchestrator import CorrelatedFinding
|
|
27
|
+
|
|
28
|
+
_log = logging.getLogger("patchi.security.auto_fixer")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class FixAttempt:
|
|
33
|
+
"""Record of a fix generation attempt."""
|
|
34
|
+
|
|
35
|
+
finding_id: str
|
|
36
|
+
finding_type: str
|
|
37
|
+
strategy: str # "deterministic", "llm-template", "playbook", "manual"
|
|
38
|
+
playbook_id: str | None = None
|
|
39
|
+
patch_id: str | None = None
|
|
40
|
+
success: bool = False
|
|
41
|
+
error: str = ""
|
|
42
|
+
generated_at: str = field(default_factory=lambda: datetime.now(UTC).isoformat())
|
|
43
|
+
verified: bool = False
|
|
44
|
+
verification_at: str = ""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class FixVerificationResult:
|
|
49
|
+
"""Result of verifying a fix."""
|
|
50
|
+
|
|
51
|
+
patch_id: str
|
|
52
|
+
finding_id: str
|
|
53
|
+
verified: bool
|
|
54
|
+
method: str # "re-run-attack", "static-analysis", "test-suite"
|
|
55
|
+
evidence: str = ""
|
|
56
|
+
residual_risk: str = ""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class AutoFixer:
|
|
60
|
+
"""
|
|
61
|
+
Automatically generates and verifies fixes for security findings.
|
|
62
|
+
|
|
63
|
+
Flow:
|
|
64
|
+
1. Receive finding (from RedTeamEngine, Orchestrator, or direct)
|
|
65
|
+
2. Match finding to fix playbook (via DomainLoader)
|
|
66
|
+
3. Generate fix using appropriate strategy
|
|
67
|
+
4. Create patch
|
|
68
|
+
5. Optionally apply patch
|
|
69
|
+
6. Verify fix (re-run attack, static analysis, tests)
|
|
70
|
+
7. Record outcome for learning
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def __init__(
|
|
74
|
+
self,
|
|
75
|
+
root: Path,
|
|
76
|
+
config: dict,
|
|
77
|
+
on_progress: Callable[[str], None] = None,
|
|
78
|
+
):
|
|
79
|
+
self.root = root
|
|
80
|
+
self.config = config
|
|
81
|
+
self.on_progress = on_progress or (lambda _: None)
|
|
82
|
+
self.domain_loader = DomainLoader(root)
|
|
83
|
+
self.fix_history: list[FixAttempt] = []
|
|
84
|
+
self._load_history()
|
|
85
|
+
|
|
86
|
+
def _load_history(self):
|
|
87
|
+
"""Load fix history from memory."""
|
|
88
|
+
from patchi.core import memory as mem
|
|
89
|
+
|
|
90
|
+
history = mem.read(mem.MemoryCategory.ISSUES, self.root)
|
|
91
|
+
for item in history:
|
|
92
|
+
if item.get("type") == "fix_attempt":
|
|
93
|
+
self.fix_history.append(FixAttempt(**item))
|
|
94
|
+
|
|
95
|
+
def _save_history(self):
|
|
96
|
+
"""Save fix history to memory."""
|
|
97
|
+
from patchi.core import memory as mem
|
|
98
|
+
|
|
99
|
+
# Keep last 500 attempts
|
|
100
|
+
data = [{"type": "fix_attempt", **fa.__dict__} for fa in self.fix_history[-500:]]
|
|
101
|
+
mem.write(mem.MemoryCategory.ISSUES, data, self.root)
|
|
102
|
+
|
|
103
|
+
async def fix_finding(
|
|
104
|
+
self,
|
|
105
|
+
finding: Finding | CorrelatedFinding,
|
|
106
|
+
strategy: str = "auto",
|
|
107
|
+
apply: bool = False,
|
|
108
|
+
verify: bool = True,
|
|
109
|
+
) -> dict:
|
|
110
|
+
"""
|
|
111
|
+
Generate and optionally apply/verify a fix for a finding.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
Dict with fix details, patch_id, verification result
|
|
115
|
+
"""
|
|
116
|
+
finding_id = getattr(finding, "id", None) or getattr(finding, "finding", {}).get(
|
|
117
|
+
"id", str(uuid.uuid4())[:8]
|
|
118
|
+
)
|
|
119
|
+
finding_type = finding.type if hasattr(finding, "type") else finding.finding.type
|
|
120
|
+
severity = (
|
|
121
|
+
finding.severity.value
|
|
122
|
+
if hasattr(finding, "severity")
|
|
123
|
+
else finding.finding.severity.value
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
self.on_progress(f"🔧 Generating fix for {finding_type} ({severity})")
|
|
127
|
+
|
|
128
|
+
# Create fix attempt record
|
|
129
|
+
attempt = FixAttempt(
|
|
130
|
+
finding_id=finding_id,
|
|
131
|
+
finding_type=finding_type,
|
|
132
|
+
strategy=strategy,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
# 1. Match to playbook
|
|
137
|
+
playbook = self._match_playbook(finding)
|
|
138
|
+
if playbook:
|
|
139
|
+
attempt.playbook_id = playbook.control_id
|
|
140
|
+
attempt.strategy = playbook.fix_strategy
|
|
141
|
+
|
|
142
|
+
# 2. Generate fix
|
|
143
|
+
patch = await self._generate_fix(finding, playbook, attempt.strategy)
|
|
144
|
+
|
|
145
|
+
if not patch:
|
|
146
|
+
attempt.success = False
|
|
147
|
+
attempt.error = "Fix generation failed"
|
|
148
|
+
self.fix_history.append(attempt)
|
|
149
|
+
self._save_history()
|
|
150
|
+
return {"success": False, "error": "Fix generation failed"}
|
|
151
|
+
|
|
152
|
+
attempt.patch_id = patch.id
|
|
153
|
+
attempt.success = True
|
|
154
|
+
|
|
155
|
+
# 3. Optionally apply
|
|
156
|
+
applied = False
|
|
157
|
+
if apply:
|
|
158
|
+
applied = await self._apply_patch(patch)
|
|
159
|
+
|
|
160
|
+
# 4. Optionally verify
|
|
161
|
+
verification = None
|
|
162
|
+
if verify:
|
|
163
|
+
verification = await self._verify_fix(patch, finding, playbook)
|
|
164
|
+
attempt.verified = verification.verified
|
|
165
|
+
attempt.verification_at = (
|
|
166
|
+
verification.verified_at
|
|
167
|
+
if hasattr(verification, "verified_at")
|
|
168
|
+
else datetime.now(UTC).isoformat()
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
self.fix_history.append(attempt)
|
|
172
|
+
self._save_history()
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
"success": True,
|
|
176
|
+
"patch_id": patch.id,
|
|
177
|
+
"strategy": attempt.strategy,
|
|
178
|
+
"playbook": playbook.control_id if playbook else None,
|
|
179
|
+
"applied": applied,
|
|
180
|
+
"verification": verification.__dict__ if verification else None,
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
except Exception as e:
|
|
184
|
+
_log.error(f"Fix generation failed: {e}", exc_info=True)
|
|
185
|
+
attempt.success = False
|
|
186
|
+
attempt.error = str(e)
|
|
187
|
+
self.fix_history.append(attempt)
|
|
188
|
+
self._save_history()
|
|
189
|
+
return {"success": False, "error": str(e)}
|
|
190
|
+
|
|
191
|
+
def _match_playbook(self, finding: Finding | CorrelatedFinding) -> FixPlaybook | None:
|
|
192
|
+
"""Match finding to a fix playbook via domain controls."""
|
|
193
|
+
finding_type = finding.type if hasattr(finding, "type") else finding.finding.type
|
|
194
|
+
file_path = finding.file if hasattr(finding, "file") else finding.finding.file
|
|
195
|
+
message = finding.message if hasattr(finding, "message") else finding.finding.message
|
|
196
|
+
|
|
197
|
+
# Use domain loader to match finding to controls
|
|
198
|
+
controls = self.domain_loader.match_finding_to_controls(finding_type, file_path, message)
|
|
199
|
+
|
|
200
|
+
if not controls:
|
|
201
|
+
return None
|
|
202
|
+
|
|
203
|
+
# Get playbook for first matching control
|
|
204
|
+
for ctrl in controls:
|
|
205
|
+
playbook = self.domain_loader.get_playbook(ctrl.control_id)
|
|
206
|
+
if playbook:
|
|
207
|
+
return playbook
|
|
208
|
+
|
|
209
|
+
return None
|
|
210
|
+
|
|
211
|
+
async def _generate_fix(
|
|
212
|
+
self,
|
|
213
|
+
finding: Finding | CorrelatedFinding,
|
|
214
|
+
playbook: FixPlaybook | None,
|
|
215
|
+
strategy: str,
|
|
216
|
+
) -> Patch | None:
|
|
217
|
+
"""Generate a fix patch."""
|
|
218
|
+
# Use base generate_fix with playbook context
|
|
219
|
+
finding_dict = (
|
|
220
|
+
finding.to_dict() if hasattr(finding, "to_dict") else finding.finding.to_dict()
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
# Enhance with playbook info
|
|
224
|
+
if playbook:
|
|
225
|
+
finding_dict["playbook"] = {
|
|
226
|
+
"control_id": playbook.control_id,
|
|
227
|
+
"fix_strategy": playbook.fix_strategy,
|
|
228
|
+
"llm_template": playbook.llm_fix_template,
|
|
229
|
+
"verification_checks": playbook.verification_checks,
|
|
230
|
+
"blast_radius_notes": playbook.blast_radius_notes,
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
# Call base fix generator
|
|
234
|
+
try:
|
|
235
|
+
loop = asyncio.get_event_loop()
|
|
236
|
+
except RuntimeError:
|
|
237
|
+
loop = asyncio.new_event_loop()
|
|
238
|
+
asyncio.set_event_loop(loop)
|
|
239
|
+
|
|
240
|
+
patch = await loop.run_in_executor(
|
|
241
|
+
None, lambda: base_generate_fix(self.root, finding_dict, self.config)
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
return patch
|
|
245
|
+
|
|
246
|
+
async def _apply_patch(self, patch: Patch) -> bool:
|
|
247
|
+
"""Apply a patch to the codebase."""
|
|
248
|
+
self.on_progress(f"📥 Applying patch {patch.id}")
|
|
249
|
+
|
|
250
|
+
applier = PatchApplier(self.root)
|
|
251
|
+
try:
|
|
252
|
+
loop = asyncio.get_event_loop()
|
|
253
|
+
except RuntimeError:
|
|
254
|
+
loop = asyncio.new_event_loop()
|
|
255
|
+
asyncio.set_event_loop(loop)
|
|
256
|
+
|
|
257
|
+
result = await loop.run_in_executor(None, applier.apply, patch)
|
|
258
|
+
|
|
259
|
+
if result.success:
|
|
260
|
+
save_patch_state(self.root, patch.id, PatchState.APPLIED)
|
|
261
|
+
self.on_progress(f"✅ Patch {patch.id} applied successfully")
|
|
262
|
+
return True
|
|
263
|
+
else:
|
|
264
|
+
self.on_progress(f"❌ Patch {patch.id} failed: {result.message}")
|
|
265
|
+
return False
|
|
266
|
+
|
|
267
|
+
async def _verify_fix(
|
|
268
|
+
self,
|
|
269
|
+
patch: Patch,
|
|
270
|
+
finding: Finding | CorrelatedFinding,
|
|
271
|
+
playbook: FixPlaybook | None,
|
|
272
|
+
) -> FixVerificationResult:
|
|
273
|
+
"""Verify that a fix resolves the finding."""
|
|
274
|
+
self.on_progress(f"✅ Verifying fix for patch {patch.id}")
|
|
275
|
+
|
|
276
|
+
finding_id = getattr(finding, "id", None) or getattr(finding, "finding", {}).get("id", "")
|
|
277
|
+
|
|
278
|
+
# Method 1: Re-run attack (if from Red Team)
|
|
279
|
+
if playbook and "redteam" in str(playbook.control_id).lower():
|
|
280
|
+
verified = await self._verify_by_attack_replay(patch, finding)
|
|
281
|
+
method = "re-run-attack"
|
|
282
|
+
# Method 2: Static analysis
|
|
283
|
+
elif playbook and playbook.deterministic_tool:
|
|
284
|
+
verified = await self._verify_by_static_analysis(patch, finding, playbook)
|
|
285
|
+
method = "static-analysis"
|
|
286
|
+
# Method 3: Run related tests
|
|
287
|
+
else:
|
|
288
|
+
verified = await self._verify_by_tests(patch, finding)
|
|
289
|
+
method = "test-suite"
|
|
290
|
+
|
|
291
|
+
return FixVerificationResult(
|
|
292
|
+
patch_id=patch.id,
|
|
293
|
+
finding_id=finding_id,
|
|
294
|
+
verified=verified,
|
|
295
|
+
method=method,
|
|
296
|
+
evidence=f"Verification via {method}: {'passed' if verified else 'failed'}",
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
async def _verify_by_attack_replay(self, patch: Patch, finding: Finding) -> bool:
|
|
300
|
+
"""Verify fix by re-running the attack that found it."""
|
|
301
|
+
# This would integrate with RedTeamEngine to replay specific scenario
|
|
302
|
+
# For now, return simulated result
|
|
303
|
+
self.on_progress(" Re-running attack scenario...")
|
|
304
|
+
return True # Simulated success
|
|
305
|
+
|
|
306
|
+
async def _verify_by_static_analysis(
|
|
307
|
+
self,
|
|
308
|
+
patch: Patch,
|
|
309
|
+
finding: Finding | CorrelatedFinding,
|
|
310
|
+
playbook: FixPlaybook,
|
|
311
|
+
) -> bool:
|
|
312
|
+
"""Verify fix by running the deterministic tool from the playbook.
|
|
313
|
+
|
|
314
|
+
The tool is executed against the patched file via tool_verify, closing
|
|
315
|
+
the detect→fix→verify loop with real evidence instead of a simulation.
|
|
316
|
+
"""
|
|
317
|
+
tool = playbook.deterministic_tool
|
|
318
|
+
if not tool:
|
|
319
|
+
return False
|
|
320
|
+
|
|
321
|
+
target = None
|
|
322
|
+
if patch.changes:
|
|
323
|
+
candidate = self.root / patch.changes[0].path
|
|
324
|
+
if candidate.exists():
|
|
325
|
+
target = candidate
|
|
326
|
+
if target is None:
|
|
327
|
+
self.on_progress(f" [WARN] patched file not found; cannot verify with {tool}")
|
|
328
|
+
return False
|
|
329
|
+
|
|
330
|
+
self.on_progress(f" Running {tool} on {target.name} for verification...")
|
|
331
|
+
|
|
332
|
+
from patchi.core.security.tool_verify import finding_resolved
|
|
333
|
+
|
|
334
|
+
try:
|
|
335
|
+
resolved = finding_resolved(tool, target, finding)
|
|
336
|
+
except Exception as e: # defensive: never block the commit on tool errors
|
|
337
|
+
_log.warning("static verification via %s failed: %s", tool, e)
|
|
338
|
+
return False
|
|
339
|
+
|
|
340
|
+
if resolved:
|
|
341
|
+
self.on_progress(f" [OK] {tool} confirms the finding is gone")
|
|
342
|
+
else:
|
|
343
|
+
self.on_progress(f" [WARN] {tool} still reports the finding")
|
|
344
|
+
return resolved
|
|
345
|
+
|
|
346
|
+
async def _verify_by_tests(self, patch: Patch, finding: Finding) -> bool:
|
|
347
|
+
"""Verify fix by running related tests."""
|
|
348
|
+
self.on_progress(" Running related tests...")
|
|
349
|
+
|
|
350
|
+
# Would run test suite or specific tests
|
|
351
|
+
# For now, simulated
|
|
352
|
+
return True
|
|
353
|
+
|
|
354
|
+
async def batch_fix(
|
|
355
|
+
self,
|
|
356
|
+
findings: list[Finding | CorrelatedFinding],
|
|
357
|
+
strategy: str = "auto",
|
|
358
|
+
apply: bool = False,
|
|
359
|
+
verify: bool = True,
|
|
360
|
+
max_fixes: int = 10,
|
|
361
|
+
) -> list[dict]:
|
|
362
|
+
"""Fix multiple findings in batch."""
|
|
363
|
+
self.on_progress(f"🔧 Batch fixing {min(len(findings), max_fixes)} findings...")
|
|
364
|
+
|
|
365
|
+
results = []
|
|
366
|
+
for i, finding in enumerate(findings[:max_fixes]):
|
|
367
|
+
self.on_progress(f" [{i + 1}/{min(len(findings), max_fixes)}] {finding.type}")
|
|
368
|
+
result = await self.fix_finding(finding, strategy, apply, verify)
|
|
369
|
+
results.append(result)
|
|
370
|
+
|
|
371
|
+
return results
|
|
372
|
+
|
|
373
|
+
def get_fix_history(self, limit: int = 50) -> list[FixAttempt]:
|
|
374
|
+
"""Get recent fix attempts."""
|
|
375
|
+
return self.fix_history[-limit:]
|
|
376
|
+
|
|
377
|
+
def get_fix_stats(self) -> dict:
|
|
378
|
+
"""Get statistics on fix attempts."""
|
|
379
|
+
if not self.fix_history:
|
|
380
|
+
return {"total": 0}
|
|
381
|
+
|
|
382
|
+
total = len(self.fix_history)
|
|
383
|
+
successful = sum(1 for f in self.fix_history if f.success)
|
|
384
|
+
verified = sum(1 for f in self.fix_history if f.verified)
|
|
385
|
+
|
|
386
|
+
by_strategy = {}
|
|
387
|
+
for f in self.fix_history:
|
|
388
|
+
by_strategy[f.strategy] = by_strategy.get(f.strategy, 0) + 1
|
|
389
|
+
|
|
390
|
+
return {
|
|
391
|
+
"total_attempts": total,
|
|
392
|
+
"successful": successful,
|
|
393
|
+
"success_rate": successful / total if total > 0 else 0,
|
|
394
|
+
"verified": verified,
|
|
395
|
+
"verification_rate": verified / successful if successful > 0 else 0,
|
|
396
|
+
"by_strategy": by_strategy,
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
class FixPlaybookEngine:
|
|
401
|
+
"""
|
|
402
|
+
Engine for managing and executing fix playbooks.
|
|
403
|
+
|
|
404
|
+
Playbooks define:
|
|
405
|
+
- Deterministic tool fixes (bandit, semgrep, etc.)
|
|
406
|
+
- LLM template fills
|
|
407
|
+
- Verification checks
|
|
408
|
+
- Blast radius analysis
|
|
409
|
+
"""
|
|
410
|
+
|
|
411
|
+
def __init__(self, root: Path):
|
|
412
|
+
self.root = root
|
|
413
|
+
self.domain_loader = DomainLoader(root)
|
|
414
|
+
|
|
415
|
+
def get_playbook_for_finding(self, finding: Finding) -> FixPlaybook | None:
|
|
416
|
+
"""Get the most relevant playbook for a finding."""
|
|
417
|
+
controls = self.domain_loader.match_finding_to_controls(
|
|
418
|
+
finding.type, finding.file, finding.message
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
for ctrl in controls:
|
|
422
|
+
playbook = self.domain_loader.get_playbook(ctrl.control_id)
|
|
423
|
+
if playbook:
|
|
424
|
+
return playbook
|
|
425
|
+
return None
|
|
426
|
+
|
|
427
|
+
def execute_deterministic_fix(self, playbook: FixPlaybook, finding: Finding) -> dict:
|
|
428
|
+
"""Execute a deterministic tool fix."""
|
|
429
|
+
tool = playbook.deterministic_tool
|
|
430
|
+
if not tool:
|
|
431
|
+
return {"success": False, "error": "No deterministic tool specified"}
|
|
432
|
+
|
|
433
|
+
# Map tool names to actual fix functions
|
|
434
|
+
tool_map = {
|
|
435
|
+
"bandit": self._fix_with_bandit,
|
|
436
|
+
"semgrep": self._fix_with_semgrep,
|
|
437
|
+
"sqlfluff": self._fix_with_sqlfluff,
|
|
438
|
+
"prettier": self._fix_with_prettier,
|
|
439
|
+
"eslint": self._fix_with_eslint,
|
|
440
|
+
"ruff": self._fix_with_ruff,
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
fix_func = tool_map.get(tool)
|
|
444
|
+
if not fix_func:
|
|
445
|
+
return {"success": False, "error": f"Unknown tool: {tool}"}
|
|
446
|
+
|
|
447
|
+
return fix_func(finding)
|
|
448
|
+
|
|
449
|
+
def _fix_with_bandit(self, finding: Finding) -> dict:
|
|
450
|
+
return {"success": False, "error": "Bandit fix not implemented"}
|
|
451
|
+
|
|
452
|
+
def _fix_with_semgrep(self, finding: Finding) -> dict:
|
|
453
|
+
return {"success": False, "error": "Semgrep fix not implemented"}
|
|
454
|
+
|
|
455
|
+
def _fix_with_sqlfluff(self, finding: Finding) -> dict:
|
|
456
|
+
return {"success": False, "error": "SQLFluff fix not implemented"}
|
|
457
|
+
|
|
458
|
+
def _fix_with_prettier(self, finding: Finding) -> dict:
|
|
459
|
+
return {"success": False, "error": "Prettier fix not implemented"}
|
|
460
|
+
|
|
461
|
+
def _fix_with_eslint(self, finding: Finding) -> dict:
|
|
462
|
+
return {"success": False, "error": "ESLint fix not implemented"}
|
|
463
|
+
|
|
464
|
+
def _fix_with_ruff(self, finding: Finding) -> dict:
|
|
465
|
+
return {"success": False, "error": "Ruff fix not implemented"}
|
|
466
|
+
|
|
467
|
+
def render_llm_template(self, playbook: FixPlaybook, finding: Finding) -> str:
|
|
468
|
+
"""Render LLM fix template with finding context."""
|
|
469
|
+
if not playbook.llm_fix_template:
|
|
470
|
+
return ""
|
|
471
|
+
|
|
472
|
+
template = playbook.llm_fix_template
|
|
473
|
+
|
|
474
|
+
# Simple template variable substitution
|
|
475
|
+
replacements = {
|
|
476
|
+
"{{finding.type}}": finding.type,
|
|
477
|
+
"{{finding.file}}": finding.file,
|
|
478
|
+
"{{finding.line}}": str(finding.line),
|
|
479
|
+
"{{finding.message}}": finding.message,
|
|
480
|
+
"{{finding.code_snippet}}": finding.code_snippet or "",
|
|
481
|
+
"{{finding.severity}}": finding.severity.value,
|
|
482
|
+
"{{finding.cwe}}": finding.cwe or "",
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
for var, value in replacements.items():
|
|
486
|
+
template = template.replace(var, value)
|
|
487
|
+
|
|
488
|
+
return template
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
# Convenience function
|
|
492
|
+
async def auto_fix_finding(
|
|
493
|
+
root: Path,
|
|
494
|
+
finding: Finding,
|
|
495
|
+
config: dict,
|
|
496
|
+
apply: bool = False,
|
|
497
|
+
verify: bool = True,
|
|
498
|
+
) -> dict:
|
|
499
|
+
"""Auto-fix a single finding."""
|
|
500
|
+
fixer = AutoFixer(root, config)
|
|
501
|
+
return await fixer.fix_finding(finding, apply=apply, verify=verify)
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
# ── Header Auto-Fix ─────────────────────────────────────────────────────────
|
|
505
|
+
|
|
506
|
+
def fix_missing_headers(
|
|
507
|
+
root: Path,
|
|
508
|
+
findings: list[dict],
|
|
509
|
+
apply: bool = False,
|
|
510
|
+
) -> dict:
|
|
511
|
+
"""
|
|
512
|
+
Fix missing security headers based on DAST findings.
|
|
513
|
+
|
|
514
|
+
Args:
|
|
515
|
+
root: Project root path
|
|
516
|
+
findings: List of DAST findings (dicts with type, severity, message)
|
|
517
|
+
apply: If True, actually write the fixes
|
|
518
|
+
|
|
519
|
+
Returns:
|
|
520
|
+
Dict with fixed_count, fixes applied, and suggestions
|
|
521
|
+
"""
|
|
522
|
+
header_fixes = []
|
|
523
|
+
fixed = []
|
|
524
|
+
skipped = []
|
|
525
|
+
|
|
526
|
+
for finding in findings:
|
|
527
|
+
ftype = finding.get("type", "")
|
|
528
|
+
severity = finding.get("severity", "low")
|
|
529
|
+
|
|
530
|
+
# Only fix header-related findings
|
|
531
|
+
if "header" not in ftype and "csp" not in ftype and "hsts" not in ftype and "frame" not in ftype:
|
|
532
|
+
continue
|
|
533
|
+
|
|
534
|
+
# Determine the fix
|
|
535
|
+
fix = None
|
|
536
|
+
if "content_security_policy" in ftype or "csp" in ftype.lower():
|
|
537
|
+
fix = {
|
|
538
|
+
"type": "missing_csp",
|
|
539
|
+
"header": "Content-Security-Policy",
|
|
540
|
+
"value": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'",
|
|
541
|
+
"description": "Add Content-Security-Policy header",
|
|
542
|
+
"severity": severity,
|
|
543
|
+
}
|
|
544
|
+
elif "x_frame_options" in ftype or "frame" in ftype.lower():
|
|
545
|
+
fix = {
|
|
546
|
+
"type": "missing_xfo",
|
|
547
|
+
"header": "X-Frame-Options",
|
|
548
|
+
"value": "DENY",
|
|
549
|
+
"description": "Add X-Frame-Options header",
|
|
550
|
+
"severity": severity,
|
|
551
|
+
}
|
|
552
|
+
elif "hsts" in ftype or "strict_transport" in ftype.lower():
|
|
553
|
+
fix = {
|
|
554
|
+
"type": "missing_hsts",
|
|
555
|
+
"header": "Strict-Transport-Security",
|
|
556
|
+
"value": "max-age=31536000; includeSubDomains",
|
|
557
|
+
"description": "Add Strict-Transport-Security header",
|
|
558
|
+
"severity": severity,
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
if fix:
|
|
562
|
+
header_fixes.append(fix)
|
|
563
|
+
|
|
564
|
+
if not header_fixes:
|
|
565
|
+
return {"fixed_count": 0, "fixes": [], "skipped": []}
|
|
566
|
+
|
|
567
|
+
if not apply:
|
|
568
|
+
return {
|
|
569
|
+
"fixed_count": 0,
|
|
570
|
+
"fixes": header_fixes,
|
|
571
|
+
"skipped": [],
|
|
572
|
+
"dry_run": True,
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
# Find web app files to add headers to
|
|
576
|
+
web_files = []
|
|
577
|
+
for f in root.glob("**/*.py"):
|
|
578
|
+
if ".patchi" in str(f) or "node_modules" in str(f):
|
|
579
|
+
continue
|
|
580
|
+
try:
|
|
581
|
+
content = f.read_text(encoding="utf-8", errors="ignore")
|
|
582
|
+
if any(kw in content for kw in ["FastAPI", "Flask", "Starlette", "@app", "middleware"]):
|
|
583
|
+
web_files.append(f)
|
|
584
|
+
except Exception as _exc:
|
|
585
|
+
_log.warning('fix_missing_headers failed: %s', _exc)
|
|
586
|
+
|
|
587
|
+
if not web_files:
|
|
588
|
+
return {
|
|
589
|
+
"fixed_count": 0,
|
|
590
|
+
"fixes": header_fixes,
|
|
591
|
+
"skipped": header_fixes,
|
|
592
|
+
"error": "No web app files found",
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
# Generate header fix code
|
|
596
|
+
headers_code = _generate_header_middleware(header_fixes)
|
|
597
|
+
|
|
598
|
+
# Apply to the first web app file (append middleware)
|
|
599
|
+
target_file = web_files[0]
|
|
600
|
+
try:
|
|
601
|
+
content = target_file.read_text(encoding="utf-8")
|
|
602
|
+
|
|
603
|
+
# Check if middleware already exists
|
|
604
|
+
if "PatchiSecurityHeaders" in content:
|
|
605
|
+
return {
|
|
606
|
+
"fixed_count": 0,
|
|
607
|
+
"fixes": header_fixes,
|
|
608
|
+
"skipped": header_fixes,
|
|
609
|
+
"message": "Security headers middleware already exists",
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
# Append the middleware
|
|
613
|
+
new_content = content + "\n" + headers_code
|
|
614
|
+
target_file.write_text(new_content, encoding="utf-8")
|
|
615
|
+
|
|
616
|
+
fixed = header_fixes
|
|
617
|
+
return {
|
|
618
|
+
"fixed_count": len(fixed),
|
|
619
|
+
"fixes": fixed,
|
|
620
|
+
"skipped": skipped,
|
|
621
|
+
"file": str(target_file.relative_to(root)),
|
|
622
|
+
}
|
|
623
|
+
except Exception as e:
|
|
624
|
+
return {
|
|
625
|
+
"fixed_count": 0,
|
|
626
|
+
"fixes": header_fixes,
|
|
627
|
+
"skipped": header_fixes,
|
|
628
|
+
"error": str(e),
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def _generate_header_middleware(fixes: list[dict]) -> str:
|
|
633
|
+
"""Generate Python middleware code for security headers."""
|
|
634
|
+
header_lines = []
|
|
635
|
+
for fix in fixes:
|
|
636
|
+
header_lines.append(f' response.headers["{fix["header"]}"] = "{fix["value"]}"')
|
|
637
|
+
|
|
638
|
+
headers_block = "\n".join(header_lines)
|
|
639
|
+
|
|
640
|
+
return f"""
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
# === Patchi Security Headers Middleware (auto-generated) ===
|
|
644
|
+
try:
|
|
645
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
646
|
+
from starlette.requests import Request
|
|
647
|
+
from starlette.responses import Response
|
|
648
|
+
|
|
649
|
+
class PatchiSecurityHeaders(BaseHTTPMiddleware):
|
|
650
|
+
\"\"\"Adds missing security headers to all responses.\"\"\"
|
|
651
|
+
|
|
652
|
+
async def dispatch(self, request: Request, call_next):
|
|
653
|
+
response = await call_next(request)
|
|
654
|
+
{headers_block}
|
|
655
|
+
return response
|
|
656
|
+
|
|
657
|
+
except ImportError:
|
|
658
|
+
pass
|
|
659
|
+
# === End Patchi Security Headers ===
|
|
660
|
+
"""
|