diff --git a/lib/analyzer/package-managers/rpm.ts b/lib/analyzer/package-managers/rpm.ts index 2d1e00ad4..db177647d 100644 --- a/lib/analyzer/package-managers/rpm.ts +++ b/lib/analyzer/package-managers/rpm.ts @@ -60,7 +60,7 @@ function purl( qualifiers.repositories = repos.join(","); } - if (pkg.epoch) { + if (pkg.epoch !== undefined && pkg.epoch !== null) { qualifiers.epoch = String(pkg.epoch); } diff --git a/test/lib/analyzer/package-managers/rpm.spec.ts b/test/lib/analyzer/package-managers/rpm.spec.ts index f28627624..df7a96019 100644 --- a/test/lib/analyzer/package-managers/rpm.spec.ts +++ b/test/lib/analyzer/package-managers/rpm.spec.ts @@ -1,8 +1,216 @@ +import { PackageInfo } from "@snyk/rpm-parser/lib/rpm/types"; import * as fs from "fs"; import * as path from "path"; -import { parseSourceRPM } from "../../../../lib/analyzer/package-managers/rpm"; +import { + analyze, + mapRpmSqlitePackages, + parseSourceRPM, +} from "../../../../lib/analyzer/package-managers/rpm"; import { SourcePackage } from "../../../../lib/analyzer/types"; +describe("RPM Package Version and Epoch Handling", () => { + describe("formats version without epoch", () => { + it("should format version string correctly", () => { + const mappedResults = mapRpmSqlitePackages( + "test-image", + [ + { + name: "bash", + version: "5.1.16", + release: "4.el9", + size: 1024, + }, + ], + [], + ); + + expect(mappedResults.Analysis).toHaveLength(1); + expect(mappedResults.Analysis[0].Name).toBe("bash"); + expect(mappedResults.Analysis[0].Version).toBe("5.1.16-4.el9"); + expect(mappedResults.Analysis[0].Purl).toBe("pkg:rpm/bash@5.1.16-4.el9"); + }); + }); + + describe("formats version with epoch == 0", () => { + it("should include epoch=0 in PURL qualifier (bug fix)", () => { + const mappedResults = mapRpmSqlitePackages( + "test-image", + [ + { + name: "libarchive", + version: "3.7.7", + release: "4.el10_0", + epoch: 0, + size: 2048, + }, + ], + [], + ); + + expect(mappedResults.Analysis).toHaveLength(1); + expect(mappedResults.Analysis[0].Name).toBe("libarchive"); + expect(mappedResults.Analysis[0].Version).toBe("3.7.7-4.el10_0"); + // Critical: epoch=0 must be explicitly included in PURL + expect(mappedResults.Analysis[0].Purl).toBe( + "pkg:rpm/libarchive@3.7.7-4.el10_0?epoch=0", + ); + }); + + it("should include epoch=0 with distro qualifier", () => { + const mappedResults = mapRpmSqlitePackages( + "test-image", + [ + { + name: "sqlite-libs", + version: "3.34.1", + release: "5.el8", + epoch: 0, + size: 1500, + }, + ], + [], + { name: "rhel", version: "8.5" }, + ); + + expect(mappedResults.Analysis[0].Purl).toBe( + "pkg:rpm/rhel/sqlite-libs@3.34.1-5.el8?distro=rhel-8.5&epoch=0", + ); + }); + }); + + describe("formats version with non-zero epoch", () => { + it("should format version with epoch=1", () => { + const mappedResults = mapRpmSqlitePackages( + "test-image", + [ + { + name: "findutils", + version: "4.5.11", + release: "6.amzn2", + epoch: 1, + size: 3000, + }, + ], + [], + ); + + expect(mappedResults.Analysis[0].Name).toBe("findutils"); + expect(mappedResults.Analysis[0].Version).toBe("1:4.5.11-6.amzn2"); + expect(mappedResults.Analysis[0].Purl).toBe( + "pkg:rpm/findutils@1:4.5.11-6.amzn2?epoch=1", + ); + }); + }); + + describe("epoch consistency across functions", () => { + it("should handle epoch=0 consistently in analyze() function", async () => { + const pkgs: PackageInfo[] = [ + { + name: "openssl-libs", + version: "1.1.1", + release: "15.el8", + epoch: 0, + size: 6000, + }, + ]; + + const result = await analyze("test-image", pkgs, [], { + name: "rhel", + version: "8.2", + }); + + expect(result.Analysis[0].Purl).toBe( + "pkg:rpm/rhel/openssl-libs@1.1.1-15.el8?distro=rhel-8.2&epoch=0", + ); + }); + }); + + describe("multiple packages with different epochs", () => { + it("should correctly handle mixed epoch values", () => { + const packages: PackageInfo[] = [ + { + name: "pkg-no-epoch", + version: "1.0.0", + release: "1", + size: 100, + }, + { + name: "pkg-epoch-zero", + version: "2.0.0", + release: "1", + epoch: 0, + size: 200, + }, + { + name: "pkg-epoch-one", + version: "3.0.0", + release: "1", + epoch: 1, + size: 300, + }, + ]; + + const result = mapRpmSqlitePackages("test-image", packages, []); + + expect(result.Analysis[0].Purl).toBe("pkg:rpm/pkg-no-epoch@1.0.0-1"); + expect(result.Analysis[1].Purl).toBe( + "pkg:rpm/pkg-epoch-zero@2.0.0-1?epoch=0", + ); + expect(result.Analysis[2].Purl).toBe( + "pkg:rpm/pkg-epoch-one@1:3.0.0-1?epoch=1", + ); + }); + }); + + describe("epoch with repositories and modules", () => { + it("should include epoch with other qualifiers", () => { + const result = mapRpmSqlitePackages( + "test-image", + [ + { + name: "nodejs", + version: "10.21.0", + release: "3.module+el8.2.0", + epoch: 1, + module: "nodejs:10", + size: 7000, + }, + ], + ["rhel-8-appstream"], + { name: "rhel", version: "8.2" }, + ); + + const purl = result.Analysis[0].Purl; + expect(purl).toContain("?"); + expect(purl).toContain("epoch=1"); + expect(purl).toContain("module=nodejs%3A10"); // : is URL-encoded as %3A + expect(purl).toContain("repositories=rhel-8-appstream"); + expect(purl).toContain("distro=rhel-8.2"); + }); + + it("should include epoch=0 with sourceRPM upstream qualifier", () => { + const result = mapRpmSqlitePackages( + "test-image", + [ + { + name: "libxml2", + version: "2.9.7", + release: "14.el8", + epoch: 0, + sourceRPM: "libxml2-2.9.7-14.el8.src.rpm", + size: 8000, + }, + ], + [], + ); + + const purl = result.Analysis[0].Purl; + expect(purl).toContain("epoch=0"); + expect(purl).toContain("upstream=libxml2%402.9.7"); // @ is URL-encoded as %40 + }); + }); +}); + describe("parseSourceRPM", () => { it("should correctly parse all valid source RPM strings from source_rpms.csv", () => { const csvFilePath = path.join( diff --git a/test/system/operating-systems/__snapshots__/centos6.spec.ts.snap b/test/system/operating-systems/__snapshots__/centos6.spec.ts.snap index 524ca11cf..661d9618b 100644 --- a/test/system/operating-systems/__snapshots__/centos6.spec.ts.snap +++ b/test/system/operating-systems/__snapshots__/centos6.spec.ts.snap @@ -2662,7 +2662,7 @@ Object { "id": "perl-CGI@3.51-144.el6", "info": Object { "name": "perl-CGI", - "purl": "pkg:rpm/centos/perl-CGI@3.51-144.el6?distro=centos-6&upstream=perl%405.10.1", + "purl": "pkg:rpm/centos/perl-CGI@3.51-144.el6?distro=centos-6&epoch=0&upstream=perl%405.10.1", "version": "3.51-144.el6", }, }, @@ -2670,7 +2670,7 @@ Object { "id": "perl-ExtUtils-MakeMaker@6.55-144.el6", "info": Object { "name": "perl-ExtUtils-MakeMaker", - "purl": "pkg:rpm/centos/perl-ExtUtils-MakeMaker@6.55-144.el6?distro=centos-6&upstream=perl%405.10.1", + "purl": "pkg:rpm/centos/perl-ExtUtils-MakeMaker@6.55-144.el6?distro=centos-6&epoch=0&upstream=perl%405.10.1", "version": "6.55-144.el6", }, }, @@ -2710,7 +2710,7 @@ Object { "id": "perl-Test-Harness@3.17-144.el6", "info": Object { "name": "perl-Test-Harness", - "purl": "pkg:rpm/centos/perl-Test-Harness@3.17-144.el6?distro=centos-6&upstream=perl%405.10.1", + "purl": "pkg:rpm/centos/perl-Test-Harness@3.17-144.el6?distro=centos-6&epoch=0&upstream=perl%405.10.1", "version": "3.17-144.el6", }, }, @@ -2718,7 +2718,7 @@ Object { "id": "perl-Test-Simple@0.92-144.el6", "info": Object { "name": "perl-Test-Simple", - "purl": "pkg:rpm/centos/perl-Test-Simple@0.92-144.el6?distro=centos-6&upstream=perl%405.10.1", + "purl": "pkg:rpm/centos/perl-Test-Simple@0.92-144.el6?distro=centos-6&epoch=0&upstream=perl%405.10.1", "version": "0.92-144.el6", }, }, @@ -5902,7 +5902,7 @@ Object { "id": "perl-CGI@3.51-144.el6", "info": Object { "name": "perl-CGI", - "purl": "pkg:rpm/centos/perl-CGI@3.51-144.el6?distro=centos-6&upstream=perl%405.10.1", + "purl": "pkg:rpm/centos/perl-CGI@3.51-144.el6?distro=centos-6&epoch=0&upstream=perl%405.10.1", "version": "3.51-144.el6", }, }, @@ -5910,7 +5910,7 @@ Object { "id": "perl-ExtUtils-MakeMaker@6.55-144.el6", "info": Object { "name": "perl-ExtUtils-MakeMaker", - "purl": "pkg:rpm/centos/perl-ExtUtils-MakeMaker@6.55-144.el6?distro=centos-6&upstream=perl%405.10.1", + "purl": "pkg:rpm/centos/perl-ExtUtils-MakeMaker@6.55-144.el6?distro=centos-6&epoch=0&upstream=perl%405.10.1", "version": "6.55-144.el6", }, }, @@ -5950,7 +5950,7 @@ Object { "id": "perl-Test-Harness@3.17-144.el6", "info": Object { "name": "perl-Test-Harness", - "purl": "pkg:rpm/centos/perl-Test-Harness@3.17-144.el6?distro=centos-6&upstream=perl%405.10.1", + "purl": "pkg:rpm/centos/perl-Test-Harness@3.17-144.el6?distro=centos-6&epoch=0&upstream=perl%405.10.1", "version": "3.17-144.el6", }, }, @@ -5958,7 +5958,7 @@ Object { "id": "perl-Test-Simple@0.92-144.el6", "info": Object { "name": "perl-Test-Simple", - "purl": "pkg:rpm/centos/perl-Test-Simple@0.92-144.el6?distro=centos-6&upstream=perl%405.10.1", + "purl": "pkg:rpm/centos/perl-Test-Simple@0.92-144.el6?distro=centos-6&epoch=0&upstream=perl%405.10.1", "version": "0.92-144.el6", }, }, diff --git a/test/system/operating-systems/__snapshots__/ubi8.spec.ts.snap b/test/system/operating-systems/__snapshots__/ubi8.spec.ts.snap index 8e5d371df..6b0cfec4f 100644 --- a/test/system/operating-systems/__snapshots__/ubi8.spec.ts.snap +++ b/test/system/operating-systems/__snapshots__/ubi8.spec.ts.snap @@ -5110,7 +5110,7 @@ Object { "id": "perl-Errno@1.28-416.el8", "info": Object { "name": "perl-Errno", - "purl": "pkg:rpm/rhel/perl-Errno@1.28-416.el8?distro=rhel-8.2&repositories=rhel-8-for-x86_64-baseos-rpms%2Crhel-8-for-x86_64-appstream-rpms%2Crhel-8-for-x86_64-baseos-beta-rpms%2Crhel-8-for-x86_64-appstream-beta-rpms%2Crhel-8-for-x86_64-baseos-htb-rpms%2Crhel-8-for-x86_64-appstream-htb-rpms&upstream=perl%405.26.3", + "purl": "pkg:rpm/rhel/perl-Errno@1.28-416.el8?distro=rhel-8.2&epoch=0&repositories=rhel-8-for-x86_64-baseos-rpms%2Crhel-8-for-x86_64-appstream-rpms%2Crhel-8-for-x86_64-baseos-beta-rpms%2Crhel-8-for-x86_64-appstream-beta-rpms%2Crhel-8-for-x86_64-baseos-htb-rpms%2Crhel-8-for-x86_64-appstream-htb-rpms&upstream=perl%405.26.3", "version": "1.28-416.el8", }, }, @@ -5174,7 +5174,7 @@ Object { "id": "perl-IO@1.38-416.el8", "info": Object { "name": "perl-IO", - "purl": "pkg:rpm/rhel/perl-IO@1.38-416.el8?distro=rhel-8.2&repositories=rhel-8-for-x86_64-baseos-rpms%2Crhel-8-for-x86_64-appstream-rpms%2Crhel-8-for-x86_64-baseos-beta-rpms%2Crhel-8-for-x86_64-appstream-beta-rpms%2Crhel-8-for-x86_64-baseos-htb-rpms%2Crhel-8-for-x86_64-appstream-htb-rpms&upstream=perl%405.26.3", + "purl": "pkg:rpm/rhel/perl-IO@1.38-416.el8?distro=rhel-8.2&epoch=0&repositories=rhel-8-for-x86_64-baseos-rpms%2Crhel-8-for-x86_64-appstream-rpms%2Crhel-8-for-x86_64-baseos-beta-rpms%2Crhel-8-for-x86_64-appstream-beta-rpms%2Crhel-8-for-x86_64-baseos-htb-rpms%2Crhel-8-for-x86_64-appstream-htb-rpms&upstream=perl%405.26.3", "version": "1.38-416.el8", }, },