pwn 0.5.562 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fcfdf05e72d9c3c650d02dfb875519d3a4d95591e1b32227355b316e6fc1a8a6
4
- data.tar.gz: 1af1dc3a3a01390e7b1ee57a5168ed42f58534b2565ebb94da9df120cd5dc21c
3
+ metadata.gz: 33e312831eb63249fad1a0f98b4386085255ee84188e76ddb1259ce9a5083e34
4
+ data.tar.gz: e61b59c15b896fbc3c5206e5e95a796d5e0f279d0c401b506b3fc075846426bf
5
5
  SHA512:
6
- metadata.gz: a7344c5b670d5fa5b93e1f93c3a0a79830a7d1f790b8276ebfe9e7de730ab6f06bfccbc8d43ed47827de7d0b5a2df295dde687ee8e4cc5f009e10935f83be4a9
7
- data.tar.gz: 84d8a15f6eedf53563a3c0261c3c34544afefb195778f70bdfbf9a2db360b8e31e69ae6b747f243e6410715a50d816250655832386f01fa254d824dc9163cbee
6
+ metadata.gz: e22d19452e6316c0aca8a4059b734302177eb03fe8bd59765fd0473fec096b3761dcc784fe0976241b21ab3e31349a2e7776a1ccccdb963edff63e24189308d8
7
+ data.tar.gz: ec98c1cc869f2e91b40cbc0be9ba05ae4c82ef5721e49dd959c3610d06dc1c9985b3580a464a1fc274a55762e27405140de52d096ce34f18fd74f19c87819b67
data/.rubocop.yml CHANGED
@@ -6,7 +6,7 @@ Layout/LineLength:
6
6
  Lint/UselessRescue:
7
7
  Enabled: false
8
8
  Metrics/AbcSize:
9
- Max: 563.8
9
+ Max: 720
10
10
  Metrics/BlockLength:
11
11
  Max: 292
12
12
  Metrics/BlockNesting:
data/.rubocop_todo.yml CHANGED
@@ -1,6 +1,6 @@
1
1
  # This configuration was generated by
2
2
  # `rubocop --auto-gen-config`
3
- # on 2026-02-27 17:49:20 UTC using RuboCop version 1.85.0.
3
+ # on 2026-06-05 17:51:34 UTC using RuboCop version 1.86.1.
4
4
  # The point is for the user to remove these configuration records
5
5
  # one by one as the offenses are removed from the code base.
6
6
  # Note that changes in the inspected code, or installation of new
@@ -36,7 +36,7 @@ Lint/ShadowedException:
36
36
  - 'lib/pwn/sdr/decoder/flex.rb'
37
37
  - 'lib/pwn/sdr/decoder/pocsag.rb'
38
38
 
39
- # Offense count: 301
39
+ # Offense count: 302
40
40
  # This cop supports safe autocorrection (--autocorrect).
41
41
  Lint/UselessAssignment:
42
42
  Enabled: false
@@ -66,9 +66,10 @@ Metrics/MethodLength:
66
66
  Exclude:
67
67
  - 'lib/pwn/banner/code_cave.rb'
68
68
 
69
- # Offense count: 7
69
+ # Offense count: 8
70
70
  Naming/AccessorMethodName:
71
71
  Exclude:
72
+ - 'lib/pwn/ai/anthropic.rb'
72
73
  - 'lib/pwn/ai/grok.rb'
73
74
  - 'lib/pwn/ai/ollama.rb'
74
75
  - 'lib/pwn/ai/open_ai.rb'
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 4.0.1
1
+ 4.0.5
data/Gemfile CHANGED
@@ -20,11 +20,11 @@ gem 'base32', '0.3.4'
20
20
  gem 'bitcoin-ruby', '0.0.20'
21
21
  gem 'brakeman', '8.0.4'
22
22
  gem 'bson', '5.2.0'
23
- gem 'bundler', '>=4.0.11'
23
+ gem 'bundler', '>=4.0.13'
24
24
  gem 'bundler-audit', '>=0.9.3'
25
25
  gem 'bunny', '3.1.0'
26
26
  gem 'colorize', '1.1.0'
27
- gem 'credit_card_validations', '8.0.0'
27
+ gem 'credit_card_validations', '9.0.0'
28
28
  gem 'curses', '1.6.0'
29
29
  gem 'diffy', '3.4.4'
30
30
  gem 'eventmachine', '1.2.7'
@@ -37,7 +37,7 @@ gem 'gdb', '1.0.0'
37
37
  gem 'gem-wrappers', '1.4.0'
38
38
  gem 'geocoder', '1.8.6'
39
39
  gem 'gist', '6.0.0'
40
- gem 'gruff', '0.30.0'
40
+ gem 'gruff', '0.32.0'
41
41
  gem 'htmlentities', '4.4.2'
42
42
  gem 'ipaddress', '0.8.3'
43
43
  gem 'jenkins_api_client2', '1.9.0'
@@ -45,14 +45,14 @@ gem 'js-beautify', '0.1.8'
45
45
  gem 'json', '>=2.13.2'
46
46
  gem 'jsonpath', '1.1.5'
47
47
  gem 'json_schemer', '2.5.0'
48
- gem 'jwt', '3.1.2'
48
+ gem 'jwt', '3.2.0'
49
49
  gem 'libusb', '0.7.2'
50
50
  gem 'luhn', '3.0.0'
51
51
  gem 'mail', '2.9.0'
52
- gem 'mcp', '0.14.0'
53
- gem 'meshtastic', '0.0.160'
54
- gem 'metasm', '1.0.5'
55
- gem 'mongo', '2.24.0'
52
+ gem 'mcp', '0.18.0'
53
+ gem 'meshtastic', '0.0.163'
54
+ gem 'metasm', '1.0.6'
55
+ gem 'mongo', '2.24.1'
56
56
  gem 'msfrpc-client', '1.1.2'
57
57
  gem 'netaddr', '2.0.6'
58
58
  gem 'net-ldap', '0.20.0'
@@ -74,27 +74,27 @@ gem 'pry-doc', '1.7.0'
74
74
  gem 'rake', '13.4.2'
75
75
  gem 'rb-readline', '0.5.5'
76
76
  gem 'rbvmomi2', '3.10.0'
77
- gem 'rdoc', '7.0.3'
77
+ gem 'rdoc', '7.0.4'
78
78
  gem 'rest-client', '2.1.0'
79
79
  gem 'rex', '2.0.13'
80
- gem 'rmagick', '6.3.0'
80
+ gem 'rmagick', '7.0.3'
81
81
  gem 'rqrcode', '3.2.0'
82
82
  gem 'rspec', '3.13.2'
83
83
  gem 'rtesseract', '3.1.4'
84
- gem 'rubocop', '1.86.1'
84
+ gem 'rubocop', '1.87.0'
85
85
  gem 'rubocop-rake', '0.7.1'
86
- gem 'rubocop-rspec', '3.9.0'
86
+ gem 'rubocop-rspec', '3.10.2'
87
87
  gem 'ruby-audio', '1.6.1'
88
88
  gem 'ruby-nmap', '1.0.3'
89
89
  gem 'ruby-saml', '1.18.1'
90
90
  gem 'rvm', '1.11.3.9'
91
- gem 'savon', '2.15.1'
92
- gem 'selenium-devtools', '0.147.0'
93
- gem 'selenium-webdriver', '4.43.0'
91
+ gem 'savon', '2.17.1'
92
+ gem 'selenium-devtools', '0.148.0'
93
+ gem 'selenium-webdriver', '4.44.0'
94
94
  gem 'slack-ruby-client', '3.1.0'
95
95
  gem 'socksify', '1.8.1'
96
96
  gem 'spreadsheet', '1.3.5'
97
- gem 'sqlite3', '2.9.3'
97
+ gem 'sqlite3', '2.9.5'
98
98
  gem 'thin', '2.0.1'
99
99
  gem 'tty-prompt', '0.23.1'
100
100
  gem 'tty-spinner', '0.9.3'
@@ -105,4 +105,4 @@ gem 'webrick', '1.9.2'
105
105
  gem 'whois', '6.0.3'
106
106
  gem 'whois-parser', '2.0.0'
107
107
  gem 'wicked_pdf', '2.8.2'
108
- gem 'yard', '0.9.43'
108
+ gem 'yard', '0.9.44'
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.562]:001 >>> PWN.help
40
+ pwn[v0.5.573]:001 >>> PWN.help
41
41
  ```
42
42
 
43
43
  [![Installing the pwn Security Automation Framework](https://raw.githubusercontent.com/0dayInc/pwn/master/documentation/pwn_install.png)](https://youtu.be/G7iLUY4FzsI)
@@ -48,21 +48,21 @@ pwn[v0.5.562]:001 >>> PWN.help
48
48
  It's wise to update pwn often as numerous versions are released/week:
49
49
  ```
50
50
  $ rvm list gemsets
51
- $ rvm use ruby-4.0.1@pwn
51
+ $ 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.562]:001 >>> PWN.help
55
+ pwn[v0.5.573]:001 >>> PWN.help
56
56
  ```
57
57
 
58
58
  If you're using a multi-user install of RVM do:
59
59
  ```
60
60
  $ rvm list gemsets
61
- $ rvm use ruby-4.0.1@pwn
61
+ $ 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.562]:001 >>> PWN.help
65
+ pwn[v0.5.573]: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/build_gem.sh CHANGED
@@ -1,17 +1,22 @@
1
1
  #!/bin/bash --login
2
+ set -eo pipefail
3
+ shopt -s nullglob
4
+ export rvmsudo_secure_path=1
5
+
2
6
  if [[ -d '/opt/pwn' ]]; then
3
7
  pwn_root='/opt/pwn'
4
8
  else
5
9
  pwn_root="${PWN_ROOT}"
6
10
  fi
7
11
 
8
- ls pkg/*.gem | while read previous_gems; do
9
- rvmsudo rm $previous_gems
12
+ for previous_gem in pkg/*.gem; do
13
+ rvmsudo rm "$previous_gem"
10
14
  done
11
15
  old_ruby_version=`cat ${pwn_root}/.ruby-version`
12
16
  # Default Strategy is to merge codebase
13
- rvmsudo git config pull.rebase false
14
- rvmsudo git pull
17
+ sudo chown -R $USER:$USER $pwn_root
18
+ git config pull.rebase false
19
+ git pull
15
20
  new_ruby_version=`cat ${pwn_root}/.ruby-version`
16
21
 
17
22
  rvmsudo gem update --system
@@ -19,13 +24,12 @@ rvmsudo gem update --system
19
24
  if [[ $old_ruby_version == $new_ruby_version ]]; then
20
25
  export rvmsudo_secure_path=1
21
26
  rvmsudo /bin/bash --login -c "cd ${pwn_root} && ./reinstall_gemset.sh"
22
- cd /tmp && cd $pwn_root
23
- rvmsudo rake
24
- rvmsudo rake install
25
- rvmsudo rake rerdoc
26
- rvmsudo gem rdoc --backtrace --rdoc --ri --overwrite -V pwn
27
+ cd /tmp && cd "$pwn_root"
28
+ rvmsudo /bin/bash --login -c "cd ${pwn_root} && bundle exec rake"
29
+ rvmsudo /bin/bash --login -c "cd ${pwn_root} && bundle exec rake install"
30
+ rvmsudo /bin/bash --login -c "cd ${pwn_root} && bundle exec rake rerdoc"
27
31
  echo "Invoking bundle-audit Gemfile Scanner..."
28
- rvmsudo bundle-audit
32
+ rvmsudo /bin/bash --login -c "cd ${pwn_root} && bundle exec bundle-audit"
29
33
  else
30
34
  cd $pwn_root && ./upgrade_ruby.sh $new_ruby_version $old_ruby_version
31
35
  fi
@@ -0,0 +1,27 @@
1
+ campaign:
2
+ id: github-collaborator-revoke
3
+ label: GitHub collaborator removal replay
4
+ target: https://github.com/acme/private-repo
5
+ change_event: remove_collaborator
6
+ notes: Capture access before and after collaborator removal.
7
+
8
+ actors:
9
+ - id: owner
10
+ label: Repository owner account
11
+ - id: revoked_user
12
+ label: User removed from repository
13
+
14
+ surfaces:
15
+ - id: repo_settings_page
16
+ label: Repository settings HTML page
17
+ - id: repo_metadata_api
18
+ label: Repository metadata API endpoint
19
+
20
+ checkpoints:
21
+ - pre_change
22
+ - post_change_t0
23
+ - post_change_tn
24
+
25
+ expected_denied_after:
26
+ - post_change_t0
27
+ - post_change_tn
@@ -0,0 +1,281 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'rest-client'
5
+ require 'tty-spinner'
6
+ require 'securerandom'
7
+
8
+ module PWN
9
+ module AI
10
+ # This plugin interacts with Anthropic's Claude API.
11
+ # It provides methods to list models, generate completions, and chat.
12
+ # API documentation: https://docs.anthropic.com/en/api
13
+ # Obtain an API key from https://console.anthropic.com/
14
+ module Anthropic
15
+ # Supported Method Parameters::
16
+ # anthropic_rest_call(
17
+ # token: 'required - anthropic api key',
18
+ # http_method: 'optional HTTP method (defaults to GET)',
19
+ # base_uri: 'optional base anthropic api URI (defaults to https://api.anthropic.com/v1)',
20
+ # rest_call: 'required rest call to make per the schema',
21
+ # params: 'optional params passed in the URI or HTTP Headers',
22
+ # http_body: 'optional HTTP body sent in HTTP methods that support it e.g. POST',
23
+ # timeout: 'optional timeout in seconds (defaults to 300)',
24
+ # spinner: 'optional - display spinner (defaults to false)'
25
+ # )
26
+
27
+ private_class_method def self.anthropic_rest_call(opts = {})
28
+ engine = PWN::Env[:ai][:anthropic]
29
+ raise 'ERROR: Anthropic Hash not found in PWN::Env. Run `pwn -Y default.yaml`, then `PWN::Env` for usage.' if engine.nil?
30
+
31
+ token = engine[:key] ||= PWN::Plugins::AuthenticationHelper.mask_password(prompt: 'Anthropic API Key')
32
+
33
+ http_method = if opts[:http_method].nil?
34
+ :get
35
+ else
36
+ opts[:http_method].to_s.scrub.to_sym
37
+ end
38
+
39
+ base_uri = engine[:base_uri] ||= 'https://api.anthropic.com/v1'
40
+ rest_call = opts[:rest_call].to_s.scrub
41
+ params = opts[:params]
42
+ headers = {
43
+ content_type: 'application/json; charset=UTF-8',
44
+ 'x-api-key': token,
45
+ 'anthropic-version': '2023-06-01'
46
+ }
47
+
48
+ http_body = opts[:http_body]
49
+ http_body ||= {}
50
+
51
+ timeout = opts[:timeout]
52
+ timeout ||= 300
53
+
54
+ spinner = opts[:spinner] || false
55
+
56
+ browser_obj = PWN::Plugins::TransparentBrowser.open(browser_type: :rest)
57
+ rest_client = browser_obj[:browser]::Request
58
+
59
+ if spinner
60
+ spin = TTY::Spinner.new(format: :dots)
61
+ spin.auto_spin
62
+ end
63
+
64
+ retry_count = 0
65
+ begin
66
+ case http_method
67
+ when :delete, :get
68
+ headers[:params] = params
69
+ response = rest_client.execute(
70
+ method: http_method,
71
+ url: "#{base_uri}/#{rest_call}",
72
+ headers: headers,
73
+ verify_ssl: false,
74
+ timeout: timeout
75
+ )
76
+
77
+ when :post
78
+ if http_body.key?(:multipart)
79
+ headers[:content_type] = 'multipart/form-data'
80
+
81
+ response = rest_client.execute(
82
+ method: http_method,
83
+ url: "#{base_uri}/#{rest_call}",
84
+ headers: headers,
85
+ payload: http_body,
86
+ verify_ssl: false,
87
+ timeout: timeout
88
+ )
89
+ else
90
+ response = rest_client.execute(
91
+ method: http_method,
92
+ url: "#{base_uri}/#{rest_call}",
93
+ headers: headers,
94
+ payload: http_body.to_json,
95
+ verify_ssl: false,
96
+ timeout: timeout
97
+ )
98
+ end
99
+ else
100
+ raise @@logger.error("Unsupported HTTP Method #{http_method} for #{self} Plugin")
101
+ end
102
+
103
+ response
104
+ rescue RestClient::TooManyRequests => e
105
+ retry_after = e.response.headers[:retry_after]&.to_i ||= (0.5 * (retry_count + 1))
106
+ sleep(retry_after + rand(0.3..5.0))
107
+ retry_count += 1
108
+
109
+ retry
110
+ end
111
+ rescue RestClient::ExceptionWithResponse => e
112
+ puts "ERROR: #{e.message}: #{e.response}"
113
+ rescue StandardError => e
114
+ case e.message
115
+ when '400 Bad Request', '404 Resource Not Found'
116
+ "#{e.message}: #{e.response}"
117
+ else
118
+ raise e
119
+ end
120
+ ensure
121
+ spin.stop if spinner
122
+ end
123
+
124
+ # Supported Method Parameters::
125
+ # models = PWN::AI::Anthropic.get_models
126
+
127
+ public_class_method def self.get_models
128
+ models = anthropic_rest_call(rest_call: 'models')
129
+
130
+ JSON.parse(models, symbolize_names: true)[:data]
131
+ rescue StandardError => e
132
+ raise e
133
+ end
134
+
135
+ # Supported Method Parameters::
136
+ # response = PWN::AI::Anthropic.chat(
137
+ # request: 'required - message to Anthropic',
138
+ # model: 'optional - model to use for text generation (defaults to PWN::Env[:ai][:anthropic][:model])',
139
+ # temp: 'optional - creative response float (defaults to PWN::Env[:ai][:anthropic][:temp])',
140
+ # system_role_content: 'optional - context to set up the model behavior for conversation (Default: PWN::Env[:ai][:anthropic][:system_role_content])',
141
+ # response_history: 'optional - pass response back in to have a conversation',
142
+ # speak_answer: 'optional speak answer using PWN::Plugins::Voice.text_to_speech (Default: nil)',
143
+ # timeout: 'optional timeout in seconds (defaults to 300)',
144
+ # spinner: 'optional - display spinner (defaults to false)'
145
+ # )
146
+
147
+ public_class_method def self.chat(opts = {})
148
+ engine = PWN::Env[:ai][:anthropic]
149
+ request = opts[:request]
150
+ max_prompt_length = engine[:max_prompt_length] ||= 200_000
151
+ request_trunc_idx = ((max_prompt_length - 1) / 3.36).floor
152
+ request = request[0..request_trunc_idx]
153
+
154
+ model = opts[:model] ||= engine[:model]
155
+ raise 'ERROR: Model is required. Call #get_models method for details' if model.nil?
156
+
157
+ temp = opts[:temp].to_f ||= engine[:temp].to_f
158
+ temp = 1 if temp.zero?
159
+
160
+ rest_call = 'messages'
161
+
162
+ response_history = opts[:response_history]
163
+
164
+ system_role_content = opts[:system_role_content] ||= engine[:system_role_content]
165
+ system_role_content = response_history[:choices].first[:content] if response_history && response_history[:choices]
166
+
167
+ system_role = {
168
+ role: 'system',
169
+ content: system_role_content
170
+ }
171
+
172
+ user_role = {
173
+ role: 'user',
174
+ content: request
175
+ }
176
+
177
+ response_history ||= { choices: [system_role] }
178
+
179
+ http_body = {
180
+ model: model,
181
+ max_tokens: 4096,
182
+ temperature: temp,
183
+ system: system_role_content,
184
+ messages: []
185
+ }
186
+
187
+ if response_history[:choices].length > 1
188
+ response_history[:choices][1..].each do |message|
189
+ next if message[:role] == 'system'
190
+
191
+ http_body[:messages].push(role: message[:role].to_s, content: message[:content].to_s)
192
+ end
193
+ end
194
+
195
+ http_body[:messages].push(role: 'user', content: request)
196
+
197
+ timeout = opts[:timeout]
198
+ spinner = opts[:spinner]
199
+
200
+ response = anthropic_rest_call(
201
+ http_method: :post,
202
+ rest_call: rest_call,
203
+ http_body: http_body,
204
+ timeout: timeout,
205
+ spinner: spinner
206
+ )
207
+
208
+ json_resp = JSON.parse(response, symbolize_names: true)
209
+ assistant_content = if json_resp[:content] && json_resp[:content].is_a?(Array) && json_resp[:content].first
210
+ json_resp[:content].first[:text]
211
+ else
212
+ ''
213
+ end
214
+ assistant_resp = {
215
+ role: 'assistant',
216
+ content: assistant_content
217
+ }
218
+
219
+ # Build choices for PWN compatibility: [system, ...history..., user, assistant]
220
+ json_resp[:choices] = [system_role] + http_body[:messages]
221
+ json_resp[:choices].push(assistant_resp)
222
+
223
+ # Ensure compatibility fields
224
+ json_resp[:id] ||= "msg_#{SecureRandom.hex(8)}"
225
+ json_resp[:object] ||= 'message'
226
+ json_resp[:model] ||= model
227
+
228
+ if json_resp[:usage].is_a?(Hash)
229
+ inp_tokens = json_resp[:usage][:input_tokens] || 0
230
+ out_tokens = json_resp[:usage][:output_tokens] || 0
231
+ json_resp[:usage][:total_tokens] = inp_tokens + out_tokens
232
+ else
233
+ json_resp[:usage] = { input_tokens: 0, output_tokens: 0, total_tokens: 0 }
234
+ end
235
+
236
+ speak_answer = true if opts[:speak_answer]
237
+
238
+ if speak_answer
239
+ answer = assistant_resp[:content]
240
+ text_path = "/tmp/#{SecureRandom.hex}.pwn_voice"
241
+ File.write(text_path, answer)
242
+ PWN::Plugins::Voice.text_to_speech(text_path: text_path)
243
+ File.unlink(text_path)
244
+ end
245
+
246
+ json_resp
247
+ rescue StandardError => e
248
+ raise e
249
+ end
250
+
251
+ # Author(s):: 0day Inc. <support@0dayinc.com>
252
+
253
+ public_class_method def self.authors
254
+ "AUTHOR(S):
255
+ 0day Inc. <support@0dayinc.com>
256
+ "
257
+ end
258
+
259
+ # Display Usage for this Module
260
+
261
+ public_class_method def self.help
262
+ puts "USAGE:
263
+ models = #{self}.get_models
264
+
265
+ response = #{self}.chat(
266
+ request: 'required - message to Anthropic',
267
+ model: 'optional - model to use for text generation (defaults to PWN::Env[:ai][:anthropic][:model])',
268
+ temp: 'optional - creative response float (defaults to PWN::Env[:ai][:anthropic][:temp])',
269
+ system_role_content: 'optional - context to set up the model behavior for conversation (Default: PWN::Env[:ai][:anthropic][:system_role_content])',
270
+ response_history: 'optional - pass response back in to have a conversation',
271
+ speak_answer: 'optional speak answer using PWN::Plugins::Voice.text_to_speech (Default: nil)',
272
+ timeout: 'optional - timeout in seconds (defaults to 300)',
273
+ spinner: 'optional - display spinner (defaults to false)'
274
+ )
275
+
276
+ #{self}.authors
277
+ "
278
+ end
279
+ end
280
+ end
281
+ end
data/lib/pwn/ai/grok.rb CHANGED
@@ -25,9 +25,14 @@ module PWN
25
25
 
26
26
  private_class_method def self.grok_rest_call(opts = {})
27
27
  engine = PWN::Env[:ai][:grok]
28
- raise 'ERROR: Jira Server Hash not found in PWN::Env. Run i`pwn -Y default.yaml`, then `PWN::Env` for usage.' if engine.nil?
29
-
30
- token = engine[:key] ||= PWN::Plugins::AuthenticationHelper.mask_password(prompt: 'Grok API Key')
28
+ raise 'ERROR: Grok Hash not found in PWN::Env. Run `pwn -Y default.yaml`, then `PWN::Env` for usage.' if engine.nil?
29
+
30
+ oauth = engine[:oauth] || {}
31
+ token = if oauth[:access_token] && !oauth[:access_token].to_s.empty? && !oauth[:access_token].to_s.match?(/optional/i)
32
+ oauth[:access_token]
33
+ else
34
+ engine[:key] ||= PWN::Plugins::AuthenticationHelper.mask_password(prompt: 'Grok API Key (or set oauth:access_token in pwn-vault for xAI SuperGrok subscriptions)')
35
+ end
31
36
 
32
37
  http_method = if opts[:http_method].nil?
33
38
  :get
@@ -65,6 +65,15 @@ module PWN
65
65
  response = response[:choices].last[:text] if response[:choices].last.keys.include?(:text)
66
66
  response = response[:choices].last[:content] if response[:choices].last.keys.include?(:content)
67
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)
68
77
  end
69
78
  end
70
79
 
@@ -98,8 +98,12 @@ module PWN
98
98
  end
99
99
  response
100
100
  rescue RestClient::TooManyRequests => e
101
- retry_after = e.response.headers[:retry_after]&.to_i ||= (0.5 * (retry_count + 1))
102
- sleep(retry_after + rand(0.3..5.0))
101
+ duration = 0
102
+ if e.response
103
+ retry_after = e.response.headers[:retry_after]&.to_i ||= (0.5 * (retry_count + 1))
104
+ duration = retry_after.to_i
105
+ end
106
+ sleep(duration + rand(0.3..5.0))
103
107
  retry_count += 1
104
108
 
105
109
  retry
data/lib/pwn/ai.rb CHANGED
@@ -6,6 +6,7 @@ module PWN
6
6
  # http://www.rubyinside.com/ruby-techniques-revealed-autoload-1652.html
7
7
  module AI
8
8
  autoload :Agent, 'pwn/ai/agent'
9
+ autoload :Anthropic, 'pwn/ai/anthropic'
9
10
  autoload :Grok, 'pwn/ai/grok'
10
11
  autoload :Introspection, 'pwn/ai/introspection'
11
12
  autoload :Ollama, 'pwn/ai/ollama'