pwn 0.5.331 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5ac40ad5725903ee8da2c282e5819a5af3776a2fc73c64635d56de5f513a2b6e
4
- data.tar.gz: 509e65cd59d2f8a7e98c1e5c7b8389ebecea0a3f42ac33f63fb7009bddc30d97
3
+ metadata.gz: 416b3161579a8f088d76f1de382c47a802945491767e0e9db044e1daaa263a77
4
+ data.tar.gz: ae3acfd4171b49ae5675047c924f731f654ca705e20bd4213a9bf03ef643f433
5
5
  SHA512:
6
- metadata.gz: de7079e5a9b79d5303c12e156c02d05f1db4ce950ca4031d85b4ada34818be4fed6cc1f3d55ee17ef451ac61669b40949dd479cbbef35fb8056ae7155dba3ea7
7
- data.tar.gz: b55508106c075eb9aee38c583de961584fe7eedb6df7e3b86c7ef8f5b14694105d2532237fca1ad145f48cb692f21833d7121622910b91e7ad9026a8f809d11e
6
+ metadata.gz: 0374d5fa47a7eae2baca814efebf33f98f265d2824139b28ad6b400567002a00eee847b4d9f039aedbb0df3d737d7b6b1de649aa748e18d8a1d160fbe38d3179
7
+ data.tar.gz: da79785268a5a8bff16553f29b1473d7e3a4529a1c891736627b19ab1d1d13df1d904f2fac108b729a245b66b305886b7726f85d2854a1cabdaac27f299358b9
data/.rubocop.yml CHANGED
@@ -18,7 +18,7 @@ Metrics/CyclomaticComplexity:
18
18
  Metrics/MethodLength:
19
19
  Max: 485
20
20
  Metrics/ModuleLength:
21
- Max: 495
21
+ Max: 500
22
22
  Metrics/PerceivedComplexity:
23
23
  Max: 101
24
24
  Style/HashEachMethods:
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.1'
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.5.9'
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'
@@ -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("http://#{pwn_burp_api}/sitemap", content_type: 'application/json; charset=UTF8')
206
- # json_sitemap = JSON.parse(sitemap, symbolize_names: true)
207
- # json_sitemap is an array of hashes.
208
- # each hash contains a :request and :response key.
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
- # stop(burp_obj: burp_obj) unless burp_obj.nil?
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PWN
4
- VERSION = '0.5.331'
4
+ VERSION = '0.5.332'
5
5
  end
@@ -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
@@ -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: 'options - 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',\ntarget_config: 'optional - path to burp suite pro target config JSON file'\n\n)\n"}]}
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"}]}
@@ -800,6 +804,7 @@
800
804
  {"messages":[{"role":"user","content":"PWN::Plugins::OwaspZap.breakpoint Usage"},{"role":"assistant","content":"`PWN::Plugins::OwaspZap.breakpoint`: Supported Method Parameters\n\nPWN::Plugins::OwaspZap.breakpoint(\n\nzap_obj: 'required - zap_obj returned from #open method',\nregex_type: 'required - :url, :request_header, :request_body, :response_header or :response_body',\nregex_pattern: 'required - regex pattern to search for respective regex_type',\nenabled: 'optional - boolean (defaults to true)'\n\n)\n"}]}
801
805
  {"messages":[{"role":"user","content":"PWN::Plugins::OwaspZap.generate_report Usage"},{"role":"assistant","content":"`PWN::Plugins::OwaspZap.generate_report`: Supported Method Parameters\n\nreport_path = PWN::Plugins::OwaspZap.generate_report(\n\nzap_obj: 'required - zap_obj returned from #open method',\noutput_dir: 'required - directory to save report',\nreport_type: 'required - <html|markdown|xml>'\n\n)\n"}]}
802
806
  {"messages":[{"role":"user","content":"PWN::Plugins::OwaspZap.help Usage"},{"role":"assistant","content":"`PWN::Plugins::OwaspZap.help`: "}]}
807
+ {"messages":[{"role":"user","content":"PWN::Plugins::OwaspZap.import_openapi_spec_file Usage"},{"role":"assistant","content":"`PWN::Plugins::OwaspZap.import_openapi_spec_file`: Supported Method Parameters\n\nPWN::Plugins::OwaspZap.import_openapi_spec_file(\n\nzap_obj: 'required - zap_obj returned from #open method',\nspec: 'required - path to OpenAPI spec file (e.g. /path/to/openapi.yaml)',\ntarget: 'required - target URL to ovverride the service URL in the OpenAPI spec (e.g. https://fq.dn)',\ncontext_id: 'optional - ID of the ZAP context (Defaults to first context, if any)',\nuser_id: 'optional - ID of the ZAP user (Defaults to first user, if any)'\n\n)\n"}]}
803
808
  {"messages":[{"role":"user","content":"PWN::Plugins::OwaspZap.request Usage"},{"role":"assistant","content":"`PWN::Plugins::OwaspZap.request`: Supported Method Parameters\n\nwatir_resp = PWN::Plugins::OwaspZap.request(\n\nzap_obj: 'required - zap_obj returned from #open method',\nbrowser_obj: 'required - browser_obj w/ browser_type: :firefox||:headless returned from #open method',\ninstruction: 'required - watir instruction to make (e.g. button(text: \"Google Search\").click)'\n\n)\n"}]}
804
809
  {"messages":[{"role":"user","content":"PWN::Plugins::OwaspZap.spider Usage"},{"role":"assistant","content":"`PWN::Plugins::OwaspZap.spider`: Supported Method Parameters\n\nPWN::Plugins::OwaspZap.spider(\n\nzap_obj: 'required - zap_obj returned from #open method',\ntarget: 'required - url to spider'\n\n)\n"}]}
805
810
  {"messages":[{"role":"user","content":"PWN::Plugins::OwaspZap.start Usage"},{"role":"assistant","content":"`PWN::Plugins::OwaspZap.start`: Supported Method Parameters\n\nzap_obj = PWN::Plugins::OwaspZap.start(\n\napi_key: 'required - api key for API authorization',\nzap_bin_path: 'optional - path to zap.sh file'\nheadless: 'optional - run zap headless if set to true',\nproxy: 'optional - change local zap proxy listener (defaults to http://127.0.0.1:<Random 1024-65535>)',\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.331
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.1
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.1
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.5.9
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.5.9
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