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