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 +4 -4
- data/README.md +3 -0
- data/lib/bundler/sbom/cli.rb +22 -10
- data/lib/bundler/sbom/cyclonedx.rb +15 -12
- data/lib/bundler/sbom/generator.rb +44 -3
- data/lib/bundler/sbom/reporter.rb +1 -1
- data/lib/bundler/sbom/spdx.rb +7 -2
- data/lib/bundler/sbom/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c2d31deef79ab54416961ff900632cf386f95207f35afd2c2e491d97c53d4c0e
|
|
4
|
+
data.tar.gz: 6060186d5f3394f12f9c58a2ad1aa4a01b554500f58ef606403536675e506f18
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
data/lib/bundler/sbom/cli.rb
CHANGED
|
@@ -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
|
-
|
|
64
|
+
input_file = if format == "xml" || (format.nil? && File.exist?("bom.xml"))
|
|
65
|
+
"bom.xml"
|
|
64
66
|
elsif File.exist?("bom-cyclonedx.json")
|
|
65
|
-
|
|
67
|
+
"bom-cyclonedx.json"
|
|
66
68
|
elsif File.exist?("bom-cyclonedx.xml")
|
|
67
|
-
|
|
69
|
+
"bom-cyclonedx.xml"
|
|
68
70
|
else
|
|
69
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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| {
|
|
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 << {
|
|
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
|
-
|
|
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
|
-
|
|
200
|
-
|
|
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
|
-
|
|
219
|
-
|
|
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(
|
|
28
|
+
CycloneDX.generate(gems, document_name)
|
|
26
29
|
else # default to spdx
|
|
27
|
-
SPDX.generate(
|
|
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
|
data/lib/bundler/sbom/spdx.rb
CHANGED
|
@@ -5,7 +5,7 @@ require "rexml/document"
|
|
|
5
5
|
module Bundler
|
|
6
6
|
module Sbom
|
|
7
7
|
class SPDX
|
|
8
|
-
def self.generate(
|
|
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
|
-
|
|
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 = []
|
data/lib/bundler/sbom/version.rb
CHANGED
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.
|
|
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:
|
|
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: []
|