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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a22cb6e2f3394ccd7abb7e8a28d7282f11e4a9277cf1451dca16060d00ceb0f6
4
- data.tar.gz: aaf00ea47acdb68969fb20a638a9300849fe363c1d7e8dfeb1955405d6a9d570
3
+ metadata.gz: b296a99b1a2ba980aedaa542532fd88bdea816209af3a2be1999a6e4305d0167
4
+ data.tar.gz: 1e9a7c1e385a4ec311baac0ea055453631d559e76ab4bab67e34cfcf3166f536
5
5
  SHA512:
6
- metadata.gz: 8151a326269d029319b4dcb69bd61edcf6ef5745a673cda96464bb91afa2103aa5896d4c8e315951e54ac605b031582af3bd1eae0eb2d7f7b673cd4afc8999df
7
- data.tar.gz: 6c47ebcbbdeef79496348fdde56280b90ac6e90bec8e52cded836d95415fa1ec0b8b2ad3874992d3bd7ca9e928114e57cabc1a72d7525e1923faf94db3273d07
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)
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sbom
4
- VERSION = "0.2.0"
4
+ VERSION = "0.4.0"
5
5
  end
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.2.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.1
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: []