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
|
end
|
||||||
active_spec_sym = formula.active_spec_sym
|
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 = {
|
attributes = {
|
||||||
name: formula.name,
|
name: formula.name,
|
||||||
homebrew_version: homebrew_version_maybe_dev,
|
homebrew_version: HOMEBREW_VERSION,
|
||||||
spdxfile: SBOM.spdxfile(formula),
|
spdxfile: SBOM.spdxfile(formula),
|
||||||
time: tab.time,
|
time: tab.time || Time.now,
|
||||||
source_modified_time: tab.source_modified_time.to_i,
|
source_modified_time: tab.source_modified_time.to_i,
|
||||||
compiler: tab.compiler,
|
compiler: tab.compiler,
|
||||||
stdlib: tab.stdlib,
|
stdlib: tab.stdlib,
|
||||||
@ -92,22 +85,27 @@ class SBOM
|
|||||||
@schema ||= JSON.parse(SCHEMA_FILE.read, freeze: true)
|
@schema ||= JSON.parse(SCHEMA_FILE.read, freeze: true)
|
||||||
end
|
end
|
||||||
|
|
||||||
sig { params(bottling: T::Boolean).returns(T::Boolean) }
|
sig { params(bottling: T::Boolean).returns(T::Array[T::Hash[String, T.untyped]]) }
|
||||||
def valid?(bottling: false)
|
def schema_validation_errors(bottling: false)
|
||||||
unless require? "json_schemer"
|
unless require? "json_schemer"
|
||||||
error_message = "Need json_schemer to validate SBOM, run `brew install-bundler-gems --add-groups=bottle`!"
|
error_message = "Need json_schemer to validate SBOM, run `brew install-bundler-gems --add-groups=bottle`!"
|
||||||
odie error_message if ENV["HOMEBREW_ENFORCE_SBOM"]
|
odie error_message if ENV["HOMEBREW_ENFORCE_SBOM"]
|
||||||
return true
|
return []
|
||||||
end
|
end
|
||||||
|
|
||||||
schemer = JSONSchemer.schema(SBOM.schema)
|
schemer = JSONSchemer.schema(SBOM.schema)
|
||||||
data = to_spdx_sbom(bottling:)
|
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:"
|
opoo "SBOM validation errors:"
|
||||||
schemer.validate(data).to_a.each do |error|
|
validation_errors.each(&:puts)
|
||||||
puts error["error"]
|
|
||||||
end
|
|
||||||
|
|
||||||
odie "Failed to validate SBOM against JSON schema!" if ENV["HOMEBREW_ENFORCE_SBOM"]
|
odie "Failed to validate SBOM against JSON schema!" if ENV["HOMEBREW_ENFORCE_SBOM"]
|
||||||
|
|
||||||
@ -210,7 +208,7 @@ class SBOM
|
|||||||
filesAnalyzed: false,
|
filesAnalyzed: false,
|
||||||
licenseDeclared: assert_value(nil),
|
licenseDeclared: assert_value(nil),
|
||||||
builtDate: source_modified_time.to_s,
|
builtDate: source_modified_time.to_s,
|
||||||
licenseConcluded: license,
|
licenseConcluded: assert_value(license),
|
||||||
downloadLocation: bottle_info.fetch("url"),
|
downloadLocation: bottle_info.fetch("url"),
|
||||||
copyrightText: assert_value(nil),
|
copyrightText: assert_value(nil),
|
||||||
externalRefs: [
|
externalRefs: [
|
||||||
@ -323,8 +321,8 @@ class SBOM
|
|||||||
if stdlib.present?
|
if stdlib.present?
|
||||||
compiler_info["SPDXRef-Stdlib"] = {
|
compiler_info["SPDXRef-Stdlib"] = {
|
||||||
SPDXID: "SPDXRef-Stdlib",
|
SPDXID: "SPDXRef-Stdlib",
|
||||||
name: stdlib,
|
name: stdlib.to_s,
|
||||||
versionInfo: stdlib,
|
versionInfo: stdlib.to_s,
|
||||||
filesAnalyzed: false,
|
filesAnalyzed: false,
|
||||||
licenseDeclared: assert_value(nil),
|
licenseDeclared: assert_value(nil),
|
||||||
licenseConcluded: assert_value(nil),
|
licenseConcluded: assert_value(nil),
|
||||||
@ -335,15 +333,21 @@ class SBOM
|
|||||||
}
|
}
|
||||||
end
|
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:)
|
packages = generate_packages_json(runtime_full, compiler_info, bottling:)
|
||||||
{
|
{
|
||||||
SPDXID: "SPDXRef-DOCUMENT",
|
SPDXID: "SPDXRef-DOCUMENT",
|
||||||
spdxVersion: "SPDX-2.3",
|
spdxVersion: "SPDX-2.3",
|
||||||
name: "SBOM-SPDX-#{name}-#{spec_version}",
|
name: "SBOM-SPDX-#{name}-#{spec_version}",
|
||||||
creationInfo: {
|
creationInfo: { created:, creators: },
|
||||||
created: (Time.at(time).utc.iso8601 if time.present? && !bottling),
|
|
||||||
creators: ["Tool: https://github.com/homebrew/brew@#{homebrew_version}"],
|
|
||||||
},
|
|
||||||
dataLicense: "CC0-1.0",
|
dataLicense: "CC0-1.0",
|
||||||
documentNamespace: "https://formulae.brew.sh/spdx/#{name}-#{spec_version}.json",
|
documentNamespace: "https://formulae.brew.sh/spdx/#{name}-#{spec_version}.json",
|
||||||
documentDescribes: packages.map { |dependency| dependency[:SPDXID] },
|
documentDescribes: packages.map { |dependency| dependency[:SPDXID] },
|
||||||
|
|||||||
@ -3,77 +3,103 @@
|
|||||||
require "sbom"
|
require "sbom"
|
||||||
|
|
||||||
RSpec.describe SBOM do
|
RSpec.describe SBOM do
|
||||||
describe "#valid?" do
|
describe "#schema_validation_errors" do
|
||||||
it "returns true if a minimal SBOM is valid" do
|
subject(:sbom) { described_class.create(f, tab) }
|
||||||
f = formula { url "foo-1.0" }
|
|
||||||
sbom = described_class.create(f, Tab.new)
|
before { ENV.delete("HOMEBREW_ENFORCE_SBOM") }
|
||||||
expect(sbom).to be_valid
|
|
||||||
|
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
|
end
|
||||||
|
|
||||||
it "returns true if a maximal SBOM is valid" do
|
it "returns true if valid when bottling" do
|
||||||
f = formula do
|
expect(sbom.schema_validation_errors(bottling: true)).to be_empty
|
||||||
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
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it "returns false if an SBOM is invalid" do
|
context "with a maximal SBOM" do
|
||||||
f = formula { url "foo-1.0" }
|
let(:f) do
|
||||||
sbom = described_class.create(f, Tab.new)
|
formula do
|
||||||
allow(sbom).to receive(:to_spdx_sbom).and_return({}) # fake an empty SBOM
|
homepage "https://brew.sh"
|
||||||
expect(sbom).not_to be_valid
|
|
||||||
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user