mirror of
https://github.com/aquasecurity/trivy.git
synced 2025-12-05 20:40:16 -08:00
fix(python): add package name and version validation for requirements.txt files. (#6804)
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
5
pkg/dependency/parser/python/pip/testdata/requirements_with_templating_engine.txt
vendored
Normal file
5
pkg/dependency/parser/python/pip/testdata/requirements_with_templating_engine.txt
vendored
Normal file
@@ -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'
|
||||
Reference in New Issue
Block a user