From 78a16df8d18d3c50123c6d97eabf967072ed4b2e Mon Sep 17 00:00:00 2001 From: maxjeffos <44034094+maxjeffos@users.noreply.github.com> Date: Sun, 1 Aug 2021 11:06:41 -0400 Subject: [PATCH 1/8] test: add test case to test `.add` path --- test/jest/acceptance/analytics.spec.ts | 93 ++++++++++++++++++++++++-- 1 file changed, 86 insertions(+), 7 deletions(-) diff --git a/test/jest/acceptance/analytics.spec.ts b/test/jest/acceptance/analytics.spec.ts index 52ab44263f1..a258585cf8d 100644 --- a/test/jest/acceptance/analytics.spec.ts +++ b/test/jest/acceptance/analytics.spec.ts @@ -1,4 +1,5 @@ import { fakeServer } from '../../acceptance/fake-server'; +import { createProject } from '../util/createProject'; import { runSnykCLI } from '../util/runSnykCLI'; describe('analytics module', () => { @@ -27,11 +28,13 @@ describe('analytics module', () => { }); }); - test('validate actual analytics call', async () => { - server.setNextResponse({}); - const { code } = await runSnykCLI(`version --org=fooOrg --all-projects`, { - env, - }); + it('sends correct analytics data for simple command (`snyk version`)', async () => { + const { code } = await runSnykCLI( + `version --org=fooOrg --all-projects --integrationName=JENKINS --integrationVersion=1.2.3`, + { + env, + }, + ); expect(code).toBe(0); const lastRequest = server.popRequest(); @@ -52,6 +55,8 @@ describe('analytics module', () => { { org: 'fooOrg', allProjects: true, + integrationName: 'JENKINS', + integrationVersion: '1.2.3', }, ], ci: expect.any(Boolean), @@ -63,8 +68,8 @@ describe('analytics module', () => { id: expect.any(String), integrationEnvironment: '', integrationEnvironmentVersion: '', - integrationName: '', - integrationVersion: '', + integrationName: 'JENKINS', + integrationVersion: '1.2.3', // prettier-ignore metrics: { 'network_time': { @@ -87,4 +92,78 @@ describe('analytics module', () => { }, }); }); + + // improves upon the `snyk version` test because the `snyk test` path will include hitting `analytics.add` + it('sends correct analytics data for `snyk test` command', async () => { + const project = await createProject('../acceptance/workspaces/npm-package'); + const { code, stdout } = await runSnykCLI( + 'test --integrationName=JENKINS --integrationVersion=1.2.3', + { + cwd: project.path(), + env, + }, + ); + + console.log(stdout); + expect(code).toBe(0); + + const lastRequest = server.popRequest(); + expect(lastRequest).toMatchObject({ + headers: { + host: 'localhost:12345', + accept: 'application/json', + authorization: 'token 123456789', + 'content-type': 'application/json; charset=utf-8', + 'x-snyk-cli-version': '1.0.0-monorepo', + }, + query: {}, + body: { + data: { + args: [ + { + integrationName: 'JENKINS', + integrationVersion: '1.2.3', + }, + ], + ci: expect.any(Boolean), + command: 'test', + metadata: { + pluginName: 'snyk-nodejs-lockfile-parser', + packageManager: 'npm', + packageName: 'npm-package', + packageVersion: '1.0.0', + isDocker: false, + depGraph: true, + vulns: 0, + }, + durationMs: expect.any(Number), + environment: { + npmVersion: expect.any(String), + }, + id: expect.any(String), + integrationEnvironment: '', + integrationEnvironmentVersion: '', + integrationName: 'JENKINS', + integrationVersion: '1.2.3', + // prettier-ignore + metrics: { + 'network_time': { + type: 'timer', + values: expect.any(Array), + total: expect.any(Number), + }, + 'cpu_time': { + type: 'synthetic', + values: [expect.any(Number)], + total: expect.any(Number), + }, + }, + nodeVersion: process.version, + os: expect.any(String), + standalone: false, + version: '1.0.0-monorepo', + }, + }, + }); + }); }); From 63a221e28edf069551924950ed6aa6e36a6c9ba1 Mon Sep 17 00:00:00 2001 From: maxjeffos <44034094+maxjeffos@users.noreply.github.com> Date: Sun, 1 Aug 2021 14:04:20 -0400 Subject: [PATCH 2/8] refactor: getStandardData for standard analytics data --- src/lib/analytics/getStandardData.ts | 66 +++++++ src/lib/analytics/index.ts | 69 ++----- src/lib/analytics/types.ts | 19 ++ test/jest/system/analytics.spec.ts | 171 ------------------ .../lib/analytics/getStandardData.spec.ts | 89 +++++++++ 5 files changed, 186 insertions(+), 228 deletions(-) create mode 100644 src/lib/analytics/getStandardData.ts create mode 100644 src/lib/analytics/types.ts delete mode 100644 test/jest/system/analytics.spec.ts create mode 100644 test/jest/unit/lib/analytics/getStandardData.spec.ts diff --git a/src/lib/analytics/getStandardData.ts b/src/lib/analytics/getStandardData.ts new file mode 100644 index 00000000000..59cccd41dce --- /dev/null +++ b/src/lib/analytics/getStandardData.ts @@ -0,0 +1,66 @@ +import * as version from '../version'; +import { v4 as uuidv4 } from 'uuid'; +import * as os from 'os'; +const osName = require('os-name'); +import * as crypto from 'crypto'; +import { isCI } from '../is-ci'; +import { + getIntegrationName, + getIntegrationVersion, + getIntegrationEnvironment, + getIntegrationEnvironmentVersion, + getCommandVersion, +} from './sources'; + +import { StandardAnalyticsData } from './types'; +import { MetricsCollector } from '../metrics'; +import * as createDebug from 'debug'; +import { ArgsOptions } from '../../cli/args'; + +const debug = createDebug('snyk'); +const START_TIME = Date.now(); + +function getMetrics(durationMs: number): any[] | undefined { + try { + const networkTime = MetricsCollector.NETWORK_TIME.getTotal(); + const cpuTime = durationMs - networkTime; + MetricsCollector.CPU_TIME.createInstance().setValue(cpuTime); + return MetricsCollector.getAllMetrics(); + } catch (err) { + debug('Error with metrics', err); + } +} + +export async function getStandardData( + args: ArgsOptions[], +): Promise { + const isStandalone = version.isStandaloneBuild(); + const snykVersion = await version.getVersion(); + const seed = uuidv4(); + const shasum = crypto.createHash('sha1'); + const environment = isStandalone + ? {} + : { + npmVersion: await getCommandVersion('npm'), + }; + const durationMs = Date.now() - START_TIME; + + const metrics = getMetrics(durationMs); + + const data = { + os: osName(os.platform(), os.release()), + version: snykVersion, + nodeVersion: process.version, + standalone: isStandalone, + integrationName: getIntegrationName(args), + integrationVersion: getIntegrationVersion(args), + integrationEnvironment: getIntegrationEnvironment(args), + integrationEnvironmentVersion: getIntegrationEnvironmentVersion(args), + id: shasum.update(seed).digest('hex'), + ci: isCI(), + environment, + durationMs, + metrics, + }; + return data; +} diff --git a/src/lib/analytics/index.ts b/src/lib/analytics/index.ts index 60561f8a30e..18f0dd9774c 100644 --- a/src/lib/analytics/index.ts +++ b/src/lib/analytics/index.ts @@ -1,27 +1,13 @@ -import { v4 as uuidv4 } from 'uuid'; const snyk = require('../../lib'); const config = require('../config'); -const version = require('../version'); import { makeRequest } from '../request'; -const { - getIntegrationName, - getIntegrationVersion, - getIntegrationEnvironment, - getIntegrationEnvironmentVersion, - getCommandVersion, -} = require('./sources'); -const isCI = require('../is-ci').isCI; const debug = require('debug')('snyk'); -const os = require('os'); -const osName = require('os-name'); -const crypto = require('crypto'); const stripAnsi = require('strip-ansi'); import * as needle from 'needle'; -const { MetricsCollector } = require('../metrics'); +import { getStandardData } from './getStandardData'; const metadata = {}; // analytics module is required at the beginning of the CLI run cycle -const startTime = Date.now(); /** * @@ -59,10 +45,10 @@ export function allowAnalytics(): boolean { /** * Actually send the analytics to the backend. This can be used standalone to send only the data * given by the data parameter, or called from {@link addDataAndSend}. - * @param data the analytics data to send to the backend. + * @param customData the analytics data to send to the backend. */ export async function postAnalytics( - data, + customData, ): Promise { // if the user opt'ed out of analytics, then let's bail out early // ths applies to all sending to protect user's privacy @@ -72,60 +58,29 @@ export async function postAnalytics( } try { - const isStandalone = version.isStandaloneBuild(); - const snykVersion = await version.getVersion(); - - data.version = snykVersion; - data.os = osName(os.platform(), os.release()); - data.nodeVersion = process.version; - data.standalone = isStandalone; - data.integrationName = getIntegrationName(data.args); - data.integrationVersion = getIntegrationVersion(data.args); - data.integrationEnvironment = getIntegrationEnvironment(data.args); - data.integrationEnvironmentVersion = getIntegrationEnvironmentVersion( - data.args, - ); - - const seed = uuidv4(); - const shasum = crypto.createHash('sha1'); - data.id = shasum.update(seed).digest('hex'); + const standardData = await getStandardData(customData.args); + const analyticsData = { + ...customData, + ...standardData, + }; + debug('analytics', JSON.stringify(analyticsData, null, ' ')); const headers = {}; if (snyk.api) { headers['authorization'] = 'token ' + snyk.api; } - data.ci = isCI(); - - data.environment = {}; - if (!isStandalone) { - data.environment.npmVersion = await getCommandVersion('npm'); - } - - data.durationMs = Date.now() - startTime; - - try { - const networkTime = MetricsCollector.NETWORK_TIME.getTotal(); - const cpuTime = data.durationMs - networkTime; - MetricsCollector.CPU_TIME.createInstance().setValue(cpuTime); - data.metrics = MetricsCollector.getAllMetrics(); - } catch (err) { - debug('Error with metrics', err); - } - const queryStringParams = {}; - if (data.org) { - queryStringParams['org'] = data.org; + if (analyticsData.org) { + queryStringParams['org'] = analyticsData.org; } - debug('analytics', JSON.stringify(data, null, ' ')); - const queryString = Object.keys(queryStringParams).length > 0 ? queryStringParams : undefined; return makeRequest({ body: { - data: data, + data: analyticsData, }, qs: queryString, url: config.API + '/analytics/cli', diff --git a/src/lib/analytics/types.ts b/src/lib/analytics/types.ts new file mode 100644 index 00000000000..d6a80c854cc --- /dev/null +++ b/src/lib/analytics/types.ts @@ -0,0 +1,19 @@ +export type Environment = { + npmVersion?: string | undefined; +}; + +export type StandardAnalyticsData = { + version: string; + os: string; + nodeVersion: string; + standalone: boolean; + integrationName: string; + integrationVersion: string; + integrationEnvironment: string; + integrationEnvironmentVersion: string; + id: string; + ci: boolean; + environment: Environment; + durationMs: number; + metrics: any[] | undefined; +}; diff --git a/test/jest/system/analytics.spec.ts b/test/jest/system/analytics.spec.ts deleted file mode 100644 index c4e60fe86b7..00000000000 --- a/test/jest/system/analytics.spec.ts +++ /dev/null @@ -1,171 +0,0 @@ -const analytics = require('../../../src/lib/analytics'); - -const testTimeout = 50000; -describe('Analytics basic testing', () => { - it( - 'Has all analytics arguments', - async () => { - analytics.add('foo', 'bar'); - const data = { args: [], command: '__test__' }; - const res = await analytics.addDataAndSend(data); - if (!res) { - throw 'analytics creation failed!'; - } - const keys = Object.keys(data).sort(); - expect(keys).toEqual( - [ - 'command', - 'os', - 'version', - 'id', - 'ci', - 'environment', - 'metadata', - 'metrics', - 'args', - 'nodeVersion', - 'standalone', - 'durationMs', - 'integrationName', - 'integrationVersion', - 'integrationEnvironment', - 'integrationEnvironmentVersion', - ].sort(), - ); - }, - testTimeout, - ); - - it( - 'Has all analytics arguments when org is specified', - async () => { - analytics.add('foo', 'bar'); - const data = { args: [], command: '__test__', org: '__snyk__' }; - const res = await analytics.addDataAndSend(data); - if (!res) { - throw 'analytics creation failed!'; - } - const keys = Object.keys(data).sort(); - expect(keys).toEqual( - [ - 'command', - 'os', - 'version', - 'id', - 'ci', - 'environment', - 'metadata', - 'metrics', - 'args', - 'nodeVersion', - 'standalone', - 'durationMs', - 'org', - 'integrationName', - 'integrationVersion', - 'integrationEnvironment', - 'integrationEnvironmentVersion', - ].sort(), - ); - }, - testTimeout, - ); - - it( - 'Has all analytics arguments when args are given', - async () => { - analytics.add('foo', 'bar'); - const data = { - args: [{ integrationName: 'JENKINS', integrationVersion: '1.2.3' }], - command: '__test__', - }; - const res = await analytics.addDataAndSend(data); - if (!res) { - throw 'analytics creation failed!'; - } - const keys = Object.keys(data).sort(); - expect(keys).toEqual( - [ - 'command', - 'os', - 'version', - 'id', - 'ci', - 'environment', - 'metadata', - 'metrics', - 'args', - 'nodeVersion', - 'standalone', - 'durationMs', - 'integrationName', - 'integrationVersion', - 'integrationEnvironment', - 'integrationEnvironmentVersion', - ].sort(), - ); - }, - testTimeout, - ); - - it( - 'Has all analytics arguments when org is specified and args are given', - async () => { - analytics.add('foo', 'bar'); - const data = { - args: [{ integrationName: 'JENKINS', integrationVersion: '1.2.3' }], - command: '__test__', - org: '__snyk__', - }; - const res = await analytics.addDataAndSend(data); - if (!res) { - throw 'analytics creation failed!'; - } - const keys = Object.keys(data).sort(); - expect(keys).toEqual( - [ - 'command', - 'os', - 'version', - 'id', - 'ci', - 'environment', - 'metadata', - 'metrics', - 'args', - 'nodeVersion', - 'standalone', - 'durationMs', - 'org', - 'integrationName', - 'integrationVersion', - 'integrationEnvironment', - 'integrationEnvironmentVersion', - ].sort(), - ); - }, - testTimeout, - ); - - it( - 'Has analytics given values', - async () => { - analytics.add('foo', 'bar'); - const data = { - args: [{ integrationName: 'JENKINS', integrationVersion: '1.2.3' }], - command: '__test__', - org: '__snyk__', - }; - const res = await analytics.addDataAndSend(data); - if (!res) { - throw 'analytics creation failed!'; - } - const vals = Object.values(data); - expect(vals).toContain('__test__'); - expect(vals).toContain('__snyk__'); - expect(vals).toContain('JENKINS'); - expect(vals).toContain('1.2.3'); - }, - testTimeout, - ); -}); diff --git a/test/jest/unit/lib/analytics/getStandardData.spec.ts b/test/jest/unit/lib/analytics/getStandardData.spec.ts new file mode 100644 index 00000000000..74f90cae535 --- /dev/null +++ b/test/jest/unit/lib/analytics/getStandardData.spec.ts @@ -0,0 +1,89 @@ +import { getStandardData } from '../../../../../src/lib/analytics/getStandardData'; +import { getCommandVersion } from '../../../../../src/lib/analytics/sources'; +import { ArgsOptions } from '../../../../../src/cli/args'; + +function argsFrom(args: { [key: string]: string }): ArgsOptions[] { + const fullArgs = ([ + { + ...args, + }, + ] as any) as ArgsOptions[]; + return fullArgs; +} + +describe('getStandardData returns object', () => { + it('contains all the required fields', async () => { + const args = argsFrom({}); + const standardData = await getStandardData(args); + + expect(standardData).toMatchObject({ + os: expect.any(String), + version: '1.0.0-monorepo', + id: expect.any(String), + ci: expect.any(Boolean), + environment: { + npmVersion: await getCommandVersion('npm'), + }, + // prettier-ignore + metrics: { + 'network_time': { + type: 'timer', + values: [], + total: expect.any(Number), + }, + 'cpu_time': { + type: 'synthetic', + values: expect.any(Array), + total: expect.any(Number), + }, + }, + nodeVersion: expect.any(String), + standalone: false, + durationMs: expect.any(Number), + integrationName: expect.any(String), + integrationVersion: expect.any(String), + integrationEnvironment: expect.any(String), + integrationEnvironmentVersion: expect.any(String), + }); + }); + + it('contains all the required fields with integration info', async () => { + const args = argsFrom({ + integrationName: 'JENKINS', + integrationVersion: '1.2.3', + integrationEnvironment: 'TEST_INTEGRATION_ENV', + integrationEnvironmentVersion: '2020.2', + }); + + const standardData = await getStandardData(args); + expect(standardData).toMatchObject({ + os: expect.any(String), + version: '1.0.0-monorepo', + id: expect.any(String), + ci: expect.any(Boolean), + environment: { + npmVersion: await getCommandVersion('npm'), + }, + // prettier-ignore + metrics: { + 'network_time': { + type: 'timer', + values: [], + total: expect.any(Number), + }, + 'cpu_time': { + type: 'synthetic', + values: expect.any(Array), + total: expect.any(Number), + }, + }, + nodeVersion: expect.any(String), + standalone: false, + durationMs: expect.any(Number), + integrationName: 'JENKINS', + integrationVersion: '1.2.3', + integrationEnvironment: 'TEST_INTEGRATION_ENV', + integrationEnvironmentVersion: '2020.2', + }); + }); +}); From b836a9e9b174ab9fab4b5cd19efc7aa1f492a80a Mon Sep 17 00:00:00 2001 From: maxjeffos <44034094+maxjeffos@users.noreply.github.com> Date: Fri, 6 Aug 2021 15:45:09 -0400 Subject: [PATCH 3/8] test: replace analytics tap tests with new acceptance tests --- test/analytics.test.ts | 300 ---------- .../package-lock.json | 27 + .../with-vulnerable-lodash-dep/package.json | 14 + .../test-dep-graph-result.json | 515 ++++++++++++++++++ test/jest/acceptance/analytics.spec.ts | 220 +++++++- 5 files changed, 774 insertions(+), 302 deletions(-) create mode 100644 test/fixtures/npm/with-vulnerable-lodash-dep/package-lock.json create mode 100644 test/fixtures/npm/with-vulnerable-lodash-dep/package.json create mode 100644 test/fixtures/npm/with-vulnerable-lodash-dep/test-dep-graph-result.json diff --git a/test/analytics.test.ts b/test/analytics.test.ts index cba51ff0576..3f831faaf0b 100644 --- a/test/analytics.test.ts +++ b/test/analytics.test.ts @@ -2,7 +2,6 @@ import * as tap from 'tap'; import * as Proxyquire from 'proxyquire'; import * as sinon from 'sinon'; import * as snyk from '../src/lib'; -import * as semver from 'semver'; let old; const proxyquire = Proxyquire.noPreserveCache(); const { test } = tap; @@ -50,302 +49,3 @@ test('analytics disabled', (t) => { t.equal(spy.called, false, 'the request should not have been made'); }); }); - -test('analytics', (t) => { - const spy = sinon.spy(); - const analytics = proxyquire('../src/lib/analytics', { - '../request': { - makeRequest: spy, - }, - }); - - analytics.add('foo', 'bar'); - - return analytics - .addDataAndSend({ - command: '__test__', - args: [ - { - integrationName: 'JENKINS', - integrationVersion: '1.2.3', - }, - ], - }) - .then(() => { - const body = spy.lastCall.args[0].body.data; - t.deepEqual( - Object.keys(body).sort(), - [ - 'command', - 'os', - 'version', - 'id', - 'ci', - 'environment', - 'metadata', - 'metrics', - 'args', - 'nodeVersion', - 'standalone', - 'durationMs', - 'integrationName', - 'integrationVersion', - 'integrationEnvironment', - 'integrationEnvironmentVersion', - ].sort(), - 'keys as expected', - ); - - const queryString = spy.lastCall.args[0].qs; - t.deepEqual(queryString, undefined, 'query string is empty'); - }); -}); - -test('analytics with args', (t) => { - const spy = sinon.spy(); - const analytics = proxyquire('../src/lib/analytics', { - '../request': { - makeRequest: spy, - }, - }); - - analytics.add('foo', 'bar'); - - return analytics - .addDataAndSend({ - command: '__test__', - args: [], - }) - .then(() => { - const body = spy.lastCall.args[0].body.data; - t.deepEqual( - Object.keys(body).sort(), - [ - 'command', - 'os', - 'version', - 'id', - 'ci', - 'environment', - 'metadata', - 'metrics', - 'args', - 'nodeVersion', - 'standalone', - 'durationMs', - 'integrationName', - 'integrationVersion', - 'integrationEnvironment', - 'integrationEnvironmentVersion', - ].sort(), - 'keys as expected', - ); - - const queryString = spy.lastCall.args[0].qs; - t.deepEqual(queryString, undefined, 'query string is empty'); - }); -}); - -test('analytics with args and org', (t) => { - const spy = sinon.spy(); - const analytics = proxyquire('../src/lib/analytics', { - '../request': { - makeRequest: spy, - }, - }); - - analytics.add('foo', 'bar'); - - return analytics - .addDataAndSend({ - command: '__test__', - args: [], - org: 'snyk', - }) - .then(() => { - const body = spy.lastCall.args[0].body.data; - t.deepEqual( - Object.keys(body).sort(), - [ - 'command', - 'os', - 'version', - 'id', - 'ci', - 'environment', - 'metadata', - 'metrics', - 'args', - 'nodeVersion', - 'standalone', - 'durationMs', - 'org', - 'integrationName', - 'integrationVersion', - 'integrationEnvironment', - 'integrationEnvironmentVersion', - ].sort(), - 'keys as expected', - ); - - const queryString = spy.lastCall.args[0].qs; - t.deepEqual( - queryString, - { org: 'snyk' }, - 'query string has the expected values', - ); - }); -}); - -test('analytics npm version capture', (t) => { - const spy = sinon.spy(); - const analytics = proxyquire('../src/lib/analytics', { - '../request': { - makeRequest: spy, - }, - }); - - analytics.add('foo', 'bar'); - - return analytics - .addDataAndSend({ - command: '__test__', - args: [], - }) - .then(() => { - const body = spy.lastCall.args[0].body.data; - if (body.environment.npmVersion === undefined) { - t.ok( - semver.valid(body.environment.npmVersion) === null, - 'captured npm version is not valid as expected', - ); - } else { - t.ok( - semver.valid(body.environment.npmVersion) !== null, - 'captured npm version is valid', - ); - } - }); -}); - -test('bad command', (t) => { - const spy = sinon.spy(); - process.argv = ['node', 'script.js', 'random command', '-q']; - const cli = proxyquire('../src/cli', { - '../lib/analytics': proxyquire('../src/lib/analytics', { - '../request': { - makeRequest: spy, - }, - }), - }); - - return cli.then(() => { - t.equal(spy.callCount, 1, 'analytics was called'); - - const payload = spy.args[0][0].body; - t.equal(payload.data.command, 'bad-command', 'correct event name'); - t.equal( - payload.data.metadata.command, - 'random command', - 'found original command', - ); - t.equal( - payload.data.metadata['error-message'], - 'Unknown command "random command"', - 'got correct error', - ); - }); -}); - -test('bad command with string error', (t) => { - const spy = sinon.spy(); - process.argv = ['node', 'script.js', 'test', '-q']; - const error = new Error('Some error') as any; - error.code = 'CODE'; - const cli = proxyquire('../src/cli', { - '../lib/analytics': proxyquire('../src/lib/analytics', { - '../request': { - makeRequest: spy, - }, - }), - - './args': proxyquire('../src/cli/args', { - './commands': proxyquire('../src/cli/commands', { - '../../lib/hotload': proxyquire('../src/lib/hotload', { - // windows-based testing uses windows path separator - '..\\cli\\commands\\test'() { - return Promise.reject(error); - }, - '../cli/commands/test'() { - return Promise.reject(error); - }, - }), - }), - }), - }); - - return cli.then(() => { - t.equal(spy.callCount, 1, 'analytics was called'); - - const payload = spy.args[0][0].body; - t.equal(payload.data.command, 'bad-command', 'correct event name'); - t.equal(payload.data.metadata.command, 'test', 'found original command'); - t.match(payload.data.metadata.error, 'Some error', 'got correct error'); - }); -}); - -test('vulns found (thrown as an error)', (t) => { - const spy = sinon.spy(); - process.argv = ['node', 'script.js', 'test', '-q']; - const error = new Error('7 vulnerable dependency paths') as any; - error.code = 'VULNS'; - const cli = proxyquire('../src/cli', { - '../lib/analytics': proxyquire('../src/lib/analytics', { - '../request': { - makeRequest: spy, - }, - }), - - './args': proxyquire('../src/cli/args', { - './commands': proxyquire('../src/cli/commands', { - '../../lib/hotload': proxyquire('../src/lib/hotload', { - // windows-based testing uses windows path separator - '..\\cli\\commands\\test'() { - return Promise.reject(error); - }, - '../cli/commands/test'() { - return Promise.reject(error); - }, - }), - }), - }), - }); - - return cli.then(() => { - t.equal(spy.callCount, 1, 'analytics was called'); - - const payload = spy.args[0][0].body; - t.equal(payload.data.command, 'test', 'correct event name'); - t.equal(payload.data.metadata.command, 'test', 'found original command'); - t.equal( - payload.data.metadata['error-message'], - 'Vulnerabilities found', - 'got correct vuln count', - ); - }); -}); - -test('analytics was called', (t) => { - const spy = sinon.spy(); - const cli = proxyquire('../src/cli', { - '../lib/analytics': proxyquire('../src/lib/analytics', { - '../request': { - makeRequest: spy, - }, - }), - }); - - return cli.then(() => { - t.equal(spy.callCount, 1, 'analytics was called'); - }); -}); diff --git a/test/fixtures/npm/with-vulnerable-lodash-dep/package-lock.json b/test/fixtures/npm/with-vulnerable-lodash-dep/package-lock.json new file mode 100644 index 00000000000..04abbe4c528 --- /dev/null +++ b/test/fixtures/npm/with-vulnerable-lodash-dep/package-lock.json @@ -0,0 +1,27 @@ +{ + "name": "with-vulnerable-lodash-dep", + "version": "1.2.3", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "version": "1.2.3", + "license": "ISC", + "dependencies": { + "lodash": "4.17.15" + } + }, + "node_modules/lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + } + }, + "dependencies": { + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + } + } +} diff --git a/test/fixtures/npm/with-vulnerable-lodash-dep/package.json b/test/fixtures/npm/with-vulnerable-lodash-dep/package.json new file mode 100644 index 00000000000..2250171781d --- /dev/null +++ b/test/fixtures/npm/with-vulnerable-lodash-dep/package.json @@ -0,0 +1,14 @@ +{ + "name": "with-vulnerable-lodash-dep", + "version": "1.2.3", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "license": "ISC", + "dependencies": { + "lodash": "4.17.15" + } +} diff --git a/test/fixtures/npm/with-vulnerable-lodash-dep/test-dep-graph-result.json b/test/fixtures/npm/with-vulnerable-lodash-dep/test-dep-graph-result.json new file mode 100644 index 00000000000..6af6876b2b9 --- /dev/null +++ b/test/fixtures/npm/with-vulnerable-lodash-dep/test-dep-graph-result.json @@ -0,0 +1,515 @@ +{ + "result": { + "affectedPkgs": { + "lodash@4.17.15": { + "pkg": { + "name": "lodash", + "version": "4.17.15" + }, + "issues": { + "SNYK-JS-LODASH-1018905": { + "issueId": "SNYK-JS-LODASH-1018905", + "fixInfo": { + "isPatchable": false, + "upgradePaths": [ + { + "path": [ + { + "name": "with-vulnerable-lodash-dep", + "version": "1.2.3" + }, + { + "name": "lodash", + "version": "4.17.15", + "newVersion": "4.17.21" + } + ] + } + ], + "isPinnable": false + } + }, + "SNYK-JS-LODASH-1040724": { + "issueId": "SNYK-JS-LODASH-1040724", + "fixInfo": { + "isPatchable": false, + "upgradePaths": [ + { + "path": [ + { + "name": "with-vulnerable-lodash-dep", + "version": "1.2.3" + }, + { + "name": "lodash", + "version": "4.17.15", + "newVersion": "4.17.21" + } + ] + } + ], + "isPinnable": false + } + }, + "SNYK-JS-LODASH-567746": { + "issueId": "SNYK-JS-LODASH-567746", + "fixInfo": { + "isPatchable": true, + "upgradePaths": [ + { + "path": [ + { + "name": "with-vulnerable-lodash-dep", + "version": "1.2.3" + }, + { + "name": "lodash", + "version": "4.17.15", + "newVersion": "4.17.16" + } + ] + } + ], + "isPinnable": false + } + }, + "SNYK-JS-LODASH-590103": { + "issueId": "SNYK-JS-LODASH-590103", + "fixInfo": { + "isPatchable": false, + "upgradePaths": [ + { + "path": [ + { + "name": "with-vulnerable-lodash-dep", + "version": "1.2.3" + }, + { + "name": "lodash", + "version": "4.17.15", + "newVersion": "4.17.20" + } + ] + } + ], + "isPinnable": false + } + }, + "SNYK-JS-LODASH-608086": { + "issueId": "SNYK-JS-LODASH-608086", + "fixInfo": { + "isPatchable": false, + "upgradePaths": [ + { + "path": [ + { + "name": "with-vulnerable-lodash-dep", + "version": "1.2.3" + }, + { + "name": "lodash", + "version": "4.17.15", + "newVersion": "4.17.17" + } + ] + } + ], + "isPinnable": false + } + } + } + } + }, + "issuesData": { + "SNYK-JS-LODASH-1018905": { + "CVSSv3": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L/E:P", + "alternativeIds": [], + "creationTime": "2020-10-16T16:48:40.985673Z", + "credit": [ + "Liyuan Chen" + ], + "cvssScore": 5.3, + "description": "## Overview\n[lodash](https://www.npmjs.com/package/lodash) is a modern JavaScript utility library delivering modularity, performance, & extras.\n\nAffected versions of this package are vulnerable to Regular Expression Denial of Service (ReDoS) via the `toNumber`, `trim` and `trimEnd` functions.\r\n\r\n### POC\r\n```\r\nvar lo = require('lodash');\r\n\r\nfunction build_blank (n) {\r\nvar ret = \"1\"\r\nfor (var i = 0; i < n; i++) {\r\nret += \" \"\r\n}\r\n\r\nreturn ret + \"1\";\r\n}\r\n\r\nvar s = build_blank(50000)\r\nvar time0 = Date.now();\r\nlo.trim(s)\r\nvar time_cost0 = Date.now() - time0;\r\nconsole.log(\"time_cost0: \" + time_cost0)\r\n\r\nvar time1 = Date.now();\r\nlo.toNumber(s)\r\nvar time_cost1 = Date.now() - time1;\r\nconsole.log(\"time_cost1: \" + time_cost1)\r\n\r\nvar time2 = Date.now();\r\nlo.trimEnd(s)\r\nvar time_cost2 = Date.now() - time2;\r\nconsole.log(\"time_cost2: \" + time_cost2)\r\n```\n\n## Details\n\nDenial of Service (DoS) describes a family of attacks, all aimed at making a system inaccessible to its original and legitimate users. There are many types of DoS attacks, ranging from trying to clog the network pipes to the system by generating a large volume of traffic from many machines (a Distributed Denial of Service - DDoS - attack) to sending crafted requests that cause a system to crash or take a disproportional amount of time to process.\n\nThe Regular expression Denial of Service (ReDoS) is a type of Denial of Service attack. Regular expressions are incredibly powerful, but they aren't very intuitive and can ultimately end up making it easy for attackers to take your site down.\n\nLet’s take the following regular expression as an example:\n```js\nregex = /A(B|C+)+D/\n```\n\nThis regular expression accomplishes the following:\n- `A` The string must start with the letter 'A'\n- `(B|C+)+` The string must then follow the letter A with either the letter 'B' or some number of occurrences of the letter 'C' (the `+` matches one or more times). The `+` at the end of this section states that we can look for one or more matches of this section.\n- `D` Finally, we ensure this section of the string ends with a 'D'\n\nThe expression would match inputs such as `ABBD`, `ABCCCCD`, `ABCBCCCD` and `ACCCCCD`\n\nIt most cases, it doesn't take very long for a regex engine to find a match:\n\n```bash\n$ time node -e '/A(B|C+)+D/.test(\"ACCCCCCCCCCCCCCCCCCCCCCCCCCCCD\")'\n0.04s user 0.01s system 95% cpu 0.052 total\n\n$ time node -e '/A(B|C+)+D/.test(\"ACCCCCCCCCCCCCCCCCCCCCCCCCCCCX\")'\n1.79s user 0.02s system 99% cpu 1.812 total\n```\n\nThe entire process of testing it against a 30 characters long string takes around ~52ms. But when given an invalid string, it takes nearly two seconds to complete the test, over ten times as long as it took to test a valid string. The dramatic difference is due to the way regular expressions get evaluated.\n\nMost Regex engines will work very similarly (with minor differences). The engine will match the first possible way to accept the current character and proceed to the next one. If it then fails to match the next one, it will backtrack and see if there was another way to digest the previous character. If it goes too far down the rabbit hole only to find out the string doesn’t match in the end, and if many characters have multiple valid regex paths, the number of backtracking steps can become very large, resulting in what is known as _catastrophic backtracking_.\n\nLet's look at how our expression runs into this problem, using a shorter string: \"ACCCX\". While it seems fairly straightforward, there are still four different ways that the engine could match those three C's:\n1. CCC\n2. CC+C\n3. C+CC\n4. C+C+C.\n\nThe engine has to try each of those combinations to see if any of them potentially match against the expression. When you combine that with the other steps the engine must take, we can use [RegEx 101 debugger](https://regex101.com/debugger) to see the engine has to take a total of 38 steps before it can determine the string doesn't match.\n\nFrom there, the number of steps the engine must use to validate a string just continues to grow.\n\n| String | Number of C's | Number of steps |\n| -------|-------------:| -----:|\n| ACCCX | 3 | 38\n| ACCCCX | 4 | 71\n| ACCCCCX | 5 | 136\n| ACCCCCCCCCCCCCCX | 14 | 65,553\n\n\nBy the time the string includes 14 C's, the engine has to take over 65,000 steps just to see if the string is valid. These extreme situations can cause them to work very slowly (exponentially related to input size, as shown above), allowing an attacker to exploit this and can cause the service to excessively consume CPU, resulting in a Denial of Service.\n\n## Remediation\nUpgrade `lodash` to version 4.17.21 or higher.\n## References\n- [GitHub Commit](https://github.com/lodash/lodash/commit/c4847ebe7d14540bb28a8b932a9ce1b9ecbfee1a)\n- [GitHub Fix PR](https://github.com/lodash/lodash/pull/5065)\n", + "disclosureTime": "2020-10-16T16:47:34Z", + "exploit": "Proof of Concept", + "fixedIn": [ + "4.17.21" + ], + "functions": [], + "functions_new": [], + "id": "SNYK-JS-LODASH-1018905", + "identifiers": { + "CVE": [ + "CVE-2020-28500" + ], + "CWE": [ + "CWE-400" + ] + }, + "language": "js", + "modificationTime": "2021-02-22T09:58:41.562106Z", + "moduleName": "lodash", + "packageManager": "npm", + "packageName": "lodash", + "patches": [], + "proprietary": true, + "publicationTime": "2021-02-15T11:50:49Z", + "references": [ + { + "title": "GitHub Commit", + "url": "https://github.com/lodash/lodash/commit/c4847ebe7d14540bb28a8b932a9ce1b9ecbfee1a" + }, + { + "title": "GitHub Fix PR", + "url": "https://github.com/lodash/lodash/pull/5065" + } + ], + "semver": { + "vulnerable": [ + "<4.17.21" + ] + }, + "severity": "medium", + "severityWithCritical": "medium", + "socialTrendAlert": false, + "title": "Regular Expression Denial of Service (ReDoS)" + }, + "SNYK-JS-LODASH-1040724": { + "CVSSv3": "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H/E:P/RL:U/RC:C", + "alternativeIds": [], + "creationTime": "2020-11-17T14:07:17.048472Z", + "credit": [ + "Marc Hassan" + ], + "cvssScore": 7.2, + "description": "## Overview\n[lodash](https://www.npmjs.com/package/lodash) is a modern JavaScript utility library delivering modularity, performance, & extras.\n\nAffected versions of this package are vulnerable to Command Injection via `template`.\r\n\r\n### PoC\r\n```\r\nvar _ = require('lodash');\r\n\r\n_.template('', { variable: '){console.log(process.env)}; with(obj' })()\r\n```\n## Remediation\nUpgrade `lodash` to version 4.17.21 or higher.\n## References\n- [GitHub Commit](https://github.com/lodash/lodash/commit/3469357cff396a26c363f8c1b5a91dde28ba4b1c)\n- [Vulnerable Code](https://github.com/lodash/lodash/blob/ddfd9b11a0126db2302cb70ec9973b66baec0975/lodash.js#L14851)\n", + "disclosureTime": "2020-11-17T13:02:10Z", + "exploit": "Proof of Concept", + "fixedIn": [ + "4.17.21" + ], + "functions": [], + "functions_new": [], + "id": "SNYK-JS-LODASH-1040724", + "identifiers": { + "CVE": [ + "CVE-2021-23337" + ], + "CWE": [ + "CWE-78" + ], + "GHSA": [ + "GHSA-35jh-r3h4-6jhm" + ] + }, + "language": "js", + "modificationTime": "2021-02-22T09:58:04.543992Z", + "moduleName": "lodash", + "packageManager": "npm", + "packageName": "lodash", + "patches": [], + "proprietary": true, + "publicationTime": "2021-02-15T11:50:50Z", + "references": [ + { + "title": "GitHub Commit", + "url": "https://github.com/lodash/lodash/commit/3469357cff396a26c363f8c1b5a91dde28ba4b1c" + }, + { + "title": "Vulnerable Code", + "url": "https://github.com/lodash/lodash/blob/ddfd9b11a0126db2302cb70ec9973b66baec0975/lodash.js%23L14851" + } + ], + "semver": { + "vulnerable": [ + "<4.17.21" + ] + }, + "severity": "high", + "severityWithCritical": "high", + "socialTrendAlert": false, + "title": "Command Injection" + }, + "SNYK-JS-LODASH-567746": { + "CVSSv3": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:L/E:P/RL:U/RC:C", + "alternativeIds": [], + "creationTime": "2020-04-28T14:32:13.683154Z", + "credit": [ + "posix" + ], + "cvssScore": 6.3, + "description": "## Overview\n[lodash](https://www.npmjs.com/package/lodash) is a modern JavaScript utility library delivering modularity, performance, & extras.\n\nAffected versions of this package are vulnerable to Prototype Pollution. The function `zipObjectDeep` can be tricked into adding or modifying properties of the Object prototype. These properties will be present on all objects.\r\n\r\n## PoC\r\n```\r\nconst _ = require('lodash');\r\n_.zipObjectDeep(['__proto__.z'],[123])\r\nconsole.log(z) // 123\r\n```\n\n## Details\n\nPrototype Pollution is a vulnerability affecting JavaScript. Prototype Pollution refers to the ability to inject properties into existing JavaScript language construct prototypes, such as objects. JavaScript allows all Object attributes to be altered, including their magical attributes such as `_proto_`, `constructor` and `prototype`. An attacker manipulates these attributes to overwrite, or pollute, a JavaScript application object prototype of the base object by injecting other values. Properties on the `Object.prototype` are then inherited by all the JavaScript objects through the prototype chain. When that happens, this leads to either denial of service by triggering JavaScript exceptions, or it tampers with the application source code to force the code path that the attacker injects, thereby leading to remote code execution.\n\nThere are two main ways in which the pollution of prototypes occurs:\n\n- Unsafe `Object` recursive merge\n \n- Property definition by path\n \n\n### Unsafe Object recursive merge\n\nThe logic of a vulnerable recursive merge function follows the following high-level model:\n```\nmerge (target, source)\n\n foreach property of source\n\n if property exists and is an object on both the target and the source\n\n merge(target[property], source[property])\n\n else\n\n target[property] = source[property]\n```\n
\n\nWhen the source object contains a property named `_proto_` defined with `Object.defineProperty()` , the condition that checks if the property exists and is an object on both the target and the source passes and the merge recurses with the target, being the prototype of `Object` and the source of `Object` as defined by the attacker. Properties are then copied on the `Object` prototype.\n\nClone operations are a special sub-class of unsafe recursive merges, which occur when a recursive merge is conducted on an empty object: `merge({},source)`.\n\n`lodash` and `Hoek` are examples of libraries susceptible to recursive merge attacks.\n\n### Property definition by path\n\nThere are a few JavaScript libraries that use an API to define property values on an object based on a given path. The function that is generally affected contains this signature: `theFunction(object, path, value)`\n\nIf the attacker can control the value of “path”, they can set this value to `_proto_.myValue`. `myValue` is then assigned to the prototype of the class of the object.\n\n## Types of attacks\n\nThere are a few methods by which Prototype Pollution can be manipulated:\n\n| Type |Origin |Short description |\n|--|--|--|\n| **Denial of service (DoS)**|Client |This is the most likely attack.
DoS occurs when `Object` holds generic functions that are implicitly called for various operations (for example, `toString` and `valueOf`).
The attacker pollutes `Object.prototype.someattr` and alters its state to an unexpected value such as `Int` or `Object`. In this case, the code fails and is likely to cause a denial of service.
**For example:** if an attacker pollutes `Object.prototype.toString` by defining it as an integer, if the codebase at any point was reliant on `someobject.toString()` it would fail. |\n |**Remote Code Execution**|Client|Remote code execution is generally only possible in cases where the codebase evaluates a specific attribute of an object, and then executes that evaluation.
**For example:** `eval(someobject.someattr)`. In this case, if the attacker pollutes `Object.prototype.someattr` they are likely to be able to leverage this in order to execute code.|\n|**Property Injection**|Client|The attacker pollutes properties that the codebase relies on for their informative value, including security properties such as cookies or tokens.
**For example:** if a codebase checks privileges for `someuser.isAdmin`, then when the attacker pollutes `Object.prototype.isAdmin` and sets it to equal `true`, they can then achieve admin privileges.|\n\n## Affected environments\n\nThe following environments are susceptible to a Prototype Pollution attack:\n\n- Application server\n \n- Web server\n \n\n## How to prevent\n\n1. Freeze the prototype— use `Object.freeze (Object.prototype)`.\n \n2. Require schema validation of JSON input.\n \n3. Avoid using unsafe recursive merge functions.\n \n4. Consider using objects without prototypes (for example, `Object.create(null)`), breaking the prototype chain and preventing pollution.\n \n5. As a best practice use `Map` instead of `Object`.\n\n### For more information on this vulnerability type:\n\n[Arteau, Oliver. “JavaScript prototype pollution attack in NodeJS application.” GitHub, 26 May 2018](https://github.com/HoLyVieR/prototype-pollution-nsec18/blob/master/paper/JavaScript_prototype_pollution_attack_in_NodeJS.pdf)\n\n## Remediation\nUpgrade `lodash` to version 4.17.16 or higher.\n## References\n- [GitHub PR](https://github.com/lodash/lodash/pull/4759)\n- [HackerOne Report](https://hackerone.com/reports/712065)\n", + "disclosureTime": "2020-04-27T22:14:18Z", + "exploit": "Proof of Concept", + "fixedIn": [ + "4.17.16" + ], + "functions": [], + "functions_new": [], + "id": "SNYK-JS-LODASH-567746", + "identifiers": { + "CVE": [ + "CVE-2020-8203" + ], + "CWE": [ + "CWE-400" + ], + "GHSA": [ + "GHSA-p6mc-m468-83gw" + ], + "NSP": [ + 1523 + ] + }, + "language": "js", + "modificationTime": "2020-07-09T08:34:04.944267Z", + "moduleName": "lodash", + "packageManager": "npm", + "packageName": "lodash", + "patches": [ + { + "comments": [], + "id": "patch:SNYK-JS-LODASH-567746:0", + "modificationTime": "2020-04-30T14:28:46.729327Z", + "urls": [ + "https://snyk-patches.s3.amazonaws.com/npm/lodash/20200430/lodash_0_0_20200430_6baae67d501e4c45021280876d42efe351e77551.patch" + ], + "version": ">=4.14.2" + } + ], + "proprietary": false, + "publicationTime": "2020-04-28T14:59:14Z", + "references": [ + { + "title": "GitHub PR", + "url": "https://github.com/lodash/lodash/pull/4759" + }, + { + "title": "HackerOne Report", + "url": "https://hackerone.com/reports/712065" + } + ], + "semver": { + "vulnerable": [ + "<4.17.16" + ] + }, + "severity": "medium", + "severityWithCritical": "medium", + "socialTrendAlert": false, + "title": "Prototype Pollution" + }, + "SNYK-JS-LODASH-590103": { + "CVSSv3": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", + "alternativeIds": [], + "creationTime": "2020-07-24T12:05:01.916784Z", + "credit": [ + "reeser" + ], + "cvssScore": 9.8, + "description": "## Overview\n[lodash](https://www.npmjs.com/package/lodash) is a modern JavaScript utility library delivering modularity, performance, & extras.\n\nAffected versions of this package are vulnerable to Prototype Pollution in `zipObjectDeep` due to an incomplete fix for [CVE-2020-8203](https://snyk.io/vuln/SNYK-JS-LODASH-567746).\n\n## Details\n\nPrototype Pollution is a vulnerability affecting JavaScript. Prototype Pollution refers to the ability to inject properties into existing JavaScript language construct prototypes, such as objects. JavaScript allows all Object attributes to be altered, including their magical attributes such as `_proto_`, `constructor` and `prototype`. An attacker manipulates these attributes to overwrite, or pollute, a JavaScript application object prototype of the base object by injecting other values. Properties on the `Object.prototype` are then inherited by all the JavaScript objects through the prototype chain. When that happens, this leads to either denial of service by triggering JavaScript exceptions, or it tampers with the application source code to force the code path that the attacker injects, thereby leading to remote code execution.\n\nThere are two main ways in which the pollution of prototypes occurs:\n\n- Unsafe `Object` recursive merge\n \n- Property definition by path\n \n\n### Unsafe Object recursive merge\n\nThe logic of a vulnerable recursive merge function follows the following high-level model:\n```\nmerge (target, source)\n\n foreach property of source\n\n if property exists and is an object on both the target and the source\n\n merge(target[property], source[property])\n\n else\n\n target[property] = source[property]\n```\n
\n\nWhen the source object contains a property named `_proto_` defined with `Object.defineProperty()` , the condition that checks if the property exists and is an object on both the target and the source passes and the merge recurses with the target, being the prototype of `Object` and the source of `Object` as defined by the attacker. Properties are then copied on the `Object` prototype.\n\nClone operations are a special sub-class of unsafe recursive merges, which occur when a recursive merge is conducted on an empty object: `merge({},source)`.\n\n`lodash` and `Hoek` are examples of libraries susceptible to recursive merge attacks.\n\n### Property definition by path\n\nThere are a few JavaScript libraries that use an API to define property values on an object based on a given path. The function that is generally affected contains this signature: `theFunction(object, path, value)`\n\nIf the attacker can control the value of “path”, they can set this value to `_proto_.myValue`. `myValue` is then assigned to the prototype of the class of the object.\n\n## Types of attacks\n\nThere are a few methods by which Prototype Pollution can be manipulated:\n\n| Type |Origin |Short description |\n|--|--|--|\n| **Denial of service (DoS)**|Client |This is the most likely attack.
DoS occurs when `Object` holds generic functions that are implicitly called for various operations (for example, `toString` and `valueOf`).
The attacker pollutes `Object.prototype.someattr` and alters its state to an unexpected value such as `Int` or `Object`. In this case, the code fails and is likely to cause a denial of service.
**For example:** if an attacker pollutes `Object.prototype.toString` by defining it as an integer, if the codebase at any point was reliant on `someobject.toString()` it would fail. |\n |**Remote Code Execution**|Client|Remote code execution is generally only possible in cases where the codebase evaluates a specific attribute of an object, and then executes that evaluation.
**For example:** `eval(someobject.someattr)`. In this case, if the attacker pollutes `Object.prototype.someattr` they are likely to be able to leverage this in order to execute code.|\n|**Property Injection**|Client|The attacker pollutes properties that the codebase relies on for their informative value, including security properties such as cookies or tokens.
**For example:** if a codebase checks privileges for `someuser.isAdmin`, then when the attacker pollutes `Object.prototype.isAdmin` and sets it to equal `true`, they can then achieve admin privileges.|\n\n## Affected environments\n\nThe following environments are susceptible to a Prototype Pollution attack:\n\n- Application server\n \n- Web server\n \n\n## How to prevent\n\n1. Freeze the prototype— use `Object.freeze (Object.prototype)`.\n \n2. Require schema validation of JSON input.\n \n3. Avoid using unsafe recursive merge functions.\n \n4. Consider using objects without prototypes (for example, `Object.create(null)`), breaking the prototype chain and preventing pollution.\n \n5. As a best practice use `Map` instead of `Object`.\n\n### For more information on this vulnerability type:\n\n[Arteau, Oliver. “JavaScript prototype pollution attack in NodeJS application.” GitHub, 26 May 2018](https://github.com/HoLyVieR/prototype-pollution-nsec18/blob/master/paper/JavaScript_prototype_pollution_attack_in_NodeJS.pdf)\n\n## Remediation\nUpgrade `lodash` to version 4.17.20 or higher.\n## References\n- [GitHub Issue](https://github.com/lodash/lodash/issues/4874)\n", + "disclosureTime": "2020-07-24T12:00:52Z", + "exploit": "Not Defined", + "fixedIn": [ + "4.17.20" + ], + "functions": [], + "functions_new": [], + "id": "SNYK-JS-LODASH-590103", + "identifiers": { + "CVE": [], + "CWE": [ + "CWE-400" + ] + }, + "language": "js", + "modificationTime": "2020-08-16T12:11:40.402299Z", + "moduleName": "lodash", + "packageManager": "npm", + "packageName": "lodash", + "patches": [], + "proprietary": false, + "publicationTime": "2020-08-16T13:09:06Z", + "references": [ + { + "title": "GitHub Issue", + "url": "https://github.com/lodash/lodash/issues/4874" + } + ], + "semver": { + "vulnerable": [ + "<4.17.20" + ] + }, + "severity": "high", + "severityWithCritical": "critical", + "socialTrendAlert": false, + "title": "Prototype Pollution" + }, + "SNYK-JS-LODASH-608086": { + "CVSSv3": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L/E:P/RL:O/RC:C", + "alternativeIds": [], + "creationTime": "2020-08-21T12:52:58.443440Z", + "credit": [ + "awarau" + ], + "cvssScore": 7.3, + "description": "## Overview\n[lodash](https://www.npmjs.com/package/lodash) is a modern JavaScript utility library delivering modularity, performance, & extras.\n\nAffected versions of this package are vulnerable to Prototype Pollution via the `setWith` and `set` functions.\r\n\r\n### PoC by awarau\r\n* Create a JS file with this contents:\r\n```\r\nlod = require('lodash')\r\nlod.setWith({}, \"__proto__[test]\", \"123\")\r\nlod.set({}, \"__proto__[test2]\", \"456\")\r\nconsole.log(Object.prototype)\r\n```\r\n* Execute it with `node`\r\n* Observe that `test` and `test2` is now in the `Object.prototype`.\n\n## Details\n\nPrototype Pollution is a vulnerability affecting JavaScript. Prototype Pollution refers to the ability to inject properties into existing JavaScript language construct prototypes, such as objects. JavaScript allows all Object attributes to be altered, including their magical attributes such as `_proto_`, `constructor` and `prototype`. An attacker manipulates these attributes to overwrite, or pollute, a JavaScript application object prototype of the base object by injecting other values. Properties on the `Object.prototype` are then inherited by all the JavaScript objects through the prototype chain. When that happens, this leads to either denial of service by triggering JavaScript exceptions, or it tampers with the application source code to force the code path that the attacker injects, thereby leading to remote code execution.\n\nThere are two main ways in which the pollution of prototypes occurs:\n\n- Unsafe `Object` recursive merge\n \n- Property definition by path\n \n\n### Unsafe Object recursive merge\n\nThe logic of a vulnerable recursive merge function follows the following high-level model:\n```\nmerge (target, source)\n\n foreach property of source\n\n if property exists and is an object on both the target and the source\n\n merge(target[property], source[property])\n\n else\n\n target[property] = source[property]\n```\n
\n\nWhen the source object contains a property named `_proto_` defined with `Object.defineProperty()` , the condition that checks if the property exists and is an object on both the target and the source passes and the merge recurses with the target, being the prototype of `Object` and the source of `Object` as defined by the attacker. Properties are then copied on the `Object` prototype.\n\nClone operations are a special sub-class of unsafe recursive merges, which occur when a recursive merge is conducted on an empty object: `merge({},source)`.\n\n`lodash` and `Hoek` are examples of libraries susceptible to recursive merge attacks.\n\n### Property definition by path\n\nThere are a few JavaScript libraries that use an API to define property values on an object based on a given path. The function that is generally affected contains this signature: `theFunction(object, path, value)`\n\nIf the attacker can control the value of “path”, they can set this value to `_proto_.myValue`. `myValue` is then assigned to the prototype of the class of the object.\n\n## Types of attacks\n\nThere are a few methods by which Prototype Pollution can be manipulated:\n\n| Type |Origin |Short description |\n|--|--|--|\n| **Denial of service (DoS)**|Client |This is the most likely attack.
DoS occurs when `Object` holds generic functions that are implicitly called for various operations (for example, `toString` and `valueOf`).
The attacker pollutes `Object.prototype.someattr` and alters its state to an unexpected value such as `Int` or `Object`. In this case, the code fails and is likely to cause a denial of service.
**For example:** if an attacker pollutes `Object.prototype.toString` by defining it as an integer, if the codebase at any point was reliant on `someobject.toString()` it would fail. |\n |**Remote Code Execution**|Client|Remote code execution is generally only possible in cases where the codebase evaluates a specific attribute of an object, and then executes that evaluation.
**For example:** `eval(someobject.someattr)`. In this case, if the attacker pollutes `Object.prototype.someattr` they are likely to be able to leverage this in order to execute code.|\n|**Property Injection**|Client|The attacker pollutes properties that the codebase relies on for their informative value, including security properties such as cookies or tokens.
**For example:** if a codebase checks privileges for `someuser.isAdmin`, then when the attacker pollutes `Object.prototype.isAdmin` and sets it to equal `true`, they can then achieve admin privileges.|\n\n## Affected environments\n\nThe following environments are susceptible to a Prototype Pollution attack:\n\n- Application server\n \n- Web server\n \n\n## How to prevent\n\n1. Freeze the prototype— use `Object.freeze (Object.prototype)`.\n \n2. Require schema validation of JSON input.\n \n3. Avoid using unsafe recursive merge functions.\n \n4. Consider using objects without prototypes (for example, `Object.create(null)`), breaking the prototype chain and preventing pollution.\n \n5. As a best practice use `Map` instead of `Object`.\n\n### For more information on this vulnerability type:\n\n[Arteau, Oliver. “JavaScript prototype pollution attack in NodeJS application.” GitHub, 26 May 2018](https://github.com/HoLyVieR/prototype-pollution-nsec18/blob/master/paper/JavaScript_prototype_pollution_attack_in_NodeJS.pdf)\n\n## Remediation\nUpgrade `lodash` to version 4.17.17 or higher.\n## References\n- [HackerOne Report](https://hackerone.com/reports/864701)\n", + "disclosureTime": "2020-08-21T10:34:29Z", + "exploit": "Proof of Concept", + "fixedIn": [ + "4.17.17" + ], + "functions": [], + "functions_new": [], + "id": "SNYK-JS-LODASH-608086", + "identifiers": { + "CVE": [], + "CWE": [ + "CWE-400" + ] + }, + "language": "js", + "modificationTime": "2020-08-27T16:44:20.914177Z", + "moduleName": "lodash", + "packageManager": "npm", + "packageName": "lodash", + "patches": [], + "proprietary": false, + "publicationTime": "2020-08-21T12:53:03Z", + "references": [ + { + "title": "HackerOne Report", + "url": "https://hackerone.com/reports/864701" + } + ], + "semver": { + "vulnerable": [ + "<4.17.17" + ] + }, + "severity": "high", + "severityWithCritical": "high", + "socialTrendAlert": false, + "title": "Prototype Pollution" + } + }, + "remediation": { + "unresolved": [], + "upgrade": { + "lodash@4.17.15": { + "upgradeTo": "lodash@4.17.21", + "upgrades": [ + "lodash@4.17.15", + "lodash@4.17.15", + "lodash@4.17.15", + "lodash@4.17.15", + "lodash@4.17.15" + ], + "vulns": [ + "SNYK-JS-LODASH-1018905", + "SNYK-JS-LODASH-1040724", + "SNYK-JS-LODASH-590103", + "SNYK-JS-LODASH-608086", + "SNYK-JS-LODASH-567746" + ] + } + }, + "patch": {}, + "ignore": {}, + "pin": {} + } + }, + "meta": { + "isPrivate": true, + "isLicensesEnabled": true, + "policy": "# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.\nversion: v1.21.5\nignore: {}\npatch: {}\n", + "ignoreSettings": null, + "org": "demo-applications", + "licensesPolicy": { + "severities": {}, + "orgLicenseRules": { + "AGPL-1.0": { + "licenseType": "AGPL-1.0", + "severity": "high", + "instructions": "" + }, + "AGPL-3.0": { + "licenseType": "AGPL-3.0", + "severity": "high", + "instructions": "" + }, + "Artistic-1.0": { + "licenseType": "Artistic-1.0", + "severity": "medium", + "instructions": "" + }, + "Artistic-2.0": { + "licenseType": "Artistic-2.0", + "severity": "medium", + "instructions": "" + }, + "CDDL-1.0": { + "licenseType": "CDDL-1.0", + "severity": "medium", + "instructions": "" + }, + "CPOL-1.02": { + "licenseType": "CPOL-1.02", + "severity": "high", + "instructions": "" + }, + "EPL-1.0": { + "licenseType": "EPL-1.0", + "severity": "medium", + "instructions": "" + }, + "GPL-2.0": { + "licenseType": "GPL-2.0", + "severity": "high", + "instructions": "" + }, + "GPL-3.0": { + "licenseType": "GPL-3.0", + "severity": "high", + "instructions": "" + }, + "LGPL-2.0": { + "licenseType": "LGPL-2.0", + "severity": "medium", + "instructions": "" + }, + "LGPL-2.1": { + "licenseType": "LGPL-2.1", + "severity": "medium", + "instructions": "" + }, + "LGPL-3.0": { + "licenseType": "LGPL-3.0", + "severity": "medium", + "instructions": "" + }, + "MPL-1.1": { + "licenseType": "MPL-1.1", + "severity": "medium", + "instructions": "" + }, + "MPL-2.0": { + "licenseType": "MPL-2.0", + "severity": "medium", + "instructions": "" + }, + "MS-RL": { + "licenseType": "MS-RL", + "severity": "medium", + "instructions": "" + }, + "SimPL-2.0": { + "licenseType": "SimPL-2.0", + "severity": "high", + "instructions": "" + } + } + } + } +} \ No newline at end of file diff --git a/test/jest/acceptance/analytics.spec.ts b/test/jest/acceptance/analytics.spec.ts index a258585cf8d..5d585dacddd 100644 --- a/test/jest/acceptance/analytics.spec.ts +++ b/test/jest/acceptance/analytics.spec.ts @@ -1,6 +1,7 @@ import { fakeServer } from '../../acceptance/fake-server'; import { createProject } from '../util/createProject'; import { runSnykCLI } from '../util/runSnykCLI'; +import * as fs from 'fs'; describe('analytics module', () => { let server; @@ -93,10 +94,11 @@ describe('analytics module', () => { }); }); + // test for `snyk test` with a project that has no vulns // improves upon the `snyk version` test because the `snyk test` path will include hitting `analytics.add` it('sends correct analytics data for `snyk test` command', async () => { const project = await createProject('../acceptance/workspaces/npm-package'); - const { code, stdout } = await runSnykCLI( + const { code } = await runSnykCLI( 'test --integrationName=JENKINS --integrationVersion=1.2.3', { cwd: project.path(), @@ -104,7 +106,6 @@ describe('analytics module', () => { }, ); - console.log(stdout); expect(code).toBe(0); const lastRequest = server.popRequest(); @@ -166,4 +167,219 @@ describe('analytics module', () => { }, }); }); + + // test for `snyk test` when vulns are found + it('sends correct analytics data for `snyk test` command', async () => { + const testDepGraphResult = JSON.parse( + fs.readFileSync( + 'test/fixtures/npm/with-vulnerable-lodash-dep/test-dep-graph-result.json', + 'utf-8', + ), + ); + server.setNextResponse(testDepGraphResult); + const project = await createProject('npm/with-vulnerable-lodash-dep'); + + const { code } = await runSnykCLI( + 'test --integrationName=JENKINS --integrationVersion=1.2.3', + { + cwd: project.path(), + env, + }, + ); + + expect(code).toBe(1); + + const lastRequest = server.popRequest(); + expect(lastRequest).toMatchObject({ + headers: { + host: 'localhost:12345', + accept: 'application/json', + authorization: 'token 123456789', + 'content-type': 'application/json; charset=utf-8', + 'x-snyk-cli-version': '1.0.0-monorepo', + }, + query: {}, + body: { + data: { + args: [ + { + integrationName: 'JENKINS', + integrationVersion: '1.2.3', + }, + ], + ci: expect.any(Boolean), + command: 'test', + metadata: { + 'generating-node-dependency-tree': { + lockFile: true, + targetFile: 'package-lock.json', + }, + lockfileVersion: 2, + pluginName: 'snyk-nodejs-lockfile-parser', + packageManager: 'npm', + packageName: 'with-vulnerable-lodash-dep', + packageVersion: '1.2.3', + prePrunedPathsCount: 2, + depGraph: true, + isDocker: false, + 'vulns-pre-policy': 5, + vulns: 5, + actionableRemediation: true, + 'error-code': 'VULNS', + 'error-message': 'Vulnerabilities found', + }, + durationMs: expect.any(Number), + environment: { + npmVersion: expect.any(String), + }, + id: expect.any(String), + integrationEnvironment: '', + integrationEnvironmentVersion: '', + integrationName: 'JENKINS', + integrationVersion: '1.2.3', + // prettier-ignore + metrics: { + 'network_time': { + type: 'timer', + values: expect.any(Array), + total: expect.any(Number), + }, + 'cpu_time': { + type: 'synthetic', + values: [expect.any(Number)], + total: expect.any(Number), + }, + }, + nodeVersion: process.version, + os: expect.any(String), + standalone: false, + version: '1.0.0-monorepo', + }, + }, + }); + }); + + // test for a bad command + it('sends correct analytics data a bad command', async () => { + const project = await createProject('../acceptance/workspaces/npm-package'); + const { code } = await runSnykCLI( + 'random-nonsense-command --some-option --integrationName=JENKINS --integrationVersion=1.2.3', + { + cwd: project.path(), + env, + }, + ); + + expect(code).toBe(2); + + const lastRequest = server.popRequest(); + expect(lastRequest).toMatchObject({ + headers: { + host: 'localhost:12345', + accept: 'application/json', + authorization: 'token 123456789', + 'content-type': 'application/json; charset=utf-8', + 'x-snyk-cli-version': '1.0.0-monorepo', + }, + query: {}, + body: { + data: { + args: ['random-nonsense-command'], + ci: expect.any(Boolean), + command: 'bad-command', + metadata: { + command: 'random-nonsense-command', + error: expect.stringContaining( + 'Error: Unknown command "random-nonsense-command"', + ), + 'error-code': 'UNKNOWN_COMMAND', + 'error-message': 'Unknown command "random-nonsense-command"', + }, + durationMs: expect.any(Number), + environment: { + npmVersion: expect.any(String), + }, + id: expect.any(String), + integrationEnvironment: '', + integrationEnvironmentVersion: '', + integrationName: '', + integrationVersion: '', + // prettier-ignore + metrics: { + 'network_time': { + type: 'timer', + values: expect.any(Array), + total: expect.any(Number), + }, + 'cpu_time': { + type: 'synthetic', + values: [expect.any(Number)], + total: expect.any(Number), + }, + }, + nodeVersion: process.version, + os: expect.any(String), + standalone: false, + version: '1.0.0-monorepo', + }, + }, + }); + }); + + // no command, i.e. user enters just `snyk` + it('sends correct analytics data a bad command', async () => { + const project = await createProject('../acceptance/workspaces/npm-package'); + const { code } = await runSnykCLI('', { + cwd: project.path(), + env, + }); + + expect(code).toBe(0); + + const lastRequest = server.popRequest(); + console.log(lastRequest.body.data); + expect(lastRequest).toMatchObject({ + headers: { + host: 'localhost:12345', + accept: 'application/json', + authorization: 'token 123456789', + 'content-type': 'application/json; charset=utf-8', + 'x-snyk-cli-version': '1.0.0-monorepo', + }, + query: {}, + body: { + data: { + args: ['help', {}], + ci: expect.any(Boolean), + command: 'help', + durationMs: expect.any(Number), + environment: { + npmVersion: expect.any(String), + }, + id: expect.any(String), + integrationEnvironment: '', + integrationEnvironmentVersion: '', + integrationName: '', + integrationVersion: '', + // prettier-ignore + metrics: { + 'network_time': { + type: 'timer', + values: expect.any(Array), + total: expect.any(Number), + }, + 'cpu_time': { + type: 'synthetic', + values: [expect.any(Number)], + total: expect.any(Number), + }, + }, + nodeVersion: process.version, + os: expect.any(String), + standalone: false, + version: '1.0.0-monorepo', + }, + }, + }); + }); }); From 42a84525f44779dde7676877664597fbf7191521 Mon Sep 17 00:00:00 2001 From: maxjeffos <44034094+maxjeffos@users.noreply.github.com> Date: Fri, 6 Aug 2021 16:51:17 -0400 Subject: [PATCH 4/8] test: migrate last analytics tests from tap to jest --- test/analytics.test.ts | 51 ------------------- test/jest/acceptance/analytics.spec.ts | 18 ++++++- .../lib/analytics/getStandardData.spec.ts | 11 +--- test/jest/unit/lib/analytics/utils.ts | 15 ++++++ 4 files changed, 33 insertions(+), 62 deletions(-) delete mode 100644 test/analytics.test.ts create mode 100644 test/jest/unit/lib/analytics/utils.ts diff --git a/test/analytics.test.ts b/test/analytics.test.ts deleted file mode 100644 index 3f831faaf0b..00000000000 --- a/test/analytics.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import * as tap from 'tap'; -import * as Proxyquire from 'proxyquire'; -import * as sinon from 'sinon'; -import * as snyk from '../src/lib'; -let old; -const proxyquire = Proxyquire.noPreserveCache(); -const { test } = tap; - -tap.beforeEach((done) => { - old = snyk.config.get('disable-analytics'); - snyk.config.delete('disable-analytics'); - done(); -}); - -tap.afterEach((done) => { - if (old === undefined) { - snyk.config.delete('disable-analytics'); - } else { - snyk.config.set('disable-analytics', old); - } - done(); -}); - -test('analyticsAllowed returns false if disable-analytics set in snyk config', (t) => { - t.plan(1); - snyk.config.set('disable-analytics', '1'); - const analytics = require('../src/lib/analytics'); - const analyticsAllowed: boolean = analytics.allowAnalytics(); - t.notOk(analyticsAllowed); -}); - -test('analyticsAllowed returns true if disable-analytics is not set snyk config', (t) => { - t.plan(1); - const analytics = require('../src/lib/analytics'); - const analyticsAllowed: boolean = analytics.allowAnalytics(); - t.ok(analyticsAllowed); -}); - -test('analytics disabled', (t) => { - const spy = sinon.spy(); - snyk.config.set('disable-analytics', '1'); - const analytics = proxyquire('../src/lib/analytics', { - '../request': { - makeRequest: spy, - }, - }); - - return analytics.addDataAndSend().then(() => { - t.equal(spy.called, false, 'the request should not have been made'); - }); -}); diff --git a/test/jest/acceptance/analytics.spec.ts b/test/jest/acceptance/analytics.spec.ts index 5d585dacddd..50b6fee3365 100644 --- a/test/jest/acceptance/analytics.spec.ts +++ b/test/jest/acceptance/analytics.spec.ts @@ -1,6 +1,7 @@ import { fakeServer } from '../../acceptance/fake-server'; import { createProject } from '../util/createProject'; import { runSnykCLI } from '../util/runSnykCLI'; +import * as request from '../../../src/lib/request'; import * as fs from 'fs'; describe('analytics module', () => { @@ -23,6 +24,10 @@ describe('analytics module', () => { }); }); + afterEach(() => { + jest.restoreAllMocks(); + }); + afterAll((done) => { server.close(() => { done(); @@ -337,7 +342,6 @@ describe('analytics module', () => { expect(code).toBe(0); const lastRequest = server.popRequest(); - console.log(lastRequest.body.data); expect(lastRequest).toMatchObject({ headers: { host: 'localhost:12345', @@ -382,4 +386,16 @@ describe('analytics module', () => { }, }); }); + + it("won't send analytics if disable analytics is set", async () => { + const requestSpy = jest.spyOn(request, 'makeRequest'); + const { code } = await runSnykCLI(`version`, { + env: { + ...env, + SNYK_DISABLE_ANALYTICS: '1', + }, + }); + expect(code).toBe(0); + expect(requestSpy).not.toBeCalled(); + }); }); diff --git a/test/jest/unit/lib/analytics/getStandardData.spec.ts b/test/jest/unit/lib/analytics/getStandardData.spec.ts index 74f90cae535..3f909663a7a 100644 --- a/test/jest/unit/lib/analytics/getStandardData.spec.ts +++ b/test/jest/unit/lib/analytics/getStandardData.spec.ts @@ -1,15 +1,6 @@ import { getStandardData } from '../../../../../src/lib/analytics/getStandardData'; import { getCommandVersion } from '../../../../../src/lib/analytics/sources'; -import { ArgsOptions } from '../../../../../src/cli/args'; - -function argsFrom(args: { [key: string]: string }): ArgsOptions[] { - const fullArgs = ([ - { - ...args, - }, - ] as any) as ArgsOptions[]; - return fullArgs; -} +import { argsFrom } from './utils'; describe('getStandardData returns object', () => { it('contains all the required fields', async () => { diff --git a/test/jest/unit/lib/analytics/utils.ts b/test/jest/unit/lib/analytics/utils.ts new file mode 100644 index 00000000000..35119a9a09d --- /dev/null +++ b/test/jest/unit/lib/analytics/utils.ts @@ -0,0 +1,15 @@ +import { ArgsOptions } from '../../../../../src/cli/args'; + +/** + * Test helper to make a proper ArgsOptions[] out of a simpler input. + * @param args basic key value pairs object + * @returns a ArgsOptions[] with just the stuff we need for the tests. + */ +export function argsFrom(args: { [key: string]: string }): ArgsOptions[] { + const fullArgs = ([ + { + ...args, + }, + ] as any) as ArgsOptions[]; + return fullArgs; +} From 191b06b9e5b454d2ce2d29716f0fc4b793082374 Mon Sep 17 00:00:00 2001 From: maxjeffos <44034094+maxjeffos@users.noreply.github.com> Date: Mon, 9 Aug 2021 11:11:57 -0400 Subject: [PATCH 5/8] test: use env var rather than arg for int name/version --- test/jest/acceptance/analytics.spec.ts | 68 +++++++++----------------- 1 file changed, 23 insertions(+), 45 deletions(-) diff --git a/test/jest/acceptance/analytics.spec.ts b/test/jest/acceptance/analytics.spec.ts index 50b6fee3365..e3049eedb78 100644 --- a/test/jest/acceptance/analytics.spec.ts +++ b/test/jest/acceptance/analytics.spec.ts @@ -16,6 +16,8 @@ describe('analytics module', () => { SNYK_API: 'http://localhost:' + port + baseApi, SNYK_HOST: 'http://localhost:' + port, SNYK_TOKEN: '123456789', + SNYK_INTEGRATION_NAME: 'JENKINS', + SNYK_INTEGRATION_VERSION: '1.2.3', }; server = fakeServer(baseApi, env.SNYK_TOKEN); @@ -35,12 +37,9 @@ describe('analytics module', () => { }); it('sends correct analytics data for simple command (`snyk version`)', async () => { - const { code } = await runSnykCLI( - `version --org=fooOrg --all-projects --integrationName=JENKINS --integrationVersion=1.2.3`, - { - env, - }, - ); + const { code } = await runSnykCLI(`version --org=fooOrg --all-projects`, { + env, + }); expect(code).toBe(0); const lastRequest = server.popRequest(); @@ -61,8 +60,6 @@ describe('analytics module', () => { { org: 'fooOrg', allProjects: true, - integrationName: 'JENKINS', - integrationVersion: '1.2.3', }, ], ci: expect.any(Boolean), @@ -103,13 +100,10 @@ describe('analytics module', () => { // improves upon the `snyk version` test because the `snyk test` path will include hitting `analytics.add` it('sends correct analytics data for `snyk test` command', async () => { const project = await createProject('../acceptance/workspaces/npm-package'); - const { code } = await runSnykCLI( - 'test --integrationName=JENKINS --integrationVersion=1.2.3', - { - cwd: project.path(), - env, - }, - ); + const { code } = await runSnykCLI('test', { + cwd: project.path(), + env, + }); expect(code).toBe(0); @@ -125,12 +119,7 @@ describe('analytics module', () => { query: {}, body: { data: { - args: [ - { - integrationName: 'JENKINS', - integrationVersion: '1.2.3', - }, - ], + args: [{}], ci: expect.any(Boolean), command: 'test', metadata: { @@ -184,13 +173,10 @@ describe('analytics module', () => { server.setNextResponse(testDepGraphResult); const project = await createProject('npm/with-vulnerable-lodash-dep'); - const { code } = await runSnykCLI( - 'test --integrationName=JENKINS --integrationVersion=1.2.3', - { - cwd: project.path(), - env, - }, - ); + const { code } = await runSnykCLI('test', { + cwd: project.path(), + env, + }); expect(code).toBe(1); @@ -206,12 +192,7 @@ describe('analytics module', () => { query: {}, body: { data: { - args: [ - { - integrationName: 'JENKINS', - integrationVersion: '1.2.3', - }, - ], + args: [{}], ci: expect.any(Boolean), command: 'test', metadata: { @@ -267,13 +248,10 @@ describe('analytics module', () => { // test for a bad command it('sends correct analytics data a bad command', async () => { const project = await createProject('../acceptance/workspaces/npm-package'); - const { code } = await runSnykCLI( - 'random-nonsense-command --some-option --integrationName=JENKINS --integrationVersion=1.2.3', - { - cwd: project.path(), - env, - }, - ); + const { code } = await runSnykCLI('random-nonsense-command --some-option', { + cwd: project.path(), + env, + }); expect(code).toBe(2); @@ -307,8 +285,8 @@ describe('analytics module', () => { id: expect.any(String), integrationEnvironment: '', integrationEnvironmentVersion: '', - integrationName: '', - integrationVersion: '', + integrationName: 'JENKINS', + integrationVersion: '1.2.3', // prettier-ignore metrics: { 'network_time': { @@ -363,8 +341,8 @@ describe('analytics module', () => { id: expect.any(String), integrationEnvironment: '', integrationEnvironmentVersion: '', - integrationName: '', - integrationVersion: '', + integrationName: 'JENKINS', + integrationVersion: '1.2.3', // prettier-ignore metrics: { 'network_time': { From ec9e48f1710fcaf2bc0068389f51078f1544117b Mon Sep 17 00:00:00 2001 From: maxjeffos <44034094+maxjeffos@users.noreply.github.com> Date: Mon, 9 Aug 2021 11:14:01 -0400 Subject: [PATCH 6/8] test: remove redundant test case --- test/jest/acceptance/analytics.spec.ts | 61 -------------------------- 1 file changed, 61 deletions(-) diff --git a/test/jest/acceptance/analytics.spec.ts b/test/jest/acceptance/analytics.spec.ts index e3049eedb78..aa7e14eda02 100644 --- a/test/jest/acceptance/analytics.spec.ts +++ b/test/jest/acceptance/analytics.spec.ts @@ -36,68 +36,7 @@ describe('analytics module', () => { }); }); - it('sends correct analytics data for simple command (`snyk version`)', async () => { - const { code } = await runSnykCLI(`version --org=fooOrg --all-projects`, { - env, - }); - expect(code).toBe(0); - - const lastRequest = server.popRequest(); - expect(lastRequest).toMatchObject({ - headers: { - host: 'localhost:12345', - accept: 'application/json', - authorization: 'token 123456789', - 'content-type': 'application/json; charset=utf-8', - 'x-snyk-cli-version': '1.0.0-monorepo', - }, - query: { - org: 'fooOrg', - }, - body: { - data: { - args: [ - { - org: 'fooOrg', - allProjects: true, - }, - ], - ci: expect.any(Boolean), - command: 'version', - durationMs: expect.any(Number), - environment: { - npmVersion: expect.any(String), - }, - id: expect.any(String), - integrationEnvironment: '', - integrationEnvironmentVersion: '', - integrationName: 'JENKINS', - integrationVersion: '1.2.3', - // prettier-ignore - metrics: { - 'network_time': { - type: 'timer', - values: [], - total: expect.any(Number), - }, - 'cpu_time': { - type: 'synthetic', - values: [expect.any(Number)], - total: expect.any(Number), - }, - }, - nodeVersion: process.version, - org: 'fooOrg', - os: expect.any(String), - standalone: false, - version: '1.0.0-monorepo', - }, - }, - }); - }); - // test for `snyk test` with a project that has no vulns - // improves upon the `snyk version` test because the `snyk test` path will include hitting `analytics.add` it('sends correct analytics data for `snyk test` command', async () => { const project = await createProject('../acceptance/workspaces/npm-package'); const { code } = await runSnykCLI('test', { From bb1d447994b3e74e6f32f1650818a7b0f4455127 Mon Sep 17 00:00:00 2001 From: maxjeffos <44034094+maxjeffos@users.noreply.github.com> Date: Mon, 9 Aug 2021 11:25:09 -0400 Subject: [PATCH 7/8] test: improve test labeling --- test/jest/acceptance/analytics.spec.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/test/jest/acceptance/analytics.spec.ts b/test/jest/acceptance/analytics.spec.ts index aa7e14eda02..4e68d50c017 100644 --- a/test/jest/acceptance/analytics.spec.ts +++ b/test/jest/acceptance/analytics.spec.ts @@ -36,8 +36,7 @@ describe('analytics module', () => { }); }); - // test for `snyk test` with a project that has no vulns - it('sends correct analytics data for `snyk test` command', async () => { + it('sends analytics for `snyk test` with no vulns found', async () => { const project = await createProject('../acceptance/workspaces/npm-package'); const { code } = await runSnykCLI('test', { cwd: project.path(), @@ -101,8 +100,7 @@ describe('analytics module', () => { }); }); - // test for `snyk test` when vulns are found - it('sends correct analytics data for `snyk test` command', async () => { + it('sends analytics for `snyk test` with vulns found', async () => { const testDepGraphResult = JSON.parse( fs.readFileSync( 'test/fixtures/npm/with-vulnerable-lodash-dep/test-dep-graph-result.json', @@ -184,7 +182,6 @@ describe('analytics module', () => { }); }); - // test for a bad command it('sends correct analytics data a bad command', async () => { const project = await createProject('../acceptance/workspaces/npm-package'); const { code } = await runSnykCLI('random-nonsense-command --some-option', { @@ -248,8 +245,7 @@ describe('analytics module', () => { }); }); - // no command, i.e. user enters just `snyk` - it('sends correct analytics data a bad command', async () => { + it('sends analytics data a bad command', async () => { const project = await createProject('../acceptance/workspaces/npm-package'); const { code } = await runSnykCLI('', { cwd: project.path(), From 4ac3d5fe242102dd7741dde49aeb8aaaed7bceda Mon Sep 17 00:00:00 2001 From: maxjeffos <44034094+maxjeffos@users.noreply.github.com> Date: Mon, 9 Aug 2021 13:07:46 -0400 Subject: [PATCH 8/8] chore: use large instance in CCI for test-linux --- .circleci/config.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 200f3e07a08..61dbcd07e3a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -308,6 +308,7 @@ jobs: <<: *defaults docker: - image: circleci/node:<< parameters.node_version >> + resource_class: large steps: - install_sdkman_linux - install_jdk_linux