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,330 @@
|
|
|
1
|
+
"""Findings route — findings list with filters + chain/intent tabs, DAST screenshots, history."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json as _json
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
import sqlite3
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from fastapi import APIRouter, Request
|
|
13
|
+
from fastapi.responses import HTMLResponse, JSONResponse
|
|
14
|
+
from fastapi.templating import Jinja2Templates
|
|
15
|
+
|
|
16
|
+
_log = logging.getLogger("patchi.web.routes.findings")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
router = APIRouter()
|
|
20
|
+
templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templates"))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _load_chain_intent(root: Path) -> dict:
|
|
24
|
+
"""Load persisted chain/intent data from the last scan."""
|
|
25
|
+
ci_path = root / ".patchi" / "chain_intent.json"
|
|
26
|
+
if ci_path.is_file():
|
|
27
|
+
try:
|
|
28
|
+
return _json.loads(ci_path.read_text(encoding="utf-8"))
|
|
29
|
+
except Exception as _exc:
|
|
30
|
+
_log.warning('_load_chain_intent failed: %s', _exc)
|
|
31
|
+
return {"chains": [], "intent_report": None}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _load_visual_regression(root: Path) -> dict:
|
|
35
|
+
"""Load visual regression screenshots, diffs, and baselines."""
|
|
36
|
+
evidence_dir = root / ".patchi" / "evidence" / "screenshots" / "visual_regression"
|
|
37
|
+
baseline_dir = root / ".patchi" / "visual_baselines"
|
|
38
|
+
|
|
39
|
+
screenshots = []
|
|
40
|
+
if evidence_dir.is_dir():
|
|
41
|
+
for f in sorted(
|
|
42
|
+
evidence_dir.glob("*.png"), key=lambda x: x.stat().st_mtime, reverse=True
|
|
43
|
+
):
|
|
44
|
+
try:
|
|
45
|
+
stat = f.stat()
|
|
46
|
+
is_diff = "_diff" in f.stem
|
|
47
|
+
screenshots.append(
|
|
48
|
+
{
|
|
49
|
+
"name": f.stem,
|
|
50
|
+
"filename": f.name,
|
|
51
|
+
"path": str(f.relative_to(root)),
|
|
52
|
+
"serve_url": f"/api/live-testing/screenshot/{f.name}",
|
|
53
|
+
"size_kb": round(stat.st_size / 1024, 1),
|
|
54
|
+
"is_diff": is_diff,
|
|
55
|
+
# Pair diff images with their source: "foo_diff" -> "foo"
|
|
56
|
+
"source_name": f.stem.replace("_diff", "") if is_diff else None,
|
|
57
|
+
}
|
|
58
|
+
)
|
|
59
|
+
except Exception as _exc:
|
|
60
|
+
_log.warning('_load_visual_regression failed: %s', _exc)
|
|
61
|
+
|
|
62
|
+
# Pair screenshots: for each diff, find its source screenshot
|
|
63
|
+
diff_map = {s["source_name"]: s for s in screenshots if s["is_diff"]}
|
|
64
|
+
pairs = []
|
|
65
|
+
seen = set()
|
|
66
|
+
for s in screenshots:
|
|
67
|
+
if s["is_diff"]:
|
|
68
|
+
continue # handled via source pairing
|
|
69
|
+
pair = {"source": s, "diff": diff_map.get(s["name"])}
|
|
70
|
+
pairs.append(pair)
|
|
71
|
+
seen.add(s["name"])
|
|
72
|
+
# Add orphaned diffs (source missing)
|
|
73
|
+
for name, diff in diff_map.items():
|
|
74
|
+
if name not in seen:
|
|
75
|
+
pairs.append({"source": None, "diff": diff})
|
|
76
|
+
|
|
77
|
+
baseline_count = 0
|
|
78
|
+
if baseline_dir.is_dir():
|
|
79
|
+
baseline_count = len(list(baseline_dir.rglob("*.png")))
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
"screenshots": screenshots,
|
|
83
|
+
"pairs": pairs,
|
|
84
|
+
"total": len([s for s in screenshots if not s["is_diff"]]),
|
|
85
|
+
"diffs": len([s for s in screenshots if s["is_diff"]]),
|
|
86
|
+
"baselines": baseline_count,
|
|
87
|
+
"total_size_kb": round(sum(s["size_kb"] for s in screenshots), 1),
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _load_dast_evidence(root: Path) -> dict[str, dict]:
|
|
92
|
+
"""Load DAST screenshot evidence from .patchi/evidence/dast/."""
|
|
93
|
+
evidence_dir = root / ".patchi" / "evidence" / "dast"
|
|
94
|
+
evidence_map: dict[str, dict] = {}
|
|
95
|
+
if not evidence_dir.is_dir():
|
|
96
|
+
return evidence_map
|
|
97
|
+
for entry in os.scandir(evidence_dir):
|
|
98
|
+
if not entry.is_file():
|
|
99
|
+
continue
|
|
100
|
+
name = entry.name
|
|
101
|
+
stem = Path(name).stem # e.g. "dast_xss_reflected_abc123"
|
|
102
|
+
# Strip the random suffix (last _XXXXX) to get the test name
|
|
103
|
+
parts = stem.rsplit("_", 1)
|
|
104
|
+
test_name = parts[0] if len(parts) > 1 else stem
|
|
105
|
+
# Extract category from test name: "dast_xss_reflected" -> "xss"
|
|
106
|
+
cat = ""
|
|
107
|
+
if test_name.startswith("dast_"):
|
|
108
|
+
cat = test_name[5:].split("_")[0]
|
|
109
|
+
evidence_map.setdefault(cat, {}).setdefault(test_name, []).append(
|
|
110
|
+
f"/evidence/dast/{name}"
|
|
111
|
+
)
|
|
112
|
+
return evidence_map
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@router.get("/findings", response_class=HTMLResponse)
|
|
116
|
+
async def findings(request: Request):
|
|
117
|
+
root = request.app.state.root
|
|
118
|
+
from patchi.core import memory as mem
|
|
119
|
+
|
|
120
|
+
mem.get_brain(root)
|
|
121
|
+
scan_results = mem.get_scan_results(root)
|
|
122
|
+
|
|
123
|
+
# Flatten findings from all agents and attach remediation
|
|
124
|
+
all_findings = []
|
|
125
|
+
try:
|
|
126
|
+
from patchi.core.security.remediation import get_remediation, get_remediation_confidence
|
|
127
|
+
|
|
128
|
+
has_remediation = True
|
|
129
|
+
except ImportError:
|
|
130
|
+
has_remediation = False
|
|
131
|
+
|
|
132
|
+
# Collect DAST-specific metadata and scan stats
|
|
133
|
+
dast_findings_count = 0
|
|
134
|
+
dast_tests_run = 0
|
|
135
|
+
dast_target = ""
|
|
136
|
+
scan_agent_count = len(scan_results)
|
|
137
|
+
|
|
138
|
+
for agent_name, data in scan_results.items():
|
|
139
|
+
# Accumulate DAST metadata from the DASTAgent result
|
|
140
|
+
if agent_name == "DASTAgent":
|
|
141
|
+
dast_findings_count = data.get("findings_count", 0)
|
|
142
|
+
dast_tests_run = data.get("tests_run", 0)
|
|
143
|
+
dast_target = data.get("target_url", "")
|
|
144
|
+
|
|
145
|
+
for f in data.get("findings", []):
|
|
146
|
+
f["agent"] = agent_name
|
|
147
|
+
# Flag DAST findings for template
|
|
148
|
+
if agent_name == "DASTAgent" or f.get("type", "").startswith("dast_"):
|
|
149
|
+
f["is_dast"] = True
|
|
150
|
+
# Attach remediation suggestion for non-chain findings
|
|
151
|
+
if has_remediation:
|
|
152
|
+
ftype = f.get("type", "")
|
|
153
|
+
rem = get_remediation(ftype)
|
|
154
|
+
if rem:
|
|
155
|
+
f["remediation"] = {
|
|
156
|
+
"action": rem.action,
|
|
157
|
+
"code_pattern": rem.code_pattern,
|
|
158
|
+
"auto_fixable": rem.auto_fixable,
|
|
159
|
+
"playbook_id": rem.playbook_id,
|
|
160
|
+
"confidence": round(get_remediation_confidence(ftype, root), 2),
|
|
161
|
+
}
|
|
162
|
+
all_findings.append(f)
|
|
163
|
+
|
|
164
|
+
# Sort by severity
|
|
165
|
+
sev_order = {"critical": 0, "high": 1, "medium": 2, "low": 3, "info": 4}
|
|
166
|
+
all_findings.sort(key=lambda f: (0 if f.get("is_dast") else 1, sev_order.get(f.get("severity", "info"), 5)))
|
|
167
|
+
|
|
168
|
+
# Load DAST screenshot evidence
|
|
169
|
+
dast_evidence = _load_dast_evidence(root)
|
|
170
|
+
|
|
171
|
+
# Attach screenshot paths to DAST findings
|
|
172
|
+
for f in all_findings:
|
|
173
|
+
if f.get("is_dast"):
|
|
174
|
+
ftype = f.get("type", "")
|
|
175
|
+
cat = ftype.replace("dast_", "") if ftype.startswith("dast_") else ""
|
|
176
|
+
if cat in dast_evidence:
|
|
177
|
+
# Get first screenshot for this category
|
|
178
|
+
screenshots = []
|
|
179
|
+
for _test_name, paths in dast_evidence[cat].items():
|
|
180
|
+
screenshots.extend(paths[:1]) # one per test type
|
|
181
|
+
f["screenshots"] = screenshots[:3] # max 3 per finding
|
|
182
|
+
else:
|
|
183
|
+
f["screenshots"] = []
|
|
184
|
+
|
|
185
|
+
# Load chain/intent data with remediation suggestions
|
|
186
|
+
ci = _load_chain_intent(root)
|
|
187
|
+
chains = ci.get("chains", [])
|
|
188
|
+
intent = ci.get("intent_report")
|
|
189
|
+
|
|
190
|
+
# Attach remediation to each chain step
|
|
191
|
+
try:
|
|
192
|
+
from patchi.core.security.remediation import get_remediation_for_step
|
|
193
|
+
|
|
194
|
+
for chain in chains:
|
|
195
|
+
chain["remediations"] = [
|
|
196
|
+
get_remediation_for_step(step) for step in chain.get("steps", [])
|
|
197
|
+
]
|
|
198
|
+
except ImportError:
|
|
199
|
+
pass
|
|
200
|
+
|
|
201
|
+
# Build chain-membership map: (file, line, type) -> list of chain indices
|
|
202
|
+
chain_members: dict[str, list[int]] = {}
|
|
203
|
+
for ci, chain in enumerate(chains):
|
|
204
|
+
for step in chain.get("steps", []):
|
|
205
|
+
key = f"{step.get('file', '')}:{step.get('line', 0)}:{step.get('type', '')}"
|
|
206
|
+
chain_members.setdefault(key, []).append(ci)
|
|
207
|
+
|
|
208
|
+
# Load charter violations
|
|
209
|
+
charter_violations: list[dict] = []
|
|
210
|
+
try:
|
|
211
|
+
from patchi.core.security.charter import check_all_violations, load_charter
|
|
212
|
+
charter = load_charter(root)
|
|
213
|
+
if charter.rules:
|
|
214
|
+
charter_violations = [v.to_dict() for v in check_all_violations(charter)]
|
|
215
|
+
except Exception as _exc:
|
|
216
|
+
_log.warning('findings failed: %s', _exc)
|
|
217
|
+
|
|
218
|
+
# Load scan history
|
|
219
|
+
scan_history = _load_scan_history(root)
|
|
220
|
+
|
|
221
|
+
# Load visual regression data
|
|
222
|
+
vr_data = _load_visual_regression(root)
|
|
223
|
+
|
|
224
|
+
return templates.TemplateResponse(
|
|
225
|
+
request,
|
|
226
|
+
"findings.html",
|
|
227
|
+
{
|
|
228
|
+
"request": request,
|
|
229
|
+
"findings": all_findings,
|
|
230
|
+
"total": len(all_findings),
|
|
231
|
+
"chains": chains,
|
|
232
|
+
"intent": intent,
|
|
233
|
+
"chain_members": chain_members,
|
|
234
|
+
"charter_violations": charter_violations,
|
|
235
|
+
"dast_findings_count": dast_findings_count,
|
|
236
|
+
"dast_tests_run": dast_tests_run,
|
|
237
|
+
"dast_target": dast_target,
|
|
238
|
+
"scan_agent_count": scan_agent_count,
|
|
239
|
+
"scan_history": scan_history,
|
|
240
|
+
"vr_pairs": vr_data["pairs"],
|
|
241
|
+
"vr_total": vr_data["total"],
|
|
242
|
+
"vr_diffs": vr_data["diffs"],
|
|
243
|
+
"vr_baselines": vr_data["baselines"],
|
|
244
|
+
"vr_size_kb": vr_data["total_size_kb"],
|
|
245
|
+
},
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
@router.get("/api/chains")
|
|
250
|
+
async def api_chains(request: Request):
|
|
251
|
+
"""JSON endpoint for chain/intent data."""
|
|
252
|
+
root = request.app.state.root
|
|
253
|
+
return JSONResponse(_load_chain_intent(root))
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
@router.get("/api/findings/dast-evidence")
|
|
257
|
+
async def api_dast_evidence(request: Request):
|
|
258
|
+
"""JSON endpoint for DAST screenshot evidence."""
|
|
259
|
+
root = request.app.state.root
|
|
260
|
+
return JSONResponse(_load_dast_evidence(root))
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _load_scan_history(root: Path, limit: int = 50) -> list[dict]:
|
|
264
|
+
"""Load scan history from the SQLite database."""
|
|
265
|
+
db_path = root / ".patchi" / "patchi_history.db"
|
|
266
|
+
if not db_path.is_file():
|
|
267
|
+
return []
|
|
268
|
+
try:
|
|
269
|
+
db = sqlite3.connect(str(db_path))
|
|
270
|
+
cursor = db.cursor()
|
|
271
|
+
cursor.execute(
|
|
272
|
+
"SELECT scan_id, timestamp, tool, findings_count, "
|
|
273
|
+
"severity_breakdown, duration_ms, health_score, metrics "
|
|
274
|
+
"FROM scan_history ORDER BY timestamp DESC LIMIT ?",
|
|
275
|
+
(limit,),
|
|
276
|
+
)
|
|
277
|
+
rows = cursor.fetchall()
|
|
278
|
+
db.close()
|
|
279
|
+
results = []
|
|
280
|
+
for row in rows:
|
|
281
|
+
entry = {
|
|
282
|
+
"scan_id": row[0],
|
|
283
|
+
"timestamp": row[1],
|
|
284
|
+
"tool": row[2],
|
|
285
|
+
"findings_count": row[3],
|
|
286
|
+
"severity_breakdown": _json.loads(row[4]) if row[4] else {},
|
|
287
|
+
"duration_ms": row[5],
|
|
288
|
+
"health_score": row[6],
|
|
289
|
+
"metrics": _json.loads(row[7]) if row[7] else {},
|
|
290
|
+
}
|
|
291
|
+
results.append(entry)
|
|
292
|
+
return results
|
|
293
|
+
except Exception:
|
|
294
|
+
return []
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
@router.get("/api/findings/history")
|
|
298
|
+
async def api_findings_history(request: Request, limit: int = 50):
|
|
299
|
+
"""JSON endpoint for scan history."""
|
|
300
|
+
root = request.app.state.root
|
|
301
|
+
return JSONResponse(_load_scan_history(root, limit))
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
@router.get("/api/findings/history/export")
|
|
305
|
+
async def api_findings_history_export(request: Request):
|
|
306
|
+
"""Export full scan history + current findings as JSON for CI/CD."""
|
|
307
|
+
root = request.app.state.root
|
|
308
|
+
history = _load_scan_history(root, limit=500)
|
|
309
|
+
|
|
310
|
+
# Also include current findings snapshot
|
|
311
|
+
from patchi.core import memory as mem
|
|
312
|
+
scan_results = mem.get_scan_results(root)
|
|
313
|
+
current_findings = []
|
|
314
|
+
for agent_name, data in scan_results.items():
|
|
315
|
+
for f in data.get("findings", []):
|
|
316
|
+
f["agent"] = agent_name
|
|
317
|
+
current_findings.append(f)
|
|
318
|
+
|
|
319
|
+
export = {
|
|
320
|
+
"exported_at": datetime.utcnow().isoformat() + "Z",
|
|
321
|
+
"project": str(root),
|
|
322
|
+
"history": history,
|
|
323
|
+
"current_findings": current_findings,
|
|
324
|
+
"summary": {
|
|
325
|
+
"total_scans": len(history),
|
|
326
|
+
"total_findings": len(current_findings),
|
|
327
|
+
"latest_health_score": history[0]["health_score"] if history else None,
|
|
328
|
+
},
|
|
329
|
+
}
|
|
330
|
+
return JSONResponse(export)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Guard route — hosted guard monitoring."""
|
|
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("/guard", response_class=HTMLResponse)
|
|
16
|
+
async def guard(request: Request):
|
|
17
|
+
root = request.app.state.root
|
|
18
|
+
from patchi.core import config as cfg
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
config = cfg.load(root)
|
|
22
|
+
except Exception:
|
|
23
|
+
config = {}
|
|
24
|
+
|
|
25
|
+
hosted = config.get("hosted", {})
|
|
26
|
+
|
|
27
|
+
return templates.TemplateResponse(
|
|
28
|
+
request,
|
|
29
|
+
"guard.html",
|
|
30
|
+
{
|
|
31
|
+
"request": request,
|
|
32
|
+
"enabled": hosted.get("enabled", False),
|
|
33
|
+
"log_path": hosted.get("log_path", ""),
|
|
34
|
+
},
|
|
35
|
+
)
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""History route — scan and fix history with filtering/sorting."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from fastapi import APIRouter, Query, Request
|
|
10
|
+
from fastapi.responses import HTMLResponse, JSONResponse
|
|
11
|
+
from fastapi.templating import Jinja2Templates
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger("patchi.web.history")
|
|
14
|
+
|
|
15
|
+
router = APIRouter()
|
|
16
|
+
templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templates"))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@router.get("/history", response_class=HTMLResponse)
|
|
20
|
+
async def history_page(request: Request):
|
|
21
|
+
root = request.app.state.root
|
|
22
|
+
from patchi.core import memory as mem
|
|
23
|
+
|
|
24
|
+
brain = mem.get_brain(root)
|
|
25
|
+
patches = mem.list_patches(root)
|
|
26
|
+
|
|
27
|
+
scan_history = []
|
|
28
|
+
try:
|
|
29
|
+
from patchi.core.security.history import patchi_get_history
|
|
30
|
+
|
|
31
|
+
scan_history = patchi_get_history(root, limit=100)
|
|
32
|
+
except Exception as e:
|
|
33
|
+
logger.warning("Failed to fetch scan history: %s", e)
|
|
34
|
+
|
|
35
|
+
# Extract unique tools for filter dropdown
|
|
36
|
+
tools = sorted({s.get("tool", "unknown") for s in scan_history})
|
|
37
|
+
|
|
38
|
+
return templates.TemplateResponse(
|
|
39
|
+
request,
|
|
40
|
+
"history.html",
|
|
41
|
+
{
|
|
42
|
+
"request": request,
|
|
43
|
+
"patches": patches,
|
|
44
|
+
"scan_history": scan_history,
|
|
45
|
+
"scan_history_json": json.dumps(scan_history),
|
|
46
|
+
"tools": tools,
|
|
47
|
+
"file_count": brain.get("file_count", 0),
|
|
48
|
+
"route_count": brain.get("route_count", 0),
|
|
49
|
+
"framework": brain.get("framework", "Unknown"),
|
|
50
|
+
},
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@router.get("/api/history/filter")
|
|
55
|
+
async def filter_history(
|
|
56
|
+
request: Request,
|
|
57
|
+
date_from: str | None = Query(None, description="Start date (YYYY-MM-DD)"),
|
|
58
|
+
date_to: str | None = Query(None, description="End date (YYYY-MM-DD)"),
|
|
59
|
+
severity: str | None = Query(None, description="Comma-separated severities"),
|
|
60
|
+
tool: str | None = Query(None, description="Filter by tool name"),
|
|
61
|
+
sort_by: str = Query("timestamp", description="Sort field"),
|
|
62
|
+
sort_dir: str = Query("desc", description="Sort direction (asc/desc)"),
|
|
63
|
+
limit: int = Query(50, description="Max results"),
|
|
64
|
+
):
|
|
65
|
+
"""Filter and sort scan history."""
|
|
66
|
+
root = request.app.state.root
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
from patchi.core.security.history import patchi_get_history
|
|
70
|
+
|
|
71
|
+
all_history = patchi_get_history(root, limit=200)
|
|
72
|
+
except Exception as e:
|
|
73
|
+
logger.warning("Failed to fetch scan history: %s", e)
|
|
74
|
+
all_history = []
|
|
75
|
+
|
|
76
|
+
# Apply filters
|
|
77
|
+
filtered = all_history
|
|
78
|
+
|
|
79
|
+
# Date range filter
|
|
80
|
+
if date_from:
|
|
81
|
+
filtered = [s for s in filtered if s.get("timestamp", "") >= date_from]
|
|
82
|
+
if date_to:
|
|
83
|
+
# Include the full end day
|
|
84
|
+
filtered = [s for s in filtered if s.get("timestamp", "") <= date_to + "T23:59:59"]
|
|
85
|
+
|
|
86
|
+
# Severity filter (check if any matching severity has count > 0)
|
|
87
|
+
if severity:
|
|
88
|
+
sev_list = [s.strip() for s in severity.split(",") if s.strip()]
|
|
89
|
+
if sev_list:
|
|
90
|
+
filtered = [
|
|
91
|
+
s for s in filtered
|
|
92
|
+
if any(
|
|
93
|
+
s.get("severity_breakdown", {}).get(sev, 0) > 0
|
|
94
|
+
for sev in sev_list
|
|
95
|
+
)
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
# Tool filter
|
|
99
|
+
if tool and tool != "all":
|
|
100
|
+
filtered = [s for s in filtered if s.get("tool", "") == tool]
|
|
101
|
+
|
|
102
|
+
# Sorting
|
|
103
|
+
reverse = sort_dir == "desc"
|
|
104
|
+
if sort_by == "timestamp":
|
|
105
|
+
filtered.sort(key=lambda s: s.get("timestamp", ""), reverse=reverse)
|
|
106
|
+
elif sort_by == "findings":
|
|
107
|
+
filtered.sort(key=lambda s: s.get("findings_count", 0), reverse=reverse)
|
|
108
|
+
elif sort_by == "health":
|
|
109
|
+
filtered.sort(key=lambda s: s.get("health_score", 0), reverse=reverse)
|
|
110
|
+
elif sort_by == "duration":
|
|
111
|
+
filtered.sort(key=lambda s: s.get("duration_ms", 0), reverse=reverse)
|
|
112
|
+
elif sort_by == "severity":
|
|
113
|
+
# Sort by total critical+high findings
|
|
114
|
+
def _sev_score(s):
|
|
115
|
+
sb = s.get("severity_breakdown", {})
|
|
116
|
+
return sb.get("critical", 0) * 100 + sb.get("high", 0) * 10
|
|
117
|
+
filtered.sort(key=_sev_score, reverse=reverse)
|
|
118
|
+
|
|
119
|
+
# Limit
|
|
120
|
+
filtered = filtered[:limit]
|
|
121
|
+
|
|
122
|
+
return JSONResponse({
|
|
123
|
+
"total": len(all_history),
|
|
124
|
+
"filtered": len(filtered),
|
|
125
|
+
"results": filtered,
|
|
126
|
+
})
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Public marketing landing — Snyk × Linear style, standalone.
|
|
2
|
+
|
|
3
|
+
Not the localhost app dashboard (/). This is the external-facing
|
|
4
|
+
story: hero + 3-up + Brain + Council + Live Tests + CLI parity + freemium.
|
|
5
|
+
Shares tokens with the app but is a separate band layout for marketing.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from fastapi import APIRouter, Request
|
|
14
|
+
from fastapi.responses import HTMLResponse
|
|
15
|
+
from starlette.templating import Jinja2Templates
|
|
16
|
+
|
|
17
|
+
router = APIRouter()
|
|
18
|
+
|
|
19
|
+
templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templates"))
|
|
20
|
+
templates.env.filters["tojson"] = lambda v: json.dumps(v)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@router.get("/landing", response_class=HTMLResponse)
|
|
24
|
+
@router.get("/welcome", response_class=HTMLResponse)
|
|
25
|
+
async def landing(request: Request):
|
|
26
|
+
root = request.app.state.root
|
|
27
|
+
from patchi.core import memory as mem
|
|
28
|
+
from patchi.core.health import compute as compute_health
|
|
29
|
+
|
|
30
|
+
brain = mem.get_brain(root)
|
|
31
|
+
hs = compute_health(root)
|
|
32
|
+
|
|
33
|
+
# Stats for hero proof + metrics
|
|
34
|
+
scan_results = mem.get_scan_results(root)
|
|
35
|
+
total_findings = sum(len(v.get("findings", [])) for v in scan_results.values() if isinstance(v, dict))
|
|
36
|
+
|
|
37
|
+
return templates.TemplateResponse(
|
|
38
|
+
request,
|
|
39
|
+
"landing.html",
|
|
40
|
+
{
|
|
41
|
+
"request": request,
|
|
42
|
+
"health_score": hs.total,
|
|
43
|
+
"health_grade": hs.grade,
|
|
44
|
+
"file_count": brain.get("file_count", 0),
|
|
45
|
+
"route_count": brain.get("route_count", 0),
|
|
46
|
+
"framework": brain.get("framework", "Unknown"),
|
|
47
|
+
"languages": brain.get("languages", {}),
|
|
48
|
+
"recent_findings": total_findings,
|
|
49
|
+
"active_domains": brain.get("active_security_domains", [])[:6],
|
|
50
|
+
"project_purpose": brain.get("project_purpose", "AI-powered code security & quality agent colony"),
|
|
51
|
+
},
|
|
52
|
+
)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Live Testing page — browser automation, screenshots, stress tests."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter, Request
|
|
8
|
+
from starlette.templating import Jinja2Templates
|
|
9
|
+
|
|
10
|
+
router = APIRouter()
|
|
11
|
+
_templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templates"))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@router.get("/live-tests")
|
|
15
|
+
async def live_tests_page(request: Request):
|
|
16
|
+
"""Render the live testing dashboard."""
|
|
17
|
+
return _templates.TemplateResponse("live_testing.html", {"request": request})
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@router.get("/live-testing")
|
|
21
|
+
async def live_testing_redirect():
|
|
22
|
+
"""Legacy path — forwards to the unified live tests page."""
|
|
23
|
+
from fastapi.responses import RedirectResponse
|
|
24
|
+
return RedirectResponse(url="/live-tests", status_code=307)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Review route — patch review with diffs."""
|
|
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("/review", response_class=HTMLResponse)
|
|
16
|
+
async def review(request: Request):
|
|
17
|
+
root = request.app.state.root
|
|
18
|
+
from patchi.core import memory as mem
|
|
19
|
+
|
|
20
|
+
raw_patches = mem.read("patches", root)
|
|
21
|
+
|
|
22
|
+
# Enrich each patch with diff content if available
|
|
23
|
+
patches = []
|
|
24
|
+
for p in raw_patches:
|
|
25
|
+
if isinstance(p, dict):
|
|
26
|
+
# Build a unified diff string from FileChange objects if not already present
|
|
27
|
+
if not p.get("diff") and p.get("changes"):
|
|
28
|
+
diff_lines = []
|
|
29
|
+
for change in p["changes"]:
|
|
30
|
+
path = change.get("path", "unknown")
|
|
31
|
+
before = change.get("before", "")
|
|
32
|
+
proposed = change.get("proposed", "")
|
|
33
|
+
if before or proposed:
|
|
34
|
+
import difflib
|
|
35
|
+
|
|
36
|
+
diff = list(
|
|
37
|
+
difflib.unified_diff(
|
|
38
|
+
before.splitlines(keepends=True) if before else [],
|
|
39
|
+
proposed.splitlines(keepends=True) if proposed else [],
|
|
40
|
+
fromfile=f"a/{path}",
|
|
41
|
+
tofile=f"b/{path}",
|
|
42
|
+
lineterm="",
|
|
43
|
+
)
|
|
44
|
+
)
|
|
45
|
+
diff_lines.extend(diff)
|
|
46
|
+
p["diff"] = "\n".join(diff_lines) if diff_lines else ""
|
|
47
|
+
patches.append(p)
|
|
48
|
+
|
|
49
|
+
return templates.TemplateResponse(
|
|
50
|
+
request,
|
|
51
|
+
"review.html",
|
|
52
|
+
{
|
|
53
|
+
"request": request,
|
|
54
|
+
"patches": patches,
|
|
55
|
+
"total": len(patches),
|
|
56
|
+
},
|
|
57
|
+
)
|