pwn 0.5.627 → 0.5.628

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.
@@ -65,6 +65,36 @@ module PWN
65
65
  attach_function :firfilt_rrrf_execute_block,
66
66
  %i[pointer pointer uint pointer],
67
67
  :int
68
+
69
+ # ── msresamp_crcf (complex arbitrary resampler) ───────────────
70
+ attach_function :msresamp_crcf_create, %i[float float], :pointer
71
+ attach_function :msresamp_crcf_destroy, [:pointer], :int
72
+ attach_function :msresamp_crcf_execute,
73
+ %i[pointer pointer uint pointer pointer],
74
+ :int
75
+
76
+ # ── nco_crcf (numerically-controlled oscillator / mixer) ──────
77
+ LIQUID_NCO = 0
78
+ LIQUID_VCO = 1
79
+ attach_function :nco_crcf_create, [:int], :pointer
80
+ attach_function :nco_crcf_destroy, [:pointer], :int
81
+ attach_function :nco_crcf_set_frequency, %i[pointer float], :int
82
+ attach_function :nco_crcf_mix_block_down,
83
+ %i[pointer pointer pointer uint],
84
+ :int
85
+
86
+ # ── gmskdem (GMSK demodulator: k samp/sym → bit) ──────────────
87
+ attach_function :gmskdem_create, %i[uint uint float], :pointer
88
+ attach_function :gmskdem_destroy, [:pointer], :int
89
+ attach_function :gmskdem_reset, [:pointer], :int
90
+ attach_function :gmskdem_demodulate,
91
+ %i[pointer pointer pointer],
92
+ :int
93
+
94
+ # ── fskdem (M-FSK demodulator: k samp/sym → symbol) ───────────
95
+ attach_function :fskdem_create, %i[uint uint float uint], :pointer
96
+ attach_function :fskdem_destroy, [:pointer], :int
97
+ attach_function :fskdem_demodulate, %i[pointer pointer], :uint
68
98
  end
69
99
 
70
100
  # Supported Method Parameters::
@@ -210,6 +240,151 @@ module PWN
210
240
  end
211
241
  end
212
242
 
243
+ # Supported Method Parameters::
244
+ # out = PWN::FFI::Liquid.resample_iq(
245
+ # iq: 'required - interleaved I/Q Array<Float>',
246
+ # rate: 'required - output/input ratio (>0)',
247
+ # as_db: 'optional - stop-band attenuation dB (default 60.0)'
248
+ # )
249
+ # Returns interleaved Array<Float> [I0,Q0,…] resampled by `rate`.
250
+
251
+ public_class_method def self.resample_iq(opts = {})
252
+ raise 'ERROR: libliquid not available' unless available?
253
+
254
+ iq = opts[:iq]
255
+ rate = opts[:rate].to_f
256
+ as_db = (opts[:as_db] || 60.0).to_f
257
+ n = iq.length / 2
258
+ raise 'ERROR: rate must be > 0' unless rate.positive?
259
+ return iq.dup if n.zero?
260
+
261
+ q = msresamp_crcf_create(rate, as_db)
262
+ raise 'ERROR: msresamp_crcf_create failed' if q.null?
263
+
264
+ begin
265
+ ny_max = [1, (2 + (2 * rate * n)).ceil].max
266
+ in_ptr = float_ptr(iq.first(n * 2))
267
+ out_ptr = PubFFI::MemoryPointer.new(:float, ny_max * 2)
268
+ ny_ptr = PubFFI::MemoryPointer.new(:uint, 1)
269
+ rc = msresamp_crcf_execute(q, in_ptr, n, out_ptr, ny_ptr)
270
+ raise "ERROR: msresamp_crcf_execute rc=#{rc}" unless rc.zero?
271
+
272
+ ny = ny_ptr.read_uint
273
+ out_ptr.read_array_of_float(ny * 2)
274
+ ensure
275
+ msresamp_crcf_destroy(q)
276
+ end
277
+ end
278
+
279
+ # Supported Method Parameters::
280
+ # out = PWN::FFI::Liquid.mix_down(
281
+ # iq: 'required - interleaved I/Q Array<Float>',
282
+ # freq: 'required - normalised angular freq (rad/sample, i.e. 2π·f/fs)'
283
+ # )
284
+ # Returns interleaved Array<Float> shifted down by `freq`.
285
+
286
+ public_class_method def self.mix_down(opts = {})
287
+ raise 'ERROR: libliquid not available' unless available?
288
+
289
+ iq = opts[:iq]
290
+ freq = opts[:freq].to_f
291
+ n = iq.length / 2
292
+ return [] if n.zero?
293
+
294
+ q = nco_crcf_create(LIQUID_NCO)
295
+ raise 'ERROR: nco_crcf_create failed' if q.null?
296
+
297
+ begin
298
+ nco_crcf_set_frequency(q, freq)
299
+ in_ptr = float_ptr(iq.first(n * 2))
300
+ out_ptr = PubFFI::MemoryPointer.new(:float, n * 2)
301
+ rc = nco_crcf_mix_block_down(q, in_ptr, out_ptr, n)
302
+ raise "ERROR: nco_crcf_mix_block_down rc=#{rc}" unless rc.zero?
303
+
304
+ out_ptr.read_array_of_float(n * 2)
305
+ ensure
306
+ nco_crcf_destroy(q)
307
+ end
308
+ end
309
+
310
+ # Supported Method Parameters::
311
+ # bits = PWN::FFI::Liquid.gmsk_demod(
312
+ # iq: 'required - interleaved I/Q Array<Float>, length = k·sym·2',
313
+ # sps: 'required - samples per symbol (integer ≥ 2)',
314
+ # m: 'optional - filter delay (default 3)',
315
+ # bt: 'optional - Gaussian BT (default 0.35)'
316
+ # )
317
+ # Returns Array<0|1> (one bit per symbol, floor(n/sps) bits).
318
+
319
+ public_class_method def self.gmsk_demod(opts = {})
320
+ raise 'ERROR: libliquid not available' unless available?
321
+
322
+ iq = opts[:iq]
323
+ k = opts[:sps].to_i
324
+ m = (opts[:m] || 3).to_i
325
+ bt = (opts[:bt] || 0.35).to_f
326
+ raise 'ERROR: sps must be ≥ 2' if k < 2
327
+
328
+ n = iq.length / 2
329
+ nsym = n / k
330
+ return [] if nsym.zero?
331
+
332
+ q = gmskdem_create(k, m, bt)
333
+ raise 'ERROR: gmskdem_create failed' if q.null?
334
+
335
+ begin
336
+ in_ptr = float_ptr(iq.first(nsym * k * 2))
337
+ bit_ptr = PubFFI::MemoryPointer.new(:uint, 1)
338
+ bits = Array.new(nsym)
339
+ i = 0
340
+ while i < nsym
341
+ gmskdem_demodulate(q, in_ptr + (i * k * 2 * 4), bit_ptr)
342
+ bits[i] = bit_ptr.read_uint & 1
343
+ i += 1
344
+ end
345
+ bits
346
+ ensure
347
+ gmskdem_destroy(q)
348
+ end
349
+ end
350
+
351
+ # Supported Method Parameters::
352
+ # syms = PWN::FFI::Liquid.mfsk_demod(
353
+ # iq: 'required - interleaved I/Q Array<Float>',
354
+ # m: 'required - bits per symbol (1=2FSK, 2=4FSK, …)',
355
+ # sps: 'required - samples per symbol (integer ≥ 2^m)',
356
+ # bw: 'optional - normalised bandwidth 0..0.5 (default 0.25)'
357
+ # )
358
+ # Returns Array<Integer> of demodulated symbols (0..2^m-1).
359
+
360
+ public_class_method def self.mfsk_demod(opts = {})
361
+ raise 'ERROR: libliquid not available' unless available?
362
+
363
+ iq = opts[:iq]
364
+ mb = opts[:m].to_i
365
+ k = opts[:sps].to_i
366
+ bw = (opts[:bw] || 0.25).to_f
367
+ n = iq.length / 2
368
+ nsym = n / k
369
+ return [] if nsym.zero?
370
+
371
+ q = fskdem_create(mb, k, bw, 0)
372
+ raise 'ERROR: fskdem_create failed' if q.null?
373
+
374
+ begin
375
+ in_ptr = float_ptr(iq.first(nsym * k * 2))
376
+ syms = Array.new(nsym)
377
+ i = 0
378
+ while i < nsym
379
+ syms[i] = fskdem_demodulate(q, in_ptr + (i * k * 2 * 4))
380
+ i += 1
381
+ end
382
+ syms
383
+ ensure
384
+ fskdem_destroy(q)
385
+ end
386
+ end
387
+
213
388
  # Author(s):: 0day Inc. <support@0dayinc.com>
214
389
 
215
390
  public_class_method def self.authors
@@ -225,6 +400,10 @@ module PWN
225
400
  #{self}.resample(samples:, rate:, as_db: 60.0) # arbitrary rate
226
401
  #{self}.fir_kaiser(samples:, length: 51, fc: 0.1, as_db: 60.0)
227
402
  #{self}.dc_block(samples:, m: 7, as_db: 60.0)
403
+ #{self}.resample_iq(iq:, rate:, as_db: 60.0) # complex resample
404
+ #{self}.mix_down(iq:, freq:) # NCO mix (rad/samp)
405
+ #{self}.gmsk_demod(iq:, sps:, m: 3, bt: 0.35) # I/Q → bits
406
+ #{self}.mfsk_demod(iq:, m:, sps:, bw: 0.25) # I/Q → symbols
228
407
 
229
408
  #{self}.authors
230
409
  "
data/lib/pwn/memory.rb CHANGED
@@ -82,6 +82,20 @@ module PWN
82
82
  results.to_a.first(limit).to_h
83
83
  end
84
84
 
85
+ # Supported Method Parameters::
86
+ # hits = PWN::Memory.recall_semantic(query: 'nmap sweep', limit: 6)
87
+ #
88
+ # Relevance-ranked recall via PWN::MemoryIndex (local Ollama embeddings
89
+ # + cosine over ~/.pwn/memory.idx). Falls back to substring .recall
90
+ # when no embedding backend is configured.
91
+ public_class_method def self.recall_semantic(opts = {})
92
+ return recall(query: opts[:query], limit: opts[:limit]) unless defined?(PWN::MemoryIndex) && PWN::MemoryIndex.available?
93
+
94
+ PWN::MemoryIndex.recall_semantic(query: opts[:query], limit: opts[:limit])
95
+ rescue StandardError
96
+ recall(query: opts[:query], limit: opts[:limit])
97
+ end
98
+
85
99
  # Supported Method Parameters::
86
100
  # PWN::Memory.forget(key: :some_key)
87
101
  public_class_method def self.forget(opts = {}) # rubocop:disable Naming/PredicateMethod
@@ -127,6 +141,7 @@ module PWN
127
141
  mem = PWN::Memory.load
128
142
  PWN::Memory.remember(key: :user_prefers_ruby, value: 'Always prefer pure Ruby + RestClient patterns', category: :preference)
129
143
  facts = PWN::Memory.recall(query: 'recon', category: :fact, limit: 10)
144
+ hits = PWN::Memory.recall_semantic(query: 'recon', limit: 6) # embedding-ranked
130
145
  PWN::Memory.forget(key: :some_key)
131
146
  PWN::Memory.clear
132
147
  context_str = PWN::Memory.to_context
@@ -0,0 +1,233 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'fileutils'
5
+ require 'digest'
6
+
7
+ module PWN
8
+ # PWN::MemoryIndex is a lightweight local embedding index over
9
+ # PWN::Memory (~/.pwn/memory.json) so PromptBuilder can inject the N
10
+ # MOST-RELEVANT memories for the current request instead of the N
11
+ # newest. Embeddings come from the local Ollama instance
12
+ # (PWN::Env[:ai][:ollama][:embed_model], default 'nomic-embed-text') via
13
+ # its native /api/embed endpoint — everything stays on-box.
14
+ #
15
+ # Index layout (~/.pwn/memory.idx):
16
+ # { "<key>": { "sha": "<sha16 of value>", "vec": [Float,…] }, … }
17
+ #
18
+ # Rebuilds are incremental: only entries whose value-sha changed are
19
+ # (re)embedded, so a warm index costs one embed call (the query).
20
+ module MemoryIndex
21
+ INDEX_FILE = File.join(Dir.home, '.pwn', 'memory.idx')
22
+ DEFAULT_EMBED_MODEL = 'nomic-embed-text'
23
+
24
+ # Supported Method Parameters::
25
+ # bool = PWN::MemoryIndex.available?
26
+ #
27
+ # True when a local Ollama base_uri is configured. All public methods
28
+ # degrade to substring recall when this is false.
29
+
30
+ public_class_method def self.available?
31
+ !ollama_base.nil?
32
+ rescue StandardError
33
+ false
34
+ end
35
+
36
+ # Supported Method Parameters::
37
+ # hits = PWN::MemoryIndex.recall_semantic(
38
+ # query: 'required - user request / search text',
39
+ # limit: 'optional - top-K by cosine similarity (default 6)'
40
+ # )
41
+ #
42
+ # Returns [{ key:, value:, category:, timestamp:, score: }, …]
43
+ # (newest-first Memory.recall shape + :score) or falls back to
44
+ # PWN::Memory.recall(query:) when embedding is unavailable.
45
+
46
+ public_class_method def self.recall_semantic(opts = {})
47
+ query = opts[:query].to_s
48
+ limit = (opts[:limit] || 6).to_i
49
+ mem = PWN::Memory.load
50
+ return [] if mem.empty? || query.strip.empty?
51
+
52
+ qv = embed(texts: [query]).first
53
+ return fallback(query: query, limit: limit) unless qv
54
+
55
+ idx = refresh(mem: mem)
56
+ scored = mem.map do |k, v|
57
+ vec = idx.dig(k, :vec)
58
+ next unless vec
59
+
60
+ { key: k, value: v[:value], category: v[:category], timestamp: v[:timestamp], score: cosine(a: qv, b: vec) }
61
+ end
62
+ scored.compact.sort_by { |h| -h[:score] }.first(limit)
63
+ rescue StandardError
64
+ fallback(query: query, limit: limit)
65
+ end
66
+
67
+ # Supported Method Parameters::
68
+ # ctx = PWN::MemoryIndex.to_context(query:, limit: 6)
69
+ #
70
+ # Drop-in replacement for PWN::Memory.to_context that ranks by
71
+ # relevance to +query+ instead of insertion order. Emitted format is
72
+ # identical so PromptBuilder needs no special-casing.
73
+
74
+ public_class_method def self.to_context(opts = {})
75
+ hits = recall_semantic(query: opts[:query], limit: opts[:limit] || 6)
76
+ return PWN::Memory.to_context(limit: opts[:limit] || 6) if hits.empty?
77
+
78
+ ctx = "\n\nPERSISTENT MEMORY (relevance-ranked for this request - use PWN::Memory.remember to store new ones):\n"
79
+ hits.each do |h|
80
+ ctx += "- #{h[:key]} [#{h[:category]} @ #{h[:timestamp]}]: #{h[:value].to_s[0, 300]}\n"
81
+ end
82
+ ctx
83
+ end
84
+
85
+ # Supported Method Parameters::
86
+ # idx = PWN::MemoryIndex.refresh(mem: 'optional - preloaded PWN::Memory hash')
87
+ #
88
+ # Incrementally (re)embed changed entries and prune deleted keys.
89
+ # Returns the in-memory index Hash keyed by memory key (Symbol).
90
+
91
+ public_class_method def self.refresh(opts = {})
92
+ mem = opts[:mem] || PWN::Memory.load
93
+ idx = load_index
94
+ idx.delete_if { |k, _| !mem.key?(k) }
95
+
96
+ todo = mem.reject { |k, v| idx.dig(k, :sha) == sha(text: v[:value].to_s) }
97
+ unless todo.empty?
98
+ vecs = embed(texts: todo.values.map { |v| "#{v[:category]}: #{v[:value]}" })
99
+ todo.keys.each_with_index do |k, i|
100
+ next unless vecs[i]
101
+
102
+ idx[k] = { sha: sha(text: mem[k][:value].to_s), vec: vecs[i] }
103
+ end
104
+ save_index(idx: idx)
105
+ end
106
+ idx
107
+ rescue StandardError
108
+ idx || {}
109
+ end
110
+
111
+ # Supported Method Parameters::
112
+ # vecs = PWN::MemoryIndex.embed(texts: ['a', 'b'])
113
+ #
114
+ # POST /api/embed on the local Ollama; returns Array<Array<Float>> (one
115
+ # vector per input, nil on per-item failure). Batches of 32.
116
+
117
+ public_class_method def self.embed(opts = {})
118
+ texts = Array(opts[:texts]).map(&:to_s)
119
+ base = ollama_base
120
+ return Array.new(texts.length) unless base && !texts.empty?
121
+
122
+ model = (PWN::Env.dig(:ai, :ollama, :embed_model) if defined?(PWN::Env)) || DEFAULT_EMBED_MODEL
123
+ browser = PWN::Plugins::TransparentBrowser.open(browser_type: :rest)
124
+ rest = browser[:browser]::Request
125
+ headers = { content_type: 'application/json; charset=UTF-8' }
126
+ token = PWN::Env.dig(:ai, :ollama, :key) if defined?(PWN::Env)
127
+ headers[:authorization] = "Bearer #{token}" if token
128
+
129
+ out = []
130
+ texts.each_slice(32) do |batch|
131
+ resp = rest.execute(
132
+ method: :post,
133
+ url: "#{base}/ollama/api/embed",
134
+ headers: headers,
135
+ payload: { model: model, input: batch }.to_json,
136
+ verify_ssl: false,
137
+ timeout: 120
138
+ )
139
+ j = JSON.parse(resp, symbolize_names: true)
140
+ out.concat(Array(j[:embeddings]))
141
+ end
142
+ out
143
+ rescue StandardError
144
+ Array.new(texts.length)
145
+ end
146
+
147
+ # Supported Method Parameters::
148
+ # PWN::MemoryIndex.reset
149
+
150
+ public_class_method def self.reset
151
+ FileUtils.rm_f(INDEX_FILE)
152
+ {}
153
+ end
154
+
155
+ # -----------------------------------------------------------------
156
+ # privates
157
+ # -----------------------------------------------------------------
158
+
159
+ private_class_method def self.load_index
160
+ return {} unless File.exist?(INDEX_FILE)
161
+
162
+ JSON.parse(File.read(INDEX_FILE), symbolize_names: true)
163
+ rescue StandardError
164
+ {}
165
+ end
166
+
167
+ private_class_method def self.save_index(opts = {})
168
+ FileUtils.mkdir_p(File.dirname(INDEX_FILE))
169
+ File.write(INDEX_FILE, JSON.generate(opts[:idx] || {}))
170
+ end
171
+
172
+ private_class_method def self.cosine(opts = {})
173
+ a = opts[:a]
174
+ b = opts[:b]
175
+ return 0.0 unless a && b && a.length == b.length
176
+
177
+ dot = na = nb = 0.0
178
+ a.each_with_index do |x, i|
179
+ y = b[i]
180
+ dot += x * y
181
+ na += x * x
182
+ nb += y * y
183
+ end
184
+ return 0.0 if na.zero? || nb.zero?
185
+
186
+ dot / (Math.sqrt(na) * Math.sqrt(nb))
187
+ end
188
+
189
+ private_class_method def self.sha(opts = {})
190
+ Digest::SHA256.hexdigest(opts[:text].to_s)[0, 16]
191
+ end
192
+
193
+ private_class_method def self.ollama_base
194
+ return nil unless defined?(PWN::Env) && PWN::Env.is_a?(Hash)
195
+
196
+ b = PWN::Env.dig(:ai, :ollama, :base_uri).to_s
197
+ b.start_with?('http') ? b.chomp('/') : nil
198
+ end
199
+
200
+ private_class_method def self.fallback(opts = {})
201
+ PWN::Memory.recall(query: opts[:query], limit: opts[:limit]).map do |k, v|
202
+ { key: k, value: v[:value], category: v[:category], timestamp: v[:timestamp], score: 0.0 }
203
+ end
204
+ rescue StandardError
205
+ []
206
+ end
207
+
208
+ # Author(s):: 0day Inc. <support@0dayinc.com>
209
+
210
+ public_class_method def self.authors
211
+ "AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
212
+ end
213
+
214
+ # Display Usage for this Module
215
+
216
+ public_class_method def self.help
217
+ puts <<~USAGE
218
+ USAGE:
219
+ PWN::MemoryIndex.available?
220
+ PWN::MemoryIndex.recall_semantic(query: 'nmap sweep', limit: 6)
221
+ PWN::MemoryIndex.to_context(query: 'nmap sweep', limit: 6) # PromptBuilder drop-in
222
+ PWN::MemoryIndex.refresh # incremental (re)embed
223
+ PWN::MemoryIndex.embed(texts: ['a', 'b']) # raw vectors
224
+ PWN::MemoryIndex.reset
225
+
226
+ Config:
227
+ PWN::Env[:ai][:ollama][:embed_model] = 'nomic-embed-text' # or bge-m3, mxbai-embed-large
228
+
229
+ #{self}.authors
230
+ USAGE
231
+ end
232
+ end
233
+ end