mirror of
https://github.com/anchore/syft.git
synced 2026-02-12 02:26:42 +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] starlette @ 0.37.2 (.)",
|
||||
"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 (.)",
|
||||
"orjson @ 3.10.3 (.) [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 {
|
||||
cleanExtra := strings.TrimSpace(extra)
|
||||
cleanName := strings.TrimSpace(name)
|
||||
// normalize both package name and extra to ensure case-insensitive matching per Python packaging spec
|
||||
// https://packaging.python.org/en/latest/specifications/name-normalization/
|
||||
cleanName := normalize(strings.TrimSpace(name))
|
||||
cleanExtra := normalize(strings.TrimSpace(extra))
|
||||
if cleanExtra == "" {
|
||||
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 {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@ -195,6 +219,38 @@ func Test_poetryLockDependencySpecifier_againstPoetryLock(t *testing.T) {
|
||||
fixture string
|
||||
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",
|
||||
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) {
|
||||
tests := []struct {
|
||||
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