Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: [OSM-1038] nodejs plugin from cli with pnpm support #2

Merged
merged 1 commit into from
Apr 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .releaserc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"branches": [
"main"
]
}
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Snyk helps you find, fix and monitor for known vulnerabilities in your dependenc

## Snyk Node.js Plugin

This plugin provides dependency metadata for npm and Yarn projects and workspaces. It is an internal component intended for use by our [CLI tool](https://github.com/snyk/snyk).
This plugin provides dependency metadata for npm, Yarn and pnpm projects and workspaces. It is an internal component intended for use by our [CLI tool](https://github.com/snyk/snyk).

# Support

Expand All @@ -33,6 +33,7 @@ This plugin provides dependency metadata for npm and Yarn projects and workspace
| ----------- | --------- |
| npm | ✅ |
| Yarn | ✅ |
| Pnpm | ✅ |

## Supported Node versions

Expand Down
56 changes: 12 additions & 44 deletions lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,44 +1,12 @@
import * as modulesParser from './npm-modules-parser';
import * as lockParser from './npm-lock-parser';
import * as types from './types';
import { MissingTargetFileError } from './errors';
import { MultiProjectResult } from '@snyk/cli-interface/legacy/plugin';
import { DepGraph } from '@snyk/dep-graph';
import { PkgTree } from 'snyk-nodejs-lockfile-parser';

function isResDepGraph(depRes: PkgTree | DepGraph): depRes is DepGraph {
return 'rootPkg' in depRes;
}

export async function inspect(
root: string,
targetFile: string,
options: types.Options = {},
): Promise<MultiProjectResult> {
if (!targetFile) {
throw MissingTargetFileError(root);
}
const isLockFileBased =
targetFile.endsWith('package-lock.json') ||
targetFile.endsWith('yarn.lock');

const getLockFileDeps = isLockFileBased && !options.traverseNodeModules;
const depRes: PkgTree | DepGraph = getLockFileDeps
? await lockParser.parse(root, targetFile, options)
: await modulesParser.parse(root, targetFile, options);

let scannedProjects: any[] = [];
if (isResDepGraph(depRes)) {
scannedProjects = [{ depGraph: depRes }];
} else {
scannedProjects = [{ depTree: depRes }];
}

return {
plugin: {
name: 'snyk-nodejs-lockfile-parser',
runtime: process.version,
},
scannedProjects,
};
}
import { inspect } from './inspect';
import {
processNpmWorkspaces,
processPnpmWorkspaces,
processYarnWorkspaces,
} from './workspaces';
export {
inspect,
processNpmWorkspaces,
processPnpmWorkspaces,
processYarnWorkspaces,
};
42 changes: 42 additions & 0 deletions lib/inspect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import * as modulesParser from './npm-modules-parser';
import * as lockParser from './lock-parser';
import * as types from './types';
import { MissingTargetFileError } from './errors';
import { MultiProjectResult } from '@snyk/cli-interface/legacy/plugin';
import { DepGraph } from '@snyk/dep-graph';
import { PkgTree } from 'snyk-nodejs-lockfile-parser';
import { isResDepGraph } from './utils';

export async function inspect(
root: string,
targetFile: string,
options: types.Options = {},
): Promise<MultiProjectResult> {
if (!targetFile) {
throw MissingTargetFileError(root);
}
const isLockFileBased =
targetFile.endsWith('package-lock.json') ||
targetFile.endsWith('yarn.lock') ||
targetFile.endsWith('pnpm-lock.yaml');

const getLockFileDeps = isLockFileBased && !options.traverseNodeModules;
const depRes: PkgTree | DepGraph = getLockFileDeps
? await lockParser.parse(root, targetFile, options)
: await modulesParser.parse(root, targetFile, options);

let scannedProjects: any[] = [];
if (isResDepGraph(depRes)) {
scannedProjects = [{ depGraph: depRes }];
} else {
scannedProjects = [{ depTree: depRes }];
}

return {
plugin: {
name: 'snyk-nodejs-lockfile-parser',
runtime: process.version,
},
scannedProjects,
};
}
86 changes: 14 additions & 72 deletions lib/npm-lock-parser.ts → lib/lock-parser/build-dep-graph.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
import * as baseDebug from 'debug';
const debug = baseDebug('snyk-test');
import * as path from 'path';
import * as fs from 'fs';
import * as lockFileParser from 'snyk-nodejs-lockfile-parser';
import {
NodeLockfileVersion,
PkgTree,
InvalidUserInputError,
ProjectParseOptions,
} from 'snyk-nodejs-lockfile-parser';
import { Options } from './types';
import { DepGraph } from '@snyk/dep-graph';

async function buildDepGraph(
export async function buildDepGraph(
root: string,
manifestFilePath: string,
lockfilePath: string,
Expand All @@ -38,6 +34,19 @@ async function buildDepGraph(
const lockFileContents = fs.readFileSync(lockFileFullPath, 'utf-8');

switch (lockfileVersion) {
case NodeLockfileVersion.PnpmLockV5:
case NodeLockfileVersion.PnpmLockV6:
return await lockFileParser.parsePnpmProject(
manifestFileContents,
lockFileContents,
{
includeDevDeps: options.includeDevDeps,
includeOptionalDeps: options.includeOptionalDeps,
pruneWithinTopLevelDeps: true,
strictOutOfSync: options.strictOutOfSync,
},
lockfileVersion,
);
case NodeLockfileVersion.YarnLockV1:
return await lockFileParser.parseYarnLockV1Project(
manifestFileContents,
Expand Down Expand Up @@ -72,70 +81,3 @@ async function buildDepGraph(
throw new Error('Failed to build dep graph from current project');
}
}

export async function parse(
root: string,
targetFile: string,
options: Options,
): Promise<PkgTree | DepGraph> {
const lockFileFullPath = path.resolve(root, targetFile);
if (!fs.existsSync(lockFileFullPath)) {
throw new Error(
'Lockfile ' + targetFile + ' not found at location: ' + lockFileFullPath,
);
}

const fullPath = path.parse(lockFileFullPath);
const manifestFileFullPath = path.resolve(fullPath.dir, 'package.json');
const shrinkwrapFullPath = path.resolve(fullPath.dir, 'npm-shrinkwrap.json');

if (!fs.existsSync(manifestFileFullPath)) {
throw new Error(
`Could not find package.json at ${manifestFileFullPath} ` +
`(lockfile found at ${targetFile})`,
);
}

if (fs.existsSync(shrinkwrapFullPath)) {
throw new Error(
'Both `npm-shrinkwrap.json` and `package-lock.json` were found in ' +
fullPath.dir +
'.\n' +
'Please run your command again specifying `--file=package.json` flag.',
);
}

const resolveModuleSpinnerLabel = `Analyzing npm dependencies for ${lockFileFullPath}`;
debug(resolveModuleSpinnerLabel);

const strictOutOfSync = options.strictOutOfSync !== false;
const lockfileVersion =
lockFileParser.getLockfileVersionFromFile(lockFileFullPath);
if (
lockfileVersion === NodeLockfileVersion.YarnLockV1 ||
lockfileVersion === NodeLockfileVersion.YarnLockV2 ||
lockfileVersion === NodeLockfileVersion.NpmLockV2 ||
lockfileVersion === NodeLockfileVersion.NpmLockV3
) {
return await buildDepGraph(
root,
manifestFileFullPath,
lockFileFullPath,
lockfileVersion,
{
includeDevDeps: options.dev || false,
includeOptionalDeps: true,
strictOutOfSync,
pruneCycles: true,
},
);
}

return lockFileParser.buildDepTreeFromFiles(
root,
manifestFileFullPath,
lockFileFullPath,
options.dev,
strictOutOfSync,
);
}
73 changes: 73 additions & 0 deletions lib/lock-parser/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import * as path from 'path';
import * as fs from 'fs';
import { buildDepGraph } from './build-dep-graph';
import * as lockFileParser from 'snyk-nodejs-lockfile-parser';
import { NodeLockfileVersion, PkgTree } from 'snyk-nodejs-lockfile-parser';
import { Options } from '../types';
import { DepGraph } from '@snyk/dep-graph';

export async function parse(
root: string,
targetFile: string,
options: Options,
): Promise<PkgTree | DepGraph> {
const lockFileFullPath = path.resolve(root, targetFile);
if (!fs.existsSync(lockFileFullPath)) {
throw new Error(
'Lockfile ' + targetFile + ' not found at location: ' + lockFileFullPath,
);
}

const fullPath = path.parse(lockFileFullPath);
const manifestFileFullPath = path.resolve(fullPath.dir, 'package.json');
const shrinkwrapFullPath = path.resolve(fullPath.dir, 'npm-shrinkwrap.json');

if (!fs.existsSync(manifestFileFullPath)) {
throw new Error(
`Could not find package.json at ${manifestFileFullPath} ` +
`(lockfile found at ${targetFile})`,
);
}

if (fs.existsSync(shrinkwrapFullPath)) {
throw new Error(
'Both `npm-shrinkwrap.json` and `package-lock.json` were found in ' +
fullPath.dir +
'.\n' +
'Please run your command again specifying `--file=package.json` flag.',
);
}

const strictOutOfSync = options.strictOutOfSync !== false;
const lockfileVersion =
lockFileParser.getLockfileVersionFromFile(lockFileFullPath);
if (
lockfileVersion === NodeLockfileVersion.YarnLockV1 ||
lockfileVersion === NodeLockfileVersion.YarnLockV2 ||
lockfileVersion === NodeLockfileVersion.NpmLockV2 ||
lockfileVersion === NodeLockfileVersion.NpmLockV3 ||
lockfileVersion === NodeLockfileVersion.PnpmLockV5 ||
lockfileVersion === NodeLockfileVersion.PnpmLockV6
) {
return await buildDepGraph(
root,
manifestFileFullPath,
lockFileFullPath,
lockfileVersion,
{
includeDevDeps: options.dev || false,
includeOptionalDeps: true,
strictOutOfSync,
pruneCycles: true,
},
);
}

return lockFileParser.buildDepTreeFromFiles(
root,
manifestFileFullPath,
lockFileFullPath,
options.dev,
strictOutOfSync,
);
}
6 changes: 5 additions & 1 deletion lib/npm-modules-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ export async function parse(
options.file && options.file.replace('yarn.lock', 'package.json');
}

if (targetFile.endsWith('pnpm-lock.yaml')) {
options.file =
options.file && options.file.replace('pnpm-lock.yaml', 'package.json');
}

// package-lock.json falls back to package.json (used in wizard code)
if (targetFile.endsWith('package-lock.json')) {
options.file =
Expand Down Expand Up @@ -64,7 +69,6 @@ export async function parse(
`without dependencies.\nPlease run '${packageManager} install' first.`,
);
}

return resolveNodeDeps(
root,
Object.assign({}, options, { noFromArrays: true }),
Expand Down
6 changes: 6 additions & 0 deletions lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,9 @@ export interface MultiProjectResultCustom
scannedProjects: ScannedProjectCustom[];
failedResults?: FailedProjectScanError[];
}

export interface PnpmWorkspacesMap {
[packageJsonName: string]: {
workspaces: string[];
};
}
22 changes: 22 additions & 0 deletions lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import * as fs from 'fs';
import * as path from 'path';
import { DepGraph } from '@snyk/dep-graph';
import { PkgTree } from 'snyk-nodejs-lockfile-parser';

export function getFileContents(
root: string,
Expand All @@ -25,3 +27,23 @@ export function fileExists(root: string, fileName: string): boolean {
const fullPath = path.resolve(root, fileName);
return fs.existsSync(fullPath);
}

export function isResDepGraph(depRes: PkgTree | DepGraph): depRes is DepGraph {
return 'rootPkg' in depRes;
}

export function normalizeFilePath(filePath: string): string {
return path.normalize(filePath).replace(/\\/g, '/');
}

export function isSubpath(subpath: string, parentPath: string): boolean {
// Normalize both paths (ensure consistent separators)
const normalizedSubpath = normalizeFilePath(subpath);
const normalizedParentPath = normalizeFilePath(parentPath);

// Ensure subpath starts with parent path
return (
normalizedSubpath == normalizedParentPath ||
normalizedSubpath.startsWith(`${normalizedParentPath}/`)
);
}
4 changes: 4 additions & 0 deletions lib/workspaces/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { processNpmWorkspaces } from './npm-workspaces-parser';
import { processPnpmWorkspaces } from './pnpm-workspaces-parser';
import { processYarnWorkspaces } from './yarn-workspaces-parser';
export { processNpmWorkspaces, processPnpmWorkspaces, processYarnWorkspaces };
Loading