github.com/quay/claircore@v1.5.28/alpine/distributionscanner.go (about)

     1  package alpine
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"errors"
     7  	"fmt"
     8  	"io/fs"
     9  	"regexp"
    10  	"runtime/trace"
    11  	"strings"
    12  
    13  	"github.com/quay/zlog"
    14  
    15  	"github.com/quay/claircore"
    16  	"github.com/quay/claircore/indexer"
    17  	"github.com/quay/claircore/osrelease"
    18  )
    19  
    20  // Alpine linux has patch releases but their security database
    21  // aggregates security information by major release. We choose
    22  // to normalize detected distributions into major.minor releases and
    23  // parse vulnerabilities into major.minor releases
    24  
    25  const (
    26  	scannerName    = "alpine"
    27  	scannerVersion = "3"
    28  	scannerKind    = "distribution"
    29  
    30  	issuePath = `etc/issue`
    31  
    32  	edgeVersion    = `edge`
    33  	edgePrettyName = `Alpine Linux edge`
    34  )
    35  
    36  var (
    37  	_ indexer.DistributionScanner = (*DistributionScanner)(nil)
    38  	_ indexer.VersionedScanner    = (*DistributionScanner)(nil)
    39  
    40  	issueRegexp     = regexp.MustCompile(`Alpine Linux ([[:digit:]]+\.[[:digit:]]+)`)
    41  	edgeIssueRegexp = regexp.MustCompile(`Alpine Linux [[:digit:]]+\.\w+ \(edge\)`)
    42  )
    43  
    44  // DistributionScanner attempts to discover if a layer
    45  // displays characteristics of a alpine distribution
    46  type DistributionScanner struct{}
    47  
    48  // Name implements scanner.VersionedScanner.
    49  func (*DistributionScanner) Name() string { return scannerName }
    50  
    51  // Version implements scanner.VersionedScanner.
    52  func (*DistributionScanner) Version() string { return scannerVersion }
    53  
    54  // Kind implements scanner.VersionedScanner.
    55  func (*DistributionScanner) Kind() string { return scannerKind }
    56  
    57  // Scan will inspect the layer for an os-release or issue file
    58  // and perform a regex match for keywords indicating the associated alpine release
    59  //
    60  // If neither file is found a (nil, nil) is returned.
    61  // If the files are found but all regexp fail to match an empty slice is returned.
    62  func (s *DistributionScanner) Scan(ctx context.Context, l *claircore.Layer) ([]*claircore.Distribution, error) {
    63  	defer trace.StartRegion(ctx, "Scanner.Scan").End()
    64  	ctx = zlog.ContextWithValues(ctx,
    65  		"component", "alpine/DistributionScanner.Scan",
    66  		"version", s.Version(),
    67  		"layer", l.Hash.String())
    68  	zlog.Debug(ctx).Msg("start")
    69  	defer zlog.Debug(ctx).Msg("done")
    70  	sys, err := l.FS()
    71  	if err != nil {
    72  		return nil, fmt.Errorf("alpine: unable to open layer: %w", err)
    73  	}
    74  	return s.scanFs(ctx, sys)
    75  }
    76  
    77  func (*DistributionScanner) scanFs(ctx context.Context, sys fs.FS) (d []*claircore.Distribution, err error) {
    78  	for _, f := range []distFunc{readOSRelease, readIssue} {
    79  		dist, err := f(ctx, sys)
    80  		if err != nil {
    81  			return nil, err
    82  		}
    83  		if dist != nil {
    84  			return []*claircore.Distribution{dist}, nil
    85  		}
    86  	}
    87  
    88  	// Found nothing.
    89  	return nil, nil
    90  }
    91  
    92  type distFunc func(context.Context, fs.FS) (*claircore.Distribution, error)
    93  
    94  // ReadOSRelease looks for the distribution in an os-release file, if it exists.
    95  func readOSRelease(ctx context.Context, sys fs.FS) (*claircore.Distribution, error) {
    96  	b, err := fs.ReadFile(sys, osrelease.Path)
    97  	switch {
    98  	case errors.Is(err, nil):
    99  		// parse here
   100  		m, err := osrelease.Parse(ctx, bytes.NewReader(b))
   101  		if err != nil {
   102  			return nil, err
   103  		}
   104  		if id := m[`ID`]; id != `alpine` {
   105  			zlog.Debug(ctx).Str("id", id).Msg("seemingly not alpine")
   106  			break
   107  		}
   108  		vid := m[`VERSION_ID`]
   109  		idx := strings.LastIndexByte(vid, '.')
   110  		if idx == -1 {
   111  			zlog.Debug(ctx).Str("val", vid).Msg("martian VERSION_ID")
   112  			break
   113  		}
   114  		v := vid[:idx]
   115  		if m[`PRETTY_NAME`] == edgePrettyName {
   116  			v = edgeVersion
   117  		}
   118  		return &claircore.Distribution{
   119  			Name:    m[`NAME`],
   120  			DID:     m[`ID`],
   121  			Version: v,
   122  			// BUG(hank) The current version omit the VERSION_ID data. Need to
   123  			// investigate why. Probably because it's not in the etc/issue
   124  			// file.
   125  			// VersionID:  vid,
   126  			PrettyName: m[`PRETTY_NAME`],
   127  		}, nil
   128  	case errors.Is(err, fs.ErrNotExist):
   129  		zlog.Debug(ctx).
   130  			Str("path", osrelease.Path).
   131  			Msg("file doesn't exist")
   132  	default:
   133  		return nil, err
   134  	}
   135  
   136  	// Found nothing.
   137  	return nil, nil
   138  }
   139  
   140  // ReadIssue looks for the distribution in an issue file, if it exists.
   141  func readIssue(ctx context.Context, sys fs.FS) (*claircore.Distribution, error) {
   142  	b, err := fs.ReadFile(sys, issuePath)
   143  	switch {
   144  	case errors.Is(err, nil):
   145  		if isEdge := edgeIssueRegexp.Match(b); isEdge {
   146  			return &claircore.Distribution{
   147  				Name:       `Alpine Linux`,
   148  				DID:        `alpine`,
   149  				Version:    edgeVersion,
   150  				PrettyName: edgePrettyName,
   151  			}, nil
   152  		}
   153  
   154  		ms := issueRegexp.FindSubmatch(b)
   155  		if ms == nil {
   156  			zlog.Debug(ctx).Msg("seemingly not alpine")
   157  			break
   158  		}
   159  		v := string(ms[1])
   160  		return &claircore.Distribution{
   161  			Name:       `Alpine Linux`,
   162  			DID:        `alpine`,
   163  			Version:    v,
   164  			PrettyName: fmt.Sprintf(`Alpine Linux v%s`, v),
   165  		}, nil
   166  	case errors.Is(err, fs.ErrNotExist):
   167  		zlog.Debug(ctx).
   168  			Str("path", issuePath).
   169  			Msg("file doesn't exist")
   170  	default:
   171  		return nil, err
   172  	}
   173  
   174  	// Found nothing.
   175  	return nil, nil
   176  }