pwn 0.5.510 → 0.5.512
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 +8 -8
- data/README.md +3 -3
- data/bin/pwn_gqrx_scanner +58 -83
- data/{build_pwn_gem.sh → build_gem.sh} +1 -1
- data/{git_commit_test_reinit_gem.sh → git_commit.sh} +4 -4
- data/lib/pwn/sdr/decoder/gsm.rb +24 -35
- data/lib/pwn/sdr/frequency_allocation.rb +81 -100
- data/lib/pwn/sdr/gqrx.rb +287 -119
- data/lib/pwn/version.rb +1 -1
- data/packer/provisioners/pwn.sh +1 -1
- data/third_party/pwn_rdoc.jsonl +5 -3
- data/{update_pwn.sh → upgrade_pwn.sh} +1 -1
- data/upgrade_ruby.sh +2 -2
- data/vagrant/provisioners/pwn.sh +1 -1
- metadata +23 -23
- /data/{reinstall_pwn_gemset.sh → reinstall_gemset.sh} +0 -0
- /data/{find_latest_gem_versions_per_Gemfile.sh → upgrade_Gemfile_gems.sh} +0 -0
data/lib/pwn/sdr/gqrx.rb
CHANGED
|
@@ -30,6 +30,43 @@ module PWN
|
|
|
30
30
|
end
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
+
# Supported Method Parameters::
|
|
34
|
+
# scan_resp = PWN::SDR::GQRX.log_signals(
|
|
35
|
+
# signals_arr: 'required - Array of detected signals',
|
|
36
|
+
# timestamp_start: 'required - Scan start timestamp',
|
|
37
|
+
# scan_log: 'required - Path to save detected signals log'
|
|
38
|
+
# )
|
|
39
|
+
private_class_method def self.log_signals(opts = {})
|
|
40
|
+
signals_arr = opts[:signals_arr]
|
|
41
|
+
timestamp_start = opts[:timestamp_start]
|
|
42
|
+
scan_log = opts[:scan_log]
|
|
43
|
+
|
|
44
|
+
signals = signals_arr.sort_by { |s| s[:freq].to_s.raw_hz }
|
|
45
|
+
timestamp_end = Time.now.strftime('%Y-%m-%d %H:%M:%S%z')
|
|
46
|
+
duration_secs = Time.parse(timestamp_end) - Time.parse(timestamp_start)
|
|
47
|
+
# Convert duration seconds to hours minutes seconds
|
|
48
|
+
hours = (duration_secs / 3600).to_i
|
|
49
|
+
minutes = ((duration_secs % 3600) / 60).to_i
|
|
50
|
+
seconds = (duration_secs % 60).to_i
|
|
51
|
+
duration = format('%<hrs>02d:%<mins>02d:%<secs>02d', hrs: hours, mins: minutes, secs: seconds)
|
|
52
|
+
|
|
53
|
+
scan_resp = {
|
|
54
|
+
signals: signals,
|
|
55
|
+
timestamp_start: timestamp_start,
|
|
56
|
+
timestamp_end: timestamp_end,
|
|
57
|
+
duration: duration
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
File.write(
|
|
61
|
+
scan_log,
|
|
62
|
+
JSON.pretty_generate(scan_resp)
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
scan_resp
|
|
66
|
+
rescue StandardError => e
|
|
67
|
+
raise e
|
|
68
|
+
end
|
|
69
|
+
|
|
33
70
|
# Supported Method Parameters::
|
|
34
71
|
# gqrx_sock = PWN::SDR::GQRX.connect(
|
|
35
72
|
# target: 'optional - GQRX target IP address (defaults to 127.0.0.1)',
|
|
@@ -170,33 +207,48 @@ module PWN
|
|
|
170
207
|
end
|
|
171
208
|
|
|
172
209
|
# Supported Method Parameters::
|
|
173
|
-
#
|
|
210
|
+
# freq_obj = PWN::SDR::GQRX.init_freq(
|
|
174
211
|
# gqrx_sock: 'required - GQRX socket object returned from #connect method',
|
|
175
212
|
# freq: 'required - Frequency to set',
|
|
176
213
|
# demodulator_mode: 'optional - Demodulator mode (defaults to WFM)',
|
|
177
214
|
# bandwidth: 'optional - Bandwidth (defaults to 200_000)',
|
|
178
215
|
# squelch: 'optional - Squelch level to set (Defaults to current value)',
|
|
179
216
|
# decoder: 'optional - Decoder key (e.g., :gsm) to start live decoding (starts recording if provided)',
|
|
180
|
-
# record_dir: 'optional - Directory where GQRX saves recordings (required if decoder provided; defaults to
|
|
181
|
-
#
|
|
182
|
-
#
|
|
217
|
+
# record_dir: 'optional - Directory where GQRX saves recordings (required if decoder provided; defaults to /tmp/gqrx_recordings)',
|
|
218
|
+
# suppress_details: 'optional - Boolean to include extra frequency details in return hash (defaults to false)',
|
|
219
|
+
# keep_alive: 'optional - Boolean to keep GQRX connection alive after method completion (defaults to false)'
|
|
183
220
|
# )
|
|
184
221
|
public_class_method def self.init_freq(opts = {})
|
|
185
222
|
gqrx_sock = opts[:gqrx_sock]
|
|
186
223
|
freq = opts[:freq]
|
|
187
|
-
|
|
224
|
+
valid_demodulator_modes = %i[
|
|
225
|
+
AM
|
|
226
|
+
AM_SYNC
|
|
227
|
+
CW
|
|
228
|
+
CWL
|
|
229
|
+
CWU
|
|
230
|
+
FM
|
|
231
|
+
OFF
|
|
232
|
+
LSB
|
|
233
|
+
RAW
|
|
234
|
+
USB
|
|
235
|
+
WFM
|
|
236
|
+
WFM_ST
|
|
237
|
+
WFM_ST_OIRT
|
|
238
|
+
]
|
|
239
|
+
demodulator_mode = opts[:demodulator_mode] ||= :WFM
|
|
240
|
+
raise "ERROR: Invalid demodulator_mode '#{demodulator_mode}'. Valid modes: #{valid_demodulator_modes.join(', ')}" unless valid_demodulator_modes.include?(demodulator_mode.to_sym)
|
|
241
|
+
|
|
188
242
|
bandwidth = opts[:bandwidth] ||= 200_000
|
|
189
243
|
squelch = opts[:squelch]
|
|
190
244
|
decoder = opts[:decoder]
|
|
191
|
-
record_dir = opts[:record_dir] ||=
|
|
192
|
-
decoder_opts = opts[:decoder_opts] ||= {}
|
|
245
|
+
record_dir = opts[:record_dir] ||= '/tmp'
|
|
193
246
|
suppress_details = opts[:suppress_details] || false
|
|
247
|
+
keep_alive = opts[:keep_alive] || false
|
|
194
248
|
|
|
195
249
|
raise "ERROR: record_dir '#{record_dir}' does not exist. Please create it or provide a valid path." if decoder && !Dir.exist?(record_dir)
|
|
196
250
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
if squelch.is_a?(Float) && squelch >= -100.0 && squelch <= 0.0
|
|
251
|
+
if squelch.is_a?(Float) && squelch >= -100.0 && squelch <= 0.0 && !keep_alive
|
|
200
252
|
change_squelch_resp = gqrx_cmd(
|
|
201
253
|
gqrx_sock: gqrx_sock,
|
|
202
254
|
cmd: "L SQL #{squelch}",
|
|
@@ -206,18 +258,20 @@ module PWN
|
|
|
206
258
|
|
|
207
259
|
change_freq_resp = gqrx_cmd(
|
|
208
260
|
gqrx_sock: gqrx_sock,
|
|
209
|
-
cmd: "F #{
|
|
261
|
+
cmd: "F #{freq.to_s.raw_hz}",
|
|
210
262
|
resp_ok: 'RPRT 0'
|
|
211
263
|
)
|
|
212
264
|
|
|
213
265
|
# Set demod mode and bandwidth (always, using defaults if not provided)
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
266
|
+
unless keep_alive
|
|
267
|
+
mode_str = demodulator_mode.to_s.upcase
|
|
268
|
+
passband_hz = bandwidth.to_s.raw_hz
|
|
269
|
+
gqrx_cmd(
|
|
270
|
+
gqrx_sock: gqrx_sock,
|
|
271
|
+
cmd: "M #{mode_str} #{passband_hz}",
|
|
272
|
+
resp_ok: 'RPRT 0'
|
|
273
|
+
)
|
|
274
|
+
end
|
|
221
275
|
|
|
222
276
|
# Get demodulator mode n passband
|
|
223
277
|
demod_n_passband = gqrx_cmd(
|
|
@@ -231,9 +285,10 @@ module PWN
|
|
|
231
285
|
cmd: 'f'
|
|
232
286
|
)
|
|
233
287
|
|
|
234
|
-
|
|
288
|
+
freq_obj = {
|
|
289
|
+
demodulator_mode: demodulator_mode,
|
|
235
290
|
demod_mode_n_passband: demod_n_passband,
|
|
236
|
-
|
|
291
|
+
freq: current_freq,
|
|
237
292
|
bandwidth: bandwidth
|
|
238
293
|
}
|
|
239
294
|
|
|
@@ -269,67 +324,67 @@ module PWN
|
|
|
269
324
|
cmd: 'l BB_GAIN'
|
|
270
325
|
)
|
|
271
326
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
# Start recording and decoding if decoder provided
|
|
280
|
-
decoder_module = nil
|
|
281
|
-
decoder_thread = nil
|
|
282
|
-
record_path = nil
|
|
283
|
-
if decoder
|
|
284
|
-
# Resolve decoder module via case statement for extensibility
|
|
285
|
-
case decoder
|
|
286
|
-
when :gsm
|
|
287
|
-
decoder_module = PWN::SDR::Decoder::GSM
|
|
288
|
-
else
|
|
289
|
-
raise "ERROR: Unknown decoder key: #{decoder}. Supported: :gsm"
|
|
290
|
-
end
|
|
327
|
+
freq_obj[:audio_gain_db] = audio_gain_db
|
|
328
|
+
freq_obj[:squelch_set] = current_squelch
|
|
329
|
+
freq_obj[:rf_gain] = rf_gain
|
|
330
|
+
freq_obj[:if_gain] = if_gain
|
|
331
|
+
freq_obj[:bb_gain] = bb_gain
|
|
332
|
+
freq_obj[:strength_db] = strength_db
|
|
333
|
+
end
|
|
291
334
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
# Build partial gqrx_obj for decoder start
|
|
305
|
-
gqrx_obj_partial = {
|
|
306
|
-
gqrx_sock: gqrx_sock,
|
|
307
|
-
record_path: record_path,
|
|
308
|
-
frequency: current_freq,
|
|
309
|
-
bandwidth: bandwidth,
|
|
310
|
-
demodulator_mode: demodulator_mode
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
# Initialize and start decoder (module style: .start returns thread)
|
|
314
|
-
decoder_thread = decoder_module.start(
|
|
315
|
-
gqrx_obj: gqrx_obj_partial,
|
|
316
|
-
**decoder_opts
|
|
317
|
-
)
|
|
318
|
-
|
|
319
|
-
init_freq_hash[:decoder] = decoder
|
|
320
|
-
init_freq_hash[:decoder_module] = decoder_module
|
|
321
|
-
init_freq_hash[:decoder_thread] = decoder_thread
|
|
322
|
-
init_freq_hash[:record_path] = record_path
|
|
335
|
+
# Start recording and decoding if decoder provided
|
|
336
|
+
decoder_module = nil
|
|
337
|
+
decoder_thread = nil
|
|
338
|
+
record_path = nil
|
|
339
|
+
if decoder
|
|
340
|
+
# Resolve decoder module via case statement for extensibility
|
|
341
|
+
case decoder
|
|
342
|
+
when :gsm
|
|
343
|
+
decoder_module = PWN::SDR::Decoder::GSM
|
|
344
|
+
else
|
|
345
|
+
raise "ERROR: Unknown decoder key: #{decoder}. Supported: :gsm"
|
|
323
346
|
end
|
|
347
|
+
|
|
348
|
+
# Ensure recording is off before starting
|
|
349
|
+
record_status = gqrx_cmd(gqrx_sock: gqrx_sock, cmd: 'u RECORD')
|
|
350
|
+
gqrx_cmd(gqrx_sock: gqrx_sock, cmd: 'U RECORD 0', resp_ok: 'RPRT 0') if record_status == '1'
|
|
351
|
+
|
|
352
|
+
# Start recording
|
|
353
|
+
gqrx_cmd(gqrx_sock: gqrx_sock, cmd: 'U RECORD 1', resp_ok: 'RPRT 0')
|
|
354
|
+
|
|
355
|
+
# Prepare for decoder
|
|
356
|
+
start_time = Time.now
|
|
357
|
+
expected_filename = "gqrx_#{start_time.strftime('%Y%m%d_%H%M%S')}_#{current_freq_raw}.wav"
|
|
358
|
+
record_path = File.join(record_dir, expected_filename)
|
|
359
|
+
|
|
360
|
+
# Build partial gqrx_obj for decoder start
|
|
361
|
+
freq_obj[:record_path] = record_path
|
|
362
|
+
|
|
363
|
+
# Initialize and start decoder (module style: .start returns thread)
|
|
364
|
+
freq_obj[:gqrx_sock] = gqrx_sock
|
|
365
|
+
decoder_thread = decoder_module.start(freq_obj: freq_obj)
|
|
366
|
+
freq_obj.delete(:gqrx_sock)
|
|
367
|
+
|
|
368
|
+
freq_obj[:decoder] = decoder
|
|
369
|
+
freq_obj[:decoder_module] = decoder_module
|
|
370
|
+
freq_obj[:decoder_thread] = decoder_thread
|
|
371
|
+
freq_obj[:record_path] = record_path
|
|
324
372
|
end
|
|
325
373
|
|
|
326
|
-
|
|
374
|
+
freq_obj
|
|
327
375
|
rescue StandardError => e
|
|
328
376
|
raise e
|
|
329
377
|
ensure
|
|
330
|
-
# Ensure
|
|
331
|
-
|
|
332
|
-
|
|
378
|
+
# Ensure decoder recording stops
|
|
379
|
+
if decoder
|
|
380
|
+
gqrx_cmd(
|
|
381
|
+
gqrx_sock: gqrx_sock,
|
|
382
|
+
cmd: 'U RECORD 0',
|
|
383
|
+
resp_ok: 'RPRT 0'
|
|
384
|
+
)
|
|
385
|
+
decoder_module.stop(freq_obj: freq_obj)
|
|
386
|
+
end
|
|
387
|
+
disconnect(gqrx_sock: gqrx_sock) if gqrx_sock.is_a?(TCPSocket) && !keep_alive
|
|
333
388
|
end
|
|
334
389
|
|
|
335
390
|
# Supported Method Parameters::
|
|
@@ -344,13 +399,26 @@ module PWN
|
|
|
344
399
|
# lock_freq_duration: 'optional - Lock frequency duration in seconds (defaults to 0.04)',
|
|
345
400
|
# strength_lock: 'optional - Strength lock in dBFS (defaults to -70.0)',
|
|
346
401
|
# squelch: 'optional - Squelch level in dBFS (defaults to strength_lock - 3.0)',
|
|
402
|
+
# audio_gain_db: 'optional - Audio gain in dB (defaults to 6.0)',
|
|
403
|
+
# rf_gain: 'optional - RF gain (defaults to 0.0)',
|
|
404
|
+
# intermediate_gain: 'optional - Intermediate gain (defaults to 32.0)',
|
|
405
|
+
# baseband_gain: 'optional - Baseband gain (defaults to 10.0)',
|
|
406
|
+
# scan_log: 'optional - Path to save detected signals log (defaults to /tmp/pwn_sdr_gqrx_scan_<start_freq>-<target_freq>_<timestamp>.json)',
|
|
347
407
|
# location: 'optional - Location string to include in AI analysis (e.g., "New York, NY", 90210, GPS coords, etc.)'
|
|
348
408
|
# )
|
|
349
409
|
|
|
350
410
|
public_class_method def self.scan_range(opts = {})
|
|
411
|
+
timestamp_start = Time.now.strftime('%Y-%m-%d %H:%M:%S%z')
|
|
412
|
+
log_timestamp = Time.now.strftime('%Y-%m-%d')
|
|
413
|
+
|
|
351
414
|
gqrx_sock = opts[:gqrx_sock]
|
|
415
|
+
|
|
352
416
|
start_freq = opts[:start_freq]
|
|
417
|
+
hz_start = start_freq.to_s.raw_hz
|
|
418
|
+
|
|
353
419
|
target_freq = opts[:target_freq]
|
|
420
|
+
hz_target = target_freq.to_s.raw_hz
|
|
421
|
+
|
|
354
422
|
demodulator_mode = opts[:demodulator_mode]
|
|
355
423
|
bandwidth = opts[:bandwidth] ||= 200_000
|
|
356
424
|
overlap_protection = opts[:overlap_protection] || false
|
|
@@ -358,16 +426,20 @@ module PWN
|
|
|
358
426
|
lock_freq_duration = opts[:lock_freq_duration] ||= 0.04
|
|
359
427
|
strength_lock = opts[:strength_lock] ||= -70.0
|
|
360
428
|
squelch = opts[:squelch] ||= (strength_lock - 3.0)
|
|
429
|
+
scan_log = opts[:scan_log] ||= "/tmp/pwn_sdr_gqrx_scan_#{hz_start.pretty_hz}-#{hz_target.pretty_hz}_#{log_timestamp}.json"
|
|
361
430
|
location = opts[:location] ||= 'United States'
|
|
362
431
|
|
|
363
|
-
timestamp_start = Time.now.strftime('%Y-%m-%d %H:%M:%S%z')
|
|
364
|
-
|
|
365
|
-
hz_start = start_freq.to_s.raw_hz
|
|
366
|
-
hz_target = target_freq.to_s.raw_hz
|
|
367
432
|
step_hz = 10**(precision - 1)
|
|
368
433
|
step = hz_start > hz_target ? -step_hz : step_hz
|
|
369
434
|
|
|
370
|
-
# Set
|
|
435
|
+
# Set squelch once for the scan
|
|
436
|
+
change_squelch_resp = gqrx_cmd(
|
|
437
|
+
gqrx_sock: gqrx_sock,
|
|
438
|
+
cmd: "L SQL #{squelch}",
|
|
439
|
+
resp_ok: 'RPRT 0'
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
# Set demodulator mode & passband once for the scan
|
|
371
443
|
mode_str = demodulator_mode.to_s.upcase
|
|
372
444
|
passband_hz = bandwidth.to_s.raw_hz
|
|
373
445
|
gqrx_cmd(
|
|
@@ -376,26 +448,46 @@ module PWN
|
|
|
376
448
|
resp_ok: 'RPRT 0'
|
|
377
449
|
)
|
|
378
450
|
|
|
379
|
-
|
|
380
|
-
|
|
451
|
+
audio_gain_db = opts[:audio_gain_db] ||= 6.0
|
|
452
|
+
audio_gain_db = audio_gain_db.to_f
|
|
453
|
+
audio_gain_db_resp = PWN::SDR::GQRX.gqrx_cmd(
|
|
381
454
|
gqrx_sock: gqrx_sock,
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
455
|
+
cmd: "L AF #{audio_gain_db}",
|
|
456
|
+
resp_ok: 'RPRT 0'
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
rf_gain = opts[:rf_gain] ||= 0.0
|
|
460
|
+
rf_gain = rf_gain.to_f
|
|
461
|
+
rf_gain_resp = PWN::SDR::GQRX.gqrx_cmd(
|
|
462
|
+
gqrx_sock: gqrx_sock,
|
|
463
|
+
cmd: "L RF_GAIN #{rf_gain}",
|
|
464
|
+
resp_ok: 'RPRT 0'
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
intermediate_gain = opts[:intermediate_gain] ||= 32.0
|
|
468
|
+
intermediate_gain = intermediate_gain.to_f
|
|
469
|
+
intermediate_resp = PWN::SDR::GQRX.gqrx_cmd(
|
|
470
|
+
gqrx_sock: gqrx_sock,
|
|
471
|
+
cmd: "L IF_GAIN #{intermediate_gain}",
|
|
472
|
+
resp_ok: 'RPRT 0'
|
|
387
473
|
)
|
|
388
|
-
|
|
389
|
-
|
|
474
|
+
|
|
475
|
+
baseband_gain = opts[:baseband_gain] ||= 10.0
|
|
476
|
+
baseband_gain = baseband_gain.to_f
|
|
477
|
+
baseband_resp = PWN::SDR::GQRX.gqrx_cmd(
|
|
478
|
+
gqrx_sock: gqrx_sock,
|
|
479
|
+
cmd: "L BB_GAIN #{baseband_gain}",
|
|
480
|
+
resp_ok: 'RPRT 0'
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
prev_freq_obj = {}
|
|
390
484
|
|
|
391
485
|
in_signal = false
|
|
392
486
|
candidate_signals = []
|
|
393
487
|
strength_history = []
|
|
394
488
|
|
|
395
|
-
# ──────────────────────────────────────────────────────────────
|
|
396
489
|
# Adaptive peak finder – trims weakest ends after each pass
|
|
397
490
|
# Converges quickly to the true center of the bell curve
|
|
398
|
-
# ──────────────────────────────────────────────────────────────
|
|
399
491
|
find_best_peak = lambda do |opts = {}|
|
|
400
492
|
beg_of_signal_hz = opts[:beg_of_signal_hz].to_s.raw_hz
|
|
401
493
|
top_of_signal_hz = opts[:top_of_signal_hz].to_s.raw_hz
|
|
@@ -497,6 +589,7 @@ module PWN
|
|
|
497
589
|
puts "INFO: Scanning from #{hz_start.pretty_hz} to #{hz_target.pretty_hz} in steps of #{step.abs.pretty_hz} Hz.\nIf scans are slow and/or you're experiencing false positives/negatives, consider adjusting:\n1. The SDR's sample rate in GQRX\n\s\s- Click on `Configure I/O devices`.\n\s\s- A lower `Input rate` value seems counter-intuitive but works well (e.g. ADALM PLUTO ~ 1000000).\n2. Adjust the :strength_lock parameter.\n3. Adjust the :lock_freq_duration parameter.\n4. Adjust the :precision parameter.\n5. Disable AI introspection in PWN::Env\nHappy scanning!\n\n"
|
|
498
590
|
|
|
499
591
|
signals_arr = []
|
|
592
|
+
# Format timestamp_start for filename
|
|
500
593
|
hz_start.step(by: step, to: hz_target) do |hz|
|
|
501
594
|
gqrx_cmd(gqrx_sock: gqrx_sock, cmd: "F #{hz}")
|
|
502
595
|
sleep lock_freq_duration
|
|
@@ -522,11 +615,11 @@ module PWN
|
|
|
522
615
|
top_of_signal_hz = candidate_signals.map { |s| s[:hz] }.max - step_hz
|
|
523
616
|
|
|
524
617
|
skip_signal = false
|
|
525
|
-
|
|
526
|
-
distance_from_prev_detected_freq_hz = (beg_of_signal_hz -
|
|
618
|
+
prev_freq = prev_freq_obj[:freq].to_s.raw_hz
|
|
619
|
+
distance_from_prev_detected_freq_hz = (beg_of_signal_hz - prev_freq).abs
|
|
527
620
|
half_bandwidth = (bandwidth / 2).to_i
|
|
528
621
|
skip_signal = true if distance_from_prev_detected_freq_hz < half_bandwidth && overlap_protection
|
|
529
|
-
puts "Prev Dect Freq: #{
|
|
622
|
+
puts "Prev Dect Freq: #{prev_freq} | New Freq Edge: #{beg_of_signal_hz} | Distance from Prev Dect Freq: #{distance_from_prev_detected_freq_hz} Hz | Step Hz: #{step_hz} | Bandwidth: #{bandwidth} Hz | Half Bandwidth: #{half_bandwidth} Hz | Overlap Protection? #{overlap_protection} | Skip Signal? #{skip_signal}"
|
|
530
623
|
next if skip_signal
|
|
531
624
|
|
|
532
625
|
best_peak = find_best_peak.call(
|
|
@@ -535,27 +628,33 @@ module PWN
|
|
|
535
628
|
)
|
|
536
629
|
|
|
537
630
|
if best_peak[:hz] && best_peak[:strength_db] > strength_lock
|
|
538
|
-
|
|
631
|
+
prev_freq_obj = init_freq(
|
|
539
632
|
gqrx_sock: gqrx_sock,
|
|
540
633
|
freq: best_peak[:hz],
|
|
541
634
|
demodulator_mode: demodulator_mode,
|
|
542
635
|
bandwidth: bandwidth,
|
|
543
636
|
squelch: squelch,
|
|
544
|
-
suppress_details: true
|
|
637
|
+
suppress_details: true,
|
|
638
|
+
keep_alive: true
|
|
545
639
|
)
|
|
546
|
-
|
|
547
|
-
|
|
640
|
+
prev_freq_obj[:lock_freq_duration] = lock_freq_duration
|
|
641
|
+
prev_freq_obj[:strength_lock] = strength_lock
|
|
548
642
|
|
|
549
643
|
system_role_content = "Analyze signal data captured by a software-defined-radio using GQRX at the following location: #{location}. Respond with just FCC information about the transmission if available. If the frequency is unlicensed or not found in FCC records, state that clearly. Be clear and concise in your analysis."
|
|
550
644
|
ai_analysis = PWN::AI::Introspection.reflect_on(
|
|
551
|
-
request:
|
|
645
|
+
request: prev_freq_obj.to_json,
|
|
552
646
|
system_role_content: system_role_content,
|
|
553
647
|
suppress_pii_warning: true
|
|
554
648
|
)
|
|
555
|
-
|
|
649
|
+
prev_freq_obj[:ai_analysis] = ai_analysis unless ai_analysis.nil?
|
|
556
650
|
puts "\n**** Detected Signal ****"
|
|
557
|
-
puts JSON.pretty_generate(
|
|
558
|
-
signals_arr.push(
|
|
651
|
+
puts JSON.pretty_generate(prev_freq_obj)
|
|
652
|
+
signals_arr.push(prev_freq_obj)
|
|
653
|
+
log_signals(
|
|
654
|
+
signals_arr: signals_arr,
|
|
655
|
+
timestamp_start: timestamp_start,
|
|
656
|
+
scan_log: scan_log
|
|
657
|
+
)
|
|
559
658
|
end
|
|
560
659
|
candidate_signals.clear
|
|
561
660
|
sleep lock_freq_duration
|
|
@@ -564,21 +663,73 @@ module PWN
|
|
|
564
663
|
strength_history = []
|
|
565
664
|
end
|
|
566
665
|
end
|
|
567
|
-
signals = signals_arr.sort_by { |s| s[:frequency].to_s.raw_hz }
|
|
568
|
-
timestamp_end = Time.now.strftime('%Y-%m-%d %H:%M:%S%z')
|
|
569
|
-
duration_secs = Time.parse(timestamp_end) - Time.parse(timestamp_start)
|
|
570
|
-
# Convert duration seconds to hours minutes seconds
|
|
571
|
-
hours = (duration_secs / 3600).to_i
|
|
572
|
-
minutes = ((duration_secs % 3600) / 60).to_i
|
|
573
|
-
seconds = (duration_secs % 60).to_i
|
|
574
|
-
duration = format('%<hrs>02d:%<mins>02d:%<secs>02d', hrs: hours, mins: minutes, secs: seconds)
|
|
575
666
|
|
|
576
|
-
|
|
577
|
-
|
|
667
|
+
log_signals(
|
|
668
|
+
signals_arr: signals_arr,
|
|
578
669
|
timestamp_start: timestamp_start,
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
670
|
+
scan_log: scan_log
|
|
671
|
+
)
|
|
672
|
+
rescue StandardError => e
|
|
673
|
+
raise e
|
|
674
|
+
ensure
|
|
675
|
+
disconnect(gqrx_sock: gqrx_sock)
|
|
676
|
+
end
|
|
677
|
+
|
|
678
|
+
# Supported Method Parameters::
|
|
679
|
+
# PWN::SDR::GQRX.analyze_scan(
|
|
680
|
+
# scan_resp: 'required - Scan response hash returned from #scan_range method',
|
|
681
|
+
# target: 'optional - GQRX target IP address (defaults to 127.0.0.1)',
|
|
682
|
+
# port: 'optional - GQRX target port (defaults to 7356)'
|
|
683
|
+
# )
|
|
684
|
+
public_class_method def self.analyze_scan(opts = {})
|
|
685
|
+
scan_resp = opts[:scan_resp]
|
|
686
|
+
raise 'ERROR: scan_resp is required.' if scan_resp.nil? || scan_resp[:signals].nil? || scan_resp[:signals].empty?
|
|
687
|
+
|
|
688
|
+
target = opts[:target]
|
|
689
|
+
port = opts[:port]
|
|
690
|
+
gqrx_sock = connect(
|
|
691
|
+
target: target,
|
|
692
|
+
port: port
|
|
693
|
+
)
|
|
694
|
+
|
|
695
|
+
scan_resp[:signals].each do |signal|
|
|
696
|
+
freq_obj = { gqrx_sock: gqrx_sock, keep_alive: true }
|
|
697
|
+
freq_obj = signal.merge(freq_obj)
|
|
698
|
+
freq_obj = init_freq(freq_obj)
|
|
699
|
+
# Redact gqrx_sock from output
|
|
700
|
+
freq_obj.delete(:gqrx_sock)
|
|
701
|
+
puts JSON.pretty_generate(freq_obj)
|
|
702
|
+
print 'Press [ENTER] to continue...'
|
|
703
|
+
gets
|
|
704
|
+
puts "\n" * 3
|
|
705
|
+
end
|
|
706
|
+
rescue StandardError => e
|
|
707
|
+
raise e
|
|
708
|
+
ensure
|
|
709
|
+
disconnect(gqrx_sock: gqrx_sock)
|
|
710
|
+
end
|
|
711
|
+
|
|
712
|
+
# Supported Method Parameters::
|
|
713
|
+
# PWN::SDR::GQRX.analyze_log(
|
|
714
|
+
# scan_log: 'required - Path to signals log file',
|
|
715
|
+
# target: 'optional - GQRX target IP address (defaults to 127.0.0.1)',
|
|
716
|
+
# port: 'optional - GQRX target port (defaults to 7356)'
|
|
717
|
+
# )
|
|
718
|
+
public_class_method def self.analyze_log(opts = {})
|
|
719
|
+
scan_log = opts[:scan_log]
|
|
720
|
+
raise 'ERROR: scan_log path is required.' unless File.exist?(scan_log)
|
|
721
|
+
|
|
722
|
+
scan_resp = JSON.parse(File.read(scan_log), symbolize_names: true)
|
|
723
|
+
raise 'ERROR: No signals found in log.' if scan_resp[:signals].nil? || scan_resp[:signals].empty?
|
|
724
|
+
|
|
725
|
+
target = opts[:target]
|
|
726
|
+
port = opts[:port]
|
|
727
|
+
|
|
728
|
+
analyze_scan(
|
|
729
|
+
scan_resp: scan_resp,
|
|
730
|
+
target: target,
|
|
731
|
+
port: port
|
|
732
|
+
)
|
|
582
733
|
rescue StandardError => e
|
|
583
734
|
raise e
|
|
584
735
|
end
|
|
@@ -608,7 +759,7 @@ module PWN
|
|
|
608
759
|
public_class_method def self.help
|
|
609
760
|
puts "USAGE:
|
|
610
761
|
gqrx_sock = #{self}.connect(
|
|
611
|
-
|
|
762
|
+
target: 'optional - GQRX target IP address (defaults to 127.0.0.1)',
|
|
612
763
|
port: 'optional - GQRX target port (defaults to 7356)'
|
|
613
764
|
)
|
|
614
765
|
|
|
@@ -618,15 +769,15 @@ module PWN
|
|
|
618
769
|
resp_ok: 'optional - Expected response from GQRX to indicate success'
|
|
619
770
|
)
|
|
620
771
|
|
|
621
|
-
|
|
772
|
+
freq_obj = #{self}.init_freq(
|
|
622
773
|
gqrx_sock: 'required - GQRX socket object returned from #connect method',
|
|
623
774
|
freq: 'required - Frequency to set',
|
|
624
775
|
demodulator_mode: 'optional - Demodulator mode (defaults to WFM)',
|
|
625
776
|
bandwidth: 'optional - Bandwidth (defaults to 200_000)',
|
|
626
777
|
decoder: 'optional - Decoder key (e.g., :gsm) to start live decoding (starts recording if provided)',
|
|
627
|
-
record_dir: 'optional - Directory where GQRX saves recordings (required if decoder provided; defaults to
|
|
628
|
-
|
|
629
|
-
|
|
778
|
+
record_dir: 'optional - Directory where GQRX saves recordings (required if decoder provided; defaults to /tmp/gqrx_recordings)',
|
|
779
|
+
suppress_details: 'optional - Boolean to include extra frequency details in return hash (defaults to false)',
|
|
780
|
+
keep_alive: 'optional - Boolean to keep GQRX connection alive after method completion (defaults to false)'
|
|
630
781
|
)
|
|
631
782
|
|
|
632
783
|
scan_resp = #{self}.scan_range(
|
|
@@ -640,9 +791,26 @@ module PWN
|
|
|
640
791
|
lock_freq_duration: 'optional - Lock frequency duration in seconds (defaults to 0.04)',
|
|
641
792
|
strength_lock: 'optional - Strength lock (defaults to -70.0)',
|
|
642
793
|
squelch: 'optional - Squelch level (defaults to strength_lock - 3.0)',
|
|
794
|
+
audio_gain_db: 'optional - Audio gain in dB (defaults to 6.0)',
|
|
795
|
+
rf_gain: 'optional - RF gain (defaults to 0.0)',
|
|
796
|
+
intermediate_gain: 'optional - Intermediate gain (defaults to 32.0)',
|
|
797
|
+
baseband_gain: 'optional - Baseband gain (defaults to 10.0)',
|
|
798
|
+
scan_log: 'optional - Path to save detected signals log (defaults to /tmp/pwn_sdr_gqrx_scan_<start_freq>-<target_freq>_<timestamp>.json)',
|
|
643
799
|
location: 'optional - Location string to include in AI analysis (e.g., \"New York, NY\", 90210, GPS coords, etc.)'
|
|
644
800
|
)
|
|
645
801
|
|
|
802
|
+
#{self}.analyze_scan(
|
|
803
|
+
scan_resp: 'required - Scan response object from #scan_range method',
|
|
804
|
+
target: 'optional - GQRX target IP address (defaults to 127.0.0.1)',
|
|
805
|
+
port: 'optional - GQRX target port (defaults to 7356)'
|
|
806
|
+
)
|
|
807
|
+
|
|
808
|
+
#{self}.analyze_log(
|
|
809
|
+
scan_log: 'required - Path to signals log file',
|
|
810
|
+
target: 'optional - GQRX target IP address (defaults to 127.0.0.1)',
|
|
811
|
+
port: 'optional - GQRX target port (defaults to 7356)'
|
|
812
|
+
)
|
|
813
|
+
|
|
646
814
|
#{self}.disconnect(
|
|
647
815
|
gqrx_sock: 'required - GQRX socket object returned from #connect method'
|
|
648
816
|
)
|
data/lib/pwn/version.rb
CHANGED
data/packer/provisioners/pwn.sh
CHANGED
|
@@ -97,4 +97,4 @@ case $os in
|
|
|
97
97
|
exit 1
|
|
98
98
|
esac
|
|
99
99
|
|
|
100
|
-
rvmsudo /bin/bash --login -c "cd ${pwn_root} && cp etc/userland/${pwn_provider}/metasploit/vagrant.yaml.EXAMPLE etc/userland/${pwn_provider}/metasploit/vagrant.yaml && ./
|
|
100
|
+
rvmsudo /bin/bash --login -c "cd ${pwn_root} && cp etc/userland/${pwn_provider}/metasploit/vagrant.yaml.EXAMPLE etc/userland/${pwn_provider}/metasploit/vagrant.yaml && ./reinstall_gemset.sh && ./build_gem.sh && rubocop"
|
data/third_party/pwn_rdoc.jsonl
CHANGED
|
@@ -1287,15 +1287,17 @@
|
|
|
1287
1287
|
{"messages":[{"role":"user","content":"PWN::SDR::FlipperZero.request Usage"},{"role":"assistant","content":"`PWN::SDR::FlipperZero.request`: Supported Method Parameters\n\nresponse = PWN::SDR::FlipperZero.request(\n\nflipper_zero_obj: 'required - flipper_zero_obj returned from #connect method',\npayload: 'optional - payload to send to the device (defaults to help)'\n\n)\n"}]}
|
|
1288
1288
|
{"messages":[{"role":"user","content":"PWN::SDR::FrequencyAllocation.authors Usage"},{"role":"assistant","content":"`PWN::SDR::FrequencyAllocation.authors`: Author(s)\n\n0day Inc. <support@0dayinc.com>\n"}]}
|
|
1289
1289
|
{"messages":[{"role":"user","content":"PWN::SDR::FrequencyAllocation.help Usage"},{"role":"assistant","content":"`PWN::SDR::FrequencyAllocation.help`: "}]}
|
|
1290
|
-
{"messages":[{"role":"user","content":"PWN::SDR::FrequencyAllocation.load Usage"},{"role":"assistant","content":"`PWN::SDR::FrequencyAllocation.load`: Supported Method Parameters\n\nopts = PWN::SDR::FrequencyAllocation.load(\n\nprofile: 'required - valid FrequencyAllocation profile name returned from #profiles method'\n\n)\n"}]}
|
|
1291
1290
|
{"messages":[{"role":"user","content":"PWN::SDR::FrequencyAllocation.profiles Usage"},{"role":"assistant","content":"`PWN::SDR::FrequencyAllocation.profiles`: Supported Method Parameters\n\nprofiles = PWN::SDR::FrequencyAllocation.profiles\n"}]}
|
|
1291
|
+
{"messages":[{"role":"user","content":"PWN::SDR::GQRX.analyze_log Usage"},{"role":"assistant","content":"`PWN::SDR::GQRX.analyze_log`: Supported Method Parameters\n\nPWN::SDR::GQRX.analyze_log(\n\nscan_log: 'required - Path to signals log file',\ntarget: 'optional - GQRX target IP address (defaults to 127.0.0.1)',\nport: 'optional - GQRX target port (defaults to 7356)'\n\n)\n"}]}
|
|
1292
|
+
{"messages":[{"role":"user","content":"PWN::SDR::GQRX.analyze_scan Usage"},{"role":"assistant","content":"`PWN::SDR::GQRX.analyze_scan`: Supported Method Parameters\n\nPWN::SDR::GQRX.analyze_scan(\n\nscan_resp: 'required - Scan response hash returned from #scan_range method',\ntarget: 'optional - GQRX target IP address (defaults to 127.0.0.1)',\nport: 'optional - GQRX target port (defaults to 7356)'\n\n)\n"}]}
|
|
1292
1293
|
{"messages":[{"role":"user","content":"PWN::SDR::GQRX.authors Usage"},{"role":"assistant","content":"`PWN::SDR::GQRX.authors`: Author(s)\n\n0day Inc. <support@0dayinc.com>\n"}]}
|
|
1293
1294
|
{"messages":[{"role":"user","content":"PWN::SDR::GQRX.connect Usage"},{"role":"assistant","content":"`PWN::SDR::GQRX.connect`: Supported Method Parameters\n\ngqrx_sock = PWN::SDR::GQRX.connect(\n\ntarget: 'optional - GQRX target IP address (defaults to 127.0.0.1)',\nport: 'optional - GQRX target port (defaults to 7356)'\n\n)\n"}]}
|
|
1294
1295
|
{"messages":[{"role":"user","content":"PWN::SDR::GQRX.disconnect Usage"},{"role":"assistant","content":"`PWN::SDR::GQRX.disconnect`: Supported Method Parameters\n\nPWN::SDR::GQRX.disconnect(\n\ngqrx_sock: 'required - GQRX socket object returned from #connect method'\n\n)\n"}]}
|
|
1295
1296
|
{"messages":[{"role":"user","content":"PWN::SDR::GQRX.gqrx_cmd Usage"},{"role":"assistant","content":"`PWN::SDR::GQRX.gqrx_cmd`: Supported Method Parameters\n\ngqrx_resp = PWN::SDR::GQRX.gqrx_cmd(\n\ngqrx_sock: 'required - GQRX socket object returned from #connect method',\ncmd: 'required - GQRX command to execute',\nresp_ok: 'optional - Expected response from GQRX to indicate success'\n\n)\n"}]}
|
|
1296
1297
|
{"messages":[{"role":"user","content":"PWN::SDR::GQRX.help Usage"},{"role":"assistant","content":"`PWN::SDR::GQRX.help`: "}]}
|
|
1297
|
-
{"messages":[{"role":"user","content":"PWN::SDR::GQRX.init_freq Usage"},{"role":"assistant","content":"`PWN::SDR::GQRX.init_freq`: Supported Method Parameters\n\
|
|
1298
|
-
{"messages":[{"role":"user","content":"PWN::SDR::GQRX.
|
|
1298
|
+
{"messages":[{"role":"user","content":"PWN::SDR::GQRX.init_freq Usage"},{"role":"assistant","content":"`PWN::SDR::GQRX.init_freq`: Supported Method Parameters\n\nfreq_obj = PWN::SDR::GQRX.init_freq(\n\ngqrx_sock: 'required - GQRX socket object returned from #connect method',\nfreq: 'required - Frequency to set',\ndemodulator_mode: 'optional - Demodulator mode (defaults to WFM)',\nbandwidth: 'optional - Bandwidth (defaults to 200_000)',\nsquelch: 'optional - Squelch level to set (Defaults to current value)',\ndecoder: 'optional - Decoder key (e.g., :gsm) to start live decoding (starts recording if provided)',\nrecord_dir: 'optional - Directory where GQRX saves recordings (required if decoder provided; defaults to /tmp/gqrx_recordings)',\nsuppress_details: 'optional - Boolean to include extra frequency details in return hash (defaults to false)',\nkeep_alive: 'optional - Boolean to keep GQRX connection alive after method completion (defaults to false)'\n\n)\n"}]}
|
|
1299
|
+
{"messages":[{"role":"user","content":"PWN::SDR::GQRX.log_signals Usage"},{"role":"assistant","content":"`PWN::SDR::GQRX.log_signals`: Supported Method Parameters\n\nscan_resp = PWN::SDR::GQRX.log_signals(\n\nsignals_arr: 'required - Array of detected signals',\ntimestamp_start: 'required - Scan start timestamp',\nscan_log: 'required - Path to save detected signals log'\n\n)\n"}]}
|
|
1300
|
+
{"messages":[{"role":"user","content":"PWN::SDR::GQRX.scan_range Usage"},{"role":"assistant","content":"`PWN::SDR::GQRX.scan_range`: Supported Method Parameters\n\nscan_resp = PWN::SDR::GQRX.scan_range(\n\ngqrx_sock: 'required - GQRX socket object returned from #connect method',\nstart_freq: 'required - Start frequency of scan range',\ntarget_freq: 'required - Target frequency of scan range',\ndemodulator_mode: 'optional - Demodulator mode (e.g. WFM, AM, FM, USB, LSB, RAW, CW, RTTY / defaults to WFM)',\nbandwidth: 'optional - Bandwidth in Hz (Defaults to 200_000)',\noverlap_protection: 'optional - Boolean to enable/disable bandwidth overlap protection (defaults to false)',\nprecision: 'optional - Frequency step precision (number of digits; defaults to 1)',\nlock_freq_duration: 'optional - Lock frequency duration in seconds (defaults to 0.04)',\nstrength_lock: 'optional - Strength lock in dBFS (defaults to -70.0)',\nsquelch: 'optional - Squelch level in dBFS (defaults to strength_lock - 3.0)',\naudio_gain_db: 'optional - Audio gain in dB (defaults to 6.0)',\nrf_gain: 'optional - RF gain (defaults to 0.0)',\nintermediate_gain: 'optional - Intermediate gain (defaults to 32.0)',\nbaseband_gain: 'optional - Baseband gain (defaults to 10.0)',\nscan_log: 'optional - Path to save detected signals log (defaults to /tmp/pwn_sdr_gqrx_scan_<start_freq>-<target_freq>_<timestamp>.json)',\nlocation: 'optional - Location string to include in AI analysis (e.g., \"New York, NY\", 90210, GPS coords, etc.)'\n\n)\n"}]}
|
|
1299
1301
|
{"messages":[{"role":"user","content":"PWN::SDR::GQRX.pretty_hz Usage"},{"role":"assistant","content":"`PWN::SDR::GQRX.pretty_hz`: "}]}
|
|
1300
1302
|
{"messages":[{"role":"user","content":"PWN::SDR::GQRX.raw_hz Usage"},{"role":"assistant","content":"`PWN::SDR::GQRX.raw_hz`: "}]}
|
|
1301
1303
|
{"messages":[{"role":"user","content":"PWN::SDR::RFIDler.authors Usage"},{"role":"assistant","content":"`PWN::SDR::RFIDler.authors`: Author(s)\n\n0day Inc. <support@0dayinc.com>\n"}]}
|