pwn 0.5.330 → 0.5.332
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/Gemfile +2 -2
- data/README.md +3 -3
- data/lib/pwn/plugins/burp_suite.rb +81 -13
- data/lib/pwn/plugins/open_api.rb +410 -0
- data/lib/pwn/plugins.rb +1 -0
- data/lib/pwn/version.rb +1 -1
- data/spec/lib/pwn/plugins/open_api_spec.rb +15 -0
- data/third_party/pwn_rdoc.jsonl +5 -1
- metadata +7 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 416b3161579a8f088d76f1de382c47a802945491767e0e9db044e1daaa263a77
|
4
|
+
data.tar.gz: ae3acfd4171b49ae5675047c924f731f654ca705e20bd4213a9bf03ef643f433
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0374d5fa47a7eae2baca814efebf33f98f265d2824139b28ad6b400567002a00eee847b4d9f039aedbb0df3d737d7b6b1de649aa748e18d8a1d160fbe38d3179
|
7
|
+
data.tar.gz: da79785268a5a8bff16553f29b1473d7e3a4529a1c891736627b19ab1d1d13df1d904f2fac108b729a245b66b305886b7726f85d2854a1cabdaac27f299358b9
|
data/.rubocop.yml
CHANGED
data/Gemfile
CHANGED
@@ -41,7 +41,7 @@ gem 'htmlentities', '4.3.4'
|
|
41
41
|
gem 'ipaddress', '0.8.3'
|
42
42
|
gem 'jenkins_api_client2', '1.9.0'
|
43
43
|
gem 'js-beautify', '0.1.8'
|
44
|
-
gem 'json', '2.13.
|
44
|
+
gem 'json', '2.13.2'
|
45
45
|
gem 'jsonpath', '1.1.5'
|
46
46
|
gem 'jwt', '3.1.2'
|
47
47
|
gem 'libusb', '0.7.2'
|
@@ -68,7 +68,7 @@ gem 'ostruct', '0.6.3'
|
|
68
68
|
gem 'packetfu', '2.0.0'
|
69
69
|
gem 'packetgen', '4.1.0'
|
70
70
|
gem 'pdf-reader', '2.14.1'
|
71
|
-
gem 'pg', '1.
|
71
|
+
gem 'pg', '1.6.0'
|
72
72
|
gem 'pry', '0.15.2'
|
73
73
|
gem 'pry-doc', '1.6.0'
|
74
74
|
gem 'rake', '13.3.0'
|
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.329]:001 >>> PWN.help
|
41
41
|
```
|
42
42
|
|
43
43
|
[](https://youtu.be/G7iLUY4FzsI)
|
@@ -52,7 +52,7 @@ $ rvm use ruby-3.4.4@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.329]: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-3.4.4@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.329]: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:
|
@@ -194,32 +194,100 @@ module PWN
|
|
194
194
|
|
195
195
|
# Supported Method Parameters::
|
196
196
|
# json_sitemap = PWN::Plugins::BurpSuite.get_current_sitemap(
|
197
|
-
# burp_obj: 'required - burp_obj returned by #start method'
|
197
|
+
# burp_obj: 'required - burp_obj returned by #start method',
|
198
|
+
# target_url: 'optional - target URL to filter sitemap results (defaults to entire sitemap)'
|
198
199
|
# )
|
199
200
|
|
200
201
|
public_class_method def self.get_current_sitemap(opts = {})
|
201
202
|
burp_obj = opts[:burp_obj]
|
202
203
|
rest_browser = burp_obj[:rest_browser]
|
203
204
|
pwn_burp_api = burp_obj[:pwn_burp_api]
|
205
|
+
target_url = opts[:target_url]
|
204
206
|
|
205
|
-
sitemap = rest_browser.get(
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
# both of these values are Base64 encoded strings.
|
210
|
-
# We want to decode them in an array of hashes.
|
211
|
-
# json_sitemap.map do |site|
|
212
|
-
# site[:request] = Base64.decode64(site[:request]) if site[:request]
|
213
|
-
# site[:response] = Base64.decode64(site[:response]) if site[:response]
|
214
|
-
# end
|
207
|
+
sitemap = rest_browser.get(
|
208
|
+
"http://#{pwn_burp_api}/sitemap",
|
209
|
+
content_type: 'application/json; charset=UTF8'
|
210
|
+
)
|
215
211
|
|
216
|
-
# json_sitemap
|
217
212
|
JSON.parse(sitemap, symbolize_names: true)
|
218
213
|
rescue StandardError => e
|
219
214
|
stop(burp_obj: burp_obj) unless burp_obj.nil?
|
220
215
|
raise e
|
221
216
|
end
|
222
217
|
|
218
|
+
# Supported Method Parameters::
|
219
|
+
# json_sitemap = PWN::Plugins::BurpSuite.add_to_sitemap(
|
220
|
+
# burp_obj: 'required - burp_obj returned by #start method',
|
221
|
+
# url: 'required - URL to add to sitemap',
|
222
|
+
# method: 'required - HTTP method to use (e.g., GET, POST, PUT, DELETE)',
|
223
|
+
# req_headers: 'optional - array of headers to include in the request (defaults to [])',
|
224
|
+
# req_body: 'optional - body of the request (defaults to empty string)',
|
225
|
+
# highlight: 'optional - highlight color :none|:red|:orange|:yellow|:green|:cyan|:blue|:pink|:magenta|:gray (defaults to :none)',
|
226
|
+
# status_code: 'optional - HTTP status code to use in the response (defaults to 200)',
|
227
|
+
# resp_headers: 'optional - array of response headers to include in the response (defaults to [])',
|
228
|
+
# resp_body: 'optional - body of the response (defaults to empty JSON object "{}")',
|
229
|
+
# comment: 'optional - comment to add to the sitemap entry (defaults to empty string)'
|
230
|
+
# )
|
231
|
+
|
232
|
+
public_class_method def self.add_to_sitemap(opts = {})
|
233
|
+
burp_obj = opts[:burp_obj]
|
234
|
+
rest_browser = burp_obj[:rest_browser]
|
235
|
+
pwn_burp_api = burp_obj[:pwn_burp_api]
|
236
|
+
url = opts[:url]
|
237
|
+
raise ArgumentError, 'url parameter is required' if url.nil?
|
238
|
+
|
239
|
+
valid_methods = %w[GET POST PUT DELETE PATCH OPTIONS HEAD TRACE CONNECT]
|
240
|
+
method = opts[:method].to_s.upcase
|
241
|
+
raise ArgumentError, "Invalid HTTP method: #{method}" unless valid_methods.include?(method)
|
242
|
+
|
243
|
+
req_headers = opts[:req_headers] ||= []
|
244
|
+
req_body = opts[:body].to_s
|
245
|
+
valid_highlights = %i[none red orange yellow green cyan blue pink magenta gray]
|
246
|
+
highlight = opts[:highlight] ||= :none
|
247
|
+
raise ArgumentError, "Invalid highlight color: #{highlight}" unless valid_highlights.include?(highlight)
|
248
|
+
|
249
|
+
status_code = opts[:status_code] ||= 200
|
250
|
+
resp_headers = opts[:resp_headers] ||= []
|
251
|
+
resp_body = opts[:resp_body] ||= '{}'
|
252
|
+
comment = opts[:comment].to_s
|
253
|
+
|
254
|
+
protocol = URI.parse(url).scheme
|
255
|
+
host = URI.parse(url).host
|
256
|
+
path = URI.parse(url).path
|
257
|
+
port = URI.parse(url).port || (url.start_with?('https') ? 443 : 80)
|
258
|
+
|
259
|
+
sitemap_message = {
|
260
|
+
request: {
|
261
|
+
method: method,
|
262
|
+
url: url,
|
263
|
+
path: path,
|
264
|
+
headers: req_headers,
|
265
|
+
body: req_body
|
266
|
+
},
|
267
|
+
response: {
|
268
|
+
statusCode: status_code.to_i,
|
269
|
+
headers: resp_headers,
|
270
|
+
body: resp_body
|
271
|
+
},
|
272
|
+
http_service: {
|
273
|
+
protocol: protocol,
|
274
|
+
host: host,
|
275
|
+
port: port
|
276
|
+
},
|
277
|
+
highlight: highlight,
|
278
|
+
comment: comment
|
279
|
+
}
|
280
|
+
|
281
|
+
RestClient.post(
|
282
|
+
"#{pwn_burp_api}/sitemap",
|
283
|
+
sitemap_message.to_json,
|
284
|
+
content_type: 'application/json; charset=UTF8'
|
285
|
+
)
|
286
|
+
rescue StandardError => e
|
287
|
+
# stop(burp_obj: burp_obj) unless burp_obj.nil?
|
288
|
+
raise e
|
289
|
+
end
|
290
|
+
|
223
291
|
# Supported Method Parameters::
|
224
292
|
# json_in_scope = PWN::Plugins::BurpSuite.add_to_scope(
|
225
293
|
# burp_obj: 'required - burp_obj returned by #start method',
|
@@ -385,7 +453,7 @@ module PWN
|
|
385
453
|
rescue RestClient::BadRequest => e
|
386
454
|
puts e.response
|
387
455
|
rescue StandardError => e
|
388
|
-
|
456
|
+
stop(burp_obj: burp_obj) unless burp_obj.nil?
|
389
457
|
raise e
|
390
458
|
end
|
391
459
|
|
@@ -0,0 +1,410 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
require 'json'
|
5
|
+
require 'uri'
|
6
|
+
require 'fileutils'
|
7
|
+
|
8
|
+
module PWN
|
9
|
+
module Plugins
|
10
|
+
# Plugins to interact with OpenAPI specifications,
|
11
|
+
# while automatically resolving schema dependencies.
|
12
|
+
module OpenAPI
|
13
|
+
# Supported Method Parameters:
|
14
|
+
# openapi_spec = PWN::Plugins::OpenAPI.import_spec(
|
15
|
+
# spec_paths: 'required - array of OpenAPI file paths to merge into a the output_json_path',
|
16
|
+
# base_url: 'required - base URL for OpenAPI endpoints (e.g., http://fqdn.com)',
|
17
|
+
# output_json_path: 'optional - path to save the merged OpenAPI JSON file (e.g., /path/to/merged_openapi.json)'
|
18
|
+
# )
|
19
|
+
def self.import_spec(opts = {})
|
20
|
+
spec_paths = opts[:spec_paths] ||= []
|
21
|
+
raise ArgumentError, 'spec_paths must be a non-empty array' if spec_paths.empty?
|
22
|
+
|
23
|
+
base_url = opts[:base_url]
|
24
|
+
raise ArgumentError, 'base_url is required' if base_url.nil? || base_url.empty?
|
25
|
+
|
26
|
+
# Normalize base_url to ensure it's an absolute URL
|
27
|
+
normalized_base_url = normalize_url(base_url)
|
28
|
+
output_json_path = opts[:output_json_path]
|
29
|
+
|
30
|
+
begin
|
31
|
+
# Load and parse all OpenAPI files
|
32
|
+
specs = {}
|
33
|
+
spec_paths.each do |path|
|
34
|
+
raise Errno::ENOENT, "OpenAPI file not found: #{path}" unless File.exist?(path)
|
35
|
+
|
36
|
+
begin
|
37
|
+
case File.extname(path).downcase
|
38
|
+
when '.yaml', '.yml'
|
39
|
+
specs[path] = YAML.load_file(path, permitted_classes: [Symbol, Date, Time])
|
40
|
+
when '.json'
|
41
|
+
specs[path] = JSON.parse(File.read(path))
|
42
|
+
else
|
43
|
+
raise "Unsupported file type: #{path} - only .yaml, .yml, and .json files"
|
44
|
+
end
|
45
|
+
rescue YAML::SyntaxError, JSON::ParserError => e
|
46
|
+
raise "Error parsing OpenAPI file #{path}: #{e.message}"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Determine dependencies based on $ref
|
51
|
+
dependencies = {}
|
52
|
+
specs.each do |path, spec|
|
53
|
+
dependencies[path] = []
|
54
|
+
refs = extract_refs(spec: spec, spec_paths: spec_paths)
|
55
|
+
refs.each do |ref|
|
56
|
+
dep_path = resolve_ref_path(ref, spec_paths, referencing_file: path)
|
57
|
+
dependencies[path] << dep_path if specs.key?(dep_path) && dep_path != path
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Sort files by dependencies (topological sort with cycle detection)
|
62
|
+
ordered_paths, cycle_info = topological_sort(dependencies: dependencies)
|
63
|
+
if cycle_info
|
64
|
+
puts "Warning: Cyclic dependencies detected: #{cycle_info.join(' -> ')}. Processing files in provided order."
|
65
|
+
ordered_paths = spec_paths
|
66
|
+
end
|
67
|
+
|
68
|
+
# Merge OpenAPI specs into a single specification
|
69
|
+
merged_spec = {
|
70
|
+
'openapi' => '3.0.3', # Default to OpenAPI 3.0.3
|
71
|
+
'info' => {},
|
72
|
+
'servers' => [{ 'url' => normalized_base_url }],
|
73
|
+
'paths' => {},
|
74
|
+
'components' => {},
|
75
|
+
'tags' => [],
|
76
|
+
'security' => []
|
77
|
+
}
|
78
|
+
|
79
|
+
ordered_paths.each do |path|
|
80
|
+
spec = specs[path]
|
81
|
+
unless spec.is_a?(Hash)
|
82
|
+
puts "Skipping #{path}: Invalid OpenAPI specification"
|
83
|
+
next
|
84
|
+
end
|
85
|
+
|
86
|
+
# Resolve external $ref references
|
87
|
+
resolved_spec = resolve_refs(spec: spec, specs: specs, spec_paths: spec_paths, referencing_file: path)
|
88
|
+
|
89
|
+
# Validate and fix path parameters
|
90
|
+
resolved_spec['paths'] = validate_path_parameters(resolved_spec['paths'], path) if resolved_spec['paths'].is_a?(Hash)
|
91
|
+
|
92
|
+
# Merge 'openapi' version
|
93
|
+
merged_spec['openapi'] = resolved_spec['openapi'] if resolved_spec['openapi'] && (resolved_spec['openapi'] > merged_spec['openapi'])
|
94
|
+
|
95
|
+
# Merge 'info'
|
96
|
+
merged_spec['info'] = deep_merge(merged_spec['info'], resolved_spec['info']) if resolved_spec['info'].is_a?(Hash)
|
97
|
+
|
98
|
+
# Merge 'servers'
|
99
|
+
if resolved_spec['servers']
|
100
|
+
servers = resolved_spec['servers'].is_a?(Array) ? resolved_spec['servers'] : [resolved_spec['servers']]
|
101
|
+
servers.each do |server|
|
102
|
+
server_url = server.is_a?(Hash) ? server['url'] : server
|
103
|
+
next unless server_url.is_a?(String)
|
104
|
+
|
105
|
+
absolute_url = normalize_url(server_url, base_url: normalized_base_url)
|
106
|
+
server_obj = server.is_a?(Hash) ? server.merge('url' => absolute_url) : { 'url' => absolute_url }
|
107
|
+
merged_spec['servers'] << server_obj unless merged_spec['servers'].any? { |s| s['url'] == absolute_url }
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Merge 'paths'
|
112
|
+
if resolved_spec['paths'].is_a?(Hash)
|
113
|
+
merged_spec['paths'].merge!(resolved_spec['paths']) do |api_endpoint, _existing, new|
|
114
|
+
puts "Warning: Path '#{api_endpoint}' in #{path} conflicts with existing path. Overwriting."
|
115
|
+
new
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Merge 'components'
|
120
|
+
merged_spec['components'] = deep_merge(merged_spec['components'], resolved_spec['components']) if resolved_spec['components'].is_a?(Hash)
|
121
|
+
|
122
|
+
# Merge 'tags'
|
123
|
+
next unless resolved_spec['tags'].is_a?(Array)
|
124
|
+
|
125
|
+
resolved_spec['tags'].each do |tag|
|
126
|
+
merged_spec['tags'] << tag unless merged_spec['tags'].include?(tag)
|
127
|
+
end
|
128
|
+
|
129
|
+
# Merge 'security'
|
130
|
+
next unless resolved_spec['security'].is_a?(Array)
|
131
|
+
|
132
|
+
resolved_spec['security'].each do |security|
|
133
|
+
merged_spec['security'] << security unless merged_spec['security'].include?(security)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# Ensure at least one valid server URL
|
138
|
+
if merged_spec['servers'].empty?
|
139
|
+
merged_spec['servers'] = [{ 'url' => normalized_base_url }]
|
140
|
+
puts "Warning: No valid server URLs found in specs. Using base_url: #{normalized_base_url}"
|
141
|
+
end
|
142
|
+
|
143
|
+
# Write merged spec to JSON file if provided
|
144
|
+
if output_json_path
|
145
|
+
FileUtils.mkdir_p(File.dirname(output_json_path))
|
146
|
+
File.write(output_json_path, JSON.pretty_generate(merged_spec))
|
147
|
+
puts "Merged OpenAPI specification written to: #{output_json_path}"
|
148
|
+
end
|
149
|
+
|
150
|
+
{ individual_specs: specs, merged_spec: merged_spec }
|
151
|
+
rescue Errno::ENOENT => e
|
152
|
+
raise "Error accessing file: #{e.message}"
|
153
|
+
rescue StandardError => e
|
154
|
+
raise "Unexpected error: #{e.message}"
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
# Validates and fixes path parameters in paths object
|
159
|
+
private_class_method def self.validate_path_parameters(paths, file_path)
|
160
|
+
paths.transform_values do |path_item|
|
161
|
+
next path_item unless path_item.is_a?(Hash)
|
162
|
+
|
163
|
+
# Extract path parameters from the endpoint
|
164
|
+
path_params = path_item['parameters']&.select { |p| p['in'] == 'path' }&.map { |p| p['name'] } || []
|
165
|
+
endpoint = path_item['$ref'] || path_item.keys.join('/')
|
166
|
+
path_item.each_value do |operation|
|
167
|
+
next unless operation.is_a?(Hash)
|
168
|
+
|
169
|
+
# Find path parameters in the endpoint URL
|
170
|
+
required_params = endpoint.scan(/\{([^}]+)\}/).flatten
|
171
|
+
operation_params = operation['parameters']&.select { |p| p['in'] == 'path' }&.map { |p| p['name'] } || []
|
172
|
+
|
173
|
+
# Check for missing path parameters
|
174
|
+
next if (missing_params = required_params - (path_params + operation_params)).empty?
|
175
|
+
|
176
|
+
puts "Warning: In #{file_path}, path '#{endpoint}' has undeclared path parameters: #{missing_params.join(', ')}. Adding default definitions."
|
177
|
+
operation['parameters'] ||= []
|
178
|
+
missing_params.each do |param|
|
179
|
+
operation['parameters'] << {
|
180
|
+
'name' => param,
|
181
|
+
'in' => 'path',
|
182
|
+
'required' => true,
|
183
|
+
'schema' => { 'type' => 'string' }
|
184
|
+
}
|
185
|
+
end
|
186
|
+
end
|
187
|
+
path_item
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
# Normalizes URLs to absolute form
|
192
|
+
private_class_method def self.normalize_url(url, base_url: nil)
|
193
|
+
return url if url.nil? || url.empty?
|
194
|
+
|
195
|
+
begin
|
196
|
+
uri = URI.parse(url)
|
197
|
+
return uri.to_s if uri.absolute? && uri.scheme && uri.host
|
198
|
+
|
199
|
+
# If no base_url provided, use a default
|
200
|
+
base_uri = if base_url && !base_url.empty?
|
201
|
+
URI.parse(normalize_url(base_url))
|
202
|
+
else
|
203
|
+
URI.parse('http://localhost')
|
204
|
+
end
|
205
|
+
|
206
|
+
# Handle relative URLs
|
207
|
+
if url.start_with?('/')
|
208
|
+
# Absolute path relative to base_url
|
209
|
+
uri = base_uri.dup
|
210
|
+
uri.path = url
|
211
|
+
uri.query = nil
|
212
|
+
uri.fragment = nil
|
213
|
+
else
|
214
|
+
# Relative path
|
215
|
+
uri = base_uri.merge(url)
|
216
|
+
end
|
217
|
+
uri.to_s
|
218
|
+
rescue URI::InvalidURIError => e
|
219
|
+
puts "Warning: Invalid URL '#{url}' - using base_url or default: #{base_url || 'http://localhost'}"
|
220
|
+
base_url || 'http://localhost'
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
# Resolves $ref references using spec_paths
|
225
|
+
private_class_method def self.resolve_refs(spec:, specs:, spec_paths:, referencing_file:)
|
226
|
+
case spec
|
227
|
+
when Hash
|
228
|
+
resolved = {}
|
229
|
+
spec.each do |key, value|
|
230
|
+
next resolved[key] = resolve_refs(spec: value, specs: specs, spec_paths: spec_paths, referencing_file: referencing_file) unless key == '$ref' && value.is_a?(String)
|
231
|
+
|
232
|
+
ref_path, json_pointer = value.split('#', 2)
|
233
|
+
json_pointer ||= ''
|
234
|
+
matched_path = resolve_ref_path(ref_path, spec_paths, referencing_file: referencing_file)
|
235
|
+
|
236
|
+
unless specs.key?(matched_path)
|
237
|
+
puts "Warning: Unable to load RELATIVE ref: #{ref_path} from #{referencing_file} (no match in spec_paths)"
|
238
|
+
begin
|
239
|
+
# Attempt to load the file if it exists
|
240
|
+
unless File.exist?(ref_path)
|
241
|
+
puts "Warning: File #{ref_path} does not exist on filesystem"
|
242
|
+
return value
|
243
|
+
end
|
244
|
+
case File.extname(ref_path).downcase
|
245
|
+
when '.yaml', '.yml'
|
246
|
+
specs[ref_path] = YAML.load_file(ref_path, permitted_classes: [Symbol, Date, Time])
|
247
|
+
spec_paths << ref_path unless spec_paths.include?(ref_path)
|
248
|
+
when '.json'
|
249
|
+
specs[ref_path] = JSON.parse(File.read(ref_path))
|
250
|
+
spec_paths << ref_path unless spec_paths.include?(ref_path)
|
251
|
+
else
|
252
|
+
puts "Warning: Unsupported file type for #{ref_path}"
|
253
|
+
return value
|
254
|
+
end
|
255
|
+
rescue StandardError => e
|
256
|
+
puts "Warning: Failed to load #{ref_path}: #{e.message}"
|
257
|
+
return value
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
ref_spec = specs[matched_path]
|
262
|
+
resolved[key] = if json_pointer.empty?
|
263
|
+
resolve_refs(spec: ref_spec, specs: specs, spec_paths: spec_paths, referencing_file: matched_path)
|
264
|
+
else
|
265
|
+
pointer_parts = json_pointer.split('/').reject(&:empty?)
|
266
|
+
target = ref_spec
|
267
|
+
pointer_parts.each do |part|
|
268
|
+
target = target[part] if target.is_a?(Hash) || target.is_a?(Array)
|
269
|
+
break unless target
|
270
|
+
end
|
271
|
+
if target
|
272
|
+
resolve_refs(spec: target, specs: specs, spec_paths: spec_paths, referencing_file: matched_path)
|
273
|
+
else
|
274
|
+
puts "Warning: Invalid JSON pointer #{json_pointer} in #{matched_path} from #{referencing_file}"
|
275
|
+
value
|
276
|
+
end
|
277
|
+
end
|
278
|
+
end
|
279
|
+
resolved
|
280
|
+
when Array
|
281
|
+
spec.map { |item| resolve_refs(spec: item, specs: specs, spec_paths: spec_paths, referencing_file: referencing_file) }
|
282
|
+
else
|
283
|
+
spec
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
# Resolves a $ref path by matching against spec_paths
|
288
|
+
private_class_method def self.resolve_ref_path(ref, spec_paths, referencing_file:)
|
289
|
+
# Remove 'file://' prefix if present
|
290
|
+
ref = ref.sub('file://', '') if ref.start_with?('file://')
|
291
|
+
|
292
|
+
# If ref is an HTTP/HTTPS URL, return it unchanged
|
293
|
+
return ref if ref.start_with?('http://', 'https://')
|
294
|
+
|
295
|
+
# Normalize ref by removing leading './' or '/' for matching
|
296
|
+
normalized_ref = ref.sub(%r{^\./}, '').sub(%r{^/}, '')
|
297
|
+
|
298
|
+
# Check if ref matches any path in spec_paths
|
299
|
+
spec_paths.each do |path|
|
300
|
+
normalized_path = path.sub(%r{^\./}, '').sub(%r{^/}, '')
|
301
|
+
return path if normalized_path == normalized_ref || File.basename(normalized_path) == File.basename(normalized_ref)
|
302
|
+
end
|
303
|
+
|
304
|
+
# If no match, return the original ref to allow fallback loading
|
305
|
+
puts "Warning: Could not resolve $ref '#{ref}' from #{referencing_file} to any spec_paths entry"
|
306
|
+
ref
|
307
|
+
end
|
308
|
+
|
309
|
+
# Deep merges two hashes
|
310
|
+
private_class_method def self.deep_merge(hash1, hash2)
|
311
|
+
hash1.merge(hash2) do |_key, old_val, new_val|
|
312
|
+
if old_val.is_a?(Hash) && new_val.is_a?(Hash)
|
313
|
+
deep_merge(old_val, new_val)
|
314
|
+
elsif old_val.is_a?(Array) && new_val.is_a?(Array)
|
315
|
+
(old_val + new_val).uniq
|
316
|
+
else
|
317
|
+
new_val || old_val
|
318
|
+
end
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
# Extracts $ref references and matches against spec_paths
|
323
|
+
private_class_method def self.extract_refs(opts = {})
|
324
|
+
spec = opts[:spec]
|
325
|
+
spec_paths = opts[:spec_paths]
|
326
|
+
refs = opts[:refs] ||= Set.new
|
327
|
+
case spec
|
328
|
+
when Hash
|
329
|
+
spec.each do |key, value|
|
330
|
+
if key == '$ref' && value.is_a?(String)
|
331
|
+
ref_path = value.split('#', 2).first
|
332
|
+
resolved_path = resolve_ref_path(ref_path, spec_paths, referencing_file: nil)
|
333
|
+
refs << resolved_path unless ref_path.start_with?('http://', 'https://')
|
334
|
+
end
|
335
|
+
extract_refs(spec: value, spec_paths: spec_paths, refs: refs)
|
336
|
+
end
|
337
|
+
when Array
|
338
|
+
spec.each { |item| extract_refs(spec: item, spec_paths: spec_paths, refs: refs) }
|
339
|
+
end
|
340
|
+
refs
|
341
|
+
end
|
342
|
+
|
343
|
+
# Depth-first search for topological sort with cycle detection
|
344
|
+
# rubocop:disable Metrics/ParameterLists
|
345
|
+
private_class_method def self.dfs(node, dependencies, visited, temp, result, path)
|
346
|
+
if temp.include?(node)
|
347
|
+
path << node
|
348
|
+
cycle_start = path.index(node)
|
349
|
+
cycle = path[cycle_start..-1]
|
350
|
+
return cycle # Return the cycle path
|
351
|
+
end
|
352
|
+
|
353
|
+
unless visited.include?(node)
|
354
|
+
temp.add(node)
|
355
|
+
path << node
|
356
|
+
dependencies[node]&.each do |dep|
|
357
|
+
cycle = dfs(dep, dependencies, visited, temp, result, path)
|
358
|
+
return cycle if cycle # Propagate cycle if found
|
359
|
+
end
|
360
|
+
visited.add(node)
|
361
|
+
temp.delete(node)
|
362
|
+
result << node
|
363
|
+
path.pop
|
364
|
+
end
|
365
|
+
nil # No cycle found
|
366
|
+
end
|
367
|
+
# rubocop:enable Metrics/ParameterLists
|
368
|
+
|
369
|
+
# Topological sort for dependency resolution
|
370
|
+
private_class_method def self.topological_sort(dependencies:)
|
371
|
+
result = []
|
372
|
+
visited = Set.new
|
373
|
+
temp = Set.new
|
374
|
+
path = []
|
375
|
+
|
376
|
+
cycle = nil
|
377
|
+
dependencies.each_key do |node|
|
378
|
+
next if visited.include?(node)
|
379
|
+
|
380
|
+
cycle = dfs(node, dependencies, visited, temp, result, path)
|
381
|
+
break if cycle
|
382
|
+
end
|
383
|
+
|
384
|
+
if cycle
|
385
|
+
[result.reverse, cycle]
|
386
|
+
else
|
387
|
+
[result.reverse, nil]
|
388
|
+
end
|
389
|
+
end
|
390
|
+
|
391
|
+
public_class_method def self.authors
|
392
|
+
"AUTHOR(S):
|
393
|
+
0day Inc. <support@0dayinc.com>
|
394
|
+
"
|
395
|
+
end
|
396
|
+
|
397
|
+
public_class_method def self.help
|
398
|
+
puts "USAGE:
|
399
|
+
openapi_spec = #{self}.import_spec(
|
400
|
+
spec_paths: 'required - array of OpenAPI file paths to merge into the output_json_path',
|
401
|
+
base_url: 'required - base URL to use for OpenAPI endpoints (e.g. http://fqdn.com)',
|
402
|
+
output_json_path: 'optional - path to save the merged OpenAPI JSON file (e.g., /path/to/merged_openapi.json)'
|
403
|
+
)
|
404
|
+
|
405
|
+
#{self}.authors
|
406
|
+
"
|
407
|
+
end
|
408
|
+
end
|
409
|
+
end
|
410
|
+
end
|
data/lib/pwn/plugins.rb
CHANGED
@@ -49,6 +49,7 @@ module PWN
|
|
49
49
|
autoload :OCR, 'pwn/plugins/ocr'
|
50
50
|
autoload :Ollama, 'pwn/plugins/ollama'
|
51
51
|
autoload :OpenAI, 'pwn/plugins/open_ai'
|
52
|
+
autoload :OpenAPI, 'pwn/plugins/open_api'
|
52
53
|
autoload :OpenVAS, 'pwn/plugins/openvas'
|
53
54
|
autoload :OwaspZap, 'pwn/plugins/owasp_zap'
|
54
55
|
autoload :Packet, 'pwn/plugins/packet'
|
data/lib/pwn/version.rb
CHANGED
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe PWN::Plugins::OpenAPI do
|
6
|
+
it 'should display information for authors' do
|
7
|
+
authors_response = PWN::Plugins::OpenAPI
|
8
|
+
expect(authors_response).to respond_to :authors
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'should display information for existing help method' do
|
12
|
+
help_response = PWN::Plugins::OpenAPI
|
13
|
+
expect(help_response).to respond_to :help
|
14
|
+
end
|
15
|
+
end
|
data/third_party/pwn_rdoc.jsonl
CHANGED
@@ -492,19 +492,23 @@
|
|
492
492
|
{"messages":[{"role":"user","content":"PWN::Plugins::BlackDuckBinaryAnalysis.get_vulnerabilities Usage"},{"role":"assistant","content":"`PWN::Plugins::BlackDuckBinaryAnalysis.get_vulnerabilities`: Supported Method Parameters\n\nresponse = PWN::Plugins::BlackDuckBinaryAnalysis.get_vulnerabilities(\n\ntoken: 'required - Bearer token'\n\n)\n"}]}
|
493
493
|
{"messages":[{"role":"user","content":"PWN::Plugins::BlackDuckBinaryAnalysis.help Usage"},{"role":"assistant","content":"`PWN::Plugins::BlackDuckBinaryAnalysis.help`: "}]}
|
494
494
|
{"messages":[{"role":"user","content":"PWN::Plugins::BlackDuckBinaryAnalysis.upload_file Usage"},{"role":"assistant","content":"`PWN::Plugins::BlackDuckBinaryAnalysis.upload_file`: Supported Method Parameters\n\nresponse = PWN::Plugins::BlackDuckBinaryAnalysis.upload_file(\n\ntoken: 'required - Bearer token',\nfile: 'required - path of file to upload',\ngroup_id: 'optional - group id',\ndelete_binary: 'optional - delete binary after upload (defaults to false)',\nforce_scan: 'optional - force scan (defaults to false)',\ncallback_url: 'optional - callback url',\nscan_infoleak: 'optional - scan infoleak (defaults to true)',\ncode_analysis: 'optional - code analysis (defaults to true)',\nscan_code_familiarity: 'optional - scan code familiarity (defaults to false)',\nversion: 'optional - version',\nproduct_id: 'optional - product id'\n\n)\n"}]}
|
495
|
+
{"messages":[{"role":"user","content":"PWN::Plugins::BurpSuite.add_proxy_listener Usage"},{"role":"assistant","content":"`PWN::Plugins::BurpSuite.add_proxy_listener`: Supported Method Parameters\n\njson_proxy_listener = PWN::Plugins::BurpSuite.add_proxy_listener(\n\nburp_obj: 'required - burp_obj returned by #start method',\nbind_address: 'required - bind address for the proxy listener (e.g., \"127.0.0.1\")',\nport: 'required - port for the proxy listener (e.g., 8081)',\nenabled: 'optional - enable the listener (defaults to true)'\n\n)\n"}]}
|
495
496
|
{"messages":[{"role":"user","content":"PWN::Plugins::BurpSuite.add_to_scope Usage"},{"role":"assistant","content":"`PWN::Plugins::BurpSuite.add_to_scope`: Supported Method Parameters\n\njson_in_scope = PWN::Plugins::BurpSuite.add_to_scope(\n\nburp_obj: 'required - burp_obj returned by #start method',\ntarget_url: 'required - target url to add to scope'\n\n)\n"}]}
|
496
497
|
{"messages":[{"role":"user","content":"PWN::Plugins::BurpSuite.authors Usage"},{"role":"assistant","content":"`PWN::Plugins::BurpSuite.authors`: Author(s)\n\n0day Inc. <support@0dayinc.com>\n"}]}
|
498
|
+
{"messages":[{"role":"user","content":"PWN::Plugins::BurpSuite.delete_proxy_listener Usage"},{"role":"assistant","content":"`PWN::Plugins::BurpSuite.delete_proxy_listener`: Supported Method Parameters\n\nPWN::Plugins::BurpSuite.delete_proxy_listener(\n\nburp_obj: 'required - burp_obj returned by #start method',\nid: 'required - ID of the proxy listener (e.g., \"127.0.0.1:8080\")'\n\n)\n"}]}
|
497
499
|
{"messages":[{"role":"user","content":"PWN::Plugins::BurpSuite.disable_proxy Usage"},{"role":"assistant","content":"`PWN::Plugins::BurpSuite.disable_proxy`: Supported Method Parameters\n\nPWN::Plugins::BurpSuite.disable_proxy(\n\nburp_obj: 'required - burp_obj returned by #start method'\n\n)\n"}]}
|
498
500
|
{"messages":[{"role":"user","content":"PWN::Plugins::BurpSuite.enable_proxy Usage"},{"role":"assistant","content":"`PWN::Plugins::BurpSuite.enable_proxy`: Supported Method Parameters\n\nPWN::Plugins::BurpSuite.enable_proxy(\n\nburp_obj: 'required - burp_obj returned by #start method'\n\n)\n"}]}
|
499
501
|
{"messages":[{"role":"user","content":"PWN::Plugins::BurpSuite.format_uri_from_sitemap_resp Usage"},{"role":"assistant","content":"`PWN::Plugins::BurpSuite.format_uri_from_sitemap_resp`: Supported Method Parameters\n\nuri = PWN::Plugins::BurpSuite.format_uri_from_sitemap_resp(\n\nscheme: 'required - scheme of the URI (http|https)',\nhost: 'required - host of the URI',\nport: 'optional - port of the URI',\npath: 'optional - path of the URI'\n\n)\n"}]}
|
500
502
|
{"messages":[{"role":"user","content":"PWN::Plugins::BurpSuite.generate_scan_report Usage"},{"role":"assistant","content":"`PWN::Plugins::BurpSuite.generate_scan_report`: Supported Method Parameters\n\nPWN::Plugins::BurpSuite.generate_scan_report(\n\nburp_obj: 'required - burp_obj returned by #start method',\ntarget_url: 'required - target_url passed to #invoke_active_scan method',\nreport_type: :html|:xml|:both,\noutput_path: 'required - path to save report results'\n\n)\n"}]}
|
501
503
|
{"messages":[{"role":"user","content":"PWN::Plugins::BurpSuite.get_current_sitemap Usage"},{"role":"assistant","content":"`PWN::Plugins::BurpSuite.get_current_sitemap`: Supported Method Parameters\n\njson_sitemap = PWN::Plugins::BurpSuite.get_current_sitemap(\n\nburp_obj: 'required - burp_obj returned by #start method'\n\n)\n"}]}
|
504
|
+
{"messages":[{"role":"user","content":"PWN::Plugins::BurpSuite.get_proxy_listeners Usage"},{"role":"assistant","content":"`PWN::Plugins::BurpSuite.get_proxy_listeners`: Supported Method Parameters\n\njson_proxy_listeners = PWN::Plugins::BurpSuite.get_proxy_listeners(\n\nburp_obj: 'required - burp_obj returned by #start method'\n\n)\n"}]}
|
502
505
|
{"messages":[{"role":"user","content":"PWN::Plugins::BurpSuite.get_scan_issues Usage"},{"role":"assistant","content":"`PWN::Plugins::BurpSuite.get_scan_issues`: Supported Method Parameters\n\njson_scan_issues = PWN::Plugins::BurpSuite.get_scan_issues(\n\nburp_obj: 'required - burp_obj returned by #start method'\n\n)\n"}]}
|
503
506
|
{"messages":[{"role":"user","content":"PWN::Plugins::BurpSuite.help Usage"},{"role":"assistant","content":"`PWN::Plugins::BurpSuite.help`: "}]}
|
504
507
|
{"messages":[{"role":"user","content":"PWN::Plugins::BurpSuite.invoke_active_scan Usage"},{"role":"assistant","content":"`PWN::Plugins::BurpSuite.invoke_active_scan`: Supported Method Parameters\n\nactive_scan_url_arr = PWN::Plugins::BurpSuite.invoke_active_scan(\n\nburp_obj: 'required - burp_obj returned by #start method',\ntarget_url: 'required - target url to scan in sitemap (should be loaded & authenticated w/ burp_obj[:burp_browser])'\n\n)\n"}]}
|
505
|
-
{"messages":[{"role":"user","content":"PWN::Plugins::BurpSuite.start Usage"},{"role":"assistant","content":"`PWN::Plugins::BurpSuite.start`: Supported Method Parameters\n\nburp_obj = PWN::Plugins::BurpSuite.start(\n\nburp_jar_path: '
|
508
|
+
{"messages":[{"role":"user","content":"PWN::Plugins::BurpSuite.start Usage"},{"role":"assistant","content":"`PWN::Plugins::BurpSuite.start`: Supported Method Parameters\n\nburp_obj = PWN::Plugins::BurpSuite.start(\n\nburp_jar_path: 'optional - path of burp suite pro jar file (defaults to /opt/burpsuite/burpsuite_pro.jar)',\nheadless: 'optional - run burp headless if set to true',\nbrowser_type: 'optional - defaults to :firefox. See PWN::Plugins::TransparentBrowser.help for a list of types',\nburp_ip: 'optional - IP address for the Burp proxy (defaults to 127.0.0.1)',\nburp_port: 'optional - port for the Burp proxy (defaults to a random unused port)',\npwn_burp_ip: 'optional - IP address for the PWN Burp API (defaults to 127.0.0.1)',\npwn_burp_port: 'optional - port for the PWN Burp API (defaults to a random unused port)'\n\n)\n"}]}
|
506
509
|
{"messages":[{"role":"user","content":"PWN::Plugins::BurpSuite.stop Usage"},{"role":"assistant","content":"`PWN::Plugins::BurpSuite.stop`: Supported Method Parameters\n\nPWN::Plugins::BurpSuite.stop(\n\nburp_obj: 'required - burp_obj returned by #start method'\n\n)\n"}]}
|
507
510
|
{"messages":[{"role":"user","content":"PWN::Plugins::BurpSuite.update_burp_jar Usage"},{"role":"assistant","content":"`PWN::Plugins::BurpSuite.update_burp_jar`: Supported Method Parameters\n\nPWN::Plugins::BurpSuite.update_burp_jar( )\n"}]}
|
511
|
+
{"messages":[{"role":"user","content":"PWN::Plugins::BurpSuite.update_proxy_listener Usage"},{"role":"assistant","content":"`PWN::Plugins::BurpSuite.update_proxy_listener`: Supported Method Parameters\n\njson_proxy_listener = PWN::Plugins::BurpSuite.update_proxy_listener(\n\nburp_obj: 'required - burp_obj returned by #start method',\nid: 'optional - ID of the proxy listener (defaults to \"127.0.0.1:8080\")',\nbind_address: 'optional - bind address for the proxy listener (defaults to \"127.0.0.1\")',\nport: 'optional - port for the proxy listener (defaults to 8080)',\nenabled: 'optional - enable or disable the listener (defaults to true)'\n\n)\n"}]}
|
508
512
|
{"messages":[{"role":"user","content":"PWN::Plugins::BurpSuite.uri_in_scope Usage"},{"role":"assistant","content":"`PWN::Plugins::BurpSuite.uri_in_scope`: Supported Method Parameters\n\nuri_in_scope_bool = PWN::Plugins::BurpSuite.uri_in_scope(\n\ntarget_config: 'required - path to burp suite pro target config JSON file',\nuri: 'required - URI to determine if in scope'\n\n)\n"}]}
|
509
513
|
{"messages":[{"role":"user","content":"PWN::Plugins::BusPirate.authors Usage"},{"role":"assistant","content":"`PWN::Plugins::BusPirate.authors`: Author(s)\n\n0day Inc. <support@0dayinc.com>\n"}]}
|
510
514
|
{"messages":[{"role":"user","content":"PWN::Plugins::BusPirate.connect Usage"},{"role":"assistant","content":"`PWN::Plugins::BusPirate.connect`: Supported Method Parameters\n\nbus_pirate_obj = PWN::Plugins::BusPirate.connect(\n\nblock_dev: 'optional serial block device path (defaults to /dev/ttyUSB0)',\nbaud: 'optional (defaults to 9600)',\ndata_bits: 'optional (defaults to 8)',\nstop_bits: 'optional (defaults to 1)',\nparity: 'optional (defaults to SerialPort::NONE)',\nflow_control: 'optional (defaults to SerialPort::HARD) SerialPort::NONE|SerialPort::SOFT|SerialPort::HARD'\n\n)\n"}]}
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pwn
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.5.
|
4
|
+
version: 0.5.332
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- 0day Inc.
|
@@ -407,14 +407,14 @@ dependencies:
|
|
407
407
|
requirements:
|
408
408
|
- - '='
|
409
409
|
- !ruby/object:Gem::Version
|
410
|
-
version: 2.13.
|
410
|
+
version: 2.13.2
|
411
411
|
type: :runtime
|
412
412
|
prerelease: false
|
413
413
|
version_requirements: !ruby/object:Gem::Requirement
|
414
414
|
requirements:
|
415
415
|
- - '='
|
416
416
|
- !ruby/object:Gem::Version
|
417
|
-
version: 2.13.
|
417
|
+
version: 2.13.2
|
418
418
|
- !ruby/object:Gem::Dependency
|
419
419
|
name: jsonpath
|
420
420
|
requirement: !ruby/object:Gem::Requirement
|
@@ -743,14 +743,14 @@ dependencies:
|
|
743
743
|
requirements:
|
744
744
|
- - '='
|
745
745
|
- !ruby/object:Gem::Version
|
746
|
-
version: 1.
|
746
|
+
version: 1.6.0
|
747
747
|
type: :runtime
|
748
748
|
prerelease: false
|
749
749
|
version_requirements: !ruby/object:Gem::Requirement
|
750
750
|
requirements:
|
751
751
|
- - '='
|
752
752
|
- !ruby/object:Gem::Version
|
753
|
-
version: 1.
|
753
|
+
version: 1.6.0
|
754
754
|
- !ruby/object:Gem::Dependency
|
755
755
|
name: pry
|
756
756
|
requirement: !ruby/object:Gem::Requirement
|
@@ -1853,6 +1853,7 @@ files:
|
|
1853
1853
|
- lib/pwn/plugins/ocr.rb
|
1854
1854
|
- lib/pwn/plugins/ollama.rb
|
1855
1855
|
- lib/pwn/plugins/open_ai.rb
|
1856
|
+
- lib/pwn/plugins/open_api.rb
|
1856
1857
|
- lib/pwn/plugins/openvas.rb
|
1857
1858
|
- lib/pwn/plugins/owasp_zap.rb
|
1858
1859
|
- lib/pwn/plugins/packet.rb
|
@@ -2192,6 +2193,7 @@ files:
|
|
2192
2193
|
- spec/lib/pwn/plugins/ocr_spec.rb
|
2193
2194
|
- spec/lib/pwn/plugins/ollama_spec.rb
|
2194
2195
|
- spec/lib/pwn/plugins/open_ai_spec.rb
|
2196
|
+
- spec/lib/pwn/plugins/open_api_spec.rb
|
2195
2197
|
- spec/lib/pwn/plugins/openvas_spec.rb
|
2196
2198
|
- spec/lib/pwn/plugins/owasp_zap_spec.rb
|
2197
2199
|
- spec/lib/pwn/plugins/packet_spec.rb
|