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.
- checksums.yaml +4 -4
- data/.ruby-version +1 -1
- data/Gemfile +8 -8
- data/README.md +37 -0
- data/Rakefile +19 -1
- data/bin/pwn +2 -1
- data/bin/pwn_setup +1 -1
- data/bin/pwn_web_cache_deception +35 -7
- data/build_gem.sh +1 -1
- data/documentation/AI-Integration.md +7 -5
- data/documentation/Agent-Tool-Registry.md +2 -2
- data/documentation/Backup-Retention.md +82 -0
- data/documentation/Configuration.md +10 -6
- data/documentation/Contributing.md +1 -1
- data/documentation/Cron.md +64 -35
- data/documentation/Diagrams.md +2 -2
- data/documentation/General-PWN-Usage.md +4 -3
- data/documentation/Home.md +6 -5
- data/documentation/How-PWN-Works.md +7 -6
- data/documentation/Installation.md +254 -10
- data/documentation/Mission-Cancellation.md +134 -0
- data/documentation/Operability.md +17 -0
- data/documentation/Persistence.md +8 -6
- data/documentation/Plugin-Degradation.md +12 -0
- data/documentation/Plugins.md +3 -2
- data/documentation/Reinforcement-Learning.md +5 -6
- data/documentation/Runtime-Source-Protection.md +162 -0
- data/documentation/Transparent-Browser.md +11 -0
- data/documentation/Troubleshooting.md +3 -2
- data/documentation/What-is-PWN.md +2 -2
- data/documentation/YubiKey-Vaults.md +183 -0
- data/documentation/YubiKey.md +339 -0
- data/documentation/diagrams/cron-scheduling.svg +130 -128
- data/documentation/diagrams/dot/cron-scheduling.dot +5 -5
- data/documentation/diagrams/dot/persistence-filesystem.dot +5 -3
- data/documentation/diagrams/dot/plugin-ecosystem.dot +2 -2
- data/documentation/diagrams/persistence-filesystem.svg +172 -142
- data/documentation/diagrams/plugin-ecosystem.svg +64 -64
- data/documentation/pwn-ai-Agent.md +10 -4
- data/etc/default_cron/session_cleanup.rb +1245 -22
- data/etc/default_skills/pwn/ai/agent/mission_namespace/SKILL.md +21 -0
- data/etc/default_skills/pwn/ai/agent/tools/browser_act/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/tools/loot_query/SKILL.md +27 -26
- data/etc/default_skills/pwn/ai/agent/tools/recon_run/SKILL.md +25 -15
- data/etc/default_skills/pwn/ai/agent/tools/workspace_tool/SKILL.md +24 -0
- data/etc/default_skills/pwn/ai/server_tools/SKILL.md +25 -0
- data/etc/default_skills/pwn/backups/SKILL.md +54 -0
- data/etc/default_skills/pwn/plugins/recon/SKILL.md +12 -1
- data/etc/default_skills/pwn/plugins/yubikey/SKILL.md +157 -0
- data/etc/default_skills/pwn/plugins/yubikey/fido/SKILL.md +189 -0
- data/etc/default_skills/pwn/plugins/yubikey/hsmauth/SKILL.md +83 -0
- data/etc/default_skills/pwn/plugins/yubikey/management/SKILL.md +80 -0
- data/etc/default_skills/pwn/plugins/yubikey/oath/SKILL.md +115 -0
- data/etc/default_skills/pwn/plugins/yubikey/openpgp/SKILL.md +144 -0
- data/etc/default_skills/pwn/plugins/yubikey/otp/SKILL.md +97 -0
- data/etc/default_skills/pwn/plugins/yubikey/piv/SKILL.md +110 -0
- data/etc/default_skills/pwn/plugins/yubikey/protocol/SKILL.md +21 -0
- data/etc/default_skills/pwn/plugins/yubikey/security_domain/SKILL.md +75 -0
- data/etc/default_skills/pwn/plugins/yubikey/security_domain/scp/SKILL.md +46 -0
- data/etc/default_skills/pwn/plugins/yubikey/transport/SKILL.md +98 -0
- data/etc/default_skills/pwn/redaction/SKILL.md +13 -1
- data/lib/pwn/ai/agent/dispatch.rb +26 -15
- data/lib/pwn/ai/agent/extrospection.rb +6 -7
- data/lib/pwn/ai/agent/learning.rb +62 -25
- data/lib/pwn/ai/agent/loop.rb +17 -1
- data/lib/pwn/ai/agent/mission_namespace.rb +331 -0
- data/lib/pwn/ai/agent/policy.rb +5 -0
- data/lib/pwn/ai/agent/policy_evaluation.rb +8 -8
- data/lib/pwn/ai/agent/prompt_builder.rb +13 -2
- data/lib/pwn/ai/agent/request_runtime.rb +122 -18
- data/lib/pwn/ai/agent/reward.rb +5 -3
- data/lib/pwn/ai/agent/skill_consolidation.rb +3 -2
- data/lib/pwn/ai/agent/skill_review.rb +9 -5
- data/lib/pwn/ai/agent/swarm.rb +35 -16
- data/lib/pwn/ai/agent/tools/browser_act.rb +32 -0
- data/lib/pwn/ai/agent/tools/loot_query.rb +11 -1
- data/lib/pwn/ai/agent/tools/recon_run.rb +11 -1
- data/lib/pwn/ai/agent/tools/workspace_tool.rb +29 -0
- data/lib/pwn/ai/agent/turn_finalizer.rb +6 -2
- data/lib/pwn/ai/agent.rb +1 -0
- data/lib/pwn/ai/anthropic.rb +8 -33
- data/lib/pwn/ai/cli.rb +34 -5
- data/lib/pwn/ai/gemini.rb +1 -0
- data/lib/pwn/ai/grok.rb +39 -28
- data/lib/pwn/ai/ollama.rb +4 -2
- data/lib/pwn/ai/open_ai.rb +11 -32
- data/lib/pwn/ai/open_web_ui.rb +62 -4
- data/lib/pwn/ai/router.rb +53 -3
- data/lib/pwn/ai/server_tools.rb +231 -0
- data/lib/pwn/ai.rb +1 -0
- data/lib/pwn/backups.rb +347 -0
- data/lib/pwn/config.rb +70 -39
- data/lib/pwn/cron.rb +120 -49
- data/lib/pwn/migrate.rb +150 -68
- data/lib/pwn/module_skills.rb +7 -0
- data/lib/pwn/plugins/ai_sandbox.rb +538 -1
- data/lib/pwn/plugins/authentication_helper.rb +74 -4
- data/lib/pwn/plugins/basic_auth.rb +2 -1
- data/lib/pwn/plugins/binary_parser.rb +5 -4
- data/lib/pwn/plugins/ghidra.rb +4 -3
- data/lib/pwn/plugins/google_workspace.rb +4 -21
- data/lib/pwn/plugins/jobs.rb +1 -1
- data/lib/pwn/plugins/log.rb +21 -3
- data/lib/pwn/plugins/preflight_checker.rb +34 -9
- data/lib/pwn/plugins/recon.rb +4 -1
- data/lib/pwn/plugins/repl/ai.rb +305 -27
- data/lib/pwn/plugins/repl/ai_console.rb +273 -89
- data/lib/pwn/plugins/repl/ai_swarm.rb +181 -62
- data/lib/pwn/plugins/repl/mesh.rb +3 -16
- data/lib/pwn/plugins/repl/vault.rb +4 -15
- data/lib/pwn/plugins/repl.rb +1 -216
- data/lib/pwn/plugins/sandbox/driver.rb +4 -0
- data/lib/pwn/plugins/transparent_browser.rb +215 -0
- data/lib/pwn/plugins/vault.rb +412 -135
- data/lib/pwn/plugins/yubikey/fido.rb +1288 -0
- data/lib/pwn/plugins/yubikey/hsmauth.rb +439 -0
- data/lib/pwn/plugins/yubikey/management.rb +686 -0
- data/lib/pwn/plugins/yubikey/oath.rb +766 -0
- data/lib/pwn/plugins/yubikey/openpgp.rb +1063 -0
- data/lib/pwn/plugins/yubikey/otp.rb +897 -0
- data/lib/pwn/plugins/yubikey/piv.rb +1315 -0
- data/lib/pwn/plugins/yubikey/protocol.rb +341 -0
- data/lib/pwn/plugins/yubikey/security_domain/scp.rb +482 -0
- data/lib/pwn/plugins/yubikey/security_domain.rb +464 -0
- data/lib/pwn/plugins/yubikey/transport.rb +1017 -0
- data/lib/pwn/plugins/yubikey.rb +844 -0
- data/lib/pwn/plugins.rb +1 -0
- data/lib/pwn/redaction.rb +69 -12
- data/lib/pwn/session_trace.rb +4 -0
- data/lib/pwn/sessions.rb +58 -16
- data/lib/pwn/setup.rb +123 -41
- data/lib/pwn/version.rb +1 -1
- data/lib/pwn/www/app_cobalt_io.rb +16 -5
- data/lib/pwn/www/bug_crowd.rb +16 -5
- data/lib/pwn/www/coinbase_pro.rb +16 -5
- data/lib/pwn/www/github.rb +30 -14
- data/lib/pwn/www/paypal.rb +15 -4
- data/lib/pwn/www/synack.rb +16 -5
- data/lib/pwn/www/twitter.rb +17 -6
- data/lib/pwn.rb +1 -0
- data/scripts/build_gate.rb +58 -0
- data/spec/documentation/installation_md_spec.rb +1 -1
- data/spec/fixtures/yubikey/apdu_frames.json +162 -0
- data/spec/integration/authentication_helper_oath_spec.rb +117 -0
- data/spec/integration/build_gate_spec.rb +73 -0
- data/spec/integration/controller_spill_spec.rb +48 -0
- data/spec/integration/harness_process_spec.rb +97 -0
- data/spec/integration/mission_cancel_spec.rb +79 -0
- data/spec/integration/mission_curses_pty_spec.rb +44 -0
- data/spec/integration/persistence_roundtrip_spec.rb +1 -7
- data/spec/integration/readme_security_spec.rb +47 -0
- data/spec/integration/web_cache_deception_yubikey_spec.rb +159 -0
- data/spec/lib/pwn/ai/agent/dispatch_spec.rb +6 -3
- data/spec/lib/pwn/ai/agent/dispatch_vault_safety_spec.rb +86 -0
- data/spec/lib/pwn/ai/agent/injection_guard_spec.rb +2 -0
- data/spec/lib/pwn/ai/agent/learning_spec.rb +4 -4
- data/spec/lib/pwn/ai/agent/loop_artifacts_spec.rb +5 -0
- data/spec/lib/pwn/ai/agent/loop_spec.rb +104 -29
- data/spec/lib/pwn/ai/agent/mission_namespace_spec.rb +138 -0
- data/spec/lib/pwn/ai/agent/policy_spec.rb +27 -0
- data/spec/lib/pwn/ai/agent/request_runtime_spec.rb +48 -0
- data/spec/lib/pwn/ai/agent/shell_search_effect_spec.rb +44 -0
- data/spec/lib/pwn/ai/agent/solve_spec.rb +6 -4
- data/spec/lib/pwn/ai/agent/tools/browser_act_spec.rb +44 -0
- data/spec/lib/pwn/ai/agent/tools/loot_query_yubikey_spec.rb +55 -0
- data/spec/lib/pwn/ai/agent/tools/recon_run_yubikey_spec.rb +43 -0
- data/spec/lib/pwn/ai/agent/tools/workspace_tool_spec.rb +20 -0
- data/spec/lib/pwn/ai/agent/turn_finalizer_spec.rb +7 -0
- data/spec/lib/pwn/ai/anthropic_vault_enrollment_spec.rb +37 -0
- data/spec/lib/pwn/ai/cli_spec.rb +6 -9
- data/spec/lib/pwn/ai/oauth_vault_persistence_spec.rb +195 -0
- data/spec/lib/pwn/ai/open_ai_spec.rb +2 -2
- data/spec/lib/pwn/ai/open_web_ui_workspace_spec.rb +36 -0
- data/spec/lib/pwn/ai/router_spec.rb +31 -3
- data/spec/lib/pwn/ai/server_tools_spec.rb +39 -0
- data/spec/lib/pwn/backups_spec.rb +114 -0
- data/spec/lib/pwn/config_redaction_yubikey_spec.rb +28 -0
- data/spec/lib/pwn/config_yubikey_spec.rb +92 -0
- data/spec/lib/pwn/cron_spec.rb +113 -6
- data/spec/lib/pwn/migrate_spec.rb +108 -5
- data/spec/lib/pwn/migrate_yubikey_spec.rb +128 -0
- data/spec/lib/pwn/module_skills_spec.rb +8 -0
- data/spec/lib/pwn/plugins/ai_sandbox_spec.rb +230 -0
- data/spec/lib/pwn/plugins/authentication_helper_native_yubikey_spec.rb +40 -0
- data/spec/lib/pwn/plugins/authentication_helper_yubikey_spec.rb +90 -0
- data/spec/lib/pwn/plugins/capability_broker/daemon_spec.rb +1 -1
- data/spec/lib/pwn/plugins/google_workspace_spec.rb +17 -35
- data/spec/lib/pwn/plugins/log_spec.rb +7 -0
- data/spec/lib/pwn/plugins/log_yubikey_spec.rb +47 -0
- data/spec/lib/pwn/plugins/preflight_checker_native_yubikey_spec.rb +19 -0
- data/spec/lib/pwn/plugins/preflight_checker_yubikey_spec.rb +55 -0
- data/spec/lib/pwn/plugins/recon_yubikey_spec.rb +43 -0
- data/spec/lib/pwn/plugins/repl/ai_console_spec.rb +125 -78
- data/spec/lib/pwn/plugins/repl/ai_legacy_containment_spec.rb +200 -0
- data/spec/lib/pwn/plugins/repl/ai_swarm_spec.rb +30 -16
- data/spec/lib/pwn/plugins/repl/ai_vault_edit_yubikey_spec.rb +103 -0
- data/spec/lib/pwn/plugins/repl/ai_yubikey_spec.rb +48 -0
- data/spec/lib/pwn/plugins/repl/mesh_vault_persistence_spec.rb +149 -0
- data/spec/lib/pwn/plugins/repl/vault_yaml_serialization_spec.rb +11 -0
- data/spec/lib/pwn/plugins/repl/vault_yubikey_spec.rb +39 -0
- data/spec/lib/pwn/plugins/repl_spec.rb +56 -5
- data/spec/lib/pwn/plugins/transparent_browser_jev_spec.rb +46 -0
- data/spec/lib/pwn/plugins/vault_native_yubikey_spec.rb +29 -0
- data/spec/lib/pwn/plugins/vault_publication_safety_spec.rb +258 -0
- data/spec/lib/pwn/plugins/vault_store_safety_spec.rb +241 -0
- data/spec/lib/pwn/plugins/vault_yubikey_safety_spec.rb +168 -0
- data/spec/lib/pwn/plugins/vault_yubikey_spec.rb +128 -0
- data/spec/lib/pwn/plugins/vault_yubikey_vector_spec.rb +45 -0
- data/spec/lib/pwn/plugins/yubikey/fido_spec.rb +854 -0
- data/spec/lib/pwn/plugins/yubikey/hsmauth_spec.rb +235 -0
- data/spec/lib/pwn/plugins/yubikey/management_spec.rb +277 -0
- data/spec/lib/pwn/plugins/yubikey/oath_spec.rb +349 -0
- data/spec/lib/pwn/plugins/yubikey/openpgp_spec.rb +810 -0
- data/spec/lib/pwn/plugins/yubikey/otp_spec.rb +337 -0
- data/spec/lib/pwn/plugins/yubikey/piv_spec.rb +646 -0
- data/spec/lib/pwn/plugins/yubikey/protocol_secure_spec.rb +28 -0
- data/spec/lib/pwn/plugins/yubikey/protocol_spec.rb +149 -0
- data/spec/lib/pwn/plugins/yubikey/protocol_vectors_spec.rb +18 -0
- data/spec/lib/pwn/plugins/yubikey/security_domain/scp_spec.rb +256 -0
- data/spec/lib/pwn/plugins/yubikey/security_domain/support.rb +153 -0
- data/spec/lib/pwn/plugins/yubikey/security_domain_spec.rb +296 -0
- data/spec/lib/pwn/plugins/yubikey/transport_spec.rb +760 -0
- data/spec/lib/pwn/plugins/yubikey_security_spec.rb +260 -0
- data/spec/lib/pwn/plugins/yubikey_spec.rb +209 -0
- data/spec/lib/pwn/plugins/yubikey_terminal_spec.rb +147 -0
- data/spec/lib/pwn/redaction_spec.rb +13 -10
- data/spec/lib/pwn/redaction_yubikey_spec.rb +61 -0
- data/spec/lib/pwn/session_cleanup_concurrency_spec.rb +201 -0
- data/spec/lib/pwn/session_cleanup_default_spec.rb +167 -2
- data/spec/lib/pwn/session_cleanup_legacy_spec.rb +101 -0
- data/spec/lib/pwn/session_cleanup_payload_spec.rb +335 -2
- data/spec/lib/pwn/session_cleanup_recovery_spec.rb +403 -0
- data/spec/lib/pwn/sessions_spec.rb +112 -0
- data/spec/lib/pwn/setup_native_yubikey_spec.rb +66 -0
- data/spec/lib/pwn/setup_spec.rb +3 -1
- data/spec/lib/pwn/setup_vault_yubikey_spec.rb +61 -0
- data/spec/lib/pwn/setup_yubikey_spec.rb +40 -0
- data/spec/lib/pwn/www/login_yubikey_spec.rb +273 -0
- data/spec/spec_helper.rb +15 -0
- data/spec/support/gate_failure_formatter.rb +14 -0
- data/spec/support/harness/cli_faults.rb +13 -9
- data/spec/support/harness/controller_spill.rb +26 -0
- data/spec/support/harness/controller_state.rb +54 -0
- data/spec/support/harness/evaluate.rb +12 -1
- data/spec/support/harness/experiments.rb +1 -1
- data/spec/support/harness/fake_provider.rb +22 -6
- data/spec/support/harness/fault_preload.rb +3 -1
- data/spec/support/harness/mission_console.rb +54 -0
- data/spec/support/harness/mission_curses_pty.rb +41 -0
- data/spec/support/harness/mission_dispatch.rb +30 -0
- data/spec/support/harness/mission_fixture_preload.rb +24 -0
- data/spec/support/harness/mission_job_lifetime.rb +25 -0
- data/spec/support/harness/mission_legacy.rb +25 -0
- data/spec/support/harness/mission_normal_exit.rb +34 -0
- data/spec/support/harness/mission_swarm.rb +23 -0
- data/spec/support/harness/mission_unsupported.rb +29 -0
- data/spec/support/harness/package_acceptance.rb +33 -5
- data/spec/support/harness/session_acceptance.rb +4 -4
- data/spec/support/harness/swarm_acceptance.rb +1 -1
- data/spec/support/harness/swarm_control.rb +44 -0
- data/spec/support/mission_namespace.rb +14 -0
- data/spec/support/mission_pipe_queue.rb +33 -0
- data/third_party/pwn_rdoc.jsonl +599 -15
- metadata +140 -17
data/lib/pwn/migrate.rb
CHANGED
|
@@ -4,6 +4,9 @@ require 'json'
|
|
|
4
4
|
require 'yaml'
|
|
5
5
|
require 'fileutils'
|
|
6
6
|
require 'time'
|
|
7
|
+
require 'digest'
|
|
8
|
+
require 'find'
|
|
9
|
+
require 'securerandom'
|
|
7
10
|
|
|
8
11
|
module PWN
|
|
9
12
|
# PWN::Migrate — the ~/.pwn state **doctor** and **auto-migrator**.
|
|
@@ -49,7 +52,7 @@ module PWN
|
|
|
49
52
|
# Bump this whenever the shape of any file under ~/.pwn changes in a
|
|
50
53
|
# way that requires a one-time transform. Add the transform as an
|
|
51
54
|
# entry in MIGRATIONS keyed by the NEW schema number.
|
|
52
|
-
SCHEMA_VERSION =
|
|
55
|
+
SCHEMA_VERSION = 12
|
|
53
56
|
|
|
54
57
|
OK = "\e[32mok\e[0m"
|
|
55
58
|
BAD = "\e[31mFAIL\e[0m"
|
|
@@ -81,7 +84,7 @@ module PWN
|
|
|
81
84
|
|
|
82
85
|
enc = PWN::Plugins::Vault.file_encrypted?(file: p)
|
|
83
86
|
# Shape (missing keys vs env_template) is checked separately in
|
|
84
|
-
# .vault_drift because it requires the
|
|
87
|
+
# .vault_drift because it requires access to the legacy key or YubiKey.
|
|
85
88
|
{ ok: enc, why: enc ? nil : 'not encrypted (run PWN::Plugins::Vault.create)' }
|
|
86
89
|
}
|
|
87
90
|
},
|
|
@@ -242,33 +245,41 @@ module PWN
|
|
|
242
245
|
File.write(path, YAML.dump(raw))
|
|
243
246
|
io.puts " · agents.yml added default model to #{patched} personas"
|
|
244
247
|
},
|
|
245
|
-
4 => lambda { |_root, io|
|
|
248
|
+
4 => lambda { |_root, io, vault_opts = {}|
|
|
246
249
|
io.puts ' · pwn.yaml ai_sandbox, ai_router, model_routes task classes'
|
|
247
|
-
PWN::Migrate.backfill_vault(io: io)
|
|
250
|
+
PWN::Migrate.backfill_vault(vault_opts.merge(io: io))
|
|
248
251
|
},
|
|
249
|
-
5 => lambda { |_root, io|
|
|
252
|
+
5 => lambda { |_root, io, vault_opts = {}|
|
|
250
253
|
io.puts ' · pwn.yaml ai.agent.skill_review'
|
|
251
|
-
PWN::Migrate.backfill_vault(io: io)
|
|
254
|
+
PWN::Migrate.backfill_vault(vault_opts.merge(io: io))
|
|
252
255
|
},
|
|
253
|
-
6 => lambda { |_root, io|
|
|
256
|
+
6 => lambda { |_root, io, vault_opts = {}|
|
|
254
257
|
io.puts ' · pwn.yaml ai.tui.theme'
|
|
255
|
-
PWN::Migrate.backfill_vault(io: io)
|
|
258
|
+
PWN::Migrate.backfill_vault(vault_opts.merge(io: io))
|
|
256
259
|
},
|
|
257
|
-
7 => lambda { |_root, io|
|
|
260
|
+
7 => lambda { |_root, io, vault_opts = {}|
|
|
258
261
|
io.puts ' · pwn.yaml ai.tui.theme.title'
|
|
259
|
-
PWN::Migrate.backfill_vault(io: io)
|
|
262
|
+
PWN::Migrate.backfill_vault(vault_opts.merge(io: io))
|
|
260
263
|
},
|
|
261
264
|
8 => lambda { |_root, io|
|
|
262
265
|
io.puts ' · guarded session_cleanup default cron payload and job'
|
|
263
266
|
PWN::Cron.install_defaults
|
|
264
267
|
},
|
|
265
|
-
9 => lambda { |_root, io|
|
|
268
|
+
9 => lambda { |_root, io, vault_opts = {}|
|
|
266
269
|
io.puts ' · pwn.yaml plugins.meshtastic.channel short_name'
|
|
267
|
-
PWN::Migrate.backfill_vault(io: io)
|
|
270
|
+
PWN::Migrate.backfill_vault(vault_opts.merge(io: io))
|
|
268
271
|
},
|
|
269
|
-
10 => lambda { |_root, io|
|
|
272
|
+
10 => lambda { |_root, io, vault_opts = {}|
|
|
270
273
|
io.puts ' · pwn.yaml ai reasoning_effort for anthropic, gemini, ollama, openwebui'
|
|
271
|
-
PWN::Migrate.backfill_vault(io: io)
|
|
274
|
+
PWN::Migrate.backfill_vault(vault_opts.merge(io: io))
|
|
275
|
+
},
|
|
276
|
+
11 => lambda { |_root, io|
|
|
277
|
+
io.puts ' · versioned housekeeping policy and managed session_cleanup helper'
|
|
278
|
+
PWN::Cron.install_defaults
|
|
279
|
+
},
|
|
280
|
+
12 => lambda { |root, io|
|
|
281
|
+
io.puts ' · global seven-folder recovery policy; legacy relocation remains explicit'
|
|
282
|
+
PWN::Backups.configure(root: File.join(root, 'backup'))
|
|
272
283
|
}
|
|
273
284
|
}.freeze
|
|
274
285
|
|
|
@@ -302,14 +313,15 @@ module PWN
|
|
|
302
313
|
end
|
|
303
314
|
|
|
304
315
|
# Supported Method Parameters::
|
|
305
|
-
# rows = PWN::Migrate.status
|
|
316
|
+
# rows = PWN::Migrate.status(pwn_env_path: 'optional - Selected configuration vault path')
|
|
306
317
|
#
|
|
307
318
|
# Per-file compatibility report — the machine-readable half of
|
|
308
319
|
# `.check`. Never writes. Never raises (each verifier is rescued).
|
|
309
320
|
|
|
310
|
-
public_class_method def self.status
|
|
321
|
+
public_class_method def self.status(opts = {})
|
|
322
|
+
selected_vault = opts[:pwn_env_path] || vault_paths[:file]
|
|
311
323
|
files = STATE_FILES.map do |rel, meta|
|
|
312
|
-
path = File.join(PWN_ROOT, rel)
|
|
324
|
+
path = meta[:kind] == :vault ? selected_vault : File.join(PWN_ROOT, rel)
|
|
313
325
|
exists = meta[:kind] == :dir ? File.directory?(path) : File.file?(path)
|
|
314
326
|
raw = exists && meta[:kind] != :dir ? safe_read(path: path) : nil
|
|
315
327
|
v = verify_one(path: path, raw: raw, meta: meta)
|
|
@@ -378,19 +390,25 @@ module PWN
|
|
|
378
390
|
# and decryptor available) 5. stamp .schema.
|
|
379
391
|
|
|
380
392
|
public_class_method def self.run(opts = {})
|
|
393
|
+
return PWN::Backups.transaction(root: BACKUP_ROOT) { run(opts) } unless opts[:dry_run] || PWN::Backups.active?
|
|
394
|
+
|
|
381
395
|
fix = opts[:fix] ? true : false
|
|
382
396
|
dry_run = opts[:dry_run] ? true : false
|
|
383
397
|
backup = opts.fetch(:backup, true)
|
|
384
398
|
io = opts[:io] || $stdout
|
|
385
399
|
|
|
386
|
-
FileUtils.mkdir_p(PWN_ROOT)
|
|
387
|
-
before = status
|
|
400
|
+
FileUtils.mkdir_p(PWN_ROOT) unless dry_run
|
|
401
|
+
before = status(pwn_env_path: opts[:pwn_env_path])
|
|
388
402
|
io.puts "PWN::Migrate — pwn #{before[:pwn_version]} · ~/.pwn schema " \
|
|
389
403
|
"#{before[:schema_installed]} → #{before[:schema_current]}" \
|
|
390
404
|
"#{' (dry-run)' if dry_run}"
|
|
391
405
|
|
|
406
|
+
vault_opts = { key: opts[:key], iv: opts[:iv], yubikey: opts[:yubikey], pwn_env_path: opts[:pwn_env_path], pwn_dec_path: opts[:pwn_dec_path] }
|
|
407
|
+
drift = fix ? vault_drift(vault_opts) : nil
|
|
408
|
+
changes = before[:schema_installed] < SCHEMA_VERSION ||
|
|
409
|
+
(fix && (!before[:incompatible].empty? || (drift && !drift[:missing].empty?)))
|
|
392
410
|
bpath = nil
|
|
393
|
-
if backup && !dry_run && File.directory?(PWN_ROOT)
|
|
411
|
+
if backup && changes && !dry_run && File.directory?(PWN_ROOT)
|
|
394
412
|
bpath = backup!
|
|
395
413
|
io.puts " backup : #{bpath}"
|
|
396
414
|
end
|
|
@@ -402,7 +420,9 @@ module PWN
|
|
|
402
420
|
next unless m
|
|
403
421
|
|
|
404
422
|
io.puts " migrate : v#{n - 1} → v#{n}"
|
|
405
|
-
|
|
423
|
+
unless dry_run
|
|
424
|
+
m.arity == 2 ? m.call(PWN_ROOT, io) : m.call(PWN_ROOT, io, vault_opts)
|
|
425
|
+
end
|
|
406
426
|
applied << n
|
|
407
427
|
end
|
|
408
428
|
|
|
@@ -418,13 +438,13 @@ module PWN
|
|
|
418
438
|
|
|
419
439
|
# pwn.yaml key backfill runs even when the vault verifier passed —
|
|
420
440
|
# "encrypted" ≠ "has every key this release added".
|
|
421
|
-
vd = backfill_vault(
|
|
441
|
+
vd = backfill_vault(vault_opts.merge(dry_run: dry_run, io: io))
|
|
422
442
|
fixed << vd if vd && vd[:added].to_i.positive?
|
|
423
443
|
end
|
|
424
444
|
|
|
425
|
-
stamp!
|
|
445
|
+
stamp! if !dry_run && changes
|
|
426
446
|
|
|
427
|
-
after = status
|
|
447
|
+
after = status(pwn_env_path: opts[:pwn_env_path])
|
|
428
448
|
io.puts
|
|
429
449
|
io.puts " result : #{after[:incompatible].length} incompatible remaining " \
|
|
430
450
|
"(was #{before[:incompatible].length}) · schema now #{after[:schema_installed]}"
|
|
@@ -442,16 +462,15 @@ module PWN
|
|
|
442
462
|
#
|
|
443
463
|
# Deep-diff the user's decrypted pwn.yaml against the current
|
|
444
464
|
# PWN::Config.env_template. → { missing: [dotted.paths], user: {…} }
|
|
445
|
-
# Returns nil when
|
|
465
|
+
# Returns nil when vault access is unavailable (never prompts).
|
|
446
466
|
|
|
447
467
|
public_class_method def self.vault_drift(opts = {})
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
yaml = File.join(PWN_ROOT, 'pwn.yaml')
|
|
468
|
+
paths = vault_paths(opts)
|
|
469
|
+
yaml = paths[:file]
|
|
452
470
|
return nil unless File.file?(yaml) && PWN::Plugins::Vault.file_encrypted?(file: yaml)
|
|
453
471
|
|
|
454
|
-
|
|
472
|
+
access = PWN::Plugins::Vault.access_options(paths.merge(key: opts[:key], iv: opts[:iv], yubikey: opts[:yubikey]))
|
|
473
|
+
user = PWN::Plugins::Vault.dump(access.merge(file: yaml))
|
|
455
474
|
tmpl = env_template
|
|
456
475
|
{ missing: missing_paths(tmpl: tmpl, user: user), user: user, template: tmpl }
|
|
457
476
|
rescue StandardError
|
|
@@ -464,7 +483,7 @@ module PWN
|
|
|
464
483
|
# )
|
|
465
484
|
#
|
|
466
485
|
# Decrypt → deep-merge template UNDER user (never overwrites) →
|
|
467
|
-
# re-encrypt with the SAME key/iv
|
|
486
|
+
# re-encrypt with the SAME legacy key/iv or hardware selector. This fixes the
|
|
468
487
|
# upgrade break: `NoMethodError: undefined method '[]' for nil` when
|
|
469
488
|
# a release added `env[:ai][:new_engine][:…]` and the user's vault
|
|
470
489
|
# predates it.
|
|
@@ -472,9 +491,9 @@ module PWN
|
|
|
472
491
|
public_class_method def self.backfill_vault(opts = {})
|
|
473
492
|
io = opts[:io] || $stdout
|
|
474
493
|
dry_run = opts[:dry_run] ? true : false
|
|
475
|
-
d = vault_drift(
|
|
494
|
+
d = vault_drift(opts)
|
|
476
495
|
unless d
|
|
477
|
-
io.puts ' vault : skipped (
|
|
496
|
+
io.puts ' vault : skipped (vault access unavailable — check YubiKey or legacy decryptor/key/IV)'
|
|
478
497
|
return nil
|
|
479
498
|
end
|
|
480
499
|
if d[:missing].empty?
|
|
@@ -487,13 +506,10 @@ module PWN
|
|
|
487
506
|
io.puts " … (+#{d[:missing].length - 8} more)" if d[:missing].length > 8
|
|
488
507
|
return { rel: 'pwn.yaml', added: d[:missing].length, missing: d[:missing], dry_run: true } if dry_run
|
|
489
508
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
File.write(yaml, YAML.dump(merged).gsub(/^(\s*):/, '\1'))
|
|
495
|
-
File.chmod(0o600, yaml)
|
|
496
|
-
PWN::Plugins::Vault.encrypt(file: yaml, key: key, iv: iv)
|
|
509
|
+
access = vault_paths(opts).merge(key: opts[:key], iv: opts[:iv], yubikey: opts[:yubikey])
|
|
510
|
+
PWN::Plugins::Vault.update(access) do |user|
|
|
511
|
+
user.replace(deep_defaults(user: user, tmpl: d[:template]))
|
|
512
|
+
end
|
|
497
513
|
|
|
498
514
|
{ rel: 'pwn.yaml', added: d[:missing].length, missing: d[:missing] }
|
|
499
515
|
rescue StandardError => e
|
|
@@ -612,19 +628,74 @@ module PWN
|
|
|
612
628
|
"dropped #{bad} bad line(s), kept #{good.length}"
|
|
613
629
|
end
|
|
614
630
|
|
|
631
|
+
# Supported Method Parameters::
|
|
632
|
+
# PWN::Migrate.prune_backups(dry_run: true)
|
|
633
|
+
# Retention now runs only at shared producer admission, never separately.
|
|
634
|
+
public_class_method def self.prune_backups(opts = {})
|
|
635
|
+
{ eligible: [], deleted: [], retained: [], incomplete: false,
|
|
636
|
+
dry_run: opts.fetch(:dry_run, true), keep: PWN::Backups::LIMIT,
|
|
637
|
+
scope: 'global', retention: 'shared_producer_admission' }
|
|
638
|
+
end
|
|
639
|
+
|
|
640
|
+
private_class_method def self.backup_input_names
|
|
641
|
+
(STATE_FILES.keys.map { |rel| rel.delete_suffix('/') } + %w[.schema pwn.yaml.decryptor]).uniq.reject { |rel| %w[sessions swarm].include?(rel.split('/').first) }
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
private_class_method def self.backup_inventory(opts = {})
|
|
645
|
+
path = opts[:path]
|
|
646
|
+
admission = opts[:admission]
|
|
647
|
+
names = opts[:names]
|
|
648
|
+
raise IOError, 'Unsafe backup directory' unless File.realpath(path) == path && File.directory?(path)
|
|
649
|
+
|
|
650
|
+
rows = {}
|
|
651
|
+
Find.find(path) do |file|
|
|
652
|
+
next if file == path
|
|
653
|
+
|
|
654
|
+
rel = file.delete_prefix("#{path}/")
|
|
655
|
+
Find.prune if names && names.none? { |name| rel == name || rel.start_with?("#{name}/") || name.start_with?("#{rel}/") }
|
|
656
|
+
stat = File.lstat(file)
|
|
657
|
+
raise IOError, 'Unsafe backup entry' unless (stat.file? || stat.directory?) && stat.uid == Process.uid
|
|
658
|
+
next if names && names.none? { |name| rel == name || rel.start_with?("#{name}/") }
|
|
659
|
+
raise IOError, 'Backup work budget exhausted' if admission && !admission.call(stat.file? ? stat.size + 1 : 0)
|
|
660
|
+
|
|
661
|
+
row = [stat.ftype, stat.mode, stat.uid, stat.gid]
|
|
662
|
+
if stat.file?
|
|
663
|
+
row += [stat.size, Digest::SHA256.file(file).hexdigest]
|
|
664
|
+
after = File.lstat(file)
|
|
665
|
+
raise IOError, 'Backup changed while hashing' unless [stat.ino, stat.size, stat.mtime, stat.ctime] == [after.ino, after.size, after.mtime, after.ctime]
|
|
666
|
+
end
|
|
667
|
+
rows[rel] = row
|
|
668
|
+
end
|
|
669
|
+
rows
|
|
670
|
+
end
|
|
671
|
+
|
|
615
672
|
private_class_method def self.backup!
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
673
|
+
PWN::Backups.transaction(root: BACKUP_ROOT) do
|
|
674
|
+
dst = PWN::Backups.directory(label: 'migration')
|
|
675
|
+
names = backup_input_names
|
|
676
|
+
inventory = backup_inventory(path: PWN_ROOT, names: names)
|
|
677
|
+
names.each do |name|
|
|
678
|
+
src = File.join(PWN_ROOT, name)
|
|
679
|
+
next unless File.exist?(src)
|
|
680
|
+
|
|
681
|
+
target = File.join(dst, name)
|
|
682
|
+
FileUtils.mkdir_p(File.dirname(target), mode: 0o700)
|
|
683
|
+
FileUtils.cp_r(src, target, preserve: true)
|
|
684
|
+
end
|
|
685
|
+
raise IOError, 'Migration input changed during backup' unless inventory == backup_inventory(path: PWN_ROOT, names: names)
|
|
686
|
+
raise IOError, 'Backup verification failed' unless inventory == backup_inventory(path: dst, names: names)
|
|
621
687
|
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
688
|
+
inventory.each do |name, row|
|
|
689
|
+
File.open(File.join(dst, name), File::RDONLY, &:fsync) if row.first == 'file'
|
|
690
|
+
end
|
|
691
|
+
File.open(File.join(dst, '.snapshot-manifest.json'), File::WRONLY | File::CREAT | File::EXCL, 0o600) do |file|
|
|
692
|
+
file.write(JSON.generate(version: 1, complete: true, scope: names.sort, files: inventory))
|
|
693
|
+
file.flush
|
|
694
|
+
file.fsync
|
|
695
|
+
end
|
|
696
|
+
File.open(dst, File::RDONLY, &:fsync)
|
|
697
|
+
dst
|
|
626
698
|
end
|
|
627
|
-
dst
|
|
628
699
|
end
|
|
629
700
|
|
|
630
701
|
private_class_method def self.stamp!
|
|
@@ -640,18 +711,12 @@ module PWN
|
|
|
640
711
|
SCHEMA_VERSION
|
|
641
712
|
end
|
|
642
713
|
|
|
643
|
-
private_class_method def self.
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
return [nil, nil] unless File.file?(dec)
|
|
650
|
-
|
|
651
|
-
d = YAML.load_file(dec, symbolize_names: true)
|
|
652
|
-
[d[:key], d[:iv]]
|
|
653
|
-
rescue StandardError
|
|
654
|
-
[nil, nil]
|
|
714
|
+
private_class_method def self.vault_paths(opts = {})
|
|
715
|
+
driver = defined?(PWN::Env) && PWN::Env.is_a?(Hash) ? PWN::Env[:driver_opts] : nil
|
|
716
|
+
driver = {} unless driver.is_a?(Hash)
|
|
717
|
+
file = opts[:pwn_env_path] || driver[:pwn_env_path] || File.join(PWN_ROOT, 'pwn.yaml')
|
|
718
|
+
decryptor_file = opts[:pwn_dec_path] || driver[:pwn_dec_path] || "#{file}.decryptor"
|
|
719
|
+
{ file: file, decryptor_file: decryptor_file }
|
|
655
720
|
end
|
|
656
721
|
|
|
657
722
|
private_class_method def self.safe_read(opts = {})
|
|
@@ -743,7 +808,9 @@ module PWN
|
|
|
743
808
|
#{self}.needed?
|
|
744
809
|
|
|
745
810
|
# Run status and return its result
|
|
746
|
-
#{self}.status
|
|
811
|
+
#{self}.status(
|
|
812
|
+
pwn_env_path: 'optional - Vault path; defaults to driver options then ~/.pwn/pwn.yaml'
|
|
813
|
+
)
|
|
747
814
|
|
|
748
815
|
# Human-readable ~/.pwn state doctor. Read-only. Called from
|
|
749
816
|
#{self}.check(
|
|
@@ -757,20 +824,35 @@ module PWN
|
|
|
757
824
|
dry_run: 'optional - report what WOULD happen, write nothing (default false)',
|
|
758
825
|
key: 'optional - vault key (default: read ~/.pwn/pwn.yaml.decryptor)',
|
|
759
826
|
iv: 'optional - vault iv (default: read ~/.pwn/pwn.yaml.decryptor)',
|
|
827
|
+
yubikey: 'optional - Hardware selector overrides: slot, serial, reader, device_path, connection, timeout',
|
|
828
|
+
pwn_env_path: 'optional - Vault path; defaults to driver options then ~/.pwn/pwn.yaml',
|
|
829
|
+
pwn_dec_path: 'optional - Legacy decryptor path; defaults to driver options then vault sidecar',
|
|
760
830
|
io: 'optional - IO to write to (default $stdout)'
|
|
761
831
|
)
|
|
762
832
|
|
|
763
833
|
# Run vault drift and return its result
|
|
764
834
|
#{self}.vault_drift(
|
|
765
|
-
key: 'optional - vault key
|
|
835
|
+
key: 'optional - Legacy vault key override',
|
|
836
|
+
iv: 'optional - Legacy vault IV override',
|
|
837
|
+
yubikey: 'optional - Hardware selector overrides: slot, serial, reader, device_path, connection, timeout',
|
|
838
|
+
pwn_env_path: 'optional - Vault path; defaults to driver options then ~/.pwn/pwn.yaml',
|
|
839
|
+
pwn_dec_path: 'optional - Legacy decryptor path; hardware vaults need no sidecar'
|
|
766
840
|
)
|
|
767
841
|
|
|
768
842
|
# Run backfill vault and return its result
|
|
769
843
|
#{self}.backfill_vault(
|
|
770
|
-
key: 'optional -
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
844
|
+
key: 'optional - Legacy vault key override',
|
|
845
|
+
iv: 'optional - Legacy vault IV override',
|
|
846
|
+
yubikey: 'optional - Hardware selector overrides: slot, serial, reader, device_path, connection, timeout',
|
|
847
|
+
pwn_env_path: 'optional - Vault path; defaults to driver options then ~/.pwn/pwn.yaml',
|
|
848
|
+
pwn_dec_path: 'optional - Legacy decryptor path; hardware vaults need no sidecar',
|
|
849
|
+
io: 'optional - Report output; defaults to $stdout',
|
|
850
|
+
dry_run: 'optional - Report missing keys without writing; defaults to false'
|
|
851
|
+
)
|
|
852
|
+
|
|
853
|
+
# Compatibility report; shared producer admission owns retention.
|
|
854
|
+
#{self}.prune_backups(
|
|
855
|
+
dry_run: 'optional - preview without writes, default true'
|
|
774
856
|
)
|
|
775
857
|
|
|
776
858
|
# Run env template and return its result
|
data/lib/pwn/module_skills.rb
CHANGED
|
@@ -273,6 +273,13 @@ module PWN
|
|
|
273
273
|
pending = []
|
|
274
274
|
next
|
|
275
275
|
end
|
|
276
|
+
if line =~ /^\s*class\s+([A-Z][\w:]*)/
|
|
277
|
+
ident = Regexp.last_match(1)
|
|
278
|
+
if snake(name: ident.split('::').last) == File.basename(src, '.rb')
|
|
279
|
+
const = ident.include?('::') ? ident : (stack + [ident]).join('::')
|
|
280
|
+
blurb = pending.join(' ').strip if blurb.empty?
|
|
281
|
+
end
|
|
282
|
+
end
|
|
276
283
|
pending = []
|
|
277
284
|
next if line =~ /^\s*private_class_method/
|
|
278
285
|
|