Skip to content

Commit dd7743f

Browse files
authored
feat: add pnpm lockfile support for container scanning (#734)
* feat: add pnpm lockfile support for container scanning Add support for scanning Node.js applications that use pnpm as their package manager. This enables vulnerability detection in container images that contain pnpm-lock.yaml files. Implementation changes: - Add pnpm-lock.yaml to the list of extracted node app files so the extractor picks up pnpm lockfiles from container images - Introduce detectLockFile() helper function that finds lockfiles in a directory with priority order: npm > yarn > pnpm. This consolidates the lockfile detection logic and makes it easier to extend - Refactor findManifestLockPairsInSameDirectory() and findManifestNodeModulesFilesInSameDirectory() to use the new detectLockFile() helper - Add PnpmLockV5, PnpmLockV6, and PnpmLockV9 cases to buildDepGraph() which calls lockFileParser.parsePnpmProject() to generate dependency graphs from pnpm lockfiles - Update shouldBuildDepTree() to include pnpm lockfile versions, indicating they can be parsed directly without dep tree conversion Test changes: - Add unit tests for detectLockFile() covering all lockfile types, null case, and priority ordering when multiple lockfiles exist - Add pnpm v5 test case to getLockFileVersion tests - Update shouldBuildDepTree tests to verify pnpm versions return false (they don't need dep tree conversion) - Update extractContent test to verify pnpm-lock.yaml files are extracted alongside package.json, package-lock.json, and yarn.lock - Add integration test for scanning container images with pnpm v6 and v9 lockfiles, verifying the dep graph is correctly generated with pkgManager.name set to "pnpm" - Add test fixtures: pnpmlockv6.tar and pnpmlockv9.tar containing Alpine-based images with pnpm projects CN-552 * fix: pr suggestions, more unit tests
1 parent 0ce2f96 commit dd7743f

7 files changed

Lines changed: 1198 additions & 28 deletions

File tree

lib/analyzer/applications/node.ts

Lines changed: 60 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,33 @@ async function depGraphFromManifestFiles(
215215
return scanResults;
216216
}
217217

218+
export interface LockFileInfo {
219+
path: string;
220+
type: lockFileParser.LockfileType;
221+
}
222+
223+
export function detectLockFile(
224+
directoryPath: string,
225+
filesInDirectory: Set<string>,
226+
): LockFileInfo | null {
227+
const lockFiles: Array<{
228+
filename: string;
229+
type: lockFileParser.LockfileType;
230+
}> = [
231+
{ filename: "package-lock.json", type: lockFileParser.LockfileType.npm },
232+
{ filename: "yarn.lock", type: lockFileParser.LockfileType.yarn },
233+
{ filename: "pnpm-lock.yaml", type: lockFileParser.LockfileType.pnpm },
234+
];
235+
236+
for (const { filename, type } of lockFiles) {
237+
const lockPath = path.join(directoryPath, filename);
238+
if (filesInDirectory.has(lockPath)) {
239+
return { path: lockPath, type };
240+
}
241+
}
242+
return null;
243+
}
244+
218245
function findManifestLockPairsInSameDirectory(
219246
fileNamesGroupedByDirectory: FilesByDirMap,
220247
): ManifestLockPathPair[] {
@@ -231,26 +258,21 @@ function findManifestLockPairsInSameDirectory(
231258
}
232259

233260
const expectedManifest = path.join(directoryPath, "package.json");
234-
const expectedNpmLockFile = path.join(directoryPath, "package-lock.json");
235-
const expectedYarnLockFile = path.join(directoryPath, "yarn.lock");
236-
237-
const hasManifestFile = filesInDirectory.has(expectedManifest);
238-
const hasLockFile =
239-
filesInDirectory.has(expectedNpmLockFile) ||
240-
filesInDirectory.has(expectedYarnLockFile);
261+
if (!filesInDirectory.has(expectedManifest)) {
262+
continue;
263+
}
241264

242-
if (hasManifestFile && hasLockFile) {
243-
manifestLockPathPairs.push({
244-
manifest: expectedManifest,
245-
// TODO: correlate filtering action with expected lockfile types
246-
lock: filesInDirectory.has(expectedNpmLockFile)
247-
? expectedNpmLockFile
248-
: expectedYarnLockFile,
249-
lockType: filesInDirectory.has(expectedNpmLockFile)
250-
? lockFileParser.LockfileType.npm
251-
: lockFileParser.LockfileType.yarn,
252-
});
265+
// TODO: correlate filtering action with expected lockfile types
266+
const lockFile = detectLockFile(directoryPath, filesInDirectory);
267+
if (!lockFile) {
268+
continue;
253269
}
270+
271+
manifestLockPathPairs.push({
272+
manifest: expectedManifest,
273+
lock: lockFile.path,
274+
lockType: lockFile.type,
275+
});
254276
}
255277

256278
return manifestLockPathPairs;
@@ -269,13 +291,9 @@ function findManifestNodeModulesFilesInSameDirectory(
269291
}
270292

271293
const expectedManifest = path.join(directoryPath, "package.json");
272-
const expectedNpmLockFile = path.join(directoryPath, "package-lock.json");
273-
const expectedYarnLockFile = path.join(directoryPath, "yarn.lock");
274-
275294
const hasManifestFile = filesInDirectory.has(expectedManifest);
276295
const hasLockFile =
277-
filesInDirectory.has(expectedNpmLockFile) ||
278-
filesInDirectory.has(expectedYarnLockFile);
296+
detectLockFile(directoryPath, filesInDirectory) !== null;
279297

280298
if (hasManifestFile && hasLockFile) {
281299
continue;
@@ -347,6 +365,21 @@ async function buildDepGraph(
347365
strictOutOfSync: shouldBeStrictForManifestAndLockfileOutOfSync,
348366
},
349367
);
368+
case NodeLockfileVersion.PnpmLockV5:
369+
case NodeLockfileVersion.PnpmLockV6:
370+
case NodeLockfileVersion.PnpmLockV9:
371+
return await lockFileParser.parsePnpmProject(
372+
manifestFileContents,
373+
lockFileContents,
374+
{
375+
includeDevDeps: shouldIncludeDevDependencies,
376+
includeOptionalDeps: true,
377+
includePeerDeps: false,
378+
pruneWithinTopLevelDeps: true,
379+
strictOutOfSync: shouldBeStrictForManifestAndLockfileOutOfSync,
380+
},
381+
lockfileVersion,
382+
);
350383
}
351384
throw new Error(
352385
"Failed to build dep graph from current project, unknown lockfile version : " +
@@ -401,6 +434,9 @@ export function shouldBuildDepTree(lockfileVersion: NodeLockfileVersion) {
401434
lockfileVersion === NodeLockfileVersion.YarnLockV1 ||
402435
lockfileVersion === NodeLockfileVersion.YarnLockV2 ||
403436
lockfileVersion === NodeLockfileVersion.NpmLockV2 ||
404-
lockfileVersion === NodeLockfileVersion.NpmLockV3
437+
lockfileVersion === NodeLockfileVersion.NpmLockV3 ||
438+
lockfileVersion === NodeLockfileVersion.PnpmLockV5 ||
439+
lockfileVersion === NodeLockfileVersion.PnpmLockV6 ||
440+
lockfileVersion === NodeLockfileVersion.PnpmLockV9
405441
);
406442
}

lib/inputs/node/static.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@ import { basename } from "path";
22
import { ExtractAction } from "../../extractor/types";
33
import { streamToString } from "../../stream-utils";
44

5-
const nodeAppFiles = ["package.json", "package-lock.json", "yarn.lock"];
5+
const nodeAppFiles = [
6+
"package.json",
7+
"package-lock.json",
8+
"yarn.lock",
9+
"pnpm-lock.yaml",
10+
];
611
const deletedAppFiles = nodeAppFiles.map((file) => ".wh." + file);
712

813
const nodeJsTsAppFileSuffixes = [

test/fixtures/pnpm/pnpmlockv6.tar

3.28 MB
Binary file not shown.

test/fixtures/pnpm/pnpmlockv9.tar

3.28 MB
Binary file not shown.

test/lib/save/image.tar

45.2 MB
Binary file not shown.

0 commit comments

Comments
 (0)