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
patchi/core/fix/base.py
ADDED
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base functionality for fix agents.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Callable
|
|
10
|
+
|
|
11
|
+
from patchi.core.fix.patch import (
|
|
12
|
+
FileChange,
|
|
13
|
+
Patch,
|
|
14
|
+
PatchState,
|
|
15
|
+
PatchType,
|
|
16
|
+
compute_confidence,
|
|
17
|
+
compute_risk_score,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
_log = logging.getLogger("patchi.fix")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _call_ai(
|
|
24
|
+
prompt: str,
|
|
25
|
+
config: dict,
|
|
26
|
+
max_tokens: int = 1500,
|
|
27
|
+
system_prompt: str = "",
|
|
28
|
+
progress_callback: Callable[[str], None] | None = None
|
|
29
|
+
) -> str:
|
|
30
|
+
if os.environ.get("PATCHI_OFFLINE"):
|
|
31
|
+
return ""
|
|
32
|
+
"""
|
|
33
|
+
Call the configured AI model with a prompt.
|
|
34
|
+
Delegates to the canonical client. Uses skill-specific system prompt when provided.
|
|
35
|
+
"""
|
|
36
|
+
from patchi.core.ai.client import call_ai
|
|
37
|
+
|
|
38
|
+
sys_prompt = system_prompt or "You are a helpful coding assistant."
|
|
39
|
+
result = call_ai(config, sys_prompt, prompt, max_tokens, progress_callback=progress_callback)
|
|
40
|
+
return result or ""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _read_file(path: Path) -> str:
|
|
44
|
+
try:
|
|
45
|
+
return path.read_text(encoding="utf-8", errors="replace")
|
|
46
|
+
except OSError:
|
|
47
|
+
return ""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _extract_code_block(text: str) -> str:
|
|
51
|
+
"""Extract the first ```...``` block from an AI response."""
|
|
52
|
+
m = re.search(r"```(?:\w+)?\n(.*?)```", text, re.DOTALL)
|
|
53
|
+
return m.group(1) if m else text.strip()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _make_patch(
|
|
57
|
+
agent_name: str,
|
|
58
|
+
patch_type: PatchType,
|
|
59
|
+
changes: list[FileChange],
|
|
60
|
+
description: str,
|
|
61
|
+
ai_explanation: str,
|
|
62
|
+
finding_id: str = "",
|
|
63
|
+
blast_radius: int = 0,
|
|
64
|
+
agent_certainty: float = 0.9,
|
|
65
|
+
has_test_coverage: bool = True,
|
|
66
|
+
scanner_agent: str = "",
|
|
67
|
+
source_finding: dict | None = None,
|
|
68
|
+
) -> Patch:
|
|
69
|
+
risk = compute_risk_score(changes, blast_radius)
|
|
70
|
+
conf = compute_confidence(changes, agent_certainty, has_test_coverage=has_test_coverage)
|
|
71
|
+
return Patch(
|
|
72
|
+
agent=agent_name,
|
|
73
|
+
patch_type=patch_type,
|
|
74
|
+
finding_id=finding_id,
|
|
75
|
+
changes=changes,
|
|
76
|
+
description=description,
|
|
77
|
+
ai_explanation=ai_explanation,
|
|
78
|
+
risk_score=risk,
|
|
79
|
+
confidence=conf,
|
|
80
|
+
blast_radius=blast_radius,
|
|
81
|
+
state=PatchState.PROPOSED,
|
|
82
|
+
scanner_agent=scanner_agent,
|
|
83
|
+
source_finding=source_finding or {},
|
|
84
|
+
verify_retries=2,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
_SOURCE_EXTENSIONS = {
|
|
89
|
+
".py",
|
|
90
|
+
".js",
|
|
91
|
+
".jsx",
|
|
92
|
+
".mjs",
|
|
93
|
+
".cjs",
|
|
94
|
+
".ts",
|
|
95
|
+
".tsx",
|
|
96
|
+
".mts",
|
|
97
|
+
".java",
|
|
98
|
+
".kt",
|
|
99
|
+
".kts",
|
|
100
|
+
".go",
|
|
101
|
+
".rs",
|
|
102
|
+
".rb",
|
|
103
|
+
".php",
|
|
104
|
+
".cs",
|
|
105
|
+
".swift",
|
|
106
|
+
".dart",
|
|
107
|
+
".c",
|
|
108
|
+
".cpp",
|
|
109
|
+
".cxx",
|
|
110
|
+
".cc",
|
|
111
|
+
".h",
|
|
112
|
+
".hpp",
|
|
113
|
+
".scala",
|
|
114
|
+
".svelte",
|
|
115
|
+
".vue",
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def compute_blast_radius(file_path: str, root: Path) -> int:
|
|
120
|
+
"""Count how many files in the project import this file."""
|
|
121
|
+
rel = file_path
|
|
122
|
+
stem = Path(rel).stem
|
|
123
|
+
count = 0
|
|
124
|
+
for src in root.rglob("*"):
|
|
125
|
+
if src.suffix not in _SOURCE_EXTENSIONS or not src.is_file():
|
|
126
|
+
continue
|
|
127
|
+
try:
|
|
128
|
+
content = src.read_text(encoding="utf-8", errors="ignore")
|
|
129
|
+
if rel in content or stem in content:
|
|
130
|
+
count += 1
|
|
131
|
+
except OSError:
|
|
132
|
+
pass
|
|
133
|
+
return count
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# Domain-specific fix patterns: finding_type → (search_regex, replacement)
|
|
137
|
+
_FIX_PATTERNS: list[tuple[str, str, str]] = [
|
|
138
|
+
# Secrets
|
|
139
|
+
(
|
|
140
|
+
r'(?:password|secret|api_key|token)\s*=\s*["\'][^"\']+["\']',
|
|
141
|
+
"hardcoded_secret",
|
|
142
|
+
"Use os.environ.get() or a secrets manager instead of hardcoded values",
|
|
143
|
+
),
|
|
144
|
+
# Debug mode
|
|
145
|
+
(
|
|
146
|
+
r"DEBUG\s*=\s*True",
|
|
147
|
+
"debug_mode",
|
|
148
|
+
"DEBUG = os.environ.get('DEBUG', 'false').lower() == 'true'",
|
|
149
|
+
),
|
|
150
|
+
# SQL injection
|
|
151
|
+
(
|
|
152
|
+
r"execute\(.*%s.*\%",
|
|
153
|
+
"sqli",
|
|
154
|
+
"Use parameterized queries: cursor.execute('SELECT * FROM users WHERE id = %s', (user_id,))",
|
|
155
|
+
),
|
|
156
|
+
# Missing HTTPS redirect
|
|
157
|
+
(
|
|
158
|
+
r"@app\.route.*methods.*GET",
|
|
159
|
+
"missing_header",
|
|
160
|
+
"Add @app.before_request to redirect HTTP to HTTPS",
|
|
161
|
+
),
|
|
162
|
+
# Weak hash
|
|
163
|
+
(r"hashlib\.(md5|sha1)\(", "weak_hash", "Use hashlib.sha256() or hashlib.sha3_256() instead"),
|
|
164
|
+
# Eval/exec
|
|
165
|
+
(
|
|
166
|
+
r"\b(eval|exec)\s*\(",
|
|
167
|
+
"injection",
|
|
168
|
+
"Avoid eval()/exec(); use ast.literal_eval() or safe parsing",
|
|
169
|
+
),
|
|
170
|
+
# Pickle deserialization
|
|
171
|
+
(
|
|
172
|
+
r"pickle\.loads?\s*\(",
|
|
173
|
+
"injection",
|
|
174
|
+
"Use json.loads() or a safe serialization format instead of pickle",
|
|
175
|
+
),
|
|
176
|
+
# CORS wildcard
|
|
177
|
+
(
|
|
178
|
+
r"Access-Control-Allow-Origin.*\*",
|
|
179
|
+
"cors_wildcard",
|
|
180
|
+
"Restrict CORS to specific trusted origins instead of wildcard (*)",
|
|
181
|
+
),
|
|
182
|
+
# Missing rate limiting
|
|
183
|
+
(r"@app\.route", "missing_rate_limit", "Add Flask-Limiter or similar rate limiting middleware"),
|
|
184
|
+
]
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _find_vulnerable_line(
|
|
188
|
+
content: str, finding_type: str, file_path: str
|
|
189
|
+
) -> tuple[int, str] | None:
|
|
190
|
+
"""Find the specific line that needs fixing based on finding type."""
|
|
191
|
+
import re
|
|
192
|
+
|
|
193
|
+
lines = content.splitlines()
|
|
194
|
+
for search_re, ftype, _ in _FIX_PATTERNS:
|
|
195
|
+
if ftype == finding_type or ftype in finding_type:
|
|
196
|
+
for i, line in enumerate(lines):
|
|
197
|
+
if re.search(search_re, line):
|
|
198
|
+
return (i, line)
|
|
199
|
+
return None
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _apply_fix_pattern(line: str, finding_type: str) -> str | None:
|
|
203
|
+
"""Apply a fix pattern to a specific line."""
|
|
204
|
+
import re
|
|
205
|
+
|
|
206
|
+
if "hardcoded_secret" in finding_type or "password" in finding_type:
|
|
207
|
+
# Replace hardcoded value with env var lookup
|
|
208
|
+
m = re.search(r'(\w+)\s*=\s*["\']([^"\']+)["\']', line)
|
|
209
|
+
if m:
|
|
210
|
+
var_name = m.group(1)
|
|
211
|
+
indent = line[: len(line) - len(line.lstrip())]
|
|
212
|
+
return f'{indent}{var_name} = os.environ.get("{var_name.upper()}", "")'
|
|
213
|
+
|
|
214
|
+
elif "debug_mode" in finding_type:
|
|
215
|
+
indent = line[: len(line) - len(line.lstrip())]
|
|
216
|
+
return f"{indent}DEBUG = os.environ.get('DEBUG', 'false').lower() == 'true'"
|
|
217
|
+
|
|
218
|
+
elif "weak_hash" in finding_type:
|
|
219
|
+
return line.replace("hashlib.md5(", "hashlib.sha256(").replace(
|
|
220
|
+
"hashlib.sha1(", "hashlib.sha256("
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
elif "eval" in finding_type or "exec" in finding_type:
|
|
224
|
+
indent = line[: len(line) - len(line.lstrip())]
|
|
225
|
+
return f"{indent}# SECURITY: Replaced eval/exec with safe alternative\n{line}"
|
|
226
|
+
|
|
227
|
+
elif "pickle" in finding_type:
|
|
228
|
+
return line.replace("pickle.loads(", "json.loads(").replace("pickle.load(", "json.load(")
|
|
229
|
+
|
|
230
|
+
elif "cors_wildcard" in finding_type:
|
|
231
|
+
return line.replace("*", "os.environ.get('ALLOWED_ORIGINS', '').split(',')")
|
|
232
|
+
|
|
233
|
+
return None
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _build_fix_prompt(
|
|
237
|
+
finding_type: str,
|
|
238
|
+
file_path: str,
|
|
239
|
+
original: str,
|
|
240
|
+
line_num: int,
|
|
241
|
+
suggestion: str,
|
|
242
|
+
playbook: dict | None,
|
|
243
|
+
message: str = "",
|
|
244
|
+
) -> str:
|
|
245
|
+
"""Build the AI prompt for fix generation."""
|
|
246
|
+
# Extract context around the vulnerable line + caller hint via Understander/body_tags
|
|
247
|
+
lines = original.splitlines()
|
|
248
|
+
start = max(0, line_num - 5) if line_num else 0
|
|
249
|
+
end = min(len(lines), line_num + 10) if line_num else min(20, len(lines))
|
|
250
|
+
context = "\n".join(f"{i + 1:4d} | {line}" for i, line in enumerate(lines[start:end], start))
|
|
251
|
+
caller_hint = ""
|
|
252
|
+
try:
|
|
253
|
+
from pathlib import Path as _Pth
|
|
254
|
+
|
|
255
|
+
_root = _Pth.cwd()
|
|
256
|
+
for cand in [Path.cwd(), Path(".")]:
|
|
257
|
+
if (cand / ".patchi").exists():
|
|
258
|
+
_root = cand.resolve()
|
|
259
|
+
break
|
|
260
|
+
from patchi.core.brain.body_tags import load_body_tags
|
|
261
|
+
from patchi.core.brain.understander import Understander
|
|
262
|
+
|
|
263
|
+
_tags = load_body_tags(_root)
|
|
264
|
+
if _tags and file_path in _tags:
|
|
265
|
+
tag = _tags[file_path]
|
|
266
|
+
caller_hint = f"\nBody: role={tag.get('role')} fan_in={tag.get('fan_in')} score={tag.get('score')} layer={tag.get('layer')}\n"
|
|
267
|
+
# add function block
|
|
268
|
+
_u = Understander(_root, [], _tags, {}, [])
|
|
269
|
+
caller_hint += _u.function_at(file_path, line_num)[:1200]
|
|
270
|
+
except Exception:
|
|
271
|
+
caller_hint = ""
|
|
272
|
+
|
|
273
|
+
prompt = f"""You are a security expert. Fix the following vulnerability.
|
|
274
|
+
|
|
275
|
+
## Finding
|
|
276
|
+
- Type: {finding_type}
|
|
277
|
+
- File: {file_path}
|
|
278
|
+
- Line: {line_num or "unknown"}
|
|
279
|
+
- Message: {message or suggestion or finding_type}
|
|
280
|
+
"""
|
|
281
|
+
|
|
282
|
+
if playbook:
|
|
283
|
+
prompt += f"- Control ID: {playbook.get('control_id', 'unknown')}\n"
|
|
284
|
+
prompt += f"- Fix strategy: {playbook.get('fix_strategy', 'unknown')}\n"
|
|
285
|
+
if playbook.get("llm_template"):
|
|
286
|
+
prompt += f"- Template hint: {playbook['llm_template']}\n"
|
|
287
|
+
|
|
288
|
+
prompt += f"""
|
|
289
|
+
## Code Context (lines {start + 1}-{end})
|
|
290
|
+
```python
|
|
291
|
+
{context}
|
|
292
|
+
```
|
|
293
|
+
{caller_hint}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
## Requirements
|
|
297
|
+
1. Fix the vulnerability with minimal changes
|
|
298
|
+
2. Preserve the original code style and indentation
|
|
299
|
+
3. Do NOT add comments explaining the fix
|
|
300
|
+
4. Do NOT change unrelated code
|
|
301
|
+
5. Output ONLY the fixed line(s), no explanation
|
|
302
|
+
|
|
303
|
+
## Fixed code
|
|
304
|
+
```python
|
|
305
|
+
"""
|
|
306
|
+
|
|
307
|
+
return prompt
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _generate_fix_with_ai(
|
|
311
|
+
root: Path,
|
|
312
|
+
file_path: str,
|
|
313
|
+
original: str,
|
|
314
|
+
finding_type: str,
|
|
315
|
+
line_num: int,
|
|
316
|
+
suggestion: str,
|
|
317
|
+
playbook: dict | None,
|
|
318
|
+
config: dict,
|
|
319
|
+
message: str = "",
|
|
320
|
+
progress_callback: Callable[[str], None] | None = None,
|
|
321
|
+
) -> FileChange | None:
|
|
322
|
+
"""Use AI to generate a fix for the vulnerability."""
|
|
323
|
+
prompt = _build_fix_prompt(
|
|
324
|
+
finding_type, file_path, original, line_num, suggestion, playbook, message
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
system_prompt = (
|
|
328
|
+
"You are a security engineer fixing vulnerabilities in Python/JS/Go code. "
|
|
329
|
+
"Output only the fixed code in a code block. No explanations. "
|
|
330
|
+
"Make minimal changes to fix the security issue."
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
try:
|
|
334
|
+
response = _call_ai(prompt, config, max_tokens=500, system_prompt=system_prompt, progress_callback=progress_callback)
|
|
335
|
+
except Exception as e:
|
|
336
|
+
_log.warning("AI fix generation failed: %s", e)
|
|
337
|
+
return None
|
|
338
|
+
|
|
339
|
+
if not response:
|
|
340
|
+
return None
|
|
341
|
+
|
|
342
|
+
# Extract the fixed code from AI response
|
|
343
|
+
fixed_code = _extract_code_block(response)
|
|
344
|
+
if not fixed_code or fixed_code.strip() == "":
|
|
345
|
+
return None
|
|
346
|
+
|
|
347
|
+
# Find the vulnerable line to replace
|
|
348
|
+
lines = original.splitlines()
|
|
349
|
+
if line_num and 0 < line_num <= len(lines):
|
|
350
|
+
original_line = lines[line_num - 1]
|
|
351
|
+
# Use the first non-empty line from the AI response
|
|
352
|
+
fixed_lines = [line for line in fixed_code.strip().splitlines() if line.strip()]
|
|
353
|
+
if fixed_lines:
|
|
354
|
+
proposed_line = fixed_lines[0]
|
|
355
|
+
# Preserve original indentation
|
|
356
|
+
indent = len(original_line) - len(original_line.lstrip())
|
|
357
|
+
proposed_line = " " * indent + proposed_line.strip()
|
|
358
|
+
if proposed_line != original_line:
|
|
359
|
+
return FileChange(
|
|
360
|
+
path=file_path,
|
|
361
|
+
original=original_line,
|
|
362
|
+
proposed=proposed_line,
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
return None
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def generate_fix(
|
|
369
|
+
root: Path,
|
|
370
|
+
finding_dict: dict,
|
|
371
|
+
config: dict | None = None,
|
|
372
|
+
progress_callback: Callable[[str], None] | None = None,
|
|
373
|
+
) -> Patch | None:
|
|
374
|
+
"""Generate a fix patch for a finding.
|
|
375
|
+
|
|
376
|
+
Strategy:
|
|
377
|
+
1. AI-powered fix generation (calls LLM with file context)
|
|
378
|
+
2. Pattern-based fix (regex replacements)
|
|
379
|
+
3. Playbook template fix
|
|
380
|
+
4. Suggestion-based fallback
|
|
381
|
+
|
|
382
|
+
The generated patch passes through RiskGate for mode-based approval.
|
|
383
|
+
"""
|
|
384
|
+
config = config or {}
|
|
385
|
+
suggestion = finding_dict.get("suggestion", "")
|
|
386
|
+
file_path = finding_dict.get("file", "")
|
|
387
|
+
finding_type = finding_dict.get("type", "unknown")
|
|
388
|
+
line_num = finding_dict.get("line", 0)
|
|
389
|
+
playbook = finding_dict.get("playbook")
|
|
390
|
+
message = finding_dict.get("message", "")
|
|
391
|
+
|
|
392
|
+
if not file_path:
|
|
393
|
+
return None
|
|
394
|
+
|
|
395
|
+
target = root / file_path
|
|
396
|
+
if not target.is_file():
|
|
397
|
+
return None
|
|
398
|
+
|
|
399
|
+
try:
|
|
400
|
+
original = target.read_text(encoding="utf-8", errors="ignore")
|
|
401
|
+
except OSError:
|
|
402
|
+
return None
|
|
403
|
+
|
|
404
|
+
lines = original.splitlines()
|
|
405
|
+
changes: list[FileChange] = []
|
|
406
|
+
ai_fix_used = False
|
|
407
|
+
|
|
408
|
+
# Strategy 1: AI-powered fix generation
|
|
409
|
+
ai_change = _generate_fix_with_ai(
|
|
410
|
+
root,
|
|
411
|
+
file_path,
|
|
412
|
+
original,
|
|
413
|
+
finding_type,
|
|
414
|
+
line_num,
|
|
415
|
+
suggestion,
|
|
416
|
+
playbook,
|
|
417
|
+
config,
|
|
418
|
+
message,
|
|
419
|
+
progress_callback,
|
|
420
|
+
)
|
|
421
|
+
if ai_change:
|
|
422
|
+
changes.append(ai_change)
|
|
423
|
+
ai_fix_used = True
|
|
424
|
+
|
|
425
|
+
# Strategy 2: Pattern-based fix (fallback)
|
|
426
|
+
if not changes:
|
|
427
|
+
found = _find_vulnerable_line(original, finding_type, file_path)
|
|
428
|
+
if found:
|
|
429
|
+
line_idx, old_line = found
|
|
430
|
+
fixed_line = _apply_fix_pattern(old_line, finding_type)
|
|
431
|
+
if fixed_line and fixed_line != old_line:
|
|
432
|
+
changes.append(
|
|
433
|
+
FileChange(
|
|
434
|
+
path=file_path,
|
|
435
|
+
original=old_line,
|
|
436
|
+
proposed=fixed_line,
|
|
437
|
+
)
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
# Strategy 3: Playbook template fix (fallback)
|
|
441
|
+
if not changes and playbook and playbook.get("llm_template"):
|
|
442
|
+
control_id = playbook.get("control_id", finding_type)
|
|
443
|
+
found = _find_vulnerable_line(original, control_id, file_path)
|
|
444
|
+
if found:
|
|
445
|
+
line_idx, old_line = found
|
|
446
|
+
fixed_line = _apply_fix_pattern(old_line, control_id)
|
|
447
|
+
if fixed_line and fixed_line != old_line:
|
|
448
|
+
changes.append(
|
|
449
|
+
FileChange(
|
|
450
|
+
path=file_path,
|
|
451
|
+
original=old_line,
|
|
452
|
+
proposed=fixed_line,
|
|
453
|
+
)
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
# Strategy 4: Line-number targeted fix (fallback)
|
|
457
|
+
if not changes and line_num and 0 < line_num <= len(lines):
|
|
458
|
+
old_line = lines[line_num - 1]
|
|
459
|
+
fixed_line = _apply_fix_pattern(old_line, finding_type)
|
|
460
|
+
if fixed_line and fixed_line != old_line:
|
|
461
|
+
changes.append(
|
|
462
|
+
FileChange(
|
|
463
|
+
path=file_path,
|
|
464
|
+
original=old_line,
|
|
465
|
+
proposed=fixed_line,
|
|
466
|
+
)
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
# Strategy 5: Suggestion-based fallback (last resort)
|
|
470
|
+
if not changes and suggestion:
|
|
471
|
+
changes.append(
|
|
472
|
+
FileChange(
|
|
473
|
+
path=file_path,
|
|
474
|
+
original=lines[0] if lines else "",
|
|
475
|
+
proposed=f"# TODO: {suggestion}\n{lines[0] if lines else ''}",
|
|
476
|
+
)
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
if not changes:
|
|
480
|
+
return None
|
|
481
|
+
|
|
482
|
+
# Build the patch
|
|
483
|
+
patch = _make_patch(
|
|
484
|
+
agent_name="auto_fixer",
|
|
485
|
+
patch_type=PatchType.SECURITY,
|
|
486
|
+
changes=changes,
|
|
487
|
+
description=f"{'AI-generated' if ai_fix_used else 'Pattern-based'} fix for {finding_type}",
|
|
488
|
+
ai_explanation=suggestion or message or f"Fixed {finding_type} in {file_path}",
|
|
489
|
+
finding_id=finding_type,
|
|
490
|
+
blast_radius=compute_blast_radius(file_path, root),
|
|
491
|
+
agent_certainty=0.8 if ai_fix_used else 0.6,
|
|
492
|
+
source_finding=finding_dict,
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
# Pass through RiskGate
|
|
496
|
+
try:
|
|
497
|
+
from patchi.core.fix.risk_gate import RiskGate
|
|
498
|
+
|
|
499
|
+
gate = RiskGate(root)
|
|
500
|
+
gate_result = gate.evaluate(patch)
|
|
501
|
+
_log.info(
|
|
502
|
+
"RiskGate for %s: %s (risk=%d)",
|
|
503
|
+
finding_type,
|
|
504
|
+
"auto" if gate_result.is_auto else "blocked" if gate_result.is_blocked else "review",
|
|
505
|
+
patch.risk_score,
|
|
506
|
+
)
|
|
507
|
+
except Exception as e:
|
|
508
|
+
_log.warning("RiskGate evaluation failed: %s", e)
|
|
509
|
+
|
|
510
|
+
return patch
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CodeFixer - Surgical line-level bug and error patches.
|
|
3
|
+
Handles: null/undefined errors, missing error handling, off-by-one, type mismatch.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import textwrap
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from patchi.core.agents.base import (
|
|
13
|
+
AgentGroup,
|
|
14
|
+
AgentInput,
|
|
15
|
+
AgentResult,
|
|
16
|
+
BaseAgent,
|
|
17
|
+
Finding,
|
|
18
|
+
register,
|
|
19
|
+
)
|
|
20
|
+
from patchi.core.debug import debug_context_from_finding
|
|
21
|
+
from patchi.core.fix.patch import FileChange, Patch, PatchType
|
|
22
|
+
|
|
23
|
+
# Import common functionality from base module
|
|
24
|
+
from .base import _call_ai, _extract_code_block, _make_patch, _read_file, compute_blast_radius
|
|
25
|
+
from .fix_agents import _detect_language
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@register
|
|
29
|
+
class CodeFixer(BaseAgent):
|
|
30
|
+
"""
|
|
31
|
+
Surgical line-level bug and error patches.
|
|
32
|
+
Handles: null/undefined errors, missing error handling, off-by-one, type mismatch.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
name = "CodeFixer"
|
|
36
|
+
group = AgentGroup.FIX
|
|
37
|
+
timeout = 120
|
|
38
|
+
|
|
39
|
+
def _run(self, inp: AgentInput, result: AgentResult) -> None:
|
|
40
|
+
findings = self._get_findings(
|
|
41
|
+
inp, ["parse_error", "unprotected_sensitive_route", "bug", "test_failure"]
|
|
42
|
+
)
|
|
43
|
+
patches: list[Patch] = []
|
|
44
|
+
debug_cache: dict[tuple[str, str], dict | None] = {}
|
|
45
|
+
|
|
46
|
+
for finding in findings[:10]:
|
|
47
|
+
ftype = finding.get("type", "")
|
|
48
|
+
finding_file = finding.get("file", "")
|
|
49
|
+
|
|
50
|
+
cache_key = (str(inp.root), finding_file)
|
|
51
|
+
if cache_key not in debug_cache:
|
|
52
|
+
debug_cache[cache_key] = debug_context_from_finding(finding, inp.root)
|
|
53
|
+
dbg = debug_cache[cache_key]
|
|
54
|
+
|
|
55
|
+
target_file = finding_file
|
|
56
|
+
if ftype == "test_failure" and dbg:
|
|
57
|
+
frames = dbg.get("frames") or []
|
|
58
|
+
ti = dbg.get("trigger_frame", 0)
|
|
59
|
+
if 0 <= ti < len(frames):
|
|
60
|
+
cand = frames[ti].get("path", "")
|
|
61
|
+
if cand:
|
|
62
|
+
p = Path(cand)
|
|
63
|
+
if not p.is_absolute():
|
|
64
|
+
p = inp.root / p
|
|
65
|
+
p = p.resolve()
|
|
66
|
+
try:
|
|
67
|
+
p.relative_to(inp.root.resolve())
|
|
68
|
+
if p.is_file():
|
|
69
|
+
# Normalise to forward slashes so patch paths stay
|
|
70
|
+
# consistent across platforms (the rest of the
|
|
71
|
+
# pipeline stores relative paths with '/' separators).
|
|
72
|
+
target_file = p.relative_to(inp.root).as_posix()
|
|
73
|
+
except ValueError:
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
fpath = target_file
|
|
77
|
+
path = inp.root / fpath
|
|
78
|
+
original = _read_file(path)
|
|
79
|
+
if not original:
|
|
80
|
+
continue
|
|
81
|
+
|
|
82
|
+
blast_radius = 0
|
|
83
|
+
if hasattr(inp, "extra") and "brain_report" in inp.extra:
|
|
84
|
+
brain_report = inp.extra["brain_report"]
|
|
85
|
+
if (
|
|
86
|
+
hasattr(brain_report, "blast_radius_map")
|
|
87
|
+
and fpath in brain_report.blast_radius_map
|
|
88
|
+
):
|
|
89
|
+
blast_radius = len(brain_report.blast_radius_map[fpath].all_dependents)
|
|
90
|
+
else:
|
|
91
|
+
blast_radius = compute_blast_radius(fpath, inp.root)
|
|
92
|
+
|
|
93
|
+
lang = _detect_language(fpath)
|
|
94
|
+
debug_section = ""
|
|
95
|
+
if dbg:
|
|
96
|
+
debug_section = f"\n\nRUNTIME STATE AT FAILURE:\n{json.dumps(dbg, indent=2)}"
|
|
97
|
+
|
|
98
|
+
test_context = ""
|
|
99
|
+
if ftype == "test_failure" and finding_file:
|
|
100
|
+
test_path = inp.root / finding_file
|
|
101
|
+
test_content = _read_file(test_path)
|
|
102
|
+
if test_content:
|
|
103
|
+
test_context = (
|
|
104
|
+
f"\n\nFailing test file ({finding_file}):\n```\n{test_content[:2000]}\n```"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
prompt = textwrap.dedent(f"""
|
|
108
|
+
Fix this issue in the file below. Return ONLY the corrected file content
|
|
109
|
+
inside a code block. Do not explain. Do not add comments. Make the
|
|
110
|
+
minimal change needed.
|
|
111
|
+
|
|
112
|
+
Language: {lang}
|
|
113
|
+
Issue: {finding.get("message", "")}
|
|
114
|
+
File: {fpath}
|
|
115
|
+
Line: {finding.get("line", 0)}
|
|
116
|
+
Suggestion: {finding.get("suggestion", "")}
|
|
117
|
+
{test_context}
|
|
118
|
+
|
|
119
|
+
File content:
|
|
120
|
+
```
|
|
121
|
+
{original[:3000]}
|
|
122
|
+
```
|
|
123
|
+
{debug_section}
|
|
124
|
+
|
|
125
|
+
Return the full corrected file inside triple backticks.
|
|
126
|
+
""").strip()
|
|
127
|
+
|
|
128
|
+
ai_response = _call_ai(prompt, inp.config, progress_callback=inp.on_ai_progress)
|
|
129
|
+
if not ai_response:
|
|
130
|
+
continue
|
|
131
|
+
|
|
132
|
+
proposed = _extract_code_block(ai_response)
|
|
133
|
+
if proposed == original or not proposed.strip():
|
|
134
|
+
continue
|
|
135
|
+
|
|
136
|
+
change = FileChange(path=fpath, original=original, proposed=proposed)
|
|
137
|
+
if not change.diff.strip():
|
|
138
|
+
continue
|
|
139
|
+
|
|
140
|
+
patch = _make_patch(
|
|
141
|
+
agent_name=self.name,
|
|
142
|
+
patch_type=PatchType.BUG_FIX,
|
|
143
|
+
changes=[change],
|
|
144
|
+
description=f"Fix: {finding.get('message', '')[:80]}",
|
|
145
|
+
ai_explanation=finding.get("suggestion", ""),
|
|
146
|
+
finding_id=finding.get("type", ""),
|
|
147
|
+
blast_radius=blast_radius,
|
|
148
|
+
# Only default the scanner to UnitTestAgent for test_failure
|
|
149
|
+
# findings — for other types an empty scanner keeps the
|
|
150
|
+
# applier's M-16 _verify_fix on its generic fallback path
|
|
151
|
+
# instead of re-running the test suite inside apply().
|
|
152
|
+
scanner_agent=(
|
|
153
|
+
"UnitTestAgent" if ftype == "test_failure" else finding.get("agent", "")
|
|
154
|
+
),
|
|
155
|
+
source_finding=finding,
|
|
156
|
+
)
|
|
157
|
+
patches.append(patch)
|
|
158
|
+
result.data.setdefault("patches", []).append(patch.to_dict())
|
|
159
|
+
|
|
160
|
+
result.files_scanned = len(findings)
|
|
161
|
+
result.data["patch_count"] = len(patches)
|
|
162
|
+
result.ai_calls_made = len(patches)
|
|
163
|
+
|
|
164
|
+
def _get_findings(self, inp: AgentInput, types: list[str]) -> list[Finding]:
|
|
165
|
+
# Fix agents receive findings via extra dict (set by the fix command)
|
|
166
|
+
all_findings = inp.extra.get("findings", [])
|
|
167
|
+
return [
|
|
168
|
+
f for f in all_findings if f.get("type") in types and f.get("fix_agent") == self.name
|
|
169
|
+
]
|