Skip to content

Commit 774e04d

Browse files
authored
feat(python): add support for poetry dev dependencies (#8152)
Signed-off-by: nikpivkin <[email protected]>
1 parent 735335f commit 774e04d

File tree

5 files changed

+248
-22
lines changed

5 files changed

+248
-22
lines changed

docs/docs/coverage/language/python.md

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

3232

@@ -128,6 +128,9 @@ To build the correct dependency graph, `pyproject.toml` also needs to be present
128128

129129
License detection is not supported for `Poetry`.
130130

131+
By default, Trivy doesn't report development dependencies. Use the `--include-dev-deps` flag to include them.
132+
133+
131134
### uv
132135
Trivy uses `uv.lock` to identify dependencies and find vulnerabilities.
133136

pkg/fanal/analyzer/language/python/poetry/poetry.go

+17-17
Original file line numberDiff line numberDiff line change
@@ -104,45 +104,45 @@ func (a poetryAnalyzer) mergePyProject(fsys fs.FS, dir string, app *types.Applic
104104
return xerrors.Errorf("unable to parse %s: %w", path, err)
105105
}
106106

107-
// Identify the direct/transitive dependencies
107+
directDeps := directDeps(project)
108+
prodDeps := prodPackages(app, project.Tool.Poetry.Dependencies)
109+
110+
// Identify the direct/transitive/dev dependencies
108111
for i, pkg := range app.Packages {
109-
if project.Tool.Poetry.Dependencies.Contains(pkg.Name) {
112+
app.Packages[i].Dev = !prodDeps.Contains(pkg.ID)
113+
if directDeps.Contains(pkg.Name) {
110114
app.Packages[i].Relationship = types.RelationshipDirect
111115
} else {
112116
app.Packages[i].Indirect = true
113117
app.Packages[i].Relationship = types.RelationshipIndirect
114118
}
115119
}
116-
117-
filterProdPackages(project, app)
118120
return nil
119121
}
120122

121-
func filterProdPackages(project pyproject.PyProject, app *types.Application) {
123+
func directDeps(project pyproject.PyProject) set.Set[string] {
124+
deps := project.Tool.Poetry.Dependencies.Clone()
125+
for _, groupDeps := range project.Tool.Poetry.Groups {
126+
deps.Append(groupDeps.Dependencies.Items()...)
127+
}
128+
return deps
129+
}
130+
131+
func prodPackages(app *types.Application, prodRootDeps set.Set[string]) set.Set[string] {
122132
packages := lo.SliceToMap(app.Packages, func(pkg types.Package) (string, types.Package) {
123133
return pkg.ID, pkg
124134
})
125135

126136
visited := set.New[string]()
127-
deps := project.Tool.Poetry.Dependencies
128-
129-
for group, groupDeps := range project.Tool.Poetry.Groups {
130-
if group == "dev" {
131-
continue
132-
}
133-
deps.Set = deps.Union(groupDeps.Dependencies)
134-
}
135137

136138
for _, pkg := range packages {
137-
if !deps.Contains(pkg.Name) {
139+
if !prodRootDeps.Contains(pkg.Name) {
138140
continue
139141
}
140142
walkPackageDeps(pkg.ID, packages, visited)
141143
}
142144

143-
app.Packages = lo.Filter(app.Packages, func(pkg types.Package, _ int) bool {
144-
return visited.Contains(pkg.ID)
145-
})
145+
return visited
146146
}
147147

148148
func walkPackageDeps(pkgID string, packages map[string]types.Package, visited set.Set[string]) {

pkg/fanal/analyzer/language/python/poetry/poetry_test.go

+123-2
Original file line numberDiff line numberDiff line change
@@ -187,17 +187,31 @@ func Test_poetryLibraryAnalyzer_Analyze(t *testing.T) {
187187
// export PATH="/root/.local/bin:$PATH"
188188
// poetry new groups && cd groups
189189
// poetry add [email protected]
190+
// poetry add [email protected] --extras socks
191+
// poetry add --optional [email protected]
190192
// poetry add --group dev [email protected]
191193
// poetry add --group lint [email protected]
192-
// poetry add --optional [email protected]
193-
name: "skip deps from groups",
194+
name: "with groups",
194195
dir: "testdata/with-groups",
195196
want: &analyzer.AnalysisResult{
196197
Applications: []types.Application{
197198
{
198199
Type: types.Poetry,
199200
FilePath: "poetry.lock",
200201
Packages: types.Packages{
202+
{
203+
204+
Name: "anyio",
205+
Version: "4.7.0",
206+
Indirect: true,
207+
Relationship: types.RelationshipIndirect,
208+
DependsOn: []string{
209+
210+
211+
212+
213+
},
214+
},
201215
{
202216
203217
Name: "certifi",
@@ -212,20 +226,105 @@ func Test_poetryLibraryAnalyzer_Analyze(t *testing.T) {
212226
Indirect: true,
213227
Relationship: types.RelationshipIndirect,
214228
},
229+
{
230+
231+
Name: "colorama",
232+
Version: "0.4.6",
233+
Indirect: true,
234+
Relationship: types.RelationshipIndirect,
235+
Dev: true,
236+
},
237+
{
238+
239+
Name: "exceptiongroup",
240+
Version: "1.2.2",
241+
Indirect: true,
242+
Relationship: types.RelationshipIndirect,
243+
},
244+
{
245+
246+
Name: "h11",
247+
Version: "0.14.0",
248+
Indirect: true,
249+
Relationship: types.RelationshipIndirect,
250+
},
251+
{
252+
253+
Name: "httpcore",
254+
Version: "1.0.7",
255+
Indirect: true,
256+
Relationship: types.RelationshipIndirect,
257+
DependsOn: []string{
258+
259+
260+
},
261+
},
262+
{
263+
264+
Name: "httpx",
265+
Version: "0.28.1",
266+
Relationship: types.RelationshipDirect,
267+
DependsOn: []string{
268+
269+
270+
271+
272+
273+
},
274+
},
215275
{
216276
217277
Name: "idna",
218278
Version: "3.10",
219279
Indirect: true,
220280
Relationship: types.RelationshipIndirect,
221281
},
282+
{
283+
284+
Name: "iniconfig",
285+
Version: "2.0.0",
286+
Indirect: true,
287+
Relationship: types.RelationshipIndirect,
288+
Dev: true,
289+
},
222290
{
223291
224292
Name: "mypy-extensions",
225293
Version: "1.0.0",
226294
Indirect: true,
227295
Relationship: types.RelationshipIndirect,
228296
},
297+
{
298+
299+
Name: "packaging",
300+
Version: "24.2",
301+
Indirect: true,
302+
Relationship: types.RelationshipIndirect,
303+
Dev: true,
304+
},
305+
{
306+
307+
Name: "pluggy",
308+
Version: "1.5.0",
309+
Indirect: true,
310+
Relationship: types.RelationshipIndirect,
311+
Dev: true,
312+
},
313+
{
314+
315+
Name: "pytest",
316+
Version: "8.3.4",
317+
Relationship: types.RelationshipDirect,
318+
Dev: true,
319+
DependsOn: []string{
320+
321+
322+
323+
324+
325+
326+
},
327+
},
229328
{
230329
231330
Name: "requests",
@@ -242,8 +341,30 @@ func Test_poetryLibraryAnalyzer_Analyze(t *testing.T) {
242341
243342
Name: "ruff",
244343
Version: "0.8.3",
344+
Relationship: types.RelationshipDirect,
345+
Dev: true,
346+
},
347+
{
348+
349+
Name: "sniffio",
350+
Version: "1.3.1",
351+
Indirect: true,
352+
Relationship: types.RelationshipIndirect,
353+
},
354+
{
355+
356+
Name: "socksio",
357+
Version: "1.0.0",
358+
Indirect: true,
359+
Relationship: types.RelationshipIndirect,
360+
},
361+
{
362+
363+
Name: "tomli",
364+
Version: "2.2.1",
245365
Indirect: true,
246366
Relationship: types.RelationshipIndirect,
367+
Dev: true,
247368
},
248369
{
249370

pkg/fanal/analyzer/language/python/poetry/testdata/with-groups/poetry.lock

+103-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/fanal/analyzer/language/python/poetry/testdata/with-groups/pyproject.toml

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ readme = "README.md"
99
python = "^3.9"
1010
requests = "2.32.3"
1111
typing-inspect = {version = "0.9.0", optional = true}
12+
httpx = {version = "0.28.1", extras = ["socks"]}
1213

1314

1415
[tool.poetry.group.dev.dependencies]

0 commit comments

Comments
 (0)