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

     1  package ubuntu
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"net/http"
     8  	"net/url"
     9  	"path"
    10  	"strings"
    11  	"sync"
    12  
    13  	"github.com/quay/zlog"
    14  	"golang.org/x/sync/errgroup"
    15  
    16  	"github.com/quay/claircore"
    17  	"github.com/quay/claircore/libvuln/driver"
    18  )
    19  
    20  var (
    21  	_ driver.Configurable      = (*Factory)(nil)
    22  	_ driver.UpdaterSetFactory = (*Factory)(nil)
    23  )
    24  
    25  //doc:url updater
    26  const (
    27  	defaultAPI = `https://api.launchpad.net/1.0/`
    28  	ovalURL    = `https://security-metadata.canonical.com/oval/com.ubuntu.%s.cve.oval.xml`
    29  )
    30  const defaultName = `ubuntu`
    31  
    32  // NewFactory constructs a Factory.
    33  //
    34  // The returned Factory must have [Configure] called before [UpdaterSet].
    35  func NewFactory(ctx context.Context) (*Factory, error) {
    36  	return &Factory{}, nil
    37  }
    38  
    39  // Factory implements [driver.UpdaterSetFactory].
    40  //
    41  // [Configure] must be called before [UpdaterSet].
    42  type Factory struct {
    43  	c     *http.Client
    44  	api   string
    45  	force [][2]string
    46  }
    47  
    48  // FactoryConfig is the configuration for Factories.
    49  type FactoryConfig struct {
    50  	// URL should be the address of a [Launchpad API] server.
    51  	//
    52  	// [Launchpad API]: https://launchpad.net/+apidoc/1.0.html
    53  	URL string `json:"url" yaml:"url"`
    54  	// Name is the distribution name, as used in talking to the Launchpad API.
    55  	Name string `json:"name" yaml:"name"`
    56  	// Force is a list of name, version pairs to put in the resulting UpdaterSet regardless
    57  	// of their existence or "active" status in the API response. The resulting Updaters
    58  	// will have guesses at reasonable settings, but the individual Updater's configuration
    59  	// should be used to ensure correct parameters.
    60  	//
    61  	// For example, the name, version pair for Ubuntu 20.04 would be "focal", "20.04".
    62  	Force []struct {
    63  		Name    string `json:"name" yaml:"name"`
    64  		Version string `json:"version" yaml:"version"`
    65  	}
    66  }
    67  
    68  // Configure implements [driver.Configurable].
    69  func (f *Factory) Configure(ctx context.Context, cf driver.ConfigUnmarshaler, c *http.Client) error {
    70  	ctx = zlog.ContextWithValues(ctx,
    71  		"component", "ubuntu/Factory.Configure")
    72  	var cfg FactoryConfig
    73  	if err := cf(&cfg); err != nil {
    74  		return err
    75  	}
    76  	f.c = c
    77  
    78  	u, err := url.Parse(defaultAPI)
    79  	if err != nil {
    80  		panic("programmer error: " + err.Error())
    81  	}
    82  	if cfg.URL != "" {
    83  		u, err = url.Parse(cfg.URL)
    84  		if err != nil {
    85  			return fmt.Errorf("ubuntu: unable to parse provided URL: %w", err)
    86  		}
    87  		zlog.Info(ctx).
    88  			Msg("configured URL")
    89  	}
    90  	n := defaultName
    91  	if cfg.Name != "" {
    92  		n = cfg.Name
    93  	}
    94  	u, err = u.Parse(path.Join(n, "series"))
    95  	if err != nil {
    96  		return fmt.Errorf("ubuntu: unable to parse constructed URL: %w", err)
    97  	}
    98  	f.api = u.String()
    99  
   100  	for _, p := range cfg.Force {
   101  		f.force = append(f.force, [2]string{p.Name, p.Version})
   102  	}
   103  
   104  	return nil
   105  }
   106  
   107  // UpdaterSet implements [driver.UpdaterSetFactory]
   108  func (f *Factory) UpdaterSet(ctx context.Context) (driver.UpdaterSet, error) {
   109  	ctx = zlog.ContextWithValues(ctx,
   110  		"component", "ubuntu/Factory.UpdaterSet")
   111  
   112  	set := driver.NewUpdaterSet()
   113  	req, err := http.NewRequestWithContext(ctx, http.MethodGet, f.api, nil)
   114  	if err != nil {
   115  		return set, fmt.Errorf("ubuntu: unable to construct request: %w", err)
   116  	}
   117  	// There's no way to do conditional requests to this endpoint, as per [the docs].
   118  	// It should change very slowly, but it seems like there's no alternative to asking for
   119  	// a few KB of JSON every so often.
   120  	//
   121  	// [the docs]: https://help.launchpad.net/API/Hacking
   122  	req.Header.Set(`TE`, `gzip`)
   123  	req.Header.Set(`Accept`, `application/json`)
   124  	res, err := f.c.Do(req)
   125  	if err != nil {
   126  		return set, fmt.Errorf("ubuntu: error requesting series collection: %w", err)
   127  	}
   128  	defer res.Body.Close()
   129  	switch res.StatusCode {
   130  	case http.StatusOK:
   131  	default:
   132  		return set, fmt.Errorf("ubuntu: unexpected status requesting %q: %q", f.api, res.Status)
   133  	}
   134  	var series seriesResponse
   135  	if err := json.NewDecoder(res.Body).Decode(&series); err != nil {
   136  		return set, fmt.Errorf("ubuntu: error requesting series collection: %w", err)
   137  	}
   138  
   139  	eg, ctx := errgroup.WithContext(ctx)
   140  	ch := make(chan *distroSeries)
   141  	us := make(chan *updater)
   142  	eg.Go(func() error {
   143  		// Send active distribution series down the channel.
   144  		defer close(ch)
   145  		for i := range series.Entries {
   146  			e := &series.Entries[i]
   147  			mkDist(e.Version, e.Name)
   148  			if !e.Active {
   149  				zlog.Debug(ctx).Str("release", e.Name).Msg("release not active")
   150  				continue
   151  			}
   152  			select {
   153  			case ch <- e:
   154  			case <-ctx.Done():
   155  				return ctx.Err()
   156  			}
   157  		}
   158  		return nil
   159  	})
   160  	eg.Go(func() error {
   161  		// Double-check the distribution.
   162  		defer close(us)
   163  		for e := range ch {
   164  			url := fmt.Sprintf(ovalURL, e.Name)
   165  			req, err := http.NewRequestWithContext(ctx, http.MethodHead, url, nil)
   166  			if err != nil {
   167  				return fmt.Errorf("ubuntu: unable to construct request: %w", err)
   168  			}
   169  			req.Header.Set(`accept`, `application/x-bzip2,application/xml;q=0.9`)
   170  			res, err := f.c.Do(req)
   171  			if err != nil {
   172  				return fmt.Errorf("ubuntu: error requesting inspecting OVAL feed: %w", err)
   173  			}
   174  			defer res.Body.Close()
   175  			switch res.StatusCode {
   176  			case http.StatusOK:
   177  			case http.StatusNotFound:
   178  				zlog.Debug(ctx).
   179  					Str("name", e.Name).
   180  					Str("version", e.Version).
   181  					Msg("OVAL database missing, skipping")
   182  				continue
   183  			default:
   184  				return fmt.Errorf("ubuntu: unexpected status requesting %q: %q", url, res.Status)
   185  			}
   186  			next, err := res.Request.URL.Parse(res.Header.Get(`content-location`))
   187  			if err != nil {
   188  				return fmt.Errorf(`ubuntu: unable to parse "Content-Location": %w`, err)
   189  			}
   190  			us <- &updater{
   191  				url:      next.String(),
   192  				useBzip2: strings.EqualFold(`application/x-bzip2`, res.Header.Get(`content-type`)),
   193  				name:     e.Name,
   194  				id:       e.Version,
   195  			}
   196  		}
   197  		return nil
   198  	})
   199  	eg.Go(func() error {
   200  		// Construct the set
   201  		for u := range us {
   202  			if err := set.Add(u); err != nil {
   203  				return err
   204  			}
   205  		}
   206  		return nil
   207  	})
   208  	if err := eg.Wait(); err != nil {
   209  		return set, err
   210  	}
   211  
   212  	if len(f.force) != 0 {
   213  		zlog.Info(ctx).Msg("configuring manually specified updaters")
   214  		ns := make([]string, 0, len(f.force))
   215  		for _, p := range f.force {
   216  			u := &updater{
   217  				url:      fmt.Sprintf(ovalURL+".bz2", p[0]),
   218  				useBzip2: true,
   219  				name:     p[0],
   220  				id:       p[1],
   221  			}
   222  			if err := set.Add(u); err != nil {
   223  				// Already exists, skip.
   224  				zlog.Debug(ctx).Err(err).Msg("skipping updater")
   225  				continue
   226  			}
   227  			ns = append(ns, u.Name())
   228  		}
   229  		zlog.Info(ctx).Strs("updaters", ns).Msg("added specified updaters")
   230  	}
   231  
   232  	return set, nil
   233  }
   234  
   235  type seriesResponse struct {
   236  	Entries []distroSeries `json:"entries"`
   237  }
   238  
   239  type distroSeries struct {
   240  	Active  bool   `json:"active"`
   241  	Name    string `json:"name"`
   242  	Version string `json:"version"`
   243  }
   244  
   245  var releases sync.Map
   246  
   247  func mkDist(ver, name string) *claircore.Distribution {
   248  	v, _ := releases.LoadOrStore(ver, &claircore.Distribution{
   249  		Name:            "Ubuntu",
   250  		DID:             "ubuntu",
   251  		VersionID:       ver,
   252  		PrettyName:      "Ubuntu " + ver,
   253  		VersionCodeName: name,
   254  		Version:         fmt.Sprintf("%s (%s)", ver, strings.Title(name)),
   255  	})
   256  	return v.(*claircore.Distribution)
   257  }
   258  
   259  func lookupDist(id string) *claircore.Distribution {
   260  	v, ok := releases.Load(id)
   261  	if !ok {
   262  		panic(fmt.Sprintf("programmer error: unknown key %q", id))
   263  	}
   264  	return v.(*claircore.Distribution)
   265  }