Files
trivy/pkg/dependency/parser/rust/cargo/parse.go
2025-04-30 06:24:09 +00:00

131 lines
3.5 KiB
Go

package cargo
import (
"io"
"sort"
"strings"
"github.com/BurntSushi/toml"
"github.com/samber/lo"
"golang.org/x/xerrors"
"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 cargoPkg struct {
Name string `toml:"name"`
Version string `toml:"version"`
Source string `toml:"source,omitempty"`
Dependencies []string `toml:"dependencies,omitempty"`
}
type Lockfile struct {
Packages []cargoPkg `toml:"package"`
}
type Parser struct {
logger *log.Logger
}
func NewParser() *Parser {
return &Parser{
logger: log.WithPrefix("cargo"),
}
}
func (p *Parser) Parse(r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependency, error) {
var lockfile Lockfile
decoder := toml.NewDecoder(r)
if _, err := decoder.Decode(&lockfile); err != nil {
return nil, nil, xerrors.Errorf("decode error: %w", err)
}
if _, err := r.Seek(0, io.SeekStart); err != nil {
return nil, nil, xerrors.Errorf("seek error: %w", err)
}
// naive parser to get line numbers by package from lock file
pkgParser := naivePkgParser{r: r}
lineNumIdx := pkgParser.parse()
// We need to get version for unique dependencies for lockfile v3 from lockfile.Packages
pkgMap := lo.SliceToMap(lockfile.Packages, func(pkg cargoPkg) (string, cargoPkg) {
return pkg.Name, pkg
})
var pkgs ftypes.Packages
var deps ftypes.Dependencies
for _, lpkg := range lockfile.Packages {
pkgID := packageID(lpkg.Name, lpkg.Version)
pkg := ftypes.Package{
ID: pkgID,
Name: lpkg.Name,
Version: lpkg.Version,
}
if pos, ok := lineNumIdx[pkgID]; ok {
pkg.Locations = []ftypes.Location{
{
StartLine: pos.start,
EndLine: pos.end,
},
}
}
pkgs = append(pkgs, pkg)
dep := p.parseDependencies(pkgID, lpkg, pkgMap)
if dep != nil {
deps = append(deps, *dep)
}
}
sort.Sort(pkgs)
sort.Sort(deps)
return pkgs, deps, nil
}
func (p *Parser) parseDependencies(pkgId string, pkg cargoPkg, pkgs map[string]cargoPkg) *ftypes.Dependency {
var dependOn []string
for _, pkgDep := range pkg.Dependencies {
/*
Dependency entries look like:
old Cargo.lock - https://github.com/rust-lang/cargo/blob/46bac2dc448ab12fe0f182bee8d35cc804d9a6af/tests/testsuite/lockfile_compat.rs#L48-L50
"unsafe-any 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)"
new Cargo.lock -https://github.com/rust-lang/cargo/blob/46bac2dc448ab12fe0f182bee8d35cc804d9a6af/tests/testsuite/lockfile_compat.rs#L39-L41
"unsafe-any" - if lock file contains only 1 version of dependency
"unsafe-any 0.4.2" if lock file contains more than 1 version of dependency
*/
fields := strings.Fields(pkgDep)
switch len(fields) {
// unique dependency in new lock file
case 1:
name := fields[0]
version, ok := pkgs[name]
if !ok {
p.logger.Debug("Cannot find version", log.String("name", name))
continue
}
dependOn = append(dependOn, packageID(name, version.Version))
// 2: non-unique dependency in new lock file
// 3: old lock file
case 2, 3:
dependOn = append(dependOn, packageID(fields[0], fields[1]))
default:
p.logger.Debug("Wrong dependency format", log.String("dep", pkgDep))
continue
}
}
if len(dependOn) > 0 {
sort.Strings(dependOn)
return &ftypes.Dependency{
ID: pkgId,
DependsOn: dependOn,
}
}
return nil
}
func packageID(name, version string) string {
return dependency.ID(ftypes.Cargo, name, version)
}