pwn 0.5.709 → 0.5.710

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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +2 -2
  3. data/documentation/Installation.md +1 -1
  4. data/etc/default_skills/exploitdev/SKILL.md +38 -0
  5. data/etc/default_skills/pwn/plugins/aflplusplus/SKILL.md +1 -0
  6. data/etc/default_skills/pwn/plugins/credential_attack/SKILL.md +7 -0
  7. data/etc/default_skills/pwn/plugins/credential_attack/references/urls.md +3 -0
  8. data/etc/default_skills/pwn/plugins/exploit_db/SKILL.md +49 -0
  9. data/etc/default_skills/pwn/plugins/exploit_dev/SKILL.md +2 -0
  10. data/etc/default_skills/pwn/plugins/frida/SKILL.md +2 -0
  11. data/etc/default_skills/pwn/plugins/fuzz/SKILL.md +2 -0
  12. data/etc/default_skills/pwn/plugins/gdb/SKILL.md +1 -0
  13. data/etc/default_skills/pwn/plugins/k8s/SKILL.md +51 -0
  14. data/etc/default_skills/pwn/plugins/nuclei/SKILL.md +2 -0
  15. data/etc/default_skills/pwn/plugins/{doctor → preflight_checker}/SKILL.md +13 -11
  16. data/etc/default_skills/pwn/plugins/process_tube/SKILL.md +1 -0
  17. data/etc/default_skills/pwn/plugins/recon/SKILL.md +2 -0
  18. data/etc/default_skills/pwn/plugins/recon/references/urls.md +1 -0
  19. data/etc/default_skills/pwn/plugins/semgrep/SKILL.md +48 -0
  20. data/etc/default_skills/pwn/plugins/volatility/SKILL.md +1 -0
  21. data/lib/pwn/ai/agent/prompt_builder.rb +1 -1
  22. data/lib/pwn/ai/agent/tools/sessions.rb +8 -1
  23. data/lib/pwn/ffi/stdio.rb +3 -1
  24. data/lib/pwn/plugins/aflplusplus.rb +21 -1
  25. data/lib/pwn/plugins/android.rb +9 -3
  26. data/lib/pwn/plugins/credential_attack.rb +62 -3
  27. data/lib/pwn/plugins/exploit_db.rb +69 -0
  28. data/lib/pwn/plugins/exploit_dev.rb +69 -3
  29. data/lib/pwn/plugins/frida.rb +60 -9
  30. data/lib/pwn/plugins/fuzz.rb +33 -0
  31. data/lib/pwn/plugins/gdb.rb +25 -5
  32. data/lib/pwn/plugins/jobs.rb +1 -0
  33. data/lib/pwn/plugins/k8s.rb +73 -0
  34. data/lib/pwn/plugins/nuclei.rb +36 -3
  35. data/lib/pwn/plugins/packet.rb +1 -1
  36. data/lib/pwn/plugins/{doctor.rb → preflight_checker.rb} +22 -5
  37. data/lib/pwn/plugins/process_tube.rb +20 -1
  38. data/lib/pwn/plugins/radare2.rb +1 -1
  39. data/lib/pwn/plugins/recon.rb +39 -4
  40. data/lib/pwn/plugins/semgrep.rb +61 -0
  41. data/lib/pwn/plugins/sock.rb +1 -0
  42. data/lib/pwn/plugins/sqlmap.rb +1 -1
  43. data/lib/pwn/plugins/volatility.rb +21 -2
  44. data/lib/pwn/plugins.rb +4 -1
  45. data/lib/pwn/setup.rb +13 -1
  46. data/lib/pwn/version.rb +1 -1
  47. data/spec/integration/help_contract_spec.rb +2 -0
  48. data/spec/lib/pwn/plugins/exploit_db_spec.rb +13 -0
  49. data/spec/lib/pwn/plugins/k8s_spec.rb +13 -0
  50. data/spec/lib/pwn/plugins/{doctor_spec.rb → preflight_checker_spec.rb} +1 -1
  51. data/spec/lib/pwn/plugins/semgrep_spec.rb +13 -0
  52. data/spec/lib/pwn/req_queue_spec.rb +168 -0
  53. data/third_party/pwn_rdoc.jsonl +43 -9
  54. metadata +20 -8
@@ -19,7 +19,7 @@ module PWN
19
19
  # path: 'required - filesystem path to the binary r2 should open'
20
20
  # )
21
21
  public_class_method def self.open(opts = {})
22
- PWN::Plugins::Doctor.require_bin!(name: 'r2')
22
+ PWN::Plugins::PreflightChecker.require_bin!(name: 'r2')
23
23
  path = opts[:path].to_s
24
24
  raise 'ERROR: path is required' if path.empty?
25
25
  raise "ERROR: binary not found: #{path}" unless File.file?(path)
@@ -21,16 +21,20 @@ module PWN
21
21
  raise 'ERROR: domain is required' if domain.empty?
22
22
 
23
23
  names = []
24
- if PWN::Plugins::Doctor.bin?(name: 'subfinder')
24
+ if PWN::Plugins::PreflightChecker.bin?(name: 'subfinder')
25
25
  stdout, = Open3.capture3('subfinder', '-silent', '-d', domain)
26
26
  names.concat(stdout.lines.map(&:strip).reject(&:empty?))
27
27
  end
28
+ if PWN::Plugins::PreflightChecker.bin?(name: 'amass')
29
+ stdout, = Open3.capture3('amass', 'enum', '-passive', '-d', domain, '-nocolor')
30
+ names.concat(stdout.lines.map(&:strip).reject(&:empty?))
31
+ end
28
32
  names.concat(Array(crt_sh(domain: domain)))
29
33
  normalize_names(names: names, domain: domain)
30
34
  end
31
35
 
32
36
  public_class_method def self.httpx(opts = {})
33
- PWN::Plugins::Doctor.require_bin!(name: 'httpx')
37
+ PWN::Plugins::PreflightChecker.require_bin!(name: 'httpx')
34
38
  hosts = Array(opts[:hosts] || opts[:urls])
35
39
  stdout, = Open3.capture3('httpx', '-silent', '-json', stdin_data: "#{hosts.join("\n")}\n")
36
40
  stdout.each_line.filter_map do |ln|
@@ -41,8 +45,8 @@ module PWN
41
45
  end
42
46
 
43
47
  public_class_method def self.masscan(opts = {})
44
- bin = %w[masscan naabu].find { |b| PWN::Plugins::Doctor.bin?(name: b) }
45
- raise PWN::Plugins::Doctor::MissingBinary, 'ERROR: masscan/naabu missing' unless bin
48
+ bin = %w[masscan naabu].find { |b| PWN::Plugins::PreflightChecker.bin?(name: b) }
49
+ raise PWN::Plugins::PreflightChecker::MissingBinary, 'ERROR: masscan/naabu missing' unless bin
46
50
 
47
51
  target = opts[:target].to_s
48
52
  ports = (opts[:ports] || '1-1024').to_s
@@ -75,6 +79,26 @@ module PWN
75
79
  raise "ERROR: cert-transparency lookup failed for #{domain} (#{errors.join('; ')})"
76
80
  end
77
81
 
82
+ public_class_method def self.amass(opts = {})
83
+ PWN::Plugins::PreflightChecker.require_bin!(name: 'amass')
84
+ domain = opts[:domain].to_s
85
+ raise 'ERROR: domain is required' if domain.empty?
86
+
87
+ stdout, = Open3.capture3('amass', 'enum', '-passive', '-d', domain, '-nocolor')
88
+ stdout.lines.map(&:strip).reject(&:empty?)
89
+ end
90
+
91
+ public_class_method def self.passive_dns(opts = {})
92
+ domain = opts[:domain].to_s.downcase.strip
93
+ raise 'ERROR: domain is required' if domain.empty?
94
+
95
+ uri = URI("https://api.hackertarget.com/hostsearch/?q=#{URI.encode_www_form_component(domain)}")
96
+ rows = http_json(url: uri.to_s, timeout: opts[:timeout] || 8)
97
+ Array(rows)
98
+ rescue StandardError => e
99
+ { error: "#{e.class}: #{e.message}", domain: domain }
100
+ end
101
+
78
102
  public_class_method def self.authors
79
103
  "AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
80
104
  end
@@ -107,6 +131,17 @@ module PWN
107
131
  timeout: 'optional - seconds to wait before giving up'
108
132
  )
109
133
 
134
+ # Passive amass enum (requires amass).
135
+ #{self}.amass(
136
+ domain: 'required - FQDN to query (e.g. example.com)'
137
+ )
138
+
139
+ # Passive DNS hostsearch for a domain (hackertarget).
140
+ #{self}.passive_dns(
141
+ domain: 'required - FQDN to query (e.g. example.com)',
142
+ timeout: 'optional - seconds to wait before giving up'
143
+ )
144
+
110
145
  # Print the AUTHOR(S) string for this module.
111
146
  #{self}.authors
112
147
  "
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'open3'
5
+
6
+ module PWN
7
+ module Plugins
8
+ # semgrep wrapper feeding SAST-shaped findings.
9
+ module Semgrep
10
+ public_class_method def self.required_bins
11
+ %w[semgrep]
12
+ end
13
+
14
+ public_class_method def self.scan(opts = {})
15
+ PWN::Plugins::PreflightChecker.require_bin!(name: 'semgrep')
16
+ target = opts[:path] || opts[:target] || '.'
17
+ cmd = ['semgrep', '--json', '--quiet']
18
+ cmd += ['--config', opts[:config].to_s] if opts[:config]
19
+ cmd << target.to_s
20
+ stdout, stderr, status = Open3.capture3(*cmd)
21
+ parsed = begin
22
+ JSON.parse(stdout)
23
+ rescue JSON::ParserError
24
+ {}
25
+ end
26
+ findings = Array(parsed['results']).map do |r|
27
+ {
28
+ title: r['check_id'],
29
+ severity: r.dig('extra', 'severity'),
30
+ path: r['path'],
31
+ line: r.dig('start', 'line'),
32
+ message: r.dig('extra', 'message')
33
+ }
34
+ end
35
+ { findings: findings, stderr: stderr, exit: status.exitstatus }
36
+ end
37
+
38
+ public_class_method def self.authors
39
+ "AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
40
+ end
41
+
42
+ public_class_method def self.help
43
+ puts "USAGE:
44
+ # List host binaries this module expects to be installed.
45
+ #{self}.required_bins
46
+
47
+ # Run semgrep --json and map results into finding hashes.
48
+ #{self}.scan(
49
+ path: 'optional - directory or file to scan (defaults to .)',
50
+ target: 'optional - alias for path',
51
+ config: 'optional - semgrep --config value (e.g. auto or p/owasp-top-ten)'
52
+ )
53
+
54
+ # Print the AUTHOR(S) string for this module.
55
+ #{self}.authors
56
+ "
57
+ constants.sort
58
+ end
59
+ end
60
+ end
61
+ end
@@ -23,6 +23,7 @@ module PWN
23
23
 
24
24
  protocol = opts[:protocol]
25
25
  protocol ||= :tcp
26
+ PWN::Plugins::PreflightChecker.require_cap_net_raw! if protocol.to_s.to_sym == :raw
26
27
 
27
28
  # TODO: Add proxy support
28
29
 
@@ -11,7 +11,7 @@ module PWN
11
11
  end
12
12
 
13
13
  public_class_method def self.run(opts = {})
14
- PWN::Plugins::Doctor.require_bin!(name: 'sqlmap')
14
+ PWN::Plugins::PreflightChecker.require_bin!(name: 'sqlmap')
15
15
  url = opts[:url].to_s
16
16
  raise 'ERROR: url is required' if url.empty?
17
17
 
@@ -11,8 +11,8 @@ module PWN
11
11
  end
12
12
 
13
13
  public_class_method def self.run(opts = {})
14
- bin = %w[vol volatility3 vol3].find { |b| PWN::Plugins::Doctor.bin?(name: b) }
15
- raise PWN::Plugins::Doctor::MissingBinary, 'ERROR: volatility3 (vol) missing' unless bin
14
+ bin = %w[vol volatility3 vol3].find { |b| PWN::Plugins::PreflightChecker.bin?(name: b) }
15
+ raise PWN::Plugins::PreflightChecker::MissingBinary, 'ERROR: volatility3 (vol) missing' unless bin
16
16
 
17
17
  memory = opts[:memory] || opts[:file]
18
18
  plugin = opts[:plugin] || 'windows.pslist'
@@ -22,6 +22,16 @@ module PWN
22
22
  { stdout: stdout, stderr: stderr, exit: status.exitstatus }
23
23
  end
24
24
 
25
+ public_class_method def self.yara(opts = {})
26
+ PWN::Plugins::PreflightChecker.require_bin!(name: 'yara')
27
+ rules = opts[:rules] || opts[:rule]
28
+ target = opts[:memory] || opts[:file] || opts[:path]
29
+ raise 'ERROR: rules and memory/file are required' if rules.to_s.empty? || target.to_s.empty?
30
+
31
+ stdout, stderr, status = Open3.capture3('yara', rules.to_s, target.to_s)
32
+ { stdout: stdout, stderr: stderr, exit: status.exitstatus }
33
+ end
34
+
25
35
  public_class_method def self.authors
26
36
  "AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
27
37
  end
@@ -38,6 +48,15 @@ module PWN
38
48
  plugin: 'required - plugin value consumed by #run (defaults to windows.pslist)'
39
49
  )
40
50
 
51
+ # Scan a memory image with a yara rules file.
52
+ #{self}.yara(
53
+ rules: 'required - path to a .yar rules file',
54
+ rule: 'optional - alias for rules',
55
+ memory: 'required - memory image path',
56
+ file: 'optional - alias for memory',
57
+ path: 'optional - alias for memory'
58
+ )
59
+
41
60
  # Print the AUTHOR(S) string for this module.
42
61
  #{self}.authors
43
62
  "
data/lib/pwn/plugins.rb CHANGED
@@ -20,7 +20,7 @@ module PWN
20
20
  autoload :Char, 'pwn/plugins/char'
21
21
  autoload :CreditCard, 'pwn/plugins/credit_card'
22
22
  autoload :CredentialAttack, 'pwn/plugins/credential_attack'
23
- autoload :Doctor, 'pwn/plugins/doctor'
23
+ autoload :PreflightChecker, 'pwn/plugins/preflight_checker'
24
24
  autoload :PWNLogger, 'pwn/plugins/pwn_logger'
25
25
  autoload :DAOLDAP, 'pwn/plugins/dao_ldap'
26
26
  autoload :DAOMongo, 'pwn/plugins/dao_mongo'
@@ -29,6 +29,7 @@ module PWN
29
29
  autoload :DefectDojo, 'pwn/plugins/defect_dojo'
30
30
  autoload :DetectOS, 'pwn/plugins/detect_os'
31
31
  autoload :EIN, 'pwn/plugins/ein'
32
+ autoload :ExploitDB, 'pwn/plugins/exploit_db'
32
33
  autoload :FileFu, 'pwn/plugins/file_fu'
33
34
  autoload :Fuzz, 'pwn/plugins/fuzz'
34
35
  autoload :Frida, 'pwn/plugins/frida'
@@ -42,6 +43,7 @@ module PWN
42
43
  autoload :IPInfo, 'pwn/plugins/ip_info'
43
44
  autoload :IRC, 'pwn/plugins/irc'
44
45
  autoload :Jenkins, 'pwn/plugins/jenkins'
46
+ autoload :K8s, 'pwn/plugins/k8s'
45
47
  autoload :JiraDataCenter, 'pwn/plugins/jira_data_center'
46
48
  autoload :Jobs, 'pwn/plugins/jobs'
47
49
  autoload :JSONPathify, 'pwn/plugins/json_pathify'
@@ -68,6 +70,7 @@ module PWN
68
70
  autoload :Recon, 'pwn/plugins/recon'
69
71
  autoload :REPL, 'pwn/plugins/repl'
70
72
  autoload :ScannableCodes, 'pwn/plugins/scannable_codes'
73
+ autoload :Semgrep, 'pwn/plugins/semgrep'
71
74
  autoload :Serial, 'pwn/plugins/serial'
72
75
  autoload :Shodan, 'pwn/plugins/shodan'
73
76
  autoload :SlackClient, 'pwn/plugins/slack_client'
data/lib/pwn/setup.rb CHANGED
@@ -257,6 +257,18 @@ module PWN
257
257
  apt: %w[], dnf: %w[], pacman: %w[], brew: %w[], port: %w[],
258
258
  plugins: %w[PWN::Plugins::ExploitDev]
259
259
  },
260
+ 'pwndbg' => {
261
+ apt: %w[pwndbg], dnf: %w[], pacman: %w[pwndbg], brew: %w[], port: %w[],
262
+ plugins: %w[PWN::Plugins::GDB]
263
+ },
264
+ 'ropper' => {
265
+ apt: %w[], dnf: %w[], pacman: %w[], brew: %w[], port: %w[],
266
+ plugins: %w[PWN::Plugins::ExploitDev]
267
+ },
268
+ 'rizin' => {
269
+ apt: %w[rizin], dnf: %w[], pacman: %w[rizin], brew: %w[], port: %w[],
270
+ plugins: %w[PWN::Plugins::Radare2]
271
+ },
260
272
  'qemu-x86_64' => {
261
273
  apt: %w[qemu-user-static], dnf: %w[qemu-user-static], pacman: %w[qemu-user-static],
262
274
  brew: %w[], port: %w[],
@@ -344,7 +356,7 @@ module PWN
344
356
  re: {
345
357
  desc: 'Radare2 · GDB · ExploitDev · Frida · Android RE tier',
346
358
  gems: %w[],
347
- bins: %w[gdb r2 checksec ROPgadget one_gadget qemu-x86_64 yara frida apktool jadx]
359
+ bins: %w[gdb r2 checksec ROPgadget one_gadget qemu-x86_64 yara frida apktool jadx pwndbg ropper rizin]
348
360
  },
349
361
  full: {
350
362
  desc: 'everything above',
data/lib/pwn/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PWN
4
- VERSION = '0.5.709'
4
+ VERSION = '0.5.710'
5
5
  end
@@ -75,6 +75,8 @@ RSpec.describe 'PWN:: modules — .help / .authors behavioural contract', :aggre
75
75
 
76
76
  # .help does not raise, completes in <2 s, and produces SOMETHING
77
77
  # (stdout, a String return, or a namespace-index Array).
78
+ # PWN::FFI::Stdio attaches libc puts; help must not write fd 1 behind
79
+ # $stdout (Kernel.puts in that module). Still swallow $stdout here.
78
80
  out = nil
79
81
  expect do
80
82
  Timeout.timeout(2) { out = capture_out { mod.help } }
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe PWN::Plugins::ExploitDB 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
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe PWN::Plugins::K8s 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
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  require 'spec_helper'
4
4
 
5
- describe PWN::Plugins::Doctor do
5
+ describe PWN::Plugins::PreflightChecker do
6
6
  it 'should display information for authors' do
7
7
  expect(described_class).to respond_to :authors
8
8
  end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe PWN::Plugins::Semgrep 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
+ end
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'socket'
5
+ require 'tmpdir'
6
+
7
+ describe 'pwn_debug REQ queue remaining surface' do
8
+ describe PWN::Plugins::ExploitDev do
9
+ it 'catalogs per-arch execve_bin_sh shellcode as opcodes' do
10
+ sc = described_class.shellcode(arch: 'x86_64', kind: 'execve_bin_sh')
11
+ expect(sc.to_s).not_to be_empty
12
+ expect(sc.to_s).not_to eq("\x90")
13
+ end
14
+
15
+ it 'one_gadget returns a structured miss when the binary is absent' do
16
+ allow(PWN::Plugins::PreflightChecker).to receive(:bin?).and_call_original
17
+ allow(PWN::Plugins::PreflightChecker).to receive(:bin?).with(name: 'one_gadget').and_return(false)
18
+ row = described_class.one_gadget(path: '/lib/x86_64-linux-gnu/libc.so.6')
19
+ expect(row).to be_a(Hash)
20
+ expect(row[:error].to_s).to match(/one_gadget/i)
21
+ end
22
+
23
+ it 'libc_offsets extracts named symbols from an ELF' do
24
+ path = %w[/bin/ls /usr/bin/ls /bin/true /usr/bin/true].find { |p| File.file?(p) }
25
+ skip 'no ELF on host' unless path
26
+
27
+ row = described_class.libc_offsets(path: path)
28
+ expect(row).to include(:path)
29
+ expect(row[:symbols]).to be_a(Hash)
30
+ end
31
+ end
32
+
33
+ describe PWN::Plugins::GDB do
34
+ it 'core raises without a core path' do
35
+ expect { described_class.core(core: '') }.to raise_error(/core is required/)
36
+ end
37
+
38
+ it 'breakpoints includes break commands in the gdb batch' do
39
+ allow(Open3).to receive(:capture3).and_return(['ok', '', instance_double(Process::Status, exitstatus: 0)])
40
+ described_class.breakpoints(binary: '/bin/true', breakpoints: ['main', '*0x401000'])
41
+ expect(Open3).to have_received(:capture3) do |*argv|
42
+ joined = argv.flatten.join(' ')
43
+ expect(joined).to include('break main')
44
+ expect(joined).to include('break *0x401000')
45
+ end
46
+ end
47
+ end
48
+
49
+ describe PWN::Plugins::ProcessTube do
50
+ it 'connect uses the same write_line/recvuntil API over TCP' do
51
+ server = TCPServer.new('127.0.0.1', 0)
52
+ port = server.addr[1]
53
+ thr = Thread.new do
54
+ sock = server.accept
55
+ sock.write("banner\n")
56
+ sock.flush
57
+ line = sock.gets
58
+ sock.write("ack #{line}")
59
+ sock.close
60
+ end
61
+ tube = described_class.connect(host: '127.0.0.1', port: port)
62
+ described_class.recvuntil(id: tube[:id], until: "\n", timeout: 2)
63
+ described_class.write_line(id: tube[:id], line: 'hi')
64
+ out = described_class.recvuntil(id: tube[:id], until: 'hi', timeout: 2)
65
+ expect(out).to include('ack')
66
+ ensure
67
+ described_class.close(id: tube[:id]) if tube
68
+ thr&.kill
69
+ server&.close
70
+ end
71
+ end
72
+
73
+ describe PWN::Setup do
74
+ it 're profile includes pwndbg, ropper, and rizin' do
75
+ bins = PWN::Setup::PROFILES[:re][:bins]
76
+ expect(bins).to include('pwndbg', 'ropper', 'rizin')
77
+ end
78
+ end
79
+
80
+ describe PWN::Plugins::PreflightChecker do
81
+ it 'check includes missing_services' do
82
+ row = described_class.check.find { |r| r[:plugin].to_s.include?('Packet') }
83
+ expect(row).to include(:missing_services)
84
+ end
85
+
86
+ it 'service? is false for a missing unix socket' do
87
+ expect(described_class.service?(name: 'no-such-pwn-svc', path: '/tmp/pwn-no-such.sock')).to be false
88
+ end
89
+ end
90
+
91
+ describe PWN::Plugins::Sock do
92
+ it 'protocol :raw demands CAP_NET_RAW' do
93
+ allow(PWN::Plugins::PreflightChecker).to receive(:cap_net_raw?).and_return(false)
94
+ expect do
95
+ described_class.connect(target: '127.0.0.1', port: 9, protocol: :raw)
96
+ end.to raise_error(PWN::Plugins::PreflightChecker::MissingCapability)
97
+ end
98
+ end
99
+
100
+ describe PWN::Plugins::Nuclei do
101
+ it 'to_findings maps JSONL rows into report-shaped hashes' do
102
+ rows = [{ 'info' => { 'name' => 'xss', 'severity' => 'high' }, 'matched-at' => 'https://x/' }]
103
+ out = described_class.to_findings(rows: rows)
104
+ expect(out.first).to include(:title, :severity, :url)
105
+ end
106
+ end
107
+
108
+ describe PWN::Plugins::Fuzz do
109
+ it 'http mutates a request with dictionary payloads' do
110
+ reqs = described_class.http(
111
+ request: "GET /FUZZ HTTP/1.1\r\nHost: x\r\n\r\n",
112
+ dictionary: %w[admin ../]
113
+ )
114
+ expect(reqs.length).to eq(2)
115
+ expect(reqs.first).to include('admin')
116
+ end
117
+
118
+ it 'file_format mutates a seed with dictionary blobs' do
119
+ Dir.mktmpdir do |dir|
120
+ seed = File.join(dir, 'seed.bin')
121
+ File.binwrite(seed, 'AAAA')
122
+ out = described_class.file_format(path: seed, dictionary: ["\xff\xff"])
123
+ expect(out).to be_an(Array)
124
+ expect(out.first).to be_a(String)
125
+ end
126
+ end
127
+ end
128
+
129
+ describe PWN::Plugins::ArtifactRegistry do
130
+ it 'lists artifacts for a session id' do
131
+ Dir.mktmpdir do |dir|
132
+ stub_const('PWN::Plugins::ArtifactRegistry::ROOT', dir)
133
+ src = File.join(dir, 'loot.txt')
134
+ File.write(src, 'x')
135
+ described_class.register(session_id: 'sess1', path: src, kind: 'loot')
136
+ rows = described_class.list(session_id: 'sess1')
137
+ expect(rows.first[:kind]).to eq('loot')
138
+ end
139
+ end
140
+ end
141
+
142
+ describe PWN::Plugins::Frida do
143
+ it 'ssl_pinning_script returns injectable javascript' do
144
+ js = described_class.ssl_pinning_script
145
+ expect(js).to match(/SSL_CTX|TrustManager|pinning/i)
146
+ end
147
+ end
148
+
149
+ describe PWN::Plugins::Semgrep do
150
+ it 'responds to scan' do
151
+ expect(described_class).to respond_to(:scan)
152
+ end
153
+ end
154
+
155
+ describe PWN::Plugins::ExploitDB do
156
+ it 'responds to search and cve_for_cpe' do
157
+ expect(described_class).to respond_to(:search)
158
+ expect(described_class).to respond_to(:cve_for_cpe)
159
+ end
160
+ end
161
+
162
+ describe PWN::Plugins::K8s do
163
+ it 'responds to trivy and kube_hunter' do
164
+ expect(described_class).to respond_to(:trivy)
165
+ expect(described_class).to respond_to(:kube_hunter)
166
+ end
167
+ end
168
+ end