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,320 @@
1
+ """
2
+ Dev Check API — run p dev check gates from the web UI.
3
+
4
+ Runs the three gates (ruff lint, pytest, security scan) and returns
5
+ structured results for display in the Command Center card.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ import json
12
+ import logging
13
+ import os
14
+ import time
15
+ from pathlib import Path
16
+
17
+ from fastapi import APIRouter, Request
18
+ from fastapi.responses import JSONResponse
19
+ from starlette.templating import Jinja2Templates
20
+
21
+ _log = logging.getLogger("patchi.web.api.dev_check")
22
+
23
+
24
+ router = APIRouter(prefix="/api")
25
+
26
+ templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templates"))
27
+
28
+
29
+ @router.post("/dev-check")
30
+ async def run_dev_check(request: Request, strict: bool = False, fast: bool = False) -> JSONResponse:
31
+ """Run p dev check gates and return results.
32
+
33
+ Gates:
34
+ 1. Ruff lint (fast)
35
+ 2. Pytest (may timeout on slow tests)
36
+ 3. Security scan (changed files)
37
+ """
38
+ root = request.app.state.root
39
+
40
+ results = {"gates": [], "overall": "PASS", "strict": strict}
41
+ overall_pass = True
42
+
43
+ # Gate 1: Ruff
44
+ gate1 = await _run_gate(
45
+ "Ruff Lint",
46
+ ["python", "-m", "ruff", "check", "patchi/",
47
+ "--select", "E,F,W", "--ignore", "E501,E402,E741", "--quiet"],
48
+ root,
49
+ timeout=30,
50
+ )
51
+ results["gates"].append(gate1)
52
+ if gate1["status"] != "PASS":
53
+ overall_pass = False
54
+
55
+ # Gate 2: Pytest — stable test files only
56
+ _STABLE = [
57
+ "tests/test_config.py", "tests/test_charter.py", "tests/test_contract.py",
58
+ "tests/test_base.py", "tests/test_memory.py",
59
+ "tests/test_web.py", "tests/test_assurance.py",
60
+ "tests/test_risk_gate.py", "tests/test_scanner.py", "tests/test_detector.py",
61
+ "tests/test_secrets.py", "tests/test_security_config.py",
62
+ "tests/test_sigma_engine.py", "tests/test_blast_radius_v2.py",
63
+ "tests/test_chain_engine.py",
64
+ "tests/test_language_support.py", "tests/test_ast_utils.py",
65
+ "tests/test_import_graph.py",
66
+ "tests/test_hosted_mode.py", "tests/test_hosted_tokens.py",
67
+ "tests/test_hosted_audit_log.py", "tests/test_hosted_ip_reputation.py",
68
+ "tests/test_hosted_log_parsers.py", "tests/test_hosted_watchlist.py",
69
+ "tests/test_hosted_anomaly.py",
70
+ "tests/test_new_agents.py", "tests/test_p3_agents.py",
71
+ "tests/test_v2_agent_audit.py", "tests/test_v2_smoke.py",
72
+ "tests/test_attack_agent.py", "tests/test_doc_claim_agent.py",
73
+ "tests/test_fix_agents.py", "tests/test_scanners.py",
74
+ "tests/test_smart_agent.py",
75
+ "tests/test_governor_v2.py", "tests/test_governor_integration.py",
76
+ "tests/test_layered_brain.py", "tests/test_brain_watcher.py",
77
+ "tests/test_rebuilt_modules.py",
78
+ "tests/test_reasoning.py", "tests/test_noise_reduction.py",
79
+ "tests/test_ignore_learner.py", "tests/test_corpus_noise.py",
80
+ "tests/test_ai_client.py", "tests/test_debug_capture.py",
81
+ "tests/test_debug_codelldb.py", "tests/test_debug_node.py",
82
+ "tests/test_debug_powershell.py",
83
+ "tests/test_freshness.py", "tests/test_patch.py",
84
+ "tests/test_proactive.py", "tests/test_proactive_phase5.py",
85
+ "tests/test_snapshot.py", "tests/test_verify.py",
86
+ "tests/test_verify_loop.py", "tests/test_generated_suite.py",
87
+ "tests/test_app_profile.py", "tests/test_audit.py",
88
+ "tests/test_framework.py",
89
+ "tests/test_new_features.py", "tests/test_route_mapper.py",
90
+ "tests/test_applier.py", "tests/test_dispatcher.py",
91
+ "tests/test_notifications.py", "tests/test_queue.py",
92
+ "tests/test_scheduler.py", "tests/test_domain_loader.py",
93
+ ]
94
+ _CORE = [
95
+ "tests/test_config.py", "tests/test_charter.py", "tests/test_contract.py",
96
+ "tests/test_base.py", "tests/test_memory.py", "tests/test_web.py",
97
+ "tests/test_risk_gate.py", "tests/test_scanner.py", "tests/test_detector.py",
98
+ "tests/test_secrets.py", "tests/test_language_support.py", "tests/test_ast_utils.py",
99
+ "tests/test_import_graph.py", "tests/test_ai_client.py",
100
+ "tests/test_freshness.py", "tests/test_framework.py",
101
+ ]
102
+ _ptests = _CORE if fast else _STABLE
103
+ # Use xdist only on 4+ CPU machines
104
+ _ptargs = ["python", "-m", "pytest"]
105
+ import os as _os
106
+ if _os.cpu_count() and _os.cpu_count() >= 4:
107
+ try:
108
+ import xdist # noqa: F401
109
+ _ptargs += ["-n", "auto", "--dist", "loadscope"]
110
+ except ImportError:
111
+ pass
112
+ gate2 = await _run_gate(
113
+ "Pytest", _ptargs + _ptests + [
114
+ "-q", "--tb=no"],
115
+ root,
116
+ timeout=360,
117
+ )
118
+ results["gates"].append(gate2)
119
+ if gate2["status"] != "PASS":
120
+ overall_pass = False
121
+
122
+ # Gate 3: Security Scan (changed files)
123
+ gate3 = await _run_gate(
124
+ "Security Scan",
125
+ ["python", "-m", "patchi.cli.main", "scan",
126
+ "--changed", "--quiet", "--json"],
127
+ root,
128
+ timeout=60,
129
+ )
130
+ results["gates"].append(gate3)
131
+ if gate3["status"] != "PASS":
132
+ overall_pass = False
133
+
134
+ results["overall"] = "PASS" if overall_pass else "FAIL"
135
+ results["timestamp"] = time.time()
136
+ results["strict"] = strict
137
+ results["fast"] = fast
138
+
139
+ # Record history for the timing chart
140
+ _record_history(root, results)
141
+
142
+ return JSONResponse(results)
143
+
144
+
145
+ async def _run_gate(
146
+ name: str,
147
+ cmd: list[str],
148
+ cwd: Path,
149
+ timeout: int = 60,
150
+ ) -> dict:
151
+ """Run a single gate and return structured result."""
152
+ start = time.time()
153
+
154
+ try:
155
+ # Use temp files to avoid pipe deadlock on Windows when
156
+ # pytest subprocess tests keep pipe handles open.
157
+ import tempfile
158
+ tmp_out = tempfile.NamedTemporaryFile(mode='wb', suffix='.out', delete=False)
159
+ tmp_err = tempfile.NamedTemporaryFile(mode='wb', suffix='.err', delete=False)
160
+ try:
161
+ proc = await asyncio.create_subprocess_exec(
162
+ *cmd,
163
+ cwd=str(cwd),
164
+ stdout=tmp_out,
165
+ stderr=tmp_err,
166
+ )
167
+ await asyncio.wait_for(proc.wait(), timeout=timeout)
168
+ finally:
169
+ tmp_out.close()
170
+ tmp_err.close()
171
+ elapsed = time.time() - start
172
+ try:
173
+ output = open(tmp_out.name, 'rb').read().decode('utf-8', errors='replace') + open(tmp_err.name, 'rb').read().decode('utf-8', errors='replace')
174
+ except Exception:
175
+ output = ''
176
+ # Cleanup
177
+ try:
178
+ os.unlink(tmp_out.name)
179
+ os.unlink(tmp_err.name)
180
+ except Exception as _exc:
181
+ _log.warning('_run_gate failed: %s', _exc)
182
+
183
+ # Parse output for specific gate info
184
+ parsed = _parse_gate_output(name, output)
185
+
186
+ return {
187
+ "name": name,
188
+ "status": "PASS" if proc.returncode == 0 else "FAIL",
189
+ "exit_code": proc.returncode,
190
+ "elapsed": round(elapsed, 1),
191
+ "output": output[:2000], # Truncate long output
192
+ "parsed": parsed,
193
+ }
194
+
195
+ except TimeoutError:
196
+ return {
197
+ "name": name,
198
+ "status": "TIMEOUT",
199
+ "exit_code": -1,
200
+ "elapsed": timeout,
201
+ "output": f"Gate timed out after {timeout}s",
202
+ "parsed": {},
203
+ }
204
+ except Exception as e:
205
+ return {
206
+ "name": name,
207
+ "status": "ERROR",
208
+ "exit_code": -1,
209
+ "elapsed": time.time() - start,
210
+ "output": str(e),
211
+ "parsed": {},
212
+ }
213
+
214
+
215
+ def _record_history(root: Path, results: dict) -> None:
216
+ """Append gate timing data to history file for the chart."""
217
+ try:
218
+ import json as _json
219
+
220
+ history_path = root / ".patchi" / "dev_check_history.json"
221
+ history_path.parent.mkdir(parents=True, exist_ok=True)
222
+
223
+ history: list[dict] = []
224
+ if history_path.is_file():
225
+ try:
226
+ history = _json.loads(history_path.read_text(encoding="utf-8"))
227
+ except Exception:
228
+ history = []
229
+
230
+ entry = {
231
+ "timestamp": results.get("timestamp", time.time()),
232
+ "overall": results.get("overall", "?"),
233
+ "fast": results.get("fast", False),
234
+ "gates": [],
235
+ }
236
+ for g in results.get("gates", []):
237
+ entry["gates"].append({
238
+ "name": g.get("name", "?"),
239
+ "status": g.get("status", "?"),
240
+ "elapsed": g.get("elapsed", 0),
241
+ })
242
+ entry["total_time"] = sum(g["elapsed"] for g in entry["gates"])
243
+
244
+ history.append(entry)
245
+
246
+ # Keep last 50 runs
247
+ if len(history) > 50:
248
+ history = history[-50:]
249
+
250
+ tmp = history_path.with_suffix(".json.tmp")
251
+ tmp.write_text(_json.dumps(history, indent=2), encoding="utf-8")
252
+ tmp.replace(history_path)
253
+ except Exception as _exc:
254
+ _log.debug("dev_check history write skipped: %s", _exc)
255
+
256
+
257
+ @router.get("/dev-check/history")
258
+ async def dev_check_history(request: Request) -> JSONResponse:
259
+ """Return gate execution history for the timing chart."""
260
+ root = request.app.state.root
261
+ import json as _json
262
+
263
+ history_path = root / ".patchi" / "dev_check_history.json"
264
+ history: list[dict] = []
265
+ if history_path.is_file():
266
+ try:
267
+ history = _json.loads(history_path.read_text(encoding="utf-8"))
268
+ except Exception:
269
+ history = []
270
+
271
+ # Keep last 20 for the chart
272
+ recent = history[-20:]
273
+ return JSONResponse({"ok": True, "runs": recent, "count": len(recent)})
274
+
275
+
276
+ def _parse_gate_output(gate_name: str, output: str) -> dict:
277
+ """Parse gate output for specific metrics."""
278
+ parsed = {}
279
+
280
+ if gate_name == "Ruff Lint":
281
+ # Count errors
282
+ error_lines = [line for line in output.splitlines() if line.startswith("Found ")]
283
+ if error_lines:
284
+ parts = error_lines[0].split()
285
+ if len(parts) >= 2:
286
+ parsed["error_count"] = int(parts[1])
287
+
288
+ # Count by code
289
+ codes = {}
290
+ for line in output.splitlines():
291
+ if " E" in line or " F" in line or " W" in line:
292
+ parts = line.split()
293
+ for p in parts:
294
+ if len(p) == 4 and p[0] in "EFW":
295
+ codes[p] = codes.get(p, 0) + 1
296
+ parsed["by_code"] = codes
297
+
298
+ elif gate_name == "Pytest":
299
+ # Parse pytest summary
300
+ for line in output.splitlines():
301
+ if "passed" in line or "failed" in line:
302
+ parsed["summary"] = line.strip()
303
+ if "error" in line.lower() and "test" in line.lower():
304
+ parsed["collection_error"] = line.strip()
305
+
306
+ elif gate_name == "Security Scan":
307
+ # Try to parse JSON output
308
+ try:
309
+ # Find JSON in output (may have other text before/after)
310
+ for line in output.splitlines():
311
+ line = line.strip()
312
+ if line.startswith("{"):
313
+ data = json.loads(line)
314
+ parsed["findings"] = data.get("total_findings", 0)
315
+ parsed["domains"] = data.get("domains", [])
316
+ break
317
+ except (json.JSONDecodeError, AttributeError):
318
+ parsed["parse_error"] = "Could not parse JSON output"
319
+
320
+ return parsed
patchi/web/api/fix.py ADDED
@@ -0,0 +1,134 @@
1
+ """Fix API — apply/reject patches."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from fastapi import APIRouter, Request
6
+ from fastapi.responses import JSONResponse
7
+
8
+ from patchi.core.tenant import tenant_context
9
+
10
+ router = APIRouter(prefix="/api/fix")
11
+
12
+
13
+ @router.post("/apply/{patch_id}")
14
+ async def apply_patch(patch_id: str, request: Request) -> JSONResponse:
15
+ root = request.app.state.root
16
+ # IDOR fix: use context manager for tenant isolation and verify patch ownership
17
+ with tenant_context(root):
18
+ from patchi.core import memory as mem
19
+
20
+ patches = mem.read("patches", root)
21
+ patch = next((p for p in patches if p.get("id") == patch_id), None)
22
+
23
+ if not patch:
24
+ return JSONResponse({"ok": False, "error": "Patch not found"}, status_code=404)
25
+
26
+ # Verify patch belongs to this tenant/project (prevent IDOR enumeration)
27
+ patch_project = patch.get("project") or patch.get("tenant") or ""
28
+ if patch_project and patch_project != str(root) and patch_project not in str(root):
29
+ return JSONResponse({"ok": False, "error": "Patch not found"}, status_code=404)
30
+
31
+ from patchi.core.fix.applier import PatchApplier
32
+ from patchi.core.fix.patch import Patch
33
+
34
+ try:
35
+ p = Patch.from_dict(patch)
36
+ applier = PatchApplier(root)
37
+ applier.apply(p)
38
+ return JSONResponse({"ok": True, "message": f"Applied patch {patch_id}"})
39
+ except Exception as e:
40
+ return JSONResponse({"ok": False, "error": str(e)}, status_code=500)
41
+
42
+ # Fallback (unreachable due to with)
43
+ return JSONResponse({"ok": False, "error": "Tenant error"}, status_code=500)
44
+
45
+
46
+ @router.post("/reject/{patch_id}")
47
+ async def reject_patch(patch_id: str, request: Request) -> JSONResponse:
48
+ root = request.app.state.root
49
+ with tenant_context(root):
50
+ from patchi.core import memory as mem
51
+
52
+ try:
53
+ mem.record_rejection(patch_id, "web_user", root)
54
+ return JSONResponse({"ok": True, "message": f"Rejected patch {patch_id}"})
55
+ except Exception as e:
56
+ return JSONResponse({"ok": False, "error": str(e)}, status_code=500)
57
+
58
+
59
+ @router.post("/apply-all-safe")
60
+ async def apply_all_safe(request: Request) -> JSONResponse:
61
+ """Apply every pending patch that passes the risk gate for auto-apply.
62
+
63
+ Patches the gate flags as BLOCK or REQUIRE_REVIEW are skipped and reported,
64
+ never applied. This backs the "Apply all safe" button in the review UI.
65
+ """
66
+ root = request.app.state.root
67
+ from patchi.core import memory as mem
68
+ from patchi.core.fix.applier import PatchApplier
69
+ from patchi.core.fix.patch import Patch
70
+ from patchi.core.fix.risk_gate import RiskGate
71
+
72
+ patches_raw = mem.read("patches", root)
73
+ if not patches_raw:
74
+ return JSONResponse(
75
+ {
76
+ "ok": True,
77
+ "applied": [],
78
+ "skipped": [],
79
+ "failed": [],
80
+ "message": "No patches pending",
81
+ }
82
+ )
83
+
84
+ gate = RiskGate(root)
85
+ applier = PatchApplier(root)
86
+
87
+ applied: list[dict] = []
88
+ skipped: list[dict] = []
89
+ failed: list[dict] = []
90
+
91
+ for raw in patches_raw:
92
+ pid = raw.get("id", "?")
93
+ state = raw.get("state", "")
94
+ if state in ("applied", "rejected"):
95
+ continue # only pending patches
96
+ try:
97
+ patch = Patch.from_dict(raw)
98
+ except Exception as e:
99
+ failed.append({"id": pid, "reason": f"unparseable: {e}"})
100
+ continue
101
+
102
+ decision = gate.evaluate(patch)
103
+ if decision.is_blocked() or decision.needs_review():
104
+ skipped.append(
105
+ {
106
+ "id": pid,
107
+ "reason": decision.decision.value
108
+ if hasattr(decision.decision, "value")
109
+ else str(decision.decision),
110
+ "detail": getattr(decision, "reasons", None) or "",
111
+ }
112
+ )
113
+ continue
114
+
115
+ try:
116
+ result = applier.apply(patch)
117
+ entry = {"id": pid}
118
+ if result.success:
119
+ applied.append(entry)
120
+ else:
121
+ entry["reason"] = getattr(result, "error", "") or "applier refused"
122
+ failed.append(entry)
123
+ except Exception as e:
124
+ failed.append({"id": pid, "reason": str(e)})
125
+
126
+ return JSONResponse(
127
+ {
128
+ "ok": True,
129
+ "applied": applied,
130
+ "skipped": skipped,
131
+ "failed": failed,
132
+ "message": f"{len(applied)} applied, {len(skipped)} need review, {len(failed)} failed",
133
+ }
134
+ )
@@ -0,0 +1,65 @@
1
+ """Guard API — HTMX fragments for hosted guard monitoring."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+
7
+ from fastapi import APIRouter, Request
8
+ from fastapi.responses import HTMLResponse
9
+
10
+ logger = logging.getLogger("patchi.web.api.guard")
11
+
12
+ router = APIRouter(prefix="/api/guard")
13
+
14
+
15
+ @router.get("/threats", response_class=HTMLResponse)
16
+ async def get_threats(request: Request) -> str:
17
+ root = request.app.state.root
18
+ lines = [
19
+ "<table style='width:100%;font-size:12px;'>",
20
+ "<tr><th>IP</th><th>Score</th><th>Reason</th></tr>",
21
+ ]
22
+ try:
23
+ from patchi.core.hosted.watchlist import WatchlistTracker
24
+
25
+ tracker = WatchlistTracker(root)
26
+ for t in tracker.top(10):
27
+ ip = t.get("ip", "?")
28
+ score = t.get("score", 0)
29
+ reason = (t.get("reason", "") or "")[:60]
30
+ lines.append(
31
+ f"<tr><td style='font-family:var(--font-mono)'>{ip}</td>"
32
+ f"<td>{score}</td><td>{reason}</td></tr>"
33
+ )
34
+ except Exception:
35
+ lines.append("<tr><td colspan='3'>No threats</td></tr>")
36
+ lines.append("</table>")
37
+ return "".join(lines)
38
+
39
+
40
+ @router.get("/live", response_class=HTMLResponse)
41
+ async def get_live(request: Request) -> str:
42
+ root = request.app.state.root
43
+ lines: list[str] = []
44
+ try:
45
+ import time
46
+
47
+ from patchi.core.hosted.audit_log import read_recent
48
+
49
+ raw = read_recent(root, 10)
50
+ for e in reversed(raw):
51
+ ts = e.get("timestamp", 0)
52
+ tstr = time.strftime("%H:%M:%S", time.localtime(ts)) if ts else "--:--:--"
53
+ data = e.get("data", {})
54
+ msg = data.get("title", data.get("message", e.get("event", "")))
55
+ lines.append(
56
+ f"<div style='margin-bottom:3px'>"
57
+ f"<span style='color:var(--text-dim)'>{tstr}</span> "
58
+ f"<span>{msg}</span>"
59
+ f"</div>"
60
+ )
61
+ except Exception as e:
62
+ logger.warning("Failed to load recent audit events: %s", e)
63
+ if not lines:
64
+ lines.append("<div style='color:var(--text-dim)'>No recent events</div>")
65
+ return "".join(lines)
@@ -0,0 +1,128 @@
1
+ """Hosted API — hosted mode actions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from fastapi import APIRouter, Request
6
+ from fastapi.responses import JSONResponse
7
+
8
+ router = APIRouter(prefix="/api/hosted")
9
+
10
+
11
+ @router.get("/status")
12
+ async def hosted_status(request: Request) -> JSONResponse:
13
+ root = request.app.state.root
14
+ from patchi.core import config as cfg
15
+
16
+ try:
17
+ config = cfg.load(root)
18
+ except Exception:
19
+ config = {}
20
+
21
+ hosted = config.get("hosted", {})
22
+ return JSONResponse(
23
+ {
24
+ "enabled": hosted.get("enabled", False),
25
+ "log_path": hosted.get("log_path", ""),
26
+ }
27
+ )
28
+
29
+
30
+ @router.post("/init")
31
+ async def hosted_init(request: Request) -> JSONResponse:
32
+ root = request.app.state.root
33
+ from patchi.core import config as cfg_mod
34
+
35
+ try:
36
+ body = await request.json()
37
+ log_path = body.get("log_path", "/var/log/nginx/access.log")
38
+ log_format = body.get("log_format", "nginx")
39
+ escalate = body.get("escalate", True)
40
+
41
+ cfg_mod.set_value("hosted.log_path", log_path, root)
42
+ cfg_mod.set_value("hosted.log_format", log_format, root)
43
+ cfg_mod.set_value("hosted.escalate", escalate, root)
44
+ cfg_mod.set_value("hosted.enabled", True, root)
45
+
46
+ hosted_dir = root / ".patchi" / "hosted"
47
+ hosted_dir.mkdir(parents=True, exist_ok=True)
48
+
49
+ return JSONResponse(
50
+ {
51
+ "ok": True,
52
+ "message": "Hosted mode configured",
53
+ "log_path": log_path,
54
+ "log_format": log_format,
55
+ }
56
+ )
57
+ except Exception as e:
58
+ return JSONResponse({"ok": False, "error": str(e)}, status_code=500)
59
+
60
+
61
+ @router.get("/tokens")
62
+ async def list_tokens(request: Request) -> JSONResponse:
63
+ root = request.app.state.root
64
+ from patchi.core.hosted import tokens as tokens_mod
65
+
66
+ return JSONResponse({"tokens": tokens_mod.list_tokens(root)})
67
+
68
+
69
+ @router.post("/tokens")
70
+ async def create_token(request: Request) -> JSONResponse:
71
+ root = request.app.state.root
72
+ from patchi.core.hosted import tokens as tokens_mod
73
+
74
+ try:
75
+ body = await request.json()
76
+ name = body.get("name", "unnamed")
77
+ plaintext = tokens_mod.generate(root, name)
78
+ return JSONResponse(
79
+ {
80
+ "ok": True,
81
+ "token": plaintext,
82
+ "message": "Token generated — shown once",
83
+ }
84
+ )
85
+ except Exception as e:
86
+ return JSONResponse({"ok": False, "error": str(e)}, status_code=500)
87
+
88
+
89
+ @router.delete("/tokens/{token_id}")
90
+ async def revoke_token(token_id: str, request: Request) -> JSONResponse:
91
+ root = request.app.state.root
92
+ from patchi.core.tenant import tenant_context
93
+
94
+ with tenant_context(root):
95
+ from patchi.core.hosted import tokens as tokens_mod
96
+
97
+ # IDOR fix: verify token exists and belongs to tenant (list scope)
98
+ existing = {t.get("id") for t in tokens_mod.list_tokens(root)}
99
+ if token_id not in existing:
100
+ return JSONResponse({"ok": False, "error": "Token not found"}, status_code=404)
101
+ ok = tokens_mod.revoke(root, token_id)
102
+ if ok:
103
+ return JSONResponse({"ok": True, "message": f"Token {token_id} revoked"})
104
+ return JSONResponse({"ok": False, "error": "Token not found"}, status_code=404)
105
+
106
+
107
+ @router.post("/block/{ip}")
108
+ async def block_ip(ip: str, request: Request) -> JSONResponse:
109
+ root = request.app.state.root
110
+ from patchi.core.tenant import tenant_context
111
+
112
+ with tenant_context(root):
113
+ from patchi.core.hosted import ip_reputation
114
+
115
+ ip_reputation.block(ip, root)
116
+ return JSONResponse({"ok": True, "message": f"Blocked {ip}"})
117
+
118
+
119
+ @router.post("/unblock/{ip}")
120
+ async def unblock_ip(ip: str, request: Request) -> JSONResponse:
121
+ root = request.app.state.root
122
+ from patchi.core.tenant import tenant_context
123
+
124
+ with tenant_context(root):
125
+ from patchi.core.hosted import ip_reputation
126
+
127
+ ip_reputation.unblock(ip, root)
128
+ return JSONResponse({"ok": True, "message": f"Unblocked {ip}"})