pwn 0.5.739 → 0.5.741
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/Gemfile +2 -2
- data/documentation/Agent-Tool-Registry.md +163 -6
- data/documentation/Diagrams.md +1 -1
- data/documentation/Home.md +1 -1
- data/documentation/How-PWN-Works.md +1 -1
- data/documentation/Plugin-Degradation.md +6 -2
- data/documentation/Recon-Findings-API.md +33 -1
- data/documentation/What-is-PWN.md +1 -1
- data/documentation/pwn-ai-Agent.md +1 -1
- data/etc/default_skills/pwn/ai/agent/confirmation/SKILL.md +49 -0
- data/etc/default_skills/pwn/ai/agent/result/SKILL.md +4 -1
- data/etc/default_skills/pwn/ai/agent/task_dag/SKILL.md +50 -0
- data/etc/default_skills/pwn/ai/agent/tool_guard/SKILL.md +2 -0
- data/etc/default_skills/pwn/ai/agent/tools/browser_goto/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/tools/loot_query/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/tools/nmap_scan/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/tools/nuclei_scan/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/tools/radare2/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/turn_finalizer/SKILL.md +3 -0
- data/etc/default_skills/pwn/ai/router/SKILL.md +51 -0
- data/etc/default_skills/pwn/engagement/SKILL.md +2 -0
- data/etc/default_skills/pwn/ffi/capstone/SKILL.md +54 -0
- data/etc/default_skills/pwn/ffi/keystone/SKILL.md +50 -0
- data/etc/default_skills/pwn/plugins/aflplusplus/SKILL.md +5 -1
- data/etc/default_skills/pwn/plugins/ai_sandbox/SKILL.md +52 -0
- data/etc/default_skills/pwn/plugins/artifact_registry/SKILL.md +3 -0
- data/etc/default_skills/pwn/plugins/assembly/SKILL.md +2 -0
- data/etc/default_skills/pwn/plugins/binary_parser/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/exploit_dev/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/findings/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/gdbmi/SKILL.md +8 -2
- data/etc/default_skills/pwn/plugins/httpx/SKILL.md +49 -0
- data/etc/default_skills/pwn/plugins/jobs/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/method_catalog/SKILL.md +50 -0
- data/etc/default_skills/pwn/plugins/nmap_it/SKILL.md +2 -0
- data/etc/default_skills/pwn/plugins/nuclei/SKILL.md +2 -1
- data/etc/default_skills/pwn/plugins/process_tube/SKILL.md +2 -0
- data/etc/default_skills/pwn/plugins/radare2/SKILL.md +3 -0
- data/etc/default_skills/pwn/plugins/recon/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/rop/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/transparent_browser/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/vault/SKILL.md +3 -0
- data/etc/default_skills/pwn/reports/SKILL.md +2 -0
- data/lib/pwn/ai/agent/confirmation.rb +163 -0
- data/lib/pwn/ai/agent/dispatch.rb +43 -5
- data/lib/pwn/ai/agent/loop.rb +45 -11
- data/lib/pwn/ai/agent/prompt_builder.rb +7 -4
- data/lib/pwn/ai/agent/registry.rb +2 -1
- data/lib/pwn/ai/agent/result.rb +76 -6
- data/lib/pwn/ai/agent/task_dag.rb +220 -0
- data/lib/pwn/ai/agent/tool_guard.rb +166 -24
- data/lib/pwn/ai/agent/tools/artifacts.rb +35 -5
- data/lib/pwn/ai/agent/tools/binary_triage.rb +7 -3
- data/lib/pwn/ai/agent/tools/browser_goto.rb +32 -0
- data/lib/pwn/ai/agent/tools/debug_lane.rb +23 -0
- data/lib/pwn/ai/agent/tools/exploitdev.rb +32 -12
- data/lib/pwn/ai/agent/tools/finding_record.rb +31 -5
- data/lib/pwn/ai/agent/tools/fuzz_campaign.rb +44 -21
- data/lib/pwn/ai/agent/tools/job_run.rb +30 -18
- data/lib/pwn/ai/agent/tools/loot_query.rb +44 -0
- data/lib/pwn/ai/agent/tools/nmap_scan.rb +32 -0
- data/lib/pwn/ai/agent/tools/nuclei_scan.rb +31 -0
- data/lib/pwn/ai/agent/tools/radare2.rb +55 -0
- data/lib/pwn/ai/agent/tools/ruby_eval.rb +19 -2
- data/lib/pwn/ai/agent/tools/shell.rb +22 -10
- data/lib/pwn/ai/agent/turn_finalizer.rb +46 -5
- data/lib/pwn/ai/agent.rb +3 -1
- data/lib/pwn/ai/cli.rb +26 -2
- data/lib/pwn/ai/context.rb +4 -2
- data/lib/pwn/ai/router.rb +196 -0
- data/lib/pwn/ai/tools/README.md +5 -0
- data/lib/pwn/ai.rb +1 -0
- data/lib/pwn/config.rb +10 -0
- data/lib/pwn/engagement.rb +45 -1
- data/lib/pwn/ffi/capstone.rb +120 -0
- data/lib/pwn/ffi/keystone.rb +107 -0
- data/lib/pwn/ffi.rb +2 -0
- data/lib/pwn/migrate.rb +5 -1
- data/lib/pwn/plugins/aflplusplus.rb +238 -13
- data/lib/pwn/plugins/ai_sandbox.rb +240 -0
- data/lib/pwn/plugins/artifact_registry.rb +188 -22
- data/lib/pwn/plugins/assembly.rb +73 -2
- data/lib/pwn/plugins/basic_auth.rb +17 -4
- data/lib/pwn/plugins/binary_parser.rb +439 -38
- data/lib/pwn/plugins/exploit_dev.rb +83 -14
- data/lib/pwn/plugins/findings.rb +208 -42
- data/lib/pwn/plugins/gdbmi.rb +265 -38
- data/lib/pwn/plugins/httpx.rb +93 -0
- data/lib/pwn/plugins/jobs.rb +260 -102
- data/lib/pwn/plugins/method_catalog.rb +328 -0
- data/lib/pwn/plugins/nmap_it.rb +157 -26
- data/lib/pwn/plugins/nuclei.rb +122 -29
- data/lib/pwn/plugins/preflight_checker.rb +3 -2
- data/lib/pwn/plugins/process_tube.rb +51 -2
- data/lib/pwn/plugins/radare2.rb +120 -26
- data/lib/pwn/plugins/recon.rb +33 -0
- data/lib/pwn/plugins/repl/mesh.rb +203 -13
- data/lib/pwn/plugins/rop.rb +121 -31
- data/lib/pwn/plugins/transparent_browser.rb +212 -14
- data/lib/pwn/plugins/vault.rb +182 -24
- data/lib/pwn/plugins.rb +4 -0
- data/lib/pwn/reports/engagement.rb +8 -2
- data/lib/pwn/reports/html.rb +28 -9
- data/lib/pwn/reports/json.rb +2 -1
- data/lib/pwn/reports/markdown.rb +49 -12
- data/lib/pwn/reports/sarif.rb +17 -3
- data/lib/pwn/reports.rb +184 -34
- data/lib/pwn/version.rb +1 -1
- data/spec/integration/reports_spec.rb +93 -0
- data/spec/integration/result_condition_spec.rb +11 -8
- data/spec/lib/pwn/ai/agent/confirmation_spec.rb +59 -0
- data/spec/lib/pwn/ai/agent/dispatch_confirmation_spec.rb +52 -0
- data/spec/lib/pwn/ai/agent/dispatch_policy_spec.rb +51 -0
- data/spec/lib/pwn/ai/agent/loop_spec.rb +86 -0
- data/spec/lib/pwn/ai/agent/registry_spec.rb +1 -1
- data/spec/lib/pwn/ai/agent/result_spec.rb +84 -1
- data/spec/lib/pwn/ai/agent/task_dag_spec.rb +63 -0
- data/spec/lib/pwn/ai/agent/tool_guard_spec.rb +48 -0
- data/spec/lib/pwn/ai/agent/tools/ai_sandbox_spec.rb +36 -0
- data/spec/lib/pwn/ai/agent/tools/binary_triage_spec.rb +21 -0
- data/spec/lib/pwn/ai/agent/tools/browser_goto_spec.rb +57 -0
- data/spec/lib/pwn/ai/agent/tools/exploitdev_spec.rb +42 -0
- data/spec/lib/pwn/ai/agent/tools/finding_record_spec.rb +46 -2
- data/spec/lib/pwn/ai/agent/tools/fuzz_campaign_spec.rb +46 -0
- data/spec/lib/pwn/ai/agent/tools/job_run_spec.rb +52 -0
- data/spec/lib/pwn/ai/agent/tools/loot_query_spec.rb +46 -0
- data/spec/lib/pwn/ai/agent/tools/nmap_scan_spec.rb +26 -0
- data/spec/lib/pwn/ai/agent/tools/nuclei_scan_spec.rb +37 -0
- data/spec/lib/pwn/ai/agent/tools/pwn_eval_schema_spec.rb +27 -0
- data/spec/lib/pwn/ai/agent/tools/radare2_spec.rb +15 -0
- data/spec/lib/pwn/ai/agent/turn_finalizer_spec.rb +76 -0
- data/spec/lib/pwn/ai/cli_spec.rb +12 -1
- data/spec/lib/pwn/ai/router_spec.rb +57 -0
- data/spec/lib/pwn/ffi/capstone_spec.rb +33 -0
- data/spec/lib/pwn/ffi/keystone_spec.rb +33 -0
- data/spec/lib/pwn/migrate_spec.rb +30 -2
- data/spec/lib/pwn/plugins/aflplusplus_spec.rb +40 -0
- data/spec/lib/pwn/plugins/ai_sandbox_spec.rb +44 -0
- data/spec/lib/pwn/plugins/artifact_registry_spec.rb +116 -1
- data/spec/lib/pwn/plugins/assembly_spec.rb +12 -0
- data/spec/lib/pwn/plugins/basic_auth_spec.rb +15 -0
- data/spec/lib/pwn/plugins/binary_parser_spec.rb +59 -2
- data/spec/lib/pwn/plugins/exploit_dev_spec.rb +44 -0
- data/spec/lib/pwn/plugins/findings_structured_spec.rb +196 -1
- data/spec/lib/pwn/plugins/gdbmi_spec.rb +99 -1
- data/spec/lib/pwn/plugins/httpx_spec.rb +23 -0
- data/spec/lib/pwn/plugins/jobs_spec.rb +141 -0
- data/spec/lib/pwn/plugins/method_catalog_spec.rb +29 -0
- data/spec/lib/pwn/plugins/nmap_it_spec.rb +49 -0
- data/spec/lib/pwn/plugins/nuclei_spec.rb +33 -1
- data/spec/lib/pwn/plugins/radare2_spec.rb +115 -0
- data/spec/lib/pwn/plugins/recon_spec.rb +15 -0
- data/spec/lib/pwn/plugins/repl_mesh_channel_route_spec.rb +107 -1
- data/spec/lib/pwn/plugins/repl_mesh_history_spec.rb +122 -0
- data/spec/lib/pwn/plugins/rop_spec.rb +19 -0
- data/spec/lib/pwn/plugins/transparent_browser_spec.rb +71 -0
- data/spec/lib/pwn/plugins/vault_spec.rb +41 -0
- data/spec/lib/pwn/reports/engagement_spec.rb +51 -0
- data/spec/lib/pwn/reports_spec.rb +196 -0
- data/spec/spec_helper.rb +3 -0
- data/third_party/pwn_rdoc.jsonl +230 -4
- metadata +47 -5
|
@@ -1,8 +1,60 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'spec_helper'
|
|
4
|
+
require 'tmpdir'
|
|
5
|
+
require 'json'
|
|
6
|
+
require 'open3'
|
|
4
7
|
|
|
5
8
|
describe PWN::Plugins::GDBMI do
|
|
9
|
+
def with_fake_gdb
|
|
10
|
+
Dir.mktmpdir('pwn-gdbmi-') do |dir|
|
|
11
|
+
fake = File.join(dir, 'gdb')
|
|
12
|
+
File.write(fake, <<~RUBY)
|
|
13
|
+
#!/usr/bin/env ruby
|
|
14
|
+
$stdout.sync = true
|
|
15
|
+
$stdin.binmode
|
|
16
|
+
$stdout.puts '=thread-group-added,id="i1"'
|
|
17
|
+
$stdout.puts '(gdb) '
|
|
18
|
+
loop do
|
|
19
|
+
line = $stdin.gets
|
|
20
|
+
break if line.nil?
|
|
21
|
+
|
|
22
|
+
cmd = line.to_s.strip
|
|
23
|
+
break if cmd == '-gdb-exit' || cmd == 'quit'
|
|
24
|
+
|
|
25
|
+
case cmd
|
|
26
|
+
when /^-break-insert /
|
|
27
|
+
$stdout.puts '^done,bkpt={number="1",type="breakpoint",disp="keep",enabled="y",addr="0x401000",func="main"}'
|
|
28
|
+
when /^-exec-run/, /^-exec-continue/, /^-interpreter-exec console/
|
|
29
|
+
$stdout.puts '^running'
|
|
30
|
+
$stdout.puts '*stopped,reason="signal-received",signal-name="SIGSEGV",frame={addr="0x41414141",func="vuln",offset="0"}'
|
|
31
|
+
when /^-data-list-register-values/
|
|
32
|
+
$stdout.puts '^done,register-values=[{number="16",name="rip",value="0x41414141"},{number="7",name="rsp",value="0x7fffffffe000"}]'
|
|
33
|
+
when /^-data-read-memory-bytes/
|
|
34
|
+
$stdout.puts '^done,memory=[{begin="0x41414141",offset="0",end="0x41414145",contents="41414141"}]'
|
|
35
|
+
when /^-stack-list-frames/
|
|
36
|
+
$stdout.puts '^done,stack=[frame={level="0",addr="0x41414141",func="vuln"},frame={level="1",addr="0x401200",func="main"}]'
|
|
37
|
+
when /si_addr/
|
|
38
|
+
$stdout.puts '^done,value="0x41414141"'
|
|
39
|
+
else
|
|
40
|
+
$stdout.puts '^done'
|
|
41
|
+
end
|
|
42
|
+
$stdout.puts '(gdb) '
|
|
43
|
+
end
|
|
44
|
+
RUBY
|
|
45
|
+
File.chmod(0o755, fake)
|
|
46
|
+
path = File.join(dir, 'fixture.bin')
|
|
47
|
+
File.binwrite(path, 'ELF')
|
|
48
|
+
original = ENV.fetch('PATH', '')
|
|
49
|
+
ENV['PATH'] = "#{dir}:#{original}"
|
|
50
|
+
begin
|
|
51
|
+
yield path
|
|
52
|
+
ensure
|
|
53
|
+
ENV['PATH'] = original
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
6
58
|
it 'should display information for authors' do
|
|
7
59
|
expect(described_class).to respond_to :authors
|
|
8
60
|
end
|
|
@@ -12,6 +64,52 @@ describe PWN::Plugins::GDBMI do
|
|
|
12
64
|
end
|
|
13
65
|
|
|
14
66
|
it 'break requires a location' do
|
|
15
|
-
expect { described_class.break(location: '') }.to raise_error(/location/)
|
|
67
|
+
expect { described_class.break(location: '') }.to raise_error(/location|addr/)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it 'is available as GDBMi and speaks mi3' do
|
|
71
|
+
expect(PWN::Plugins::GDBMi).to eq(described_class)
|
|
72
|
+
with_fake_gdb do |path|
|
|
73
|
+
sid = described_class.open(binary: path)
|
|
74
|
+
expect(described_class.session(session: sid)[:interpreter]).to eq('mi3')
|
|
75
|
+
described_class.close(session: sid)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it 'rejects injected breakpoint locations before spawning gdb' do
|
|
80
|
+
expect(Open3).not_to receive(:popen2)
|
|
81
|
+
expect { described_class.break(session: 'missing', location: 'main;!id') }.to raise_error(ArgumentError, /location|addr/)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it 'returns a structured crash report from run_to_crash' do
|
|
85
|
+
with_fake_gdb do |path|
|
|
86
|
+
crash = described_class.run_to_crash(binary: path, stdin: 'AAAA')
|
|
87
|
+
expect(crash).to include(signal: 'SIGSEGV', pc: '0x41414141', fault_addr: '0x41414141')
|
|
88
|
+
expect(crash[:exploitability]).to eq('pc_control')
|
|
89
|
+
expect(crash[:backtrace]).to be_an(Array)
|
|
90
|
+
expect(crash[:registers]).to include('rip' => '0x41414141')
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it 'feeds ExploitDev a cyclic offset from the crash PC' do
|
|
95
|
+
payload = PWN::Plugins::ExploitDev.cyclic(length: 64)
|
|
96
|
+
crash = { signal: 'SIGSEGV', pc: '0x41414141', fault_addr: '0x41414141', exploitability: 'pc_control' }
|
|
97
|
+
row = PWN::Plugins::ExploitDev.from_crash(crash: crash, payload: payload)
|
|
98
|
+
expect(row[:offset]).to be_a(Integer)
|
|
99
|
+
expect(payload[row[:offset], 4]).to eq('AAAA')
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
it 'triages a compiled crashing binary to a structured report', :gdb_integration do
|
|
103
|
+
Dir.mktmpdir('pwn-gdb-real-') do |dir|
|
|
104
|
+
source = File.join(dir, 'crash.c')
|
|
105
|
+
path = File.join(dir, 'crash')
|
|
106
|
+
File.write(source, "int main(void) { *(volatile int *)0 = 1; return 0; }\n")
|
|
107
|
+
out, status = Open3.capture2e('cc', '-O0', '-g', '-fno-stack-protector', '-o', path, source)
|
|
108
|
+
expect(status.success?).to eq(true), out
|
|
109
|
+
crash = described_class.run_to_crash(binary: path)
|
|
110
|
+
expect(crash[:signal].to_s).to match(/SEGV/)
|
|
111
|
+
expect(crash[:pc].to_s).to match(/\A0x[0-9a-f]+\z/)
|
|
112
|
+
expect(crash).to include(:fault_addr, :exploitability)
|
|
113
|
+
end
|
|
16
114
|
end
|
|
17
115
|
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'tmpdir'
|
|
6
|
+
|
|
7
|
+
describe PWN::Plugins::Httpx 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 'parses httpx JSONL tech stack without a live probe' do
|
|
17
|
+
jsonl = "#{JSON.generate('url' => 'https://wp.example.test', 'status_code' => 200, 'title' => 'WordPress', 'tech' => %w[nginx PHP WordPress], 'webserver' => 'nginx')}\n"
|
|
18
|
+
row = described_class.probe(jsonl: jsonl)
|
|
19
|
+
expect(row[:hosts].first['url']).to include('wp.example.test')
|
|
20
|
+
expect(row[:techs].join).to match(/wordpress/i)
|
|
21
|
+
expect(row[:techs].join).to match(/nginx/i)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -3,6 +3,147 @@
|
|
|
3
3
|
require 'spec_helper'
|
|
4
4
|
|
|
5
5
|
describe PWN::Plugins::Jobs do
|
|
6
|
+
before do
|
|
7
|
+
@root = Dir.mktmpdir('pwn-jobs-')
|
|
8
|
+
stub_const('PWN::Plugins::Jobs::JOBS_DIR', @root)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
after do
|
|
12
|
+
FileUtils.remove_entry(@root)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def fresh(code)
|
|
16
|
+
setup = "require 'pwn'; PWN::Plugins::Jobs.send(:remove_const, :JOBS_DIR); PWN::Plugins::Jobs.const_set(:JOBS_DIR, #{described_class::JOBS_DIR.inspect}); "
|
|
17
|
+
out, err, status = Open3.capture3(RbConfig.ruby, '-I', File.expand_path('../../../../lib', __dir__), '-e', setup + code)
|
|
18
|
+
raise err unless status.success?
|
|
19
|
+
|
|
20
|
+
JSON.parse(out, symbolize_names: true)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def finished(id)
|
|
24
|
+
Timeout.timeout(8) do
|
|
25
|
+
loop do
|
|
26
|
+
row = described_class.status(id: id)
|
|
27
|
+
return row unless row[:status] == 'RUNNING'
|
|
28
|
+
|
|
29
|
+
sleep 0.05
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it 'survives an exited creator and records completion in a fresh session' do
|
|
35
|
+
row = fresh("puts JSON.generate(PWN::Plugins::Jobs.start(command: 'sleep 2; printf durable', max_runtime: 21600, session_id: 'old'))")
|
|
36
|
+
expect(row[:status]).to eq('RUNNING')
|
|
37
|
+
expect(row[:worker_pid]).to be_a(Integer)
|
|
38
|
+
expect(row[:worker_identity]).not_to be_nil
|
|
39
|
+
worker_session = Process.getsid(row[:worker_pid])
|
|
40
|
+
expect(fresh("puts JSON.generate(PWN::Plugins::Jobs.status(id: '#{row[:id]}'))")[:status]).to eq('RUNNING')
|
|
41
|
+
done = finished(row[:id])
|
|
42
|
+
expect(worker_session).to eq(row[:worker_pid])
|
|
43
|
+
expect(done).to include(status: 'COMPLETED', exit_code: 0, max_runtime: 21_600, session_id: 'old')
|
|
44
|
+
expect(done[:finished_at]).not_to be_nil
|
|
45
|
+
expect(fresh("puts JSON.generate(PWN::Plugins::Jobs.result(id: '#{row[:id]}'))")[:tail]).to eq('durable')
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it 'enforces timeout without polling and kills a TERM-resistant process tree' do
|
|
49
|
+
marker = File.join(@root, 'escaped')
|
|
50
|
+
row = described_class.start(command: "trap '' TERM; (sleep 2; touch #{marker}) & wait", max_runtime: 0.2)
|
|
51
|
+
sleep 2.5
|
|
52
|
+
persisted = JSON.parse(File.read(File.join(@root, "#{row[:id]}.json")), symbolize_names: true)
|
|
53
|
+
expect(persisted).to include(status: 'TIMEOUT', signal: 9)
|
|
54
|
+
expect(File.exist?(marker)).to be(false)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it 'cancels durably without directly signaling a persisted PID' do
|
|
58
|
+
row = described_class.start(command: "trap '' TERM; sleep 10", max_runtime: 10)
|
|
59
|
+
expect(described_class.stop(handle: row[:id])).to be(true)
|
|
60
|
+
expect(File.exist?(File.join(@root, "#{row[:id]}.cancel"))).to be(true)
|
|
61
|
+
expect(finished(row[:id])).to include(status: 'STOPPED', signal: 9)
|
|
62
|
+
expect(described_class.stop(id: row[:id])).to be(false)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it 'pages binary logs with byte offsets and distinguishes EOF from completion' do
|
|
66
|
+
row = described_class.start(command: "printf 'abc\\377def'; sleep 1", max_runtime: 5)
|
|
67
|
+
first = described_class.job_tail(handle: row[:id], offset: 0, length: 4)
|
|
68
|
+
expect(first).to include(bytes: 4, next_offset: 4, encoding: 'base64', eof: false, status: 'RUNNING')
|
|
69
|
+
expect(Base64.strict_decode64(first[:data])).to eq("abc\xFF".b)
|
|
70
|
+
last = described_class.tail(id: row[:id], offset: 4, length: 100)
|
|
71
|
+
expect(last).to include(body: 'def', data: 'def', bytes: 3, next_offset: 7, eof: true, status: 'RUNNING')
|
|
72
|
+
finished(row[:id])
|
|
73
|
+
expect(described_class.tail(id: row[:id], offset: 7)).to include(bytes: 0, eof: true, status: 'COMPLETED')
|
|
74
|
+
expect(described_class.tail(id: row[:id])).to be_a(String)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it 'filters and limits durable jobs across sessions' do
|
|
78
|
+
first = described_class.start(command: 'true', session_id: 'one')
|
|
79
|
+
finished(first[:id])
|
|
80
|
+
second = described_class.start(command: 'true', session_id: 'two')
|
|
81
|
+
finished(second[:id])
|
|
82
|
+
expect(described_class.list(session_id: 'one', limit: 1).map { |row| row[:id] }).to eq([first[:id]])
|
|
83
|
+
expect(described_class.list(limit: 1).size).to eq(1)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it 'records real nonzero exits, signals and spawn errors' do
|
|
87
|
+
row = described_class.start(command: 'exit 7')
|
|
88
|
+
expect(finished(row[:id])).to include(status: 'FAILED', exit_code: 7, signal: nil)
|
|
89
|
+
row = described_class.start(command: 'kill -TERM $$')
|
|
90
|
+
expect(finished(row[:id])).to include(status: 'FAILED', exit_code: nil, signal: 15)
|
|
91
|
+
row = described_class.start(command: 'true', cwd: File.join(@root, 'absent'))
|
|
92
|
+
expect(finished(row[:id])).to include(status: 'FAILED')
|
|
93
|
+
expect(finished(row[:id])[:error]).to include('ENOENT')
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
it 'reuses an idempotency key across concurrent fresh creators' do
|
|
97
|
+
command = "printf once >> #{@root}/runs"
|
|
98
|
+
code = "puts JSON.generate(PWN::Plugins::Jobs.start(command: #{command.inspect}, idempotency_key: 'retry'))"
|
|
99
|
+
rows = 2.times.map { Thread.new { fresh(code) } }.map(&:value)
|
|
100
|
+
expect(rows.map { |row| row[:id] }.uniq.size).to eq(1)
|
|
101
|
+
finished(rows.first[:id])
|
|
102
|
+
expect(File.read(File.join(@root, 'runs'))).to eq('once')
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
it 'marks a dead supervisor LOST and never signals a reused metadata PID' do
|
|
106
|
+
row = described_class.start(command: 'sleep 1', max_runtime: 5)
|
|
107
|
+
Process.kill('KILL', row[:worker_pid])
|
|
108
|
+
sleep 0.1
|
|
109
|
+
path = File.join(@root, "#{row[:id]}.json")
|
|
110
|
+
meta = JSON.parse(File.read(path))
|
|
111
|
+
meta['pid'] = Process.pid
|
|
112
|
+
File.write(path, JSON.generate(meta))
|
|
113
|
+
expect(described_class.status(id: row[:id])).to include(status: 'LOST')
|
|
114
|
+
expect(described_class.stop(id: row[:id])).to be(false)
|
|
115
|
+
expect(JSON.parse(File.read(path))['status']).to eq('LOST')
|
|
116
|
+
sleep 1
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
it 'honors cwd and string environment maps with private files and nil defaults' do
|
|
120
|
+
row = described_class.start(cmd: 'printf "$JOB_TEST:$PWD"', cwd: @root, env: { 'JOB_TEST' => 'value' }, max_runtime: nil, session_id: nil, idempotency_key: nil)
|
|
121
|
+
expect(finished(row[:id])).to include(max_runtime: 0)
|
|
122
|
+
expect(described_class.result(id: row[:id])[:tail]).to eq("value:#{@root}")
|
|
123
|
+
expect(File.stat(@root).mode & 0o777).to eq(0o700)
|
|
124
|
+
expect(File.stat(row[:log]).mode & 0o777).to eq(0o600)
|
|
125
|
+
expect(File.stat(File.join(@root, "#{row[:id]}.json")).mode & 0o777).to eq(0o600)
|
|
126
|
+
expect { described_class.status(handle: '../invalid') }.to raise_error(ArgumentError)
|
|
127
|
+
expect { described_class.start(command: 'true', env: { nope: 1 }) }.to raise_error(ArgumentError)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
it 'preserves synchronous graph dependencies and drains skipped descendants' do
|
|
131
|
+
graph = described_class.graph(artifact_dir: @root, jobs: [
|
|
132
|
+
{ id: 'a', command: 'exit 1' },
|
|
133
|
+
{ id: 'b', command: 'true', needs: ['a'] },
|
|
134
|
+
{ id: 'c', command: 'true', needs: ['b'] }
|
|
135
|
+
])
|
|
136
|
+
expect(graph[:ok]).to be(false)
|
|
137
|
+
expect(graph[:jobs]['c'][:skipped]).to be(true)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
it 'treats pre-supervisor metadata as unknown rather than inventing completion' do
|
|
141
|
+
id = '0123456789ab'
|
|
142
|
+
File.write(File.join(@root, "#{id}.json"), JSON.generate(id: id, pid: Process.pid, started_at: Time.now.utc.iso8601))
|
|
143
|
+
expect(described_class.status(id: id)).to include(status: 'LOST', alive: false)
|
|
144
|
+
expect(described_class.stop(id: id)).to be(false)
|
|
145
|
+
end
|
|
146
|
+
|
|
6
147
|
it 'should display information for authors' do
|
|
7
148
|
expect(described_class).to respond_to :authors
|
|
8
149
|
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
describe PWN::Plugins::MethodCatalog 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 'builds a JSON schema with side-effect class from YARD-style plugin docs' do
|
|
16
|
+
row = described_class.schema(mod: 'PWN::Plugins::BasicAuth', method: 'encode')
|
|
17
|
+
expect(row[:parameters][:properties].keys.map(&:to_s)).to include('username', 'password')
|
|
18
|
+
expect(row[:side_effect].to_s).to match(/read_only|active_scan|exploit|destructive/)
|
|
19
|
+
expect(row[:parameters][:additionalProperties]).to eq(false)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it 'rejects an unknown keyword arg pre-execution and echoes the schema' do
|
|
23
|
+
out = described_class.guard_eval(code: "PWN::Plugins::BasicAuth.encode(not_a_key: 1, username: 'a')")
|
|
24
|
+
expect(out[:error].to_s).to match(/unknown keyword/i)
|
|
25
|
+
expect(out[:error].to_s).to include('not_a_key')
|
|
26
|
+
expect(out[:schema]).to be_a(Hash)
|
|
27
|
+
expect(out[:schema][:parameters] || out[:schema]['parameters']).not_to be_nil
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'spec_helper'
|
|
4
|
+
require 'tmpdir'
|
|
5
|
+
require 'fileutils'
|
|
4
6
|
|
|
5
7
|
describe PWN::Plugins::NmapIt do
|
|
6
8
|
it 'should display information for authors' do
|
|
@@ -23,4 +25,51 @@ describe PWN::Plugins::NmapIt do
|
|
|
23
25
|
it 'to_findings returns [] for a missing xml file' do
|
|
24
26
|
expect(described_class.to_findings(xml_file: '/tmp/no-such-nmap.xml')).to eq([])
|
|
25
27
|
end
|
|
28
|
+
|
|
29
|
+
def write_nmap_xml(path, hosts)
|
|
30
|
+
body = +'<?xml version="1.0"?><nmaprun>'
|
|
31
|
+
hosts.each do |host|
|
|
32
|
+
body << %(<host><status state="up"/><address addr="#{host[:ip]}" addrtype="ipv4"/>)
|
|
33
|
+
body << '<ports>'
|
|
34
|
+
Array(host[:ports]).each do |port|
|
|
35
|
+
body << %(<port protocol="#{port[:proto] || 'tcp'}" portid="#{port[:id]}"><state state="#{port[:state] || 'open'}"/><service name="#{port[:service]}" version="#{port[:version]}"/>)
|
|
36
|
+
Array(port[:scripts]).each do |name, output|
|
|
37
|
+
body << %(<script id="#{name}" output="#{output}"/>)
|
|
38
|
+
end
|
|
39
|
+
body << '</port>'
|
|
40
|
+
end
|
|
41
|
+
body << '</ports>'
|
|
42
|
+
if host[:scripts]
|
|
43
|
+
body << '<hostscript>'
|
|
44
|
+
host[:scripts].each { |name, output| body << %(<script id="#{name}" output="#{output}"/>) }
|
|
45
|
+
body << '</hostscript>'
|
|
46
|
+
end
|
|
47
|
+
body << '</host>'
|
|
48
|
+
end
|
|
49
|
+
body << '</nmaprun>'
|
|
50
|
+
File.write(path, body)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it 'ingests XML into the engagement store and answers what changed since yesterday in one query' do
|
|
54
|
+
Dir.mktmpdir('pwn-nmap-store-') do |dir|
|
|
55
|
+
stub_const('PWN::AI::Agent::Engagement::ROOT', dir)
|
|
56
|
+
stub_const('PWN::AI::Agent::Engagement::ACTIVE_FILE', File.join(dir, 'active'))
|
|
57
|
+
PWN::Engagement.open(name: 'lab', scope_cidrs: ['10.0.0.0/8'])
|
|
58
|
+
yesterday = File.join(dir, 'yesterday.xml')
|
|
59
|
+
today = File.join(dir, 'today.xml')
|
|
60
|
+
write_nmap_xml(yesterday, [{ ip: '10.0.0.1', ports: [{ id: 22, service: 'ssh', version: 'OpenSSH' }], scripts: { 'smb-os-discovery' => 'Windows' } }])
|
|
61
|
+
write_nmap_xml(today, [{ ip: '10.0.0.1', ports: [{ id: 22, service: 'ssh', version: 'OpenSSH' }, { id: 80, service: 'http', scripts: { 'http-title' => 'Admin' } }] }])
|
|
62
|
+
first = described_class.scan(xml: yesterday, engagement: 'lab', override: true, at: Time.now.utc - (26 * 3600))
|
|
63
|
+
expect(first[:hosts].first[:host]).to eq('10.0.0.1')
|
|
64
|
+
expect(first[:hosts].first[:scripts].to_s).to include('smb-os-discovery')
|
|
65
|
+
stored = PWN::Engagement.hosts(name: 'lab')
|
|
66
|
+
expect(stored.values.first[:services]).to include('ssh')
|
|
67
|
+
second = described_class.scan(xml: today, engagement: 'lab', override: true, at: Time.now.utc)
|
|
68
|
+
expect(second[:diff][:added_ports].map { |row| row[:port] }).to include(80)
|
|
69
|
+
delta = described_class.changes(since: 'yesterday', engagement: 'lab')
|
|
70
|
+
expect(delta[:added_ports].map { |row| row[:port] }).to include(80)
|
|
71
|
+
expect(delta[:added_ports].map { |row| row[:port] }).not_to include(22)
|
|
72
|
+
expect(delta[:changed_scripts].any? { |row| row[:name].to_s.include?('http-title') || row[:after].to_s.include?('Admin') }).to eq(true)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
26
75
|
end
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'spec_helper'
|
|
4
|
+
require 'tmpdir'
|
|
5
|
+
require 'json'
|
|
4
6
|
|
|
5
7
|
describe PWN::Plugins::Nuclei do
|
|
6
8
|
it 'should display information for authors' do
|
|
@@ -12,8 +14,38 @@ describe PWN::Plugins::Nuclei do
|
|
|
12
14
|
end
|
|
13
15
|
|
|
14
16
|
it 'to_findings maps JSONL rows into report-shaped hashes' do
|
|
15
|
-
rows = [{ 'info' => { 'name' => 'xss', 'severity' => 'high' }, 'matched-at' => 'https://x/' }]
|
|
17
|
+
rows = [{ 'info' => { 'name' => 'xss', 'severity' => 'high' }, 'matched-at' => 'https://x/', 'template-id' => 'xss-generic' }]
|
|
16
18
|
out = described_class.to_findings(rows: rows)
|
|
17
19
|
expect(out.first).to include(:title, :severity, :url)
|
|
20
|
+
expect(out.first[:template_id] || out.first[:template]).to eq('xss-generic')
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'ingests nuclei JSONL into the findings store with matched-at, template id, and severity' do
|
|
24
|
+
Dir.mktmpdir('pwn-nuclei-') do |dir|
|
|
25
|
+
stub_const('PWN::Plugins::Findings::FILE', File.join(dir, 'findings.jsonl'))
|
|
26
|
+
jsonl = File.join(dir, 'hits.jsonl')
|
|
27
|
+
File.write(jsonl, "#{JSON.generate(
|
|
28
|
+
'template-id' => 'cve-2024-wordpress-rce',
|
|
29
|
+
'matched-at' => 'https://wp.example.test/xmlrpc.php',
|
|
30
|
+
'host' => 'https://wp.example.test',
|
|
31
|
+
'info' => { 'name' => 'WordPress XML-RPC RCE', 'severity' => 'high', 'description' => 'RCE via xmlrpc' }
|
|
32
|
+
)}\n")
|
|
33
|
+
row = described_class.scan(jsonl: jsonl, record: true)
|
|
34
|
+
finding = Array(row[:findings]).first
|
|
35
|
+
expect(finding[:url] || finding[:matched_at]).to include('https://wp.example.test/xmlrpc.php')
|
|
36
|
+
expect(finding[:template_id] || finding[:template]).to eq('cve-2024-wordpress-rce')
|
|
37
|
+
expect(finding[:severity].to_s).to match(/high/i)
|
|
38
|
+
stored = PWN::Plugins::Findings.report
|
|
39
|
+
expect(stored.map { |item| item[:title] }.join).to include('WordPress')
|
|
40
|
+
expect(stored.first[:matched_at] || stored.first[:url]).to include('xmlrpc.php')
|
|
41
|
+
expect(stored.first[:template_id]).to eq('cve-2024-wordpress-rce')
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it 'selects nuclei tags from httpx-detected tech stack' do
|
|
46
|
+
tags = described_class.select_templates(techs: %w[nginx WordPress PHP])
|
|
47
|
+
blob = Array(tags[:tags] || tags).join(',')
|
|
48
|
+
expect(blob).to match(/wordpress/i)
|
|
49
|
+
expect(blob).to match(/nginx/i)
|
|
18
50
|
end
|
|
19
51
|
end
|
|
@@ -1,8 +1,56 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'spec_helper'
|
|
4
|
+
require 'tmpdir'
|
|
5
|
+
require 'json'
|
|
6
|
+
require 'open3'
|
|
4
7
|
|
|
5
8
|
describe PWN::Plugins::Radare2 do
|
|
9
|
+
def with_fake_r2
|
|
10
|
+
Dir.mktmpdir('pwn-r2pipe-') do |dir|
|
|
11
|
+
fake = File.join(dir, 'r2')
|
|
12
|
+
File.write(fake, <<~'RUBY')
|
|
13
|
+
#!/usr/bin/env ruby
|
|
14
|
+
$stdout.binmode
|
|
15
|
+
$stdin.binmode
|
|
16
|
+
$stdout.write("\x00")
|
|
17
|
+
$stdout.flush
|
|
18
|
+
loop do
|
|
19
|
+
line = $stdin.gets
|
|
20
|
+
break if line.nil?
|
|
21
|
+
|
|
22
|
+
cmd = line.chomp
|
|
23
|
+
break if cmd == 'q'
|
|
24
|
+
|
|
25
|
+
out = case cmd
|
|
26
|
+
when 'aaa' then ''
|
|
27
|
+
when 'aflj' then '[{"name":"main","offset":4198400,"size":32}]'
|
|
28
|
+
when /^pdfj/ then '{"name":"main","addr":4198400,"ops":[{"offset":4198400,"opcode":"push rbp","type":"rpush"}]}'
|
|
29
|
+
when /^pdj/ then '[{"offset":4198400,"opcode":"push rbp"}]'
|
|
30
|
+
when /^axtj/ then '[{"from":4198432,"type":"CALL","name":"sym.main"}]'
|
|
31
|
+
when /^wx/, /^s / then ''
|
|
32
|
+
when 's' then '0x401000'
|
|
33
|
+
when /^pxj/ then '[144,144]'
|
|
34
|
+
when 'pd 2 @ main' then "0x00401000 push rbp\n"
|
|
35
|
+
else '{}'
|
|
36
|
+
end
|
|
37
|
+
$stdout.write("#{out}\x00")
|
|
38
|
+
$stdout.flush
|
|
39
|
+
end
|
|
40
|
+
RUBY
|
|
41
|
+
File.chmod(0o755, fake)
|
|
42
|
+
path = File.join(dir, 'fixture.bin')
|
|
43
|
+
File.binwrite(path, 'MZ')
|
|
44
|
+
original = ENV.fetch('PATH', nil)
|
|
45
|
+
ENV['PATH'] = "#{dir}:#{original}"
|
|
46
|
+
begin
|
|
47
|
+
yield path
|
|
48
|
+
ensure
|
|
49
|
+
ENV['PATH'] = original
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
6
54
|
it 'should display information for authors' do
|
|
7
55
|
expect(described_class).to respond_to :authors
|
|
8
56
|
end
|
|
@@ -10,4 +58,71 @@ describe PWN::Plugins::Radare2 do
|
|
|
10
58
|
it 'should display information for existing help method' do
|
|
11
59
|
expect(described_class).to respond_to :help
|
|
12
60
|
end
|
|
61
|
+
|
|
62
|
+
it 'rejects injected addresses before any r2pipe command is written' do
|
|
63
|
+
expect(Open3).not_to receive(:popen2)
|
|
64
|
+
%i[disasm xrefs_to seek patch_bytes].each do |name|
|
|
65
|
+
expect { described_class.public_send(name, session: 'missing', addr: 'main;!id', n: 8, hex: '90') }.to raise_error(ArgumentError, /addr|address|hex/)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it 'enumerates functions and returns structured main disassembly from two JSON helpers' do
|
|
70
|
+
with_fake_r2 do |path|
|
|
71
|
+
sid = described_class.open(bin: path)
|
|
72
|
+
functions = described_class.functions(session: sid)
|
|
73
|
+
disasm = described_class.disasm(session: sid, addr: 'main')
|
|
74
|
+
described_class.close(session: sid)
|
|
75
|
+
expect(functions).to be_an(Array)
|
|
76
|
+
expect(functions.first).to include('name' => 'main')
|
|
77
|
+
expect(disasm).to be_a(Hash)
|
|
78
|
+
expect(disasm.fetch('ops')).not_to be_empty
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it 'keeps cmd raw, cmdj parsed, seek JSON, and write-gated patch_bytes' do
|
|
83
|
+
with_fake_r2 do |path|
|
|
84
|
+
sid = described_class.open(bin: path)
|
|
85
|
+
expect(described_class.cmd(session: sid, cmd: 'pd 2 @ main')).to include('push rbp')
|
|
86
|
+
expect(described_class.cmdj(session: sid, cmd: 'afl')).to be_an(Array)
|
|
87
|
+
expect(described_class.xrefs_to(session: sid, addr: 'main')).to be_an(Array)
|
|
88
|
+
expect(described_class.seek(session: sid, addr: 'main')).to include(addr: '0x401000', offset: 0x401000)
|
|
89
|
+
expect { described_class.patch_bytes(session: sid, addr: 'main', hex: '9090') }.to raise_error(ArgumentError, /write/)
|
|
90
|
+
described_class.close(session: sid)
|
|
91
|
+
write = described_class.open(bin: path, write: true)
|
|
92
|
+
expect(described_class.patch_bytes(session: write, addr: 'main', hex: '9090')).to eq([144, 144])
|
|
93
|
+
described_class.close(session: write)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
it 'lets the agent enumerate functions and disassemble main in two structured tool calls' do
|
|
98
|
+
PWN::AI::Agent::Registry.discover(force: true)
|
|
99
|
+
load '/opt/pwn/lib/pwn/ai/agent/tools/radare2.rb'
|
|
100
|
+
with_fake_r2 do |path|
|
|
101
|
+
functions = PWN::AI::Agent::Registry.lookup(name: 'r2_functions').handler.call('bin' => path)
|
|
102
|
+
disasm = PWN::AI::Agent::Registry.lookup(name: 'r2_disasm').handler.call('session' => functions[:session], 'addr' => 'main')
|
|
103
|
+
expect(functions[:command]).to eq('aflj')
|
|
104
|
+
expect(functions[:functions].first).to include('name' => 'main')
|
|
105
|
+
expect(disasm[:command]).to eq('pdfj')
|
|
106
|
+
expect(disasm[:disasm].fetch('ops')).not_to be_empty
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
it 'enumerates functions and pulls main disassembly from a compiled binary', :radare2_integration do
|
|
111
|
+
Dir.mktmpdir('pwn-r2-real-') do |dir|
|
|
112
|
+
source = File.join(dir, 'fixture.c')
|
|
113
|
+
path = File.join(dir, 'fixture')
|
|
114
|
+
File.write(source, "int main(void) { return 0; }\n")
|
|
115
|
+
out, status = Open3.capture2e('cc', '-O0', '-g', '-o', path, source)
|
|
116
|
+
expect(status.success?).to eq(true), out
|
|
117
|
+
sid = described_class.open(bin: path)
|
|
118
|
+
functions = described_class.functions(session: sid)
|
|
119
|
+
disasm = described_class.disasm(session: sid, addr: 'main')
|
|
120
|
+
described_class.close(session: sid)
|
|
121
|
+
expect(functions).to be_an(Array)
|
|
122
|
+
expect(functions.any? { |row| row['name'].to_s.include?('main') }).to eq(true)
|
|
123
|
+
expect(disasm).to be_a(Hash)
|
|
124
|
+
expect(disasm['ops']).to be_an(Array)
|
|
125
|
+
expect(disasm['ops']).not_to be_empty
|
|
126
|
+
end
|
|
127
|
+
end
|
|
13
128
|
end
|
|
@@ -23,4 +23,19 @@ describe PWN::Plugins::Recon do
|
|
|
23
23
|
names = described_class.crt_sh(domain: '0dayinc.com')
|
|
24
24
|
expect(names).to include('www.0dayinc.com', 'vpn.0dayinc.com')
|
|
25
25
|
end
|
|
26
|
+
|
|
27
|
+
it 'harvests recon banners into the loot store' do
|
|
28
|
+
Dir.mktmpdir('pwn-recon-loot-') do |dir|
|
|
29
|
+
allow(Dir).to receive(:home).and_return(dir)
|
|
30
|
+
described_class.harvest_loot(
|
|
31
|
+
engagement_id: 'lab',
|
|
32
|
+
assets: [{
|
|
33
|
+
address: 'app.example.test',
|
|
34
|
+
observations: [{ banner: "SSH-2.0\nusername=admin\npassword=hunter2\n" }]
|
|
35
|
+
}]
|
|
36
|
+
)
|
|
37
|
+
offered = PWN::Plugins::Vault.offer(host: 'app.example.test', engagement: 'lab')
|
|
38
|
+
expect(offered.first[:secret]).to eq('hunter2')
|
|
39
|
+
end
|
|
40
|
+
end
|
|
26
41
|
end
|