sbom 0.1.0 → 0.2.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/CHANGELOG.md +7 -0
- data/CONTRIBUTING.md +49 -0
- data/LICENSE +21 -0
- data/README.md +21 -0
- data/SECURITY.md +21 -0
- data/exe/sbom +48 -0
- data/lib/sbom/enricher.rb +162 -0
- data/lib/sbom/version.rb +1 -1
- data/lib/sbom.rb +10 -0
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a22cb6e2f3394ccd7abb7e8a28d7282f11e4a9277cf1451dca16060d00ceb0f6
|
|
4
|
+
data.tar.gz: aaf00ea47acdb68969fb20a638a9300849fe363c1d7e8dfeb1955405d6a9d570
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8151a326269d029319b4dcb69bd61edcf6ef5745a673cda96464bb91afa2103aa5896d4c8e315951e54ac605b031582af3bd1eae0eb2d7f7b673cd4afc8999df
|
|
7
|
+
data.tar.gz: 6c47ebcbbdeef79496348fdde56280b90ac6e90bec8e52cded836d95415fa1ec0b8b2ad3874992d3bd7ca9e928114e57cabc1a72d7525e1923faf94db3273d07
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.2.0] - 2025-12-14
|
|
4
|
+
|
|
5
|
+
- Add `enrich` command to CLI for enriching SBOMs with data from ecosyste.ms
|
|
6
|
+
- Add `Sbom.enrich` and `Sbom.enrich_file` library methods
|
|
7
|
+
- Add `Sbom::Enricher` class for enriching packages with metadata and security advisories
|
|
8
|
+
- Enrichment adds: description, homepage, download location, license, repository URL, registry URL, documentation URL, supplier info, and security advisories
|
|
9
|
+
|
|
3
10
|
## [0.1.0] - 2025-12-14
|
|
4
11
|
|
|
5
12
|
- Initial release
|
data/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/andrew/sbom.
|
|
4
|
+
|
|
5
|
+
## Getting Started
|
|
6
|
+
|
|
7
|
+
1. Fork the repository
|
|
8
|
+
2. Clone your fork and set up the development environment:
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
git clone https://github.com/YOUR_USERNAME/sbom.git
|
|
12
|
+
cd sbom
|
|
13
|
+
bin/setup
|
|
14
|
+
git submodule update --init --recursive
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
3. Run the tests to make sure everything works:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
bundle exec rake test
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Making Changes
|
|
24
|
+
|
|
25
|
+
1. Create a feature branch from `main`
|
|
26
|
+
2. Make your changes
|
|
27
|
+
3. Add tests for any new functionality
|
|
28
|
+
4. Ensure all tests pass
|
|
29
|
+
5. Submit a pull request
|
|
30
|
+
|
|
31
|
+
## Code Style
|
|
32
|
+
|
|
33
|
+
- Follow existing code conventions
|
|
34
|
+
- Keep commits focused and atomic
|
|
35
|
+
- Write clear commit messages
|
|
36
|
+
|
|
37
|
+
## Running Tests
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
bundle exec rake test
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Reporting Bugs
|
|
44
|
+
|
|
45
|
+
When reporting bugs, please include:
|
|
46
|
+
- Ruby version
|
|
47
|
+
- Steps to reproduce
|
|
48
|
+
- Expected vs actual behavior
|
|
49
|
+
- Sample SBOM files if relevant (sanitized of any sensitive data)
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Andrew Nesbitt
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
CHANGED
|
@@ -69,6 +69,22 @@ end
|
|
|
69
69
|
Sbom::Validator.validate_file!("example.cdx.json")
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
+
### Enriching SBOMs
|
|
73
|
+
|
|
74
|
+
Enrich packages with metadata from [ecosyste.ms](https://ecosyste.ms):
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
# Enrich an entire SBOM
|
|
78
|
+
sbom = Sbom.parse_file("example.cdx.json")
|
|
79
|
+
enriched = Sbom.enrich(sbom)
|
|
80
|
+
|
|
81
|
+
# Or parse and enrich in one step
|
|
82
|
+
enriched = Sbom.enrich_file("example.cdx.json")
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Enrichment adds: description, homepage, download location, license, repository URL, registry URL, documentation URL, supplier info, and security advisories.
|
|
87
|
+
|
|
72
88
|
### Building Packages
|
|
73
89
|
|
|
74
90
|
The Package class provides an object interface for building package data:
|
|
@@ -118,6 +134,11 @@ sbom document outline example.cdx.json
|
|
|
118
134
|
sbom document info example.spdx.json
|
|
119
135
|
sbom document query example.cdx.json --package rails
|
|
120
136
|
sbom document query example.cdx.json --license MIT
|
|
137
|
+
|
|
138
|
+
# Enrich SBOM with ecosyste.ms data
|
|
139
|
+
sbom enrich example.cdx.json
|
|
140
|
+
sbom enrich example.cdx.json --output enriched.json
|
|
141
|
+
cat example.cdx.json | sbom enrich -
|
|
121
142
|
```
|
|
122
143
|
|
|
123
144
|
## Supported Formats
|
data/SECURITY.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Supported Versions
|
|
4
|
+
|
|
5
|
+
| Version | Supported |
|
|
6
|
+
| ------- | ------------------ |
|
|
7
|
+
| 0.1.x | :white_check_mark: |
|
|
8
|
+
|
|
9
|
+
## Reporting a Vulnerability
|
|
10
|
+
|
|
11
|
+
If you discover a security vulnerability in this project, please report it privately by emailing andrewnez@gmail.com.
|
|
12
|
+
|
|
13
|
+
Please include:
|
|
14
|
+
- A description of the vulnerability
|
|
15
|
+
- Steps to reproduce the issue
|
|
16
|
+
- Potential impact
|
|
17
|
+
- Any suggested fixes (optional)
|
|
18
|
+
|
|
19
|
+
You can expect an initial response within 48 hours. We will work with you to understand and address the issue promptly.
|
|
20
|
+
|
|
21
|
+
Please do not open public issues for security vulnerabilities.
|
data/exe/sbom
CHANGED
|
@@ -25,6 +25,8 @@ module Sbom
|
|
|
25
25
|
validate_command
|
|
26
26
|
when "convert"
|
|
27
27
|
convert_command
|
|
28
|
+
when "enrich"
|
|
29
|
+
enrich_command
|
|
28
30
|
when "document"
|
|
29
31
|
document_command
|
|
30
32
|
when "version", "-v", "--version"
|
|
@@ -158,6 +160,51 @@ module Sbom
|
|
|
158
160
|
abort "Conversion error: #{e.message}"
|
|
159
161
|
end
|
|
160
162
|
|
|
163
|
+
def enrich_command
|
|
164
|
+
parser = OptionParser.new do |opts|
|
|
165
|
+
opts.banner = "Usage: sbom enrich <file> [options]"
|
|
166
|
+
opts.on("-t", "--type TYPE", "SBOM type (spdx, cyclonedx, auto)") { |v| @options[:type] = v.to_sym }
|
|
167
|
+
opts.on("-f", "--format FORMAT", "Output format (json, yaml)") { |v| @options[:format] = v }
|
|
168
|
+
opts.on("-o", "--output FILE", "Output file") { |v| @options[:output] = v }
|
|
169
|
+
opts.on("--advisories", "Include security advisories") { @options[:advisories] = true }
|
|
170
|
+
opts.on("-h", "--help", "Show help") { puts opts; exit }
|
|
171
|
+
end
|
|
172
|
+
parser.parse!(@args)
|
|
173
|
+
|
|
174
|
+
file = @args.shift
|
|
175
|
+
abort "Error: No file specified. Use '-' for stdin." unless file
|
|
176
|
+
|
|
177
|
+
sbom = if file == "-"
|
|
178
|
+
content = $stdin.read
|
|
179
|
+
Sbom::Parser.parse_string(content, sbom_type: @options[:type] || :auto)
|
|
180
|
+
else
|
|
181
|
+
Sbom::Parser.parse_file(file, sbom_type: @options[:type] || :auto)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
enricher = Sbom::Enricher.new(sbom)
|
|
185
|
+
enriched = enricher.enrich
|
|
186
|
+
|
|
187
|
+
format = @options[:format] || "json"
|
|
188
|
+
output = case format
|
|
189
|
+
when "yaml"
|
|
190
|
+
enriched.to_h.to_yaml
|
|
191
|
+
else
|
|
192
|
+
JSON.pretty_generate(enriched.to_h)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
if @options[:output]
|
|
196
|
+
File.write(@options[:output], output)
|
|
197
|
+
warn "Enriched SBOM written to #{@options[:output]}"
|
|
198
|
+
warn "Enrichment errors: #{enricher.errors.count}" if enricher.errors.any?
|
|
199
|
+
else
|
|
200
|
+
puts output
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
enricher.errors.each { |e| warn "Warning: #{e[:purl]} - #{e[:error]}" } if enricher.errors.any?
|
|
204
|
+
rescue Sbom::ParserError => e
|
|
205
|
+
abort "Error: #{e.message}"
|
|
206
|
+
end
|
|
207
|
+
|
|
161
208
|
def document_command
|
|
162
209
|
subcommand = @args.shift
|
|
163
210
|
|
|
@@ -329,6 +376,7 @@ module Sbom
|
|
|
329
376
|
generate Create a new SBOM
|
|
330
377
|
validate Validate SBOM against schema
|
|
331
378
|
convert Convert between SBOM formats
|
|
379
|
+
enrich Enrich SBOM with data from ecosyste.ms
|
|
332
380
|
document Work with SBOM documents
|
|
333
381
|
version Show version
|
|
334
382
|
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sbom
|
|
4
|
+
class Enricher
|
|
5
|
+
attr_reader :sbom, :errors
|
|
6
|
+
|
|
7
|
+
def initialize(sbom)
|
|
8
|
+
@sbom = sbom
|
|
9
|
+
@errors = []
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def enrich
|
|
13
|
+
@sbom.packages.each do |package|
|
|
14
|
+
enrich_package(package)
|
|
15
|
+
end
|
|
16
|
+
@sbom
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def enrich_package(package)
|
|
20
|
+
purl_string = package[:purl] || find_purl_in_external_refs(package)
|
|
21
|
+
return unless purl_string
|
|
22
|
+
|
|
23
|
+
parsed = parse_purl(purl_string)
|
|
24
|
+
return unless parsed
|
|
25
|
+
|
|
26
|
+
lookup_data = fetch_lookup(parsed)
|
|
27
|
+
enrich_from_lookup(package, lookup_data) if lookup_data
|
|
28
|
+
|
|
29
|
+
advisories = fetch_advisories(parsed)
|
|
30
|
+
enrich_from_advisories(package, advisories) if advisories&.any?
|
|
31
|
+
rescue StandardError => e
|
|
32
|
+
@errors << { purl: purl_string, error: e.message }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.enrich(sbom)
|
|
36
|
+
new(sbom).enrich
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.enrich_package(package)
|
|
40
|
+
purl_string = package[:purl] || find_purl_in_refs(package)
|
|
41
|
+
return package unless purl_string
|
|
42
|
+
|
|
43
|
+
parsed = Purl.parse(purl_string)
|
|
44
|
+
return package unless parsed
|
|
45
|
+
|
|
46
|
+
lookup_data = parsed.lookup
|
|
47
|
+
apply_lookup_enrichment(package, lookup_data) if lookup_data
|
|
48
|
+
|
|
49
|
+
advisories = parsed.advisories
|
|
50
|
+
apply_advisory_enrichment(package, advisories) if advisories&.any?
|
|
51
|
+
|
|
52
|
+
package
|
|
53
|
+
rescue StandardError
|
|
54
|
+
package
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def find_purl_in_external_refs(package)
|
|
60
|
+
refs = package[:external_references] || []
|
|
61
|
+
refs.find { |_, type, _| type == "purl" }&.last
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def self.find_purl_in_refs(package)
|
|
65
|
+
refs = package[:external_references] || []
|
|
66
|
+
refs.find { |_, type, _| type == "purl" }&.last
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def parse_purl(purl_string)
|
|
70
|
+
Purl.parse(purl_string)
|
|
71
|
+
rescue Purl::InvalidPackageURL
|
|
72
|
+
@errors << { purl: purl_string, error: "Invalid PURL format" }
|
|
73
|
+
nil
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def fetch_lookup(parsed_purl)
|
|
77
|
+
parsed_purl.lookup
|
|
78
|
+
rescue StandardError => e
|
|
79
|
+
@errors << { purl: parsed_purl.to_s, error: "Lookup failed: #{e.message}" }
|
|
80
|
+
nil
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def fetch_advisories(parsed_purl)
|
|
84
|
+
parsed_purl.advisories
|
|
85
|
+
rescue StandardError => e
|
|
86
|
+
@errors << { purl: parsed_purl.to_s, error: "Advisories fetch failed: #{e.message}" }
|
|
87
|
+
[]
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def enrich_from_lookup(package, data)
|
|
91
|
+
self.class.apply_lookup_enrichment(package, data)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def enrich_from_advisories(package, advisories)
|
|
95
|
+
self.class.apply_advisory_enrichment(package, advisories)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def self.apply_lookup_enrichment(package, data)
|
|
99
|
+
pkg_data = data[:package] || {}
|
|
100
|
+
version_data = data[:version] || {}
|
|
101
|
+
|
|
102
|
+
package[:description] ||= pkg_data[:description]
|
|
103
|
+
package[:homepage] ||= pkg_data[:homepage]
|
|
104
|
+
package[:download_location] ||= version_data[:download_url]
|
|
105
|
+
|
|
106
|
+
if pkg_data[:licenses] && !package[:license_concluded]
|
|
107
|
+
package[:license_concluded] = pkg_data[:licenses]
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
package[:repository_url] ||= pkg_data[:repository_url]
|
|
111
|
+
package[:registry_url] ||= pkg_data[:registry_url]
|
|
112
|
+
package[:documentation_url] ||= pkg_data[:documentation_url]
|
|
113
|
+
|
|
114
|
+
if pkg_data[:maintainers]&.any? && !package[:supplier]
|
|
115
|
+
first_maintainer = pkg_data[:maintainers].first
|
|
116
|
+
package[:supplier] = first_maintainer[:login] if first_maintainer
|
|
117
|
+
package[:supplier_type] = "Organization"
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
if pkg_data[:keywords]&.any?
|
|
121
|
+
package[:tags] ||= []
|
|
122
|
+
package[:tags].concat(pkg_data[:keywords]).uniq!
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
package[:properties] ||= []
|
|
126
|
+
if pkg_data[:latest_version]
|
|
127
|
+
package[:properties] << ["ecosystems:latest_version", pkg_data[:latest_version]]
|
|
128
|
+
end
|
|
129
|
+
if pkg_data[:latest_version_published_at]
|
|
130
|
+
package[:properties] << ["ecosystems:latest_version_published_at", pkg_data[:latest_version_published_at]]
|
|
131
|
+
end
|
|
132
|
+
if pkg_data[:versions_count]
|
|
133
|
+
package[:properties] << ["ecosystems:versions_count", pkg_data[:versions_count].to_s]
|
|
134
|
+
end
|
|
135
|
+
if version_data[:published_at]
|
|
136
|
+
package[:properties] << ["ecosystems:version_published_at", version_data[:published_at]]
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
package
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def self.apply_advisory_enrichment(package, advisories)
|
|
143
|
+
package[:advisories] ||= []
|
|
144
|
+
|
|
145
|
+
advisories.each do |advisory|
|
|
146
|
+
package[:advisories] << {
|
|
147
|
+
id: advisory[:id],
|
|
148
|
+
title: advisory[:title],
|
|
149
|
+
description: advisory[:description],
|
|
150
|
+
severity: advisory[:severity],
|
|
151
|
+
cvss_score: advisory[:cvss_score],
|
|
152
|
+
url: advisory[:url],
|
|
153
|
+
published_at: advisory[:published_at],
|
|
154
|
+
source: advisory[:source_kind],
|
|
155
|
+
references: advisory[:references]
|
|
156
|
+
}
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
package
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
data/lib/sbom/version.rb
CHANGED
data/lib/sbom.rb
CHANGED
|
@@ -32,6 +32,7 @@ require_relative "sbom/generator"
|
|
|
32
32
|
require_relative "sbom/validation_result"
|
|
33
33
|
require_relative "sbom/validator"
|
|
34
34
|
require_relative "sbom/output"
|
|
35
|
+
require_relative "sbom/enricher"
|
|
35
36
|
|
|
36
37
|
module Sbom
|
|
37
38
|
class << self
|
|
@@ -50,5 +51,14 @@ module Sbom
|
|
|
50
51
|
def validate_file(filename, sbom_type: :auto)
|
|
51
52
|
Validator.validate_file(filename, sbom_type: sbom_type)
|
|
52
53
|
end
|
|
54
|
+
|
|
55
|
+
def enrich(sbom)
|
|
56
|
+
Enricher.enrich(sbom)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def enrich_file(filename, sbom_type: :auto)
|
|
60
|
+
sbom = parse_file(filename, sbom_type: sbom_type)
|
|
61
|
+
Enricher.enrich(sbom)
|
|
62
|
+
end
|
|
53
63
|
end
|
|
54
64
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: sbom
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrew Nesbitt
|
|
@@ -63,8 +63,11 @@ files:
|
|
|
63
63
|
- ".gitmodules"
|
|
64
64
|
- CHANGELOG.md
|
|
65
65
|
- CODE_OF_CONDUCT.md
|
|
66
|
+
- CONTRIBUTING.md
|
|
67
|
+
- LICENSE
|
|
66
68
|
- README.md
|
|
67
69
|
- Rakefile
|
|
70
|
+
- SECURITY.md
|
|
68
71
|
- exe/sbom
|
|
69
72
|
- lib/sbom.rb
|
|
70
73
|
- lib/sbom/cyclonedx/generator.rb
|
|
@@ -74,6 +77,7 @@ files:
|
|
|
74
77
|
- lib/sbom/data/package.rb
|
|
75
78
|
- lib/sbom/data/relationship.rb
|
|
76
79
|
- lib/sbom/data/sbom.rb
|
|
80
|
+
- lib/sbom/enricher.rb
|
|
77
81
|
- lib/sbom/error.rb
|
|
78
82
|
- lib/sbom/generator.rb
|
|
79
83
|
- lib/sbom/license/data/spdx_licenses.json
|