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,69 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Quiet hours enforcement for Patchi notifications.
|
|
3
|
+
|
|
4
|
+
Quiet hours block non-urgent alerts during the configured window.
|
|
5
|
+
CRITICAL and HIGH severity always bypass quiet hours.
|
|
6
|
+
MEDIUM, LOW, INFO are suppressed during the window.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from datetime import datetime, time
|
|
12
|
+
|
|
13
|
+
# Severities that always fire regardless of quiet hours
|
|
14
|
+
_BYPASS_SEVERITIES: frozenset[str] = frozenset({"critical", "high"})
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _parse_time(value: str) -> time:
|
|
18
|
+
"""Parse HH:MM string to a time object. Raises ValueError on bad format."""
|
|
19
|
+
try:
|
|
20
|
+
h, m = value.split(":")
|
|
21
|
+
return time(int(h), int(m))
|
|
22
|
+
except (ValueError, AttributeError) as exc:
|
|
23
|
+
raise ValueError(f"Invalid time format '{value}' — expected HH:MM") from exc
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def is_quiet_now(quiet_config: dict) -> bool:
|
|
27
|
+
"""
|
|
28
|
+
Return True if the current moment falls inside the configured quiet window.
|
|
29
|
+
|
|
30
|
+
quiet_config shape (from config.json):
|
|
31
|
+
{
|
|
32
|
+
"enabled": bool,
|
|
33
|
+
"start": "22:00",
|
|
34
|
+
"end": "08:00",
|
|
35
|
+
"timezone": "UTC" # currently ignored — uses system local time
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
Handles overnight windows (e.g. 22:00 → 08:00 wraps past midnight).
|
|
39
|
+
"""
|
|
40
|
+
if not quiet_config.get("enabled", False):
|
|
41
|
+
return False
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
start = _parse_time(quiet_config.get("start", "22:00"))
|
|
45
|
+
end = _parse_time(quiet_config.get("end", "08:00"))
|
|
46
|
+
except ValueError:
|
|
47
|
+
# Misconfigured quiet hours — treat as off to avoid silent notification loss
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
now = datetime.now().time()
|
|
51
|
+
|
|
52
|
+
if start <= end:
|
|
53
|
+
# Same-day window: e.g. 09:00 → 17:00
|
|
54
|
+
return start <= now < end
|
|
55
|
+
else:
|
|
56
|
+
# Overnight window: e.g. 22:00 → 08:00
|
|
57
|
+
return now >= start or now < end
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def should_suppress(severity: str, quiet_config: dict) -> bool:
|
|
61
|
+
"""
|
|
62
|
+
Return True if this alert should be suppressed due to quiet hours.
|
|
63
|
+
|
|
64
|
+
CRITICAL and HIGH always fire.
|
|
65
|
+
MEDIUM/LOW/INFO are suppressed during the quiet window.
|
|
66
|
+
"""
|
|
67
|
+
if severity in _BYPASS_SEVERITIES:
|
|
68
|
+
return False
|
|
69
|
+
return is_quiet_now(quiet_config)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Plugin system for Patchi.
|
|
3
|
+
|
|
4
|
+
Provides a formal contract for analyzers, tools, and agents that can be
|
|
5
|
+
added without touching core code. New plugins are discovered automatically
|
|
6
|
+
from the plugins directory.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
# To add a new analyzer, create a file in patchi/core/plugins/analyzers/
|
|
10
|
+
# and define a class that inherits from Analyzer:
|
|
11
|
+
|
|
12
|
+
from patchi.core.plugins.analyzer import Analyzer, AnalyzerResult
|
|
13
|
+
|
|
14
|
+
class MyAnalyzer(Analyzer):
|
|
15
|
+
name = "my-analyzer"
|
|
16
|
+
version = "1.0.0"
|
|
17
|
+
description = "Does something useful"
|
|
18
|
+
|
|
19
|
+
def analyze(self, context) -> AnalyzerResult:
|
|
20
|
+
# Your analysis logic here
|
|
21
|
+
return AnalyzerResult(findings=[...])
|
|
22
|
+
|
|
23
|
+
# That's it! The plugin is automatically discovered and registered.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from patchi.core.plugins.analyzer import (
|
|
27
|
+
Analyzer,
|
|
28
|
+
AnalyzerContext,
|
|
29
|
+
AnalyzerResult,
|
|
30
|
+
Finding,
|
|
31
|
+
Severity,
|
|
32
|
+
)
|
|
33
|
+
from patchi.core.plugins.registry import (
|
|
34
|
+
PluginRegistry,
|
|
35
|
+
get_registry,
|
|
36
|
+
list_plugins,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
"Analyzer",
|
|
41
|
+
"AnalyzerContext",
|
|
42
|
+
"AnalyzerResult",
|
|
43
|
+
"Finding",
|
|
44
|
+
"Severity",
|
|
45
|
+
"PluginRegistry",
|
|
46
|
+
"get_registry",
|
|
47
|
+
"list_plugins",
|
|
48
|
+
]
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Formal Analyzer plugin interface for Patchi.
|
|
3
|
+
|
|
4
|
+
This module defines the contract that all analyzers must follow:
|
|
5
|
+
- Input: AnalyzerContext (AST, graph, project metadata)
|
|
6
|
+
- Output: AnalyzerResult (findings with stable IDs)
|
|
7
|
+
|
|
8
|
+
Stable Finding IDs:
|
|
9
|
+
Every finding has a deterministic ID based on:
|
|
10
|
+
sha256(f"{analyzer_name}:{file}:{line}:{type}:{message}")[:16]
|
|
11
|
+
|
|
12
|
+
This means:
|
|
13
|
+
1. The same issue always has the same ID (deterministic)
|
|
14
|
+
2. Different analyzers produce different IDs (namespaced)
|
|
15
|
+
3. Moving a line changes the ID (line-sensitive)
|
|
16
|
+
4. IDs are short (16 chars) for readability
|
|
17
|
+
|
|
18
|
+
Adding a new analyzer:
|
|
19
|
+
1. Create a file in patchi/core/plugins/analyzers/
|
|
20
|
+
2. Define a class that inherits from Analyzer
|
|
21
|
+
3. Implement analyze(context) -> AnalyzerResult
|
|
22
|
+
4. That's it! Auto-discovered and registered.
|
|
23
|
+
|
|
24
|
+
Example:
|
|
25
|
+
from patchi.core.plugins.analyzer import Analyzer, AnalyzerResult, Finding
|
|
26
|
+
|
|
27
|
+
class SecretScanner(Analyzer):
|
|
28
|
+
name = "secret-scanner"
|
|
29
|
+
version = "1.0.0"
|
|
30
|
+
description = "Scans for hardcoded secrets"
|
|
31
|
+
|
|
32
|
+
def analyze(self, context) -> AnalyzerResult:
|
|
33
|
+
findings = []
|
|
34
|
+
for file in context.files:
|
|
35
|
+
if "password" in file.content.lower():
|
|
36
|
+
findings.append(Finding(
|
|
37
|
+
file=file.path,
|
|
38
|
+
line=find_line(file.content, "password"),
|
|
39
|
+
type="hardcoded-secret",
|
|
40
|
+
severity=Severity.CRITICAL,
|
|
41
|
+
message="Possible hardcoded secret",
|
|
42
|
+
))
|
|
43
|
+
return AnalyzerResult(findings=findings)
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
from __future__ import annotations
|
|
47
|
+
|
|
48
|
+
import hashlib
|
|
49
|
+
from abc import ABC, abstractmethod
|
|
50
|
+
from dataclasses import dataclass, field
|
|
51
|
+
from enum import StrEnum
|
|
52
|
+
from pathlib import Path
|
|
53
|
+
from typing import Any
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class Severity(StrEnum):
|
|
57
|
+
"""Finding severity levels."""
|
|
58
|
+
CRITICAL = "critical"
|
|
59
|
+
HIGH = "high"
|
|
60
|
+
MEDIUM = "medium"
|
|
61
|
+
LOW = "low"
|
|
62
|
+
INFO = "info"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(frozen=True)
|
|
66
|
+
class FileNode:
|
|
67
|
+
"""A file in the project with its content and metadata."""
|
|
68
|
+
path: str # relative to project root
|
|
69
|
+
content: str = ""
|
|
70
|
+
language: str = ""
|
|
71
|
+
size: int = 0
|
|
72
|
+
modified_at: str = ""
|
|
73
|
+
metadata: dict = field(default_factory=dict)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass(frozen=True)
|
|
77
|
+
class ASTNode:
|
|
78
|
+
"""An AST node with position information."""
|
|
79
|
+
type: str # e.g. "function", "class", "import"
|
|
80
|
+
name: str = ""
|
|
81
|
+
start_line: int = 0
|
|
82
|
+
end_line: int = 0
|
|
83
|
+
start_col: int = 0
|
|
84
|
+
end_col: int = 0
|
|
85
|
+
children: tuple[ASTNode, ...] = ()
|
|
86
|
+
metadata: dict = field(default_factory=dict)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass(frozen=True)
|
|
90
|
+
class GraphEdge:
|
|
91
|
+
"""An edge in the dependency/call graph."""
|
|
92
|
+
source: str # file or symbol
|
|
93
|
+
target: str # file or symbol
|
|
94
|
+
edge_type: str = "calls" # calls, imports, inherits, etc.
|
|
95
|
+
metadata: dict = field(default_factory=dict)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass
|
|
99
|
+
class AnalyzerContext:
|
|
100
|
+
"""Input context for an analyzer.
|
|
101
|
+
|
|
102
|
+
Contains everything an analyzer needs:
|
|
103
|
+
- Project root and files
|
|
104
|
+
- AST trees per file (if parsed)
|
|
105
|
+
- Dependency/call graph
|
|
106
|
+
- Project metadata (from brain)
|
|
107
|
+
- Configuration
|
|
108
|
+
"""
|
|
109
|
+
root: Path
|
|
110
|
+
files: list[FileNode] = field(default_factory=list)
|
|
111
|
+
asts: dict[str, list[ASTNode]] = field(default_factory=dict) # file -> AST nodes
|
|
112
|
+
graph_edges: list[GraphEdge] = field(default_factory=list)
|
|
113
|
+
project_name: str = ""
|
|
114
|
+
project_type: str = ""
|
|
115
|
+
tech_stack: list[str] = field(default_factory=list)
|
|
116
|
+
config: dict = field(default_factory=dict)
|
|
117
|
+
scope: list[str] = field(default_factory=list) # empty = all files
|
|
118
|
+
extra: dict = field(default_factory=dict) # analyzer-specific params
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@dataclass
|
|
122
|
+
class Finding:
|
|
123
|
+
"""A single finding from an analyzer.
|
|
124
|
+
|
|
125
|
+
Has a stable, deterministic ID based on the finding's properties.
|
|
126
|
+
"""
|
|
127
|
+
file: str
|
|
128
|
+
line: int
|
|
129
|
+
type: str
|
|
130
|
+
severity: Severity
|
|
131
|
+
message: str
|
|
132
|
+
detail: str = ""
|
|
133
|
+
code_snippet: str = ""
|
|
134
|
+
suggestion: str = ""
|
|
135
|
+
cwe: str = ""
|
|
136
|
+
fix_hint: str = "" # hint for fix agents
|
|
137
|
+
confidence: float = 1.0 # 0.0 to 1.0
|
|
138
|
+
extra: dict = field(default_factory=dict)
|
|
139
|
+
|
|
140
|
+
# Computed stable ID
|
|
141
|
+
_id: str = field(default="", repr=False)
|
|
142
|
+
|
|
143
|
+
def __post_init__(self):
|
|
144
|
+
if not self._id:
|
|
145
|
+
self._id = self._compute_id()
|
|
146
|
+
|
|
147
|
+
def _compute_id(self) -> str:
|
|
148
|
+
"""Compute deterministic ID from finding properties."""
|
|
149
|
+
# Include analyzer name in the hash for namespacing
|
|
150
|
+
# This is set by AnalyzerResult.__post_init__
|
|
151
|
+
analyzer = self.extra.get("_analyzer", "unknown")
|
|
152
|
+
raw = f"{analyzer}:{self.file}:{self.line}:{self.type}:{self.message}"
|
|
153
|
+
return hashlib.sha256(raw.encode()).hexdigest()[:16]
|
|
154
|
+
|
|
155
|
+
@property
|
|
156
|
+
def id(self) -> str:
|
|
157
|
+
return self._id
|
|
158
|
+
|
|
159
|
+
def to_dict(self) -> dict:
|
|
160
|
+
return {
|
|
161
|
+
"id": self.id,
|
|
162
|
+
"file": self.file,
|
|
163
|
+
"line": self.line,
|
|
164
|
+
"type": self.type,
|
|
165
|
+
"severity": self.severity.value,
|
|
166
|
+
"message": self.message,
|
|
167
|
+
"detail": self.detail,
|
|
168
|
+
"code_snippet": self.code_snippet,
|
|
169
|
+
"suggestion": self.suggestion,
|
|
170
|
+
"cwe": self.cwe,
|
|
171
|
+
"fix_hint": self.fix_hint,
|
|
172
|
+
"confidence": self.confidence,
|
|
173
|
+
**self.extra,
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
@classmethod
|
|
177
|
+
def from_dict(cls, d: dict) -> Finding:
|
|
178
|
+
return cls(
|
|
179
|
+
file=d.get("file", ""),
|
|
180
|
+
line=d.get("line", 0),
|
|
181
|
+
type=d.get("type", ""),
|
|
182
|
+
severity=Severity(d.get("severity", "medium")),
|
|
183
|
+
message=d.get("message", ""),
|
|
184
|
+
detail=d.get("detail", ""),
|
|
185
|
+
code_snippet=d.get("code_snippet", ""),
|
|
186
|
+
suggestion=d.get("suggestion", ""),
|
|
187
|
+
cwe=d.get("cwe", ""),
|
|
188
|
+
fix_hint=d.get("fix_hint", ""),
|
|
189
|
+
confidence=d.get("confidence", 1.0),
|
|
190
|
+
extra={k: v for k, v in d.items() if k not in {
|
|
191
|
+
"id", "file", "line", "type", "severity", "message",
|
|
192
|
+
"detail", "code_snippet", "suggestion", "cwe", "fix_hint", "confidence",
|
|
193
|
+
}},
|
|
194
|
+
_id=d.get("id", ""),
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@dataclass
|
|
199
|
+
class AnalyzerResult:
|
|
200
|
+
"""Output from an analyzer.
|
|
201
|
+
|
|
202
|
+
Contains findings, metrics, and metadata about the analysis run.
|
|
203
|
+
"""
|
|
204
|
+
findings: list[Finding] = field(default_factory=list)
|
|
205
|
+
metrics: dict[str, Any] = field(default_factory=dict)
|
|
206
|
+
errors: list[str] = field(default_factory=list)
|
|
207
|
+
warnings: list[str] = field(default_factory=list)
|
|
208
|
+
duration_ms: int = 0
|
|
209
|
+
files_analyzed: int = 0
|
|
210
|
+
|
|
211
|
+
def __post_init__(self):
|
|
212
|
+
# Set analyzer name on all findings for stable ID computation
|
|
213
|
+
pass
|
|
214
|
+
|
|
215
|
+
def to_dict(self) -> dict:
|
|
216
|
+
return {
|
|
217
|
+
"findings": [f.to_dict() for f in self.findings],
|
|
218
|
+
"metrics": self.metrics,
|
|
219
|
+
"errors": self.errors,
|
|
220
|
+
"warnings": self.warnings,
|
|
221
|
+
"duration_ms": self.duration_ms,
|
|
222
|
+
"files_analyzed": self.files_analyzed,
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class Analyzer(ABC):
|
|
227
|
+
"""Base class for all analyzers.
|
|
228
|
+
|
|
229
|
+
Subclasses must implement:
|
|
230
|
+
- name: unique identifier for this analyzer
|
|
231
|
+
- version: semantic version
|
|
232
|
+
- description: human-readable description
|
|
233
|
+
- analyze(context): perform analysis and return results
|
|
234
|
+
|
|
235
|
+
Optional overrides:
|
|
236
|
+
- supported_languages: restrict to specific languages
|
|
237
|
+
- supported_file_patterns: restrict to specific file patterns
|
|
238
|
+
- priority: execution order (lower = first)
|
|
239
|
+
- timeout: maximum execution time in seconds
|
|
240
|
+
"""
|
|
241
|
+
|
|
242
|
+
# Required metadata
|
|
243
|
+
name: str = "unnamed-analyzer"
|
|
244
|
+
version: str = "0.0.0"
|
|
245
|
+
description: str = ""
|
|
246
|
+
|
|
247
|
+
# Optional configuration
|
|
248
|
+
supported_languages: list[str] = [] # empty = all languages
|
|
249
|
+
supported_file_patterns: list[str] = [] # empty = all files
|
|
250
|
+
priority: int = 100 # lower = runs first
|
|
251
|
+
timeout: int = 60 # seconds
|
|
252
|
+
enabled: bool = True
|
|
253
|
+
|
|
254
|
+
@abstractmethod
|
|
255
|
+
def analyze(self, context: AnalyzerContext) -> AnalyzerResult:
|
|
256
|
+
"""Perform analysis and return results.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
context: The analysis context containing files, ASTs, graph, etc.
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
AnalyzerResult with findings and metadata.
|
|
263
|
+
"""
|
|
264
|
+
...
|
|
265
|
+
|
|
266
|
+
def should_analyze(self, file: FileNode) -> bool:
|
|
267
|
+
"""Check if this analyzer should process the given file.
|
|
268
|
+
|
|
269
|
+
Override to implement custom filtering logic.
|
|
270
|
+
"""
|
|
271
|
+
if self.supported_languages and file.language not in self.supported_languages:
|
|
272
|
+
return False
|
|
273
|
+
if self.supported_file_patterns:
|
|
274
|
+
import fnmatch
|
|
275
|
+
if not any(fnmatch.fnmatch(file.path, p) for p in self.supported_file_patterns):
|
|
276
|
+
return False
|
|
277
|
+
return True
|
|
278
|
+
|
|
279
|
+
def describe(self) -> dict:
|
|
280
|
+
"""Return metadata for plugin discovery."""
|
|
281
|
+
return {
|
|
282
|
+
"name": self.name,
|
|
283
|
+
"version": self.version,
|
|
284
|
+
"description": self.description,
|
|
285
|
+
"supported_languages": self.supported_languages,
|
|
286
|
+
"supported_file_patterns": self.supported_file_patterns,
|
|
287
|
+
"priority": self.priority,
|
|
288
|
+
"timeout": self.timeout,
|
|
289
|
+
"enabled": self.enabled,
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
# ── Helper for creating findings ──────────────────────────────────────────────
|
|
294
|
+
|
|
295
|
+
def make_finding(
|
|
296
|
+
file: str,
|
|
297
|
+
line: int,
|
|
298
|
+
type: str,
|
|
299
|
+
severity: Severity,
|
|
300
|
+
message: str,
|
|
301
|
+
detail: str = "",
|
|
302
|
+
code_snippet: str = "",
|
|
303
|
+
suggestion: str = "",
|
|
304
|
+
cwe: str = "",
|
|
305
|
+
fix_hint: str = "",
|
|
306
|
+
confidence: float = 1.0,
|
|
307
|
+
**extra,
|
|
308
|
+
) -> Finding:
|
|
309
|
+
"""Create a Finding with the given properties.
|
|
310
|
+
|
|
311
|
+
This is a convenience function for creating findings in analyzers.
|
|
312
|
+
"""
|
|
313
|
+
return Finding(
|
|
314
|
+
file=file,
|
|
315
|
+
line=line,
|
|
316
|
+
type=type,
|
|
317
|
+
severity=severity,
|
|
318
|
+
message=message,
|
|
319
|
+
detail=detail,
|
|
320
|
+
code_snippet=code_snippet,
|
|
321
|
+
suggestion=suggestion,
|
|
322
|
+
cwe=cwe,
|
|
323
|
+
fix_hint=fix_hint,
|
|
324
|
+
confidence=confidence,
|
|
325
|
+
extra=extra,
|
|
326
|
+
)
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Dead Code Analyzer — detects unused imports, variables, and functions.
|
|
3
|
+
|
|
4
|
+
This is a built-in analyzer that scans for:
|
|
5
|
+
- Unused imports
|
|
6
|
+
- Unused variables
|
|
7
|
+
- Unused functions
|
|
8
|
+
- Unreachable code
|
|
9
|
+
|
|
10
|
+
Example usage:
|
|
11
|
+
from patchi.core.plugins import get_registry, AnalyzerContext
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
registry = get_registry()
|
|
15
|
+
context = AnalyzerContext(root=Path("."))
|
|
16
|
+
result = registry.run("dead-code-analyzer", context)
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import ast
|
|
22
|
+
|
|
23
|
+
from patchi.core.plugins.analyzer import (
|
|
24
|
+
Analyzer,
|
|
25
|
+
AnalyzerContext,
|
|
26
|
+
AnalyzerResult,
|
|
27
|
+
FileNode,
|
|
28
|
+
Finding,
|
|
29
|
+
Severity,
|
|
30
|
+
make_finding,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class DeadCodeAnalyzer(Analyzer):
|
|
35
|
+
"""Detects unused imports, variables, and functions."""
|
|
36
|
+
|
|
37
|
+
name = "dead-code-analyzer"
|
|
38
|
+
version = "1.0.0"
|
|
39
|
+
description = "Detects unused imports, variables, and functions"
|
|
40
|
+
priority = 50 # Run after secrets, before TODOs
|
|
41
|
+
supported_languages = ["python"] # Python only for now
|
|
42
|
+
|
|
43
|
+
def analyze(self, context: AnalyzerContext) -> AnalyzerResult:
|
|
44
|
+
findings: list[Finding] = []
|
|
45
|
+
files_scanned = 0
|
|
46
|
+
|
|
47
|
+
for file in context.files:
|
|
48
|
+
if not self.should_analyze(file):
|
|
49
|
+
continue
|
|
50
|
+
|
|
51
|
+
if not file.path.endswith(".py"):
|
|
52
|
+
continue
|
|
53
|
+
|
|
54
|
+
files_scanned += 1
|
|
55
|
+
file_findings = self._scan_python_file(file)
|
|
56
|
+
findings.extend(file_findings)
|
|
57
|
+
|
|
58
|
+
return AnalyzerResult(
|
|
59
|
+
findings=findings,
|
|
60
|
+
files_analyzed=files_scanned,
|
|
61
|
+
metrics={
|
|
62
|
+
"files_scanned": files_scanned,
|
|
63
|
+
"findings_count": len(findings),
|
|
64
|
+
},
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
def _scan_python_file(self, file: FileNode) -> list[Finding]:
|
|
68
|
+
"""Scan a Python file for dead code."""
|
|
69
|
+
findings: list[Finding] = []
|
|
70
|
+
|
|
71
|
+
if not file.content:
|
|
72
|
+
return findings
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
tree = ast.parse(file.content)
|
|
76
|
+
except SyntaxError:
|
|
77
|
+
return findings
|
|
78
|
+
|
|
79
|
+
# Collect all imports
|
|
80
|
+
imports = []
|
|
81
|
+
for node in ast.walk(tree):
|
|
82
|
+
if isinstance(node, ast.Import):
|
|
83
|
+
for alias in node.names:
|
|
84
|
+
name = alias.asname or alias.name
|
|
85
|
+
imports.append((name, node.lineno))
|
|
86
|
+
elif isinstance(node, ast.ImportFrom):
|
|
87
|
+
for alias in node.names:
|
|
88
|
+
name = alias.asname or alias.name
|
|
89
|
+
imports.append((name, node.lineno))
|
|
90
|
+
|
|
91
|
+
# Collect all names used in the file
|
|
92
|
+
used_names = set()
|
|
93
|
+
for node in ast.walk(tree):
|
|
94
|
+
if isinstance(node, ast.Name):
|
|
95
|
+
used_names.add(node.id)
|
|
96
|
+
elif isinstance(node, ast.Attribute):
|
|
97
|
+
# Handle attribute access like os.path
|
|
98
|
+
if isinstance(node.value, ast.Name):
|
|
99
|
+
used_names.add(node.value.id)
|
|
100
|
+
|
|
101
|
+
# Check for unused imports
|
|
102
|
+
for name, lineno in imports:
|
|
103
|
+
# Skip wildcard imports
|
|
104
|
+
if name == "*":
|
|
105
|
+
continue
|
|
106
|
+
|
|
107
|
+
# Check if name is used (excluding the import itself)
|
|
108
|
+
if name not in used_names:
|
|
109
|
+
findings.append(make_finding(
|
|
110
|
+
file=file.path,
|
|
111
|
+
line=lineno,
|
|
112
|
+
type="unused-import",
|
|
113
|
+
severity=Severity.LOW,
|
|
114
|
+
message=f"Unused import: {name}",
|
|
115
|
+
detail=f"Import '{name}' is defined but never used in this file",
|
|
116
|
+
suggestion=f"Remove unused import '{name}'",
|
|
117
|
+
confidence=0.9,
|
|
118
|
+
))
|
|
119
|
+
|
|
120
|
+
return findings
|