bundler-sbom 0.1.7 → 0.1.8
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/Gemfile +1 -1
- data/README.md +43 -5
- data/Rakefile +1 -1
- data/lib/bundler/sbom/cli.rb +8 -8
- data/lib/bundler/sbom/cyclonedx.rb +28 -28
- data/lib/bundler/sbom/generator.rb +2 -2
- data/lib/bundler/sbom/reporter.rb +2 -2
- data/lib/bundler/sbom/spdx.rb +24 -22
- data/lib/bundler/sbom/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 590a4da5b45a12b7d7946aa14626eae531db3220a2441047895db77c078ccf4b
|
4
|
+
data.tar.gz: 525d811c49ee31132eafbccc53e96d7fc4b39e92cd1dac8c704da334120584d4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a4bc5d533a846c9c0786f5fcc17d9ca6f85fe6bbd72719ff016caeebedd2a6e6b68fef6d86c59fced37a5a96a1c601eb5f6fbda00554a67b3e2cb287b1c95a37
|
7
|
+
data.tar.gz: b7e135ea95bbdc7cb93b90864f7f0daf75cda5640f6be982813f6d156d7942bfb4d6c8da8df7f688fb1399afdf9d30d428e75bb4dfcccb4db0ff6fe905d44b9c
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -14,24 +14,62 @@ $ bundler plugin install bundler-sbom
|
|
14
14
|
|
15
15
|
### Generate SBOM
|
16
16
|
|
17
|
-
To generate an SBOM file
|
17
|
+
To generate an SBOM file from your project's Gemfile.lock:
|
18
18
|
|
19
19
|
```
|
20
|
-
$ bundle sbom dump
|
20
|
+
$ bundle sbom dump [options]
|
21
21
|
```
|
22
22
|
|
23
|
-
|
23
|
+
Available options:
|
24
|
+
- `-f, --format FORMAT`: Output format (json or xml, default: json)
|
25
|
+
- `-s, --sbom FORMAT`: SBOM specification format (spdx or cyclonedx, default: spdx)
|
26
|
+
|
27
|
+
Generated files will be named according to the following pattern:
|
28
|
+
- SPDX format: `bom.json` or `bom.xml`
|
29
|
+
- CycloneDX format: `bom-cyclonedx.json` or `bom-cyclonedx.xml`
|
30
|
+
|
31
|
+
Examples:
|
32
|
+
```
|
33
|
+
$ bundle sbom dump # Generates SPDX format in JSON (bom.json)
|
34
|
+
$ bundle sbom dump -f xml # Generates SPDX format in XML (bom.xml)
|
35
|
+
$ bundle sbom dump -s cyclonedx # Generates CycloneDX format in JSON (bom-cyclonedx.json)
|
36
|
+
$ bundle sbom dump -s cyclonedx -f xml # Generates CycloneDX format in XML (bom-cyclonedx.xml)
|
37
|
+
```
|
24
38
|
|
25
39
|
### Analyze License Information
|
26
40
|
|
27
41
|
To view a summary of licenses used in your project's dependencies:
|
28
42
|
|
29
43
|
```
|
30
|
-
$ bundle sbom license
|
44
|
+
$ bundle sbom license [options]
|
31
45
|
```
|
32
46
|
|
47
|
+
Available options:
|
48
|
+
- `-f, --file PATH`: Input SBOM file path
|
49
|
+
- `-F, --format FORMAT`: Input format (json or xml)
|
50
|
+
|
51
|
+
If no options are specified, the command will automatically look for SBOM files in the following order:
|
52
|
+
1. `bom.xml` (if format is xml)
|
53
|
+
2. `bom-cyclonedx.json`
|
54
|
+
3. `bom-cyclonedx.xml`
|
55
|
+
4. `bom.json`
|
56
|
+
|
33
57
|
This command will show:
|
34
58
|
- A count of packages using each license
|
35
59
|
- A detailed list of packages grouped by license
|
36
60
|
|
37
|
-
Note: The `license` command requires that you've already generated the SBOM using `bundle sbom dump`.
|
61
|
+
Note: The `license` command requires that you've already generated the SBOM using `bundle sbom dump`.
|
62
|
+
|
63
|
+
## Supported SBOM Formats
|
64
|
+
|
65
|
+
### SPDX
|
66
|
+
[SPDX (Software Package Data Exchange)](https://spdx.dev/) is a standard format for communicating software bill of material information, including components, licenses, copyrights, and security references.
|
67
|
+
|
68
|
+
### CycloneDX
|
69
|
+
[CycloneDX](https://cyclonedx.org/) is a lightweight SBOM specification designed for use in application security contexts and supply chain component analysis.
|
70
|
+
|
71
|
+
## References
|
72
|
+
|
73
|
+
- [SPDX Specification](https://spdx.github.io/spdx-spec/)
|
74
|
+
- [CycloneDX Specification](https://cyclonedx.org/specification/overview/)
|
75
|
+
- [About Software Bill of Materials (SBOM)](https://www.cisa.gov/sbom)
|
data/Rakefile
CHANGED
data/lib/bundler/sbom/cli.rb
CHANGED
@@ -26,24 +26,24 @@ module Bundler
|
|
26
26
|
|
27
27
|
# Generate SBOM based on specified format
|
28
28
|
sbom = Bundler::Sbom::Generator.generate_sbom(sbom_format)
|
29
|
-
|
29
|
+
|
30
30
|
# Determine file extension based on output format
|
31
31
|
ext = format == "json" ? "json" : "xml"
|
32
|
-
|
32
|
+
|
33
33
|
# Determine filename prefix based on SBOM format
|
34
34
|
prefix = sbom_format == "spdx" ? "bom" : "bom-cyclonedx"
|
35
35
|
output_file = "#{prefix}.#{ext}"
|
36
|
-
|
36
|
+
|
37
37
|
if format == "json"
|
38
38
|
File.write(output_file, JSON.pretty_generate(sbom))
|
39
39
|
else # xml
|
40
40
|
xml_content = Bundler::Sbom::Generator.convert_to_xml(sbom)
|
41
41
|
File.write(output_file, xml_content)
|
42
42
|
end
|
43
|
-
|
43
|
+
|
44
44
|
Bundler.ui.info("Generated #{sbom_format.upcase} SBOM at #{output_file}")
|
45
45
|
end
|
46
|
-
|
46
|
+
|
47
47
|
desc "license", "Display license report from SBOM file"
|
48
48
|
method_option :file, type: :string, desc: "Input SBOM file path", aliases: "-f"
|
49
49
|
method_option :format, type: :string, desc: "Input format: json or xml", aliases: "-F"
|
@@ -79,13 +79,13 @@ module Bundler
|
|
79
79
|
|
80
80
|
begin
|
81
81
|
content = File.read(input_file)
|
82
|
-
|
82
|
+
|
83
83
|
sbom = if format == "xml" || (!format && File.extname(input_file) == ".xml")
|
84
84
|
Bundler::Sbom::Generator.parse_xml(content)
|
85
85
|
else
|
86
86
|
JSON.parse(content)
|
87
87
|
end
|
88
|
-
|
88
|
+
|
89
89
|
Bundler::Sbom::Reporter.display_license_report(sbom)
|
90
90
|
rescue JSON::ParserError
|
91
91
|
Bundler.ui.error("Error: #{input_file} is not a valid JSON file")
|
@@ -102,4 +102,4 @@ module Bundler
|
|
102
102
|
end
|
103
103
|
end
|
104
104
|
end
|
105
|
-
end
|
105
|
+
end
|
@@ -68,7 +68,7 @@ module Bundler
|
|
68
68
|
def self.to_xml(sbom)
|
69
69
|
doc = REXML::Document.new
|
70
70
|
doc << REXML::XMLDecl.new("1.0", "UTF-8")
|
71
|
-
|
71
|
+
|
72
72
|
# Root element
|
73
73
|
root = REXML::Element.new("bom")
|
74
74
|
root.add_namespace("http://cyclonedx.org/schema/bom/1.4")
|
@@ -77,63 +77,63 @@ module Bundler
|
|
77
77
|
"version" => sbom["version"].to_s,
|
78
78
|
})
|
79
79
|
doc.add_element(root)
|
80
|
-
|
80
|
+
|
81
81
|
# Metadata
|
82
82
|
metadata = REXML::Element.new("metadata")
|
83
83
|
root.add_element(metadata)
|
84
|
-
|
84
|
+
|
85
85
|
add_element(metadata, "timestamp", sbom["metadata"]["timestamp"])
|
86
|
-
|
86
|
+
|
87
87
|
# Tools
|
88
88
|
tools = REXML::Element.new("tools")
|
89
89
|
metadata.add_element(tools)
|
90
|
-
|
90
|
+
|
91
91
|
sbom["metadata"]["tools"].each do |tool_data|
|
92
92
|
tool = REXML::Element.new("tool")
|
93
93
|
tools.add_element(tool)
|
94
|
-
|
94
|
+
|
95
95
|
add_element(tool, "vendor", tool_data["vendor"])
|
96
96
|
add_element(tool, "name", tool_data["name"])
|
97
97
|
add_element(tool, "version", tool_data["version"].to_s)
|
98
98
|
end
|
99
|
-
|
99
|
+
|
100
100
|
# Component (root project)
|
101
101
|
component = REXML::Element.new("component")
|
102
102
|
component.add_attribute("type", sbom["metadata"]["component"]["type"])
|
103
103
|
metadata.add_element(component)
|
104
|
-
|
104
|
+
|
105
105
|
add_element(component, "name", sbom["metadata"]["component"]["name"])
|
106
106
|
add_element(component, "version", sbom["metadata"]["component"]["version"])
|
107
|
-
|
107
|
+
|
108
108
|
# Components
|
109
109
|
components = REXML::Element.new("components")
|
110
110
|
root.add_element(components)
|
111
|
-
|
111
|
+
|
112
112
|
sbom["components"].each do |comp_data|
|
113
113
|
comp = REXML::Element.new("component")
|
114
114
|
comp.add_attribute("type", comp_data["type"])
|
115
115
|
components.add_element(comp)
|
116
|
-
|
116
|
+
|
117
117
|
add_element(comp, "name", comp_data["name"])
|
118
118
|
add_element(comp, "version", comp_data["version"])
|
119
119
|
add_element(comp, "purl", comp_data["purl"])
|
120
|
-
|
120
|
+
|
121
121
|
# Licenses
|
122
122
|
if comp_data["licenses"] && !comp_data["licenses"].empty?
|
123
123
|
licenses = REXML::Element.new("licenses")
|
124
124
|
comp.add_element(licenses)
|
125
|
-
|
125
|
+
|
126
126
|
comp_data["licenses"].each do |license_data|
|
127
127
|
license = REXML::Element.new("license")
|
128
128
|
licenses.add_element(license)
|
129
|
-
|
129
|
+
|
130
130
|
if license_data["license"]["id"]
|
131
131
|
add_element(license, "id", license_data["license"]["id"])
|
132
132
|
end
|
133
133
|
end
|
134
134
|
end
|
135
135
|
end
|
136
|
-
|
136
|
+
|
137
137
|
formatter = REXML::Formatters::Pretty.new(2)
|
138
138
|
formatter.compact = true
|
139
139
|
output = ""
|
@@ -143,7 +143,7 @@ module Bundler
|
|
143
143
|
|
144
144
|
def self.parse_xml(doc)
|
145
145
|
root = doc.root
|
146
|
-
|
146
|
+
|
147
147
|
sbom = {
|
148
148
|
"bomFormat" => "CycloneDX",
|
149
149
|
"specVersion" => "1.4",
|
@@ -160,7 +160,7 @@ module Bundler
|
|
160
160
|
},
|
161
161
|
"components" => []
|
162
162
|
}
|
163
|
-
|
163
|
+
|
164
164
|
# Collect tools
|
165
165
|
REXML::XPath.each(root, "metadata/tools/tool") do |tool|
|
166
166
|
tool_data = {
|
@@ -170,7 +170,7 @@ module Bundler
|
|
170
170
|
}
|
171
171
|
sbom["metadata"]["tools"] << tool_data
|
172
172
|
end
|
173
|
-
|
173
|
+
|
174
174
|
# Collect components
|
175
175
|
REXML::XPath.each(root, "components/component") do |comp|
|
176
176
|
component = {
|
@@ -179,14 +179,14 @@ module Bundler
|
|
179
179
|
"version" => get_element_text(comp, "version"),
|
180
180
|
"purl" => get_element_text(comp, "purl")
|
181
181
|
}
|
182
|
-
|
182
|
+
|
183
183
|
# Collect licenses
|
184
184
|
licenses = []
|
185
185
|
REXML::XPath.each(comp, "licenses/license") do |license|
|
186
186
|
license_id = get_element_text(license, "id")
|
187
187
|
licenses << { "license" => { "id" => license_id } } if license_id
|
188
188
|
end
|
189
|
-
|
189
|
+
|
190
190
|
component["licenses"] = licenses unless licenses.empty?
|
191
191
|
sbom["components"] << component
|
192
192
|
end
|
@@ -195,10 +195,10 @@ module Bundler
|
|
195
195
|
converted_sbom = {
|
196
196
|
"packages" => sbom["components"].map do |comp|
|
197
197
|
license_string = if comp["licenses"]
|
198
|
-
|
198
|
+
comp["licenses"].map { |l| l["license"]["id"] }.join(", ")
|
199
199
|
else
|
200
200
|
"NOASSERTION"
|
201
|
-
|
201
|
+
end
|
202
202
|
{
|
203
203
|
"name" => comp["name"],
|
204
204
|
"versionInfo" => comp["version"],
|
@@ -206,7 +206,7 @@ module Bundler
|
|
206
206
|
}
|
207
207
|
end
|
208
208
|
}
|
209
|
-
|
209
|
+
|
210
210
|
converted_sbom
|
211
211
|
end
|
212
212
|
|
@@ -214,10 +214,10 @@ module Bundler
|
|
214
214
|
{
|
215
215
|
"packages" => sbom["components"].map do |comp|
|
216
216
|
license_string = if comp["licenses"]
|
217
|
-
|
217
|
+
comp["licenses"].map { |l| l["license"]["id"] }.join(", ")
|
218
218
|
else
|
219
219
|
"NOASSERTION"
|
220
|
-
|
220
|
+
end
|
221
221
|
{
|
222
222
|
"name" => comp["name"],
|
223
223
|
"versionInfo" => comp["version"],
|
@@ -228,17 +228,17 @@ module Bundler
|
|
228
228
|
end
|
229
229
|
|
230
230
|
private
|
231
|
-
|
231
|
+
|
232
232
|
def self.add_element(parent, name, value)
|
233
233
|
element = REXML::Element.new(name)
|
234
234
|
element.text = value
|
235
235
|
parent.add_element(element)
|
236
236
|
end
|
237
|
-
|
237
|
+
|
238
238
|
def self.get_element_text(element, xpath)
|
239
239
|
result = REXML::XPath.first(element, xpath)
|
240
240
|
result ? result.text : nil
|
241
241
|
end
|
242
242
|
end
|
243
243
|
end
|
244
|
-
end
|
244
|
+
end
|
@@ -39,7 +39,7 @@ module Bundler
|
|
39
39
|
def self.parse_xml(xml_content)
|
40
40
|
doc = REXML::Document.new(xml_content)
|
41
41
|
root = doc.root
|
42
|
-
|
42
|
+
|
43
43
|
# Determine if it's CycloneDX or SPDX
|
44
44
|
if root.name == "bom" && root.namespace.include?("cyclonedx.org")
|
45
45
|
CycloneDX.parse_xml(doc)
|
@@ -49,4 +49,4 @@ module Bundler
|
|
49
49
|
end
|
50
50
|
end
|
51
51
|
end
|
52
|
-
end
|
52
|
+
end
|
data/lib/bundler/sbom/spdx.rb
CHANGED
@@ -6,7 +6,7 @@ module Bundler
|
|
6
6
|
module Sbom
|
7
7
|
class SPDX
|
8
8
|
def self.generate(lockfile, document_name)
|
9
|
-
spdx_id =
|
9
|
+
spdx_id = generate_spdx_id
|
10
10
|
sbom = {
|
11
11
|
"SPDXID" => "SPDXRef-DOCUMENT",
|
12
12
|
"spdxVersion" => "SPDX-2.3",
|
@@ -67,39 +67,39 @@ module Bundler
|
|
67
67
|
def self.to_xml(sbom)
|
68
68
|
doc = REXML::Document.new
|
69
69
|
doc << REXML::XMLDecl.new("1.0", "UTF-8")
|
70
|
-
|
70
|
+
|
71
71
|
# Root element
|
72
72
|
root = REXML::Element.new("SpdxDocument")
|
73
73
|
root.add_namespace("https://spdx.org/spdxdocs/")
|
74
74
|
doc.add_element(root)
|
75
|
-
|
75
|
+
|
76
76
|
# Document info
|
77
77
|
add_element(root, "SPDXID", sbom["SPDXID"])
|
78
78
|
add_element(root, "spdxVersion", sbom["spdxVersion"])
|
79
79
|
add_element(root, "name", sbom["name"])
|
80
80
|
add_element(root, "dataLicense", sbom["dataLicense"])
|
81
81
|
add_element(root, "documentNamespace", sbom["documentNamespace"])
|
82
|
-
|
82
|
+
|
83
83
|
# Creation info
|
84
84
|
creation_info = REXML::Element.new("creationInfo")
|
85
85
|
root.add_element(creation_info)
|
86
86
|
add_element(creation_info, "created", sbom["creationInfo"]["created"])
|
87
87
|
add_element(creation_info, "licenseListVersion", sbom["creationInfo"]["licenseListVersion"])
|
88
|
-
|
88
|
+
|
89
89
|
sbom["creationInfo"]["creators"].each do |creator|
|
90
90
|
add_element(creation_info, "creator", creator)
|
91
91
|
end
|
92
|
-
|
92
|
+
|
93
93
|
# Describes
|
94
94
|
sbom["documentDescribes"].each do |describes|
|
95
95
|
add_element(root, "documentDescribes", describes)
|
96
96
|
end
|
97
|
-
|
97
|
+
|
98
98
|
# Packages
|
99
99
|
sbom["packages"].each do |pkg|
|
100
100
|
package = REXML::Element.new("package")
|
101
101
|
root.add_element(package)
|
102
|
-
|
102
|
+
|
103
103
|
add_element(package, "SPDXID", pkg["SPDXID"])
|
104
104
|
add_element(package, "name", pkg["name"])
|
105
105
|
add_element(package, "versionInfo", pkg["versionInfo"])
|
@@ -109,20 +109,20 @@ module Bundler
|
|
109
109
|
add_element(package, "licenseDeclared", pkg["licenseDeclared"])
|
110
110
|
add_element(package, "copyrightText", pkg["copyrightText"])
|
111
111
|
add_element(package, "supplier", pkg["supplier"])
|
112
|
-
|
112
|
+
|
113
113
|
# External references
|
114
114
|
if pkg["externalRefs"]
|
115
115
|
pkg["externalRefs"].each do |ref|
|
116
116
|
ext_ref = REXML::Element.new("externalRef")
|
117
117
|
package.add_element(ext_ref)
|
118
|
-
|
118
|
+
|
119
119
|
add_element(ext_ref, "referenceCategory", ref["referenceCategory"])
|
120
120
|
add_element(ext_ref, "referenceType", ref["referenceType"])
|
121
121
|
add_element(ext_ref, "referenceLocator", ref["referenceLocator"])
|
122
122
|
end
|
123
123
|
end
|
124
124
|
end
|
125
|
-
|
125
|
+
|
126
126
|
formatter = REXML::Formatters::Pretty.new(2)
|
127
127
|
formatter.compact = true
|
128
128
|
output = ""
|
@@ -132,7 +132,7 @@ module Bundler
|
|
132
132
|
|
133
133
|
def self.parse_xml(doc)
|
134
134
|
root = doc.root
|
135
|
-
|
135
|
+
|
136
136
|
sbom = {
|
137
137
|
"SPDXID" => get_element_text(root, "SPDXID"),
|
138
138
|
"spdxVersion" => get_element_text(root, "spdxVersion"),
|
@@ -147,17 +147,17 @@ module Bundler
|
|
147
147
|
"packages" => [],
|
148
148
|
"documentDescribes" => []
|
149
149
|
}
|
150
|
-
|
150
|
+
|
151
151
|
# Collect creators
|
152
152
|
REXML::XPath.each(root, "creationInfo/creator") do |creator|
|
153
153
|
sbom["creationInfo"]["creators"] << creator.text
|
154
154
|
end
|
155
|
-
|
155
|
+
|
156
156
|
# Collect documentDescribes
|
157
157
|
REXML::XPath.each(root, "documentDescribes") do |describes|
|
158
158
|
sbom["documentDescribes"] << describes.text
|
159
159
|
end
|
160
|
-
|
160
|
+
|
161
161
|
# Collect packages
|
162
162
|
REXML::XPath.each(root, "package") do |pkg_element|
|
163
163
|
package = {
|
@@ -172,7 +172,7 @@ module Bundler
|
|
172
172
|
"supplier" => get_element_text(pkg_element, "supplier"),
|
173
173
|
"externalRefs" => []
|
174
174
|
}
|
175
|
-
|
175
|
+
|
176
176
|
# Collect external references
|
177
177
|
REXML::XPath.each(pkg_element, "externalRef") do |ref_element|
|
178
178
|
ref = {
|
@@ -182,10 +182,10 @@ module Bundler
|
|
182
182
|
}
|
183
183
|
package["externalRefs"] << ref
|
184
184
|
end
|
185
|
-
|
185
|
+
|
186
186
|
sbom["packages"] << package
|
187
187
|
end
|
188
|
-
|
188
|
+
|
189
189
|
sbom
|
190
190
|
end
|
191
191
|
|
@@ -203,18 +203,20 @@ module Bundler
|
|
203
203
|
}
|
204
204
|
end
|
205
205
|
|
206
|
-
|
207
|
-
|
206
|
+
def self.generate_spdx_id
|
207
|
+
SecureRandom.uuid
|
208
|
+
end
|
209
|
+
|
208
210
|
def self.add_element(parent, name, value)
|
209
211
|
element = REXML::Element.new(name)
|
210
212
|
element.text = value
|
211
213
|
parent.add_element(element)
|
212
214
|
end
|
213
|
-
|
215
|
+
|
214
216
|
def self.get_element_text(element, xpath)
|
215
217
|
result = REXML::XPath.first(element, xpath)
|
216
218
|
result ? result.text : nil
|
217
219
|
end
|
218
220
|
end
|
219
221
|
end
|
220
|
-
end
|
222
|
+
end
|
data/lib/bundler/sbom/version.rb
CHANGED
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: bundler-sbom
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.8
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- SHIBATA Hiroshi
|
8
8
|
bindir: exe
|
9
9
|
cert_chain: []
|
10
|
-
date:
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: bundler
|
@@ -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.
|
81
|
+
rubygems_version: 3.6.9
|
82
82
|
specification_version: 4
|
83
83
|
summary: Generate SPDX SBOM(Software Bill of Materials) files with Bundler
|
84
84
|
test_files: []
|