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

     1  package rhcc
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"io/fs"
     9  	"net/http"
    10  	"os"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/quay/zlog"
    15  
    16  	"github.com/quay/claircore"
    17  	"github.com/quay/claircore/indexer"
    18  	"github.com/quay/claircore/internal/zreader"
    19  	"github.com/quay/claircore/pkg/rhctag"
    20  	"github.com/quay/claircore/rhel/dockerfile"
    21  	"github.com/quay/claircore/rhel/internal/common"
    22  )
    23  
    24  var (
    25  	_ indexer.PackageScanner = (*scanner)(nil)
    26  	_ indexer.RPCScanner     = (*scanner)(nil)
    27  )
    28  
    29  type scanner struct {
    30  	upd    *common.Updater
    31  	client *http.Client
    32  	cfg    ScannerConfig
    33  }
    34  
    35  // ScannerConfig is the configuration for the package scanner.
    36  //
    37  // The interaction between the "URL" and "File" members is the same as described
    38  // in the [github.com/quay/claircore/rhel.RepositoryScannerConfig] documentation.
    39  //
    40  // By convention, it's in a "rhel_containerscanner" key.
    41  type ScannerConfig struct {
    42  	// Name2ReposMappingURL is a URL where a mapping file can be fetched.
    43  	//
    44  	// See also [DefaultName2ReposMappingURL]
    45  	Name2ReposMappingURL string `json:"name2repos_mapping_url" yaml:"name2repos_mapping_url"`
    46  	// Name2ReposMappingFile is a path to a local mapping file.
    47  	Name2ReposMappingFile string `json:"name2repos_mapping_file" yaml:"name2repos_mapping_file"`
    48  	// Timeout is a timeout for all network calls made to update the mapping
    49  	// file.
    50  	//
    51  	// The default is 10 seconds.
    52  	Timeout time.Duration `json:"timeout" yaml:"timeout"`
    53  }
    54  
    55  // DefaultName2ReposMappingURL is the default URL with a mapping file provided by Red
    56  // Hat.
    57  //
    58  //doc:url indexer
    59  const DefaultName2ReposMappingURL = "https://access.redhat.com/security/data/metrics/container-name-repos-map.json"
    60  
    61  // Configure implements [indexer.RPCScanner].
    62  func (s *scanner) Configure(ctx context.Context, f indexer.ConfigDeserializer, c *http.Client) error {
    63  	ctx = zlog.ContextWithValues(ctx, "component", "rhel/rhcc/scanner.Configure")
    64  	s.client = c
    65  	if err := f(&s.cfg); err != nil {
    66  		return err
    67  	}
    68  
    69  	if s.cfg.Timeout == 0 {
    70  		s.cfg.Timeout = 10 * time.Second
    71  	}
    72  	var mf *mappingFile
    73  	switch {
    74  	case s.cfg.Name2ReposMappingURL == "" && s.cfg.Name2ReposMappingFile == "":
    75  		// defaults
    76  		s.cfg.Name2ReposMappingURL = DefaultName2ReposMappingURL
    77  	case s.cfg.Name2ReposMappingURL != "" && s.cfg.Name2ReposMappingFile == "":
    78  		// remote only
    79  	case s.cfg.Name2ReposMappingFile != "":
    80  		// local only
    81  		f, err := os.Open(s.cfg.Name2ReposMappingFile)
    82  		if err != nil {
    83  			return err
    84  		}
    85  		defer f.Close()
    86  		z, err := zreader.Reader(f)
    87  		if err != nil {
    88  			return err
    89  		}
    90  		defer z.Close()
    91  		mf = &mappingFile{}
    92  		if err := json.NewDecoder(z).Decode(mf); err != nil {
    93  			return err
    94  		}
    95  	}
    96  	s.upd = common.NewUpdater(s.cfg.Name2ReposMappingURL, mf)
    97  	tctx, done := context.WithTimeout(ctx, s.cfg.Timeout)
    98  	defer done()
    99  	s.upd.Get(tctx, c)
   100  
   101  	return nil
   102  }
   103  
   104  // Name implements [indexer.VersionedScanner].
   105  func (s *scanner) Name() string { return "rhel_containerscanner" }
   106  
   107  // Version implements [indexer.VersionedScanner].
   108  func (s *scanner) Version() string { return "1" }
   109  
   110  // Kind implements [indexer.VersionedScanner].
   111  func (s *scanner) Kind() string { return "package" }
   112  
   113  // Scan performs a package scan on the given layer and returns all
   114  // the RHEL container identifying metadata
   115  
   116  // Scan implements [indexer.PackageScanner].
   117  func (s *scanner) Scan(ctx context.Context, l *claircore.Layer) ([]*claircore.Package, error) {
   118  	ctx = zlog.ContextWithValues(ctx, "component", "rhel/rhcc/scanner.Scan")
   119  	const (
   120  		compLabel = `com.redhat.component`
   121  		nameLabel = `name`
   122  		archLabel = `architecture`
   123  	)
   124  	if err := ctx.Err(); err != nil {
   125  		return nil, err
   126  	}
   127  	sys, err := l.FS()
   128  	if err != nil {
   129  		return nil, fmt.Errorf("rhcc: unable to open layer: %w", err)
   130  	}
   131  
   132  	// add source package from component label
   133  	labels, p, err := findLabels(ctx, sys)
   134  	switch {
   135  	case errors.Is(err, nil):
   136  	case errors.Is(err, errNotFound):
   137  		return nil, nil
   138  	default:
   139  		return nil, err
   140  	}
   141  
   142  	vr := getVR(p)
   143  	rhctagVersion, err := rhctag.Parse(vr)
   144  	if err != nil {
   145  		// This can happen for containers which don't use semantic versioning,
   146  		// such as UBI.
   147  		return nil, nil
   148  	}
   149  	var buildName, arch, name string
   150  	for _, chk := range []struct {
   151  		Found *string
   152  		Want  string
   153  	}{
   154  		{&buildName, compLabel},
   155  		{&arch, archLabel},
   156  		{&name, nameLabel},
   157  	} {
   158  		var ok bool
   159  		(*chk.Found), ok = labels[chk.Want]
   160  		if !ok {
   161  			zlog.Info(ctx).Str("label", chk.Want).Msg("expected label not found in dockerfile")
   162  			return nil, nil
   163  		}
   164  	}
   165  
   166  	minorRange := rhctagVersion.MinorStart()
   167  	src := claircore.Package{
   168  		Kind:              claircore.SOURCE,
   169  		Name:              buildName,
   170  		Version:           vr,
   171  		NormalizedVersion: minorRange.Version(true),
   172  		PackageDB:         p,
   173  		Arch:              arch,
   174  		RepositoryHint:    `rhcc`,
   175  	}
   176  	pkgs := []*claircore.Package{&src}
   177  
   178  	tctx, done := context.WithTimeout(ctx, s.cfg.Timeout)
   179  	defer done()
   180  	vi, err := s.upd.Get(tctx, s.client)
   181  	if err != nil && vi == nil {
   182  		return nil, err
   183  	}
   184  	v, ok := vi.(*mappingFile)
   185  	if !ok || v == nil {
   186  		return nil, fmt.Errorf("rhcc: unable to create a mappingFile object")
   187  	}
   188  	repos, ok := v.Data[name]
   189  	if ok {
   190  		zlog.Debug(ctx).Str("name", name).
   191  			Msg("name present in mapping file")
   192  	} else {
   193  		// Didn't find external_repos in mapping, use name label as package
   194  		// name.
   195  		repos = []string{name}
   196  	}
   197  	for _, name := range repos {
   198  		// Add each external repo as a binary package. The same container image
   199  		// can ship to multiple repos eg. `"toolbox-container":
   200  		// ["rhel8/toolbox", "ubi8/toolbox"]`. Therefore, we want a binary
   201  		// package entry for each.
   202  		pkgs = append(pkgs, &claircore.Package{
   203  			Kind:              claircore.BINARY,
   204  			Name:              name,
   205  			Version:           vr,
   206  			NormalizedVersion: minorRange.Version(true),
   207  			Source:            &src,
   208  			PackageDB:         p,
   209  			Arch:              arch,
   210  			RepositoryHint:    `rhcc`,
   211  		})
   212  	}
   213  	return pkgs, nil
   214  }
   215  
   216  // MappingFile is a struct for mapping file between container NAME label and
   217  // container registry repository location.
   218  type mappingFile struct {
   219  	Data map[string][]string `json:"data"`
   220  }
   221  
   222  func findLabels(ctx context.Context, sys fs.FS) (map[string]string, string, error) {
   223  	ms, err := fs.Glob(sys, "root/buildinfo/Dockerfile-*")
   224  	if err != nil { // Can only return ErrBadPattern.
   225  		panic("progammer error: " + err.Error())
   226  	}
   227  	if len(ms) == 0 {
   228  		return nil, "", errNotFound
   229  	}
   230  	zlog.Debug(ctx).
   231  		Strs("paths", ms).
   232  		Msg("found possible buildinfo Dockerfile(s)")
   233  	var p string
   234  	for _, m := range ms {
   235  		if strings.Count(m, "-") > 1 {
   236  			p = m
   237  			break
   238  		}
   239  	}
   240  	if p == "" {
   241  		return nil, "", errNotFound
   242  	}
   243  	zlog.Info(ctx).
   244  		Str("path", p).
   245  		Msg("found buildinfo Dockerfile")
   246  	f, err := sys.Open(p)
   247  	if err != nil {
   248  		return nil, "", err
   249  	}
   250  	defer f.Close()
   251  	labels, err := dockerfile.GetLabels(ctx, f)
   252  	if err != nil {
   253  		return nil, "", err
   254  	}
   255  	return labels, p, nil
   256  }
   257  
   258  var errNotFound = errors.New("not found")
   259  
   260  // GetVR extracts the version-release string from the provided string ending in
   261  // an NVR.
   262  //
   263  // Panics if passed malformed input.
   264  func getVR(nvr string) string {
   265  	if strings.Count(nvr, "-") < 2 {
   266  		panic("programmer error: not an nvr string: " + nvr)
   267  	}
   268  	i := strings.LastIndexByte(nvr, '-')
   269  	i = strings.LastIndexByte(nvr[:i], '-')
   270  	return nvr[i+1:]
   271  }
   272  
   273  type reposcanner struct{}
   274  
   275  var _ indexer.RepositoryScanner = (*reposcanner)(nil)
   276  
   277  // Name implements [indexer.VersionedScanner].
   278  func (s *reposcanner) Name() string { return "rhel_containerscanner" }
   279  
   280  // Version implements [indexer.VersionedScanner].
   281  func (s *reposcanner) Version() string { return "1" }
   282  
   283  // Kind implements [indexer.VersionedScanner].
   284  func (s *reposcanner) Kind() string { return "repository" }
   285  
   286  // Scan implements [indexer.RepositoryScanner].
   287  func (s *reposcanner) Scan(ctx context.Context, l *claircore.Layer) ([]*claircore.Repository, error) {
   288  	ctx = zlog.ContextWithValues(ctx, "component", "rhel/rhcc/reposcanner.Scan")
   289  	sys, err := l.FS()
   290  	if err != nil {
   291  		return nil, fmt.Errorf("rhcc: unable to open layer: %w", err)
   292  	}
   293  	ms, err := fs.Glob(sys, "root/buildinfo/Dockerfile-*")
   294  	if err != nil { // Can only return ErrBadPattern.
   295  		panic("progammer error")
   296  	}
   297  	if len(ms) == 0 {
   298  		return nil, nil
   299  	}
   300  	zlog.Debug(ctx).
   301  		Msg("found buildinfo Dockerfile")
   302  	return []*claircore.Repository{&goldRepo}, nil
   303  }