sbom 0.2.0 → 0.4.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/.ruby-version +1 -0
- data/CHANGELOG.md +12 -0
- data/README.md +40 -0
- data/exe/sbom +37 -0
- data/lib/sbom/cyclonedx/generator.rb +47 -0
- data/lib/sbom/merger.rb +120 -0
- data/lib/sbom/version.rb +1 -1
- data/lib/sbom.rb +9 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b296a99b1a2ba980aedaa542532fd88bdea816209af3a2be1999a6e4305d0167
|
|
4
|
+
data.tar.gz: 1e9a7c1e385a4ec311baac0ea055453631d559e76ab4bab67e34cfcf3166f536
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 60e7f56853ad7c7874b3d1b073c06ccbf6a455985ce86b10fba011273d0491a798a6067e866f3d399a2317150128aa035b05dbcae9b53979652fb56e3f924f73
|
|
7
|
+
data.tar.gz: 4f35f84db7c6d1dfe219e73cc7ecdefd14f701ce5fd091c504318ba7bad4c036b18dc04a17a375ecf6b0ee396a5bbf590c30d4f4d71b96812a375da54d3a84f8
|
data/.ruby-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
4.0.0
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.4.0] - 2026-01-08
|
|
4
|
+
|
|
5
|
+
- Add CycloneDX vulnerabilities array support to generator
|
|
6
|
+
|
|
7
|
+
## [0.3.0] - 2025-12-23
|
|
8
|
+
|
|
9
|
+
- Add `merge` command to CLI for combining multiple SBOMs into one
|
|
10
|
+
- Add `Sbom.merge` and `Sbom.merge_files` library methods
|
|
11
|
+
- Add `Sbom::Merger` class for merging SBOMs with configurable deduplication
|
|
12
|
+
- Merge deduplicates packages by PURL by default, with option to keep all
|
|
13
|
+
- Supports merging across formats (SPDX + CycloneDX)
|
|
14
|
+
|
|
3
15
|
## [0.2.0] - 2025-12-14
|
|
4
16
|
|
|
5
17
|
- Add `enrich` command to CLI for enriching SBOMs with data from ecosyste.ms
|
data/README.md
CHANGED
|
@@ -52,6 +52,24 @@ puts generator.output
|
|
|
52
52
|
generator = Sbom::Generator.new(sbom_type: :cyclonedx)
|
|
53
53
|
generator.generate("MyProject", sbom_data)
|
|
54
54
|
File.write("sbom.cdx.json", generator.output)
|
|
55
|
+
|
|
56
|
+
# Generate CycloneDX with vulnerabilities
|
|
57
|
+
data = {
|
|
58
|
+
packages: packages_data,
|
|
59
|
+
vulnerabilities: [
|
|
60
|
+
{
|
|
61
|
+
id: "CVE-2024-1234",
|
|
62
|
+
source: { name: "OSV", url: "https://osv.dev" },
|
|
63
|
+
ratings: [{ severity: "high", score: 8.1, method: "CVSSv31" }],
|
|
64
|
+
description: "A critical vulnerability",
|
|
65
|
+
affects: [{ ref: "pkg:npm/lodash@4.17.20" }],
|
|
66
|
+
published: "2024-01-15T00:00:00Z",
|
|
67
|
+
updated: "2024-01-20T12:00:00Z"
|
|
68
|
+
}
|
|
69
|
+
]
|
|
70
|
+
}
|
|
71
|
+
generator = Sbom::Generator.new(sbom_type: :cyclonedx)
|
|
72
|
+
generator.generate("MyProject", data)
|
|
55
73
|
```
|
|
56
74
|
|
|
57
75
|
### Validating SBOMs
|
|
@@ -85,6 +103,23 @@ enriched = Sbom.enrich_file("example.cdx.json")
|
|
|
85
103
|
|
|
86
104
|
Enrichment adds: description, homepage, download location, license, repository URL, registry URL, documentation URL, supplier info, and security advisories.
|
|
87
105
|
|
|
106
|
+
### Merging SBOMs
|
|
107
|
+
|
|
108
|
+
Combine multiple SBOMs into one:
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
# Merge from files (dedupes by PURL by default)
|
|
112
|
+
merged = Sbom.merge_files(["app1.cdx.json", "app2.spdx.json"])
|
|
113
|
+
|
|
114
|
+
# Merge SBOM objects
|
|
115
|
+
merged = Sbom.merge([sbom1, sbom2, sbom3])
|
|
116
|
+
|
|
117
|
+
# Keep all packages without deduplication
|
|
118
|
+
merged = Sbom.merge([sbom1, sbom2], dedupe: :none)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Merging works across formats. Packages are deduplicated by PURL by default. Relationships and licenses are also deduplicated.
|
|
122
|
+
|
|
88
123
|
### Building Packages
|
|
89
124
|
|
|
90
125
|
The Package class provides an object interface for building package data:
|
|
@@ -139,6 +174,11 @@ sbom document query example.cdx.json --license MIT
|
|
|
139
174
|
sbom enrich example.cdx.json
|
|
140
175
|
sbom enrich example.cdx.json --output enriched.json
|
|
141
176
|
cat example.cdx.json | sbom enrich -
|
|
177
|
+
|
|
178
|
+
# Merge multiple SBOMs
|
|
179
|
+
sbom merge app1.cdx.json app2.spdx.json --output merged.json
|
|
180
|
+
sbom merge app1.json app2.json --no-dedupe
|
|
181
|
+
sbom merge app1.json app2.json --type cyclonedx
|
|
142
182
|
```
|
|
143
183
|
|
|
144
184
|
## Supported Formats
|
data/exe/sbom
CHANGED
|
@@ -27,6 +27,8 @@ module Sbom
|
|
|
27
27
|
convert_command
|
|
28
28
|
when "enrich"
|
|
29
29
|
enrich_command
|
|
30
|
+
when "merge"
|
|
31
|
+
merge_command
|
|
30
32
|
when "document"
|
|
31
33
|
document_command
|
|
32
34
|
when "version", "-v", "--version"
|
|
@@ -205,6 +207,40 @@ module Sbom
|
|
|
205
207
|
abort "Error: #{e.message}"
|
|
206
208
|
end
|
|
207
209
|
|
|
210
|
+
def merge_command
|
|
211
|
+
parser = OptionParser.new do |opts|
|
|
212
|
+
opts.banner = "Usage: sbom merge <file1> <file2> [file3...] [options]"
|
|
213
|
+
opts.on("-t", "--type TYPE", "Output SBOM type (spdx, cyclonedx)") { |v| @options[:type] = v.to_sym }
|
|
214
|
+
opts.on("-f", "--format FORMAT", "Output format (json, yaml, tag)") { |v| @options[:format] = v.to_sym }
|
|
215
|
+
opts.on("-o", "--output FILE", "Output file") { |v| @options[:output] = v }
|
|
216
|
+
opts.on("--no-dedupe", "Keep all packages (don't dedupe by PURL)") { @options[:dedupe] = :none }
|
|
217
|
+
opts.on("-h", "--help", "Show help") { puts opts; exit }
|
|
218
|
+
end
|
|
219
|
+
parser.parse!(@args)
|
|
220
|
+
|
|
221
|
+
abort "Error: At least 2 files required" if @args.size < 2
|
|
222
|
+
|
|
223
|
+
dedupe = @options[:dedupe] || :purl
|
|
224
|
+
merged = Sbom::Merger.merge_files(@args, dedupe: dedupe)
|
|
225
|
+
|
|
226
|
+
output_type = @options[:type] || merged.sbom_type || :spdx
|
|
227
|
+
output_format = @options[:format] || :json
|
|
228
|
+
|
|
229
|
+
generator = Sbom::Generator.new(sbom_type: output_type, format: output_format)
|
|
230
|
+
generator.generate(merged.document&.dig(:name) || "Merged SBOM", merged.to_h)
|
|
231
|
+
|
|
232
|
+
output = generator.output
|
|
233
|
+
|
|
234
|
+
if @options[:output]
|
|
235
|
+
File.write(@options[:output], output)
|
|
236
|
+
puts "Merged SBOM written to #{@options[:output]}"
|
|
237
|
+
else
|
|
238
|
+
puts output
|
|
239
|
+
end
|
|
240
|
+
rescue Sbom::ParserError, Sbom::GeneratorError => e
|
|
241
|
+
abort "Error: #{e.message}"
|
|
242
|
+
end
|
|
243
|
+
|
|
208
244
|
def document_command
|
|
209
245
|
subcommand = @args.shift
|
|
210
246
|
|
|
@@ -377,6 +413,7 @@ module Sbom
|
|
|
377
413
|
validate Validate SBOM against schema
|
|
378
414
|
convert Convert between SBOM formats
|
|
379
415
|
enrich Enrich SBOM with data from ecosyste.ms
|
|
416
|
+
merge Combine multiple SBOMs into one
|
|
380
417
|
document Work with SBOM documents
|
|
381
418
|
version Show version
|
|
382
419
|
|
|
@@ -24,6 +24,7 @@ module Sbom
|
|
|
24
24
|
@output = {}
|
|
25
25
|
@components = []
|
|
26
26
|
@dependencies = []
|
|
27
|
+
@vulnerabilities = []
|
|
27
28
|
@element_refs = {}
|
|
28
29
|
end
|
|
29
30
|
|
|
@@ -41,6 +42,7 @@ module Sbom
|
|
|
41
42
|
generate_document_header(project_name, component_data, uuid, bom_version)
|
|
42
43
|
generate_components(data[:packages])
|
|
43
44
|
generate_dependencies(data[:relationships])
|
|
45
|
+
generate_vulnerabilities(data[:vulnerabilities])
|
|
44
46
|
|
|
45
47
|
finalize_output
|
|
46
48
|
end
|
|
@@ -234,9 +236,54 @@ module Sbom
|
|
|
234
236
|
end
|
|
235
237
|
end
|
|
236
238
|
|
|
239
|
+
def generate_vulnerabilities(vulnerabilities_data)
|
|
240
|
+
return unless vulnerabilities_data&.any?
|
|
241
|
+
|
|
242
|
+
vulnerabilities_data.each do |vuln|
|
|
243
|
+
generate_vulnerability(vuln)
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def generate_vulnerability(vuln)
|
|
248
|
+
return unless vuln[:id]
|
|
249
|
+
|
|
250
|
+
vulnerability = { "id" => vuln[:id] }
|
|
251
|
+
|
|
252
|
+
if vuln[:source]
|
|
253
|
+
source = {}
|
|
254
|
+
source["name"] = vuln[:source][:name] if vuln[:source][:name]
|
|
255
|
+
source["url"] = vuln[:source][:url] if vuln[:source][:url]
|
|
256
|
+
vulnerability["source"] = source if source.any?
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
if vuln[:ratings]&.any?
|
|
260
|
+
vulnerability["ratings"] = vuln[:ratings].map do |rating|
|
|
261
|
+
r = {}
|
|
262
|
+
r["severity"] = rating[:severity] if rating[:severity]
|
|
263
|
+
r["score"] = rating[:score] if rating[:score]
|
|
264
|
+
r["method"] = rating[:method] if rating[:method]
|
|
265
|
+
r
|
|
266
|
+
end.reject(&:empty?)
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
vulnerability["description"] = vuln[:description] if vuln[:description]
|
|
270
|
+
|
|
271
|
+
if vuln[:affects]&.any?
|
|
272
|
+
vulnerability["affects"] = vuln[:affects].map do |affect|
|
|
273
|
+
{ "ref" => affect[:ref] }
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
vulnerability["published"] = vuln[:published] if vuln[:published]
|
|
278
|
+
vulnerability["updated"] = vuln[:updated] if vuln[:updated]
|
|
279
|
+
|
|
280
|
+
@vulnerabilities << vulnerability
|
|
281
|
+
end
|
|
282
|
+
|
|
237
283
|
def finalize_output
|
|
238
284
|
@output["components"] = @components if @components.any?
|
|
239
285
|
@output["dependencies"] = @dependencies if @dependencies.any?
|
|
286
|
+
@output["vulnerabilities"] = @vulnerabilities if @vulnerabilities.any?
|
|
240
287
|
end
|
|
241
288
|
|
|
242
289
|
def version_at_least?(version)
|
data/lib/sbom/merger.rb
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sbom
|
|
4
|
+
class Merger
|
|
5
|
+
attr_reader :sboms, :options, :result
|
|
6
|
+
|
|
7
|
+
def initialize(sboms, dedupe: :purl)
|
|
8
|
+
@sboms = sboms
|
|
9
|
+
@options = { dedupe: dedupe }
|
|
10
|
+
@result = nil
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def merge
|
|
14
|
+
@result = Data::Sbom.new(sbom_type: determine_sbom_type)
|
|
15
|
+
@result.version = determine_version
|
|
16
|
+
|
|
17
|
+
build_document
|
|
18
|
+
merge_packages
|
|
19
|
+
merge_files
|
|
20
|
+
merge_relationships
|
|
21
|
+
merge_licenses
|
|
22
|
+
merge_annotations
|
|
23
|
+
|
|
24
|
+
@result
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def determine_sbom_type
|
|
28
|
+
types = @sboms.map(&:sbom_type).uniq
|
|
29
|
+
return types.first if types.size == 1
|
|
30
|
+
|
|
31
|
+
:spdx
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def determine_version
|
|
35
|
+
versions = @sboms.map(&:version).compact.uniq
|
|
36
|
+
versions.first
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def build_document
|
|
40
|
+
names = @sboms.map { |s| s.document&.dig(:name) }.compact
|
|
41
|
+
merged_name = names.any? ? "Merged: #{names.join(', ')}" : "Merged SBOM"
|
|
42
|
+
|
|
43
|
+
@result.document = {
|
|
44
|
+
name: merged_name,
|
|
45
|
+
id: "SPDXRef-DOCUMENT",
|
|
46
|
+
created: Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
47
|
+
}
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def merge_packages
|
|
51
|
+
seen_purls = {}
|
|
52
|
+
|
|
53
|
+
@sboms.each do |sbom|
|
|
54
|
+
sbom.packages.each do |pkg|
|
|
55
|
+
if @options[:dedupe] == :purl && pkg[:purl]
|
|
56
|
+
next if seen_purls[pkg[:purl]]
|
|
57
|
+
|
|
58
|
+
seen_purls[pkg[:purl]] = true
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
@result.add_package(pkg)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def merge_files
|
|
67
|
+
@sboms.each do |sbom|
|
|
68
|
+
sbom.files.each do |file|
|
|
69
|
+
@result.add_file(file)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def merge_relationships
|
|
75
|
+
seen = Set.new
|
|
76
|
+
|
|
77
|
+
@sboms.each do |sbom|
|
|
78
|
+
sbom.relationships.each do |rel|
|
|
79
|
+
key = [rel[:source], rel[:type], rel[:target]]
|
|
80
|
+
next if seen.include?(key)
|
|
81
|
+
|
|
82
|
+
seen.add(key)
|
|
83
|
+
@result.add_relationship(rel)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def merge_licenses
|
|
89
|
+
seen = Set.new
|
|
90
|
+
|
|
91
|
+
@sboms.each do |sbom|
|
|
92
|
+
sbom.licenses.each do |lic|
|
|
93
|
+
next if seen.include?(lic)
|
|
94
|
+
|
|
95
|
+
seen.add(lic)
|
|
96
|
+
@result.add_license(lic)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def merge_annotations
|
|
102
|
+
@sboms.each do |sbom|
|
|
103
|
+
sbom.annotations.each do |ann|
|
|
104
|
+
@result.add_annotation(ann)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
class << self
|
|
110
|
+
def merge(sboms, dedupe: :purl)
|
|
111
|
+
new(sboms, dedupe: dedupe).merge
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def merge_files(filenames, dedupe: :purl)
|
|
115
|
+
sboms = filenames.map { |f| Parser.parse_file(f) }
|
|
116
|
+
merge(sboms, dedupe: dedupe)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
data/lib/sbom/version.rb
CHANGED
data/lib/sbom.rb
CHANGED
|
@@ -33,6 +33,7 @@ require_relative "sbom/validation_result"
|
|
|
33
33
|
require_relative "sbom/validator"
|
|
34
34
|
require_relative "sbom/output"
|
|
35
35
|
require_relative "sbom/enricher"
|
|
36
|
+
require_relative "sbom/merger"
|
|
36
37
|
|
|
37
38
|
module Sbom
|
|
38
39
|
class << self
|
|
@@ -60,5 +61,13 @@ module Sbom
|
|
|
60
61
|
sbom = parse_file(filename, sbom_type: sbom_type)
|
|
61
62
|
Enricher.enrich(sbom)
|
|
62
63
|
end
|
|
64
|
+
|
|
65
|
+
def merge(sboms, dedupe: :purl)
|
|
66
|
+
Merger.merge(sboms, dedupe: dedupe)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def merge_files(filenames, dedupe: :purl)
|
|
70
|
+
Merger.merge_files(filenames, dedupe: dedupe)
|
|
71
|
+
end
|
|
63
72
|
end
|
|
64
73
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: sbom
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrew Nesbitt
|
|
@@ -61,6 +61,7 @@ extensions: []
|
|
|
61
61
|
extra_rdoc_files: []
|
|
62
62
|
files:
|
|
63
63
|
- ".gitmodules"
|
|
64
|
+
- ".ruby-version"
|
|
64
65
|
- CHANGELOG.md
|
|
65
66
|
- CODE_OF_CONDUCT.md
|
|
66
67
|
- CONTRIBUTING.md
|
|
@@ -82,6 +83,7 @@ files:
|
|
|
82
83
|
- lib/sbom/generator.rb
|
|
83
84
|
- lib/sbom/license/data/spdx_licenses.json
|
|
84
85
|
- lib/sbom/license/scanner.rb
|
|
86
|
+
- lib/sbom/merger.rb
|
|
85
87
|
- lib/sbom/output.rb
|
|
86
88
|
- lib/sbom/parser.rb
|
|
87
89
|
- lib/sbom/spdx/generator.rb
|
|
@@ -112,7 +114,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
112
114
|
- !ruby/object:Gem::Version
|
|
113
115
|
version: '0'
|
|
114
116
|
requirements: []
|
|
115
|
-
rubygems_version: 4.0.
|
|
117
|
+
rubygems_version: 4.0.3
|
|
116
118
|
specification_version: 4
|
|
117
119
|
summary: Parse, generate, and validate Software Bill of Materials (SBOM)
|
|
118
120
|
test_files: []
|