Skip to content

Commit

Permalink
fix(python): skip dev group's deps for poetry
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 16, 2024
1 parent d7ac286 commit 23d4942
Show file tree
Hide file tree
Showing 8 changed files with 624 additions and 37 deletions.
12 changes: 6 additions & 6 deletions pkg/dependency/parser/python/poetry/parse_testcase.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ package poetry
import ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"

var (
// docker run --name pipenv --rm -it python@sha256:e1141f10176d74d1a0e87a7c0a0a5a98dd98ec5ac12ce867768f40c6feae2fd9 sh
// docker run --name poetry --rm -it python@sha256:e1141f10176d74d1a0e87a7c0a0a5a98dd98ec5ac12ce867768f40c6feae2fd9 sh
// apk add curl
// curl -sSL https://install.python-poetry.org | python3 -
// curl -sSL https://install.python-poetry.org | POETRY_VERSION=1.1.7 python3 -
// export PATH=/root/.local/bin:$PATH
// poetry new normal && cd normal
// poetry add pypi@2.1
Expand All @@ -14,9 +14,9 @@ var (
{ID: "pypi@2.1", Name: "pypi", Version: "2.1"},
}

// docker run --name pipenv --rm -it python@sha256:e1141f10176d74d1a0e87a7c0a0a5a98dd98ec5ac12ce867768f40c6feae2fd9 sh
// docker run --name poetry --rm -it python@sha256:e1141f10176d74d1a0e87a7c0a0a5a98dd98ec5ac12ce867768f40c6feae2fd9 sh
// apk add curl
// curl -sSL https://install.python-poetry.org | python3 -
// curl -sSL https://install.python-poetry.org | POETRY_VERSION=1.1.7 python3 -
// export PATH=/root/.local/bin:$PATH
// poetry new many && cd many
// curl -o poetry.lock https://raw.githubusercontent.com/python-poetry/poetry/c8945eb110aeda611cc6721565d7ad0c657d453a/poetry.lock
Expand Down Expand Up @@ -108,9 +108,9 @@ var (
{ID: "xattr@0.10.1", DependsOn: []string{"cffi@1.15.1"}},
}

// docker run --name pipenv --rm -it python@sha256:e1141f10176d74d1a0e87a7c0a0a5a98dd98ec5ac12ce867768f40c6feae2fd9 sh
// docker run --name poetry --rm -it python@sha256:e1141f10176d74d1a0e87a7c0a0a5a98dd98ec5ac12ce867768f40c6feae2fd9 sh
// apk add curl
// curl -sSL https://install.python-poetry.org | python3 -
// curl -sSL https://install.python-poetry.org | POETRY_VERSION=1.1.7 python3 -
// export PATH=/root/.local/bin:$PATH
// poetry new web && cd web
// poetry add flask@1.0.3
Expand Down
39 changes: 35 additions & 4 deletions pkg/dependency/parser/python/pyproject/pyproject.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package pyproject

import (
"fmt"
"io"

"github.com/BurntSushi/toml"
"github.com/samber/lo"
"golang.org/x/xerrors"

"github.com/aquasecurity/trivy/pkg/dependency/parser/python/poetry"
)

type PyProject struct {
Expand All @@ -15,8 +19,35 @@ type Tool struct {
Poetry Poetry `toml:"poetry"`
}

type PackageName string

func (a *PackageName) UnmarshalText(text []byte) error {
var err error
*a = PackageName(poetry.NormalizePkgName(string(text)))
return err
}

type Poetry struct {
Dependencies map[string]any `toml:"dependencies"`
Dependencies dependencies `toml:"dependencies"`
Groups map[string]Group `toml:"group"`
}

type Group struct {
Dependencies dependencies `toml:"dependencies"`
}

type dependencies map[string]any

func (d *dependencies) UnmarshalTOML(data any) error {
m, ok := data.(map[string]any)
if !ok {
return fmt.Errorf("dependencies must be map, but got: %T", data)
}

*d = lo.MapKeys(m, func(_ any, pkgName string) string {
return poetry.NormalizePkgName(pkgName)
})
return nil
}

// Parser parses pyproject.toml defined in PEP518.
Expand All @@ -28,10 +59,10 @@ func NewParser() *Parser {
return &Parser{}
}

func (p *Parser) Parse(r io.Reader) (map[string]any, error) {
func (p *Parser) Parse(r io.Reader) (PyProject, error) {
var conf PyProject
if _, err := toml.NewDecoder(r).Decode(&conf); err != nil {
return nil, xerrors.Errorf("toml decode error: %w", err)
return PyProject{}, xerrors.Errorf("toml decode error: %w", err)
}
return conf.Tool.Poetry.Dependencies, nil
return conf, nil
}
48 changes: 33 additions & 15 deletions pkg/dependency/parser/python/pyproject/pyproject_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,26 +15,44 @@ func TestParser_Parse(t *testing.T) {
tests := []struct {
name string
file string
want map[string]any
want pyproject.PyProject
wantErr assert.ErrorAssertionFunc
}{
{
name: "happy path",
file: "testdata/happy.toml",
want: map[string]any{
"flask": "^1.0",
"python": "^3.9",
"requests": map[string]any{
"version": "2.28.1",
"optional": true,
},
"virtualenv": []any{
map[string]any{
"version": "^20.4.3,!=20.4.5,!=20.4.6",
},
map[string]any{
"version": "<20.16.6",
"markers": "sys_platform == 'win32' and python_version == '3.9'",
want: pyproject.PyProject{
Tool: pyproject.Tool{
Poetry: pyproject.Poetry{
Dependencies: map[string]any{
"flask": "^1.0",
"python": "^3.9",
"requests": map[string]any{
"version": "2.28.1",
"optional": true,
},
"virtualenv": []any{
map[string]any{
"version": "^20.4.3,!=20.4.5,!=20.4.6",
},
map[string]any{
"version": "<20.16.6",
"markers": "sys_platform == 'win32' and python_version == '3.9'",
},
},
},
Groups: map[string]pyproject.Group{
"dev": {
Dependencies: map[string]any{
"pytest": "8.3.4",
},
},
"lint": {
Dependencies: map[string]any{
"ruff": "0.8.3",
},
},
},
},
},
},
Expand Down
7 changes: 7 additions & 0 deletions pkg/dependency/parser/python/pyproject/testdata/happy.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ virtualenv = [

[tool.poetry.dev-dependencies]

[tool.poetry.group.dev.dependencies]
pytest = "8.3.4"


[tool.poetry.group.lint.dependencies]
ruff = "0.8.3"

[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
67 changes: 55 additions & 12 deletions pkg/fanal/analyzer/language/python/poetry/poetry.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"io/fs"
"os"
"path/filepath"
"strings"

"github.com/samber/lo"
"golang.org/x/xerrors"
Expand Down Expand Up @@ -94,7 +95,7 @@ func (a poetryAnalyzer) parsePoetryLock(path string, r io.Reader) (*types.Applic
func (a poetryAnalyzer) mergePyProject(fsys fs.FS, dir string, app *types.Application) error {
// Parse pyproject.toml to identify the direct dependencies
path := filepath.Join(dir, types.PyProject)
p, err := a.parsePyProject(fsys, path)
project, err := a.parsePyProject(fsys, path)
if errors.Is(err, fs.ErrNotExist) {
// Assume all the packages are direct dependencies as it cannot identify them from poetry.lock
a.logger.Debug("pyproject.toml not found", log.FilePath(path))
Expand All @@ -105,34 +106,76 @@ func (a poetryAnalyzer) mergePyProject(fsys fs.FS, dir string, app *types.Applic

// Identify the direct/transitive dependencies
for i, pkg := range app.Packages {
if _, ok := p[pkg.Name]; ok {
if _, ok := project.Tool.Poetry.Dependencies[pkg.Name]; ok {
app.Packages[i].Relationship = types.RelationshipDirect
} else {
app.Packages[i].Indirect = true
app.Packages[i].Relationship = types.RelationshipIndirect
}
}

prodDeps := getProdDeps(project, app)

app.Packages = lo.Filter(app.Packages, func(pkg types.Package, _ int) bool {
_, ok := prodDeps[packageNameFromID(pkg.ID)]
return ok
})

return nil
}

func (a poetryAnalyzer) parsePyProject(fsys fs.FS, path string) (map[string]any, error) {
func getProdDeps(project pyproject.PyProject, app *types.Application) map[string]struct{} {
packages := lo.SliceToMap(app.Packages, func(pkg types.Package) (string, types.Package) {
return packageNameFromID(pkg.ID), pkg
})

visited := make(map[string]struct{})
for depName := range project.Tool.Poetry.Dependencies {
walkPackageDeps(depName, packages, visited)
}

for group, groupDeps := range project.Tool.Poetry.Groups {
if group == "dev" {
continue
}
for depName := range groupDeps.Dependencies {
walkPackageDeps(depName, packages, visited)
}
}
return visited
}

func walkPackageDeps(packageName string, packages map[string]types.Package, visited map[string]struct{}) {
if _, ok := visited[packageName]; ok {
return
}
visited[packageName] = struct{}{}
pkg, exists := packages[packageName]
if !exists {
return
}

for _, dep := range pkg.DependsOn {
walkPackageDeps(packageNameFromID(dep), packages, visited)
}
}

func packageNameFromID(id string) string {
return strings.Split(id, "@")[0]
}

func (a poetryAnalyzer) parsePyProject(fsys fs.FS, path string) (pyproject.PyProject, error) {
// Parse pyproject.toml
f, err := fsys.Open(path)
if err != nil {
return nil, xerrors.Errorf("file open error: %w", err)
return pyproject.PyProject{}, xerrors.Errorf("file open error: %w", err)
}
defer f.Close()

parsed, err := a.pyprojectParser.Parse(f)
project, err := a.pyprojectParser.Parse(f)
if err != nil {
return nil, err
return pyproject.PyProject{}, err
}

// Packages from `pyproject.toml` can use uppercase characters, `.` and `_`.
parsed = lo.MapKeys(parsed, func(_ any, pkgName string) string {
return poetry.NormalizePkgName(pkgName)
})

return parsed, nil
return project, nil
}
93 changes: 93 additions & 0 deletions pkg/fanal/analyzer/language/python/poetry/poetry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,99 @@ func Test_poetryLibraryAnalyzer_Analyze(t *testing.T) {
dir: "testdata/sad",
want: &analyzer.AnalysisResult{},
},
{
// docker run --name poetry --rm -it python@sha256:e1141f10176d74d1a0e87a7c0a0a5a98dd98ec5ac12ce867768f40c6feae2fd9 sh
// wget -qO- https://install.python-poetry.org | POETRY_VERSION=1.8.5 python3 -
// export PATH="/root/.local/bin:$PATH"
// poetry new groups && cd groups
// poetry add requests@2.32.3
// poetry add --group dev pytest@8.3.4
// poetry add --group lint ruff@0.8.3
// poetry add --optional typing-inspect@0.9.0
name: "skip deps from groups",
dir: "testdata/with-groups",
want: &analyzer.AnalysisResult{
Applications: []types.Application{
{
Type: types.Poetry,
FilePath: "poetry.lock",
Packages: types.Packages{
{
ID: "certifi@2024.12.14",
Name: "certifi",
Version: "2024.12.14",
Indirect: true,
Relationship: types.RelationshipIndirect,
},
{
ID: "charset-normalizer@3.4.0",
Name: "charset-normalizer",
Version: "3.4.0",
Indirect: true,
Relationship: types.RelationshipIndirect,
},
{
ID: "idna@3.10",
Name: "idna",
Version: "3.10",
Indirect: true,
Relationship: types.RelationshipIndirect,
},
{
ID: "mypy-extensions@1.0.0",
Name: "mypy-extensions",
Version: "1.0.0",
Indirect: true,
Relationship: types.RelationshipIndirect,
},
{
ID: "requests@2.32.3",
Name: "requests",
Version: "2.32.3",
DependsOn: []string{
"certifi@2024.12.14",
"charset-normalizer@3.4.0",
"idna@3.10",
"urllib3@2.2.3",
},
Relationship: types.RelationshipDirect,
},
{
ID: "ruff@0.8.3",
Name: "ruff",
Version: "0.8.3",
Indirect: true,
Relationship: types.RelationshipIndirect,
},
{
ID: "typing-extensions@4.12.2",
Name: "typing-extensions",
Version: "4.12.2",
Indirect: true,
Relationship: types.RelationshipIndirect,
},
{
ID: "typing-inspect@0.9.0",
Name: "typing-inspect",
Version: "0.9.0",
DependsOn: []string{
"mypy-extensions@1.0.0",
"typing-extensions@4.12.2",
},
Relationship: types.RelationshipDirect,
},
{
ID: "urllib3@2.2.3",
Name: "urllib3",
Version: "2.2.3",
Indirect: true,
Relationship: types.RelationshipIndirect,
},
},
},
},
},
},
}

for _, tt := range tests {
Expand Down
Loading

0 comments on commit 23d4942

Please sign in to comment.