Integrated a fetcher for openSUSE and for SUSE Linux Enterprise

We extracted oval parser from rhel and used that for opensuse and
SUSE Linux Enterpise

Signed-off-by: Thomas Boerger <tboerger@suse.de>
Signed-off-by: Jordi Massaguer Pla <jmassaguerpla@suse.de>
This commit is contained in:
Jordi Massaguer Pla 2016-09-23 11:59:22 +02:00
parent 051564facd
commit b8ceb0c461
10 changed files with 994 additions and 306 deletions

View File

@ -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"

View File

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

View File

@ -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)
}
}
}

View File

@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8"?>
<oval_definitions
xsi:schemaLocation="http://oval.mitre.org/XMLSchema/oval-definitions-5#linux linux-definitions-schema.xsd http://oval.mitre.org/XMLSchema/oval-definitions-5#unix unix-definitions-schema.xsd http://oval.mitre.org/XMLSchema/oval-definitions-5 oval-definitions-schema.xsd http://oval.mitre.org/XMLSchema/oval-common-5 oval-common-schema.xsd"
xmlns="http://oval.mitre.org/XMLSchema/oval-definitions-5"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:oval="http://oval.mitre.org/XMLSchema/oval-common-5"
xmlns:oval-def="http://oval.mitre.org/XMLSchema/oval-definitions-5">
<generator>
<oval:product_name>Marcus Updateinfo to OVAL Converter</oval:product_name>
<oval:schema_version>5.5</oval:schema_version>
<oval:timestamp>2016-06-27T04:04:46</oval:timestamp>
</generator>
<definitions>
<definition id="oval:org.opensuse.security:def:20122150" version="1" class="vulnerability">
<metadata>
<title>CVE-2012-2150</title>
<affected family="unix">
<platform>openSUSE Leap 42.1</platform>
</affected>
<reference ref_id="CVE-2012-2150" ref_url="http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2012-2150" source="CVE"/>
<description>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.</description>
</metadata>
<criteria operator="AND">
<criterion test_ref="oval:org.opensuse.security:tst:2009117743" comment="openSUSE Leap 42.1 is installed"/>
<criteria operator="OR">
<criterion test_ref="oval:org.opensuse.security:tst:2009120999" comment="xfsprogs-3.2.1-5.1 is installed"/>
<criterion test_ref="oval:org.opensuse.security:tst:2009121000" comment="xfsprogs-devel-3.2.1-5.1 is installed"/>
</criteria>
</criteria>
</definition>
</definitions>
<tests>
<rpminfo_test id="oval:org.opensuse.security:tst:2009117743" version="1" comment="openSUSE-release is ==42.1" check="at least one" xmlns="http://oval.mitre.org/XMLSchema/oval-definitions-5#linux">
<object object_ref="oval:org.opensuse.security:obj:2009031246"/>
<state state_ref="oval:org.opensuse.security:ste:2009046321"/>
</rpminfo_test>
<rpminfo_test id="oval:org.opensuse.security:tst:2009120999" version="1" comment="xfsprogs is &lt;3.2.1-5.1" check="at least one" xmlns="http://oval.mitre.org/XMLSchema/oval-definitions-5#linux">
<object object_ref="oval:org.opensuse.security:obj:2009032555"/>
<state state_ref="oval:org.opensuse.security:ste:2009046736"/>
</rpminfo_test>
<rpminfo_test id="oval:org.opensuse.security:tst:2009121000" version="1" comment="xfsprogs-devel is &lt;3.2.1-5.1" check="at least one" xmlns="http://oval.mitre.org/XMLSchema/oval-definitions-5#linux">
<object object_ref="oval:org.opensuse.security:obj:2009032648"/>
<state state_ref="oval:org.opensuse.security:ste:2009046736"/>
</rpminfo_test>
</tests>
<objects>
<rpminfo_object id="oval:org.opensuse.security:obj:2009032648" version="1" xmlns="http://oval.mitre.org/XMLSchema/oval-definitions-5#linux">
<name>xfsprogs-devel</name>
</rpminfo_object>
<rpminfo_object id="oval:org.opensuse.security:obj:2009031246" version="1" xmlns="http://oval.mitre.org/XMLSchema/oval-definitions-5#linux">
<name>openSUSE-release</name>
</rpminfo_object>
<rpminfo_object id="oval:org.opensuse.security:obj:2009032555" version="1" xmlns="http://oval.mitre.org/XMLSchema/oval-definitions-5#linux">
<name>xfsprogs</name>
</rpminfo_object>
</objects>
<states>
<rpminfo_state id="oval:org.opensuse.security:ste:2009046736" version="1" xmlns="http://oval.mitre.org/XMLSchema/oval-definitions-5#linux">
<evr datatype="evr_string" operation="less than">0:3.2.1-5.1</evr>
</rpminfo_state>
<rpminfo_state id="oval:org.opensuse.security:ste:2009046321" version="1" xmlns="http://oval.mitre.org/XMLSchema/oval-definitions-5#linux">
<version operation="equals">42.1</version>
</rpminfo_state>
</states>
</oval_definitions>

View File

@ -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() {}

View File

@ -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)

View File

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

View File

@ -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)
}
}
}

View File

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8"?>
<oval_definitions
xsi:schemaLocation="http://oval.mitre.org/XMLSchema/oval-definitions-5#linux linux-definitions-schema.xsd http://oval.mitre.org/XMLSchema/oval-definitions-5#unix unix-definitions-schema.xsd http://oval.mitre.org/XMLSchema/oval-definitions-5 oval-definitions-schema.xsd http://oval.mitre.org/XMLSchema/oval-common-5 oval-common-schema.xsd"
xmlns="http://oval.mitre.org/XMLSchema/oval-definitions-5"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:oval="http://oval.mitre.org/XMLSchema/oval-common-5"
xmlns:oval-def="http://oval.mitre.org/XMLSchema/oval-definitions-5">
<generator>
<oval:product_name>Marcus Updateinfo to OVAL Converter</oval:product_name>
<oval:schema_version>5.5</oval:schema_version>
<oval:timestamp>2016-06-27T04:04:46</oval:timestamp>
</generator>
<definitions>
<definition id="oval:org.opensuse.security:def:20122150" version="1" class="vulnerability">
<metadata>
<title>CVE-2012-2150</title>
<affected family="unix">
<platform>SUSE Linux Enterprise Server 12</platform>
</affected>
<reference ref_id="CVE-2012-2150" ref_url="http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2012-2150" source="CVE"/>
<description>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.</description>
</metadata>
<criteria operator="OR">
<criteria operator="AND">
<criterion test_ref="oval:org.opensuse.security:tst:2009116126" comment="SUSE Linux Enterprise Server 12 is installed"/>
<criterion test_ref="oval:org.opensuse.security:tst:2009116182" comment="xfsprogs-3.2.1-3.5 is installed"/>
</criteria>
<criteria operator="AND">
<criterion test_ref="oval:org.opensuse.security:tst:2009118803" comment="SUSE Linux Enterprise Server 12 SP1 is installed"/>
<criterion test_ref="oval:org.opensuse.security:tst:2009116182" comment="xfsprogs-3.2.1-3.5 is installed"/>
</criteria>
</criteria>
</definition>
</definitions>
<tests>
<rpminfo_test id="oval:org.opensuse.security:tst:2009116126" version="1" comment="sles-release is ==12" check="at least one" xmlns="http://oval.mitre.org/XMLSchema/oval-definitions-5#linux">
<object object_ref="oval:org.opensuse.security:obj:2009030884"/>
<state state_ref="oval:org.opensuse.security:ste:2009045919"/>
</rpminfo_test>
<rpminfo_test id="oval:org.opensuse.security:tst:2009116126" version="1" comment="sles-release is ==12.1" check="at least one" xmlns="http://oval.mitre.org/XMLSchema/oval-definitions-5#linux">
<object object_ref="oval:org.opensuse.security:obj:2009030884"/>
<state state_ref="oval:org.opensuse.security:ste:2009045920"/>
</rpminfo_test>
<rpminfo_test id="oval:org.opensuse.security:tst:2009116182" version="1" comment="xfsprogs is &lt;3.2.1-3.5" check="at least one" xmlns="http://oval.mitre.org/XMLSchema/oval-definitions-5#linux">
<object object_ref="oval:org.opensuse.security:obj:2009032555"/>
<state state_ref="oval:org.opensuse.security:ste:2009046736"/>
</rpminfo_test>
</tests>
<objects>
<rpminfo_object id="oval:org.opensuse.security:obj:2009030884" version="1" xmlns="http://oval.mitre.org/XMLSchema/oval-definitions-5#linux">
<name>sles-release</name>
</rpminfo_object>
<rpminfo_object id="oval:org.opensuse.security:obj:2009032555" version="1" xmlns="http://oval.mitre.org/XMLSchema/oval-definitions-5#linux">
<name>xfsprogs</name>
</rpminfo_object>
</objects>
<states>
<rpminfo_state id="oval:org.opensuse.security:ste:2009046736" version="1" xmlns="http://oval.mitre.org/XMLSchema/oval-definitions-5#linux">
<evr datatype="evr_string" operation="less than">0:3.2.1-3.5</evr>
</rpminfo_state>
<rpminfo_state id="oval:org.opensuse.security:ste:2009045919" version="1" xmlns="http://oval.mitre.org/XMLSchema/oval-definitions-5#linux">
<version operation="equals">12</version>
</rpminfo_state>
<rpminfo_state id="oval:org.opensuse.security:ste:2009045920" version="1" xmlns="http://oval.mitre.org/XMLSchema/oval-definitions-5#linux">
<version operation="equals">12.1</version>
</rpminfo_state>
</states>
</oval_definitions>

429
utils/oval/oval.go Normal file
View File

@ -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
// <oval_definitions xmlns=.....>
// <definitions>
// <definition>
// <metadata>
// <title>CVE-1111-11</title>
// <description>blablabla</description>
// <reference source="CVE" ref_id="CVE-1111-11" ref_url="http...."/>
// <reference source="RHSA" ref_id="RHSA-111:11" ref_url="http...."/>
// </metadata>
// <criteria operator="AND">
// <criterion test_ref="123" comment="glibc is ....">
// </criterion>
// <criterion test_ref="456" comment=".... is signed with Red Hat....">
// </criterion>
// </criteria>
// </definition>
// </definitions>
// <tests>
// ...
// </tests>
// <objects>
// ...
// </objects>
// <states>
// ...
// </states>
// </oval_definitions>
// 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
}