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,141 @@
1
+ """
2
+ Attack Feedback — closes the loop between attack outcomes and learning.
3
+
4
+ When the RedTeamEngine or assurance attackers confirm a vulnerability:
5
+ 1. The confirmed payload is added to the fuzz corpus as a high-score entry
6
+ 2. False-positive paths are recorded in the ignore_learner
7
+ 3. Agent trust scores are updated via the learning brain
8
+
9
+ This turns every scan into training data for the next scan.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import logging
15
+ import time
16
+ from pathlib import Path
17
+ from typing import Any
18
+
19
+ from patchi.core.brain import learning as learn
20
+
21
+ _log = logging.getLogger("patchi.security.attack_feedback")
22
+
23
+
24
+ def record_confirmed_attack(
25
+ root: Path,
26
+ attack: dict[str, Any],
27
+ *,
28
+ agent_name: str = "red_team",
29
+ ) -> None:
30
+ """Feed a confirmed attack result into the learning loop.
31
+
32
+ ``attack`` is a dict from RedTeamEngine or AttackPlanner with keys:
33
+ hypothesis, payload, endpoint, severity, evidence, tool, success
34
+ """
35
+ payload = attack.get("payload", "")
36
+ endpoint = attack.get("endpoint", "")
37
+ severity = attack.get("severity", "medium")
38
+ evidence = attack.get("evidence", "")
39
+
40
+ # --- 1. Fuzz corpus: confirmed payloads are high-value seeds ---
41
+ try:
42
+ from patchi.core.fuzz.corpus import CorpusEntry, FuzzCorpus
43
+
44
+ corpus = FuzzCorpus(root)
45
+ label = f"confirmed_{attack.get('tool', 'unknown')}_{endpoint or 'global'}"
46
+ score = {"critical": 1.0, "high": 0.85, "medium": 0.6, "low": 0.4}.get(severity, 0.5)
47
+ corpus.add(
48
+ CorpusEntry(
49
+ label=label[:120],
50
+ value=payload[:500] if payload else evidence[:500],
51
+ strategy="attack_feedback",
52
+ score=score,
53
+ )
54
+ )
55
+ corpus.save()
56
+ _log.info("Fed confirmed attack into corpus: %s (score=%.2f)", label[:60], score)
57
+ except Exception as exc:
58
+ _log.debug("Corpus update failed: %s", exc)
59
+
60
+ # --- 2. Learning brain: record acceptance (confirmed = good detection) ---
61
+ finding_type = attack.get("tool", "unknown")
62
+ learn.record_acceptance(finding_type, agent_name, root)
63
+ _log.info("Recorded acceptance for %s via %s", finding_type, agent_name)
64
+
65
+
66
+ def record_false_positive(
67
+ root: Path,
68
+ finding: dict[str, Any],
69
+ *,
70
+ agent_name: str = "unknown",
71
+ ) -> None:
72
+ """Feed a rejected/false-positive finding into the learning loop.
73
+
74
+ Updates the ignore_learner FP stats and records a rejection.
75
+ """
76
+ finding_type = finding.get("control_id", finding.get("type", "unknown"))
77
+ file_path = finding.get("file", "")
78
+
79
+ # --- 1. Learning brain: record rejection ---
80
+ learn.record_rejection(finding_type, agent_name, root)
81
+
82
+ # --- 2. Ignore learner: record FP for this file/directory ---
83
+ try:
84
+ from patchi.core.security.ignore_learner import IgnoreLearner
85
+
86
+ learner = IgnoreLearner(root)
87
+ # Build entries from known FP store
88
+ fp_store = root / ".patchi" / "known_false_positives.json"
89
+ fps: list[dict] = []
90
+ if fp_store.exists():
91
+ import json
92
+
93
+ fps = json.loads(fp_store.read_text(encoding="utf-8"))
94
+
95
+ # Add this new FP
96
+ fps.append(
97
+ {
98
+ "file": file_path,
99
+ "type": finding_type,
100
+ "timestamp": time.time(),
101
+ "agent": agent_name,
102
+ }
103
+ )
104
+ fp_store.parent.mkdir(parents=True, exist_ok=True)
105
+ import json
106
+
107
+ fp_store.write_text(json.dumps(fps[-500:], indent=2), encoding="utf-8")
108
+
109
+ # Rebuild learner with updated FP list
110
+ learner.build(known_fps=fps)
111
+ promoted = learner.promote_to_global()
112
+ if promoted:
113
+ _log.info("Promoted %d ignore patterns to global store", promoted)
114
+ except Exception as exc:
115
+ _log.debug("Ignore learner update failed: %s", exc)
116
+
117
+
118
+ def record_fix_outcome(
119
+ root: Path,
120
+ finding_type: str,
121
+ file_path: str,
122
+ fix_strategy: str,
123
+ accepted: bool,
124
+ ) -> None:
125
+ """Record whether a generated fix was accepted or rejected."""
126
+ if accepted:
127
+ learn.record_fix_pattern(finding_type, file_path, fix_strategy, "accepted", root)
128
+ learn.record_acceptance(finding_type, "auto_fixer", root)
129
+ else:
130
+ learn.record_rejection(finding_type, "auto_fixer", root)
131
+ _log.info(
132
+ "Fix outcome recorded: %s on %s → %s",
133
+ finding_type,
134
+ file_path,
135
+ "accepted" if accepted else "rejected",
136
+ )
137
+
138
+
139
+ def get_learning_summary(root: Path) -> dict:
140
+ """Return the full learning summary for display."""
141
+ return learn.get_summary(root)
@@ -0,0 +1 @@
1
+ """Attack scenario YAML library."""
@@ -0,0 +1,176 @@
1
+ """
2
+ Attack Tree generation — renders exploit chains as goal-oriented trees.
3
+
4
+ Where ChainAnalyzer answers "how do findings connect?", the attack tree
5
+ answers "what could an attacker achieve, and via which paths?".
6
+
7
+ A tree is built per GOAL (e.g. "Steal Secrets", "Execute Code", "Impersonate
8
+ Users"). Each chain whose terminal impact maps to that goal becomes a path in
9
+ the tree; intermediate nodes become branches. The result renders as an ASCII
10
+ tree for the CLI and a nested dict for the web panel.
11
+
12
+ Deterministic: same findings + graph -> same tree. Zero AI calls.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import logging
18
+ from collections.abc import Iterable
19
+ from dataclasses import dataclass, field
20
+ from typing import Any
21
+
22
+ from patchi.core.security.chain_analyzer import Chain, ChainAnalyzer
23
+
24
+ _log = logging.getLogger("patchi.security.attack_tree")
25
+
26
+ # Impact finding types -> attacker goals
27
+ _GOAL_MAP: dict[str, str] = {
28
+ "secret": "Steal Credentials / Secrets",
29
+ "hardcoded": "Steal Credentials / Secrets",
30
+ "password": "Steal Credentials / Secrets",
31
+ "sensitive_data": "Exfiltrate Sensitive Data",
32
+ "session": "Impersonate Users",
33
+ "rce": "Execute Arbitrary Code",
34
+ "deserialization": "Execute Arbitrary Code",
35
+ "command_injection": "Execute Arbitrary Code",
36
+ }
37
+
38
+ _SEVERITY_ORDER = {"info": 0, "low": 1, "medium": 2, "high": 3, "critical": 4}
39
+
40
+
41
+ @dataclass
42
+ class TreeNode:
43
+ """One node in an attack tree."""
44
+
45
+ label: str
46
+ detail: str = ""
47
+ children: list[TreeNode] = field(default_factory=list)
48
+ severity: str = "medium"
49
+ score: float = 0.0
50
+
51
+ def to_dict(self) -> dict:
52
+ return {
53
+ "label": self.label,
54
+ "detail": self.detail,
55
+ "severity": self.severity,
56
+ "score": round(self.score, 1),
57
+ "children": [c.to_dict() for c in self.children],
58
+ }
59
+
60
+
61
+ @dataclass
62
+ class AttackTree:
63
+ """Goal-oriented view over a set of exploit chains."""
64
+
65
+ goal: str
66
+ root: TreeNode
67
+ chain_count: int = 0
68
+ worst_severity: str = "info"
69
+
70
+ def render(self) -> str:
71
+ """ASCII rendering for CLI output."""
72
+ lines = [f"[ATTACK TREE] Goal: {self.goal}"]
73
+ lines.append(f" Paths: {self.chain_count} Worst: {self.worst_severity}")
74
+
75
+ def _walk(node: TreeNode, prefix: str, is_last: bool) -> None:
76
+ connector = "`-- " if is_last else "|-- "
77
+ sev_tag = f" ({node.severity})" if node.severity != "medium" else ""
78
+ lines.append(f"{prefix}{connector}{node.label}{sev_tag}")
79
+ kids = node.children
80
+ for i, child in enumerate(kids):
81
+ ext = " " if is_last else "| "
82
+ _walk(child, prefix + ext, i == len(kids) - 1)
83
+
84
+ for i, child in enumerate(self.root.children):
85
+ _walk(child, "", i == len(self.root.children) - 1)
86
+ return "\n".join(lines)
87
+
88
+ def to_dict(self) -> dict:
89
+ return {
90
+ "goal": self.goal,
91
+ "chain_count": self.chain_count,
92
+ "worst_severity": self.worst_severity,
93
+ "root": self.root.to_dict(),
94
+ }
95
+
96
+
97
+ def _goal_for(chain: Chain) -> str | None:
98
+ """Map a chain's terminal node to an attacker goal."""
99
+ terminal = chain.nodes[-1].finding.type.lower()
100
+ for pattern, goal in _GOAL_MAP.items():
101
+ if pattern in terminal:
102
+ return goal
103
+ return None
104
+
105
+
106
+ def build_attack_trees(
107
+ chains: list[Chain],
108
+ max_depth: int = 6,
109
+ ) -> list[AttackTree]:
110
+ """
111
+ Group chains into goal-oriented attack trees.
112
+
113
+ Args:
114
+ chains: output of ChainAnalyzer.find_chains()
115
+ max_depth: cap on tree rendering depth
116
+
117
+ Returns:
118
+ Trees sorted by worst severity, highest first.
119
+ """
120
+ by_goal: dict[str, list[Chain]] = {}
121
+ for chain in chains:
122
+ goal = _goal_for(chain)
123
+ if goal is None:
124
+ continue
125
+ by_goal.setdefault(goal, []).append(chain)
126
+
127
+ trees: list[AttackTree] = []
128
+ for goal, goal_chains in by_goal.items():
129
+ worst = max((_SEVERITY_ORDER.get(c.severity, 0) for c in goal_chains), default=0)
130
+ root = TreeNode(label=f"Goal: {goal}")
131
+ # Each chain becomes one path under the root; branch labels come from
132
+ # the entry/exploit steps, leaf is the impact.
133
+ for chain in sorted(goal_chains, key=lambda c: c.score, reverse=True)[:max_depth]:
134
+ branch_label = f"{chain.nodes[0].finding.type} @ {chain.nodes[0].finding.file}"
135
+ branch = TreeNode(
136
+ label=branch_label,
137
+ detail=chain.narrative,
138
+ severity=chain.severity,
139
+ score=chain.score,
140
+ )
141
+ if len(chain.nodes) > 2:
142
+ mid = TreeNode(label=f"via {chain.nodes[1].finding.type}")
143
+ leaf_label = f"{chain.nodes[-1].finding.type} @ {chain.nodes[-1].finding.file}"
144
+ leaf = TreeNode(
145
+ label=leaf_label,
146
+ severity=chain.severity,
147
+ score=chain.score,
148
+ )
149
+ mid.children.append(leaf)
150
+ branch.children.append(mid)
151
+ else:
152
+ leaf_label = f"{chain.nodes[-1].finding.type} @ {chain.nodes[-1].finding.file}"
153
+ branch.children.append(TreeNode(label=leaf_label, severity=chain.severity))
154
+ root.children.append(branch)
155
+
156
+ trees.append(
157
+ AttackTree(
158
+ goal=goal,
159
+ root=root,
160
+ chain_count=len(goal_chains),
161
+ worst_severity=[s for s, v in _SEVERITY_ORDER.items() if v == worst][0] or "info",
162
+ )
163
+ )
164
+
165
+ trees.sort(key=lambda t: _SEVERITY_ORDER.get(t.worst_severity, 0), reverse=True)
166
+ return trees
167
+
168
+
169
+ def trees_from_findings(
170
+ findings: Iterable[Any],
171
+ import_edges: dict[str, set[str]] | None = None,
172
+ ) -> tuple[list[AttackTree], list[Chain]]:
173
+ """Convenience: findings -> chains -> trees in one call."""
174
+ analyzer = ChainAnalyzer(findings, import_edges=import_edges)
175
+ chains = analyzer.find_chains()
176
+ return build_attack_trees(chains), chains
@@ -0,0 +1,344 @@
1
+ """
2
+ AuthenticationAuditAgent — Authentication and session security analysis.
3
+
4
+ Scans for:
5
+ - Missing CSRF protection on state-changing endpoints
6
+ - Weak password hashing (MD5, SHA1, no salt)
7
+ - Session fixation vulnerabilities
8
+ - Missing brute-force protection indicators
9
+ - Insecure session cookie configuration
10
+ - Missing MFA/2FA indicators
11
+ - OAuth implementation issues
12
+ - Hardcoded session secrets
13
+ - Missing rate limiting on auth endpoints
14
+ - Improper session invalidation on logout
15
+
16
+ Multi-language: scans all tree-sitter-supported source languages. Hash-function
17
+ calls are detected structurally via tree-sitter AST (find_calls); hardcoded
18
+ secrets, CSRF markers, OAuth config and rate-limiting indicators are detected
19
+ with high-quality literal-pattern regex (acceptable per the audit). Does NOT
20
+ call AI. Does NOT write to disk. Does NOT touch the queue.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import logging
26
+ import os
27
+ import re
28
+ from pathlib import Path
29
+
30
+ from patchi.core.brain.languages import DEFAULT_IGNORE_DIRS, EXTENSION_MAP, Lang
31
+
32
+ from ..agents.base import (
33
+ AgentDomain,
34
+ AgentGroup,
35
+ AgentInput,
36
+ AgentResult,
37
+ AgentStatus,
38
+ BaseAgent,
39
+ Finding,
40
+ Severity,
41
+ make_finding,
42
+ register,
43
+ )
44
+ from ..brain.ast_utils import find_calls
45
+
46
+ # Source extensions to scan (language parity).
47
+ _SCAN_EXTENSIONS = [
48
+ ".py",
49
+ ".js",
50
+ ".jsx",
51
+ ".ts",
52
+ ".tsx",
53
+ ".java",
54
+ ".rb",
55
+ ".php",
56
+ ".go",
57
+ ".rs",
58
+ ".cpp",
59
+ ".cxx",
60
+ ".cc",
61
+ ".c",
62
+ ".h",
63
+ ".hpp",
64
+ ".cs",
65
+ ".kt",
66
+ ".kts",
67
+ ".swift",
68
+ ".dart",
69
+ ]
70
+
71
+ # Hash function call names that are weak (detected structurally via AST).
72
+ _WEAK_HASH_NAMES = {"md5", "sha1", "sha256", "sha512", "crypt"}
73
+ # Per-name severity / message for AST-detected weak hashes.
74
+ _WEAK_HASH_INFO = {
75
+ "md5": (Severity.CRITICAL, "MD5 used for password hashing — cryptographically broken"),
76
+ "sha1": (Severity.CRITICAL, "SHA1 used for password hashing — cryptographically weak"),
77
+ "sha256": (
78
+ Severity.HIGH,
79
+ "SHA-256 used for password hashing — use bcrypt/scrypt/argon2 instead",
80
+ ),
81
+ "sha512": (Severity.MEDIUM, "SHA-512 used for password hashing — prefer bcrypt/scrypt/argon2"),
82
+ "crypt": (Severity.MEDIUM, "Unix crypt() for password hashing — use modern alternatives"),
83
+ }
84
+ _GOOD_HASH_PATTERNS = [
85
+ re.compile(r"(?:bcrypt|scrypt|argon2|pbkdf2)", re.I),
86
+ ]
87
+
88
+ # ── Weak password hashing literal patterns (regex fallback / unsupported langs) ──
89
+ _WEAK_HASH_PATTERNS = [
90
+ (
91
+ re.compile(r"(?:hashlib\.)?md5\s*\(", re.I),
92
+ Severity.CRITICAL,
93
+ "MD5 used for password hashing — cryptographically broken",
94
+ ),
95
+ (
96
+ re.compile(r"(?:hashlib\.)?sha1\s*\(", re.I),
97
+ Severity.CRITICAL,
98
+ "SHA1 used for password hashing — cryptographically weak",
99
+ ),
100
+ (
101
+ re.compile(r"(?:hashlib\.)?sha256\s*\(", re.I),
102
+ Severity.HIGH,
103
+ "SHA-256 used for password hashing — use bcrypt/scrypt/argon2 instead",
104
+ ),
105
+ (
106
+ re.compile(r"hashlib\.sha512\s*\(", re.I),
107
+ Severity.MEDIUM,
108
+ "SHA-512 used for password hashing — prefer bcrypt/scrypt/argon2",
109
+ ),
110
+ (
111
+ re.compile(r"crypt\s*\(", re.I),
112
+ Severity.MEDIUM,
113
+ "Unix crypt() for password hashing — use modern alternatives",
114
+ ),
115
+ ]
116
+
117
+ # ── Session patterns ────────────────────────────────────────────────────
118
+ _SESSION_PATTERNS = [
119
+ (
120
+ re.compile(r"session\[.*?\]\s*=", re.I),
121
+ Severity.LOW,
122
+ "Session assignment — verify secure configuration",
123
+ ),
124
+ (
125
+ re.compile(
126
+ r'(?:session\.secret|SECRET_KEY|session_secret)\s*[:=]\s*["\'][^"\']{4,}["\']', re.I
127
+ ),
128
+ Severity.CRITICAL,
129
+ "Hardcoded session secret in source code",
130
+ ),
131
+ (
132
+ re.compile(r"(?:cookie_httponly|httponly.*cookie|Secure.*cookie)", re.I),
133
+ Severity.LOW,
134
+ "Cookie security configuration found — verify all flags set",
135
+ ),
136
+ (
137
+ re.compile(r"(?:set_cookie|set_cookie_attr|response\.set_cookie)", re.I),
138
+ Severity.LOW,
139
+ "Cookie being set — verify httponly, secure, and samesite flags",
140
+ ),
141
+ ]
142
+
143
+ # ── Auth endpoint patterns ──────────────────────────────────────────────
144
+ _AUTH_ENDPOINT_PATTERNS = [
145
+ re.compile(r"(?:/login|/signin|/auth|/authenticate|/register|/signup|/token)", re.I),
146
+ re.compile(r"(?:login|signin|auth|authenticate|register|signup)\s*\(", re.I),
147
+ ]
148
+
149
+ # ── Rate limiting patterns ──────────────────────────────────────────────
150
+ _RATE_LIMIT_PATTERNS = [
151
+ re.compile(r"(?:rate.?limit|throttl|brute.?force|login.?attempt|max.?attempt)", re.I),
152
+ ]
153
+
154
+ # ── OAuth patterns ──────────────────────────────────────────────────────
155
+ _OAUTH_PATTERNS = [
156
+ (
157
+ re.compile(r"(?:oauth|openid|oidc)", re.I),
158
+ Severity.INFO,
159
+ "OAuth/OIDC code found — verify implementation security",
160
+ ),
161
+ (
162
+ re.compile(r'(?:redirect_uri|callback_url)\s*[:=]\s*["\'][^"\']*["\']', re.I),
163
+ Severity.MEDIUM,
164
+ "OAuth redirect URI — verify it's not open redirect",
165
+ ),
166
+ (
167
+ re.compile(r'(?:client_secret|client_id)\s*[:=]\s*["\'][^"\']{8,}["\']', re.I),
168
+ Severity.CRITICAL,
169
+ "OAuth client secret hardcoded in source",
170
+ ),
171
+ ]
172
+
173
+ _SKIP_DIRS = DEFAULT_IGNORE_DIRS
174
+
175
+
176
+ _log = logging.getLogger("patchi.security.auth_audit_agent")
177
+
178
+
179
+ @register
180
+ class AuthenticationAuditAgent(BaseAgent):
181
+ """Agent for auditing authentication and session security."""
182
+
183
+ group = AgentGroup.SECURITY
184
+ domain = AgentDomain.SECURITY
185
+ name = "AuthenticationAuditAgent"
186
+ description = (
187
+ "Auth security: CSRF, password hashing, session management, brute-force protection"
188
+ )
189
+
190
+ def _run(self, inp: AgentInput, result: AgentResult) -> None:
191
+ from patchi.core.brain.project_context import agent_is_relevant
192
+
193
+ relevant, reason = agent_is_relevant(inp.domain, inp.purpose, self.name, inp.active_domains)
194
+ if not relevant:
195
+ result.status = AgentStatus.SKIPPED
196
+ result.data.update({"skip_reason": reason})
197
+ return
198
+
199
+ findings = []
200
+ files_scanned = 0
201
+
202
+ for ext in _SCAN_EXTENSIONS:
203
+ for p in self._safe_rglob(inp.root, f"*{ext}"):
204
+ rel = str(p.relative_to(inp.root))
205
+ if self._should_skip(rel):
206
+ continue
207
+ files_scanned += 1
208
+ try:
209
+ content = p.read_text(encoding="utf-8", errors="ignore")
210
+ except Exception as e:
211
+ _log.warning("AuthenticationAuditAgent._run failed: %s", e)
212
+ continue
213
+ lang = EXTENSION_MAP.get(ext)
214
+ findings.extend(self._check_password_hashing(content, rel, lang))
215
+ findings.extend(self._check_session_security(content, rel))
216
+ findings.extend(self._check_oauth(content, rel))
217
+ findings.extend(self._check_rate_limiting(content, rel))
218
+
219
+ result.status = AgentStatus.DONE
220
+ result.findings = findings
221
+ result.files_scanned = files_scanned
222
+ result.data.update({"files_scanned": files_scanned, "finding_count": len(findings)})
223
+ return
224
+
225
+ # ── Password hashing (AST + regex) ─────────────────────────────────────
226
+
227
+ def _check_password_hashing(
228
+ self, content: str, rel_path: str, lang: Lang | None
229
+ ) -> list[Finding]:
230
+ findings = []
231
+ lines = content.splitlines()
232
+
233
+ # AST-based weak hash detection (structural, language-agnostic where possible).
234
+ ast_hits: dict[int, tuple[Severity, str]] = {}
235
+ if lang is not None:
236
+ for call in find_calls(content, lang, _WEAK_HASH_NAMES):
237
+ name = call.get("name", "")
238
+ leaf = name.split(".")[-1].lower()
239
+ info = _WEAK_HASH_INFO.get(leaf)
240
+ if info:
241
+ ast_hits[call.get("line", 0)] = info
242
+
243
+ # Literal-pattern regex fallback (also catches hashlib.md5(...), etc.).
244
+ for line_num, line in enumerate(lines, 1):
245
+ stripped = line.strip()
246
+ if stripped.startswith("#"):
247
+ continue
248
+ for pattern, severity, message in _WEAK_HASH_PATTERNS:
249
+ if pattern.search(line):
250
+ ast_hits.setdefault(line_num, (severity, message))
251
+
252
+ has_good_hash = any(p.search(content) for p in _GOOD_HASH_PATTERNS)
253
+
254
+ for line_num in sorted(ast_hits):
255
+ severity, message = ast_hits[line_num]
256
+ if has_good_hash:
257
+ severity = Severity.INFO
258
+ message += " (but good hash found elsewhere in file)"
259
+ findings.append(
260
+ make_finding(
261
+ self.name,
262
+ "weak_password_hash",
263
+ severity,
264
+ rel_path,
265
+ message,
266
+ line=line_num,
267
+ code_snippet=lines[line_num - 1].strip()[:120],
268
+ suggestion="Use bcrypt, scrypt, or argon2 for password hashing.",
269
+ )
270
+ )
271
+ return findings
272
+
273
+ # ── Literal-pattern checks (multi-language) ─────────────────────────────
274
+
275
+ def _check_session_security(self, content: str, rel_path: str) -> list[Finding]:
276
+ return self._line_scan(
277
+ content,
278
+ rel_path,
279
+ _SESSION_PATTERNS,
280
+ "session_security",
281
+ "Ensure session cookies use httponly, secure, and samesite flags.",
282
+ )
283
+
284
+ def _check_oauth(self, content: str, rel_path: str) -> list[Finding]:
285
+ return self._line_scan(
286
+ content,
287
+ rel_path,
288
+ _OAUTH_PATTERNS,
289
+ "oauth_security",
290
+ "Verify OAuth implementation follows security best practices.",
291
+ )
292
+
293
+ def _line_scan(self, content, rel_path, patterns, finding_type, suggestion) -> list[Finding]:
294
+ findings = []
295
+ for line_num, line in enumerate(content.splitlines(), 1):
296
+ stripped = line.strip()
297
+ if stripped.startswith("#"):
298
+ continue
299
+ for pattern, severity, message in patterns:
300
+ if pattern.search(line):
301
+ findings.append(
302
+ make_finding(
303
+ self.name,
304
+ finding_type,
305
+ severity,
306
+ rel_path,
307
+ message,
308
+ line=line_num,
309
+ code_snippet=stripped[:120],
310
+ suggestion=suggestion,
311
+ )
312
+ )
313
+ return findings
314
+
315
+ def _check_rate_limiting(self, content: str, rel_path: str) -> list[Finding]:
316
+ has_rate_limit = any(p.search(content) for p in _RATE_LIMIT_PATTERNS)
317
+ has_auth_endpoint = any(p.search(content) for p in _AUTH_ENDPOINT_PATTERNS)
318
+ if has_auth_endpoint and not has_rate_limit:
319
+ return [
320
+ make_finding(
321
+ self.name,
322
+ "missing_rate_limit",
323
+ Severity.HIGH,
324
+ rel_path,
325
+ "Auth endpoints found but no rate limiting/brute-force protection detected",
326
+ suggestion="Add rate limiting to login/auth endpoints to prevent brute-force attacks.",
327
+ )
328
+ ]
329
+ return []
330
+
331
+ # ── Helpers ───────────────────────────────────────────────────────────
332
+
333
+ def _should_skip(self, rel_path: str) -> bool:
334
+ parts = rel_path.replace("\\", "/").split("/")
335
+ return any(p in _SKIP_DIRS for p in parts)
336
+
337
+ def _safe_rglob(self, root: Path, pattern: str):
338
+ """Directory-walking with pruning."""
339
+ suffix = pattern.replace("*.", ".")
340
+ for dirpath, dirnames, filenames in os.walk(root):
341
+ dirnames[:] = [d for d in dirnames if d not in _SKIP_DIRS]
342
+ for fn in filenames:
343
+ if fn.endswith(suffix):
344
+ yield Path(dirpath) / fn