diff --git a/Library/Homebrew/dev-cmd/verify.rb b/Library/Homebrew/dev-cmd/verify.rb new file mode 100644 index 0000000000..7e989ec9fb --- /dev/null +++ b/Library/Homebrew/dev-cmd/verify.rb @@ -0,0 +1,85 @@ +# typed: strict +# frozen_string_literal: true + +require "abstract_command" +require "attestation" + +module Homebrew + module DevCmd + class Verify < AbstractCommand + cmd_args do + description <<~EOS + Verify the build provenance of bottles using GitHub's attestation tools. + This is done by first fetching the given bottles and then verifying + their provenance. + + Note that this command depends on the GitHub CLI. Run `brew install gh`. + EOS + flag "--os=", + description: "Download for the given operating system." \ + "(Pass `all` to download for all operating systems.)" + flag "--arch=", + description: "Download for the given CPU architecture." \ + "(Pass `all` to download for all architectures.)" + flag "--bottle-tag=", + description: "Download a bottle for given tag." + switch "--deps", + description: "Also download dependencies for any listed ." + switch "-f", "--force", + description: "Remove a previously cached version and re-fetch." + switch "-j", "--json", + description: "Return JSON for the attestation data for each bottle." + conflicts "--os", "--bottle-tag" + conflicts "--arch", "--bottle-tag" + named_args [:formula], min: 1 + end + + sig { override.void } + def run + bucket = if args.deps? + args.named.to_formulae.flat_map do |formula| + [formula, *formula.recursive_dependencies.map(&:to_formula)] + end + else + args.named.to_formulae + end.uniq + + os_arch_combinations = args.os_arch_combinations + json_results = [] + bucket.each do |formula| + os_arch_combinations.each do |os, arch| + SimulateSystem.with(os:, arch:) do + bottle_tag = if (bottle_tag = args.bottle_tag&.to_sym) + Utils::Bottles::Tag.from_symbol(bottle_tag) + else + Utils::Bottles::Tag.new(system: os, arch:) + end + + bottle = formula.bottle_for_tag(bottle_tag) + + if bottle + bottle.clear_cache if args.force? + bottle.fetch + begin + attestation = Homebrew::Attestation.check_core_attestation bottle + oh1 "#{bottle.filename} has a valid attestation" + json_results.push(attestation) + rescue Homebrew::Attestation::InvalidAttestationError => e + ofail <<~ERR + Failed to verify #{bottle.filename} with tag #{bottle_tag} due to error: + + #{e} + ERR + end + else + opoo "Bottle for tag #{bottle_tag.to_sym.inspect} is unavailable." + end + end + end + end + + puts json_results.to_json if args.json? + end + end + end +end diff --git a/Library/Homebrew/sorbet/rbi/dsl/homebrew/dev_cmd/verify.rbi b/Library/Homebrew/sorbet/rbi/dsl/homebrew/dev_cmd/verify.rbi new file mode 100644 index 0000000000..f3d6d9cad9 --- /dev/null +++ b/Library/Homebrew/sorbet/rbi/dsl/homebrew/dev_cmd/verify.rbi @@ -0,0 +1,37 @@ +# typed: true + +# DO NOT EDIT MANUALLY +# This is an autogenerated file for dynamic methods in `Homebrew::DevCmd::Verify`. +# Please instead update this file by running `bin/tapioca dsl Homebrew::DevCmd::Verify`. + + +class Homebrew::DevCmd::Verify + sig { returns(Homebrew::DevCmd::Verify::Args) } + def args; end +end + +class Homebrew::DevCmd::Verify::Args < Homebrew::CLI::Args + sig { returns(T.nilable(String)) } + def arch; end + + sig { returns(T.nilable(String)) } + def bottle_tag; end + + sig { returns(T::Boolean) } + def deps?; end + + sig { returns(T::Boolean) } + def f?; end + + sig { returns(T::Boolean) } + def force?; end + + sig { returns(T::Boolean) } + def j?; end + + sig { returns(T::Boolean) } + def json?; end + + sig { returns(T.nilable(String)) } + def os; end +end diff --git a/Library/Homebrew/test/dev-cmd/verify_spec.rb b/Library/Homebrew/test/dev-cmd/verify_spec.rb new file mode 100644 index 0000000000..4dbf72cc81 --- /dev/null +++ b/Library/Homebrew/test/dev-cmd/verify_spec.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +require "cmd/shared_examples/args_parse" +require "dev-cmd/verify" + +RSpec.describe Homebrew::DevCmd::Verify do + it_behaves_like "parseable arguments" +end