pwn 0.5.720 → 0.5.721
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/etc/default_skills/pwn/ai/agent/loop/SKILL.md +4 -2
- data/etc/default_skills/pwn/ai/agent/metrics/SKILL.md +1 -0
- data/etc/default_skills/pwn/ai/agent/registry/SKILL.md +3 -0
- data/etc/default_skills/pwn/ai/agent/tool_guard/SKILL.md +2 -0
- data/etc/default_skills/pwn/ai/agent/turn_finalizer/SKILL.md +2 -0
- data/etc/default_skills/pwn/plugins/artifact_registry/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/exploit_dev/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/preflight_checker/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/process_tube/SKILL.md +2 -0
- data/lib/pwn/ai/agent/dispatch.rb +6 -5
- data/lib/pwn/ai/agent/learning.rb +17 -1
- data/lib/pwn/ai/agent/loop.rb +63 -25
- data/lib/pwn/ai/agent/metrics.rb +35 -1
- data/lib/pwn/ai/agent/registry.rb +28 -0
- data/lib/pwn/ai/agent/reward.rb +4 -3
- data/lib/pwn/ai/agent/tool_guard.rb +54 -9
- data/lib/pwn/ai/agent/turn_finalizer.rb +20 -1
- data/lib/pwn/plugins/artifact_registry.rb +32 -2
- data/lib/pwn/plugins/exploit_dev.rb +33 -0
- data/lib/pwn/plugins/findings.rb +9 -1
- data/lib/pwn/plugins/ghidra.rb +20 -2
- data/lib/pwn/plugins/preflight_checker.rb +23 -0
- data/lib/pwn/plugins/process_tube.rb +31 -1
- data/lib/pwn/sessions.rb +4 -0
- data/lib/pwn/version.rb +1 -1
- data/spec/lib/pwn/ai/agent/dispatch_spec.rb +9 -0
- data/spec/lib/pwn/ai/agent/loop_spec.rb +44 -0
- data/spec/lib/pwn/ai/agent/reward_spec.rb +1 -1
- data/spec/lib/pwn/ai/agent/tool_guard_spec.rb +9 -0
- data/spec/lib/pwn/plugins/findings_spec.rb +4 -4
- data/third_party/pwn_rdoc.jsonl +11 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c26adc03f68a0e0275c0eb1a85aa2bb06def95aeb014411b000f52c6a83e3e40
|
|
4
|
+
data.tar.gz: 6a082b81f8f1885de1b3ac42c604f1f21a08d48e7260609ce35e00d1461afa79
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 719d713266b6a3d4200895bb41d4ac5ec7ccfc6d142463cb70085cd6c8d02a514191553b5b12317de45a38abf3d4d1ccb47101f54dc5e588e596a13bc3e77af8
|
|
7
|
+
data.tar.gz: 271d9ff938ac37275ebe9da7d3031305bd09f587e5b6d14f0386fc006bd120b6e684b00aa651c935da7eefbd3a71c298b114ef07ec21b7a9099a75d9f71df647
|
|
@@ -28,11 +28,12 @@ Class methods take `(opts = {})` and read `opts`.
|
|
|
28
28
|
|
|
29
29
|
```ruby
|
|
30
30
|
PWN::AI::Agent::Loop.help
|
|
31
|
-
PWN::AI::Agent::Loop.
|
|
31
|
+
PWN::AI::Agent::Loop.evidence_satisfied(opts)
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
## Public methods
|
|
35
35
|
|
|
36
|
+
- `evidence_satisfied`
|
|
36
37
|
- `debug_on`
|
|
37
38
|
- `catalog_lookup`
|
|
38
39
|
- `world_knowledge`
|
|
@@ -46,6 +47,7 @@ PWN::AI::Agent::Loop.debug_on(opts)
|
|
|
46
47
|
- `help`
|
|
47
48
|
- `catalog_lookup?`
|
|
48
49
|
- `debug_on?`
|
|
50
|
+
- `evidence_satisfied?`
|
|
49
51
|
- `needs_host_work?`
|
|
50
52
|
- `world_knowledge?`
|
|
51
53
|
|
|
@@ -59,5 +61,5 @@ PWN::AI::Agent::Loop.debug_on(opts)
|
|
|
59
61
|
|
|
60
62
|
## Verification
|
|
61
63
|
|
|
62
|
-
`PWN::AI::Agent::Loop.respond_to?(:
|
|
64
|
+
`PWN::AI::Agent::Loop.respond_to?(:evidence_satisfied)` after the
|
|
63
65
|
module is loaded. Read the source for parameter names.
|
|
@@ -61,6 +61,7 @@ PWN::AI::Agent::ToolGuard.present(opts)
|
|
|
61
61
|
- `refuse_copied_persist`
|
|
62
62
|
- `scope_refusal`
|
|
63
63
|
- `policy_decision`
|
|
64
|
+
- `scope_check`
|
|
64
65
|
- `rfc1918`
|
|
65
66
|
- `ip_in_cidr`
|
|
66
67
|
- `command_class`
|
|
@@ -81,6 +82,7 @@ PWN::AI::Agent::ToolGuard.present(opts)
|
|
|
81
82
|
- `refuse_copied_persist?`
|
|
82
83
|
- `reset_timeout_budget!`
|
|
83
84
|
- `rfc1918?`
|
|
85
|
+
- `scope_check!`
|
|
84
86
|
- `shell_bash?`
|
|
85
87
|
|
|
86
88
|
## Source
|
|
@@ -43,9 +43,11 @@ PWN::AI::Agent::TurnFinalizer.enter_user_path(opts)
|
|
|
43
43
|
- `pending`
|
|
44
44
|
- `arbitrate`
|
|
45
45
|
- `evidence_ledger`
|
|
46
|
+
- `evidence_satisfied`
|
|
46
47
|
- `authors`
|
|
47
48
|
- `help`
|
|
48
49
|
- `enter_user_path!`
|
|
50
|
+
- `evidence_satisfied?`
|
|
49
51
|
- `join_all!`
|
|
50
52
|
- `leave_user_path!`
|
|
51
53
|
- `should_defer?`
|
|
@@ -52,7 +52,8 @@ module PWN
|
|
|
52
52
|
args = expand_vault_args(args: args)
|
|
53
53
|
end
|
|
54
54
|
if defined?(Engagement)
|
|
55
|
-
denied =
|
|
55
|
+
denied = ToolGuard.scope_check!(args: args, command: blob) if defined?(ToolGuard) && ToolGuard.respond_to?(:scope_check!)
|
|
56
|
+
denied ||= Engagement.deny_if_out_of_scope(args: args, command: blob)
|
|
56
57
|
return JSON.generate(denied) if denied
|
|
57
58
|
end
|
|
58
59
|
if defined?(ToolGuard) && ToolGuard.respond_to?(:policy_decision)
|
|
@@ -343,8 +344,8 @@ module PWN
|
|
|
343
344
|
return :store if STORE_TOOLS.include?(name)
|
|
344
345
|
|
|
345
346
|
blob = argv_blob(args: opts[:args])
|
|
346
|
-
return :browse if blob.match?(BROWSE_ARGV_RX)
|
|
347
347
|
return :write if blob.match?(WRITE_ARGV_RX)
|
|
348
|
+
return :browse if blob.match?(BROWSE_ARGV_RX)
|
|
348
349
|
return :eval if name == 'pwn_eval'
|
|
349
350
|
|
|
350
351
|
:read
|
|
@@ -381,7 +382,7 @@ module PWN
|
|
|
381
382
|
|
|
382
383
|
private_class_method def self.note_taint(opts = {})
|
|
383
384
|
text = opts[:text].to_s
|
|
384
|
-
grams = text.scan(/.{
|
|
385
|
+
grams = text.scan(/.{24,}/).first(20)
|
|
385
386
|
store = Thread.current[:pwn_taint] ||= []
|
|
386
387
|
grams.each { |g| store << g[0, 64] }
|
|
387
388
|
store.shift while store.length > 200
|
|
@@ -393,10 +394,10 @@ module PWN
|
|
|
393
394
|
return false if mode == 'off'
|
|
394
395
|
|
|
395
396
|
blob = opts[:args].inspect
|
|
396
|
-
return false if blob.length <
|
|
397
|
+
return false if blob.length < 24
|
|
397
398
|
return false if opts[:args].is_a?(Hash) && (opts[:args][:taint_ack] == true || opts[:args]['taint_ack'] == true)
|
|
398
399
|
|
|
399
|
-
hit = Array(Thread.current[:pwn_taint]).any? { |g| g.length >=
|
|
400
|
+
hit = Array(Thread.current[:pwn_taint]).any? { |g| g.length >= 24 && blob.include?(g) }
|
|
400
401
|
return false unless hit
|
|
401
402
|
return false unless blob.match?(/curl |bash -c|sh -c|\|\s*sh\b/i)
|
|
402
403
|
|
|
@@ -196,11 +196,21 @@ module PWN
|
|
|
196
196
|
jmean = total.positive? ? weighted_judge_mean(rows: rows) : nil
|
|
197
197
|
distrust = 0.0
|
|
198
198
|
distrust = Reward.proxy_distrust.to_f.clamp(0.0, 1.0) if defined?(Reward) && Reward.respond_to?(:proxy_distrust)
|
|
199
|
+
orm = rows.select { |r| r[:judge_source].to_s != 'heuristic' && r[:source].to_s != 'heuristic' }
|
|
200
|
+
heur = rows.select { |r| r[:judge_source].to_s == 'heuristic' || r[:source].to_s == 'heuristic' }
|
|
201
|
+
orm_n = orm.length
|
|
202
|
+
heur_n = heur.length
|
|
203
|
+
orm_ok = orm.count { |r| r[:success] == true }
|
|
204
|
+
heur_ok = heur.count { |r| r[:success] == true }
|
|
199
205
|
{
|
|
200
206
|
total_outcomes: total,
|
|
201
207
|
successes: ok,
|
|
202
208
|
failures: total - ok,
|
|
203
209
|
success_rate: raw,
|
|
210
|
+
success_rate_orm: orm_n.positive? ? (orm_ok.to_f / orm_n).round(3) : 0.0,
|
|
211
|
+
success_rate_heur: heur_n.positive? ? (heur_ok.to_f / heur_n).round(3) : 0.0,
|
|
212
|
+
orm_n: orm_n,
|
|
213
|
+
heur_n: heur_n,
|
|
204
214
|
adjusted_success_rate: discount_success_rate(proxy: raw, judge: jmean, distrust: distrust),
|
|
205
215
|
proxy_distrust: distrust,
|
|
206
216
|
skills_known: skills,
|
|
@@ -227,6 +237,12 @@ module PWN
|
|
|
227
237
|
fails = prefer_primary_tasks(rows: outcomes(limit: 200, success: false))
|
|
228
238
|
fails = fails.reject { |r| r[:status].to_s == 'conflicted' }
|
|
229
239
|
fails = fails.reject { |r| r[:verifier_verdict].to_s == 'pass' }
|
|
240
|
+
fails = fails.reject { |r| r[:details].to_s.match?(/\bPASS\b/) && r[:success] == true }
|
|
241
|
+
fails = fails.select do |r|
|
|
242
|
+
v = r[:verdict].to_s
|
|
243
|
+
v == 'wrong' || v == 'refused' || (r[:score].to_f < 0.3 && !r[:details].to_s.match?(/\bPASS\b/)) || r[:success] == false
|
|
244
|
+
end
|
|
245
|
+
fails = fails.reject { |r| r[:success] == true }
|
|
230
246
|
# Do not mirror the same ids under both headings — that doubled the
|
|
231
247
|
# failure signal and made RECENT OUTCOMES == RECENT FAILURES when the
|
|
232
248
|
# last N attempts all failed (the injected block looked "stuck").
|
|
@@ -260,7 +276,7 @@ module PWN
|
|
|
260
276
|
d = s[:proxy_distrust].to_f
|
|
261
277
|
rate = d > 0.05 ? s[:adjusted_success_rate] : s[:success_rate]
|
|
262
278
|
tag = d > 0.05 ? ' adj' : ''
|
|
263
|
-
hdr = "RECENT OUTCOMES (success_rate=#{(rate.to_f * 100).round(1)}%#{tag}#{" judge_mean=#{jm}" if jm} over #{s[:total_outcomes]} attempts)"
|
|
279
|
+
hdr = "RECENT OUTCOMES (success_rate=#{(rate.to_f * 100).round(1)}%#{tag} success=orm:#{(s[:success_rate_orm].to_f * 100).round(1)}%(#{s[:orm_n]}) / heur:#{(s[:success_rate_heur].to_f * 100).round(1)}%(#{s[:heur_n]})#{" judge_mean=#{jm}" if jm} over #{s[:total_outcomes]} attempts)"
|
|
264
280
|
out = "#{hdr}\n#{rows.map(&fmt).join("\n")}\n"
|
|
265
281
|
out += "RECENT FAILURES (learn from these — do not repeat)\n#{fails.map(&fmt).join("\n")}\n" unless fails.empty?
|
|
266
282
|
"#{out}\n"
|
data/lib/pwn/ai/agent/loop.rb
CHANGED
|
@@ -64,6 +64,18 @@ module PWN
|
|
|
64
64
|
unsatisfied incomplete_final empty_final evidence_final
|
|
65
65
|
].freeze
|
|
66
66
|
|
|
67
|
+
public_class_method def self.evidence_satisfied?(opts = {})
|
|
68
|
+
messages = Array(opts[:messages] || opts[:trace])
|
|
69
|
+
text = opts[:text].to_s
|
|
70
|
+
if defined?(TurnFinalizer) && TurnFinalizer.respond_to?(:arbitrate)
|
|
71
|
+
row = TurnFinalizer.arbitrate(request: opts[:request].to_s, messages: messages, paths: [])
|
|
72
|
+
return true if row[:complete] && row[:unmet].empty? && row[:ledger].any? { |_p, v| v[:write] && v[:read] }
|
|
73
|
+
end
|
|
74
|
+
write_or_read_evidenced?(messages: messages) && !text.strip.empty?
|
|
75
|
+
rescue StandardError
|
|
76
|
+
false
|
|
77
|
+
end
|
|
78
|
+
|
|
67
79
|
public_class_method def self.debug_on?(opts = {})
|
|
68
80
|
return true if opts[:debug]
|
|
69
81
|
return true if defined?(PWN::Plugins::Log) && PWN::Plugins::Log.debug_enabled?
|
|
@@ -394,6 +406,7 @@ module PWN
|
|
|
394
406
|
# the original request → force synthesis. English tasks are an advisory
|
|
395
407
|
# compass only — an open verify tail must not block a finished ask.
|
|
396
408
|
# The original request is the completion signal.
|
|
409
|
+
|
|
397
410
|
private_class_method def self.evidence_enough_to_finalize?(opts = {})
|
|
398
411
|
messages = Array(opts[:messages])
|
|
399
412
|
turn_fails = opts[:turn_fails] || {}
|
|
@@ -595,7 +608,7 @@ module PWN
|
|
|
595
608
|
request: request,
|
|
596
609
|
messages: opts[:messages]
|
|
597
610
|
)
|
|
598
|
-
return true if need == :write && !write_verified?(effects: effects)
|
|
611
|
+
return true if need == :write && !write_verified?(effects: effects, request: request, messages: opts[:messages])
|
|
599
612
|
return true if need == :browse && !effects.include?(:browse)
|
|
600
613
|
return true if need == :any && !effects.intersect?(%i[write browse eval])
|
|
601
614
|
|
|
@@ -607,10 +620,27 @@ module PWN
|
|
|
607
620
|
private_class_method def self.write_verified?(opts = {})
|
|
608
621
|
effects = Array(opts[:effects])
|
|
609
622
|
idx = effects.index(:write)
|
|
610
|
-
|
|
623
|
+
if idx
|
|
624
|
+
tail = effects[(idx + 1)..] || []
|
|
625
|
+
return true if tail.intersect?(%i[read eval])
|
|
626
|
+
end
|
|
627
|
+
|
|
628
|
+
saw_write_argv = false
|
|
629
|
+
Array(opts[:messages]).each do |msg|
|
|
630
|
+
next unless msg.is_a?(Hash)
|
|
631
|
+
|
|
632
|
+
if msg[:role].to_s == 'assistant' && defined?(Dispatch)
|
|
633
|
+
Array(msg[:tool_calls]).each do |tc|
|
|
634
|
+
blob = Dispatch.send(:argv_blob, args: tc.dig(:function, :arguments) || tc.dig('function', 'arguments'))
|
|
635
|
+
saw_write_argv = true if blob.to_s.match?(Dispatch::WRITE_ARGV_RX)
|
|
636
|
+
end
|
|
637
|
+
elsif saw_write_argv && msg[:role].to_s == 'tool'
|
|
638
|
+
fx = stamped_effect(content: msg[:content])
|
|
639
|
+
return true if %i[read eval].include?(fx)
|
|
640
|
+
end
|
|
641
|
+
end
|
|
611
642
|
|
|
612
|
-
|
|
613
|
-
tail.intersect?(%i[read eval])
|
|
643
|
+
false
|
|
614
644
|
rescue StandardError
|
|
615
645
|
false
|
|
616
646
|
end
|
|
@@ -875,6 +905,7 @@ module PWN
|
|
|
875
905
|
end
|
|
876
906
|
|
|
877
907
|
private_class_method def self.may_finalize?(opts = {})
|
|
908
|
+
return true if evidence_satisfied?(messages: opts[:messages], text: opts[:text], request: opts[:request])
|
|
878
909
|
return false if incomplete_final?(text: opts[:text], last_iter: false)
|
|
879
910
|
return false if request_unsatisfied?(
|
|
880
911
|
request: opts[:request],
|
|
@@ -1678,6 +1709,7 @@ module PWN
|
|
|
1678
1709
|
}
|
|
1679
1710
|
# Ollama + abliterated / weak chat-templates often ignore tools: and
|
|
1680
1711
|
# answer in prose (or print shell(...) as text). Force native
|
|
1712
|
+
|
|
1681
1713
|
# tool_calls until at least one tool result is already in history;
|
|
1682
1714
|
# after that, auto so the model can emit a real final answer.
|
|
1683
1715
|
# Respect explicit PWN::Env[:ai][:ollama][:tool_choice] override.
|
|
@@ -1705,10 +1737,6 @@ module PWN
|
|
|
1705
1737
|
end
|
|
1706
1738
|
end
|
|
1707
1739
|
|
|
1708
|
-
# 3.1 — sliding-window history compaction for local models.
|
|
1709
|
-
# Keep: system, original user, PLAN assistant (if any), last K tool
|
|
1710
|
-
# pairs (assistant+tool), and the most recent assistant. Stale tool
|
|
1711
|
-
# bodies are truncated to history_tool_max_chars.
|
|
1712
1740
|
private_class_method def self.session_chat_history(opts = {})
|
|
1713
1741
|
return [] unless defined?(PWN::Sessions) && PWN::Sessions.respond_to?(:to_llm_messages)
|
|
1714
1742
|
|
|
@@ -1831,15 +1859,15 @@ module PWN
|
|
|
1831
1859
|
|
|
1832
1860
|
private_class_method def self.spill_tool_body(opts = {})
|
|
1833
1861
|
text = opts[:text].to_s
|
|
1834
|
-
digest = Digest::SHA256.hexdigest(text)
|
|
1835
|
-
dir =
|
|
1862
|
+
digest = Digest::SHA256.hexdigest(text)
|
|
1863
|
+
dir = File.join(Dir.home, '.pwn', 'artifacts', 'transcripts', (Thread.current[:pwn_session_id] || 'default').to_s)
|
|
1836
1864
|
FileUtils.mkdir_p(dir)
|
|
1837
|
-
path = File.join(dir, "#{digest}.txt")
|
|
1865
|
+
path = File.join(dir, "#{digest[0, 12]}.txt")
|
|
1838
1866
|
File.binwrite(path, text) unless File.file?(path)
|
|
1839
1867
|
head = text.byteslice(0, 2_048).to_s
|
|
1840
1868
|
tail = text.bytesize > 3_072 ? text.byteslice(-1_024, 1_024).to_s : ''
|
|
1841
1869
|
mid = tail.empty? ? '' : "\n...\n#{tail}"
|
|
1842
|
-
"#{head}#{mid}\n[compacted path=#{path} sha256=#{digest} bytes=#{text.bytesize}]"
|
|
1870
|
+
"#{head}#{mid}\n[compacted path=#{path} sha256=#{digest} bytes=#{text.bytesize} ref=#{path}]"
|
|
1843
1871
|
rescue StandardError
|
|
1844
1872
|
"[compacted bytes=#{opts[:text].to_s.bytesize}]"
|
|
1845
1873
|
end
|
|
@@ -3002,17 +3030,21 @@ module PWN
|
|
|
3002
3030
|
text: text
|
|
3003
3031
|
)
|
|
3004
3032
|
turn_fails['unsatisfied'] += 1
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3033
|
+
if turn_fails['unsatisfied'] >= 2 && evidence_satisfied?(request: request, messages: messages, text: text)
|
|
3034
|
+
debug_progress(msg: 'nag cap: evidence_satisfied after 2 bounces')
|
|
3035
|
+
else
|
|
3036
|
+
unmet = completion_unmet(request: request, messages: messages)
|
|
3037
|
+
warn "[pwn-ai/loop] original request not evidenced on iter=#{i} unmet=#{unmet.join(',')}; continuing"
|
|
3038
|
+
debug_progress(msg: "bounce unsatisfied unmet=#{unmet.join(',')} snippet=#{debug_snippet(text: text)}")
|
|
3039
|
+
messages << {
|
|
3040
|
+
role: 'user',
|
|
3041
|
+
content: "[pwn-ai] The original request is not evidenced yet. unmet=#{unmet.join(',')} " \
|
|
3042
|
+
'Keep calling CORE_TOOLS (shell, pwn_eval) until that request is ' \
|
|
3043
|
+
'done or a tool returned failure evidence. pwn-ai does not decide ' \
|
|
3044
|
+
'authorization. Do not declare completion from a listing or a refusal.'
|
|
3045
|
+
}
|
|
3046
|
+
next
|
|
3047
|
+
end
|
|
3016
3048
|
end
|
|
3017
3049
|
debug_progress(msg: "final accepted chars=#{text.to_s.length}")
|
|
3018
3050
|
quiet_debug_tui!(reason: 'final')
|
|
@@ -3163,8 +3195,6 @@ module PWN
|
|
|
3163
3195
|
"AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
|
|
3164
3196
|
end
|
|
3165
3197
|
|
|
3166
|
-
# Display Usage for this Module
|
|
3167
|
-
|
|
3168
3198
|
public_class_method def self.help
|
|
3169
3199
|
puts "USAGE:
|
|
3170
3200
|
# Run debug on and return its result
|
|
@@ -3172,6 +3202,14 @@ module PWN
|
|
|
3172
3202
|
debug: 'optional - debug value consumed by #debug_on?'
|
|
3173
3203
|
)
|
|
3174
3204
|
|
|
3205
|
+
# True when write-then-readback evidence satisfies the original request.
|
|
3206
|
+
#{self}.evidence_satisfied?(
|
|
3207
|
+
messages: 'optional - Array of role/content hashes',
|
|
3208
|
+
trace: 'optional - alias for messages',
|
|
3209
|
+
text: 'optional - final answer text',
|
|
3210
|
+
request: 'optional - original request'
|
|
3211
|
+
)
|
|
3212
|
+
|
|
3175
3213
|
# True only when the ask needs a live host/file/browser effect. World-knowledge
|
|
3176
3214
|
#{self}.catalog_lookup?(
|
|
3177
3215
|
request: 'required - request value consumed by #catalog_lookup?'
|
data/lib/pwn/ai/agent/metrics.rb
CHANGED
|
@@ -99,9 +99,32 @@ module PWN
|
|
|
99
99
|
bump(bucket: e, success: success, duration: duration, error: error)
|
|
100
100
|
end
|
|
101
101
|
save(metrics: metrics)
|
|
102
|
+
append_jsonl(opts.merge(name: name, success: success, duration: duration, error: error, engine: engine))
|
|
102
103
|
t
|
|
103
104
|
end
|
|
104
105
|
|
|
106
|
+
public_class_method def self.append_jsonl(opts = {})
|
|
107
|
+
path = File.join(Dir.home, '.pwn', 'logs', 'tool_metrics.jsonl')
|
|
108
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
109
|
+
row = {
|
|
110
|
+
ts: Time.now.utc.iso8601,
|
|
111
|
+
session: Thread.current[:pwn_session_id],
|
|
112
|
+
tool: opts[:name],
|
|
113
|
+
latency_ms: (opts[:duration].to_f * 1000).round,
|
|
114
|
+
timeout_used: opts[:timeout],
|
|
115
|
+
outcome: opts[:success] ? 'ok' : 'err',
|
|
116
|
+
error_class: opts[:error].to_s.split(':').first,
|
|
117
|
+
bytes_out: opts[:bytes_out].to_i
|
|
118
|
+
}
|
|
119
|
+
File.open(path, 'a') do |f|
|
|
120
|
+
f.flock(File::LOCK_EX)
|
|
121
|
+
f.puts(JSON.generate(row))
|
|
122
|
+
end
|
|
123
|
+
row
|
|
124
|
+
rescue StandardError
|
|
125
|
+
nil
|
|
126
|
+
end
|
|
127
|
+
|
|
105
128
|
# Supported Method Parameters::
|
|
106
129
|
# rows = PWN::AI::Agent::Metrics.summary(
|
|
107
130
|
# limit: 'optional - cap number of tools returned (default 25)',
|
|
@@ -664,11 +687,22 @@ module PWN
|
|
|
664
687
|
#{self}.record(
|
|
665
688
|
name: 'required - tool name that was dispatched',
|
|
666
689
|
success: 'required - Boolean, did the handler complete without error',
|
|
667
|
-
duration: 'optional - Float seconds the dispatch took',
|
|
690
|
+
duration: 'optional - Float seconds the dispatch took (wall time)',
|
|
668
691
|
error: 'optional - String error message when success is false',
|
|
669
692
|
engine: 'optional - Symbol/String AI engine that chose this tool (segments telemetry)'
|
|
670
693
|
)
|
|
671
694
|
|
|
695
|
+
# Append one JSONL telemetry row under ~/.pwn/logs/tool_metrics.jsonl.
|
|
696
|
+
#{self}.append_jsonl(
|
|
697
|
+
name: 'required - tool name',
|
|
698
|
+
success: 'optional - Boolean outcome',
|
|
699
|
+
duration: 'optional - Float seconds the dispatch took (wall time)',
|
|
700
|
+
error: 'optional - error string',
|
|
701
|
+
engine: 'optional - engine name',
|
|
702
|
+
timeout: 'optional - timeout used',
|
|
703
|
+
bytes_out: 'optional - output byte count'
|
|
704
|
+
)
|
|
705
|
+
|
|
672
706
|
# Run summary and return its result
|
|
673
707
|
#{self}.summary(
|
|
674
708
|
limit: 'optional - cap number of tools returned (default 25)',
|
|
@@ -113,6 +113,7 @@ module PWN
|
|
|
113
113
|
# )
|
|
114
114
|
|
|
115
115
|
public_class_method def self.definitions(opts = {})
|
|
116
|
+
eager_load!
|
|
116
117
|
enabled = opts[:enabled]
|
|
117
118
|
enabled = enabled.map(&:to_s) if enabled
|
|
118
119
|
pool = @entries.values.select { |e| (enabled.nil? || enabled.include?(e.toolset)) && safe_check(entry: e) }
|
|
@@ -296,6 +297,10 @@ module PWN
|
|
|
296
297
|
# )
|
|
297
298
|
|
|
298
299
|
public_class_method def self.discover(opts = {})
|
|
300
|
+
eager_load!(opts)
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
public_class_method def self.eager_load!(opts = {})
|
|
299
304
|
force = opts[:force] ||= false
|
|
300
305
|
return @entries.keys if @discovered && !force
|
|
301
306
|
|
|
@@ -311,6 +316,19 @@ module PWN
|
|
|
311
316
|
@entries.keys
|
|
312
317
|
end
|
|
313
318
|
|
|
319
|
+
public_class_method def self.selftest(opts = {})
|
|
320
|
+
_dry = opts[:dry_run]
|
|
321
|
+
eager_load!
|
|
322
|
+
tools_dir = File.join(__dir__, 'tools')
|
|
323
|
+
files = Dir[File.join(tools_dir, '*.rb')]
|
|
324
|
+
rows = @entries.map do |name, e|
|
|
325
|
+
schema = e.schema.is_a?(Hash)
|
|
326
|
+
required = Array(e.schema&.dig(:parameters, :required) || e.schema&.dig('parameters', 'required'))
|
|
327
|
+
{ name: name, schema_valid: schema, handler: !e.handler.nil?, required: required }
|
|
328
|
+
end
|
|
329
|
+
{ tools: rows, files: files.length, registered: @entries.length, ok: rows.length >= files.length }
|
|
330
|
+
end
|
|
331
|
+
|
|
314
332
|
private_class_method def self.safe_check(opts = {})
|
|
315
333
|
entry = opts[:entry]
|
|
316
334
|
entry.check.call
|
|
@@ -428,6 +446,16 @@ module PWN
|
|
|
428
446
|
force: 'optional - re-require tool files even if already discovered (default false)'
|
|
429
447
|
)
|
|
430
448
|
|
|
449
|
+
# Eager-load every tools/*.rb so Registry.definitions works in a cold REPL.
|
|
450
|
+
#{self}.eager_load!(
|
|
451
|
+
force: 'optional - re-require tool files even if already discovered (default false)'
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
# Schema/handler contract check for every registered tool.
|
|
455
|
+
#{self}.selftest(
|
|
456
|
+
dry_run: 'optional - reserved dry-run flag'
|
|
457
|
+
)
|
|
458
|
+
|
|
431
459
|
# Print the AUTHOR(S) string for this module.
|
|
432
460
|
#{self}.authors
|
|
433
461
|
"
|
data/lib/pwn/ai/agent/reward.rb
CHANGED
|
@@ -203,10 +203,11 @@ module PWN
|
|
|
203
203
|
v[:engine] = eng
|
|
204
204
|
v[:task_class] = request.match?(/analy[sz]e|summar|strength|weakness|fitness/i) ? 'analysis' : 'operational'
|
|
205
205
|
if pass
|
|
206
|
-
|
|
207
|
-
v[:score] = [
|
|
206
|
+
score = v[:score].to_f
|
|
207
|
+
v[:score] = [score, 0.6].max
|
|
208
208
|
v[:success] = true if prec
|
|
209
209
|
v[:verifier_verdict] ||= :pass
|
|
210
|
+
v[:verdict] = :solved
|
|
210
211
|
end
|
|
211
212
|
v[:score_components] ||= {
|
|
212
213
|
judge: v[:score].to_f,
|
|
@@ -1099,7 +1100,7 @@ module PWN
|
|
|
1099
1100
|
CHEAP_ORM_TEMP = 0.1
|
|
1100
1101
|
CHEAP_ORM_TRACE_N = 12
|
|
1101
1102
|
ORM_SAMPLE_WEIGHT = 1.0
|
|
1102
|
-
HEURISTIC_SAMPLE_WEIGHT = 0.
|
|
1103
|
+
HEURISTIC_SAMPLE_WEIGHT = 0.45
|
|
1103
1104
|
ERROR_SAMPLE_WEIGHT = 0.15
|
|
1104
1105
|
ENGINE_CHAT_MODS = {
|
|
1105
1106
|
openai: 'PWN::AI::OpenAI',
|
|
@@ -5,6 +5,9 @@ require 'json'
|
|
|
5
5
|
require 'fileutils'
|
|
6
6
|
require 'securerandom'
|
|
7
7
|
require 'time'
|
|
8
|
+
require 'openssl'
|
|
9
|
+
require 'uri'
|
|
10
|
+
require 'base64'
|
|
8
11
|
|
|
9
12
|
module PWN
|
|
10
13
|
module AI
|
|
@@ -41,7 +44,12 @@ module PWN
|
|
|
41
44
|
end
|
|
42
45
|
|
|
43
46
|
public_class_method def self.placeholder?(opts = {})
|
|
44
|
-
|
|
47
|
+
s = opts[:text].to_s.dup
|
|
48
|
+
s.gsub!(/<<[-~]?\s*(['"])(\w+)\1.*?^\2\s*$/m, ' ')
|
|
49
|
+
s.gsub!(/<<[-~]?\s*(\w+).*?^\1\s*$/m, ' ')
|
|
50
|
+
s.gsub!(/'[^']*'/, "''")
|
|
51
|
+
s.gsub!(/"([^"\\]|\\.)*"/, '""')
|
|
52
|
+
PLACEHOLDER_RX.match?(s)
|
|
45
53
|
rescue StandardError
|
|
46
54
|
false
|
|
47
55
|
end
|
|
@@ -60,18 +68,36 @@ module PWN
|
|
|
60
68
|
end
|
|
61
69
|
|
|
62
70
|
public_class_method def self.mint_canary(opts = {})
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
71
|
+
_bytes = opts[:bytes]
|
|
72
|
+
turn = (opts[:turn] || Thread.current[:pwn_loop_iter] || 0).to_i
|
|
73
|
+
sid = (opts[:session_id] || Thread.current[:pwn_session_id] || 'sess').to_s
|
|
74
|
+
key = (Thread.current[:pwn_canary_key] ||= SecureRandom.hex(16))
|
|
75
|
+
tok = OpenSSL::HMAC.hexdigest('SHA256', key, "#{sid}:#{turn}")[0, 16]
|
|
76
|
+
hist = Thread.current[:pwn_canary_hist] ||= []
|
|
77
|
+
hist << tok
|
|
78
|
+
hist.shift while hist.length > 4
|
|
66
79
|
Thread.current[:pwn_canary] = tok
|
|
67
80
|
tok
|
|
68
81
|
end
|
|
69
82
|
|
|
70
83
|
public_class_method def self.canary_leak?(opts = {})
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
84
|
+
text = opts[:text].to_s
|
|
85
|
+
toks = Array(Thread.current[:pwn_canary_hist])
|
|
86
|
+
toks << Thread.current[:pwn_canary].to_s
|
|
87
|
+
toks = toks.reject(&:empty?).uniq
|
|
88
|
+
return false if toks.empty?
|
|
89
|
+
|
|
90
|
+
require 'base64'
|
|
91
|
+
toks.any? do |tok|
|
|
92
|
+
next true if text.include?(tok)
|
|
93
|
+
|
|
94
|
+
b64 = Base64.strict_encode64(tok)
|
|
95
|
+
b64u = Base64.urlsafe_encode64(tok)
|
|
96
|
+
hex = tok.each_byte.map { |b| format('%02x', b) }.join
|
|
97
|
+
enc = URI.encode_www_form_component(tok)
|
|
98
|
+
rot = tok.tr('A-Za-z', 'N-ZA-Mn-za-m')
|
|
99
|
+
text.include?(b64) || text.include?(b64u) || text.include?(hex) || text.include?(enc) || text.include?(rot)
|
|
100
|
+
end
|
|
75
101
|
end
|
|
76
102
|
|
|
77
103
|
public_class_method def self.injection_score(opts = {})
|
|
@@ -475,6 +501,16 @@ module PWN
|
|
|
475
501
|
nil
|
|
476
502
|
end
|
|
477
503
|
|
|
504
|
+
public_class_method def self.scope_check!(opts = {})
|
|
505
|
+
return nil unless defined?(Engagement)
|
|
506
|
+
|
|
507
|
+
Engagement.deny_if_out_of_scope(
|
|
508
|
+
args: opts[:args],
|
|
509
|
+
command: opts[:command] || opts[:args].inspect,
|
|
510
|
+
text: opts[:text]
|
|
511
|
+
)
|
|
512
|
+
end
|
|
513
|
+
|
|
478
514
|
public_class_method def self.rfc1918?(opts = {})
|
|
479
515
|
oct = opts[:ip].to_s.split('.').map(&:to_i)
|
|
480
516
|
return false unless oct.length == 4
|
|
@@ -568,7 +604,9 @@ module PWN
|
|
|
568
604
|
|
|
569
605
|
# Mint a per-thread session canary token stored on Thread.current.
|
|
570
606
|
#{self}.mint_canary(
|
|
571
|
-
bytes: 'optional -
|
|
607
|
+
bytes: 'optional - unused reserved length (HMAC uses 16 hex chars)',
|
|
608
|
+
turn: 'optional - turn number mixed into the HMAC',
|
|
609
|
+
session_id: 'optional - session id mixed into the HMAC'
|
|
572
610
|
)
|
|
573
611
|
|
|
574
612
|
# True when text contains the current session canary.
|
|
@@ -729,6 +767,13 @@ module PWN
|
|
|
729
767
|
args: 'required - Hash of tool arguments'
|
|
730
768
|
)
|
|
731
769
|
|
|
770
|
+
# Block network args outside the active engagement scope.
|
|
771
|
+
#{self}.scope_check!(
|
|
772
|
+
args: 'optional - Hash of tool arguments',
|
|
773
|
+
command: 'optional - command string to scan for hosts',
|
|
774
|
+
text: 'optional - free-form blob to scan'
|
|
775
|
+
)
|
|
776
|
+
|
|
732
777
|
# Print the AUTHOR(S) string for this module.
|
|
733
778
|
#{self}.authors
|
|
734
779
|
"
|
|
@@ -227,6 +227,16 @@ module PWN
|
|
|
227
227
|
ledger
|
|
228
228
|
end
|
|
229
229
|
|
|
230
|
+
public_class_method def self.evidence_satisfied?(opts = {})
|
|
231
|
+
row = arbitrate(
|
|
232
|
+
request: opts[:request],
|
|
233
|
+
messages: opts[:messages] || opts[:trace],
|
|
234
|
+
paths: opts[:paths],
|
|
235
|
+
session_t0: opts[:session_t0]
|
|
236
|
+
)
|
|
237
|
+
row[:complete] == true && Array(row[:unmet]).empty?
|
|
238
|
+
end
|
|
239
|
+
|
|
230
240
|
public_class_method def self.authors
|
|
231
241
|
"AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
|
|
232
242
|
end
|
|
@@ -279,7 +289,16 @@ module PWN
|
|
|
279
289
|
|
|
280
290
|
# Build path → {write,read} ledger from tool messages.
|
|
281
291
|
#{self}.evidence_ledger(
|
|
282
|
-
messages: 'optional - Array of role/content hashes
|
|
292
|
+
messages: 'optional - Array of role/content hashes'
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
# True when write-then-readback evidence satisfies the original request.
|
|
296
|
+
#{self}.evidence_satisfied?(
|
|
297
|
+
request: 'optional - original request text',
|
|
298
|
+
messages: 'optional - Array of role/content hashes',
|
|
299
|
+
trace: 'optional - alias for messages (Array of role/content hashes)',
|
|
300
|
+
paths: 'optional - Array of declared deliverable paths',
|
|
301
|
+
session_t0: 'optional - session start Time'
|
|
283
302
|
)
|
|
284
303
|
|
|
285
304
|
# Print the AUTHOR(S) string for this module.
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require 'fileutils'
|
|
4
4
|
require 'json'
|
|
5
5
|
require 'digest'
|
|
6
|
+
require 'time'
|
|
6
7
|
|
|
7
8
|
module PWN
|
|
8
9
|
module Plugins
|
|
@@ -70,6 +71,26 @@ module PWN
|
|
|
70
71
|
end
|
|
71
72
|
end
|
|
72
73
|
|
|
74
|
+
public_class_method def self.put(opts = {})
|
|
75
|
+
bytes = opts[:bytes]
|
|
76
|
+
src = opts[:path].to_s
|
|
77
|
+
bytes = File.binread(src) if bytes.nil? && File.file?(src)
|
|
78
|
+
raise 'ERROR: bytes or path is required' if bytes.nil?
|
|
79
|
+
|
|
80
|
+
sha = Digest::SHA256.hexdigest(bytes)
|
|
81
|
+
dir = File.join(ROOT, 'sha256', sha[0, 2])
|
|
82
|
+
FileUtils.mkdir_p(dir)
|
|
83
|
+
dest = File.join(dir, sha)
|
|
84
|
+
File.binwrite(dest, bytes) unless File.file?(dest)
|
|
85
|
+
meta = File.join(ROOT, 'manifest.jsonl')
|
|
86
|
+
FileUtils.mkdir_p(ROOT)
|
|
87
|
+
File.open(meta, 'a') do |f|
|
|
88
|
+
f.flock(File::LOCK_EX)
|
|
89
|
+
f.puts(JSON.generate(sha256: sha, size: bytes.bytesize, tool: opts[:tool], session: opts[:session_id], kind: opts[:kind], created_at: Time.now.utc.iso8601, source_path: src, dest: dest))
|
|
90
|
+
end
|
|
91
|
+
{ sha256: sha, path: dest, size: bytes.bytesize }
|
|
92
|
+
end
|
|
93
|
+
|
|
73
94
|
public_class_method def self.authors
|
|
74
95
|
"AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
|
|
75
96
|
end
|
|
@@ -81,14 +102,14 @@ module PWN
|
|
|
81
102
|
|
|
82
103
|
# Run register and return its result
|
|
83
104
|
#{self}.register(
|
|
84
|
-
session_id: 'optional - session id
|
|
105
|
+
session_id: 'optional - pwn-ai session id that produced this artifact',
|
|
85
106
|
path: 'required - filesystem path to read or write',
|
|
86
107
|
kind: 'optional - kind value consumed by #register'
|
|
87
108
|
)
|
|
88
109
|
|
|
89
110
|
# Run list and return its result
|
|
90
111
|
#{self}.list(
|
|
91
|
-
session_id: 'optional - session id
|
|
112
|
+
session_id: 'optional - pwn-ai session id whose artifacts to list'
|
|
92
113
|
)
|
|
93
114
|
|
|
94
115
|
# Read an artifact file and return sha256 plus a body cap.
|
|
@@ -104,6 +125,15 @@ module PWN
|
|
|
104
125
|
mode: 'optional - text, hex, or base64 (defaults to text)'
|
|
105
126
|
)
|
|
106
127
|
|
|
128
|
+
# Store bytes under artifacts/sha256/<h2>/<hash> and append manifest.jsonl.
|
|
129
|
+
#{self}.put(
|
|
130
|
+
bytes: 'optional - raw bytes to store',
|
|
131
|
+
path: 'optional - filesystem path to read when bytes is omitted',
|
|
132
|
+
tool: 'optional - tool name for provenance',
|
|
133
|
+
session_id: 'optional - pwn-ai session id for provenance',
|
|
134
|
+
kind: 'optional - artifact kind'
|
|
135
|
+
)
|
|
136
|
+
|
|
107
137
|
# Print the AUTHOR(S) string for this module.
|
|
108
138
|
#{self}.authors
|
|
109
139
|
"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'open3'
|
|
4
|
+
require 'fileutils'
|
|
4
5
|
|
|
5
6
|
module PWN
|
|
6
7
|
module Plugins
|
|
@@ -171,6 +172,31 @@ module PWN
|
|
|
171
172
|
end
|
|
172
173
|
end
|
|
173
174
|
|
|
175
|
+
public_class_method def self.scaffold(opts = {})
|
|
176
|
+
path = (opts[:path] || opts[:bin]).to_s
|
|
177
|
+
raise 'ERROR: path is required' if path.empty?
|
|
178
|
+
|
|
179
|
+
triage = {}
|
|
180
|
+
triage = PWN::Plugins::BinaryParser.triage(path: path) if defined?(PWN::Plugins::BinaryParser)
|
|
181
|
+
gadgets = []
|
|
182
|
+
gadgets = Array(self.gadgets(path: path)[:gadgets]) if File.file?(path)
|
|
183
|
+
body = <<~RB
|
|
184
|
+
# frozen_string_literal: true
|
|
185
|
+
# scaffold for #{path}
|
|
186
|
+
# arch=#{triage[:arch]} pie=#{triage.dig(:protections, :pie)} canary=#{triage.dig(:protections, :canary)}
|
|
187
|
+
require 'pwn'
|
|
188
|
+
bin = #{path.inspect}
|
|
189
|
+
cyclic = PWN::Plugins::ExploitDev.cyclic(length: 200)
|
|
190
|
+
io = PWN::Plugins::ExploitDev.io(cmd: bin)
|
|
191
|
+
PWN::Plugins::ProcessTube.write_line(id: io[:id], line: cyclic)
|
|
192
|
+
RB
|
|
193
|
+
out = opts[:out].to_s
|
|
194
|
+
out = File.join(Dir.home, '.pwn', 'artifacts', 'scaffolds', "#{File.basename(path)}.rb") if out.empty?
|
|
195
|
+
FileUtils.mkdir_p(File.dirname(out))
|
|
196
|
+
File.write(out, body)
|
|
197
|
+
{ path: out, triage: triage, gadgets: gadgets.first(8) }
|
|
198
|
+
end
|
|
199
|
+
|
|
174
200
|
public_class_method def self.authors
|
|
175
201
|
"AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
|
|
176
202
|
end
|
|
@@ -294,6 +320,13 @@ module PWN
|
|
|
294
320
|
port: 'optional - remote TCP port'
|
|
295
321
|
)
|
|
296
322
|
|
|
323
|
+
# Emit a ruby exploit scaffold bound to triage output.
|
|
324
|
+
#{self}.scaffold(
|
|
325
|
+
path: 'required - binary path to scaffold against',
|
|
326
|
+
bin: 'optional - alias for path',
|
|
327
|
+
out: 'optional - output .rb path (defaults under ~/.pwn/artifacts/scaffolds)'
|
|
328
|
+
)
|
|
329
|
+
|
|
297
330
|
# Run gadgets and return its result
|
|
298
331
|
#{self}.gadgets(
|
|
299
332
|
path: 'required - filesystem path of the binary to search for gadgets'
|
data/lib/pwn/plugins/findings.rb
CHANGED
|
@@ -23,6 +23,9 @@ module PWN
|
|
|
23
23
|
arts << opts[:poc].to_s unless opts[:poc].to_s.empty?
|
|
24
24
|
raise 'ERROR: poc_artifacts are required' if arts.empty?
|
|
25
25
|
|
|
26
|
+
ev = opts[:evidence].to_s
|
|
27
|
+
raise 'ERROR: evidence must be at least 40 characters citing the PoC' if ev.length < 40
|
|
28
|
+
|
|
26
29
|
row = {
|
|
27
30
|
id: SecureRandom.hex(6),
|
|
28
31
|
title: title,
|
|
@@ -83,7 +86,12 @@ module PWN
|
|
|
83
86
|
parent_id = opts[:parent_id].to_s
|
|
84
87
|
raise 'ERROR: parent_id is required' if parent_id.empty?
|
|
85
88
|
|
|
86
|
-
child = record(
|
|
89
|
+
child = record(
|
|
90
|
+
opts.merge(
|
|
91
|
+
chain_parent_id: parent_id,
|
|
92
|
+
evidence: (opts[:evidence].to_s.length >= 40 ? opts[:evidence] : 'Chained finding reuses parent PoC evidence and raises composite impact.')
|
|
93
|
+
)
|
|
94
|
+
)
|
|
87
95
|
ranks = { 'info' => 0, 'low' => 1, 'medium' => 2, 'high' => 3, 'critical' => 4 }
|
|
88
96
|
parent = report.find { |r| r[:id].to_s == parent_id }
|
|
89
97
|
sev = [parent&.[](:severity), child[:severity]].compact.max_by { |s| ranks[s.to_s] || 0 }
|
data/lib/pwn/plugins/ghidra.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require 'open3'
|
|
4
4
|
require 'fileutils'
|
|
5
5
|
require 'json'
|
|
6
|
+
require 'digest'
|
|
6
7
|
|
|
7
8
|
module PWN
|
|
8
9
|
module Plugins
|
|
@@ -31,12 +32,28 @@ module PWN
|
|
|
31
32
|
end
|
|
32
33
|
|
|
33
34
|
public_class_method def self.decompile(opts = {})
|
|
34
|
-
|
|
35
|
+
path = (opts[:bin] || opts[:path]).to_s
|
|
36
|
+
raise 'ERROR: bin is required' if path.empty?
|
|
37
|
+
|
|
38
|
+
sha = Digest::SHA256.file(path).hexdigest if File.file?(path)
|
|
39
|
+
cache = File.join(Dir.home, '.pwn', 'cache', 'decompile', "#{sha}.json") if sha
|
|
40
|
+
return JSON.parse(File.read(cache), symbolize_names: true).merge(cached: true) if cache && File.file?(cache)
|
|
41
|
+
|
|
42
|
+
row = analyze(opts)
|
|
43
|
+
fn = opts[:function].to_s
|
|
44
|
+
row[:function] = fn unless fn.empty?
|
|
45
|
+
FileUtils.mkdir_p(File.dirname(cache)) if cache
|
|
46
|
+
File.write(cache, JSON.generate(row)) if cache
|
|
47
|
+
row
|
|
35
48
|
end
|
|
36
49
|
|
|
37
50
|
private_class_method def self.r2_fallback(opts = {})
|
|
38
51
|
path = opts[:path].to_s
|
|
39
52
|
stdout, stderr, status = Open3.capture3('r2', '-q', '-c', 'aaa;s main;pdg', path)
|
|
53
|
+
if stdout.to_s.strip.empty?
|
|
54
|
+
stdout, stderr, status = Open3.capture3('r2', '-q', '-c', 'aaa;s main;pdc', path)
|
|
55
|
+
return { stdout: stdout, stderr: stderr, exit: status.exitstatus, engine: 'r2-pdc' }
|
|
56
|
+
end
|
|
40
57
|
{ stdout: stdout, stderr: stderr, exit: status.exitstatus, engine: 'r2' }
|
|
41
58
|
rescue StandardError => e
|
|
42
59
|
{ error: e.message, engine: 'r2' }
|
|
@@ -62,7 +79,8 @@ module PWN
|
|
|
62
79
|
#{self}.decompile(
|
|
63
80
|
bin: 'required - filesystem path of the binary',
|
|
64
81
|
path: 'optional - alias for bin',
|
|
65
|
-
project_dir: 'optional - Ghidra project directory'
|
|
82
|
+
project_dir: 'optional - Ghidra project directory',
|
|
83
|
+
function: 'optional - function name to decompile instead of the whole program'
|
|
66
84
|
)
|
|
67
85
|
|
|
68
86
|
# Print the AUTHOR(S) string for this module.
|
|
@@ -67,6 +67,23 @@ module PWN
|
|
|
67
67
|
{ ok: false, name: name, degraded: true, hint: "missing #{name}; install via pwn setup --deps" }
|
|
68
68
|
end
|
|
69
69
|
|
|
70
|
+
CAPABILITIES = {
|
|
71
|
+
web_scan: %w[nuclei burpsuite zaproxy],
|
|
72
|
+
raw_packet: %w[],
|
|
73
|
+
decompile: %w[analyzeHeadless r2],
|
|
74
|
+
debug: %w[gdb]
|
|
75
|
+
}.freeze
|
|
76
|
+
|
|
77
|
+
public_class_method def self.capability_coverage(opts = {})
|
|
78
|
+
want = opts[:capability] || opts[:cap]
|
|
79
|
+
if want
|
|
80
|
+
bins = Array(CAPABILITIES[want.to_sym])
|
|
81
|
+
covered = bins.any? { |b| bin?(name: b) }
|
|
82
|
+
return { capability: want.to_s, covered: covered, bins: bins }
|
|
83
|
+
end
|
|
84
|
+
CAPABILITIES.keys.map { |k| capability_coverage(capability: k) }
|
|
85
|
+
end
|
|
86
|
+
|
|
70
87
|
TASK_BINS = {
|
|
71
88
|
'proxy' => %w[burpsuite zaproxy],
|
|
72
89
|
'vuln-scan' => %w[nuclei openvas],
|
|
@@ -176,6 +193,12 @@ module PWN
|
|
|
176
193
|
name: 'required - binary name to probe'
|
|
177
194
|
)
|
|
178
195
|
|
|
196
|
+
# Report whether a named capability is covered by any healthy plugin binary.
|
|
197
|
+
#{self}.capability_coverage(
|
|
198
|
+
capability: 'optional - capability name such as web_scan',
|
|
199
|
+
cap: 'optional - alias for capability'
|
|
200
|
+
)
|
|
201
|
+
|
|
179
202
|
# First healthy binary for a task (proxy, vuln-scan, re).
|
|
180
203
|
#{self}.pick(
|
|
181
204
|
task: 'required - capability name such as proxy or vuln-scan'
|
|
@@ -21,7 +21,7 @@ module PWN
|
|
|
21
21
|
argv = cmd.is_a?(Array) ? cmd.map(&:to_s) : ['bash', '-lc', cmd.to_s]
|
|
22
22
|
r, w, pid = PTY.spawn(*argv)
|
|
23
23
|
id = "tube_#{pid}"
|
|
24
|
-
@tubes[id] = { r: r, w: w, pid: pid, buf: +'' }
|
|
24
|
+
@tubes[id] = { r: r, w: w, pid: pid, buf: +'', started_at: Time.now, last_io: Time.now }
|
|
25
25
|
{ id: id, pid: pid }
|
|
26
26
|
end
|
|
27
27
|
|
|
@@ -105,6 +105,26 @@ module PWN
|
|
|
105
105
|
n
|
|
106
106
|
end
|
|
107
107
|
|
|
108
|
+
public_class_method def self.list(opts = {})
|
|
109
|
+
_idle = opts[:idle]
|
|
110
|
+
now = Time.now
|
|
111
|
+
@tubes.map do |id, t|
|
|
112
|
+
{ id: id, pid: t[:pid], started_at: t[:started_at], last_io: t[:last_io], age_s: (now - (t[:started_at] || now)).to_i }
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
public_class_method def self.kill(opts = {})
|
|
117
|
+
id = opts[:id].to_s
|
|
118
|
+
t = @tubes[id]
|
|
119
|
+
return { ok: false, id: id } unless t
|
|
120
|
+
|
|
121
|
+
Process.kill('TERM', t[:pid]) if t[:pid]
|
|
122
|
+
close(id: id)
|
|
123
|
+
{ ok: true, id: id }
|
|
124
|
+
rescue StandardError => e
|
|
125
|
+
{ ok: false, id: id, error: e.message }
|
|
126
|
+
end
|
|
127
|
+
|
|
108
128
|
public_class_method def self.authors
|
|
109
129
|
"AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
|
|
110
130
|
end
|
|
@@ -165,6 +185,16 @@ module PWN
|
|
|
165
185
|
session_id: 'optional - agent session id for transcript grouping'
|
|
166
186
|
)
|
|
167
187
|
|
|
188
|
+
# List live tubes with age.
|
|
189
|
+
#{self}.list(
|
|
190
|
+
idle: 'optional - unused reserved idle-seconds filter'
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
# SIGTERM then close a tube by id.
|
|
194
|
+
#{self}.kill(
|
|
195
|
+
id: 'required - tube id from spawn or connect'
|
|
196
|
+
)
|
|
197
|
+
|
|
168
198
|
# Print the AUTHOR(S) string for this module.
|
|
169
199
|
#{self}.authors
|
|
170
200
|
"
|
data/lib/pwn/sessions.rb
CHANGED
|
@@ -172,6 +172,10 @@ module PWN
|
|
|
172
172
|
next unless %w[user assistant].include?(row[:role].to_s)
|
|
173
173
|
|
|
174
174
|
body = row[:content].to_s
|
|
175
|
+
if body.include?('[compacted') && body =~ /ref=(\S+)/
|
|
176
|
+
ref = Regexp.last_match(1)
|
|
177
|
+
body = File.read(ref) if File.file?(ref)
|
|
178
|
+
end
|
|
175
179
|
next if body.strip.empty?
|
|
176
180
|
next if !target.empty? && !body.downcase.include?(target)
|
|
177
181
|
|
data/lib/pwn/version.rb
CHANGED
|
@@ -28,6 +28,15 @@ describe PWN::AI::Agent::Dispatch do
|
|
|
28
28
|
expect(described_class.effect(name: 'memory_recall', args: { query: 'docs' })).to eq :recall
|
|
29
29
|
expect(described_class.effect(name: 'memory_remember', args: { key: 'sop' })).to eq :store
|
|
30
30
|
expect(described_class.effect(name: 'learning_note_outcome', args: { task: 'x' })).to eq :store
|
|
31
|
+
expect(
|
|
32
|
+
described_class.effect(
|
|
33
|
+
name: 'shell',
|
|
34
|
+
args: {
|
|
35
|
+
command: "cat >> ~/.pwn/logs/pwn-ai-DEBUG-2026-09-03.md <<'EOF'\n" \
|
|
36
|
+
"ToolGuard, TransparentBrowser, and devtools are strengths.\nEOF"
|
|
37
|
+
}
|
|
38
|
+
)
|
|
39
|
+
).to eq :write
|
|
31
40
|
src = File.read(described_class.method(:effect).source_location.first)
|
|
32
41
|
expect(src).to match(/def self\.effect/)
|
|
33
42
|
end
|
|
@@ -985,6 +985,50 @@ describe PWN::AI::Agent::Loop do # rubocop:disable Metrics/BlockLength
|
|
|
985
985
|
Thread.current[:pwn_loop_deliverables] = nil
|
|
986
986
|
end
|
|
987
987
|
|
|
988
|
+
it 'does not bounce with empty unmet after a redirected write whose body mentions browsers' do
|
|
989
|
+
Dir.mktmpdir do |dir|
|
|
990
|
+
path = File.join(dir, 'pwn-ai-DEBUG-2026-09-03.md')
|
|
991
|
+
File.write(path, "# assessment\n" * 40)
|
|
992
|
+
req = "analyze how well pwn-ai enables pentest. Store this list in #{path}."
|
|
993
|
+
write_cmd = "cat >> #{path} <<'EOF'\nTransparentBrowser and devtools are strengths.\nEOF"
|
|
994
|
+
msgs = [
|
|
995
|
+
{ role: 'user', content: req },
|
|
996
|
+
{
|
|
997
|
+
role: 'assistant',
|
|
998
|
+
tool_calls: [{ function: { name: 'shell', arguments: { command: write_cmd }.to_json } }]
|
|
999
|
+
},
|
|
1000
|
+
{
|
|
1001
|
+
role: 'tool',
|
|
1002
|
+
name: 'shell',
|
|
1003
|
+
content: {
|
|
1004
|
+
success: true,
|
|
1005
|
+
result: { stdout: 'APPENDED', exit: 0 },
|
|
1006
|
+
effect: PWN::AI::Agent::Dispatch.effect(name: 'shell', args: { command: write_cmd })
|
|
1007
|
+
}.to_json
|
|
1008
|
+
},
|
|
1009
|
+
{
|
|
1010
|
+
role: 'assistant',
|
|
1011
|
+
tool_calls: [{ function: { name: 'shell', arguments: { command: "cat #{path}" }.to_json } }]
|
|
1012
|
+
},
|
|
1013
|
+
{
|
|
1014
|
+
role: 'tool',
|
|
1015
|
+
name: 'shell',
|
|
1016
|
+
content: { success: true, result: { stdout: File.read(path), exit: 0 }, effect: 'read' }.to_json
|
|
1017
|
+
}
|
|
1018
|
+
]
|
|
1019
|
+
expect(described_class.send(:completion_unmet, request: req, messages: msgs)).to eq([])
|
|
1020
|
+
expect(described_class.send(:request_unsatisfied?, request: req, messages: msgs)).to eq(false)
|
|
1021
|
+
expect(
|
|
1022
|
+
described_class.send(
|
|
1023
|
+
:may_finalize?,
|
|
1024
|
+
request: req,
|
|
1025
|
+
messages: msgs,
|
|
1026
|
+
text: "Done. The assessment is at #{path}."
|
|
1027
|
+
)
|
|
1028
|
+
).to eq(true)
|
|
1029
|
+
end
|
|
1030
|
+
end
|
|
1031
|
+
|
|
988
1032
|
it 'keeps issue_work unsatisfied until proofs exist, without Crit/High regex' do
|
|
989
1033
|
src = File.read(described_class.method(:run).source_location.first)
|
|
990
1034
|
expect(src).not_to include('Crit/High')
|
|
@@ -411,7 +411,7 @@ describe 'PWN::AI::Agent::Reward vs TUI plan' do
|
|
|
411
411
|
|
|
412
412
|
it 'floors verified PASS analytical answers at 0.6' do
|
|
413
413
|
src = File.read(PWN::AI::Agent::Reward.method(:judge).source_location.first)
|
|
414
|
-
expect(src).to include('[score, 0.
|
|
414
|
+
expect(src).to include('[score, 0.6].max')
|
|
415
415
|
expect(src).to include('\bPASS\b')
|
|
416
416
|
end
|
|
417
417
|
|
|
@@ -36,6 +36,15 @@ describe PWN::AI::Agent::ToolGuard do
|
|
|
36
36
|
it 'rejects a real command' do
|
|
37
37
|
expect(described_class.placeholder?(text: 'uname -r')).to be false
|
|
38
38
|
end
|
|
39
|
+
|
|
40
|
+
it 'does not treat ellipsis inside a heredoc body as a placeholder command' do
|
|
41
|
+
body = "cat <<'EOF'\nThe token ... lives in the document body.\nEOF\n"
|
|
42
|
+
expect(described_class.placeholder?(text: body)).to be false
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it 'still denies a command that is only a placeholder token' do
|
|
46
|
+
expect(described_class.placeholder?(text: ' ... ')).to be true
|
|
47
|
+
end
|
|
39
48
|
end
|
|
40
49
|
|
|
41
50
|
describe '.coerce_args' do
|
|
@@ -7,7 +7,7 @@ describe PWN::Plugins::Findings do
|
|
|
7
7
|
it 'records and reports a finding' do
|
|
8
8
|
Dir.mktmpdir do |dir|
|
|
9
9
|
stub_const('PWN::Plugins::Findings::FILE', File.join(dir, 'findings.jsonl'))
|
|
10
|
-
row = described_class.record(title: 'xss', severity: 'high', host: '127.0.0.1', poc: '/tmp/poc.rb')
|
|
10
|
+
row = described_class.record(title: 'xss', severity: 'high', host: '127.0.0.1', poc: '/tmp/poc.rb', evidence: 'PoC at /tmp/poc.rb reproduces XSS in search with a reflected payload.')
|
|
11
11
|
expect(row[:id]).not_to be_empty
|
|
12
12
|
expect(described_class.report.first[:title]).to eq('xss')
|
|
13
13
|
end
|
|
@@ -19,7 +19,7 @@ describe PWN::Plugins::Findings do
|
|
|
19
19
|
expect { described_class.record(title: 'xss') }.to raise_error(/poc/i)
|
|
20
20
|
poc = File.join(dir, 'poc.rb')
|
|
21
21
|
File.write(poc, 'puts 1')
|
|
22
|
-
row = described_class.record(title: 'xss', poc_artifacts: [poc], severity: 'high')
|
|
22
|
+
row = described_class.record(title: 'xss', poc_artifacts: [poc], severity: 'high', evidence: 'PoC file reproduces reflected XSS in search; see artifact path.')
|
|
23
23
|
expect(row[:poc_artifacts]).to include(poc)
|
|
24
24
|
out = described_class.render(dir_path: dir, report_name: 'findings')
|
|
25
25
|
expect(File.file?(out[:markdown])).to be true
|
|
@@ -33,7 +33,7 @@ describe PWN::Plugins::Findings do
|
|
|
33
33
|
stub_const('PWN::Plugins::Findings::FILE', File.join(dir, 'findings.jsonl'))
|
|
34
34
|
poc = File.join(dir, 'poc.rb')
|
|
35
35
|
File.write(poc, 'puts 1')
|
|
36
|
-
parent = described_class.record(title: 'xss', poc_artifacts: [poc], severity: 'medium')
|
|
36
|
+
parent = described_class.record(title: 'xss', poc_artifacts: [poc], severity: 'medium', evidence: 'PoC file reproduces reflected XSS in search; see artifact path.')
|
|
37
37
|
child = described_class.chain(parent_id: parent[:id], title: 'account-takeover', poc_artifacts: [poc], severity: 'high')
|
|
38
38
|
expect(child[:chain_parent_id]).to eq(parent[:id])
|
|
39
39
|
expect(child[:composite_severity]).to eq('high')
|
|
@@ -46,7 +46,7 @@ describe PWN::Plugins::Findings do
|
|
|
46
46
|
stub_const('PWN::Plugins::Findings::FILE', File.join(dir, 'findings.jsonl'))
|
|
47
47
|
poc = File.join(dir, 'poc.rb')
|
|
48
48
|
File.write(poc, 'puts 1')
|
|
49
|
-
described_class.record(title: 'xss', poc_artifacts: [poc], engagement_id: 'lab')
|
|
49
|
+
described_class.record(title: 'xss', poc_artifacts: [poc], engagement_id: 'lab', evidence: 'PoC file reproduces reflected XSS in search; see artifact path.')
|
|
50
50
|
expect(described_class.evidence_verify(engagement_id: 'lab')[:ok]).to eq(true)
|
|
51
51
|
File.write(poc, 'tampered')
|
|
52
52
|
expect(described_class.evidence_verify(engagement_id: 'lab')[:ok]).to eq(false)
|
data/third_party/pwn_rdoc.jsonl
CHANGED
|
@@ -374,6 +374,7 @@
|
|
|
374
374
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.escalate Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.escalate`: "}]}
|
|
375
375
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.evidence_blob Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.evidence_blob`: "}]}
|
|
376
376
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.evidence_enough_to_finalize? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.evidence_enough_to_finalize?`: "}]}
|
|
377
|
+
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.evidence_satisfied? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.evidence_satisfied?`: "}]}
|
|
377
378
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.evidence_tokens_missing? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.evidence_tokens_missing?`: "}]}
|
|
378
379
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.expose_current_session Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.expose_current_session`: "}]}
|
|
379
380
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.extract_recall_match Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.extract_recall_match`: "}]}
|
|
@@ -439,6 +440,7 @@
|
|
|
439
440
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.write_or_read_evidenced? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.write_or_read_evidenced?`: "}]}
|
|
440
441
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.write_verified? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.write_verified?`: "}]}
|
|
441
442
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Metrics.advantage Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Metrics.advantage`: Supported Method Parameters\n\na = PWN::AI::Agent::Metrics.advantage(name: ‘shell’)\n\nC1 — tool.success_rate − global_rate over the rolling window.\n"}]}
|
|
443
|
+
{"messages":[{"role":"user","content":"PWN::AI::Agent::Metrics.append_jsonl Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Metrics.append_jsonl`: "}]}
|
|
442
444
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Metrics.authors Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Metrics.authors`: Author(s)\n\n0day Inc. <support@0dayinc.com>\n"}]}
|
|
443
445
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Metrics.beta_sample Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Metrics.beta_sample`: "}]}
|
|
444
446
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Metrics.blank_bucket Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Metrics.blank_bucket`: "}]}
|
|
@@ -602,6 +604,7 @@
|
|
|
602
604
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Registry.definitions Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Registry.definitions`: Supported Method Parameters\n\ntools = PWN::AI::Agent::Registry.definitions(\n\nenabled: 'optional - Array of toolset names to include; nil = all whose check passes',\nrelevance: 'optional - user request; when set AND :tool_router is enabled, slim to CORE + top-K keyword matches',\ntop_k: 'optional - keyword-ranked tools to keep beyond CORE (default 10)',\norder: 'optional - preference list forwarded only when this key is present',\npreference: 'optional - alias of :order; same key-present rule'\n\n)\n"}]}
|
|
603
605
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Registry.discover Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Registry.discover`: Supported Method Parameters\n\nnames = PWN::AI::Agent::Registry.discover(\n\nforce: 'optional - re-require tool files even if already discovered (default false)'\n\n)\n"}]}
|
|
604
606
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Registry.domain_pin_names Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Registry.domain_pin_names`: "}]}
|
|
607
|
+
{"messages":[{"role":"user","content":"PWN::AI::Agent::Registry.eager_load! Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Registry.eager_load!`: "}]}
|
|
605
608
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Registry.help Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Registry.help`: "}]}
|
|
606
609
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Registry.lookup Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Registry.lookup`: Supported Method Parameters\n\nentry = PWN::AI::Agent::Registry.lookup(\n\nname: 'required - registered tool name'\n\n)\n"}]}
|
|
607
610
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Registry.metrics_rates Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Registry.metrics_rates`: "}]}
|
|
@@ -610,6 +613,7 @@
|
|
|
610
613
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Registry.register Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Registry.register`: Supported Method Parameters\n\nPWN::AI::Agent::Registry.register(\n\nname: 'required - tool name exposed to the model',\ntoolset: 'required - grouping for enable/disable (terminal, file, pwn, memory…)',\nschema: 'required - OpenAI function schema {name:, description:, parameters:}',\nhandler: 'required - ->(args_hash) { ... } returning a JSON-serialisable object',\ncheck: 'optional - -> { bool } gate; tool only advertised when truthy',\nmax_chars: 'optional - cap on serialised result (default 24_000)'\n\n)\n"}]}
|
|
611
614
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Registry.router_enabled? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Registry.router_enabled?`: "}]}
|
|
612
615
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Registry.safe_check Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Registry.safe_check`: "}]}
|
|
616
|
+
{"messages":[{"role":"user","content":"PWN::AI::Agent::Registry.selftest Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Registry.selftest`: "}]}
|
|
613
617
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Registry.toolsets Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Registry.toolsets`: Supported Method Parameters\n\nnames = PWN::AI::Agent::Registry.toolsets\n"}]}
|
|
614
618
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Result.authors Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Result.authors`: Author(s)\n\n0day Inc. <support@0dayinc.com>\n"}]}
|
|
615
619
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Result.collect Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Result.collect`: "}]}
|
|
@@ -807,6 +811,7 @@
|
|
|
807
811
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::ToolGuard.reset_timeout_budget Usage"},{"role":"assistant","content":"`PWN::AI::Agent::ToolGuard.reset_timeout_budget`: "}]}
|
|
808
812
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::ToolGuard.reset_timeout_budget! Usage"},{"role":"assistant","content":"`PWN::AI::Agent::ToolGuard.reset_timeout_budget!`: "}]}
|
|
809
813
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::ToolGuard.rfc1918? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::ToolGuard.rfc1918?`: "}]}
|
|
814
|
+
{"messages":[{"role":"user","content":"PWN::AI::Agent::ToolGuard.scope_check! Usage"},{"role":"assistant","content":"`PWN::AI::Agent::ToolGuard.scope_check!`: "}]}
|
|
810
815
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::ToolGuard.scope_refusal Usage"},{"role":"assistant","content":"`PWN::AI::Agent::ToolGuard.scope_refusal`: "}]}
|
|
811
816
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::ToolGuard.shell_bash? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::ToolGuard.shell_bash?`: "}]}
|
|
812
817
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::ToolGuard.shell_name Usage"},{"role":"assistant","content":"`PWN::AI::Agent::ToolGuard.shell_name`: "}]}
|
|
@@ -827,6 +832,7 @@
|
|
|
827
832
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::TurnFinalizer.defer Usage"},{"role":"assistant","content":"`PWN::AI::Agent::TurnFinalizer.defer`: Supported Method Parameters\n\nresult = PWN::AI::Agent::TurnFinalizer.defer(\n\nsession_id: 'required - PWN::Sessions id',\nrequest: 'optional - original user request',\nfinal: 'optional - user-visible final answer',\npredicted: 'optional - W3 predicted score',\nplan: 'optional - tangible-task plan',\nts_state: 'optional - TaskSummarizer state'\n\n)\n\nSnapshots the live Policy episode onto this thread, clears it so Loop.maybe_finish_policy is a no-op, and runs Learning.auto_introspect on a daemon thread with the episode re-attached.\n"}]}
|
|
828
833
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::TurnFinalizer.enter_user_path! Usage"},{"role":"assistant","content":"`PWN::AI::Agent::TurnFinalizer.enter_user_path!`: Supported Method Parameters\n\nPWN::AI::Agent::TurnFinalizer.enter_user_path!\n"}]}
|
|
829
834
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::TurnFinalizer.evidence_ledger Usage"},{"role":"assistant","content":"`PWN::AI::Agent::TurnFinalizer.evidence_ledger`: "}]}
|
|
835
|
+
{"messages":[{"role":"user","content":"PWN::AI::Agent::TurnFinalizer.evidence_satisfied? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::TurnFinalizer.evidence_satisfied?`: "}]}
|
|
830
836
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::TurnFinalizer.finalize Usage"},{"role":"assistant","content":"`PWN::AI::Agent::TurnFinalizer.finalize`: Supported Method Parameters\n\nresult = PWN::AI::Agent::TurnFinalizer.finalize(\n\nsession_id:, request:, final:, predicted:, plan:, ts_state:,\nshould: true|false\n\n)\n\nSingle entry used when a caller wants the Hermes split without going through Learning.auto_introspect‘s gate.\n"}]}
|
|
831
837
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::TurnFinalizer.help Usage"},{"role":"assistant","content":"`PWN::AI::Agent::TurnFinalizer.help`: "}]}
|
|
832
838
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::TurnFinalizer.join_all! Usage"},{"role":"assistant","content":"`PWN::AI::Agent::TurnFinalizer.join_all!`: Supported Method Parameters\n\nn = PWN::AI::Agent::TurnFinalizer.join_all!(timeout: 15)\n"}]}
|
|
@@ -1804,6 +1810,7 @@
|
|
|
1804
1810
|
{"messages":[{"role":"user","content":"PWN::Plugins::ArtifactRegistry.get Usage"},{"role":"assistant","content":"`PWN::Plugins::ArtifactRegistry.get`: "}]}
|
|
1805
1811
|
{"messages":[{"role":"user","content":"PWN::Plugins::ArtifactRegistry.help Usage"},{"role":"assistant","content":"`PWN::Plugins::ArtifactRegistry.help`: "}]}
|
|
1806
1812
|
{"messages":[{"role":"user","content":"PWN::Plugins::ArtifactRegistry.list Usage"},{"role":"assistant","content":"`PWN::Plugins::ArtifactRegistry.list`: "}]}
|
|
1813
|
+
{"messages":[{"role":"user","content":"PWN::Plugins::ArtifactRegistry.put Usage"},{"role":"assistant","content":"`PWN::Plugins::ArtifactRegistry.put`: "}]}
|
|
1807
1814
|
{"messages":[{"role":"user","content":"PWN::Plugins::ArtifactRegistry.read_page Usage"},{"role":"assistant","content":"`PWN::Plugins::ArtifactRegistry.read_page`: "}]}
|
|
1808
1815
|
{"messages":[{"role":"user","content":"PWN::Plugins::ArtifactRegistry.register Usage"},{"role":"assistant","content":"`PWN::Plugins::ArtifactRegistry.register`: "}]}
|
|
1809
1816
|
{"messages":[{"role":"user","content":"PWN::Plugins::ArtifactRegistry.required_bins Usage"},{"role":"assistant","content":"`PWN::Plugins::ArtifactRegistry.required_bins`: "}]}
|
|
@@ -2072,6 +2079,7 @@
|
|
|
2072
2079
|
{"messages":[{"role":"user","content":"PWN::Plugins::ExploitDev.p8 Usage"},{"role":"assistant","content":"`PWN::Plugins::ExploitDev.p8`: "}]}
|
|
2073
2080
|
{"messages":[{"role":"user","content":"PWN::Plugins::ExploitDev.pack_int Usage"},{"role":"assistant","content":"`PWN::Plugins::ExploitDev.pack_int`: "}]}
|
|
2074
2081
|
{"messages":[{"role":"user","content":"PWN::Plugins::ExploitDev.required_bins Usage"},{"role":"assistant","content":"`PWN::Plugins::ExploitDev.required_bins`: "}]}
|
|
2082
|
+
{"messages":[{"role":"user","content":"PWN::Plugins::ExploitDev.scaffold Usage"},{"role":"assistant","content":"`PWN::Plugins::ExploitDev.scaffold`: "}]}
|
|
2075
2083
|
{"messages":[{"role":"user","content":"PWN::Plugins::ExploitDev.shellcode Usage"},{"role":"assistant","content":"`PWN::Plugins::ExploitDev.shellcode`: "}]}
|
|
2076
2084
|
{"messages":[{"role":"user","content":"PWN::Plugins::ExploitDev.u16 Usage"},{"role":"assistant","content":"`PWN::Plugins::ExploitDev.u16`: "}]}
|
|
2077
2085
|
{"messages":[{"role":"user","content":"PWN::Plugins::ExploitDev.u32 Usage"},{"role":"assistant","content":"`PWN::Plugins::ExploitDev.u32`: "}]}
|
|
@@ -2515,6 +2523,7 @@
|
|
|
2515
2523
|
{"messages":[{"role":"user","content":"PWN::Plugins::PreflightChecker.authors Usage"},{"role":"assistant","content":"`PWN::Plugins::PreflightChecker.authors`: "}]}
|
|
2516
2524
|
{"messages":[{"role":"user","content":"PWN::Plugins::PreflightChecker.bin? Usage"},{"role":"assistant","content":"`PWN::Plugins::PreflightChecker.bin?`: "}]}
|
|
2517
2525
|
{"messages":[{"role":"user","content":"PWN::Plugins::PreflightChecker.cap_net_raw? Usage"},{"role":"assistant","content":"`PWN::Plugins::PreflightChecker.cap_net_raw?`: "}]}
|
|
2526
|
+
{"messages":[{"role":"user","content":"PWN::Plugins::PreflightChecker.capability_coverage Usage"},{"role":"assistant","content":"`PWN::Plugins::PreflightChecker.capability_coverage`: "}]}
|
|
2518
2527
|
{"messages":[{"role":"user","content":"PWN::Plugins::PreflightChecker.check Usage"},{"role":"assistant","content":"`PWN::Plugins::PreflightChecker.check`: "}]}
|
|
2519
2528
|
{"messages":[{"role":"user","content":"PWN::Plugins::PreflightChecker.help Usage"},{"role":"assistant","content":"`PWN::Plugins::PreflightChecker.help`: "}]}
|
|
2520
2529
|
{"messages":[{"role":"user","content":"PWN::Plugins::PreflightChecker.host_summary Usage"},{"role":"assistant","content":"`PWN::Plugins::PreflightChecker.host_summary`: "}]}
|
|
@@ -2529,6 +2538,8 @@
|
|
|
2529
2538
|
{"messages":[{"role":"user","content":"PWN::Plugins::ProcessTube.connect Usage"},{"role":"assistant","content":"`PWN::Plugins::ProcessTube.connect`: "}]}
|
|
2530
2539
|
{"messages":[{"role":"user","content":"PWN::Plugins::ProcessTube.expect Usage"},{"role":"assistant","content":"`PWN::Plugins::ProcessTube.expect`: "}]}
|
|
2531
2540
|
{"messages":[{"role":"user","content":"PWN::Plugins::ProcessTube.help Usage"},{"role":"assistant","content":"`PWN::Plugins::ProcessTube.help`: "}]}
|
|
2541
|
+
{"messages":[{"role":"user","content":"PWN::Plugins::ProcessTube.kill Usage"},{"role":"assistant","content":"`PWN::Plugins::ProcessTube.kill`: "}]}
|
|
2542
|
+
{"messages":[{"role":"user","content":"PWN::Plugins::ProcessTube.list Usage"},{"role":"assistant","content":"`PWN::Plugins::ProcessTube.list`: "}]}
|
|
2532
2543
|
{"messages":[{"role":"user","content":"PWN::Plugins::ProcessTube.reap_orphans Usage"},{"role":"assistant","content":"`PWN::Plugins::ProcessTube.reap_orphans`: "}]}
|
|
2533
2544
|
{"messages":[{"role":"user","content":"PWN::Plugins::ProcessTube.recvline Usage"},{"role":"assistant","content":"`PWN::Plugins::ProcessTube.recvline`: "}]}
|
|
2534
2545
|
{"messages":[{"role":"user","content":"PWN::Plugins::ProcessTube.recvuntil Usage"},{"role":"assistant","content":"`PWN::Plugins::ProcessTube.recvuntil`: "}]}
|