pwn 0.5.621 → 0.5.626

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 (124) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +2 -2
  3. data/Gemfile +1 -1
  4. data/README.md +7 -1
  5. data/bin/pwn_gqrx_scanner +78 -9
  6. data/documentation/Agent-Tool-Registry.md +3 -3
  7. data/documentation/Configuration.md +301 -47
  8. data/documentation/Cron.md +5 -3
  9. data/documentation/Diagrams.md +1 -1
  10. data/documentation/Extrospection.md +472 -26
  11. data/documentation/FFI.md +72 -8
  12. data/documentation/Home.md +4 -4
  13. data/documentation/How-PWN-Works.md +7 -3
  14. data/documentation/Mistakes.md +9 -0
  15. data/documentation/SDR.md +83 -10
  16. data/documentation/Skills-Memory-Learning.md +101 -11
  17. data/documentation/diagrams/agent-tool-registry.svg +56 -55
  18. data/documentation/diagrams/ai-integration-tool-calling.svg +24 -24
  19. data/documentation/diagrams/aws-cloud-security.svg +25 -25
  20. data/documentation/diagrams/burp-vs-zap-preference.svg +12 -12
  21. data/documentation/diagrams/code-scanning-sast.svg +25 -25
  22. data/documentation/diagrams/cron-scheduling.svg +23 -23
  23. data/documentation/diagrams/dot/agent-tool-registry.dot +2 -2
  24. data/documentation/diagrams/dot/extrospection-world-awareness.dot +33 -6
  25. data/documentation/diagrams/dot/hardware-hacking.dot +3 -1
  26. data/documentation/diagrams/dot/memory-skills-detailed.dot +4 -2
  27. data/documentation/diagrams/dot/mistakes-negative-feedback.dot +4 -2
  28. data/documentation/diagrams/dot/overall-pwn-architecture.dot +3 -3
  29. data/documentation/diagrams/dot/persistence-filesystem.dot +3 -3
  30. data/documentation/diagrams/dot/pwn-ai-feedback-learning-loop.dot +10 -4
  31. data/documentation/diagrams/dot/sdr-radio-flow.dot +39 -16
  32. data/documentation/diagrams/driver-framework.svg +13 -13
  33. data/documentation/diagrams/extrospection-world-awareness.svg +383 -177
  34. data/documentation/diagrams/fuzzing-workflow.svg +24 -24
  35. data/documentation/diagrams/hardware-hacking.svg +84 -69
  36. data/documentation/diagrams/history-to-drivers.svg +18 -18
  37. data/documentation/diagrams/memory-skills-detailed.svg +137 -120
  38. data/documentation/diagrams/mistakes-negative-feedback.svg +152 -137
  39. data/documentation/diagrams/network-infra-testing.svg +27 -27
  40. data/documentation/diagrams/overall-pwn-architecture.svg +97 -97
  41. data/documentation/diagrams/penetration-testing-workflow.svg +30 -30
  42. data/documentation/diagrams/persistence-filesystem.svg +100 -94
  43. data/documentation/diagrams/plugin-ecosystem.svg +35 -35
  44. data/documentation/diagrams/pwn-ai-feedback-learning-loop.svg +254 -207
  45. data/documentation/diagrams/pwn-repl-prototyping.svg +20 -20
  46. data/documentation/diagrams/reporting-pipeline.svg +18 -18
  47. data/documentation/diagrams/reverse-engineering-flow.svg +21 -21
  48. data/documentation/diagrams/sdr-radio-flow.svg +223 -91
  49. data/documentation/diagrams/sessions-cron-automation.svg +18 -18
  50. data/documentation/diagrams/swarm-multi-agent.svg +39 -39
  51. data/documentation/diagrams/web-application-testing.svg +26 -26
  52. data/documentation/diagrams/zero-day-research-flow.svg +25 -25
  53. data/documentation/pwn-ai-Agent.md +16 -10
  54. data/documentation/pwn_silent_help_learn_demo.gif +0 -0
  55. data/lib/pwn/ai/agent/assembly.rb +1 -1
  56. data/lib/pwn/ai/agent/btc.rb +1 -1
  57. data/lib/pwn/ai/agent/burp_suite.rb +1 -1
  58. data/lib/pwn/ai/agent/extrospection.rb +2562 -57
  59. data/lib/pwn/ai/agent/gqrx.rb +2 -2
  60. data/lib/pwn/ai/agent/hacker_one.rb +1 -1
  61. data/lib/pwn/ai/agent/learning.rb +16 -16
  62. data/lib/pwn/ai/agent/loop.rb +1 -1
  63. data/lib/pwn/ai/agent/{introspection.rb → reflect.rb} +12 -12
  64. data/lib/pwn/ai/agent/sast.rb +1 -1
  65. data/lib/pwn/ai/agent/tools/extrospection.rb +311 -8
  66. data/lib/pwn/ai/agent/tools/learning.rb +7 -7
  67. data/lib/pwn/ai/agent/transparent_browser.rb +1 -1
  68. data/lib/pwn/ai/agent/vuln_gen.rb +2 -2
  69. data/lib/pwn/ai/agent.rb +1 -1
  70. data/lib/pwn/ai/open_ai.rb +10 -6
  71. data/lib/pwn/config.rb +5 -4
  72. data/lib/pwn/ffi/adalm_pluto.rb +498 -0
  73. data/lib/pwn/ffi/fftw.rb +192 -0
  74. data/lib/pwn/ffi/hack_rf.rb +193 -0
  75. data/lib/pwn/ffi/liquid.rb +244 -0
  76. data/lib/pwn/ffi/rtl_sdr.rb +210 -0
  77. data/lib/pwn/ffi/soapy_sdr.rb +177 -0
  78. data/lib/pwn/ffi/stdio.rb +9 -1
  79. data/lib/pwn/ffi/volk.rb +241 -0
  80. data/lib/pwn/ffi.rb +45 -2
  81. data/lib/pwn/plugins/burp_suite.rb +2 -2
  82. data/lib/pwn/sast/pom_version.rb +2 -2
  83. data/lib/pwn/sast/test_case_engine.rb +1 -1
  84. data/lib/pwn/sdr/decoder/adsb.rb +201 -12
  85. data/lib/pwn/sdr/decoder/apt.rb +19 -5
  86. data/lib/pwn/sdr/decoder/base.rb +314 -1
  87. data/lib/pwn/sdr/decoder/bluetooth.rb +91 -18
  88. data/lib/pwn/sdr/decoder/dect.rb +81 -23
  89. data/lib/pwn/sdr/decoder/dsp.rb +229 -15
  90. data/lib/pwn/sdr/decoder/flex.rb +23 -6
  91. data/lib/pwn/sdr/decoder/gps.rb +80 -18
  92. data/lib/pwn/sdr/decoder/gsm.rb +83 -28
  93. data/lib/pwn/sdr/decoder/iridium.rb +80 -18
  94. data/lib/pwn/sdr/decoder/lora.rb +84 -28
  95. data/lib/pwn/sdr/decoder/lte.rb +80 -18
  96. data/lib/pwn/sdr/decoder/morse.rb +23 -6
  97. data/lib/pwn/sdr/decoder/p25.rb +90 -31
  98. data/lib/pwn/sdr/decoder/pager.rb +23 -6
  99. data/lib/pwn/sdr/decoder/pocsag.rb +23 -6
  100. data/lib/pwn/sdr/decoder/rds.rb +219 -52
  101. data/lib/pwn/sdr/decoder/rfid.rb +81 -27
  102. data/lib/pwn/sdr/decoder/rtl433.rb +85 -27
  103. data/lib/pwn/sdr/decoder/rtty.rb +23 -6
  104. data/lib/pwn/sdr/decoder/wifi.rb +86 -32
  105. data/lib/pwn/sdr/decoder/zigbee.rb +88 -35
  106. data/lib/pwn/sdr/decoder.rb +6 -5
  107. data/lib/pwn/sdr/frequency_allocation.rb +124 -81
  108. data/lib/pwn/sdr/gqrx.rb +1513 -165
  109. data/lib/pwn/version.rb +1 -1
  110. data/spec/lib/pwn/ai/agent/extrospection_spec.rb +96 -0
  111. data/spec/lib/pwn/ai/agent/{introspection_spec.rb → reflect_spec.rb} +3 -3
  112. data/spec/lib/pwn/ai/agent/tools/extrospection_spec.rb +8 -24
  113. data/spec/lib/pwn/ffi/adalm_pluto_spec.rb +37 -0
  114. data/spec/lib/pwn/ffi/fftw_spec.rb +26 -0
  115. data/spec/lib/pwn/ffi/hack_rf_spec.rb +25 -0
  116. data/spec/lib/pwn/ffi/liquid_spec.rb +42 -0
  117. data/spec/lib/pwn/ffi/rtl_sdr_spec.rb +24 -0
  118. data/spec/lib/pwn/ffi/soapy_sdr_spec.rb +25 -0
  119. data/spec/lib/pwn/ffi/volk_spec.rb +36 -0
  120. data/spec/lib/pwn/sdr/decoder/base_iq_spec.rb +65 -0
  121. data/spec/lib/pwn/sdr/decoder/dsp_spec.rb +50 -4
  122. data/spec/lib/pwn/sdr/decoder/rds_spec.rb +45 -4
  123. data/third_party/pwn_rdoc.jsonl +279 -30
  124. metadata +21 -5
@@ -3,13 +3,69 @@
3
3
  module PWN
4
4
  module SDR
5
5
  module Decoder
6
- # Pure-Ruby Iridium L-band burst detector.
7
- #
8
- # Iridium simplex/duplex bursts are 25 kbit/s DE-QPSK across
9
- # 1616–1626.5 MHz. Native mode reports burst count/duration/energy
10
- # per channel. `parse_line` retained for offline iridium-toolkit
11
- # text analysis.
6
+ # True-air + detector-fallback decoder for Iridium.
7
+ # Prefers PWN::FFI I/Q (RTL-SDR / ADALM-Pluto / HackRF / capture file)
8
+ # via Base.run_iq; degrades to Base.run_detector with no hardware.
12
9
  module Iridium
10
+ # Streaming I/Q energy/burst demod for Base.run_iq.
11
+ class DemodIQ
12
+ def initialize(rate:, protocol:, modulation:, extra: {})
13
+ @rate = rate.to_f
14
+ @protocol = protocol
15
+ @modulation = modulation
16
+ @extra = extra
17
+ @floor = nil
18
+ @in_burst = false
19
+ @burst_t0 = nil
20
+ @peak = -200.0
21
+ @burst_n = 0
22
+ @threshold = (extra[:threshold] || 8.0).to_f
23
+ end
24
+
25
+ def feed_iq(samples, rate: nil, &)
26
+ @rate = rate.to_f if rate
27
+ m2 = PWN::SDR::Decoder::DSP.mag_sq(iq: samples)
28
+ hop = [(@rate / 1000.0).round, 1].max
29
+ i = 0
30
+ while i < m2.length
31
+ win = m2[i, hop]
32
+ break if win.nil? || win.empty?
33
+
34
+ ms = win.sum / win.length
35
+ lvl = ms.positive? ? (10.0 * Math.log10(ms)) : -120.0
36
+ @floor = @floor.nil? ? lvl : ((@floor * 0.98) + (lvl * 0.02))
37
+ delta = lvl - @floor
38
+ if delta >= @threshold
39
+ unless @in_burst
40
+ @in_burst = true
41
+ @burst_t0 = Time.now
42
+ @peak = lvl
43
+ end
44
+ @peak = lvl if lvl > @peak
45
+ elsif @in_burst
46
+ @in_burst = false
47
+ @burst_n += 1
48
+ dur_ms = ((Time.now - @burst_t0) * 1000).round
49
+ msg = {
50
+ protocol: @protocol, event: 'burst', source: 'iq',
51
+ burst_no: @burst_n, peak_dbfs: @peak.round(1),
52
+ floor_dbfs: @floor.round(1), delta_db: (@peak - @floor).round(1),
53
+ duration_ms: dur_ms, modulation: @modulation,
54
+ sample_rate: @rate.to_i
55
+ }.merge(@extra.except(:threshold))
56
+ # protocol-specific enrichment
57
+ msg.merge!(self.class.enrich(msg)) if self.class.respond_to?(:enrich)
58
+ msg[:summary] = format(
59
+ '%<p>s IQ-burst #%<n>d peak=%<pk>+.1f dBFS Δ=%<d>.1f dB dur=%<ms>d ms',
60
+ p: @protocol, n: @burst_n, pk: @peak, d: @peak - @floor, ms: dur_ms
61
+ )
62
+ yield msg if block_given?
63
+ end
64
+ i += hop
65
+ end
66
+ end
67
+ end
68
+
13
69
  # Supported Method Parameters::
14
70
  # PWN::SDR::Decoder::Iridium.decode(
15
71
  # freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
@@ -17,18 +73,26 @@ module PWN
17
73
 
18
74
  public_class_method def self.decode(opts = {})
19
75
  freq_obj = opts[:freq_obj]
20
- PWN::SDR::Decoder::Base.run_detector(
76
+
77
+ rate = (opts[:sample_rate] || freq_obj[:iq_rate] || 2_000_000).to_i
78
+ proto = 'IRIDIUM'
79
+ demod = DemodIQ.new(
80
+ rate: rate, protocol: proto, modulation: 'DE-QPSK',
81
+ extra: { threshold: 7.0 }
82
+ )
83
+ PWN::SDR::Decoder::Base.run_iq(
21
84
  freq_obj: freq_obj,
22
- protocol: 'IRIDIUM',
23
- note: '25 kbit/s DE-QPSK — native mode reports burst timing/energy only.',
85
+ protocol: proto,
86
+ sample_rate: rate,
87
+ source: opts[:source],
88
+ file: opts[:file],
89
+ demod: demod,
24
90
  threshold: 7.0,
91
+ note: '25 kbit/s DE-QPSK — true-air I/Q path reports burst timing/energy.',
25
92
  describe: proc { |b| { modulation: 'DE-QPSK', symbol_rate: 25_000, classification: b[:duration_ms] < 10 ? 'simplex-burst' : 'duplex-frame' } }
26
93
  )
27
94
  end
28
95
 
29
- # Supported Method Parameters::
30
- # PWN::SDR::Decoder::Iridium.parse_line(line: 'IRA: sat:23 beam:14 ...')
31
-
32
96
  public_class_method def self.parse_line(opts = {})
33
97
  line = opts[:line].to_s
34
98
  out = { protocol: 'IRIDIUM' }
@@ -47,16 +111,14 @@ module PWN
47
111
  "AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
48
112
  end
49
113
 
50
- # Display Usage for this Module
51
-
52
114
  public_class_method def self.help
53
- puts "USAGE (ruby-native detector, no external binaries):
115
+ puts "USAGE (true-air I/Q via PWN::FFI + detector fallback):
54
116
  #{self}.decode(
55
- freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
117
+ freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq',
118
+ source: 'optional - :auto|:rtlsdr|:adalm_pluto|:file',
119
+ file: 'optional - .cu8/.cs16 capture'
56
120
  )
57
121
 
58
- #{self}.parse_line(line: 'IRA: sat:23 beam:14 pos=(+32.1/-097.0) ...')
59
-
60
122
  #{self}.authors
61
123
  "
62
124
  end
@@ -5,13 +5,69 @@ require 'json'
5
5
  module PWN
6
6
  module SDR
7
7
  module Decoder
8
- # Pure-Ruby LoRa / LoRaWAN CSS activity detector.
9
- #
10
- # LoRa is chirp-spread-spectrum over 125/250/500 kHz recoverable
11
- # only from raw I/Q at ≥250 ksps, not from a 48 kHz audio tap.
12
- # Native mode reports chirp-burst duration/energy (from which SF
13
- # can be estimated). `parse_line` retained for offline JSON analysis.
8
+ # True-air + detector-fallback decoder for LoRa.
9
+ # Prefers PWN::FFI I/Q (RTL-SDR / ADALM-Pluto / HackRF / capture file)
10
+ # via Base.run_iq; degrades to Base.run_detector with no hardware.
14
11
  module LoRa
12
+ # Streaming I/Q energy/burst demod for Base.run_iq.
13
+ class DemodIQ
14
+ def initialize(rate:, protocol:, modulation:, extra: {})
15
+ @rate = rate.to_f
16
+ @protocol = protocol
17
+ @modulation = modulation
18
+ @extra = extra
19
+ @floor = nil
20
+ @in_burst = false
21
+ @burst_t0 = nil
22
+ @peak = -200.0
23
+ @burst_n = 0
24
+ @threshold = (extra[:threshold] || 8.0).to_f
25
+ end
26
+
27
+ def feed_iq(samples, rate: nil, &)
28
+ @rate = rate.to_f if rate
29
+ m2 = PWN::SDR::Decoder::DSP.mag_sq(iq: samples)
30
+ hop = [(@rate / 1000.0).round, 1].max
31
+ i = 0
32
+ while i < m2.length
33
+ win = m2[i, hop]
34
+ break if win.nil? || win.empty?
35
+
36
+ ms = win.sum / win.length
37
+ lvl = ms.positive? ? (10.0 * Math.log10(ms)) : -120.0
38
+ @floor = @floor.nil? ? lvl : ((@floor * 0.98) + (lvl * 0.02))
39
+ delta = lvl - @floor
40
+ if delta >= @threshold
41
+ unless @in_burst
42
+ @in_burst = true
43
+ @burst_t0 = Time.now
44
+ @peak = lvl
45
+ end
46
+ @peak = lvl if lvl > @peak
47
+ elsif @in_burst
48
+ @in_burst = false
49
+ @burst_n += 1
50
+ dur_ms = ((Time.now - @burst_t0) * 1000).round
51
+ msg = {
52
+ protocol: @protocol, event: 'burst', source: 'iq',
53
+ burst_no: @burst_n, peak_dbfs: @peak.round(1),
54
+ floor_dbfs: @floor.round(1), delta_db: (@peak - @floor).round(1),
55
+ duration_ms: dur_ms, modulation: @modulation,
56
+ sample_rate: @rate.to_i
57
+ }.merge(@extra.except(:threshold))
58
+ # protocol-specific enrichment
59
+ msg.merge!(self.class.enrich(msg)) if self.class.respond_to?(:enrich)
60
+ msg[:summary] = format(
61
+ '%<p>s IQ-burst #%<n>d peak=%<pk>+.1f dBFS Δ=%<d>.1f dB dur=%<ms>d ms',
62
+ p: @protocol, n: @burst_n, pk: @peak, d: @peak - @floor, ms: dur_ms
63
+ )
64
+ yield msg if block_given?
65
+ end
66
+ i += hop
67
+ end
68
+ end
69
+ end
70
+
15
71
  # Supported Method Parameters::
16
72
  # PWN::SDR::Decoder::LoRa.decode(
17
73
  # freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
@@ -19,23 +75,26 @@ module PWN
19
75
 
20
76
  public_class_method def self.decode(opts = {})
21
77
  freq_obj = opts[:freq_obj]
22
- PWN::SDR::Decoder::Base.run_detector(
78
+
79
+ rate = (opts[:sample_rate] || freq_obj[:iq_rate] || 1_000_000).to_i
80
+ proto = 'LoRa'
81
+ demod = DemodIQ.new(
82
+ rate: rate, protocol: proto, modulation: 'CSS',
83
+ extra: { threshold: 8.0 }
84
+ )
85
+ PWN::SDR::Decoder::Base.run_iq(
23
86
  freq_obj: freq_obj,
24
- protocol: 'LoRa',
25
- note: 'CSS over 125–500 kHz — native mode reports chirp bursts and estimates SF from duration.',
87
+ protocol: proto,
88
+ sample_rate: rate,
89
+ source: opts[:source],
90
+ file: opts[:file],
91
+ demod: demod,
26
92
  threshold: 8.0,
27
- describe: proc { |b|
28
- # Preamble 8 symbols; T_sym = 2^SF / BW. Assume BW=125k.
29
- t_sym_ms = b[:duration_ms] / 20.0
30
- sf_est = t_sym_ms.positive? ? Math.log2(t_sym_ms * 125).round.clamp(6, 12) : nil
31
- { modulation: 'CSS', bw_khz_assumed: 125, sf_estimate: sf_est }.compact
32
- }
93
+ note: 'CSS over 125–500 kHz — true-air I/Q path estimates SF from chirp duration; full dechirp via FFTW is layered when available.',
94
+ describe: proc { |b| { modulation: 'CSS', bw_khz_assumed: 125, sf_estimate: (begin; t = b[:duration_ms] / 20.0; t.positive? ? Math.log2(t * 125).round.clamp(6, 12) : nil; end) }.compact }
33
95
  )
34
96
  end
35
97
 
36
- # Supported Method Parameters::
37
- # PWN::SDR::Decoder::LoRa.parse_line(line: '{"model":"LoRa",...}')
38
-
39
98
  public_class_method def self.parse_line(opts = {})
40
99
  line = opts[:line].to_s
41
100
  h = begin
@@ -48,10 +107,9 @@ module PWN
48
107
  bits << "SF#{out[:sf]}" if out[:sf]
49
108
  bits << "BW#{out[:bw]}" if out[:bw]
50
109
  bits << "CR#{out[:cr]}" if out[:cr]
51
- bits << "DevAddr=#{out[:devaddr] || out[:id]}" if out[:devaddr] || out[:id]
52
- bits << "FCnt=#{out[:fcnt]}" if out[:fcnt]
53
- bits << "len=#{out[:len] || (out[:data].to_s.length / 2)}" if out[:len] || out[:data]
54
- out[:summary] = "LoRa #{bits.join(' ')}".strip
110
+ bits << "RSSI=#{out[:rssi]}" if out[:rssi]
111
+ bits << out[:payload].to_s[0, 40] if out[:payload]
112
+ out[:summary] = bits.empty? ? line[0, 120] : bits.join(' ')
55
113
  out
56
114
  end
57
115
 
@@ -61,16 +119,14 @@ module PWN
61
119
  "AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
62
120
  end
63
121
 
64
- # Display Usage for this Module
65
-
66
122
  public_class_method def self.help
67
- puts "USAGE (ruby-native detector, no external binaries):
123
+ puts "USAGE (true-air I/Q via PWN::FFI + detector fallback):
68
124
  #{self}.decode(
69
- freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
125
+ freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq',
126
+ source: 'optional - :auto|:rtlsdr|:adalm_pluto|:file',
127
+ file: 'optional - .cu8/.cs16 capture'
70
128
  )
71
129
 
72
- #{self}.parse_line(line: '{\"model\":\"LoRa\",\"sf\":7,\"bw\":125,...}')
73
-
74
130
  #{self}.authors
75
131
  "
76
132
  end
@@ -3,13 +3,69 @@
3
3
  module PWN
4
4
  module SDR
5
5
  module Decoder
6
- # Pure-Ruby LTE / UMTS / CDMA cellular activity detector.
7
- #
8
- # 1.4–20 MHz OFDMA/WCDMA is far beyond a 48 kHz audio tap and beyond
9
- # interpreted Ruby's real-time I/Q throughput. Native mode reports
10
- # channel occupancy / power only. `parse_line` retained for offline
11
- # cell_search / CellSearch text analysis.
6
+ # True-air + detector-fallback decoder for LTE.
7
+ # Prefers PWN::FFI I/Q (RTL-SDR / ADALM-Pluto / HackRF / capture file)
8
+ # via Base.run_iq; degrades to Base.run_detector with no hardware.
12
9
  module LTE
10
+ # Streaming I/Q energy/burst demod for Base.run_iq.
11
+ class DemodIQ
12
+ def initialize(rate:, protocol:, modulation:, extra: {})
13
+ @rate = rate.to_f
14
+ @protocol = protocol
15
+ @modulation = modulation
16
+ @extra = extra
17
+ @floor = nil
18
+ @in_burst = false
19
+ @burst_t0 = nil
20
+ @peak = -200.0
21
+ @burst_n = 0
22
+ @threshold = (extra[:threshold] || 8.0).to_f
23
+ end
24
+
25
+ def feed_iq(samples, rate: nil, &)
26
+ @rate = rate.to_f if rate
27
+ m2 = PWN::SDR::Decoder::DSP.mag_sq(iq: samples)
28
+ hop = [(@rate / 1000.0).round, 1].max
29
+ i = 0
30
+ while i < m2.length
31
+ win = m2[i, hop]
32
+ break if win.nil? || win.empty?
33
+
34
+ ms = win.sum / win.length
35
+ lvl = ms.positive? ? (10.0 * Math.log10(ms)) : -120.0
36
+ @floor = @floor.nil? ? lvl : ((@floor * 0.98) + (lvl * 0.02))
37
+ delta = lvl - @floor
38
+ if delta >= @threshold
39
+ unless @in_burst
40
+ @in_burst = true
41
+ @burst_t0 = Time.now
42
+ @peak = lvl
43
+ end
44
+ @peak = lvl if lvl > @peak
45
+ elsif @in_burst
46
+ @in_burst = false
47
+ @burst_n += 1
48
+ dur_ms = ((Time.now - @burst_t0) * 1000).round
49
+ msg = {
50
+ protocol: @protocol, event: 'burst', source: 'iq',
51
+ burst_no: @burst_n, peak_dbfs: @peak.round(1),
52
+ floor_dbfs: @floor.round(1), delta_db: (@peak - @floor).round(1),
53
+ duration_ms: dur_ms, modulation: @modulation,
54
+ sample_rate: @rate.to_i
55
+ }.merge(@extra.except(:threshold))
56
+ # protocol-specific enrichment
57
+ msg.merge!(self.class.enrich(msg)) if self.class.respond_to?(:enrich)
58
+ msg[:summary] = format(
59
+ '%<p>s IQ-burst #%<n>d peak=%<pk>+.1f dBFS Δ=%<d>.1f dB dur=%<ms>d ms',
60
+ p: @protocol, n: @burst_n, pk: @peak, d: @peak - @floor, ms: dur_ms
61
+ )
62
+ yield msg if block_given?
63
+ end
64
+ i += hop
65
+ end
66
+ end
67
+ end
68
+
13
69
  # Supported Method Parameters::
14
70
  # PWN::SDR::Decoder::LTE.decode(
15
71
  # freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
@@ -17,18 +73,26 @@ module PWN
17
73
 
18
74
  public_class_method def self.decode(opts = {})
19
75
  freq_obj = opts[:freq_obj]
20
- PWN::SDR::Decoder::Base.run_detector(
76
+
77
+ rate = (opts[:sample_rate] || freq_obj[:iq_rate] || 2_000_000).to_i
78
+ proto = 'LTE'
79
+ demod = DemodIQ.new(
80
+ rate: rate, protocol: proto, modulation: 'OFDMA',
81
+ extra: { threshold: 4.0 }
82
+ )
83
+ PWN::SDR::Decoder::Base.run_iq(
21
84
  freq_obj: freq_obj,
22
- protocol: 'LTE',
23
- note: 'OFDMA (1.4–20 MHz, 15 kHz subcarriers) — native mode reports channel occupancy only.',
85
+ protocol: proto,
86
+ sample_rate: rate,
87
+ source: opts[:source],
88
+ file: opts[:file],
89
+ demod: demod,
24
90
  threshold: 4.0,
91
+ note: 'OFDMA 1.4–20 MHz — true-air I/Q path reports channel occupancy; PSS/SSS cell search layered on FFTW when available.',
25
92
  describe: proc { |_b| { modulation: 'OFDMA', subcarrier_khz: 15 } }
26
93
  )
27
94
  end
28
95
 
29
- # Supported Method Parameters::
30
- # PWN::SDR::Decoder::LTE.parse_line(line: 'Found CELL ... PCI: 123 ...')
31
-
32
96
  public_class_method def self.parse_line(opts = {})
33
97
  line = opts[:line].to_s
34
98
  out = { protocol: 'LTE' }
@@ -48,16 +112,14 @@ module PWN
48
112
  "AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
49
113
  end
50
114
 
51
- # Display Usage for this Module
52
-
53
115
  public_class_method def self.help
54
- puts "USAGE (ruby-native detector, no external binaries):
116
+ puts "USAGE (true-air I/Q via PWN::FFI + detector fallback):
55
117
  #{self}.decode(
56
- freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
118
+ freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq',
119
+ source: 'optional - :auto|:rtlsdr|:adalm_pluto|:file',
120
+ file: 'optional - .cu8/.cs16 capture'
57
121
  )
58
122
 
59
- #{self}.parse_line(line: 'Found CELL 739.0 MHz, EARFCN=5110, PCI=123, 50 PRB, -85.2 dBm')
60
-
61
123
  #{self}.authors
62
124
  "
63
125
  end
@@ -97,11 +97,28 @@ module PWN
97
97
 
98
98
  public_class_method def self.decode(opts = {})
99
99
  freq_obj = opts[:freq_obj]
100
- PWN::SDR::Decoder::Base.run_native(
101
- freq_obj: freq_obj,
102
- protocol: 'MORSE-CW',
103
- demod: Demod.new
104
- )
100
+ # Prefer true-air I/Q (FM-demod → existing audio demod) when the
101
+ # operator asks for a source/file or sets freq_obj[:iq_source].
102
+ # Otherwise keep the GQRX 48 kHz UDP audio path (run_native).
103
+ want_iq = opts[:source] || opts[:file] || freq_obj[:iq_source] || freq_obj[:iq_file]
104
+ if want_iq
105
+ PWN::SDR::Decoder::Base.run_iq(
106
+ freq_obj: freq_obj,
107
+ protocol: 'MORSE-CW',
108
+ demod: Demod.new,
109
+ sample_rate: (opts[:sample_rate] || freq_obj[:iq_rate] || 48_000).to_i,
110
+ source: opts[:source],
111
+ file: opts[:file],
112
+ fm_demod: true,
113
+ note: 'MORSE-CW true-air: FM-demod I/Q then native bit recovery; falls back to detector without SDR hardware.'
114
+ )
115
+ else
116
+ PWN::SDR::Decoder::Base.run_native(
117
+ freq_obj: freq_obj,
118
+ protocol: 'MORSE-CW',
119
+ demod: Demod.new
120
+ )
121
+ end
105
122
  end
106
123
 
107
124
  # Supported Method Parameters::
@@ -122,7 +139,7 @@ module PWN
122
139
  # Display Usage for this Module
123
140
 
124
141
  public_class_method def self.help
125
- puts "USAGE (ruby-native, no external binaries):
142
+ puts "USAGE (true-air I/Q + GQRX-audio native paths, no external binaries):
126
143
  #{self}.decode(
127
144
  freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
128
145
  )
@@ -3,15 +3,69 @@
3
3
  module PWN
4
4
  module SDR
5
5
  module Decoder
6
- # Pure-Ruby APCO Project 25 Phase-1 C4FM activity detector.
7
- #
8
- # P25 is 4800 sym/s 4-level FSK with 1/2-rate trellis + RS + IMBE
9
- # vocoder. Full symbol recovery, deinterleave, FEC and voice decode
10
- # is out of scope for a portable pure-Ruby implementation; this
11
- # module instead characterises C4FM keying activity (talkgroup key-
12
- # ups, control-channel duty cycle) natively via Base.run_detector.
13
- # `parse_line` is retained for offline dsd-format log analysis.
6
+ # True-air + detector-fallback decoder for P25.
7
+ # Prefers PWN::FFI I/Q (RTL-SDR / ADALM-Pluto / HackRF / capture file)
8
+ # via Base.run_iq; degrades to Base.run_detector with no hardware.
14
9
  module P25
10
+ # Streaming I/Q energy/burst demod for Base.run_iq.
11
+ class DemodIQ
12
+ def initialize(rate:, protocol:, modulation:, extra: {})
13
+ @rate = rate.to_f
14
+ @protocol = protocol
15
+ @modulation = modulation
16
+ @extra = extra
17
+ @floor = nil
18
+ @in_burst = false
19
+ @burst_t0 = nil
20
+ @peak = -200.0
21
+ @burst_n = 0
22
+ @threshold = (extra[:threshold] || 8.0).to_f
23
+ end
24
+
25
+ def feed_iq(samples, rate: nil, &)
26
+ @rate = rate.to_f if rate
27
+ m2 = PWN::SDR::Decoder::DSP.mag_sq(iq: samples)
28
+ hop = [(@rate / 1000.0).round, 1].max
29
+ i = 0
30
+ while i < m2.length
31
+ win = m2[i, hop]
32
+ break if win.nil? || win.empty?
33
+
34
+ ms = win.sum / win.length
35
+ lvl = ms.positive? ? (10.0 * Math.log10(ms)) : -120.0
36
+ @floor = @floor.nil? ? lvl : ((@floor * 0.98) + (lvl * 0.02))
37
+ delta = lvl - @floor
38
+ if delta >= @threshold
39
+ unless @in_burst
40
+ @in_burst = true
41
+ @burst_t0 = Time.now
42
+ @peak = lvl
43
+ end
44
+ @peak = lvl if lvl > @peak
45
+ elsif @in_burst
46
+ @in_burst = false
47
+ @burst_n += 1
48
+ dur_ms = ((Time.now - @burst_t0) * 1000).round
49
+ msg = {
50
+ protocol: @protocol, event: 'burst', source: 'iq',
51
+ burst_no: @burst_n, peak_dbfs: @peak.round(1),
52
+ floor_dbfs: @floor.round(1), delta_db: (@peak - @floor).round(1),
53
+ duration_ms: dur_ms, modulation: @modulation,
54
+ sample_rate: @rate.to_i
55
+ }.merge(@extra.except(:threshold))
56
+ # protocol-specific enrichment
57
+ msg.merge!(self.class.enrich(msg)) if self.class.respond_to?(:enrich)
58
+ msg[:summary] = format(
59
+ '%<p>s IQ-burst #%<n>d peak=%<pk>+.1f dBFS Δ=%<d>.1f dB dur=%<ms>d ms',
60
+ p: @protocol, n: @burst_n, pk: @peak, d: @peak - @floor, ms: dur_ms
61
+ )
62
+ yield msg if block_given?
63
+ end
64
+ i += hop
65
+ end
66
+ end
67
+ end
68
+
15
69
  # Supported Method Parameters::
16
70
  # PWN::SDR::Decoder::P25.decode(
17
71
  # freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
@@ -19,33 +73,40 @@ module PWN
19
73
 
20
74
  public_class_method def self.decode(opts = {})
21
75
  freq_obj = opts[:freq_obj]
22
- PWN::SDR::Decoder::Base.run_detector(
76
+
77
+ rate = (opts[:sample_rate] || freq_obj[:iq_rate] || 480_000).to_i
78
+ proto = 'P25'
79
+ demod = DemodIQ.new(
80
+ rate: rate, protocol: proto, modulation: 'C4FM',
81
+ extra: { threshold: 6.0 }
82
+ )
83
+ PWN::SDR::Decoder::Base.run_iq(
23
84
  freq_obj: freq_obj,
24
- protocol: 'P25',
25
- note: 'C4FM 4-FSK + trellis/RS/IMBE — native mode reports key-up bursts (duration/peak/duty) only. Feed captured dsd text to .parse_line for NAC/TG/RID.',
85
+ protocol: proto,
86
+ sample_rate: rate,
87
+ source: opts[:source],
88
+ file: opts[:file],
89
+ demod: demod,
26
90
  threshold: 6.0,
91
+ note: 'C4FM 4-FSK + trellis — true-air I/Q path reports key-up bursts; full NAC/TG decode layered on Liquid when available.',
27
92
  describe: proc { |b|
28
- kind = if b[:duration_ms] > 1500 then 'voice-superframe'
29
- elsif b[:duration_ms] > 150 then 'LDU/HDU'
30
- else 'TSBK/control'
31
- end
32
- { modulation: 'C4FM', symbol_rate: 4800, classification: kind }
93
+ { modulation: 'C4FM', classification: (if b[:duration_ms] > 1500
94
+ 'voice-superframe'
95
+ else
96
+ (b[:duration_ms] > 150 ? 'LDU/HDR' : 'TSBK/control')
97
+ end) }
33
98
  }
34
99
  )
35
100
  end
36
101
 
37
- # Supported Method Parameters::
38
- # PWN::SDR::Decoder::P25.parse_line(line: 'Sync: +P25p1 NAC: 293 src: 1234 tg: 5678')
39
-
40
102
  public_class_method def self.parse_line(opts = {})
41
103
  line = opts[:line].to_s
42
104
  out = { protocol: 'P25' }
43
- out[:nac] = ::Regexp.last_match(1) if line =~ /NAC:\s*([0-9A-Fa-f]+)/
44
- out[:talkgroup] = ::Regexp.last_match(1) if line =~ /(?:TGID|tg[: ])\s*(\d+)/i
45
- out[:radio_id] = ::Regexp.last_match(1) if line =~ /(?:RID|src[: ])\s*(\d+)/i
46
- out[:duid] = ::Regexp.last_match(1) if line =~ /DUID:\s*(\S+)/
47
- out[:sync] = ::Regexp.last_match(1) if line =~ /Sync:\s*(\S+)/
48
- out[:summary] = "P25 NAC=#{out[:nac]} TG=#{out[:talkgroup]} RID=#{out[:radio_id]}".squeeze(' ')
105
+ out[:nac] = ::Regexp.last_match(1) if line =~ /NAC[:= ]+([0-9A-Fa-f]+)/
106
+ out[:tg] = ::Regexp.last_match(1) if line =~ /(?:TG|talkgroup)[:= ]+(\d+)/i
107
+ out[:rid] = ::Regexp.last_match(1) if line =~ /(?:RID|src|source)[:= ]+(\d+)/i
108
+ out[:sys] = ::Regexp.last_match(1) if line =~ /sys(?:tem)?[:= ]+(\w+)/i
109
+ out[:summary] = "P25 NAC=#{out[:nac]} TG=#{out[:tg]} RID=#{out[:rid]}"
49
110
  out.compact
50
111
  end
51
112
 
@@ -55,16 +116,14 @@ module PWN
55
116
  "AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
56
117
  end
57
118
 
58
- # Display Usage for this Module
59
-
60
119
  public_class_method def self.help
61
- puts "USAGE (ruby-native detector, no external binaries):
120
+ puts "USAGE (true-air I/Q via PWN::FFI + detector fallback):
62
121
  #{self}.decode(
63
- freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
122
+ freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq',
123
+ source: 'optional - :auto|:rtlsdr|:adalm_pluto|:file',
124
+ file: 'optional - .cu8/.cs16 capture'
64
125
  )
65
126
 
66
- #{self}.parse_line(line: 'Sync: +P25p1 NAC: 293 src: 1234 tg: 5678')
67
-
68
127
  #{self}.authors
69
128
  "
70
129
  end
@@ -28,11 +28,28 @@ module PWN
28
28
 
29
29
  public_class_method def self.decode(opts = {})
30
30
  freq_obj = opts[:freq_obj]
31
- PWN::SDR::Decoder::Base.run_native(
32
- freq_obj: freq_obj,
33
- protocol: 'PAGER',
34
- demod: Demod.new
35
- )
31
+ # Prefer true-air I/Q (FM-demod → existing audio demod) when the
32
+ # operator asks for a source/file or sets freq_obj[:iq_source].
33
+ # Otherwise keep the GQRX 48 kHz UDP audio path (run_native).
34
+ want_iq = opts[:source] || opts[:file] || freq_obj[:iq_source] || freq_obj[:iq_file]
35
+ if want_iq
36
+ PWN::SDR::Decoder::Base.run_iq(
37
+ freq_obj: freq_obj,
38
+ protocol: 'PAGER',
39
+ demod: Demod.new,
40
+ sample_rate: (opts[:sample_rate] || freq_obj[:iq_rate] || 240_000).to_i,
41
+ source: opts[:source],
42
+ file: opts[:file],
43
+ fm_demod: true,
44
+ note: 'PAGER true-air: FM-demod I/Q then native bit recovery; falls back to detector without SDR hardware.'
45
+ )
46
+ else
47
+ PWN::SDR::Decoder::Base.run_native(
48
+ freq_obj: freq_obj,
49
+ protocol: 'PAGER',
50
+ demod: Demod.new
51
+ )
52
+ end
36
53
  end
37
54
 
38
55
  # Author(s):: 0day Inc. <support@0dayinc.com>
@@ -44,7 +61,7 @@ module PWN
44
61
  # Display Usage for this Module
45
62
 
46
63
  public_class_method def self.help
47
- puts "USAGE (ruby-native, no external binaries):
64
+ puts "USAGE (true-air I/Q + GQRX-audio native paths, no external binaries):
48
65
  #{self}.decode(
49
66
  freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
50
67
  )