pwn 0.5.719 → 0.5.721

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.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/etc/default_skills/pwn/ai/agent/engagement/SKILL.md +53 -0
  3. data/etc/default_skills/pwn/ai/agent/learning/SKILL.md +4 -0
  4. data/etc/default_skills/pwn/ai/agent/loop/SKILL.md +5 -2
  5. data/etc/default_skills/pwn/ai/agent/metrics/SKILL.md +1 -0
  6. data/etc/default_skills/pwn/ai/agent/registry/SKILL.md +3 -0
  7. data/etc/default_skills/pwn/ai/agent/swarm/SKILL.md +1 -0
  8. data/etc/default_skills/pwn/ai/agent/tool_guard/SKILL.md +3 -0
  9. data/etc/default_skills/pwn/ai/agent/tools/debug_lane/SKILL.md +45 -0
  10. data/etc/default_skills/pwn/ai/agent/tools/engagement/SKILL.md +45 -0
  11. data/etc/default_skills/pwn/ai/agent/turn_finalizer/SKILL.md +4 -0
  12. data/etc/default_skills/pwn/plugins/artifact_registry/SKILL.md +1 -0
  13. data/etc/default_skills/pwn/plugins/bin_workspace/SKILL.md +49 -0
  14. data/etc/default_skills/pwn/plugins/binary_parser/SKILL.md +1 -0
  15. data/etc/default_skills/pwn/plugins/capability/SKILL.md +48 -0
  16. data/etc/default_skills/pwn/plugins/detonate/SKILL.md +47 -0
  17. data/etc/default_skills/pwn/plugins/emulator/SKILL.md +47 -0
  18. data/etc/default_skills/pwn/plugins/exploit_dev/SKILL.md +1 -0
  19. data/etc/default_skills/pwn/plugins/findings/SKILL.md +1 -0
  20. data/etc/default_skills/pwn/plugins/gdb/SKILL.md +2 -0
  21. data/etc/default_skills/pwn/plugins/packet/SKILL.md +1 -0
  22. data/etc/default_skills/pwn/plugins/preflight_checker/SKILL.md +1 -0
  23. data/etc/default_skills/pwn/plugins/process_tube/SKILL.md +2 -0
  24. data/etc/default_skills/pwn/plugins/repl/SKILL.md +1 -0
  25. data/etc/default_skills/pwn/plugins/vault/SKILL.md +4 -0
  26. data/etc/default_skills/pwn/reports/engagement/SKILL.md +47 -0
  27. data/lib/pwn/ai/agent/dispatch.rb +69 -1
  28. data/lib/pwn/ai/agent/engagement.rb +167 -0
  29. data/lib/pwn/ai/agent/learning.rb +105 -5
  30. data/lib/pwn/ai/agent/loop.rb +83 -25
  31. data/lib/pwn/ai/agent/metrics.rb +35 -1
  32. data/lib/pwn/ai/agent/registry.rb +28 -0
  33. data/lib/pwn/ai/agent/reward.rb +80 -15
  34. data/lib/pwn/ai/agent/swarm.rb +36 -0
  35. data/lib/pwn/ai/agent/tool_guard.rb +93 -9
  36. data/lib/pwn/ai/agent/tools/debug_lane.rb +160 -0
  37. data/lib/pwn/ai/agent/tools/engagement.rb +40 -0
  38. data/lib/pwn/ai/agent/turn_finalizer.rb +84 -0
  39. data/lib/pwn/ai/agent.rb +1 -0
  40. data/lib/pwn/config.rb +18 -2
  41. data/lib/pwn/plugins/artifact_registry.rb +32 -2
  42. data/lib/pwn/plugins/bin_workspace.rb +81 -0
  43. data/lib/pwn/plugins/binary_parser.rb +27 -0
  44. data/lib/pwn/plugins/capability.rb +68 -0
  45. data/lib/pwn/plugins/detonate.rb +62 -0
  46. data/lib/pwn/plugins/emulator.rb +64 -0
  47. data/lib/pwn/plugins/exploit_dev.rb +33 -0
  48. data/lib/pwn/plugins/findings.rb +56 -1
  49. data/lib/pwn/plugins/gdb.rb +42 -0
  50. data/lib/pwn/plugins/ghidra.rb +20 -2
  51. data/lib/pwn/plugins/packet.rb +26 -0
  52. data/lib/pwn/plugins/preflight_checker.rb +23 -0
  53. data/lib/pwn/plugins/process_tube.rb +31 -1
  54. data/lib/pwn/plugins/repl.rb +25 -2
  55. data/lib/pwn/plugins/vault.rb +116 -0
  56. data/lib/pwn/plugins.rb +4 -0
  57. data/lib/pwn/reports/engagement.rb +63 -0
  58. data/lib/pwn/reports.rb +1 -0
  59. data/lib/pwn/sessions.rb +4 -0
  60. data/lib/pwn/version.rb +1 -1
  61. data/spec/lib/pwn/ai/agent/dispatch_spec.rb +9 -0
  62. data/spec/lib/pwn/ai/agent/engagement_spec.rb +27 -0
  63. data/spec/lib/pwn/ai/agent/learning_spec.rb +23 -0
  64. data/spec/lib/pwn/ai/agent/loop_spec.rb +44 -0
  65. data/spec/lib/pwn/ai/agent/reward_spec.rb +17 -1
  66. data/spec/lib/pwn/ai/agent/swarm_spec.rb +11 -0
  67. data/spec/lib/pwn/ai/agent/tool_guard_spec.rb +9 -0
  68. data/spec/lib/pwn/ai/agent/tools/debug_lane_spec.rb +16 -0
  69. data/spec/lib/pwn/ai/agent/tools/engagement_spec.rb +16 -0
  70. data/spec/lib/pwn/ai/agent/turn_finalizer_spec.rb +18 -0
  71. data/spec/lib/pwn/plugins/bin_workspace_spec.rb +26 -0
  72. data/spec/lib/pwn/plugins/capability_spec.rb +19 -0
  73. data/spec/lib/pwn/plugins/detonate_spec.rb +24 -0
  74. data/spec/lib/pwn/plugins/emulator_spec.rb +18 -0
  75. data/spec/lib/pwn/plugins/findings_spec.rb +16 -3
  76. data/spec/lib/pwn/plugins/gdb_spec.rb +10 -0
  77. data/spec/lib/pwn/plugins/packet_spec.rb +6 -0
  78. data/spec/lib/pwn/plugins/repl_spec.rb +1 -1
  79. data/spec/lib/pwn/plugins/vault_spec.rb +11 -0
  80. data/spec/lib/pwn/reports/engagement_spec.rb +30 -0
  81. data/third_party/pwn_rdoc.jsonl +73 -0
  82. metadata +25 -1
@@ -411,7 +411,7 @@ describe 'PWN::AI::Agent::Reward vs TUI plan' do
411
411
 
412
412
  it 'floors verified PASS analytical answers at 0.6' do
413
413
  src = File.read(PWN::AI::Agent::Reward.method(:judge).source_location.first)
414
- expect(src).to include('[score, 0.7].max')
414
+ expect(src).to include('[score, 0.6].max')
415
415
  expect(src).to include('\bPASS\b')
416
416
  end
417
417
 
@@ -422,4 +422,20 @@ describe 'PWN::AI::Agent::Reward vs TUI plan' do
422
422
  m = PWN::AI::Agent::Reward.send(:judge_model)
423
423
  expect(m).to eq('local-judge')
424
424
  end
425
+
426
+ it 'records verifier PASS as success even when the judge scores 0.0' do
427
+ allow(PWN::AI::Agent::Reward).to receive(:llm_judge).and_return(
428
+ { score: 0.0, source: 'heuristic', verdict: :wrong, rationale: 'overlap=0.01', confidence: 0.3 }
429
+ )
430
+ v = PWN::AI::Agent::Reward.judge(
431
+ request: 'write /tmp/x',
432
+ final: 'PASS file exists',
433
+ trace: ['{"success":true}'],
434
+ commit: false,
435
+ verifier_verdict: :pass
436
+ )
437
+ expect(v[:success]).to eq(true)
438
+ expect(v[:verifier_verdict]).to eq(:pass)
439
+ expect(v[:judge_score]).to be < 0.6
440
+ end
425
441
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'spec_helper'
4
+ require 'tmpdir'
4
5
 
5
6
  describe PWN::AI::Agent::Swarm do
6
7
  it 'should display information for authors' do
@@ -43,4 +44,14 @@ describe PWN::AI::Agent::Swarm do
43
44
  expect(rows.length).to eq(4)
44
45
  expect(rows.map { |r| r[:port] }.uniq.length).to eq(4)
45
46
  end
47
+
48
+ it 'allows only one live claim per unit' do
49
+ Dir.mktmpdir do |dir|
50
+ allow(Dir).to receive(:home).and_return(dir)
51
+ a = described_class.claim(unit: '10.0.0.5:recon', agent_id: 'a', ttl: 60, engagement_id: 'lab')
52
+ b = described_class.claim(unit: '10.0.0.5:recon', agent_id: 'b', ttl: 60, engagement_id: 'lab')
53
+ expect(a[:ok]).to eq(true)
54
+ expect(b[:ok]).to eq(false)
55
+ end
56
+ end
46
57
  end
@@ -36,6 +36,15 @@ describe PWN::AI::Agent::ToolGuard do
36
36
  it 'rejects a real command' do
37
37
  expect(described_class.placeholder?(text: 'uname -r')).to be false
38
38
  end
39
+
40
+ it 'does not treat ellipsis inside a heredoc body as a placeholder command' do
41
+ body = "cat <<'EOF'\nThe token ... lives in the document body.\nEOF\n"
42
+ expect(described_class.placeholder?(text: body)).to be false
43
+ end
44
+
45
+ it 'still denies a command that is only a placeholder token' do
46
+ expect(described_class.placeholder?(text: ' ... ')).to be true
47
+ end
39
48
  end
40
49
 
41
50
  describe '.coerce_args' do
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe 'PWN::AI::Agent::Tools debug_lane' do
6
+ before(:all) do
7
+ PWN::AI::Agent::Registry.discover(force: true)
8
+ load '/opt/pwn/lib/pwn/ai/agent/tools/debug_lane.rb'
9
+ end
10
+
11
+ it 'registers debug_session binary_diff crash_triage emulate detonate budget_status pty_expect' do
12
+ %w[debug_session binary_diff crash_triage capability_request emulate detonate budget_status pty_expect].each do |name|
13
+ expect(PWN::AI::Agent::Registry.lookup(name: name)).not_to be_nil
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe 'PWN::AI::Agent::Tools engagement' do
6
+ before(:all) do
7
+ PWN::AI::Agent::Registry.discover(force: true)
8
+ load '/opt/pwn/lib/pwn/ai/agent/tools/engagement.rb'
9
+ end
10
+
11
+ it 'registers engagement_open close and status' do
12
+ expect(PWN::AI::Agent::Registry.lookup(name: 'engagement_open')).not_to be_nil
13
+ expect(PWN::AI::Agent::Registry.lookup(name: 'engagement_close')).not_to be_nil
14
+ expect(PWN::AI::Agent::Registry.lookup(name: 'engagement_status')).not_to be_nil
15
+ end
16
+ end
@@ -103,4 +103,22 @@ describe PWN::AI::Agent::TurnFinalizer do
103
103
  described_class.leave_user_path! while described_class.user_path?
104
104
  end
105
105
  end
106
+
107
+ it 'finalizes write-then-readback and names unmet without readback' do
108
+ Dir.mktmpdir do |dir|
109
+ path = File.join(dir, 'out.pdf')
110
+ File.write(path, 'pdf')
111
+ msgs = [
112
+ { role: 'tool', content: { success: true, result: { path: path }, effect: 'write' }.to_json },
113
+ { role: 'tool', content: { success: true, result: { stdout: path }, effect: 'read' }.to_json }
114
+ ]
115
+ row = described_class.arbitrate(request: "store #{path}", messages: msgs, paths: [path])
116
+ expect(row[:complete]).to eq(true)
117
+ expect(row[:unmet]).to eq([])
118
+ write_only = [{ role: 'tool', content: { success: true, result: { path: path }, effect: 'write' }.to_json }]
119
+ nag = described_class.arbitrate(request: "store #{path}", messages: write_only, paths: [path])
120
+ expect(nag[:complete]).to eq(false)
121
+ expect(nag[:unmet].map { |u| u[:criterion] }).to include('readback_missing')
122
+ end
123
+ end
106
124
  end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'tmpdir'
5
+
6
+ describe PWN::Plugins::BinWorkspace do
7
+ it 'should display information for authors' do
8
+ expect(described_class).to respond_to :authors
9
+ end
10
+
11
+ it 'should display information for existing help method' do
12
+ expect(described_class).to respond_to :help
13
+ end
14
+
15
+ it 'persists annotations by sha256 across opens' do
16
+ Dir.mktmpdir do |dir|
17
+ allow(Dir).to receive(:home).and_return(dir)
18
+ stub_const('PWN::Plugins::BinWorkspace::ROOT', File.join(dir, '.pwn', 'binws'))
19
+ bin = File.join(dir, 'a.bin')
20
+ File.binwrite(bin, 'ELF')
21
+ described_class.annotate(path: bin, addr: '0x401000', kind: 'rename', name: 'win')
22
+ notes = described_class.notes(path: bin)
23
+ expect(notes[:'0x401000'][:name]).to eq('win')
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe PWN::Plugins::Capability do
6
+ it 'should display information for authors' do
7
+ expect(described_class).to respond_to :authors
8
+ end
9
+
10
+ it 'should display information for existing help method' do
11
+ expect(described_class).to respond_to :help
12
+ end
13
+
14
+ it 'does not apply setcap without operator_ack' do
15
+ row = described_class.request(cap: 'cap_net_raw', reason: 'packet')
16
+ expect(row[:needs_ack]).to eq(true)
17
+ expect(row[:ok]).to eq(false)
18
+ end
19
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'tmpdir'
5
+
6
+ describe PWN::Plugins::Detonate do
7
+ it 'should display information for authors' do
8
+ expect(described_class).to respond_to :authors
9
+ end
10
+
11
+ it 'should display information for existing help method' do
12
+ expect(described_class).to respond_to :help
13
+ end
14
+
15
+ it 'refuses when isolation preconditions are unmet' do
16
+ Dir.mktmpdir do |dir|
17
+ sample = File.join(dir, 's.bin')
18
+ File.write(sample, 'MZ')
19
+ allow(PWN::Plugins::PreflightChecker).to receive(:bin?).with(name: 'podman').and_return(false)
20
+ row = described_class.detonate(path: sample)
21
+ expect(row[:error].to_s).to match(/isolation/)
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe PWN::Plugins::Emulator do
6
+ it 'should display information for authors' do
7
+ expect(described_class).to respond_to :authors
8
+ end
9
+
10
+ it 'should display information for existing help method' do
11
+ expect(described_class).to respond_to :help
12
+ end
13
+
14
+ it 'returns stub plaintext for a decode function' do
15
+ row = described_class.emulate(binary: '/bin/true', addr: '0x401000', backend: 'stub', plaintext: 'secret')
16
+ expect(row[:ret]).to eq('secret')
17
+ end
18
+ end
@@ -7,7 +7,7 @@ describe PWN::Plugins::Findings do
7
7
  it 'records and reports a finding' do
8
8
  Dir.mktmpdir do |dir|
9
9
  stub_const('PWN::Plugins::Findings::FILE', File.join(dir, 'findings.jsonl'))
10
- row = described_class.record(title: 'xss', severity: 'high', host: '127.0.0.1', poc: '/tmp/poc.rb')
10
+ row = described_class.record(title: 'xss', severity: 'high', host: '127.0.0.1', poc: '/tmp/poc.rb', evidence: 'PoC at /tmp/poc.rb reproduces XSS in search with a reflected payload.')
11
11
  expect(row[:id]).not_to be_empty
12
12
  expect(described_class.report.first[:title]).to eq('xss')
13
13
  end
@@ -19,7 +19,7 @@ describe PWN::Plugins::Findings do
19
19
  expect { described_class.record(title: 'xss') }.to raise_error(/poc/i)
20
20
  poc = File.join(dir, 'poc.rb')
21
21
  File.write(poc, 'puts 1')
22
- row = described_class.record(title: 'xss', poc_artifacts: [poc], severity: 'high')
22
+ row = described_class.record(title: 'xss', poc_artifacts: [poc], severity: 'high', evidence: 'PoC file reproduces reflected XSS in search; see artifact path.')
23
23
  expect(row[:poc_artifacts]).to include(poc)
24
24
  out = described_class.render(dir_path: dir, report_name: 'findings')
25
25
  expect(File.file?(out[:markdown])).to be true
@@ -33,10 +33,23 @@ describe PWN::Plugins::Findings do
33
33
  stub_const('PWN::Plugins::Findings::FILE', File.join(dir, 'findings.jsonl'))
34
34
  poc = File.join(dir, 'poc.rb')
35
35
  File.write(poc, 'puts 1')
36
- parent = described_class.record(title: 'xss', poc_artifacts: [poc], severity: 'medium')
36
+ parent = described_class.record(title: 'xss', poc_artifacts: [poc], severity: 'medium', evidence: 'PoC file reproduces reflected XSS in search; see artifact path.')
37
37
  child = described_class.chain(parent_id: parent[:id], title: 'account-takeover', poc_artifacts: [poc], severity: 'high')
38
38
  expect(child[:chain_parent_id]).to eq(parent[:id])
39
39
  expect(child[:composite_severity]).to eq('high')
40
40
  end
41
41
  end
42
+
43
+ it 'flags tampered evidence files' do
44
+ Dir.mktmpdir do |dir|
45
+ allow(Dir).to receive(:home).and_return(dir)
46
+ stub_const('PWN::Plugins::Findings::FILE', File.join(dir, 'findings.jsonl'))
47
+ poc = File.join(dir, 'poc.rb')
48
+ File.write(poc, 'puts 1')
49
+ described_class.record(title: 'xss', poc_artifacts: [poc], engagement_id: 'lab', evidence: 'PoC file reproduces reflected XSS in search; see artifact path.')
50
+ expect(described_class.evidence_verify(engagement_id: 'lab')[:ok]).to eq(true)
51
+ File.write(poc, 'tampered')
52
+ expect(described_class.evidence_verify(engagement_id: 'lab')[:ok]).to eq(false)
53
+ end
54
+ end
42
55
  end
@@ -25,4 +25,14 @@ describe PWN::Plugins::GDB do
25
25
  expect(joined).to include('break *0x401000')
26
26
  end
27
27
  end
28
+
29
+ it 'parses crash_info from gdb output' do
30
+ allow(Open3).to receive(:capture3).and_return(
31
+ ["Program received signal SIGSEGV\nrip 0x401000\n#0 0x401000 in foo ()\n", '', instance_double(Process::Status, exitstatus: 0)]
32
+ )
33
+ allow(PWN::Plugins::PreflightChecker).to receive(:require_bin!)
34
+ row = described_class.crash_info(binary: '/bin/true')
35
+ expect(row[:signal]).to include('SEGV')
36
+ expect(row[:exploitability_heuristic]).to eq('likely-exploitable')
37
+ end
28
38
  end
@@ -12,4 +12,10 @@ describe PWN::Plugins::Packet do
12
12
  help_response = PWN::Plugins::Packet
13
13
  expect(help_response).to respond_to :help
14
14
  end
15
+
16
+ it 'flags connect-scan results as degraded' do
17
+ row = described_class.tcp_connect_scan(hosts: ['127.0.0.1'], ports: [1], timeout: 0.05)
18
+ expect(row[:degraded]).to eq(true)
19
+ expect(row[:results]).to be_an(Array)
20
+ end
15
21
  end
@@ -82,7 +82,7 @@ describe PWN::Plugins::REPL do
82
82
  hits = described_class.pwn_ai_complete(target: '/sk', line: '/sk')
83
83
  expect(hits).to include('/skills')
84
84
  hits = described_class.pwn_ai_complete(target: '/', line: '/')
85
- %w[/cron /skills /sessions /memory /debug /trace /back /help /model].each do |cmd|
85
+ %w[/cron /skills /sessions /memory /debug /trace /back /help /model /learning].each do |cmd|
86
86
  expect(hits).to include(cmd)
87
87
  end
88
88
  hits = described_class.pwn_ai_complete(target: 'li', line: '/cron li')
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'spec_helper'
4
+ require 'tmpdir'
4
5
 
5
6
  describe PWN::Plugins::Vault do
6
7
  it 'should display information for authors' do
@@ -12,4 +13,14 @@ describe PWN::Plugins::Vault do
12
13
  help_response = PWN::Plugins::Vault
13
14
  expect(help_response).to respond_to :help
14
15
  end
16
+
17
+ it 'expands vault tokens and redacts stored secrets' do
18
+ Dir.mktmpdir do |dir|
19
+ allow(Dir).to receive(:home).and_return(dir)
20
+ described_class.store(label: 'api', secret: 's3cret-value')
21
+ expect(described_class.fetch(label: 'api')).to eq('s3cret-value')
22
+ expect(described_class.expand(text: 'tok={{vault:api}}')).to include('s3cret-value')
23
+ expect(described_class.redact(text: 'tok=s3cret-value')).to include('{{vault:api}}')
24
+ end
25
+ end
15
26
  end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'tmpdir'
5
+
6
+ describe PWN::Reports::Engagement do
7
+ it 'should display information for authors' do
8
+ expect(described_class).to respond_to :authors
9
+ end
10
+
11
+ it 'should display information for existing help method' do
12
+ expect(described_class).to respond_to :help
13
+ end
14
+
15
+ it 'writes html for two findings with poc paths' do
16
+ Dir.mktmpdir do |dir|
17
+ poc = File.join(dir, 'poc.rb')
18
+ File.write(poc, 'puts 1')
19
+ out = described_class.generate(
20
+ engagement: 'lab',
21
+ dir_path: dir,
22
+ findings: [
23
+ { title: 'a', severity: 'high', poc: poc },
24
+ { title: 'b', severity: 'low', poc: poc }
25
+ ]
26
+ )
27
+ expect(File.file?(out[:html])).to eq(true)
28
+ end
29
+ end
30
+ end