diff --git a/cmd/clair/main.go b/cmd/clair/main.go index df3d0ba2..732a8923 100644 --- a/cmd/clair/main.go +++ b/cmd/clair/main.go @@ -29,7 +29,9 @@ import ( _ "github.com/coreos/clair/notifier/notifiers" _ "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 new file mode 100644 index 00000000..87c8cb6f --- /dev/null +++ b/updater/fetchers/opensuse/opensuse.go @@ -0,0 +1,129 @@ +// 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 new file mode 100644 index 00000000..b2317009 --- /dev/null +++ b/updater/fetchers/opensuse/opensuse_test.go @@ -0,0 +1,66 @@ +// 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 new file mode 100644 index 00000000..258f8254 --- /dev/null +++ b/updater/fetchers/opensuse/testdata/fetcher_opensuse_test.1.xml @@ -0,0 +1,66 @@ + + + + + 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 43d1d5fe..d0a47fd2 100644 --- a/updater/fetchers/rhel/rhel.go +++ b/updater/fetchers/rhel/rhel.go @@ -15,17 +15,12 @@ package rhel import ( - "bufio" - "encoding/xml" - "io" - "net/http" "regexp" "strconv" "strings" - "github.com/coreos/clair/database" "github.com/coreos/clair/updater" - cerrors "github.com/coreos/clair/utils/errors" + "github.com/coreos/clair/utils/oval" "github.com/coreos/clair/utils/types" "github.com/coreos/pkg/capnslog" ) @@ -34,327 +29,101 @@ 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{ + rhsaRegexp = regexp.MustCompile(`com.redhat.rhsa-(\d+).xml`) + log = capnslog.NewPackageLogger("github.com/coreos/clair", "updater/fetchers/rhel") +) + +func init() { + rhelInfo := &RHELInfo{} + updater.RegisterFetcher(rhelInfo.DistName(), + &oval.OvalFetcher{OsInfo: rhelInfo}) +} + +// RHELInfo implements oval.OsInfo interface +// See oval.OsInfo for more info on what each method is +type RHELInfo struct { +} + +func (f *RHELInfo) DistFile(item string) string { + rhsaFilePrefix := "com.redhat.rhsa-" + return f.OvalURI() + rhsaFilePrefix + item + ".xml" +} + +func (f *RHELInfo) SecToken() string { + return "RHSA" +} + +func (f *RHELInfo) IgnoredCriterions() []string { + return []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") -) - -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"` +func (f *RHELInfo) OvalURI() string { + return "https://www.redhat.com/security/data/oval/" } -type reference struct { - Source string `xml:"source,attr"` - URI string `xml:"ref_url,attr"` +func (f *RHELInfo) DistName() string { + return "RHEL" } -type criteria struct { - Operator string `xml:"operator,attr"` - Criterias []*criteria `xml:"criteria"` - Criterions []criterion `xml:"criterion"` +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" } -type criterion struct { - Comment string `xml:"comment,attr"` +func (f *RHELInfo) ParseOsVersion(comment string) string { + if !strings.Contains(comment, " is installed") { + return "" + } + const prefixLen = len("Red Hat Enterprise Linux ") + osVersion := strings.TrimSpace(comment[prefixLen : prefixLen+strings.Index(comment[prefixLen:], " ")]) + if !f.ValidOsVersion(osVersion) { + return "" + } + return osVersion } -// 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) ParsePackageNameVersion(comment string) (string, string) { + if !strings.Contains(comment, " is earlier than ") { + return "", "" + } + 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 } -// 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") +func (f *RHELInfo) ParseFilenameDist(line string) string { + r := rhsaRegexp.FindStringSubmatch(line) + if len(r) != 2 { + return "" + } + rhsaNo, _ := strconv.Atoi(r[1]) + if rhsaNo <= firstRHEL5RHSA { + return "" + } + return f.DistFile(r[1]) +} - // Get the first RHSA we have to manage. - flagValue, err := datastore.GetKeyValue(updaterFlag) +// Not in the interface + +func (f *RHELInfo) ValidOsVersion(osVersion string) bool { + version, err := strconv.Atoi(osVersion) if err != nil { - return resp, err + return false } - firstRHSA, err := strconv.Atoi(flagValue) - if firstRHSA == 0 || err != nil { - firstRHSA = firstRHEL5RHSA - } - - // Fetch the update list. - r, err := http.Get(ovalURI) + _, err = types.NewVersion(osVersion) if err != nil { - log.Errorf("could not download RHEL's update list: %s", err) - return resp, cerrors.ErrCouldNotDownload + return false } - - // 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 + return version >= firstConsideredRHEL } - -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 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) - } - } - - 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{} -} - -func getPossibilities(node criteria) [][]criterion { - if len(node.Criterias) == 0 { - return getCriterions(node) - } - - 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 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) - } - } - - // Convert the map to slice. - var featureVersionParametersArray []database.FeatureVersion - for _, fv := range featureVersionParameters { - featureVersionParametersArray = append(featureVersionParametersArray, fv) - } - - return featureVersionParametersArray -} - -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 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 - } - } - - 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 - } -} - -// 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 0e9ddbd0..778eed51 100644 --- a/updater/fetchers/rhel/rhel_test.go +++ b/updater/fetchers/rhel/rhel_test.go @@ -21,6 +21,7 @@ import ( "testing" "github.com/coreos/clair/database" + "github.com/coreos/clair/utils/oval" "github.com/coreos/clair/utils/types" "github.com/stretchr/testify/assert" ) @@ -31,7 +32,9 @@ func TestRHELParser(t *testing.T) { // Test parsing testdata/fetcher_rhel_test.1.xml testFile, _ := os.Open(path + "/testdata/fetcher_rhel_test.1.xml") - vulnerabilities, err := parseRHSA(testFile) + rhInfo := &RHELInfo{} + ov := &oval.OvalFetcher{OsInfo: rhInfo} + vulnerabilities, err := ov.ParseOval(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) @@ -69,7 +72,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 = parseRHSA(testFile) + vulnerabilities, err = ov.ParseOval(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 new file mode 100644 index 00000000..d3f83305 --- /dev/null +++ b/updater/fetchers/sle/sle.go @@ -0,0 +1,88 @@ +// 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 new file mode 100644 index 00000000..10c7c465 --- /dev/null +++ b/updater/fetchers/sle/sle_test.go @@ -0,0 +1,67 @@ +// 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 new file mode 100644 index 00000000..e718def9 --- /dev/null +++ b/updater/fetchers/sle/testdata/fetcher_sle_test.1.xml @@ -0,0 +1,69 @@ + + + + + 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 new file mode 100644 index 00000000..e27c6eff --- /dev/null +++ b/utils/oval/oval.go @@ -0,0 +1,429 @@ +// 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.Info("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.Warning("No OS version found for criterions") + log.Warning(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 +}