From 0e3db6ebf6a9df5ea7c2ced203b3bbff41fbc04e Mon Sep 17 00:00:00 2001 From: Sean Molenaar Date: Mon, 5 Feb 2024 17:42:27 +0100 Subject: [PATCH] feat: add generated SPDX file on bottling --- Library/Homebrew/dev-cmd/bottle.rb | 1 + Library/Homebrew/tab.rb | 190 +++++++++++++++++++++++++++-- 2 files changed, 181 insertions(+), 10 deletions(-) diff --git a/Library/Homebrew/dev-cmd/bottle.rb b/Library/Homebrew/dev-cmd/bottle.rb index 0204380b8fa15..3582b5b47fcc6 100644 --- a/Library/Homebrew/dev-cmd/bottle.rb +++ b/Library/Homebrew/dev-cmd/bottle.rb @@ -437,6 +437,7 @@ def self.bottle_formula(formula, args:) tab.tabfile.unlink else tab.write + tab.write_sbom end keg.consistent_reproducible_symlink_permissions! diff --git a/Library/Homebrew/tab.rb b/Library/Homebrew/tab.rb index 44225a085bc4e..620d5b3e801df 100644 --- a/Library/Homebrew/tab.rb +++ b/Library/Homebrew/tab.rb @@ -12,10 +12,11 @@ class Tab extend Cachable FILENAME = "INSTALL_RECEIPT.json" + SPDX_FILENAME = "spdx.sbom.json" attr_accessor :homebrew_version, :tabfile, :built_as_bottle, :installed_as_dependency, :installed_on_request, :changed_files, :poured_from_bottle, :loaded_from_api, :time, :stdlib, :aliases, :arch, :source, - :built_on + :built_on, :license attr_writer :used_options, :unused_options, :compiler, :runtime_dependencies, :source_modified_time # Instantiates a {Tab} for a new installation of a formula. @@ -39,12 +40,17 @@ def self.create(formula, compiler, stdlib) "aliases" => formula.aliases, "runtime_dependencies" => Tab.runtime_deps_hash(formula, runtime_deps), "arch" => Hardware::CPU.arch, + "license" => SPDX.license_expression_to_string(formula.license), "source" => { - "path" => formula.specified_path.to_s, - "tap" => formula.tap&.name, - "tap_git_head" => nil, # Filled in later if possible - "spec" => formula.active_spec_sym.to_s, - "versions" => { + "path" => formula.specified_path.to_s, + "tap" => formula.tap&.name, + "tap_git_head" => nil, # Filled in later if possible + "spec" => formula.active_spec_sym.to_s, + "stable_url" => formula.stable&.url, + "stable_checksum" => formula.stable&.checksum, + "patches" => formula.stable&.patches, + "bottle" => formula.bottle_hash["files"], + "versions" => { "stable" => formula.stable&.version&.to_s, "head" => formula.head&.version&.to_s, "version_scheme" => formula.version_scheme, @@ -172,10 +178,14 @@ def self.for_formula(formula) tab = empty tab.unused_options = formula.options.as_flags tab.source = { - "path" => formula.specified_path.to_s, - "tap" => formula.tap&.name, - "spec" => formula.active_spec_sym.to_s, - "versions" => { + "path" => formula.specified_path.to_s, + "tap" => formula.tap&.name, + "spec" => formula.active_spec_sym.to_s, + "stable_url" => formula.stable&.url, + "stable_checksum" => formula.stable&.checksum, + "patches" => formula.stable&.patches, + "bottle" => formula.bottle_hash["files"], + "versions" => { "stable" => formula.stable&.version&.to_s, "head" => formula.head&.version&.to_s, "version_scheme" => formula.version_scheme, @@ -203,6 +213,7 @@ def self.empty "aliases" => [], "runtime_dependencies" => nil, "arch" => nil, + "license" => nil, "source" => { "path" => nil, "tap" => nil, @@ -223,12 +234,14 @@ def self.empty def self.runtime_deps_hash(formula, deps) deps.map do |dep| f = dep.to_formula + odebug SPDX.license_expression_to_string(f.license) { "full_name" => f.full_name, "version" => f.version.to_s, "revision" => f.revision, "pkg_version" => f.pkg_version.to_s, "declared_directly" => formula.deps.include?(dep), + "license" => SPDX.license_expression_to_string(f.license), } end end @@ -389,6 +402,16 @@ def write tabfile.atomic_write(to_json) end + def write_sbom + spdxfile = Pathname(tabfile.to_s.sub!(FILENAME, SPDX_FILENAME)) + # 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 + spdxfile.atomic_write(JSON.pretty_generate(to_spdx_sbom)) + end + sig { returns(String) } def to_s s = [] @@ -407,4 +430,151 @@ def to_s end s.join(" ") end + + sig { returns(Hash) } + def to_spdx_sbom + name = tabfile.dirname.dirname.basename + runtime_full = runtime_dependencies.map do |dependency| + { + "SPDXID" => "SPDXRef-Package-SPDXRef-#{dependency["full_name"].split("/").last}-#{dependency["version"]}", + "name" => dependency["full_name"].split("/").last, + "versionInfo" => dependency["pkg_version"], + "filesAnalyzed" => false, + "licenseDeclared" => "NOASSERTION", + "licenseConcluded" => dependency["license"] || "NOASSERTION", + "downloadLocation" => "NOASSERTION", + "copyrightText" => "NOASSERTION", + "checksums" => [], + "externalRefs" => [ + { + "referenceCategory" => "PACKAGE-MANAGER", + "referenceLocator" => "pkg:brew/#{dependency["full_name"]}@#{dependency["version"]}", + "referenceType" => "purl", + }, + ], + } + end + + compiler_info = { + "SPDXRef-Compiler" => { + "SPDXID" => "SPDXRef-Compiler", + "name" => compiler, + "versionInfo" => built_on["xcode"], + "filesAnalyzed" => false, + "licenseDeclared" => "NOASSERTION", + "licenseConcluded" => "NOASSERTION", + "copyrightText" => "NOASSERTION", + "checksums" => [], + "externalRefs" => [], + }, + } + + if stdlib.present? + compiler_info["SPDXRef-Stdlib"] = { + "SPDXID" => "SPDXRef-Stdlib", + "name" => stdlib, + "versionInfo" => stdlib, + "filesAnalyzed" => false, + "licenseDeclared" => "NOASSERTION", + "licenseConcluded" => "NOASSERTION", + "copyrightText" => "NOASSERTION", + "checksums" => [], + "externalRefs" => [], + } + end + + { + "SPDXID" => "SPDXRef-DOCUMENT", + "spdxVersion" => "SPDX-2.3", + "name" => "SBOM-SPDX-#{name}-#{stable_version}", + "creationInfo" => { + "created" => DateTime.now.to_s, + "creators" => ["Tool: https://github.com/homebrew/brew@q#{homebrew_version}"], + }, + "dataLicense" => "CC0-1.0", + "documentNamespace" => "https://formulae.brew.sh/spdx/#{name}-#{stable_version}.json", + "documentDescribes" => + runtime_full.map { |dependency| dependency["SPDXID"] } + + compiler_info.keys + + [ + "SPDXRef-Package-#{name}", + "SPDXRef-Archive-#{name}-src", + ], + "packages" => [ + { + "SPDXID" => "SPDXRef-Bottle-#{name}", + "name" => name.to_s, + "versionInfo" => stable_version.to_s, + "filesAnalyzed" => false, + "licenseDeclared" => "NOASSERTION", + "builtDate" => source_modified_time, + "licenseConcluded" => license, + "downloadLocation" => source["bottle"][Utils::Bottles.tag.to_s]["url"], + "copyrightText" => "NOASSERTION", + "externalRefs" => [ + { + "referenceCategory" => "PACKAGE-MANAGER", + "referenceLocator" => "pkg:brew/#{tap}/#{name}@#{stable_version}", + "referenceType" => "purl", + }, + ], + "checksums" => [ + { + "algorithm" => "SHA256", + "checksumValue" => source["bottle"][Utils::Bottles.tag.to_s]["sha256"], + }, + ], + }, + { + "SPDXID" => "SPDXRef-Archive-#{name}-src", + "name" => name.to_s, + "versionInfo" => stable_version.to_s, + "filesAnalyzed" => false, + "licenseDeclared" => "NOASSERTION", + "builtDate" => source_modified_time, + "licenseConcluded" => license || "NOASSERTION", + "downloadLocation" => source["stable_url"], + "copyrightText" => "NOASSERTION", + "externalRefs" => [], + "checksums" => [ + { + "algorithm" => "SHA256", + "checksumValue" => source["stable_checksum"], + }, + ], + }, + ] + runtime_full + compiler_info.values, + files: [], + "relationships" => runtime_full.map { |dependency| + { + "spdxElementId" => dependency["SPDXID"], + "relationshipType" => "RUNTIME_DEPENDENCY_OF", + "relatedSpdxElement" => "SPDXRef-Bottle-#{name}", + } + } + [ + { + "spdxElementId" => "SPDXRef-File-#{name}", + "relationshipType" => "PACKAGE_OF", + "relatedSpdxElement" => "SPDXRef-Archive-#{name}-src", + }, + { + "spdxElementId" => "SPDXRef-Compiler", + "relationshipType" => "BUILD_TOOL_OF", + "relatedSpdxElement" => "SPDXRef-Package-#{name}-src", + }, + (if compiler_info["SPDXRef-Stdlib"].present? + { + "spdxElementId" => "SPDXRef-Stdlib", + "relationshipType" => "DEPENDENCY_OF", + "relatedSpdxElement" => "SPDXRef-Bottle-#{name}", + } + end), + { + "spdxElementId" => "SPDXRef-Patch-#{name}", + "relationshipType" => "PATCH_APPLIED", + "relatedSpdxElement" => "SPDXRef-Archive-#{name}-src", + }, + ], + } + end end