Class: Brew::Vulns::Formula

Inherits:
Object
  • Object
show all
Defined in:
lib/brew/vulns/formula.rb

Constant Summary collapse

CYCLONEDX_PATCH_TYPES =
{
  "backport"    => "backport",
  "cherry_pick" => "cherry-pick",
  "unofficial"  => "unofficial",
}.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(data) ⇒ Formula

Returns a new instance of Formula.



11
12
13
14
15
16
17
18
# File 'lib/brew/vulns/formula.rb', line 11

def initialize(data)
  @name = data["name"] || data["full_name"]
  @version = data.dig("versions", "stable") || data["version"]
  @source_url = data.dig("urls", "stable", "url")
  @head_url = data.dig("urls", "head", "url")
  @dependencies = data["dependencies"] || []
  @patches = data["patches"] || []
end

Instance Attribute Details

#dependenciesObject (readonly)

Returns the value of attribute dependencies.



9
10
11
# File 'lib/brew/vulns/formula.rb', line 9

def dependencies
  @dependencies
end

#head_urlObject (readonly)

Returns the value of attribute head_url.



9
10
11
# File 'lib/brew/vulns/formula.rb', line 9

def head_url
  @head_url
end

#nameObject (readonly)

Returns the value of attribute name.



9
10
11
# File 'lib/brew/vulns/formula.rb', line 9

def name
  @name
end

#patchesObject (readonly)

Returns the value of attribute patches.



9
10
11
# File 'lib/brew/vulns/formula.rb', line 9

def patches
  @patches
end

#source_urlObject (readonly)

Returns the value of attribute source_url.



9
10
11
# File 'lib/brew/vulns/formula.rb', line 9

def source_url
  @source_url
end

#versionObject (readonly)

Returns the value of attribute version.



9
10
11
# File 'lib/brew/vulns/formula.rb', line 9

def version
  @version
end

Class Method Details

.load_allObject

Raises:



103
104
105
106
107
108
109
# File 'lib/brew/vulns/formula.rb', line 103

def self.load_all
  json, status = Open3.capture2("brew", "info", "--json=v2", "--eval-all")
  raise Error, "brew info failed with status #{status.exitstatus}" unless status.success?

  data = JSON.parse(json)
  data["formulae"].map { |f| new(f) }
end

.load_from_brewfile(brewfile_path, include_deps: false) ⇒ Object

Raises:



149
150
151
152
153
154
# File 'lib/brew/vulns/formula.rb', line 149

def self.load_from_brewfile(brewfile_path, include_deps: false)
  raise Error, "Brewfile not found: #{brewfile_path}" unless File.exist?(brewfile_path)

  formula_names = parse_brewfile(brewfile_path)
  load_named(formula_names, include_deps: include_deps)
end

.load_installedObject

Raises:



111
112
113
114
115
116
117
# File 'lib/brew/vulns/formula.rb', line 111

def self.load_installed
  json, status = Open3.capture2("brew", "info", "--json=v2", "--installed")
  raise Error, "brew info failed with status #{status.exitstatus}" unless status.success?

  data = JSON.parse(json)
  data["formulae"].map { |f| new(f) }
end

.load_named(formula_names, include_deps: false) ⇒ Object

Raises:



119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/brew/vulns/formula.rb', line 119

def self.load_named(formula_names, include_deps: false)
  return [] if formula_names.empty?

  json, status = Open3.capture2("brew", "info", "--json=v2", *formula_names)
  raise Error, "brew info failed with status #{status.exitstatus}" unless status.success?

  data = JSON.parse(json)
  formulae = data["formulae"].map { |f| new(f) }

  if include_deps
    dep_names = []
    formula_names.each do |name|
      deps_output, = Open3.capture2("brew", "deps", name)
      dep_names.concat(deps_output.split("\n").map(&:strip))
    end
    dep_names.uniq!
    dep_names -= formula_names

    if dep_names.any?
      deps_json, deps_status = Open3.capture2("brew", "info", "--json=v2", *dep_names)
      if deps_status.success?
        deps_data = JSON.parse(deps_json)
        formulae.concat(deps_data["formulae"].map { |f| new(f) })
      end
    end
  end

  formulae.uniq { |f| f.name }
end

.parse_brewfile(brewfile_path) ⇒ Object

Raises:



156
157
158
159
160
161
# File 'lib/brew/vulns/formula.rb', line 156

def self.parse_brewfile(brewfile_path)
  output, status = Open3.capture2("brew", "bundle", "list", "--file=#{brewfile_path}", "--formula")
  raise Error, "brew bundle list failed with status #{status.exitstatus}" unless status.success?

  output.split("\n").map(&:strip).reject(&:empty?)
end

Instance Method Details

#codeberg?Boolean

Returns:

  • (Boolean)


89
90
91
# File 'lib/brew/vulns/formula.rb', line 89

def codeberg?
  repo_url&.include?("codeberg.org")
end

#cyclonedx_pedigreeObject

Maps brew info --json=v2 patch data to a CycloneDX pedigree hash (symbol-keyed for the sbom gem). Returns nil when there are no patches.



49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/brew/vulns/formula.rb', line 49

def cyclonedx_pedigree
  return nil if patches.empty?

  cdx_patches = patches.map do |p|
    patch = { type: CYCLONEDX_PATCH_TYPES.fetch(p["type"].to_s, "unofficial") }
    patch[:diff] = { url: p["url"] } if p["url"]

    resolves = Array(p["resolves"]).filter_map do |r|
      next unless r.is_a?(Hash) && r["type"] && r["id"]

      { type: r["type"], id: r["id"] }
    end
    patch[:resolves] = resolves if resolves.any?

    patch
  end

  { patches: cdx_patches }
end

#github?Boolean

Returns:

  • (Boolean)


81
82
83
# File 'lib/brew/vulns/formula.rb', line 81

def github?
  repo_url&.include?("github.com")
end

#gitlab?Boolean

Returns:

  • (Boolean)


85
86
87
# File 'lib/brew/vulns/formula.rb', line 85

def gitlab?
  repo_url&.include?("gitlab.com")
end

#repo_urlObject



69
70
71
72
73
# File 'lib/brew/vulns/formula.rb', line 69

def repo_url
  return @repo_url if defined?(@repo_url)

  @repo_url = extract_repo_url(source_url) || extract_repo_url(head_url)
end

#resolved_vulnerability_idsObject

CVE/GHSA identifiers declared as resolved by this formula's patches. Populated from brew info --json=v2 patches[].resolves[] (Homebrew >= 6.0.4); empty on older Homebrew versions or for formulae with no annotated patches.



23
24
25
26
27
28
29
30
31
32
# File 'lib/brew/vulns/formula.rb', line 23

def resolved_vulnerability_ids
  return @resolved_vulnerability_ids if defined?(@resolved_vulnerability_ids)

  @resolved_vulnerability_ids = patches
    .flat_map { |p| Array(p["resolves"]) }
    .select { |r| r.is_a?(Hash) && r["type"] == "security" }
    .map { |r| r["id"].to_s.upcase }
    .reject(&:empty?)
    .uniq
end

#resolves?(vulnerability) ⇒ Boolean

Returns:

  • (Boolean)


34
35
36
37
38
39
# File 'lib/brew/vulns/formula.rb', line 34

def resolves?(vulnerability)
  ids = resolved_vulnerability_ids
  return false if ids.empty?

  vulnerability.identifiers.any? { |id| ids.include?(id.to_s.upcase) }
end

#supported_forge?Boolean

Returns:

  • (Boolean)


93
94
95
# File 'lib/brew/vulns/formula.rb', line 93

def supported_forge?
  github? || gitlab? || codeberg?
end

#tagObject



75
76
77
78
79
# File 'lib/brew/vulns/formula.rb', line 75

def tag
  return @tag if defined?(@tag)

  @tag = extract_tag_from_url(source_url)
end

#to_osv_queryObject



97
98
99
100
101
# File 'lib/brew/vulns/formula.rb', line 97

def to_osv_query
  return nil unless repo_url && tag

  { repo_url: repo_url, version: tag, name: name }
end