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,320 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Dev Check API — run p dev check gates from the web UI.
|
|
3
|
+
|
|
4
|
+
Runs the three gates (ruff lint, pytest, security scan) and returns
|
|
5
|
+
structured results for display in the Command Center card.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
import os
|
|
14
|
+
import time
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from fastapi import APIRouter, Request
|
|
18
|
+
from fastapi.responses import JSONResponse
|
|
19
|
+
from starlette.templating import Jinja2Templates
|
|
20
|
+
|
|
21
|
+
_log = logging.getLogger("patchi.web.api.dev_check")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
router = APIRouter(prefix="/api")
|
|
25
|
+
|
|
26
|
+
templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templates"))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@router.post("/dev-check")
|
|
30
|
+
async def run_dev_check(request: Request, strict: bool = False, fast: bool = False) -> JSONResponse:
|
|
31
|
+
"""Run p dev check gates and return results.
|
|
32
|
+
|
|
33
|
+
Gates:
|
|
34
|
+
1. Ruff lint (fast)
|
|
35
|
+
2. Pytest (may timeout on slow tests)
|
|
36
|
+
3. Security scan (changed files)
|
|
37
|
+
"""
|
|
38
|
+
root = request.app.state.root
|
|
39
|
+
|
|
40
|
+
results = {"gates": [], "overall": "PASS", "strict": strict}
|
|
41
|
+
overall_pass = True
|
|
42
|
+
|
|
43
|
+
# Gate 1: Ruff
|
|
44
|
+
gate1 = await _run_gate(
|
|
45
|
+
"Ruff Lint",
|
|
46
|
+
["python", "-m", "ruff", "check", "patchi/",
|
|
47
|
+
"--select", "E,F,W", "--ignore", "E501,E402,E741", "--quiet"],
|
|
48
|
+
root,
|
|
49
|
+
timeout=30,
|
|
50
|
+
)
|
|
51
|
+
results["gates"].append(gate1)
|
|
52
|
+
if gate1["status"] != "PASS":
|
|
53
|
+
overall_pass = False
|
|
54
|
+
|
|
55
|
+
# Gate 2: Pytest — stable test files only
|
|
56
|
+
_STABLE = [
|
|
57
|
+
"tests/test_config.py", "tests/test_charter.py", "tests/test_contract.py",
|
|
58
|
+
"tests/test_base.py", "tests/test_memory.py",
|
|
59
|
+
"tests/test_web.py", "tests/test_assurance.py",
|
|
60
|
+
"tests/test_risk_gate.py", "tests/test_scanner.py", "tests/test_detector.py",
|
|
61
|
+
"tests/test_secrets.py", "tests/test_security_config.py",
|
|
62
|
+
"tests/test_sigma_engine.py", "tests/test_blast_radius_v2.py",
|
|
63
|
+
"tests/test_chain_engine.py",
|
|
64
|
+
"tests/test_language_support.py", "tests/test_ast_utils.py",
|
|
65
|
+
"tests/test_import_graph.py",
|
|
66
|
+
"tests/test_hosted_mode.py", "tests/test_hosted_tokens.py",
|
|
67
|
+
"tests/test_hosted_audit_log.py", "tests/test_hosted_ip_reputation.py",
|
|
68
|
+
"tests/test_hosted_log_parsers.py", "tests/test_hosted_watchlist.py",
|
|
69
|
+
"tests/test_hosted_anomaly.py",
|
|
70
|
+
"tests/test_new_agents.py", "tests/test_p3_agents.py",
|
|
71
|
+
"tests/test_v2_agent_audit.py", "tests/test_v2_smoke.py",
|
|
72
|
+
"tests/test_attack_agent.py", "tests/test_doc_claim_agent.py",
|
|
73
|
+
"tests/test_fix_agents.py", "tests/test_scanners.py",
|
|
74
|
+
"tests/test_smart_agent.py",
|
|
75
|
+
"tests/test_governor_v2.py", "tests/test_governor_integration.py",
|
|
76
|
+
"tests/test_layered_brain.py", "tests/test_brain_watcher.py",
|
|
77
|
+
"tests/test_rebuilt_modules.py",
|
|
78
|
+
"tests/test_reasoning.py", "tests/test_noise_reduction.py",
|
|
79
|
+
"tests/test_ignore_learner.py", "tests/test_corpus_noise.py",
|
|
80
|
+
"tests/test_ai_client.py", "tests/test_debug_capture.py",
|
|
81
|
+
"tests/test_debug_codelldb.py", "tests/test_debug_node.py",
|
|
82
|
+
"tests/test_debug_powershell.py",
|
|
83
|
+
"tests/test_freshness.py", "tests/test_patch.py",
|
|
84
|
+
"tests/test_proactive.py", "tests/test_proactive_phase5.py",
|
|
85
|
+
"tests/test_snapshot.py", "tests/test_verify.py",
|
|
86
|
+
"tests/test_verify_loop.py", "tests/test_generated_suite.py",
|
|
87
|
+
"tests/test_app_profile.py", "tests/test_audit.py",
|
|
88
|
+
"tests/test_framework.py",
|
|
89
|
+
"tests/test_new_features.py", "tests/test_route_mapper.py",
|
|
90
|
+
"tests/test_applier.py", "tests/test_dispatcher.py",
|
|
91
|
+
"tests/test_notifications.py", "tests/test_queue.py",
|
|
92
|
+
"tests/test_scheduler.py", "tests/test_domain_loader.py",
|
|
93
|
+
]
|
|
94
|
+
_CORE = [
|
|
95
|
+
"tests/test_config.py", "tests/test_charter.py", "tests/test_contract.py",
|
|
96
|
+
"tests/test_base.py", "tests/test_memory.py", "tests/test_web.py",
|
|
97
|
+
"tests/test_risk_gate.py", "tests/test_scanner.py", "tests/test_detector.py",
|
|
98
|
+
"tests/test_secrets.py", "tests/test_language_support.py", "tests/test_ast_utils.py",
|
|
99
|
+
"tests/test_import_graph.py", "tests/test_ai_client.py",
|
|
100
|
+
"tests/test_freshness.py", "tests/test_framework.py",
|
|
101
|
+
]
|
|
102
|
+
_ptests = _CORE if fast else _STABLE
|
|
103
|
+
# Use xdist only on 4+ CPU machines
|
|
104
|
+
_ptargs = ["python", "-m", "pytest"]
|
|
105
|
+
import os as _os
|
|
106
|
+
if _os.cpu_count() and _os.cpu_count() >= 4:
|
|
107
|
+
try:
|
|
108
|
+
import xdist # noqa: F401
|
|
109
|
+
_ptargs += ["-n", "auto", "--dist", "loadscope"]
|
|
110
|
+
except ImportError:
|
|
111
|
+
pass
|
|
112
|
+
gate2 = await _run_gate(
|
|
113
|
+
"Pytest", _ptargs + _ptests + [
|
|
114
|
+
"-q", "--tb=no"],
|
|
115
|
+
root,
|
|
116
|
+
timeout=360,
|
|
117
|
+
)
|
|
118
|
+
results["gates"].append(gate2)
|
|
119
|
+
if gate2["status"] != "PASS":
|
|
120
|
+
overall_pass = False
|
|
121
|
+
|
|
122
|
+
# Gate 3: Security Scan (changed files)
|
|
123
|
+
gate3 = await _run_gate(
|
|
124
|
+
"Security Scan",
|
|
125
|
+
["python", "-m", "patchi.cli.main", "scan",
|
|
126
|
+
"--changed", "--quiet", "--json"],
|
|
127
|
+
root,
|
|
128
|
+
timeout=60,
|
|
129
|
+
)
|
|
130
|
+
results["gates"].append(gate3)
|
|
131
|
+
if gate3["status"] != "PASS":
|
|
132
|
+
overall_pass = False
|
|
133
|
+
|
|
134
|
+
results["overall"] = "PASS" if overall_pass else "FAIL"
|
|
135
|
+
results["timestamp"] = time.time()
|
|
136
|
+
results["strict"] = strict
|
|
137
|
+
results["fast"] = fast
|
|
138
|
+
|
|
139
|
+
# Record history for the timing chart
|
|
140
|
+
_record_history(root, results)
|
|
141
|
+
|
|
142
|
+
return JSONResponse(results)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
async def _run_gate(
|
|
146
|
+
name: str,
|
|
147
|
+
cmd: list[str],
|
|
148
|
+
cwd: Path,
|
|
149
|
+
timeout: int = 60,
|
|
150
|
+
) -> dict:
|
|
151
|
+
"""Run a single gate and return structured result."""
|
|
152
|
+
start = time.time()
|
|
153
|
+
|
|
154
|
+
try:
|
|
155
|
+
# Use temp files to avoid pipe deadlock on Windows when
|
|
156
|
+
# pytest subprocess tests keep pipe handles open.
|
|
157
|
+
import tempfile
|
|
158
|
+
tmp_out = tempfile.NamedTemporaryFile(mode='wb', suffix='.out', delete=False)
|
|
159
|
+
tmp_err = tempfile.NamedTemporaryFile(mode='wb', suffix='.err', delete=False)
|
|
160
|
+
try:
|
|
161
|
+
proc = await asyncio.create_subprocess_exec(
|
|
162
|
+
*cmd,
|
|
163
|
+
cwd=str(cwd),
|
|
164
|
+
stdout=tmp_out,
|
|
165
|
+
stderr=tmp_err,
|
|
166
|
+
)
|
|
167
|
+
await asyncio.wait_for(proc.wait(), timeout=timeout)
|
|
168
|
+
finally:
|
|
169
|
+
tmp_out.close()
|
|
170
|
+
tmp_err.close()
|
|
171
|
+
elapsed = time.time() - start
|
|
172
|
+
try:
|
|
173
|
+
output = open(tmp_out.name, 'rb').read().decode('utf-8', errors='replace') + open(tmp_err.name, 'rb').read().decode('utf-8', errors='replace')
|
|
174
|
+
except Exception:
|
|
175
|
+
output = ''
|
|
176
|
+
# Cleanup
|
|
177
|
+
try:
|
|
178
|
+
os.unlink(tmp_out.name)
|
|
179
|
+
os.unlink(tmp_err.name)
|
|
180
|
+
except Exception as _exc:
|
|
181
|
+
_log.warning('_run_gate failed: %s', _exc)
|
|
182
|
+
|
|
183
|
+
# Parse output for specific gate info
|
|
184
|
+
parsed = _parse_gate_output(name, output)
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
"name": name,
|
|
188
|
+
"status": "PASS" if proc.returncode == 0 else "FAIL",
|
|
189
|
+
"exit_code": proc.returncode,
|
|
190
|
+
"elapsed": round(elapsed, 1),
|
|
191
|
+
"output": output[:2000], # Truncate long output
|
|
192
|
+
"parsed": parsed,
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
except TimeoutError:
|
|
196
|
+
return {
|
|
197
|
+
"name": name,
|
|
198
|
+
"status": "TIMEOUT",
|
|
199
|
+
"exit_code": -1,
|
|
200
|
+
"elapsed": timeout,
|
|
201
|
+
"output": f"Gate timed out after {timeout}s",
|
|
202
|
+
"parsed": {},
|
|
203
|
+
}
|
|
204
|
+
except Exception as e:
|
|
205
|
+
return {
|
|
206
|
+
"name": name,
|
|
207
|
+
"status": "ERROR",
|
|
208
|
+
"exit_code": -1,
|
|
209
|
+
"elapsed": time.time() - start,
|
|
210
|
+
"output": str(e),
|
|
211
|
+
"parsed": {},
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _record_history(root: Path, results: dict) -> None:
|
|
216
|
+
"""Append gate timing data to history file for the chart."""
|
|
217
|
+
try:
|
|
218
|
+
import json as _json
|
|
219
|
+
|
|
220
|
+
history_path = root / ".patchi" / "dev_check_history.json"
|
|
221
|
+
history_path.parent.mkdir(parents=True, exist_ok=True)
|
|
222
|
+
|
|
223
|
+
history: list[dict] = []
|
|
224
|
+
if history_path.is_file():
|
|
225
|
+
try:
|
|
226
|
+
history = _json.loads(history_path.read_text(encoding="utf-8"))
|
|
227
|
+
except Exception:
|
|
228
|
+
history = []
|
|
229
|
+
|
|
230
|
+
entry = {
|
|
231
|
+
"timestamp": results.get("timestamp", time.time()),
|
|
232
|
+
"overall": results.get("overall", "?"),
|
|
233
|
+
"fast": results.get("fast", False),
|
|
234
|
+
"gates": [],
|
|
235
|
+
}
|
|
236
|
+
for g in results.get("gates", []):
|
|
237
|
+
entry["gates"].append({
|
|
238
|
+
"name": g.get("name", "?"),
|
|
239
|
+
"status": g.get("status", "?"),
|
|
240
|
+
"elapsed": g.get("elapsed", 0),
|
|
241
|
+
})
|
|
242
|
+
entry["total_time"] = sum(g["elapsed"] for g in entry["gates"])
|
|
243
|
+
|
|
244
|
+
history.append(entry)
|
|
245
|
+
|
|
246
|
+
# Keep last 50 runs
|
|
247
|
+
if len(history) > 50:
|
|
248
|
+
history = history[-50:]
|
|
249
|
+
|
|
250
|
+
tmp = history_path.with_suffix(".json.tmp")
|
|
251
|
+
tmp.write_text(_json.dumps(history, indent=2), encoding="utf-8")
|
|
252
|
+
tmp.replace(history_path)
|
|
253
|
+
except Exception as _exc:
|
|
254
|
+
_log.debug("dev_check history write skipped: %s", _exc)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
@router.get("/dev-check/history")
|
|
258
|
+
async def dev_check_history(request: Request) -> JSONResponse:
|
|
259
|
+
"""Return gate execution history for the timing chart."""
|
|
260
|
+
root = request.app.state.root
|
|
261
|
+
import json as _json
|
|
262
|
+
|
|
263
|
+
history_path = root / ".patchi" / "dev_check_history.json"
|
|
264
|
+
history: list[dict] = []
|
|
265
|
+
if history_path.is_file():
|
|
266
|
+
try:
|
|
267
|
+
history = _json.loads(history_path.read_text(encoding="utf-8"))
|
|
268
|
+
except Exception:
|
|
269
|
+
history = []
|
|
270
|
+
|
|
271
|
+
# Keep last 20 for the chart
|
|
272
|
+
recent = history[-20:]
|
|
273
|
+
return JSONResponse({"ok": True, "runs": recent, "count": len(recent)})
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _parse_gate_output(gate_name: str, output: str) -> dict:
|
|
277
|
+
"""Parse gate output for specific metrics."""
|
|
278
|
+
parsed = {}
|
|
279
|
+
|
|
280
|
+
if gate_name == "Ruff Lint":
|
|
281
|
+
# Count errors
|
|
282
|
+
error_lines = [line for line in output.splitlines() if line.startswith("Found ")]
|
|
283
|
+
if error_lines:
|
|
284
|
+
parts = error_lines[0].split()
|
|
285
|
+
if len(parts) >= 2:
|
|
286
|
+
parsed["error_count"] = int(parts[1])
|
|
287
|
+
|
|
288
|
+
# Count by code
|
|
289
|
+
codes = {}
|
|
290
|
+
for line in output.splitlines():
|
|
291
|
+
if " E" in line or " F" in line or " W" in line:
|
|
292
|
+
parts = line.split()
|
|
293
|
+
for p in parts:
|
|
294
|
+
if len(p) == 4 and p[0] in "EFW":
|
|
295
|
+
codes[p] = codes.get(p, 0) + 1
|
|
296
|
+
parsed["by_code"] = codes
|
|
297
|
+
|
|
298
|
+
elif gate_name == "Pytest":
|
|
299
|
+
# Parse pytest summary
|
|
300
|
+
for line in output.splitlines():
|
|
301
|
+
if "passed" in line or "failed" in line:
|
|
302
|
+
parsed["summary"] = line.strip()
|
|
303
|
+
if "error" in line.lower() and "test" in line.lower():
|
|
304
|
+
parsed["collection_error"] = line.strip()
|
|
305
|
+
|
|
306
|
+
elif gate_name == "Security Scan":
|
|
307
|
+
# Try to parse JSON output
|
|
308
|
+
try:
|
|
309
|
+
# Find JSON in output (may have other text before/after)
|
|
310
|
+
for line in output.splitlines():
|
|
311
|
+
line = line.strip()
|
|
312
|
+
if line.startswith("{"):
|
|
313
|
+
data = json.loads(line)
|
|
314
|
+
parsed["findings"] = data.get("total_findings", 0)
|
|
315
|
+
parsed["domains"] = data.get("domains", [])
|
|
316
|
+
break
|
|
317
|
+
except (json.JSONDecodeError, AttributeError):
|
|
318
|
+
parsed["parse_error"] = "Could not parse JSON output"
|
|
319
|
+
|
|
320
|
+
return parsed
|
patchi/web/api/fix.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Fix API — apply/reject patches."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, Request
|
|
6
|
+
from fastapi.responses import JSONResponse
|
|
7
|
+
|
|
8
|
+
from patchi.core.tenant import tenant_context
|
|
9
|
+
|
|
10
|
+
router = APIRouter(prefix="/api/fix")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@router.post("/apply/{patch_id}")
|
|
14
|
+
async def apply_patch(patch_id: str, request: Request) -> JSONResponse:
|
|
15
|
+
root = request.app.state.root
|
|
16
|
+
# IDOR fix: use context manager for tenant isolation and verify patch ownership
|
|
17
|
+
with tenant_context(root):
|
|
18
|
+
from patchi.core import memory as mem
|
|
19
|
+
|
|
20
|
+
patches = mem.read("patches", root)
|
|
21
|
+
patch = next((p for p in patches if p.get("id") == patch_id), None)
|
|
22
|
+
|
|
23
|
+
if not patch:
|
|
24
|
+
return JSONResponse({"ok": False, "error": "Patch not found"}, status_code=404)
|
|
25
|
+
|
|
26
|
+
# Verify patch belongs to this tenant/project (prevent IDOR enumeration)
|
|
27
|
+
patch_project = patch.get("project") or patch.get("tenant") or ""
|
|
28
|
+
if patch_project and patch_project != str(root) and patch_project not in str(root):
|
|
29
|
+
return JSONResponse({"ok": False, "error": "Patch not found"}, status_code=404)
|
|
30
|
+
|
|
31
|
+
from patchi.core.fix.applier import PatchApplier
|
|
32
|
+
from patchi.core.fix.patch import Patch
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
p = Patch.from_dict(patch)
|
|
36
|
+
applier = PatchApplier(root)
|
|
37
|
+
applier.apply(p)
|
|
38
|
+
return JSONResponse({"ok": True, "message": f"Applied patch {patch_id}"})
|
|
39
|
+
except Exception as e:
|
|
40
|
+
return JSONResponse({"ok": False, "error": str(e)}, status_code=500)
|
|
41
|
+
|
|
42
|
+
# Fallback (unreachable due to with)
|
|
43
|
+
return JSONResponse({"ok": False, "error": "Tenant error"}, status_code=500)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@router.post("/reject/{patch_id}")
|
|
47
|
+
async def reject_patch(patch_id: str, request: Request) -> JSONResponse:
|
|
48
|
+
root = request.app.state.root
|
|
49
|
+
with tenant_context(root):
|
|
50
|
+
from patchi.core import memory as mem
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
mem.record_rejection(patch_id, "web_user", root)
|
|
54
|
+
return JSONResponse({"ok": True, "message": f"Rejected patch {patch_id}"})
|
|
55
|
+
except Exception as e:
|
|
56
|
+
return JSONResponse({"ok": False, "error": str(e)}, status_code=500)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@router.post("/apply-all-safe")
|
|
60
|
+
async def apply_all_safe(request: Request) -> JSONResponse:
|
|
61
|
+
"""Apply every pending patch that passes the risk gate for auto-apply.
|
|
62
|
+
|
|
63
|
+
Patches the gate flags as BLOCK or REQUIRE_REVIEW are skipped and reported,
|
|
64
|
+
never applied. This backs the "Apply all safe" button in the review UI.
|
|
65
|
+
"""
|
|
66
|
+
root = request.app.state.root
|
|
67
|
+
from patchi.core import memory as mem
|
|
68
|
+
from patchi.core.fix.applier import PatchApplier
|
|
69
|
+
from patchi.core.fix.patch import Patch
|
|
70
|
+
from patchi.core.fix.risk_gate import RiskGate
|
|
71
|
+
|
|
72
|
+
patches_raw = mem.read("patches", root)
|
|
73
|
+
if not patches_raw:
|
|
74
|
+
return JSONResponse(
|
|
75
|
+
{
|
|
76
|
+
"ok": True,
|
|
77
|
+
"applied": [],
|
|
78
|
+
"skipped": [],
|
|
79
|
+
"failed": [],
|
|
80
|
+
"message": "No patches pending",
|
|
81
|
+
}
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
gate = RiskGate(root)
|
|
85
|
+
applier = PatchApplier(root)
|
|
86
|
+
|
|
87
|
+
applied: list[dict] = []
|
|
88
|
+
skipped: list[dict] = []
|
|
89
|
+
failed: list[dict] = []
|
|
90
|
+
|
|
91
|
+
for raw in patches_raw:
|
|
92
|
+
pid = raw.get("id", "?")
|
|
93
|
+
state = raw.get("state", "")
|
|
94
|
+
if state in ("applied", "rejected"):
|
|
95
|
+
continue # only pending patches
|
|
96
|
+
try:
|
|
97
|
+
patch = Patch.from_dict(raw)
|
|
98
|
+
except Exception as e:
|
|
99
|
+
failed.append({"id": pid, "reason": f"unparseable: {e}"})
|
|
100
|
+
continue
|
|
101
|
+
|
|
102
|
+
decision = gate.evaluate(patch)
|
|
103
|
+
if decision.is_blocked() or decision.needs_review():
|
|
104
|
+
skipped.append(
|
|
105
|
+
{
|
|
106
|
+
"id": pid,
|
|
107
|
+
"reason": decision.decision.value
|
|
108
|
+
if hasattr(decision.decision, "value")
|
|
109
|
+
else str(decision.decision),
|
|
110
|
+
"detail": getattr(decision, "reasons", None) or "",
|
|
111
|
+
}
|
|
112
|
+
)
|
|
113
|
+
continue
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
result = applier.apply(patch)
|
|
117
|
+
entry = {"id": pid}
|
|
118
|
+
if result.success:
|
|
119
|
+
applied.append(entry)
|
|
120
|
+
else:
|
|
121
|
+
entry["reason"] = getattr(result, "error", "") or "applier refused"
|
|
122
|
+
failed.append(entry)
|
|
123
|
+
except Exception as e:
|
|
124
|
+
failed.append({"id": pid, "reason": str(e)})
|
|
125
|
+
|
|
126
|
+
return JSONResponse(
|
|
127
|
+
{
|
|
128
|
+
"ok": True,
|
|
129
|
+
"applied": applied,
|
|
130
|
+
"skipped": skipped,
|
|
131
|
+
"failed": failed,
|
|
132
|
+
"message": f"{len(applied)} applied, {len(skipped)} need review, {len(failed)} failed",
|
|
133
|
+
}
|
|
134
|
+
)
|
patchi/web/api/guard.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Guard API — HTMX fragments for hosted guard monitoring."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter, Request
|
|
8
|
+
from fastapi.responses import HTMLResponse
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger("patchi.web.api.guard")
|
|
11
|
+
|
|
12
|
+
router = APIRouter(prefix="/api/guard")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@router.get("/threats", response_class=HTMLResponse)
|
|
16
|
+
async def get_threats(request: Request) -> str:
|
|
17
|
+
root = request.app.state.root
|
|
18
|
+
lines = [
|
|
19
|
+
"<table style='width:100%;font-size:12px;'>",
|
|
20
|
+
"<tr><th>IP</th><th>Score</th><th>Reason</th></tr>",
|
|
21
|
+
]
|
|
22
|
+
try:
|
|
23
|
+
from patchi.core.hosted.watchlist import WatchlistTracker
|
|
24
|
+
|
|
25
|
+
tracker = WatchlistTracker(root)
|
|
26
|
+
for t in tracker.top(10):
|
|
27
|
+
ip = t.get("ip", "?")
|
|
28
|
+
score = t.get("score", 0)
|
|
29
|
+
reason = (t.get("reason", "") or "")[:60]
|
|
30
|
+
lines.append(
|
|
31
|
+
f"<tr><td style='font-family:var(--font-mono)'>{ip}</td>"
|
|
32
|
+
f"<td>{score}</td><td>{reason}</td></tr>"
|
|
33
|
+
)
|
|
34
|
+
except Exception:
|
|
35
|
+
lines.append("<tr><td colspan='3'>No threats</td></tr>")
|
|
36
|
+
lines.append("</table>")
|
|
37
|
+
return "".join(lines)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@router.get("/live", response_class=HTMLResponse)
|
|
41
|
+
async def get_live(request: Request) -> str:
|
|
42
|
+
root = request.app.state.root
|
|
43
|
+
lines: list[str] = []
|
|
44
|
+
try:
|
|
45
|
+
import time
|
|
46
|
+
|
|
47
|
+
from patchi.core.hosted.audit_log import read_recent
|
|
48
|
+
|
|
49
|
+
raw = read_recent(root, 10)
|
|
50
|
+
for e in reversed(raw):
|
|
51
|
+
ts = e.get("timestamp", 0)
|
|
52
|
+
tstr = time.strftime("%H:%M:%S", time.localtime(ts)) if ts else "--:--:--"
|
|
53
|
+
data = e.get("data", {})
|
|
54
|
+
msg = data.get("title", data.get("message", e.get("event", "")))
|
|
55
|
+
lines.append(
|
|
56
|
+
f"<div style='margin-bottom:3px'>"
|
|
57
|
+
f"<span style='color:var(--text-dim)'>{tstr}</span> "
|
|
58
|
+
f"<span>{msg}</span>"
|
|
59
|
+
f"</div>"
|
|
60
|
+
)
|
|
61
|
+
except Exception as e:
|
|
62
|
+
logger.warning("Failed to load recent audit events: %s", e)
|
|
63
|
+
if not lines:
|
|
64
|
+
lines.append("<div style='color:var(--text-dim)'>No recent events</div>")
|
|
65
|
+
return "".join(lines)
|
patchi/web/api/hosted.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Hosted API — hosted mode actions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, Request
|
|
6
|
+
from fastapi.responses import JSONResponse
|
|
7
|
+
|
|
8
|
+
router = APIRouter(prefix="/api/hosted")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@router.get("/status")
|
|
12
|
+
async def hosted_status(request: Request) -> JSONResponse:
|
|
13
|
+
root = request.app.state.root
|
|
14
|
+
from patchi.core import config as cfg
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
config = cfg.load(root)
|
|
18
|
+
except Exception:
|
|
19
|
+
config = {}
|
|
20
|
+
|
|
21
|
+
hosted = config.get("hosted", {})
|
|
22
|
+
return JSONResponse(
|
|
23
|
+
{
|
|
24
|
+
"enabled": hosted.get("enabled", False),
|
|
25
|
+
"log_path": hosted.get("log_path", ""),
|
|
26
|
+
}
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@router.post("/init")
|
|
31
|
+
async def hosted_init(request: Request) -> JSONResponse:
|
|
32
|
+
root = request.app.state.root
|
|
33
|
+
from patchi.core import config as cfg_mod
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
body = await request.json()
|
|
37
|
+
log_path = body.get("log_path", "/var/log/nginx/access.log")
|
|
38
|
+
log_format = body.get("log_format", "nginx")
|
|
39
|
+
escalate = body.get("escalate", True)
|
|
40
|
+
|
|
41
|
+
cfg_mod.set_value("hosted.log_path", log_path, root)
|
|
42
|
+
cfg_mod.set_value("hosted.log_format", log_format, root)
|
|
43
|
+
cfg_mod.set_value("hosted.escalate", escalate, root)
|
|
44
|
+
cfg_mod.set_value("hosted.enabled", True, root)
|
|
45
|
+
|
|
46
|
+
hosted_dir = root / ".patchi" / "hosted"
|
|
47
|
+
hosted_dir.mkdir(parents=True, exist_ok=True)
|
|
48
|
+
|
|
49
|
+
return JSONResponse(
|
|
50
|
+
{
|
|
51
|
+
"ok": True,
|
|
52
|
+
"message": "Hosted mode configured",
|
|
53
|
+
"log_path": log_path,
|
|
54
|
+
"log_format": log_format,
|
|
55
|
+
}
|
|
56
|
+
)
|
|
57
|
+
except Exception as e:
|
|
58
|
+
return JSONResponse({"ok": False, "error": str(e)}, status_code=500)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@router.get("/tokens")
|
|
62
|
+
async def list_tokens(request: Request) -> JSONResponse:
|
|
63
|
+
root = request.app.state.root
|
|
64
|
+
from patchi.core.hosted import tokens as tokens_mod
|
|
65
|
+
|
|
66
|
+
return JSONResponse({"tokens": tokens_mod.list_tokens(root)})
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@router.post("/tokens")
|
|
70
|
+
async def create_token(request: Request) -> JSONResponse:
|
|
71
|
+
root = request.app.state.root
|
|
72
|
+
from patchi.core.hosted import tokens as tokens_mod
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
body = await request.json()
|
|
76
|
+
name = body.get("name", "unnamed")
|
|
77
|
+
plaintext = tokens_mod.generate(root, name)
|
|
78
|
+
return JSONResponse(
|
|
79
|
+
{
|
|
80
|
+
"ok": True,
|
|
81
|
+
"token": plaintext,
|
|
82
|
+
"message": "Token generated — shown once",
|
|
83
|
+
}
|
|
84
|
+
)
|
|
85
|
+
except Exception as e:
|
|
86
|
+
return JSONResponse({"ok": False, "error": str(e)}, status_code=500)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@router.delete("/tokens/{token_id}")
|
|
90
|
+
async def revoke_token(token_id: str, request: Request) -> JSONResponse:
|
|
91
|
+
root = request.app.state.root
|
|
92
|
+
from patchi.core.tenant import tenant_context
|
|
93
|
+
|
|
94
|
+
with tenant_context(root):
|
|
95
|
+
from patchi.core.hosted import tokens as tokens_mod
|
|
96
|
+
|
|
97
|
+
# IDOR fix: verify token exists and belongs to tenant (list scope)
|
|
98
|
+
existing = {t.get("id") for t in tokens_mod.list_tokens(root)}
|
|
99
|
+
if token_id not in existing:
|
|
100
|
+
return JSONResponse({"ok": False, "error": "Token not found"}, status_code=404)
|
|
101
|
+
ok = tokens_mod.revoke(root, token_id)
|
|
102
|
+
if ok:
|
|
103
|
+
return JSONResponse({"ok": True, "message": f"Token {token_id} revoked"})
|
|
104
|
+
return JSONResponse({"ok": False, "error": "Token not found"}, status_code=404)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@router.post("/block/{ip}")
|
|
108
|
+
async def block_ip(ip: str, request: Request) -> JSONResponse:
|
|
109
|
+
root = request.app.state.root
|
|
110
|
+
from patchi.core.tenant import tenant_context
|
|
111
|
+
|
|
112
|
+
with tenant_context(root):
|
|
113
|
+
from patchi.core.hosted import ip_reputation
|
|
114
|
+
|
|
115
|
+
ip_reputation.block(ip, root)
|
|
116
|
+
return JSONResponse({"ok": True, "message": f"Blocked {ip}"})
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@router.post("/unblock/{ip}")
|
|
120
|
+
async def unblock_ip(ip: str, request: Request) -> JSONResponse:
|
|
121
|
+
root = request.app.state.root
|
|
122
|
+
from patchi.core.tenant import tenant_context
|
|
123
|
+
|
|
124
|
+
with tenant_context(root):
|
|
125
|
+
from patchi.core.hosted import ip_reputation
|
|
126
|
+
|
|
127
|
+
ip_reputation.unblock(ip, root)
|
|
128
|
+
return JSONResponse({"ok": True, "message": f"Unblocked {ip}"})
|