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.
- checksums.yaml +4 -4
- data/Gemfile +2 -2
- data/README.md +14 -6
- data/bin/pwn_gqrx_scanner +25 -5
- data/documentation/Agent-Tool-Registry.md +4 -4
- data/documentation/Cron.md +13 -0
- data/documentation/Diagrams.md +6 -2
- data/documentation/Extrospection.md +96 -11
- data/documentation/Home.md +5 -4
- data/documentation/How-PWN-Works.md +7 -4
- data/documentation/Mistakes.md +97 -0
- data/documentation/Persistence.md +4 -1
- data/documentation/SDR.md +23 -5
- data/documentation/Skills-Memory-Learning.md +16 -5
- data/documentation/Transparent-Browser.md +18 -1
- data/documentation/diagrams/agent-tool-registry.svg +103 -100
- data/documentation/diagrams/ai-integration-tool-calling.svg +24 -24
- data/documentation/diagrams/aws-cloud-security.svg +25 -25
- data/documentation/diagrams/burp-vs-zap-preference.svg +12 -12
- data/documentation/diagrams/code-scanning-sast.svg +25 -25
- data/documentation/diagrams/cron-scheduling.svg +23 -23
- data/documentation/diagrams/dot/agent-tool-registry.dot +3 -3
- data/documentation/diagrams/dot/extrospection-world-awareness.dot +33 -10
- data/documentation/diagrams/dot/memory-skills-detailed.dot +16 -5
- data/documentation/diagrams/dot/mistakes-negative-feedback.dot +93 -0
- data/documentation/diagrams/dot/overall-pwn-architecture.dot +7 -5
- data/documentation/diagrams/dot/persistence-filesystem.dot +4 -2
- data/documentation/diagrams/dot/pwn-ai-feedback-learning-loop.dot +25 -7
- data/documentation/diagrams/dot/sdr-radio-flow.dot +13 -11
- data/documentation/diagrams/driver-framework.svg +13 -13
- data/documentation/diagrams/extrospection-world-awareness.svg +241 -93
- data/documentation/diagrams/fuzzing-workflow.svg +24 -24
- data/documentation/diagrams/hardware-hacking.svg +18 -18
- data/documentation/diagrams/history-to-drivers.svg +18 -18
- data/documentation/diagrams/memory-skills-detailed.svg +169 -97
- data/documentation/diagrams/mistakes-negative-feedback.svg +266 -0
- data/documentation/diagrams/network-infra-testing.svg +27 -27
- data/documentation/diagrams/overall-pwn-architecture.svg +184 -173
- data/documentation/diagrams/penetration-testing-workflow.svg +30 -30
- data/documentation/diagrams/persistence-filesystem.svg +111 -87
- data/documentation/diagrams/plugin-ecosystem.svg +35 -35
- data/documentation/diagrams/pwn-ai-feedback-learning-loop.svg +263 -149
- data/documentation/diagrams/pwn-repl-prototyping.svg +20 -20
- data/documentation/diagrams/reporting-pipeline.svg +18 -18
- data/documentation/diagrams/reverse-engineering-flow.svg +21 -21
- data/documentation/diagrams/sdr-radio-flow.svg +76 -56
- data/documentation/diagrams/sessions-cron-automation.svg +18 -18
- data/documentation/diagrams/swarm-multi-agent.svg +39 -39
- data/documentation/diagrams/web-application-testing.svg +26 -26
- data/documentation/diagrams/zero-day-research-flow.svg +25 -25
- data/documentation/pwn-ai-Agent.md +28 -15
- data/lib/pwn/ai/agent/extrospection.rb +494 -6
- data/lib/pwn/ai/agent/learning.rb +67 -7
- data/lib/pwn/ai/agent/loop.rb +72 -6
- data/lib/pwn/ai/agent/mistakes.rb +369 -0
- data/lib/pwn/ai/agent/prompt_builder.rb +10 -1
- data/lib/pwn/ai/agent/tools/extrospection.rb +88 -3
- data/lib/pwn/ai/agent/tools/mistakes.rb +132 -0
- data/lib/pwn/ai/agent.rb +1 -0
- data/lib/pwn/plugins/jira_data_center.rb +2 -2
- data/lib/pwn/sdr/decoder/adsb.rb +82 -0
- data/lib/pwn/sdr/decoder/apt.rb +136 -0
- data/lib/pwn/sdr/decoder/base.rb +258 -171
- data/lib/pwn/sdr/decoder/bluetooth.rb +71 -0
- data/lib/pwn/sdr/decoder/dect.rb +70 -0
- data/lib/pwn/sdr/decoder/dsp.rb +396 -0
- data/lib/pwn/sdr/decoder/flex.rb +177 -210
- data/lib/pwn/sdr/decoder/gps.rb +74 -0
- data/lib/pwn/sdr/decoder/gsm.rb +31 -62
- data/lib/pwn/sdr/decoder/iridium.rb +66 -0
- data/lib/pwn/sdr/decoder/lora.rb +80 -0
- data/lib/pwn/sdr/decoder/lte.rb +67 -0
- data/lib/pwn/sdr/decoder/morse.rb +138 -0
- data/lib/pwn/sdr/decoder/p25.rb +74 -0
- data/lib/pwn/sdr/decoder/pager.rb +61 -0
- data/lib/pwn/sdr/decoder/pocsag.rb +176 -54
- data/lib/pwn/sdr/decoder/rfid.rb +79 -0
- data/lib/pwn/sdr/decoder/rtl433.rb +86 -0
- data/lib/pwn/sdr/decoder/rtty.rb +139 -0
- data/lib/pwn/sdr/decoder/wifi.rb +77 -0
- data/lib/pwn/sdr/decoder/zigbee.rb +74 -0
- data/lib/pwn/sdr/decoder.rb +75 -7
- data/lib/pwn/sdr/frequency_allocation.rb +82 -41
- data/lib/pwn/sdr/gqrx.rb +186 -95
- data/lib/pwn/version.rb +1 -1
- data/spec/lib/pwn/ai/agent/mistakes_spec.rb +36 -0
- data/spec/lib/pwn/ai/agent/tools/mistakes_spec.rb +12 -0
- data/spec/lib/pwn/sdr/decoder/adsb_spec.rb +15 -0
- data/spec/lib/pwn/sdr/decoder/apt_spec.rb +15 -0
- data/spec/lib/pwn/sdr/decoder/bluetooth_spec.rb +15 -0
- data/spec/lib/pwn/sdr/decoder/dect_spec.rb +15 -0
- data/spec/lib/pwn/sdr/decoder/dsp_spec.rb +15 -0
- data/spec/lib/pwn/sdr/decoder/gps_spec.rb +15 -0
- data/spec/lib/pwn/sdr/decoder/iridium_spec.rb +15 -0
- data/spec/lib/pwn/sdr/decoder/lora_spec.rb +15 -0
- data/spec/lib/pwn/sdr/decoder/lte_spec.rb +15 -0
- data/spec/lib/pwn/sdr/decoder/morse_spec.rb +15 -0
- data/spec/lib/pwn/sdr/decoder/p25_spec.rb +15 -0
- data/spec/lib/pwn/sdr/decoder/pager_spec.rb +15 -0
- data/spec/lib/pwn/sdr/decoder/rfid_spec.rb +15 -0
- data/spec/lib/pwn/sdr/decoder/rtl433_spec.rb +15 -0
- data/spec/lib/pwn/sdr/decoder/rtty_spec.rb +15 -0
- data/spec/lib/pwn/sdr/decoder/wifi_spec.rb +15 -0
- data/spec/lib/pwn/sdr/decoder/zigbee_spec.rb +15 -0
- data/third_party/pwn_rdoc.jsonl +115 -5
- metadata +46 -6
- 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
|
|
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.
|
|
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.
|
|
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
|