purl 0.1.0 ā 1.1.0
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/CHANGELOG.md +43 -2
- data/CODE_OF_CONDUCT.md +1 -1
- data/CONTRIBUTING.md +167 -0
- data/LICENSE +21 -0
- data/README.md +409 -13
- data/Rakefile +538 -0
- data/SECURITY.md +164 -0
- data/lib/purl/errors.rb +64 -0
- data/lib/purl/package_url.rb +520 -0
- data/lib/purl/registry_url.rb +543 -0
- data/lib/purl/version.rb +1 -1
- data/lib/purl.rb +133 -1
- data/purl-types.json +583 -0
- data/schemas/purl-types.schema.json +154 -0
- data/schemas/test-suite-data.schema.json +134 -0
- data/test-suite-data.json +710 -0
- metadata +15 -2
data/Rakefile
CHANGED
|
@@ -6,3 +6,541 @@ require "minitest/test_task"
|
|
|
6
6
|
Minitest::TestTask.create
|
|
7
7
|
|
|
8
8
|
task default: :test
|
|
9
|
+
|
|
10
|
+
namespace :spec do
|
|
11
|
+
desc "Show available PURL specification tasks"
|
|
12
|
+
task :help do
|
|
13
|
+
puts "š§ PURL Specification Tasks"
|
|
14
|
+
puts "=" * 30
|
|
15
|
+
puts "rake spec:update - Fetch latest test cases from official PURL spec repository"
|
|
16
|
+
puts "rake spec:stats - Show statistics about current test suite data"
|
|
17
|
+
puts "rake spec:compliance - Run all compliance tests against the official test suite"
|
|
18
|
+
puts "rake spec:debug - Show detailed info about failing test cases"
|
|
19
|
+
puts "rake spec:types - Show information about all PURL types and their support"
|
|
20
|
+
puts "rake spec:verify_types - Verify our types list against the official specification"
|
|
21
|
+
puts "rake spec:validate_schemas - Validate JSON files against their schemas"
|
|
22
|
+
puts "rake spec:validate_examples - Validate all PURL examples in purl-types.json"
|
|
23
|
+
puts "rake spec:help - Show this help message"
|
|
24
|
+
puts
|
|
25
|
+
puts "Example workflow:"
|
|
26
|
+
puts " 1. rake spec:update # Get latest test cases"
|
|
27
|
+
puts " 2. rake spec:stats # Review test suite composition"
|
|
28
|
+
puts " 3. rake spec:compliance # Run compliance tests"
|
|
29
|
+
puts " 4. rake spec:debug # Debug any failures"
|
|
30
|
+
puts
|
|
31
|
+
puts "The test suite data is stored in test-suite-data.json at the project root."
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
desc "Import/update official PURL specification test cases"
|
|
35
|
+
task :update do
|
|
36
|
+
require "net/http"
|
|
37
|
+
require "uri"
|
|
38
|
+
require "json"
|
|
39
|
+
require "fileutils"
|
|
40
|
+
|
|
41
|
+
puts "Fetching official PURL specification test cases..."
|
|
42
|
+
|
|
43
|
+
# URL for the official test suite data
|
|
44
|
+
url = "https://raw.githubusercontent.com/package-url/purl-spec/master/test-suite-data.json"
|
|
45
|
+
test_file_path = File.join(__dir__, "test-suite-data.json")
|
|
46
|
+
backup_path = "#{test_file_path}.backup"
|
|
47
|
+
|
|
48
|
+
begin
|
|
49
|
+
# Create backup of existing file if it exists
|
|
50
|
+
if File.exist?(test_file_path)
|
|
51
|
+
puts "Creating backup of existing test file..."
|
|
52
|
+
FileUtils.cp(test_file_path, backup_path)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Fetch the latest test data
|
|
56
|
+
uri = URI(url)
|
|
57
|
+
response = Net::HTTP.get_response(uri)
|
|
58
|
+
|
|
59
|
+
if response.code == "200"
|
|
60
|
+
# Validate that we got valid JSON
|
|
61
|
+
test_data = JSON.parse(response.body)
|
|
62
|
+
|
|
63
|
+
# Write the new test data
|
|
64
|
+
File.write(test_file_path, response.body)
|
|
65
|
+
|
|
66
|
+
puts "ā
Successfully updated test suite data!"
|
|
67
|
+
puts " - Test cases: #{test_data.length}"
|
|
68
|
+
puts " - File: #{test_file_path}"
|
|
69
|
+
|
|
70
|
+
# Remove backup if update was successful
|
|
71
|
+
File.delete(backup_path) if File.exist?(backup_path)
|
|
72
|
+
|
|
73
|
+
# Show summary of test case types
|
|
74
|
+
types = test_data.group_by { |tc| tc["type"] || "unknown" }.transform_values(&:count)
|
|
75
|
+
puts "\nš Test case distribution by package type:"
|
|
76
|
+
types.sort_by { |type, _| type.to_s }.each do |type, count|
|
|
77
|
+
puts " #{type}: #{count} cases"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Show invalid vs valid cases
|
|
81
|
+
invalid_count = test_data.count { |tc| tc["is_invalid"] }
|
|
82
|
+
valid_count = test_data.count { |tc| !tc["is_invalid"] }
|
|
83
|
+
puts "\nš Test case categories:"
|
|
84
|
+
puts " Valid cases: #{valid_count}"
|
|
85
|
+
puts " Invalid cases: #{invalid_count}"
|
|
86
|
+
|
|
87
|
+
else
|
|
88
|
+
raise "HTTP request failed with status #{response.code}: #{response.message}"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
rescue => e
|
|
92
|
+
puts "ā Failed to update test suite data: #{e.message}"
|
|
93
|
+
|
|
94
|
+
# Restore backup if update failed
|
|
95
|
+
if File.exist?(backup_path)
|
|
96
|
+
puts "Restoring backup..."
|
|
97
|
+
FileUtils.mv(backup_path, test_file_path)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
exit 1
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
desc "Show current test suite statistics"
|
|
105
|
+
task :stats do
|
|
106
|
+
require "json"
|
|
107
|
+
|
|
108
|
+
test_file_path = File.join(__dir__, "test-suite-data.json")
|
|
109
|
+
|
|
110
|
+
unless File.exist?(test_file_path)
|
|
111
|
+
puts "ā Test suite data file not found. Run 'rake spec:update' first."
|
|
112
|
+
exit 1
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
begin
|
|
116
|
+
test_data = JSON.parse(File.read(test_file_path))
|
|
117
|
+
|
|
118
|
+
puts "š PURL Test Suite Statistics"
|
|
119
|
+
puts "=" * 40
|
|
120
|
+
puts "Total test cases: #{test_data.length}"
|
|
121
|
+
puts "File location: #{test_file_path}"
|
|
122
|
+
puts "File size: #{File.size(test_file_path)} bytes"
|
|
123
|
+
puts "Last modified: #{File.mtime(test_file_path)}"
|
|
124
|
+
|
|
125
|
+
# Distribution by package type
|
|
126
|
+
puts "\nš¦ Distribution by package type:"
|
|
127
|
+
types = test_data.group_by { |tc| tc["type"] || "unknown" }.transform_values(&:count)
|
|
128
|
+
types.sort_by { |_, count| -count }.each do |type, count|
|
|
129
|
+
percentage = (count.to_f / test_data.length * 100).round(1)
|
|
130
|
+
puts " #{type.to_s.ljust(12)} #{count.to_s.rjust(3)} cases (#{percentage}%)"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Valid vs invalid cases
|
|
134
|
+
invalid_count = test_data.count { |tc| tc["is_invalid"] }
|
|
135
|
+
valid_count = test_data.count { |tc| !tc["is_invalid"] }
|
|
136
|
+
puts "\nā
Test case validity:"
|
|
137
|
+
puts " Valid cases: #{valid_count} (#{(valid_count.to_f / test_data.length * 100).round(1)}%)"
|
|
138
|
+
puts " Invalid cases: #{invalid_count} (#{(invalid_count.to_f / test_data.length * 100).round(1)}%)"
|
|
139
|
+
|
|
140
|
+
# Cases with different components
|
|
141
|
+
has_namespace = test_data.count { |tc| tc["namespace"] }
|
|
142
|
+
has_version = test_data.count { |tc| tc["version"] }
|
|
143
|
+
has_qualifiers = test_data.count { |tc| tc["qualifiers"] && !tc["qualifiers"].empty? }
|
|
144
|
+
has_subpath = test_data.count { |tc| tc["subpath"] }
|
|
145
|
+
|
|
146
|
+
puts "\nš§ Component usage:"
|
|
147
|
+
puts " With namespace: #{has_namespace} cases"
|
|
148
|
+
puts " With version: #{has_version} cases"
|
|
149
|
+
puts " With qualifiers: #{has_qualifiers} cases"
|
|
150
|
+
puts " With subpath: #{has_subpath} cases"
|
|
151
|
+
|
|
152
|
+
rescue JSON::ParserError => e
|
|
153
|
+
puts "ā Failed to parse test suite data: #{e.message}"
|
|
154
|
+
exit 1
|
|
155
|
+
rescue => e
|
|
156
|
+
puts "ā Error reading test suite data: #{e.message}"
|
|
157
|
+
exit 1
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
desc "Run compliance tests against the official test suite"
|
|
162
|
+
task :compliance do
|
|
163
|
+
puts "Running PURL specification compliance tests..."
|
|
164
|
+
system("ruby test/test_purl_spec_compliance.rb")
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
desc "Verify our PURL types against the official specification"
|
|
168
|
+
task :verify_types do
|
|
169
|
+
require "net/http"
|
|
170
|
+
require "uri"
|
|
171
|
+
require_relative "lib/purl"
|
|
172
|
+
|
|
173
|
+
puts "š Verifying PURL Types Against Official Specification"
|
|
174
|
+
puts "=" * 60
|
|
175
|
+
|
|
176
|
+
begin
|
|
177
|
+
# Fetch the official PURL-TYPES.rst file
|
|
178
|
+
url = "https://raw.githubusercontent.com/package-url/purl-spec/main/PURL-TYPES.rst"
|
|
179
|
+
uri = URI(url)
|
|
180
|
+
response = Net::HTTP.get_response(uri)
|
|
181
|
+
|
|
182
|
+
if response.code != "200"
|
|
183
|
+
puts "ā Failed to fetch official specification: HTTP #{response.code}"
|
|
184
|
+
exit 1
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
content = response.body
|
|
188
|
+
|
|
189
|
+
# Extract type names from the specification
|
|
190
|
+
# Look for lines like "**alpm**" or lines that start type definitions
|
|
191
|
+
official_types = []
|
|
192
|
+
content.scan(/^\*\*(\w+)\*\*/) do |match|
|
|
193
|
+
official_types << match[0].downcase
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Also look for types in different format patterns (but be more restrictive)
|
|
197
|
+
content.scan(/^(\w+)\s*$/) do |match|
|
|
198
|
+
type = match[0].downcase
|
|
199
|
+
# Filter out common words that aren't types and document sections
|
|
200
|
+
unless %w[types purl package url specification license abstract].include?(type)
|
|
201
|
+
official_types << type if type.length > 2 && type != "license"
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
official_types = official_types.uniq.sort
|
|
206
|
+
our_types = Purl.known_types.sort
|
|
207
|
+
|
|
208
|
+
puts "š Comparison Results:"
|
|
209
|
+
puts " Official specification: #{official_types.length} types"
|
|
210
|
+
puts " Our implementation: #{our_types.length} types"
|
|
211
|
+
|
|
212
|
+
# Find missing types
|
|
213
|
+
missing_from_ours = official_types - our_types
|
|
214
|
+
extra_in_ours = our_types - official_types
|
|
215
|
+
|
|
216
|
+
if missing_from_ours.empty? && extra_in_ours.empty?
|
|
217
|
+
puts "\nā
Perfect match! All types are synchronized."
|
|
218
|
+
else
|
|
219
|
+
if missing_from_ours.any?
|
|
220
|
+
puts "\nā Types in specification but missing from our list:"
|
|
221
|
+
missing_from_ours.each { |type| puts " - #{type}" }
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
if extra_in_ours.any?
|
|
225
|
+
puts "\nā ļø Types in our list but not found in specification:"
|
|
226
|
+
extra_in_ours.each { |type| puts " + #{type}" }
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
puts "\nš All Official Types Found:"
|
|
231
|
+
official_types.each_with_index do |type, index|
|
|
232
|
+
status = our_types.include?(type) ? "ā" : "ā"
|
|
233
|
+
puts " #{status} #{(index + 1).to_s.rjust(2)}. #{type}"
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
rescue => e
|
|
237
|
+
puts "ā Error verifying types: #{e.message}"
|
|
238
|
+
exit 1
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
desc "Show information about PURL types"
|
|
243
|
+
task :types do
|
|
244
|
+
require_relative "lib/purl"
|
|
245
|
+
|
|
246
|
+
puts "š PURL Type Information"
|
|
247
|
+
puts "=" * 40
|
|
248
|
+
|
|
249
|
+
puts "\nš All Known PURL Types (#{Purl.known_types.length}):"
|
|
250
|
+
Purl.known_types.each_slice(4) do |slice|
|
|
251
|
+
puts " #{slice.map { |t| t.ljust(12) }.join(" ")}"
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
puts "\nš Registry URL Generation Support (#{Purl.registry_supported_types.length}):"
|
|
255
|
+
Purl.registry_supported_types.each_slice(4) do |slice|
|
|
256
|
+
puts " #{slice.map { |t| t.ljust(12) }.join(" ")}"
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
puts "\nš Reverse Parsing Support (#{Purl.reverse_parsing_supported_types.length}):"
|
|
260
|
+
Purl.reverse_parsing_supported_types.each_slice(4) do |slice|
|
|
261
|
+
puts " #{slice.map { |t| t.ljust(12) }.join(" ")}"
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
puts "\nš Type Support Matrix:"
|
|
265
|
+
puts " Type Known Registry Reverse"
|
|
266
|
+
puts " " + "-" * 35
|
|
267
|
+
|
|
268
|
+
all_types = (Purl.known_types + Purl.registry_supported_types).uniq.sort
|
|
269
|
+
all_types.each do |type|
|
|
270
|
+
info = Purl.type_info(type)
|
|
271
|
+
known_mark = info[:known] ? "ā" : "ā"
|
|
272
|
+
registry_mark = info[:registry_url_generation] ? "ā" : "ā"
|
|
273
|
+
reverse_mark = info[:reverse_parsing] ? "ā" : "ā"
|
|
274
|
+
|
|
275
|
+
puts " #{type.ljust(12)} #{known_mark.center(5)} #{registry_mark.center(9)} #{reverse_mark.center(7)}"
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
puts "\nš¤ Route Patterns Examples:"
|
|
279
|
+
["gem", "npm", "maven"].each do |type|
|
|
280
|
+
patterns = Purl::RegistryURL.route_patterns_for(type)
|
|
281
|
+
if patterns.any?
|
|
282
|
+
puts "\n #{type.upcase}:"
|
|
283
|
+
patterns.each { |pattern| puts " #{pattern}" }
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
desc "Show failing test cases for debugging"
|
|
289
|
+
task :debug do
|
|
290
|
+
require "json"
|
|
291
|
+
require_relative "lib/purl"
|
|
292
|
+
|
|
293
|
+
test_file_path = File.join(__dir__, "test-suite-data.json")
|
|
294
|
+
|
|
295
|
+
unless File.exist?(test_file_path)
|
|
296
|
+
puts "ā Test suite data file not found. Run 'rake spec:update' first."
|
|
297
|
+
exit 1
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
test_data = JSON.parse(File.read(test_file_path))
|
|
301
|
+
|
|
302
|
+
puts "š Debugging failing test cases..."
|
|
303
|
+
puts "=" * 50
|
|
304
|
+
|
|
305
|
+
failed_cases = []
|
|
306
|
+
|
|
307
|
+
test_data.each_with_index do |test_case, index|
|
|
308
|
+
description = test_case["description"]
|
|
309
|
+
purl_string = test_case["purl"]
|
|
310
|
+
is_invalid = test_case["is_invalid"]
|
|
311
|
+
|
|
312
|
+
begin
|
|
313
|
+
if is_invalid
|
|
314
|
+
begin
|
|
315
|
+
Purl::PackageURL.parse(purl_string)
|
|
316
|
+
failed_cases << {
|
|
317
|
+
index: index + 1,
|
|
318
|
+
description: description,
|
|
319
|
+
purl: purl_string,
|
|
320
|
+
error: "Expected parsing to fail but it succeeded",
|
|
321
|
+
type: "validation"
|
|
322
|
+
}
|
|
323
|
+
rescue Purl::Error
|
|
324
|
+
# Correctly failed - this is expected
|
|
325
|
+
end
|
|
326
|
+
else
|
|
327
|
+
purl = Purl::PackageURL.parse(purl_string)
|
|
328
|
+
|
|
329
|
+
# Check if canonical form matches expected
|
|
330
|
+
expected_canonical = test_case["canonical_purl"]
|
|
331
|
+
if expected_canonical && purl.to_s != expected_canonical
|
|
332
|
+
failed_cases << {
|
|
333
|
+
index: index + 1,
|
|
334
|
+
description: description,
|
|
335
|
+
purl: purl_string,
|
|
336
|
+
error: "Canonical mismatch: expected '#{expected_canonical}', got '#{purl.to_s}'",
|
|
337
|
+
type: "canonical"
|
|
338
|
+
}
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
# Check component mismatches
|
|
342
|
+
%w[type namespace name version qualifiers subpath].each do |component|
|
|
343
|
+
expected = test_case[component]
|
|
344
|
+
actual = purl.send(component)
|
|
345
|
+
|
|
346
|
+
if expected != actual
|
|
347
|
+
failed_cases << {
|
|
348
|
+
index: index + 1,
|
|
349
|
+
description: description,
|
|
350
|
+
purl: purl_string,
|
|
351
|
+
error: "#{component.capitalize} mismatch: expected #{expected.inspect}, got #{actual.inspect}",
|
|
352
|
+
type: "component"
|
|
353
|
+
}
|
|
354
|
+
break # Only report first mismatch per test case
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
rescue => e
|
|
359
|
+
failed_cases << {
|
|
360
|
+
index: index + 1,
|
|
361
|
+
description: description,
|
|
362
|
+
purl: purl_string,
|
|
363
|
+
error: "#{e.class}: #{e.message}",
|
|
364
|
+
type: "exception"
|
|
365
|
+
}
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
if failed_cases.empty?
|
|
370
|
+
puts "š All test cases are passing!"
|
|
371
|
+
else
|
|
372
|
+
puts "ā Found #{failed_cases.length} failing test cases:\n"
|
|
373
|
+
|
|
374
|
+
# Group by failure type
|
|
375
|
+
failed_cases.group_by { |fc| fc[:type] }.each do |failure_type, cases|
|
|
376
|
+
puts "#{failure_type.upcase} FAILURES (#{cases.length}):"
|
|
377
|
+
puts "-" * 30
|
|
378
|
+
|
|
379
|
+
cases.first(5).each do |failed_case| # Show first 5 of each type
|
|
380
|
+
puts "Case #{failed_case[:index]}: #{failed_case[:description]}"
|
|
381
|
+
puts " PURL: #{failed_case[:purl]}"
|
|
382
|
+
puts " Error: #{failed_case[:error]}"
|
|
383
|
+
puts
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
if cases.length > 5
|
|
387
|
+
puts " ... and #{cases.length - 5} more #{failure_type} failures\n"
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
success_rate = ((test_data.length - failed_cases.length).to_f / test_data.length * 100).round(1)
|
|
392
|
+
puts "Overall success rate: #{success_rate}% (#{test_data.length - failed_cases.length}/#{test_data.length})"
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
desc "Validate JSON files against their schemas"
|
|
397
|
+
task :validate_schemas do
|
|
398
|
+
require "json"
|
|
399
|
+
require "json-schema"
|
|
400
|
+
|
|
401
|
+
puts "š Validating JSON files against schemas..."
|
|
402
|
+
puts "=" * 50
|
|
403
|
+
|
|
404
|
+
schemas_dir = File.join(__dir__, "schemas")
|
|
405
|
+
|
|
406
|
+
validations = [
|
|
407
|
+
{
|
|
408
|
+
name: "PURL Types Configuration",
|
|
409
|
+
data_file: "purl-types.json",
|
|
410
|
+
schema_file: File.join(schemas_dir, "purl-types.schema.json")
|
|
411
|
+
},
|
|
412
|
+
{
|
|
413
|
+
name: "Test Suite Data",
|
|
414
|
+
data_file: "test-suite-data.json",
|
|
415
|
+
schema_file: File.join(schemas_dir, "test-suite-data.schema.json")
|
|
416
|
+
}
|
|
417
|
+
]
|
|
418
|
+
|
|
419
|
+
all_valid = true
|
|
420
|
+
|
|
421
|
+
validations.each do |validation|
|
|
422
|
+
puts "\nš Validating #{validation[:name]}..."
|
|
423
|
+
|
|
424
|
+
data_path = File.join(__dir__, validation[:data_file])
|
|
425
|
+
schema_path = validation[:schema_file]
|
|
426
|
+
|
|
427
|
+
# Check if files exist
|
|
428
|
+
unless File.exist?(data_path)
|
|
429
|
+
puts " ā Data file not found: #{validation[:data_file]}"
|
|
430
|
+
all_valid = false
|
|
431
|
+
next
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
unless File.exist?(schema_path)
|
|
435
|
+
puts " ā Schema file not found: #{validation[:schema_file]}"
|
|
436
|
+
all_valid = false
|
|
437
|
+
next
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
begin
|
|
441
|
+
# Load and parse files
|
|
442
|
+
data = JSON.parse(File.read(data_path))
|
|
443
|
+
schema = JSON.parse(File.read(schema_path))
|
|
444
|
+
|
|
445
|
+
# Validate
|
|
446
|
+
errors = JSON::Validator.fully_validate(schema, data)
|
|
447
|
+
|
|
448
|
+
if errors.empty?
|
|
449
|
+
puts " ā
Valid - conforms to schema"
|
|
450
|
+
else
|
|
451
|
+
puts " ā Invalid - found #{errors.length} error(s):"
|
|
452
|
+
errors.first(5).each { |error| puts " ⢠#{error}" }
|
|
453
|
+
if errors.length > 5
|
|
454
|
+
puts " ⢠... and #{errors.length - 5} more errors"
|
|
455
|
+
end
|
|
456
|
+
all_valid = false
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
rescue JSON::ParserError => e
|
|
460
|
+
puts " ā JSON parsing error: #{e.message}"
|
|
461
|
+
all_valid = false
|
|
462
|
+
rescue => e
|
|
463
|
+
puts " ā Validation error: #{e.message}"
|
|
464
|
+
all_valid = false
|
|
465
|
+
end
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
puts "\n" + "=" * 50
|
|
469
|
+
if all_valid
|
|
470
|
+
puts "š All JSON files are valid according to their schemas!"
|
|
471
|
+
else
|
|
472
|
+
puts "ā One or more JSON files failed schema validation"
|
|
473
|
+
exit 1
|
|
474
|
+
end
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
desc "Validate all PURL examples in purl-types.json"
|
|
478
|
+
task :validate_examples do
|
|
479
|
+
require "json"
|
|
480
|
+
require_relative "lib/purl"
|
|
481
|
+
|
|
482
|
+
puts "š Validating PURL examples in purl-types.json..."
|
|
483
|
+
puts "=" * 60
|
|
484
|
+
|
|
485
|
+
project_root = __dir__
|
|
486
|
+
purl_types_data = JSON.parse(File.read(File.join(project_root, "purl-types.json")))
|
|
487
|
+
|
|
488
|
+
total_examples = 0
|
|
489
|
+
invalid_examples = []
|
|
490
|
+
|
|
491
|
+
purl_types_data["types"].each do |type_name, type_config|
|
|
492
|
+
examples = type_config["examples"]
|
|
493
|
+
next unless examples && examples.is_a?(Array)
|
|
494
|
+
|
|
495
|
+
puts "\nš¦ #{type_name} (#{examples.length} examples):"
|
|
496
|
+
|
|
497
|
+
examples.each do |example_purl|
|
|
498
|
+
total_examples += 1
|
|
499
|
+
|
|
500
|
+
begin
|
|
501
|
+
# Try to parse the example PURL
|
|
502
|
+
parsed = Purl::PackageURL.parse(example_purl)
|
|
503
|
+
|
|
504
|
+
# Verify the type matches
|
|
505
|
+
if parsed.type == type_name
|
|
506
|
+
puts " ā
#{example_purl}"
|
|
507
|
+
else
|
|
508
|
+
puts " ā #{example_purl} - Type mismatch: expected '#{type_name}', got '#{parsed.type}'"
|
|
509
|
+
invalid_examples << {
|
|
510
|
+
type: type_name,
|
|
511
|
+
example: example_purl,
|
|
512
|
+
error: "Type mismatch: expected '#{type_name}', got '#{parsed.type}'"
|
|
513
|
+
}
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
rescue => e
|
|
517
|
+
puts " ā #{example_purl} - #{e.class}: #{e.message}"
|
|
518
|
+
invalid_examples << {
|
|
519
|
+
type: type_name,
|
|
520
|
+
example: example_purl,
|
|
521
|
+
error: "#{e.class}: #{e.message}"
|
|
522
|
+
}
|
|
523
|
+
end
|
|
524
|
+
end
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
puts "\n" + "=" * 60
|
|
528
|
+
puts "š Validation Summary:"
|
|
529
|
+
puts " Total examples: #{total_examples}"
|
|
530
|
+
puts " Valid examples: #{total_examples - invalid_examples.length}"
|
|
531
|
+
puts " Invalid examples: #{invalid_examples.length}"
|
|
532
|
+
|
|
533
|
+
if invalid_examples.empty?
|
|
534
|
+
puts "\nš All PURL examples are valid!"
|
|
535
|
+
else
|
|
536
|
+
puts "\nā Found #{invalid_examples.length} invalid examples:"
|
|
537
|
+
invalid_examples.each do |invalid|
|
|
538
|
+
puts " ⢠#{invalid[:type]}: #{invalid[:example]}"
|
|
539
|
+
puts " Error: #{invalid[:error]}"
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
puts "\nš These examples should be reported upstream to the PURL specification maintainers."
|
|
543
|
+
exit 1
|
|
544
|
+
end
|
|
545
|
+
end
|
|
546
|
+
end
|
data/SECURITY.md
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Supported Versions
|
|
4
|
+
|
|
5
|
+
We actively support and provide security updates for the following versions:
|
|
6
|
+
|
|
7
|
+
| Version | Supported |
|
|
8
|
+
| ------- | ------------------ |
|
|
9
|
+
| 1.x.x | :white_check_mark: |
|
|
10
|
+
| < 1.0 | :x: |
|
|
11
|
+
|
|
12
|
+
## Reporting a Vulnerability
|
|
13
|
+
|
|
14
|
+
The Purl team takes security seriously. If you discover a security vulnerability, please follow these steps:
|
|
15
|
+
|
|
16
|
+
### 1. Do NOT Create a Public Issue
|
|
17
|
+
|
|
18
|
+
Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.
|
|
19
|
+
|
|
20
|
+
### 2. Report Privately
|
|
21
|
+
|
|
22
|
+
Send a detailed report to **andrew@ecosyste.ms** with:
|
|
23
|
+
|
|
24
|
+
- **Subject**: `[SECURITY] Purl Ruby - [Brief Description]`
|
|
25
|
+
- **Description** of the vulnerability
|
|
26
|
+
- **Steps to reproduce** the issue
|
|
27
|
+
- **Potential impact** assessment
|
|
28
|
+
- **Suggested fix** (if you have one)
|
|
29
|
+
- **Your contact information** for follow-up
|
|
30
|
+
|
|
31
|
+
### 3. What to Include
|
|
32
|
+
|
|
33
|
+
Please provide as much information as possible:
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
- Affected versions
|
|
37
|
+
- Attack vectors
|
|
38
|
+
- Proof of concept (if safe to share)
|
|
39
|
+
- Environmental details (Ruby version, OS, etc.)
|
|
40
|
+
- Any relevant configuration details
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Response Process
|
|
44
|
+
|
|
45
|
+
### Initial Response
|
|
46
|
+
|
|
47
|
+
- **24-48 hours**: We will acknowledge receipt of your report
|
|
48
|
+
- **Initial assessment**: Within 1 week of acknowledgment
|
|
49
|
+
- **Status updates**: Weekly until resolution
|
|
50
|
+
|
|
51
|
+
### Investigation
|
|
52
|
+
|
|
53
|
+
We will:
|
|
54
|
+
1. **Confirm** the vulnerability exists
|
|
55
|
+
2. **Assess** the severity and impact
|
|
56
|
+
3. **Develop** a fix and mitigation strategy
|
|
57
|
+
4. **Test** the fix thoroughly
|
|
58
|
+
5. **Coordinate** disclosure timeline
|
|
59
|
+
|
|
60
|
+
### Resolution
|
|
61
|
+
|
|
62
|
+
- **High/Critical**: Immediate fix and release
|
|
63
|
+
- **Medium**: Fix within 30 days
|
|
64
|
+
- **Low**: Fix in next regular release cycle
|
|
65
|
+
|
|
66
|
+
## Security Considerations
|
|
67
|
+
|
|
68
|
+
### Input Validation
|
|
69
|
+
|
|
70
|
+
The Purl library processes Package URL strings and performs:
|
|
71
|
+
|
|
72
|
+
- **Scheme validation**: Ensures proper `pkg:` prefix
|
|
73
|
+
- **Component parsing**: Validates type, namespace, name, version
|
|
74
|
+
- **URI encoding**: Handles percent-encoded characters
|
|
75
|
+
- **Qualifier parsing**: Processes key-value parameters
|
|
76
|
+
|
|
77
|
+
### Potential Risk Areas
|
|
78
|
+
|
|
79
|
+
Areas that warrant security attention:
|
|
80
|
+
|
|
81
|
+
1. **URL Parsing**: Malformed URLs could cause parsing errors
|
|
82
|
+
2. **Regular Expressions**: Complex patterns may be vulnerable to ReDoS
|
|
83
|
+
3. **JSON Processing**: Configuration files require safe parsing
|
|
84
|
+
4. **Network Requests**: Registry URL generation involves external URLs
|
|
85
|
+
|
|
86
|
+
### Safe Usage Practices
|
|
87
|
+
|
|
88
|
+
When using Purl in applications:
|
|
89
|
+
|
|
90
|
+
- **Validate input**: Don't trust user-provided PURL strings
|
|
91
|
+
- **Handle errors**: Properly catch and handle parsing exceptions
|
|
92
|
+
- **Sanitize output**: Be careful when displaying parsed components
|
|
93
|
+
- **Rate limiting**: If parsing many PURLs, implement appropriate limits
|
|
94
|
+
|
|
95
|
+
## Disclosure Policy
|
|
96
|
+
|
|
97
|
+
### Coordinated Disclosure
|
|
98
|
+
|
|
99
|
+
We follow coordinated disclosure principles:
|
|
100
|
+
|
|
101
|
+
1. **Private reporting** allows us to fix issues before public disclosure
|
|
102
|
+
2. **Reasonable timeline** for fixes (typically 90 days maximum)
|
|
103
|
+
3. **Credit and recognition** for responsible reporters
|
|
104
|
+
4. **Public disclosure** after fixes are available
|
|
105
|
+
|
|
106
|
+
### Public Disclosure
|
|
107
|
+
|
|
108
|
+
After a fix is released:
|
|
109
|
+
|
|
110
|
+
1. **Security advisory** published on GitHub
|
|
111
|
+
2. **CVE requested** if applicable
|
|
112
|
+
3. **Release notes** include security information
|
|
113
|
+
4. **Community notification** through appropriate channels
|
|
114
|
+
|
|
115
|
+
## Security Updates
|
|
116
|
+
|
|
117
|
+
### Notification Channels
|
|
118
|
+
|
|
119
|
+
Security updates are announced through:
|
|
120
|
+
|
|
121
|
+
- **GitHub Security Advisories**
|
|
122
|
+
- **RubyGems security alerts**
|
|
123
|
+
- **Release notes and CHANGELOG**
|
|
124
|
+
- **Project README updates**
|
|
125
|
+
|
|
126
|
+
### Update Recommendations
|
|
127
|
+
|
|
128
|
+
To stay secure:
|
|
129
|
+
|
|
130
|
+
- **Monitor** our security advisories
|
|
131
|
+
- **Update regularly** to the latest version
|
|
132
|
+
- **Review** release notes for security fixes
|
|
133
|
+
- **Subscribe** to GitHub notifications for this repository
|
|
134
|
+
|
|
135
|
+
## Bug Bounty
|
|
136
|
+
|
|
137
|
+
Currently, we do not offer a formal bug bounty program. However, we deeply appreciate security researchers who help improve the project's security posture.
|
|
138
|
+
|
|
139
|
+
### Recognition
|
|
140
|
+
|
|
141
|
+
Contributors who responsibly disclose security issues will be:
|
|
142
|
+
|
|
143
|
+
- **Credited** in security advisories (with permission)
|
|
144
|
+
- **Mentioned** in release notes
|
|
145
|
+
- **Recognized** in project documentation
|
|
146
|
+
- **Thanked** publicly (unless anonymity is requested)
|
|
147
|
+
|
|
148
|
+
## Contact Information
|
|
149
|
+
|
|
150
|
+
**Security Contact**: andrew@ecosyste.ms
|
|
151
|
+
|
|
152
|
+
**PGP Key**: Available upon request for encrypted communications
|
|
153
|
+
|
|
154
|
+
**Response Time**: We aim to acknowledge security reports within 24-48 hours
|
|
155
|
+
|
|
156
|
+
## Additional Resources
|
|
157
|
+
|
|
158
|
+
- [PURL Specification Security Considerations](https://github.com/package-url/purl-spec)
|
|
159
|
+
- [Ruby Security Best Practices](https://guides.rubyonrails.org/security.html)
|
|
160
|
+
- [OWASP Secure Coding Practices](https://owasp.org/www-project-secure-coding-practices-quick-reference-guide/)
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
Thank you for helping keep Purl and its users safe!
|