mirror of
https://github.com/aquasecurity/trivy.git
synced 2025-12-23 15:37:50 -08:00
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>
189 lines
5.5 KiB
Go
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)
|
|
}
|