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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: da1f346a63c2a3b01cdb7ec600e6ae7a727ae4598d271c314282e8277f09a27d
4
- data.tar.gz: 8226717e5181b99a8d2640fc46d97b33d7f9904d01b62f57bb919b42d3b6275b
3
+ metadata.gz: 33e312831eb63249fad1a0f98b4386085255ee84188e76ddb1259ce9a5083e34
4
+ data.tar.gz: e61b59c15b896fbc3c5206e5e95a796d5e0f279d0c401b506b3fc075846426bf
5
5
  SHA512:
6
- metadata.gz: 5b7db3ff61f7a3c57271674712613d8501e10072f0a8442623f1ef9138d36e4444f90c3afd507f0b0946120e4e1c22393a6978c8d0d96c1aaeada080bcea0425
7
- data.tar.gz: b0d75d83a38592831c55a21b5f00fd9a362e2ad1c224afe72f27a9065ca060065b3a5425b82a059c669ca3770e8b61b9abf6454829361750412646ec34d6844b
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.10'
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,21 +45,21 @@ 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.13.0'
53
- gem 'meshtastic', '0.0.160'
54
- gem 'metasm', '1.0.5'
55
- gem 'mongo', '2.23.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'
59
59
  gem 'net-openvpn', '0.8.7'
60
60
  gem 'net-smtp', '0.5.1'
61
61
  gem 'nexpose', '7.3.0'
62
- gem 'nokogiri', '1.19.2'
62
+ gem 'nokogiri', '1.19.3'
63
63
  gem 'nokogiri-diff', '0.3.0'
64
64
  gem 'oily_png', '1.2.1'
65
65
  gem 'open3', '0.2.1'
@@ -73,28 +73,28 @@ gem 'pry', '0.16.0'
73
73
  gem 'pry-doc', '1.7.0'
74
74
  gem 'rake', '13.4.2'
75
75
  gem 'rb-readline', '0.5.5'
76
- gem 'rbvmomi2', '3.9.0'
77
- gem 'rdoc', '7.0.3'
76
+ gem 'rbvmomi2', '3.10.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', '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
- gem 'spreadsheet', '1.3.4'
97
- gem 'sqlite3', '2.9.3'
96
+ gem 'spreadsheet', '1.3.5'
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.561]: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.561]: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.561]: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.561]: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,37 @@
1
+ # Vulnerability Report Template
2
+
3
+ **Program:** [HackerOne Program Name]
4
+ **Report Date:** [YYYY-MM-DD]
5
+ **Severity:** [Critical/High/Medium/Low]
6
+
7
+ ## 1. Detailed finding description with technical depth and PoC when possible
8
+
9
+ [Insert deep technical analysis, affected endpoints, steps to reproduce, PoC code/requests here.]
10
+
11
+ ## 2. Business impact
12
+
13
+ [Business/reputational/financial consequences.]
14
+
15
+ ## 3. Remediation recommendations, including compensating controls / stop gaps
16
+
17
+ [Fix recommendations, WAF rules, logging, etc.]
18
+
19
+ ## 4. CVSS score, vector string, and first.org calculator URI
20
+
21
+ **CVSS v3.1 Score:** X.X (High)
22
+ **Vector:** AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N
23
+ **Calculator:** https://www.first.org/cvss/calculator/3.1#CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N
24
+
25
+ ## 5. CWE category, brief description, and CWE URI
26
+
27
+ **CWE-XXX:** [Name] - [Brief desc]
28
+ https://cwe.mitre.org/data/definitions/XXX.html
29
+
30
+ ## 6. Relevant NIST 800-53 control
31
+
32
+ **Control:** [e.g. SI-10, AC-6]
33
+ [Description and how it maps.]
34
+
35
+ ---
36
+
37
+ *Generated with PWN::AI::Agent::VulnGen*
@@ -1,18 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'fileutils'
4
+
3
5
  module PWN
4
6
  module AI
5
7
  module Agent
6
- # This module is an AI agent designed to analyze generic vulnerability descriptions and generate detailed security findings, including business impact, remediation recommendations, CVSS scoring, CWE categorization, and relevant NIST 800-53 controls. It leverages the PWN::AI::Introspection.reflect_on method to process the input request and produce comprehensive markdown-formatted findings.
8
+ # This module is an AI agent designed to analyze generic vulnerability descriptions and generate detailed security findings following the exact bug bounty writeup structure:
9
+ # 1. Detailed finding description with technical depth and PoC when possible
10
+ # 2. Business impact
11
+ # 3. Remediation recommendations, including compensating controls / stop gaps
12
+ # 4. CVSS score, vector string, and first.org calculator URI
13
+ # 5. CWE category, brief description, and CWE URI
14
+ # 6. Relevant NIST 800-53 control
15
+ # It leverages the PWN::AI::Introspection.reflect_on method. Defaults to Jira for existing workflow compatibility.
7
16
  module VulnGen
8
17
  # Supported Method Parameters::
9
18
  # ai_analysis = PWN::AI::Agent::VulnGen.analyze(
10
19
  # request: 'required - high level description of vulnerability discovered (e.g. "Discovered a SQLi vulnerability in /login"',
11
- # markup_type: 'optional - specify the type of markup to generate :jira|:markdown|:html|:confluence|:xml (default: :jira)'
20
+ # markup_type: 'optional - specify the type of markup to generate :jira|:markdown|:html|:confluence|:xml (default: :jira)',
21
+ # output_path: 'optional - path to save the generated markdown report'
12
22
  # )
13
23
 
14
24
  public_class_method def self.analyze(opts = {})
15
25
  request = opts[:request]
26
+ output_path = opts[:output_path]
16
27
  raise 'ERROR: request parameter is required' if request.nil? || request.empty?
17
28
 
18
29
  markup_type = opts[:markup_type] ||= :jira
@@ -34,28 +45,36 @@ module PWN
34
45
  end
35
46
 
36
47
  system_role_content = "
37
- _ALWAYS_ Generate #{markup} security findings for the message provided with the following content:
48
+ _ALWAYS_ Generate #{markup} security findings for the message provided using **EXACTLY** this structure and section headers:
38
49
 
39
- 1. Detailed Finding Description: This should be a deep, detailed technical description that should include exploit proof-of-concepts when possible.
50
+ 1. Detailed Finding Description: This should be a deep, detailed technical description that should include exploit proof-of-concepts when possible. The description should be technical in nature and provide enough information for a security engineer to understand the vulnerability and how it can be exploited. Code snippets should be included where applicable to demonstrate the vulnerability and potential exploit paths.
40
51
 
41
52
  2. Business Impact: This should describe, in business terms, the importance of fixing the issue. Reputational and/or financial impact should be considered for this section.
42
53
 
43
54
  3. Remediation Recommendations: Targeted towards technical engineers that can ascertain a reasonable approach to fix the vulnerability based upon common security remediation patterns. Be sure to consider compensating controls / stop gaps that can be implemented (e.g. WAF, additional logging, etc.) until such time the vulnerability can be fixed. Provide examples in cases where code fixes may be required.
44
55
 
45
- 4. CVSS Score (Severity), Base CVSS Vector string as /AV:`N|L|A|P`/AC:`L|H`/PR:`N|L|H`/UI:`N|R`/S:`U|C`/C:`N|L|H`/I:`N|L|H`/A:`N|L|H`, and first.org CVSS calculator URI as https://www.first.org/cvss/calculator/3-1#CVSS:3.1/AV:`N|L|A|P`/AC:`L|H`/PR:`N|L|H`/UI:`N|R`/S:`U|C`/C:`N|L|H`/I:`N|L|H`/A:`N|L|H`. The Vector string must be formatted like: `/AV:%s/AC:%s/PR:%s/UI:%s/S:%s/C:%s/I:%s/A:%s`. Ensure the score and severity aligns with the vector string calculation.
56
+ 4. CVSS Score (Severity), Base CVSS Vector string as /AV:`N|L|A|P`/AC:`L|H`/PR:`N|L|H`/UI:`N|R`/S:`U|C`/C:`N|L|H`/I:`N|L|H`/A:`N|L|H`, and first.org CVSS calculator URI as https://www.first.org/cvss/calculator/3-1#CVSS:3.1/AV:`N|L|A|P`/AC:`L|H`/PR:`N|L|H`/UI:`N|R`/S:`U|C`/C:`N|L|H`/I:`N|L|H`/A:`N|L|H`. The Vector string must be formatted like: `/AV:%s/AC:%s/PR:%s/UI:%s/S:%s/C:%s/I:%s/A:%s`. _Ensure the CVSS score and severity aligns with the vector string calculation._
46
57
 
47
58
  5. CWE Category, Brief CWE description, and CWE URI
48
59
 
49
60
  6. NIST 800-53 Security Control that is impacted by this vulnerability.
50
61
  "
51
62
 
52
- PWN::AI::Introspection.reflect_on(
63
+ analysis = PWN::AI::Introspection.reflect_on(
53
64
  system_role_content: system_role_content,
54
65
  request: request,
55
66
  suppress_pii_warning: true
56
67
  )
68
+
69
+ if output_path
70
+ FileUtils.mkdir_p(File.dirname(output_path))
71
+ File.write(output_path, analysis.to_s)
72
+ puts "\nVulnerability report written to: #{output_path}"
73
+ end
74
+
75
+ analysis
57
76
  rescue StandardError => e
58
- raise e.backtrace
77
+ raise e
59
78
  end
60
79
 
61
80
  # Author(s):: 0day Inc. <support@0dayinc.com>
@@ -72,7 +91,8 @@ module PWN
72
91
  puts "USAGE:
73
92
  ai_analysis = #{self}.analyze(
74
93
  request: 'required - high level description of vulnerability discovered (e.g. \"Discovered a SQLi vulnerability in /login\"',
75
- markup_type: 'optional - specify the type of markup to generate :jira|:markdown|:html|:confluence|:xml (default: :jira)'
94
+ markup_type: 'optional - specify the type of markup to generate :jira|:markdown|:html|:confluence|:xml (default: :jira)',
95
+ output_path: 'optional - full path to save the generated report as .md (e.g. /home/claw/reports/sqli-finding.md)'
76
96
  )
77
97
 
78
98
  #{self}.authors
@@ -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