Skip to content

Commit

Permalink
feat(python): add support for uv
Browse files Browse the repository at this point in the history
Signed-off-by: nikpivkin <nikita.pivkin@smartforce.io>
  • Loading branch information
nikpivkin committed Dec 11, 2024
1 parent 4202c4b commit 49610b8
Show file tree
Hide file tree
Showing 10 changed files with 519 additions and 2 deletions.
2 changes: 1 addition & 1 deletion pkg/detector/library/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ func NewDriver(libType ftypes.LangType) (Driver, bool) {
case ftypes.NuGet, ftypes.DotNetCore, ftypes.PackagesProps:
ecosystem = vulnerability.NuGet
comparer = compare.GenericComparer{}
case ftypes.Pipenv, ftypes.Poetry, ftypes.Pip, ftypes.PythonPkg:
case ftypes.Pipenv, ftypes.Poetry, ftypes.Pip, ftypes.PythonPkg, ftypes.Uv:
ecosystem = vulnerability.Pip
comparer = pep440.Comparer{}
case ftypes.Pub:
Expand Down
1 change: 1 addition & 0 deletions pkg/fanal/analyzer/all/import.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import (
_ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/python/pip"
_ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/python/pipenv"
_ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/python/poetry"
_ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/python/uv"
_ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/ruby/bundler"
_ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/ruby/gemspec"
_ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/rust/binary"
Expand Down
1 change: 1 addition & 0 deletions pkg/fanal/analyzer/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ const (
TypePip Type = "pip"
TypePipenv Type = "pipenv"
TypePoetry Type = "poetry"
TypeUv Type = "uv"

// Go
TypeGoBinary Type = "gobinary"
Expand Down
163 changes: 163 additions & 0 deletions pkg/fanal/analyzer/language/python/uv/lock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package uv

import (
"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"
xio "github.com/aquasecurity/trivy/pkg/x/io"
)

type Lock struct {
Packages []Package `toml:"package"`
}

func (l Lock) packages() map[string]Package {
return lo.SliceToMap(l.Packages, func(pkg Package) (string, Package) {
return pkg.Name, pkg
})
}

func (l Lock) directDeps() map[string]struct{} {
deps := make(map[string]struct{})
root, exists := l.root()
if !exists {
return deps
}
for _, dep := range root.Dependencies {
deps[dep.Name] = struct{}{}
}
return deps
}

func (l Lock) devDeps() map[string]struct{} {
devDeps := make(map[string]struct{})

root, ok := l.root()
if !ok {
return devDeps
}

packages := l.packages()
visited := make(map[string]struct{})

var walkDeps func(Package)
walkDeps = func(pkg Package) {
if _, ok := visited[pkg.Name]; ok {
return
}
visited[pkg.Name] = struct{}{}
for _, dep := range pkg.Dependencies {
depPkg, exists := packages[dep.Name]
if !exists {
continue
}
walkDeps(depPkg)
}
}

walkDeps(root)

for _, pkg := range packages {
if _, exists := visited[pkg.Name]; !exists {
devDeps[pkg.Name] = struct{}{}
}
}

return devDeps
}

func (l Lock) root() (Package, bool) {
for _, pkg := range l.Packages {
if pkg.isRoot() {
return pkg, true
}
}

return Package{}, false
}

type Package struct {
Name string `toml:"name"`
Version string `toml:"version"`
Source Source `toml:"source"`
Dependencies []Dependency `toml:"dependencies"`
DevDependencies struct {
Dev []Dependency `toml:"dev"`
} `toml:"dev-dependencies"`
}

// https://github.com/astral-sh/uv/blob/f7d647e81d7e1e3be189324b06024ed2057168e6/crates/uv-resolver/src/lock/mod.rs#L572-L579
func (p Package) isRoot() bool {
return p.Source.Editable == "." || p.Source.Virtual == "."
}

type Source struct {
Editable string `toml:"editable"`
Virtual string `toml:"virtual"`
}

type Dependency struct {
Name string `toml:"name"`
}

type lockParser struct{}

func (p *lockParser) Parse(r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependency, error) {
var lock Lock
if _, err := toml.NewDecoder(r).Decode(&lock); err != nil {
return nil, nil, xerrors.Errorf("failed to decode uv lock file: %w", err)
}

packages := lock.packages()
directDeps := lock.directDeps()
devDeps := lock.devDeps()

var (
pkgs []ftypes.Package
deps []ftypes.Dependency
)

for _, pkg := range lock.Packages {
if _, ok := devDeps[pkg.Name]; ok {
continue
}

pkgID := dependency.ID(ftypes.Uv, pkg.Name, pkg.Version)
relationship := ftypes.RelationshipIndirect
if pkg.isRoot() {
relationship = ftypes.RelationshipRoot
} else if _, ok := directDeps[pkg.Name]; ok {
relationship = ftypes.RelationshipDirect
}

pkgs = append(pkgs, ftypes.Package{
ID: pkgID,
Name: pkg.Name,
Version: pkg.Version,
Indirect: relationship == ftypes.RelationshipIndirect,
Relationship: relationship,
})

dependsOn := make([]string, 0, len(pkg.Dependencies))

for _, dep := range pkg.Dependencies {
depPkg, exists := packages[dep.Name]
if !exists {
continue
}
dependsOn = append(dependsOn, dependency.ID(ftypes.Uv, dep.Name, depPkg.Version))
}

if len(dependsOn) > 0 {
deps = append(deps, ftypes.Dependency{
ID: pkgID,
DependsOn: dependsOn,
})
}
}

return pkgs, deps, nil
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 49610b8

Please sign in to comment.