pwn 0.5.620 → 0.5.621
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/README.md +1 -1
- data/documentation/Agent-Tool-Registry.md +2 -2
- data/documentation/Cron.md +13 -0
- data/documentation/Diagrams.md +1 -1
- data/documentation/Extrospection.md +71 -5
- data/documentation/Home.md +2 -2
- data/documentation/How-PWN-Works.md +1 -1
- data/documentation/Mistakes.md +2 -1
- data/documentation/Persistence.md +2 -1
- data/documentation/Skills-Memory-Learning.md +1 -0
- data/documentation/Transparent-Browser.md +18 -1
- data/documentation/diagrams/agent-tool-registry.svg +55 -54
- data/documentation/diagrams/ai-integration-tool-calling.svg +24 -24
- data/documentation/diagrams/aws-cloud-security.svg +25 -25
- data/documentation/diagrams/burp-vs-zap-preference.svg +12 -12
- data/documentation/diagrams/code-scanning-sast.svg +25 -25
- data/documentation/diagrams/cron-scheduling.svg +23 -23
- data/documentation/diagrams/dot/agent-tool-registry.dot +2 -2
- data/documentation/diagrams/dot/extrospection-world-awareness.dot +29 -8
- data/documentation/diagrams/dot/memory-skills-detailed.dot +4 -0
- data/documentation/diagrams/dot/overall-pwn-architecture.dot +2 -2
- data/documentation/diagrams/dot/persistence-filesystem.dot +2 -1
- data/documentation/diagrams/dot/pwn-ai-feedback-learning-loop.dot +10 -4
- data/documentation/diagrams/driver-framework.svg +13 -13
- data/documentation/diagrams/extrospection-world-awareness.svg +229 -99
- data/documentation/diagrams/fuzzing-workflow.svg +24 -24
- data/documentation/diagrams/hardware-hacking.svg +18 -18
- data/documentation/diagrams/history-to-drivers.svg +18 -18
- data/documentation/diagrams/memory-skills-detailed.svg +124 -101
- data/documentation/diagrams/mistakes-negative-feedback.svg +55 -55
- data/documentation/diagrams/network-infra-testing.svg +27 -27
- data/documentation/diagrams/overall-pwn-architecture.svg +85 -85
- data/documentation/diagrams/penetration-testing-workflow.svg +30 -30
- data/documentation/diagrams/persistence-filesystem.svg +94 -85
- data/documentation/diagrams/plugin-ecosystem.svg +35 -35
- data/documentation/diagrams/pwn-ai-feedback-learning-loop.svg +226 -180
- data/documentation/diagrams/pwn-repl-prototyping.svg +20 -20
- data/documentation/diagrams/reporting-pipeline.svg +18 -18
- data/documentation/diagrams/reverse-engineering-flow.svg +21 -21
- data/documentation/diagrams/sdr-radio-flow.svg +31 -31
- data/documentation/diagrams/sessions-cron-automation.svg +18 -18
- data/documentation/diagrams/swarm-multi-agent.svg +39 -39
- data/documentation/diagrams/web-application-testing.svg +26 -26
- data/documentation/diagrams/zero-day-research-flow.svg +25 -25
- data/documentation/pwn-ai-Agent.md +1 -1
- data/lib/pwn/ai/agent/extrospection.rb +433 -4
- data/lib/pwn/ai/agent/tools/extrospection.rb +88 -3
- data/lib/pwn/sdr/decoder/adsb.rb +14 -33
- data/lib/pwn/sdr/decoder/apt.rb +97 -36
- data/lib/pwn/sdr/decoder/base.rb +258 -171
- data/lib/pwn/sdr/decoder/bluetooth.rb +13 -20
- data/lib/pwn/sdr/decoder/dect.rb +13 -27
- data/lib/pwn/sdr/decoder/dsp.rb +396 -0
- data/lib/pwn/sdr/decoder/flex.rb +177 -210
- data/lib/pwn/sdr/decoder/gps.rb +12 -21
- data/lib/pwn/sdr/decoder/gsm.rb +21 -73
- data/lib/pwn/sdr/decoder/iridium.rb +11 -26
- data/lib/pwn/sdr/decoder/lora.rb +16 -23
- data/lib/pwn/sdr/decoder/lte.rb +11 -32
- data/lib/pwn/sdr/decoder/morse.rb +95 -21
- data/lib/pwn/sdr/decoder/p25.rb +19 -16
- data/lib/pwn/sdr/decoder/pager.rb +22 -38
- data/lib/pwn/sdr/decoder/pocsag.rb +175 -68
- data/lib/pwn/sdr/decoder/rfid.rb +24 -33
- data/lib/pwn/sdr/decoder/rtl433.rb +21 -30
- data/lib/pwn/sdr/decoder/rtty.rb +100 -27
- data/lib/pwn/sdr/decoder/wifi.rb +21 -42
- data/lib/pwn/sdr/decoder/zigbee.rb +18 -40
- data/lib/pwn/sdr/decoder.rb +8 -0
- data/lib/pwn/version.rb +1 -1
- data/spec/lib/pwn/sdr/decoder/dsp_spec.rb +15 -0
- data/third_party/pwn_rdoc.jsonl +24 -3
- metadata +3 -1
data/lib/pwn/sdr/decoder/base.rb
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'json'
|
|
4
|
-
require 'open3'
|
|
5
4
|
require 'tty-spinner'
|
|
6
5
|
require 'tty-screen'
|
|
7
6
|
require 'io/wait'
|
|
@@ -9,199 +8,205 @@ require 'io/wait'
|
|
|
9
8
|
module PWN
|
|
10
9
|
module SDR
|
|
11
10
|
module Decoder
|
|
12
|
-
# Shared pipeline plumbing for every
|
|
11
|
+
# Shared, 100 % Ruby-native pipeline plumbing for every
|
|
12
|
+
# PWN::SDR::Decoder::* module.
|
|
13
13
|
#
|
|
14
|
-
#
|
|
15
|
-
# PWN::SDR::Decoder::RDS:
|
|
14
|
+
# Two entry points, neither of which shell out to any external binary:
|
|
16
15
|
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
16
|
+
# run_native — Bind the GQRX 48 kHz s16le mono UDP audio tap, unpack
|
|
17
|
+
# the samples with PWN::SDR::Decoder::DSP, hand each
|
|
18
|
+
# chunk to a caller-supplied `demod:` object that
|
|
19
|
+
# responds to `#feed(samples, &emit)`. Every Hash the
|
|
20
|
+
# demodulator emits is merged with freq_obj, JSON-
|
|
21
|
+
# pretty-printed, JSONL-logged, and shown on the
|
|
22
|
+
# spinner. [ENTER] stops cleanly.
|
|
23
23
|
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
24
|
+
# run_detector — For protocols whose bit-rate/bandwidth cannot be
|
|
25
|
+
# recovered from a 48 kHz demodulated-audio tap (GSM,
|
|
26
|
+
# LTE, ADS-B, WiFi, LoRa, GPS, DECT, ZigBee, Bluetooth,
|
|
27
|
+
# Iridium, P25, ISM/RFID …). Pure-Ruby energy / burst
|
|
28
|
+
# characteriser: polls GQRX `l STRENGTH`, and (when the
|
|
29
|
+
# UDP tap is enabled) computes RMS-dBFS on the audio.
|
|
30
|
+
# Emits `{event: 'burst', dbfs:, duration_ms:}` frames
|
|
31
|
+
# whenever the signal crosses an adaptive threshold, so
|
|
32
|
+
# the operator still gets structured, logged intel
|
|
33
|
+
# without ANY external decoding binary.
|
|
28
34
|
module Base
|
|
29
35
|
# Supported Method Parameters::
|
|
30
|
-
# PWN::SDR::Decoder::Base.
|
|
31
|
-
# freq_obj:
|
|
32
|
-
# protocol:
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
# line_match: 'optional - Regexp/String a stdout line must match',
|
|
36
|
-
# parser: 'optional - Proc.new { |line| Hash } to structure a line',
|
|
37
|
-
# required_bins: 'optional - Array of executables that must exist on PATH',
|
|
38
|
-
# resample_hz: 'optional - sox output rate for decode_cmd (default 22050)'
|
|
36
|
+
# PWN::SDR::Decoder::Base.run_native(
|
|
37
|
+
# freq_obj: 'required - freq_obj Hash from PWN::SDR::GQRX.init_freq',
|
|
38
|
+
# protocol: 'required - short name for banner / log filename',
|
|
39
|
+
# demod: 'required - object responding to #feed(samples,&emit)',
|
|
40
|
+
# rate: 'optional - assumed UDP sample rate (default 48000)'
|
|
39
41
|
# )
|
|
40
42
|
|
|
41
|
-
public_class_method def self.
|
|
42
|
-
freq_obj
|
|
43
|
-
protocol
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
line_match = opts[:line_match]
|
|
47
|
-
parser = opts[:parser]
|
|
48
|
-
required_bins = Array(opts[:required_bins])
|
|
49
|
-
resample_hz = opts[:resample_hz] || 22_050
|
|
43
|
+
public_class_method def self.run_native(opts = {})
|
|
44
|
+
freq_obj = opts[:freq_obj]
|
|
45
|
+
protocol = opts[:protocol] || 'SIGNAL'
|
|
46
|
+
demod = opts[:demod]
|
|
47
|
+
rate = (opts[:rate] || 48_000).to_i
|
|
50
48
|
|
|
51
49
|
raise 'ERROR: :freq_obj is required' unless freq_obj.is_a?(Hash)
|
|
50
|
+
raise 'ERROR: :demod must respond to #feed' unless demod.respond_to?(:feed)
|
|
52
51
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
freq_obj = freq_obj.dup
|
|
57
|
-
freq_obj.delete(:gqrx_sock)
|
|
58
|
-
freq_obj.delete(:decoder_module)
|
|
59
|
-
|
|
60
|
-
skip_freq_char = "\n"
|
|
52
|
+
udp_ip = freq_obj[:udp_ip] || '127.0.0.1'
|
|
53
|
+
udp_port = freq_obj[:udp_port] || 7355
|
|
54
|
+
log_obj = strip_freq_obj(freq_obj: freq_obj)
|
|
61
55
|
|
|
62
|
-
puts JSON.pretty_generate(
|
|
63
|
-
puts "\n*** #{protocol} Decoder ***"
|
|
56
|
+
puts JSON.pretty_generate(log_obj)
|
|
57
|
+
puts "\n*** #{protocol} Decoder (ruby-native) ***"
|
|
64
58
|
puts 'Press [ENTER] to continue to next frequency...'
|
|
65
59
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
puts "[!] Missing required executable(s): #{missing.join(', ')}"
|
|
69
|
-
puts ' Install them and re-run, or press [ENTER] to skip.'
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
spinner = TTY::Spinner.new(
|
|
73
|
-
'[:spinner] :status',
|
|
74
|
-
format: :arrow_pulse,
|
|
75
|
-
clear: true,
|
|
76
|
-
hide_cursor: true
|
|
60
|
+
spinner, max_len = build_spinner(
|
|
61
|
+
banner: "INFO: Decoding #{protocol} on udp://#{udp_ip}:#{udp_port} @ #{rate} Hz (native)"
|
|
77
62
|
)
|
|
78
|
-
|
|
79
|
-
max_title_length = [TTY::Screen.width - spinner_overhead, 50].max
|
|
80
|
-
banner = "INFO: Decoding #{protocol} on udp://#{udp_ip}:#{udp_port} ..."
|
|
81
|
-
banner = "INFO: Decoding #{protocol} via `#{direct_cmd.to_s.split.first}` ..." if direct_cmd
|
|
82
|
-
banner = banner[0...max_title_length] if banner.length > max_title_length
|
|
83
|
-
spinner.update(status: banner)
|
|
84
|
-
spinner.auto_spin
|
|
85
|
-
|
|
86
|
-
log_file = "/tmp/#{protocol.downcase.gsub(/[^a-z0-9]+/, '_')}_decoder_#{Time.now.strftime('%Y%m%d')}.log"
|
|
87
|
-
|
|
88
|
-
udp_listener = nil
|
|
89
|
-
receiver_thread = nil
|
|
90
|
-
mm_stdin = mm_stdout = mm_stderr = mm_wait_thr = nil
|
|
63
|
+
log_file = log_path(protocol: protocol)
|
|
91
64
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
udp_listener = PWN::SDR::GQRX.listen_udp(udp_ip: udp_ip, udp_port: udp_port)
|
|
65
|
+
udp_listener = PWN::SDR::GQRX.listen_udp(udp_ip: udp_ip, udp_port: udp_port)
|
|
66
|
+
audio_q = Queue.new
|
|
67
|
+
current_line = 'Waiting for data frames...'
|
|
96
68
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
receiver_thread = Thread.new do
|
|
102
|
-
loop do
|
|
103
|
-
data, = udp_listener.recv(4096)
|
|
104
|
-
next unless data.to_s.bytesize.positive?
|
|
105
|
-
|
|
106
|
-
mm_stdin.write(data)
|
|
107
|
-
begin
|
|
108
|
-
mm_stdin.flush
|
|
109
|
-
rescue StandardError
|
|
110
|
-
nil
|
|
111
|
-
end
|
|
112
|
-
end
|
|
113
|
-
rescue IOError, Errno::EPIPE, Errno::ECONNRESET
|
|
114
|
-
nil
|
|
69
|
+
receiver_thread = Thread.new do
|
|
70
|
+
loop do
|
|
71
|
+
data, = udp_listener.recv(4096)
|
|
72
|
+
audio_q.push(data) if data.to_s.bytesize.positive?
|
|
115
73
|
end
|
|
74
|
+
rescue IOError, Errno::ECONNRESET, Errno::EBADF
|
|
75
|
+
nil
|
|
116
76
|
end
|
|
117
77
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
next if line_match && !match_line?(line: line, matcher: line_match)
|
|
130
|
-
|
|
131
|
-
dec_msg = { decoded_at: Time.now.strftime('%Y-%m-%d %H:%M:%S%z'), raw: line }
|
|
132
|
-
dec_msg.merge!(parser.call(line)) if parser.respond_to?(:call)
|
|
133
|
-
final_msg = freq_obj.merge(dec_msg)
|
|
134
|
-
|
|
135
|
-
spinner.stop
|
|
136
|
-
puts JSON.pretty_generate(final_msg)
|
|
137
|
-
spinner.auto_spin
|
|
138
|
-
|
|
139
|
-
File.open(log_file, 'a') { |f| f.puts("#{JSON.generate(final_msg)},") }
|
|
140
|
-
disp = dec_msg[:summary] || line
|
|
141
|
-
current_title = disp[0...max_title_length]
|
|
142
|
-
end
|
|
143
|
-
rescue IOError
|
|
144
|
-
break
|
|
145
|
-
end
|
|
78
|
+
decoder_thread = Thread.new do
|
|
79
|
+
emit = proc do |msg|
|
|
80
|
+
next unless msg.is_a?(Hash)
|
|
81
|
+
|
|
82
|
+
final = log_obj.merge(decoded_at: Time.now.strftime('%Y-%m-%d %H:%M:%S%z')).merge(msg)
|
|
83
|
+
spinner.stop
|
|
84
|
+
puts JSON.pretty_generate(final)
|
|
85
|
+
spinner.auto_spin
|
|
86
|
+
File.open(log_file, 'a') { |f| f.puts("#{JSON.generate(final)},") }
|
|
87
|
+
disp = (msg[:summary] || msg[:raw] || msg.values.compact.first).to_s
|
|
88
|
+
current_line = disp[0...max_len]
|
|
146
89
|
end
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
lvl = PWN::SDR::GQRX.cmd(gqrx_sock: gqrx_sock, cmd: 'l STRENGTH').to_f
|
|
152
|
-
current_title = "signal #{format('%+.1f', lvl)} dBFS (analog / no external demod)"
|
|
153
|
-
sleep 0.3
|
|
154
|
-
end
|
|
155
|
-
rescue StandardError
|
|
156
|
-
nil
|
|
157
|
-
end
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
loop do
|
|
161
|
-
spinner.update(status: current_title)
|
|
162
|
-
next unless $stdin.wait_readable(0)
|
|
163
|
-
|
|
164
|
-
begin
|
|
165
|
-
char = $stdin.read_nonblock(1)
|
|
166
|
-
next unless char == skip_freq_char
|
|
167
|
-
|
|
168
|
-
puts "\n[!] ENTER pressed → stopping #{protocol} decoder..."
|
|
169
|
-
break
|
|
170
|
-
rescue IO::WaitReadable, EOFError
|
|
171
|
-
nil
|
|
90
|
+
loop do
|
|
91
|
+
raw = audio_q.pop
|
|
92
|
+
samples = PWN::SDR::Decoder::DSP.unpack_s16le(data: raw)
|
|
93
|
+
demod.feed(samples, &emit)
|
|
172
94
|
end
|
|
95
|
+
rescue StandardError => e
|
|
96
|
+
current_line = "demod error: #{e.class}: #{e.message}"
|
|
173
97
|
end
|
|
174
98
|
|
|
99
|
+
wait_for_enter(spinner: spinner, title_ref: -> { current_line })
|
|
175
100
|
spinner.success('Decoding stopped')
|
|
176
101
|
rescue StandardError => e
|
|
177
|
-
spinner
|
|
102
|
+
spinner&.error("Decoding failed: #{e.message}") if defined?(spinner)
|
|
178
103
|
raise
|
|
179
104
|
ensure
|
|
180
|
-
[receiver_thread, decoder_thread].compact.each { |t| t.kill if t
|
|
181
|
-
[mm_stdin, mm_stdout, mm_stderr].compact.each do |io|
|
|
182
|
-
io.close
|
|
183
|
-
rescue StandardError
|
|
184
|
-
nil
|
|
185
|
-
end
|
|
186
|
-
begin
|
|
187
|
-
mm_wait_thr&.value
|
|
188
|
-
rescue StandardError
|
|
189
|
-
nil
|
|
190
|
-
end
|
|
105
|
+
[receiver_thread, decoder_thread].compact.each { |t| t.kill if t&.alive? }
|
|
191
106
|
PWN::SDR::GQRX.disconnect_udp(udp_listener: udp_listener) if udp_listener
|
|
192
|
-
spinner
|
|
107
|
+
spinner&.stop if defined?(spinner)
|
|
193
108
|
end
|
|
194
109
|
|
|
195
110
|
# Supported Method Parameters::
|
|
196
|
-
# PWN::SDR::Decoder::Base.
|
|
111
|
+
# PWN::SDR::Decoder::Base.run_detector(
|
|
112
|
+
# freq_obj: 'required - freq_obj Hash from PWN::SDR::GQRX.init_freq',
|
|
113
|
+
# protocol: 'required - short name for banner / log filename',
|
|
114
|
+
# note: 'optional - one-line explanation shown once',
|
|
115
|
+
# threshold: 'optional - dBFS above rolling floor to call a burst (default 8.0)',
|
|
116
|
+
# describe: 'optional - Proc.new { |burst_hash| Hash } extra fields'
|
|
117
|
+
# )
|
|
197
118
|
|
|
198
|
-
public_class_method def self.
|
|
199
|
-
|
|
200
|
-
|
|
119
|
+
public_class_method def self.run_detector(opts = {})
|
|
120
|
+
freq_obj = opts[:freq_obj]
|
|
121
|
+
protocol = opts[:protocol] || 'SIGNAL'
|
|
122
|
+
note = opts[:note]
|
|
123
|
+
threshold = (opts[:threshold] || 8.0).to_f
|
|
124
|
+
describe = opts[:describe]
|
|
201
125
|
|
|
202
|
-
|
|
203
|
-
|
|
126
|
+
raise 'ERROR: :freq_obj is required' unless freq_obj.is_a?(Hash)
|
|
127
|
+
|
|
128
|
+
gqrx_sock = freq_obj[:gqrx_sock]
|
|
129
|
+
udp_ip = freq_obj[:udp_ip] || '127.0.0.1'
|
|
130
|
+
udp_port = freq_obj[:udp_port] || 7355
|
|
131
|
+
log_obj = strip_freq_obj(freq_obj: freq_obj)
|
|
132
|
+
|
|
133
|
+
puts JSON.pretty_generate(log_obj)
|
|
134
|
+
puts "\n*** #{protocol} Signal Detector (ruby-native) ***"
|
|
135
|
+
puts "[i] #{note}" if note
|
|
136
|
+
puts 'Press [ENTER] to continue to next frequency...'
|
|
137
|
+
|
|
138
|
+
spinner, max_len = build_spinner(
|
|
139
|
+
banner: "INFO: Characterising #{protocol} activity on #{log_obj[:freq] || "udp://#{udp_ip}:#{udp_port}"} (native)"
|
|
140
|
+
)
|
|
141
|
+
log_file = log_path(protocol: protocol)
|
|
142
|
+
|
|
143
|
+
udp_listener = begin
|
|
144
|
+
PWN::SDR::GQRX.listen_udp(udp_ip: udp_ip, udp_port: udp_port)
|
|
145
|
+
rescue StandardError
|
|
146
|
+
nil
|
|
204
147
|
end
|
|
148
|
+
|
|
149
|
+
current_line = 'Establishing noise floor...'
|
|
150
|
+
floor = nil
|
|
151
|
+
in_burst = false
|
|
152
|
+
burst_t0 = nil
|
|
153
|
+
peak = -200.0
|
|
154
|
+
burst_n = 0
|
|
155
|
+
|
|
156
|
+
detector_thread = Thread.new do
|
|
157
|
+
emit = proc do |msg|
|
|
158
|
+
final = log_obj.merge(decoded_at: Time.now.strftime('%Y-%m-%d %H:%M:%S%z')).merge(msg)
|
|
159
|
+
spinner.stop
|
|
160
|
+
puts JSON.pretty_generate(final)
|
|
161
|
+
spinner.auto_spin
|
|
162
|
+
File.open(log_file, 'a') { |f| f.puts("#{JSON.generate(final)},") }
|
|
163
|
+
current_line = (msg[:summary] || '').to_s[0...max_len]
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
loop do
|
|
167
|
+
lvl = read_level(gqrx_sock: gqrx_sock, udp_listener: udp_listener)
|
|
168
|
+
if lvl
|
|
169
|
+
floor = floor.nil? ? lvl : ((floor * 0.98) + (lvl * 0.02))
|
|
170
|
+
delta = lvl - floor
|
|
171
|
+
if delta >= threshold
|
|
172
|
+
unless in_burst
|
|
173
|
+
in_burst = true
|
|
174
|
+
burst_t0 = Time.now
|
|
175
|
+
peak = lvl
|
|
176
|
+
end
|
|
177
|
+
peak = lvl if lvl > peak
|
|
178
|
+
current_line = format('%<p>s BURST %<l>+.1f dBFS (Δ%<d>+.1f, floor %<f>+.1f)', p: protocol, l: lvl, d: delta, f: floor)[0...max_len]
|
|
179
|
+
elsif in_burst
|
|
180
|
+
in_burst = false
|
|
181
|
+
burst_n += 1
|
|
182
|
+
dur_ms = ((Time.now - burst_t0) * 1000).round
|
|
183
|
+
msg = {
|
|
184
|
+
protocol: protocol, event: 'burst', burst_no: burst_n,
|
|
185
|
+
peak_dbfs: peak.round(1), floor_dbfs: floor.round(1),
|
|
186
|
+
delta_db: (peak - floor).round(1), duration_ms: dur_ms,
|
|
187
|
+
summary: format('%<p>s burst #%<n>d peak=%<pk>+.1f dBFS Δ=%<d>.1f dB dur=%<ms>d ms', p: protocol, n: burst_n, pk: peak, d: peak - floor, ms: dur_ms)
|
|
188
|
+
}
|
|
189
|
+
msg.merge!(describe.call(msg)) if describe.respond_to?(:call)
|
|
190
|
+
emit.call(msg)
|
|
191
|
+
else
|
|
192
|
+
current_line = format('%<p>s idle %<l>+.1f dBFS (floor %<f>+.1f, Δ%<d>+.1f)', p: protocol, l: lvl, f: floor, d: delta)[0...max_len]
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
sleep(udp_listener ? 0 : 0.1)
|
|
196
|
+
end
|
|
197
|
+
rescue StandardError => e
|
|
198
|
+
current_line = "detector error: #{e.class}: #{e.message}"
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
wait_for_enter(spinner: spinner, title_ref: -> { current_line })
|
|
202
|
+
spinner.success('Detector stopped')
|
|
203
|
+
rescue StandardError => e
|
|
204
|
+
spinner&.error("Detector failed: #{e.message}") if defined?(spinner)
|
|
205
|
+
raise
|
|
206
|
+
ensure
|
|
207
|
+
detector_thread&.kill if defined?(detector_thread) && detector_thread&.alive?
|
|
208
|
+
PWN::SDR::GQRX.disconnect_udp(udp_listener: udp_listener) if udp_listener
|
|
209
|
+
spinner&.stop if defined?(spinner)
|
|
205
210
|
end
|
|
206
211
|
|
|
207
212
|
# Supported Method Parameters::
|
|
@@ -218,6 +223,85 @@ module PWN
|
|
|
218
223
|
end
|
|
219
224
|
end
|
|
220
225
|
|
|
226
|
+
# ---------------------------------------------------------------
|
|
227
|
+
# Internals
|
|
228
|
+
# ---------------------------------------------------------------
|
|
229
|
+
|
|
230
|
+
# Supported Method Parameters::
|
|
231
|
+
# h = PWN::SDR::Decoder::Base.strip_freq_obj(freq_obj: {...})
|
|
232
|
+
|
|
233
|
+
private_class_method def self.strip_freq_obj(opts = {})
|
|
234
|
+
fo = opts[:freq_obj].dup
|
|
235
|
+
fo.delete(:gqrx_sock)
|
|
236
|
+
fo.delete(:decoder_module)
|
|
237
|
+
fo
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Supported Method Parameters::
|
|
241
|
+
# spinner, max_len = PWN::SDR::Decoder::Base.build_spinner(banner: '...')
|
|
242
|
+
|
|
243
|
+
private_class_method def self.build_spinner(opts = {})
|
|
244
|
+
banner = opts[:banner].to_s
|
|
245
|
+
spinner = TTY::Spinner.new('[:spinner] :status', format: :arrow_pulse, clear: true, hide_cursor: true)
|
|
246
|
+
overhead = 12
|
|
247
|
+
max_len = [TTY::Screen.width - overhead, 50].max
|
|
248
|
+
banner = banner[0...max_len] if banner.length > max_len
|
|
249
|
+
spinner.update(status: banner)
|
|
250
|
+
spinner.auto_spin
|
|
251
|
+
[spinner, max_len]
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# Supported Method Parameters::
|
|
255
|
+
# path = PWN::SDR::Decoder::Base.log_path(protocol: 'POCSAG')
|
|
256
|
+
|
|
257
|
+
private_class_method def self.log_path(opts = {})
|
|
258
|
+
protocol = opts[:protocol].to_s
|
|
259
|
+
"/tmp/#{protocol.downcase.gsub(/[^a-z0-9]+/, '_')}_decoder_#{Time.now.strftime('%Y%m%d')}.log"
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Supported Method Parameters::
|
|
263
|
+
# lvl = PWN::SDR::Decoder::Base.read_level(gqrx_sock:, udp_listener:)
|
|
264
|
+
|
|
265
|
+
private_class_method def self.read_level(opts = {})
|
|
266
|
+
gqrx_sock = opts[:gqrx_sock]
|
|
267
|
+
udp_listener = opts[:udp_listener]
|
|
268
|
+
if udp_listener
|
|
269
|
+
begin
|
|
270
|
+
data, = udp_listener.recv(4096)
|
|
271
|
+
return PWN::SDR::Decoder::DSP.rms_dbfs(samples: PWN::SDR::Decoder::DSP.unpack_s16le(data: data)) if data.to_s.bytesize.positive?
|
|
272
|
+
rescue IOError, Errno::ECONNRESET, Errno::EBADF
|
|
273
|
+
nil
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
return nil unless gqrx_sock
|
|
277
|
+
|
|
278
|
+
PWN::SDR::GQRX.cmd(gqrx_sock: gqrx_sock, cmd: 'l STRENGTH').to_f
|
|
279
|
+
rescue StandardError
|
|
280
|
+
nil
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# Supported Method Parameters::
|
|
284
|
+
# PWN::SDR::Decoder::Base.wait_for_enter(spinner:, title_ref:)
|
|
285
|
+
|
|
286
|
+
private_class_method def self.wait_for_enter(opts = {})
|
|
287
|
+
spinner = opts[:spinner]
|
|
288
|
+
title_ref = opts[:title_ref]
|
|
289
|
+
loop do
|
|
290
|
+
spinner.update(status: title_ref.call)
|
|
291
|
+
next unless $stdin.wait_readable(0)
|
|
292
|
+
|
|
293
|
+
begin
|
|
294
|
+
char = $stdin.read_nonblock(1)
|
|
295
|
+
next unless char == "\n"
|
|
296
|
+
|
|
297
|
+
puts "\n[!] ENTER pressed → stopping..."
|
|
298
|
+
break
|
|
299
|
+
rescue IO::WaitReadable, EOFError
|
|
300
|
+
nil
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
221
305
|
# Author(s):: 0day Inc. <support@0dayinc.com>
|
|
222
306
|
|
|
223
307
|
public_class_method def self.authors
|
|
@@ -227,19 +311,22 @@ module PWN
|
|
|
227
311
|
# Display Usage for this Module
|
|
228
312
|
|
|
229
313
|
public_class_method def self.help
|
|
230
|
-
puts "USAGE:
|
|
231
|
-
#{self}.
|
|
232
|
-
freq_obj:
|
|
233
|
-
protocol:
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
314
|
+
puts "USAGE (100 % ruby-native — no external binaries):
|
|
315
|
+
#{self}.run_native(
|
|
316
|
+
freq_obj: 'required - freq_obj from PWN::SDR::GQRX.init_freq',
|
|
317
|
+
protocol: 'required - short protocol name',
|
|
318
|
+
demod: 'required - object with #feed(samples,&emit)',
|
|
319
|
+
rate: 'optional - UDP sample rate (default 48000)'
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
#{self}.run_detector(
|
|
323
|
+
freq_obj: 'required - freq_obj from PWN::SDR::GQRX.init_freq',
|
|
324
|
+
protocol: 'required - short protocol name',
|
|
325
|
+
note: 'optional - explanation shown to operator',
|
|
326
|
+
threshold: 'optional - dB above floor for burst (default 8.0)',
|
|
327
|
+
describe: 'optional - Proc { |burst| Hash } extra fields'
|
|
240
328
|
)
|
|
241
329
|
|
|
242
|
-
#{self}.bin_available?(bin: 'multimon-ng')
|
|
243
330
|
#{self}.match_line?(line: str, matcher: Regexp|String|Array)
|
|
244
331
|
|
|
245
332
|
#{self}.authors
|
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'shellwords'
|
|
4
|
-
|
|
5
3
|
module PWN
|
|
6
4
|
module SDR
|
|
7
5
|
module Decoder
|
|
8
|
-
# Bluetooth Classic (BR/EDR) & BLE
|
|
6
|
+
# Pure-Ruby Bluetooth Classic (BR/EDR) & BLE activity detector.
|
|
9
7
|
#
|
|
10
|
-
# 1 Mbit/s GFSK with 79 (BR/EDR) or 40 (BLE) FHSS channels
|
|
11
|
-
#
|
|
12
|
-
# `
|
|
13
|
-
# advertising follow) directly and structures each output line.
|
|
8
|
+
# 1 Mbit/s GFSK with 79 (BR/EDR) or 40 (BLE) FHSS channels — native
|
|
9
|
+
# mode reports per-channel hop bursts and derives channel index from
|
|
10
|
+
# freq_obj. `parse_line` retained for offline text analysis.
|
|
14
11
|
module Bluetooth
|
|
15
12
|
# Supported Method Parameters::
|
|
16
13
|
# PWN::SDR::Decoder::Bluetooth.decode(
|
|
@@ -19,18 +16,17 @@ module PWN
|
|
|
19
16
|
|
|
20
17
|
public_class_method def self.decode(opts = {})
|
|
21
18
|
freq_obj = opts[:freq_obj]
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
hz = PWN::SDR.hz_to_i(freq: freq_obj[:freq])
|
|
24
20
|
ble = freq_obj[:ble] || freq_obj[:mode].to_s.casecmp('ble').zero?
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
PWN::SDR::Decoder::Base.run_pipeline(
|
|
21
|
+
ch = ble ? ((hz - 2_402_000_000) / 2_000_000).clamp(0, 39) : ((hz - 2_402_000_000) / 1_000_000).clamp(0, 78)
|
|
22
|
+
PWN::SDR::Decoder::Base.run_detector(
|
|
28
23
|
freq_obj: freq_obj,
|
|
29
24
|
protocol: ble ? 'BLE' : 'BT-BR/EDR',
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
25
|
+
note: '1 Mbit/s GFSK FHSS — native mode reports single-channel hop bursts only.',
|
|
26
|
+
threshold: 9.0,
|
|
27
|
+
describe: proc { |b|
|
|
28
|
+
{ modulation: 'GFSK', channel: ch, hop_slots: (b[:duration_ms] / 0.625).round, classification: ble && [37, 38, 39].include?(ch) ? 'BLE-advertising' : 'data-hop' }
|
|
29
|
+
}
|
|
34
30
|
)
|
|
35
31
|
end
|
|
36
32
|
|
|
@@ -59,14 +55,11 @@ module PWN
|
|
|
59
55
|
# Display Usage for this Module
|
|
60
56
|
|
|
61
57
|
public_class_method def self.help
|
|
62
|
-
puts "USAGE:
|
|
58
|
+
puts "USAGE (ruby-native detector, no external binaries):
|
|
63
59
|
#{self}.decode(
|
|
64
60
|
freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
|
|
65
61
|
)
|
|
66
62
|
|
|
67
|
-
NOTE: Requires an Ubertooth One and `ubertooth-rx` /
|
|
68
|
-
`ubertooth-btle`. Set freq_obj[:ble] = true for BLE mode.
|
|
69
|
-
|
|
70
63
|
#{self}.parse_line(line: 'systime=... ch=37 LAP=9e8b33 ...')
|
|
71
64
|
|
|
72
65
|
#{self}.authors
|
data/lib/pwn/sdr/decoder/dect.rb
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'shellwords'
|
|
4
|
-
|
|
5
3
|
module PWN
|
|
6
4
|
module SDR
|
|
7
5
|
module Decoder
|
|
8
|
-
# DECT (
|
|
9
|
-
#
|
|
10
|
-
# 1.152 Mbit/s GFSK across 10 TDMA carriers
|
|
11
|
-
#
|
|
12
|
-
#
|
|
6
|
+
# Pure-Ruby DECT (1.88–1.90 GHz EU / 1.92–1.93 GHz US) activity detector.
|
|
7
|
+
#
|
|
8
|
+
# 1.152 Mbit/s GFSK across 10 TDMA carriers — native mode reports
|
|
9
|
+
# slot bursts (417 μs) and derives carrier index from freq_obj.
|
|
10
|
+
# `parse_line` retained for offline text analysis.
|
|
13
11
|
module DECT
|
|
14
12
|
# Supported Method Parameters::
|
|
15
13
|
# PWN::SDR::Decoder::DECT.decode(
|
|
@@ -18,26 +16,17 @@ module PWN
|
|
|
18
16
|
|
|
19
17
|
public_class_method def self.decode(opts = {})
|
|
20
18
|
freq_obj = opts[:freq_obj]
|
|
21
|
-
raise 'ERROR: :freq_obj is required' unless freq_obj.is_a?(Hash)
|
|
22
|
-
|
|
23
19
|
hz = PWN::SDR.hz_to_i(freq: freq_obj[:freq])
|
|
24
|
-
# DECT carriers are 1.728 MHz apart; ch0 = 1 897.344 MHz (EU).
|
|
25
20
|
ch = ((1_897_344_000 - hz) / 1_728_000.0).round.clamp(0, 9)
|
|
26
|
-
|
|
27
|
-
direct_cmd =
|
|
28
|
-
if PWN::SDR::Decoder::Base.bin_available?(bin: 'dect_cli')
|
|
29
|
-
"dect_cli -s #{ch}"
|
|
30
|
-
else
|
|
31
|
-
"dectrx -c #{ch}"
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
PWN::SDR::Decoder::Base.run_pipeline(
|
|
21
|
+
PWN::SDR::Decoder::Base.run_detector(
|
|
35
22
|
freq_obj: freq_obj,
|
|
36
23
|
protocol: 'DECT',
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
24
|
+
note: '1.152 Mbit/s GFSK 24-slot TDMA — native mode reports slot bursts and carrier index.',
|
|
25
|
+
threshold: 7.0,
|
|
26
|
+
describe: proc { |b|
|
|
27
|
+
slots = (b[:duration_ms] / 0.417).round
|
|
28
|
+
{ modulation: 'GFSK', carrier: ch, tdma_slots: slots, classification: slots >= 24 ? 'FP-beacon-frame' : 'PP-burst' }
|
|
29
|
+
}
|
|
41
30
|
)
|
|
42
31
|
end
|
|
43
32
|
|
|
@@ -65,14 +54,11 @@ module PWN
|
|
|
65
54
|
# Display Usage for this Module
|
|
66
55
|
|
|
67
56
|
public_class_method def self.help
|
|
68
|
-
puts "USAGE:
|
|
57
|
+
puts "USAGE (ruby-native detector, no external binaries):
|
|
69
58
|
#{self}.decode(
|
|
70
59
|
freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
|
|
71
60
|
)
|
|
72
61
|
|
|
73
|
-
NOTE: Requires gr-dect2's `dect_cli` (or re-DECTed `dectrx`).
|
|
74
|
-
Owns the SDR directly.
|
|
75
|
-
|
|
76
62
|
#{self}.parse_line(line: 'RFPI: 01 23 45 67 89 slot 4 carrier 3')
|
|
77
63
|
|
|
78
64
|
#{self}.authors
|