diff --git a/Library/Homebrew/dev-cmd/bottle.rb b/Library/Homebrew/dev-cmd/bottle.rb index fa87397904e5c..2495494c112d8 100644 --- a/Library/Homebrew/dev-cmd/bottle.rb +++ b/Library/Homebrew/dev-cmd/bottle.rb @@ -494,7 +494,6 @@ def bottle_formula(formula) Tab.clear_cache Dependency.clear_cache Requirement.clear_cache - SBOM.clear_cache tab = keg.tab original_tab = tab.dup @@ -509,7 +508,7 @@ def bottle_formula(formula) end sbom = SBOM.create(formula, tab) - sbom.write + sbom.write(bottling: true) keg.consistent_reproducible_symlink_permissions! diff --git a/Library/Homebrew/formula_installer.rb b/Library/Homebrew/formula_installer.rb index cb6ae4795b6d5..48510ff628471 100644 --- a/Library/Homebrew/formula_installer.rb +++ b/Library/Homebrew/formula_installer.rb @@ -829,8 +829,8 @@ def finish tab.runtime_dependencies = Tab.runtime_deps_hash(formula, f_runtime_deps) tab.write - # write a SBOM file (if we don't already have one and aren't bottling) - if !build_bottle? && !SBOM.exist?(formula) + # write/update a SBOM file (if we aren't bottling) + unless build_bottle? sbom = SBOM.create(formula, tab) sbom.write(validate: Homebrew::EnvConfig.developer?) end diff --git a/Library/Homebrew/sbom.rb b/Library/Homebrew/sbom.rb index 0da3a404ed3e0..3148269251307 100644 --- a/Library/Homebrew/sbom.rb +++ b/Library/Homebrew/sbom.rb @@ -9,8 +9,6 @@ # Rather than calling `new` directly, use one of the class methods like {SBOM.create}. class SBOM - extend Cachable - FILENAME = "sbom.spdx.json" SCHEMA_URL = "https://spdx.github.io/spdx-3-model/model.jsonld" SCHEMA_FILENAME = "sbom.spdx.schema.3.json" @@ -23,7 +21,7 @@ def self.create(formula, tab) name: formula.name, homebrew_version: HOMEBREW_VERSION, spdxfile: SBOM.spdxfile(formula), - time: Time.now.to_i, + time: tab.time, source_modified_time: tab.source_modified_time.to_i, compiler: tab.compiler, stdlib: tab.stdlib, @@ -116,8 +114,8 @@ def self.fetch_schema! end end - sig { returns(T::Boolean) } - def valid? + sig { params(bottling: T::Boolean).returns(T::Boolean) } + def valid?(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"] @@ -132,7 +130,7 @@ def valid? end schemer = JSONSchemer.schema(schema) - data = to_spdx_sbom + data = to_spdx_sbom(bottling:) return true if schemer.valid?(data) opoo "SBOM validation errors:" @@ -145,20 +143,18 @@ def valid? false end - sig { params(validate: T::Boolean).void } - def write(validate: true) + sig { params(validate: T::Boolean, bottling: T::Boolean).void } + def write(validate: true, bottling: false) # If this is a new installation, the cache of installed formulae # will no longer be valid. Formula.clear_cache unless spdxfile.exist? - self.class.cache[spdxfile] = self - - if validate && !valid? + if validate && !valid?(bottling:) opoo "SBOM is not valid, not writing to disk!" return end - spdxfile.atomic_write(JSON.pretty_generate(to_spdx_sbom)) + spdxfile.atomic_write(JSON.pretty_generate(to_spdx_sbom(bottling:))) end private @@ -171,8 +167,14 @@ def initialize(attributes = {}) attributes.each { |key, value| instance_variable_set(:"@#{key}", value) } end - sig { params(runtime_dependency_declaration: T::Array[Hash], compiler_declaration: Hash).returns(T::Array[Hash]) } - def generate_relations_json(runtime_dependency_declaration, compiler_declaration) + sig { + params( + runtime_dependency_declaration: T::Array[Hash], + compiler_declaration: Hash, + bottling: T::Boolean, + ).returns(T::Array[Hash]) + } + def generate_relations_json(runtime_dependency_declaration, compiler_declaration, bottling:) runtime = runtime_dependency_declaration.map do |dependency| { spdxElementId: dependency[:SPDXID], @@ -180,6 +182,7 @@ def generate_relations_json(runtime_dependency_declaration, compiler_declaration relatedSpdxElement: "SPDXRef-Bottle-#{name}", } end + patches = source[:patches].each_with_index.map do |_patch, index| { spdxElementId: "SPDXRef-Patch-#{name}-#{index}", @@ -188,25 +191,26 @@ def generate_relations_json(runtime_dependency_declaration, compiler_declaration } end - base = [ - { - spdxElementId: "SPDXRef-File-#{name}", - relationshipType: "PACKAGE_OF", - relatedSpdxElement: "SPDXRef-Archive-#{name}-src", - }, - { + base = T.let([{ + spdxElementId: "SPDXRef-File-#{name}", + relationshipType: "PACKAGE_OF", + relatedSpdxElement: "SPDXRef-Archive-#{name}-src", + }], T::Array[Hash]) + + unless bottling + base << { spdxElementId: "SPDXRef-Compiler", relationshipType: "BUILD_TOOL_OF", relatedSpdxElement: "SPDXRef-Package-#{name}-src", - }, - ] - - if compiler_declaration["SPDXRef-Stdlib"].present? - base << { - spdxElementId: "SPDXRef-Stdlib", - relationshipType: "DEPENDENCY_OF", - relatedSpdxElement: "SPDXRef-Bottle-#{name}", } + + if compiler_declaration["SPDXRef-Stdlib"].present? + base << { + spdxElementId: "SPDXRef-Stdlib", + relationshipType: "DEPENDENCY_OF", + relatedSpdxElement: "SPDXRef-Bottle-#{name}", + } + end end runtime + patches + base @@ -214,13 +218,19 @@ def generate_relations_json(runtime_dependency_declaration, compiler_declaration sig { params(runtime_dependency_declaration: T::Array[Hash], - compiler_declaration: Hash).returns(T::Array[T::Hash[Symbol, - T.any(String, - T::Array[T::Hash[Symbol, String]])]]) + compiler_declaration: Hash, + bottling: T::Boolean).returns( + T::Array[ + T::Hash[ + Symbol, + T.any(String, T::Array[T::Hash[Symbol, String]]) + ], + ], + ) } - def generate_packages_json(runtime_dependency_declaration, compiler_declaration) + def generate_packages_json(runtime_dependency_declaration, compiler_declaration, bottling:) bottle = [] - if (bottle_info = get_bottle_info(source[:bottle])) + if !bottling && (bottle_info = get_bottle_info(source[:bottle])) bottle << { SPDXID: "SPDXRef-Bottle-#{name}", name: name.to_s, @@ -247,6 +257,12 @@ def generate_packages_json(runtime_dependency_declaration, compiler_declaration) } end + compiler_declarations = if bottling + [] + else + compiler_declaration.values + end + [ { SPDXID: "SPDXRef-Archive-#{name}-src", @@ -266,7 +282,7 @@ def generate_packages_json(runtime_dependency_declaration, compiler_declaration) }, ], }, - ] + runtime_dependency_declaration + compiler_declaration.values + bottle + ] + runtime_dependency_declaration + compiler_declarations + bottle end sig { returns(T::Array[T::Hash[Symbol, T.any(T::Boolean, String, T::Array[T::Hash[Symbol, String]])]]) } @@ -308,8 +324,8 @@ def full_spdx_runtime_dependencies end end - sig { returns(T::Hash[Symbol, T.any(String, T::Array[T::Hash[Symbol, String]])]) } - def to_spdx_sbom + sig { params(bottling: T::Boolean).returns(T::Hash[Symbol, T.any(String, T::Array[T::Hash[Symbol, String]])]) } + def to_spdx_sbom(bottling:) runtime_full = full_spdx_runtime_dependencies compiler_info = { @@ -342,13 +358,13 @@ def to_spdx_sbom } end - packages = generate_packages_json(runtime_full, compiler_info) + packages = generate_packages_json(runtime_full, compiler_info, bottling:) { SPDXID: "SPDXRef-DOCUMENT", spdxVersion: "SPDX-2.3", name: "SBOM-SPDX-#{name}-#{stable_version}", creationInfo: { - created: DateTime.now.to_s, + created: (Time.at(time).utc if time.present? && !bottling), creators: ["Tool: https://github.com/homebrew/brew@#{homebrew_version}"], }, dataLicense: "CC0-1.0", @@ -356,7 +372,7 @@ def to_spdx_sbom documentDescribes: packages.map { |dependency| dependency[:SPDXID] }, files: [], packages:, - relationships: generate_relations_json(runtime_full, compiler_info), + relationships: generate_relations_json(runtime_full, compiler_info, bottling:), } end @@ -388,7 +404,7 @@ def stable_version sig { returns(Time) } def source_modified_time - Time.at(@source_modified_time || 0).utc + Time.at(@source_modified_time).utc end sig { params(val: T.untyped).returns(T.any(String, Symbol)) }