pwn 0.5.620 → 0.5.622
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 +1 -1
- data/README.md +1 -1
- data/documentation/Agent-Tool-Registry.md +3 -3
- data/documentation/Configuration.md +292 -47
- data/documentation/Cron.md +14 -1
- data/documentation/Diagrams.md +1 -1
- data/documentation/Extrospection.md +71 -5
- data/documentation/Home.md +2 -2
- data/documentation/How-PWN-Works.md +1 -1
- data/documentation/Mistakes.md +2 -1
- data/documentation/Persistence.md +2 -1
- data/documentation/Skills-Memory-Learning.md +3 -2
- data/documentation/Transparent-Browser.md +18 -1
- data/documentation/diagrams/agent-tool-registry.svg +55 -54
- 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 +2 -2
- data/documentation/diagrams/dot/extrospection-world-awareness.dot +29 -8
- data/documentation/diagrams/dot/memory-skills-detailed.dot +4 -0
- data/documentation/diagrams/dot/overall-pwn-architecture.dot +2 -2
- data/documentation/diagrams/dot/persistence-filesystem.dot +2 -1
- data/documentation/diagrams/dot/pwn-ai-feedback-learning-loop.dot +11 -5
- data/documentation/diagrams/driver-framework.svg +13 -13
- data/documentation/diagrams/extrospection-world-awareness.svg +229 -99
- 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 +124 -101
- data/documentation/diagrams/mistakes-negative-feedback.svg +55 -55
- data/documentation/diagrams/network-infra-testing.svg +27 -27
- data/documentation/diagrams/overall-pwn-architecture.svg +85 -85
- data/documentation/diagrams/penetration-testing-workflow.svg +30 -30
- data/documentation/diagrams/persistence-filesystem.svg +94 -85
- data/documentation/diagrams/plugin-ecosystem.svg +35 -35
- data/documentation/diagrams/pwn-ai-feedback-learning-loop.svg +226 -180
- 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 +31 -31
- 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 +4 -4
- 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 +435 -6
- data/lib/pwn/ai/agent/gqrx.rb +2 -2
- data/lib/pwn/ai/agent/hacker_one.rb +1 -1
- data/lib/pwn/ai/agent/learning.rb +16 -16
- data/lib/pwn/ai/agent/loop.rb +1 -1
- data/lib/pwn/ai/agent/{introspection.rb → reflect.rb} +12 -12
- data/lib/pwn/ai/agent/sast.rb +1 -1
- data/lib/pwn/ai/agent/tools/extrospection.rb +89 -4
- data/lib/pwn/ai/agent/tools/learning.rb +7 -7
- 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 +1 -1
- data/lib/pwn/ai/open_ai.rb +10 -6
- data/lib/pwn/config.rb +5 -4
- data/lib/pwn/plugins/burp_suite.rb +2 -2
- data/lib/pwn/sast/pom_version.rb +2 -2
- data/lib/pwn/sast/test_case_engine.rb +1 -1
- data/lib/pwn/sdr/decoder/adsb.rb +14 -33
- data/lib/pwn/sdr/decoder/apt.rb +97 -36
- data/lib/pwn/sdr/decoder/base.rb +258 -171
- data/lib/pwn/sdr/decoder/bluetooth.rb +13 -20
- data/lib/pwn/sdr/decoder/dect.rb +13 -27
- 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 +12 -21
- data/lib/pwn/sdr/decoder/gsm.rb +21 -73
- data/lib/pwn/sdr/decoder/iridium.rb +11 -26
- data/lib/pwn/sdr/decoder/lora.rb +16 -23
- data/lib/pwn/sdr/decoder/lte.rb +11 -32
- data/lib/pwn/sdr/decoder/morse.rb +95 -21
- data/lib/pwn/sdr/decoder/p25.rb +19 -16
- data/lib/pwn/sdr/decoder/pager.rb +22 -38
- data/lib/pwn/sdr/decoder/pocsag.rb +175 -68
- data/lib/pwn/sdr/decoder/rfid.rb +24 -33
- data/lib/pwn/sdr/decoder/rtl433.rb +21 -30
- data/lib/pwn/sdr/decoder/rtty.rb +100 -27
- data/lib/pwn/sdr/decoder/wifi.rb +21 -42
- data/lib/pwn/sdr/decoder/zigbee.rb +18 -40
- data/lib/pwn/sdr/decoder.rb +8 -0
- data/lib/pwn/sdr/gqrx.rb +1 -1
- data/lib/pwn/version.rb +1 -1
- data/spec/lib/pwn/ai/agent/{introspection_spec.rb → reflect_spec.rb} +3 -3
- data/spec/lib/pwn/sdr/decoder/dsp_spec.rb +15 -0
- data/third_party/pwn_rdoc.jsonl +75 -19
- metadata +7 -5
|
@@ -9,6 +9,7 @@ require 'open3'
|
|
|
9
9
|
require 'etc'
|
|
10
10
|
require 'uri'
|
|
11
11
|
require 'shellwords'
|
|
12
|
+
require 'timeout'
|
|
12
13
|
|
|
13
14
|
module PWN
|
|
14
15
|
module AI
|
|
@@ -43,6 +44,12 @@ module PWN
|
|
|
43
44
|
MAX_OBSERVATIONS = 500
|
|
44
45
|
RF_BINS = %w[rtl_sdr rtl_test rtl_433 hackrf_info gqrx dump1090 multimon-ng SoapySDRUtil].freeze
|
|
45
46
|
PROBE_BINS = (%w[nmap curl git ruby python3 gcc msfconsole sqlmap burpsuite zaproxy openssl docker] + RF_BINS).freeze
|
|
47
|
+
WEB_SHOT_DIR = File.join(Dir.home, '.pwn', 'extrospection', 'web')
|
|
48
|
+
DEFAULT_WEB_ANCHORS = %w[
|
|
49
|
+
https://services.nvd.nist.gov/rest/json/cves/2.0
|
|
50
|
+
https://www.exploit-db.com/
|
|
51
|
+
https://raw.githubusercontent.com/0dayinc/pwn/master/lib/pwn/version.rb
|
|
52
|
+
].freeze
|
|
46
53
|
|
|
47
54
|
# Supported Method Parameters::
|
|
48
55
|
# store = PWN::AI::Agent::Extrospection.load
|
|
@@ -72,7 +79,7 @@ module PWN
|
|
|
72
79
|
# Supported Method Parameters::
|
|
73
80
|
# snap = PWN::AI::Agent::Extrospection.snapshot(
|
|
74
81
|
# persist: 'optional - Boolean, write snapshot to disk & rotate previous (default true)',
|
|
75
|
-
# sections: 'optional - Array subset of [:host, :net, :toolchain, :repo, :env, :rf] (default all)'
|
|
82
|
+
# sections: 'optional - Array subset of [:host, :net, :toolchain, :repo, :env, :rf, :web] (default all except :web)'
|
|
76
83
|
# )
|
|
77
84
|
#
|
|
78
85
|
# Captures a fingerprint of the OUTSIDE world. When persist:true the
|
|
@@ -94,6 +101,7 @@ module PWN
|
|
|
94
101
|
snap[:repo] = probe_repo if sections.include?(:repo)
|
|
95
102
|
snap[:env] = probe_env if sections.include?(:env)
|
|
96
103
|
snap[:rf] = probe_rf if sections.include?(:rf)
|
|
104
|
+
snap[:web] = probe_web if sections.include?(:web)
|
|
97
105
|
snap[:captured_at] = Time.now.utc.iso8601
|
|
98
106
|
snap[:fingerprint] = Digest::SHA256.hexdigest(JSON.generate(snap.except(:captured_at)))[0, 16]
|
|
99
107
|
|
|
@@ -112,7 +120,7 @@ module PWN
|
|
|
112
120
|
# obs = PWN::AI::Agent::Extrospection.observe(
|
|
113
121
|
# source: 'required - where the observation came from (nmap, shodan, burp, cve, human, ...)',
|
|
114
122
|
# data: 'required - the observation payload (String or Hash)',
|
|
115
|
-
# category: 'optional - :recon, :vuln, :intel, :target, :network, :env, :rf, :misc (default :misc)',
|
|
123
|
+
# category: 'optional - :recon, :vuln, :intel, :target, :network, :env, :rf, :web, :misc (default :misc)',
|
|
116
124
|
# target: 'optional - host/ip/url/asset the observation is about',
|
|
117
125
|
# tags: 'optional - Array of String labels',
|
|
118
126
|
# ttl: 'optional - seconds until this observation is considered stale (default nil = forever)'
|
|
@@ -238,6 +246,189 @@ module PWN
|
|
|
238
246
|
{ query: query, feeds: feeds, results: results, total: results.values.flatten.compact.length }
|
|
239
247
|
end
|
|
240
248
|
|
|
249
|
+
# Supported Method Parameters::
|
|
250
|
+
# verdict = PWN::AI::Agent::Extrospection.verify(
|
|
251
|
+
# claim: 'required - factual claim to fact-check against the live web',
|
|
252
|
+
# kind: 'optional - :cve | :version | :doc | :generic (default auto-detect)',
|
|
253
|
+
# url: 'optional - explicit URL the claim cites (forces kind: :doc)',
|
|
254
|
+
# commit: 'optional - write Mistakes/Memory/observe on refute/confirm (default true)',
|
|
255
|
+
# proxy: 'optional - upstream proxy for TransparentBrowser (e.g. "tor")'
|
|
256
|
+
# )
|
|
257
|
+
#
|
|
258
|
+
# Browser-backed self fact-checking. Drives PWN::Plugins::TransparentBrowser
|
|
259
|
+
# in :headless mode against a canonical source for the claim class,
|
|
260
|
+
# renders the DOM (JS executed), and returns:
|
|
261
|
+
#
|
|
262
|
+
# { claim:, kind:, verdict: :confirmed|:refuted|:unknown,
|
|
263
|
+
# confidence: 0.0..1.0,
|
|
264
|
+
# evidence: [{url:, title:, excerpt:, dom_sha:, screenshot:}],
|
|
265
|
+
# action_taken: :mistakes_record | :extro_observe | :learning_note | nil }
|
|
266
|
+
#
|
|
267
|
+
# On :refuted -> Mistakes.record(tool:'assumption', error:claim) so the
|
|
268
|
+
# KNOWN MISTAKES block warns every future run.
|
|
269
|
+
# On :confirmed-> observe(category::intel, ttl:30d) reinforces w/ freshness.
|
|
270
|
+
# On :unknown -> Learning.note_outcome(success:false, tags:['needs_human']).
|
|
271
|
+
#
|
|
272
|
+
# This is the extrospective mirror of Mistakes: a PROACTIVE trigger that
|
|
273
|
+
# catches the model being wrong about the world before a human does.
|
|
274
|
+
|
|
275
|
+
public_class_method def self.verify(opts = {})
|
|
276
|
+
claim = opts[:claim].to_s.strip
|
|
277
|
+
raise 'ERROR: claim is required' if claim.empty?
|
|
278
|
+
|
|
279
|
+
kind = (opts[:kind] || detect_claim_kind(claim: claim, url: opts[:url])).to_sym
|
|
280
|
+
commit = opts.key?(:commit) ? !opts[:commit].nil? && opts[:commit] != false : true
|
|
281
|
+
proxy = opts[:proxy] || web_config[:proxy]
|
|
282
|
+
|
|
283
|
+
evidence = []
|
|
284
|
+
verdict = :unknown
|
|
285
|
+
conf = 0.0
|
|
286
|
+
|
|
287
|
+
with_headless_browser(proxy: proxy) do |bo|
|
|
288
|
+
case kind
|
|
289
|
+
when :cve
|
|
290
|
+
cve = claim[/CVE-\d{4}-\d{4,}/i].to_s.upcase
|
|
291
|
+
%W[https://nvd.nist.gov/vuln/detail/#{cve} https://www.cve.org/CVERecord?id=#{cve}].each do |u|
|
|
292
|
+
fp = fingerprint_page(browser_obj: bo, url: u)
|
|
293
|
+
evidence << fp if fp
|
|
294
|
+
end
|
|
295
|
+
body = evidence.map { |e| e[:text].to_s }.join(' ')
|
|
296
|
+
if evidence.empty? || evidence.all? { |e| e[:status].to_i >= 400 } || body =~ /could not be found|does not exist|reserved but/i
|
|
297
|
+
verdict = :refuted
|
|
298
|
+
conf = 0.8
|
|
299
|
+
else
|
|
300
|
+
prod = claim.sub(/CVE-\d{4}-\d{4,}/i, '').gsub(/affects?|is|in|the/i, ' ').split.select { |w| w.length > 2 }
|
|
301
|
+
hit = prod.count { |w| body.downcase.include?(w.downcase) }
|
|
302
|
+
if prod.empty? || hit.to_f / prod.length >= 0.5
|
|
303
|
+
verdict = :confirmed
|
|
304
|
+
conf = prod.empty? ? 0.6 : [0.5 + (hit.to_f / prod.length * 0.5), 0.95].min
|
|
305
|
+
else
|
|
306
|
+
verdict = :refuted
|
|
307
|
+
conf = 0.6
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
when :doc
|
|
311
|
+
u = opts[:url] || claim[URI::DEFAULT_PARSER.make_regexp(%w[http https])]
|
|
312
|
+
fp = fingerprint_page(browser_obj: bo, url: u)
|
|
313
|
+
evidence << fp if fp
|
|
314
|
+
if fp.nil? || fp[:status].to_i >= 400 || fp[:reachable] == false
|
|
315
|
+
verdict = :refuted
|
|
316
|
+
conf = 0.7
|
|
317
|
+
else
|
|
318
|
+
snippet = claim.sub(u.to_s, '').strip
|
|
319
|
+
overlap = fuzzy_overlap(needle: snippet, haystack: fp[:text].to_s)
|
|
320
|
+
if fp[:title].to_s =~ /not found|404|error/i && overlap < 0.2
|
|
321
|
+
verdict = :refuted
|
|
322
|
+
conf = 0.7
|
|
323
|
+
else
|
|
324
|
+
verdict = overlap >= 0.4 ? :confirmed : :unknown
|
|
325
|
+
conf = overlap
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
when :version
|
|
329
|
+
proj = claim[/[A-Za-z][\w.+-]{2,}/].to_s
|
|
330
|
+
ver = claim[/v?\d+\.\d+(?:\.\d+)?/].to_s
|
|
331
|
+
%W[https://rubygems.org/gems/#{proj} https://pypi.org/project/#{proj}/ https://github.com/search?q=#{URI.encode_www_form_component(proj)}&type=repositories].each do |u|
|
|
332
|
+
fp = fingerprint_page(browser_obj: bo, url: u)
|
|
333
|
+
evidence << fp if fp
|
|
334
|
+
end
|
|
335
|
+
body = evidence.map { |e| e[:text].to_s }.join(' ')
|
|
336
|
+
if !ver.empty? && body.include?(ver)
|
|
337
|
+
verdict = :confirmed
|
|
338
|
+
conf = 0.7
|
|
339
|
+
elsif body =~ /\d+\.\d+(?:\.\d+)?/
|
|
340
|
+
verdict = :unknown
|
|
341
|
+
conf = 0.3
|
|
342
|
+
end
|
|
343
|
+
else
|
|
344
|
+
q = URI.encode_www_form_component(claim)
|
|
345
|
+
fp = fingerprint_page(browser_obj: bo, url: "https://html.duckduckgo.com/html/?q=#{q}")
|
|
346
|
+
evidence << fp if fp
|
|
347
|
+
overlap = fuzzy_overlap(needle: claim, haystack: fp ? fp[:text].to_s : '')
|
|
348
|
+
verdict = overlap >= 0.5 ? :confirmed : :unknown
|
|
349
|
+
conf = overlap
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
evidence.each { |e| e.delete(:text) }
|
|
354
|
+
action = commit ? commit_verdict(claim: claim, kind: kind, verdict: verdict, confidence: conf, evidence: evidence) : nil
|
|
355
|
+
{ claim: claim, kind: kind, verdict: verdict, confidence: conf.round(2), evidence: evidence, action_taken: action }
|
|
356
|
+
rescue StandardError => e
|
|
357
|
+
{ claim: claim, kind: kind, verdict: :unknown, confidence: 0.0, evidence: evidence, error: "#{e.class}: #{e.message}", action_taken: nil }
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# Supported Method Parameters::
|
|
361
|
+
# obs = PWN::AI::Agent::Extrospection.watch(
|
|
362
|
+
# url: 'required - URL to render and fingerprint',
|
|
363
|
+
# selector: 'optional - CSS selector whose innerText to hash (default full body)',
|
|
364
|
+
# ttl: 'optional - seconds until stale (default 7 days)',
|
|
365
|
+
# proxy: 'optional - upstream proxy for TransparentBrowser'
|
|
366
|
+
# )
|
|
367
|
+
#
|
|
368
|
+
# Passive change-detection on an external artefact you care about
|
|
369
|
+
# (target /api/version, vendor changelog, bounty scope page).
|
|
370
|
+
# Renders headless, hashes the DOM text, screenshots, and persists as
|
|
371
|
+
# observe(category: :web). On subsequent snapshot(sections:[:web]) or
|
|
372
|
+
# watch of the same URL, a hash mismatch surfaces in drift() as
|
|
373
|
+
# ~web.<host>.dom_sha exactly like ~toolchain.nmap today.
|
|
374
|
+
|
|
375
|
+
public_class_method def self.watch(opts = {})
|
|
376
|
+
url = opts[:url].to_s
|
|
377
|
+
raise 'ERROR: url is required' if url.empty?
|
|
378
|
+
|
|
379
|
+
ttl = opts[:ttl] || (7 * 24 * 3600)
|
|
380
|
+
proxy = opts[:proxy] || web_config[:proxy]
|
|
381
|
+
fp = nil
|
|
382
|
+
with_headless_browser(proxy: proxy) do |bo|
|
|
383
|
+
fp = fingerprint_page(browser_obj: bo, url: url, selector: opts[:selector], screenshot: true)
|
|
384
|
+
end
|
|
385
|
+
raise "unreachable: #{url}" unless fp
|
|
386
|
+
|
|
387
|
+
prior = observations(category: 'web', target: url, limit: 1).first
|
|
388
|
+
changed = prior && prior.dig(:data, :dom_sha) && prior.dig(:data, :dom_sha) != fp[:dom_sha]
|
|
389
|
+
data = fp.slice(:status, :final_url, :title, :dom_sha, :cert_fp, :cert_not_after, :screenshot).merge(excerpt: fp[:text].to_s[0, 200])
|
|
390
|
+
observe(source: 'transparent_browser', category: :web, target: url, data: data, tags: (['watch'] + Array(opts[:tags])).compact, ttl: ttl)
|
|
391
|
+
{ url: url, changed: changed, prior_sha: prior && prior.dig(:data, :dom_sha), current: data }
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
# Supported Method Parameters::
|
|
395
|
+
# report = PWN::AI::Agent::Extrospection.revalidate_memory(
|
|
396
|
+
# limit: 'optional - max :fact entries to check (default 25)',
|
|
397
|
+
# proxy: 'optional - upstream proxy for TransparentBrowser'
|
|
398
|
+
# )
|
|
399
|
+
#
|
|
400
|
+
# The browser as garbage-collector for PWN::Memory. Walks :fact entries
|
|
401
|
+
# containing a CVE id, version string or URL, runs verify() on each and
|
|
402
|
+
# prefixes stale ones with [UNVERIFIED yyyy-mm-dd] so the injected
|
|
403
|
+
# MEMORY block stops calcifying into confidently-wrong priors.
|
|
404
|
+
# Designed to be scheduled: cron_create(schedule:'0 4 * * 0',
|
|
405
|
+
# ruby:'PWN::AI::Agent::Extrospection.revalidate_memory')
|
|
406
|
+
|
|
407
|
+
public_class_method def self.revalidate_memory(opts = {})
|
|
408
|
+
return { checked: 0, refuted: [], confirmed: [], unknown: [] } unless defined?(PWN::Memory)
|
|
409
|
+
|
|
410
|
+
lim = opts[:limit] || 25
|
|
411
|
+
proxy = opts[:proxy] || web_config[:proxy]
|
|
412
|
+
rx = %r{(CVE-\d{4}-\d{4,}|v?\d+\.\d+\.\d+|https?://\S+)}i
|
|
413
|
+
out = { checked: 0, refuted: [], confirmed: [], unknown: [] }
|
|
414
|
+
|
|
415
|
+
PWN::Memory.load.each do |key, v|
|
|
416
|
+
break if out[:checked] >= lim
|
|
417
|
+
next unless v.is_a?(Hash) && v[:category].to_s == 'fact' && v[:value].to_s =~ rx
|
|
418
|
+
next if v[:value].to_s.start_with?('[UNVERIFIED')
|
|
419
|
+
|
|
420
|
+
res = verify(claim: v[:value].to_s[0, 400], commit: false, proxy: proxy)
|
|
421
|
+
out[:checked] += 1
|
|
422
|
+
out[res[:verdict]] << key
|
|
423
|
+
next unless res[:verdict] == :refuted && res[:confidence] >= 0.6
|
|
424
|
+
|
|
425
|
+
stamp = Time.now.utc.strftime('%Y-%m-%d')
|
|
426
|
+
PWN::Memory.remember(key: key, value: "[UNVERIFIED #{stamp}] #{v[:value]}", category: :fact)
|
|
427
|
+
observe(source: 'extro_verify', category: :web, target: key.to_s, data: "Memory :fact '#{key}' failed re-validation (#{res[:confidence]})", tags: %w[stale memory])
|
|
428
|
+
end
|
|
429
|
+
out
|
|
430
|
+
end
|
|
431
|
+
|
|
241
432
|
# Supported Method Parameters::
|
|
242
433
|
# findings = PWN::AI::Agent::Extrospection.correlate(
|
|
243
434
|
# limit: 'optional - max findings returned (default 10)'
|
|
@@ -304,7 +495,40 @@ module PWN
|
|
|
304
495
|
end
|
|
305
496
|
end
|
|
306
497
|
|
|
307
|
-
# 5)
|
|
498
|
+
# 5) :web drift on a target the same day a Learning failure references it —
|
|
499
|
+
# "your exploit stopped working because the TARGET changed, not your approach"
|
|
500
|
+
if defined?(Learning)
|
|
501
|
+
observations(category: 'web', limit: 50).each do |ob|
|
|
502
|
+
tgt = ob[:target].to_s
|
|
503
|
+
host = safe_host(url: tgt)
|
|
504
|
+
Learning.outcomes(limit: 30, success: false).each do |o|
|
|
505
|
+
next unless o[:task].to_s.include?(host) || o[:details].to_s.include?(host)
|
|
506
|
+
|
|
507
|
+
findings << { kind: :target_web_drift, target: tgt, dom_sha: ob.dig(:data, :dom_sha), task: o[:task], advice: "Target #{host} DOM changed (#{ob[:timestamp]}) around this failure — re-recon before assuming your technique is wrong." }
|
|
508
|
+
end
|
|
509
|
+
end
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
# 6) extro_verify refutations whose claim substring appears in a PWN::Memory :fact -> stale memory
|
|
513
|
+
if defined?(PWN::Memory)
|
|
514
|
+
observations(category: 'web', tag: 'stale', limit: 50).each do |ob|
|
|
515
|
+
findings << { kind: :stale_memory_fact, key: ob[:target], evidence: ob[:data], advice: "PWN::Memory[:#{ob[:target]}] failed browser re-verification — audit or memory_forget it before it poisons future prompts." }
|
|
516
|
+
end
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
# 7) :intel observations whose source anchor is currently probe_web-unreachable -> downgrade
|
|
520
|
+
web = snap[:web] || {}
|
|
521
|
+
web.each do |host, fp|
|
|
522
|
+
next unless fp.is_a?(Hash) && (fp[:reachable] == false || fp[:status].to_i >= 500)
|
|
523
|
+
|
|
524
|
+
observations(category: 'intel', limit: 100).each do |ob|
|
|
525
|
+
next unless ob[:source].to_s.include?(host.to_s) || ob[:data].to_s.include?(host.to_s)
|
|
526
|
+
|
|
527
|
+
findings << { kind: :intel_source_unreachable, source: host, status: fp[:status], intel: ob[:data].to_s[0, 120], advice: "Feed anchor #{host} is currently unreachable (#{fp[:status] || 'down'}) — treat this :intel as stale until probe_web sees it 2xx again." }
|
|
528
|
+
end
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
# 8) raw drift as low-priority findings when nothing else matched
|
|
308
532
|
Array(delta[:added]).first(5).each { |c| findings << { kind: :env_added, detail: c } } if findings.empty?
|
|
309
533
|
Array(delta[:removed]).first(5).each { |c| findings << { kind: :env_removed, detail: c } } if findings.empty?
|
|
310
534
|
|
|
@@ -363,7 +587,8 @@ module PWN
|
|
|
363
587
|
drift_removed: Array(delta[:removed]).length,
|
|
364
588
|
toolchain_bins: (snap[:toolchain] || {}).count { |_, v| !v.to_s.empty? },
|
|
365
589
|
listening_ports: Array(snap.dig(:net, :listening)).length,
|
|
366
|
-
rf_devices: (snap[:rf] || {}).values_at(:rtl_sdr, :hackrf, :flipper, :gqrx_sock).count { |v| rf_present?(val: v) }
|
|
590
|
+
rf_devices: (snap[:rf] || {}).values_at(:rtl_sdr, :hackrf, :flipper, :gqrx_sock).count { |v| rf_present?(val: v) },
|
|
591
|
+
web_anchors: (snap[:web] || {}).count { |_, v| v.is_a?(Hash) && v[:reachable] }
|
|
367
592
|
}
|
|
368
593
|
end
|
|
369
594
|
|
|
@@ -372,7 +597,7 @@ module PWN
|
|
|
372
597
|
# session_id: 'optional - id of the just-completed session (for tagging)'
|
|
373
598
|
# )
|
|
374
599
|
#
|
|
375
|
-
# Called by Learning.
|
|
600
|
+
# Called by Learning.auto_introspect when
|
|
376
601
|
# PWN::Env[:ai][:agent][:auto_extrospect] is truthy. Captures a fresh
|
|
377
602
|
# snapshot and, if drift is non-trivial, records it as an observation
|
|
378
603
|
# and a PWN::Memory :env fact so the NEXT session's system prompt
|
|
@@ -536,6 +761,202 @@ module PWN
|
|
|
536
761
|
[]
|
|
537
762
|
end
|
|
538
763
|
|
|
764
|
+
# Rendered-DOM fingerprint of a small, config-declared set of canonical
|
|
765
|
+
# web anchors the agent depends on for truth (feed reachability,
|
|
766
|
+
# upstream drift, in-scope target tech fingerprint). OPT-IN — never
|
|
767
|
+
# part of the default auto_extrospect section set because a headless
|
|
768
|
+
# browser is ~1-3s cold vs ~50ms for probe_host.
|
|
769
|
+
private_class_method def self.probe_web
|
|
770
|
+
anchors = web_anchors
|
|
771
|
+
return { anchors: 0 } if anchors.empty?
|
|
772
|
+
|
|
773
|
+
out = {}
|
|
774
|
+
with_headless_browser(proxy: web_config[:proxy]) do |bo|
|
|
775
|
+
anchors.first(web_config[:max_anchors]).each do |u|
|
|
776
|
+
host = safe_host(url: u).tr('.', '_')
|
|
777
|
+
fp = fingerprint_page(browser_obj: bo, url: u, screenshot: web_config[:screenshot])
|
|
778
|
+
out[host.to_sym] = fp ? fp.slice(:status, :final_url, :title, :dom_sha, :server, :generator, :cert_fp, :cert_not_after, :reachable, :screenshot) : { reachable: false }
|
|
779
|
+
end
|
|
780
|
+
end
|
|
781
|
+
out
|
|
782
|
+
rescue StandardError => e
|
|
783
|
+
{ error: "#{e.class}: #{e.message}" }
|
|
784
|
+
end
|
|
785
|
+
|
|
786
|
+
private_class_method def self.web_config
|
|
787
|
+
cfg = (PWN::Env.dig(:ai, :agent, :extrospection, :web) if defined?(PWN::Env) && PWN::Env.is_a?(Hash)) || {}
|
|
788
|
+
{
|
|
789
|
+
anchors: Array(cfg[:anchors] || cfg[:web_anchors]),
|
|
790
|
+
proxy: cfg[:proxy],
|
|
791
|
+
max_anchors: (cfg[:max_anchors] || 8).to_i,
|
|
792
|
+
per_page_timeout: (cfg[:per_page_timeout] || 15).to_i,
|
|
793
|
+
screenshot: cfg.key?(:screenshot) ? !cfg[:screenshot].nil? && cfg[:screenshot] != false : false,
|
|
794
|
+
allow_targets: cfg[:allow_targets] ? true : false
|
|
795
|
+
}
|
|
796
|
+
rescue StandardError
|
|
797
|
+
{ anchors: [], proxy: nil, max_anchors: 8, per_page_timeout: 15, screenshot: false, allow_targets: false }
|
|
798
|
+
end
|
|
799
|
+
|
|
800
|
+
private_class_method def self.web_anchors
|
|
801
|
+
a = web_config[:anchors]
|
|
802
|
+
a = DEFAULT_WEB_ANCHORS.dup if a.empty?
|
|
803
|
+
a += Array(PWN::Env[:targets]) if defined?(PWN::Env) && PWN::Env.is_a?(Hash) && web_config[:allow_targets]
|
|
804
|
+
a.compact.map(&:to_s).reject(&:empty?).uniq
|
|
805
|
+
rescue StandardError
|
|
806
|
+
DEFAULT_WEB_ANCHORS.dup
|
|
807
|
+
end
|
|
808
|
+
|
|
809
|
+
private_class_method def self.safe_host(opts = {})
|
|
810
|
+
URI.parse(opts[:url].to_s).host || opts[:url].to_s
|
|
811
|
+
rescue StandardError
|
|
812
|
+
opts[:url].to_s
|
|
813
|
+
end
|
|
814
|
+
|
|
815
|
+
# One shared headless browser per call — open once, reuse across
|
|
816
|
+
# anchors, always close in ensure. Prefers :headless (firefox) which
|
|
817
|
+
# is lightest; degrades to :rest (RestClient) if watir/selenium is
|
|
818
|
+
# unavailable so probe_web/verify/watch never hard-fail the loop.
|
|
819
|
+
private_class_method def self.with_headless_browser(opts = {})
|
|
820
|
+
require 'pwn/plugins/transparent_browser'
|
|
821
|
+
bo = nil
|
|
822
|
+
begin
|
|
823
|
+
bo = PWN::Plugins::TransparentBrowser.open(browser_type: :headless, proxy: opts[:proxy])
|
|
824
|
+
rescue StandardError
|
|
825
|
+
bo = PWN::Plugins::TransparentBrowser.open(browser_type: :rest, proxy: opts[:proxy])
|
|
826
|
+
end
|
|
827
|
+
yield bo
|
|
828
|
+
ensure
|
|
829
|
+
begin
|
|
830
|
+
PWN::Plugins::TransparentBrowser.close(browser_obj: bo) if bo
|
|
831
|
+
rescue StandardError
|
|
832
|
+
nil
|
|
833
|
+
end
|
|
834
|
+
end
|
|
835
|
+
|
|
836
|
+
private_class_method def self.fingerprint_page(opts = {})
|
|
837
|
+
bo = opts[:browser_obj]
|
|
838
|
+
url = opts[:url].to_s
|
|
839
|
+
return nil if bo.nil? || url.empty?
|
|
840
|
+
|
|
841
|
+
tmo = web_config[:per_page_timeout]
|
|
842
|
+
b = bo[:browser]
|
|
843
|
+
if bo[:type] == :rest
|
|
844
|
+
resp = b.get(url) { |r, _rq, _res| r }
|
|
845
|
+
body = resp.body.to_s
|
|
846
|
+
require 'nokogiri'
|
|
847
|
+
doc = Nokogiri::HTML.parse(body)
|
|
848
|
+
text = doc.text.to_s
|
|
849
|
+
cfp, cna = tls_fingerprint(url: url)
|
|
850
|
+
return { url: url, final_url: url, status: resp.code.to_i, title: doc.title.to_s,
|
|
851
|
+
server: resp.headers[:server].to_s, generator: doc.at('meta[name="generator"]')&.[]('content').to_s,
|
|
852
|
+
dom_sha: Digest::SHA256.hexdigest(text)[0, 16], text: text, cert_fp: cfp,
|
|
853
|
+
cert_not_after: cna, reachable: resp.code.to_i < 400 }
|
|
854
|
+
end
|
|
855
|
+
Timeout.timeout(tmo) { b.goto(url) }
|
|
856
|
+
text = extract_text(browser: b, selector: opts[:selector])
|
|
857
|
+
title = safe_attr(browser: b, meth: :title)
|
|
858
|
+
final = safe_attr(browser: b, meth: :url) || url
|
|
859
|
+
gen = safe_meta(browser: b, name: 'generator')
|
|
860
|
+
shot = opts[:screenshot] ? capture_screenshot(browser: b, url: url) : nil
|
|
861
|
+
cfp, cna = tls_fingerprint(url: final)
|
|
862
|
+
{ url: url, final_url: final, status: 200, title: title.to_s, generator: gen.to_s, server: nil,
|
|
863
|
+
dom_sha: Digest::SHA256.hexdigest(text.to_s)[0, 16], text: text.to_s,
|
|
864
|
+
cert_fp: cfp, cert_not_after: cna, screenshot: shot, reachable: true }
|
|
865
|
+
rescue StandardError => e
|
|
866
|
+
{ url: url, reachable: false, status: 0, error: "#{e.class}: #{e.message.to_s[0, 120]}", text: '' }
|
|
867
|
+
end
|
|
868
|
+
|
|
869
|
+
private_class_method def self.extract_text(opts = {})
|
|
870
|
+
b = opts[:browser]
|
|
871
|
+
sel = opts[:selector]
|
|
872
|
+
return b.element(css: sel).text if sel && b.element(css: sel).exists?
|
|
873
|
+
|
|
874
|
+
b.text
|
|
875
|
+
rescue StandardError
|
|
876
|
+
b.respond_to?(:text) ? b.text.to_s : ''
|
|
877
|
+
end
|
|
878
|
+
|
|
879
|
+
private_class_method def self.safe_attr(opts = {})
|
|
880
|
+
opts[:browser].public_send(opts[:meth])
|
|
881
|
+
rescue StandardError
|
|
882
|
+
nil
|
|
883
|
+
end
|
|
884
|
+
|
|
885
|
+
private_class_method def self.safe_meta(opts = {})
|
|
886
|
+
el = opts[:browser].element(css: "meta[name=\"#{opts[:name]}\"]")
|
|
887
|
+
el.exists? ? el.attribute_value('content') : ''
|
|
888
|
+
rescue StandardError
|
|
889
|
+
''
|
|
890
|
+
end
|
|
891
|
+
|
|
892
|
+
private_class_method def self.capture_screenshot(opts = {})
|
|
893
|
+
FileUtils.mkdir_p(WEB_SHOT_DIR)
|
|
894
|
+
host = safe_host(url: opts[:url]).gsub(/[^\w.-]/, '_')
|
|
895
|
+
path = File.join(WEB_SHOT_DIR, "#{host}.png")
|
|
896
|
+
opts[:browser].screenshot.save(path)
|
|
897
|
+
path
|
|
898
|
+
rescue StandardError
|
|
899
|
+
nil
|
|
900
|
+
end
|
|
901
|
+
|
|
902
|
+
private_class_method def self.tls_fingerprint(opts = {})
|
|
903
|
+
u = URI.parse(opts[:url].to_s)
|
|
904
|
+
return [nil, nil] unless u.scheme == 'https'
|
|
905
|
+
|
|
906
|
+
require 'openssl'
|
|
907
|
+
ssl = tcp = nil
|
|
908
|
+
tcp = TCPSocket.new(u.host, u.port || 443)
|
|
909
|
+
ctx = OpenSSL::SSL::SSLContext.new
|
|
910
|
+
ctx.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
|
911
|
+
ssl = OpenSSL::SSL::SSLSocket.new(tcp, ctx)
|
|
912
|
+
ssl.hostname = u.host
|
|
913
|
+
ssl.connect
|
|
914
|
+
cert = ssl.peer_cert
|
|
915
|
+
[Digest::SHA256.hexdigest(cert.to_der)[0, 16], cert.not_after.utc.iso8601]
|
|
916
|
+
rescue StandardError
|
|
917
|
+
[nil, nil]
|
|
918
|
+
ensure
|
|
919
|
+
ssl&.close
|
|
920
|
+
tcp&.close
|
|
921
|
+
end
|
|
922
|
+
|
|
923
|
+
private_class_method def self.fuzzy_overlap(opts = {})
|
|
924
|
+
needle = opts[:needle].to_s.downcase.scan(/[a-z0-9]{3,}/).uniq
|
|
925
|
+
hay = opts[:haystack].to_s.downcase
|
|
926
|
+
return 0.0 if needle.empty?
|
|
927
|
+
|
|
928
|
+
hit = needle.count { |w| hay.include?(w) }
|
|
929
|
+
hit.to_f / needle.length
|
|
930
|
+
end
|
|
931
|
+
|
|
932
|
+
private_class_method def self.detect_claim_kind(opts = {})
|
|
933
|
+
c = opts[:claim].to_s
|
|
934
|
+
return :doc if opts[:url] || c =~ URI::DEFAULT_PARSER.make_regexp(%w[http https])
|
|
935
|
+
return :cve if c =~ /CVE-\d{4}-\d{4,}/i
|
|
936
|
+
return :version if c =~ /\bv?\d+\.\d+(?:\.\d+)?\b/
|
|
937
|
+
|
|
938
|
+
:generic
|
|
939
|
+
end
|
|
940
|
+
|
|
941
|
+
private_class_method def self.commit_verdict(opts = {})
|
|
942
|
+
claim = opts[:claim]
|
|
943
|
+
ev = Array(opts[:evidence]).first
|
|
944
|
+
case opts[:verdict]
|
|
945
|
+
when :refuted
|
|
946
|
+
Mistakes.record(tool: 'assumption', error: "REFUTED (extro_verify #{opts[:kind]}, conf=#{opts[:confidence].round(2)}): #{claim}", args: ev&.dig(:final_url), source: :model) if defined?(Mistakes)
|
|
947
|
+
observe(source: 'extro_verify', category: :web, target: ev&.dig(:final_url), data: "REFUTED: #{claim}", tags: %w[verify refuted], ttl: 30 * 24 * 3600)
|
|
948
|
+
:mistakes_record
|
|
949
|
+
when :confirmed
|
|
950
|
+
observe(source: 'extro_verify', category: :intel, target: ev&.dig(:final_url), data: claim, tags: %w[verify confirmed], ttl: 30 * 24 * 3600)
|
|
951
|
+
:extro_observe
|
|
952
|
+
else
|
|
953
|
+
Learning.note_outcome(task: "extro_verify: #{claim[0, 120]}", success: false, details: 'verdict :unknown — needs human review', tags: %w[needs_human extro_verify]) if defined?(Learning) && Learning.respond_to?(:note_outcome)
|
|
954
|
+
:learning_note
|
|
955
|
+
end
|
|
956
|
+
rescue StandardError
|
|
957
|
+
nil
|
|
958
|
+
end
|
|
959
|
+
|
|
539
960
|
private_class_method def self.flatten(opts = {})
|
|
540
961
|
hash = opts[:hash] || {}
|
|
541
962
|
prefix = opts[:prefix].to_s
|
|
@@ -662,12 +1083,20 @@ module PWN
|
|
|
662
1083
|
PWN::AI::Agent::Extrospection.correlate # introspection x extrospection findings
|
|
663
1084
|
PWN::AI::Agent::Extrospection.to_context # injected by PromptBuilder
|
|
664
1085
|
PWN::AI::Agent::Extrospection.stats
|
|
665
|
-
PWN::AI::Agent::Extrospection.auto_extrospect(session_id: sid) # called from Learning.
|
|
1086
|
+
PWN::AI::Agent::Extrospection.auto_extrospect(session_id: sid) # called from Learning.auto_introspect
|
|
1087
|
+
PWN::AI::Agent::Extrospection.snapshot(sections: %i[web]) # opt-in browser probe of web_anchors
|
|
1088
|
+
PWN::AI::Agent::Extrospection.watch(url: 'https://target/api/version')
|
|
1089
|
+
PWN::AI::Agent::Extrospection.verify(claim: 'CVE-2026-12345 affects OpenSSL 3.2.1')
|
|
1090
|
+
PWN::AI::Agent::Extrospection.revalidate_memory # cron: GC stale PWN::Memory :fact entries
|
|
666
1091
|
PWN::AI::Agent::Extrospection.reset
|
|
667
1092
|
|
|
668
1093
|
Enable end-of-run auto-extrospection with:
|
|
669
1094
|
PWN::Env[:ai][:agent][:auto_extrospect] = true
|
|
670
1095
|
|
|
1096
|
+
Configure browser-backed :web probe / verify / watch with:
|
|
1097
|
+
PWN::Env[:ai][:agent][:extrospection][:web] =
|
|
1098
|
+
{ anchors: [...], proxy: 'tor', max_anchors: 8, per_page_timeout: 15, screenshot: false, allow_targets: false }
|
|
1099
|
+
|
|
671
1100
|
#{self}.authors
|
|
672
1101
|
USAGE
|
|
673
1102
|
end
|
data/lib/pwn/ai/agent/gqrx.rb
CHANGED
|
@@ -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::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::Agent::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::Agent::
|
|
23
|
+
PWN::AI::Agent::Reflect.on(
|
|
24
24
|
system_role_content: system_role_content,
|
|
25
25
|
request: request,
|
|
26
26
|
suppress_pii_warning: true
|
|
@@ -16,7 +16,7 @@ module PWN
|
|
|
16
16
|
#
|
|
17
17
|
# Data flows:
|
|
18
18
|
# Loop.run --(tool telemetry)--> Metrics.record
|
|
19
|
-
# Loop.run --(final answer)----> Learning.
|
|
19
|
+
# Loop.run --(final answer)----> Learning.auto_introspect (opt-in)
|
|
20
20
|
# model --(tool calls)------> learning_note_outcome / _distill_skill
|
|
21
21
|
# PromptBuilder <----------------- Learning.to_context + Metrics.to_context
|
|
22
22
|
#
|
|
@@ -164,9 +164,9 @@ module PWN
|
|
|
164
164
|
# dry_run: 'optional - when true, do not write to Memory/Skills (default false)'
|
|
165
165
|
# )
|
|
166
166
|
#
|
|
167
|
-
# Uses PWN::AI::Agent::
|
|
167
|
+
# Uses PWN::AI::Agent::Reflect (when available) to LLM-summarise the
|
|
168
168
|
# session into structured lessons. Falls back to a heuristic
|
|
169
|
-
# extractor when
|
|
169
|
+
# extractor when module_reflection is disabled so learning never stops.
|
|
170
170
|
|
|
171
171
|
public_class_method def self.reflect(opts = {})
|
|
172
172
|
session_id = opts[:session_id]
|
|
@@ -193,19 +193,19 @@ module PWN
|
|
|
193
193
|
end
|
|
194
194
|
|
|
195
195
|
# Supported Method Parameters::
|
|
196
|
-
# PWN::AI::Agent::Learning.
|
|
196
|
+
# PWN::AI::Agent::Learning.auto_introspect(
|
|
197
197
|
# session_id: 'required - id of the just-completed session',
|
|
198
198
|
# request: 'optional - original user request (for outcome logging)',
|
|
199
199
|
# final: 'optional - final assistant answer (for outcome logging)'
|
|
200
200
|
# )
|
|
201
201
|
#
|
|
202
|
-
# Called by Loop.run when PWN::Env[:ai][:agent][:
|
|
202
|
+
# Called by Loop.run when PWN::Env[:ai][:agent][:auto_introspect] is
|
|
203
203
|
# truthy. Never raises — learning must not break the primary loop.
|
|
204
204
|
|
|
205
|
-
public_class_method def self.
|
|
205
|
+
public_class_method def self.auto_introspect(opts = {})
|
|
206
206
|
session_id = opts[:session_id]
|
|
207
207
|
return unless session_id
|
|
208
|
-
return unless
|
|
208
|
+
return unless auto_introspect_enabled?
|
|
209
209
|
|
|
210
210
|
ok = infer_success(session_id: session_id, final: opts[:final])
|
|
211
211
|
note_outcome(
|
|
@@ -218,7 +218,7 @@ module PWN
|
|
|
218
218
|
reflect(session_id: session_id)
|
|
219
219
|
Extrospection.auto_extrospect(session_id: session_id) if defined?(Extrospection)
|
|
220
220
|
rescue StandardError => e
|
|
221
|
-
warn "[pwn-ai/learning]
|
|
221
|
+
warn "[pwn-ai/learning] auto_introspect swallowed: #{e.class}: #{e.message}"
|
|
222
222
|
nil
|
|
223
223
|
end
|
|
224
224
|
|
|
@@ -301,10 +301,10 @@ module PWN
|
|
|
301
301
|
# privates
|
|
302
302
|
# -------------------------------------------------------------
|
|
303
303
|
|
|
304
|
-
private_class_method def self.
|
|
304
|
+
private_class_method def self.auto_introspect_enabled?
|
|
305
305
|
return false unless defined?(PWN::Env) && PWN::Env.is_a?(Hash)
|
|
306
306
|
|
|
307
|
-
PWN::Env.dig(:ai, :agent, :
|
|
307
|
+
PWN::Env.dig(:ai, :agent, :auto_introspect) ? true : false
|
|
308
308
|
rescue StandardError
|
|
309
309
|
false
|
|
310
310
|
end
|
|
@@ -314,7 +314,7 @@ module PWN
|
|
|
314
314
|
# Derive a success signal stronger than "final answer non-empty":
|
|
315
315
|
# look at the tool-failure ratio inside the just-completed turn AND
|
|
316
316
|
# scan the final text for self-reported failure language. Without
|
|
317
|
-
# this,
|
|
317
|
+
# this, auto_introspect logs ~100 % success and the negative-feedback
|
|
318
318
|
# side of the learning loop never fires.
|
|
319
319
|
private_class_method def self.infer_success(opts = {})
|
|
320
320
|
final = opts[:final].to_s
|
|
@@ -353,11 +353,11 @@ module PWN
|
|
|
353
353
|
|
|
354
354
|
private_class_method def self.introspective_lessons(opts = {})
|
|
355
355
|
transcript = opts[:transcript] || []
|
|
356
|
-
return [] unless defined?(PWN::AI::Agent::
|
|
357
|
-
return [] unless defined?(PWN::Env) && PWN::Env.is_a?(Hash) && PWN::Env.dig(:ai, :
|
|
356
|
+
return [] unless defined?(PWN::AI::Agent::Reflect)
|
|
357
|
+
return [] unless defined?(PWN::Env) && PWN::Env.is_a?(Hash) && PWN::Env.dig(:ai, :module_reflection)
|
|
358
358
|
|
|
359
359
|
req = "Analyse this pwn-ai session transcript and emit up to 5 durable, generalizable lessons (one per line, no numbering, imperative voice) that would make future runs faster or more reliable. Focus on tool selection, error recovery, and target-agnostic technique. Ignore trivia.\n\nTRANSCRIPT:\n#{transcript_text(transcript: transcript)}"
|
|
360
|
-
resp = PWN::AI::Agent::
|
|
360
|
+
resp = PWN::AI::Agent::Reflect.on(request: req, suppress_pii_warning: true)
|
|
361
361
|
resp.to_s.lines.map(&:strip).reject(&:empty?).first(5)
|
|
362
362
|
rescue StandardError
|
|
363
363
|
[]
|
|
@@ -420,7 +420,7 @@ module PWN
|
|
|
420
420
|
PWN::AI::Agent::Learning.note_outcome(task: 'nmap sweep 10.0.0.0/24', success: true, details: '12 hosts up')
|
|
421
421
|
PWN::AI::Agent::Learning.outcomes(limit: 20, success: false)
|
|
422
422
|
PWN::AI::Agent::Learning.reflect(session_id: sid) # LLM or heuristic → PWN::Memory
|
|
423
|
-
PWN::AI::Agent::Learning.
|
|
423
|
+
PWN::AI::Agent::Learning.auto_introspect(session_id: sid, request: req, final: text)
|
|
424
424
|
PWN::AI::Agent::Learning.distill_skill(name: 'quick_recon', session_id: sid)
|
|
425
425
|
PWN::AI::Agent::Learning.consolidate(max_entries: 200) # dedupe + prune Memory
|
|
426
426
|
PWN::AI::Agent::Learning.to_context(limit: 5) # injected by PromptBuilder
|
|
@@ -428,7 +428,7 @@ module PWN
|
|
|
428
428
|
PWN::AI::Agent::Learning.reset
|
|
429
429
|
|
|
430
430
|
Enable end-of-run auto-learning with:
|
|
431
|
-
PWN::Env[:ai][:agent][:
|
|
431
|
+
PWN::Env[:ai][:agent][:auto_introspect] = true
|
|
432
432
|
|
|
433
433
|
#{self}.authors
|
|
434
434
|
USAGE
|
data/lib/pwn/ai/agent/loop.rb
CHANGED
|
@@ -241,7 +241,7 @@ module PWN
|
|
|
241
241
|
if calls.empty?
|
|
242
242
|
text = msg[:content].to_s
|
|
243
243
|
append_session(session_id: session_id, role: 'assistant', content: text)
|
|
244
|
-
Learning.
|
|
244
|
+
Learning.auto_introspect(session_id: session_id, request: request, final: text) if defined?(Learning)
|
|
245
245
|
return text
|
|
246
246
|
end
|
|
247
247
|
|