Class: Homebrew::Vulns::Match Private

Inherits:
Object
  • Object
show all
Includes:
Utils::Output::Mixin
Defined in:
vulns/match.rb

Overview

This class is part of a private API. This class may only be used in the Homebrew/brew repository. Third parties should avoid using this class if possible, as it may be removed or changed without warning.

Authoring-time advisory matcher. For a given Formula it derives every OSV.dev query key it can (forge repository, language-registry package for the primary URL and each resource, distro source packages via Repology, CPAN distribution via CPANSec), issues versionless queries against each, resolves distro advisories to their upstream CVEs, and evaluates each hit's affected range against the version we ship.

Runs in Homebrew/advisory-database CI and the homebrew-core PR bot to produce candidate BREW-* records for human review; never on a user's machine, so request volume is traded for recall and every candidate carries a strategy/confidence label for the reviewer.

Defined Under Namespace

Classes: Evidence, HistoricalObservation, Hit, Identity, Interval, ReconciledHistory, ResourceRecord

Constant Summary collapse

STRATEGY_PRECISION =

This constant is part of a private API. This constant may only be used in the Homebrew/brew repository. Third parties should avoid using this constant if possible, as it may be removed or changed without warning.

Descending precision. When several strategies reach the same CVE the highest is reported as the hit's primary strategy; the rest are kept as supporting evidence.

T.let(
  { git: 4, registry: 3, cpansa: 2, distro: 1 }.freeze,
  T::Hash[Symbol, Integer],
)
CONFIDENCE =

This constant is part of a private API. This constant may only be used in the Homebrew/brew repository. Third parties should avoid using this constant if possible, as it may be removed or changed without warning.

Recorded in database_specific.confidence for the reviewer.

T.let(
  { git: "high", registry: "high", cpansa: "medium", distro: "low" }.freeze,
  T::Hash[Symbol, String],
)

Instance Method Summary collapse

Methods included from Utils::Output::Mixin

#issue_reporting_message, #odebug, #odeprecated, #odie, #odisabled, #ofail, #oh1, #oh1_title, #ohai, #ohai_title, #onoe, #opoo, #opoo_once, #opoo_outside_github_actions, #opoo_without_github_actions_annotation, #pretty_cannot_install, #pretty_deprecated, #pretty_disabled, #pretty_duration, #pretty_install_status, #pretty_installed, #pretty_removed, #pretty_uninstalled, #pretty_unmarked, #pretty_upgradable, #pretty_warning

Constructor Details

#initialize(repology: nil, cpan_sec: nil, overrides: nil, bulk: false, strict_upstream: false) ⇒ void

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Parameters:

  • repology (Repology, nil) (defaults to: nil)
  • cpan_sec (CPANSec, nil) (defaults to: nil)
  • overrides (AdvisoryOverrides, nil) (defaults to: nil)
  • bulk (Boolean) (defaults to: false)
  • strict_upstream (Boolean) (defaults to: false)


225
226
227
228
229
230
231
232
233
234
235
236
237
238
# File 'vulns/match.rb', line 225

def initialize(repology: nil, cpan_sec: nil, overrides: nil, bulk: false, strict_upstream: false)
  @repology = repology
  @cpan_sec = cpan_sec
  @overrides = overrides
  @bulk = bulk
  @strict_upstream = strict_upstream
  @vuln_cache = T.let({}, T::Hash[String, T.nilable(Vulnerability)])
  @vuln_errors = T.let({}, T::Hash[String, OSV::Error])
  @history = T.let(History.new, History)
  @reconciliation_observations = T.let(
    {}.compare_by_identity,
    T::Hash[Formula, T::Hash[[T::Array[String], T::Array[Evidence]], HistoricalObservation]],
  )
end

Instance Method Details

#advisories_for(formula) ⇒ Array<Hit>

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Returns one Hit per distinct vulnerability reached by any strategy, grouped by its connected id/alias family. Distro-ecosystem records are resolved to their upstream CVE(s), so multi-CVE advisories split into per-CVE hits and collapse onto the same CVE reached via GIT/registry. All queries are versionless so historic bump-fixed advisories are returned; #range_status evaluates each hit against the shipped version.

Parameters:

Returns:



303
304
305
306
307
# File 'vulns/match.rb', line 303

def advisories_for(formula)
  result = T.let([], T::Array[Hit])
  each_advisory_batch([formula]) { |_, hits| result = hits }
  result
end

#affected_entry(formula, hit, events, fixed, status, status_evidence) ⇒ Hash{Symbol => T.untyped}

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Parameters:

Returns:



880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
# File 'vulns/match.rb', line 880

def affected_entry(formula, hit, events, fixed, status, status_evidence)
  eco = T.let({ fix: fixed ? "bump" : nil }, T::Hash[Symbol, T.nilable(String)])
  eco[:range_state] = status.state.to_s if status
  eco[:upstream_fixed_in] = status.fixed_in if status&.fixed_in
  # Attribute the resource whose subject decided the state, falling back
  # to the highest-precision evidence when nothing was comparable.
  if (resource = status_evidence&.resource || hit.resource)
    eco[:resource] = resource
    eco[:resource_purl] = (status_evidence if status_evidence&.resource)&.key ||
                          hit.evidence.find { |e| e.resource == resource }&.key
  end
  {
    package:            {
      ecosystem: OsvExport::ECOSYSTEM,
      name:      formula.name,
      purl:      OsvExport.purl(formula.name),
    },
    ranges:             [{ type: "ECOSYSTEM", events: }],
    ecosystem_specific: eco,
  }
end

#aggregate_state_at(formula, hit) ⇒ Symbol?

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Parameters:

Returns:



1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
# File 'vulns/match.rb', line 1160

def aggregate_state_at(formula, hit)
  # Evidence built without a subject_version (distro queries, own-
  # identity rows for a formula with no derivable tag) is deliberately
  # uncheckable and must stay that way at historical revisions too;
  # substituting the historical formula version would compare it
  # against the distro record's distro-versioned range.
  subjects = hit.evidence.reject { |ev| ev.subject_version.nil? }.group_by(&:resource)
  results = subjects.flat_map do |_, evidence|
    states = evidence.map do |ev|
      present, subject = subject_version_at(formula, ev)
      # Absence means this formula revision did not ship the vulnerable
      # package. Treat it as fixed for boundary walking so a temporary
      # removal can be the fix boundary while still allowing the walk to
      # find an older affected revision.
      next :absent unless present
      next :unknown if subject.nil?
      next :prerelease_boundary if evidence_prerelease_boundary?(ev, subject)

      evidence_range_status(ev, subject)&.state || :unknown
    end
    # As in `range_status`, a source that cannot be compared (such as a
    # commit-only GIT range) does not hide a comparison of the same
    # subject from another source. Other subjects still fail closed.
    states -= [:unknown] if states.intersect?([:affected, :fixed, :not_applicable])
    states.map { |state| (state == :absent) ? :fixed : state }
  end
  return if results.empty? || results.include?(:prerelease_boundary)
  return :affected if results.include?(:affected)
  return if results.include?(:unknown)
  return :fixed if results.include?(:fixed)

  :not_applicable
end

#build_osv_queries(identity, formula_version) ⇒ Array<Array<(OSV::Package, Evidence)>>

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Parameters:

Returns:



423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
# File 'vulns/match.rb', line 423

def build_osv_queries(identity, formula_version)
  queries = T.let([], T::Array[[OSV::Package, Evidence]])

  if (repo = identity.git_repo)
    queries << [{ ecosystem: "GIT", name: repo, version: nil },
                Evidence.new(strategy: :git, ecosystem: "GIT", name: repo,
                             subject_version: identity.git_tag || formula_version,
                             key: repo).freeze]
  end

  if (pkg = identity.primary_package) && pkg.ecosystem != "CPAN"
    queries << [{ ecosystem: pkg.ecosystem, name: pkg.name, version: nil },
                Evidence.new(strategy: :registry, ecosystem: pkg.ecosystem, name: pkg.name,
                             subject_version: pkg.version, key: pkg.purl).freeze]
  end

  identity.resource_packages.each do |resource, pkg|
    next if pkg.ecosystem == "CPAN"

    queries << [{ ecosystem: pkg.ecosystem, name: pkg.name, version: nil },
                Evidence.new(strategy: :registry, ecosystem: pkg.ecosystem, name: pkg.name,
                             subject_version: pkg.version, key: pkg.purl, resource:).freeze]
  end

  identity.distro_packages.each do |ecosystem, srcnames|
    srcnames.each do |srcname|
      queries << [{ ecosystem:, name: srcname, version: nil },
                  Evidence.new(strategy: :distro, ecosystem:, name: srcname,
                               key: "#{ecosystem}/#{srcname}").freeze]
    end
  end

  queries
end

#confidence_for(hit, status) ⇒ String

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Parameters:

Returns:



866
867
868
869
870
871
872
# File 'vulns/match.rb', line 866

def confidence_for(hit, status)
  base = CONFIDENCE.fetch(hit.strategy)
  return base if status

  # No comparable range: the reviewer must set the boundary by hand.
  (base == "high") ? "medium" : "low"
end

#corrected_vulnerability(id, formula_name:) ⇒ Vulnerability?

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Parameters:

Returns:



646
647
648
649
650
651
652
# File 'vulns/match.rb', line 646

def corrected_vulnerability(id, formula_name:)
  record = fetch_vulnerability(id)
  excluded = @overrides&.excluded_aliases(formula_name, id) if formula_name
  return record if record.nil? || excluded.blank?

  record.excluding_aliases(excluded)
end

#cpan_evidence(identity) ⇒ Array<Evidence>

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Parameters:

Returns:



495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
# File 'vulns/match.rb', line 495

def cpan_evidence(identity)
  result = T.let([], T::Array[Evidence])
  primary = identity.primary_package
  if primary&.ecosystem == "CPAN"
    result << Evidence.new(strategy: :cpansa, ecosystem: "CPAN", name: primary.name,
                           subject_version: primary.version, key: primary.purl)
  end
  identity.resource_packages.each do |resource, pkg|
    next if pkg.ecosystem != "CPAN"

    result << Evidence.new(strategy: :cpansa, ecosystem: "CPAN", name: pkg.name,
                           subject_version: pkg.version, key: pkg.purl, resource:)
  end
  result
end

#cpan_sec ⇒ CPANSec

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Returns:



249
250
251
# File 'vulns/match.rb', line 249

def cpan_sec
  @cpan_sec ||= CPANSec.load
end

#cpansa_vulnerability(adv, id:) ⇒ Vulnerability

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Synthesise a Vulnerability for a CPANSA advisory when OSV has no record. id is scoped to the single CVE (or CPANSA id) being handled so a multi-CVE advisory whose CVEs are absent from OSV yields distinct records instead of collapsing under the lowest CVE in dedup.

Parameters:

Returns:



410
411
412
413
414
415
416
417
418
# File 'vulns/match.rb', line 410

def cpansa_vulnerability(adv, id:)
  summary = adv.description.to_s.lines.first&.strip
  Vulnerability.new({
    "id"         => id,
    "summary"    => summary,
    "details"    => adv.description,
    "references" => adv.references.map { |u| { "type" => "WEB", "url" => u } },
  }.compact)
end

#current_prerelease_boundary?(hit) ⇒ Boolean

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Reuse the same ambiguity rule as historical observations for every current subject, including evidence retained from other aliases.

Parameters:

Returns:

  • (Boolean)


759
760
761
# File 'vulns/match.rb', line 759

def current_prerelease_boundary?(hit)
  hit.evidence.any? { |ev| evidence_prerelease_boundary?(ev, ev.subject_version) }
end

#dedup_by_aliases(hits) ⇒ Array<Hit>

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Parameters:

Returns:



676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
# File 'vulns/match.rb', line 676

def dedup_by_aliases(hits)
  groups = T.let([], T::Array[T::Array[Hit]])
  pending = hits.dup
  until pending.empty?
    first = pending.shift
    raise ArgumentError, "Cannot start an empty alias group" if first.nil?

    # Evidence provenance can legitimately lead to several vulnerabilities,
    # so only the vulnerability's own identifiers connect hits.
    identifiers = T.let({}, T::Hash[String, T::Boolean])
    first.vulnerability.identifiers.each { |identifier| identifiers[identifier] = true }
    group = T.let([first], T::Array[Hit])
    loop do
      connected, remaining = pending.partition do |hit|
        hit.vulnerability.identifiers.any? { |identifier| identifiers.key?(identifier) }
      end
      break if connected.empty?

      connected.each do |hit|
        hit.vulnerability.identifiers.each { |identifier| identifiers[identifier] = true }
      end
      group.concat(connected)
      pending = remaining
    end
    groups << group
  end

  groups.map do |group|
    next group.fetch(0) if group.one?

    # Members of one alias family usually share a `canonical_id`, so the
    # record id is what actually settles the order for most groups. Rank
    # once so the merged evidence order is fixed too, not just the primary.
    ranked = group.sort_by { |h| [-STRATEGY_PRECISION.fetch(h.strategy), h.canonical_id, h.vulnerability.id] }
    Hit.new(vulnerability: ranked.fetch(0).vulnerability,
            evidence:      ranked.flat_map(&:evidence).uniq)
  end
end

#distro_packages_for(name) ⇒ Repology::DistroMap

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Bulk mode (the --all sweep) trusts the published index; only a single-formula run (the PR bot, or an explicit named check) may hit the live Repology API for a formula the index doesn't yet cover.

Parameters:

Returns:



607
608
609
610
611
612
613
614
615
# File 'vulns/match.rb', line 607

def distro_packages_for(name)
  indexed = repology.distro_packages_for(name)
  return indexed if indexed.any? || @bulk

  Repology.lookup(name)
rescue CachedFeed::Error => e
  odebug "Repology lookup for #{name} failed: #{e.message}"
  {}
end

#each_advisory_batch(formulae, on_error: nil, &_blk) ⇒ void

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

This method returns an undefined value.

Bulk form of #advisories_for: builds the labelled queries for a chunk of formulae at once, sends them through a single OSV.query_batch (which itself slices at BATCH_SIZE), then yields (formula, hits) in input order. Per-formula query counts vary widely (one distro entry per ecosystem×srcname), so chunking bounds memory without accumulating the whole tap's queries or records; the @vuln_cache still spans chunks.

Parameters:



323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
# File 'vulns/match.rb', line 323

def each_advisory_batch(formulae, on_error: nil, &_blk)
  e = T.let(nil, T.nilable(OSV::Error))
  formulae.each_slice(BULK_CHUNK) do |chunk|
    identities = T.let({}, T::Hash[Formula, Identity])
    chunk.each do |formula|
      next if skip_formula?(formula.name)

      identities[formula] = identify(formula)
    end
    labelled = T.let([], T::Array[[OSV::Package, [Formula, Evidence]]])
    identities.each do |f, identity|
      next unless identity.identifiable?

      build_osv_queries(identity, f.version.to_s).each do |query, evidence|
        labelled << [query, [f, evidence]]
      end
    end

    by_formula = T.let({}, T::Hash[Formula, T::Hash[String, T::Array[Evidence]]])
    if labelled.any?
      begin
        responses = OSV.query_batch(labelled.map(&:first))
      rescue OSV::Error => e
        raise unless on_error

        chunk.each { |formula| on_error.call(formula, e) }
        next
      end
      responses.each_with_index do |stubs, i|
        formula, evidence = labelled.fetch(i).last
        id_evidence = by_formula[formula] ||= {}
        stubs.each { |stub| (id_evidence[stub.fetch("id")] ||= []) << evidence }
      end
    end

    prefetch_vulnerabilities(by_formula.each_value.flat_map(&:keys))

    chunk.each do |f|
      identity = identities[f]
      next yield f, [] unless identity&.identifiable?

      begin
        hits = hits_from(by_formula[f] || {}, identity, formula_name: f.name)
      rescue OSV::Error => e
        raise unless on_error

        on_error.call(f, e)
        next
      end
      yield f, hits
    end
  end
end

#evidence_prerelease_boundary?(evidence, version) ⇒ Boolean

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

CPANSA uses its own constraints rather than the attached OSV ranges.

Parameters:

Returns:

  • (Boolean)


765
766
767
768
769
# File 'vulns/match.rb', line 765

def evidence_prerelease_boundary?(evidence, version)
  return false if evidence.strategy == :cpansa || version.nil?

  evidence.source_record&.prerelease_boundary?(evidence.ecosystem, evidence.name, version) || false
end

#evidence_range_status(evidence, subject_version) ⇒ Vulnerability::RangeStatus?

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Parameters:

Returns:



775
776
777
778
779
780
781
782
783
784
# File 'vulns/match.rb', line 775

def evidence_range_status(evidence, subject_version)
  return if subject_version.nil?

  if evidence.strategy == :cpansa
    adv = evidence.advisory
    CPANSec.range_status(adv, subject_version) if adv
  else
    evidence.source_record&.range_status(evidence.ecosystem, evidence.name, subject_version)
  end
end

#fetch_vulnerability(id) ⇒ Vulnerability?

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Parameters:

Returns:



655
656
657
658
659
660
661
662
663
# File 'vulns/match.rb', line 655

def fetch_vulnerability(id)
  error = @vuln_errors[id]
  raise error if error

  @vuln_cache.fetch(id) { @vuln_cache[id] = load_vulnerability(id) }
rescue OSV::Error => e
  @vuln_errors[id] = e
  raise
end

#first_fixed_version(formula, hit) ⇒ String, ...

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Walk homebrew-core git history (newest first) via History and return the pkg_version at the oldest revision where the aggregate of every checkable subject is still :fixed. Re-running the full 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.
  • :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>}.
  • :history_unavailable when the tap is a shallow clone, the formula has no git history or a revision cannot be loaded or compared, so the caller can skip the candidate rather than inventing a boundary. This includes a version that moved backwards, leaving no later fixed version above the affected one.
  • :shared_fixed_version when the current pkg_version also has an affected build, so no released version is fixed yet.
  • a pkg_version String when the walk hits :affected. A fix that changed a resource without a revision bump shares its version with affected builds, which the version cannot distinguish, so the boundary is the next version with only fixed builds.

Parameters:

Returns:



929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
# File 'vulns/match.rb', line 929

def first_fixed_version(formula, hit)
  return unless range_status(hit, formula_name: formula.name)&.first&.fixed?

  fixed_versions = T.let([formula.pkg_version], T::Array[PkgVersion])
  result = @history.walk(formula) do |old|
    aggregate = aggregate_state_at(old, hit)
    version = old.pkg_version
    case aggregate
    when :fixed
      fixed_versions << version if fixed_versions.last != version
      nil
    when :affected
      later = fixed_versions.take_while { |fixed| fixed != version }
      boundary = later.last
      if boundary.nil?
        :shared_fixed_version
      elsif boundary > version
        boundary.to_s
      else
        :history_unavailable
      end
    when :not_applicable then :never_affected
    when nil then :history_unavailable
    else raise TypeError, "unexpected historical aggregate: #{aggregate.inspect}"
    end
  end
  result || :never_affected
end

#first_introduced_version(formula, hit, first_fixed: nil) ⇒ String, Symbol

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Return the earliest affected pkg_version for a new record. Check all history: one interval must cover every affected version and no version with only non-affected builds. A resource change without a revision bump leaves the shared version affected if any of its builds were affected. Unreadable history and unrepresentable intervals require manual review.

Parameters:

Returns:



1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
# File 'vulns/match.rb', line 1083

def first_introduced_version(formula, hit, first_fixed: nil)
  current_state = aggregate_state_at(formula, hit)
  return :history_unavailable unless [:affected, :fixed].include?(current_state)

  affected_versions = T.let([], T::Array[PkgVersion])
  unaffected_versions = T.let([], T::Array[PkgVersion])
  ((current_state == :affected) ? affected_versions : unaffected_versions) << formula.pkg_version
  result = @history.walk(formula) do |old|
    case aggregate_state_at(old, hit)
    when :affected then affected_versions << old.pkg_version
    when :fixed, :not_applicable then unaffected_versions << old.pkg_version
    else next :history_unavailable
    end
    nil
  end
  return :history_unavailable unless result.nil?

  introduced = affected_versions.min
  return :history_unavailable unless introduced

  fixed = PkgVersion.parse(first_fixed) if first_fixed
  return :history_unavailable if fixed && affected_versions.any? { |version| version >= fixed }

  unaffected_versions -= affected_versions
  if unaffected_versions.any? { |version| version >= introduced && (!fixed || version < fixed) }
    return :history_unavailable
  end

  introduced.to_s
rescue ArgumentError
  :history_unavailable
end

#first_reintroduced_version(formula, hit) ⇒ String, ...

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Return the lowest representable formula pkg_version in the newest contiguous affected run. This is the introduced boundary when a reviewed fixed range becomes affected again after a formula or resource regression, including a run whose version strings move backwards after a version_scheme change. After finding the transition, the remaining history is checked so the new interval cannot cover a known non-affected formula version. :history_unavailable means a revision could not be loaded or compared, or the formula's history is missing or shallow. :not_reintroduced means no representable transition was found in the available history.

Parameters:

Returns:



1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
# File 'vulns/match.rb', line 1127

def first_reintroduced_version(formula, hit)
  return unless range_status(hit, formula_name: formula.name)&.first&.affected?

  first_affected = T.let(formula.pkg_version.to_s, String)
  transition_found = T.let(false, T::Boolean)
  result = @history.walk(formula) do |old|
    aggregate = aggregate_state_at(old, hit)
    next :history_unavailable if aggregate.nil?

    pkg_version = old.pkg_version.to_s
    begin
      historical = PkgVersion.parse(pkg_version)
      boundary = PkgVersion.parse(first_affected)
      if transition_found
        next :not_reintroduced if aggregate != :affected && historical >= boundary
      elsif aggregate == :affected
        first_affected = pkg_version if historical < boundary
      else
        next :not_reintroduced if historical >= boundary

        transition_found = true
      end
    rescue ArgumentError
      next :history_unavailable
    end
    nil
  end
  return result unless result.nil?

  transition_found ? first_affected : :not_reintroduced
end

#history_load_failures ⇒ Array<History::LoadFailure>

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.



241
# File 'vulns/match.rb', line 241

def history_load_failures = @history.load_failures

#hits_from(id_evidence, identity, formula_name: nil) ⇒ Array<Hit>

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Parameters:

Returns:



381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
# File 'vulns/match.rb', line 381

def hits_from(id_evidence, identity, formula_name: nil)
  hits = resolve_upstream(id_evidence, identity, formula_name:)
  cpan_evidence(identity).each do |ev|
    cpan_sec.advisories_for(ev.name).each do |adv|
      annotated = Evidence.new(**ev.to_h, advisory: adv).freeze
      if adv.cves.any?
        adv.cves.each do |cve|
          record = begin
            corrected_vulnerability(cve, formula_name:)
          rescue OSV::NotFoundError
            nil
          end
          record ||= cpansa_vulnerability(adv, id: cve)
          hits << Hit.new(vulnerability: record, evidence: [annotated])
        end
      else
        hits << Hit.new(vulnerability: cpansa_vulnerability(adv, id: adv.id.to_s),
                        evidence:      [annotated])
      end
    end
  end
  dedup_by_aliases(hits)
end

#identify(formula) ⇒ Identity

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Parameters:

Returns:



254
255
256
257
258
259
260
261
262
263
264
265
266
267
# File 'vulns/match.rb', line 254

def identify(formula)
  stable = formula.stable
  stable_url = stable&.url
  Identity.new(
    git_repo:          Identify.repo_url(stable_url, formula.head&.url, formula.homepage),
    git_tag:           Identify.tag(stable_url) || stable&.specs&.dig(:tag) || stable&.version&.to_s,
    primary_package:   primary_registry_package(formula),
    resource_packages: formula.resources.filter_map do |r|
      pkg = Identify.registry_package(r.url)
      [r.name, pkg] if pkg
    end.to_h.freeze,
    distro_packages:   distro_packages_for(formula.name),
  ).freeze
end

#load_vulnerability(id) ⇒ Vulnerability?

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Parameters:

Returns:



666
667
668
669
670
671
672
673
# File 'vulns/match.rb', line 666

def load_vulnerability(id)
  Vulnerability.new(OSV.vulnerability(id))
rescue OSV::Error => e
  raise if @strict_upstream

  odebug "OSV.vulnerability(#{id}) failed: #{e.message}"
  nil
end

#own_evidence(identity) ⇒ Array<Evidence>

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Evidence rows pointing at our own identity keys (git repo, primary registry package) with the formula/package version as subject. Attached to distro-resolved upstream hits so #range_status can evaluate the upstream CVE record's affected[] against something comparable.

Parameters:

Returns:



590
591
592
593
594
595
596
597
598
599
600
601
# File 'vulns/match.rb', line 590

def own_evidence(identity)
  result = T.let([], T::Array[Evidence])
  if (repo = identity.git_repo)
    result << Evidence.new(strategy: :distro, ecosystem: "GIT", name: repo,
                           subject_version: identity.git_tag, key: "upstream:#{repo}").freeze
  end
  if (pkg = identity.primary_package)
    result << Evidence.new(strategy: :distro, ecosystem: pkg.ecosystem, name: pkg.name,
                           subject_version: pkg.version, key: "upstream:#{pkg.purl}").freeze
  end
  result
end

#prefetch_vulnerabilities(ids) ⇒ void

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

This method returns an undefined value.

OSV querybatch returns id/modified stubs. Warm @vuln_cache with the full records for a chunk's stub ids before per-formula processing so #resolve_upstream reads mostly from cache.

Parameters:



624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
# File 'vulns/match.rb', line 624

def prefetch_vulnerabilities(ids)
  missing = ids.uniq.reject { |id| @vuln_cache.key?(id) || @vuln_errors.key?(id) }
  missing.each_slice(MAX_VULN_FETCH_THREADS) do |slice|
    threads = slice.map do |id|
      [id, Thread.new do
        load_vulnerability(id)
      rescue OSV::Error => e
        e
      end]
    end
    threads.each do |id, thread|
      result = thread.value
      if result.is_a?(OSV::Error)
        @vuln_errors[id] = result
      else
        @vuln_cache[id] = result
      end
    end
  end
end

#preserve_homebrew_ranges?(formula_name, identifiers) ⇒ Boolean

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Parameters:

Returns:

  • (Boolean)


485
486
487
# File 'vulns/match.rb', line 485

def preserve_homebrew_ranges?(formula_name, identifiers)
  !!@overrides&.preserve_homebrew_ranges?(formula_name, identifiers)
end

#primary_registry_package(formula, strict: true) ⇒ Identify::RegistryPackage?

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Return the formula's source-derived registry package, unless a reviewed override supplies an identity for formulae built from non-registry URLs. Historical conflicts are uncheckable so one stale override cannot abort matching every formula.

Parameters:

  • formula (Formula)
  • strict (Boolean) (defaults to: true)

Returns:



276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
# File 'vulns/match.rb', line 276

def primary_registry_package(formula, strict: true)
  derived = Identify.registry_package(formula.stable&.url)
  override = @overrides&.registry_package_override(formula.name)
  return derived unless override

  if derived
    if derived.ecosystem != override.ecosystem || derived.name != override.name
      raise AdvisoryOverrides::Error, "#{formula.name}.registry_package conflicts with its source URL" if strict

      return
    end
    return derived
  end

  version = formula.stable&.version&.to_s
  return if version.nil?

  Identify.registry_package_for(ecosystem: override.ecosystem, name: override.name, version:)
end

#range_status(hit, formula_name: nil) ⇒ Array<(Vulnerability::RangeStatus, Evidence)>?

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Evaluate hit against every evidence's subject, each against the record that evidence was matched against, and aggregate: :affected if any subject is affected (a fixed primary must not hide an affected resource, or vice versa), else unknown if any subject is unresolved, else :fixed if any is fixed, else :not_applicable only when every comparable subject says so. Returns [status, evidence] where evidence is the one whose result was chosen (used by #first_fixed_version and for the emitted record's resource attribution), or nil if no evidence produced a checkable answer. A current prerelease ambiguity needs an explicit reviewed state.

Parameters:

  • hit (Hit)
  • formula_name (String, nil) (defaults to: nil)

Returns:



729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
# File 'vulns/match.rb', line 729

def range_status(hit, formula_name: nil)
  override = @overrides&.advisory_override(formula_name, hit.identifiers) if formula_name
  return if !override&.state && current_prerelease_boundary?(hit)

  subjects = hit.evidence.reject { |ev| ev.strategy == :distro && ev.subject_version.nil? }
                .group_by(&:resource).map do |_, evidence|
    results = evidence.filter_map do |ev|
      status = evidence_range_status(ev, ev.subject_version)
      [status, ev] if status
    end
    results.find { |s, _| s.affected? } || results.find { |s, _| s.fixed? } || results.first
  end
  results = subjects.compact
  selected = results.find { |s, _| s.affected? }
  selected ||= results.find { |s, _| s.fixed? } || results.first unless subjects.include?(nil)
  return selected unless override

  status, evidence = selected ||
                     (results.find { |candidate, _| candidate.state == override.state } if override.state) ||
                     [nil, hit.primary_evidence]
  state = override.state || status&.state
  return selected unless state

  fixed_in = override.fixed_in_overridden ? override.fixed_in : status&.fixed_in
  [Vulnerability::RangeStatus.new(state:, fixed_in:).freeze, evidence]
end

#reconcile_history(formula, hit, require_resource_presence: false, reuse_platforms: false) ⇒ ReconciledHistory

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Reconstruct every representable affected interval from complete history. Callers can emit new candidates or reconcile matched records; generated patch-fix records must retain their annotation-based ranges. Unlike first_fixed_version, this must visit builds before the first transition, including below-introduced versions and removed resources. An unresolved or protected result never supplies replacement boundaries.

Parameters:

  • formula (Formula)
  • hit (Hit)
  • require_resource_presence (Boolean) (defaults to: false)
  • reuse_platforms (Boolean) (defaults to: false)

Returns:



968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
# File 'vulns/match.rb', line 968

def reconcile_history(formula, hit, require_resource_presence: false, reuse_platforms: false)
  if preserve_homebrew_ranges?(formula.name, hit.identifiers)
    return ReconciledHistory.new(state: :preserved, reasons: [])
  end

  unseen = require_resource_presence ? hit.evidence.select(&:resource).dup : []
  # Matching metadata alone does not identify an observation: constraints
  # and the alias family (including patch annotations) must also agree.
  observation_key = [hit.identifiers, hit.evidence]
  observe = lambda do |build|
    unseen.reject! do |ev|
      present, version = subject_version_at(build, ev)
      present && !version.nil?
    end
    if reuse_platforms && @history.platform_independent?(build)
      cached = @reconciliation_observations[build] ||= {}
      cached[observation_key] ||= reconciliation_observation(build, hit)
    else
      reconciliation_observation(build, hit)
    end
  end
  observations = [observe.call(formula)]
  result = @history.walk(formula, complete: true, reuse_platforms:) do |old|
    observations << observe.call(old)
    nil
  end
  reasons = observations.flat_map(&:reasons)
  reasons << :resource_not_found if unseen.any?
  reasons << :history_unavailable unless result.nil?
  if @overrides&.advisory_override(formula.name, hit.identifiers)
    reasons << :upstream_override_requires_review
  end

  intervals = T.let([], T::Array[Interval])
  observations.sort_by(&:version).chunk_while { |left, right| left.version == right.version }.each do |builds|
    version = builds.fetch(0).version
    previous = intervals.last
    if builds.any? { |build| build.state == :affected }
      intervals << Interval.new(introduced: version.to_s) if previous.nil? || previous.fixed
    elsif previous && previous.fixed.nil?
      intervals[-1] = previous.with(fixed: version.to_s)
    end
  end
  if intervals.last&.fixed.nil? && intervals.any? &&
     observations.none? { |row| row.version == formula.pkg_version && row.state == :affected }
    reasons << :unrepresentable_interval
  end

  if observations.any? do |row|
    row.patched && (intervals.empty? || intervals.any? do |interval|
      fixed = interval.fixed
      row.version >= PkgVersion.parse(interval.introduced) && (!fixed || row.version <= PkgVersion.parse(fixed))
    end)
  end
    reasons << :advisory_patch_requires_review
  end

  return ReconciledHistory.new(state: :unresolved, reasons: reasons.uniq.sort) if reasons.any?
  return ReconciledHistory.new(state: :never_affected, reasons: []) if intervals.empty?

  ReconciledHistory.new(state: :range, intervals:, reasons: [])
end

#reconciliation_observation(formula, hit) ⇒ HistoricalObservation

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Preserve disagreements within an upstream alias family and unknown subjects even when another subject is affected. Both prevent a history rewrite, although ordinary matching can still report the affected hit.

Parameters:

Returns:



1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
# File 'vulns/match.rb', line 1035

def reconciliation_observation(formula, hit)
  reasons = T.let([], T::Array[Symbol])
  subjects = hit.evidence.reject { |ev| ev.strategy == :distro && ev.subject_version.nil? }
                .group_by { |ev| [ev.ecosystem, ev.name, ev.resource] }.map do |_, evidence|
    states = evidence.map do |ev|
      next :unknown if ev.subject_version.nil?

      present, version = subject_version_at(formula, ev)
      next(ev.resource ? :fixed : :subject_changed) unless present
      next :prerelease_boundary if evidence_prerelease_boundary?(ev, version)

      evidence_range_status(ev, version)&.state || :unknown
    end.uniq
    reasons << :uncomparable_source if states.include?(:unknown)
    reasons << :subject_changed if states.include?(:subject_changed)
    reasons << :prerelease_boundary if states.include?(:prerelease_boundary)
    if states.include?(:affected) && states.intersect?([:fixed, :not_applicable])
      reasons << :conflicting_upstream
    end
    states
  end.flatten
  reasons << :uncomparable_source if subjects.empty?
  state = if subjects.include?(:affected)
    :affected
  elsif subjects.intersect?([:unknown, :subject_changed, :prerelease_boundary]) || subjects.empty?
    :unknown
  elsif subjects.include?(:fixed)
    :fixed
  else
    :not_applicable
  end

  resolved = Scanner.resolved_ids(formula.serialized_patches)
  formula.resources.each do |resource|
    resource.patches.each do |patch|
      resolved.concat(patch.resolves.map(&:upcase)) if patch.is_a?(ExternalPatch) || patch.is_a?(LocalPatch)
    end
  end
  HistoricalObservation.new(version: formula.pkg_version, state:,
                            patched: resolved.intersect?(hit.identifiers.map(&:upcase)), reasons:)
end

#record_id(formula, hit) ⇒ String

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Parameters:

Returns:



854
855
856
# File 'vulns/match.rb', line 854

def record_id(formula, hit)
  OsvExport.record_id(formula, hit.canonical_id)
end

#record_ids(formula, hit) ⇒ Array<String>

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Parameters:

Returns:



859
860
861
# File 'vulns/match.rb', line 859

def record_ids(formula, hit)
  ([record_id(formula, hit)] + hit.identifiers.map { |id| OsvExport.record_id(formula, id) }).uniq
end

#repology ⇒ Repology

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Returns:



244
245
246
# File 'vulns/match.rb', line 244

def repology
  @repology ||= Repology.load
end

#resolve_to_cves(record, seen, budget, formula_name: nil) ⇒ Array<Vulnerability>

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Returns the set of CVE records record derives from. [record] if it is one already; [] if the walk exhausts without reaching a CVE (the caller then keeps record itself as a low-confidence hit).

Parameters:

Returns:



567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
# File 'vulns/match.rb', line 567

def resolve_to_cves(record, seen, budget, formula_name: nil)
  return [record] if record.cve_ids.any?
  return [] if budget.zero?

  follow = record.upstream.presence
  follow ||= record.related.grep(CVE_ID) if record.id.start_with?(RELATED_AS_UPSTREAM_PREFIX)
  Array(follow).uniq.flat_map do |ref|
    next [] unless seen.add?(ref)

    upstream = corrected_vulnerability(ref, formula_name:)
    upstream ? resolve_to_cves(upstream, seen, budget - 1, formula_name:) : []
  rescue OSV::NotFoundError
    # A dangling upstream link supplies no record. Keep the cached error
    # so fetching this ID directly from query results still fails closed.
    []
  end.uniq(&:id)
end

#resolve_upstream(id_evidence, identity, formula_name: nil) ⇒ Array<Hit>

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Turn id => [Evidence, ...] into [Hit, ...], resolving each record to the CVE(s) it derives from. upstream is walked transitively with a per-walk visited set (chains like USN -> UBUNTU-CVE-* -> CVE-* occur in practice). related links to different vulnerabilities per the OSV schema and is only consulted for its bare CVE ids when upstream is empty (AlmaLinux ALSA records use it that way). A record that is already a CVE by id or alias, or that reaches no CVE within the hop budget, is kept as-is. Each resolved hit gains synthesised evidence pointing at our own identity so #range_status can check the CVE record's affected[] against our version.

Parameters:

Returns:



531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
# File 'vulns/match.rb', line 531

def resolve_upstream(id_evidence, identity, formula_name: nil)
  own = own_evidence(identity)
  hits = T.let([], T::Array[Hit])

  id_evidence.each do |id, evidence|
    record = corrected_vulnerability(id, formula_name:)
    next if record.nil?

    resolved = resolve_to_cves(record, Set[id], MAX_UPSTREAM_HOPS, formula_name:)
    if resolved.empty?
      hits << Hit.new(vulnerability: record, evidence:)
      next
    end

    resolved.each do |cve_record|
      ev = cve_record.equal?(record) ? evidence : evidence + own
      hits << Hit.new(vulnerability: cve_record, evidence: ev)
    end
  end

  hits
end

#resource_advisories_for(records, formula_name: nil) ⇒ Array<Hit>

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Rediscover stored resource subjects using fresh versionless queries. Stored versions only reproduce the candidate's range basis. The history walk must establish every version and witness each resource's identity.

Parameters:

Returns:



462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
# File 'vulns/match.rb', line 462

def resource_advisories_for(records, formula_name: nil)
  evidence = records.flat_map(&:evidence).uniq
  return [] if evidence.empty?

  subjects = evidence.group_by { |ev| [ev.ecosystem, ev.name] }
  packages = subjects.keys.map { |ecosystem, name| { ecosystem:, name:, version: nil } }
  results = OSV.query_batch(packages)
  id_evidence = T.let({}, T::Hash[String, T::Array[Evidence]])
  subjects.values.zip(results).each do |rows, stubs|
    Array(stubs).each do |stub|
      id = stub["id"]
      next unless id.is_a?(String)

      (id_evidence[id] ||= []).concat(rows)
    end
  end
  prefetch_vulnerabilities(id_evidence.keys)
  identity = Identity.new(resource_packages: [], distro_packages: {})
  hits = dedup_by_aliases(resolve_upstream(id_evidence, identity, formula_name:))
  hits.select { |hit| records.any? { |record| record.upstream.intersect?(hit.identifiers) } }
end

#skip_formula?(formula_name) ⇒ Boolean

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Parameters:

Returns:

  • (Boolean)


490
491
492
# File 'vulns/match.rb', line 490

def skip_formula?(formula_name)
  !!@overrides&.skip_formula?(formula_name)
end

#subject_version_at(formula, evidence) ⇒ Array<(Boolean, [String, nil])>

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Resolve a historical subject by upstream package identity so resource label changes do not look like removals. The boolean distinguishes a package that was absent (not affected at that revision) from one that was present but had no comparable version (uncheckable).

Parameters:

Returns:



1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
# File 'vulns/match.rb', line 1199

def subject_version_at(formula, evidence)
  unless evidence.resource
    stable = formula.stable
    stable_url = stable&.url
    if evidence.ecosystem == "GIT"
      repo = Identify.repo_url(stable_url, formula.head&.url, formula.homepage)
      return [true, nil] if repo.nil?
      return [true, nil] if repo != evidence.name

      version = Identify.tag(stable_url) || stable&.specs&.dig(:tag) || stable&.version&.to_s
      return [true, version]
    end

    primary_package = primary_registry_package(formula, strict: false)
    return [true, nil] if primary_package.nil?
    if primary_package.ecosystem != evidence.ecosystem || primary_package.name != evidence.name
      return [false, nil]
    end

    return [true, primary_package.version]
  end

  exact_resource = formula.resources.find { |resource| resource.name == evidence.resource }
  exact_identity_unknown = T.let(false, T::Boolean)
  if exact_resource
    exact_package = Identify.registry_package(exact_resource.url)
    if exact_package.nil?
      # A reused resource label is not enough to prove package identity.
      # Search the remaining resources in case the package was renamed,
      # then leave this revision uncheckable if no identity can be found.
      exact_identity_unknown = true
    elsif exact_package.ecosystem == evidence.ecosystem && exact_package.name == evidence.name
      return [true, exact_package.version]
    end
  end

  formula.resources.each do |resource|
    next if resource.equal?(exact_resource)

    package = Identify.registry_package(resource.url)
    next if package.nil?
    next if package.ecosystem != evidence.ecosystem || package.name != evidence.name

    return [true, package.version]
  end

  return [true, nil] if exact_identity_unknown

  [false, nil]
end

#to_brew_record(formula, hit, first_fixed: nil, first_introduced: nil, history: nil, now: Time.now.utc) ⇒ Hash{Symbol => T.untyped}

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Emit a candidate BREW-* OSV record for hit against formula.

first_fixed is the PkgVersion at which Homebrew first shipped a fix (from #first_fixed_version or a hand-set value), and first_introduced is the first affected Homebrew version (from #first_introduced_version or #first_reintroduced_version). Otherwise #range_status is consulted: affected? == false sets fixed: pkg_version and ecosystem_specific.fix: "bump"; affected? == true (or no comparable range) emits no fixed event and fix: null. As with OsvExport.record_for, OsvExport.merge_existing preserves on-disk ranges on rewrite so a hand-corrected boundary sticks. With history, emit all reconstructed intervals and derive fix metadata from the last interval, including a final open interval.

Parameters:

  • formula (Formula)
  • hit (Hit)
  • first_fixed (String, nil) (defaults to: nil)
  • first_introduced (String, nil) (defaults to: nil)
  • history (ReconciledHistory, nil) (defaults to: nil)
  • now (Time) (defaults to: Time.now.utc)

Returns:



804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
# File 'vulns/match.rb', line 804

def to_brew_record(formula, hit, first_fixed: nil, first_introduced: nil, history: nil, now: Time.now.utc)
  vuln = hit.vulnerability
  timestamp = now.strftime("%Y-%m-%dT%H:%M:%SZ")
  status, status_evidence = range_status(hit, formula_name: formula.name)

  if history && (history.state != :range || history.intervals.empty? ||
                 history.intervals.any? { |interval| !interval.valid? })
    raise ArgumentError, "Candidate history must contain valid affected intervals"
  end

  fixed = history ? history.fixed : first_fixed
  fixed ||= formula.pkg_version.to_s if !history && status&.fixed?
  events = T.let(history ? history.events : [{ introduced: first_introduced || "0" }],
                 T::Array[T::Hash[Symbol, String]])
  events << { fixed: } if !history && fixed

  record = T.let({
    schema_version:    OsvExport::SCHEMA_VERSION,
    id:                record_id(formula, hit),
    published:         timestamp,
    modified:          timestamp,
    upstream:          hit.identifiers,
    affected:          [affected_entry(formula, hit, events, fixed, status, status_evidence)],
    database_specific: {
      source:            "matched",
      strategy:          hit.strategy.to_s,
      confidence:        confidence_for(hit, status),
      upstream_evidence: hit.evidence.map { |e| e.to_h.except(:advisory, :source_record).compact }.uniq,
    },
  }, T::Hash[Symbol, T.untyped])

  if history
    record.fetch(:affected).fetch(0).fetch(:ecosystem_specific)[:range_state] = fixed ? "fixed" : "affected"
  end

  if status.nil? && current_prerelease_boundary?(hit)
    record[:database_specific][:review_reason] = "prerelease_boundary"
  end

  record[:summary] = vuln.summary if vuln.summary
  record[:details] = vuln.details if vuln.details
  record[:severity] = vuln.severity_entries if vuln.severity_entries.any?
  if (refs = vuln.references).any?
    record[:references] = refs.uniq { |r| [r["type"], URI::RFC2396_PARSER.unescape(r["url"].to_s)] }
  end

  record
end