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