pwn 0.5.783 → 0.5.785

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 (264) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/Gemfile +8 -8
  4. data/README.md +37 -0
  5. data/Rakefile +19 -1
  6. data/bin/pwn +2 -1
  7. data/bin/pwn_setup +1 -1
  8. data/bin/pwn_web_cache_deception +35 -7
  9. data/build_gem.sh +1 -1
  10. data/documentation/AI-Integration.md +7 -5
  11. data/documentation/Agent-Tool-Registry.md +2 -2
  12. data/documentation/Backup-Retention.md +82 -0
  13. data/documentation/Configuration.md +10 -6
  14. data/documentation/Contributing.md +1 -1
  15. data/documentation/Cron.md +64 -35
  16. data/documentation/Diagrams.md +2 -2
  17. data/documentation/General-PWN-Usage.md +4 -3
  18. data/documentation/Home.md +6 -5
  19. data/documentation/How-PWN-Works.md +7 -6
  20. data/documentation/Installation.md +254 -10
  21. data/documentation/Mission-Cancellation.md +134 -0
  22. data/documentation/Operability.md +17 -0
  23. data/documentation/Persistence.md +8 -6
  24. data/documentation/Plugin-Degradation.md +12 -0
  25. data/documentation/Plugins.md +3 -2
  26. data/documentation/Reinforcement-Learning.md +5 -6
  27. data/documentation/Runtime-Source-Protection.md +162 -0
  28. data/documentation/Transparent-Browser.md +11 -0
  29. data/documentation/Troubleshooting.md +3 -2
  30. data/documentation/What-is-PWN.md +2 -2
  31. data/documentation/YubiKey-Vaults.md +183 -0
  32. data/documentation/YubiKey.md +339 -0
  33. data/documentation/diagrams/cron-scheduling.svg +130 -128
  34. data/documentation/diagrams/dot/cron-scheduling.dot +5 -5
  35. data/documentation/diagrams/dot/persistence-filesystem.dot +5 -3
  36. data/documentation/diagrams/dot/plugin-ecosystem.dot +2 -2
  37. data/documentation/diagrams/persistence-filesystem.svg +172 -142
  38. data/documentation/diagrams/plugin-ecosystem.svg +64 -64
  39. data/documentation/pwn-ai-Agent.md +10 -4
  40. data/etc/default_cron/session_cleanup.rb +1245 -22
  41. data/etc/default_skills/pwn/ai/agent/mission_namespace/SKILL.md +21 -0
  42. data/etc/default_skills/pwn/ai/agent/tools/browser_act/SKILL.md +45 -0
  43. data/etc/default_skills/pwn/ai/agent/tools/loot_query/SKILL.md +27 -26
  44. data/etc/default_skills/pwn/ai/agent/tools/recon_run/SKILL.md +25 -15
  45. data/etc/default_skills/pwn/ai/agent/tools/workspace_tool/SKILL.md +24 -0
  46. data/etc/default_skills/pwn/ai/server_tools/SKILL.md +25 -0
  47. data/etc/default_skills/pwn/backups/SKILL.md +54 -0
  48. data/etc/default_skills/pwn/plugins/recon/SKILL.md +12 -1
  49. data/etc/default_skills/pwn/plugins/yubikey/SKILL.md +157 -0
  50. data/etc/default_skills/pwn/plugins/yubikey/fido/SKILL.md +189 -0
  51. data/etc/default_skills/pwn/plugins/yubikey/hsmauth/SKILL.md +83 -0
  52. data/etc/default_skills/pwn/plugins/yubikey/management/SKILL.md +80 -0
  53. data/etc/default_skills/pwn/plugins/yubikey/oath/SKILL.md +115 -0
  54. data/etc/default_skills/pwn/plugins/yubikey/openpgp/SKILL.md +144 -0
  55. data/etc/default_skills/pwn/plugins/yubikey/otp/SKILL.md +97 -0
  56. data/etc/default_skills/pwn/plugins/yubikey/piv/SKILL.md +110 -0
  57. data/etc/default_skills/pwn/plugins/yubikey/protocol/SKILL.md +21 -0
  58. data/etc/default_skills/pwn/plugins/yubikey/security_domain/SKILL.md +75 -0
  59. data/etc/default_skills/pwn/plugins/yubikey/security_domain/scp/SKILL.md +46 -0
  60. data/etc/default_skills/pwn/plugins/yubikey/transport/SKILL.md +98 -0
  61. data/etc/default_skills/pwn/redaction/SKILL.md +13 -1
  62. data/lib/pwn/ai/agent/dispatch.rb +26 -15
  63. data/lib/pwn/ai/agent/extrospection.rb +6 -7
  64. data/lib/pwn/ai/agent/learning.rb +62 -25
  65. data/lib/pwn/ai/agent/loop.rb +17 -1
  66. data/lib/pwn/ai/agent/mission_namespace.rb +331 -0
  67. data/lib/pwn/ai/agent/policy.rb +5 -0
  68. data/lib/pwn/ai/agent/policy_evaluation.rb +8 -8
  69. data/lib/pwn/ai/agent/prompt_builder.rb +13 -2
  70. data/lib/pwn/ai/agent/request_runtime.rb +122 -18
  71. data/lib/pwn/ai/agent/reward.rb +5 -3
  72. data/lib/pwn/ai/agent/skill_consolidation.rb +3 -2
  73. data/lib/pwn/ai/agent/skill_review.rb +9 -5
  74. data/lib/pwn/ai/agent/swarm.rb +35 -16
  75. data/lib/pwn/ai/agent/tools/browser_act.rb +32 -0
  76. data/lib/pwn/ai/agent/tools/loot_query.rb +11 -1
  77. data/lib/pwn/ai/agent/tools/recon_run.rb +11 -1
  78. data/lib/pwn/ai/agent/tools/workspace_tool.rb +29 -0
  79. data/lib/pwn/ai/agent/turn_finalizer.rb +6 -2
  80. data/lib/pwn/ai/agent.rb +1 -0
  81. data/lib/pwn/ai/anthropic.rb +8 -33
  82. data/lib/pwn/ai/cli.rb +34 -5
  83. data/lib/pwn/ai/gemini.rb +1 -0
  84. data/lib/pwn/ai/grok.rb +39 -28
  85. data/lib/pwn/ai/ollama.rb +4 -2
  86. data/lib/pwn/ai/open_ai.rb +11 -32
  87. data/lib/pwn/ai/open_web_ui.rb +62 -4
  88. data/lib/pwn/ai/router.rb +53 -3
  89. data/lib/pwn/ai/server_tools.rb +231 -0
  90. data/lib/pwn/ai.rb +1 -0
  91. data/lib/pwn/backups.rb +347 -0
  92. data/lib/pwn/config.rb +70 -39
  93. data/lib/pwn/cron.rb +120 -49
  94. data/lib/pwn/migrate.rb +150 -68
  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/binary_parser.rb +5 -4
  100. data/lib/pwn/plugins/ghidra.rb +4 -3
  101. data/lib/pwn/plugins/google_workspace.rb +4 -21
  102. data/lib/pwn/plugins/jobs.rb +1 -1
  103. data/lib/pwn/plugins/log.rb +21 -3
  104. data/lib/pwn/plugins/preflight_checker.rb +34 -9
  105. data/lib/pwn/plugins/recon.rb +4 -1
  106. data/lib/pwn/plugins/repl/ai.rb +305 -27
  107. data/lib/pwn/plugins/repl/ai_console.rb +273 -89
  108. data/lib/pwn/plugins/repl/ai_swarm.rb +181 -62
  109. data/lib/pwn/plugins/repl/mesh.rb +3 -16
  110. data/lib/pwn/plugins/repl/vault.rb +4 -15
  111. data/lib/pwn/plugins/repl.rb +1 -216
  112. data/lib/pwn/plugins/sandbox/driver.rb +4 -0
  113. data/lib/pwn/plugins/transparent_browser.rb +215 -0
  114. data/lib/pwn/plugins/vault.rb +412 -135
  115. data/lib/pwn/plugins/yubikey/fido.rb +1288 -0
  116. data/lib/pwn/plugins/yubikey/hsmauth.rb +439 -0
  117. data/lib/pwn/plugins/yubikey/management.rb +686 -0
  118. data/lib/pwn/plugins/yubikey/oath.rb +766 -0
  119. data/lib/pwn/plugins/yubikey/openpgp.rb +1063 -0
  120. data/lib/pwn/plugins/yubikey/otp.rb +897 -0
  121. data/lib/pwn/plugins/yubikey/piv.rb +1315 -0
  122. data/lib/pwn/plugins/yubikey/protocol.rb +341 -0
  123. data/lib/pwn/plugins/yubikey/security_domain/scp.rb +482 -0
  124. data/lib/pwn/plugins/yubikey/security_domain.rb +464 -0
  125. data/lib/pwn/plugins/yubikey/transport.rb +1017 -0
  126. data/lib/pwn/plugins/yubikey.rb +844 -0
  127. data/lib/pwn/plugins.rb +1 -0
  128. data/lib/pwn/redaction.rb +69 -12
  129. data/lib/pwn/session_trace.rb +4 -0
  130. data/lib/pwn/sessions.rb +58 -16
  131. data/lib/pwn/setup.rb +123 -41
  132. data/lib/pwn/version.rb +1 -1
  133. data/lib/pwn/www/app_cobalt_io.rb +16 -5
  134. data/lib/pwn/www/bug_crowd.rb +16 -5
  135. data/lib/pwn/www/coinbase_pro.rb +16 -5
  136. data/lib/pwn/www/github.rb +30 -14
  137. data/lib/pwn/www/paypal.rb +15 -4
  138. data/lib/pwn/www/synack.rb +16 -5
  139. data/lib/pwn/www/twitter.rb +17 -6
  140. data/lib/pwn.rb +1 -0
  141. data/scripts/build_gate.rb +58 -0
  142. data/spec/documentation/installation_md_spec.rb +1 -1
  143. data/spec/fixtures/yubikey/apdu_frames.json +162 -0
  144. data/spec/integration/authentication_helper_oath_spec.rb +117 -0
  145. data/spec/integration/build_gate_spec.rb +73 -0
  146. data/spec/integration/controller_spill_spec.rb +48 -0
  147. data/spec/integration/harness_process_spec.rb +97 -0
  148. data/spec/integration/mission_cancel_spec.rb +79 -0
  149. data/spec/integration/mission_curses_pty_spec.rb +44 -0
  150. data/spec/integration/persistence_roundtrip_spec.rb +1 -7
  151. data/spec/integration/readme_security_spec.rb +47 -0
  152. data/spec/integration/web_cache_deception_yubikey_spec.rb +159 -0
  153. data/spec/lib/pwn/ai/agent/dispatch_spec.rb +6 -3
  154. data/spec/lib/pwn/ai/agent/dispatch_vault_safety_spec.rb +86 -0
  155. data/spec/lib/pwn/ai/agent/injection_guard_spec.rb +2 -0
  156. data/spec/lib/pwn/ai/agent/learning_spec.rb +4 -4
  157. data/spec/lib/pwn/ai/agent/loop_artifacts_spec.rb +5 -0
  158. data/spec/lib/pwn/ai/agent/loop_spec.rb +104 -29
  159. data/spec/lib/pwn/ai/agent/mission_namespace_spec.rb +138 -0
  160. data/spec/lib/pwn/ai/agent/policy_spec.rb +27 -0
  161. data/spec/lib/pwn/ai/agent/request_runtime_spec.rb +48 -0
  162. data/spec/lib/pwn/ai/agent/shell_search_effect_spec.rb +44 -0
  163. data/spec/lib/pwn/ai/agent/solve_spec.rb +6 -4
  164. data/spec/lib/pwn/ai/agent/tools/browser_act_spec.rb +44 -0
  165. data/spec/lib/pwn/ai/agent/tools/loot_query_yubikey_spec.rb +55 -0
  166. data/spec/lib/pwn/ai/agent/tools/recon_run_yubikey_spec.rb +43 -0
  167. data/spec/lib/pwn/ai/agent/tools/workspace_tool_spec.rb +20 -0
  168. data/spec/lib/pwn/ai/agent/turn_finalizer_spec.rb +7 -0
  169. data/spec/lib/pwn/ai/anthropic_vault_enrollment_spec.rb +37 -0
  170. data/spec/lib/pwn/ai/cli_spec.rb +6 -9
  171. data/spec/lib/pwn/ai/oauth_vault_persistence_spec.rb +195 -0
  172. data/spec/lib/pwn/ai/open_ai_spec.rb +2 -2
  173. data/spec/lib/pwn/ai/open_web_ui_workspace_spec.rb +36 -0
  174. data/spec/lib/pwn/ai/router_spec.rb +31 -3
  175. data/spec/lib/pwn/ai/server_tools_spec.rb +39 -0
  176. data/spec/lib/pwn/backups_spec.rb +114 -0
  177. data/spec/lib/pwn/config_redaction_yubikey_spec.rb +28 -0
  178. data/spec/lib/pwn/config_yubikey_spec.rb +92 -0
  179. data/spec/lib/pwn/cron_spec.rb +113 -6
  180. data/spec/lib/pwn/migrate_spec.rb +108 -5
  181. data/spec/lib/pwn/migrate_yubikey_spec.rb +128 -0
  182. data/spec/lib/pwn/module_skills_spec.rb +8 -0
  183. data/spec/lib/pwn/plugins/ai_sandbox_spec.rb +230 -0
  184. data/spec/lib/pwn/plugins/authentication_helper_native_yubikey_spec.rb +40 -0
  185. data/spec/lib/pwn/plugins/authentication_helper_yubikey_spec.rb +90 -0
  186. data/spec/lib/pwn/plugins/capability_broker/daemon_spec.rb +1 -1
  187. data/spec/lib/pwn/plugins/google_workspace_spec.rb +17 -35
  188. data/spec/lib/pwn/plugins/log_spec.rb +7 -0
  189. data/spec/lib/pwn/plugins/log_yubikey_spec.rb +47 -0
  190. data/spec/lib/pwn/plugins/preflight_checker_native_yubikey_spec.rb +19 -0
  191. data/spec/lib/pwn/plugins/preflight_checker_yubikey_spec.rb +55 -0
  192. data/spec/lib/pwn/plugins/recon_yubikey_spec.rb +43 -0
  193. data/spec/lib/pwn/plugins/repl/ai_console_spec.rb +125 -78
  194. data/spec/lib/pwn/plugins/repl/ai_legacy_containment_spec.rb +200 -0
  195. data/spec/lib/pwn/plugins/repl/ai_swarm_spec.rb +30 -16
  196. data/spec/lib/pwn/plugins/repl/ai_vault_edit_yubikey_spec.rb +103 -0
  197. data/spec/lib/pwn/plugins/repl/ai_yubikey_spec.rb +48 -0
  198. data/spec/lib/pwn/plugins/repl/mesh_vault_persistence_spec.rb +149 -0
  199. data/spec/lib/pwn/plugins/repl/vault_yaml_serialization_spec.rb +11 -0
  200. data/spec/lib/pwn/plugins/repl/vault_yubikey_spec.rb +39 -0
  201. data/spec/lib/pwn/plugins/repl_spec.rb +56 -5
  202. data/spec/lib/pwn/plugins/transparent_browser_jev_spec.rb +46 -0
  203. data/spec/lib/pwn/plugins/vault_native_yubikey_spec.rb +29 -0
  204. data/spec/lib/pwn/plugins/vault_publication_safety_spec.rb +258 -0
  205. data/spec/lib/pwn/plugins/vault_store_safety_spec.rb +241 -0
  206. data/spec/lib/pwn/plugins/vault_yubikey_safety_spec.rb +168 -0
  207. data/spec/lib/pwn/plugins/vault_yubikey_spec.rb +128 -0
  208. data/spec/lib/pwn/plugins/vault_yubikey_vector_spec.rb +45 -0
  209. data/spec/lib/pwn/plugins/yubikey/fido_spec.rb +854 -0
  210. data/spec/lib/pwn/plugins/yubikey/hsmauth_spec.rb +235 -0
  211. data/spec/lib/pwn/plugins/yubikey/management_spec.rb +277 -0
  212. data/spec/lib/pwn/plugins/yubikey/oath_spec.rb +349 -0
  213. data/spec/lib/pwn/plugins/yubikey/openpgp_spec.rb +810 -0
  214. data/spec/lib/pwn/plugins/yubikey/otp_spec.rb +337 -0
  215. data/spec/lib/pwn/plugins/yubikey/piv_spec.rb +646 -0
  216. data/spec/lib/pwn/plugins/yubikey/protocol_secure_spec.rb +28 -0
  217. data/spec/lib/pwn/plugins/yubikey/protocol_spec.rb +149 -0
  218. data/spec/lib/pwn/plugins/yubikey/protocol_vectors_spec.rb +18 -0
  219. data/spec/lib/pwn/plugins/yubikey/security_domain/scp_spec.rb +256 -0
  220. data/spec/lib/pwn/plugins/yubikey/security_domain/support.rb +153 -0
  221. data/spec/lib/pwn/plugins/yubikey/security_domain_spec.rb +296 -0
  222. data/spec/lib/pwn/plugins/yubikey/transport_spec.rb +760 -0
  223. data/spec/lib/pwn/plugins/yubikey_security_spec.rb +260 -0
  224. data/spec/lib/pwn/plugins/yubikey_spec.rb +209 -0
  225. data/spec/lib/pwn/plugins/yubikey_terminal_spec.rb +147 -0
  226. data/spec/lib/pwn/redaction_spec.rb +13 -10
  227. data/spec/lib/pwn/redaction_yubikey_spec.rb +61 -0
  228. data/spec/lib/pwn/session_cleanup_concurrency_spec.rb +201 -0
  229. data/spec/lib/pwn/session_cleanup_default_spec.rb +167 -2
  230. data/spec/lib/pwn/session_cleanup_legacy_spec.rb +101 -0
  231. data/spec/lib/pwn/session_cleanup_payload_spec.rb +335 -2
  232. data/spec/lib/pwn/session_cleanup_recovery_spec.rb +403 -0
  233. data/spec/lib/pwn/sessions_spec.rb +112 -0
  234. data/spec/lib/pwn/setup_native_yubikey_spec.rb +66 -0
  235. data/spec/lib/pwn/setup_spec.rb +3 -1
  236. data/spec/lib/pwn/setup_vault_yubikey_spec.rb +61 -0
  237. data/spec/lib/pwn/setup_yubikey_spec.rb +40 -0
  238. data/spec/lib/pwn/www/login_yubikey_spec.rb +273 -0
  239. data/spec/spec_helper.rb +15 -0
  240. data/spec/support/gate_failure_formatter.rb +14 -0
  241. data/spec/support/harness/cli_faults.rb +13 -9
  242. data/spec/support/harness/controller_spill.rb +26 -0
  243. data/spec/support/harness/controller_state.rb +54 -0
  244. data/spec/support/harness/evaluate.rb +12 -1
  245. data/spec/support/harness/experiments.rb +1 -1
  246. data/spec/support/harness/fake_provider.rb +22 -6
  247. data/spec/support/harness/fault_preload.rb +3 -1
  248. data/spec/support/harness/mission_console.rb +54 -0
  249. data/spec/support/harness/mission_curses_pty.rb +41 -0
  250. data/spec/support/harness/mission_dispatch.rb +30 -0
  251. data/spec/support/harness/mission_fixture_preload.rb +24 -0
  252. data/spec/support/harness/mission_job_lifetime.rb +25 -0
  253. data/spec/support/harness/mission_legacy.rb +25 -0
  254. data/spec/support/harness/mission_normal_exit.rb +34 -0
  255. data/spec/support/harness/mission_swarm.rb +23 -0
  256. data/spec/support/harness/mission_unsupported.rb +29 -0
  257. data/spec/support/harness/package_acceptance.rb +33 -5
  258. data/spec/support/harness/session_acceptance.rb +4 -4
  259. data/spec/support/harness/swarm_acceptance.rb +1 -1
  260. data/spec/support/harness/swarm_control.rb +44 -0
  261. data/spec/support/mission_namespace.rb +14 -0
  262. data/spec/support/mission_pipe_queue.rb +33 -0
  263. data/third_party/pwn_rdoc.jsonl +599 -15
  264. metadata +140 -17
@@ -17,7 +17,7 @@ module PWN
17
17
  basic_user = opts[:username]
18
18
  basic_pass = opts[:password]
19
19
  if (basic_user.nil? || basic_pass.nil?) && !opts[:host].to_s.empty?
20
- hit = Array(PWN::Plugins::Vault.offer(host: opts[:host], service: opts[:service], engagement: opts[:engagement] || opts[:engagement_id])).first
20
+ hit = Array(PWN::Plugins::Vault.offer(host: opts[:host], service: opts[:service], engagement: opts[:engagement] || opts[:engagement_id], yubikey: opts[:yubikey])).first
21
21
  if hit
22
22
  basic_user = hit[:username] if basic_user.nil?
23
23
  basic_pass = hit[:secret] if basic_pass.nil?
@@ -63,6 +63,7 @@ module PWN
63
63
  password: 'optional - password; omitted values are filled from Vault.offer',
64
64
  host: 'optional - hostname used to offer recon loot when username or password is omitted',
65
65
  service: 'optional - service name such as http or ssh used to match loot',
66
+ yubikey: 'optional - YubiKey selector Hash forwarded to hardware-protected Vault loot',
66
67
  engagement: 'optional - engagement id scoping the loot file (defaults to default)',
67
68
  engagement_id: 'optional - alias for engagement'
68
69
  )
@@ -101,16 +101,17 @@ module PWN
101
101
  sha = Digest::SHA256.file(path).hexdigest
102
102
  sid = (opts[:session_id] || 'triage').to_s
103
103
  cache_dir = File.join(Dir.home, '.pwn', 'cache', 'triage')
104
- FileUtils.mkdir_p(cache_dir)
105
104
  cache = File.join(cache_dir, "#{sha}.json")
106
- if File.file?(cache)
107
- cached = JSON.parse(File.read(cache), symbolize_names: true)
105
+ require File.expand_path('../../../etc/default_cron/session_cleanup', __dir__)
106
+ cached_bytes = PWN::SessionCleanup.cache(path: cache)
107
+ if cached_bytes
108
+ cached = JSON.parse(cached_bytes, symbolize_names: true)
108
109
  spilled = persist_triage(body: cached, session_id: sid)
109
110
  return cached.merge(cached: true, handle: spilled[:handle], artifact: spilled)
110
111
  end
111
112
 
112
113
  body = inspect_binary(opts.merge(path: path, sha256: sha))
113
- File.write(cache, JSON.pretty_generate(body))
114
+ PWN::SessionCleanup.cache(path: cache, bytes: JSON.pretty_generate(body))
114
115
  facts_dir = File.join(Dir.home, '.pwn', 'engagements', 'default', 'binaries', sha)
115
116
  FileUtils.mkdir_p(facts_dir)
116
117
  File.write(File.join(facts_dir, 'facts.json'), JSON.pretty_generate(body))
@@ -37,13 +37,14 @@ module PWN
37
37
 
38
38
  sha = Digest::SHA256.file(path).hexdigest if File.file?(path)
39
39
  cache = File.join(Dir.home, '.pwn', 'cache', 'decompile', "#{sha}.json") if sha
40
- return JSON.parse(File.read(cache), symbolize_names: true).merge(cached: true) if cache && File.file?(cache)
40
+ require File.expand_path('../../../etc/default_cron/session_cleanup', __dir__)
41
+ cached = PWN::SessionCleanup.cache(path: cache) if cache
42
+ return JSON.parse(cached, symbolize_names: true).merge(cached: true) if cached
41
43
 
42
44
  row = analyze(opts)
43
45
  fn = opts[:function].to_s
44
46
  row[:function] = fn unless fn.empty?
45
- FileUtils.mkdir_p(File.dirname(cache)) if cache
46
- File.write(cache, JSON.generate(row)) if cache
47
+ PWN::SessionCleanup.cache(path: cache, bytes: JSON.generate(row)) if cache
47
48
  row
48
49
  end
49
50
 
@@ -397,25 +397,13 @@ module PWN
397
397
  dec_path = dec_path.to_s.strip
398
398
  dec_path = "#{env_path}.decryptor" if dec_path.empty?
399
399
 
400
- unless File.exist?(env_path) && File.exist?(dec_path) && File.readable?(dec_path)
400
+ unless File.exist?(env_path)
401
401
  puts '[*] INFO: Google Workspace OAuth tokens updated in this session only; ' \
402
- "persistence to #{env_path} skipped (missing decryption artifacts)."
402
+ "persistence to #{env_path} skipped (missing vault)."
403
403
  return false
404
404
  end
405
405
 
406
- decryptor = YAML.load_file(dec_path, symbolize_names: true)
407
- key = decryptor.is_a?(Hash) ? decryptor[:key] : nil
408
- iv = decryptor.is_a?(Hash) ? decryptor[:iv] : nil
409
- unless real_config_value?(value: key) && real_config_value?(value: iv)
410
- puts '[*] INFO: Google Workspace OAuth tokens updated in this session only; ' \
411
- 'decryptor has no usable key/iv.'
412
- return false
413
- end
414
-
415
- PWN::Plugins::Vault.decrypt(file: env_path, key: key, iv: iv)
416
- begin
417
- vault = YAML.load_file(env_path, symbolize_names: true)
418
- vault = {} unless vault.is_a?(Hash)
406
+ PWN::Plugins::Vault.update(file: env_path, decryptor_file: dec_path) do |vault|
419
407
  vault[:plugins] = {} unless vault[:plugins].is_a?(Hash)
420
408
  vault[:plugins][:google_workspace] = {} unless vault[:plugins][:google_workspace].is_a?(Hash)
421
409
  slot = vault[:plugins][:google_workspace][:oauth]
@@ -423,15 +411,10 @@ module PWN
423
411
  %i[bearer_token refresh_token expires_at client_id client_secret redirect_uri scope].each do |k|
424
412
  slot[k] = oauth[k] if oauth.key?(k) && !oauth[k].nil?
425
413
  end
426
- yaml_env = YAML.dump(vault).gsub(/^(\s*):/, '\1')
427
- File.write(env_path, yaml_env)
428
- File.chmod(0o600, env_path)
429
- ensure
430
- PWN::Plugins::Vault.encrypt(file: env_path, key: key, iv: iv)
431
414
  end
432
415
  true
433
416
  rescue StandardError => e
434
- warn "[!] Google Workspace OAuth vault persistence failed (session tokens still updated): #{e.class}: #{e.message}"
417
+ warn "[!] Google Workspace OAuth vault persistence failed (session tokens still updated): #{e.class}"
435
418
  false
436
419
  end
437
420
 
@@ -26,7 +26,7 @@ module PWN
26
26
 
27
27
  root = File.expand_path(JOBS_DIR)
28
28
  FileUtils.mkdir_p(root, mode: 0o700)
29
- File.chmod(0o700, root)
29
+ File.chmod(0o700, root) unless (File.stat(root).mode & 0o777) == 0o700
30
30
  runtime = Float(opts[:max_runtime] || 0)
31
31
  raise ArgumentError, 'max_runtime must be finite and nonnegative' unless runtime.finite? && runtime >= 0
32
32
 
@@ -27,7 +27,7 @@ module PWN
27
27
  PWN::AI::Agent::Loop
28
28
  PWN::AI::Agent::Dispatch
29
29
  ].freeze
30
- SECRET_KEY_RX = /password|passwd|secret|token|api[_-]?key|authorization|bearer|cookie|session[_-]?id|private[_-]?key|decryptor|credential|ssh[_-]?key|client[_-]?secret|refresh[_-]?token|access[_-]?token|id[_-]?token|vault|csrf/i
30
+ SECRET_KEY_RX = Regexp.union(PWN::Redaction::MASKED_FIELD, /password|passwd|secret|token|api[_-]?key|authorization|bearer|cookie|session[_-]?id|private[_-]?key|decryptor|credential|ssh[_-]?key|client[_-]?secret|refresh[_-]?token|access[_-]?token|id[_-]?token|vault|csrf/i)
31
31
  SECRET_VALUE_RX = %r{
32
32
  -----BEGIN\ [A-Z ]*PRIVATE\ KEY----- |
33
33
  Bearer\s+[A-Za-z0-9\-._~+/]+=* |
@@ -60,8 +60,9 @@ module PWN
60
60
  log_file_path = "/tmp/pwn-ai-#{session}.json" if level == :learning
61
61
  log_file = File.open(log_file_path, 'w')
62
62
  else
63
- log_file_path = '/tmp/pwn.log'
64
- log_file = File.open(log_file_path, 'a')
63
+ require 'stringio'
64
+ log_file_path = File.join(Dir.home, '.pwn', 'logs', 'pwn.log')
65
+ log_file = StringIO.new
65
66
  end
66
67
 
67
68
  # Leave 10 "old" log files where
@@ -124,6 +125,10 @@ module PWN
124
125
  end
125
126
 
126
127
  logger.add(logger.level, PWN::Redaction.redact(value: log_event), PWN::Redaction.redact(value: which_self))
128
+ unless level == :learning
129
+ require File.expand_path('../../../etc/default_cron/session_cleanup', __dir__)
130
+ PWN::SessionCleanup.append_log(path: log_file_path, bytes: log_file.string)
131
+ end
127
132
  logger.close
128
133
  rescue Interrupt
129
134
  note_interrupt!(where: 'CTRL+C', which_self: self) if debug_enabled?
@@ -424,9 +429,18 @@ module PWN
424
429
  unless @debug_file.nil? || @debug_file.closed?
425
430
  @debug_file.puts(PWN::Redaction.token(kind: 'stream', value: @stderr_redaction_buffer)) unless @stderr_redaction_buffer.to_s.empty?
426
431
  @debug_file.close
432
+ @debug_lease&.close
433
+ @debug_lease = nil
434
+ if @debug_path.to_s.start_with?("#{File.join(Dir.home, '.pwn', 'logs')}/")
435
+ require File.expand_path('../../../etc/default_cron/session_cleanup', __dir__)
436
+ PWN::SessionCleanup.register_disposable(path: @debug_path, category: 'logs')
437
+ end
427
438
  end
428
439
  rescue StandardError
429
440
  nil
441
+ ensure
442
+ @debug_lease&.close
443
+ @debug_lease = nil
430
444
  end
431
445
  @debug_file = nil
432
446
  @stderr_redaction_buffer = ''
@@ -439,6 +453,8 @@ module PWN
439
453
 
440
454
  close_debug_file!
441
455
  FileUtils.mkdir_p(File.dirname(path))
456
+ @debug_lease = File.open("#{path}.lease", File::RDWR | File::CREAT | File::NOFOLLOW, 0o600)
457
+ @debug_lease.flock(File::LOCK_EX)
442
458
  io = File.open(path, 'a', 0o600)
443
459
  io.chmod(0o600)
444
460
  io.sync = true
@@ -628,11 +644,13 @@ module PWN
628
644
  private_class_method def self.inspect_debug(opts = {})
629
645
  val = opts[:value]
630
646
  key = opts[:key]
647
+ return PWN::Redaction::CONSTANT_MASK if key.to_s.match?(PWN::Redaction::MASKED_FIELD)
631
648
  return PWN::Redaction.token(kind: 'credential', value: val.to_s) if secret_key?(key: key)
632
649
  return PWN::Redaction.token(kind: 'opaque', value: val) if opaque_secret?(key: key, value: val)
633
650
  return format_debug_hash(hash: val) if val.is_a?(Hash)
634
651
  return sanitize_debug_text(text: val) if val.is_a?(String) && val.match?(SECRET_VALUE_RX)
635
652
 
653
+ val = PWN::Redaction.redact(value: val) if val.is_a?(Array)
636
654
  raw = sanitize_debug_text(text: val.inspect)
637
655
  raw = "#{raw[0, DEBUG_VALUE_MAX]}…" if raw.length > DEBUG_VALUE_MAX
638
656
  raw
@@ -6,9 +6,9 @@ require 'time'
6
6
 
7
7
  module PWN
8
8
  module Plugins
9
- # Plugin/binary/capability preflight. Plugins declare required_bins /
10
- # required_caps; HOST summaries list degraded modules without autoloading
11
- # the whole plugin tree.
9
+ # Plugin/binary/capability preflight. Native probes load host libraries
10
+ # only: no device enumeration, authentication or I/O. HOST summaries list
11
+ # degraded modules without autoloading the whole plugin tree.
12
12
  module PreflightChecker
13
13
  class MissingBinary < StandardError; end
14
14
  class MissingCapability < StandardError; end
@@ -32,6 +32,13 @@ module PWN
32
32
  'PWN::Plugins::Semgrep' => { bins: %w[semgrep] },
33
33
  'PWN::Plugins::ExploitDB' => { bins: %w[searchsploit] },
34
34
  'PWN::Plugins::Recon' => { bins: %w[subfinder httpx] },
35
+ 'PWN::Plugins::YubiKey' => {
36
+ bins: [],
37
+ native: [
38
+ { name: 'FIDO native transport', probe: 'PWN::Plugins::YubiKey', options: { application: :fido } },
39
+ { name: 'PC/SC native transport', probe: 'PWN::Plugins::YubiKey', options: { application: :oath } }
40
+ ]
41
+ },
35
42
  'PWN::Plugins::CredentialAttack' => { bins: %w[hydra john hashcat] }
36
43
  }.freeze
37
44
 
@@ -97,6 +104,11 @@ module PWN
97
104
  PLUGIN_DEPS.each do |mod, dep|
98
105
  missing = Array(dep[:bins]).reject { |b| bin?(name: b) }
99
106
  caps[mod] = { bins: dep[:bins], missing: missing, ok: missing.empty?, caps: Array(dep[:caps]) }
107
+ next unless dep[:native]
108
+
109
+ missing_native = Array(dep[:native]).reject { |probe| native_available?(probe: probe) }.map { |probe| probe[:name] }
110
+ caps[mod][:missing_native] = missing_native
111
+ caps[mod][:ok] &&= missing_native.empty?
100
112
  end
101
113
  row = {
102
114
  at: Time.now.utc.iso8601,
@@ -164,21 +176,25 @@ module PWN
164
176
  bins = Array(dep[:bins])
165
177
  caps = Array(dep[:caps])
166
178
  services = Array(dep[:services])
179
+ native = Array(dep[:native])
167
180
  missing_bins = bins.reject { |b| bin?(name: b) }
181
+ missing_native = native.reject { |probe| native_available?(probe: probe) }.map { |probe| probe[:name] }
168
182
  missing_caps = []
169
183
  if caps.include?('CAP_NET_RAW')
170
184
  pkt_ok = defined?(PWN::Plugins::Packet) && PWN::Plugins::Packet.respond_to?(:health) && PWN::Plugins::Packet.health[:ok]
171
185
  missing_caps << 'CAP_NET_RAW' unless pkt_ok || cap_net_raw?
172
186
  end
173
187
  missing_services = services.reject { |s| service?(name: s[:name] || s['name'], path: s[:path] || s['path']) }.map { |s| s[:name] || s['name'] }
174
- status = if missing_bins.empty? && missing_caps.empty? && missing_services.empty?
188
+ status = if missing_bins.empty? && missing_caps.empty? && missing_services.empty? && missing_native.empty?
175
189
  :ok
176
- elsif bins.any? && missing_bins.length == bins.length
190
+ elsif (bins.any? && missing_bins.length == bins.length) || (native.any? && missing_native.length == native.length)
177
191
  :dead
178
192
  else
179
193
  :degraded
180
194
  end
181
- { plugin: plugin, status: status, missing_bins: missing_bins, missing_caps: missing_caps, missing_services: missing_services }
195
+ row = { plugin: plugin, status: status, missing_bins: missing_bins, missing_caps: missing_caps, missing_services: missing_services }
196
+ row[:missing_native] = missing_native if dep[:native]
197
+ row
182
198
  end
183
199
  end
184
200
 
@@ -189,7 +205,7 @@ module PWN
189
205
  degraded = rows.select { |r| r[:status] == :degraded }
190
206
  lines = ["HOST plugins ok=#{rows.count { |r| r[:status] == :ok }} degraded=#{degraded.length} dead=#{dead.length}"]
191
207
  (degraded + dead).first(cap).each do |r|
192
- miss = (Array(r[:missing_bins]) + Array(r[:missing_caps]) + Array(r[:missing_services])).join(',')
208
+ miss = (Array(r[:missing_bins]) + Array(r[:missing_caps]) + Array(r[:missing_services]) + Array(r[:missing_native])).join(',')
193
209
  fb = Array(FALLBACKS[r[:plugin]]).join(',')
194
210
  extra = fb.empty? ? '' : " fallback=#{fb}"
195
211
  anchor = r[:plugin].to_s.downcase.gsub(/[^a-z0-9]+/, '-').gsub(/\A-+|-+\z/, '')
@@ -198,6 +214,15 @@ module PWN
198
214
  lines.join("\n")
199
215
  end
200
216
 
217
+ # Only trusted declarations above may supply a host-only available? probe.
218
+ private_class_method def self.native_available?(opts = {})
219
+ probe = opts[:probe]
220
+ provider = probe.fetch(:probe).split('::').inject(Object) { |scope, name| scope.const_get(name, false) }
221
+ provider.available?(probe.fetch(:options, {})) == true
222
+ rescue LoadError, StandardError
223
+ false
224
+ end
225
+
201
226
  public_class_method def self.authors
202
227
  "AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
203
228
  end
@@ -247,7 +272,7 @@ module PWN
247
272
  path: 'required - unix socket path (e.g. /var/run/docker.sock)'
248
273
  )
249
274
 
250
- # Run check and return its result
275
+ # Report dependencies; native rows add missing_native from host-only library probes, never device readiness.
251
276
  #{self}.check(
252
277
  names: 'optional - Array names value consumed by #check'
253
278
  )
@@ -258,7 +283,7 @@ module PWN
258
283
  limit: 'optional - limit value consumed by #host_summary'
259
284
  )
260
285
 
261
- # Refresh ~/.pwn/capabilities.json with bin/CAP_* presence.
286
+ # Refresh ~/.pwn/capabilities.json with bin/CAP_* and host-native-library presence; no device discovery.
262
287
  #{self}.manifest(
263
288
  refresh: 'optional - unused reserved refresh flag'
264
289
  )
@@ -138,7 +138,7 @@ module PWN
138
138
  File.rename(temp, path)
139
139
  end
140
140
  result = JSON.parse(File.read(path), symbolize_names: true).merge(path: path)
141
- result[:loot] = harvest_loot(assets: result[:assets], engagement_id: engagement)
141
+ result[:loot] = harvest_loot(assets: result[:assets], engagement_id: engagement, yubikey: opts[:yubikey])
142
142
  if opts[:ingest]
143
143
  begin
144
144
  ingestor = opts[:ingestor] || PWN::AI::Context.method(:ingest)
@@ -285,6 +285,7 @@ module PWN
285
285
  source: 'recon',
286
286
  where: opts[:where] || "banner:#{obs[:port] || asset[:port]}",
287
287
  engagement: eng,
288
+ yubikey: opts[:yubikey],
288
289
  finding_id: opts[:finding_id]
289
290
  )
290
291
  end
@@ -354,6 +355,7 @@ module PWN
354
355
  modules: 'optional - Array nmap/banner/tls/subfinder/nuclei; defaults nmap/banner',
355
356
  ports: 'optional - Array of integer ports; defaults 80 and 443',
356
357
  engagement_id: 'optional - simple identifier; defaults default',
358
+ yubikey: 'optional - hardware selector Hash for harvested loot; device failure raises without software fallback',
357
359
  root: 'optional - engagements directory; defaults ~/.pwn/engagements',
358
360
  timeout: 'optional - per-operation timeout seconds; defaults 15, maximum 300',
359
361
  ingest: 'optional - invoke Context.ingest after persistence; defaults false',
@@ -366,6 +368,7 @@ module PWN
366
368
  assets: 'required - Array of recon asset hashes with address and observations',
367
369
  engagement_id: 'optional - engagement id scoping the loot file (defaults to default)',
368
370
  engagement: 'optional - alias for engagement_id',
371
+ yubikey: 'optional - hardware selector Hash forwarded to Vault.ingest for captured secrets',
369
372
  where: 'optional - provenance path overlay when observation port is missing',
370
373
  finding_id: 'optional - finding id linked onto harvested secrets'
371
374
  )