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
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'pwn/redaction'
|
|
5
|
+
|
|
6
|
+
RSpec.describe PWN::Redaction, 'hardware and MFA field redaction' do
|
|
7
|
+
it 'masks complete management-secret containers in serialized text' do
|
|
8
|
+
samples = [
|
|
9
|
+
'scp_keys=["fixture-enc", "fixture-mac"] slot=2',
|
|
10
|
+
'scp_keys: {enc: "fixture-enc", nested: ["fixture-mac", "fixture ] bracket"]} slot=2',
|
|
11
|
+
"scp_keys=[\n\"fixture-enc\",\n[\"fixture-mac\"]\n] slot=2"
|
|
12
|
+
]
|
|
13
|
+
samples.each do |text|
|
|
14
|
+
clean = described_class.redact(value: text)
|
|
15
|
+
expect(clean).not_to include('fixture-enc', 'fixture-mac', 'fixture ] bracket')
|
|
16
|
+
expect(clean).to include('[REDACTED]', 'slot=2')
|
|
17
|
+
expect(described_class.redact(value: clean)).to eq(clean)
|
|
18
|
+
end
|
|
19
|
+
expect(described_class.redact(value: 'scp_keys=["fixture-enc"')).to eq('scp_keys=[REDACTED]')
|
|
20
|
+
expect(described_class.redact(value: 'oath_secret="fixture truncated secret')).to eq('oath_secret=[REDACTED]')
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'does not let an existing mask exempt an adjacent ordinary secret from capture' do
|
|
24
|
+
text = 'password=fixture-prefix[REDACTED]fixture-suffix'
|
|
25
|
+
clean = described_class.capture(value: text, vault: false)
|
|
26
|
+
expect(clean[:redacted]).not_to include('fixture-prefix', 'fixture-suffix')
|
|
27
|
+
expect(clean[:refs]).not_to be_empty
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it 'masks key-value text before ordinary token hashing or loot capture' do
|
|
31
|
+
samples = [
|
|
32
|
+
'pin=123456', 'PUK: 654321', 'new_pin="123 456"', "'management_key' => 'fixture key'",
|
|
33
|
+
'--access-code=112233', 'oath_secret=fixture-oath', 'static_password=fixture-static',
|
|
34
|
+
'scp_enc=fixture-enc', 'mfa_code=102938', '{"pin": "123456", "puk": 654321}'
|
|
35
|
+
]
|
|
36
|
+
expect(PWN::Plugins::Vault).not_to receive(:store)
|
|
37
|
+
expect(PWN::Plugins::Vault).not_to receive(:fetch)
|
|
38
|
+
expect(PWN::Plugins::Vault).not_to receive(:expand)
|
|
39
|
+
samples.each do |text|
|
|
40
|
+
clean = described_class.redact(value: text)
|
|
41
|
+
expect(clean).to include('[REDACTED]'), text
|
|
42
|
+
expect(clean).not_to match(/123|654|112|102|fixture|\[REDACTED:[^\]]+/), text
|
|
43
|
+
expect(described_class.redact(value: clean)).to eq(clean)
|
|
44
|
+
expect(described_class.capture(value: text, entropy: true, yubikey: { slot: 2 })).to eq(redacted: clean, refs: [])
|
|
45
|
+
end
|
|
46
|
+
expect(described_class.redact(value: { api_key: 'fixture-api' })).to eq(api_key: described_class.token(kind: 'api_key', value: 'fixture-api'))
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it 'uses a constant mask for structured PINs, PUKs, MFA codes and management secrets' do
|
|
50
|
+
fields = %w[pin PIN new_pin current-pin admin_pin user_pin fido2_pin piv_pin openpgp_pin puk new_puk access_code management_key management_password oath_secret static_password scp scp_key scp_keys scp_enc scp_mac scp_dek scp03_enc_key mfa_code mfa_token otp totp hotp otp_code totp_secret recovery_code verification_code]
|
|
51
|
+
fields.each do |field|
|
|
52
|
+
first = described_class.redact(value: { field => '123456' })
|
|
53
|
+
second = described_class.redact(value: { field => '654321' })
|
|
54
|
+
expect(first).to eq(field => '[REDACTED]'), field
|
|
55
|
+
expect(second).to eq(first), field
|
|
56
|
+
end
|
|
57
|
+
input = { nested: [{ pin: 1234, puk: nil, scp_keys: %w[fixture-one fixture-two] }], slot: 2, serial: '12345678', shipping: 'public' }
|
|
58
|
+
expect(described_class.redact(value: input)).to eq(nested: [{ pin: '[REDACTED]', puk: nil, scp_keys: '[REDACTED]' }], slot: 2, serial: '12345678', shipping: 'public')
|
|
59
|
+
expect(input[:nested].first[:pin]).to eq(1234)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -32,7 +32,7 @@ RSpec.describe 'Bundled session cleanup' do
|
|
|
32
32
|
File.write(policy, JSON.generate(config))
|
|
33
33
|
File.write(PWN::Migrate::SCHEMA_FILE, JSON.generate(schema: 10))
|
|
34
34
|
upgrade = PWN::Migrate.run(fix: false, backup: false, io: StringIO.new)
|
|
35
|
-
raise 'missing upgrade' unless upgrade[:applied_migrations] == [11]
|
|
35
|
+
raise 'missing upgrade' unless upgrade[:applied_migrations] == [11, 12]
|
|
36
36
|
raise 'overwritten policy' unless JSON.parse(File.read(policy))['batch_size'] == 7
|
|
37
37
|
raise 'non-idempotent upgrade' unless PWN::Migrate.run(fix: false, backup: false, io: StringIO.new)[:applied_migrations].empty?
|
|
38
38
|
raise 'wrong helper' unless File.read(File.join(PWN::Cron.cron_dir, 'session_cleanup.rb')).include?('session_cleanup v10')
|
|
@@ -60,8 +60,10 @@ RSpec.describe 'Bundled session cleanup' do
|
|
|
60
60
|
begin
|
|
61
61
|
job = PWN::Plugins::Jobs.start(command: command, session_id: sid, cwd: Dir.home, max_runtime: 20)
|
|
62
62
|
raise 'job not running' unless PWN::Plugins::Jobs.status(id: job[:id])[:status] == 'RUNNING'
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
proc_root = File.join(Dir.home, 'fixture-proc')
|
|
64
|
+
FileUtils.mkdir_p(proc_root)
|
|
65
|
+
maintenance = PWN::SessionCleanup.coordinate(dry_run: false, proc_root: proc_root)
|
|
66
|
+
raise "missing coordinator phases: \#{maintenance.inspect}" unless maintenance[:phases].keys == %i[maintenance memory_consolidation learning_retention mistakes_retention policy_retention]
|
|
65
67
|
raise 'active job session deleted' unless File.file?(File.join(PWN::Sessions.sessions_dir, sid + '.jsonl'))
|
|
66
68
|
raise 'job dependency omitted' unless PWN::Sessions.dependency_index[:dependencies].fetch(sid).any? { |path| path.include?('/jobs/') }
|
|
67
69
|
File.write(release, 'release')
|
|
@@ -79,7 +81,7 @@ RSpec.describe 'Bundled session cleanup' do
|
|
|
79
81
|
RUBY
|
|
80
82
|
output, error, ran = Open3.capture3(env, RbConfig.ruby, '-e', script, chdir: @tmp)
|
|
81
83
|
expect(ran.success?).to be(true), "#{output}\n#{error}"
|
|
82
|
-
expect(JSON.parse(output)).to include('installed' => true, 'schema' =>
|
|
84
|
+
expect(JSON.parse(output)).to include('installed' => true, 'schema' => 12)
|
|
83
85
|
end
|
|
84
86
|
|
|
85
87
|
it 'never seeds curriculum on fresh install, upgrade or repeated migration after deletion' do
|
|
@@ -187,7 +189,7 @@ RSpec.describe 'Bundled session cleanup' do
|
|
|
187
189
|
original = PWN::Cron.create(name: 'session_cleanup', schedule: '12 9 * * *', ruby: 'custom guard', enabled: false)
|
|
188
190
|
File.write(File.join(@tmp, '.schema'), JSON.generate(schema: 7))
|
|
189
191
|
result = PWN::Migrate.run(fix: false, backup: false, io: StringIO.new)
|
|
190
|
-
expect(result[:applied_migrations]).to eq([8, 9, 10, 11])
|
|
192
|
+
expect(result[:applied_migrations]).to eq([8, 9, 10, 11, 12])
|
|
191
193
|
expect(PWN::Cron.list.fetch(original[:id])).to eq(original)
|
|
192
194
|
expect(PWN::Cron.list.values.count { |job| job[:name] == 'session_cleanup' }).to eq(1)
|
|
193
195
|
expect(File).to exist(File.join(PWN::Cron.cron_dir, 'session_cleanup.rb'))
|
|
@@ -183,8 +183,8 @@ RSpec.describe PWN::SessionCleanup do
|
|
|
183
183
|
Thread.current[:pwn_steering_input] = nil
|
|
184
184
|
end
|
|
185
185
|
|
|
186
|
-
it '
|
|
187
|
-
file = File.join(
|
|
186
|
+
it 'leaves centralized recovery expiry to shared admission rather than quarantine pruning' do
|
|
187
|
+
file = File.join(described_class.disposable_directory('tmp'), 'expired-backup')
|
|
188
188
|
FileUtils.mkdir_p(File.dirname(file))
|
|
189
189
|
File.write(file, 'disposable')
|
|
190
190
|
described_class.register_disposable(path: file, category: 'tmp', expires_at: 0)
|
|
@@ -192,11 +192,10 @@ RSpec.describe PWN::SessionCleanup do
|
|
|
192
192
|
backup = result[:records].first['backup']
|
|
193
193
|
old = Time.now - (100 * 86_400)
|
|
194
194
|
File.utime(old, old, result[:ledger])
|
|
195
|
-
expect(described_class.prune_quarantine(dry_run: true)[:eligible]).to eq([
|
|
196
|
-
expect(
|
|
197
|
-
expect(
|
|
198
|
-
expect(File.
|
|
199
|
-
expect(JSON.parse(File.read(result[:ledger]))['state']).to eq('expired')
|
|
195
|
+
expect(described_class.prune_quarantine(dry_run: true)[:eligible]).to eq([])
|
|
196
|
+
expect(described_class.prune_quarantine(dry_run: false)[:deleted]).to eq([])
|
|
197
|
+
expect(File.read(backup)).to eq('disposable')
|
|
198
|
+
expect(JSON.parse(File.read(result[:ledger]))['state']).to eq('committed')
|
|
200
199
|
end
|
|
201
200
|
|
|
202
201
|
it 'requires explicit continuation retirement and preserves an active association' do
|
|
@@ -235,7 +234,7 @@ RSpec.describe PWN::SessionCleanup do
|
|
|
235
234
|
end
|
|
236
235
|
|
|
237
236
|
it 'preserves failed writes and exposes interrupted prepared records instead of declaring completion' do
|
|
238
|
-
file = File.join(
|
|
237
|
+
file = File.join(described_class.disposable_directory('tmp'), 'failure')
|
|
239
238
|
FileUtils.mkdir_p(File.dirname(file))
|
|
240
239
|
File.binwrite(file, "binary\xFF".b)
|
|
241
240
|
described_class.register_disposable(path: file, category: 'tmp', expires_at: 0)
|
|
@@ -252,7 +251,7 @@ RSpec.describe PWN::SessionCleanup do
|
|
|
252
251
|
end
|
|
253
252
|
|
|
254
253
|
it 'recovers exact disposable backups without overwriting changed destinations' do
|
|
255
|
-
file = File.join(
|
|
254
|
+
file = File.join(described_class.disposable_directory('tmp'), 'binary')
|
|
256
255
|
FileUtils.mkdir_p(File.dirname(file))
|
|
257
256
|
bytes = "café\xFF\x00".b
|
|
258
257
|
File.binwrite(file, bytes)
|
|
@@ -321,8 +320,9 @@ RSpec.describe PWN::SessionCleanup do
|
|
|
321
320
|
|
|
322
321
|
it 'reports all storage categories read-only and preserves unknown files' do
|
|
323
322
|
root = File.join(Dir.home, '.pwn')
|
|
324
|
-
|
|
325
|
-
|
|
323
|
+
runtime = described_class.disposable_directory('tmp')
|
|
324
|
+
FileUtils.mkdir_p(runtime)
|
|
325
|
+
unknown = File.join(runtime, 'unknown.bin')
|
|
326
326
|
File.binwrite(unknown, "\xFF\x00".b)
|
|
327
327
|
before = Dir.glob(File.join(root, '**', '*'), File::FNM_DOTMATCH).sort
|
|
328
328
|
status = described_class.status
|
|
@@ -90,7 +90,7 @@ RSpec.describe PWN::SessionCleanup do
|
|
|
90
90
|
expect(JSON.parse(File.read(report[:transaction_ledger]))['state']).to eq('interrupted')
|
|
91
91
|
end
|
|
92
92
|
|
|
93
|
-
it '
|
|
93
|
+
it 'keeps centralized copies out of the legacy compactor and retains learning rollback bytes' do
|
|
94
94
|
described_class.install
|
|
95
95
|
allow(described_class).to receive(:legacy_owners).and_return([])
|
|
96
96
|
original = File.binread(path)
|
|
@@ -103,15 +103,15 @@ RSpec.describe PWN::SessionCleanup do
|
|
|
103
103
|
PWN::Sessions.append(session_id: id, role: 'user', content: 'evidence')
|
|
104
104
|
File.write(PWN::AI::Agent::Learning::LEARNING_FILE, JSON.generate(session_id: id, outcome: 'fixture'))
|
|
105
105
|
migration = PWN::AI::Agent::Learning.migrate_evidence(dry_run: false)
|
|
106
|
-
learning_backup = migration[:ledger].
|
|
106
|
+
learning_backup = JSON.parse(File.read(migration[:ledger])).fetch('backup')
|
|
107
107
|
inventory = described_class.legacy_backups.map { |row| row[:path] }
|
|
108
|
-
expect(inventory
|
|
108
|
+
expect(inventory & (backups + [learning_backup])).to be_empty
|
|
109
109
|
config = described_class.policy
|
|
110
110
|
config['retention_days']['maintenance'] = 0
|
|
111
111
|
File.write(File.join(Dir.home, '.pwn', 'maintenance', 'policy.json'), JSON.generate(config))
|
|
112
112
|
result = described_class.prune_legacy_backups(dry_run: false)
|
|
113
|
-
expect(result[:compacted] & backups).
|
|
114
|
-
expect(File.
|
|
113
|
+
expect(result[:compacted] & backups).to be_empty
|
|
114
|
+
expect(backups.map { |file| File.binread(file) }).to eq([original, original])
|
|
115
115
|
expect(File.file?(learning_backup)).to be(true)
|
|
116
116
|
record = JSON.parse(File.read(migration[:ledger])).merge('state' => 'prepared')
|
|
117
117
|
File.write(migration[:ledger], JSON.generate(record))
|
|
@@ -125,8 +125,11 @@ RSpec.describe PWN::SessionCleanup do
|
|
|
125
125
|
layout = File.join(directory, 'layout.json')
|
|
126
126
|
File.write(layout, JSON.generate(version: 1, categories: { learning: ['learning_evidence'] }))
|
|
127
127
|
described_class.install
|
|
128
|
-
expect(JSON.parse(File.read(layout))).to include('version' =>
|
|
128
|
+
expect(JSON.parse(File.read(layout))).to include('version' => 3, 'categories' => described_class::STORAGE)
|
|
129
129
|
expect(File.binread(File.join(directory, 'policy.json'))).to eq(config)
|
|
130
|
+
File.write(layout, JSON.generate(version: 2, categories: { 'tmp' => ['tmp'] }))
|
|
131
|
+
described_class.install
|
|
132
|
+
expect(JSON.parse(File.read(layout))).to include('version' => 3, 'categories' => described_class::STORAGE)
|
|
130
133
|
File.write(layout, JSON.generate(version: 99, categories: {}))
|
|
131
134
|
expect { described_class.install }.to raise_error(IOError, /inventory/)
|
|
132
135
|
end
|
|
@@ -146,7 +149,7 @@ RSpec.describe PWN::SessionCleanup do
|
|
|
146
149
|
lease&.close
|
|
147
150
|
end
|
|
148
151
|
|
|
149
|
-
it '
|
|
152
|
+
it 'preserves centralized continuation recovery and does not run a competing compactor' do
|
|
150
153
|
described_class.install
|
|
151
154
|
root = File.join(Dir.home, '.pwn', 'swarm')
|
|
152
155
|
swarm = '20200101_010203_abcdef'
|
|
@@ -165,25 +168,19 @@ RSpec.describe PWN::SessionCleanup do
|
|
|
165
168
|
File.write(File.join(Dir.home, '.pwn', 'maintenance', 'policy.json'), JSON.generate(config))
|
|
166
169
|
expect(File.identical?(*backups)).to be(false)
|
|
167
170
|
report = described_class.prune_legacy_backups(dry_run: false)
|
|
168
|
-
expect(report[:compacted]
|
|
169
|
-
expect(File.identical?(*backups)).to be(
|
|
170
|
-
expect(backups.map { |file| File.binread(file) }).to eq([original, original])
|
|
171
|
-
expect(described_class.prune_legacy_backups(dry_run: false)[:compacted]).to be_empty
|
|
172
|
-
expect(described_class.recovery_status(ledger: report[:ledger])[:state]).to eq('committed')
|
|
173
|
-
# Model interruption after the atomic replacement but before commit readback.
|
|
174
|
-
interrupted = JSON.parse(File.read(report[:ledger]))
|
|
175
|
-
interrupted['state'] = 'prepared'
|
|
176
|
-
interrupted['records'].each { |row| row['state'] = 'prepared' }
|
|
177
|
-
File.write(report[:ledger], JSON.generate(interrupted))
|
|
178
|
-
expect(described_class.recover(ledger: report[:ledger], action: 'resume', dry_run: false)[:state]).to eq('committed')
|
|
171
|
+
expect(report[:compacted]).to be_empty
|
|
172
|
+
expect(File.identical?(*backups)).to be(false)
|
|
179
173
|
expect(backups.map { |file| File.binread(file) }).to eq([original, original])
|
|
174
|
+
ledger = File.join(File.dirname(backups.last), 'ledger.json')
|
|
175
|
+
expect(described_class.recover(ledger: ledger, action: 'rollback', dry_run: false)[:restored]).to eq([map])
|
|
176
|
+
expect(File.binread(map)).to eq(original)
|
|
180
177
|
end
|
|
181
178
|
|
|
182
179
|
it 'owns actual festival input through use and removes it without a global tmp sweep' do
|
|
183
180
|
observed = nil
|
|
184
181
|
allow(PWN::Plugins::Voice).to receive(:text_to_speech) do |opts|
|
|
185
182
|
observed = opts[:text_path]
|
|
186
|
-
expect(observed).to start_with("#{
|
|
183
|
+
expect(observed).to start_with("#{described_class.disposable_directory('tmp')}/")
|
|
187
184
|
expect(File.read(observed)).to eq('fixture speech')
|
|
188
185
|
expect(described_class.clean_disposables(dry_run: false)[:deleted]).to be_empty
|
|
189
186
|
end
|
|
@@ -224,11 +221,20 @@ RSpec.describe PWN::SessionCleanup do
|
|
|
224
221
|
config['retention_days']['maintenance'] = 0
|
|
225
222
|
File.write(File.join(Dir.home, '.pwn', 'maintenance', 'policy.json'), JSON.generate(config))
|
|
226
223
|
2.times do |index|
|
|
227
|
-
file = File.join(
|
|
224
|
+
file = File.join(described_class.disposable_directory('tmp'), "expiry-#{index}")
|
|
228
225
|
File.write(file, "bytes-#{index}")
|
|
229
226
|
described_class.register_disposable(path: file, category: 'tmp', expires_at: 0)
|
|
230
227
|
end
|
|
231
228
|
result = described_class.clean_disposables(dry_run: false)
|
|
229
|
+
# Exercise the retained legacy recovery reader with a genuine old layout.
|
|
230
|
+
old_directory = File.dirname(result[:ledger])
|
|
231
|
+
legacy_directory = File.join(Dir.home, '.pwn', 'maintenance', 'quarantine', 'legacy-expiry')
|
|
232
|
+
FileUtils.mkdir_p(File.dirname(legacy_directory))
|
|
233
|
+
FileUtils.mv(old_directory, legacy_directory)
|
|
234
|
+
result = JSON.parse(JSON.generate(result).gsub(old_directory, legacy_directory), symbolize_names: true)
|
|
235
|
+
data = JSON.parse(File.read(result[:ledger]).gsub(old_directory, legacy_directory))
|
|
236
|
+
File.write(result[:ledger], JSON.generate(data))
|
|
237
|
+
result[:records] = data.fetch('records')
|
|
232
238
|
backups = result[:records].map { |row| row['backup'] }
|
|
233
239
|
allow(File).to receive(:unlink).and_call_original
|
|
234
240
|
allow(File).to receive(:unlink).with(backups.last).and_raise(Interrupt)
|
|
@@ -309,6 +315,14 @@ RSpec.describe PWN::SessionCleanup, 'disposable admission and scan progress' do
|
|
|
309
315
|
described_class.cache(path: cache, bytes: 'q' * 1_048_576)
|
|
310
316
|
described_class.register_disposable(path: cache, category: 'cache', expires_at: 0)
|
|
311
317
|
removed = described_class.clean_disposables(dry_run: false)
|
|
318
|
+
old_directory = File.dirname(removed[:ledger])
|
|
319
|
+
legacy_directory = File.join(Dir.home, '.pwn', 'maintenance', 'quarantine', 'legacy-budget')
|
|
320
|
+
FileUtils.mkdir_p(File.dirname(legacy_directory))
|
|
321
|
+
FileUtils.mv(old_directory, legacy_directory)
|
|
322
|
+
removed = JSON.parse(JSON.generate(removed).gsub(old_directory, legacy_directory), symbolize_names: true)
|
|
323
|
+
data = JSON.parse(File.read(removed[:ledger]).gsub(old_directory, legacy_directory))
|
|
324
|
+
File.write(removed[:ledger], JSON.generate(data))
|
|
325
|
+
removed[:records] = data.fetch('records')
|
|
312
326
|
backup = removed[:records].first.fetch('backup')
|
|
313
327
|
config = described_class.policy.merge('work_bytes' => 8192)
|
|
314
328
|
config['retention_days']['maintenance'] = 0
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'ffi'
|
|
5
|
+
|
|
6
|
+
RSpec.describe PWN::Setup, 'native library doctor' do
|
|
7
|
+
around do |example|
|
|
8
|
+
Dir.mktmpdir('native-library-doctor-') 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
|
+
allow(PWN::Migrate).to receive(:check).and_return(incompatible: [])
|
|
18
|
+
allow(described_class).to receive(:pkg_manager).and_return(key: :apt, install: 'install-package')
|
|
19
|
+
allow(described_class).to receive(:which).and_return('/fixture/bin')
|
|
20
|
+
allow(described_class).to receive(:gem_loadable?).and_return(true)
|
|
21
|
+
allow(described_class).to receive(:detect_os_attr).with(attr: :type, fallback: anything).and_return(:linux)
|
|
22
|
+
allow(described_class).to receive(:detect_os_attr).with(attr: :arch, fallback: anything).and_return('fixture')
|
|
23
|
+
allow(FFI::DynamicLibrary).to receive(:open).and_raise(LoadError, 'fixture library missing')
|
|
24
|
+
device = double('untouched native device')
|
|
25
|
+
stub_const('PWN::Plugins::YubiKey', device)
|
|
26
|
+
%i[devices info command challenge_response oath_code].each { |method| expect(device).not_to receive(method) }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it 'reports missing optional libraries but gates only hardware and full profiles on them' do
|
|
30
|
+
%i[core hardware full].each do |profile|
|
|
31
|
+
io = StringIO.new
|
|
32
|
+
report = described_class.check(profile: profile, io: io)
|
|
33
|
+
expect(report[:ok]).to eq(profile == :core)
|
|
34
|
+
expect(report[:native_libraries_missing]).to eq(%w[hidapi pcsc])
|
|
35
|
+
expect(report[:native_gems_missing]).to eq([])
|
|
36
|
+
expect(report[:toolchain_missing]).to eq([])
|
|
37
|
+
expect(io.string).to include('Native libraries', 'hidapi', 'pcsc', 'no device discovery')
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it 'loads only library handles and accepts the alternative HIDAPI libusb backend' do
|
|
42
|
+
flags = FFI::DynamicLibrary::RTLD_LAZY | FFI::DynamicLibrary::RTLD_LOCAL
|
|
43
|
+
allow(FFI::DynamicLibrary).to receive(:open).with('libhidapi-libusb.so.0', flags).and_return(double('HID library'))
|
|
44
|
+
allow(FFI::DynamicLibrary).to receive(:open).with('libpcsclite.so.1', flags).and_return(double('PCSC library'))
|
|
45
|
+
report = described_class.check(profile: :hardware, io: StringIO.new)
|
|
46
|
+
expect(report[:ok]).to be(true)
|
|
47
|
+
expect(report[:native_libraries_missing]).to eq([])
|
|
48
|
+
expect(FFI::DynamicLibrary).to have_received(:open).with('libhidapi-hidraw.so.0', flags)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it 'recognizes the built-in macOS PCSC framework and Homebrew HIDAPI path' do
|
|
52
|
+
allow(described_class).to receive(:detect_os_attr).with(attr: :type, fallback: anything).and_return(:osx)
|
|
53
|
+
flags = FFI::DynamicLibrary::RTLD_LAZY | FFI::DynamicLibrary::RTLD_LOCAL
|
|
54
|
+
allow(FFI::DynamicLibrary).to receive(:open).with('/System/Library/Frameworks/PCSC.framework/PCSC', flags).and_return(double('system PCSC framework'))
|
|
55
|
+
allow(FFI::DynamicLibrary).to receive(:open).with('/opt/homebrew/lib/libhidapi.dylib', flags).and_return(double('HID library'))
|
|
56
|
+
report = described_class.check(profile: :hardware, io: StringIO.new)
|
|
57
|
+
expect(report[:ok]).to be(true)
|
|
58
|
+
expect(report[:native_libraries_missing]).to eq([])
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it 'lists optional native libraries in profile descriptions' do
|
|
62
|
+
io = StringIO.new
|
|
63
|
+
described_class.list_profiles(io: io)
|
|
64
|
+
expect(io.string).to include('libs: hidapi pcsc')
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'tmpdir'
|
|
5
|
+
require 'stringio'
|
|
6
|
+
|
|
7
|
+
RSpec.describe PWN::Setup, 'hardware vault doctor' do
|
|
8
|
+
around do |example|
|
|
9
|
+
Dir.mktmpdir('setup-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
|
+
@pwn_root = File.join(@root, '.pwn')
|
|
18
|
+
FileUtils.mkdir_p(@pwn_root)
|
|
19
|
+
stub_const('PWN::Env', {})
|
|
20
|
+
stub_const('PWN::Migrate::PWN_ROOT', @pwn_root)
|
|
21
|
+
stub_const('PWN::Migrate::SCHEMA_FILE', File.join(@pwn_root, '.schema'))
|
|
22
|
+
stub_const('PWN::Setup::NATIVE_GEMS', {})
|
|
23
|
+
stub_const('PWN::Setup::TOOLCHAIN', {})
|
|
24
|
+
stub_const('PWN::Setup::NATIVE_LIBRARIES', {})
|
|
25
|
+
allow(described_class).to receive(:pkg_manager).and_return(key: :fixture)
|
|
26
|
+
allow(described_class).to receive(:detect_os_attr).and_return('fixture')
|
|
27
|
+
stub_const('PWN::Plugins::YubiKey', double('synthetic YubiKey'))
|
|
28
|
+
allow(PWN::Plugins::YubiKey).to receive(:challenge_response) do |opts|
|
|
29
|
+
OpenSSL::HMAC.digest('SHA1', 'synthetic-slot-secret', opts[:challenge])
|
|
30
|
+
end
|
|
31
|
+
@path = File.join(@pwn_root, 'pwn.yaml')
|
|
32
|
+
File.binwrite(@path, PWN::Plugins::Vault.encrypt_data(data: YAML.dump(ai: { active: 'ollama' }), yubikey: { slot: 2 }))
|
|
33
|
+
@io = StringIO.new
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it 'uses Config driver paths consistently in setup and the migration state verifier' do
|
|
37
|
+
custom = File.join(@root, 'custom-vault')
|
|
38
|
+
File.rename(@path, custom)
|
|
39
|
+
PWN::Env[:driver_opts] = { pwn_env_path: custom, pwn_dec_path: File.join(@root, 'custom-sidecar') }
|
|
40
|
+
expect(PWN::Plugins::YubiKey).not_to receive(:challenge_response)
|
|
41
|
+
report = described_class.check(io: @io)
|
|
42
|
+
expect(report[:ok]).to be(true)
|
|
43
|
+
expect(report.dig(:state, :files).find { |row| row[:rel] == 'pwn.yaml' }).to include(ok: true, path: custom)
|
|
44
|
+
expect(@io.string).to include('YubiKey', 'no software decryptor required')
|
|
45
|
+
expect(File).not_to exist(@path)
|
|
46
|
+
expect(File).not_to exist(PWN::Env.dig(:driver_opts, :pwn_dec_path))
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it 'recognizes hardware vaults without a missing-decryptor warning or a device operation' do
|
|
50
|
+
bytes = File.binread(@path)
|
|
51
|
+
expect(PWN::Plugins::YubiKey).not_to receive(:challenge_response)
|
|
52
|
+
expect(PWN::Plugins::Vault).not_to receive(:create)
|
|
53
|
+
report = described_class.check(io: @io)
|
|
54
|
+
expect(report[:ok]).to be(true)
|
|
55
|
+
expect(report.dig(:state, :incompatible)).to be_empty
|
|
56
|
+
expect(@io.string).to include('YubiKey', 'no software decryptor required')
|
|
57
|
+
expect(@io.string).not_to include('decryptor MISSING')
|
|
58
|
+
expect(File.binread(@path)).to eq(bytes)
|
|
59
|
+
expect(Dir.children(@pwn_root)).to eq(['pwn.yaml'])
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe PWN::Setup, 'optional native YubiKey libraries' do
|
|
6
|
+
before do
|
|
7
|
+
allow(described_class).to receive(:check).and_return(ok: true)
|
|
8
|
+
allow(described_class).to receive(:gem_loadable?).and_return(true)
|
|
9
|
+
expect(described_class).not_to receive(:system)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it 'replaces executable provisioning with optional HIDAPI and PC/SC libraries' do
|
|
13
|
+
expect(described_class::TOOLCHAIN).not_to have_key('ykman')
|
|
14
|
+
expect(described_class::PROFILES.fetch(:hardware).fetch(:bins)).not_to include('ykman')
|
|
15
|
+
expect(described_class::PROFILES.fetch(:full).fetch(:bins)).not_to include('ykman')
|
|
16
|
+
expect(described_class::PROFILES.fetch(:hardware).fetch(:libs)).to eq(%w[hidapi pcsc])
|
|
17
|
+
expect(described_class::PROFILES.fetch(:full).fetch(:libs)).to eq(described_class::NATIVE_LIBRARIES.keys)
|
|
18
|
+
expect(Array(described_class::PROFILES.fetch(:core)[:libs])).to be_empty
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
{ apt: %w[libhidapi-hidraw0 libpcsclite1 pcscd libccid],
|
|
22
|
+
dnf: %w[hidapi pcsc-lite-libs pcsc-lite pcsc-lite-ccid],
|
|
23
|
+
pacman: %w[hidapi pcsclite ccid], brew: %w[hidapi], port: %w[hidapi] }.each do |manager, libraries|
|
|
24
|
+
it "plans native host packages for #{manager} without a Python or CLI backend" do
|
|
25
|
+
allow(described_class).to receive(:pkg_manager).and_return(key: manager, install: 'install-package')
|
|
26
|
+
result = described_class.deps(profile: :hardware, dry_run: true, io: StringIO.new, distro: :fixture, version: 'test')
|
|
27
|
+
commands = result.fetch(:ran).map { |row| row.fetch(:cmd) }
|
|
28
|
+
libraries.each { |package| expect(commands).to include("install-package #{package}") }
|
|
29
|
+
expect(commands.join('\n')).not_to match(/ykman|yubikey-manager|python|\bpip(?:[23x])?\b/)
|
|
30
|
+
expect(result.fetch(:ran)).to all(include(dry_run: true))
|
|
31
|
+
expect(commands.join('\n')).not_to match(/pcsc|ccid/) if %i[brew port].include?(manager)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it 'does not install hardware packages in the core profile' do
|
|
36
|
+
allow(described_class).to receive(:pkg_manager).and_return(key: :apt, install: 'install-package')
|
|
37
|
+
result = described_class.deps(profile: :core, dry_run: true, io: StringIO.new, distro: :fixture, version: 'test')
|
|
38
|
+
expect(result.fetch(:ran).map { |row| row.fetch(:cmd) }.join('\n')).not_to match(/hidapi|pcsclite|pcscd|ccid|ykman/)
|
|
39
|
+
end
|
|
40
|
+
end
|