brew-vulns 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: d1e6ed689e85382d79c401a23c21c55e393038dba7b5f3ec52f90196904effe1
4
+ data.tar.gz: 55400c9cafeb368de7f8079363d0e46ae851e0f4cd16f4984139da655e84a36d
5
+ SHA512:
6
+ metadata.gz: b2f086436e55e908a72b5e22b07e7890f99f5ff34a98c4db863f4121f3a7961f86036ca772e5a35d171beaa4c70f29425f5b224513c931f612a8ed3f183eeb0f
7
+ data.tar.gz: dd7ba735193a41955bca5e1a66cfc6340441bcf4baad3cca231b1490b51f0efede7436e1cb564eb6a02f889d682241ef4c73a904754121ea7080044abbf90931
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 4.0.0
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2026-01-08
4
+
5
+ - Initial release
@@ -0,0 +1,10 @@
1
+ # Code of Conduct
2
+
3
+ "brew-vulns" follows [The Ruby Community Conduct Guideline](https://www.ruby-lang.org/en/conduct) in all "collaborative space", which is defined as community communications channels (such as mailing lists, submitted patches, commit comments, etc.):
4
+
5
+ * Participants will be tolerant of opposing views.
6
+ * Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks.
7
+ * When interpreting the words and actions of others, participants should always assume good intentions.
8
+ * Behaviour which can be reasonably considered harassment will not be tolerated.
9
+
10
+ If you have any concerns about behaviour within this project, please contact us at ["andrewnez@gmail.com"](mailto:"andrewnez@gmail.com").
@@ -0,0 +1,25 @@
1
+ class BrewVulns < Formula
2
+ desc "Check Homebrew packages for known vulnerabilities via osv.dev"
3
+ homepage "https://github.com/andrew/brew-vulns"
4
+ url "https://github.com/andrew/brew-vulns/archive/refs/tags/v0.1.0.tar.gz"
5
+ sha256 "UPDATE_WITH_SHA256_AFTER_RELEASE"
6
+ license "MIT"
7
+
8
+ depends_on "ruby"
9
+
10
+ def install
11
+ ENV["GEM_HOME"] = libexec
12
+
13
+ system "git", "init"
14
+ system "git", "add", "."
15
+
16
+ system "gem", "build", "brew-vulns.gemspec"
17
+ system "gem", "install", "--no-document", "brew-vulns-#{version}.gem"
18
+ bin.install libexec/"bin/brew-vulns"
19
+ bin.env_script_all_files(libexec/"bin", GEM_HOME: ENV.fetch("GEM_HOME", nil))
20
+ end
21
+
22
+ test do
23
+ system bin/"brew-vulns", "--help"
24
+ end
25
+ end
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Andrew Nesbitt
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,84 @@
1
+ # brew-vulns
2
+
3
+ A Homebrew subcommand that checks installed packages for known vulnerabilities using the [OSV.dev](https://osv.dev) database.
4
+
5
+ ## Installation
6
+
7
+ Via Homebrew:
8
+
9
+ ```bash
10
+ brew tap andrew/brew-vulns https://github.com/andrew/brew-vulns
11
+ brew install brew-vulns
12
+ ```
13
+
14
+ Or via RubyGems:
15
+
16
+ ```bash
17
+ gem install brew-vulns
18
+ ```
19
+
20
+ Once installed, the command is available as `brew vulns`.
21
+
22
+ ## Usage
23
+
24
+ ```bash
25
+ # Check all installed packages
26
+ brew vulns
27
+
28
+ # Check a specific formula
29
+ brew vulns openssl
30
+
31
+ # Check a formula and its dependencies
32
+ brew vulns python --deps
33
+
34
+ # Output as JSON (useful for CI/CD)
35
+ brew vulns --json
36
+
37
+ # Show help
38
+ brew vulns --help
39
+ ```
40
+
41
+ ## How it works
42
+
43
+ 1. Reads installed Homebrew formulae via `brew info --json=v2 --installed`
44
+ 2. Extracts the GitHub repository URL and version tag from each formula's source URL
45
+ 3. Queries the OSV API using the GIT ecosystem to find known vulnerabilities
46
+ 4. Reports any vulnerabilities found with their severity and CVE identifiers
47
+
48
+ Only packages with GitHub source URLs can be checked. Packages from other sources are skipped.
49
+
50
+ ## Example output
51
+
52
+ ```
53
+ Checking 104 packages for vulnerabilities...
54
+ (119 packages skipped - no GitHub source URL)
55
+
56
+ expat (2.7.3)
57
+ CVE-2025-66382 (HIGH) - XML parsing vulnerability...
58
+
59
+ hdf5 (1.14.6)
60
+ OSV-2023-1091 (MEDIUM) - Buffer overflow in...
61
+ OSV-2023-1223 (MEDIUM) - ...
62
+
63
+ Found 15 vulnerabilities in 3 packages
64
+ ```
65
+
66
+ ## Exit codes
67
+
68
+ - `0` - No vulnerabilities found
69
+ - `1` - Vulnerabilities found (or error occurred)
70
+
71
+ This makes it suitable for use in CI/CD pipelines.
72
+
73
+ ## Development
74
+
75
+ ```bash
76
+ git clone https://github.com/andrewnesbitt/brew-vulns
77
+ cd brew-vulns
78
+ bin/setup
79
+ rake test
80
+ ```
81
+
82
+ ## License
83
+
84
+ MIT License. See [LICENSE](LICENSE) for details.
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ task default: :test
data/exe/brew-vulns ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "brew/vulns"
5
+
6
+ exit Brew::Vulns::CLI.run(ARGV)
@@ -0,0 +1,176 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Brew
4
+ module Vulns
5
+ class CLI
6
+ def self.run(args)
7
+ new(args).run
8
+ end
9
+
10
+ def initialize(args)
11
+ @args = args
12
+ @formula_filter = args.first unless args.first&.start_with?("-")
13
+ @include_deps = args.include?("--deps") || args.include?("-d")
14
+ @json_output = args.include?("--json") || args.include?("-j")
15
+ @help = args.include?("--help") || args.include?("-h")
16
+ end
17
+
18
+ def run
19
+ if @help
20
+ print_help
21
+ return 0
22
+ end
23
+
24
+ formulae = load_formulae
25
+ if formulae.empty?
26
+ puts "No installed formulae found."
27
+ return 0
28
+ end
29
+
30
+ queryable = formulae.select(&:github?).select(&:tag)
31
+ skipped = formulae.size - queryable.size
32
+
33
+ unless @json_output
34
+ puts "Checking #{queryable.size} packages for vulnerabilities..."
35
+ puts "(#{skipped} packages skipped - no GitHub source URL)" if skipped > 0
36
+ puts
37
+ end
38
+
39
+ results = scan_vulnerabilities(queryable)
40
+ output_results(results, formulae)
41
+ rescue OsvClient::Error => e
42
+ $stderr.puts "Error querying OSV: #{e.message}"
43
+ 1
44
+ rescue Error => e
45
+ $stderr.puts "Error: #{e.message}"
46
+ 1
47
+ rescue JSON::ParserError => e
48
+ $stderr.puts "Error parsing brew output: #{e.message}"
49
+ 1
50
+ end
51
+
52
+ private
53
+
54
+ def load_formulae
55
+ if @include_deps && @formula_filter
56
+ Formula.load_with_dependencies(@formula_filter)
57
+ else
58
+ Formula.load_installed(@formula_filter)
59
+ end
60
+ end
61
+
62
+ def scan_vulnerabilities(formulae)
63
+ client = OsvClient.new
64
+ queries = formulae.map(&:to_osv_query).compact
65
+
66
+ vuln_results = client.query_batch(queries)
67
+
68
+ results = {}
69
+ formulae.each_with_index do |formula, idx|
70
+ vulns = Vulnerability.from_osv_list(vuln_results[idx] || [])
71
+ results[formula] = vulns if vulns.any?
72
+ end
73
+
74
+ results
75
+ end
76
+
77
+ def output_results(results, all_formulae)
78
+ if @json_output
79
+ output_json(results)
80
+ else
81
+ output_text(results, all_formulae)
82
+ end
83
+ end
84
+
85
+ def output_json(results)
86
+ data = results.map do |formula, vulns|
87
+ {
88
+ formula: formula.name,
89
+ version: formula.version,
90
+ tag: formula.tag,
91
+ repo_url: formula.repo_url,
92
+ vulnerabilities: vulns.map do |v|
93
+ {
94
+ id: v.id,
95
+ severity: v.severity_display,
96
+ summary: v.summary,
97
+ aliases: v.aliases,
98
+ fixed_versions: v.fixed_versions
99
+ }
100
+ end
101
+ }
102
+ end
103
+
104
+ puts JSON.pretty_generate(data)
105
+ results.empty? ? 0 : 1
106
+ end
107
+
108
+ def output_text(results, all_formulae)
109
+ if results.empty?
110
+ puts "No vulnerabilities found."
111
+ return 0
112
+ end
113
+
114
+ total_vulns = 0
115
+ sorted = results.sort_by { |_, vulns| -vulns.map(&:severity_level).max }
116
+
117
+ sorted.each do |formula, vulns|
118
+ puts "#{formula.name} (#{formula.version})"
119
+ vulns.sort_by { |v| -v.severity_level }.each do |vuln|
120
+ total_vulns += 1
121
+ severity = colorize_severity(vuln.severity_display)
122
+
123
+ line = " #{vuln.id} (#{severity})"
124
+ if vuln.summary
125
+ summary = vuln.summary.length > 60 ? "#{vuln.summary.slice(0, 60)}..." : vuln.summary
126
+ line = "#{line} - #{summary}"
127
+ end
128
+ puts line
129
+
130
+ if vuln.fixed_versions.any?
131
+ puts " Fixed in: #{vuln.fixed_versions.join(", ")}"
132
+ end
133
+ end
134
+ puts
135
+ end
136
+
137
+ puts "Found #{total_vulns} vulnerabilities in #{results.size} packages"
138
+ 1
139
+ end
140
+
141
+ def colorize_severity(severity)
142
+ return severity unless $stdout.tty?
143
+
144
+ case severity
145
+ when "CRITICAL" then "\e[1;31m#{severity}\e[0m"
146
+ when "HIGH" then "\e[31m#{severity}\e[0m"
147
+ when "MEDIUM" then "\e[33m#{severity}\e[0m"
148
+ when "LOW" then "\e[32m#{severity}\e[0m"
149
+ else severity
150
+ end
151
+ end
152
+
153
+ def print_help
154
+ puts <<~HELP
155
+ Usage: brew vulns [formula] [options]
156
+
157
+ Check installed Homebrew packages for known vulnerabilities via osv.dev.
158
+
159
+ Arguments:
160
+ formula Check only this formula (optional)
161
+
162
+ Options:
163
+ -d, --deps Include dependencies when checking a specific formula
164
+ -j, --json Output results as JSON
165
+ -h, --help Show this help message
166
+
167
+ Examples:
168
+ brew vulns Check all installed packages
169
+ brew vulns openssl Check only openssl
170
+ brew vulns vim --deps Check vim and its dependencies
171
+ brew vulns --json Output as JSON for CI/CD
172
+ HELP
173
+ end
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "open3"
5
+
6
+ module Brew
7
+ module Vulns
8
+ class Formula
9
+ attr_reader :name, :version, :source_url, :head_url, :dependencies
10
+
11
+ def initialize(data)
12
+ @name = data["name"] || data["full_name"]
13
+ @version = data.dig("versions", "stable") || data["version"]
14
+ @source_url = data.dig("urls", "stable", "url")
15
+ @head_url = data.dig("urls", "head", "url")
16
+ @dependencies = data["dependencies"] || []
17
+ end
18
+
19
+ def repo_url
20
+ return @repo_url if defined?(@repo_url)
21
+
22
+ @repo_url = extract_repo_url(source_url) || extract_repo_url(head_url)
23
+ end
24
+
25
+ def tag
26
+ return @tag if defined?(@tag)
27
+
28
+ @tag = extract_tag_from_url(source_url)
29
+ end
30
+
31
+ def github?
32
+ repo_url&.include?("github.com")
33
+ end
34
+
35
+ def to_osv_query
36
+ return nil unless repo_url && tag
37
+
38
+ { repo_url: repo_url, version: tag, name: name }
39
+ end
40
+
41
+ def self.load_installed(formula_filter = nil)
42
+ json, status = Open3.capture2("brew", "info", "--json=v2", "--installed")
43
+ raise Error, "brew info failed with status #{status.exitstatus}" unless status.success?
44
+
45
+ data = JSON.parse(json)
46
+ formulae = data["formulae"].map { |f| new(f) }
47
+
48
+ if formula_filter
49
+ formulae.select! { |f| f.name == formula_filter || f.name.start_with?("#{formula_filter}@") }
50
+ end
51
+
52
+ formulae
53
+ end
54
+
55
+ def self.load_with_dependencies(formula_filter = nil)
56
+ json, status = Open3.capture2("brew", "info", "--json=v2", "--installed")
57
+ raise Error, "brew info failed with status #{status.exitstatus}" unless status.success?
58
+
59
+ data = JSON.parse(json)
60
+ all_formulae = data["formulae"].map { |f| new(f) }
61
+ formulae_by_name = all_formulae.each_with_object({}) { |f, h| h[f.name] = f }
62
+
63
+ if formula_filter
64
+ filtered = all_formulae.select { |f| f.name == formula_filter || f.name.start_with?("#{formula_filter}@") }
65
+ return [] if filtered.empty?
66
+
67
+ deps_output, = Open3.capture2("brew", "deps", "--installed", formula_filter)
68
+ dep_names = deps_output.split("\n").map(&:strip)
69
+
70
+ result = filtered.each_with_object({}) { |f, h| h[f.name] = f }
71
+ dep_names.each do |dep_name|
72
+ dep = formulae_by_name[dep_name]
73
+ result[dep_name] = dep if dep && !result[dep_name]
74
+ end
75
+
76
+ result.values
77
+ else
78
+ all_formulae
79
+ end
80
+ end
81
+
82
+ private
83
+
84
+ def extract_repo_url(url)
85
+ return nil unless url
86
+ return nil unless url.include?("github.com")
87
+
88
+ match = url.match(%r{https?://github\.com/([^/]+/[^/]+)})
89
+ if match
90
+ repo_path = match[1].sub(/\.git$/, "")
91
+ return "https://github.com/#{repo_path}"
92
+ end
93
+
94
+ nil
95
+ end
96
+
97
+ def extract_tag_from_url(url)
98
+ return nil unless url
99
+
100
+ patterns = [
101
+ %r{/archive/refs/tags/([^/]+)\.tar\.gz$},
102
+ %r{/archive/refs/tags/([^/]+)\.zip$},
103
+ %r{/archive/([^/]+)\.tar\.gz$},
104
+ %r{/archive/([^/]+)\.zip$},
105
+ %r{/releases/download/([^/]+)/},
106
+ %r{/tarball/([^/]+)$}
107
+ ]
108
+
109
+ patterns.each do |pattern|
110
+ match = url.match(pattern)
111
+ return match[1] if match
112
+ end
113
+
114
+ nil
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "json"
5
+ require "uri"
6
+
7
+ module Brew
8
+ module Vulns
9
+ class OsvClient
10
+ API_BASE = "https://api.osv.dev/v1"
11
+ BATCH_SIZE = 1000
12
+ OPEN_TIMEOUT = 10
13
+ READ_TIMEOUT = 30
14
+
15
+ class Error < StandardError; end
16
+ class ApiError < Error; end
17
+
18
+ def query(repo_url:, version:)
19
+ payload = {
20
+ package: {
21
+ name: repo_url,
22
+ ecosystem: "GIT"
23
+ },
24
+ version: version
25
+ }
26
+
27
+ response = post("/query", payload)
28
+ fetch_all_pages(response, payload)
29
+ end
30
+
31
+ def query_batch(packages)
32
+ return [] if packages.empty?
33
+
34
+ results = Array.new(packages.size) { [] }
35
+
36
+ packages.each_slice(BATCH_SIZE).with_index do |batch, batch_idx|
37
+ queries = batch.map do |pkg|
38
+ {
39
+ package: {
40
+ name: pkg[:repo_url],
41
+ ecosystem: "GIT"
42
+ },
43
+ version: pkg[:version]
44
+ }
45
+ end
46
+
47
+ response = post("/querybatch", { queries: queries })
48
+ batch_results = response["results"] || []
49
+
50
+ batch_results.each_with_index do |result, idx|
51
+ global_idx = batch_idx * BATCH_SIZE + idx
52
+ results[global_idx] = result["vulns"] || []
53
+ end
54
+ end
55
+
56
+ results
57
+ end
58
+
59
+ def get_vulnerability(vuln_id)
60
+ get("/vulns/#{URI.encode_uri_component(vuln_id)}")
61
+ end
62
+
63
+ def post(path, payload)
64
+ uri = URI("#{API_BASE}#{path}")
65
+ request = Net::HTTP::Post.new(uri)
66
+ request["Content-Type"] = "application/json"
67
+ request.body = JSON.generate(payload)
68
+
69
+ execute_request(uri, request)
70
+ end
71
+
72
+ def get(path)
73
+ uri = URI("#{API_BASE}#{path}")
74
+ request = Net::HTTP::Get.new(uri)
75
+ request["Content-Type"] = "application/json"
76
+
77
+ execute_request(uri, request)
78
+ end
79
+
80
+ def execute_request(uri, request)
81
+ http = Net::HTTP.new(uri.host, uri.port)
82
+ http.use_ssl = uri.scheme == "https"
83
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
84
+ http.open_timeout = OPEN_TIMEOUT
85
+ http.read_timeout = READ_TIMEOUT
86
+
87
+ response = http.request(request)
88
+
89
+ case response
90
+ when Net::HTTPSuccess
91
+ JSON.parse(response.body)
92
+ else
93
+ raise ApiError, "OSV API error: #{response.code} #{response.message}"
94
+ end
95
+ rescue JSON::ParserError => e
96
+ raise ApiError, "Invalid JSON response from OSV API: #{e.message}"
97
+ rescue Net::OpenTimeout, Net::ReadTimeout => e
98
+ raise ApiError, "OSV API timeout: #{e.message}"
99
+ rescue SocketError, Errno::ECONNREFUSED => e
100
+ raise ApiError, "OSV API connection error: #{e.message}"
101
+ rescue OpenSSL::SSL::SSLError => e
102
+ raise ApiError, "OSV API SSL error: #{e.message}"
103
+ end
104
+
105
+ def fetch_all_pages(response, original_payload)
106
+ vulns = response["vulns"] || []
107
+ page_token = response["next_page_token"]
108
+
109
+ while page_token
110
+ payload = original_payload.merge(page_token: page_token)
111
+ response = post("/query", payload)
112
+ vulns.concat(response["vulns"] || [])
113
+ page_token = response["next_page_token"]
114
+ end
115
+
116
+ vulns
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Brew
4
+ module Vulns
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Brew
4
+ module Vulns
5
+ class Vulnerability
6
+ attr_reader :id, :summary, :details, :severity, :aliases, :references, :affected
7
+
8
+ def initialize(data)
9
+ @id = data["id"]
10
+ @summary = data["summary"]
11
+ @details = data["details"]
12
+ @aliases = data["aliases"] || []
13
+ @references = data["references"] || []
14
+ @affected = data["affected"] || []
15
+ @severity = extract_severity(data)
16
+ end
17
+
18
+ def severity_display
19
+ severity&.upcase || "UNKNOWN"
20
+ end
21
+
22
+ def severity_level
23
+ case severity&.downcase
24
+ when "critical" then 4
25
+ when "high" then 3
26
+ when "medium" then 2
27
+ when "low" then 1
28
+ else 0
29
+ end
30
+ end
31
+
32
+ def cve_ids
33
+ ([id] + aliases).select { |a| a.start_with?("CVE-") }
34
+ end
35
+
36
+ def advisory_url
37
+ ref = references.find { |r| r["type"] == "ADVISORY" }
38
+ ref&.dig("url")
39
+ end
40
+
41
+ def fix_urls
42
+ references.select { |r| r["type"] == "FIX" }.map { |r| r["url"] }
43
+ end
44
+
45
+ def fixed_versions
46
+ versions = []
47
+ affected.each do |aff|
48
+ (aff["ranges"] || []).each do |range|
49
+ (range["events"] || []).each do |event|
50
+ versions << event["fixed"] if event["fixed"]
51
+ end
52
+ end
53
+ end
54
+ versions.uniq
55
+ end
56
+
57
+ def self.from_osv_list(vulns_data)
58
+ vulns_data.map { |data| new(data) }
59
+ end
60
+
61
+ private
62
+
63
+ def extract_severity(data)
64
+ if data["severity"]&.any?
65
+ sev = data["severity"].first
66
+ if sev["score"]&.include?("CVSS")
67
+ return severity_from_cvss(sev["score"])
68
+ end
69
+ end
70
+
71
+ if data.dig("database_specific", "severity")
72
+ return normalize_severity(data.dig("database_specific", "severity"))
73
+ end
74
+
75
+ data["affected"]&.each do |aff|
76
+ db_sev = aff.dig("database_specific", "severity")
77
+ return normalize_severity(db_sev) if db_sev
78
+ end
79
+
80
+ nil
81
+ end
82
+
83
+ def normalize_severity(severity)
84
+ return nil unless severity
85
+
86
+ case severity.downcase
87
+ when "critical" then "critical"
88
+ when "high" then "high"
89
+ when "moderate", "medium" then "medium"
90
+ when "low" then "low"
91
+ end
92
+ end
93
+
94
+ def severity_from_cvss(vector)
95
+ return nil unless vector
96
+ return nil unless vector.include?("CVSS:3")
97
+
98
+ metrics = parse_cvss_metrics(vector)
99
+ return nil if metrics.empty?
100
+
101
+ impact_high = %w[C I A].count { |m| metrics[m] == "H" }
102
+ network_attack = metrics["AV"] == "N"
103
+ no_privs = metrics["PR"] == "N"
104
+ no_interaction = metrics["UI"] == "N"
105
+
106
+ if impact_high >= 2 && network_attack && no_privs
107
+ "critical"
108
+ elsif impact_high >= 1 && network_attack
109
+ "high"
110
+ elsif impact_high >= 1 || (network_attack && no_privs && no_interaction)
111
+ "medium"
112
+ else
113
+ "low"
114
+ end
115
+ end
116
+
117
+ def parse_cvss_metrics(vector)
118
+ metrics = {}
119
+ vector.scan(%r{([A-Z]+):([A-Z])}).each do |key, value|
120
+ metrics[key] = value
121
+ end
122
+ metrics
123
+ end
124
+ end
125
+ end
126
+ end
data/lib/brew/vulns.rb ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "vulns/version"
4
+ require_relative "vulns/osv_client"
5
+ require_relative "vulns/formula"
6
+ require_relative "vulns/vulnerability"
7
+ require_relative "vulns/cli"
8
+
9
+ module Brew
10
+ module Vulns
11
+ class Error < StandardError; end
12
+ end
13
+ end
@@ -0,0 +1,6 @@
1
+ module Brew
2
+ module Vulns
3
+ VERSION: String
4
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
5
+ end
6
+ end
metadata ADDED
@@ -0,0 +1,60 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: brew-vulns
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Andrew Nesbitt
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: A Homebrew subcommand that checks installed packages for vulnerabilities
13
+ via osv.dev
14
+ email:
15
+ - andrewnez@gmail.com
16
+ executables:
17
+ - brew-vulns
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - ".ruby-version"
22
+ - CHANGELOG.md
23
+ - CODE_OF_CONDUCT.md
24
+ - Formula/brew-vulns.rb
25
+ - LICENSE
26
+ - README.md
27
+ - Rakefile
28
+ - exe/brew-vulns
29
+ - lib/brew/vulns.rb
30
+ - lib/brew/vulns/cli.rb
31
+ - lib/brew/vulns/formula.rb
32
+ - lib/brew/vulns/osv_client.rb
33
+ - lib/brew/vulns/version.rb
34
+ - lib/brew/vulns/vulnerability.rb
35
+ - sig/brew/vulns.rbs
36
+ homepage: https://github.com/andrewnesbitt/brew-vulns
37
+ licenses:
38
+ - MIT
39
+ metadata:
40
+ homepage_uri: https://github.com/andrewnesbitt/brew-vulns
41
+ source_code_uri: https://github.com/andrewnesbitt/brew-vulns
42
+ changelog_uri: https://github.com/andrewnesbitt/brew-vulns/blob/main/CHANGELOG.md
43
+ rdoc_options: []
44
+ require_paths:
45
+ - lib
46
+ required_ruby_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: 3.2.0
51
+ required_rubygems_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ requirements: []
57
+ rubygems_version: 4.0.3
58
+ specification_version: 4
59
+ summary: Check Homebrew packages for known vulnerabilities
60
+ test_files: []