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 +4 -4
- data/.rubocop.yml +1 -1
- data/.rubocop_todo.yml +4 -3
- data/.ruby-version +1 -1
- data/Gemfile +17 -17
- data/README.md +5 -5
- data/build_gem.sh +14 -10
- data/documentation/lifecycle_authz_replay.example.yaml +27 -0
- data/lib/pwn/ai/anthropic.rb +281 -0
- data/lib/pwn/ai/grok.rb +8 -3
- data/lib/pwn/ai/introspection.rb +9 -0
- data/lib/pwn/ai/open_ai.rb +6 -2
- data/lib/pwn/ai.rb +1 -0
- data/lib/pwn/bounty/lifecycle_authz_replay.rb +505 -0
- data/lib/pwn/bounty.rb +16 -0
- data/lib/pwn/config.rb +145 -7
- data/lib/pwn/cron.rb +210 -0
- data/lib/pwn/driver.rb +9 -0
- data/lib/pwn/memory.rb +136 -0
- data/lib/pwn/plugins/repl.rb +395 -50
- data/lib/pwn/plugins/xxd.rb +24 -0
- data/lib/pwn/sessions.rb +160 -0
- data/lib/pwn/version.rb +1 -1
- data/lib/pwn.rb +4 -0
- data/spec/lib/pwn/ai/anthropic_spec.rb +15 -0
- data/spec/lib/pwn/bounty/lifecycle_authz_replay_spec.rb +53 -0
- data/spec/lib/pwn/bounty_spec.rb +10 -0
- data/spec/lib/pwn/cron_spec.rb +15 -0
- data/spec/lib/pwn/memory_spec.rb +24 -0
- data/spec/lib/pwn/sessions_spec.rb +25 -0
- data/spec/smoke/lifecycle_authz_replay_smoke_test.rb +59 -0
- data/third_party/pwn_rdoc.jsonl +69 -2
- data/upgrade_pwn.sh +1 -1
- metadata +50 -36
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 33e312831eb63249fad1a0f98b4386085255ee84188e76ddb1259ce9a5083e34
|
|
4
|
+
data.tar.gz: e61b59c15b896fbc3c5206e5e95a796d5e0f279d0c401b506b3fc075846426bf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e22d19452e6316c0aca8a4059b734302177eb03fe8bd59765fd0473fec096b3761dcc784fe0976241b21ab3e31349a2e7776a1ccccdb963edff63e24189308d8
|
|
7
|
+
data.tar.gz: ec98c1cc869f2e91b40cbc0be9ba05ae4c82ef5721e49dd959c3610d06dc1c9985b3580a464a1fc274a55762e27405140de52d096ce34f18fd74f19c87819b67
|
data/.rubocop.yml
CHANGED
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-
|
|
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:
|
|
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:
|
|
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
|
+
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.
|
|
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', '
|
|
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.
|
|
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.
|
|
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.
|
|
53
|
-
gem 'meshtastic', '0.0.
|
|
54
|
-
gem 'metasm', '1.0.
|
|
55
|
-
gem 'mongo', '2.24.
|
|
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.
|
|
77
|
+
gem 'rdoc', '7.0.4'
|
|
78
78
|
gem 'rest-client', '2.1.0'
|
|
79
79
|
gem 'rex', '2.0.13'
|
|
80
|
-
gem 'rmagick', '
|
|
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.
|
|
84
|
+
gem 'rubocop', '1.87.0'
|
|
85
85
|
gem 'rubocop-rake', '0.7.1'
|
|
86
|
-
gem 'rubocop-rspec', '3.
|
|
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.
|
|
92
|
-
gem 'selenium-devtools', '0.
|
|
93
|
-
gem 'selenium-webdriver', '4.
|
|
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.
|
|
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.
|
|
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.
|
|
40
|
+
pwn[v0.5.573]:001 >>> PWN.help
|
|
41
41
|
```
|
|
42
42
|
|
|
43
43
|
[](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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
9
|
-
rvmsudo rm $
|
|
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
|
-
|
|
14
|
-
|
|
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:
|
|
29
|
-
|
|
30
|
-
|
|
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
|
data/lib/pwn/ai/introspection.rb
CHANGED
|
@@ -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
|
|
data/lib/pwn/ai/open_ai.rb
CHANGED
|
@@ -98,8 +98,12 @@ module PWN
|
|
|
98
98
|
end
|
|
99
99
|
response
|
|
100
100
|
rescue RestClient::TooManyRequests => e
|
|
101
|
-
|
|
102
|
-
|
|
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'
|