sbom 0.1.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 +7 -0
- data/.gitmodules +7 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/README.md +153 -0
- data/Rakefile +8 -0
- data/exe/sbom +346 -0
- data/lib/sbom/cyclonedx/generator.rb +307 -0
- data/lib/sbom/cyclonedx/parser.rb +275 -0
- data/lib/sbom/data/document.rb +143 -0
- data/lib/sbom/data/file.rb +169 -0
- data/lib/sbom/data/package.rb +417 -0
- data/lib/sbom/data/relationship.rb +43 -0
- data/lib/sbom/data/sbom.rb +124 -0
- data/lib/sbom/error.rb +13 -0
- data/lib/sbom/generator.rb +79 -0
- data/lib/sbom/license/data/spdx_licenses.json +8533 -0
- data/lib/sbom/license/scanner.rb +101 -0
- data/lib/sbom/output.rb +88 -0
- data/lib/sbom/parser.rb +111 -0
- data/lib/sbom/spdx/generator.rb +337 -0
- data/lib/sbom/spdx/parser.rb +426 -0
- data/lib/sbom/validation_result.rb +30 -0
- data/lib/sbom/validator.rb +261 -0
- data/lib/sbom/version.rb +5 -0
- data/lib/sbom.rb +54 -0
- data/sig/sbom.rbs +4 -0
- metadata +114 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: e5779ca3a21e46aa2032845504cda20229928dad1570d5de2a055ffe2a2dae8d
|
|
4
|
+
data.tar.gz: d57cc2a38620373ca402ca34322e0cfcb4d30bfa88b8a6791868c92f87b2393d
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 221caf4d995e38991fd6c720b00e662fe90ae7709b7032a35d6592848ce669e5245591ca66866b66b29b68326d78afc213ce5292815b4e0f18cce51dcbcf651d
|
|
7
|
+
data.tar.gz: 1a4e9340fda31c7fff6dddbfc12b912220a7aee701c4e3df860c41c00e26c6c5a7999727017bb7af201a09d5e505b8fd24a67d5188eef0b3450b1efdfbfc50e7
|
data/.gitmodules
ADDED
data/CHANGELOG.md
ADDED
data/CODE_OF_CONDUCT.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Code of Conduct
|
|
2
|
+
|
|
3
|
+
"sbom" follows [The Ruby Community Conduct Guideline](https://www.ruby-lang.org/en/conduct) in all "collaborative space", which is defined as community communications channels (such as mailing lists, submitted patches, commit comments, etc.):
|
|
4
|
+
|
|
5
|
+
* Participants will be tolerant of opposing views.
|
|
6
|
+
* Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks.
|
|
7
|
+
* When interpreting the words and actions of others, participants should always assume good intentions.
|
|
8
|
+
* Behaviour which can be reasonably considered harassment will not be tolerated.
|
|
9
|
+
|
|
10
|
+
If you have any concerns about behaviour within this project, please contact us at ["andrewnez@gmail.com"](mailto:"andrewnez@gmail.com").
|
data/README.md
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# SBOM
|
|
2
|
+
|
|
3
|
+
A Ruby library for parsing, generating, and validating Software Bill of Materials in SPDX and CycloneDX formats.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add to your Gemfile:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem 'sbom'
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or install directly:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
gem install sbom
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
### Parsing SBOMs
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
require 'sbom'
|
|
25
|
+
|
|
26
|
+
# Parse from file (auto-detects format)
|
|
27
|
+
sbom = Sbom.parse_file("example.spdx.json")
|
|
28
|
+
|
|
29
|
+
# Parse from string
|
|
30
|
+
sbom = Sbom.parse_string(content, sbom_type: :cyclonedx)
|
|
31
|
+
|
|
32
|
+
# Parsed data is returned as hashes
|
|
33
|
+
sbom.packages.each do |pkg|
|
|
34
|
+
puts "#{pkg[:name]} @ #{pkg[:version]}"
|
|
35
|
+
puts " License: #{pkg[:license_concluded]}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
sbom.relationships.each do |rel|
|
|
39
|
+
puts "#{rel[:source_id]} --[#{rel[:type]}]--> #{rel[:target_id]}"
|
|
40
|
+
end
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Generating SBOMs
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
# Generate SPDX JSON
|
|
47
|
+
generator = Sbom::Generator.new(sbom_type: :spdx, format: :json)
|
|
48
|
+
generator.generate("MyProject", { packages: packages_data })
|
|
49
|
+
puts generator.output
|
|
50
|
+
|
|
51
|
+
# Generate CycloneDX
|
|
52
|
+
generator = Sbom::Generator.new(sbom_type: :cyclonedx)
|
|
53
|
+
generator.generate("MyProject", sbom_data)
|
|
54
|
+
File.write("sbom.cdx.json", generator.output)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Validating SBOMs
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
result = Sbom.validate_file("example.cdx.json")
|
|
61
|
+
|
|
62
|
+
if result.valid?
|
|
63
|
+
puts "#{result.format}: version #{result.version}"
|
|
64
|
+
else
|
|
65
|
+
puts "Invalid: #{result.errors.join(', ')}"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Or raise on invalid
|
|
69
|
+
Sbom::Validator.validate_file!("example.cdx.json")
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Building Packages
|
|
73
|
+
|
|
74
|
+
The Package class provides an object interface for building package data:
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
package = Sbom::Data::Package.new
|
|
78
|
+
package.name = "rails"
|
|
79
|
+
package.version = "7.0.0"
|
|
80
|
+
package.license_concluded = "MIT"
|
|
81
|
+
package.add_checksum("SHA256", "abc123...")
|
|
82
|
+
|
|
83
|
+
# Generate a PURL
|
|
84
|
+
package.generate_purl(type: "gem")
|
|
85
|
+
# => "pkg:gem/rails@7.0.0"
|
|
86
|
+
|
|
87
|
+
# Or set an existing PURL
|
|
88
|
+
package.purl = "pkg:npm/%40angular/core@16.0.0"
|
|
89
|
+
|
|
90
|
+
# Access parsed PURL components
|
|
91
|
+
package.purl_type # => "npm"
|
|
92
|
+
package.purl_namespace # => "@angular"
|
|
93
|
+
package.purl_name # => "core"
|
|
94
|
+
package.purl_version # => "16.0.0"
|
|
95
|
+
|
|
96
|
+
# Convert to hash for generation
|
|
97
|
+
package.to_h
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## CLI
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
# Parse and display SBOM
|
|
104
|
+
sbom parse example.spdx.json
|
|
105
|
+
sbom parse example.cdx.json --format json
|
|
106
|
+
|
|
107
|
+
# Validate SBOM against schema
|
|
108
|
+
sbom validate example.spdx.json
|
|
109
|
+
|
|
110
|
+
# Convert between formats
|
|
111
|
+
sbom convert example.spdx.json --type cyclonedx --output example.cdx.json
|
|
112
|
+
|
|
113
|
+
# Generate new SBOM
|
|
114
|
+
sbom generate --name MyProject --type spdx --format json
|
|
115
|
+
|
|
116
|
+
# Document commands
|
|
117
|
+
sbom document outline example.cdx.json
|
|
118
|
+
sbom document info example.spdx.json
|
|
119
|
+
sbom document query example.cdx.json --package rails
|
|
120
|
+
sbom document query example.cdx.json --license MIT
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Supported Formats
|
|
124
|
+
|
|
125
|
+
**SPDX** (versions 2.2, 2.3):
|
|
126
|
+
- Tag-Value (.spdx)
|
|
127
|
+
- JSON (.spdx.json)
|
|
128
|
+
- YAML (.spdx.yaml, .spdx.yml)
|
|
129
|
+
- XML (.spdx.xml)
|
|
130
|
+
- RDF (.spdx.rdf)
|
|
131
|
+
|
|
132
|
+
**CycloneDX** (versions 1.4, 1.5, 1.6, 1.7):
|
|
133
|
+
- JSON (.cdx.json, .bom.json)
|
|
134
|
+
- XML (.cdx.xml, .bom.xml)
|
|
135
|
+
|
|
136
|
+
## Related Libraries
|
|
137
|
+
|
|
138
|
+
- [purl](https://github.com/andrew/purl) - Package URL (PURL) parsing and generation
|
|
139
|
+
- [vers](https://github.com/andrew/vers) - Version range parsing and matching
|
|
140
|
+
|
|
141
|
+
## Development
|
|
142
|
+
|
|
143
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then run `rake test` to run the tests.
|
|
144
|
+
|
|
145
|
+
The project uses git submodules for the official SPDX and CycloneDX specifications:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
git submodule update --init --recursive
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Contributing
|
|
152
|
+
|
|
153
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/andrew/sbom.
|
data/Rakefile
ADDED
data/exe/sbom
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "optparse"
|
|
5
|
+
require "sbom"
|
|
6
|
+
|
|
7
|
+
module Sbom
|
|
8
|
+
class CLI
|
|
9
|
+
def initialize(args)
|
|
10
|
+
@args = args
|
|
11
|
+
@options = {}
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def run
|
|
15
|
+
return show_help if @args.empty?
|
|
16
|
+
|
|
17
|
+
command = @args.shift
|
|
18
|
+
|
|
19
|
+
case command
|
|
20
|
+
when "parse"
|
|
21
|
+
parse_command
|
|
22
|
+
when "generate"
|
|
23
|
+
generate_command
|
|
24
|
+
when "validate"
|
|
25
|
+
validate_command
|
|
26
|
+
when "convert"
|
|
27
|
+
convert_command
|
|
28
|
+
when "document"
|
|
29
|
+
document_command
|
|
30
|
+
when "version", "-v", "--version"
|
|
31
|
+
puts "sbom #{Sbom::VERSION}"
|
|
32
|
+
when "help", "-h", "--help"
|
|
33
|
+
show_help
|
|
34
|
+
else
|
|
35
|
+
warn "Unknown command: #{command}"
|
|
36
|
+
show_help
|
|
37
|
+
exit 1
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def parse_command
|
|
44
|
+
parser = OptionParser.new do |opts|
|
|
45
|
+
opts.banner = "Usage: sbom parse <file> [options]"
|
|
46
|
+
opts.on("-t", "--type TYPE", "SBOM type (spdx, cyclonedx, auto)") { |v| @options[:type] = v.to_sym }
|
|
47
|
+
opts.on("-f", "--format FORMAT", "Output format (json, yaml, summary)") { |v| @options[:format] = v }
|
|
48
|
+
opts.on("-h", "--help", "Show help") { puts opts; exit }
|
|
49
|
+
end
|
|
50
|
+
parser.parse!(@args)
|
|
51
|
+
|
|
52
|
+
file = @args.shift
|
|
53
|
+
abort "Error: No file specified" unless file
|
|
54
|
+
|
|
55
|
+
sbom_type = @options[:type] || :auto
|
|
56
|
+
sbom = Sbom::Parser.parse_file(file, sbom_type: sbom_type)
|
|
57
|
+
|
|
58
|
+
format = @options[:format] || "summary"
|
|
59
|
+
|
|
60
|
+
case format
|
|
61
|
+
when "json"
|
|
62
|
+
puts JSON.pretty_generate(sbom.to_h)
|
|
63
|
+
when "yaml"
|
|
64
|
+
puts sbom.to_h.to_yaml
|
|
65
|
+
else
|
|
66
|
+
print_summary(sbom)
|
|
67
|
+
end
|
|
68
|
+
rescue Sbom::ParserError => e
|
|
69
|
+
abort "Parse error: #{e.message}"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def generate_command
|
|
73
|
+
parser = OptionParser.new do |opts|
|
|
74
|
+
opts.banner = "Usage: sbom generate [options]"
|
|
75
|
+
opts.on("-n", "--name NAME", "Project name") { |v| @options[:name] = v }
|
|
76
|
+
opts.on("-t", "--type TYPE", "SBOM type (spdx, cyclonedx)") { |v| @options[:type] = v.to_sym }
|
|
77
|
+
opts.on("-f", "--format FORMAT", "Output format (tag, json, yaml)") { |v| @options[:format] = v.to_sym }
|
|
78
|
+
opts.on("-o", "--output FILE", "Output file") { |v| @options[:output] = v }
|
|
79
|
+
opts.on("-h", "--help", "Show help") { puts opts; exit }
|
|
80
|
+
end
|
|
81
|
+
parser.parse!(@args)
|
|
82
|
+
|
|
83
|
+
name = @options[:name] || "SBOM"
|
|
84
|
+
sbom_type = @options[:type] || :spdx
|
|
85
|
+
format = @options[:format] || :json
|
|
86
|
+
|
|
87
|
+
generator = Sbom::Generator.new(sbom_type: sbom_type, format: format)
|
|
88
|
+
|
|
89
|
+
sbom_data = { packages: {} }
|
|
90
|
+
generator.generate(name, sbom_data)
|
|
91
|
+
|
|
92
|
+
output = generator.output
|
|
93
|
+
|
|
94
|
+
if @options[:output]
|
|
95
|
+
File.write(@options[:output], output)
|
|
96
|
+
puts "SBOM written to #{@options[:output]}"
|
|
97
|
+
else
|
|
98
|
+
puts output
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def validate_command
|
|
103
|
+
parser = OptionParser.new do |opts|
|
|
104
|
+
opts.banner = "Usage: sbom validate <file> [options]"
|
|
105
|
+
opts.on("-t", "--type TYPE", "SBOM type (spdx, cyclonedx, auto)") { |v| @options[:type] = v.to_sym }
|
|
106
|
+
opts.on("-h", "--help", "Show help") { puts opts; exit }
|
|
107
|
+
end
|
|
108
|
+
parser.parse!(@args)
|
|
109
|
+
|
|
110
|
+
file = @args.shift
|
|
111
|
+
abort "Error: No file specified" unless file
|
|
112
|
+
|
|
113
|
+
sbom_type = @options[:type] || :auto
|
|
114
|
+
validator = Sbom::Validator.new(sbom_type: sbom_type)
|
|
115
|
+
result = validator.validate_file(file)
|
|
116
|
+
|
|
117
|
+
if result.valid?
|
|
118
|
+
puts "#{result.format.to_s.upcase}: Valid (version #{result.version})"
|
|
119
|
+
else
|
|
120
|
+
puts "#{result.format&.to_s&.upcase || 'SBOM'}: INVALID"
|
|
121
|
+
result.errors.each { |e| puts " - #{e}" }
|
|
122
|
+
exit 1
|
|
123
|
+
end
|
|
124
|
+
rescue Sbom::ValidatorError => e
|
|
125
|
+
abort "Validation error: #{e.message}"
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def convert_command
|
|
129
|
+
parser = OptionParser.new do |opts|
|
|
130
|
+
opts.banner = "Usage: sbom convert <file> [options]"
|
|
131
|
+
opts.on("-t", "--type TYPE", "Output SBOM type (spdx, cyclonedx)") { |v| @options[:type] = v.to_sym }
|
|
132
|
+
opts.on("-f", "--format FORMAT", "Output format (tag, json, yaml)") { |v| @options[:format] = v.to_sym }
|
|
133
|
+
opts.on("-o", "--output FILE", "Output file") { |v| @options[:output] = v }
|
|
134
|
+
opts.on("-h", "--help", "Show help") { puts opts; exit }
|
|
135
|
+
end
|
|
136
|
+
parser.parse!(@args)
|
|
137
|
+
|
|
138
|
+
file = @args.shift
|
|
139
|
+
abort "Error: No file specified" unless file
|
|
140
|
+
|
|
141
|
+
sbom = Sbom::Parser.parse_file(file)
|
|
142
|
+
|
|
143
|
+
output_type = @options[:type] || :spdx
|
|
144
|
+
output_format = @options[:format] || :json
|
|
145
|
+
|
|
146
|
+
generator = Sbom::Generator.new(sbom_type: output_type, format: output_format)
|
|
147
|
+
generator.generate(sbom.document&.dig(:name) || "Converted", sbom.to_h)
|
|
148
|
+
|
|
149
|
+
output = generator.output
|
|
150
|
+
|
|
151
|
+
if @options[:output]
|
|
152
|
+
File.write(@options[:output], output)
|
|
153
|
+
puts "Converted SBOM written to #{@options[:output]}"
|
|
154
|
+
else
|
|
155
|
+
puts output
|
|
156
|
+
end
|
|
157
|
+
rescue Sbom::ParserError, Sbom::GeneratorError => e
|
|
158
|
+
abort "Conversion error: #{e.message}"
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def document_command
|
|
162
|
+
subcommand = @args.shift
|
|
163
|
+
|
|
164
|
+
case subcommand
|
|
165
|
+
when "outline"
|
|
166
|
+
document_outline_command
|
|
167
|
+
when "query"
|
|
168
|
+
document_query_command
|
|
169
|
+
when "info"
|
|
170
|
+
document_info_command
|
|
171
|
+
else
|
|
172
|
+
puts "Usage: sbom document <subcommand>"
|
|
173
|
+
puts ""
|
|
174
|
+
puts "Subcommands:"
|
|
175
|
+
puts " outline Show structure of an SBOM document"
|
|
176
|
+
puts " query Search for information in an SBOM"
|
|
177
|
+
puts " info Show document metadata"
|
|
178
|
+
exit 1
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def document_outline_command
|
|
183
|
+
parser = OptionParser.new do |opts|
|
|
184
|
+
opts.banner = "Usage: sbom document outline <file>"
|
|
185
|
+
opts.on("-h", "--help", "Show help") { puts opts; exit }
|
|
186
|
+
end
|
|
187
|
+
parser.parse!(@args)
|
|
188
|
+
|
|
189
|
+
file = @args.shift
|
|
190
|
+
abort "Error: No file specified" unless file
|
|
191
|
+
|
|
192
|
+
sbom = Sbom::Parser.parse_file(file)
|
|
193
|
+
|
|
194
|
+
puts "SBOM Structure"
|
|
195
|
+
puts "=============="
|
|
196
|
+
puts ""
|
|
197
|
+
puts "Type: #{sbom.sbom_type}"
|
|
198
|
+
puts "Version: #{sbom.version}"
|
|
199
|
+
puts ""
|
|
200
|
+
|
|
201
|
+
if sbom.document
|
|
202
|
+
puts "Document:"
|
|
203
|
+
puts " Name: #{sbom.document[:name]}"
|
|
204
|
+
puts " ID: #{sbom.document[:id]}"
|
|
205
|
+
puts ""
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
puts "Packages (#{sbom.packages.count}):"
|
|
209
|
+
sbom.packages.each_with_index do |pkg, i|
|
|
210
|
+
puts " #{i + 1}. #{pkg[:name]}#{pkg[:version] ? " @ #{pkg[:version]}" : ""}"
|
|
211
|
+
end
|
|
212
|
+
puts ""
|
|
213
|
+
|
|
214
|
+
if sbom.files.any?
|
|
215
|
+
puts "Files (#{sbom.files.count}):"
|
|
216
|
+
sbom.files.each_with_index do |file_data, i|
|
|
217
|
+
puts " #{i + 1}. #{file_data[:name]}"
|
|
218
|
+
end
|
|
219
|
+
puts ""
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
puts "Relationships (#{sbom.relationships.count}):"
|
|
223
|
+
sbom.relationships.each do |rel|
|
|
224
|
+
puts " #{rel[:source]} --[#{rel[:type]}]--> #{rel[:target]}"
|
|
225
|
+
end
|
|
226
|
+
rescue Sbom::ParserError => e
|
|
227
|
+
abort "Error: #{e.message}"
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def document_query_command
|
|
231
|
+
parser = OptionParser.new do |opts|
|
|
232
|
+
opts.banner = "Usage: sbom document query <file> [options]"
|
|
233
|
+
opts.on("-p", "--package NAME", "Find package by name") { |v| @options[:package] = v }
|
|
234
|
+
opts.on("-l", "--license LICENSE", "Find packages with license") { |v| @options[:license] = v }
|
|
235
|
+
opts.on("--purl PURL", "Find package by PURL") { |v| @options[:purl] = v }
|
|
236
|
+
opts.on("-h", "--help", "Show help") { puts opts; exit }
|
|
237
|
+
end
|
|
238
|
+
parser.parse!(@args)
|
|
239
|
+
|
|
240
|
+
file = @args.shift
|
|
241
|
+
abort "Error: No file specified" unless file
|
|
242
|
+
|
|
243
|
+
sbom = Sbom::Parser.parse_file(file)
|
|
244
|
+
|
|
245
|
+
if @options[:package]
|
|
246
|
+
results = sbom.packages.select { |p| p[:name]&.include?(@options[:package]) }
|
|
247
|
+
puts "Packages matching '#{@options[:package]}':"
|
|
248
|
+
results.each do |pkg|
|
|
249
|
+
puts " - #{pkg[:name]} @ #{pkg[:version]}"
|
|
250
|
+
puts " License: #{pkg[:license_concluded] || pkg[:license_declared] || 'Unknown'}"
|
|
251
|
+
end
|
|
252
|
+
elsif @options[:license]
|
|
253
|
+
results = sbom.packages.select do |p|
|
|
254
|
+
(p[:license_concluded] || p[:license_declared])&.include?(@options[:license])
|
|
255
|
+
end
|
|
256
|
+
puts "Packages with license '#{@options[:license]}':"
|
|
257
|
+
results.each { |pkg| puts " - #{pkg[:name]} @ #{pkg[:version]}" }
|
|
258
|
+
elsif @options[:purl]
|
|
259
|
+
results = sbom.packages.select { |p| p[:purl]&.include?(@options[:purl]) }
|
|
260
|
+
puts "Packages matching PURL '#{@options[:purl]}':"
|
|
261
|
+
results.each { |pkg| puts " - #{pkg[:name]} @ #{pkg[:version]}" }
|
|
262
|
+
else
|
|
263
|
+
puts "Specify a query option. Use --help for options."
|
|
264
|
+
end
|
|
265
|
+
rescue Sbom::ParserError => e
|
|
266
|
+
abort "Error: #{e.message}"
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def document_info_command
|
|
270
|
+
parser = OptionParser.new do |opts|
|
|
271
|
+
opts.banner = "Usage: sbom document info <file>"
|
|
272
|
+
opts.on("-h", "--help", "Show help") { puts opts; exit }
|
|
273
|
+
end
|
|
274
|
+
parser.parse!(@args)
|
|
275
|
+
|
|
276
|
+
file = @args.shift
|
|
277
|
+
abort "Error: No file specified" unless file
|
|
278
|
+
|
|
279
|
+
sbom = Sbom::Parser.parse_file(file)
|
|
280
|
+
|
|
281
|
+
puts "Document Information"
|
|
282
|
+
puts "===================="
|
|
283
|
+
puts ""
|
|
284
|
+
puts "Format: #{sbom.sbom_type.to_s.upcase}"
|
|
285
|
+
puts "Version: #{sbom.version}"
|
|
286
|
+
|
|
287
|
+
if sbom.document
|
|
288
|
+
doc = sbom.document
|
|
289
|
+
puts "Name: #{doc[:name]}" if doc[:name]
|
|
290
|
+
puts "ID: #{doc[:id]}" if doc[:id]
|
|
291
|
+
puts "Namespace: #{doc[:namespace]}" if doc[:namespace]
|
|
292
|
+
puts "Created: #{doc[:created]}" if doc[:created]
|
|
293
|
+
puts "Supplier: #{doc[:metadata_supplier]}" if doc[:metadata_supplier]
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
puts ""
|
|
297
|
+
puts "Statistics:"
|
|
298
|
+
puts " Packages: #{sbom.packages.count}"
|
|
299
|
+
puts " Files: #{sbom.files.count}"
|
|
300
|
+
puts " Relationships: #{sbom.relationships.count}"
|
|
301
|
+
|
|
302
|
+
licenses = sbom.packages.map { |p| p[:license_concluded] || p[:license_declared] }.compact.uniq
|
|
303
|
+
if licenses.any?
|
|
304
|
+
puts ""
|
|
305
|
+
puts "Licenses found:"
|
|
306
|
+
licenses.each { |lic| puts " - #{lic}" }
|
|
307
|
+
end
|
|
308
|
+
rescue Sbom::ParserError => e
|
|
309
|
+
abort "Error: #{e.message}"
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def print_summary(sbom)
|
|
313
|
+
puts "SBOM Summary"
|
|
314
|
+
puts "============"
|
|
315
|
+
puts "Type: #{sbom.sbom_type}"
|
|
316
|
+
puts "Packages: #{sbom.packages.count}"
|
|
317
|
+
puts "Files: #{sbom.files.count}"
|
|
318
|
+
puts "Relationships: #{sbom.relationships.count}"
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def show_help
|
|
322
|
+
puts <<~HELP
|
|
323
|
+
sbom - Software Bill of Materials tool
|
|
324
|
+
|
|
325
|
+
Usage: sbom <command> [options]
|
|
326
|
+
|
|
327
|
+
Commands:
|
|
328
|
+
parse Parse and display SBOM contents
|
|
329
|
+
generate Create a new SBOM
|
|
330
|
+
validate Validate SBOM against schema
|
|
331
|
+
convert Convert between SBOM formats
|
|
332
|
+
document Work with SBOM documents
|
|
333
|
+
version Show version
|
|
334
|
+
|
|
335
|
+
Document subcommands:
|
|
336
|
+
outline Show structure of an SBOM
|
|
337
|
+
query Search for information in an SBOM
|
|
338
|
+
info Show document metadata
|
|
339
|
+
|
|
340
|
+
Run 'sbom <command> --help' for more information on a command.
|
|
341
|
+
HELP
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
Sbom::CLI.new(ARGV).run
|