From e1651dd27fe0d361e809e4f6f14a0ae5580aaf79 Mon Sep 17 00:00:00 2001 From: Liliana Kastilio Date: Wed, 22 Jan 2020 19:12:09 +0000 Subject: [PATCH] feat: add python to auto manifest discovery Adding support for Pipfile and requirements.txt with --all-projects. Refactoring tests as well as adding tests for Pipfile and requirements.txt. --- src/lib/detect.ts | 2 + src/lib/find-files.ts | 22 + .../cli-monitor.all-projects.spec.ts | 399 ++++++++++++------ .../cli-test/cli-test.all-projects.spec.ts | 303 +++++++++++-- .../workspaces/mono-repo-project/Pipfile | 11 + .../python-app-with-req-file/requirements.txt | 3 + .../mono-repo-project/requirements.txt | 3 + .../test/paket-app/paket.dependencies | 15 +- .../test/paket-app/paket.lock | 10 + 9 files changed, 578 insertions(+), 190 deletions(-) create mode 100644 test/acceptance/workspaces/mono-repo-project/Pipfile create mode 100644 test/acceptance/workspaces/mono-repo-project/python-app-with-req-file/requirements.txt create mode 100644 test/acceptance/workspaces/mono-repo-project/requirements.txt diff --git a/src/lib/detect.ts b/src/lib/detect.ts index f501be059f..9aeaaa7fd0 100644 --- a/src/lib/detect.ts +++ b/src/lib/detect.ts @@ -48,6 +48,8 @@ export const AUTO_DETECTABLE_FILES: string[] = [ 'Gopkg.lock', 'go.mod', 'vendor.json', + 'Pipfile', + 'requirements.txt', ]; // when file is specified with --file, we look it up here diff --git a/src/lib/find-files.ts b/src/lib/find-files.ts index 1a279fb2cd..5eb9cab53c 100644 --- a/src/lib/find-files.ts +++ b/src/lib/find-files.ts @@ -2,6 +2,9 @@ import * as fs from 'fs'; import * as pathLib from 'path'; import * as _ from 'lodash'; import { detectPackageManagerFromFile } from './detect'; +import * as debugModule from 'debug'; +const debug = debugModule('snyk'); + // TODO: use util.promisify once we move to node 8 /** @@ -180,25 +183,44 @@ function chooseBestManifest( ['package-lock.json', 'yarn.lock'].includes(path.base), )[0]; if (lockFile) { + debug( + 'Encountered multiple npm manifest files, defaulting to package-lock.json / yarn.lock', + ); return lockFile.path; } const packageJson = files.filter((path) => ['package.json'].includes(path.base), )[0]; + debug( + 'Encountered multiple npm manifest files, defaulting to package.json', + ); return packageJson.path; } case 'rubygems': { + debug( + 'Encountered multiple gem manifest files, defaulting to Gemfile.lock', + ); const defaultManifest = files.filter((path) => ['Gemfile.lock'].includes(path.base), )[0]; return defaultManifest.path; } case 'cocoapods': { + debug( + 'Encountered multiple cocoapod manifest files, defaulting to Podfile', + ); const defaultManifest = files.filter((path) => ['Podfile'].includes(path.base), )[0]; return defaultManifest.path; } + case 'pip': { + debug('Encountered multiple pip manifest files, defaulting to Pipfile'); + const defaultManifest = files.filter((path) => + ['Pipfile'].includes(path.base), + )[0]; + return defaultManifest.path; + } default: { return null; } 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 82f78592e5..f00cef8f4f 100644 --- a/test/acceptance/cli-monitor/cli-monitor.all-projects.spec.ts +++ b/test/acceptance/cli-monitor/cli-monitor.all-projects.spec.ts @@ -1,5 +1,6 @@ import * as sinon from 'sinon'; import * as _ from 'lodash'; +import * as path from 'path'; interface AcceptanceTests { language: string; @@ -11,62 +12,69 @@ interface AcceptanceTests { export const AllProjectsTests: AcceptanceTests = { language: 'Mixed', tests: { - '`monitor mono-repo-project with lockfiles --all-projects`': ( + '`monitor mono-repo-project --all-projects --detection-depth=1`': ( params, utils, ) => async (t) => { utils.chdirWorkspaces(); - const spyPlugin = sinon.spy(params.plugins, 'loadPlugin'); - t.teardown(spyPlugin.restore); + + // mock python plugin becuase CI tooling doesn't have pipenv installed + const mockPlugin = { + async inspect() { + return { + plugin: { + targetFile: 'Pipfile', + name: 'snyk-python-plugin', + }, + package: {}, + }; + }, + }; + const loadPlugin = sinon.stub(params.plugins, 'loadPlugin'); + t.teardown(loadPlugin.restore); + loadPlugin.withArgs('pip').returns(mockPlugin); + loadPlugin.callThrough(); // don't mock other plugins const result = await params.cli.monitor('mono-repo-project', { allProjects: true, detectionDepth: 1, }); - 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'); - t.ok(spyPlugin.withArgs('nuget').calledOnce, 'calls nuget plugin'); - t.ok(spyPlugin.withArgs('paket').calledOnce, 'calls nuget plugin'); + t.ok(loadPlugin.withArgs('rubygems').calledOnce, 'calls rubygems plugin'); + t.ok(loadPlugin.withArgs('npm').calledOnce, 'calls npm plugin'); + t.ok(loadPlugin.withArgs('maven').calledOnce, 'calls maven plugin'); + t.ok(loadPlugin.withArgs('nuget').calledOnce, 'calls nuget plugin'); + t.ok(loadPlugin.withArgs('paket').calledOnce, 'calls nuget plugin'); + t.ok(loadPlugin.withArgs('pip').calledOnce, 'calls pip plugin'); + + t.match(result, 'rubygems/some/project-id', 'ruby project in output'); + t.match(result, 'npm/graph/some/project-id', 'npm project in output'); + t.match(result, 'maven/some/project-id', 'maven project in output '); + t.match(result, 'nuget/some/project-id', 'nuget project in output'); + t.match(result, 'paket/some/project-id', 'paket project in output'); + t.match(result, 'pip/some/project-id', 'python project in output '); - // 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', - ); - // nuget - t.match(result, 'nuget/some/project-id', 'nuget project was monitored'); - // paket - t.match(result, 'paket/some/project-id', 'paket 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(6) + .popRequests(7) .filter((req) => req.url.includes('/monitor/')); - t.equal(requests.length, 5, 'Correct amount of monitor requests'); + t.equal(requests.length, 6, 'correct amount of monitor requests'); - const pluginsWithoutTragetFilesInBody = [ + const pluginsWithoutTargetFileInBody = [ 'snyk-nodejs-lockfile-parser', 'bundled:maven', 'bundled:rubygems', ]; requests.forEach((req) => { - t.match(req.url, '/monitor/', 'puts at correct url'); - if ( - pluginsWithoutTragetFilesInBody.includes(req.body.meta.pluginName) - ) { + t.match( + req.url, + /\/api\/v1\/monitor\/(npm\/graph|rubygems|maven|nuget|paket|pip)/, + 'puts at correct url', + ); + if (pluginsWithoutTargetFileInBody.includes(req.body.meta.pluginName)) { t.notOk( req.body.targetFile, - `doesn\'t send the targetFile for ${req.body.meta.pluginName}`, + `doesn't send the targetFile for ${req.body.meta.pluginName}`, ); } else { t.ok( @@ -104,16 +112,12 @@ export const AllProjectsTests: AcceptanceTests = { 2, 'calls maven plugin twice', ); - // maven t.match(result, 'maven/some/project-id', 'maven project was monitored '); const requests = params.server.popRequests(2); requests.forEach((request) => { - // once we have depth increase released - t.ok(request, 'Monitor POST request'); - - t.match(request.url, '/monitor/', 'puts at correct url'); + t.match(request.url, '/api/v1/monitor/maven', 'puts at correct url'); t.notOk(request.body.targetFile, "doesn't send the targetFile"); t.equal(request.method, 'PUT', 'makes PUT request'); t.equal( @@ -137,19 +141,25 @@ export const AllProjectsTests: AcceptanceTests = { 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.notMatch( + result, + 'yarn/graph/some/project-id', + 'yarn project was not monitored', + ); - t.match(request.url, '/monitor/', 'puts at correct url'); + const request = params.server.popRequest(); + t.equal( + params.server._reqLog.length, + 0, + 'no other requests sent (yarn error ignored)', + ); + t.match(request.url, '/api/v1/monitor/rubygems', 'puts at correct url'); t.notOk(request.body.targetFile, "doesn't send the targetFile"); t.equal(request.method, 'PUT', 'makes PUT request'); t.equal( @@ -158,110 +168,116 @@ export const AllProjectsTests: AcceptanceTests = { 'sends version number', ); }, - '`monitor mono-repo-project with lockfiles --all-projects and without same meta`': ( + '`monitor mono-repo-project --all-projects sends same payload as --file`': ( params, utils, ) => async (t) => { utils.chdirWorkspaces(); - const spyPlugin = sinon.spy(params.plugins, 'loadPlugin'); - t.teardown(spyPlugin.restore); + + // mock python plugin becuase CI tooling doesn't have pipenv installed + const mockPlugin = { + async inspect() { + return { + plugin: { + targetFile: 'Pipfile', + name: 'snyk-python-plugin', + }, + package: {}, + }; + }, + }; + const loadPlugin = sinon.stub(params.plugins, 'loadPlugin'); + t.teardown(loadPlugin.restore); + loadPlugin.withArgs('pip').returns(mockPlugin); + loadPlugin.callThrough(); // don't mock other plugins await params.cli.monitor('mono-repo-project', { allProjects: true, detectionDepth: 1, }); + // Pop all calls to server and filter out calls to `featureFlag` endpoint - const [ - rubyAll, - npmAll, - nugetAll, - paketAll, - mavenAll, - ] = params.server - .popRequests(6) + const requests = params.server + .popRequests(7) .filter((req) => req.url.includes('/monitor/')); + // find each type of request + const rubyAll = requests.find((req) => req.url.indexOf('rubygems') > -1); + const pipAll = requests.find((req) => req.url.indexOf('pip') > -1); + const npmAll = requests.find((req) => req.url.indexOf('npm') > -1); + const nugetAll = requests.find((req) => req.url.indexOf('nuget') > -1); + const paketAll = requests.find((req) => req.url.indexOf('paket') > -1); + const mavenAll = requests.find((req) => req.url.indexOf('maven') > -1); - // nuget await params.cli.monitor('mono-repo-project', { - file: 'packages.config', + file: 'Gemfile.lock', }); - const [requestsNuget] = params.server - .popRequests(2) - .filter((req) => req.url.includes('/monitor/')); + const rubyFile = params.server.popRequest(); - // Ruby await params.cli.monitor('mono-repo-project', { - file: 'Gemfile.lock', + file: 'Pipfile', }); - const [requestsRuby] = params.server - .popRequests(2) - .filter((req) => req.url.includes('/monitor/')); + const pipFile = params.server.popRequest(); - // npm await params.cli.monitor('mono-repo-project', { file: 'package-lock.json', }); - const [requestsNpm] = params.server + // Pop all calls to server and filter out calls to `featureFlag` endpoint + const [npmFile] = params.server .popRequests(2) .filter((req) => req.url.includes('/monitor/')); - // maven await params.cli.monitor('mono-repo-project', { - file: 'pom.xml', + file: 'packages.config', }); - const [requestsMaven] = params.server - .popRequests(2) - .filter((req) => req.url.includes('/monitor/')); + const nugetFile = params.server.popRequest(); - // paket await params.cli.monitor('mono-repo-project', { file: 'paket.dependencies', }); - const [requestsPaket] = params.server - .popRequests(2) - .filter((req) => req.url.includes('/monitor/')); + const paketFile = params.server.popRequest(); - // Ruby project + await params.cli.monitor('mono-repo-project', { + file: 'pom.xml', + }); + const mavenFile = params.server.popRequest(); - t.deepEqual( + t.same( rubyAll.body, - requestsRuby.body, - 'Same body for --all-projects and --file=Gemfile.lock', + rubyFile.body, + 'same body for --all-projects and --file=Gemfile.lock', ); - // NPM project + t.same( + pipAll.body, + pipFile.body, + 'same body for --all-projects and --file=Pipfile', + ); - t.deepEqual( + t.same( npmAll.body, - requestsNpm.body, - 'Same body for --all-projects and --file=package-lock.json', + npmFile.body, + 'same body for --all-projects and --file=package-lock.json', ); - // NUGET project - - t.deepEqual( + t.same( nugetAll.body, - requestsNuget.body, - 'Same body for --all-projects and --file=packages.config', + nugetFile.body, + 'same body for --all-projects and --file=packages.config', ); - // Maven project - - t.deepEqual( - mavenAll.body, - requestsMaven.body, - 'Same body for --all-projects and --file=pom.xml', + t.same( + paketAll.body, + paketFile.body, + 'same body for --all-projects and --file=paket.dependencies', ); - // Paket project - - t.deepEqual( - paketAll.body, - requestsPaket.body, - 'Same body for --all-projects and --file=paket.dependencies', + t.same( + mavenAll.body, + mavenFile.body, + 'same body for --all-projects and --file=pom.xml', ); }, - '`monitor composer-app with --all-projects and without same meta`': ( + '`monitor composer-app with --all-projects sends same payload as --file`': ( params, utils, ) => async (t) => { @@ -272,22 +288,17 @@ export const AllProjectsTests: AcceptanceTests = { await params.cli.monitor('composer-app', { allProjects: true, }); - // Pop all calls to server and filter out calls to `featureFlag` endpoint - const [composerAll] = params.server - .popRequests(2) - .filter((req) => req.url.includes('/monitor/')); + const composerAll = params.server.popRequest(); await params.cli.monitor('composer-app', { file: 'composer.lock', }); - const [requestsComposer] = params.server - .popRequests(2) - .filter((req) => req.url.includes('/monitor/')); + const composerFile = params.server.popRequest(); - t.deepEqual( + t.same( composerAll.body, - requestsComposer.body, - 'Same body for --all-projects and --file=composer.lock', + composerFile.body, + 'same body for --all-projects and --file=composer.lock', ); }, '`monitor mono-repo-project with lockfiles --all-projects --json`': ( @@ -296,14 +307,48 @@ export const AllProjectsTests: AcceptanceTests = { ) => async (t) => { try { utils.chdirWorkspaces(); - const spyPlugin = sinon.spy(params.plugins, 'loadPlugin'); - t.teardown(spyPlugin.restore); + + // mock python plugin becuase CI tooling doesn't have pipenv installed + const mockPlugin = { + async inspect() { + return { + plugin: { + targetFile: 'Pipfile', + name: 'snyk-python-plugin', + }, + package: { + name: 'mono-repo-project', // used by projectName + }, + }; + }, + }; + const loadPlugin = sinon.stub(params.plugins, 'loadPlugin'); + t.teardown(loadPlugin.restore); + loadPlugin.withArgs('pip').returns(mockPlugin); + loadPlugin.callThrough(); // don't mock other plugins const response = await params.cli.monitor('mono-repo-project', { json: true, allProjects: true, + detectionDepth: 1, }); - JSON.parse(response).forEach((res) => { + + const requests = params.server.popRequests(7); + t.equal(requests.length, 7, 'sends expected # requests'); // extra feature-flags request + t.equal( + params.server._reqLog.length, + 0, + `${params.server._reqLog.length} pending requests`, + ); + + const jsonResponse = JSON.parse(response); + t.equal( + jsonResponse.length, + 6, + 'json response array has expected # elements', + ); + + jsonResponse.forEach((res) => { if (_.isObject(res)) { t.pass('monitor outputted JSON'); } else { @@ -350,7 +395,6 @@ export const AllProjectsTests: AcceptanceTests = { ); t.match(result, 'maven/some/project-id', 'maven project was monitored '); 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'); @@ -360,27 +404,108 @@ export const AllProjectsTests: AcceptanceTests = { 'sends version number', ); }, - '`monitor monorepo-with-nuget with Cocoapods --all-projects and without same meta`': ( + '`monitor monorepo-with-nuget --all-projects sends same payload as --file`': ( params, utils, ) => async (t) => { utils.chdirWorkspaces(); - const spyPlugin = sinon.spy(params.plugins, 'loadPlugin'); - t.teardown(spyPlugin.restore); - await params.cli.monitor('monorepo-with-nuget/src/cocoapods-app', { + // mock go plugin becuase CI tooling doesn't have go installed + const mockPlugin = { + async inspect() { + return { + plugin: { + targetFile: 'Gopkg.lock', + name: 'snyk-go-plugin', + runtime: 'go', + }, + package: {}, + }; + }, + }; + const loadPlugin = sinon.stub(params.plugins, 'loadPlugin'); + t.teardown(loadPlugin.restore); + loadPlugin.withArgs('golangdep').returns(mockPlugin); + loadPlugin.callThrough(); // don't mock other plugins + + await params.cli.monitor('monorepo-with-nuget', { allProjects: true, + detectionDepth: 4, }); - const cocoapodsAll = params.server.popRequest(); - // Cocoapods - await params.cli.monitor('monorepo-with-nuget/src/cocoapods-app', { - file: 'Podfile', + + // Pop all calls to server and filter out calls to `featureFlag` endpoint + const [ + projectAssetsAll, + cocoapodsAll, + golangdepAll, + npmAll, + packageConfigAll, + paketAll, + ] = params.server + .popRequests(7) + .filter((req) => req.url.includes('/monitor/')); + + await params.cli.monitor('monorepo-with-nuget', { + file: `src${path.sep}cartservice-nuget${path.sep}obj${path.sep}project.assets.json`, }); - const requestsCocoapods = params.server.popRequest(); - t.deepEqual( + const projectAssetsFile = params.server.popRequest(); + + await params.cli.monitor('monorepo-with-nuget', { + file: `src${path.sep}cocoapods-app${path.sep}Podfile.lock`, + }); + const cocoapodsFile = params.server.popRequest(); + + await params.cli.monitor('monorepo-with-nuget', { + file: `src${path.sep}frontend${path.sep}Gopkg.lock`, + }); + const golangdepFile = params.server.popRequest(); + + await params.cli.monitor('monorepo-with-nuget', { + file: `src${path.sep}paymentservice${path.sep}package-lock.json`, + }); + const [npmFile] = params.server + .popRequests(2) + .filter((req) => req.url.includes('/monitor/')); + + await params.cli.monitor('monorepo-with-nuget', { + file: `test${path.sep}nuget-app-4${path.sep}packages.config`, + }); + const packageConfigFile = params.server.popRequest(); + + await params.cli.monitor('monorepo-with-nuget', { + file: `test${path.sep}paket-app${path.sep}paket.dependencies`, + }); + const paketFile = params.server.popRequest(); + + t.same( + projectAssetsAll.body, + projectAssetsFile.body, + `same body for --all-projects and --file=src${path.sep}cartservice-nuget${path.sep}obj${path.sep}project.assets.json`, + ); + t.same( cocoapodsAll.body, - requestsCocoapods.body, - 'Same body for --all-projects and --file=src/cocoapods-app/Podfile', + cocoapodsFile.body, + `same body for --all-projects and --file=src${path.sep}cocoapods-app${path.sep}Podfile.lock`, + ); + t.same( + golangdepAll.body, + golangdepFile.body, + `same body for --all-projects and --file=src${path.sep}frontend${path.sep}Gopkg.lock`, + ); + t.same( + npmAll.body, + npmFile.body, + `same body for --all-projects and --file=src${path.sep}paymentservice${path.sep}package-lock.json`, + ); + t.same( + packageConfigAll.body, + packageConfigFile.body, + `same body for --all-projects and --file=test${path.sep}nuget-app-4${path.sep}packages.config`, + ); + t.same( + paketAll.body, + paketFile.body, + `same body for --all-projects and --file=test${path.sep}paket-app${path.sep}paket.dependencies`, ); }, '`monitor mono-repo-go/hello-dep --all-projects sends same body as --file`': ( @@ -415,7 +540,7 @@ export const AllProjectsTests: AcceptanceTests = { t.same( allProjectsBody.body, fileBody.body, - 'Same body for --all-projects and --file=mono-repo-go/hello-dep/Gopkg.lock', + 'same body for --all-projects and --file=mono-repo-go/hello-dep/Gopkg.lock', ); }, '`monitor mono-repo-go/hello-mod --all-projects sends same body as --file`': ( @@ -450,7 +575,7 @@ export const AllProjectsTests: AcceptanceTests = { t.same( allProjectsBody.body, fileBody.body, - 'Same body for --all-projects and --file=mono-repo-go/hello-mod/go.mod', + 'same body for --all-projects and --file=mono-repo-go/hello-mod/go.mod', ); }, '`monitor mono-repo-go/hello-vendor --all-projects sends same body as --file`': ( @@ -485,7 +610,7 @@ export const AllProjectsTests: AcceptanceTests = { t.same( allProjectsBody.body, fileBody.body, - 'Same body for --all-projects and --file=mono-repo-go/hello-vendor/vendor/vendor.json', + 'same body for --all-projects and --file=mono-repo-go/hello-vendor/vendor/vendor.json', ); }, @@ -509,8 +634,8 @@ export const AllProjectsTests: AcceptanceTests = { t.teardown(loadPlugin.restore); loadPlugin.withArgs('golangdep').returns(mockPlugin); loadPlugin.withArgs('gomodules').returns(mockPlugin); - loadPlugin.withArgs('npm').returns(mockPlugin); loadPlugin.withArgs('govendor').returns(mockPlugin); + loadPlugin.callThrough(); // don't mock npm plugin const result = await params.cli.monitor('mono-repo-go', { allProjects: true, detectionDepth: 3, @@ -527,10 +652,14 @@ export const AllProjectsTests: AcceptanceTests = { const requests = params.server .popRequests(5) .filter((req) => req.url.includes('/monitor/')); - t.equal(requests.length, 4, 'Correct amount of monitor requests'); + t.equal(requests.length, 4, 'correct amount of monitor requests'); requests.forEach((req) => { - t.match(req.url, '/monitor/', 'puts at correct url'); + t.match( + req.url, + /\/api\/v1\/monitor\/(npm\/graph|golangdep|gomodules|govendor)/, + 'puts at correct url', + ); t.notOk(req.body.targetFile, "doesn't send the targetFile"); t.equal(req.method, 'PUT', 'makes PUT request'); t.equal( 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 ce2404ae13..0ac6cf9619 100644 --- a/test/acceptance/cli-test/cli-test.all-projects.spec.ts +++ b/test/acceptance/cli-test/cli-test.all-projects.spec.ts @@ -11,20 +11,37 @@ export const AllProjectsTests: AcceptanceTests = { utils, ) => async (t) => { utils.chdirWorkspaces(); - const spyPlugin = sinon.spy(params.plugins, 'loadPlugin'); - t.teardown(spyPlugin.restore); + + // mock python plugin becuase CI tooling doesn't have pipenv installed + const mockPlugin = { + async inspect() { + return { + plugin: { + targetFile: 'Pipfile', + name: 'snyk-python-plugin', + }, + package: {}, + }; + }, + }; + const loadPlugin = sinon.stub(params.plugins, 'loadPlugin'); + t.teardown(loadPlugin.restore); + loadPlugin.withArgs('pip').returns(mockPlugin); + loadPlugin.callThrough(); // don't mock other plugins const result = await params.cli.test('mono-repo-project', { allProjects: true, detectionDepth: 1, + skipUnresolved: 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'); - t.ok(spyPlugin.withArgs('nuget').calledOnce, 'calls nuget plugin'); - t.ok(spyPlugin.withArgs('paket').calledOnce, 'calls nuget plugin'); + t.ok(loadPlugin.withArgs('rubygems').calledOnce, 'calls rubygems plugin'); + t.ok(loadPlugin.withArgs('npm').calledOnce, 'calls npm plugin'); + t.ok(loadPlugin.withArgs('maven').calledOnce, 'calls maven plugin'); + t.ok(loadPlugin.withArgs('nuget').calledOnce, 'calls nuget plugin'); + t.ok(loadPlugin.withArgs('paket').calledOnce, 'calls nuget plugin'); + t.ok(loadPlugin.withArgs('pip').calledOnce, 'calls pip plugin'); - params.server.popRequests(5).forEach((req) => { + params.server.popRequests(6).forEach((req) => { t.equal(req.method, 'POST', 'makes POST request'); t.equal( req.headers['x-snyk-cli-version'], @@ -35,11 +52,12 @@ export const AllProjectsTests: AcceptanceTests = { t.ok(req.body.depGraph, 'body contains depGraph'); t.match( req.body.depGraph.pkgManager.name, - /(npm|rubygems|maven|nuget|paket)/, + /(npm|rubygems|maven|nuget|paket|pip)/, 'depGraph has package manager', ); }); // results should contain test results from both package managers + t.match( result, 'Package manager: rubygems', @@ -71,79 +89,274 @@ export const AllProjectsTests: AcceptanceTests = { 'Target file: pom.xml', 'contains target file pom.xml', ); + t.match( + result, + 'Target file: Pipfile', + 'contains target file Pipfile', + ); }, - '`test --all-projects and --file payloads are the same`': ( + '`test mono-repo-project --all-projects --detection-depth=3`': ( params, utils, ) => async (t) => { utils.chdirWorkspaces(); - const spyPlugin = sinon.spy(params.plugins, 'loadPlugin'); - t.teardown(spyPlugin.restore); + + // mock python plugin becuase CI tooling doesn't have pipenv installed + const mockPlugin = { + async inspect() { + return { + plugin: { + targetFile: 'Pipfile', + name: 'snyk-python-plugin', + }, + package: {}, + }; + }, + }; + const loadPlugin = sinon.stub(params.plugins, 'loadPlugin'); + t.teardown(loadPlugin.restore); + // detect pip plugin called only with Pipfile (and not requirement.txt) + loadPlugin + .withArgs('pip', sinon.match.has('file', 'Pipfile')) + .returns(mockPlugin); + loadPlugin.callThrough(); // don't mock other plugins + + const result = await params.cli.test('mono-repo-project', { + allProjects: true, + detectionDepth: 3, + allowMissing: true, // allow requirements.txt to pass when deps not installed + }); + + t.equals( + loadPlugin.withArgs('rubygems').callCount, + 2, + 'calls rubygems plugin', + ); + t.equals(loadPlugin.withArgs('npm').callCount, 2, 'calls npm plugin'); + t.ok(loadPlugin.withArgs('maven').calledOnce, 'calls maven plugin'); + t.ok(loadPlugin.withArgs('nuget').calledOnce, 'calls nuget plugin'); + t.ok(loadPlugin.withArgs('paket').calledOnce, 'calls nuget plugin'); + t.equals(loadPlugin.withArgs('pip').callCount, 2, 'calls pip plugin'); + + params.server.popRequests(9).forEach((req) => { + t.equal(req.method, 'POST', 'makes POST request'); + t.equal( + req.headers['x-snyk-cli-version'], + params.versionNumber, + 'sends version number', + ); + t.match(req.url, '/api/v1/test-dep-graph', 'posts to correct url'); + t.ok(req.body.depGraph, 'body contains depGraph'); + t.match( + req.body.depGraph.pkgManager.name, + /(npm|rubygems|maven|nuget|paket|pip)/, + 'depGraph has package manager', + ); + }); + + // ruby + t.match( + result, + 'Package manager: rubygems', + 'contains package manager rubygems', + ); + t.match( + result, + 'Target file: Gemfile.lock', + 'contains target file Gemfile.lock', + ); + t.match( + result, + `Target file: bundler-app${path.sep}Gemfile.lock`, + `contains target file bundler-app${path.sep}Gemfile.lock`, + ); + + // npm + t.match( + result, + 'Project name: shallow-goof', + 'contains correct project name for npm', + ); + t.match( + result, + 'Project name: goof', + 'contains correct project name for npm', + ); + t.match(result, 'Package manager: npm', 'contains package manager npm'); + t.match( + result, + 'Target file: package-lock.json', + 'contains target file package-lock.json', + ); + t.match( + result, + `Target file: npm-project${path.sep}package.json`, + `contains target file npm-project${path.sep}package.json`, + ); + + // maven + t.match( + result, + 'Package manager: maven', + 'contains package manager maven', + ); + t.match( + result, + 'Target file: pom.xml', + 'contains target file pom.xml', + ); + + // nuget + t.match( + result, + 'Package manager: nuget', + 'contains package manager nuget', + ); + t.match( + result, + 'Target file: packages.config', + 'contains target file packages.config', + ); + + // paket + t.match( + result, + 'Package manager: paket', + 'contains package manager paket', + ); + t.match( + result, + 'Target file: paket.dependencies', + 'contains target file paket.dependencies', + ); + + // pip + t.match(result, 'Package manager: pip', 'contains package manager pip'); + t.match( + result, + 'Target file: Pipfile', + 'contains target file Pipfile', + ); + t.match( + result, + `Target file: python-app-with-req-file${path.sep}requirements.txt`, + `contains target file python-app-with-req-file${path.sep}requirements.txt`, + ); + }, + + '`test mono-repo-project --all-projects and --file payloads are the same`': ( + params, + utils, + ) => async (t) => { + utils.chdirWorkspaces(); + + // mock python plugin becuase CI tooling doesn't have pipenv installed + const mockPlugin = { + async inspect() { + return { + plugin: { + targetFile: 'Pipfile', + name: 'snyk-python-plugin', + }, + package: {}, + }; + }, + }; + const loadPlugin = sinon.stub(params.plugins, 'loadPlugin'); + t.teardown(loadPlugin.restore); + loadPlugin.withArgs('pip').returns(mockPlugin); + loadPlugin.callThrough(); // don't mock other plugins await params.cli.test('mono-repo-project', { allProjects: true, detectionDepth: 1, }); - const [ - rubyAllProjectsBody, - npmAllProjectsBody, - nugetAllProjectsBody, - paketAllProjectsBody, - mavenAllProjectsBody, - ] = params.server.popRequests(5).map((req) => req.body); + + const requests = params.server.popRequests(6); + + // find each type of request + const rubyAll = requests.find( + (req) => req.body.depGraph.pkgManager.name === 'rubygems', + ); + const pipAll = requests.find( + (req) => req.body.depGraph.pkgManager.name === 'pip', + ); + const npmAll = requests.find( + (req) => req.body.depGraph.pkgManager.name === 'npm', + ); + const nugetAll = requests.find( + (req) => req.body.depGraph.pkgManager.name === 'nuget', + ); + const paketAll = requests.find( + (req) => req.body.depGraph.pkgManager.name === 'paket', + ); + const mavenAll = requests.find( + (req) => req.body.depGraph.pkgManager.name === 'maven', + ); await params.cli.test('mono-repo-project', { file: 'Gemfile.lock', }); - const { body: rubyFileBody } = params.server.popRequest(); + const rubyFile = params.server.popRequest(); + + await params.cli.test('mono-repo-project', { + file: 'Pipfile', + }); + const pipFile = params.server.popRequest(); await params.cli.test('mono-repo-project', { file: 'paket.dependencies', }); - const { body: paketFileBody } = params.server.popRequest(); + const paketFile = params.server.popRequest(); await params.cli.test('mono-repo-project', { file: 'packages.config', }); - const { body: nugetFileBody } = params.server.popRequest(); + const nugetFile = params.server.popRequest(); await params.cli.test('mono-repo-project', { file: 'package-lock.json', }); - const { body: npmFileBody } = params.server.popRequest(); + const npmFile = params.server.popRequest(); await params.cli.test('mono-repo-project', { file: 'pom.xml', }); - const { body: mavenFileBody } = params.server.popRequest(); + const mavenFile = params.server.popRequest(); + + t.same( + pipAll.body, + pipFile.body, + 'Same body for --all-projects and --file=Pipfile', + ); t.same( - rubyAllProjectsBody, - rubyFileBody, + rubyAll.body, + rubyFile.body, 'Same body for --all-projects and --file=Gemfile.lock', ); t.same( - npmAllProjectsBody, - npmFileBody, + npmAll.body, + npmFile.body, 'Same body for --all-projects and --file=package-lock.json', ); t.same( - paketAllProjectsBody, - paketFileBody, + paketAll.body, + paketFile.body, 'Same body for --all-projects and --file=package-lock.json', ); t.same( - nugetAllProjectsBody, - nugetFileBody, + nugetAll.body, + nugetFile.body, 'Same body for --all-projects and --file=package-lock.json', ); t.same( - mavenAllProjectsBody, - mavenFileBody, + mavenAll.body, + mavenFile.body, 'Same body for --all-projects and --file=pom.xml', ); }, @@ -427,10 +640,8 @@ export const AllProjectsTests: AcceptanceTests = { const loadPlugin = sinon.stub(params.plugins, 'loadPlugin'); t.teardown(loadPlugin.restore); // prevent plugin inspect from actually running (requires go to be installed) - loadPlugin.withArgs('nuget').returns(mockPlugin); - loadPlugin.withArgs('cocoapods').returns(mockPlugin); - loadPlugin.withArgs('npm').returns(mockPlugin); loadPlugin.withArgs('golangdep').returns(mockPlugin); + loadPlugin.callThrough(); // don't mock other plugins try { const res = await params.cli.test('monorepo-with-nuget', { @@ -451,10 +662,11 @@ export const AllProjectsTests: AcceptanceTests = { loadPlugin.withArgs('golangdep').calledOnce, 'calls golangdep plugin', ); + t.ok(loadPlugin.withArgs('paket').calledOnce, 'calls nuget plugin'); t.match( res, - /Tested 5 projects, no vulnerable paths were found./, - 'Five projects tested', + /Tested 6 projects, no vulnerable paths were found./, + 'Six projects tested', ); t.match( res, @@ -481,19 +693,20 @@ export const AllProjectsTests: AcceptanceTests = { `Target file: test${path.sep}nuget-app-4${path.sep}packages.config`, 'Nuget project targetFile is as expected', ); - t.match(res, 'Package manager: nuget', 'Nuget package manager'); t.match( res, - 'Package manager: cocoapods', - 'Cocoapods package manager', + `Target file: test${path.sep}paket-app${path.sep}paket.dependencies`, + 'Paket project targetFile is as expected', ); - t.match(res, 'Package manager: npm', 'Npm package manager'); - t.match(res, 'Package manager: golangdep', 'Go dep package manager'); + + t.match(res, 'Package manager: nuget', 'Nuget package manager'); t.match( res, 'Package manager: cocoapods', 'Cocoapods package manager', ); + t.match(res, 'Package manager: npm', 'Npm package manager'); + t.match(res, 'Package manager: golangdep', 'Go dep package manager'); } catch (err) { t.fail('expected to pass'); } @@ -549,8 +762,8 @@ export const AllProjectsTests: AcceptanceTests = { // prevent plugin inspect from actually running (requires go to be installed) loadPlugin.withArgs('golangdep').returns(mockPlugin); loadPlugin.withArgs('gomodules').returns(mockPlugin); - loadPlugin.withArgs('npm').returns(mockPlugin); loadPlugin.withArgs('govendor').returns(mockPlugin); + loadPlugin.callThrough(); // don't mock npm plugin const res = await params.cli.test('mono-repo-go', { allProjects: true, diff --git a/test/acceptance/workspaces/mono-repo-project/Pipfile b/test/acceptance/workspaces/mono-repo-project/Pipfile new file mode 100644 index 0000000000..5b44da8b8c --- /dev/null +++ b/test/acceptance/workspaces/mono-repo-project/Pipfile @@ -0,0 +1,11 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +"Jinja2" = "*" + +[dev-packages] + +[requires] diff --git a/test/acceptance/workspaces/mono-repo-project/python-app-with-req-file/requirements.txt b/test/acceptance/workspaces/mono-repo-project/python-app-with-req-file/requirements.txt new file mode 100644 index 0000000000..4255004086 --- /dev/null +++ b/test/acceptance/workspaces/mono-repo-project/python-app-with-req-file/requirements.txt @@ -0,0 +1,3 @@ +# The following library requires Python >= 3.4.2 +# For more see: https://pypi.python.org/pypi?:action=browse&show=all&c=595 +aiohttp==2.2.2 diff --git a/test/acceptance/workspaces/mono-repo-project/requirements.txt b/test/acceptance/workspaces/mono-repo-project/requirements.txt new file mode 100644 index 0000000000..4255004086 --- /dev/null +++ b/test/acceptance/workspaces/mono-repo-project/requirements.txt @@ -0,0 +1,3 @@ +# The following library requires Python >= 3.4.2 +# For more see: https://pypi.python.org/pypi?:action=browse&show=all&c=595 +aiohttp==2.2.2 diff --git a/test/acceptance/workspaces/monorepo-with-nuget/test/paket-app/paket.dependencies b/test/acceptance/workspaces/monorepo-with-nuget/test/paket-app/paket.dependencies index edd5b3f9b1..21a28f61e1 100644 --- a/test/acceptance/workspaces/monorepo-with-nuget/test/paket-app/paket.dependencies +++ b/test/acceptance/workspaces/monorepo-with-nuget/test/paket-app/paket.dependencies @@ -1,10 +1,5 @@ -REDIRECTS: ON -NUGET - remote: https://www.nuget.org/api/v2 - FAKE (5.8.4) - FSharp.Compiler.Service (2.0.0.6) - FSharp.Formatting (2.14.4) - FSharp.Compiler.Service (2.0.0.6) - FSharpVSPowerTools.Core (>= 2.3 < 2.4) - FSharpVSPowerTools.Core (2.3) - FSharp.Compiler.Service (>= 2.0.0.3) +redirects: on +source https://nuget.org/api/v2 + +nuget FSharp.Formatting +nuget FAKE diff --git a/test/acceptance/workspaces/monorepo-with-nuget/test/paket-app/paket.lock b/test/acceptance/workspaces/monorepo-with-nuget/test/paket-app/paket.lock index e69de29bb2..a101caa4fd 100644 --- a/test/acceptance/workspaces/monorepo-with-nuget/test/paket-app/paket.lock +++ b/test/acceptance/workspaces/monorepo-with-nuget/test/paket-app/paket.lock @@ -0,0 +1,10 @@ +REDIRECTS: ON +NUGET + remote: https://www.nuget.org/api/v2 + FAKE (5.8.4) + FSharp.Compiler.Service (2.0.0.6) + FSharp.Formatting (2.14.4) + FSharp.Compiler.Service (2.0.0.6) + FSharpVSPowerTools.Core (>= 2.3 < 2.4) + FSharpVSPowerTools.Core (2.3) + FSharp.Compiler.Service (>= 2.0.0.3) \ No newline at end of file