pwn 0.5.615 → 0.5.618

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 (153) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +109 -85
  3. data/README.md.bak +200 -0
  4. data/bin/pwn +47 -2
  5. data/bin/pwn_gqrx_scanner +75 -18
  6. data/documentation/AI-Integration.md +42 -26
  7. data/documentation/AWS.md +57 -0
  8. data/documentation/Agent-Tool-Registry.md +56 -0
  9. data/documentation/Banner.md +17 -0
  10. data/documentation/Blockchain.md +18 -0
  11. data/documentation/Bounty.md +21 -0
  12. data/documentation/BurpSuite.md +41 -16
  13. data/documentation/CLI-Drivers.md +58 -0
  14. data/documentation/Configuration.md +66 -0
  15. data/documentation/Contributing.md +33 -19
  16. data/documentation/Cron.md +47 -0
  17. data/documentation/Diagrams.md +123 -40
  18. data/documentation/Drivers.md +43 -16
  19. data/documentation/Extrospection.md +54 -0
  20. data/documentation/FFI.md +14 -0
  21. data/documentation/Fuzzing.md +36 -0
  22. data/documentation/General-PWN-Usage.md +50 -30
  23. data/documentation/Hardware.md +40 -0
  24. data/documentation/Home.md +78 -51
  25. data/documentation/How-PWN-Works.md +59 -57
  26. data/documentation/Installation.md +53 -28
  27. data/documentation/Metasploit.md +34 -0
  28. data/documentation/NmapIt.md +18 -12
  29. data/documentation/PWN.png +0 -0
  30. data/documentation/PWN_Contributors_and_Users.png +0 -0
  31. data/documentation/Persistence.md +38 -0
  32. data/documentation/Plugins.md +97 -47
  33. data/documentation/Reporting.md +25 -18
  34. data/documentation/SAST.md +39 -22
  35. data/documentation/SDR.md +40 -0
  36. data/documentation/Sessions.md +39 -0
  37. data/documentation/Skills-Memory-Learning.md +49 -29
  38. data/documentation/Swarm.md +71 -0
  39. data/documentation/Transparent-Browser.md +26 -22
  40. data/documentation/Troubleshooting.md +44 -25
  41. data/documentation/WWW.md +32 -0
  42. data/documentation/What-is-PWN.md +51 -32
  43. data/documentation/Why-PWN.md +41 -11
  44. data/documentation/diagrams/agent-tool-registry.svg +284 -0
  45. data/documentation/diagrams/ai-integration-tool-calling.svg +141 -103
  46. data/documentation/diagrams/aws-cloud-security.svg +166 -0
  47. data/documentation/diagrams/build.sh +20 -0
  48. data/documentation/diagrams/burp-vs-zap-preference.svg +47 -62
  49. data/documentation/diagrams/code-scanning-sast.svg +139 -79
  50. data/documentation/diagrams/cron-scheduling.svg +148 -0
  51. data/documentation/diagrams/dot/_THEME.md +27 -0
  52. data/documentation/diagrams/dot/agent-tool-registry.dot +59 -0
  53. data/documentation/diagrams/dot/ai-integration-tool-calling.dot +47 -20
  54. data/documentation/diagrams/dot/aws-cloud-security.dot +48 -0
  55. data/documentation/diagrams/dot/burp-vs-zap-preference.dot +23 -12
  56. data/documentation/diagrams/dot/code-scanning-sast.dot +46 -18
  57. data/documentation/diagrams/dot/cron-scheduling.dot +40 -0
  58. data/documentation/diagrams/dot/driver-framework.dot +36 -14
  59. data/documentation/diagrams/dot/extrospection-world-awareness.dot +55 -0
  60. data/documentation/diagrams/dot/fuzzing-workflow.dot +44 -24
  61. data/documentation/diagrams/dot/hardware-hacking.dot +48 -0
  62. data/documentation/diagrams/dot/history-to-drivers.dot +31 -17
  63. data/documentation/diagrams/dot/memory-skills-detailed.dot +57 -25
  64. data/documentation/diagrams/dot/network-infra-testing.dot +51 -19
  65. data/documentation/diagrams/dot/overall-pwn-architecture.dot +110 -44
  66. data/documentation/diagrams/dot/penetration-testing-workflow.dot +56 -25
  67. data/documentation/diagrams/dot/persistence-filesystem.dot +29 -0
  68. data/documentation/diagrams/dot/plugin-ecosystem.dot +41 -27
  69. data/documentation/diagrams/dot/pwn-ai-feedback-learning-loop.dot +87 -46
  70. data/documentation/diagrams/dot/pwn-repl-prototyping.dot +49 -30
  71. data/documentation/diagrams/dot/reporting-pipeline.dot +43 -14
  72. data/documentation/diagrams/dot/reverse-engineering-flow.dot +40 -18
  73. data/documentation/diagrams/dot/sdr-radio-flow.dot +45 -0
  74. data/documentation/diagrams/dot/sessions-cron-automation.dot +41 -14
  75. data/documentation/diagrams/dot/swarm-multi-agent.dot +70 -0
  76. data/documentation/diagrams/dot/web-application-testing.dot +45 -23
  77. data/documentation/diagrams/dot/zero-day-research-flow.dot +46 -24
  78. data/documentation/diagrams/driver-framework.svg +87 -66
  79. data/documentation/diagrams/extrospection-world-awareness.svg +185 -0
  80. data/documentation/diagrams/fuzzing-workflow.svg +128 -115
  81. data/documentation/diagrams/hardware-hacking.svg +163 -0
  82. data/documentation/diagrams/history-to-drivers.svg +84 -82
  83. data/documentation/diagrams/memory-skills-detailed.svg +167 -119
  84. data/documentation/diagrams/network-infra-testing.svg +146 -84
  85. data/documentation/diagrams/overall-pwn-architecture.svg +356 -185
  86. data/documentation/diagrams/penetration-testing-workflow.svg +161 -108
  87. data/documentation/diagrams/persistence-filesystem.svg +174 -0
  88. data/documentation/diagrams/plugin-ecosystem.svg +225 -121
  89. data/documentation/diagrams/pwn-ai-feedback-learning-loop.svg +257 -185
  90. data/documentation/diagrams/pwn-repl-prototyping.svg +126 -137
  91. data/documentation/diagrams/reporting-pipeline.svg +128 -68
  92. data/documentation/diagrams/reverse-engineering-flow.svg +100 -80
  93. data/documentation/diagrams/sdr-radio-flow.svg +146 -0
  94. data/documentation/diagrams/sessions-cron-automation.svg +112 -68
  95. data/documentation/diagrams/swarm-multi-agent.svg +225 -0
  96. data/documentation/diagrams/web-application-testing.svg +136 -100
  97. data/documentation/diagrams/zero-day-research-flow.svg +116 -112
  98. data/documentation/fax-spectrogram.png +0 -0
  99. data/documentation/fax-waveform.png +0 -0
  100. data/documentation/pwn-REPL.md +40 -24
  101. data/documentation/pwn-ai-Agent.md +59 -30
  102. data/documentation/pwn_android_war_dialer_session.png +0 -0
  103. data/documentation/pwn_install.png +0 -0
  104. data/documentation/pwn_wallpaper.jpg +0 -0
  105. data/documentation/ringing-spectrogram.png +0 -0
  106. data/documentation/ringing-waveform.png +0 -0
  107. data/git_commit.sh +1 -1
  108. data/lib/pwn/ai/agent/assembly.rb +1 -1
  109. data/lib/pwn/ai/agent/btc.rb +1 -1
  110. data/lib/pwn/ai/agent/burp_suite.rb +1 -1
  111. data/lib/pwn/ai/agent/extrospection.rb +618 -0
  112. data/lib/pwn/ai/agent/gqrx.rb +2 -2
  113. data/lib/pwn/ai/agent/hacker_one.rb +1 -1
  114. data/lib/pwn/ai/agent/introspection.rb +91 -0
  115. data/lib/pwn/ai/agent/learning.rb +6 -4
  116. data/lib/pwn/ai/agent/loop.rb +15 -0
  117. data/lib/pwn/ai/agent/prompt_builder.rb +10 -1
  118. data/lib/pwn/ai/agent/sast.rb +1 -1
  119. data/lib/pwn/ai/agent/swarm.rb +437 -0
  120. data/lib/pwn/ai/agent/tools/cron.rb +163 -0
  121. data/lib/pwn/ai/agent/tools/extrospection.rb +280 -0
  122. data/lib/pwn/ai/agent/tools/learning.rb +108 -0
  123. data/lib/pwn/ai/agent/tools/memory.rb +27 -0
  124. data/lib/pwn/ai/agent/tools/metrics.rb +61 -0
  125. data/lib/pwn/ai/agent/tools/sessions.rb +139 -0
  126. data/lib/pwn/ai/agent/tools/skills.rb +30 -0
  127. data/lib/pwn/ai/agent/tools/swarm.rb +229 -0
  128. data/lib/pwn/ai/agent/transparent_browser.rb +1 -1
  129. data/lib/pwn/ai/agent/vuln_gen.rb +2 -2
  130. data/lib/pwn/ai/agent.rb +3 -0
  131. data/lib/pwn/ai/anthropic.rb +19 -4
  132. data/lib/pwn/ai.rb +0 -1
  133. data/lib/pwn/config.rb +10 -23
  134. data/lib/pwn/cron.rb +16 -7
  135. data/lib/pwn/plugins/repl.rb +90 -281
  136. data/lib/pwn/sdr/decoder/base.rb +251 -0
  137. data/lib/pwn/sdr/decoder/gsm.rb +84 -185
  138. data/lib/pwn/sdr/decoder/pocsag.rb +62 -217
  139. data/lib/pwn/sdr/decoder.rb +1 -0
  140. data/lib/pwn/sdr/gqrx.rb +446 -65
  141. data/lib/pwn/version.rb +1 -1
  142. data/spec/lib/pwn/ai/agent/extrospection_spec.rb +15 -0
  143. data/spec/lib/pwn/ai/agent/introspection_spec.rb +15 -0
  144. data/spec/lib/pwn/ai/agent/swarm_spec.rb +34 -0
  145. data/spec/lib/pwn/ai/agent/tools/cron_spec.rb +40 -0
  146. data/spec/lib/pwn/ai/agent/tools/extrospection_spec.rb +40 -0
  147. data/spec/lib/pwn/ai/agent/tools/metrics_spec.rb +20 -0
  148. data/spec/lib/pwn/ai/agent/tools/sessions_spec.rb +35 -0
  149. data/spec/lib/pwn/ai/agent/tools/swarm_spec.rb +17 -0
  150. data/spec/lib/pwn/{ai/introspection_spec.rb → sdr/decoder/base_spec.rb} +3 -3
  151. data/third_party/pwn_rdoc.jsonl +199 -37
  152. metadata +65 -3
  153. data/lib/pwn/ai/introspection.rb +0 -76
@@ -0,0 +1,618 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'fileutils'
5
+ require 'time'
6
+ require 'digest'
7
+ require 'socket'
8
+ require 'open3'
9
+ require 'etc'
10
+ require 'uri'
11
+ require 'shellwords'
12
+
13
+ module PWN
14
+ module AI
15
+ module Agent
16
+ # PWN::AI::Agent::Extrospection is the outward-facing counterpart to
17
+ # PWN::AI::Agent::Learning (introspection). Where Learning/Metrics
18
+ # look INWARD at the agent's own tool telemetry, task outcomes and
19
+ # session transcripts, Extrospection looks OUTWARD at the world the
20
+ # agent operates in: host state, toolchain versions, network posture,
21
+ # repo drift, and external threat-intel (CVE / Exploit-DB / ATT&CK).
22
+ #
23
+ # Together they close BOTH halves of the pwn-ai feedback loop:
24
+ #
25
+ # INTROSPECTIVE (self) EXTROSPECTIVE (world)
26
+ # ---------------------- -------------------------------------
27
+ # Metrics.record Extrospection.snapshot (host probe)
28
+ # Learning.note_outcome Extrospection.observe (recon fact)
29
+ # Learning.reflect Extrospection.drift (env delta)
30
+ # Learning.stats Extrospection.intel (CVE/EDB)
31
+ # Extrospection.correlate (self x world)
32
+ #
33
+ # PromptBuilder re-injects Extrospection.to_context on every turn so
34
+ # the model gains situational awareness of what changed on THIS host
35
+ # between sessions ("kernel upgraded", "nmap now missing", "port 8080
36
+ # newly listening", "CVE-2026-XXXX matches installed openssl") and
37
+ # can correlate that drift against introspective failures.
38
+ #
39
+ # Everything is file-backed under ~/.pwn/extrospection.json so it
40
+ # survives across REPL restarts and is shared by every future session.
41
+ module Extrospection
42
+ EXTRO_FILE = File.join(Dir.home, '.pwn', 'extrospection.json')
43
+ MAX_OBSERVATIONS = 500
44
+ PROBE_BINS = %w[nmap curl git ruby python3 gcc msfconsole sqlmap burpsuite zaproxy openssl docker].freeze
45
+
46
+ # Supported Method Parameters::
47
+ # store = PWN::AI::Agent::Extrospection.load
48
+
49
+ public_class_method def self.load
50
+ FileUtils.mkdir_p(File.dirname(EXTRO_FILE))
51
+ return { snapshot: {}, previous: {}, observations: [], updated_at: nil } unless File.exist?(EXTRO_FILE)
52
+
53
+ JSON.parse(File.read(EXTRO_FILE), symbolize_names: true)
54
+ rescue StandardError
55
+ { snapshot: {}, previous: {}, observations: [], updated_at: nil }
56
+ end
57
+
58
+ # Supported Method Parameters::
59
+ # PWN::AI::Agent::Extrospection.save(
60
+ # store: 'required - Hash returned by .load / mutated in place'
61
+ # )
62
+
63
+ public_class_method def self.save(opts = {})
64
+ store = opts[:store] ||= { snapshot: {}, previous: {}, observations: [] }
65
+ store[:updated_at] = Time.now.utc.iso8601
66
+ FileUtils.mkdir_p(File.dirname(EXTRO_FILE))
67
+ File.write(EXTRO_FILE, JSON.pretty_generate(store))
68
+ store
69
+ end
70
+
71
+ # Supported Method Parameters::
72
+ # snap = PWN::AI::Agent::Extrospection.snapshot(
73
+ # persist: 'optional - Boolean, write snapshot to disk & rotate previous (default true)',
74
+ # sections: 'optional - Array subset of [:host, :net, :toolchain, :repo, :env] (default all)'
75
+ # )
76
+ #
77
+ # Captures a fingerprint of the OUTSIDE world. When persist:true the
78
+ # prior snapshot is rotated into :previous so .drift can diff them.
79
+
80
+ public_class_method def self.snapshot(opts = {})
81
+ persist = if opts.key?(:persist)
82
+ opts[:persist] ? true : false
83
+ else
84
+ true
85
+ end
86
+ sections = Array(opts[:sections]).map(&:to_sym)
87
+ sections = %i[host net toolchain repo env] if sections.empty?
88
+
89
+ snap = {}
90
+ snap[:host] = probe_host if sections.include?(:host)
91
+ snap[:net] = probe_net if sections.include?(:net)
92
+ snap[:toolchain] = probe_toolchain if sections.include?(:toolchain)
93
+ snap[:repo] = probe_repo if sections.include?(:repo)
94
+ snap[:env] = probe_env if sections.include?(:env)
95
+ snap[:captured_at] = Time.now.utc.iso8601
96
+ snap[:fingerprint] = Digest::SHA256.hexdigest(JSON.generate(snap.except(:captured_at)))[0, 16]
97
+
98
+ if persist
99
+ store = load
100
+ store[:previous] = store[:snapshot] || {}
101
+ store[:snapshot] = snap
102
+ save(store: store)
103
+ end
104
+
105
+ drift = compute_drift(before: load[:previous] || {}, after: snap)
106
+ { snapshot: snap, drift: drift, persisted: persist }
107
+ end
108
+
109
+ # Supported Method Parameters::
110
+ # obs = PWN::AI::Agent::Extrospection.observe(
111
+ # source: 'required - where the observation came from (nmap, shodan, burp, cve, human, ...)',
112
+ # data: 'required - the observation payload (String or Hash)',
113
+ # category: 'optional - :recon, :vuln, :intel, :target, :network, :misc (default :misc)',
114
+ # target: 'optional - host/ip/url/asset the observation is about',
115
+ # tags: 'optional - Array of String labels',
116
+ # ttl: 'optional - seconds until this observation is considered stale (default nil = forever)'
117
+ # )
118
+ #
119
+ # Records a fact about the OUTSIDE world (as opposed to
120
+ # Learning.note_outcome which records a fact about the agent's own
121
+ # behaviour). Observations are re-injected via .to_context so recon
122
+ # findings and threat-intel persist across sessions.
123
+
124
+ public_class_method def self.observe(opts = {})
125
+ source = opts[:source].to_s
126
+ data = opts[:data]
127
+ raise 'ERROR: source is required' if source.strip.empty?
128
+ raise 'ERROR: data is required' if data.to_s.strip.empty?
129
+
130
+ entry = {
131
+ id: Digest::SHA256.hexdigest("#{source}-#{data}-#{Time.now.to_f}")[0, 12],
132
+ source: source,
133
+ category: (opts[:category] || :misc).to_s,
134
+ target: opts[:target].to_s.empty? ? nil : opts[:target].to_s,
135
+ data: data.is_a?(String) ? data[0, 4_000] : data,
136
+ tags: Array(opts[:tags]).map(&:to_s),
137
+ ttl: opts[:ttl]&.to_i,
138
+ timestamp: Time.now.utc.iso8601
139
+ }
140
+ store = load
141
+ store[:observations] ||= []
142
+ store[:observations] << entry
143
+ store[:observations].shift(store[:observations].length - MAX_OBSERVATIONS) if store[:observations].length > MAX_OBSERVATIONS
144
+ save(store: store)
145
+ entry
146
+ end
147
+
148
+ # Supported Method Parameters::
149
+ # rows = PWN::AI::Agent::Extrospection.observations(
150
+ # limit: 'optional - max entries newest-first (default 50)',
151
+ # source: 'optional - filter by source substring',
152
+ # category: 'optional - filter by category',
153
+ # target: 'optional - filter by target substring',
154
+ # tag: 'optional - filter by tag substring',
155
+ # fresh_only: 'optional - drop entries whose ttl has expired (default false)'
156
+ # )
157
+
158
+ public_class_method def self.observations(opts = {})
159
+ limit = opts[:limit] || 50
160
+ src = opts[:source].to_s.downcase
161
+ cat = opts[:category].to_s.downcase
162
+ tgt = opts[:target].to_s.downcase
163
+ tag = opts[:tag].to_s.downcase
164
+ fresh = opts[:fresh_only] ? true : false
165
+ now = Time.now.utc
166
+
167
+ rows = Array(load[:observations])
168
+ rows = rows.reject do |o|
169
+ next true if fresh && o[:ttl] && (Time.parse(o[:timestamp].to_s) + o[:ttl].to_i) < now
170
+
171
+ (src.empty? ? false : !o[:source].to_s.downcase.include?(src)) ||
172
+ (cat.empty? ? false : o[:category].to_s.downcase != cat) ||
173
+ (tgt.empty? ? false : !o[:target].to_s.downcase.include?(tgt)) ||
174
+ (tag.empty? ? false : Array(o[:tags]).none? { |t| t.to_s.downcase.include?(tag) })
175
+ rescue StandardError
176
+ false
177
+ end
178
+ rows.reverse.first(limit)
179
+ end
180
+
181
+ # Supported Method Parameters::
182
+ # delta = PWN::AI::Agent::Extrospection.drift(
183
+ # live: 'optional - probe host NOW and diff vs stored snapshot (default true). When false, diff stored :snapshot vs stored :previous.'
184
+ # )
185
+
186
+ public_class_method def self.drift(opts = {})
187
+ live = if opts.key?(:live)
188
+ opts[:live] ? true : false
189
+ else
190
+ true
191
+ end
192
+ store = load
193
+ if live
194
+ after = snapshot(persist: false)[:snapshot]
195
+ before = store[:snapshot] || {}
196
+ else
197
+ after = store[:snapshot] || {}
198
+ before = store[:previous] || {}
199
+ end
200
+ compute_drift(before: before, after: after)
201
+ end
202
+
203
+ # Supported Method Parameters::
204
+ # hits = PWN::AI::Agent::Extrospection.intel(
205
+ # query: 'required - keyword / product / CVE id to search external threat-intel for',
206
+ # feeds: 'optional - Array subset of [:nvd, :circl, :exploitdb] (default all)',
207
+ # limit: 'optional - max results per feed (default 5)',
208
+ # record: 'optional - also persist each hit as an observation (default false)'
209
+ # )
210
+ #
211
+ # Best-effort external lookups. Network / API failures degrade to
212
+ # an empty result for that feed rather than raising, so learning
213
+ # never breaks the primary loop when offline.
214
+
215
+ public_class_method def self.intel(opts = {})
216
+ query = opts[:query].to_s.strip
217
+ raise 'ERROR: query is required' if query.empty?
218
+
219
+ feeds = Array(opts[:feeds]).map(&:to_sym)
220
+ feeds = %i[nvd circl exploitdb] if feeds.empty?
221
+ limit = opts[:limit] || 5
222
+ record = opts[:record] ? true : false
223
+
224
+ results = {}
225
+ results[:nvd] = intel_nvd(query: query, limit: limit) if feeds.include?(:nvd)
226
+ results[:circl] = intel_circl(query: query, limit: limit) if feeds.include?(:circl)
227
+ results[:exploitdb] = intel_exploitdb(query: query, limit: limit) if feeds.include?(:exploitdb)
228
+
229
+ if record
230
+ results.each do |feed, hits|
231
+ Array(hits).each do |h|
232
+ observe(source: feed.to_s, category: :intel, data: h, tags: ['intel', query], target: h.is_a?(Hash) ? h[:id].to_s : nil)
233
+ end
234
+ end
235
+ end
236
+ { query: query, feeds: feeds, results: results, total: results.values.flatten.compact.length }
237
+ end
238
+
239
+ # Supported Method Parameters::
240
+ # findings = PWN::AI::Agent::Extrospection.correlate(
241
+ # limit: 'optional - max findings returned (default 10)'
242
+ # )
243
+ #
244
+ # THE join between introspection and extrospection. Cross-references:
245
+ # * Metrics tools with success_rate < 50 % vs toolchain drift / missing bins
246
+ # * Learning failures vs host / net drift on the same day
247
+ # * Observations tagged :vuln / :intel vs installed package versions
248
+ # Emits human-readable, actionable findings the model can reason on.
249
+
250
+ public_class_method def self.correlate(opts = {})
251
+ limit = opts[:limit] || 10
252
+ findings = []
253
+ delta = drift(live: false)
254
+ snap = load[:snapshot] || {}
255
+
256
+ # 1) failing tools whose backing binary vanished / changed version
257
+ if defined?(Metrics)
258
+ Metrics.summary(limit: 50).each do |m|
259
+ next unless m[:success_rate] < 0.5 && m[:calls] > 2
260
+
261
+ bin = m[:name].to_s.split('_').first
262
+ tc = Array(delta[:changed]).find { |c| c[:path].to_s.include?("toolchain.#{bin}") }
263
+ miss = snap.dig(:toolchain, bin.to_sym).to_s.empty?
264
+ next unless tc || miss
265
+
266
+ findings << { kind: :tool_env_mismatch, tool: m[:name], success_rate: m[:success_rate], evidence: tc || "binary '#{bin}' not found in PATH", advice: "Re-verify `which #{bin}` / reinstall before relying on #{m[:name]}." }
267
+ end
268
+ end
269
+
270
+ # 2) introspective failures coinciding with extrospective drift
271
+ if defined?(Learning)
272
+ Learning.outcomes(limit: 30, success: false).each do |o|
273
+ day = o[:timestamp].to_s[0, 10]
274
+ hit = Array(delta[:changed]).find { |c| c[:after].to_s.include?(day) || c[:path].to_s.match?(/kernel|repo|net/) }
275
+ findings << { kind: :failure_during_drift, task: o[:task], on: day, drift: hit, advice: 'Environment changed around this failure — re-test under current snapshot before trusting the negative result.' } if hit
276
+ end
277
+ end
278
+
279
+ # 3) intel observations matching installed components
280
+ pkgs = snap[:toolchain] || {}
281
+ observations(category: 'intel', limit: 100).each do |ob|
282
+ blob = ob[:data].to_s.downcase
283
+ pkgs.each do |bin, ver|
284
+ next if ver.to_s.empty?
285
+ next unless blob.include?(bin.to_s.downcase)
286
+
287
+ findings << { kind: :intel_matches_host, component: bin, installed: ver, intel: ob[:data], source: ob[:source], advice: "Review #{ob[:source]} advisory for #{bin} #{ver} on this host." }
288
+ end
289
+ end
290
+
291
+ # 4) raw drift as low-priority findings when nothing else matched
292
+ Array(delta[:added]).first(5).each { |c| findings << { kind: :env_added, detail: c } } if findings.empty?
293
+ Array(delta[:removed]).first(5).each { |c| findings << { kind: :env_removed, detail: c } } if findings.empty?
294
+
295
+ findings.uniq.first(limit)
296
+ end
297
+
298
+ # Supported Method Parameters::
299
+ # ctx = PWN::AI::Agent::Extrospection.to_context(
300
+ # drift_limit: 'optional - max drift lines (default 4)',
301
+ # obs_limit: 'optional - max observation lines (default 4)'
302
+ # )
303
+
304
+ public_class_method def self.to_context(opts = {})
305
+ dlim = opts[:drift_limit] || 4
306
+ olim = opts[:obs_limit] || 4
307
+ store = load
308
+ return '' if (store[:snapshot] || {}).empty? && Array(store[:observations]).empty?
309
+
310
+ snap = store[:snapshot] || {}
311
+ delta = compute_drift(before: store[:previous] || {}, after: snap)
312
+ lines = []
313
+ host = snap[:host] || {}
314
+ lines << " host_fp : #{snap[:fingerprint]} (#{host[:os]} #{host[:kernel]}, #{host[:arch]})" if snap[:fingerprint]
315
+ lines << " captured : #{snap[:captured_at]}" if snap[:captured_at]
316
+
317
+ ch = Array(delta[:changed]).first(dlim).map { |c| " ~ #{c[:path]}: #{c[:before].to_s[0, 40]} -> #{c[:after].to_s[0, 40]}" }
318
+ ad = Array(delta[:added]).first(dlim).map { |c| " + #{c[:path]}: #{c[:after].to_s[0, 60]}" }
319
+ rm = Array(delta[:removed]).first(dlim).map { |c| " - #{c[:path]}: #{c[:before].to_s[0, 60]}" }
320
+ unless ch.empty? && ad.empty? && rm.empty?
321
+ lines << ' drift :'
322
+ lines.concat(ch).concat(ad).concat(rm)
323
+ end
324
+
325
+ obs = observations(limit: olim, fresh_only: true)
326
+ unless obs.empty?
327
+ lines << ' observed :'
328
+ obs.each { |o| lines << " * [#{o[:category]}/#{o[:source]}] #{"#{o[:target]} — " if o[:target]}#{o[:data].to_s.gsub(/\s+/, ' ')[0, 120]}" }
329
+ end
330
+
331
+ "EXTROSPECTION (world-state; correlate with introspective failures)\n#{lines.join("\n")}\n\n"
332
+ end
333
+
334
+ # Supported Method Parameters::
335
+ # stats = PWN::AI::Agent::Extrospection.stats
336
+
337
+ public_class_method def self.stats
338
+ store = load
339
+ snap = store[:snapshot] || {}
340
+ delta = compute_drift(before: store[:previous] || {}, after: snap)
341
+ {
342
+ snapshot_captured_at: snap[:captured_at],
343
+ snapshot_fingerprint: snap[:fingerprint],
344
+ observations: Array(store[:observations]).length,
345
+ drift_changed: Array(delta[:changed]).length,
346
+ drift_added: Array(delta[:added]).length,
347
+ drift_removed: Array(delta[:removed]).length,
348
+ toolchain_bins: (snap[:toolchain] || {}).count { |_, v| !v.to_s.empty? },
349
+ listening_ports: Array(snap.dig(:net, :listening)).length
350
+ }
351
+ end
352
+
353
+ # Supported Method Parameters::
354
+ # PWN::AI::Agent::Extrospection.auto_extrospect(
355
+ # session_id: 'optional - id of the just-completed session (for tagging)'
356
+ # )
357
+ #
358
+ # Called by Learning.auto_reflect when
359
+ # PWN::Env[:ai][:agent][:auto_extrospect] is truthy. Captures a fresh
360
+ # snapshot and, if drift is non-trivial, records it as an observation
361
+ # and a PWN::Memory :env fact so the NEXT session's system prompt
362
+ # already knows the world moved. Never raises.
363
+
364
+ public_class_method def self.auto_extrospect(opts = {})
365
+ sid = opts[:session_id]
366
+ return unless auto_extrospect_enabled?
367
+
368
+ res = snapshot(persist: true)
369
+ delta = res[:drift]
370
+ moved = Array(delta[:changed]).length + Array(delta[:added]).length + Array(delta[:removed]).length
371
+ return res if moved.zero?
372
+
373
+ summary = summarise_drift(delta: delta)
374
+ observe(source: 'auto_extrospect', category: :env, data: summary, tags: ['drift', sid.to_s].reject(&:empty?))
375
+ if defined?(PWN::Memory)
376
+ key = :"extro_drift_#{res[:snapshot][:fingerprint]}"
377
+ PWN::Memory.remember(key: key, value: "ENV DRIFT #{res[:snapshot][:captured_at]}: #{summary[0, 260]}", category: :env)
378
+ end
379
+ res
380
+ rescue StandardError => e
381
+ warn "[pwn-ai/extrospection] auto_extrospect swallowed: #{e.class}: #{e.message}"
382
+ nil
383
+ end
384
+
385
+ # Supported Method Parameters::
386
+ # PWN::AI::Agent::Extrospection.reset
387
+
388
+ public_class_method def self.reset
389
+ FileUtils.rm_f(EXTRO_FILE)
390
+ { cleared: true }
391
+ end
392
+
393
+ # -------------------------------------------------------------
394
+ # privates
395
+ # -------------------------------------------------------------
396
+
397
+ private_class_method def self.auto_extrospect_enabled?
398
+ return false unless defined?(PWN::Env) && PWN::Env.is_a?(Hash)
399
+
400
+ PWN::Env.dig(:ai, :agent, :auto_extrospect) ? true : false
401
+ rescue StandardError
402
+ false
403
+ end
404
+
405
+ private_class_method def self.sh(opts = {})
406
+ cmd = opts[:cmd].to_s
407
+ return '' if cmd.empty?
408
+
409
+ out, = Open3.capture2e(cmd)
410
+ out.to_s.strip
411
+ rescue StandardError
412
+ ''
413
+ end
414
+
415
+ private_class_method def self.probe_host
416
+ {
417
+ hostname: begin
418
+ Socket.gethostname
419
+ rescue StandardError
420
+ ''
421
+ end,
422
+ os: sh(cmd: 'uname -s'),
423
+ kernel: sh(cmd: 'uname -r'),
424
+ arch: sh(cmd: 'uname -m'),
425
+ distro: begin
426
+ File.read('/etc/os-release')[/PRETTY_NAME="?([^"\n]+)/, 1]
427
+ rescue StandardError
428
+ nil
429
+ end,
430
+ uptime_s: begin
431
+ File.read('/proc/uptime').split.first.to_i
432
+ rescue StandardError
433
+ nil
434
+ end,
435
+ user: ENV.fetch('USER', ''),
436
+ cpu_count: (Etc.respond_to?(:nprocessors) ? Etc.nprocessors : nil)
437
+ }
438
+ end
439
+
440
+ private_class_method def self.probe_net
441
+ listen = sh(cmd: 'ss -tlnH 2>/dev/null').lines.map { |l| l.split[3].to_s.split(':').last }.compact.uniq.sort
442
+ ifaces = sh(cmd: 'ip -o -4 addr show 2>/dev/null').lines.map do |l|
443
+ p = l.split
444
+ { if: p[1], addr: p[3] }
445
+ end
446
+ route = sh(cmd: 'ip route show default 2>/dev/null').lines.first.to_s.strip
447
+ { listening: listen, interfaces: ifaces, default_route: route }
448
+ end
449
+
450
+ private_class_method def self.probe_toolchain
451
+ PROBE_BINS.each_with_object({}) do |b, h|
452
+ path = sh(cmd: "which #{b} 2>/dev/null")
453
+ h[b.to_sym] = path.empty? ? '' : "#{path} #{sh(cmd: "#{b} --version 2>/dev/null").lines.first.to_s.strip[0, 80]}"
454
+ end
455
+ end
456
+
457
+ private_class_method def self.probe_repo
458
+ root = defined?(PWN::ROOT) ? PWN::ROOT.to_s : Dir.pwd
459
+ return {} unless Dir.exist?(File.join(root, '.git'))
460
+
461
+ {
462
+ root: root,
463
+ head: sh(cmd: "git -C #{root} rev-parse --short HEAD"),
464
+ branch: sh(cmd: "git -C #{root} rev-parse --abbrev-ref HEAD"),
465
+ dirty: !sh(cmd: "git -C #{root} status --porcelain").empty?,
466
+ pwn_version: (defined?(PWN::VERSION) ? PWN::VERSION : nil)
467
+ }
468
+ end
469
+
470
+ private_class_method def self.probe_env
471
+ {
472
+ cwd: Dir.pwd,
473
+ ruby: RUBY_VERSION,
474
+ path_entries: ENV.fetch('PATH', '').split(':').length,
475
+ ai_engine: (PWN::Env.dig(:ai, :active) if defined?(PWN::Env) && PWN::Env.is_a?(Hash))
476
+ }
477
+ rescue StandardError
478
+ { cwd: Dir.pwd, ruby: RUBY_VERSION }
479
+ end
480
+
481
+ private_class_method def self.flatten(opts = {})
482
+ hash = opts[:hash] || {}
483
+ prefix = opts[:prefix].to_s
484
+ out = opts[:out] || {}
485
+ hash.each do |k, v|
486
+ path = prefix.empty? ? k.to_s : "#{prefix}.#{k}"
487
+ case v
488
+ when Hash then flatten(hash: v, prefix: path, out: out)
489
+ when Array then out[path] = v.map(&:to_s).sort
490
+ else out[path] = v
491
+ end
492
+ end
493
+ out
494
+ end
495
+
496
+ private_class_method def self.compute_drift(opts = {})
497
+ before = flatten(hash: (opts[:before] || {}).except(:captured_at, :fingerprint))
498
+ after = flatten(hash: (opts[:after] || {}).except(:captured_at, :fingerprint))
499
+ changed = []
500
+ added = []
501
+ removed = []
502
+ (before.keys | after.keys).each do |k|
503
+ next if k =~ /uptime_s$/
504
+
505
+ b = before[k]
506
+ a = after[k]
507
+ if b.nil? && !a.nil?
508
+ added << { path: k, after: a }
509
+ elsif a.nil? && !b.nil?
510
+ removed << { path: k, before: b }
511
+ elsif a != b
512
+ changed << { path: k, before: b, after: a }
513
+ end
514
+ end
515
+ { changed: changed, added: added, removed: removed }
516
+ end
517
+
518
+ private_class_method def self.summarise_drift(opts = {})
519
+ d = opts[:delta] || {}
520
+ parts = Array(d[:changed]).first(5).map { |c| "~#{c[:path]}:#{c[:before].to_s[0, 30]}->#{c[:after].to_s[0, 30]}" }
521
+ Array(d[:added]).first(5).each { |c| parts << "+#{c[:path]}" }
522
+ Array(d[:removed]).first(5).each { |c| parts << "-#{c[:path]}" }
523
+ parts.empty? ? '(no drift)' : parts.join(' | ')
524
+ end
525
+
526
+ private_class_method def self.http_get_json(opts = {})
527
+ url = opts[:url].to_s
528
+ return nil if url.empty?
529
+
530
+ require 'rest-client'
531
+ resp = RestClient::Request.execute(method: :get, url: url, timeout: 8, open_timeout: 4, headers: { accept: :json, user_agent: 'pwn-ai-extrospection' })
532
+ JSON.parse(resp.body, symbolize_names: true)
533
+ rescue StandardError
534
+ nil
535
+ end
536
+
537
+ private_class_method def self.intel_nvd(opts = {})
538
+ q = opts[:query].to_s
539
+ limit = opts[:limit] || 5
540
+ url = "https://services.nvd.nist.gov/rest/json/cves/2.0?keywordSearch=#{URI.encode_www_form_component(q)}&resultsPerPage=#{limit}"
541
+ body = http_get_json(url: url)
542
+ return [] unless body
543
+
544
+ Array(body[:vulnerabilities]).first(limit).map do |v|
545
+ c = v[:cve] || {}
546
+ { id: c[:id], published: c[:published], desc: Array(c[:descriptions])&.first&.dig(:value).to_s[0, 300], cvss: c.dig(:metrics, :cvssMetricV31, 0, :cvssData, :baseScore) }
547
+ end
548
+ rescue StandardError
549
+ []
550
+ end
551
+
552
+ private_class_method def self.intel_circl(opts = {})
553
+ q = opts[:query].to_s
554
+ limit = opts[:limit] || 5
555
+ body = if q =~ /^CVE-\d{4}-\d+$/i
556
+ r = http_get_json(url: "https://cve.circl.lu/api/cve/#{q.upcase}")
557
+ r ? [r] : []
558
+ else
559
+ http_get_json(url: "https://cve.circl.lu/api/search/#{URI.encode_www_form_component(q)}") || []
560
+ end
561
+ Array(body).flatten.first(limit).map do |c|
562
+ { id: c[:id], cvss: c[:cvss], summary: c[:summary].to_s[0, 300], refs: Array(c[:references]).first(3) }
563
+ end
564
+ rescue StandardError
565
+ []
566
+ end
567
+
568
+ private_class_method def self.intel_exploitdb(opts = {})
569
+ q = opts[:query].to_s
570
+ limit = opts[:limit] || 5
571
+ bin = sh(cmd: 'which searchsploit 2>/dev/null')
572
+ return [] if bin.empty?
573
+
574
+ raw = sh(cmd: "searchsploit --json #{q.shellescape} 2>/dev/null")
575
+ j = begin
576
+ JSON.parse(raw, symbolize_names: true)
577
+ rescue StandardError
578
+ nil
579
+ end
580
+ return [] unless j
581
+
582
+ Array(j[:RESULTS_EXPLOIT]).first(limit).map { |e| { id: "EDB-#{e[:'EDB-ID']}", title: e[:Title].to_s[0, 200], path: e[:Path] } }
583
+ rescue StandardError
584
+ []
585
+ end
586
+
587
+ # Author(s):: 0day Inc. <support@0dayinc.com>
588
+
589
+ public_class_method def self.authors
590
+ "AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
591
+ end
592
+
593
+ # Display Usage for this Module
594
+
595
+ public_class_method def self.help
596
+ puts <<~USAGE
597
+ USAGE:
598
+ PWN::AI::Agent::Extrospection.snapshot # probe host, persist, return {snapshot:, drift:}
599
+ PWN::AI::Agent::Extrospection.drift(live: true) # what changed vs last snapshot
600
+ PWN::AI::Agent::Extrospection.observe(source: 'nmap', category: :recon, target: '10.0.0.5', data: '22/tcp open ssh 9.6')
601
+ PWN::AI::Agent::Extrospection.observations(category: 'recon', target: '10.0.0.5')
602
+ PWN::AI::Agent::Extrospection.intel(query: 'openssl 3.0', record: true)
603
+ PWN::AI::Agent::Extrospection.correlate # introspection x extrospection findings
604
+ PWN::AI::Agent::Extrospection.to_context # injected by PromptBuilder
605
+ PWN::AI::Agent::Extrospection.stats
606
+ PWN::AI::Agent::Extrospection.auto_extrospect(session_id: sid) # called from Learning.auto_reflect
607
+ PWN::AI::Agent::Extrospection.reset
608
+
609
+ Enable end-of-run auto-extrospection with:
610
+ PWN::Env[:ai][:agent][:auto_extrospect] = true
611
+
612
+ #{self}.authors
613
+ USAGE
614
+ end
615
+ end
616
+ end
617
+ end
618
+ end
@@ -3,7 +3,7 @@
3
3
  module PWN
4
4
  module AI
5
5
  module Agent
6
- # This module is an AI agent designed to analyze signal data captured by a software-defined-radio using GQRX. It uses the PWN::AI::Introspection.reflect_on method to analyze the signal data and provide insights based on the location where the data was captured. The agent can determine if the frequency is licensed or unlicensed based on FCC records and provide relevant information about the transmission. This module is useful for security professionals, researchers, and hobbyists interested in analyzing radio signals and understanding their context.
6
+ # This module is an AI agent designed to analyze signal data captured by a software-defined-radio using GQRX. It uses the PWN::AI::Agent::Introspection.reflect_on method to analyze the signal data and provide insights based on the location where the data was captured. The agent can determine if the frequency is licensed or unlicensed based on FCC records and provide relevant information about the transmission. This module is useful for security professionals, researchers, and hobbyists interested in analyzing radio signals and understanding their context.
7
7
  module GQRX
8
8
  # Supported Method Parameters::
9
9
  # ai_analysis = PWN::AI::Agent::GQRX.analyze(
@@ -20,7 +20,7 @@ module PWN
20
20
 
21
21
  system_role_content = "Analyze signal data captured by a software-defined-radio using GQRX at the following location: #{location}. Respond with just FCC information about the transmission if available. If the frequency is unlicensed or not found in FCC records, state that clearly. Be clear and concise in your analysis."
22
22
 
23
- PWN::AI::Introspection.reflect_on(
23
+ PWN::AI::Agent::Introspection.reflect_on(
24
24
  system_role_content: system_role_content,
25
25
  request: request,
26
26
  suppress_pii_warning: true
@@ -29,7 +29,7 @@ module PWN
29
29
  raise "ERROR: type parameter value of #{type} is not supported"
30
30
  end
31
31
 
32
- PWN::AI::Introspection.reflect_on(
32
+ PWN::AI::Agent::Introspection.reflect_on(
33
33
  system_role_content: system_role_content,
34
34
  request: request,
35
35
  spinner: true,