diff --git a/pkg/ovalutil/rpm.go b/pkg/ovalutil/rpm.go index f3cd9430b..e2f1814f2 100644 --- a/pkg/ovalutil/rpm.go +++ b/pkg/ovalutil/rpm.go @@ -23,12 +23,10 @@ const ( NoneDefinition DefinitionType = "none" ) -var moduleCommentRegex, definitionTypeRegex *regexp.Regexp - -func init() { - moduleCommentRegex = regexp.MustCompile(`(Module )(.*)( is enabled)`) - definitionTypeRegex = regexp.MustCompile(`^oval\:com\.redhat\.([a-z]+)\:def\:\d+$`) -} +var ( + moduleCommentRegex = regexp.MustCompile(`(Module )(.*)( is enabled)`) + definitionTypeRegex = regexp.MustCompile(`^oval:com\.redhat\.([a-z]+):def:\d+$`) +) // ProtoVulnsFunc allows a caller to create prototype vulnerabilities that will be // copied and further defined for every applicable oval.Criterion discovered. diff --git a/rhel/parser.go b/rhel/parser.go index 27ca353e5..a90c5fa8f 100644 --- a/rhel/parser.go +++ b/rhel/parser.go @@ -5,6 +5,9 @@ import ( "encoding/xml" "fmt" "io" + "regexp" + "strconv" + "strings" "github.com/quay/goval-parser/oval" "github.com/quay/zlog" @@ -16,6 +19,10 @@ import ( "github.com/quay/claircore/toolkit/types/cpe" ) +var ( + openshift4CPEPattern = regexp.MustCompile(`^cpe:/a:redhat:openshift:(?P4(\.(?P\d+))?)(::el\d+)?$`) +) + // Parse implements [driver.Updater]. // // Parse treats the data inside the provided io.ReadCloser as Red Hat @@ -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 } @@ -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, @@ -75,6 +83,41 @@ 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. + // 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. + // + // Note: VEX files do not specify an OpenShift 4 minor version, so this will have to be revamped + // once VEX files are supported. + 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 } @@ -85,8 +128,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 } diff --git a/rhel/parse_test.go b/rhel/parser_test.go similarity index 87% rename from rhel/parse_test.go rename to rhel/parser_test.go index 62a9e7465..f3494ef81 100644 --- a/rhel/parse_test.go +++ b/rhel/parser_test.go @@ -7,6 +7,7 @@ import ( "testing" "time" + "github.com/google/go-cmp/cmp" "github.com/quay/goval-parser/oval" "github.com/quay/zlog" @@ -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"},