pwn 0.5.721 → 0.5.723

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 (108) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +2 -2
  3. data/bin/pwn_setup +5 -5
  4. data/documentation/AI-Integration.md +34 -1
  5. data/documentation/Policy-Benchmark.md +151 -0
  6. data/documentation/Reinforcement-Learning.md +27 -1
  7. data/etc/default_skills/pwn/ai/agent/engagement/SKILL.md +1 -0
  8. data/etc/default_skills/pwn/ai/agent/learning/SKILL.md +2 -0
  9. data/etc/default_skills/pwn/ai/agent/metrics/SKILL.md +4 -0
  10. data/etc/default_skills/pwn/ai/agent/mistakes/SKILL.md +2 -0
  11. data/etc/default_skills/pwn/ai/agent/policy/SKILL.md +1 -0
  12. data/etc/default_skills/pwn/ai/agent/reward/SKILL.md +2 -0
  13. data/etc/default_skills/pwn/ai/agent/swarm/SKILL.md +6 -0
  14. data/etc/default_skills/pwn/ai/agent/tools/capabilities/SKILL.md +45 -0
  15. data/etc/default_skills/pwn/ai/agent/tools/context/SKILL.md +45 -0
  16. data/etc/default_skills/pwn/ai/context/SKILL.md +50 -0
  17. data/etc/default_skills/pwn/ai/http_retry/SKILL.md +7 -0
  18. data/etc/default_skills/pwn/ai/http_retry/references/urls.md +4 -0
  19. data/etc/default_skills/pwn/ai/open_ai/SKILL.md +1 -0
  20. data/etc/default_skills/pwn/ai/open_ai/references/urls.md +1 -0
  21. data/etc/default_skills/pwn/plugins/exploit_dev/SKILL.md +1 -0
  22. data/etc/default_skills/pwn/plugins/findings/SKILL.md +1 -0
  23. data/etc/default_skills/pwn/plugins/gdb/SKILL.md +1 -0
  24. data/etc/default_skills/pwn/plugins/gdbmi/SKILL.md +55 -0
  25. data/etc/default_skills/pwn/plugins/ghidra_headless/SKILL.md +49 -0
  26. data/etc/default_skills/pwn/plugins/jobs/SKILL.md +6 -0
  27. data/etc/default_skills/pwn/plugins/packet/SKILL.md +3 -0
  28. data/etc/default_skills/pwn/plugins/preflight_checker/SKILL.md +1 -0
  29. data/etc/default_skills/pwn/plugins/radare2/SKILL.md +1 -0
  30. data/etc/default_skills/pwn/plugins/transparent_browser/SKILL.md +3 -0
  31. data/etc/default_skills/pwn/reports/engagement/SKILL.md +3 -2
  32. data/lib/pwn/ai/agent/curriculum.rb +37 -45
  33. data/lib/pwn/ai/agent/dispatch.rb +9 -0
  34. data/lib/pwn/ai/agent/engagement.rb +59 -0
  35. data/lib/pwn/ai/agent/learning.rb +111 -44
  36. data/lib/pwn/ai/agent/loop.rb +50 -8
  37. data/lib/pwn/ai/agent/metrics.rb +70 -2
  38. data/lib/pwn/ai/agent/mistakes.rb +67 -1
  39. data/lib/pwn/ai/agent/policy.rb +142 -16
  40. data/lib/pwn/ai/agent/registry.rb +1 -1
  41. data/lib/pwn/ai/agent/reward.rb +153 -53
  42. data/lib/pwn/ai/agent/swarm.rb +235 -35
  43. data/lib/pwn/ai/agent/tool_guard.rb +13 -1
  44. data/lib/pwn/ai/agent/tools/artifacts.rb +50 -3
  45. data/lib/pwn/ai/agent/tools/capabilities.rb +19 -0
  46. data/lib/pwn/ai/agent/tools/context.rb +38 -0
  47. data/lib/pwn/ai/agent/tools/finding_record.rb +18 -0
  48. data/lib/pwn/ai/agent/tools/fuzz_campaign.rb +10 -1
  49. data/lib/pwn/ai/agent/tools/job_run.rb +32 -0
  50. data/lib/pwn/ai/agent/tools/learning.rb +5 -6
  51. data/lib/pwn/ai/agent/tools/metrics.rb +16 -0
  52. data/lib/pwn/ai/agent/tools/pty_session.rb +4 -4
  53. data/lib/pwn/ai/agent/tools/ruby_eval.rb +6 -5
  54. data/lib/pwn/ai/agent/tools/shell.rb +10 -1
  55. data/lib/pwn/ai/agent/tools/skills.rb +30 -0
  56. data/lib/pwn/ai/agent/tools/swarm.rb +8 -2
  57. data/lib/pwn/ai/context.rb +193 -0
  58. data/lib/pwn/ai/http_retry.rb +53 -7
  59. data/lib/pwn/ai/open_ai.rb +302 -45
  60. data/lib/pwn/ai.rb +1 -0
  61. data/lib/pwn/migrate.rb +10 -1
  62. data/lib/pwn/plugins/artifact_registry.rb +40 -17
  63. data/lib/pwn/plugins/binary_parser.rb +4 -1
  64. data/lib/pwn/plugins/exploit_dev.rb +15 -0
  65. data/lib/pwn/plugins/findings.rb +48 -8
  66. data/lib/pwn/plugins/gdb.rb +17 -0
  67. data/lib/pwn/plugins/gdbmi.rb +128 -0
  68. data/lib/pwn/plugins/ghidra_headless.rb +104 -0
  69. data/lib/pwn/plugins/jobs.rb +72 -0
  70. data/lib/pwn/plugins/packet.rb +51 -0
  71. data/lib/pwn/plugins/preflight_checker.rb +29 -0
  72. data/lib/pwn/plugins/process_tube.rb +24 -7
  73. data/lib/pwn/plugins/radare2.rb +14 -2
  74. data/lib/pwn/plugins/repl.rb +6 -1
  75. data/lib/pwn/plugins/transparent_browser.rb +64 -0
  76. data/lib/pwn/plugins.rb +2 -0
  77. data/lib/pwn/reports/engagement.rb +19 -0
  78. data/lib/pwn/sessions.rb +3 -1
  79. data/lib/pwn/version.rb +1 -1
  80. data/scripts/benchmark_policy.rb +276 -0
  81. data/spec/documentation/installation_md_spec.rb +18 -4
  82. data/spec/integration/reinforced_feedback_loop_spec.rb +20 -13
  83. data/spec/lib/pwn/ai/agent/curriculum_spec.rb +267 -0
  84. data/spec/lib/pwn/ai/agent/engagement_spec.rb +12 -0
  85. data/spec/lib/pwn/ai/agent/learning_spec.rb +81 -3
  86. data/spec/lib/pwn/ai/agent/loop_spec.rb +51 -0
  87. data/spec/lib/pwn/ai/agent/metrics_spec.rb +44 -0
  88. data/spec/lib/pwn/ai/agent/mistakes_spec.rb +10 -0
  89. data/spec/lib/pwn/ai/agent/policy_spec.rb +103 -0
  90. data/spec/lib/pwn/ai/agent/registry_spec.rb +62 -0
  91. data/spec/lib/pwn/ai/agent/reward_spec.rb +175 -12
  92. data/spec/lib/pwn/ai/agent/swarm_spec.rb +121 -1
  93. data/spec/lib/pwn/ai/agent/tool_guard_spec.rb +6 -0
  94. data/spec/lib/pwn/ai/agent/tools/capabilities_spec.rb +14 -0
  95. data/spec/lib/pwn/ai/agent/tools/context_spec.rb +14 -0
  96. data/spec/lib/pwn/ai/agent/tools/job_run_spec.rb +2 -0
  97. data/spec/lib/pwn/ai/agent/tools/learning_spec.rb +25 -0
  98. data/spec/lib/pwn/ai/context_spec.rb +48 -0
  99. data/spec/lib/pwn/ai/http_retry_spec.rb +27 -0
  100. data/spec/lib/pwn/ai/open_ai_oauth_transport_spec.rb +245 -0
  101. data/spec/lib/pwn/ai/open_ai_spec.rb +150 -0
  102. data/spec/lib/pwn/migrate_spec.rb +24 -0
  103. data/spec/lib/pwn/plugins/artifact_registry_spec.rb +19 -0
  104. data/spec/lib/pwn/plugins/findings_spec.rb +2 -0
  105. data/spec/lib/pwn/plugins/gdbmi_spec.rb +17 -0
  106. data/spec/lib/pwn/plugins/ghidra_headless_spec.rb +17 -0
  107. data/third_party/pwn_rdoc.jsonl +86 -2
  108. metadata +24 -5
@@ -0,0 +1,49 @@
1
+ ---
2
+ name: pwn-plugins-ghidraheadless
3
+ description: Drive PWN::Plugins::GhidraHeadless from pwn_eval.
4
+ license: MIT
5
+ allowed-tools: [pwn, pwn_eval]
6
+ metadata:
7
+ bundled: true
8
+ generated: true
9
+ module: PWN::Plugins::GhidraHeadless
10
+ source: pwn/plugins/ghidra_headless.rb
11
+ ---
12
+
13
+ # PWN::Plugins::GhidraHeadless
14
+
15
+ analyzeHeadless wrapper that exports decompiled C plus the symbol table as JSON, cached by binary SHA-256.
16
+
17
+ ## When to use
18
+
19
+ Call `PWN::Plugins::GhidraHeadless` 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_headless.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::GhidraHeadless.help
31
+ PWN::Plugins::GhidraHeadless.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_headless.rb`
45
+
46
+ ## Verification
47
+
48
+ `PWN::Plugins::GhidraHeadless.respond_to?(:required_bins)` after the
49
+ module is loaded. Read the source for parameter names.
@@ -35,6 +35,12 @@ PWN::Plugins::Jobs.required_bins(opts)
35
35
 
36
36
  - `required_bins`
37
37
  - `start`
38
+ - `run`
39
+ - `job_run`
40
+ - `job_status`
41
+ - `job_tail`
42
+ - `job_kill`
43
+ - `watch`
38
44
  - `status`
39
45
  - `tail`
40
46
  - `result`
@@ -45,8 +45,11 @@ PWN::Plugins::Packet.open_pcap_file(opts)
45
45
  - `construct_udp`
46
46
  - `send`
47
47
  - `tcp_connect_scan`
48
+ - `cap_net_raw`
49
+ - `capture`
48
50
  - `authors`
49
51
  - `help`
52
+ - `cap_net_raw?`
50
53
 
51
54
  ## Source
52
55
 
@@ -38,6 +38,7 @@ PWN::Plugins::PreflightChecker.required_bins(opts)
38
38
  - `require_bin`
39
39
  - `route`
40
40
  - `capability_coverage`
41
+ - `manifest`
41
42
  - `pick`
42
43
  - `cap_net_raw`
43
44
  - `require_cap_net_raw`
@@ -40,6 +40,7 @@ PWN::Plugins::Radare2.required_bins(opts)
40
40
  - `close`
41
41
  - `functions`
42
42
  - `xrefs_to`
43
+ - `xrefs_from`
43
44
  - `disasm`
44
45
  - `strings`
45
46
  - `imports`
@@ -56,6 +56,9 @@ PWN::Plugins::TransparentBrowser.open(opts)
56
56
  - `jmp_devtools_panel`
57
57
  - `close`
58
58
  - `evidence`
59
+ - `intercept`
60
+ - `har_export`
61
+ - `replay`
59
62
  - `authors`
60
63
  - `help`
61
64
  - `evidence!`
@@ -28,11 +28,12 @@ Class methods take `(opts = {})` and read `opts`.
28
28
 
29
29
  ```ruby
30
30
  PWN::Reports::Engagement.help
31
- PWN::Reports::Engagement.generate(opts)
31
+ PWN::Reports::Engagement.render(opts)
32
32
  ```
33
33
 
34
34
  ## Public methods
35
35
 
36
+ - `render`
36
37
  - `generate`
37
38
  - `authors`
38
39
  - `help`
@@ -43,5 +44,5 @@ PWN::Reports::Engagement.generate(opts)
43
44
 
44
45
  ## Verification
45
46
 
46
- `PWN::Reports::Engagement.respond_to?(:generate)` after the
47
+ `PWN::Reports::Engagement.respond_to?(:render)` after the
47
48
  module is loaded. Read the source for parameter names.
@@ -104,11 +104,12 @@ module PWN
104
104
 
105
105
  prompts = generate_reproducers(mistake: m, count: [per, 2].max)
106
106
  runs = dry_run ? [] : prompts.map { |p| self_play(prompt: p, tag: "practice:#{m[:signature]}") }
107
- solved = runs.select { |r| r[:score].to_f >= 0.6 }
108
- mean = runs.empty? ? 0.0 : (runs.sum { |r| r[:score].to_f } / runs.length)
107
+ solved = runs.select { |r| r[:success] == true && !r[:training_score].nil? }
108
+ scored = runs.reject { |r| r[:training_score].nil? }
109
+ mean = scored.empty? ? nil : (scored.sum { |r| r[:training_score].to_f } / scored.length)
109
110
  resolved = false
110
111
  # 2.4 — auto-resolve only with N≥2 holdout successes + store trace
111
- # P23 — auto-resolve only with N≥2 holdouts at judge≥0.7 AND a
112
+ # P23 — auto-resolve only with N≥2 trusted holdout successes AND a
112
113
  # real tool trace (not empty-final luck). Budget fingerprints
113
114
  # additionally require mean holdout ≥0.7 and short-horizon tags.
114
115
  if solved.length >= 2 && defined?(Mistakes)
@@ -130,7 +131,7 @@ module PWN
130
131
  bump_cooldown!(cooldown: cool, signature: m[:signature], mean: mean) unless dry_run
131
132
  results << {
132
133
  signature: m[:signature], tool: m[:tool], prompts: prompts,
133
- runs: runs.map { |r| { score: r[:score], verdict: r[:verdict] } },
134
+ runs: runs,
134
135
  resolved: false, mean_score: mean.round(3),
135
136
  reason: 'holdouts_ok_but_trace_weak'
136
137
  }
@@ -177,14 +178,14 @@ module PWN
177
178
  end
178
179
  resolved = true
179
180
  cool.delete(m[:signature].to_s)
180
- elsif !dry_run
181
+ elsif !dry_run && !mean.nil?
181
182
  # P1 — track zero-progress nights; park after COOLDOWN_FAIL_NIGHTS
182
183
  bump_cooldown!(cooldown: cool, signature: m[:signature], mean: mean)
183
184
  end
184
185
  results << {
185
186
  signature: m[:signature], tool: m[:tool], prompts: prompts,
186
- runs: runs.map { |r| { score: r[:score], verdict: r[:verdict] } },
187
- resolved: resolved, mean_score: mean.round(3)
187
+ runs: runs,
188
+ resolved: resolved, mean_score: mean&.round(3)
188
189
  }
189
190
  end
190
191
  end
@@ -264,7 +265,7 @@ module PWN
264
265
 
265
266
  if commit && defined?(Learning)
266
267
  prior = Learning.outcomes(limit: 500).find do |o|
267
- o[:session_id].to_s == sid.to_s && o[:score] && Array(o[:tags]).include?('offline_judge')
268
+ o[:session_id].to_s == sid.to_s && !o[:training_score].nil? && Array(o[:tags]).include?('offline_judge')
268
269
  end
269
270
  next if prior
270
271
  end
@@ -278,38 +279,31 @@ module PWN
278
279
  next if req.strip.empty? || fin.strip.empty?
279
280
 
280
281
  v = Reward.judge(request: req, final: fin, session_id: sid, commit: commit)
281
- Reward.prm(request: req, session_id: sid) if do_prm
282
+ known = !v[:training_score].nil?
283
+ Reward.prm(request: req, session_id: sid) if do_prm && commit && known
282
284
  # P7/W3 — offline path must also fill calibration so the controller
283
285
  # (force plan_first/critic at n≥8) actually becomes reachable under
284
286
  # :failure_only local introspect. Pull p(success)= out of any PLAN.
285
- if commit
287
+ if commit && known
286
288
  plan = t.find { |e| e[:role].to_s == 'assistant' && e[:content].to_s.start_with?('PLAN:') }
287
289
  pred = plan && plan[:content].to_s[/p\(success\)\s*=\s*([01](?:\.\d+)?)/i, 1]
288
290
  if pred
289
291
  eng = (PWN::Env.dig(:ai, :active) if defined?(PWN::Env))
290
- calibrate(predicted: pred.to_f, actual: v[:score].to_f, engine: eng)
292
+ calibrate(predicted: pred.to_f, actual: v[:success] ? 1.0 : 0.0, engine: eng)
291
293
  end
292
294
  end
293
295
  if commit && defined?(Learning)
294
- # P29 — keep verdict tag score-aligned (same as auto_introspect).
295
- sc = v[:score].to_f
296
- verd = if defined?(Learning) && Learning.respond_to?(:verdict_for_score, true)
297
- Learning.send(:verdict_for_score, score: sc).to_s
298
- elsif sc >= 0.6 then 'solved'
299
- elsif sc >= 0.3 then 'partial'
300
- else 'wrong'
301
- end
296
+ verd = v[:verdict].to_s
302
297
  Learning.note_outcome(
303
298
  task: req[0, 120],
304
- success: sc >= 0.6,
305
- score: sc,
306
- details: "offline_judge #{verd}(#{sc.round(2)}) #{v[:rationale]}",
299
+ outcome: v,
300
+ details: "offline_judge #{verd}(#{v[:score]}) #{v[:rationale]}",
307
301
  session_id: sid,
308
302
  tags: ['offline_judge', 'auto', verd],
309
303
  judge_source: v[:source]
310
304
  )
311
305
  end
312
- scored << { session_id: sid, score: v[:score], verdict: v[:verdict] }
306
+ scored << v.merge(session_id: sid)
313
307
  end
314
308
 
315
309
  mean = scored.empty? ? nil : (scored.sum { |r| r[:score].to_f } / scored.length).round(3)
@@ -1059,9 +1053,11 @@ module PWN
1059
1053
  rescue StandardError
1060
1054
  nil
1061
1055
  end
1062
- { session_id: sid, prompt: opts[:prompt], final: final, score: v[:score], verdict: v[:verdict], trace: trace }
1056
+ v.merge(session_id: sid, prompt: opts[:prompt], final: final, trace: trace)
1063
1057
  rescue StandardError => e
1064
- { prompt: opts[:prompt], error: e.message, score: 0.0, trace: nil }
1058
+ Reward.resolve_outcome(outcome: { source: :error, error: e.message }).merge(
1059
+ session_id: sid, prompt: opts[:prompt], final: final, trace: trace
1060
+ )
1065
1061
  end
1066
1062
 
1067
1063
  private_class_method def self.score_branch(opts = {})
@@ -1204,7 +1200,8 @@ module PWN
1204
1200
  smoke_ok = cand_smoke[:resolved].to_i >= base_smoke[:resolved].to_i &&
1205
1201
  cand_smoke[:mean_score].to_f + 0.05 >= base_smoke[:mean_score].to_f
1206
1202
 
1207
- promote = resolved_win && mean_win && smoke_ok
1203
+ outcomes_known = [baseline, candid, base_smoke, cand_smoke].all? { |r| r[:unknown]&.zero? }
1204
+ promote = outcomes_known && resolved_win && mean_win && smoke_ok
1208
1205
  {
1209
1206
  baseline: opts[:baseline],
1210
1207
  candidate: opts[:candidate],
@@ -1223,6 +1220,7 @@ module PWN
1223
1220
  resolved_win: resolved_win,
1224
1221
  mean_win: mean_win,
1225
1222
  smoke_ok: smoke_ok,
1223
+ outcomes_known: outcomes_known,
1226
1224
  promote: promote,
1227
1225
  evalset_size: evalset.length,
1228
1226
  gate_version: 2
@@ -1270,34 +1268,28 @@ module PWN
1270
1268
 
1271
1269
  private_class_method def self.replay_on_detailed(opts = {})
1272
1270
  tag = opts[:tag].to_s
1273
- return { resolved: 0, mean_score: 0.0, scores: [] } if tag.empty?
1271
+ return { resolved: 0, mean_score: 0.0, scores: [], outcomes: [], unknown: Array(opts[:evalset]).length } if tag.empty?
1274
1272
 
1275
- scores = []
1273
+ outcomes = []
1276
1274
  with_ollama_model(tag: tag) do
1277
1275
  Array(opts[:evalset]).each do |e|
1278
- r = self_play(prompt: e[:prompt], tag: "gate:#{tag}")
1279
- scores << r[:score].to_f
1276
+ outcomes << self_play(prompt: e[:prompt], tag: "gate:#{tag}")
1280
1277
  end
1281
1278
  end
1282
- resolved = scores.count { |s| s >= 0.7 }
1283
- mean = scores.empty? ? 0.0 : (scores.sum / scores.length)
1284
- { resolved: resolved, mean_score: mean.round(3), scores: scores }
1279
+ known = outcomes.reject { |r| r[:training_score].nil? }
1280
+ resolved = known.count { |r| r[:success] == true }
1281
+ mean = known.empty? ? 0.0 : (known.sum { |r| r[:training_score] } / known.length)
1282
+ {
1283
+ resolved: resolved, mean_score: mean.round(3),
1284
+ scores: outcomes.map { |r| r[:score] }, outcomes: outcomes,
1285
+ unknown: outcomes.length - known.length
1286
+ }
1285
1287
  rescue StandardError
1286
- { resolved: 0, mean_score: 0.0, scores: [] }
1288
+ { resolved: 0, mean_score: 0.0, scores: [], outcomes: outcomes || [], unknown: Array(opts[:evalset]).length }
1287
1289
  end
1288
1290
 
1289
1291
  private_class_method def self.replay_on(opts = {})
1290
- tag = opts[:tag].to_s
1291
- return 0 if tag.empty?
1292
-
1293
- with_ollama_model(tag: tag) do
1294
- opts[:evalset].count do |e|
1295
- r = self_play(prompt: e[:prompt], tag: "gate:#{tag}")
1296
- r[:score].to_f >= 0.7
1297
- end
1298
- end
1299
- rescue StandardError
1300
- 0
1292
+ replay_on_detailed(tag: opts[:tag], evalset: opts[:evalset])[:resolved]
1301
1293
  end
1302
1294
 
1303
1295
  private_class_method def self.with_ollama_model(opts = {})
@@ -72,6 +72,15 @@ module PWN
72
72
  error: 'refused: memory_remember/skills_update text copied from last tool output'
73
73
  )
74
74
  end
75
+ if blob.match?(/open_sockraw|sockraw/) && defined?(PWN::Plugins::PreflightChecker) &&
76
+ PWN::Plugins::PreflightChecker.respond_to?(:cap_net_raw?) && !PWN::Plugins::PreflightChecker.cap_net_raw?
77
+ return JSON.generate(
78
+ success: false,
79
+ error: 'capability missing CAP_NET_RAW',
80
+ substitute: 'PWN::Plugins::Packet.tcp_connect_scan',
81
+ code: 'CAP_DENY'
82
+ )
83
+ end
75
84
  result = entry.handler.call(args)
76
85
  result = ToolGuard.quarantine_output(text: result) if defined?(ToolGuard) && result.is_a?(String) && ToolGuard.respond_to?(:quarantine_output)
77
86
  note_taint(text: result)
@@ -58,6 +58,32 @@ module PWN
58
58
  token = (opts[:host] || opts[:ip] || opts[:target] || opts[:url]).to_s
59
59
  return true if token.empty?
60
60
 
61
+ roe = load_roe
62
+ host = token
63
+ begin
64
+ host = URI.parse(token).host || token if token.include?('://')
65
+ rescue StandardError
66
+ host = token
67
+ end
68
+ host = host.sub(%r{\Ahttps?://}i, '').split('/').first.to_s.split(':').first
69
+ if roe[:present]
70
+ deny = Array(roe[:targets_deny])
71
+ return false if deny.any? { |ex| host == ex.to_s || host.end_with?(".#{ex}") || host.include?(ex.to_s) }
72
+
73
+ allow = Array(roe[:targets_allow])
74
+ unless allow.empty?
75
+ ok = allow.any? do |a|
76
+ a = a.to_s
77
+ host == a || host.end_with?(".#{a}") || begin
78
+ IPAddr.new(a).include?(IPAddr.new(host))
79
+ rescue StandardError
80
+ false
81
+ end
82
+ end
83
+ return false unless ok
84
+ end
85
+ end
86
+
61
87
  row = status
62
88
  return true if row[:active].nil? || row[:missing]
63
89
 
@@ -90,6 +116,16 @@ module PWN
90
116
  public_class_method def self.deny_if_out_of_scope(opts = {})
91
117
  args = opts[:args] || opts[:command] || opts[:text]
92
118
  blob = args.is_a?(Hash) ? args.inspect : args.to_s
119
+ roe = load_roe
120
+ tech_hit = Array(roe[:techniques_deny]).find { |t| blob.downcase.include?(t.to_s.downcase) }
121
+ if tech_hit
122
+ return {
123
+ success: false,
124
+ error: "roe_deny: technique #{tech_hit}",
125
+ code: 'ROE_DENY',
126
+ violating: [tech_hit]
127
+ }
128
+ end
93
129
  tokens = blob.scan(/(?:\d{1,3}\.){3}\d{1,3}|[A-Za-z0-9.-]+\.[A-Za-z]{2,}/).uniq
94
130
  bad = tokens.reject { |tok| in_scope?(host: tok) }
95
131
  return nil if bad.empty?
@@ -102,6 +138,24 @@ module PWN
102
138
  }
103
139
  end
104
140
 
141
+ public_class_method def self.load_roe(opts = {})
142
+ path = (opts[:path] || File.join(Dir.home, '.pwn', 'roe.yaml')).to_s
143
+ return { present: false, targets_allow: [], targets_deny: [], techniques_deny: [] } unless File.file?(path)
144
+
145
+ require 'yaml'
146
+ raw = YAML.safe_load_file(path, permitted_classes: [], symbolize_names: true) || {}
147
+ raw = {} unless raw.is_a?(Hash)
148
+ {
149
+ present: true,
150
+ targets_allow: Array(raw[:targets_allow] || raw['targets_allow']),
151
+ targets_deny: Array(raw[:targets_deny] || raw['targets_deny']),
152
+ techniques_deny: Array(raw[:techniques_deny] || raw['techniques_deny']),
153
+ time_windows: raw[:time_windows] || raw['time_windows']
154
+ }
155
+ rescue StandardError
156
+ { present: false, targets_allow: [], targets_deny: [], techniques_deny: [] }
157
+ end
158
+
105
159
  public_class_method def self.authors
106
160
  "AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
107
161
  end
@@ -149,6 +203,11 @@ module PWN
149
203
  text: 'optional - free-form blob to scan'
150
204
  )
151
205
 
206
+ # Load the RoE policy file when present for allow and deny lists.
207
+ #{self}.load_roe(
208
+ path: 'optional - filesystem path of roe.yaml (defaults to ~/.pwn/roe.yaml)'
209
+ )
210
+
152
211
  # Print the AUTHOR(S) string for this module.
153
212
  #{self}.authors
154
213
  "