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,245 @@
1
+ """
2
+ SessionManagementAgent — Session management vulnerability detection.
3
+
4
+ Detects:
5
+ - Session fixation (no regeneration on login)
6
+ - Weak session ID generation (user-controlled or predictable)
7
+ - Missing/insecure cookie flags (HttpOnly, Secure, SameSite)
8
+ - Session timeout not configured
9
+ - Concurrent session control missing
10
+ - Session in URL parameters
11
+ - Missing session invalidation on logout
12
+ - Weak session storage (localStorage instead of httpOnly cookies)
13
+
14
+ Uses tree-sitter AST for call/identifier detection where beneficial, with
15
+ high-quality literal-pattern checks for configuration flags. NOT AI.
16
+ Does NOT write to disk. Does NOT touch the queue.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import os
22
+ import re
23
+
24
+ from ..agents.base import (
25
+ AgentDomain,
26
+ AgentGroup,
27
+ AgentInput,
28
+ AgentResult,
29
+ BaseAgent,
30
+ Finding,
31
+ Severity,
32
+ register,
33
+ safe_rglob,
34
+ )
35
+ from ..brain.ast_utils import find_calls
36
+ from ..brain.languages import EXTENSION_MAP, Lang
37
+
38
+ # Call names that set a session cookie (used for cookie-flag checks).
39
+ _COOKIE_SETTER_CALLS = {
40
+ "set_cookie",
41
+ "setcookie",
42
+ "cookie.set",
43
+ "cookies.set",
44
+ "session.cookie",
45
+ "response.set_cookie",
46
+ "res.cookie",
47
+ "ctx.cookie",
48
+ }
49
+ # Literal header / config forms that also set cookies.
50
+ _COOKIE_SETTER_LITERALS = re.compile(
51
+ r"(?i)\b(?:Set-Cookie|set_cookie|session\.cookie|cookie\.set)\b"
52
+ )
53
+
54
+
55
+ @register
56
+ class SessionManagementAgent(BaseAgent):
57
+ """Detects session management vulnerabilities."""
58
+
59
+ group = AgentGroup.SECURITY
60
+ domain = AgentDomain.SECURITY
61
+ name = "SessionManagementAgent"
62
+ description = (
63
+ "Session fixation, weak session IDs, missing cookie flags, insecure session storage"
64
+ )
65
+
66
+ def _run(self, inp: AgentInput, result: AgentResult) -> None:
67
+ patterns = ["*.py", "*.js", "*.ts", "*.jsx", "*.tsx", "*.java", "*.rb", "*.php", "*.go"]
68
+
69
+ for pat in patterns:
70
+ for fpath in safe_rglob(inp.root, pat):
71
+ if not fpath.is_file():
72
+ continue
73
+ rel = fpath.relative_to(inp.root).as_posix()
74
+ try:
75
+ content = fpath.read_text(encoding="utf-8", errors="ignore")
76
+ except OSError:
77
+ continue
78
+ result.files_scanned += 1
79
+ ext = os.path.splitext(rel)[1].lower()
80
+ lang = EXTENSION_MAP.get(ext)
81
+ self._scan_session(content, rel, result, lang)
82
+
83
+ # ── Helpers ─────────────────────────────────────────────────────────────
84
+
85
+ def _calls(self, content: str, lang: Lang | None, names: set[str]) -> list[dict]:
86
+ if lang is None:
87
+ return []
88
+ return find_calls(content, lang, names)
89
+
90
+ def _offset_of_line(self, content: str, line: int) -> int:
91
+ lines = content.splitlines()
92
+ offset = 0
93
+ for ln in lines[: max(0, line - 1)]:
94
+ offset += len(ln) + 1
95
+ return offset
96
+
97
+ # ── Scan ────────────────────────────────────────────────────────────────
98
+
99
+ def _scan_session(self, content: str, rel: str, result: AgentResult, lang: Lang | None) -> None:
100
+ # ── Session fixation — no regeneration on login ──────────────────────
101
+ login_match = re.search(r"(?i)(?:login|signin|authenticate)\s*\(", content)
102
+ if login_match:
103
+ login_line = content[: login_match.start()].count("\n") + 1
104
+ surrounding = content[login_match.start() : login_match.start() + 800]
105
+ if re.search(r'(?i)\bsession\[["\']?(?:user_id|user|username|email)\b', surrounding):
106
+ if not re.search(
107
+ r"(?i)(?:regenerate|rotate|regenerate_id|session\.new)", surrounding
108
+ ):
109
+ result.add_finding(
110
+ Finding(
111
+ agent=self.name,
112
+ type="session_fixation",
113
+ severity=Severity.HIGH,
114
+ file=rel,
115
+ line=login_line,
116
+ message="Session not regenerated on login — vulnerable to session fixation",
117
+ suggestion="Call session.regenerate() or rotate session ID after successful login",
118
+ cwe="CWE-384",
119
+ extra={"skill": "owasp-top10-web.skill"},
120
+ )
121
+ )
122
+
123
+ # ── Cookie flags missing / insecure ──────────────────────────────────
124
+ has_cookie_setter = bool(self._calls(content, lang, _COOKIE_SETTER_CALLS)) or bool(
125
+ _COOKIE_SETTER_LITERALS.search(content)
126
+ )
127
+ if has_cookie_setter:
128
+ if not re.search(r"(?i)\b(?:HttpOnly|httponly|http_only)\b", content):
129
+ result.add_finding(
130
+ Finding(
131
+ agent=self.name,
132
+ type="missing_httponly",
133
+ severity=Severity.MEDIUM,
134
+ file=rel,
135
+ line=0,
136
+ message="HTTP-only cookie flag not set — session ID accessible to JavaScript",
137
+ suggestion="Set HttpOnly flag on session cookies",
138
+ cwe="CWE-1004",
139
+ extra={"skill": "owasp-top10-web.skill"},
140
+ )
141
+ )
142
+ if not re.search(r"\bSecure\b", content, re.IGNORECASE):
143
+ result.add_finding(
144
+ Finding(
145
+ agent=self.name,
146
+ type="missing_secure_flag",
147
+ severity=Severity.MEDIUM,
148
+ file=rel,
149
+ line=0,
150
+ message="Secure cookie flag not set — session cookies sent over HTTP",
151
+ suggestion="Set Secure flag on session cookies for HTTPS-only transmission",
152
+ cwe="CWE-614",
153
+ extra={"skill": "owasp-top10-web.skill"},
154
+ )
155
+ )
156
+
157
+ # ── Session in URL parameter ────────────────────────────────────────
158
+ for m in re.finditer(r"(?i)\b(?:session|sid|session_id|token)\s*=\s*\{?\w+", content):
159
+ line = content[: m.start()].count("\n") + 1
160
+ if m.group(0).startswith(("session=", "sid=", "session_id=", "token=")):
161
+ result.add_finding(
162
+ Finding(
163
+ agent=self.name,
164
+ type="session_in_url",
165
+ severity=Severity.HIGH,
166
+ file=rel,
167
+ line=line,
168
+ message="Session identifier in URL parameter — exposed in referrer headers and logs",
169
+ suggestion="Move session ID to httpOnly cookies, never in URL",
170
+ cwe="CWE-598",
171
+ extra={"skill": "owasp-top10-web.skill"},
172
+ )
173
+ )
174
+
175
+ # ── Weak session storage (localStorage) ────────────────────────────
176
+ for call in self._calls(content, lang, {"setItem"}):
177
+ full = call.get("full_text", "")
178
+ if re.search(r"(?i)(?:token|session|jwt|access_token)", full):
179
+ result.add_finding(
180
+ Finding(
181
+ agent=self.name,
182
+ type="insecure_session_storage",
183
+ severity=Severity.CRITICAL,
184
+ file=rel,
185
+ line=call.get("line", 0),
186
+ message="Session/token stored in localStorage — accessible to XSS attacks",
187
+ suggestion="Use httpOnly cookies for session tokens instead of localStorage",
188
+ cwe="CWE-312",
189
+ extra={"skill": "owasp-top10-web.skill"},
190
+ )
191
+ )
192
+
193
+ # ── No session timeout / expiry ─────────────────────────────────────
194
+ if re.search(
195
+ r"(?i)\b(?:session|token)\b.*\b(?:expire|timeout|ttl|max_age|maxAge)\b", content
196
+ ):
197
+ pass # OK — has expiry configuration
198
+ elif re.search(r"(?i)\bsession\.(?:start|init|create|new)\s*\(", content):
199
+ result.add_finding(
200
+ Finding(
201
+ agent=self.name,
202
+ type="missing_session_timeout",
203
+ severity=Severity.LOW,
204
+ file=rel,
205
+ line=0,
206
+ message="Session created without explicit timeout/expiry configuration",
207
+ suggestion="Configure session TTL / max-age (recommended: 30-60 minutes)",
208
+ cwe="CWE-613",
209
+ extra={"skill": "owasp-top10-web.skill"},
210
+ )
211
+ )
212
+
213
+ # ── Missing logout / invalidation ───────────────────────────────────
214
+ if re.search(r"(?i)\bsession\.(?:destroy|invalidate|clear|delete)\b", content):
215
+ pass # OK — logout properly invalidates
216
+ elif re.search(r"(?i)(?:def\s+logout|function\s+logout|logout\s*=\s*function)", content):
217
+ result.add_finding(
218
+ Finding(
219
+ agent=self.name,
220
+ type="missing_session_invalidation",
221
+ severity=Severity.MEDIUM,
222
+ file=rel,
223
+ line=0,
224
+ message="Logout function present but session invalidation may be missing",
225
+ suggestion="Call session.destroy() or session.clear() on logout",
226
+ cwe="CWE-613",
227
+ extra={"skill": "owasp-top10-web.skill"},
228
+ )
229
+ )
230
+
231
+ # ── Weak session ID via user-controlled input ──────────────────────
232
+ for m in re.finditer(r"(?i)\bsession_id\s*=\s*(?:request\.|req\.|params\.|input)", content):
233
+ result.add_finding(
234
+ Finding(
235
+ agent=self.name,
236
+ type="user_controlled_session_id",
237
+ severity=Severity.CRITICAL,
238
+ file=rel,
239
+ line=content[: m.start()].count("\n") + 1,
240
+ message="Session ID derived from user-controlled input — session hijacking risk",
241
+ suggestion="Use server-generated cryptographically random session IDs",
242
+ cwe="CWE-384",
243
+ extra={"skill": "owasp-top10-web.skill"},
244
+ )
245
+ )
@@ -0,0 +1,422 @@
1
+ """
2
+ SSRFProtectionAgent — Server-Side Request Forgery protection analysis.
3
+
4
+ Scans for:
5
+ - Unvalidated URL/user-input passed to HTTP requests
6
+ - HTTP client calls with user-controlled URLs
7
+ - SSRF via redirect following
8
+ - Internal network access from user input
9
+ - File:// and other protocol handlers
10
+ - Missing URL validation/sanitization
11
+ - DNS rebinding potential
12
+ - Cloud metadata endpoint access (169.254.169.254)
13
+
14
+ Uses AST via ast_utils for HTTP client detection;
15
+ regex for literal text patterns (IPs, protocols, URL construction).
16
+ Does NOT call AI. Does NOT write to disk. Does NOT touch the queue.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import ipaddress
22
+ import logging
23
+ import os
24
+ from pathlib import Path
25
+ from urllib.parse import urlsplit
26
+
27
+ from ..agents.base import (
28
+ AgentDomain,
29
+ AgentGroup,
30
+ AgentInput,
31
+ AgentResult,
32
+ AgentStatus,
33
+ BaseAgent,
34
+ Finding,
35
+ Severity,
36
+ make_finding,
37
+ register,
38
+ )
39
+ from ..brain.ast_utils import HTTP_CLIENTS, find_assignments, find_calls, get_call_arg
40
+ from ..brain.code_query import string_literals
41
+ from ..brain.languages import DEFAULT_IGNORE_DIRS, Lang, detect_language
42
+ from ..brain.trace_log import trace_agent
43
+ from .pattern_loader import (
44
+ get_ssrf_internal_network_patterns,
45
+ get_ssrf_metadata_patterns,
46
+ get_ssrf_protocol_patterns,
47
+ load_patterns,
48
+ )
49
+
50
+ # Load YAML patterns once at module level
51
+ _SSRF_PATTERNS = load_patterns().get("ssrf", {})
52
+ _YAML_PROTOCOL = get_ssrf_protocol_patterns()
53
+ _YAML_METADATA = get_ssrf_metadata_patterns()
54
+ _YAML_INTERNAL = get_ssrf_internal_network_patterns()
55
+ _YAML_URL_VARS = set(_SSRF_PATTERNS.get("url_var_names", []))
56
+ _YAML_USER_INPUT = set(_SSRF_PATTERNS.get("user_input_markers", []))
57
+ _YAML_REDIRECT_SINKS = _SSRF_PATTERNS.get("redirect_sinks", {})
58
+
59
+
60
+ _log = logging.getLogger("patchi.security.ssrf_agent")
61
+
62
+
63
+ @register
64
+ class SSRFProtectionAgent(BaseAgent):
65
+ """Agent for detecting SSRF vulnerabilities."""
66
+
67
+ group = AgentGroup.SECURITY
68
+ domain = AgentDomain.SECURITY
69
+ name = "SSRFProtectionAgent"
70
+ description = (
71
+ "SSRF protection: unvalidated URLs, internal network access, metadata endpoint exposure"
72
+ )
73
+
74
+ # ── Source patterns for multi-language scanning ─────────────────────────
75
+ _SOURCE_PATTERNS = [
76
+ "*.py",
77
+ "*.js",
78
+ "*.jsx",
79
+ "*.ts",
80
+ "*.tsx",
81
+ "*.java",
82
+ "*.php",
83
+ "*.rb",
84
+ "*.go",
85
+ "*.rs",
86
+ "*.cs",
87
+ ]
88
+
89
+ # ── Dangerous protocol handlers (merged from YAML + defaults) ────────────
90
+ # Dangerous schemes / hosts, matched against parsed string literals
91
+ # (urllib.parse + ipaddress — comments and formatting can't misfire).
92
+ # User-supplied YAML customs stay pattern-based (see _YAML_* below).
93
+ _DANGEROUS_SCHEMES = {
94
+ "file": (
95
+ Severity.HIGH,
96
+ "file:// protocol handler — potential local file access via SSRF",
97
+ ),
98
+ "gopher": (
99
+ Severity.HIGH,
100
+ "gopher:// protocol handler — potential SSRF vector",
101
+ ),
102
+ "dict": (
103
+ Severity.MEDIUM,
104
+ "dict:// protocol handler — potential SSRF vector",
105
+ ),
106
+ "ldap": (
107
+ Severity.MEDIUM,
108
+ "ldap:// protocol handler — potential SSRF vector",
109
+ ),
110
+ }
111
+ _METADATA_HOSTS = frozenset({"169.254.169.254", "metadata.google.internal"})
112
+
113
+ # ── Cloud metadata endpoints (merged from YAML + defaults) ──────────────
114
+ _METADATA_MESSAGES = {
115
+ "169.254.169.254": "AWS/GCP cloud metadata endpoint access — potential SSRF to cloud credentials",
116
+ "metadata.google.internal": "GCP metadata endpoint access — potential SSRF to cloud credentials",
117
+ }
118
+
119
+ # ── Internal network patterns (merged from YAML + defaults) ─────────────
120
+ _LOCALHOST_MESSAGE = "Localhost URL — verify this isn't user-controlled"
121
+ _INTERNAL_MESSAGE = "Internal network IP — potential SSRF to internal services"
122
+
123
+ # ── URL construction patterns (where user input flows to URL) ───────────
124
+ _URL_VAR_NAMES = {
125
+ "url",
126
+ "endpoint",
127
+ "target",
128
+ "host",
129
+ "base_url",
130
+ "baseUri",
131
+ "requestUrl",
132
+ "fetchUrl",
133
+ "request_url",
134
+ } | _YAML_URL_VARS
135
+
136
+ _USER_INPUT_MARKERS = {
137
+ "request.",
138
+ "req.",
139
+ "params.",
140
+ "args.",
141
+ "input",
142
+ "data[",
143
+ "form[",
144
+ "query.",
145
+ "body.",
146
+ "headers.",
147
+ "c.Query",
148
+ "r.URL",
149
+ } | _YAML_USER_INPUT
150
+
151
+ # ── Redirect/forward patterns (merged from YAML + defaults) ─────────────
152
+ _REDIRECT_CALL_SINKS = {
153
+ Lang.PYTHON: {"redirect", "redirect_url"},
154
+ Lang.JAVASCRIPT: {"res.redirect", "response.redirect"},
155
+ Lang.TYPESCRIPT: {"res.redirect", "response.redirect"},
156
+ Lang.JAVA: {"sendRedirect"},
157
+ Lang.PHP: {"header", "redirect"},
158
+ Lang.RUBY: {"redirect_to"},
159
+ Lang.GO: {"http.Redirect"},
160
+ Lang.C_SHARP: {"Redirect"},
161
+ }
162
+ for _lk, _funcs in _YAML_REDIRECT_SINKS.items():
163
+ try:
164
+ _lang = Lang(_lk)
165
+ except ValueError:
166
+ continue
167
+ _REDIRECT_CALL_SINKS.setdefault(_lang, set()).update(_funcs)
168
+
169
+ _SKIP_DIRS = DEFAULT_IGNORE_DIRS | {"test", "tests", "vendor"}
170
+
171
+ def _run(self, inp: AgentInput, result: AgentResult) -> None:
172
+ findings = []
173
+ files_scanned = 0
174
+
175
+ with trace_agent(self.name, inp.root) as trace:
176
+ for pattern in self._SOURCE_PATTERNS:
177
+ for p in self._safe_rglob(inp.root, pattern):
178
+ rel = str(p.relative_to(inp.root))
179
+ if self._should_skip(rel):
180
+ continue
181
+ files_scanned += 1
182
+ try:
183
+ content = p.read_text(encoding="utf-8", errors="ignore")
184
+ except Exception as e:
185
+ _log.warning("SSRFProtectionAgent._run failed: %s", e)
186
+ continue
187
+
188
+ lang = detect_language(p)
189
+ findings.extend(self._check_http_clients(content, lang, rel))
190
+ findings.extend(self._check_url_literals(content, lang, rel))
191
+ findings.extend(self._check_custom_patterns(content, rel))
192
+ findings.extend(self._check_url_construction(content, lang, rel))
193
+ findings.extend(self._check_redirects(content, lang, rel))
194
+ # Dedupe: structural checks and YAML customs can flag the same
195
+ # literal with slightly different messages — same place+type is one issue
196
+ seen: set[tuple] = set()
197
+ unique: list = []
198
+ for finding in findings:
199
+ key = (finding.file, finding.type, finding.line)
200
+ if key not in seen:
201
+ seen.add(key)
202
+ unique.append(finding)
203
+ findings = unique
204
+ trace.findings = len(findings)
205
+ trace.files_scanned = files_scanned
206
+
207
+ result.status = AgentStatus.DONE
208
+ result.findings = findings
209
+ result.files_scanned = files_scanned
210
+ result.data.update({"files_scanned": files_scanned, "finding_count": len(findings)})
211
+ return
212
+
213
+ def _check_http_clients(self, content: str, lang: Lang, rel_path: str) -> list[Finding]:
214
+ findings = []
215
+ for call in find_calls(content, lang, HTTP_CLIENTS):
216
+ findings.append(
217
+ make_finding(
218
+ self.name,
219
+ "ssrf_http_client",
220
+ Severity.MEDIUM,
221
+ rel_path,
222
+ f"HTTP client call ({call['name']}) — verify URL is not user-controlled",
223
+ line=call["line"],
224
+ code_snippet=call["full_text"][:120],
225
+ suggestion="Validate and whitelist URLs before making HTTP requests. Use URL parsing and allowlist.",
226
+ )
227
+ )
228
+ return findings
229
+
230
+ @staticmethod
231
+ def _split_host(value: str) -> tuple[str, str]:
232
+ """(scheme, host) from a literal; bare hostnames work without scheme."""
233
+ text = value.strip()
234
+ if "://" in text:
235
+ try:
236
+ parsed = urlsplit(text)
237
+ return parsed.scheme.lower(), parsed.hostname or ""
238
+ except ValueError:
239
+ return "", ""
240
+ return "", text
241
+
242
+ def _check_url_literals(self, content: str, lang: Lang, rel_path: str) -> list[Finding]:
243
+ """Dangerous URLs via parsed string literals (urllib.parse + ipaddress)."""
244
+ findings = []
245
+ lines = content.splitlines()
246
+
247
+ def _snippet(line_num: int) -> str:
248
+ if 0 < line_num <= len(lines):
249
+ return lines[line_num - 1].strip()[:120]
250
+ return ""
251
+
252
+ for value, line_num in string_literals(content, lang):
253
+ if "://" not in value and "." not in value and ":" not in value:
254
+ continue
255
+ scheme, host = self._split_host(value)
256
+ if scheme in self._DANGEROUS_SCHEMES:
257
+ severity, message = self._DANGEROUS_SCHEMES[scheme]
258
+ findings.append(
259
+ make_finding(
260
+ self.name,
261
+ "ssrf_dangerous_protocol",
262
+ severity,
263
+ rel_path,
264
+ message,
265
+ line=line_num,
266
+ code_snippet=_snippet(line_num),
267
+ suggestion="Block non-HTTP/HTTPS protocols in URL handling.",
268
+ )
269
+ )
270
+ continue
271
+ host = (host or "").lower().split("@")[-1].split(":")[0].strip("[]")
272
+ if not host:
273
+ continue
274
+ if host in self._METADATA_MESSAGES:
275
+ findings.append(
276
+ make_finding(
277
+ self.name,
278
+ "ssrf_metadata_access",
279
+ Severity.CRITICAL,
280
+ rel_path,
281
+ self._METADATA_MESSAGES[host],
282
+ line=line_num,
283
+ code_snippet=_snippet(line_num),
284
+ suggestion="Block access to cloud metadata endpoints. Use IMDSv2 if metadata access is required.",
285
+ )
286
+ )
287
+ continue
288
+ if host == "localhost":
289
+ findings.append(
290
+ make_finding(
291
+ self.name,
292
+ "ssrf_internal_network",
293
+ Severity.LOW,
294
+ rel_path,
295
+ self._LOCALHOST_MESSAGE,
296
+ line=line_num,
297
+ code_snippet=_snippet(line_num),
298
+ suggestion="Restrict outbound HTTP requests to prevent access to internal services.",
299
+ )
300
+ )
301
+ continue
302
+ try:
303
+ addr = ipaddress.ip_address(host)
304
+ except ValueError:
305
+ continue
306
+ if addr.is_private or addr.is_loopback or addr.is_link_local or addr.is_reserved:
307
+ if addr.is_loopback:
308
+ severity, message = Severity.LOW, self._LOCALHOST_MESSAGE
309
+ else:
310
+ severity, message = Severity.MEDIUM, self._INTERNAL_MESSAGE
311
+ findings.append(
312
+ make_finding(
313
+ self.name,
314
+ "ssrf_internal_network",
315
+ severity,
316
+ rel_path,
317
+ message,
318
+ line=line_num,
319
+ code_snippet=_snippet(line_num),
320
+ suggestion="Restrict outbound HTTP requests to prevent access to internal services.",
321
+ )
322
+ )
323
+ return findings
324
+
325
+ @staticmethod
326
+ def _as_severity(value) -> Severity:
327
+ if isinstance(value, Severity):
328
+ return value
329
+ try:
330
+ return Severity(str(value).lower())
331
+ except ValueError:
332
+ return Severity.MEDIUM
333
+
334
+ def _check_custom_patterns(self, content: str, rel_path: str) -> list[Finding]:
335
+ """User-supplied YAML customs stay pattern-based (configuration, not logic)."""
336
+ findings = []
337
+ customs: list[tuple] = []
338
+ customs.extend((p, s, m, "ssrf_dangerous_protocol") for p, s, m in _YAML_PROTOCOL)
339
+ customs.extend((p, s, m, "ssrf_metadata_access") for p, s, m in _YAML_METADATA)
340
+ customs.extend((p, s, m, "ssrf_internal_network") for p, s, m in _YAML_INTERNAL)
341
+ if not customs:
342
+ return findings
343
+ for line_num, line in enumerate(content.splitlines(), 1):
344
+ stripped = line.strip()
345
+ if stripped.startswith("#"):
346
+ continue
347
+ for pattern, severity, message, finding_type in customs:
348
+ try:
349
+ matched = pattern.search(line)
350
+ except Exception:
351
+ continue
352
+ if matched:
353
+ findings.append(
354
+ make_finding(
355
+ self.name,
356
+ finding_type,
357
+ self._as_severity(severity),
358
+ rel_path,
359
+ message,
360
+ line=line_num,
361
+ code_snippet=stripped[:120],
362
+ suggestion="Review against SSRF guidance.",
363
+ )
364
+ )
365
+ return findings
366
+
367
+ def _check_url_construction(self, content: str, lang: Lang, rel_path: str) -> list[Finding]:
368
+ findings = []
369
+ for assign in find_assignments(content, lang):
370
+ target = assign["target"].lower()
371
+ if not any(v in target for v in self._URL_VAR_NAMES):
372
+ continue
373
+ value = assign["value"]
374
+ if not any(marker in value.lower() for marker in self._USER_INPUT_MARKERS):
375
+ continue
376
+ findings.append(
377
+ make_finding(
378
+ self.name,
379
+ "ssrf_url_construction",
380
+ Severity.HIGH,
381
+ rel_path,
382
+ f"URL variable '{assign['target']}' constructed from user input — validate and sanitize before use",
383
+ line=assign["line"],
384
+ code_snippet=assign["full_text"][:120],
385
+ suggestion="Parse URLs with urllib.parse and validate against an allowlist before use.",
386
+ )
387
+ )
388
+ return findings
389
+
390
+ def _check_redirects(self, content: str, lang: Lang, rel_path: str) -> list[Finding]:
391
+ findings = []
392
+ sinks = self._REDIRECT_CALL_SINKS.get(lang, set())
393
+ if sinks:
394
+ for call in find_calls(content, lang, sinks):
395
+ arg = get_call_arg(call["full_text"])
396
+ if arg and any(marker in arg.lower() for marker in self._USER_INPUT_MARKERS):
397
+ findings.append(
398
+ make_finding(
399
+ self.name,
400
+ "ssrf_redirect",
401
+ Severity.HIGH,
402
+ rel_path,
403
+ "Redirect with user-controlled URL — potential open redirect/SSRF",
404
+ line=call["line"],
405
+ code_snippet=call["full_text"][:120],
406
+ suggestion="Validate redirect targets against an allowlist. Never redirect to user-supplied URLs.",
407
+ )
408
+ )
409
+ return findings
410
+
411
+ def _should_skip(self, rel_path: str) -> bool:
412
+ parts = rel_path.replace("\\", "/").split("/")
413
+ return any(p in self._SKIP_DIRS for p in parts)
414
+
415
+ def _safe_rglob(self, root: Path, pattern: str):
416
+ """Directory-walking with pruning."""
417
+ suffix = pattern.replace("*.", ".")
418
+ for dirpath, dirnames, filenames in os.walk(root):
419
+ dirnames[:] = [d for d in dirnames if d not in self._SKIP_DIRS]
420
+ for fn in filenames:
421
+ if fn.endswith(suffix):
422
+ yield Path(dirpath) / fn