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,951 @@
1
+ """
2
+ RouteGraphScanner — framework-aware route extraction.
3
+
4
+ Detects and extracts routes from various frameworks:
5
+ - Express.js: app.get/post/put/delete/use() calls
6
+ - FastAPI: @app.get/post/put/delete() decorators
7
+ - Flask: @app.route() decorators
8
+ - Django: urlpatterns list entries
9
+ - Next.js: pages/ and app/ directory structure
10
+ - Spring Boot: @GetMapping/@PostMapping annotations
11
+ - ASP.NET Core: [HttpGet]/[HttpPost] attributes
12
+ - Ruby on Rails: routes.rb definitions
13
+
14
+ Extracts:
15
+ - HTTP method
16
+ - Route path
17
+ - Handler function name
18
+ - Middleware chain
19
+ - Authentication status
20
+
21
+ Does NOT call AI.
22
+ Does NOT write to disk.
23
+ Does NOT touch the queue.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import ast as py_ast
29
+ import logging
30
+ import re
31
+ from pathlib import Path
32
+ from typing import Any
33
+
34
+ from ..brain.languages import Lang, detect_language, get_parser, parse_source
35
+ from .base import (
36
+ _SKIP_DIRS,
37
+ _SKIP_FILES,
38
+ AgentGroup,
39
+ AgentInput,
40
+ AgentResult,
41
+ AgentStatus,
42
+ BaseAgent,
43
+ Finding,
44
+ Severity,
45
+ make_finding,
46
+ register,
47
+ safe_rglob,
48
+ )
49
+
50
+ _log = logging.getLogger("patchi.agents.route_graph_scanner")
51
+
52
+
53
+ @register
54
+ class RouteGraphScanner(BaseAgent):
55
+ """Scanner for web framework routes."""
56
+
57
+ group = AgentGroup.SCANNER
58
+ name = "RouteGraphScanner"
59
+ description = "Framework-aware route extraction"
60
+
61
+ def _run(self, inp: AgentInput, result: AgentResult) -> None:
62
+ """Scan for routes in web framework files."""
63
+ findings = []
64
+
65
+ # Define route file patterns
66
+ route_patterns = [
67
+ # JavaScript/TypeScript (Express, Next.js, etc.)
68
+ "*.js",
69
+ "*.jsx",
70
+ "*.ts",
71
+ "*.tsx",
72
+ "**/routes/**",
73
+ "**/api/**",
74
+ "**/pages/**",
75
+ "**/app/**",
76
+ # Python (FastAPI, Flask, Django)
77
+ "*.py",
78
+ "**/urls.py",
79
+ "**/views.py",
80
+ "**/routes.py",
81
+ "**/api/**",
82
+ # Java (Spring Boot)
83
+ "*.java",
84
+ # Ruby (Rails)
85
+ "*.rb",
86
+ "routes.rb",
87
+ # PHP (Laravel, Symfony)
88
+ "*.php",
89
+ # C# (ASP.NET)
90
+ "*.cs",
91
+ ]
92
+
93
+ # Search for route files and avoid scanning the same file twice
94
+ scanned_files = set()
95
+ # ponytail: rglob traverses node_modules (30k+ files). Walk with pruning.
96
+ import fnmatch
97
+ import os
98
+
99
+ for dirpath, dirnames, filenames in os.walk(inp.root):
100
+ dirnames[:] = [d for d in dirnames if d not in _SKIP_DIRS]
101
+ rel_dir = Path(dirpath).relative_to(inp.root).as_posix()
102
+ for fname in filenames:
103
+ if fname in _SKIP_FILES:
104
+ continue
105
+ for pattern in route_patterns:
106
+ if "**" in pattern:
107
+ parts = pattern.split("/")
108
+ if len(parts) == 1:
109
+ if fnmatch.fnmatch(fname, parts[0]):
110
+ break
111
+ else:
112
+ mid = parts[1] if len(parts) > 2 else None
113
+ if mid and mid in rel_dir.split("/"):
114
+ break
115
+ else:
116
+ if fnmatch.fnmatch(fname, pattern):
117
+ break
118
+ else:
119
+ continue
120
+ file_path = Path(dirpath) / fname
121
+ rel_path = f"{rel_dir}/{fname}" if rel_dir != "." else fname
122
+ if rel_path not in scanned_files:
123
+ scanned_files.add(rel_path)
124
+ if not self._should_skip_file(rel_path, inp):
125
+ findings.extend(self._scan_route_file(file_path, rel_path))
126
+
127
+ # Get only the route findings
128
+ route_findings = [f for f in findings if f.type.startswith("route_")]
129
+
130
+ # Guard at top: if no routes found, return early
131
+ if not route_findings:
132
+ result.status = AgentStatus.SUCCEEDED
133
+ result.findings = findings
134
+ result.data.update(
135
+ {
136
+ "routes_found": 0,
137
+ "route_map": {},
138
+ "framework_detected": self._detect_framework(inp),
139
+ "needs_ai": False,
140
+ }
141
+ )
142
+ return
143
+
144
+ findings.extend(self._detect_sensitive_routes(route_findings))
145
+ route_map = self._build_route_map(route_findings)
146
+
147
+ result.status = AgentStatus.SUCCEEDED
148
+ result.findings = findings
149
+ result.data.update(
150
+ {
151
+ "routes_found": len(route_findings),
152
+ "route_map": route_map,
153
+ "framework_detected": self._detect_framework(inp),
154
+ "needs_ai": False,
155
+ }
156
+ )
157
+ return
158
+
159
+ def _should_skip_file(self, file_path: str, inp: AgentInput) -> bool:
160
+ """Check if file should be skipped based on restrictions."""
161
+ from pathlib import PurePosixPath
162
+
163
+ p = PurePosixPath(file_path)
164
+ if any(s in p.parts for s in _SKIP_DIRS):
165
+ return True
166
+ if p.name in _SKIP_FILES:
167
+ return True
168
+
169
+ # Check restrictions
170
+ restrictions = inp.config.get("restrictions", [])
171
+ for r in restrictions:
172
+ if r.get("enabled", True):
173
+ path = r["path"]
174
+ if file_path.startswith(path) or PurePosixPath(file_path).match(path):
175
+ if r["type"] == "NO_TOUCH":
176
+ return True
177
+ elif r["type"] == "SCAN_ONLY" and self.__class__.__name__ == "FixAgent":
178
+ return True
179
+ return False
180
+
181
+ def _scan_route_file(self, file_path: Path, rel_path: str) -> list[Finding]:
182
+ """Scan a file for routes."""
183
+ findings = []
184
+
185
+ try:
186
+ content = file_path.read_text(encoding="utf-8")
187
+ lang = detect_language(file_path)
188
+
189
+ if lang in [Lang.JAVASCRIPT, Lang.TYPESCRIPT]:
190
+ findings.extend(self._scan_js_routes(content, rel_path))
191
+ elif lang == Lang.PYTHON:
192
+ findings.extend(self._scan_py_routes(content, rel_path))
193
+ elif lang == Lang.JAVA:
194
+ findings.extend(self._scan_java_routes(content, rel_path))
195
+ elif lang == Lang.RUBY:
196
+ findings.extend(self._scan_ruby_routes(content, rel_path))
197
+ elif lang == Lang.PHP:
198
+ findings.extend(self._scan_php_routes(content, rel_path))
199
+ elif lang == Lang.C_SHARP:
200
+ findings.extend(self._scan_cs_routes(content, rel_path))
201
+
202
+ except Exception as e:
203
+ findings.append(
204
+ make_finding(
205
+ severity=Severity.LOW,
206
+ file=rel_path,
207
+ line_start=0,
208
+ title="Route file read error",
209
+ description=f"Could not read route file {file_path.name}: {str(e)}",
210
+ evidence=str(e),
211
+ )
212
+ )
213
+
214
+ return findings
215
+
216
+ def _scan_py_routes(self, content: str, rel_path: str) -> list[Finding]:
217
+ """Scan Python files for routes (Flask, FastAPI, Django) using AST parsing."""
218
+ findings = []
219
+
220
+ try:
221
+ tree = py_ast.parse(content)
222
+ except SyntaxError:
223
+ # If AST parsing fails, fall back to basic pattern matching
224
+ return self._scan_py_routes_fallback(content, rel_path)
225
+
226
+ for node in py_ast.walk(tree):
227
+ # FastAPI patterns - look for decorator calls like @app.get("/path")
228
+ if isinstance(node, py_ast.FunctionDef) and node.decorator_list:
229
+ for decorator in node.decorator_list:
230
+ if isinstance(decorator, py_ast.Call) and isinstance(
231
+ decorator.func, py_ast.Attribute
232
+ ):
233
+ func_attr = decorator.func.attr
234
+ if func_attr in [
235
+ "get",
236
+ "post",
237
+ "put",
238
+ "delete",
239
+ "patch",
240
+ "options",
241
+ "head",
242
+ "trace",
243
+ ]:
244
+ # Check if this is a FastAPI app method
245
+ if isinstance(
246
+ decorator.func.value, py_ast.Name
247
+ ) and decorator.func.value.id in ["app", "router"]:
248
+ # Extract the route path from the decorator arguments
249
+ path_arg = None
250
+ if decorator.args:
251
+ # First argument should be the path
252
+ first_arg = decorator.args[0]
253
+ if isinstance(first_arg, py_ast.Constant):
254
+ path_arg = first_arg.value
255
+ elif isinstance(first_arg, py_ast.Str): # Python < 3.8
256
+ path_arg = first_arg.s
257
+
258
+ if path_arg:
259
+ findings.append(
260
+ make_finding(
261
+ severity=Severity.INFO,
262
+ file=rel_path,
263
+ line_start=node.lineno,
264
+ title=f"Route: {func_attr.upper()} {path_arg} (FastAPI)",
265
+ description=f"FastAPI HTTP {func_attr.upper()} route: {path_arg}",
266
+ evidence=f"@app.{func_attr}('{path_arg}')",
267
+ )
268
+ )
269
+
270
+ # Flask patterns - look for @app.route decorator
271
+ elif isinstance(node, py_ast.FunctionDef) and node.decorator_list:
272
+ for decorator in node.decorator_list:
273
+ if (
274
+ isinstance(decorator, py_ast.Call)
275
+ and isinstance(decorator.func, py_ast.Attribute)
276
+ and isinstance(decorator.func.value, py_ast.Name)
277
+ and decorator.func.value.id == "app"
278
+ and decorator.func.attr == "route"
279
+ ):
280
+ # Extract the route path from the decorator arguments
281
+ path_arg = None
282
+ if decorator.args:
283
+ first_arg = decorator.args[0]
284
+ if isinstance(first_arg, py_ast.Constant):
285
+ path_arg = first_arg.value
286
+ elif isinstance(first_arg, py_ast.Str): # Python < 3.8
287
+ path_arg = first_arg.s
288
+
289
+ if path_arg:
290
+ # Check for methods in keyword arguments
291
+ methods = ["GET"] # Default for Flask
292
+ for keyword in decorator.keywords:
293
+ if keyword.arg == "methods" and isinstance(
294
+ keyword.value, py_ast.List
295
+ ):
296
+ for elt in keyword.value.elts:
297
+ if isinstance(elt, py_ast.Constant):
298
+ methods.append(elt.value)
299
+ elif isinstance(elt, py_ast.Str): # Python < 3.8
300
+ methods.append(elt.s)
301
+
302
+ method_str = ", ".join(methods)
303
+ findings.append(
304
+ make_finding(
305
+ severity=Severity.INFO,
306
+ file=rel_path,
307
+ line_start=node.lineno,
308
+ title=f"Route: {method_str} {path_arg} (Flask)",
309
+ description=f"Flask HTTP {method_str} route: {path_arg}",
310
+ evidence=f"@app.route('{path_arg}')",
311
+ )
312
+ )
313
+
314
+ return findings
315
+
316
+ def _scan_py_routes_fallback(self, content: str, rel_path: str) -> list[Finding]:
317
+ """Fallback regex-based scanning for Python routes."""
318
+ findings = []
319
+
320
+ # FastAPI patterns
321
+ fastapi_methods = ["get", "post", "put", "delete", "patch", "options", "head", "trace"]
322
+ for method in fastapi_methods:
323
+ pattern = rf'@app\.{method}\s*\(\s*["\']([^"\']*)["\']'
324
+ matches = re.finditer(pattern, content, re.MULTILINE)
325
+ for match in matches:
326
+ path = match.group(1)
327
+ line_start = content[: match.start()].count("\n") + 1
328
+
329
+ findings.append(
330
+ make_finding(
331
+ severity=Severity.INFO,
332
+ file=rel_path,
333
+ line_start=line_start,
334
+ title=f"Route: {method.upper()} {path} (FastAPI)",
335
+ description=f"FastAPI HTTP {method.upper()} route: {path}",
336
+ evidence=match.group(0)[:100],
337
+ )
338
+ )
339
+
340
+ # Flask patterns
341
+ flask_route_pattern = r'@app\.route\s*\(\s*["\']([^"\']*)["\']'
342
+ flask_matches = re.finditer(flask_route_pattern, content, re.MULTILINE)
343
+ for match in flask_matches:
344
+ path = match.group(1)
345
+ line_start = content[: match.start()].count("\n") + 1
346
+
347
+ # Check if there are method restrictions
348
+ methods = ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD", "TRACE"]
349
+ method_matches = []
350
+ for method in methods:
351
+ if (
352
+ f"methods=['{method}']" in content[match.start() : match.start() + 200]
353
+ or f'methods=["{method}"]' in content[match.start() : match.start() + 200]
354
+ ):
355
+ method_matches.append(method)
356
+
357
+ if method_matches:
358
+ method_str = ", ".join(method_matches)
359
+ else:
360
+ method_str = "GET" # Default for Flask routes
361
+
362
+ findings.append(
363
+ make_finding(
364
+ severity=Severity.INFO,
365
+ file=rel_path,
366
+ line_start=line_start,
367
+ title=f"Route: {method_str} {path} (Flask)",
368
+ description=f"Flask HTTP {method_str} route: {path}",
369
+ evidence=match.group(0)[:100],
370
+ )
371
+ )
372
+
373
+ # Django patterns - look for url patterns
374
+ django_patterns = [
375
+ r"url\s*\(\s*['\"]([^'\"]*?)['\"]\s*,\s*([a-zA-Z_][a-zA-Z0-9_]*[.])*([a-zA-Z_][a-zA-Z0-9_]*)",
376
+ r"path\s*\(\s*['\"]([^'\"]*?)['\"]\s*,\s*([a-zA-Z_][a-zA-Z0-9_]*[.])*([a-zA-Z_][a-zA-Z0-9_]*)",
377
+ ]
378
+
379
+ for pattern in django_patterns:
380
+ matches = re.finditer(pattern, content, re.MULTILINE)
381
+ for match in matches:
382
+ path = match.group(1) if len(match.groups()) >= 1 else "dynamic"
383
+ handler = match.group(3) if len(match.groups()) >= 3 else "unknown"
384
+ line_start = content[: match.start()].count("\n") + 1
385
+
386
+ findings.append(
387
+ make_finding(
388
+ severity=Severity.INFO,
389
+ file=rel_path,
390
+ line_start=line_start,
391
+ title=f"Route: * {path} (Django) -> {handler}",
392
+ description=f"Django route: {path} handled by {handler}",
393
+ evidence=match.group(0)[:100],
394
+ )
395
+ )
396
+
397
+ return findings
398
+
399
+ def _detect_sensitive_routes(self, route_findings: list[Finding]) -> list[Finding]:
400
+ """Detect unprotected sensitive routes like login/auth endpoints."""
401
+ findings: list[Finding] = []
402
+
403
+ # Only check actual route findings (not all findings)
404
+ for finding in route_findings:
405
+ # Extract route path from the finding title or description
406
+ # Look for patterns like "Route: METHOD /path (Framework)"
407
+ route_match = re.search(
408
+ r"Route:\s*(?:([A-Z]+(?:,\s*[A-Z]+)*)\s+)?(/[-_a-zA-Z0-9/{}:.*\[\]]+)",
409
+ finding.title,
410
+ re.IGNORECASE,
411
+ )
412
+
413
+ if route_match:
414
+ methods_part = route_match.group(1)
415
+ path = route_match.group(2).lower()
416
+
417
+ # Determine methods if not specified
418
+ methods = [m.strip() for m in methods_part.split(",")] if methods_part else ["GET"]
419
+
420
+ # Check if the path contains sensitive keywords
421
+ sensitive_keywords = [
422
+ "/auth",
423
+ "/login",
424
+ "/signin",
425
+ "/signup",
426
+ "/register",
427
+ "/password",
428
+ "/admin",
429
+ "/dashboard",
430
+ ]
431
+
432
+ for keyword in sensitive_keywords:
433
+ if keyword in path:
434
+ for method in methods:
435
+ method_upper = method.strip().upper()
436
+ # Flag sensitive routes that use unsafe methods
437
+ if method_upper in {"POST", "PUT", "PATCH", "DELETE"}:
438
+ findings.append(
439
+ make_finding(
440
+ severity=Severity.MEDIUM,
441
+ finding_type="unprotected_sensitive_route",
442
+ file=finding.file,
443
+ line_start=finding.line,
444
+ title=f"Unprotected sensitive route: {method_upper} {path}",
445
+ description=f"Sensitive route {path} with potentially unsafe HTTP method {method_upper} appears unprotected",
446
+ evidence=finding.title,
447
+ )
448
+ )
449
+ break # Break to avoid duplicate checks for the same path
450
+
451
+ return findings
452
+
453
+ def _build_route_map(self, route_findings: list[Finding]) -> dict:
454
+ """Build a map of routes for analysis."""
455
+ route_map = {}
456
+ for finding in route_findings:
457
+ route_match = re.search(
458
+ r"Route:\s*(?:([A-Z]+(?:,\s*[A-Z]+)*)\s+)?(/[-_a-zA-Z0-9/{}:.*\[\]]+)",
459
+ finding.title,
460
+ re.IGNORECASE,
461
+ )
462
+ if route_match:
463
+ methods_part = route_match.group(1)
464
+ path = route_match.group(2)
465
+ methods = [m.strip() for m in methods_part.split(",")] if methods_part else ["GET"]
466
+
467
+ route_map[path] = {
468
+ "methods": methods,
469
+ "file": finding.file,
470
+ "line": finding.line,
471
+ "framework": finding.title.split("(")[-1].split(")")[0]
472
+ if "(" in finding.title
473
+ else "unknown",
474
+ }
475
+ return route_map
476
+
477
+ def _detect_framework(self, inp: AgentInput) -> str:
478
+ """Detect the web framework used in the project."""
479
+ # Look for framework-specific files
480
+ framework_indicators = {
481
+ "FastAPI": ["main.py", "app.py"],
482
+ "Flask": ["app.py", "application.py"],
483
+ "Django": ["manage.py", "settings.py"],
484
+ "Express.js": ["server.js", "app.js"],
485
+ "Next.js": ["next.config.js", "pages/", "app/"],
486
+ "Spring Boot": ["pom.xml", "build.gradle", "Application.java"],
487
+ }
488
+
489
+ for framework, indicators in framework_indicators.items():
490
+ for indicator in indicators:
491
+ if (inp.root / indicator).exists() or any(safe_rglob(inp.root, indicator)):
492
+ return framework
493
+
494
+ return "unknown"
495
+
496
+ def _scan_js_routes(self, content: str, rel_path: str) -> list[Finding]:
497
+ """Scan JavaScript/TypeScript files for routes using tree-sitter AST."""
498
+ findings: list[Finding] = []
499
+
500
+ parser = get_parser(Lang.JAVASCRIPT)
501
+ if parser is None:
502
+ return self._scan_js_routes_regex(content, rel_path)
503
+
504
+ try:
505
+ tree = parse_source(Lang.JAVASCRIPT, content)
506
+ buf = content.encode("utf-8")
507
+ self._walk_js_route_node(tree.root_node, buf, content, rel_path, findings, None)
508
+ except Exception as e:
509
+ _log.warning("RouteGraphScanner._scan_js_routes failed: %s", e)
510
+ return self._scan_js_routes_regex(content, rel_path)
511
+
512
+ return findings
513
+
514
+ def _scan_js_routes_regex(self, content: str, rel_path: str) -> list[Finding]:
515
+ """Regex fallback for JS/TS routes."""
516
+ findings = []
517
+ for pattern, framework in [
518
+ (
519
+ r'app\.(get|post|put|delete|patch|options|head)\s*\(\s*["\']([^"\']*)["\']',
520
+ "Express.js",
521
+ ),
522
+ (
523
+ r'router\.(get|post|put|delete|patch|options|head)\s*\(\s*["\']([^"\']*)["\']',
524
+ "Express.js Router",
525
+ ),
526
+ (
527
+ r'fastify\.(get|post|put|delete|patch|options|head)\s*\(\s*["\']([^"\']*)["\']',
528
+ "Fastify",
529
+ ),
530
+ ]:
531
+ for m in re.finditer(pattern, content, re.MULTILINE):
532
+ findings.append(
533
+ self._make_route_finding(
534
+ rel_path, content, m, m.group(1).upper(), m.group(2), framework
535
+ )
536
+ )
537
+ return findings
538
+
539
+ def _walk_js_route_node(
540
+ self,
541
+ node: Any,
542
+ buf: bytes,
543
+ content: str,
544
+ rel_path: str,
545
+ findings: list[Finding],
546
+ current_obj: str | None,
547
+ ) -> None:
548
+ ntype = node.type
549
+
550
+ if ntype == "call_expression":
551
+ method = ""
552
+ path = ""
553
+ obj = ""
554
+
555
+ for child in node.children:
556
+ if child.type == "member_expression":
557
+ for sub in child.children:
558
+ if sub.type == "identifier":
559
+ obj = (
560
+ buf[sub.start_byte : sub.end_byte].decode()
561
+ if hasattr(sub, "start_byte")
562
+ else ""
563
+ )
564
+ elif sub.type == "property_identifier":
565
+ method = (
566
+ buf[sub.start_byte : sub.end_byte].decode()
567
+ if hasattr(sub, "start_byte")
568
+ else ""
569
+ )
570
+ elif child.type == "arguments":
571
+ for arg in child.children:
572
+ if arg.type == "string":
573
+ path = (
574
+ buf[arg.start_byte : arg.end_byte].decode()
575
+ if hasattr(arg, "start_byte")
576
+ else ""
577
+ )
578
+ path = path.strip("'\"")
579
+ break
580
+
581
+ if method.lower() in ("get", "post", "put", "delete", "patch", "options", "head"):
582
+ if obj in ("app", "router", "fastify"):
583
+ framework = {
584
+ "app": "Express.js",
585
+ "router": "Express.js Router",
586
+ "fastify": "Fastify",
587
+ }.get(obj, "")
588
+ findings.append(
589
+ self._make_route_finding(
590
+ rel_path,
591
+ content,
592
+ None,
593
+ method.upper(),
594
+ path,
595
+ framework,
596
+ line=node.start_point[0] + 1 if node.start_point else 0,
597
+ )
598
+ )
599
+ elif method.lower() == "get" and obj:
600
+ # Could be a route declaration
601
+ pass
602
+
603
+ for child in node.children:
604
+ self._walk_js_route_node(child, buf, content, rel_path, findings, current_obj)
605
+
606
+ def _make_route_finding(
607
+ self,
608
+ rel_path: str,
609
+ content: str,
610
+ match: Any | None,
611
+ method: str,
612
+ path: str,
613
+ framework: str,
614
+ line: int = 0,
615
+ ) -> Finding:
616
+ if match:
617
+ line = content[: match.start()].count("\n") + 1
618
+ evidence = f"{method} {path}"
619
+ return make_finding(
620
+ severity=Severity.INFO,
621
+ file=rel_path,
622
+ line_start=line,
623
+ title=f"Route: {method} {path} ({framework})",
624
+ description=f"HTTP {method} route: {path}",
625
+ evidence=evidence,
626
+ )
627
+
628
+ def _scan_java_routes(self, content: str, rel_path: str) -> list[Finding]:
629
+ """Scan Java files for routes (Spring Boot) using tree-sitter AST."""
630
+ findings: list[Finding] = []
631
+
632
+ parser = get_parser(Lang.JAVA)
633
+ if parser is None:
634
+ return self._scan_java_routes_regex(content, rel_path)
635
+
636
+ try:
637
+ tree = parse_source(Lang.JAVA, content)
638
+ buf = content.encode("utf-8")
639
+ self._walk_java_route_node(tree.root_node, buf, rel_path, findings)
640
+ except Exception as e:
641
+ _log.warning("RouteGraphScanner._scan_java_routes failed: %s", e)
642
+ return self._scan_java_routes_regex(content, rel_path)
643
+
644
+ return findings
645
+
646
+ def _scan_java_routes_regex(self, content: str, rel_path: str) -> list[Finding]:
647
+ """Regex fallback for Java routes."""
648
+ findings = []
649
+ for m in re.finditer(
650
+ r'@(GetMapping|PostMapping|PutMapping|DeleteMapping|PatchMapping|RequestMapping)\s*\(\s*["\']([^"\']*)["\']',
651
+ content,
652
+ ):
653
+ annotation = m.group(1).replace("Mapping", "").upper()
654
+ if annotation == "REQUEST":
655
+ annotation = "GET"
656
+ findings.append(
657
+ self._make_route_finding(
658
+ rel_path, content, m, annotation, m.group(2), "Spring Boot"
659
+ )
660
+ )
661
+ return findings
662
+
663
+ def _walk_java_route_node(
664
+ self, node: Any, buf: bytes, rel_path: str, findings: list[Finding]
665
+ ) -> None:
666
+ ntype = node.type
667
+
668
+ if ntype == "marker_annotation":
669
+ ann_text = _node_text_buf(node, buf)
670
+ route_methods = {
671
+ "GetMapping": "GET",
672
+ "PostMapping": "POST",
673
+ "PutMapping": "PUT",
674
+ "DeleteMapping": "DELETE",
675
+ "PatchMapping": "PATCH",
676
+ }
677
+ for ann_name, method in route_methods.items():
678
+ if ann_text.startswith("@" + ann_name):
679
+ path = ""
680
+ for child in node.children:
681
+ if hasattr(child, "type") and child.type == "annotation_argument_list":
682
+ _node_text_buf(child, buf)
683
+ for arg in child.children:
684
+ if arg.type == "string":
685
+ p = _node_text_buf(arg, buf).strip("\"'")
686
+ if p:
687
+ path = p
688
+ break
689
+ findings.append(
690
+ self._make_route_finding(
691
+ rel_path,
692
+ "",
693
+ None,
694
+ method,
695
+ path or "/",
696
+ "Spring Boot",
697
+ line=node.start_point[0] + 1,
698
+ )
699
+ )
700
+ return
701
+ # Handle @RequestMapping with value=
702
+ if ann_text.startswith("@RequestMapping"):
703
+ path = ""
704
+ for child in node.children:
705
+ if hasattr(child, "type") and child.type == "annotation_argument_list":
706
+ for arg in child.children:
707
+ if arg.type == "string":
708
+ p = _node_text_buf(arg, buf).strip("\"'")
709
+ if p:
710
+ path = p
711
+ break
712
+ findings.append(
713
+ self._make_route_finding(
714
+ rel_path,
715
+ "",
716
+ None,
717
+ "GET",
718
+ path or "/",
719
+ "Spring Boot",
720
+ line=node.start_point[0] + 1,
721
+ )
722
+ )
723
+
724
+ for child in node.children if hasattr(node, "children") else []:
725
+ self._walk_java_route_node(child, buf, rel_path, findings)
726
+
727
+ def _scan_ruby_routes(self, content: str, rel_path: str) -> list[Finding]:
728
+ """Scan Ruby files for routes (Rails) using tree-sitter AST."""
729
+ findings: list[Finding] = []
730
+
731
+ parser = get_parser(Lang.RUBY)
732
+ if parser is None:
733
+ return self._scan_ruby_routes_regex(content, rel_path)
734
+
735
+ try:
736
+ tree = parse_source(Lang.RUBY, content)
737
+ buf = content.encode("utf-8")
738
+ self._walk_ruby_route_node(tree.root_node, buf, rel_path, findings)
739
+ except Exception as e:
740
+ _log.warning("RouteGraphScanner._scan_ruby_routes failed: %s", e)
741
+ return self._scan_ruby_routes_regex(content, rel_path)
742
+
743
+ return findings
744
+
745
+ def _scan_ruby_routes_regex(self, content: str, rel_path: str) -> list[Finding]:
746
+ """Regex fallback for Ruby routes."""
747
+ findings = []
748
+ for m in re.finditer(r'(get|post|put|patch|delete)\s+["\']([^"\']*)["\']', content):
749
+ findings.append(
750
+ self._make_route_finding(
751
+ rel_path, content, m, m.group(1).upper(), m.group(2), "Rails"
752
+ )
753
+ )
754
+ for m in re.finditer(r"resources\s+:(\w+)", content):
755
+ findings.append(
756
+ self._make_route_finding(rel_path, content, m, "RESOURCE", m.group(1), "Rails")
757
+ )
758
+ return findings
759
+
760
+ def _walk_ruby_route_node(
761
+ self, node: Any, buf: bytes, rel_path: str, findings: list[Finding]
762
+ ) -> None:
763
+ ntype = node.type
764
+ if ntype == "call":
765
+ method_name = ""
766
+ for child in node.children:
767
+ if child.type == "identifier":
768
+ method_name = _node_text_buf(child, buf)
769
+ break
770
+ route_methods = {"get", "post", "put", "patch", "delete", "resources"}
771
+ if method_name.lower() in route_methods:
772
+ path = ""
773
+ for child in node.children:
774
+ if child.type == "argument_list":
775
+ for arg in child.children:
776
+ if arg.type == "simple_symbol":
777
+ path = _node_text_buf(arg, buf)
778
+ if path.startswith(":"):
779
+ path = path[1:]
780
+ break
781
+ elif arg.type == "string":
782
+ path = _node_text_buf(arg, buf).strip("\"'")
783
+ break
784
+ if path:
785
+ method_display = (
786
+ method_name.upper() if method_name.lower() != "resources" else "RESOURCE"
787
+ )
788
+ findings.append(
789
+ self._make_route_finding(
790
+ rel_path,
791
+ "",
792
+ None,
793
+ method_display,
794
+ path,
795
+ "Rails",
796
+ line=node.start_point[0] + 1,
797
+ )
798
+ )
799
+
800
+ for child in node.children if hasattr(node, "children") else []:
801
+ self._walk_ruby_route_node(child, buf, rel_path, findings)
802
+
803
+ def _scan_php_routes(self, content: str, rel_path: str) -> list[Finding]:
804
+ """Scan PHP files for routes (Laravel) using tree-sitter AST."""
805
+ findings: list[Finding] = []
806
+
807
+ parser = get_parser(Lang.PHP)
808
+ if parser is None:
809
+ return self._scan_php_routes_regex(content, rel_path)
810
+
811
+ try:
812
+ tree = parse_source(Lang.PHP, content)
813
+ buf = content.encode("utf-8")
814
+ self._walk_php_route_node(tree.root_node, buf, rel_path, findings)
815
+ except Exception as e:
816
+ _log.warning("RouteGraphScanner._scan_php_routes failed: %s", e)
817
+ return self._scan_php_routes_regex(content, rel_path)
818
+
819
+ return findings
820
+
821
+ def _scan_php_routes_regex(self, content: str, rel_path: str) -> list[Finding]:
822
+ """Regex fallback for PHP routes."""
823
+ findings = []
824
+ for m in re.finditer(
825
+ r"Route::(get|post|put|patch|delete)\s*\(\s*['\"]([^'\"]*)['\"]\s*,", content
826
+ ):
827
+ findings.append(
828
+ self._make_route_finding(
829
+ rel_path, content, m, m.group(1).upper(), m.group(2), "Laravel"
830
+ )
831
+ )
832
+ return findings
833
+
834
+ def _walk_php_route_node(
835
+ self, node: Any, buf: bytes, rel_path: str, findings: list[Finding]
836
+ ) -> None:
837
+ ntype = node.type
838
+ if ntype == "function_call_expression" or ntype == "scoped_call_expression":
839
+ call_text = _node_text_buf(node, buf)
840
+ route_match = __import__("re").match(
841
+ r"Route::(get|post|put|patch|delete)\s*\(", call_text
842
+ )
843
+ if route_match:
844
+ method = route_match.group(1).upper()
845
+ path = ""
846
+ for child in node.children if hasattr(node, "children") else []:
847
+ if child.type == "arguments" or child.type == "argument_list":
848
+ for arg in child.children if hasattr(child, "children") else []:
849
+ if arg.type in ("encapsed_string", "string"):
850
+ path = _node_text_buf(arg, buf).strip("'\"")
851
+ break
852
+ break
853
+ if path:
854
+ findings.append(
855
+ self._make_route_finding(
856
+ rel_path,
857
+ "",
858
+ None,
859
+ method,
860
+ path,
861
+ "Laravel",
862
+ line=node.start_point[0] + 1,
863
+ )
864
+ )
865
+
866
+ for child in node.children if hasattr(node, "children") else []:
867
+ self._walk_php_route_node(child, buf, rel_path, findings)
868
+
869
+ def _scan_cs_routes(self, content: str, rel_path: str) -> list[Finding]:
870
+ """Scan C# files for routes (ASP.NET Core) using tree-sitter AST."""
871
+ findings: list[Finding] = []
872
+
873
+ parser = get_parser(Lang.C_SHARP)
874
+ if parser is None:
875
+ return self._scan_cs_routes_regex(content, rel_path)
876
+
877
+ try:
878
+ tree = parse_source(Lang.C_SHARP, content)
879
+ buf = content.encode("utf-8")
880
+ self._walk_cs_route_node(tree.root_node, buf, rel_path, findings)
881
+ except Exception as e:
882
+ _log.warning("RouteGraphScanner._scan_cs_routes failed: %s", e)
883
+ return self._scan_cs_routes_regex(content, rel_path)
884
+
885
+ return findings
886
+
887
+ def _scan_cs_routes_regex(self, content: str, rel_path: str) -> list[Finding]:
888
+ """Regex fallback for C# routes."""
889
+ findings = []
890
+ for pattern, method in [
891
+ (r'\[HttpGet\(["\']([^"\']*)["\']\s*\)\]', "GET"),
892
+ (r'\[HttpPost\(["\']([^"\']*)["\']\s*\)\]', "POST"),
893
+ (r'\[HttpPut\(["\']([^"\']*)["\']\s*\)\]', "PUT"),
894
+ (r'\[HttpDelete\(["\']([^"\']*)["\']\s*\)\]', "DELETE"),
895
+ (r'\[Route\(["\']([^"\']*)["\']\s*\)\]', "ROUTE"),
896
+ ]:
897
+ for m in re.finditer(pattern, content):
898
+ findings.append(
899
+ self._make_route_finding(
900
+ rel_path, content, m, method, m.group(1), "ASP.NET Core"
901
+ )
902
+ )
903
+ return findings
904
+
905
+ def _walk_cs_route_node(
906
+ self, node: Any, buf: bytes, rel_path: str, findings: list[Finding]
907
+ ) -> None:
908
+ ntype = node.type
909
+ if ntype == "attribute":
910
+ attr_text = _node_text_buf(node, buf)
911
+ route_methods = {
912
+ "HttpGet": "GET",
913
+ "HttpPost": "POST",
914
+ "HttpPut": "PUT",
915
+ "HttpDelete": "DELETE",
916
+ "Route": "ROUTE",
917
+ }
918
+ for attr_name, method in route_methods.items():
919
+ if attr_text.startswith("[" + attr_name) or attr_text.startswith(attr_name):
920
+ path = ""
921
+ for child in node.children if hasattr(node, "children") else []:
922
+ if child.type == "attribute_argument_list":
923
+ for arg in child.children if hasattr(child, "children") else []:
924
+ if arg.type == "string_literal":
925
+ path = _node_text_buf(arg, buf).strip("\"'")
926
+ break
927
+ break
928
+ findings.append(
929
+ self._make_route_finding(
930
+ rel_path,
931
+ "",
932
+ None,
933
+ method,
934
+ path or "/",
935
+ "ASP.NET Core",
936
+ line=node.start_point[0] + 1,
937
+ )
938
+ )
939
+ return
940
+
941
+ for child in node.children if hasattr(node, "children") else []:
942
+ self._walk_cs_route_node(child, buf, rel_path, findings)
943
+
944
+
945
+ def _node_text_buf(node: Any, buf: bytes) -> str:
946
+ """Extract text from a tree-sitter node using byte buffer."""
947
+ try:
948
+ return buf[node.start_byte : node.end_byte].decode("utf-8", errors="replace")
949
+ except Exception as e:
950
+ _log.debug("_node_text_buf failed: %s", e)
951
+ return ""