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,457 @@
1
+ """
2
+ `p doctor` — System health check and stale command detection.
3
+
4
+ Checks:
5
+ 1. Stale/removed commands that should be migrated
6
+ 2. Missing dependencies
7
+ 3. API key configuration
8
+ 4. .patchi/ size warnings
9
+ 5. Config validation
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import importlib
14
+ import logging
15
+ import pathlib
16
+ import re
17
+ import shutil
18
+ import sys
19
+
20
+ from rich.panel import Panel
21
+ from rich.table import Table
22
+ from rich.text import Text
23
+
24
+ from patchi.cli.console import con
25
+
26
+ _log = logging.getLogger("patchi.cli.doctor")
27
+
28
+
29
+ # ── Stale reference auto-fixer ───────────────────────────────────────
30
+
31
+ # Patterns to search: "p <old>" as a shell command in any text file
32
+ # We match whole-word to avoid false positives (e.g. "p status" shouldn't match "p status --deep")
33
+
34
+
35
+ def _build_replacement_patterns(
36
+ found_stale: list[tuple[str, dict]],
37
+ ) -> list[tuple[re.Pattern, str]]:
38
+ """Build regex patterns for each stale command."""
39
+ patterns = []
40
+ for name, info in found_stale:
41
+ replacement = info["replacement"]
42
+ # Match: p <old> as a standalone command (word boundary after)
43
+ # Handles: "p brain", "p brain ", "p brain\n", "p brain'"
44
+ # Also handles: "patchi brain", "python -m patchi brain"
45
+ pat = re.compile(
46
+ r"(\b)(?:p|patchi|python\s+-m\s+patchi)\s+"
47
+ + re.escape(name)
48
+ + r"(\s|\'|\"|$)",
49
+ re.MULTILINE,
50
+ )
51
+ patterns.append((pat, replacement))
52
+ return patterns
53
+
54
+
55
+ def _fix_stale_references(
56
+ root: pathlib.Path | None,
57
+ found_stale: list[tuple[str, dict]],
58
+ ) -> None:
59
+ """Scan scripts/ and common locations for stale command references and fix them."""
60
+ if not root or not root.exists():
61
+ con.print("[yellow]No project root — skipping auto-fix.[/yellow]")
62
+ return
63
+
64
+ patterns = _build_replacement_patterns(found_stale)
65
+
66
+ # Directories to scan
67
+ scan_dirs = []
68
+ for d in ["scripts", ".github", ".gitlab", ".circleci", "ci", "bin"]:
69
+ p = root / d
70
+ if p.is_dir():
71
+ scan_dirs.append(p)
72
+
73
+ # Also scan root-level shell scripts and config files
74
+ scan_files = list(root.glob("*.sh")) + list(root.glob("*.bash")) + list(root.glob("*.zsh"))
75
+ scan_files += list(root.glob("Makefile")) + list(root.glob("*.mk"))
76
+ scan_files += list(root.glob("*.yml")) + list(root.glob("*.yaml"))
77
+ scan_files += list(root.glob("*.toml")) + list(root.glob("*.cfg"))
78
+ scan_files += list(root.glob("*.ini"))
79
+ scan_files += list(root.glob("docker*"))
80
+ scan_files += list(root.glob("*.bat")) + list(root.glob("*.cmd")) + list(root.glob("*.ps1"))
81
+
82
+ total_fixed = 0
83
+ fixed_files = []
84
+
85
+ # Scan directories recursively
86
+ for scan_dir in scan_dirs:
87
+ for fpath in scan_dir.rglob("*"):
88
+ if fpath.is_file() and _is_text_file(fpath):
89
+ fixed = _fix_file(fpath, patterns)
90
+ if fixed > 0:
91
+ total_fixed += fixed
92
+ fixed_files.append((fpath, fixed))
93
+
94
+ # Scan root-level files
95
+ for fpath in scan_files:
96
+ if fpath.is_file() and _is_text_file(fpath):
97
+ fixed = _fix_file(fpath, patterns)
98
+ if fixed > 0:
99
+ total_fixed += fixed
100
+ fixed_files.append((fpath, fixed))
101
+
102
+ # Report results
103
+ if total_fixed == 0:
104
+ con.print("[green]✓ No stale references found in scripts/.[/green]")
105
+ else:
106
+ con.print(f"\n[bold green]✓ Fixed {total_fixed} stale reference(s) in {len(fixed_files)} file(s):[/bold green]")
107
+ con.print()
108
+ for fpath, count in fixed_files:
109
+ rel = fpath.relative_to(root) if fpath.is_relative_to(root) else fpath
110
+ con.print(f" [cyan]{rel}[/cyan] — {count} replacement(s)")
111
+ con.print()
112
+ con.print("[dim]Review the changes with `git diff` before committing.[/dim]")
113
+
114
+
115
+ def _is_text_file(fpath: pathlib.Path) -> bool:
116
+ """Check if a file is likely a text file (not binary)."""
117
+ text_exts = {
118
+ ".sh", ".bash", ".zsh", ".bat", ".cmd", ".ps1",
119
+ ".py", ".js", ".ts", ".jsx", ".tsx",
120
+ ".yml", ".yaml", ".toml", ".cfg", ".ini", ".conf",
121
+ ".md", ".rst", ".txt",
122
+ "Makefile", "Dockerfile", ".dockerfile",
123
+ ".mk", ".cmake",
124
+ }
125
+ if fpath.name in text_exts or fpath.suffix in text_exts:
126
+ return True
127
+ # Check if it has no extension but is small (likely a script)
128
+ if not fpath.suffix and fpath.stat().st_size < 100_000:
129
+ try:
130
+ fpath.read_text(encoding="utf-8")[:100]
131
+ return True
132
+ except (UnicodeDecodeError, OSError):
133
+ return False
134
+ return False
135
+
136
+
137
+ def _fix_file(fpath: pathlib.Path, patterns: list[tuple[re.Pattern, str]]) -> int:
138
+ """Replace stale references in a file. Returns number of replacements."""
139
+ try:
140
+ text = fpath.read_text(encoding="utf-8")
141
+ except (UnicodeDecodeError, OSError):
142
+ return 0
143
+
144
+ new_text = text
145
+ total = 0
146
+ for pat, replacement in patterns:
147
+ new_text, n = pat.subn(replacement, new_text)
148
+ total += n
149
+
150
+ if total > 0 and new_text != text:
151
+ fpath.write_text(new_text, encoding="utf-8")
152
+ return total
153
+
154
+
155
+ def run(
156
+ verbose: bool = False,
157
+ json_output: bool = False,
158
+ fix: bool = False,
159
+ ) -> None:
160
+ """Entry point for `p doctor`."""
161
+ if not json_output:
162
+ con.print()
163
+ con.print("[bold #C8621A]Patchi Doctor[/bold #C8621A] [dim]system health check[/dim]")
164
+ con.print()
165
+
166
+ checks: list[tuple[str, str, str, str]] = [] # (label, status, note, color)
167
+ warnings = 0
168
+ errors = 0
169
+
170
+ # ── 1. Python version ─────────────────────────────────────────────────────
171
+ pyver = sys.version_info
172
+ if pyver >= (3, 11):
173
+ checks.append(("Python", "✓", f"{pyver.major}.{pyver.minor}.{pyver.micro}", "#4ADE80"))
174
+ else:
175
+ checks.append(("Python", "✗", f"{pyver.major}.{pyver.minor} — requires ≥ 3.11", "#FF4D6D"))
176
+ errors += 1
177
+
178
+ # ── 2. Required dependencies ──────────────────────────────────────────────
179
+ _REQUIRED = [
180
+ ("rich", "rich", "Terminal UI rendering"),
181
+ ("watchfiles", "watchfiles", "File watcher (p watch)"),
182
+ ("vulture", "vulture", "Dead code detection"),
183
+ ("httpx", "httpx", "HTTP client (AI requests)"),
184
+ ("fastapi", "fastapi", "Web dashboard backend"),
185
+ ("uvicorn", "uvicorn", "Web dashboard ASGI server"),
186
+ ("psutil", "psutil", "Process + system metrics"),
187
+ ("yaml", "pyyaml", "YAML config parsing"),
188
+ ("loguru", "loguru", "Structured logging"),
189
+ ]
190
+
191
+ for import_name, pip_name, desc in _REQUIRED:
192
+ try:
193
+ importlib.import_module(import_name)
194
+ checks.append((f"dep: {pip_name}", "✓", desc, "#4ADE80"))
195
+ except ImportError:
196
+ checks.append((f"dep: {pip_name}", "✗", f"Missing — pip install {pip_name}", "#FF4D6D"))
197
+ errors += 1
198
+
199
+ # ── 3. Project root ───────────────────────────────────────────────────────
200
+ try:
201
+ from patchi.core.config import find_project_root
202
+
203
+ root = find_project_root()
204
+ if root:
205
+ checks.append(("Project root", "✓", str(root), "#4ADE80"))
206
+ else:
207
+ checks.append(("Project root", "⚠", "Not found — run 'p init' first", "#FACC15"))
208
+ warnings += 1
209
+ except Exception as e:
210
+ checks.append(("Project root", "✗", str(e)[:60], "#FF4D6D"))
211
+ errors += 1
212
+ root = None
213
+
214
+ # ── 4. Stale commands check ────────────────────────────────────────────────
215
+ stale_commands = {
216
+ "brain": {
217
+ "replacement": "p agents list --brain",
218
+ "reason": "Merged into agents command",
219
+ },
220
+ "smart": {
221
+ "replacement": "p chat --stream",
222
+ "reason": "Merged into chat command",
223
+ },
224
+ "health": {
225
+ "replacement": "p status --deep",
226
+ "reason": "Merged into status command",
227
+ },
228
+ "doctor": {
229
+ "replacement": "p status --validate",
230
+ "reason": "Merged into status command",
231
+ },
232
+ "security": {
233
+ "replacement": "p scan",
234
+ "reason": "Merged into scan command",
235
+ },
236
+ "ignore": {
237
+ "replacement": "p memory ignore",
238
+ "reason": "Merged into memory command",
239
+ },
240
+ "explain": {
241
+ "replacement": "p chat 'explain <type>'",
242
+ "reason": "Merged into chat command",
243
+ },
244
+ "profile": {
245
+ "replacement": "p agent-stats",
246
+ "reason": "Merged into agent-stats command",
247
+ },
248
+ "learning": {
249
+ "replacement": "p agent-stats --learning",
250
+ "reason": "Merged into agent-stats command",
251
+ },
252
+ "ask": {
253
+ "replacement": "p chat",
254
+ "reason": "Alias — chat is the unified interface",
255
+ },
256
+ "chain": {
257
+ "replacement": "p chains",
258
+ "reason": "Merged into chains command (plural)",
259
+ },
260
+ "mode": {
261
+ "replacement": "p settings mode",
262
+ "reason": "Merged into settings command",
263
+ },
264
+ "reason": {
265
+ "replacement": "p chat 'reason about <topic>'",
266
+ "reason": "Merged into chat command",
267
+ },
268
+ "blast": {
269
+ "replacement": "p impact",
270
+ "reason": "Renamed to impact command",
271
+ },
272
+ }
273
+
274
+ try:
275
+ from patchi.cli.registry import COMMANDS
276
+
277
+ cmd_names = [cmd.name for cmd in COMMANDS]
278
+ found_stale = []
279
+ for name, info in stale_commands.items():
280
+ if name not in cmd_names:
281
+ found_stale.append((name, info))
282
+
283
+ if found_stale:
284
+ con.print("[bold]Stale Commands Detected[/bold]")
285
+ con.print("[dim]These commands have been merged or removed. Update your scripts:[/dim]")
286
+ con.print()
287
+
288
+ table = Table(show_header=True, header_style="bold #C8621A", box=None)
289
+ table.add_column("Command", style="red", width=15)
290
+ table.add_column("Replacement", style="green", width=25)
291
+ table.add_column("Reason", style="dim")
292
+
293
+ for name, info in found_stale:
294
+ table.add_row(
295
+ f"p {name}",
296
+ info["replacement"],
297
+ info["reason"],
298
+ )
299
+ warnings += 1
300
+
301
+ con.print(table)
302
+ con.print()
303
+
304
+ # ── --fix: auto-update stale references in scripts/ ──
305
+ if fix:
306
+ _fix_stale_references(root, found_stale)
307
+ else:
308
+ checks.append(("Stale commands", "✓", "No stale commands found", "#4ADE80"))
309
+ except Exception as e:
310
+ _log.warning("Stale command check failed: %s", e)
311
+
312
+ # ── 5. API keys ───────────────────────────────────────────────────────────
313
+ if root:
314
+ try:
315
+ from patchi.core import config as cfg
316
+
317
+ config = cfg.load(root)
318
+ ai_keys = config.get("ai", {}).get("keys", [])
319
+ if ai_keys:
320
+ checks.append(("API keys", "✓", f"{len(ai_keys)} key(s) configured", "#4ADE80"))
321
+ else:
322
+ checks.append(("API keys", "⚠", "No keys — run 'p key add'", "#FACC15"))
323
+ warnings += 1
324
+ except Exception as e:
325
+ checks.append(("API keys", "✗", str(e)[:60], "#FF4D6D"))
326
+ errors += 1
327
+
328
+ # ── 6. Optional: test tooling ─────────────────────────────────────────────
329
+ con.print()
330
+ _OPTIONAL_TEST = [
331
+ ("pytest", "pytest", "pytest", "Unit test runner"),
332
+ ("npx", "", "node/npm", "Playwright host"),
333
+ ("locust", "locust", "locust", "Stress test runner"),
334
+ ]
335
+ for cmd, import_name, pip_name, desc in _OPTIONAL_TEST:
336
+ try:
337
+ present = (shutil.which(cmd) is not None) or (
338
+ import_name and importlib.import_module(import_name)
339
+ )
340
+ except ImportError:
341
+ present = False
342
+ status = "✓" if present else "—"
343
+ color = "#4ADE80" if present else "#6B7280"
344
+ note = desc if present else f"Optional — {pip_name}"
345
+ checks.append((f"[dim]opt:[/dim] {cmd}", status, note, color))
346
+
347
+ # ── 7. Optional: security tooling ────────────────────────────────────────
348
+ try:
349
+ from patchi.core.agents.tool_health import check_tool
350
+
351
+ _OPTIONAL_SECURITY = [
352
+ ("bandit", "bandit", "bandit", "Python security linter"),
353
+ ("semgrep", "", "semgrep", "Multi-language SAST scanner"),
354
+ ]
355
+ for cmd, _import_name, pip_name, desc in _OPTIONAL_SECURITY:
356
+ try:
357
+ st = check_tool(cmd)
358
+ if st["status"] == "ok":
359
+ note = f"{desc} ({st['version']})"
360
+ checks.append((f"[dim]opt:[/dim] {cmd}", "✓", note, "#4ADE80"))
361
+ elif st["status"] == "broken":
362
+ hint = st["hint"]
363
+ checks.append(
364
+ (f"[dim]opt:[/dim] {cmd}", "✗", f"Broken: {hint}", "#FF4D6D")
365
+ )
366
+ errors += 1
367
+ else:
368
+ note = f"Optional — {pip_name}"
369
+ checks.append((f"[dim]opt:[/dim] {cmd}", "—", note, "#6B7280"))
370
+ except Exception:
371
+ note = f"Optional — {pip_name}"
372
+ checks.append((f"[dim]opt:[/dim] {cmd}", "—", note, "#6B7280"))
373
+ except ImportError:
374
+ pass
375
+
376
+ # ── 8. .patchi/ size warning ─────────────────────────────────────────────
377
+ if root:
378
+ try:
379
+ from patchi.cli.commands.cleanup_cmd import get_patchi_size
380
+
381
+ size_info = get_patchi_size(root)
382
+ total_bytes = size_info["total_bytes"]
383
+ total_files = size_info["total_files"]
384
+
385
+ mb = total_bytes / (1024 * 1024)
386
+ if total_bytes > 50 * 1024 * 1024:
387
+ size_str = f"{mb:.1f} MB"
388
+ warn_msg = f"{size_str} ({total_files} files) — consider 'p cleanup'"
389
+ checks.append((".patchi/ size", "⚠", warn_msg, "#FACC15"))
390
+ warnings += 1
391
+ else:
392
+ if total_bytes >= 1024 * 1024:
393
+ size_str = f"{mb:.1f} MB"
394
+ else:
395
+ size_str = f"{total_bytes / 1024:.1f} KB"
396
+ info_msg = f"{size_str} ({total_files} files)"
397
+ checks.append((".patchi/ size", "✓", info_msg, "#4ADE80"))
398
+ except Exception as _exc:
399
+ _log.warning('run failed: %s', _exc)
400
+
401
+ # ── Render results ────────────────────────────────────────────────────────
402
+ if json_output:
403
+ import json as _json
404
+
405
+ con.print(
406
+ _json.dumps(
407
+ {
408
+ "ok": errors == 0,
409
+ "errors": errors,
410
+ "warnings": warnings,
411
+ "checks": [
412
+ {"label": label, "status": status.strip(), "note": note}
413
+ for label, status, note, _color in checks
414
+ ],
415
+ },
416
+ indent=2,
417
+ )
418
+ )
419
+ return
420
+
421
+ table = Table(show_header=False, box=None, pad_edge=False, padding=(0, 1))
422
+ table.add_column("Status", width=4)
423
+ table.add_column("Check", style="bold #F2EDD6", width=28)
424
+ table.add_column("Note", style="dim", width=55)
425
+
426
+ for label, status, note, color in checks:
427
+ table.add_row(Text(status, style=color), label, note)
428
+
429
+ con.print(table)
430
+
431
+ # ── Summary ───────────────────────────────────────────────────────────────
432
+ con.print()
433
+ if errors == 0 and warnings == 0:
434
+ con.print(
435
+ Panel(
436
+ "[bold #4ADE80]All checks passed. Patchi is ready.[/bold #4ADE80]",
437
+ border_style="#4ADE80",
438
+ padding=(0, 1),
439
+ )
440
+ )
441
+ elif errors == 0:
442
+ con.print(
443
+ Panel(
444
+ f"[bold #FACC15]{warnings} warning(s). Core functionality OK.[/bold #FACC15]",
445
+ border_style="#FACC15",
446
+ padding=(0, 1),
447
+ )
448
+ )
449
+ else:
450
+ con.print(
451
+ Panel(
452
+ f"[bold #FF4D6D]{errors} error(s), {warnings} warning(s). Fix errors above.[/bold #FF4D6D]",
453
+ border_style="#FF4D6D",
454
+ padding=(0, 1),
455
+ )
456
+ )
457
+ con.print()
@@ -0,0 +1,187 @@
1
+ """p findings — view and manage security findings.
2
+
3
+ Subcommands:
4
+ p findings — show all findings from last scan
5
+ p findings --summary — per-agent before/after counts (repeatable)
6
+ p findings --json — machine-readable output
7
+ p findings --save-baseline — snapshot current counts as baseline
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import logging
14
+ from pathlib import Path
15
+
16
+ from rich.console import Console
17
+ from rich.table import Table
18
+
19
+ _log = logging.getLogger("patchi.cli.commands.findings_cmd")
20
+
21
+
22
+
23
+ def run(args) -> None:
24
+ con = Console()
25
+ root = Path.cwd()
26
+
27
+ if getattr(args, "save_baseline", False):
28
+ _save_baseline(root, con)
29
+ return
30
+
31
+ if getattr(args, "summary", False):
32
+ _show_summary(root, con, json_output=getattr(args, "json_output", False))
33
+ return
34
+
35
+ _show_findings(root, con, json_output=getattr(args, "json_output", False))
36
+
37
+
38
+ def _load_scan_results(root: Path) -> dict:
39
+ """Load scan results from .patchi/."""
40
+ results_path = root / ".patchi" / "memory" / "scan_results.json"
41
+ if not results_path.exists():
42
+ return {}
43
+ try:
44
+ return json.loads(results_path.read_text(encoding="utf-8"))
45
+ except Exception:
46
+ return {}
47
+
48
+
49
+ def _show_findings(root: Path, con: Console, json_output: bool = False) -> None:
50
+ """Show all findings from last scan."""
51
+ results = _load_scan_results(root)
52
+ if not results:
53
+ con.print("[yellow]No scan results found. Run `p scan` first.[/yellow]")
54
+ return
55
+
56
+ all_findings = []
57
+ for agent_name, data in results.items():
58
+ for f in data.get("findings", []):
59
+ f["agent"] = agent_name
60
+ all_findings.append(f)
61
+
62
+ sev_order = {"critical": 0, "high": 1, "medium": 2, "low": 3, "info": 4}
63
+ all_findings.sort(key=lambda f: sev_order.get(f.get("severity", "info"), 5))
64
+
65
+ if json_output:
66
+ con.print(
67
+ json.dumps(
68
+ {"total": len(all_findings), "findings": all_findings}, indent=2, default=str
69
+ )
70
+ )
71
+ return
72
+
73
+ # Summary
74
+ by_sev = {}
75
+ for f in all_findings:
76
+ s = f.get("severity", "info")
77
+ by_sev[s] = by_sev.get(s, 0) + 1
78
+ sev_parts = ", ".join(f"{s}: {c}" for s, c in sorted(by_sev.items()))
79
+ con.print(f"\n[bold]Findings[/bold] ({len(all_findings)} total: {sev_parts})\n")
80
+
81
+ # Table
82
+ table = Table(show_header=True, header_style="bold")
83
+ table.add_column("#", style="dim", width=4)
84
+ table.add_column("Sev", width=8)
85
+ table.add_column("Type", width=20)
86
+ table.add_column("File", width=30)
87
+ table.add_column("Line", width=6)
88
+ table.add_column("Agent", width=16)
89
+ table.add_column("Message", max_width=40)
90
+
91
+ for i, f in enumerate(all_findings, 1):
92
+ sev = f.get("severity", "info")
93
+ sev_style = {
94
+ "critical": "bold red",
95
+ "high": "red",
96
+ "medium": "yellow",
97
+ "low": "dim",
98
+ "info": "dim",
99
+ }.get(sev, "")
100
+ table.add_row(
101
+ str(i),
102
+ f"[{sev_style}]{sev}[/{sev_style}]",
103
+ f.get("type", "?"),
104
+ f.get("file", "?")[-30:],
105
+ str(f.get("line", 0)),
106
+ f.get("agent", "?"),
107
+ (f.get("message", "")[:80]),
108
+ )
109
+
110
+ con.print(table)
111
+
112
+
113
+ def _show_summary(root: Path, con: Console, json_output: bool = False) -> None:
114
+ """Show per-agent finding counts with before/after comparison."""
115
+ results = _load_scan_results(root)
116
+ baseline_path = root / ".patchi" / "findings_baseline.json"
117
+ baseline = {}
118
+ if baseline_path.exists():
119
+ try:
120
+ baseline = json.loads(baseline_path.read_text(encoding="utf-8"))
121
+ except Exception as _exc:
122
+ _log.warning('_show_summary failed: %s', _exc)
123
+
124
+ # Current counts
125
+ agent_counts = {}
126
+ total = 0
127
+ for agent_name, data in results.items():
128
+ count = len(data.get("findings", []))
129
+ agent_counts[agent_name] = count
130
+ total += count
131
+
132
+ if json_output:
133
+ rows = []
134
+ for agent, count in sorted(agent_counts.items()):
135
+ prev = baseline.get(agent, count)
136
+ rows.append(
137
+ {
138
+ "agent": agent,
139
+ "current": count,
140
+ "baseline": prev,
141
+ "delta": count - prev,
142
+ }
143
+ )
144
+ con.print(json.dumps({"total": total, "agents": rows}, indent=2))
145
+ return
146
+
147
+ con.print(
148
+ f"\n[bold]Finding Counts Summary[/bold] — {len(agent_counts)} agents, {total} total findings\n"
149
+ )
150
+
151
+ table = Table(show_header=True, header_style="bold")
152
+ table.add_column("Agent", width=28)
153
+ table.add_column("Current", justify="right", width=8)
154
+ table.add_column("Baseline", justify="right", width=8)
155
+ table.add_column("Delta", justify="right", width=8)
156
+
157
+ for agent, count in sorted(agent_counts.items()):
158
+ prev = baseline.get(agent, count)
159
+ delta = count - prev
160
+ delta_str = f"+{delta}" if delta > 0 else str(delta)
161
+ delta_style = "red" if delta > 0 else ("green" if delta < 0 else "dim")
162
+ table.add_row(
163
+ agent,
164
+ str(count),
165
+ str(prev),
166
+ f"[{delta_style}]{delta_str}[/{delta_style}]",
167
+ )
168
+
169
+ con.print(table)
170
+
171
+ if not baseline:
172
+ con.print("\n[dim]No baseline set. Run `p findings --save-baseline` to create one.[/dim]")
173
+
174
+
175
+ def _save_baseline(root: Path, con: Console) -> None:
176
+ """Snapshot current finding counts as baseline."""
177
+ results = _load_scan_results(root)
178
+ baseline = {}
179
+ for agent_name, data in results.items():
180
+ baseline[agent_name] = len(data.get("findings", []))
181
+
182
+ baseline_path = root / ".patchi" / "findings_baseline.json"
183
+ baseline_path.parent.mkdir(parents=True, exist_ok=True)
184
+ baseline_path.write_text(json.dumps(baseline, indent=2), encoding="utf-8")
185
+ con.print(
186
+ f"[green]Baseline saved: {len(baseline)} agents, {sum(baseline.values())} total findings[/green]"
187
+ )