sbom 0.2.0 → 0.3.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 +8 -0
- data/README.md +22 -0
- data/exe/sbom +37 -0
- data/lib/sbom/merger.rb +120 -0
- data/lib/sbom/version.rb +1 -1
- data/lib/sbom.rb +9 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '080bc0e7e22560d9779d7dc7914a6fd6d440dee48508da1db84d1c9d03fd3c5b'
|
|
4
|
+
data.tar.gz: 007f185f1ebc3a95bac42ca6419155e9a269bb0b98e1be5a478b6eab5f43e06c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e67dca4dfb55a94d8f55093938773389206f69d5765748323bed4672da6182d76e6d503bf96bda90bde6c721cabac9b21f061714de005fba6c4d4d25e874e4d5
|
|
7
|
+
data.tar.gz: fa270f7dad9214c3a15c1b1bad23d3982955bd262f4bdb332b6bf73f464ac9e846761f8ef01e4fb34058cbbe3253d80ee1f2c04460fa0e385754364acf75e1e9
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.3.0] - 2025-12-23
|
|
4
|
+
|
|
5
|
+
- Add `merge` command to CLI for combining multiple SBOMs into one
|
|
6
|
+
- Add `Sbom.merge` and `Sbom.merge_files` library methods
|
|
7
|
+
- Add `Sbom::Merger` class for merging SBOMs with configurable deduplication
|
|
8
|
+
- Merge deduplicates packages by PURL by default, with option to keep all
|
|
9
|
+
- Supports merging across formats (SPDX + CycloneDX)
|
|
10
|
+
|
|
3
11
|
## [0.2.0] - 2025-12-14
|
|
4
12
|
|
|
5
13
|
- Add `enrich` command to CLI for enriching SBOMs with data from ecosyste.ms
|
data/README.md
CHANGED
|
@@ -85,6 +85,23 @@ enriched = Sbom.enrich_file("example.cdx.json")
|
|
|
85
85
|
|
|
86
86
|
Enrichment adds: description, homepage, download location, license, repository URL, registry URL, documentation URL, supplier info, and security advisories.
|
|
87
87
|
|
|
88
|
+
### Merging SBOMs
|
|
89
|
+
|
|
90
|
+
Combine multiple SBOMs into one:
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
# Merge from files (dedupes by PURL by default)
|
|
94
|
+
merged = Sbom.merge_files(["app1.cdx.json", "app2.spdx.json"])
|
|
95
|
+
|
|
96
|
+
# Merge SBOM objects
|
|
97
|
+
merged = Sbom.merge([sbom1, sbom2, sbom3])
|
|
98
|
+
|
|
99
|
+
# Keep all packages without deduplication
|
|
100
|
+
merged = Sbom.merge([sbom1, sbom2], dedupe: :none)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Merging works across formats. Packages are deduplicated by PURL by default. Relationships and licenses are also deduplicated.
|
|
104
|
+
|
|
88
105
|
### Building Packages
|
|
89
106
|
|
|
90
107
|
The Package class provides an object interface for building package data:
|
|
@@ -139,6 +156,11 @@ sbom document query example.cdx.json --license MIT
|
|
|
139
156
|
sbom enrich example.cdx.json
|
|
140
157
|
sbom enrich example.cdx.json --output enriched.json
|
|
141
158
|
cat example.cdx.json | sbom enrich -
|
|
159
|
+
|
|
160
|
+
# Merge multiple SBOMs
|
|
161
|
+
sbom merge app1.cdx.json app2.spdx.json --output merged.json
|
|
162
|
+
sbom merge app1.json app2.json --no-dedupe
|
|
163
|
+
sbom merge app1.json app2.json --type cyclonedx
|
|
142
164
|
```
|
|
143
165
|
|
|
144
166
|
## 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
|
|
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.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrew Nesbitt
|
|
@@ -82,6 +82,7 @@ files:
|
|
|
82
82
|
- lib/sbom/generator.rb
|
|
83
83
|
- lib/sbom/license/data/spdx_licenses.json
|
|
84
84
|
- lib/sbom/license/scanner.rb
|
|
85
|
+
- lib/sbom/merger.rb
|
|
85
86
|
- lib/sbom/output.rb
|
|
86
87
|
- lib/sbom/parser.rb
|
|
87
88
|
- lib/sbom/spdx/generator.rb
|