pwn 0.5.712 → 0.5.714
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/learning/SKILL.md +3 -0
- data/etc/default_skills/pwn/ai/agent/mistakes/SKILL.md +1 -0
- data/etc/default_skills/pwn/ai/agent/swarm/SKILL.md +1 -0
- data/etc/default_skills/pwn/ai/agent/tool_guard/SKILL.md +16 -0
- data/etc/default_skills/pwn/ai/agent/tools/artifacts/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/tools/binary_triage/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/tools/decompile/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/tools/exploitdev/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/tools/finding_record/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/tools/fuzz_campaign/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/tools/intel_lookup/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/tools/job_run/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/tools/pty_session/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/tools/session_export/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/tools/swarm_map/SKILL.md +45 -0
- data/etc/default_skills/pwn/memory_index/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/aflplusplus/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/artifact_registry/SKILL.md +2 -0
- data/etc/default_skills/pwn/plugins/binary_parser/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/detect_os/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/findings/SKILL.md +50 -0
- data/etc/default_skills/pwn/plugins/ghidra/SKILL.md +49 -0
- data/etc/default_skills/pwn/plugins/jobs/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/log/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/nmap_it/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/preflight_checker/SKILL.md +2 -0
- data/etc/default_skills/pwn/plugins/transparent_browser/SKILL.md +2 -0
- data/etc/default_skills/pwn/sessions/SKILL.md +3 -0
- data/etc/default_skills/pwn/setup/references/urls.md +1 -0
- data/etc/intel/files_exploits.csv +2 -0
- data/lib/pwn/ai/agent/dispatch.rb +5 -0
- data/lib/pwn/ai/agent/learning.rb +72 -0
- data/lib/pwn/ai/agent/loop.rb +62 -13
- data/lib/pwn/ai/agent/mistakes.rb +21 -1
- data/lib/pwn/ai/agent/prompt_builder.rb +16 -13
- data/lib/pwn/ai/agent/registry.rb +21 -1
- data/lib/pwn/ai/agent/reward.rb +4 -2
- data/lib/pwn/ai/agent/swarm.rb +26 -0
- data/lib/pwn/ai/agent/tool_guard.rb +201 -1
- data/lib/pwn/ai/agent/tools/artifacts.rb +61 -0
- data/lib/pwn/ai/agent/tools/binary_triage.rb +20 -0
- data/lib/pwn/ai/agent/tools/decompile.rb +27 -0
- data/lib/pwn/ai/agent/tools/exploitdev.rb +45 -0
- data/lib/pwn/ai/agent/tools/finding_record.rb +48 -0
- data/lib/pwn/ai/agent/tools/fuzz_campaign.rb +44 -0
- data/lib/pwn/ai/agent/tools/intel_lookup.rb +24 -0
- data/lib/pwn/ai/agent/tools/job_run.rb +56 -0
- data/lib/pwn/ai/agent/tools/pty_session.rb +73 -0
- data/lib/pwn/ai/agent/tools/session_export.rb +20 -0
- data/lib/pwn/ai/agent/tools/shell.rb +5 -0
- data/lib/pwn/ai/agent/tools/swarm_map.rb +26 -0
- data/lib/pwn/config.rb +10 -1
- data/lib/pwn/memory_index.rb +50 -0
- data/lib/pwn/plugins/aflplusplus.rb +30 -1
- data/lib/pwn/plugins/artifact_registry.rb +43 -0
- data/lib/pwn/plugins/binary_parser.rb +31 -0
- data/lib/pwn/plugins/detect_os.rb +109 -2
- data/lib/pwn/plugins/exploit_db.rb +38 -1
- data/lib/pwn/plugins/findings.rb +105 -0
- data/lib/pwn/plugins/ghidra.rb +75 -0
- data/lib/pwn/plugins/jobs.rb +12 -1
- data/lib/pwn/plugins/log.rb +18 -3
- data/lib/pwn/plugins/nmap_it.rb +29 -0
- data/lib/pwn/plugins/preflight_checker.rb +36 -0
- data/lib/pwn/plugins/transparent_browser.rb +29 -0
- data/lib/pwn/plugins.rb +2 -0
- data/lib/pwn/sessions.rb +92 -1
- data/lib/pwn/setup.rb +41 -15
- data/lib/pwn/version.rb +1 -1
- data/spec/lib/pwn/ai/agent/fixtures/error_class_docker.json +7 -0
- data/spec/lib/pwn/ai/agent/learning_spec.rb +15 -0
- data/spec/lib/pwn/ai/agent/loop_spec.rb +57 -2
- data/spec/lib/pwn/ai/agent/mistakes_spec.rb +21 -0
- data/spec/lib/pwn/ai/agent/registry_spec.rb +11 -0
- data/spec/lib/pwn/ai/agent/reward_spec.rb +14 -0
- data/spec/lib/pwn/ai/agent/swarm_spec.rb +6 -0
- data/spec/lib/pwn/ai/agent/tool_guard_spec.rb +48 -0
- data/spec/lib/pwn/ai/agent/tools/artifacts_spec.rb +16 -0
- data/spec/lib/pwn/ai/agent/tools/binary_triage_spec.rb +14 -0
- data/spec/lib/pwn/ai/agent/tools/decompile_spec.rb +14 -0
- data/spec/lib/pwn/ai/agent/tools/exploitdev_spec.rb +14 -0
- data/spec/lib/pwn/ai/agent/tools/finding_record_spec.rb +15 -0
- data/spec/lib/pwn/ai/agent/tools/fuzz_campaign_spec.rb +14 -0
- data/spec/lib/pwn/ai/agent/tools/intel_lookup_spec.rb +14 -0
- data/spec/lib/pwn/ai/agent/tools/job_run_spec.rb +16 -0
- data/spec/lib/pwn/ai/agent/tools/pty_session_spec.rb +17 -0
- data/spec/lib/pwn/ai/agent/tools/session_export_spec.rb +14 -0
- data/spec/lib/pwn/ai/agent/tools/swarm_map_spec.rb +14 -0
- data/spec/lib/pwn/cron_spec.rb +2 -2
- data/spec/lib/pwn/memory_index_spec.rb +9 -0
- data/spec/lib/pwn/plugins/aflplusplus_spec.rb +9 -0
- data/spec/lib/pwn/plugins/artifact_registry_spec.rb +12 -0
- data/spec/lib/pwn/plugins/detect_os_spec.rb +40 -0
- data/spec/lib/pwn/plugins/exploit_db_spec.rb +12 -0
- data/spec/lib/pwn/plugins/exploit_dev_spec.rb +23 -0
- data/spec/lib/pwn/plugins/findings_spec.rb +30 -0
- data/spec/lib/pwn/plugins/frida_spec.rb +5 -0
- data/spec/lib/pwn/plugins/fuzz_spec.rb +20 -0
- data/spec/lib/pwn/plugins/gdb_spec.rb +15 -0
- data/spec/lib/pwn/plugins/ghidra_spec.rb +14 -0
- data/spec/lib/pwn/plugins/k8s_spec.rb +5 -0
- data/spec/lib/pwn/plugins/log_spec.rb +10 -5
- data/spec/lib/pwn/plugins/nmap_it_spec.rb +4 -0
- data/spec/lib/pwn/plugins/nuclei_spec.rb +6 -0
- data/spec/lib/pwn/plugins/preflight_checker_spec.rb +24 -0
- data/spec/lib/pwn/plugins/process_tube_spec.rb +23 -0
- data/spec/lib/pwn/plugins/semgrep_spec.rb +4 -0
- data/spec/lib/pwn/plugins/sock_spec.rb +7 -0
- data/spec/lib/pwn/plugins/transparent_browser_spec.rb +15 -0
- data/spec/lib/pwn/sessions_spec.rb +53 -0
- data/spec/lib/pwn/setup_spec.rb +32 -0
- data/third_party/pwn_rdoc.jsonl +64 -2
- metadata +42 -3
- data/spec/lib/pwn/req_queue_spec.rb +0 -168
- data/tmp_pwn_critic_skills_flaw.txt +0 -9
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pwn-plugins-ghidra
|
|
3
|
+
description: Drive PWN::Plugins::Ghidra from pwn_eval.
|
|
4
|
+
license: MIT
|
|
5
|
+
allowed-tools: [pwn, pwn_eval]
|
|
6
|
+
metadata:
|
|
7
|
+
bundled: true
|
|
8
|
+
generated: true
|
|
9
|
+
module: PWN::Plugins::Ghidra
|
|
10
|
+
source: pwn/plugins/ghidra.rb
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# PWN::Plugins::Ghidra
|
|
14
|
+
|
|
15
|
+
Headless Ghidra analyzeHeadless wrapper with r2 fallback.
|
|
16
|
+
|
|
17
|
+
## When to use
|
|
18
|
+
|
|
19
|
+
Call `PWN::Plugins::Ghidra` from `pwn_eval` when the task needs this module.
|
|
20
|
+
Do not reimplement it in shell.
|
|
21
|
+
|
|
22
|
+
## Methodologies
|
|
23
|
+
|
|
24
|
+
Generated from `pwn/plugins/ghidra.rb`. Prefer the public class methods below.
|
|
25
|
+
Class methods take `(opts = {})` and read `opts`.
|
|
26
|
+
|
|
27
|
+
## How to call
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
PWN::Plugins::Ghidra.help
|
|
31
|
+
PWN::Plugins::Ghidra.required_bins(opts)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Public methods
|
|
35
|
+
|
|
36
|
+
- `required_bins`
|
|
37
|
+
- `analyze`
|
|
38
|
+
- `decompile`
|
|
39
|
+
- `authors`
|
|
40
|
+
- `help`
|
|
41
|
+
|
|
42
|
+
## Source
|
|
43
|
+
|
|
44
|
+
`pwn/plugins/ghidra.rb`
|
|
45
|
+
|
|
46
|
+
## Verification
|
|
47
|
+
|
|
48
|
+
`PWN::Plugins::Ghidra.respond_to?(:required_bins)` after the
|
|
49
|
+
module is loaded. Read the source for parameter names.
|
|
@@ -46,6 +46,10 @@ module PWN
|
|
|
46
46
|
args = parse_args(raw: raw, entry: entry)
|
|
47
47
|
required = Array(entry.schema&.dig(:parameters, :required))
|
|
48
48
|
args = ToolGuard.coerce_args(args: args, required: required) if defined?(ToolGuard)
|
|
49
|
+
if defined?(ToolGuard) && ToolGuard.respond_to?(:canary_leak?) &&
|
|
50
|
+
ToolGuard.canary_leak?(text: args.inspect)
|
|
51
|
+
return JSON.generate(success: false, error: 'refused: session canary in outbound args')
|
|
52
|
+
end
|
|
49
53
|
if defined?(ToolGuard) && ToolGuard.respond_to?(:refuse_copied_persist?) &&
|
|
50
54
|
ToolGuard.refuse_copied_persist?(name: entry.name, args: args)
|
|
51
55
|
return JSON.generate(
|
|
@@ -54,6 +58,7 @@ module PWN
|
|
|
54
58
|
)
|
|
55
59
|
end
|
|
56
60
|
result = entry.handler.call(args)
|
|
61
|
+
result = ToolGuard.quarantine_output(text: result) if defined?(ToolGuard) && result.is_a?(String) && ToolGuard.respond_to?(:quarantine_output)
|
|
57
62
|
JSON.generate(success: true, result: result, effect: effect(name: entry.name, args: args))
|
|
58
63
|
rescue StandardError => e
|
|
59
64
|
JSON.generate(
|
|
@@ -26,6 +26,7 @@ module PWN
|
|
|
26
26
|
# restarts and is shared by every future session.
|
|
27
27
|
module Learning
|
|
28
28
|
LEARNING_FILE = File.join(Dir.home, '.pwn', 'learning.jsonl')
|
|
29
|
+
LESSONS_FILE = File.join(Dir.home, '.pwn', 'lessons.json')
|
|
29
30
|
FINETUNE_DIR = File.join(Dir.home, '.pwn', 'finetune')
|
|
30
31
|
# P0 — post-answer introspect must not train "stop early" while
|
|
31
32
|
# spending the iteration budget after the final. Soft cap skips
|
|
@@ -1762,6 +1763,61 @@ module PWN
|
|
|
1762
1763
|
{ removed: before - mem.size, remaining: mem.size }
|
|
1763
1764
|
end
|
|
1764
1765
|
|
|
1766
|
+
public_class_method def self.lesson_record(opts = {})
|
|
1767
|
+
text = opts[:text].to_s
|
|
1768
|
+
raise 'ERROR: text is required' if text.empty?
|
|
1769
|
+
|
|
1770
|
+
store = lesson_store
|
|
1771
|
+
id = Digest::SHA256.hexdigest(text)[0, 12]
|
|
1772
|
+
store[id] ||= { id: id, text: text, state: 'candidate', successes: 0, contradictions: 0 }
|
|
1773
|
+
lesson_save(store: store)
|
|
1774
|
+
store[id]
|
|
1775
|
+
end
|
|
1776
|
+
|
|
1777
|
+
public_class_method def self.lesson_observe(opts = {})
|
|
1778
|
+
id = opts[:id].to_s
|
|
1779
|
+
store = lesson_store
|
|
1780
|
+
row = store[id]
|
|
1781
|
+
return nil unless row
|
|
1782
|
+
|
|
1783
|
+
if opts[:success]
|
|
1784
|
+
row[:successes] = row[:successes].to_i + 1
|
|
1785
|
+
row[:state] = 'verified' if row[:successes] >= 2
|
|
1786
|
+
else
|
|
1787
|
+
row[:contradictions] = row[:contradictions].to_i + 1
|
|
1788
|
+
row[:state] = 'demoted' if row[:contradictions] >= 2
|
|
1789
|
+
end
|
|
1790
|
+
store[id] = row
|
|
1791
|
+
lesson_save(store: store)
|
|
1792
|
+
row
|
|
1793
|
+
end
|
|
1794
|
+
|
|
1795
|
+
public_class_method def self.lesson_prompt(opts = {})
|
|
1796
|
+
include_demoted = opts[:include_demoted] ? true : false
|
|
1797
|
+
lesson_store.values.filter_map do |row|
|
|
1798
|
+
next if !include_demoted && row[:state].to_s == 'demoted'
|
|
1799
|
+
|
|
1800
|
+
tag = row[:state].to_s == 'verified' ? '' : '[UNVERIFIED] '
|
|
1801
|
+
"#{tag}#{row[:text]}"
|
|
1802
|
+
end.join("\n")
|
|
1803
|
+
end
|
|
1804
|
+
|
|
1805
|
+
private_class_method def self.lesson_store
|
|
1806
|
+
return {} unless File.file?(LESSONS_FILE)
|
|
1807
|
+
|
|
1808
|
+
raw = JSON.parse(File.read(LESSONS_FILE))
|
|
1809
|
+
raw.each_with_object({}) do |(k, v), acc|
|
|
1810
|
+
acc[k.to_s] = v.transform_keys(&:to_sym)
|
|
1811
|
+
end
|
|
1812
|
+
rescue StandardError
|
|
1813
|
+
{}
|
|
1814
|
+
end
|
|
1815
|
+
|
|
1816
|
+
private_class_method def self.lesson_save(opts = {})
|
|
1817
|
+
FileUtils.mkdir_p(File.dirname(LESSONS_FILE))
|
|
1818
|
+
File.write(LESSONS_FILE, JSON.pretty_generate(opts[:store]))
|
|
1819
|
+
end
|
|
1820
|
+
|
|
1765
1821
|
# Author(s):: 0day Inc. <support@0dayinc.com>
|
|
1766
1822
|
|
|
1767
1823
|
public_class_method def self.authors
|
|
@@ -1909,6 +1965,22 @@ module PWN
|
|
|
1909
1965
|
# One-shot GC of the pre-R1 garbage: drops every PWN::Memory entry
|
|
1910
1966
|
#{self}.purge_noise
|
|
1911
1967
|
|
|
1968
|
+
# Record a lesson as candidate (injected with [UNVERIFIED] until verified).
|
|
1969
|
+
#{self}.lesson_record(
|
|
1970
|
+
text: 'required - lesson text to quarantine'
|
|
1971
|
+
)
|
|
1972
|
+
|
|
1973
|
+
# Count a success or contradiction against a lesson id.
|
|
1974
|
+
#{self}.lesson_observe(
|
|
1975
|
+
id: 'required - lesson id from #lesson_record',
|
|
1976
|
+
success: 'required - true to count a success, false for a contradiction'
|
|
1977
|
+
)
|
|
1978
|
+
|
|
1979
|
+
# Prompt block of non-demoted lessons; candidates prefixed [UNVERIFIED].
|
|
1980
|
+
#{self}.lesson_prompt(
|
|
1981
|
+
include_demoted: 'optional - include demoted lessons (defaults to false)'
|
|
1982
|
+
)
|
|
1983
|
+
|
|
1912
1984
|
# Print the AUTHOR(S) string for this module.
|
|
1913
1985
|
#{self}.authors
|
|
1914
1986
|
"
|
data/lib/pwn/ai/agent/loop.rb
CHANGED
|
@@ -567,7 +567,7 @@ module PWN
|
|
|
567
567
|
end
|
|
568
568
|
|
|
569
569
|
private_class_method def self.stamped_effect(opts = {})
|
|
570
|
-
raw = opts[:content].to_s
|
|
570
|
+
raw = unwrap_untrusted(text: opts[:content].to_s)
|
|
571
571
|
return if raw.empty?
|
|
572
572
|
|
|
573
573
|
parsed = JSON.parse(raw, symbolize_names: true)
|
|
@@ -661,12 +661,20 @@ module PWN
|
|
|
661
661
|
|
|
662
662
|
private_class_method def self.declared_contract_unsatisfied?(opts = {})
|
|
663
663
|
contract = declared_contract(request: opts[:request])
|
|
664
|
+
request = opts[:request].to_s
|
|
664
665
|
files = Array(contract[:paths]) + Array(contract[:proofs])
|
|
665
666
|
return true if files.any? { |path| deliverable_missing?(path: path) }
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
return true if
|
|
669
|
-
|
|
667
|
+
|
|
668
|
+
trace_files = session_files(messages: opts[:messages])
|
|
669
|
+
return true if contract[:issue_work] && Array(contract[:proofs]).empty? && files.empty? && trace_files.empty?
|
|
670
|
+
|
|
671
|
+
return true if declared_skills_missing?(skills: contract[:skills], request: request)
|
|
672
|
+
|
|
673
|
+
blob = evidence_blob(messages: opts[:messages], files: files + trace_files)
|
|
674
|
+
asked_hosts = Array(contract[:hosts]).select { |host| request.downcase.include?(host.to_s.downcase) }
|
|
675
|
+
asked_tech = Array(contract[:techniques]).select { |tech| request.downcase.include?(tech.to_s.downcase) }
|
|
676
|
+
return true if evidence_tokens_missing?(tokens: asked_hosts, blob: blob)
|
|
677
|
+
return true if evidence_tokens_missing?(tokens: asked_tech, blob: blob)
|
|
670
678
|
|
|
671
679
|
false
|
|
672
680
|
rescue StandardError
|
|
@@ -675,6 +683,8 @@ module PWN
|
|
|
675
683
|
|
|
676
684
|
private_class_method def self.declared_skills_missing?(opts = {})
|
|
677
685
|
names = Array(opts[:skills]).map(&:to_s).reject(&:empty?)
|
|
686
|
+
req = opts[:request].to_s.downcase
|
|
687
|
+
names = names.select { |name| req.include?(name.downcase) }
|
|
678
688
|
return false if names.empty?
|
|
679
689
|
return true unless defined?(PWN::Skills) && PWN::Skills.is_a?(Hash)
|
|
680
690
|
|
|
@@ -682,15 +692,41 @@ module PWN
|
|
|
682
692
|
names.any? { |name| !have.include?(name) }
|
|
683
693
|
end
|
|
684
694
|
|
|
685
|
-
private_class_method def self.
|
|
686
|
-
|
|
687
|
-
return false if
|
|
695
|
+
private_class_method def self.evidence_tokens_missing?(opts = {})
|
|
696
|
+
tokens = Array(opts[:tokens]).map(&:to_s).reject(&:empty?)
|
|
697
|
+
return false if tokens.empty?
|
|
698
|
+
|
|
699
|
+
blob = opts[:blob].to_s.downcase
|
|
700
|
+
tokens.any? { |tok| !blob.include?(tok.downcase) }
|
|
701
|
+
end
|
|
702
|
+
|
|
703
|
+
private_class_method def self.evidence_blob(opts = {})
|
|
704
|
+
parts = Array(opts[:messages]).select { |msg| msg.is_a?(Hash) && %w[tool assistant].include?(msg[:role].to_s) }.map do |msg|
|
|
705
|
+
unwrap_untrusted(text: msg[:content].to_s)
|
|
706
|
+
end
|
|
707
|
+
Array(opts[:files]).each do |path|
|
|
708
|
+
parts << File.binread(path) if File.file?(path.to_s)
|
|
709
|
+
rescue StandardError
|
|
710
|
+
nil
|
|
711
|
+
end
|
|
712
|
+
parts.join("\n").downcase
|
|
713
|
+
end
|
|
688
714
|
|
|
689
|
-
|
|
690
|
-
|
|
715
|
+
private_class_method def self.session_files(opts = {})
|
|
716
|
+
blob = Array(opts[:messages]).grep(Hash)
|
|
717
|
+
.map { |msg| unwrap_untrusted(text: msg[:content].to_s) }
|
|
691
718
|
.join("\n")
|
|
692
|
-
|
|
693
|
-
|
|
719
|
+
blob.scan(%r{(/[A-Za-z0-9._/+-]+)}).flatten.uniq.select do |path|
|
|
720
|
+
File.file?(path) && File.size(path).positive?
|
|
721
|
+
rescue StandardError
|
|
722
|
+
false
|
|
723
|
+
end
|
|
724
|
+
end
|
|
725
|
+
|
|
726
|
+
private_class_method def self.unwrap_untrusted(opts = {})
|
|
727
|
+
text = opts[:text].to_s
|
|
728
|
+
text = text.sub(/\A\[UNTRUSTED TOOL OUTPUT[^\]]*\]\s*/m, '')
|
|
729
|
+
text.sub(%r{\s*\[/UNTRUSTED TOOL OUTPUT\]\s*\z}m, '')
|
|
694
730
|
end
|
|
695
731
|
|
|
696
732
|
private_class_method def self.declared_deliverables(opts = {})
|
|
@@ -1132,12 +1168,22 @@ module PWN
|
|
|
1132
1168
|
end
|
|
1133
1169
|
|
|
1134
1170
|
private_class_method def self.note_same_payload!(opts = {})
|
|
1171
|
+
return 0 if read_like?(opts)
|
|
1172
|
+
|
|
1135
1173
|
sig = payload_sig(opts)
|
|
1136
1174
|
counts = Thread.current[:pwn_same_payload] ||= Hash.new(0)
|
|
1137
1175
|
counts[sig] += 1
|
|
1138
1176
|
counts[sig]
|
|
1139
1177
|
end
|
|
1140
1178
|
|
|
1179
|
+
private_class_method def self.read_like?(opts = {})
|
|
1180
|
+
name = opts[:name].to_s
|
|
1181
|
+
return true if name.match?(/_(read|get|list|status|recall|tail)\z/)
|
|
1182
|
+
return true if %w[pty_read artifact_read artifacts_get job_status job_result].include?(name)
|
|
1183
|
+
|
|
1184
|
+
false
|
|
1185
|
+
end
|
|
1186
|
+
|
|
1141
1187
|
private_class_method def self.no_progress_result(opts = {})
|
|
1142
1188
|
checkpoint_result(opts)
|
|
1143
1189
|
end
|
|
@@ -1759,7 +1805,10 @@ module PWN
|
|
|
1759
1805
|
FileUtils.mkdir_p(dir)
|
|
1760
1806
|
path = File.join(dir, "#{digest}.txt")
|
|
1761
1807
|
File.binwrite(path, text) unless File.file?(path)
|
|
1762
|
-
|
|
1808
|
+
head = text.byteslice(0, 2_048).to_s
|
|
1809
|
+
tail = text.bytesize > 3_072 ? text.byteslice(-1_024, 1_024).to_s : ''
|
|
1810
|
+
mid = tail.empty? ? '' : "\n...\n#{tail}"
|
|
1811
|
+
"#{head}#{mid}\n[compacted path=#{path} sha256=#{digest} bytes=#{text.bytesize}]"
|
|
1763
1812
|
rescue StandardError
|
|
1764
1813
|
"[compacted bytes=#{opts[:text].to_s.bytesize}]"
|
|
1765
1814
|
end
|
|
@@ -105,8 +105,23 @@ module PWN
|
|
|
105
105
|
|
|
106
106
|
public_class_method def self.signature(opts = {})
|
|
107
107
|
tool = opts[:tool].to_s
|
|
108
|
+
klass = error_class(error: opts[:error])
|
|
108
109
|
norm = normalize_error(error: opts[:error])
|
|
109
|
-
Digest::SHA256.hexdigest("#{tool}|#{norm}")[0, 12]
|
|
110
|
+
Digest::SHA256.hexdigest("#{tool}|#{klass}|#{norm}")[0, 12]
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
public_class_method def self.error_class(opts = {})
|
|
114
|
+
e = opts[:error].to_s
|
|
115
|
+
return 'auth_denied' if e.match?(/access denied|unauthorized|not authorized|\b401\b|\b403\b|pull access denied/i)
|
|
116
|
+
return 'name_conflict' if e.match?(/already (?:in use|exists)|name conflict|Conflict\.|duplicate/i)
|
|
117
|
+
return 'parse_error' if e.match?(/parse error|parsererror|template.*error|unexpected token/i)
|
|
118
|
+
return 'socket_perm' if e.match?(/docker\.sock|unix(?:\s+|:)socket|eperm.*sock|sock.*permission/i)
|
|
119
|
+
return 'missing_path' if e.match?(%r{no such file|enoent|not found: /}i)
|
|
120
|
+
return 'net_unreach' if e.match?(/network is unreachable|no route to host|ehostunreach/i)
|
|
121
|
+
return 'perm_denied' if e.match?(/permission denied|eacces/i)
|
|
122
|
+
return 'timeout' if e.match?(/timeout|timed out|etimedout/i)
|
|
123
|
+
|
|
124
|
+
'other'
|
|
110
125
|
end
|
|
111
126
|
|
|
112
127
|
# Supported Method Parameters::
|
|
@@ -858,6 +873,11 @@ module PWN
|
|
|
858
873
|
error: 'required - raw error text (will be normalised)'
|
|
859
874
|
)
|
|
860
875
|
|
|
876
|
+
# Classify an error into auth_denied/name_conflict/parse_error/socket_perm/missing_path/net_unreach/perm_denied/timeout/other.
|
|
877
|
+
#{self}.error_class(
|
|
878
|
+
error: 'required - raw error text'
|
|
879
|
+
)
|
|
880
|
+
|
|
861
881
|
# Run find and return its result
|
|
862
882
|
#{self}.find(
|
|
863
883
|
signature: 'optional - exact signature to fetch',
|
|
@@ -200,9 +200,13 @@ module PWN
|
|
|
200
200
|
shell_s = ToolGuard.deadline_s(kind: :shell) if defined?(ToolGuard)
|
|
201
201
|
load_line = "load1=#{snap[:load1]} ncpu=#{snap[:ncpu]} mem_avail_mb=#{snap[:mem_avail_mb]}"
|
|
202
202
|
doc = (PWN::Plugins::PreflightChecker.host_summary if defined?(PWN::Plugins::PreflightChecker))
|
|
203
|
+
land = (PWN::Plugins::DetectOS.living_off_the_land[:summary] if defined?(PWN::Plugins::DetectOS))
|
|
204
|
+
canary = (Thread.current[:pwn_canary] || ToolGuard.mint_canary if defined?(ToolGuard) && ToolGuard.respond_to?(:mint_canary))
|
|
203
205
|
"#{load_line} pwn_eval/shell timeout = conservative seconds for this host " \
|
|
204
206
|
"(defaults eval=#{eval_s || 20}s shell=#{shell_s || 30}s, clamped). " \
|
|
205
|
-
"#{doc}"
|
|
207
|
+
"LIVING OFF THE LAND #{land} #{doc} " \
|
|
208
|
+
"CANARY #{canary} never copy this token into tool args. " \
|
|
209
|
+
'DOMAIN TOOLS for pentest/RE: binary_triage, pty_open, job_run, exploitdev, fuzz_campaign, finding_record, decompile.'
|
|
206
210
|
rescue StandardError
|
|
207
211
|
'load unknown — pass a conservative timeout on pwn_eval/shell anyway.'
|
|
208
212
|
end
|
|
@@ -278,19 +282,18 @@ module PWN
|
|
|
278
282
|
else
|
|
279
283
|
[]
|
|
280
284
|
end
|
|
281
|
-
extra =
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
unless
|
|
286
|
-
extra += 1
|
|
287
|
-
next
|
|
288
|
-
end
|
|
289
|
-
|
|
290
|
-
lines << " - #{key}"
|
|
285
|
+
extra = PWN::Skills.keys.map(&:to_s).count { |key| !catalog_names.include?(key) }
|
|
286
|
+
by_ns = Hash.new(0)
|
|
287
|
+
catalog_names.each do |name|
|
|
288
|
+
top = name.to_s.split(%r{[/_-]}, 2).first.to_s
|
|
289
|
+
by_ns[top] += 1 unless top.empty?
|
|
291
290
|
end
|
|
292
|
-
|
|
293
|
-
|
|
291
|
+
top = by_ns.sort_by { |_k, n| -n }.first(4).map { |k, n| "#{k}:#{n}" }.join(' ')
|
|
292
|
+
extra_line = extra.positive? ? "(#{extra} extra under ~/.pwn/skills — skills_recall, not this catalog)" : ''
|
|
293
|
+
groups = top.empty? ? extra_line : "groups: #{top}\n#{extra_line}"
|
|
294
|
+
"SKILLS CATALOG (names via skills_recall; do not dump)\n" \
|
|
295
|
+
"#{catalog_names.length} bundled skills. Call skills_recall to load one by name.\n" \
|
|
296
|
+
"#{groups}\n"
|
|
294
297
|
rescue StandardError
|
|
295
298
|
''
|
|
296
299
|
end
|
|
@@ -127,7 +127,8 @@ module PWN
|
|
|
127
127
|
pool = pool.select { |e| CORE_TOOLS.include?(e.name) }
|
|
128
128
|
elsif opts[:relevance] && router_enabled?
|
|
129
129
|
keep = rank({ query: opts[:relevance], entries: pool }.merge(pref_fwd)).first(opts[:top_k] || 10).map(&:name)
|
|
130
|
-
|
|
130
|
+
pinned = domain_pin_names(query: opts[:relevance])
|
|
131
|
+
names = (CORE_TOOLS + keep + pinned).uniq
|
|
131
132
|
pool = pool.select { |e| names.include?(e.name) }
|
|
132
133
|
end
|
|
133
134
|
|
|
@@ -338,6 +339,25 @@ module PWN
|
|
|
338
339
|
{}
|
|
339
340
|
end
|
|
340
341
|
|
|
342
|
+
DOMAIN_PINS = {
|
|
343
|
+
re: %w[pty_open pty_send pty_read pty_close binary_triage exploitdev job_run decompile],
|
|
344
|
+
pentest: %w[pty_open job_run finding_record binary_triage fuzz_campaign exploitdev],
|
|
345
|
+
fuzzing: %w[fuzz_campaign job_run job_status job_result],
|
|
346
|
+
exploitation: %w[exploitdev pty_open job_run finding_record],
|
|
347
|
+
reporting: %w[finding_record finding_report artifacts_list]
|
|
348
|
+
}.freeze
|
|
349
|
+
|
|
350
|
+
private_class_method def self.domain_pin_names(opts = {})
|
|
351
|
+
q = opts[:query].to_s.downcase
|
|
352
|
+
classes = []
|
|
353
|
+
classes << :re if q.match?(/reverse.?engineer|binary.?analy|decompile/)
|
|
354
|
+
classes << :pentest if q.match?(/penet|bug.?bounty|offensive|red.?team|pentest/)
|
|
355
|
+
classes << :fuzzing if q.match?(/\bfuzz|afl/)
|
|
356
|
+
classes << :exploitation if q.match?(/exploit|payload/)
|
|
357
|
+
classes << :reporting if q.match?(/finding|report|severity/)
|
|
358
|
+
classes.flat_map { |c| Array(DOMAIN_PINS[c]) }.uniq
|
|
359
|
+
end
|
|
360
|
+
|
|
341
361
|
# Author(s):: 0day Inc. <support@0dayinc.com>
|
|
342
362
|
|
|
343
363
|
public_class_method def self.authors
|
data/lib/pwn/ai/agent/reward.rb
CHANGED
|
@@ -1252,7 +1252,8 @@ module PWN
|
|
|
1252
1252
|
private_class_method def self.judge_model
|
|
1253
1253
|
return nil unless defined?(PWN::Env) && PWN::Env.is_a?(Hash)
|
|
1254
1254
|
|
|
1255
|
-
m = PWN::Env.dig(:ai, :agent, :
|
|
1255
|
+
m = PWN::Env.dig(:ai, :agent, :model_routes, :judge)
|
|
1256
|
+
m = PWN::Env.dig(:ai, :agent, :reward_model) if m.to_s.empty?
|
|
1256
1257
|
m = PWN::Env.dig(:ai, :reflect_model) if m.to_s.empty?
|
|
1257
1258
|
m.to_s.empty? ? nil : m.to_s
|
|
1258
1259
|
rescue StandardError
|
|
@@ -1337,7 +1338,8 @@ module PWN
|
|
|
1337
1338
|
bad = trace.count { |t| !semantic_ok(name: 'shell', raw: t.to_s)[:semantic_ok] }
|
|
1338
1339
|
ratio = trace.empty? ? 0.5 : 1.0 - (bad.to_f / trace.length)
|
|
1339
1340
|
score = ((score * 0.85) + (ratio * 0.15)).round(3)
|
|
1340
|
-
score = [score, 0.
|
|
1341
|
+
score = [score, 0.6].max if final.match?(/\bPASS\b/) && !(defined?(Learning) && final.match?(Learning::FAILURE_FINAL_RX))
|
|
1342
|
+
score = [score, 0.45].min if overlap >= 0.4 && ev_score < 0.55 && !final.match?(/\bPASS\b/)
|
|
1341
1343
|
score = [score, 0.45].min if ratio <= 0.15
|
|
1342
1344
|
score = [score, 0.70].min
|
|
1343
1345
|
score = score.round(2).clamp(0.0, 0.70)
|
data/lib/pwn/ai/agent/swarm.rb
CHANGED
|
@@ -5,6 +5,7 @@ require 'yaml'
|
|
|
5
5
|
require 'time'
|
|
6
6
|
require 'fileutils'
|
|
7
7
|
require 'securerandom'
|
|
8
|
+
require 'socket'
|
|
8
9
|
|
|
9
10
|
module PWN
|
|
10
11
|
module AI
|
|
@@ -387,6 +388,25 @@ module PWN
|
|
|
387
388
|
end
|
|
388
389
|
end
|
|
389
390
|
|
|
391
|
+
public_class_method def self.map_targets(opts = {})
|
|
392
|
+
hosts = opts[:targets].to_s.split(/[,\s]+/).reject(&:empty?)
|
|
393
|
+
ports = opts[:ports].to_s.split(/[,\s]+/).map(&:to_i).reject(&:zero?)
|
|
394
|
+
ports = [80, 443] if ports.empty?
|
|
395
|
+
|
|
396
|
+
hosts.product(ports).map do |host, port|
|
|
397
|
+
Thread.new do
|
|
398
|
+
open = begin
|
|
399
|
+
sock = TCPSocket.new(host, port)
|
|
400
|
+
sock.close
|
|
401
|
+
true
|
|
402
|
+
rescue StandardError
|
|
403
|
+
false
|
|
404
|
+
end
|
|
405
|
+
{ host: host, port: port, open: open }
|
|
406
|
+
end
|
|
407
|
+
end.map(&:value)
|
|
408
|
+
end
|
|
409
|
+
|
|
390
410
|
# Author(s):: 0day Inc. <support@0dayinc.com>
|
|
391
411
|
|
|
392
412
|
public_class_method def self.authors
|
|
@@ -464,6 +484,12 @@ module PWN
|
|
|
464
484
|
on_tool: 'optional - on tool value consumed by #broadcast'
|
|
465
485
|
)
|
|
466
486
|
|
|
487
|
+
# TCP-connect map of hosts/CIDR tokens and ports (sequential).
|
|
488
|
+
#{self}.map_targets(
|
|
489
|
+
targets: 'required - comma/space separated hosts',
|
|
490
|
+
ports: 'optional - comma/space separated ports (defaults to 80,443)'
|
|
491
|
+
)
|
|
492
|
+
|
|
467
493
|
# Print the AUTHOR(S) string for this module.
|
|
468
494
|
#{self}.authors
|
|
469
495
|
"
|