bundler-sbom 0.1.8 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 590a4da5b45a12b7d7946aa14626eae531db3220a2441047895db77c078ccf4b
4
- data.tar.gz: 525d811c49ee31132eafbccc53e96d7fc4b39e92cd1dac8c704da334120584d4
3
+ metadata.gz: c2d31deef79ab54416961ff900632cf386f95207f35afd2c2e491d97c53d4c0e
4
+ data.tar.gz: 6060186d5f3394f12f9c58a2ad1aa4a01b554500f58ef606403536675e506f18
5
5
  SHA512:
6
- metadata.gz: a4bc5d533a846c9c0786f5fcc17d9ca6f85fe6bbd72719ff016caeebedd2a6e6b68fef6d86c59fced37a5a96a1c601eb5f6fbda00554a67b3e2cb287b1c95a37
7
- data.tar.gz: b7e135ea95bbdc7cb93b90864f7f0daf75cda5640f6be982813f6d156d7942bfb4d6c8da8df7f688fb1399afdf9d30d428e75bb4dfcccb4db0ff6fe905d44b9c
6
+ metadata.gz: bb372a7bf2da186dcdf3a3406f236682b35b3aba690a21f3617a32cd94cebf85a4902e39a755a0e1698ff463a9f37fa8c79a425230ab6d1b68c16d1790dbb476
7
+ data.tar.gz: aab0fffe6b7c684d8bb07c33040f2f5897c00d9353e5809b665333a714ee6175a36a2eae4ac01d8427c5b273e3911e91fadc8580f486d4dc04db0ec9f6ec337d
data/README.md CHANGED
@@ -23,6 +23,7 @@ $ bundle sbom dump [options]
23
23
  Available options:
24
24
  - `-f, --format FORMAT`: Output format (json or xml, default: json)
25
25
  - `-s, --sbom FORMAT`: SBOM specification format (spdx or cyclonedx, default: spdx)
26
+ - `--without GROUPS`: Exclude groups (comma or colon separated, e.g., 'development:test' or 'development,test')
26
27
 
27
28
  Generated files will be named according to the following pattern:
28
29
  - SPDX format: `bom.json` or `bom.xml`
@@ -34,6 +35,8 @@ $ bundle sbom dump # Generates SPDX format in JSON (bo
34
35
  $ bundle sbom dump -f xml # Generates SPDX format in XML (bom.xml)
35
36
  $ bundle sbom dump -s cyclonedx # Generates CycloneDX format in JSON (bom-cyclonedx.json)
36
37
  $ bundle sbom dump -s cyclonedx -f xml # Generates CycloneDX format in XML (bom-cyclonedx.xml)
38
+ $ bundle sbom dump --without development # Excludes development group
39
+ $ bundle sbom dump --without development:test # Excludes development and test groups
37
40
  ```
38
41
 
39
42
  ### Analyze License Information
@@ -8,9 +8,11 @@ module Bundler
8
8
  desc "dump", "Generate SBOM and save to file"
9
9
  method_option :format, type: :string, default: "json", desc: "Output format: json or xml", aliases: "-f"
10
10
  method_option :sbom, type: :string, default: "spdx", desc: "SBOM format: spdx or cyclonedx", aliases: "-s"
11
+ method_option :without, type: :string, desc: "Exclude groups (comma or colon separated, e.g., 'development:test' or 'development,test')"
11
12
  def dump
12
13
  format = options[:format].downcase
13
14
  sbom_format = options[:sbom].downcase
15
+ without_groups = parse_without_groups(options[:without])
14
16
 
15
17
  # Validate output format
16
18
  unless ["json", "xml"].include?(format)
@@ -25,13 +27,13 @@ module Bundler
25
27
  end
26
28
 
27
29
  # Generate SBOM based on specified format
28
- sbom = Bundler::Sbom::Generator.generate_sbom(sbom_format)
30
+ sbom = Bundler::Sbom::Generator.generate_sbom(sbom_format, without_groups: without_groups)
29
31
 
30
32
  # Determine file extension based on output format
31
- ext = format == "json" ? "json" : "xml"
33
+ ext = (format == "json") ? "json" : "xml"
32
34
 
33
35
  # Determine filename prefix based on SBOM format
34
- prefix = sbom_format == "spdx" ? "bom" : "bom-cyclonedx"
36
+ prefix = (sbom_format == "spdx") ? "bom" : "bom-cyclonedx"
35
37
  output_file = "#{prefix}.#{ext}"
36
38
 
37
39
  if format == "json"
@@ -59,19 +61,19 @@ module Bundler
59
61
 
60
62
  # Determine input file based on format or find default files
61
63
  if input_file.nil?
62
- if format == "xml" || (format.nil? && File.exist?("bom.xml"))
63
- input_file = "bom.xml"
64
+ input_file = if format == "xml" || (format.nil? && File.exist?("bom.xml"))
65
+ "bom.xml"
64
66
  elsif File.exist?("bom-cyclonedx.json")
65
- input_file = "bom-cyclonedx.json"
67
+ "bom-cyclonedx.json"
66
68
  elsif File.exist?("bom-cyclonedx.xml")
67
- input_file = "bom-cyclonedx.xml"
69
+ "bom-cyclonedx.xml"
68
70
  else
69
- input_file = "bom.json"
71
+ "bom.json"
70
72
  end
71
73
  end
72
74
 
73
75
  unless File.exist?(input_file)
74
- file_type = File.extname(input_file) == ".xml" ? "xml" : "json"
76
+ file_type = (File.extname(input_file) == ".xml") ? "xml" : "json"
75
77
  sbom_type = input_file.include?("cyclonedx") ? "cyclonedx" : "spdx"
76
78
  Bundler.ui.error("Error: #{input_file} not found. Run 'bundle sbom dump --format=#{file_type} --sbom=#{sbom_type}' first.")
77
79
  exit 1
@@ -90,7 +92,7 @@ module Bundler
90
92
  rescue JSON::ParserError
91
93
  Bundler.ui.error("Error: #{input_file} is not a valid JSON file")
92
94
  exit 1
93
- rescue StandardError => e
95
+ rescue => e
94
96
  Bundler.ui.error("Error processing #{input_file}: #{e.message}")
95
97
  exit 1
96
98
  end
@@ -100,6 +102,16 @@ module Bundler
100
102
  def self.exit_on_failure?
101
103
  true
102
104
  end
105
+
106
+ private
107
+
108
+ def parse_without_groups(without_option)
109
+ return [] unless without_option
110
+
111
+ # Split by comma or colon and clean up whitespace
112
+ groups = without_option.split(%r{[:,]}).map(&:strip).reject(&:empty?)
113
+ groups.map(&:to_sym)
114
+ end
103
115
  end
104
116
  end
105
117
  end
@@ -5,7 +5,7 @@ require "rexml/document"
5
5
  module Bundler
6
6
  module Sbom
7
7
  class CycloneDX
8
- def self.generate(lockfile, document_name)
8
+ def self.generate(gems, document_name)
9
9
  serial_number = SecureRandom.uuid
10
10
  timestamp = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
11
11
  sbom = {
@@ -31,7 +31,12 @@ module Bundler
31
31
  "components" => []
32
32
  }
33
33
 
34
- lockfile.specs.each do |spec|
34
+ # Deduplicate specs by name and version
35
+ seen_gems = Set.new
36
+ gems.each do |spec|
37
+ gem_key = "#{spec.name}:#{spec.version}"
38
+ next if seen_gems.include?(gem_key)
39
+ seen_gems.add(gem_key)
35
40
  begin
36
41
  gemspec = Gem::Specification.find_by_name(spec.name, spec.version)
37
42
  licenses = []
@@ -56,7 +61,7 @@ module Bundler
56
61
  }
57
62
 
58
63
  unless licenses.empty?
59
- component["licenses"] = licenses.map { |license| { "license" => { "id" => license } } }
64
+ component["licenses"] = licenses.map { |license| {"license" => {"id" => license}} }
60
65
  end
61
66
 
62
67
  sbom["components"] << component
@@ -74,7 +79,7 @@ module Bundler
74
79
  root.add_namespace("http://cyclonedx.org/schema/bom/1.4")
75
80
  root.add_attributes({
76
81
  "serialNumber" => sbom["serialNumber"],
77
- "version" => sbom["version"].to_s,
82
+ "version" => sbom["version"].to_s
78
83
  })
79
84
  doc.add_element(root)
80
85
 
@@ -184,7 +189,7 @@ module Bundler
184
189
  licenses = []
185
190
  REXML::XPath.each(comp, "licenses/license") do |license|
186
191
  license_id = get_element_text(license, "id")
187
- licenses << { "license" => { "id" => license_id } } if license_id
192
+ licenses << {"license" => {"id" => license_id}} if license_id
188
193
  end
189
194
 
190
195
  component["licenses"] = licenses unless licenses.empty?
@@ -192,12 +197,12 @@ module Bundler
192
197
  end
193
198
 
194
199
  # Convert CycloneDX format to SPDX-like format for compatibility with Reporter
195
- converted_sbom = {
200
+ {
196
201
  "packages" => sbom["components"].map do |comp|
197
202
  license_string = if comp["licenses"]
198
203
  comp["licenses"].map { |l| l["license"]["id"] }.join(", ")
199
- else
200
- "NOASSERTION"
204
+ else
205
+ "NOASSERTION"
201
206
  end
202
207
  {
203
208
  "name" => comp["name"],
@@ -206,8 +211,6 @@ module Bundler
206
211
  }
207
212
  end
208
213
  }
209
-
210
- converted_sbom
211
214
  end
212
215
 
213
216
  def self.to_report_format(sbom)
@@ -215,8 +218,8 @@ module Bundler
215
218
  "packages" => sbom["components"].map do |comp|
216
219
  license_string = if comp["licenses"]
217
220
  comp["licenses"].map { |l| l["license"]["id"] }.join(", ")
218
- else
219
- "NOASSERTION"
221
+ else
222
+ "NOASSERTION"
220
223
  end
221
224
  {
222
225
  "name" => comp["name"],
@@ -10,7 +10,7 @@ module Bundler
10
10
  class GemfileLockNotFoundError < StandardError; end
11
11
 
12
12
  class Generator
13
- def self.generate_sbom(format = "spdx")
13
+ def self.generate_sbom(format = "spdx", without_groups: [])
14
14
  lockfile_path = Bundler.default_lockfile
15
15
  if !lockfile_path || !lockfile_path.exist?
16
16
  Bundler.ui.error "No Gemfile.lock found. Run `bundle install` first."
@@ -20,11 +20,14 @@ module Bundler
20
20
  lockfile = Bundler::LockfileParser.new(lockfile_path.read)
21
21
  document_name = File.basename(Dir.pwd)
22
22
 
23
+ # Get gems to include based on groups
24
+ gems = get_gems_for_groups(lockfile, without_groups)
25
+
23
26
  case format.to_s.downcase
24
27
  when "cyclonedx"
25
- CycloneDX.generate(lockfile, document_name)
28
+ CycloneDX.generate(gems, document_name)
26
29
  else # default to spdx
27
- SPDX.generate(lockfile, document_name)
30
+ SPDX.generate(gems, document_name)
28
31
  end
29
32
  end
30
33
 
@@ -47,6 +50,44 @@ module Bundler
47
50
  SPDX.parse_xml(doc)
48
51
  end
49
52
  end
53
+
54
+ private
55
+
56
+ def self.get_gems_for_groups(lockfile, without_groups)
57
+ # If no groups specified, use all specs
58
+ if without_groups.empty?
59
+ return lockfile.specs
60
+ end
61
+
62
+ # Try to get group information from Bundler.definition if available
63
+ if defined?(Bundler::Definition) && Bundler.respond_to?(:definition)
64
+ begin
65
+ definition = Bundler.definition
66
+ all_groups = definition.groups
67
+ include_groups = all_groups - without_groups
68
+
69
+ # Use specs_for to get all gems (including transitive dependencies) for included groups
70
+ if definition.respond_to?(:specs_for)
71
+ definition.specs_for(include_groups)
72
+ else
73
+ # Fallback to old method if specs_for is not available
74
+ included_gems = Set.new
75
+ include_groups.each do |group|
76
+ definition.dependencies_for(group).each do |dep|
77
+ included_gems.add(dep.name)
78
+ end
79
+ end
80
+ lockfile.specs.select { |spec| included_gems.include?(spec.name) }
81
+ end
82
+ rescue => e
83
+ # Fallback to all specs if there's any issue with Bundler.definition
84
+ Bundler.ui.warn("Warning: Could not determine group information: #{e.message}")
85
+ lockfile.specs
86
+ end
87
+ else
88
+ lockfile.specs
89
+ end
90
+ end
50
91
  end
51
92
  end
52
93
  end
@@ -16,7 +16,7 @@ module Bundler
16
16
 
17
17
  def self.sbom_format(sbom)
18
18
  return :cyclonedx if sbom["bomFormat"] == "CycloneDX"
19
- return :spdx
19
+ :spdx
20
20
  end
21
21
 
22
22
  def self.display_report(sbom)
@@ -5,7 +5,7 @@ require "rexml/document"
5
5
  module Bundler
6
6
  module Sbom
7
7
  class SPDX
8
- def self.generate(lockfile, document_name)
8
+ def self.generate(gems, document_name)
9
9
  spdx_id = generate_spdx_id
10
10
  sbom = {
11
11
  "SPDXID" => "SPDXRef-DOCUMENT",
@@ -21,7 +21,12 @@ module Bundler
21
21
  "packages" => []
22
22
  }
23
23
 
24
- lockfile.specs.each do |spec|
24
+ # Deduplicate specs by name and version
25
+ seen_gems = Set.new
26
+ gems.each do |spec|
27
+ gem_key = "#{spec.name}:#{spec.version}"
28
+ next if seen_gems.include?(gem_key)
29
+ seen_gems.add(gem_key)
25
30
  begin
26
31
  gemspec = Gem::Specification.find_by_name(spec.name, spec.version)
27
32
  licenses = []
@@ -1,5 +1,5 @@
1
1
  module Bundler
2
2
  module Sbom
3
- VERSION = "0.1.8"
3
+ VERSION = "0.3.0"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bundler-sbom
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.8
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - SHIBATA Hiroshi
@@ -78,7 +78,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
78
78
  - !ruby/object:Gem::Version
79
79
  version: '0'
80
80
  requirements: []
81
- rubygems_version: 3.6.9
81
+ rubygems_version: 4.0.3
82
82
  specification_version: 4
83
83
  summary: Generate SPDX SBOM(Software Bill of Materials) files with Bundler
84
84
  test_files: []