-
Notifications
You must be signed in to change notification settings - Fork 2.4k
/
parse.go
182 lines (155 loc) · 5.31 KB
/
parse.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
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
package pnpm
import (
"fmt"
"strconv"
"strings"
"golang.org/x/xerrors"
"gopkg.in/yaml.v3"
"github.com/aquasecurity/go-version/pkg/semver"
"github.com/aquasecurity/trivy/pkg/dependency"
"github.com/aquasecurity/trivy/pkg/dependency/types"
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
"github.com/aquasecurity/trivy/pkg/log"
xio "github.com/aquasecurity/trivy/pkg/x/io"
)
type PackageResolution struct {
Tarball string `yaml:"tarball,omitempty"`
}
type PackageInfo struct {
Resolution PackageResolution `yaml:"resolution"`
Dependencies map[string]string `yaml:"dependencies,omitempty"`
DevDependencies map[string]string `yaml:"devDependencies,omitempty"`
IsDev bool `yaml:"dev,omitempty"`
Name string `yaml:"name,omitempty"`
Version string `yaml:"version,omitempty"`
}
type LockFile struct {
LockfileVersion any `yaml:"lockfileVersion"`
Dependencies map[string]any `yaml:"dependencies,omitempty"`
DevDependencies map[string]any `yaml:"devDependencies,omitempty"`
Packages map[string]PackageInfo `yaml:"packages,omitempty"`
}
type Parser struct{}
func NewParser() types.Parser {
return &Parser{}
}
func (p *Parser) ID(name, version string) string {
return dependency.ID(ftypes.Pnpm, name, version)
}
func (p *Parser) Parse(r xio.ReadSeekerAt) ([]types.Library, []types.Dependency, error) {
var lockFile LockFile
if err := yaml.NewDecoder(r).Decode(&lockFile); err != nil {
return nil, nil, xerrors.Errorf("decode error: %w", err)
}
lockVer := parseLockfileVersion(lockFile)
if lockVer < 0 {
return nil, nil, nil
}
libs, deps := p.parse(lockVer, lockFile)
return libs, deps, nil
}
func (p *Parser) parse(lockVer float64, lockFile LockFile) ([]types.Library, []types.Dependency) {
var libs []types.Library
var deps []types.Dependency
// Dependency path is a path to a dependency with a specific set of resolved subdependencies.
// cf. https://github.com/pnpm/spec/blob/ad27a225f81d9215becadfa540ef05fa4ad6dd60/dependency-path.md
for depPath, info := range lockFile.Packages {
if info.IsDev {
continue
}
// Dependency name may be present in dependencyPath or Name field. Same for Version.
// e.g. packages installed from local directory or tarball
// cf. https://github.com/pnpm/spec/blob/274ff02de23376ad59773a9f25ecfedd03a41f64/lockfile/6.0.md#packagesdependencypathname
name := info.Name
version := info.Version
if name == "" {
name, version = parsePackage(depPath, lockVer)
}
pkgID := p.ID(name, version)
dependencies := make([]string, 0, len(info.Dependencies))
for depName, depVer := range info.Dependencies {
dependencies = append(dependencies, p.ID(depName, depVer))
}
libs = append(libs, types.Library{
ID: pkgID,
Name: name,
Version: version,
Indirect: isIndirectLib(name, lockFile.Dependencies),
})
if len(dependencies) > 0 {
deps = append(deps, types.Dependency{
ID: pkgID,
DependsOn: dependencies,
})
}
}
return libs, deps
}
func parseLockfileVersion(lockFile LockFile) float64 {
switch v := lockFile.LockfileVersion.(type) {
// v5
case float64:
return v
// v6+
case string:
if lockVer, err := strconv.ParseFloat(v, 64); err != nil {
log.Logger.Debugf("Unable to convert the lock file version to float: %s", err)
return -1
} else {
return lockVer
}
default:
log.Logger.Debugf("Unknown type for the lock file version: %s", lockFile.LockfileVersion)
return -1
}
}
func isIndirectLib(name string, directDeps map[string]interface{}) bool {
_, ok := directDeps[name]
return !ok
}
// cf. https://github.com/pnpm/pnpm/blob/ce61f8d3c29eee46cee38d56ced45aea8a439a53/packages/dependency-path/src/index.ts#L112-L163
func parsePackage(depPath string, lockFileVersion float64) (string, string) {
// The version separator is different between v5 and v6+.
versionSep := "@"
if lockFileVersion < 6 {
versionSep = "/"
}
return parseDepPath(depPath, versionSep)
}
func parseDepPath(depPath, versionSep string) (string, string) {
// Skip registry
// e.g.
// - "registry.npmjs.org/lodash/4.17.10" => "lodash/4.17.10"
// - "registry.npmjs.org/@babel/generator/7.21.9" => "@babel/generator/7.21.9"
// - "/lodash/4.17.10" => "lodash/4.17.10"
_, depPath, _ = strings.Cut(depPath, "/")
// Parse scope
// e.g.
// - v5: "@babel/generator/7.21.9" => {"babel", "generator/7.21.9"}
// - v6+: "@babel/[email protected]" => "{"babel", "[email protected]"}
var scope string
if strings.HasPrefix(depPath, "@") {
scope, depPath, _ = strings.Cut(depPath, "/")
}
// Parse package name
// e.g.
// - v5: "generator/7.21.9" => {"generator", "7.21.9"}
// - v6+: "[email protected]" => {"helper-annotate-as-pure", "7.18.6"}
var name, version string
name, version, _ = strings.Cut(depPath, versionSep)
if scope != "" {
name = fmt.Sprintf("%s/%s", scope, name)
}
// Trim peer deps
// e.g.
// - v5: "7.21.5_@[email protected]" => "7.21.5"
// - v6+: "7.21.5(@babel/[email protected])" => "7.21.5"
if idx := strings.IndexAny(version, "_("); idx != -1 {
version = version[:idx]
}
if _, err := semver.Parse(version); err != nil {
log.Logger.Debugf("Skip %q package. %q doesn't match semver: %s", depPath, version, err)
return "", ""
}
return name, version
}