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 }