From ea3a124fc7162c30c7f1a59bdb28db0b3c8bb86d Mon Sep 17 00:00:00 2001 From: DmitriyLewen <91113035+DmitriyLewen@users.noreply.github.com> Date: Wed, 29 May 2024 10:53:16 +0600 Subject: [PATCH] fix(python): add package name and version validation for `requirements.txt` files. (#6804) --- pkg/dependency/parser/python/pip/parse.go | 36 ++++++++-- .../parser/python/pip/parse_test.go | 66 +++++++++++-------- .../requirements_with_templating_engine.txt | 5 ++ 3 files changed, 77 insertions(+), 30 deletions(-) create mode 100644 pkg/dependency/parser/python/pip/testdata/requirements_with_templating_engine.txt diff --git a/pkg/dependency/parser/python/pip/parse.go b/pkg/dependency/parser/python/pip/parse.go index a849eb0cbea2..0d9e040f952b 100644 --- a/pkg/dependency/parser/python/pip/parse.go +++ b/pkg/dependency/parser/python/pip/parse.go @@ -10,7 +10,9 @@ import ( "golang.org/x/text/transform" "golang.org/x/xerrors" + version "github.com/aquasecurity/go-pep440-version" ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" xio "github.com/aquasecurity/trivy/pkg/x/io" ) @@ -22,10 +24,14 @@ const ( endExtras string = "]" ) -type Parser struct{} +type Parser struct { + logger *log.Logger +} func NewParser() *Parser { - return &Parser{} + return &Parser{ + logger: log.WithPrefix("pip"), + } } func (p *Parser) Parse(r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependency, error) { @@ -40,8 +46,8 @@ func (p *Parser) Parse(r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependenc var lineNumber int for scanner.Scan() { lineNumber++ - line := scanner.Text() - line = strings.ReplaceAll(line, " ", "") + text := scanner.Text() + line := strings.ReplaceAll(text, " ", "") line = strings.ReplaceAll(line, `\`, "") line = removeExtras(line) line = rStripByKey(line, commentMarker) @@ -51,6 +57,12 @@ func (p *Parser) Parse(r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependenc if len(s) != 2 { continue } + + if !isValidName(s[0]) || !isValidVersion(s[1]) { + p.logger.Debug("Invalid package name/version in requirements.txt.", log.String("line", text)) + continue + } + pkgs = append(pkgs, ftypes.Package{ Name: s[0], Version: s[1], @@ -83,3 +95,19 @@ func removeExtras(line string) string { } return line } + +func isValidName(name string) bool { + for _, r := range name { + // only characters [A-Z0-9._-] are allowed (case insensitive) + // cf. https://peps.python.org/pep-0508/#names + if !unicode.IsLetter(r) && !unicode.IsDigit(r) && r != '.' && r != '_' && r != '-' { + return false + } + } + return true +} + +func isValidVersion(ver string) bool { + _, err := version.Parse(ver) + return err == nil +} diff --git a/pkg/dependency/parser/python/pip/parse_test.go b/pkg/dependency/parser/python/pip/parse_test.go index d887205fb148..3a13c5272cc8 100644 --- a/pkg/dependency/parser/python/pip/parse_test.go +++ b/pkg/dependency/parser/python/pip/parse_test.go @@ -2,7 +2,6 @@ package pip import ( "os" - "path" "testing" "github.com/stretchr/testify/assert" @@ -12,57 +11,72 @@ import ( ) func TestParse(t *testing.T) { - vectors := []struct { - file string - want []ftypes.Package + tests := []struct { + name string + filePath string + want []ftypes.Package }{ { - file: "testdata/requirements_flask.txt", - want: requirementsFlask, + name: "happy path", + filePath: "testdata/requirements_flask.txt", + want: requirementsFlask, }, { - file: "testdata/requirements_comments.txt", - want: requirementsComments, + name: "happy path with comments", + filePath: "testdata/requirements_comments.txt", + want: requirementsComments, }, { - file: "testdata/requirements_spaces.txt", - want: requirementsSpaces, + name: "happy path with spaces", + filePath: "testdata/requirements_spaces.txt", + want: requirementsSpaces, }, { - file: "testdata/requirements_no_version.txt", - want: requirementsNoVersion, + name: "happy path with dependency without version", + filePath: "testdata/requirements_no_version.txt", + want: requirementsNoVersion, }, { - file: "testdata/requirements_operator.txt", - want: requirementsOperator, + name: "happy path with operator", + filePath: "testdata/requirements_operator.txt", + want: requirementsOperator, }, { - file: "testdata/requirements_hash.txt", - want: requirementsHash, + name: "happy path with hash", + filePath: "testdata/requirements_hash.txt", + want: requirementsHash, }, { - file: "testdata/requirements_hyphens.txt", - want: requirementsHyphens, + name: "happy path with hyphens", + filePath: "testdata/requirements_hyphens.txt", + want: requirementsHyphens, }, { - file: "testdata/requirement_exstras.txt", - want: requirementsExtras, + name: "happy path with exstras", + filePath: "testdata/requirement_exstras.txt", + want: requirementsExtras, }, { - file: "testdata/requirements_utf16le.txt", - want: requirementsUtf16le, + name: "happy path. File uses utf16le", + filePath: "testdata/requirements_utf16le.txt", + want: requirementsUtf16le, + }, + { + name: "happy path with templating engine", + filePath: "testdata/requirements_with_templating_engine.txt", + want: nil, }, } - for _, v := range vectors { - t.Run(path.Base(v.file), func(t *testing.T) { - f, err := os.Open(v.file) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := os.Open(tt.filePath) require.NoError(t, err) got, _, err := NewParser().Parse(f) require.NoError(t, err) - assert.Equal(t, v.want, got) + assert.Equal(t, tt.want, got) }) } } diff --git a/pkg/dependency/parser/python/pip/testdata/requirements_with_templating_engine.txt b/pkg/dependency/parser/python/pip/testdata/requirements_with_templating_engine.txt new file mode 100644 index 000000000000..c121e2517d9f --- /dev/null +++ b/pkg/dependency/parser/python/pip/testdata/requirements_with_templating_engine.txt @@ -0,0 +1,5 @@ +%ifcookiecutter.command_line_interface|lower=='click'-%} +foo|bar=='1.2.4' +foo{bar}==%% +foo,bar&&foobar==1.2.3 +foo=='invalid-version' \ No newline at end of file