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
patchi/web/api/cicd.py
ADDED
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
"""CI/CD REST API — trigger scans, retrieve results, health checks.
|
|
2
|
+
|
|
3
|
+
Endpoints:
|
|
4
|
+
POST /api/cicd/scan — trigger scan and wait for results (sync)
|
|
5
|
+
POST /api/cicd/scan/async — trigger scan, return immediately (async)
|
|
6
|
+
GET /api/cicd/scan/status — get current scan status
|
|
7
|
+
GET /api/cicd/scan/results — get latest scan results
|
|
8
|
+
GET /api/cicd/health — health check
|
|
9
|
+
GET /api/cicd/assurance — get assurance graph data
|
|
10
|
+
GET /api/cicd/findings — get findings with filters
|
|
11
|
+
GET /api/cicd/summary — project summary (findings, agents, health)
|
|
12
|
+
|
|
13
|
+
Usage from CI/CD:
|
|
14
|
+
curl -X POST http://localhost:1612/api/cicd/scan -d '{"scan_type":"all"}'
|
|
15
|
+
curl http://localhost:1612/api/cicd/health
|
|
16
|
+
curl http://localhost:1612/api/cicd/findings?severity=critical&limit=10
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import asyncio
|
|
22
|
+
import hashlib
|
|
23
|
+
import logging
|
|
24
|
+
import os
|
|
25
|
+
import time
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
from fastapi import APIRouter, Depends, Header, Query, Request
|
|
29
|
+
from fastapi.responses import JSONResponse
|
|
30
|
+
from pydantic import BaseModel
|
|
31
|
+
|
|
32
|
+
_log = logging.getLogger("patchi.web.cicd")
|
|
33
|
+
|
|
34
|
+
router = APIRouter(prefix="/api/cicd")
|
|
35
|
+
|
|
36
|
+
# ── API Key Authentication ───────────────────────────────────────────────────
|
|
37
|
+
# The API key is read from:
|
|
38
|
+
# 1. Environment variable PATCHI_API_KEY (recommended for production)
|
|
39
|
+
# 2. .patchi/api_key file (auto-generated on first run if missing)
|
|
40
|
+
# 3. If neither exists, auth is disabled (local-only mode)
|
|
41
|
+
#
|
|
42
|
+
# Clients send the key via X-API-Key header.
|
|
43
|
+
# Health check endpoint is always unauthenticated.
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _get_api_key(root: Path | None = None) -> str | None:
|
|
47
|
+
"""Resolve the API key from env or .patchi/api_key file."""
|
|
48
|
+
# 1. Environment variable (always checked first)
|
|
49
|
+
env_key = os.environ.get("PATCHI_API_KEY", "")
|
|
50
|
+
if env_key:
|
|
51
|
+
return env_key.strip()
|
|
52
|
+
# 2. File-based key (generated lazily)
|
|
53
|
+
if root is None:
|
|
54
|
+
try:
|
|
55
|
+
from patchi.core.config import require_project_root
|
|
56
|
+
|
|
57
|
+
root = require_project_root()
|
|
58
|
+
except Exception:
|
|
59
|
+
return None
|
|
60
|
+
try:
|
|
61
|
+
key_file = root / ".patchi" / "api_key"
|
|
62
|
+
if key_file.is_file():
|
|
63
|
+
return key_file.read_text(encoding="utf-8").strip()
|
|
64
|
+
# Auto-generate on first access
|
|
65
|
+
import secrets
|
|
66
|
+
|
|
67
|
+
new_key = secrets.token_urlsafe(32)
|
|
68
|
+
key_file.parent.mkdir(parents=True, exist_ok=True)
|
|
69
|
+
key_file.write_text(new_key, encoding="utf-8")
|
|
70
|
+
_log.info("Generated CI/CD API key: %s...", new_key[:8])
|
|
71
|
+
return new_key
|
|
72
|
+
except Exception:
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _verify_api_key(x_api_key: str | None = Header(None)) -> str | None:
|
|
77
|
+
"""FastAPI dependency: verify X-API-Key header.
|
|
78
|
+
|
|
79
|
+
Returns the API key if valid, or None if auth is disabled.
|
|
80
|
+
Raises HTTPException 401 if auth is enabled but key is missing/wrong.
|
|
81
|
+
"""
|
|
82
|
+
required = _get_api_key()
|
|
83
|
+
if not required:
|
|
84
|
+
return None # auth disabled
|
|
85
|
+
if not x_api_key:
|
|
86
|
+
from fastapi import HTTPException
|
|
87
|
+
|
|
88
|
+
raise HTTPException(
|
|
89
|
+
status_code=401,
|
|
90
|
+
detail={
|
|
91
|
+
"error": "Missing X-API-Key header",
|
|
92
|
+
"hint": "Send X-API-Key header with your request",
|
|
93
|
+
},
|
|
94
|
+
)
|
|
95
|
+
# Constant-time comparison to prevent timing attacks
|
|
96
|
+
if not hashlib.compare_digest(x_api_key, required):
|
|
97
|
+
from fastapi import HTTPException
|
|
98
|
+
|
|
99
|
+
raise HTTPException(
|
|
100
|
+
status_code=401,
|
|
101
|
+
detail={
|
|
102
|
+
"error": "Invalid API key",
|
|
103
|
+
"hint": "Send X-API-Key header with your request",
|
|
104
|
+
},
|
|
105
|
+
)
|
|
106
|
+
return x_api_key
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# ── Models ──────────────────────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class ScanRequest(BaseModel):
|
|
113
|
+
scan_type: str = "all"
|
|
114
|
+
deep: bool = False
|
|
115
|
+
pipeline: bool = False
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# ── Scan status (shared state) ──────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
_scan_state = {
|
|
121
|
+
"running": False,
|
|
122
|
+
"started_at": None,
|
|
123
|
+
"completed_at": None,
|
|
124
|
+
"last_result": None,
|
|
125
|
+
"last_error": None,
|
|
126
|
+
"agents_run": 0,
|
|
127
|
+
"total_findings": 0,
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# ── Endpoints ───────────────────────────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@router.get("/health")
|
|
135
|
+
async def health(request: Request) -> JSONResponse:
|
|
136
|
+
"""Health check endpoint for CI/CD monitoring."""
|
|
137
|
+
root = request.app.state.root
|
|
138
|
+
|
|
139
|
+
# Check brain data
|
|
140
|
+
brain_exists = (root / ".patchi" / "brain.json").is_file()
|
|
141
|
+
|
|
142
|
+
# Check assurance graph
|
|
143
|
+
assurance_exists = (root / ".patchi" / "assurance_graph.json").is_file()
|
|
144
|
+
|
|
145
|
+
# Check scan results
|
|
146
|
+
scan_results_exist = False
|
|
147
|
+
try:
|
|
148
|
+
from patchi.core import memory as mem
|
|
149
|
+
|
|
150
|
+
results = mem.get_scan_results(root)
|
|
151
|
+
scan_results_exist = bool(results)
|
|
152
|
+
except Exception as _exc:
|
|
153
|
+
_log.warning('health failed: %s', _exc)
|
|
154
|
+
|
|
155
|
+
return JSONResponse(
|
|
156
|
+
{
|
|
157
|
+
"status": "healthy",
|
|
158
|
+
"version": "0.6.0",
|
|
159
|
+
"project_root": str(root),
|
|
160
|
+
"brain_ready": brain_exists,
|
|
161
|
+
"assurance_ready": assurance_exists,
|
|
162
|
+
"scan_results_ready": scan_results_exist,
|
|
163
|
+
"scan_running": _scan_state["running"],
|
|
164
|
+
"last_scan": _scan_state["completed_at"],
|
|
165
|
+
}
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@router.post("/scan")
|
|
170
|
+
async def scan_sync(
|
|
171
|
+
request: Request, body: ScanRequest = None, _key: str | None = Depends(_verify_api_key)
|
|
172
|
+
) -> JSONResponse:
|
|
173
|
+
"""Trigger scan and wait for results (synchronous, for CI/CD).
|
|
174
|
+
|
|
175
|
+
Returns scan results when complete. Timeout: 300s.
|
|
176
|
+
"""
|
|
177
|
+
if _scan_state["running"]:
|
|
178
|
+
return JSONResponse(
|
|
179
|
+
{
|
|
180
|
+
"ok": False,
|
|
181
|
+
"error": "Scan already running",
|
|
182
|
+
"started_at": _scan_state["started_at"],
|
|
183
|
+
},
|
|
184
|
+
status_code=409,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
body = body or ScanRequest()
|
|
188
|
+
root = request.app.state.root
|
|
189
|
+
|
|
190
|
+
_scan_state["running"] = True
|
|
191
|
+
_scan_state["started_at"] = time.time()
|
|
192
|
+
_scan_state["completed_at"] = None
|
|
193
|
+
_scan_state["last_error"] = None
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
result = await _run_scan(root, body.scan_type, body.deep, body.pipeline)
|
|
197
|
+
_scan_state["last_result"] = result
|
|
198
|
+
_scan_state["completed_at"] = time.time()
|
|
199
|
+
return JSONResponse(result)
|
|
200
|
+
except Exception as e:
|
|
201
|
+
_scan_state["last_error"] = str(e)
|
|
202
|
+
return JSONResponse(
|
|
203
|
+
{
|
|
204
|
+
"ok": False,
|
|
205
|
+
"error": str(e),
|
|
206
|
+
"duration_ms": int((time.time() - _scan_state["started_at"]) * 1000),
|
|
207
|
+
},
|
|
208
|
+
status_code=500,
|
|
209
|
+
)
|
|
210
|
+
finally:
|
|
211
|
+
_scan_state["running"] = False
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@router.post("/scan/async")
|
|
215
|
+
async def scan_async(
|
|
216
|
+
request: Request, body: ScanRequest = None, _key: str | None = Depends(_verify_api_key)
|
|
217
|
+
) -> JSONResponse:
|
|
218
|
+
"""Trigger scan and return immediately (async, for CI/CD).
|
|
219
|
+
|
|
220
|
+
Poll /api/cicd/scan/status for completion.
|
|
221
|
+
"""
|
|
222
|
+
if _scan_state["running"]:
|
|
223
|
+
return JSONResponse(
|
|
224
|
+
{
|
|
225
|
+
"ok": False,
|
|
226
|
+
"error": "Scan already running",
|
|
227
|
+
"started_at": _scan_state["started_at"],
|
|
228
|
+
},
|
|
229
|
+
status_code=409,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
body = body or ScanRequest()
|
|
233
|
+
root = request.app.state.root
|
|
234
|
+
|
|
235
|
+
_scan_state["running"] = True
|
|
236
|
+
_scan_state["started_at"] = time.time()
|
|
237
|
+
_scan_state["completed_at"] = None
|
|
238
|
+
|
|
239
|
+
async def _bg():
|
|
240
|
+
try:
|
|
241
|
+
result = await _run_scan(root, body.scan_type, body.deep, body.pipeline)
|
|
242
|
+
_scan_state["last_result"] = result
|
|
243
|
+
_scan_state["completed_at"] = time.time()
|
|
244
|
+
except Exception as e:
|
|
245
|
+
_scan_state["last_error"] = str(e)
|
|
246
|
+
finally:
|
|
247
|
+
_scan_state["running"] = False
|
|
248
|
+
|
|
249
|
+
asyncio.create_task(_bg())
|
|
250
|
+
|
|
251
|
+
return JSONResponse(
|
|
252
|
+
{
|
|
253
|
+
"ok": True,
|
|
254
|
+
"message": "Scan started",
|
|
255
|
+
"started_at": _scan_state["started_at"],
|
|
256
|
+
"poll_url": "/api/cicd/scan/status",
|
|
257
|
+
}
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
@router.get("/scan/status")
|
|
262
|
+
async def scan_status(_key: str | None = Depends(_verify_api_key)) -> JSONResponse:
|
|
263
|
+
"""Get current scan status."""
|
|
264
|
+
return JSONResponse(
|
|
265
|
+
{
|
|
266
|
+
"running": _scan_state["running"],
|
|
267
|
+
"started_at": _scan_state["started_at"],
|
|
268
|
+
"completed_at": _scan_state["completed_at"],
|
|
269
|
+
"agents_run": _scan_state["agents_run"],
|
|
270
|
+
"total_findings": _scan_state["total_findings"],
|
|
271
|
+
"error": _scan_state["last_error"],
|
|
272
|
+
}
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
@router.get("/scan/results")
|
|
277
|
+
async def scan_results(
|
|
278
|
+
request: Request, _key: str | None = Depends(_verify_api_key)
|
|
279
|
+
) -> JSONResponse:
|
|
280
|
+
"""Get latest scan results."""
|
|
281
|
+
root = request.app.state.root
|
|
282
|
+
|
|
283
|
+
if _scan_state["last_result"]:
|
|
284
|
+
return JSONResponse(_scan_state["last_result"])
|
|
285
|
+
|
|
286
|
+
# Load from memory
|
|
287
|
+
try:
|
|
288
|
+
from patchi.core import memory as mem
|
|
289
|
+
|
|
290
|
+
results = mem.get_scan_results(root)
|
|
291
|
+
findings = []
|
|
292
|
+
for agent_name, data in (results or {}).items():
|
|
293
|
+
for f in data.get("findings", []):
|
|
294
|
+
f["agent"] = agent_name
|
|
295
|
+
findings.append(f)
|
|
296
|
+
|
|
297
|
+
sev_order = {"critical": 0, "high": 1, "medium": 2, "low": 3, "info": 4}
|
|
298
|
+
findings.sort(key=lambda f: sev_order.get(f.get("severity", "info"), 5))
|
|
299
|
+
|
|
300
|
+
return JSONResponse(
|
|
301
|
+
{
|
|
302
|
+
"ok": True,
|
|
303
|
+
"total": len(findings),
|
|
304
|
+
"findings": findings[:100],
|
|
305
|
+
"by_severity": {
|
|
306
|
+
s: sum(1 for f in findings if f.get("severity") == s)
|
|
307
|
+
for s in ["critical", "high", "medium", "low", "info"]
|
|
308
|
+
},
|
|
309
|
+
}
|
|
310
|
+
)
|
|
311
|
+
except Exception as e:
|
|
312
|
+
return JSONResponse({"ok": False, "error": str(e)}, status_code=500)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
@router.get("/findings")
|
|
316
|
+
async def findings(
|
|
317
|
+
request: Request,
|
|
318
|
+
severity: str | None = None,
|
|
319
|
+
agent: str | None = None,
|
|
320
|
+
limit: int = Query(50, ge=1, le=500),
|
|
321
|
+
offset: int = Query(0, ge=0),
|
|
322
|
+
_key: str | None = Depends(_verify_api_key),
|
|
323
|
+
) -> JSONResponse:
|
|
324
|
+
"""Get findings with filters."""
|
|
325
|
+
root = request.app.state.root
|
|
326
|
+
|
|
327
|
+
try:
|
|
328
|
+
from patchi.core import memory as mem
|
|
329
|
+
|
|
330
|
+
results = mem.get_scan_results(root)
|
|
331
|
+
all_findings = []
|
|
332
|
+
for agent_name, data in (results or {}).items():
|
|
333
|
+
for f in data.get("findings", []):
|
|
334
|
+
f["agent"] = agent_name
|
|
335
|
+
all_findings.append(f)
|
|
336
|
+
|
|
337
|
+
# Apply filters
|
|
338
|
+
if severity:
|
|
339
|
+
all_findings = [f for f in all_findings if f.get("severity") == severity]
|
|
340
|
+
if agent:
|
|
341
|
+
all_findings = [f for f in all_findings if f.get("agent") == agent]
|
|
342
|
+
|
|
343
|
+
# Sort by severity
|
|
344
|
+
sev_order = {"critical": 0, "high": 1, "medium": 2, "low": 3, "info": 4}
|
|
345
|
+
all_findings.sort(key=lambda f: sev_order.get(f.get("severity", "info"), 5))
|
|
346
|
+
|
|
347
|
+
total = len(all_findings)
|
|
348
|
+
page = all_findings[offset : offset + limit]
|
|
349
|
+
|
|
350
|
+
return JSONResponse(
|
|
351
|
+
{
|
|
352
|
+
"ok": True,
|
|
353
|
+
"total": total,
|
|
354
|
+
"offset": offset,
|
|
355
|
+
"limit": limit,
|
|
356
|
+
"findings": page,
|
|
357
|
+
}
|
|
358
|
+
)
|
|
359
|
+
except Exception as e:
|
|
360
|
+
return JSONResponse({"ok": False, "error": str(e)}, status_code=500)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
@router.get("/assurance")
|
|
364
|
+
async def assurance(request: Request, _key: str | None = Depends(_verify_api_key)) -> JSONResponse:
|
|
365
|
+
"""Get assurance graph data."""
|
|
366
|
+
root = request.app.state.root
|
|
367
|
+
|
|
368
|
+
try:
|
|
369
|
+
from patchi.core.assurance.graph import AssuranceGraph
|
|
370
|
+
|
|
371
|
+
graph = AssuranceGraph.load(root)
|
|
372
|
+
coverage = graph.coverage()
|
|
373
|
+
|
|
374
|
+
claims = []
|
|
375
|
+
for claim in graph.claims.values():
|
|
376
|
+
claims.append(
|
|
377
|
+
{
|
|
378
|
+
"id": claim.id,
|
|
379
|
+
"statement": claim.statement,
|
|
380
|
+
"domain": claim.domain,
|
|
381
|
+
"verdict": claim.verdict.value,
|
|
382
|
+
"severity": claim.severity_if_disproved,
|
|
383
|
+
"evidence_count": len(claim.evidence),
|
|
384
|
+
}
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
return JSONResponse(
|
|
388
|
+
{
|
|
389
|
+
"ok": True,
|
|
390
|
+
"coverage": coverage,
|
|
391
|
+
"claims": claims,
|
|
392
|
+
"total_claims": len(claims),
|
|
393
|
+
}
|
|
394
|
+
)
|
|
395
|
+
except Exception as e:
|
|
396
|
+
return JSONResponse({"ok": False, "error": str(e)}, status_code=500)
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
@router.get("/summary")
|
|
400
|
+
async def summary(request: Request, _key: str | None = Depends(_verify_api_key)) -> JSONResponse:
|
|
401
|
+
"""Project summary for CI/CD dashboards."""
|
|
402
|
+
root = request.app.state.root
|
|
403
|
+
|
|
404
|
+
try:
|
|
405
|
+
from patchi.core import memory as mem
|
|
406
|
+
|
|
407
|
+
# Brain data
|
|
408
|
+
brain = mem.get_brain(root)
|
|
409
|
+
file_count = brain.get("file_count", 0)
|
|
410
|
+
route_count = brain.get("route_count", 0)
|
|
411
|
+
|
|
412
|
+
# Findings
|
|
413
|
+
results = mem.get_scan_results(root)
|
|
414
|
+
all_findings = []
|
|
415
|
+
for agent_name, data in (results or {}).items():
|
|
416
|
+
for f in data.get("findings", []):
|
|
417
|
+
f["agent"] = agent_name
|
|
418
|
+
all_findings.append(f)
|
|
419
|
+
|
|
420
|
+
by_severity = {
|
|
421
|
+
s: sum(1 for f in all_findings if f.get("severity") == s)
|
|
422
|
+
for s in ["critical", "high", "medium", "low", "info"]
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
# Assurance
|
|
426
|
+
assurance_claims = 0
|
|
427
|
+
assurance_proved = 0
|
|
428
|
+
try:
|
|
429
|
+
from patchi.core.assurance.graph import AssuranceGraph
|
|
430
|
+
|
|
431
|
+
graph = AssuranceGraph.load(root)
|
|
432
|
+
assurance_claims = len(graph.claims)
|
|
433
|
+
assurance_proved = sum(1 for c in graph.claims.values() if c.verdict.value == "proved")
|
|
434
|
+
except Exception as _exc:
|
|
435
|
+
_log.warning('summary failed: %s', _exc)
|
|
436
|
+
|
|
437
|
+
# Model routing stats
|
|
438
|
+
routing_stats = {}
|
|
439
|
+
try:
|
|
440
|
+
from patchi.core.ai.model_router import get_model_router
|
|
441
|
+
|
|
442
|
+
router = get_model_router(root=root)
|
|
443
|
+
routing_stats = router.get_routing_stats()
|
|
444
|
+
except Exception as _exc:
|
|
445
|
+
_log.warning('summary failed: %s', _exc)
|
|
446
|
+
|
|
447
|
+
# Tenant cost
|
|
448
|
+
tenant_cost = 0.0
|
|
449
|
+
try:
|
|
450
|
+
from patchi.core.tenant import get_tenant_cost
|
|
451
|
+
|
|
452
|
+
tenant_cost = get_tenant_cost(root)
|
|
453
|
+
except Exception as _exc:
|
|
454
|
+
_log.warning('summary failed: %s', _exc)
|
|
455
|
+
|
|
456
|
+
return JSONResponse(
|
|
457
|
+
{
|
|
458
|
+
"ok": True,
|
|
459
|
+
"project": str(root),
|
|
460
|
+
"files": file_count,
|
|
461
|
+
"routes": route_count,
|
|
462
|
+
"findings": {
|
|
463
|
+
"total": len(all_findings),
|
|
464
|
+
"by_severity": by_severity,
|
|
465
|
+
},
|
|
466
|
+
"assurance": {
|
|
467
|
+
"claims": assurance_claims,
|
|
468
|
+
"proved": assurance_proved,
|
|
469
|
+
},
|
|
470
|
+
"routing": routing_stats,
|
|
471
|
+
"tenant_cost": tenant_cost,
|
|
472
|
+
"health_score": _compute_health_score(
|
|
473
|
+
all_findings, assurance_claims, assurance_proved
|
|
474
|
+
),
|
|
475
|
+
}
|
|
476
|
+
)
|
|
477
|
+
except Exception as e:
|
|
478
|
+
return JSONResponse({"ok": False, "error": str(e)}, status_code=500)
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
# ── Helpers ─────────────────────────────────────────────────────────────────
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
async def _run_scan(root: Path, scan_type: str, deep: bool, pipeline: bool) -> dict:
|
|
485
|
+
"""Run a scan and return results."""
|
|
486
|
+
import patchi.core.security.security_agents # noqa: F401
|
|
487
|
+
from patchi.core import config as cfg
|
|
488
|
+
from patchi.core import memory as mem
|
|
489
|
+
from patchi.core.agents.base import AgentGroup, AgentInput, list_agents
|
|
490
|
+
|
|
491
|
+
config = cfg.load(root) if (root / "pyproject.toml").exists() else {}
|
|
492
|
+
brain = mem.get_brain(root)
|
|
493
|
+
|
|
494
|
+
# Determine agents
|
|
495
|
+
_SKIP = {"SecurityProber", "RedTeamAgent"}
|
|
496
|
+
if scan_type == "all":
|
|
497
|
+
to_run = [a for a in list_agents(AgentGroup.SECURITY) if a.name not in _SKIP]
|
|
498
|
+
else:
|
|
499
|
+
all_agents = {a.name: a for a in list_agents(AgentGroup.SECURITY)}
|
|
500
|
+
if scan_type in all_agents:
|
|
501
|
+
to_run = [all_agents[scan_type]]
|
|
502
|
+
else:
|
|
503
|
+
raise ValueError(f"Unknown scan type: {scan_type}")
|
|
504
|
+
|
|
505
|
+
# Run agents
|
|
506
|
+
results = []
|
|
507
|
+
for agent_cls in to_run:
|
|
508
|
+
inp = AgentInput(root=root, scope=[], brain=brain, config=config)
|
|
509
|
+
result = await asyncio.to_thread(agent_cls().run, inp)
|
|
510
|
+
results.append(result)
|
|
511
|
+
|
|
512
|
+
_scan_state["agents_run"] = len(results)
|
|
513
|
+
_scan_state["total_findings"] = sum(r.finding_count for r in results)
|
|
514
|
+
|
|
515
|
+
# Correlate
|
|
516
|
+
try:
|
|
517
|
+
from patchi.core.security.orchestrator import SecurityOrchestrator
|
|
518
|
+
|
|
519
|
+
report = SecurityOrchestrator().correlate(results)
|
|
520
|
+
total = report.total_findings
|
|
521
|
+
except Exception:
|
|
522
|
+
total = sum(r.finding_count for r in results)
|
|
523
|
+
|
|
524
|
+
# Build response
|
|
525
|
+
all_findings = []
|
|
526
|
+
for r in results:
|
|
527
|
+
for f in r.findings:
|
|
528
|
+
all_findings.append(
|
|
529
|
+
{
|
|
530
|
+
"agent": r.agent_name,
|
|
531
|
+
"type": f.type,
|
|
532
|
+
"severity": f.severity.value
|
|
533
|
+
if hasattr(f.severity, "value")
|
|
534
|
+
else str(f.severity),
|
|
535
|
+
"file": f.file,
|
|
536
|
+
"line": f.line,
|
|
537
|
+
"message": f.message,
|
|
538
|
+
}
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
return {
|
|
542
|
+
"ok": True,
|
|
543
|
+
"scan_type": scan_type,
|
|
544
|
+
"agents_run": len(results),
|
|
545
|
+
"total_findings": total,
|
|
546
|
+
"findings": all_findings[:200],
|
|
547
|
+
"by_severity": {
|
|
548
|
+
s: sum(1 for f in all_findings if f.get("severity") == s)
|
|
549
|
+
for s in ["critical", "high", "medium", "low", "info"]
|
|
550
|
+
},
|
|
551
|
+
"duration_ms": int((time.time() - _scan_state["started_at"]) * 1000),
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def _compute_health_score(findings: list, claims: int, proved: int) -> int:
|
|
556
|
+
"""Compute a 0-100 health score."""
|
|
557
|
+
score = 100
|
|
558
|
+
|
|
559
|
+
# Deductions for findings
|
|
560
|
+
for f in findings:
|
|
561
|
+
sev = f.get("severity", "info")
|
|
562
|
+
if sev == "critical":
|
|
563
|
+
score -= 15
|
|
564
|
+
elif sev == "high":
|
|
565
|
+
score -= 8
|
|
566
|
+
elif sev == "medium":
|
|
567
|
+
score -= 3
|
|
568
|
+
elif sev == "low":
|
|
569
|
+
score -= 1
|
|
570
|
+
|
|
571
|
+
# Bonus for assurance coverage
|
|
572
|
+
if claims > 0:
|
|
573
|
+
coverage_pct = proved / claims
|
|
574
|
+
score += int(coverage_pct * 10)
|
|
575
|
+
|
|
576
|
+
return max(0, min(100, score))
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Serve DAST evidence files (screenshots, logs) from .patchi/evidence/dast/."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from fastapi import APIRouter, HTTPException
|
|
9
|
+
from fastapi.responses import FileResponse
|
|
10
|
+
|
|
11
|
+
router = APIRouter()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@router.get("/evidence/dast/{filename:path}")
|
|
15
|
+
async def serve_dast_evidence(filename: str):
|
|
16
|
+
"""Serve a DAST evidence file (screenshot, etc.).
|
|
17
|
+
|
|
18
|
+
Path: /evidence/dast/<filename>
|
|
19
|
+
The file must be under the project's .patchi/evidence/dast/ directory.
|
|
20
|
+
"""
|
|
21
|
+
# Security: only allow files from .patchi/evidence/dast/
|
|
22
|
+
safe_name = os.path.basename(filename) # prevent path traversal
|
|
23
|
+
# Walk up to find project root (look for .patchi dir)
|
|
24
|
+
project_root = Path.cwd()
|
|
25
|
+
evidence_path = project_root / ".patchi" / "evidence" / "dast" / safe_name
|
|
26
|
+
|
|
27
|
+
if not evidence_path.is_file():
|
|
28
|
+
raise HTTPException(status_code=404, detail="Evidence file not found")
|
|
29
|
+
|
|
30
|
+
# Determine media type from extension
|
|
31
|
+
ext = evidence_path.suffix.lower()
|
|
32
|
+
media_types = {
|
|
33
|
+
".png": "image/png",
|
|
34
|
+
".jpg": "image/jpeg",
|
|
35
|
+
".jpeg": "image/jpeg",
|
|
36
|
+
".gif": "image/gif",
|
|
37
|
+
".webp": "image/webp",
|
|
38
|
+
".svg": "image/svg+xml",
|
|
39
|
+
".txt": "text/plain",
|
|
40
|
+
".log": "text/plain",
|
|
41
|
+
".json": "application/json",
|
|
42
|
+
}
|
|
43
|
+
media_type = media_types.get(ext, "application/octet-stream")
|
|
44
|
+
|
|
45
|
+
return FileResponse(
|
|
46
|
+
path=str(evidence_path),
|
|
47
|
+
media_type=media_type,
|
|
48
|
+
filename=safe_name,
|
|
49
|
+
)
|