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,272 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Policy Engine — YAML/JSON policy enforcement for security compliance.
|
|
3
|
+
|
|
4
|
+
Loads policy definitions from .patchi/policies/ and enforces them
|
|
5
|
+
against code and findings. Supports SOC2, HIPAA, PCI-DSS, CIS packs.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
import re
|
|
13
|
+
|
|
14
|
+
import yaml
|
|
15
|
+
|
|
16
|
+
from patchi.core.agents.base import (
|
|
17
|
+
AgentGroup,
|
|
18
|
+
AgentInput,
|
|
19
|
+
AgentResult,
|
|
20
|
+
BaseAgent,
|
|
21
|
+
Finding,
|
|
22
|
+
Severity,
|
|
23
|
+
register,
|
|
24
|
+
safe_rglob,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# ── Built-in policy packs ─────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
_BUILTIN_POLICIES: dict[str, list[dict]] = {
|
|
30
|
+
"soc2": [
|
|
31
|
+
{
|
|
32
|
+
"id": "SOC2-CC6.1",
|
|
33
|
+
"name": "Access Control",
|
|
34
|
+
"check": "no_default_credentials",
|
|
35
|
+
"severity": "critical",
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"id": "SOC2-CC6.6",
|
|
39
|
+
"name": "Encryption in Transit",
|
|
40
|
+
"check": "no_plaintext_http",
|
|
41
|
+
"severity": "high",
|
|
42
|
+
},
|
|
43
|
+
{"id": "SOC2-CC7.1", "name": "Logging", "check": "has_logging", "severity": "medium"},
|
|
44
|
+
{
|
|
45
|
+
"id": "SOC2-CC8.1",
|
|
46
|
+
"name": "Change Management",
|
|
47
|
+
"check": "has_audit_trail",
|
|
48
|
+
"severity": "medium",
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
"hipaa": [
|
|
52
|
+
{
|
|
53
|
+
"id": "HIPAA-164.312",
|
|
54
|
+
"name": "Access Control",
|
|
55
|
+
"check": "no_default_credentials",
|
|
56
|
+
"severity": "critical",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"id": "HIPAA-164.312",
|
|
60
|
+
"name": "Encryption",
|
|
61
|
+
"check": "no_weak_crypto",
|
|
62
|
+
"severity": "critical",
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"id": "HIPAA-164.312",
|
|
66
|
+
"name": "Audit Controls",
|
|
67
|
+
"check": "has_logging",
|
|
68
|
+
"severity": "high",
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"id": "HIPAA-164.312",
|
|
72
|
+
"name": "Data Integrity",
|
|
73
|
+
"check": "no_hardcoded_secrets",
|
|
74
|
+
"severity": "critical",
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
"pci_dss": [
|
|
78
|
+
{
|
|
79
|
+
"id": "PCI-DSS-2.1",
|
|
80
|
+
"name": "No Default Credentials",
|
|
81
|
+
"check": "no_default_credentials",
|
|
82
|
+
"severity": "critical",
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"id": "PCI-DSS-3.4",
|
|
86
|
+
"name": "Encryption",
|
|
87
|
+
"check": "no_weak_crypto",
|
|
88
|
+
"severity": "critical",
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"id": "PCI-DSS-6.5",
|
|
92
|
+
"name": "Secure Development",
|
|
93
|
+
"check": "no_sql_injection",
|
|
94
|
+
"severity": "critical",
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"id": "PCI-DSS-10.2",
|
|
98
|
+
"name": "Audit Trail",
|
|
99
|
+
"check": "has_audit_trail",
|
|
100
|
+
"severity": "high",
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
"cis": [
|
|
104
|
+
{
|
|
105
|
+
"id": "CIS-1.1",
|
|
106
|
+
"name": "File Permissions",
|
|
107
|
+
"check": "no_world_readable",
|
|
108
|
+
"severity": "high",
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"id": "CIS-2.1",
|
|
112
|
+
"name": "Software Updates",
|
|
113
|
+
"check": "no_outdated_deps",
|
|
114
|
+
"severity": "medium",
|
|
115
|
+
},
|
|
116
|
+
{"id": "CIS-4.1", "name": "Firewall", "check": "no_open_ports", "severity": "high"},
|
|
117
|
+
{
|
|
118
|
+
"id": "CIS-5.1",
|
|
119
|
+
"name": "Password Policy",
|
|
120
|
+
"check": "strong_passwords",
|
|
121
|
+
"severity": "medium",
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
_log = logging.getLogger("patchi.security.policy_engine")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _check_policy(check_type: str, content: str, findings: list[Finding]) -> list[str]:
|
|
131
|
+
"""Run a policy check against file content. Returns list of violation messages."""
|
|
132
|
+
violations = []
|
|
133
|
+
|
|
134
|
+
if check_type == "no_default_credentials":
|
|
135
|
+
if re.search(
|
|
136
|
+
r'(?:admin|root|password)\s*[:=]\s*["\'](?:admin|root|password|changeme)["\']',
|
|
137
|
+
content,
|
|
138
|
+
re.I,
|
|
139
|
+
):
|
|
140
|
+
violations.append("Default credentials found")
|
|
141
|
+
|
|
142
|
+
elif check_type == "no_plaintext_http":
|
|
143
|
+
if re.search(r"http://(?!localhost|127\.0\.0\.1)", content):
|
|
144
|
+
violations.append("Plaintext HTTP URL found")
|
|
145
|
+
|
|
146
|
+
elif check_type == "has_logging":
|
|
147
|
+
if not re.search(r"(?:log|logging|logger|audit)", content, re.I):
|
|
148
|
+
violations.append("No logging implementation found")
|
|
149
|
+
|
|
150
|
+
elif check_type == "has_audit_trail":
|
|
151
|
+
if not re.search(r"(?:audit|log|track|record)", content, re.I):
|
|
152
|
+
violations.append("No audit trail found")
|
|
153
|
+
|
|
154
|
+
elif check_type == "no_weak_crypto":
|
|
155
|
+
if re.search(r"\b(?:MD5|SHA1|DES|RC4)\b", content, re.I):
|
|
156
|
+
violations.append("Weak cryptographic algorithm detected")
|
|
157
|
+
|
|
158
|
+
elif check_type == "no_hardcoded_secrets":
|
|
159
|
+
if re.search(
|
|
160
|
+
r'(?:api[_-]?key|secret|password)\s*[:=]\s*["\'][^"\']{8,}["\']', content, re.I
|
|
161
|
+
):
|
|
162
|
+
violations.append("Hardcoded secret detected")
|
|
163
|
+
|
|
164
|
+
elif check_type == "no_sql_injection":
|
|
165
|
+
if re.search(r"(?:execute|query)\s*\([^)]*\+", content):
|
|
166
|
+
violations.append("Potential SQL injection — string concatenation in query")
|
|
167
|
+
|
|
168
|
+
elif check_type == "no_world_readable":
|
|
169
|
+
if re.search(r"chmod\s+777|chmod\s+\+r", content):
|
|
170
|
+
violations.append("World-readable file permissions")
|
|
171
|
+
|
|
172
|
+
elif check_type == "no_outdated_deps":
|
|
173
|
+
pass # Handled by dependency scanner
|
|
174
|
+
|
|
175
|
+
elif check_type == "no_open_ports":
|
|
176
|
+
if re.search(r"EXPOSE\s+(?:22|3389)\b", content, re.I):
|
|
177
|
+
violations.append("Sensitive port exposed")
|
|
178
|
+
|
|
179
|
+
elif check_type == "strong_passwords":
|
|
180
|
+
if re.search(r'(?:password|passwd)\s*[:=]\s*["\'][^"\']{1,7}["\']', content, re.I):
|
|
181
|
+
violations.append("Weak password (less than 8 characters)")
|
|
182
|
+
|
|
183
|
+
return violations
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@register
|
|
187
|
+
class PolicyEngineAgent(BaseAgent):
|
|
188
|
+
"""Enforces YAML/JSON security policies against code."""
|
|
189
|
+
|
|
190
|
+
name = "PolicyEngineAgent"
|
|
191
|
+
group = AgentGroup.SECURITY
|
|
192
|
+
timeout = 60
|
|
193
|
+
|
|
194
|
+
def _run(self, inp: AgentInput, result: AgentResult) -> None:
|
|
195
|
+
# Load policies from .patchi/policies/ or use built-in packs
|
|
196
|
+
policy_packs = self._load_policies(inp)
|
|
197
|
+
|
|
198
|
+
# Flatten all rules once — avoids re-iterating packs per file
|
|
199
|
+
all_rules: list[tuple[str, dict]] = [] # (pack_name, rule)
|
|
200
|
+
for pack_name, rules in policy_packs.items():
|
|
201
|
+
for rule in rules:
|
|
202
|
+
all_rules.append((pack_name, rule))
|
|
203
|
+
|
|
204
|
+
if not all_rules:
|
|
205
|
+
return
|
|
206
|
+
|
|
207
|
+
# Scan all source files — read each file ONCE, apply all rules (O(files × rules))
|
|
208
|
+
source_patterns = [
|
|
209
|
+
"*.py",
|
|
210
|
+
"*.js",
|
|
211
|
+
"*.ts",
|
|
212
|
+
"*.jsx",
|
|
213
|
+
"*.tsx",
|
|
214
|
+
"*.go",
|
|
215
|
+
"*.java",
|
|
216
|
+
"*.rb",
|
|
217
|
+
"*.php",
|
|
218
|
+
]
|
|
219
|
+
scanned = 0
|
|
220
|
+
|
|
221
|
+
for pattern in source_patterns:
|
|
222
|
+
for fpath in safe_rglob(inp.root, pattern):
|
|
223
|
+
if not fpath.is_file():
|
|
224
|
+
continue
|
|
225
|
+
rel = fpath.relative_to(inp.root).as_posix()
|
|
226
|
+
try:
|
|
227
|
+
content = fpath.read_text(encoding="utf-8", errors="ignore")
|
|
228
|
+
except OSError:
|
|
229
|
+
continue
|
|
230
|
+
scanned += 1
|
|
231
|
+
|
|
232
|
+
# Apply all rules in one pass over this file's content
|
|
233
|
+
for pack_name, rule in all_rules:
|
|
234
|
+
violations = _check_policy(rule["check"], content, [])
|
|
235
|
+
for v in violations:
|
|
236
|
+
result.add_finding(
|
|
237
|
+
Finding(
|
|
238
|
+
agent=self.name,
|
|
239
|
+
type="policy_violation",
|
|
240
|
+
severity=Severity(rule["severity"]),
|
|
241
|
+
file=rel,
|
|
242
|
+
message=f"[{rule['id']}] {rule['name']}: {v}",
|
|
243
|
+
cwe=rule["id"],
|
|
244
|
+
extra={"policy_pack": pack_name},
|
|
245
|
+
)
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
result.files_scanned = scanned
|
|
249
|
+
|
|
250
|
+
def _load_policies(self, inp: AgentInput) -> dict[str, list[dict]]:
|
|
251
|
+
"""Load policies from .patchi/policies/ directory, falling back to built-in."""
|
|
252
|
+
policies_dir = inp.root / ".patchi" / "policies"
|
|
253
|
+
custom_policies: dict[str, list[dict]] = {}
|
|
254
|
+
|
|
255
|
+
if policies_dir.is_dir():
|
|
256
|
+
for pf in policies_dir.glob("*.yaml"):
|
|
257
|
+
try:
|
|
258
|
+
data = yaml.safe_load(pf.read_text(encoding="utf-8"))
|
|
259
|
+
if isinstance(data, dict) and "rules" in data:
|
|
260
|
+
custom_policies[pf.stem] = data["rules"]
|
|
261
|
+
except Exception as e:
|
|
262
|
+
_log.warning("PolicyEngineAgent._load_policies failed: %s", e)
|
|
263
|
+
for pf in policies_dir.glob("*.json"):
|
|
264
|
+
try:
|
|
265
|
+
data = json.loads(pf.read_text(encoding="utf-8"))
|
|
266
|
+
if isinstance(data, dict) and "rules" in data:
|
|
267
|
+
custom_policies[pf.stem] = data["rules"]
|
|
268
|
+
except Exception as e:
|
|
269
|
+
_log.warning("PolicyEngineAgent._load_policies failed: %s", e)
|
|
270
|
+
|
|
271
|
+
# Use custom if found, otherwise built-in
|
|
272
|
+
return custom_policies if custom_policies else _BUILTIN_POLICIES
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cheap Pre-Checks (Layer 0) — instant gate before expensive scans.
|
|
3
|
+
|
|
4
|
+
Pure deterministic checks that run in <1 second:
|
|
5
|
+
- patchi_pattern_count: count pattern occurrences (banned APIs, refactor verification)
|
|
6
|
+
- patchi_lint_check: wrap ruff/eslint for fast style/bug detection
|
|
7
|
+
- patchi_diff_stat: before/after line counts on a change
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
import subprocess
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from patchi.core.agents.base import (
|
|
17
|
+
AgentGroup,
|
|
18
|
+
AgentInput,
|
|
19
|
+
AgentResult,
|
|
20
|
+
BaseAgent,
|
|
21
|
+
Finding,
|
|
22
|
+
Severity,
|
|
23
|
+
register,
|
|
24
|
+
safe_rglob,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# ── Banned API patterns (zero tolerance) ──────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
_BANNED_APIS = [
|
|
30
|
+
(re.compile(r"\beval\s*\("), "eval() — code injection risk"),
|
|
31
|
+
(re.compile(r"\bexec\s*\("), "exec() — code injection risk"),
|
|
32
|
+
(re.compile(r"\bpickle\.loads?\s*\("), "pickle deserialization — RCE risk"),
|
|
33
|
+
(re.compile(r"\bmarshal\.loads?\s*\("), "marshal deserialization — RCE risk"),
|
|
34
|
+
(re.compile(r"\b__import__\s*\("), "dynamic import — verify necessity"),
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def patchi_pattern_count(root: Path, pattern: str, path_filter: str = "*.py") -> int:
|
|
39
|
+
"""Count occurrences of a regex pattern across files. Returns total count."""
|
|
40
|
+
count = 0
|
|
41
|
+
for fpath in safe_rglob(root, path_filter):
|
|
42
|
+
if not fpath.is_file():
|
|
43
|
+
continue
|
|
44
|
+
try:
|
|
45
|
+
content = fpath.read_text(encoding="utf-8", errors="ignore")
|
|
46
|
+
except OSError:
|
|
47
|
+
continue
|
|
48
|
+
count += len(re.findall(pattern, content))
|
|
49
|
+
return count
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def patchi_lint_check(root: Path, language: str = "python") -> list[dict]:
|
|
53
|
+
"""Run ruff (Python) or eslint (JS/TS) and return parsed violations."""
|
|
54
|
+
violations = []
|
|
55
|
+
|
|
56
|
+
if language == "python":
|
|
57
|
+
try:
|
|
58
|
+
proc = subprocess.run(
|
|
59
|
+
["ruff", "check", "--output-format=json", str(root)],
|
|
60
|
+
capture_output=True,
|
|
61
|
+
text=True,
|
|
62
|
+
timeout=30,
|
|
63
|
+
cwd=str(root),
|
|
64
|
+
)
|
|
65
|
+
import json
|
|
66
|
+
|
|
67
|
+
data = json.loads(proc.stdout) if proc.stdout.strip() else []
|
|
68
|
+
for item in data:
|
|
69
|
+
violations.append(
|
|
70
|
+
{
|
|
71
|
+
"file": item.get("filename", ""),
|
|
72
|
+
"line": item.get("location", {}).get("row", 0),
|
|
73
|
+
"code": item.get("code", ""),
|
|
74
|
+
"message": item.get("message", ""),
|
|
75
|
+
}
|
|
76
|
+
)
|
|
77
|
+
except FileNotFoundError:
|
|
78
|
+
import logging
|
|
79
|
+
|
|
80
|
+
logging.getLogger("patchi.prechecks").warning(
|
|
81
|
+
"ruff not installed — skipping Python lint check"
|
|
82
|
+
)
|
|
83
|
+
except (subprocess.TimeoutExpired, Exception):
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
elif language in ("javascript", "typescript"):
|
|
87
|
+
try:
|
|
88
|
+
proc = subprocess.run(
|
|
89
|
+
["npx", "eslint", "--format=json", str(root)],
|
|
90
|
+
capture_output=True,
|
|
91
|
+
text=True,
|
|
92
|
+
timeout=30,
|
|
93
|
+
cwd=str(root),
|
|
94
|
+
)
|
|
95
|
+
import json
|
|
96
|
+
|
|
97
|
+
data = json.loads(proc.stdout) if proc.stdout.strip() else []
|
|
98
|
+
for item in data:
|
|
99
|
+
for msg in item.get("messages", []):
|
|
100
|
+
violations.append(
|
|
101
|
+
{
|
|
102
|
+
"file": item.get("filePath", ""),
|
|
103
|
+
"line": msg.get("line", 0),
|
|
104
|
+
"code": msg.get("ruleId", ""),
|
|
105
|
+
"message": msg.get("message", ""),
|
|
106
|
+
}
|
|
107
|
+
)
|
|
108
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, Exception):
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
return violations
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def patchi_diff_stat(original: str, proposed: str) -> dict:
|
|
115
|
+
"""Compare before/after line counts. Returns add/delete/total stats."""
|
|
116
|
+
orig_lines = original.splitlines()
|
|
117
|
+
prop_lines = proposed.splitlines()
|
|
118
|
+
orig_set = set(orig_lines)
|
|
119
|
+
prop_set = set(prop_lines)
|
|
120
|
+
added = prop_set - orig_set
|
|
121
|
+
deleted = orig_set - prop_set
|
|
122
|
+
return {
|
|
123
|
+
"orig_lines": len(orig_lines),
|
|
124
|
+
"new_lines": len(prop_lines),
|
|
125
|
+
"lines_added": len(added),
|
|
126
|
+
"lines_deleted": len(deleted),
|
|
127
|
+
"net_change": len(prop_lines) - len(orig_lines),
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# ── Pre-checks Agent ──────────────────────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@register
|
|
135
|
+
class PreCheckAgent(BaseAgent):
|
|
136
|
+
"""Layer 0: instant deterministic pre-checks before expensive scans."""
|
|
137
|
+
|
|
138
|
+
name = "PreCheckAgent"
|
|
139
|
+
group = AgentGroup.SECURITY
|
|
140
|
+
timeout = 15
|
|
141
|
+
|
|
142
|
+
def _run(self, inp: AgentInput, result: AgentResult) -> None:
|
|
143
|
+
# 1. Banned API checks
|
|
144
|
+
for pattern, msg in _BANNED_APIS:
|
|
145
|
+
count = patchi_pattern_count(inp.root, pattern.pattern)
|
|
146
|
+
if count > 0:
|
|
147
|
+
result.add_finding(
|
|
148
|
+
Finding(
|
|
149
|
+
agent=self.name,
|
|
150
|
+
type="banned_api",
|
|
151
|
+
severity=Severity.CRITICAL,
|
|
152
|
+
file="",
|
|
153
|
+
message=f"{msg} — {count} occurrence(s) found",
|
|
154
|
+
cwe="CWE-94",
|
|
155
|
+
)
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# 2. Quick lint check
|
|
159
|
+
violations = patchi_lint_check(inp.root, "python")
|
|
160
|
+
for v in violations[:20]:
|
|
161
|
+
result.add_finding(
|
|
162
|
+
Finding(
|
|
163
|
+
agent=self.name,
|
|
164
|
+
type="lint_violation",
|
|
165
|
+
severity=Severity.LOW,
|
|
166
|
+
file=v["file"],
|
|
167
|
+
line=v["line"],
|
|
168
|
+
message=f"[{v['code']}] {v['message']}",
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
result.files_scanned = len(list(safe_rglob(inp.root, "*.py")))
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pre-Commit Hook Generator - produces a POSIX sh pre-commit hook.
|
|
3
|
+
|
|
4
|
+
The generated hook runs:
|
|
5
|
+
1. ruff check (fast lint gate)
|
|
6
|
+
2. p scan --changed --quiet (on-demand security scan of changed files)
|
|
7
|
+
|
|
8
|
+
Both steps are non-blocking warnings by default; pass --strict to fail
|
|
9
|
+
the commit on violations.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
_log = logging.getLogger("patchi.security.precommit_hook")
|
|
18
|
+
|
|
19
|
+
# -- Hook Script Template -----------------------------------------------------
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def generate_hook(
|
|
23
|
+
strict: bool = False,
|
|
24
|
+
with_tests: bool = False,
|
|
25
|
+
with_scan: bool = True,
|
|
26
|
+
auto_fix: bool = False,
|
|
27
|
+
) -> str:
|
|
28
|
+
"""Generate a pre-commit hook script.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
strict: If True, failures block the commit (exit 1).
|
|
32
|
+
with_tests: If True, include pytest gate after ruff.
|
|
33
|
+
with_scan: If True, include p scan --changed gate.
|
|
34
|
+
auto_fix: If True, run proactive auto-fix before lint.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
POSIX sh script text.
|
|
38
|
+
"""
|
|
39
|
+
lines = [
|
|
40
|
+
"#!/bin/sh",
|
|
41
|
+
"# Patchi pre-commit hook - generated by `p dev hook`",
|
|
42
|
+
"#",
|
|
43
|
+
"# Gates:",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
step_num = 1
|
|
47
|
+
if auto_fix:
|
|
48
|
+
lines.append(f"# {step_num}. proactive auto-fix (charter)")
|
|
49
|
+
step_num += 1
|
|
50
|
+
lines.append(f"# {step_num}. ruff check (lint)")
|
|
51
|
+
if with_tests:
|
|
52
|
+
step_num += 1
|
|
53
|
+
lines.append(f"# {step_num}. pytest (tests)")
|
|
54
|
+
if with_scan:
|
|
55
|
+
step_num += 1
|
|
56
|
+
lines.append(f"# {step_num}. p scan --changed (security)")
|
|
57
|
+
|
|
58
|
+
# NOTE: no `set -e` - each step checks $? manually so warnings
|
|
59
|
+
# are non-blocking when STRICT=0.
|
|
60
|
+
lines.append("STRICT=" + ("1" if strict else "0"))
|
|
61
|
+
lines.append("FAIL=0")
|
|
62
|
+
lines.append("AUTO_FIX=" + ("1" if auto_fix else "0"))
|
|
63
|
+
lines.append("")
|
|
64
|
+
lines.append('echo "=== Patchi pre-commit hook ==="')
|
|
65
|
+
lines.append("")
|
|
66
|
+
|
|
67
|
+
total = step_num
|
|
68
|
+
current_step = 0
|
|
69
|
+
|
|
70
|
+
# Step: Auto-fix (optional)
|
|
71
|
+
if auto_fix:
|
|
72
|
+
current_step += 1
|
|
73
|
+
lines.extend(
|
|
74
|
+
[
|
|
75
|
+
f"# -- Step {current_step}/{total}: Proactive auto-fix ---------------------",
|
|
76
|
+
f'echo " [{current_step}/{total}] proactive auto-fix..."',
|
|
77
|
+
"python -c \"import patchi.core.security.auto_fix_proactive as afp; \
|
|
78
|
+
import sys, pathlib; \
|
|
79
|
+
files = [pathlib.Path(f) for f in sys.argv[1:]]; \
|
|
80
|
+
result = afp.proactive_fix_path(pathlib.Path.cwd(), files[0], apply=True) if files else None; \
|
|
81
|
+
print(' auto-fixed' if result else ' no changes needed')\" $GIT_PARAMS 2>/dev/null",
|
|
82
|
+
"if [ $? -ne 0 ]; then",
|
|
83
|
+
' echo " [WARN] auto-fix skipped (non-blocking)"',
|
|
84
|
+
"else",
|
|
85
|
+
' echo " [OK] auto-fix complete"',
|
|
86
|
+
"fi",
|
|
87
|
+
"",
|
|
88
|
+
]
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Step: Ruff
|
|
92
|
+
current_step += 1
|
|
93
|
+
lines.extend(
|
|
94
|
+
[
|
|
95
|
+
f"# -- Step {current_step}/{total}: Ruff lint ----------------------------",
|
|
96
|
+
f'echo " [{current_step}/{total}] ruff check..."',
|
|
97
|
+
"ruff check patchi/ --select E,F,W --ignore E501 --quiet 2>/dev/null",
|
|
98
|
+
"if [ $? -ne 0 ]; then",
|
|
99
|
+
' echo " [WARN] ruff found issues (non-blocking)"',
|
|
100
|
+
' if [ "$STRICT" = "1" ]; then',
|
|
101
|
+
' echo " [BLOCKED] Commit blocked: fix ruff errors first"',
|
|
102
|
+
" exit 1",
|
|
103
|
+
" fi",
|
|
104
|
+
" FAIL=1",
|
|
105
|
+
"else",
|
|
106
|
+
' echo " [OK] ruff clean"',
|
|
107
|
+
"fi",
|
|
108
|
+
"",
|
|
109
|
+
]
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# Step 2: Tests (optional)
|
|
113
|
+
if with_tests:
|
|
114
|
+
lines.extend(
|
|
115
|
+
[
|
|
116
|
+
f"# -- Step {current_step}/{total}: Pytest -------------------------------",
|
|
117
|
+
f'echo " [{current_step}/{total}] pytest..."',
|
|
118
|
+
"PATCHI_OFFLINE=1 python -m pytest tests/ -x -q --timeout=30 --tb=no 2>/dev/null",
|
|
119
|
+
"if [ $? -ne 0 ]; then",
|
|
120
|
+
' echo " [WARN] pytest failed (non-blocking)"',
|
|
121
|
+
' if [ "$STRICT" = "1" ]; then',
|
|
122
|
+
]
|
|
123
|
+
)
|
|
124
|
+
if with_scan:
|
|
125
|
+
lines.append(' echo " [BLOCKED] Commit blocked: fix failing tests first"')
|
|
126
|
+
else:
|
|
127
|
+
lines.append(' echo " [BLOCKED] Commit blocked: fix failing tests first"')
|
|
128
|
+
lines.extend(
|
|
129
|
+
[
|
|
130
|
+
" exit 1",
|
|
131
|
+
" fi",
|
|
132
|
+
" FAIL=1",
|
|
133
|
+
"else",
|
|
134
|
+
' echo " [OK] tests pass"',
|
|
135
|
+
"fi",
|
|
136
|
+
"",
|
|
137
|
+
]
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# Step: Scan --changed (optional)
|
|
141
|
+
if with_scan:
|
|
142
|
+
current_step += 1
|
|
143
|
+
lines.extend(
|
|
144
|
+
[
|
|
145
|
+
f"# -- Step {current_step}/{total}: On-demand security scan ----------------",
|
|
146
|
+
f'echo " [{current_step}/{total}] p scan --changed..."',
|
|
147
|
+
"python -m patchi.cli.main scan --changed --quiet &",
|
|
148
|
+
"SCAN_PID=$!",
|
|
149
|
+
"for i in 1 2 3 4 5 6; do sleep 10; kill -0 $SCAN_PID 2>/dev/null || break; done",
|
|
150
|
+
"kill $SCAN_PID 2>/dev/null && echo ' (scan timed out)' || true",
|
|
151
|
+
"",
|
|
152
|
+
]
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# Step: Fast SAST gate on staged files (Bandit + Semgrep, synchronous)
|
|
156
|
+
if with_scan:
|
|
157
|
+
current_step += 1
|
|
158
|
+
lines.extend(
|
|
159
|
+
[
|
|
160
|
+
f"# -- Step {current_step}/{total}: Fast SAST gate (Bandit+Semgrep) ------",
|
|
161
|
+
f'echo " [{current_step}/{total}] fast SAST gate on staged files..."',
|
|
162
|
+
'STAGED=$(git diff --cached --name-only | grep -E "\\.py$" || true)',
|
|
163
|
+
'if [ -n "$STAGED" ]; then',
|
|
164
|
+
" python -m patchi.core.security.sast_gate $STAGED",
|
|
165
|
+
" SAST_RC=$?",
|
|
166
|
+
' if [ "$SAST_RC" -ne 0 ]; then',
|
|
167
|
+
' echo " [WARN] fast SAST gate found issues (non-blocking)"',
|
|
168
|
+
' if [ "$STRICT" = "1" ]; then',
|
|
169
|
+
' echo " [BLOCKED] Commit blocked: resolve SAST findings first"',
|
|
170
|
+
" exit 1",
|
|
171
|
+
" fi",
|
|
172
|
+
" FAIL=1",
|
|
173
|
+
" else",
|
|
174
|
+
' echo " [OK] fast SAST gate clean"',
|
|
175
|
+
" fi",
|
|
176
|
+
"else",
|
|
177
|
+
' echo " [OK] no staged Python files"',
|
|
178
|
+
"fi",
|
|
179
|
+
"",
|
|
180
|
+
]
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
# Summary
|
|
184
|
+
lines.extend(
|
|
185
|
+
[
|
|
186
|
+
"# -- Summary --------------------------------------------------------",
|
|
187
|
+
'if [ "$FAIL" = "1" ]; then',
|
|
188
|
+
' echo "[WARN] Commit succeeded with warnings. Run "p dev check" for full gate."',
|
|
189
|
+
"else",
|
|
190
|
+
' echo "[OK] All gates passed."',
|
|
191
|
+
"fi",
|
|
192
|
+
"",
|
|
193
|
+
"exit 0",
|
|
194
|
+
]
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
return "\n".join(lines) + "\n"
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def install_hook(
|
|
201
|
+
root: Path,
|
|
202
|
+
strict: bool = False,
|
|
203
|
+
with_tests: bool = False,
|
|
204
|
+
with_scan: bool = True,
|
|
205
|
+
auto_fix: bool = False,
|
|
206
|
+
) -> Path:
|
|
207
|
+
"""Generate and install the pre-commit hook.
|
|
208
|
+
|
|
209
|
+
Returns the path to the installed hook.
|
|
210
|
+
"""
|
|
211
|
+
git_dir = root / ".git"
|
|
212
|
+
if not git_dir.exists():
|
|
213
|
+
raise FileNotFoundError(f"Not a git repository: {root}")
|
|
214
|
+
|
|
215
|
+
hook_path = git_dir / "hooks" / "pre-commit"
|
|
216
|
+
hook_path.parent.mkdir(parents=True, exist_ok=True)
|
|
217
|
+
|
|
218
|
+
script = generate_hook(
|
|
219
|
+
strict=strict, with_tests=with_tests, with_scan=with_scan, auto_fix=auto_fix
|
|
220
|
+
)
|
|
221
|
+
hook_path.write_text(script, encoding="utf-8")
|
|
222
|
+
|
|
223
|
+
try:
|
|
224
|
+
hook_path.chmod(0o755)
|
|
225
|
+
except OSError:
|
|
226
|
+
pass # Windows
|
|
227
|
+
|
|
228
|
+
_log.info("Installed pre-commit hook at %s", hook_path)
|
|
229
|
+
return hook_path
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def read_hook_status(root: Path) -> dict:
|
|
233
|
+
"""Check what the installed pre-commit hook does."""
|
|
234
|
+
hook_path = root / ".git" / "hooks" / "pre-commit"
|
|
235
|
+
if not hook_path.is_file():
|
|
236
|
+
return {"installed": False}
|
|
237
|
+
|
|
238
|
+
content = hook_path.read_text(encoding="utf-8", errors="replace")
|
|
239
|
+
return {
|
|
240
|
+
"installed": True,
|
|
241
|
+
"path": str(hook_path),
|
|
242
|
+
"has_ruff": "ruff check" in content,
|
|
243
|
+
"has_pytest": "pytest" in content,
|
|
244
|
+
"has_scan": "scan --changed" in content or "scan --quiet" in content,
|
|
245
|
+
"strict": "STRICT=1" in content,
|
|
246
|
+
"auto_fix": "AUTO_FIX=1" in content,
|
|
247
|
+
}
|