You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
clair/ext/vulnsrc/amzn/amzn.go

319 lines
9.0 KiB

// 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 amzn implements a vulnerability source updater using
// ALAS (Amazon Linux Security Advisories).
package amzn
import (
"bufio"
"compress/gzip"
"encoding/xml"
"io"
"regexp"
"strings"
log "github.com/sirupsen/logrus"
"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 (
amazonLinux1Name = "Amazon Linux 2018.03"
amazonLinux1Namespace = "amzn:2018.03"
amazonLinux1UpdaterFlag = "amazonLinux1Updater"
amazonLinux1MirrorListURI = "http://repo.us-west-2.amazonaws.com/2018.03/updates/x86_64/mirror.list"
amazonLinux2Name = "Amazon Linux 2"
amazonLinux2Namespace = "amzn:2"
amazonLinux2UpdaterFlag = "amazonLinux2Updater"
amazonLinux2MirrorListURI = "https://cdn.amazonlinux.com/2/core/latest/x86_64/mirror.list"
)
type updater struct {
Name string
Namespace string
UpdaterFlag string
MirrorListURI string
}
func init() {
// Register updater for Amazon Linux 2018.03.
amazonLinux1Updater := updater {
Name: amazonLinux1Name,
Namespace: amazonLinux1Namespace,
UpdaterFlag: amazonLinux1UpdaterFlag,
MirrorListURI: amazonLinux1MirrorListURI,
}
vulnsrc.RegisterUpdater("amzn", &amazonLinux1Updater)
// Register updater for Amazon Linux 2.
amazonLinux2Updater := updater {
Name: amazonLinux2Name,
Namespace: amazonLinux2Namespace,
UpdaterFlag: amazonLinux2UpdaterFlag,
MirrorListURI: amazonLinux2MirrorListURI,
}
vulnsrc.RegisterUpdater("amzn2", &amazonLinux2Updater)
}
func (u *updater) Update(datastore database.Datastore) (response vulnsrc.UpdateResponse, err error) {
log.WithField("package", u.Name).Info("Start fetching vulnerabilities")
// Get the flag value (the timestamp of the latest ALAS of the previous update).
flagValue, found, err := database.FindKeyValueAndRollback(datastore, u.UpdaterFlag)
if err != nil {
return response, err
}
if !found {
flagValue = "";
}
var timestamp string
// Get the ALASs from updateinfo.xml.gz from the repos.
updateInfo, err := u.getUpdateInfo()
if err != nil {
return response, err
}
// Get the ALASs which were issued/updated since the previous update.
var alasList []ALAS
for _, alas := range updateInfo.ALASList {
if compareTimestamp(alas.Updated.Date, flagValue) > 0 {
alasList = append(alasList, alas)
if compareTimestamp(alas.Updated.Date, timestamp) > 0 {
timestamp = alas.Updated.Date
}
}
}
// Get the vulnerabilities.
response.Vulnerabilities, err = u.alasListToVulnerabilities(alasList)
if err != nil {
return response, err
}
// Set the flag value.
if timestamp != "" {
response.FlagName = u.UpdaterFlag
response.FlagValue = timestamp
} else {
log.WithField("package", u.Name).Debug("no update")
}
return response, err
}
func (u *updater) Clean() {
}
func (u *updater) getUpdateInfo() (updateInfo UpdateInfo, err error) {
// Get the URI of updateinfo.xml.gz.
updateInfoURI, err := u.getUpdateInfoURI()
if err != nil {
return updateInfo, err
}
// Download updateinfo.xml.gz.
updateInfoResponse, err := httputil.GetWithUserAgent(updateInfoURI)
if err != nil {
log.WithError(err).Error("could not download updateinfo.xml.gz")
return updateInfo, commonerr.ErrCouldNotDownload
}
defer updateInfoResponse.Body.Close()
if !httputil.Status2xx(updateInfoResponse) {
log.WithField("StatusCode", updateInfoResponse.StatusCode).Error("could not download updateinfo.xml.gz")
return updateInfo, commonerr.ErrCouldNotDownload
}
// Decompress updateinfo.xml.gz.
updateInfoXml, err := gzip.NewReader(updateInfoResponse.Body)
if err != nil {
log.WithError(err).Error("could not decompress updateinfo.xml.gz")
return updateInfo, commonerr.ErrCouldNotDownload
}
defer updateInfoXml.Close()
// Decode updateinfo.xml.
updateInfo, err = decodeUpdateInfo(updateInfoXml)
if err != nil {
log.WithError(err).Error("could not decode updateinfo.xml")
return updateInfo, err
}
return
}
func (u *updater) getUpdateInfoURI() (updateInfoURI string, err error) {
// Download mirror.list
mirrorListResponse, err := httputil.GetWithUserAgent(u.MirrorListURI)
if err != nil {
log.WithError(err).Error("could not download mirror list")
return updateInfoURI, commonerr.ErrCouldNotDownload
}
defer mirrorListResponse.Body.Close()
// Parse the URI of the first mirror.
scanner := bufio.NewScanner(mirrorListResponse.Body)
success := scanner.Scan()
if success != true {
log.WithError(err).Error("could not parse mirror list")
}
mirrorURI := scanner.Text()
// Download repomd.xml.
repoMdURI := mirrorURI + "/repodata/repomd.xml"
repoMdResponse, err := httputil.GetWithUserAgent(repoMdURI)
if err != nil {
log.WithError(err).Error("could not download repomd.xml")
return updateInfoURI, commonerr.ErrCouldNotDownload
}
defer repoMdResponse.Body.Close()
// Decode repomd.xml.
var repoMd RepoMd
err = xml.NewDecoder(repoMdResponse.Body).Decode(&repoMd)
if err != nil {
log.WithError(err).Error("could not decode repomd.xml")
return updateInfoURI, commonerr.ErrCouldNotDownload
}
// Parse the URI of updateinfo.xml.gz.
for _, repo := range repoMd.RepoList {
if repo.Type == "updateinfo" {
updateInfoURI = mirrorURI + "/" + repo.Location.Href
break
}
}
if updateInfoURI == "" {
log.Error("could not find updateinfo in repomd.xml")
return updateInfoURI, commonerr.ErrCouldNotDownload
}
return
}
func decodeUpdateInfo(updateInfoReader io.Reader) (updateInfo UpdateInfo, err error) {
err = xml.NewDecoder(updateInfoReader).Decode(&updateInfo)
if err != nil {
return updateInfo, err
}
return
}
func (u *updater) alasListToVulnerabilities(alasList []ALAS) (vulnerabilities []database.VulnerabilityWithAffected, err error) {
for _, alas := range alasList {
featureVersions := u.alasToFeatureVersions(alas)
if len(featureVersions) > 0 {
vulnerability := database.VulnerabilityWithAffected{
Vulnerability: database.Vulnerability{
Name: u.alasToName(alas),
Link: u.alasToLink(alas),
Severity: u.alasToSeverity(alas),
Description: u.alasToDescription(alas),
},
Affected: featureVersions,
}
vulnerabilities = append(vulnerabilities, vulnerability)
}
}
return
}
func (u *updater) alasToName(alas ALAS) string {
return alas.Id
}
func (u *updater) alasToLink(alas ALAS) string {
if u.Name == amazonLinux1Name {
return "https://alas.aws.amazon.com/" + alas.Id + ".html"
}
// "ALAS2-2018-1097" becomes "https://alas.aws.amazon.com/AL2/ALAS-2018-1097.html".
re := regexp.MustCompile(`^ALAS2-(.+)$`)
return "https://alas.aws.amazon.com/AL2/ALAS-" + re.FindStringSubmatch(alas.Id)[1] + ".html"
}
func (u *updater) alasToSeverity(alas ALAS) database.Severity {
switch alas.Severity {
case "low":
return database.LowSeverity
case "medium":
return database.MediumSeverity
case "important":
return database.HighSeverity
case "critical":
return database.CriticalSeverity
default:
log.WithField("severity", alas.Severity).Warning("could not determine vulnerability severity")
return database.UnknownSeverity
}
}
func (u *updater) alasToDescription(alas ALAS) string {
re := regexp.MustCompile(`\s+`)
return re.ReplaceAllString(strings.TrimSpace(alas.Description), " ")
}
func (u *updater) alasToFeatureVersions(alas ALAS) (featureVersions []database.AffectedFeature) {
for _, p := range alas.Packages {
var version string
if p.Epoch == "0" {
version = p.Version + "-" + p.Release
} else {
version = p.Epoch + ":" + p.Version + "-" + p.Release
}
err := versionfmt.Valid(rpm.ParserName, version)
if err != nil {
log.WithError(err).WithField("version", version).Warning("could not parse package version. skipping")
continue
}
var featureVersion database.AffectedFeature
featureVersion.Namespace.Name = u.Namespace
featureVersion.Namespace.VersionFormat = rpm.ParserName
featureVersion.FeatureName = p.Name
featureVersion.AffectedVersion = version
if version != versionfmt.MaxVersion {
featureVersion.FixedInVersion = version
}
featureVersion.AffectedType = database.AffectBinaryPackage
featureVersions = append(featureVersions, featureVersion)
}
return
}
func compareTimestamp(date0 string, date1 string) int {
// format: YYYY-MM-DD hh:mm
if date0 < date1 {
return -1
} else if date0 > date1 {
return 1
} else {
return 0
}
}