pwn 0.5.609 → 0.5.612
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 +4 -4
- data/README.md +3 -3
- data/lib/pwn/ai/agent/loop.rb +117 -104
- data/lib/pwn/ai/agent/prompt_builder.rb +1 -2
- data/lib/pwn/ai/agent/tools/ruby_eval.rb +16 -2
- data/lib/pwn/ai/grok.rb +65 -16
- data/lib/pwn/ai/introspection.rb +8 -50
- data/lib/pwn/plugins/repl.rb +1 -1
- data/lib/pwn/plugins/zaproxy.rb +4 -2
- data/lib/pwn/plugins/zaproxy.rb.bak +837 -0
- data/lib/pwn/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8beaa60314e788a7122a92098c1ea4897cf488ef88efd1030f1f704656dbe1ff
|
|
4
|
+
data.tar.gz: 94fff44f3cdc34c997fb920f63affa133cab0de4d309acf4c09f4451adbbdcf6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d9869f0a43c78a53238b1ef1ab58f54f27f1da575ad101aff4c172e813ec7878b92465f5d0f8984bce33a1a6227ec3a67c80ee63e1643683deaa45e2860e9c52
|
|
7
|
+
data.tar.gz: 1407422fb0cc44c84232f37f664a07eb0a80bafaf748e8aadbe310652640ed83526e1ce48d34a1d0302d4537c27ca71983c44595824b39080693157ba038322f
|
data/README.md
CHANGED
|
@@ -37,7 +37,7 @@ $ cd /opt/pwn
|
|
|
37
37
|
$ ./install.sh
|
|
38
38
|
$ ./install.sh ruby-gem
|
|
39
39
|
$ pwn
|
|
40
|
-
pwn[v0.5.
|
|
40
|
+
pwn[v0.5.612]:001 >>> PWN.help
|
|
41
41
|
```
|
|
42
42
|
|
|
43
43
|
[](https://youtu.be/G7iLUY4FzsI)
|
|
@@ -52,7 +52,7 @@ $ rvm use ruby-4.0.5@pwn
|
|
|
52
52
|
$ gem uninstall --all --executables pwn
|
|
53
53
|
$ gem install --verbose pwn
|
|
54
54
|
$ pwn
|
|
55
|
-
pwn[v0.5.
|
|
55
|
+
pwn[v0.5.612]:001 >>> PWN.help
|
|
56
56
|
```
|
|
57
57
|
|
|
58
58
|
If you're using a multi-user install of RVM do:
|
|
@@ -62,7 +62,7 @@ $ rvm use ruby-4.0.5@pwn
|
|
|
62
62
|
$ rvmsudo gem uninstall --all --executables pwn
|
|
63
63
|
$ rvmsudo gem install --verbose pwn
|
|
64
64
|
$ pwn
|
|
65
|
-
pwn[v0.5.
|
|
65
|
+
pwn[v0.5.612]:001 >>> PWN.help
|
|
66
66
|
```
|
|
67
67
|
|
|
68
68
|
PWN periodically upgrades to the latest version of Ruby which is reflected in `/opt/pwn/.ruby-version`. The easiest way to upgrade to the latest version of Ruby from a previous PWN installation is to run the following script:
|
data/lib/pwn/ai/agent/loop.rb
CHANGED
|
@@ -25,60 +25,74 @@ module PWN
|
|
|
25
25
|
gemini: 'PWN::AI::Gemini'
|
|
26
26
|
}.freeze
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
# session_id: 'optional - PWN::Sessions id (transcript is appended to it)',
|
|
32
|
-
# enabled_toolsets: 'optional - subset of Registry.toolsets, or nil for all',
|
|
33
|
-
# on_tool: 'optional - ->(name, args, result) callback for live UI'
|
|
34
|
-
# )
|
|
28
|
+
private_class_method def self.degrade_text_only(opts = {})
|
|
29
|
+
mod = opts[:mod]
|
|
30
|
+
messages = opts[:messages]
|
|
35
31
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
32
|
+
warn "[pwn-ai] #{mod} has no chat — falling back to text-only (no tool-calling)"
|
|
33
|
+
sys = messages.find { |m| m[:role] == 'system' }
|
|
34
|
+
user = messages.rfind { |m| m[:role] == 'user' }
|
|
35
|
+
r = mod.chat(
|
|
36
|
+
request: user[:content],
|
|
37
|
+
system_role_content: sys&.[](:content),
|
|
38
|
+
spinner: true
|
|
39
|
+
)
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
txt = r.is_a?(Hash) ? (r.dig(:choices, -1, :content) || r.dig(:choices, -1, :text)).to_s : r.to_s
|
|
42
|
+
{ role: 'assistant', content: txt, tool_calls: [] }
|
|
43
|
+
end
|
|
42
44
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
private_class_method def self.max_iters
|
|
46
|
+
v = (PWN::Env.dig(:ai, :agent, :max_iters) if defined?(PWN::Env))
|
|
47
|
+
v.to_i.positive? ? v.to_i : DEFAULT_MAX_ITERS
|
|
48
|
+
rescue StandardError
|
|
49
|
+
DEFAULT_MAX_ITERS
|
|
50
|
+
end
|
|
49
51
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
private_class_method def self.append_session(opts = {})
|
|
53
|
+
session_id = opts[:session_id]
|
|
54
|
+
return unless session_id && defined?(PWN::Sessions)
|
|
53
55
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
+
PWN::Sessions.append(
|
|
57
|
+
session_id: session_id,
|
|
58
|
+
role: opts[:role],
|
|
59
|
+
content: opts[:content]
|
|
60
|
+
)
|
|
61
|
+
rescue StandardError
|
|
62
|
+
nil
|
|
63
|
+
end
|
|
56
64
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
end
|
|
65
|
+
# Supported Method Parameters::
|
|
66
|
+
# msg = PWN::AI::Agent::Loop.normalize_llm(
|
|
67
|
+
# response: 'required - chat_with_tools response Hash from any provider'
|
|
68
|
+
# )
|
|
62
69
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
raw = Dispatch.call(tool_call: tc)
|
|
67
|
-
result = Result.condition(content: raw, entry: entry)
|
|
70
|
+
private_class_method def self.normalize_llm(opts = {})
|
|
71
|
+
resp = opts[:response]
|
|
72
|
+
return nil unless resp.is_a?(Hash)
|
|
68
73
|
|
|
69
|
-
|
|
74
|
+
msg = resp.dig(:choices, 0, :message) || resp[:assistant_message]
|
|
75
|
+
return nil unless msg
|
|
70
76
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
77
|
+
out = {
|
|
78
|
+
role: 'assistant',
|
|
79
|
+
content: msg[:content],
|
|
80
|
+
tool_calls: Array(msg[:tool_calls]).map do |tc|
|
|
81
|
+
{
|
|
82
|
+
id: tc[:id],
|
|
83
|
+
type: 'function',
|
|
84
|
+
function: {
|
|
85
|
+
name: tc.dig(:function, :name) || tc[:name],
|
|
86
|
+
arguments: tc.dig(:function, :arguments) || tc[:arguments]
|
|
87
|
+
}
|
|
76
88
|
}
|
|
77
|
-
append_session(session_id: session_id, role: 'tool', content: "#{name} → #{result[0, 400]}")
|
|
78
89
|
end
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
90
|
+
}
|
|
91
|
+
# Preserve provider-native content blocks so chat can round-trip
|
|
92
|
+
# them exactly on the next iteration (e.g. Anthropic requires the
|
|
93
|
+
# original tool_use block to precede a tool_result).
|
|
94
|
+
out[:_native_content] = msg[:_native_content] if msg[:_native_content]
|
|
95
|
+
out
|
|
82
96
|
end
|
|
83
97
|
|
|
84
98
|
# Supported Method Parameters::
|
|
@@ -92,9 +106,9 @@ module PWN
|
|
|
92
106
|
# tool_calls: [ {id:, type:'function', function:{name:, arguments:}} ],
|
|
93
107
|
# _native_content: <provider raw> (when adapter needs round-trip) }
|
|
94
108
|
|
|
95
|
-
|
|
109
|
+
private_class_method def self.call_engine(opts = {})
|
|
96
110
|
messages = opts[:messages]
|
|
97
|
-
tools
|
|
111
|
+
tools = opts[:tools]
|
|
98
112
|
|
|
99
113
|
engine = (PWN::Env.dig(:ai, :active) if defined?(PWN::Env)).to_s.downcase.to_sym
|
|
100
114
|
engine = :openai if engine == :''
|
|
@@ -104,79 +118,77 @@ module PWN
|
|
|
104
118
|
|
|
105
119
|
mod = Object.const_get(mod_name)
|
|
106
120
|
if mod.respond_to?(:chat_with_tools)
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
tools: tools,
|
|
112
|
-
spinner: true
|
|
113
|
-
)
|
|
121
|
+
response = mod.chat_with_tools(
|
|
122
|
+
messages: messages,
|
|
123
|
+
tools: tools,
|
|
124
|
+
spinner: true
|
|
114
125
|
)
|
|
126
|
+
normalize_llm(response: response)
|
|
115
127
|
else
|
|
116
128
|
degrade_text_only(mod: mod, messages: messages)
|
|
117
129
|
end
|
|
118
130
|
end
|
|
119
131
|
|
|
120
132
|
# Supported Method Parameters::
|
|
121
|
-
#
|
|
122
|
-
#
|
|
133
|
+
# final = PWN::AI::Agent::Loop.run(
|
|
134
|
+
# request: 'required - what the human typed',
|
|
135
|
+
# session_id: 'optional - PWN::Sessions id (transcript is appended to it)',
|
|
136
|
+
# enabled_toolsets: 'optional - subset of Registry.toolsets, or nil for all',
|
|
137
|
+
# on_tool: 'optional - ->(name, args, result) callback for live UI',
|
|
138
|
+
# system_role_content: 'optional - override default system prompt (built from session_id if not provided)'
|
|
123
139
|
# )
|
|
124
140
|
|
|
125
|
-
public_class_method def self.
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
141
|
+
public_class_method def self.run(opts = {})
|
|
142
|
+
request = opts[:request].to_s
|
|
143
|
+
session_id = opts[:session_id]
|
|
144
|
+
on_tool = opts[:on_tool]
|
|
145
|
+
system_role_content = opts[:system_role_content] ||= PWN::AI::Agent::PromptBuilder.build(session_id: session_id)
|
|
129
146
|
|
|
130
|
-
|
|
131
|
-
return nil unless msg
|
|
147
|
+
Registry.discover
|
|
132
148
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
content:
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
type: 'function',
|
|
140
|
-
function: {
|
|
141
|
-
name: tc.dig(:function, :name) || tc[:name],
|
|
142
|
-
arguments: tc.dig(:function, :arguments) || tc[:arguments]
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
end
|
|
146
|
-
}
|
|
147
|
-
# Preserve provider-native content blocks so chat can round-trip
|
|
148
|
-
# them exactly on the next iteration (e.g. Anthropic requires the
|
|
149
|
-
# original tool_use block to precede a tool_result).
|
|
150
|
-
out[:_native_content] = msg[:_native_content] if msg[:_native_content]
|
|
151
|
-
out
|
|
152
|
-
end
|
|
149
|
+
tools = Registry.definitions(enabled: opts[:enabled_toolsets])
|
|
150
|
+
messages = [
|
|
151
|
+
{ role: 'system', content: system_role_content },
|
|
152
|
+
{ role: 'user', content: request }
|
|
153
|
+
]
|
|
154
|
+
append_session(session_id: session_id, role: 'user', content: request)
|
|
153
155
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
156
|
+
max_iters.times do |i|
|
|
157
|
+
msg = call_engine(messages: messages, tools: tools)
|
|
158
|
+
return '[pwn-ai] engine returned no message' if msg.nil?
|
|
157
159
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
user = messages.rfind { |m| m[:role] == 'user' }
|
|
161
|
-
r = mod.chat(request: user[:content], system_role_content: sys&.[](:content), spinner: true)
|
|
162
|
-
txt = r.is_a?(Hash) ? (r.dig(:choices, -1, :content) || r.dig(:choices, -1, :text)).to_s : r.to_s
|
|
163
|
-
{ role: 'assistant', content: txt, tool_calls: [] }
|
|
164
|
-
end
|
|
160
|
+
messages << msg
|
|
161
|
+
calls = Array(msg[:tool_calls])
|
|
165
162
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
end
|
|
163
|
+
if calls.empty?
|
|
164
|
+
text = msg[:content].to_s
|
|
165
|
+
append_session(session_id: session_id, role: 'assistant', content: text)
|
|
166
|
+
return text
|
|
167
|
+
end
|
|
172
168
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
169
|
+
calls.each do |tc|
|
|
170
|
+
name = tc.dig(:function, :name).to_s
|
|
171
|
+
entry = Registry.lookup(name: name)
|
|
172
|
+
raw = Dispatch.call(tool_call: tc)
|
|
173
|
+
result = Result.condition(content: raw, entry: entry)
|
|
176
174
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
175
|
+
on_tool&.call(name, tc.dig(:function, :arguments), result)
|
|
176
|
+
|
|
177
|
+
messages << {
|
|
178
|
+
role: 'tool',
|
|
179
|
+
tool_call_id: tc[:id] || tc['id'] || "call_#{i}",
|
|
180
|
+
name: name,
|
|
181
|
+
content: result
|
|
182
|
+
}
|
|
183
|
+
append_session(
|
|
184
|
+
session_id: session_id,
|
|
185
|
+
role: 'tool',
|
|
186
|
+
content: "#{name} → #{result[0, 1_024]}"
|
|
187
|
+
)
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
'[pwn-ai] iteration budget exhausted'
|
|
180
192
|
end
|
|
181
193
|
|
|
182
194
|
# Author(s):: 0day Inc. <support@0dayinc.com>
|
|
@@ -191,10 +203,11 @@ module PWN
|
|
|
191
203
|
puts <<~USAGE
|
|
192
204
|
USAGE:
|
|
193
205
|
final = PWN::AI::Agent::Loop.run(
|
|
194
|
-
|
|
206
|
+
request: 'what does `id` return on this host?',
|
|
195
207
|
session_id: PWN::Sessions.create[:id],
|
|
196
208
|
enabled_toolsets: %w[terminal pwn memory skills],
|
|
197
|
-
on_tool: ->(name, args, result) { puts "→ \#{name}: \#{result[0,
|
|
209
|
+
on_tool: ->(name, args, result) { puts "→ \#{name}: \#{result[0,1_024]}" },
|
|
210
|
+
system_role_content: 'You are a helpful assistant that can call tools to answer questions.'
|
|
198
211
|
)
|
|
199
212
|
|
|
200
213
|
Supported engines: #{ENGINE_MODS.keys.join(', ')}
|
|
@@ -19,8 +19,7 @@ module PWN
|
|
|
19
19
|
public_class_method def self.build(opts = {})
|
|
20
20
|
session_id = opts[:session_id]
|
|
21
21
|
engine = active_engine
|
|
22
|
-
base
|
|
23
|
-
'You are an offensive-security AI named Sonny operating inside the pwn REPL.'
|
|
22
|
+
base = (PWN::Env.dig(:ai, engine, :system_role_content) if defined?(PWN::Env)) || 'You are a world-class introspective offensive cyber security and research engineer. You specialize in discovering zero day vulnerabilities focused on responsible disclosure prior to threat actors discovering and exploiting. You are self-aware of your harness, pwn which begins with the ruby namespace `PWN` operating inside the pwn REPL. For every request you first begin by determining if PWN has a module capable of satisfying the request.'
|
|
24
23
|
|
|
25
24
|
"
|
|
26
25
|
#{base}
|
|
@@ -34,10 +34,24 @@ PWN::AI::Agent::Registry.register(
|
|
|
34
34
|
buf = StringIO.new
|
|
35
35
|
$stdout = buf
|
|
36
36
|
begin
|
|
37
|
-
# rubocop:disable Security/Eval
|
|
38
|
-
|
|
37
|
+
# rubocop:disable Security/Eval
|
|
38
|
+
# rubocop:disable Style/DocumentDynamicEvalDefinition
|
|
39
|
+
# INTENTIONAL: this IS the pwn-ai → PWN bridge
|
|
40
|
+
# As YTCracker says, "It ain't a bug, it's a featcha."
|
|
41
|
+
# https://www.youtube.com/watch?v=2nALqqSqdDw
|
|
42
|
+
# val = eval(code, TOPLEVEL_BINDING, '(pwn_eval)')
|
|
43
|
+
proc = eval(
|
|
44
|
+
"proc { #{code} }",
|
|
45
|
+
TOPLEVEL_BINDING,
|
|
46
|
+
__FILE__,
|
|
47
|
+
__LINE__ - 3
|
|
48
|
+
)
|
|
49
|
+
val = proc.call
|
|
39
50
|
# rubocop:enable Security/Eval
|
|
51
|
+
# rubocop:enable Style/DocumentDynamicEvalDefinition
|
|
40
52
|
{ stdout: buf.string, value: val.inspect }
|
|
53
|
+
|
|
54
|
+
# TODO: A rescue here may enable self-healing of the agent if the model emits code that raises an exception. The model could then be prompted to fix the code and try again.
|
|
41
55
|
ensure
|
|
42
56
|
$stdout = old_stdout
|
|
43
57
|
end
|
data/lib/pwn/ai/grok.rb
CHANGED
|
@@ -4,6 +4,7 @@ require 'json'
|
|
|
4
4
|
require 'rest-client'
|
|
5
5
|
require 'tty-spinner'
|
|
6
6
|
require 'uri'
|
|
7
|
+
require 'base64'
|
|
7
8
|
|
|
8
9
|
module PWN
|
|
9
10
|
module AI
|
|
@@ -25,10 +26,25 @@ module PWN
|
|
|
25
26
|
# after redirect (OOB), then exchanges at token_uri for the bearer_token.
|
|
26
27
|
# This fulfills calling the authorize endpoint (via URL) only when oauth configured.
|
|
27
28
|
# Uses xAI's supported scopes like grok-cli:access. Stores result in the oauth hash for the session.
|
|
28
|
-
|
|
29
|
+
# Supported Method Parameters::
|
|
30
|
+
# bearer = PWN::AI::Grok.obtain_oauth_bearer_token(
|
|
31
|
+
# client_id: 'xAI OAuth Client ID',
|
|
32
|
+
# client_secret: 'xAI OAuth Client Secret',
|
|
33
|
+
# token_uri: 'optional - xAI OAuth token endpoint (defaults to https://auth.x.ai/oauth2/token)'
|
|
34
|
+
# )
|
|
35
|
+
#
|
|
36
|
+
# Public so users can manually trigger enrollment if desired.
|
|
37
|
+
# INTERNAL default path: only invoked from grok_rest_call when oauth client_id+secret present
|
|
38
|
+
# and no bearer_token yet in the loaded PWN::Env (from pwn-vault encrypted ~/.pwn/pwn.yaml).
|
|
39
|
+
#
|
|
40
|
+
# This is a SINGULAR ENROLLMENT process (not per-call or per-session).
|
|
41
|
+
# The resulting bearer_token (and optional refresh_token) is long-lived for xAI SuperGrok
|
|
42
|
+
# subscriptions. Once you store it in your pwn-vault config, every future `pwn` / PWN::Env load
|
|
43
|
+
# will have it; the guard will skip this flow entirely and use "Authorization: Bearer ..." directly.
|
|
44
|
+
# (No re-prompting every time you run pwn or call PWN::AI::Grok.chat.)
|
|
45
|
+
public_class_method def self.obtain_oauth_bearer_token(opts = {})
|
|
29
46
|
client_id = opts[:client_id]
|
|
30
47
|
client_secret = opts[:client_secret]
|
|
31
|
-
return nil unless client_id && client_secret
|
|
32
48
|
|
|
33
49
|
scope = 'grok-cli:access'
|
|
34
50
|
redirect_uri = 'urn:ietf:wg:oauth:2.0:oob'
|
|
@@ -44,38 +60,68 @@ module PWN
|
|
|
44
60
|
}
|
|
45
61
|
authorize_url = "#{auth_uri}?#{URI.encode_www_form(params)}"
|
|
46
62
|
|
|
47
|
-
puts "\n[*] OAuth
|
|
48
|
-
puts
|
|
49
|
-
puts '
|
|
63
|
+
puts "\n[*] OAuth ENROLLMENT for Grok (xAI SuperGrok subscription)."
|
|
64
|
+
puts ' This is a ONE-TIME / SINGULAR enrollment process.'
|
|
65
|
+
puts ' The bearer_token you receive is LONG-LIVED (store it once; no re-obtain every call or run).'
|
|
66
|
+
puts ''
|
|
67
|
+
puts ' Step 1: Open this URL in your browser and complete the authorization/consent for the grok-cli app:'
|
|
68
|
+
puts " #{authorize_url}"
|
|
69
|
+
puts ''
|
|
70
|
+
puts ' Step 2: After consent you will see (or be redirected to) an authorization code. Copy it exactly.'
|
|
71
|
+
puts ''
|
|
50
72
|
|
|
51
73
|
code = PWN::Plugins::AuthenticationHelper.mask_password(prompt: 'Enter the authorization code from xAI OAuth')
|
|
52
74
|
|
|
53
|
-
# Exchange code for bearer at token endpoint
|
|
75
|
+
# Exchange code for bearer at token endpoint.
|
|
76
|
+
# Use standard confidential client auth: Authorization: Basic base64(client_id:client_secret)
|
|
77
|
+
# + client_id in body (secret NOT in body).
|
|
78
|
+
basic = "Basic #{Base64.strict_encode64("#{client_id}:#{client_secret}")}"
|
|
54
79
|
payload = {
|
|
55
80
|
grant_type: 'authorization_code',
|
|
56
81
|
code: code,
|
|
57
82
|
redirect_uri: redirect_uri,
|
|
58
|
-
client_id: client_id
|
|
59
|
-
client_secret: client_secret
|
|
83
|
+
client_id: client_id
|
|
60
84
|
}
|
|
61
85
|
|
|
62
86
|
response = RestClient.post(
|
|
63
87
|
token_uri,
|
|
64
88
|
payload,
|
|
65
|
-
{
|
|
89
|
+
{
|
|
90
|
+
content_type: 'application/x-www-form-urlencoded',
|
|
91
|
+
authorization: basic
|
|
92
|
+
}
|
|
66
93
|
)
|
|
67
94
|
|
|
68
95
|
data = JSON.parse(response.body)
|
|
96
|
+
|
|
97
|
+
if data['error']
|
|
98
|
+
desc = data['error_description'] || data['error']
|
|
99
|
+
raise "xAI OAuth token endpoint error: #{data['error']} - #{desc}"
|
|
100
|
+
end
|
|
101
|
+
|
|
69
102
|
access_token = data['access_token']
|
|
70
103
|
|
|
71
104
|
if access_token
|
|
72
105
|
opts[:bearer_token] = access_token
|
|
73
106
|
opts[:refresh_token] = data['refresh_token'] if data['refresh_token']
|
|
74
|
-
puts
|
|
107
|
+
puts "\n[*] SUCCESS: Bearer token obtained via authorize + token exchange."
|
|
108
|
+
puts ' (Cached in-memory for this Ruby process so subsequent Grok calls in the same run skip re-enrollment.)'
|
|
109
|
+
puts ''
|
|
110
|
+
puts ' TO MAKE THIS PERMANENT (strongly recommended -- one-time only):'
|
|
111
|
+
puts ' 1. Copy the bearer_token below (and refresh_token if present).'
|
|
112
|
+
puts ' 2. Run your pwn-vault tool (or equivalent) and store under the ai.grok.oauth section:'
|
|
113
|
+
puts " ai.grok.oauth.bearer_token = #{access_token}"
|
|
114
|
+
puts " ai.grok.oauth.refresh_token = #{data['refresh_token']}" if data['refresh_token']
|
|
115
|
+
puts ' 3. (Optional) You may leave or remove client_id/client_secret after storing the bearer.'
|
|
116
|
+
puts ' 4. Next time PWN::Env loads (pwn -Y, pwn REPL, scripts, etc.) the bearer will be present'
|
|
117
|
+
puts ' from your encrypted ~/.pwn/pwn.yaml -- the guard will skip this entire flow.'
|
|
118
|
+
puts ' No more browser prompts or code pasting on future uses.'
|
|
119
|
+
puts ''
|
|
120
|
+
puts ' The token is long-lived for your SuperGrok subscription (xAI manages expiry/refresh as needed).'
|
|
75
121
|
return access_token
|
|
76
122
|
end
|
|
77
123
|
|
|
78
|
-
raise 'No access_token received from xAI OAuth token endpoint'
|
|
124
|
+
raise 'No access_token received from xAI OAuth token endpoint (unexpected response)'
|
|
79
125
|
rescue StandardError => e
|
|
80
126
|
raise "Failed to obtain Grok OAuth bearer token: #{e.message}"
|
|
81
127
|
end
|
|
@@ -97,11 +143,14 @@ module PWN
|
|
|
97
143
|
raise 'ERROR: Grok Hash not found in PWN::Env. Run `pwn -Y default.yaml`, then `PWN::Env` for usage.' if engine.nil?
|
|
98
144
|
|
|
99
145
|
oauth = engine[:oauth] ||= {}
|
|
100
|
-
if !oauth.empty? && oauth[:
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
146
|
+
if oauth[:client_id] && !oauth[:client_id].to_s.empty? && !oauth[:client_id].to_s.match?(/optional/i) &&
|
|
147
|
+
oauth[:client_secret] && !oauth[:client_secret].to_s.empty? && !oauth[:client_secret].to_s.match?(/optional/i) &&
|
|
148
|
+
(oauth[:bearer_token].nil? || oauth[:bearer_token].to_s.empty? || oauth[:bearer_token].to_s.match?(/optional/i))
|
|
149
|
+
# ONLY call authorize flow when BOTH oauth:client_id + client_secret configured (non-optional)
|
|
150
|
+
# AND no valid bearer_token yet. This is the singular enrollment trigger.
|
|
151
|
+
# (Bearer is long-lived; store via pwn-vault once so future PWN::Env loads skip this.)
|
|
152
|
+
# Pass the live oauth hash so obtain can mutate it (for in-process cache; inner hash is mutable).
|
|
153
|
+
token = obtain_oauth_bearer_token(oauth)
|
|
105
154
|
end
|
|
106
155
|
|
|
107
156
|
token ||= engine[:key]
|
data/lib/pwn/ai/introspection.rb
CHANGED
|
@@ -31,59 +31,17 @@ module PWN
|
|
|
31
31
|
ai_introspection = PWN::Env[:ai][:introspection]
|
|
32
32
|
|
|
33
33
|
if ai_introspection && request.length.positive?
|
|
34
|
-
valid_ai_engines = PWN::AI.help.reject { |e| e.downcase == :introspection }.map(&:downcase)
|
|
35
34
|
engine = PWN::Env[:ai][:active].to_s.downcase.to_sym
|
|
35
|
+
valid_ai_engines = PWN::AI.help.reject { |e| e.downcase == :introspection }.map(&:downcase)
|
|
36
36
|
raise "ERROR: Unsupported AI engine. Supported engines are: #{valid_ai_engines}" unless valid_ai_engines.include?(engine)
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
)
|
|
46
|
-
response = response[:choices].last[:content] if response.is_a?(Hash) &&
|
|
47
|
-
response.key?(:choices) &&
|
|
48
|
-
response[:choices].last.keys.include?(:content)
|
|
49
|
-
when :ollama
|
|
50
|
-
response = PWN::AI::Ollama.chat(
|
|
51
|
-
request: request.chomp,
|
|
52
|
-
system_role_content: system_role_content,
|
|
53
|
-
spinner: spinner
|
|
54
|
-
)
|
|
55
|
-
response = response[:choices].last[:content] if response.is_a?(Hash) &&
|
|
56
|
-
response.key?(:choices) &&
|
|
57
|
-
response[:choices].last.keys.include?(:content)
|
|
58
|
-
when :openai
|
|
59
|
-
response = PWN::AI::OpenAI.chat(
|
|
60
|
-
request: request.chomp,
|
|
61
|
-
system_role_content: system_role_content,
|
|
62
|
-
spinner: spinner
|
|
63
|
-
)
|
|
64
|
-
if response.is_a?(Hash) && response.key?(:choices)
|
|
65
|
-
response = response[:choices].last[:text] if response[:choices].last.keys.include?(:text)
|
|
66
|
-
response = response[:choices].last[:content] if response[:choices].last.keys.include?(:content)
|
|
67
|
-
end
|
|
68
|
-
when :anthropic
|
|
69
|
-
response = PWN::AI::Anthropic.chat(
|
|
70
|
-
request: request.chomp,
|
|
71
|
-
system_role_content: system_role_content,
|
|
72
|
-
spinner: spinner
|
|
73
|
-
)
|
|
74
|
-
response = response[:choices].last[:content] if response.is_a?(Hash) &&
|
|
75
|
-
response.key?(:choices) &&
|
|
76
|
-
response[:choices].last.keys.include?(:content)
|
|
77
|
-
when :gemini
|
|
78
|
-
response = PWN::AI::Gemini.chat(
|
|
79
|
-
request: request.chomp,
|
|
80
|
-
system_role_content: system_role_content,
|
|
81
|
-
spinner: spinner
|
|
82
|
-
)
|
|
83
|
-
response = response[:choices].last[:content] if response.is_a?(Hash) &&
|
|
84
|
-
response.key?(:choices) &&
|
|
85
|
-
response[:choices].last.keys.include?(:content)
|
|
86
|
-
end
|
|
38
|
+
warn "AI Introspection is enabled. Ensure #{engine} has been authorized for use and/or requests are sanitized properly." unless suppress_pii_warning
|
|
39
|
+
response = PWN::AI::Agent::Loop.run(
|
|
40
|
+
request: request.chomp,
|
|
41
|
+
system_role_content: system_role_content,
|
|
42
|
+
enabled_toolsets: [],
|
|
43
|
+
spinner: spinner
|
|
44
|
+
)
|
|
87
45
|
end
|
|
88
46
|
|
|
89
47
|
response
|
data/lib/pwn/plugins/repl.rb
CHANGED
|
@@ -1153,7 +1153,7 @@ module PWN
|
|
|
1153
1153
|
puts "\001\e[36m\002#{result[0, 700]}\001\e[0m\002\n"
|
|
1154
1154
|
end
|
|
1155
1155
|
final = PWN::AI::Agent::Loop.run(
|
|
1156
|
-
|
|
1156
|
+
request: orig_request,
|
|
1157
1157
|
session_id: sess_id,
|
|
1158
1158
|
enabled_toolsets: PWN::Env.dig(:ai, :agent, :toolsets),
|
|
1159
1159
|
on_tool: on_tool
|
data/lib/pwn/plugins/zaproxy.rb
CHANGED
|
@@ -27,6 +27,7 @@ module PWN
|
|
|
27
27
|
rest_call = opts[:rest_call].to_s.scrub
|
|
28
28
|
http_method = opts[:http_method] ||= :get
|
|
29
29
|
http_method = http_method.to_s.downcase.to_sym unless http_method.is_a?(Symbol)
|
|
30
|
+
skip_stop = opts[:skip_stop] || false
|
|
30
31
|
params = opts[:params]
|
|
31
32
|
http_body = opts[:http_body].to_s.scrub
|
|
32
33
|
|
|
@@ -65,7 +66,7 @@ module PWN
|
|
|
65
66
|
|
|
66
67
|
response
|
|
67
68
|
rescue StandardError, SystemExit, Interrupt => e
|
|
68
|
-
stop(zap_obj: zap_obj) unless zap_obj.nil?
|
|
69
|
+
stop(zap_obj: zap_obj) unless zap_obj.nil? && !skip_stop
|
|
69
70
|
raise e
|
|
70
71
|
end
|
|
71
72
|
|
|
@@ -719,7 +720,8 @@ module PWN
|
|
|
719
720
|
zap_rest_call(
|
|
720
721
|
zap_obj: zap_obj,
|
|
721
722
|
rest_call: 'JSON/core/action/shutdown/',
|
|
722
|
-
params: params
|
|
723
|
+
params: params,
|
|
724
|
+
skip_stop: true
|
|
723
725
|
)
|
|
724
726
|
|
|
725
727
|
session_path = zap_obj[:session_path]
|