pwn 0.5.713 → 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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/etc/default_skills/pwn/ai/agent/tool_guard/SKILL.md +8 -0
  3. data/etc/default_skills/pwn/ai/agent/tools/decompile/SKILL.md +45 -0
  4. data/etc/default_skills/pwn/memory_index/SKILL.md +1 -0
  5. data/etc/default_skills/pwn/plugins/artifact_registry/SKILL.md +1 -0
  6. data/etc/default_skills/pwn/plugins/ghidra/SKILL.md +49 -0
  7. data/etc/default_skills/pwn/plugins/log/SKILL.md +1 -0
  8. data/etc/default_skills/pwn/setup/references/urls.md +1 -0
  9. data/lib/pwn/ai/agent/dispatch.rb +5 -0
  10. data/lib/pwn/ai/agent/loop.rb +10 -0
  11. data/lib/pwn/ai/agent/mistakes.rb +2 -1
  12. data/lib/pwn/ai/agent/prompt_builder.rb +4 -1
  13. data/lib/pwn/ai/agent/registry.rb +21 -1
  14. data/lib/pwn/ai/agent/reward.rb +2 -1
  15. data/lib/pwn/ai/agent/tool_guard.rb +83 -2
  16. data/lib/pwn/ai/agent/tools/artifacts.rb +26 -0
  17. data/lib/pwn/ai/agent/tools/decompile.rb +27 -0
  18. data/lib/pwn/config.rb +7 -1
  19. data/lib/pwn/memory_index.rb +50 -0
  20. data/lib/pwn/plugins/artifact_registry.rb +29 -0
  21. data/lib/pwn/plugins/ghidra.rb +75 -0
  22. data/lib/pwn/plugins/log.rb +18 -3
  23. data/lib/pwn/plugins.rb +1 -0
  24. data/lib/pwn/setup.rb +5 -0
  25. data/lib/pwn/version.rb +1 -1
  26. data/spec/{ai/agent/fixtures/req09_docker.json → lib/pwn/ai/agent/fixtures/error_class_docker.json} +1 -1
  27. data/spec/lib/pwn/ai/agent/learning_spec.rb +15 -0
  28. data/spec/lib/pwn/ai/agent/loop_spec.rb +15 -1
  29. data/spec/lib/pwn/ai/agent/mistakes_spec.rb +21 -0
  30. data/spec/lib/pwn/ai/agent/registry_spec.rb +11 -0
  31. data/spec/lib/pwn/ai/agent/reward_spec.rb +14 -0
  32. data/spec/lib/pwn/ai/agent/swarm_spec.rb +6 -0
  33. data/spec/lib/pwn/ai/agent/tool_guard_spec.rb +48 -0
  34. data/spec/lib/pwn/ai/agent/tools/artifacts_spec.rb +1 -0
  35. data/spec/lib/pwn/ai/agent/tools/decompile_spec.rb +14 -0
  36. data/spec/lib/pwn/cron_spec.rb +2 -2
  37. data/spec/lib/pwn/memory_index_spec.rb +9 -0
  38. data/spec/lib/pwn/plugins/aflplusplus_spec.rb +9 -0
  39. data/spec/lib/pwn/plugins/artifact_registry_spec.rb +12 -0
  40. data/spec/lib/pwn/plugins/exploit_db_spec.rb +12 -0
  41. data/spec/lib/pwn/plugins/exploit_dev_spec.rb +23 -0
  42. data/spec/lib/pwn/plugins/findings_spec.rb +15 -0
  43. data/spec/lib/pwn/plugins/frida_spec.rb +5 -0
  44. data/spec/lib/pwn/plugins/fuzz_spec.rb +20 -0
  45. data/spec/lib/pwn/plugins/gdb_spec.rb +15 -0
  46. data/spec/lib/pwn/plugins/ghidra_spec.rb +14 -0
  47. data/spec/lib/pwn/plugins/k8s_spec.rb +5 -0
  48. data/spec/lib/pwn/plugins/log_spec.rb +10 -5
  49. data/spec/lib/pwn/plugins/nmap_it_spec.rb +4 -0
  50. data/spec/lib/pwn/plugins/nuclei_spec.rb +6 -0
  51. data/spec/lib/pwn/plugins/preflight_checker_spec.rb +24 -0
  52. data/spec/lib/pwn/plugins/process_tube_spec.rb +23 -0
  53. data/spec/lib/pwn/plugins/semgrep_spec.rb +4 -0
  54. data/spec/lib/pwn/plugins/sock_spec.rb +7 -0
  55. data/spec/lib/pwn/plugins/transparent_browser_spec.rb +15 -0
  56. data/spec/lib/pwn/sessions_spec.rb +53 -0
  57. data/spec/lib/pwn/setup_spec.rb +5 -0
  58. data/third_party/pwn_rdoc.jsonl +18 -0
  59. metadata +8 -6
  60. data/spec/ai/agent/fixtures_spec.rb +0 -13
  61. data/spec/lib/pwn/remaining_req_spec.rb +0 -143
  62. data/spec/lib/pwn/req24_spec.rb +0 -82
  63. data/spec/lib/pwn/req_queue_spec.rb +0 -168
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bc496486bb8a12f9803c7fe6d75ffeffe72a885486de20f2791363c050cf011d
4
- data.tar.gz: 0a23407f3aa2707b806032e36ebc888428e63e6019825c0e24d6d6cf93269aa3
3
+ metadata.gz: 347698a0da0cfba54c8e8975a931cd0894940c126e842c17c5bad162e1307eee
4
+ data.tar.gz: fd5495f0f65d8e06acc5695b4ca796818e107ea164285c79161cf2c44c7184db
5
5
  SHA512:
6
- metadata.gz: e0882283247eb1d1389b1f2171286a14df1a460b41a244a89b05e8a06926f81c9e5d381534d10bdc603e8b0d6663e62ee4e12f1abd9aedafec792f852bbad1e7
7
- data.tar.gz: bd5dc6602f9d74153780ca07aa7fc3b81d7ae3148115916d47ea17ccc6a1b3433fe2d98fe9074b611bd00ab46bcc750062f5b94885799403697652fd8c6904d3
6
+ metadata.gz: bdcfa6f78d2ef9d8350cfc7dd69c4294e679fd537112428922858dea21eaad3ed9d295247c194dcf7372289e18c31c63fcf79076e82117792fb3c3214f2ad341
7
+ data.tar.gz: a604db47a0ea8f698cd8c3b659d626951209082a7bfff7b4ec51c6529d200d24ab9d36d7302d6796bdc422d89ab9e8b45ea67bf926b43dee4b2d4ced4c7feeff
@@ -36,6 +36,11 @@ PWN::AI::Agent::ToolGuard.present(opts)
36
36
  - `present`
37
37
  - `placeholder`
38
38
  - `bashism`
39
+ - `shell_syntax_surface`
40
+ - `mint_canary`
41
+ - `canary_leak`
42
+ - `injection_score`
43
+ - `quarantine_output`
39
44
  - `shell_bash`
40
45
  - `shell_name`
41
46
  - `protect_http`
@@ -54,6 +59,7 @@ PWN::AI::Agent::ToolGuard.present(opts)
54
59
  - `timeout_prior_count`
55
60
  - `refuse_copied_persist`
56
61
  - `scope_refusal`
62
+ - `rfc1918`
57
63
  - `ip_in_cidr`
58
64
  - `command_class`
59
65
  - `record_runtime`
@@ -63,6 +69,7 @@ PWN::AI::Agent::ToolGuard.present(opts)
63
69
  - `help`
64
70
  - `auto_job?`
65
71
  - `bashism?`
72
+ - `canary_leak?`
66
73
  - `ip_in_cidr?`
67
74
  - `note_timeout!`
68
75
  - `placeholder?`
@@ -71,6 +78,7 @@ PWN::AI::Agent::ToolGuard.present(opts)
71
78
  - `protect_http!`
72
79
  - `refuse_copied_persist?`
73
80
  - `reset_timeout_budget!`
81
+ - `rfc1918?`
74
82
  - `shell_bash?`
75
83
 
76
84
  ## Source
@@ -0,0 +1,45 @@
1
+ ---
2
+ name: pwn-ai-agent-tools-decompile
3
+ description: Drive PWN::Ai::Agent::Tools::Decompile from pwn_eval.
4
+ license: MIT
5
+ allowed-tools: [pwn, pwn_eval]
6
+ metadata:
7
+ bundled: true
8
+ generated: true
9
+ module: PWN::Ai::Agent::Tools::Decompile
10
+ source: pwn/ai/agent/tools/decompile.rb
11
+ ---
12
+
13
+ # PWN::Ai::Agent::Tools::Decompile
14
+
15
+ Public API for PWN::Ai::Agent::Tools::Decompile.
16
+
17
+ ## When to use
18
+
19
+ Call `PWN::Ai::Agent::Tools::Decompile` from `pwn_eval` when the task needs this module.
20
+ Do not reimplement it in shell.
21
+
22
+ ## Methodologies
23
+
24
+ Generated from `pwn/ai/agent/tools/decompile.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::Ai::Agent::Tools::Decompile.help
31
+ PWN::Ai::Agent::Tools::Decompile.help(opts)
32
+ ```
33
+
34
+ ## Public methods
35
+
36
+ - _(no public class methods parsed)_
37
+
38
+ ## Source
39
+
40
+ `pwn/ai/agent/tools/decompile.rb`
41
+
42
+ ## Verification
43
+
44
+ `PWN::Ai::Agent::Tools::Decompile.respond_to?(:help)` after the
45
+ module is loaded. Read the source for parameter names.
@@ -39,6 +39,7 @@ PWN::MemoryIndex.available(opts)
39
39
  - `refresh`
40
40
  - `embed`
41
41
  - `reset`
42
+ - `bm25`
42
43
  - `authors`
43
44
  - `help`
44
45
  - `available?`
@@ -37,6 +37,7 @@ PWN::Plugins::ArtifactRegistry.required_bins(opts)
37
37
  - `register`
38
38
  - `list`
39
39
  - `get`
40
+ - `read_page`
40
41
  - `authors`
41
42
  - `help`
42
43
 
@@ -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.
@@ -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`
@@ -1,3 +1,4 @@
1
1
  # PWN::Setup source links
2
2
 
3
3
  - https://install.pwndbg.re
4
+ - https://ghidra-sre.org
@@ -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(
@@ -1168,12 +1168,22 @@ module PWN
1168
1168
  end
1169
1169
 
1170
1170
  private_class_method def self.note_same_payload!(opts = {})
1171
+ return 0 if read_like?(opts)
1172
+
1171
1173
  sig = payload_sig(opts)
1172
1174
  counts = Thread.current[:pwn_same_payload] ||= Hash.new(0)
1173
1175
  counts[sig] += 1
1174
1176
  counts[sig]
1175
1177
  end
1176
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
+
1177
1187
  private_class_method def self.no_progress_result(opts = {})
1178
1188
  checkpoint_result(opts)
1179
1189
  end
@@ -115,6 +115,7 @@ module PWN
115
115
  return 'auth_denied' if e.match?(/access denied|unauthorized|not authorized|\b401\b|\b403\b|pull access denied/i)
116
116
  return 'name_conflict' if e.match?(/already (?:in use|exists)|name conflict|Conflict\.|duplicate/i)
117
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)
118
119
  return 'missing_path' if e.match?(%r{no such file|enoent|not found: /}i)
119
120
  return 'net_unreach' if e.match?(/network is unreachable|no route to host|ehostunreach/i)
120
121
  return 'perm_denied' if e.match?(/permission denied|eacces/i)
@@ -872,7 +873,7 @@ module PWN
872
873
  error: 'required - raw error text (will be normalised)'
873
874
  )
874
875
 
875
- # Classify an error into auth_denied/name_conflict/parse_error/missing_path/net_unreach/perm_denied/timeout/other.
876
+ # Classify an error into auth_denied/name_conflict/parse_error/socket_perm/missing_path/net_unreach/perm_denied/timeout/other.
876
877
  #{self}.error_class(
877
878
  error: 'required - raw error text'
878
879
  )
@@ -201,9 +201,12 @@ module PWN
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
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))
204
205
  "#{load_line} pwn_eval/shell timeout = conservative seconds for this host " \
205
206
  "(defaults eval=#{eval_s || 20}s shell=#{shell_s || 30}s, clamped). " \
206
- "LIVING OFF THE LAND #{land} #{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.'
207
210
  rescue StandardError
208
211
  'load unknown — pass a conservative timeout on pwn_eval/shell anyway.'
209
212
  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
@@ -3,6 +3,7 @@
3
3
  require 'digest'
4
4
  require 'json'
5
5
  require 'fileutils'
6
+ require 'securerandom'
6
7
 
7
8
  module PWN
8
9
  module AI
@@ -45,11 +46,50 @@ module PWN
45
46
  end
46
47
 
47
48
  public_class_method def self.bashism?(opts = {})
48
- BASHISM_RX.match?(opts[:text].to_s)
49
+ BASHISM_RX.match?(shell_syntax_surface(text: opts[:text]))
49
50
  rescue StandardError
50
51
  false
51
52
  end
52
53
 
54
+ public_class_method def self.shell_syntax_surface(opts = {})
55
+ s = opts[:text].to_s.dup
56
+ s.gsub!(/<<[-~]?\s*(['"])(\w+)\1.*?^\2\s*$/m, ' ')
57
+ s.gsub!(/'[^']*'/, "''")
58
+ s
59
+ end
60
+
61
+ public_class_method def self.mint_canary(opts = {})
62
+ n = (opts[:bytes] || 8).to_i
63
+ n = 8 if n <= 0
64
+ tok = "PWNCANARY#{SecureRandom.hex(n)}"
65
+ Thread.current[:pwn_canary] = tok
66
+ tok
67
+ end
68
+
69
+ public_class_method def self.canary_leak?(opts = {})
70
+ tok = Thread.current[:pwn_canary].to_s
71
+ return false if tok.empty?
72
+
73
+ opts[:text].to_s.include?(tok)
74
+ end
75
+
76
+ public_class_method def self.injection_score(opts = {})
77
+ t = opts[:text].to_s
78
+ score = 0
79
+ score += 3 if t.match?(/ignore (all )?(previous|prior) (instructions|directives)/i)
80
+ score += 2 if t.match?(/system:\s|tool_call|function_call/i)
81
+ score += 2 if t.match?(/\bexfiltrat|\bdo not tell (the )?(user|operator)/i)
82
+ score
83
+ end
84
+
85
+ public_class_method def self.quarantine_output(opts = {})
86
+ text = opts[:text].to_s
87
+ score = injection_score(text: text)
88
+ return text unless score >= 3
89
+
90
+ "[QUARANTINED injection_score=#{score}]\n#{text}"
91
+ end
92
+
53
93
  public_class_method def self.shell_bash?
54
94
  v = (PWN::Env.dig(:ai, :agent, :shell_bash) if defined?(PWN::Env))
55
95
  v == true || v.to_s.match?(/\A(1|true|yes|on)\z/i)
@@ -373,7 +413,7 @@ module PWN
373
413
 
374
414
  ips = cmd.scan(/\b\d{1,3}(?:\.\d{1,3}){3}\b/)
375
415
  hosts = cmd.scan(/\b[a-z0-9][a-z0-9.-]+\.[a-z]{2,}\b/i)
376
- bad_ip = allow.any? ? ips.find { |ip| allow.none? { |cidr| ip_in_cidr?(ip: ip, cidr: cidr) } } : nil
416
+ bad_ip = allow.any? ? ips.find { |ip| !rfc1918?(ip: ip) && allow.none? { |cidr| ip_in_cidr?(ip: ip, cidr: cidr) } } : nil
377
417
  bad_host = domains.any? ? hosts.find { |h| domains.none? { |d| h.downcase.end_with?(d.downcase) } } : nil
378
418
  hit = bad_ip || bad_host
379
419
  return nil unless hit
@@ -383,6 +423,17 @@ module PWN
383
423
  nil
384
424
  end
385
425
 
426
+ public_class_method def self.rfc1918?(opts = {})
427
+ oct = opts[:ip].to_s.split('.').map(&:to_i)
428
+ return false unless oct.length == 4
429
+
430
+ return true if [10, 127].include?(oct[0])
431
+ return true if oct[0] == 192 && oct[1] == 168
432
+ return true if oct[0] == 172 && oct[1].between?(16, 31)
433
+
434
+ false
435
+ end
436
+
386
437
  public_class_method def self.ip_in_cidr?(opts = {})
387
438
  ip = opts[:ip].to_s.split('.').map(&:to_i)
388
439
  cidr, bits = opts[:cidr].to_s.split('/')
@@ -458,6 +509,31 @@ module PWN
458
509
  text: 'optional - text value consumed by #bashism?'
459
510
  )
460
511
 
512
+ # Strip quoted heredocs and single-quoted strings before bash-syntax lint.
513
+ #{self}.shell_syntax_surface(
514
+ text: 'required - command string to lint'
515
+ )
516
+
517
+ # Mint a per-thread session canary token stored on Thread.current.
518
+ #{self}.mint_canary(
519
+ bytes: 'optional - hex length (defaults to 8)'
520
+ )
521
+
522
+ # True when text contains the current session canary.
523
+ #{self}.canary_leak?(
524
+ text: 'required - outbound argv or URL to inspect'
525
+ )
526
+
527
+ # Heuristic injection score for tool output (ignore-previous, tool-call spoof).
528
+ #{self}.injection_score(
529
+ text: 'required - tool output to score'
530
+ )
531
+
532
+ # Prefix high-score tool output with a QUARANTINED frame.
533
+ #{self}.quarantine_output(
534
+ text: 'required - tool output to wrap if injection_score >= 3'
535
+ )
536
+
461
537
  # Run shell bash and return its result
462
538
  #{self}.shell_bash?
463
539
 
@@ -548,6 +624,11 @@ module PWN
548
624
  command: 'required - shell command to inspect for IPs and hostnames'
549
625
  )
550
626
 
627
+ # True when ip is RFC1918 or loopback (always in-scope unless strict).
628
+ #{self}.rfc1918?(
629
+ ip: 'required - IPv4 address'
630
+ )
631
+
551
632
  # True when ip is inside cidr (e.g. 10.1.2.3 in 10.0.0.0/8).
552
633
  #{self}.ip_in_cidr?(
553
634
  ip: 'required - IPv4 address',
@@ -33,3 +33,29 @@ PWN::AI::Agent::Registry.register(
33
33
  PWN::Plugins::ArtifactRegistry.get(path: args[:path] || args['path'])
34
34
  }
35
35
  )
36
+ PWN::AI::Agent::Registry.register(
37
+ name: 'artifact_read',
38
+ toolset: 'sessions',
39
+ schema: {
40
+ name: 'artifact_read',
41
+ description: 'Page an artifact as text/hex/base64 from offset.',
42
+ parameters: {
43
+ type: 'object',
44
+ properties: {
45
+ path: { type: 'string' },
46
+ offset: { type: 'integer' },
47
+ length: { type: 'integer' },
48
+ mode: { type: 'string', description: 'text|hex|base64' }
49
+ },
50
+ required: %w[path]
51
+ }
52
+ },
53
+ handler: lambda { |args|
54
+ PWN::Plugins::ArtifactRegistry.read_page(
55
+ path: args[:path] || args['path'],
56
+ offset: args[:offset] || args['offset'],
57
+ length: args[:length] || args['length'],
58
+ mode: args[:mode] || args['mode']
59
+ )
60
+ }
61
+ )
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pwn/ai/agent/registry'
4
+
5
+ PWN::AI::Agent::Registry.register(
6
+ name: 'decompile',
7
+ toolset: 'pwn',
8
+ schema: {
9
+ name: 'decompile',
10
+ description: 'Decompile a binary via Ghidra analyzeHeadless, or r2 pdg if Ghidra is absent.',
11
+ parameters: {
12
+ type: 'object',
13
+ properties: {
14
+ bin: { type: 'string' },
15
+ path: { type: 'string' },
16
+ project_dir: { type: 'string' }
17
+ },
18
+ required: %w[bin]
19
+ }
20
+ },
21
+ handler: lambda { |args|
22
+ PWN::Plugins::Ghidra.decompile(
23
+ bin: args[:bin] || args[:path] || args['bin'],
24
+ project_dir: args[:project_dir] || args['project_dir']
25
+ )
26
+ }
27
+ )
data/lib/pwn/config.rb CHANGED
@@ -196,7 +196,13 @@ module PWN
196
196
  operator_account: nil,
197
197
  sessions_keep_days: 90,
198
198
  artifacts_keep_days: 90,
199
- max_total_mb: 512
199
+ max_total_mb: 512,
200
+ model_routes: {
201
+ summarize: nil,
202
+ judge: nil,
203
+ act: nil,
204
+ sensitive: nil
205
+ }
200
206
  # multi-agent personas : ~/.pwn/agents.yml (see PWN::AI::Agent::Swarm.help)
201
207
  # swarm bus : ~/.pwn/swarm/<swarm_id>/bus.jsonl
202
208
  }
@@ -272,6 +272,50 @@ module PWN
272
272
  []
273
273
  end
274
274
 
275
+ public_class_method def self.bm25(opts = {})
276
+ query = opts[:query].to_s.downcase
277
+ docs = Array(opts[:docs]).map(&:to_s)
278
+ qtok = expand_query(query: query)
279
+ return docs.map { |d| { doc: d, score: 0.0 } } if qtok.empty?
280
+
281
+ df = Hash.new(0)
282
+ tokenized = docs.map do |d|
283
+ toks = d.downcase.scan(/[a-z0-9]{3,}/)
284
+ toks.uniq.each { |t| df[t] += 1 }
285
+ toks
286
+ end
287
+ n = docs.length.to_f
288
+ avgdl = tokenized.sum(&:length) / [n, 1].max
289
+ k1 = 1.2
290
+ b = 0.75
291
+ ranked = docs.each_with_index.map do |doc, i|
292
+ toks = tokenized[i]
293
+ tf = Hash.new(0)
294
+ toks.each { |t| tf[t] += 1 }
295
+ dl = toks.length.to_f
296
+ score = qtok.sum do |t|
297
+ f = tf[t]
298
+ next 0.0 if f.zero?
299
+
300
+ idf = Math.log(((n - df[t] + 0.5) / (df[t] + 0.5)) + 1)
301
+ idf * ((f * (k1 + 1)) / (f + (k1 * (1 - b + (b * dl / avgdl)))))
302
+ end
303
+ { doc: doc, score: score }
304
+ end
305
+ ranked.sort_by { |h| -h[:score] }
306
+ end
307
+
308
+ private_class_method def self.expand_query(opts = {})
309
+ toks = opts[:query].to_s.downcase.scan(/[a-z0-9]{3,}/)
310
+ table = {
311
+ 'bypass' => %w[evasion evade],
312
+ 'rate' => %w[throttle],
313
+ 'limit' => %w[throttle],
314
+ 'login' => %w[auth]
315
+ }
316
+ toks.flat_map { |t| [t] + Array(table[t]) }.uniq
317
+ end
318
+
275
319
  # Author(s):: 0day Inc. <support@0dayinc.com>
276
320
 
277
321
  public_class_method def self.authors
@@ -308,6 +352,12 @@ module PWN
308
352
  texts: 'required - Array texts value consumed by #embed'
309
353
  )
310
354
 
355
+ # Rank documents by BM25 (pure Ruby; no embedder required).
356
+ #{self}.bm25(
357
+ query: 'required - search text',
358
+ docs: 'required - Array of document strings'
359
+ )
360
+
311
361
  # Run reset and return its result
312
362
  #{self}.reset
313
363
 
@@ -49,6 +49,27 @@ module PWN
49
49
  { path: path, sha256: Digest::SHA256.file(path).hexdigest, bytes: File.size(path), body: File.binread(path)[0, 65_536] }
50
50
  end
51
51
 
52
+ public_class_method def self.read_page(opts = {})
53
+ path = opts[:path].to_s
54
+ raise 'ERROR: path is required' if path.empty?
55
+ raise "ERROR: file not found: #{path}" unless File.file?(path)
56
+
57
+ offset = opts[:offset].to_i
58
+ length = (opts[:length] || 4_096).to_i
59
+ length = 4_096 if length <= 0
60
+ mode = (opts[:mode] || 'text').to_s
61
+ data = File.binread(path, length, offset).to_s
62
+ case mode
63
+ when 'hex'
64
+ { path: path, offset: offset, bytes: data.bytesize, hex: data.unpack1('H*') }
65
+ when 'base64'
66
+ require 'base64'
67
+ { path: path, offset: offset, bytes: data.bytesize, body: Base64.strict_encode64(data) }
68
+ else
69
+ { path: path, offset: offset, bytes: data.bytesize, body: data.force_encoding('UTF-8').scrub }
70
+ end
71
+ end
72
+
52
73
  public_class_method def self.authors
53
74
  "AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
54
75
  end
@@ -75,6 +96,14 @@ module PWN
75
96
  path: 'required - filesystem path of a registered artifact'
76
97
  )
77
98
 
99
+ # Page an artifact as text, hex, or base64 from an offset.
100
+ #{self}.read_page(
101
+ path: 'required - filesystem path of the artifact',
102
+ offset: 'optional - byte offset (defaults to 0)',
103
+ length: 'optional - byte count (defaults to 4096)',
104
+ mode: 'optional - text, hex, or base64 (defaults to text)'
105
+ )
106
+
78
107
  # Print the AUTHOR(S) string for this module.
79
108
  #{self}.authors
80
109
  "