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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e5779ca3a21e46aa2032845504cda20229928dad1570d5de2a055ffe2a2dae8d
4
- data.tar.gz: d57cc2a38620373ca402ca34322e0cfcb4d30bfa88b8a6791868c92f87b2393d
3
+ metadata.gz: a22cb6e2f3394ccd7abb7e8a28d7282f11e4a9277cf1451dca16060d00ceb0f6
4
+ data.tar.gz: aaf00ea47acdb68969fb20a638a9300849fe363c1d7e8dfeb1955405d6a9d570
5
5
  SHA512:
6
- metadata.gz: 221caf4d995e38991fd6c720b00e662fe90ae7709b7032a35d6592848ce669e5245591ca66866b66b29b68326d78afc213ce5292815b4e0f18cce51dcbcf651d
7
- data.tar.gz: 1a4e9340fda31c7fff6dddbfc12b912220a7aee701c4e3df860c41c00e26c6c5a7999727017bb7af201a09d5e505b8fd24a67d5188eef0b3450b1efdfbfc50e7
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sbom
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
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.1.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