From f4a60f16bd49560aafd5b005fd90f2df37b5be49 Mon Sep 17 00:00:00 2001 From: Liliana Kastilio Date: Fri, 27 Dec 2019 16:36:45 +0000 Subject: [PATCH 1/5] feat: get deps via shared getDeps function --- src/cli/commands/monitor/index.ts | 42 +++++++------------ .../test/formatters/legacy-format-issue.ts | 2 +- src/lib/plugins/get-deps-from-plugin.ts | 5 +-- src/lib/plugins/get-multi-plugin-result.ts | 8 ++-- src/lib/plugins/get-single-plugin-result.ts | 4 +- src/lib/snyk-test/run-test.ts | 8 ++-- src/lib/types.ts | 5 ++- .../cli-monitor.acceptance.test.ts | 38 +++++++++++++++++ 8 files changed, 68 insertions(+), 44 deletions(-) diff --git a/src/cli/commands/monitor/index.ts b/src/cli/commands/monitor/index.ts index 4ab6d17ac6..6dcb95f701 100644 --- a/src/cli/commands/monitor/index.ts +++ b/src/cli/commands/monitor/index.ts @@ -6,38 +6,25 @@ import { apiTokenExists } from '../../../lib/api-token'; import snyk = require('../../../lib'); // TODO(kyegupov): fix import import { monitor as snykMonitor } from '../../../lib/monitor'; import * as config from '../../../lib/config'; -import * as url from 'url'; import chalk from 'chalk'; import * as pathUtil from 'path'; import * as spinner from '../../../lib/spinner'; +import * as Debug from 'debug'; import * as detect from '../../../lib/detect'; -import * as plugins from '../../../lib/plugins'; -import { ModuleInfo } from '../../../lib/module-info'; // TODO(kyegupov): fix import -import { MonitorOptions, MonitorMeta, Options } from '../../../lib/types'; +import { MonitorOptions, MonitorMeta } from '../../../lib/types'; import { MethodArgs, ArgsOptions } from '../../args'; import { maybePrintDeps } from '../../../lib/print-deps'; import * as analytics from '../../../lib/analytics'; -import { MonitorError } from '../../../lib/errors'; import { legacyPlugin as pluginApi } from '@snyk/cli-interface'; import { formatMonitorOutput } from './formatters/format-monitor-response'; import { getSubProjectCount } from '../../../lib/plugins/get-sub-project-count'; import { processJsonMonitorResponse } from './process-json-monitor'; +import { GoodResult, BadResult } from './types'; +import { getDepsFromPlugin } from '../../../lib/plugins/get-deps-from-plugin'; const SEPARATOR = '\n-------------------------------------------------------\n'; - -interface GoodResult { - ok: true; - data: string; - path: string; - projectName?: string; -} - -interface BadResult { - ok: false; - data: MonitorError; - path: string; -} +const debug = Debug('snyk'); // This is used instead of `let x; try { x = await ... } catch { cleanup }` to avoid // declaring the type of x as possibly undefined. @@ -86,6 +73,7 @@ async function monitor(...args0: MethodArgs): Promise { // Part 1: every argument is a scan target; process them sequentially for (const path of args as string[]) { + debug(`Processing ${path}...`); try { await validateMonitorPath(path, options.docker); @@ -96,10 +84,6 @@ async function monitor(...args0: MethodArgs): Promise { ? undefined : options.file || detect.detectPackageFile(path); - const modulePlugin = plugins.loadPlugin(packageManager, options); - - const moduleInfo = ModuleInfo(modulePlugin, options.policy); - const displayPath = pathUtil.relative( '.', pathUtil.join(path, targetFile || ''), @@ -110,20 +94,18 @@ async function monitor(...args0: MethodArgs): Promise { const analyzingDepsSpinnerLabel = 'Analyzing ' + analysisType + ' dependencies for ' + displayPath; - const postingMonitorSpinnerLabel = - 'Posting monitor snapshot for ' + displayPath + ' ...'; - await spinner(analyzingDepsSpinnerLabel); // Scan the project dependencies via a plugin analytics.add('packageManager', packageManager); analytics.add('pluginOptions', options); + debug('getDepsFromPlugin ...'); // each plugin will be asked to scan once per path // some return single InspectResult & newer ones return Multi - const inspectResult: pluginApi.InspectResult = await promiseOrCleanup( - moduleInfo.inspect(path, targetFile, { ...options }), + const inspectResult = await promiseOrCleanup( + getDepsFromPlugin(path, { ...options, path, packageManager }), spinner.clear(analyzingDepsSpinnerLabel), ); @@ -131,6 +113,9 @@ async function monitor(...args0: MethodArgs): Promise { await spinner.clear(analyzingDepsSpinnerLabel)(inspectResult); + const postingMonitorSpinnerLabel = + 'Posting monitor snapshot for ' + displayPath + ' ...'; + await spinner(postingMonitorSpinnerLabel); if (inspectResult.plugin.packageManager) { packageManager = inspectResult.plugin.packageManager; @@ -169,6 +154,9 @@ async function monitor(...args0: MethodArgs): Promise { // Post the project dependencies to the Registry for (const projectDeps of perProjectResult.scannedProjects) { + debug( + `Processing ${projectDeps.depTree.name || targetFile || path}...`, + ); maybePrintDeps(options, projectDeps.depTree); const res = await promiseOrCleanup( diff --git a/src/cli/commands/test/formatters/legacy-format-issue.ts b/src/cli/commands/test/formatters/legacy-format-issue.ts index e8f8ee402e..cdc79f6ad9 100644 --- a/src/cli/commands/test/formatters/legacy-format-issue.ts +++ b/src/cli/commands/test/formatters/legacy-format-issue.ts @@ -21,7 +21,7 @@ export function formatIssues( options: Options & TestOptions, ) { const vulnID = vuln.list[0].id; - const packageManager = options.packageManager; + const packageManager = options.packageManager!; const localPackageTest = isLocalFolder(options.path); const uniquePackages = _.uniq( vuln.list.map((i) => { diff --git a/src/lib/plugins/get-deps-from-plugin.ts b/src/lib/plugins/get-deps-from-plugin.ts index 78ac985137..5ccfd93043 100644 --- a/src/lib/plugins/get-deps-from-plugin.ts +++ b/src/lib/plugins/get-deps-from-plugin.ts @@ -1,8 +1,7 @@ import * as debugModule from 'debug'; import { legacyPlugin as pluginApi } from '@snyk/cli-interface'; - import { find } from '../find-files'; -import { Options, TestOptions } from '../types'; +import { Options, TestOptions, MonitorOptions } from '../types'; import { NoSupportedManifestsFoundError } from '../errors'; import { getMultiPluginResult } from './get-multi-plugin-result'; import { getSinglePluginResult } from './get-single-plugin-result'; @@ -18,7 +17,7 @@ const debug = debugModule('snyk'); // Force getDepsFromPlugin to return scannedProjects for processing export async function getDepsFromPlugin( root: string, - options: Options & TestOptions, + options: Options & (TestOptions | MonitorOptions), ): Promise { let inspectRes: pluginApi.InspectResult; diff --git a/src/lib/plugins/get-multi-plugin-result.ts b/src/lib/plugins/get-multi-plugin-result.ts index 6028acb1da..33ba31f83a 100644 --- a/src/lib/plugins/get-multi-plugin-result.ts +++ b/src/lib/plugins/get-multi-plugin-result.ts @@ -2,7 +2,7 @@ import * as _ from 'lodash'; import * as path from 'path'; import * as cliInterface from '@snyk/cli-interface'; -import { TestOptions, Options } from '../types'; +import { TestOptions, Options, MonitorOptions } from '../types'; import { detectPackageManagerFromFile } from '../detect'; import { SupportedPackageManagers } from '../package-managers'; import { getSinglePluginResult } from './get-single-plugin-result'; @@ -14,11 +14,10 @@ export interface ScannedProjectCustom export async function getMultiPluginResult( root: string, - options: Options & TestOptions, + options: Options & (TestOptions | MonitorOptions), targetFiles: string[], ): Promise { const allResults: ScannedProjectCustom[] = []; - for (const targetFile of targetFiles) { const optionsClone = _.cloneDeep(options); optionsClone.file = path.basename(targetFile); @@ -57,8 +56,7 @@ export async function getMultiPluginResult( const customScannedProject: ScannedProjectCustom[] = resultWithScannedProjects.scannedProjects.map( (a) => { (a as ScannedProjectCustom).targetFile = optionsClone.file; - (a as ScannedProjectCustom).packageManager = - optionsClone.packageManager; + (a as ScannedProjectCustom).packageManager = optionsClone.packageManager!; return a as ScannedProjectCustom; }, ); diff --git a/src/lib/plugins/get-single-plugin-result.ts b/src/lib/plugins/get-single-plugin-result.ts index d1cecff7e6..04ba052df7 100644 --- a/src/lib/plugins/get-single-plugin-result.ts +++ b/src/lib/plugins/get-single-plugin-result.ts @@ -1,11 +1,11 @@ import plugins = require('.'); import { ModuleInfo } from '../module-info'; import { legacyPlugin as pluginApi } from '@snyk/cli-interface'; -import { TestOptions, Options } from '../types'; +import { TestOptions, Options, MonitorOptions } from '../types'; export async function getSinglePluginResult( root: string, - options: Options & TestOptions, + options: Options & (TestOptions | MonitorOptions), targetFile?: string, ): Promise { const plugin = plugins.loadPlugin(options.packageManager, options); diff --git a/src/lib/snyk-test/run-test.ts b/src/lib/snyk-test/run-test.ts index 0af9a7d095..15f1a139ef 100644 --- a/src/lib/snyk-test/run-test.ts +++ b/src/lib/snyk-test/run-test.ts @@ -324,7 +324,7 @@ async function assembleLocalPayloads( policyLocations = policyLocations.filter((loc) => { return loc !== root; }); - } else if (['npm', 'yarn'].indexOf(options.packageManager) > -1) { + } else if (['npm', 'yarn'].indexOf(options.packageManager!) > -1) { policyLocations = policyLocations.concat(pluckPolicies(pkg)); } debug('policies found', policyLocations); @@ -372,7 +372,7 @@ async function assembleLocalPayloads( }); let depGraph = await depGraphLib.legacy.depTreeToGraph( pkg, - options.packageManager, + options.packageManager!, ); debug('done converting dep-tree to dep-graph', { @@ -383,7 +383,7 @@ async function assembleLocalPayloads( const prePruneDepCount = countPathsToGraphRoot(depGraph); debug('pre prunedPathsCount: ' + prePruneDepCount); - depGraph = await pruneGraph(depGraph, options.packageManager); + depGraph = await pruneGraph(depGraph, options.packageManager!); analytics.add('prePrunedPathsCount', prePruneDepCount); const postPruneDepCount = countPathsToGraphRoot(depGraph); @@ -405,7 +405,7 @@ async function assembleLocalPayloads( body, }; - if (['yarn', 'npm'].indexOf(options.packageManager) !== -1) { + if (['yarn', 'npm'].indexOf(options.packageManager!) !== -1) { const isLockFileBased = targetFile && (targetFile.endsWith('package-lock.json') || diff --git a/src/lib/types.ts b/src/lib/types.ts index ec484f9c95..f98eb19f3b 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -40,7 +40,7 @@ export interface WizardOptions { newPolicy: boolean; } export interface Options { - org: string | null; + org?: string | null; path: string; docker?: boolean; file?: string; @@ -56,7 +56,7 @@ export interface Options { allSubProjects?: boolean; 'project-name'?: string; 'show-vulnerable-paths'?: string; - packageManager: SupportedPackageManagers; + packageManager?: SupportedPackageManagers; advertiseSubprojectsCount?: number; projectNames?: string[]; severityThreshold?: SEVERITY; @@ -78,6 +78,7 @@ export interface MonitorOptions { 'print-deps'?: boolean; 'experimental-dep-graph'?: boolean; scanAllUnmanaged?: boolean; + allProjects?: boolean; // An experimental flag to allow monitoring of bigtrees (with degraded deps info and remediation advice). 'prune-repeated-subdependencies'?: boolean; diff --git a/test/acceptance/cli-monitor/cli-monitor.acceptance.test.ts b/test/acceptance/cli-monitor/cli-monitor.acceptance.test.ts index 91ceacc888..2c7e7a8b62 100644 --- a/test/acceptance/cli-monitor/cli-monitor.acceptance.test.ts +++ b/test/acceptance/cli-monitor/cli-monitor.acceptance.test.ts @@ -703,6 +703,8 @@ test('`monitor pip-app --file=requirements.txt`', async (t) => { { args: null, file: 'requirements.txt', + packageManager: 'pip', + path: 'pip-app', }, ], 'calls python plugin', @@ -752,6 +754,9 @@ test('`monitor gradle-app`', async (t) => { 'build.gradle', { args: null, + packageManager: 'gradle', + file: 'build.gradle', + path: 'gradle-app', }, ], 'calls gradle plugin', @@ -793,6 +798,9 @@ test('`monitor gradle-app --all-sub-projects`', async (t) => { { allSubProjects: true, args: null, + file: 'build.gradle', + packageManager: 'gradle', + path: 'gradle-app', }, ], 'calls gradle plugin', @@ -844,6 +852,9 @@ test('`monitor gradle-app pip-app --all-sub-projects`', async (t) => { { allSubProjects: true, args: null, + file: 'build.gradle', + packageManager: 'gradle', + path: 'gradle-app', }, ], 'calls plugin for the 1st path', @@ -856,6 +867,9 @@ test('`monitor gradle-app pip-app --all-sub-projects`', async (t) => { { allSubProjects: true, args: null, + file: 'requirements.txt', + packageManager: 'pip', + path: 'pip-app', }, ], 'calls plugin for the 2nd path', @@ -931,6 +945,8 @@ test('`monitor golang-gomodules --file=go.mod', async (t) => { { args: null, file: 'go.mod', + packageManager: 'gomodules', + path: 'golang-gomodules', }, ], 'calls golang plugin', @@ -977,6 +993,8 @@ test('`monitor golang-app --file=Gopkg.lock', async (t) => { { args: null, file: 'Gopkg.lock', + packageManager: 'golangdep', + path: 'golang-app', }, ], 'calls golang plugin', @@ -1023,6 +1041,8 @@ test('`monitor golang-app --file=vendor/vendor.json`', async (t) => { { args: null, file: 'vendor/vendor.json', + packageManager: 'govendor', + path: 'golang-app', }, ], 'calls golang plugin', @@ -1066,6 +1086,9 @@ test('`monitor cocoapods-app (autodetect)`', async (t) => { 'Podfile', { args: null, + file: 'Podfile', + packageManager: 'cocoapods', + path: './', }, ], 'calls CocoaPods plugin', @@ -1112,6 +1135,8 @@ test('`monitor cocoapods-app --file=Podfile`', async (t) => { { args: null, file: 'Podfile', + packageManager: 'cocoapods', + path: './', }, ], 'calls CocoaPods plugin', @@ -1158,6 +1183,8 @@ test('`monitor cocoapods-app --file=Podfile.lock`', async (t) => { { args: null, file: 'Podfile.lock', + packageManager: 'cocoapods', + path: './', }, ], 'calls CocoaPods plugin', @@ -1233,7 +1260,10 @@ test('`monitor foo:latest --docker`', async (t) => { { args: null, docker: true, + file: null, org: 'explicit-org', + packageManager: null, + path: 'foo:latest', }, ], 'calls docker plugin with expected arguments', @@ -1284,6 +1314,8 @@ test('`monitor foo:latest --docker --file=Dockerfile`', async (t) => { docker: true, file: 'Dockerfile', org: 'explicit-org', + packageManager: null, + path: 'foo:latest', }, ], 'calls docker plugin with expected arguments', @@ -1326,7 +1358,10 @@ test('`monitor foo:latest --docker` doesnt send policy from cwd', async (t) => { { args: null, docker: true, + file: null, org: 'explicit-org', + packageManager: null, + path: 'foo:latest', }, ], 'calls docker plugin with expected arguments', @@ -1382,8 +1417,11 @@ test('`monitor foo:latest --docker` with custom policy path', async (t) => { { args: null, docker: true, + file: null, org: 'explicit-org', 'policy-path': 'custom-location', + packageManager: null, + path: 'foo:latest', }, ], 'calls docker plugin with expected arguments', From 7491df638ee2bdcbf818d2f1511161f4853eeea4 Mon Sep 17 00:00:00 2001 From: Liliana Kastilio Date: Mon, 30 Dec 2019 11:52:21 +0000 Subject: [PATCH 2/5] feat: monitor --all-projects feature & tests Co-authored-by: dkontorovskyy --- package.json | 2 +- src/cli/commands/monitor/index.ts | 39 ++-- src/lib/monitor/index.ts | 10 +- .../cli-monitor.acceptance.test.ts | 13 ++ .../cli-monitor.all-projects.spec.ts | 174 ++++++++++++++++++ .../cli-monitor/cli-test.all-projects.spec.ts | 69 +++++++ .../cli-test/cli-test.all-projects.spec.ts | 2 +- test/acceptance/fake-server.ts | 10 +- 8 files changed, 298 insertions(+), 21 deletions(-) create mode 100644 test/acceptance/cli-monitor/cli-monitor.all-projects.spec.ts create mode 100644 test/acceptance/cli-monitor/cli-test.all-projects.spec.ts diff --git a/package.json b/package.json index 8e16236aee..7a45aab782 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "format": "prettier --write '{src,test,scripts}/**/*.{js,ts}'", "prepare": "npm run build", "test:common": "npm run check-tests && npm run build && npm run lint && node --require ts-node/register src/cli test --org=snyk", - "test:acceptance": "tap test/acceptance/**/*.test.* -Rspec --timeout=300 --node-arg=-r --node-arg=ts-node/register", + "test:acceptance": "tap test/acceptance/**/*monitor*.test.* -Rspec --timeout=300 --node-arg=-r --node-arg=ts-node/register", "test:system": "tap test/system/*.test.* -Rspec --timeout=300 --node-arg=-r --node-arg=ts-node/register", "test:test": "tap test/*.test.* -Rspec --timeout=300 --node-arg=-r --node-arg=ts-node/register", "test": "npm run test:common && npm run test:acceptance && npm run test:system && npm run test:test", diff --git a/src/cli/commands/monitor/index.ts b/src/cli/commands/monitor/index.ts index 6dcb95f701..44e26f1e94 100644 --- a/src/cli/commands/monitor/index.ts +++ b/src/cli/commands/monitor/index.ts @@ -22,6 +22,8 @@ import { getSubProjectCount } from '../../../lib/plugins/get-sub-project-count'; import { processJsonMonitorResponse } from './process-json-monitor'; import { GoodResult, BadResult } from './types'; import { getDepsFromPlugin } from '../../../lib/plugins/get-deps-from-plugin'; +import { ScannedProjectCustom } from '../../../lib/plugins/get-multi-plugin-result'; +import { MultiProjectResult, InspectResult } from '@snyk/cli-interface/legacy/plugin'; const SEPARATOR = '\n-------------------------------------------------------\n'; const debug = Debug('snyk'); @@ -76,8 +78,11 @@ async function monitor(...args0: MethodArgs): Promise { debug(`Processing ${path}...`); try { await validateMonitorPath(path, options.docker); - - let packageManager = detect.detectPackageManager(path, options); + const guessedPackageManager = detect.detectPackageManager(path, options); + const analysisType = + (options.docker + ? 'docker' + : guessedPackageManager) || 'all'; const targetFile = !options.scanAllUnmanaged && options.docker && !options.file // snyk monitor --docker (without --file) @@ -89,8 +94,6 @@ async function monitor(...args0: MethodArgs): Promise { pathUtil.join(path, targetFile || ''), ); - const analysisType = options.docker ? 'docker' : packageManager; - const analyzingDepsSpinnerLabel = 'Analyzing ' + analysisType + ' dependencies for ' + displayPath; @@ -98,14 +101,13 @@ async function monitor(...args0: MethodArgs): Promise { // Scan the project dependencies via a plugin - analytics.add('packageManager', packageManager); analytics.add('pluginOptions', options); debug('getDepsFromPlugin ...'); // each plugin will be asked to scan once per path // some return single InspectResult & newer ones return Multi const inspectResult = await promiseOrCleanup( - getDepsFromPlugin(path, { ...options, path, packageManager }), + getDepsFromPlugin(path, { ...options, path, packageManager: guessedPackageManager }), spinner.clear(analyzingDepsSpinnerLabel), ); @@ -181,7 +183,7 @@ async function monitor(...args0: MethodArgs): Promise { res, options, projectName, - foundProjectCount, + getSubProjectCount(inspectResult), ); results.push({ ok: true, data: monOutput, path, projectName }); } @@ -225,10 +227,25 @@ async function monitor(...args0: MethodArgs): Promise { function convertMultiPluginResultToSingle( result: pluginApi.MultiProjectResult, ): pluginApi.SinglePackageResult[] { - return result.scannedProjects.map((scannedProject) => ({ - plugin: result.plugin, - package: scannedProject.depTree, - })); + return result.scannedProjects.map((scannedProject) => { + return { + plugin: result.plugin, + package: scannedProject.depTree, + }; +}); +} + +function generateMonitorMeta(options, packageManager?): MonitorMeta { + return { + method: 'cli', + packageManager, + 'policy-path': options['policy-path'], + 'project-name': options['project-name'] || config.PROJECT_NAME, + isDocker: !!options.docker, + prune: !!options['prune-repeated-subdependencies'], + 'experimental-dep-graph': !!options['experimental-dep-graph'], + 'remote-repo-url': options['remote-repo-url'], + }; } async function validateMonitorPath(path, isDocker) { diff --git a/src/lib/monitor/index.ts b/src/lib/monitor/index.ts index d6adf973ae..544834cf41 100644 --- a/src/lib/monitor/index.ts +++ b/src/lib/monitor/index.ts @@ -1,5 +1,6 @@ import * as Debug from 'debug'; import * as depGraphLib from '@snyk/dep-graph'; +import * as cliInterface from '@snyk/cli-interface'; import * as snyk from '..'; import { apiTokenExists } from '../api-token'; import request = require('../request'); @@ -8,7 +9,7 @@ import * as os from 'os'; import * as _ from 'lodash'; import { isCI } from '../is-ci'; import * as analytics from '../analytics'; -import { DepTree, MonitorMeta, MonitorResult } from '../types'; +import { DepTree, MonitorMeta, MonitorResult, PluginMetadata } from '../types'; import * as projectMetadata from '../project-metadata'; import * as path from 'path'; import { @@ -24,8 +25,6 @@ import { filterOutMissingDeps } from './filter-out-missing-deps'; import { dropEmptyDeps } from './drop-empty-deps'; import { pruneTree } from './prune-dep-tree'; import { pluckPolicies } from '../policy'; -import { ScannedProject } from '@snyk/cli-interface/legacy/common'; -import { PluginMetadata } from '@snyk/cli-interface/legacy/plugin'; const debug = Debug('snyk'); @@ -62,7 +61,7 @@ interface Meta { export async function monitor( root: string, meta: MonitorMeta, - scannedProject: ScannedProject, + scannedProject: cliInterface.legacyCommon.ScannedProject, options, pluginMeta: PluginMetadata, targetFile?: string, @@ -175,6 +174,7 @@ export async function monitor( // we take the targetFile from the plugin, // because we want to send it only for specific package-managers target, + // WARNING: be careful changing this as it affects project uniqueness targetFile: pluginMeta.targetFile, targetFileRelativePath, } as MonitorBody, @@ -212,7 +212,7 @@ export async function monitor( export async function monitorGraph( root: string, meta: MonitorMeta, - scannedProject: ScannedProject, + scannedProject: cliInterface.legacyCommon.ScannedProject, pluginMeta: PluginMetadata, targetFile?: string, ): Promise { diff --git a/test/acceptance/cli-monitor/cli-monitor.acceptance.test.ts b/test/acceptance/cli-monitor/cli-monitor.acceptance.test.ts index 2c7e7a8b62..555ad6276e 100644 --- a/test/acceptance/cli-monitor/cli-monitor.acceptance.test.ts +++ b/test/acceptance/cli-monitor/cli-monitor.acceptance.test.ts @@ -12,6 +12,7 @@ import * as userConfig from '../../../src/lib/user-config'; // ensure this is required *after* the demo server, since this will // configure our fake configuration too import * as snykPolicy from 'snyk-policy'; +import { AllProjectsTests } from './cli-test.all-projects.spec'; const { test, only } = tap; (tap as any).runOnly = false; // <- for debug. set to true, and replace a test to only(..) @@ -63,6 +64,18 @@ before('prime config', async (t) => { t.end(); }); +test(AllProjectsTests.language, async (t) => { + for (const testName of Object.keys(AllProjectsTests.tests)) { + t.test( + testName, + AllProjectsTests.tests[testName]( + { server, versionNumber, cli, plugins }, + { chdirWorkspaces }, + ), + ); + } +}); + /** * `monitor` */ diff --git a/test/acceptance/cli-monitor/cli-monitor.all-projects.spec.ts b/test/acceptance/cli-monitor/cli-monitor.all-projects.spec.ts new file mode 100644 index 0000000000..4b1b5177e5 --- /dev/null +++ b/test/acceptance/cli-monitor/cli-monitor.all-projects.spec.ts @@ -0,0 +1,174 @@ +import * as sinon from 'sinon'; + +interface AcceptanceTests { + language: string; + tests: { + [name: string]: any; + }; +} + +export const AllProjectsTests: AcceptanceTests = { + language: 'Mixed (Ruby & Npm & Maven)', + tests: { + '`monitor mono-repo-project with lockfiles --all-projects`': ( + params, + utils, + ) => async (t) => { + utils.chdirWorkspaces(); + const spyPlugin = sinon.spy(params.plugins, 'loadPlugin'); + t.teardown(spyPlugin.restore); + + const result = await params.cli.monitor('mono-repo-project', { + allProjects: true, + }); + t.ok(spyPlugin.withArgs('rubygems').calledOnce, 'calls rubygems plugin'); + t.ok(spyPlugin.withArgs('npm').calledOnce, 'calls npm plugin'); + t.ok(spyPlugin.withArgs('maven').calledOnce, 'calls maven plugin'); + // npm + t.match( + result, + 'npm/graph/some/project-id', + 'npm project was monitored (via graph endpoint)', + ); + // rubygems + t.match( + result, + 'rubygems/some/project-id', + 'rubygems project was monitored', + ); + + // maven + t.match(result, 'maven/some/project-id', 'maven project was monitored '); + // Pop all calls to server and filter out calls to `featureFlag` endpoint + const requests = params.server + .popRequests(4) + .filter((req) => req.url.includes('/monitor/')); + t.equal(requests.length, 3, 'Correct amount of monitor requests'); + + requests.forEach((req) => { + t.match(req.url, '/monitor/', 'puts at correct url'); + t.notOk(req.body.targetFile, "doesn't send the targetFile"); + t.equal(req.method, 'PUT', 'makes PUT request'); + t.equal( + req.headers['x-snyk-cli-version'], + params.versionNumber, + 'sends version number', + ); + }); + }, + '`monitor maven-multi-app --all-projects`': (params, utils) => async ( + t, + ) => { + utils.chdirWorkspaces(); + const spyPlugin = sinon.spy(params.plugins, 'loadPlugin'); + t.teardown(spyPlugin.restore); + + const result = await params.cli.monitor('maven-multi-app', { + allProjects: true, + }); + t.ok( + spyPlugin.withArgs('rubygems').notCalled, + 'did not call rubygems plugin', + ); + t.ok(spyPlugin.withArgs('npm').notCalled, 'did not call npm plugin'); + t.ok(spyPlugin.withArgs('maven').calledOnce, 'calls maven plugin'); + // maven + t.match(result, 'maven/some/project-id', 'maven project was monitored '); + + const request = params.server.popRequest(); + // TODO: bump this test to discover both pom.xml in the repo + // once we have depth increase released + t.ok(request, 'Monitor POST request'); + + t.match(request.url, '/monitor/', 'puts at correct url'); + t.notOk(request.body.targetFile, "doesn't send the targetFile"); + t.equal(request.method, 'PUT', 'makes PUT request'); + t.equal( + request.headers['x-snyk-cli-version'], + params.versionNumber, + 'sends version number', + ); + }, + '`monitor monorepo-bad-project --all-projects`': (params, utils) => async ( + t, + ) => { + utils.chdirWorkspaces(); + const spyPlugin = sinon.spy(params.plugins, 'loadPlugin'); + t.teardown(spyPlugin.restore); + + const result = await params.cli.monitor('monorepo-bad-project', { + allProjects: true, + }); + t.ok(spyPlugin.withArgs('rubygems').calledOnce, 'calls rubygems plugin'); + t.ok(spyPlugin.withArgs('yarn').calledOnce, 'calls npm plugin'); + t.ok(spyPlugin.withArgs('maven').notCalled, 'did not call maven plugin'); + + // rubygems + t.match( + result, + 'rubygems/some/project-id', + 'rubygems project was monitored', + ); + // yarn + // yarn project fails with OutOfSyncError, no monitor output shown + const request = params.server.popRequest(); + + t.ok(request, 'Monitor POST request'); + + t.match(request.url, '/monitor/', 'puts at correct url'); + t.notOk(request.body.targetFile, "doesn't send the targetFile"); + t.equal(request.method, 'PUT', 'makes PUT request'); + t.equal( + request.headers['x-snyk-cli-version'], + params.versionNumber, + 'sends version number', + ); + }, + '`monitor mono-repo-project with lockfiles --all-projects and without same meta`': ( + params, + utils, + ) => async (t) => { + utils.chdirWorkspaces(); + const spyPlugin = sinon.spy(params.plugins, 'loadPlugin'); + t.teardown(spyPlugin.restore); + + const result = await params.cli.monitor('mono-repo-project', { + allProjects: true, + }); + // Pop all calls to server and filter out calls to `featureFlag` endpoint + const requests = params.server + .popRequests(4) + .filter((req) => req.url.includes('/monitor/')); + + // TODO: Compare each with with a --file below + + // Ruby + const resultRuby = await params.cli.monitor('mono-repo-project', { + file: 'Gemfile.lock', + }); + const requestsRuby = params.server + .popRequests(2) + .filter((req) => req.url.includes('/monitor/')); + + // npm + const resultNpm = await params.cli.monitor('mono-repo-project', { + file: 'package-lock.json', + }); + const requestsNpm = params.server + .popRequests(2) + .filter((req) => req.url.includes('/monitor/')); + + // maven + const resultMaven = await params.cli.monitor('mono-repo-project', { + file: 'pom.xml', + }); + const requestsMaven = params.server + .popRequests(2) + .filter((req) => req.url.includes('/monitor/')); + + t.pass('TODO'); + }, + + // TODO: monitor with --json flag + }, +}; diff --git a/test/acceptance/cli-monitor/cli-test.all-projects.spec.ts b/test/acceptance/cli-monitor/cli-test.all-projects.spec.ts new file mode 100644 index 0000000000..5ba1977e0d --- /dev/null +++ b/test/acceptance/cli-monitor/cli-test.all-projects.spec.ts @@ -0,0 +1,69 @@ +import * as sinon from 'sinon'; + +interface AcceptanceTests { + language: string; + tests: { + [name: string]: any; + }; +} + +export const AllProjectsTests: AcceptanceTests = { + language: 'Mixed (Ruby & Npm & Maven)', + tests: { + '`monitor mono-repo-project with lockfiles --all-projects`': ( + params, + utils, + ) => async (t) => { + utils.chdirWorkspaces(); + const spyPlugin = sinon.spy(params.plugins, 'loadPlugin'); + t.teardown(spyPlugin.restore); + + const result = await params.cli.monitor('mono-repo-project', { + allProjects: true, + }); + t.ok(spyPlugin.withArgs('rubygems').calledOnce, 'calls rubygems plugin'); + t.ok(spyPlugin.withArgs('npm').calledOnce, 'calls npm plugin'); + t.ok(spyPlugin.withArgs('maven').calledOnce, 'calls maven plugin'); + + // TODO: check all results have the relevant packageManager set + // npm + t.match( + result, + 'npm/graph/some/project-id', + 'npm project was monitored (via graph endpoint)', + ); + // rubygems + t.match( + result, + 'rubygems/some/project-id', + 'rubygems project was monitored', + ); + + // maven + t.match( + result, + 'maven/graph/some/project-id', + 'maven project was monitored ', + ); + + // Pop all calls to server and filter out calls to `featureFlag` endpoint + const requests = params.server + .popRequests(6) + .filter((req) => req.url.includes('/monitor/')); + t.equal(requests.length, 3, 'Correct amount of monitor requests'); + + requests.forEach((req) => { + t.match(req.url, '/monitor/', 'puts at correct url'); + t.notOk(req.body.targetFile, "doesn't send the targetFile"); + t.equal(req.method, 'PUT', 'makes PUT request'); + t.equal( + req.headers['x-snyk-cli-version'], + params.versionNumber, + 'sends version number', + ); + }); + }, + // TODO: add a test that validates the same meta for each monitor + // call with & without using --all-projects + }, +}; diff --git a/test/acceptance/cli-test/cli-test.all-projects.spec.ts b/test/acceptance/cli-test/cli-test.all-projects.spec.ts index a7780310a8..08582e6d10 100644 --- a/test/acceptance/cli-test/cli-test.all-projects.spec.ts +++ b/test/acceptance/cli-test/cli-test.all-projects.spec.ts @@ -18,7 +18,7 @@ export const AllProjectsTests: AcceptanceTests = { }); t.ok(spyPlugin.withArgs('rubygems').calledOnce, 'calls rubygems plugin'); t.ok(spyPlugin.withArgs('npm').calledOnce, 'calls npm plugin'); - t.ok(spyPlugin.withArgs('maven').calledOnce, 'calls npm plugin'); + t.ok(spyPlugin.withArgs('maven').calledOnce, 'calls maven plugin'); params.server.popRequests(3).forEach((req) => { t.equal(req.method, 'POST', 'makes POST request'); diff --git a/test/acceptance/fake-server.ts b/test/acceptance/fake-server.ts index b7c0187de6..bb2b92ca15 100644 --- a/test/acceptance/fake-server.ts +++ b/test/acceptance/fake-server.ts @@ -20,7 +20,7 @@ export function fakeServer(root, apikey) { return server._reqLog.pop()!; }; server.popRequests = (num: number) => { - return server._reqLog.slice(server._reqLog.length - num, num); + return server._reqLog.splice(server._reqLog.length - num, num); }; server.use(restify.acceptParser(server.acceptable)); server.use(restify.queryParser()); @@ -144,14 +144,18 @@ export function fakeServer(root, apikey) { server.put(root + '/monitor/:registry/graph', (req, res, next) => { res.send({ - id: 'test', + id: 'monitor', + uri: `${req.params.registry}/graph/some/project-id`, + isMonitored: true, }); return next(); }); server.put(root + '/monitor/:registry', (req, res, next) => { res.send({ - id: 'test', + id: 'monitor', + uri: `${req.params.registry}/some/project-id`, + isMonitored: true, }); return next(); }); From a65d132781feac800220b3f3d596f10d0ac25e04 Mon Sep 17 00:00:00 2001 From: Liliana Kastilio Date: Mon, 30 Dec 2019 16:01:50 +0000 Subject: [PATCH 3/5] feat: bubble up package manager and always use multiple res --- package.json | 2 +- src/cli/commands/monitor/index.ts | 95 +- src/lib/monitor/index.ts | 12 +- ...onvert-multi-plugin-res-to-multi-custom.ts | 22 + .../convert-scanned-projects-to-custom.ts | 24 + ...vert-single-splugin-res-to-multi-custom.ts | 31 + src/lib/plugins/extract-package-manager.ts | 29 + src/lib/plugins/get-deps-from-plugin.ts | 62 +- src/lib/plugins/get-multi-plugin-result.ts | 37 +- src/lib/snyk-test/run-test.ts | 27 +- .../cli-monitor.acceptance.test.ts | 2 +- .../cli-monitor/cli-test.all-projects.spec.ts | 69 -- .../workspaces/monorepo-bad-project/Gemfile | 4 + .../monorepo-bad-project/Gemfile.lock | 15 + .../monorepo-bad-project/package.json | 19 + .../workspaces/monorepo-bad-project/yarn.lock | 813 ++++++++++++++++++ 16 files changed, 1056 insertions(+), 207 deletions(-) create mode 100644 src/lib/plugins/convert-multi-plugin-res-to-multi-custom.ts create mode 100644 src/lib/plugins/convert-scanned-projects-to-custom.ts create mode 100644 src/lib/plugins/convert-single-splugin-res-to-multi-custom.ts create mode 100644 src/lib/plugins/extract-package-manager.ts delete mode 100644 test/acceptance/cli-monitor/cli-test.all-projects.spec.ts create mode 100644 test/acceptance/workspaces/monorepo-bad-project/Gemfile create mode 100644 test/acceptance/workspaces/monorepo-bad-project/Gemfile.lock create mode 100644 test/acceptance/workspaces/monorepo-bad-project/package.json create mode 100644 test/acceptance/workspaces/monorepo-bad-project/yarn.lock diff --git a/package.json b/package.json index 7a45aab782..8e16236aee 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "format": "prettier --write '{src,test,scripts}/**/*.{js,ts}'", "prepare": "npm run build", "test:common": "npm run check-tests && npm run build && npm run lint && node --require ts-node/register src/cli test --org=snyk", - "test:acceptance": "tap test/acceptance/**/*monitor*.test.* -Rspec --timeout=300 --node-arg=-r --node-arg=ts-node/register", + "test:acceptance": "tap test/acceptance/**/*.test.* -Rspec --timeout=300 --node-arg=-r --node-arg=ts-node/register", "test:system": "tap test/system/*.test.* -Rspec --timeout=300 --node-arg=-r --node-arg=ts-node/register", "test:test": "tap test/*.test.* -Rspec --timeout=300 --node-arg=-r --node-arg=ts-node/register", "test": "npm run test:common && npm run test:acceptance && npm run test:system && npm run test:test", diff --git a/src/cli/commands/monitor/index.ts b/src/cli/commands/monitor/index.ts index 44e26f1e94..9b026a85b1 100644 --- a/src/cli/commands/monitor/index.ts +++ b/src/cli/commands/monitor/index.ts @@ -12,18 +12,26 @@ import * as spinner from '../../../lib/spinner'; import * as Debug from 'debug'; import * as detect from '../../../lib/detect'; -import { MonitorOptions, MonitorMeta } from '../../../lib/types'; +import { + MonitorOptions, + MonitorMeta, + MonitorResult, + PluginMetadata, + Options, +} from '../../../lib/types'; import { MethodArgs, ArgsOptions } from '../../args'; import { maybePrintDeps } from '../../../lib/print-deps'; import * as analytics from '../../../lib/analytics'; import { legacyPlugin as pluginApi } from '@snyk/cli-interface'; import { formatMonitorOutput } from './formatters/format-monitor-response'; -import { getSubProjectCount } from '../../../lib/plugins/get-sub-project-count'; import { processJsonMonitorResponse } from './process-json-monitor'; import { GoodResult, BadResult } from './types'; import { getDepsFromPlugin } from '../../../lib/plugins/get-deps-from-plugin'; -import { ScannedProjectCustom } from '../../../lib/plugins/get-multi-plugin-result'; -import { MultiProjectResult, InspectResult } from '@snyk/cli-interface/legacy/plugin'; +import { extractPackageManager } from '../../../lib/plugins/extract-package-manager'; +import { MultiProjectResultCustom } from '../../../lib/plugins/get-multi-plugin-result'; +import { getSubProjectCount } from '../../../lib/plugins/get-sub-project-count'; +import { convertSingleResultToMultiCustom } from '../../../lib/plugins/convert-single-splugin-res-to-multi-custom'; +import { convertMultiResultToMultiCustom } from '../../../lib/plugins/convert-multi-plugin-res-to-multi-custom'; const SEPARATOR = '\n-------------------------------------------------------\n'; const debug = Debug('snyk'); @@ -80,9 +88,7 @@ async function monitor(...args0: MethodArgs): Promise { await validateMonitorPath(path, options.docker); const guessedPackageManager = detect.detectPackageManager(path, options); const analysisType = - (options.docker - ? 'docker' - : guessedPackageManager) || 'all'; + (options.docker ? 'docker' : guessedPackageManager) || 'all'; const targetFile = !options.scanAllUnmanaged && options.docker && !options.file // snyk monitor --docker (without --file) @@ -107,74 +113,62 @@ async function monitor(...args0: MethodArgs): Promise { // each plugin will be asked to scan once per path // some return single InspectResult & newer ones return Multi const inspectResult = await promiseOrCleanup( - getDepsFromPlugin(path, { ...options, path, packageManager: guessedPackageManager }), + getDepsFromPlugin(path, { + ...options, + path, + packageManager: guessedPackageManager, + }), spinner.clear(analyzingDepsSpinnerLabel), ); analytics.add('pluginName', inspectResult.plugin.name); - await spinner.clear(analyzingDepsSpinnerLabel)(inspectResult); - const postingMonitorSpinnerLabel = 'Posting monitor snapshot for ' + displayPath + ' ...'; - await spinner(postingMonitorSpinnerLabel); - if (inspectResult.plugin.packageManager) { - packageManager = inspectResult.plugin.packageManager; - } - const monitorMeta: MonitorMeta = { - method: 'cli', - packageManager, - 'policy-path': options['policy-path'], - 'project-name': options['project-name'] || config.PROJECT_NAME, - isDocker: !!options.docker, - prune: !!options['prune-repeated-subdependencies'], - 'experimental-dep-graph': !!options['experimental-dep-graph'], - 'remote-repo-url': options['remote-repo-url'], - }; // We send results from "all-sub-projects" scanning as different Monitor objects // multi result will become default, so start migrating code to always work with it - let perProjectResult: pluginApi.MultiProjectResult; + let perProjectResult: MultiProjectResultCustom; let foundProjectCount; if (!pluginApi.isMultiResult(inspectResult)) { foundProjectCount = getSubProjectCount(inspectResult); - const { plugin, meta, package: depTree } = inspectResult; - perProjectResult = { - plugin, - scannedProjects: [ - { - depTree, - meta, - }, - ], - }; + perProjectResult = convertSingleResultToMultiCustom(inspectResult); } else { - perProjectResult = inspectResult; + perProjectResult = convertMultiResultToMultiCustom(inspectResult); } // Post the project dependencies to the Registry for (const projectDeps of perProjectResult.scannedProjects) { - debug( - `Processing ${projectDeps.depTree.name || targetFile || path}...`, + const packageManager = extractPackageManager( + projectDeps, + perProjectResult, + options as MonitorOptions & Options, ); + analytics.add('packageManager', packageManager); maybePrintDeps(options, projectDeps.depTree); - const res = await promiseOrCleanup( + debug(`Processing ${projectDeps.depTree.name}...`); + maybePrintDeps(options, projectDeps.depTree); + // TODO: not always correct for multi scan, + // targetFile needs to be derived from project scanned always + // this is not yet used anywhere in the system to leaving for now + const targetFileRelativePath = targetFile + ? pathUtil.join(pathUtil.resolve(path), targetFile) + : ''; + const res: MonitorResult = await promiseOrCleanup( snykMonitor( path, - monitorMeta, + generateMonitorMeta(options, packageManager), projectDeps, options, - perProjectResult.plugin, - targetFile, + perProjectResult.plugin as PluginMetadata, + targetFileRelativePath, ), spinner.clear(postingMonitorSpinnerLabel), ); - await spinner.clear(postingMonitorSpinnerLabel)(res); - res.path = path; const projectName = projectDeps.depTree.name; @@ -191,6 +185,8 @@ async function monitor(...args0: MethodArgs): Promise { } catch (err) { // push this error, the loop continues results.push({ ok: false, data: err, path }); + } finally { + spinner.clearAll(); } } // Part 2: process the output from the Registry @@ -224,17 +220,6 @@ async function monitor(...args0: MethodArgs): Promise { throw new Error(output); } -function convertMultiPluginResultToSingle( - result: pluginApi.MultiProjectResult, -): pluginApi.SinglePackageResult[] { - return result.scannedProjects.map((scannedProject) => { - return { - plugin: result.plugin, - package: scannedProject.depTree, - }; -}); -} - function generateMonitorMeta(options, packageManager?): MonitorMeta { return { method: 'cli', diff --git a/src/lib/monitor/index.ts b/src/lib/monitor/index.ts index 544834cf41..77c7f22a7e 100644 --- a/src/lib/monitor/index.ts +++ b/src/lib/monitor/index.ts @@ -64,7 +64,7 @@ export async function monitor( scannedProject: cliInterface.legacyCommon.ScannedProject, options, pluginMeta: PluginMetadata, - targetFile?: string, + targetFileRelativePath?: string, ): Promise { apiTokenExists(); let treeMissingDeps: string[] = []; @@ -92,7 +92,7 @@ export async function monitor( meta, scannedProject, pluginMeta, - targetFile, + targetFileRelativePath, ); } if (monitorGraphSupportedRes.userMessage) { @@ -131,9 +131,6 @@ export async function monitor( const policy = await snyk.policy.load(policyLocations, { loose: true }); const target = await projectMetadata.getInfo(pkg, meta); - const targetFileRelativePath = targetFile - ? path.join(path.resolve(root), targetFile) - : ''; if (target && target.branch) { analytics.add('targetBranch', target.branch); @@ -214,7 +211,7 @@ export async function monitorGraph( meta: MonitorMeta, scannedProject: cliInterface.legacyCommon.ScannedProject, pluginMeta: PluginMetadata, - targetFile?: string, + targetFileRelativePath?: string, ): Promise { const packageManager = meta.packageManager; analytics.add('monitorGraph', true); @@ -244,9 +241,6 @@ export async function monitorGraph( const policy = await snyk.policy.load(policyLocations, { loose: true }); const target = await projectMetadata.getInfo(pkg, meta); - const targetFileRelativePath = targetFile - ? path.join(path.resolve(root), targetFile) - : ''; if (target && target.branch) { analytics.add('targetBranch', target.branch); diff --git a/src/lib/plugins/convert-multi-plugin-res-to-multi-custom.ts b/src/lib/plugins/convert-multi-plugin-res-to-multi-custom.ts new file mode 100644 index 0000000000..2975bd1bce --- /dev/null +++ b/src/lib/plugins/convert-multi-plugin-res-to-multi-custom.ts @@ -0,0 +1,22 @@ +import { legacyPlugin as pluginApi } from '@snyk/cli-interface'; +import { MultiProjectResultCustom } from './get-multi-plugin-result'; +import { convertScannedProjectsToCustom } from './convert-scanned-projects-to-custom'; +import { SupportedPackageManagers } from '../package-managers'; + +export function convertMultiResultToMultiCustom( + inspectRes: pluginApi.MultiProjectResult, + packageManager?: SupportedPackageManagers, + targetFile?: string, +): MultiProjectResultCustom { + // convert all results from the same plugin to MultiProjectResultCustom + // and annotate each scannedProject with packageManager + return { + plugin: inspectRes.plugin, + scannedProjects: convertScannedProjectsToCustom( + inspectRes.scannedProjects, + (inspectRes.plugin.packageManager as SupportedPackageManagers) || + packageManager, + targetFile, + ), + }; +} diff --git a/src/lib/plugins/convert-scanned-projects-to-custom.ts b/src/lib/plugins/convert-scanned-projects-to-custom.ts new file mode 100644 index 0000000000..544b1b609e --- /dev/null +++ b/src/lib/plugins/convert-scanned-projects-to-custom.ts @@ -0,0 +1,24 @@ +import * as cliInterface from '@snyk/cli-interface'; +import { ScannedProjectCustom } from './get-multi-plugin-result'; +import { SupportedPackageManagers } from '../package-managers'; + +export function convertScannedProjectsToCustom( + scannedProjects: cliInterface.legacyCommon.ScannedProject[], + packageManager?: SupportedPackageManagers, + targetFile?: string, +): ScannedProjectCustom[] { + // annotate the package manager & targetFile to be used + // for test & monitor + const customScannedProject: ScannedProjectCustom[] = scannedProjects.map( + (a) => { + (a as ScannedProjectCustom).targetFile = targetFile; + (a as ScannedProjectCustom).packageManager = (a as ScannedProjectCustom) + .packageManager + ? (a as ScannedProjectCustom).packageManager + : (packageManager as SupportedPackageManagers); + return a as ScannedProjectCustom; + }, + ); + + return customScannedProject; +} diff --git a/src/lib/plugins/convert-single-splugin-res-to-multi-custom.ts b/src/lib/plugins/convert-single-splugin-res-to-multi-custom.ts new file mode 100644 index 0000000000..bc9ae476d9 --- /dev/null +++ b/src/lib/plugins/convert-single-splugin-res-to-multi-custom.ts @@ -0,0 +1,31 @@ +import { legacyPlugin as pluginApi } from '@snyk/cli-interface'; +import { MultiProjectResultCustom } from './get-multi-plugin-result'; +import { SupportedPackageManagers } from '../package-managers'; + +export function convertSingleResultToMultiCustom( + inspectRes: pluginApi.SinglePackageResult, + packageManager?: SupportedPackageManagers, +): MultiProjectResultCustom { + if (!inspectRes.package.targetFile && inspectRes.plugin) { + inspectRes.package.targetFile = inspectRes.plugin.targetFile; + } + const { plugin, meta, package: depTree } = inspectRes; + + if (!depTree.targetFile && plugin) { + depTree.targetFile = plugin.targetFile; + } + + return { + plugin, + scannedProjects: [ + { + depTree, + meta, + targetFile: plugin.targetFile, + packageManager: + (inspectRes.plugin.packageManager as SupportedPackageManagers) || + packageManager, + }, + ], + }; +} diff --git a/src/lib/plugins/extract-package-manager.ts b/src/lib/plugins/extract-package-manager.ts new file mode 100644 index 0000000000..2b0787da46 --- /dev/null +++ b/src/lib/plugins/extract-package-manager.ts @@ -0,0 +1,29 @@ +import * as cliInterface from '@snyk/cli-interface'; +import { ScannedProjectCustom } from './get-multi-plugin-result'; +import { SupportedPackageManagers } from '../package-managers'; + +export function extractPackageManager( + scannedProject: ScannedProjectCustom, + pluginRes: cliInterface.legacyPlugin.MultiProjectResult, + options: { + packageManager?: SupportedPackageManagers; + }, +): SupportedPackageManagers | undefined { + // try and use the package Manager from the plugin + // result if present + const packageManager = + scannedProject.packageManager || + (pluginRes.plugin && pluginRes.plugin.packageManager); + + if (packageManager) { + return packageManager; + } + + if (!packageManager && options.packageManager) { + // fallback to Options packageManager + return options.packageManager; + } + + // for example: docker + return undefined; +} diff --git a/src/lib/plugins/get-deps-from-plugin.ts b/src/lib/plugins/get-deps-from-plugin.ts index 5ccfd93043..cb617bc832 100644 --- a/src/lib/plugins/get-deps-from-plugin.ts +++ b/src/lib/plugins/get-deps-from-plugin.ts @@ -11,6 +11,8 @@ import { detectPackageManagerFromFile, } from '../detect'; import analytics = require('../analytics'); +import { convertSingleResultToMultiCustom } from './convert-single-splugin-res-to-multi-custom'; +import { convertMultiResultToMultiCustom } from './convert-multi-plugin-res-to-multi-custom'; const debug = debugModule('snyk'); @@ -42,51 +44,35 @@ export async function getDepsFromPlugin( }; analytics.add('allProjects', analyticData); return inspectRes; - } else { - // TODO: is this needed for the auto detect handling above? - // don't override options.file if scanning multiple files at once - if (!options.scanAllUnmanaged) { - options.file = options.file || detectPackageFile(root); - } - if (!options.docker && !(options.file || options.packageManager)) { - throw NoSupportedManifestsFoundError([...root]); - } - inspectRes = await getSinglePluginResult(root, options); } + // TODO: is this needed for the auto detect handling above? + // don't override options.file if scanning multiple files at once + if (!options.scanAllUnmanaged) { + options.file = options.file || detectPackageFile(root); + } + if (!options.docker && !(options.file || options.packageManager)) { + throw NoSupportedManifestsFoundError([...root]); + } + inspectRes = await getSinglePluginResult(root, options); + if (!pluginApi.isMultiResult(inspectRes)) { if (!inspectRes.package) { // something went wrong if both are not present... throw Error( - `error getting dependencies from ${options.packageManager} ` + - "plugin: neither 'package' nor 'scannedProjects' were found", + `error getting dependencies from ${ + options.docker ? 'docker' : options.packageManager + } ` + "plugin: neither 'package' nor 'scannedProjects' were found", ); } - if (!inspectRes.package.targetFile && inspectRes.plugin) { - inspectRes.package.targetFile = inspectRes.plugin.targetFile; - } - // We are using "options" to store some information returned from plugin that we need to use later, - // but don't want to send to Registry in the Payload. - // TODO(kyegupov): decouple inspect and payload so that we don't need this hack - if ( - inspectRes.plugin.meta && - inspectRes.plugin.meta.allSubProjectNames && - inspectRes.plugin.meta.allSubProjectNames.length > 1 - ) { - options.advertiseSubprojectsCount = - inspectRes.plugin.meta.allSubProjectNames.length; - } - return { - plugin: inspectRes.plugin, - scannedProjects: [{ depTree: inspectRes.package }], - }; - } else { - // We are using "options" to store some information returned from plugin that we need to use later, - // but don't want to send to Registry in the Payload. - // TODO(kyegupov): decouple inspect and payload so that we don't need this hack - (options as any).projectNames = inspectRes.scannedProjects.map( - (scannedProject) => scannedProject.depTree.name, - ); - return inspectRes; + + return convertSingleResultToMultiCustom(inspectRes, options.packageManager); } + // We are using "options" to store some information returned from plugin that we need to use later, + // but don't want to send to Registry in the Payload. + // TODO(kyegupov): decouple inspect and payload so that we don't need this hack + (options as any).projectNames = inspectRes.scannedProjects.map( + (scannedProject) => scannedProject.depTree.name, + ); + return convertMultiResultToMultiCustom(inspectRes, options.packageManager); } diff --git a/src/lib/plugins/get-multi-plugin-result.ts b/src/lib/plugins/get-multi-plugin-result.ts index 33ba31f83a..6aec915a97 100644 --- a/src/lib/plugins/get-multi-plugin-result.ts +++ b/src/lib/plugins/get-multi-plugin-result.ts @@ -6,17 +6,23 @@ import { TestOptions, Options, MonitorOptions } from '../types'; import { detectPackageManagerFromFile } from '../detect'; import { SupportedPackageManagers } from '../package-managers'; import { getSinglePluginResult } from './get-single-plugin-result'; +import { convertSingleResultToMultiCustom } from './convert-single-splugin-res-to-multi-custom'; +import { convertMultiResultToMultiCustom } from './convert-multi-plugin-res-to-multi-custom'; export interface ScannedProjectCustom extends cliInterface.legacyCommon.ScannedProject { packageManager: SupportedPackageManagers; } +export interface MultiProjectResultCustom + extends cliInterface.legacyPlugin.MultiProjectResult { + scannedProjects: ScannedProjectCustom[]; +} export async function getMultiPluginResult( root: string, options: Options & (TestOptions | MonitorOptions), targetFiles: string[], -): Promise { +): Promise { const allResults: ScannedProjectCustom[] = []; for (const targetFile of targetFiles) { const optionsClone = _.cloneDeep(options); @@ -33,34 +39,27 @@ export async function getMultiPluginResult( let resultWithScannedProjects: cliInterface.legacyPlugin.MultiProjectResult; if (!cliInterface.legacyPlugin.isMultiResult(inspectRes)) { - resultWithScannedProjects = { - plugin: inspectRes.plugin, - scannedProjects: [ - { - depTree: inspectRes.package, - targetFile: inspectRes.plugin.targetFile, - meta: inspectRes.meta, - }, - ], - }; + resultWithScannedProjects = convertSingleResultToMultiCustom( + inspectRes, + optionsClone.packageManager, + ); } else { resultWithScannedProjects = inspectRes; } + const pluginResultWithCustomScannedProjects = convertMultiResultToMultiCustom( + resultWithScannedProjects, + optionsClone.packageManager, + optionsClone.file, + ); // annotate the package manager, project name & targetFile to be used // for test & monitor // TODO: refactor how we display meta to not have to do this (options as any).projectNames = resultWithScannedProjects.scannedProjects.map( (scannedProject) => scannedProject.depTree.name, ); - const customScannedProject: ScannedProjectCustom[] = resultWithScannedProjects.scannedProjects.map( - (a) => { - (a as ScannedProjectCustom).targetFile = optionsClone.file; - (a as ScannedProjectCustom).packageManager = optionsClone.packageManager!; - return a as ScannedProjectCustom; - }, - ); - allResults.push(...customScannedProject); + + allResults.push(...pluginResultWithCustomScannedProjects.scannedProjects); } catch (err) { console.log(err); } diff --git a/src/lib/snyk-test/run-test.ts b/src/lib/snyk-test/run-test.ts index 15f1a139ef..dcb688b8ff 100644 --- a/src/lib/snyk-test/run-test.ts +++ b/src/lib/snyk-test/run-test.ts @@ -37,6 +37,7 @@ import { ScannedProjectCustom } from '../plugins/get-multi-plugin-result'; import request = require('../request'); import spinner = require('../spinner'); +import { extractPackageManager } from '../plugins/extract-package-manager'; import { getSubProjectCount } from '../plugins/get-sub-project-count'; const debug = debugModule('snyk'); @@ -92,7 +93,6 @@ async function runTest( _.get(payload, 'body.projectNameOverride') || _.get(payload, 'body.originalProjectName'); const foundProjectCount = _.get(payload, 'body.foundProjectCount'); - let dockerfilePackages; if ( payload.body && @@ -184,6 +184,7 @@ async function runTest( } res.uniqueCount = countUniqueVulns(res.vulnerabilities); + const result = { ...res, targetFile, @@ -298,14 +299,7 @@ async function assembleLocalPayloads( maybePrintDeps(options, pkg); } const project = scannedProject as ScannedProjectCustom; - const packageManager = - project.packageManager || (deps.plugin && deps.plugin.packageManager); - if (packageManager) { - (options as any).packageManager = packageManager; - } - if (deps.plugin && deps.plugin.packageManager) { - (options as any).packageManager = deps.plugin.packageManager; - } + const packageManager = extractPackageManager(project, deps, options); if (pkg.docker) { const baseImageFromDockerfile = pkg.docker.baseImage; @@ -324,13 +318,16 @@ async function assembleLocalPayloads( policyLocations = policyLocations.filter((loc) => { return loc !== root; }); - } else if (['npm', 'yarn'].indexOf(options.packageManager!) > -1) { + } else if ( + packageManager && + ['npm', 'yarn'].indexOf(packageManager) > -1 + ) { policyLocations = policyLocations.concat(pluckPolicies(pkg)); } debug('policies found', policyLocations); analytics.add('policies', policyLocations.length); - analytics.add('packageManager', options.packageManager); + analytics.add('packageManager', packageManager); addPackageAnalytics(pkg); let policy; @@ -372,18 +369,18 @@ async function assembleLocalPayloads( }); let depGraph = await depGraphLib.legacy.depTreeToGraph( pkg, - options.packageManager!, + packageManager!, ); debug('done converting dep-tree to dep-graph', { uniquePkgsCount: depGraph.getPkgs().length, }); - if (options['prune-repeated-subdependencies']) { + if (options['prune-repeated-subdependencies'] && packageManager) { debug('Trying to prune the graph'); const prePruneDepCount = countPathsToGraphRoot(depGraph); debug('pre prunedPathsCount: ' + prePruneDepCount); - depGraph = await pruneGraph(depGraph, options.packageManager!); + depGraph = await pruneGraph(depGraph, packageManager); analytics.add('prePrunedPathsCount', prePruneDepCount); const postPruneDepCount = countPathsToGraphRoot(depGraph); @@ -405,7 +402,7 @@ async function assembleLocalPayloads( body, }; - if (['yarn', 'npm'].indexOf(options.packageManager!) !== -1) { + if (packageManager && ['yarn', 'npm'].indexOf(packageManager) !== -1) { const isLockFileBased = targetFile && (targetFile.endsWith('package-lock.json') || diff --git a/test/acceptance/cli-monitor/cli-monitor.acceptance.test.ts b/test/acceptance/cli-monitor/cli-monitor.acceptance.test.ts index 555ad6276e..c5f6a56ba3 100644 --- a/test/acceptance/cli-monitor/cli-monitor.acceptance.test.ts +++ b/test/acceptance/cli-monitor/cli-monitor.acceptance.test.ts @@ -12,7 +12,7 @@ import * as userConfig from '../../../src/lib/user-config'; // ensure this is required *after* the demo server, since this will // configure our fake configuration too import * as snykPolicy from 'snyk-policy'; -import { AllProjectsTests } from './cli-test.all-projects.spec'; +import { AllProjectsTests } from './cli-monitor.all-projects.spec'; const { test, only } = tap; (tap as any).runOnly = false; // <- for debug. set to true, and replace a test to only(..) diff --git a/test/acceptance/cli-monitor/cli-test.all-projects.spec.ts b/test/acceptance/cli-monitor/cli-test.all-projects.spec.ts deleted file mode 100644 index 5ba1977e0d..0000000000 --- a/test/acceptance/cli-monitor/cli-test.all-projects.spec.ts +++ /dev/null @@ -1,69 +0,0 @@ -import * as sinon from 'sinon'; - -interface AcceptanceTests { - language: string; - tests: { - [name: string]: any; - }; -} - -export const AllProjectsTests: AcceptanceTests = { - language: 'Mixed (Ruby & Npm & Maven)', - tests: { - '`monitor mono-repo-project with lockfiles --all-projects`': ( - params, - utils, - ) => async (t) => { - utils.chdirWorkspaces(); - const spyPlugin = sinon.spy(params.plugins, 'loadPlugin'); - t.teardown(spyPlugin.restore); - - const result = await params.cli.monitor('mono-repo-project', { - allProjects: true, - }); - t.ok(spyPlugin.withArgs('rubygems').calledOnce, 'calls rubygems plugin'); - t.ok(spyPlugin.withArgs('npm').calledOnce, 'calls npm plugin'); - t.ok(spyPlugin.withArgs('maven').calledOnce, 'calls maven plugin'); - - // TODO: check all results have the relevant packageManager set - // npm - t.match( - result, - 'npm/graph/some/project-id', - 'npm project was monitored (via graph endpoint)', - ); - // rubygems - t.match( - result, - 'rubygems/some/project-id', - 'rubygems project was monitored', - ); - - // maven - t.match( - result, - 'maven/graph/some/project-id', - 'maven project was monitored ', - ); - - // Pop all calls to server and filter out calls to `featureFlag` endpoint - const requests = params.server - .popRequests(6) - .filter((req) => req.url.includes('/monitor/')); - t.equal(requests.length, 3, 'Correct amount of monitor requests'); - - requests.forEach((req) => { - t.match(req.url, '/monitor/', 'puts at correct url'); - t.notOk(req.body.targetFile, "doesn't send the targetFile"); - t.equal(req.method, 'PUT', 'makes PUT request'); - t.equal( - req.headers['x-snyk-cli-version'], - params.versionNumber, - 'sends version number', - ); - }); - }, - // TODO: add a test that validates the same meta for each monitor - // call with & without using --all-projects - }, -}; diff --git a/test/acceptance/workspaces/monorepo-bad-project/Gemfile b/test/acceptance/workspaces/monorepo-bad-project/Gemfile new file mode 100644 index 0000000000..e0f77a325f --- /dev/null +++ b/test/acceptance/workspaces/monorepo-bad-project/Gemfile @@ -0,0 +1,4 @@ +source :rubygems + +gem "json" +gem "lynx", "0.4.0" diff --git a/test/acceptance/workspaces/monorepo-bad-project/Gemfile.lock b/test/acceptance/workspaces/monorepo-bad-project/Gemfile.lock new file mode 100644 index 0000000000..1664b0b0b5 --- /dev/null +++ b/test/acceptance/workspaces/monorepo-bad-project/Gemfile.lock @@ -0,0 +1,15 @@ +GEM + remote: http://rubygems.org/ + specs: + json (2.0.2) + lynx (0.4.0) + +PLATFORMS + ruby + +DEPENDENCIES + json + lynx (= 0.4.0) + +BUNDLED WITH + 1.13.5 diff --git a/test/acceptance/workspaces/monorepo-bad-project/package.json b/test/acceptance/workspaces/monorepo-bad-project/package.json new file mode 100644 index 0000000000..14f1eefb88 --- /dev/null +++ b/test/acceptance/workspaces/monorepo-bad-project/package.json @@ -0,0 +1,19 @@ +{ + "name": "npm-package", + "version": "1.0.0", + "description": "Simple NPM package", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "snyk", + "license": "ISC", + "dependencies": { + "to-array": "0.1.4", + "snyk": "*" + }, + "devDependencies": { + "object-assign": "4.1.1", + "rewire": "^4.0.1" + } +} diff --git a/test/acceptance/workspaces/monorepo-bad-project/yarn.lock b/test/acceptance/workspaces/monorepo-bad-project/yarn.lock new file mode 100644 index 0000000000..0722eae3dd --- /dev/null +++ b/test/acceptance/workspaces/monorepo-bad-project/yarn.lock @@ -0,0 +1,813 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +acorn-jsx@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-3.0.1.tgz#afdf9488fb1ecefc8348f6fb22f464e32a58b36b" + dependencies: + acorn "^3.0.4" + +acorn@^3.0.4: + version "3.3.0" + resolved "http://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a" + +acorn@^5.5.0: + version "5.7.3" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.3.tgz#67aa231bf8812974b85235a96771eb6bd07ea279" + +ajv-keywords@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.1.tgz#617997fc5f60576894c435f940d819e135b80762" + +ajv@^5.2.3, ajv@^5.3.0: + version "5.5.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965" + dependencies: + co "^4.6.0" + fast-deep-equal "^1.0.0" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.3.0" + +ansi-escapes@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.1.0.tgz#f73207bb81207d75fd6c83f125af26eea378ca30" + +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + +ansi-styles@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + dependencies: + color-convert "^1.9.0" + +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + dependencies: + sprintf-js "~1.0.2" + +array-union@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" + dependencies: + array-uniq "^1.0.1" + +array-uniq@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" + +arrify@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" + +babel-code-frame@^6.22.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" + dependencies: + chalk "^1.1.3" + esutils "^2.0.2" + js-tokens "^3.0.2" + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +buffer-from@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" + +caller-path@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f" + dependencies: + callsites "^0.2.0" + +callsites@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-0.2.0.tgz#afab96262910a7f33c19a5775825c69f34e350ca" + +chalk@^1.1.3: + version "1.1.3" + resolved "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" + dependencies: + ansi-styles "^2.2.1" + escape-string-regexp "^1.0.2" + has-ansi "^2.0.0" + strip-ansi "^3.0.0" + supports-color "^2.0.0" + +chalk@^2.0.0, chalk@^2.1.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e" + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chardet@^0.4.0: + version "0.4.2" + resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.4.2.tgz#b5473b33dc97c424e5d98dc87d55d4d8a29c8bf2" + +circular-json@^0.3.1: + version "0.3.3" + resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66" + +cli-cursor@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5" + dependencies: + restore-cursor "^2.0.0" + +cli-width@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639" + +co@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + dependencies: + color-name "1.1.3" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + +concat-stream@^1.6.0: + version "1.6.2" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + +core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + +cross-spawn@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" + dependencies: + lru-cache "^4.0.1" + shebang-command "^1.2.0" + which "^1.2.9" + +debug@^3.1.0: + version "3.2.5" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.5.tgz#c2418fbfd7a29f4d4f70ff4cea604d4b64c46407" + dependencies: + ms "^2.1.1" + +deep-is@~0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" + +del@^2.0.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/del/-/del-2.2.2.tgz#c12c981d067846c84bcaf862cff930d907ffd1a8" + dependencies: + globby "^5.0.0" + is-path-cwd "^1.0.0" + is-path-in-cwd "^1.0.0" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^2.0.0" + rimraf "^2.2.8" + +doctrine@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" + dependencies: + esutils "^2.0.2" + +escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + +eslint-scope@^3.7.1: + version "3.7.3" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-3.7.3.tgz#bb507200d3d17f60247636160b4826284b108535" + dependencies: + esrecurse "^4.1.0" + estraverse "^4.1.1" + +eslint-visitor-keys@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d" + +eslint@^4.19.1: + version "4.19.1" + resolved "http://registry.npmjs.org/eslint/-/eslint-4.19.1.tgz#32d1d653e1d90408854bfb296f076ec7e186a300" + dependencies: + ajv "^5.3.0" + babel-code-frame "^6.22.0" + chalk "^2.1.0" + concat-stream "^1.6.0" + cross-spawn "^5.1.0" + debug "^3.1.0" + doctrine "^2.1.0" + eslint-scope "^3.7.1" + eslint-visitor-keys "^1.0.0" + espree "^3.5.4" + esquery "^1.0.0" + esutils "^2.0.2" + file-entry-cache "^2.0.0" + functional-red-black-tree "^1.0.1" + glob "^7.1.2" + globals "^11.0.1" + ignore "^3.3.3" + imurmurhash "^0.1.4" + inquirer "^3.0.6" + is-resolvable "^1.0.0" + js-yaml "^3.9.1" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.3.0" + lodash "^4.17.4" + minimatch "^3.0.2" + mkdirp "^0.5.1" + natural-compare "^1.4.0" + optionator "^0.8.2" + path-is-inside "^1.0.2" + pluralize "^7.0.0" + progress "^2.0.0" + regexpp "^1.0.1" + require-uncached "^1.0.3" + semver "^5.3.0" + strip-ansi "^4.0.0" + strip-json-comments "~2.0.1" + table "4.0.2" + text-table "~0.2.0" + +espree@^3.5.4: + version "3.5.4" + resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.4.tgz#b0f447187c8a8bed944b815a660bddf5deb5d1a7" + dependencies: + acorn "^5.5.0" + acorn-jsx "^3.0.0" + +esprima@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + +esquery@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.1.tgz#406c51658b1f5991a5f9b62b1dc25b00e3e5c708" + dependencies: + estraverse "^4.0.0" + +esrecurse@^4.1.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.1.tgz#007a3b9fdbc2b3bb87e4879ea19c92fdbd3942cf" + dependencies: + estraverse "^4.1.0" + +estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1: + version "4.2.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" + +esutils@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" + +external-editor@^2.0.4: + version "2.2.0" + resolved "http://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz#045511cfd8d133f3846673d1047c154e214ad3d5" + dependencies: + chardet "^0.4.0" + iconv-lite "^0.4.17" + tmp "^0.0.33" + +fast-deep-equal@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614" + +fast-json-stable-stringify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" + +fast-levenshtein@~2.0.4: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + +figures@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" + dependencies: + escape-string-regexp "^1.0.5" + +file-entry-cache@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-2.0.0.tgz#c392990c3e684783d838b8c84a45d8a048458361" + dependencies: + flat-cache "^1.2.1" + object-assign "^4.0.1" + +flat-cache@^1.2.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-1.3.0.tgz#d3030b32b38154f4e3b7e9c709f490f7ef97c481" + dependencies: + circular-json "^0.3.1" + del "^2.0.2" + graceful-fs "^4.1.2" + write "^0.2.1" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + +functional-red-black-tree@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" + +glob@^7.0.3, glob@^7.0.5, glob@^7.1.2: + version "7.1.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1" + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +globals@^11.0.1: + version "11.7.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.7.0.tgz#a583faa43055b1aca771914bf68258e2fc125673" + +globby@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-5.0.0.tgz#ebd84667ca0dbb330b99bcfc68eac2bc54370e0d" + dependencies: + array-union "^1.0.1" + arrify "^1.0.0" + glob "^7.0.3" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^2.0.0" + +graceful-fs@^4.1.2: + version "4.1.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" + +has-ansi@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" + dependencies: + ansi-regex "^2.0.0" + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + +iconv-lite@^0.4.17: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + dependencies: + safer-buffer ">= 2.1.2 < 3" + +ignore@^3.3.3: + version "3.3.10" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.10.tgz#0a97fb876986e8081c631160f8f9f389157f0043" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@^2.0.3, inherits@~2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + +inquirer@^3.0.6: + version "3.3.0" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.3.0.tgz#9dd2f2ad765dcab1ff0443b491442a20ba227dc9" + dependencies: + ansi-escapes "^3.0.0" + chalk "^2.0.0" + cli-cursor "^2.1.0" + cli-width "^2.0.0" + external-editor "^2.0.4" + figures "^2.0.0" + lodash "^4.3.0" + mute-stream "0.0.7" + run-async "^2.2.0" + rx-lite "^4.0.8" + rx-lite-aggregates "^4.0.8" + string-width "^2.1.0" + strip-ansi "^4.0.0" + through "^2.3.6" + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + +is-path-cwd@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d" + +is-path-in-cwd@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz#5ac48b345ef675339bd6c7a48a912110b241cf52" + dependencies: + is-path-inside "^1.0.0" + +is-path-inside@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.1.tgz#8ef5b7de50437a3fdca6b4e865ef7aa55cb48036" + dependencies: + path-is-inside "^1.0.1" + +is-promise@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa" + +is-resolvable@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88" + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + +js-tokens@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" + +js-yaml@^3.9.1: + version "3.12.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.0.tgz#eaed656ec8344f10f527c6bfa1b6e2244de167d1" + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +json-schema-traverse@^0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340" + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + +levn@^0.3.0, levn@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" + dependencies: + prelude-ls "~1.1.2" + type-check "~0.3.2" + +lodash@^4.17.4, lodash@^4.3.0: + version "4.17.11" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" + +lru-cache@^4.0.1: + version "4.1.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.3.tgz#a1175cf3496dfc8436c156c334b4955992bce69c" + dependencies: + pseudomap "^1.0.2" + yallist "^2.1.2" + +mimic-fn@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" + +minimatch@^3.0.2, minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + dependencies: + brace-expansion "^1.1.7" + +minimist@0.0.8: + version "0.0.8" + resolved "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + +mkdirp@^0.5.1: + version "0.5.1" + resolved "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + dependencies: + minimist "0.0.8" + +ms@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" + +mute-stream@0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + +object-assign@4.1.1, object-assign@^4.0.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + dependencies: + wrappy "1" + +onetime@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4" + dependencies: + mimic-fn "^1.0.0" + +optionator@^0.8.2: + version "0.8.2" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64" + dependencies: + deep-is "~0.1.3" + fast-levenshtein "~2.0.4" + levn "~0.3.0" + prelude-ls "~1.1.2" + type-check "~0.3.2" + wordwrap "~1.0.0" + +os-tmpdir@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + +path-is-inside@^1.0.1, path-is-inside@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" + +pify@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + +pinkie-promise@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" + dependencies: + pinkie "^2.0.0" + +pinkie@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" + +pluralize@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-7.0.0.tgz#298b89df8b93b0221dbf421ad2b1b1ea23fc6777" + +prelude-ls@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" + +process-nextick-args@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" + +progress@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.0.tgz#8a1be366bf8fc23db2bd23f10c6fe920b4389d1f" + +pseudomap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" + +readable-stream@^2.2.2: + version "2.3.6" + resolved "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +regexpp@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-1.1.0.tgz#0e3516dd0b7904f413d2d4193dce4618c3a689ab" + +require-uncached@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/require-uncached/-/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3" + dependencies: + caller-path "^0.1.0" + resolve-from "^1.0.0" + +resolve-from@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-1.0.1.tgz#26cbfe935d1aeeeabb29bc3fe5aeb01e93d44226" + +restore-cursor@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf" + dependencies: + onetime "^2.0.0" + signal-exit "^3.0.2" + +rewire@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/rewire/-/rewire-4.0.1.tgz#ba1100d400a9da759fe599fc6e0233f0879ed6da" + dependencies: + eslint "^4.19.1" + +rimraf@^2.2.8: + version "2.6.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" + dependencies: + glob "^7.0.5" + +run-async@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0" + dependencies: + is-promise "^2.1.0" + +rx-lite-aggregates@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz#753b87a89a11c95467c4ac1626c4efc4e05c67be" + dependencies: + rx-lite "*" + +rx-lite@*, rx-lite@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-4.0.8.tgz#0b1e11af8bc44836f04a6407e92da42467b79444" + +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + +semver@^5.3.0: + version "5.5.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.1.tgz#7dfdd8814bdb7cabc7be0fb1d734cfb66c940477" + +shebang-command@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" + dependencies: + shebang-regex "^1.0.0" + +shebang-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" + +signal-exit@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" + +slice-ansi@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-1.0.0.tgz#044f1a49d8842ff307aad6b505ed178bd950134d" + dependencies: + is-fullwidth-code-point "^2.0.0" + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + +string-width@^2.1.0, string-width@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^4.0.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + dependencies: + safe-buffer "~5.1.0" + +strip-ansi@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + dependencies: + ansi-regex "^2.0.0" + +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + dependencies: + ansi-regex "^3.0.0" + +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + +supports-color@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + dependencies: + has-flag "^3.0.0" + +table@4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/table/-/table-4.0.2.tgz#a33447375391e766ad34d3486e6e2aedc84d2e36" + dependencies: + ajv "^5.2.3" + ajv-keywords "^2.1.0" + chalk "^2.1.0" + lodash "^4.17.4" + slice-ansi "1.0.0" + string-width "^2.1.1" + +text-table@~0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + +through@^2.3.6: + version "2.3.8" + resolved "http://registry.npmjs.org/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + +tmp@^0.0.33: + version "0.0.33" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" + dependencies: + os-tmpdir "~1.0.2" + +to-array@0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/to-array/-/to-array-0.1.4.tgz#17e6c11f73dd4f3d74cda7a4ff3238e9ad9bf890" + +type-check@~0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" + dependencies: + prelude-ls "~1.1.2" + +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + +util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + +which@^1.2.9: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + dependencies: + isexe "^2.0.0" + +wordwrap@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + +write@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/write/-/write-0.2.1.tgz#5fc03828e264cea3fe91455476f7a3c566cb0757" + dependencies: + mkdirp "^0.5.1" + +yallist@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" From d93a996e242d6392df2d4036a6ce4828b8dfd876 Mon Sep 17 00:00:00 2001 From: Daniel Kontorovskyi Date: Wed, 1 Jan 2020 19:26:05 +0200 Subject: [PATCH 4/5] feat: add tests for --all-projects and --file comparison --- src/cli/commands/monitor/index.ts | 38 ++++++------ .../convert-scanned-projects-to-custom.ts | 20 +++---- .../cli-monitor.all-projects.spec.ts | 58 +++++++++++++++---- 3 files changed, 74 insertions(+), 42 deletions(-) diff --git a/src/cli/commands/monitor/index.ts b/src/cli/commands/monitor/index.ts index 9b026a85b1..15c4f939ae 100644 --- a/src/cli/commands/monitor/index.ts +++ b/src/cli/commands/monitor/index.ts @@ -1,17 +1,11 @@ export = monitor; -import * as _ from 'lodash'; -import * as fs from 'then-fs'; -import { apiTokenExists } from '../../../lib/api-token'; -import snyk = require('../../../lib'); // TODO(kyegupov): fix import -import { monitor as snykMonitor } from '../../../lib/monitor'; -import * as config from '../../../lib/config'; import chalk from 'chalk'; -import * as pathUtil from 'path'; -import * as spinner from '../../../lib/spinner'; +import * as fs from 'then-fs'; import * as Debug from 'debug'; +import * as pathUtil from 'path'; +import { legacyPlugin as pluginApi } from '@snyk/cli-interface'; -import * as detect from '../../../lib/detect'; import { MonitorOptions, MonitorMeta, @@ -19,19 +13,24 @@ import { PluginMetadata, Options, } from '../../../lib/types'; +import * as config from '../../../lib/config'; +import * as detect from '../../../lib/detect'; +import { GoodResult, BadResult } from './types'; +import * as spinner from '../../../lib/spinner'; +import * as analytics from '../../../lib/analytics'; import { MethodArgs, ArgsOptions } from '../../args'; +import { apiTokenExists } from '../../../lib/api-token'; import { maybePrintDeps } from '../../../lib/print-deps'; -import * as analytics from '../../../lib/analytics'; -import { legacyPlugin as pluginApi } from '@snyk/cli-interface'; -import { formatMonitorOutput } from './formatters/format-monitor-response'; +import { monitor as snykMonitor } from '../../../lib/monitor'; import { processJsonMonitorResponse } from './process-json-monitor'; -import { GoodResult, BadResult } from './types'; +import snyk = require('../../../lib'); // TODO(kyegupov): fix import +import { formatMonitorOutput } from './formatters/format-monitor-response'; import { getDepsFromPlugin } from '../../../lib/plugins/get-deps-from-plugin'; +import { getSubProjectCount } from '../../../lib/plugins/get-sub-project-count'; import { extractPackageManager } from '../../../lib/plugins/extract-package-manager'; import { MultiProjectResultCustom } from '../../../lib/plugins/get-multi-plugin-result'; -import { getSubProjectCount } from '../../../lib/plugins/get-sub-project-count'; -import { convertSingleResultToMultiCustom } from '../../../lib/plugins/convert-single-splugin-res-to-multi-custom'; import { convertMultiResultToMultiCustom } from '../../../lib/plugins/convert-multi-plugin-res-to-multi-custom'; +import { convertSingleResultToMultiCustom } from '../../../lib/plugins/convert-single-splugin-res-to-multi-custom'; const SEPARATOR = '\n-------------------------------------------------------\n'; const debug = Debug('snyk'); @@ -151,11 +150,10 @@ async function monitor(...args0: MethodArgs): Promise { debug(`Processing ${projectDeps.depTree.name}...`); maybePrintDeps(options, projectDeps.depTree); - // TODO: not always correct for multi scan, - // targetFile needs to be derived from project scanned always - // this is not yet used anywhere in the system to leaving for now - const targetFileRelativePath = targetFile - ? pathUtil.join(pathUtil.resolve(path), targetFile) + + const tFile = projectDeps.targetFile || targetFile; + const targetFileRelativePath = tFile + ? pathUtil.join(pathUtil.resolve(path), tFile) : ''; const res: MonitorResult = await promiseOrCleanup( snykMonitor( diff --git a/src/lib/plugins/convert-scanned-projects-to-custom.ts b/src/lib/plugins/convert-scanned-projects-to-custom.ts index 544b1b609e..abb96f4754 100644 --- a/src/lib/plugins/convert-scanned-projects-to-custom.ts +++ b/src/lib/plugins/convert-scanned-projects-to-custom.ts @@ -9,16 +9,12 @@ export function convertScannedProjectsToCustom( ): ScannedProjectCustom[] { // annotate the package manager & targetFile to be used // for test & monitor - const customScannedProject: ScannedProjectCustom[] = scannedProjects.map( - (a) => { - (a as ScannedProjectCustom).targetFile = targetFile; - (a as ScannedProjectCustom).packageManager = (a as ScannedProjectCustom) - .packageManager - ? (a as ScannedProjectCustom).packageManager - : (packageManager as SupportedPackageManagers); - return a as ScannedProjectCustom; - }, - ); - - return customScannedProject; + return scannedProjects.map((a) => { + (a as ScannedProjectCustom).targetFile = a.targetFile || targetFile; + (a as ScannedProjectCustom).packageManager = (a as ScannedProjectCustom) + .packageManager + ? (a as ScannedProjectCustom).packageManager + : (packageManager as SupportedPackageManagers); + return a as ScannedProjectCustom; + }); } diff --git a/test/acceptance/cli-monitor/cli-monitor.all-projects.spec.ts b/test/acceptance/cli-monitor/cli-monitor.all-projects.spec.ts index 4b1b5177e5..78a6888a2f 100644 --- a/test/acceptance/cli-monitor/cli-monitor.all-projects.spec.ts +++ b/test/acceptance/cli-monitor/cli-monitor.all-projects.spec.ts @@ -132,40 +132,78 @@ export const AllProjectsTests: AcceptanceTests = { const spyPlugin = sinon.spy(params.plugins, 'loadPlugin'); t.teardown(spyPlugin.restore); - const result = await params.cli.monitor('mono-repo-project', { + await params.cli.monitor('mono-repo-project', { allProjects: true, }); // Pop all calls to server and filter out calls to `featureFlag` endpoint - const requests = params.server + const [rubyAll, npmAll, mavenAll] = params.server .popRequests(4) .filter((req) => req.url.includes('/monitor/')); - // TODO: Compare each with with a --file below - // Ruby - const resultRuby = await params.cli.monitor('mono-repo-project', { + await params.cli.monitor('mono-repo-project', { file: 'Gemfile.lock', }); - const requestsRuby = params.server + const [requestsRuby] = params.server .popRequests(2) .filter((req) => req.url.includes('/monitor/')); // npm - const resultNpm = await params.cli.monitor('mono-repo-project', { + await params.cli.monitor('mono-repo-project', { file: 'package-lock.json', }); - const requestsNpm = params.server + const [requestsNpm] = params.server .popRequests(2) .filter((req) => req.url.includes('/monitor/')); // maven - const resultMaven = await params.cli.monitor('mono-repo-project', { + await params.cli.monitor('mono-repo-project', { file: 'pom.xml', }); - const requestsMaven = params.server + const [requestsMaven] = params.server .popRequests(2) .filter((req) => req.url.includes('/monitor/')); + // Ruby project + + // Removing properties that are different for plugin and custom-plugin + // (pluginName, pluginRuntime) + delete rubyAll.body.meta.pluginName; + delete requestsRuby.body.meta.pluginName; + delete requestsRuby.body.meta.pluginRuntime; + + t.deepEqual( + rubyAll.body, + requestsRuby.body, + 'Same body for --all-projects and --file=Gemfile.lock', + ); + + // NPM project + + // Removing properties that are different for plugin and custom-plugin + // (pluginName, pluginRuntime) + delete npmAll.body.meta.pluginName; + delete requestsNpm.body.meta.pluginName; + delete requestsNpm.body.meta.pluginRuntime; + t.deepEqual( + npmAll.body, + requestsNpm.body, + 'Same body for --all-projects and --file=package-lock.json', + ); + + // Maven project + + // Removing properties that are different for plugin and custom-plugin + // (pluginName, pluginRuntime) + delete mavenAll.body.meta.pluginName; + delete requestsMaven.body.meta.pluginName; + delete requestsMaven.body.meta.pluginRuntime; + t.deepEqual( + mavenAll.body, + requestsMaven.body, + 'Same body for --all-projects and --file=pom.xml', + ); + t.pass('TODO'); }, From ba4658ac53a617751f8accdaf7ee7a3c4fdbf43d Mon Sep 17 00:00:00 2001 From: Liliana Kastilio Date: Thu, 2 Jan 2020 10:25:20 +0000 Subject: [PATCH 5/5] fix: pass the trimmed targetFile to get deps for multi deps result --- src/lib/plugins/get-multi-plugin-result.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/plugins/get-multi-plugin-result.ts b/src/lib/plugins/get-multi-plugin-result.ts index 6aec915a97..2f66ffb5fe 100644 --- a/src/lib/plugins/get-multi-plugin-result.ts +++ b/src/lib/plugins/get-multi-plugin-result.ts @@ -34,7 +34,7 @@ export async function getMultiPluginResult( const inspectRes = await getSinglePluginResult( root, optionsClone, - targetFile, + optionsClone.file, ); let resultWithScannedProjects: cliInterface.legacyPlugin.MultiProjectResult;