pwn 0.5.728 → 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 +91 -0
- data/lib/pwn/sdr/decoder/rfid.rb +10 -2
- data/lib/pwn/sdr/decoder/rtl433.rb +9 -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/lib/pwn/sdr/decoder/rfid_spec.rb +27 -2
- data/spec/lib/pwn/sdr/decoder/rtl433_spec.rb +27 -2
- data/spec/spec_helper.rb +8 -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'
|
|
@@ -30,6 +30,97 @@ rake # rubocop + rspec - must be zero offenses
|
|
|
30
30
|
|
|
31
31
|
(`rvmsudo rake` on multi-user RVM installs.)
|
|
32
32
|
|
|
33
|
+
### Optional native decoder integration tests
|
|
34
|
+
|
|
35
|
+
The default `bundle exec rake` does not require `proxmark3` or `rtl_433`.
|
|
36
|
+
Ruby EM4100/Acurite decoding, native input validation, missing-executable errors,
|
|
37
|
+
and cancellation-before-spawn tests always run. Only real native file replays
|
|
38
|
+
are excluded with RSpec metadata (`proxmark3_integration`, `rtl433_integration`),
|
|
39
|
+
not marked pending or silently passed inside examples.
|
|
40
|
+
|
|
41
|
+
After installing the optional clients, explicitly enable their fixture tests:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
PWN_TEST_PROXMARK3=1 bundle exec rspec spec/lib/pwn/sdr/decoder/rfid_spec.rb
|
|
45
|
+
PWN_TEST_RTL433=1 bundle exec rspec spec/lib/pwn/sdr/decoder/rtl433_spec.rb
|
|
46
|
+
# Enable both in the full gate:
|
|
47
|
+
PWN_TEST_PROXMARK3=1 PWN_TEST_RTL433=1 bundle exec rake
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
These tests use committed offline amplitude/IQ captures, not RF hardware.
|
|
51
|
+
Opt-in runs fail if their client is unavailable or cannot decode the reference;
|
|
52
|
+
installed versions can differ in supported protocols and output. Fixture provenance
|
|
53
|
+
and tested versions are recorded in `spec/fixtures/sdr/rfid/README.md` and
|
|
54
|
+
`spec/fixtures/sdr/rtl433/README.md`. Neither client is installed by the test suite.
|
|
55
|
+
|
|
56
|
+
At runtime, `RFID.decode(mode: :fdxb_pm3, ...)` and
|
|
57
|
+
`RTL433.decode(mode: :native, ...)` raise an actionable `IOError` when their
|
|
58
|
+
executable is missing, suggesting installation or an explicit `executable:` path.
|
|
59
|
+
An already-true `stop:` callback raises cancellation before spawning either client.
|
|
60
|
+
Invalid inputs still raise `ArgumentError`; native decoding never silently falls
|
|
61
|
+
back to another decoder or a detector.
|
|
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
|
+
|
|
33
124
|
## Adding a plugin
|
|
34
125
|
|
|
35
126
|
1. `lib/pwn/plugins/my_thing.rb` following the conventions above.
|
data/lib/pwn/sdr/decoder/rfid.rb
CHANGED
|
@@ -191,12 +191,19 @@ module PWN
|
|
|
191
191
|
data = File.read(path)
|
|
192
192
|
raise ArgumentError, 'PM3 file must contain signed integer amplitude samples' unless data.lines.all? { |line| line.strip.match?(/\A-?\d+\z/) }
|
|
193
193
|
|
|
194
|
+
raise IOError, 'proxmark3 replay cancelled' if opts[:stop]&.call
|
|
195
|
+
|
|
194
196
|
Tempfile.create(['pwn-fdxb-', '.pm3']) do |trace|
|
|
195
197
|
Tempfile.create('pwn-fdxb-output-') do |output|
|
|
196
198
|
trace.write(data)
|
|
197
199
|
trace.flush
|
|
198
|
-
|
|
199
|
-
|
|
200
|
+
executable = opts.fetch(:executable, 'proxmark3').to_s
|
|
201
|
+
begin
|
|
202
|
+
pid = Process.spawn(executable, '--incognito', '-c',
|
|
203
|
+
"data load -f #{trace.path}; lf fdxb demod", in: File::NULL, out: output, err: output)
|
|
204
|
+
rescue Errno::ENOENT
|
|
205
|
+
raise IOError, "proxmark3 executable #{executable.inspect} not found; install the optional proxmark3 client or set executable: to its path"
|
|
206
|
+
end
|
|
200
207
|
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + duration
|
|
201
208
|
begin
|
|
202
209
|
loop do
|
|
@@ -303,6 +310,7 @@ module PWN
|
|
|
303
310
|
# file: explicit .pm3 signed integer AMPLITUDE trace, not SDR IQ; no hardware access.
|
|
304
311
|
# Valid CRC16 required. Returns national/country ID, animal and data-block flags.
|
|
305
312
|
# executable: 'proxmark3'; duration: 30s deadline; stop: callable; output/on_frame/log_file.
|
|
313
|
+
# Missing executable raises IOError with install/path guidance; already-stopped replay never spawns.
|
|
306
314
|
# PM3 mode is finite offline replay; no ISO14443/ISO15693/EPC Gen2 coverage.
|
|
307
315
|
# carrier_hz: 125000 default; clocks_per_bit: 64 default, or 32/16.
|
|
308
316
|
# threshold: 0.5 default, normalized envelope amplitude; tune to the received levels.
|
|
@@ -206,10 +206,14 @@ module PWN
|
|
|
206
206
|
command.push('-R', '0')
|
|
207
207
|
ids.each { |id| command.push('-R', id.to_s) }
|
|
208
208
|
end
|
|
209
|
+
raise IOError, 'rtl_433 replay cancelled' if opts[:stop]&.call
|
|
210
|
+
|
|
209
211
|
count = 0
|
|
210
212
|
output = opts.fetch(:output, $stdout)
|
|
211
213
|
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + duration
|
|
214
|
+
started = false
|
|
212
215
|
Open3.popen3(*command) do |input, stdout, stderr, worker|
|
|
216
|
+
started = true
|
|
213
217
|
input.close
|
|
214
218
|
buffers = { stdout => +'', stderr => +'' }
|
|
215
219
|
errors = +''
|
|
@@ -269,6 +273,10 @@ module PWN
|
|
|
269
273
|
end
|
|
270
274
|
end
|
|
271
275
|
{ protocol: 'RTL433', mode: :native, backend: 'rtl_433', frames: count, source: path }
|
|
276
|
+
rescue Errno::ENOENT
|
|
277
|
+
raise unless started == false
|
|
278
|
+
|
|
279
|
+
raise IOError, "rtl_433 executable #{command.first.inspect} not found; install the optional rtl_433 package or set executable: to its path"
|
|
272
280
|
end
|
|
273
281
|
|
|
274
282
|
public_class_method def self.detect(opts = {})
|
|
@@ -333,6 +341,7 @@ module PWN
|
|
|
333
341
|
# Native mode is finite FILE replay only: file required, no auto RF/config loading.
|
|
334
342
|
# Native options: format: :cu8/:cs16/:cf32, protocols: [positive native IDs],
|
|
335
343
|
# executable: 'rtl_433', duration: 30 seconds (deadline), stop: callable.
|
|
344
|
+
# Missing executable raises IOError with install/path guidance; already-stopped replay never spawns.
|
|
336
345
|
# Output is per-device native JSON, device_protocol preserves native numeric ID.
|
|
337
346
|
# Individual sensors/integrity depend on installed version; mic absent means not-reported.
|
|
338
347
|
# sample_rate: 250000 default, >=20000; threshold: 0.5 normalized envelope amplitude.
|
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
|
|
@@ -3,7 +3,28 @@
|
|
|
3
3
|
require 'spec_helper'
|
|
4
4
|
|
|
5
5
|
describe PWN::SDR::Decoder::RFID do
|
|
6
|
-
it '
|
|
6
|
+
it 'reports an actionable missing native executable without emitting frames' do
|
|
7
|
+
Dir.mktmpdir('pwn-missing-backend-') do |dir|
|
|
8
|
+
executable = File.join(dir, 'proxmark3')
|
|
9
|
+
frames = []
|
|
10
|
+
output = StringIO.new
|
|
11
|
+
expect do
|
|
12
|
+
described_class.decode(mode: :fdxb_pm3, file: 'spec/fixtures/sdr/rfid/fdxb_animal.pm3',
|
|
13
|
+
executable: executable, output: output, on_frame: ->(frame) { frames << frame })
|
|
14
|
+
end.to raise_error(IOError, /proxmark3.*not found.*install.*executable:/i)
|
|
15
|
+
expect(frames).to be_empty
|
|
16
|
+
expect(output.string).to be_empty
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it 'cancels an already stopped native replay before spawning' do
|
|
21
|
+
expect(Process).not_to receive(:spawn)
|
|
22
|
+
expect do
|
|
23
|
+
described_class.decode(mode: :fdxb_pm3, file: 'spec/fixtures/sdr/rfid/fdxb_animal.pm3', stop: -> { true })
|
|
24
|
+
end.to raise_error(IOError, /cancelled/)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it 'decodes independent ISO11784/11785 FDX-B amplitude captures with the native offline client', :proxmark3_integration do
|
|
7
28
|
%w[animal extended].each do |variant|
|
|
8
29
|
frames = []
|
|
9
30
|
result = described_class.decode(mode: :fdxb_pm3, file: "spec/fixtures/sdr/rfid/fdxb_#{variant}.pm3",
|
|
@@ -17,12 +38,16 @@ describe PWN::SDR::Decoder::RFID do
|
|
|
17
38
|
end
|
|
18
39
|
end
|
|
19
40
|
|
|
20
|
-
it 'rejects automatic RF and
|
|
41
|
+
it 'rejects automatic RF and supports cancellation without the native client' do
|
|
21
42
|
allow(PWN::SDR::Decoder::Base).to receive(:run_iq).and_raise('unexpected automatic RF')
|
|
22
43
|
expect { described_class.decode(mode: :fdxb_pm3) }.to raise_error(ArgumentError, /explicit/)
|
|
23
44
|
path = 'spec/fixtures/sdr/rfid/fdxb_animal.pm3'
|
|
24
45
|
expect { described_class.decode(mode: :fdxb_pm3, file: path, source: :rtl_sdr) }.to raise_error(ArgumentError, /live/)
|
|
25
46
|
expect { described_class.decode(mode: :fdxb_pm3, file: path, stop: -> { true }) }.to raise_error(IOError, /cancelled/)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it 'emits no identity for an incomplete native amplitude trace', :proxmark3_integration do
|
|
50
|
+
path = 'spec/fixtures/sdr/rfid/fdxb_animal.pm3'
|
|
26
51
|
Tempfile.create(['fdxb-short-', '.pm3']) do |file|
|
|
27
52
|
file.write(File.readlines(path).first(100).join)
|
|
28
53
|
file.flush
|
|
@@ -3,6 +3,27 @@
|
|
|
3
3
|
require 'spec_helper'
|
|
4
4
|
|
|
5
5
|
describe PWN::SDR::Decoder::RTL433 do
|
|
6
|
+
it 'reports an actionable missing native executable without emitting frames' do
|
|
7
|
+
Dir.mktmpdir('pwn-missing-backend-') do |dir|
|
|
8
|
+
executable = File.join(dir, 'rtl_433')
|
|
9
|
+
frames = []
|
|
10
|
+
output = StringIO.new
|
|
11
|
+
expect do
|
|
12
|
+
described_class.decode(mode: :native, file: 'spec/fixtures/sdr/rtl433/toyota.cu8',
|
|
13
|
+
executable: executable, output: output, on_frame: ->(frame) { frames << frame })
|
|
14
|
+
end.to raise_error(IOError, /rtl_433.*not found.*install.*executable:/i)
|
|
15
|
+
expect(frames).to be_empty
|
|
16
|
+
expect(output.string).to be_empty
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it 'cancels an already stopped native replay before spawning' do
|
|
21
|
+
expect(Open3).not_to receive(:popen3)
|
|
22
|
+
expect do
|
|
23
|
+
described_class.decode(mode: :native, file: 'spec/fixtures/sdr/rtl433/toyota.cu8', stop: -> { true })
|
|
24
|
+
end.to raise_error(IOError, /cancelled/)
|
|
25
|
+
end
|
|
26
|
+
|
|
6
27
|
it 'decodes six real over-air Acurite packets with payloads matching the upstream reference' do
|
|
7
28
|
demod = described_class::AcuriteIQ.new(rate: 250_000)
|
|
8
29
|
frames = []
|
|
@@ -30,7 +51,7 @@ describe PWN::SDR::Decoder::RTL433 do
|
|
|
30
51
|
expect { described_class.decode(mode: :all) }.to raise_error(ArgumentError, /mode/)
|
|
31
52
|
end
|
|
32
53
|
|
|
33
|
-
it 'replays OOK and FSK native catalogue decoders against published captures' do
|
|
54
|
+
it 'replays OOK and FSK native catalogue decoders against published captures', :rtl433_integration do
|
|
34
55
|
%w[acurite_th_609_001 wh31 toyota].each do |name|
|
|
35
56
|
frames = []
|
|
36
57
|
result = described_class.decode(mode: :native, file: "spec/fixtures/sdr/rtl433/#{name}.cu8",
|
|
@@ -44,7 +65,7 @@ describe PWN::SDR::Decoder::RTL433 do
|
|
|
44
65
|
end
|
|
45
66
|
end
|
|
46
67
|
|
|
47
|
-
it 'requires explicit native files
|
|
68
|
+
it 'requires explicit native files and validates formats and protocol selection without the native client' do
|
|
48
69
|
allow(PWN::SDR::Decoder::Base).to receive(:run_iq).and_raise('unexpected automatic RF')
|
|
49
70
|
expect { described_class.decode(mode: :native) }.to raise_error(ArgumentError, /explicit file/)
|
|
50
71
|
path = 'spec/fixtures/sdr/rtl433/toyota.cu8'
|
|
@@ -52,6 +73,10 @@ describe PWN::SDR::Decoder::RTL433 do
|
|
|
52
73
|
expect { described_class.decode(mode: :native, file: path, source: :rtl_sdr) }.to raise_error(ArgumentError, /live source/)
|
|
53
74
|
expect { described_class.decode(mode: :native, file: path, protocols: [0]) }.to raise_error(ArgumentError, /protocol/)
|
|
54
75
|
expect { described_class.decode(mode: :native, file: path, stop: -> { true }) }.to raise_error(IOError, /cancelled/)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it 'supports native protocol selection', :rtl433_integration do
|
|
79
|
+
path = 'spec/fixtures/sdr/rtl433/toyota.cu8'
|
|
55
80
|
expect(described_class.decode(mode: :native, file: path, protocols: [11], output: StringIO.new)[:frames]).to eq(0)
|
|
56
81
|
expect(described_class.decode(mode: :native, file: path, protocols: [88], output: StringIO.new)[:frames]).to eq(1)
|
|
57
82
|
end
|
data/spec/spec_helper.rb
CHANGED
|
@@ -19,6 +19,14 @@ Dir[File.join(__dir__, 'support', '**', '*.rb')].each { |f| require f }
|
|
|
19
19
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
20
20
|
RSpec.configure do |config|
|
|
21
21
|
config.filter_run_excluding :local_embeddings unless ENV['PWN_TEST_EMBED_ENDPOINT'] && ENV['PWN_SQLITE_VEC_EXTENSION']
|
|
22
|
+
config.filter_run_excluding :proxmark3_integration unless ENV['PWN_TEST_PROXMARK3'] == '1'
|
|
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'
|
|
22
30
|
|
|
23
31
|
next if ENV['PWN_SPEC_VERBOSE']
|
|
24
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
|