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,865 @@
1
+ """
2
+ DAST Agent — Dynamic Application Security Testing with Playwright.
3
+
4
+ Performs live security testing against running applications:
5
+ - XSS (Cross-Site Scripting) injection
6
+ - SQL Injection testing
7
+ - CSRF vulnerability detection
8
+ - Clickjacking checks
9
+ - Security header validation
10
+ - Information disclosure detection
11
+ - Screenshot capture for evidence
12
+
13
+ Uses BrowserPool for parallel testing and ScreenshotManager for evidence.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import asyncio
19
+ import logging
20
+ import time
21
+ from dataclasses import dataclass
22
+ from typing import Any
23
+
24
+ from patchi.core.agents.base import (
25
+ AgentDomain,
26
+ AgentGroup,
27
+ AgentInput,
28
+ AgentResult,
29
+ BaseAgent,
30
+ Finding,
31
+ Severity,
32
+ register,
33
+ )
34
+
35
+ _log = logging.getLogger("patchi.security.dast")
36
+
37
+ # Additional XSS payloads from DASTScanner
38
+ ADDITIONAL_XSS_PAYLOADS = [
39
+ "<img src=x onerror=alert(1)>",
40
+ "<svg onload=alert(1)>",
41
+ "'-alert(1)-'",
42
+ ]
43
+
44
+ # Additional security headers from DASTScanner
45
+ ADDITIONAL_HEADER_CHECKS = [
46
+ (
47
+ "x-content-type-options",
48
+ "nosniff",
49
+ "Missing X-Content-Type-Options header",
50
+ "medium",
51
+ ),
52
+ ("x-xss-protection", None, "Missing X-XSS-Protection header", "low"),
53
+ ]
54
+
55
+ # Auth bypass keywords from DASTScanner
56
+ AUTH_BYPASS_KEYWORDS = [
57
+ "dashboard",
58
+ "admin",
59
+ "settings",
60
+ "profile",
61
+ "logout",
62
+ ]
63
+
64
+ # Info disclosure indicators from DASTScanner
65
+ INFO_DISCLOSURE_INDICATORS = [
66
+ "traceback",
67
+ "stack trace",
68
+ "exception in",
69
+ ]
70
+
71
+ # Open redirect payloads from DASTScanner
72
+ OPEN_REDIRECT_PAYLOADS = [
73
+ "https://evil.com",
74
+ "//evil.com",
75
+ "/\\evil.com",
76
+ ]
77
+
78
+ # Common endpoint paths from DASTScanner for discovery
79
+ COMMON_ENDPOINT_PATHS = [
80
+ "/",
81
+ "/login",
82
+ "/admin",
83
+ "/api",
84
+ "/dashboard",
85
+ "/settings",
86
+ "/profile",
87
+ "/users",
88
+ "/health",
89
+ "/api/health",
90
+ "/api/status",
91
+ "/static/",
92
+ ]
93
+
94
+
95
+ @dataclass
96
+ class DASTTest:
97
+ """A single DAST test case."""
98
+
99
+ name: str
100
+ description: str
101
+ category: str # xss, sqli, csrf, header, info_disclosure, clickjacking
102
+ severity: Severity
103
+ test_fn: str # Method name to call
104
+ tags: list[str] | None = None
105
+
106
+
107
+ # ── Test Registry ────────────────────────────────────────────────────────────
108
+
109
+ DAST_TESTS: list[DASTTest] = [
110
+ # XSS Tests
111
+ DASTTest(
112
+ name="xss_reflected_basic",
113
+ description="Test for reflected XSS via basic script injection",
114
+ category="xss",
115
+ severity=Severity.HIGH,
116
+ test_fn="test_xss_reflected",
117
+ tags=["xss", "injection", "reflected"],
118
+ ),
119
+ DASTTest(
120
+ name="xss_stored_svg",
121
+ description="Test for stored XSS via SVG injection",
122
+ category="xss",
123
+ severity=Severity.HIGH,
124
+ test_fn="test_xss_svg",
125
+ tags=["xss", "injection", "stored"],
126
+ ),
127
+ # SQL Injection Tests
128
+ DASTTest(
129
+ name="sqli_error_based",
130
+ description="Test for SQL injection via error-based extraction",
131
+ category="sqli",
132
+ severity=Severity.CRITICAL,
133
+ test_fn="test_sqli_error",
134
+ tags=["sqli", "injection", "database"],
135
+ ),
136
+ DASTTest(
137
+ name="sqli_union_select",
138
+ description="Test for SQL injection via UNION SELECT",
139
+ category="sqli",
140
+ severity=Severity.CRITICAL,
141
+ test_fn="test_sqli_union",
142
+ tags=["sqli", "injection", "database"],
143
+ ),
144
+ # Security Headers
145
+ DASTTest(
146
+ name="missing_content_security_policy",
147
+ description="Check for missing Content-Security-Policy header",
148
+ category="header",
149
+ severity=Severity.MEDIUM,
150
+ test_fn="test_csp_header",
151
+ tags=["headers", "csp"],
152
+ ),
153
+ DASTTest(
154
+ name="missing_x_frame_options",
155
+ description="Check for missing X-Frame-Options header (clickjacking)",
156
+ category="clickjacking",
157
+ severity=Severity.MEDIUM,
158
+ test_fn="test_x_frame_options",
159
+ tags=["headers", "clickjacking"],
160
+ ),
161
+ DASTTest(
162
+ name="missing_hsts",
163
+ description="Check for missing Strict-Transport-Security header",
164
+ category="header",
165
+ severity=Severity.LOW,
166
+ test_fn="test_hsts_header",
167
+ tags=["headers", "hsts"],
168
+ ),
169
+ # Information Disclosure
170
+ DASTTest(
171
+ name="server_version_disclosure",
172
+ description="Check for server version disclosure in headers",
173
+ category="info_disclosure",
174
+ severity=Severity.LOW,
175
+ test_fn="test_server_version",
176
+ tags=["info", "server"],
177
+ ),
178
+ DASTTest(
179
+ name="error_page_disclosure",
180
+ description="Check if error pages reveal stack traces",
181
+ category="info_disclosure",
182
+ severity=Severity.MEDIUM,
183
+ test_fn="test_error_disclosure",
184
+ tags=["info", "errors"],
185
+ ),
186
+ # Directory Traversal
187
+ DASTTest(
188
+ name="directory_traversal",
189
+ description="Test for directory traversal via path manipulation",
190
+ category="path_traversal",
191
+ severity=Severity.HIGH,
192
+ test_fn="test_directory_traversal",
193
+ tags=["traversal", "directory"],
194
+ ),
195
+ # CSRF
196
+ DASTTest(
197
+ name="csrf_token_missing",
198
+ description="Check for missing CSRF tokens in forms",
199
+ category="csrf",
200
+ severity=Severity.MEDIUM,
201
+ test_fn="test_csrf_token",
202
+ tags=["csrf", "forms"],
203
+ ),
204
+ # Auth Bypass (from DASTScanner)
205
+ DASTTest(
206
+ name="auth_bypass",
207
+ description="Test for authentication bypass on protected pages",
208
+ category="auth_bypass",
209
+ severity=Severity.CRITICAL,
210
+ test_fn="test_auth_bypass",
211
+ tags=["auth", "bypass"],
212
+ ),
213
+ # Open Redirect (from DASTScanner)
214
+ DASTTest(
215
+ name="open_redirect",
216
+ description="Test for open redirect vulnerabilities",
217
+ category="open_redirect",
218
+ severity=Severity.HIGH,
219
+ test_fn="test_open_redirect",
220
+ tags=["redirect", "open_redirect"],
221
+ ),
222
+ # Additional Security Headers (from DASTScanner)
223
+ DASTTest(
224
+ name="missing_x_content_type_options",
225
+ description="Check for missing X-Content-Type-Options header",
226
+ category="header",
227
+ severity=Severity.MEDIUM,
228
+ test_fn="test_additional_headers",
229
+ tags=["headers", "x-content-type-options"],
230
+ ),
231
+ DASTTest(
232
+ name="missing_x_xss_protection",
233
+ description="Check for missing X-XSS-Protection header",
234
+ category="header",
235
+ severity=Severity.LOW,
236
+ test_fn="test_additional_headers",
237
+ tags=["headers", "x-xss-protection"],
238
+ ),
239
+ # Stack Trace Disclosure (from DASTScanner)
240
+ DASTTest(
241
+ name="stack_trace_disclosure",
242
+ description="Check if error pages reveal stack traces",
243
+ category="info_disclosure",
244
+ severity=Severity.HIGH,
245
+ test_fn="test_stack_trace_disclosure",
246
+ tags=["info", "stack_trace"],
247
+ ),
248
+ ]
249
+
250
+
251
+ class DASTAgent(BaseAgent):
252
+ """
253
+ Dynamic Application Security Testing agent.
254
+
255
+ Runs live security tests against a running web application using Playwright.
256
+ Captures screenshots as evidence and reports findings.
257
+ """
258
+
259
+ name = "DASTAgent"
260
+ group = AgentGroup.SECURITY
261
+ domain = AgentDomain.SECURITY
262
+ timeout = 300 # 5 minutes for browser tests
263
+
264
+ def _run(self, inp: AgentInput, result: AgentResult) -> None:
265
+ """Run DAST tests against the target application."""
266
+ target_url = self._detect_target(inp.root)
267
+ if not target_url:
268
+ result.add_finding(Finding(
269
+ agent=self.name,
270
+ type="dast_config",
271
+ severity=Severity.INFO,
272
+ message="No running web application detected. Start the web server first.",
273
+ ))
274
+ return
275
+
276
+ _log.info("DAST: Testing %s", target_url)
277
+
278
+ # Run tests synchronously (browser tests)
279
+ test_results = asyncio.run(self._run_tests(target_url, inp))
280
+
281
+ # Add findings to result
282
+ for finding in test_results:
283
+ result.add_finding(finding)
284
+
285
+ result.files_scanned = 1
286
+ result.data["target_url"] = target_url
287
+ result.data["tests_run"] = len(DAST_TESTS)
288
+ result.data["findings_count"] = len(test_results)
289
+
290
+ async def _run_tests(self, target_url: str, inp: AgentInput) -> list[Finding]:
291
+ """Run all DAST tests asynchronously with video recording."""
292
+ findings: list[Finding] = []
293
+
294
+ try:
295
+ from patchi.core.testing.live_v2.screenshot_manager import ScreenshotManager
296
+ from patchi.core.testing.live_v2.video_recorder import (
297
+ RecordingConfig,
298
+ VideoRecorder,
299
+ )
300
+
301
+ screenshot_mgr = ScreenshotManager(
302
+ baseline_dir=inp.root / ".patchi" / "evidence" / "dast"
303
+ )
304
+
305
+ # Initialize video recorder — outputs to evidence/video
306
+ video_dir = inp.root / ".patchi" / "evidence" / "video"
307
+ video_dir.mkdir(parents=True, exist_ok=True)
308
+ recorder = VideoRecorder(RecordingConfig(output_dir=video_dir))
309
+
310
+ # Run each test with video recording
311
+ for test in DAST_TESTS:
312
+ recording_id = None
313
+ try:
314
+ finding, recording_id = await self._run_test_recorded(
315
+ recorder, screenshot_mgr, test, target_url
316
+ )
317
+ if finding:
318
+ findings.append(finding)
319
+ except Exception as e:
320
+ _log.warning("DAST test %s failed: %s", test.name, e)
321
+ finally:
322
+ # Ensure recording is stopped even on error
323
+ if recording_id and recording_id in recorder._active_recordings:
324
+ try:
325
+ await recorder.stop_recording(recording_id)
326
+ except Exception as _exc:
327
+ _log.warning('_run_tests failed: %s', _exc)
328
+
329
+ except ImportError:
330
+ _log.warning("Playwright not installed. Install with: pip install playwright")
331
+ except Exception as e:
332
+ _log.error("DAST agent failed: %s", e)
333
+
334
+ return findings
335
+
336
+ async def _run_test_recorded(
337
+ self,
338
+ recorder,
339
+ screenshot_mgr,
340
+ test: DASTTest,
341
+ target_url: str,
342
+ ) -> tuple[Finding | None, str | None]:
343
+ """Run a single DAST test with video recording.
344
+
345
+ Returns (finding, recording_id) so the caller can stop the recording.
346
+ """
347
+ # Start a recorded browser context for this test
348
+ test_name = f"dast_{test.name}_{int(time.time())}"
349
+ context, browser = await recorder.start_recording(test_name)
350
+ recording_id = f"{test_name}-{int(time.time())}"
351
+
352
+ page = await context.new_page()
353
+
354
+ try:
355
+ # Get the test function
356
+ test_fn = getattr(self, test.test_fn, None)
357
+ if not test_fn:
358
+ _log.warning("DAST test function not found: %s", test.test_fn)
359
+ return None, recording_id
360
+
361
+ # Run the test
362
+ vuln_found, evidence = await test_fn(page, target_url, screenshot_mgr)
363
+
364
+ if vuln_found:
365
+ # Capture screenshot as evidence
366
+ try:
367
+ await screenshot_mgr.capture(
368
+ page, target_url, name=f"dast_{test.name}"
369
+ )
370
+ except Exception as e:
371
+ _log.debug("Screenshot capture failed: %s", e)
372
+
373
+ return Finding(
374
+ agent=self.name,
375
+ type=f"dast_{test.category}",
376
+ severity=test.severity,
377
+ message=test.description,
378
+ file=target_url,
379
+ suggestion=f"Fix {test.category} vulnerability: {evidence}",
380
+ code_snippet=evidence[:500] if evidence else "",
381
+ ), recording_id
382
+
383
+ return None, recording_id
384
+
385
+ finally:
386
+ # Close page and stop recording
387
+ try:
388
+ await page.close()
389
+ except Exception as _exc:
390
+ _log.warning('_run_test_recorded failed: %s', _exc)
391
+ # Stop recording and save video
392
+ try:
393
+ if recording_id in recorder._active_recordings:
394
+ recording = await recorder.stop_recording(recording_id)
395
+ _log.info(
396
+ "DAST video recorded: %s (%.1fs)",
397
+ test.name,
398
+ recording.total_duration_seconds,
399
+ )
400
+ except Exception as e:
401
+ _log.debug("Video recording stop failed: %s", e)
402
+ try:
403
+ await browser.close()
404
+ except Exception as _exc:
405
+ _log.warning('_run_test_recorded failed: %s', _exc)
406
+
407
+ # ── XSS Tests ──────────────────────────────────────────────────────────
408
+
409
+ async def test_xss_reflected(
410
+ self, page: Any, base_url: str, screenshot_mgr: Any
411
+ ) -> tuple[bool, str]:
412
+ """Test for reflected XSS via basic script injection."""
413
+ payloads = [
414
+ "<script>alert('XSS')</script>",
415
+ "<img src=x onerror=alert(1)>",
416
+ "<svg onload=alert(1)>",
417
+ ]
418
+
419
+ for payload in payloads:
420
+ try:
421
+ # Try common injection points
422
+ test_urls = [
423
+ f"{base_url}?q={payload}",
424
+ f"{base_url}/search?q={payload}",
425
+ f"{base_url}/?error={payload}",
426
+ ]
427
+
428
+ for url in test_urls:
429
+ await page.goto(url, wait_until="domcontentloaded", timeout=10000)
430
+ content = await page.content()
431
+
432
+ # Check if payload is reflected unescaped
433
+ if payload in content and "<script>" in payload:
434
+ return True, f"Reflected XSS at {url} with payload: {payload}"
435
+
436
+ except Exception:
437
+ continue
438
+
439
+ return False, ""
440
+
441
+ async def test_xss_svg(
442
+ self, page: Any, base_url: str, screenshot_mgr: Any
443
+ ) -> tuple[bool, str]:
444
+ """Test for stored XSS via SVG injection."""
445
+
446
+ try:
447
+ # Try common upload/comment endpoints
448
+ endpoints = ["/upload", "/comment", "/profile", "/bio"]
449
+
450
+ for endpoint in endpoints:
451
+ url = f"{base_url}{endpoint}"
452
+ await page.goto(url, wait_until="domcontentloaded", timeout=10000)
453
+
454
+ # Look for file upload inputs
455
+ file_inputs = await page.query_selector_all('input[type="file"]')
456
+ if file_inputs:
457
+ # Note: actual file upload testing would require creating a temp file
458
+ return True, f"Potential XSS via SVG upload at {url}"
459
+
460
+ except Exception as _exc:
461
+ _log.warning('test_xss_svg failed: %s', _exc)
462
+
463
+ return False, ""
464
+
465
+ # ── SQL Injection Tests ────────────────────────────────────────────────
466
+
467
+ async def test_sqli_error(
468
+ self, page: Any, base_url: str, screenshot_mgr: Any
469
+ ) -> tuple[bool, str]:
470
+ """Test for SQL injection via error-based extraction."""
471
+ payloads = [
472
+ "'",
473
+ "1' OR '1'='1",
474
+ "1' UNION SELECT NULL--",
475
+ ]
476
+
477
+ sql_errors = [
478
+ "SQL syntax",
479
+ "mysql_fetch",
480
+ "ORA-01756",
481
+ "PostgreSQL",
482
+ "SQLite3::",
483
+ "Microsoft OLE DB",
484
+ "ODBC SQL Server",
485
+ ]
486
+
487
+ for payload in payloads:
488
+ try:
489
+ test_urls = [
490
+ f"{base_url}?id={payload}",
491
+ f"{base_url}/user?id={payload}",
492
+ f"{base_url}/api/data?id={payload}",
493
+ ]
494
+
495
+ for url in test_urls:
496
+ await page.goto(url, wait_until="domcontentloaded", timeout=10000)
497
+ content = await page.content()
498
+
499
+ for error in sql_errors:
500
+ if error.lower() in content.lower():
501
+ return True, f"SQL injection error at {url}: {error}"
502
+
503
+ except Exception:
504
+ continue
505
+
506
+ return False, ""
507
+
508
+ async def test_sqli_union(
509
+ self, page: Any, base_url: str, screenshot_mgr: Any
510
+ ) -> tuple[bool, str]:
511
+ """Test for SQL injection via UNION SELECT."""
512
+ # Simplified check - look for SQL error patterns
513
+ return await self.test_sqli_error(page, base_url, screenshot_mgr)
514
+
515
+ # ── Header Tests ───────────────────────────────────────────────────────
516
+
517
+ async def test_csp_header(
518
+ self, page: Any, base_url: str, screenshot_mgr: Any
519
+ ) -> tuple[bool, str]:
520
+ """Check for missing Content-Security-Policy header."""
521
+ try:
522
+ response = await page.goto(base_url, wait_until="domcontentloaded")
523
+ csp = response.headers.get("content-security-policy", "")
524
+
525
+ if not csp:
526
+ return True, "Missing Content-Security-Policy header"
527
+
528
+ except Exception as _exc:
529
+ _log.warning('test_csp_header failed: %s', _exc)
530
+
531
+ return False, ""
532
+
533
+ async def test_x_frame_options(
534
+ self, page: Any, base_url: str, screenshot_mgr: Any
535
+ ) -> tuple[bool, str]:
536
+ """Check for missing X-Frame-Options header."""
537
+ try:
538
+ response = await page.goto(base_url, wait_until="domcontentloaded")
539
+ xfo = response.headers.get("x-frame-options", "")
540
+ csp = response.headers.get("content-security-policy", "")
541
+
542
+ if not xfo and "frame-ancestors" not in csp:
543
+ return True, "Missing X-Frame-Options header (clickjacking risk)"
544
+
545
+ except Exception as _exc:
546
+ _log.warning('test_x_frame_options failed: %s', _exc)
547
+
548
+ return False, ""
549
+
550
+ async def test_hsts_header(
551
+ self, page: Any, base_url: str, screenshot_mgr: Any
552
+ ) -> tuple[bool, str]:
553
+ """Check for missing Strict-Transport-Security header."""
554
+ if not base_url.startswith("https"):
555
+ return False, ""
556
+
557
+ try:
558
+ response = await page.goto(base_url, wait_until="domcontentloaded")
559
+ hsts = response.headers.get("strict-transport-security", "")
560
+
561
+ if not hsts:
562
+ return True, "Missing Strict-Transport-Security header"
563
+
564
+ except Exception as _exc:
565
+ _log.warning('test_hsts_header failed: %s', _exc)
566
+
567
+ return False, ""
568
+
569
+ # ── Information Disclosure ─────────────────────────────────────────────
570
+
571
+ async def test_server_version(
572
+ self, page: Any, base_url: str, screenshot_mgr: Any
573
+ ) -> tuple[bool, str]:
574
+ """Check for server version disclosure in headers."""
575
+ try:
576
+ response = await page.goto(base_url, wait_until="domcontentloaded")
577
+ server = response.headers.get("server", "")
578
+ x_powered = response.headers.get("x-powered-by", "")
579
+
580
+ if server and any(v in server.lower() for v in ["apache", "nginx", "iis"]):
581
+ return True, f"Server version disclosed: {server}"
582
+
583
+ if x_powered:
584
+ return True, f"X-Powered-By disclosed: {x_powered}"
585
+
586
+ except Exception as _exc:
587
+ _log.warning('test_server_version failed: %s', _exc)
588
+
589
+ return False, ""
590
+
591
+ async def test_error_disclosure(
592
+ self, page: Any, base_url: str, screenshot_mgr: Any
593
+ ) -> tuple[bool, str]:
594
+ """Check if error pages reveal stack traces."""
595
+ error_indicators = [
596
+ "Traceback (most recent call last)",
597
+ "Stack Trace:",
598
+ "at line",
599
+ "Exception in thread",
600
+ "Fatal error",
601
+ ]
602
+
603
+ try:
604
+ # Try to trigger error pages
605
+ error_urls = [
606
+ f"{base_url}/nonexistent-page-12345",
607
+ f"{base_url}/api/nonexistent-endpoint",
608
+ f"{base_url}/?error=trigger",
609
+ ]
610
+
611
+ for url in error_urls:
612
+ await page.goto(url, wait_until="domcontentloaded")
613
+ content = await page.content()
614
+
615
+ for indicator in error_indicators:
616
+ if indicator.lower() in content.lower():
617
+ return True, f"Error disclosure at {url}: {indicator}"
618
+
619
+ except Exception as _exc:
620
+ _log.warning('test_error_disclosure failed: %s', _exc)
621
+
622
+ return False, ""
623
+
624
+ # ── Directory Traversal ────────────────────────────────────────────────
625
+
626
+ async def test_directory_traversal(
627
+ self, page: Any, base_url: str, screenshot_mgr: Any
628
+ ) -> tuple[bool, str]:
629
+ """Test for directory traversal via path manipulation."""
630
+ payloads = [
631
+ "../../../etc/passwd",
632
+ "....//....//etc/passwd",
633
+ "%2e%2e%2f%2e%2e%2fetc/passwd",
634
+ ]
635
+
636
+ traversal_indicators = [
637
+ "root:x:0:0",
638
+ "/bin/bash",
639
+ "/bin/sh",
640
+ "[boot loader]",
641
+ ]
642
+
643
+ for payload in payloads:
644
+ try:
645
+ test_urls = [
646
+ f"{base_url}/file?path={payload}",
647
+ f"{base_url}/download?file={payload}",
648
+ f"{base_url}/view?path={payload}",
649
+ ]
650
+
651
+ for url in test_urls:
652
+ await page.goto(url, wait_until="domcontentloaded", timeout=10000)
653
+ content = await page.content()
654
+
655
+ for indicator in traversal_indicators:
656
+ if indicator in content:
657
+ return True, f"Directory traversal at {url}: {indicator}"
658
+
659
+ except Exception:
660
+ continue
661
+
662
+ return False, ""
663
+
664
+ # ── CSRF Tests ─────────────────────────────────────────────────────────
665
+
666
+ async def test_csrf_token(
667
+ self, page: Any, base_url: str, screenshot_mgr: Any
668
+ ) -> tuple[bool, str]:
669
+ """Check for missing CSRF tokens in forms."""
670
+ try:
671
+ await page.goto(base_url, wait_until="domcontentloaded")
672
+
673
+ # Find all forms
674
+ forms = await page.query_selector_all("form")
675
+
676
+ for _i, form in enumerate(forms):
677
+ # Check for CSRF token fields
678
+ csrf_inputs = await form.query_selector_all(
679
+ 'input[name*="csrf"], input[name*="token"], input[name="_token"]'
680
+ )
681
+
682
+ method = await form.get_attribute("method") or "GET"
683
+ if method.upper() == "POST" and not csrf_inputs:
684
+ action = await form.get_attribute("action") or "unknown"
685
+ return True, f"Form at {action} missing CSRF token"
686
+
687
+ except Exception as _exc:
688
+ _log.warning('test_csrf_token failed: %s', _exc)
689
+
690
+ return False, ""
691
+
692
+ # ── Endpoint Discovery (from DASTScanner) ──────────────────────────────
693
+
694
+ async def _discover_endpoints(self, base_url: str) -> list[str]:
695
+ """Discover endpoints by crawling the app (from DASTScanner)."""
696
+ import socket
697
+ endpoints = set()
698
+ endpoints.add(base_url)
699
+
700
+ for path in COMMON_ENDPOINT_PATHS:
701
+ url = f"{base_url}{path}"
702
+ try:
703
+ # Quick TCP check first
704
+ try:
705
+ with socket.create_connection(("127.0.0.1", 80), timeout=1):
706
+ pass
707
+ except Exception:
708
+ pass # Can't check easily without page context
709
+
710
+ # Use page context for actual check
711
+ async with self._temp_page() as page:
712
+ resp = await page.goto(url, wait_until="domcontentloaded", timeout=5000)
713
+ if resp and resp.status < 400:
714
+ endpoints.add(url)
715
+ # Extract links from page
716
+ links = await page.eval_on_selector_all(
717
+ "a[href]",
718
+ "els => els.map(e => e.href)",
719
+ )
720
+ for link in links:
721
+ if link.startswith(base_url):
722
+ endpoints.add(link)
723
+ except Exception as _exc:
724
+ _log.debug('_discover_endpoints failed: %s', _exc)
725
+
726
+ return list(endpoints)[:50] # cap at 50 endpoints
727
+
728
+ async def _temp_page(self):
729
+ """Create a temporary page for endpoint discovery."""
730
+ from patchi.core.testing.live_v2.browser_pool import BrowserPool
731
+ pool = BrowserPool()
732
+ await pool.initialize()
733
+ page = await pool.get_page()
734
+ return page
735
+
736
+ # ── Auth Bypass Tests (from DASTScanner) ──────────────────────────────
737
+
738
+ async def test_auth_bypass(
739
+ self, page: Any, base_url: str, screenshot_mgr: Any
740
+ ) -> tuple[bool, str]:
741
+ """Test for authentication bypass on protected pages."""
742
+ try:
743
+ resp = await page.goto(base_url, wait_until="domcontentloaded", timeout=5000)
744
+ if not resp:
745
+ return False, ""
746
+
747
+ content = await page.content()
748
+ has_login = bool(await page.query_selector("input[type=password]"))
749
+ has_protected = any(
750
+ kw in content.lower()
751
+ for kw in AUTH_BYPASS_KEYWORDS
752
+ )
753
+
754
+ if has_protected and not has_login:
755
+ # Page has protected content but no login form — possible bypass
756
+ return True, f"Auth bypass: Protected content accessible without authentication at {base_url}"
757
+
758
+ except Exception as e:
759
+ _log.debug("Auth bypass test failed: %s", e)
760
+
761
+ return False, ""
762
+
763
+ # ── Open Redirect Tests (from DASTScanner) ──────────────────────────────
764
+
765
+ async def test_open_redirect(
766
+ self, page: Any, base_url: str, screenshot_mgr: Any
767
+ ) -> tuple[bool, str]:
768
+ """Test for open redirect vulnerabilities."""
769
+ for payload in OPEN_REDIRECT_PAYLOADS:
770
+ try:
771
+ test_url = f"{base_url}?redirect={payload}"
772
+ resp = await page.goto(test_url, wait_until="domcontentloaded", timeout=5000)
773
+ if resp and page.url.startswith("http"):
774
+ current = page.url
775
+ if "evil.com" in current:
776
+ return True, f"Open redirect at {test_url}: Redirected to {current}"
777
+
778
+ except Exception:
779
+ continue
780
+
781
+ return False, ""
782
+
783
+ # ── Additional Security Headers (from DASTScanner) ──────────────────────
784
+
785
+ async def test_additional_headers(
786
+ self, page: Any, base_url: str, screenshot_mgr: Any
787
+ ) -> tuple[bool, str]:
788
+ """Check for additional missing security headers."""
789
+ try:
790
+ resp = await page.goto(base_url, wait_until="domcontentloaded")
791
+ if not resp:
792
+ return False, ""
793
+
794
+ headers = {k.lower(): v for k, v in resp.headers.items()}
795
+
796
+ for header, expected_value, message, severity in ADDITIONAL_HEADER_CHECKS:
797
+ if header not in headers:
798
+ return True, message
799
+ elif expected_value and headers[header] != expected_value:
800
+ return True, f"{header} = {headers[header]} (expected {expected_value})"
801
+
802
+ except Exception as e:
803
+ _log.debug("Additional headers test failed: %s", e)
804
+
805
+ return False, ""
806
+
807
+ # ── Info Disclosure Stack Trace (from DASTScanner) ──────────────────────
808
+
809
+ async def test_stack_trace_disclosure(
810
+ self, page: Any, base_url: str, screenshot_mgr: Any
811
+ ) -> tuple[bool, str]:
812
+ """Check if error pages reveal stack traces."""
813
+ try:
814
+ error_urls = [
815
+ f"{base_url}/nonexistent-page-12345",
816
+ f"{base_url}/api/nonexistent-endpoint",
817
+ f"{base_url}/?error=trigger",
818
+ ]
819
+
820
+ for url in error_urls:
821
+ await page.goto(url, wait_until="domcontentloaded")
822
+ content = await page.content()
823
+
824
+ for indicator in INFO_DISCLOSURE_INDICATORS:
825
+ if indicator.lower() in content.lower():
826
+ return True, f"Stack trace disclosure at {url}: {indicator}"
827
+
828
+ except Exception as _exc:
829
+ _log.warning('test_stack_trace_disclosure failed: %s', _exc)
830
+
831
+ return False, ""
832
+
833
+ # ── Helper Methods ─────────────────────────────────────────────────────
834
+
835
+ def _detect_target(self, root) -> str | None:
836
+ """Detect the target URL for testing."""
837
+ import socket
838
+
839
+ # Try common ports
840
+ ports = [1612, 8000, 3000, 8080, 80]
841
+
842
+ for port in ports:
843
+ try:
844
+ with socket.create_connection(("127.0.0.1", port), timeout=1):
845
+ return f"http://127.0.0.1:{port}"
846
+ except (ConnectionRefusedError, OSError):
847
+ continue
848
+
849
+ # Check if there's a web server in the config
850
+ try:
851
+ from patchi.core import config as cfg
852
+
853
+ config = cfg.load(root)
854
+ web_config = config.get("web", {})
855
+ host = web_config.get("host", "127.0.0.1")
856
+ port = web_config.get("port", 1612)
857
+ return f"http://{host}:{port}"
858
+ except Exception as _exc:
859
+ _log.warning('_detect_target failed: %s', _exc)
860
+
861
+ return None
862
+
863
+
864
+ # Register the agent
865
+ register(DASTAgent)