UNPKG

snyk-docker-plugin

Version:
427 lines 19.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.shouldBuildDepTree = exports.getLockFileVersion = exports.detectLockFile = exports.collectNodeModulesPackagePaths = exports.nodeFilesToScannedProjects = void 0; const dep_graph_1 = require("@snyk/dep-graph"); const Debug = require("debug"); const path = require("path"); const lockFileParser = require("snyk-nodejs-lockfile-parser"); const resolveDeps = require("snyk-resolve-deps"); const error_utils_1 = require("../../error-utils"); const debug = Debug("snyk"); const errors_1 = require("@snyk/composer-lockfile-parser/dist/errors"); const snyk_nodejs_lockfile_parser_1 = require("snyk-nodejs-lockfile-parser"); const node_modules_utils_1 = require("./node-modules-utils"); const path_canonicalization_1 = require("../package-managers/path-canonicalization"); async function nodeFilesToScannedProjects(filePathToContent, shouldIncludeNodeModules, // Per-package install paths are only consumed for APK ownership on // Chainguard/Wolfi images, so collection is gated to avoid parsing every // node_modules package.json on images that will never use it. collectOwnershipPaths = false) { const scanResults = []; /** * TODO: Add support for Yarn workspaces! * https://github.com/snyk/nodejs-lockfile-parser/blob/af8ba81930e950156b539281ecf41c1bc63dacf4/test/lib/yarn-workflows.test.ts#L7-L17 * * When building the ScanResult ensure the workspace is stored in scanResult.identity.args: * args: { * rootWorkspace: <path-of-workspace>, * }; */ if (Object.keys(filePathToContent).length === 0) { return []; } const fileNamesGroupedByDirectory = (0, node_modules_utils_1.groupNodeAppFilesByDirectory)(filePathToContent); const manifestFilePairs = findManifestLockPairsInSameDirectory(fileNamesGroupedByDirectory); if (manifestFilePairs.length !== 0) { scanResults.push(...(await depGraphFromManifestFiles(filePathToContent, manifestFilePairs))); } if (shouldIncludeNodeModules) { const appNodeModulesGroupedByDirectory = (0, node_modules_utils_1.groupNodeModulesFilesByDirectory)(filePathToContent); const nodeProjects = findManifestNodeModulesFilesInSameDirectory(appNodeModulesGroupedByDirectory); if (nodeProjects.length !== 0) { scanResults.push(...(await depGraphFromNodeModules(filePathToContent, nodeProjects, appNodeModulesGroupedByDirectory, collectOwnershipPaths))); } } return scanResults; } exports.nodeFilesToScannedProjects = nodeFilesToScannedProjects; // Match a package.json only at a real package root (parent is an immediate // child of node_modules, optional @scope/), so bundled fixtures nested deeper // aren't collected as packages. [\\/] handles both POSIX and Windows paths. const nodeModulesPackageJsonRegex = /[\\/]node_modules[\\/](?:@[^\\/]+[\\/])?[^\\/]+[\\/]package\.json$/; const nodeModulesDotDirRegex = /[\\/]node_modules[\\/]\./; /** * Collect the on-disk install directory of each package under a project's * node_modules, from the image paths already gathered for the scan. Used to * resolve per-package APK ownership downstream (response-builder), since the * scan result itself only records the shared node_modules root. Skips pnpm/.bin * virtual entries and any package.json without a name+version. */ function collectNodeModulesPackagePaths(project, fileNamesGroupedByDirectory, filePathToContent) { const files = fileNamesGroupedByDirectory.get(project); if (!files) { return []; } const packages = []; for (const filePath of files) { // Skip dot-prefixed node_modules entries (.pnpm, .bin, .cache); npm package // names never start with ".", so no real package dir is excluded. if (!nodeModulesPackageJsonRegex.test(filePath) || nodeModulesDotDirRegex.test(filePath)) { continue; } const content = filePathToContent[filePath]; if (!content) { continue; } let parsed; try { parsed = JSON.parse(content); } catch (_a) { continue; } // JSON.parse returns any; require non-empty strings since the coordinate // flows into the apkPackageOwnership fact as name/version. if (typeof parsed.name !== "string" || typeof parsed.version !== "string" || !parsed.name || !parsed.version) { continue; } packages.push({ name: parsed.name, version: parsed.version, // Normalize first so posix.dirname works on Windows backslash paths. installDir: path.posix.dirname((0, path_canonicalization_1.normalizeAbsolutePath)(filePath)), }); } return packages; } exports.collectNodeModulesPackagePaths = collectNodeModulesPackagePaths; async function depGraphFromNodeModules(filePathToContent, nodeProjects, fileNamesGroupedByDirectory, collectOwnershipPaths) { const scanResults = []; for (const project of nodeProjects) { // First, try to build dep graph from pnpm virtual store if present const pnpmResult = await tryBuildDepGraphFromPnpmStore(project, filePathToContent, fileNamesGroupedByDirectory); if (pnpmResult) { scanResults.push(pnpmResult); continue; } // Fallback to snyk-resolve-deps for non-pnpm projects const { tempDir, tempProjectPath, manifestPath } = await (0, node_modules_utils_1.persistNodeModules)(project, filePathToContent, fileNamesGroupedByDirectory); if (!tempDir) { continue; } if (!tempProjectPath) { await (0, node_modules_utils_1.cleanupAppNodeModules)(tempDir); continue; } try { const pkgTree = await resolveDeps(tempProjectPath, { dev: false, noFromArrays: true, }); if (pkgTree.numDependencies === 0) { continue; } const depGraph = await dep_graph_1.legacy.depTreeToGraph(pkgTree, pkgTree.type || "npm"); // Only collected on Chainguard/Wolfi (the caller passes the distro gate), // since per-package ownership is the only consumer. const nodeModulesPackages = collectOwnershipPaths ? collectNodeModulesPackagePaths(project, fileNamesGroupedByDirectory, filePathToContent) : []; scanResults.push({ facts: [ { type: "depGraph", data: depGraph, }, { type: "testedFiles", data: manifestPath ? manifestPath : path.join(project, "node_modules"), }, ], identity: { type: depGraph.pkgManager.name, targetFile: manifestPath ? manifestPath : path.join(project, "node_modules"), }, ...(nodeModulesPackages.length > 0 ? { nodeModulesPackagePaths: nodeModulesPackages } : {}), }); } catch (error) { debug(`An error occurred while analysing node_modules dir: ${(0, error_utils_1.getErrorMessage)(error)}`); } finally { await (0, node_modules_utils_1.cleanupAppNodeModules)(tempDir); } } return scanResults; } /** * Builds a dependency graph from pnpm's .pnpm directory. * snyk-resolve-deps doesn't understand pnpm's virtual store structure, * so we parse the package.json files directly. */ async function tryBuildDepGraphFromPnpmStore(project, filePathToContent, fileNamesGroupedByDirectory) { const projectFiles = fileNamesGroupedByDirectory.get(project); if (!projectFiles) { return null; } // Find all package.json files inside .pnpm directories const pnpmPackageJsons = Array.from(projectFiles).filter((f) => f.includes("/node_modules/.pnpm/") && f.endsWith("/package.json")); if (pnpmPackageJsons.length === 0) { return null; } // Find the root package.json (parent of node_modules/.pnpm) const pnpmMatch = pnpmPackageJsons[0].match(/^(.+?)\/node_modules\/\.pnpm\//); if (!pnpmMatch) { return null; } const rootManifestPath = path.posix.join(pnpmMatch[1], "package.json"); const rootManifestContent = filePathToContent[rootManifestPath]; if (!rootManifestContent) { return null; } let rootPkg; try { rootPkg = JSON.parse(rootManifestContent); } catch (_a) { return null; } if (!rootPkg.name) { return null; } debug(`Building pnpm dep graph for ${rootPkg.name} from .pnpm directory`); // Parse all packages from .pnpm and add them as direct dependencies const builder = new dep_graph_1.DepGraphBuilder({ name: "pnpm" }, { name: rootPkg.name, version: rootPkg.version || "0.0.0" }); const seen = new Set(); for (const pkgJsonPath of pnpmPackageJsons) { const content = filePathToContent[pkgJsonPath]; if (!content) { continue; } try { const pkg = JSON.parse(content); if (!pkg.name || !pkg.version) { continue; } const nodeId = `${pkg.name}@${pkg.version}`; if (seen.has(nodeId)) { continue; } seen.add(nodeId); builder.addPkgNode({ name: pkg.name, version: pkg.version }, nodeId); builder.connectDep(builder.rootNodeId, nodeId); } catch (_b) { // Skip unparseable files } } if (seen.size === 0) { return null; } const depGraph = builder.build(); debug(`Built pnpm dep graph with ${depGraph.getPkgs().length} packages`); return { facts: [ { type: "depGraph", data: depGraph }, { type: "testedFiles", data: rootManifestPath }, ], identity: { type: "pnpm", targetFile: rootManifestPath, }, }; } async function depGraphFromManifestFiles(filePathToContent, manifestFilePairs) { const scanResults = []; const shouldIncludeDevDependencies = false; const shouldBeStrictForManifestAndLockfileOutOfSync = false; for (const pathPair of manifestFilePairs) { let depGraph; try { const lockfileVersion = getLockFileVersion(pathPair.lock, filePathToContent[pathPair.lock]); depGraph = shouldBuildDepTree(lockfileVersion) ? await buildDepGraphFromDepTree(filePathToContent[pathPair.manifest], filePathToContent[pathPair.lock], pathPair.lockType, shouldIncludeDevDependencies, shouldBeStrictForManifestAndLockfileOutOfSync) : await buildDepGraph(filePathToContent[pathPair.manifest], filePathToContent[pathPair.lock], lockfileVersion, shouldIncludeDevDependencies, shouldBeStrictForManifestAndLockfileOutOfSync); } catch (err) { debug(`An error occurred while analysing a pair of manifest and lock files: ${(0, error_utils_1.getErrorMessage)(err)}`); continue; } const depGraphFact = { type: "depGraph", data: depGraph, }; const testedFilesFact = { type: "testedFiles", data: [path.basename(pathPair.manifest), path.basename(pathPair.lock)], }; scanResults.push({ facts: [depGraphFact, testedFilesFact], identity: { type: depGraph.pkgManager.name, targetFile: pathPair.manifest, }, }); } return scanResults; } function detectLockFile(directoryPath, filesInDirectory) { const lockFiles = [ { filename: "package-lock.json", type: lockFileParser.LockfileType.npm }, { filename: "yarn.lock", type: lockFileParser.LockfileType.yarn }, { filename: "pnpm-lock.yaml", type: lockFileParser.LockfileType.pnpm }, ]; for (const { filename, type } of lockFiles) { const lockPath = path.join(directoryPath, filename); if (filesInDirectory.has(lockPath)) { return { path: lockPath, type }; } } return null; } exports.detectLockFile = detectLockFile; function findManifestLockPairsInSameDirectory(fileNamesGroupedByDirectory) { const manifestLockPathPairs = []; for (const directoryPath of fileNamesGroupedByDirectory.keys()) { if (directoryPath.includes("node_modules")) { continue; } const filesInDirectory = fileNamesGroupedByDirectory.get(directoryPath); if (!filesInDirectory || filesInDirectory.size < 1) { // missing manifest files continue; } const expectedManifest = path.join(directoryPath, "package.json"); if (!filesInDirectory.has(expectedManifest)) { continue; } // TODO: correlate filtering action with expected lockfile types const lockFile = detectLockFile(directoryPath, filesInDirectory); if (!lockFile) { continue; } manifestLockPathPairs.push({ manifest: expectedManifest, lock: lockFile.path, lockType: lockFile.type, }); } return manifestLockPathPairs; } function findManifestNodeModulesFilesInSameDirectory(fileNamesGroupedByDirectory) { const nodeProjects = []; for (const directoryPath of fileNamesGroupedByDirectory.keys()) { const filesInDirectory = fileNamesGroupedByDirectory.get(directoryPath); if (!filesInDirectory || filesInDirectory.size < 1) { // missing manifest files continue; } const expectedManifest = path.join(directoryPath, "package.json"); const hasManifestFile = filesInDirectory.has(expectedManifest); const hasLockFile = detectLockFile(directoryPath, filesInDirectory) !== null; if (hasManifestFile && hasLockFile) { continue; } nodeProjects.push(directoryPath); } return nodeProjects; } function stripUndefinedLabels(parserResult) { const optionalLabels = parserResult.labels; const mandatoryLabels = {}; if (optionalLabels) { for (const currentLabelName of Object.keys(optionalLabels)) { if (optionalLabels[currentLabelName] !== undefined) { mandatoryLabels[currentLabelName] = optionalLabels[currentLabelName]; } } } const parserResultWithProperLabels = Object.assign({}, parserResult, { labels: mandatoryLabels, }); return parserResultWithProperLabels; } async function buildDepGraph(manifestFileContents, lockFileContents, lockfileVersion, shouldIncludeDevDependencies, shouldBeStrictForManifestAndLockfileOutOfSync) { switch (lockfileVersion) { case snyk_nodejs_lockfile_parser_1.NodeLockfileVersion.YarnLockV1: return await lockFileParser.parseYarnLockV1Project(manifestFileContents, lockFileContents, { includeDevDeps: shouldIncludeDevDependencies, includeOptionalDeps: true, includePeerDeps: false, pruneLevel: "withinTopLevelDeps", strictOutOfSync: shouldBeStrictForManifestAndLockfileOutOfSync, }); case snyk_nodejs_lockfile_parser_1.NodeLockfileVersion.YarnLockV2: return await lockFileParser.parseYarnLockV2Project(manifestFileContents, lockFileContents, { includeDevDeps: shouldIncludeDevDependencies, includeOptionalDeps: true, pruneWithinTopLevelDeps: true, strictOutOfSync: shouldBeStrictForManifestAndLockfileOutOfSync, }); case snyk_nodejs_lockfile_parser_1.NodeLockfileVersion.NpmLockV2: case snyk_nodejs_lockfile_parser_1.NodeLockfileVersion.NpmLockV3: return await lockFileParser.parseNpmLockV2Project(manifestFileContents, lockFileContents, { includeDevDeps: shouldIncludeDevDependencies, includeOptionalDeps: true, pruneCycles: true, strictOutOfSync: shouldBeStrictForManifestAndLockfileOutOfSync, }); case snyk_nodejs_lockfile_parser_1.NodeLockfileVersion.PnpmLockV5: case snyk_nodejs_lockfile_parser_1.NodeLockfileVersion.PnpmLockV6: case snyk_nodejs_lockfile_parser_1.NodeLockfileVersion.PnpmLockV9: return await lockFileParser.parsePnpmProject(manifestFileContents, lockFileContents, { includeDevDeps: shouldIncludeDevDependencies, includeOptionalDeps: true, includePeerDeps: false, pruneWithinTopLevelDeps: true, strictOutOfSync: shouldBeStrictForManifestAndLockfileOutOfSync, }, lockfileVersion); } throw new Error("Failed to build dep graph from current project, unknown lockfile version : " + lockfileVersion.toString() + "."); } async function buildDepGraphFromDepTree(manifestFileContents, lockFileContents, lockfileType, shouldIncludeDevDependencies, shouldBeStrictForManifestAndLockfileOutOfSync) { const parserResult = await lockFileParser.buildDepTree(manifestFileContents, lockFileContents, shouldIncludeDevDependencies, lockfileType, shouldBeStrictForManifestAndLockfileOutOfSync); const strippedLabelsParserResult = stripUndefinedLabels(parserResult); return await dep_graph_1.legacy.depTreeToGraph(strippedLabelsParserResult, lockfileType); } function getLockFileVersion(lockFilePath, lockFileContents) { let lockfileVersion; if (lockFilePath.endsWith("package-lock.json")) { lockfileVersion = (0, snyk_nodejs_lockfile_parser_1.getNpmLockfileVersion)(lockFileContents); } else if (lockFilePath.endsWith("yarn.lock")) { lockfileVersion = (0, snyk_nodejs_lockfile_parser_1.getYarnLockfileVersion)(lockFileContents); } else if (lockFilePath.endsWith("pnpm-lock.yaml")) { lockfileVersion = (0, snyk_nodejs_lockfile_parser_1.getPnpmLockfileVersion)(lockFileContents); } else { throw new errors_1.InvalidUserInputError(`Unknown lockfile ${lockFilePath}. ` + "Please provide either package-lock.json, yarn.lock or pnpm-lock.yaml"); } return lockfileVersion; } exports.getLockFileVersion = getLockFileVersion; function shouldBuildDepTree(lockfileVersion) { return !(lockfileVersion === snyk_nodejs_lockfile_parser_1.NodeLockfileVersion.YarnLockV1 || lockfileVersion === snyk_nodejs_lockfile_parser_1.NodeLockfileVersion.YarnLockV2 || lockfileVersion === snyk_nodejs_lockfile_parser_1.NodeLockfileVersion.NpmLockV2 || lockfileVersion === snyk_nodejs_lockfile_parser_1.NodeLockfileVersion.NpmLockV3 || lockfileVersion === snyk_nodejs_lockfile_parser_1.NodeLockfileVersion.PnpmLockV5 || lockfileVersion === snyk_nodejs_lockfile_parser_1.NodeLockfileVersion.PnpmLockV6 || lockfileVersion === snyk_nodejs_lockfile_parser_1.NodeLockfileVersion.PnpmLockV9); } exports.shouldBuildDepTree = shouldBuildDepTree; //# sourceMappingURL=node.js.map