diff --git a/cmd/clair/main.go b/cmd/clair/main.go index 00f6461f..043977cc 100644 --- a/cmd/clair/main.go +++ b/cmd/clair/main.go @@ -30,9 +30,7 @@ import ( _ "github.com/coreos/clair/updater/fetchers/alpine" _ "github.com/coreos/clair/updater/fetchers/debian" - _ "github.com/coreos/clair/updater/fetchers/opensuse" _ "github.com/coreos/clair/updater/fetchers/rhel" - _ "github.com/coreos/clair/updater/fetchers/sle" _ "github.com/coreos/clair/updater/fetchers/ubuntu" _ "github.com/coreos/clair/updater/metadata_fetchers/nvd" diff --git a/updater/fetchers/opensuse/opensuse.go b/updater/fetchers/opensuse/opensuse.go deleted file mode 100644 index 87c8cb6f..00000000 --- a/updater/fetchers/opensuse/opensuse.go +++ /dev/null @@ -1,129 +0,0 @@ -// Copyright 2015 clair authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package opensuse - -import ( - "fmt" - "regexp" - "strconv" - - "github.com/coreos/clair/updater" - "github.com/coreos/clair/utils/oval" - "github.com/coreos/pkg/capnslog" -) - -var log = capnslog.NewPackageLogger("github.com/coreos/clair", "updater/fetchers/sle") - -func init() { - opensuseInfo := &OpenSUSEInfo{} - - updater.RegisterFetcher(opensuseInfo.DistName(), - &oval.OvalFetcher{OsInfo: opensuseInfo}) -} - -// OpenSUSEInfo implements oval.OsInfo interface -// See oval.OsInfo for more info on what each method is -type OpenSUSEInfo struct { -} - -func (f *OpenSUSEInfo) SecToken() string { - return "CVE" -} - -func (f *OpenSUSEInfo) IgnoredCriterions() []string { - return []string{} -} - -func (f *OpenSUSEInfo) OvalURI() string { - return "http://ftp.suse.com/pub/projects/security/oval/" -} - -func (f *OpenSUSEInfo) DistName() string { - return "opensuse" -} - -func (f *OpenSUSEInfo) Namespace() string { - return f.DistName() -} - -func (f *OpenSUSEInfo) ParseOsVersion(comment string) string { - return f.ParseOsVersionR(comment, f.CritSystem()) -} - -func (f *OpenSUSEInfo) ParseOsVersionR(comment string, exp *regexp.Regexp) string { - systemMatch := exp.FindStringSubmatch(comment) - if len(systemMatch) < 2 { - return "" - } - osVersion := systemMatch[1] - if len(systemMatch) == 4 && systemMatch[3] != "" { - sp := systemMatch[3] - osVersion = fmt.Sprintf("%s.%s", osVersion, sp) - } - - return osVersion -} - -func (f *OpenSUSEInfo) ParsePackageNameVersion(comment string) (string, string) { - packageMatch := f.CritPackage().FindStringSubmatch(comment) - - if len(packageMatch) != 3 { - return "", "" - } - name := packageMatch[1] - version := packageMatch[2] - return name, version -} - -func (f *OpenSUSEInfo) ParseFilenameDist(line string) string { - return f.ParseFilenameDistR(line, f.DistRegexp(), f.DistMinVersion()) -} - -func (f *OpenSUSEInfo) ParseFilenameDistR(line string, exp *regexp.Regexp, minVersion float64) string { - r := exp.FindStringSubmatch(line) - if len(r) != 2 { - return "" - } - if r[0] == "" || r[1] == "" { - return "" - } - distVersion, _ := strconv.ParseFloat(r[1], 32) - if distVersion < minVersion { - return "" - } - return f.DistFile(r[0]) -} - -// These are not in the interface - -func (f *OpenSUSEInfo) DistFile(item string) string { - return f.OvalURI() + item -} - -func (f *OpenSUSEInfo) CritSystem() *regexp.Regexp { - return regexp.MustCompile(`openSUSE [^0-9]*(\d+\.\d+)[^0-9]* is installed`) -} - -func (f *OpenSUSEInfo) CritPackage() *regexp.Regexp { - return regexp.MustCompile(`(.*)-(.*\-[\d\.]+) is installed`) -} - -func (f *OpenSUSEInfo) DistRegexp() *regexp.Regexp { - return regexp.MustCompile(`opensuse.[^0-9]*(\d+\.\d+).xml`) -} - -func (f *OpenSUSEInfo) DistMinVersion() float64 { - return 13.1 -} diff --git a/updater/fetchers/opensuse/opensuse_test.go b/updater/fetchers/opensuse/opensuse_test.go deleted file mode 100644 index b2317009..00000000 --- a/updater/fetchers/opensuse/opensuse_test.go +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright 2015 clair authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package opensuse - -import ( - "os" - "path/filepath" - "runtime" - "testing" - - "github.com/coreos/clair/database" - "github.com/coreos/clair/utils/oval" - "github.com/coreos/clair/utils/types" - "github.com/stretchr/testify/assert" -) - -func TestOpenSUSEParser(t *testing.T) { - _, filename, _, _ := runtime.Caller(0) - path := filepath.Join(filepath.Dir(filename)) - - // Test parsing testdata/fetcher_opensuse_test.1.xml - testFile, _ := os.Open(path + "/testdata/fetcher_opensuse_test.1.xml") - ov := &oval.OvalFetcher{OsInfo: &OpenSUSEInfo{}} - vulnerabilities, err := ov.ParseOval(testFile) - if assert.Nil(t, err) && assert.Len(t, vulnerabilities, 1) { - assert.Equal(t, "CVE-2012-2150", vulnerabilities[0].Name) - assert.Equal(t, "http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2012-2150", vulnerabilities[0].Link) - // Severity is not defined for openSUSE - assert.Equal(t, types.Unknown, vulnerabilities[0].Severity) - assert.Equal(t, `xfs_metadump in xfsprogs before 3.2.4 does not properly obfuscate file data, which allows remote attackers to obtain sensitive information by reading a generated image.`, vulnerabilities[0].Description) - - expectedFeatureVersions := []database.FeatureVersion{ - { - Feature: database.Feature{ - Namespace: database.Namespace{Name: "opensuse:42.1"}, - Name: "xfsprogs", - }, - Version: types.NewVersionUnsafe("3.2.1-5.1"), - }, - { - Feature: database.Feature{ - Namespace: database.Namespace{Name: "opensuse:42.1"}, - Name: "xfsprogs-devel", - }, - Version: types.NewVersionUnsafe("3.2.1-5.1"), - }, - } - - for _, expectedFeatureVersion := range expectedFeatureVersions { - assert.Contains(t, vulnerabilities[0].FixedIn, expectedFeatureVersion) - } - } - -} diff --git a/updater/fetchers/opensuse/testdata/fetcher_opensuse_test.1.xml b/updater/fetchers/opensuse/testdata/fetcher_opensuse_test.1.xml deleted file mode 100644 index 258f8254..00000000 --- a/updater/fetchers/opensuse/testdata/fetcher_opensuse_test.1.xml +++ /dev/null @@ -1,66 +0,0 @@ - - - - - Marcus Updateinfo to OVAL Converter - 5.5 - 2016-06-27T04:04:46 - - - - - CVE-2012-2150 - - openSUSE Leap 42.1 - - - xfs_metadump in xfsprogs before 3.2.4 does not properly obfuscate file data, which allows remote attackers to obtain sensitive information by reading a generated image. - - - - - - - - - - - - - - - - - - - - - - - - - - - xfsprogs-devel - - - openSUSE-release - - - xfsprogs - - - - - 0:3.2.1-5.1 - - - 42.1 - - - diff --git a/updater/fetchers/rhel/rhel.go b/updater/fetchers/rhel/rhel.go index d0a47fd2..43d1d5fe 100644 --- a/updater/fetchers/rhel/rhel.go +++ b/updater/fetchers/rhel/rhel.go @@ -15,12 +15,17 @@ package rhel import ( + "bufio" + "encoding/xml" + "io" + "net/http" "regexp" "strconv" "strings" + "github.com/coreos/clair/database" "github.com/coreos/clair/updater" - "github.com/coreos/clair/utils/oval" + cerrors "github.com/coreos/clair/utils/errors" "github.com/coreos/clair/utils/types" "github.com/coreos/pkg/capnslog" ) @@ -29,101 +34,327 @@ const ( // Before this RHSA, it deals only with RHEL <= 4. firstRHEL5RHSA = 20070044 firstConsideredRHEL = 5 + + ovalURI = "https://www.redhat.com/security/data/oval/" + rhsaFilePrefix = "com.redhat.rhsa-" + updaterFlag = "rhelUpdater" ) var ( + ignoredCriterions = []string{ + " is signed with Red Hat ", + " Client is installed", + " Workstation is installed", + " ComputeNode is installed", + } + rhsaRegexp = regexp.MustCompile(`com.redhat.rhsa-(\d+).xml`) - log = capnslog.NewPackageLogger("github.com/coreos/clair", "updater/fetchers/rhel") + + log = capnslog.NewPackageLogger("github.com/coreos/clair", "updater/fetchers/rhel") ) -func init() { - rhelInfo := &RHELInfo{} - updater.RegisterFetcher(rhelInfo.DistName(), - &oval.OvalFetcher{OsInfo: rhelInfo}) +type oval struct { + Definitions []definition `xml:"definitions>definition"` } -// RHELInfo implements oval.OsInfo interface -// See oval.OsInfo for more info on what each method is -type RHELInfo struct { +type definition struct { + Title string `xml:"metadata>title"` + Description string `xml:"metadata>description"` + References []reference `xml:"metadata>reference"` + Criteria criteria `xml:"criteria"` } -func (f *RHELInfo) DistFile(item string) string { - rhsaFilePrefix := "com.redhat.rhsa-" - return f.OvalURI() + rhsaFilePrefix + item + ".xml" +type reference struct { + Source string `xml:"source,attr"` + URI string `xml:"ref_url,attr"` } -func (f *RHELInfo) SecToken() string { - return "RHSA" +type criteria struct { + Operator string `xml:"operator,attr"` + Criterias []*criteria `xml:"criteria"` + Criterions []criterion `xml:"criterion"` } -func (f *RHELInfo) IgnoredCriterions() []string { - return []string{ - " is signed with Red Hat ", - " Client is installed", - " Workstation is installed", - " ComputeNode is installed", - } +type criterion struct { + Comment string `xml:"comment,attr"` } -func (f *RHELInfo) OvalURI() string { - return "https://www.redhat.com/security/data/oval/" +// RHELFetcher implements updater.Fetcher and gets vulnerability updates from +// the Red Hat OVAL definitions. +type RHELFetcher struct{} + +func init() { + updater.RegisterFetcher("Red Hat", &RHELFetcher{}) } -func (f *RHELInfo) DistName() string { - return "RHEL" +// FetchUpdate gets vulnerability updates from the Red Hat OVAL definitions. +func (f *RHELFetcher) FetchUpdate(datastore database.Datastore) (resp updater.FetcherResponse, err error) { + log.Info("fetching Red Hat vulnerabilities") + + // Get the first RHSA we have to manage. + flagValue, err := datastore.GetKeyValue(updaterFlag) + if err != nil { + return resp, err + } + firstRHSA, err := strconv.Atoi(flagValue) + if firstRHSA == 0 || err != nil { + firstRHSA = firstRHEL5RHSA + } + + // Fetch the update list. + r, err := http.Get(ovalURI) + if err != nil { + log.Errorf("could not download RHEL's update list: %s", err) + return resp, cerrors.ErrCouldNotDownload + } + + // Get the list of RHSAs that we have to process. + var rhsaList []int + scanner := bufio.NewScanner(r.Body) + for scanner.Scan() { + line := scanner.Text() + r := rhsaRegexp.FindStringSubmatch(line) + if len(r) == 2 { + rhsaNo, _ := strconv.Atoi(r[1]) + if rhsaNo > firstRHSA { + rhsaList = append(rhsaList, rhsaNo) + } + } + } + + for _, rhsa := range rhsaList { + // Download the RHSA's XML file. + r, err := http.Get(ovalURI + rhsaFilePrefix + strconv.Itoa(rhsa) + ".xml") + if err != nil { + log.Errorf("could not download RHEL's update file: %s", err) + return resp, cerrors.ErrCouldNotDownload + } + + // Parse the XML. + vs, err := parseRHSA(r.Body) + if err != nil { + return resp, err + } + + // Collect vulnerabilities. + for _, v := range vs { + resp.Vulnerabilities = append(resp.Vulnerabilities, v) + } + } + + // Set the flag if we found anything. + if len(rhsaList) > 0 { + resp.FlagName = updaterFlag + resp.FlagValue = strconv.Itoa(rhsaList[len(rhsaList)-1]) + } else { + log.Debug("no Red Hat update.") + } + + return resp, nil } -func (f *RHELInfo) Namespace() string { - // TODO this is where to set different labels for centos and rhel. See: - // https://github.com/coreos/clair/commit/ce8d31bbb323471bf2a69427e4a645b3ce8a25c1 - // https://github.com/coreos/clair/pull/193 - return "centos" +func parseRHSA(ovalReader io.Reader) (vulnerabilities []database.Vulnerability, err error) { + // Decode the XML. + var ov oval + err = xml.NewDecoder(ovalReader).Decode(&ov) + if err != nil { + log.Errorf("could not decode RHEL's XML: %s", err) + err = cerrors.ErrCouldNotParse + return + } + + // Iterate over the definitions and collect any vulnerabilities that affect + // at least one package. + for _, definition := range ov.Definitions { + pkgs := toFeatureVersions(definition.Criteria) + if len(pkgs) > 0 { + vulnerability := database.Vulnerability{ + Name: name(definition), + Link: link(definition), + Severity: priority(definition), + Description: description(definition), + } + for _, p := range pkgs { + vulnerability.FixedIn = append(vulnerability.FixedIn, p) + } + vulnerabilities = append(vulnerabilities, vulnerability) + } + } + + return } -func (f *RHELInfo) ParseOsVersion(comment string) string { - if !strings.Contains(comment, " is installed") { - return "" +func getCriterions(node criteria) [][]criterion { + // Filter useless criterions. + var criterions []criterion + for _, c := range node.Criterions { + ignored := false + + for _, ignoredItem := range ignoredCriterions { + if strings.Contains(c.Comment, ignoredItem) { + ignored = true + break + } + } + + if !ignored { + criterions = append(criterions, c) + } } - const prefixLen = len("Red Hat Enterprise Linux ") - osVersion := strings.TrimSpace(comment[prefixLen : prefixLen+strings.Index(comment[prefixLen:], " ")]) - if !f.ValidOsVersion(osVersion) { - return "" + + if node.Operator == "AND" { + return [][]criterion{criterions} + } else if node.Operator == "OR" { + var possibilities [][]criterion + for _, c := range criterions { + possibilities = append(possibilities, []criterion{c}) + } + return possibilities } - return osVersion + + return [][]criterion{} } -func (f *RHELInfo) ParsePackageNameVersion(comment string) (string, string) { - if !strings.Contains(comment, " is earlier than ") { - return "", "" +func getPossibilities(node criteria) [][]criterion { + if len(node.Criterias) == 0 { + return getCriterions(node) } - const prefixLen = len(" is earlier than ") - name := strings.TrimSpace(comment[:strings.Index(comment, " is earlier than ")]) - version := comment[strings.Index(comment, " is earlier than ")+prefixLen:] - return name, version + + var possibilitiesToCompose [][][]criterion + for _, criteria := range node.Criterias { + possibilitiesToCompose = append(possibilitiesToCompose, getPossibilities(*criteria)) + } + if len(node.Criterions) > 0 { + possibilitiesToCompose = append(possibilitiesToCompose, getCriterions(node)) + } + + var possibilities [][]criterion + if node.Operator == "AND" { + for _, possibility := range possibilitiesToCompose[0] { + possibilities = append(possibilities, possibility) + } + + for _, possibilityGroup := range possibilitiesToCompose[1:] { + var newPossibilities [][]criterion + + for _, possibility := range possibilities { + for _, possibilityInGroup := range possibilityGroup { + var p []criterion + p = append(p, possibility...) + p = append(p, possibilityInGroup...) + newPossibilities = append(newPossibilities, p) + } + } + + possibilities = newPossibilities + } + } else if node.Operator == "OR" { + for _, possibilityGroup := range possibilitiesToCompose { + for _, possibility := range possibilityGroup { + possibilities = append(possibilities, possibility) + } + } + } + + return possibilities } -func (f *RHELInfo) ParseFilenameDist(line string) string { - r := rhsaRegexp.FindStringSubmatch(line) - if len(r) != 2 { - return "" +func toFeatureVersions(criteria criteria) []database.FeatureVersion { + // There are duplicates in Red Hat .xml files. + // This map is for deduplication. + featureVersionParameters := make(map[string]database.FeatureVersion) + + possibilities := getPossibilities(criteria) + for _, criterions := range possibilities { + var ( + featureVersion database.FeatureVersion + osVersion int + err error + ) + + // Attempt to parse package data from trees of criterions. + for _, c := range criterions { + if strings.Contains(c.Comment, " is installed") { + const prefixLen = len("Red Hat Enterprise Linux ") + osVersion, err = strconv.Atoi(strings.TrimSpace(c.Comment[prefixLen : prefixLen+strings.Index(c.Comment[prefixLen:], " ")])) + if err != nil { + log.Warningf("could not parse Red Hat release version from: '%s'.", c.Comment) + } + } else if strings.Contains(c.Comment, " is earlier than ") { + const prefixLen = len(" is earlier than ") + featureVersion.Feature.Name = strings.TrimSpace(c.Comment[:strings.Index(c.Comment, " is earlier than ")]) + featureVersion.Version, err = types.NewVersion(c.Comment[strings.Index(c.Comment, " is earlier than ")+prefixLen:]) + if err != nil { + log.Warningf("could not parse package version '%s': %s. skipping", c.Comment[strings.Index(c.Comment, " is earlier than ")+prefixLen:], err.Error()) + } + } + } + + if osVersion >= firstConsideredRHEL { + // TODO(vbatts) this is where features need multiple labels ('centos' and 'rhel') + featureVersion.Feature.Namespace.Name = "centos" + ":" + strconv.Itoa(osVersion) + } else { + continue + } + + if featureVersion.Feature.Namespace.Name != "" && featureVersion.Feature.Name != "" && featureVersion.Version.String() != "" { + featureVersionParameters[featureVersion.Feature.Namespace.Name+":"+featureVersion.Feature.Name] = featureVersion + } else { + log.Warningf("could not determine a valid package from criterions: %v", criterions) + } } - rhsaNo, _ := strconv.Atoi(r[1]) - if rhsaNo <= firstRHEL5RHSA { - return "" + + // Convert the map to slice. + var featureVersionParametersArray []database.FeatureVersion + for _, fv := range featureVersionParameters { + featureVersionParametersArray = append(featureVersionParametersArray, fv) } - return f.DistFile(r[1]) + + return featureVersionParametersArray } -// Not in the interface +func description(def definition) (desc string) { + // It is much more faster to proceed like this than using a Replacer. + desc = strings.Replace(def.Description, "\n\n\n", " ", -1) + desc = strings.Replace(desc, "\n\n", " ", -1) + desc = strings.Replace(desc, "\n", " ", -1) + return +} -func (f *RHELInfo) ValidOsVersion(osVersion string) bool { - version, err := strconv.Atoi(osVersion) - if err != nil { - return false +func name(def definition) string { + return strings.TrimSpace(def.Title[:strings.Index(def.Title, ": ")]) +} + +func link(def definition) (link string) { + for _, reference := range def.References { + if reference.Source == "RHSA" { + link = reference.URI + break + } } - _, err = types.NewVersion(osVersion) - if err != nil { - return false + + return +} + +func priority(def definition) types.Priority { + // Parse the priority. + priority := strings.TrimSpace(def.Title[strings.LastIndex(def.Title, "(")+1 : len(def.Title)-1]) + + // Normalize the priority. + switch priority { + case "Low": + return types.Low + case "Moderate": + return types.Medium + case "Important": + return types.High + case "Critical": + return types.Critical + default: + log.Warning("could not determine vulnerability priority from: %s.", priority) + return types.Unknown } - return version >= firstConsideredRHEL } + +// Clean deletes any allocated resources. +func (f *RHELFetcher) Clean() {} diff --git a/updater/fetchers/rhel/rhel_test.go b/updater/fetchers/rhel/rhel_test.go index 778eed51..0e9ddbd0 100644 --- a/updater/fetchers/rhel/rhel_test.go +++ b/updater/fetchers/rhel/rhel_test.go @@ -21,7 +21,6 @@ import ( "testing" "github.com/coreos/clair/database" - "github.com/coreos/clair/utils/oval" "github.com/coreos/clair/utils/types" "github.com/stretchr/testify/assert" ) @@ -32,9 +31,7 @@ func TestRHELParser(t *testing.T) { // Test parsing testdata/fetcher_rhel_test.1.xml testFile, _ := os.Open(path + "/testdata/fetcher_rhel_test.1.xml") - rhInfo := &RHELInfo{} - ov := &oval.OvalFetcher{OsInfo: rhInfo} - vulnerabilities, err := ov.ParseOval(testFile) + vulnerabilities, err := parseRHSA(testFile) if assert.Nil(t, err) && assert.Len(t, vulnerabilities, 1) { assert.Equal(t, "RHSA-2015:1193", vulnerabilities[0].Name) assert.Equal(t, "https://rhn.redhat.com/errata/RHSA-2015-1193.html", vulnerabilities[0].Link) @@ -72,7 +69,7 @@ func TestRHELParser(t *testing.T) { // Test parsing testdata/fetcher_rhel_test.2.xml testFile, _ = os.Open(path + "/testdata/fetcher_rhel_test.2.xml") - vulnerabilities, err = ov.ParseOval(testFile) + vulnerabilities, err = parseRHSA(testFile) if assert.Nil(t, err) && assert.Len(t, vulnerabilities, 1) { assert.Equal(t, "RHSA-2015:1207", vulnerabilities[0].Name) assert.Equal(t, "https://rhn.redhat.com/errata/RHSA-2015-1207.html", vulnerabilities[0].Link) diff --git a/updater/fetchers/sle/sle.go b/updater/fetchers/sle/sle.go deleted file mode 100644 index d3f83305..00000000 --- a/updater/fetchers/sle/sle.go +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright 2015 clair authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package sle - -import ( - "regexp" - - "github.com/coreos/clair/updater" - "github.com/coreos/clair/updater/fetchers/opensuse" - "github.com/coreos/clair/utils/oval" - "github.com/coreos/pkg/capnslog" -) - -var log = capnslog.NewPackageLogger("github.com/coreos/clair", "updater/fetchers/sle") -var opensuseInfo = &opensuse.OpenSUSEInfo{} - -func init() { - sleInfo := &SLEInfo{} - - updater.RegisterFetcher(sleInfo.DistName(), - &oval.OvalFetcher{OsInfo: sleInfo}) -} - -// SLEInfo implements oval.OsInfo interface -// See oval.OsInfo for more info on what each method is -// SLE and openSUSE shares most of the code, there are just subtle diffs on -// the name and versions of the distribution -type SLEInfo struct { -} - -func (f *SLEInfo) SecToken() string { - return opensuseInfo.SecToken() -} - -func (f *SLEInfo) IgnoredCriterions() []string { - return opensuseInfo.IgnoredCriterions() -} - -func (f *SLEInfo) OvalURI() string { - return opensuseInfo.OvalURI() -} - -// This differs from openSUSE -func (f *SLEInfo) DistName() string { - return "sle" -} - -func (f *SLEInfo) Namespace() string { - return f.DistName() -} - -func (f *SLEInfo) ParseOsVersion(comment string) string { - return opensuseInfo.ParseOsVersionR(comment, f.CritSystem()) -} - -func (f *SLEInfo) ParsePackageNameVersion(comment string) (string, string) { - return opensuseInfo.ParsePackageNameVersion(comment) -} - -func (f *SLEInfo) ParseFilenameDist(line string) string { - return opensuseInfo.ParseFilenameDistR(line, f.DistRegexp(), f.DistMinVersion()) -} - -// These are diffs with openSUSE - -func (f *SLEInfo) CritSystem() *regexp.Regexp { - return regexp.MustCompile(`SUSE Linux Enterprise Server [^0-9]*(\d+)\s*(SP(\d+)|) is installed`) -} - -func (f *SLEInfo) DistRegexp() *regexp.Regexp { - return regexp.MustCompile(`suse.linux.enterprise.(\d+).xml`) -} - -func (f *SLEInfo) DistMinVersion() float64 { - return 11.4 -} diff --git a/updater/fetchers/sle/sle_test.go b/updater/fetchers/sle/sle_test.go deleted file mode 100644 index 10c7c465..00000000 --- a/updater/fetchers/sle/sle_test.go +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright 2015 clair authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package sle - -import ( - "os" - "path/filepath" - "runtime" - "testing" - - "github.com/coreos/clair/database" - "github.com/coreos/clair/utils/oval" - "github.com/coreos/clair/utils/types" - "github.com/stretchr/testify/assert" -) - -func TestSLEParser(t *testing.T) { - _, filename, _, _ := runtime.Caller(0) - path := filepath.Join(filepath.Dir(filename)) - - // Test parsing testdata/fetcher_sle_test.1.xml - testFile, _ := os.Open(path + "/testdata/fetcher_sle_test.1.xml") - ov := &oval.OvalFetcher{OsInfo: &SLEInfo{}} - vulnerabilities, err := ov.ParseOval(testFile) - if assert.Nil(t, err) && assert.Len(t, vulnerabilities, 1) { - assert.Equal(t, "CVE-2012-2150", vulnerabilities[0].Name) - assert.Equal(t, "http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2012-2150", vulnerabilities[0].Link) - // Severity is not defined for SLE - assert.Equal(t, types.Unknown, vulnerabilities[0].Severity) - assert.Equal(t, `xfs_metadump in xfsprogs before 3.2.4 does not properly obfuscate file data, which allows remote attackers to obtain sensitive information by reading a generated image.`, vulnerabilities[0].Description) - - expectedFeatureVersions := []database.FeatureVersion{ - { - Feature: database.Feature{ - Namespace: database.Namespace{Name: "sle:12"}, - Name: "xfsprogs", - }, - Version: types.NewVersionUnsafe("3.2.1-3.5"), - }, - { - Feature: database.Feature{ - Namespace: database.Namespace{Name: "sle:12.1"}, - Name: "xfsprogs", - }, - Version: types.NewVersionUnsafe("3.2.1-3.5"), - }, - } - - for _, expectedFeatureVersion := range expectedFeatureVersions { - assert.Contains(t, vulnerabilities[0].FixedIn, expectedFeatureVersion) - } - - } - -} diff --git a/updater/fetchers/sle/testdata/fetcher_sle_test.1.xml b/updater/fetchers/sle/testdata/fetcher_sle_test.1.xml deleted file mode 100644 index e718def9..00000000 --- a/updater/fetchers/sle/testdata/fetcher_sle_test.1.xml +++ /dev/null @@ -1,69 +0,0 @@ - - - - - Marcus Updateinfo to OVAL Converter - 5.5 - 2016-06-27T04:04:46 - - - - - CVE-2012-2150 - - SUSE Linux Enterprise Server 12 - - - xfs_metadump in xfsprogs before 3.2.4 does not properly obfuscate file data, which allows remote attackers to obtain sensitive information by reading a generated image. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - sles-release - - - xfsprogs - - - - - 0:3.2.1-3.5 - - - 12 - - - 12.1 - - - diff --git a/utils/oval/oval.go b/utils/oval/oval.go deleted file mode 100644 index f7775b92..00000000 --- a/utils/oval/oval.go +++ /dev/null @@ -1,428 +0,0 @@ -// Copyright 2015 clair authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// This package contains the OvalFetcher definition which is being used -// for fetching update information on OVAL format -// see: https://oval.mitre.org/about/faqs.html#a1 -// -// Example of an oval definition -// -// -// -// -// CVE-1111-11 -// blablabla -// -// -// -// -// -// -// -// -// -// -// -// -// ... -// -// -// ... -// -// -// ... -// -// -// see more complete examples here -// https://oval.mitre.org/language/about/definition.html -// The methods here use an interface (see below) that must be implemented for -// each Distribution in updated/fetchers/ -package oval - -import ( - "bufio" - "encoding/xml" - "fmt" - "io" - "net/http" - "strings" - - "github.com/coreos/clair/database" - "github.com/coreos/clair/updater" - cerrors "github.com/coreos/clair/utils/errors" - "github.com/coreos/clair/utils/types" - "github.com/coreos/pkg/capnslog" -) - -type oval struct { - Definitions []definition `xml:"definitions>definition"` -} - -type definition struct { - Title string `xml:"metadata>title"` - Description string `xml:"metadata>description"` - References []reference `xml:"metadata>reference"` - Criteria criteria `xml:"criteria"` -} - -type reference struct { - Source string `xml:"source,attr"` - URI string `xml:"ref_url,attr"` -} - -type criteria struct { - Operator string `xml:"operator,attr"` - Criterias []*criteria `xml:"criteria"` - Criterions []criterion `xml:"criterion"` -} - -type criterion struct { - Comment string `xml:"comment,attr"` -} - -// OvalFetcher implements updater.Fetcher. -type OvalFetcher struct { - // OsInfo contains specifics to each Linux Distribution (see below) - OsInfo OSInfo -} - -// OSInfo interface contains specifics methods for parsing OVAL definitions -// that must be implemented by each Linux Distribution that uses OVAL -// i.e. Red Hat and SUSE -type OSInfo interface { - // ParsePackageNameVersion should, given a comment in a criterion, return - // the name and the version of the package. - // For example, if the comment is - // glibc is earlier than 3.2 - // it should return glibc and 3.2. - // - // This is based on the assumption that the distributions generate the - // comments automatically and they won't change (I know, not very - // reliable...). - ParsePackageNameVersion(comment string) (string, string) - - // ParseOsVersion should, given a comment in a criterion, return the - // version of the Operating System. - // For example, if the comment is - // SUSE Linux Enterpise Server 12 is installed - // should return 12 - // - // This is based on the assumption that the distributions generate the - // comments automatically and they won't change it (I know, not very - // reliable...). - ParseOsVersion(comment string) string - - // Given a line, parse for the xml file that contains the oval definition - // and returns the filename. - // For example if the line contains - // com.redhat.rhsa-2003.xml, this will be returned. - // - // This is being used in conjunction with OvalUri (see below). Oval Uri - // contains a list of files, and you need ParseFilenameDist to get the - // right ones. - ParseFilenameDist(line string) string - - // OvalUri returns the url where the oval definitions are stored for given - // distributions. See examples: - // https://www.redhat.com/security/data/oval/ - // http://ftp.suse.com/pub/projects/security/oval/ - OvalURI() string - - // DistName returns the distribution name. Mostly used for debugging - // purposes. - DistName() string - - // IgnoredCriterions returns a list of strings that must be ignored when - // parsing the criterions. - // Oval parses parses all criterions by default trying to identify either - // package name and version or distribution version. - IgnoredCriterions() []string - - // SecToken returns a string that is compared with the value of - // reference.source in order to know if that is a security reference for, - // for example, using its url value. - // Example return values: CVE, RHSA. - SecToken() string - - // Namespace stores the namespace that will be used in clair to store the - // vulnerabilities. - Namespace() string -} - -var ( - log = capnslog.NewPackageLogger("github.com/coreos/clair", "utils/oval") -) - -// FetchUpdate gets vulnerability updates from the OVAL definitions. -func (f *OvalFetcher) FetchUpdate(datastore database.Datastore) (resp updater.FetcherResponse, err error) { - log.Infof("fetching %s vulnerabilities", f.OsInfo.DistName()) - - r, err := http.Get(f.OsInfo.OvalURI()) - if err != nil { - log.Errorf("could not download %s's update list: %s", f.OsInfo.DistName(), err) - return resp, cerrors.ErrCouldNotDownload - } - - var distList []string - scanner := bufio.NewScanner(r.Body) - - for scanner.Scan() { - line := scanner.Text() - filename := f.OsInfo.ParseFilenameDist(line) - if filename != "" { - distList = append(distList, filename) - } - } - - for _, filename := range distList { - r, err := http.Get(filename) - if err != nil { - log.Errorf("could not download %s's update file: %s", f.OsInfo.DistName(), err) - return resp, cerrors.ErrCouldNotDownload - } - - vs, err := f.ParseOval(r.Body) - if err != nil { - return resp, err - } - - resp.Vulnerabilities = append(resp.Vulnerabilities, vs...) - } - - // Set the flag if we found anything. - if len(distList) > 0 { - resp.FlagName = f.OsInfo.DistName() + "_updater" - resp.FlagValue = distList[len(distList)-1] - } else { - log.Debug("no files to parse found for %s", f.OsInfo.DistName()) - log.Debug("in %s", f.OsInfo.OvalURI()) - } - - return resp, nil -} - -// Clean deletes any allocated resources. -func (f *OvalFetcher) Clean() {} - -// Parse criterions into an array of FeatureVersion for storing into the database -func (f *OvalFetcher) ToFeatureVersions(possibilities [][]criterion) []database.FeatureVersion { - featureVersionParameters := make(map[string]database.FeatureVersion) - - for _, criterions := range possibilities { - var ( - featureVersion database.FeatureVersion - osVersion string - ) - - for _, c := range criterions { - if osVersion != "" && featureVersion.Feature.Name != "" && - featureVersion.Version.String() != "" { - break - } - tmp_v := f.OsInfo.ParseOsVersion(c.Comment) - if tmp_v != "" { - osVersion = tmp_v - continue - } - - tmp_p_name, tmp_p_version := f.OsInfo.ParsePackageNameVersion(c.Comment) - if tmp_p_version != "" && tmp_p_name != "" { - featureVersion.Feature.Name = tmp_p_name - featureVersion.Version, _ = types.NewVersion(tmp_p_version) - continue - } - - log.Warningf("could not parse criteria: '%s'.", c.Comment) - } - - if osVersion == "" { - log.Warningf("No OS version found for criterions: %#v", criterions) - continue - } - - featureVersion.Feature.Namespace.Name = fmt.Sprintf("%s:%s", f.OsInfo.Namespace(), osVersion) - - if featureVersion.Feature.Name != "" && featureVersion.Version.String() != "" { - featureVersionParameters[featureVersion.Feature.Namespace.Name+":"+featureVersion.Feature.Name] = featureVersion - } else { - log.Warningf("could not determine a valid package from criterions: %v", criterions) - } - } - - var featureVersionParametersArray []database.FeatureVersion - for _, fv := range featureVersionParameters { - featureVersionParametersArray = append(featureVersionParametersArray, fv) - } - - return featureVersionParametersArray -} - -// Parse an Oval file. -func (f *OvalFetcher) ParseOval(ovalReader io.Reader) (vulnerabilities []database.Vulnerability, err error) { - var ov oval - err = xml.NewDecoder(ovalReader).Decode(&ov) - if err != nil { - log.Errorf("could not decode %s's XML: %s", f.OsInfo.DistName(), err) - return vulnerabilities, cerrors.ErrCouldNotParse - } - - for _, definition := range ov.Definitions { - pkgs := f.ToFeatureVersions(f.Possibilities(definition.Criteria)) - - if len(pkgs) > 0 { - vulnerability := database.Vulnerability{ - Name: name(definition), - Link: link(definition, f.OsInfo.SecToken()), - Severity: priority(definition), - Description: description(definition), - } - - vulnerability.FixedIn = append(vulnerability.FixedIn, pkgs...) - - vulnerabilities = append(vulnerabilities, vulnerability) - } - } - - return -} - -// Get the description from a definition element -func description(def definition) (desc string) { - desc = strings.Replace(def.Description, "\n\n\n", " ", -1) - desc = strings.Replace(desc, "\n\n", " ", -1) - desc = strings.Replace(desc, "\n", " ", -1) - - return -} - -// Get the name form a definition element -func name(def definition) string { - title := def.Title - index := strings.Index(title, ": ") - if index == -1 { - index = len(title) - } - return strings.TrimSpace(title[:index]) -} - -// Get the link from a definition element where reference.source matches the secToken -func link(def definition, secToken string) (link string) { - for _, reference := range def.References { - if reference.Source == secToken { - link = reference.URI - break - } - } - - return -} - -// Get priority from a definition -func priority(def definition) types.Priority { - // Parse the priority. - priority := strings.TrimSpace(def.Title[strings.LastIndex(def.Title, "(")+1 : len(def.Title)-1]) - - // Normalize the priority. - switch priority { - case "Low": - return types.Low - case "Moderate": - return types.Medium - case "Important": - return types.High - case "Critical": - return types.Critical - default: - log.Warning("could not determine vulnerability priority from: %s.", priority) - return types.Unknown - } -} - -// Get Criterions elements from a criteria element -func (f *OvalFetcher) Criterions(node criteria) [][]criterion { - var criterions []criterion - - for _, c := range node.Criterions { - ignored := false - for _, ignoredItem := range f.OsInfo.IgnoredCriterions() { - if strings.Contains(c.Comment, ignoredItem) { - ignored = true - break - } - } - - if !ignored { - criterions = append(criterions, c) - } - } - - if node.Operator == "AND" { - return [][]criterion{criterions} - } else if node.Operator == "OR" { - var possibilities [][]criterion - - for _, c := range criterions { - possibilities = append(possibilities, []criterion{c}) - } - - return possibilities - } - - return [][]criterion{} -} - -// Get Possibilities from a criteria element -func (f *OvalFetcher) Possibilities(node criteria) [][]criterion { - if len(node.Criterias) == 0 { - return f.Criterions(node) - } - - var possibilitiesToCompose [][][]criterion - - for _, criteria := range node.Criterias { - possibilitiesToCompose = append(possibilitiesToCompose, f.Possibilities(*criteria)) - } - - if len(node.Criterions) > 0 { - possibilitiesToCompose = append(possibilitiesToCompose, f.Criterions(node)) - } - - var possibilities [][]criterion - - if node.Operator == "AND" { - possibilities = append(possibilities, possibilitiesToCompose[0]...) - - for _, possibilityGroup := range possibilitiesToCompose[1:] { - var newPossibilities [][]criterion - - for _, possibility := range possibilities { - for _, possibilityInGroup := range possibilityGroup { - var p []criterion - - p = append(p, possibility...) - p = append(p, possibilityInGroup...) - - newPossibilities = append(newPossibilities, p) - } - } - - possibilities = newPossibilities - } - } else if node.Operator == "OR" { - for _, possibilityGroup := range possibilitiesToCompose { - possibilities = append(possibilities, possibilityGroup...) - } - } - return possibilities -}