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 4db5a1b
Show file tree
Hide file tree
Showing 13 changed files with 526 additions and 2 deletions.
2 changes: 2 additions & 0 deletions docs/docs/configuration/reporting.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ The following languages are currently supported:
| | [yarn.lock][yarn-lock] |
| .NET | [packages.lock.json][dotnet-packages-lock] |
| Python | [poetry.lock][poetry-lock] |
| | [uv.lock][uv-lock] |
| Ruby | [Gemfile.lock][gemfile-lock] |
| Rust | [cargo-auditable binaries][cargo-binaries] |
| Go | [go.mod][go-mod] |
Expand Down Expand Up @@ -449,6 +450,7 @@ $ trivy convert --format table --severity CRITICAL result.json
[yarn-lock]: ../coverage/language/nodejs.md#yarn
[dotnet-packages-lock]: ../coverage/language/dotnet.md#packageslockjson
[poetry-lock]: ../coverage/language/python.md#poetry
[uv-lock]: ../coverage/language/python.md#uv
[gemfile-lock]: ../coverage/language/ruby.md#bundler
[go-mod]: ../coverage/language/golang.md#go-module
[composer-lock]: ../coverage/language/php.md#composerlock
Expand Down
1 change: 1 addition & 0 deletions docs/docs/coverage/language/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ On the other hand, when the target is a post-build artifact, like a container im
| | gemspec ||| - | - |
| [Python](python.md) | Pipfile.lock | - | - |||
| | poetry.lock | - | - |||
| | uv.lock | - | - |||
| | requirements.txt | - | - |||
| | egg package[^1] ||| - | - |
| | wheel package[^2] ||| - | - |
Expand Down
7 changes: 7 additions & 0 deletions docs/docs/coverage/language/python.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ The following scanners are supported for package managers.
| pip ||||
| Pipenv ||| - |
| Poetry ||| - |
| uv ||| - |

In addition, Trivy supports three formats of Python packages: `egg`, `wheel` and `conda`.
The following scanners are supported for Python packages.
Expand All @@ -26,6 +27,7 @@ The following table provides an outline of the features Trivy offers.
| pip | requirements.txt | - | Include | - |||
| Pipenv | Pipfile.lock || Include | - || Not needed |
| Poetry | poetry.lock || Exclude || - | Not needed |
| uv | uv.lock || Exclude || - | Not needed |


| Packaging | Dependency graph |
Expand Down Expand Up @@ -126,6 +128,11 @@ To build the correct dependency graph, `pyproject.toml` also needs to be present

License detection is not supported for `Poetry`.

### uv
Trivy uses `uv.lock` to identify dependencies and find vulnerabilities.

License detection is not supported for `uv`.

## Packaging
Trivy parses the manifest files of installed packages in container image scanning and so on.
See [here](https://packaging.python.org/en/latest/discussions/package-formats/) for the detail.
Expand Down
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
160 changes: 160 additions & 0 deletions pkg/fanal/analyzer/language/python/uv/lock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
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"`
}

// 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 4db5a1b

Please sign in to comment.