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,541 @@
|
|
|
1
|
+
"""Assurance route — assurance graph, campaigns, attackers, fuzz."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from datetime import UTC, datetime
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from fastapi import APIRouter, Request
|
|
10
|
+
from fastapi.responses import HTMLResponse, JSONResponse
|
|
11
|
+
from fastapi.templating import Jinja2Templates
|
|
12
|
+
|
|
13
|
+
_log = logging.getLogger("patchi.web.routes.assurance")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
router = APIRouter()
|
|
17
|
+
templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templates"))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@router.get("/assurance", response_class=HTMLResponse)
|
|
21
|
+
async def assurance(request: Request):
|
|
22
|
+
root = request.app.state.root
|
|
23
|
+
|
|
24
|
+
from patchi.core.assurance.graph import AssuranceGraph
|
|
25
|
+
|
|
26
|
+
graph = AssuranceGraph.load(root)
|
|
27
|
+
coverage = graph.coverage()
|
|
28
|
+
|
|
29
|
+
# Run attackers
|
|
30
|
+
attacker_results = []
|
|
31
|
+
try:
|
|
32
|
+
from patchi.core.attackers import AttackPlanner
|
|
33
|
+
|
|
34
|
+
planner = AttackPlanner(graph)
|
|
35
|
+
planner.plan()
|
|
36
|
+
attack_results = planner.run_all()
|
|
37
|
+
confirmed = [r for r in attack_results if r.confirmed]
|
|
38
|
+
attacker_results = [r.to_dict() for r in confirmed[:20]]
|
|
39
|
+
except Exception as _exc:
|
|
40
|
+
_log.warning('assurance failed: %s', _exc)
|
|
41
|
+
|
|
42
|
+
# Run campaigns
|
|
43
|
+
campaign_results = []
|
|
44
|
+
try:
|
|
45
|
+
from patchi.core.campaigns import CampaignOrchestrator
|
|
46
|
+
|
|
47
|
+
orch = CampaignOrchestrator(graph)
|
|
48
|
+
result = orch.run_all()
|
|
49
|
+
campaign_results = [c.to_dict() for c in result.campaigns]
|
|
50
|
+
except Exception as _exc:
|
|
51
|
+
_log.warning('assurance failed: %s', _exc)
|
|
52
|
+
|
|
53
|
+
# Fuzz stats
|
|
54
|
+
fuzz_endpoints = len([c for c in graph.claims.values() if "endpoint" in c.domain])
|
|
55
|
+
|
|
56
|
+
# DAST scan results (correlated with static analysis)
|
|
57
|
+
dast_findings = []
|
|
58
|
+
dast_tests_run = 0
|
|
59
|
+
dast_target = ""
|
|
60
|
+
try:
|
|
61
|
+
from patchi.core import memory as mem
|
|
62
|
+
mem.get_brain(root)
|
|
63
|
+
scan_results = mem.get_scan_results(root)
|
|
64
|
+
if "DASTAgent" in scan_results:
|
|
65
|
+
dast_data = scan_results["DASTAgent"]
|
|
66
|
+
dast_tests_run = dast_data.get("tests_run", 0)
|
|
67
|
+
dast_target = dast_data.get("target_url", "")
|
|
68
|
+
for f in dast_data.get("findings", []):
|
|
69
|
+
dast_findings.append({
|
|
70
|
+
"type": f.get("type", "dast_unknown"),
|
|
71
|
+
"severity": f.get("severity", "low"),
|
|
72
|
+
"message": f.get("message", ""),
|
|
73
|
+
"file": f.get("file", dast_target),
|
|
74
|
+
"suggestion": f.get("suggestion", ""),
|
|
75
|
+
"code_snippet": (f.get("code_snippet", "") or "")[:500],
|
|
76
|
+
"source": "dast",
|
|
77
|
+
})
|
|
78
|
+
# Also load static analysis findings for cross-correlation
|
|
79
|
+
static_high = []
|
|
80
|
+
for agent_name, data in scan_results.items():
|
|
81
|
+
if agent_name == "DASTAgent":
|
|
82
|
+
continue
|
|
83
|
+
for f in data.get("findings", []):
|
|
84
|
+
if f.get("severity") in ("critical", "high"):
|
|
85
|
+
static_high.append({
|
|
86
|
+
"agent": agent_name,
|
|
87
|
+
"type": f.get("type", ""),
|
|
88
|
+
"severity": f.get("severity", ""),
|
|
89
|
+
"message": f.get("message", ""),
|
|
90
|
+
"file": f.get("file", ""),
|
|
91
|
+
})
|
|
92
|
+
except Exception as _exc:
|
|
93
|
+
_log.warning('assurance failed: %s', _exc)
|
|
94
|
+
|
|
95
|
+
# Correlate DAST findings with static analysis findings (same severity/type)
|
|
96
|
+
dast_correlations = []
|
|
97
|
+
for df in dast_findings:
|
|
98
|
+
matches = [
|
|
99
|
+
s for s in static_high
|
|
100
|
+
if s["severity"] == df["severity"] or s["type"] in df["type"]
|
|
101
|
+
]
|
|
102
|
+
if matches:
|
|
103
|
+
dast_correlations.append({
|
|
104
|
+
"dast": df,
|
|
105
|
+
"static_matches": matches[:3], # max 3 per DAST finding
|
|
106
|
+
})
|
|
107
|
+
dast_uncorrelated = [f for f in dast_findings if not any(
|
|
108
|
+
c["dast"] == f for c in dast_correlations
|
|
109
|
+
)]
|
|
110
|
+
|
|
111
|
+
# Build claims data for the template
|
|
112
|
+
claims_data = []
|
|
113
|
+
for claim in graph.claims.values():
|
|
114
|
+
claims_data.append(
|
|
115
|
+
{
|
|
116
|
+
"id": claim.id,
|
|
117
|
+
"statement": claim.statement,
|
|
118
|
+
"domain": claim.domain,
|
|
119
|
+
"verdict": claim.verdict.value,
|
|
120
|
+
"severity": claim.severity_if_disproved,
|
|
121
|
+
"evidence_count": len(claim.evidence),
|
|
122
|
+
}
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# Sort by verdict (disproved first)
|
|
126
|
+
verdict_order = {"disproved": 0, "not_proved": 1, "unproven": 2, "proved": 3}
|
|
127
|
+
claims_data.sort(key=lambda c: verdict_order.get(c["verdict"], 4))
|
|
128
|
+
|
|
129
|
+
# Chain/intent claims (from chain_to_assurance bridge)
|
|
130
|
+
chain_claims = [c for c in claims_data if c["domain"] in ("exploit-chain", "intent-gap")]
|
|
131
|
+
invariant_claims = [
|
|
132
|
+
c for c in claims_data if c["domain"] not in ("exploit-chain", "intent-gap")
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
# Load raw chain data for the chain explorer tab
|
|
136
|
+
chain_raw = []
|
|
137
|
+
try:
|
|
138
|
+
ci_path = root / ".patchi" / "chain_intent.json"
|
|
139
|
+
if ci_path.is_file():
|
|
140
|
+
import json
|
|
141
|
+
|
|
142
|
+
ci = json.loads(ci_path.read_text(encoding="utf-8"))
|
|
143
|
+
chain_raw = ci.get("chains", [])
|
|
144
|
+
except Exception as _exc:
|
|
145
|
+
_log.warning('assurance failed: %s', _exc)
|
|
146
|
+
|
|
147
|
+
return templates.TemplateResponse(
|
|
148
|
+
request,
|
|
149
|
+
"assurance.html",
|
|
150
|
+
{
|
|
151
|
+
"request": request,
|
|
152
|
+
"coverage": coverage,
|
|
153
|
+
"claims": invariant_claims,
|
|
154
|
+
"chain_claims": chain_claims,
|
|
155
|
+
"total_claims": len(claims_data),
|
|
156
|
+
"attacker_results": attacker_results,
|
|
157
|
+
"attacker_count": len(attacker_results),
|
|
158
|
+
"campaign_results": campaign_results,
|
|
159
|
+
"campaign_count": len(campaign_results),
|
|
160
|
+
"fuzz_endpoints": fuzz_endpoints,
|
|
161
|
+
"chain_raw": chain_raw,
|
|
162
|
+
"dast_findings": dast_findings,
|
|
163
|
+
"dast_tests_run": dast_tests_run,
|
|
164
|
+
"dast_target": dast_target,
|
|
165
|
+
"dast_correlations": dast_correlations,
|
|
166
|
+
"dast_uncorrelated": dast_uncorrelated,
|
|
167
|
+
},
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@router.get("/api/assurance")
|
|
172
|
+
async def assurance_api(request: Request) -> JSONResponse:
|
|
173
|
+
"""JSON API for assurance data (for AJAX updates)."""
|
|
174
|
+
root = request.app.state.root
|
|
175
|
+
from patchi.core.assurance.graph import AssuranceGraph
|
|
176
|
+
|
|
177
|
+
graph = AssuranceGraph.load(root)
|
|
178
|
+
coverage = graph.coverage()
|
|
179
|
+
|
|
180
|
+
# Run attackers
|
|
181
|
+
attacker_count = 0
|
|
182
|
+
confirmed_count = 0
|
|
183
|
+
try:
|
|
184
|
+
from patchi.core.attackers import AttackPlanner
|
|
185
|
+
|
|
186
|
+
planner = AttackPlanner(graph)
|
|
187
|
+
attack_results = planner.run_all()
|
|
188
|
+
confirmed = [r for r in attack_results if r.confirmed]
|
|
189
|
+
attacker_count = len(attack_results)
|
|
190
|
+
confirmed_count = len(confirmed)
|
|
191
|
+
except Exception as _exc:
|
|
192
|
+
_log.warning('assurance_api failed: %s', _exc)
|
|
193
|
+
|
|
194
|
+
# Run campaigns
|
|
195
|
+
campaign_data = []
|
|
196
|
+
try:
|
|
197
|
+
from patchi.core.campaigns import CampaignOrchestrator
|
|
198
|
+
|
|
199
|
+
orch = CampaignOrchestrator(graph)
|
|
200
|
+
result = orch.run_all()
|
|
201
|
+
campaign_data = [c.to_dict() for c in result.campaigns]
|
|
202
|
+
except Exception as _exc:
|
|
203
|
+
_log.warning('assurance_api failed: %s', _exc)
|
|
204
|
+
|
|
205
|
+
return JSONResponse(
|
|
206
|
+
{
|
|
207
|
+
"coverage": coverage,
|
|
208
|
+
"attacker": {"total": attacker_count, "confirmed": confirmed_count},
|
|
209
|
+
"campaigns": campaign_data,
|
|
210
|
+
"fuzz_endpoints": len([c for c in graph.claims.values() if "endpoint" in c.domain]),
|
|
211
|
+
}
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@router.get("/api/assurance/heatmap")
|
|
216
|
+
async def assurance_heatmap(request: Request) -> JSONResponse:
|
|
217
|
+
"""Return per-domain coverage data for the dashboard heatmap."""
|
|
218
|
+
root = request.app.state.root
|
|
219
|
+
from patchi.core.assurance.graph import AssuranceGraph
|
|
220
|
+
|
|
221
|
+
graph = AssuranceGraph.load(root)
|
|
222
|
+
coverage = graph.coverage()
|
|
223
|
+
by_domain = coverage.get("by_domain", {})
|
|
224
|
+
by_verdict = coverage.get("by_verdict", {})
|
|
225
|
+
total = coverage.get("claims_total", 0)
|
|
226
|
+
|
|
227
|
+
# Build heatmap tiles: one per domain with coverage %
|
|
228
|
+
tiles = []
|
|
229
|
+
for domain, counts in by_domain.items():
|
|
230
|
+
proved = counts.get("proved", 0)
|
|
231
|
+
domain_total = counts.get("total", 0)
|
|
232
|
+
pct = round(proved / domain_total * 100) if domain_total > 0 else 0
|
|
233
|
+
tiles.append({
|
|
234
|
+
"domain": domain,
|
|
235
|
+
"proved": proved,
|
|
236
|
+
"total": domain_total,
|
|
237
|
+
"coverage_pct": pct,
|
|
238
|
+
})
|
|
239
|
+
# Sort by coverage ascending (worst first)
|
|
240
|
+
tiles.sort(key=lambda t: t["coverage_pct"])
|
|
241
|
+
|
|
242
|
+
# Overall stats
|
|
243
|
+
proved_total = by_verdict.get("proved", 0)
|
|
244
|
+
disproved_total = by_verdict.get("disproved", 0)
|
|
245
|
+
unproven_total = by_verdict.get("unproven", 0)
|
|
246
|
+
not_proved_total = by_verdict.get("not_proved", 0)
|
|
247
|
+
overall_pct = round(proved_total / total * 100) if total > 0 else 0
|
|
248
|
+
|
|
249
|
+
return JSONResponse({
|
|
250
|
+
"ok": True,
|
|
251
|
+
"overall_pct": overall_pct,
|
|
252
|
+
"total_claims": total,
|
|
253
|
+
"proved": proved_total,
|
|
254
|
+
"disproved": disproved_total,
|
|
255
|
+
"unproven": unproven_total,
|
|
256
|
+
"not_proved": not_proved_total,
|
|
257
|
+
"tiles": tiles,
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
@router.post("/api/assurance/run-dast")
|
|
262
|
+
async def run_dast_scan(request: Request):
|
|
263
|
+
"""Trigger a DAST scan and return results."""
|
|
264
|
+
root = request.app.state.root
|
|
265
|
+
try:
|
|
266
|
+
# Discover running app URL
|
|
267
|
+
import httpx
|
|
268
|
+
|
|
269
|
+
from patchi.core import memory as mem
|
|
270
|
+
from patchi.core.security.dast_agent import DASTAgent
|
|
271
|
+
target_url = None
|
|
272
|
+
for port in (8000, 3000, 5000, 8080, 1612):
|
|
273
|
+
url = f"http://127.0.0.1:{port}"
|
|
274
|
+
try:
|
|
275
|
+
r = httpx.get(url, timeout=2.0)
|
|
276
|
+
if r.status_code < 500:
|
|
277
|
+
target_url = url
|
|
278
|
+
break
|
|
279
|
+
except Exception:
|
|
280
|
+
continue
|
|
281
|
+
|
|
282
|
+
if not target_url:
|
|
283
|
+
return JSONResponse({
|
|
284
|
+
"ok": False,
|
|
285
|
+
"error": "No running app found. Start your app first (e.g. p web).",
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
# Run DAST agent
|
|
289
|
+
agent = DASTAgent(root)
|
|
290
|
+
result = agent.scan(target_url)
|
|
291
|
+
|
|
292
|
+
# Save results to memory
|
|
293
|
+
scan_results = mem.get_scan_results(root)
|
|
294
|
+
scan_results["DASTAgent"] = {
|
|
295
|
+
"tests_run": result.get("tests_run", 0),
|
|
296
|
+
"target_url": target_url,
|
|
297
|
+
"findings": result.get("findings", []),
|
|
298
|
+
}
|
|
299
|
+
mem.save_scan_results(scan_results, root)
|
|
300
|
+
|
|
301
|
+
return JSONResponse({
|
|
302
|
+
"ok": True,
|
|
303
|
+
"target_url": target_url,
|
|
304
|
+
"tests_run": result.get("tests_run", 0),
|
|
305
|
+
"findings_count": len(result.get("findings", [])),
|
|
306
|
+
"findings": result.get("findings", []),
|
|
307
|
+
})
|
|
308
|
+
except Exception as e:
|
|
309
|
+
return JSONResponse({"ok": False, "error": str(e)})
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
@router.get("/api/assurance/dast-screenshots")
|
|
313
|
+
async def list_dast_screenshots(request: Request):
|
|
314
|
+
"""List DAST screenshots for comparison view."""
|
|
315
|
+
root = request.app.state.root
|
|
316
|
+
evidence_dir = root / ".patchi" / "evidence" / "dast"
|
|
317
|
+
visual_dir = root / ".patchi" / "visual_baselines"
|
|
318
|
+
|
|
319
|
+
screenshots = []
|
|
320
|
+
|
|
321
|
+
# Scan evidence/dast/ directory
|
|
322
|
+
if evidence_dir.is_dir():
|
|
323
|
+
for f in sorted(evidence_dir.rglob("*.png"), key=lambda x: x.stat().st_mtime, reverse=True):
|
|
324
|
+
try:
|
|
325
|
+
screenshots.append({
|
|
326
|
+
"path": str(f.relative_to(root)),
|
|
327
|
+
"name": f.stem,
|
|
328
|
+
"timestamp": datetime.fromtimestamp(f.stat().st_mtime, UTC).isoformat(),
|
|
329
|
+
"size": f.stat().st_size,
|
|
330
|
+
"source": "dast",
|
|
331
|
+
})
|
|
332
|
+
except Exception as _exc:
|
|
333
|
+
_log.warning('list_dast_screenshots failed: %s', _exc)
|
|
334
|
+
|
|
335
|
+
# Scan visual_baselines/current/ directory
|
|
336
|
+
current_dir = visual_dir / "current"
|
|
337
|
+
if current_dir.is_dir():
|
|
338
|
+
for f in sorted(current_dir.rglob("*.png"), key=lambda x: x.stat().st_mtime, reverse=True):
|
|
339
|
+
try:
|
|
340
|
+
screenshots.append({
|
|
341
|
+
"path": str(f.relative_to(root)),
|
|
342
|
+
"name": f.stem,
|
|
343
|
+
"timestamp": datetime.fromtimestamp(f.stat().st_mtime, UTC).isoformat(),
|
|
344
|
+
"size": f.stat().st_size,
|
|
345
|
+
"source": "visual",
|
|
346
|
+
})
|
|
347
|
+
except Exception as _exc:
|
|
348
|
+
_log.warning('list_dast_screenshots failed: %s', _exc)
|
|
349
|
+
|
|
350
|
+
# Scan visual_baselines/baselines/ directory (saved baselines)
|
|
351
|
+
baselines_dir = visual_dir / "baselines"
|
|
352
|
+
baselines = []
|
|
353
|
+
if baselines_dir.is_dir():
|
|
354
|
+
for f in sorted(baselines_dir.glob("*.png")):
|
|
355
|
+
try:
|
|
356
|
+
meta_file = f.with_suffix(".json")
|
|
357
|
+
meta = {}
|
|
358
|
+
if meta_file.exists():
|
|
359
|
+
import json
|
|
360
|
+
meta = json.loads(meta_file.read_text(encoding="utf-8"))
|
|
361
|
+
baselines.append({
|
|
362
|
+
"path": str(f.relative_to(root)),
|
|
363
|
+
"name": f.stem,
|
|
364
|
+
"timestamp": meta.get("created_at", datetime.fromtimestamp(f.stat().st_mtime, UTC).isoformat()),
|
|
365
|
+
"size": f.stat().st_size,
|
|
366
|
+
})
|
|
367
|
+
except Exception as _exc:
|
|
368
|
+
_log.warning('list_dast_screenshots failed: %s', _exc)
|
|
369
|
+
|
|
370
|
+
return JSONResponse({
|
|
371
|
+
"ok": True,
|
|
372
|
+
"screenshots": screenshots[:50], # Limit to 50 most recent
|
|
373
|
+
"baselines": baselines[:20], # Limit to 20 baselines
|
|
374
|
+
"total": len(screenshots),
|
|
375
|
+
})
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
@router.get("/api/assurance/screenshot/{path:path}")
|
|
379
|
+
async def serve_screenshot(path: str, request: Request):
|
|
380
|
+
"""Serve a screenshot file."""
|
|
381
|
+
root = request.app.state.root
|
|
382
|
+
file_path = root / path
|
|
383
|
+
|
|
384
|
+
# Security: only allow serving from .patchi directory
|
|
385
|
+
try:
|
|
386
|
+
file_path.resolve().relative_to(root.resolve())
|
|
387
|
+
except ValueError:
|
|
388
|
+
return JSONResponse({"error": "Access denied"}, status_code=403)
|
|
389
|
+
|
|
390
|
+
if not file_path.exists() or not file_path.is_file():
|
|
391
|
+
return JSONResponse({"error": "File not found"}, status_code=404)
|
|
392
|
+
|
|
393
|
+
from starlette.responses import FileResponse
|
|
394
|
+
return FileResponse(file_path, media_type="image/png")
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
@router.post("/api/assurance/fix-headers")
|
|
398
|
+
async def fix_security_headers(request: Request):
|
|
399
|
+
"""Fix missing security headers based on DAST findings."""
|
|
400
|
+
root = request.app.state.root
|
|
401
|
+
try:
|
|
402
|
+
body = await request.json() if request.headers.get("content-type") == "application/json" else {}
|
|
403
|
+
except Exception:
|
|
404
|
+
body = {}
|
|
405
|
+
|
|
406
|
+
dry_run = body.get("dry_run", False)
|
|
407
|
+
|
|
408
|
+
# Get DAST findings from memory
|
|
409
|
+
from patchi.core import memory as mem
|
|
410
|
+
scan_results = mem.get_scan_results(root)
|
|
411
|
+
dast_findings = []
|
|
412
|
+
|
|
413
|
+
if "DASTAgent" in scan_results:
|
|
414
|
+
for f in scan_results["DASTAgent"].get("findings", []):
|
|
415
|
+
# Only include header-related findings
|
|
416
|
+
ftype = f.get("type", "")
|
|
417
|
+
if "header" in ftype or "csp" in ftype or "hsts" in ftype or "frame" in ftype:
|
|
418
|
+
dast_findings.append(f)
|
|
419
|
+
|
|
420
|
+
if not dast_findings:
|
|
421
|
+
return JSONResponse({
|
|
422
|
+
"ok": True,
|
|
423
|
+
"message": "No header findings to fix",
|
|
424
|
+
"fixed_count": 0,
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
# Apply fixes
|
|
428
|
+
from patchi.core.security.auto_fixer import fix_missing_headers
|
|
429
|
+
result = fix_missing_headers(root, dast_findings, apply=not dry_run)
|
|
430
|
+
|
|
431
|
+
return JSONResponse({
|
|
432
|
+
"ok": True,
|
|
433
|
+
"dry_run": dry_run,
|
|
434
|
+
"fixed_count": result.get("fixed_count", 0),
|
|
435
|
+
"fixes": result.get("fixes", []),
|
|
436
|
+
"skipped": result.get("skipped", []),
|
|
437
|
+
"file": result.get("file"),
|
|
438
|
+
"error": result.get("error"),
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
@router.get("/api/assurance/videos")
|
|
443
|
+
async def list_dast_videos(request: Request):
|
|
444
|
+
"""List DAST video recordings from all recording directories."""
|
|
445
|
+
root = request.app.state.root
|
|
446
|
+
videos = []
|
|
447
|
+
|
|
448
|
+
# Scan all possible video directories
|
|
449
|
+
video_dirs = [
|
|
450
|
+
root / ".patchi" / "evidence" / "video",
|
|
451
|
+
root / ".patchi" / "recordings",
|
|
452
|
+
root / ".patchi" / "recordings" / "raw",
|
|
453
|
+
]
|
|
454
|
+
|
|
455
|
+
seen = set()
|
|
456
|
+
for video_dir in video_dirs:
|
|
457
|
+
if not video_dir.is_dir():
|
|
458
|
+
continue
|
|
459
|
+
for ext in ("*.webm", "*.mp4"):
|
|
460
|
+
for f in sorted(video_dir.rglob(ext), key=lambda x: x.stat().st_mtime, reverse=True):
|
|
461
|
+
try:
|
|
462
|
+
resolved = f.resolve()
|
|
463
|
+
if resolved in seen:
|
|
464
|
+
continue
|
|
465
|
+
seen.add(resolved)
|
|
466
|
+
|
|
467
|
+
stat = f.stat()
|
|
468
|
+
videos.append({
|
|
469
|
+
"name": f.stem,
|
|
470
|
+
"filename": f.name,
|
|
471
|
+
"path": str(f.relative_to(root)),
|
|
472
|
+
"size_bytes": stat.st_size,
|
|
473
|
+
"size_kb": round(stat.st_size / 1024, 1),
|
|
474
|
+
"timestamp": datetime.fromtimestamp(stat.st_mtime, UTC).isoformat(),
|
|
475
|
+
"source_dir": str(video_dir.relative_to(root)),
|
|
476
|
+
})
|
|
477
|
+
except Exception as _exc:
|
|
478
|
+
_log.warning('list_dast_videos failed: %s', _exc)
|
|
479
|
+
|
|
480
|
+
return JSONResponse({
|
|
481
|
+
"ok": True,
|
|
482
|
+
"videos": videos[:50],
|
|
483
|
+
"total": len(videos),
|
|
484
|
+
"total_size_kb": round(sum(v["size_kb"] for v in videos), 1),
|
|
485
|
+
})
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
@router.get("/api/assurance/video/{filename}")
|
|
489
|
+
async def serve_dast_video(filename: str, request: Request):
|
|
490
|
+
"""Serve a video recording file for playback."""
|
|
491
|
+
root = request.app.state.root
|
|
492
|
+
|
|
493
|
+
video_dirs = [
|
|
494
|
+
root / ".patchi" / "evidence" / "video",
|
|
495
|
+
root / ".patchi" / "recordings",
|
|
496
|
+
root / ".patchi" / "recordings" / "raw",
|
|
497
|
+
]
|
|
498
|
+
|
|
499
|
+
for video_dir in video_dirs:
|
|
500
|
+
file_path = video_dir / filename
|
|
501
|
+
if file_path.exists() and file_path.is_file():
|
|
502
|
+
try:
|
|
503
|
+
file_path.resolve().relative_to(root.resolve())
|
|
504
|
+
except ValueError:
|
|
505
|
+
return JSONResponse({"error": "Access denied"}, status_code=403)
|
|
506
|
+
|
|
507
|
+
media_type = "video/webm" if file_path.suffix == ".webm" else "video/mp4"
|
|
508
|
+
from starlette.responses import FileResponse
|
|
509
|
+
return FileResponse(file_path, media_type=media_type)
|
|
510
|
+
|
|
511
|
+
return JSONResponse({"error": "Video not found"}, status_code=404)
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
@router.get("/api/assurance/trend")
|
|
515
|
+
async def assurance_trend(request: Request) -> JSONResponse:
|
|
516
|
+
"""Return coverage history for the trend line chart."""
|
|
517
|
+
root = request.app.state.root
|
|
518
|
+
from patchi.core.assurance.graph import AssuranceGraph
|
|
519
|
+
|
|
520
|
+
history = AssuranceGraph.load_coverage_history(root)
|
|
521
|
+
|
|
522
|
+
# Compute per-domain trend lines
|
|
523
|
+
domain_trends: dict[str, list[dict]] = {}
|
|
524
|
+
for snap in history:
|
|
525
|
+
ts = snap.get("timestamp", "")
|
|
526
|
+
for dom, dom_data in snap.get("by_domain", {}).items():
|
|
527
|
+
if dom not in domain_trends:
|
|
528
|
+
domain_trends[dom] = []
|
|
529
|
+
domain_trends[dom].append({
|
|
530
|
+
"t": ts,
|
|
531
|
+
"pct": dom_data.get("pct", 0),
|
|
532
|
+
"proved": dom_data.get("proved", 0),
|
|
533
|
+
"total": dom_data.get("total", 0),
|
|
534
|
+
})
|
|
535
|
+
|
|
536
|
+
return JSONResponse({
|
|
537
|
+
"ok": True,
|
|
538
|
+
"snapshots": history,
|
|
539
|
+
"count": len(history),
|
|
540
|
+
"domain_trends": domain_trends,
|
|
541
|
+
})
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Brain route — brain knowledge viewer."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter, Request
|
|
8
|
+
from fastapi.responses import HTMLResponse
|
|
9
|
+
from fastapi.templating import Jinja2Templates
|
|
10
|
+
|
|
11
|
+
router = APIRouter()
|
|
12
|
+
templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templates"))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@router.get("/brain", response_class=HTMLResponse)
|
|
16
|
+
async def brain(request: Request):
|
|
17
|
+
root = request.app.state.root
|
|
18
|
+
brain_md_path = root / ".patchi" / "BRAIN.md"
|
|
19
|
+
|
|
20
|
+
import re
|
|
21
|
+
|
|
22
|
+
content = ""
|
|
23
|
+
if brain_md_path.exists():
|
|
24
|
+
raw = brain_md_path.read_text(encoding="utf-8")
|
|
25
|
+
content = re.sub(r"<[^>]*>", "", raw)
|
|
26
|
+
else:
|
|
27
|
+
content = "*No brain data yet. Run `p scan` first.*"
|
|
28
|
+
|
|
29
|
+
return templates.TemplateResponse(
|
|
30
|
+
request,
|
|
31
|
+
"brain.html",
|
|
32
|
+
{
|
|
33
|
+
"request": request,
|
|
34
|
+
"brain_content": content,
|
|
35
|
+
},
|
|
36
|
+
)
|