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,253 +1,220 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'json'
4
- require 'open3'
5
- require 'tty-spinner'
6
- require 'io/wait'
7
-
8
3
  module PWN
9
4
  module SDR
10
5
  module Decoder
11
- # Flex Decoder Module for Pagers
6
+ # Pure-Ruby FLEX™ pager decoder.
7
+ #
8
+ # FLEX is Motorola's synchronous paging protocol running at 1600,
9
+ # 3200 or 6400 bps in 2- or 4-level FSK. GQRX's NBFM discriminator
10
+ # audio (48 kHz UDP tap) is NRZ-sliced at 1600 baud (2-FSK mode),
11
+ # locked onto the Sync-1 A-word (bit-sync 0xAAAA + A1 = 0x78F3), then
12
+ # each 1.875 s frame's 88 × 32-bit BCH(31,21) codewords are walked to
13
+ # recover Frame-Info, Address and Vector/Message words. Alphanumeric
14
+ # (7-bit ASCII, 3 chars per 21-bit payload) and numeric (BCD) message
15
+ # bodies are extracted. No `multimon-ng`, no `sox` — 100 % Ruby.
16
+ #
17
+ # Limitation: 3200/6400 bps 4-FSK modes require raw discriminator
18
+ # amplitude quantisation into four levels; only frame/cycle/capcode
19
+ # metadata (not message body) is emitted for those speeds.
12
20
  module Flex
13
- # Supported Method Parameters::
14
- # pocsag_resp = PWN::SDR::Decoder::Flex.decode(
15
- # freq_obj: 'required - GQRX socket object returned from #connect method'
16
- # )
17
-
18
- public_class_method def self.decode(opts = {})
19
- freq_obj = opts[:freq_obj]
20
- gqrx_sock = freq_obj[:gqrx_sock]
21
- udp_ip = freq_obj[:udp_ip] || '127.0.0.1'
22
- udp_port = freq_obj[:udp_port] || 7355
23
-
24
- freq_obj.delete(:gqrx_sock)
25
-
26
- skip_freq_char = "\n"
27
-
28
- puts JSON.pretty_generate(freq_obj)
29
- puts 'Press [ENTER] to Continue to next frequency...'
30
-
31
- # Spinner setup with dynamic terminal width awareness
32
- spinner = TTY::Spinner.new(
33
- '[:spinner] :status',
34
- format: :arrow_pulse,
35
- clear: true,
36
- hide_cursor: true
37
- )
38
-
39
- spinner_overhead = 12
40
- max_title_length = [TTY::Screen.width - spinner_overhead, 50].max
41
-
42
- initial_title = "INFO: Decoding #{self.to_s.split('::').last} on udp://#{udp_ip}:#{udp_port} ..."
43
- initial_title = initial_title[0...max_title_length] if initial_title.length > max_title_length
44
- spinner.update(status: initial_title)
45
- spinner.auto_spin
46
-
47
- skip_freq = false
48
-
49
- # === Replace netcat with PWN::Plugins::Sock.listen ===
50
- udp_listener = PWN::SDR::GQRX.listen_udp(
51
- udp_ip: udp_ip,
52
- udp_port: udp_port
53
- )
21
+ BS1 = 0xAAAA
22
+ A1 = 0x78F3 # 1600 / 2-FSK Sync-1 A-word
23
+ A_TABLE = {
24
+ 0x870C => [1600, 2], 0x78F3 => [1600, 2],
25
+ 0xB068 => [3200, 2], 0x4F97 => [3200, 2],
26
+ 0x7B18 => [3200, 4], 0x84E7 => [3200, 4],
27
+ 0xDEA0 => [6400, 4], 0x215F => [6400, 4]
28
+ }.freeze
29
+ NUM_TABLE = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '/', 'U', ' ', '-', ']', '['].freeze
30
+
31
+ # Streaming FLEX demodulator fed by Base.run_native.
32
+ class Demod
33
+ def initialize(rate: 48_000)
34
+ @rate = rate
35
+ @buf = []
36
+ @inv = nil
37
+ @carry = []
38
+ end
54
39
 
55
- # Combined processing pipeline: sox → multimon-ng
56
- decode_cmd = '
57
- sox -t raw -e signed-integer -b 16 -r 48000 -c 1 - \
58
- -t raw -e signed-integer -b 16 -r 22050 -c 1 - | \
59
- multimon-ng -t raw \
60
- -a FLEX \
61
- -a FLEX_NEXT \
62
- -
63
- '
40
+ def feed(samples, &)
41
+ @buf.concat(samples)
42
+ max = (@rate * 4).to_i
43
+ @buf.shift(@buf.length - max) if @buf.length > max
44
+ return if @buf.length < (@rate * 2) # need ≥1 frame
64
45
 
65
- mm_stdin, mm_stdout, mm_stderr, mm_wait_thr = Open3.popen3(decode_cmd)
46
+ try_lock if @inv.nil?
47
+ return if @inv.nil?
66
48
 
67
- current_title = 'Waiting for data frames...'
49
+ bits = PWN::SDR::Decoder::DSP.nrz_slice(samples: @buf, rate: @rate, baud: 1600, invert: @inv)
50
+ @buf.clear
51
+ bits = @carry + bits
52
+ @carry = Flex.decode_bits(bits: bits, &)
53
+ end
68
54
 
69
- # Thread: read from UDP listener and feed to sox|multimon pipeline
70
- receiver_thread = Thread.new do
71
- begin
72
- loop do
73
- data, _sender = udp_listener.recv(4096)
74
- next unless data.to_s.bytesize.positive?
55
+ private
75
56
 
76
- mm_stdin.write(data)
77
- mm_stdin.flush rescue nil
78
- end
79
- rescue IOError, Errno::EPIPE, EOFError, Errno::ECONNRESET
80
- # normal exit path when shutting down
57
+ def try_lock
58
+ @inv = [false, true].find do |inv|
59
+ bits = PWN::SDR::Decoder::DSP.nrz_slice(samples: @buf, rate: @rate, baud: 1600, invert: inv)
60
+ PWN::SDR::Decoder::DSP.find_sync(bits: bits, pattern: (BS1 << 16) | A1, width: 32, max_err: 3)
81
61
  end
82
62
  end
63
+ end
83
64
 
84
- # Thread: read decoded output from multimon-ng and display it
85
- decoder_thread = Thread.new do
86
- # buffer = +''
87
- buffer = ''
88
-
89
- valid_types = %w[ALN BIN HEX NUM TON TONE UNK]
90
- loop do
91
- begin
92
- chunk = mm_stdout.readpartial(4096)
93
- # buffer << chunk
94
- buffer = "#{buffer}#{chunk}"
95
-
96
- while (line = buffer.slice!(/^.*\n/))
97
- line = line.chomp
98
- next if line.empty? || !line.start_with?('FLEX')
99
-
100
- decoded_at = Time.now.strftime('%Y-%m-%d %H:%M:%S%z')
101
- dec_msg = { decoded_at: decoded_at }
102
- dec_msg[:raw_inspected] = line.inspect
103
-
104
- protocol = line[0..8]
105
- protocol = 'FLEX' unless protocol == 'FLEX_NEXT'
106
- dec_msg[:protocol] = protocol
107
-
108
- # ────────────────────────────── Detect format ──────────────────────────────
109
- # Sometimes Flex is space delimited, sometimes pipe delimited
110
- # FLEX_NEXT appears to always be pipe delimited
111
-
112
- delimiter = '|'
113
- space_delim = false
114
- if line.start_with?('FLEX: ')
115
- delimiter = ' '
116
- space_delim = true
117
- end
118
-
119
- flex_pipe_delim = false
120
- flex_pipe_delim = true if line.start_with?('FLEX|')
121
-
122
- parts = line.split(delimiter)
123
-
124
- # protocol index already used
125
- idx_already_used = [0]
126
- target_parts_idx = 1
127
- target_parts_idx += 1 if flex_pipe_delim
128
- target_parts_idx += 2 if space_delim
129
- dec_msg[:speed] = parts[target_parts_idx] if parts[target_parts_idx]
130
- idx_already_used.push(target_parts_idx)
131
-
132
- target_parts_idx += 2
133
- dec_msg[:capcode] = parts[target_parts_idx] if parts[target_parts_idx]
134
- idx_already_used.push(target_parts_idx)
135
-
136
- target_parts_idx -= 1
137
- dec_msg[:capcode_loc] = parts[target_parts_idx] if parts[target_parts_idx]
138
- idx_already_used.push(target_parts_idx)
139
-
140
- while target_parts_idx < parts.size
141
- if idx_already_used.include?(target_parts_idx)
142
- target_parts_idx += 1
143
- next
144
- end
145
-
146
- key = parts[target_parts_idx]
147
- key = 'long_sequence_number' if key == 'LS'
148
-
149
- if key && valid_types.include?(key)
150
- dec_msg[:type] = key
151
-
152
- dec_msg[:type_desc] = case key
153
- when 'ALN'
154
- 'Human-readable text'
155
- when 'BIN'
156
- 'Binary / data payload (typically 32 bit words)'
157
- when 'HEX'
158
- 'Raw hex representation of data'
159
- when 'NUM'
160
- 'Numbers only'
161
- when 'TON', 'TONE'
162
- 'Just alert tone, no message'
163
- when 'UNK'
164
- 'Decoded but type unknown / unsupported format'
165
- end
166
-
167
- target_parts_idx += 1
168
- payload_parts = parts[target_parts_idx..]
169
- dec_msg[:type_payload] = payload_parts.join(delimiter)
170
-
171
- break
172
- else
173
- target_parts_idx += 1
174
- dec_msg[key] = parts[target_parts_idx] if parts[target_parts_idx]
175
- end
176
-
177
- idx_already_used.push(target_parts_idx)
178
- target_parts_idx += 1
179
- end
65
+ # Supported Method Parameters::
66
+ # carry = PWN::SDR::Decoder::Flex.decode_bits(bits: [...]) { |msg| ... }
180
67
 
181
- final_msg = freq_obj.merge(dec_msg)
182
- puts JSON.pretty_generate(final_msg)
183
- # TODO: Append dec_msg to a log file in a better way
184
- flex_log_file = "/tmp/flex_decoder_#{Time.now.strftime('%Y%m%d')}.log"
185
- File.open(flex_log_file, 'a') do |f|
186
- f.puts("#{JSON.generate(final_msg)},")
187
- end
188
- end
189
- rescue EOFError, IOError
190
- break
191
- end
68
+ public_class_method def self.decode_bits(opts = {})
69
+ bits = opts[:bits] || []
70
+ i = 0
71
+ loop do
72
+ # Sync-1 = 16-bit BS1 + 16-bit A + 16-bit B (=~A) + 16-bit ~A
73
+ idx = PWN::SDR::Decoder::DSP.find_sync(bits: bits, pattern: BS1, width: 16, max_err: 1, from: i)
74
+ break unless idx
75
+ break if idx + 144 > bits.length
76
+
77
+ a_word = PWN::SDR::Decoder::DSP.bits_to_int(bits: bits[idx + 16, 16])
78
+ speed = A_TABLE[a_word]
79
+ unless speed
80
+ i = idx + 1
81
+ next
82
+ end
83
+ # Frame Info Word follows Sync-1 (32-bit BCH codeword)
84
+ fiw = PWN::SDR::Decoder::DSP.bits_to_int(bits: bits[idx + 64, 32])
85
+ cycle = (fiw >> 4) & 0x0F
86
+ frame = (fiw >> 8) & 0x7F
87
+ block0 = idx + 64 + 32 + 16 # after FIW + Sync-2 marker
88
+ frame_bits = 1600 * 2 # ≈ one FLEX frame @ 1600 bps (upper bound)
89
+ avail = [bits.length - block0, frame_bits].min
90
+ words = []
91
+ (avail / 32).times do |w|
92
+ words << PWN::SDR::Decoder::DSP.bits_to_int(bits: bits[block0 + (w * 32), 32])
192
93
  end
94
+ emit_frame(words: words, cycle: cycle, frame: frame, speed: speed) { |m| yield m if block_given? }
95
+ i = block0 + avail
193
96
  end
97
+ tail_from = [bits.length - 3200, 0].max
98
+ bits[tail_from..] || []
99
+ end
194
100
 
195
- loop do
196
- spinner.update(status: current_title)
197
-
198
- # Non-blocking ENTER check to exit gracefully
199
- next unless $stdin.wait_readable(0)
200
-
201
- begin
202
- char = $stdin.read_nonblock(1)
203
- next unless char == skip_freq_char
204
-
205
- skip_freq = true
206
- puts "\n[!] ENTER pressed → stopping decoder..."
101
+ # Supported Method Parameters::
102
+ # PWN::SDR::Decoder::Flex.emit_frame(words:, cycle:, frame:, speed:) { |msg| ... }
103
+
104
+ public_class_method def self.emit_frame(opts = {})
105
+ words = opts[:words] || []
106
+ cycle = opts[:cycle]
107
+ frame = opts[:frame]
108
+ speed = opts[:speed] || [1600, 2]
109
+ return if words.empty?
110
+
111
+ # Block Info Word (word 0): a = address-start, v = vector-start
112
+ biw = words[0]
113
+ a_start = (biw >> 8) & 0x3F
114
+ v_start = (biw >> 2) & 0x3F
115
+ v_start = a_start + 1 if v_start <= a_start
116
+ addr_words = words[a_start...v_start] || []
117
+ addr_words.each_with_index do |aw, k|
118
+ next if aw.nil? || aw.zero?
119
+
120
+ capcode = (aw >> 11) & 0x1FFFFF
121
+ vec = words[v_start + k]
122
+ type = vec ? ((vec >> 4) & 0x7) : nil
123
+ mstart = vec ? ((vec >> 7) & 0x7F) : nil
124
+ mlen = vec ? ((vec >> 14) & 0x7F) : 0
125
+ mwords = mstart && mlen.positive? ? (words[mstart, mlen] || []) : []
126
+ body, tname =
127
+ case type
128
+ when 5 then [alpha_decode(words: mwords), 'ALN']
129
+ when 3 then [numeric_decode(words: mwords), 'NUM']
130
+ when 0, nil then [nil, 'TON']
131
+ else [hex_decode(words: mwords), 'BIN']
132
+ end
133
+ out = {
134
+ protocol: 'FLEX', speed: "#{speed[0]}/#{speed[1]}",
135
+ cycle: cycle, frame: frame,
136
+ capcode: capcode.to_s.rjust(9, '0'),
137
+ type: tname, type_payload: body
138
+ }.compact
139
+ out[:summary] = "FLEX RIC=#{out[:capcode]} #{tname}: #{body}"[0, 160]
140
+ yield out if block_given?
141
+ end
142
+ end
207
143
 
208
- break
209
- rescue IO::WaitReadable, EOFError
210
- # ignore
144
+ # Supported Method Parameters::
145
+ # str = PWN::SDR::Decoder::Flex.alpha_decode(words: [Integer, ...])
146
+
147
+ public_class_method def self.alpha_decode(opts = {})
148
+ words = opts[:words] || []
149
+ out = +''
150
+ words.each do |w|
151
+ payload = (w >> 11) & 0x1FFFFF
152
+ 3.times do |c|
153
+ ch = (payload >> (c * 7)) & 0x7F
154
+ next if ch < 0x20 || ch == 0x7F
155
+
156
+ out << ch.chr
211
157
  end
212
-
213
- break if skip_freq
214
158
  end
159
+ out.strip
160
+ end
215
161
 
216
- spinner.success('Decoding stopped')
217
- rescue StandardError => e
218
- spinner.error("Decoding failed: #{e.message}") if defined?(spinner)
219
- raise
220
- ensure
221
- # Cleanup
222
- [receiver_thread, decoder_thread].each do |thread|
223
- thread.kill if thread.alive?
162
+ # Supported Method Parameters::
163
+ # str = PWN::SDR::Decoder::Flex.numeric_decode(words: [Integer, ...])
164
+
165
+ public_class_method def self.numeric_decode(opts = {})
166
+ words = opts[:words] || []
167
+ out = +''
168
+ words.each do |w|
169
+ payload = (w >> 11) & 0x1FFFFF
170
+ 5.times { |d| out << NUM_TABLE[(payload >> (d * 4)) & 0xF] }
224
171
  end
172
+ out.strip
173
+ end
225
174
 
226
- [mm_stdin, mm_stdout, mm_stderr].each { |io| io.close rescue nil }
175
+ # Supported Method Parameters::
176
+ # str = PWN::SDR::Decoder::Flex.hex_decode(words: [Integer, ...])
227
177
 
228
- mm_wait_thr&.value rescue nil
178
+ public_class_method def self.hex_decode(opts = {})
179
+ words = opts[:words] || []
180
+ words.map { |w| format('%08X', w & 0xFFFFFFFF) }.join(' ')
181
+ end
229
182
 
230
- PWN::SDR::GQRX.disconnect_udp(udp_listener: udp_listener) if defined?(udp_listener) && udp_listener
183
+ # Supported Method Parameters::
184
+ # PWN::SDR::Decoder::Flex.decode(
185
+ # freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
186
+ # )
231
187
 
232
- spinner.stop if defined?(spinner) && spinner
188
+ public_class_method def self.decode(opts = {})
189
+ freq_obj = opts[:freq_obj]
190
+ PWN::SDR::Decoder::Base.run_native(
191
+ freq_obj: freq_obj,
192
+ protocol: 'FLEX',
193
+ demod: Demod.new
194
+ )
233
195
  end
234
196
 
235
197
  # Author(s):: 0day Inc. <support@0dayinc.com>
236
198
 
237
199
  public_class_method def self.authors
238
- "AUTHOR(S):
239
- 0day Inc. <support@0dayinc.com>
240
- "
200
+ "AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
241
201
  end
242
202
 
243
203
  # Display Usage for this Module
244
204
 
245
205
  public_class_method def self.help
246
- puts "USAGE:
206
+ puts "USAGE (ruby-native, no external binaries):
247
207
  #{self}.decode(
248
- freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq method'
208
+ freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
249
209
  )
250
210
 
211
+ #{self}.decode_bits(bits: [0,1,...]) { |msg| ... }
212
+ #{self}.alpha_decode(words: [Integer, ...])
213
+ #{self}.numeric_decode(words: [Integer, ...])
214
+
215
+ NOTE: Set GQRX to Narrow FM (~15 kHz). 1600 bps 2-FSK is fully
216
+ decoded; 3200/6400 4-FSK emit frame/capcode metadata only.
217
+
251
218
  #{self}.authors
252
219
  "
253
220
  end
@@ -1,14 +1,14 @@
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
- # GNSS / GPS L1-C/A (1575.42 MHz) & L2C (1227.60 MHz) decoder. BPSK/DSSS
9
- # at 1.023 / 10.23 Mcps needs raw I/Q, so this drives `gnss-sdr` (or
10
- # `rtl_gps`) directly and structures the PVT / NMEA lines it emits
11
- # (satellite PRNs acquired, C/N0, computed position fix, UTC time).
6
+ # Pure-Ruby GNSS / GPS L1-C/A activity detector.
7
+ #
8
+ # 1.023 Mcps BPSK/DSSS spread across 2 MHz acquisition/tracking of
9
+ # 32 PRNs in real time is not feasible in interpreted Ruby. Native
10
+ # mode reports L1 carrier presence / C/N₀ proxy only. `parse_line`
11
+ # retained for offline gnss-sdr / NMEA text analysis.
12
12
  module GPS
13
13
  # Supported Method Parameters::
14
14
  # PWN::SDR::Decoder::GPS.decode(
@@ -17,23 +17,17 @@ module PWN
17
17
 
18
18
  public_class_method def self.decode(opts = {})
19
19
  freq_obj = opts[:freq_obj]
20
- raise 'ERROR: :freq_obj is required' unless freq_obj.is_a?(Hash)
21
-
22
- conf = (freq_obj[:gnss_conf] || '/usr/share/gnss-sdr/conf/gnss-sdr_GPS_L1_rtlsdr_realtime.conf').to_s
23
- direct_cmd = "gnss-sdr --config_file=#{Shellwords.escape(conf)}"
24
-
25
- PWN::SDR::Decoder::Base.run_pipeline(
20
+ PWN::SDR::Decoder::Base.run_detector(
26
21
  freq_obj: freq_obj,
27
22
  protocol: 'GPS',
28
- required_bins: %w[gnss-sdr],
29
- direct_cmd: direct_cmd,
30
- line_match: /(PVT|Position|PRN|Tracking|\$G[PN])/,
31
- parser: proc { |line| parse_line(line: line) }
23
+ note: 'BPSK/DSSS 1.023 Mcps below thermal noise — native mode reports composite L1 energy only.',
24
+ threshold: 3.0,
25
+ describe: proc { |_b| { modulation: 'BPSK/DSSS', chip_rate: 1_023_000 } }
32
26
  )
33
27
  end
34
28
 
35
29
  # Supported Method Parameters::
36
- # PWN::SDR::Decoder::GPS.parse_line(line: 'Position at ... Lat = 32.7 [deg], Long = -97.1 ...')
30
+ # PWN::SDR::Decoder::GPS.parse_line(line: 'Position at ... Lat = 32.7 [deg] ...')
37
31
 
38
32
  public_class_method def self.parse_line(opts = {})
39
33
  line = opts[:line].to_s
@@ -64,14 +58,11 @@ module PWN
64
58
  # Display Usage for this Module
65
59
 
66
60
  public_class_method def self.help
67
- puts "USAGE:
61
+ puts "USAGE (ruby-native detector, no external binaries):
68
62
  #{self}.decode(
69
63
  freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
70
64
  )
71
65
 
72
- NOTE: Requires `gnss-sdr` and an active-antenna-biased front end.
73
- Override the receiver config via freq_obj[:gnss_conf].
74
-
75
66
  #{self}.parse_line(line: 'Position ... Lat = 32.7 Long = -97.1 Height = 210')
76
67
 
77
68
  #{self}.authors
@@ -1,38 +1,21 @@
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
- # GSM (2G) broadcast-channel decoder.
9
- #
10
- # GSM is 270.833 kbit/s GMSK — it CANNOT be recovered from GQRX's 48 kHz
11
- # demodulated-audio UDP tap. This module therefore drives the SDR
12
- # directly via `grgsm_livemon_headless` (from gr-gsm), which publishes
13
- # decoded Um bursts as GSMTAP on udp/4729, and reads them back with
14
- # `tshark` for structured field extraction (MCC/MNC/LAC/CI/ARFCN, paging
15
- # IMSIs/TMSIs, System Information messages, etc.).
16
- #
17
- # Interface matches PWN::SDR::Decoder::Flex / ::RDS so the GQRX
18
- # dispatcher (`decoder: :gsm`) works uniformly.
6
+ # Pure-Ruby GSM (2G) BCCH activity detector.
19
7
  #
20
- # NOTE: grgsm_livemon_headless opens the SDR hardware itself. If GQRX
21
- # already owns the device, pass a distinct `--args` string via
22
- # freq_obj[:sdr_args] (e.g. 'rtl=1' or 'hackrf=0') or stop GQRX's DSP
23
- # first (`U DSP 0`).
8
+ # GSM is 270.833 kbit/s GMSK across a 200 kHz channel — physically
9
+ # unrecoverable from a 48 kHz demodulated-audio tap and too fast for
10
+ # interpreted Ruby to demodulate from raw I/Q in real time. Rather
11
+ # than shell out to `grgsm_livemon_headless` + `tshark`, this module
12
+ # runs Base.run_detector to characterise BCCH/CCCH burst structure
13
+ # (577 μs slots × 8 = 4.615 ms TDMA frame) natively. `parse_line` is
14
+ # retained for offline GSMTAP-tshark pipe-delimited analysis.
24
15
  module GSM
25
16
  TSHARK_FIELDS = %w[
26
- frame.time
27
- gsmtap.arfcn
28
- gsmtap.chan_type
29
- gsm_a.imsi
30
- gsm_a.tmsi
31
- e212.mcc
32
- e212.mnc
33
- gsm_a.lac
34
- gsm_a.bssmap.cell_ci
35
- gsm_a.dtap.msg_rr_type
17
+ frame.time gsmtap.arfcn gsmtap.chan_type gsm_a.imsi gsm_a.tmsi
18
+ e212.mcc e212.mnc gsm_a.lac gsm_a.bssmap.cell_ci gsm_a.dtap.msg_rr_type
36
19
  ].freeze
37
20
 
38
21
  # Supported Method Parameters::
@@ -42,35 +25,15 @@ module PWN
42
25
 
43
26
  public_class_method def self.decode(opts = {})
44
27
  freq_obj = opts[:freq_obj]
45
- raise 'ERROR: :freq_obj is required' unless freq_obj.is_a?(Hash)
46
-
47
- hz = PWN::SDR.hz_to_i(freq: freq_obj[:freq])
48
- gain = (freq_obj[:rf_gain] || 40).to_s.to_f
49
- sdr_args = freq_obj[:sdr_args].to_s
50
- gsmtap = (freq_obj[:gsmtap_port] || 4729).to_i
51
-
52
- grgsm = ['grgsm_livemon_headless', '-f', hz.to_s, '-g', gain.to_s]
53
- grgsm.push('--args', sdr_args) unless sdr_args.empty?
54
-
55
- tshark = ['tshark', '-i', 'lo', '-l', '-n',
56
- '-f', "udp port #{gsmtap}",
57
- '-Y', 'gsmtap',
58
- '-T', 'fields', '-E', 'separator=|']
59
- TSHARK_FIELDS.each { |f| tshark.push('-e', f) }
60
-
61
- # bash -c '<grgsm> >/dev/null 2>&1 & pid=$!; trap ... ; <tshark>'
62
- inner = "#{Shellwords.join(grgsm)} >/dev/null 2>&1 & " \
63
- 'LMPID=$!; trap "kill $LMPID 2>/dev/null" EXIT INT TERM; ' \
64
- "sleep 2; exec #{Shellwords.join(tshark)}"
65
- direct_cmd = "bash -c #{Shellwords.escape(inner)}"
66
-
67
- PWN::SDR::Decoder::Base.run_pipeline(
28
+ PWN::SDR::Decoder::Base.run_detector(
68
29
  freq_obj: freq_obj,
69
30
  protocol: 'GSM',
70
- required_bins: %w[grgsm_livemon_headless tshark],
71
- direct_cmd: direct_cmd,
72
- line_match: /\S/,
73
- parser: proc { |line| parse_line(line: line) }
31
+ note: '270.833 kbit/s GMSK exceeds audio-tap Nyquist; native mode reports TDMA burst duty/energy only. Feed captured GSMTAP-tshark fields to .parse_line for MCC/MNC/LAC/CI/IMSI.',
32
+ threshold: 5.0,
33
+ describe: proc { |b|
34
+ frames = (b[:duration_ms] / 4.615).round
35
+ { modulation: 'GMSK', symbol_rate: 270_833, tdma_frames: frames, classification: frames > 10 ? 'BCCH/CCCH-continuous' : 'RACH/paging-burst' }
36
+ }
74
37
  )
75
38
  end
76
39
 
@@ -81,25 +44,15 @@ module PWN
81
44
  line = opts[:line].to_s
82
45
  f = line.split('|', -1)
83
46
  out = {
84
- protocol: 'GSM',
85
- frame_time: f[0],
86
- arfcn: f[1],
87
- chan_type: f[2],
88
- imsi: f[3],
89
- tmsi: f[4],
90
- mcc: f[5],
91
- mnc: f[6],
92
- lac: f[7],
93
- cell_id: f[8],
94
- rr_msg_type: f[9]
47
+ protocol: 'GSM', frame_time: f[0], arfcn: f[1], chan_type: f[2],
48
+ imsi: f[3], tmsi: f[4], mcc: f[5], mnc: f[6], lac: f[7],
49
+ cell_id: f[8], rr_msg_type: f[9]
95
50
  }.reject { |_, v| v.to_s.empty? }
96
-
97
51
  if out[:imsi].to_s.length.between?(14, 16)
98
52
  out[:imsi_mcc] = out[:imsi][0, 3]
99
53
  out[:imsi_mnc] = out[:imsi][3, 3]
100
54
  out[:imsi_msin] = out[:imsi][6..]
101
55
  end
102
-
103
56
  bits = []
104
57
  bits << "ARFCN=#{out[:arfcn]}" if out[:arfcn]
105
58
  bits << "MCC/MNC=#{out[:mcc]}/#{out[:mnc]}" if out[:mcc]
@@ -119,16 +72,11 @@ module PWN
119
72
  # Display Usage for this Module
120
73
 
121
74
  public_class_method def self.help
122
- puts "USAGE:
75
+ puts "USAGE (ruby-native detector, no external binaries):
123
76
  #{self}.decode(
124
77
  freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
125
78
  )
126
79
 
127
- NOTE: Requires `grgsm_livemon_headless` (gr-gsm) and `tshark`.
128
- GSM cannot be decoded from GQRX's 48 kHz audio tap; this
129
- module drives the SDR directly and reads GSMTAP on lo:4729.
130
- Set freq_obj[:sdr_args] (e.g. 'rtl=1') if GQRX owns device 0.
131
-
132
80
  #{self}.parse_line(line: 'ts|arfcn|chan|imsi|tmsi|mcc|mnc|lac|ci|rr')
133
81
 
134
82
  #{self}.authors