snyk-docker-plugin
Version:
Snyk CLI docker plugin
247 lines (210 loc) • 6.31 kB
text/typescript
import * as Debug from "debug";
import { mkdir, mkdtemp, rm, stat, writeFile } from "fs/promises";
import * as path from "path";
import { FilePathToContent, FilesByDirMap } from "./types";
const debug = Debug("snyk");
const nodeModulesRegex = /^(.*?)(?:[\\\/]node_modules)/;
export {
persistNodeModules,
cleanupAppNodeModules,
groupNodeAppFilesByDirectory,
groupNodeModulesFilesByDirectory,
};
interface ScanPaths {
tempDir: string;
tempProjectPath: string;
manifestPath?: string;
}
async function createTempProjectDir(
projectDir: string,
): Promise<{ tmpDir: string; tempProjectRoot: string }> {
const tmpDir = await mkdtemp("snyk");
const tempProjectRoot = path.join(tmpDir, projectDir);
await mkdir(tempProjectRoot, { recursive: true });
return {
tmpDir,
tempProjectRoot,
};
}
const manifestName: string = "package.json";
async function fileExists(path: string): Promise<boolean> {
return await stat(path)
.then(() => true)
.catch(() => false);
}
async function createSyntheticManifest(
tempRootManifestDir: string,
): Promise<void> {
const tempRootManifestPath = path.join(tempRootManifestDir, manifestName);
debug(`Creating an empty synthetic manifest file: ${tempRootManifestPath}`);
try {
await writeFile(tempRootManifestPath, "{}", "utf-8");
} catch (error) {
debug(
`Error while writing file ${tempRootManifestPath} : ${error.message}`,
);
}
}
async function saveOnDisk(
tempDir: string,
modules: Set<string>,
filePathToContent: FilePathToContent,
): Promise<void> {
for (const module of modules) {
const manifestContent = filePathToContent[module];
if (!manifestContent) {
continue;
}
await createFile(path.join(tempDir, module), manifestContent);
}
}
async function persistNodeModules(
project: string,
filePathToContent: FilePathToContent,
fileNamesGroupedByDirectory: FilesByDirMap,
): Promise<ScanPaths> {
const modules = fileNamesGroupedByDirectory.get(project);
const tmpDir: string = "";
const tempProjectRoot: string = "";
if (!modules || modules.size === 0) {
debug(`Empty application directory tree.`);
return {
tempDir: tmpDir,
tempProjectPath: tempProjectRoot,
};
}
try {
const { tmpDir, tempProjectRoot } = await createTempProjectDir(project);
await saveOnDisk(tmpDir, modules, filePathToContent);
const result: ScanPaths = {
tempDir: tmpDir,
tempProjectPath: tempProjectRoot,
manifestPath: path.join(
tempProjectRoot.substring(tmpDir.length),
manifestName,
),
};
const manifestFileExists = await fileExists(
path.join(tempProjectRoot, manifestName),
);
if (!manifestFileExists) {
await createSyntheticManifest(tempProjectRoot);
delete result.manifestPath;
}
return result;
} catch (error) {
debug(
`Failed to copy the application manifest files locally: ${error.message}`,
);
return {
tempDir: tmpDir,
tempProjectPath: tempProjectRoot,
};
}
}
async function createFile(filePath, fileContent): Promise<void> {
try {
await mkdir(path.dirname(filePath), { recursive: true });
await writeFile(filePath, fileContent, "utf-8");
} catch (error) {
debug(`Error while creating file ${filePath} : ${error.message}`);
}
}
function isYarnCacheDependency(filePath: string): boolean {
if (
filePath.includes(".yarn/cache") ||
filePath.includes(".cache/yarn") ||
filePath.includes("yarn\\cache") ||
filePath.includes("cache\\yarn") ||
filePath.includes("Cache\\Yarn") ||
filePath.includes("Yarn\\Cache")
) {
return true;
}
return false;
}
function isNpmCacheDependency(filePath: string): boolean {
if (filePath.includes(".npm/") || filePath.includes("\\npm-cache")) {
return true;
}
return false;
}
// TODO: Enable custom cache filtering if needed
// function isCustomCache(filePath: string): boolean {
// return (filePath.includes("cache") || filePath.includes("Cache"));
// }
function isPnpmCacheDependency(filePath: string): boolean {
if (
filePath.includes("pnpm-store") ||
filePath.includes("pnpm/store") ||
filePath.includes("pnpm\\store")
) {
return true;
}
return false;
}
function getNodeModulesParentDir(filePath: string): string | null {
const nodeModulesParentDirMatch = nodeModulesRegex.exec(filePath);
if (nodeModulesParentDirMatch && nodeModulesParentDirMatch.length > 1) {
const nodeModulesParentDir = nodeModulesParentDirMatch[1];
if (nodeModulesParentDir === "") {
return "/"; // ensuring the same behavior of path.dirname for '/' dir
}
return nodeModulesParentDir;
}
return null;
}
function groupNodeAppFilesByDirectory(
filePathToContent: FilePathToContent,
): FilesByDirMap {
const filesByDir: FilesByDirMap = new Map();
const filePaths = Object.keys(filePathToContent);
for (const filePath of filePaths) {
const directory = path.dirname(filePath);
if (!filesByDir.has(directory)) {
filesByDir.set(directory, new Set());
}
filesByDir.get(directory)?.add(filePath);
}
return filesByDir;
}
function getGroupingDir(filePath: string): string {
const nodeModulesParentDir = getNodeModulesParentDir(filePath);
if (nodeModulesParentDir) {
return nodeModulesParentDir;
}
return path.dirname(filePath);
}
function groupNodeModulesFilesByDirectory(
filePathToContent: FilePathToContent,
): FilesByDirMap {
const filesByDir: FilesByDirMap = new Map();
const filePaths = Object.keys(filePathToContent);
for (const filePath of filePaths) {
if (isNpmCacheDependency(filePath)) {
continue;
}
if (isYarnCacheDependency(filePath)) {
continue;
}
if (isPnpmCacheDependency(filePath)) {
continue;
}
const directory = getGroupingDir(filePath);
if (!filesByDir.has(directory)) {
filesByDir.set(directory, new Set());
}
filesByDir.get(directory)?.add(filePath);
}
return filesByDir;
}
async function cleanupAppNodeModules(appRootDir: string): Promise<void> {
if (!appRootDir) {
return;
}
try {
await rm(appRootDir, { recursive: true });
} catch (error) {
debug(`Error while removing ${appRootDir} : ${error.message}`);
}
}