Skip to content

Commit

Permalink
Merge pull request #17284 from Homebrew/sbom_fixes
Browse files Browse the repository at this point in the history
SBOM: more fixes.
  • Loading branch information
MikeMcQuaid authored May 13, 2024
2 parents 4167900 + 7461bf8 commit b6d7230
Show file tree
Hide file tree
Showing 3 changed files with 60 additions and 45 deletions.
3 changes: 1 addition & 2 deletions Library/Homebrew/dev-cmd/bottle.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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!

Expand Down
4 changes: 2 additions & 2 deletions Library/Homebrew/formula_installer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
98 changes: 57 additions & 41 deletions Library/Homebrew/sbom.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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,
Expand Down Expand Up @@ -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"]
Expand All @@ -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:"
Expand All @@ -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
Expand All @@ -171,15 +167,22 @@ 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],
relationshipType: "RUNTIME_DEPENDENCY_OF",
relatedSpdxElement: "SPDXRef-Bottle-#{name}",
}
end

patches = source[:patches].each_with_index.map do |_patch, index|
{
spdxElementId: "SPDXRef-Patch-#{name}-#{index}",
Expand All @@ -188,39 +191,46 @@ 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
end

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,
Expand All @@ -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",
Expand All @@ -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]])]]) }
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -342,21 +358,21 @@ 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",
documentNamespace: "https://formulae.brew.sh/spdx/#{name}-#{stable_version}.json",
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

Expand Down Expand Up @@ -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)) }
Expand Down

0 comments on commit b6d7230

Please sign in to comment.