Skip to content

Commit 49c54b4

Browse files
authored
feat(python): add support for uv dev and optional dependencies (#8134)
Signed-off-by: nikpivkin <[email protected]>
1 parent 774e04d commit 49c54b4

File tree

6 files changed

+421
-68
lines changed

6 files changed

+421
-68
lines changed

docs/docs/coverage/language/python.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ The following table provides an outline of the features Trivy offers.
2727
| pip | requirements.txt | - | Include | - |||
2828
| Pipenv | Pipfile.lock || Include | - || Not needed |
2929
| Poetry | poetry.lock || [Exclude](#poetry) || - | Not needed |
30-
| uv | uv.lock || Exclude || - | Not needed |
30+
| uv | uv.lock || [Exclude](#uv) || - | Not needed | |
3131

3232

3333
| Packaging | Dependency graph |
@@ -136,6 +136,8 @@ Trivy uses `uv.lock` to identify dependencies and find vulnerabilities.
136136

137137
License detection is not supported for `uv`.
138138

139+
By default, Trivy doesn't report development dependencies. Use the `--include-dev-deps` flag to include them.
140+
139141
## Packaging
140142
Trivy parses the manifest files of installed packages in container image scanning and so on.
141143
See [here](https://packaging.python.org/en/latest/discussions/package-formats/) for the detail.

pkg/dependency/parser/python/uv/parse.go

+42-22
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,6 @@ func (l Lock) packages() map[string]Package {
2323
})
2424
}
2525

26-
func (l Lock) directDeps(root Package) set.Set[string] {
27-
deps := set.New[string]()
28-
for _, dep := range root.Dependencies {
29-
deps.Append(dep.Name)
30-
}
31-
return deps
32-
}
33-
3426
func prodDeps(root Package, packages map[string]Package) set.Set[string] {
3527
visited := set.New[string]()
3628
walkPackageDeps(root, packages, visited)
@@ -42,8 +34,8 @@ func walkPackageDeps(pkg Package, packages map[string]Package, visited set.Set[s
4234
return
4335
}
4436
visited.Append(pkg.Name)
45-
for _, dep := range pkg.Dependencies {
46-
depPkg, exists := packages[dep.Name]
37+
for depName := range pkg.nonDevDeps().Iter() {
38+
depPkg, exists := packages[depName]
4739
if !exists {
4840
continue
4941
}
@@ -69,10 +61,41 @@ func (l Lock) root() (Package, error) {
6961
}
7062

7163
type Package struct {
72-
Name string `toml:"name"`
73-
Version string `toml:"version"`
74-
Source Source `toml:"source"`
75-
Dependencies []Dependency `toml:"dependencies"`
64+
Name string `toml:"name"`
65+
Version string `toml:"version"`
66+
Source Source `toml:"source"`
67+
Dependencies Dependencies `toml:"dependencies"`
68+
DevDependencies map[string]Dependencies `toml:"dev-dependencies"`
69+
OptionalDependencies map[string]Dependencies `toml:"optional-dependencies"`
70+
}
71+
72+
func (p Package) directDeps() set.Set[string] {
73+
deps := p.nonDevDeps()
74+
for _, groupDeps := range p.DevDependencies {
75+
deps.Append(groupDeps.toSet().Items()...)
76+
77+
}
78+
return deps
79+
}
80+
81+
func (p Package) nonDevDeps() set.Set[string] {
82+
deps := p.Dependencies.toSet()
83+
for _, groupDeps := range p.OptionalDependencies {
84+
deps.Append(groupDeps.toSet().Items()...)
85+
}
86+
return deps
87+
}
88+
89+
type Dependencies []struct {
90+
Name string `toml:"name"`
91+
}
92+
93+
func (d Dependencies) toSet() set.Set[string] {
94+
deps := set.New[string]()
95+
for _, dep := range d {
96+
deps.Append(dep.Name)
97+
}
98+
return deps
7699
}
77100

78101
// https://github.com/astral-sh/uv/blob/f7d647e81d7e1e3be189324b06024ed2057168e6/crates/uv-resolver/src/lock/mod.rs#L572-L579
@@ -107,7 +130,7 @@ func (p *Parser) Parse(r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependenc
107130
}
108131

109132
packages := lock.packages()
110-
directDeps := lock.directDeps(rootPackage)
133+
directDeps := rootPackage.directDeps()
111134

112135
// Since each lockfile contains a root package with a list of direct dependencies,
113136
// we can identify all production dependencies by traversing the dependency graph
@@ -120,10 +143,6 @@ func (p *Parser) Parse(r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependenc
120143
)
121144

122145
for _, pkg := range lock.Packages {
123-
if !prodDeps.Contains(pkg.Name) {
124-
continue
125-
}
126-
127146
pkgID := packageID(pkg.Name, pkg.Version)
128147
relationship := ftypes.RelationshipIndirect
129148
if pkg.isRoot() {
@@ -137,16 +156,17 @@ func (p *Parser) Parse(r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependenc
137156
Name: pkg.Name,
138157
Version: pkg.Version,
139158
Relationship: relationship,
159+
Dev: !prodDeps.Contains(pkg.Name),
140160
})
141161

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

144-
for _, dep := range pkg.Dependencies {
145-
depPkg, exists := packages[dep.Name]
164+
for depName := range pkg.directDeps().Iter() {
165+
depPkg, exists := packages[depName]
146166
if !exists {
147167
continue
148168
}
149-
dependsOn = append(dependsOn, packageID(dep.Name, depPkg.Version))
169+
dependsOn = append(dependsOn, packageID(depName, depPkg.Version))
150170
}
151171

152172
if len(dependsOn) > 0 {

pkg/dependency/parser/python/uv/parse_testcase.go

+22-1
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,44 @@ var (
99
// uv init normal && cd normal
1010
// uv add requests==2.32.0
1111
// uv add --group dev pytest==8.3.4
12+
// uv add httpx==0.28.1 --extra socks
13+
// uv add orjson==3.10.12 --optional json
1214
// apk add jq
1315
// uv pip list --format json |jq -c 'sort_by(.name) | .[] | {"ID": (.name + "@" + .version), "Name": .name, "Version": .version}' | sed 's/$/,/' | sed 's/\"\([^"]*\)\":/\1:/g'
1416

1517
// add a root project
1618
// fill in the relationships between the packages
1719
uvNormal = []ftypes.Package{
1820
{ID: "[email protected]", Name: "normal", Version: "0.1.0", Relationship: ftypes.RelationshipRoot},
21+
{ID: "[email protected]", Name: "httpx", Version: "0.28.1", Relationship: ftypes.RelationshipDirect},
22+
{ID: "[email protected]", Name: "orjson", Version: "3.10.12", Relationship: ftypes.RelationshipDirect},
23+
{ID: "[email protected]", Name: "pytest", Version: "8.3.4", Relationship: ftypes.RelationshipDirect, Dev: true},
1924
{ID: "[email protected]", Name: "requests", Version: "2.32.0", Relationship: ftypes.RelationshipDirect},
25+
{ID: "[email protected]", Name: "anyio", Version: "4.7.0", Relationship: ftypes.RelationshipIndirect},
2026
{ID: "[email protected]", Name: "certifi", Version: "2024.12.14", Relationship: ftypes.RelationshipIndirect},
2127
{ID: "[email protected]", Name: "charset-normalizer", Version: "3.4.0", Relationship: ftypes.RelationshipIndirect},
28+
{ID: "[email protected]", Name: "colorama", Version: "0.4.6", Relationship: ftypes.RelationshipIndirect, Dev: true},
29+
{ID: "[email protected]", Name: "exceptiongroup", Version: "1.2.2", Relationship: ftypes.RelationshipIndirect},
30+
{ID: "[email protected]", Name: "h11", Version: "0.14.0", Relationship: ftypes.RelationshipIndirect},
31+
{ID: "[email protected]", Name: "httpcore", Version: "1.0.7", Relationship: ftypes.RelationshipIndirect},
2232
{ID: "[email protected]", Name: "idna", Version: "3.10", Relationship: ftypes.RelationshipIndirect},
33+
{ID: "[email protected]", Name: "iniconfig", Version: "2.0.0", Relationship: ftypes.RelationshipIndirect, Dev: true},
34+
{ID: "[email protected]", Name: "packaging", Version: "24.2", Relationship: ftypes.RelationshipIndirect, Dev: true},
35+
{ID: "[email protected]", Name: "pluggy", Version: "1.5.0", Relationship: ftypes.RelationshipIndirect, Dev: true},
36+
{ID: "[email protected]", Name: "sniffio", Version: "1.3.1", Relationship: ftypes.RelationshipIndirect},
37+
{ID: "[email protected]", Name: "socksio", Version: "1.0.0", Relationship: ftypes.RelationshipIndirect},
38+
{ID: "[email protected]", Name: "tomli", Version: "2.2.1", Relationship: ftypes.RelationshipIndirect, Dev: true},
39+
{ID: "[email protected]", Name: "typing-extensions", Version: "4.12.2", Relationship: ftypes.RelationshipIndirect},
2340
{ID: "[email protected]", Name: "urllib3", Version: "2.2.3", Relationship: ftypes.RelationshipIndirect},
2441
}
2542

2643
// add a root project
2744
uvNormalDeps = []ftypes.Dependency{
28-
{ID: "[email protected]", DependsOn: []string{"[email protected]"}},
45+
46+
{ID: "[email protected]", DependsOn: []string{"[email protected]", "[email protected]"}},
47+
48+
49+
2950
3051
}
3152
)

0 commit comments

Comments
 (0)