Files
trivy/pkg/dependency/parser/nodejs/pnpm/parse.go
Teppei Fukuda 3eecfc6b6e refactor: unify Library and Package structs (#6633)
Signed-off-by: knqyf263 <knqyf263@gmail.com>
Co-authored-by: DmitriyLewen <91113035+DmitriyLewen@users.noreply.github.com>
Co-authored-by: DmitriyLewen <dmitriy.lewen@smartforce.io>
2024-05-07 12:25:52 +00:00

189 lines
5.5 KiB
Go

package pnpm
import (
"fmt"
"strconv"
"strings"
"github.com/samber/lo"
"golang.org/x/xerrors"
"gopkg.in/yaml.v3"
"github.com/aquasecurity/go-version/pkg/semver"
"github.com/aquasecurity/trivy/pkg/dependency"
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 {
logger *log.Logger
}
func NewParser() *Parser {
return &Parser{
logger: log.WithPrefix("pnpm"),
}
}
func (p *Parser) Parse(r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependency, error) {
var lockFile LockFile
if err := yaml.NewDecoder(r).Decode(&lockFile); err != nil {
return nil, nil, xerrors.Errorf("decode error: %w", err)
}
lockVer := p.parseLockfileVersion(lockFile)
if lockVer < 0 {
return nil, nil, nil
}
pkgs, deps := p.parse(lockVer, lockFile)
return pkgs, deps, nil
}
func (p *Parser) parse(lockVer float64, lockFile LockFile) ([]ftypes.Package, []ftypes.Dependency) {
var pkgs []ftypes.Package
var deps []ftypes.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 = p.parsePackage(depPath, lockVer)
}
pkgID := packageID(name, version)
dependencies := make([]string, 0, len(info.Dependencies))
for depName, depVer := range info.Dependencies {
dependencies = append(dependencies, packageID(depName, depVer))
}
pkgs = append(pkgs, ftypes.Package{
ID: pkgID,
Name: name,
Version: version,
Relationship: lo.Ternary(isDirectPkg(name, lockFile.Dependencies), ftypes.RelationshipDirect, ftypes.RelationshipIndirect),
})
if len(dependencies) > 0 {
deps = append(deps, ftypes.Dependency{
ID: pkgID,
DependsOn: dependencies,
})
}
}
return pkgs, deps
}
func (p *Parser) 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 {
p.logger.Debug("Unable to convert the lock file version to float", log.Err(err))
return -1
} else {
return lockVer
}
default:
p.logger.Debug("Unknown type for the lock file version",
log.Any("version", lockFile.LockfileVersion))
return -1
}
}
// cf. https://github.com/pnpm/pnpm/blob/ce61f8d3c29eee46cee38d56ced45aea8a439a53/packages/dependency-path/src/index.ts#L112-L163
func (p *Parser) parsePackage(depPath string, lockFileVersion float64) (string, string) {
// The version separator is different between v5 and v6+.
versionSep := "@"
if lockFileVersion < 6 {
versionSep = "/"
}
return p.parseDepPath(depPath, versionSep)
}
func (p *Parser) 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/helper-annotate-as-pure@7.18.6" => "{"babel", "helper-annotate-as-pure@7.18.6"}
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+: "helper-annotate-as-pure@7.18.6" => {"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_@babel+core@7.21.8" => "7.21.5"
// - v6+: "7.21.5(@babel/core@7.20.7)" => "7.21.5"
if idx := strings.IndexAny(version, "_("); idx != -1 {
version = version[:idx]
}
if _, err := semver.Parse(version); err != nil {
p.logger.Debug("Skip non-semver package", log.String("pkg_path", depPath),
log.String("version", version), log.Err(err))
return "", ""
}
return name, version
}
func isDirectPkg(name string, directDeps map[string]interface{}) bool {
_, ok := directDeps[name]
return ok
}
func packageID(name, version string) string {
return dependency.ID(ftypes.Pnpm, name, version)
}