github.com/devseccon/trivy@v0.47.1-0.20231123133102-bd902a0bd996/pkg/fanal/analyzer/language/nodejs/npm/npm.go (about)

     1  package npm
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"io"
     7  	"io/fs"
     8  	"os"
     9  	"path"
    10  	"path/filepath"
    11  	"strings"
    12  
    13  	"golang.org/x/xerrors"
    14  
    15  	dio "github.com/aquasecurity/go-dep-parser/pkg/io"
    16  	"github.com/aquasecurity/go-dep-parser/pkg/nodejs/npm"
    17  	"github.com/aquasecurity/go-dep-parser/pkg/nodejs/packagejson"
    18  	godeptypes "github.com/aquasecurity/go-dep-parser/pkg/types"
    19  	"github.com/devseccon/trivy/pkg/fanal/analyzer"
    20  	"github.com/devseccon/trivy/pkg/fanal/analyzer/language"
    21  	"github.com/devseccon/trivy/pkg/fanal/types"
    22  	"github.com/devseccon/trivy/pkg/log"
    23  	"github.com/devseccon/trivy/pkg/utils/fsutils"
    24  	xpath "github.com/devseccon/trivy/pkg/x/path"
    25  )
    26  
    27  func init() {
    28  	analyzer.RegisterPostAnalyzer(analyzer.TypeNpmPkgLock, newNpmLibraryAnalyzer)
    29  }
    30  
    31  const (
    32  	version = 1
    33  )
    34  
    35  type npmLibraryAnalyzer struct {
    36  	lockParser    godeptypes.Parser
    37  	packageParser *packagejson.Parser
    38  }
    39  
    40  func newNpmLibraryAnalyzer(_ analyzer.AnalyzerOptions) (analyzer.PostAnalyzer, error) {
    41  	return &npmLibraryAnalyzer{
    42  		lockParser:    npm.NewParser(),
    43  		packageParser: packagejson.NewParser(),
    44  	}, nil
    45  }
    46  
    47  func (a npmLibraryAnalyzer) PostAnalyze(_ context.Context, input analyzer.PostAnalysisInput) (*analyzer.AnalysisResult, error) {
    48  	// Parse package-lock.json
    49  	required := func(path string, _ fs.DirEntry) bool {
    50  		return filepath.Base(path) == types.NpmPkgLock
    51  	}
    52  
    53  	var apps []types.Application
    54  	err := fsutils.WalkDir(input.FS, ".", required, func(filePath string, d fs.DirEntry, r io.Reader) error {
    55  		// Find all licenses from package.json files under node_modules dirs
    56  		licenses, err := a.findLicenses(input.FS, filePath)
    57  		if err != nil {
    58  			log.Logger.Errorf("Unable to collect licenses: %s", err)
    59  			licenses = make(map[string]string)
    60  		}
    61  
    62  		app, err := a.parseNpmPkgLock(input.FS, filePath)
    63  		if err != nil {
    64  			return xerrors.Errorf("parse error: %w", err)
    65  		} else if app == nil {
    66  			return nil
    67  		}
    68  
    69  		// Fill licenses
    70  		for i, lib := range app.Libraries {
    71  			if license, ok := licenses[lib.ID]; ok {
    72  				app.Libraries[i].Licenses = []string{license}
    73  			}
    74  		}
    75  
    76  		apps = append(apps, *app)
    77  		return nil
    78  	})
    79  	if err != nil {
    80  		return nil, xerrors.Errorf("package-lock.json/package.json walk error: %w", err)
    81  	}
    82  
    83  	return &analyzer.AnalysisResult{
    84  		Applications: apps,
    85  	}, nil
    86  }
    87  
    88  func (a npmLibraryAnalyzer) Required(filePath string, _ os.FileInfo) bool {
    89  	fileName := filepath.Base(filePath)
    90  	if fileName == types.NpmPkgLock && !xpath.Contains(filePath, "node_modules") {
    91  		return true
    92  	}
    93  	// The file path to package.json - */node_modules/<package_name>/package.json
    94  	// The path is slashed in analyzers.
    95  	dirs := strings.Split(path.Dir(filePath), "/")
    96  	if len(dirs) > 1 && dirs[len(dirs)-2] == "node_modules" && fileName == types.NpmPkg {
    97  		return true
    98  	}
    99  	return false
   100  }
   101  
   102  func (a npmLibraryAnalyzer) Type() analyzer.Type {
   103  	return analyzer.TypeNpmPkgLock
   104  }
   105  
   106  func (a npmLibraryAnalyzer) Version() int {
   107  	return version
   108  }
   109  
   110  func (a npmLibraryAnalyzer) parseNpmPkgLock(fsys fs.FS, filePath string) (*types.Application, error) {
   111  	f, err := fsys.Open(filePath)
   112  	if err != nil {
   113  		return nil, xerrors.Errorf("file open error: %w", err)
   114  	}
   115  	defer func() { _ = f.Close() }()
   116  
   117  	file, ok := f.(dio.ReadSeekCloserAt)
   118  	if !ok {
   119  		return nil, xerrors.Errorf("type assertion error: %w", err)
   120  	}
   121  
   122  	// parse package-lock.json file
   123  	return language.Parse(types.Npm, filePath, file, a.lockParser)
   124  }
   125  
   126  func (a npmLibraryAnalyzer) findLicenses(fsys fs.FS, lockPath string) (map[string]string, error) {
   127  	dir := path.Dir(lockPath)
   128  	root := path.Join(dir, "node_modules")
   129  	if _, err := fs.Stat(fsys, root); errors.Is(err, fs.ErrNotExist) {
   130  		log.Logger.Infof(`To collect the license information of packages in %q, "npm install" needs to be performed beforehand`, lockPath)
   131  		return nil, nil
   132  	}
   133  
   134  	// Parse package.json
   135  	required := func(path string, _ fs.DirEntry) bool {
   136  		return filepath.Base(path) == types.NpmPkg
   137  	}
   138  
   139  	// Traverse node_modules dir and find licenses
   140  	// Note that fs.FS is always slashed regardless of the platform,
   141  	// and path.Join should be used rather than filepath.Join.
   142  	licenses := make(map[string]string)
   143  	err := fsutils.WalkDir(fsys, root, required, func(filePath string, d fs.DirEntry, r io.Reader) error {
   144  		pkg, err := a.packageParser.Parse(r)
   145  		if err != nil {
   146  			return xerrors.Errorf("unable to parse %q: %w", filePath, err)
   147  		}
   148  
   149  		licenses[pkg.ID] = pkg.License
   150  		return nil
   151  	})
   152  	if err != nil {
   153  		return nil, xerrors.Errorf("walk error: %w", err)
   154  	}
   155  	return licenses, nil
   156  }