Skip to content

Commit e5e784d

Browse files
authored
fix: EUS application with multiple advisories (#2841)
Signed-off-by: Keith Zantow <[email protected]>
1 parent 3c264fa commit e5e784d

File tree

4 files changed

+156
-4
lines changed

4 files changed

+156
-4
lines changed

grype/matcher/rpm/rhel_eus.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,7 @@ func redhatEUSMatches(provider result.Provider, searchPkg pkg.Package) ([]match.
7272
search.ByPackageName(searchPkg.Name),
7373
search.ByDistro(distroWithoutEUS), // e.g. >= 9.0 && < 10 (no EUS channel)
7474
internal.OnlyQualifiedPackages(searchPkg),
75-
// note: we could apply a version constraint here (there would be no functional issue with this) but
76-
// the current approach taken is to allow fixes from mainline disclosures to be accounted for in the final
77-
// merge step below.
75+
internal.OnlyVulnerableVersions(pkgVersion), // if these records indicate the version of the package is not vulnerable, do not include them
7876
)
7977
if err != nil {
8078
return nil, fmt.Errorf("matcher failed to fetch disclosures for distro=%q pkg=%q: %w", searchPkg.Distro, searchPkg.Name, err)
@@ -98,12 +96,17 @@ func redhatEUSMatches(provider result.Provider, searchPkg pkg.Package) ([]match.
9896
return nil, fmt.Errorf("matcher failed to fetch resolutions for distro=%q pkg=%q: %w", searchPkg.Distro, searchPkg.Name, err)
9997
}
10098

99+
eusFixes := resolutions.Filter(search.ByFixedVersion(*pkgVersion))
100+
101+
// remove EUS fixed vulns for this version
102+
remaining := disclosures.Remove(eusFixes)
103+
101104
// combine disclosures and fixes so that:
102105
// a. disclosures that have EUS fixes that resolve the disclosure for an earlier version of the package (thus we're not vulnerable) are removed.
103106
// b. disclosures that have EUS fixes that resolve the disclosure for future versions of the package (thus we're vulnerable) are kept.
104107
// c. all fixes from the incoming resolutions are patched onto the disclosures in the returned collection, so the
105108
// final set of vulnerabilities is a fused set of disclosures and fixes together.
106-
remaining := disclosures.Merge(resolutions, mergeEUSAdvisoriesIntoMainDisclosures(pkgVersion, false))
109+
remaining = remaining.Merge(resolutions, mergeEUSAdvisoriesIntoMainDisclosures(pkgVersion, false))
107110

108111
return remaining.ToMatches(), err
109112
}

grype/matcher/rpm/rhel_eus_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1117,6 +1117,51 @@ func TestRedhatEUSMatches(t *testing.T) {
11171117
},
11181118
},
11191119
},
1120+
{
1121+
name: "multiple advisories with mixed fix state relative to search package",
1122+
catalogPkg: testPkg1,
1123+
disclosureVulns: []vulnerability.Vulnerability{
1124+
{
1125+
Reference: vulnerability.Reference{
1126+
ID: "CVE-2021-1",
1127+
Namespace: "namespace",
1128+
},
1129+
PackageName: "test-pkg", // direct match
1130+
Constraint: version.MustGetConstraint("", version.RpmFormat),
1131+
Fix: vulnerability.Fix{
1132+
State: vulnerability.FixStateUnknown,
1133+
Versions: []string{},
1134+
},
1135+
},
1136+
},
1137+
resolutionVulns: []vulnerability.Vulnerability{
1138+
{
1139+
Reference: vulnerability.Reference{
1140+
ID: "CVE-2021-1",
1141+
Namespace: "namespace",
1142+
},
1143+
PackageName: "test-pkg", // direct match
1144+
Constraint: version.MustGetConstraint("< 1.5.0", version.RpmFormat),
1145+
Fix: vulnerability.Fix{
1146+
State: vulnerability.FixStateFixed,
1147+
Versions: []string{"1.5.0"},
1148+
},
1149+
},
1150+
{
1151+
Reference: vulnerability.Reference{
1152+
ID: "CVE-2021-1",
1153+
Namespace: "namespace",
1154+
},
1155+
PackageName: "test-pkg", // direct match
1156+
Constraint: version.MustGetConstraint("< 1.0.0", version.RpmFormat),
1157+
Fix: vulnerability.Fix{
1158+
State: vulnerability.FixStateFixed,
1159+
Versions: []string{"1.0.0"},
1160+
},
1161+
},
1162+
},
1163+
want: []match.Match{},
1164+
},
11201165
{
11211166
name: "error fetching disclosures",
11221167
catalogPkg: testPkg1,

grype/search/version_constraint.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,30 @@ func (v VersionCriteria) criteria(constraint version.Constraint) (bool, error) {
5757
return satisfied, nil
5858
}
5959

60+
// ByFixedVersion returns criteria which constrains vulnerabilities to those that are fixed based on the provided version,
61+
// in other words: vulnerabilities where the fix version is less than v
62+
func ByFixedVersion(v version.Version) vulnerability.Criteria {
63+
return &funcCriteria{
64+
func(vuln vulnerability.Vulnerability) (bool, string, error) {
65+
var err error
66+
if vuln.Fix.State != vulnerability.FixStateFixed {
67+
return false, "", nil
68+
}
69+
for _, fixVersion := range vuln.Fix.Versions {
70+
cmp, e := version.New(fixVersion, v.Format).Compare(&v)
71+
if e != nil {
72+
err = e
73+
}
74+
if cmp <= 0 {
75+
// fix version is less than or equal to the provided version, so is considered fixed
76+
return true, fmt.Sprintf("fix version %v is less than %v", v, fixVersion), err
77+
}
78+
}
79+
return false, "", err
80+
},
81+
}
82+
}
83+
6084
// ByVersion returns criteria which constrains vulnerabilities to those with matching version constraints
6185
func ByVersion(v version.Version) vulnerability.Criteria {
6286
return &VersionCriteria{

grype/search/version_constraint_test.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,3 +100,83 @@ func Test_ByConstraintFunc(t *testing.T) {
100100
})
101101
}
102102
}
103+
104+
func Test_ByFixedVersion(t *testing.T) {
105+
tests := []struct {
106+
name string
107+
version string
108+
input vulnerability.Vulnerability
109+
matches bool
110+
}{
111+
{
112+
name: "fixed version is lower",
113+
version: "1.1.0",
114+
input: vulnerability.Vulnerability{
115+
Fix: vulnerability.Fix{
116+
Versions: []string{"1.0.0"},
117+
State: vulnerability.FixStateFixed,
118+
},
119+
},
120+
matches: true,
121+
},
122+
{
123+
name: "fixed version is equal",
124+
version: "1.1.0",
125+
input: vulnerability.Vulnerability{
126+
Fix: vulnerability.Fix{
127+
Versions: []string{"1.1.0"},
128+
State: vulnerability.FixStateFixed,
129+
},
130+
},
131+
matches: true,
132+
},
133+
{
134+
name: "one of multiple fix versions matches",
135+
version: "1.1.0",
136+
input: vulnerability.Vulnerability{
137+
Fix: vulnerability.Fix{
138+
Versions: []string{"1.0.0", "1.2.0"},
139+
State: vulnerability.FixStateFixed,
140+
},
141+
},
142+
matches: true,
143+
},
144+
{
145+
name: "fixed version is higher",
146+
version: "1.1.0",
147+
input: vulnerability.Vulnerability{
148+
Fix: vulnerability.Fix{
149+
Versions: []string{"1.2.0"},
150+
State: vulnerability.FixStateFixed,
151+
},
152+
},
153+
matches: false,
154+
},
155+
{
156+
name: "no fix versions",
157+
version: "1.1.0",
158+
input: vulnerability.Vulnerability{
159+
Fix: vulnerability.Fix{
160+
Versions: []string{},
161+
State: vulnerability.FixStateWontFix,
162+
},
163+
},
164+
matches: false,
165+
},
166+
}
167+
168+
for _, tt := range tests {
169+
t.Run(tt.name, func(t *testing.T) {
170+
v := version.New(tt.version, version.SemanticFormat)
171+
constraint := ByFixedVersion(*v)
172+
matches, reason, err := constraint.MatchesVulnerability(tt.input)
173+
require.NoError(t, err)
174+
assert.Equal(t, tt.matches, matches)
175+
if matches {
176+
assert.NotEmpty(t, reason)
177+
} else {
178+
assert.Empty(t, reason)
179+
}
180+
})
181+
}
182+
}

0 commit comments

Comments
 (0)