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
data/lib/pwn/plugins.rb
CHANGED
|
@@ -111,6 +111,7 @@ module PWN
|
|
|
111
111
|
autoload :Volatility, 'pwn/plugins/volatility'
|
|
112
112
|
autoload :Vsphere, 'pwn/plugins/vsphere'
|
|
113
113
|
autoload :XXD, 'pwn/plugins/xxd'
|
|
114
|
+
autoload :YubiKey, 'pwn/plugins/yubikey'
|
|
114
115
|
autoload :Zaproxy, 'pwn/plugins/zaproxy'
|
|
115
116
|
|
|
116
117
|
# Display a List of Every PWN::Plugins Module
|
data/lib/pwn/redaction.rb
CHANGED
|
@@ -4,7 +4,7 @@ require 'digest'
|
|
|
4
4
|
require 'json'
|
|
5
5
|
|
|
6
6
|
module PWN
|
|
7
|
-
# Write-boundary redaction;
|
|
7
|
+
# Write-boundary redaction; redact never opens a credential store. Capture optionally stores ordinary secrets. Hardware/MFA fields use constant masks, never digest markers or loot capture.
|
|
8
8
|
module Redaction
|
|
9
9
|
PATTERNS = {
|
|
10
10
|
pem: /-----BEGIN [A-Z ]*PRIVATE KEY-----.*?(?:-----END [A-Z ]*PRIVATE KEY-----|\z)/m,
|
|
@@ -17,6 +17,15 @@ module PWN
|
|
|
17
17
|
cookie: /\bSet-Cookie:\s*(?!\[REDACTED:)[^\r\n]+/i
|
|
18
18
|
}.freeze
|
|
19
19
|
SECRET_FIELD = /\A(?:password|passwd|secret|api[_-]?key|authorization|proxy-authorization|access[_-]?token|refresh[_-]?token|private[_-]?key|client[_-]?secret)\z/i
|
|
20
|
+
CONSTANT_MASK = '[REDACTED]'
|
|
21
|
+
MASKED_NAME = /(?:
|
|
22
|
+
(?:(?:new|current|old|admin|user|fido2?|piv|openpgp)[_-])?(?:pin|puk) |
|
|
23
|
+
access[_-]?code | management[_-]?(?:key|password) | oath[_-]?secret | static[_-]?password |
|
|
24
|
+
scp(?:03|11)?(?:[_-](?:key|keys|enc|mac|dek)(?:[_-]key)?)? |
|
|
25
|
+
(?:mfa|otp|totp|hotp)(?:[_-](?:code|token|secret))? | (?:recovery|verification)[_-]?code
|
|
26
|
+
)/ix
|
|
27
|
+
MASKED_FIELD = /\A#{MASKED_NAME}\z/
|
|
28
|
+
MASKED_TEXT = /(?<![\w-])((?:--)?["']?#{MASKED_NAME}["']?\s*(?:=>|[:=])\s*)(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\[REDACTED(?::[^\]]+)?\]|[^\s,;}\])]+)/
|
|
20
29
|
|
|
21
30
|
public_class_method def self.redact(opts = {})
|
|
22
31
|
value = opts[:value]
|
|
@@ -46,7 +55,9 @@ module PWN
|
|
|
46
55
|
case value
|
|
47
56
|
when Hash
|
|
48
57
|
value.to_h do |key, item|
|
|
49
|
-
clean = if key.to_s.match?(
|
|
58
|
+
clean = if key.to_s.match?(MASKED_FIELD) && !item.nil?
|
|
59
|
+
CONSTANT_MASK
|
|
60
|
+
elsif key.to_s.match?(SECRET_FIELD) && !item.nil?
|
|
50
61
|
token(kind: key.to_s.downcase, value: item.to_s)
|
|
51
62
|
else
|
|
52
63
|
redact_value(value: item)
|
|
@@ -55,15 +66,58 @@ module PWN
|
|
|
55
66
|
end
|
|
56
67
|
when Array then value.map { |item| redact_value(value: item) }
|
|
57
68
|
when String
|
|
58
|
-
PATTERNS.reduce(value
|
|
59
|
-
text.split(/(\[REDACTED
|
|
60
|
-
part.start_with?('[REDACTED
|
|
69
|
+
PATTERNS.reduce(mask_fields(text: value)) do |text, (kind, regex)|
|
|
70
|
+
text.split(/(\[REDACTED(?::[^:\]]+:[0-9a-f]{8})?\])/).map do |part|
|
|
71
|
+
part.start_with?('[REDACTED') ? part : part.gsub(regex) { |match| token(kind: kind, value: match) }
|
|
61
72
|
end.join
|
|
62
73
|
end
|
|
63
74
|
else value
|
|
64
75
|
end
|
|
65
76
|
end
|
|
66
77
|
|
|
78
|
+
private_class_method def self.mask_fields(opts = {})
|
|
79
|
+
text = opts[:text]
|
|
80
|
+
clean = +''
|
|
81
|
+
offset = 0
|
|
82
|
+
while (match = MASKED_TEXT.match(text, offset))
|
|
83
|
+
clean << text[offset...match.begin(0)] << match[1] << CONSTANT_MASK
|
|
84
|
+
offset = masked_value_end(text: text, start: match.end(1), finish: match.end(0))
|
|
85
|
+
end
|
|
86
|
+
clean << text[offset..]
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Iterative scan prevents partial masks of serialized arrays/hashes of keys.
|
|
90
|
+
private_class_method def self.masked_value_end(opts = {})
|
|
91
|
+
text = opts[:text]
|
|
92
|
+
start = opts[:start]
|
|
93
|
+
pairs = { '[' => ']', '{' => '}' }
|
|
94
|
+
return opts[:finish] unless pairs.key?(text[start]) || ['"', "'"].include?(text[start])
|
|
95
|
+
|
|
96
|
+
stack = []
|
|
97
|
+
quote = nil
|
|
98
|
+
escaped = false
|
|
99
|
+
text[start..].each_char.with_index(start) do |char, index|
|
|
100
|
+
if quote
|
|
101
|
+
if escaped
|
|
102
|
+
escaped = false
|
|
103
|
+
elsif char == '\\'
|
|
104
|
+
escaped = true
|
|
105
|
+
elsif char == quote
|
|
106
|
+
quote = nil
|
|
107
|
+
return index + 1 if stack.empty?
|
|
108
|
+
end
|
|
109
|
+
elsif ['"', "'"].include?(char)
|
|
110
|
+
quote = char
|
|
111
|
+
elsif pairs.key?(char)
|
|
112
|
+
stack << pairs[char]
|
|
113
|
+
elsif pairs.value?(char)
|
|
114
|
+
return text.length unless stack.pop == char
|
|
115
|
+
return index + 1 if stack.empty?
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
text.length
|
|
119
|
+
end
|
|
120
|
+
|
|
67
121
|
public_class_method def self.token(opts = {})
|
|
68
122
|
value = opts[:value].to_s
|
|
69
123
|
return value if value.match?(/\A\[REDACTED:[^:]+:[0-9a-f]{8}\]\z/)
|
|
@@ -75,19 +129,21 @@ module PWN
|
|
|
75
129
|
value = opts[:value]
|
|
76
130
|
return { redacted: redact(value: value), refs: [] } unless value.is_a?(String)
|
|
77
131
|
|
|
78
|
-
text = value
|
|
132
|
+
text = mask_fields(text: value)
|
|
79
133
|
refs = []
|
|
80
134
|
redacted = PATTERNS.reduce(text.dup) do |acc, (kind, regex)|
|
|
81
135
|
acc.gsub(regex) do |match|
|
|
136
|
+
next match if match.match?(/\A[\w-]+\s*[:=]\s*\[REDACTED\]\z/)
|
|
137
|
+
|
|
82
138
|
tok = token(kind: kind, value: match)
|
|
83
139
|
refs << { kind: kind, token: tok }
|
|
84
|
-
PWN::Plugins::Vault.store(label: tok, secret: match, engagement: opts[:engagement]) if opts[:vault] != false && defined?(PWN::Plugins::Vault)
|
|
140
|
+
PWN::Plugins::Vault.store(label: tok, secret: match, engagement: opts[:engagement], yubikey: opts[:yubikey]) if opts[:vault] != false && defined?(PWN::Plugins::Vault)
|
|
85
141
|
tok
|
|
86
142
|
rescue StandardError
|
|
87
143
|
tok
|
|
88
144
|
end
|
|
89
145
|
end
|
|
90
|
-
redacted = capture_entropy(text: redacted, refs: refs, vault: opts[:vault], engagement: opts[:engagement]) if opts[:entropy] == true
|
|
146
|
+
redacted = capture_entropy(text: redacted, refs: refs, vault: opts[:vault], engagement: opts[:engagement], yubikey: opts[:yubikey]) if opts[:entropy] == true
|
|
91
147
|
{ redacted: redacted, refs: refs }
|
|
92
148
|
end
|
|
93
149
|
|
|
@@ -100,10 +156,10 @@ module PWN
|
|
|
100
156
|
|
|
101
157
|
tok = token(kind: 'entropy', value: match)
|
|
102
158
|
refs << { kind: 'entropy', token: tok }
|
|
103
|
-
PWN::Plugins::Vault.store(label: tok, secret: match, engagement: opts[:engagement]) if opts[:vault] != false && defined?(PWN::Plugins::Vault)
|
|
159
|
+
PWN::Plugins::Vault.store(label: tok, secret: match, engagement: opts[:engagement], yubikey: opts[:yubikey]) if opts[:vault] != false && defined?(PWN::Plugins::Vault)
|
|
104
160
|
tok
|
|
105
161
|
rescue StandardError
|
|
106
|
-
match
|
|
162
|
+
tok || token(kind: 'entropy', value: match)
|
|
107
163
|
end
|
|
108
164
|
end
|
|
109
165
|
|
|
@@ -122,7 +178,7 @@ module PWN
|
|
|
122
178
|
|
|
123
179
|
public_class_method def self.help
|
|
124
180
|
puts "USAGE:
|
|
125
|
-
# Redact
|
|
181
|
+
# Redact before persistence; hardware/MFA fields use constant masks. Reject cycles/depth above 100 with JSON::NestingError.
|
|
126
182
|
#{self}.redact(
|
|
127
183
|
value: 'required - string or nested JSON-compatible data'
|
|
128
184
|
)
|
|
@@ -131,10 +187,11 @@ module PWN
|
|
|
131
187
|
kind: 'required - secret type label',
|
|
132
188
|
value: 'required - sensitive value to hash'
|
|
133
189
|
)
|
|
134
|
-
# Capture secrets into Vault labels
|
|
190
|
+
# Capture ordinary secrets into Vault labels; mask hardware/MFA fields without storing them.
|
|
135
191
|
#{self}.capture(
|
|
136
192
|
value: 'required - string that may contain credentials',
|
|
137
193
|
vault: 'optional - false skips Vault.store',
|
|
194
|
+
yubikey: 'optional - selector Hash for hardware-encrypted captured loot; no software fallback on failure',
|
|
138
195
|
engagement: 'optional - engagement name stored with the vault label',
|
|
139
196
|
entropy: 'optional - true also captures high-entropy tokens'
|
|
140
197
|
)
|
data/lib/pwn/session_trace.rb
CHANGED
|
@@ -91,6 +91,10 @@ module PWN
|
|
|
91
91
|
timeout = Float(opts.fetch(:timeout, 30))
|
|
92
92
|
raise ArgumentError, 'Invalid rerun deadline' unless timeout.positive? && timeout <= 300
|
|
93
93
|
|
|
94
|
+
# A user-namespace caller that is not uid 0 may still hold capabilities.
|
|
95
|
+
# Bubblewrap rejects those. Uid 0 can already create the nested map;
|
|
96
|
+
# dropping every capability there makes the uid-map setup fail.
|
|
97
|
+
argv.unshift('setpriv', '--ambient-caps=-all', '--inh-caps=-all', '--bounding-set=-all', '--') if PWN::AI::Agent::MissionNamespace.prepared? && !Process.uid.zero?
|
|
94
98
|
Open3.popen3(*argv, unsetenv_others: true, pgroup: true) do |stdin, stdout, stderr, wait|
|
|
95
99
|
readers = [Thread.new { stdout.read }, Thread.new { stderr.read }]
|
|
96
100
|
begin
|
data/lib/pwn/sessions.rb
CHANGED
|
@@ -88,11 +88,13 @@ module PWN
|
|
|
88
88
|
when :trace then ['traces', File.join(id, 'trace.jsonl')]
|
|
89
89
|
else raise ArgumentError, 'Invalid session support kind'
|
|
90
90
|
end
|
|
91
|
-
|
|
91
|
+
directory = opts[:read_only] ? SESSIONS_DIR : sessions_dir
|
|
92
|
+
legacy = File.join(directory, name)
|
|
92
93
|
return legacy if File.exist?(legacy) || File.symlink?(legacy)
|
|
93
94
|
|
|
94
|
-
path = File.join(
|
|
95
|
-
return legacy unless File.exist?(path) || File.symlink?(path) || File.file?(File.join(
|
|
95
|
+
path = File.join(directory, 'support', folder, name)
|
|
96
|
+
return legacy unless File.exist?(path) || File.symlink?(path) || File.file?(File.join(directory, '.layout-v2'))
|
|
97
|
+
return path if opts[:read_only]
|
|
96
98
|
|
|
97
99
|
FileUtils.mkdir_p(File.dirname(path), mode: 0o700)
|
|
98
100
|
raise IOError, 'Unsafe session support directory' unless File.realpath(File.dirname(path)) == File.expand_path(File.dirname(path))
|
|
@@ -183,11 +185,31 @@ module PWN
|
|
|
183
185
|
files.concat(Dir.glob(File.join(root, 'artifacts', '{manifest.jsonl,*/index.jsonl,*/engagement-memory.json}')))
|
|
184
186
|
databases = Dir.glob(File.join(root, 'embeddings', '*.db'))
|
|
185
187
|
errors.concat(%w[artifacts embeddings].map { |name| File.join(root, name) }.select { |dir| File.symlink?(dir) || (File.exist?(dir) && !File.directory?(dir)) })
|
|
188
|
+
admission = Thread.current[:pwn_maintenance_admission]
|
|
186
189
|
directories.each do |dir|
|
|
187
190
|
next unless File.directory?(dir) && !File.symlink?(dir)
|
|
188
191
|
|
|
189
|
-
|
|
190
|
-
|
|
192
|
+
pending = [[dir, Dir.each_child(dir)]]
|
|
193
|
+
until pending.empty?
|
|
194
|
+
begin
|
|
195
|
+
path = File.join(pending.last[0], pending.last[1].next)
|
|
196
|
+
rescue StopIteration
|
|
197
|
+
pending.pop
|
|
198
|
+
next
|
|
199
|
+
end
|
|
200
|
+
if admission && !admission.call
|
|
201
|
+
errors << 'incomplete_dependency_index'
|
|
202
|
+
break
|
|
203
|
+
end
|
|
204
|
+
if File.symlink?(path)
|
|
205
|
+
errors << path
|
|
206
|
+
elsif File.directory?(path)
|
|
207
|
+
pending << [path, Dir.each_child(path)]
|
|
208
|
+
elsif %w[.json .jsonl].include?(File.extname(path)) && File.basename(path) != 'loot.json'
|
|
209
|
+
files << path
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
break if errors.include?('incomplete_dependency_index')
|
|
191
213
|
end
|
|
192
214
|
dependencies = {}
|
|
193
215
|
evidence_dependencies = {}
|
|
@@ -206,12 +228,20 @@ module PWN
|
|
|
206
228
|
errors << path
|
|
207
229
|
end
|
|
208
230
|
files.uniq.sort.each do |path|
|
|
231
|
+
if admission && !admission.call
|
|
232
|
+
errors << 'incomplete_dependency_index'
|
|
233
|
+
break
|
|
234
|
+
end
|
|
209
235
|
next unless File.exist?(path) || File.symlink?(path)
|
|
210
236
|
|
|
211
237
|
begin
|
|
212
238
|
raise IOError, 'Unsafe dependency parent' if errors.any? { |parent| path.start_with?("#{parent}/") }
|
|
213
239
|
raise IOError, 'Unsafe dependency store' unless File.file?(path) && File.realpath(path) == File.expand_path(path)
|
|
214
240
|
|
|
241
|
+
if admission && !admission.call(File.size(path))
|
|
242
|
+
errors << 'incomplete_dependency_index'
|
|
243
|
+
break
|
|
244
|
+
end
|
|
215
245
|
bytes = File.binread(path)
|
|
216
246
|
units = dependency_units(path: path, bytes: bytes)
|
|
217
247
|
units.each do |locator, record|
|
|
@@ -560,9 +590,12 @@ module PWN
|
|
|
560
590
|
sid = opts[:session_id].to_s
|
|
561
591
|
raise ArgumentError, 'Invalid session ID' if sid.empty? || File.basename(sid) != sid || sid == '.' || sid == '..'
|
|
562
592
|
|
|
563
|
-
|
|
593
|
+
path = support_path(kind: :lease, session_id: sid, read_only: true)
|
|
594
|
+
File.open(path, File::RDONLY | File::NOFOLLOW) do |lock|
|
|
564
595
|
!lock.flock(File::LOCK_EX | File::LOCK_NB)
|
|
565
596
|
end
|
|
597
|
+
rescue Errno::ENOENT
|
|
598
|
+
false
|
|
566
599
|
end
|
|
567
600
|
|
|
568
601
|
# Supported Method Parameters::
|
|
@@ -752,7 +785,9 @@ module PWN
|
|
|
752
785
|
# Character chunks keep Unicode intact, including four-byte characters.
|
|
753
786
|
text.chars.each_slice(1500) do |chunk|
|
|
754
787
|
output = PWN::AI::Router.summarize(text: "Previous overall summary: #{result[:summary]}\nNew conversation turns:\n#{chunk.join}", llm: true, session: true)
|
|
755
|
-
|
|
788
|
+
raise IOError, 'Session title provider returned no text' if output.to_s.strip.empty?
|
|
789
|
+
|
|
790
|
+
parsed = JSON.parse(output.to_s.strip.sub(/\A```(?:json)?\s*/, '').sub(/\s*```\z/, ''), symbolize_names: true)
|
|
756
791
|
raise ArgumentError, 'Invalid summary response' unless parsed.is_a?(Hash) && parsed[:title].is_a?(String) && parsed[:summary].is_a?(String)
|
|
757
792
|
raise ArgumentError, 'Invalid summary title' if !summary_title_valid?(title: parsed[:title]) || parsed[:summary].strip.empty?
|
|
758
793
|
|
|
@@ -835,7 +870,14 @@ module PWN
|
|
|
835
870
|
content: content,
|
|
836
871
|
timestamp: Time.now.utc.iso8601
|
|
837
872
|
}
|
|
838
|
-
File.open(path,
|
|
873
|
+
File.open("#{path}.write.lock", File::RDWR | File::CREAT | File::NOFOLLOW, 0o600) do |writer|
|
|
874
|
+
writer.flock(File::LOCK_EX)
|
|
875
|
+
File.open(path, File::WRONLY | File::APPEND | File::NOFOLLOW) do |file|
|
|
876
|
+
file.puts(JSON.dump(entry))
|
|
877
|
+
file.flush
|
|
878
|
+
file.fsync
|
|
879
|
+
end
|
|
880
|
+
end
|
|
839
881
|
provisional_title(session_id: sid, request: content) if role == 'user'
|
|
840
882
|
entry
|
|
841
883
|
ensure
|
|
@@ -1236,12 +1278,10 @@ module PWN
|
|
|
1236
1278
|
return false unless File.exist?(path)
|
|
1237
1279
|
|
|
1238
1280
|
lease = activate(session_id: File.basename(path, '.jsonl'))
|
|
1281
|
+
writer = File.open("#{path}.write.lock", File::RDWR | File::CREAT | File::NOFOLLOW, 0o600) # rubocop:disable Style/FileOpen -- held through atomic replace; ensure closes it
|
|
1282
|
+
writer.flock(File::LOCK_EX)
|
|
1239
1283
|
lines = File.readlines(path)
|
|
1240
|
-
rows = lines.map
|
|
1241
|
-
JSON.parse(l, symbolize_names: true)
|
|
1242
|
-
rescue StandardError
|
|
1243
|
-
nil
|
|
1244
|
-
end.compact
|
|
1284
|
+
rows = lines.map { |line| JSON.parse(line, symbolize_names: true) }
|
|
1245
1285
|
return false if rows.empty?
|
|
1246
1286
|
|
|
1247
1287
|
# Preserve last assistant fully-ish; compact tools and older assistants
|
|
@@ -1266,17 +1306,19 @@ module PWN
|
|
|
1266
1306
|
end
|
|
1267
1307
|
return changed if dry || !changed
|
|
1268
1308
|
|
|
1269
|
-
tmp = "#{path}.#{
|
|
1270
|
-
File.open(tmp, File::WRONLY | File::CREAT | File::
|
|
1309
|
+
tmp = "#{path}.#{SecureRandom.hex(12)}.tmp"
|
|
1310
|
+
File.open(tmp, File::WRONLY | File::CREAT | File::EXCL | File::NOFOLLOW, 0o600) do |f|
|
|
1271
1311
|
f.flock(File::LOCK_EX)
|
|
1272
1312
|
rows.each { |e| f.puts(JSON.generate(e)) }
|
|
1273
1313
|
f.flush
|
|
1274
1314
|
f.fsync
|
|
1275
1315
|
end
|
|
1276
1316
|
File.rename(tmp, path)
|
|
1317
|
+
File.open(File.dirname(path), File::RDONLY, &:fsync)
|
|
1277
1318
|
true
|
|
1278
1319
|
ensure
|
|
1279
1320
|
FileUtils.rm_f(tmp) if defined?(tmp) && tmp && File.exist?(tmp)
|
|
1321
|
+
writer&.close
|
|
1280
1322
|
deactivate(lease: lease)
|
|
1281
1323
|
end
|
|
1282
1324
|
|
|
@@ -1358,7 +1400,7 @@ module PWN
|
|
|
1358
1400
|
#{self}.list(include_hidden: 'optional - include unretained sessions for diagnostics and explicit resume', current_session_id: 'optional - include this active session regardless of naming classification', references: 'optional - precomputed structural dependency index')
|
|
1359
1401
|
|
|
1360
1402
|
# Resolve housekeeping paths with legacy compatibility.
|
|
1361
|
-
#{self}.support_path(kind: 'required - metadata, lease, owner or trace', session_id: 'required - support record identifier')
|
|
1403
|
+
#{self}.support_path(kind: 'required - metadata, lease, owner or trace', session_id: 'required - support record identifier', read_only: 'optional - resolve without creating session or support directories')
|
|
1362
1404
|
|
|
1363
1405
|
# Page retained sessions separately from global store diagnostics.
|
|
1364
1406
|
#{self}.catalog(offset: 'optional - nonnegative retained session offset, omitted selects newest page', limit: 'optional - page size from one to 200, defaults 25 plus active row if outside page', current_session_id: 'optional - active session id always included without granting retention')
|
data/lib/pwn/setup.rb
CHANGED
|
@@ -129,10 +129,44 @@ module PWN
|
|
|
129
129
|
}
|
|
130
130
|
}.freeze
|
|
131
131
|
|
|
132
|
+
# Optional runtime libraries, not executables or Ruby build dependencies.
|
|
133
|
+
# macOS provides PC/SC as a system framework. Linux PC/SC also needs a
|
|
134
|
+
# daemon and CCID reader driver for actual I/O; doctor only loads libraries.
|
|
135
|
+
NATIVE_LIBRARIES = {
|
|
136
|
+
'hidapi' => {
|
|
137
|
+
apt: %w[libhidapi-hidraw0], dnf: %w[hidapi], pacman: %w[hidapi],
|
|
138
|
+
brew: %w[hidapi], port: %w[hidapi],
|
|
139
|
+
libraries: {
|
|
140
|
+
unix: %w[libhidapi-hidraw.so.0 libhidapi-hidraw.so libhidapi-libusb.so.0 libhidapi-libusb.so libhidapi.so],
|
|
141
|
+
macos: %w[libhidapi.dylib /opt/homebrew/lib/libhidapi.dylib /usr/local/lib/libhidapi.dylib /opt/local/lib/libhidapi.dylib],
|
|
142
|
+
windows: %w[hidapi.dll]
|
|
143
|
+
},
|
|
144
|
+
plugins: %w[PWN::Plugins::YubiKey]
|
|
145
|
+
},
|
|
146
|
+
'pcsc' => {
|
|
147
|
+
apt: %w[libpcsclite1 pcscd libccid], dnf: %w[pcsc-lite-libs pcsc-lite pcsc-lite-ccid],
|
|
148
|
+
pacman: %w[pcsclite ccid], brew: %w[], port: %w[],
|
|
149
|
+
libraries: {
|
|
150
|
+
unix: %w[libpcsclite.so.1 libpcsclite.so],
|
|
151
|
+
macos: %w[/System/Library/Frameworks/PCSC.framework/PCSC],
|
|
152
|
+
windows: %w[winscard.dll]
|
|
153
|
+
},
|
|
154
|
+
plugins: %w[PWN::Plugins::YubiKey]
|
|
155
|
+
}
|
|
156
|
+
}.freeze
|
|
157
|
+
|
|
132
158
|
# External binaries PWN wraps → OS packages that provide them.
|
|
133
159
|
# (Extrospection.probe_toolchain already knows how to *detect* these;
|
|
134
160
|
# this table knows how to *install* them.)
|
|
135
161
|
TOOLCHAIN = {
|
|
162
|
+
'setpriv' => {
|
|
163
|
+
apt: %w[util-linux], dnf: %w[util-linux], pacman: %w[util-linux], brew: %w[], port: %w[],
|
|
164
|
+
plugins: %w[PWN::SessionTrace]
|
|
165
|
+
},
|
|
166
|
+
'unshare' => {
|
|
167
|
+
apt: %w[util-linux], dnf: %w[util-linux], pacman: %w[util-linux], brew: %w[], port: %w[],
|
|
168
|
+
plugins: %w[PWN::AI::Agent::MissionNamespace]
|
|
169
|
+
},
|
|
136
170
|
'nmap' => {
|
|
137
171
|
apt: %w[nmap], dnf: %w[nmap], pacman: %w[nmap], brew: %w[nmap], port: %w[nmap],
|
|
138
172
|
plugins: %w[PWN::Plugins::NmapIt]
|
|
@@ -408,7 +442,7 @@ module PWN
|
|
|
408
442
|
ai: {
|
|
409
443
|
desc: 'verify at least one AI engine key/oauth in ~/.pwn/pwn.yaml',
|
|
410
444
|
gems: %w[],
|
|
411
|
-
bins: %w[jq]
|
|
445
|
+
bins: %w[jq unshare setpriv]
|
|
412
446
|
},
|
|
413
447
|
web: {
|
|
414
448
|
desc: 'TransparentBrowser · BurpSuite · Zaproxy · extro_verify · extro_watch',
|
|
@@ -446,9 +480,10 @@ module PWN
|
|
|
446
480
|
bins: %w[msfconsole sqlmap]
|
|
447
481
|
},
|
|
448
482
|
hardware: {
|
|
449
|
-
desc: 'Serial · BusPirate · Android · BareSIP · extro_serial · extro_telecomm',
|
|
483
|
+
desc: 'Serial · BusPirate · Android · BareSIP · YubiKey · extro_serial · extro_telecomm',
|
|
450
484
|
gems: %w[libusb],
|
|
451
|
-
bins: %w[adb baresip]
|
|
485
|
+
bins: %w[adb baresip],
|
|
486
|
+
libs: %w[hidapi pcsc]
|
|
452
487
|
},
|
|
453
488
|
re: {
|
|
454
489
|
desc: 'Radare2 · GDB · ExploitDev · Frida · Android RE tier',
|
|
@@ -458,7 +493,8 @@ module PWN
|
|
|
458
493
|
full: {
|
|
459
494
|
desc: 'everything above',
|
|
460
495
|
gems: NATIVE_GEMS.keys,
|
|
461
|
-
bins: TOOLCHAIN.keys
|
|
496
|
+
bins: TOOLCHAIN.keys,
|
|
497
|
+
libs: NATIVE_LIBRARIES.keys
|
|
462
498
|
}
|
|
463
499
|
}.freeze
|
|
464
500
|
|
|
@@ -572,15 +608,23 @@ module PWN
|
|
|
572
608
|
io.puts
|
|
573
609
|
|
|
574
610
|
# ~/.pwn -----------------------------------------------------------
|
|
575
|
-
pwn_root
|
|
576
|
-
|
|
577
|
-
|
|
611
|
+
pwn_root = File.join(Dir.home, '.pwn')
|
|
612
|
+
driver = defined?(PWN::Env) && PWN::Env.is_a?(Hash) ? PWN::Env[:driver_opts] : nil
|
|
613
|
+
driver = {} unless driver.is_a?(Hash)
|
|
614
|
+
pwn_yaml = driver[:pwn_env_path] || File.join(pwn_root, 'pwn.yaml')
|
|
615
|
+
pwn_dec = driver[:pwn_dec_path] || "#{pwn_yaml}.decryptor"
|
|
578
616
|
root_ok = File.directory?(pwn_root)
|
|
579
617
|
root_note = root_ok ? "(#{Dir.children(pwn_root).length} entries)" : '(will be created on first `pwn` launch)'
|
|
580
618
|
io.puts "#{'~/.pwn/'.ljust(26)} #{root_ok ? OK : MISS} #{root_note}"
|
|
581
619
|
yaml_ok = File.file?(pwn_yaml)
|
|
582
620
|
dec_state = File.file?(pwn_dec) ? 'present' : 'MISSING'
|
|
583
|
-
yaml_note = yaml_ok
|
|
621
|
+
yaml_note = if !yaml_ok
|
|
622
|
+
'(run `pwn` once, then `pwn-vault` to configure)'
|
|
623
|
+
elsif PWN::Plugins::Vault.hardware_encrypted?(file: pwn_yaml)
|
|
624
|
+
'(YubiKey encrypted; no software decryptor required)'
|
|
625
|
+
else
|
|
626
|
+
"(encrypted, decryptor #{dec_state})"
|
|
627
|
+
end
|
|
584
628
|
io.puts "#{'~/.pwn/pwn.yaml'.ljust(26)} #{yaml_ok ? OK : MISS} #{yaml_note}"
|
|
585
629
|
engine = ai_active_engine
|
|
586
630
|
key_ok = ai_key_configured?(engine: engine)
|
|
@@ -603,6 +647,20 @@ module PWN
|
|
|
603
647
|
end
|
|
604
648
|
io.puts
|
|
605
649
|
|
|
650
|
+
# Runtime libraries ------------------------------------------------
|
|
651
|
+
io.puts 'Native libraries (host loading only; no device discovery)'
|
|
652
|
+
lib_missing = []
|
|
653
|
+
NATIVE_LIBRARIES.each do |name, meta|
|
|
654
|
+
ok = native_library_available?(name: name, os: os)
|
|
655
|
+
lib_missing << name unless ok
|
|
656
|
+
packages = Array(meta[pm[:key]])
|
|
657
|
+
hint = packages.empty? ? 'native OS library; see hardware profile' : packages.join(' ')
|
|
658
|
+
need = ok ? '' : "(needs: #{hint})"
|
|
659
|
+
io.puts " #{name.ljust(14)} #{ok ? OK : MISS} #{need} → #{Array(meta[:plugins]).join(', ')}"
|
|
660
|
+
end
|
|
661
|
+
io.puts ' Library presence does not verify device permissions, PC/SC service, reader or token readiness.'
|
|
662
|
+
io.puts
|
|
663
|
+
|
|
606
664
|
# Toolchain --------------------------------------------------------
|
|
607
665
|
io.puts "#{'External toolchain'.ljust(46)} used by"
|
|
608
666
|
bin_missing = []
|
|
@@ -621,36 +679,41 @@ module PWN
|
|
|
621
679
|
io.puts
|
|
622
680
|
end
|
|
623
681
|
|
|
624
|
-
total = NATIVE_GEMS.size + TOOLCHAIN.size
|
|
625
|
-
missing = gem_missing.size + bin_missing.size
|
|
682
|
+
total = NATIVE_GEMS.size + TOOLCHAIN.size + NATIVE_LIBRARIES.size
|
|
683
|
+
missing = gem_missing.size + bin_missing.size + lib_missing.size
|
|
626
684
|
io.puts "#{total - missing} / #{total} capabilities usable · #{missing} degraded"
|
|
627
685
|
io.puts
|
|
628
686
|
unless missing.zero?
|
|
629
|
-
io.puts 'Run `pwn setup --deps` to install missing OS headers/tools, or'
|
|
687
|
+
io.puts 'Run `pwn setup --deps` to install missing OS headers/tools/libraries, or'
|
|
630
688
|
io.puts ' `pwn setup --profile <name>` for a subset. See `pwn setup --list-profiles`.'
|
|
631
689
|
end
|
|
632
690
|
|
|
633
691
|
# When a profile is given (`pwn setup --check --profile core`), :ok is
|
|
634
|
-
# gated ONLY on that profile's gems/bins so a partially-provisioned host
|
|
692
|
+
# gated ONLY on that profile's gems/bins/libs so a partially-provisioned host
|
|
635
693
|
# can still pass its own doctor gate. `packer/provisioners/pwn.sh` and
|
|
636
694
|
# spec/integration/fresh_install_spec.rb rely on this — a `core` leg of
|
|
637
695
|
# install-matrix must not fail because gqrx/burpsuite/etc. are absent.
|
|
638
696
|
gate_gems = gem_missing
|
|
639
697
|
gate_bins = bin_missing
|
|
698
|
+
gate_libs = lib_missing
|
|
640
699
|
if profile && PROFILES.key?(profile)
|
|
641
700
|
prof = PROFILES[profile]
|
|
642
701
|
gate_gems = gem_missing & Array(prof[:gems])
|
|
643
702
|
gate_bins = bin_missing & Array(prof[:bins])
|
|
703
|
+
gate_libs = lib_missing & Array(prof[:libs])
|
|
644
704
|
io.puts
|
|
705
|
+
library_note = ", libs: #{gate_libs.empty? ? '—' : gate_libs.join(' ')}" unless Array(prof[:libs]).empty?
|
|
645
706
|
io.puts "Profile '#{profile}' gate: " \
|
|
646
|
-
"#{gate_gems.empty? && gate_bins.empty? ? OK : MISS} " \
|
|
707
|
+
"#{gate_gems.empty? && gate_bins.empty? && gate_libs.empty? ? OK : MISS} " \
|
|
647
708
|
"(gems: #{gate_gems.empty? ? '—' : gate_gems.join(' ')}, " \
|
|
648
|
-
"bins: #{gate_bins.empty? ? '—' : gate_bins.join(' ')}
|
|
709
|
+
"bins: #{gate_bins.empty? ? '—' : gate_bins.join(' ')}" \
|
|
710
|
+
"#{library_note})"
|
|
649
711
|
end
|
|
650
712
|
|
|
651
|
-
{ ok: gate_gems.empty? && gate_bins.empty? && Array(migrate && migrate[:incompatible]).empty?,
|
|
713
|
+
{ ok: gate_gems.empty? && gate_bins.empty? && gate_libs.empty? && Array(migrate && migrate[:incompatible]).empty?,
|
|
652
714
|
native_gems_missing: gem_missing, toolchain_missing: bin_missing,
|
|
653
|
-
state: migrate, pkg_manager: pm[:key], os: os, arch: arch
|
|
715
|
+
state: migrate, pkg_manager: pm[:key], os: os, arch: arch,
|
|
716
|
+
native_libraries_missing: lib_missing }
|
|
654
717
|
rescue StandardError => e
|
|
655
718
|
raise e
|
|
656
719
|
end
|
|
@@ -690,6 +753,7 @@ module PWN
|
|
|
690
753
|
bins = Array(prof[:bins])
|
|
691
754
|
os_pkgs = []
|
|
692
755
|
gems.each { |g| os_pkgs.concat(Array(NATIVE_GEMS.dig(g, pm[:key]))) }
|
|
756
|
+
Array(prof[:libs]).each { |lib| os_pkgs.concat(Array(NATIVE_LIBRARIES.dig(lib, pm[:key]))) }
|
|
693
757
|
bins.each { |b| os_pkgs.concat(packages_for(bin: b, distro: distro, version: version, pm_key: pm[:key])) }
|
|
694
758
|
os_pkgs = os_pkgs.reject(&:empty?).uniq
|
|
695
759
|
alts = alt_install_cmds(bins: bins, pm_key: pm[:key], distro: distro, version: version)
|
|
@@ -765,7 +829,7 @@ module PWN
|
|
|
765
829
|
# third_party/terminator/pwn_shift_enter.py to
|
|
766
830
|
# ~/.config/terminator/plugins/ and enables the PWNShiftEnter
|
|
767
831
|
# plugin in ~/.config/terminator/config — WITH confirmation and
|
|
768
|
-
# a
|
|
832
|
+
# a shared recovery backup. This is the only VTE emulator we can fix at
|
|
769
833
|
# the GTK layer without patching libvte itself.
|
|
770
834
|
#
|
|
771
835
|
# Everything here is host-config mutation, which is why it lives in
|
|
@@ -831,7 +895,7 @@ module PWN
|
|
|
831
895
|
io.puts
|
|
832
896
|
io.puts " write #{plugin_dst}"
|
|
833
897
|
io.puts " edit #{cfg_path} (add PWNShiftEnter to enabled_plugins)"
|
|
834
|
-
io.puts
|
|
898
|
+
io.puts ' backup ~/.pwn/backup (shared seven-generation store)'
|
|
835
899
|
io.puts
|
|
836
900
|
|
|
837
901
|
unless yes
|
|
@@ -858,7 +922,7 @@ module PWN
|
|
|
858
922
|
if cfg.include?('PWNShiftEnter')
|
|
859
923
|
io.puts " #{OK} PWNShiftEnter already enabled in #{cfg_path}"
|
|
860
924
|
else
|
|
861
|
-
|
|
925
|
+
|
|
862
926
|
new_cfg =
|
|
863
927
|
if cfg =~ /^\s*enabled_plugins\s*=\s*(.*)$/
|
|
864
928
|
cfg.sub(/^(\s*enabled_plugins\s*=\s*)(.*)$/) do
|
|
@@ -871,8 +935,11 @@ module PWN
|
|
|
871
935
|
else
|
|
872
936
|
"[global_config]\n enabled_plugins = PWNShiftEnter\n#{cfg}"
|
|
873
937
|
end
|
|
874
|
-
|
|
875
|
-
|
|
938
|
+
PWN::Backups.transaction do
|
|
939
|
+
backup = PWN::Backups.capture(path: cfg_path, label: 'terminator')
|
|
940
|
+
File.write(cfg_path, new_cfg)
|
|
941
|
+
io.puts " #{OK} enabled PWNShiftEnter in #{cfg_path} (backup: #{backup})"
|
|
942
|
+
end
|
|
876
943
|
end
|
|
877
944
|
else
|
|
878
945
|
FileUtils.mkdir_p(File.dirname(cfg_path))
|
|
@@ -900,6 +967,7 @@ module PWN
|
|
|
900
967
|
io.puts " #{name.to_s.ljust(10)} #{meta[:desc]}"
|
|
901
968
|
io.puts " #{''.ljust(10)} gems: #{meta[:gems].join(' ')}" unless meta[:gems].empty?
|
|
902
969
|
io.puts " #{''.ljust(10)} bins: #{meta[:bins].join(' ')}" unless meta[:bins].empty?
|
|
970
|
+
io.puts " #{''.ljust(10)} libs: #{meta[:libs].join(' ')}" unless Array(meta[:libs]).empty?
|
|
903
971
|
io.puts
|
|
904
972
|
end
|
|
905
973
|
PROFILES.keys
|
|
@@ -998,6 +1066,28 @@ module PWN
|
|
|
998
1066
|
''
|
|
999
1067
|
end
|
|
1000
1068
|
|
|
1069
|
+
private_class_method def self.native_library_available?(opts = {})
|
|
1070
|
+
require 'ffi'
|
|
1071
|
+
os = opts[:os].to_s
|
|
1072
|
+
platform = if os.match?(/darwin|osx|macos/i)
|
|
1073
|
+
:macos
|
|
1074
|
+
elsif os.match?(/mswin|mingw|cygwin|windows/i)
|
|
1075
|
+
:windows
|
|
1076
|
+
else
|
|
1077
|
+
:unix
|
|
1078
|
+
end
|
|
1079
|
+
libraries = NATIVE_LIBRARIES.fetch(opts[:name]).fetch(:libraries).fetch(platform)
|
|
1080
|
+
flags = ::FFI::DynamicLibrary::RTLD_LAZY | ::FFI::DynamicLibrary::RTLD_LOCAL
|
|
1081
|
+
libraries.any? do |library|
|
|
1082
|
+
::FFI::DynamicLibrary.open(library, flags)
|
|
1083
|
+
true
|
|
1084
|
+
rescue LoadError
|
|
1085
|
+
false
|
|
1086
|
+
end
|
|
1087
|
+
rescue LoadError
|
|
1088
|
+
false
|
|
1089
|
+
end
|
|
1090
|
+
|
|
1001
1091
|
private_class_method def self.gem_loadable?(opts = {})
|
|
1002
1092
|
name = opts[:name].to_s
|
|
1003
1093
|
Gem::Specification.find_by_name(name)
|
|
@@ -1047,22 +1137,20 @@ module PWN
|
|
|
1047
1137
|
end
|
|
1048
1138
|
|
|
1049
1139
|
if dry_run
|
|
1050
|
-
st = PWN::Cron.
|
|
1140
|
+
st = { worker: PWN::Cron.worker_status, backend: :worker }
|
|
1051
1141
|
io.puts "#{'cron worker'.ljust(26)} #{st.dig(:worker, :running) ? OK : MISS} (dry-run, backend=#{st[:backend]}, enabled=#{enabled})"
|
|
1052
1142
|
return { ok: true, dry_run: true, worker: st[:worker], backend: st[:backend], enabled: enabled }
|
|
1053
1143
|
end
|
|
1054
1144
|
|
|
1055
1145
|
unless enabled
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
io.puts "#{'cron scheduler'.ljust(26)} #{MISS} #{e.class}: #{e.message}"
|
|
1060
|
-
{ ok: false, error: "#{e.class}: #{e.message}" }
|
|
1061
|
-
end
|
|
1146
|
+
PWN::Cron.persist_scheduler_config(enabled: false)
|
|
1147
|
+
worker = PWN::Cron.stop_worker
|
|
1148
|
+
native = { backend: :worker, worker: worker }
|
|
1062
1149
|
io.puts "#{'cron scheduler'.ljust(26)} #{OK} disabled (#{native[:backend] || 'none'})"
|
|
1063
1150
|
return { ok: true, enabled: false, native: native, seeded: [] }
|
|
1064
1151
|
end
|
|
1065
1152
|
|
|
1153
|
+
PWN::Cron.persist_scheduler_config(enabled: true) if opts.key?(:enabled)
|
|
1066
1154
|
seeded = begin
|
|
1067
1155
|
PWN::Cron.install_defaults
|
|
1068
1156
|
rescue StandardError => e
|
|
@@ -1078,6 +1166,7 @@ module PWN
|
|
|
1078
1166
|
seeded: seeded,
|
|
1079
1167
|
worker: live.merge(already_running: true),
|
|
1080
1168
|
skipped_spawn: true,
|
|
1169
|
+
backend: :worker,
|
|
1081
1170
|
enabled: true
|
|
1082
1171
|
}
|
|
1083
1172
|
end
|
|
@@ -1097,16 +1186,9 @@ module PWN
|
|
|
1097
1186
|
mark = st[:running] ? OK : MISS
|
|
1098
1187
|
io.puts "#{'cron worker'.ljust(26)} #{mark} #{label} pid=#{st[:pid] || worker[:pid] || 'none'}"
|
|
1099
1188
|
|
|
1100
|
-
native
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
apply: opts.fetch(:apply, PWN::Cron.apply_native?)
|
|
1104
|
-
)
|
|
1105
|
-
rescue StandardError => e
|
|
1106
|
-
io.puts "#{'cron scheduler'.ljust(26)} #{MISS} #{e.class}: #{e.message}"
|
|
1107
|
-
{ ok: false, error: "#{e.class}: #{e.message}" }
|
|
1108
|
-
end
|
|
1109
|
-
backend = native.is_a?(Hash) ? native[:backend] : nil
|
|
1189
|
+
# Setup never installs, starts, removes or repairs native schedulers.
|
|
1190
|
+
native = { ok: true, backend: :worker, persisted: false }
|
|
1191
|
+
backend = :worker
|
|
1110
1192
|
io.puts "#{'cron scheduler'.ljust(26)} #{native[:ok] == false ? MISS : OK} backend=#{backend || 'none'}"
|
|
1111
1193
|
|
|
1112
1194
|
{
|
|
@@ -1145,13 +1227,13 @@ module PWN
|
|
|
1145
1227
|
pm_key: 'optional - package-manager family (:apt :dnf :pacman :brew :port :pkg)'
|
|
1146
1228
|
)
|
|
1147
1229
|
|
|
1148
|
-
#
|
|
1230
|
+
# Check gems, binaries and native host libraries; native_libraries_missing reports HIDAPI/PCSC without device discovery.
|
|
1149
1231
|
#{self}.check(
|
|
1150
1232
|
io: 'optional - IO to write the report to (default $stdout)',
|
|
1151
|
-
profile: 'optional - profile
|
|
1233
|
+
profile: 'optional - gate only this profile; core never requires hardware libraries'
|
|
1152
1234
|
)
|
|
1153
1235
|
|
|
1154
|
-
# Install OS packages
|
|
1236
|
+
# Install OS packages and gems; hardware/full add HIDAPI and PC/SC, not a Python or executable YubiKey backend.
|
|
1155
1237
|
#{self}.deps(
|
|
1156
1238
|
profile: 'optional - one of PROFILES.keys (default :full)',
|
|
1157
1239
|
yes: 'optional - non-interactive; assume yes to prompts (default false)',
|