Module: Homebrew::Vulns::Identify Private

Defined in:
vulns/identify.rb

Overview

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

Derives OSV.dev query keys (forge repo URL, release tag) from formula source URLs. Shared between Scanner and the advisory-matching pipeline.

Defined Under Namespace

Classes: RegistryPackage

Class Method Summary collapse

Class Method Details

.decode(component) ⇒ 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.

Percent-decode a URL path segment. Unlike decode_www_form_component this leaves + alone and unlike decode_uri_component (missing from Sorbet's stdlib RBI) it never raises on malformed input.

Parameters:

Returns:



200
201
202
203
204
205
# File 'vulns/identify.rb', line 200

def self.decode(component)
  return component unless component.include?("%")

  component.b.gsub(/%[0-9A-Fa-f]{2}/) { |m| Integer(m[1, 2], 16).chr }
             .force_encoding(component.encoding)
end

.gem_name_version(basename) ⇒ Array<([String, nil], [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.

Split a .gem basename into name and version, discarding any trailing Gem::Platform suffix (e.g. nokogiri-1.16.0-arm64-darwin-22).

Parameters:

Returns:



219
220
221
222
223
224
225
# File 'vulns/identify.rb', line 219

def self.gem_name_version(basename)
  deplatformed = basename.sub(GEM_PLATFORM_SUFFIX, "")
  name, sep, version = deplatformed.rpartition("-")
  return [nil, nil] if sep.empty? || !version.match?(/\A\d[\w.]*\z/)

  [name, version]
end

.registry_package(url) ⇒ 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.

Parameters:

Returns:



118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'vulns/identify.rb', line 118

def self.registry_package(url)
  return if url.nil?

  ecosystem, purl = registry_purl(url)
  return if purl.nil?

  name = case purl.type
  when "maven" then "#{purl.namespace}:#{purl.name}"
  # OSV keys PyPI packages by their PEP 503 normalised name.
  when "pypi" then purl.name.gsub(/[-_.]+/, "-")
  # CPANSA is keyed on the distribution name alone, without the author.
  when "cpan" then purl.name
  else purl.namespace ? "#{purl.namespace}/#{purl.name}" : purl.name
  end
  RegistryPackage.new(ecosystem:, name:, version: purl.version, purl: purl.to_s).freeze
end

.registry_purl(url) ⇒ Array<(String, Purl)>?

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:



136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
# File 'vulns/identify.rb', line 136

def self.registry_purl(url)
  basename = decode(File.basename(url)).sub(ARCHIVE_EXTENSIONS, "")

  case url
  when %r{\Ahttps://files\.pythonhosted\.org/packages/(?:[^/]+/){3}(?![^/]+\.whl\z)}
    # PEP 440 canonical versions contain no hyphen, so the last one delimits.
    name, _, version = basename.rpartition("-")
    return if name.empty?

    ["PyPI", Purl.new(type: "pypi", name:, version:)]
  when %r{\Ahttps://registry\.npmjs\.org/(?:((?:@|%40)[^/]+)/)?([^/@%][^/]*)/-/}
    namespace = Regexp.last_match(1)
    name = T.must(Regexp.last_match(2))
    namespace &&= "@#{decode(namespace).delete_prefix("@")}"
    name = decode(name)
    return unless (version = version_after_prefix(basename, name))

    ["npm", Purl.new(type: "npm", namespace:, name:, version:)]
  when %r{\Ahttps://static\.crates\.io/crates/([^/]+)/}
    name = decode(T.must(Regexp.last_match(1)))
    return unless (version = version_after_prefix(basename, name))

    ["crates.io", Purl.new(type: "cargo", name:, version:)]
  when %r{\Ahttps://rubygems\.org/(?:downloads|gems)/}
    name, version = gem_name_version(basename)
    return if name.nil?

    ["RubyGems", Purl.new(type: "gem", name:, version:)]
  when %r{\Ahttps://hackage\.haskell\.org/package/([^/]+)}
    match = T.must(Regexp.last_match(1)).match(HACKAGE_PKGID)
    return if match.nil?

    ["Hackage", Purl.new(type: "hackage", name: T.must(match[1]), version: match[2])]
  when %r{\Ahttps://repo\.hex\.pm/tarballs/}
    # Hex package names are `[a-z][a-z0-9_]*` so the first hyphen delimits.
    name, sep, version = basename.partition("-")
    return if sep.empty?

    ["Hex", Purl.new(type: "hex", name:, version:)]
  when %r{/authors/id/[A-Z]/[A-Z]{2}/([A-Z][A-Z0-9-]+)/}
    author = T.must(Regexp.last_match(1))
    match = basename.match(CPAN_DISTNAME)
    return if match.nil?

    ["CPAN", Purl.new(type: "cpan", namespace: author, name: T.must(match[1]), version: match[2])]
  # Maven Central only: OSV's bare `Maven` ecosystem is Central-scoped,
  # so third-party repositories (Google, fabricmc, jfrog, ...) are skipped.
  when %r{\Ahttps://repo1?\.maven\.(?:apache\.)?org/maven2/(.+)/([^/]+)/([^/]+)/\2-\3[.-][^/]+\z},
       %r{\Ahttps://search\.maven\.org/remotecontent\?filepath=(.+)/([^/]+)/([^/]+)/\2-\3[.-][^/]+\z}
    group_id = T.must(Regexp.last_match(1)).tr("/", ".")
    artifact_id = T.must(Regexp.last_match(2))
    version = Regexp.last_match(3)
    ["Maven", Purl.new(type: "maven", namespace: group_id, name: artifact_id, version:)]
  when %r{\Ahttps://(?:cran|cloud)\.r-project\.org/src/contrib/(?:Archive/[^/]+/)?([^/_]+)_([^/]+)\.tar\.gz\z}
    ["CRAN", Purl.new(type: "cran", name: T.must(Regexp.last_match(1)), version: Regexp.last_match(2))]
  when %r{\Ahttps://(?:api|www)\.nuget\.org/(?:v3-flatcontainer|api/v2/package)/([^/]+)/([^/]+)(?:/|\z)}
    ["NuGet", Purl.new(type: "nuget", name: T.must(Regexp.last_match(1)), version: Regexp.last_match(2))]
  end
end

.repo_url(*urls) ⇒ 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:



58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'vulns/identify.rb', line 58

def self.repo_url(*urls)
  urls.each do |url|
    next if url.nil?

    url = url.sub(WAYBACK_PREFIX, "")
    FORGES.each do |host, path_pattern|
      match = url.match(%r{\Ahttps?://#{Regexp.escape(host)}#{path_pattern}})
      next if match.nil?

      repo_path = T.must(match[1]).sub(/\.git$/, "")
      repo_path = repo_path.downcase if LOWERCASE_PATH_HOSTS.include?(host)
      return "https://#{host}/#{repo_path}"
    end
  end
  nil
end

.tag(url) ⇒ 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:



76
77
78
79
80
81
82
83
84
# File 'vulns/identify.rb', line 76

def self.tag(url)
  return if url.nil?

  TAG_PATTERNS.each do |pattern|
    match = url.match(pattern)
    return match[1] if match
  end
  nil
end

.version_after_prefix(basename, name) ⇒ 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:



208
209
210
211
212
213
214
# File 'vulns/identify.rb', line 208

def self.version_after_prefix(basename, name)
  prefix = "#{name}-"
  return unless basename.start_with?(prefix)

  version = basename[prefix.length..]
  version.presence
end