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,442 @@
1
+ """
2
+ TestScanner — test file discovery, coverage mapping.
3
+
4
+ Scans for test files and maps which source files they cover:
5
+ - Jest/React (test.js, test.jsx, spec.js, spec.jsx, *.test.*, *.spec.*)
6
+ - PyTest/Python (test_*.py, *_test.py, test/*.py)
7
+ - JUnit/Java (Test*.java, *Test.java)
8
+ - PHPUnit/PHP (Test*.php, *Test.php)
9
+ - RSpec/Ruby (*_spec.rb, spec/*.rb)
10
+
11
+ Maps test → source relationships and identifies uncovered files.
12
+
13
+ Does NOT call AI.
14
+ Does NOT write to disk.
15
+ Does NOT touch the queue.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import logging
21
+ import re
22
+ from pathlib import Path
23
+
24
+ from patchi.core.brain.languages import DEFAULT_IGNORE_DIRS
25
+
26
+ from .base import (
27
+ AgentGroup,
28
+ AgentInput,
29
+ AgentResult,
30
+ BaseAgent,
31
+ Finding,
32
+ Severity,
33
+ make_finding,
34
+ register,
35
+ safe_rglob,
36
+ )
37
+
38
+ _log = logging.getLogger("patchi.agents.test_scanner")
39
+
40
+
41
+ @register
42
+ class TestScanner(BaseAgent):
43
+ """Scanner for test files and coverage mapping."""
44
+
45
+ group = AgentGroup.SCANNER
46
+ name = "TestScanner"
47
+ description = "Test file discovery, coverage mapping"
48
+
49
+ def _run(self, inp: AgentInput, result: AgentResult) -> None:
50
+ """Scan for test files and map coverage."""
51
+ findings = []
52
+
53
+ # Define test file patterns
54
+ test_patterns = [
55
+ # JavaScript/TypeScript
56
+ "*test*.js",
57
+ "*test*.jsx",
58
+ "*test*.ts",
59
+ "*test*.tsx",
60
+ "*spec*.js",
61
+ "*spec*.jsx",
62
+ "*spec*.ts",
63
+ "*spec*.tsx",
64
+ "*.test.js",
65
+ "*.test.jsx",
66
+ "*.test.ts",
67
+ "*.test.tsx",
68
+ "*.spec.js",
69
+ "*.spec.jsx",
70
+ "*.spec.ts",
71
+ "*.spec.tsx",
72
+ "**/test/**",
73
+ "**/__tests__/**",
74
+ "**/tests/**",
75
+ # Python
76
+ "test_*.py",
77
+ "*_test.py",
78
+ "tests/**/*.py",
79
+ "**/test/**/*.py",
80
+ "conftest.py",
81
+ "pytest.ini",
82
+ "tox.ini",
83
+ # Java
84
+ "*Test.java",
85
+ "Test*.java",
86
+ "**/test/**/java/**",
87
+ "**/src/test/**",
88
+ # PHP
89
+ "*Test.php",
90
+ "Test*.php",
91
+ "*Test.php",
92
+ "**/tests/**",
93
+ # Ruby
94
+ "*_spec.rb",
95
+ "spec/**/*.rb",
96
+ "**/spec/**",
97
+ # Go
98
+ "*_test.go",
99
+ # Rust
100
+ "*_test.rs",
101
+ "tests/**/*.rs",
102
+ ]
103
+
104
+ # Search for test files
105
+ test_files = set()
106
+ for pattern in test_patterns:
107
+ for file_path in safe_rglob(inp.root, pattern):
108
+ if file_path.is_file():
109
+ rel_path = file_path.relative_to(inp.root).as_posix()
110
+ if not self._should_skip_file(rel_path, inp):
111
+ test_files.add(rel_path)
112
+
113
+ # Map test files to source files
114
+ test_mappings = self._map_tests_to_source(test_files, inp)
115
+
116
+ # Count total test files
117
+ findings.append(
118
+ self._mkf(
119
+ severity=Severity.INFO,
120
+ file="__summary__",
121
+ line_start=0,
122
+ title=f"Test Files Found: {len(test_files)}",
123
+ description=f"Discovered {len(test_files)} test files in project",
124
+ evidence=f"Patterns: {len(test_patterns)} used",
125
+ )
126
+ )
127
+
128
+ # Add findings for each test file
129
+ for test_file in sorted(test_files):
130
+ findings.append(
131
+ self._mkf(
132
+ severity=Severity.INFO,
133
+ file=test_file,
134
+ line_start=0,
135
+ title="Test File",
136
+ description="Discovered test file",
137
+ evidence="Test file identified by pattern matching",
138
+ )
139
+ )
140
+
141
+ # Add findings for coverage mappings
142
+ for test_file, source_files in test_mappings.items():
143
+ if source_files:
144
+ findings.append(
145
+ self._mkf(
146
+ severity=Severity.INFO,
147
+ file=test_file,
148
+ line_start=0,
149
+ title="Covers Source Files",
150
+ description=f"Test file covers: {', '.join(sorted(source_files)[:3])}{'...' if len(source_files) > 3 else ''}",
151
+ evidence=f"Covers {len(source_files)} source files",
152
+ )
153
+ )
154
+
155
+ # Identify uncovered source files
156
+ all_source_files = self._find_all_source_files(inp)
157
+ covered_files = set()
158
+ for source_list in test_mappings.values():
159
+ covered_files.update(source_list)
160
+
161
+ uncovered_files = all_source_files - covered_files
162
+ for uncovered in sorted(uncovered_files):
163
+ findings.append(
164
+ self._mkf(
165
+ severity=Severity.MEDIUM,
166
+ file=uncovered,
167
+ line_start=0,
168
+ title="Uncovered Source File",
169
+ description="Source file has no corresponding test coverage",
170
+ evidence="Not referenced by any test file",
171
+ )
172
+ )
173
+
174
+ # Calculate coverage percentage
175
+ total_sources = len(all_source_files)
176
+ covered_count = len(covered_files)
177
+ coverage_pct = (covered_count / total_sources * 100) if total_sources > 0 else 0
178
+
179
+ findings.append(
180
+ self._mkf(
181
+ severity=Severity.INFO,
182
+ file="__coverage__",
183
+ line_start=0,
184
+ title=f"Test Coverage: {coverage_pct:.1f}%",
185
+ description=f"Coverage: {covered_count}/{total_sources} files covered",
186
+ evidence=f"{covered_count} covered, {len(uncovered_files)} uncovered",
187
+ )
188
+ )
189
+
190
+ for finding in findings:
191
+ result.add_finding(finding)
192
+
193
+ result.data["test_files"] = sorted(test_files)
194
+ result.data["source_files"] = sorted(all_source_files)
195
+ result.data["coverage_pct"] = coverage_pct
196
+ result.data["uncovered_files"] = sorted(uncovered_files)
197
+ result.data["test_files_found"] = len(test_files)
198
+ result.data["covered_files"] = covered_count
199
+ result.data["uncovered_file_count"] = len(uncovered_files)
200
+ result.data["needs_ai"] = False
201
+ result.files_scanned = len(test_files) + len(all_source_files)
202
+
203
+ def _mkf(self, *args, **kwargs) -> Finding:
204
+ """Compatibility wrapper for finding construction."""
205
+ if args and isinstance(args[0], Severity):
206
+ severity = args[0]
207
+ file = args[1] if len(args) > 1 else ""
208
+ line_start = args[2] if len(args) > 2 else 0
209
+ title = args[3] if len(args) > 3 else ""
210
+ description = args[4] if len(args) > 4 else ""
211
+ evidence = args[5] if len(args) > 5 else ""
212
+ finding_type = kwargs.pop("finding_type", None) or title.lower().replace(
213
+ " ", "_"
214
+ ).replace(":", "").replace("'", "").replace("-", "_")
215
+ return make_finding(
216
+ self.name,
217
+ finding_type,
218
+ severity,
219
+ file,
220
+ description,
221
+ line=line_start,
222
+ evidence=evidence,
223
+ **kwargs,
224
+ )
225
+
226
+ if "severity" in kwargs and ("file" in kwargs or "file_path" in kwargs):
227
+ severity = kwargs.pop("severity")
228
+ file = kwargs.pop("file", kwargs.pop("file_path", ""))
229
+ line_start = kwargs.pop("line_start", kwargs.pop("line", 0))
230
+ title = kwargs.pop("title", "")
231
+ description = kwargs.pop("description", title)
232
+ evidence = kwargs.pop("evidence", "")
233
+ finding_type = kwargs.pop("finding_type", None) or title.lower().replace(
234
+ " ", "_"
235
+ ).replace(":", "").replace("'", "").replace("-", "_")
236
+ return make_finding(
237
+ self.name,
238
+ finding_type,
239
+ severity,
240
+ file,
241
+ description,
242
+ line=line_start,
243
+ evidence=evidence,
244
+ **kwargs,
245
+ )
246
+
247
+ return make_finding(*args, **kwargs)
248
+
249
+ def _should_skip_file(self, file_path: str, inp: AgentInput) -> bool:
250
+ """Check if file should be skipped based on restrictions."""
251
+ # Skip ignored directories (node_modules, .venv, etc.)
252
+ from pathlib import PurePosixPath
253
+
254
+ if any(p in DEFAULT_IGNORE_DIRS for p in PurePosixPath(file_path).parts):
255
+ return True
256
+
257
+ # Check restrictions
258
+ restrictions = inp.config.get("restrictions", [])
259
+ for r in restrictions:
260
+ if r.get("enabled", True):
261
+ path = r["path"]
262
+ if file_path.startswith(path) or PurePosixPath(file_path).match(path):
263
+ if r["type"] == "NO_TOUCH":
264
+ return True
265
+ elif r["type"] == "SCAN_ONLY" and self.__class__.__name__ == "FixAgent":
266
+ return True
267
+ return False
268
+
269
+ def _map_tests_to_source(self, test_files: set[str], inp: AgentInput) -> dict[str, list[str]]:
270
+ """Map test files to the source files they likely cover."""
271
+ mappings = {}
272
+
273
+ for test_file in test_files:
274
+ test_path = Path(test_file)
275
+ test_name = test_path.stem.replace(".test", "").replace(".spec", "")
276
+
277
+ # Remove common test suffixes/prefixes
278
+ clean_test_name = test_name
279
+ for suffix in ["_test", "test_", "_spec", "spec_"]:
280
+ if clean_test_name.endswith(suffix):
281
+ clean_test_name = clean_test_name[: -len(suffix)]
282
+ if clean_test_name.startswith(suffix):
283
+ clean_test_name = clean_test_name[len(suffix) :]
284
+
285
+ # Find potential source file matches
286
+ potential_matches = []
287
+
288
+ # Look for source file with same name in adjacent directories
289
+ test_dir = test_path.parent
290
+ for ext in [".js", ".jsx", ".ts", ".tsx", ".py", ".java", ".php", ".rb", ".go", ".rs"]:
291
+ # Same directory
292
+ alt_name = test_dir / f"{clean_test_name}{ext}"
293
+ if (inp.root / alt_name).exists():
294
+ potential_matches.append(str(alt_name))
295
+
296
+ # Adjacent src directory
297
+ if "test" in str(test_dir).lower() or "spec" in str(test_dir).lower():
298
+ # We're in a test directory, look for source in sibling src/ directory
299
+ parts = test_dir.parts
300
+ for i, part in enumerate(parts):
301
+ if part in ["test", "tests", "__tests__", "spec"]:
302
+ # Try replacing with 'src'
303
+ new_parts = list(parts)
304
+ new_parts[i] = "src"
305
+ src_dir = Path(*new_parts)
306
+ src_file = src_dir / f"{clean_test_name}{ext}"
307
+ if (inp.root / src_file).exists():
308
+ potential_matches.append(str(src_file))
309
+
310
+ # Look for source in src directory
311
+ src_alt = (
312
+ Path("src") / test_path.relative_to(test_dir).parent / f"{clean_test_name}{ext}"
313
+ )
314
+ if (inp.root / src_alt).exists():
315
+ potential_matches.append(str(src_alt))
316
+
317
+ # Also look for imports/requires in the test file that might indicate covered sources
318
+ try:
319
+ test_content_path = inp.root / test_file
320
+ if test_content_path.exists():
321
+ content = test_content_path.read_text(encoding="utf-8")
322
+ imported_sources = self._find_imported_sources(content, test_file, inp)
323
+ potential_matches.extend(imported_sources)
324
+ except Exception as e:
325
+ _log.warning("TestScanner._map_tests_to_source failed: %s", e)
326
+
327
+ # Remove duplicates and filter to actual existing files
328
+ unique_matches = []
329
+ for match in potential_matches:
330
+ if match not in unique_matches and (inp.root / match).exists():
331
+ unique_matches.append(match)
332
+
333
+ mappings[test_file] = unique_matches
334
+
335
+ return mappings
336
+
337
+ def _find_imported_sources(self, content: str, test_file: str, inp: AgentInput) -> list[str]:
338
+ """Find source files imported by the test file."""
339
+ matches = []
340
+ test_ext = Path(test_file).suffix
341
+
342
+ # Different import patterns based on language
343
+ if test_ext in [".js", ".jsx", ".ts", ".tsx"]:
344
+ # JavaScript/TypeScript import patterns
345
+ patterns = [
346
+ r"from\s+['\"](\.{1,2}/[^'\"]+)['\"]", # ES6 imports
347
+ r"import\s+['\"](\.{1,2}/[^'\"]+)['\"]", # import statements
348
+ r"require\s*\(\s*['\"](\.{1,2}/[^'\"]+)['\"]\s*\)", # require calls
349
+ r"require\.resolve\s*\(\s*['\"](\.{1,2}/[^'\"]+)['\"]\s*\)", # require.resolve
350
+ ]
351
+
352
+ for pattern in patterns:
353
+ for match in re.finditer(pattern, content):
354
+ import_path = match.group(1)
355
+ # Resolve relative path
356
+ resolved_path = Path(test_file).parent / import_path
357
+ resolved_path = resolved_path.resolve().relative_to(inp.root.resolve())
358
+ source_path = str(resolved_path).replace("\\", "/")
359
+
360
+ # Check if it exists and is not another test file
361
+ if (inp.root / source_path).exists() and not self._is_test_file(source_path):
362
+ matches.append(source_path)
363
+
364
+ elif test_ext == ".py":
365
+ # Python import patterns
366
+ patterns = [
367
+ r"from\s+([a-zA-Z_][a-zA-Z0-9_.]*)\s+import", # from x import y
368
+ r"import\s+([a-zA-Z_][a-zA-Z0-9_.]+)", # import x
369
+ r"from\s+\.{1,2}/?([a-zA-Z_][a-zA-Z0-9_/]+)", # relative imports
370
+ ]
371
+
372
+ for pattern in patterns:
373
+ for match in re.finditer(pattern, content):
374
+ import_module = match.group(1).replace(".", "/")
375
+ # Try to convert module name to file path
376
+ for ext in [".py", ".pyx"]:
377
+ module_path = f"{import_module}{ext}"
378
+ if (inp.root / module_path).exists() and not self._is_test_file(
379
+ module_path
380
+ ):
381
+ matches.append(module_path)
382
+
383
+ return matches
384
+
385
+ def _is_test_file(self, file_path: str) -> bool:
386
+ """Check if a file is a test file."""
387
+ path = Path(file_path)
388
+ stem = path.stem.lower()
389
+ parent_dirs = [part.lower() for part in path.parts]
390
+
391
+ # Check if filename contains test/spec patterns
392
+ test_indicators = ["test", "spec"]
393
+ if any(indicator in stem for indicator in test_indicators):
394
+ return True
395
+
396
+ # Check if file is in test directories
397
+ if any(test_dir in parent_dirs for test_dir in ["test", "tests", "__tests__", "spec"]):
398
+ return True
399
+
400
+ # Check if it's a test file by extension pattern
401
+ if any(
402
+ test_pattern in file_path.lower()
403
+ for test_pattern in ["test.", "spec.", ".test", ".spec"]
404
+ ):
405
+ return True
406
+
407
+ return False
408
+
409
+ def _find_all_source_files(self, inp: AgentInput) -> set[str]:
410
+ """Find all source files in the project (excluding test files)."""
411
+ source_extensions = {
412
+ ".js",
413
+ ".jsx",
414
+ ".ts",
415
+ ".tsx", # JavaScript/TypeScript
416
+ ".py", # Python
417
+ ".java", # Java
418
+ ".php", # PHP
419
+ ".rb", # Ruby
420
+ ".go", # Go
421
+ ".rs", # Rust
422
+ ".cpp",
423
+ ".cxx",
424
+ ".cc",
425
+ ".c",
426
+ ".h",
427
+ ".hpp", # C/C++
428
+ ".cs", # C#
429
+ }
430
+
431
+ all_files = set()
432
+
433
+ for ext in source_extensions:
434
+ for file_path in safe_rglob(inp.root, f"*{ext}"):
435
+ if file_path.is_file():
436
+ rel_path = file_path.relative_to(inp.root).as_posix()
437
+ if not self._is_test_file(rel_path) and not self._should_skip_file(
438
+ rel_path, inp
439
+ ):
440
+ all_files.add(rel_path)
441
+
442
+ return all_files
@@ -0,0 +1,109 @@
1
+ """
2
+ Tool Health — three-state probe for external security tooling.
3
+
4
+ Reconstructed module (referenced by cli/commands/doctor_cmd.py; original
5
+ was lost before any commit existed). A tool that is installed but not
6
+ runnable must be reported as *broken*, never silently as present-and-fine
7
+ or as absent:
8
+
9
+ check_tool("bandit") -> {"status": "ok", "version": "1.7.5", "hint": ""}
10
+ {"status": "broken", "version": "", "hint": "..."}
11
+ {"status": "missing", "version": "", "hint": ""}
12
+
13
+ _TOOLS is the registry of known probeable tools; unknown names fall back
14
+ to the caller's own PATH/import check.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import logging
20
+ import re
21
+ import shutil
22
+ import subprocess
23
+
24
+ _log = logging.getLogger("patchi.core.agents.tool_health")
25
+
26
+ _VERSION_TIMEOUT_S = 10
27
+
28
+ # Tools this module knows how to probe end-to-end.
29
+ _TOOLS = frozenset(
30
+ {
31
+ "bandit",
32
+ "semgrep",
33
+ "codeql",
34
+ "pyre",
35
+ "safety",
36
+ "pip-audit",
37
+ "git",
38
+ "node",
39
+ "npm",
40
+ "ollama",
41
+ }
42
+ )
43
+
44
+ # Per-tool run flags: some tools don't answer `--version` nicely.
45
+ _VERSION_ARGS: dict[str, list[str]] = {
46
+ "codeql": ["version"],
47
+ "pyre": ["--version"],
48
+ }
49
+
50
+ # Focused hints when a present tool fails to run.
51
+ _BROKEN_HINTS: dict[str, str] = {
52
+ "codeql": "codeql binary missing — install the CodeQL CLI and add it to PATH",
53
+ "pyre": "pyre.bin not runnable on this platform — see pyre-check install docs",
54
+ }
55
+
56
+
57
+ def _extract_version(output: str, tool: str) -> str:
58
+ """Pull a plausible version token out of a --version blob."""
59
+ for line in output.splitlines():
60
+ line = line.strip()
61
+ if not line:
62
+ continue
63
+ m = re.search(r"\d+\.\d+(?:\.\d+)?[a-zA-Z0-9.\-]*", line)
64
+ if m:
65
+ return m.group(0)
66
+ # Fall back to first non-empty line, minus the tool name echo
67
+ lowered = line.lower()
68
+ if tool.lower() not in lowered:
69
+ return line[:40]
70
+ return ""
71
+
72
+
73
+ def check_tool(tool: str) -> dict:
74
+ """Probe one external tool. Never raises."""
75
+ path = shutil.which(tool)
76
+ if path is None:
77
+ return {"status": "missing", "version": "", "hint": ""}
78
+
79
+ args = _VERSION_ARGS.get(tool, ["--version"])
80
+ try:
81
+ proc = subprocess.run( # noqa: S603 — fixed argv, no shell
82
+ [path, *args],
83
+ capture_output=True,
84
+ text=True,
85
+ timeout=_VERSION_TIMEOUT_S,
86
+ )
87
+ except subprocess.TimeoutExpired:
88
+ hint = _BROKEN_HINTS.get(tool, f"{tool} did not answer within {_VERSION_TIMEOUT_S}s")
89
+ return {"status": "broken", "version": "", "hint": hint}
90
+ except OSError as exc:
91
+ return {
92
+ "status": "broken",
93
+ "version": "",
94
+ "hint": _BROKEN_HINTS.get(tool, f"cannot execute {path}: {exc}"),
95
+ }
96
+
97
+ if proc.returncode != 0:
98
+ hint = _BROKEN_HINTS.get(
99
+ tool,
100
+ f"{tool} exits {proc.returncode} on {args[0]} — reinstall or check its runtime deps",
101
+ )
102
+ return {"status": "broken", "version": "", "hint": hint}
103
+
104
+ blob = (proc.stdout or "") + (proc.stderr or "")
105
+ return {
106
+ "status": "ok",
107
+ "version": _extract_version(blob, tool),
108
+ "hint": "",
109
+ }