Module: Brew::Vulns::OsvExport

Defined in:
lib/brew/vulns/osv_export.rb

Overview

Emits OSV-schema records for a Homebrew ecosystem describing CVEs that homebrew-core formulae resolve via shipped patches.

One record is written per (formula, vulnerability id) pair found in patches[].resolves. The record states that the formula was affected up to (but not including) the currently shipped version+revision; this is the "fixed at or before what we ship today" approximation, since the precise fix boundary requires homebrew-core git archaeology.

Record shape follows the OSV 1.7 schema and mirrors the Debian DSA layout (upstream listing the source CVE, affected[].ranges of type ECOSYSTEM, ecosystem_specific carrying the patch detail).

Constant Summary collapse

SCHEMA_VERSION =
"1.7.3"
ECOSYSTEM =
"Homebrew"
ID_PREFIX =
"BREW"

Class Method Summary collapse

Class Method Details

.affected_entry(formula, vuln_id) ⇒ Object



69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/brew/vulns/osv_export.rb', line 69

def affected_entry(formula, vuln_id)
  {
    package: {
      ecosystem: ECOSYSTEM,
      name:      formula.name,
      purl:      formula.purl(with_version: false),
    },
    ranges: [
      {
        type:   "ECOSYSTEM",
        events: [
          { introduced: "0" },
          { fixed: formula.full_version },
        ],
      },
    ],
    ecosystem_specific: {
      fix:     "patch",
      patches: formula.patches_resolving(vuln_id).filter_map { |p| patch_ref(p) },
    },
  }
end

.fetch_upstream(client, vuln_id) ⇒ Object



102
103
104
105
106
# File 'lib/brew/vulns/osv_export.rb', line 102

def fetch_upstream(client, vuln_id)
  client.get_vulnerability(vuln_id)
rescue OsvClient::Error
  nil
end

.patch_ref(patch) ⇒ Object



92
93
94
95
96
97
98
99
100
# File 'lib/brew/vulns/osv_export.rb', line 92

def patch_ref(patch)
  ref = {}
  ref[:type] = patch["type"] if patch["type"]
  ref[:url] = patch["url"] if patch["url"]
  ref[:file] = patch["file"] if patch["file"]
  ref[:apply] = patch["apply"] if patch["apply"]
  ref[:data] = true if patch["data"]
  ref.empty? ? nil : ref
end

.record_for(formula, vuln_id, upstream: nil, now: Time.now.utc) ⇒ Object



45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/brew/vulns/osv_export.rb', line 45

def record_for(formula, vuln_id, upstream: nil, now: Time.now.utc)
  record = {
    schema_version: SCHEMA_VERSION,
    id:             record_id(formula, vuln_id),
    modified:       now.strftime("%Y-%m-%dT%H:%M:%SZ"),
    upstream:       [vuln_id],
    affected:       [affected_entry(formula, vuln_id)],
  }

  if upstream
    record[:summary] = upstream["summary"] if upstream["summary"]
    record[:details] = upstream["details"] if upstream["details"]
    record[:severity] = upstream["severity"] if upstream["severity"]
    record[:upstream] = ([vuln_id] + Array(upstream["aliases"])).uniq
    record[:references] = upstream["references"] if upstream["references"]
  end

  record
end

.record_id(formula, vuln_id) ⇒ Object



65
66
67
# File 'lib/brew/vulns/osv_export.rb', line 65

def record_id(formula, vuln_id)
  "#{ID_PREFIX}-#{formula.name}-#{vuln_id}"
end

.run(formulae, dir, client: OsvClient.new, now: Time.now.utc) ⇒ Object



28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# File 'lib/brew/vulns/osv_export.rb', line 28

def run(formulae, dir, client: OsvClient.new, now: Time.now.utc)
  FileUtils.mkdir_p(dir)
  written = []

  formulae.each do |formula|
    formula.resolved_vulnerability_ids.each do |vuln_id|
      upstream = fetch_upstream(client, vuln_id)
      record = record_for(formula, vuln_id, upstream: upstream, now: now)
      path = File.join(dir, "#{record[:id]}.json")
      File.write(path, JSON.pretty_generate(record))
      written << path
    end
  end

  written
end