pwn 0.5.667 → 0.5.668

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: aa499352cfb75931341be5441fff894e39fbdff2292872d73a1e47617d89561f
4
- data.tar.gz: b84bc29d866707b543f3813d60cd3df045b953aca991fd23af637ce126e9e1a9
3
+ metadata.gz: 21fe6bb04741fbc78392d434588c1c86d0f605db0988bfa2192f68e9baa870ec
4
+ data.tar.gz: cf66359fb95bb12f08705d11dd304625878115902133d9e1ad0f8becfa8fb83a
5
5
  SHA512:
6
- metadata.gz: a79c88587274434b654149e2a45593e14e445ae7ac2d322e3bf45da6e3902709544de0ab061b03a449cdd170376771adf3fa46b06aac2cc2b3b65f808f474f56
7
- data.tar.gz: 915b78058dd9af752236a97c4c040a5005d850af0ba3ed4554459858d64afcc2fa325c4c5bcc438d716753b5130ac37f97fa728d879eae430ac76d7cde6b0a34
6
+ metadata.gz: 35f079f6d7c765c6395f490f07c06ce3494b81487bba31c978367fc2b8a23a421a6b1d9a68cda02592fd94868ca343a13c441323b6e1dee0656f46ade8c88aef
7
+ data.tar.gz: b9a45751eaf30849ac1c98bd8d603df89d8522fb950dfc830f188769b0fc8e7b69848ed1f71053f7053da24a85e0c2f56f0f9a2c26b95de2040835a7e9871c47
data/.rubocop_todo.yml CHANGED
@@ -73,6 +73,7 @@ Naming/AccessorMethodName:
73
73
  - 'lib/pwn/ai/grok.rb'
74
74
  - 'lib/pwn/ai/ollama.rb'
75
75
  - 'lib/pwn/ai/open_ai.rb'
76
+ - 'lib/pwn/ai/open_web_ui.rb'
76
77
  - 'lib/pwn/blockchain/btc.rb'
77
78
  - 'lib/pwn/plugins/jira_data_center.rb'
78
79
  - 'lib/pwn/plugins/vin.rb'
data/Gemfile CHANGED
@@ -67,7 +67,7 @@ gem 'os', '1.1.4'
67
67
  gem 'ostruct', '0.6.3'
68
68
  gem 'packetfu', '2.0.0'
69
69
  gem 'packetgen', '4.1.1'
70
- gem 'pdf-reader', '2.15.1'
70
+ gem 'pdf-reader', '2.16.0'
71
71
  gem 'pg', '1.6.3'
72
72
  gem 'pry', '0.16.0'
73
73
  gem 'pry-doc', '1.7.0'
@@ -89,12 +89,12 @@ gem 'ruby-nmap', '1.0.3'
89
89
  gem 'ruby-saml', '1.18.1'
90
90
  gem 'rvm', '1.11.3.9'
91
91
  gem 'savon', '2.17.4'
92
- gem 'selenium-devtools', '0.150.0'
93
- gem 'selenium-webdriver', '4.46.0'
92
+ gem 'selenium-devtools', '0.151.0'
93
+ gem 'selenium-webdriver', '4.47.0'
94
94
  gem 'slack-ruby-client', '3.2.0'
95
95
  gem 'socksify', '1.8.1'
96
96
  gem 'spreadsheet', '1.3.5'
97
- gem 'sqlite3', '2.9.5'
97
+ gem 'sqlite3', '2.9.6'
98
98
  gem 'thin', '2.0.1'
99
99
  gem 'tty-prompt', '0.23.1'
100
100
  gem 'tty-spinner', '0.9.3'
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'json'
4
+ require 'securerandom'
4
5
 
5
6
  module PWN
6
7
  module AI
@@ -118,6 +119,165 @@ module PWN
118
119
  raise ArgumentError, "invalid JSON arguments: #{e.message}"
119
120
  end
120
121
 
122
+ # Supported Method Parameters::
123
+ # calls = PWN::AI::Agent::Dispatch.tool_calls_from_text(
124
+ # text: 'required - assistant plain-text that may embed shell(...) / JSON tool forms'
125
+ # )
126
+ #
127
+ # Local / abliterated models often print tool invocations as content
128
+ # instead of native message.tool_calls. Supported shapes include:
129
+ # shell(command="id") / shell({"command":"id"}) / shell("id")
130
+ # {"name":"shell","arguments":{...}} / {"function":{"name":...}}
131
+ # {"tool":"shell","arguments":{...}} / {"call":"shell","arguments":{...}}
132
+ # call:shell{command: "uname -s"} / tool:shell{"command":"id"}
133
+ # When structured tool_calls are empty, Loop coerces those strings into
134
+ # OpenAI-shaped tool_call hashes so Dispatch runs them instead of
135
+ # treating the string as a FINAL answer.
136
+
137
+ public_class_method def self.tool_calls_from_text(opts = {})
138
+ text = opts[:text].to_s
139
+ return [] if text.strip.empty?
140
+
141
+ Registry.discover if defined?(Registry) && Registry.respond_to?(:discover)
142
+ known = if defined?(Registry)
143
+ Registry.all.map { |e| e.name.to_s }.reject(&:empty?)
144
+ else
145
+ %w[shell pwn_eval]
146
+ end
147
+ return [] if known.empty?
148
+
149
+ names_alt = known.map { |n| Regexp.escape(n) }.join('|')
150
+ calls = []
151
+ seen = {}
152
+
153
+ add = lambda do |name, args|
154
+ name = name.to_s
155
+ next unless known.include?(name)
156
+
157
+ args_h = case args
158
+ when Hash then symbolize(hash: args)
159
+ when String
160
+ s = args.strip
161
+ begin
162
+ parsed = JSON.parse(s, symbolize_names: true)
163
+ parsed.is_a?(Hash) ? parsed : { value: parsed }
164
+ rescue JSON::ParserError
165
+ h = {}
166
+ s.scan(/([A-Za-z_]\w*)\s*[:=]\s*(?:"((?:\\.|[^"])*)"|'((?:\\.|[^'])*)'|([^\s,)}{]+))/) do
167
+ k = Regexp.last_match(1)
168
+ h[k.to_sym] = Regexp.last_match(2) || Regexp.last_match(3) || Regexp.last_match(4)
169
+ end
170
+ if h.empty?
171
+ entry = (Registry.lookup(name: name) if defined?(Registry))
172
+ req = Array(entry&.schema&.dig(:parameters, :required))
173
+ h = req.length == 1 ? { req.first.to_sym => s } : { command: s }
174
+ end
175
+ h
176
+ end
177
+ else
178
+ {}
179
+ end
180
+ key = "#{name}|#{JSON.generate(args_h)}"
181
+ next if seen[key]
182
+
183
+ seen[key] = true
184
+ calls << {
185
+ id: "textcall_#{calls.length + 1}_#{SecureRandom.hex(3)}",
186
+ type: 'function',
187
+ function: {
188
+ name: name,
189
+ # OpenAI/xAI wire format requires a JSON string, not a map.
190
+ arguments: JSON.generate(args_h)
191
+ }
192
+ }
193
+ end
194
+
195
+ # Balanced-delimiter extractor used for name(...) and call:name{...}.
196
+ extract_balanced = lambda do |open_ch, close_ch, from|
197
+ depth = 1
198
+ i = from
199
+ in_s = nil
200
+ esc = false
201
+ while i < text.length && depth.positive?
202
+ ch = text[i]
203
+ if in_s
204
+ if esc
205
+ esc = false
206
+ elsif ch == '\\'
207
+ esc = true
208
+ elsif ch == in_s
209
+ in_s = nil
210
+ end
211
+ elsif ['"', "'"].include?(ch)
212
+ in_s = ch
213
+ elsif ch == open_ch
214
+ depth += 1
215
+ elsif ch == close_ch
216
+ depth -= 1
217
+ end
218
+ i += 1
219
+ end
220
+ depth.zero? ? [text[from...(i - 1)].to_s.strip, i] : nil
221
+ end
222
+
223
+ # JSON object forms:
224
+ # {"name":"shell","arguments":{...}}
225
+ # {"function":{"name":"shell","arguments":{...}}}
226
+ # {"tool":"shell","arguments":{...}} / {"call":"shell",...}
227
+ # {"type":"call","name":"shell",...}
228
+ text.scan(/\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}/m).each do |blob|
229
+ begin
230
+ j = JSON.parse(blob, symbolize_names: true)
231
+ rescue JSON::ParserError
232
+ next
233
+ end
234
+ next unless j.is_a?(Hash)
235
+
236
+ name = (
237
+ j[:name] || j[:tool] || j[:call] ||
238
+ j.dig(:function, :name) || j.dig(:tool_call, :name)
239
+ ).to_s
240
+ # Skip pure type tags mistaken as names (e.g. {"call":{...}} trees).
241
+ next if name.empty? || %w[function tool_call].include?(name)
242
+
243
+ args = j[:arguments] || j[:args] || j[:parameters] ||
244
+ j.dig(:function, :arguments) || j.dig(:tool_call, :arguments) || {}
245
+ add.call(name, args)
246
+ end
247
+
248
+ # Colon-brace forms (OpenWebUI / abliterated dumps):
249
+ # call:shell{command: "uname -s"}
250
+ # tool:shell{"command":"id"}
251
+ # call:shell{command="id"}
252
+ rx_colon = /\b(?:call|tool)\s*:\s*(#{names_alt})\s*\{/i
253
+ idx = 0
254
+ while (m = text.match(rx_colon, idx))
255
+ name = m[1]
256
+ extracted = extract_balanced.call('{', '}', m.end(0))
257
+ if extracted
258
+ # Re-wrap: balanced extractor yields the interior only. Paren form
259
+ # shell({...}) keeps braces inside (...); brace form must restore
260
+ # them so JSON.parse / kwarg scan see a full object body.
261
+ add.call(name, "{#{extracted[0]}}")
262
+ end
263
+ idx = m.begin(0) + 1
264
+ end
265
+
266
+ # Call forms: shell(command="...") / shell({"command":"id"}) / shell("id")
267
+ rx = /\b(#{names_alt})\s*\(/i
268
+ idx = 0
269
+ while (m = text.match(rx, idx))
270
+ name = m[1]
271
+ extracted = extract_balanced.call('(', ')', m.end(0))
272
+ add.call(name, extracted[0]) if extracted
273
+ idx = m.begin(0) + 1
274
+ end
275
+
276
+ calls
277
+ rescue StandardError
278
+ []
279
+ end
280
+
121
281
  private_class_method def self.symbolize(opts = {})
122
282
  hash = opts[:hash] ||= {}
123
283
  hash.each_with_object({}) { |(k, v), m| m[k.to_sym] = v }
@@ -143,6 +303,7 @@ module PWN
143
303
  )
144
304
 
145
305
  PWN::AI::Agent::Dispatch.repair_name(name: 'run_shell') # => 'shell'
306
+ PWN::AI::Agent::Dispatch.tool_calls_from_text(text: 'shell(command="id")')
146
307
 
147
308
  #{self}.authors
148
309
  USAGE
@@ -1189,7 +1189,8 @@ module PWN
1189
1189
 
1190
1190
  bin = m[:name].to_s.split('_').first
1191
1191
  tc = Array(delta[:changed]).find { |c| c[:path].to_s.include?("toolchain.#{bin}") }
1192
- miss = snap.dig(:toolchain, bin.to_sym).to_s.empty?
1192
+ tc_map = snap[:toolchain].is_a?(Hash) ? snap[:toolchain] : {}
1193
+ miss = tc_map[bin.to_sym].to_s.empty?
1193
1194
  next unless tc || miss
1194
1195
 
1195
1196
  findings << { kind: :tool_env_mismatch, tool: m[:name], success_rate: m[:success_rate], evidence: tc || "binary '#{bin}' not found in PATH", advice: "Re-verify `which #{bin}` / reinstall before relying on #{m[:name]}." }
@@ -1206,7 +1207,7 @@ module PWN
1206
1207
  end
1207
1208
 
1208
1209
  # 3) intel observations matching installed components
1209
- pkgs = snap[:toolchain] || {}
1210
+ pkgs = snap[:toolchain].is_a?(Hash) ? snap[:toolchain] : {}
1210
1211
  observations(category: 'intel', limit: 100).each do |ob|
1211
1212
  blob = ob[:data].to_s.downcase
1212
1213
  pkgs.each do |bin, ver|
@@ -1218,7 +1219,7 @@ module PWN
1218
1219
  end
1219
1220
 
1220
1221
  # 4) :rf observations vs missing SDR hardware / binaries
1221
- rf = snap[:rf] || {}
1222
+ rf = snap[:rf].is_a?(Hash) ? snap[:rf] : {}
1222
1223
  hw_present = %i[rtl_sdr hackrf flipper gqrx_sock].any? { |k| rf_present?(val: rf[k]) }
1223
1224
  observations(category: 'rf', limit: 50).each do |ob|
1224
1225
  miss = RF_BINS.select { |b| pkgs[b.to_sym].to_s.empty? }
@@ -1240,7 +1241,10 @@ module PWN
1240
1241
  Learning.outcomes(limit: 30, success: false).each do |o|
1241
1242
  next unless o[:task].to_s.include?(host) || o[:details].to_s.include?(host)
1242
1243
 
1243
- findings << { kind: :target_web_drift, target: tgt, dom_sha: ob.dig(:data, :dom_sha), task: o[:task], advice: "Target #{host} DOM changed (#{ob[:timestamp]}) around this failure - re-recon before assuming your technique is wrong." }
1244
+ dom_sha = case ob[:data]
1245
+ when Hash then ob[:data][:dom_sha] || ob[:data]['dom_sha']
1246
+ end
1247
+ findings << { kind: :target_web_drift, target: tgt, dom_sha: dom_sha, task: o[:task], advice: "Target #{host} DOM changed (#{ob[:timestamp]}) around this failure - re-recon before assuming your technique is wrong." }
1244
1248
  end
1245
1249
  end
1246
1250
  end
@@ -1253,7 +1257,7 @@ module PWN
1253
1257
  end
1254
1258
 
1255
1259
  # 7) :intel observations whose source anchor is currently probe_web-unreachable -> downgrade
1256
- web = snap[:web] || {}
1260
+ web = snap[:web].is_a?(Hash) ? snap[:web] : {}
1257
1261
  web.each do |host, fp|
1258
1262
  next unless fp.is_a?(Hash) && (fp[:reachable] == false || fp[:status].to_i >= 500)
1259
1263
 
@@ -1088,7 +1088,8 @@ module PWN
1088
1088
  # Mistakes(tool:'assumption') so KNOWN MISTAKES warns every future
1089
1089
  # run off that specific hallucination.
1090
1090
  private_class_method def self.fact_check_local_final(opts = {})
1091
- return unless defined?(PWN::Env) && PWN::Env.dig(:ai, :active).to_s.downcase.to_sym == :ollama
1091
+ eng = defined?(PWN::Env) ? PWN::Env.dig(:ai, :active).to_s.downcase.to_sym : nil
1092
+ return unless %i[ollama openwebui].include?(eng)
1092
1093
  return unless defined?(Extrospection) && Extrospection.respond_to?(:verify)
1093
1094
 
1094
1095
  claims = opts[:final].to_s.scan(CLAIM_RX).flatten.compact.uniq