pwn 0.5.737 → 0.5.739

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 (83) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +0 -1
  3. data/Gemfile +2 -2
  4. data/documentation/Agent-Tool-Registry.md +17 -3
  5. data/documentation/Diagrams.md +1 -1
  6. data/documentation/How-PWN-Works.md +1 -1
  7. data/documentation/Plugin-Degradation.md +74 -0
  8. data/documentation/Swarm.md +2 -3
  9. data/documentation/What-is-PWN.md +1 -1
  10. data/documentation/pwn-REPL.md +0 -1
  11. data/documentation/pwn-ai-Agent.md +1 -1
  12. data/etc/default_skills/browser-exploitation/SKILL.md +10 -0
  13. data/etc/default_skills/firmware-unpack-emulate/SKILL.md +10 -0
  14. data/etc/default_skills/heap-exploitation/SKILL.md +11 -0
  15. data/etc/default_skills/kernel-pwn-triage/SKILL.md +11 -0
  16. data/etc/default_skills/pwn/ai/agent/swarm/SKILL.md +1 -1
  17. data/etc/default_skills/pwn/ai/agent/tools/sbom_scan/SKILL.md +45 -0
  18. data/etc/default_skills/pwn/corpus/SKILL.md +52 -0
  19. data/etc/default_skills/pwn/corpus/references/urls.md +3 -0
  20. data/etc/default_skills/pwn/engagement/SKILL.md +55 -0
  21. data/etc/default_skills/pwn/plugins/debugger/SKILL.md +62 -0
  22. data/etc/default_skills/pwn/plugins/fuzz/SKILL.md +1 -0
  23. data/etc/default_skills/pwn/plugins/handler/SKILL.md +52 -0
  24. data/etc/default_skills/pwn/plugins/jobs/SKILL.md +1 -0
  25. data/etc/default_skills/pwn/plugins/mitm_proxy/SKILL.md +1 -0
  26. data/etc/default_skills/pwn/plugins/nmap_it/SKILL.md +1 -0
  27. data/etc/default_skills/pwn/plugins/packet/SKILL.md +1 -0
  28. data/etc/default_skills/pwn/plugins/process_tube/SKILL.md +2 -0
  29. data/etc/default_skills/pwn/plugins/redb/SKILL.md +52 -0
  30. data/etc/default_skills/pwn/plugins/sbom/SKILL.md +48 -0
  31. data/etc/default_skills/pwn/redaction/SKILL.md +1 -0
  32. data/etc/default_skills/rop-jop-chain-construction/SKILL.md +10 -0
  33. data/etc/default_skills/windows-internals-triage/SKILL.md +10 -0
  34. data/lib/pwn/ai/agent/loop.rb +80 -11
  35. data/lib/pwn/ai/agent/prompt_builder.rb +19 -1
  36. data/lib/pwn/ai/agent/registry.rb +34 -4
  37. data/lib/pwn/ai/agent/swarm.rb +4 -5
  38. data/lib/pwn/ai/agent/tool_guard.rb +11 -4
  39. data/lib/pwn/ai/agent/tools/fuzz_campaign.rb +2 -1
  40. data/lib/pwn/ai/agent/tools/job_run.rb +9 -3
  41. data/lib/pwn/ai/agent/tools/pty_session.rb +7 -1
  42. data/lib/pwn/ai/agent/tools/sbom_scan.rb +25 -0
  43. data/lib/pwn/ai/agent/tools/shell.rb +3 -2
  44. data/lib/pwn/ai/agent/tools/swarm.rb +3 -6
  45. data/lib/pwn/ai/agent/turn_finalizer.rb +2 -2
  46. data/lib/pwn/corpus.rb +107 -0
  47. data/lib/pwn/engagement.rb +158 -0
  48. data/lib/pwn/plugins/debugger.rb +284 -0
  49. data/lib/pwn/plugins/findings.rb +14 -3
  50. data/lib/pwn/plugins/fuzz.rb +62 -0
  51. data/lib/pwn/plugins/handler.rb +171 -0
  52. data/lib/pwn/plugins/jobs.rb +55 -0
  53. data/lib/pwn/plugins/mitm_proxy.rb +37 -2
  54. data/lib/pwn/plugins/nmap_it.rb +32 -0
  55. data/lib/pwn/plugins/packet.rb +18 -0
  56. data/lib/pwn/plugins/preflight_checker.rb +6 -2
  57. data/lib/pwn/plugins/process_tube.rb +99 -8
  58. data/lib/pwn/plugins/redb.rb +210 -0
  59. data/lib/pwn/plugins/repl.rb +0 -3
  60. data/lib/pwn/plugins/sbom.rb +145 -0
  61. data/lib/pwn/plugins.rb +4 -0
  62. data/lib/pwn/redaction.rb +53 -0
  63. data/lib/pwn/reports/engagement.rb +2 -1
  64. data/lib/pwn/sessions.rb +3 -1
  65. data/lib/pwn/version.rb +1 -1
  66. data/lib/pwn.rb +2 -0
  67. data/packer/provisioners/irc.sh +1 -1
  68. data/spec/lib/pwn/ai/agent/harness_2026_09_15_spec.rb +144 -0
  69. data/spec/lib/pwn/ai/agent/registry_spec.rb +50 -0
  70. data/spec/lib/pwn/ai/agent/tools/mcp_spec.rb +31 -0
  71. data/spec/lib/pwn/ai/agent/tools/sbom_scan_spec.rb +10 -0
  72. data/spec/lib/pwn/corpus_spec.rb +23 -0
  73. data/spec/lib/pwn/engagement_spec.rb +27 -0
  74. data/spec/lib/pwn/plugins/debugger_spec.rb +27 -0
  75. data/spec/lib/pwn/plugins/handler_spec.rb +25 -0
  76. data/spec/lib/pwn/plugins/redb_spec.rb +33 -0
  77. data/spec/lib/pwn/plugins/repl_spec.rb +7 -1
  78. data/spec/lib/pwn/plugins/sbom_spec.rb +22 -0
  79. data/third_party/pwn_rdoc.jsonl +97 -4
  80. metadata +36 -9
  81. data/etc/default_skills/pwn/plugins/repl/irc/SKILL.md +0 -47
  82. data/lib/pwn/plugins/repl/irc.rb +0 -153
  83. data/spec/lib/pwn/plugins/repl/irc_spec.rb +0 -21
@@ -47,6 +47,56 @@ describe PWN::AI::Agent::Registry do
47
47
  expect(names.length).to eq(described_class::CORE_TOOLS.length)
48
48
  end
49
49
 
50
+ it 'advertises the MCP broker for a named backend in the normal core-only prompt path' do
51
+ described_class.discover
52
+ request = 'Use combo.nation to inspect the menu catalog'
53
+ enabled = PWN::AI::Agent::Loop.send(:default_interactive_toolsets, request: request)
54
+ expect(PWN::AI::MCP::ComboNation).not_to receive(:connect)
55
+ definitions = described_class.definitions(core_only: true, relevance: request, enabled: enabled)
56
+ tool = definitions.find { |row| row.dig(:function, :name) == 'mcp' }
57
+ expect(tool).not_to be_nil
58
+ expect(tool.dig(:function, :description)).to include('combo_nation', 'PWN::AI::MCP::ComboNation', 'menu_catalog')
59
+ end
60
+
61
+ it 'routes MCP names and capabilities without relying on keyword rank or changing unrelated core prompts' do
62
+ described_class.discover
63
+ allow(described_class).to receive(:router_enabled?).and_return(true)
64
+ ['PWN::AI::MCP::ComboNation', 'combo_nation', 'ComboNation', 'combo.nation', 'menu_dial_calibration', 'MCP backends'].each do |query|
65
+ [true, false].each do |core_only|
66
+ names = described_class.definitions(core_only: core_only, relevance: query, top_k: 0).map { |row| row.dig(:function, :name) }
67
+ expect(names).to include('mcp')
68
+ end
69
+ end
70
+ names = described_class.definitions(core_only: true, relevance: 'inspect a local Ruby file').map { |row| row.dig(:function, :name) }
71
+ expect(names).to eq(described_class::CORE_TOOLS)
72
+ end
73
+
74
+ it 'discovers another MCP client from its metadata and respects explicit toolset exclusion' do
75
+ described_class.discover
76
+ client = Module.new
77
+ %i[connect disconnect list_tools call_tool].each { |method| client.define_singleton_method(method) { |_| {} } }
78
+ client.const_set(:TOOLS, %w[inspect_widget])
79
+ stub_const('PWN::AI::MCP::WidgetLab', client)
80
+ expect(client).not_to receive(:connect)
81
+ %w[widget_lab inspect_widget].each do |query|
82
+ tools = described_class.definitions(core_only: true, relevance: query)
83
+ mcp = tools.find { |row| row.dig(:function, :name) == 'mcp' }
84
+ expect(mcp.dig(:function, :description)).to include('widget_lab', 'inspect_widget')
85
+ expect(described_class.definitions(core_only: true, relevance: query, enabled: [])).to eq([])
86
+ names = described_class.definitions(core_only: true, relevance: query, enabled: ['pwn']).map { |row| row.dig(:function, :name) }
87
+ expect(names).not_to include('mcp')
88
+ end
89
+ expect(described_class.lookup(name: 'mcp').schema[:description]).not_to include('WidgetLab')
90
+ end
91
+
92
+ it 'does not let MCP relevance bypass a failed availability check' do
93
+ described_class.discover
94
+ entry = described_class.lookup(name: 'mcp')
95
+ allow(entry.check).to receive(:call).and_return(false)
96
+ tools = described_class.definitions(core_only: true, relevance: 'combo.nation')
97
+ expect(tools.map { |row| row.dig(:function, :name) }).not_to include('mcp')
98
+ end
99
+
50
100
  it 'applies observed shell and Ruby prerequisites to the existing core tools' do
51
101
  described_class.discover
52
102
  context = { environment: :local, capabilities: { shell: false, ruby: false } }
@@ -51,6 +51,37 @@ describe 'PWN::AI::Agent::Tools mcp' do
51
51
  expect(PWN::AI::Agent::Registry.lookup(name: 'mcp').toolset).to eq('mcp')
52
52
  end
53
53
 
54
+ it 'dispatches the broker advertised for a combo.nation prompt and preserves menu IDs across calls' do
55
+ registry = PWN::AI::Agent::Registry
56
+ tools = registry.definitions(core_only: true, relevance: 'Inspect combo.nation menus')
57
+ schema = tools.find { |tool| tool.dig(:function, :name) == 'mcp' }
58
+ expect(schema).not_to be_nil
59
+ fake = start_fake_server
60
+ # Only replace process spawning; exercise the real protocol client, broker,
61
+ # schema validation and Dispatch with JSON arguments as returned by a model.
62
+ allow(PWN::AI::MCP::ComboNation).to receive(:connect).and_wrap_original do |method, opts|
63
+ expect(opts[:allow_hardware]).not_to eq(true)
64
+ method.call(opts.merge(reader: fake[:reader], writer: fake[:writer], timeout: 2))
65
+ end
66
+ calls = [
67
+ { action: 'list_tools', backend: 'combo_nation' },
68
+ { action: 'call_tool', backend: 'combo_nation', name: 'menu_guess', arguments: { option: '5.10' } }
69
+ ]
70
+ results = calls.map do |args|
71
+ JSON.parse(PWN::AI::Agent::Dispatch.call(
72
+ scope_policy: { enabled: false },
73
+ tool_call: { id: 'mcp-test', type: 'function', function: { name: schema.dig(:function, :name), arguments: JSON.generate(args) } }
74
+ ))
75
+ end
76
+ expect(results.first.dig('result', 'tools').map { |tool| tool['name'] }).to include('menu_catalog')
77
+ expect(results.last.dig('result', 'parsed', 'arguments')).to eq('option' => '5.10')
78
+ expect(results.last.dig('result', 'allow_hardware')).to eq(false)
79
+ expect(PWN::AI::MCP::ComboNation).to have_received(:connect).once
80
+ ensure
81
+ PWN::AI::MCP.reset!
82
+ fake[:thread].join(2) if fake
83
+ end
84
+
54
85
  it 'lists MCP backends and drives combo.nation tools through Dispatch-shaped args' do
55
86
  entry = PWN::AI::Agent::Registry.lookup(name: 'mcp')
56
87
  listed = entry.handler.call(action: 'backends')
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe 'PWN::AI::Agent::Tools sbom_scan' do
6
+ it 'registers sbom_scan' do
7
+ load '/opt/pwn/lib/pwn/ai/agent/tools/sbom_scan.rb'
8
+ expect(PWN::AI::Agent::Registry.lookup(name: 'sbom_scan')).not_to be_nil
9
+ end
10
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'fileutils'
5
+
6
+ describe PWN::Corpus 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 'resolves a named builtin corpus from cache' do
16
+ Dir.mktmpdir('corpus') do |dir|
17
+ stub_const('PWN::Corpus::ROOT', dir)
18
+ path = described_class.get(name: :fuzz_short, offline: true)
19
+ expect(File.read(path)).to include('../')
20
+ expect(described_class.get(name: :fuzz_short, offline: true)).to eq(path)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'fileutils'
5
+
6
+ describe PWN::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 'persists host state and requires override for out-of-scope targets' do
16
+ Dir.mktmpdir('eng') do |dir|
17
+ stub_const('PWN::AI::Agent::Engagement::ROOT', dir)
18
+ stub_const('PWN::AI::Agent::Engagement::ACTIVE_FILE', File.join(dir, 'active'))
19
+ described_class.open(name: 'lab', scope_cidrs: ['10.0.0.0/8'])
20
+ described_class.record_host(host: '10.1.2.3', ports: [22], notes: ['ssh'])
21
+ expect(described_class.hosts.values.first[:ports]).to include(22)
22
+ expect(described_class.warn_unless_in_scope(host: '8.8.8.8')[:override_required]).to eq(true)
23
+ described_class.record_host(host: '8.8.8.8', ports: [53], override: true)
24
+ expect(described_class.hosts.values.map { |row| row[:host] }).to include('8.8.8.8')
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'tmpdir'
5
+
6
+ describe PWN::Plugins::Debugger 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 'recovers a cyclic offset without parsing gdb text' do
16
+ pattern = described_class.cyclic(length: 200)
17
+ slice = pattern[80, 4]
18
+ expect(described_class.cyclic_find(value: slice, length: 200)).to eq(80)
19
+ expect(described_class.to_pwntools_offsets(value: slice, length: 200)[:offset]).to eq(80)
20
+ end
21
+
22
+ it 'parses MI register values into a hash' do
23
+ row = described_class.send(:parse_mi, raw: '^done,register-values=[{number="16",name="rip",value="0x41414141"}]')
24
+ expect(row[:ok]).to eq(true)
25
+ expect(row[:registers]['rip']).to eq('0x41414141')
26
+ end
27
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'socket'
5
+
6
+ describe PWN::Plugins::Handler 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 'generates a reverse shell payload without Metasploit' do
16
+ row = described_class.generate(kind: 'rev_sh', host: '127.0.0.1', port: 4444)
17
+ expect(row[:payload]).to include('127.0.0.1')
18
+ expect(row[:payload]).to include('4444')
19
+ end
20
+
21
+ it 'generates bind python and powershell payloads' do
22
+ expect(described_class.generate(kind: 'bind_python', port: 5555)[:payload]).to include('5555')
23
+ expect(described_class.generate(kind: 'bind_powershell', port: 5555)[:payload]).to include('GetStream')
24
+ end
25
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'tmpdir'
5
+ require 'fileutils'
6
+
7
+ describe PWN::Plugins::REDB do
8
+ it 'should display information for authors' do
9
+ expect(described_class).to respond_to :authors
10
+ end
11
+
12
+ it 'should display information for existing help method' do
13
+ expect(described_class).to respond_to :help
14
+ end
15
+
16
+ it 'answers who calls strcpy from cache on a second open' do
17
+ Dir.mktmpdir('redb') do |dir|
18
+ stub_const('PWN::Plugins::REDB::ROOT', File.join(dir, 'redb'))
19
+ src = File.join(dir, 'x.c')
20
+ bin = File.join(dir, 'x')
21
+ File.write(src, "#include <string.h>\nvoid caller(void){ char b[8]; strcpy(b, \"x\"); }\nint main(void){ caller(); return 0; }\n")
22
+ system('cc', '-O0', '-g', '-fno-builtin', '-fno-inline', '-o', bin, src)
23
+ described_class.open(bin: bin)
24
+ t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
25
+ described_class.open(bin: bin)
26
+ xrefs = described_class.xrefs_to(bin: bin, sym: 'strcpy')
27
+ elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0
28
+ expect(elapsed).to be < 1.0
29
+ expect(xrefs).to be_an(Array)
30
+ expect(xrefs.map { |row| row['dst'] || row[:dst] }).to include('strcpy')
31
+ end
32
+ end
33
+ end
@@ -8,12 +8,18 @@ describe PWN::Plugins::REPL do # rubocop:disable Metrics/BlockLength
8
8
  described_class,
9
9
  described_class::ASM,
10
10
  described_class::AI,
11
- described_class::IRC,
12
11
  described_class::Mesh,
13
12
  described_class::Vault
14
13
  ].map { |mod| File.read(mod.method(:add_commands).source_location.first) }.uniq.join("\n")
15
14
  end
16
15
 
16
+ it 'does not register pwn-irc or define REPL::IRC' do
17
+ described_class.add_commands
18
+ expect(Pry::Commands.find_command('pwn-irc')).to be_nil
19
+ expect(defined?(PWN::Plugins::REPL::IRC)).to be_nil
20
+ expect(PWN::Plugins::IRC).to be_a(Module)
21
+ end
22
+
17
23
  describe 'ai.memory pinned engagement block' do
18
24
  it 'handles dotted commands locally in AI mode and registers the Pry command' do
19
25
  described_class.add_commands
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe PWN::Plugins::SBOM 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 'normalizes grype JSON into package/cve rows' do
15
+ json = { 'matches' => [{ 'artifact' => { 'name' => 'openssl', 'version' => '1.1.1' },
16
+ 'vulnerability' => { 'id' => 'CVE-2024-0001', 'severity' => 'HIGH', 'fix' => { 'versions' => ['1.1.1w'] } } }] }
17
+ allow(Open3).to receive(:capture2).and_return([JSON.generate(json), instance_double(Process::Status, success?: true)])
18
+ allow(PWN::Plugins::PreflightChecker).to receive(:bin?).and_return(true)
19
+ out = described_class.scan(path_or_image: '/tmp/lock', engine: 'grype')
20
+ expect(out[:vulns].first).to include(package: 'openssl', cve: 'CVE-2024-0001', severity: 'high', fix_version: '1.1.1w')
21
+ end
22
+ end