pwn 0.5.618 → 0.5.621

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 (107) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +2 -2
  3. data/README.md +14 -6
  4. data/bin/pwn_gqrx_scanner +25 -5
  5. data/documentation/Agent-Tool-Registry.md +4 -4
  6. data/documentation/Cron.md +13 -0
  7. data/documentation/Diagrams.md +6 -2
  8. data/documentation/Extrospection.md +96 -11
  9. data/documentation/Home.md +5 -4
  10. data/documentation/How-PWN-Works.md +7 -4
  11. data/documentation/Mistakes.md +97 -0
  12. data/documentation/Persistence.md +4 -1
  13. data/documentation/SDR.md +23 -5
  14. data/documentation/Skills-Memory-Learning.md +16 -5
  15. data/documentation/Transparent-Browser.md +18 -1
  16. data/documentation/diagrams/agent-tool-registry.svg +103 -100
  17. data/documentation/diagrams/ai-integration-tool-calling.svg +24 -24
  18. data/documentation/diagrams/aws-cloud-security.svg +25 -25
  19. data/documentation/diagrams/burp-vs-zap-preference.svg +12 -12
  20. data/documentation/diagrams/code-scanning-sast.svg +25 -25
  21. data/documentation/diagrams/cron-scheduling.svg +23 -23
  22. data/documentation/diagrams/dot/agent-tool-registry.dot +3 -3
  23. data/documentation/diagrams/dot/extrospection-world-awareness.dot +33 -10
  24. data/documentation/diagrams/dot/memory-skills-detailed.dot +16 -5
  25. data/documentation/diagrams/dot/mistakes-negative-feedback.dot +93 -0
  26. data/documentation/diagrams/dot/overall-pwn-architecture.dot +7 -5
  27. data/documentation/diagrams/dot/persistence-filesystem.dot +4 -2
  28. data/documentation/diagrams/dot/pwn-ai-feedback-learning-loop.dot +25 -7
  29. data/documentation/diagrams/dot/sdr-radio-flow.dot +13 -11
  30. data/documentation/diagrams/driver-framework.svg +13 -13
  31. data/documentation/diagrams/extrospection-world-awareness.svg +241 -93
  32. data/documentation/diagrams/fuzzing-workflow.svg +24 -24
  33. data/documentation/diagrams/hardware-hacking.svg +18 -18
  34. data/documentation/diagrams/history-to-drivers.svg +18 -18
  35. data/documentation/diagrams/memory-skills-detailed.svg +169 -97
  36. data/documentation/diagrams/mistakes-negative-feedback.svg +266 -0
  37. data/documentation/diagrams/network-infra-testing.svg +27 -27
  38. data/documentation/diagrams/overall-pwn-architecture.svg +184 -173
  39. data/documentation/diagrams/penetration-testing-workflow.svg +30 -30
  40. data/documentation/diagrams/persistence-filesystem.svg +111 -87
  41. data/documentation/diagrams/plugin-ecosystem.svg +35 -35
  42. data/documentation/diagrams/pwn-ai-feedback-learning-loop.svg +263 -149
  43. data/documentation/diagrams/pwn-repl-prototyping.svg +20 -20
  44. data/documentation/diagrams/reporting-pipeline.svg +18 -18
  45. data/documentation/diagrams/reverse-engineering-flow.svg +21 -21
  46. data/documentation/diagrams/sdr-radio-flow.svg +76 -56
  47. data/documentation/diagrams/sessions-cron-automation.svg +18 -18
  48. data/documentation/diagrams/swarm-multi-agent.svg +39 -39
  49. data/documentation/diagrams/web-application-testing.svg +26 -26
  50. data/documentation/diagrams/zero-day-research-flow.svg +25 -25
  51. data/documentation/pwn-ai-Agent.md +28 -15
  52. data/lib/pwn/ai/agent/extrospection.rb +494 -6
  53. data/lib/pwn/ai/agent/learning.rb +67 -7
  54. data/lib/pwn/ai/agent/loop.rb +72 -6
  55. data/lib/pwn/ai/agent/mistakes.rb +369 -0
  56. data/lib/pwn/ai/agent/prompt_builder.rb +10 -1
  57. data/lib/pwn/ai/agent/tools/extrospection.rb +88 -3
  58. data/lib/pwn/ai/agent/tools/mistakes.rb +132 -0
  59. data/lib/pwn/ai/agent.rb +1 -0
  60. data/lib/pwn/plugins/jira_data_center.rb +2 -2
  61. data/lib/pwn/sdr/decoder/adsb.rb +82 -0
  62. data/lib/pwn/sdr/decoder/apt.rb +136 -0
  63. data/lib/pwn/sdr/decoder/base.rb +258 -171
  64. data/lib/pwn/sdr/decoder/bluetooth.rb +71 -0
  65. data/lib/pwn/sdr/decoder/dect.rb +70 -0
  66. data/lib/pwn/sdr/decoder/dsp.rb +396 -0
  67. data/lib/pwn/sdr/decoder/flex.rb +177 -210
  68. data/lib/pwn/sdr/decoder/gps.rb +74 -0
  69. data/lib/pwn/sdr/decoder/gsm.rb +31 -62
  70. data/lib/pwn/sdr/decoder/iridium.rb +66 -0
  71. data/lib/pwn/sdr/decoder/lora.rb +80 -0
  72. data/lib/pwn/sdr/decoder/lte.rb +67 -0
  73. data/lib/pwn/sdr/decoder/morse.rb +138 -0
  74. data/lib/pwn/sdr/decoder/p25.rb +74 -0
  75. data/lib/pwn/sdr/decoder/pager.rb +61 -0
  76. data/lib/pwn/sdr/decoder/pocsag.rb +176 -54
  77. data/lib/pwn/sdr/decoder/rfid.rb +79 -0
  78. data/lib/pwn/sdr/decoder/rtl433.rb +86 -0
  79. data/lib/pwn/sdr/decoder/rtty.rb +139 -0
  80. data/lib/pwn/sdr/decoder/wifi.rb +77 -0
  81. data/lib/pwn/sdr/decoder/zigbee.rb +74 -0
  82. data/lib/pwn/sdr/decoder.rb +75 -7
  83. data/lib/pwn/sdr/frequency_allocation.rb +82 -41
  84. data/lib/pwn/sdr/gqrx.rb +186 -95
  85. data/lib/pwn/version.rb +1 -1
  86. data/spec/lib/pwn/ai/agent/mistakes_spec.rb +36 -0
  87. data/spec/lib/pwn/ai/agent/tools/mistakes_spec.rb +12 -0
  88. data/spec/lib/pwn/sdr/decoder/adsb_spec.rb +15 -0
  89. data/spec/lib/pwn/sdr/decoder/apt_spec.rb +15 -0
  90. data/spec/lib/pwn/sdr/decoder/bluetooth_spec.rb +15 -0
  91. data/spec/lib/pwn/sdr/decoder/dect_spec.rb +15 -0
  92. data/spec/lib/pwn/sdr/decoder/dsp_spec.rb +15 -0
  93. data/spec/lib/pwn/sdr/decoder/gps_spec.rb +15 -0
  94. data/spec/lib/pwn/sdr/decoder/iridium_spec.rb +15 -0
  95. data/spec/lib/pwn/sdr/decoder/lora_spec.rb +15 -0
  96. data/spec/lib/pwn/sdr/decoder/lte_spec.rb +15 -0
  97. data/spec/lib/pwn/sdr/decoder/morse_spec.rb +15 -0
  98. data/spec/lib/pwn/sdr/decoder/p25_spec.rb +15 -0
  99. data/spec/lib/pwn/sdr/decoder/pager_spec.rb +15 -0
  100. data/spec/lib/pwn/sdr/decoder/rfid_spec.rb +15 -0
  101. data/spec/lib/pwn/sdr/decoder/rtl433_spec.rb +15 -0
  102. data/spec/lib/pwn/sdr/decoder/rtty_spec.rb +15 -0
  103. data/spec/lib/pwn/sdr/decoder/wifi_spec.rb +15 -0
  104. data/spec/lib/pwn/sdr/decoder/zigbee_spec.rb +15 -0
  105. data/third_party/pwn_rdoc.jsonl +115 -5
  106. metadata +46 -6
  107. data/README.md.bak +0 -200
@@ -29,7 +29,7 @@ PWN::AI::Agent::Registry.register(
29
29
  type: 'object',
30
30
  properties: {
31
31
  persist: { type: 'boolean', default: true, description: 'Write to disk & rotate previous baseline.' },
32
- sections: { type: 'array', items: { type: 'string', enum: %w[host net toolchain repo env] }, description: 'Subset of probes to run (default all).' }
32
+ sections: { type: 'array', items: { type: 'string', enum: %w[host net toolchain repo env rf web] }, description: 'Subset of probes to run (default all).' }
33
33
  },
34
34
  required: []
35
35
  }
@@ -86,7 +86,7 @@ PWN::AI::Agent::Registry.register(
86
86
  properties: {
87
87
  source: { type: 'string', description: 'Where it came from (nmap, shodan, burp, cve, human, ...).' },
88
88
  data: { type: 'string', description: 'The observation itself.' },
89
- category: { type: 'string', enum: %w[recon vuln intel target network env misc], default: 'misc' },
89
+ category: { type: 'string', enum: %w[recon vuln intel target network env rf web misc], default: 'misc' },
90
90
  target: { type: 'string', description: 'Host / IP / URL / asset the observation is about.' },
91
91
  tags: { type: 'array', items: { type: 'string' } },
92
92
  ttl: { type: 'integer', description: 'Seconds until stale (omit = forever).' }
@@ -187,7 +187,10 @@ PWN::AI::Agent::Registry.register(
187
187
  'against toolchain drift / missing binaries, (b) Learning ' \
188
188
  'failures against host/net/repo drift on the same day, ' \
189
189
  '(c) recorded :intel observations against installed ' \
190
- 'component versions. Returns actionable findings so the ' \
190
+ 'component versions, (d) :web DOM drift on watched targets ' \
191
+ 'against Learning failures citing that host, (e) refuted ' \
192
+ 'extro_verify claims against stale PWN::Memory :fact ' \
193
+ 'entries. Returns actionable findings so the ' \
191
194
  'agent can distinguish "I did it wrong" from "the world ' \
192
195
  'changed under me".',
193
196
  parameters: {
@@ -278,3 +281,85 @@ PWN::AI::Agent::Registry.register(
278
281
  { previous: prev, current: ai[:agent][:auto_extrospect] ? true : false }
279
282
  }
280
283
  )
284
+
285
+ PWN::AI::Agent::Registry.register(
286
+ name: 'extro_verify',
287
+ toolset: 'extrospection',
288
+ schema: {
289
+ name: 'extro_verify',
290
+ description: 'Browser-backed SELF FACT-CHECK. Drives ' \
291
+ 'PWN::Plugins::TransparentBrowser (:headless) against a ' \
292
+ 'canonical source for the claim class (NVD/CVE.org for ' \
293
+ ':cve, rubygems/PyPI/GitHub for :version, the cited URL ' \
294
+ 'for :doc, DuckDuckGo HTML for :generic), renders the DOM ' \
295
+ 'with JS executed, and returns {claim:, kind:, verdict: ' \
296
+ ':confirmed|:refuted|:unknown, confidence:, evidence:[], ' \
297
+ 'action_taken:}. On :refuted → Mistakes.record(tool:' \
298
+ "'assumption') so KNOWN MISTAKES warns every future run; " \
299
+ 'on :confirmed → observe(:intel, ttl:30d); on :unknown → ' \
300
+ 'Learning.note_outcome(tags:[needs_human]). This is the ' \
301
+ 'PROACTIVE trigger that catches the model being wrong ' \
302
+ 'about the world before a human does — the extrospective ' \
303
+ 'mirror of mistakes_record.',
304
+ parameters: {
305
+ type: 'object',
306
+ properties: {
307
+ claim: { type: 'string', description: 'Factual claim to fact-check (a CVE assertion, "latest X is v1.2.3", a cited URL + quoted snippet, or free text).' },
308
+ kind: { type: 'string', enum: %w[cve version doc generic], description: 'Force a verifier. Omit to auto-detect from the claim.' },
309
+ url: { type: 'string', description: 'Explicit URL the claim cites (forces kind: :doc).' },
310
+ commit: { type: 'boolean', default: true, description: 'Write Mistakes/observe/Learning on the verdict.' },
311
+ proxy: { type: 'string', description: 'Upstream proxy for TransparentBrowser (e.g. "tor" or http://127.0.0.1:8080).' }
312
+ },
313
+ required: %w[claim]
314
+ }
315
+ },
316
+ check: -> { defined?(PWN::AI::Agent::Extrospection) && PWN::AI::Agent::Extrospection.respond_to?(:verify) },
317
+ handler: lambda { |args|
318
+ o = { claim: args[:claim] }
319
+ o[:kind] = args[:kind] if args[:kind]
320
+ o[:url] = args[:url] if args[:url]
321
+ o[:commit] = args[:commit] if args.key?(:commit)
322
+ o[:proxy] = args[:proxy] if args[:proxy]
323
+ PWN::AI::Agent::Extrospection.verify(o)
324
+ }
325
+ )
326
+
327
+ PWN::AI::Agent::Registry.register(
328
+ name: 'extro_watch',
329
+ toolset: 'extrospection',
330
+ schema: {
331
+ name: 'extro_watch',
332
+ description: 'Passive change-detection on an external web artefact you ' \
333
+ 'care about (a target /api/version, a vendor changelog, a ' \
334
+ 'bug-bounty scope page). Renders the URL headlessly via ' \
335
+ 'PWN::Plugins::TransparentBrowser, hashes the RENDERED DOM ' \
336
+ 'text (JS-delivered changes count), captures title / TLS ' \
337
+ 'cert fp / screenshot, and persists it as observe(' \
338
+ 'category: :web). Re-running against the same URL returns ' \
339
+ '{changed: true|false, prior_sha:, current:{…}}; a ' \
340
+ 'subsequent extro_snapshot(sections:["web"]) surfaces the ' \
341
+ 'delta in drift() as ~web.<host>.dom_sha exactly like ' \
342
+ '~toolchain.nmap today.',
343
+ parameters: {
344
+ type: 'object',
345
+ properties: {
346
+ url: { type: 'string', description: 'URL to render, hash and watch.' },
347
+ selector: { type: 'string', description: 'CSS selector whose innerText to hash (default full body).' },
348
+ ttl: { type: 'integer', description: 'Seconds until this :web observation is stale (default 604800 = 7d).' },
349
+ tags: { type: 'array', items: { type: 'string' } },
350
+ proxy: { type: 'string', description: 'Upstream proxy for TransparentBrowser (e.g. "tor").' }
351
+ },
352
+ required: %w[url]
353
+ }
354
+ },
355
+ check: -> { defined?(PWN::AI::Agent::Extrospection) && PWN::AI::Agent::Extrospection.respond_to?(:watch) },
356
+ handler: lambda { |args|
357
+ PWN::AI::Agent::Extrospection.watch(
358
+ url: args[:url],
359
+ selector: args[:selector],
360
+ ttl: args[:ttl],
361
+ tags: args[:tags],
362
+ proxy: args[:proxy]
363
+ )
364
+ }
365
+ )
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pwn/ai/agent/registry'
4
+ require 'pwn/ai/agent/mistakes'
5
+
6
+ # Expose the negative-feedback ledger to the model so it can inspect its
7
+ # own recurring failure patterns, RECORD mistakes it recognises
8
+ # semantically (not just tool-dispatch errors), and — critically — RESOLVE
9
+ # them with an explicit fix, which is then re-injected into every future
10
+ # system prompt as an actionable "do THIS instead" lesson AND handed
11
+ # straight back inline on the next recurrence via
12
+ # Mistakes.correction_hint.
13
+
14
+ PWN::AI::Agent::Registry.register(
15
+ name: 'mistakes_list',
16
+ toolset: 'learning',
17
+ schema: {
18
+ name: 'mistakes_list',
19
+ description: 'List recorded failure fingerprints from ~/.pwn/mistakes.json ' \
20
+ '(signature, tool, normalised error, count, first/last seen, ' \
21
+ 'resolved, fix). Sorted by recurrence count. These are the ' \
22
+ 'specific mistakes the agent keeps making — inspect before ' \
23
+ 'attempting the same tool again, and call mistakes_resolve ' \
24
+ 'once a working alternative is found.',
25
+ parameters: {
26
+ type: 'object',
27
+ properties: {
28
+ limit: { type: 'integer', default: 20 },
29
+ unresolved_only: { type: 'boolean', default: true }
30
+ },
31
+ required: []
32
+ }
33
+ },
34
+ check: -> { defined?(PWN::AI::Agent::Mistakes) },
35
+ handler: lambda { |args|
36
+ PWN::AI::Agent::Mistakes.top(
37
+ limit: args[:limit] || 20,
38
+ unresolved_only: args.key?(:unresolved_only) ? args[:unresolved_only] : true
39
+ )
40
+ }
41
+ )
42
+
43
+ PWN::AI::Agent::Registry.register(
44
+ name: 'mistakes_record',
45
+ toolset: 'learning',
46
+ schema: {
47
+ name: 'mistakes_record',
48
+ description: 'Proactively fingerprint a mistake YOU just made (wrong ' \
49
+ 'assumption, wrong file edited, bad approach, hallucinated ' \
50
+ 'API) so the negative-feedback loop captures failures that ' \
51
+ 'are NOT tool-dispatch errors. The (tool, error) pair is ' \
52
+ 'normalised and its cross-session count incremented; once ' \
53
+ 'count ≥ 3 it is flagged [REPEATING] in every future system ' \
54
+ 'prompt. Follow up with mistakes_resolve when you find the fix.',
55
+ parameters: {
56
+ type: 'object',
57
+ properties: {
58
+ tool: {
59
+ type: 'string',
60
+ description: 'Component / area the mistake was in (e.g. "assumption", "shell", "pwn_eval", "plan", a module name).'
61
+ },
62
+ error: { type: 'string', description: 'What went wrong, in your own words.' },
63
+ args: { type: 'string', description: 'Optional sample of the input that triggered it.' }
64
+ },
65
+ required: %w[tool error]
66
+ }
67
+ },
68
+ check: -> { defined?(PWN::AI::Agent::Mistakes) },
69
+ handler: lambda { |args|
70
+ sid = (PWN::Env.dig(:ai, :session_id) if defined?(PWN::Env))
71
+ PWN::AI::Agent::Mistakes.record(
72
+ tool: args[:tool], error: args[:error], args: args[:args],
73
+ session_id: sid, source: :model
74
+ )
75
+ }
76
+ )
77
+
78
+ PWN::AI::Agent::Registry.register(
79
+ name: 'mistakes_resolve',
80
+ toolset: 'learning',
81
+ schema: {
82
+ name: 'mistakes_resolve',
83
+ description: 'Mark a recorded mistake as RESOLVED and attach the fix ' \
84
+ '(what to do INSTEAD). The fix is (a) promoted to a ' \
85
+ 'PWN::Memory :lesson so every future run sees "AVOID X — ' \
86
+ 'FIX: Y" in its system prompt, AND (b) handed straight back ' \
87
+ 'inline via correction_hint the next time that signature ' \
88
+ 'recurs. If the same signature recurs after resolution it is ' \
89
+ 'automatically re-opened and tagged [REGRESSED]. Call this ' \
90
+ 'the moment you find a working alternative to a ' \
91
+ 'previously-failing approach.',
92
+ parameters: {
93
+ type: 'object',
94
+ properties: {
95
+ signature: { type: 'string', description: 'Mistake signature from mistakes_list / KNOWN MISTAKES block / correction_hint.' },
96
+ fix: { type: 'string', description: 'What to do instead next time.' }
97
+ },
98
+ required: %w[signature fix]
99
+ }
100
+ },
101
+ check: -> { defined?(PWN::AI::Agent::Mistakes) },
102
+ handler: lambda { |args|
103
+ PWN::AI::Agent::Mistakes.resolve(signature: args[:signature], fix: args[:fix])
104
+ }
105
+ )
106
+
107
+ PWN::AI::Agent::Registry.register(
108
+ name: 'mistakes_reset',
109
+ toolset: 'learning',
110
+ schema: {
111
+ name: 'mistakes_reset',
112
+ description: 'Wipe ~/.pwn/mistakes.json (all recorded failure fingerprints ' \
113
+ 'and their fixes). Use only when moving to a new host/engagement ' \
114
+ 'where prior failure patterns no longer apply. IRREVERSIBLE — ' \
115
+ 'must pass confirm:true.',
116
+ parameters: {
117
+ type: 'object',
118
+ properties: {
119
+ confirm: { type: 'boolean', description: 'Must be true to actually reset.' }
120
+ },
121
+ required: %w[confirm]
122
+ }
123
+ },
124
+ check: -> { defined?(PWN::AI::Agent::Mistakes) },
125
+ handler: lambda { |args|
126
+ raise ArgumentError, 'refusing to reset mistakes without confirm:true' unless args[:confirm] == true
127
+
128
+ before = PWN::AI::Agent::Mistakes.load.keys.length
129
+ PWN::AI::Agent::Mistakes.reset
130
+ { reset: true, mistakes_cleared: before, file: PWN::AI::Agent::Mistakes::MISTAKES_FILE }
131
+ }
132
+ )
data/lib/pwn/ai/agent.rb CHANGED
@@ -25,6 +25,7 @@ module PWN
25
25
  autoload :Loop, 'pwn/ai/agent/loop'
26
26
  autoload :Metrics, 'pwn/ai/agent/metrics'
27
27
  autoload :Learning, 'pwn/ai/agent/learning'
28
+ autoload :Mistakes, 'pwn/ai/agent/mistakes'
28
29
  autoload :Extrospection, 'pwn/ai/agent/extrospection'
29
30
  autoload :Introspection, 'pwn/ai/agent/introspection'
30
31
  autoload :Swarm, 'pwn/ai/agent/swarm'
@@ -522,7 +522,7 @@ module PWN
522
522
  if elem.is_a?(Hash)
523
523
  # Retain common identifier keys; if none present keep full hash
524
524
  keys_to_keep = %i[id key name value]
525
- elem.keys.any? { |kk| keys_to_keep.include?(kk) } ? elem.slice(*keys_to_keep) : elem
525
+ elem.keys.intersect?(keys_to_keep) ? elem.slice(*keys_to_keep) : elem
526
526
  else
527
527
  elem
528
528
  end
@@ -530,7 +530,7 @@ module PWN
530
530
  when Hash
531
531
  keys_to_keep = %i[id key name value]
532
532
  filtered_fields[k] =
533
- if v.keys.any? { |kk| keys_to_keep.include?(kk) }
533
+ if v.keys.intersect?(keys_to_keep)
534
534
  v.slice(*keys_to_keep)
535
535
  else
536
536
  v
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PWN
4
+ module SDR
5
+ module Decoder
6
+ # Pure-Ruby ADS-B (1090 MHz Mode-S / 978 MHz UAT) activity detector.
7
+ #
8
+ # Mode-S Extended Squitter is 2 Mbit/s PPM with 8/120 μs frames on a
9
+ # 2 MHz-wide channel — physically unrecoverable from GQRX's 48 kHz
10
+ # demodulated-audio tap. Rather than shell out to `dump1090`, this
11
+ # module runs Base.run_detector to characterise squitter density
12
+ # (bursts/sec, peak dBFS, floor) natively in Ruby. `parse_line` is
13
+ # retained for offline SBS-1 CSV analysis.
14
+ module ADSB
15
+ SBS_FIELDS = %i[
16
+ msg_type tx_type session_id aircraft_id icao24 flight_id
17
+ date_gen time_gen date_log time_log callsign altitude_ft
18
+ ground_speed_kt track_deg lat lon vertical_rate_fpm squawk
19
+ alert emergency spi on_ground
20
+ ].freeze
21
+
22
+ # Supported Method Parameters::
23
+ # PWN::SDR::Decoder::ADSB.decode(
24
+ # freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
25
+ # )
26
+
27
+ public_class_method def self.decode(opts = {})
28
+ freq_obj = opts[:freq_obj]
29
+ hz = PWN::SDR.hz_to_i(freq: freq_obj[:freq])
30
+ uat = hz.between?(977_000_000, 979_000_000)
31
+ PWN::SDR::Decoder::Base.run_detector(
32
+ freq_obj: freq_obj,
33
+ protocol: uat ? 'ADSB-UAT978' : 'ADSB-1090ES',
34
+ note: '2 Mbit/s PPM squitters exceed the 48 kHz audio-tap Nyquist limit; native mode reports squitter-burst density only. Feed captured SBS-1 CSV to .parse_line for full field decode.',
35
+ describe: proc { |b| { modulation: 'PPM', frame_len_us: 120, classification: b[:duration_ms] < 5 ? 'squitter' : 'interrogation-train' } }
36
+ )
37
+ end
38
+
39
+ # Supported Method Parameters::
40
+ # PWN::SDR::Decoder::ADSB.parse_line(line: 'MSG,3,1,1,ABCDEF,...')
41
+
42
+ public_class_method def self.parse_line(opts = {})
43
+ line = opts[:line].to_s
44
+ return { protocol: 'ADSB-UAT', summary: line.strip } unless line.start_with?('MSG,')
45
+
46
+ f = line.split(',', -1)
47
+ out = { protocol: 'ADSB' }
48
+ SBS_FIELDS.each_with_index { |k, i| out[k] = f[i] unless f[i].to_s.empty? }
49
+ bits = []
50
+ bits << "ICAO=#{out[:icao24]}" if out[:icao24]
51
+ bits << "CS=#{out[:callsign].to_s.strip}" if out[:callsign]
52
+ bits << "ALT=#{out[:altitude_ft]}ft" if out[:altitude_ft]
53
+ bits << "POS=#{out[:lat]},#{out[:lon]}" if out[:lat] && out[:lon]
54
+ bits << "GS=#{out[:ground_speed_kt]}kt" if out[:ground_speed_kt]
55
+ bits << "SQK=#{out[:squawk]}" if out[:squawk]
56
+ out[:summary] = "ADSB #{bits.join(' ')}".strip
57
+ out
58
+ end
59
+
60
+ # Author(s):: 0day Inc. <support@0dayinc.com>
61
+
62
+ public_class_method def self.authors
63
+ "AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
64
+ end
65
+
66
+ # Display Usage for this Module
67
+
68
+ public_class_method def self.help
69
+ puts "USAGE (ruby-native detector, no external binaries):
70
+ #{self}.decode(
71
+ freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
72
+ )
73
+
74
+ #{self}.parse_line(line: 'MSG,3,1,1,ABCDEF,1,...')
75
+
76
+ #{self}.authors
77
+ "
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PWN
4
+ module SDR
5
+ module Decoder
6
+ # Pure-Ruby NOAA APT (Automatic Picture Transmission) decoder for the
7
+ # 137 MHz polar-orbiting weather satellites (NOAA-15/18/19).
8
+ #
9
+ # APT is a 2400 Hz AM subcarrier inside a ~34 kHz-wide FM downlink
10
+ # carrying two 909-pixel image channels at 2 lines/second (4160
11
+ # words/line). This module envelope-demodulates the 2400 Hz carrier
12
+ # from GQRX's 48 kHz UDP audio, resamples to 4160 words/sec, aligns
13
+ # each line on the 7-pulse Sync-A pattern, and appends the resulting
14
+ # 8-bit greyscale rows to a Netpbm PGM (P5) file — all in Ruby.
15
+ # No `sox`, no `noaa-apt`.
16
+ module APT
17
+ WORDS_PER_LINE = 4160
18
+ LINES_PER_SEC = 2
19
+ WORD_RATE = WORDS_PER_LINE * LINES_PER_SEC # 8320 Hz
20
+ # Sync-A: 7 cycles of 1040 Hz square = 1 1 0 0 repeated 7 times
21
+ SYNC_A = ([1, 1, 0, 0] * 7).freeze
22
+
23
+ # Streaming APT demodulator fed by Base.run_native.
24
+ class Demod
25
+ def initialize(rate: 48_000, out_path: nil)
26
+ @rate = rate
27
+ @carrier = 2400.0
28
+ @env_win = (rate / @carrier).round # ≈20 samples per cycle
29
+ @stamp = Time.now.strftime('%Y%m%d_%H%M%S')
30
+ @pgm_path = out_path || "/tmp/apt_#{@stamp}.pgm"
31
+ @rows = []
32
+ @word_buf = []
33
+ @minv = 1.0
34
+ @maxv = 0.0
35
+ end
36
+
37
+ def feed(samples, &)
38
+ env = PWN::SDR::Decoder::DSP.envelope(samples: samples, window: @env_win)
39
+ words = PWN::SDR::Decoder::DSP.resample(samples: env, src_rate: @rate, dst_rate: WORD_RATE)
40
+ @word_buf.concat(words)
41
+ extract_lines(&)
42
+ end
43
+
44
+ private
45
+
46
+ def extract_lines
47
+ while @word_buf.length >= WORDS_PER_LINE * 2
48
+ off = sync_offset(@word_buf[0, WORDS_PER_LINE])
49
+ @word_buf.shift(off) if off.positive?
50
+ break if @word_buf.length < WORDS_PER_LINE
51
+
52
+ row = @word_buf.shift(WORDS_PER_LINE)
53
+ lo, hi = row.minmax
54
+ @minv = lo if lo < @minv
55
+ @maxv = hi if hi > @maxv
56
+ @rows << row
57
+ write_pgm if (@rows.length % 20).zero?
58
+ yield(
59
+ protocol: 'NOAA-APT',
60
+ lines: @rows.length,
61
+ seconds: @rows.length / LINES_PER_SEC,
62
+ pgm: @pgm_path,
63
+ summary: "APT line #{@rows.length} (#{@rows.length / LINES_PER_SEC}s) → #{@pgm_path}"
64
+ )
65
+ end
66
+ end
67
+
68
+ def sync_offset(row)
69
+ mid = row.sum / row.length
70
+ best_off = 0
71
+ best_cor = -1.0
72
+ (0..(row.length - SYNC_A.length)).each do |o|
73
+ cor = 0.0
74
+ SYNC_A.each_with_index do |s, i|
75
+ cor += (row[o + i] - mid) * (s.zero? ? -1.0 : 1.0)
76
+ end
77
+ if cor > best_cor
78
+ best_cor = cor
79
+ best_off = o
80
+ end
81
+ end
82
+ best_off
83
+ end
84
+
85
+ def write_pgm
86
+ span = @maxv - @minv
87
+ span = 1.0 if span <= 0
88
+ File.open(@pgm_path, 'wb') do |f|
89
+ f.write("P5\n#{WORDS_PER_LINE} #{@rows.length}\n255\n")
90
+ @rows.each do |row|
91
+ bytes = row.map { |v| (((v - @minv) / span) * 255).clamp(0, 255).round }
92
+ f.write(bytes.pack('C*'))
93
+ end
94
+ end
95
+ end
96
+ end
97
+
98
+ # Supported Method Parameters::
99
+ # PWN::SDR::Decoder::APT.decode(
100
+ # freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
101
+ # )
102
+
103
+ public_class_method def self.decode(opts = {})
104
+ freq_obj = opts[:freq_obj]
105
+ PWN::SDR::Decoder::Base.run_native(
106
+ freq_obj: freq_obj,
107
+ protocol: 'NOAA-APT',
108
+ demod: Demod.new
109
+ )
110
+ end
111
+
112
+ # Author(s):: 0day Inc. <support@0dayinc.com>
113
+
114
+ public_class_method def self.authors
115
+ "AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
116
+ end
117
+
118
+ # Display Usage for this Module
119
+
120
+ public_class_method def self.help
121
+ puts "USAGE (ruby-native, no external binaries):
122
+ #{self}.decode(
123
+ freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
124
+ )
125
+
126
+ NOTE: Set GQRX to WFM (mono), ~34 kHz filter. Writes an 8-bit
127
+ greyscale Netpbm P5 image to /tmp/apt_<ts>.pgm every 10 s
128
+ of received pass. Both A/B channels are in one strip.
129
+
130
+ #{self}.authors
131
+ "
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end