From 63232bf7255e289d71a9912f2101aa472c6cff9b Mon Sep 17 00:00:00 2001 From: Keith Zantow Date: Mon, 8 Jun 2026 11:12:47 -0400 Subject: [PATCH] fix: local version identifiers in python requirements parsing (#4959) Signed-off-by: Keith Zantow --- .../cataloger/python/parse_requirements.go | 4 +- .../python/parse_requirements_test.go | 39 +++++++++++++++++++ .../python/testdata/requires/requirements.txt | 1 + 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/syft/pkg/cataloger/python/parse_requirements.go b/syft/pkg/cataloger/python/parse_requirements.go index dfaf01cf9..08d284c79 100644 --- a/syft/pkg/cataloger/python/parse_requirements.go +++ b/syft/pkg/cataloger/python/parse_requirements.go @@ -29,8 +29,8 @@ const ( // namePattern matches: requests[security] namePattern = `(?P\w[\w\[\],\s-_\.]+)` - // versionConstraintPattern matches: == 2.8.* - versionConstraintPattern = `(?P([^\S\r\n]*[~=>!<]+\s*[0-9a-zA-Z.*]+[^\S\r\n]*,?)+)?(@[^\S\r\n]*(?P[^;]*))?` + // versionConstraintPattern matches: == 2.8.* (including local version identifiers, e.g. == 1.2.3+gcr.2) + versionConstraintPattern = `(?P([^\S\r\n]*[~=>!<]+\s*[0-9a-zA-Z.*+]+[^\S\r\n]*,?)+)?(@[^\S\r\n]*(?P[^;]*))?` // markersPattern matches: python_version < "2.7" and sys_platform == "linux" markersPattern = `(;(?P.*))?` diff --git a/syft/pkg/cataloger/python/parse_requirements_test.go b/syft/pkg/cataloger/python/parse_requirements_test.go index 2b831ae33..8c9d4e54c 100644 --- a/syft/pkg/cataloger/python/parse_requirements_test.go +++ b/syft/pkg/cataloger/python/parse_requirements_test.go @@ -140,6 +140,18 @@ func TestParseRequirementsTxt(t *testing.T) { VersionConstraint: "== 1.0.0", }, }, + { + Name: "local-version", + Version: "1.2.3+gcr.2", + PURL: "pkg:pypi/local-version@1.2.3%2Bgcr.2", + Locations: locations, + Language: pkg.Python, + Type: pkg.PythonPkg, + Metadata: pkg.PythonRequirementsEntry{ + Name: "local-version", + VersionConstraint: "== 1.2.3+gcr.2", + }, + }, } var testCases = []struct { @@ -357,6 +369,23 @@ func Test_newRequirement(t *testing.T) { Markers: "sys_platform == 'win32'", }, }, + { + name: "local version identifier", + raw: "local-version == 1.2.3+gcr.2", + want: &unprocessedRequirement{ + Name: "local-version", + VersionConstraint: "== 1.2.3+gcr.2", + }, + }, + { + name: "local version identifier with markers", + raw: "local-version == 1.2.3+ubuntu1 ; sys_platform == 'linux'", + want: &unprocessedRequirement{ + Name: "local-version", + VersionConstraint: "== 1.2.3+ubuntu1", + Markers: "sys_platform == 'linux'", + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -388,6 +417,16 @@ func Test_parseVersion(t *testing.T) { version: " === 1.26.20 ", want: "1.26.20", }, + { + name: "local version identifier", + version: " == 1.2.3+gcr.2 ", + want: "1.2.3+gcr.2", + }, + { + name: "arbitrary equality with local version identifier", + version: " === 1.2.3+ubuntu1 ", + want: "1.2.3+ubuntu1", + }, { name: "resolve lowest, simple constraint", version: " >= 1.0.0 ", diff --git a/syft/pkg/cataloger/python/testdata/requires/requirements.txt b/syft/pkg/cataloger/python/testdata/requires/requirements.txt index 656a4e583..ed59a2f5a 100644 --- a/syft/pkg/cataloger/python/testdata/requires/requirements.txt +++ b/syft/pkg/cataloger/python/testdata/requires/requirements.txt @@ -25,3 +25,4 @@ celery[redis, pytest] == 4.4.7 # should remove [redis, pytest] requests[security] == 2.8.* ; python_version < "2.7" and sys_platform == "linux" GithubSampleProject == 3.7.1 @ git+https://github.com/owner/repo@releases/tag/v3.7.1 FrIeNdLy-_-bArD == 1.0.0 +local-version == 1.2.3+gcr.2