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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d5d7fea7d39796fd36fec03e1e5b13695fdccb0cfc6926cfb5a8631d86f94f09
4
- data.tar.gz: fe3abc900c32f11af2b0c1f432c9b376042b48a9d8e69c293ccc76b7383a97b0
3
+ metadata.gz: 590a4da5b45a12b7d7946aa14626eae531db3220a2441047895db77c078ccf4b
4
+ data.tar.gz: 525d811c49ee31132eafbccc53e96d7fc4b39e92cd1dac8c704da334120584d4
5
5
  SHA512:
6
- metadata.gz: 8b79df0b6ab84ce586694fffcf9b9a25e4de2586f85b0430be60f93404df699675e5890936a4ea963f528ed7526b6ffb9e9afe591ee8c8b327952981457654a0
7
- data.tar.gz: dec35c52cf5cd36d1b0692a3eb6bb73431f2e07836226af366fd7a8d0c58c964c9577545bf10c01e5178333acbf75a299a63f205bbc3496c557835af5473496b
6
+ metadata.gz: a4bc5d533a846c9c0786f5fcc17d9ca6f85fe6bbd72719ff016caeebedd2a6e6b68fef6d86c59fced37a5a96a1c601eb5f6fbda00554a67b3e2cb287b1c95a37
7
+ data.tar.gz: b7e135ea95bbdc7cb93b90864f7f0daf75cda5640f6be982813f6d156d7942bfb4d6c8da8df7f688fb1399afdf9d30d428e75bb4dfcccb4db0ff6fe905d44b9c
data/Gemfile CHANGED
@@ -9,4 +9,4 @@ group :development do
9
9
  gem "simplecov", require: false
10
10
  gem "rspec-its"
11
11
  gem "rspec-mocks"
12
- end
12
+ end
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 in SPDX format from your project's Gemfile.lock:
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
- This will create a `bom.json` file in your project directory.
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
@@ -3,4 +3,4 @@ require "rspec/core/rake_task"
3
3
 
4
4
  RSpec::Core::RakeTask.new(:spec)
5
5
 
6
- task :default => :spec
6
+ task default: :spec
@@ -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
- comp["licenses"].map { |l| l["license"]["id"] }.join(", ")
198
+ comp["licenses"].map { |l| l["license"]["id"] }.join(", ")
199
199
  else
200
200
  "NOASSERTION"
201
- end
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
- comp["licenses"].map { |l| l["license"]["id"] }.join(", ")
217
+ comp["licenses"].map { |l| l["license"]["id"] }.join(", ")
218
218
  else
219
219
  "NOASSERTION"
220
- end
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
@@ -8,7 +8,7 @@ module Bundler
8
8
  else
9
9
  SPDX.to_report_format(sbom)
10
10
  end
11
-
11
+
12
12
  display_report(sbom)
13
13
  end
14
14
 
@@ -60,4 +60,4 @@ module Bundler
60
60
  end
61
61
  end
62
62
  end
63
- end
63
+ end
@@ -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 = SecureRandom.uuid
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
- private
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
@@ -1,5 +1,5 @@
1
1
  module Bundler
2
2
  module Sbom
3
- VERSION = "0.1.7"
3
+ VERSION = "0.1.8"
4
4
  end
5
5
  end
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.7
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: 2025-03-06 00:00:00.000000000 Z
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.2
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: []