pwn 0.5.784 → 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 +6 -6
- data/README.md +37 -0
- data/bin/pwn +2 -1
- data/bin/pwn_web_cache_deception +35 -7
- data/documentation/AI-Integration.md +7 -5
- data/documentation/Agent-Tool-Registry.md +2 -2
- data/documentation/Backup-Retention.md +70 -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/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 +17 -0
- data/documentation/Persistence.md +7 -5
- data/documentation/Plugin-Degradation.md +12 -0
- data/documentation/Plugins.md +3 -2
- 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 -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/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 +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 +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 +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 +347 -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 +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/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/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 +1 -1
- 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/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_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 +589 -19
- metadata +129 -13
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe PWN::Config, 'YubiKey configuration redaction' do
|
|
6
|
+
it 'redacts string-keyed hardware settings inside arrays without hiding public selectors' do
|
|
7
|
+
config = { devices: [{ 'PIN' => '1', 'management-key' => 'synthetic-key', 'oath-secret' => 'synthetic-oath', 'serial' => '12345678', 'slot' => 2 }] }
|
|
8
|
+
result = described_class.redact_sensitive_artifacts(config: config)
|
|
9
|
+
masked = result[:devices].first
|
|
10
|
+
expect(masked.values_at('PIN', 'management-key', 'oath-secret').uniq).to eq(['>>> REDACTED >>> USE `pwn-vault` FOR ADMINISTRATION <<< REDACTED <<<'])
|
|
11
|
+
expect(masked).to include('serial' => '12345678', 'slot' => 2)
|
|
12
|
+
expect(config.dig(:devices, 0, 'PIN')).to eq('1')
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it 'uses the same constant mask for short PINs and hardware secrets while retaining public selectors' do
|
|
16
|
+
fields = %i[pin new_pin admin_pin puk new_puk management_key new_management_key management_password access_code new_access_code oath_secret oath_password unlock_code unlock_password lock_code new_lock_code reset_code scp scp_key scp_keys scp03_key scp11_key static_password hmac_secret]
|
|
17
|
+
values = fields.to_h { |field| [field, '123456'] }
|
|
18
|
+
values[:pin] = '1'
|
|
19
|
+
values[:puk] = '87654321'
|
|
20
|
+
config = { plugins: { yubikey: values.merge(serial: '12345678', slot: 2, reader: 'fixture-reader') } }
|
|
21
|
+
before = Marshal.dump(config)
|
|
22
|
+
redacted = described_class.redact_sensitive_artifacts(config: config).dig(:plugins, :yubikey)
|
|
23
|
+
mask = described_class.redact_sensitive_artifacts(config: { password: 'different-value' })[:password]
|
|
24
|
+
fields.each { |field| expect(redacted[field]).to eq(mask), "#{field} must use the constant mask" }
|
|
25
|
+
expect(redacted).to include(serial: '12345678', slot: 2, reader: 'fixture-reader')
|
|
26
|
+
expect(Marshal.dump(config)).to eq(before)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'tmpdir'
|
|
5
|
+
|
|
6
|
+
RSpec.describe PWN::Config, 'hardware vault configuration' do
|
|
7
|
+
around do |example|
|
|
8
|
+
Dir.mktmpdir('config-yubikey-') do |root|
|
|
9
|
+
@root = root
|
|
10
|
+
example.run
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
before do
|
|
15
|
+
allow(Dir).to receive(:home).and_return(@root)
|
|
16
|
+
stub_const('PWN::Env', {})
|
|
17
|
+
stub_const('PWN::EnvRedacted', {})
|
|
18
|
+
stub_const('PWN::Plugins::YubiKey', double('synthetic YubiKey'))
|
|
19
|
+
allow(PWN::Plugins::YubiKey).to receive(:challenge_response) do |opts|
|
|
20
|
+
OpenSSL::HMAC.digest('SHA1', 'synthetic-slot-secret', opts[:challenge])
|
|
21
|
+
end
|
|
22
|
+
allow(described_class).to receive(:install_default_skills)
|
|
23
|
+
allow(described_class).to receive(:load_skills)
|
|
24
|
+
allow(PWN::Memory).to receive(:load)
|
|
25
|
+
allow(PWN::Cron).to receive(:install_defaults)
|
|
26
|
+
allow(PWN::Migrate).to receive(:needed?).and_return(false)
|
|
27
|
+
@path = File.join(@root, 'pwn.yaml')
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it 'rejects an invalid hardware selector rather than falling back to a software sidecar' do
|
|
31
|
+
expect { described_class.default_env(pwn_env_path: @path, yubikey: false) }.to raise_error(ArgumentError)
|
|
32
|
+
expect(File).not_to exist(@path)
|
|
33
|
+
expect(File).not_to exist("#{@path}.decryptor")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it 'creates the default hardware configuration path when called directly' do
|
|
37
|
+
described_class.default_env(yubikey: { slot: 2 })
|
|
38
|
+
path = File.join(@root, '.pwn', 'pwn.yaml')
|
|
39
|
+
expect(PWN::Plugins::Vault.dump(file: path)).to eq(described_class.env_template)
|
|
40
|
+
expect(File).not_to exist("#{path}.decryptor")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it 'preserves multiline template values containing leading colons during creation' do
|
|
44
|
+
template = Marshal.load(Marshal.dump(described_class.env_template))
|
|
45
|
+
text = "before\n:leading-colon\nafter\n"
|
|
46
|
+
template[:ai][:ollama][:system_role_content] = text
|
|
47
|
+
allow(described_class).to receive(:env_template).and_return(template)
|
|
48
|
+
described_class.default_env(pwn_env_path: @path, yubikey: {})
|
|
49
|
+
expect(PWN::Plugins::Vault.dump(file: @path).dig(:ai, :ollama, :system_role_content)).to eq(text)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it 'retains software creation by default and honors the explicitly selected legacy decryptor' do
|
|
53
|
+
sidecar = File.join(@root, 'custom-sidecar')
|
|
54
|
+
expect(PWN::Plugins::YubiKey).not_to receive(:challenge_response)
|
|
55
|
+
described_class.refresh_env(pwn_env_path: @path, pwn_dec_path: sidecar)
|
|
56
|
+
expect(PWN::Plugins::Vault.hardware_encrypted?(file: @path)).to be(false)
|
|
57
|
+
credentials = YAML.safe_load_file(sidecar, symbolize_names: true)
|
|
58
|
+
expect(PWN::Plugins::Vault.dump(credentials.merge(file: @path))).to eq(described_class.env_template)
|
|
59
|
+
described_class.refresh_env(pwn_env_path: @path, pwn_dec_path: sidecar)
|
|
60
|
+
expect(PWN::Env.dig(:driver_opts, :pwn_dec_path)).to eq(sidecar)
|
|
61
|
+
expect(File).not_to exist("#{@path}.decryptor")
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it 'creates a hardware vault only on explicit opt-in, without staging plaintext or making a sidecar' do
|
|
65
|
+
described_class.refresh_env(pwn_env_path: @path, yubikey: { slot: 2 })
|
|
66
|
+
expect(PWN::Plugins::Vault.hardware_encrypted?(file: @path)).to be(true)
|
|
67
|
+
expect(PWN::Plugins::Vault.dump(file: @path)).to eq(described_class.env_template)
|
|
68
|
+
expect(File).not_to exist("#{@path}.decryptor")
|
|
69
|
+
expect(File.stat(@path).mode & 0o777).to eq(0o600)
|
|
70
|
+
expect(PWN::Env).not_to have_key(:yubikey)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it 'leaves no plaintext configuration behind when opted-in hardware creation fails' do
|
|
74
|
+
allow(PWN::Plugins::YubiKey).to receive(:challenge_response).and_raise(IOError, 'synthetic device unavailable')
|
|
75
|
+
expect { described_class.default_env(pwn_env_path: @path, yubikey: { slot: 2 }) }.to raise_error(IOError)
|
|
76
|
+
expect(File).not_to exist(@path)
|
|
77
|
+
expect(File).not_to exist("#{@path}.decryptor")
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it 'refreshes a self-describing hardware vault without a decryptor or software-key prompt' do
|
|
81
|
+
original = { ai: { active: 'ollama', ollama: { model: 'fixture-model' } }, retained: 'fixture-value' }
|
|
82
|
+
bytes = PWN::Plugins::Vault.encrypt_data(data: YAML.dump(original), yubikey: { slot: 2, serial: '12345678' })
|
|
83
|
+
File.binwrite(@path, bytes)
|
|
84
|
+
expect(PWN::Plugins::AuthenticationHelper).not_to receive(:mask_password)
|
|
85
|
+
described_class.refresh_env(pwn_env_path: @path)
|
|
86
|
+
expect(PWN::Env.dig(:ai, :ollama, :model)).to eq('fixture-model')
|
|
87
|
+
expect(PWN::Env[:retained]).to eq('fixture-value')
|
|
88
|
+
expect(PWN::Env.dig(:driver_opts, :pwn_env_path)).to eq(@path)
|
|
89
|
+
expect(File.binread(@path)).to eq(bytes)
|
|
90
|
+
expect(File).not_to exist("#{@path}.decryptor")
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -30,7 +30,7 @@ describe PWN::Migrate, 'exact migration snapshot scope' do
|
|
|
30
30
|
File.write(File.join(@tmp, 'cron', 'worker.log'), 'new diagnostic')
|
|
31
31
|
File.write(File.join(@tmp, 'cron', 'schedule-backups', 'receipt.json'), '{}')
|
|
32
32
|
result = described_class.prune_backups(dry_run: false, expire_history: true)
|
|
33
|
-
expect(result
|
|
33
|
+
expect(result).to include(deleted: [], keep: 7, scope: 'global')
|
|
34
34
|
expect(File.read(File.join(@tmp, 'cron', 'worker.log'))).to eq('new diagnostic')
|
|
35
35
|
end
|
|
36
36
|
end
|
|
@@ -76,57 +76,17 @@ describe PWN::Migrate do
|
|
|
76
76
|
allow(PWN::Config).to receive(:pwn_skills_path).and_return(File.join(@tmp, 'skills'))
|
|
77
77
|
end
|
|
78
78
|
|
|
79
|
-
it '
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
expect(
|
|
89
|
-
expect(File).to
|
|
90
|
-
expect(File.read(File.join(@tmp, 'memory.json'))).to eq('{"current":true}')
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
context 'bounded historical expiry safeguards' do
|
|
94
|
-
before do
|
|
95
|
-
File.write(File.join(@tmp, 'memory.json'), '{}')
|
|
96
|
-
5.times do |i|
|
|
97
|
-
path = File.join(PWN::Migrate::BACKUP_ROOT, "2026010#{i + 1}_000000")
|
|
98
|
-
FileUtils.mkdir_p(path)
|
|
99
|
-
File.write(File.join(path, 'memory.json'), '{}')
|
|
100
|
-
end
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
it 'pins actual referenced evidence, not quoted inventory prose' do
|
|
104
|
-
File.write(File.join(@tmp, 'learning.jsonl'), "#{JSON.generate(evidence_path: "#{PWN::Migrate::BACKUP_ROOT}/20260101_000000/memory.json", prose: "old path #{PWN::Migrate::BACKUP_ROOT}/20260102_000000/memory.json")}\n")
|
|
105
|
-
FileUtils.cp(File.join(@tmp, 'learning.jsonl'), File.join(PWN::Migrate::BACKUP_ROOT, '20260105_000000', 'learning.jsonl'))
|
|
106
|
-
result = PWN::Migrate.prune_backups(dry_run: false, expire_history: true)
|
|
107
|
-
expect(result[:deleted]).to eq(['20260102_000000'])
|
|
108
|
-
expect(result[:pins]).to eq(['20260101_000000'])
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
it 'refuses expiry without current coverage or matching credential recovery' do
|
|
112
|
-
File.write(File.join(@tmp, 'pwn.yaml'), 'encrypted-current-fixture')
|
|
113
|
-
result = PWN::Migrate.prune_backups(dry_run: false, expire_history: true)
|
|
114
|
-
expect(result[:incomplete]).to be true
|
|
115
|
-
expect(result[:deleted]).to be_empty
|
|
116
|
-
File.write(File.join(PWN::Migrate::BACKUP_ROOT, '20260105_000000', 'pwn.yaml'), 'encrypted-old-fixture')
|
|
117
|
-
expect(PWN::Migrate.prune_backups(dry_run: false, expire_history: true)[:error]).to match(/encrypted configuration/)
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
it 'refuses symlinks and held producer locks and exhausted admission' do
|
|
121
|
-
File.symlink(File.join(@tmp, 'memory.json'), File.join(PWN::Migrate::BACKUP_ROOT, '20260101_000000', 'unsafe'))
|
|
122
|
-
expect(PWN::Migrate.prune_backups(dry_run: false, expire_history: true)[:deleted]).to be_empty
|
|
123
|
-
File.unlink(File.join(PWN::Migrate::BACKUP_ROOT, '20260101_000000', 'unsafe'))
|
|
124
|
-
File.open(File.join(PWN::Migrate::BACKUP_ROOT, '.retention.lock'), 'w') do |lock|
|
|
125
|
-
lock.flock(File::LOCK_EX)
|
|
126
|
-
expect(PWN::Migrate.prune_backups(dry_run: false, expire_history: true)[:error]).to eq('Backup maintenance active')
|
|
127
|
-
end
|
|
128
|
-
expect(PWN::Migrate.prune_backups(dry_run: false, expire_history: true, admission: ->(_bytes) { false })[:deleted]).to be_empty
|
|
129
|
-
end
|
|
79
|
+
it 'upgrades the existing three-folder policy to seven without pruning legacy data' do
|
|
80
|
+
FileUtils.mkdir_p(described_class::BACKUP_ROOT, mode: 0o700)
|
|
81
|
+
policy = File.join(described_class::BACKUP_ROOT, '.retention-policy.json')
|
|
82
|
+
File.write(policy, JSON.generate(version: 1, keep: 3, pins: [], expire_history: true))
|
|
83
|
+
File.write(described_class::SCHEMA_FILE, JSON.generate(schema: 11))
|
|
84
|
+
result = described_class.run(backup: false, io: io)
|
|
85
|
+
expect(result[:applied_migrations]).to eq([12])
|
|
86
|
+
expect(JSON.parse(File.read(policy))).to include('keep' => 7, 'scope' => 'global')
|
|
87
|
+
bytes = File.binread(policy)
|
|
88
|
+
expect(described_class.run(backup: false, io: io)[:applied_migrations]).to eq([])
|
|
89
|
+
expect(File.binread(policy)).to eq(bytes)
|
|
130
90
|
end
|
|
131
91
|
|
|
132
92
|
it '.needed? / .status on a fresh (empty) root' do
|
|
@@ -166,7 +126,7 @@ describe PWN::Migrate do
|
|
|
166
126
|
File.write(File.join(@tmp, '.schema'), JSON.generate(schema: 2))
|
|
167
127
|
expect(described_class.needed?).to be(true)
|
|
168
128
|
result = described_class.run(fix: false, backup: false, io: io)
|
|
169
|
-
expect(result[:applied_migrations]).to eq([3, 4, 5, 6, 7, 8, 9, 10, 11])
|
|
129
|
+
expect(result[:applied_migrations]).to eq([3, 4, 5, 6, 7, 8, 9, 10, 11, 12])
|
|
170
130
|
expected = Marshal.load(Marshal.dump(original))
|
|
171
131
|
expected['custom']['model'] = nil
|
|
172
132
|
expect(YAML.safe_load_file(path)).to eq(expected)
|
|
@@ -275,7 +235,7 @@ describe PWN::Migrate do
|
|
|
275
235
|
File.write(File.join(@tmp, '.schema'), JSON.generate(schema: 3))
|
|
276
236
|
expect(described_class.needed?).to be(true)
|
|
277
237
|
result = described_class.run(fix: true, backup: false, io: io)
|
|
278
|
-
expect(result[:applied_migrations]).to eq([4, 5, 6, 7, 8, 9, 10, 11])
|
|
238
|
+
expect(result[:applied_migrations]).to eq([4, 5, 6, 7, 8, 9, 10, 11, 12])
|
|
279
239
|
creds = YAML.safe_load_file(dec, symbolize_names: true)
|
|
280
240
|
user = PWN::Plugins::Vault.dump(file: yaml, key: creds[:key], iv: creds[:iv])
|
|
281
241
|
expect(user[:ai][:active]).to eq('grok')
|
|
@@ -301,7 +261,7 @@ describe PWN::Migrate do
|
|
|
301
261
|
PWN::Plugins::Vault.create(file: yaml, decryptor_file: dec)
|
|
302
262
|
File.write(File.join(@tmp, '.schema'), JSON.generate(schema: 4))
|
|
303
263
|
result = described_class.run(fix: true, backup: false, io: io)
|
|
304
|
-
expect(result[:applied_migrations]).to eq([5, 6, 7, 8, 9, 10, 11])
|
|
264
|
+
expect(result[:applied_migrations]).to eq([5, 6, 7, 8, 9, 10, 11, 12])
|
|
305
265
|
creds = YAML.safe_load_file(dec, symbolize_names: true)
|
|
306
266
|
user = PWN::Plugins::Vault.dump(file: yaml, key: creds[:key], iv: creds[:iv])
|
|
307
267
|
expect(user.dig(:ai, :agent, :skill_review)).to eq('off')
|
|
@@ -326,7 +286,7 @@ describe PWN::Migrate do
|
|
|
326
286
|
File.write(yaml, YAML.dump(stale).gsub(/^(\s*):/, '\1'))
|
|
327
287
|
PWN::Plugins::Vault.create(file: yaml, decryptor_file: dec)
|
|
328
288
|
File.write(File.join(@tmp, '.schema'), JSON.generate(schema: 5))
|
|
329
|
-
expect(described_class.run(fix: true, backup: false, io: io)[:applied_migrations]).to eq([6, 7, 8, 9, 10, 11])
|
|
289
|
+
expect(described_class.run(fix: true, backup: false, io: io)[:applied_migrations]).to eq([6, 7, 8, 9, 10, 11, 12])
|
|
330
290
|
creds = YAML.safe_load_file(dec, symbolize_names: true)
|
|
331
291
|
user = PWN::Plugins::Vault.dump(file: yaml, key: creds[:key], iv: creds[:iv])
|
|
332
292
|
theme = user.dig(:ai, :tui, :theme)
|
|
@@ -373,7 +333,7 @@ describe PWN::Migrate do
|
|
|
373
333
|
File.write(yaml, YAML.dump(stale).gsub(/^(\s*):/, '\1'))
|
|
374
334
|
PWN::Plugins::Vault.create(file: yaml, decryptor_file: dec)
|
|
375
335
|
File.write(File.join(@tmp, '.schema'), JSON.generate(schema: 8))
|
|
376
|
-
expect(described_class.run(fix: true, backup: false, io: io)[:applied_migrations]).to eq([9, 10, 11])
|
|
336
|
+
expect(described_class.run(fix: true, backup: false, io: io)[:applied_migrations]).to eq([9, 10, 11, 12])
|
|
377
337
|
creds = YAML.safe_load_file(dec, symbolize_names: true)
|
|
378
338
|
user = PWN::Plugins::Vault.dump(file: yaml, key: creds[:key], iv: creds[:iv])
|
|
379
339
|
long_fast = user.dig(:plugins, :meshtastic, :channel, :LongFast)
|
|
@@ -411,20 +371,18 @@ describe PWN::Migrate, 'backup housekeeping' do
|
|
|
411
371
|
expect(File.binread(File.join(@tmp, '.schema'))).to eq(before)
|
|
412
372
|
end
|
|
413
373
|
|
|
414
|
-
it '
|
|
374
|
+
it 'leaves legacy snapshots alone instead of running a competing pruner' do
|
|
415
375
|
root = described_class::BACKUP_ROOT
|
|
416
|
-
|
|
376
|
+
8.times do |i|
|
|
417
377
|
dir = File.join(root, "2026010#{i + 1}_000000")
|
|
418
378
|
FileUtils.mkdir_p(dir)
|
|
419
|
-
File.write(File.join(dir, 'learning.jsonl'),
|
|
379
|
+
File.write(File.join(dir, 'learning.jsonl'), 'unique evidence')
|
|
420
380
|
end
|
|
421
|
-
|
|
422
|
-
expect(
|
|
423
|
-
expect(Dir.children(root).length).to eq(
|
|
424
|
-
|
|
425
|
-
expect(
|
|
426
|
-
expect(File.binread(File.join(root, '20260101_000000', 'learning.jsonl'))).to eq('unique evidence')
|
|
427
|
-
expect(Dir.children(root).grep(/\A\d/).length).to eq(4)
|
|
381
|
+
result = described_class.prune_backups(dry_run: false, keep: 3, expire_history: true)
|
|
382
|
+
expect(result).to include(keep: 7, scope: 'global', deleted: [])
|
|
383
|
+
expect(Dir.children(root).length).to eq(8)
|
|
384
|
+
expect { described_class.send(:backup!) }.to raise_error(IOError)
|
|
385
|
+
expect(Dir.children(root).grep(/\A\d/).length).to eq(8)
|
|
428
386
|
end
|
|
429
387
|
|
|
430
388
|
it 'publishes a verified scoped snapshot and fails closed on a copy failure' do
|
|
@@ -441,21 +399,11 @@ describe PWN::Migrate, 'backup housekeeping' do
|
|
|
441
399
|
expect { described_class.send(:backup!) }.to raise_error(IOError, 'copy failed')
|
|
442
400
|
end
|
|
443
401
|
|
|
444
|
-
it 'rejects symlinked backup
|
|
402
|
+
it 'rejects a symlinked backup root without touching its target' do
|
|
445
403
|
root = described_class::BACKUP_ROOT
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
expect(
|
|
449
|
-
File.unlink(File.join(root, '20260101_000000'))
|
|
450
|
-
4.times do |i|
|
|
451
|
-
dir = File.join(root, "2026010#{i + 1}_000000")
|
|
452
|
-
FileUtils.mkdir_p(dir)
|
|
453
|
-
File.write(File.join(dir, 'state'), 'same')
|
|
454
|
-
end
|
|
455
|
-
result = described_class.prune_backups(dry_run: false, admission: ->(_bytes) { false })
|
|
456
|
-
expect(result[:incomplete]).to be(true)
|
|
457
|
-
expect(result[:deleted]).to eq([])
|
|
458
|
-
expect(Dir.children(root).grep(/\A\d/).length).to eq(4)
|
|
404
|
+
File.symlink(@tmp, root)
|
|
405
|
+
expect { described_class.send(:backup!) }.to raise_error(IOError, /Symlink/)
|
|
406
|
+
expect(File.symlink?(root)).to be(true)
|
|
459
407
|
end
|
|
460
408
|
|
|
461
409
|
it 'skips backups for fix mode when schema, files and vault have no drift' do
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'tmpdir'
|
|
5
|
+
require 'stringio'
|
|
6
|
+
|
|
7
|
+
RSpec.describe PWN::Migrate, 'hardware vault migrations' do
|
|
8
|
+
around do |example|
|
|
9
|
+
Dir.mktmpdir('migrate-yubikey-') do |root|
|
|
10
|
+
@root = root
|
|
11
|
+
example.run
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
before do
|
|
16
|
+
allow(Dir).to receive(:home).and_return(@root)
|
|
17
|
+
stub_const('PWN::Migrate::PWN_ROOT', @root)
|
|
18
|
+
stub_const('PWN::Env', { driver_opts: {} })
|
|
19
|
+
stub_const('PWN::Plugins::YubiKey', double('synthetic YubiKey'))
|
|
20
|
+
allow(PWN::Plugins::YubiKey).to receive(:challenge_response) do |opts|
|
|
21
|
+
OpenSSL::HMAC.digest('SHA1', 'synthetic-slot-secret', opts[:challenge])
|
|
22
|
+
end
|
|
23
|
+
@path = File.join(@root, 'pwn.yaml')
|
|
24
|
+
@original = { ai: { active: 'ollama', ollama: { model: 'fixture-model' } }, retained: 'fixture-value' }
|
|
25
|
+
File.binwrite(@path, PWN::Plugins::Vault.encrypt_data(data: YAML.dump(@original), yubikey: { slot: 2 }))
|
|
26
|
+
@io = StringIO.new
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it 'backfills through an atomic encrypted update, preserving the hardware format and user choices' do
|
|
30
|
+
expect(File).not_to receive(:write).with(@path, anything)
|
|
31
|
+
expect(PWN::Plugins::AuthenticationHelper).not_to receive(:mask_password)
|
|
32
|
+
result = described_class.backfill_vault(io: @io)
|
|
33
|
+
expect(result[:added]).to be_positive
|
|
34
|
+
expect(PWN::Plugins::Vault.hardware_encrypted?(file: @path)).to be(true)
|
|
35
|
+
document = PWN::Plugins::Vault.dump(file: @path)
|
|
36
|
+
expect(document.dig(:ai, :ollama, :model)).to eq('fixture-model')
|
|
37
|
+
expect(document[:retained]).to eq('fixture-value')
|
|
38
|
+
expect(document.dig(:ai, :agent)).to eq(PWN::Config.env_template.dig(:ai, :agent))
|
|
39
|
+
bytes = File.binread(@path)
|
|
40
|
+
expect(described_class.backfill_vault(io: @io)[:added]).to eq(0)
|
|
41
|
+
expect(File.binread(@path)).to eq(bytes)
|
|
42
|
+
expect(Dir.children(@root)).to eq(['pwn.yaml'])
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it 'uses driver-selected hardware vault paths without touching the canonical vault' do
|
|
46
|
+
custom = File.join(@root, 'selected-vault')
|
|
47
|
+
FileUtils.cp(@path, custom)
|
|
48
|
+
PWN::Env[:driver_opts] = { pwn_env_path: custom, pwn_dec_path: File.join(@root, 'absent-sidecar') }
|
|
49
|
+
canonical = File.binread(@path)
|
|
50
|
+
expect(described_class.backfill_vault(io: @io)[:added]).to be_positive
|
|
51
|
+
expect(described_class.vault_drift[:missing]).to be_empty
|
|
52
|
+
expect(File.binread(@path)).to eq(canonical)
|
|
53
|
+
expect(PWN::Plugins::Vault.hardware_encrypted?(file: custom)).to be(true)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it 'honors an explicit independent legacy decryptor and vault over driver defaults' do
|
|
57
|
+
custom = File.join(@root, 'legacy-vault')
|
|
58
|
+
sidecar = File.join(@root, 'separate-legacy-decryptor')
|
|
59
|
+
File.write(custom, YAML.dump(@original))
|
|
60
|
+
PWN::Plugins::Vault.create(file: custom, decryptor_file: sidecar)
|
|
61
|
+
canonical = File.binread(@path)
|
|
62
|
+
credentials = YAML.safe_load_file(sidecar, symbolize_names: true)
|
|
63
|
+
result = described_class.backfill_vault(pwn_env_path: custom, pwn_dec_path: sidecar, io: @io)
|
|
64
|
+
expect(result[:added]).to be_positive
|
|
65
|
+
expect(PWN::Plugins::Vault.dump(credentials.merge(file: custom)).dig(:ai, :agent)).to be_a(Hash)
|
|
66
|
+
expect(File.binread(@path)).to eq(canonical)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it 'forwards explicit vault selection and hardware overrides through numbered migrations and fix runs' do
|
|
70
|
+
custom = File.join(@root, 'selected-vault')
|
|
71
|
+
FileUtils.cp(@path, custom)
|
|
72
|
+
canonical = File.binread(@path)
|
|
73
|
+
allow(PWN::Backups).to receive(:active?).and_return(true)
|
|
74
|
+
allow(described_class).to receive(:status).and_return(schema_installed: 3, schema_current: 4, pwn_version: 'fixture', incompatible: [])
|
|
75
|
+
allow(described_class).to receive(:stamp!)
|
|
76
|
+
stub_const('PWN::Migrate::SCHEMA_VERSION', 4)
|
|
77
|
+
allow(PWN::Plugins::YubiKey).to receive(:challenge_response) do |opts|
|
|
78
|
+
raise IOError, 'selector was not forwarded' unless opts[:device_path] == '/fixture/native-device'
|
|
79
|
+
|
|
80
|
+
OpenSSL::HMAC.digest('SHA1', 'synthetic-slot-secret', opts[:challenge])
|
|
81
|
+
end
|
|
82
|
+
[false, true].each do |fix|
|
|
83
|
+
File.binwrite(custom, canonical)
|
|
84
|
+
allow(described_class).to receive(:status).and_return(schema_installed: fix ? 4 : 3, schema_current: 4, pwn_version: 'fixture', incompatible: [])
|
|
85
|
+
result = described_class.run(fix: fix, backup: false, io: @io, pwn_env_path: custom, yubikey: { device_path: '/fixture/native-device' })
|
|
86
|
+
expect(result[:applied_migrations]).to eq(fix ? [] : [4])
|
|
87
|
+
document = PWN::Plugins::Vault.dump(file: custom, yubikey: { device_path: '/fixture/native-device' })
|
|
88
|
+
expect(document.dig(:ai, :agent)).to be_a(Hash)
|
|
89
|
+
expect(File.binread(@path) == canonical).to be(true)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it 'reports the explicitly selected vault rather than a missing canonical file during migration' do
|
|
94
|
+
custom = File.join(@root, 'selected-vault')
|
|
95
|
+
File.rename(@path, custom)
|
|
96
|
+
stub_const('PWN::Migrate::SCHEMA_FILE', File.join(@root, '.schema'))
|
|
97
|
+
File.write(described_class::SCHEMA_FILE, JSON.generate(schema: described_class::SCHEMA_VERSION))
|
|
98
|
+
allow(PWN::Backups).to receive(:active?).and_return(true)
|
|
99
|
+
result = described_class.run(fix: true, backup: false, io: @io, pwn_env_path: custom)
|
|
100
|
+
expect(result[:before][:incompatible]).to be_empty
|
|
101
|
+
expect(result[:after][:incompatible]).to be_empty
|
|
102
|
+
expect(result[:after][:files].find { |row| row[:rel] == 'pwn.yaml' }[:path]).to eq(custom)
|
|
103
|
+
expect(File).not_to exist(@path)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
it 'does not write during a dry run or when hardware encryption fails or is interrupted' do
|
|
107
|
+
bytes = File.binread(@path)
|
|
108
|
+
expect(described_class.backfill_vault(dry_run: true, io: @io)[:added]).to be_positive
|
|
109
|
+
expect(File.binread(@path)).to eq(bytes)
|
|
110
|
+
allow(PWN::Plugins::Vault).to receive(:encrypt_data).and_raise(IOError, 'synthetic device unavailable')
|
|
111
|
+
expect(described_class.backfill_vault(io: @io)).to be_nil
|
|
112
|
+
expect(File.binread(@path)).to eq(bytes)
|
|
113
|
+
allow(PWN::Plugins::Vault).to receive(:encrypt_data).and_raise(Interrupt)
|
|
114
|
+
expect { described_class.backfill_vault(io: @io) }.to raise_error(Interrupt)
|
|
115
|
+
expect(File.binread(@path)).to eq(bytes)
|
|
116
|
+
expect(Dir.children(@root)).to eq(['pwn.yaml'])
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
it 'reports missing keys in a hardware vault without a software decryptor' do
|
|
120
|
+
bytes = File.binread(@path)
|
|
121
|
+
expect(PWN::Plugins::AuthenticationHelper).not_to receive(:mask_password)
|
|
122
|
+
result = described_class.vault_drift
|
|
123
|
+
expect(result).to include(user: @original)
|
|
124
|
+
expect(result[:missing]).to include('ai.agent')
|
|
125
|
+
expect(File.binread(@path)).to eq(bytes)
|
|
126
|
+
expect(File).not_to exist("#{@path}.decryptor")
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -51,6 +51,14 @@ describe PWN::ModuleSkills do
|
|
|
51
51
|
expect(described_class).to respond_to :help
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
+
it 'identifies the file-owning class rather than its enclosing module or nested error class' do
|
|
55
|
+
lib = File.expand_path('../../../lib', __dir__)
|
|
56
|
+
rec = described_class.send(:parse_file, path: File.join(lib, 'pwn/ai/agent/mission_namespace.rb'), lib_root: lib).first
|
|
57
|
+
expect(rec[:const]).to eq('PWN::AI::Agent::MissionNamespace')
|
|
58
|
+
expect(rec[:source]).to eq('pwn/ai/agent/mission_namespace.rb')
|
|
59
|
+
expect(described_class.render(module: rec)).to include('module: PWN::AI::Agent::MissionNamespace')
|
|
60
|
+
end
|
|
61
|
+
|
|
54
62
|
it 'maps a constant or source file to a namespace-mirrored SKILL.md path' do
|
|
55
63
|
expect(described_class.relpath(const: 'PWN::Plugins::TransparentBrowser')).to eq(
|
|
56
64
|
'pwn/plugins/transparent_browser/SKILL.md'
|