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.
- checksums.yaml +4 -4
- data/.ruby-version +1 -1
- data/Gemfile +6 -6
- data/README.md +37 -0
- data/bin/pwn +2 -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 +69 -15
- data/documentation/Configuration.md +10 -6
- data/documentation/Contributing.md +1 -1
- data/documentation/Cron.md +2 -2
- data/documentation/Diagrams.md +2 -2
- data/documentation/General-PWN-Usage.md +4 -3
- data/documentation/Harness-Evaluation.md +1 -1
- data/documentation/Home.md +6 -5
- data/documentation/How-PWN-Works.md +6 -5
- data/documentation/Installation.md +12 -9
- data/documentation/Mission-Cancellation.md +134 -0
- data/documentation/Operability.md +18 -0
- data/documentation/Persistence.md +7 -5
- data/documentation/Plugin-Degradation.md +14 -0
- data/documentation/Plugins.md +3 -2
- data/documentation/Policy-Benchmark.md +32 -33
- 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 +185 -0
- data/documentation/YubiKey.md +218 -0
- data/documentation/diagrams/cron-scheduling.svg +130 -130
- data/documentation/diagrams/dot/cron-scheduling.dot +3 -3
- data/documentation/diagrams/dot/persistence-filesystem.dot +5 -3
- data/documentation/diagrams/dot/plugin-ecosystem.dot +2 -2
- data/documentation/diagrams/dot/pwn-ai-feedback-learning-loop.dot +1 -1
- data/documentation/diagrams/dot/reinforcement-learning.dot +2 -2
- data/documentation/diagrams/persistence-filesystem.svg +172 -142
- data/documentation/diagrams/plugin-ecosystem.svg +64 -64
- data/documentation/diagrams/pwn-ai-feedback-learning-loop.svg +1 -1
- data/documentation/diagrams/reinforcement-learning.svg +89 -89
- data/documentation/pwn-ai-Agent.md +10 -4
- data/etc/default_cron/session_cleanup.rb +43 -27
- 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/learning.rb +14 -18
- 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_evaluation.rb +334 -36
- 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 +4 -0
- 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 +8 -1
- data/lib/pwn/ai/server_tools.rb +231 -0
- data/lib/pwn/ai.rb +1 -0
- data/lib/pwn/backups.rb +349 -0
- data/lib/pwn/config.rb +70 -39
- data/lib/pwn/migrate.rb +85 -280
- 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/google_workspace.rb +4 -21
- data/lib/pwn/plugins/jobs.rb +1 -1
- data/lib/pwn/plugins/log.rb +3 -1
- data/lib/pwn/plugins/preflight_checker.rb +34 -9
- data/lib/pwn/plugins/recon.rb +4 -1
- data/lib/pwn/plugins/repl/ai.rb +293 -26
- data/lib/pwn/plugins/repl/ai_console.rb +268 -88
- data/lib/pwn/plugins/repl/ai_swarm.rb +177 -58
- data/lib/pwn/plugins/repl/mesh.rb +14 -19
- data/lib/pwn/plugins/repl/vault.rb +4 -15
- data/lib/pwn/plugins/repl.rb +9 -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/setup.rb +114 -24
- 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/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/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/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_evaluation_spec.rb +10 -23
- 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 +8 -16
- 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 +1 -1
- data/spec/lib/pwn/ai/server_tools_spec.rb +39 -0
- data/spec/lib/pwn/backups_spec.rb +129 -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/migrate_spec.rb +29 -81
- 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_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 +110 -75
- data/spec/lib/pwn/plugins/repl/ai_legacy_containment_spec.rb +200 -0
- data/spec/lib/pwn/plugins/repl/ai_swarm_spec.rb +19 -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_mesh_layout_spec.rb +2 -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_yubikey_spec.rb +61 -0
- data/spec/lib/pwn/session_cleanup_default_spec.rb +7 -5
- data/spec/lib/pwn/session_cleanup_payload_spec.rb +11 -11
- data/spec/lib/pwn/session_cleanup_recovery_spec.rb +34 -20
- data/spec/lib/pwn/setup_native_yubikey_spec.rb +66 -0
- 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/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 +605 -19
- metadata +129 -16
- data/scripts/benchmark_policy.rb +0 -376
- data/scripts/build_gate.rb +0 -58
- data/spec/integration/build_gate_spec.rb +0 -73
data/lib/pwn/backups.rb
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'digest'
|
|
6
|
+
require 'securerandom'
|
|
7
|
+
require 'find'
|
|
8
|
+
|
|
9
|
+
module PWN
|
|
10
|
+
# All first-party recovery copies share one serialized seven-generation store.
|
|
11
|
+
# Evidence artifacts are not recovery copies and must not enter this store.
|
|
12
|
+
module Backups
|
|
13
|
+
LIMIT = 7
|
|
14
|
+
GENERATION = /\A\d{8}_\d{6}_[a-f0-9]{8}\z/
|
|
15
|
+
|
|
16
|
+
# Supported Method Parameters::
|
|
17
|
+
# PWN::Backups.active?
|
|
18
|
+
public_class_method def self.active?
|
|
19
|
+
owner = Thread.current[:pwn_backup_transaction]
|
|
20
|
+
!owner.nil? && owner[:pid] == Process.pid && owner[:thread] == Thread.current
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Supported Method Parameters::
|
|
24
|
+
# PWN::Backups.transaction(root: nil) { ... }
|
|
25
|
+
public_class_method def self.transaction(opts = {})
|
|
26
|
+
owner = Thread.current[:pwn_backup_transaction]
|
|
27
|
+
override = Thread.current[:pwn_backup_root]
|
|
28
|
+
real_home = Thread.current[:pwn_backup_real_home]
|
|
29
|
+
override = nil if real_home && Dir.home != real_home
|
|
30
|
+
root = File.expand_path(opts[:root] || (owner[:root] if active?) || override || File.join(Dir.home, '.pwn', 'backup'))
|
|
31
|
+
return yield if owner && owner[:pid] == Process.pid && owner[:thread] == Thread.current && owner[:root] == root
|
|
32
|
+
raise IOError, 'Nested backup roots are not supported' if active?
|
|
33
|
+
|
|
34
|
+
learning = PWN::AI::Agent::Learning.send(:learning_lock)
|
|
35
|
+
safe_directory(path: root)
|
|
36
|
+
File.open(File.join(root, '.retention.lock'), File::RDWR | File::CREAT | File::NOFOLLOW, 0o600) do |lock|
|
|
37
|
+
raise IOError, 'Unsafe backup lock' unless lock.stat.uid == Process.uid && lock.stat.nlink == 1
|
|
38
|
+
|
|
39
|
+
lock.flock(File::LOCK_EX)
|
|
40
|
+
context = { pid: Process.pid, thread: Thread.current, root: root }
|
|
41
|
+
Thread.current[:pwn_backup_transaction] = context
|
|
42
|
+
begin
|
|
43
|
+
result = yield
|
|
44
|
+
failed = result.is_a?(Hash) && (result[:error] || result[:ok] == false || result[:promoted] == false || result[:incomplete] || %w[prepared interrupted blocked].include?(result[:state].to_s))
|
|
45
|
+
write_state(context: context, state: 'complete') if context[:directory] && !failed
|
|
46
|
+
result
|
|
47
|
+
ensure
|
|
48
|
+
# Exceptions leave a prepared recovery generation, never silently expire it.
|
|
49
|
+
Thread.current[:pwn_backup_transaction] = owner
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
ensure
|
|
53
|
+
PWN::AI::Agent::Learning.send(:learning_unlock, lock: learning) if learning
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Supported Method Parameters::
|
|
57
|
+
# PWN::Backups.directory(label: 'maintenance')
|
|
58
|
+
# Caller must hold transaction through the final source mutation and ledger.
|
|
59
|
+
public_class_method def self.directory(opts = {})
|
|
60
|
+
label = opts[:label].to_s
|
|
61
|
+
raise ArgumentError, 'Invalid backup label' unless label.match?(/\A[a-zA-Z0-9_-]+\z/)
|
|
62
|
+
|
|
63
|
+
context = Thread.current[:pwn_backup_transaction]
|
|
64
|
+
raise IOError, 'Backup transaction required' unless context && context[:pid] == Process.pid && context[:thread] == Thread.current
|
|
65
|
+
|
|
66
|
+
unless context[:directory]
|
|
67
|
+
root = context[:root]
|
|
68
|
+
names = Dir.children(root).select { |name| File.directory?(File.join(root, name)) || File.symlink?(File.join(root, name)) }.sort
|
|
69
|
+
pins = names.length >= LIMIT ? reference_pins(root: root) : []
|
|
70
|
+
raise IOError, 'Pinned backup missing' unless (pins - names).empty?
|
|
71
|
+
|
|
72
|
+
candidates = names.select do |name|
|
|
73
|
+
next false unless name.match?(GENERATION) && !pins.include?(name)
|
|
74
|
+
|
|
75
|
+
path = File.join(root, name)
|
|
76
|
+
safe_directory(path: path)
|
|
77
|
+
state = File.join(path, '.generation.json')
|
|
78
|
+
next false unless File.file?(state) && !File.symlink?(state)
|
|
79
|
+
|
|
80
|
+
record = JSON.parse(File.binread(state))
|
|
81
|
+
# Capture pins that nothing references may be expired. Policy pins and
|
|
82
|
+
# evidence references are already in `pins`. Interrupted generations are not.
|
|
83
|
+
record['version'] == 1 && record['state'] == 'complete'
|
|
84
|
+
end
|
|
85
|
+
count = [names.length - LIMIT + 1, 0].max
|
|
86
|
+
raise IOError, 'Backup capacity occupied by legacy, pinned or interrupted generations' if candidates.length < count
|
|
87
|
+
|
|
88
|
+
candidates.sort_by { |name| File.mtime(File.join(root, name)).to_r }.first(count).each { |name| FileUtils.remove_entry_secure(File.join(root, name)) }
|
|
89
|
+
context[:directory] = File.join(root, "#{Time.now.utc.strftime('%Y%m%d_%H%M%S')}_#{SecureRandom.hex(4)}")
|
|
90
|
+
Dir.mkdir(context[:directory], 0o700)
|
|
91
|
+
write_state(context: context, state: 'prepared')
|
|
92
|
+
end
|
|
93
|
+
path = File.join(context[:directory], "#{label}-#{SecureRandom.hex(8)}")
|
|
94
|
+
Dir.mkdir(path, 0o700)
|
|
95
|
+
path
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Supported Method Parameters::
|
|
99
|
+
# PWN::Backups.capture(path: '/path/to/source', label: 'learning')
|
|
100
|
+
public_class_method def self.capture(opts = {})
|
|
101
|
+
source = File.expand_path(opts[:path])
|
|
102
|
+
label = opts[:label] || 'snapshot'
|
|
103
|
+
transaction do
|
|
104
|
+
Thread.current[:pwn_backup_transaction][:pinned] = true if opts[:pin]
|
|
105
|
+
raise IOError, 'Unsafe backup source' unless File.file?(source) && File.realpath(source) == source && File.stat(source).uid == Process.uid
|
|
106
|
+
|
|
107
|
+
directory = self.directory(label: label)
|
|
108
|
+
target = File.join(directory, File.basename(source))
|
|
109
|
+
before = Digest::SHA256.file(source).hexdigest
|
|
110
|
+
File.open(source, File::RDONLY | File::NOFOLLOW) do |input|
|
|
111
|
+
File.open(target, File::WRONLY | File::CREAT | File::EXCL | File::NOFOLLOW, 0o600) do |output|
|
|
112
|
+
IO.copy_stream(input, output)
|
|
113
|
+
output.flush
|
|
114
|
+
output.fsync
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
raise IOError, 'Backup source changed or copy failed' unless Digest::SHA256.file(source).hexdigest == before && Digest::SHA256.file(target).hexdigest == before
|
|
118
|
+
|
|
119
|
+
File.open(directory, File::RDONLY, &:fsync)
|
|
120
|
+
target
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Supported Method Parameters::
|
|
125
|
+
# PWN::Backups.configure(root: nil)
|
|
126
|
+
public_class_method def self.configure(opts = {})
|
|
127
|
+
root = opts[:root] || File.join(Dir.home, '.pwn', 'backup')
|
|
128
|
+
transaction(root: root) do
|
|
129
|
+
path = File.join(root, '.retention-policy.json')
|
|
130
|
+
raise IOError, 'Unsafe retention policy' if File.symlink?(path)
|
|
131
|
+
|
|
132
|
+
policy = File.file?(path) ? JSON.parse(File.binread(path)) : { 'version' => 1, 'pins' => [] }
|
|
133
|
+
raise IOError, 'Invalid retention policy' unless policy['version'] == 1 && policy['pins'].is_a?(Array)
|
|
134
|
+
|
|
135
|
+
updated = policy.merge('keep' => LIMIT, 'expire_history' => true, 'scope' => 'global')
|
|
136
|
+
next updated if updated == policy
|
|
137
|
+
|
|
138
|
+
temporary = "#{path}.#{SecureRandom.hex(8)}"
|
|
139
|
+
File.open(temporary, File::WRONLY | File::CREAT | File::EXCL | File::NOFOLLOW, 0o600) do |file|
|
|
140
|
+
file.write(JSON.generate(updated))
|
|
141
|
+
file.flush
|
|
142
|
+
file.fsync
|
|
143
|
+
end
|
|
144
|
+
File.rename(temporary, path)
|
|
145
|
+
File.open(root, File::RDONLY, &:fsync)
|
|
146
|
+
updated
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Supported Method Parameters::
|
|
151
|
+
# PWN::Backups.reconcile(generation: 'name', expected_sha256: 'digest', confirmed: true)
|
|
152
|
+
public_class_method def self.reconcile(opts = {})
|
|
153
|
+
generation = opts[:generation].to_s
|
|
154
|
+
raise ArgumentError, 'Exact generation and explicit recovery confirmation required' unless generation.match?(GENERATION) && opts[:confirmed] == true
|
|
155
|
+
|
|
156
|
+
transaction do
|
|
157
|
+
root = Thread.current[:pwn_backup_transaction][:root]
|
|
158
|
+
directory = File.join(root, generation)
|
|
159
|
+
raise IOError, 'Recovery generation missing' unless File.directory?(directory)
|
|
160
|
+
|
|
161
|
+
safe_directory(path: directory)
|
|
162
|
+
path = File.join(directory, '.generation.json')
|
|
163
|
+
raise IOError, 'Recovery manifest changed' unless !File.symlink?(path) && Digest::SHA256.file(path).hexdigest == opts[:expected_sha256]
|
|
164
|
+
|
|
165
|
+
record = JSON.parse(File.read(path))
|
|
166
|
+
raise IOError, 'Unknown recovery manifest' unless record['version'] == 1 && %w[prepared complete].include?(record['state'])
|
|
167
|
+
|
|
168
|
+
write_state(context: { root: root, directory: directory, pinned: opts.fetch(:pinned, true) }, state: 'complete')
|
|
169
|
+
{ generation: generation, state: 'complete', pinned: opts.fetch(:pinned, true) }
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Supported Method Parameters::
|
|
174
|
+
# PWN::Backups.relocate_legacy(paths: [], dry_run: true, writers_stopped: false)
|
|
175
|
+
public_class_method def self.relocate_legacy(opts = {})
|
|
176
|
+
paths = Array(opts[:paths]).map { |path| File.expand_path(path) }.uniq
|
|
177
|
+
dry_run = opts.fetch(:dry_run, true)
|
|
178
|
+
raise ArgumentError, 'Stop all legacy writers before relocation' unless dry_run || opts[:writers_stopped] == true
|
|
179
|
+
|
|
180
|
+
home = Dir.home
|
|
181
|
+
root = File.join(home, '.pwn')
|
|
182
|
+
paths.each do |path|
|
|
183
|
+
relative = path.delete_prefix("#{root}/")
|
|
184
|
+
recognized = relative.match?(%r{\A(?:learning\.jsonl\.bak[^/]*|preferences\.jsonl\.bak[^/]*|skills/.+/SKILL\.md\.[a-f0-9]{64}\.bak|policy\.json\.rollback-[a-f0-9]{64}\.json|learning/migrations/[a-f0-9]{64}\.backup\.jsonl|session_cleanup_archive/[^/]+|maintenance/(?:retirements|quarantine)/[^/]+)\z})
|
|
185
|
+
recognized ||= path == File.join(home, '.config', 'terminator', 'config.pwnbak')
|
|
186
|
+
raise ArgumentError, 'Unrecognized legacy recovery path' unless recognized
|
|
187
|
+
raise IOError, 'Unsafe legacy recovery path' unless File.realpath(path) == path && File.stat(path).uid == Process.uid
|
|
188
|
+
end
|
|
189
|
+
raise ArgumentError, 'Overlapping legacy sources' if paths.any? { |path| paths.any? { |other| path != other && path.start_with?("#{other}/") } }
|
|
190
|
+
|
|
191
|
+
return { planned: paths, moved: [] } if dry_run
|
|
192
|
+
|
|
193
|
+
transaction do
|
|
194
|
+
legacy_unreferenced(paths: paths, root: root)
|
|
195
|
+
mappings = paths.to_h do |path|
|
|
196
|
+
target = File.join(directory(label: 'legacy'), File.basename(path))
|
|
197
|
+
before = legacy_inventory(path: path)
|
|
198
|
+
FileUtils.cp_r(path, target, preserve: true)
|
|
199
|
+
raise IOError, 'Legacy recovery changed during copy' unless before == legacy_inventory(path: path) && before == legacy_inventory(path: target)
|
|
200
|
+
|
|
201
|
+
[path, { target: target, inventory: before }]
|
|
202
|
+
end
|
|
203
|
+
legacy_unreferenced(paths: paths, root: root)
|
|
204
|
+
mappings.each do |path, row|
|
|
205
|
+
raise IOError, 'Legacy recovery changed before relocation' unless row[:inventory] == legacy_inventory(path: path)
|
|
206
|
+
end
|
|
207
|
+
ledger = File.join(Thread.current[:pwn_backup_transaction][:directory], '.relocation.json')
|
|
208
|
+
File.open(ledger, File::WRONLY | File::CREAT | File::EXCL | File::NOFOLLOW, 0o600) do |file|
|
|
209
|
+
file.write(JSON.generate(version: 1, mappings: mappings))
|
|
210
|
+
file.flush
|
|
211
|
+
file.fsync
|
|
212
|
+
end
|
|
213
|
+
mappings.each_key { |path| FileUtils.remove_entry_secure(path) }
|
|
214
|
+
{ moved: mappings.transform_values { |row| row[:target] }, ledger: ledger }
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
private_class_method def self.legacy_inventory(opts = {})
|
|
219
|
+
path = opts[:path]
|
|
220
|
+
rows = {}
|
|
221
|
+
Find.find(path) do |file|
|
|
222
|
+
stat = File.lstat(file)
|
|
223
|
+
raise IOError, 'Unsafe legacy recovery entry' unless (stat.file? || stat.directory?) && stat.uid == Process.uid
|
|
224
|
+
|
|
225
|
+
rows[file.delete_prefix(path)] = [stat.ftype, stat.mode, stat.file? ? Digest::SHA256.file(file).hexdigest : nil]
|
|
226
|
+
File.open(file, File::RDONLY, &:fsync)
|
|
227
|
+
end
|
|
228
|
+
rows
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
private_class_method def self.legacy_unreferenced(opts = {})
|
|
232
|
+
paths = opts[:paths]
|
|
233
|
+
root = opts[:root]
|
|
234
|
+
return unless File.directory?(root)
|
|
235
|
+
|
|
236
|
+
Find.find(root) do |file|
|
|
237
|
+
Find.prune if file == File.join(root, 'backup') || paths.include?(file)
|
|
238
|
+
raise IOError, 'Unsafe legacy reference scope' if File.symlink?(file)
|
|
239
|
+
next unless File.file?(file) && file.match?(/\.jsonl?\z/)
|
|
240
|
+
|
|
241
|
+
body = File.binread(file)
|
|
242
|
+
raise IOError, 'Legacy recovery is referenced; preserve original paths' if paths.any? { |path| body.include?(path) || body.include?(path.delete_prefix("#{root}/")) }
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
private_class_method def self.reference_pins(opts = {})
|
|
247
|
+
root = opts[:root]
|
|
248
|
+
policy = File.join(root, '.retention-policy.json')
|
|
249
|
+
raise IOError, 'Unsafe retention policy' if File.symlink?(policy)
|
|
250
|
+
|
|
251
|
+
pins = File.file?(policy) ? JSON.parse(File.binread(policy)).fetch('pins') : []
|
|
252
|
+
raise IOError, 'Invalid backup pins' unless pins.is_a?(Array) && pins.all? { |name| name.is_a?(String) && name.match?(/\A\d{8}_\d{6}(?:_[a-f0-9]{8})?\z/) }
|
|
253
|
+
|
|
254
|
+
pins = pins.dup
|
|
255
|
+
state_root = File.dirname(root)
|
|
256
|
+
names = %w[learning learning.jsonl memory.json mistakes.json policy.json policy_traj.jsonl preferences.jsonl engagements artifacts sessions swarm jobs runs missions open_goals open_goal.json curriculum finetune extrospection skill_review.jsonl maintenance]
|
|
257
|
+
names.each do |name|
|
|
258
|
+
path = File.join(state_root, name)
|
|
259
|
+
next unless File.exist?(path) || File.symlink?(path)
|
|
260
|
+
|
|
261
|
+
Find.find(path) do |file|
|
|
262
|
+
stat = File.lstat(file)
|
|
263
|
+
raise IOError, 'Unsafe backup reference entry' unless (stat.file? || stat.directory?) && stat.uid == Process.uid
|
|
264
|
+
next unless stat.file? && file.match?(/\.jsonl?\z/)
|
|
265
|
+
|
|
266
|
+
admission = Thread.current[:pwn_maintenance_admission]
|
|
267
|
+
raise IOError, 'Backup reference budget exhausted' if admission && !admission.call(stat.size + 1)
|
|
268
|
+
|
|
269
|
+
values = file.end_with?('.jsonl') ? File.foreach(file).reject { |line| line.strip.empty? }.map { |line| JSON.parse(line) } : [JSON.parse(File.binread(file))]
|
|
270
|
+
values.select! { |value| value.is_a?(Hash) && (%w[prepared interrupted].include?(value['state']) || value['pinned'] == true) } if name == 'maintenance'
|
|
271
|
+
until values.empty?
|
|
272
|
+
value = values.pop
|
|
273
|
+
case value
|
|
274
|
+
when Hash then values.concat(value.values)
|
|
275
|
+
when Array then values.concat(value)
|
|
276
|
+
when String
|
|
277
|
+
match = value.match(%r{\A(?:#{Regexp.escape(root)}/|backup/)(\d{8}_\d{6}(?:_[a-f0-9]{8})?)(?:/|\z)})
|
|
278
|
+
pins << match[1] if match
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
pins.uniq
|
|
284
|
+
rescue JSON::ParserError, KeyError => e
|
|
285
|
+
raise IOError, "Unreadable backup references: #{e.class}"
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
private_class_method def self.safe_directory(opts = {})
|
|
289
|
+
path = opts[:path]
|
|
290
|
+
ancestor = path
|
|
291
|
+
loop do
|
|
292
|
+
raise IOError, 'Symlinked backup ancestor' if File.symlink?(ancestor)
|
|
293
|
+
break if File.dirname(ancestor) == ancestor
|
|
294
|
+
|
|
295
|
+
ancestor = File.dirname(ancestor)
|
|
296
|
+
end
|
|
297
|
+
FileUtils.mkdir_p(path, mode: 0o700)
|
|
298
|
+
stat = File.stat(path)
|
|
299
|
+
if stat.uid == Process.uid && !stat.mode.nobits?(0o077)
|
|
300
|
+
File.chmod(0o700, path)
|
|
301
|
+
stat = File.stat(path)
|
|
302
|
+
end
|
|
303
|
+
raise IOError, 'Unsafe backup directory' unless stat.uid == Process.uid && stat.mode.nobits?(0o077)
|
|
304
|
+
|
|
305
|
+
path
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
private_class_method def self.write_state(opts = {})
|
|
309
|
+
context = opts[:context]
|
|
310
|
+
path = File.join(context[:directory], '.generation.json')
|
|
311
|
+
temporary = "#{path}.tmp"
|
|
312
|
+
File.open(temporary, File::WRONLY | File::CREAT | File::EXCL | File::NOFOLLOW, 0o600) do |file|
|
|
313
|
+
file.write(JSON.generate(version: 1, state: opts[:state], pinned: context[:pinned] == true))
|
|
314
|
+
file.flush
|
|
315
|
+
file.fsync
|
|
316
|
+
end
|
|
317
|
+
File.rename(temporary, path)
|
|
318
|
+
File.open(context[:directory], File::RDONLY, &:fsync)
|
|
319
|
+
File.open(context[:root], File::RDONLY, &:fsync)
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
# Author(s):: 0day Inc. <support@0dayinc.com>
|
|
323
|
+
public_class_method def self.authors
|
|
324
|
+
"AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
# Display Usage for this Module
|
|
328
|
+
public_class_method def self.help
|
|
329
|
+
puts "
|
|
330
|
+
# Hold the shared lock through backup and source mutation.
|
|
331
|
+
#{self}.transaction(root: 'optional - backup root; defaults to Thread.current[:pwn_backup_root] or ~/.pwn/backup') { ... }
|
|
332
|
+
# Allocate a producer directory inside the current generation.
|
|
333
|
+
#{self}.directory(label: 'required - producer label')
|
|
334
|
+
# Copy and hash-verify an owned regular file.
|
|
335
|
+
#{self}.capture(path: 'required - source path', label: 'optional - producer label', pin: 'optional - retain until explicitly reconciled')
|
|
336
|
+
# Install the global seven-folder policy without expiring existing data.
|
|
337
|
+
#{self}.configure(root: 'optional - backup root; defaults ~/.pwn/backup')
|
|
338
|
+
# Acknowledge completed store-specific recovery; this does not restore data.
|
|
339
|
+
#{self}.reconcile(generation: 'required - exact generation name', expected_sha256: 'required - inspected generation manifest digest', confirmed: 'required - explicit true after store recovery', pinned: 'optional - retain by default; false permits ordinary retention')
|
|
340
|
+
# Explicitly relocate recognized unreferenced legacy recovery copies.
|
|
341
|
+
#{self}.relocate_legacy(paths: 'required - exact legacy paths', dry_run: 'optional - preview only by default', writers_stopped: 'optional - must be true to apply')
|
|
342
|
+
# Check whether this thread owns the backup transaction.
|
|
343
|
+
#{self}.active?
|
|
344
|
+
# Show module authors.
|
|
345
|
+
#{self}.authors
|
|
346
|
+
"
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
end
|
data/lib/pwn/config.rb
CHANGED
|
@@ -364,13 +364,20 @@ module PWN
|
|
|
364
364
|
|
|
365
365
|
# Supported Method Parameters::
|
|
366
366
|
# env = PWN::Config.default_env(
|
|
367
|
-
# pwn_env_path: 'optional - Path to pwn.yaml file.
|
|
367
|
+
# pwn_env_path: 'optional - Path to pwn.yaml file. Defaults to ~/.pwn/pwn.yaml',
|
|
368
|
+
# pwn_dec_path: 'optional - Legacy decryptor path; defaults to the vault path plus .decryptor',
|
|
369
|
+
# yubikey: 'optional - Explicit hardware opt-in with slot, serial, reader, device_path, connection, timeout selector'
|
|
368
370
|
# )
|
|
369
371
|
public_class_method def self.default_env(opts = {})
|
|
370
|
-
pwn_env_path = opts[:pwn_env_path]
|
|
371
|
-
pwn_dec_path = "#{pwn_env_path}.decryptor"
|
|
372
|
-
|
|
373
|
-
|
|
372
|
+
pwn_env_path = opts[:pwn_env_path] || File.join(Dir.home, '.pwn', 'pwn.yaml')
|
|
373
|
+
pwn_dec_path = opts[:pwn_dec_path] || "#{pwn_env_path}.decryptor"
|
|
374
|
+
yubikey = opts[:yubikey]
|
|
375
|
+
raise ArgumentError, 'yubikey must be a selector Hash' unless yubikey.nil? || yubikey.is_a?(Hash)
|
|
376
|
+
|
|
377
|
+
if yubikey
|
|
378
|
+
puts "[*] Writing YubiKey-protected PWN::Env to #{pwn_env_path}; no software decryptor is created."
|
|
379
|
+
else
|
|
380
|
+
puts "
|
|
374
381
|
[*] NOTICE:
|
|
375
382
|
1. Writing minimal PWN::Env to:
|
|
376
383
|
#{pwn_env_path}
|
|
@@ -381,15 +388,22 @@ module PWN
|
|
|
381
388
|
4. For optimal security, it's recommended to move:
|
|
382
389
|
#{pwn_dec_path}
|
|
383
390
|
to a secure location and use the --pwn-dec parameter for PWN drivers.
|
|
384
|
-
|
|
391
|
+
"
|
|
392
|
+
end
|
|
385
393
|
env = env_template
|
|
386
394
|
|
|
387
|
-
#
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
395
|
+
# YAML tags preserve symbol keys without rewriting multiline scalar bytes.
|
|
396
|
+
yaml_env = YAML.dump(env)
|
|
397
|
+
FileUtils.mkdir_p(File.dirname(pwn_env_path), mode: 0o700)
|
|
398
|
+
if yubikey
|
|
399
|
+
encrypted = PWN::Plugins::Vault.encrypt_data(data: yaml_env, yubikey: yubikey)
|
|
400
|
+
File.open(pwn_env_path, File::WRONLY | File::CREAT | File::EXCL, 0o600) { |file| file.write(encrypted) }
|
|
401
|
+
else
|
|
402
|
+
File.write(pwn_env_path, yaml_env)
|
|
403
|
+
# Change file permission to 600
|
|
404
|
+
File.chmod(0o600, pwn_env_path)
|
|
405
|
+
PWN::Plugins::Vault.create(file: pwn_env_path, decryptor_file: pwn_dec_path)
|
|
406
|
+
end
|
|
393
407
|
|
|
394
408
|
# Ensure skills dir for pwn-ai agent (in parent of pwn_env_path)
|
|
395
409
|
pwn_env_root = File.dirname(pwn_env_path)
|
|
@@ -401,11 +415,6 @@ module PWN
|
|
|
401
415
|
pwn_dec_path: pwn_dec_path
|
|
402
416
|
}
|
|
403
417
|
|
|
404
|
-
PWN::Plugins::Vault.create(
|
|
405
|
-
file: pwn_env_path,
|
|
406
|
-
decryptor_file: pwn_dec_path
|
|
407
|
-
)
|
|
408
|
-
|
|
409
418
|
Pry.config.refresh_pwn_env = false if defined?(Pry)
|
|
410
419
|
env[:pwn_skills_path] = pwn_skills_path
|
|
411
420
|
PWN::Config.install_default_skills(pwn_skills_path: pwn_skills_path)
|
|
@@ -430,6 +439,7 @@ module PWN
|
|
|
430
439
|
# )
|
|
431
440
|
public_class_method def self.redact_sensitive_artifacts(opts = {})
|
|
432
441
|
config = opts[:config] ||= PWN::Env
|
|
442
|
+
return config.map { |value| value.is_a?(Hash) || value.is_a?(Array) ? redact_sensitive_artifacts(config: value) : value } if config.is_a?(Array)
|
|
433
443
|
|
|
434
444
|
sensitive_keys = %i[
|
|
435
445
|
admin_key
|
|
@@ -445,14 +455,38 @@ module PWN
|
|
|
445
455
|
refresh_token
|
|
446
456
|
secret_key
|
|
447
457
|
token
|
|
458
|
+
pin
|
|
459
|
+
new_pin
|
|
460
|
+
admin_pin
|
|
461
|
+
puk
|
|
462
|
+
new_puk
|
|
463
|
+
management_key
|
|
464
|
+
new_management_key
|
|
465
|
+
management_password
|
|
466
|
+
access_code
|
|
467
|
+
new_access_code
|
|
468
|
+
oath_secret
|
|
469
|
+
oath_password
|
|
470
|
+
unlock_code
|
|
471
|
+
unlock_password
|
|
472
|
+
lock_code
|
|
473
|
+
new_lock_code
|
|
474
|
+
reset_code
|
|
475
|
+
scp
|
|
476
|
+
scp_key
|
|
477
|
+
scp_keys
|
|
478
|
+
scp03_key
|
|
479
|
+
scp11_key
|
|
480
|
+
static_password
|
|
481
|
+
hmac_secret
|
|
448
482
|
]
|
|
449
483
|
|
|
450
484
|
# Transform values at the current level: redact sensitive keys
|
|
451
485
|
config.transform_values.with_index do |v, k|
|
|
452
|
-
if sensitive_keys.include?(config.keys[k])
|
|
486
|
+
if sensitive_keys.include?(config.keys[k].to_s.downcase.tr('-', '_').to_sym)
|
|
453
487
|
'>>> REDACTED >>> USE `pwn-vault` FOR ADMINISTRATION <<< REDACTED <<<'
|
|
454
488
|
else
|
|
455
|
-
v.is_a?(Hash) ? redact_sensitive_artifacts(config: v) : v
|
|
489
|
+
v.is_a?(Hash) || v.is_a?(Array) ? redact_sensitive_artifacts(config: v) : v
|
|
456
490
|
end
|
|
457
491
|
end
|
|
458
492
|
rescue StandardError => e
|
|
@@ -477,7 +511,8 @@ module PWN
|
|
|
477
511
|
# Supported Method Parameters::
|
|
478
512
|
# PWN::Config.refresh_env(
|
|
479
513
|
# pwn_env_path: 'optional - Path to pwn.yaml file. Defaults to ~/.pwn/pwn.yaml',
|
|
480
|
-
# pwn_dec_path: 'optional -
|
|
514
|
+
# pwn_dec_path: 'optional - Legacy decryptor path; not needed for hardware vaults',
|
|
515
|
+
# yubikey: 'optional - Hardware selector override or explicit opt-in for a new vault'
|
|
481
516
|
# )
|
|
482
517
|
|
|
483
518
|
# Deep-fill missing Hash keys from defaults into target (mutates target).
|
|
@@ -541,27 +576,20 @@ module PWN
|
|
|
541
576
|
pwn_skills_path = File.join(pwn_env_root, 'skills')
|
|
542
577
|
FileUtils.mkdir_p(pwn_skills_path)
|
|
543
578
|
|
|
544
|
-
return default_env(pwn_env_path: pwn_env_path) unless File.exist?(pwn_env_path)
|
|
579
|
+
return default_env(pwn_env_path: pwn_env_path, pwn_dec_path: opts[:pwn_dec_path], yubikey: opts[:yubikey]) unless File.exist?(pwn_env_path)
|
|
545
580
|
|
|
546
581
|
is_encrypted = PWN::Plugins::Vault.file_encrypted?(file: pwn_env_path)
|
|
547
582
|
raise "PWN Environment (#{pwn_env_path}) is not encrypted! Use PWN::Vault.create(file: '#{pwn_env_path}', decryptor_file: '#{pwn_env_path}.decryptor') to encrypt it." unless is_encrypted
|
|
548
583
|
|
|
549
584
|
pwn_dec_path = opts[:pwn_dec_path] ||= "#{pwn_env_path}.decryptor"
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
pwn_decryptor = YAML.load_file(pwn_dec_path, symbolize_names: true)
|
|
553
|
-
|
|
554
|
-
key = opts[:key] ||= pwn_decryptor[:key] ||= ENV.fetch('PWN_DECRYPTOR_KEY')
|
|
555
|
-
key = PWN::Plugins::AuthenticationHelper.mask_password(prompt: 'Decryption Key') if key.nil?
|
|
556
|
-
|
|
557
|
-
iv = opts[:iv] ||= pwn_decryptor[:iv] ||= ENV.fetch('PWN_DECRYPTOR_IV')
|
|
558
|
-
iv = PWN::Plugins::AuthenticationHelper.mask_password(prompt: 'Decryption IV') if iv.nil?
|
|
559
|
-
|
|
560
|
-
env = PWN::Plugins::Vault.dump(
|
|
585
|
+
access = PWN::Plugins::Vault.access_options(
|
|
561
586
|
file: pwn_env_path,
|
|
562
|
-
|
|
563
|
-
|
|
587
|
+
decryptor_file: pwn_dec_path,
|
|
588
|
+
key: opts[:key],
|
|
589
|
+
iv: opts[:iv],
|
|
590
|
+
yubikey: opts[:yubikey]
|
|
564
591
|
)
|
|
592
|
+
env = PWN::Plugins::Vault.dump(access.merge(file: pwn_env_path))
|
|
565
593
|
|
|
566
594
|
valid_ai_engines = PWN::AI.help.reject { |e| e.downcase == :agent }.map(&:downcase)
|
|
567
595
|
|
|
@@ -1142,7 +1170,9 @@ module PWN
|
|
|
1142
1170
|
|
|
1143
1171
|
# Run default env and return its result
|
|
1144
1172
|
#{self}.default_env(
|
|
1145
|
-
pwn_env_path: 'optional - Path to pwn.yaml file.
|
|
1173
|
+
pwn_env_path: 'optional - Path to pwn.yaml file. Defaults to ~/.pwn/pwn.yaml',
|
|
1174
|
+
pwn_dec_path: 'optional - Legacy decryptor path; defaults to the vault path plus .decryptor',
|
|
1175
|
+
yubikey: 'optional - Explicit hardware opt-in with slot, serial, reader, device_path, connection, timeout selector; no sidecar'
|
|
1146
1176
|
)
|
|
1147
1177
|
|
|
1148
1178
|
# Run redact sensitive artifacts and return its result
|
|
@@ -1155,10 +1185,11 @@ module PWN
|
|
|
1155
1185
|
|
|
1156
1186
|
# Run refresh env and return its result
|
|
1157
1187
|
#{self}.refresh_env(
|
|
1158
|
-
pwn_env_path: '
|
|
1159
|
-
pwn_dec_path: '
|
|
1160
|
-
key: 'optional -
|
|
1161
|
-
iv: 'optional -
|
|
1188
|
+
pwn_env_path: 'optional - Configuration vault path; defaults to ~/.pwn/pwn.yaml',
|
|
1189
|
+
pwn_dec_path: 'optional - Legacy decryptor path; not needed for hardware vaults',
|
|
1190
|
+
key: 'optional - Legacy vault key override',
|
|
1191
|
+
iv: 'optional - Legacy vault IV override',
|
|
1192
|
+
yubikey: 'optional - Hardware selector with slot, serial, reader, device_path, connection, timeout; opts in for new vaults'
|
|
1162
1193
|
)
|
|
1163
1194
|
|
|
1164
1195
|
# Run pwn skills path and return its result
|