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.
Files changed (74) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/documentation/Agent-Tool-Registry.md +2 -2
  4. data/documentation/Cron.md +13 -0
  5. data/documentation/Diagrams.md +1 -1
  6. data/documentation/Extrospection.md +71 -5
  7. data/documentation/Home.md +2 -2
  8. data/documentation/How-PWN-Works.md +1 -1
  9. data/documentation/Mistakes.md +2 -1
  10. data/documentation/Persistence.md +2 -1
  11. data/documentation/Skills-Memory-Learning.md +1 -0
  12. data/documentation/Transparent-Browser.md +18 -1
  13. data/documentation/diagrams/agent-tool-registry.svg +55 -54
  14. data/documentation/diagrams/ai-integration-tool-calling.svg +24 -24
  15. data/documentation/diagrams/aws-cloud-security.svg +25 -25
  16. data/documentation/diagrams/burp-vs-zap-preference.svg +12 -12
  17. data/documentation/diagrams/code-scanning-sast.svg +25 -25
  18. data/documentation/diagrams/cron-scheduling.svg +23 -23
  19. data/documentation/diagrams/dot/agent-tool-registry.dot +2 -2
  20. data/documentation/diagrams/dot/extrospection-world-awareness.dot +29 -8
  21. data/documentation/diagrams/dot/memory-skills-detailed.dot +4 -0
  22. data/documentation/diagrams/dot/overall-pwn-architecture.dot +2 -2
  23. data/documentation/diagrams/dot/persistence-filesystem.dot +2 -1
  24. data/documentation/diagrams/dot/pwn-ai-feedback-learning-loop.dot +10 -4
  25. data/documentation/diagrams/driver-framework.svg +13 -13
  26. data/documentation/diagrams/extrospection-world-awareness.svg +229 -99
  27. data/documentation/diagrams/fuzzing-workflow.svg +24 -24
  28. data/documentation/diagrams/hardware-hacking.svg +18 -18
  29. data/documentation/diagrams/history-to-drivers.svg +18 -18
  30. data/documentation/diagrams/memory-skills-detailed.svg +124 -101
  31. data/documentation/diagrams/mistakes-negative-feedback.svg +55 -55
  32. data/documentation/diagrams/network-infra-testing.svg +27 -27
  33. data/documentation/diagrams/overall-pwn-architecture.svg +85 -85
  34. data/documentation/diagrams/penetration-testing-workflow.svg +30 -30
  35. data/documentation/diagrams/persistence-filesystem.svg +94 -85
  36. data/documentation/diagrams/plugin-ecosystem.svg +35 -35
  37. data/documentation/diagrams/pwn-ai-feedback-learning-loop.svg +226 -180
  38. data/documentation/diagrams/pwn-repl-prototyping.svg +20 -20
  39. data/documentation/diagrams/reporting-pipeline.svg +18 -18
  40. data/documentation/diagrams/reverse-engineering-flow.svg +21 -21
  41. data/documentation/diagrams/sdr-radio-flow.svg +31 -31
  42. data/documentation/diagrams/sessions-cron-automation.svg +18 -18
  43. data/documentation/diagrams/swarm-multi-agent.svg +39 -39
  44. data/documentation/diagrams/web-application-testing.svg +26 -26
  45. data/documentation/diagrams/zero-day-research-flow.svg +25 -25
  46. data/documentation/pwn-ai-Agent.md +1 -1
  47. data/lib/pwn/ai/agent/extrospection.rb +433 -4
  48. data/lib/pwn/ai/agent/tools/extrospection.rb +88 -3
  49. data/lib/pwn/sdr/decoder/adsb.rb +14 -33
  50. data/lib/pwn/sdr/decoder/apt.rb +97 -36
  51. data/lib/pwn/sdr/decoder/base.rb +258 -171
  52. data/lib/pwn/sdr/decoder/bluetooth.rb +13 -20
  53. data/lib/pwn/sdr/decoder/dect.rb +13 -27
  54. data/lib/pwn/sdr/decoder/dsp.rb +396 -0
  55. data/lib/pwn/sdr/decoder/flex.rb +177 -210
  56. data/lib/pwn/sdr/decoder/gps.rb +12 -21
  57. data/lib/pwn/sdr/decoder/gsm.rb +21 -73
  58. data/lib/pwn/sdr/decoder/iridium.rb +11 -26
  59. data/lib/pwn/sdr/decoder/lora.rb +16 -23
  60. data/lib/pwn/sdr/decoder/lte.rb +11 -32
  61. data/lib/pwn/sdr/decoder/morse.rb +95 -21
  62. data/lib/pwn/sdr/decoder/p25.rb +19 -16
  63. data/lib/pwn/sdr/decoder/pager.rb +22 -38
  64. data/lib/pwn/sdr/decoder/pocsag.rb +175 -68
  65. data/lib/pwn/sdr/decoder/rfid.rb +24 -33
  66. data/lib/pwn/sdr/decoder/rtl433.rb +21 -30
  67. data/lib/pwn/sdr/decoder/rtty.rb +100 -27
  68. data/lib/pwn/sdr/decoder/wifi.rb +21 -42
  69. data/lib/pwn/sdr/decoder/zigbee.rb +18 -40
  70. data/lib/pwn/sdr/decoder.rb +8 -0
  71. data/lib/pwn/version.rb +1 -1
  72. data/spec/lib/pwn/sdr/decoder/dsp_spec.rb +15 -0
  73. data/third_party/pwn_rdoc.jsonl +24 -3
  74. metadata +3 -1
@@ -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 PWN::SDR::Decoder::* module.
11
+ # Shared, 100 % Ruby-native pipeline plumbing for every
12
+ # PWN::SDR::Decoder::* module.
13
13
  #
14
- # Encapsulates the pattern established by PWN::SDR::Decoder::Flex and
15
- # PWN::SDR::Decoder::RDS:
14
+ # Two entry points, neither of which shell out to any external binary:
16
15
  #
17
- # 1. Bind to the GQRX UDP audio stream (48 kHz s16le mono).
18
- # 2. Feed it through `sox` (resample to 22 050 Hz) into an external
19
- # demodulator (multimon-ng, rtl_433, acarsdec, dsd, ...).
20
- # 3. Read decoded lines back on a background thread, hand each to a
21
- # caller-supplied parser Proc, merge with freq_obj, JSON-log it.
22
- # 4. Show a TTY::Spinner status line and exit cleanly on [ENTER].
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
- # Decoders whose external tool needs raw I/Q direct from the SDR (ADSB,
25
- # GSM, LoRa, WiFi, ZigBee, ...) can instead pass `direct_cmd:` — Base
26
- # will spawn it stand-alone (no UDP sox bridge) and still handle the
27
- # spinner / stdout parser / logging / [ENTER]-to-skip loop uniformly.
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.run_pipeline(
31
- # freq_obj: 'required - freq_obj Hash from PWN::SDR::GQRX.init_freq',
32
- # protocol: 'required - short name used in banner / log filename',
33
- # decode_cmd: 'optional - shell pipeline reading 22 050 Hz s16le on stdin',
34
- # direct_cmd: 'optional - stand-alone shell cmd (owns the SDR itself)',
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.run_pipeline(opts = {})
42
- freq_obj = opts[:freq_obj]
43
- protocol = opts[:protocol] || 'SIGNAL'
44
- decode_cmd = opts[:decode_cmd]
45
- direct_cmd = opts[:direct_cmd]
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
- gqrx_sock = freq_obj[:gqrx_sock]
54
- udp_ip = freq_obj[:udp_ip] || '127.0.0.1'
55
- udp_port = freq_obj[:udp_port] || 7355
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(freq_obj)
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
- missing = required_bins.reject { |b| bin_available?(bin: b) }
67
- unless missing.empty?
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
- spinner_overhead = 12
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
- if direct_cmd && missing.empty?
93
- mm_stdin, mm_stdout, mm_stderr, mm_wait_thr = Open3.popen3(direct_cmd)
94
- elsif decode_cmd && missing.empty?
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
- full_cmd = 'sox -t raw -e signed-integer -b 16 -r 48000 -c 1 - ' \
98
- "-t raw -e signed-integer -b 16 -r #{resample_hz} -c 1 - | #{decode_cmd}"
99
- mm_stdin, mm_stdout, mm_stderr, mm_wait_thr = Open3.popen3(full_cmd)
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
- current_title = 'Waiting for data frames...'
119
- decoder_thread = nil
120
- if mm_stdout
121
- decoder_thread = Thread.new do
122
- buffer = ''
123
- loop do
124
- chunk = mm_stdout.readpartial(4096)
125
- buffer = "#{buffer}#{chunk}"
126
- while (line = buffer.slice!(/^.*\n/))
127
- line = line.chomp
128
- next if line.empty?
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
- elsif gqrx_sock
148
- # No external decoder — fall back to live signal-strength telemetry
149
- decoder_thread = Thread.new do
150
- loop do
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.error("Decoding failed: #{e.message}") if defined?(spinner) && 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.alive? }
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.stop if defined?(spinner) && spinner
107
+ spinner&.stop if defined?(spinner)
193
108
  end
194
109
 
195
110
  # Supported Method Parameters::
196
- # PWN::SDR::Decoder::Base.bin_available?(bin: 'multimon-ng')
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.bin_available?(opts = {})
199
- bin = opts[:bin].to_s
200
- return false if bin.empty?
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
- ENV.fetch('PATH', '').split(File::PATH_SEPARATOR).any? do |dir|
203
- File.executable?(File.join(dir, bin))
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}.run_pipeline(
232
- freq_obj: 'required - freq_obj from PWN::SDR::GQRX.init_freq',
233
- protocol: 'required - short protocol name',
234
- decode_cmd: 'optional - stdin pipeline (sox-resampled 22 050 Hz s16le)',
235
- direct_cmd: 'optional - stand-alone cmd that owns the SDR',
236
- line_match: 'optional - Regexp/String/Array line filter',
237
- parser: 'optional - Proc { |line| Hash } structured extractor',
238
- required_bins: 'optional - Array of executables that must be on PATH',
239
- resample_hz: 'optional - sox output rate (default 22050)'
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 advertising decoder for 2.402–2.480 GHz.
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 cannot be
11
- # recovered from GQRX audio. This module drives an Ubertooth One via
12
- # `ubertooth-rx` (LAP/UAP discovery) or `ubertooth-btle -f -p` (BLE
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
- raise 'ERROR: :freq_obj is required' unless freq_obj.is_a?(Hash)
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
- direct_cmd = ble ? 'ubertooth-btle -f -p' : 'ubertooth-rx -z'
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
- required_bins: [ble ? 'ubertooth-btle' : 'ubertooth-rx'],
31
- direct_cmd: direct_cmd,
32
- line_match: /(LAP|AdvA|ADV_|SCAN_|BD_ADDR|systime)/i,
33
- parser: proc { |line| parse_line(line: line) }
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
@@ -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 (Digital Enhanced Cordless Telecommunications) decoder for the
9
- # 1.880–1.900 GHz (EU) / 1.920–1.930 GHz (US DECT-6.0) allocation.
10
- # 1.152 Mbit/s GFSK across 10 TDMA carriers requires raw I/Q; this
11
- # module drives gr-dect2's `dect_cli` (or the `re-DECTed` toolkit)
12
- # directly and structures each RFPI/PP scan line.
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
- required_bins: %w[dect_cli],
38
- direct_cmd: direct_cmd,
39
- line_match: /(RFPI|FP|PP|slot|carrier|RSSI)/i,
40
- parser: proc { |line| parse_line(line: line) }
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