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.
Files changed (496) hide show
  1. patchi/__init__.py +1 -0
  2. patchi/__main__.py +13 -0
  3. patchi/cli/__init__.py +0 -0
  4. patchi/cli/command_families.py +235 -0
  5. patchi/cli/commands/__init__.py +0 -0
  6. patchi/cli/commands/access_cmd.py +65 -0
  7. patchi/cli/commands/agent_stats_cmd.py +273 -0
  8. patchi/cli/commands/agents_cmd.py +187 -0
  9. patchi/cli/commands/ai_cmd.py +308 -0
  10. patchi/cli/commands/assure_cmd.py +538 -0
  11. patchi/cli/commands/audit_cmd.py +286 -0
  12. patchi/cli/commands/auto_cmd.py +110 -0
  13. patchi/cli/commands/blame_cmd.py +53 -0
  14. patchi/cli/commands/chains_cmd.py +327 -0
  15. patchi/cli/commands/charter_cmd.py +173 -0
  16. patchi/cli/commands/chat_cmd.py +597 -0
  17. patchi/cli/commands/check_cmd.py +232 -0
  18. patchi/cli/commands/cleanup_cmd.py +328 -0
  19. patchi/cli/commands/cockpit_cmd.py +208 -0
  20. patchi/cli/commands/commands_cmd.py +160 -0
  21. patchi/cli/commands/cross_repo_cmd.py +113 -0
  22. patchi/cli/commands/deps_cmd.py +110 -0
  23. patchi/cli/commands/dev_check_cmd.py +342 -0
  24. patchi/cli/commands/dev_cmd.py +445 -0
  25. patchi/cli/commands/doctor_cmd.py +457 -0
  26. patchi/cli/commands/findings_cmd.py +187 -0
  27. patchi/cli/commands/fix_cmd.py +648 -0
  28. patchi/cli/commands/fix_review_cmd.py +273 -0
  29. patchi/cli/commands/heatmap_cmd.py +62 -0
  30. patchi/cli/commands/help_cmd.py +159 -0
  31. patchi/cli/commands/hosted_cmd.py +980 -0
  32. patchi/cli/commands/init.py +456 -0
  33. patchi/cli/commands/key_cmd.py +571 -0
  34. patchi/cli/commands/learn_cmd.py +140 -0
  35. patchi/cli/commands/link_cmd.py +104 -0
  36. patchi/cli/commands/log_cmd.py +57 -0
  37. patchi/cli/commands/memory_cmd.py +215 -0
  38. patchi/cli/commands/model_cmd.py +207 -0
  39. patchi/cli/commands/notify_cmd.py +213 -0
  40. patchi/cli/commands/notify_prompts.py +29 -0
  41. patchi/cli/commands/patch_cmd.py +231 -0
  42. patchi/cli/commands/plan_cmd.py +128 -0
  43. patchi/cli/commands/plugins_cmd.py +307 -0
  44. patchi/cli/commands/queue_cmd.py +152 -0
  45. patchi/cli/commands/quick_cmd.py +159 -0
  46. patchi/cli/commands/ready_cmd.py +292 -0
  47. patchi/cli/commands/report_cmd.py +416 -0
  48. patchi/cli/commands/restrict_cmd.py +127 -0
  49. patchi/cli/commands/review_cmd.py +225 -0
  50. patchi/cli/commands/rules_cmd.py +233 -0
  51. patchi/cli/commands/scan_cmd.py +1908 -0
  52. patchi/cli/commands/settings_cmd.py +45 -0
  53. patchi/cli/commands/status_cmd.py +679 -0
  54. patchi/cli/commands/test_cmd.py +1052 -0
  55. patchi/cli/commands/trend_cmd.py +296 -0
  56. patchi/cli/commands/undo_cmd.py +235 -0
  57. patchi/cli/commands/update_cmd.py +345 -0
  58. patchi/cli/commands/verify_cmd.py +56 -0
  59. patchi/cli/commands/vr_cmd.py +440 -0
  60. patchi/cli/commands/watch_cmd.py +304 -0
  61. patchi/cli/commands/web_cmd.py +114 -0
  62. patchi/cli/console.py +101 -0
  63. patchi/cli/display/__init__.py +1 -0
  64. patchi/cli/display/live_audit.py +269 -0
  65. patchi/cli/display/live_progress.py +169 -0
  66. patchi/cli/framework.py +241 -0
  67. patchi/cli/logo.py +128 -0
  68. patchi/cli/main.py +222 -0
  69. patchi/cli/registry.py +1171 -0
  70. patchi/cli/style.py +68 -0
  71. patchi/cli/themes.py +187 -0
  72. patchi/cli/ux.py +444 -0
  73. patchi/core/__init__.py +0 -0
  74. patchi/core/agents/__init__.py +0 -0
  75. patchi/core/agents/api_fuzzer_agent.py +141 -0
  76. patchi/core/agents/attack_agent.py +242 -0
  77. patchi/core/agents/base.py +1012 -0
  78. patchi/core/agents/bug_predictor_agent.py +58 -0
  79. patchi/core/agents/build_tool_validator.py +150 -0
  80. patchi/core/agents/cache.py +192 -0
  81. patchi/core/agents/chaos_agent.py +133 -0
  82. patchi/core/agents/cicd_generator.py +240 -0
  83. patchi/core/agents/comment_scanner.py +280 -0
  84. patchi/core/agents/console_logging_agent.py +156 -0
  85. patchi/core/agents/coordinator.py +858 -0
  86. patchi/core/agents/core_scanner.py +269 -0
  87. patchi/core/agents/coverage_prioritizer.py +181 -0
  88. patchi/core/agents/dead_code_hygiene.py +296 -0
  89. patchi/core/agents/dead_code_scanner.py +692 -0
  90. patchi/core/agents/dependency_scanner.py +725 -0
  91. patchi/core/agents/doc_claim_agent.py +573 -0
  92. patchi/core/agents/duplicate_scanner.py +568 -0
  93. patchi/core/agents/env_scanner.py +410 -0
  94. patchi/core/agents/feature_flag_agent.py +89 -0
  95. patchi/core/agents/frontend_framework_agent.py +133 -0
  96. patchi/core/agents/goroutine_agent.py +82 -0
  97. patchi/core/agents/governor.py +1972 -0
  98. patchi/core/agents/i18n_agent.py +95 -0
  99. patchi/core/agents/license_compliance.py +247 -0
  100. patchi/core/agents/linking_agent.py +251 -0
  101. patchi/core/agents/memory_profiler_agent.py +102 -0
  102. patchi/core/agents/modernization_agent.py +84 -0
  103. patchi/core/agents/mutation_agent.py +174 -0
  104. patchi/core/agents/nl_query_agent.py +74 -0
  105. patchi/core/agents/promise_rejection_agent.py +100 -0
  106. patchi/core/agents/refactoring_agent.py +286 -0
  107. patchi/core/agents/resource_leak_agent.py +91 -0
  108. patchi/core/agents/route_graph_scanner.py +951 -0
  109. patchi/core/agents/run_agent.py +106 -0
  110. patchi/core/agents/rust_unwrap_agent.py +100 -0
  111. patchi/core/agents/sbom_generator.py +284 -0
  112. patchi/core/agents/scanners.py +67 -0
  113. patchi/core/agents/side/__init__.py +1 -0
  114. patchi/core/agents/side/build_agent.py +65 -0
  115. patchi/core/agents/side/format_agent.py +50 -0
  116. patchi/core/agents/side/install_agent.py +63 -0
  117. patchi/core/agents/side_file_scanner.py +549 -0
  118. patchi/core/agents/snapshot_drift_detector.py +146 -0
  119. patchi/core/agents/spa_route_inventory.py +203 -0
  120. patchi/core/agents/test_scanner.py +442 -0
  121. patchi/core/agents/tool_health.py +109 -0
  122. patchi/core/agents/tool_runner.py +162 -0
  123. patchi/core/agents/type_scanner.py +168 -0
  124. patchi/core/agents/ui_scanner.py +558 -0
  125. patchi/core/ai/__init__.py +20 -0
  126. patchi/core/ai/agent_audit.py +153 -0
  127. patchi/core/ai/agent_profiler.py +277 -0
  128. patchi/core/ai/client.py +499 -0
  129. patchi/core/ai/cost_tracker.py +107 -0
  130. patchi/core/ai/model_router.py +318 -0
  131. patchi/core/ai/orchestrator.py +625 -0
  132. patchi/core/ai/prompts.py +681 -0
  133. patchi/core/ai/smart.py +302 -0
  134. patchi/core/ai/tool_executor.py +24 -0
  135. patchi/core/ai/tools/__init__.py +10 -0
  136. patchi/core/ai/tools/executor.py +621 -0
  137. patchi/core/ai/tools/realize.py +1160 -0
  138. patchi/core/ai/tools/registry.py +1577 -0
  139. patchi/core/assurance/__init__.py +33 -0
  140. patchi/core/assurance/builder.py +211 -0
  141. patchi/core/assurance/graph.py +342 -0
  142. patchi/core/assurance/invariants.py +168 -0
  143. patchi/core/atomic.py +61 -0
  144. patchi/core/attackers/__init__.py +17 -0
  145. patchi/core/attackers/api_abuse.py +89 -0
  146. patchi/core/attackers/auth_bypass.py +73 -0
  147. patchi/core/attackers/base.py +91 -0
  148. patchi/core/attackers/business_logic.py +88 -0
  149. patchi/core/attackers/planner.py +108 -0
  150. patchi/core/attackers/priv_escalation.py +88 -0
  151. patchi/core/attackers/recon.py +87 -0
  152. patchi/core/brain/__init__.py +0 -0
  153. patchi/core/brain/ast_utils/__init__.py +81 -0
  154. patchi/core/brain/ast_utils/assignments.py +219 -0
  155. patchi/core/brain/ast_utils/calls.py +139 -0
  156. patchi/core/brain/ast_utils/config.py +132 -0
  157. patchi/core/brain/ast_utils/control_flow.py +74 -0
  158. patchi/core/brain/ast_utils/dead_symbols.py +205 -0
  159. patchi/core/brain/ast_utils/decorators.py +108 -0
  160. patchi/core/brain/ast_utils/helpers.py +144 -0
  161. patchi/core/brain/ast_utils/imports.py +169 -0
  162. patchi/core/brain/ast_utils/scan.py +143 -0
  163. patchi/core/brain/ast_utils/taint.py +310 -0
  164. patchi/core/brain/audit.py +379 -0
  165. patchi/core/brain/baseline.py +195 -0
  166. patchi/core/brain/blast_radius.py +312 -0
  167. patchi/core/brain/body_tags.py +144 -0
  168. patchi/core/brain/brain.py +1161 -0
  169. patchi/core/brain/brain_context.py +393 -0
  170. patchi/core/brain/brain_watcher.py +340 -0
  171. patchi/core/brain/charter.py +473 -0
  172. patchi/core/brain/classifier.py +214 -0
  173. patchi/core/brain/cockpit.py +350 -0
  174. patchi/core/brain/code_query.py +710 -0
  175. patchi/core/brain/contract.py +889 -0
  176. patchi/core/brain/contract_diff.py +155 -0
  177. patchi/core/brain/council.py +800 -0
  178. patchi/core/brain/cross_repo.py +104 -0
  179. patchi/core/brain/dead_code_tools.py +157 -0
  180. patchi/core/brain/doc_validator.py +197 -0
  181. patchi/core/brain/domain_activator.py +2183 -0
  182. patchi/core/brain/enriched_context.py +220 -0
  183. patchi/core/brain/file_corpus.py +220 -0
  184. patchi/core/brain/framework.py +953 -0
  185. patchi/core/brain/freshness.py +273 -0
  186. patchi/core/brain/git_aware.py +206 -0
  187. patchi/core/brain/heatmap.py +104 -0
  188. patchi/core/brain/ignore_parser.py +153 -0
  189. patchi/core/brain/import_graph.py +486 -0
  190. patchi/core/brain/languages.py +397 -0
  191. patchi/core/brain/layered_brain.py +419 -0
  192. patchi/core/brain/learning.py +229 -0
  193. patchi/core/brain/orphaned_endpoints.py +313 -0
  194. patchi/core/brain/personas/__init__.py +648 -0
  195. patchi/core/brain/personas/bad_user.py +165 -0
  196. patchi/core/brain/personas/base.py +349 -0
  197. patchi/core/brain/proactive.py +807 -0
  198. patchi/core/brain/project_context.py +121 -0
  199. patchi/core/brain/project_reader.py +521 -0
  200. patchi/core/brain/reasoning.py +269 -0
  201. patchi/core/brain/route_detector/__init__.py +15 -0
  202. patchi/core/brain/route_detector/base.py +31 -0
  203. patchi/core/brain/route_detector/csharp.py +209 -0
  204. patchi/core/brain/route_detector/file_based.py +81 -0
  205. patchi/core/brain/route_detector/go.py +152 -0
  206. patchi/core/brain/route_detector/java.py +195 -0
  207. patchi/core/brain/route_detector/javascript.py +197 -0
  208. patchi/core/brain/route_detector/php.py +158 -0
  209. patchi/core/brain/route_detector/python.py +193 -0
  210. patchi/core/brain/route_detector/registry.py +25 -0
  211. patchi/core/brain/route_detector/ruby.py +134 -0
  212. patchi/core/brain/route_detector/rust.py +216 -0
  213. patchi/core/brain/route_detector/swift.py +118 -0
  214. patchi/core/brain/route_mapper.py +265 -0
  215. patchi/core/brain/scanner.py +2136 -0
  216. patchi/core/brain/secrets.py +280 -0
  217. patchi/core/brain/symbol_graph.py +1955 -0
  218. patchi/core/brain/team.py +28 -0
  219. patchi/core/brain/trace_log.py +132 -0
  220. patchi/core/brain/type_checker/__init__.py +65 -0
  221. patchi/core/brain/type_checker/base.py +60 -0
  222. patchi/core/brain/type_checker/csharp.py +59 -0
  223. patchi/core/brain/type_checker/dart.py +59 -0
  224. patchi/core/brain/type_checker/go.py +81 -0
  225. patchi/core/brain/type_checker/java.py +75 -0
  226. patchi/core/brain/type_checker/kotlin.py +75 -0
  227. patchi/core/brain/type_checker/php.py +88 -0
  228. patchi/core/brain/type_checker/python.py +87 -0
  229. patchi/core/brain/type_checker/rust.py +81 -0
  230. patchi/core/brain/type_checker/swift.py +59 -0
  231. patchi/core/brain/type_checker/typescript.py +298 -0
  232. patchi/core/brain/understander.py +130 -0
  233. patchi/core/brain/verify.py +379 -0
  234. patchi/core/campaigns/__init__.py +14 -0
  235. patchi/core/campaigns/base.py +62 -0
  236. patchi/core/campaigns/data_flow_abuse.py +153 -0
  237. patchi/core/campaigns/orchestrator.py +140 -0
  238. patchi/core/campaigns/state_transitions.py +178 -0
  239. patchi/core/ci_bundle.py +104 -0
  240. patchi/core/config.py +311 -0
  241. patchi/core/constants.py +245 -0
  242. patchi/core/debug/__init__.py +25 -0
  243. patchi/core/debug/adapters/__init__.py +11 -0
  244. patchi/core/debug/adapters/codelldb.py +379 -0
  245. patchi/core/debug/adapters/node.py +222 -0
  246. patchi/core/debug/adapters/powershell.py +210 -0
  247. patchi/core/debug/adapters/python.py +244 -0
  248. patchi/core/debug/capture.py +210 -0
  249. patchi/core/debug/dap_client.py +268 -0
  250. patchi/core/detector/__init__.py +0 -0
  251. patchi/core/detector/audit.py +194 -0
  252. patchi/core/detector/bus.py +226 -0
  253. patchi/core/detector/dispatcher.py +275 -0
  254. patchi/core/detector/event.py +254 -0
  255. patchi/core/detector/sigma_engine.py +378 -0
  256. patchi/core/detector/triage.py +289 -0
  257. patchi/core/export/sarif.py +113 -0
  258. patchi/core/fix/__init__.py +17 -0
  259. patchi/core/fix/applier.py +574 -0
  260. patchi/core/fix/base.py +510 -0
  261. patchi/core/fix/code_fixer.py +169 -0
  262. patchi/core/fix/dead_code_remover.py +131 -0
  263. patchi/core/fix/fix_agents.py +775 -0
  264. patchi/core/fix/patch.py +444 -0
  265. patchi/core/fix/risk_gate.py +383 -0
  266. patchi/core/fix/security_fixer.py +93 -0
  267. patchi/core/fix/verify_loop.py +204 -0
  268. patchi/core/fuzz/__init__.py +8 -0
  269. patchi/core/fuzz/corpus.py +137 -0
  270. patchi/core/fuzz/input_fuzzer.py +256 -0
  271. patchi/core/fuzz/sequence_fuzzer.py +162 -0
  272. patchi/core/fuzz/state_fuzzer.py +236 -0
  273. patchi/core/health.py +407 -0
  274. patchi/core/hosted/__init__.py +1 -0
  275. patchi/core/hosted/anomaly.py +298 -0
  276. patchi/core/hosted/audit_log.py +102 -0
  277. patchi/core/hosted/compliance_report.py +148 -0
  278. patchi/core/hosted/ip_reputation.py +169 -0
  279. patchi/core/hosted/log_parsers.py +251 -0
  280. patchi/core/hosted/tokens.py +127 -0
  281. patchi/core/hosted/watchlist.py +164 -0
  282. patchi/core/hosted/webhooks.py +156 -0
  283. patchi/core/memory.py +437 -0
  284. patchi/core/notifications/__init__.py +18 -0
  285. patchi/core/notifications/channels.py +192 -0
  286. patchi/core/notifications/digest.py +141 -0
  287. patchi/core/notifications/escalation.py +158 -0
  288. patchi/core/notifications/notifier.py +182 -0
  289. patchi/core/notifications/quiet_hours.py +69 -0
  290. patchi/core/plugins/__init__.py +48 -0
  291. patchi/core/plugins/analyzer.py +326 -0
  292. patchi/core/plugins/analyzers/__init__.py +6 -0
  293. patchi/core/plugins/analyzers/dead_code_analyzer.py +120 -0
  294. patchi/core/plugins/analyzers/secret_scanner.py +158 -0
  295. patchi/core/plugins/analyzers/todo_scanner.py +115 -0
  296. patchi/core/plugins/registry.py +335 -0
  297. patchi/core/queue.py +375 -0
  298. patchi/core/reliability/__init__.py +13 -0
  299. patchi/core/reliability/chaos.py +163 -0
  300. patchi/core/reliability/fault_injector.py +172 -0
  301. patchi/core/reliability/idempotency.py +128 -0
  302. patchi/core/reliability/recovery.py +147 -0
  303. patchi/core/runtime/__init__.py +5 -0
  304. patchi/core/runtime/tracer.py +230 -0
  305. patchi/core/scan_bus.py +156 -0
  306. patchi/core/security/__init__.py +1 -0
  307. patchi/core/security/ai_validator.py +462 -0
  308. patchi/core/security/app_mapper.py +216 -0
  309. patchi/core/security/app_profile.py +464 -0
  310. patchi/core/security/attack_feedback.py +141 -0
  311. patchi/core/security/attack_scenarios/__init__.py +1 -0
  312. patchi/core/security/attack_tree.py +176 -0
  313. patchi/core/security/auth_audit_agent.py +344 -0
  314. patchi/core/security/authz_agent.py +602 -0
  315. patchi/core/security/auto_fix_proactive.py +620 -0
  316. patchi/core/security/auto_fixer.py +660 -0
  317. patchi/core/security/auto_ticket.py +53 -0
  318. patchi/core/security/bandit_agent.py +152 -0
  319. patchi/core/security/blast_radius.py +226 -0
  320. patchi/core/security/browser_tester.py +333 -0
  321. patchi/core/security/business_logic_agent.py +263 -0
  322. patchi/core/security/catch_block_auditor.py +395 -0
  323. patchi/core/security/cdn_cache_agent.py +357 -0
  324. patchi/core/security/chain_analyzer.py +321 -0
  325. patchi/core/security/chain_to_assurance.py +168 -0
  326. patchi/core/security/charter.py +510 -0
  327. patchi/core/security/cloud_waf_detector.py +295 -0
  328. patchi/core/security/codeql_agent.py +233 -0
  329. patchi/core/security/compliance_agent.py +348 -0
  330. patchi/core/security/confidence_gate.py +423 -0
  331. patchi/core/security/container_scanner.py +282 -0
  332. patchi/core/security/crypto_agent.py +378 -0
  333. patchi/core/security/cve_monitor.py +262 -0
  334. patchi/core/security/dast_agent.py +865 -0
  335. patchi/core/security/defectdojo.py +238 -0
  336. patchi/core/security/defenders/__init__.py +75 -0
  337. patchi/core/security/defenders/auth_middleware.py +115 -0
  338. patchi/core/security/defenders/base.py +97 -0
  339. patchi/core/security/defenders/block_ip.py +71 -0
  340. patchi/core/security/defenders/block_ws_origin.py +36 -0
  341. patchi/core/security/defenders/crypto_fix.py +118 -0
  342. patchi/core/security/defenders/enforce_rate_limit.py +108 -0
  343. patchi/core/security/defenders/fix_code.py +93 -0
  344. patchi/core/security/defenders/invalidate_session.py +40 -0
  345. patchi/core/security/defenders/patch_config.py +70 -0
  346. patchi/core/security/defenders/rotate_secret.py +54 -0
  347. patchi/core/security/defenders/suspend_account.py +41 -0
  348. patchi/core/security/defenders/update_dependency.py +160 -0
  349. patchi/core/security/defense_layer.py +217 -0
  350. patchi/core/security/dependency_vulnerability_agent.py +616 -0
  351. patchi/core/security/detection_pipeline.py +289 -0
  352. patchi/core/security/dns_security_agent.py +497 -0
  353. patchi/core/security/domain_activator_v2.py +913 -0
  354. patchi/core/security/domain_loader.py +1004 -0
  355. patchi/core/security/domains/__init__.py +0 -0
  356. patchi/core/security/email_authentication_agent.py +417 -0
  357. patchi/core/security/env_var_validator.py +304 -0
  358. patchi/core/security/evidence.py +100 -0
  359. patchi/core/security/falco_runtime_agent.py +387 -0
  360. patchi/core/security/fix-playbooks/__init__.py +0 -0
  361. patchi/core/security/gated_finding.py +85 -0
  362. patchi/core/security/git_diff_activator.py +367 -0
  363. patchi/core/security/governance.py +208 -0
  364. patchi/core/security/gradual_gate.py +34 -0
  365. patchi/core/security/history.py +315 -0
  366. patchi/core/security/iac_scanner.py +250 -0
  367. patchi/core/security/ignore_expiry.py +111 -0
  368. patchi/core/security/ignore_learner.py +416 -0
  369. patchi/core/security/injection_agent.py +319 -0
  370. patchi/core/security/insecure_randomness_agent.py +288 -0
  371. patchi/core/security/intent_analyzer.py +326 -0
  372. patchi/core/security/jwt_agent.py +453 -0
  373. patchi/core/security/kubernetes_agent.py +638 -0
  374. patchi/core/security/layer2_orchestrator.py +341 -0
  375. patchi/core/security/llm_security_agent.py +252 -0
  376. patchi/core/security/misconfig_agent.py +600 -0
  377. patchi/core/security/mobile_security_agent.py +474 -0
  378. patchi/core/security/network_agent.py +376 -0
  379. patchi/core/security/noise_filter.py +274 -0
  380. patchi/core/security/orchestrator.py +313 -0
  381. patchi/core/security/pattern_context.py +125 -0
  382. patchi/core/security/pattern_loader.py +101 -0
  383. patchi/core/security/pentest/__init__.py +4 -0
  384. patchi/core/security/pentest/base.py +58 -0
  385. patchi/core/security/pentest/dalfox_adapter.py +40 -0
  386. patchi/core/security/pentest/ffuf_adapter.py +48 -0
  387. patchi/core/security/pentest/nuclei_adapter.py +50 -0
  388. patchi/core/security/pentest/registry.py +80 -0
  389. patchi/core/security/pentest/shannon_adapter.py +107 -0
  390. patchi/core/security/pentest/sqlmap_adapter.py +56 -0
  391. patchi/core/security/pentest/zap_adapter.py +52 -0
  392. patchi/core/security/plan_auditor.py +276 -0
  393. patchi/core/security/policy_engine.py +272 -0
  394. patchi/core/security/prechecks.py +172 -0
  395. patchi/core/security/precommit_hook.py +247 -0
  396. patchi/core/security/privacy_agent.py +361 -0
  397. patchi/core/security/push_notification_agent.py +417 -0
  398. patchi/core/security/pysa_agent.py +143 -0
  399. patchi/core/security/reasoning.py +442 -0
  400. patchi/core/security/red_team_agent.py +184 -0
  401. patchi/core/security/red_team_engine.py +1142 -0
  402. patchi/core/security/remediation.py +360 -0
  403. patchi/core/security/request_interceptor.py +294 -0
  404. patchi/core/security/runtime_validator.py +245 -0
  405. patchi/core/security/saml_sso_agent.py +401 -0
  406. patchi/core/security/sast_agent.py +213 -0
  407. patchi/core/security/sast_gate.py +95 -0
  408. patchi/core/security/scheduler.py +287 -0
  409. patchi/core/security/secrets_runtime_agent.py +476 -0
  410. patchi/core/security/security_agents.py +175 -0
  411. patchi/core/security/security_config.py +551 -0
  412. patchi/core/security/security_probe.py +762 -0
  413. patchi/core/security/security_taint.py +1005 -0
  414. patchi/core/security/sensitive_data_agent.py +271 -0
  415. patchi/core/security/service_mesh_agent.py +412 -0
  416. patchi/core/security/session_management_agent.py +245 -0
  417. patchi/core/security/ssrf_agent.py +422 -0
  418. patchi/core/security/supply_chain.py +449 -0
  419. patchi/core/security/threat_model_generator.py +378 -0
  420. patchi/core/security/threat_model_updater.py +196 -0
  421. patchi/core/security/tool_adapters.py +135 -0
  422. patchi/core/security/tool_verify.py +173 -0
  423. patchi/core/security/websocket_security_agent.py +188 -0
  424. patchi/core/snapshot.py +242 -0
  425. patchi/core/tenant.py +386 -0
  426. patchi/core/testing/__init__.py +17 -0
  427. patchi/core/testing/_browser.py +147 -0
  428. patchi/core/testing/ads_agent.py +141 -0
  429. patchi/core/testing/api_contract_agent.py +450 -0
  430. patchi/core/testing/app_discovery_agent.py +641 -0
  431. patchi/core/testing/app_launcher.py +184 -0
  432. patchi/core/testing/browser_test_agent.py +516 -0
  433. patchi/core/testing/e2e_flow_agent.py +354 -0
  434. patchi/core/testing/flake_detector_agent.py +292 -0
  435. patchi/core/testing/gate.py +31 -0
  436. patchi/core/testing/live_test_runner.py +304 -0
  437. patchi/core/testing/live_v2/__init__.py +1 -0
  438. patchi/core/testing/live_v2/browser_pool.py +349 -0
  439. patchi/core/testing/live_v2/browser_test_runner.py +313 -0
  440. patchi/core/testing/live_v2/runner.py +626 -0
  441. patchi/core/testing/live_v2/screenshot_manager.py +474 -0
  442. patchi/core/testing/live_v2/stress_orchestrator.py +523 -0
  443. patchi/core/testing/live_v2/video_recorder.py +303 -0
  444. patchi/core/testing/regression_agent.py +302 -0
  445. patchi/core/testing/security_test_agent.py +253 -0
  446. patchi/core/testing/stress_test_agent.py +203 -0
  447. patchi/core/testing/test_agents.py +50 -0
  448. patchi/core/testing/test_config.py +194 -0
  449. patchi/core/testing/ui_accessibility_agent.py +324 -0
  450. patchi/core/testing/ui_button_agent.py +294 -0
  451. patchi/core/testing/ui_layout_agent.py +330 -0
  452. patchi/core/testing/unit_test_agent.py +1149 -0
  453. patchi/core/testing/visual_regression_agent.py +260 -0
  454. patchi/web/__init__.py +1 -0
  455. patchi/web/api/__init__.py +1 -0
  456. patchi/web/api/brain_map.py +114 -0
  457. patchi/web/api/charts.py +47 -0
  458. patchi/web/api/chat.py +224 -0
  459. patchi/web/api/cicd.py +576 -0
  460. patchi/web/api/dast_evidence.py +49 -0
  461. patchi/web/api/dev_check.py +320 -0
  462. patchi/web/api/fix.py +134 -0
  463. patchi/web/api/guard.py +65 -0
  464. patchi/web/api/hosted.py +128 -0
  465. patchi/web/api/hosted_v2.py +234 -0
  466. patchi/web/api/live_testing.py +699 -0
  467. patchi/web/api/scan.py +582 -0
  468. patchi/web/api/smart.py +111 -0
  469. patchi/web/api/tenant.py +117 -0
  470. patchi/web/api/tools.py +85 -0
  471. patchi/web/api_legacy.py +2401 -0
  472. patchi/web/app.py +310 -0
  473. patchi/web/events.py +483 -0
  474. patchi/web/routes/__init__.py +1 -0
  475. patchi/web/routes/assurance.py +541 -0
  476. patchi/web/routes/brain.py +36 -0
  477. patchi/web/routes/charter.py +227 -0
  478. patchi/web/routes/chat.py +23 -0
  479. patchi/web/routes/dashboard.py +558 -0
  480. patchi/web/routes/dashboard_v2.py +402 -0
  481. patchi/web/routes/findings.py +330 -0
  482. patchi/web/routes/guard.py +35 -0
  483. patchi/web/routes/history.py +126 -0
  484. patchi/web/routes/landing.py +52 -0
  485. patchi/web/routes/live_testing.py +24 -0
  486. patchi/web/routes/review.py +57 -0
  487. patchi/web/routes/self_improvement.py +110 -0
  488. patchi/web/routes/settings.py +49 -0
  489. patchi/web/routes/tokens.py +29 -0
  490. patchi/web/spawn.py +139 -0
  491. patchi/web/ws.py +141 -0
  492. patchi-0.7.5.dist-info/METADATA +315 -0
  493. patchi-0.7.5.dist-info/RECORD +496 -0
  494. patchi-0.7.5.dist-info/WHEEL +5 -0
  495. patchi-0.7.5.dist-info/licenses/LICENSE +60 -0
  496. 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
+ )