3503ddb96f
Get one vulnerability per CVE for Oracle instead of one per ELSA so we can have NVD metadata added to the vulnerabilities. Related: #495, #499.
441 lines
12 KiB
Go
441 lines
12 KiB
Go
// Copyright 2017 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 oracle implements a vulnerability source updater using the
|
|
// Oracle Linux OVAL Database.
|
|
package oracle
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/xml"
|
|
"io"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
"fmt"
|
|
|
|
"github.com/coreos/clair/database"
|
|
"github.com/coreos/clair/ext/versionfmt"
|
|
"github.com/coreos/clair/ext/versionfmt/rpm"
|
|
"github.com/coreos/clair/ext/vulnsrc"
|
|
"github.com/coreos/clair/pkg/commonerr"
|
|
"github.com/coreos/clair/pkg/httputil"
|
|
)
|
|
|
|
const (
|
|
firstOracle5ELSA = 20070057
|
|
ovalURI = "https://linux.oracle.com/oval/"
|
|
elsaFilePrefix = "com.oracle.elsa-"
|
|
updaterFlag = "oracleUpdater"
|
|
affectedType = database.AffectBinaryPackage
|
|
)
|
|
|
|
var (
|
|
ignoredCriterions = []string{
|
|
" is signed with the Oracle Linux",
|
|
".ksplice1.",
|
|
}
|
|
|
|
elsaRegexp = regexp.MustCompile(`com.oracle.elsa-(\d+).xml`)
|
|
)
|
|
|
|
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"`
|
|
Severity string `xml:"metadata>advisory>severity"`
|
|
CVEs []cve `xml:"metadata>advisory>cve"`
|
|
}
|
|
|
|
type reference struct {
|
|
Source string `xml:"source,attr"`
|
|
URI string `xml:"ref_url,attr"`
|
|
ID string `xml:"ref_id,attr"`
|
|
}
|
|
|
|
type cve struct {
|
|
Impact string `xml:"impact,attr"`
|
|
Href string `xml:"href,attr"`
|
|
ID string `xml:",chardata"`
|
|
}
|
|
|
|
type criteria struct {
|
|
Operator string `xml:"operator,attr"`
|
|
Criterias []*criteria `xml:"criteria"`
|
|
Criterions []criterion `xml:"criterion"`
|
|
}
|
|
|
|
type criterion struct {
|
|
Comment string `xml:"comment,attr"`
|
|
}
|
|
|
|
type updater struct{}
|
|
|
|
func init() {
|
|
vulnsrc.RegisterUpdater("oracle", &updater{})
|
|
}
|
|
|
|
func compareELSA(left, right int) int {
|
|
// Fast path equals.
|
|
if right == left {
|
|
return 0
|
|
}
|
|
|
|
lstr := strconv.Itoa(left)
|
|
rstr := strconv.Itoa(right)
|
|
|
|
for i := range lstr {
|
|
// If right is too short to be indexed, left is greater.
|
|
if i >= len(rstr) {
|
|
return 1
|
|
}
|
|
|
|
ldigit, _ := strconv.Atoi(string(lstr[i]))
|
|
rdigit, _ := strconv.Atoi(string(rstr[i]))
|
|
|
|
if ldigit > rdigit {
|
|
return 1
|
|
} else if ldigit < rdigit {
|
|
return -1
|
|
}
|
|
continue
|
|
}
|
|
|
|
// Everything the length of left is the same.
|
|
return len(lstr) - len(rstr)
|
|
}
|
|
|
|
func (u *updater) Update(datastore database.Datastore) (resp vulnsrc.UpdateResponse, err error) {
|
|
log.WithField("package", "Oracle Linux").Info("Start fetching vulnerabilities")
|
|
// Get the first ELSA we have to manage.
|
|
flagValue, ok, err := database.FindKeyValueAndRollback(datastore, updaterFlag)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
if !ok {
|
|
flagValue = ""
|
|
}
|
|
|
|
firstELSA, err := strconv.Atoi(flagValue)
|
|
if firstELSA == 0 || err != nil {
|
|
firstELSA = firstOracle5ELSA
|
|
}
|
|
|
|
// Fetch the update list.
|
|
r, err := httputil.GetWithUserAgent(ovalURI)
|
|
if err != nil {
|
|
log.WithError(err).Error("could not download Oracle's update list")
|
|
return resp, commonerr.ErrCouldNotDownload
|
|
}
|
|
defer r.Body.Close()
|
|
|
|
if !httputil.Status2xx(r) {
|
|
log.WithField("StatusCode", r.StatusCode).Error("Failed to update Oracle")
|
|
return resp, commonerr.ErrCouldNotDownload
|
|
}
|
|
|
|
// Get the list of ELSAs that we have to process.
|
|
var elsaList []int
|
|
scanner := bufio.NewScanner(r.Body)
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
r := elsaRegexp.FindStringSubmatch(line)
|
|
if len(r) == 2 {
|
|
elsaNo, _ := strconv.Atoi(r[1])
|
|
if compareELSA(elsaNo, firstELSA) > 0 {
|
|
elsaList = append(elsaList, elsaNo)
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, elsa := range elsaList {
|
|
// Download the ELSA's XML file.
|
|
r, err := httputil.GetWithUserAgent(ovalURI + elsaFilePrefix + strconv.Itoa(elsa) + ".xml")
|
|
if err != nil {
|
|
log.WithError(err).Error("could not download Oracle's update list")
|
|
return resp, commonerr.ErrCouldNotDownload
|
|
}
|
|
defer r.Body.Close()
|
|
|
|
if !httputil.Status2xx(r) {
|
|
log.WithField("StatusCode", r.StatusCode).Error("Failed to update Oracle")
|
|
return resp, commonerr.ErrCouldNotDownload
|
|
}
|
|
|
|
// Parse the XML.
|
|
vs, err := parseELSA(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(elsaList) > 0 {
|
|
resp.FlagName = updaterFlag
|
|
resp.FlagValue = strconv.Itoa(largest(elsaList))
|
|
} else {
|
|
log.WithField("package", "Oracle Linux").Debug("no update")
|
|
}
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
func largest(list []int) (largest int) {
|
|
for _, element := range list {
|
|
if element > largest {
|
|
largest = element
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (u *updater) Clean() {}
|
|
|
|
func parseELSA(ovalReader io.Reader) (vulnerabilities []database.VulnerabilityWithAffected, err error) {
|
|
// Decode the XML.
|
|
var ov oval
|
|
err = xml.NewDecoder(ovalReader).Decode(&ov)
|
|
if err != nil {
|
|
log.WithError(err).Error("could not decode Oracle's XML")
|
|
err = commonerr.ErrCouldNotParse
|
|
return
|
|
}
|
|
|
|
// Iterate over the definitions and collect any vulnerabilities that affect
|
|
// at least one package.
|
|
for _, definition := range ov.Definitions {
|
|
pkgs := toFeatures(definition.Criteria)
|
|
if len(pkgs) > 0 {
|
|
vulnerability := database.VulnerabilityWithAffected{
|
|
Vulnerability: database.Vulnerability{
|
|
Name: name(definition),
|
|
Link: link(definition),
|
|
Severity: severity(definition.Severity),
|
|
Description: description(definition),
|
|
},
|
|
}
|
|
for _, p := range pkgs {
|
|
vulnerability.Affected = append(vulnerability.Affected, p)
|
|
}
|
|
|
|
// Only ELSA is present
|
|
if len(definition.CVEs) == 0 {
|
|
vulnerabilities = append(vulnerabilities, vulnerability)
|
|
continue
|
|
}
|
|
|
|
// Create one vulnerability per CVE
|
|
for _, currentCVE := range definition.CVEs {
|
|
vulnerability.Name = currentCVE.ID
|
|
vulnerability.Link = currentCVE.Href
|
|
if currentCVE.Impact != "" {
|
|
vulnerability.Severity = severity(currentCVE.Impact)
|
|
} else {
|
|
vulnerability.Severity = severity(definition.Severity)
|
|
}
|
|
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 toFeatures(criteria criteria) []database.AffectedFeature {
|
|
// There are duplicates in Oracle .xml files.
|
|
// This map is for deduplication.
|
|
featureVersionParameters := make(map[string]database.AffectedFeature)
|
|
|
|
possibilities := getPossibilities(criteria)
|
|
for _, criterions := range possibilities {
|
|
var (
|
|
featureVersion database.AffectedFeature
|
|
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("Oracle Linux ")
|
|
osVersion, err = strconv.Atoi(strings.TrimSpace(c.Comment[prefixLen : prefixLen+strings.Index(c.Comment[prefixLen:], " ")]))
|
|
if err != nil {
|
|
log.WithError(err).WithField("comment", c.Comment).Warning("could not parse Oracle Linux release version from comment")
|
|
}
|
|
} else if strings.Contains(c.Comment, " is earlier than ") {
|
|
const prefixLen = len(" is earlier than ")
|
|
featureVersion.FeatureName = strings.TrimSpace(c.Comment[:strings.Index(c.Comment, " is earlier than ")])
|
|
featureVersion.AffectedType = affectedType
|
|
version := c.Comment[strings.Index(c.Comment, " is earlier than ")+prefixLen:]
|
|
err := versionfmt.Valid(rpm.ParserName, version)
|
|
if err != nil {
|
|
log.WithError(err).WithField("version", version).Warning("could not parse package version. skipping")
|
|
} else {
|
|
featureVersion.AffectedVersion = version
|
|
if version != versionfmt.MaxVersion {
|
|
featureVersion.FixedInVersion = version
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
featureVersion.Namespace.Name = "oracle" + ":" + strconv.Itoa(osVersion)
|
|
featureVersion.Namespace.VersionFormat = rpm.ParserName
|
|
|
|
if featureVersion.Namespace.Name != "" && featureVersion.FeatureName != "" && featureVersion.AffectedVersion != "" && featureVersion.FixedInVersion != "" {
|
|
featureVersionParameters[featureVersion.Namespace.Name+":"+featureVersion.FeatureName] = featureVersion
|
|
} else {
|
|
log.WithField("criterions", fmt.Sprintf("%v", criterions)).Warning("could not determine a valid package from criterions")
|
|
}
|
|
}
|
|
|
|
// Convert the map to slice.
|
|
var featureVersionParametersArray []database.AffectedFeature
|
|
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 == "elsa" {
|
|
link = reference.URI
|
|
break
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func severity(sev string) database.Severity {
|
|
switch strings.ToLower(sev) {
|
|
case "n/a":
|
|
return database.NegligibleSeverity
|
|
case "low":
|
|
return database.LowSeverity
|
|
case "moderate":
|
|
return database.MediumSeverity
|
|
case "important", "high": // some ELSAs have "high" instead of "important"
|
|
return database.HighSeverity
|
|
case "critical":
|
|
return database.CriticalSeverity
|
|
default:
|
|
log.WithField("severity", sev).Warning("could not determine vulnerability severity")
|
|
return database.UnknownSeverity
|
|
}
|
|
}
|