pwn 0.5.784 → 0.5.787

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 (254) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/Gemfile +6 -6
  4. data/README.md +37 -0
  5. data/bin/pwn +2 -1
  6. data/bin/pwn_web_cache_deception +35 -7
  7. data/build_gem.sh +1 -1
  8. data/documentation/AI-Integration.md +7 -5
  9. data/documentation/Agent-Tool-Registry.md +2 -2
  10. data/documentation/Backup-Retention.md +69 -15
  11. data/documentation/Configuration.md +10 -6
  12. data/documentation/Contributing.md +1 -1
  13. data/documentation/Cron.md +2 -2
  14. data/documentation/Diagrams.md +2 -2
  15. data/documentation/General-PWN-Usage.md +4 -3
  16. data/documentation/Harness-Evaluation.md +1 -1
  17. data/documentation/Home.md +6 -5
  18. data/documentation/How-PWN-Works.md +6 -5
  19. data/documentation/Installation.md +12 -9
  20. data/documentation/Mission-Cancellation.md +134 -0
  21. data/documentation/Operability.md +18 -0
  22. data/documentation/Persistence.md +7 -5
  23. data/documentation/Plugin-Degradation.md +14 -0
  24. data/documentation/Plugins.md +3 -2
  25. data/documentation/Policy-Benchmark.md +32 -33
  26. data/documentation/Runtime-Source-Protection.md +162 -0
  27. data/documentation/Transparent-Browser.md +11 -0
  28. data/documentation/Troubleshooting.md +3 -2
  29. data/documentation/What-is-PWN.md +2 -2
  30. data/documentation/YubiKey-Vaults.md +185 -0
  31. data/documentation/YubiKey.md +218 -0
  32. data/documentation/diagrams/cron-scheduling.svg +130 -130
  33. data/documentation/diagrams/dot/cron-scheduling.dot +3 -3
  34. data/documentation/diagrams/dot/persistence-filesystem.dot +5 -3
  35. data/documentation/diagrams/dot/plugin-ecosystem.dot +2 -2
  36. data/documentation/diagrams/dot/pwn-ai-feedback-learning-loop.dot +1 -1
  37. data/documentation/diagrams/dot/reinforcement-learning.dot +2 -2
  38. data/documentation/diagrams/persistence-filesystem.svg +172 -142
  39. data/documentation/diagrams/plugin-ecosystem.svg +64 -64
  40. data/documentation/diagrams/pwn-ai-feedback-learning-loop.svg +1 -1
  41. data/documentation/diagrams/reinforcement-learning.svg +89 -89
  42. data/documentation/pwn-ai-Agent.md +10 -4
  43. data/etc/default_cron/session_cleanup.rb +43 -27
  44. data/etc/default_skills/pwn/ai/agent/mission_namespace/SKILL.md +21 -0
  45. data/etc/default_skills/pwn/ai/agent/tools/browser_act/SKILL.md +45 -0
  46. data/etc/default_skills/pwn/ai/agent/tools/loot_query/SKILL.md +27 -26
  47. data/etc/default_skills/pwn/ai/agent/tools/recon_run/SKILL.md +25 -15
  48. data/etc/default_skills/pwn/ai/agent/tools/workspace_tool/SKILL.md +24 -0
  49. data/etc/default_skills/pwn/ai/server_tools/SKILL.md +25 -0
  50. data/etc/default_skills/pwn/backups/SKILL.md +54 -0
  51. data/etc/default_skills/pwn/plugins/recon/SKILL.md +12 -1
  52. data/etc/default_skills/pwn/plugins/yubikey/SKILL.md +157 -0
  53. data/etc/default_skills/pwn/plugins/yubikey/fido/SKILL.md +189 -0
  54. data/etc/default_skills/pwn/plugins/yubikey/hsmauth/SKILL.md +83 -0
  55. data/etc/default_skills/pwn/plugins/yubikey/management/SKILL.md +80 -0
  56. data/etc/default_skills/pwn/plugins/yubikey/oath/SKILL.md +115 -0
  57. data/etc/default_skills/pwn/plugins/yubikey/openpgp/SKILL.md +144 -0
  58. data/etc/default_skills/pwn/plugins/yubikey/otp/SKILL.md +97 -0
  59. data/etc/default_skills/pwn/plugins/yubikey/piv/SKILL.md +110 -0
  60. data/etc/default_skills/pwn/plugins/yubikey/protocol/SKILL.md +21 -0
  61. data/etc/default_skills/pwn/plugins/yubikey/security_domain/SKILL.md +75 -0
  62. data/etc/default_skills/pwn/plugins/yubikey/security_domain/scp/SKILL.md +46 -0
  63. data/etc/default_skills/pwn/plugins/yubikey/transport/SKILL.md +98 -0
  64. data/etc/default_skills/pwn/redaction/SKILL.md +13 -1
  65. data/lib/pwn/ai/agent/dispatch.rb +26 -15
  66. data/lib/pwn/ai/agent/learning.rb +14 -18
  67. data/lib/pwn/ai/agent/loop.rb +17 -1
  68. data/lib/pwn/ai/agent/mission_namespace.rb +331 -0
  69. data/lib/pwn/ai/agent/policy_evaluation.rb +334 -36
  70. data/lib/pwn/ai/agent/prompt_builder.rb +13 -2
  71. data/lib/pwn/ai/agent/request_runtime.rb +122 -18
  72. data/lib/pwn/ai/agent/reward.rb +5 -3
  73. data/lib/pwn/ai/agent/skill_consolidation.rb +3 -2
  74. data/lib/pwn/ai/agent/skill_review.rb +9 -5
  75. data/lib/pwn/ai/agent/swarm.rb +4 -0
  76. data/lib/pwn/ai/agent/tools/browser_act.rb +32 -0
  77. data/lib/pwn/ai/agent/tools/loot_query.rb +11 -1
  78. data/lib/pwn/ai/agent/tools/recon_run.rb +11 -1
  79. data/lib/pwn/ai/agent/tools/workspace_tool.rb +29 -0
  80. data/lib/pwn/ai/agent/turn_finalizer.rb +6 -2
  81. data/lib/pwn/ai/agent.rb +1 -0
  82. data/lib/pwn/ai/anthropic.rb +8 -33
  83. data/lib/pwn/ai/cli.rb +34 -5
  84. data/lib/pwn/ai/gemini.rb +1 -0
  85. data/lib/pwn/ai/grok.rb +39 -28
  86. data/lib/pwn/ai/ollama.rb +4 -2
  87. data/lib/pwn/ai/open_ai.rb +11 -32
  88. data/lib/pwn/ai/open_web_ui.rb +62 -4
  89. data/lib/pwn/ai/router.rb +8 -1
  90. data/lib/pwn/ai/server_tools.rb +231 -0
  91. data/lib/pwn/ai.rb +1 -0
  92. data/lib/pwn/backups.rb +349 -0
  93. data/lib/pwn/config.rb +70 -39
  94. data/lib/pwn/migrate.rb +85 -280
  95. data/lib/pwn/module_skills.rb +7 -0
  96. data/lib/pwn/plugins/ai_sandbox.rb +538 -1
  97. data/lib/pwn/plugins/authentication_helper.rb +74 -4
  98. data/lib/pwn/plugins/basic_auth.rb +2 -1
  99. data/lib/pwn/plugins/google_workspace.rb +4 -21
  100. data/lib/pwn/plugins/jobs.rb +1 -1
  101. data/lib/pwn/plugins/log.rb +3 -1
  102. data/lib/pwn/plugins/preflight_checker.rb +34 -9
  103. data/lib/pwn/plugins/recon.rb +4 -1
  104. data/lib/pwn/plugins/repl/ai.rb +293 -26
  105. data/lib/pwn/plugins/repl/ai_console.rb +268 -88
  106. data/lib/pwn/plugins/repl/ai_swarm.rb +177 -58
  107. data/lib/pwn/plugins/repl/mesh.rb +14 -19
  108. data/lib/pwn/plugins/repl/vault.rb +4 -15
  109. data/lib/pwn/plugins/repl.rb +9 -216
  110. data/lib/pwn/plugins/sandbox/driver.rb +4 -0
  111. data/lib/pwn/plugins/transparent_browser.rb +215 -0
  112. data/lib/pwn/plugins/vault.rb +412 -135
  113. data/lib/pwn/plugins/yubikey/fido.rb +1288 -0
  114. data/lib/pwn/plugins/yubikey/hsmauth.rb +439 -0
  115. data/lib/pwn/plugins/yubikey/management.rb +686 -0
  116. data/lib/pwn/plugins/yubikey/oath.rb +766 -0
  117. data/lib/pwn/plugins/yubikey/openpgp.rb +1063 -0
  118. data/lib/pwn/plugins/yubikey/otp.rb +897 -0
  119. data/lib/pwn/plugins/yubikey/piv.rb +1315 -0
  120. data/lib/pwn/plugins/yubikey/protocol.rb +341 -0
  121. data/lib/pwn/plugins/yubikey/security_domain/scp.rb +482 -0
  122. data/lib/pwn/plugins/yubikey/security_domain.rb +464 -0
  123. data/lib/pwn/plugins/yubikey/transport.rb +1017 -0
  124. data/lib/pwn/plugins/yubikey.rb +844 -0
  125. data/lib/pwn/plugins.rb +1 -0
  126. data/lib/pwn/redaction.rb +69 -12
  127. data/lib/pwn/session_trace.rb +4 -0
  128. data/lib/pwn/setup.rb +114 -24
  129. data/lib/pwn/version.rb +1 -1
  130. data/lib/pwn/www/app_cobalt_io.rb +16 -5
  131. data/lib/pwn/www/bug_crowd.rb +16 -5
  132. data/lib/pwn/www/coinbase_pro.rb +16 -5
  133. data/lib/pwn/www/github.rb +30 -14
  134. data/lib/pwn/www/paypal.rb +15 -4
  135. data/lib/pwn/www/synack.rb +16 -5
  136. data/lib/pwn/www/twitter.rb +17 -6
  137. data/lib/pwn.rb +1 -0
  138. data/spec/documentation/installation_md_spec.rb +1 -1
  139. data/spec/fixtures/yubikey/apdu_frames.json +162 -0
  140. data/spec/integration/authentication_helper_oath_spec.rb +117 -0
  141. data/spec/integration/controller_spill_spec.rb +48 -0
  142. data/spec/integration/harness_process_spec.rb +97 -0
  143. data/spec/integration/mission_cancel_spec.rb +79 -0
  144. data/spec/integration/mission_curses_pty_spec.rb +44 -0
  145. data/spec/integration/readme_security_spec.rb +47 -0
  146. data/spec/integration/web_cache_deception_yubikey_spec.rb +159 -0
  147. data/spec/lib/pwn/ai/agent/dispatch_spec.rb +6 -3
  148. data/spec/lib/pwn/ai/agent/dispatch_vault_safety_spec.rb +86 -0
  149. data/spec/lib/pwn/ai/agent/injection_guard_spec.rb +2 -0
  150. data/spec/lib/pwn/ai/agent/learning_spec.rb +4 -4
  151. data/spec/lib/pwn/ai/agent/loop_artifacts_spec.rb +5 -0
  152. data/spec/lib/pwn/ai/agent/loop_spec.rb +104 -29
  153. data/spec/lib/pwn/ai/agent/mission_namespace_spec.rb +138 -0
  154. data/spec/lib/pwn/ai/agent/policy_evaluation_spec.rb +10 -23
  155. data/spec/lib/pwn/ai/agent/request_runtime_spec.rb +48 -0
  156. data/spec/lib/pwn/ai/agent/shell_search_effect_spec.rb +44 -0
  157. data/spec/lib/pwn/ai/agent/solve_spec.rb +6 -4
  158. data/spec/lib/pwn/ai/agent/tools/browser_act_spec.rb +44 -0
  159. data/spec/lib/pwn/ai/agent/tools/loot_query_yubikey_spec.rb +55 -0
  160. data/spec/lib/pwn/ai/agent/tools/recon_run_yubikey_spec.rb +43 -0
  161. data/spec/lib/pwn/ai/agent/tools/workspace_tool_spec.rb +20 -0
  162. data/spec/lib/pwn/ai/agent/turn_finalizer_spec.rb +7 -0
  163. data/spec/lib/pwn/ai/anthropic_vault_enrollment_spec.rb +37 -0
  164. data/spec/lib/pwn/ai/cli_spec.rb +8 -16
  165. data/spec/lib/pwn/ai/oauth_vault_persistence_spec.rb +195 -0
  166. data/spec/lib/pwn/ai/open_ai_spec.rb +2 -2
  167. data/spec/lib/pwn/ai/open_web_ui_workspace_spec.rb +36 -0
  168. data/spec/lib/pwn/ai/router_spec.rb +1 -1
  169. data/spec/lib/pwn/ai/server_tools_spec.rb +39 -0
  170. data/spec/lib/pwn/backups_spec.rb +129 -0
  171. data/spec/lib/pwn/config_redaction_yubikey_spec.rb +28 -0
  172. data/spec/lib/pwn/config_yubikey_spec.rb +92 -0
  173. data/spec/lib/pwn/migrate_spec.rb +29 -81
  174. data/spec/lib/pwn/migrate_yubikey_spec.rb +128 -0
  175. data/spec/lib/pwn/module_skills_spec.rb +8 -0
  176. data/spec/lib/pwn/plugins/ai_sandbox_spec.rb +230 -0
  177. data/spec/lib/pwn/plugins/authentication_helper_native_yubikey_spec.rb +40 -0
  178. data/spec/lib/pwn/plugins/authentication_helper_yubikey_spec.rb +90 -0
  179. data/spec/lib/pwn/plugins/capability_broker/daemon_spec.rb +1 -1
  180. data/spec/lib/pwn/plugins/google_workspace_spec.rb +17 -35
  181. data/spec/lib/pwn/plugins/log_yubikey_spec.rb +47 -0
  182. data/spec/lib/pwn/plugins/preflight_checker_native_yubikey_spec.rb +19 -0
  183. data/spec/lib/pwn/plugins/preflight_checker_yubikey_spec.rb +55 -0
  184. data/spec/lib/pwn/plugins/recon_yubikey_spec.rb +43 -0
  185. data/spec/lib/pwn/plugins/repl/ai_console_spec.rb +110 -75
  186. data/spec/lib/pwn/plugins/repl/ai_legacy_containment_spec.rb +200 -0
  187. data/spec/lib/pwn/plugins/repl/ai_swarm_spec.rb +19 -16
  188. data/spec/lib/pwn/plugins/repl/ai_vault_edit_yubikey_spec.rb +103 -0
  189. data/spec/lib/pwn/plugins/repl/ai_yubikey_spec.rb +48 -0
  190. data/spec/lib/pwn/plugins/repl/mesh_vault_persistence_spec.rb +149 -0
  191. data/spec/lib/pwn/plugins/repl/vault_yaml_serialization_spec.rb +11 -0
  192. data/spec/lib/pwn/plugins/repl/vault_yubikey_spec.rb +39 -0
  193. data/spec/lib/pwn/plugins/repl_mesh_layout_spec.rb +2 -0
  194. data/spec/lib/pwn/plugins/repl_spec.rb +56 -5
  195. data/spec/lib/pwn/plugins/transparent_browser_jev_spec.rb +46 -0
  196. data/spec/lib/pwn/plugins/vault_native_yubikey_spec.rb +29 -0
  197. data/spec/lib/pwn/plugins/vault_publication_safety_spec.rb +258 -0
  198. data/spec/lib/pwn/plugins/vault_store_safety_spec.rb +241 -0
  199. data/spec/lib/pwn/plugins/vault_yubikey_safety_spec.rb +168 -0
  200. data/spec/lib/pwn/plugins/vault_yubikey_spec.rb +128 -0
  201. data/spec/lib/pwn/plugins/vault_yubikey_vector_spec.rb +45 -0
  202. data/spec/lib/pwn/plugins/yubikey/fido_spec.rb +854 -0
  203. data/spec/lib/pwn/plugins/yubikey/hsmauth_spec.rb +235 -0
  204. data/spec/lib/pwn/plugins/yubikey/management_spec.rb +277 -0
  205. data/spec/lib/pwn/plugins/yubikey/oath_spec.rb +349 -0
  206. data/spec/lib/pwn/plugins/yubikey/openpgp_spec.rb +810 -0
  207. data/spec/lib/pwn/plugins/yubikey/otp_spec.rb +337 -0
  208. data/spec/lib/pwn/plugins/yubikey/piv_spec.rb +646 -0
  209. data/spec/lib/pwn/plugins/yubikey/protocol_secure_spec.rb +28 -0
  210. data/spec/lib/pwn/plugins/yubikey/protocol_spec.rb +149 -0
  211. data/spec/lib/pwn/plugins/yubikey/protocol_vectors_spec.rb +18 -0
  212. data/spec/lib/pwn/plugins/yubikey/security_domain/scp_spec.rb +256 -0
  213. data/spec/lib/pwn/plugins/yubikey/security_domain/support.rb +153 -0
  214. data/spec/lib/pwn/plugins/yubikey/security_domain_spec.rb +296 -0
  215. data/spec/lib/pwn/plugins/yubikey/transport_spec.rb +760 -0
  216. data/spec/lib/pwn/plugins/yubikey_security_spec.rb +260 -0
  217. data/spec/lib/pwn/plugins/yubikey_spec.rb +209 -0
  218. data/spec/lib/pwn/plugins/yubikey_terminal_spec.rb +147 -0
  219. data/spec/lib/pwn/redaction_yubikey_spec.rb +61 -0
  220. data/spec/lib/pwn/session_cleanup_default_spec.rb +7 -5
  221. data/spec/lib/pwn/session_cleanup_payload_spec.rb +11 -11
  222. data/spec/lib/pwn/session_cleanup_recovery_spec.rb +34 -20
  223. data/spec/lib/pwn/setup_native_yubikey_spec.rb +66 -0
  224. data/spec/lib/pwn/setup_vault_yubikey_spec.rb +61 -0
  225. data/spec/lib/pwn/setup_yubikey_spec.rb +40 -0
  226. data/spec/lib/pwn/www/login_yubikey_spec.rb +273 -0
  227. data/spec/spec_helper.rb +15 -0
  228. data/spec/support/harness/cli_faults.rb +13 -9
  229. data/spec/support/harness/controller_spill.rb +26 -0
  230. data/spec/support/harness/controller_state.rb +54 -0
  231. data/spec/support/harness/evaluate.rb +12 -1
  232. data/spec/support/harness/experiments.rb +1 -1
  233. data/spec/support/harness/fake_provider.rb +22 -6
  234. data/spec/support/harness/fault_preload.rb +3 -1
  235. data/spec/support/harness/mission_console.rb +54 -0
  236. data/spec/support/harness/mission_curses_pty.rb +41 -0
  237. data/spec/support/harness/mission_dispatch.rb +30 -0
  238. data/spec/support/harness/mission_fixture_preload.rb +24 -0
  239. data/spec/support/harness/mission_job_lifetime.rb +25 -0
  240. data/spec/support/harness/mission_legacy.rb +25 -0
  241. data/spec/support/harness/mission_normal_exit.rb +34 -0
  242. data/spec/support/harness/mission_swarm.rb +23 -0
  243. data/spec/support/harness/mission_unsupported.rb +29 -0
  244. data/spec/support/harness/package_acceptance.rb +33 -5
  245. data/spec/support/harness/session_acceptance.rb +4 -4
  246. data/spec/support/harness/swarm_acceptance.rb +1 -1
  247. data/spec/support/harness/swarm_control.rb +44 -0
  248. data/spec/support/mission_namespace.rb +14 -0
  249. data/spec/support/mission_pipe_queue.rb +33 -0
  250. data/third_party/pwn_rdoc.jsonl +605 -19
  251. metadata +129 -16
  252. data/scripts/benchmark_policy.rb +0 -376
  253. data/scripts/build_gate.rb +0 -58
  254. data/spec/integration/build_gate_spec.rb +0 -73
@@ -5,6 +5,8 @@ require 'yaml'
5
5
  module PWN
6
6
  module WWW
7
7
  # This plugin supports github.com actions.
8
+ # YubiKey OATH MFA requires explicit confirm: true because HOTP advances counters
9
+ # and protected OATH accounts may require authentication; no automatic repair or deletion occurs.
8
10
  module GitHub
9
11
  # Supported Method Parameters::
10
12
  # browser_obj = PWN::WWW::GitHub.open(
@@ -29,10 +31,12 @@ module PWN
29
31
  # username: 'required - username',
30
32
  # password: 'optional - passwd (will prompt if blank)',
31
33
  # mfa: 'optional - MFA / TOTP token string, or true to prompt (defaults to false)',
32
- # mfa_token: 'optional - MFA / TOTP token used to reach a post-authenticated state'
34
+ # mfa_token: 'optional - MFA / TOTP token used to reach a post-authenticated state',
35
+ # yubikey: 'optional - Hash selecting an exact OATH account (required :account and literal :confirm true); optional :serial exact decimal String, :reader or :device_path nonempty control-free String, :connection native exchange object, :timeout finite positive seconds (default 30), in-memory :password or callable :secret_provider; serial/reader and reader/device_path are mutually exclusive; no OTP slot; explicit mfa/mfa_token values conflict'
33
36
  # )
34
37
 
35
38
  public_class_method def self.login(opts = {})
39
+ PWN::Plugins::AuthenticationHelper.validate_mfa_options(opts)
36
40
  browser_obj = opts[:browser_obj]
37
41
  username = opts[:username].to_s.scrub.strip.chomp
38
42
  password = opts[:password]
@@ -45,6 +49,7 @@ module PWN
45
49
  password = opts[:password].to_s.scrub.strip.chomp
46
50
  end
47
51
  mfa = opts[:mfa]
52
+ yubikey = opts[:yubikey]
48
53
  mfa_token = opts[:mfa_token].to_s.scrub.strip.chomp
49
54
  mfa_token = mfa.to_s.scrub.strip.chomp if mfa_token.empty? && mfa.is_a?(String)
50
55
 
@@ -54,7 +59,7 @@ module PWN
54
59
  browser.text_field(name: 'password').wait_until(&:present?).set(password)
55
60
  browser.input(name: 'commit').click!
56
61
 
57
- if mfa || !mfa_token.empty?
62
+ if mfa || !mfa_token.empty? || !yubikey.nil?
58
63
  until browser.url == 'https://github.com/' ||
59
64
  browser.url.include?('https://github.com/dashboard') ||
60
65
  (
@@ -63,23 +68,28 @@ module PWN
63
68
  !browser.url.include?('/session')
64
69
  )
65
70
 
71
+ mfa_field = if browser.text_field(name: 'app_otp').exist?
72
+ browser.text_field(name: 'app_otp')
73
+ elsif browser.text_field(id: 'app_totp').exist?
74
+ browser.text_field(id: 'app_totp')
75
+ elsif browser.text_field(name: 'otp').exist?
76
+ browser.text_field(name: 'otp')
77
+ else
78
+ raise ArgumentError, 'YubiKey OATH requires an authenticator challenge, not SMS' unless yubikey.nil?
79
+
80
+ browser.text_field(name: 'sms_otp')
81
+ end
82
+ mfa_field.wait_until(&:present?)
83
+
66
84
  token = if mfa_token.empty?
67
- PWN::Plugins::AuthenticationHelper.mfa(prompt: 'enter mfa token')
85
+ PWN::Plugins::AuthenticationHelper.mfa(prompt: 'enter mfa token', yubikey: yubikey)
68
86
  else
69
87
  consumed = mfa_token
70
88
  mfa_token = ''
71
89
  consumed
72
90
  end
73
91
 
74
- if browser.text_field(name: 'app_otp').exist?
75
- browser.text_field(name: 'app_otp').wait_until(&:present?).set(token)
76
- elsif browser.text_field(id: 'app_totp').exist?
77
- browser.text_field(id: 'app_totp').wait_until(&:present?).set(token)
78
- elsif browser.text_field(name: 'otp').exist?
79
- browser.text_field(name: 'otp').wait_until(&:present?).set(token)
80
- else
81
- browser.text_field(name: 'sms_otp').wait_until(&:present?).set(token)
82
- end
92
+ mfa_field.set(token)
83
93
 
84
94
  if browser.input(name: 'commit').exist?
85
95
  browser.input(name: 'commit').click!
@@ -151,13 +161,19 @@ module PWN
151
161
  proxy: 'optional - scheme://proxy_host:port || tor'
152
162
  )
153
163
 
154
- # Run login and return its result
164
+ # Log in with manual MFA or explicitly selected YubiKey OATH.
165
+ # YubiKey enables MFA even when mfa is false; failures never fall back to a prompt.
166
+ # Configure the service for an authenticator challenge, not SMS.
167
+ # Example yubikey: { account: 'EXACT_OATH_NAME', confirm: true, serial: '123456', timeout: 30 }
168
+ # Explicit consent is required: HOTP advances counters and protected OATH accounts may require authentication; no automatic repair or deletion occurs.
169
+ # Reject conflicting explicit tokens before any login action.
155
170
  #{self}.login(
156
171
  browser_obj: 'required - browser_obj returned from #open method',
157
172
  username: 'required - username value consumed by #login',
158
173
  password: 'optional - passwd (will prompt if blank)',
159
174
  mfa: 'optional - MFA / TOTP token string, or true to prompt (defaults to false)',
160
- mfa_token: 'optional - MFA / TOTP token used to reach a post-authenticated state'
175
+ mfa_token: 'optional - MFA / TOTP token used to reach a post-authenticated state',
176
+ yubikey: 'optional - Hash selecting an exact OATH account (required :account and literal :confirm true); optional :serial exact decimal String, :reader or :device_path nonempty control-free String, :connection native exchange object, :timeout finite positive seconds (default 30), in-memory :password or callable :secret_provider; serial/reader and reader/device_path are mutually exclusive; no OTP slot; explicit mfa/mfa_token values conflict'
161
177
  )
162
178
 
163
179
  # Run logout and return its result
@@ -5,6 +5,9 @@ require 'yaml'
5
5
  module PWN
6
6
  module WWW
7
7
  # This plugin supports paypal.com actions.
8
+ # Its legacy login adapter selects SMS MFA; YubiKey OATH is unsupported even
9
+ # with confirm: true; it is rejected before login rather than submitting
10
+ # an authenticator code as SMS.
8
11
  module Paypal
9
12
  # Supported Method Parameters::
10
13
  # browser_obj = PWN::WWW::Paypal.open(
@@ -86,10 +89,14 @@ module PWN
86
89
  # browser_obj: 'required - browser_obj returned from #open method',
87
90
  # username: 'required - username (email address)',
88
91
  # password: 'optional - passwd (will prompt if blank)',
89
- # mfa: 'optional - if true prompt for mfa token (defaults to false)'
92
+ # mfa: 'optional - if true prompt for mfa token (defaults to false)',
93
+ # yubikey: 'optional - rejected: exact OATH account Hash with :serial, :reader, :device_path, :connection, :timeout is unsupported even with confirm: true; this adapter is SMS-only; explicit mfa/mfa_token values conflict; omit to use manual SMS'
90
94
  # )
91
95
 
92
96
  public_class_method def self.login(opts = {})
97
+ PWN::Plugins::AuthenticationHelper.validate_mfa_options(opts)
98
+ raise ArgumentError, 'PayPal YubiKey OATH login is unsupported by this SMS-only browser adapter' unless opts[:yubikey].nil?
99
+
93
100
  browser_obj = opts[:browser_obj]
94
101
  username = opts[:username].to_s.scrub.strip.chomp
95
102
  password = opts[:password]
@@ -110,7 +117,7 @@ module PWN
110
117
  browser.button(id: 'btnLogin').click!
111
118
 
112
119
  if mfa
113
- # Send code to SMS
120
+ # Send code to SMS; this legacy adapter does not select an OATH authenticator.
114
121
  browser.button(id: 'btnSelectSoftToken').wait_until(&:present?).click!
115
122
  until browser.url == 'https://www.paypal.com/myaccount/home'
116
123
  browser.text_field(id: 'security-code').wait_until(&:present?).set(PWN::Plugins::AuthenticationHelper.mfa(prompt: 'enter mfa token'))
@@ -188,12 +195,16 @@ module PWN
188
195
  mfa: 'optional - mfa value consumed by #signup'
189
196
  )
190
197
 
191
- # Run login and return its result
198
+ # Log in using the existing manual SMS challenge.
199
+ # YubiKey OATH is unsupported even with confirm: true; this SMS-only browser adapter rejects it before login.
200
+ # Service-level authenticator availability does not establish a compatible adapter flow.
201
+ # Validate options and reject conflicting explicit tokens before any login action.
192
202
  #{self}.login(
193
203
  browser_obj: 'required - browser_obj returned from #open method',
194
204
  username: 'required - username (email address)',
195
205
  password: 'optional - passwd (will prompt if blank)',
196
- mfa: 'optional - if true prompt for mfa token (defaults to false)'
206
+ mfa: 'optional - if true prompt for mfa token (defaults to false)',
207
+ yubikey: 'optional - rejected: exact OATH account Hash with :serial, :reader, :device_path, :connection, :timeout is unsupported even with confirm: true; this adapter is SMS-only; explicit mfa/mfa_token values conflict; omit to use manual SMS'
197
208
  )
198
209
 
199
210
  # Run logout and return its result
@@ -5,6 +5,8 @@ require 'yaml'
5
5
  module PWN
6
6
  module WWW
7
7
  # This plugin supports platform.synack.com actions.
8
+ # YubiKey OATH MFA requires explicit confirm: true because HOTP advances counters
9
+ # and protected OATH accounts may require authentication; no automatic repair or deletion occurs.
8
10
  module Synack
9
11
  # Supported Method Parameters::
10
12
  # browser_obj = PWN::WWW::Synack.open(
@@ -28,10 +30,13 @@ module PWN
28
30
  # browser_obj: 'required - browser_obj returned from #open method',
29
31
  # username: 'required - username',
30
32
  # password: 'optional - passwd (will prompt if blank)',
31
- # mfa: 'optional - if true prompt for mfa token (defaults to false)'
33
+ # mfa: 'optional - if true prompt for mfa token (defaults to false)',
34
+ # yubikey: 'optional - Hash selecting an exact OATH account (required :account and literal :confirm true); optional :serial exact decimal String, :reader or :device_path nonempty control-free String, :connection native exchange object, :timeout finite positive seconds (default 30), in-memory :password or callable :secret_provider; serial/reader and reader/device_path are mutually exclusive; no OTP slot; explicit mfa/mfa_token values conflict'
32
35
  # )
33
36
 
34
37
  public_class_method def self.login(opts = {})
38
+ PWN::Plugins::AuthenticationHelper.validate_mfa_options(opts)
39
+ yubikey = opts[:yubikey]
35
40
  browser_obj = opts[:browser_obj]
36
41
  username = opts[:username].to_s.scrub.strip.chomp
37
42
  password = opts[:password]
@@ -51,9 +56,9 @@ module PWN
51
56
  browser.text_field(name: 'password').wait_until(&:present?).set(password)
52
57
  browser.button(class: 'btn').click!
53
58
 
54
- if mfa
59
+ if mfa || !yubikey.nil?
55
60
  until browser.url.include?('https://platform.synack.com')
56
- browser.text_field(name: 'authy_token').wait_until(&:present?).set(PWN::Plugins::AuthenticationHelper.mfa(prompt: 'enter mfa token'))
61
+ browser.text_field(name: 'authy_token').wait_until(&:present?).set(PWN::Plugins::AuthenticationHelper.mfa(prompt: 'enter mfa token', yubikey: yubikey))
57
62
  browser.button(class: 'btn').click!
58
63
  sleep 3
59
64
  end
@@ -114,12 +119,18 @@ module PWN
114
119
  proxy: 'optional - scheme://proxy_host:port || tor'
115
120
  )
116
121
 
117
- # Run login and return its result
122
+ # Log in with manual MFA or explicitly selected YubiKey OATH.
123
+ # YubiKey enables MFA even when mfa is false; failures never fall back to a prompt.
124
+ # Configure the service for an authenticator challenge, not SMS.
125
+ # Example yubikey: { account: 'EXACT_OATH_NAME', confirm: true, serial: '123456', timeout: 30 }
126
+ # Explicit consent is required: HOTP advances counters and protected OATH accounts may require authentication; no automatic repair or deletion occurs.
127
+ # Reject conflicting explicit tokens before any login action.
118
128
  #{self}.login(
119
129
  browser_obj: 'required - browser_obj returned from #open method',
120
130
  username: 'required - username value consumed by #login',
121
131
  password: 'optional - passwd (will prompt if blank)',
122
- mfa: 'optional - if true prompt for mfa token (defaults to false)'
132
+ mfa: 'optional - if true prompt for mfa token (defaults to false)',
133
+ yubikey: 'optional - Hash selecting an exact OATH account (required :account and literal :confirm true); optional :serial exact decimal String, :reader or :device_path nonempty control-free String, :connection native exchange object, :timeout finite positive seconds (default 30), in-memory :password or callable :secret_provider; serial/reader and reader/device_path are mutually exclusive; no OTP slot; explicit mfa/mfa_token values conflict'
123
134
  )
124
135
 
125
136
  # Run logout and return its result
@@ -5,6 +5,8 @@ require 'yaml'
5
5
  module PWN
6
6
  module WWW
7
7
  # This plugin supports twitter.com actions.
8
+ # YubiKey OATH MFA requires explicit confirm: true because HOTP advances counters
9
+ # and protected OATH accounts may require authentication; no automatic repair or deletion occurs.
8
10
  module Twitter
9
11
  # Supported Method Parameters::
10
12
  # browser_obj = PWN::WWW::Twitter.open(
@@ -28,10 +30,13 @@ module PWN
28
30
  # browser_obj: 'required - browser_obj returned from #open method',
29
31
  # username: 'required - username',
30
32
  # password: 'optional - passwd (will prompt if blank)',
31
- # mfa: 'optional - if true prompt for mfa token (defaults to false)'
33
+ # mfa: 'optional - if true prompt for mfa token (defaults to false)',
34
+ # yubikey: 'optional - Hash selecting an exact OATH account (required :account and literal :confirm true); optional :serial exact decimal String, :reader or :device_path nonempty control-free String, :connection native exchange object, :timeout finite positive seconds (default 30), in-memory :password or callable :secret_provider; serial/reader and reader/device_path are mutually exclusive; no OTP slot; explicit mfa/mfa_token values conflict'
32
35
  # )
33
36
 
34
37
  public_class_method def self.login(opts = {})
38
+ PWN::Plugins::AuthenticationHelper.validate_mfa_options(opts)
39
+ yubikey = opts[:yubikey]
35
40
  browser_obj = opts[:browser_obj]
36
41
  username = opts[:username].to_s.scrub.strip.chomp
37
42
  password = opts[:password]
@@ -45,17 +50,17 @@ module PWN
45
50
  end
46
51
  mfa = opts[:mfa]
47
52
 
48
- browser_obj.goto('https://twitter.com/login')
53
+ browser.goto('https://twitter.com/login')
49
54
 
50
55
  browser.text_field(index: 2).wait_until(&:present?).set(username)
51
56
  browser.text_field(index: 3).wait_until(&:present?).set(password)
52
57
  browser.button(index: 1).click!
53
58
 
54
- if mfa
59
+ if mfa || !yubikey.nil?
55
60
  until browser.url == 'https://twitter.com/' ||
56
61
  browser.url == 'https://twitter.com/home'
57
62
 
58
- browser.text_field(id: 'challenge_response').wait_until(&:present?).set(PWN::Plugins::AuthenticationHelper.mfa(prompt: 'enter mfa token'))
63
+ browser.text_field(id: 'challenge_response').wait_until(&:present?).set(PWN::Plugins::AuthenticationHelper.mfa(prompt: 'enter mfa token', yubikey: yubikey))
59
64
  browser.button(id: 'email_challenge_submit').click!
60
65
  sleep 3
61
66
  end
@@ -116,12 +121,18 @@ module PWN
116
121
  proxy: 'optional - scheme://proxy_host:port || tor'
117
122
  )
118
123
 
119
- # Run login and return its result
124
+ # Log in with manual MFA or explicitly selected YubiKey OATH.
125
+ # YubiKey enables MFA even when mfa is false; failures never fall back to a prompt.
126
+ # Configure the service for an authenticator challenge, not SMS.
127
+ # Example yubikey: { account: 'EXACT_OATH_NAME', confirm: true, serial: '123456', timeout: 30 }
128
+ # Explicit consent is required: HOTP advances counters and protected OATH accounts may require authentication; no automatic repair or deletion occurs.
129
+ # Reject conflicting explicit tokens before any login action.
120
130
  #{self}.login(
121
131
  browser_obj: 'required - browser_obj returned from #open method',
122
132
  username: 'required - username value consumed by #login',
123
133
  password: 'optional - passwd (will prompt if blank)',
124
- mfa: 'optional - if true prompt for mfa token (defaults to false)'
134
+ mfa: 'optional - if true prompt for mfa token (defaults to false)',
135
+ yubikey: 'optional - Hash selecting an exact OATH account (required :account and literal :confirm true); optional :serial exact decimal String, :reader or :device_path nonempty control-free String, :connection native exchange object, :timeout finite positive seconds (default 30), in-memory :password or callable :secret_provider; serial/reader and reader/device_path are mutually exclusive; no OTP slot; explicit mfa/mfa_token values conflict'
125
136
  )
126
137
 
127
138
  # Run logout and return its result
data/lib/pwn.rb CHANGED
@@ -24,6 +24,7 @@ module PWN
24
24
  autoload :Memory, 'pwn/memory'
25
25
  autoload :MemoryIndex, 'pwn/memory_index'
26
26
  autoload :Migrate, 'pwn/migrate'
27
+ autoload :Backups, 'pwn/backups'
27
28
  autoload :ModuleSkills, 'pwn/module_skills'
28
29
  autoload :Sessions, 'pwn/sessions'
29
30
  autoload :SessionTrace, 'pwn/session_trace'
@@ -66,7 +66,7 @@ RSpec.describe 'documentation/Installation.md' do
66
66
  it 'programmatic API section is truthful' do
67
67
  expect(PWN::Setup).to respond_to(:check, :deps, :list_profiles, :pkg_manager, :migrate)
68
68
  expect(PWN::Setup.check(io: StringIO.new).keys)
69
- .to eq(%i[ok native_gems_missing toolchain_missing state pkg_manager os arch])
69
+ .to eq(%i[ok native_gems_missing toolchain_missing state pkg_manager os arch native_libraries_missing])
70
70
  expect(PWN::Setup.pkg_manager.keys).to eq(%i[key install sudo])
71
71
  expect(PWN::Setup.constants).to include(:NATIVE_GEMS, :TOOLCHAIN, :PROFILES)
72
72
  end
@@ -0,0 +1,162 @@
1
+ {
2
+ "reference": "Yubico yubikey-manager 5.9.1 smartcard formatters; generated offline with synthetic bytes, not a hardware capture",
3
+ "source": "https://github.com/Yubico/yubikey-manager/blob/5.9.1/yubikit/core/smartcard/__init__.py",
4
+ "vectors": [
5
+ {
6
+ "name": "short-0-0",
7
+ "options": {
8
+ "cla": 0,
9
+ "ins": 202,
10
+ "p1": 1,
11
+ "p2": 2,
12
+ "data_hex": "",
13
+ "le": null,
14
+ "extended": false
15
+ },
16
+ "expected_hex": "00ca010200"
17
+ },
18
+ {
19
+ "name": "short-0-1",
20
+ "options": {
21
+ "cla": 0,
22
+ "ins": 202,
23
+ "p1": 1,
24
+ "p2": 2,
25
+ "data_hex": "",
26
+ "le": 1,
27
+ "extended": false
28
+ },
29
+ "expected_hex": "00ca010201"
30
+ },
31
+ {
32
+ "name": "short-1-0",
33
+ "options": {
34
+ "cla": 0,
35
+ "ins": 202,
36
+ "p1": 1,
37
+ "p2": 2,
38
+ "data_hex": "00",
39
+ "le": null,
40
+ "extended": false
41
+ },
42
+ "expected_hex": "00ca01020100"
43
+ },
44
+ {
45
+ "name": "short-16-16",
46
+ "options": {
47
+ "cla": 0,
48
+ "ins": 202,
49
+ "p1": 1,
50
+ "p2": 2,
51
+ "data_hex": "000102030405060708090a0b0c0d0e0f",
52
+ "le": 16,
53
+ "extended": false
54
+ },
55
+ "expected_hex": "00ca010210000102030405060708090a0b0c0d0e0f10"
56
+ },
57
+ {
58
+ "name": "short-255-0",
59
+ "options": {
60
+ "cla": 0,
61
+ "ins": 202,
62
+ "p1": 1,
63
+ "p2": 2,
64
+ "data_hex": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfe",
65
+ "le": null,
66
+ "extended": false
67
+ },
68
+ "expected_hex": "00ca0102ff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfe"
69
+ },
70
+ {
71
+ "name": "short-255-255",
72
+ "options": {
73
+ "cla": 0,
74
+ "ins": 202,
75
+ "p1": 1,
76
+ "p2": 2,
77
+ "data_hex": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfe",
78
+ "le": 255,
79
+ "extended": false
80
+ },
81
+ "expected_hex": "00ca0102ff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff"
82
+ },
83
+ {
84
+ "name": "extended-0-0",
85
+ "options": {
86
+ "cla": 0,
87
+ "ins": 202,
88
+ "p1": 1,
89
+ "p2": 2,
90
+ "data_hex": "",
91
+ "le": null,
92
+ "extended": true
93
+ },
94
+ "expected_hex": "00ca0102"
95
+ },
96
+ {
97
+ "name": "extended-0-256",
98
+ "options": {
99
+ "cla": 0,
100
+ "ins": 202,
101
+ "p1": 1,
102
+ "p2": 2,
103
+ "data_hex": "",
104
+ "le": 256,
105
+ "extended": true
106
+ },
107
+ "expected_hex": "00ca0102000100"
108
+ },
109
+ {
110
+ "name": "extended-1-0",
111
+ "options": {
112
+ "cla": 0,
113
+ "ins": 202,
114
+ "p1": 1,
115
+ "p2": 2,
116
+ "data_hex": "00",
117
+ "le": null,
118
+ "extended": true
119
+ },
120
+ "expected_hex": "00ca010200000100"
121
+ },
122
+ {
123
+ "name": "extended-255-256",
124
+ "options": {
125
+ "cla": 0,
126
+ "ins": 202,
127
+ "p1": 1,
128
+ "p2": 2,
129
+ "data_hex": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfe",
130
+ "le": 256,
131
+ "extended": true
132
+ },
133
+ "expected_hex": "00ca01020000ff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfe0100"
134
+ },
135
+ {
136
+ "name": "extended-256-0",
137
+ "options": {
138
+ "cla": 0,
139
+ "ins": 202,
140
+ "p1": 1,
141
+ "p2": 2,
142
+ "data_hex": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff",
143
+ "le": null,
144
+ "extended": true
145
+ },
146
+ "expected_hex": "00ca0102000100000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff"
147
+ },
148
+ {
149
+ "name": "extended-512-1024",
150
+ "options": {
151
+ "cla": 0,
152
+ "ins": 202,
153
+ "p1": 1,
154
+ "p2": 2,
155
+ "data_hex": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff",
156
+ "le": 1024,
157
+ "extended": true
158
+ },
159
+ "expected_hex": "00ca0102000200000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff0400"
160
+ }
161
+ ]
162
+ }
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'tty-prompt'
5
+
6
+ RSpec.describe 'AuthenticationHelper native OATH integration' do
7
+ let(:account) { '-例:tester@example.invalid .+ ' }
8
+ let(:serial) { '00123456' }
9
+ let(:connection) { double('offline APDU connection', descriptor: { protocol: :apdu }, close: :closed) }
10
+ let(:yubikey) { { account: account, confirm: true, serial: serial, connection: connection, timeout: 5 }.freeze }
11
+ let(:select_apdu) { ['00a4040007a0000005272101'].pack('H*') }
12
+ let(:list_apdu) { ['00a1000000'].pack('H*') }
13
+ let(:calculate_apdu) do
14
+ body = [0x71, account.bytesize].pack('CC') + account.b + [0x74, 8].pack('CC') + [1].pack('Q>')
15
+ [0, 0xa2, 0, 1, body.bytesize].pack('C*') + body
16
+ end
17
+
18
+ before do
19
+ @ready = false
20
+ @frames = []
21
+ allow(Time).to receive(:now).and_return(Time.at(59))
22
+ allow(TTY::Prompt).to receive(:new).and_raise('unexpected interactive prompt')
23
+ expect(PWN::Plugins::YubiKey::Transport).not_to receive(:open)
24
+ expect(connection).not_to receive(:close)
25
+ allow(connection).to receive(:exchange) do |request|
26
+ expect(@ready).to be(true)
27
+ expect(request[:protocol]).to eq(:apdu)
28
+ @frames << request[:data]
29
+ case request[:data]
30
+ when select_apdu
31
+ [0x79, 3, 5, 7, 1, 0x71, 8].pack('C*') + 'fixture!'.b + "\x90\0".b
32
+ when list_apdu
33
+ [0x72, account.bytesize + 1, 0x21].pack('C*') + account.b + "\x90\0".b
34
+ when calculate_apdu
35
+ [0x76, 5, 8, 12_345].pack('CCCN') + "\x90\0".b
36
+ else
37
+ raise 'Unexpected APDU in offline OATH fixture'
38
+ end
39
+ end
40
+ end
41
+
42
+ it 'runs the real helper, root facade, OATH.code and Protocol against a fake APDU transport' do
43
+ @ready = true
44
+ options = { yubikey: yubikey }.freeze
45
+ expect do
46
+ expect(PWN::Plugins::AuthenticationHelper.mfa(options)).to eq('00012345')
47
+ end.not_to output.to_stdout
48
+ expect(@frames).to eq([select_apdu, list_apdu, calculate_apdu])
49
+ expect(options[:yubikey]).to equal(yubikey)
50
+ end
51
+
52
+ [nil, false, 'true'].each do |confirm|
53
+ it "rejects unconfirmed OATH #{confirm.inspect} before any APDU or prompt" do
54
+ expect do
55
+ PWN::Plugins::AuthenticationHelper.mfa(yubikey: yubikey.merge(confirm: confirm).freeze)
56
+ end.to raise_error(ArgumentError, /confirm: true/)
57
+ expect(@frames).to be_empty
58
+ end
59
+ end
60
+
61
+ it 'propagates exact-account errors without calculation, credential repair or manual fallback' do
62
+ @ready = true
63
+ expect do
64
+ PWN::Plugins::AuthenticationHelper.mfa(yubikey: yubikey.merge(account: 'missing@example.invalid'))
65
+ end.to raise_error(PWN::Plugins::YubiKey::ResponseError, /exact account/)
66
+ expect(@frames).to eq([select_apdu, list_apdu])
67
+ end
68
+
69
+ context 'through the real GitHub login and helper' do
70
+ let(:browser) { double('offline browser') }
71
+ let(:browser_obj) { { browser: browser } }
72
+ let(:credential_field) { double('offline credential field', set: nil, present?: true) }
73
+ let(:otp_field) { double('offline OATH field', present?: true, exist?: true) }
74
+ let(:submit) { double('offline submit', exist?: true) }
75
+ let(:login_opts) { { browser_obj: browser_obj, username: 'tester@example.invalid', password: 'fake-password', mfa: false, yubikey: yubikey } }
76
+
77
+ before do
78
+ @authenticated = false
79
+ @otp_written = false
80
+ allow(PWN::WWW::GitHub).to receive(:sleep)
81
+ allow(browser).to receive(:goto)
82
+ allow(browser).to receive(:url) { @authenticated ? 'https://github.com/' : 'https://github.com/sessions/two-factor' }
83
+ allow(browser).to receive(:text_field).and_return(credential_field)
84
+ allow(credential_field).to receive(:wait_until).and_return(credential_field)
85
+ allow(browser).to receive(:text_field).with(name: 'app_otp').and_return(otp_field)
86
+ allow(otp_field).to receive(:wait_until) do
87
+ expect(@frames).to be_empty
88
+ @ready = true
89
+ otp_field
90
+ end
91
+ allow(browser).to receive(:input).with(name: 'commit').and_return(submit)
92
+ allow(submit).to receive(:click!) { @authenticated = true if @otp_written }
93
+ end
94
+
95
+ it 'types actual native OATH digits only after the authenticator field is ready' do
96
+ expect(otp_field).to receive(:set).with('00012345') { @otp_written = true }
97
+ expect(PWN::WWW::GitHub.login(login_opts)).to equal(browser_obj)
98
+ expect(@frames).to eq([select_apdu, list_apdu, calculate_apdu])
99
+ end
100
+
101
+ it 'does not touch a token for an already authenticated redirect' do
102
+ @authenticated = true
103
+ expect(otp_field).not_to receive(:set)
104
+ expect(PWN::WWW::GitHub.login(login_opts)).to equal(browser_obj)
105
+ expect(@frames).to be_empty
106
+ end
107
+
108
+ it 'rejects absent consent before asking for a password or navigating' do
109
+ expect(PWN::Plugins::AuthenticationHelper).not_to receive(:mask_password)
110
+ expect(browser).not_to receive(:goto)
111
+ expect do
112
+ PWN::WWW::GitHub.login(login_opts.except(:password).merge(yubikey: yubikey.except(:confirm)))
113
+ end.to raise_error(ArgumentError, /confirm: true/)
114
+ expect(@frames).to be_empty
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'open3'
5
+ require 'tmpdir'
6
+ require 'fileutils'
7
+ require 'json'
8
+
9
+ RSpec.describe 'controller spill boundary through actual Dispatch' do
10
+ %w[checkpoint transcript].each do |mode|
11
+ it "does not create source files when a worker redirects the parent #{mode} spill path" do
12
+ Dir.mktmpdir('spill-boundary-') do |root|
13
+ source = File.join(root, 'source')
14
+ home = File.join(root, 'home')
15
+ work = File.join(root, 'work')
16
+ FileUtils.mkdir_p([source, home, work])
17
+ fixture = File.expand_path('../support/harness/controller_spill.rb', __dir__)
18
+ stdout, stderr, status = Open3.capture3({ 'HOME' => home }, RbConfig.ruby, '-I', File.expand_path('../../lib', __dir__), fixture, source, work, mode, chdir: work)
19
+ expect(status.success?).to be(true), stderr
20
+ row = JSON.parse(stdout)
21
+ expect(row['changed']).to be(false), row.inspect
22
+ expect(row['dispatch_success']).to be(false)
23
+ expect(row['dispatch_error']).to include('Permission denied')
24
+ end
25
+ end
26
+ end
27
+
28
+ %w[ordinary aliased].each do |mode|
29
+ it "denies the #{mode} controller-state inventory while preserving workspace output" do
30
+ Dir.mktmpdir('controller-inventory-') do |root|
31
+ source = File.join(root, 'source')
32
+ home = File.join(root, 'home')
33
+ work = File.join(root, 'work')
34
+ FileUtils.mkdir_p([source, home, work])
35
+ fixture = File.expand_path('../support/harness/controller_state.rb', __dir__)
36
+ stdout, stderr, status = Open3.capture3({ 'HOME' => home }, RbConfig.ruby, '-I', File.expand_path('../../lib', __dir__), fixture, source, work, mode, chdir: work)
37
+ expect(status.success?).to be(true), stderr
38
+ row = JSON.parse(stdout)
39
+ expect(row.dig('result', 'success')).to be(true), row.inspect
40
+ attempts = row.dig('result', 'result')
41
+ expect(attempts.length).to eq(29)
42
+ expect(attempts.values.uniq).to eq(['denied']), attempts.inspect
43
+ expect(row['source_unchanged']).to be(true)
44
+ expect(row['output']).to eq('output remains writable')
45
+ end
46
+ end
47
+ end
48
+ end