pwn 0.5.783 → 0.5.785
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.ruby-version +1 -1
- data/Gemfile +8 -8
- data/README.md +37 -0
- data/Rakefile +19 -1
- data/bin/pwn +2 -1
- data/bin/pwn_setup +1 -1
- data/bin/pwn_web_cache_deception +35 -7
- data/build_gem.sh +1 -1
- data/documentation/AI-Integration.md +7 -5
- data/documentation/Agent-Tool-Registry.md +2 -2
- data/documentation/Backup-Retention.md +82 -0
- data/documentation/Configuration.md +10 -6
- data/documentation/Contributing.md +1 -1
- data/documentation/Cron.md +64 -35
- data/documentation/Diagrams.md +2 -2
- data/documentation/General-PWN-Usage.md +4 -3
- data/documentation/Home.md +6 -5
- data/documentation/How-PWN-Works.md +7 -6
- data/documentation/Installation.md +254 -10
- data/documentation/Mission-Cancellation.md +134 -0
- data/documentation/Operability.md +17 -0
- data/documentation/Persistence.md +8 -6
- data/documentation/Plugin-Degradation.md +12 -0
- data/documentation/Plugins.md +3 -2
- data/documentation/Reinforcement-Learning.md +5 -6
- data/documentation/Runtime-Source-Protection.md +162 -0
- data/documentation/Transparent-Browser.md +11 -0
- data/documentation/Troubleshooting.md +3 -2
- data/documentation/What-is-PWN.md +2 -2
- data/documentation/YubiKey-Vaults.md +183 -0
- data/documentation/YubiKey.md +339 -0
- data/documentation/diagrams/cron-scheduling.svg +130 -128
- data/documentation/diagrams/dot/cron-scheduling.dot +5 -5
- data/documentation/diagrams/dot/persistence-filesystem.dot +5 -3
- data/documentation/diagrams/dot/plugin-ecosystem.dot +2 -2
- data/documentation/diagrams/persistence-filesystem.svg +172 -142
- data/documentation/diagrams/plugin-ecosystem.svg +64 -64
- data/documentation/pwn-ai-Agent.md +10 -4
- data/etc/default_cron/session_cleanup.rb +1245 -22
- data/etc/default_skills/pwn/ai/agent/mission_namespace/SKILL.md +21 -0
- data/etc/default_skills/pwn/ai/agent/tools/browser_act/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/tools/loot_query/SKILL.md +27 -26
- data/etc/default_skills/pwn/ai/agent/tools/recon_run/SKILL.md +25 -15
- data/etc/default_skills/pwn/ai/agent/tools/workspace_tool/SKILL.md +24 -0
- data/etc/default_skills/pwn/ai/server_tools/SKILL.md +25 -0
- data/etc/default_skills/pwn/backups/SKILL.md +54 -0
- data/etc/default_skills/pwn/plugins/recon/SKILL.md +12 -1
- data/etc/default_skills/pwn/plugins/yubikey/SKILL.md +157 -0
- data/etc/default_skills/pwn/plugins/yubikey/fido/SKILL.md +189 -0
- data/etc/default_skills/pwn/plugins/yubikey/hsmauth/SKILL.md +83 -0
- data/etc/default_skills/pwn/plugins/yubikey/management/SKILL.md +80 -0
- data/etc/default_skills/pwn/plugins/yubikey/oath/SKILL.md +115 -0
- data/etc/default_skills/pwn/plugins/yubikey/openpgp/SKILL.md +144 -0
- data/etc/default_skills/pwn/plugins/yubikey/otp/SKILL.md +97 -0
- data/etc/default_skills/pwn/plugins/yubikey/piv/SKILL.md +110 -0
- data/etc/default_skills/pwn/plugins/yubikey/protocol/SKILL.md +21 -0
- data/etc/default_skills/pwn/plugins/yubikey/security_domain/SKILL.md +75 -0
- data/etc/default_skills/pwn/plugins/yubikey/security_domain/scp/SKILL.md +46 -0
- data/etc/default_skills/pwn/plugins/yubikey/transport/SKILL.md +98 -0
- data/etc/default_skills/pwn/redaction/SKILL.md +13 -1
- data/lib/pwn/ai/agent/dispatch.rb +26 -15
- data/lib/pwn/ai/agent/extrospection.rb +6 -7
- data/lib/pwn/ai/agent/learning.rb +62 -25
- data/lib/pwn/ai/agent/loop.rb +17 -1
- data/lib/pwn/ai/agent/mission_namespace.rb +331 -0
- data/lib/pwn/ai/agent/policy.rb +5 -0
- data/lib/pwn/ai/agent/policy_evaluation.rb +8 -8
- data/lib/pwn/ai/agent/prompt_builder.rb +13 -2
- data/lib/pwn/ai/agent/request_runtime.rb +122 -18
- data/lib/pwn/ai/agent/reward.rb +5 -3
- data/lib/pwn/ai/agent/skill_consolidation.rb +3 -2
- data/lib/pwn/ai/agent/skill_review.rb +9 -5
- data/lib/pwn/ai/agent/swarm.rb +35 -16
- data/lib/pwn/ai/agent/tools/browser_act.rb +32 -0
- data/lib/pwn/ai/agent/tools/loot_query.rb +11 -1
- data/lib/pwn/ai/agent/tools/recon_run.rb +11 -1
- data/lib/pwn/ai/agent/tools/workspace_tool.rb +29 -0
- data/lib/pwn/ai/agent/turn_finalizer.rb +6 -2
- data/lib/pwn/ai/agent.rb +1 -0
- data/lib/pwn/ai/anthropic.rb +8 -33
- data/lib/pwn/ai/cli.rb +34 -5
- data/lib/pwn/ai/gemini.rb +1 -0
- data/lib/pwn/ai/grok.rb +39 -28
- data/lib/pwn/ai/ollama.rb +4 -2
- data/lib/pwn/ai/open_ai.rb +11 -32
- data/lib/pwn/ai/open_web_ui.rb +62 -4
- data/lib/pwn/ai/router.rb +53 -3
- data/lib/pwn/ai/server_tools.rb +231 -0
- data/lib/pwn/ai.rb +1 -0
- data/lib/pwn/backups.rb +347 -0
- data/lib/pwn/config.rb +70 -39
- data/lib/pwn/cron.rb +120 -49
- data/lib/pwn/migrate.rb +150 -68
- data/lib/pwn/module_skills.rb +7 -0
- data/lib/pwn/plugins/ai_sandbox.rb +538 -1
- data/lib/pwn/plugins/authentication_helper.rb +74 -4
- data/lib/pwn/plugins/basic_auth.rb +2 -1
- data/lib/pwn/plugins/binary_parser.rb +5 -4
- data/lib/pwn/plugins/ghidra.rb +4 -3
- data/lib/pwn/plugins/google_workspace.rb +4 -21
- data/lib/pwn/plugins/jobs.rb +1 -1
- data/lib/pwn/plugins/log.rb +21 -3
- data/lib/pwn/plugins/preflight_checker.rb +34 -9
- data/lib/pwn/plugins/recon.rb +4 -1
- data/lib/pwn/plugins/repl/ai.rb +305 -27
- data/lib/pwn/plugins/repl/ai_console.rb +273 -89
- data/lib/pwn/plugins/repl/ai_swarm.rb +181 -62
- data/lib/pwn/plugins/repl/mesh.rb +3 -16
- data/lib/pwn/plugins/repl/vault.rb +4 -15
- data/lib/pwn/plugins/repl.rb +1 -216
- data/lib/pwn/plugins/sandbox/driver.rb +4 -0
- data/lib/pwn/plugins/transparent_browser.rb +215 -0
- data/lib/pwn/plugins/vault.rb +412 -135
- data/lib/pwn/plugins/yubikey/fido.rb +1288 -0
- data/lib/pwn/plugins/yubikey/hsmauth.rb +439 -0
- data/lib/pwn/plugins/yubikey/management.rb +686 -0
- data/lib/pwn/plugins/yubikey/oath.rb +766 -0
- data/lib/pwn/plugins/yubikey/openpgp.rb +1063 -0
- data/lib/pwn/plugins/yubikey/otp.rb +897 -0
- data/lib/pwn/plugins/yubikey/piv.rb +1315 -0
- data/lib/pwn/plugins/yubikey/protocol.rb +341 -0
- data/lib/pwn/plugins/yubikey/security_domain/scp.rb +482 -0
- data/lib/pwn/plugins/yubikey/security_domain.rb +464 -0
- data/lib/pwn/plugins/yubikey/transport.rb +1017 -0
- data/lib/pwn/plugins/yubikey.rb +844 -0
- data/lib/pwn/plugins.rb +1 -0
- data/lib/pwn/redaction.rb +69 -12
- data/lib/pwn/session_trace.rb +4 -0
- data/lib/pwn/sessions.rb +58 -16
- data/lib/pwn/setup.rb +123 -41
- data/lib/pwn/version.rb +1 -1
- data/lib/pwn/www/app_cobalt_io.rb +16 -5
- data/lib/pwn/www/bug_crowd.rb +16 -5
- data/lib/pwn/www/coinbase_pro.rb +16 -5
- data/lib/pwn/www/github.rb +30 -14
- data/lib/pwn/www/paypal.rb +15 -4
- data/lib/pwn/www/synack.rb +16 -5
- data/lib/pwn/www/twitter.rb +17 -6
- data/lib/pwn.rb +1 -0
- data/scripts/build_gate.rb +58 -0
- data/spec/documentation/installation_md_spec.rb +1 -1
- data/spec/fixtures/yubikey/apdu_frames.json +162 -0
- data/spec/integration/authentication_helper_oath_spec.rb +117 -0
- data/spec/integration/build_gate_spec.rb +73 -0
- data/spec/integration/controller_spill_spec.rb +48 -0
- data/spec/integration/harness_process_spec.rb +97 -0
- data/spec/integration/mission_cancel_spec.rb +79 -0
- data/spec/integration/mission_curses_pty_spec.rb +44 -0
- data/spec/integration/persistence_roundtrip_spec.rb +1 -7
- data/spec/integration/readme_security_spec.rb +47 -0
- data/spec/integration/web_cache_deception_yubikey_spec.rb +159 -0
- data/spec/lib/pwn/ai/agent/dispatch_spec.rb +6 -3
- data/spec/lib/pwn/ai/agent/dispatch_vault_safety_spec.rb +86 -0
- data/spec/lib/pwn/ai/agent/injection_guard_spec.rb +2 -0
- data/spec/lib/pwn/ai/agent/learning_spec.rb +4 -4
- data/spec/lib/pwn/ai/agent/loop_artifacts_spec.rb +5 -0
- data/spec/lib/pwn/ai/agent/loop_spec.rb +104 -29
- data/spec/lib/pwn/ai/agent/mission_namespace_spec.rb +138 -0
- data/spec/lib/pwn/ai/agent/policy_spec.rb +27 -0
- data/spec/lib/pwn/ai/agent/request_runtime_spec.rb +48 -0
- data/spec/lib/pwn/ai/agent/shell_search_effect_spec.rb +44 -0
- data/spec/lib/pwn/ai/agent/solve_spec.rb +6 -4
- data/spec/lib/pwn/ai/agent/tools/browser_act_spec.rb +44 -0
- data/spec/lib/pwn/ai/agent/tools/loot_query_yubikey_spec.rb +55 -0
- data/spec/lib/pwn/ai/agent/tools/recon_run_yubikey_spec.rb +43 -0
- data/spec/lib/pwn/ai/agent/tools/workspace_tool_spec.rb +20 -0
- data/spec/lib/pwn/ai/agent/turn_finalizer_spec.rb +7 -0
- data/spec/lib/pwn/ai/anthropic_vault_enrollment_spec.rb +37 -0
- data/spec/lib/pwn/ai/cli_spec.rb +6 -9
- data/spec/lib/pwn/ai/oauth_vault_persistence_spec.rb +195 -0
- data/spec/lib/pwn/ai/open_ai_spec.rb +2 -2
- data/spec/lib/pwn/ai/open_web_ui_workspace_spec.rb +36 -0
- data/spec/lib/pwn/ai/router_spec.rb +31 -3
- data/spec/lib/pwn/ai/server_tools_spec.rb +39 -0
- data/spec/lib/pwn/backups_spec.rb +114 -0
- data/spec/lib/pwn/config_redaction_yubikey_spec.rb +28 -0
- data/spec/lib/pwn/config_yubikey_spec.rb +92 -0
- data/spec/lib/pwn/cron_spec.rb +113 -6
- data/spec/lib/pwn/migrate_spec.rb +108 -5
- data/spec/lib/pwn/migrate_yubikey_spec.rb +128 -0
- data/spec/lib/pwn/module_skills_spec.rb +8 -0
- data/spec/lib/pwn/plugins/ai_sandbox_spec.rb +230 -0
- data/spec/lib/pwn/plugins/authentication_helper_native_yubikey_spec.rb +40 -0
- data/spec/lib/pwn/plugins/authentication_helper_yubikey_spec.rb +90 -0
- data/spec/lib/pwn/plugins/capability_broker/daemon_spec.rb +1 -1
- data/spec/lib/pwn/plugins/google_workspace_spec.rb +17 -35
- data/spec/lib/pwn/plugins/log_spec.rb +7 -0
- data/spec/lib/pwn/plugins/log_yubikey_spec.rb +47 -0
- data/spec/lib/pwn/plugins/preflight_checker_native_yubikey_spec.rb +19 -0
- data/spec/lib/pwn/plugins/preflight_checker_yubikey_spec.rb +55 -0
- data/spec/lib/pwn/plugins/recon_yubikey_spec.rb +43 -0
- data/spec/lib/pwn/plugins/repl/ai_console_spec.rb +125 -78
- data/spec/lib/pwn/plugins/repl/ai_legacy_containment_spec.rb +200 -0
- data/spec/lib/pwn/plugins/repl/ai_swarm_spec.rb +30 -16
- data/spec/lib/pwn/plugins/repl/ai_vault_edit_yubikey_spec.rb +103 -0
- data/spec/lib/pwn/plugins/repl/ai_yubikey_spec.rb +48 -0
- data/spec/lib/pwn/plugins/repl/mesh_vault_persistence_spec.rb +149 -0
- data/spec/lib/pwn/plugins/repl/vault_yaml_serialization_spec.rb +11 -0
- data/spec/lib/pwn/plugins/repl/vault_yubikey_spec.rb +39 -0
- data/spec/lib/pwn/plugins/repl_spec.rb +56 -5
- data/spec/lib/pwn/plugins/transparent_browser_jev_spec.rb +46 -0
- data/spec/lib/pwn/plugins/vault_native_yubikey_spec.rb +29 -0
- data/spec/lib/pwn/plugins/vault_publication_safety_spec.rb +258 -0
- data/spec/lib/pwn/plugins/vault_store_safety_spec.rb +241 -0
- data/spec/lib/pwn/plugins/vault_yubikey_safety_spec.rb +168 -0
- data/spec/lib/pwn/plugins/vault_yubikey_spec.rb +128 -0
- data/spec/lib/pwn/plugins/vault_yubikey_vector_spec.rb +45 -0
- data/spec/lib/pwn/plugins/yubikey/fido_spec.rb +854 -0
- data/spec/lib/pwn/plugins/yubikey/hsmauth_spec.rb +235 -0
- data/spec/lib/pwn/plugins/yubikey/management_spec.rb +277 -0
- data/spec/lib/pwn/plugins/yubikey/oath_spec.rb +349 -0
- data/spec/lib/pwn/plugins/yubikey/openpgp_spec.rb +810 -0
- data/spec/lib/pwn/plugins/yubikey/otp_spec.rb +337 -0
- data/spec/lib/pwn/plugins/yubikey/piv_spec.rb +646 -0
- data/spec/lib/pwn/plugins/yubikey/protocol_secure_spec.rb +28 -0
- data/spec/lib/pwn/plugins/yubikey/protocol_spec.rb +149 -0
- data/spec/lib/pwn/plugins/yubikey/protocol_vectors_spec.rb +18 -0
- data/spec/lib/pwn/plugins/yubikey/security_domain/scp_spec.rb +256 -0
- data/spec/lib/pwn/plugins/yubikey/security_domain/support.rb +153 -0
- data/spec/lib/pwn/plugins/yubikey/security_domain_spec.rb +296 -0
- data/spec/lib/pwn/plugins/yubikey/transport_spec.rb +760 -0
- data/spec/lib/pwn/plugins/yubikey_security_spec.rb +260 -0
- data/spec/lib/pwn/plugins/yubikey_spec.rb +209 -0
- data/spec/lib/pwn/plugins/yubikey_terminal_spec.rb +147 -0
- data/spec/lib/pwn/redaction_spec.rb +13 -10
- data/spec/lib/pwn/redaction_yubikey_spec.rb +61 -0
- data/spec/lib/pwn/session_cleanup_concurrency_spec.rb +201 -0
- data/spec/lib/pwn/session_cleanup_default_spec.rb +167 -2
- data/spec/lib/pwn/session_cleanup_legacy_spec.rb +101 -0
- data/spec/lib/pwn/session_cleanup_payload_spec.rb +335 -2
- data/spec/lib/pwn/session_cleanup_recovery_spec.rb +403 -0
- data/spec/lib/pwn/sessions_spec.rb +112 -0
- data/spec/lib/pwn/setup_native_yubikey_spec.rb +66 -0
- data/spec/lib/pwn/setup_spec.rb +3 -1
- data/spec/lib/pwn/setup_vault_yubikey_spec.rb +61 -0
- data/spec/lib/pwn/setup_yubikey_spec.rb +40 -0
- data/spec/lib/pwn/www/login_yubikey_spec.rb +273 -0
- data/spec/spec_helper.rb +15 -0
- data/spec/support/gate_failure_formatter.rb +14 -0
- data/spec/support/harness/cli_faults.rb +13 -9
- data/spec/support/harness/controller_spill.rb +26 -0
- data/spec/support/harness/controller_state.rb +54 -0
- data/spec/support/harness/evaluate.rb +12 -1
- data/spec/support/harness/experiments.rb +1 -1
- data/spec/support/harness/fake_provider.rb +22 -6
- data/spec/support/harness/fault_preload.rb +3 -1
- data/spec/support/harness/mission_console.rb +54 -0
- data/spec/support/harness/mission_curses_pty.rb +41 -0
- data/spec/support/harness/mission_dispatch.rb +30 -0
- data/spec/support/harness/mission_fixture_preload.rb +24 -0
- data/spec/support/harness/mission_job_lifetime.rb +25 -0
- data/spec/support/harness/mission_legacy.rb +25 -0
- data/spec/support/harness/mission_normal_exit.rb +34 -0
- data/spec/support/harness/mission_swarm.rb +23 -0
- data/spec/support/harness/mission_unsupported.rb +29 -0
- data/spec/support/harness/package_acceptance.rb +33 -5
- data/spec/support/harness/session_acceptance.rb +4 -4
- data/spec/support/harness/swarm_acceptance.rb +1 -1
- data/spec/support/harness/swarm_control.rb +44 -0
- data/spec/support/mission_namespace.rb +14 -0
- data/spec/support/mission_pipe_queue.rb +33 -0
- data/third_party/pwn_rdoc.jsonl +599 -15
- metadata +140 -17
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
{
|
|
2
|
+
"reference": "Yubico yubikey-manager 5.9.1 smartcard formatters; generated offline with synthetic bytes, not a hardware capture",
|
|
3
|
+
"source": "https://github.com/Yubico/yubikey-manager/blob/5.9.1/yubikit/core/smartcard/__init__.py",
|
|
4
|
+
"vectors": [
|
|
5
|
+
{
|
|
6
|
+
"name": "short-0-0",
|
|
7
|
+
"options": {
|
|
8
|
+
"cla": 0,
|
|
9
|
+
"ins": 202,
|
|
10
|
+
"p1": 1,
|
|
11
|
+
"p2": 2,
|
|
12
|
+
"data_hex": "",
|
|
13
|
+
"le": null,
|
|
14
|
+
"extended": false
|
|
15
|
+
},
|
|
16
|
+
"expected_hex": "00ca010200"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"name": "short-0-1",
|
|
20
|
+
"options": {
|
|
21
|
+
"cla": 0,
|
|
22
|
+
"ins": 202,
|
|
23
|
+
"p1": 1,
|
|
24
|
+
"p2": 2,
|
|
25
|
+
"data_hex": "",
|
|
26
|
+
"le": 1,
|
|
27
|
+
"extended": false
|
|
28
|
+
},
|
|
29
|
+
"expected_hex": "00ca010201"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"name": "short-1-0",
|
|
33
|
+
"options": {
|
|
34
|
+
"cla": 0,
|
|
35
|
+
"ins": 202,
|
|
36
|
+
"p1": 1,
|
|
37
|
+
"p2": 2,
|
|
38
|
+
"data_hex": "00",
|
|
39
|
+
"le": null,
|
|
40
|
+
"extended": false
|
|
41
|
+
},
|
|
42
|
+
"expected_hex": "00ca01020100"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"name": "short-16-16",
|
|
46
|
+
"options": {
|
|
47
|
+
"cla": 0,
|
|
48
|
+
"ins": 202,
|
|
49
|
+
"p1": 1,
|
|
50
|
+
"p2": 2,
|
|
51
|
+
"data_hex": "000102030405060708090a0b0c0d0e0f",
|
|
52
|
+
"le": 16,
|
|
53
|
+
"extended": false
|
|
54
|
+
},
|
|
55
|
+
"expected_hex": "00ca010210000102030405060708090a0b0c0d0e0f10"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"name": "short-255-0",
|
|
59
|
+
"options": {
|
|
60
|
+
"cla": 0,
|
|
61
|
+
"ins": 202,
|
|
62
|
+
"p1": 1,
|
|
63
|
+
"p2": 2,
|
|
64
|
+
"data_hex": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfe",
|
|
65
|
+
"le": null,
|
|
66
|
+
"extended": false
|
|
67
|
+
},
|
|
68
|
+
"expected_hex": "00ca0102ff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfe"
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"name": "short-255-255",
|
|
72
|
+
"options": {
|
|
73
|
+
"cla": 0,
|
|
74
|
+
"ins": 202,
|
|
75
|
+
"p1": 1,
|
|
76
|
+
"p2": 2,
|
|
77
|
+
"data_hex": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfe",
|
|
78
|
+
"le": 255,
|
|
79
|
+
"extended": false
|
|
80
|
+
},
|
|
81
|
+
"expected_hex": "00ca0102ff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff"
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"name": "extended-0-0",
|
|
85
|
+
"options": {
|
|
86
|
+
"cla": 0,
|
|
87
|
+
"ins": 202,
|
|
88
|
+
"p1": 1,
|
|
89
|
+
"p2": 2,
|
|
90
|
+
"data_hex": "",
|
|
91
|
+
"le": null,
|
|
92
|
+
"extended": true
|
|
93
|
+
},
|
|
94
|
+
"expected_hex": "00ca0102"
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"name": "extended-0-256",
|
|
98
|
+
"options": {
|
|
99
|
+
"cla": 0,
|
|
100
|
+
"ins": 202,
|
|
101
|
+
"p1": 1,
|
|
102
|
+
"p2": 2,
|
|
103
|
+
"data_hex": "",
|
|
104
|
+
"le": 256,
|
|
105
|
+
"extended": true
|
|
106
|
+
},
|
|
107
|
+
"expected_hex": "00ca0102000100"
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"name": "extended-1-0",
|
|
111
|
+
"options": {
|
|
112
|
+
"cla": 0,
|
|
113
|
+
"ins": 202,
|
|
114
|
+
"p1": 1,
|
|
115
|
+
"p2": 2,
|
|
116
|
+
"data_hex": "00",
|
|
117
|
+
"le": null,
|
|
118
|
+
"extended": true
|
|
119
|
+
},
|
|
120
|
+
"expected_hex": "00ca010200000100"
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
"name": "extended-255-256",
|
|
124
|
+
"options": {
|
|
125
|
+
"cla": 0,
|
|
126
|
+
"ins": 202,
|
|
127
|
+
"p1": 1,
|
|
128
|
+
"p2": 2,
|
|
129
|
+
"data_hex": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfe",
|
|
130
|
+
"le": 256,
|
|
131
|
+
"extended": true
|
|
132
|
+
},
|
|
133
|
+
"expected_hex": "00ca01020000ff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfe0100"
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"name": "extended-256-0",
|
|
137
|
+
"options": {
|
|
138
|
+
"cla": 0,
|
|
139
|
+
"ins": 202,
|
|
140
|
+
"p1": 1,
|
|
141
|
+
"p2": 2,
|
|
142
|
+
"data_hex": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff",
|
|
143
|
+
"le": null,
|
|
144
|
+
"extended": true
|
|
145
|
+
},
|
|
146
|
+
"expected_hex": "00ca0102000100000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff"
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
"name": "extended-512-1024",
|
|
150
|
+
"options": {
|
|
151
|
+
"cla": 0,
|
|
152
|
+
"ins": 202,
|
|
153
|
+
"p1": 1,
|
|
154
|
+
"p2": 2,
|
|
155
|
+
"data_hex": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff",
|
|
156
|
+
"le": 1024,
|
|
157
|
+
"extended": true
|
|
158
|
+
},
|
|
159
|
+
"expected_hex": "00ca0102000200000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff0400"
|
|
160
|
+
}
|
|
161
|
+
]
|
|
162
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'tty-prompt'
|
|
5
|
+
|
|
6
|
+
RSpec.describe 'AuthenticationHelper native OATH integration' do
|
|
7
|
+
let(:account) { '-例:tester@example.invalid .+ ' }
|
|
8
|
+
let(:serial) { '00123456' }
|
|
9
|
+
let(:connection) { double('offline APDU connection', descriptor: { protocol: :apdu }, close: :closed) }
|
|
10
|
+
let(:yubikey) { { account: account, confirm: true, serial: serial, connection: connection, timeout: 5 }.freeze }
|
|
11
|
+
let(:select_apdu) { ['00a4040007a0000005272101'].pack('H*') }
|
|
12
|
+
let(:list_apdu) { ['00a1000000'].pack('H*') }
|
|
13
|
+
let(:calculate_apdu) do
|
|
14
|
+
body = [0x71, account.bytesize].pack('CC') + account.b + [0x74, 8].pack('CC') + [1].pack('Q>')
|
|
15
|
+
[0, 0xa2, 0, 1, body.bytesize].pack('C*') + body
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
before do
|
|
19
|
+
@ready = false
|
|
20
|
+
@frames = []
|
|
21
|
+
allow(Time).to receive(:now).and_return(Time.at(59))
|
|
22
|
+
allow(TTY::Prompt).to receive(:new).and_raise('unexpected interactive prompt')
|
|
23
|
+
expect(PWN::Plugins::YubiKey::Transport).not_to receive(:open)
|
|
24
|
+
expect(connection).not_to receive(:close)
|
|
25
|
+
allow(connection).to receive(:exchange) do |request|
|
|
26
|
+
expect(@ready).to be(true)
|
|
27
|
+
expect(request[:protocol]).to eq(:apdu)
|
|
28
|
+
@frames << request[:data]
|
|
29
|
+
case request[:data]
|
|
30
|
+
when select_apdu
|
|
31
|
+
[0x79, 3, 5, 7, 1, 0x71, 8].pack('C*') + 'fixture!'.b + "\x90\0".b
|
|
32
|
+
when list_apdu
|
|
33
|
+
[0x72, account.bytesize + 1, 0x21].pack('C*') + account.b + "\x90\0".b
|
|
34
|
+
when calculate_apdu
|
|
35
|
+
[0x76, 5, 8, 12_345].pack('CCCN') + "\x90\0".b
|
|
36
|
+
else
|
|
37
|
+
raise 'Unexpected APDU in offline OATH fixture'
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it 'runs the real helper, root facade, OATH.code and Protocol against a fake APDU transport' do
|
|
43
|
+
@ready = true
|
|
44
|
+
options = { yubikey: yubikey }.freeze
|
|
45
|
+
expect do
|
|
46
|
+
expect(PWN::Plugins::AuthenticationHelper.mfa(options)).to eq('00012345')
|
|
47
|
+
end.not_to output.to_stdout
|
|
48
|
+
expect(@frames).to eq([select_apdu, list_apdu, calculate_apdu])
|
|
49
|
+
expect(options[:yubikey]).to equal(yubikey)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
[nil, false, 'true'].each do |confirm|
|
|
53
|
+
it "rejects unconfirmed OATH #{confirm.inspect} before any APDU or prompt" do
|
|
54
|
+
expect do
|
|
55
|
+
PWN::Plugins::AuthenticationHelper.mfa(yubikey: yubikey.merge(confirm: confirm).freeze)
|
|
56
|
+
end.to raise_error(ArgumentError, /confirm: true/)
|
|
57
|
+
expect(@frames).to be_empty
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it 'propagates exact-account errors without calculation, credential repair or manual fallback' do
|
|
62
|
+
@ready = true
|
|
63
|
+
expect do
|
|
64
|
+
PWN::Plugins::AuthenticationHelper.mfa(yubikey: yubikey.merge(account: 'missing@example.invalid'))
|
|
65
|
+
end.to raise_error(PWN::Plugins::YubiKey::ResponseError, /exact account/)
|
|
66
|
+
expect(@frames).to eq([select_apdu, list_apdu])
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
context 'through the real GitHub login and helper' do
|
|
70
|
+
let(:browser) { double('offline browser') }
|
|
71
|
+
let(:browser_obj) { { browser: browser } }
|
|
72
|
+
let(:credential_field) { double('offline credential field', set: nil, present?: true) }
|
|
73
|
+
let(:otp_field) { double('offline OATH field', present?: true, exist?: true) }
|
|
74
|
+
let(:submit) { double('offline submit', exist?: true) }
|
|
75
|
+
let(:login_opts) { { browser_obj: browser_obj, username: 'tester@example.invalid', password: 'fake-password', mfa: false, yubikey: yubikey } }
|
|
76
|
+
|
|
77
|
+
before do
|
|
78
|
+
@authenticated = false
|
|
79
|
+
@otp_written = false
|
|
80
|
+
allow(PWN::WWW::GitHub).to receive(:sleep)
|
|
81
|
+
allow(browser).to receive(:goto)
|
|
82
|
+
allow(browser).to receive(:url) { @authenticated ? 'https://github.com/' : 'https://github.com/sessions/two-factor' }
|
|
83
|
+
allow(browser).to receive(:text_field).and_return(credential_field)
|
|
84
|
+
allow(credential_field).to receive(:wait_until).and_return(credential_field)
|
|
85
|
+
allow(browser).to receive(:text_field).with(name: 'app_otp').and_return(otp_field)
|
|
86
|
+
allow(otp_field).to receive(:wait_until) do
|
|
87
|
+
expect(@frames).to be_empty
|
|
88
|
+
@ready = true
|
|
89
|
+
otp_field
|
|
90
|
+
end
|
|
91
|
+
allow(browser).to receive(:input).with(name: 'commit').and_return(submit)
|
|
92
|
+
allow(submit).to receive(:click!) { @authenticated = true if @otp_written }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it 'types actual native OATH digits only after the authenticator field is ready' do
|
|
96
|
+
expect(otp_field).to receive(:set).with('00012345') { @otp_written = true }
|
|
97
|
+
expect(PWN::WWW::GitHub.login(login_opts)).to equal(browser_obj)
|
|
98
|
+
expect(@frames).to eq([select_apdu, list_apdu, calculate_apdu])
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it 'does not touch a token for an already authenticated redirect' do
|
|
102
|
+
@authenticated = true
|
|
103
|
+
expect(otp_field).not_to receive(:set)
|
|
104
|
+
expect(PWN::WWW::GitHub.login(login_opts)).to equal(browser_obj)
|
|
105
|
+
expect(@frames).to be_empty
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it 'rejects absent consent before asking for a password or navigating' do
|
|
109
|
+
expect(PWN::Plugins::AuthenticationHelper).not_to receive(:mask_password)
|
|
110
|
+
expect(browser).not_to receive(:goto)
|
|
111
|
+
expect do
|
|
112
|
+
PWN::WWW::GitHub.login(login_opts.except(:password).merge(yubikey: yubikey.except(:confirm)))
|
|
113
|
+
end.to raise_error(ArgumentError, /confirm: true/)
|
|
114
|
+
expect(@frames).to be_empty
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'open3'
|
|
5
|
+
require 'tmpdir'
|
|
6
|
+
|
|
7
|
+
RSpec.describe 'build gate diagnostics' do
|
|
8
|
+
it 'reports the RSpec child signal rather than only a generic command failure' do
|
|
9
|
+
Dir.mktmpdir('pwn-rake-signal-') do |home|
|
|
10
|
+
spec = File.join(home, 'terminated_spec.rb')
|
|
11
|
+
File.write(spec, "RSpec.describe('fixture') { it('terminates') { Process.kill('KILL', Process.pid) } }\n")
|
|
12
|
+
output, status = Open3.capture2e({ 'HOME' => home, 'SPEC' => spec, 'SPEC_OPTS' => nil }, 'bundle', 'exec', 'rake', 'spec')
|
|
13
|
+
expect(status.success?).to be(false)
|
|
14
|
+
expect(output).to match(/RSpec child failed: (signal=KILL|exit=137)/)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it 'retains the first failure details even if a later example terminates RSpec' do
|
|
19
|
+
Dir.mktmpdir('pwn-rake-failure-') do |home|
|
|
20
|
+
spec = File.join(home, 'terminated_spec.rb')
|
|
21
|
+
File.write(spec, "RSpec.describe('fixture') { it('fails first') { expect(1).to eq(2) }; it('terminates') { Process.kill('KILL', Process.pid) } }\n")
|
|
22
|
+
output, status = Open3.capture2e({ 'HOME' => home, 'SPEC' => spec, 'SPEC_OPTS' => nil }, 'bundle', 'exec', 'rake', 'spec')
|
|
23
|
+
expect(status.success?).to be(false)
|
|
24
|
+
expect(output).to include('fixture fails first', 'expected: 2', 'got: 1')
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it 'keeps a bounded log and preserves successful exit after large output' do
|
|
29
|
+
Dir.mktmpdir('pwn-build-bound-') do |home|
|
|
30
|
+
script = File.expand_path('../../scripts/build_gate.rb', __dir__)
|
|
31
|
+
output, status = Open3.capture2e({ 'HOME' => home }, RbConfig.ruby, script, RbConfig.ruby, '-e', 'puts "x" * 2_000_000; puts "finished fixture"')
|
|
32
|
+
expect(status.success?).to be(true)
|
|
33
|
+
expect(output).to include('finished fixture')
|
|
34
|
+
log = File.join(home, '.pwn', 'logs', 'build-gate.log')
|
|
35
|
+
expect(File.size(log)).to be <= 1_048_576
|
|
36
|
+
expect(File.read(log)).to include('finished fixture', 'exit=0')
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it 'records a directly signalled gate without inventing a summary' do
|
|
41
|
+
Dir.mktmpdir('pwn-build-signal-') do |home|
|
|
42
|
+
script = File.expand_path('../../scripts/build_gate.rb', __dir__)
|
|
43
|
+
output, status = Open3.capture2e({ 'HOME' => home }, RbConfig.ruby, script, RbConfig.ruby, '-e', 'Process.kill("TERM", Process.pid)')
|
|
44
|
+
expect(status.exitstatus).to eq(143)
|
|
45
|
+
expect(output).to include('signal=TERM')
|
|
46
|
+
expect(File.read(File.join(home, '.pwn', 'logs', 'build-gate.log'))).to include('signal=TERM')
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it 'finishes recording when both terminal streams disconnect' do
|
|
51
|
+
Dir.mktmpdir('pwn-build-disconnect-') do |home|
|
|
52
|
+
script = File.expand_path('../../scripts/build_gate.rb', __dir__)
|
|
53
|
+
reader, writer = IO.pipe
|
|
54
|
+
reader.close
|
|
55
|
+
pid = Process.spawn({ 'HOME' => home }, RbConfig.ruby, script, RbConfig.ruby, '-e', 'warn "Failures: disconnected fixture"; exit 23', out: writer, err: writer)
|
|
56
|
+
writer.close
|
|
57
|
+
expect(Process.wait2(pid).last.exitstatus).to eq(23)
|
|
58
|
+
expect(File.read(File.join(home, '.pwn', 'logs', 'build-gate.log'))).to include('Failures: disconnected fixture', 'exit=23')
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
it 'preserves merged failure output and the gate exit status outside the terminal stream' do
|
|
63
|
+
Dir.mktmpdir('pwn-build-gate-') do |home|
|
|
64
|
+
script = File.expand_path('../../scripts/build_gate.rb', __dir__)
|
|
65
|
+
output, status = Open3.capture2e({ 'HOME' => home }, RbConfig.ruby, script, RbConfig.ruby, '-e', '$stdout.sync = true; puts "F.F"; warn "Failures: fixture assertion"; exit 23')
|
|
66
|
+
expect(status.exitstatus).to eq(23)
|
|
67
|
+
expect(output).to include('Failures: fixture assertion', 'exit=23', 'Build gate log:')
|
|
68
|
+
log = File.join(home, '.pwn', 'logs', 'build-gate.log')
|
|
69
|
+
expect(File.read(log)).to include('F.F', 'Failures: fixture assertion', 'exit=23')
|
|
70
|
+
expect(File.stat(log).mode & 0o777).to eq(0o600)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'open3'
|
|
5
|
+
require 'tmpdir'
|
|
6
|
+
require 'fileutils'
|
|
7
|
+
require 'json'
|
|
8
|
+
|
|
9
|
+
RSpec.describe 'controller spill boundary through actual Dispatch' do
|
|
10
|
+
%w[checkpoint transcript].each do |mode|
|
|
11
|
+
it "does not create source files when a worker redirects the parent #{mode} spill path" do
|
|
12
|
+
Dir.mktmpdir('spill-boundary-') do |root|
|
|
13
|
+
source = File.join(root, 'source')
|
|
14
|
+
home = File.join(root, 'home')
|
|
15
|
+
work = File.join(root, 'work')
|
|
16
|
+
FileUtils.mkdir_p([source, home, work])
|
|
17
|
+
fixture = File.expand_path('../support/harness/controller_spill.rb', __dir__)
|
|
18
|
+
stdout, stderr, status = Open3.capture3({ 'HOME' => home }, RbConfig.ruby, '-I', File.expand_path('../../lib', __dir__), fixture, source, work, mode, chdir: work)
|
|
19
|
+
expect(status.success?).to be(true), stderr
|
|
20
|
+
row = JSON.parse(stdout)
|
|
21
|
+
expect(row['changed']).to be(false), row.inspect
|
|
22
|
+
expect(row['dispatch_success']).to be(false)
|
|
23
|
+
expect(row['dispatch_error']).to include('Permission denied')
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
%w[ordinary aliased].each do |mode|
|
|
29
|
+
it "denies the #{mode} controller-state inventory while preserving workspace output" do
|
|
30
|
+
Dir.mktmpdir('controller-inventory-') do |root|
|
|
31
|
+
source = File.join(root, 'source')
|
|
32
|
+
home = File.join(root, 'home')
|
|
33
|
+
work = File.join(root, 'work')
|
|
34
|
+
FileUtils.mkdir_p([source, home, work])
|
|
35
|
+
fixture = File.expand_path('../support/harness/controller_state.rb', __dir__)
|
|
36
|
+
stdout, stderr, status = Open3.capture3({ 'HOME' => home }, RbConfig.ruby, '-I', File.expand_path('../../lib', __dir__), fixture, source, work, mode, chdir: work)
|
|
37
|
+
expect(status.success?).to be(true), stderr
|
|
38
|
+
row = JSON.parse(stdout)
|
|
39
|
+
expect(row.dig('result', 'success')).to be(true), row.inspect
|
|
40
|
+
attempts = row.dig('result', 'result')
|
|
41
|
+
expect(attempts.length).to eq(29)
|
|
42
|
+
expect(attempts.values.uniq).to eq(['denied']), attempts.inspect
|
|
43
|
+
expect(row['source_unchanged']).to be(true)
|
|
44
|
+
expect(row['output']).to eq('output remains writable')
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require_relative '../support/harness/package_acceptance'
|
|
5
|
+
require_relative '../support/harness/cli_faults'
|
|
6
|
+
|
|
7
|
+
RSpec.describe 'harness process lifecycle' do
|
|
8
|
+
it 'preserves actual exit and stderr when startup fails before readiness' do
|
|
9
|
+
Dir.mktmpdir('pwn-harness-process-') do |root|
|
|
10
|
+
ready = File.join(root, 'ready')
|
|
11
|
+
command = [RbConfig.ruby, '-e', "warn 'unsupported architecture fixture'; exit 78"]
|
|
12
|
+
result = HarnessEvaluation.capture({ 'HOME' => root }, command, root, 1, ready_path: ready, startup_seconds: 2)
|
|
13
|
+
expect(result).to include(exit_status: 78, stderr: "unsupported architecture fixture\n", resource_cap: false, ready_observed: false, timeout_phase: nil)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it 'distinguishes a bounded boot timeout from an execution timeout' do
|
|
18
|
+
Dir.mktmpdir('pwn-harness-process-') do |root|
|
|
19
|
+
result = HarnessEvaluation.capture({ 'HOME' => root }, [RbConfig.ruby, '-e', 'sleep 60'], root, 1,
|
|
20
|
+
ready_path: File.join(root, 'ready'), startup_seconds: 0.05)
|
|
21
|
+
expect(result).to include(resource_cap: true, ready_observed: false, timeout_phase: 'startup', exit_status: nil, signal: Signal.list.fetch('KILL'))
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it 'reports the real child failure when its fingerprint is missing, without inventing one' do
|
|
26
|
+
Dir.mktmpdir('pwn-harness-process-') do |root|
|
|
27
|
+
process = HarnessEvaluation.capture({ 'HOME' => root }, [RbConfig.ruby, '-e', "warn 'namespace unavailable'; exit 78"], root, 2)
|
|
28
|
+
File.write(File.join(root, 'initial-process.json'), JSON.generate(process))
|
|
29
|
+
row = { id: 'checkpoint-restart', artifact_root: root }
|
|
30
|
+
expect { HarnessPackageAcceptance.validate_fingerprints!([row], '/isolated/gems/pwn') }
|
|
31
|
+
.to raise_error(RuntimeError) { |error| expect(error.message).to include('checkpoint-restart', 'fingerprint', '"exit_status":78', 'namespace unavailable') }
|
|
32
|
+
expect(File.exist?(File.join(root, 'fingerprint.json'))).to be(false)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it 'records observed loaded components before an abrupt fixture exit bypasses at_exit' do
|
|
37
|
+
Dir.mktmpdir('pwn-harness-process-') do |root|
|
|
38
|
+
fixture = File.join(root, 'provider.json')
|
|
39
|
+
fingerprint = File.join(root, 'fingerprint.json')
|
|
40
|
+
File.write(fixture, JSON.generate(responses: []))
|
|
41
|
+
env = { 'HOME' => root, 'GEM_HOME' => Gem.dir, 'GEM_PATH' => Gem.path.join(File::PATH_SEPARATOR),
|
|
42
|
+
'PWN_HARNESS_FIXTURE' => fixture, 'PWN_HARNESS_EVENTS' => File.join(root, 'events.jsonl'), 'PWN_HARNESS_FINGERPRINT' => fingerprint }
|
|
43
|
+
command = [RbConfig.ruby, '-I', File.join(HarnessEvaluation::ROOT, 'lib'), '-r',
|
|
44
|
+
File.join(HarnessEvaluation::ROOT, 'spec/support/harness/fake_provider.rb'), '-e', 'exit!(73)']
|
|
45
|
+
result = HarnessEvaluation.agent_capture(env, command, root, 5)
|
|
46
|
+
expect(result).to include(exit_status: 73, ready_observed: true)
|
|
47
|
+
expect(File.file?(fingerprint)).to be(true), result.inspect
|
|
48
|
+
expect(JSON.parse(File.read(fingerprint)).fetch('components')).not_to be_empty
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it 'loads checkpoint controls across the real namespace exec boundary' do
|
|
53
|
+
Dir.mktmpdir('pwn-harness-process-') do |root|
|
|
54
|
+
fixture = File.join(root, 'provider.json')
|
|
55
|
+
runtime = File.join(root, 'runtime')
|
|
56
|
+
File.write(fixture, JSON.generate(responses: [{ fault: 'crash' }]))
|
|
57
|
+
env = { 'HOME' => root, 'PATH' => HarnessEvaluation.isolated_path, 'GEM_HOME' => Gem.dir, 'GEM_PATH' => Gem.path.join(File::PATH_SEPARATOR),
|
|
58
|
+
'PWN_HARNESS_FIXTURE' => fixture, 'PWN_HARNESS_EVENTS' => File.join(root, 'events.jsonl'),
|
|
59
|
+
'PWN_HARNESS_FINGERPRINT' => File.join(root, 'fingerprint.json'), 'PWN_HARNESS_RUNTIME' => runtime }
|
|
60
|
+
command = [RbConfig.ruby, '-I', File.join(HarnessEvaluation::ROOT, 'lib'), '-r',
|
|
61
|
+
File.join(HarnessEvaluation::ROOT, 'spec/support/harness/fault_preload.rb'), '-e',
|
|
62
|
+
'PWN::AI::Agent::MissionNamespace.force_exec = true; load ARGV.shift', File.join(HarnessEvaluation::ROOT, 'bin/pwn-ai'), '--ai', 'Offline checkpoint fixture']
|
|
63
|
+
result = HarnessEvaluation.agent_capture(env, command, root, 10)
|
|
64
|
+
expect(result).to include(exit_status: 73)
|
|
65
|
+
expect(File.file?(File.join(runtime, 'identity.json'))).to be(true), result.inspect
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it 'does not resume a checkpoint that never started after a real child failure' do
|
|
70
|
+
Dir.mktmpdir('pwn-harness-process-') do |root|
|
|
71
|
+
failure = { exit_status: 78, resource_cap: false, ready_observed: false, stdout: '', stderr: 'namespace unavailable' }
|
|
72
|
+
allow(HarnessCLIFaults).to receive(:invoke).and_return(failure)
|
|
73
|
+
report = HarnessCLIFaults.run(output: root)
|
|
74
|
+
expect(HarnessCLIFaults).to have_received(:invoke).exactly(5).times
|
|
75
|
+
expect(report.fetch(:results)).to all(include(outcome: 'FAIL', exit_status: 78))
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it 'redacts child diagnostics before reporting a missing fingerprint' do
|
|
80
|
+
Dir.mktmpdir('pwn-harness-process-') do |root|
|
|
81
|
+
row = { id: 'failed-boot', artifact_root: root, exit_status: 78, stderr: 'password=offline-secret' }
|
|
82
|
+
expect { HarnessPackageAcceptance.validate_fingerprints!([row], '/isolated/gems/pwn') }.to raise_error(RuntimeError) do |error|
|
|
83
|
+
expect(error.message).to include('REDACTED', '78')
|
|
84
|
+
expect(error.message).not_to include('offline-secret')
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
it 'requires fresh readiness on restart rather than accepting the previous process marker' do
|
|
90
|
+
Dir.mktmpdir('pwn-harness-process-') do |root|
|
|
91
|
+
File.write(File.join(root, 'harness-ready'), 'previous invocation')
|
|
92
|
+
command = [RbConfig.ruby, '-e', "sleep 0.2; puts 'booted'; STDOUT.flush; File.write(ENV.fetch('PWN_HARNESS_READY'), 'ready'); sleep 60"]
|
|
93
|
+
result = HarnessEvaluation.agent_capture({ 'HOME' => root }, command, root, 0.05)
|
|
94
|
+
expect(result).to include(resource_cap: true, ready_observed: true, stdout: "booted\n", timeout_phase: 'execution')
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'open3'
|
|
5
|
+
require 'tmpdir'
|
|
6
|
+
require 'fileutils'
|
|
7
|
+
require 'json'
|
|
8
|
+
|
|
9
|
+
RSpec.describe 'mission cancellation through real subprocesses' do
|
|
10
|
+
def mission_fixture(name, *args)
|
|
11
|
+
Dir.mktmpdir('mission-cancel-') do |root|
|
|
12
|
+
home = File.join(root, 'home')
|
|
13
|
+
work = File.join(root, 'work')
|
|
14
|
+
FileUtils.mkdir_p([home, work])
|
|
15
|
+
fixture = File.expand_path("../support/harness/#{name}.rb", __dir__)
|
|
16
|
+
stdout, stderr, status = Open3.capture3({ 'HOME' => home }, RbConfig.ruby, '-I', File.expand_path('../../lib', __dir__), fixture, work, *args, chdir: work)
|
|
17
|
+
expect(status.success?).to be(true), stderr
|
|
18
|
+
JSON.parse(stdout)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it 'cancels the legacy interactive entrypoint through a native terminal and accepts its next mission' do
|
|
23
|
+
require 'pty'
|
|
24
|
+
require 'timeout'
|
|
25
|
+
Dir.mktmpdir('mission-legacy-') do |root|
|
|
26
|
+
FileUtils.mkdir_p(File.join(root, 'home'))
|
|
27
|
+
fixture = File.expand_path('../support/harness/mission_legacy.rb', __dir__)
|
|
28
|
+
PTY.spawn({ 'HOME' => File.join(root, 'home') }, RbConfig.ruby, '-I', File.expand_path('../../lib', __dir__), fixture, root, chdir: root) do |reader, writer, pid|
|
|
29
|
+
Timeout.timeout(15) { sleep 0.01 until File.exist?(File.join(root, 'ready')) }
|
|
30
|
+
writer.write("\x03")
|
|
31
|
+
output = +''
|
|
32
|
+
Timeout.timeout(15) do
|
|
33
|
+
loop { output << reader.readpartial(4096) }
|
|
34
|
+
rescue EOFError, Errno::EIO
|
|
35
|
+
nil
|
|
36
|
+
end
|
|
37
|
+
_child, status = Process.wait2(pid)
|
|
38
|
+
expect(status.success?).to be(true), output
|
|
39
|
+
row = output.lines.find { |line| line.start_with?('{') }
|
|
40
|
+
expect(JSON.parse(row)).to include('ready' => true, 'late' => false, 'second' => true, 'tty' => true)
|
|
41
|
+
ensure
|
|
42
|
+
begin
|
|
43
|
+
Process.kill('KILL', pid)
|
|
44
|
+
Process.wait(pid)
|
|
45
|
+
rescue Errno::ESRCH, Errno::ECHILD
|
|
46
|
+
nil
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it 'reaps provider execution before accepting the next console mission' do
|
|
53
|
+
expect(mission_fixture('mission_console')).to include('ready' => true, 'late' => false, 'second' => true, 'fenced' => true)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it 'hard cancels local command descendants before the next mission' do
|
|
57
|
+
expect(mission_fixture('mission_console', 'command')).to include('ready' => true, 'late' => false, 'second' => true, 'fenced' => true)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it 'keeps detached tool jobs alive until their enclosing mission exits' do
|
|
61
|
+
expect(mission_fixture('mission_job_lifetime')).to include('survived_tool' => true)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it 'hard cancels independent swarm provider and tool descendants' do
|
|
65
|
+
expect(mission_fixture('mission_swarm')).to include('ready' => true, 'late' => false)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it 'fails closed and reaps startup when namespace creation is denied' do
|
|
69
|
+
expect(mission_fixture('mission_unsupported')).to include('blocked' => true, 'actionable' => true, 'reaped' => true)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it 'owns double-forked jobs on normal exit without killing unrelated processes' do
|
|
73
|
+
expect(mission_fixture('mission_normal_exit')).to include('reaped' => true, 'late' => false, 'unrelated' => true)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it 'stops and reaps a setsid descendant before cancellation returns' do
|
|
77
|
+
expect(mission_fixture('mission_dispatch')).to include('cancelled' => true, 'descendant_started' => true, 'reaped' => true, 'post_cancel_write' => false)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'pty'
|
|
5
|
+
require 'io/console'
|
|
6
|
+
require 'timeout'
|
|
7
|
+
|
|
8
|
+
RSpec.describe 'native curses mission cancellation' do
|
|
9
|
+
%w[ordinary command].each do |mode|
|
|
10
|
+
it "handles terminal CTRL+C during #{mode} execution and accepts the next mission without late writes" do
|
|
11
|
+
Dir.mktmpdir('mission-curses-') do |root|
|
|
12
|
+
home = File.join(root, 'home')
|
|
13
|
+
work = File.join(root, 'work')
|
|
14
|
+
FileUtils.mkdir_p([home, work])
|
|
15
|
+
fixture = File.expand_path('../support/harness/mission_curses_pty.rb', __dir__)
|
|
16
|
+
PTY.spawn({ 'HOME' => home, 'TERM' => 'xterm-256color' }, RbConfig.ruby, '-I', File.expand_path('../../lib', __dir__), fixture, work, mode, chdir: work) do |reader, writer, pid|
|
|
17
|
+
writer.winsize = [32, 120]
|
|
18
|
+
output = +''
|
|
19
|
+
pump = Thread.new do
|
|
20
|
+
loop { output << reader.readpartial(4096) }
|
|
21
|
+
rescue EOFError, Errno::EIO
|
|
22
|
+
nil
|
|
23
|
+
end
|
|
24
|
+
%w[idle ready cancelled second].zip([mode == 'command' ? "/mcp fixture\r" : "first\r", "\x03", "second\r", "back\r"]).each do |marker, keys|
|
|
25
|
+
Timeout.timeout(20) { sleep 0.01 until File.exist?(File.join(work, marker)) }
|
|
26
|
+
writer.write(keys)
|
|
27
|
+
end
|
|
28
|
+
Timeout.timeout(20) { pump.join }
|
|
29
|
+
_child, status = Process.wait2(pid)
|
|
30
|
+
expect(status.success?).to be(true), output
|
|
31
|
+
expect(JSON.parse(output[/\{"ready":.*\}/])).to include('ready' => true, 'late' => false, 'second' => true, 'tty' => true)
|
|
32
|
+
ensure
|
|
33
|
+
begin
|
|
34
|
+
Process.kill('KILL', pid)
|
|
35
|
+
Process.wait(pid)
|
|
36
|
+
rescue Errno::ESRCH, Errno::ECHILD
|
|
37
|
+
nil
|
|
38
|
+
end
|
|
39
|
+
pump&.join(1)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -58,13 +58,7 @@ RSpec.describe 'PWN persistence stores — write → reload → delete round-tri
|
|
|
58
58
|
expect(PWN::Cron.list.keys).not_to include(job[:id])
|
|
59
59
|
|
|
60
60
|
seeded = PWN::Cron.install_defaults
|
|
61
|
-
expect(seeded.map { |j| j[:name] }).to
|
|
62
|
-
'curriculum_practice_nightly',
|
|
63
|
-
'curriculum_train_weekly',
|
|
64
|
-
'curriculum_offline_judge',
|
|
65
|
-
'learning_consolidate_nightly',
|
|
66
|
-
'pwn_stores_lean_nightly'
|
|
67
|
-
)
|
|
61
|
+
expect(seeded.map { |j| j[:name] }).to eq(['session_cleanup'])
|
|
68
62
|
expect(PWN::Cron.install_defaults).to eq([]) # idempotent
|
|
69
63
|
end
|
|
70
64
|
end
|