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.
Files changed (116) hide show
  1. checksums.yaml +4 -4
  2. data/etc/default_skills/pwn/ai/agent/learning/SKILL.md +3 -0
  3. data/etc/default_skills/pwn/ai/agent/mistakes/SKILL.md +1 -0
  4. data/etc/default_skills/pwn/ai/agent/swarm/SKILL.md +1 -0
  5. data/etc/default_skills/pwn/ai/agent/tool_guard/SKILL.md +16 -0
  6. data/etc/default_skills/pwn/ai/agent/tools/artifacts/SKILL.md +45 -0
  7. data/etc/default_skills/pwn/ai/agent/tools/binary_triage/SKILL.md +45 -0
  8. data/etc/default_skills/pwn/ai/agent/tools/decompile/SKILL.md +45 -0
  9. data/etc/default_skills/pwn/ai/agent/tools/exploitdev/SKILL.md +45 -0
  10. data/etc/default_skills/pwn/ai/agent/tools/finding_record/SKILL.md +45 -0
  11. data/etc/default_skills/pwn/ai/agent/tools/fuzz_campaign/SKILL.md +45 -0
  12. data/etc/default_skills/pwn/ai/agent/tools/intel_lookup/SKILL.md +45 -0
  13. data/etc/default_skills/pwn/ai/agent/tools/job_run/SKILL.md +45 -0
  14. data/etc/default_skills/pwn/ai/agent/tools/pty_session/SKILL.md +45 -0
  15. data/etc/default_skills/pwn/ai/agent/tools/session_export/SKILL.md +45 -0
  16. data/etc/default_skills/pwn/ai/agent/tools/swarm_map/SKILL.md +45 -0
  17. data/etc/default_skills/pwn/memory_index/SKILL.md +1 -0
  18. data/etc/default_skills/pwn/plugins/aflplusplus/SKILL.md +1 -0
  19. data/etc/default_skills/pwn/plugins/artifact_registry/SKILL.md +2 -0
  20. data/etc/default_skills/pwn/plugins/binary_parser/SKILL.md +1 -0
  21. data/etc/default_skills/pwn/plugins/detect_os/SKILL.md +1 -0
  22. data/etc/default_skills/pwn/plugins/findings/SKILL.md +50 -0
  23. data/etc/default_skills/pwn/plugins/ghidra/SKILL.md +49 -0
  24. data/etc/default_skills/pwn/plugins/jobs/SKILL.md +1 -0
  25. data/etc/default_skills/pwn/plugins/log/SKILL.md +1 -0
  26. data/etc/default_skills/pwn/plugins/nmap_it/SKILL.md +1 -0
  27. data/etc/default_skills/pwn/plugins/preflight_checker/SKILL.md +2 -0
  28. data/etc/default_skills/pwn/plugins/transparent_browser/SKILL.md +2 -0
  29. data/etc/default_skills/pwn/sessions/SKILL.md +3 -0
  30. data/etc/default_skills/pwn/setup/references/urls.md +1 -0
  31. data/etc/intel/files_exploits.csv +2 -0
  32. data/lib/pwn/ai/agent/dispatch.rb +5 -0
  33. data/lib/pwn/ai/agent/learning.rb +72 -0
  34. data/lib/pwn/ai/agent/loop.rb +62 -13
  35. data/lib/pwn/ai/agent/mistakes.rb +21 -1
  36. data/lib/pwn/ai/agent/prompt_builder.rb +16 -13
  37. data/lib/pwn/ai/agent/registry.rb +21 -1
  38. data/lib/pwn/ai/agent/reward.rb +4 -2
  39. data/lib/pwn/ai/agent/swarm.rb +26 -0
  40. data/lib/pwn/ai/agent/tool_guard.rb +201 -1
  41. data/lib/pwn/ai/agent/tools/artifacts.rb +61 -0
  42. data/lib/pwn/ai/agent/tools/binary_triage.rb +20 -0
  43. data/lib/pwn/ai/agent/tools/decompile.rb +27 -0
  44. data/lib/pwn/ai/agent/tools/exploitdev.rb +45 -0
  45. data/lib/pwn/ai/agent/tools/finding_record.rb +48 -0
  46. data/lib/pwn/ai/agent/tools/fuzz_campaign.rb +44 -0
  47. data/lib/pwn/ai/agent/tools/intel_lookup.rb +24 -0
  48. data/lib/pwn/ai/agent/tools/job_run.rb +56 -0
  49. data/lib/pwn/ai/agent/tools/pty_session.rb +73 -0
  50. data/lib/pwn/ai/agent/tools/session_export.rb +20 -0
  51. data/lib/pwn/ai/agent/tools/shell.rb +5 -0
  52. data/lib/pwn/ai/agent/tools/swarm_map.rb +26 -0
  53. data/lib/pwn/config.rb +10 -1
  54. data/lib/pwn/memory_index.rb +50 -0
  55. data/lib/pwn/plugins/aflplusplus.rb +30 -1
  56. data/lib/pwn/plugins/artifact_registry.rb +43 -0
  57. data/lib/pwn/plugins/binary_parser.rb +31 -0
  58. data/lib/pwn/plugins/detect_os.rb +109 -2
  59. data/lib/pwn/plugins/exploit_db.rb +38 -1
  60. data/lib/pwn/plugins/findings.rb +105 -0
  61. data/lib/pwn/plugins/ghidra.rb +75 -0
  62. data/lib/pwn/plugins/jobs.rb +12 -1
  63. data/lib/pwn/plugins/log.rb +18 -3
  64. data/lib/pwn/plugins/nmap_it.rb +29 -0
  65. data/lib/pwn/plugins/preflight_checker.rb +36 -0
  66. data/lib/pwn/plugins/transparent_browser.rb +29 -0
  67. data/lib/pwn/plugins.rb +2 -0
  68. data/lib/pwn/sessions.rb +92 -1
  69. data/lib/pwn/setup.rb +41 -15
  70. data/lib/pwn/version.rb +1 -1
  71. data/spec/lib/pwn/ai/agent/fixtures/error_class_docker.json +7 -0
  72. data/spec/lib/pwn/ai/agent/learning_spec.rb +15 -0
  73. data/spec/lib/pwn/ai/agent/loop_spec.rb +57 -2
  74. data/spec/lib/pwn/ai/agent/mistakes_spec.rb +21 -0
  75. data/spec/lib/pwn/ai/agent/registry_spec.rb +11 -0
  76. data/spec/lib/pwn/ai/agent/reward_spec.rb +14 -0
  77. data/spec/lib/pwn/ai/agent/swarm_spec.rb +6 -0
  78. data/spec/lib/pwn/ai/agent/tool_guard_spec.rb +48 -0
  79. data/spec/lib/pwn/ai/agent/tools/artifacts_spec.rb +16 -0
  80. data/spec/lib/pwn/ai/agent/tools/binary_triage_spec.rb +14 -0
  81. data/spec/lib/pwn/ai/agent/tools/decompile_spec.rb +14 -0
  82. data/spec/lib/pwn/ai/agent/tools/exploitdev_spec.rb +14 -0
  83. data/spec/lib/pwn/ai/agent/tools/finding_record_spec.rb +15 -0
  84. data/spec/lib/pwn/ai/agent/tools/fuzz_campaign_spec.rb +14 -0
  85. data/spec/lib/pwn/ai/agent/tools/intel_lookup_spec.rb +14 -0
  86. data/spec/lib/pwn/ai/agent/tools/job_run_spec.rb +16 -0
  87. data/spec/lib/pwn/ai/agent/tools/pty_session_spec.rb +17 -0
  88. data/spec/lib/pwn/ai/agent/tools/session_export_spec.rb +14 -0
  89. data/spec/lib/pwn/ai/agent/tools/swarm_map_spec.rb +14 -0
  90. data/spec/lib/pwn/cron_spec.rb +2 -2
  91. data/spec/lib/pwn/memory_index_spec.rb +9 -0
  92. data/spec/lib/pwn/plugins/aflplusplus_spec.rb +9 -0
  93. data/spec/lib/pwn/plugins/artifact_registry_spec.rb +12 -0
  94. data/spec/lib/pwn/plugins/detect_os_spec.rb +40 -0
  95. data/spec/lib/pwn/plugins/exploit_db_spec.rb +12 -0
  96. data/spec/lib/pwn/plugins/exploit_dev_spec.rb +23 -0
  97. data/spec/lib/pwn/plugins/findings_spec.rb +30 -0
  98. data/spec/lib/pwn/plugins/frida_spec.rb +5 -0
  99. data/spec/lib/pwn/plugins/fuzz_spec.rb +20 -0
  100. data/spec/lib/pwn/plugins/gdb_spec.rb +15 -0
  101. data/spec/lib/pwn/plugins/ghidra_spec.rb +14 -0
  102. data/spec/lib/pwn/plugins/k8s_spec.rb +5 -0
  103. data/spec/lib/pwn/plugins/log_spec.rb +10 -5
  104. data/spec/lib/pwn/plugins/nmap_it_spec.rb +4 -0
  105. data/spec/lib/pwn/plugins/nuclei_spec.rb +6 -0
  106. data/spec/lib/pwn/plugins/preflight_checker_spec.rb +24 -0
  107. data/spec/lib/pwn/plugins/process_tube_spec.rb +23 -0
  108. data/spec/lib/pwn/plugins/semgrep_spec.rb +4 -0
  109. data/spec/lib/pwn/plugins/sock_spec.rb +7 -0
  110. data/spec/lib/pwn/plugins/transparent_browser_spec.rb +15 -0
  111. data/spec/lib/pwn/sessions_spec.rb +53 -0
  112. data/spec/lib/pwn/setup_spec.rb +32 -0
  113. data/third_party/pwn_rdoc.jsonl +64 -2
  114. metadata +42 -3
  115. data/spec/lib/pwn/req_queue_spec.rb +0 -168
  116. 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.
@@ -37,6 +37,7 @@ PWN::Plugins::Jobs.required_bins(opts)
37
37
  - `start`
38
38
  - `status`
39
39
  - `tail`
40
+ - `result`
40
41
  - `stop`
41
42
  - `authors`
42
43
  - `help`
@@ -38,6 +38,7 @@ PWN::Plugins::Log.append(opts)
38
38
  - `debug_log_path`
39
39
  - `start_debug`
40
40
  - `trace_enabled`
41
+ - `debug_dir`
41
42
  - `next_request_log`
42
43
  - `finish_request_log`
43
44
  - `stop_debug`
@@ -36,6 +36,7 @@ PWN::Plugins::NmapIt.port_scan(opts)
36
36
  - `port_scan`
37
37
  - `parse_xml_results`
38
38
  - `diff_xml_results`
39
+ - `to_findings`
39
40
  - `authors`
40
41
  - `help`
41
42
 
@@ -36,6 +36,8 @@ PWN::Plugins::PreflightChecker.required_bins(opts)
36
36
  - `required_bins`
37
37
  - `bin`
38
38
  - `require_bin`
39
+ - `route`
40
+ - `pick`
39
41
  - `cap_net_raw`
40
42
  - `require_cap_net_raw`
41
43
  - `service`
@@ -55,8 +55,10 @@ PWN::Plugins::TransparentBrowser.open(opts)
55
55
  - `toggle_devtools`
56
56
  - `jmp_devtools_panel`
57
57
  - `close`
58
+ - `evidence`
58
59
  - `authors`
59
60
  - `help`
61
+ - `evidence!`
60
62
 
61
63
  ## Source
62
64
 
@@ -46,6 +46,9 @@ PWN::Sessions.sessions_dir(opts)
46
46
  - `stats`
47
47
  - `lean`
48
48
  - `protected_session_ids`
49
+ - `export`
50
+ - `retain`
51
+ - `redact`
49
52
  - `authors`
50
53
  - `help`
51
54
  - `lean!`
@@ -1,3 +1,4 @@
1
1
  # PWN::Setup source links
2
2
 
3
3
  - https://install.pwndbg.re
4
+ - https://ghidra-sre.org
@@ -0,0 +1,2 @@
1
+ id,file,description,date,author,type,platform,port,cve,cvss
2
+ 50592,exploits/multiple/remote/50592.py,Apache Log4j 2 JNDI injection Log4Shell,2021-12-10,pwn,remote,multiple,,CVE-2021-44228,10.0
@@ -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
  "
@@ -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
- return true if declared_skills_missing?(skills: contract[:skills])
667
- return true if declared_hosts_missing?(hosts: contract[:hosts], messages: opts[:messages])
668
- return true if declared_hosts_missing?(hosts: contract[:techniques], messages: opts[:messages])
669
- return true if contract[:issue_work] && Array(contract[:proofs]).empty?
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.declared_hosts_missing?(opts = {})
686
- hosts = Array(opts[:hosts]).map(&:to_s).reject(&:empty?)
687
- return false if hosts.empty?
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
- blob = Array(opts[:messages]).select { |msg| msg.is_a?(Hash) && msg[:role].to_s == 'tool' }
690
- .map { |msg| msg[:content].to_s }
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
- .downcase
693
- hosts.any? { |host| !blob.include?(host.to_s.downcase) }
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
- "[compacted path=#{path} sha256=#{digest} bytes=#{text.bytesize}]"
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 = 0
282
- lines = []
283
- PWN::Skills.each_key do |name|
284
- key = name.to_s
285
- unless catalog_names.include?(key)
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
- extra_line = extra.positive? ? " (#{extra} additional files under ~/.pwn/skills call skills_recall to search; they are not this catalog)\n" : ''
293
- "SKILLS CATALOG (bundled pwn-ai; call skills_recall with no query to list)\n#{lines.join("\n")}\n#{extra_line}\n"
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
- names = (CORE_TOOLS + keep).uniq
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
@@ -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, :reward_model)
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.45].min if overlap >= 0.4 && ev_score < 0.55
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)
@@ -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
  "