pwn 0.5.729 → 0.5.731
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 +3 -3
- data/documentation/Contributing.md +61 -0
- data/lib/pwn/version.rb +1 -1
- data/spec/lib/pwn/ai/agent/verification_spec.rb +51 -9
- data/spec/lib/pwn/ffi/adalm_pluto_spec.rb +25 -11
- data/spec/lib/pwn/ffi/fftw_spec.rb +7 -2
- data/spec/lib/pwn/ffi/hack_rf_spec.rb +8 -2
- data/spec/lib/pwn/ffi/liquid_spec.rb +21 -5
- data/spec/lib/pwn/ffi/rtl_sdr_spec.rb +27 -4
- data/spec/lib/pwn/ffi/soapy_sdr_spec.rb +8 -2
- data/spec/lib/pwn/ffi/volk_spec.rb +10 -4
- data/spec/lib/pwn/plugins/binary_analysis_spec.rb +41 -11
- data/spec/lib/pwn/sdr/decoder/dsp_spec.rb +40 -15
- data/spec/spec_helper.rb +6 -0
- metadata +7 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fba7b3d4693ca9b846825957a7bde24e2a1cd6fd18577666cac32f3abfc0fcb2
|
|
4
|
+
data.tar.gz: a1ea687f125fb659f363d7195ac4f32f332918b7707ecf1aa78acafa653eff36
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 50cd5f5f05075760b364ef46dc82ff469d1698bbaf724880563f97793108437fcfbbc441e2283817ce10b44c46f36566b6254f36879be66f67432cdeecc8c57a
|
|
7
|
+
data.tar.gz: 2085d58ae27b634c5d0893ab0c30f428e9442a15695721a4e45d3c923cbbaa67eb95d35e578bb07d1e1b6253be089ac2c833381ba839adf406c2137229063cd3
|
data/Gemfile
CHANGED
|
@@ -50,9 +50,9 @@ gem 'libusb', '0.8.0'
|
|
|
50
50
|
gem 'luhn', '3.0.0'
|
|
51
51
|
gem 'mail', '2.9.1'
|
|
52
52
|
gem 'mcp', '1.5.1'
|
|
53
|
-
gem 'meshtastic', '0.0.
|
|
53
|
+
gem 'meshtastic', '0.0.176'
|
|
54
54
|
gem 'metasm', '1.0.6'
|
|
55
|
-
gem 'mongo', '2.
|
|
55
|
+
gem 'mongo', '2.26.0'
|
|
56
56
|
gem 'msfrpc-client', '1.1.2'
|
|
57
57
|
gem 'netaddr', '2.0.6'
|
|
58
58
|
gem 'net-ldap', '0.20.0'
|
|
@@ -81,7 +81,7 @@ gem 'rmagick', '7.1.4'
|
|
|
81
81
|
gem 'rqrcode', '3.2.0'
|
|
82
82
|
gem 'rspec', '3.13.2'
|
|
83
83
|
gem 'rtesseract', '3.1.4'
|
|
84
|
-
gem 'rubocop', '1.
|
|
84
|
+
gem 'rubocop', '1.91.0'
|
|
85
85
|
gem 'rubocop-rake', '0.7.1'
|
|
86
86
|
gem 'rubocop-rspec', '3.10.2'
|
|
87
87
|
gem 'ruby-audio', '1.6.1'
|
|
@@ -60,6 +60,67 @@ An already-true `stop:` callback raises cancellation before spawning either clie
|
|
|
60
60
|
Invalid inputs still raise `ArgumentError`; native decoding never silently falls
|
|
61
61
|
back to another decoder or a detector.
|
|
62
62
|
|
|
63
|
+
### Optional Liquid and radare2 integration tests
|
|
64
|
+
|
|
65
|
+
Native Liquid DSP examples and the real radare2 JSON replay are also explicit
|
|
66
|
+
opt-ins, even when the backends happen to be installed. The default gate excludes
|
|
67
|
+
`liquid_integration` and `radare2_integration` metadata rather than marking tests
|
|
68
|
+
pending. Liquid interface checks, missing-library errors, pure-Ruby DSP fallback,
|
|
69
|
+
radare2 argument validation, and binutils fallback coverage still run by default.
|
|
70
|
+
The binary fallback tests require the existing binutils tools and `/bin/true`.
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
PWN_TEST_LIQUID=1 bundle exec rspec spec/lib/pwn/ffi/liquid_spec.rb
|
|
74
|
+
PWN_TEST_RADARE2=1 bundle exec rspec spec/lib/pwn/plugins/binary_analysis_spec.rb
|
|
75
|
+
# Enable both in the full gate:
|
|
76
|
+
PWN_TEST_LIQUID=1 PWN_TEST_RADARE2=1 bundle exec rake
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Liquid opt-in requires the liquid-dsp shared library (`libliquid`) visible to the
|
|
80
|
+
dynamic loader. Radare2 opt-in requires `r2` and a C compiler named `cc` on PATH.
|
|
81
|
+
The radare2 example compiles a small unstripped C fixture in a temporary directory,
|
|
82
|
+
checks real JSON disassembly for its known `main` symbol, and removes the fixture
|
|
83
|
+
afterward. It does not assume the host's stripped `/bin/true` has a discoverable
|
|
84
|
+
`main`. Opt-in runs fail with installation guidance when a prerequisite is absent;
|
|
85
|
+
they never skip or accept a degraded backend as native success. The tests install
|
|
86
|
+
nothing and require no radio hardware or network access.
|
|
87
|
+
|
|
88
|
+
### Optional FFTW, Volk, and SDR shared-library tests
|
|
89
|
+
|
|
90
|
+
The default gate also excludes real FFTW/Volk computations and radio-library
|
|
91
|
+
integration checks, regardless of what is installed. Missing-library behavior,
|
|
92
|
+
mocked device inventory and stream lifecycle tests, and Ruby DSP fallbacks run
|
|
93
|
+
without these shared libraries and produce no pending examples.
|
|
94
|
+
|
|
95
|
+
| Environment opt-in | RSpec metadata | Loader prerequisite / coverage |
|
|
96
|
+
| --- | --- | --- |
|
|
97
|
+
| `PWN_TEST_FFTW=1` | `fftw_integration` | `libfftw3f`; native impulse FFT |
|
|
98
|
+
| `PWN_TEST_VOLK=1` | `volk_integration` | `libvolk`; native conversion, accumulation, DSP dispatch |
|
|
99
|
+
| `PWN_TEST_RTL_SDR=1` | `rtl_sdr_integration` | `librtlsdr`; binding resolution only, no USB scan |
|
|
100
|
+
| `PWN_TEST_ADALM_PLUTO=1` | `adalm_pluto_integration` | Compatible `libiio`; library version only |
|
|
101
|
+
| `PWN_TEST_SOAPY_SDR=1` | `soapy_sdr_integration` | `libSoapySDR`; library/API version only |
|
|
102
|
+
| `PWN_TEST_HACK_RF=1` | `hack_rf_integration` | `libhackrf`; library version only |
|
|
103
|
+
|
|
104
|
+
For example:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
PWN_TEST_FFTW=1 bundle exec rspec spec/lib/pwn/ffi/fftw_spec.rb
|
|
108
|
+
PWN_TEST_VOLK=1 bundle exec rspec spec/lib/pwn/ffi/volk_spec.rb spec/lib/pwn/sdr/decoder/dsp_spec.rb
|
|
109
|
+
PWN_TEST_ADALM_PLUTO=1 bundle exec rspec spec/lib/pwn/ffi/adalm_pluto_spec.rb
|
|
110
|
+
PWN_TEST_LIQUID=1 bundle exec rspec spec/lib/pwn/ffi/liquid_spec.rb spec/lib/pwn/sdr/decoder/dsp_spec.rb
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Each opt-in fails with installation guidance if its library cannot load. Native
|
|
114
|
+
DSP integration tests assert real backend dispatch, not just a matching fallback.
|
|
115
|
+
None of these library opt-ins opens, tunes, receives from, or transmits through a
|
|
116
|
+
radio. Pluto URI discovery is mocked, including scan cleanup.
|
|
117
|
+
|
|
118
|
+
Actual RTL-SDR USB enumeration has a separate `rtl_sdr_hardware` tag, enabled
|
|
119
|
+
only by `PWN_TEST_RTL_SDR_HARDWARE=1`. It requires a connected RTL-SDR and USB
|
|
120
|
+
permissions, and fails on an empty inventory. Do not enable it in unattended
|
|
121
|
+
upgrade gates; library opt-ins do not enable hardware tests. It performs inventory
|
|
122
|
+
only, not tuning or reception. The test suite installs no dependencies.
|
|
123
|
+
|
|
63
124
|
## Adding a plugin
|
|
64
125
|
|
|
65
126
|
1. `lib/pwn/plugins/my_thing.rb` following the conventions above.
|
data/lib/pwn/version.rb
CHANGED
|
@@ -101,15 +101,57 @@ RSpec.describe 'PWN::AI::Agent::Verification' do
|
|
|
101
101
|
expect(report[:status]).to eq(:fail)
|
|
102
102
|
end
|
|
103
103
|
|
|
104
|
-
it 'executes opt-in test commands with bounded
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
expect(
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
104
|
+
it 'executes opt-in test commands with a bounded budget that allows interpreter startup' do
|
|
105
|
+
report = PWN::AI::Agent::Verification.run(
|
|
106
|
+
request: 'Run tests', requirements: ['Run tests'], root: @root, allow_commands: true, timeout: 10,
|
|
107
|
+
checks: [{ requirement: 'Run tests', kind: :command, argv: [RbConfig.ruby, '-e', 'puts "verified"'], expected: "verified\n" }]
|
|
108
|
+
)
|
|
109
|
+
expect(report[:status]).to eq(:pass)
|
|
110
|
+
expect(report[:checks].first).to include(passed: true, exit_code: 0, evidence: Digest::SHA256.hexdigest("verified\n"))
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
it 'allows simulated slow interpreter startup within the successful command budget' do
|
|
114
|
+
# Loading a startup file before -e models a slower host without mocking the runner.
|
|
115
|
+
startup = File.join(@root, 'startup.rb')
|
|
116
|
+
File.write(startup, 'sleep 0.3')
|
|
117
|
+
report = PWN::AI::Agent::Verification.run(
|
|
118
|
+
request: 'Run tests', requirements: ['Run tests'], root: @root, allow_commands: true, timeout: 10,
|
|
119
|
+
checks: [{ requirement: 'Run tests', kind: :command, argv: [RbConfig.ruby, '-r', startup, '-e', 'puts "verified"'], expected: "verified\n" }]
|
|
120
|
+
)
|
|
121
|
+
expect(report[:status]).to eq(:pass)
|
|
122
|
+
expect(report[:checks].first).to include(passed: true, exit_code: 0, evidence: Digest::SHA256.hexdigest("verified\n"))
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
it 'reports an actual unsuccessful command exit as failure' do
|
|
126
|
+
report = PWN::AI::Agent::Verification.run(
|
|
127
|
+
request: 'Run tests', requirements: ['Run tests'], root: @root, allow_commands: true, timeout: 10,
|
|
128
|
+
checks: [{ requirement: 'Run tests', kind: :command, argv: [RbConfig.ruby, '-e', 'exit 1'] }]
|
|
129
|
+
)
|
|
130
|
+
expect(report[:status]).to eq(:fail)
|
|
131
|
+
expect(report[:checks].first).to include(passed: false, exit_code: 1)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
it 'bounds an unfinished command and reports a timeout as unknown, not failure or success' do
|
|
135
|
+
# This command cannot finish within the deadline, regardless of startup speed.
|
|
136
|
+
report = Timeout.timeout(5) do
|
|
137
|
+
PWN::AI::Agent::Verification.run(
|
|
138
|
+
request: 'Run tests', requirements: ['Run tests'], root: @root, allow_commands: true, timeout: 0.2,
|
|
139
|
+
checks: [{ requirement: 'Run tests', kind: :command, argv: [RbConfig.ruby, '-e', 'sleep 60'] }]
|
|
140
|
+
)
|
|
141
|
+
end
|
|
142
|
+
expect(report[:status]).to eq(:unknown)
|
|
143
|
+
expect(report[:checks].first).to include(passed: nil, evidence: 'unavailable', error: 'Timeout::Error')
|
|
144
|
+
expect(report[:checks].first).not_to have_key(:exit_code)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
it 'does not spawn test commands without explicit opt-in' do
|
|
148
|
+
expect(Open3).not_to receive(:popen3)
|
|
149
|
+
report = PWN::AI::Agent::Verification.run(
|
|
150
|
+
request: 'Run tests', requirements: ['Run tests'], root: @root, allow_commands: false,
|
|
151
|
+
checks: [{ requirement: 'Run tests', kind: :command, argv: [RbConfig.ruby, '-e', 'puts "verified"'] }]
|
|
152
|
+
)
|
|
153
|
+
expect(report[:status]).to eq(:unknown)
|
|
154
|
+
expect(report[:checks].first).to include(passed: nil, error: 'ArgumentError')
|
|
113
155
|
end
|
|
114
156
|
|
|
115
157
|
it 'attributes a checked artifact only to its last observed writer rather than a later noop' do
|
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
require 'spec_helper'
|
|
4
4
|
|
|
5
5
|
describe PWN::FFI::AdalmPluto do
|
|
6
|
+
it 'handles a missing shared library without native calls' do
|
|
7
|
+
allow(described_class).to receive(:available?).and_return(false)
|
|
8
|
+
expect(described_class.info).to include(available: false)
|
|
9
|
+
expect(described_class.list_uris).to eq([])
|
|
10
|
+
end
|
|
11
|
+
|
|
6
12
|
it 'waits for a cancelled refill to return before destroying its buffer' do
|
|
7
13
|
handle = { buffer: FFI::MemoryPointer.new(:char, 4) }
|
|
8
14
|
entered = Queue.new
|
|
@@ -62,8 +68,8 @@ describe PWN::FFI::AdalmPluto do
|
|
|
62
68
|
expect(PWN::FFI::AdalmPluto).to respond_to :available?
|
|
63
69
|
end
|
|
64
70
|
|
|
65
|
-
it 'should report library info
|
|
66
|
-
|
|
71
|
+
it 'should report library info', :adalm_pluto_integration do
|
|
72
|
+
expect(described_class.available?).to be(true), 'Install compatible libiio and make it visible to the dynamic loader (PWN_TEST_ADALM_PLUTO=1).'
|
|
67
73
|
|
|
68
74
|
info = PWN::FFI::AdalmPluto.info
|
|
69
75
|
expect(info[:available]).to eq(true)
|
|
@@ -71,15 +77,23 @@ describe PWN::FFI::AdalmPluto do
|
|
|
71
77
|
expect(info[:minor]).to be_a(Integer)
|
|
72
78
|
end
|
|
73
79
|
|
|
74
|
-
it '
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
80
|
+
it 'maps URIs and releases the mocked scan without hardware discovery' do
|
|
81
|
+
allow(described_class).to receive(:available?).and_return(true)
|
|
82
|
+
scan = FFI::MemoryPointer.new(:char)
|
|
83
|
+
entry = FFI::MemoryPointer.new(:char)
|
|
84
|
+
entries = FFI::MemoryPointer.new(:pointer)
|
|
85
|
+
entries.write_pointer(entry)
|
|
86
|
+
expect(described_class).to receive(:iio_create_scan_context).with('usb,local', 0).and_return(scan)
|
|
87
|
+
expect(described_class).to receive(:iio_scan_context_get_info_list) do |context, output|
|
|
88
|
+
expect(context).to eq(scan)
|
|
89
|
+
output.write_pointer(entries)
|
|
90
|
+
1
|
|
91
|
+
end
|
|
92
|
+
allow(described_class).to receive(:iio_context_info_get_uri).with(entry).and_return('usb:mock')
|
|
93
|
+
allow(described_class).to receive(:iio_context_info_get_description).with(entry).and_return('Mock Pluto')
|
|
94
|
+
expect(described_class).to receive(:iio_context_info_list_free).with(entries)
|
|
95
|
+
expect(described_class).to receive(:iio_scan_context_destroy).with(scan)
|
|
96
|
+
expect(described_class.list_uris(backends: 'usb,local')).to eq([{ uri: 'usb:mock', description: 'Mock Pluto' }])
|
|
83
97
|
end
|
|
84
98
|
|
|
85
99
|
it 'should appear in PWN::FFI.backends' do
|
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
require 'spec_helper'
|
|
4
4
|
|
|
5
5
|
describe PWN::FFI::FFTW do
|
|
6
|
+
it 'handles a missing shared library without native calls' do
|
|
7
|
+
allow(described_class).to receive(:available?).and_return(false)
|
|
8
|
+
expect { described_class.rfft(samples: [1.0]) }.to raise_error(RuntimeError, /libfftw3f not available/)
|
|
9
|
+
end
|
|
10
|
+
|
|
6
11
|
it 'should display information for authors' do
|
|
7
12
|
expect(PWN::FFI::FFTW).to respond_to :authors
|
|
8
13
|
end
|
|
@@ -15,8 +20,8 @@ describe PWN::FFI::FFTW do
|
|
|
15
20
|
expect(PWN::FFI::FFTW).to respond_to :available?
|
|
16
21
|
end
|
|
17
22
|
|
|
18
|
-
it 'should compute an rfft impulse response
|
|
19
|
-
|
|
23
|
+
it 'should compute an rfft impulse response', :fftw_integration do
|
|
24
|
+
expect(described_class.available?).to be(true), 'Install libfftw3f and make it visible to the dynamic loader (PWN_TEST_FFTW=1).'
|
|
20
25
|
|
|
21
26
|
spec = PWN::FFI::FFTW.rfft(samples: [1.0, 0, 0, 0, 0, 0, 0, 0])
|
|
22
27
|
expect(spec.length).to eq(5) # n/2+1
|
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
require 'spec_helper'
|
|
4
4
|
|
|
5
5
|
describe PWN::FFI::HackRF do
|
|
6
|
+
it 'handles a missing shared library without native calls' do
|
|
7
|
+
allow(described_class).to receive(:available?).and_return(false)
|
|
8
|
+
expect(described_class.info).to include(available: false)
|
|
9
|
+
expect { described_class.open }.to raise_error(RuntimeError, /libhackrf not available/)
|
|
10
|
+
end
|
|
11
|
+
|
|
6
12
|
it 'releases the GVL while native stop waits for callbacks' do
|
|
7
13
|
source = File.read(File.expand_path('../../../../lib/pwn/ffi/hack_rf.rb', __dir__))
|
|
8
14
|
expect(source).to match(/attach_function :hackrf_stop_rx, \[:pointer\], :int, blocking: true/)
|
|
@@ -51,8 +57,8 @@ describe PWN::FFI::HackRF do
|
|
|
51
57
|
expect(PWN::FFI::HackRF).to respond_to :available?
|
|
52
58
|
end
|
|
53
59
|
|
|
54
|
-
it 'should report library info
|
|
55
|
-
|
|
60
|
+
it 'should report library info', :hack_rf_integration do
|
|
61
|
+
expect(described_class.available?).to be(true), 'Install libhackrf and make it visible to the dynamic loader (PWN_TEST_HACK_RF=1).'
|
|
56
62
|
|
|
57
63
|
info = PWN::FFI::HackRF.info
|
|
58
64
|
expect(info[:available]).to eq(true)
|
|
@@ -15,17 +15,33 @@ describe PWN::FFI::Liquid do
|
|
|
15
15
|
expect(PWN::FFI::Liquid).to respond_to :available?
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
it '
|
|
19
|
-
|
|
18
|
+
it 'rejects native DSP calls when libliquid is unavailable' do
|
|
19
|
+
allow(described_class).to receive(:available?).and_return(false)
|
|
20
|
+
expect { described_class.resample(samples: [0.0], rate: 0.5) }.to raise_error(RuntimeError, /libliquid not available/)
|
|
21
|
+
expect { described_class.freq_demod(iq: [1.0, 0.0]) }.to raise_error(RuntimeError, /libliquid not available/)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it 'keeps DSP resampling functional when libliquid is unavailable' do
|
|
25
|
+
allow(described_class).to receive(:available?).and_return(false)
|
|
26
|
+
allow(PWN::SDR::Decoder::DSP).to receive(:native).and_return(true)
|
|
27
|
+
expect(described_class).not_to receive(:resample)
|
|
28
|
+
result = PWN::SDR::Decoder::DSP.resample(samples: [0.0, 1.0, 2.0, 3.0], src_rate: 4, dst_rate: 2)
|
|
29
|
+
expect(result).to eq([0.0, 2.0])
|
|
30
|
+
end
|
|
20
31
|
|
|
32
|
+
before(:each, :liquid_integration) do
|
|
33
|
+
expect(described_class.available?).to be(true),
|
|
34
|
+
'PWN_TEST_LIQUID=1 requires liquid-dsp: install libliquid and make it visible to the dynamic loader. ' \
|
|
35
|
+
"Load error: #{described_class.load_error}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 'should resample at half rate with libliquid', :liquid_integration do
|
|
21
39
|
samples = Array.new(64) { |i| Math.sin(2 * Math::PI * i / 16.0) }
|
|
22
40
|
out = PWN::FFI::Liquid.resample(samples: samples, rate: 0.5)
|
|
23
41
|
expect(out.length).to be_between(24, 40)
|
|
24
42
|
end
|
|
25
43
|
|
|
26
|
-
it 'should FM-demod a complex tone
|
|
27
|
-
skip 'libliquid not installed' unless PWN::FFI::Liquid.available?
|
|
28
|
-
|
|
44
|
+
it 'should FM-demod a complex tone with libliquid', :liquid_integration do
|
|
29
45
|
iq = []
|
|
30
46
|
phase = 0.0
|
|
31
47
|
dphi = 2 * Math::PI * 0.05
|
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
require 'spec_helper'
|
|
4
4
|
|
|
5
5
|
describe PWN::FFI::RTLSdr do
|
|
6
|
+
it 'handles a missing shared library without native calls' do
|
|
7
|
+
allow(described_class).to receive(:available?).and_return(false)
|
|
8
|
+
expect { described_class.list_devices }.to raise_error(RuntimeError, /librtlsdr not available/)
|
|
9
|
+
expect { described_class.open }.to raise_error(RuntimeError, /librtlsdr not available/)
|
|
10
|
+
end
|
|
11
|
+
|
|
6
12
|
it 'releases the GVL during native reads so DSP workers can run' do
|
|
7
13
|
source = File.read(File.expand_path('../../../../lib/pwn/ffi/rtl_sdr.rb', __dir__))
|
|
8
14
|
declaration = source[/attach_function :rtlsdr_read_sync,.*?(?=attach_function)/m]
|
|
@@ -31,10 +37,27 @@ describe PWN::FFI::RTLSdr do
|
|
|
31
37
|
expect(PWN::FFI::RTLSdr).to respond_to :available?
|
|
32
38
|
end
|
|
33
39
|
|
|
34
|
-
it '
|
|
35
|
-
|
|
40
|
+
it 'maps device inventory using mocked USB calls' do
|
|
41
|
+
allow(described_class).to receive(:available?).and_return(true)
|
|
42
|
+
allow(described_class).to receive(:rtlsdr_get_device_count).and_return(1)
|
|
43
|
+
allow(described_class).to receive(:rtlsdr_get_device_name).with(0).and_return('Mock tuner')
|
|
44
|
+
expect(described_class).to receive(:rtlsdr_get_device_usb_strings) do |index, manufacturer, product, serial|
|
|
45
|
+
expect(index).to eq(0)
|
|
46
|
+
manufacturer.write_string('Test')
|
|
47
|
+
product.write_string('SDR')
|
|
48
|
+
serial.write_string('123')
|
|
49
|
+
0
|
|
50
|
+
end
|
|
51
|
+
expect(described_class.list_devices).to eq([{ index: 0, name: 'Mock tuner', manufacturer: 'Test', product: 'SDR', serial: '123' }])
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it 'loads the native inventory and read bindings without scanning USB', :rtl_sdr_integration do
|
|
55
|
+
expect(described_class.available?).to be(true), 'Install librtlsdr and make it visible to the dynamic loader (PWN_TEST_RTL_SDR=1).'
|
|
56
|
+
expect(described_class).to respond_to(:rtlsdr_get_device_count, :rtlsdr_read_sync)
|
|
57
|
+
end
|
|
36
58
|
|
|
37
|
-
|
|
38
|
-
expect(
|
|
59
|
+
it 'lists a connected RTL-SDR', :rtl_sdr_hardware do
|
|
60
|
+
expect(described_class.available?).to be(true), 'Install librtlsdr before enabling PWN_TEST_RTL_SDR_HARDWARE=1.'
|
|
61
|
+
expect(described_class.list_devices).not_to be_empty, 'Connect an RTL-SDR with USB permissions before enabling PWN_TEST_RTL_SDR_HARDWARE=1.'
|
|
39
62
|
end
|
|
40
63
|
end
|
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
require 'spec_helper'
|
|
4
4
|
|
|
5
5
|
describe PWN::FFI::SoapySDR do
|
|
6
|
+
it 'handles a missing shared library without native calls' do
|
|
7
|
+
allow(described_class).to receive(:available?).and_return(false)
|
|
8
|
+
expect(described_class.info).to include(available: false)
|
|
9
|
+
expect { described_class.make(args: 'driver=mock') }.to raise_error(RuntimeError, /libSoapySDR not available/)
|
|
10
|
+
end
|
|
11
|
+
|
|
6
12
|
it 'reports close failure and retains the stream for cleanup retry' do
|
|
7
13
|
handle = { device: :device, stream: :stream }
|
|
8
14
|
allow(described_class).to receive(:SoapySDRDevice_deactivateStream).and_return(0)
|
|
@@ -39,8 +45,8 @@ describe PWN::FFI::SoapySDR do
|
|
|
39
45
|
expect(PWN::FFI::SoapySDR).to respond_to :available?
|
|
40
46
|
end
|
|
41
47
|
|
|
42
|
-
it 'should report API info
|
|
43
|
-
|
|
48
|
+
it 'should report API info', :soapy_sdr_integration do
|
|
49
|
+
expect(described_class.available?).to be(true), 'Install libSoapySDR and make it visible to the dynamic loader (PWN_TEST_SOAPY_SDR=1).'
|
|
44
50
|
|
|
45
51
|
info = PWN::FFI::SoapySDR.info
|
|
46
52
|
expect(info[:available]).to eq(true)
|
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
require 'spec_helper'
|
|
4
4
|
|
|
5
5
|
describe PWN::FFI::Volk do
|
|
6
|
+
it 'handles a missing shared library without native calls' do
|
|
7
|
+
allow(described_class).to receive(:available?).and_return(false)
|
|
8
|
+
expect { described_class.unpack_s16le(data: '') }.to raise_error(RuntimeError, /libvolk not available/)
|
|
9
|
+
expect { described_class.accumulate(samples: []) }.to raise_error(RuntimeError, /libvolk not available/)
|
|
10
|
+
end
|
|
11
|
+
|
|
6
12
|
it 'should display information for authors' do
|
|
7
13
|
expect(PWN::FFI::Volk).to respond_to :authors
|
|
8
14
|
end
|
|
@@ -15,8 +21,8 @@ describe PWN::FFI::Volk do
|
|
|
15
21
|
expect(PWN::FFI::Volk).to respond_to :available?
|
|
16
22
|
end
|
|
17
23
|
|
|
18
|
-
it 'should unpack s16le to unit-range floats
|
|
19
|
-
|
|
24
|
+
it 'should unpack s16le to unit-range floats', :volk_integration do
|
|
25
|
+
expect(described_class.available?).to be(true), 'Install libvolk and make it visible to the dynamic loader (PWN_TEST_VOLK=1).'
|
|
20
26
|
|
|
21
27
|
raw = [0, 16_384, -16_384, 32_767].pack('s<*')
|
|
22
28
|
out = PWN::FFI::Volk.unpack_s16le(data: raw)
|
|
@@ -27,8 +33,8 @@ describe PWN::FFI::Volk do
|
|
|
27
33
|
expect(out[3]).to be_within(1e-3).of(1.0)
|
|
28
34
|
end
|
|
29
35
|
|
|
30
|
-
it 'should accumulate floats
|
|
31
|
-
|
|
36
|
+
it 'should accumulate floats', :volk_integration do
|
|
37
|
+
expect(described_class.available?).to be(true), 'Install libvolk and make it visible to the dynamic loader (PWN_TEST_VOLK=1).'
|
|
32
38
|
|
|
33
39
|
sum = PWN::FFI::Volk.accumulate(samples: [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0])
|
|
34
40
|
expect(sum).to be_within(1e-4).of(36.0)
|
|
@@ -5,20 +5,50 @@ require 'tmpdir'
|
|
|
5
5
|
require 'digest'
|
|
6
6
|
|
|
7
7
|
describe 'normalized binary analysis' do
|
|
8
|
-
it 'executes the actual radare2 read-only JSON interfaces
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
8
|
+
it 'executes the actual radare2 read-only JSON interfaces on a controlled binary', :radare2_integration do
|
|
9
|
+
expect(PWN::Plugins::BinaryAnalysis.available?(name: 'r2')).to be(true),
|
|
10
|
+
'PWN_TEST_RADARE2=1 requires radare2: install r2 and add it to PATH.'
|
|
11
|
+
expect(PWN::Plugins::BinaryAnalysis.available?(name: 'cc')).to be(true),
|
|
12
|
+
'PWN_TEST_RADARE2=1 requires a C compiler: install cc and add it to PATH.'
|
|
13
|
+
|
|
14
|
+
Dir.mktmpdir('pwn-radare2-spec-') do |dir|
|
|
15
|
+
path = File.join(dir, 'fixture')
|
|
16
|
+
source = File.join(dir, 'fixture.c')
|
|
17
|
+
File.write(source, "#include <stdio.h>\nint main(void) { return puts(\"pwn radare2 fixture\") < 0; }\n")
|
|
18
|
+
output, status = Open3.capture2e('cc', '-O0', '-g', '-o', path, source, chdir: dir)
|
|
19
|
+
expect(status.success?).to be(true), "Unable to compile radare2 fixture with cc: #{output}"
|
|
20
|
+
sha = Digest::SHA256.file(path).hexdigest
|
|
21
|
+
|
|
22
|
+
result = PWN::Plugins::Radare2.analyze_all(path: path)
|
|
23
|
+
expect(result).to include(backend: 'radare2', status: 'ok', sha256: sha)
|
|
24
|
+
expect(result[:functions]).not_to be_empty
|
|
25
|
+
expect(PWN::Plugins::Radare2.list_functions(path: path)[:data]).not_to be_empty
|
|
26
|
+
disassembly = PWN::Plugins::Radare2.disasm_function(path: path, function: 'main')
|
|
27
|
+
expect(disassembly).to include(backend: 'radare2', status: 'ok')
|
|
28
|
+
expect(disassembly[:data]).to include('ops')
|
|
29
|
+
expect(disassembly[:data]['ops']).not_to be_empty
|
|
30
|
+
xrefs = PWN::Plugins::Radare2.xrefs(path: path, addr: 'main')
|
|
31
|
+
expect(xrefs).to include(backend: 'radare2', status: 'ok')
|
|
32
|
+
expect(xrefs[:data]).to be_a(Array)
|
|
33
|
+
expect(PWN::Plugins::Radare2.strings(path: path)[:data]).not_to be_empty
|
|
34
|
+
expect(PWN::Plugins::Radare2.imports(path: path)[:data]).not_to be_empty
|
|
35
|
+
expect(Digest::SHA256.file(path).hexdigest).to eq(sha)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it 'rejects command injection without requiring a native backend' do
|
|
40
|
+
expect(PWN::Plugins::BinaryAnalysis).not_to receive(:run)
|
|
19
41
|
expect { PWN::Plugins::Radare2.disasm_function(path: '/bin/true', function: 'main;!id') }.to raise_error(ArgumentError)
|
|
20
42
|
end
|
|
21
43
|
|
|
44
|
+
it 'falls back deterministically when radare2 is unavailable' do
|
|
45
|
+
allow(PWN::Plugins::BinaryAnalysis).to receive(:available?).with(name: 'r2').and_return(false)
|
|
46
|
+
result = PWN::Plugins::Radare2.analyze_all(path: '/bin/true')
|
|
47
|
+
expect(result).to include(backend: 'binutils', status: 'degraded')
|
|
48
|
+
expect(result[:strings]).not_to be_empty
|
|
49
|
+
expect(result[:warnings]).not_to be_empty
|
|
50
|
+
end
|
|
51
|
+
|
|
22
52
|
it 'reports an honest Ghidra fallback and uses the requested hash cache directory' do
|
|
23
53
|
Dir.mktmpdir do |dir|
|
|
24
54
|
result = PWN::Plugins::GhidraHeadless.analyze(path: '/bin/true', backend: 'binutils', cache_dir: dir)
|
|
@@ -29,33 +29,58 @@ describe PWN::SDR::Decoder::DSP do
|
|
|
29
29
|
PWN::SDR::Decoder::DSP.native = was
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
-
it 'should unpack s16le via Volk
|
|
33
|
-
skip 'libvolk not installed' unless PWN::FFI.available?(mod: :Volk)
|
|
34
|
-
|
|
32
|
+
it 'should unpack s16le via Volk', :volk_integration do
|
|
35
33
|
was = PWN::SDR::Decoder::DSP.native
|
|
34
|
+
expect(PWN::FFI.available?(mod: :Volk)).to be(true), 'Install libvolk and make it visible to the dynamic loader (PWN_TEST_VOLK=1).'
|
|
36
35
|
PWN::SDR::Decoder::DSP.native = true
|
|
37
36
|
raw = [0, 16_384, -16_384, 32_767].pack('s<*')
|
|
37
|
+
native_out = PWN::FFI::Volk.unpack_s16le(data: raw)
|
|
38
|
+
expect(PWN::FFI::Volk).to receive(:unpack_s16le).with(data: raw).and_call_original
|
|
38
39
|
out = PWN::SDR::Decoder::DSP.unpack_s16le(data: raw)
|
|
40
|
+
expect(out).to eq(native_out)
|
|
39
41
|
expect(out[1]).to be_within(1e-3).of(0.5)
|
|
40
42
|
ensure
|
|
41
43
|
PWN::SDR::Decoder::DSP.native = was
|
|
42
44
|
end
|
|
43
45
|
|
|
44
|
-
it '
|
|
45
|
-
|
|
46
|
+
it 'falls back to Ruby unpacking with native enabled but Volk absent' do
|
|
47
|
+
was = described_class.native
|
|
48
|
+
described_class.native = true
|
|
49
|
+
allow(PWN::FFI).to receive(:available?).with(mod: :Volk).and_return(false)
|
|
50
|
+
expect(PWN::FFI::Volk).not_to receive(:unpack_s16le)
|
|
51
|
+
raw = [0, 16_384, -16_384, 32_767].pack('s<*')
|
|
52
|
+
expect(described_class.unpack_s16le(data: raw)).to eq([0.0, 0.5, -0.5, 32_767 / 32_768.0])
|
|
53
|
+
ensure
|
|
54
|
+
described_class.native = was
|
|
55
|
+
end
|
|
46
56
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
57
|
+
it 'falls back to Ruby resampling with native enabled but Liquid absent' do
|
|
58
|
+
was = described_class.native
|
|
59
|
+
samples = Array.new(200) { |i| Math.sin(2 * Math::PI * i / 40.0) }
|
|
60
|
+
described_class.native = false
|
|
61
|
+
ruby_out = described_class.resample(samples: samples, src_rate: 48_000, dst_rate: 24_000)
|
|
50
62
|
expect(ruby_out.length).to eq(100)
|
|
51
63
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
expect(liq_out.length).to be_between(90, 120)
|
|
57
|
-
end
|
|
64
|
+
allow(PWN::FFI).to receive(:available?).with(mod: :Liquid).and_return(false)
|
|
65
|
+
expect(PWN::FFI::Liquid).not_to receive(:resample)
|
|
66
|
+
described_class.native = true
|
|
67
|
+
expect(described_class.resample(samples: samples, src_rate: 48_000, dst_rate: 24_000)).to eq(ruby_out)
|
|
58
68
|
ensure
|
|
59
|
-
|
|
69
|
+
described_class.native = was
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it 'resamples using Liquid to a comparable length', :liquid_integration do
|
|
73
|
+
was = described_class.native
|
|
74
|
+
expect(PWN::FFI.available?(mod: :Liquid)).to be(true), 'Install libliquid and make it visible to the dynamic loader (PWN_TEST_LIQUID=1).'
|
|
75
|
+
samples = Array.new(200) { |i| Math.sin(2 * Math::PI * i / 40.0) }
|
|
76
|
+
described_class.native = true
|
|
77
|
+
native_out = PWN::FFI::Liquid.resample(samples: samples, rate: 0.5)
|
|
78
|
+
expect(PWN::FFI::Liquid).to receive(:resample).with(samples: samples, rate: 0.5).and_call_original
|
|
79
|
+
liq_out = described_class.resample(samples: samples, src_rate: 48_000, dst_rate: 24_000)
|
|
80
|
+
expect(liq_out).to eq(native_out)
|
|
81
|
+
# Multi-stage resampler length can differ due to delay.
|
|
82
|
+
expect(liq_out.length).to be_between(90, 120)
|
|
83
|
+
ensure
|
|
84
|
+
described_class.native = was
|
|
60
85
|
end
|
|
61
86
|
end
|
data/spec/spec_helper.rb
CHANGED
|
@@ -21,6 +21,12 @@ RSpec.configure do |config|
|
|
|
21
21
|
config.filter_run_excluding :local_embeddings unless ENV['PWN_TEST_EMBED_ENDPOINT'] && ENV['PWN_SQLITE_VEC_EXTENSION']
|
|
22
22
|
config.filter_run_excluding :proxmark3_integration unless ENV['PWN_TEST_PROXMARK3'] == '1'
|
|
23
23
|
config.filter_run_excluding :rtl433_integration unless ENV['PWN_TEST_RTL433'] == '1'
|
|
24
|
+
config.filter_run_excluding :liquid_integration unless ENV['PWN_TEST_LIQUID'] == '1'
|
|
25
|
+
config.filter_run_excluding :radare2_integration unless ENV['PWN_TEST_RADARE2'] == '1'
|
|
26
|
+
%w[fftw volk rtl_sdr adalm_pluto soapy_sdr hack_rf].each do |backend|
|
|
27
|
+
config.filter_run_excluding :"#{backend}_integration" unless ENV["PWN_TEST_#{backend.upcase}"] == '1'
|
|
28
|
+
end
|
|
29
|
+
config.filter_run_excluding :rtl_sdr_hardware unless ENV['PWN_TEST_RTL_SDR_HARDWARE'] == '1'
|
|
24
30
|
|
|
25
31
|
next if ENV['PWN_SPEC_VERBOSE']
|
|
26
32
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pwn
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.5.
|
|
4
|
+
version: 0.5.731
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- 0day Inc.
|
|
@@ -519,14 +519,14 @@ dependencies:
|
|
|
519
519
|
requirements:
|
|
520
520
|
- - '='
|
|
521
521
|
- !ruby/object:Gem::Version
|
|
522
|
-
version: 0.0.
|
|
522
|
+
version: 0.0.176
|
|
523
523
|
type: :development
|
|
524
524
|
prerelease: false
|
|
525
525
|
version_requirements: !ruby/object:Gem::Requirement
|
|
526
526
|
requirements:
|
|
527
527
|
- - '='
|
|
528
528
|
- !ruby/object:Gem::Version
|
|
529
|
-
version: 0.0.
|
|
529
|
+
version: 0.0.176
|
|
530
530
|
- !ruby/object:Gem::Dependency
|
|
531
531
|
name: metasm
|
|
532
532
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -547,14 +547,14 @@ dependencies:
|
|
|
547
547
|
requirements:
|
|
548
548
|
- - '='
|
|
549
549
|
- !ruby/object:Gem::Version
|
|
550
|
-
version: 2.
|
|
550
|
+
version: 2.26.0
|
|
551
551
|
type: :runtime
|
|
552
552
|
prerelease: false
|
|
553
553
|
version_requirements: !ruby/object:Gem::Requirement
|
|
554
554
|
requirements:
|
|
555
555
|
- - '='
|
|
556
556
|
- !ruby/object:Gem::Version
|
|
557
|
-
version: 2.
|
|
557
|
+
version: 2.26.0
|
|
558
558
|
- !ruby/object:Gem::Dependency
|
|
559
559
|
name: msfrpc-client
|
|
560
560
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -953,14 +953,14 @@ dependencies:
|
|
|
953
953
|
requirements:
|
|
954
954
|
- - '='
|
|
955
955
|
- !ruby/object:Gem::Version
|
|
956
|
-
version: 1.
|
|
956
|
+
version: 1.91.0
|
|
957
957
|
type: :runtime
|
|
958
958
|
prerelease: false
|
|
959
959
|
version_requirements: !ruby/object:Gem::Requirement
|
|
960
960
|
requirements:
|
|
961
961
|
- - '='
|
|
962
962
|
- !ruby/object:Gem::Version
|
|
963
|
-
version: 1.
|
|
963
|
+
version: 1.91.0
|
|
964
964
|
- !ruby/object:Gem::Dependency
|
|
965
965
|
name: rubocop-rake
|
|
966
966
|
requirement: !ruby/object:Gem::Requirement
|