sbom: fix errors, improve reproducibility, tests.

- Remove/change data from bottle SBOM to avoid harming reproduciblity
- Add `schema_validation_errors` method to provide nicer test failures
- Add tests more tests for SBOM when bottling
- Cleanup SBOM tests to use more typical RSpec form and be DRYer
This commit is contained in:
Mike McQuaid 2024-08-08 09:34:32 +01:00
parent 6b186f9874
commit 42a6b59de5
No known key found for this signature in database
2 changed files with 120 additions and 90 deletions

View File

@ -22,18 +22,11 @@ class SBOM
end
active_spec_sym = formula.active_spec_sym
homebrew_version_maybe_dev = if (match_data = HOMEBREW_VERSION.match(/^[\d.]+/))
suffix = "-dev" if HOMEBREW_VERSION.include?("-")
match_data[0] + suffix.to_s
else
HOMEBREW_VERSION
end
attributes = {
name: formula.name,
homebrew_version: homebrew_version_maybe_dev,
homebrew_version: HOMEBREW_VERSION,
spdxfile: SBOM.spdxfile(formula),
time: tab.time,
time: tab.time || Time.now,
source_modified_time: tab.source_modified_time.to_i,
compiler: tab.compiler,
stdlib: tab.stdlib,
@ -92,22 +85,27 @@ class SBOM
@schema ||= JSON.parse(SCHEMA_FILE.read, freeze: true)
end
sig { params(bottling: T::Boolean).returns(T::Boolean) }
def valid?(bottling: false)
sig { params(bottling: T::Boolean).returns(T::Array[T::Hash[String, T.untyped]]) }
def schema_validation_errors(bottling: false)
unless require? "json_schemer"
error_message = "Need json_schemer to validate SBOM, run `brew install-bundler-gems --add-groups=bottle`!"
odie error_message if ENV["HOMEBREW_ENFORCE_SBOM"]
return true
return []
end
schemer = JSONSchemer.schema(SBOM.schema)
data = to_spdx_sbom(bottling:)
return true if schemer.valid?(data)
schemer.validate(data).map { |error| error["error"] }
end
sig { params(bottling: T::Boolean).returns(T::Boolean) }
def valid?(bottling: false)
validation_errors = schema_validation_errors(bottling:)
return true if validation_errors.empty?
opoo "SBOM validation errors:"
schemer.validate(data).to_a.each do |error|
puts error["error"]
end
validation_errors.each(&:puts)
odie "Failed to validate SBOM against JSON schema!" if ENV["HOMEBREW_ENFORCE_SBOM"]
@ -210,7 +208,7 @@ class SBOM
filesAnalyzed: false,
licenseDeclared: assert_value(nil),
builtDate: source_modified_time.to_s,
licenseConcluded: license,
licenseConcluded: assert_value(license),
downloadLocation: bottle_info.fetch("url"),
copyrightText: assert_value(nil),
externalRefs: [
@ -323,8 +321,8 @@ class SBOM
if stdlib.present?
compiler_info["SPDXRef-Stdlib"] = {
SPDXID: "SPDXRef-Stdlib",
name: stdlib,
versionInfo: stdlib,
name: stdlib.to_s,
versionInfo: stdlib.to_s,
filesAnalyzed: false,
licenseDeclared: assert_value(nil),
licenseConcluded: assert_value(nil),
@ -335,15 +333,21 @@ class SBOM
}
end
# Improve reproducibility when bottling.
if bottling
created = source_modified_time.iso8601
creators = ["Tool: https://github.com/Homebrew/brew"]
else
created = Time.at(time).utc.iso8601
creators = ["Tool: https://github.com/Homebrew/brew@#{homebrew_version}"]
end
packages = generate_packages_json(runtime_full, compiler_info, bottling:)
{
SPDXID: "SPDXRef-DOCUMENT",
spdxVersion: "SPDX-2.3",
name: "SBOM-SPDX-#{name}-#{spec_version}",
creationInfo: {
created: (Time.at(time).utc.iso8601 if time.present? && !bottling),
creators: ["Tool: https://github.com/homebrew/brew@#{homebrew_version}"],
},
creationInfo: { created:, creators: },
dataLicense: "CC0-1.0",
documentNamespace: "https://formulae.brew.sh/spdx/#{name}-#{spec_version}.json",
documentDescribes: packages.map { |dependency| dependency[:SPDXID] },

View File

@ -3,15 +3,25 @@
require "sbom"
RSpec.describe SBOM do
describe "#valid?" do
it "returns true if a minimal SBOM is valid" do
f = formula { url "foo-1.0" }
sbom = described_class.create(f, Tab.new)
expect(sbom).to be_valid
describe "#schema_validation_errors" do
subject(:sbom) { described_class.create(f, tab) }
before { ENV.delete("HOMEBREW_ENFORCE_SBOM") }
let(:f) { formula { url "foo-1.0" } }
let(:tab) { Tab.new }
it "returns true if valid" do
expect(sbom.schema_validation_errors).to be_empty
end
it "returns true if a maximal SBOM is valid" do
f = formula do
it "returns true if valid when bottling" do
expect(sbom.schema_validation_errors(bottling: true)).to be_empty
end
context "with a maximal SBOM" do
let(:f) do
formula do
homepage "https://brew.sh"
url "https://brew.sh/test-0.1.tbz"
@ -32,7 +42,8 @@ RSpec.describe SBOM do
uses_from_macos "python" => :build
uses_from_macos "zlib"
end
end
let(:tab) do
beanstalkd = formula "beanstalkd" do
url "one-1.1"
@ -59,21 +70,36 @@ RSpec.describe SBOM do
"declared_directly" => true,
}
end
expect(Tab).to receive(:runtime_deps_hash).and_return(runtime_deps_hash)
allow(Tab).to receive(:runtime_deps_hash).and_return(runtime_deps_hash)
tab = Tab.create(f, DevelopmentTools.default_compiler, :libcxx)
expect(Formulary).to receive(:factory).with("beanstalkd").and_return(beanstalkd)
expect(Formulary).to receive(:factory).with("zlib").and_return(zlib)
allow(Formulary).to receive(:factory).with("beanstalkd").and_return(beanstalkd)
allow(Formulary).to receive(:factory).with("zlib").and_return(zlib)
sbom = described_class.create(f, tab)
expect(sbom).to be_valid
tab
end
it "returns false if an SBOM is invalid" do
f = formula { url "foo-1.0" }
sbom = described_class.create(f, Tab.new)
it "returns true if valid" do
expect(sbom.schema_validation_errors).to be_empty
end
it "returns true if valid when bottling" do
expect(sbom.schema_validation_errors(bottling: true)).to be_empty
end
end
context "with an invalid SBOM" do
before do
allow(sbom).to receive(:to_spdx_sbom).and_return({}) # fake an empty SBOM
expect(sbom).not_to be_valid
end
it "returns false" do
expect(sbom.schema_validation_errors).not_to be_empty
end
it "returns false when bottling" do
expect(sbom.schema_validation_errors(bottling: true)).not_to be_empty
end
end
end
end