github.com/quay/claircore@v1.5.28/dpkg/distroless_scanner.go (about)

     1  package dpkg
     2  
     3  import (
     4  	"bufio"
     5  	"context"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"io/fs"
    10  	"net/textproto"
    11  	"path/filepath"
    12  	"runtime/trace"
    13  
    14  	"github.com/quay/zlog"
    15  
    16  	"github.com/quay/claircore"
    17  	"github.com/quay/claircore/indexer"
    18  )
    19  
    20  const (
    21  	distrolessName    = "dpkg-distroless"
    22  	distrolessKind    = "package"
    23  	distrolessVersion = "1"
    24  )
    25  
    26  var (
    27  	_ indexer.VersionedScanner = (*Scanner)(nil)
    28  	_ indexer.PackageScanner   = (*Scanner)(nil)
    29  )
    30  
    31  // DistrolessScanner implements the scanner.PackageScanner interface.
    32  //
    33  // This looks for directories that look like dpkg databases and examines the
    34  // files it finds there.
    35  //
    36  // The zero value is ready to use.
    37  type DistrolessScanner struct{}
    38  
    39  // Name implements scanner.VersionedScanner.
    40  func (ps *DistrolessScanner) Name() string { return distrolessName }
    41  
    42  // Version implements scanner.VersionedScanner.
    43  func (ps *DistrolessScanner) Version() string { return distrolessVersion }
    44  
    45  // Kind implements scanner.VersionedScanner.
    46  func (ps *DistrolessScanner) Kind() string { return distrolessKind }
    47  
    48  // Scan attempts to find a dpkg database files in the layer and read all
    49  // of the installed packages it can find. These files are found in the
    50  // dpkg/status.d directory.
    51  //
    52  // It's expected to return (nil, nil) if there's no dpkg databases in the layer.
    53  //
    54  // It does not respect any dpkg configuration files.
    55  func (ps *DistrolessScanner) Scan(ctx context.Context, layer *claircore.Layer) ([]*claircore.Package, error) {
    56  	defer trace.StartRegion(ctx, "Scanner.Scan").End()
    57  	trace.Log(ctx, "layer", layer.Hash.String())
    58  	ctx = zlog.ContextWithValues(ctx,
    59  		"component", "dpkg/DistrolessScanner.Scan",
    60  		"version", ps.Version(),
    61  		"layer", layer.Hash.String())
    62  	zlog.Debug(ctx).Msg("start")
    63  	defer zlog.Debug(ctx).Msg("done")
    64  
    65  	sys, err := layer.FS()
    66  	if err != nil {
    67  		return nil, fmt.Errorf("dpkg-distroless: opening layer failed: %w", err)
    68  	}
    69  
    70  	var pkgs []*claircore.Package
    71  	walk := func(p string, d fs.DirEntry, err error) error {
    72  		if err != nil {
    73  			return err
    74  		}
    75  		if d.Name() == "status.d" && d.IsDir() {
    76  			zlog.Debug(ctx).Str("path", p).Msg("found potential distroless dpkg db directory")
    77  			dbFiles, err := fs.ReadDir(sys, p)
    78  			if err != nil {
    79  				return fmt.Errorf("error reading DB directory: %w", err)
    80  			}
    81  			for _, f := range dbFiles {
    82  				pkgCt := 0
    83  				fn := filepath.Join(p, f.Name())
    84  				ctx = zlog.ContextWithValues(ctx, "database-file", fn)
    85  				zlog.Debug(ctx).Msg("examining package database")
    86  				db, err := sys.Open(fn)
    87  				if err != nil {
    88  					return fmt.Errorf("reading database files from layer failed: %w", err)
    89  				}
    90  
    91  				// The database is actually an RFC822-like message with "\n\n"
    92  				// separators, so don't be alarmed by the usage of the "net/textproto"
    93  				// package here.
    94  				tp := textproto.NewReader(bufio.NewReader(db))
    95  			Restart:
    96  				hdr, err := tp.ReadMIMEHeader()
    97  				for ; (err == nil || errors.Is(err, io.EOF)) && len(hdr) > 0; hdr, err = tp.ReadMIMEHeader() {
    98  					// NB The "Status" header is not considered here. It seems
    99  					// to not be populated in the "distroless" scheme.
   100  					name := hdr.Get("Package")
   101  					v := hdr.Get("Version")
   102  					p := &claircore.Package{
   103  						Name:      name,
   104  						Version:   v,
   105  						Kind:      claircore.BINARY,
   106  						Arch:      hdr.Get("Architecture"),
   107  						PackageDB: fn,
   108  					}
   109  					if src := hdr.Get("Source"); src != "" {
   110  						p.Source = &claircore.Package{
   111  							Name: src,
   112  							Kind: claircore.SOURCE,
   113  							// Right now, this is an assumption that discovered source
   114  							// packages relate to their binary versions. We see this in
   115  							// Debian.
   116  							Version:   v,
   117  							PackageDB: fn,
   118  						}
   119  					}
   120  					pkgCt++
   121  					pkgs = append(pkgs, p)
   122  				}
   123  				switch {
   124  				case errors.Is(err, io.EOF):
   125  				default:
   126  					if _, ok := err.(textproto.ProtocolError); ok {
   127  						zlog.Warn(ctx).Err(err).Msg("unable to read DB entry")
   128  						goto Restart
   129  					}
   130  					zlog.Warn(ctx).Err(err).Msg("error reading DB file")
   131  				}
   132  				zlog.Debug(ctx).
   133  					Int("count", pkgCt).
   134  					Msg("found packages")
   135  			}
   136  		}
   137  		return nil
   138  	}
   139  
   140  	if err := fs.WalkDir(sys, ".", walk); err != nil {
   141  		return nil, err
   142  	}
   143  	return pkgs, nil
   144  }