Skip to content

Commit 4b4f0de

Browse files
author
Federico Builes
authored
Merge pull request #623 from actions/fix-advisory-filters
Fix GHSA Filtering
2 parents 550520e + a93fa86 commit 4b4f0de

File tree

5 files changed

+156
-54
lines changed

5 files changed

+156
-54
lines changed

__tests__/filter.test.ts

Lines changed: 111 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const npmChange: Change = {
1919
vulnerabilities: [
2020
{
2121
severity: 'critical',
22-
advisory_ghsa_id: 'first-random_string',
22+
advisory_ghsa_id: 'vulnerable-ghsa-id',
2323
advisory_summary: 'very dangerous',
2424
advisory_url: 'github.com/future-funk'
2525
}
@@ -39,13 +39,13 @@ const rubyChange: Change = {
3939
vulnerabilities: [
4040
{
4141
severity: 'moderate',
42-
advisory_ghsa_id: 'second-random_string',
42+
advisory_ghsa_id: 'moderate-ghsa-id',
4343
advisory_summary: 'not so dangerous',
4444
advisory_url: 'github.com/future-funk'
4545
},
4646
{
4747
severity: 'low',
48-
advisory_ghsa_id: 'third-random_string',
48+
advisory_ghsa_id: 'low-ghsa-id',
4949
advisory_summary: 'dont page me',
5050
advisory_url: 'github.com/future-funk'
5151
}
@@ -65,6 +65,64 @@ const noVulnNpmChange: Change = {
6565
vulnerabilities: []
6666
}
6767

68+
const lodashChange: Change = {
69+
change_type: 'added',
70+
manifest: 'package.json',
71+
ecosystem: 'npm',
72+
name: 'lodash',
73+
version: '4.17.0',
74+
package_url: 'pkg:npm/lodash@4.17.0',
75+
license: 'MIT',
76+
source_repository_url: 'https://github.com/lodash/lodash',
77+
scope: 'runtime',
78+
vulnerabilities: [
79+
{
80+
severity: 'critical',
81+
advisory_ghsa_id: 'GHSA-jf85-cpcp-j695',
82+
advisory_summary: 'Prototype Pollution in lodash',
83+
advisory_url: 'https://github.com/advisories/GHSA-jf85-cpcp-j695'
84+
},
85+
{
86+
severity: 'high',
87+
advisory_ghsa_id: 'GHSA-4xc9-xhrj-v574',
88+
advisory_summary: 'Prototype Pollution in lodash',
89+
advisory_url: 'https://github.com/advisories/GHSA-4xc9-xhrj-v574'
90+
},
91+
{
92+
severity: 'high',
93+
advisory_ghsa_id: 'GHSA-35jh-r3h4-6jhm',
94+
advisory_summary: 'Command Injection in lodash',
95+
advisory_url: 'https://github.com/advisories/GHSA-35jh-r3h4-6jhm'
96+
},
97+
{
98+
severity: 'high',
99+
advisory_ghsa_id: 'GHSA-p6mc-m468-83gw',
100+
advisory_summary: 'Prototype Pollution in lodash',
101+
advisory_url: 'https://github.com/advisories/GHSA-p6mc-m468-83gw'
102+
},
103+
{
104+
severity: 'moderate',
105+
advisory_ghsa_id: 'GHSA-x5rq-j2xg-h7qm',
106+
advisory_summary:
107+
'Regular Expression Denial of Service (ReDoS) in lodash',
108+
advisory_url: 'https://github.com/advisories/GHSA-x5rq-j2xg-h7qm'
109+
},
110+
{
111+
severity: 'moderate',
112+
advisory_ghsa_id: 'GHSA-29mw-wpgm-hmr9',
113+
advisory_summary:
114+
'Regular Expression Denial of Service (ReDoS) in lodash',
115+
advisory_url: 'https://github.com/advisories/GHSA-29mw-wpgm-hmr9'
116+
},
117+
{
118+
severity: 'low',
119+
advisory_ghsa_id: 'GHSA-fvqr-27wr-82fm',
120+
advisory_summary: 'Prototype Pollution in lodash',
121+
advisory_url: 'https://github.com/advisories/GHSA-fvqr-27wr-82fm'
122+
}
123+
]
124+
}
125+
68126
test('it properly filters changes by severity', async () => {
69127
const changes = [npmChange, rubyChange]
70128
let result = filterChangesBySeverity('high', changes)
@@ -99,25 +157,61 @@ test('it properly handles undefined advisory IDs', async () => {
99157
test('it properly filters changes with allowed vulnerabilities', async () => {
100158
const changes = [npmChange, rubyChange, noVulnNpmChange]
101159

102-
let result = filterAllowedAdvisories(['notrealGHSAID'], changes)
103-
expect(result).toEqual([npmChange, rubyChange, noVulnNpmChange])
160+
const fakeGHSAChanges = filterAllowedAdvisories(['notrealGHSAID'], changes)
161+
expect(fakeGHSAChanges).toEqual([npmChange, rubyChange, noVulnNpmChange])
162+
})
163+
164+
test('it properly filters only allowed vulnerabilities', async () => {
165+
const changes = [npmChange, rubyChange, noVulnNpmChange]
166+
const oldVulns = [
167+
...npmChange.vulnerabilities,
168+
...rubyChange.vulnerabilities,
169+
...noVulnNpmChange.vulnerabilities
170+
]
104171

105-
result = filterAllowedAdvisories(['first-random_string'], changes)
106-
expect(result).toEqual([rubyChange, noVulnNpmChange])
172+
const vulnerable = filterAllowedAdvisories(['vulnerable-ghsa-id'], changes)
107173

108-
result = filterAllowedAdvisories(
109-
['second-random_string', 'third-random_string'],
110-
changes
174+
const newVulns = vulnerable.map(change => change.vulnerabilities).flat()
175+
176+
expect(newVulns.length).toEqual(oldVulns.length - 1)
177+
expect(newVulns).not.toContainEqual(
178+
expect.objectContaining({advisory_ghsa_id: 'vulnerable-ghsa-id'})
111179
)
112-
expect(result).toEqual([npmChange, noVulnNpmChange])
180+
})
113181

114-
result = filterAllowedAdvisories(
115-
['first-random_string', 'second-random_string', 'third-random_string'],
182+
test('does not drop dependencies when filtering by GHSA', async () => {
183+
const changes = [npmChange, rubyChange, noVulnNpmChange]
184+
const result = filterAllowedAdvisories(
185+
['moderate-ghsa-id', 'low-ghsa-id', 'GHSA-jf85-cpcp-j695'],
116186
changes
117187
)
118-
expect(result).toEqual([noVulnNpmChange])
119188

120-
// if we have a change with multiple vulnerabilities but only one is allowed, we still should not filter out that change
121-
result = filterAllowedAdvisories(['second-random_string'], changes)
122-
expect(result).toEqual([npmChange, rubyChange, noVulnNpmChange])
189+
expect(result.map(change => change.name)).toEqual(
190+
changes.map(change => change.name)
191+
)
192+
})
193+
194+
test('it properly filters multiple GHSAs', async () => {
195+
const allowedGHSAs = ['vulnerable-ghsa-id', 'moderate-ghsa-id', 'low-ghsa-id']
196+
const changes = [npmChange, rubyChange, noVulnNpmChange]
197+
const oldVulns = changes.map(change => change.vulnerabilities).flat()
198+
199+
const result = filterAllowedAdvisories(allowedGHSAs, changes)
200+
201+
const newVulns = result.map(change => change.vulnerabilities).flat()
202+
203+
expect(newVulns.length).toEqual(oldVulns.length - 3)
204+
})
205+
206+
test('it filters out GHSA dependencies', async () => {
207+
const lodash = filterAllowedAdvisories(
208+
['GHSA-jf85-cpcp-j695'],
209+
[lodashChange]
210+
)[0]
211+
// the filter should have removed a single GHSA from the list
212+
const expected = lodashChange.vulnerabilities.filter(
213+
vuln => vuln.advisory_ghsa_id !== 'GHSA-jf85-cpcp-j695'
214+
)
215+
expect(expected.length).toEqual(lodashChange.vulnerabilities.length - 1)
216+
expect(lodash.vulnerabilities).toEqual(expected)
123217
})

dist/index.js

Lines changed: 19 additions & 17 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/index.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/filter.ts

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
import {Changes, Severity, SEVERITIES, Scope} from './schemas'
22

3+
/**
4+
* Filters changes by a severity level. Only vulnerable
5+
* dependencies will be returned.
6+
*
7+
* @param severity - The severity level to filter by.
8+
* @param changes - The array of changes to filter.
9+
* @returns The filtered array of changes that match the specified severity level and have vulnerabilities.
10+
*/
311
export function filterChangesBySeverity(
412
severity: Severity,
513
changes: Changes
@@ -31,7 +39,14 @@ export function filterChangesBySeverity(
3139
filteredChanges = filteredChanges.filter(
3240
change => change.vulnerabilities.length > 0
3341
)
34-
return filteredChanges
42+
43+
// only report vulnerability additions
44+
return filteredChanges.filter(
45+
change =>
46+
change.change_type === 'added' &&
47+
change.vulnerabilities !== undefined &&
48+
change.vulnerabilities.length > 0
49+
)
3550
}
3651

3752
export function filterChangesByScopes(
@@ -67,25 +82,20 @@ export function filterAllowedAdvisories(
6782
return changes
6883
}
6984

70-
const filteredChanges = changes.filter(change => {
85+
const filteredChanges = changes.map(change => {
7186
const noAdvisories =
7287
change.vulnerabilities === undefined ||
7388
change.vulnerabilities.length === 0
7489

7590
if (noAdvisories) {
76-
return true
91+
return change
7792
}
93+
const newChange = {...change}
94+
newChange.vulnerabilities = change.vulnerabilities.filter(
95+
vuln => !ghsas.includes(vuln.advisory_ghsa_id)
96+
)
7897

79-
let allAllowedAdvisories = true
80-
// if there's at least one advisory that is not allowlisted, we will keep the change
81-
for (const vulnerability of change.vulnerabilities) {
82-
if (!ghsas.includes(vulnerability.advisory_ghsa_id)) {
83-
allAllowedAdvisories = false
84-
}
85-
if (!allAllowedAdvisories) {
86-
return true
87-
}
88-
}
98+
return newChange
8999
})
90100

91101
return filteredChanges

src/main.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -80,21 +80,17 @@ async function run(): Promise<void> {
8080
return
8181
}
8282

83-
const minSeverity = config.fail_on_severity
8483
const scopedChanges = filterChangesByScopes(config.fail_on_scopes, changes)
84+
8585
const filteredChanges = filterAllowedAdvisories(
8686
config.allow_ghsas,
8787
scopedChanges
8888
)
8989

90+
const minSeverity = config.fail_on_severity
9091
const vulnerableChanges = filterChangesBySeverity(
9192
minSeverity,
9293
filteredChanges
93-
).filter(
94-
change =>
95-
change.change_type === 'added' &&
96-
change.vulnerabilities !== undefined &&
97-
change.vulnerabilities.length > 0
9894
)
9995

10096
const invalidLicenseChanges = await getInvalidLicenseChanges(

0 commit comments

Comments
 (0)