Skip to content

Commit

Permalink
Merge pull request #1183 from snyk/feat/yarn-workspaces
Browse files Browse the repository at this point in the history
feat: introduce yarn workspace scanning test & monitor only
  • Loading branch information
lili2311 committed Jun 29, 2020
2 parents 86380be + 5f7bee6 commit 9cb4e4b
Show file tree
Hide file tree
Showing 29 changed files with 1,777 additions and 81 deletions.
16 changes: 10 additions & 6 deletions help/help.txt
Expand Up @@ -32,19 +32,19 @@ Options:
Note gradle is not supported, use --all-sub-projects instead.
--detection-depth=<number>
(test & monitor commands only)
Use with --all-projects to indicate how many sub-directories to search.
Use with --all-projects or --yarn-workspaces to indicate how many sub-directories to search.
Defaults to 2 (the current working directory and one sub-directory).
--exclude=<comma seperated list of directory names>
--exclude=<comma separated list of directory names>
(test & monitor commands only)
Can only be used with --all-projects to indicate sub-directories to exclude.
Directories must be comma seperated.
Can be used with --all-projects and --yarn-workspaces to indicate sub-directories to exclude.
Directories must be comma separated.
If using with --detection-depth exclude ignores directories at any level deep.
--dev .............. Include devDependencies (defaults to production only).
--file=<File> ...... Sets package file. For more help run `snyk help file`.
--org=<org-name> ... Specify the org machine-name to run Snyk with a specific
organization. For more help run `snyk help orgs`.
--ignore-policy .... Ignores the current policy in .snyk file, org level ignores and project policy on snyk.io.
--trust-policies ... Applies and uses ignore rules from your dependencies's
--trust-policies ... Applies and uses ignore rules from your dependencies'
Snyk policies, otherwise ignore policies are only
shown as a suggestion.
--show-vulnerable-paths=<none|some|all>
Expand Down Expand Up @@ -88,7 +88,7 @@ Options:

Maven options:
--scan-all-unmanaged
Autodetects maven jars and wars in given directory.
Auto detects maven jars and wars in given directory.
Individual testing can be done with --file=<jar-file-name>

Gradle options:
Expand Down Expand Up @@ -121,6 +121,8 @@ npm options:
Yarn options:
--strict-out-of-sync=<true|false>
Prevent testing out of sync lockfiles. Defaults to true.
--yarn-workspaces Detect and scan yarn workspaces. You can specify how many sub-directories to search using --detection-depth and exclude directories using --exclude.


CocoaPods options:
--strict-out-of-sync=<true|false>
Expand Down Expand Up @@ -153,6 +155,8 @@ Examples:
$ snyk monitor --project-name=my-project
$ snyk test --docker ubuntu:18.04 --org=my-team
$ snyk test --docker app:latest --file=Dockerfile --policy-path=path/to/.snyk
$ snyk test --yarn-workspaces --detection-depth=4 --strict-out-of-sync=false


Possible exit statuses and their meaning:

Expand Down
29 changes: 20 additions & 9 deletions src/cli/args.ts
Expand Up @@ -3,6 +3,10 @@ import { MethodResult } from './commands/types';

import debugModule = require('debug');
import { parseMode, displayModeHelp } from './modes';
import {
SupportedCliCommands,
SupportedUserReachableFacingCliArgs,
} from '../lib/types';

export declare interface Global extends NodeJS.Global {
ignoreUnknownCA: boolean;
Expand Down Expand Up @@ -157,19 +161,24 @@ export function args(rawArgv: string[]): Args {
argv._.push(command);
}

const commands: SupportedCliCommands[] = [
'protect',
'test',
'monitor',
'wizard',
'ignore',
'woof',
];
// TODO decide why we can't do this cart blanche...
if (
['protect', 'test', 'monitor', 'wizard', 'ignore', 'woof'].indexOf(
command,
) !== -1
) {
if (commands.indexOf(command as SupportedCliCommands) !== -1) {
// copy all the options across to argv._ as an object
argv._.push(argv);
}

// arguments that needs transformation from dash-case to camelCase
// should be added here
for (const dashedArg of [
// TODO: eventually all arguments should be transformed like this.
const argumentsToTransform: Array<Partial<
SupportedUserReachableFacingCliArgs
>> = [
'package-manager',
'packages-folder',
'severity-threshold',
Expand All @@ -181,9 +190,11 @@ export function args(rawArgv: string[]): Args {
'scan-all-unmanaged',
'fail-on',
'all-projects',
'yarn-workspaces',
'detection-depth',
'reachable-vulns',
]) {
];
for (const dashedArg of argumentsToTransform) {
if (argv[dashedArg]) {
const camelCased = dashToCamelCase(dashedArg);
argv[camelCased] = argv[dashedArg];
Expand Down
Expand Up @@ -22,9 +22,6 @@ export function formatMonitorOutput(
: res.path;
const strOutput =
chalk.bold.white('\nMonitoring ' + humanReadableName + '...\n\n') +
(packageManager === 'yarn'
? 'A yarn.lock file was detected - continuing as a Yarn project.\n'
: '') +
'Explore this snapshot at ' +
res.uri +
'\n\n' +
Expand Down
2 changes: 1 addition & 1 deletion src/cli/commands/monitor/index.ts
Expand Up @@ -120,7 +120,7 @@ async function monitor(...args0: MethodArgs): Promise<any> {
validateMonitorPath(path, options.docker);
let analysisType = 'all';
let packageManager;
if (options.allProjects) {
if (options.allProjects || options.yarnWorkspaces) {
analysisType = 'all';
} else if (options.docker) {
analysisType = 'docker';
Expand Down
122 changes: 78 additions & 44 deletions src/cli/index.ts
Expand Up @@ -32,6 +32,12 @@ import {
createDirectory,
writeContentsToFileSwallowingErrors,
} from '../lib/json-file-output';
import {
Options,
TestOptions,
MonitorOptions,
SupportedUserReachableFacingCliArgs,
} from '../lib/types';

const debug = Debug('snyk');
const EXIT_CODES = {
Expand Down Expand Up @@ -217,6 +223,8 @@ function checkPaths(args) {
}
}

type AllSupportedCliOptions = Options & MonitorOptions & TestOptions;

async function main() {
updateCheck();
checkRuntime();
Expand All @@ -227,50 +235,10 @@ async function main() {
let exitCode = EXIT_CODES.ERROR;
try {
modeValidation(args);

if (args.options.scanAllUnmanaged && args.options.file) {
throw new UnsupportedOptionCombinationError([
'file',
'scan-all-unmanaged',
]);
}

if (args.options['project-name'] && args.options.allProjects) {
throw new UnsupportedOptionCombinationError([
'project-name',
'all-projects',
]);
}
if (args.options.file && args.options.allProjects) {
throw new UnsupportedOptionCombinationError(['file', 'all-projects']);
}
if (args.options.packageManager && args.options.allProjects) {
throw new UnsupportedOptionCombinationError([
'package-manager',
'all-projects',
]);
}
if (args.options.docker && args.options.allProjects) {
throw new UnsupportedOptionCombinationError(['docker', 'all-projects']);
}
if (args.options.allSubProjects && args.options.allProjects) {
throw new UnsupportedOptionCombinationError([
'all-sub-projects',
'all-projects',
]);
}

if (args.options.exclude) {
if (typeof args.options.exclude !== 'string') {
throw new ExcludeFlagBadInputError();
}
if (!args.options.allProjects) {
throw new OptionMissingErrorError('--exclude', '--all-projects');
}
if (args.options.exclude.indexOf(pathLib.sep) > -1) {
throw new ExcludeFlagInvalidInputError();
}
}
// TODO: fix this, we do transformation to options and teh type doesn't reflect it
validateUnsupportedOptionCombinations(
(args.options as unknown) as AllSupportedCliOptions,
);

if (
args.options.file &&
Expand Down Expand Up @@ -340,3 +308,69 @@ if (module.parent) {
// eslint-disable-next-line id-blacklist
module.exports = cli;
}

function validateUnsupportedOptionCombinations(
options: AllSupportedCliOptions,
): void {
const unsupportedAllProjectsCombinations: {
[name: string]: SupportedUserReachableFacingCliArgs;
} = {
'project-name': 'project-name',
file: 'file',
yarnWorkspaces: 'yarn-workspaces',
packageManager: 'package-manager',
docker: 'docker',
allSubProjects: 'all-sub-projects',
};

const unsupportedYarnWorkspacesCombinations: {
[name: string]: SupportedUserReachableFacingCliArgs;
} = {
'project-name': 'project-name',
file: 'file',
packageManager: 'package-manager',
docker: 'docker',
allSubProjects: 'all-sub-projects',
};

if (options.scanAllUnmanaged && options.file) {
throw new UnsupportedOptionCombinationError(['file', 'scan-all-unmanaged']);
}

if (options.allProjects) {
for (const option in unsupportedAllProjectsCombinations) {
if (options[option]) {
throw new UnsupportedOptionCombinationError([
unsupportedAllProjectsCombinations[option],
'all-projects',
]);
}
}
}

if (options.yarnWorkspaces) {
for (const option in unsupportedYarnWorkspacesCombinations) {
if (options[option]) {
throw new UnsupportedOptionCombinationError([
unsupportedAllProjectsCombinations[option],
'yarn-workspaces',
]);
}
}
}

if (options.exclude) {
if (!(options.allProjects || options.yarnWorkspaces)) {
throw new OptionMissingErrorError('--exclude', [
'--yarn-workspaces',
'--all-projects',
]);
}
if (typeof options.exclude !== 'string') {
throw new ExcludeFlagBadInputError();
}
if (options.exclude.indexOf(pathLib.sep) > -1) {
throw new ExcludeFlagInvalidInputError();
}
}
}
2 changes: 1 addition & 1 deletion src/lib/errors/exclude-flag-invalid-input.ts
Expand Up @@ -3,7 +3,7 @@ import { CustomError } from './custom-error';
export class ExcludeFlagInvalidInputError extends CustomError {
private static ERROR_CODE = 422;
private static ERROR_MESSAGE =
'The --exclude argument must be a comma seperated list of directory names and cannot contain a path.';
'The --exclude argument must be a comma separated list of directory names and cannot contain a path.';

constructor() {
super(ExcludeFlagInvalidInputError.ERROR_MESSAGE);
Expand Down
6 changes: 4 additions & 2 deletions src/lib/errors/option-missing-error.ts
@@ -1,8 +1,10 @@
import { CustomError } from './custom-error';

export class OptionMissingErrorError extends CustomError {
constructor(option: string, required: string) {
const msg = `The ${option} option can only be use in combination with ${required}.`;
constructor(option: string, required: string[]) {
const msg = `The ${option} option can only be use in combination with ${required
.sort()
.join(' or ')}.`;
super(msg);
this.code = 422;
this.userMessage = msg;
Expand Down
2 changes: 1 addition & 1 deletion src/lib/options-validator.ts
Expand Up @@ -9,7 +9,7 @@ export async function validateOptions(
): Promise<void> {
if (options.reachableVulns) {
// Throwing error only in case when both packageManager and allProjects not defined
if (!packageManager && !options.allProjects) {
if (!packageManager && !options.allProjects && !options.yarnWorkspaces) {
throw new Error('Could not determine package manager');
}
const org = options.org || config.org;
Expand Down
25 changes: 21 additions & 4 deletions src/lib/plugins/get-deps-from-plugin.ts
Expand Up @@ -13,23 +13,36 @@ import {
import analytics = require('../analytics');
import { convertSingleResultToMultiCustom } from './convert-single-splugin-res-to-multi-custom';
import { convertMultiResultToMultiCustom } from './convert-multi-plugin-res-to-multi-custom';
import { processYarnWorkspaces } from './nodejs-plugin/yarn-workspaces-parser';

const debug = debugModule('snyk-test');

const multiProjectProcessors = {
yarnWorkspaces: {
handler: processYarnWorkspaces,
files: ['package.json'],
},
allProjects: {
handler: getMultiPluginResult,
files: AUTO_DETECTABLE_FILES,
},
};

// Force getDepsFromPlugin to return scannedProjects for processing
export async function getDepsFromPlugin(
root: string,
options: Options & (TestOptions | MonitorOptions),
): Promise<pluginApi.MultiProjectResult> {
let inspectRes: pluginApi.InspectResult;

if (options.allProjects) {
if (Object.keys(multiProjectProcessors).some((key) => options[key])) {
const scanType = options.yarnWorkspaces ? 'yarnWorkspaces' : 'allProjects';
const levelsDeep = options.detectionDepth;
const ignore = options.exclude ? options.exclude.split(',') : [];
const targetFiles = await find(
root,
ignore,
AUTO_DETECTABLE_FILES,
multiProjectProcessors[scanType].files,
levelsDeep,
);
debug(
Expand All @@ -39,7 +52,11 @@ export async function getDepsFromPlugin(
if (targetFiles.length === 0) {
throw NoSupportedManifestsFoundError([root]);
}
inspectRes = await getMultiPluginResult(root, options, targetFiles);
inspectRes = await multiProjectProcessors[scanType].handler(
root,
options,
targetFiles,
);
const analyticData = {
scannedProjects: inspectRes.scannedProjects.length,
targetFiles,
Expand All @@ -49,7 +66,7 @@ export async function getDepsFromPlugin(
levelsDeep,
ignore,
};
analytics.add('allProjects', analyticData);
analytics.add(scanType, analyticData);
return inspectRes;
}

Expand Down

0 comments on commit 9cb4e4b

Please sign in to comment.