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

     1  package rhel
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"net/http"
     7  	"net/url"
     8  	"regexp"
     9  	"strconv"
    10  	"strings"
    11  
    12  	"github.com/quay/zlog"
    13  
    14  	"github.com/quay/claircore/libvuln/driver"
    15  	"github.com/quay/claircore/rhel/internal/pulp"
    16  )
    17  
    18  // DefaultManifest is the url for the Red Hat OVAL pulp repository.
    19  //
    20  //doc:url updater
    21  const DefaultManifest = `https://access.redhat.com/security/data/oval/v2/PULP_MANIFEST`
    22  
    23  // NewFactory creates a Factory making updaters based on the contents of the
    24  // provided pulp manifest.
    25  func NewFactory(_ context.Context, manifest string) (*Factory, error) {
    26  	var err error
    27  	var f Factory
    28  	f.url, err = url.Parse(manifest)
    29  	if err != nil {
    30  		return nil, err
    31  	}
    32  
    33  	return &f, nil
    34  }
    35  
    36  // Factory contains the configuration for fetching and parsing a Pulp manifest.
    37  type Factory struct {
    38  	url             *url.URL
    39  	client          *http.Client
    40  	manifestEtag    string
    41  	ignoreUnpatched bool
    42  }
    43  
    44  // FactoryConfig is the configuration accepted by the rhel updaters.
    45  //
    46  // By convention, this should be in a map called "rhel".
    47  type FactoryConfig struct {
    48  	URL string `json:"url" yaml:"url"`
    49  	// IgnoreUnpatched dictates whether to ingest unpatched advisory data
    50  	// from the RHEL security feeds.
    51  	IgnoreUnpatched bool `json:"ignore_unpatched" yaml:"ignore_unpatched"`
    52  }
    53  
    54  var _ driver.Configurable = (*Factory)(nil)
    55  
    56  // Configure implements [driver.Configurable].
    57  func (f *Factory) Configure(ctx context.Context, cfg driver.ConfigUnmarshaler, c *http.Client) error {
    58  	ctx = zlog.ContextWithValues(ctx, "component", "rhel/Factory.Configure")
    59  	var fc FactoryConfig
    60  
    61  	if err := cfg(&fc); err != nil {
    62  		return err
    63  	}
    64  	zlog.Debug(ctx).Msg("loaded incoming config")
    65  
    66  	if fc.URL != "" {
    67  		u, err := url.Parse(fc.URL)
    68  		if err != nil {
    69  			return err
    70  		}
    71  		zlog.Info(ctx).
    72  			Stringer("url", u).
    73  			Msg("configured manifest URL")
    74  		f.url = u
    75  	}
    76  
    77  	if c != nil {
    78  		zlog.Info(ctx).
    79  			Msg("configured HTTP client")
    80  		f.client = c
    81  	}
    82  	f.ignoreUnpatched = fc.IgnoreUnpatched
    83  	return nil
    84  }
    85  
    86  // UpdaterSet implements [driver.UpdaterSetFactory].
    87  //
    88  // The returned Updaters determine the [claircore.Distribution] it's associated
    89  // with based on the path in the Pulp manifest.
    90  func (f *Factory) UpdaterSet(ctx context.Context) (driver.UpdaterSet, error) {
    91  	s := driver.NewUpdaterSet()
    92  
    93  	req, err := http.NewRequestWithContext(ctx, http.MethodGet, f.url.String(), nil)
    94  	if err != nil {
    95  		return s, err
    96  	}
    97  	if f.manifestEtag != "" {
    98  		req.Header.Set("if-none-match", f.manifestEtag)
    99  	}
   100  
   101  	res, err := f.client.Do(req)
   102  	if res != nil {
   103  		defer res.Body.Close()
   104  	}
   105  	if err != nil {
   106  		return s, err
   107  	}
   108  
   109  	switch res.StatusCode {
   110  	case http.StatusOK:
   111  		if t := f.manifestEtag; t == "" || t != res.Header.Get("etag") {
   112  			break
   113  		}
   114  		fallthrough
   115  	case http.StatusNotModified:
   116  		// return stub updater to allow us to record that all rhel updaters are up to date
   117  		stubUpdater := Updater{name: "rhel-all"}
   118  		s.Add(&stubUpdater)
   119  		return s, nil
   120  	default:
   121  		return s, fmt.Errorf("unexpected response: %v", res.Status)
   122  	}
   123  
   124  	m := pulp.Manifest{}
   125  	if err := m.Load(res.Body); err != nil {
   126  		return s, err
   127  	}
   128  
   129  	for _, e := range m {
   130  		name := strings.TrimSuffix(strings.Replace(e.Path, "/", "-", -1), ".oval.xml.bz2")
   131  		// We need to disregard this OVAL stream because some advisories therein have
   132  		// been released with the CPEs identical to those used in classic RHEL stream.
   133  		// This in turn causes false CVEs to appear in scanned images. Red Hat Product
   134  		// Security is working on fixing this situation and the plan is to remove this
   135  		// exception in the future.
   136  		if name == "RHEL7-rhel-7-alt" {
   137  			continue
   138  		}
   139  		uri, err := f.url.Parse(e.Path)
   140  		if err != nil {
   141  			return s, err
   142  		}
   143  		m := guessFromPath.FindStringSubmatch(uri.Path)
   144  		if m == nil {
   145  			continue
   146  		}
   147  		r, err := strconv.Atoi(m[1])
   148  		if err != nil {
   149  			zlog.Info(ctx).
   150  				Err(err).
   151  				Str("path", uri.Path).
   152  				Msg("unable to parse pattern into int")
   153  			continue
   154  		}
   155  		up, err := NewUpdater(name, r, uri.String(), f.ignoreUnpatched)
   156  		if err != nil {
   157  			return s, err
   158  		}
   159  		_ = s.Add(up)
   160  	}
   161  	f.manifestEtag = res.Header.Get("etag")
   162  
   163  	return s, nil
   164  }
   165  
   166  var guessFromPath = regexp.MustCompile(`RHEL([0-9]+)`)