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:
parent
6b186f9874
commit
42a6b59de5
@ -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] },
|
||||
|
||||
@ -3,77 +3,103 @@
|
||||
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
|
||||
homepage "https://brew.sh"
|
||||
|
||||
url "https://brew.sh/test-0.1.tbz"
|
||||
sha256 TEST_SHA256
|
||||
|
||||
patch do
|
||||
url "patch_macos"
|
||||
end
|
||||
|
||||
bottle do
|
||||
sha256 all: "9befdad158e59763fb0622083974a6252878019702d8c961e1bec3a5f5305339"
|
||||
end
|
||||
|
||||
# some random dependencies to test with
|
||||
depends_on "cmake" => :build
|
||||
depends_on "beanstalkd"
|
||||
|
||||
uses_from_macos "python" => :build
|
||||
uses_from_macos "zlib"
|
||||
end
|
||||
|
||||
beanstalkd = formula "beanstalkd" do
|
||||
url "one-1.1"
|
||||
|
||||
bottle do
|
||||
sha256 all: "ac4c0330b70dae06eaa8065bfbea78dda277699d1ae8002478017a1bd9cf1908"
|
||||
end
|
||||
end
|
||||
|
||||
zlib = formula "zlib" do
|
||||
url "two-1.1"
|
||||
|
||||
bottle do
|
||||
sha256 all: "6a4642964fe5c4d1cc8cd3507541736d5b984e34a303a814ef550d4f2f8242f9"
|
||||
end
|
||||
end
|
||||
|
||||
runtime_dependencies = [beanstalkd, zlib]
|
||||
runtime_deps_hash = runtime_dependencies.map do |dep|
|
||||
{
|
||||
"full_name" => dep.full_name,
|
||||
"version" => dep.version.to_s,
|
||||
"revision" => dep.revision,
|
||||
"pkg_version" => dep.pkg_version.to_s,
|
||||
"declared_directly" => true,
|
||||
}
|
||||
end
|
||||
expect(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)
|
||||
|
||||
sbom = described_class.create(f, tab)
|
||||
expect(sbom).to be_valid
|
||||
it "returns true if valid when bottling" do
|
||||
expect(sbom.schema_validation_errors(bottling: true)).to be_empty
|
||||
end
|
||||
|
||||
it "returns false if an SBOM is invalid" do
|
||||
f = formula { url "foo-1.0" }
|
||||
sbom = described_class.create(f, Tab.new)
|
||||
allow(sbom).to receive(:to_spdx_sbom).and_return({}) # fake an empty SBOM
|
||||
expect(sbom).not_to be_valid
|
||||
context "with a maximal SBOM" do
|
||||
let(:f) do
|
||||
formula do
|
||||
homepage "https://brew.sh"
|
||||
|
||||
url "https://brew.sh/test-0.1.tbz"
|
||||
sha256 TEST_SHA256
|
||||
|
||||
patch do
|
||||
url "patch_macos"
|
||||
end
|
||||
|
||||
bottle do
|
||||
sha256 all: "9befdad158e59763fb0622083974a6252878019702d8c961e1bec3a5f5305339"
|
||||
end
|
||||
|
||||
# some random dependencies to test with
|
||||
depends_on "cmake" => :build
|
||||
depends_on "beanstalkd"
|
||||
|
||||
uses_from_macos "python" => :build
|
||||
uses_from_macos "zlib"
|
||||
end
|
||||
end
|
||||
let(:tab) do
|
||||
beanstalkd = formula "beanstalkd" do
|
||||
url "one-1.1"
|
||||
|
||||
bottle do
|
||||
sha256 all: "ac4c0330b70dae06eaa8065bfbea78dda277699d1ae8002478017a1bd9cf1908"
|
||||
end
|
||||
end
|
||||
|
||||
zlib = formula "zlib" do
|
||||
url "two-1.1"
|
||||
|
||||
bottle do
|
||||
sha256 all: "6a4642964fe5c4d1cc8cd3507541736d5b984e34a303a814ef550d4f2f8242f9"
|
||||
end
|
||||
end
|
||||
|
||||
runtime_dependencies = [beanstalkd, zlib]
|
||||
runtime_deps_hash = runtime_dependencies.map do |dep|
|
||||
{
|
||||
"full_name" => dep.full_name,
|
||||
"version" => dep.version.to_s,
|
||||
"revision" => dep.revision,
|
||||
"pkg_version" => dep.pkg_version.to_s,
|
||||
"declared_directly" => true,
|
||||
}
|
||||
end
|
||||
allow(Tab).to receive(:runtime_deps_hash).and_return(runtime_deps_hash)
|
||||
tab = Tab.create(f, DevelopmentTools.default_compiler, :libcxx)
|
||||
|
||||
allow(Formulary).to receive(:factory).with("beanstalkd").and_return(beanstalkd)
|
||||
allow(Formulary).to receive(:factory).with("zlib").and_return(zlib)
|
||||
|
||||
tab
|
||||
end
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user