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,692 @@
1
+ """
2
+ DeadCodeScanner — vulture + import-graph dual-signal dead code detection.
3
+
4
+ Strategy (per MASTER_REBUILD_BRIEF FIX 2C):
5
+ - Import graph detects file-level dead code (files with no importers).
6
+ - vulture (when installed) detects symbol-level dead code at confidence >= 90.
7
+ - Symbol findings are only reported when BOTH graph AND vulture agree.
8
+ - File-level dead code is always detected via graph alone.
9
+ - Patchi's own infrastructure folders are never flagged.
10
+
11
+ No keyword grep. No hardcoded summaries. Every scan derives from real code.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ import logging
17
+ import shutil
18
+ import subprocess
19
+ from pathlib import Path
20
+
21
+ from ..brain.ast_utils import find_dead_symbols
22
+ from ..brain.import_graph import ImportGraph, build_import_graph, find_dead_files
23
+ from ..brain.languages import EXTENSION_MAP, Lang
24
+ from .base import (
25
+ AgentGroup,
26
+ AgentInput,
27
+ AgentResult,
28
+ BaseAgent,
29
+ Severity,
30
+ make_finding,
31
+ register,
32
+ )
33
+
34
+ # Folders that are intentional entry points or infrastructure — never flag them
35
+ _SKIP_SEGMENTS = frozenset(
36
+ {
37
+ "cli",
38
+ "core",
39
+ "web",
40
+ "tests",
41
+ "test",
42
+ "commands",
43
+ "notifications",
44
+ "hosted",
45
+ "docs",
46
+ "scripts",
47
+ "benchmark",
48
+ "examples",
49
+ "example",
50
+ "demo",
51
+ "__pycache__",
52
+ ".patchi",
53
+ "node_modules",
54
+ "dist",
55
+ "build",
56
+ }
57
+ )
58
+
59
+
60
+ _log = logging.getLogger("patchi.agents.dead_code_scanner")
61
+
62
+
63
+ def _run_sglyon_deadcode(root: Path) -> list[dict] | None:
64
+ """Run sglyon/deadcode if installed; parses JSON output."""
65
+ import shutil
66
+ import subprocess
67
+ import json
68
+
69
+ # Try binary first, then python -m deadcode
70
+ candidates = []
71
+ if shutil.which("deadcode"):
72
+ candidates.append(["deadcode", "--format", "json", str(root)])
73
+ # sglyon/deadcode also provides `deadcode` entrypoint via pip; fallback to module
74
+ candidates.append([shutil.which("python") or "python", "-m", "deadcode", "--format", "json", str(root)])
75
+ for cmd in candidates:
76
+ if not cmd[0] or (cmd[0] not in ("python", "python3") and not shutil.which(cmd[0])):
77
+ continue
78
+ try:
79
+ proc = subprocess.run(cmd, capture_output=True, text=True, timeout=90)
80
+ if proc.returncode not in (0, 1):
81
+ continue
82
+ raw = proc.stdout.strip() or proc.stderr.strip()
83
+ if not raw:
84
+ continue
85
+ data = json.loads(raw)
86
+ # sglyon/deadcode JSON: [{file, line, symbol, kind}]
87
+ out: list[dict] = []
88
+ for item in data if isinstance(data, list) else data.get("results", []):
89
+ f = item.get("file") or item.get("filename") or ""
90
+ try:
91
+ rel = Path(f).relative_to(root).as_posix() if f else ""
92
+ except ValueError:
93
+ rel = f
94
+ if _should_skip(rel):
95
+ continue
96
+ out.append(
97
+ {
98
+ "file": rel or f,
99
+ "line": int(item.get("line", 0) or 0),
100
+ "name": item.get("symbol") or item.get("name", ""),
101
+ "type": item.get("kind") or item.get("type", "deadcode"),
102
+ "message": f"sglyon/deadcode: {item.get('kind','unused')} {item.get('symbol','')}",
103
+ "confidence": 90,
104
+ "code": "",
105
+ }
106
+ )
107
+ if out:
108
+ return out
109
+ except (subprocess.TimeoutExpired, json.JSONDecodeError, FileNotFoundError, OSError) as exc:
110
+ _log.debug("sglyon deadcode %s failed: %s", cmd[0], exc)
111
+ continue
112
+ return None
113
+
114
+
115
+ @register
116
+ class DeadCodeScanner(BaseAgent):
117
+ """Dead code detection via vulture + import graph dual-signal."""
118
+
119
+ group = AgentGroup.SCANNER
120
+ name = "DeadCodeScanner"
121
+ timeout = 120
122
+
123
+ def _run(self, inp: AgentInput, result: AgentResult) -> None:
124
+ # 1. Try sglyon/deadcode first (unified JSON for Python/JS/TS/Go/Elixir)
125
+ sg = _run_sglyon_deadcode(inp.root)
126
+ if sg:
127
+ for item in sg:
128
+ result.findings.append(
129
+ make_finding(
130
+ severity=Severity.LOW,
131
+ file=item.get("file", ""),
132
+ line_start=item.get("line", 0),
133
+ title=f"Dead code (sglyon): {item.get('name', '')}",
134
+ description=item.get("message", ""),
135
+ evidence=item.get("code", ""),
136
+ finding_type="dead_code",
137
+ )
138
+ )
139
+ result.data["orchestrator"] = "sglyon/deadcode"
140
+ result.data["sglyon_count"] = len(sg)
141
+ result.status = AgentStatus.SUCCEEDED
142
+ result.files_scanned = len({f.file for f in result.findings}) if result.findings else 0
143
+ result.data["total_findings"] = len(result.findings)
144
+ return
145
+
146
+ # 2. Fallback: existing dual-signal (import graph + vulture + per-language tools)
147
+ # Build import graph — always available, no external tool needed
148
+ try:
149
+ graph = build_import_graph(inp.root)
150
+ except Exception as exc:
151
+ result.add_error(f"import graph failed: {exc}")
152
+ graph = ImportGraph()
153
+
154
+ # File-level dead code from graph alone
155
+ try:
156
+ from ..brain.scanner import FileScanner
157
+
158
+ scanner = FileScanner(inp.root, ignore_paths=inp.config.get("ignore_paths", []))
159
+ all_files = scanner.scan()
160
+ graph_dead_files = set(find_dead_files(all_files, graph))
161
+ except Exception as e:
162
+ _log.warning("DeadCodeScanner._run failed: %s", e)
163
+ all_files = []
164
+ graph_dead_files = set()
165
+
166
+ # Run vulture for symbol-level analysis (optional)
167
+ vulture_findings = _run_vulture(inp.root)
168
+
169
+ # Run per-language dead code tools (ts-prune, cargo deadlinks, etc.)
170
+ tool_findings = _run_all_tools(inp.root)
171
+ vulture_dead_files: set[str] = set()
172
+ if vulture_findings is not None:
173
+ for vf in vulture_findings:
174
+ if vf.get("confidence", 0) >= 90:
175
+ vulture_dead_files.add(vf["file"])
176
+
177
+ confirmed_dead: list[str] = []
178
+ uncertain: list[str] = []
179
+
180
+ # File-level findings
181
+ for rel_path in sorted(graph_dead_files):
182
+ if _should_skip(rel_path):
183
+ continue
184
+ is_vulture_confirmed = vulture_findings is not None and rel_path in vulture_dead_files
185
+ if is_vulture_confirmed:
186
+ confirmed_dead.append(rel_path)
187
+ result.findings.append(
188
+ make_finding(
189
+ severity=Severity.MEDIUM,
190
+ file=rel_path,
191
+ line_start=0,
192
+ title=f"Confirmed dead file: {rel_path}",
193
+ description="Not imported anywhere and vulture confirms no live symbols.",
194
+ finding_type="confirmed_dead",
195
+ )
196
+ )
197
+ else:
198
+ uncertain.append(rel_path)
199
+ result.findings.append(
200
+ make_finding(
201
+ severity=Severity.LOW,
202
+ file=rel_path,
203
+ line_start=0,
204
+ title=f"Possibly unused file: {rel_path}",
205
+ description="Not imported anywhere in the project.",
206
+ finding_type="uncertain_dead",
207
+ )
208
+ )
209
+
210
+ # Symbol-level findings from vulture (only when file is also graph-dead)
211
+ if vulture_findings:
212
+ for vf in vulture_findings:
213
+ rel_path = vf.get("file", "")
214
+ if _should_skip(rel_path):
215
+ continue
216
+ if vf.get("confidence", 0) < 90:
217
+ continue
218
+ # Only report symbol if the file is already in our dead set
219
+ if rel_path not in graph_dead_files:
220
+ continue
221
+ if rel_path in confirmed_dead or rel_path in uncertain:
222
+ continue # already reported at file level
223
+ result.findings.append(
224
+ make_finding(
225
+ severity=Severity.MEDIUM,
226
+ file=rel_path,
227
+ line_start=vf.get("line", 0),
228
+ title=f"Unused {vf.get('type', 'symbol')}: {vf.get('name', '')}",
229
+ description=f"vulture ({vf.get('confidence')}%): {vf.get('message', '')}",
230
+ evidence=vf.get("code", ""),
231
+ finding_type="dead_code",
232
+ )
233
+ )
234
+
235
+ # Per-language tool findings (not graph-cross-referenced)
236
+ for tf in tool_findings:
237
+ if not tf.get("file") or _should_skip(tf["file"]):
238
+ continue
239
+ result.findings.append(
240
+ make_finding(
241
+ severity=Severity.LOW,
242
+ file=tf["file"],
243
+ line_start=tf.get("line", 0),
244
+ title=f"Dead code ({tf.get('type', 'unknown')}): {tf.get('name', '')}",
245
+ description=tf.get("message", ""),
246
+ evidence=tf.get("code", ""),
247
+ finding_type="dead_code",
248
+ )
249
+ )
250
+
251
+ result.data["confirmed_dead"] = confirmed_dead
252
+ result.data["uncertain"] = uncertain
253
+ result.data["vulture_available"] = vulture_findings is not None
254
+ result.data["tool_findings_count"] = len(tool_findings)
255
+ result.files_scanned = len(graph.nodes)
256
+
257
+ # ── AST-based intra-file dead symbol detection (all parser langs) ──────
258
+ # Complements vulture (Python) and external per-language tools. Symbols
259
+ # for graph-dead files get a line-level dead-code signal even when no
260
+ # external toolchain is installed (multi-language parity).
261
+ ast_symbol_count = 0
262
+ for rel_path in sorted(graph_dead_files):
263
+ if _should_skip(rel_path):
264
+ continue
265
+ ext = Path(rel_path).suffix.lower()
266
+ lang = EXTENSION_MAP.get(ext)
267
+ if lang is None:
268
+ continue
269
+ if lang == Lang.PYTHON and vulture_findings is not None:
270
+ continue # vulture covers Python more precisely
271
+ fp = inp.root / rel_path
272
+ try:
273
+ content = fp.read_text(encoding="utf-8", errors="replace")
274
+ except OSError:
275
+ continue
276
+ for sym in find_dead_symbols(content, lang):
277
+ ast_symbol_count += 1
278
+ result.findings.append(
279
+ make_finding(
280
+ severity=Severity.LOW,
281
+ file=rel_path,
282
+ line_start=sym["line"],
283
+ title=f"Unused {sym['kind']}: {sym['name']}",
284
+ description=(
285
+ f"Symbol '{sym['name']}' is never referenced "
286
+ f"within {rel_path} (intra-file dead code)."
287
+ ),
288
+ finding_type="dead_code",
289
+ )
290
+ )
291
+ result.data["ast_symbol_findings"] = ast_symbol_count
292
+
293
+
294
+ # ── Per-language dead code tool helpers ─────────────────────────────────────────
295
+
296
+
297
+ def _detect_project_langs(root: Path) -> set[str]:
298
+ """Detect which languages the project uses by looking for manifest files."""
299
+ langs: set[str] = set()
300
+ if (root / "Cargo.toml").exists():
301
+ langs.add("rust")
302
+ if (root / "go.mod").exists():
303
+ langs.add("go")
304
+ if (root / "Package.swift").exists():
305
+ langs.add("swift")
306
+ if (root / "composer.json").exists():
307
+ langs.add("php")
308
+ if (root / "Gemfile").exists():
309
+ langs.add("ruby")
310
+ if (root / "pom.xml").exists():
311
+ langs.add("java")
312
+ if (root / "build.gradle").exists() or (root / "build.gradle.kts").exists():
313
+ langs.add("java")
314
+ if (root / "package.json").exists():
315
+ langs.add("javascript")
316
+ if (
317
+ (root / "requirements.txt").exists()
318
+ or (root / "setup.py").exists()
319
+ or (root / "pyproject.toml").exists()
320
+ ):
321
+ langs.add("python")
322
+ if list(root.glob("*.csproj")):
323
+ langs.add("csharp")
324
+ return langs
325
+
326
+
327
+ def _run_tool(
328
+ cmd: list[str],
329
+ root: Path,
330
+ timeout: int = 90,
331
+ ) -> subprocess.CompletedProcess | None:
332
+ if not shutil.which(cmd[0]):
333
+ return None
334
+ try:
335
+ return subprocess.run(
336
+ cmd,
337
+ capture_output=True,
338
+ text=True,
339
+ timeout=timeout,
340
+ )
341
+ except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
342
+ return None
343
+
344
+
345
+ def _run_vulture(root: Path) -> list[dict] | None:
346
+ """Run vulture for Python. Returns None if not installed."""
347
+ proc = _run_tool(["vulture", str(root), "--min-confidence", "90", "--json"], root)
348
+ if proc is None or proc.returncode not in (0, 1):
349
+ return None
350
+ raw = proc.stdout.strip()
351
+ if not raw:
352
+ return []
353
+ try:
354
+ data = json.loads(raw)
355
+ except json.JSONDecodeError:
356
+ return []
357
+ findings: list[dict] = []
358
+ for item in data:
359
+ abs_path = item.get("filename", "")
360
+ try:
361
+ rel = Path(abs_path).relative_to(root).as_posix()
362
+ except ValueError:
363
+ rel = abs_path
364
+ findings.append(
365
+ {
366
+ "file": rel,
367
+ "line": item.get("first_lineno", 0),
368
+ "name": item.get("name", ""),
369
+ "type": item.get("type", ""),
370
+ "message": item.get("message", ""),
371
+ "confidence": item.get("confidence", 0),
372
+ "code": item.get("code", ""),
373
+ }
374
+ )
375
+ return findings
376
+
377
+
378
+ def _run_ts_prune(root: Path) -> list[dict] | None:
379
+ """
380
+ Run ts-prune for TypeScript unused exports.
381
+ npm install -g ts-prune or npx ts-prune
382
+ """
383
+ proc = _run_tool(["npx", "--yes", "ts-prune", "--project", str(root / "tsconfig.json")], root)
384
+ if proc is None:
385
+ return None
386
+ findings: list[dict] = []
387
+ for line in proc.stdout.strip().splitlines():
388
+ if " - " not in line:
389
+ continue
390
+ parts = line.split(" - ", 1)
391
+ loc = parts[0].strip()
392
+ symbol = parts[1].strip() if len(parts) > 1 else ""
393
+ if ":" in loc:
394
+ file_part, line_part = loc.rsplit(":", 1)
395
+ line_num = int(line_part) if line_part.isdigit() else 0
396
+ else:
397
+ file_part = loc
398
+ line_num = 0
399
+ try:
400
+ rel = Path(file_part).as_posix()
401
+ except ValueError:
402
+ rel = file_part
403
+ findings.append(
404
+ {
405
+ "file": rel,
406
+ "line": line_num,
407
+ "name": symbol,
408
+ "type": "export",
409
+ "message": f"Unused export: {symbol}",
410
+ "confidence": 95,
411
+ "code": "",
412
+ }
413
+ )
414
+ return findings
415
+
416
+
417
+ def _run_cargo_dead_code(root: Path) -> list[dict] | None:
418
+ """Run cargo udeps for Rust unused dependencies."""
419
+ findings: list[dict] = []
420
+ proc = _run_tool(["cargo", "udeps"], root, timeout=120)
421
+ if proc is not None:
422
+ for line in proc.stderr.strip().splitlines():
423
+ if "unused" in line.lower():
424
+ findings.append(
425
+ {
426
+ "file": "Cargo.toml",
427
+ "line": 0,
428
+ "name": line.strip(),
429
+ "type": "unused_dependency",
430
+ "message": line.strip(),
431
+ "confidence": 90,
432
+ "code": "",
433
+ }
434
+ )
435
+ return findings or None
436
+
437
+
438
+ def _run_go_dead_code(root: Path) -> list[dict] | None:
439
+ """Run go vet + staticcheck for Go dead code."""
440
+ findings: list[dict] = []
441
+ # go vet -unusedresult removed in Go 1.19+; use go vet ./... with check for unused diagnostics
442
+ proc = _run_tool(["go", "vet", "./..."], root, timeout=120)
443
+ if proc is not None:
444
+ for line in proc.stderr.strip().splitlines():
445
+ if "unused" in line.lower() or "dead" in line.lower():
446
+ findings.append(
447
+ {
448
+ "file": "",
449
+ "line": 0,
450
+ "name": line.strip(),
451
+ "type": "go_dead_code",
452
+ "message": line.strip(),
453
+ "confidence": 85,
454
+ "code": "",
455
+ }
456
+ )
457
+ # golangci-lint with unused linter
458
+ proc = _run_tool(
459
+ ["golangci-lint", "run", "--disable-all", "--enable=unused", "--timeout=2m"],
460
+ root,
461
+ timeout=180,
462
+ )
463
+ if proc is not None:
464
+ for line in proc.stdout.strip().splitlines():
465
+ if "unused" in line.lower():
466
+ findings.append(
467
+ {
468
+ "file": "",
469
+ "line": 0,
470
+ "name": line.strip(),
471
+ "type": "go_dead_code",
472
+ "message": line.strip(),
473
+ "confidence": 90,
474
+ "code": "",
475
+ }
476
+ )
477
+ return findings or None
478
+
479
+
480
+ def _run_debride(root: Path) -> list[dict] | None:
481
+ """Run debride for Ruby dead code."""
482
+ proc = _run_tool(["debride", "."], root, timeout=120)
483
+ if proc is None:
484
+ return None
485
+ findings: list[dict] = []
486
+ for line in proc.stdout.strip().splitlines():
487
+ if ":" not in line:
488
+ continue
489
+ parts = line.split(":", 2)
490
+ if len(parts) < 2:
491
+ continue
492
+ file_part, line_part = parts[0], parts[1]
493
+ rest = parts[2] if len(parts) > 2 else ""
494
+ line_num = int(line_part) if line_part.isdigit() else 0
495
+ try:
496
+ rel = Path(file_part).as_posix()
497
+ except ValueError:
498
+ rel = file_part
499
+ findings.append(
500
+ {
501
+ "file": rel,
502
+ "line": line_num,
503
+ "name": rest.strip(),
504
+ "type": "ruby_dead_code",
505
+ "message": rest.strip(),
506
+ "confidence": 90,
507
+ "code": "",
508
+ }
509
+ )
510
+ return findings or None
511
+
512
+
513
+ def _run_composer_unused(root: Path) -> list[dict] | None:
514
+ """Run composer-unused for PHP unused dependencies."""
515
+ proc = _run_tool(["composer", "unused"], root, timeout=120)
516
+ if proc is None:
517
+ return None
518
+ findings: list[dict] = []
519
+ for line in proc.stdout.strip().splitlines():
520
+ if "unused" in line.lower():
521
+ findings.append(
522
+ {
523
+ "file": "composer.json",
524
+ "line": 0,
525
+ "name": line.strip(),
526
+ "type": "unused_dependency",
527
+ "message": line.strip(),
528
+ "confidence": 85,
529
+ "code": "",
530
+ }
531
+ )
532
+ return findings or None
533
+
534
+
535
+ def _run_dotnet_dead_code(root: Path) -> list[dict] | None:
536
+ """Run dotnet tool for C# dead code analysis."""
537
+ csproj_files = list(root.glob("*.csproj"))
538
+ if not csproj_files:
539
+ csproj_files = list(root.glob("**/*.csproj"))
540
+ if not csproj_files:
541
+ return None
542
+ proc = _run_tool(
543
+ ["dotnet", "list", str(csproj_files[0]), "package", "--vulnerable"], root, timeout=120
544
+ )
545
+ if proc is None:
546
+ return None
547
+ findings: list[dict] = []
548
+ for line in proc.stdout.strip().splitlines():
549
+ if "unused" in line.lower() or "not used" in line.lower():
550
+ findings.append(
551
+ {
552
+ "file": csproj_files[0].relative_to(root).as_posix(),
553
+ "line": 0,
554
+ "name": line.strip(),
555
+ "type": "unused_dependency",
556
+ "message": line.strip(),
557
+ "confidence": 80,
558
+ "code": "",
559
+ }
560
+ )
561
+ return findings or None
562
+
563
+
564
+ def _run_jdeps(root: Path) -> list[dict] | None:
565
+ """Run jdeps for Java unused dependencies."""
566
+ proc = _run_tool(["jdeps", "-summary", "-recursive", "."], root, timeout=120)
567
+ if proc is None:
568
+ # Try mvn dependency:analyze as an alternative
569
+ proc = _run_tool(["mvn", "dependency:analyze", "-DignoreNonCompile"], root, timeout=180)
570
+ if proc is None:
571
+ return None
572
+ findings: list[dict] = []
573
+ for line in proc.stdout.strip().splitlines():
574
+ if "Unused" in line or "unused" in line:
575
+ findings.append(
576
+ {
577
+ "file": "pom.xml",
578
+ "line": 0,
579
+ "name": line.strip(),
580
+ "type": "unused_dependency",
581
+ "message": line.strip(),
582
+ "confidence": 85,
583
+ "code": "",
584
+ }
585
+ )
586
+ return findings or None
587
+ findings = []
588
+ for line in proc.stdout.strip().splitlines():
589
+ if "->" not in line:
590
+ continue
591
+ parts = line.split("->")
592
+ if len(parts) >= 2:
593
+ source = parts[0].strip()
594
+ findings.append(
595
+ {
596
+ "file": source if source else "",
597
+ "line": 0,
598
+ "name": parts[1].strip(),
599
+ "type": "java_dependency",
600
+ "message": line.strip(),
601
+ "confidence": 80,
602
+ "code": "",
603
+ }
604
+ )
605
+ return findings or None
606
+
607
+
608
+ def _run_swift_dead_code(root: Path) -> list[dict] | None:
609
+ """Run swiftc dead code analysis for Swift."""
610
+ proc = _run_tool(["swiftc", "-typecheck", "-unused-imports"], root, timeout=120)
611
+ if proc is None:
612
+ return None
613
+ findings: list[dict] = []
614
+ for line in proc.stderr.strip().splitlines():
615
+ if "unused" in line.lower():
616
+ findings.append(
617
+ {
618
+ "file": "",
619
+ "line": 0,
620
+ "name": line.strip(),
621
+ "type": "swift_unused",
622
+ "message": line.strip(),
623
+ "confidence": 85,
624
+ "code": "",
625
+ }
626
+ )
627
+ return findings or None
628
+
629
+
630
+ def _run_clang_tidy(root: Path) -> list[dict] | None:
631
+ """Run clang-tidy for C/C++ dead code detection."""
632
+ proc = _run_tool(
633
+ ["clang-tidy", "--checks=-*,misc-unused-*", "--list-checks"], root, timeout=120
634
+ )
635
+ if proc is None:
636
+ return None
637
+ findings: list[dict] = []
638
+ for line in proc.stdout.strip().splitlines():
639
+ if "unused" in line.lower():
640
+ findings.append(
641
+ {
642
+ "file": "",
643
+ "line": 0,
644
+ "name": line.strip(),
645
+ "type": "c_cpp_unused",
646
+ "message": line.strip(),
647
+ "confidence": 80,
648
+ "code": "",
649
+ }
650
+ )
651
+ return findings or None
652
+
653
+
654
+ TOOL_DISPATCH: dict[str, list[tuple[str, str, str]]] = {
655
+ # Note: python/vulture is NOT listed — it's called directly in _run() for graph cross-referencing
656
+ "javascript": [("ts-prune", "unused exports", "ts_prune")],
657
+ "rust": [("cargo deadlinks/udeps", "dead code/deps", "cargo_dead_code")],
658
+ "go": [("go vet/golangci-lint", "dead code", "go_dead_code")],
659
+ "ruby": [("debride", "dead methods", "debride")],
660
+ "php": [("composer-unused", "unused deps", "composer_unused")],
661
+ "csharp": [("dotnet list package", "unused deps", "dotnet_dead_code")],
662
+ "java": [("jdeps/mvn dependency:analyze", "unused deps", "jdeps")],
663
+ "swift": [("swiftc -unused-imports", "unused imports", "swift_dead_code")],
664
+ "c": [("clang-tidy", "unused declarations", "clang_tidy")],
665
+ "cpp": [("clang-tidy", "unused declarations", "clang_tidy")],
666
+ }
667
+
668
+
669
+ def _run_all_tools(root: Path) -> list[dict]:
670
+ """Run all available dead code tools for the project's detected languages."""
671
+ all_findings: list[dict] = []
672
+ langs = _detect_project_langs(root)
673
+ for lang in langs:
674
+ _, _, func_suffix = (TOOL_DISPATCH.get(lang) or [("", "", "")])[0]
675
+ if not func_suffix:
676
+ continue
677
+ func_name = f"_run_{func_suffix}"
678
+ func = globals().get(func_name)
679
+ if func is None:
680
+ continue
681
+ try:
682
+ findings = func(root)
683
+ if findings:
684
+ all_findings.extend(findings)
685
+ except Exception as e:
686
+ _log.warning("_run_all_tools failed: %s", e)
687
+ return all_findings
688
+
689
+
690
+ def _should_skip(rel_path: str) -> bool:
691
+ parts = rel_path.lower().split("/")
692
+ return any(seg in _SKIP_SEGMENTS for seg in parts)