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
data/lib/pwn/plugins.rb CHANGED
@@ -111,6 +111,7 @@ module PWN
111
111
  autoload :Volatility, 'pwn/plugins/volatility'
112
112
  autoload :Vsphere, 'pwn/plugins/vsphere'
113
113
  autoload :XXD, 'pwn/plugins/xxd'
114
+ autoload :YubiKey, 'pwn/plugins/yubikey'
114
115
  autoload :Zaproxy, 'pwn/plugins/zaproxy'
115
116
 
116
117
  # Display a List of Every PWN::Plugins Module
data/lib/pwn/redaction.rb CHANGED
@@ -4,7 +4,7 @@ require 'digest'
4
4
  require 'json'
5
5
 
6
6
  module PWN
7
- # Write-boundary redaction; reject cyclic or over-100-level containers before inspecting secrets. Never opens a credential store.
7
+ # Write-boundary redaction; redact never opens a credential store. Capture optionally stores ordinary secrets. Hardware/MFA fields use constant masks, never digest markers or loot capture.
8
8
  module Redaction
9
9
  PATTERNS = {
10
10
  pem: /-----BEGIN [A-Z ]*PRIVATE KEY-----.*?(?:-----END [A-Z ]*PRIVATE KEY-----|\z)/m,
@@ -17,6 +17,15 @@ module PWN
17
17
  cookie: /\bSet-Cookie:\s*(?!\[REDACTED:)[^\r\n]+/i
18
18
  }.freeze
19
19
  SECRET_FIELD = /\A(?:password|passwd|secret|api[_-]?key|authorization|proxy-authorization|access[_-]?token|refresh[_-]?token|private[_-]?key|client[_-]?secret)\z/i
20
+ CONSTANT_MASK = '[REDACTED]'
21
+ MASKED_NAME = /(?:
22
+ (?:(?:new|current|old|admin|user|fido2?|piv|openpgp)[_-])?(?:pin|puk) |
23
+ access[_-]?code | management[_-]?(?:key|password) | oath[_-]?secret | static[_-]?password |
24
+ scp(?:03|11)?(?:[_-](?:key|keys|enc|mac|dek)(?:[_-]key)?)? |
25
+ (?:mfa|otp|totp|hotp)(?:[_-](?:code|token|secret))? | (?:recovery|verification)[_-]?code
26
+ )/ix
27
+ MASKED_FIELD = /\A#{MASKED_NAME}\z/
28
+ MASKED_TEXT = /(?<![\w-])((?:--)?["']?#{MASKED_NAME}["']?\s*(?:=>|[:=])\s*)(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\[REDACTED(?::[^\]]+)?\]|[^\s,;}\])]+)/
20
29
 
21
30
  public_class_method def self.redact(opts = {})
22
31
  value = opts[:value]
@@ -46,7 +55,9 @@ module PWN
46
55
  case value
47
56
  when Hash
48
57
  value.to_h do |key, item|
49
- clean = if key.to_s.match?(SECRET_FIELD) && !item.nil?
58
+ clean = if key.to_s.match?(MASKED_FIELD) && !item.nil?
59
+ CONSTANT_MASK
60
+ elsif key.to_s.match?(SECRET_FIELD) && !item.nil?
50
61
  token(kind: key.to_s.downcase, value: item.to_s)
51
62
  else
52
63
  redact_value(value: item)
@@ -55,15 +66,58 @@ module PWN
55
66
  end
56
67
  when Array then value.map { |item| redact_value(value: item) }
57
68
  when String
58
- PATTERNS.reduce(value.dup) do |text, (kind, regex)|
59
- text.split(/(\[REDACTED:[^:\]]+:[0-9a-f]{8}\])/).map do |part|
60
- part.start_with?('[REDACTED:') ? part : part.gsub(regex) { |match| token(kind: kind, value: match) }
69
+ PATTERNS.reduce(mask_fields(text: value)) do |text, (kind, regex)|
70
+ text.split(/(\[REDACTED(?::[^:\]]+:[0-9a-f]{8})?\])/).map do |part|
71
+ part.start_with?('[REDACTED') ? part : part.gsub(regex) { |match| token(kind: kind, value: match) }
61
72
  end.join
62
73
  end
63
74
  else value
64
75
  end
65
76
  end
66
77
 
78
+ private_class_method def self.mask_fields(opts = {})
79
+ text = opts[:text]
80
+ clean = +''
81
+ offset = 0
82
+ while (match = MASKED_TEXT.match(text, offset))
83
+ clean << text[offset...match.begin(0)] << match[1] << CONSTANT_MASK
84
+ offset = masked_value_end(text: text, start: match.end(1), finish: match.end(0))
85
+ end
86
+ clean << text[offset..]
87
+ end
88
+
89
+ # Iterative scan prevents partial masks of serialized arrays/hashes of keys.
90
+ private_class_method def self.masked_value_end(opts = {})
91
+ text = opts[:text]
92
+ start = opts[:start]
93
+ pairs = { '[' => ']', '{' => '}' }
94
+ return opts[:finish] unless pairs.key?(text[start]) || ['"', "'"].include?(text[start])
95
+
96
+ stack = []
97
+ quote = nil
98
+ escaped = false
99
+ text[start..].each_char.with_index(start) do |char, index|
100
+ if quote
101
+ if escaped
102
+ escaped = false
103
+ elsif char == '\\'
104
+ escaped = true
105
+ elsif char == quote
106
+ quote = nil
107
+ return index + 1 if stack.empty?
108
+ end
109
+ elsif ['"', "'"].include?(char)
110
+ quote = char
111
+ elsif pairs.key?(char)
112
+ stack << pairs[char]
113
+ elsif pairs.value?(char)
114
+ return text.length unless stack.pop == char
115
+ return index + 1 if stack.empty?
116
+ end
117
+ end
118
+ text.length
119
+ end
120
+
67
121
  public_class_method def self.token(opts = {})
68
122
  value = opts[:value].to_s
69
123
  return value if value.match?(/\A\[REDACTED:[^:]+:[0-9a-f]{8}\]\z/)
@@ -75,19 +129,21 @@ module PWN
75
129
  value = opts[:value]
76
130
  return { redacted: redact(value: value), refs: [] } unless value.is_a?(String)
77
131
 
78
- text = value.to_s
132
+ text = mask_fields(text: value)
79
133
  refs = []
80
134
  redacted = PATTERNS.reduce(text.dup) do |acc, (kind, regex)|
81
135
  acc.gsub(regex) do |match|
136
+ next match if match.match?(/\A[\w-]+\s*[:=]\s*\[REDACTED\]\z/)
137
+
82
138
  tok = token(kind: kind, value: match)
83
139
  refs << { kind: kind, token: tok }
84
- PWN::Plugins::Vault.store(label: tok, secret: match, engagement: opts[:engagement]) if opts[:vault] != false && defined?(PWN::Plugins::Vault)
140
+ PWN::Plugins::Vault.store(label: tok, secret: match, engagement: opts[:engagement], yubikey: opts[:yubikey]) if opts[:vault] != false && defined?(PWN::Plugins::Vault)
85
141
  tok
86
142
  rescue StandardError
87
143
  tok
88
144
  end
89
145
  end
90
- redacted = capture_entropy(text: redacted, refs: refs, vault: opts[:vault], engagement: opts[:engagement]) if opts[:entropy] == true
146
+ redacted = capture_entropy(text: redacted, refs: refs, vault: opts[:vault], engagement: opts[:engagement], yubikey: opts[:yubikey]) if opts[:entropy] == true
91
147
  { redacted: redacted, refs: refs }
92
148
  end
93
149
 
@@ -100,10 +156,10 @@ module PWN
100
156
 
101
157
  tok = token(kind: 'entropy', value: match)
102
158
  refs << { kind: 'entropy', token: tok }
103
- PWN::Plugins::Vault.store(label: tok, secret: match, engagement: opts[:engagement]) if opts[:vault] != false && defined?(PWN::Plugins::Vault)
159
+ PWN::Plugins::Vault.store(label: tok, secret: match, engagement: opts[:engagement], yubikey: opts[:yubikey]) if opts[:vault] != false && defined?(PWN::Plugins::Vault)
104
160
  tok
105
161
  rescue StandardError
106
- match
162
+ tok || token(kind: 'entropy', value: match)
107
163
  end
108
164
  end
109
165
 
@@ -122,7 +178,7 @@ module PWN
122
178
 
123
179
  public_class_method def self.help
124
180
  puts "USAGE:
125
- # Redact secret patterns before persistence; reject cycles/depth above 100 with JSON::NestingError.
181
+ # Redact before persistence; hardware/MFA fields use constant masks. Reject cycles/depth above 100 with JSON::NestingError.
126
182
  #{self}.redact(
127
183
  value: 'required - string or nested JSON-compatible data'
128
184
  )
@@ -131,10 +187,11 @@ module PWN
131
187
  kind: 'required - secret type label',
132
188
  value: 'required - sensitive value to hash'
133
189
  )
134
- # Capture secrets into Vault labels and return a redacted copy.
190
+ # Capture ordinary secrets into Vault labels; mask hardware/MFA fields without storing them.
135
191
  #{self}.capture(
136
192
  value: 'required - string that may contain credentials',
137
193
  vault: 'optional - false skips Vault.store',
194
+ yubikey: 'optional - selector Hash for hardware-encrypted captured loot; no software fallback on failure',
138
195
  engagement: 'optional - engagement name stored with the vault label',
139
196
  entropy: 'optional - true also captures high-entropy tokens'
140
197
  )
@@ -91,6 +91,10 @@ module PWN
91
91
  timeout = Float(opts.fetch(:timeout, 30))
92
92
  raise ArgumentError, 'Invalid rerun deadline' unless timeout.positive? && timeout <= 300
93
93
 
94
+ # A user-namespace caller that is not uid 0 may still hold capabilities.
95
+ # Bubblewrap rejects those. Uid 0 can already create the nested map;
96
+ # dropping every capability there makes the uid-map setup fail.
97
+ argv.unshift('setpriv', '--ambient-caps=-all', '--inh-caps=-all', '--bounding-set=-all', '--') if PWN::AI::Agent::MissionNamespace.prepared? && !Process.uid.zero?
94
98
  Open3.popen3(*argv, unsetenv_others: true, pgroup: true) do |stdin, stdout, stderr, wait|
95
99
  readers = [Thread.new { stdout.read }, Thread.new { stderr.read }]
96
100
  begin
data/lib/pwn/setup.rb CHANGED
@@ -129,10 +129,44 @@ module PWN
129
129
  }
130
130
  }.freeze
131
131
 
132
+ # Optional runtime libraries, not executables or Ruby build dependencies.
133
+ # macOS provides PC/SC as a system framework. Linux PC/SC also needs a
134
+ # daemon and CCID reader driver for actual I/O; doctor only loads libraries.
135
+ NATIVE_LIBRARIES = {
136
+ 'hidapi' => {
137
+ apt: %w[libhidapi-hidraw0], dnf: %w[hidapi], pacman: %w[hidapi],
138
+ brew: %w[hidapi], port: %w[hidapi],
139
+ libraries: {
140
+ unix: %w[libhidapi-hidraw.so.0 libhidapi-hidraw.so libhidapi-libusb.so.0 libhidapi-libusb.so libhidapi.so],
141
+ macos: %w[libhidapi.dylib /opt/homebrew/lib/libhidapi.dylib /usr/local/lib/libhidapi.dylib /opt/local/lib/libhidapi.dylib],
142
+ windows: %w[hidapi.dll]
143
+ },
144
+ plugins: %w[PWN::Plugins::YubiKey]
145
+ },
146
+ 'pcsc' => {
147
+ apt: %w[libpcsclite1 pcscd libccid], dnf: %w[pcsc-lite-libs pcsc-lite pcsc-lite-ccid],
148
+ pacman: %w[pcsclite ccid], brew: %w[], port: %w[],
149
+ libraries: {
150
+ unix: %w[libpcsclite.so.1 libpcsclite.so],
151
+ macos: %w[/System/Library/Frameworks/PCSC.framework/PCSC],
152
+ windows: %w[winscard.dll]
153
+ },
154
+ plugins: %w[PWN::Plugins::YubiKey]
155
+ }
156
+ }.freeze
157
+
132
158
  # External binaries PWN wraps → OS packages that provide them.
133
159
  # (Extrospection.probe_toolchain already knows how to *detect* these;
134
160
  # this table knows how to *install* them.)
135
161
  TOOLCHAIN = {
162
+ 'setpriv' => {
163
+ apt: %w[util-linux], dnf: %w[util-linux], pacman: %w[util-linux], brew: %w[], port: %w[],
164
+ plugins: %w[PWN::SessionTrace]
165
+ },
166
+ 'unshare' => {
167
+ apt: %w[util-linux], dnf: %w[util-linux], pacman: %w[util-linux], brew: %w[], port: %w[],
168
+ plugins: %w[PWN::AI::Agent::MissionNamespace]
169
+ },
136
170
  'nmap' => {
137
171
  apt: %w[nmap], dnf: %w[nmap], pacman: %w[nmap], brew: %w[nmap], port: %w[nmap],
138
172
  plugins: %w[PWN::Plugins::NmapIt]
@@ -408,7 +442,7 @@ module PWN
408
442
  ai: {
409
443
  desc: 'verify at least one AI engine key/oauth in ~/.pwn/pwn.yaml',
410
444
  gems: %w[],
411
- bins: %w[jq]
445
+ bins: %w[jq unshare setpriv]
412
446
  },
413
447
  web: {
414
448
  desc: 'TransparentBrowser · BurpSuite · Zaproxy · extro_verify · extro_watch',
@@ -446,9 +480,10 @@ module PWN
446
480
  bins: %w[msfconsole sqlmap]
447
481
  },
448
482
  hardware: {
449
- desc: 'Serial · BusPirate · Android · BareSIP · extro_serial · extro_telecomm',
483
+ desc: 'Serial · BusPirate · Android · BareSIP · YubiKey · extro_serial · extro_telecomm',
450
484
  gems: %w[libusb],
451
- bins: %w[adb baresip]
485
+ bins: %w[adb baresip],
486
+ libs: %w[hidapi pcsc]
452
487
  },
453
488
  re: {
454
489
  desc: 'Radare2 · GDB · ExploitDev · Frida · Android RE tier',
@@ -458,7 +493,8 @@ module PWN
458
493
  full: {
459
494
  desc: 'everything above',
460
495
  gems: NATIVE_GEMS.keys,
461
- bins: TOOLCHAIN.keys
496
+ bins: TOOLCHAIN.keys,
497
+ libs: NATIVE_LIBRARIES.keys
462
498
  }
463
499
  }.freeze
464
500
 
@@ -572,15 +608,23 @@ module PWN
572
608
  io.puts
573
609
 
574
610
  # ~/.pwn -----------------------------------------------------------
575
- pwn_root = File.join(Dir.home, '.pwn')
576
- pwn_yaml = File.join(pwn_root, 'pwn.yaml')
577
- pwn_dec = File.join(pwn_root, 'pwn.yaml.decryptor')
611
+ pwn_root = File.join(Dir.home, '.pwn')
612
+ driver = defined?(PWN::Env) && PWN::Env.is_a?(Hash) ? PWN::Env[:driver_opts] : nil
613
+ driver = {} unless driver.is_a?(Hash)
614
+ pwn_yaml = driver[:pwn_env_path] || File.join(pwn_root, 'pwn.yaml')
615
+ pwn_dec = driver[:pwn_dec_path] || "#{pwn_yaml}.decryptor"
578
616
  root_ok = File.directory?(pwn_root)
579
617
  root_note = root_ok ? "(#{Dir.children(pwn_root).length} entries)" : '(will be created on first `pwn` launch)'
580
618
  io.puts "#{'~/.pwn/'.ljust(26)} #{root_ok ? OK : MISS} #{root_note}"
581
619
  yaml_ok = File.file?(pwn_yaml)
582
620
  dec_state = File.file?(pwn_dec) ? 'present' : 'MISSING'
583
- yaml_note = yaml_ok ? "(encrypted, decryptor #{dec_state})" : '(run `pwn` once, then `pwn-vault` to configure)'
621
+ yaml_note = if !yaml_ok
622
+ '(run `pwn` once, then `pwn-vault` to configure)'
623
+ elsif PWN::Plugins::Vault.hardware_encrypted?(file: pwn_yaml)
624
+ '(YubiKey encrypted; no software decryptor required)'
625
+ else
626
+ "(encrypted, decryptor #{dec_state})"
627
+ end
584
628
  io.puts "#{'~/.pwn/pwn.yaml'.ljust(26)} #{yaml_ok ? OK : MISS} #{yaml_note}"
585
629
  engine = ai_active_engine
586
630
  key_ok = ai_key_configured?(engine: engine)
@@ -603,6 +647,20 @@ module PWN
603
647
  end
604
648
  io.puts
605
649
 
650
+ # Runtime libraries ------------------------------------------------
651
+ io.puts 'Native libraries (host loading only; no device discovery)'
652
+ lib_missing = []
653
+ NATIVE_LIBRARIES.each do |name, meta|
654
+ ok = native_library_available?(name: name, os: os)
655
+ lib_missing << name unless ok
656
+ packages = Array(meta[pm[:key]])
657
+ hint = packages.empty? ? 'native OS library; see hardware profile' : packages.join(' ')
658
+ need = ok ? '' : "(needs: #{hint})"
659
+ io.puts " #{name.ljust(14)} #{ok ? OK : MISS} #{need} → #{Array(meta[:plugins]).join(', ')}"
660
+ end
661
+ io.puts ' Library presence does not verify device permissions, PC/SC service, reader or token readiness.'
662
+ io.puts
663
+
606
664
  # Toolchain --------------------------------------------------------
607
665
  io.puts "#{'External toolchain'.ljust(46)} used by"
608
666
  bin_missing = []
@@ -621,36 +679,41 @@ module PWN
621
679
  io.puts
622
680
  end
623
681
 
624
- total = NATIVE_GEMS.size + TOOLCHAIN.size
625
- missing = gem_missing.size + bin_missing.size
682
+ total = NATIVE_GEMS.size + TOOLCHAIN.size + NATIVE_LIBRARIES.size
683
+ missing = gem_missing.size + bin_missing.size + lib_missing.size
626
684
  io.puts "#{total - missing} / #{total} capabilities usable · #{missing} degraded"
627
685
  io.puts
628
686
  unless missing.zero?
629
- io.puts 'Run `pwn setup --deps` to install missing OS headers/tools, or'
687
+ io.puts 'Run `pwn setup --deps` to install missing OS headers/tools/libraries, or'
630
688
  io.puts ' `pwn setup --profile <name>` for a subset. See `pwn setup --list-profiles`.'
631
689
  end
632
690
 
633
691
  # When a profile is given (`pwn setup --check --profile core`), :ok is
634
- # gated ONLY on that profile's gems/bins so a partially-provisioned host
692
+ # gated ONLY on that profile's gems/bins/libs so a partially-provisioned host
635
693
  # can still pass its own doctor gate. `packer/provisioners/pwn.sh` and
636
694
  # spec/integration/fresh_install_spec.rb rely on this — a `core` leg of
637
695
  # install-matrix must not fail because gqrx/burpsuite/etc. are absent.
638
696
  gate_gems = gem_missing
639
697
  gate_bins = bin_missing
698
+ gate_libs = lib_missing
640
699
  if profile && PROFILES.key?(profile)
641
700
  prof = PROFILES[profile]
642
701
  gate_gems = gem_missing & Array(prof[:gems])
643
702
  gate_bins = bin_missing & Array(prof[:bins])
703
+ gate_libs = lib_missing & Array(prof[:libs])
644
704
  io.puts
705
+ library_note = ", libs: #{gate_libs.empty? ? '—' : gate_libs.join(' ')}" unless Array(prof[:libs]).empty?
645
706
  io.puts "Profile '#{profile}' gate: " \
646
- "#{gate_gems.empty? && gate_bins.empty? ? OK : MISS} " \
707
+ "#{gate_gems.empty? && gate_bins.empty? && gate_libs.empty? ? OK : MISS} " \
647
708
  "(gems: #{gate_gems.empty? ? '—' : gate_gems.join(' ')}, " \
648
- "bins: #{gate_bins.empty? ? '—' : gate_bins.join(' ')})"
709
+ "bins: #{gate_bins.empty? ? '—' : gate_bins.join(' ')}" \
710
+ "#{library_note})"
649
711
  end
650
712
 
651
- { ok: gate_gems.empty? && gate_bins.empty? && Array(migrate && migrate[:incompatible]).empty?,
713
+ { ok: gate_gems.empty? && gate_bins.empty? && gate_libs.empty? && Array(migrate && migrate[:incompatible]).empty?,
652
714
  native_gems_missing: gem_missing, toolchain_missing: bin_missing,
653
- state: migrate, pkg_manager: pm[:key], os: os, arch: arch }
715
+ state: migrate, pkg_manager: pm[:key], os: os, arch: arch,
716
+ native_libraries_missing: lib_missing }
654
717
  rescue StandardError => e
655
718
  raise e
656
719
  end
@@ -690,6 +753,7 @@ module PWN
690
753
  bins = Array(prof[:bins])
691
754
  os_pkgs = []
692
755
  gems.each { |g| os_pkgs.concat(Array(NATIVE_GEMS.dig(g, pm[:key]))) }
756
+ Array(prof[:libs]).each { |lib| os_pkgs.concat(Array(NATIVE_LIBRARIES.dig(lib, pm[:key]))) }
693
757
  bins.each { |b| os_pkgs.concat(packages_for(bin: b, distro: distro, version: version, pm_key: pm[:key])) }
694
758
  os_pkgs = os_pkgs.reject(&:empty?).uniq
695
759
  alts = alt_install_cmds(bins: bins, pm_key: pm[:key], distro: distro, version: version)
@@ -765,7 +829,7 @@ module PWN
765
829
  # third_party/terminator/pwn_shift_enter.py to
766
830
  # ~/.config/terminator/plugins/ and enables the PWNShiftEnter
767
831
  # plugin in ~/.config/terminator/config — WITH confirmation and
768
- # a .pwnbak backup. This is the only VTE emulator we can fix at
832
+ # a shared recovery backup. This is the only VTE emulator we can fix at
769
833
  # the GTK layer without patching libvte itself.
770
834
  #
771
835
  # Everything here is host-config mutation, which is why it lives in
@@ -831,7 +895,7 @@ module PWN
831
895
  io.puts
832
896
  io.puts " write #{plugin_dst}"
833
897
  io.puts " edit #{cfg_path} (add PWNShiftEnter to enabled_plugins)"
834
- io.puts " backup #{cfg_path}.pwnbak"
898
+ io.puts ' backup ~/.pwn/backup (shared seven-generation store)'
835
899
  io.puts
836
900
 
837
901
  unless yes
@@ -858,7 +922,7 @@ module PWN
858
922
  if cfg.include?('PWNShiftEnter')
859
923
  io.puts " #{OK} PWNShiftEnter already enabled in #{cfg_path}"
860
924
  else
861
- FileUtils.cp(cfg_path, "#{cfg_path}.pwnbak") unless File.exist?("#{cfg_path}.pwnbak")
925
+
862
926
  new_cfg =
863
927
  if cfg =~ /^\s*enabled_plugins\s*=\s*(.*)$/
864
928
  cfg.sub(/^(\s*enabled_plugins\s*=\s*)(.*)$/) do
@@ -871,8 +935,11 @@ module PWN
871
935
  else
872
936
  "[global_config]\n enabled_plugins = PWNShiftEnter\n#{cfg}"
873
937
  end
874
- File.write(cfg_path, new_cfg)
875
- io.puts " #{OK} enabled PWNShiftEnter in #{cfg_path} (backup: config.pwnbak)"
938
+ PWN::Backups.transaction do
939
+ backup = PWN::Backups.capture(path: cfg_path, label: 'terminator')
940
+ File.write(cfg_path, new_cfg)
941
+ io.puts " #{OK} enabled PWNShiftEnter in #{cfg_path} (backup: #{backup})"
942
+ end
876
943
  end
877
944
  else
878
945
  FileUtils.mkdir_p(File.dirname(cfg_path))
@@ -900,6 +967,7 @@ module PWN
900
967
  io.puts " #{name.to_s.ljust(10)} #{meta[:desc]}"
901
968
  io.puts " #{''.ljust(10)} gems: #{meta[:gems].join(' ')}" unless meta[:gems].empty?
902
969
  io.puts " #{''.ljust(10)} bins: #{meta[:bins].join(' ')}" unless meta[:bins].empty?
970
+ io.puts " #{''.ljust(10)} libs: #{meta[:libs].join(' ')}" unless Array(meta[:libs]).empty?
903
971
  io.puts
904
972
  end
905
973
  PROFILES.keys
@@ -998,6 +1066,28 @@ module PWN
998
1066
  ''
999
1067
  end
1000
1068
 
1069
+ private_class_method def self.native_library_available?(opts = {})
1070
+ require 'ffi'
1071
+ os = opts[:os].to_s
1072
+ platform = if os.match?(/darwin|osx|macos/i)
1073
+ :macos
1074
+ elsif os.match?(/mswin|mingw|cygwin|windows/i)
1075
+ :windows
1076
+ else
1077
+ :unix
1078
+ end
1079
+ libraries = NATIVE_LIBRARIES.fetch(opts[:name]).fetch(:libraries).fetch(platform)
1080
+ flags = ::FFI::DynamicLibrary::RTLD_LAZY | ::FFI::DynamicLibrary::RTLD_LOCAL
1081
+ libraries.any? do |library|
1082
+ ::FFI::DynamicLibrary.open(library, flags)
1083
+ true
1084
+ rescue LoadError
1085
+ false
1086
+ end
1087
+ rescue LoadError
1088
+ false
1089
+ end
1090
+
1001
1091
  private_class_method def self.gem_loadable?(opts = {})
1002
1092
  name = opts[:name].to_s
1003
1093
  Gem::Specification.find_by_name(name)
@@ -1137,13 +1227,13 @@ module PWN
1137
1227
  pm_key: 'optional - package-manager family (:apt :dnf :pacman :brew :port :pkg)'
1138
1228
  )
1139
1229
 
1140
- # Run check and return its result
1230
+ # Check gems, binaries and native host libraries; native_libraries_missing reports HIDAPI/PCSC without device discovery.
1141
1231
  #{self}.check(
1142
1232
  io: 'optional - IO to write the report to (default $stdout)',
1143
- profile: 'optional - profile value consumed by #check'
1233
+ profile: 'optional - gate only this profile; core never requires hardware libraries'
1144
1234
  )
1145
1235
 
1146
- # Install OS packages + native gems for a capability profile.
1236
+ # Install OS packages and gems; hardware/full add HIDAPI and PC/SC, not a Python or executable YubiKey backend.
1147
1237
  #{self}.deps(
1148
1238
  profile: 'optional - one of PROFILES.keys (default :full)',
1149
1239
  yes: 'optional - non-interactive; assume yes to prompts (default false)',
data/lib/pwn/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PWN
4
- VERSION = '0.5.784'
4
+ VERSION = '0.5.787'
5
5
  end
@@ -5,6 +5,8 @@ require 'yaml'
5
5
  module PWN
6
6
  module WWW
7
7
  # This plugin supports app.cobalt.io 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 AppCobaltIO
9
11
  # Supported Method Parameters::
10
12
  # browser_obj = PWN::WWW::AppCobaltIO.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]
@@ -54,9 +59,9 @@ module PWN
54
59
  # name: 'commit' doesn't work
55
60
  browser.button(index: 6).click! # no name or id in button element
56
61
 
57
- if mfa
62
+ if mfa || !yubikey.nil?
58
63
  until browser.url == 'https://app.cobalt.io/dashboard'
59
- browser.text_field(id: 'code').wait_until(&:present?).set(PWN::Plugins::AuthenticationHelper.mfa(prompt: 'enter mfa token'))
64
+ browser.text_field(id: 'code').wait_until(&:present?).set(PWN::Plugins::AuthenticationHelper.mfa(prompt: 'enter mfa token', yubikey: yubikey))
60
65
  browser.button(name: 'commit').click!
61
66
  sleep 3
62
67
  end
@@ -117,12 +122,18 @@ module PWN
117
122
  proxy: 'optional - scheme://proxy_host:port || tor'
118
123
  )
119
124
 
120
- # Run login and return its result
125
+ # Log in with manual MFA or explicitly selected YubiKey OATH.
126
+ # YubiKey enables MFA even when mfa is false; failures never fall back to a prompt.
127
+ # Configure the service for an authenticator challenge, not SMS.
128
+ # Example yubikey: { account: 'EXACT_OATH_NAME', confirm: true, serial: '123456', timeout: 30 }
129
+ # Explicit consent is required: HOTP advances counters and protected OATH accounts may require authentication; no automatic repair or deletion occurs.
130
+ # Reject conflicting explicit tokens before any login action.
121
131
  #{self}.login(
122
132
  browser_obj: 'required - browser_obj returned from #open method',
123
133
  username: 'required - username value consumed by #login',
124
134
  password: 'optional - passwd (will prompt if blank)',
125
- mfa: 'optional - if true prompt for mfa token (defaults to false)'
135
+ mfa: 'optional - if true prompt for mfa token (defaults to false)',
136
+ 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'
126
137
  )
127
138
 
128
139
  # 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 bugcrowd.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 BugCrowd
9
11
  # Supported Method Parameters::
10
12
  # browser_obj = PWN::WWW::BugCrowd.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(id: 'user_password').wait_until(&:present?).set(password)
52
57
  browser.button(name: 'button').click!
53
58
 
54
- if mfa
59
+ if mfa || !yubikey.nil?
55
60
  until browser.url == 'https://bugcrowd.com/programs'
56
- browser.text_field(name: 'otp_attempt').wait_until(&:present?).set(PWN::Plugins::AuthenticationHelper.mfa(prompt: 'enter mfa token'))
61
+ browser.text_field(name: 'otp_attempt').wait_until(&:present?).set(PWN::Plugins::AuthenticationHelper.mfa(prompt: 'enter mfa token', yubikey: yubikey))
57
62
  browser.button(name: 'commit').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 tradingview.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 CoinbasePro
9
11
  # Supported Method Parameters::
10
12
  # browser_obj = PWN::WWW::CoinbasePro.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]
@@ -52,9 +57,9 @@ module PWN
52
57
  browser.text_field(name: 'password').wait_until(&:present?).set(password)
53
58
  browser.button(text: 'Sign In').click!
54
59
 
55
- if mfa
60
+ if mfa || !yubikey.nil?
56
61
  until browser.url.include?('https://pro.coinbase.com')
57
- browser.text_field(name: 'token').wait_until(&:present?).set(PWN::Plugins::AuthenticationHelper.mfa(prompt: 'enter mfa token'))
62
+ browser.text_field(name: 'token').wait_until(&:present?).set(PWN::Plugins::AuthenticationHelper.mfa(prompt: 'enter mfa token', yubikey: yubikey))
58
63
  browser.button(text: 'Verify').click!
59
64
  sleep 3
60
65
  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