pwn 0.5.613 → 0.5.617

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 (134) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +10 -10
  3. data/README.md +117 -17
  4. data/bin/pwn +47 -2
  5. data/bin/pwn_gqrx_scanner +75 -18
  6. data/documentation/AI-Integration.md +36 -0
  7. data/documentation/BurpSuite.md +26 -0
  8. data/documentation/Contributing.md +31 -0
  9. data/documentation/Diagrams.md +140 -0
  10. data/documentation/Drivers.md +25 -0
  11. data/documentation/General-PWN-Usage.md +60 -0
  12. data/documentation/Home.md +78 -0
  13. data/documentation/How-PWN-Works.md +62 -0
  14. data/documentation/Installation.md +75 -0
  15. data/documentation/NmapIt.md +18 -0
  16. data/documentation/Plugins.md +59 -0
  17. data/documentation/Reporting.md +28 -0
  18. data/documentation/SAST.md +32 -0
  19. data/documentation/Skills-Memory-Learning.md +44 -0
  20. data/documentation/Transparent-Browser.md +32 -0
  21. data/documentation/Troubleshooting.md +38 -0
  22. data/documentation/What-is-PWN.md +53 -0
  23. data/documentation/Why-PWN.md +48 -0
  24. data/documentation/diagrams/ai-integration-tool-calling.svg +172 -0
  25. data/documentation/diagrams/build.sh +20 -0
  26. data/documentation/diagrams/burp-vs-zap-preference.svg +69 -0
  27. data/documentation/diagrams/code-scanning-sast.svg +172 -0
  28. data/documentation/diagrams/dot/_THEME.md +27 -0
  29. data/documentation/diagrams/dot/agent-tool-registry.dot +59 -0
  30. data/documentation/diagrams/dot/ai-integration-tool-calling.dot +50 -0
  31. data/documentation/diagrams/dot/aws-cloud-security.dot +48 -0
  32. data/documentation/diagrams/dot/burp-vs-zap-preference.dot +26 -0
  33. data/documentation/diagrams/dot/code-scanning-sast.dot +49 -0
  34. data/documentation/diagrams/dot/cron-scheduling.dot +40 -0
  35. data/documentation/diagrams/dot/driver-framework.dot +39 -0
  36. data/documentation/diagrams/dot/extrospection-world-awareness.dot +55 -0
  37. data/documentation/diagrams/dot/fuzzing-workflow.dot +47 -0
  38. data/documentation/diagrams/dot/history-to-drivers.dot +34 -0
  39. data/documentation/diagrams/dot/memory-skills-detailed.dot +60 -0
  40. data/documentation/diagrams/dot/network-infra-testing.dot +54 -0
  41. data/documentation/diagrams/dot/overall-pwn-architecture.dot +115 -0
  42. data/documentation/diagrams/dot/penetration-testing-workflow.dot +60 -0
  43. data/documentation/diagrams/dot/plugin-ecosystem.dot +45 -0
  44. data/documentation/diagrams/dot/pwn-ai-feedback-learning-loop.dot +98 -0
  45. data/documentation/diagrams/dot/pwn-repl-prototyping.dot +56 -0
  46. data/documentation/diagrams/dot/reporting-pipeline.dot +46 -0
  47. data/documentation/diagrams/dot/reverse-engineering-flow.dot +43 -0
  48. data/documentation/diagrams/dot/sdr-radio-flow.dot +45 -0
  49. data/documentation/diagrams/dot/sessions-cron-automation.dot +44 -0
  50. data/documentation/diagrams/dot/swarm-multi-agent.dot +70 -0
  51. data/documentation/diagrams/dot/web-application-testing.dot +49 -0
  52. data/documentation/diagrams/dot/zero-day-research-flow.dot +49 -0
  53. data/documentation/diagrams/driver-framework.svg +111 -0
  54. data/documentation/diagrams/fuzzing-workflow.svg +160 -0
  55. data/documentation/diagrams/history-to-drivers.svg +119 -0
  56. data/documentation/diagrams/memory-skills-detailed.svg +202 -0
  57. data/documentation/diagrams/network-infra-testing.svg +183 -0
  58. data/documentation/diagrams/overall-pwn-architecture.svg +405 -0
  59. data/documentation/diagrams/penetration-testing-workflow.svg +201 -0
  60. data/documentation/diagrams/plugin-ecosystem.svg +276 -0
  61. data/documentation/diagrams/pwn-ai-feedback-learning-loop.svg +306 -0
  62. data/documentation/diagrams/pwn-repl-prototyping.svg +168 -0
  63. data/documentation/diagrams/reporting-pipeline.svg +151 -0
  64. data/documentation/diagrams/reverse-engineering-flow.svg +134 -0
  65. data/documentation/diagrams/sessions-cron-automation.svg +136 -0
  66. data/documentation/diagrams/web-application-testing.svg +173 -0
  67. data/documentation/diagrams/zero-day-research-flow.svg +153 -0
  68. data/documentation/pwn-REPL.md +33 -0
  69. data/documentation/pwn-ai-Agent.md +43 -0
  70. data/git_commit.sh +1 -1
  71. data/lib/pwn/ai/agent/assembly.rb +1 -1
  72. data/lib/pwn/ai/agent/btc.rb +1 -1
  73. data/lib/pwn/ai/agent/burp_suite.rb +1 -1
  74. data/lib/pwn/ai/agent/extrospection.rb +618 -0
  75. data/lib/pwn/ai/agent/gqrx.rb +2 -2
  76. data/lib/pwn/ai/agent/hacker_one.rb +1 -1
  77. data/lib/pwn/ai/agent/introspection.rb +91 -0
  78. data/lib/pwn/ai/agent/learning.rb +379 -0
  79. data/lib/pwn/ai/agent/loop.rb +33 -3
  80. data/lib/pwn/ai/agent/metrics.rb +155 -0
  81. data/lib/pwn/ai/agent/prompt_builder.rb +32 -3
  82. data/lib/pwn/ai/agent/sast.rb +1 -1
  83. data/lib/pwn/ai/agent/swarm.rb +437 -0
  84. data/lib/pwn/ai/agent/tools/cron.rb +163 -0
  85. data/lib/pwn/ai/agent/tools/extrospection.rb +280 -0
  86. data/lib/pwn/ai/agent/tools/learning.rb +221 -0
  87. data/lib/pwn/ai/agent/tools/memory.rb +27 -0
  88. data/lib/pwn/ai/agent/tools/metrics.rb +61 -0
  89. data/lib/pwn/ai/agent/tools/sessions.rb +139 -0
  90. data/lib/pwn/ai/agent/tools/skills.rb +124 -8
  91. data/lib/pwn/ai/agent/tools/swarm.rb +229 -0
  92. data/lib/pwn/ai/agent/transparent_browser.rb +1 -1
  93. data/lib/pwn/ai/agent/vuln_gen.rb +2 -2
  94. data/lib/pwn/ai/agent.rb +5 -0
  95. data/lib/pwn/ai/anthropic.rb +19 -4
  96. data/lib/pwn/ai/grok.rb +249 -106
  97. data/lib/pwn/ai.rb +0 -1
  98. data/lib/pwn/config.rb +96 -36
  99. data/lib/pwn/cron.rb +16 -7
  100. data/lib/pwn/plugins/monkey_patch.rb +2 -2
  101. data/lib/pwn/plugins/repl.rb +158 -284
  102. data/lib/pwn/sdr/decoder/base.rb +251 -0
  103. data/lib/pwn/sdr/decoder/gsm.rb +84 -185
  104. data/lib/pwn/sdr/decoder/pocsag.rb +62 -217
  105. data/lib/pwn/sdr/decoder.rb +1 -0
  106. data/lib/pwn/sdr/gqrx.rb +446 -65
  107. data/lib/pwn/version.rb +1 -1
  108. data/spec/lib/pwn/ai/agent/extrospection_spec.rb +15 -0
  109. data/spec/lib/pwn/ai/agent/introspection_spec.rb +15 -0
  110. data/spec/lib/pwn/ai/agent/learning_spec.rb +68 -0
  111. data/spec/lib/pwn/ai/agent/metrics_spec.rb +27 -0
  112. data/spec/lib/pwn/ai/agent/swarm_spec.rb +34 -0
  113. data/spec/lib/pwn/ai/agent/tools/cron_spec.rb +40 -0
  114. data/spec/lib/pwn/ai/agent/tools/extrospection_spec.rb +40 -0
  115. data/spec/lib/pwn/ai/agent/tools/learning_spec.rb +30 -0
  116. data/spec/lib/pwn/ai/agent/tools/metrics_spec.rb +20 -0
  117. data/spec/lib/pwn/ai/agent/tools/sessions_spec.rb +35 -0
  118. data/spec/lib/pwn/ai/agent/tools/swarm_spec.rb +17 -0
  119. data/spec/lib/pwn/{ai/introspection_spec.rb → sdr/decoder/base_spec.rb} +3 -3
  120. data/third_party/pwn_rdoc.jsonl +178 -33
  121. metadata +110 -36
  122. data/documentation/PWN.png +0 -0
  123. data/documentation/PWN_Contributors_and_Users.png +0 -0
  124. data/documentation/fax-spectrogram.png +0 -0
  125. data/documentation/fax-waveform.png +0 -0
  126. data/documentation/frozen_string_literal_files_changed_from_false_to_true.txt +0 -54
  127. data/documentation/lifecycle_authz_replay.example.yaml +0 -27
  128. data/documentation/pwn_android_war_dialer_session.png +0 -0
  129. data/documentation/pwn_install.png +0 -0
  130. data/documentation/pwn_wallpaper.jpg +0 -0
  131. data/documentation/ringing-spectrogram.png +0 -0
  132. data/documentation/ringing-waveform.png +0 -0
  133. data/documentation/vulnerability_report_template.md +0 -37
  134. data/lib/pwn/ai/introspection.rb +0 -76
data/lib/pwn/sdr/gqrx.rb CHANGED
@@ -1237,6 +1237,363 @@ module PWN
1237
1237
 
1238
1238
  # Author(s):: 0day Inc. <support@0dayinc.com>
1239
1239
 
1240
+ public_class_method def self.get_spectrum_snapshot(opts = {})
1241
+ gqrx_sock = opts[:gqrx_sock]
1242
+ raise 'ERROR: gqrx_sock is required!' if gqrx_sock.nil?
1243
+
1244
+ center_freq = opts[:center_freq]
1245
+ center_freq ||= cmd(gqrx_sock: gqrx_sock, cmd: 'f')
1246
+ center_hz = PWN::SDR.hz_to_i(freq: center_freq)
1247
+
1248
+ sample_rate = (opts[:sample_rate] || 1_000_000).to_i
1249
+ nfft = (opts[:nfft] || 2048).to_i
1250
+ avg = (opts[:avg] || 4).to_i
1251
+ capture_secs = (opts[:capture_secs] || 0.05).to_f
1252
+ strength_offset_db = (opts[:strength_offset_db] || 0.0).to_f
1253
+
1254
+ num_samples = (sample_rate * capture_secs).to_i
1255
+ num_samples = [num_samples, nfft].max
1256
+ num_samples = ((num_samples.to_f / nfft).ceil * nfft).to_i
1257
+
1258
+ puts "[*] Capturing ~#{format('%.3f', capture_secs)}s I/Q (#{num_samples} samples @ #{sample_rate} SPS) => #{sample_rate / 1_000_000.0} MHz instantaneous span"
1259
+
1260
+ # Start a fresh short IQ recording for snapshot (entire visible band at once)
1261
+ begin
1262
+ cmd(gqrx_sock: gqrx_sock, cmd: 'U IQRECORD 0', resp_ok: 'RPRT 0')
1263
+ rescue StandardError
1264
+ nil
1265
+ end
1266
+ sleep 0.02
1267
+ cmd(gqrx_sock: gqrx_sock, cmd: 'U IQRECORD 1', resp_ok: 'RPRT 0')
1268
+ sleep(capture_secs + 0.12)
1269
+ cmd(gqrx_sock: gqrx_sock, cmd: 'U IQRECORD 0', resp_ok: 'RPRT 0')
1270
+
1271
+ # Newest raw I/Q produced by GQRX (usually ~/gqrx_*.raw , float32 I/Q interleaved @ sample_rate)
1272
+ home = Dir.home
1273
+ iq_raw_file = Dir.glob("#{home}/gqrx_*.raw").max_by { |f| File.mtime(f) }
1274
+ raise "ERROR: No GQRX .raw file found after capture (looked in #{home})" unless iq_raw_file && File.exist?(iq_raw_file)
1275
+
1276
+ # Read tail of most recent bytes
1277
+ total_bytes = num_samples * 8 # float32 * 2 channels
1278
+ fsize = File.size(iq_raw_file)
1279
+ start_pos = [0, fsize - total_bytes].max
1280
+ raw_bytes = File.binread(iq_raw_file, total_bytes, start_pos)
1281
+
1282
+ # ---- Pure-Ruby FFT + SDRangel-style peak detection (no external interpreters) ----
1283
+ raise 'ERROR: I/Q read empty or short' if raw_bytes.nil? || raw_bytes.bytesize < 8
1284
+
1285
+ # GQRX raw I/Q: little-endian float32 interleaved I,Q,I,Q,...
1286
+ floats = raw_bytes.unpack('e*')
1287
+ n_iq = [floats.length / 2, num_samples].min
1288
+ iq = Array.new(n_iq) { |i| Complex(floats[2 * i], floats[(2 * i) + 1]) }
1289
+
1290
+ # Hann window
1291
+ two_pi = 2.0 * Math::PI
1292
+ hann = Array.new(nfft) { |k| 0.5 * (1.0 - Math.cos(two_pi * k / (nfft - 1))) }
1293
+
1294
+ # Iterative in-place radix-2 Cooley-Tukey FFT (nfft must be a power of two)
1295
+ raise "ERROR: nfft (#{nfft}) must be a power of two" unless nfft.nobits?(nfft - 1)
1296
+
1297
+ log2n = Math.log2(nfft).to_i
1298
+ fft_proc = lambda do |x|
1299
+ n = x.length
1300
+ # bit-reversal permutation
1301
+ j = 0
1302
+ (0...(n - 1)).each do |i|
1303
+ x[i], x[j] = x[j], x[i] if i < j
1304
+ k = n >> 1
1305
+ while k <= j
1306
+ j -= k
1307
+ k >>= 1
1308
+ end
1309
+ j += k
1310
+ end
1311
+ # butterflies
1312
+ (1..log2n).each do |stage|
1313
+ m = 1 << stage
1314
+ half = m >> 1
1315
+ wm = Complex.polar(1.0, -two_pi / m)
1316
+ (0...n).step(m) do |kk|
1317
+ w = Complex(1.0, 0.0)
1318
+ (0...half).each do |jj|
1319
+ t = w * x[kk + jj + half]
1320
+ u = x[kk + jj]
1321
+ x[kk + jj] = u + t
1322
+ x[kk + jj + half] = u - t
1323
+ w *= wm
1324
+ end
1325
+ end
1326
+ end
1327
+ x
1328
+ end
1329
+
1330
+ # Overlapping (50%) windowed FFTs, power-averaged (Welch-style)
1331
+ hop = [nfft / 2, 1].max
1332
+ specs = []
1333
+ pos = 0
1334
+ while pos + nfft <= iq.length && specs.length < avg
1335
+ blk = Array.new(nfft) { |k| iq[pos + k] * hann[k] }
1336
+ sp = fft_proc.call(blk)
1337
+ half = nfft / 2
1338
+ shifted = sp[half, nfft - half] + sp[0, half]
1339
+ specs << shifted.map(&:abs2)
1340
+ pos += hop
1341
+ end
1342
+ if specs.empty?
1343
+ blk = Array.new(nfft) do |k|
1344
+ (k < iq.length ? iq[k] : Complex(0.0, 0.0)) * hann[k]
1345
+ end
1346
+ sp = fft_proc.call(blk)
1347
+ half = nfft / 2
1348
+ shifted = sp[half, nfft - half] + sp[0, half]
1349
+ specs << shifted.map(&:abs2)
1350
+ end
1351
+
1352
+ avg_pwr = Array.new(nfft, 0.0)
1353
+ specs.each { |ps| ps.each_with_index { |v, i| avg_pwr[i] += v } }
1354
+ cnt = specs.length.to_f
1355
+ avg_pwr.map! { |v| v / cnt }
1356
+
1357
+ db = avg_pwr.map { |v| (10.0 * Math.log10(v + 1e-12)) + strength_offset_db }
1358
+
1359
+ # fftshift(fftfreq(nfft, 1/sr)) => bins from -sr/2 .. +sr/2 (exclusive), step sr/nfft
1360
+ res_hz = sample_rate / nfft.to_f
1361
+ freq_off = Array.new(nfft) { |i| (i - (nfft / 2)) * res_hz }
1362
+
1363
+ bins_out = Array.new(nfft) do |ii|
1364
+ fh = (center_hz + freq_off[ii]).to_i
1365
+ {
1366
+ bin: ii,
1367
+ freq_hz: fh,
1368
+ freq: PWN::SDR.hz_to_s(freq: fh),
1369
+ power_db: db[ii].round(2)
1370
+ }
1371
+ end
1372
+
1373
+ # Noise floor: 12th percentile of dB
1374
+ sorted_db = db.sort
1375
+ pct_idx = ((sorted_db.length - 1) * 0.12).floor
1376
+ noise_floor = sorted_db[pct_idx].to_f
1377
+
1378
+ # SDRangel-style peak detection: local maxima above noise_floor+6dB with
1379
+ # min bin separation and >= 4 dB prominence.
1380
+ height_thr = noise_floor + 6.0
1381
+ min_dist = [3, (6000.0 / res_hz).to_i].max
1382
+ prom_thr = 4.0
1383
+
1384
+ candidates = []
1385
+ (1...(nfft - 1)).each do |i|
1386
+ next unless db[i] >= height_thr
1387
+ next unless db[i] > db[i - 1] && db[i] >= db[i + 1]
1388
+
1389
+ # prominence: peak - highest of the two side-valley minima toward the
1390
+ # nearest higher-or-equal neighbour (scipy.signal.peak_prominences algorithm)
1391
+ left_min = db[i]
1392
+ li = i - 1
1393
+ while li >= 0 && db[li] <= db[i]
1394
+ left_min = db[li] if db[li] < left_min
1395
+ li -= 1
1396
+ end
1397
+ right_min = db[i]
1398
+ ri = i + 1
1399
+ while ri < nfft && db[ri] <= db[i]
1400
+ right_min = db[ri] if db[ri] < right_min
1401
+ ri += 1
1402
+ end
1403
+ prom = db[i] - [left_min, right_min].max
1404
+ next if prom < prom_thr
1405
+
1406
+ candidates << { idx: i, pwr: db[i], prom: prom }
1407
+ end
1408
+
1409
+ # Enforce minimum distance between peaks (keep strongest first)
1410
+ candidates.sort_by! { |c| -c[:pwr] }
1411
+ selected = []
1412
+ candidates.each do |c|
1413
+ selected << c unless selected.any? { |s2| (s2[:idx] - c[:idx]).abs < min_dist }
1414
+ end
1415
+ selected.sort_by! { |c| c[:idx] }
1416
+
1417
+ edge_thr = noise_floor + 3.5
1418
+ signals = selected.map do |c|
1419
+ p = c[:idx]
1420
+ l = p
1421
+ l -= 1 while l.positive? && db[l] >= edge_thr
1422
+ r = p
1423
+ r += 1 while r < (nfft - 1) && db[r] >= edge_thr
1424
+ bw_hz = ([r - l, 1].max * res_hz).to_i
1425
+ center = (center_hz + freq_off[p]).to_i
1426
+ {
1427
+ hz: center,
1428
+ freq: PWN::SDR.hz_to_s(freq: center),
1429
+ power_db: c[:pwr].round(2),
1430
+ noise_floor_db: noise_floor.round(2),
1431
+ bw_hz: bw_hz,
1432
+ snr_db: (c[:pwr] - noise_floor).round(2),
1433
+ peak_bin: p,
1434
+ prominence_db: c[:prom].round(2)
1435
+ }
1436
+ end
1437
+
1438
+ # Fallback: simple threshold if prominence-based detection found nothing
1439
+ if signals.empty?
1440
+ bins_out.each do |b|
1441
+ next unless b[:power_db] > (noise_floor + 7.0)
1442
+
1443
+ signals << b.merge(
1444
+ hz: b[:freq_hz],
1445
+ noise_floor_db: noise_floor.round(2),
1446
+ bw_hz: (res_hz * 2).to_i,
1447
+ snr_db: (b[:power_db] - noise_floor).round(2)
1448
+ )
1449
+ end
1450
+ end
1451
+
1452
+ {
1453
+ center_freq_hz: center_hz,
1454
+ center_freq: PWN::SDR.hz_to_s(freq: center_hz),
1455
+ sample_rate: sample_rate,
1456
+ visible_span_hz: sample_rate,
1457
+ nfft: nfft,
1458
+ avg: avg,
1459
+ resolution_hz: res_hz.round(2),
1460
+ samples: iq.length,
1461
+ capture_secs: capture_secs,
1462
+ spectrum: bins_out,
1463
+ signals: signals,
1464
+ noise_floor_db: noise_floor.round(2),
1465
+ timestamp: Time.now.strftime('%Y-%m-%d %H:%M:%S%z')
1466
+ }
1467
+ rescue StandardError => e
1468
+ begin
1469
+ cmd(gqrx_sock: gqrx_sock, cmd: 'U IQRECORD 0', resp_ok: 'RPRT 0')
1470
+ rescue StandardError
1471
+ nil
1472
+ end
1473
+ raise e
1474
+ end
1475
+
1476
+ # Supported Method Parameters::
1477
+ # fast_resp = PWN::SDR::GQRX.fast_scan_range(
1478
+ # gqrx_sock: 'required - GQRX socket object',
1479
+ # ranges: 'required - Array<Hash> of {start_freq:, target_freq: }',
1480
+ # sample_rate: 'optional - Set this to GQRX visible input sample rate (the span width)',
1481
+ # nfft: 'optional - FFT size',
1482
+ # avg: 'optional',
1483
+ # capture_secs: 'optional',
1484
+ # strength_lock: 'optional',
1485
+ # keep_spectrum: 'optional - if true return raw spectrum arrays too (large)'
1486
+ # )
1487
+ #
1488
+ # Uses chunk-wise retuning where chunk = sample_rate so that the *entire visible band* (waterfall width)
1489
+ # is captured via a single FFT each time rather than point-by-point hops.
1490
+ # This yields near real-time panoramic coverage. Update rate is roughly (retune + capture + fft) per chunk.
1491
+ public_class_method def self.fast_scan_range(opts = {})
1492
+ gqrx_sock = opts[:gqrx_sock]
1493
+ raise 'gqrx_sock required' if gqrx_sock.nil?
1494
+
1495
+ ranges = opts[:ranges]
1496
+ raise 'ranges required as array of hashes' unless ranges.is_a?(Array) && !ranges.empty?
1497
+
1498
+ sr = (opts[:sample_rate] || 1_000_000).to_i
1499
+ nfft = opts[:nfft] || 2048
1500
+ avgs = opts[:avg] || 2
1501
+ cap = opts[:capture_secs] || 0.03
1502
+ strength_lock = opts[:strength_lock] || -70.0
1503
+ keep_spec = opts[:keep_spectrum] ? true : false
1504
+ scan_log = opts[:scan_log] || "/tmp/pwn_fastscan_#{Time.now.to_i}.json"
1505
+
1506
+ ts_start = Time.now.strftime('%Y-%m-%d %H:%M:%S%z')
1507
+ detected = []
1508
+ all_specs = [] if keep_spec
1509
+
1510
+ ranges.each do |r|
1511
+ s_hz = PWN::SDR.hz_to_i(freq: r[:start_freq])
1512
+ t_hz = PWN::SDR.hz_to_i(freq: r[:target_freq])
1513
+ dir = t_hz >= s_hz ? 1 : -1
1514
+ step = (sr * 0.85).to_i # chunk step - use 85% to have overlap / avoid edge artifacts
1515
+ step = sr if step < 100_000
1516
+
1517
+ puts "[FAST-SCAN] Panoramic covering #{PWN::SDR.hz_to_s(freq: s_hz)}..#{PWN::SDR.hz_to_s(freq: t_hz)} using #{sr} SPS chunks (step #{PWN::SDR.hz_to_s(freq: step)})"
1518
+
1519
+ h = s_hz
1520
+ while dir.positive? ? (h <= t_hz) : (h >= t_hz)
1521
+ # retune to put this chunk in the visible IF
1522
+ tune_to(gqrx_sock: gqrx_sock, hz: h)
1523
+ sleep 0.15 # allow GQRX / SDR to settle the IF filter & AGC etc.
1524
+
1525
+ snap = get_spectrum_snapshot(
1526
+ gqrx_sock: gqrx_sock,
1527
+ center_freq: h,
1528
+ sample_rate: sr,
1529
+ nfft: nfft,
1530
+ avg: avgs,
1531
+ capture_secs: cap,
1532
+ strength_offset_db: opts[:strength_offset_db]
1533
+ )
1534
+
1535
+ all_specs << snap if keep_spec
1536
+
1537
+ sigs = snap[:signals] || []
1538
+ if sigs.any?
1539
+ # Prefer SDRangel-style identified signals (with BW / SNR / noise_floor)
1540
+ sigs.each do |sig|
1541
+ next if sig[:power_db] && sig[:power_db] < strength_lock
1542
+
1543
+ detected << {
1544
+ hz: sig[:hz] || sig[:freq_hz],
1545
+ freq: sig[:freq],
1546
+ strength: sig[:power_db],
1547
+ bw_hz: sig[:bw_hz],
1548
+ snr_db: sig[:snr_db],
1549
+ noise_floor_db: sig[:noise_floor_db],
1550
+ chunk_center: PWN::SDR.hz_to_s(freq: h),
1551
+ method: :fast_spectrum_sdrangel_like
1552
+ }
1553
+ end
1554
+ else
1555
+ # legacy fallback on raw spectrum bins
1556
+ snap[:spectrum].each do |bin|
1557
+ next if bin[:power_db] < strength_lock
1558
+
1559
+ detected << {
1560
+ hz: bin[:freq_hz],
1561
+ freq: bin[:freq],
1562
+ strength: bin[:power_db],
1563
+ chunk_center: PWN::SDR.hz_to_s(freq: h),
1564
+ method: :fast_spectrum
1565
+ }
1566
+ end
1567
+ end
1568
+ h += step * dir
1569
+ end
1570
+ end
1571
+
1572
+ detected.uniq! { |d| d[:hz] }
1573
+ detected.sort_by! { |d| d[:hz] }
1574
+
1575
+ resp = {
1576
+ signals: detected,
1577
+ total: detected.length,
1578
+ spectrums: keep_spec ? all_specs : nil,
1579
+ timestamp_start: ts_start,
1580
+ timestamp_end: Time.now.strftime('%Y-%m-%d %H:%M:%S%z'),
1581
+ sample_rate_used: sr,
1582
+ nfft: nfft,
1583
+ method: :fast_scan_range
1584
+ }
1585
+
1586
+ File.write(scan_log, JSON.pretty_generate(resp))
1587
+ begin
1588
+ log_signals(signals_detected: detected, timestamp_start: ts_start, scan_log: scan_log)
1589
+ rescue StandardError
1590
+ nil
1591
+ end
1592
+ resp
1593
+ rescue StandardError => e
1594
+ raise e
1595
+ end
1596
+
1240
1597
  public_class_method def self.authors
1241
1598
  "AUTHOR(S):
1242
1599
  0day Inc. <support@0dayinc.com>
@@ -1246,82 +1603,106 @@ module PWN
1246
1603
  # Display Usage for this Module
1247
1604
 
1248
1605
  public_class_method def self.help
1249
- puts "USAGE:
1250
- gqrx_sock = #{self}.connect(
1251
- target: 'optional - GQRX target IP address (defaults to 127.0.0.1)',
1252
- port: 'optional - GQRX target port (defaults to 7356)'
1253
- )
1606
+ puts <<~USAGE
1607
+ USAGE:
1608
+ gqrx_sock = #{self}.connect(
1609
+ target: 'optional - GQRX target IP address (defaults to 127.0.0.1)',
1610
+ port: 'optional - GQRX target port (defaults to 7356)'
1611
+ )
1254
1612
 
1255
- gqrx_resp = #{self}.cmd(
1256
- gqrx_sock: 'required - GQRX socket object returned from #connect method',
1257
- cmd: 'required - GQRX command to send',
1258
- resp_ok: 'optional - Expected OK response (defaults to nil / no check)'
1259
- )
1613
+ gqrx_resp = #{self}.cmd(
1614
+ gqrx_sock: 'required - GQRX socket object returned from #connect method',
1615
+ cmd: 'required - GQRX command to send',
1616
+ resp_ok: 'optional - Expected OK response (defaults to nil / no check)'
1617
+ )
1260
1618
 
1261
- freq_obj = #{self}.init_freq(
1262
- gqrx_sock: 'required - GQRX socket object returned from #connect method',
1263
- freq: 'required - Frequency to set',
1264
- precision: 'optional - Frequency step precision (number of digits; defaults to 6)',
1265
- demodulator_mode: 'optional - Demodulator mode (defaults to WFM)',
1266
- bandwidth: 'optional - Bandwidth (defaults to \"200.000\")',
1267
- decoder: 'optional - Decoder key (e.g., :gsm) to start live decoding (starts recording if provided)',
1268
- suppress_details: 'optional - Boolean to include extra frequency details in return hash (defaults to false)',
1269
- keep_alive: 'optional - Boolean to keep GQRX connection alive after method completion (defaults to false)'
1270
- )
1619
+ freq_obj = #{self}.init_freq(
1620
+ gqrx_sock: 'required - GQRX socket object returned from #connect method',
1621
+ freq: 'required - Frequency to set',
1622
+ precision: 'optional - Frequency step precision (number of digits; defaults to 6)',
1623
+ demodulator_mode: 'optional - Demodulator mode (defaults to WFM)',
1624
+ bandwidth: 'optional - Bandwidth (defaults to "200.000")',
1625
+ decoder: 'optional - Decoder key (e.g., :gsm) to start live decoding (starts recording if provided)',
1626
+ suppress_details: 'optional - Boolean to include extra frequency details in return hash (defaults to false)',
1627
+ keep_alive: 'optional - Boolean to keep GQRX connection alive after method completion (defaults to false)'
1628
+ )
1271
1629
 
1272
- scan_resp = #{self}.scan_range(
1273
- gqrx_sock: 'required - GQRX socket object returned from #connect method',
1274
- ranges: 'required - Array of Hash objects with :start_freq and :target_freq keys defining scan ranges',
1275
- demodulator_mode: 'optional - Demodulator mode (e.g. WFM, AM, FM, USB, LSB, RAW, CW, RTTY / defaults to WFM)',
1276
- bandwidth: 'optional - Bandwidth in Hz (Defaults to \"200.000\")',
1277
- precision: 'optional - Precision (Defaults to 1)',
1278
- strength_lock: 'optional - Strength lock (defaults to -70.0)',
1279
- squelch: 'optional - Squelch level (defaults to strength_lock - 3.0)',
1280
- audio_gain_db: 'optional - Audio gain in dB (defaults to 0.0)',
1281
- rf_gain: 'optional - RF gain (defaults to 0.0)',
1282
- intermediate_gain: 'optional - Intermediate gain (defaults to 32.0)',
1283
- baseband_gain: 'optional - Baseband gain (defaults to 10.0)',
1284
- keep_looping: 'optional - Boolean to keep scanning indefinitely (defaults to false)',
1285
- scan_log: 'optional - Path to save detected signals log (defaults to /tmp/pwn_sdr_gqrx_scan_<start_freq>-<target_freq>_<timestamp>.json)',
1286
- location: 'optional - Location string to include in AI analysis (e.g., \"New York, NY\", 90210, GPS coords, etc.)'
1287
- )
1630
+ scan_resp = #{self}.scan_range(
1631
+ gqrx_sock: 'required - GQRX socket object returned from #connect method',
1632
+ ranges: 'required - Array of Hash objects with :start_freq and :target_freq keys defining scan ranges',
1633
+ demodulator_mode: 'optional - Demodulator mode (e.g. WFM, AM, FM, USB, LSB, RAW, CW, RTTY / defaults to WFM)',
1634
+ bandwidth: 'optional - Bandwidth in Hz (Defaults to "200.000")',
1635
+ precision: 'optional - Precision (Defaults to 1)',
1636
+ strength_lock: 'optional - Strength lock (defaults to -70.0)',
1637
+ squelch: 'optional - Squelch level (defaults to strength_lock - 3.0)',
1638
+ audio_gain_db: 'optional - Audio gain in dB (defaults to 0.0)',
1639
+ rf_gain: 'optional - RF gain (defaults to 0.0)',
1640
+ intermediate_gain: 'optional - Intermediate gain (defaults to 32.0)',
1641
+ baseband_gain: 'optional - Baseband gain (defaults to 10.0)',
1642
+ keep_looping: 'optional - Boolean to keep scanning indefinitely (defaults to false)',
1643
+ scan_log: 'optional - Path to save detected signals log (defaults to /tmp/pwn_sdr_gqrx_scan_<start_freq>-<target_freq>_<timestamp>.json)',
1644
+ location: 'optional - Location string to include in AI analysis (e.g., "New York, NY", 90210, GPS coords, etc.)'
1645
+ )
1288
1646
 
1289
- #{self}.analyze_scan(
1290
- scan_resp: 'required - Scan response object from #scan_range method',
1291
- target: 'optional - GQRX target IP address (defaults to 127.0.0.1)',
1292
- port: 'optional - GQRX target port (defaults to 7356)'
1293
- )
1647
+ snapshot = #{self}.get_spectrum_snapshot(
1648
+ gqrx_sock: 'required - GQRX socket object returned from #connect method',
1649
+ center_freq: 'optional - Center frequency (Hz) for snapshot (defaults to current tuned freq)',
1650
+ sample_rate: 'optional - Instantaneous bandwidth / sample rate in Hz (defaults to 1_000_000)',
1651
+ nfft: 'optional - FFT bin size (defaults to 2048)',
1652
+ avg: 'optional - Number of FFT averages (defaults to 4)',
1653
+ capture_secs: 'optional - Duration of I/Q capture in seconds (defaults to 0.05)',
1654
+ strength_offset_db: 'optional - Add this many dB to all power levels (defaults to 0.0)'
1655
+ )
1294
1656
 
1295
- #{self}.analyze_log(
1296
- scan_log: 'required - Path to signals log file',
1297
- target: 'optional - GQRX target IP address (defaults to 127.0.0.1)',
1298
- port: 'optional - GQRX target port (defaults to 7356)'
1299
- )
1657
+ fast_scan_resp = #{self}.fast_scan_range(
1658
+ gqrx_sock: 'required - GQRX socket object returned from #connect method',
1659
+ ranges: 'required - Array of Hash objects with :start_freq and :target_freq keys defining scan ranges',
1660
+ sample_rate: 'optional - Chunk size / visible span in Hz (defaults to 1_000_000)',
1661
+ nfft: 'optional - FFT size (defaults to 2048)',
1662
+ avg: 'optional - Number of averages (defaults to 2)',
1663
+ capture_secs: 'optional - Seconds of capture per chunk (defaults to 0.03)',
1664
+ strength_lock: 'optional - Minimum signal strength in dBFS to report (defaults to -70.0)',
1665
+ keep_spectrum: 'optional - If true, include full spectrum data in result (can be large, defaults to false)',
1666
+ strength_offset_db: 'optional - Add this many dB to all power levels (defaults to 0.0)',
1667
+ scan_log: 'optional - Path to save fast scan results (defaults to /tmp/pwn_fastscan_<timestamp>.json)'
1668
+ )
1300
1669
 
1301
- udp_listener = #{self}.listen_udp(
1302
- udp_ip: 'optional - IP address to bind UDP listener (defaults to 127.0.0.1)',
1303
- udp_port: 'optional - Port to bind UDP listener (defaults to 7355)'
1304
- )
1670
+ #{self}.analyze_scan(
1671
+ scan_resp: 'required - Scan response object from #scan_range or #fast_scan_range method',
1672
+ target: 'optional - GQRX target IP address (defaults to 127.0.0.1)',
1673
+ port: 'optional - GQRX target port (defaults to 7356)'
1674
+ )
1305
1675
 
1306
- #{self}.disconnect_udp(
1307
- udp_listener: 'required - UDP socket object returned from #listen_udp method'
1308
- )
1676
+ #{self}.analyze_log(
1677
+ scan_log: 'required - Path to signals log file',
1678
+ target: 'optional - GQRX target IP address (defaults to 127.0.0.1)',
1679
+ port: 'optional - GQRX target port (defaults to 7356)'
1680
+ )
1309
1681
 
1310
- iq_raw_file = #{self}.record(
1311
- gqrx_sock: 'required - GQRX socket object returned from #connect method'
1312
- )
1682
+ udp_listener = #{self}.listen_udp(
1683
+ udp_ip: 'optional - IP address to bind UDP listener (defaults to 127.0.0.1)',
1684
+ udp_port: 'optional - Port to bind UDP listener (defaults to 7355)'
1685
+ )
1313
1686
 
1314
- #{self}.stop_recording(
1315
- gqrx_sock: 'required - GQRX socket object returned from #connect method',
1316
- iq_raw_file: 'required - iq_raw_file returned from #record method'
1317
- )
1687
+ #{self}.disconnect_udp(
1688
+ udp_listener: 'required - UDP socket object returned from #listen_udp method'
1689
+ )
1318
1690
 
1319
- #{self}.disconnect(
1320
- gqrx_sock: 'required - GQRX socket object returned from #connect method'
1321
- )
1691
+ iq_raw_file = #{self}.record(
1692
+ gqrx_sock: 'required - GQRX socket object returned from #connect method'
1693
+ )
1322
1694
 
1323
- #{self}.authors
1324
- "
1695
+ #{self}.stop_recording(
1696
+ gqrx_sock: 'required - GQRX socket object returned from #connect method',
1697
+ iq_raw_file: 'required - iq_raw_file returned from #record method'
1698
+ )
1699
+
1700
+ #{self}.disconnect(
1701
+ gqrx_sock: 'required - GQRX socket object returned from #connect method'
1702
+ )
1703
+
1704
+ #{self}.authors
1705
+ USAGE
1325
1706
  end
1326
1707
  end
1327
1708
  end
data/lib/pwn/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PWN
4
- VERSION = '0.5.613'
4
+ VERSION = '0.5.617'
5
5
  end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe PWN::AI::Agent::Extrospection do
6
+ it 'should display information for authors' do
7
+ authors_response = PWN::AI::Agent::Extrospection
8
+ expect(authors_response).to respond_to :authors
9
+ end
10
+
11
+ it 'should display information for existing help method' do
12
+ help_response = PWN::AI::Agent::Extrospection
13
+ expect(help_response).to respond_to :help
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe PWN::AI::Agent::Introspection do
6
+ it 'should display information for authors' do
7
+ authors_response = PWN::AI::Agent::Introspection
8
+ expect(authors_response).to respond_to :authors
9
+ end
10
+
11
+ it 'should display information for existing help method' do
12
+ help_response = PWN::AI::Agent::Introspection
13
+ expect(help_response).to respond_to :help
14
+ end
15
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'tmpdir'
5
+
6
+ describe PWN::AI::Agent::Learning do
7
+ it 'should display information for authors' do
8
+ authors_response = PWN::AI::Agent::Learning
9
+ expect(authors_response).to respond_to :authors
10
+ end
11
+
12
+ it 'should display information for existing help method' do
13
+ help_response = PWN::AI::Agent::Learning
14
+ expect(help_response).to respond_to :help
15
+ end
16
+
17
+ it 'notes outcomes, surfaces context, and consolidates memory' do
18
+ tmp = Dir.mktmpdir
19
+ stub_const('PWN::AI::Agent::Learning::LEARNING_FILE', File.join(tmp, 'learning.jsonl'))
20
+ stub_const('PWN::Memory::MEMORY_FILE', File.join(tmp, 'memory.json'))
21
+
22
+ PWN::AI::Agent::Learning.reset
23
+ e = PWN::AI::Agent::Learning.note_outcome(task: 'nmap sweep', success: true, details: '3 hosts up', tags: %w[recon])
24
+ expect(e[:success]).to be true
25
+
26
+ rows = PWN::AI::Agent::Learning.outcomes(limit: 10)
27
+ expect(rows.first[:task]).to eq 'nmap sweep'
28
+
29
+ ctx = PWN::AI::Agent::Learning.to_context
30
+ expect(ctx).to include('nmap sweep')
31
+
32
+ stats = PWN::AI::Agent::Learning.stats
33
+ expect(stats[:total_outcomes]).to be >= 1
34
+
35
+ # duplicate lesson in memory then consolidate
36
+ PWN::Memory.remember(key: :dup_a, value: 'same lesson', category: :lesson)
37
+ PWN::Memory.remember(key: :dup_b, value: 'same lesson', category: :lesson)
38
+ res = PWN::AI::Agent::Learning.consolidate(max_entries: 100)
39
+ expect(res[:removed]).to be >= 1
40
+ end
41
+
42
+ it 'reflects on a session using the heuristic extractor' do
43
+ tmp = Dir.mktmpdir
44
+ stub_const('PWN::AI::Agent::Learning::LEARNING_FILE', File.join(tmp, 'learning.jsonl'))
45
+ stub_const('PWN::Memory::MEMORY_FILE', File.join(tmp, 'memory.json'))
46
+ stub_const('PWN::Sessions::SESSIONS_DIR', File.join(tmp, 'sessions'))
47
+
48
+ s = PWN::Sessions.create(title: 'learning spec')
49
+ PWN::Sessions.append(session_id: s[:id], role: 'user', content: 'scan target')
50
+ PWN::Sessions.append(session_id: s[:id], role: 'tool', content: 'shell → {"success":false,"error":"timeout after 120s"}')
51
+ PWN::Sessions.append(session_id: s[:id], role: 'assistant', content: 'Retry with -T2')
52
+
53
+ report = PWN::AI::Agent::Learning.reflect(session_id: s[:id])
54
+ expect(report[:count]).to be >= 1
55
+ expect(PWN::Memory.recall(query: 'failure').keys).not_to be_empty
56
+ end
57
+
58
+ it 'distills a skill from an explicit body' do
59
+ tmp = Dir.mktmpdir
60
+ stub_const('PWN::AI::Agent::Learning::LEARNING_FILE', File.join(tmp, 'learning.jsonl'))
61
+ stub_const('PWN::Memory::MEMORY_FILE', File.join(tmp, 'memory.json'))
62
+ allow(PWN::AI::Agent::Learning).to receive(:skills_dir).and_return(File.join(tmp, 'skills'))
63
+
64
+ out = PWN::AI::Agent::Learning.distill_skill(name: 'spec_skill', content: "# Spec Skill\nDo the thing.")
65
+ expect(out[:saved]).to be true
66
+ expect(File.exist?(out[:path])).to be true
67
+ end
68
+ end