mirror of
https://github.com/aquasecurity/trivy.git
synced 2026-01-31 13:53:14 +08:00
feat(python): add pylock.toml (PEP 751) parser (#9632)
Co-authored-by: DmitriyLewen <dmitriy.lewen@smartforce.io>
This commit is contained in:
committed by
GitHub
parent
cc64eebbd0
commit
1a72b326bb
98
pkg/dependency/parser/python/pylock/parse.go
Normal file
98
pkg/dependency/parser/python/pylock/parse.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package pylock
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
"github.com/samber/lo"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/aquasecurity/trivy/pkg/dependency"
|
||||
"github.com/aquasecurity/trivy/pkg/dependency/parser/python"
|
||||
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
|
||||
xio "github.com/aquasecurity/trivy/pkg/x/io"
|
||||
)
|
||||
|
||||
type Pylock struct {
|
||||
Packages []Package `toml:"packages"`
|
||||
}
|
||||
|
||||
type Package struct {
|
||||
Name string `toml:"name"`
|
||||
Version string `toml:"version"`
|
||||
Dependencies []Dependency `toml:"dependencies"`
|
||||
}
|
||||
|
||||
type Dependency struct {
|
||||
Name string `toml:"name"`
|
||||
Version string `toml:"version"`
|
||||
}
|
||||
|
||||
// Parser parses pylock.toml defined in PEP 751.
|
||||
// https://peps.python.org/pep-0751
|
||||
type Parser struct{}
|
||||
|
||||
func NewParser() *Parser {
|
||||
return &Parser{}
|
||||
}
|
||||
|
||||
func (p *Parser) Parse(r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependency, error) {
|
||||
var lock Pylock
|
||||
if _, err := toml.NewDecoder(r).Decode(&lock); err != nil {
|
||||
return nil, nil, xerrors.Errorf("failed to decode pylock.toml: %w", err)
|
||||
}
|
||||
|
||||
pkgs := make(map[string]ftypes.Package)
|
||||
deps := make(map[string][]string)
|
||||
|
||||
for _, pkg := range lock.Packages {
|
||||
normalizedPkgName := python.NormalizePkgName(pkg.Name, true)
|
||||
pkgID := packageID(normalizedPkgName, pkg.Version)
|
||||
|
||||
pkgs[pkgID] = ftypes.Package{
|
||||
ID: pkgID,
|
||||
Name: normalizedPkgName,
|
||||
Version: pkg.Version,
|
||||
}
|
||||
|
||||
var dependsOn []string
|
||||
for _, dep := range pkg.Dependencies {
|
||||
depName := python.NormalizePkgName(dep.Name, true)
|
||||
depID := packageID(depName, dep.Version)
|
||||
dependsOn = append(dependsOn, depID)
|
||||
}
|
||||
if len(dependsOn) > 0 {
|
||||
sort.Strings(dependsOn)
|
||||
deps[pkgID] = dependsOn
|
||||
}
|
||||
}
|
||||
|
||||
depSlice := lo.MapToSlice(deps, func(pkgID string, dependsOn []string) ftypes.Dependency {
|
||||
if _, ok := pkgs[pkgID]; !ok {
|
||||
return ftypes.Dependency{}
|
||||
}
|
||||
|
||||
// Filter out dependencies that are not in the package list
|
||||
var dependsOnIDs []string
|
||||
for _, depID := range dependsOn {
|
||||
if _, ok := pkgs[depID]; ok {
|
||||
dependsOnIDs = append(dependsOnIDs, depID)
|
||||
}
|
||||
}
|
||||
|
||||
return ftypes.Dependency{
|
||||
ID: pkgID,
|
||||
DependsOn: dependsOnIDs,
|
||||
}
|
||||
})
|
||||
|
||||
pkgSlice := lo.Values(pkgs)
|
||||
sort.Sort(ftypes.Packages(pkgSlice))
|
||||
sort.Sort(ftypes.Dependencies(depSlice))
|
||||
|
||||
return pkgSlice, depSlice, nil
|
||||
}
|
||||
|
||||
func packageID(name, ver string) string {
|
||||
return dependency.ID(ftypes.PyLock, name, ver)
|
||||
}
|
||||
102
pkg/dependency/parser/python/pylock/parse_test.go
Normal file
102
pkg/dependency/parser/python/pylock/parse_test.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package pylock_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/aquasecurity/trivy/pkg/dependency/parser/python/pylock"
|
||||
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
|
||||
)
|
||||
|
||||
func TestParser_Parse(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
file string
|
||||
wantPkgs []ftypes.Package
|
||||
wantDeps []ftypes.Dependency
|
||||
wantErr assert.ErrorAssertionFunc
|
||||
}{
|
||||
{
|
||||
name: "happy path",
|
||||
file: "testdata/pylock.toml",
|
||||
wantPkgs: []ftypes.Package{
|
||||
{
|
||||
ID: "attrs@25.4.0",
|
||||
Name: "attrs",
|
||||
Version: "25.4.0",
|
||||
},
|
||||
{
|
||||
ID: "certifi@2025.10.5",
|
||||
Name: "certifi",
|
||||
Version: "2025.10.5",
|
||||
},
|
||||
{
|
||||
ID: "charset-normalizer@3.4.3",
|
||||
Name: "charset-normalizer",
|
||||
Version: "3.4.3",
|
||||
},
|
||||
{
|
||||
ID: "ham@3.0.0",
|
||||
Name: "ham",
|
||||
Version: "3.0.0",
|
||||
},
|
||||
{
|
||||
ID: "idna@3.10",
|
||||
Name: "idna",
|
||||
Version: "3.10",
|
||||
},
|
||||
{
|
||||
ID: "requests@2.32.5",
|
||||
Name: "requests",
|
||||
Version: "2.32.5",
|
||||
},
|
||||
{
|
||||
ID: "spam@1.0.0",
|
||||
Name: "spam",
|
||||
Version: "1.0.0",
|
||||
},
|
||||
{
|
||||
ID: "spam@1.1.0",
|
||||
Name: "spam",
|
||||
Version: "1.1.0",
|
||||
},
|
||||
{
|
||||
ID: "urllib3@2.5.0",
|
||||
Name: "urllib3",
|
||||
Version: "2.5.0",
|
||||
},
|
||||
},
|
||||
wantDeps: []ftypes.Dependency{
|
||||
{
|
||||
ID: "ham@3.0.0",
|
||||
DependsOn: []string{"spam@1.1.0"},
|
||||
},
|
||||
},
|
||||
wantErr: assert.NoError,
|
||||
},
|
||||
{
|
||||
name: "sad path",
|
||||
file: "testdata/sad.toml",
|
||||
wantErr: assert.Error,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
f, err := os.Open(tt.file)
|
||||
require.NoError(t, err)
|
||||
defer f.Close()
|
||||
|
||||
p := pylock.NewParser()
|
||||
gotPkgs, gotDeps, err := p.Parse(f)
|
||||
if !tt.wantErr(t, err, fmt.Sprintf("Parse(%v)", tt.file)) {
|
||||
return
|
||||
}
|
||||
assert.Equalf(t, tt.wantPkgs, gotPkgs, "Parse(%v)", tt.file)
|
||||
assert.Equalf(t, tt.wantDeps, gotDeps, "Parse(%v)", tt.file)
|
||||
})
|
||||
}
|
||||
}
|
||||
109
pkg/dependency/parser/python/pylock/testdata/pylock.toml
vendored
Normal file
109
pkg/dependency/parser/python/pylock/testdata/pylock.toml
vendored
Normal file
@@ -0,0 +1,109 @@
|
||||
lock-version = "1.0"
|
||||
created-by = "pip"
|
||||
|
||||
[[packages]]
|
||||
name = "attrs"
|
||||
version = "25.4.0"
|
||||
|
||||
[[packages.wheels]]
|
||||
name = "attrs-25.4.0-py3-none-any.whl"
|
||||
url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl"
|
||||
|
||||
[packages.wheels.hashes]
|
||||
sha256 = "adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373"
|
||||
|
||||
[[packages]]
|
||||
name = "certifi"
|
||||
version = "2025.10.5"
|
||||
|
||||
[[packages.wheels]]
|
||||
name = "certifi-2025.10.5-py3-none-any.whl"
|
||||
url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl"
|
||||
|
||||
[packages.wheels.hashes]
|
||||
sha256 = "0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de"
|
||||
|
||||
[[packages]]
|
||||
name = "charset-normalizer"
|
||||
version = "3.4.3"
|
||||
|
||||
[[packages.wheels]]
|
||||
name = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"
|
||||
url = "https://files.pythonhosted.org/packages/87/df/b7737ff046c974b183ea9aa111b74185ac8c3a326c6262d413bd5a1b8c69/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl"
|
||||
|
||||
[packages.wheels.hashes]
|
||||
sha256 = "0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07"
|
||||
|
||||
[[packages]]
|
||||
name = "idna"
|
||||
version = "3.10"
|
||||
|
||||
[[packages.wheels]]
|
||||
name = "idna-3.10-py3-none-any.whl"
|
||||
url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl"
|
||||
|
||||
[packages.wheels.hashes]
|
||||
sha256 = "946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"
|
||||
|
||||
[[packages]]
|
||||
name = "requests"
|
||||
version = "2.32.5"
|
||||
|
||||
[[packages.wheels]]
|
||||
name = "requests-2.32.5-py3-none-any.whl"
|
||||
url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl"
|
||||
|
||||
[packages.wheels.hashes]
|
||||
sha256 = "2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"
|
||||
|
||||
[[packages]]
|
||||
name = "urllib3"
|
||||
version = "2.5.0"
|
||||
|
||||
[[packages.wheels]]
|
||||
name = "urllib3-2.5.0-py3-none-any.whl"
|
||||
url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl"
|
||||
|
||||
[packages.wheels.hashes]
|
||||
sha256 = "e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"
|
||||
|
||||
# Packages with same name but different versions for different environments
|
||||
[[packages]]
|
||||
name = "spam"
|
||||
version = "1.0.0"
|
||||
marker = "sys_platform == 'linux'"
|
||||
|
||||
[[packages.wheels]]
|
||||
name = "spam-1.0.0-py3-none-any.whl"
|
||||
url = "https://example.invalid/files/spam-1.0.0-py3-none-any.whl"
|
||||
|
||||
[packages.wheels.hashes]
|
||||
sha256 = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"
|
||||
|
||||
[[packages]]
|
||||
name = "spam"
|
||||
version = "1.1.0"
|
||||
marker = "sys_platform == 'win32'"
|
||||
|
||||
[[packages.wheels]]
|
||||
name = "spam-1.1.0-py3-none-any.whl"
|
||||
url = "https://example.invalid/files/spam-1.1.0-py3-none-any.whl"
|
||||
|
||||
[packages.wheels.hashes]
|
||||
sha256 = "feebdaedfeebdaedfeebdaedfeebdaedfeebdaedfeebdaedfeebdaedfeebdaed"
|
||||
|
||||
[[packages]]
|
||||
name = "ham"
|
||||
version = "3.0.0"
|
||||
marker = "sys_platform == 'linux'"
|
||||
dependencies = [
|
||||
{ name = "spam", version = "1.0.0" }
|
||||
]
|
||||
|
||||
[[packages]]
|
||||
name = "ham"
|
||||
version = "3.0.0"
|
||||
marker = "sys_platform == 'win32'"
|
||||
dependencies = [
|
||||
{ name = "spam", version = "1.1.0" }
|
||||
]
|
||||
1
pkg/dependency/parser/python/pylock/testdata/sad.toml
vendored
Normal file
1
pkg/dependency/parser/python/pylock/testdata/sad.toml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
[
|
||||
@@ -214,6 +214,7 @@ const (
|
||||
PipfileLock = "Pipfile.lock"
|
||||
PoetryLock = "poetry.lock"
|
||||
UvLock = "uv.lock"
|
||||
PyLock = "pylock.toml"
|
||||
|
||||
GemfileLock = "Gemfile.lock"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user