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,158 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Secret Scanner Analyzer — detects hardcoded secrets and credentials.
|
|
3
|
+
|
|
4
|
+
This is a built-in analyzer that scans for:
|
|
5
|
+
- Hardcoded passwords
|
|
6
|
+
- API keys
|
|
7
|
+
- Private keys
|
|
8
|
+
- Connection strings
|
|
9
|
+
- Tokens
|
|
10
|
+
|
|
11
|
+
Example usage:
|
|
12
|
+
from patchi.core.plugins import get_registry, AnalyzerContext
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
registry = get_registry()
|
|
16
|
+
context = AnalyzerContext(root=Path("."))
|
|
17
|
+
result = registry.run("secret-scanner", context)
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import re
|
|
23
|
+
|
|
24
|
+
from patchi.core.plugins.analyzer import (
|
|
25
|
+
Analyzer,
|
|
26
|
+
AnalyzerContext,
|
|
27
|
+
AnalyzerResult,
|
|
28
|
+
FileNode,
|
|
29
|
+
Finding,
|
|
30
|
+
Severity,
|
|
31
|
+
make_finding,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# Patterns that indicate secrets
|
|
35
|
+
SECRET_PATTERNS = [
|
|
36
|
+
# Passwords
|
|
37
|
+
(r'(?i)(password|passwd|pwd)\s*[=:]\s*["\']([^"\']{8,})["\']', "hardcoded-password", Severity.CRITICAL),
|
|
38
|
+
(r'(?i)(password|passwd|pwd)\s*[=:]\s*([^\s"\'#]{8,})', "hardcoded-password", Severity.CRITICAL),
|
|
39
|
+
|
|
40
|
+
# API Keys
|
|
41
|
+
(r'(?i)(api[_-]?key|apikey)\s*[=:]\s*["\']([A-Za-z0-9_\-]{20,})["\']', "hardcoded-api-key", Severity.HIGH),
|
|
42
|
+
(r'(?i)(secret[_-]?key|client[_-]?secret)\s*[=:]\s*["\']([A-Za-z0-9_\-]{20,})["\']', "hardcoded-secret-key", Severity.HIGH),
|
|
43
|
+
|
|
44
|
+
# Private keys
|
|
45
|
+
(r'-----BEGIN\s+(RSA\s+)?PRIVATE\s+KEY-----', "private-key-exposure", Severity.CRITICAL),
|
|
46
|
+
|
|
47
|
+
# Connection strings
|
|
48
|
+
(r'(?i)(mysql|postgres|postgresql|mongodb|redis)://[^\s"\'<>]{20,}', "hardcoded-connection-string", Severity.HIGH),
|
|
49
|
+
|
|
50
|
+
# Tokens
|
|
51
|
+
(r'(?i)(token|access[_-]?token|auth[_-]?token)\s*[=:]\s*["\']([A-Za-z0-9_\-\.]{20,})["\']', "hardcoded-token", Severity.HIGH),
|
|
52
|
+
|
|
53
|
+
# AWS keys
|
|
54
|
+
(r'AKIA[0-9A-Z]{16}', "aws-access-key", Severity.CRITICAL),
|
|
55
|
+
|
|
56
|
+
# GitHub tokens
|
|
57
|
+
(r'ghp_[A-Za-z0-9]{36}', "github-personal-access-token", Severity.CRITICAL),
|
|
58
|
+
|
|
59
|
+
# JWT tokens
|
|
60
|
+
(r'eyJ[A-Za-z0-9_\-]*\.eyJ[A-Za-z0-9_\-]*\.[A-Za-z0-9_\-]*', "jwt-token-exposure", Severity.HIGH),
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
# Files to skip
|
|
64
|
+
SKIP_PATTERNS = [
|
|
65
|
+
"*.min.js",
|
|
66
|
+
"*.min.css",
|
|
67
|
+
"*.map",
|
|
68
|
+
"*.lock",
|
|
69
|
+
"package-lock.json",
|
|
70
|
+
"yarn.lock",
|
|
71
|
+
"*.pyc",
|
|
72
|
+
"__pycache__/*",
|
|
73
|
+
"node_modules/*",
|
|
74
|
+
".git/*",
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class SecretScanner(Analyzer):
|
|
79
|
+
"""Scans for hardcoded secrets and credentials."""
|
|
80
|
+
|
|
81
|
+
name = "secret-scanner"
|
|
82
|
+
version = "1.0.0"
|
|
83
|
+
description = "Detects hardcoded passwords, API keys, tokens, and other secrets"
|
|
84
|
+
priority = 10 # Run early
|
|
85
|
+
supported_file_patterns = [] # All files
|
|
86
|
+
|
|
87
|
+
def analyze(self, context: AnalyzerContext) -> AnalyzerResult:
|
|
88
|
+
findings: list[Finding] = []
|
|
89
|
+
files_scanned = 0
|
|
90
|
+
|
|
91
|
+
for file in context.files:
|
|
92
|
+
if not self.should_analyze(file):
|
|
93
|
+
continue
|
|
94
|
+
|
|
95
|
+
# Skip binary/minified files
|
|
96
|
+
if any(re.match(p.replace("*", ".*"), file.path) for p in SKIP_PATTERNS):
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
files_scanned += 1
|
|
100
|
+
file_findings = self._scan_file(file)
|
|
101
|
+
findings.extend(file_findings)
|
|
102
|
+
|
|
103
|
+
return AnalyzerResult(
|
|
104
|
+
findings=findings,
|
|
105
|
+
files_analyzed=files_scanned,
|
|
106
|
+
metrics={
|
|
107
|
+
"files_scanned": files_scanned,
|
|
108
|
+
"findings_count": len(findings),
|
|
109
|
+
},
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def _scan_file(self, file: FileNode) -> list[Finding]:
|
|
113
|
+
"""Scan a single file for secrets."""
|
|
114
|
+
findings: list[Finding] = []
|
|
115
|
+
|
|
116
|
+
if not file.content:
|
|
117
|
+
return findings
|
|
118
|
+
|
|
119
|
+
lines = file.content.splitlines()
|
|
120
|
+
|
|
121
|
+
for line_num, line in enumerate(lines, start=1):
|
|
122
|
+
for pattern, finding_type, severity in SECRET_PATTERNS:
|
|
123
|
+
matches = re.finditer(pattern, line)
|
|
124
|
+
for match in matches:
|
|
125
|
+
# Skip false positives
|
|
126
|
+
if self._is_false_positive(line, match):
|
|
127
|
+
continue
|
|
128
|
+
|
|
129
|
+
findings.append(make_finding(
|
|
130
|
+
file=file.path,
|
|
131
|
+
line=line_num,
|
|
132
|
+
type=finding_type,
|
|
133
|
+
severity=severity,
|
|
134
|
+
message=f"Possible {finding_type.replace('-', ' ')} detected",
|
|
135
|
+
detail=f"Pattern matched: {match.group()[:50]}...",
|
|
136
|
+
code_snippet=line.strip()[:200],
|
|
137
|
+
suggestion="Move secrets to environment variables or a secrets manager",
|
|
138
|
+
confidence=0.8,
|
|
139
|
+
))
|
|
140
|
+
|
|
141
|
+
return findings
|
|
142
|
+
|
|
143
|
+
def _is_false_positive(self, line: str, match: re.Match) -> bool:
|
|
144
|
+
"""Check if a match is likely a false positive."""
|
|
145
|
+
# Skip comments
|
|
146
|
+
stripped = line.strip()
|
|
147
|
+
if stripped.startswith("#") or stripped.startswith("//") or stripped.startswith("*"):
|
|
148
|
+
return True
|
|
149
|
+
|
|
150
|
+
# Skip test files
|
|
151
|
+
if "test" in line.lower() or "example" in line.lower() or "mock" in line.lower():
|
|
152
|
+
return True
|
|
153
|
+
|
|
154
|
+
# Skip placeholders
|
|
155
|
+
if any(p in match.group().lower() for p in ["xxx", "yyy", "placeholder", "your-", "example"]):
|
|
156
|
+
return True
|
|
157
|
+
|
|
158
|
+
return False
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TODO Scanner Analyzer — detects TODO, FIXME, HACK, and other code comments.
|
|
3
|
+
|
|
4
|
+
This is a built-in analyzer that scans for:
|
|
5
|
+
- TODO comments
|
|
6
|
+
- FIXME comments
|
|
7
|
+
- HACK comments
|
|
8
|
+
- XXX comments
|
|
9
|
+
- NOTE comments
|
|
10
|
+
|
|
11
|
+
Example usage:
|
|
12
|
+
from patchi.core.plugins import get_registry, AnalyzerContext
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
registry = get_registry()
|
|
16
|
+
context = AnalyzerContext(root=Path("."))
|
|
17
|
+
result = registry.run("todo-scanner", context)
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import re
|
|
23
|
+
|
|
24
|
+
from patchi.core.plugins.analyzer import (
|
|
25
|
+
Analyzer,
|
|
26
|
+
AnalyzerContext,
|
|
27
|
+
AnalyzerResult,
|
|
28
|
+
FileNode,
|
|
29
|
+
Finding,
|
|
30
|
+
Severity,
|
|
31
|
+
make_finding,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# TODO-like patterns
|
|
35
|
+
TODO_PATTERNS = [
|
|
36
|
+
(r'(?i)\bTODO\b', "todo-comment", Severity.LOW),
|
|
37
|
+
(r'(?i)\bFIXME\b', "fixme-comment", Severity.MEDIUM),
|
|
38
|
+
(r'(?i)\bHACK\b', "hack-comment", Severity.MEDIUM),
|
|
39
|
+
(r'(?i)\bXXX\b', "xxx-comment", Severity.MEDIUM),
|
|
40
|
+
(r'(?i)\bNOTE\b', "note-comment", Severity.INFO),
|
|
41
|
+
(r'(?i)\bBUG\b', "bug-comment", Severity.MEDIUM),
|
|
42
|
+
(r'(?i)\bOPTIMIZE\b', "optimize-comment", Severity.LOW),
|
|
43
|
+
(r'(?i)\bREFACTOR\b', "refactor-comment", Severity.LOW),
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class TodoScanner(Analyzer):
|
|
48
|
+
"""Scans for TODO, FIXME, HACK, and other code comments."""
|
|
49
|
+
|
|
50
|
+
name = "todo-scanner"
|
|
51
|
+
version = "1.0.0"
|
|
52
|
+
description = "Detects TODO, FIXME, HACK, and other code comments"
|
|
53
|
+
priority = 200 # Run last
|
|
54
|
+
supported_file_patterns = [] # All files
|
|
55
|
+
|
|
56
|
+
def analyze(self, context: AnalyzerContext) -> AnalyzerResult:
|
|
57
|
+
findings: list[Finding] = []
|
|
58
|
+
files_scanned = 0
|
|
59
|
+
|
|
60
|
+
for file in context.files:
|
|
61
|
+
if not self.should_analyze(file):
|
|
62
|
+
continue
|
|
63
|
+
|
|
64
|
+
files_scanned += 1
|
|
65
|
+
file_findings = self._scan_file(file)
|
|
66
|
+
findings.extend(file_findings)
|
|
67
|
+
|
|
68
|
+
return AnalyzerResult(
|
|
69
|
+
findings=findings,
|
|
70
|
+
files_analyzed=files_scanned,
|
|
71
|
+
metrics={
|
|
72
|
+
"files_scanned": files_scanned,
|
|
73
|
+
"findings_count": len(findings),
|
|
74
|
+
},
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
def _scan_file(self, file: FileNode) -> list[Finding]:
|
|
78
|
+
"""Scan a single file for TODO-like comments."""
|
|
79
|
+
findings: list[Finding] = []
|
|
80
|
+
|
|
81
|
+
if not file.content:
|
|
82
|
+
return findings
|
|
83
|
+
|
|
84
|
+
lines = file.content.splitlines()
|
|
85
|
+
|
|
86
|
+
for line_num, line in enumerate(lines, start=1):
|
|
87
|
+
stripped = line.strip()
|
|
88
|
+
|
|
89
|
+
# Skip non-comment lines
|
|
90
|
+
if not any(stripped.startswith(p) for p in ["#", "//", "/*", "*", "<!--"]):
|
|
91
|
+
# Check if it's inline comment
|
|
92
|
+
if not any(p in line for p in ["#", "//", "/*", "<!--"]):
|
|
93
|
+
continue
|
|
94
|
+
|
|
95
|
+
for pattern, finding_type, severity in TODO_PATTERNS:
|
|
96
|
+
match = re.search(pattern, line)
|
|
97
|
+
if match:
|
|
98
|
+
# Extract the comment text
|
|
99
|
+
comment_text = line.strip()
|
|
100
|
+
if len(comment_text) > 200:
|
|
101
|
+
comment_text = comment_text[:200] + "..."
|
|
102
|
+
|
|
103
|
+
findings.append(make_finding(
|
|
104
|
+
file=file.path,
|
|
105
|
+
line=line_num,
|
|
106
|
+
type=finding_type,
|
|
107
|
+
severity=severity,
|
|
108
|
+
message=f"Code comment: {match.group().upper()}",
|
|
109
|
+
detail=comment_text,
|
|
110
|
+
code_snippet=line.strip()[:200],
|
|
111
|
+
suggestion=f"Address the {match.group().upper()} comment or remove it",
|
|
112
|
+
confidence=1.0,
|
|
113
|
+
))
|
|
114
|
+
|
|
115
|
+
return findings
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Plugin registry with auto-discovery for Patchi.
|
|
3
|
+
|
|
4
|
+
Automatically discovers and registers analyzers from:
|
|
5
|
+
1. patchi/core/plugins/analyzers/ (built-in)
|
|
6
|
+
2. ~/.patchi/plugins/ (user plugins)
|
|
7
|
+
3. .patchi/plugins/ (project plugins)
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
from patchi.core.plugins.registry import get_registry
|
|
11
|
+
|
|
12
|
+
registry = get_registry()
|
|
13
|
+
results = registry.run_all(context)
|
|
14
|
+
|
|
15
|
+
# Or run specific analyzer
|
|
16
|
+
result = registry.run("secret-scanner", context)
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import importlib
|
|
22
|
+
import importlib.util
|
|
23
|
+
import logging
|
|
24
|
+
import sys
|
|
25
|
+
import time
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
from patchi.core.plugins.analyzer import (
|
|
29
|
+
Analyzer,
|
|
30
|
+
AnalyzerContext,
|
|
31
|
+
AnalyzerResult,
|
|
32
|
+
Finding,
|
|
33
|
+
Severity,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
_log = logging.getLogger("patchi.plugins")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class PluginRegistry:
|
|
40
|
+
"""Registry for discovering and running analyzers.
|
|
41
|
+
|
|
42
|
+
Features:
|
|
43
|
+
- Auto-discovery from plugin directories
|
|
44
|
+
- Lazy loading (only import when needed)
|
|
45
|
+
- Priority-based execution order
|
|
46
|
+
- Timeout enforcement
|
|
47
|
+
- Error isolation (one plugin can't crash others)
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(self) -> None:
|
|
51
|
+
self._analyzers: dict[str, Analyzer] = {}
|
|
52
|
+
self._loaded: bool = False
|
|
53
|
+
self._load_time_ms: int = 0
|
|
54
|
+
|
|
55
|
+
def discover(self, extra_dirs: list[Path] | None = None) -> list[str]:
|
|
56
|
+
"""Discover and register all analyzers.
|
|
57
|
+
|
|
58
|
+
Returns list of errors (empty = success).
|
|
59
|
+
"""
|
|
60
|
+
if self._loaded:
|
|
61
|
+
return []
|
|
62
|
+
|
|
63
|
+
t0 = time.monotonic()
|
|
64
|
+
errors: list[str] = []
|
|
65
|
+
|
|
66
|
+
# Plugin search paths
|
|
67
|
+
search_paths = [
|
|
68
|
+
Path(__file__).parent / "analyzers", # built-in
|
|
69
|
+
Path.home() / ".patchi" / "plugins", # user
|
|
70
|
+
Path.cwd() / ".patchi" / "plugins", # project
|
|
71
|
+
]
|
|
72
|
+
if extra_dirs:
|
|
73
|
+
search_paths.extend(extra_dirs)
|
|
74
|
+
|
|
75
|
+
for plugin_dir in search_paths:
|
|
76
|
+
if not plugin_dir.exists():
|
|
77
|
+
continue
|
|
78
|
+
|
|
79
|
+
_log.debug("Discovering plugins in %s", plugin_dir)
|
|
80
|
+
|
|
81
|
+
# Walk all Python files in the plugin directory
|
|
82
|
+
for py_file in plugin_dir.glob("**/*.py"):
|
|
83
|
+
if py_file.name.startswith("_"):
|
|
84
|
+
continue
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
self._load_plugin_file(py_file, plugin_dir)
|
|
88
|
+
except Exception as e:
|
|
89
|
+
errors.append(f"{py_file}: {type(e).__name__}: {e}")
|
|
90
|
+
_log.debug("Failed to load plugin %s: %s", py_file, e)
|
|
91
|
+
|
|
92
|
+
# Also discover from AGENT_PACKAGES (existing agents)
|
|
93
|
+
try:
|
|
94
|
+
self._discover_from_agent_packages()
|
|
95
|
+
except Exception as e:
|
|
96
|
+
errors.append(f"Agent package discovery: {type(e).__name__}: {e}")
|
|
97
|
+
|
|
98
|
+
self._loaded = True
|
|
99
|
+
self._load_time_ms = int((time.monotonic() - t0) * 1000)
|
|
100
|
+
|
|
101
|
+
_log.info(
|
|
102
|
+
"Plugin discovery complete: %d analyzers in %dms",
|
|
103
|
+
len(self._analyzers),
|
|
104
|
+
self._load_time_ms,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
return errors
|
|
108
|
+
|
|
109
|
+
def _load_plugin_file(self, py_file: Path, base_dir: Path) -> None:
|
|
110
|
+
"""Load a single plugin file and register any Analyzer subclasses."""
|
|
111
|
+
# Compute module name relative to base_dir
|
|
112
|
+
rel = py_file.relative_to(base_dir)
|
|
113
|
+
module_name = f"patchi.plugins.{rel.stem}"
|
|
114
|
+
|
|
115
|
+
# Skip if already loaded
|
|
116
|
+
if module_name in sys.modules:
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
# Load the module
|
|
120
|
+
spec = importlib.util.spec_from_file_location(module_name, py_file)
|
|
121
|
+
if not spec or not spec.loader:
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
module = importlib.util.module_from_spec(spec)
|
|
125
|
+
sys.modules[module_name] = module
|
|
126
|
+
spec.loader.exec_module(module)
|
|
127
|
+
|
|
128
|
+
# Find and register any Analyzer subclasses
|
|
129
|
+
for attr_name in dir(module):
|
|
130
|
+
attr = getattr(module, attr_name)
|
|
131
|
+
if (
|
|
132
|
+
isinstance(attr, type)
|
|
133
|
+
and issubclass(attr, Analyzer)
|
|
134
|
+
and attr is not Analyzer
|
|
135
|
+
):
|
|
136
|
+
self.register_analyzer(attr())
|
|
137
|
+
|
|
138
|
+
def _discover_from_agent_packages(self) -> None:
|
|
139
|
+
"""Discover analyzers from existing agent packages.
|
|
140
|
+
|
|
141
|
+
This wraps existing BaseAgent subclasses as analyzers for backwards
|
|
142
|
+
compatibility.
|
|
143
|
+
"""
|
|
144
|
+
try:
|
|
145
|
+
from patchi.core.agents.base import (
|
|
146
|
+
_REGISTRY,
|
|
147
|
+
discover_agent_modules,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# Discover all agents first
|
|
151
|
+
discover_agent_modules()
|
|
152
|
+
|
|
153
|
+
# Wrap each agent as an analyzer
|
|
154
|
+
for name, agent_cls in _REGISTRY.items():
|
|
155
|
+
if name not in self._analyzers:
|
|
156
|
+
wrapper = AgentAnalyzerWrapper(agent_cls)
|
|
157
|
+
self._analyzers[name] = wrapper
|
|
158
|
+
|
|
159
|
+
except ImportError:
|
|
160
|
+
pass
|
|
161
|
+
|
|
162
|
+
def register_analyzer(self, analyzer: Analyzer) -> None:
|
|
163
|
+
"""Register an analyzer instance."""
|
|
164
|
+
if analyzer.name in self._analyzers:
|
|
165
|
+
_log.warning(
|
|
166
|
+
"Analyzer '%s' already registered, overriding",
|
|
167
|
+
analyzer.name,
|
|
168
|
+
)
|
|
169
|
+
self._analyzers[analyzer.name] = analyzer
|
|
170
|
+
_log.debug("Registered analyzer: %s v%s", analyzer.name, analyzer.version)
|
|
171
|
+
|
|
172
|
+
def get(self, name: str) -> Analyzer | None:
|
|
173
|
+
"""Get an analyzer by name."""
|
|
174
|
+
if not self._loaded:
|
|
175
|
+
self.discover()
|
|
176
|
+
return self._analyzers.get(name)
|
|
177
|
+
|
|
178
|
+
def list_analyzers(self, enabled_only: bool = True) -> list[Analyzer]:
|
|
179
|
+
"""List all registered analyzers."""
|
|
180
|
+
if not self._loaded:
|
|
181
|
+
self.discover()
|
|
182
|
+
|
|
183
|
+
analyzers = list(self._analyzers.values())
|
|
184
|
+
if enabled_only:
|
|
185
|
+
analyzers = [a for a in analyzers if a.enabled]
|
|
186
|
+
|
|
187
|
+
return sorted(analyzers, key=lambda a: (a.priority, a.name))
|
|
188
|
+
|
|
189
|
+
def run(self, name: str, context: AnalyzerContext) -> AnalyzerResult:
|
|
190
|
+
"""Run a specific analyzer by name."""
|
|
191
|
+
analyzer = self.get(name)
|
|
192
|
+
if not analyzer:
|
|
193
|
+
return AnalyzerResult(
|
|
194
|
+
errors=[f"Analyzer '{name}' not found"],
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
return self._run_with_timeout(analyzer, context)
|
|
198
|
+
|
|
199
|
+
def run_all(
|
|
200
|
+
self,
|
|
201
|
+
context: AnalyzerContext,
|
|
202
|
+
analyzers: list[str] | None = None,
|
|
203
|
+
) -> dict[str, AnalyzerResult]:
|
|
204
|
+
"""Run all (or specified) analyzers and return results.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
context: The analysis context
|
|
208
|
+
analyzers: Optional list of analyzer names to run (None = all)
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
Dict mapping analyzer name to its result
|
|
212
|
+
"""
|
|
213
|
+
if not self._loaded:
|
|
214
|
+
self.discover()
|
|
215
|
+
|
|
216
|
+
to_run = self.list_analyzers(enabled_only=True)
|
|
217
|
+
if analyzers:
|
|
218
|
+
to_run = [a for a in to_run if a.name in analyzers]
|
|
219
|
+
|
|
220
|
+
results: dict[str, AnalyzerResult] = {}
|
|
221
|
+
for analyzer in to_run:
|
|
222
|
+
try:
|
|
223
|
+
results[analyzer.name] = self._run_with_timeout(analyzer, context)
|
|
224
|
+
except Exception as e:
|
|
225
|
+
results[analyzer.name] = AnalyzerResult(
|
|
226
|
+
errors=[f"{type(e).__name__}: {e}"],
|
|
227
|
+
)
|
|
228
|
+
_log.debug("Analyzer %s failed: %s", analyzer.name, e)
|
|
229
|
+
|
|
230
|
+
return results
|
|
231
|
+
|
|
232
|
+
def _run_with_timeout(
|
|
233
|
+
self,
|
|
234
|
+
analyzer: Analyzer,
|
|
235
|
+
context: AnalyzerContext,
|
|
236
|
+
) -> AnalyzerResult:
|
|
237
|
+
"""Run an analyzer with timeout enforcement."""
|
|
238
|
+
import concurrent.futures
|
|
239
|
+
|
|
240
|
+
t0 = time.monotonic()
|
|
241
|
+
|
|
242
|
+
try:
|
|
243
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
|
|
244
|
+
future = executor.submit(analyzer.analyze, context)
|
|
245
|
+
result = future.result(timeout=analyzer.timeout)
|
|
246
|
+
result.duration_ms = int((time.monotonic() - t0) * 1000)
|
|
247
|
+
|
|
248
|
+
# Set analyzer name on all findings for stable IDs
|
|
249
|
+
for finding in result.findings:
|
|
250
|
+
finding.extra["_analyzer"] = analyzer.name
|
|
251
|
+
|
|
252
|
+
return result
|
|
253
|
+
|
|
254
|
+
except concurrent.futures.TimeoutError:
|
|
255
|
+
return AnalyzerResult(
|
|
256
|
+
errors=[f"Analyzer '{analyzer.name}' timed out after {analyzer.timeout}s"],
|
|
257
|
+
duration_ms=int((time.monotonic() - t0) * 1000),
|
|
258
|
+
)
|
|
259
|
+
except Exception as e:
|
|
260
|
+
return AnalyzerResult(
|
|
261
|
+
errors=[f"{type(e).__name__}: {e}"],
|
|
262
|
+
duration_ms=int((time.monotonic() - t0) * 1000),
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
class AgentAnalyzerWrapper(Analyzer):
|
|
267
|
+
"""Wraps an existing BaseAgent as an Analyzer for backwards compatibility."""
|
|
268
|
+
|
|
269
|
+
def __init__(self, agent_cls: type) -> None:
|
|
270
|
+
self._agent_cls = agent_cls
|
|
271
|
+
self.name = getattr(agent_cls, "name", "unknown")
|
|
272
|
+
self.version = "0.0.0"
|
|
273
|
+
self.description = f"Legacy agent wrapper for {self.name}"
|
|
274
|
+
self.priority = 200 # Run after native analyzers
|
|
275
|
+
self.timeout = getattr(agent_cls, "timeout", 60)
|
|
276
|
+
|
|
277
|
+
def analyze(self, context: AnalyzerContext) -> AnalyzerResult:
|
|
278
|
+
"""Wrap agent execution as analyzer."""
|
|
279
|
+
from patchi.core.agents.base import AgentInput
|
|
280
|
+
|
|
281
|
+
# Convert AnalyzerContext to AgentInput
|
|
282
|
+
agent_input = AgentInput(
|
|
283
|
+
root=context.root,
|
|
284
|
+
scope=context.scope,
|
|
285
|
+
brain={},
|
|
286
|
+
config=context.config,
|
|
287
|
+
extra=context.extra,
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
# Run the agent
|
|
291
|
+
agent = self._agent_cls()
|
|
292
|
+
result = agent.run(agent_input)
|
|
293
|
+
|
|
294
|
+
# Convert AgentResult to AnalyzerResult
|
|
295
|
+
findings = []
|
|
296
|
+
for f in result.findings:
|
|
297
|
+
findings.append(Finding(
|
|
298
|
+
file=f.file,
|
|
299
|
+
line=f.line,
|
|
300
|
+
type=f.type,
|
|
301
|
+
severity=Severity(f.severity.value),
|
|
302
|
+
message=f.message,
|
|
303
|
+
detail=f.detail,
|
|
304
|
+
code_snippet=f.code_snippet,
|
|
305
|
+
suggestion=f.suggestion,
|
|
306
|
+
cwe=f.cwe,
|
|
307
|
+
extra={"_legacy_agent": True},
|
|
308
|
+
))
|
|
309
|
+
|
|
310
|
+
return AnalyzerResult(
|
|
311
|
+
findings=findings,
|
|
312
|
+
metrics=result.data,
|
|
313
|
+
errors=result.errors,
|
|
314
|
+
duration_ms=result.duration_ms,
|
|
315
|
+
files_analyzed=len(context.files),
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
# ── Singleton ─────────────────────────────────────────────────────────────────
|
|
320
|
+
|
|
321
|
+
_registry: PluginRegistry | None = None
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def get_registry() -> PluginRegistry:
|
|
325
|
+
"""Get or create the global plugin registry."""
|
|
326
|
+
global _registry
|
|
327
|
+
if _registry is None:
|
|
328
|
+
_registry = PluginRegistry()
|
|
329
|
+
return _registry
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def list_plugins() -> list[dict]:
|
|
333
|
+
"""List all registered plugins with their metadata."""
|
|
334
|
+
registry = get_registry()
|
|
335
|
+
return [a.describe() for a in registry.list_analyzers()]
|