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
patchi/web/api/scan.py ADDED
@@ -0,0 +1,582 @@
1
+ """Scan API — trigger security scans.
2
+
3
+ Scans run agents in parallel batches (default: 8 concurrent) so the
4
+ scan finishes faster while the event loop stays free for dashboard
5
+ requests. Each agent gets its own timeout so a hung agent can't block
6
+ the whole scan.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import asyncio
12
+ import logging
13
+ import time
14
+ from datetime import UTC, datetime
15
+ from pathlib import Path
16
+
17
+ from fastapi import APIRouter, Request
18
+ from fastapi.responses import HTMLResponse, JSONResponse
19
+
20
+ from patchi.core.tenant import tenant_context
21
+
22
+ _log = logging.getLogger("patchi.web.api.scan")
23
+
24
+
25
+ router = APIRouter(prefix="/api")
26
+
27
+ # ── Scan state ──────────────────────────────────────────────────────────────
28
+ # Global scan state so the dashboard and cancel endpoint can inspect it.
29
+ _scan_state: dict = {
30
+ "running": False,
31
+ "task": None, # asyncio.Task | None
32
+ "cancel": None, # asyncio.Event — set to abort
33
+ "started_at": 0.0,
34
+ "agent_index": 0,
35
+ "agent_total": 0,
36
+ "current_agent": "",
37
+ }
38
+
39
+ # Max agents that run concurrently inside each batch.
40
+ BATCH_SIZE = 8
41
+
42
+ # ── Background domain preload (CLI parity) ─────────────────────────────────
43
+ # The 800-domain YAML taxonomy takes seconds-to-tens-of-seconds to parse on
44
+ # first use. The CLI scan parses it in a background thread while agents run;
45
+ # web scans do the same so post-scan domain work (quick_scan activation,
46
+ # assurance, enrichment) never blocks on a cold DomainLoader. The parse also
47
+ # populates the shared disk pickle cache, so every later load is fast.
48
+ _domain_preload_future = None
49
+
50
+
51
+ def _detect_component_types(root: Path) -> list[str]:
52
+ """Cheap auto-detect of project component types (mirrors CLI scan)."""
53
+ ctypes: list[str] = []
54
+ try:
55
+ if any((root / d).exists() for d in ("templates", "static", "public")):
56
+ ctypes.append("frontend-web")
57
+ if any(
58
+ (root / f).exists() for f in ("requirements.txt", "pyproject.toml", "setup.py")
59
+ ):
60
+ ctypes.append("backend-api")
61
+ if (
62
+ any((root / d).exists() for d in ("docker", "k8s", "kubernetes", ".github"))
63
+ or (root / "Dockerfile").exists()
64
+ ):
65
+ ctypes.append("infra")
66
+ except Exception as _exc:
67
+ _log.debug("component type detect skipped: %s", _exc)
68
+ return ctypes
69
+
70
+
71
+ def _preload_domain_loader(root: Path):
72
+ """Start DomainLoader parsing in the background; return the future.
73
+
74
+ Reuses an in-flight preload if one is already running.
75
+ """
76
+ global _domain_preload_future
77
+ if _domain_preload_future is not None:
78
+ if not _domain_preload_future.done():
79
+ return _domain_preload_future # already in flight
80
+ try:
81
+ _domain_preload_future.result(timeout=0) # completed OK — reuse it
82
+ return _domain_preload_future
83
+ except Exception:
84
+ pass # failed preload — fall through and retry
85
+ try:
86
+ import concurrent.futures as _cf
87
+
88
+ from patchi.core.security.domain_loader import DomainLoader as _DL
89
+
90
+ ctypes = _detect_component_types(root)
91
+
92
+ def _preload() -> _DL:
93
+ _ldr = _DL(root, component_types=ctypes if ctypes else None)
94
+ _ldr.list_domains() # public force-load; parses + caches taxonomy
95
+ return _ldr
96
+
97
+ _pool = _cf.ThreadPoolExecutor(max_workers=1, thread_name_prefix="web-dl-preload")
98
+ _domain_preload_future = _pool.submit(_preload)
99
+ _pool.shutdown(wait=False)
100
+ except Exception as _exc:
101
+ _log.debug("domain preload suppressed: %s", _exc)
102
+ _domain_preload_future = None
103
+ return _domain_preload_future
104
+
105
+
106
+ def _joined_domain_loader(root: Path):
107
+ """Return the preloaded DomainLoader if it already finished, else None."""
108
+ fut = _domain_preload_future
109
+ if fut is None:
110
+ return None
111
+ try:
112
+ return fut.result(timeout=0)
113
+ except Exception:
114
+ return None
115
+ # Per-agent hard timeout (seconds). Agents that exceed this are killed.
116
+ AGENT_TIMEOUT = 120
117
+
118
+
119
+ @router.post("/scan")
120
+ async def trigger_scan(
121
+ request: Request,
122
+ scan_type: str = "all",
123
+ auto_fix: bool = False,
124
+ ) -> JSONResponse:
125
+ """Trigger a security scan. Runs in background, sends progress via WS."""
126
+ if _scan_state["running"]:
127
+ return JSONResponse(
128
+ status_code=409,
129
+ content={"ok": False, "error": "Scan already running"},
130
+ )
131
+
132
+ root = request.app.state.root
133
+ # Background domain preload (CLI parity): parse the taxonomy off the hot
134
+ # path while the agents run, so post-scan domain consumers are never cold.
135
+ _preload_domain_loader(root)
136
+ import patchi.core.security.security_agents # noqa: F401
137
+ from patchi.core import config as cfg
138
+ from patchi.core import memory as mem
139
+ from patchi.core.agents.base import AgentGroup, AgentInput, list_agents
140
+ from patchi.web.ws import (
141
+ evt_scan_cancelled,
142
+ evt_scan_complete,
143
+ evt_scan_progress,
144
+ evt_scan_started,
145
+ )
146
+
147
+ try:
148
+ config = cfg.load(root)
149
+ except Exception:
150
+ config = {}
151
+
152
+ brain = mem.get_brain(root)
153
+
154
+ # Determine agents to run
155
+ all_agents = {a.name: a for a in list_agents(AgentGroup.SECURITY)}
156
+ if scan_type == "all":
157
+ _SKIP = {"SecurityProber", "RedTeamAgent"}
158
+ to_run = [a for a in list_agents(AgentGroup.SECURITY) if a.name not in _SKIP]
159
+ elif scan_type in all_agents:
160
+ to_run = [all_agents[scan_type]]
161
+ else:
162
+ return JSONResponse({"ok": False, "error": f"Unknown scan type: {scan_type}"})
163
+
164
+ cancel_event = asyncio.Event()
165
+ _scan_state.update({
166
+ "running": True,
167
+ "cancel": cancel_event,
168
+ "started_at": time.time(),
169
+ "agent_total": len(to_run),
170
+ })
171
+
172
+ async def _run():
173
+ try:
174
+ # Wrap everything in tenant_context for proper scoping
175
+ with tenant_context(root):
176
+ await evt_scan_started(len(to_run))
177
+
178
+ results = []
179
+ total = len(to_run)
180
+
181
+ # Run agents in batches so the event loop stays free.
182
+ for batch_start in range(0, total, BATCH_SIZE):
183
+ if cancel_event.is_set():
184
+ break
185
+
186
+ batch = to_run[batch_start : batch_start + BATCH_SIZE]
187
+ batch_tasks = []
188
+
189
+ for agent_cls in batch:
190
+ if cancel_event.is_set():
191
+ break
192
+ idx = batch_start + batch.index(agent_cls) + 1
193
+ _scan_state["agent_index"] = idx
194
+ _scan_state["current_agent"] = agent_cls.name
195
+ await evt_scan_progress(agent_cls.name, "scanning", idx, total)
196
+
197
+ inp = AgentInput(root=root, scope=[], brain=brain, config=config)
198
+ task = asyncio.create_task(
199
+ _run_agent_with_timeout(agent_cls, inp, AGENT_TIMEOUT)
200
+ )
201
+ batch_tasks.append(task)
202
+
203
+ # Wait for all agents in this batch concurrently.
204
+ # yield control between batches so the event loop can
205
+ # handle HTTP requests, WS pings, etc.
206
+ if batch_tasks:
207
+ batch_results = await asyncio.gather(
208
+ *batch_tasks, return_exceptions=True
209
+ )
210
+ for r in batch_results:
211
+ if isinstance(r, Exception):
212
+ # Agent timed out or crashed — record as empty result
213
+ from patchi.core.agents.base import (
214
+ AgentResult,
215
+ AgentStatus,
216
+ )
217
+ r = AgentResult(
218
+ agent_name="unknown",
219
+ status=AgentStatus.FAILED,
220
+ errors=[str(r)],
221
+ )
222
+ results.append(r)
223
+ await evt_scan_progress(
224
+ getattr(r, "agent_name", "?"), "done",
225
+ min(len(results), total), total,
226
+ )
227
+
228
+ total_findings = sum(r.finding_count for r in results)
229
+
230
+ # Deduplicate + correlate
231
+ try:
232
+ from patchi.core.security.orchestrator import SecurityOrchestrator
233
+ report = SecurityOrchestrator().correlate(results)
234
+ deduped_count = report.total_findings
235
+ except Exception:
236
+ deduped_count = total_findings
237
+
238
+ # Non-blocking join: make sure the background taxonomy parse
239
+ # finished so the shared cache is warm for whatever the
240
+ # dashboard touches right after the scan.
241
+ _joined_domain_loader(root)
242
+
243
+ if cancel_event.is_set():
244
+ await evt_scan_cancelled(deduped_count)
245
+ else:
246
+ await evt_scan_complete(deduped_count, 0)
247
+
248
+ # Fresh data is in — drop the health/scan cache so the
249
+ # dashboard reflects the new scan immediately.
250
+ try:
251
+ from patchi.core.health import invalidate_cache
252
+
253
+ invalidate_cache()
254
+ except Exception as _exc:
255
+ _log.debug('suppressed: %s', _exc)
256
+
257
+ # Record scan in history
258
+ try:
259
+ import time as _time
260
+
261
+ from patchi.core.memory import record_scan
262
+ elapsed = round(_time.time() - _scan_state.get('started_at', _time.time()), 1)
263
+ agent_names = [getattr(r, 'agent_name', '?') for r in results]
264
+ record_scan({
265
+ 'timestamp': datetime.now(UTC).isoformat(),
266
+ 'total_findings': deduped_count,
267
+ 'agents_run': len(results),
268
+ 'agents_list': agent_names[:20],
269
+ 'duration_s': elapsed,
270
+ 'auto_fix': auto_fix,
271
+ 'cancelled': cancel_event.is_set(),
272
+ }, root)
273
+ except Exception as _exc:
274
+ _log.debug("record_scan history write skipped: %s", _exc)
275
+
276
+ # Auto-fix
277
+ if auto_fix and deduped_count > 0 and not cancel_event.is_set():
278
+ await evt_scan_progress("auto_fix", "running", total, total)
279
+ try:
280
+ await asyncio.to_thread(_run_proactive_fix, root, config)
281
+ except Exception as e:
282
+ import logging
283
+ logging.getLogger("patchi.web.scan").warning(
284
+ "Auto-fix failed: %s", e
285
+ )
286
+ await evt_scan_progress("auto_fix", "done", total + 1, total + 1)
287
+ finally:
288
+ _scan_state["running"] = False
289
+ _scan_state["cancel"] = None
290
+ _scan_state["task"] = None
291
+
292
+ task = asyncio.create_task(_run())
293
+ _scan_state["task"] = task
294
+
295
+ return JSONResponse({"ok": True, "message": f"Scan started with {len(to_run)} agents"})
296
+
297
+
298
+ async def _run_agent_with_timeout(agent_cls, inp, timeout: int):
299
+ """Run a single agent with a hard timeout."""
300
+ return await asyncio.wait_for(
301
+ asyncio.to_thread(agent_cls().run, inp),
302
+ timeout=timeout,
303
+ )
304
+
305
+
306
+ @router.post("/scan/cancel")
307
+ async def cancel_scan() -> JSONResponse:
308
+ """Cancel a running scan."""
309
+ if not _scan_state["running"]:
310
+ return JSONResponse({"ok": True, "message": "No scan running"})
311
+ cancel = _scan_state.get("cancel")
312
+ if cancel:
313
+ cancel.set()
314
+ task = _scan_state.get("task")
315
+ if task and not task.done():
316
+ task.cancel()
317
+ from patchi.web.ws import evt_scan_cancelled
318
+
319
+ # task.cancel() kills _run() with CancelledError before it reaches its own
320
+ # completion broadcast, so the dashboard would otherwise sit on the polling
321
+ # fallback and show "Scan complete". Emit the cancellation event here.
322
+ try:
323
+ await evt_scan_cancelled(0)
324
+ except Exception as _exc:
325
+ _log.debug("cancel broadcast suppressed: %s", _exc)
326
+ return JSONResponse({"ok": True, "message": "Scan cancellation requested"})
327
+
328
+
329
+ @router.get("/scan/status")
330
+ async def scan_status() -> JSONResponse:
331
+ """Get current scan status for polling."""
332
+ if not _scan_state["running"]:
333
+ return JSONResponse({"running": False})
334
+ elapsed = time.time() - _scan_state["started_at"]
335
+ return JSONResponse({
336
+ "running": True,
337
+ "elapsed": round(elapsed, 1),
338
+ "current_agent": _scan_state["current_agent"],
339
+ "agent_index": _scan_state["agent_index"],
340
+ "agent_total": _scan_state["agent_total"],
341
+ })
342
+
343
+
344
+ @router.get("/scan/history")
345
+ async def scan_history(request: Request, limit: int = 10) -> JSONResponse:
346
+ """Return the last N scan summaries from history."""
347
+ root = request.app.state.root
348
+ from patchi.core.memory import get_scan_history
349
+ history = get_scan_history(root)
350
+ return JSONResponse({"ok": True, "scans": history[:limit], "total": len(history)})
351
+
352
+
353
+ def _run_proactive_fix(root, config):
354
+ """Run proactive fixes after a scan."""
355
+ from patchi.core.brain.proactive import ProactiveFixer
356
+
357
+ try:
358
+ fixer = ProactiveFixer(root)
359
+ fixer.run()
360
+ except Exception as e:
361
+ import logging
362
+ logging.getLogger("patchi.web.scan").warning("_run_proactive_fix failed: %s", e)
363
+
364
+
365
+ @router.get("/scan")
366
+ async def get_scan_report(request: Request) -> JSONResponse:
367
+ """GET /api/scan — return cached scan results / report."""
368
+ root = request.app.state.root
369
+ from patchi.core import memory as mem
370
+
371
+ scan_results = mem.get_scan_results(root)
372
+ all_findings = []
373
+ for agent_name, data in scan_results.items():
374
+ if not isinstance(data, dict):
375
+ continue
376
+ for f in data.get("findings", []):
377
+ if isinstance(f, dict):
378
+ f["agent"] = agent_name
379
+ all_findings.append(f)
380
+
381
+ try:
382
+ from patchi.core.agents.base import AgentGroup, AgentResult, Finding
383
+ from patchi.core.security.orchestrator import SecurityOrchestrator
384
+
385
+ synth = []
386
+ for name, data in scan_results.items():
387
+ if not isinstance(data, dict):
388
+ continue
389
+ ar = AgentResult(agent_name=name, agent_group=AgentGroup.SECURITY)
390
+ for f in data.get("findings", []):
391
+ if isinstance(f, dict):
392
+ ar.findings.append(Finding.from_dict(f))
393
+ synth.append(ar)
394
+ if synth:
395
+ report = SecurityOrchestrator().correlate(synth)
396
+ payload = report.to_dict()
397
+ payload["ok"] = True
398
+ payload["cached"] = True
399
+ payload["total_findings"] = report.total_findings
400
+ return JSONResponse(payload)
401
+ except Exception as _exc:
402
+ _log.warning('get_scan_report failed: %s', _exc)
403
+
404
+ return JSONResponse({
405
+ "ok": True,
406
+ "total_findings": len(all_findings),
407
+ "findings": all_findings[:50],
408
+ "cached": True,
409
+ })
410
+
411
+
412
+ @router.post("/scan/quick")
413
+ async def quick_scan(request: Request) -> JSONResponse:
414
+ """On-demand scan: runs only agents relevant to git-diff changed files."""
415
+ root = request.app.state.root
416
+
417
+ try:
418
+ import patchi.core.agents.scanners # noqa: F401
419
+ from patchi.core.agents.base import AgentGroup, AgentInput, list_agents
420
+ from patchi.core.security.domain_activator_v2 import DomainActivatorV2
421
+ from patchi.core.security.git_diff_activator import activate_from_diff
422
+ from patchi.web.ws import evt_scan_complete
423
+
424
+ # 1. Activate domains from git diff
425
+ diff_result = activate_from_diff(root, commits=1)
426
+ if diff_result.error:
427
+ return JSONResponse({"ok": False, "error": diff_result.error})
428
+
429
+ # 2. Map domains to agents (reuse the background-preloaded taxonomy)
430
+ _preload_domain_loader(root)
431
+ activator = DomainActivatorV2(root, domain_loader=_joined_domain_loader(root))
432
+ relevant = activator.get_relevant_agents(list(diff_result.activated_domains.keys()))
433
+ relevant.extend(["PreCheckAgent", "PlanAuditorAgent"])
434
+ relevant = list(dict.fromkeys(relevant))
435
+
436
+ all_agents = {a.name: a for a in list_agents(AgentGroup.SCANNER)}
437
+ to_run = [all_agents[n] for n in relevant if n in all_agents]
438
+
439
+ if not to_run:
440
+ return JSONResponse(
441
+ {
442
+ "ok": True,
443
+ "message": "No agents match changed files",
444
+ "changed_files": diff_result.changed_files,
445
+ "domains": diff_result.activated_domains,
446
+ "agents_run": 0,
447
+ }
448
+ )
449
+
450
+ # 3. Run filtered agents
451
+ from patchi.core import config as cfg
452
+ from patchi.core import memory as mem
453
+
454
+ config = cfg.load(root) if root.exists() else {}
455
+ brain = mem.get_brain(root)
456
+
457
+ async def _run():
458
+ results = []
459
+ for agent_cls in to_run:
460
+ inp = AgentInput(root=root, scope=[], brain=brain, config=config)
461
+ result = await asyncio.to_thread(agent_cls().run, inp)
462
+ results.append(result)
463
+ total = sum(r.finding_count for r in results)
464
+ await evt_scan_complete(total, 0)
465
+
466
+ asyncio.create_task(_run())
467
+
468
+ return JSONResponse(
469
+ {
470
+ "ok": True,
471
+ "message": f"Quick scan started: {len(to_run)} agents",
472
+ "changed_files": diff_result.changed_files[:20],
473
+ "domains": diff_result.activated_domains,
474
+ "agents_queued": [a.name for a in to_run],
475
+ }
476
+ )
477
+
478
+ except Exception as e:
479
+ return JSONResponse({"ok": False, "error": str(e)}, status_code=500)
480
+
481
+
482
+ @router.post("/scan/dast")
483
+ async def trigger_dast(request: Request) -> JSONResponse:
484
+ """Trigger DAST (Dynamic Application Security Testing) scan with Playwright."""
485
+ root = request.app.state.root
486
+
487
+ from patchi.core import config as cfg
488
+ from patchi.core import memory as mem
489
+ from patchi.core.agents.base import AgentInput
490
+ from patchi.web.ws import evt_scan_complete, evt_scan_progress, evt_scan_started
491
+
492
+ try:
493
+ config = cfg.load(root)
494
+ except Exception:
495
+ config = {}
496
+
497
+ brain = mem.get_brain(root)
498
+
499
+ # Import DAST agent
500
+ try:
501
+ from patchi.core.security.dast_agent import DASTAgent
502
+ except ImportError:
503
+ return JSONResponse({"ok": False, "error": "Playwright not installed. Run: pip install playwright && playwright install"}, status_code=500)
504
+
505
+ async def _run():
506
+ with tenant_context(root):
507
+ await evt_scan_started(1)
508
+ await evt_scan_progress("DASTAgent", "scanning", 0, 1)
509
+
510
+ inp = AgentInput(root=root, scope=[], brain=brain, config=config)
511
+ agent = DASTAgent()
512
+ result = await asyncio.to_thread(agent.run, inp)
513
+
514
+ total_findings = result.finding_count
515
+ await evt_scan_progress("DASTAgent", "done", 1, 1)
516
+ await evt_scan_complete(total_findings, 0)
517
+
518
+ asyncio.create_task(_run())
519
+
520
+ return JSONResponse({"ok": True, "message": "DAST scan started"})
521
+
522
+
523
+ @router.get("/findings-table")
524
+ async def get_findings(request: Request, limit: int = 20) -> HTMLResponse:
525
+ """Get recent findings as HTML fragment."""
526
+ root = request.app.state.root
527
+ from fastapi.templating import Jinja2Templates
528
+
529
+ from patchi.core import memory as mem
530
+
531
+ templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templates"))
532
+
533
+ scan_results = mem.get_scan_results(root)
534
+ all_findings = []
535
+ for agent_name, data in scan_results.items():
536
+ for f in data.get("findings", [])[:limit]:
537
+ f["agent"] = agent_name
538
+ all_findings.append(f)
539
+
540
+ sev_order = {"critical": 0, "high": 1, "medium": 2, "low": 3, "info": 4}
541
+ all_findings.sort(key=lambda f: sev_order.get(f.get("severity", "info"), 5))
542
+
543
+ return templates.TemplateResponse(
544
+ request,
545
+ "partials/findings_list.html",
546
+ {
547
+ "request": request,
548
+ "findings": all_findings[:limit],
549
+ },
550
+ )
551
+
552
+
553
+ @router.get("/agents/status")
554
+ async def get_agents_status(request: Request) -> HTMLResponse:
555
+ """Get agent status as HTML fragment."""
556
+ root = request.app.state.root
557
+ from fastapi.templating import Jinja2Templates
558
+
559
+ from patchi.core import memory as mem
560
+
561
+ templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templates"))
562
+
563
+ scan_results = mem.get_scan_results(root)
564
+ agents = []
565
+ for agent_name, data in scan_results.items():
566
+ agents.append(
567
+ {
568
+ "name": agent_name,
569
+ "status": data.get("status", "unknown"),
570
+ "findings": data.get("finding_count", 0),
571
+ "duration_ms": data.get("duration_ms", 0),
572
+ }
573
+ )
574
+
575
+ return templates.TemplateResponse(
576
+ request,
577
+ "partials/agent_grid.html",
578
+ {
579
+ "request": request,
580
+ "agents": agents,
581
+ },
582
+ )
@@ -0,0 +1,111 @@
1
+ """
2
+ Web API + page for the Smart Agent (now powered by the Orchestrator brain).
3
+
4
+ POST /api/smart/run — kicks off an Orchestrator run; every tool event is streamed
5
+ live to all connected WebSocket clients (the "see it work
6
+ as it happens" view).
7
+ GET /smart — the control console page (type a goal, watch it run).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import asyncio
13
+ import logging
14
+ from pathlib import Path
15
+
16
+ from fastapi import APIRouter, Request
17
+ from fastapi.responses import HTMLResponse
18
+ from fastapi.templating import Jinja2Templates
19
+ from pydantic import BaseModel
20
+
21
+ _log = logging.getLogger("patchi.web.smart")
22
+
23
+ _templates = Jinja2Templates(
24
+ directory=str(Path(__file__).resolve().parent.parent / "templates")
25
+ )
26
+
27
+ router = APIRouter()
28
+
29
+ _current_task: asyncio.Task | None = None
30
+
31
+
32
+ class SmartRunRequest(BaseModel):
33
+ goal: str
34
+ max_steps: int = 10
35
+
36
+
37
+ @router.post("/api/smart/run")
38
+ async def smart_run(req: SmartRunRequest, request: Request):
39
+ root = request.app.state.root
40
+ loop = asyncio.get_event_loop()
41
+ from patchi.web.ws import manager
42
+
43
+ def on_event(payload: dict) -> None:
44
+ try:
45
+ asyncio.run_coroutine_threadsafe(
46
+ manager.broadcast(payload["event"], payload["data"]), loop
47
+ )
48
+ except Exception as e:
49
+ _log.debug("ws broadcast failed: %s", e)
50
+
51
+ def on_progress(msg: str) -> None:
52
+ try:
53
+ asyncio.run_coroutine_threadsafe(
54
+ manager.broadcast("agent.progress", {
55
+ "agent": "orchestrator",
56
+ "progress_pct": 0,
57
+ "current_file": msg,
58
+ }), loop
59
+ )
60
+ except Exception as _exc:
61
+ _log.warning('on_progress failed: %s', _exc)
62
+
63
+ async def _run() -> None:
64
+ from patchi.core.ai.orchestrator import Orchestrator
65
+
66
+ orchestrator = Orchestrator(
67
+ root,
68
+ on_event=on_event,
69
+ on_progress=on_progress,
70
+ )
71
+ try:
72
+ await orchestrator.run(req.goal, max_steps=req.max_steps)
73
+ except Exception as e:
74
+ _log.error("Orchestrator run failed: %s", e)
75
+ on_event({
76
+ "event": "agent.error",
77
+ "data": {"agent": "orchestrator", "error": str(e)},
78
+ })
79
+
80
+ global _current_task
81
+ _current_task = asyncio.create_task(_run())
82
+ return {
83
+ "success": True,
84
+ "message": "Orchestrator started — events stream over /ws",
85
+ "goal": req.goal,
86
+ }
87
+
88
+
89
+ @router.post("/api/smart/cancel")
90
+ async def smart_cancel() -> dict:
91
+ """Cancel a running Orchestrator run, if any."""
92
+ global _current_task
93
+ task = _current_task
94
+ if task is None or task.done():
95
+ return {
96
+ "success": True,
97
+ "message": "No active Orchestrator run to cancel",
98
+ "cancelled": False,
99
+ }
100
+ task.cancel()
101
+ _current_task = None
102
+ return {
103
+ "success": True,
104
+ "message": "Orchestrator run cancelled",
105
+ "cancelled": True,
106
+ }
107
+
108
+
109
+ @router.get("/smart", response_class=HTMLResponse)
110
+ async def smart_page(request: Request):
111
+ return _templates.TemplateResponse(request, "smart.html", {"request": request})