Skip to content

Commit a9bfdc7

Browse files
authored
Merge pull request #660 from snyk/feat/CN-70-rpm-source
feat: added source package name and version to purl for rpm packages …
2 parents 09a3029 + 8552e27 commit a9bfdc7

18 files changed

Lines changed: 13728 additions & 2673 deletions

File tree

lib/analyzer/package-managers/rpm.ts

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
AnalyzedPackageWithVersion,
77
ImagePackagesAnalysis,
88
OSRelease,
9+
SourcePackage,
910
} from "../types";
1011

1112
export function analyze(
@@ -18,14 +19,15 @@ export function analyze(
1819
Image: targetImage,
1920
AnalyzeType: AnalysisType.Rpm,
2021
Analysis: pkgs.map((pkgInfo) => {
22+
const generatedPurl = purl(pkgInfo, repositories, osRelease);
2123
return {
2224
Name: pkgInfo.name,
2325
Version: formatRpmPackageVersion(pkgInfo),
2426
Source: undefined,
2527
Provides: [],
2628
Deps: {},
2729
AutoInstalled: undefined,
28-
Purl: purl(pkgInfo, repositories, osRelease),
30+
Purl: generatedPurl,
2931
};
3032
}),
3133
});
@@ -43,6 +45,17 @@ function purl(
4345
qualifiers.module = modName + ":" + modVersion;
4446
}
4547

48+
if (pkg.sourceRPM) {
49+
const sourcePackage = parseSourceRPM(pkg.sourceRPM);
50+
if (sourcePackage) {
51+
let upstream = sourcePackage.name;
52+
if (sourcePackage.version) {
53+
upstream += `@${sourcePackage.version}`;
54+
}
55+
qualifiers.upstream = upstream;
56+
}
57+
}
58+
4659
if (repos.length > 0) {
4760
qualifiers.repositories = repos.join(",");
4861
}
@@ -61,13 +74,59 @@ function purl(
6174
vendor,
6275
pkg.name,
6376
formatRpmPackageVersion(pkg),
64-
// make sure that we pass in undefined if there are no qualifiers, because
65-
// the packageurl-js library doesn't handle that properly...
6677
Object.keys(qualifiers).length !== 0 ? qualifiers : undefined,
6778
undefined,
6879
).toString();
6980
}
7081

82+
export function parseSourceRPM(
83+
sourceRPM: string | undefined,
84+
): SourcePackage | undefined {
85+
if (!sourceRPM || !sourceRPM.endsWith(".src.rpm")) {
86+
return undefined;
87+
}
88+
89+
const baseName = sourceRPM.substring(0, sourceRPM.length - ".src.rpm".length);
90+
91+
const lastHyphenIdx = baseName.lastIndexOf("-");
92+
// Ensure there's something after the last hyphen (release) and something before it (name-version)
93+
if (
94+
lastHyphenIdx === -1 ||
95+
lastHyphenIdx === 0 ||
96+
lastHyphenIdx === baseName.length - 1
97+
) {
98+
return undefined;
99+
}
100+
101+
const release = baseName.substring(lastHyphenIdx + 1);
102+
const nameVersionPart = baseName.substring(0, lastHyphenIdx);
103+
104+
const secondLastHyphenIdx = nameVersionPart.lastIndexOf("-");
105+
// Ensure there's something after the second-last hyphen (version) and something before it (name)
106+
if (
107+
secondLastHyphenIdx === -1 ||
108+
secondLastHyphenIdx === 0 ||
109+
secondLastHyphenIdx === nameVersionPart.length - 1
110+
) {
111+
return undefined;
112+
}
113+
114+
const version = nameVersionPart.substring(secondLastHyphenIdx + 1);
115+
const name = nameVersionPart.substring(0, secondLastHyphenIdx);
116+
117+
// Final check for empty parts, which could happen with malformed inputs
118+
// or if hyphens were at the very start/end of segments.
119+
if (!name || !version || !release) {
120+
return undefined;
121+
}
122+
123+
return {
124+
name,
125+
version,
126+
release,
127+
};
128+
}
129+
71130
export function mapRpmSqlitePackages(
72131
targetImage: string,
73132
rpmPackages: PackageInfo[],
@@ -78,14 +137,16 @@ export function mapRpmSqlitePackages(
78137

79138
if (rpmPackages) {
80139
analysis = rpmPackages.map((pkg) => {
140+
const generatedPurl = purl(pkg, repositories, osRelease);
141+
81142
return {
82143
Name: pkg.name,
83144
Version: formatRpmPackageVersion(pkg),
84145
Source: undefined,
85146
Provides: [],
86147
Deps: {},
87148
AutoInstalled: undefined,
88-
Purl: purl(pkg, repositories, osRelease),
149+
Purl: generatedPurl,
89150
};
90151
});
91152
}

lib/analyzer/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,3 +108,9 @@ export interface DestinationDir {
108108
name: string;
109109
removeCallback: () => void;
110110
}
111+
112+
export interface SourcePackage {
113+
name: string;
114+
version: string;
115+
release: string;
116+
}

lib/sub-process.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ function execute(
1212
args: string[],
1313
options?,
1414
): Promise<CmdOutput> {
15-
const spawnOptions: any = { shell: true, env: { ...process.env } };
15+
const spawnOptions: any = {
16+
shell: process.platform !== "win32" ? "/bin/bash" : true,
17+
env: { ...process.env },
18+
};
1619
if (options && options.cwd) {
1720
spawnOptions.cwd = options.cwd;
1821
}

test/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@ To run system tests the following environment variables need to be set:
1010

1111
This is an image that is hosted on Docker Hub but not available publicly.
1212
Note that the variables above are used purely for system test and are unrelated to the `SNYK_REGISTRY_USERNAME
13-
` / `SNYK_REGISTRY_PASSWORD` environment variables which may be present at runtime.
13+
` / `SNYK_REGISTRY_PASSWORD` environment variables which need to be present at runtime and can be found in 1password.
1414

1515
If tests should fail because that config hasn't been done, some artifacts will be left over/my_custom/image/save/path
1616
/auth/{someGuid}. This needs to be manually deleted before subsequent runs to avoid more failures.
1717

18-
Additionally, you should have Docker installed and the Docker daemon should be running. This is because some tests require pulling container images beforehand and also other tests check functionality like pulling directly from the Docker socket.
18+
Additionally, you should have Docker installed and the Docker daemon should be running. This is because some tests require pulling container images beforehand and also other tests check functionality like pulling directly from the Docker socket. Also double check that `Use containerd for pulling and storing images` is unchecked in Docker desktop settings as using `containerd` leads to issues with SHA's not matching in some tests.
1919

2020
## Writing tests
2121

test/fixtures/rpm/source_rpms.csv

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
redhat-release-10.0-29.el10.src.rpm,gcc-14.2.1-7.el10.src.rpm,setup-2.14.5-4.el10.src.rpm,fonts-rpm-macros-2.0.5-18.el10.src.rpm,tzdata-2025a-1.el10.src.rpm,filesystem-3.18-16.el10.src.rpm,google-noto-fonts-20240401-5.el10.src.rpm,subscription-manager-rhsm-certificates-20220623-6.el10.src.rpm,google-noto-fonts-20240401-5.el10.src.rpm,google-noto-fonts-20240401-5.el10.src.rpm,google-noto-fonts-20240401-5.el10.src.rpm,basesystem-11-22.el10.src.rpm,redhat-fonts-4.0.3-14.el10.src.rpm,redhat-fonts-4.0.3-14.el10.src.rpm,langpacks-4.1-3.el10.src.rpm,langpacks-4.1-3.el10.src.rpm,langpacks-4.1-3.el10.src.rpm,vim-9.1.083-5.el10.src.rpm,pcre2-10.44-1.el10.3.src.rpm,ncurses-6.4-14.20240127.el10.src.rpm,glibc-2.39-37.el10.src.rpm,glibc-2.39-37.el10.src.rpm,glibc-2.39-37.el10.src.rpm,ncurses-6.4-14.20240127.el10.src.rpm,bash-5.2.26-6.el10.src.rpm,zlib-ng-2.2.3-1.el10.src.rpm,bzip2-1.0.8-25.el10.src.rpm,util-linux-2.40.2-10.el10.src.rpm,xz-5.6.2-3.el10.src.rpm,util-linux-2.40.2-10.el10.src.rpm,libffi-3.4.4-9.el10.src.rpm,libxcrypt-4.4.36-10.el10.src.rpm,zstd-1.5.5-9.el10.src.rpm,elfutils-0.192-5.el10.src.rpm,popt-1.19-8.el10.src.rpm,libxml2-2.12.5-5.el10_0.src.rpm,crypto-policies-20250214-1.gitfd9b9b9.el10.src.rpm,readline-8.2-11.el10.src.rpm,attr-2.5.2-5.el10.src.rpm,acl-2.3.2-4.el10.src.rpm,util-linux-2.40.2-10.el10.src.rpm,gcc-14.2.1-7.el10.src.rpm,pcre2-10.44-1.el10.3.src.rpm,sqlite-3.46.1-3.el10.src.rpm,dmidecode-3.6-3.el10.src.rpm,expat-2.6.4-1.el10.src.rpm,gdbm-1.23-11.el10_0.src.rpm,json-c-0.18-3.el10.src.rpm,keyutils-1.6.3-5.el10.src.rpm,libcap-ng-0.8.4-6.el10.src.rpm,audit-4.0.3-1.el10.src.rpm,libeconf-0.6.2-4.el10.src.rpm,pam-1.6.1-7.el10.src.rpm,libcap-2.69-7.el10.src.rpm,systemd-257-9.el10.src.rpm,libtasn1-4.20.0-1.el10.src.rpm,p11-kit-0.25.5-7.el10.src.rpm,grep-3.11-10.el10.src.rpm,libbpf-1.5.0-4.el10.src.rpm,util-linux-2.40.2-10.el10.src.rpm,gmp-6.2.1-10.el10.src.rpm,libsepol-3.8-1.el10.src.rpm,libselinux-3.8-1.el10.src.rpm,coreutils-9.5-6.el10.src.rpm,util-linux-2.40.2-10.el10.src.rpm,sed-4.9-3.el10.src.rpm,util-linux-2.40.2-10.el10.src.rpm,findutils-4.10.0-5.el10.src.rpm,libunistring-1.1-10.el10.src.rpm,libidn2-2.3.7-3.el10.src.rpm,lua-5.4.6-7.el10.src.rpm,gzip-1.13-3.el10.src.rpm,cracklib-2.9.11-8.el10.src.rpm,cracklib-2.9.11-8.el10.src.rpm,libpwquality-1.4.5-12.el10.src.rpm,openssl-fips-provider-3.0.7-6.el10.src.rpm,openssl-fips-provider-3.0.7-6.el10.src.rpm,which-2.21-43.el10.src.rpm,libsemanage-3.8-1.el10.src.rpm,shadow-utils-4.15.0-5.el10.src.rpm,libutempter-1.2.1-15.el10.src.rpm,mpfr-4.2.1-5.el10.src.rpm,gawk-5.3.0-6.el10.src.rpm,dbus-1.14.10-5.el10.src.rpm,keyutils-1.6.3-5.el10.src.rpm,gdbm-1.23-11.el10_0.src.rpm,libcomps-0.1.21-3.el10.src.rpm,attr-2.5.2-5.el10.src.rpm,file-5.45-7.el10.src.rpm,dbus-1.14.10-5.el10.src.rpm,dbus-broker-36-1.el10.src.rpm,dbus-1.14.10-5.el10.src.rpm,psmisc-23.6-8.el10.src.rpm,chkconfig-1.30-2.el10.src.rpm,p11-kit-0.25.5-7.el10.src.rpm,ca-certificates-2024.2.69_v8.0.303-102.3.el10.src.rpm,openssl-3.2.2-16.el10.src.rpm,pam-1.6.1-7.el10.src.rpm,rust-rpm-sequoia-1.6.0-6.el10.src.rpm,rpm-4.19.1.1-12.el10.src.rpm,libsolv-0.7.29-7.el10.src.rpm,tpm2-tss-4.1.3-5.el10.src.rpm,ima-evm-utils-1.6.2-1.el10.src.rpm,rpm-4.19.1.1-12.el10.src.rpm,python-pip-23.3.2-7.el10.src.rpm,gnutls-3.8.9-9.el10.src.rpm,glib2-2.80.4-4.el10.src.rpm,gobject-introspection-1.79.1-6.el10.src.rpm,json-glib-1.8.0-5.el10.src.rpm,librhsm-0.0.3-16.el10.src.rpm,e2fsprogs-1.47.1-3.el10.src.rpm,gcc-14.2.1-7.el10.src.rpm,libmnl-1.0.5-7.el10.src.rpm,iproute-6.11.0-1.el10.src.rpm,nghttp2-1.64.0-2.el10.src.rpm,libseccomp-2.5.3-10.el10.src.rpm,libverto-0.3.2-10.el10.src.rpm,krb5-1.21.3-7.el10.src.rpm,curl-8.9.1-5.el10.src.rpm,librepo-1.18.0-3.el10.src.rpm,curl-8.9.1-5.el10.src.rpm,libyaml-0.2.5-16.el10.src.rpm,libmodulemd-2.15.0-12.el10.src.rpm,libdnf-0.73.1-7.el10.src.rpm,subscription-manager-1.30.5-1.el10.src.rpm,lz4-1.9.4-8.el10.src.rpm,libarchive-3.7.7-1.el10.src.rpm,util-linux-2.40.2-10.el10.src.rpm,authselect-1.5.0-8.el10.src.rpm,elfutils-0.192-5.el10.src.rpm,elfutils-0.192-5.el10.src.rpm,systemd-257-9.el10.src.rpm,systemd-257-9.el10.src.rpm,rpm-4.19.1.1-12.el10.src.rpm,virt-what-1.27-2.el10.src.rpm,mpdecimal-2.5.1-12.el10.src.rpm,python3.12-3.12.9-1.el10.src.rpm,python3.12-3.12.9-1.el10.src.rpm,dbus-python-1.3.2-8.el10.src.rpm,libdnf-0.73.1-7.el10.src.rpm,libdnf-0.73.1-7.el10.src.rpm,pygobject3-3.46.0-7.el10.src.rpm,pygobject3-3.46.0-7.el10.src.rpm,python-idna-3.7-4.el10.src.rpm,rpm-4.19.1.1-12.el10.src.rpm,python-six-1.16.0-16.el10.src.rpm,python-dateutil-2.8.2-15.el10.src.rpm,python-iniparse-0.5-10.el10.src.rpm,python-urllib3-1.26.19-2.el10.src.rpm,libcomps-0.1.21-3.el10.src.rpm,librepo-1.18.0-3.el10.src.rpm,python-charset-normalizer-3.3.2-7.el10.src.rpm,python-requests-2.32.3-2.el10.src.rpm,subscription-manager-1.30.5-1.el10.src.rpm,subscription-manager-1.30.5-1.el10.src.rpm,python-decorator-5.1.1-12.el10.src.rpm,python-inotify-0.9.6-36.el10.src.rpm,python-systemd-235-11.el10.src.rpm,dnf-4.20.0-11.el10.src.rpm,dnf-4.20.0-11.el10.src.rpm,dnf-4.20.0-11.el10.src.rpm,dnf-plugins-core-4.7.0-8.el10.src.rpm,subscription-manager-1.30.5-1.el10.src.rpm,dnf-4.20.0-11.el10.src.rpm,crypto-policies-20250214-1.gitfd9b9b9.el10.src.rpm,authselect-1.5.0-8.el10.src.rpm,rpm-4.19.1.1-12.el10.src.rpm,tar-1.35-7.el10.src.rpm,vim-9.1.083-5.el10.src.rpm,gdb-14.2-4.el10.src.rpm,langpacks-4.1-3.el10.src.rpm,rootfiles-8.1-41.el10.src.rpm
Lines changed: 102 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,88 +1,114 @@
1-
import { PackageInfo } from "@snyk/rpm-parser/lib/rpm/types";
2-
import { mapRpmSqlitePackages } from "../../../../lib/analyzer/package-managers/rpm";
1+
import * as fs from "fs";
2+
import * as path from "path";
3+
import { parseSourceRPM } from "../../../../lib/analyzer/package-managers/rpm";
4+
import { SourcePackage } from "../../../../lib/analyzer/types";
35

4-
describe("Correctly maps RPM package version", () => {
5-
it("formats version without epoch", () => {
6-
const mappedResults = mapRpmSqlitePackages(
7-
"image",
8-
[
9-
{
10-
name: "pkg1",
11-
version: "1.2.3",
12-
release: "1",
13-
size: 1,
14-
},
15-
],
16-
[],
6+
describe("parseSourceRPM", () => {
7+
it("should correctly parse all valid source RPM strings from source_rpms.csv", () => {
8+
const csvFilePath = path.join(
9+
__dirname,
10+
"../../../../test/fixtures/rpm/source_rpms.csv",
1711
);
12+
let fileContent;
13+
try {
14+
fileContent = fs.readFileSync(csvFilePath, "utf-8");
15+
} catch (error) {
16+
throw new Error(
17+
`Failed to read source_rpms.csv: ${error.message}. Please ensure the file exists at test/fixtures/rpm/source_rpms.csv.`,
18+
);
19+
}
20+
21+
const sourceRpmStrings = fileContent
22+
.split(",")
23+
.filter((s) => s.trim() !== "");
24+
25+
if (sourceRpmStrings.length === 0) {
26+
console.warn(
27+
"source_rpms.csv is empty or contains no valid entries. Test will pass trivially.",
28+
);
29+
return;
30+
}
1831

19-
const expected = [
20-
{
21-
Name: "pkg1",
22-
Version: "1.2.3-1",
23-
Source: undefined,
24-
Provides: [],
25-
Deps: {},
26-
AutoInstalled: undefined,
27-
Purl: "pkg:rpm/pkg1@1.2.3-1",
28-
},
29-
];
30-
expect(mappedResults.Analysis).toMatchObject(expected);
32+
let failedCount = 0;
33+
sourceRpmStrings.forEach((rpmString) => {
34+
const parsed = parseSourceRPM(rpmString.trim());
35+
36+
try {
37+
expect(parsed).toBeDefined();
38+
if (parsed) {
39+
expect(parsed.name).toEqual(expect.any(String));
40+
expect(parsed.name.length).toBeGreaterThan(0);
41+
expect(parsed.version).toEqual(expect.any(String));
42+
expect(parsed.version.length).toBeGreaterThan(0);
43+
expect(parsed.release).toEqual(expect.any(String));
44+
expect(parsed.release.length).toBeGreaterThan(0);
45+
}
46+
} catch (e) {
47+
console.error(`Failed to parse or assert: '${rpmString}'`, e);
48+
failedCount++;
49+
}
50+
});
51+
52+
if (failedCount > 0) {
53+
throw new Error(
54+
`${failedCount} out of ${sourceRpmStrings.length} RPM strings failed parsing or assertion.`,
55+
);
56+
}
57+
expect(failedCount).toBe(0);
3158
});
32-
it("formats version with epoch == 0", () => {
33-
const mappedResults = mapRpmSqlitePackages(
34-
"image",
35-
[
36-
{
37-
name: "pkg2",
38-
version: "1.2.3",
39-
release: "2",
40-
epoch: 0,
41-
size: 1,
42-
},
43-
],
44-
[],
45-
);
4659

47-
const expected = [
48-
{
49-
Name: "pkg2",
50-
Version: "1.2.3-2",
51-
Source: undefined,
52-
Provides: [],
53-
Deps: {},
54-
AutoInstalled: undefined,
55-
Purl: "pkg:rpm/pkg2@1.2.3-2",
56-
},
57-
];
58-
expect(mappedResults.Analysis).toMatchObject(expected);
60+
// Add more specific test cases if needed
61+
it("should return undefined for invalid or malformed source RPM strings", () => {
62+
expect(parseSourceRPM("invalid-rpm-string")).toBeUndefined();
63+
expect(parseSourceRPM("nameonly.src.rpm")).toBeUndefined();
64+
expect(parseSourceRPM("name-versiononly.src.rpm")).toBeUndefined();
65+
expect(parseSourceRPM("name-1.2.3-.src.rpm")).toBeUndefined(); // empty release
66+
expect(parseSourceRPM("name--release.src.rpm")).toBeUndefined(); // empty version
67+
expect(parseSourceRPM("-version-release.src.rpm")).toBeUndefined(); // empty name
68+
expect(parseSourceRPM("not-an-rpm-at-all")).toBeUndefined();
69+
expect(parseSourceRPM("")).toBeUndefined();
70+
expect(parseSourceRPM(undefined)).toBeUndefined();
5971
});
60-
it("formats version with epoch", () => {
61-
const mappedResults = mapRpmSqlitePackages(
62-
"image",
72+
73+
it("should correctly parse known valid source RPM strings", () => {
74+
const cases: Array<{ input: string; expected: SourcePackage | undefined }> =
6375
[
6476
{
65-
name: "pkg3",
66-
version: "1.2.3",
67-
release: "3",
68-
epoch: 1,
69-
size: 1,
77+
input: "bash-5.1.16-4.el9.src.rpm",
78+
expected: {
79+
name: "bash",
80+
version: "5.1.16",
81+
release: "4.el9",
82+
},
7083
},
71-
],
72-
[],
73-
);
84+
{
85+
input: "libreport-filesystem-2.17.11-1.fc38.src.rpm",
86+
expected: {
87+
name: "libreport-filesystem",
88+
version: "2.17.11",
89+
release: "1.fc38",
90+
},
91+
},
92+
{
93+
input: "kernel-6.5.0-0.rc1.20230722gitb1c0ddc7f7e1.42.fc39.src.rpm",
94+
expected: {
95+
name: "kernel",
96+
version: "6.5.0",
97+
release: "0.rc1.20230722gitb1c0ddc7f7e1.42.fc39",
98+
},
99+
},
100+
{
101+
input: "hyphen-name-package-1.2.3-1.src.rpm",
102+
expected: {
103+
name: "hyphen-name-package",
104+
version: "1.2.3",
105+
release: "1",
106+
},
107+
},
108+
];
74109

75-
const expected = [
76-
{
77-
Name: "pkg3",
78-
Version: "1:1.2.3-3",
79-
Source: undefined,
80-
Provides: [],
81-
Deps: {},
82-
AutoInstalled: undefined,
83-
Purl: "pkg:rpm/pkg3@1:1.2.3-3?epoch=1",
84-
},
85-
];
86-
expect(mappedResults.Analysis).toMatchObject(expected);
110+
for (const tc of cases) {
111+
expect(parseSourceRPM(tc.input)).toEqual(tc.expected);
112+
}
87113
});
88114
});

0 commit comments

Comments
 (0)