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,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe 'README security overview', :aggregate_failures do
|
|
6
|
+
let(:root) { File.expand_path('../..', __dir__) }
|
|
7
|
+
let(:readme) { File.read(File.join(root, 'README.md')) }
|
|
8
|
+
let(:security) do
|
|
9
|
+
readme[/^##### \*\*Vault and Sandboxing\*\* #####\n(.*?)(?=^#### )/m, 1].to_s.gsub(/\s+/, ' ')
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it 'links a security subsection under Why PWN without replacing How PWN Works' do
|
|
13
|
+
expect(readme).to include('[Vault and Sandboxing](#vault-and-sandboxing)')
|
|
14
|
+
expect(readme).to match(/^#### \*\*Why PWN\*\* ####.*^##### \*\*Vault and Sandboxing\*\* #####.*^#### \*\*How PWN Works\*\* ####/m)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it 'explains credential handling at the tool boundary and engagement loot scope' do
|
|
18
|
+
expect(security).to include('security research', 'bug bounty hunting', 'penetration testing',
|
|
19
|
+
'at rest', '{{vault:label}}', 'Dispatch', 'tool boundary',
|
|
20
|
+
'prompts', 'logs', 'engagement', 'provenance', 'across engagements')
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'documents the mandatory local boundary independently of optional sandbox profiles' do
|
|
24
|
+
expect(security).to include('untrusted PoCs', 'prompt injection', '~/.pwn/workspaces/runtime',
|
|
25
|
+
'original operator request', 'parent', 'source', 'controller state',
|
|
26
|
+
'read-only even if named', 'fail closed', 'Landlock', 'seccomp',
|
|
27
|
+
'namespace', '`strict`', '`permissive`', '`off`')
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it 'keeps encryption, provider privacy and operator responsibilities distinct' do
|
|
31
|
+
expect(security).to include('active process', 'LLM-provider privacy', 'whole-filesystem',
|
|
32
|
+
'evidence/artifact encryption', 'redaction can miss',
|
|
33
|
+
'not authorization', 'network isolation', 'perfect container',
|
|
34
|
+
'guarantee against bugs', 'target authorization', 'least-privilege',
|
|
35
|
+
'retention')
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 'links implementation and boundary documentation with existing local targets' do
|
|
39
|
+
expect(security).to include('(lib/pwn/plugins/vault.rb)', '(lib/pwn/ai/agent/dispatch.rb)',
|
|
40
|
+
'(documentation/Runtime-Source-Protection.md)',
|
|
41
|
+
'(documentation/Mission-Cancellation.md)',
|
|
42
|
+
'(documentation/Configuration.md)', '(documentation/YubiKey-Vaults.md)')
|
|
43
|
+
refs = readme.scan(/\]\((?!https?:|#|mailto:)([^)\s]+)\)/).flatten.uniq
|
|
44
|
+
broken = refs.reject { |ref| File.exist?(File.expand_path(ref.split('#', 2).first, root)) }
|
|
45
|
+
expect(broken).to be_empty, "broken README relative links: #{broken.join(', ')}"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe 'pwn_web_cache_deception YubiKey options' do
|
|
6
|
+
let(:script) { File.expand_path('../../bin/pwn_web_cache_deception', __dir__) }
|
|
7
|
+
let(:browser) { double('offline browser', goto: nil, html: '<html>offline</html>') }
|
|
8
|
+
let(:browser_obj) { { browser: browser } }
|
|
9
|
+
let(:provider) { double('offline WWW provider', open: browser_obj, login: browser_obj, logout: browser_obj, close: nil) }
|
|
10
|
+
let(:driver_opts) { {} }
|
|
11
|
+
let(:account) { 'Example:tester@example.invalid' }
|
|
12
|
+
let(:serial) { '00123456' }
|
|
13
|
+
let(:args) do
|
|
14
|
+
['--target-url=https://example.invalid/test', "--output-results=#{File.join(@tmpdir, 'results.txt')}",
|
|
15
|
+
'--pwn-www-mod=GitHub', '--username=tester@example.invalid', '--password=fake-password',
|
|
16
|
+
"--yubikey-account=#{account}", '--yubikey-confirm', "--yubikey-serial=#{serial}", '--no-mfa']
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
around do |example|
|
|
20
|
+
Dir.mktmpdir('pwn-wcd-yubikey-') do |dir|
|
|
21
|
+
@tmpdir = dir
|
|
22
|
+
example.run
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
before do
|
|
27
|
+
stub_const('ARGV', args.dup)
|
|
28
|
+
allow(PWN::Env).to receive(:[]).and_call_original
|
|
29
|
+
allow(PWN::Env).to receive(:[]).with(:driver_opts).and_return(driver_opts)
|
|
30
|
+
allow(PWN::Config).to receive(:refresh_env)
|
|
31
|
+
stub_const('PWN::WWW::GitHub', provider)
|
|
32
|
+
stub_const('PWN::Plugins::YubiKey', double('offline device boundary'))
|
|
33
|
+
stub_const('PWN::Plugins::TransparentBrowser', double('offline browser boundary', close: nil))
|
|
34
|
+
allow(PWN::Plugins::AuthenticationHelper).to receive(:mask_password).and_raise('unexpected password prompt')
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it 'forwards exact account and serial options to GitHub login even with --no-mfa' do
|
|
38
|
+
expected = { browser_obj: browser_obj, username: 'tester@example.invalid', password: 'fake-password', mfa: false, yubikey: { account: account, confirm: true, serial: serial } }
|
|
39
|
+
expect(provider).to receive(:login).with(expected).and_return(browser_obj)
|
|
40
|
+
expect(PWN::Plugins::YubiKey).not_to receive(:oath_code)
|
|
41
|
+
load script, true
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
%w[Synack Twitter BugCrowd CoinbasePro AppCobaltIO].each do |name|
|
|
45
|
+
it "forwards YubiKey options only at #{name} login, not browser open" do
|
|
46
|
+
ARGV.map! { |arg| arg == '--pwn-www-mod=GitHub' ? "--pwn-www-mod=#{name}" : arg }
|
|
47
|
+
stub_const("PWN::WWW::#{name}", provider)
|
|
48
|
+
expect(provider).to receive(:open).with(browser_type: :chrome).and_return(browser_obj)
|
|
49
|
+
expect(provider).to receive(:login).with(hash_including(yubikey: { account: account, confirm: true, serial: serial }, mfa: false)).and_return(browser_obj)
|
|
50
|
+
load script, true
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
%w[Google Paypal].each do |name|
|
|
55
|
+
it "rejects YubiKey for #{name} before password prompts or opening a browser" do
|
|
56
|
+
ARGV.map! { |arg| arg == '--pwn-www-mod=GitHub' ? "--pwn-www-mod=#{name}" : arg }
|
|
57
|
+
ARGV.delete('--password=fake-password')
|
|
58
|
+
stub_const("PWN::WWW::#{name}", provider)
|
|
59
|
+
expect(provider).not_to receive(:open)
|
|
60
|
+
expect(provider).not_to receive(:login)
|
|
61
|
+
expect { load script, true }.to raise_error(ArgumentError, /YubiKey OATH.*supported/)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it 'requires a selected compatible provider before prompting or opening a browser' do
|
|
66
|
+
ARGV.delete('--pwn-www-mod=GitHub')
|
|
67
|
+
ARGV.delete('--password=fake-password')
|
|
68
|
+
expect { load script, true }.to raise_error(ArgumentError, /YubiKey OATH.*supported/)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
it 'rejects a serial without an exact account instead of silently disabling YubiKey' do
|
|
72
|
+
ARGV.delete("--yubikey-account=#{account}")
|
|
73
|
+
ARGV.delete('--password=fake-password')
|
|
74
|
+
expect(provider).not_to receive(:open)
|
|
75
|
+
expect { load script, true }.to raise_error(ArgumentError, /serial.*account/)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it 'requires --yubikey-confirm before password prompts or browser open' do
|
|
79
|
+
ARGV.delete('--yubikey-confirm')
|
|
80
|
+
ARGV.delete('--password=fake-password')
|
|
81
|
+
expect(provider).not_to receive(:open)
|
|
82
|
+
expect(provider).not_to receive(:login)
|
|
83
|
+
expect { load script, true }.to raise_error(ArgumentError, /confirm: true.*HOTP.*authenticat/i)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it 'rejects confirmation without an account before password prompts or browser open' do
|
|
87
|
+
ARGV.reject! { |arg| arg.start_with?('--yubikey-account=', '--yubikey-serial=') || arg == '--password=fake-password' }
|
|
88
|
+
expect(provider).not_to receive(:open)
|
|
89
|
+
expect(provider).not_to receive(:login)
|
|
90
|
+
expect { load script, true }.to raise_error(ArgumentError, /--yubikey-confirm.*--yubikey-account/)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it 'does not accept a string value for the boolean --yubikey-confirm flag' do
|
|
94
|
+
ARGV.map! { |arg| arg == '--yubikey-confirm' ? '--yubikey-confirm=true' : arg }
|
|
95
|
+
expect(provider).not_to receive(:open)
|
|
96
|
+
expect { load script, true }.to raise_error(OptionParser::NeedlessArgument)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
['', '123x', ' 123', '-1'].each do |invalid|
|
|
100
|
+
it "rejects malformed serial #{invalid.inspect} before prompting or opening a browser" do
|
|
101
|
+
ARGV.map! { |arg| arg == "--yubikey-serial=#{serial}" ? "--yubikey-serial=#{invalid}" : arg }
|
|
102
|
+
ARGV.delete('--password=fake-password')
|
|
103
|
+
expect(provider).not_to receive(:open)
|
|
104
|
+
expect { load script, true }.to raise_error(ArgumentError, /serial.*decimal/)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it 'validates an empty account before password prompts or browser open' do
|
|
109
|
+
ARGV.map! { |arg| arg == "--yubikey-account=#{account}" ? '--yubikey-account=' : arg }
|
|
110
|
+
ARGV.delete('--yubikey-confirm')
|
|
111
|
+
ARGV.delete('--password=fake-password')
|
|
112
|
+
expect(provider).not_to receive(:open)
|
|
113
|
+
expect { load script, true }.to raise_error(ArgumentError, /account/)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
it 'preserves an explicit MFA token from driver options rather than coercing it to true' do
|
|
117
|
+
ARGV.reject! { |arg| arg.start_with?('--yubikey-') || arg == '--no-mfa' }
|
|
118
|
+
driver_opts[:mfa] = '654321'
|
|
119
|
+
expect(provider).to receive(:login).with(hash_including(mfa: '654321')).and_return(browser_obj)
|
|
120
|
+
expect { load script, true }.not_to output(/fake-password|654321/).to_stdout
|
|
121
|
+
expect(File.read(File.join(@tmpdir, 'results.txt'))).not_to match(/fake-password|654321/)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
it 'keeps the generic driver options unchanged when no YubiKey is requested' do
|
|
125
|
+
ARGV.reject! { |arg| arg.start_with?('--yubikey-') }
|
|
126
|
+
ARGV.map! { |arg| arg == '--pwn-www-mod=GitHub' ? '--pwn-www-mod=Google' : arg }
|
|
127
|
+
stub_const('PWN::WWW::Google', provider)
|
|
128
|
+
expect(provider).to receive(:login) do |options|
|
|
129
|
+
expect(options).not_to have_key(:yubikey)
|
|
130
|
+
expect(options[:mfa]).to be(false)
|
|
131
|
+
browser_obj
|
|
132
|
+
end
|
|
133
|
+
load script, true
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
it 'preserves the exact OATH name when the optional serial is omitted' do
|
|
137
|
+
exact = " #{account} "
|
|
138
|
+
ARGV.reject! { |arg| arg.start_with?('--yubikey-serial=') }
|
|
139
|
+
ARGV.map! { |arg| arg == "--yubikey-account=#{account}" ? "--yubikey-account=#{exact}" : arg }
|
|
140
|
+
expect(provider).to receive(:login).with(hash_including(yubikey: { account: exact, confirm: true })).and_return(browser_obj)
|
|
141
|
+
expect { load script, true }.not_to output(/fake-password|012345/).to_stdout
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
it 'documents account, consent risks, and serial flags with the supported-provider boundary' do
|
|
145
|
+
ARGV.replace(['--help'])
|
|
146
|
+
expect do
|
|
147
|
+
expect { load script, true }.to raise_error(SystemExit) { |error| expect(error.status).to eq(0) }
|
|
148
|
+
end.to output(/--yubikey-account.*GitHub.*Synack.*Twitter.*BugCrowd.*CoinbasePro.*AppCobaltIO.*Paypal.*--yubikey-confirm.*HOTP.*delete.*corrupt.*--yubikey-serial/m).to_stdout
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
it 'rejects an explicit token conflicting with YubiKey before password prompts' do
|
|
152
|
+
ARGV.delete('--no-mfa')
|
|
153
|
+
ARGV.delete('--yubikey-confirm')
|
|
154
|
+
ARGV.delete('--password=fake-password')
|
|
155
|
+
driver_opts[:mfa] = '654321'
|
|
156
|
+
expect(provider).not_to receive(:open)
|
|
157
|
+
expect { load script, true }.to raise_error(ArgumentError, /yubikey.*mfa/i)
|
|
158
|
+
end
|
|
159
|
+
end
|
|
@@ -6,18 +6,21 @@ require 'json'
|
|
|
6
6
|
describe PWN::AI::Agent::Dispatch do
|
|
7
7
|
it 'does not replay a timed-out real shell mutation after restoring its request checkpoint' do
|
|
8
8
|
Dir.mktmpdir do |root|
|
|
9
|
-
|
|
9
|
+
work = File.join(root, 'work')
|
|
10
|
+
FileUtils.mkdir_p(work)
|
|
11
|
+
allow(PWN::Plugins::AISandbox).to receive(:workspace).and_return(work)
|
|
12
|
+
options = { request: 'Write counter', session_id: 'timeout', request_id: 'fixture', artifact_root: File.join(root, 'control'), trace: false }
|
|
10
13
|
runtime = PWN::AI::Agent::RequestRuntime.new(options)
|
|
11
14
|
Thread.current[:pwn_request_runtime] = runtime
|
|
12
15
|
PWN::AI::Agent::Registry.discover
|
|
13
|
-
call = { id: 'mutation', function: { name: 'shell', arguments: { command: "printf effect >> #{
|
|
16
|
+
call = { id: 'mutation', function: { name: 'shell', arguments: { command: "printf effect >> #{work}/counter; sleep 3", timeout: 1 } } }
|
|
14
17
|
first = JSON.parse(described_class.call(tool_call: call, scope_policy: { enabled: false }))
|
|
15
18
|
expect(first.dig('result', 'error')).to include('timeout')
|
|
16
19
|
Thread.current[:pwn_request_runtime] = PWN::AI::Agent::RequestRuntime.new(options.merge(resume: true))
|
|
17
20
|
second = JSON.parse(described_class.call(tool_call: call, scope_policy: { enabled: false }))
|
|
18
21
|
expect(second['success']).to be false
|
|
19
22
|
expect(second.dig('result', 'error')).to eq('uncertain_side_effect')
|
|
20
|
-
expect(File.read(File.join(
|
|
23
|
+
expect(File.read(File.join(work, 'counter'))).to eq('effect')
|
|
21
24
|
expect(call.dig(:function, :arguments, :timeout)).to eq(1)
|
|
22
25
|
end
|
|
23
26
|
ensure
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
RSpec.describe PWN::AI::Agent::Dispatch, 'vault access safety' do
|
|
7
|
+
before do
|
|
8
|
+
@calls = []
|
|
9
|
+
@result = { status: 'fixture-complete' }
|
|
10
|
+
handler = lambda do |args|
|
|
11
|
+
@calls << args
|
|
12
|
+
@result
|
|
13
|
+
end
|
|
14
|
+
entry = PWN::AI::Agent::Registry::Entry.new(name: 'vault_fixture', schema: { parameters: { type: 'object' } }, handler: handler)
|
|
15
|
+
allow(PWN::AI::Agent::Registry).to receive(:lookup).with(name: 'vault_fixture').and_return(entry)
|
|
16
|
+
allow(PWN::AI::Agent::Manifest).to receive(:load).and_return({})
|
|
17
|
+
allow(PWN::AI::Agent::Manifest).to receive(:check).and_return(nil)
|
|
18
|
+
allow(PWN::AI::Agent::ToolGuard).to receive_messages(scope_check!: nil, policy_decision: nil, canary_leak?: false, refuse_copied_persist?: false)
|
|
19
|
+
allow(PWN::AI::Agent::Engagement).to receive(:deny_if_out_of_scope).and_return(nil)
|
|
20
|
+
allow(PWN::AI::Agent::Mission).to receive(:active).and_return(nil)
|
|
21
|
+
allow(PWN::AI::Agent::Confirmation).to receive(:gate).and_return(nil)
|
|
22
|
+
allow(PWN::Plugins::Vault).to receive(:expand) { |opts| opts[:text] }
|
|
23
|
+
allow(PWN::Plugins::Vault).to receive(:redact) { |opts| opts[:text] }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it 'expands nested vault references once per leaf before invoking the handler' do
|
|
27
|
+
allow(PWN::Plugins::Vault).to receive(:expand).with(text: '{{vault:fixture}}').and_return('fixture-unlocked')
|
|
28
|
+
arguments = { nested: [{ secret: '{{vault:fixture}}' }], slot: 2 }
|
|
29
|
+
response = JSON.parse(described_class.call(tool_call: { function: { name: 'vault_fixture', arguments: arguments } }))
|
|
30
|
+
expect(response['success']).to be(true)
|
|
31
|
+
expect(@calls).to eq([{ nested: [{ secret: 'fixture-unlocked' }], slot: 2 }])
|
|
32
|
+
expect(PWN::Plugins::Vault).to have_received(:expand).with(text: '{{vault:fixture}}').once
|
|
33
|
+
expect(arguments[:nested].first[:secret]).to eq('{{vault:fixture}}')
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it 'blocks a nested vault access failure without exposing its exception text' do
|
|
37
|
+
allow(PWN::Plugins::Vault).to receive(:expand).with(text: '{{vault:fixture}}').and_raise(IOError, 'fixture-private-detail')
|
|
38
|
+
response = JSON.parse(described_class.call(tool_call: { function: { name: 'vault_fixture', arguments: { nested: ['{{vault:fixture}}'] } } }))
|
|
39
|
+
expect(response).to include('success' => false, 'code' => 'VAULT_EXPANSION_FAILED')
|
|
40
|
+
expect(@calls).to be_empty
|
|
41
|
+
expect(response.to_s).not_to include('fixture-private-detail')
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it 'withholds results when vault output redaction fails after handler execution' do
|
|
45
|
+
allow(PWN::Plugins::Vault).to receive(:redact).and_raise(IOError, 'fixture-private-detail')
|
|
46
|
+
response = JSON.parse(described_class.call(tool_call: { function: { name: 'vault_fixture', arguments: {} } }))
|
|
47
|
+
expect(@calls.length).to eq(1)
|
|
48
|
+
expect(response).to include('success' => false, 'code' => 'VAULT_REDACTION_FAILED', 'handler_executed' => true)
|
|
49
|
+
expect(response.to_s).not_to include('fixture-private-detail', 'fixture-complete')
|
|
50
|
+
expect(response).not_to have_key('result')
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it 'redacts secrets in array results such as loot offers before returning them' do
|
|
54
|
+
@result = [{ secret: 'fixture-private-value' }]
|
|
55
|
+
allow(PWN::Plugins::Vault).to receive(:redact) { |opts| opts[:text].gsub('fixture-private-value', '{{vault:fixture}}') }
|
|
56
|
+
response = JSON.parse(described_class.call(tool_call: { function: { name: 'vault_fixture', arguments: {} } }))
|
|
57
|
+
expect(response).to include('success' => true, 'result' => [{ 'secret' => '{{vault:fixture}}' }])
|
|
58
|
+
expect(response.to_s).not_to include('fixture-private-value')
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it 'blocks a real hardware-envelope expansion when the offline device fixture fails' do
|
|
62
|
+
Dir.mktmpdir('pwn-dispatch-yubikey-') do |root|
|
|
63
|
+
allow(Dir).to receive(:home).and_return(root)
|
|
64
|
+
allow(PWN::AI::Agent::Mission).to receive(:active_id).and_return(nil)
|
|
65
|
+
device = double('offline YubiKey fixture')
|
|
66
|
+
stub_const('PWN::Plugins::YubiKey', device)
|
|
67
|
+
allow(device).to receive(:challenge_response) { |opts| OpenSSL::HMAC.digest('SHA1', 'synthetic-slot-secret', opts.fetch(:challenge)) }
|
|
68
|
+
PWN::Plugins::Vault.store(label: 'fixture', secret: 'fixture-private-value', yubikey: { slot: 2 })
|
|
69
|
+
allow(PWN::Plugins::Vault).to receive(:expand).and_call_original
|
|
70
|
+
allow(device).to receive(:challenge_response).and_raise(IOError, 'fixture-private-detail')
|
|
71
|
+
response = JSON.parse(described_class.call(tool_call: { function: { name: 'vault_fixture', arguments: { password: '{{vault:fixture}}' } } }))
|
|
72
|
+
expect(response).to include('success' => false, 'code' => 'VAULT_EXPANSION_FAILED')
|
|
73
|
+
expect(@calls).to be_empty
|
|
74
|
+
expect(response.to_s).not_to include('fixture-private-detail', 'fixture-private-value')
|
|
75
|
+
expect(File).not_to exist(File.join(root, '.pwn-vault.key'))
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it 'never invokes a real handler after expansion of a vault argument fails' do
|
|
80
|
+
allow(PWN::Plugins::Vault).to receive(:expand).with(text: '{{vault:fixture}}').and_raise(IOError, 'device absent; fixture-private-detail')
|
|
81
|
+
response = JSON.parse(described_class.call(tool_call: { function: { name: 'vault_fixture', arguments: { password: '{{vault:fixture}}' } } }))
|
|
82
|
+
expect(@calls).to be_empty
|
|
83
|
+
expect(response).to include('success' => false, 'code' => 'VAULT_EXPANSION_FAILED')
|
|
84
|
+
expect(response.to_s).not_to include('fixture-private-detail')
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -34,6 +34,7 @@ describe 'indirect prompt-injection guards' do
|
|
|
34
34
|
it 'does not re-infer the acceptance contract on a nested loop' do
|
|
35
35
|
Thread.current[:pwn_loop_active] = true
|
|
36
36
|
Thread.current[:pwn_loop_nested] = true
|
|
37
|
+
PWN::AI::Agent::TurnFinalizer.commit_artifacts!(request: 'Write /tmp/frozen.pdf')
|
|
37
38
|
Thread.current[:pwn_loop_deliverables] = {
|
|
38
39
|
paths: ['/tmp/frozen.pdf'], min_seconds: 0, skills: [], proofs: [], hosts: []
|
|
39
40
|
}
|
|
@@ -44,6 +45,7 @@ describe 'indirect prompt-injection guards' do
|
|
|
44
45
|
ensure
|
|
45
46
|
Thread.current[:pwn_loop_active] = nil
|
|
46
47
|
Thread.current[:pwn_loop_nested] = nil
|
|
48
|
+
Thread.current[:pwn_artifact_contract] = nil
|
|
47
49
|
Thread.current[:pwn_loop_deliverables] = nil
|
|
48
50
|
end
|
|
49
51
|
|
|
@@ -89,7 +89,7 @@ describe PWN::AI::Agent::Learning do
|
|
|
89
89
|
expect(described_class.evidence_complete?(row: row)).to be(true)
|
|
90
90
|
expect(File.binread(@source)).to eq(original_source)
|
|
91
91
|
migration = Digest::SHA256.hexdigest(@original)
|
|
92
|
-
backup = File.
|
|
92
|
+
backup = JSON.parse(File.read(result[:ledger])).fetch('backup')
|
|
93
93
|
expect(File.binread(backup)).to eq(@original.b)
|
|
94
94
|
expect(described_class.rollback_evidence(migration: migration)[:restored]).to be(true)
|
|
95
95
|
expect(File.binread(@learning)).to eq(@original.b)
|
|
@@ -778,14 +778,14 @@ describe 'PWN::AI::Agent::Learning conflicted outcomes' do
|
|
|
778
778
|
end
|
|
779
779
|
end
|
|
780
780
|
|
|
781
|
-
it '
|
|
781
|
+
it 'leaves legacy siblings untouched and reports the global seven-folder policy' do
|
|
782
782
|
Dir.mktmpdir do |dir|
|
|
783
783
|
stub_const('PWN::AI::Agent::Learning::LEARNING_FILE', File.join(dir, 'learning.jsonl'))
|
|
784
784
|
File.write(File.join(dir, 'learning.jsonl'), "{}\n")
|
|
785
785
|
7.times { |i| File.write(File.join(dir, "learning.jsonl.bak-#{i}"), 'x') }
|
|
786
786
|
out = PWN::AI::Agent::Learning.compact!(max_baks: 5)
|
|
787
|
-
expect(Dir[File.join(dir, '*.bak*')].length).to eq(
|
|
788
|
-
expect(out
|
|
787
|
+
expect(Dir[File.join(dir, '*.bak*')].length).to eq(7)
|
|
788
|
+
expect(out).to include(pruned: 0, max_baks: 7, scope: 'global')
|
|
789
789
|
end
|
|
790
790
|
end
|
|
791
791
|
end
|
|
@@ -21,6 +21,11 @@ RSpec.describe PWN::AI::Agent::Loop do
|
|
|
21
21
|
allow(PWN::AI::Agent::Mistakes).to receive(:record)
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
+
it 'never adopts cached model destinations absent from the original request' do
|
|
25
|
+
Thread.current[:pwn_loop_deliverables] = { paths: ['/tmp/invented.pdf'] }
|
|
26
|
+
expect(described_class.send(:declared_deliverables, request: 'Generate a report')).to eq([])
|
|
27
|
+
end
|
|
28
|
+
|
|
24
29
|
it 'does not let source read evidence bypass the destination contract' do
|
|
25
30
|
Dir.mktmpdir do |dir|
|
|
26
31
|
source = File.join(dir, 'source.md')
|