brew-vulns 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -1
- data/Formula/brew-vulns.rb +12 -2
- data/README.md +18 -5
- data/examples/pr-vuln-check.yml +1 -1
- data/examples/scan-all.yml +1 -1
- data/exe/brew-vulns +2 -0
- data/lib/brew/vulns/cli.rb +55 -6
- data/lib/brew/vulns/formula.rb +45 -19
- data/lib/brew/vulns/osv_client.rb +18 -0
- data/lib/brew/vulns/osv_export.rb +109 -0
- data/lib/brew/vulns/version.rb +8 -1
- data/lib/brew/vulns.rb +1 -0
- metadata +8 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a4db816ba6a05913a6c043982fa279cd27c20637ee999737323480519c504ce5
|
|
4
|
+
data.tar.gz: 47a3baef2449740c693d43f09e90dfe87d5f9b1ab3b596cffeb5f2afaa0c0371
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f11cd24893e2a5609b5a499932c9aa3d11d00e7579aeec82ad381945df16a2508a1b90b022f787a28959414f165d7e14e02c639ebcc593942151b14af6c6877b
|
|
7
|
+
data.tar.gz: 137ad2269080760fd54751bf662ec26283b4bc1054b0ed4ec683bb2a2aba88c194402a3e74b5f8a9911f6a47fe8ccb297bc1299d8c3ef6afa81ebd4178654b9a
|
data/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
## [
|
|
1
|
+
## [0.5.0] - 2026-07-14
|
|
2
|
+
|
|
3
|
+
- Deprecate in favour of the built-in `brew vulns` command shipped in Homebrew 6.0.11 (see [Homebrew/brew#23080](https://github.com/Homebrew/brew/pull/23080)). A deprecation notice is now printed to stderr on every run and after `gem install`. The formula is marked deprecated. This tap and gem will be archived after one release cycle.
|
|
4
|
+
- Query OSV for formulae whose source is not on GitHub/GitLab/Codeberg by falling back to the formula's `head` git URL, a `tag:`-style stable git URL, or a forge URL in `homepage`, with the bare formula version. Removes the forge whitelist; any formula with a derivable git repository URL is now sent in the batch query
|
|
5
|
+
- CycloneDX output: percent-encode `@` in `pkg:brew/` purls so versioned formula names like `glibc@2.13` are not misparsed as `name@version`
|
|
6
|
+
- Add `--osv-export DIR` (experimental) to write OSV-schema records under a `Homebrew` ecosystem for every CVE listed in a formula's `patches[].resolves`. Each record marks the formula as fixed at the currently shipped version+revision and carries the resolving patch detail in `ecosystem_specific`. Intended as the seed for a published Homebrew advisory feed.
|
|
7
|
+
- Retry the final OSV API attempt over IPv4 (via `Net::HTTP#ipaddr=`) when earlier attempts time out or fail to connect, to work around IPv6 paths that complete the TCP/TLS handshake but then drop data. Add `--ipv4` to force IPv4 from the first attempt.
|
|
2
8
|
|
|
3
9
|
## [0.4.0] - 2026-06-28
|
|
4
10
|
|
data/Formula/brew-vulns.rb
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
class BrewVulns < Formula
|
|
2
2
|
desc "Check Homebrew packages for known vulnerabilities via osv.dev"
|
|
3
3
|
homepage "https://github.com/Homebrew/homebrew-brew-vulns"
|
|
4
|
-
url "https://github.com/Homebrew/homebrew-brew-vulns/archive/refs/tags/v0.
|
|
5
|
-
sha256 "
|
|
4
|
+
url "https://github.com/Homebrew/homebrew-brew-vulns/archive/refs/tags/v0.5.0.tar.gz"
|
|
5
|
+
sha256 "f20b9dadb01d3ec8c4e97fe5e3225aac58ca609f935ce17928b23deabd415ba7"
|
|
6
6
|
license "MIT"
|
|
7
7
|
|
|
8
|
+
deprecate! date: "2026-07-14", because: "is now built into Homebrew as `brew vulns`"
|
|
9
|
+
|
|
8
10
|
depends_on "ruby"
|
|
9
11
|
|
|
10
12
|
def install
|
|
@@ -19,6 +21,14 @@ class BrewVulns < Formula
|
|
|
19
21
|
bin.env_script_all_files(libexec/"bin", GEM_HOME: ENV.fetch("GEM_HOME", nil))
|
|
20
22
|
end
|
|
21
23
|
|
|
24
|
+
def caveats
|
|
25
|
+
<<~EOS
|
|
26
|
+
`brew vulns` is now a built-in Homebrew command. Run `brew update` and
|
|
27
|
+
use `brew vulns` directly, then `brew uninstall brew-vulns` and
|
|
28
|
+
`brew untap homebrew/brew-vulns`.
|
|
29
|
+
EOS
|
|
30
|
+
end
|
|
31
|
+
|
|
22
32
|
test do
|
|
23
33
|
system bin/"brew-vulns", "--help"
|
|
24
34
|
end
|
data/README.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# brew-vulns
|
|
2
2
|
|
|
3
|
+
> [!WARNING]
|
|
4
|
+
> **Deprecated.** `brew vulns` is now a built-in Homebrew command as of Homebrew 6.0.11. Run `brew update` and use `brew vulns` directly; this tap and gem are no longer needed and will be archived. See [Homebrew/brew#23080](https://github.com/Homebrew/brew/pull/23080).
|
|
5
|
+
>
|
|
6
|
+
> Interface changes in the built-in command:
|
|
7
|
+
>
|
|
8
|
+
> - `--all` is dropped; with no arguments the built-in command scans every trusted formula (or just installed formulae if tap trust is disabled)
|
|
9
|
+
> - `-b` short flag is dropped; use `--brewfile`
|
|
10
|
+
> - `--sarif`, `--cyclonedx` and `--ipv4` are not available (SARIF and CycloneDX are planned as follow-ups)
|
|
11
|
+
> - `--osv-export` moved to `brew generate-vulns-advisories` ([Homebrew/brew#23106](https://github.com/Homebrew/brew/pull/23106)); its output lives at [Homebrew/advisory-database](https://github.com/Homebrew/advisory-database)
|
|
12
|
+
> - Errors no longer exit with code `2`; brew's standard error handling applies. Exit `0` (clean) and `1` (vulnerabilities found) are unchanged.
|
|
13
|
+
>
|
|
14
|
+
> To migrate: `brew uninstall brew-vulns && brew untap homebrew/brew-vulns` (or `gem uninstall brew-vulns`).
|
|
15
|
+
|
|
3
16
|
A Homebrew subcommand that checks installed packages for known vulnerabilities using the [OSV.dev](https://osv.dev) database.
|
|
4
17
|
|
|
5
18
|
## Installation
|
|
@@ -91,11 +104,11 @@ brew vulns --help
|
|
|
91
104
|
## How it works
|
|
92
105
|
|
|
93
106
|
1. Reads Homebrew formulae via `brew info --json=v2` (installed packages by default, or any named formulae passed as arguments)
|
|
94
|
-
2.
|
|
107
|
+
2. Derives a git repository URL for each formula from its stable source URL, `head` URL or homepage, and a version from the release tag or formula version
|
|
95
108
|
3. Queries the OSV API using the GIT ecosystem to find known vulnerabilities
|
|
96
109
|
4. Reports any vulnerabilities found with their severity and CVE identifiers
|
|
97
110
|
|
|
98
|
-
|
|
111
|
+
A formula is queried when a repository URL can be derived. GitHub, GitLab and Codeberg URLs are recognised in any of the stable, `head` or homepage fields; for other hosts, the `head` URL or a `tag:`-style stable git URL is used as-is. Formulae with none of these are skipped.
|
|
99
112
|
|
|
100
113
|
### Patched vulnerabilities
|
|
101
114
|
|
|
@@ -106,8 +119,8 @@ This relies on `patches[].resolves` data in `brew info --json=v2`, available fro
|
|
|
106
119
|
## Example output
|
|
107
120
|
|
|
108
121
|
```
|
|
109
|
-
Checking
|
|
110
|
-
(
|
|
122
|
+
Checking 181 packages for vulnerabilities...
|
|
123
|
+
(42 packages skipped - no source repository URL or version)
|
|
111
124
|
|
|
112
125
|
expat (2.7.3)
|
|
113
126
|
CVE-2025-66382 (HIGH) - XML parsing vulnerability...
|
|
@@ -197,7 +210,7 @@ See [examples/](examples/) for workflows that check changed formulae on tap pull
|
|
|
197
210
|
|
|
198
211
|
```bash
|
|
199
212
|
git clone https://github.com/Homebrew/homebrew-brew-vulns
|
|
200
|
-
cd brew-vulns
|
|
213
|
+
cd homebrew-brew-vulns
|
|
201
214
|
bin/setup
|
|
202
215
|
rake test
|
|
203
216
|
```
|
data/examples/pr-vuln-check.yml
CHANGED
|
@@ -18,7 +18,7 @@ jobs:
|
|
|
18
18
|
steps:
|
|
19
19
|
- name: Set up Homebrew
|
|
20
20
|
id: set-up-homebrew
|
|
21
|
-
uses: Homebrew/actions/setup-homebrew@
|
|
21
|
+
uses: Homebrew/actions/setup-homebrew@1f8e202ffddf94def7f42f6fa3a482e821489f9c # 2026.07.10.1
|
|
22
22
|
|
|
23
23
|
- name: Install brew-vulns
|
|
24
24
|
run: brew install homebrew/brew-vulns/brew-vulns
|
data/examples/scan-all.yml
CHANGED
|
@@ -16,7 +16,7 @@ jobs:
|
|
|
16
16
|
contents: write
|
|
17
17
|
steps:
|
|
18
18
|
- name: Set up Homebrew
|
|
19
|
-
uses: Homebrew/actions/setup-homebrew@
|
|
19
|
+
uses: Homebrew/actions/setup-homebrew@1f8e202ffddf94def7f42f6fa3a482e821489f9c # 2026.07.10.1
|
|
20
20
|
|
|
21
21
|
- name: Install brew-vulns
|
|
22
22
|
run: brew install homebrew/brew-vulns/brew-vulns
|
data/exe/brew-vulns
CHANGED
data/lib/brew/vulns/cli.rb
CHANGED
|
@@ -5,12 +5,15 @@ module Brew
|
|
|
5
5
|
class CLI
|
|
6
6
|
def self.run(args)
|
|
7
7
|
new(args).run
|
|
8
|
+
rescue Error => e
|
|
9
|
+
$stderr.puts "Error: #{e.message}"
|
|
10
|
+
2
|
|
8
11
|
end
|
|
9
12
|
|
|
10
13
|
DEFAULT_MAX_SUMMARY = 60
|
|
11
14
|
SEVERITY_LEVELS = { "low" => 1, "medium" => 2, "high" => 3, "critical" => 4 }.freeze
|
|
12
15
|
MAX_VULN_FETCH_THREADS = 15
|
|
13
|
-
FLAGS_WITH_VALUE = %w[-b --brewfile -m --max-summary -s --severity].freeze
|
|
16
|
+
FLAGS_WITH_VALUE = %w[-b --brewfile -m --max-summary -s --severity --osv-export].freeze
|
|
14
17
|
|
|
15
18
|
def initialize(args)
|
|
16
19
|
@args = args
|
|
@@ -25,6 +28,8 @@ module Brew
|
|
|
25
28
|
@max_summary = parse_max_summary(args)
|
|
26
29
|
@min_severity = parse_severity(args)
|
|
27
30
|
@brewfile = parse_brewfile_path(args)
|
|
31
|
+
@osv_export_dir = parse_osv_export_dir(args)
|
|
32
|
+
@force_ipv4 = args.include?("--ipv4")
|
|
28
33
|
end
|
|
29
34
|
|
|
30
35
|
def parse_formula_names(args)
|
|
@@ -71,6 +76,23 @@ module Brew
|
|
|
71
76
|
0
|
|
72
77
|
end
|
|
73
78
|
|
|
79
|
+
def parse_osv_export_dir(args)
|
|
80
|
+
args.each_with_index do |arg, idx|
|
|
81
|
+
if arg == "--osv-export"
|
|
82
|
+
value = args[idx + 1]
|
|
83
|
+
return value if value && !value.empty? && !value.start_with?("-")
|
|
84
|
+
|
|
85
|
+
raise Error, "--osv-export requires a directory argument"
|
|
86
|
+
elsif arg.start_with?("--osv-export=")
|
|
87
|
+
value = arg.split("=", 2).last
|
|
88
|
+
raise Error, "--osv-export requires a directory argument" if value.empty?
|
|
89
|
+
|
|
90
|
+
return value
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
nil
|
|
94
|
+
end
|
|
95
|
+
|
|
74
96
|
def parse_brewfile_path(args)
|
|
75
97
|
args.each_with_index do |arg, idx|
|
|
76
98
|
if arg == "--brewfile" || arg == "-b"
|
|
@@ -90,18 +112,20 @@ module Brew
|
|
|
90
112
|
return 0
|
|
91
113
|
end
|
|
92
114
|
|
|
115
|
+
return run_osv_export if @osv_export_dir
|
|
116
|
+
|
|
93
117
|
formulae = load_formulae
|
|
94
118
|
if formulae.empty?
|
|
95
119
|
puts "No formulae found."
|
|
96
120
|
return 0
|
|
97
121
|
end
|
|
98
122
|
|
|
99
|
-
queryable = formulae.select(&:
|
|
123
|
+
queryable = formulae.select(&:to_osv_query)
|
|
100
124
|
skipped = formulae.size - queryable.size
|
|
101
125
|
|
|
102
126
|
unless @json_output || @sarif_output || @cyclonedx_output
|
|
103
127
|
puts "Checking #{queryable.size} packages for vulnerabilities..."
|
|
104
|
-
puts "(#{skipped} packages skipped - no
|
|
128
|
+
puts "(#{skipped} packages skipped - no source repository URL or version)" if skipped > 0
|
|
105
129
|
puts
|
|
106
130
|
end
|
|
107
131
|
|
|
@@ -118,6 +142,28 @@ module Brew
|
|
|
118
142
|
2
|
|
119
143
|
end
|
|
120
144
|
|
|
145
|
+
# OSV export always operates on the full homebrew-core set (it generates
|
|
146
|
+
# an ecosystem-wide advisory feed), or on explicitly named formulae for
|
|
147
|
+
# testing. --brewfile / installed selection are intentionally ignored.
|
|
148
|
+
def run_osv_export
|
|
149
|
+
formulae = if @formula_names.any?
|
|
150
|
+
Formula.load_named(@formula_names)
|
|
151
|
+
else
|
|
152
|
+
Formula.load_all
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
annotated = formulae.select { |f| f.resolved_vulnerability_ids.any? }
|
|
156
|
+
$stderr.puts "#{annotated.size} formulae with security `resolves` annotations"
|
|
157
|
+
|
|
158
|
+
written = OsvExport.run(annotated, @osv_export_dir)
|
|
159
|
+
written.each { |p| $stderr.puts " wrote #{p}" }
|
|
160
|
+
$stderr.puts "#{written.size} records written to #{@osv_export_dir}"
|
|
161
|
+
0
|
|
162
|
+
rescue Error => e
|
|
163
|
+
$stderr.puts "Error: #{e.message}"
|
|
164
|
+
2
|
|
165
|
+
end
|
|
166
|
+
|
|
121
167
|
private
|
|
122
168
|
|
|
123
169
|
def load_formulae
|
|
@@ -133,7 +179,7 @@ module Brew
|
|
|
133
179
|
end
|
|
134
180
|
|
|
135
181
|
def scan_vulnerabilities(formulae)
|
|
136
|
-
client = OsvClient.new
|
|
182
|
+
client = OsvClient.new(force_ipv4: @force_ipv4)
|
|
137
183
|
queries = formulae.map(&:to_osv_query).compact
|
|
138
184
|
|
|
139
185
|
vuln_results = client.query_batch(queries)
|
|
@@ -213,7 +259,7 @@ module Brew
|
|
|
213
259
|
type: "library",
|
|
214
260
|
name: formula.name,
|
|
215
261
|
version: formula.version,
|
|
216
|
-
purl:
|
|
262
|
+
purl: formula.purl,
|
|
217
263
|
}
|
|
218
264
|
pedigree = formula.cyclonedx_pedigree
|
|
219
265
|
component[:pedigree] = pedigree if pedigree
|
|
@@ -255,7 +301,7 @@ module Brew
|
|
|
255
301
|
source: { name: "OSV", url: "https://osv.dev" },
|
|
256
302
|
ratings: [{ severity: vuln.severity_display&.downcase }],
|
|
257
303
|
description: vuln.summary,
|
|
258
|
-
affects: [{ ref:
|
|
304
|
+
affects: [{ ref: formula.purl }],
|
|
259
305
|
}
|
|
260
306
|
end
|
|
261
307
|
|
|
@@ -421,8 +467,10 @@ module Brew
|
|
|
421
467
|
-j, --json Output results as JSON
|
|
422
468
|
--cyclonedx Output results as CycloneDX SBOM with vulnerabilities
|
|
423
469
|
--sarif Output results as SARIF for GitHub code scanning
|
|
470
|
+
--osv-export DIR Write OSV-schema records for patch-resolved CVEs to DIR (experimental)
|
|
424
471
|
-m, --max-summary N Truncate summaries to N characters (default: 60, 0 for no limit)
|
|
425
472
|
-s, --severity LEVEL Only show vulnerabilities at or above LEVEL (low, medium, high, critical)
|
|
473
|
+
--ipv4 Connect to api.osv.dev over IPv4 only
|
|
426
474
|
-h, --help Show this help message
|
|
427
475
|
|
|
428
476
|
Examples:
|
|
@@ -438,6 +486,7 @@ module Brew
|
|
|
438
486
|
brew vulns --cyclonedx Output as CycloneDX SBOM
|
|
439
487
|
brew vulns --sarif Output as SARIF for GitHub Actions
|
|
440
488
|
brew vulns --severity high Only show HIGH and CRITICAL vulnerabilities
|
|
489
|
+
brew vulns --osv-export ./advisories Generate Homebrew-ecosystem OSV records
|
|
441
490
|
HELP
|
|
442
491
|
end
|
|
443
492
|
end
|
data/lib/brew/vulns/formula.rb
CHANGED
|
@@ -2,21 +2,47 @@
|
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
4
|
require "open3"
|
|
5
|
+
require "purl"
|
|
5
6
|
|
|
6
7
|
module Brew
|
|
7
8
|
module Vulns
|
|
8
9
|
class Formula
|
|
9
|
-
attr_reader :name, :version, :source_url, :head_url, :dependencies, :patches
|
|
10
|
+
attr_reader :name, :version, :revision, :source_url, :head_url, :homepage, :dependencies, :patches
|
|
10
11
|
|
|
11
12
|
def initialize(data)
|
|
12
13
|
@name = data["name"] || data["full_name"]
|
|
13
14
|
@version = data.dig("versions", "stable") || data["version"]
|
|
15
|
+
@revision = data["revision"].to_i
|
|
14
16
|
@source_url = data.dig("urls", "stable", "url")
|
|
17
|
+
@stable_tag = data.dig("urls", "stable", "tag")
|
|
15
18
|
@head_url = data.dig("urls", "head", "url")
|
|
19
|
+
@homepage = data["homepage"]
|
|
16
20
|
@dependencies = data["dependencies"] || []
|
|
17
21
|
@patches = data["patches"] || []
|
|
18
22
|
end
|
|
19
23
|
|
|
24
|
+
# The Homebrew package version including the revision suffix, e.g. "1.81.6_6".
|
|
25
|
+
# This is the version string as it appears in `pkg:brew/` purls and bottle paths.
|
|
26
|
+
def full_version
|
|
27
|
+
revision.zero? ? version : "#{version}_#{revision}"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Package URL for this formula. Uses the `purl` gem so that special
|
|
31
|
+
# characters in the formula name (notably `@` in versioned formulae like
|
|
32
|
+
# `glibc@2.13`) are percent-encoded; otherwise the `@` would be parsed as
|
|
33
|
+
# the version separator.
|
|
34
|
+
def purl(with_version: true)
|
|
35
|
+
Purl::PackageURL.new(type: "brew", name: name, version: with_version ? version : nil).to_s
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Patches whose `resolves` list contains `vuln_id` (case-insensitive).
|
|
39
|
+
def patches_resolving(vuln_id)
|
|
40
|
+
target = vuln_id.to_s.upcase
|
|
41
|
+
patches.select do |p|
|
|
42
|
+
Array(p["resolves"]).any? { |r| r.is_a?(Hash) && r["type"] == "security" && r["id"].to_s.upcase == target }
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
20
46
|
# CVE/GHSA identifiers declared as resolved by this formula's patches.
|
|
21
47
|
# Populated from `brew info --json=v2` `patches[].resolves[]` (Homebrew >= 6.0.4);
|
|
22
48
|
# empty on older Homebrew versions or for formulae with no annotated patches.
|
|
@@ -66,32 +92,32 @@ module Brew
|
|
|
66
92
|
{ patches: cdx_patches }
|
|
67
93
|
end
|
|
68
94
|
|
|
95
|
+
# The git repository URL to use as the OSV `GIT` ecosystem package name.
|
|
96
|
+
# In order of preference:
|
|
97
|
+
# 1. owner/repo extracted from a recognised forge URL on stable, head,
|
|
98
|
+
# or homepage. Known forges are preferred even over a canonical
|
|
99
|
+
# non-forge git URL, because OSV's GIT coverage is much better there.
|
|
100
|
+
# 2. the stable URL itself when stable is a git checkout (`url ..., tag: ...`)
|
|
101
|
+
# 3. the head URL verbatim (head specs are always git URLs)
|
|
102
|
+
# Returns nil when none of the above yield a URL.
|
|
69
103
|
def repo_url
|
|
70
104
|
return @repo_url if defined?(@repo_url)
|
|
71
105
|
|
|
72
|
-
@repo_url = extract_repo_url(source_url) ||
|
|
106
|
+
@repo_url = extract_repo_url(source_url) ||
|
|
107
|
+
extract_repo_url(head_url) ||
|
|
108
|
+
extract_repo_url(homepage) ||
|
|
109
|
+
(source_url if @stable_tag) ||
|
|
110
|
+
head_url
|
|
73
111
|
end
|
|
74
112
|
|
|
113
|
+
# The version identifier to send as the OSV query `version`. Prefers an
|
|
114
|
+
# explicit git tag (from a `tag:` stable spec or parsed from a forge
|
|
115
|
+
# tarball URL); falls back to the bare formula version, which OSV's GIT
|
|
116
|
+
# ecosystem accepts for at least some upstreams.
|
|
75
117
|
def tag
|
|
76
118
|
return @tag if defined?(@tag)
|
|
77
119
|
|
|
78
|
-
@tag = extract_tag_from_url(source_url)
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
def github?
|
|
82
|
-
repo_url&.include?("github.com")
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def gitlab?
|
|
86
|
-
repo_url&.include?("gitlab.com")
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
def codeberg?
|
|
90
|
-
repo_url&.include?("codeberg.org")
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def supported_forge?
|
|
94
|
-
github? || gitlab? || codeberg?
|
|
120
|
+
@tag = @stable_tag || extract_tag_from_url(source_url) || version
|
|
95
121
|
end
|
|
96
122
|
|
|
97
123
|
def to_osv_query
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "net/http"
|
|
4
4
|
require "json"
|
|
5
5
|
require "uri"
|
|
6
|
+
require "socket"
|
|
6
7
|
require "brew/vulns/version"
|
|
7
8
|
|
|
8
9
|
module Brew
|
|
@@ -20,6 +21,10 @@ module Brew
|
|
|
20
21
|
|
|
21
22
|
USER_AGENT = "brew-vulns/#{Brew::Vulns::VERSION} (+https://github.com/Homebrew/homebrew-brew-vulns)"
|
|
22
23
|
|
|
24
|
+
def initialize(force_ipv4: false)
|
|
25
|
+
@force_ipv4 = force_ipv4
|
|
26
|
+
end
|
|
27
|
+
|
|
23
28
|
def query(repo_url:, version:)
|
|
24
29
|
payload = {
|
|
25
30
|
package: {
|
|
@@ -86,6 +91,7 @@ module Brew
|
|
|
86
91
|
|
|
87
92
|
def execute_request(uri, request)
|
|
88
93
|
attempts = 0
|
|
94
|
+
ipv4_addr = resolve_ipv4(uri.host) if @force_ipv4
|
|
89
95
|
|
|
90
96
|
begin
|
|
91
97
|
attempts += 1
|
|
@@ -94,6 +100,7 @@ module Brew
|
|
|
94
100
|
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
|
95
101
|
http.open_timeout = OPEN_TIMEOUT
|
|
96
102
|
http.read_timeout = READ_TIMEOUT
|
|
103
|
+
http.ipaddr = ipv4_addr if ipv4_addr
|
|
97
104
|
|
|
98
105
|
response = http.request(request)
|
|
99
106
|
|
|
@@ -107,12 +114,14 @@ module Brew
|
|
|
107
114
|
raise ApiError, "Invalid JSON response from OSV API: #{e.message}"
|
|
108
115
|
rescue Net::OpenTimeout, Net::ReadTimeout => e
|
|
109
116
|
if attempts < MAX_RETRIES
|
|
117
|
+
ipv4_addr ||= resolve_ipv4(uri.host) if attempts == MAX_RETRIES - 1
|
|
110
118
|
sleep RETRY_DELAY
|
|
111
119
|
retry
|
|
112
120
|
end
|
|
113
121
|
raise ApiError, "OSV API timeout after #{attempts} attempts: #{e.message}"
|
|
114
122
|
rescue SocketError, Errno::ECONNREFUSED => e
|
|
115
123
|
if attempts < MAX_RETRIES
|
|
124
|
+
ipv4_addr ||= resolve_ipv4(uri.host) if attempts == MAX_RETRIES - 1
|
|
116
125
|
sleep RETRY_DELAY
|
|
117
126
|
retry
|
|
118
127
|
end
|
|
@@ -122,6 +131,15 @@ module Brew
|
|
|
122
131
|
end
|
|
123
132
|
end
|
|
124
133
|
|
|
134
|
+
# Resolve the first IPv4 address for `host`, for use with Net::HTTP#ipaddr=
|
|
135
|
+
# to bypass a misbehaving IPv6 path. Returns nil (so the caller falls back
|
|
136
|
+
# to default resolution) if no A record is found or DNS fails.
|
|
137
|
+
def resolve_ipv4(host)
|
|
138
|
+
Addrinfo.getaddrinfo(host, nil, Socket::AF_INET, Socket::SOCK_STREAM).first&.ip_address
|
|
139
|
+
rescue SocketError
|
|
140
|
+
nil
|
|
141
|
+
end
|
|
142
|
+
|
|
125
143
|
MAX_PAGES = 100
|
|
126
144
|
|
|
127
145
|
def fetch_all_pages(response, original_payload)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "time"
|
|
5
|
+
require "fileutils"
|
|
6
|
+
|
|
7
|
+
module Brew
|
|
8
|
+
module Vulns
|
|
9
|
+
# Emits OSV-schema records for a `Homebrew` ecosystem describing CVEs that
|
|
10
|
+
# homebrew-core formulae resolve via shipped patches.
|
|
11
|
+
#
|
|
12
|
+
# One record is written per (formula, vulnerability id) pair found in
|
|
13
|
+
# `patches[].resolves`. The record states that the formula was affected up
|
|
14
|
+
# to (but not including) the currently shipped version+revision; this is the
|
|
15
|
+
# "fixed at or before what we ship today" approximation, since the precise
|
|
16
|
+
# fix boundary requires homebrew-core git archaeology.
|
|
17
|
+
#
|
|
18
|
+
# Record shape follows the OSV 1.7 schema and mirrors the Debian DSA layout
|
|
19
|
+
# (`upstream` listing the source CVE, `affected[].ranges` of type
|
|
20
|
+
# `ECOSYSTEM`, `ecosystem_specific` carrying the patch detail).
|
|
21
|
+
module OsvExport
|
|
22
|
+
SCHEMA_VERSION = "1.7.3"
|
|
23
|
+
ECOSYSTEM = "Homebrew"
|
|
24
|
+
ID_PREFIX = "BREW"
|
|
25
|
+
|
|
26
|
+
module_function
|
|
27
|
+
|
|
28
|
+
def run(formulae, dir, client: OsvClient.new, now: Time.now.utc)
|
|
29
|
+
FileUtils.mkdir_p(dir)
|
|
30
|
+
written = []
|
|
31
|
+
|
|
32
|
+
formulae.each do |formula|
|
|
33
|
+
formula.resolved_vulnerability_ids.each do |vuln_id|
|
|
34
|
+
upstream = fetch_upstream(client, vuln_id)
|
|
35
|
+
record = record_for(formula, vuln_id, upstream: upstream, now: now)
|
|
36
|
+
path = File.join(dir, "#{record[:id]}.json")
|
|
37
|
+
File.write(path, JSON.pretty_generate(record))
|
|
38
|
+
written << path
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
written
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def record_for(formula, vuln_id, upstream: nil, now: Time.now.utc)
|
|
46
|
+
record = {
|
|
47
|
+
schema_version: SCHEMA_VERSION,
|
|
48
|
+
id: record_id(formula, vuln_id),
|
|
49
|
+
modified: now.strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
50
|
+
upstream: [vuln_id],
|
|
51
|
+
affected: [affected_entry(formula, vuln_id)],
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if upstream
|
|
55
|
+
record[:summary] = upstream["summary"] if upstream["summary"]
|
|
56
|
+
record[:details] = upstream["details"] if upstream["details"]
|
|
57
|
+
record[:severity] = upstream["severity"] if upstream["severity"]
|
|
58
|
+
record[:upstream] = ([vuln_id] + Array(upstream["aliases"])).uniq
|
|
59
|
+
record[:references] = upstream["references"] if upstream["references"]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
record
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def record_id(formula, vuln_id)
|
|
66
|
+
"#{ID_PREFIX}-#{formula.name}-#{vuln_id}"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def affected_entry(formula, vuln_id)
|
|
70
|
+
{
|
|
71
|
+
package: {
|
|
72
|
+
ecosystem: ECOSYSTEM,
|
|
73
|
+
name: formula.name,
|
|
74
|
+
purl: formula.purl(with_version: false),
|
|
75
|
+
},
|
|
76
|
+
ranges: [
|
|
77
|
+
{
|
|
78
|
+
type: "ECOSYSTEM",
|
|
79
|
+
events: [
|
|
80
|
+
{ introduced: "0" },
|
|
81
|
+
{ fixed: formula.full_version },
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
ecosystem_specific: {
|
|
86
|
+
fix: "patch",
|
|
87
|
+
patches: formula.patches_resolving(vuln_id).filter_map { |p| patch_ref(p) },
|
|
88
|
+
},
|
|
89
|
+
}
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def patch_ref(patch)
|
|
93
|
+
ref = {}
|
|
94
|
+
ref[:type] = patch["type"] if patch["type"]
|
|
95
|
+
ref[:url] = patch["url"] if patch["url"]
|
|
96
|
+
ref[:file] = patch["file"] if patch["file"]
|
|
97
|
+
ref[:apply] = patch["apply"] if patch["apply"]
|
|
98
|
+
ref[:data] = true if patch["data"]
|
|
99
|
+
ref.empty? ? nil : ref
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def fetch_upstream(client, vuln_id)
|
|
103
|
+
client.get_vulnerability(vuln_id)
|
|
104
|
+
rescue OsvClient::Error
|
|
105
|
+
nil
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
data/lib/brew/vulns/version.rb
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
module Brew
|
|
4
4
|
module Vulns
|
|
5
|
-
VERSION = "0.
|
|
5
|
+
VERSION = "0.5.0"
|
|
6
|
+
|
|
7
|
+
DEPRECATION_MESSAGE = <<~MSG.freeze
|
|
8
|
+
brew-vulns is deprecated: `brew vulns` is now built into Homebrew.
|
|
9
|
+
Run `brew update` and use `brew vulns` directly, then remove this
|
|
10
|
+
package with `brew uninstall brew-vulns` or `gem uninstall brew-vulns`.
|
|
11
|
+
See https://github.com/Homebrew/brew/pull/23080 for details.
|
|
12
|
+
MSG
|
|
6
13
|
end
|
|
7
14
|
end
|
data/lib/brew/vulns.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: brew-vulns
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrew Nesbitt
|
|
@@ -110,6 +110,7 @@ files:
|
|
|
110
110
|
- lib/brew/vulns/cli.rb
|
|
111
111
|
- lib/brew/vulns/formula.rb
|
|
112
112
|
- lib/brew/vulns/osv_client.rb
|
|
113
|
+
- lib/brew/vulns/osv_export.rb
|
|
113
114
|
- lib/brew/vulns/version.rb
|
|
114
115
|
- lib/brew/vulns/vulnerability.rb
|
|
115
116
|
- sig/brew/vulns.rbs
|
|
@@ -120,6 +121,11 @@ metadata:
|
|
|
120
121
|
homepage_uri: https://github.com/Homebrew/homebrew-brew-vulns
|
|
121
122
|
source_code_uri: https://github.com/Homebrew/homebrew-brew-vulns
|
|
122
123
|
changelog_uri: https://github.com/Homebrew/homebrew-brew-vulns/blob/main/CHANGELOG.md
|
|
124
|
+
post_install_message: |
|
|
125
|
+
brew-vulns is deprecated: `brew vulns` is now built into Homebrew.
|
|
126
|
+
Run `brew update` and use `brew vulns` directly, then remove this
|
|
127
|
+
package with `brew uninstall brew-vulns` or `gem uninstall brew-vulns`.
|
|
128
|
+
See https://github.com/Homebrew/brew/pull/23080 for details.
|
|
123
129
|
rdoc_options: []
|
|
124
130
|
require_paths:
|
|
125
131
|
- lib
|
|
@@ -134,7 +140,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
134
140
|
- !ruby/object:Gem::Version
|
|
135
141
|
version: '0'
|
|
136
142
|
requirements: []
|
|
137
|
-
rubygems_version: 4.0.
|
|
143
|
+
rubygems_version: 4.0.12
|
|
138
144
|
specification_version: 4
|
|
139
145
|
summary: Check Homebrew packages for known vulnerabilities
|
|
140
146
|
test_files: []
|