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
@@ -250,17 +250,21 @@ module PWN
250
250
  end
251
251
  messages.push(user_role)
252
252
 
253
- # `max_tokens` is deprecated on /v1/chat/completions; the unified
254
- # parameter is `max_completion_tokens` and works for every chat model
253
+ # Wire config key `max_tokens` (kept for cross-engine naming parity with
254
+ # Anthropic/Gemini) to the OpenAI wire-format field `max_completion_tokens`.
255
+ # OpenAI deprecated the request-body key `max_tokens` on /v1/chat/completions
256
+ # in favour of `max_completion_tokens`, which works for every chat model
255
257
  # including the reasoning family. Don't try to guess per-model caps —
256
- # let the server clamp; default to a generous ceiling that the
257
- # operator can override via PWN::Env[:ai][:openai][:max_completion_tokens].
258
- max_completion_tokens = (engine[:max_completion_tokens] || 16_384).to_i
258
+ # let the server clamp; default to a generous ceiling that the operator
259
+ # can override via PWN::Env[:ai][:openai][:max_tokens].
260
+ # Accept legacy :max_completion_tokens as a one-release alias so existing
261
+ # pwn.yaml files keep working without a silent clamp to the default.
262
+ max_tokens = (engine[:max_tokens] || engine[:max_completion_tokens] || 16_384).to_i
259
263
 
260
264
  http_body = {
261
265
  model: model,
262
266
  messages: messages,
263
- max_completion_tokens: max_completion_tokens
267
+ max_completion_tokens: max_tokens
264
268
  }
265
269
  # Reasoning models reject sampler params (temperature, top_p, etc.)
266
270
  http_body[:temperature] = temp unless reasoning
data/lib/pwn/config.rb CHANGED
@@ -29,7 +29,7 @@ module PWN
29
29
  env = {
30
30
  ai: {
31
31
  active: 'grok',
32
- introspection: false,
32
+ module_reflection: false,
33
33
  grok: {
34
34
  base_uri: 'optional - Base URI for Grok - Use private base OR defaults to https://api.x.ai/v1',
35
35
  key: 'required - xAI Grok API Key',
@@ -58,6 +58,7 @@ module PWN
58
58
  model: 'optional - OpenAI model to use',
59
59
  system_role_content: 'You are an ethically hacking OpenAI agent.',
60
60
  temp: 'optional - OpenAI temperature',
61
+ max_tokens: 'optional - Max output tokens per response (default 16384). Mapped to OpenAI wire param max_completion_tokens.',
61
62
  max_prompt_length: 128_000
62
63
  },
63
64
  ollama: {
@@ -90,9 +91,9 @@ module PWN
90
91
  max_iters: 25,
91
92
  # Swarm (agent_ask/agent_debate) sub-agent recursion cap
92
93
  max_depth: 3,
93
- # run PWN::AI::Agent::Learning.auto_reflect after every final answer
94
- auto_reflect: true,
95
- # also run PWN::AI::Agent::Extrospection.auto_extrospect from auto_reflect
94
+ # run PWN::AI::Agent::Learning.auto_introspect after every final answer
95
+ auto_introspect: true,
96
+ # also run PWN::AI::Agent::Extrospection.auto_extrospect from auto_introspect
96
97
  auto_extrospect: false,
97
98
  toolsets: nil
98
99
  # multi-agent personas : ~/.pwn/agents.yml (see PWN::AI::Agent::Swarm.help)
@@ -0,0 +1,498 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ffi'
4
+
5
+ PubFFI = ::FFI unless defined?(PubFFI) # rubocop:disable Style/RedundantConstantBase
6
+
7
+ module PWN
8
+ module FFI
9
+ # Thin libiio binding specialised for the ADALM-PLUTO (AD9363).
10
+ #
11
+ # Control-plane + blocking RX of interleaved CS16 I/Q so
12
+ # PWN::SDR::Decoder::* can pull MHz-rate complex samples without
13
+ # GQRX's 48 kHz audio tap and without shelling out to iio_*.
14
+ # libad9361 is used only for optional bb_rate helpers when present.
15
+ #
16
+ # If libiio is missing `.available?` is false and callers fall back
17
+ # to RTLSdr / HackRF / SoapySDR / pure-Ruby detector paths.
18
+ #
19
+ # Default URI tries USB (local) first: "ip:192.168.2.1" is the stock
20
+ # Ethernet/USB-gadget address of an unconfigured Pluto.
21
+ module AdalmPluto
22
+ extend PubFFI::Library
23
+
24
+ @load_error = nil
25
+ begin
26
+ ffi_lib %w[iio libiio.so.0 libiio.so]
27
+ rescue LoadError => e
28
+ @load_error = e
29
+ end
30
+
31
+ class << self
32
+ attr_reader :load_error
33
+ end
34
+
35
+ # Optional libad9361 (bb rate convenience). Soft-fail.
36
+ @ad9361_load_error = nil
37
+ # Optional soft binding to libad9361 (bb_rate helper only).
38
+ module Ad9361
39
+ extend PubFFI::Library
40
+
41
+ @load_error = nil
42
+ begin
43
+ ffi_lib %w[ad9361 libad9361.so.0 libad9361.so]
44
+ rescue LoadError => e
45
+ @load_error = e
46
+ end
47
+ class << self
48
+ attr_reader :load_error
49
+ end
50
+ attach_function :ad9361_set_bb_rate, %i[pointer ulong], :int unless @load_error
51
+ end
52
+
53
+ unless @load_error
54
+ # ── library / strerror ─────────────────────────────────────────
55
+ attach_function :iio_library_get_version,
56
+ %i[pointer pointer pointer],
57
+ :void
58
+ attach_function :iio_strerror, %i[int pointer size_t], :void
59
+
60
+ # ── scan / enumerate ───────────────────────────────────────────
61
+ attach_function :iio_create_scan_context, %i[string int], :pointer
62
+ attach_function :iio_scan_context_destroy, [:pointer], :void
63
+ attach_function :iio_scan_context_get_info_list,
64
+ %i[pointer pointer],
65
+ :ssize_t
66
+ attach_function :iio_context_info_list_free, [:pointer], :void
67
+ attach_function :iio_context_info_get_description, [:pointer], :string
68
+ attach_function :iio_context_info_get_uri, [:pointer], :string
69
+
70
+ # ── context ────────────────────────────────────────────────────
71
+ attach_function :iio_create_default_context, [], :pointer
72
+ attach_function :iio_create_context_from_uri, [:string], :pointer
73
+ attach_function :iio_create_network_context, [:string], :pointer
74
+ attach_function :iio_context_destroy, [:pointer], :void
75
+ attach_function :iio_context_get_name, [:pointer], :string
76
+ attach_function :iio_context_get_description, [:pointer], :string
77
+ attach_function :iio_context_get_devices_count, [:pointer], :uint
78
+ attach_function :iio_context_get_device, %i[pointer uint], :pointer
79
+ attach_function :iio_context_find_device, %i[pointer string], :pointer
80
+ attach_function :iio_context_set_timeout, %i[pointer uint], :int
81
+
82
+ # ── device ─────────────────────────────────────────────────────
83
+ attach_function :iio_device_get_id, [:pointer], :string
84
+ attach_function :iio_device_get_name, [:pointer], :string
85
+ attach_function :iio_device_get_channels_count, [:pointer], :uint
86
+ attach_function :iio_device_get_channel, %i[pointer uint], :pointer
87
+ attach_function :iio_device_find_channel,
88
+ %i[pointer string bool],
89
+ :pointer
90
+ attach_function :iio_device_attr_write,
91
+ %i[pointer string string],
92
+ :ssize_t
93
+ attach_function :iio_device_attr_write_longlong,
94
+ %i[pointer string long_long],
95
+ :int
96
+ attach_function :iio_device_attr_read,
97
+ %i[pointer string pointer size_t],
98
+ :ssize_t
99
+ attach_function :iio_device_attr_read_longlong,
100
+ %i[pointer string pointer],
101
+ :int
102
+ attach_function :iio_device_create_buffer,
103
+ %i[pointer size_t bool],
104
+ :pointer
105
+
106
+ # ── channel ────────────────────────────────────────────────────
107
+ attach_function :iio_channel_get_id, [:pointer], :string
108
+ attach_function :iio_channel_get_name, [:pointer], :string
109
+ attach_function :iio_channel_is_output, [:pointer], :bool
110
+ attach_function :iio_channel_is_scan_element, [:pointer], :bool
111
+ attach_function :iio_channel_enable, [:pointer], :void
112
+ attach_function :iio_channel_disable, [:pointer], :void
113
+ attach_function :iio_channel_attr_write,
114
+ %i[pointer string string],
115
+ :ssize_t
116
+ attach_function :iio_channel_attr_write_longlong,
117
+ %i[pointer string long_long],
118
+ :int
119
+ attach_function :iio_channel_attr_read_longlong,
120
+ %i[pointer string pointer],
121
+ :int
122
+
123
+ # ── buffer ─────────────────────────────────────────────────────
124
+ attach_function :iio_buffer_destroy, [:pointer], :void
125
+ attach_function :iio_buffer_refill, [:pointer], :ssize_t
126
+ attach_function :iio_buffer_start, [:pointer], :pointer
127
+ attach_function :iio_buffer_end, [:pointer], :pointer
128
+ attach_function :iio_buffer_step, [:pointer], :ptrdiff_t
129
+ attach_function :iio_buffer_first, %i[pointer pointer], :pointer
130
+ attach_function :iio_buffer_set_blocking_mode, %i[pointer bool], :int
131
+ attach_function :iio_buffer_cancel, [:pointer], :void
132
+ end
133
+
134
+ DEFAULT_URI = 'ip:192.168.2.1'
135
+ PHY_NAME = 'ad9361-phy'
136
+ RX_NAME = 'cf-ad9361-lpc'
137
+ # Stock Pluto RX LO channel
138
+ RX_LO_NAME = 'altvoltage0'
139
+
140
+ # Supported Method Parameters::
141
+ # PWN::FFI::AdalmPluto.available?
142
+
143
+ public_class_method def self.available?
144
+ !@load_error && respond_to?(:iio_create_default_context, true)
145
+ rescue StandardError
146
+ false
147
+ end
148
+
149
+ # Supported Method Parameters::
150
+ # info = PWN::FFI::AdalmPluto.info
151
+ # Returns { available:, major:, minor:, git_tag:, ad9361: }
152
+
153
+ public_class_method def self.info
154
+ return { available: false, error: @load_error&.message } unless available?
155
+
156
+ maj = PubFFI::MemoryPointer.new(:uint)
157
+ min = PubFFI::MemoryPointer.new(:uint)
158
+ # git_tag is a caller-owned char[8] buffer (NOT char**).
159
+ git = PubFFI::MemoryPointer.new(:char, 8)
160
+ git.put_bytes(0, "\0" * 8)
161
+ iio_library_get_version(maj, min, git)
162
+ tag = git.read_string
163
+ {
164
+ available: true,
165
+ major: maj.read_uint,
166
+ minor: min.read_uint,
167
+ git_tag: tag,
168
+ ad9361: Ad9361.load_error.nil? && Ad9361.respond_to?(:ad9361_set_bb_rate, true)
169
+ }
170
+ end
171
+
172
+ # Supported Method Parameters::
173
+ # uris = PWN::FFI::AdalmPluto.list_uris(backends: 'usb,ip,local')
174
+ # Returns Array<Hash> of { uri:, description: }
175
+
176
+ public_class_method def self.list_uris(opts = {})
177
+ raise 'ERROR: libiio not available' unless available?
178
+
179
+ backends = (opts[:backends] || 'usb,ip,local').to_s
180
+ scan = iio_create_scan_context(backends, 0)
181
+ return [] if scan.null?
182
+
183
+ info_ptr = PubFFI::MemoryPointer.new(:pointer)
184
+ info_ptr.write_pointer(PubFFI::Pointer::NULL)
185
+ begin
186
+ n = iio_scan_context_get_info_list(scan, info_ptr)
187
+ return [] if n <= 0
188
+
189
+ base = info_ptr.read_pointer
190
+ return [] if base.null?
191
+
192
+ Array.new(n) do |i|
193
+ entry = base.get_pointer(i * PubFFI::Pointer.size)
194
+ next { uri: '', description: '' } if entry.null?
195
+
196
+ {
197
+ uri: iio_context_info_get_uri(entry).to_s,
198
+ description: iio_context_info_get_description(entry).to_s
199
+ }
200
+ end
201
+ ensure
202
+ base = info_ptr.read_pointer
203
+ iio_context_info_list_free(base) if base && !base.null?
204
+ iio_scan_context_destroy(scan)
205
+ end
206
+ rescue StandardError
207
+ []
208
+ end
209
+
210
+ # Supported Method Parameters::
211
+ # ctx = PWN::FFI::AdalmPluto.open(uri: 'ip:192.168.2.1')
212
+ # Returns opaque iio_context* pointer. ALWAYS pair with .close.
213
+
214
+ public_class_method def self.open(opts = {})
215
+ raise 'ERROR: libiio not available' unless available?
216
+
217
+ uri = opts[:uri]
218
+ ctx =
219
+ if uri.to_s.empty?
220
+ # try default USB/local first, then stock IP
221
+ c = iio_create_default_context
222
+ (c.null? ? iio_create_context_from_uri(DEFAULT_URI) : c)
223
+ else
224
+ iio_create_context_from_uri(uri.to_s)
225
+ end
226
+ raise "ERROR: iio_create_context failed for #{uri.inspect}" if ctx.null?
227
+
228
+ iio_context_set_timeout(ctx, (opts[:timeout_ms] || 5_000).to_i)
229
+ ctx
230
+ end
231
+
232
+ # Supported Method Parameters::
233
+ # PWN::FFI::AdalmPluto.close(context: pointer)
234
+
235
+ public_class_method def self.close(opts = {})
236
+ ctx = opts[:context]
237
+ return unless ctx && !ctx.null?
238
+
239
+ iio_context_destroy(ctx)
240
+ nil
241
+ end
242
+
243
+ # Supported Method Parameters::
244
+ # meta = PWN::FFI::AdalmPluto.device_info(context: pointer)
245
+ # Returns { name:, description:, devices: [{id:, name:}, ...] }
246
+
247
+ public_class_method def self.device_info(opts = {})
248
+ ctx = opts[:context]
249
+ raise 'ERROR: :context required' if ctx.nil? || ctx.null?
250
+
251
+ n = iio_context_get_devices_count(ctx)
252
+ devices = Array.new(n) do |i|
253
+ d = iio_context_get_device(ctx, i)
254
+ {
255
+ id: iio_device_get_id(d).to_s,
256
+ name: (iio_device_get_name(d) || '').to_s
257
+ }
258
+ end
259
+ {
260
+ name: iio_context_get_name(ctx).to_s,
261
+ description: iio_context_get_description(ctx).to_s,
262
+ devices: devices
263
+ }
264
+ end
265
+
266
+ # Supported Method Parameters::
267
+ # PWN::FFI::AdalmPluto.configure(
268
+ # context: 'required - pointer from .open',
269
+ # freq_hz: 'required - RX LO frequency Hz',
270
+ # rate_hz: 'optional - sample rate (default 2_500_000)',
271
+ # bw_hz: 'optional - RF bandwidth (default = rate_hz)',
272
+ # gain_db: 'optional - manual gain dB; nil = slow_attack AGC',
273
+ # gain_mode:'optional - slow_attack|fast_attack|manual|hybrid'
274
+ # )
275
+
276
+ public_class_method def self.configure(opts = {}) # rubocop:disable Naming/PredicateMethod
277
+ ctx = opts[:context]
278
+ raise 'ERROR: :context required' if ctx.nil? || ctx.null?
279
+
280
+ freq = opts[:freq_hz].to_i
281
+ rate = (opts[:rate_hz] || 2_500_000).to_i
282
+ bw = (opts[:bw_hz] || rate).to_i
283
+ raise 'ERROR: :freq_hz required' if freq <= 0
284
+
285
+ phy = iio_context_find_device(ctx, PHY_NAME)
286
+ raise "ERROR: phy device #{PHY_NAME.inspect} not found" if phy.null?
287
+
288
+ # RX LO
289
+ lo = iio_device_find_channel(phy, RX_LO_NAME, true) # output channel
290
+ if lo.null?
291
+ # some firmwares expose "RX_LO"
292
+ lo = iio_device_find_channel(phy, 'RX_LO', true)
293
+ end
294
+ raise 'ERROR: RX LO channel not found' if lo.null?
295
+
296
+ check_ssize!(iio_channel_attr_write_longlong(lo, 'frequency', freq), 'RX LO frequency')
297
+
298
+ # Sampling frequency + RF bandwidth on voltage0 (RX)
299
+ rx_chn = iio_device_find_channel(phy, 'voltage0', false)
300
+ raise 'ERROR: phy voltage0 (RX) not found' if rx_chn.null?
301
+
302
+ check_ssize!(iio_channel_attr_write_longlong(rx_chn, 'sampling_frequency', rate), 'sampling_frequency')
303
+ check_ssize!(iio_channel_attr_write_longlong(rx_chn, 'rf_bandwidth', bw), 'rf_bandwidth')
304
+
305
+ gain_mode = opts[:gain_mode]
306
+ if opts.key?(:gain_db) && !opts[:gain_db].nil?
307
+ gain_mode ||= 'manual'
308
+ check_ssize!(iio_channel_attr_write(rx_chn, 'gain_control_mode', gain_mode), 'gain_control_mode')
309
+ check_ssize!(iio_channel_attr_write_longlong(rx_chn, 'hardwaregain', opts[:gain_db].to_i), 'hardwaregain')
310
+ else
311
+ gain_mode ||= 'slow_attack'
312
+ check_ssize!(iio_channel_attr_write(rx_chn, 'gain_control_mode', gain_mode), 'gain_control_mode')
313
+ end
314
+
315
+ # Prefer libad9361 bb_rate when linked (programs FIR + HB chain)
316
+ if Ad9361.load_error.nil? && Ad9361.respond_to?(:ad9361_set_bb_rate, true)
317
+ begin
318
+ Ad9361.ad9361_set_bb_rate(phy, rate)
319
+ rescue StandardError
320
+ nil
321
+ end
322
+ end
323
+
324
+ true
325
+ end
326
+
327
+ # Supported Method Parameters::
328
+ # handle = PWN::FFI::AdalmPluto.start_rx(
329
+ # context: 'required - pointer from .open',
330
+ # samples: 'optional - samples-per-refill (default 262144)',
331
+ # cyclic: 'optional - cyclic buffer (default false)'
332
+ # )
333
+ # Returns { buffer:, rx:, i_chn:, q_chn:, samples: } — pair with .stop_rx.
334
+
335
+ public_class_method def self.start_rx(opts = {})
336
+ ctx = opts[:context]
337
+ raise 'ERROR: :context required' if ctx.nil? || ctx.null?
338
+
339
+ nsamps = (opts[:samples] || 262_144).to_i
340
+ rx = iio_context_find_device(ctx, RX_NAME)
341
+ raise "ERROR: RX streaming device #{RX_NAME.inspect} not found" if rx.null?
342
+
343
+ # Enable voltage0 (I) + voltage1 (Q) scan elements
344
+ i_chn = iio_device_find_channel(rx, 'voltage0', false)
345
+ q_chn = iio_device_find_channel(rx, 'voltage1', false)
346
+ raise 'ERROR: RX I/Q channels not found' if i_chn.null? || q_chn.null?
347
+
348
+ iio_channel_enable(i_chn)
349
+ iio_channel_enable(q_chn)
350
+
351
+ buf = iio_device_create_buffer(rx, nsamps, opts[:cyclic] ? true : false)
352
+ raise 'ERROR: iio_device_create_buffer failed' if buf.null?
353
+
354
+ iio_buffer_set_blocking_mode(buf, true)
355
+ { buffer: buf, rx: rx, i_chn: i_chn, q_chn: q_chn, samples: nsamps }
356
+ end
357
+
358
+ # Supported Method Parameters::
359
+ # iq_cs16 = PWN::FFI::AdalmPluto.read_sync(handle: hash_from_start_rx)
360
+ # Returns binary String of interleaved little-endian signed-16 I/Q
361
+ # (I0,Q0,I1,Q1,…) — same contract as a cs16 capture file.
362
+
363
+ public_class_method def self.read_sync(opts = {})
364
+ h = opts[:handle]
365
+ raise 'ERROR: :handle required' unless h.is_a?(Hash)
366
+
367
+ buf = h[:buffer]
368
+ raise 'ERROR: handle missing :buffer' if buf.nil? || buf.null?
369
+
370
+ nbytes = iio_buffer_refill(buf)
371
+ raise "ERROR: iio_buffer_refill rc=#{nbytes}" if nbytes.negative?
372
+
373
+ start = iio_buffer_start(buf)
374
+ # Pluto packs I then Q as sequential int16 scan elements; step covers both.
375
+ start.read_string(nbytes)
376
+ end
377
+
378
+ # Supported Method Parameters::
379
+ # PWN::FFI::AdalmPluto.stop_rx(handle: hash_from_start_rx)
380
+
381
+ public_class_method def self.stop_rx(opts = {})
382
+ h = opts[:handle]
383
+ return unless h.is_a?(Hash)
384
+
385
+ buf = h[:buffer]
386
+ if buf && !buf.null?
387
+ begin
388
+ iio_buffer_cancel(buf)
389
+ rescue StandardError
390
+ nil
391
+ end
392
+ iio_buffer_destroy(buf)
393
+ end
394
+ nil
395
+ end
396
+
397
+ # High-level one-shot: open → configure → start_rx → N refills → stop → close.
398
+ # Supported Method Parameters::
399
+ # iq = PWN::FFI::AdalmPluto.capture(
400
+ # freq_hz: 'required',
401
+ # rate_hz: 2_500_000,
402
+ # bytes: 1_048_576, # approximate payload size
403
+ # uri: nil,
404
+ # gain_db: nil
405
+ # )
406
+ # Returns { iq_cs16: String, rate_hz:, freq_hz:, samples: }
407
+
408
+ public_class_method def self.capture(opts = {})
409
+ raise 'ERROR: :freq_hz required' unless opts[:freq_hz]
410
+
411
+ rate = (opts[:rate_hz] || 2_500_000).to_i
412
+ want = (opts[:bytes] || 1_048_576).to_i
413
+ ctx = self.open(uri: opts[:uri], timeout_ms: opts[:timeout_ms])
414
+ handle = nil
415
+ begin
416
+ configure(
417
+ context: ctx,
418
+ freq_hz: opts[:freq_hz],
419
+ rate_hz: rate,
420
+ bw_hz: opts[:bw_hz],
421
+ gain_db: opts[:gain_db],
422
+ gain_mode: opts[:gain_mode]
423
+ )
424
+ # each sample = 4 bytes (I s16 + Q s16)
425
+ nsamps = (want / 4).clamp(4_096, 1_048_576)
426
+ handle = start_rx(context: ctx, samples: nsamps)
427
+ chunks = []
428
+ got = 0
429
+ while got < want
430
+ chunk = read_sync(handle: handle)
431
+ break if chunk.bytesize.zero?
432
+
433
+ chunks << chunk
434
+ got += chunk.bytesize
435
+ end
436
+ {
437
+ iq_cs16: chunks.join,
438
+ rate_hz: rate,
439
+ freq_hz: opts[:freq_hz].to_i,
440
+ samples: got / 4
441
+ }
442
+ ensure
443
+ stop_rx(handle: handle) if handle
444
+ close(context: ctx)
445
+ end
446
+ end
447
+
448
+ # Author(s):: 0day Inc. <support@0dayinc.com>
449
+
450
+ public_class_method def self.authors
451
+ "AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
452
+ end
453
+
454
+ # Display Usage for this Module
455
+
456
+ public_class_method def self.help
457
+ puts "USAGE:
458
+ #{self}.available?
459
+ #{self}.info
460
+ #{self}.list_uris
461
+ ctx = #{self}.open(uri: 'ip:192.168.2.1') # or nil for default
462
+ #{self}.device_info(context: ctx)
463
+ #{self}.configure(context: ctx, freq_hz:, rate_hz: 2_500_000, gain_db: nil)
464
+ h = #{self}.start_rx(context: ctx, samples: 262_144)
465
+ iq = #{self}.read_sync(handle: h) # cs16le interleaved String
466
+ #{self}.stop_rx(handle: h)
467
+ #{self}.close(context: ctx)
468
+
469
+ # one-shot
470
+ cap = #{self}.capture(freq_hz: 1090e6, rate_hz: 2_500_000, bytes: 1_048_576)
471
+
472
+ #{self}.authors
473
+ "
474
+ end
475
+
476
+ class << self
477
+ private
478
+
479
+ def check_ssize!(rc, what) # rubocop:disable Naming/MethodParameterName
480
+ return if rc.is_a?(Integer) && rc >= 0
481
+
482
+ msg = strerror((-rc).to_i)
483
+ raise "ERROR: AdalmPluto #{what} rc=#{rc} (#{msg})"
484
+ end
485
+
486
+ def strerror(err)
487
+ return err.to_s unless available?
488
+
489
+ buf = PubFFI::MemoryPointer.new(:char, 256)
490
+ iio_strerror(err, buf, 256)
491
+ buf.read_string
492
+ rescue StandardError
493
+ err.to_s
494
+ end
495
+ end
496
+ end
497
+ end
498
+ end