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,367 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Git Diff Activator — on-demand security domain activation from file changes.
|
|
3
|
+
|
|
4
|
+
When ``p scan --changed`` is used, this module:
|
|
5
|
+
1. Reads changed files from ``git diff`` (or ``git log``)
|
|
6
|
+
2. Maps each changed file to relevant security domains via path patterns,
|
|
7
|
+
file extensions, and content heuristics
|
|
8
|
+
3. Returns a scored list of domains to activate
|
|
9
|
+
|
|
10
|
+
This lets the scan run only the agents relevant to what actually changed,
|
|
11
|
+
cutting scan time from minutes to seconds for small diffs.
|
|
12
|
+
|
|
13
|
+
Usage::
|
|
14
|
+
|
|
15
|
+
from patchi.core.security.git_diff_activator import activate_from_diff
|
|
16
|
+
|
|
17
|
+
domains = activate_from_diff(root, commits=1)
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import json
|
|
23
|
+
import logging
|
|
24
|
+
import subprocess
|
|
25
|
+
import time
|
|
26
|
+
from dataclasses import dataclass, field
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
_log = logging.getLogger("patchi.security.git_diff_activator")
|
|
30
|
+
|
|
31
|
+
_CACHE_FILE = ".patchi/diff_activation_cache.json"
|
|
32
|
+
_CACHE_TTL = 300 # 5 minutes max age even if HEAD unchanged
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ── Path → Domain Mapping ────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
# Patterns: (glob-like substring match, domain, weight)
|
|
38
|
+
_PATH_DOMAIN_MAP: list[tuple[str, str, float]] = [
|
|
39
|
+
# Auth / session
|
|
40
|
+
("auth", "authentication", 0.8),
|
|
41
|
+
("login", "authentication", 0.8),
|
|
42
|
+
("session", "session-management", 0.7),
|
|
43
|
+
("jwt", "jwt-security", 0.9),
|
|
44
|
+
("token", "jwt-security", 0.6),
|
|
45
|
+
("oauth", "authentication", 0.8),
|
|
46
|
+
("permission", "authorization", 0.8),
|
|
47
|
+
("role", "authorization", 0.7),
|
|
48
|
+
("admin", "authorization", 0.7),
|
|
49
|
+
("guard", "authorization", 0.6),
|
|
50
|
+
("middleware", "authentication", 0.5),
|
|
51
|
+
# Injection / XSS
|
|
52
|
+
("query", "injection-sql", 0.6),
|
|
53
|
+
("sql", "injection-sql", 0.8),
|
|
54
|
+
("database", "injection-sql", 0.5),
|
|
55
|
+
("search", "xss-reflected", 0.4),
|
|
56
|
+
("template", "xss-stored", 0.4),
|
|
57
|
+
("render", "xss-dom", 0.4),
|
|
58
|
+
("markup", "xss-stored", 0.5),
|
|
59
|
+
("form", "csrf", 0.5),
|
|
60
|
+
# Secrets / crypto
|
|
61
|
+
("secret", "secrets-management", 0.9),
|
|
62
|
+
("credential", "secrets-management", 0.9),
|
|
63
|
+
("key", "secrets-management", 0.6),
|
|
64
|
+
("env", "secrets-management", 0.5),
|
|
65
|
+
("encrypt", "cryptography", 0.8),
|
|
66
|
+
("decrypt", "cryptography", 0.8),
|
|
67
|
+
("hash", "cryptography", 0.7),
|
|
68
|
+
("ssl", "tls-ssl", 0.8),
|
|
69
|
+
("tls", "tls-ssl", 0.8),
|
|
70
|
+
("cert", "tls-ssl", 0.7),
|
|
71
|
+
# Network / API
|
|
72
|
+
("api", "api-security", 0.5),
|
|
73
|
+
("route", "api-security", 0.4),
|
|
74
|
+
("endpoint", "api-security", 0.5),
|
|
75
|
+
("fetch", "ssrf", 0.4),
|
|
76
|
+
("request", "ssrf", 0.4),
|
|
77
|
+
("http", "ssrf", 0.3),
|
|
78
|
+
("cors", "cors", 0.9),
|
|
79
|
+
("header", "security-headers", 0.7),
|
|
80
|
+
("rate", "rate-limiting", 0.6),
|
|
81
|
+
("limit", "rate-limiting", 0.5),
|
|
82
|
+
# Config / infra
|
|
83
|
+
("config", "configuration-hardening", 0.5),
|
|
84
|
+
("setting", "configuration-hardening", 0.4),
|
|
85
|
+
("docker", "container-security", 0.8),
|
|
86
|
+
("kubernetes", "kubernetes-security", 0.9),
|
|
87
|
+
("k8s", "kubernetes-security", 0.9),
|
|
88
|
+
("helm", "kubernetes-security", 0.7),
|
|
89
|
+
("terraform", "cloud-security", 0.8),
|
|
90
|
+
("cloud", "cloud-security", 0.6),
|
|
91
|
+
("infra", "container-security", 0.5),
|
|
92
|
+
("iac", "container-security", 0.7),
|
|
93
|
+
# Dependencies
|
|
94
|
+
("requirements", "dependency-vulnerability", 0.7),
|
|
95
|
+
("package.json", "dependency-vulnerability", 0.7),
|
|
96
|
+
("Cargo.toml", "supply-chain", 0.7),
|
|
97
|
+
("go.mod", "supply-chain", 0.7),
|
|
98
|
+
("lock", "supply-chain", 0.5),
|
|
99
|
+
# Business logic / privacy
|
|
100
|
+
("payment", "business-logic", 0.8),
|
|
101
|
+
("order", "business-logic", 0.7),
|
|
102
|
+
("cart", "business-logic", 0.6),
|
|
103
|
+
("privacy", "privacy-gdpr", 0.9),
|
|
104
|
+
("gdpr", "privacy-gdpr", 0.9),
|
|
105
|
+
("pii", "privacy-gdpr", 0.8),
|
|
106
|
+
("data", "data-layer", 0.4),
|
|
107
|
+
("audit", "audit-logging", 0.7),
|
|
108
|
+
("log", "audit-logging", 0.4),
|
|
109
|
+
("history", "audit-logging", 0.5),
|
|
110
|
+
# File / upload
|
|
111
|
+
("upload", "file-handling", 0.8),
|
|
112
|
+
("file", "file-handling", 0.4),
|
|
113
|
+
("download", "file-handling", 0.6),
|
|
114
|
+
("path", "path-traversal", 0.5),
|
|
115
|
+
("static", "file-handling", 0.3),
|
|
116
|
+
]
|
|
117
|
+
|
|
118
|
+
# Extension → domain boost
|
|
119
|
+
_EXT_DOMAIN_MAP: list[tuple[str, str, float]] = [
|
|
120
|
+
(".yaml", "configuration-hardening", 0.4),
|
|
121
|
+
(".yml", "configuration-hardening", 0.4),
|
|
122
|
+
(".toml", "supply-chain", 0.4),
|
|
123
|
+
(".env", "secrets-management", 0.8),
|
|
124
|
+
(".tf", "cloud-security", 0.9),
|
|
125
|
+
(".hcl", "cloud-security", 0.7),
|
|
126
|
+
(".sql", "injection-sql", 0.8),
|
|
127
|
+
(".sh", "audit-logging", 0.3),
|
|
128
|
+
(".dockerfile", "container-security", 0.8),
|
|
129
|
+
(".go", "supply-chain", 0.3),
|
|
130
|
+
(".rs", "supply-chain", 0.3),
|
|
131
|
+
(".java", "supply-chain", 0.3),
|
|
132
|
+
(".rb", "supply-chain", 0.3),
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
# Security-critical file names that always activate
|
|
136
|
+
_ALWAYS_ACTIVE: list[tuple[str, str, float]] = [
|
|
137
|
+
("Dockerfile", "container-security", 0.9),
|
|
138
|
+
("docker-compose", "container-security", 0.9),
|
|
139
|
+
("Makefile", "audit-logging", 0.3),
|
|
140
|
+
(".github", "cicd-pipeline", 0.7),
|
|
141
|
+
("ci.yml", "cicd-pipeline", 0.8),
|
|
142
|
+
("cd.yml", "cicd-pipeline", 0.8),
|
|
143
|
+
]
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@dataclass
|
|
147
|
+
class DiffActivationResult:
|
|
148
|
+
"""Result of git-diff-based domain activation."""
|
|
149
|
+
|
|
150
|
+
changed_files: list[str] = field(default_factory=list)
|
|
151
|
+
activated_domains: dict[str, float] = field(default_factory=dict)
|
|
152
|
+
commits_analyzed: int = 0
|
|
153
|
+
error: str = ""
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _get_head_hash(root: Path) -> str:
|
|
157
|
+
"""Get the current HEAD commit hash."""
|
|
158
|
+
try:
|
|
159
|
+
result = subprocess.run(
|
|
160
|
+
["git", "rev-parse", "HEAD"],
|
|
161
|
+
capture_output=True,
|
|
162
|
+
text=True,
|
|
163
|
+
cwd=str(root),
|
|
164
|
+
timeout=5,
|
|
165
|
+
)
|
|
166
|
+
return result.stdout.strip() if result.returncode == 0 else ""
|
|
167
|
+
except Exception:
|
|
168
|
+
return ""
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _load_cache(root: Path) -> dict:
|
|
172
|
+
path = root / _CACHE_FILE
|
|
173
|
+
if not path.is_file():
|
|
174
|
+
return {}
|
|
175
|
+
try:
|
|
176
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
177
|
+
except Exception:
|
|
178
|
+
return {}
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _save_cache(root: Path, data: dict) -> None:
|
|
182
|
+
path = root / _CACHE_FILE
|
|
183
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
184
|
+
tmp = path.with_suffix(".json.tmp")
|
|
185
|
+
tmp.write_text(json.dumps(data, indent=2), encoding="utf-8")
|
|
186
|
+
tmp.replace(path)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def get_changed_files(root: Path, commits: int = 1) -> list[str]:
|
|
190
|
+
"""Get files changed in the last N commits via git diff.
|
|
191
|
+
|
|
192
|
+
Falls back to uncommitted changes if HEAD has no prior commits.
|
|
193
|
+
"""
|
|
194
|
+
try:
|
|
195
|
+
# Try diff against N commits back
|
|
196
|
+
result = subprocess.run(
|
|
197
|
+
["git", "diff", "--name-only", f"HEAD~{commits}", "HEAD"],
|
|
198
|
+
capture_output=True,
|
|
199
|
+
text=True,
|
|
200
|
+
cwd=str(root),
|
|
201
|
+
timeout=10,
|
|
202
|
+
)
|
|
203
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
204
|
+
return [f.strip() for f in result.stdout.strip().splitlines() if f.strip()]
|
|
205
|
+
|
|
206
|
+
# Fallback: uncommitted changes
|
|
207
|
+
result = subprocess.run(
|
|
208
|
+
["git", "diff", "--name-only", "HEAD"],
|
|
209
|
+
capture_output=True,
|
|
210
|
+
text=True,
|
|
211
|
+
cwd=str(root),
|
|
212
|
+
timeout=10,
|
|
213
|
+
)
|
|
214
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
215
|
+
return [f.strip() for f in result.stdout.strip().splitlines() if f.strip()]
|
|
216
|
+
|
|
217
|
+
# Fallback: staged changes
|
|
218
|
+
result = subprocess.run(
|
|
219
|
+
["git", "diff", "--cached", "--name-only"],
|
|
220
|
+
capture_output=True,
|
|
221
|
+
text=True,
|
|
222
|
+
cwd=str(root),
|
|
223
|
+
timeout=10,
|
|
224
|
+
)
|
|
225
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
226
|
+
return [f.strip() for f in result.stdout.strip().splitlines() if f.strip()]
|
|
227
|
+
|
|
228
|
+
return []
|
|
229
|
+
except Exception as exc:
|
|
230
|
+
_log.warning("git diff failed: %s", exc)
|
|
231
|
+
return []
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def map_file_to_domains(file_path: str) -> dict[str, float]:
|
|
235
|
+
"""Map a single file path to domain scores."""
|
|
236
|
+
scores: dict[str, float] = {}
|
|
237
|
+
fp_lower = file_path.lower()
|
|
238
|
+
|
|
239
|
+
# Check path patterns
|
|
240
|
+
for pattern, domain, weight in _PATH_DOMAIN_MAP:
|
|
241
|
+
if pattern in fp_lower:
|
|
242
|
+
scores[domain] = max(scores.get(domain, 0), weight)
|
|
243
|
+
|
|
244
|
+
# Check extension
|
|
245
|
+
for ext, domain, weight in _EXT_DOMAIN_MAP:
|
|
246
|
+
if fp_lower.endswith(ext):
|
|
247
|
+
scores[domain] = max(scores.get(domain, 0), weight)
|
|
248
|
+
|
|
249
|
+
# Check always-active names
|
|
250
|
+
for name, domain, weight in _ALWAYS_ACTIVE:
|
|
251
|
+
if name.lower() in fp_lower:
|
|
252
|
+
scores[domain] = max(scores.get(domain, 0), weight)
|
|
253
|
+
|
|
254
|
+
return scores
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def activate_from_diff(
|
|
258
|
+
root: Path,
|
|
259
|
+
commits: int = 1,
|
|
260
|
+
min_score: float = 0.4,
|
|
261
|
+
max_domains: int = 20,
|
|
262
|
+
use_cache: bool = True,
|
|
263
|
+
) -> DiffActivationResult:
|
|
264
|
+
"""Activate security domains based on git-diff changed files.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
root: Project root
|
|
268
|
+
commits: How many commits back to diff (1 = last commit only)
|
|
269
|
+
min_score: Minimum domain score to activate
|
|
270
|
+
max_domains: Maximum domains to activate
|
|
271
|
+
use_cache: Whether to use persisted cache (default True)
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
DiffActivationResult with activated domains and their scores
|
|
275
|
+
"""
|
|
276
|
+
# ── Cache lookup ──────────────────────────────────────────────────────
|
|
277
|
+
cache_key = f"{commits}:{min_score}:{max_domains}"
|
|
278
|
+
if use_cache:
|
|
279
|
+
head = _get_head_hash(root)
|
|
280
|
+
cache = _load_cache(root)
|
|
281
|
+
cached = cache.get(cache_key)
|
|
282
|
+
if (
|
|
283
|
+
cached
|
|
284
|
+
and cached.get("head") == head
|
|
285
|
+
and cached.get("uncommitted_count", 0) == 0
|
|
286
|
+
and (time.time() - cached.get("ts", 0)) < _CACHE_TTL
|
|
287
|
+
):
|
|
288
|
+
_log.debug("Using cached diff activation (head=%s)", head[:8])
|
|
289
|
+
return DiffActivationResult(
|
|
290
|
+
changed_files=cached["changed_files"],
|
|
291
|
+
activated_domains={k: float(v) for k, v in cached["domains"].items()},
|
|
292
|
+
commits_analyzed=commits,
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
changed = get_changed_files(root, commits)
|
|
296
|
+
if not changed:
|
|
297
|
+
return DiffActivationResult(error="No changed files found (not a git repo or no changes)")
|
|
298
|
+
|
|
299
|
+
# Aggregate domain scores across all changed files
|
|
300
|
+
all_scores: dict[str, float] = {}
|
|
301
|
+
for fp in changed:
|
|
302
|
+
file_scores = map_file_to_domains(fp)
|
|
303
|
+
for domain, score in file_scores.items():
|
|
304
|
+
all_scores[domain] = max(all_scores.get(domain, 0), score)
|
|
305
|
+
|
|
306
|
+
# Filter by minimum score
|
|
307
|
+
activated = {d: s for d, s in all_scores.items() if s >= min_score}
|
|
308
|
+
|
|
309
|
+
# Sort by score descending and limit
|
|
310
|
+
sorted_domains = sorted(activated.items(), key=lambda x: -x[1])
|
|
311
|
+
if len(sorted_domains) > max_domains:
|
|
312
|
+
sorted_domains = sorted_domains[:max_domains]
|
|
313
|
+
activated = dict(sorted_domains)
|
|
314
|
+
|
|
315
|
+
_log.info(
|
|
316
|
+
"Git-diff activation: %d changed files → %d domains (from %d total signals)",
|
|
317
|
+
len(changed),
|
|
318
|
+
len(activated),
|
|
319
|
+
len(all_scores),
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
result = DiffActivationResult(
|
|
323
|
+
changed_files=changed,
|
|
324
|
+
activated_domains=activated,
|
|
325
|
+
commits_analyzed=commits,
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
# ── Persist cache ─────────────────────────────────────────────────────
|
|
329
|
+
if use_cache:
|
|
330
|
+
head = _get_head_hash(root)
|
|
331
|
+
# Count uncommitted changes to invalidate cache on dirty tree
|
|
332
|
+
uncommitted = 0
|
|
333
|
+
try:
|
|
334
|
+
u = subprocess.run(
|
|
335
|
+
["git", "diff", "--name-only", "HEAD"],
|
|
336
|
+
capture_output=True,
|
|
337
|
+
text=True,
|
|
338
|
+
cwd=str(root),
|
|
339
|
+
timeout=5,
|
|
340
|
+
)
|
|
341
|
+
if u.returncode == 0:
|
|
342
|
+
uncommitted = len([line for line in u.stdout.strip().splitlines() if line.strip()])
|
|
343
|
+
except Exception as _exc:
|
|
344
|
+
_log.warning('activate_from_diff failed: %s', _exc)
|
|
345
|
+
cache = _load_cache(root)
|
|
346
|
+
cache[cache_key] = {
|
|
347
|
+
"head": head,
|
|
348
|
+
"ts": time.time(),
|
|
349
|
+
"changed_files": changed,
|
|
350
|
+
"domains": dict(activated.items()),
|
|
351
|
+
"uncommitted_count": uncommitted,
|
|
352
|
+
}
|
|
353
|
+
_save_cache(root, cache)
|
|
354
|
+
|
|
355
|
+
return result
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def read_changed_file_content(root: Path, file_path: str, max_lines: int = 100) -> str:
|
|
359
|
+
"""Read the content of a changed file for deeper analysis."""
|
|
360
|
+
full = root / file_path
|
|
361
|
+
if not full.is_file():
|
|
362
|
+
return ""
|
|
363
|
+
try:
|
|
364
|
+
lines = full.read_text(encoding="utf-8", errors="replace").splitlines()
|
|
365
|
+
return "\n".join(lines[:max_lines])
|
|
366
|
+
except Exception:
|
|
367
|
+
return ""
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Governance & Audit (Layer 3) — action logging and policy gates.
|
|
3
|
+
|
|
4
|
+
- patchi_action_log: SQLite audit trail of all Patchi actions
|
|
5
|
+
- patchi_policy_gate: YAML allow/deny policy check before destructive actions
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import sqlite3
|
|
12
|
+
import time
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from patchi.core.agents.base import (
|
|
16
|
+
AgentGroup,
|
|
17
|
+
AgentInput,
|
|
18
|
+
AgentResult,
|
|
19
|
+
BaseAgent,
|
|
20
|
+
Finding,
|
|
21
|
+
Severity,
|
|
22
|
+
register,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# ── SQLite Action Log ─────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
_DB_NAME = "patchi_actions.db"
|
|
28
|
+
|
|
29
|
+
_CREATE_TABLES = """
|
|
30
|
+
CREATE TABLE IF NOT EXISTS action_log (
|
|
31
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
32
|
+
timestamp TEXT NOT NULL,
|
|
33
|
+
action TEXT NOT NULL,
|
|
34
|
+
target TEXT NOT NULL,
|
|
35
|
+
agent TEXT,
|
|
36
|
+
detail TEXT,
|
|
37
|
+
status TEXT DEFAULT 'ok'
|
|
38
|
+
);
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
_log = logging.getLogger("patchi.security.governance")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _get_db(root: Path) -> sqlite3.Connection:
|
|
46
|
+
db_path = root / ".patchi" / _DB_NAME
|
|
47
|
+
db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
48
|
+
conn = sqlite3.connect(str(db_path), timeout=10.0)
|
|
49
|
+
# WAL + busy_timeout make concurrent writers (parallel agents calling
|
|
50
|
+
# patchi_action_log) queue instead of failing with "database or disk is full".
|
|
51
|
+
conn.execute("PRAGMA journal_mode=WAL")
|
|
52
|
+
conn.execute("PRAGMA busy_timeout=10000")
|
|
53
|
+
conn.execute(_CREATE_TABLES)
|
|
54
|
+
return conn
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def patchi_action_log(
|
|
58
|
+
root: Path, action: str, target: str, agent: str = "", detail: str = "", status: str = "ok"
|
|
59
|
+
) -> None:
|
|
60
|
+
"""Log an action to the local SQLite audit trail."""
|
|
61
|
+
row = (
|
|
62
|
+
time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
|
63
|
+
action,
|
|
64
|
+
target,
|
|
65
|
+
agent,
|
|
66
|
+
detail,
|
|
67
|
+
status,
|
|
68
|
+
)
|
|
69
|
+
last_err: Exception | None = None
|
|
70
|
+
for attempt in range(3):
|
|
71
|
+
try:
|
|
72
|
+
conn = _get_db(root)
|
|
73
|
+
try:
|
|
74
|
+
conn.execute(
|
|
75
|
+
"INSERT INTO action_log (timestamp, action, target, agent, detail, status) VALUES (?, ?, ?, ?, ?, ?)",
|
|
76
|
+
row,
|
|
77
|
+
)
|
|
78
|
+
conn.commit()
|
|
79
|
+
finally:
|
|
80
|
+
conn.close()
|
|
81
|
+
return
|
|
82
|
+
except sqlite3.DatabaseError as e: # OperationalError is a subclass
|
|
83
|
+
last_err = e
|
|
84
|
+
time.sleep(0.1 * (attempt + 1))
|
|
85
|
+
_log.warning("patchi_action_log failed after 3 attempts: %s", last_err)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def patchi_get_actions(root: Path, limit: int = 50) -> list[dict]:
|
|
89
|
+
"""Retrieve recent actions from the audit log."""
|
|
90
|
+
conn = _get_db(root)
|
|
91
|
+
try:
|
|
92
|
+
cursor = conn.execute(
|
|
93
|
+
"SELECT timestamp, action, target, agent, detail, status FROM action_log ORDER BY id DESC LIMIT ?",
|
|
94
|
+
(limit,),
|
|
95
|
+
)
|
|
96
|
+
return [
|
|
97
|
+
{
|
|
98
|
+
"timestamp": r[0],
|
|
99
|
+
"action": r[1],
|
|
100
|
+
"target": r[2],
|
|
101
|
+
"agent": r[3],
|
|
102
|
+
"detail": r[4],
|
|
103
|
+
"status": r[5],
|
|
104
|
+
}
|
|
105
|
+
for r in cursor.fetchall()
|
|
106
|
+
]
|
|
107
|
+
finally:
|
|
108
|
+
conn.close()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# ── Policy Gate ───────────────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
_DEFAULT_POLICY = {
|
|
114
|
+
"never_touch": [".env", "docker-compose.yml", "*.key", "*.pem", "*.p12"],
|
|
115
|
+
"auto_apply_max_severity": "medium",
|
|
116
|
+
"require_confirmation_above": "high",
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _load_policy(root: Path) -> dict:
|
|
121
|
+
"""Load policy from .patchi/policies/default.yaml or use defaults."""
|
|
122
|
+
policy_file = root / ".patchi" / "policies" / "default.yaml"
|
|
123
|
+
if policy_file.exists():
|
|
124
|
+
try:
|
|
125
|
+
import yaml
|
|
126
|
+
|
|
127
|
+
data = yaml.safe_load(policy_file.read_text(encoding="utf-8"))
|
|
128
|
+
if isinstance(data, dict):
|
|
129
|
+
return {**_DEFAULT_POLICY, **data}
|
|
130
|
+
except Exception as e:
|
|
131
|
+
_log.warning("_load_policy failed: %s", e)
|
|
132
|
+
return _DEFAULT_POLICY.copy()
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def patchi_policy_gate(
|
|
136
|
+
root: Path, action: str, target: str, severity: str = "medium"
|
|
137
|
+
) -> tuple[bool, str]:
|
|
138
|
+
"""
|
|
139
|
+
Check if an action is allowed by the policy gate.
|
|
140
|
+
Returns (allowed, reason).
|
|
141
|
+
"""
|
|
142
|
+
policy = _load_policy(root)
|
|
143
|
+
|
|
144
|
+
# Check never_touch patterns
|
|
145
|
+
from fnmatch import fnmatch
|
|
146
|
+
|
|
147
|
+
for pattern in policy.get("never_touch", []):
|
|
148
|
+
if fnmatch(target, pattern) or fnmatch(target.split("/")[-1], pattern):
|
|
149
|
+
return (
|
|
150
|
+
False,
|
|
151
|
+
f"Policy blocks action on '{target}' (matches never_touch pattern: {pattern})",
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Check severity threshold
|
|
155
|
+
max_auto = policy.get("auto_apply_max_severity", "medium")
|
|
156
|
+
sev_order = {"info": 0, "low": 1, "medium": 2, "high": 3, "critical": 4}
|
|
157
|
+
if sev_order.get(severity, 0) > sev_order.get(max_auto, 2):
|
|
158
|
+
return (
|
|
159
|
+
False,
|
|
160
|
+
f"Severity '{severity}' exceeds auto-apply threshold '{max_auto}' — requires confirmation",
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
return True, "Allowed by policy"
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
# ── Governance Agent ──────────────────────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@register
|
|
170
|
+
class GovernanceAgent(BaseAgent):
|
|
171
|
+
"""Action logging and policy gate enforcement."""
|
|
172
|
+
|
|
173
|
+
name = "GovernanceAgent"
|
|
174
|
+
group = AgentGroup.GUARD
|
|
175
|
+
timeout = 30
|
|
176
|
+
|
|
177
|
+
def _run(self, inp: AgentInput, result: AgentResult) -> None:
|
|
178
|
+
# Log that a governance check was performed
|
|
179
|
+
patchi_action_log(inp.root, "governance_check", "security_scan", agent=self.name)
|
|
180
|
+
|
|
181
|
+
# Check policy violations — use pattern-targeted glob instead of O(all files) walk (ARCH-06 fix)
|
|
182
|
+
policy = _load_policy(inp.root)
|
|
183
|
+
|
|
184
|
+
never_touch_patterns = policy.get("never_touch", [])
|
|
185
|
+
if not never_touch_patterns:
|
|
186
|
+
result.files_scanned = 0
|
|
187
|
+
return
|
|
188
|
+
|
|
189
|
+
# Only walk files that could match the patterns — much faster than safe_rglob("*")
|
|
190
|
+
scanned = 0
|
|
191
|
+
for pattern in never_touch_patterns:
|
|
192
|
+
# Use glob directly with the pattern to avoid full tree walk
|
|
193
|
+
for fpath in inp.root.rglob(pattern):
|
|
194
|
+
if not fpath.is_file():
|
|
195
|
+
continue
|
|
196
|
+
rel = fpath.relative_to(inp.root).as_posix()
|
|
197
|
+
scanned += 1
|
|
198
|
+
result.add_finding(
|
|
199
|
+
Finding(
|
|
200
|
+
agent=self.name,
|
|
201
|
+
type="policy_violation",
|
|
202
|
+
severity=Severity.HIGH,
|
|
203
|
+
file=rel,
|
|
204
|
+
message=f"File matches never_touch policy pattern: {pattern}",
|
|
205
|
+
)
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
result.files_scanned = scanned
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Gradual Enforcement §10.2.2 — ratcheting baseline down week by week.
|
|
3
|
+
|
|
4
|
+
Stores baseline file count, fails if count exceeds target ratchet.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
_GATE_FILE = ".patchi/gradual_gate.json"
|
|
13
|
+
|
|
14
|
+
def ratchet_check(root: Path, current_count: int, weekly_reduction: int = 5) -> tuple[bool, str]:
|
|
15
|
+
p=root/_GATE_FILE
|
|
16
|
+
data=json.loads(p.read_text(encoding="utf-8")) if p.exists() else {"baseline": current_count, "target": current_count}
|
|
17
|
+
target=data.get("target", current_count)
|
|
18
|
+
# reduce target by weekly_reduction if week passed
|
|
19
|
+
import time
|
|
20
|
+
last=data.get("ts", time.time())
|
|
21
|
+
if time.time()-last > 7*24*3600 and target>0:
|
|
22
|
+
target=max(0, target-weekly_reduction)
|
|
23
|
+
data["target"]=target
|
|
24
|
+
data["ts"]=time.time()
|
|
25
|
+
p.write_text(json.dumps(data, indent=2), encoding="utf-8")
|
|
26
|
+
if current_count > target:
|
|
27
|
+
return False, f"Ratchet fail: {current_count} > target {target} (baseline {data.get('baseline')}) — reduce {current_count-target} findings this week"
|
|
28
|
+
return True, f"Ratchet pass: {current_count} <= target {target}"
|
|
29
|
+
|
|
30
|
+
def set_baseline(root: Path, count: int) -> None:
|
|
31
|
+
p=root/_GATE_FILE
|
|
32
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
33
|
+
import time, json
|
|
34
|
+
p.write_text(json.dumps({"baseline": count, "target": count, "ts": time.time()}, indent=2), encoding="utf-8")
|