diff --git a/cmd/clair/main.go b/cmd/clair/main.go index df3d0ba2be..4c07ea0d8a 100644 --- a/cmd/clair/main.go +++ b/cmd/clair/main.go @@ -29,6 +29,7 @@ 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/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 0000000000..a4b57f989c --- /dev/null +++ b/updater/fetchers/opensuse/opensuse.go @@ -0,0 +1,101 @@ +// 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/updater/fetchers/oval" + "github.com/coreos/pkg/capnslog" +) + +var log = capnslog.NewPackageLogger("github.com/coreos/clair", "updater/fetchers/sle") + +func init() { + opensuse_info := &OpenSUSE_Info{} + + updater.RegisterFetcher(opensuse_info.DistName(), + &oval.OvalFetcher{Os_info: opensuse_info}) +} + +type OpenSUSE_Info struct { +} + +func (f *OpenSUSE_Info) CritSystem() *regexp.Regexp { + return regexp.MustCompile(`openSUSE [^0-9]*(\d+\.\d+)[^0-9]* is installed`) +} + +func (f *OpenSUSE_Info) CritPackage() *regexp.Regexp { + return regexp.MustCompile(`(.*)-(.*\-[\d\.]+) is installed`) +} + +func (f *OpenSUSE_Info) DistRegexp() *regexp.Regexp { + return regexp.MustCompile(`opensuse.[^0-9]*(\d+\.\d+).xml`) +} + +func (f *OpenSUSE_Info) OvalURI() string { + return "http://ftp.suse.com/pub/projects/security/oval/" +} + +func (f *OpenSUSE_Info) UpdaterFlag() string { + return "opensuseUpdater" +} + +func (f *OpenSUSE_Info) DistMinVersion() float64 { + return 13.1 +} + +func (f *OpenSUSE_Info) DistName() string { + return "opensuse" +} + +func (f *OpenSUSE_Info) ParseOsVersion(comment string) string { + systemMatch := f.CritSystem().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) + } + + // log.Infof("got %s as system version from: %s", osVersion, c.Comment) + return osVersion + +} + +func (f *OpenSUSE_Info) 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 *OpenSUSE_Info) ParseFilenameDistVersion(line string) (string, float64) { + r := f.DistRegexp().FindStringSubmatch(line) + if len(r) == 2 { + distVersion, _ := strconv.ParseFloat(r[1], 32) + return r[0], distVersion + } + return "", 0 +} diff --git a/updater/fetchers/opensuse/opensuse_test.go b/updater/fetchers/opensuse/opensuse_test.go new file mode 100644 index 0000000000..eb2d2f6e40 --- /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/updater/fetchers/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{Os_info: &OpenSUSE_Info{}} + 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 0000000000..258f8254e7 --- /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/oval/oval.go b/updater/fetchers/oval/oval.go new file mode 100644 index 0000000000..e9275e79e6 --- /dev/null +++ b/updater/fetchers/oval/oval.go @@ -0,0 +1,306 @@ +// 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 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 { + Os_info OSInfo +} + +type OSInfo interface { + ParsePackageNameVersion(string) (string, string) + ParseOsVersion(string) string + ParseFilenameDistVersion(string) (string, float64) + OvalURI() string + UpdaterFlag() string + DistMinVersion() float64 + DistName() string +} + +var ( + log = capnslog.NewPackageLogger("github.com/coreos/clair", "updater/fetchers/sle") +) + +// 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.Os_info.DistName()) + + // TODO: Skip already loaded records + + // flagValue, err := datastore.KeyValue(f.Os_info.UpdaterFlag) + + // if err != nil { + // return resp, err + // } + + r, err := http.Get(f.Os_info.OvalURI()) + + if err != nil { + log.Errorf("could not download %s's update list: %s", f.Os_info.DistName(), err) + return resp, cerrors.ErrCouldNotDownload + } + + var distList []string + scanner := bufio.NewScanner(r.Body) + + for scanner.Scan() { + line := scanner.Text() + filename, distVersion := f.Os_info.ParseFilenameDistVersion(line) + if distVersion >= f.Os_info.DistMinVersion() { + distList = append(distList, filename) + } + } + + for _, distFile := range distList { + r, err := http.Get(f.Os_info.OvalURI() + distFile) + + if err != nil { + log.Errorf("could not download %s's update file: %s", f.Os_info.DistName(), err) + return resp, cerrors.ErrCouldNotDownload + } + + vs, err := f.ParseOval(r.Body) + + if err != nil { + return resp, err + } + + for _, v := range vs { + resp.Vulnerabilities = append(resp.Vulnerabilities, v) + } + } + + return resp, nil +} + +// Clean deletes any allocated resources. +func (f *OvalFetcher) Clean() {} + +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 { + tmp_v := f.Os_info.ParseOsVersion(c.Comment) + if tmp_v != "" { + osVersion = tmp_v + continue + } + + tmp_p_name, tmp_p_version := f.Os_info.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(criterions) + } + + featureVersion.Feature.Namespace.Name = fmt.Sprintf("%s:%s", f.Os_info.DistName(), 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 +} +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.Os_info.DistName(), err) + return vulnerabilities, cerrors.ErrCouldNotParse + } + + for _, definition := range ov.Definitions { + pkgs := f.ToFeatureVersions(getPossibilities(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 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 +} + +func name(def definition) string { + return strings.TrimSpace(def.Title) +} + +func link(def definition) (link string) { + for _, reference := range def.References { + if reference.Source == "CVE" { + link = reference.URI + break + } + } + + return +} + +func priority(def definition) types.Priority { + // The OVAL files provided by SUSE doesn't include any priority/severity yet. + return types.Unknown +} + +func getCriterions(node criteria) [][]criterion { + var criterions []criterion + + for _, c := range node.Criterions { + 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 +} diff --git a/updater/fetchers/sle/sle.go b/updater/fetchers/sle/sle.go new file mode 100644 index 0000000000..b7addcd2ad --- /dev/null +++ b/updater/fetchers/sle/sle.go @@ -0,0 +1,101 @@ +// 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 ( + "fmt" + "regexp" + "strconv" + + "github.com/coreos/clair/updater" + "github.com/coreos/clair/updater/fetchers/oval" + "github.com/coreos/pkg/capnslog" +) + +var log = capnslog.NewPackageLogger("github.com/coreos/clair", "updater/fetchers/sle") + +func init() { + sle_info := &SLE_Info{} + + updater.RegisterFetcher(sle_info.DistName(), + &oval.OvalFetcher{Os_info: sle_info}) +} + +type SLE_Info struct { +} + +func (f *SLE_Info) CritSystem() *regexp.Regexp { + return regexp.MustCompile(`SUSE Linux Enterprise Server [^0-9]*(\d+)\s*(SP(\d+)|) is installed`) +} + +func (f *SLE_Info) CritPackage() *regexp.Regexp { + return regexp.MustCompile(`(.*)-(.*\-[\d\.]+) is installed`) +} + +func (f *SLE_Info) DistRegexp() *regexp.Regexp { + return regexp.MustCompile(`suse.linux.enterprise.(\d+).xml`) +} + +func (f *SLE_Info) OvalURI() string { + return "http://ftp.suse.com/pub/projects/security/oval/" +} + +func (f *SLE_Info) UpdaterFlag() string { + return "sleUpdater" +} + +func (f *SLE_Info) DistMinVersion() float64 { + return 11.4 +} + +func (f *SLE_Info) DistName() string { + return "sle" +} + +func (f *SLE_Info) ParseOsVersion(comment string) string { + systemMatch := f.CritSystem().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) + } + + // log.Infof("got %s as system version from: %s", osVersion, c.Comment) + return osVersion + +} + +func (f *SLE_Info) 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 *SLE_Info) ParseFilenameDistVersion(line string) (string, float64) { + r := f.DistRegexp().FindStringSubmatch(line) + if len(r) == 2 { + distVersion, _ := strconv.ParseFloat(r[1], 32) + return r[0], distVersion + } + return "", 0 +} diff --git a/updater/fetchers/sle/sle_test.go b/updater/fetchers/sle/sle_test.go new file mode 100644 index 0000000000..394ed3dcc6 --- /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/updater/fetchers/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{Os_info: &SLE_Info{}} + 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 0000000000..e718def9de --- /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 + + +