Class: Brew::Vulns::Vulnerability

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

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(data) ⇒ Vulnerability

Returns a new instance of Vulnerability.



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

def initialize(data)
  @id = data["id"]
  @summary = data["summary"]
  @details = data["details"]
  @aliases = data["aliases"] || []
  @references = data["references"] || []
  @affected = data["affected"] || []
  @severity = extract_severity(data)
end

Instance Attribute Details

#affectedObject (readonly)

Returns the value of attribute affected.



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

def affected
  @affected
end

#aliasesObject (readonly)

Returns the value of attribute aliases.



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

def aliases
  @aliases
end

#detailsObject (readonly)

Returns the value of attribute details.



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

def details
  @details
end

#idObject (readonly)

Returns the value of attribute id.



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

def id
  @id
end

#referencesObject (readonly)

Returns the value of attribute references.



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

def references
  @references
end

#severityObject (readonly)

Returns the value of attribute severity.



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

def severity
  @severity
end

#summaryObject (readonly)

Returns the value of attribute summary.



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

def summary
  @summary
end

Class Method Details

.from_osv_list(vulns_data) ⇒ Object



73
74
75
# File 'lib/brew/vulns/vulnerability.rb', line 73

def self.from_osv_list(vulns_data)
  vulns_data.map { |data| new(data) }
end

Instance Method Details

#advisory_urlObject



39
40
41
42
# File 'lib/brew/vulns/vulnerability.rb', line 39

def advisory_url
  ref = references.find { |r| r["type"] == "ADVISORY" }
  ref&.dig("url")
end

#affects_version?(version, default_ecosystem = "gem") ⇒ Boolean

Returns:

  • (Boolean)


60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/brew/vulns/vulnerability.rb', line 60

def affects_version?(version, default_ecosystem = "gem")
  return true if affected.empty?

  normalized_version = normalize_version(version)

  affected.any? do |aff|
    ecosystem = extract_ecosystem(aff, default_ecosystem)

    in_explicit_versions?(aff, normalized_version) ||
      in_semver_ranges?(aff, normalized_version, ecosystem)
  end
end

#build_constraints(events) ⇒ Object



180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
# File 'lib/brew/vulns/vulnerability.rb', line 180

def build_constraints(events)
  constraints = []
  events.each do |event|
    if event["introduced"]
      intro = normalize_version(event["introduced"])
      constraints << ">=#{intro}" unless intro == "0"
    end
    if event["fixed"]
      constraints << "<#{normalize_version(event["fixed"])}"
    end
    if event["last_affected"]
      constraints << "<=#{normalize_version(event["last_affected"])}"
    end
  end
  constraints
end

#cve_idsObject



35
36
37
# File 'lib/brew/vulns/vulnerability.rb', line 35

def cve_ids
  ([id] + aliases).select { |a| a.start_with?("CVE-") }
end

#extract_ecosystem(aff, default_ecosystem) ⇒ Object



143
144
145
146
147
148
149
150
151
152
# File 'lib/brew/vulns/vulnerability.rb', line 143

def extract_ecosystem(aff, default_ecosystem)
  purl_str = aff.dig("package", "purl")
  return default_ecosystem unless purl_str

  purl = Purl.parse(purl_str)
  purl.type
rescue StandardError => e
  warn "Warning: Failed to parse purl '#{purl_str}': #{e.message}"
  default_ecosystem
end

#extract_severity(data) ⇒ Object



77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/brew/vulns/vulnerability.rb', line 77

def extract_severity(data)
  if data["severity"]&.any?
    sev = data["severity"].first
    if sev["score"]&.include?("CVSS")
      return severity_from_cvss(sev["score"])
    end
  end

  if data.dig("database_specific", "severity")
    return normalize_severity(data.dig("database_specific", "severity"))
  end

  data["affected"]&.each do |aff|
    db_sev = aff.dig("database_specific", "severity")
    return normalize_severity(db_sev) if db_sev
  end

  nil
end

#fix_urlsObject



44
45
46
# File 'lib/brew/vulns/vulnerability.rb', line 44

def fix_urls
  references.select { |r| r["type"] == "FIX" }.map { |r| r["url"] }
end

#fixed_versionsObject



48
49
50
51
52
53
54
55
56
57
58
# File 'lib/brew/vulns/vulnerability.rb', line 48

def fixed_versions
  versions = []
  affected.each do |aff|
    (aff["ranges"] || []).each do |range|
      (range["events"] || []).each do |event|
        versions << event["fixed"] if event["fixed"]
      end
    end
  end
  versions.uniq
end

#in_explicit_versions?(aff, version) ⇒ Boolean

Returns:

  • (Boolean)


154
155
156
157
# File 'lib/brew/vulns/vulnerability.rb', line 154

def in_explicit_versions?(aff, version)
  versions = aff["versions"] || []
  versions.any? { |v| normalize_version(v) == version }
end

#in_semver_ranges?(aff, version, ecosystem) ⇒ Boolean

Returns:

  • (Boolean)


159
160
161
162
163
164
165
166
# File 'lib/brew/vulns/vulnerability.rb', line 159

def in_semver_ranges?(aff, version, ecosystem)
  ranges = aff["ranges"] || []
  semver_ranges = ranges.select { |r| r["type"] == "SEMVER" }

  semver_ranges.any? do |range|
    version_in_range?(version, range["events"], ecosystem)
  end
end

#normalize_severity(severity) ⇒ Object



97
98
99
100
101
102
103
104
105
106
# File 'lib/brew/vulns/vulnerability.rb', line 97

def normalize_severity(severity)
  return nil unless severity

  case severity.downcase
  when "critical" then "critical"
  when "high" then "high"
  when "moderate", "medium" then "medium"
  when "low" then "low"
  end
end

#normalize_version(version) ⇒ Object



139
140
141
# File 'lib/brew/vulns/vulnerability.rb', line 139

def normalize_version(version)
  version.sub(/^v/, "")
end

#parse_cvss_metrics(vector) ⇒ Object



131
132
133
134
135
136
137
# File 'lib/brew/vulns/vulnerability.rb', line 131

def parse_cvss_metrics(vector)
  metrics = {}
  vector.scan(%r{([A-Z]+):([A-Z])}).each do |key, value|
    metrics[key] = value
  end
  metrics
end

#severity_displayObject



21
22
23
# File 'lib/brew/vulns/vulnerability.rb', line 21

def severity_display
  severity&.upcase || "UNKNOWN"
end

#severity_from_cvss(vector) ⇒ Object



108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# File 'lib/brew/vulns/vulnerability.rb', line 108

def severity_from_cvss(vector)
  return nil unless vector
  return nil unless vector.include?("CVSS:3")

  metrics = parse_cvss_metrics(vector)
  return nil if metrics.empty?

  impact_high = %w[C I A].count { |m| metrics[m] == "H" }
  network_attack = metrics["AV"] == "N"
  no_privs = metrics["PR"] == "N"
  no_interaction = metrics["UI"] == "N"

  if impact_high >= 2 && network_attack && no_privs
    "critical"
  elsif impact_high >= 1 && network_attack
    "high"
  elsif impact_high >= 1 || (network_attack && no_privs && no_interaction)
    "medium"
  else
    "low"
  end
end

#severity_levelObject



25
26
27
28
29
30
31
32
33
# File 'lib/brew/vulns/vulnerability.rb', line 25

def severity_level
  case severity&.downcase
  when "critical" then 4
  when "high" then 3
  when "medium" then 2
  when "low" then 1
  else 0
  end
end

#version_in_range?(version, events, ecosystem) ⇒ Boolean

Returns:

  • (Boolean)


168
169
170
171
172
173
174
175
176
177
178
# File 'lib/brew/vulns/vulnerability.rb', line 168

def version_in_range?(version, events, ecosystem)
  return false if events.nil? || events.empty?

  constraints = build_constraints(events)
  return false if constraints.empty?

  Vers.satisfies?(version, constraints.join(","), ecosystem)
rescue StandardError => e
  warn "Warning: Failed to check version '#{version}' against constraints: #{e.message}"
  false
end