From 9bd78a9c2850fedf91d651e715832f6662bbfc35 Mon Sep 17 00:00:00 2001 From: Sueun Cho Date: Tue, 11 Aug 2026 13:13:31 +0900 Subject: [PATCH 1/2] fix(rpm): keep the epoch when parsing RPM manifest packages newMetadataFromManifestLine decoded the epoch (EPOCHNUM) and the size into the same converted variable and set epoch = &converted, then reassigned converted to the size. The returned epoch pointer therefore held the size whenever a package had a real (non-"(none)") epoch, producing a wrong version and PURL. Epoch is highest-precedence in RPM version comparison, so this breaks advisory matching downstream. Bind the epoch to its own variable and add a fixture line with a non-"(none)" epoch to cover the branch (every existing fixture line used "(none)"). Signed-off-by: Sueun Cho --- syft/pkg/cataloger/redhat/package.go | 10 +++------- .../cataloger/redhat/parse_rpm_manifest_test.go | 17 +++++++++++++++++ .../redhat/testdata/container-manifest-2 | 1 + 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/syft/pkg/cataloger/redhat/package.go b/syft/pkg/cataloger/redhat/package.go index fc2da4fd1c8..bbbad1ac380 100644 --- a/syft/pkg/cataloger/redhat/package.go +++ b/syft/pkg/cataloger/redhat/package.go @@ -59,17 +59,13 @@ func newMetadataFromManifestLine(entry string) (*pkg.RpmDBEntry, error) { version := versionParts[0] release := versionParts[1] - converted, err := strconv.Atoi(parts[8]) var epoch *int - if err != nil || parts[5] == "(none)" { - epoch = nil - } else { - epoch = &converted + if epochNum, err := strconv.Atoi(parts[8]); err == nil && parts[5] != "(none)" { + epoch = &epochNum } - converted, err = strconv.Atoi(parts[6]) var size int - if err == nil { + if converted, err := strconv.Atoi(parts[6]); err == nil { size = converted } return &pkg.RpmDBEntry{ diff --git a/syft/pkg/cataloger/redhat/parse_rpm_manifest_test.go b/syft/pkg/cataloger/redhat/parse_rpm_manifest_test.go index fdc80dce5c4..b88aed5c48d 100644 --- a/syft/pkg/cataloger/redhat/parse_rpm_manifest_test.go +++ b/syft/pkg/cataloger/redhat/parse_rpm_manifest_test.go @@ -80,6 +80,23 @@ func TestParseRpmManifest(t *testing.T) { Vendor: "Microsoft Corporation", }, }, + { + Name: "vim", + Version: "2:9.0-1.cm2", + PURL: "pkg:rpm/vim@9.0-1.cm2?arch=x86_64&epoch=2&upstream=vim-9.0-1.cm2.src.rpm", + Locations: file.NewLocationSet(location), + Type: pkg.RpmPkg, + Metadata: pkg.RpmDBEntry{ + Name: "vim", + Epoch: intRef(2), + Arch: "x86_64", + Release: "1.cm2", + Version: "9.0", + SourceRpm: "vim-9.0-1.cm2.src.rpm", + Size: 45000, + Vendor: "Microsoft Corporation", + }, + }, } pkgtest.NewCatalogTester(). diff --git a/syft/pkg/cataloger/redhat/testdata/container-manifest-2 b/syft/pkg/cataloger/redhat/testdata/container-manifest-2 index 1b934cd2c6c..5a95aedaa6d 100644 --- a/syft/pkg/cataloger/redhat/testdata/container-manifest-2 +++ b/syft/pkg/cataloger/redhat/testdata/container-manifest-2 @@ -2,4 +2,5 @@ mariner-release 2.0-12.cm2 1653816591 1653753130 Microsoft Corporation (none) 58 filesystem 1.1-9.cm2 1653816591 1653628924 Microsoft Corporation (none) 7596 x86_64 0 filesystem-1.1-9.cm2.src.rpm glibc 2.35-2.cm2 1653816591 1653628955 Microsoft Corporation (none) 10855265 x86_64 0 glibc-2.35-2.cm2.src.rpm openssl-libs 1.1.1k-15.cm2 1653816591 1653631609 Microsoft Corporation (none) 4365048 x86_64 0 openssl-1.1.1k-15.cm2.src.rpm +vim 9.0-1.cm2 1653816591 1653753130 Microsoft Corporation 2 45000 x86_64 2 vim-9.0-1.cm2.src.rpm From 02db8fb6dfde0f5f1df5aa4aacff363e416db956 Mon Sep 17 00:00:00 2001 From: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> Date: Fri, 21 Aug 2026 14:27:19 -0400 Subject: [PATCH 2/2] refactor(rpm): reuse parseEpoch for manifest entries and cover epoch/size edge cases Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> --- syft/pkg/cataloger/redhat/package.go | 5 +- .../redhat/parse_rpm_manifest_test.go | 75 +++++++++++++++++++ 2 files changed, 78 insertions(+), 2 deletions(-) diff --git a/syft/pkg/cataloger/redhat/package.go b/syft/pkg/cataloger/redhat/package.go index bbbad1ac380..1fcbeaf1b49 100644 --- a/syft/pkg/cataloger/redhat/package.go +++ b/syft/pkg/cataloger/redhat/package.go @@ -59,9 +59,10 @@ func newMetadataFromManifestLine(entry string) (*pkg.RpmDBEntry, error) { version := versionParts[0] release := versionParts[1] + // EPOCH is "(none)" when the package has no epoch; EPOCHNUM is the numeric form ("0" in that case) var epoch *int - if epochNum, err := strconv.Atoi(parts[8]); err == nil && parts[5] != "(none)" { - epoch = &epochNum + if parts[5] != "(none)" { + epoch = parseEpoch(parts[8]) } var size int diff --git a/syft/pkg/cataloger/redhat/parse_rpm_manifest_test.go b/syft/pkg/cataloger/redhat/parse_rpm_manifest_test.go index b88aed5c48d..5c1f2004fb6 100644 --- a/syft/pkg/cataloger/redhat/parse_rpm_manifest_test.go +++ b/syft/pkg/cataloger/redhat/parse_rpm_manifest_test.go @@ -1,8 +1,12 @@ package redhat import ( + "strings" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest" @@ -105,3 +109,74 @@ func TestParseRpmManifest(t *testing.T) { TestParser(t, parseRpmManifest) } + +func TestNewMetadataFromManifestLine(t *testing.T) { + // fields are: NAME, VERSION-RELEASE, INSTALLTIME, BUILDTIME, VENDOR, EPOCH, SIZE, ARCH, EPOCHNUM, SOURCERPM + line := func(epoch, size, epochNum string) string { + return strings.Join([]string{ + "vim", "9.0-1.cm2", "1653816591", "1653753130", "Microsoft Corporation", + epoch, size, "x86_64", epochNum, "vim-9.0-1.cm2.src.rpm", + }, "\t") + } + + tests := []struct { + name string + entry string + wantEpoch *int + wantSize int + wantErr require.ErrorAssertionFunc + }{ + { + name: "no epoch", + entry: line("(none)", "45000", "0"), + wantEpoch: nil, + wantSize: 45000, + }, + { + name: "explicit zero epoch is distinct from no epoch", + entry: line("0", "45000", "0"), + wantEpoch: intRef(0), + wantSize: 45000, + }, + { + name: "unparsable epochnum yields no epoch", + entry: line("2", "45000", ""), + wantEpoch: nil, + wantSize: 45000, + }, + { + name: "unparsable size yields zero size", + entry: line("2", "bogus", "2"), + wantEpoch: intRef(2), + wantSize: 0, + }, + { + name: "too few fields", + entry: "vim\t9.0-1.cm2", + wantErr: require.Error, + }, + { + name: "version field missing the release", + entry: strings.Replace(line("(none)", "45000", "0"), "9.0-1.cm2", "9.0", 1), + wantErr: require.Error, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.wantErr == nil { + tt.wantErr = require.NoError + } + + metadata, err := newMetadataFromManifestLine(tt.entry) + tt.wantErr(t, err) + if err != nil { + return + } + + require.NotNil(t, metadata) + assert.Equal(t, tt.wantEpoch, metadata.Epoch) + assert.Equal(t, tt.wantSize, metadata.Size) + }) + } +}