pwn 0.5.561 → 0.5.573

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.
@@ -4,6 +4,7 @@ require 'curses'
4
4
  require 'fileutils'
5
5
  require 'meshtastic'
6
6
  require 'pry'
7
+ require 'reline'
7
8
  require 'tty-prompt'
8
9
  require 'unicode/display_width'
9
10
  require 'yaml'
@@ -12,10 +13,59 @@ module PWN
12
13
  module Plugins
13
14
  # This module contains methods related to the pwn REPL Driver.
14
15
  module REPL
15
- # Supported Method Parameters::
16
- # PWN::Plugins::REPL.refresh_ps1_proc(
17
- # mode: 'required - :splat or nil'
18
- # )
16
+ # Custom input handler for pwn-ai to support multi-line submissions:
17
+ # - SHIFT+ENTER (common terminal sequences) or Ctrl+J / Alt+Enter inserts a newline (continue editing)
18
+ # - plain ENTER submits the full prompt (possibly multi-line) to the AI
19
+ # - Multi-line pastes are supported (Reline handles \n in buffer; submit with ENTER)
20
+ class PwnAIInput
21
+ attr_reader :line_buffer
22
+
23
+ def initialize
24
+ @line_buffer = ''
25
+ end
26
+
27
+ def readline(prompt)
28
+ # Common escape sequences for SHIFT+ENTER across terminals (xterm, modern, etc.)
29
+ shift_enter_seqs = [
30
+ # Common SHIFT+ENTER sequences across terminals (xterm, gnome-terminal, kitty, wezterm, recent xterm w/ modifyOtherKeys, etc.)
31
+ # If SHIFT+ENTER still submits in your terminal, try Ctrl+J or Alt+Enter (M-^J) as alternatives (terminals vary in emitted bytes for SHIFT+ENTER)
32
+ '\\e[13;2~',
33
+ '\\e[1;2~',
34
+ '\\e\\r',
35
+ '\\e\\n',
36
+ '\\e[13;2u',
37
+ '\\u001b[13;2u',
38
+ '\\e[27;2;13~',
39
+ '\\e[27;2;13u',
40
+ '\\e[13;2u',
41
+ '\\e[1;2~',
42
+ '\\e[27;2;13~'
43
+ ]
44
+ shift_enter_seqs.each do |seq|
45
+ Reline.config.add_oneshot_key_binding(seq.bytes, :key_newline)
46
+ end
47
+
48
+ begin
49
+ # readmultiline with confirm block that *always* returns true:
50
+ # => normal ENTER triggers finish/submit of the (multi-line) buffer
51
+ # The bound SHIFT+ENTER (and built-in Ctrl+J / M-^M) trigger key_newline (insert \n, stay in edit)
52
+ # Reline in multiline mode also handles multi-line pastes by splitting on \n in the buffer.
53
+ @line_buffer = Reline.readmultiline(prompt, true) { |_buffer| true } || ''
54
+ ensure
55
+ Reline.config.reset_oneshot_key_bindings
56
+ end
57
+ @line_buffer
58
+ end
59
+
60
+ # Compatibility with Pry input expectations (used by hooks for line_buffer, and possibly completer/tty checks)
61
+ def tty?
62
+ true
63
+ end
64
+
65
+ def winsize
66
+ [TTY::Screen.rows || 24, TTY::Screen.columns || 80]
67
+ end
68
+ end
19
69
 
20
70
  public_class_method def self.refresh_ps1_proc(opts = {})
21
71
  mode = opts[:mode]
@@ -117,13 +167,147 @@ module PWN
117
167
  end
118
168
 
119
169
  Pry::Commands.create_command 'pwn-ai' do
120
- description 'Initiate pwn.ai chat interface.'
170
+ description 'Initiate pwn.ai autonomous agent TUI (instruct tasks using PWN modules + CLI tools; memory/sessions/agents/cron/skills-aware from PWN::Config/PWN::Memory etc).'
121
171
 
122
172
  def process
123
173
  pi = pry_instance
124
174
  pi.config.pwn_ai = true
175
+ pi.config.pwn_ai_agent = true
125
176
  pi.config.color = false if pi.config.pwn_ai
126
- pi.config.color = true unless pi.config.pwn_ai
177
+
178
+ # Switch to custom multi-line input for pwn-ai (SHIFT+ENTER newline, ENTER submit)
179
+ pi.config.pwn_ai_original_input ||= Pry.config.input
180
+ Pry.config.input = PwnAIInput.new
181
+
182
+ # Load and make aware of skills folder (scaled in PWN::Config per user pwn_env_path parent)
183
+ skills_path = begin
184
+ PWN::Config.pwn_skills_path
185
+ rescue StandardError
186
+ "#{Dir.home}/.pwn/skills"
187
+ end
188
+ PWN::Config.load_skills(pwn_skills_path: skills_path)
189
+ skills_count = (PWN.const_defined?(:Skills) ? PWN::Skills.keys.length : 0)
190
+
191
+ # Hermes-equivalent memory/sessions/cron init for this pwn-ai activation
192
+ PWN::Config.load_memory
193
+ mem_count = (PWN.const_defined?(:Memory) ? PWN::Memory.load.keys.length : 0)
194
+ sess = begin
195
+ PWN::Sessions.create(title: "pwn-ai #{Time.now.strftime('%Y-%m-%d %H:%M')}", source: 'pwn-ai-repl')
196
+ rescue StandardError
197
+ nil
198
+ end
199
+ pi.config.pwn_ai_session_id = sess[:id] if sess
200
+ cron_count = (PWN.const_defined?(:Cron) ? PWN::Cron.list.keys.length : 0)
201
+
202
+ puts "\
203
+ [*] pwn-ai agent TUI activated (PWN REPL driver w/ memory, sessions, delegation, cron)."
204
+ puts "[*] Memory facts: #{mem_count} | Session: #{pi.config.pwn_ai_session_id} | Cron jobs: #{cron_count} | Skills: #{skills_count}"
205
+ puts '[*] Instruct the AI agent to carry out a task, e.g.:'
206
+ puts " 'Use NmapIt to port scan target.com then use TransparentBrowser to spider and SAST::TestCaseEngine to analyze code if cloned. Generate report with PWN::Reports.'"
207
+ puts " 'Execute CLI nmap -sV target.com and summarize findings using PWN modules.'"
208
+ puts "[*] Skills loaded from #{skills_path} (#{skills_count} available) + memory/sessions/cron to expand autonomous capabilities."
209
+ puts "[*] Type 'toggle-pwn-ai' or normal pwn commands to exit agent mode.
210
+ "
211
+ end
212
+ end
213
+
214
+ Pry::Commands.create_command 'pwn-ai-memory' do
215
+ description 'Manage pwn-ai persistent memory (Hermes equiv).'
216
+
217
+ def process
218
+ cmd = args[0]
219
+ case cmd
220
+ when 'list', 'recall', nil
221
+ q = args[1]
222
+ res = PWN::Memory.recall(query: q)
223
+ puts res.inspect
224
+ when 'remember'
225
+ key = args[1]
226
+ val = args[2..-1].join(' ')
227
+ PWN::Memory.remember(key: key, value: val)
228
+ puts "Remembered #{key}"
229
+ when 'forget'
230
+ PWN::Memory.forget(args[1])
231
+ puts "Forgot #{args[1]}"
232
+ when 'clear'
233
+ PWN::Memory.clear
234
+ puts 'Memory cleared'
235
+ else
236
+ puts PWN::Memory.help
237
+ end
238
+ end
239
+ end
240
+
241
+ Pry::Commands.create_command 'pwn-ai-sessions' do
242
+ description 'List/resume/delete pwn-ai sessions (Hermes equiv).'
243
+
244
+ def process
245
+ cmd = args[0]
246
+ case cmd
247
+ when 'list', nil
248
+ puts PWN::Sessions.list.inspect
249
+ when 'resume'
250
+ sid = args[1]
251
+ hist = PWN::Sessions.to_response_history(session_id: sid)
252
+ puts "Loaded session #{sid} with #{hist[:choices].size} entries (set manually into response_history if needed)"
253
+ when 'delete'
254
+ PWN::Sessions.delete(session_id: args[1])
255
+ puts "Deleted #{args[1]}"
256
+ when 'stats'
257
+ puts PWN::Sessions.stats
258
+ else
259
+ puts PWN::Sessions.help
260
+ end
261
+ end
262
+ end
263
+
264
+ Pry::Commands.create_command 'pwn-ai-cron' do
265
+ description 'Manage scheduled pwn-ai / cron jobs (Hermes equiv).'
266
+
267
+ def process
268
+ cmd = args[0]
269
+ case cmd
270
+ when 'list', nil
271
+ puts PWN::Cron.list.inspect
272
+ when 'create'
273
+ # simplistic: pwn-ai-cron create '0 * * * *' 'prompt here'
274
+ sched = args[1]
275
+ pr = args[2..-1].join(' ')
276
+ job = PWN::Cron.create(schedule: sched, prompt: pr)
277
+ puts "Created #{job}"
278
+ when 'run'
279
+ res = PWN::Cron.run(id: args[1])
280
+ puts res
281
+ when 'remove'
282
+ PWN::Cron.remove(id: args[1])
283
+ puts 'Removed'
284
+ else
285
+ puts PWN::Cron.help
286
+ end
287
+ end
288
+ end
289
+
290
+ Pry::Commands.create_command 'pwn-ai-delegate' do
291
+ description 'Delegate sub-task to a PWN::AI::Agent or simple sub-chat (Hermes delegation equiv).'
292
+
293
+ def process
294
+ goal = args.join(' ')
295
+ puts "[*] Delegating: #{goal}"
296
+ # Simple delegation: use a specialized agent if matches, else another chat turn
297
+ if goal =~ /sast|code|scan/i
298
+ res = PWN::AI::Agent::SAST.analyze(request: goal)
299
+ elsif goal =~ /vuln|report/i
300
+ res = PWN::AI::Agent::VulnGen.analyze(request: goal)
301
+ else
302
+ # fallback sub call to active engine (no full loop here)
303
+ engine = PWN::Env[:ai][:active].to_s.downcase.to_sym
304
+ case engine
305
+ when :anthropic then res = PWN::AI::Anthropic.chat(request: goal)
306
+ when :grok then res = PWN::AI::Grok.chat(request: goal)
307
+ else res = PWN::AI::Ollama.chat(request: goal)
308
+ end
309
+ end
310
+ puts res
127
311
  end
128
312
  end
129
313
 
@@ -339,6 +523,12 @@ module PWN
339
523
  response_history: response_history,
340
524
  spinner: false
341
525
  )
526
+ when :anthropic
527
+ response = PWN::AI::Anthropic.chat(
528
+ request: request,
529
+ response_history: response_history,
530
+ spinner: false
531
+ )
342
532
  end
343
533
 
344
534
  response_history = {
@@ -742,9 +932,14 @@ module PWN
742
932
  pi.config.color = true
743
933
  pi.config.pwn_asm = false if pi.config.pwn_asm
744
934
  pi.config.pwn_ai = false if pi.config.pwn_ai
935
+ pi.config.pwn_ai_agent = false if pi.config.pwn_ai_agent
745
936
  pi.config.pwn_ai_debug = false if pi.config.pwn_ai_debug
746
937
  pi.config.pwn_ai_speak = false if pi.config.pwn_ai_speak
747
938
  pi.config.completer = Pry::InputCompleter
939
+ if pi.config.pwn_ai_original_input
940
+ Pry.config.input = pi.config.pwn_ai_original_input
941
+ pi.config.pwn_ai_original_input = nil
942
+ end
748
943
  return unless pi.config.pwn_mesh
749
944
 
750
945
  pi.config.pwn_mesh = false
@@ -834,65 +1029,215 @@ module PWN
834
1029
 
835
1030
  Pry.config.hooks.add_hook(:after_read, :pwn_ai_hook) do |request, pi|
836
1031
  if pi.config.pwn_ai && !request.chomp.empty?
837
- request = pi.input.line_buffer.to_s
1032
+ orig_request = pi.input.line_buffer.to_s
1033
+ request = orig_request
838
1034
  debug = pi.config.pwn_ai_debug
839
1035
  engine = PWN::Env[:ai][:active].to_s.downcase.to_sym
840
1036
  response_history = PWN::Env[:ai][engine][:response_history]
841
1037
  speak_answer = pi.config.pwn_ai_speak
1038
+ is_agent = (pi.config.pwn_ai_agent == true)
842
1039
 
843
- case engine
844
- when :grok
845
- response = PWN::AI::Grok.chat(
846
- request: request.chomp,
847
- response_history: response_history,
848
- speak_answer: speak_answer,
849
- spinner: true
850
- )
851
- when :ollama
852
- response = PWN::AI::Ollama.chat(
853
- request: request.chomp,
854
- response_history: response_history,
855
- speak_answer: speak_answer,
856
- spinner: true
857
- )
858
- when :openai
859
- response = PWN::AI::OpenAI.chat(
860
- request: request.chomp,
1040
+ # pwn-ai agent mode (Hermes TUI equiv): load skills context for autonomous task carrying
1041
+ skills_context = ''
1042
+ PWN::Skills.each { |n, m| skills_context += "\n--- SKILL #{n} ---\n#{m[:content].to_s[0, 1200]}\n" } if is_agent && PWN.const_defined?(:Skills) && PWN::Skills.is_a?(Hash)
1043
+
1044
+ memory_context = ''
1045
+ memory_context = PWN::Memory.to_context(limit: 25) if is_agent && PWN.const_defined?(:Memory)
1046
+
1047
+ sess_id = begin
1048
+ pi.config.pwn_ai_session_id
1049
+ rescue StandardError
1050
+ nil
1051
+ end
1052
+
1053
+ # Pre-process for clear CLI execution intent (e.g. "what does `id` return?")
1054
+ # This makes the agent actually *run* commands instead of just explaining them.
1055
+ curr_req = request.chomp
1056
+ if is_agent && sess_id && PWN.const_defined?(:Sessions)
1057
+ begin
1058
+ PWN::Sessions.append(session_id: sess_id, role: 'user', content: orig_request)
1059
+ rescue StandardError
1060
+ nil
1061
+ end
1062
+ end
1063
+ if is_agent && request =~ /`([^`]+)`/
1064
+ potential = ::Regexp.last_match(1).strip
1065
+ # Looks like a shell command (not PWN ruby)
1066
+ unless potential =~ /^(PWN::|def |class |require |puts |pp )/
1067
+ curr_req = "The user wants the *actual raw output* of this command (do not just describe it): `#{potential}`. " \
1068
+ 'To fulfill the request accurately, you MUST immediately output ONLY a bash code block with the exact command. ' \
1069
+ "Example format: ```bash\n#{potential}\n``` . After the host executes it, you will receive the OBSERVATION with the real output."
1070
+ end
1071
+ end
1072
+
1073
+ # Strict system prompt for agent mode (forces tool use over explanation)
1074
+ system_role = nil
1075
+ if is_agent
1076
+ base = PWN::Env[:ai][engine][:system_role_content] || 'You are an ethical hacker.'
1077
+ system_role = base + <<~PROMPT
1078
+
1079
+ You are operating as a Hermes-style autonomous agent inside the PWN REPL driver.
1080
+
1081
+ PRIMARY RULE FOR CLI AND TOOLS: When the user asks for the output of a command, "what does X return?", "run X", or anything that requires real execution, you MUST use a tool call.#{' '}
1082
+ NEVER just explain what a command does or what its output "would be".#{' '}
1083
+ To execute anything:
1084
+ - Output *exactly and only* a fenced code block.
1085
+ - For shell/CLI: ```bash
1086
+ <exact command here>
1087
+ ```
1088
+ - For PWN Ruby modules: ```ruby
1089
+ PWN::Plugins::NmapIt.port_scan(...)
1090
+ ```
1091
+ The host will execute it (Ruby in full PWN context, bash via shell) and reply with an OBSERVATION containing the real result.#{' '}
1092
+ Then continue or give the final answer.
1093
+
1094
+ Available tools include all PWN::Plugins (NmapIt, TransparentBrowser, etc.), SAST, Reports, and any CLI via bash blocks.
1095
+ Skills available this session:#{skills_context}
1096
+ #{memory_context}
1097
+
1098
+ PERSISTENT CAPABILITIES (use via ruby code blocks or direct calls):
1099
+ - Memory (cross-session): PWN::Memory.remember(key: :key, value: val, category: :fact|:preference|:lesson)
1100
+ PWN::Memory.recall(query: 'foo'), PWN::Memory.forget(key)
1101
+ - Sessions: current session id = #{sess_id}; PWN::Sessions.append(session_id: '#{sess_id}', role: 'observation', content: obs)
1102
+ - Cron: PWN::Cron.create(schedule: '0 * * * *', prompt: 'task here', name: 'foo')
1103
+ PWN::Cron.run(id: 'id'); list with PWN::Cron.list
1104
+ - Agents/Delegation: PWN::AI::Agent::SAST.analyze(request: ...); PWN::AI::Agent::VulnGen etc.
1105
+ For sub-agents use threads or separate eval calls and feed results back as OBS.
1106
+
1107
+ After receiving an observation, decide the next step or conclude.
1108
+ If you output text without a code block, it will be treated as your final answer to the user.
1109
+ PROMPT
1110
+ end
1111
+
1112
+ max_turns = is_agent ? 7 : 1
1113
+ turn = 0
1114
+ last_response = ''
1115
+ tool_was_executed_this_turn = false
1116
+
1117
+ while turn < max_turns
1118
+ chat_opts = {
1119
+ request: curr_req,
861
1120
  response_history: response_history,
862
1121
  speak_answer: speak_answer,
863
1122
  spinner: true
864
- )
865
- else
866
- raise "ERROR: Unsupported AI Engine: #{engine}"
867
- end
868
- # puts response.inspect
1123
+ }
1124
+ chat_opts[:system_role_content] = system_role if system_role
1125
+
1126
+ case engine
1127
+ when :grok
1128
+ response = PWN::AI::Grok.chat(**chat_opts)
1129
+ when :ollama
1130
+ response = PWN::AI::Ollama.chat(**chat_opts)
1131
+ when :openai
1132
+ response = PWN::AI::OpenAI.chat(**chat_opts)
1133
+ when :anthropic
1134
+ response = PWN::AI::Anthropic.chat(**chat_opts)
1135
+ else
1136
+ raise "ERROR: Unsupported AI Engine: #{engine}"
1137
+ end
869
1138
 
870
- last_response = ''
871
- if response.nil?
872
- last_response = "Model: #{model} not currently supported with API key."
873
- else
874
- if response[:choices].last.keys.include?(:text)
875
- last_response = response[:choices].last[:text]
1139
+ if response.nil?
1140
+ last_response = 'Model not currently supported with API key.'
876
1141
  else
877
- last_response = response[:choices].last[:content]
1142
+ if response[:choices].last.keys.include?(:text)
1143
+ last_response = response[:choices].last[:text].to_s
1144
+ else
1145
+ last_response = response[:choices].last[:content].to_s
1146
+ end
1147
+ response_history = {
1148
+ id: response[:id],
1149
+ object: response[:object],
1150
+ model: response[:model],
1151
+ usage: response[:usage]
1152
+ }
1153
+ response_history[:choices] ||= response[:choices]
878
1154
  end
879
1155
 
880
- response_history = {
881
- id: response[:id],
882
- object: response[:object],
883
- model: response[:model],
884
- usage: response[:usage]
885
- }
886
- response_history[:choices] ||= response[:choices]
1156
+ puts "\n\001\e[32m\002#{last_response}\001\e[0m\002\n\n"
1157
+ if is_agent && sess_id && PWN.const_defined?(:Sessions)
1158
+ begin
1159
+ PWN::Sessions.append(session_id: sess_id, role: 'assistant', content: last_response)
1160
+ rescue StandardError
1161
+ nil
1162
+ end
1163
+ end
1164
+
1165
+ if debug
1166
+ puts 'DEBUG: response_history => '
1167
+ pp response_history
1168
+ end
1169
+ PWN::Env[:ai][engine][:response_history] = response_history
1170
+
1171
+ # === Agent tool execution: parse code blocks from *this* response and actually run them ===
1172
+ tool_was_executed_this_turn = false
1173
+ if is_agent
1174
+ # Robust regex: tolerate language specifier, extra whitespace, and text around the block
1175
+ last_response.scan(/```(?:\s*(ruby|bash|sh|shell|zsh))?\s*\n?(.*?)\n?```/m).each do |lang, code|
1176
+ code = code.strip
1177
+ next if code.empty? || tool_was_executed_this_turn
1178
+
1179
+ lang = (lang || 'bash').downcase
1180
+ puts "\001\e[33m\002[ pwn-ai AGENT EXEC #{lang} ]\e[0m\002 #{code[0..90]}..."
1181
+
1182
+ obs = ''
1183
+ begin
1184
+ if lang == 'ruby'
1185
+ require 'stringio'
1186
+ old_stdout = $stdout
1187
+ $stdout = StringIO.new
1188
+ res = eval(code, TOPLEVEL_BINDING) # rubocop:disable Security/Eval -- intentional for pwn-ai agent to run PWN Ruby modules/tools in REPL context
1189
+ captured = $stdout.string
1190
+ $stdout = old_stdout
1191
+ obs = (captured + "\n=> #{res.inspect}").strip
1192
+ else
1193
+ # CLI execution - use Open3 for cleaner capture (no extra shell if possible, but backticks are simple and work)
1194
+ require 'open3'
1195
+ stdout, stderr, status = Open3.capture3(code)
1196
+ obs = stdout
1197
+ obs += "\n[stderr]\n#{stderr}" unless stderr.to_s.strip.empty?
1198
+ obs += "\n[exit: #{status.exitstatus}]" unless status.success?
1199
+ obs = obs.strip
1200
+ end
1201
+ rescue StandardError => e
1202
+ obs = "ERROR executing #{lang} block: #{e.class} - #{e.message}"
1203
+ end
1204
+
1205
+ puts "\001\e[36m\002[OBSERVATION from #{lang}]\001\e[0m\002\n#{obs[0..700]}\n"
1206
+ if is_agent && sess_id && PWN.const_defined?(:Sessions)
1207
+ begin
1208
+ PWN::Sessions.append(session_id: sess_id, role: 'observation', content: obs)
1209
+ rescue StandardError
1210
+ nil
1211
+ end
1212
+ end
1213
+
1214
+ # Feed real result back to the model as the next "user" message in the loop
1215
+ curr_req = "OBSERVATION (#{lang} execution result for previous block):\n#{obs}\n\n" \
1216
+ "Now continue fulfilling the original user request: #{orig_request}. " \
1217
+ 'If the task is complete, give the final answer (no more code blocks). Otherwise output the next needed tool block.'
1218
+
1219
+ tool_was_executed_this_turn = true
1220
+ turn += 1
1221
+ break # one execution per model turn for controlled pacing
1222
+ end
1223
+ end
1224
+
1225
+ # If we executed something, loop to let the model react to the OBS
1226
+ next if tool_was_executed_this_turn
1227
+
1228
+ # No tool executed this turn -> this last_response is the final answer
1229
+ break
887
1230
  end
888
- puts "\n\001\e[32m\002#{last_response}\001\e[0m\002\n\n"
889
1231
 
890
- if debug
891
- puts 'DEBUG: response_history => '
892
- pp response_history
893
- puts "\nresponse_history[:choices] Length: #{response_history[:choices].length}\n" unless response_history.nil?
1232
+ # If in agent mode and the model never produced an executable block but the query clearly wanted execution,
1233
+ # give one last chance with a strong reminder (helps weaker models like some Ollama ones)
1234
+ if is_agent && !tool_was_executed_this_turn && orig_request =~ /`[^`]+`/ && turn < max_turns
1235
+ reminder = 'The user explicitly asked about the output of a command in backticks. ' \
1236
+ 'Do not describe the command. Output *only* the corresponding ```bash block now so the host can run it and give you the real result.'
1237
+ curr_req = "#{reminder}\nOriginal: #{orig_request}"
1238
+ # One final direct call (no full re-loop to avoid complexity)
1239
+ # (The main loop already handled most cases; this is a safety net)
894
1240
  end
895
- PWN::Env[:ai][engine][:response_history] = response_history
896
1241
  end
897
1242
  end
898
1243
 
@@ -144,6 +144,25 @@ module PWN
144
144
  )
145
145
  end
146
146
 
147
+ # Supported Method Parameters::
148
+ # PWN::Plugins::XXD.reverse_dump(
149
+ # string: 'required - continuous hex string to reverse (i.e. "68656c6c6f")',
150
+ # file: 'required - path to binary file to dump'
151
+ # )
152
+
153
+ def self.reverse_hex_string(opts = {})
154
+ string = opts[:string]
155
+ file = opts[:file]
156
+
157
+ raise ArgumentError, 'string is required' if string.nil?
158
+
159
+ raise ArgumentError, 'output file is required' if file.nil?
160
+
161
+ File.binwrite(file, [string].pack('H*'))
162
+ rescue StandardError => e
163
+ raise e
164
+ end
165
+
147
166
  # Supported Method Parameters::
148
167
  # PWN::Plugins::XXD.reverse_dump(
149
168
  # hexdump: 'required - hexdump returned from #dump method',
@@ -284,6 +303,11 @@ module PWN
284
303
  # <step through via F7, F8, F9, etc. to get to desired instruction>
285
304
  # ```
286
305
 
306
+ #{self}.reverse_hex_string(
307
+ string: 'required - continuous hex string to reverse (i.e. \"68656c6f\")',
308
+ file: 'required - path to binary file to dump'
309
+ )
310
+
287
311
  #{self}.reverse_dump(
288
312
  hexdump: 'required - hexdump returned from #dump method',
289
313
  file: 'required - path to binary file to dump',