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

     1  package ubuntu
     2  
     3  import (
     4  	"compress/bzip2"
     5  	"context"
     6  	"encoding/xml"
     7  	"fmt"
     8  	"io"
     9  	"net/http"
    10  	"net/url"
    11  
    12  	"github.com/quay/goval-parser/oval"
    13  	"github.com/quay/zlog"
    14  
    15  	"github.com/quay/claircore"
    16  	"github.com/quay/claircore/internal/xmlutil"
    17  	"github.com/quay/claircore/libvuln/driver"
    18  	"github.com/quay/claircore/pkg/ovalutil"
    19  	"github.com/quay/claircore/pkg/tmp"
    20  )
    21  
    22  var (
    23  	_ driver.Updater      = (*updater)(nil)
    24  	_ driver.Configurable = (*updater)(nil)
    25  )
    26  
    27  // Updater fetches and parses Ubuntu-flavored OVAL.
    28  //
    29  // Updaters are constructed exclusively by the [Factory].
    30  type updater struct {
    31  	// the url to fetch the OVAL db from
    32  	url      string
    33  	useBzip2 bool
    34  	name     string
    35  	id       string
    36  	c        *http.Client
    37  }
    38  
    39  // Name implements [driver.Updater].
    40  func (u *updater) Name() string {
    41  	return fmt.Sprintf("ubuntu/updater/%s", u.name)
    42  }
    43  
    44  // Configure implements [driver.Configurable].
    45  func (u *updater) Configure(ctx context.Context, f driver.ConfigUnmarshaler, c *http.Client) error {
    46  	ctx = zlog.ContextWithValues(ctx,
    47  		"component", "ubuntu/Updater.Configure",
    48  		"updater", u.Name())
    49  	u.c = c
    50  
    51  	var cfg UpdaterConfig
    52  	if err := f(&cfg); err != nil {
    53  		return err
    54  	}
    55  
    56  	if cfg.URL != "" {
    57  		if _, err := url.Parse(cfg.URL); err != nil {
    58  			return err
    59  		}
    60  		u.url = cfg.URL
    61  		zlog.Info(ctx).
    62  			Msg("configured database URL")
    63  	}
    64  	if cfg.UseBzip2 != nil {
    65  		u.useBzip2 = *cfg.UseBzip2
    66  	}
    67  
    68  	return nil
    69  }
    70  
    71  // UpdaterConfig is the configuration for the updater.
    72  //
    73  // By convention, this is in a map called "ubuntu/updater/${RELEASE}", e.g.
    74  // "ubuntu/updater/focal".
    75  type UpdaterConfig struct {
    76  	URL      string `json:"url" yaml:"url"`
    77  	UseBzip2 *bool  `json:"use_bzip2" yaml:"use_bzip2"`
    78  }
    79  
    80  // Fetch implements [driver.Updater].
    81  func (u *updater) Fetch(ctx context.Context, fingerprint driver.Fingerprint) (io.ReadCloser, driver.Fingerprint, error) {
    82  	ctx = zlog.ContextWithValues(ctx,
    83  		"component", "ubuntu/Updater.Fetch",
    84  		"database", u.url)
    85  
    86  	req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.url, nil)
    87  	if err != nil {
    88  		return nil, "", fmt.Errorf("failed to create request")
    89  	}
    90  	if fingerprint != "" {
    91  		req.Header.Set("if-none-match", string(fingerprint))
    92  	}
    93  
    94  	// fetch OVAL xml database
    95  	resp, err := u.c.Do(req)
    96  	if err != nil {
    97  		return nil, "", fmt.Errorf("ubuntu: failed to retrieve OVAL database: %w", err)
    98  	}
    99  	defer resp.Body.Close()
   100  
   101  	switch resp.StatusCode {
   102  	case http.StatusOK:
   103  		if fp := string(fingerprint); fp == "" || fp != resp.Header.Get("etag") {
   104  			zlog.Info(ctx).Msg("fetching latest oval database")
   105  			break
   106  		}
   107  		fallthrough
   108  	case http.StatusNotModified:
   109  		return nil, fingerprint, driver.Unchanged
   110  	default:
   111  		return nil, "", fmt.Errorf("ubuntu: unexpected response: %s", resp.Status)
   112  	}
   113  
   114  	fp := resp.Header.Get("etag")
   115  	f, err := tmp.NewFile("", "ubuntu.")
   116  	if err != nil {
   117  		return nil, "", err
   118  	}
   119  	var success bool
   120  	defer func() {
   121  		if !success {
   122  			if err := f.Close(); err != nil {
   123  				zlog.Warn(ctx).Err(err).Msg("unable to close spool")
   124  			}
   125  		}
   126  	}()
   127  	var r io.Reader = resp.Body
   128  	if u.useBzip2 {
   129  		r = bzip2.NewReader(r)
   130  	}
   131  	if _, err := io.Copy(f, r); err != nil {
   132  		return nil, "", fmt.Errorf("ubuntu: failed to read http body: %w", err)
   133  	}
   134  	if _, err := f.Seek(0, io.SeekStart); err != nil {
   135  		return nil, "", fmt.Errorf("ubuntu: failed to seek body: %w", err)
   136  	}
   137  
   138  	success = true
   139  	zlog.Info(ctx).Msg("fetched latest oval database successfully")
   140  	return f, driver.Fingerprint(fp), err
   141  }
   142  
   143  // Parse implements [driver.Updater].
   144  func (u *updater) Parse(ctx context.Context, r io.ReadCloser) ([]*claircore.Vulnerability, error) {
   145  	ctx = zlog.ContextWithValues(ctx,
   146  		"component", "ubuntu/Updater.Parse")
   147  	zlog.Info(ctx).Msg("starting parse")
   148  	defer r.Close()
   149  	root := oval.Root{}
   150  	dec := xml.NewDecoder(r)
   151  	dec.CharsetReader = xmlutil.CharsetReader
   152  	if err := dec.Decode(&root); err != nil {
   153  		return nil, fmt.Errorf("ubuntu: unable to decode OVAL document: %w", err)
   154  	}
   155  	zlog.Debug(ctx).Msg("xml decoded")
   156  
   157  	nameLookupFunc := func(def oval.Definition, name *oval.DpkgName) []string {
   158  		// if the dpkginfo_object>name field has a var_ref it indicates
   159  		// a variable lookup for all packages affected by this vuln is necessary.
   160  		//
   161  		// if the name.Ref field is empty it indicates a single package is affected
   162  		// by the vuln and that package's name is in name.Body.
   163  		var ns []string
   164  		if len(name.Ref) == 0 {
   165  			ns = append(ns, name.Body)
   166  			return ns
   167  		}
   168  		_, i, err := root.Variables.Lookup(name.Ref)
   169  		if err != nil {
   170  			zlog.Error(ctx).Err(err).Msg("could not lookup variable id")
   171  			return ns
   172  		}
   173  		consts := root.Variables.ConstantVariables[i]
   174  		for _, v := range consts.Values {
   175  			ns = append(ns, v.Body)
   176  		}
   177  		return ns
   178  	}
   179  
   180  	protoVulns := func(def oval.Definition) ([]*claircore.Vulnerability, error) {
   181  		vs := []*claircore.Vulnerability{}
   182  		v := &claircore.Vulnerability{
   183  			Updater:            u.Name(),
   184  			Name:               def.Title,
   185  			Description:        def.Description,
   186  			Issued:             def.Advisory.Issued.Date,
   187  			Links:              ovalutil.Links(def),
   188  			NormalizedSeverity: normalizeSeverity(def.Advisory.Severity),
   189  			Dist:               lookupDist(u.id),
   190  		}
   191  		vs = append(vs, v)
   192  		return vs, nil
   193  	}
   194  	vulns, err := ovalutil.DpkgDefsToVulns(ctx, &root, protoVulns, nameLookupFunc)
   195  	if err != nil {
   196  		return nil, err
   197  	}
   198  	return vulns, nil
   199  }
   200  
   201  func normalizeSeverity(severity string) claircore.Severity {
   202  	switch severity {
   203  	case "Negligible":
   204  		return claircore.Negligible
   205  	case "Low":
   206  		return claircore.Low
   207  	case "Medium":
   208  		return claircore.Medium
   209  	case "High":
   210  		return claircore.High
   211  	case "Critical":
   212  		return claircore.Critical
   213  	default:
   214  	}
   215  	return claircore.Unknown
   216  }