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.
- checksums.yaml +4 -4
- data/Gemfile +10 -10
- data/README.md +117 -17
- data/bin/pwn +47 -2
- data/bin/pwn_gqrx_scanner +75 -18
- data/documentation/AI-Integration.md +36 -0
- data/documentation/BurpSuite.md +26 -0
- data/documentation/Contributing.md +31 -0
- data/documentation/Diagrams.md +140 -0
- data/documentation/Drivers.md +25 -0
- data/documentation/General-PWN-Usage.md +60 -0
- data/documentation/Home.md +78 -0
- data/documentation/How-PWN-Works.md +62 -0
- data/documentation/Installation.md +75 -0
- data/documentation/NmapIt.md +18 -0
- data/documentation/Plugins.md +59 -0
- data/documentation/Reporting.md +28 -0
- data/documentation/SAST.md +32 -0
- data/documentation/Skills-Memory-Learning.md +44 -0
- data/documentation/Transparent-Browser.md +32 -0
- data/documentation/Troubleshooting.md +38 -0
- data/documentation/What-is-PWN.md +53 -0
- data/documentation/Why-PWN.md +48 -0
- data/documentation/diagrams/ai-integration-tool-calling.svg +172 -0
- data/documentation/diagrams/build.sh +20 -0
- data/documentation/diagrams/burp-vs-zap-preference.svg +69 -0
- data/documentation/diagrams/code-scanning-sast.svg +172 -0
- data/documentation/diagrams/dot/_THEME.md +27 -0
- data/documentation/diagrams/dot/agent-tool-registry.dot +59 -0
- data/documentation/diagrams/dot/ai-integration-tool-calling.dot +50 -0
- data/documentation/diagrams/dot/aws-cloud-security.dot +48 -0
- data/documentation/diagrams/dot/burp-vs-zap-preference.dot +26 -0
- data/documentation/diagrams/dot/code-scanning-sast.dot +49 -0
- data/documentation/diagrams/dot/cron-scheduling.dot +40 -0
- data/documentation/diagrams/dot/driver-framework.dot +39 -0
- data/documentation/diagrams/dot/extrospection-world-awareness.dot +55 -0
- data/documentation/diagrams/dot/fuzzing-workflow.dot +47 -0
- data/documentation/diagrams/dot/history-to-drivers.dot +34 -0
- data/documentation/diagrams/dot/memory-skills-detailed.dot +60 -0
- data/documentation/diagrams/dot/network-infra-testing.dot +54 -0
- data/documentation/diagrams/dot/overall-pwn-architecture.dot +115 -0
- data/documentation/diagrams/dot/penetration-testing-workflow.dot +60 -0
- data/documentation/diagrams/dot/plugin-ecosystem.dot +45 -0
- data/documentation/diagrams/dot/pwn-ai-feedback-learning-loop.dot +98 -0
- data/documentation/diagrams/dot/pwn-repl-prototyping.dot +56 -0
- data/documentation/diagrams/dot/reporting-pipeline.dot +46 -0
- data/documentation/diagrams/dot/reverse-engineering-flow.dot +43 -0
- data/documentation/diagrams/dot/sdr-radio-flow.dot +45 -0
- data/documentation/diagrams/dot/sessions-cron-automation.dot +44 -0
- data/documentation/diagrams/dot/swarm-multi-agent.dot +70 -0
- data/documentation/diagrams/dot/web-application-testing.dot +49 -0
- data/documentation/diagrams/dot/zero-day-research-flow.dot +49 -0
- data/documentation/diagrams/driver-framework.svg +111 -0
- data/documentation/diagrams/fuzzing-workflow.svg +160 -0
- data/documentation/diagrams/history-to-drivers.svg +119 -0
- data/documentation/diagrams/memory-skills-detailed.svg +202 -0
- data/documentation/diagrams/network-infra-testing.svg +183 -0
- data/documentation/diagrams/overall-pwn-architecture.svg +405 -0
- data/documentation/diagrams/penetration-testing-workflow.svg +201 -0
- data/documentation/diagrams/plugin-ecosystem.svg +276 -0
- data/documentation/diagrams/pwn-ai-feedback-learning-loop.svg +306 -0
- data/documentation/diagrams/pwn-repl-prototyping.svg +168 -0
- data/documentation/diagrams/reporting-pipeline.svg +151 -0
- data/documentation/diagrams/reverse-engineering-flow.svg +134 -0
- data/documentation/diagrams/sessions-cron-automation.svg +136 -0
- data/documentation/diagrams/web-application-testing.svg +173 -0
- data/documentation/diagrams/zero-day-research-flow.svg +153 -0
- data/documentation/pwn-REPL.md +33 -0
- data/documentation/pwn-ai-Agent.md +43 -0
- data/git_commit.sh +1 -1
- data/lib/pwn/ai/agent/assembly.rb +1 -1
- data/lib/pwn/ai/agent/btc.rb +1 -1
- data/lib/pwn/ai/agent/burp_suite.rb +1 -1
- data/lib/pwn/ai/agent/extrospection.rb +618 -0
- data/lib/pwn/ai/agent/gqrx.rb +2 -2
- data/lib/pwn/ai/agent/hacker_one.rb +1 -1
- data/lib/pwn/ai/agent/introspection.rb +91 -0
- data/lib/pwn/ai/agent/learning.rb +379 -0
- data/lib/pwn/ai/agent/loop.rb +33 -3
- data/lib/pwn/ai/agent/metrics.rb +155 -0
- data/lib/pwn/ai/agent/prompt_builder.rb +32 -3
- data/lib/pwn/ai/agent/sast.rb +1 -1
- data/lib/pwn/ai/agent/swarm.rb +437 -0
- data/lib/pwn/ai/agent/tools/cron.rb +163 -0
- data/lib/pwn/ai/agent/tools/extrospection.rb +280 -0
- data/lib/pwn/ai/agent/tools/learning.rb +221 -0
- data/lib/pwn/ai/agent/tools/memory.rb +27 -0
- data/lib/pwn/ai/agent/tools/metrics.rb +61 -0
- data/lib/pwn/ai/agent/tools/sessions.rb +139 -0
- data/lib/pwn/ai/agent/tools/skills.rb +124 -8
- data/lib/pwn/ai/agent/tools/swarm.rb +229 -0
- data/lib/pwn/ai/agent/transparent_browser.rb +1 -1
- data/lib/pwn/ai/agent/vuln_gen.rb +2 -2
- data/lib/pwn/ai/agent.rb +5 -0
- data/lib/pwn/ai/anthropic.rb +19 -4
- data/lib/pwn/ai/grok.rb +249 -106
- data/lib/pwn/ai.rb +0 -1
- data/lib/pwn/config.rb +96 -36
- data/lib/pwn/cron.rb +16 -7
- data/lib/pwn/plugins/monkey_patch.rb +2 -2
- data/lib/pwn/plugins/repl.rb +158 -284
- data/lib/pwn/sdr/decoder/base.rb +251 -0
- data/lib/pwn/sdr/decoder/gsm.rb +84 -185
- data/lib/pwn/sdr/decoder/pocsag.rb +62 -217
- data/lib/pwn/sdr/decoder.rb +1 -0
- data/lib/pwn/sdr/gqrx.rb +446 -65
- data/lib/pwn/version.rb +1 -1
- data/spec/lib/pwn/ai/agent/extrospection_spec.rb +15 -0
- data/spec/lib/pwn/ai/agent/introspection_spec.rb +15 -0
- data/spec/lib/pwn/ai/agent/learning_spec.rb +68 -0
- data/spec/lib/pwn/ai/agent/metrics_spec.rb +27 -0
- data/spec/lib/pwn/ai/agent/swarm_spec.rb +34 -0
- data/spec/lib/pwn/ai/agent/tools/cron_spec.rb +40 -0
- data/spec/lib/pwn/ai/agent/tools/extrospection_spec.rb +40 -0
- data/spec/lib/pwn/ai/agent/tools/learning_spec.rb +30 -0
- data/spec/lib/pwn/ai/agent/tools/metrics_spec.rb +20 -0
- data/spec/lib/pwn/ai/agent/tools/sessions_spec.rb +35 -0
- data/spec/lib/pwn/ai/agent/tools/swarm_spec.rb +17 -0
- data/spec/lib/pwn/{ai/introspection_spec.rb → sdr/decoder/base_spec.rb} +3 -3
- data/third_party/pwn_rdoc.jsonl +178 -33
- metadata +110 -36
- data/documentation/PWN.png +0 -0
- data/documentation/PWN_Contributors_and_Users.png +0 -0
- data/documentation/fax-spectrogram.png +0 -0
- data/documentation/fax-waveform.png +0 -0
- data/documentation/frozen_string_literal_files_changed_from_false_to_true.txt +0 -54
- data/documentation/lifecycle_authz_replay.example.yaml +0 -27
- data/documentation/pwn_android_war_dialer_session.png +0 -0
- data/documentation/pwn_install.png +0 -0
- data/documentation/pwn_wallpaper.jpg +0 -0
- data/documentation/ringing-spectrogram.png +0 -0
- data/documentation/ringing-waveform.png +0 -0
- data/documentation/vulnerability_report_template.md +0 -37
- 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
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
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
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
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
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
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
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
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
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
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
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
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
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
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
|
-
|
|
1307
|
-
|
|
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
|
-
|
|
1311
|
-
|
|
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
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
)
|
|
1687
|
+
#{self}.disconnect_udp(
|
|
1688
|
+
udp_listener: 'required - UDP socket object returned from #listen_udp method'
|
|
1689
|
+
)
|
|
1318
1690
|
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1691
|
+
iq_raw_file = #{self}.record(
|
|
1692
|
+
gqrx_sock: 'required - GQRX socket object returned from #connect method'
|
|
1693
|
+
)
|
|
1322
1694
|
|
|
1323
|
-
|
|
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
|
@@ -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
|