github.com/quay/claircore@v1.5.28/nodejs/packagescanner.go (about)

     1  // Package nodejs contains components for interrogating nodejs packages in
     2  // container layers.
     3  package nodejs
     4  
     5  import (
     6  	"bufio"
     7  	"context"
     8  	"encoding/json"
     9  	"fmt"
    10  	"io/fs"
    11  	"path/filepath"
    12  	"runtime/trace"
    13  	"strings"
    14  
    15  	"github.com/quay/zlog"
    16  
    17  	"github.com/Masterminds/semver"
    18  	"github.com/quay/claircore"
    19  	"github.com/quay/claircore/indexer"
    20  )
    21  
    22  const repository = "npm"
    23  
    24  var (
    25  	_ indexer.VersionedScanner   = (*Scanner)(nil)
    26  	_ indexer.PackageScanner     = (*Scanner)(nil)
    27  	_ indexer.DefaultRepoScanner = (*Scanner)(nil)
    28  
    29  	Repository = claircore.Repository{
    30  		Name: repository,
    31  		URI:  "https://www.npmjs.com/",
    32  	}
    33  )
    34  
    35  // Scanner implements the scanner.PackageScanner interface.
    36  //
    37  // It looks for files that seem like package.json and looks at the
    38  // metadata recorded there.
    39  //
    40  // The zero value is ready to use.
    41  type Scanner struct{}
    42  
    43  // Name implements scanner.VersionedScanner.
    44  func (*Scanner) Name() string { return "nodejs" }
    45  
    46  // Version implements scanner.VersionedScanner.
    47  func (*Scanner) Version() string { return "2" }
    48  
    49  // Kind implements scanner.VersionedScanner.
    50  func (*Scanner) Kind() string { return "package" }
    51  
    52  // packageJSON represents the fields of a package.json file
    53  // useful for package scanning.
    54  //
    55  // See https://docs.npmjs.com/files/package.json/ for more details
    56  // about the format of package.json files.
    57  type packageJSON struct {
    58  	Name    string `json:"name"`
    59  	Version string `json:"version"`
    60  }
    61  
    62  // Scan attempts to find package.json files and record the package
    63  // information there.
    64  //
    65  // A return of (nil, nil) is expected if there's nothing found.
    66  func (s *Scanner) Scan(ctx context.Context, layer *claircore.Layer) ([]*claircore.Package, error) {
    67  	defer trace.StartRegion(ctx, "Scanner.Scan").End()
    68  	trace.Log(ctx, "layer", layer.Hash.String())
    69  	ctx = zlog.ContextWithValues(ctx,
    70  		"component", "nodejs/Scanner.Scan",
    71  		"version", s.Version(),
    72  		"layer", layer.Hash.String())
    73  	zlog.Debug(ctx).Msg("start")
    74  	defer zlog.Debug(ctx).Msg("done")
    75  	if err := ctx.Err(); err != nil {
    76  		return nil, err
    77  	}
    78  
    79  	sys, err := layer.FS()
    80  	if err != nil {
    81  		return nil, fmt.Errorf("nodejs: unable to open layer: %w", err)
    82  	}
    83  
    84  	pkgs, err := packages(ctx, sys)
    85  	if err != nil {
    86  		return nil, fmt.Errorf("nodejs: failed to find packages: %w", err)
    87  	}
    88  	if len(pkgs) == 0 {
    89  		return nil, nil
    90  	}
    91  
    92  	ret := make([]*claircore.Package, 0, len(pkgs))
    93  	var invalidPkgs []string
    94  	for _, p := range pkgs {
    95  		f, err := sys.Open(p)
    96  		if err != nil {
    97  			return nil, fmt.Errorf("nodejs: unable to open file %q: %w", p, err)
    98  		}
    99  
   100  		var pkgJSON packageJSON
   101  		err = json.NewDecoder(bufio.NewReader(f)).Decode(&pkgJSON)
   102  		if err != nil {
   103  			invalidPkgs = append(invalidPkgs, p)
   104  			continue
   105  		}
   106  
   107  		pkg := &claircore.Package{
   108  			Name:           pkgJSON.Name,
   109  			Version:        pkgJSON.Version,
   110  			Kind:           claircore.BINARY,
   111  			PackageDB:      "nodejs:" + p,
   112  			Filepath:       p,
   113  			RepositoryHint: repository,
   114  		}
   115  		if sv, err := semver.NewVersion(pkgJSON.Version); err == nil {
   116  			pkg.NormalizedVersion = claircore.FromSemver(sv)
   117  		} else {
   118  			zlog.Info(ctx).
   119  				Str("package", pkg.Name).
   120  				Str("version", pkg.Version).
   121  				Msg("invalid semantic version")
   122  		}
   123  
   124  		ret = append(ret, pkg)
   125  	}
   126  
   127  	if len(invalidPkgs) > 0 {
   128  		zlog.Debug(ctx).Strs("paths", invalidPkgs).Msg("unable to decode package.json, skipping")
   129  	}
   130  
   131  	return ret, nil
   132  }
   133  
   134  func packages(ctx context.Context, sys fs.FS) (out []string, err error) {
   135  	return out, fs.WalkDir(sys, ".", func(p string, d fs.DirEntry, err error) error {
   136  		ev := zlog.Debug(ctx).
   137  			Str("file", p)
   138  		var success bool
   139  		defer func() {
   140  			if !success {
   141  				ev.Discard().Send()
   142  			}
   143  		}()
   144  		switch {
   145  		case err != nil:
   146  			return err
   147  		case !d.Type().IsRegular():
   148  			// Should we chase symlinks with the correct name?
   149  			return nil
   150  		case strings.HasPrefix(filepath.Base(p), ".wh."):
   151  			return nil
   152  		case !strings.Contains(p, "node_modules/"):
   153  			// Only bother with package.json files within node_modules/ directories.
   154  			// See https://docs.npmjs.com/cli/v7/configuring-npm/folders#node-modules
   155  			// for more information.
   156  			return nil
   157  		case strings.HasSuffix(p, "/package.json"):
   158  			ev = ev.Str("kind", "package.json")
   159  		default:
   160  			return nil
   161  		}
   162  		ev.Msg("found package")
   163  		success = true
   164  		out = append(out, p)
   165  		return nil
   166  	})
   167  }
   168  
   169  // DefaultRepository implements [indexer.DefaultRepoScanner].
   170  func (*Scanner) DefaultRepository(_ context.Context) *claircore.Repository {
   171  	return &Repository
   172  }