Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

rhel: support unfixed OpenShift vulnerabilities #1182

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions rhel/parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"testing"
"time"

"github.com/google/go-cmp/cmp"
"github.com/quay/goval-parser/oval"
"github.com/quay/zlog"

Expand Down Expand Up @@ -100,6 +101,86 @@ func TestParse(t *testing.T) {
}
}

func TestAllKnownOpenShift4CPEs(t *testing.T) {
table := []struct {
cpe string
expected []string
}{
{
cpe: "cpe:/a:redhat:openshift:4.14",
expected: []string{
"cpe:/a:redhat:openshift:4.0",
"cpe:/a:redhat:openshift:4.1",
"cpe:/a:redhat:openshift:4.2",
"cpe:/a:redhat:openshift:4.3",
"cpe:/a:redhat:openshift:4.4",
"cpe:/a:redhat:openshift:4.5",
"cpe:/a:redhat:openshift:4.6",
"cpe:/a:redhat:openshift:4.7",
"cpe:/a:redhat:openshift:4.8",
"cpe:/a:redhat:openshift:4.9",
"cpe:/a:redhat:openshift:4.10",
"cpe:/a:redhat:openshift:4.11",
"cpe:/a:redhat:openshift:4.12",
"cpe:/a:redhat:openshift:4.13",
},
},
{
cpe: "cpe:/a:redhat:openshift:4.15::el8",
expected: []string{
"cpe:/a:redhat:openshift:4.0::el8",
"cpe:/a:redhat:openshift:4.1::el8",
"cpe:/a:redhat:openshift:4.2::el8",
"cpe:/a:redhat:openshift:4.3::el8",
"cpe:/a:redhat:openshift:4.4::el8",
"cpe:/a:redhat:openshift:4.5::el8",
"cpe:/a:redhat:openshift:4.6::el8",
"cpe:/a:redhat:openshift:4.7::el8",
"cpe:/a:redhat:openshift:4.8::el8",
"cpe:/a:redhat:openshift:4.9::el8",
"cpe:/a:redhat:openshift:4.10::el8",
"cpe:/a:redhat:openshift:4.11::el8",
"cpe:/a:redhat:openshift:4.12::el8",
"cpe:/a:redhat:openshift:4.13::el8",
"cpe:/a:redhat:openshift:4.14::el8",
},
},
{
cpe: "cpe:/a:redhat:openshift:4.15::el9",
expected: []string{
"cpe:/a:redhat:openshift:4.0::el9",
"cpe:/a:redhat:openshift:4.1::el9",
"cpe:/a:redhat:openshift:4.2::el9",
"cpe:/a:redhat:openshift:4.3::el9",
"cpe:/a:redhat:openshift:4.4::el9",
"cpe:/a:redhat:openshift:4.5::el9",
"cpe:/a:redhat:openshift:4.6::el9",
"cpe:/a:redhat:openshift:4.7::el9",
"cpe:/a:redhat:openshift:4.8::el9",
"cpe:/a:redhat:openshift:4.9::el9",
"cpe:/a:redhat:openshift:4.10::el9",
"cpe:/a:redhat:openshift:4.11::el9",
"cpe:/a:redhat:openshift:4.12::el9",
"cpe:/a:redhat:openshift:4.13::el9",
"cpe:/a:redhat:openshift:4.14::el9",
},
},
}

for _, test := range table {
t.Run(test.cpe, func(t *testing.T) {
cpes, err := allKnownOpenShift4CPEs(test.cpe)
if err != nil {
t.Fatal(err)
}

if !cmp.Equal(cpes, test.expected) {
t.Fatal(cmp.Diff(cpes, test.expected))
}
})
}
}

// Here's a giant restructured struct for reference and tests.
var ovalDef = oval.Definition{
XMLName: xml.Name{Space: "http://oval.mitre.org/XMLSchema/oval-definitions-5", Local: "definition"},
Expand Down
84 changes: 81 additions & 3 deletions rhel/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import (
"encoding/xml"
"fmt"
"io"
"regexp"
"strconv"
"strings"

"github.com/quay/goval-parser/oval"
"github.com/quay/zlog"
Expand All @@ -16,6 +19,10 @@ import (
"github.com/quay/claircore/toolkit/types/cpe"
)

var (
openshift4CPEPattern = regexp.MustCompile(`^cpe:/a:redhat:openshift:(?P<openshiftVersion>4(\.(?P<minorVersion>\d+))?)(::el\d+)?$`)
)

// Parse implements [driver.Updater].
//
// Parse treats the data inside the provided io.ReadCloser as Red Hat
Expand Down Expand Up @@ -43,7 +50,7 @@ func (u *Updater) Parse(ctx context.Context, r io.ReadCloser) ([]*claircore.Vuln
// Red Hat OVAL data include information about vulnerabilities,
// that actually don't affect the package in any way. Storing them
// would increase number of records in DB without adding any value.
if isSkippableDefinitionType(defType, u.ignoreUnpatched) {
if u.shouldSkipDefType(defType) {
return vs, nil
}

Expand All @@ -59,6 +66,7 @@ func (u *Updater) Parse(ctx context.Context, r io.ReadCloser) ([]*claircore.Vuln
if err != nil {
return nil, err
}

v := &claircore.Vulnerability{
Updater: u.Name(),
Name: def.Title,
Expand All @@ -75,6 +83,38 @@ func (u *Updater) Parse(ctx context.Context, r io.ReadCloser) ([]*claircore.Vuln
Dist: u.dist,
}
vs = append(vs, v)

// If this is an unfixed OpenShift 4.x vulnerability, add a CPE for each minor version
// below the given minor version.
// There is only a single OVAL v2 file for all OpenShift 4 versions for each RHEL version,
// and it is assumed the CPE specified for the vulnerability indicates
// versions y such that 4.0 <= y <= 4.x are affected, where x is the next,
// unreleased minor version of OpenShift 4 specified in the CPE.
//
// It is expected the CPE is of the form cpe:/a:redhat:openshift:4.x or
// cpe:/a:redhat:openshift:4.x::el<RHEL version>.
// For example: cpe:/a:redhat:openshift:4.14 or cpe:/a:redhat:openshift:4.15::el9.
//
// Any other OpenShift 4-related CPEs are not supported at this time.
if defType == ovalutil.CVEDefinition && strings.HasPrefix(affected, "cpe:/a:redhat:openshift:4") {
if openshiftCPEs, err := allKnownOpenShift4CPEs(affected); err != nil {
zlog.Warn(ctx).Msgf("Skipping addition of extra OpenShift 4 CPEs for the unpatched vulnerability %q: %v", def.Title, err)
} else {
for _, openshiftCPE := range openshiftCPEs {
wfn, err := cpe.Unbind(openshiftCPE)
if err != nil {
return nil, err
}
v := *v
v.Repo = &claircore.Repository{
Name: openshiftCPE,
CPE: wfn,
Key: repositoryKey,
}
vs = append(vs, &v)
}
}
}
}
return vs, nil
}
Expand All @@ -85,8 +125,46 @@ func (u *Updater) Parse(ctx context.Context, r io.ReadCloser) ([]*claircore.Vuln
return vulns, nil
}

func isSkippableDefinitionType(defType ovalutil.DefinitionType, ignoreUnpatched bool) bool {
// ShouldSkipDefType returns "true" if any of the following is "true":
//
// * defType == ovalutil.UnaffectedDefinition
// * defType == ovalutil.NoneDefinition
// * u.ignoreUnpatched && defType == ovalutil.CVEDefinition
func (u *Updater) shouldSkipDefType(defType ovalutil.DefinitionType) bool {
return defType == ovalutil.UnaffectedDefinition ||
defType == ovalutil.NoneDefinition ||
(ignoreUnpatched && defType == ovalutil.CVEDefinition)
(u.ignoreUnpatched && defType == ovalutil.CVEDefinition)
}

// AllKnownOpenShift4CPEs returns a slice of other CPEs related to the given Red Hat OpenShift 4 CPE.
// For example, given "cpe:/a:redhat:openshift:4.2", this returns
// ["cpe:/a:redhat:openshift:4.0", "cpe:/a:redhat:openshift:4.1"].
// Note: "cpe:/a:redhat:openshift:4.2" is skipped, as it does not exist.
func allKnownOpenShift4CPEs(cpe string) ([]string, error) {
// These must all stay in-sync at all times.
const (
openshiftVersionIdx = 1
minorVersionIdx = 3
submatchLength = 5
)

match := openshift4CPEPattern.FindStringSubmatch(cpe)
if len(match) != submatchLength || match[minorVersionIdx] == "" {
return nil, fmt.Errorf("CPE %q does not match an expected OpenShift 4 CPE format", cpe)
}

maxMinorVersion, err := strconv.Atoi(match[minorVersionIdx])
if err != nil {
return nil, fmt.Errorf("CPE %q does not match an expected OpenShift 4 CPE format: %w", cpe, err)
}

openshiftVersion := match[openshiftVersionIdx]
cpes := make([]string, 0, maxMinorVersion)
// Skip maxMinorVersion, as this version of OpenShift 4 does not exist yet.
for i := 0; i < maxMinorVersion; i++ {
version := strconv.Itoa(i)
cpes = append(cpes, strings.Replace(cpe, openshiftVersion, "4."+version, 1))
}

return cpes, nil
}