mirror of
https://github.com/Homebrew/brew.git
synced 2026-10-28 09:34:42 +08:00
vulns/match: return :never_affected when Homebrew skipped the interval
first_fixed_version now distinguishes three outcomes for a currently-
:fixed hit: the pkg_version at the :fixed -> :affected boundary (or at
the last loadable revision, best-effort); :never_affected when the walk
reaches :not_applicable or the start of the formula's history without
seeing :affected (Homebrew jumped from below introduced straight past
fixed and never shipped an affected build); and nil when the current
aggregate is not :fixed. dev-cmd/advisory-match drops :never_affected
candidates instead of emitting {introduced: 0, fixed: <first-shipped>}.
This commit is contained in:
3 files changed
+55
-11
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: <first>}`.
|
||||
# - 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)) }
|
||||
|
||||
Reference in new issue
Block a user