mirror of
https://github.com/anchore/syft.git
synced 2026-02-12 10:36:45 +01:00
fix: update mixed case dependencies in python to be normalized (#4573)
--------- Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>
This commit is contained in:
parent
e8b4527bfb
commit
9a250a4b4b
@ -635,6 +635,7 @@ func Test_PackageCataloger_Relationships(t *testing.T) {
|
|||||||
"jinja2 @ 3.1.4 (.) [dependency-of] fastapi @ 0.111.0 (.)",
|
"jinja2 @ 3.1.4 (.) [dependency-of] fastapi @ 0.111.0 (.)",
|
||||||
"jinja2 @ 3.1.4 (.) [dependency-of] starlette @ 0.37.2 (.)",
|
"jinja2 @ 3.1.4 (.) [dependency-of] starlette @ 0.37.2 (.)",
|
||||||
"markdown-it-py @ 3.0.0 (.) [dependency-of] rich @ 13.7.1 (.)",
|
"markdown-it-py @ 3.0.0 (.) [dependency-of] rich @ 13.7.1 (.)",
|
||||||
|
"markupsafe @ 2.1.5 (.) [dependency-of] jinja2 @ 3.1.4 (.)", // MarkupSafe (mixed case) -> markupsafe
|
||||||
"mdurl @ 0.1.2 (.) [dependency-of] markdown-it-py @ 3.0.0 (.)",
|
"mdurl @ 0.1.2 (.) [dependency-of] markdown-it-py @ 3.0.0 (.)",
|
||||||
"orjson @ 3.10.3 (.) [dependency-of] fastapi @ 0.111.0 (.)",
|
"orjson @ 3.10.3 (.) [dependency-of] fastapi @ 0.111.0 (.)",
|
||||||
"pydantic @ 2.7.1 (.) [dependency-of] fastapi @ 0.111.0 (.)",
|
"pydantic @ 2.7.1 (.) [dependency-of] fastapi @ 0.111.0 (.)",
|
||||||
|
|||||||
@ -74,8 +74,10 @@ func isDependencyForExtra(dep pkg.PythonPoetryLockDependencyEntry) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func packageRef(name, extra string) string {
|
func packageRef(name, extra string) string {
|
||||||
cleanExtra := strings.TrimSpace(extra)
|
// normalize both package name and extra to ensure case-insensitive matching per Python packaging spec
|
||||||
cleanName := strings.TrimSpace(name)
|
// https://packaging.python.org/en/latest/specifications/name-normalization/
|
||||||
|
cleanName := normalize(strings.TrimSpace(name))
|
||||||
|
cleanExtra := normalize(strings.TrimSpace(extra))
|
||||||
if cleanExtra == "" {
|
if cleanExtra == "" {
|
||||||
return cleanName
|
return cleanName
|
||||||
}
|
}
|
||||||
|
|||||||
@ -181,6 +181,30 @@ func Test_poetryLockDependencySpecifier(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "dependency names with mixed case should be normalized",
|
||||||
|
p: pkg.Package{
|
||||||
|
Name: "dj-rest-auth",
|
||||||
|
Metadata: pkg.PythonPoetryLockEntry{
|
||||||
|
Dependencies: []pkg.PythonPoetryLockDependencyEntry{
|
||||||
|
{
|
||||||
|
Name: "Django", // note: capital D
|
||||||
|
Version: ">=4.2,<6.0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "djangorestframework",
|
||||||
|
Version: ">=3.13.0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: dependency.Specification{
|
||||||
|
ProvidesRequires: dependency.ProvidesRequires{
|
||||||
|
Provides: []string{"dj-rest-auth"},
|
||||||
|
Requires: []string{"django", "djangorestframework"}, // "Django" should be normalized to "django"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
@ -195,6 +219,38 @@ func Test_poetryLockDependencySpecifier_againstPoetryLock(t *testing.T) {
|
|||||||
fixture string
|
fixture string
|
||||||
want []dependency.Specification
|
want []dependency.Specification
|
||||||
}{
|
}{
|
||||||
|
{
|
||||||
|
name: "case-insensitive dependency resolution",
|
||||||
|
fixture: "test-fixtures/poetry/case-sensitivity/poetry.lock",
|
||||||
|
want: []dependency.Specification{
|
||||||
|
// packages are in the order they appear in the lock file
|
||||||
|
{
|
||||||
|
ProvidesRequires: dependency.ProvidesRequires{
|
||||||
|
Provides: []string{"django"},
|
||||||
|
Requires: []string{"asgiref", "sqlparse"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ProvidesRequires: dependency.ProvidesRequires{
|
||||||
|
Provides: []string{"djangorestframework"},
|
||||||
|
Requires: []string{"django"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// dj-rest-auth depends on Django (capital D) which should resolve to django
|
||||||
|
ProvidesRequires: dependency.ProvidesRequires{
|
||||||
|
Provides: []string{"dj-rest-auth"},
|
||||||
|
Requires: []string{"django", "djangorestframework"}, // Django normalized to django
|
||||||
|
},
|
||||||
|
Variants: []dependency.ProvidesRequires{
|
||||||
|
{
|
||||||
|
Provides: []string{"dj-rest-auth[with-social]"},
|
||||||
|
Requires: []string{"django-allauth"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "simple dependencies with extras",
|
name: "simple dependencies with extras",
|
||||||
fixture: "test-fixtures/poetry/simple-deps/poetry.lock",
|
fixture: "test-fixtures/poetry/simple-deps/poetry.lock",
|
||||||
@ -276,6 +332,64 @@ func Test_poetryLockDependencySpecifier_againstPoetryLock(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Test_packageRef verifies that package references are normalized according to
|
||||||
|
// the Python Packaging specification for names and extras:
|
||||||
|
// https://packaging.python.org/en/latest/specifications/name-normalization/
|
||||||
|
func Test_packageRef(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
pkg string
|
||||||
|
extra string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "simple package name",
|
||||||
|
pkg: "requests",
|
||||||
|
want: "requests",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "package with extra",
|
||||||
|
pkg: "requests",
|
||||||
|
extra: "security",
|
||||||
|
want: "requests[security]",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "package name with mixed case",
|
||||||
|
pkg: "Django",
|
||||||
|
want: "django",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "package name with underscores",
|
||||||
|
pkg: "some_package",
|
||||||
|
want: "some-package",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "package name with mixed case and extra",
|
||||||
|
pkg: "Django",
|
||||||
|
extra: "argon2",
|
||||||
|
want: "django[argon2]",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "extra with mixed case",
|
||||||
|
pkg: "package",
|
||||||
|
extra: "Security",
|
||||||
|
want: "package[security]",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "both with mixed case and separators",
|
||||||
|
pkg: "Some_Package",
|
||||||
|
extra: "Dev_Extra",
|
||||||
|
want: "some-package[dev-extra]",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := packageRef(tt.pkg, tt.extra)
|
||||||
|
assert.Equal(t, tt.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func Test_extractPackageName(t *testing.T) {
|
func Test_extractPackageName(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|||||||
55
syft/pkg/cataloger/python/test-fixtures/poetry/case-sensitivity/poetry.lock
generated
Normal file
55
syft/pkg/cataloger/python/test-fixtures/poetry/case-sensitivity/poetry.lock
generated
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
# This file is automatically @generated by Poetry and should not be edited manually.
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "django"
|
||||||
|
version = "5.2.6"
|
||||||
|
description = "A high-level Python web framework"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.10"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "Django-5.2.6-py3-none-any.whl", hash = "sha256:example1"},
|
||||||
|
{file = "django-5.2.6.tar.gz", hash = "sha256:example2"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
asgiref = ">=3.8.1,<4"
|
||||||
|
sqlparse = ">=0.3.1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "djangorestframework"
|
||||||
|
version = "3.16.1"
|
||||||
|
description = "Web APIs for Django, made easy."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.9"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "djangorestframework-3.16.1-py3-none-any.whl", hash = "sha256:example3"},
|
||||||
|
{file = "djangorestframework-3.16.1.tar.gz", hash = "sha256:example4"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
django = ">=4.2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dj-rest-auth"
|
||||||
|
version = "7.0.1"
|
||||||
|
description = "Authentication and Registration in Django Rest Framework"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "dj-rest-auth-7.0.1.tar.gz", hash = "sha256:3f8c744cbcf05355ff4bcbef0c8a63645da38e29a0fdef3c3332d4aced52fb90"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
Django = ">=4.2,<6.0"
|
||||||
|
djangorestframework = ">=3.13.0"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
with-social = ["django-allauth[socialaccount] (>=64.0.0)"]
|
||||||
|
|
||||||
|
[metadata]
|
||||||
|
lock-version = "2.1"
|
||||||
|
python-versions = "^3.10"
|
||||||
|
content-hash = "example"
|
||||||
Loading…
x
Reference in New Issue
Block a user