sbom 0.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 +7 -0
- data/.gitmodules +7 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/README.md +153 -0
- data/Rakefile +8 -0
- data/exe/sbom +346 -0
- data/lib/sbom/cyclonedx/generator.rb +307 -0
- data/lib/sbom/cyclonedx/parser.rb +275 -0
- data/lib/sbom/data/document.rb +143 -0
- data/lib/sbom/data/file.rb +169 -0
- data/lib/sbom/data/package.rb +417 -0
- data/lib/sbom/data/relationship.rb +43 -0
- data/lib/sbom/data/sbom.rb +124 -0
- data/lib/sbom/error.rb +13 -0
- data/lib/sbom/generator.rb +79 -0
- data/lib/sbom/license/data/spdx_licenses.json +8533 -0
- data/lib/sbom/license/scanner.rb +101 -0
- data/lib/sbom/output.rb +88 -0
- data/lib/sbom/parser.rb +111 -0
- data/lib/sbom/spdx/generator.rb +337 -0
- data/lib/sbom/spdx/parser.rb +426 -0
- data/lib/sbom/validation_result.rb +30 -0
- data/lib/sbom/validator.rb +261 -0
- data/lib/sbom/version.rb +5 -0
- data/lib/sbom.rb +54 -0
- data/sig/sbom.rbs +4 -0
- metadata +114 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "pathname"
|
|
5
|
+
|
|
6
|
+
module Sbom
|
|
7
|
+
class Validator
|
|
8
|
+
SPDX_VERSIONS = %w[2.2 2.3].freeze
|
|
9
|
+
CYCLONEDX_VERSIONS = %w[1.4 1.5 1.6 1.7].freeze
|
|
10
|
+
|
|
11
|
+
EXTENSION_MAP = {
|
|
12
|
+
".spdx" => :spdx,
|
|
13
|
+
".spdx.json" => :spdx,
|
|
14
|
+
".spdx.yaml" => :spdx,
|
|
15
|
+
".spdx.yml" => :spdx,
|
|
16
|
+
".spdx.xml" => :spdx,
|
|
17
|
+
".spdx.rdf" => :spdx,
|
|
18
|
+
".cdx.json" => :cyclonedx,
|
|
19
|
+
".bom.json" => :cyclonedx,
|
|
20
|
+
".cdx.xml" => :cyclonedx,
|
|
21
|
+
".bom.xml" => :cyclonedx
|
|
22
|
+
}.freeze
|
|
23
|
+
|
|
24
|
+
def initialize(sbom_type: :auto, version: nil, schema_dir: nil)
|
|
25
|
+
@sbom_type = sbom_type
|
|
26
|
+
@version = version
|
|
27
|
+
@schema_dir = schema_dir || default_schema_dir
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def validate_file(filename)
|
|
31
|
+
raise ValidatorError, "File not found: #{filename}" unless File.exist?(filename)
|
|
32
|
+
raise ValidatorError, "Empty file: #{filename}" if File.size(filename).zero?
|
|
33
|
+
|
|
34
|
+
content = File.read(filename)
|
|
35
|
+
sbom_type = detect_type(filename, content)
|
|
36
|
+
|
|
37
|
+
validate_content(content, sbom_type)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def validate_file!(filename)
|
|
41
|
+
result = validate_file(filename)
|
|
42
|
+
raise ValidatorError, "Invalid SBOM: #{result.errors.join(', ')}" if result.invalid?
|
|
43
|
+
|
|
44
|
+
result
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def validate_string(content, sbom_type: nil)
|
|
48
|
+
sbom_type ||= detect_type_from_content(content)
|
|
49
|
+
validate_content(content, sbom_type)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def validate_string!(content, sbom_type: nil)
|
|
53
|
+
result = validate_string(content, sbom_type: sbom_type)
|
|
54
|
+
raise ValidatorError, "Invalid SBOM: #{result.errors.join(', ')}" if result.invalid?
|
|
55
|
+
|
|
56
|
+
result
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def self.validate_file(filename, sbom_type: :auto)
|
|
60
|
+
new(sbom_type: sbom_type).validate_file(filename)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def self.validate_file!(filename, sbom_type: :auto)
|
|
64
|
+
new(sbom_type: sbom_type).validate_file!(filename)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def default_schema_dir
|
|
70
|
+
spec_dir = File.expand_path("../../spec", __dir__)
|
|
71
|
+
return spec_dir if File.directory?(spec_dir)
|
|
72
|
+
|
|
73
|
+
nil
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def detect_type(filename, content)
|
|
77
|
+
return @sbom_type unless @sbom_type == :auto
|
|
78
|
+
|
|
79
|
+
EXTENSION_MAP.each do |ext, type|
|
|
80
|
+
return type if filename.end_with?(ext)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
detect_type_from_content(content)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def detect_type_from_content(content)
|
|
87
|
+
stripped = content.strip
|
|
88
|
+
|
|
89
|
+
if stripped.start_with?("{")
|
|
90
|
+
begin
|
|
91
|
+
data = JSON.parse(stripped)
|
|
92
|
+
return :cyclonedx if data["bomFormat"] == "CycloneDX"
|
|
93
|
+
return :spdx if data["spdxVersion"]
|
|
94
|
+
rescue JSON::ParserError
|
|
95
|
+
nil
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
return :spdx if stripped.include?("SPDXVersion:")
|
|
100
|
+
return :spdx if stripped.include?("<spdx:")
|
|
101
|
+
return :cyclonedx if stripped.include?("cyclonedx")
|
|
102
|
+
|
|
103
|
+
:unknown
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def validate_content(content, sbom_type)
|
|
107
|
+
case sbom_type
|
|
108
|
+
when :spdx
|
|
109
|
+
validate_spdx(content)
|
|
110
|
+
when :cyclonedx
|
|
111
|
+
validate_cyclonedx(content)
|
|
112
|
+
else
|
|
113
|
+
ValidationResult.new(valid: false, errors: ["Unknown SBOM format"])
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def validate_spdx(content)
|
|
118
|
+
stripped = content.strip
|
|
119
|
+
|
|
120
|
+
if stripped.start_with?("{")
|
|
121
|
+
validate_spdx_json(content)
|
|
122
|
+
elsif stripped.include?("SPDXID:")
|
|
123
|
+
validate_spdx_yaml(content)
|
|
124
|
+
else
|
|
125
|
+
validate_spdx_tag(content)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def validate_spdx_json(content)
|
|
130
|
+
unless json_schemer_available?
|
|
131
|
+
return ValidationResult.new(valid: true, format: :spdx, version: extract_spdx_version_json(content))
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
schema_path = spdx_schema_path
|
|
135
|
+
unless schema_path && File.exist?(schema_path)
|
|
136
|
+
return ValidationResult.new(valid: true, format: :spdx, version: extract_spdx_version_json(content))
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
begin
|
|
140
|
+
data = JSON.parse(content)
|
|
141
|
+
schema = JSONSchemer.schema(Pathname.new(schema_path))
|
|
142
|
+
errors = schema.validate(data).map { |e| e["error"] }
|
|
143
|
+
|
|
144
|
+
if errors.empty?
|
|
145
|
+
version = data["spdxVersion"]&.gsub("SPDX-", "")
|
|
146
|
+
ValidationResult.new(valid: true, format: :spdx, version: version)
|
|
147
|
+
else
|
|
148
|
+
ValidationResult.new(valid: false, format: :spdx, errors: errors.first(5))
|
|
149
|
+
end
|
|
150
|
+
rescue JSON::ParserError => e
|
|
151
|
+
ValidationResult.new(valid: false, format: :spdx, errors: ["JSON parse error: #{e.message}"])
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def validate_spdx_yaml(content)
|
|
156
|
+
begin
|
|
157
|
+
data = YAML.safe_load(content)
|
|
158
|
+
version = data["spdxVersion"]&.gsub("SPDX-", "")
|
|
159
|
+
ValidationResult.new(valid: true, format: :spdx, version: version)
|
|
160
|
+
rescue Psych::SyntaxError => e
|
|
161
|
+
ValidationResult.new(valid: false, format: :spdx, errors: ["YAML parse error: #{e.message}"])
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def validate_spdx_tag(content)
|
|
166
|
+
version = nil
|
|
167
|
+
content.each_line do |line|
|
|
168
|
+
if line.start_with?("SPDXVersion:")
|
|
169
|
+
version = line.split(":").last.strip.gsub("SPDX-", "")
|
|
170
|
+
break
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
ValidationResult.new(valid: true, format: :spdx, version: version)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def validate_cyclonedx(content)
|
|
178
|
+
stripped = content.strip
|
|
179
|
+
|
|
180
|
+
if stripped.start_with?("{")
|
|
181
|
+
validate_cyclonedx_json(content)
|
|
182
|
+
else
|
|
183
|
+
validate_cyclonedx_xml(content)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def validate_cyclonedx_json(content)
|
|
188
|
+
unless json_schemer_available?
|
|
189
|
+
return ValidationResult.new(valid: true, format: :cyclonedx, version: extract_cyclonedx_version_json(content))
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
versions_to_try = @version ? [@version] : CYCLONEDX_VERSIONS.reverse
|
|
193
|
+
|
|
194
|
+
begin
|
|
195
|
+
data = JSON.parse(content)
|
|
196
|
+
rescue JSON::ParserError => e
|
|
197
|
+
return ValidationResult.new(valid: false, format: :cyclonedx, errors: ["JSON parse error: #{e.message}"])
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
versions_to_try.each do |version|
|
|
201
|
+
schema_path = cyclonedx_schema_path(version)
|
|
202
|
+
next unless schema_path && File.exist?(schema_path)
|
|
203
|
+
|
|
204
|
+
schema = JSONSchemer.schema(Pathname.new(schema_path))
|
|
205
|
+
return ValidationResult.new(valid: true, format: :cyclonedx, version: version) if schema.valid?(data)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
ValidationResult.new(valid: false, format: :cyclonedx, errors: ["Does not match any known CycloneDX schema"])
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def validate_cyclonedx_xml(content)
|
|
212
|
+
begin
|
|
213
|
+
doc = REXML::Document.new(content)
|
|
214
|
+
root = doc.root
|
|
215
|
+
|
|
216
|
+
if root && root.name == "bom"
|
|
217
|
+
namespace = root.namespace
|
|
218
|
+
version = namespace&.match(/bom[\/\-](\d+\.\d+)/)&.captures&.first
|
|
219
|
+
return ValidationResult.new(valid: true, format: :cyclonedx, version: version)
|
|
220
|
+
end
|
|
221
|
+
rescue REXML::ParseException => e
|
|
222
|
+
return ValidationResult.new(valid: false, format: :cyclonedx, errors: ["XML parse error: #{e.message}"])
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
ValidationResult.new(valid: false, format: :cyclonedx, errors: ["Not a valid CycloneDX XML document"])
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def extract_spdx_version_json(content)
|
|
229
|
+
data = JSON.parse(content)
|
|
230
|
+
data["spdxVersion"]&.gsub("SPDX-", "")
|
|
231
|
+
rescue JSON::ParserError
|
|
232
|
+
nil
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def extract_cyclonedx_version_json(content)
|
|
236
|
+
data = JSON.parse(content)
|
|
237
|
+
data["specVersion"]
|
|
238
|
+
rescue JSON::ParserError
|
|
239
|
+
nil
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def spdx_schema_path
|
|
243
|
+
return nil unless @schema_dir
|
|
244
|
+
|
|
245
|
+
File.join(@schema_dir, "spdx", "schemas", "spdx-schema.json")
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def cyclonedx_schema_path(version)
|
|
249
|
+
return nil unless @schema_dir
|
|
250
|
+
|
|
251
|
+
File.join(@schema_dir, "cyclonedx", "schema", "bom-#{version}.schema.json")
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def json_schemer_available?
|
|
255
|
+
require "json_schemer"
|
|
256
|
+
true
|
|
257
|
+
rescue LoadError
|
|
258
|
+
false
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|
data/lib/sbom/version.rb
ADDED
data/lib/sbom.rb
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "yaml"
|
|
5
|
+
require "rexml/document"
|
|
6
|
+
require "purl"
|
|
7
|
+
|
|
8
|
+
require_relative "sbom/version"
|
|
9
|
+
require_relative "sbom/error"
|
|
10
|
+
|
|
11
|
+
# Data models
|
|
12
|
+
require_relative "sbom/data/document"
|
|
13
|
+
require_relative "sbom/data/package"
|
|
14
|
+
require_relative "sbom/data/file"
|
|
15
|
+
require_relative "sbom/data/relationship"
|
|
16
|
+
require_relative "sbom/data/sbom"
|
|
17
|
+
|
|
18
|
+
# License handling
|
|
19
|
+
require_relative "sbom/license/scanner"
|
|
20
|
+
|
|
21
|
+
# SPDX implementation
|
|
22
|
+
require_relative "sbom/spdx/parser"
|
|
23
|
+
require_relative "sbom/spdx/generator"
|
|
24
|
+
|
|
25
|
+
# CycloneDX implementation
|
|
26
|
+
require_relative "sbom/cyclonedx/parser"
|
|
27
|
+
require_relative "sbom/cyclonedx/generator"
|
|
28
|
+
|
|
29
|
+
# Facade classes
|
|
30
|
+
require_relative "sbom/parser"
|
|
31
|
+
require_relative "sbom/generator"
|
|
32
|
+
require_relative "sbom/validation_result"
|
|
33
|
+
require_relative "sbom/validator"
|
|
34
|
+
require_relative "sbom/output"
|
|
35
|
+
|
|
36
|
+
module Sbom
|
|
37
|
+
class << self
|
|
38
|
+
def parse_file(filename, sbom_type: :auto)
|
|
39
|
+
Parser.parse_file(filename, sbom_type: sbom_type)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def parse_string(content, sbom_type: :auto)
|
|
43
|
+
Parser.parse_string(content, sbom_type: sbom_type)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def generate(project_name, sbom_data, sbom_type: :spdx, format: :json)
|
|
47
|
+
Generator.generate(project_name, sbom_data, sbom_type: sbom_type, format: format)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def validate_file(filename, sbom_type: :auto)
|
|
51
|
+
Validator.validate_file(filename, sbom_type: sbom_type)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
data/sig/sbom.rbs
ADDED
metadata
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: sbom
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Andrew Nesbitt
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: json_schemer
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '2.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '2.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: purl
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '1.6'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '1.6'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rexml
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '3.2'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '3.2'
|
|
54
|
+
description: A Ruby library for working with Software Bill of Materials in SPDX and
|
|
55
|
+
CycloneDX formats. Supports parsing, generation, validation, and format conversion.
|
|
56
|
+
email:
|
|
57
|
+
- andrewnez@gmail.com
|
|
58
|
+
executables:
|
|
59
|
+
- sbom
|
|
60
|
+
extensions: []
|
|
61
|
+
extra_rdoc_files: []
|
|
62
|
+
files:
|
|
63
|
+
- ".gitmodules"
|
|
64
|
+
- CHANGELOG.md
|
|
65
|
+
- CODE_OF_CONDUCT.md
|
|
66
|
+
- README.md
|
|
67
|
+
- Rakefile
|
|
68
|
+
- exe/sbom
|
|
69
|
+
- lib/sbom.rb
|
|
70
|
+
- lib/sbom/cyclonedx/generator.rb
|
|
71
|
+
- lib/sbom/cyclonedx/parser.rb
|
|
72
|
+
- lib/sbom/data/document.rb
|
|
73
|
+
- lib/sbom/data/file.rb
|
|
74
|
+
- lib/sbom/data/package.rb
|
|
75
|
+
- lib/sbom/data/relationship.rb
|
|
76
|
+
- lib/sbom/data/sbom.rb
|
|
77
|
+
- lib/sbom/error.rb
|
|
78
|
+
- lib/sbom/generator.rb
|
|
79
|
+
- lib/sbom/license/data/spdx_licenses.json
|
|
80
|
+
- lib/sbom/license/scanner.rb
|
|
81
|
+
- lib/sbom/output.rb
|
|
82
|
+
- lib/sbom/parser.rb
|
|
83
|
+
- lib/sbom/spdx/generator.rb
|
|
84
|
+
- lib/sbom/spdx/parser.rb
|
|
85
|
+
- lib/sbom/validation_result.rb
|
|
86
|
+
- lib/sbom/validator.rb
|
|
87
|
+
- lib/sbom/version.rb
|
|
88
|
+
- sig/sbom.rbs
|
|
89
|
+
homepage: https://github.com/andrew/sbom
|
|
90
|
+
licenses:
|
|
91
|
+
- MIT
|
|
92
|
+
metadata:
|
|
93
|
+
homepage_uri: https://github.com/andrew/sbom
|
|
94
|
+
source_code_uri: https://github.com/andrew/sbom
|
|
95
|
+
changelog_uri: https://github.com/andrew/sbom/blob/main/CHANGELOG.md
|
|
96
|
+
rubygems_mfa_required: 'true'
|
|
97
|
+
rdoc_options: []
|
|
98
|
+
require_paths:
|
|
99
|
+
- lib
|
|
100
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
101
|
+
requirements:
|
|
102
|
+
- - ">="
|
|
103
|
+
- !ruby/object:Gem::Version
|
|
104
|
+
version: 3.2.0
|
|
105
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - ">="
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: '0'
|
|
110
|
+
requirements: []
|
|
111
|
+
rubygems_version: 4.0.1
|
|
112
|
+
specification_version: 4
|
|
113
|
+
summary: Parse, generate, and validate Software Bill of Materials (SBOM)
|
|
114
|
+
test_files: []
|