github.com/quay/claircore@v1.5.28/rhel/rhcc/updater.go (about)

     1  package rhcc
     2  
     3  import (
     4  	"compress/bzip2"
     5  	"context"
     6  	"encoding/xml"
     7  	"fmt"
     8  	"io"
     9  	"net/http"
    10  	"sort"
    11  
    12  	"github.com/quay/zlog"
    13  	"golang.org/x/text/cases"
    14  	"golang.org/x/text/language"
    15  
    16  	"github.com/quay/claircore"
    17  	"github.com/quay/claircore/internal/xmlutil"
    18  	"github.com/quay/claircore/libvuln/driver"
    19  	"github.com/quay/claircore/pkg/rhctag"
    20  	"github.com/quay/claircore/pkg/tmp"
    21  	"github.com/quay/claircore/rhel/internal/common"
    22  	"github.com/quay/claircore/toolkit/types/cpe"
    23  )
    24  
    25  //doc:url updater
    26  const (
    27  	dbURL  = "https://access.redhat.com/security/data/metrics/cvemap.xml"
    28  	cveURL = "https://access.redhat.com/security/cve/"
    29  )
    30  
    31  var (
    32  	_ driver.Updater      = (*updater)(nil)
    33  	_ driver.Configurable = (*updater)(nil)
    34  )
    35  
    36  // updater fetches and parses cvemap.xml
    37  type updater struct {
    38  	client  *http.Client
    39  	url     string
    40  	bzipped bool
    41  }
    42  
    43  // UpdaterConfig is the configuration for the container catalog's updater.
    44  //
    45  // By convention, this is in a "rhel-container-updater" key.
    46  type UpdaterConfig struct {
    47  	// URL is the URL to a "cvemap.xml" file.
    48  	//
    49  	// The Updater's configuration hook will check for a version with an
    50  	// additional ".bz2" extension.
    51  	URL string `json:"url" yaml:"url"`
    52  }
    53  
    54  const updaterName = "rhel-container-updater"
    55  
    56  func (*updater) Name() string {
    57  	return updaterName
    58  }
    59  
    60  // UpdaterSet returns the rhcc UpdaterSet.
    61  func UpdaterSet(_ context.Context) (driver.UpdaterSet, error) {
    62  	us := driver.NewUpdaterSet()
    63  	if err := us.Add(&updater{}); err != nil {
    64  		return us, err
    65  	}
    66  	return us, nil
    67  }
    68  
    69  // Configure implements [driver.Configurable].
    70  func (u *updater) Configure(ctx context.Context, f driver.ConfigUnmarshaler, c *http.Client) error {
    71  	u.url = dbURL
    72  	u.client = c
    73  	var cfg UpdaterConfig
    74  	if err := f(&cfg); err != nil {
    75  		return err
    76  	}
    77  	if cfg.URL != "" {
    78  		u.url = cfg.URL
    79  	}
    80  
    81  	// This could check the reported content type perhaps, but just relying on
    82  	// the extension is quicker and we have inside information that it's
    83  	// correct.
    84  	tryURL := cfg.URL + ".bz2"
    85  	req, err := http.NewRequestWithContext(ctx, http.MethodHead, tryURL, nil)
    86  	if err != nil {
    87  		// WTF?
    88  		return err
    89  	}
    90  	res, err := u.client.Do(req)
    91  	if err == nil { // NB swapped conditional
    92  		res.Body.Close()
    93  		if res.StatusCode == http.StatusOK {
    94  			u.url = tryURL
    95  			u.bzipped = true
    96  		}
    97  	}
    98  	return nil
    99  }
   100  
   101  // Fetch implements [driver.Updater].
   102  func (u *updater) Fetch(ctx context.Context, hint driver.Fingerprint) (io.ReadCloser, driver.Fingerprint, error) {
   103  	ctx = zlog.ContextWithValues(ctx, "component", "rhel/rhcc/Updater.Fetch")
   104  
   105  	zlog.Info(ctx).Str("database", u.url).Msg("starting fetch")
   106  	req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.url, nil)
   107  	if err != nil {
   108  		return nil, hint, fmt.Errorf("rhcc: unable to construct request: %w", err)
   109  	}
   110  
   111  	if hint != "" {
   112  		zlog.Debug(ctx).
   113  			Str("hint", string(hint)).
   114  			Msg("using hint")
   115  		req.Header.Set("if-none-match", string(hint))
   116  	}
   117  
   118  	res, err := u.client.Do(req)
   119  	if err != nil {
   120  		return nil, hint, fmt.Errorf("rhcc: error making request: %w", err)
   121  	}
   122  	defer res.Body.Close()
   123  
   124  	switch res.StatusCode {
   125  	case http.StatusOK:
   126  		if t := string(hint); t == "" || t != res.Header.Get("etag") {
   127  			break
   128  		}
   129  		fallthrough
   130  	case http.StatusNotModified:
   131  		zlog.Info(ctx).Msg("database unchanged since last fetch")
   132  		return nil, hint, driver.Unchanged
   133  	default:
   134  		return nil, hint, fmt.Errorf("rhcc: http response error: %s %d", res.Status, res.StatusCode)
   135  	}
   136  	zlog.Debug(ctx).Msg("successfully requested database")
   137  
   138  	tf, err := tmp.NewFile("", updaterName+".")
   139  	if err != nil {
   140  		return nil, hint, fmt.Errorf("rhcc: unable to open tempfile: %w", err)
   141  	}
   142  	zlog.Debug(ctx).
   143  		Str("name", tf.Name()).
   144  		Msg("created tempfile")
   145  	var success bool
   146  	defer func() {
   147  		if !success {
   148  			if err := tf.Close(); err != nil {
   149  				zlog.Warn(ctx).Err(err).Msg("unable to close spool")
   150  			}
   151  		}
   152  	}()
   153  
   154  	var r io.Reader = res.Body
   155  	if u.bzipped {
   156  		// No cleanup/pooling.
   157  		r = bzip2.NewReader(res.Body)
   158  	}
   159  	if _, err := io.Copy(tf, r); err != nil {
   160  		return nil, hint, fmt.Errorf("rhcc: unable to copy resp body to tempfile: %w", err)
   161  	}
   162  	if n, err := tf.Seek(0, io.SeekStart); err != nil || n != 0 {
   163  		return nil, hint, fmt.Errorf("rhcc: unable to seek database to start: %w", err)
   164  	}
   165  	zlog.Debug(ctx).Msg("decompressed and buffered database")
   166  
   167  	success = true
   168  	hint = driver.Fingerprint(res.Header.Get("etag"))
   169  	zlog.Debug(ctx).
   170  		Str("hint", string(hint)).
   171  		Msg("using new hint")
   172  
   173  	return tf, hint, nil
   174  }
   175  
   176  // Parse implements [driver.Updater].
   177  func (u *updater) Parse(ctx context.Context, r io.ReadCloser) ([]*claircore.Vulnerability, error) {
   178  	ctx = zlog.ContextWithValues(ctx, "component", "rhel/rhcc/Updater.Parse")
   179  	zlog.Info(ctx).Msg("parse start")
   180  	defer r.Close()
   181  	defer zlog.Info(ctx).Msg("parse done")
   182  
   183  	var cvemap cveMap
   184  	dec := xml.NewDecoder(r)
   185  	dec.CharsetReader = xmlutil.CharsetReader
   186  	if err := dec.Decode(&cvemap); err != nil {
   187  		return nil, fmt.Errorf("rhel: unable to decode cvemap: %w", err)
   188  	}
   189  	zlog.Debug(ctx).Msg("xml decoded")
   190  
   191  	zlog.Debug(ctx).
   192  		Int("count", len(cvemap.RedHatVulnerabilities)).Msg("found raw entries")
   193  
   194  	vs := []*claircore.Vulnerability{}
   195  	for _, vuln := range cvemap.RedHatVulnerabilities {
   196  		description := getDescription(vuln.Details)
   197  		versionsByContainer := make(map[string]map[rhctag.Version]*consolidatedRelease)
   198  		for _, release := range vuln.AffectedReleases {
   199  			match, packageName, version := parseContainerPackage(release.Package)
   200  			if !match {
   201  				continue
   202  			}
   203  			// parse version
   204  			v, err := rhctag.Parse(version)
   205  			if err != nil {
   206  				zlog.Debug(ctx).
   207  					Str("package", packageName).
   208  					Str("version", version).
   209  					Err(err).
   210  					Msgf("tag parse error")
   211  				continue
   212  			}
   213  			// parse severity
   214  			var severity string
   215  			if release.Impact != "" {
   216  				severity = release.Impact
   217  			} else {
   218  				severity = vuln.ThreatSeverity
   219  			}
   220  			titleCase := cases.Title(language.Und)
   221  			severity = titleCase.String(severity)
   222  			// parse cpe
   223  			cpe, err := cpe.Unbind(release.Cpe)
   224  			if err != nil {
   225  				zlog.Warn(ctx).
   226  					Err(err).
   227  					Str("cpe", release.Cpe).
   228  					Msg("could not unbind cpe")
   229  				continue
   230  			}
   231  			// collect minor keys
   232  			minorKey := v.MinorStart()
   233  			// initialize and update the minorKey to consolidated release map
   234  			if versionsByContainer[packageName] == nil {
   235  				versionsByContainer[packageName] = make(map[rhctag.Version]*consolidatedRelease)
   236  			}
   237  			versionsByContainer[packageName][minorKey] = &consolidatedRelease{
   238  				Cpe:          cpe,
   239  				Issued:       release.ReleaseDate.time,
   240  				Severity:     severity,
   241  				AdvisoryLink: release.Advisory.URL,
   242  				AdvisoryName: release.Advisory.Text,
   243  			}
   244  			// initialize and update the fixed in versions slice
   245  			if versionsByContainer[packageName][minorKey].FixedInVersions == nil {
   246  				vs := make(rhctag.Versions, 0)
   247  				versionsByContainer[packageName][minorKey].FixedInVersions = &vs
   248  			}
   249  			newVersions := versionsByContainer[packageName][minorKey].FixedInVersions.Append(v)
   250  			versionsByContainer[packageName][minorKey].FixedInVersions = &newVersions
   251  		}
   252  
   253  		// Build the Vulnerability slice
   254  		for pkg, releasesByMinor := range versionsByContainer {
   255  			p := &claircore.Package{
   256  				Name: pkg,
   257  				Kind: claircore.BINARY,
   258  			}
   259  			// sort minor keys
   260  			minorKeys := make(rhctag.Versions, 0)
   261  			for k := range releasesByMinor {
   262  				minorKeys = append(minorKeys, k)
   263  			}
   264  			sort.Sort(minorKeys)
   265  			// iterate minor key map in order
   266  			for idx, minor := range minorKeys {
   267  				// sort the fixed in versions
   268  				sort.Sort(releasesByMinor[minor].FixedInVersions)
   269  				// The first minor version range should match all previous versions
   270  				start := minor
   271  				if idx == 0 {
   272  					start = rhctag.Version{}
   273  				}
   274  				// For containers such as openshift-logging/elasticsearch6-rhel8 we need to match
   275  				// the first Fixed in Version here.
   276  				// Most of the time this will return the only Fixed In Version for minor version
   277  				firstPatch, _ := releasesByMinor[minor].FixedInVersions.First()
   278  				r := &claircore.Range{
   279  					Lower: start.Version(true),
   280  					Upper: firstPatch.Version(false),
   281  				}
   282  				links := fmt.Sprintf("%s %s%s", releasesByMinor[minor].AdvisoryLink, cveURL, vuln.Name)
   283  				v := &claircore.Vulnerability{
   284  					Updater:            updaterName,
   285  					Name:               releasesByMinor[minor].AdvisoryName,
   286  					Description:        description,
   287  					Issued:             releasesByMinor[minor].Issued,
   288  					Severity:           releasesByMinor[minor].Severity,
   289  					NormalizedSeverity: common.NormalizeSeverity(releasesByMinor[minor].Severity),
   290  					Package:            p,
   291  					Repo:               &goldRepo,
   292  					Links:              links,
   293  					FixedInVersion:     firstPatch.Original,
   294  					Range:              r,
   295  				}
   296  				vs = append(vs, v)
   297  			}
   298  		}
   299  	}
   300  	zlog.Debug(ctx).
   301  		Int("count", len(vs)).
   302  		Msg("found vulnerabilities")
   303  	return vs, nil
   304  }