brew-vulns 0.3.0 → 0.4.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 -0
- data/Formula/brew-vulns.rb +2 -2
- data/README.md +7 -0
- data/lib/brew/vulns/cli.rb +83 -38
- data/lib/brew/vulns/formula.rb +51 -1
- data/lib/brew/vulns/version.rb +1 -1
- data/lib/brew/vulns/vulnerability.rb +5 -1
- metadata +9 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 80d99be04bbb5f94ac9b6ce0572699c06291a38d3cad5643f40fcc5991a11bcf
|
|
4
|
+
data.tar.gz: 50af431e418e7af72b006acbdbc3d88dcf177726f0796eb2ead936406c5242ba
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c45915f201f9b804298836a2ccf16f815eeb48899253499c315ae74a7684e52da207b344682fe8b48af14afe4003bbcdeffb233a11a984cca5821554e687626d
|
|
7
|
+
data.tar.gz: 4aea92e8df8fe2143d8e1f4268d0173c98d11f721ce1195b79e68c5c67e41357a2f9b96ded95d5b57d62baf821bf20e5cd43beebf18995fc039d8da6cb2b746c
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.4.0] - 2026-06-28
|
|
4
|
+
|
|
5
|
+
- Suppress vulnerabilities that a formula's patches declare as resolved (via `patches[].resolves` in `brew info --json=v2`, Homebrew 6.0.4+); list them separately in text/JSON output, exclude them from SARIF output, and exclude them from the exit code
|
|
6
|
+
- CycloneDX output: emit patched vulnerabilities with `analysis.state = resolved` and include formula patches as `pedigree.patches` on each component
|
|
7
|
+
- Add `--no-ignore-patches` to report patched vulnerabilities as open findings
|
|
8
|
+
- Fix invalid SARIF output when no vulnerabilities are found (GitHub code scanning rejected the file)
|
|
9
|
+
|
|
3
10
|
## [0.3.0] - 2026-05-29
|
|
4
11
|
|
|
5
12
|
- Add `--all` flag to scan every formula in homebrew-core
|
data/Formula/brew-vulns.rb
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
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.3.0.tar.gz"
|
|
5
|
+
sha256 "1b9fbb03d46192350ad7dcac4279b382debb917d40688a4b816319214a2570d4"
|
|
6
6
|
license "MIT"
|
|
7
7
|
|
|
8
8
|
depends_on "ruby"
|
data/README.md
CHANGED
|
@@ -31,6 +31,7 @@ brew vulns [formula...] [options]
|
|
|
31
31
|
| | `--all` | Scan every formula in homebrew-core |
|
|
32
32
|
| `-b PATH` | `--brewfile PATH` | Scan packages from a Brewfile (default: ./Brewfile) |
|
|
33
33
|
| `-d` | `--deps` | Include dependencies when checking a specific formula or Brewfile |
|
|
34
|
+
| | `--no-ignore-patches` | Report vulnerabilities even when the formula applies a patch that resolves them |
|
|
34
35
|
| `-j` | `--json` | Output results as JSON |
|
|
35
36
|
| | `--cyclonedx` | Output results as CycloneDX SBOM with vulnerabilities |
|
|
36
37
|
| | `--sarif` | Output results as SARIF for GitHub code scanning |
|
|
@@ -96,6 +97,12 @@ brew vulns --help
|
|
|
96
97
|
|
|
97
98
|
Packages with GitHub, GitLab, or Codeberg source URLs are checked. Packages from other sources are skipped.
|
|
98
99
|
|
|
100
|
+
### Patched vulnerabilities
|
|
101
|
+
|
|
102
|
+
Some Homebrew formulae apply patches that fix CVEs without changing the upstream version number. Where a formula's `patch` block declares (or infers) a `resolves` entry for a CVE or GHSA identifier, `brew vulns` treats matching OSV results as already resolved: they are listed separately in text and `--json` output, omitted from `--sarif` output, emitted in `--cyclonedx` output with `analysis.state` set to `resolved`, and do not affect the exit code. The CycloneDX SBOM also records each formula's patches under `components[].pedigree.patches`. Pass `--no-ignore-patches` to report them as open findings instead.
|
|
103
|
+
|
|
104
|
+
This relies on `patches[].resolves` data in `brew info --json=v2`, available from Homebrew 6.0.4 onwards. With an older Homebrew, or for formulae whose patches are not yet annotated, no suppression happens.
|
|
105
|
+
|
|
99
106
|
## Example output
|
|
100
107
|
|
|
101
108
|
```
|
data/lib/brew/vulns/cli.rb
CHANGED
|
@@ -17,6 +17,7 @@ module Brew
|
|
|
17
17
|
@formula_names = parse_formula_names(args)
|
|
18
18
|
@all = args.include?("--all")
|
|
19
19
|
@include_deps = args.include?("--deps") || args.include?("-d")
|
|
20
|
+
@ignore_patches = !args.include?("--no-ignore-patches")
|
|
20
21
|
@json_output = args.include?("--json") || args.include?("-j")
|
|
21
22
|
@sarif_output = args.include?("--sarif")
|
|
22
23
|
@cyclonedx_output = args.include?("--cyclonedx")
|
|
@@ -104,8 +105,8 @@ module Brew
|
|
|
104
105
|
puts
|
|
105
106
|
end
|
|
106
107
|
|
|
107
|
-
results = scan_vulnerabilities(queryable)
|
|
108
|
-
output_results(results, formulae)
|
|
108
|
+
results, patched = scan_vulnerabilities(queryable)
|
|
109
|
+
output_results(results, patched, formulae)
|
|
109
110
|
rescue OsvClient::Error => e
|
|
110
111
|
$stderr.puts "Error querying OSV: #{e.message}"
|
|
111
112
|
2
|
|
@@ -138,6 +139,7 @@ module Brew
|
|
|
138
139
|
vuln_results = client.query_batch(queries)
|
|
139
140
|
|
|
140
141
|
results = {}
|
|
142
|
+
patched = {}
|
|
141
143
|
formulae.each_with_index do |formula, idx|
|
|
142
144
|
batch_vulns = vuln_results[idx] || []
|
|
143
145
|
next if batch_vulns.empty?
|
|
@@ -155,40 +157,49 @@ module Brew
|
|
|
155
157
|
vulns = vulns.select { |v| v.affects_version?(version) }
|
|
156
158
|
vulns = vulns.select { |v| v.severity_level >= @min_severity } if @min_severity > 0
|
|
157
159
|
|
|
160
|
+
if @ignore_patches
|
|
161
|
+
resolved, vulns = vulns.partition { |v| formula.resolves?(v) }
|
|
162
|
+
patched[formula] = resolved if resolved.any?
|
|
163
|
+
end
|
|
164
|
+
|
|
158
165
|
results[formula] = vulns if vulns.any?
|
|
159
166
|
end
|
|
160
167
|
|
|
161
|
-
results
|
|
168
|
+
[results, patched]
|
|
162
169
|
end
|
|
163
170
|
|
|
164
|
-
def output_results(results, all_formulae)
|
|
171
|
+
def output_results(results, patched, all_formulae)
|
|
165
172
|
if @cyclonedx_output
|
|
166
|
-
output_cyclonedx(results, all_formulae)
|
|
173
|
+
output_cyclonedx(results, patched, all_formulae)
|
|
167
174
|
elsif @sarif_output
|
|
168
175
|
output_sarif(results)
|
|
169
176
|
elsif @json_output
|
|
170
|
-
output_json(results)
|
|
177
|
+
output_json(results, patched)
|
|
171
178
|
else
|
|
172
|
-
output_text(results, all_formulae)
|
|
179
|
+
output_text(results, patched, all_formulae)
|
|
173
180
|
end
|
|
174
181
|
end
|
|
175
182
|
|
|
176
|
-
def
|
|
177
|
-
|
|
183
|
+
def vuln_json(vuln)
|
|
184
|
+
{
|
|
185
|
+
id: vuln.id,
|
|
186
|
+
severity: vuln.severity_display,
|
|
187
|
+
summary: vuln.summary,
|
|
188
|
+
aliases: vuln.aliases,
|
|
189
|
+
fixed_versions: vuln.fixed_versions,
|
|
190
|
+
}
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def output_json(results, patched)
|
|
194
|
+
formulae = (results.keys + patched.keys).uniq
|
|
195
|
+
data = formulae.map do |formula|
|
|
178
196
|
{
|
|
179
|
-
formula:
|
|
180
|
-
version:
|
|
181
|
-
tag:
|
|
182
|
-
repo_url:
|
|
183
|
-
vulnerabilities:
|
|
184
|
-
|
|
185
|
-
id: v.id,
|
|
186
|
-
severity: v.severity_display,
|
|
187
|
-
summary: v.summary,
|
|
188
|
-
aliases: v.aliases,
|
|
189
|
-
fixed_versions: v.fixed_versions
|
|
190
|
-
}
|
|
191
|
-
end
|
|
197
|
+
formula: formula.name,
|
|
198
|
+
version: formula.version,
|
|
199
|
+
tag: formula.tag,
|
|
200
|
+
repo_url: formula.repo_url,
|
|
201
|
+
vulnerabilities: (results[formula] || []).map { |v| vuln_json(v) },
|
|
202
|
+
patched: (patched[formula] || []).map { |v| vuln_json(v) },
|
|
192
203
|
}
|
|
193
204
|
end
|
|
194
205
|
|
|
@@ -196,39 +207,58 @@ module Brew
|
|
|
196
207
|
results.empty? ? 0 : 1
|
|
197
208
|
end
|
|
198
209
|
|
|
199
|
-
def output_cyclonedx(results, all_formulae)
|
|
210
|
+
def output_cyclonedx(results, patched, all_formulae)
|
|
200
211
|
components = all_formulae.map do |formula|
|
|
201
|
-
{
|
|
202
|
-
type:
|
|
203
|
-
name:
|
|
212
|
+
component = {
|
|
213
|
+
type: "library",
|
|
214
|
+
name: formula.name,
|
|
204
215
|
version: formula.version,
|
|
205
|
-
purl:
|
|
216
|
+
purl: "pkg:brew/#{formula.name}@#{formula.version}",
|
|
206
217
|
}
|
|
218
|
+
pedigree = formula.cyclonedx_pedigree
|
|
219
|
+
component[:pedigree] = pedigree if pedigree
|
|
220
|
+
component
|
|
207
221
|
end
|
|
208
222
|
|
|
209
223
|
vulnerabilities = []
|
|
210
224
|
results.each do |formula, vulns|
|
|
211
225
|
vulns.each do |vuln|
|
|
212
|
-
vulnerabilities <<
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
226
|
+
vulnerabilities << cyclonedx_vulnerability(formula, vuln)
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
patched.each do |formula, vulns|
|
|
230
|
+
vulns.each do |vuln|
|
|
231
|
+
matched = (formula.resolved_vulnerability_ids & vuln.identifiers.map { |i| i.to_s.upcase }).join(", ")
|
|
232
|
+
vulnerabilities << cyclonedx_vulnerability(formula, vuln).merge(
|
|
233
|
+
analysis: {
|
|
234
|
+
state: "resolved",
|
|
235
|
+
response: ["update"],
|
|
236
|
+
detail: "Resolved by Homebrew formula patch (#{matched}).",
|
|
237
|
+
},
|
|
238
|
+
)
|
|
219
239
|
end
|
|
220
240
|
end
|
|
221
241
|
|
|
222
242
|
generator = Sbom::Cyclonedx::Generator.new(format: :json)
|
|
223
243
|
generator.generate("brew-vulns", {
|
|
224
|
-
packages:
|
|
225
|
-
vulnerabilities: vulnerabilities
|
|
244
|
+
packages: components,
|
|
245
|
+
vulnerabilities: vulnerabilities,
|
|
226
246
|
})
|
|
227
247
|
|
|
228
248
|
puts generator.output
|
|
229
249
|
results.empty? ? 0 : 1
|
|
230
250
|
end
|
|
231
251
|
|
|
252
|
+
def cyclonedx_vulnerability(formula, vuln)
|
|
253
|
+
{
|
|
254
|
+
id: vuln.id,
|
|
255
|
+
source: { name: "OSV", url: "https://osv.dev" },
|
|
256
|
+
ratings: [{ severity: vuln.severity_display&.downcase }],
|
|
257
|
+
description: vuln.summary,
|
|
258
|
+
affects: [{ ref: "pkg:brew/#{formula.name}@#{formula.version}" }],
|
|
259
|
+
}
|
|
260
|
+
end
|
|
261
|
+
|
|
232
262
|
def output_sarif(results)
|
|
233
263
|
rules = []
|
|
234
264
|
sarif_results = []
|
|
@@ -300,9 +330,10 @@ module Brew
|
|
|
300
330
|
end
|
|
301
331
|
end
|
|
302
332
|
|
|
303
|
-
def output_text(results, all_formulae)
|
|
333
|
+
def output_text(results, patched, all_formulae)
|
|
304
334
|
if results.empty?
|
|
305
|
-
puts "No vulnerabilities found."
|
|
335
|
+
puts patched.empty? ? "No vulnerabilities found." : "No open vulnerabilities found."
|
|
336
|
+
output_patched_summary(patched)
|
|
306
337
|
return 0
|
|
307
338
|
end
|
|
308
339
|
|
|
@@ -336,9 +367,22 @@ module Brew
|
|
|
336
367
|
end
|
|
337
368
|
|
|
338
369
|
puts "Found #{total_vulns} vulnerabilities in #{results.size} packages"
|
|
370
|
+
output_patched_summary(patched)
|
|
339
371
|
1
|
|
340
372
|
end
|
|
341
373
|
|
|
374
|
+
def output_patched_summary(patched)
|
|
375
|
+
return if patched.empty?
|
|
376
|
+
|
|
377
|
+
total = patched.values.sum(&:size)
|
|
378
|
+
puts
|
|
379
|
+
puts "#{total} resolved by formula patches (not counted; pass --no-ignore-patches to include):"
|
|
380
|
+
patched.sort_by { |f, _| f.name }.each do |formula, vulns|
|
|
381
|
+
ids = vulns.map { |v| sanitize_terminal_escapes(v.id) }.join(", ")
|
|
382
|
+
puts " #{sanitize_terminal_escapes(formula.name)}: #{ids}"
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
|
|
342
386
|
def sanitize_terminal_escapes(text)
|
|
343
387
|
text.to_s
|
|
344
388
|
.gsub(/\e\][^\a\e]*(?:\a|\e\\)/, "")
|
|
@@ -373,6 +417,7 @@ module Brew
|
|
|
373
417
|
--all Scan every formula in homebrew-core
|
|
374
418
|
-b, --brewfile PATH Scan packages from a Brewfile (default: ./Brewfile)
|
|
375
419
|
-d, --deps Include dependencies when checking a specific formula or Brewfile
|
|
420
|
+
--no-ignore-patches Report vulnerabilities even when the formula applies a patch that resolves them
|
|
376
421
|
-j, --json Output results as JSON
|
|
377
422
|
--cyclonedx Output results as CycloneDX SBOM with vulnerabilities
|
|
378
423
|
--sarif Output results as SARIF for GitHub code scanning
|
data/lib/brew/vulns/formula.rb
CHANGED
|
@@ -6,7 +6,7 @@ require "open3"
|
|
|
6
6
|
module Brew
|
|
7
7
|
module Vulns
|
|
8
8
|
class Formula
|
|
9
|
-
attr_reader :name, :version, :source_url, :head_url, :dependencies
|
|
9
|
+
attr_reader :name, :version, :source_url, :head_url, :dependencies, :patches
|
|
10
10
|
|
|
11
11
|
def initialize(data)
|
|
12
12
|
@name = data["name"] || data["full_name"]
|
|
@@ -14,6 +14,56 @@ module Brew
|
|
|
14
14
|
@source_url = data.dig("urls", "stable", "url")
|
|
15
15
|
@head_url = data.dig("urls", "head", "url")
|
|
16
16
|
@dependencies = data["dependencies"] || []
|
|
17
|
+
@patches = data["patches"] || []
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# CVE/GHSA identifiers declared as resolved by this formula's patches.
|
|
21
|
+
# Populated from `brew info --json=v2` `patches[].resolves[]` (Homebrew >= 6.0.4);
|
|
22
|
+
# empty on older Homebrew versions or for formulae with no annotated patches.
|
|
23
|
+
def resolved_vulnerability_ids
|
|
24
|
+
return @resolved_vulnerability_ids if defined?(@resolved_vulnerability_ids)
|
|
25
|
+
|
|
26
|
+
@resolved_vulnerability_ids = patches
|
|
27
|
+
.flat_map { |p| Array(p["resolves"]) }
|
|
28
|
+
.select { |r| r.is_a?(Hash) && r["type"] == "security" }
|
|
29
|
+
.map { |r| r["id"].to_s.upcase }
|
|
30
|
+
.reject(&:empty?)
|
|
31
|
+
.uniq
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def resolves?(vulnerability)
|
|
35
|
+
ids = resolved_vulnerability_ids
|
|
36
|
+
return false if ids.empty?
|
|
37
|
+
|
|
38
|
+
vulnerability.identifiers.any? { |id| ids.include?(id.to_s.upcase) }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
CYCLONEDX_PATCH_TYPES = {
|
|
42
|
+
"backport" => "backport",
|
|
43
|
+
"cherry_pick" => "cherry-pick",
|
|
44
|
+
"unofficial" => "unofficial",
|
|
45
|
+
}.freeze
|
|
46
|
+
|
|
47
|
+
# Maps `brew info --json=v2` patch data to a CycloneDX `pedigree` hash
|
|
48
|
+
# (symbol-keyed for the `sbom` gem). Returns nil when there are no patches.
|
|
49
|
+
def cyclonedx_pedigree
|
|
50
|
+
return nil if patches.empty?
|
|
51
|
+
|
|
52
|
+
cdx_patches = patches.map do |p|
|
|
53
|
+
patch = { type: CYCLONEDX_PATCH_TYPES.fetch(p["type"].to_s, "unofficial") }
|
|
54
|
+
patch[:diff] = { url: p["url"] } if p["url"]
|
|
55
|
+
|
|
56
|
+
resolves = Array(p["resolves"]).filter_map do |r|
|
|
57
|
+
next unless r.is_a?(Hash) && r["type"] && r["id"]
|
|
58
|
+
|
|
59
|
+
{ type: r["type"], id: r["id"] }
|
|
60
|
+
end
|
|
61
|
+
patch[:resolves] = resolves if resolves.any?
|
|
62
|
+
|
|
63
|
+
patch
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
{ patches: cdx_patches }
|
|
17
67
|
end
|
|
18
68
|
|
|
19
69
|
def repo_url
|
data/lib/brew/vulns/version.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.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrew Nesbitt
|
|
@@ -44,6 +44,9 @@ dependencies:
|
|
|
44
44
|
- - "~>"
|
|
45
45
|
- !ruby/object:Gem::Version
|
|
46
46
|
version: '0.1'
|
|
47
|
+
- - ">="
|
|
48
|
+
- !ruby/object:Gem::Version
|
|
49
|
+
version: 0.1.1
|
|
47
50
|
type: :runtime
|
|
48
51
|
prerelease: false
|
|
49
52
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -51,20 +54,23 @@ dependencies:
|
|
|
51
54
|
- - "~>"
|
|
52
55
|
- !ruby/object:Gem::Version
|
|
53
56
|
version: '0.1'
|
|
57
|
+
- - ">="
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: 0.1.1
|
|
54
60
|
- !ruby/object:Gem::Dependency
|
|
55
61
|
name: sbom
|
|
56
62
|
requirement: !ruby/object:Gem::Requirement
|
|
57
63
|
requirements:
|
|
58
64
|
- - "~>"
|
|
59
65
|
- !ruby/object:Gem::Version
|
|
60
|
-
version: '0.
|
|
66
|
+
version: '0.5'
|
|
61
67
|
type: :runtime
|
|
62
68
|
prerelease: false
|
|
63
69
|
version_requirements: !ruby/object:Gem::Requirement
|
|
64
70
|
requirements:
|
|
65
71
|
- - "~>"
|
|
66
72
|
- !ruby/object:Gem::Version
|
|
67
|
-
version: '0.
|
|
73
|
+
version: '0.5'
|
|
68
74
|
- !ruby/object:Gem::Dependency
|
|
69
75
|
name: vers
|
|
70
76
|
requirement: !ruby/object:Gem::Requirement
|