mirror of
https://github.com/aquasecurity/trivy.git
synced 2026-02-01 22:33:14 +08:00
156 lines
4.4 KiB
Go
156 lines
4.4 KiB
Go
package poetry
|
|
|
|
import (
|
|
"context"
|
|
"slices"
|
|
|
|
"github.com/BurntSushi/toml"
|
|
"golang.org/x/xerrors"
|
|
|
|
version "github.com/aquasecurity/go-pep440-version"
|
|
"github.com/aquasecurity/trivy/pkg/dependency"
|
|
"github.com/aquasecurity/trivy/pkg/dependency/parser/python"
|
|
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
|
|
"github.com/aquasecurity/trivy/pkg/log"
|
|
xio "github.com/aquasecurity/trivy/pkg/x/io"
|
|
)
|
|
|
|
type Lockfile struct {
|
|
Packages []struct {
|
|
Category string `toml:"category"`
|
|
Groups []string `toml:"groups"`
|
|
Description string `toml:"description"`
|
|
Marker string `toml:"marker,omitempty"`
|
|
Name string `toml:"name"`
|
|
Optional bool `toml:"optional"`
|
|
PythonVersions string `toml:"python-versions"`
|
|
Version string `toml:"version"`
|
|
Dependencies map[string]any `toml:"dependencies"`
|
|
Metadata any
|
|
} `toml:"package"`
|
|
}
|
|
|
|
type Parser struct {
|
|
logger *log.Logger
|
|
}
|
|
|
|
func NewParser() *Parser {
|
|
return &Parser{
|
|
logger: log.WithPrefix("poetry"),
|
|
}
|
|
}
|
|
|
|
func (p *Parser) Parse(_ context.Context, r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependency, error) {
|
|
var lockfile Lockfile
|
|
if _, err := toml.NewDecoder(r).Decode(&lockfile); err != nil {
|
|
return nil, nil, xerrors.Errorf("failed to decode poetry.lock: %w", err)
|
|
}
|
|
|
|
// Keep all installed versions
|
|
pkgVersions := p.parseVersions(lockfile)
|
|
|
|
var pkgs []ftypes.Package
|
|
var deps []ftypes.Dependency
|
|
for _, pkg := range lockfile.Packages {
|
|
pkgID := packageID(pkg.Name, pkg.Version)
|
|
pkgs = append(pkgs, ftypes.Package{
|
|
ID: pkgID,
|
|
Name: pkg.Name,
|
|
Version: pkg.Version,
|
|
// TODO upgrade logic for working with groups
|
|
// Mark only:
|
|
// - `category = "dev"`
|
|
// - groups without `main`. e.g. `groups = ["dev"]`
|
|
Dev: pkg.Category == "dev" || (len(pkg.Groups) > 0 && !slices.Contains(pkg.Groups, "main")),
|
|
})
|
|
|
|
dependsOn := p.parseDependencies(pkg.Dependencies, pkgVersions)
|
|
if len(dependsOn) != 0 {
|
|
deps = append(deps, ftypes.Dependency{
|
|
ID: pkgID,
|
|
DependsOn: dependsOn,
|
|
})
|
|
}
|
|
}
|
|
return pkgs, deps, nil
|
|
}
|
|
|
|
// parseVersions stores all installed versions of packages for use in dependsOn
|
|
// as the dependencies of packages use version range.
|
|
func (p *Parser) parseVersions(lockfile Lockfile) map[string][]string {
|
|
pkgVersions := make(map[string][]string)
|
|
for _, pkg := range lockfile.Packages {
|
|
if pkg.Category == "dev" {
|
|
continue
|
|
}
|
|
if vers, ok := pkgVersions[pkg.Name]; ok {
|
|
pkgVersions[pkg.Name] = append(vers, pkg.Version)
|
|
} else {
|
|
pkgVersions[pkg.Name] = []string{pkg.Version}
|
|
}
|
|
}
|
|
return pkgVersions
|
|
}
|
|
|
|
func (p *Parser) parseDependencies(deps map[string]any, pkgVersions map[string][]string) []string {
|
|
var dependsOn []string
|
|
for name, versRange := range deps {
|
|
if dep, err := p.parseDependency(name, versRange, pkgVersions); err != nil {
|
|
p.logger.Debug("Failed to parse poetry dependency", log.Err(err))
|
|
} else if dep != "" {
|
|
dependsOn = append(dependsOn, dep)
|
|
}
|
|
}
|
|
slices.Sort(dependsOn)
|
|
return dependsOn
|
|
}
|
|
|
|
func (p *Parser) parseDependency(name string, versRange any, pkgVersions map[string][]string) (string, error) {
|
|
name = python.NormalizePkgName(name, true)
|
|
vers, ok := pkgVersions[name]
|
|
if !ok {
|
|
return "", xerrors.Errorf("no version found for %q", name)
|
|
}
|
|
|
|
for _, ver := range vers {
|
|
var vRange string
|
|
|
|
switch r := versRange.(type) {
|
|
case string:
|
|
vRange = r
|
|
case map[string]any:
|
|
for k, v := range r {
|
|
if k == "version" {
|
|
vRange = v.(string)
|
|
}
|
|
}
|
|
}
|
|
|
|
if matched, err := matchVersion(ver, vRange); err != nil {
|
|
return "", xerrors.Errorf("failed to match version for %s: %w", name, err)
|
|
} else if matched {
|
|
return packageID(name, ver), nil
|
|
}
|
|
}
|
|
return "", xerrors.Errorf("no matched version found for %q", name)
|
|
}
|
|
|
|
// matchVersion checks if the package version satisfies the given constraint.
|
|
func matchVersion(currentVersion, constraint string) (bool, error) {
|
|
v, err := version.Parse(currentVersion)
|
|
if err != nil {
|
|
return false, xerrors.Errorf("python version error (%s): %s", currentVersion, err)
|
|
}
|
|
|
|
c, err := version.NewSpecifiers(constraint, version.WithPreRelease(true))
|
|
if err != nil {
|
|
return false, xerrors.Errorf("python constraint error (%s): %s", constraint, err)
|
|
}
|
|
|
|
return c.Check(v), nil
|
|
}
|
|
|
|
func packageID(name, ver string) string {
|
|
return dependency.ID(ftypes.Poetry, name, ver)
|
|
}
|