brew-vulns 0.2.2 → 0.3.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/.rubocop.yml +242 -0
- data/.ruby-version +1 -1
- data/CHANGELOG.md +20 -0
- data/Formula/brew-vulns.rb +3 -3
- data/README.md +19 -12
- data/Rakefile +5 -3
- data/examples/README.md +9 -0
- data/examples/pr-vuln-check.yml +55 -0
- data/examples/scan-all.yml +45 -0
- data/lib/brew/vulns/cli.rb +63 -22
- data/lib/brew/vulns/formula.rb +13 -34
- data/lib/brew/vulns/osv_client.rb +14 -0
- data/lib/brew/vulns/version.rb +1 -1
- data/lib/brew/vulns/vulnerability.rb +57 -42
- metadata +24 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 393c81aa55ad74bc34018843abde797c2067c055fd669d8669712a32c2e6313c
|
|
4
|
+
data.tar.gz: a95b21cd1a777076b06b0a6b507f0429c7fde11476b193a712aae67784332d49
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 718026189245d3ed3bcc08fc1829610ab1d5752b44bead5f9ac9b4c7718aac88074c0f99acc77c6e058cc182932a3ee05593efd9c26035411d4199044686b017
|
|
7
|
+
data.tar.gz: fe75aa4ff545a67a8cfa361512393a4b9c64a9ad2edeaa7958fd4495424e28287c5363e2f99595ff751bdcb14a0cb618703773e5874ccda4271917a9289fe31c
|
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
# This file is synced from `Homebrew/brew` by the `.github` repository, do not modify it directly.
|
|
2
|
+
---
|
|
3
|
+
AllCops:
|
|
4
|
+
TargetRubyVersion: 4.0
|
|
5
|
+
NewCops: enable
|
|
6
|
+
Include:
|
|
7
|
+
- "**/*.rbi"
|
|
8
|
+
Exclude:
|
|
9
|
+
- Homebrew/sorbet/rbi/{annotations,dsl,gems}/**/*.rbi
|
|
10
|
+
- Homebrew/sorbet/rbi/parser*.rbi
|
|
11
|
+
- Homebrew/bin/*
|
|
12
|
+
- Homebrew/vendor/**/*
|
|
13
|
+
- Taps/*/*/vendor/**/*
|
|
14
|
+
- "**/.github/copilot-instructions.md"
|
|
15
|
+
- "**/vendor/**/*"
|
|
16
|
+
SuggestExtensions:
|
|
17
|
+
rubocop-minitest: false
|
|
18
|
+
Layout/ArgumentAlignment:
|
|
19
|
+
Exclude:
|
|
20
|
+
- Taps/*/*/*.rb
|
|
21
|
+
- "/**/Formula/**/*.rb"
|
|
22
|
+
- "**/Formula/**/*.rb"
|
|
23
|
+
Layout/CaseIndentation:
|
|
24
|
+
EnforcedStyle: end
|
|
25
|
+
Layout/FirstArrayElementIndentation:
|
|
26
|
+
EnforcedStyle: consistent
|
|
27
|
+
Layout/FirstHashElementIndentation:
|
|
28
|
+
EnforcedStyle: consistent
|
|
29
|
+
Layout/EndAlignment:
|
|
30
|
+
EnforcedStyleAlignWith: start_of_line
|
|
31
|
+
Layout/HashAlignment:
|
|
32
|
+
EnforcedHashRocketStyle: table
|
|
33
|
+
EnforcedColonStyle: table
|
|
34
|
+
Layout/LeadingCommentSpace:
|
|
35
|
+
Exclude:
|
|
36
|
+
- Taps/*/*/cmd/*.rb
|
|
37
|
+
Layout/LineLength:
|
|
38
|
+
Max: 118
|
|
39
|
+
AllowedPatterns:
|
|
40
|
+
- "#: "
|
|
41
|
+
- ' url "'
|
|
42
|
+
- ' mirror "'
|
|
43
|
+
- " plist_options "
|
|
44
|
+
- ' executable: "'
|
|
45
|
+
- ' font "'
|
|
46
|
+
- ' homepage "'
|
|
47
|
+
- ' name "'
|
|
48
|
+
- ' pkg "'
|
|
49
|
+
- ' pkgutil: "'
|
|
50
|
+
- " sha256 cellar: "
|
|
51
|
+
- " sha256 "
|
|
52
|
+
- "#{language}"
|
|
53
|
+
- "#{version."
|
|
54
|
+
- ' "/Library/Application Support/'
|
|
55
|
+
- "\"/Library/Caches/"
|
|
56
|
+
- "\"/Library/PreferencePanes/"
|
|
57
|
+
- ' "~/Library/Application Support/'
|
|
58
|
+
- "\"~/Library/Caches/"
|
|
59
|
+
- "\"~/Library/Containers"
|
|
60
|
+
- "\"~/Application Support"
|
|
61
|
+
- " was verified as official when first introduced to the cask"
|
|
62
|
+
Layout/SpaceAroundOperators:
|
|
63
|
+
Enabled: false
|
|
64
|
+
Layout/SpaceBeforeBrackets:
|
|
65
|
+
Exclude:
|
|
66
|
+
- "**/*_spec.rb"
|
|
67
|
+
- Taps/*/*/*.rb
|
|
68
|
+
- "/**/{Formula,Casks}/**/*.rb"
|
|
69
|
+
- "**/{Formula,Casks}/**/*.rb"
|
|
70
|
+
Lint/AmbiguousBlockAssociation:
|
|
71
|
+
Enabled: false
|
|
72
|
+
Lint/DuplicateBranch:
|
|
73
|
+
Exclude:
|
|
74
|
+
- Taps/*/*/*.rb
|
|
75
|
+
- "/**/{Formula,Casks}/**/*.rb"
|
|
76
|
+
- "**/{Formula,Casks}/**/*.rb"
|
|
77
|
+
Lint/ParenthesesAsGroupedExpression:
|
|
78
|
+
Exclude:
|
|
79
|
+
- Taps/*/*/*.rb
|
|
80
|
+
- "/**/Formula/**/*.rb"
|
|
81
|
+
- "**/Formula/**/*.rb"
|
|
82
|
+
Lint/UnusedMethodArgument:
|
|
83
|
+
AllowUnusedKeywordArguments: true
|
|
84
|
+
Metrics:
|
|
85
|
+
Enabled: false
|
|
86
|
+
Naming/BlockForwarding:
|
|
87
|
+
Enabled: false
|
|
88
|
+
Naming/FileName:
|
|
89
|
+
Regex: !ruby/regexp /^[\w\@\-\+\.]+(\.rb)?$/
|
|
90
|
+
Naming/HeredocDelimiterNaming:
|
|
91
|
+
ForbiddenDelimiters:
|
|
92
|
+
- END, EOD, EOF
|
|
93
|
+
Naming/InclusiveLanguage:
|
|
94
|
+
CheckStrings: true
|
|
95
|
+
FlaggedTerms:
|
|
96
|
+
slave:
|
|
97
|
+
AllowedRegex:
|
|
98
|
+
- gitslave
|
|
99
|
+
- log_slave
|
|
100
|
+
- ssdb_slave
|
|
101
|
+
- var_slave
|
|
102
|
+
- patches/13_fix_scope_for_show_slave_status_data.patch
|
|
103
|
+
Naming/MethodName:
|
|
104
|
+
AllowedPatterns:
|
|
105
|
+
- "\\A(fetch_)?HEAD\\?\\Z"
|
|
106
|
+
Naming/MethodParameterName:
|
|
107
|
+
inherit_mode:
|
|
108
|
+
merge:
|
|
109
|
+
- AllowedNames
|
|
110
|
+
Naming/PredicateMethod:
|
|
111
|
+
AllowBangMethods: true
|
|
112
|
+
Naming/VariableNumber:
|
|
113
|
+
Enabled: false
|
|
114
|
+
Style/AndOr:
|
|
115
|
+
EnforcedStyle: always
|
|
116
|
+
Style/ArgumentsForwarding:
|
|
117
|
+
Enabled: false
|
|
118
|
+
Style/AutoResourceCleanup:
|
|
119
|
+
Enabled: true
|
|
120
|
+
Style/BarePercentLiterals:
|
|
121
|
+
EnforcedStyle: percent_q
|
|
122
|
+
Style/BlockDelimiters:
|
|
123
|
+
BracesRequiredMethods:
|
|
124
|
+
- sig
|
|
125
|
+
Style/ClassAndModuleChildren:
|
|
126
|
+
Exclude:
|
|
127
|
+
- "**/*.rbi"
|
|
128
|
+
Style/CollectionMethods:
|
|
129
|
+
Enabled: true
|
|
130
|
+
Style/DisableCopsWithinSourceCodeDirective:
|
|
131
|
+
Enabled: true
|
|
132
|
+
Include:
|
|
133
|
+
- Taps/*/*/*.rb
|
|
134
|
+
- "/**/{Formula,Casks}/**/*.rb"
|
|
135
|
+
- "**/{Formula,Casks}/**/*.rb"
|
|
136
|
+
Style/Documentation:
|
|
137
|
+
Exclude:
|
|
138
|
+
- Taps/**/*
|
|
139
|
+
- "/**/{Formula,Casks}/**/*.rb"
|
|
140
|
+
- "**/{Formula,Casks}/**/*.rb"
|
|
141
|
+
- "**/*.rbi"
|
|
142
|
+
Style/EmptyMethod:
|
|
143
|
+
Exclude:
|
|
144
|
+
- "**/*.rbi"
|
|
145
|
+
Style/FetchEnvVar:
|
|
146
|
+
Exclude:
|
|
147
|
+
- Taps/*/*/*.rb
|
|
148
|
+
- "/**/Formula/**/*.rb"
|
|
149
|
+
- "**/Formula/**/*.rb"
|
|
150
|
+
Style/OneClassPerFile:
|
|
151
|
+
Exclude:
|
|
152
|
+
- "**/*.rbi"
|
|
153
|
+
- Taps/*/*/*.rb
|
|
154
|
+
- "/**/Abstract/**/*.rb"
|
|
155
|
+
- "**/Abstract/**/*.rb"
|
|
156
|
+
- "/**/Formula/**/*.rb"
|
|
157
|
+
- "**/Formula/**/*.rb"
|
|
158
|
+
- "/**/developer/bin/*"
|
|
159
|
+
- "**/developer/bin/*"
|
|
160
|
+
Style/FrozenStringLiteralComment:
|
|
161
|
+
EnforcedStyle: always
|
|
162
|
+
Exclude:
|
|
163
|
+
- Taps/*/*/*.rb
|
|
164
|
+
- "/**/{Formula,Casks}/**/*.rb"
|
|
165
|
+
- "**/{Formula,Casks}/**/*.rb"
|
|
166
|
+
- Homebrew/test/**/Casks/**/*.rb
|
|
167
|
+
- "**/*.rbi"
|
|
168
|
+
- "**/Brewfile"
|
|
169
|
+
Style/GuardClause:
|
|
170
|
+
Exclude:
|
|
171
|
+
- Taps/*/*/*.rb
|
|
172
|
+
- "/**/{Formula,Casks}/**/*.rb"
|
|
173
|
+
- "**/{Formula,Casks}/**/*.rb"
|
|
174
|
+
Style/HashAsLastArrayItem:
|
|
175
|
+
Exclude:
|
|
176
|
+
- Taps/*/*/*.rb
|
|
177
|
+
- "/**/Formula/**/*.rb"
|
|
178
|
+
- "**/Formula/**/*.rb"
|
|
179
|
+
Style/InverseMethods:
|
|
180
|
+
InverseMethods:
|
|
181
|
+
:blank?: :present?
|
|
182
|
+
Style/InvertibleUnlessCondition:
|
|
183
|
+
Enabled: true
|
|
184
|
+
InverseMethods:
|
|
185
|
+
:==: :!=
|
|
186
|
+
:zero?:
|
|
187
|
+
:blank?: :present?
|
|
188
|
+
Style/ItBlockParameter:
|
|
189
|
+
EnforcedStyle: only_numbered_parameters
|
|
190
|
+
Style/MutableConstant:
|
|
191
|
+
EnforcedStyle: strict
|
|
192
|
+
Style/NumericLiteralPrefix:
|
|
193
|
+
EnforcedOctalStyle: zero_only
|
|
194
|
+
Style/NumericLiterals:
|
|
195
|
+
MinDigits: 11
|
|
196
|
+
Strict: true
|
|
197
|
+
Style/OpenStructUse:
|
|
198
|
+
Exclude:
|
|
199
|
+
- Taps/**/*
|
|
200
|
+
Style/OptionalBooleanParameter:
|
|
201
|
+
AllowedMethods:
|
|
202
|
+
- respond_to?
|
|
203
|
+
- respond_to_missing?
|
|
204
|
+
Style/RescueStandardError:
|
|
205
|
+
EnforcedStyle: implicit
|
|
206
|
+
Style/ReturnNil:
|
|
207
|
+
Enabled: true
|
|
208
|
+
Style/StderrPuts:
|
|
209
|
+
Enabled: false
|
|
210
|
+
Style/StringConcatenation:
|
|
211
|
+
Exclude:
|
|
212
|
+
- Taps/*/*/*.rb
|
|
213
|
+
- "/**/{Formula,Casks}/**/*.rb"
|
|
214
|
+
- "**/{Formula,Casks}/**/*.rb"
|
|
215
|
+
Style/StringLiterals:
|
|
216
|
+
EnforcedStyle: double_quotes
|
|
217
|
+
Style/StringLiteralsInInterpolation:
|
|
218
|
+
EnforcedStyle: double_quotes
|
|
219
|
+
Style/StringMethods:
|
|
220
|
+
Enabled: true
|
|
221
|
+
Style/SuperWithArgsParentheses:
|
|
222
|
+
Enabled: false
|
|
223
|
+
Style/SymbolArray:
|
|
224
|
+
EnforcedStyle: brackets
|
|
225
|
+
Style/TernaryParentheses:
|
|
226
|
+
EnforcedStyle: require_parentheses_when_complex
|
|
227
|
+
Style/TopLevelMethodDefinition:
|
|
228
|
+
Enabled: true
|
|
229
|
+
Exclude:
|
|
230
|
+
- Taps/**/*
|
|
231
|
+
Style/TrailingCommaInArguments:
|
|
232
|
+
EnforcedStyleForMultiline: comma
|
|
233
|
+
Style/TrailingCommaInArrayLiteral:
|
|
234
|
+
EnforcedStyleForMultiline: comma
|
|
235
|
+
Style/TrailingCommaInHashLiteral:
|
|
236
|
+
EnforcedStyleForMultiline: comma
|
|
237
|
+
Style/UnlessLogicalOperators:
|
|
238
|
+
Enabled: true
|
|
239
|
+
EnforcedStyle: forbid_logical_operators
|
|
240
|
+
Style/WordArray:
|
|
241
|
+
MinSize: 4
|
|
242
|
+
|
data/.ruby-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
4.0.
|
|
1
|
+
4.0.5
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.3.0] - 2026-05-29
|
|
4
|
+
|
|
5
|
+
- Add `--all` flag to scan every formula in homebrew-core
|
|
6
|
+
- Accept one or more formula names as arguments to scan specific formulae, including ones that are not installed
|
|
7
|
+
- Exit with status 2 on errors so callers can distinguish errors from "vulnerabilities found" (exit 1)
|
|
8
|
+
- Add example GitHub Actions workflows for tap PR checks and full homebrew-core scans
|
|
9
|
+
- Compute severity bands from CVSS vector strings when OSV data does not provide a severity label
|
|
10
|
+
- Improve CVSS severity fallback handling when multiple score sources are present
|
|
11
|
+
- Handle unbounded `introduced: 0` OSV ranges and multi-interval SEMVER ranges correctly
|
|
12
|
+
- Fail closed (report as affected) when a version range comparison raises instead of silently skipping
|
|
13
|
+
- Sanitize ANSI/terminal escape sequences, carriage returns and backspaces from text output
|
|
14
|
+
- Cap concurrent requests when fetching vulnerability details to avoid unbounded thread spawning
|
|
15
|
+
- Cap OSV pagination at a fixed page limit to avoid unbounded loops on bad responses
|
|
16
|
+
- Set a `User-Agent` header on OSV API requests
|
|
17
|
+
|
|
18
|
+
## [0.2.3] - 2026-02-05
|
|
19
|
+
|
|
20
|
+
- Move repository to the Homebrew organisation and update install instructions, formula and links accordingly
|
|
21
|
+
- Internal: shared CI/lint configuration sync and dependency updates
|
|
22
|
+
|
|
3
23
|
## [0.2.2] - 2026-01-25
|
|
4
24
|
|
|
5
25
|
- Add retry logic to OSV API requests (up to 3 attempts on timeout or connection errors)
|
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
|
-
homepage "https://github.com/
|
|
4
|
-
url "https://github.com/
|
|
5
|
-
sha256 "
|
|
3
|
+
homepage "https://github.com/Homebrew/homebrew-brew-vulns"
|
|
4
|
+
url "https://github.com/Homebrew/homebrew-brew-vulns/archive/refs/tags/v0.2.3.tar.gz"
|
|
5
|
+
sha256 "1f1bdc60daeeded30d22026ba80e66854a95a299f92392c8624997b75d0f971e"
|
|
6
6
|
license "MIT"
|
|
7
7
|
|
|
8
8
|
depends_on "ruby"
|
data/README.md
CHANGED
|
@@ -7,8 +7,7 @@ A Homebrew subcommand that checks installed packages for known vulnerabilities u
|
|
|
7
7
|
Via Homebrew:
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
brew
|
|
11
|
-
brew install brew-vulns
|
|
10
|
+
brew install homebrew/brew-vulns/brew-vulns
|
|
12
11
|
```
|
|
13
12
|
|
|
14
13
|
Or via RubyGems:
|
|
@@ -22,13 +21,14 @@ Once installed, the command is available as `brew vulns`.
|
|
|
22
21
|
## Usage
|
|
23
22
|
|
|
24
23
|
```bash
|
|
25
|
-
brew vulns [formula] [options]
|
|
24
|
+
brew vulns [formula...] [options]
|
|
26
25
|
```
|
|
27
26
|
|
|
28
27
|
### Options
|
|
29
28
|
|
|
30
29
|
| Flag | Long form | Description |
|
|
31
30
|
|------|-----------|-------------|
|
|
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
34
|
| `-j` | `--json` | Output results as JSON |
|
|
@@ -44,9 +44,15 @@ brew vulns [formula] [options]
|
|
|
44
44
|
# Check all installed packages
|
|
45
45
|
brew vulns
|
|
46
46
|
|
|
47
|
-
#
|
|
47
|
+
# Scan every formula in homebrew-core
|
|
48
|
+
brew vulns --all --json > vulns.json
|
|
49
|
+
|
|
50
|
+
# Check a specific formula (does not need to be installed)
|
|
48
51
|
brew vulns openssl
|
|
49
52
|
|
|
53
|
+
# Check several formulae at once
|
|
54
|
+
brew vulns vim curl jq
|
|
55
|
+
|
|
50
56
|
# Check a formula and its dependencies
|
|
51
57
|
brew vulns python --deps
|
|
52
58
|
|
|
@@ -83,7 +89,7 @@ brew vulns --help
|
|
|
83
89
|
|
|
84
90
|
## How it works
|
|
85
91
|
|
|
86
|
-
1. Reads
|
|
92
|
+
1. Reads Homebrew formulae via `brew info --json=v2` (installed packages by default, or any named formulae passed as arguments)
|
|
87
93
|
2. Extracts the repository URL and version tag from each formula's source URL
|
|
88
94
|
3. Queries the OSV API using the GIT ecosystem to find known vulnerabilities
|
|
89
95
|
4. Reports any vulnerabilities found with their severity and CVE identifiers
|
|
@@ -109,9 +115,10 @@ Found 15 vulnerabilities in 3 packages
|
|
|
109
115
|
## Exit codes
|
|
110
116
|
|
|
111
117
|
- `0` - No vulnerabilities found
|
|
112
|
-
- `1` - Vulnerabilities found
|
|
118
|
+
- `1` - Vulnerabilities found
|
|
119
|
+
- `2` - An error occurred (network failure, `brew` failure, parse error)
|
|
113
120
|
|
|
114
|
-
This makes it suitable for use in CI/CD pipelines.
|
|
121
|
+
This makes it suitable for use in CI/CD pipelines. To let a job continue when vulnerabilities are found but still fail on scan errors, use `brew vulns ... || [ $? -eq 1 ]`.
|
|
115
122
|
|
|
116
123
|
## GitHub Actions
|
|
117
124
|
|
|
@@ -135,8 +142,7 @@ jobs:
|
|
|
135
142
|
run: gem install brew-vulns
|
|
136
143
|
|
|
137
144
|
- name: Run vulnerability scan
|
|
138
|
-
run: brew vulns --sarif > results.sarif
|
|
139
|
-
continue-on-error: true
|
|
145
|
+
run: brew vulns --sarif > results.sarif || [ $? -eq 1 ]
|
|
140
146
|
|
|
141
147
|
- name: Upload SARIF results
|
|
142
148
|
uses: github/codeql-action/upload-sarif@v3
|
|
@@ -168,8 +174,7 @@ jobs:
|
|
|
168
174
|
run: gem install brew-vulns
|
|
169
175
|
|
|
170
176
|
- name: Generate SBOM
|
|
171
|
-
run: brew vulns --cyclonedx > sbom.cdx.json
|
|
172
|
-
continue-on-error: true
|
|
177
|
+
run: brew vulns --cyclonedx > sbom.cdx.json || [ $? -eq 1 ]
|
|
173
178
|
|
|
174
179
|
- name: Submit to dependency graph
|
|
175
180
|
uses: evryfs/sbom-dependency-submission-action@v0
|
|
@@ -179,10 +184,12 @@ jobs:
|
|
|
179
184
|
|
|
180
185
|
This adds your Homebrew packages to the repository's dependency graph, enabling Dependabot alerts.
|
|
181
186
|
|
|
187
|
+
See [examples/](examples/) for workflows that check changed formulae on tap pull requests and publish a daily scan of all of homebrew-core.
|
|
188
|
+
|
|
182
189
|
## Development
|
|
183
190
|
|
|
184
191
|
```bash
|
|
185
|
-
git clone https://github.com/
|
|
192
|
+
git clone https://github.com/Homebrew/homebrew-brew-vulns
|
|
186
193
|
cd brew-vulns
|
|
187
194
|
bin/setup
|
|
188
195
|
rake test
|
data/Rakefile
CHANGED
|
@@ -5,7 +5,9 @@ require "minitest/test_task"
|
|
|
5
5
|
require "digest"
|
|
6
6
|
require "open-uri"
|
|
7
7
|
|
|
8
|
-
Minitest::TestTask.create
|
|
8
|
+
Minitest::TestTask.create do |t|
|
|
9
|
+
t.framework = %(require "test/test_helper.rb")
|
|
10
|
+
end
|
|
9
11
|
|
|
10
12
|
task default: :test
|
|
11
13
|
|
|
@@ -14,7 +16,7 @@ task :update_formula do
|
|
|
14
16
|
require_relative "lib/brew/vulns/version"
|
|
15
17
|
|
|
16
18
|
version = Brew::Vulns::VERSION
|
|
17
|
-
url = "https://github.com/
|
|
19
|
+
url = "https://github.com/Homebrew/homebrew-brew-vulns/archive/refs/tags/v#{version}.tar.gz"
|
|
18
20
|
formula_path = File.expand_path("Formula/brew-vulns.rb", __dir__)
|
|
19
21
|
|
|
20
22
|
puts "Downloading #{url}..."
|
|
@@ -23,7 +25,7 @@ task :update_formula do
|
|
|
23
25
|
puts "SHA256: #{sha256}"
|
|
24
26
|
|
|
25
27
|
formula = File.read(formula_path)
|
|
26
|
-
formula.gsub!(%r{url "https://github.com/
|
|
28
|
+
formula.gsub!(%r{url "https://github.com/Homebrew/homebrew-brew-vulns/archive/refs/tags/v[^"]+\.tar\.gz"},
|
|
27
29
|
"url \"#{url}\"")
|
|
28
30
|
formula.gsub!(/sha256 "[^"]+"/, "sha256 \"#{sha256}\"")
|
|
29
31
|
File.write(formula_path, formula)
|
data/examples/README.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Example workflows
|
|
2
|
+
|
|
3
|
+
GitHub Actions workflows showing how `brew vulns` can be wired into a Homebrew tap.
|
|
4
|
+
|
|
5
|
+
`pr-vuln-check.yml` runs on pull requests that touch `Formula/**/*.rb`. It works out which formulae changed, runs `brew vulns <names> --sarif` against them, and uploads the result to GitHub code scanning so vulnerabilities appear as annotations on the PR.
|
|
6
|
+
|
|
7
|
+
`scan-all.yml` runs daily, scans every formula in homebrew-core with `brew vulns --all --json`, and publishes `vulns.json` as an asset on a rolling `vulns` release. Downstream tooling (including Homebrew itself) can fetch that file rather than querying OSV directly.
|
|
8
|
+
|
|
9
|
+
Copy whichever you need into `.github/workflows/` in the target repository and adjust to taste.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Check formulae changed in a pull request for known vulnerabilities.
|
|
2
|
+
# Intended for use in a Homebrew tap (e.g. homebrew-core).
|
|
3
|
+
name: Vulnerability check
|
|
4
|
+
|
|
5
|
+
on:
|
|
6
|
+
pull_request:
|
|
7
|
+
paths:
|
|
8
|
+
- "Formula/**/*.rb"
|
|
9
|
+
|
|
10
|
+
permissions: {}
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
vulns:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
permissions:
|
|
16
|
+
contents: read
|
|
17
|
+
security-events: write
|
|
18
|
+
steps:
|
|
19
|
+
- name: Set up Homebrew
|
|
20
|
+
id: set-up-homebrew
|
|
21
|
+
uses: Homebrew/actions/setup-homebrew@main
|
|
22
|
+
|
|
23
|
+
- name: Install brew-vulns
|
|
24
|
+
run: brew install homebrew/brew-vulns/brew-vulns
|
|
25
|
+
|
|
26
|
+
- name: Detect changed formulae
|
|
27
|
+
id: changed
|
|
28
|
+
env:
|
|
29
|
+
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
|
30
|
+
run: |
|
|
31
|
+
cd "$(brew --repository "${GITHUB_REPOSITORY}")"
|
|
32
|
+
git fetch --quiet --depth=1 origin "${BASE_SHA}"
|
|
33
|
+
names=$(git diff --name-only --diff-filter=AM "${BASE_SHA}" -- Formula/ \
|
|
34
|
+
| xargs -r -n1 basename \
|
|
35
|
+
| sed 's/\.rb$//' \
|
|
36
|
+
| tr '\n' ' ')
|
|
37
|
+
echo "names=${names}" >> "${GITHUB_OUTPUT}"
|
|
38
|
+
|
|
39
|
+
- name: Check vulnerabilities
|
|
40
|
+
id: scan
|
|
41
|
+
if: steps.changed.outputs.names != ''
|
|
42
|
+
env:
|
|
43
|
+
FORMULAE: ${{ steps.changed.outputs.names }}
|
|
44
|
+
run: |
|
|
45
|
+
brew vulns ${FORMULAE} --sarif > vulns.sarif || [ $? -eq 1 ]
|
|
46
|
+
if [ -s vulns.sarif ]; then
|
|
47
|
+
echo "sarif=true" >> "${GITHUB_OUTPUT}"
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
- name: Upload SARIF
|
|
51
|
+
if: steps.scan.outputs.sarif == 'true'
|
|
52
|
+
uses: github/codeql-action/upload-sarif@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
|
|
53
|
+
with:
|
|
54
|
+
sarif_file: vulns.sarif
|
|
55
|
+
category: brew-vulns
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Scan every formula in homebrew-core for known vulnerabilities and
|
|
2
|
+
# publish the result as vulns.json on a rolling release.
|
|
3
|
+
name: Scan all formulae
|
|
4
|
+
|
|
5
|
+
on:
|
|
6
|
+
schedule:
|
|
7
|
+
- cron: "0 6 * * *"
|
|
8
|
+
workflow_dispatch:
|
|
9
|
+
|
|
10
|
+
permissions: {}
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
scan:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
permissions:
|
|
16
|
+
contents: write
|
|
17
|
+
steps:
|
|
18
|
+
- name: Set up Homebrew
|
|
19
|
+
uses: Homebrew/actions/setup-homebrew@main
|
|
20
|
+
|
|
21
|
+
- name: Install brew-vulns
|
|
22
|
+
run: brew install homebrew/brew-vulns/brew-vulns
|
|
23
|
+
|
|
24
|
+
- name: Scan
|
|
25
|
+
run: brew vulns --all --json > vulns.json
|
|
26
|
+
continue-on-error: true
|
|
27
|
+
|
|
28
|
+
- name: Validate output
|
|
29
|
+
run: ruby -rjson -e 'abort "vulns.json is not a JSON array" unless JSON.load_file("vulns.json").is_a?(Array)'
|
|
30
|
+
|
|
31
|
+
- name: Upload artifact
|
|
32
|
+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
33
|
+
with:
|
|
34
|
+
name: vulns
|
|
35
|
+
path: vulns.json
|
|
36
|
+
|
|
37
|
+
- name: Publish to release
|
|
38
|
+
env:
|
|
39
|
+
GH_TOKEN: ${{ github.token }}
|
|
40
|
+
GH_REPO: ${{ github.repository }}
|
|
41
|
+
run: |
|
|
42
|
+
if ! gh release view vulns >/dev/null 2>&1; then
|
|
43
|
+
gh release create vulns --title "Vulnerability data" --notes "Rolling vulnerability scan of homebrew-core"
|
|
44
|
+
fi
|
|
45
|
+
gh release upload vulns vulns.json --clobber
|
data/lib/brew/vulns/cli.rb
CHANGED
|
@@ -9,10 +9,13 @@ module Brew
|
|
|
9
9
|
|
|
10
10
|
DEFAULT_MAX_SUMMARY = 60
|
|
11
11
|
SEVERITY_LEVELS = { "low" => 1, "medium" => 2, "high" => 3, "critical" => 4 }.freeze
|
|
12
|
+
MAX_VULN_FETCH_THREADS = 15
|
|
13
|
+
FLAGS_WITH_VALUE = %w[-b --brewfile -m --max-summary -s --severity].freeze
|
|
12
14
|
|
|
13
15
|
def initialize(args)
|
|
14
16
|
@args = args
|
|
15
|
-
@
|
|
17
|
+
@formula_names = parse_formula_names(args)
|
|
18
|
+
@all = args.include?("--all")
|
|
16
19
|
@include_deps = args.include?("--deps") || args.include?("-d")
|
|
17
20
|
@json_output = args.include?("--json") || args.include?("-j")
|
|
18
21
|
@sarif_output = args.include?("--sarif")
|
|
@@ -23,6 +26,25 @@ module Brew
|
|
|
23
26
|
@brewfile = parse_brewfile_path(args)
|
|
24
27
|
end
|
|
25
28
|
|
|
29
|
+
def parse_formula_names(args)
|
|
30
|
+
names = []
|
|
31
|
+
skip_next = false
|
|
32
|
+
args.each do |arg|
|
|
33
|
+
if skip_next
|
|
34
|
+
skip_next = false
|
|
35
|
+
next
|
|
36
|
+
end
|
|
37
|
+
if FLAGS_WITH_VALUE.include?(arg)
|
|
38
|
+
skip_next = true
|
|
39
|
+
next
|
|
40
|
+
end
|
|
41
|
+
next if arg.start_with?("-")
|
|
42
|
+
|
|
43
|
+
names << arg
|
|
44
|
+
end
|
|
45
|
+
names
|
|
46
|
+
end
|
|
47
|
+
|
|
26
48
|
def parse_max_summary(args)
|
|
27
49
|
args.each_with_index do |arg, idx|
|
|
28
50
|
if arg == "--max-summary" || arg == "-m"
|
|
@@ -69,7 +91,7 @@ module Brew
|
|
|
69
91
|
|
|
70
92
|
formulae = load_formulae
|
|
71
93
|
if formulae.empty?
|
|
72
|
-
puts "No
|
|
94
|
+
puts "No formulae found."
|
|
73
95
|
return 0
|
|
74
96
|
end
|
|
75
97
|
|
|
@@ -86,24 +108,26 @@ module Brew
|
|
|
86
108
|
output_results(results, formulae)
|
|
87
109
|
rescue OsvClient::Error => e
|
|
88
110
|
$stderr.puts "Error querying OSV: #{e.message}"
|
|
89
|
-
|
|
111
|
+
2
|
|
90
112
|
rescue Error => e
|
|
91
113
|
$stderr.puts "Error: #{e.message}"
|
|
92
|
-
|
|
114
|
+
2
|
|
93
115
|
rescue JSON::ParserError => e
|
|
94
116
|
$stderr.puts "Error parsing brew output: #{e.message}"
|
|
95
|
-
|
|
117
|
+
2
|
|
96
118
|
end
|
|
97
119
|
|
|
98
120
|
private
|
|
99
121
|
|
|
100
122
|
def load_formulae
|
|
101
|
-
if @
|
|
123
|
+
if @all
|
|
124
|
+
Formula.load_all
|
|
125
|
+
elsif @brewfile
|
|
102
126
|
Formula.load_from_brewfile(@brewfile, include_deps: @include_deps)
|
|
103
|
-
elsif @
|
|
104
|
-
Formula.
|
|
127
|
+
elsif @formula_names.any?
|
|
128
|
+
Formula.load_named(@formula_names, include_deps: @include_deps)
|
|
105
129
|
else
|
|
106
|
-
Formula.load_installed
|
|
130
|
+
Formula.load_installed
|
|
107
131
|
end
|
|
108
132
|
end
|
|
109
133
|
|
|
@@ -118,10 +142,13 @@ module Brew
|
|
|
118
142
|
batch_vulns = vuln_results[idx] || []
|
|
119
143
|
next if batch_vulns.empty?
|
|
120
144
|
|
|
121
|
-
|
|
122
|
-
|
|
145
|
+
full_vulns = batch_vulns.each_slice(MAX_VULN_FETCH_THREADS).flat_map do |slice|
|
|
146
|
+
threads = slice.map do |v|
|
|
147
|
+
Thread.new { client.get_vulnerability(v["id"]) }
|
|
148
|
+
end
|
|
149
|
+
threads.map(&:value)
|
|
123
150
|
end
|
|
124
|
-
|
|
151
|
+
|
|
125
152
|
vulns = Vulnerability.from_osv_list(full_vulns)
|
|
126
153
|
|
|
127
154
|
version = formula.tag || formula.version
|
|
@@ -251,7 +278,7 @@ module Brew
|
|
|
251
278
|
driver: Sarif::ToolComponent.new(
|
|
252
279
|
name: "brew-vulns",
|
|
253
280
|
version: VERSION,
|
|
254
|
-
information_uri: "https://github.com/
|
|
281
|
+
information_uri: "https://github.com/Homebrew/homebrew-brew-vulns",
|
|
255
282
|
rules: rules.uniq { |r| r.id }
|
|
256
283
|
)
|
|
257
284
|
),
|
|
@@ -283,24 +310,26 @@ module Brew
|
|
|
283
310
|
sorted = results.sort_by { |_, vulns| -vulns.map(&:severity_level).max }
|
|
284
311
|
|
|
285
312
|
sorted.each do |formula, vulns|
|
|
286
|
-
puts "#{formula.name} (#{formula.version})"
|
|
313
|
+
puts "#{sanitize_terminal_escapes(formula.name)} (#{sanitize_terminal_escapes(formula.version)})"
|
|
287
314
|
vulns.sort_by { |v| -v.severity_level }.each do |vuln|
|
|
288
315
|
total_vulns += 1
|
|
289
316
|
severity = colorize_severity(vuln.severity_display)
|
|
290
317
|
|
|
291
|
-
line = " #{vuln.id} (#{severity})"
|
|
318
|
+
line = " #{sanitize_terminal_escapes(vuln.id)} (#{severity})"
|
|
292
319
|
if vuln.summary
|
|
293
|
-
|
|
294
|
-
|
|
320
|
+
sanitized_summary = sanitize_terminal_escapes(vuln.summary)
|
|
321
|
+
summary = if @max_summary > 0 && sanitized_summary.length > @max_summary
|
|
322
|
+
"#{sanitized_summary.slice(0, @max_summary)}..."
|
|
295
323
|
else
|
|
296
|
-
|
|
324
|
+
sanitized_summary
|
|
297
325
|
end
|
|
298
326
|
line = "#{line} - #{summary}"
|
|
299
327
|
end
|
|
300
328
|
puts line
|
|
301
329
|
|
|
302
330
|
if vuln.fixed_versions.any?
|
|
303
|
-
|
|
331
|
+
fixed_versions = vuln.fixed_versions.map { |version| sanitize_terminal_escapes(version) }
|
|
332
|
+
puts " Fixed in: #{fixed_versions.join(", ")}"
|
|
304
333
|
end
|
|
305
334
|
end
|
|
306
335
|
puts
|
|
@@ -310,6 +339,15 @@ module Brew
|
|
|
310
339
|
1
|
|
311
340
|
end
|
|
312
341
|
|
|
342
|
+
def sanitize_terminal_escapes(text)
|
|
343
|
+
text.to_s
|
|
344
|
+
.gsub(/\e\][^\a\e]*(?:\a|\e\\)/, "")
|
|
345
|
+
.gsub(/\u009d[^\a\u009c]*(?:\a|\u009c)/, "")
|
|
346
|
+
.gsub(/\u009b[0-?]*[ -\/]*[@-~]/, "")
|
|
347
|
+
.gsub(/\e\[[0-?]*[ -\/]*[@-~]/, "")
|
|
348
|
+
.delete("\e\b\r\u0007\u0080-\u009f")
|
|
349
|
+
end
|
|
350
|
+
|
|
313
351
|
def colorize_severity(severity)
|
|
314
352
|
return severity unless $stdout.tty?
|
|
315
353
|
|
|
@@ -324,14 +362,15 @@ module Brew
|
|
|
324
362
|
|
|
325
363
|
def print_help
|
|
326
364
|
puts <<~HELP
|
|
327
|
-
Usage: brew vulns [formula] [options]
|
|
365
|
+
Usage: brew vulns [formula...] [options]
|
|
328
366
|
|
|
329
|
-
Check
|
|
367
|
+
Check Homebrew packages for known vulnerabilities via osv.dev.
|
|
330
368
|
|
|
331
369
|
Arguments:
|
|
332
|
-
formula Check only
|
|
370
|
+
formula Check only the named formulae (optional, does not need to be installed)
|
|
333
371
|
|
|
334
372
|
Options:
|
|
373
|
+
--all Scan every formula in homebrew-core
|
|
335
374
|
-b, --brewfile PATH Scan packages from a Brewfile (default: ./Brewfile)
|
|
336
375
|
-d, --deps Include dependencies when checking a specific formula or Brewfile
|
|
337
376
|
-j, --json Output results as JSON
|
|
@@ -343,7 +382,9 @@ module Brew
|
|
|
343
382
|
|
|
344
383
|
Examples:
|
|
345
384
|
brew vulns Check all installed packages
|
|
385
|
+
brew vulns --all --json Scan every homebrew-core formula, output JSON
|
|
346
386
|
brew vulns openssl Check only openssl
|
|
387
|
+
brew vulns vim curl jq Check several formulae at once
|
|
347
388
|
brew vulns vim --deps Check vim and its dependencies
|
|
348
389
|
brew vulns --brewfile Scan packages listed in ./Brewfile
|
|
349
390
|
brew vulns -b ~/project/Brewfile Scan a specific Brewfile
|
data/lib/brew/vulns/formula.rb
CHANGED
|
@@ -50,51 +50,23 @@ module Brew
|
|
|
50
50
|
{ repo_url: repo_url, version: tag, name: name }
|
|
51
51
|
end
|
|
52
52
|
|
|
53
|
-
def self.
|
|
54
|
-
json, status = Open3.capture2("brew", "info", "--json=v2", "--
|
|
53
|
+
def self.load_all
|
|
54
|
+
json, status = Open3.capture2("brew", "info", "--json=v2", "--eval-all")
|
|
55
55
|
raise Error, "brew info failed with status #{status.exitstatus}" unless status.success?
|
|
56
56
|
|
|
57
57
|
data = JSON.parse(json)
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if formula_filter
|
|
61
|
-
formulae.select! { |f| f.name == formula_filter || f.name.split("@").first == formula_filter }
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
formulae
|
|
58
|
+
data["formulae"].map { |f| new(f) }
|
|
65
59
|
end
|
|
66
60
|
|
|
67
|
-
def self.
|
|
61
|
+
def self.load_installed
|
|
68
62
|
json, status = Open3.capture2("brew", "info", "--json=v2", "--installed")
|
|
69
63
|
raise Error, "brew info failed with status #{status.exitstatus}" unless status.success?
|
|
70
64
|
|
|
71
65
|
data = JSON.parse(json)
|
|
72
|
-
|
|
73
|
-
formulae_by_name = all_formulae.each_with_object({}) { |f, h| h[f.name] = f }
|
|
74
|
-
|
|
75
|
-
if formula_filter
|
|
76
|
-
filtered = all_formulae.select { |f| f.name == formula_filter || f.name.split("@").first == formula_filter }
|
|
77
|
-
return [] if filtered.empty?
|
|
78
|
-
|
|
79
|
-
deps_output, = Open3.capture2("brew", "deps", "--installed", formula_filter)
|
|
80
|
-
dep_names = deps_output.split("\n").map(&:strip)
|
|
81
|
-
|
|
82
|
-
result = filtered.each_with_object({}) { |f, h| h[f.name] = f }
|
|
83
|
-
dep_names.each do |dep_name|
|
|
84
|
-
dep = formulae_by_name[dep_name]
|
|
85
|
-
result[dep_name] = dep if dep && !result[dep_name]
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
result.values
|
|
89
|
-
else
|
|
90
|
-
all_formulae
|
|
91
|
-
end
|
|
66
|
+
data["formulae"].map { |f| new(f) }
|
|
92
67
|
end
|
|
93
68
|
|
|
94
|
-
def self.
|
|
95
|
-
raise Error, "Brewfile not found: #{brewfile_path}" unless File.exist?(brewfile_path)
|
|
96
|
-
|
|
97
|
-
formula_names = parse_brewfile(brewfile_path)
|
|
69
|
+
def self.load_named(formula_names, include_deps: false)
|
|
98
70
|
return [] if formula_names.empty?
|
|
99
71
|
|
|
100
72
|
json, status = Open3.capture2("brew", "info", "--json=v2", *formula_names)
|
|
@@ -124,6 +96,13 @@ module Brew
|
|
|
124
96
|
formulae.uniq { |f| f.name }
|
|
125
97
|
end
|
|
126
98
|
|
|
99
|
+
def self.load_from_brewfile(brewfile_path, include_deps: false)
|
|
100
|
+
raise Error, "Brewfile not found: #{brewfile_path}" unless File.exist?(brewfile_path)
|
|
101
|
+
|
|
102
|
+
formula_names = parse_brewfile(brewfile_path)
|
|
103
|
+
load_named(formula_names, include_deps: include_deps)
|
|
104
|
+
end
|
|
105
|
+
|
|
127
106
|
def self.parse_brewfile(brewfile_path)
|
|
128
107
|
output, status = Open3.capture2("brew", "bundle", "list", "--file=#{brewfile_path}", "--formula")
|
|
129
108
|
raise Error, "brew bundle list failed with status #{status.exitstatus}" unless status.success?
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "net/http"
|
|
4
4
|
require "json"
|
|
5
5
|
require "uri"
|
|
6
|
+
require "brew/vulns/version"
|
|
6
7
|
|
|
7
8
|
module Brew
|
|
8
9
|
module Vulns
|
|
@@ -17,6 +18,8 @@ module Brew
|
|
|
17
18
|
class Error < StandardError; end
|
|
18
19
|
class ApiError < Error; end
|
|
19
20
|
|
|
21
|
+
USER_AGENT = "brew-vulns/#{Brew::Vulns::VERSION} (+https://github.com/Homebrew/homebrew-brew-vulns)"
|
|
22
|
+
|
|
20
23
|
def query(repo_url:, version:)
|
|
21
24
|
payload = {
|
|
22
25
|
package: {
|
|
@@ -66,6 +69,7 @@ module Brew
|
|
|
66
69
|
uri = URI("#{API_BASE}#{path}")
|
|
67
70
|
request = Net::HTTP::Post.new(uri)
|
|
68
71
|
request["Content-Type"] = "application/json"
|
|
72
|
+
request["User-Agent"] = USER_AGENT
|
|
69
73
|
request.body = JSON.generate(payload)
|
|
70
74
|
|
|
71
75
|
execute_request(uri, request)
|
|
@@ -75,6 +79,7 @@ module Brew
|
|
|
75
79
|
uri = URI("#{API_BASE}#{path}")
|
|
76
80
|
request = Net::HTTP::Get.new(uri)
|
|
77
81
|
request["Content-Type"] = "application/json"
|
|
82
|
+
request["User-Agent"] = USER_AGENT
|
|
78
83
|
|
|
79
84
|
execute_request(uri, request)
|
|
80
85
|
end
|
|
@@ -117,15 +122,24 @@ module Brew
|
|
|
117
122
|
end
|
|
118
123
|
end
|
|
119
124
|
|
|
125
|
+
MAX_PAGES = 100
|
|
126
|
+
|
|
120
127
|
def fetch_all_pages(response, original_payload)
|
|
121
128
|
vulns = response["vulns"] || []
|
|
122
129
|
page_token = response["next_page_token"]
|
|
130
|
+
page_count = 1
|
|
123
131
|
|
|
124
132
|
while page_token
|
|
133
|
+
if page_count >= MAX_PAGES
|
|
134
|
+
raise ApiError,
|
|
135
|
+
"OSV API returned more than #{MAX_PAGES} pages of results; aborting to avoid an unbounded loop"
|
|
136
|
+
end
|
|
137
|
+
|
|
125
138
|
payload = original_payload.merge(page_token: page_token)
|
|
126
139
|
response = post("/query", payload)
|
|
127
140
|
vulns.concat(response["vulns"] || [])
|
|
128
141
|
page_token = response["next_page_token"]
|
|
142
|
+
page_count += 1
|
|
129
143
|
end
|
|
130
144
|
|
|
131
145
|
vulns
|
data/lib/brew/vulns/version.rb
CHANGED
|
@@ -2,10 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
require "purl"
|
|
4
4
|
require "vers"
|
|
5
|
+
require "cvss_suite"
|
|
5
6
|
|
|
6
7
|
module Brew
|
|
7
8
|
module Vulns
|
|
8
9
|
class Vulnerability
|
|
10
|
+
CVSS_TYPE_PRIORITY = {
|
|
11
|
+
"CVSS_V4" => 4,
|
|
12
|
+
"CVSS_V3" => 3,
|
|
13
|
+
"CVSS_V2" => 2
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
9
16
|
attr_reader :id, :summary, :details, :severity, :aliases, :references, :affected
|
|
10
17
|
|
|
11
18
|
def initialize(data)
|
|
@@ -76,9 +83,9 @@ module Brew
|
|
|
76
83
|
|
|
77
84
|
def extract_severity(data)
|
|
78
85
|
if data["severity"]&.any?
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
return
|
|
86
|
+
cvss_severities(data["severity"]).each do |sev|
|
|
87
|
+
cvss_severity = severity_from_cvss(sev["score"])
|
|
88
|
+
return cvss_severity if cvss_severity
|
|
82
89
|
end
|
|
83
90
|
end
|
|
84
91
|
|
|
@@ -97,6 +104,12 @@ module Brew
|
|
|
97
104
|
nil
|
|
98
105
|
end
|
|
99
106
|
|
|
107
|
+
def cvss_severities(severities)
|
|
108
|
+
severities
|
|
109
|
+
.select { |sev| CVSS_TYPE_PRIORITY.key?(sev["type"]) }
|
|
110
|
+
.sort_by { |sev| -CVSS_TYPE_PRIORITY.fetch(sev["type"], 0) }
|
|
111
|
+
end
|
|
112
|
+
|
|
100
113
|
def normalize_severity(severity)
|
|
101
114
|
return nil unless severity
|
|
102
115
|
|
|
@@ -109,34 +122,15 @@ module Brew
|
|
|
109
122
|
end
|
|
110
123
|
|
|
111
124
|
def severity_from_cvss(vector)
|
|
112
|
-
return nil
|
|
113
|
-
return nil unless vector.include?("CVSS:3")
|
|
114
|
-
|
|
115
|
-
metrics = parse_cvss_metrics(vector)
|
|
116
|
-
return nil if metrics.empty?
|
|
117
|
-
|
|
118
|
-
impact_high = %w[C I A].count { |m| metrics[m] == "H" }
|
|
119
|
-
network_attack = metrics["AV"] == "N"
|
|
120
|
-
no_privs = metrics["PR"] == "N"
|
|
121
|
-
no_interaction = metrics["UI"] == "N"
|
|
122
|
-
|
|
123
|
-
if impact_high >= 2 && network_attack && no_privs
|
|
124
|
-
"critical"
|
|
125
|
-
elsif impact_high >= 1 && network_attack
|
|
126
|
-
"high"
|
|
127
|
-
elsif impact_high >= 1 || (network_attack && no_privs && no_interaction)
|
|
128
|
-
"medium"
|
|
129
|
-
else
|
|
130
|
-
"low"
|
|
131
|
-
end
|
|
132
|
-
end
|
|
125
|
+
return nil if vector.to_s.empty?
|
|
133
126
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
127
|
+
cvss = CvssSuite.new(vector)
|
|
128
|
+
|
|
129
|
+
normalize_severity(cvss.severity)
|
|
130
|
+
rescue StandardError
|
|
131
|
+
warn "Warning: Failed to determine severity from CVSS vector " \
|
|
132
|
+
"'#{vector}' for '#{id}'"
|
|
133
|
+
nil
|
|
140
134
|
end
|
|
141
135
|
|
|
142
136
|
def normalize_version(version)
|
|
@@ -171,30 +165,51 @@ module Brew
|
|
|
171
165
|
def version_in_range?(version, events, ecosystem)
|
|
172
166
|
return false if events.nil? || events.empty?
|
|
173
167
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
Vers.satisfies?(version, constraints.join(","), ecosystem)
|
|
168
|
+
build_constraint_sets(events).any? do |constraints|
|
|
169
|
+
constraints.empty? || Vers.satisfies?(version, constraints.join(","), ecosystem)
|
|
170
|
+
end
|
|
178
171
|
rescue StandardError => e
|
|
179
172
|
warn "Warning: Failed to check version '#{version}' against constraints: #{e.message}"
|
|
180
|
-
|
|
173
|
+
true
|
|
181
174
|
end
|
|
182
175
|
|
|
183
|
-
def
|
|
184
|
-
|
|
176
|
+
def build_constraint_sets(events)
|
|
177
|
+
constraint_sets = []
|
|
178
|
+
constraints = nil
|
|
179
|
+
|
|
185
180
|
events.each do |event|
|
|
186
181
|
if event["introduced"]
|
|
182
|
+
constraints = []
|
|
187
183
|
intro = normalize_version(event["introduced"])
|
|
188
184
|
constraints << ">=#{intro}" unless intro == "0"
|
|
189
|
-
|
|
190
|
-
|
|
185
|
+
elsif event["fixed"]
|
|
186
|
+
constraints ||= []
|
|
191
187
|
constraints << "<#{normalize_version(event["fixed"])}"
|
|
192
|
-
|
|
193
|
-
|
|
188
|
+
constraint_sets << constraints
|
|
189
|
+
constraints = nil
|
|
190
|
+
elsif event["last_affected"]
|
|
191
|
+
constraints ||= []
|
|
194
192
|
constraints << "<=#{normalize_version(event["last_affected"])}"
|
|
193
|
+
constraint_sets << constraints
|
|
194
|
+
constraints = nil
|
|
195
|
+
elsif event["limit"]
|
|
196
|
+
constraints ||= []
|
|
197
|
+
limit_constraint = build_limit_constraint(event["limit"])
|
|
198
|
+
constraints << limit_constraint if limit_constraint
|
|
199
|
+
constraint_sets << constraints
|
|
200
|
+
constraints = nil
|
|
195
201
|
end
|
|
196
202
|
end
|
|
197
|
-
|
|
203
|
+
|
|
204
|
+
constraint_sets << constraints if constraints
|
|
205
|
+
constraint_sets
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def build_limit_constraint(limit)
|
|
209
|
+
limit = limit.to_s
|
|
210
|
+
return if limit == "*"
|
|
211
|
+
|
|
212
|
+
"<#{normalize_version(limit)}"
|
|
198
213
|
end
|
|
199
214
|
end
|
|
200
215
|
end
|
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.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrew Nesbitt
|
|
@@ -9,6 +9,20 @@ bindir: exe
|
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: cvss-suite
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '4.1'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '4.1'
|
|
12
26
|
- !ruby/object:Gem::Dependency
|
|
13
27
|
name: purl
|
|
14
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -74,6 +88,7 @@ executables:
|
|
|
74
88
|
extensions: []
|
|
75
89
|
extra_rdoc_files: []
|
|
76
90
|
files:
|
|
91
|
+
- ".rubocop.yml"
|
|
77
92
|
- ".ruby-version"
|
|
78
93
|
- CHANGELOG.md
|
|
79
94
|
- CODE_OF_CONDUCT.md
|
|
@@ -81,6 +96,9 @@ files:
|
|
|
81
96
|
- LICENSE
|
|
82
97
|
- README.md
|
|
83
98
|
- Rakefile
|
|
99
|
+
- examples/README.md
|
|
100
|
+
- examples/pr-vuln-check.yml
|
|
101
|
+
- examples/scan-all.yml
|
|
84
102
|
- exe/brew-vulns
|
|
85
103
|
- lib/brew/vulns.rb
|
|
86
104
|
- lib/brew/vulns/cli.rb
|
|
@@ -89,13 +107,13 @@ files:
|
|
|
89
107
|
- lib/brew/vulns/version.rb
|
|
90
108
|
- lib/brew/vulns/vulnerability.rb
|
|
91
109
|
- sig/brew/vulns.rbs
|
|
92
|
-
homepage: https://github.com/
|
|
110
|
+
homepage: https://github.com/Homebrew/homebrew-brew-vulns
|
|
93
111
|
licenses:
|
|
94
112
|
- MIT
|
|
95
113
|
metadata:
|
|
96
|
-
homepage_uri: https://github.com/
|
|
97
|
-
source_code_uri: https://github.com/
|
|
98
|
-
changelog_uri: https://github.com/
|
|
114
|
+
homepage_uri: https://github.com/Homebrew/homebrew-brew-vulns
|
|
115
|
+
source_code_uri: https://github.com/Homebrew/homebrew-brew-vulns
|
|
116
|
+
changelog_uri: https://github.com/Homebrew/homebrew-brew-vulns/blob/main/CHANGELOG.md
|
|
99
117
|
rdoc_options: []
|
|
100
118
|
require_paths:
|
|
101
119
|
- lib
|
|
@@ -110,7 +128,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
110
128
|
- !ruby/object:Gem::Version
|
|
111
129
|
version: '0'
|
|
112
130
|
requirements: []
|
|
113
|
-
rubygems_version: 4.0.
|
|
131
|
+
rubygems_version: 4.0.10
|
|
114
132
|
specification_version: 4
|
|
115
133
|
summary: Check Homebrew packages for known vulnerabilities
|
|
116
134
|
test_files: []
|