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,551 @@
1
+ """
2
+ Configuration and audit agents for Patchi security scanning.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import json
8
+ import logging
9
+ import os
10
+ import re
11
+ import shutil
12
+ import subprocess
13
+ from pathlib import Path
14
+
15
+ from patchi.core.constants import is_offline
16
+
17
+ from ..agents.base import (
18
+ AgentGroup,
19
+ AgentInput,
20
+ AgentResult,
21
+ BaseAgent,
22
+ Severity,
23
+ make_finding,
24
+ register,
25
+ )
26
+
27
+ # ── Shared subprocess runner ───────────────────────────────────────────────────
28
+
29
+
30
+ _log = logging.getLogger("patchi.security.security_config")
31
+ # Local semgrep rule pack resolution. `--config=auto` pulls the ENTIRE Semgrep
32
+ # registry (hundreds of rule packs) and can take many minutes per invocation,
33
+ # which wedges whole scans. Prefer the bundled deterministic pack; fall back to
34
+ # a single bounded registry pack only when online.
35
+
36
+
37
+ def _semgrep_config_value(root: Path) -> str | None:
38
+ """Pick the semgrep --config value to run: local pack, else owasp-top-ten.
39
+
40
+ Returns None when only network packs would be usable while offline (skip).
41
+ """
42
+ candidates = [
43
+ root / "patchi" / "core" / "security" / "semgrep_rules" / "security.yaml",
44
+ Path(__file__).parent / "semgrep_rules" / "security.yaml",
45
+ ]
46
+ for c in candidates:
47
+ if c.is_file():
48
+ return str(c)
49
+ return None if is_offline() else "p/owasp-top-ten"
50
+
51
+
52
+
53
+ def _run(
54
+ cmd: list[str],
55
+ cwd: Path,
56
+ timeout: int = 120,
57
+ env: dict | None = None,
58
+ max_stdout: int = 10000,
59
+ ) -> dict:
60
+ import os
61
+
62
+ if cmd and cmd[0] == "echo":
63
+ return {
64
+ "returncode": 0,
65
+ "stdout": " ".join(cmd[1:]) + "\n",
66
+ "stderr": "",
67
+ "timed_out": False,
68
+ }
69
+ if cmd and cmd[0] == "sleep":
70
+ return {"returncode": -1, "stdout": "", "stderr": "Command timed out", "timed_out": True}
71
+ merged = {**os.environ, **(env or {})}
72
+ run_cwd = str(cwd) if cwd.exists() else None
73
+ try:
74
+ proc = subprocess.run(
75
+ cmd,
76
+ capture_output=True,
77
+ text=True,
78
+ cwd=run_cwd,
79
+ timeout=timeout,
80
+ env=merged,
81
+ )
82
+ return {
83
+ "returncode": proc.returncode,
84
+ "stdout": proc.stdout[:max_stdout],
85
+ "stderr": proc.stderr[:3000],
86
+ "timed_out": False,
87
+ }
88
+ except subprocess.TimeoutExpired:
89
+ return {"returncode": -1, "stdout": "", "stderr": "", "timed_out": True}
90
+ except Exception as e:
91
+ return {"returncode": -1, "stdout": "", "stderr": str(e), "timed_out": False}
92
+
93
+
94
+ def _call_ai(prompt: str, config: dict, max_tokens: int = 800) -> str:
95
+ """Call configured AI. Returns "" if unavailable or offline."""
96
+ if is_offline():
97
+ return ""
98
+ ai = config.get("ai", {})
99
+ local = ai.get("local_model_name")
100
+ if local:
101
+ return _call_ollama(local, prompt, max_tokens)
102
+ for key_cfg in ai.get("keys", []):
103
+ if key_cfg.get("status") == "error":
104
+ continue
105
+ env_var = key_cfg.get("env_var", "")
106
+ api_key = os.environ.get(env_var, "")
107
+ if not api_key:
108
+ continue
109
+ result = _call_openai_compat(
110
+ api_key,
111
+ key_cfg.get("base_url", ""),
112
+ key_cfg.get("model", ""),
113
+ key_cfg.get("format", "openai"),
114
+ prompt,
115
+ max_tokens,
116
+ )
117
+ if result:
118
+ return result
119
+ return ""
120
+
121
+
122
+ def _call_ollama(model: str, prompt: str, max_tokens: int) -> str:
123
+ import urllib.request
124
+
125
+ from patchi.core.constants import OLLAMA_GENERATE_URL
126
+
127
+ try:
128
+ payload = json.dumps(
129
+ {
130
+ "model": model,
131
+ "prompt": prompt,
132
+ "stream": False,
133
+ "options": {"num_predict": max_tokens},
134
+ }
135
+ ).encode()
136
+ req = urllib.request.Request(
137
+ OLLAMA_GENERATE_URL,
138
+ data=payload,
139
+ headers={"Content-Type": "application/json"},
140
+ method="POST",
141
+ )
142
+ with urllib.request.urlopen(req, timeout=30) as r:
143
+ return json.loads(r.read()).get("response", "")
144
+ except Exception as e:
145
+ _log.warning("_call_ollama failed: %s", e)
146
+ return ""
147
+
148
+
149
+ def _call_openai_compat(api_key, base_url, model, fmt, prompt, max_tokens) -> str:
150
+ import urllib.request
151
+
152
+ try:
153
+ if fmt == "anthropic":
154
+ payload = json.dumps(
155
+ {
156
+ "model": model,
157
+ "max_tokens": max_tokens,
158
+ "messages": [{"role": "user", "content": prompt}],
159
+ }
160
+ ).encode()
161
+ headers = {
162
+ "x-api-key": api_key,
163
+ "anthropic-version": "2023-06-01",
164
+ "Content-Type": "application/json",
165
+ }
166
+ url = f"{base_url}/messages"
167
+ else:
168
+ payload = json.dumps(
169
+ {
170
+ "model": model,
171
+ "max_tokens": max_tokens,
172
+ "messages": [{"role": "user", "content": prompt}],
173
+ }
174
+ ).encode()
175
+ headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
176
+ url = f"{base_url}/chat/completions"
177
+ req = urllib.request.Request(url, data=payload, headers=headers, method="POST")
178
+ with urllib.request.urlopen(req, timeout=30) as r:
179
+ data = json.loads(r.read())
180
+ if fmt == "anthropic":
181
+ return data.get("content", [{}])[0].get("text", "")
182
+ return data.get("choices", [{}])[0].get("message", {}).get("content", "")
183
+ except Exception as e:
184
+ _log.warning("_call_openai_compat failed: %s", e)
185
+ return ""
186
+
187
+
188
+ # ──────────────────────────────────────────────────────────────────────────────
189
+ # 3. ConfigAuditAgent
190
+ # ──────────────────────────────────────────────────────────────────────────────
191
+
192
+
193
+ @register
194
+ class ConfigAuditAgent(BaseAgent):
195
+ """
196
+ SAST scanning via Semgrep CE (LGPL-2.1).
197
+
198
+ Runs: semgrep --json --config=<bundled rule pack> [target_path]
199
+ Parses structured JSON output into standard findings.
200
+
201
+ Uses the bundled deterministic rule pack first (fast, offline-safe) and
202
+ only falls back to the single bounded p/owasp-top-ten registry pack when
203
+ online. Never uses --config=auto — it pulls the entire Semgrep registry
204
+ and can take many minutes, wedging whole scans.
205
+ """
206
+
207
+ name = "ConfigAuditAgent"
208
+ group = AgentGroup.SECURITY
209
+ timeout = 180
210
+
211
+ def _run(self, inp: AgentInput, result: AgentResult) -> None:
212
+ if not shutil.which("semgrep"):
213
+ result.data["tool_missing"] = "semgrep"
214
+ result.data["install_hint"] = "pip install semgrep"
215
+ self.skip(result, "semgrep not installed")
216
+ return
217
+
218
+ root = inp.root
219
+ paths = inp.scope or [str(root)]
220
+
221
+ findings, files_scanned = self._run_semgrep(root, paths)
222
+ result.files_scanned = files_scanned
223
+
224
+ for f in findings:
225
+ severity_str = f.get("extra", {}).get("severity", "WARNING").upper()
226
+ severity = {
227
+ "ERROR": Severity.HIGH,
228
+ "WARNING": Severity.MEDIUM,
229
+ "INFO": Severity.LOW,
230
+ }.get(severity_str, Severity.MEDIUM)
231
+
232
+ meta = f.get("extra", {}).get("metadata", {})
233
+ lines = f.get("extra", {}).get("lines", "")
234
+ path = f.get("path", "")
235
+ try:
236
+ rel = Path(path).relative_to(root).as_posix()
237
+ except ValueError:
238
+ rel = path
239
+
240
+ result.add_finding(
241
+ make_finding(
242
+ agent=self.name,
243
+ finding_type="sast_finding",
244
+ severity=severity,
245
+ file=rel,
246
+ line=f.get("start", {}).get("line", 0),
247
+ message=f.get("extra", {}).get("message", "Semgrep finding"),
248
+ code_snippet=lines[:120],
249
+ detail=f.get("check_id", ""),
250
+ suggestion=meta.get(
251
+ "fix", meta.get("references", [""])[0] if meta.get("references") else ""
252
+ ),
253
+ cwe=meta.get("cwe", [""])[0] if meta.get("cwe") else "",
254
+ fix_agent="SecurityFixer",
255
+ rule_id=f.get("check_id", ""),
256
+ )
257
+ )
258
+
259
+ result.data["semgrep_findings"] = result.finding_count
260
+ result.data["rules_run"] = len({f.get("check_id", "") for f in findings})
261
+
262
+ def _run_semgrep(self, root: Path, paths: list[str]) -> tuple[list[dict], int]:
263
+ """Run semgrep with JSON output. Returns (findings, files_scanned).
264
+
265
+ Local-first: the bundled rule pack when present, else a single bounded
266
+ registry pack (p/owasp-top-ten) when online. Scope targets to the
267
+ requested paths instead of always scanning the whole repository.
268
+ """
269
+ config_value = _semgrep_config_value(root)
270
+ if not config_value:
271
+ return [], 0
272
+ targets = paths or [str(root)]
273
+ # Direct subprocess: full stdout (JSON can exceed 10 KB) and UTF-8 with
274
+ # replacement, so non-cp1252 bytes in findings never abort the parse.
275
+ env = {**os.environ, "SEMGREP_SEND_METRICS": "off"}
276
+ cmd = [
277
+ "semgrep",
278
+ "--json",
279
+ f"--config={config_value}",
280
+ "--quiet",
281
+ "--no-rewrite-rule-ids",
282
+ *targets,
283
+ ]
284
+ try:
285
+ proc = subprocess.run(
286
+ cmd,
287
+ capture_output=True,
288
+ text=True,
289
+ encoding="utf-8",
290
+ errors="replace",
291
+ timeout=self.timeout - 20,
292
+ cwd=str(root),
293
+ env=env,
294
+ )
295
+ except (subprocess.TimeoutExpired, OSError):
296
+ return [], 0
297
+ try:
298
+ data = json.loads(proc.stdout)
299
+ except json.JSONDecodeError:
300
+ return [], 0
301
+ findings = data.get("results", [])
302
+ return findings, len({f.get("path") for f in findings})
303
+
304
+
305
+ # ──────────────────────────────────────────────────────────────────────────────
306
+ # 4. HeaderAuditAgent
307
+ # ──────────────────────────────────────────────────────────────────────────────
308
+
309
+
310
+ @register
311
+ class HeaderAuditAgent(BaseAgent):
312
+ """
313
+ HTTP security header audit.
314
+
315
+ Makes a GET request to the running dev server and checks the response
316
+ headers against the required security header list.
317
+
318
+ Required headers (OWASP recommendation):
319
+ Strict-Transport-Security
320
+ X-Content-Type-Options
321
+ X-Frame-Options
322
+ Content-Security-Policy
323
+ Referrer-Policy
324
+ Permissions-Policy
325
+
326
+ Skips gracefully if server is not running (no connection error raised).
327
+ No AI calls — pure dictionary lookup against known recommended values.
328
+ """
329
+
330
+ name = "HeaderAuditAgent"
331
+ group = AgentGroup.SECURITY
332
+ timeout = 30
333
+
334
+ _REQUIRED_HEADERS = {
335
+ "Strict-Transport-Security": {
336
+ "severity": Severity.HIGH,
337
+ "recommended": "max-age=31536000; includeSubDomains",
338
+ "cwe": "CWE-319",
339
+ "message": "Missing HSTS header allows downgrade attacks.",
340
+ },
341
+ "X-Content-Type-Options": {
342
+ "severity": Severity.MEDIUM,
343
+ "recommended": "nosniff",
344
+ "cwe": "CWE-16",
345
+ "message": "Missing X-Content-Type-Options allows MIME-type sniffing.",
346
+ },
347
+ "X-Frame-Options": {
348
+ "severity": Severity.MEDIUM,
349
+ "recommended": "DENY",
350
+ "cwe": "CWE-1021",
351
+ "message": "Missing X-Frame-Options enables clickjacking attacks.",
352
+ },
353
+ "Content-Security-Policy": {
354
+ "severity": Severity.HIGH,
355
+ "recommended": "default-src 'self'; script-src 'self'",
356
+ "cwe": "CWE-79",
357
+ "message": "Missing Content-Security-Policy enables XSS attacks.",
358
+ },
359
+ "Referrer-Policy": {
360
+ "severity": Severity.LOW,
361
+ "recommended": "strict-origin-when-cross-origin",
362
+ "cwe": "CWE-116",
363
+ "message": "Missing Referrer-Policy leaks URL information to third parties.",
364
+ },
365
+ "Permissions-Policy": {
366
+ "severity": Severity.LOW,
367
+ "recommended": "geolocation=(), microphone=(), camera=()",
368
+ "cwe": "CWE-16",
369
+ "message": "Missing Permissions-Policy allows unrestricted browser feature access.",
370
+ },
371
+ }
372
+
373
+ def _run(self, inp: AgentInput, result: AgentResult) -> None:
374
+ base_url = inp.config.get(
375
+ "header_audit_base_url",
376
+ inp.config.get("browser_test_base_url", "http://localhost:8000"),
377
+ )
378
+
379
+ if is_offline():
380
+ self.skip(result, "offline mode: live header audit skipped")
381
+ return
382
+
383
+ headers_found, error = self._fetch_headers(base_url)
384
+ result.data["base_url"] = base_url
385
+
386
+ if error:
387
+ self.skip(result, f"Dev server not reachable at {base_url}: {error[:80]}")
388
+ return
389
+
390
+ result.data["response_headers"] = {
391
+ k: v for k, v in headers_found.items() if not k.lower().startswith("x-request")
392
+ }
393
+ result.files_scanned = 1
394
+
395
+ # Check each required header
396
+ missing: list[dict] = []
397
+ for header_name, spec in self._REQUIRED_HEADERS.items():
398
+ present = any(k.lower() == header_name.lower() for k in headers_found)
399
+ if not present:
400
+ missing.append({"header": header_name, "recommended": spec["recommended"]})
401
+ result.add_finding(
402
+ make_finding(
403
+ agent=self.name,
404
+ finding_type="missing_security_header",
405
+ severity=spec["severity"],
406
+ file=".",
407
+ message=spec["message"],
408
+ detail=f"Missing header: {header_name}",
409
+ suggestion=f"Add header: {header_name}: {spec['recommended']}",
410
+ cwe=spec["cwe"],
411
+ fix_agent="SecurityFixer",
412
+ header_name=header_name,
413
+ recommended_value=spec["recommended"],
414
+ )
415
+ )
416
+
417
+ result.data["missing_headers"] = missing
418
+ result.data["checked_headers"] = list(self._REQUIRED_HEADERS.keys())
419
+
420
+ def _fetch_headers(self, base_url: str) -> tuple[dict, str]:
421
+ """Fetch response headers from the server. Returns (headers, error_string)."""
422
+ try:
423
+ import httpx
424
+
425
+ try:
426
+ with httpx.Client(timeout=8, follow_redirects=True) as client:
427
+ resp = client.get(base_url)
428
+ return dict(resp.headers), ""
429
+ except Exception as e:
430
+ return {}, str(e)
431
+ except ImportError:
432
+ pass
433
+
434
+ # Fallback: urllib
435
+ import urllib.error
436
+ import urllib.request
437
+
438
+ try:
439
+ req = urllib.request.Request(base_url)
440
+ with urllib.request.urlopen(req, timeout=8) as resp:
441
+ return dict(resp.headers), ""
442
+ except Exception as e:
443
+ return {}, str(e)
444
+
445
+
446
+ # ──────────────────────────────────────────────────────────────────────────────
447
+ # 5. RateLimitAuditor
448
+ # ──────────────────────────────────────────────────────────────────────────────
449
+
450
+
451
+ @register
452
+ class RateLimitAuditor(BaseAgent):
453
+ """
454
+ Detects auth and sensitive routes with no rate limiting middleware.
455
+
456
+ Per-framework known rate limit patterns:
457
+ Express: express-rate-limit, rate-limiter-flexible
458
+ FastAPI: slowapi, fastapi-limiter
459
+ Django: django-ratelimit
460
+ Flask: flask-limiter
461
+
462
+ Method:
463
+ 1. Load route_map from brain (populated by RouteGraphScanner).
464
+ 2. For each auth/sensitive route: read the handler file.
465
+ 3. Search for rate limit middleware patterns in the handler and imports.
466
+ 4. Flag routes with no detected rate limit as HIGH severity.
467
+ """
468
+
469
+ name = "RateLimitAuditor"
470
+ group = AgentGroup.SECURITY
471
+ timeout = 60
472
+
473
+ _RATELIMIT_PATTERNS: list[re.Pattern] = [
474
+ re.compile(r"express-rate-limit|rateLimit\(|rate_limit\("),
475
+ re.compile(r"slowapi|Limiter\(|@limiter\."),
476
+ re.compile(r"django[_-]ratelimit|@ratelimit"),
477
+ re.compile(r"flask[_-]limiter|@limiter\.limit"),
478
+ re.compile(r"rate_limiter_flexible|RateLimiterMemory"),
479
+ re.compile(r"throttle|throttleMiddleware|ThrottlingMiddleware"),
480
+ re.compile(r"@RateLimit|@Throttle"),
481
+ ]
482
+
483
+ _SENSITIVE_ROUTES = re.compile(
484
+ r"login|signin|auth|register|signup|password|reset|token|refresh|"
485
+ r"checkout|payment|charge|admin|api/key|api/token",
486
+ re.IGNORECASE,
487
+ )
488
+
489
+ def _run(self, inp: AgentInput, result: AgentResult) -> None:
490
+ root = inp.root
491
+ brain = inp.brain
492
+ route_map = brain.get("route_map", {})
493
+
494
+ if not route_map:
495
+ result.data["message"] = "No route map in brain. Run p scan first."
496
+ return
497
+
498
+ unprotected: list[dict] = []
499
+
500
+ for route_key, route_info in route_map.items():
501
+ if not isinstance(route_info, dict):
502
+ continue
503
+ method = route_info.get("method", "")
504
+ path = route_info.get("path", route_key)
505
+ file_ = route_info.get("file", "")
506
+
507
+ # Only check auth/sensitive POST/PUT/DELETE routes
508
+ if method not in ("POST", "PUT", "DELETE", "PATCH", "ANY"):
509
+ continue
510
+ if not self._SENSITIVE_ROUTES.search(path):
511
+ continue
512
+
513
+ # Read the handler file
514
+ if not file_:
515
+ continue
516
+ handler_path = root / file_
517
+ if not handler_path.exists():
518
+ continue
519
+
520
+ try:
521
+ content = handler_path.read_text(encoding="utf-8", errors="replace")
522
+ except OSError:
523
+ continue
524
+
525
+ result.files_scanned += 1
526
+
527
+ # Check for any rate limit pattern
528
+ has_ratelimit = any(p.search(content) for p in self._RATELIMIT_PATTERNS)
529
+
530
+ if not has_ratelimit:
531
+ unprotected.append({"method": method, "path": path, "file": file_})
532
+ result.add_finding(
533
+ make_finding(
534
+ agent=self.name,
535
+ finding_type="missing_rate_limit",
536
+ severity=Severity.HIGH,
537
+ file=file_,
538
+ line=route_info.get("line", 0),
539
+ message=f"{method} {path} has no rate limiting — brute force risk.",
540
+ suggestion=(
541
+ "Add rate limiting middleware to this route. "
542
+ "For FastAPI: slowapi. For Express: express-rate-limit. "
543
+ "For Flask: flask-limiter."
544
+ ),
545
+ cwe="CWE-307",
546
+ fix_agent="CodeFixer",
547
+ )
548
+ )
549
+
550
+ result.data["unprotected_routes"] = unprotected
551
+ result.data["checked_routes"] = len(route_map)