diff --git a/Library/Homebrew/dev-cmd/advisory-match.rb b/Library/Homebrew/dev-cmd/advisory-match.rb index f7a31c1f84..4d8facf30b 100644 --- a/Library/Homebrew/dev-cmd/advisory-match.rb +++ b/Library/Homebrew/dev-cmd/advisory-match.rb @@ -61,7 +61,10 @@ module Homebrew next if status&.state == :not_applicable first_fixed = matcher.first_fixed_version(formula, hit) unless args.no_history? - emitter << matcher.to_brew_record(formula, hit, first_fixed:) + next if first_fixed == :never_affected + + boundary = first_fixed if first_fixed.is_a?(String) + emitter << matcher.to_brew_record(formula, hit, first_fixed: boundary) end end rescue Homebrew::Vulns::OSV::Error => e diff --git a/Library/Homebrew/test/vulns/match_spec.rb b/Library/Homebrew/test/vulns/match_spec.rb index db20b93fe5..c7e79fb6b7 100644 --- a/Library/Homebrew/test/vulns/match_spec.rb +++ b/Library/Homebrew/test/vulns/match_spec.rb @@ -689,6 +689,31 @@ RSpec.describe Homebrew::Vulns::Match do expect(matcher.first_fixed_version(requests, hit)).to eq "2.1" end + it "returns :never_affected when Homebrew jumped from below introduced straight past fixed" do + # Advisory {introduced: 2.0, fixed: 3.0}; Homebrew went 1.0 -> 4.0 and + # never shipped a 2.x, so no BREW record should be emitted. + stub_history(["4.0", "1.0"]) + current = formula("requests") do + T.bind(self, T.class_of(Formula)) + url "https://files.pythonhosted.org/packages/aa/bb/cc/requests-4.0.tar.gz" + end + hit = make_hit( + vuln("id" => "CVE-1", "affected" => [ + { "package" => { "ecosystem" => "PyPI", "name" => "requests" }, + "ranges" => [{ "type" => "ECOSYSTEM", + "events" => [{ "introduced" => "2.0" }, { "fixed" => "3.0" }] }] }, + ]), + ev(:registry, ecosystem: "PyPI", name: "requests", subject_version: "4.0"), + ) + + expect(matcher.first_fixed_version(current, hit)).to eq :never_affected + end + + it "returns :never_affected when the formula was already past fixed at its first revision" do + stub_history(["2.31.0"]) + expect(matcher.first_fixed_version(requests, hit_fixed_at("2.28.1"))).to eq :never_affected + end + it "keeps versionless (distro) evidence uncheckable at historical revisions too" do stub_history(["2.31.0", "2.30.0", "2.28.1", "2.28.0"]) # A distro record whose Debian range would spuriously match our formula diff --git a/Library/Homebrew/vulns/match.rb b/Library/Homebrew/vulns/match.rb index 5930d60cd1..0b64434599 100644 --- a/Library/Homebrew/vulns/match.rb +++ b/Library/Homebrew/vulns/match.rb @@ -565,10 +565,21 @@ module Homebrew # per-evidence range check with each revision's subject versions keeps # `last_affected` and exclusive-bound semantics intact and stops as soon # as any subject (primary or a resource) drops back into `:affected`, so - # a primary fixed at 2.0 with a resource fixed at 3.0 yields 3.0. Returns - # nil when the current aggregate is not `:fixed`. The rev-list and - # per-revision loads are cached per formula. - sig { params(formula: Formula, hit: Hit).returns(T.nilable(String)) } + # a primary fixed at 2.0 with a resource fixed at 3.0 yields 3.0. + # + # Returns: + # - `nil` when the current aggregate is not `:fixed`. + # - `:never_affected` when the walk reaches `:not_applicable` (or the + # start of the formula's history) without ever seeing `:affected`, + # i.e. Homebrew jumped from a version below `introduced` straight past + # `fixed` and never shipped an affected build. The caller drops the + # candidate rather than emitting `{introduced: "0", fixed: }`. + # - a `pkg_version` String when the walk hits `:affected`, or when it + # stops at an unloadable revision (best-effort boundary; the reviewer + # can tighten). + # + # The rev-list and per-revision loads are cached per formula. + sig { params(formula: Formula, hit: Hit).returns(T.nilable(T.any(String, Symbol))) } def first_fixed_version(formula, hit) return unless range_status(hit)&.first&.fixed? @@ -576,16 +587,21 @@ module Homebrew revs = @formula_rev_lists[formula.name] ||= [].tap { |a| fv.rev_list("HEAD") { |rev, entry| a << [rev, entry] } } - last_fixed = T.let(formula.pkg_version.to_s, T.nilable(String)) + last_fixed = T.let(formula.pkg_version.to_s, String) revs.each do |rev, entry| - old_fixed = fv.formula_at_revision(rev, entry) do |old| - old.pkg_version.to_s if aggregate_state_at(old, hit) == :fixed + state = fv.formula_at_revision(rev, entry) do |old| + [aggregate_state_at(old, hit), old.pkg_version.to_s] end - return last_fixed if old_fixed.nil? + # `nil` means the revision failed to load; can't verify further. + return last_fixed if state.nil? - last_fixed = old_fixed + aggregate, pkg_version = state + return :never_affected if aggregate == :not_applicable + return last_fixed if aggregate != :fixed + + last_fixed = pkg_version end - last_fixed + :never_affected end sig { params(formula: Formula, hit: Hit).returns(T.nilable(Symbol)) }