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
+}