From 5344db4c37e03f6ec8ce287071caf9e1d95eb7de Mon Sep 17 00:00:00 2001 From: anthogez Date: Fri, 21 Aug 2020 18:47:59 +0300 Subject: [PATCH] feat: test dependencies for ecosystems Test the dependencies of each ecosystem scan result. --- package.json | 4 +- src/cli/commands/test/index.ts | 17 ++- src/lib/ecosystems.ts | 106 ++++++++++++++++-- test/ecosystems.spec.ts | 124 +++++++++++++-------- test/fixtures/cpp-project/display.txt | 8 +- test/fixtures/cpp-project/scan.json | 23 ---- test/fixtures/cpp-project/testResults.json | 120 ++++++++++++++++++++ 7 files changed, 318 insertions(+), 84 deletions(-) delete mode 100644 test/fixtures/cpp-project/scan.json create mode 100644 test/fixtures/cpp-project/testResults.json diff --git a/package.json b/package.json index dd751aaea16..e593f926c25 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "test:system": "tap test/system/*.test.* -Rspec --timeout=300 --node-arg=-r --node-arg=ts-node/register", "test:jest": "jest test/*.spec.ts", "test:test": "tap test/*.test.* -Rspec --timeout=300 --node-arg=-r --node-arg=ts-node/register", - "test": "npm run test:acceptance && npm run test:system && npm run test:test && npm run test:jest", + "test": "npm run test:acceptance && npm run test:system && npm run test:jest", "test-windows": "tap test/acceptance/**/*.test.* -Rspec --timeout=300 --node-arg=-r --node-arg=ts-node/register && npm run test:system && npm run test:test", "lint": "run-p --aggregate-output lint:*", "lint:js": "eslint --color --cache 'src/**/*.{js,ts}'", @@ -76,7 +76,7 @@ "proxy-from-env": "^1.0.0", "semver": "^6.0.0", "snyk-config": "3.1.0", - "snyk-cpp-plugin": "1.2.0", + "snyk-cpp-plugin": "1.4.0", "snyk-docker-plugin": "3.17.0", "snyk-go-plugin": "1.16.0", "snyk-gradle-plugin": "3.5.1", diff --git a/src/cli/commands/test/index.ts b/src/cli/commands/test/index.ts index 695d0ba6713..2ef46799dca 100644 --- a/src/cli/commands/test/index.ts +++ b/src/cli/commands/test/index.ts @@ -109,12 +109,16 @@ async function test(...args: MethodArgs): Promise { const ecosystem = getEcosystem(options); if (ecosystem) { - const commandResult = await testEcosystem( - ecosystem, - args as string[], - options, - ); - return commandResult; + try { + const commandResult = await testEcosystem( + ecosystem, + args as string[], + options, + ); + return commandResult; + } catch (error) { + throw new Error(error); + } } // Promise waterfall to test all other paths sequentially @@ -141,6 +145,7 @@ async function test(...args: MethodArgs): Promise { // // To standardise this, make sure we use the best _object_ to // describe the error. + if (error instanceof Error) { res = error; } else if (typeof error !== 'object') { diff --git a/src/lib/ecosystems.ts b/src/lib/ecosystems.ts index e881eeded5b..ff499ee8917 100644 --- a/src/lib/ecosystems.ts +++ b/src/lib/ecosystems.ts @@ -1,20 +1,57 @@ import * as cppPlugin from 'snyk-cpp-plugin'; import { Options } from './types'; import { TestCommandResult } from '../cli/commands/types'; +import * as config from './config'; +import { isCI } from './is-ci'; +import * as snyk from './'; +import request = require('./request'); +import { DepGraphData } from '@snyk/dep-graph'; interface Artifact { type: string; data: any; - meta?: { [key: string]: any }; + meta: { [key: string]: any }; } interface ScanResult { + type: string; artifacts: Artifact[]; + meta: { + [key: string]: any; + }; +} + +interface TestResults { + depGraph: DepGraphData; + affectedPkgs: { + [pkgId: string]: { + pkg: { + name: string; + version: string; + }; + issues: { + [issueId: string]: { + issueId: string; + }; + }; + }; + }; + issuesData: { + [issueId: string]: { + id: string; + severity: string; + title: string; + }; + }; } export interface EcosystemPlugin { scan: (options: Options) => Promise; - display: (scanResults: ScanResult[]) => Promise; + display: ( + scanResults: ScanResult[], + testResults: TestResults[], + errors: string[], + ) => Promise; } export type Ecosystem = 'cpp'; @@ -42,21 +79,76 @@ export async function testEcosystem( options: Options, ): Promise { const plugin = getPlugin(ecosystem); - let allScanResults: ScanResult[] = []; + const scanResultsByPath: { [dir: string]: ScanResult[] } = {}; + let scanResults: ScanResult[] = []; for (const path of paths) { options.path = path; - const scanResults = await plugin.scan(options); - allScanResults = allScanResults.concat(scanResults); + const results = await plugin.scan(options); + scanResultsByPath[path] = results; + scanResults = scanResults.concat(results); } - const stringifiedData = JSON.stringify(allScanResults, null, 2); + const [testResults, errors] = await testDependencies(scanResultsByPath); + const stringifiedData = JSON.stringify(testResults, null, 2); if (options.json) { return TestCommandResult.createJsonTestCommandResult(stringifiedData); } + const readableResult = await plugin.display(scanResults, testResults, errors); - const readableResult = await plugin.display(allScanResults); return TestCommandResult.createHumanReadableTestCommandResult( readableResult, stringifiedData, ); } + +export async function testDependencies(scans: { + [dir: string]: ScanResult[]; +}): Promise<[TestResults[], string[]]> { + const results: TestResults[] = []; + const errors: string[] = []; + for (const [path, scanResults] of Object.entries(scans)) { + for (const scanResult of scanResults) { + const payload = { + method: 'POST', + url: `${config.API}/test-dependencies`, + json: true, + headers: { + 'x-is-ci': isCI(), + authorization: 'token ' + snyk.api, + }, + body: { + type: 'cpp', + artifacts: scanResult.artifacts, + meta: {}, + }, + }; + try { + const response = await makeRequest(payload); + results.push(response); + } catch (error) { + if (error.code !== 200) { + throw new Error(error.message); + } + errors.push('Could not test dependencies in ' + path); + } + } + } + return [results, errors]; +} + +export async function makeRequest(payload: any): Promise { + return new Promise((resolve, reject) => { + request(payload, (error, res, body) => { + if (error) { + return reject(error); + } + if (res.statusCode !== 200) { + return reject({ + code: res.statusCode, + message: res?.body?.message || 'Error testing dependencies', + }); + } + resolve(body); + }); + }); +} diff --git a/test/ecosystems.spec.ts b/test/ecosystems.spec.ts index 2f45e751d61..54b63be750f 100644 --- a/test/ecosystems.spec.ts +++ b/test/ecosystems.spec.ts @@ -2,19 +2,19 @@ import { Options } from '../src/lib/types'; import * as cppPlugin from 'snyk-cpp-plugin'; import * as path from 'path'; import * as fs from 'fs'; -import { getPlugin, getEcosystem, testEcosystem } from '../src/lib/ecosystems'; +import * as ecosystems from '../src/lib/ecosystems'; import { TestCommandResult } from '../src/cli/commands/types'; describe('ecosystems', () => { describe('getPlugin', () => { it('should return c++ plugin when cpp ecosystem is given', () => { - const actual = getPlugin('cpp'); + const actual = ecosystems.getPlugin('cpp'); const expected = cppPlugin; expect(actual).toBe(expected); }); it('should return undefined when ecosystem is not supported', () => { - const actual = getPlugin('unsupportedEcosystem' as any); + const actual = ecosystems.getPlugin('unsupportedEcosystem' as any); const expected = undefined; expect(actual).toBe(expected); }); @@ -26,61 +26,95 @@ describe('ecosystems', () => { source: true, path: '', }; - const actual = getEcosystem(options); + const actual = ecosystems.getEcosystem(options); const expected = 'cpp'; expect(actual).toBe(expected); }); + }); - it('should return null when options source is false', () => { - const options: Options = { - source: false, - path: '', - }; - const actual = getEcosystem(options); - const expected = null; - expect(actual).toBe(expected); - }); + it('should return null when options source is false', () => { + const options: Options = { + source: false, + path: '', + }; + const actual = ecosystems.getEcosystem(options); + const expected = null; + expect(actual).toBe(expected); }); +}); - describe('testEcosystem', () => { - const fixturePath = path.join(__dirname, 'fixtures', 'cpp-project'); - const cwd = process.cwd(); +describe('testEcosystem', () => { + const fixturePath = path.join(__dirname, 'fixtures', 'cpp-project'); + const cwd = process.cwd(); - function readFixture(filename: string) { - const filePath = path.join(fixturePath, filename); - return fs.readFileSync(filePath, 'utf-8'); - } + function readFixture(filename: string) { + const filePath = path.join(fixturePath, filename); + return fs.readFileSync(filePath, 'utf-8'); + } - beforeAll(() => { - process.chdir(fixturePath); - }); - afterAll(() => { - process.chdir(cwd); - }); + beforeAll(() => { + process.chdir(fixturePath); + }); - it('should return human readable result when no json option given', async () => { - const display = readFixture('display.txt'); - const scan = readFixture('scan.json'); - const stringifiedData = JSON.stringify(JSON.parse(scan), null, 2); - const expected = TestCommandResult.createHumanReadableTestCommandResult( - display, - stringifiedData, - ); - const actual = await testEcosystem('cpp', ['.'], { path: '' }); - expect(actual).toEqual(expected); + beforeEach(() => { + jest.resetAllMocks(); + }); + + afterAll(() => { + process.chdir(cwd); + }); + + it('should return human readable result when no json option given', async () => { + const display = readFixture('display.txt'); + const testResults = readFixture('testResults.json'); + const stringifiedData = JSON.stringify(JSON.parse(testResults), null, 2); + const expected = TestCommandResult.createHumanReadableTestCommandResult( + display, + stringifiedData, + ); + + const actual = await ecosystems.testEcosystem('cpp', ['.'], { path: '' }); + expect(actual).toEqual(expected); + }); + + it('should return json result when json option', async () => { + const testResults = readFixture('testResults.json'); + const stringifiedData = JSON.stringify(JSON.parse(testResults), null, 2); + const expected = TestCommandResult.createJsonTestCommandResult( + stringifiedData, + ); + const actual = await ecosystems.testEcosystem('cpp', ['.'], { + path: '', + json: true, }); + expect(actual).toEqual(expected); + }); - it('should return json result when json option', async () => { - const scan = readFixture('scan.json'); - const stringifiedData = JSON.stringify(JSON.parse(scan), null, 2); - const expected = TestCommandResult.createJsonTestCommandResult( - stringifiedData, - ); - const actual = await testEcosystem('cpp', ['.'], { + it('should throw error when response code is not 200', async () => { + const expected = { code: 401, message: 'Invalid auth token' }; + jest.spyOn(ecosystems, 'testEcosystem').mockRejectedValue(expected); + expect.assertions(1); + try { + await ecosystems.testEcosystem('cpp', ['.'], { path: '', - json: true, }); - expect(actual).toEqual(expected); + } catch (error) { + expect(error).toEqual(expected); + } + }); + + it.skip('should return error when there was a problem testing dependencies', async () => { + //@boost: TODO finish up my implementation + // const makeRequestSpy = jest + // .spyOn(ecosystems, 'makeRequest') + // .mockRejectedValue('Something went wrong'); + // const ecosystemDisplaySpy = jest.spyOn(cppPlugin, 'display'); + const commandResult = await ecosystems.testEcosystem('cpp', ['.'], { + path: '', + json: true, }); + console.log(commandResult); + expect(commandResult).toEqual(''); + // expect(ecosystemDisplaySpy).toHaveBeenCalledWith({}); }); }); diff --git a/test/fixtures/cpp-project/display.txt b/test/fixtures/cpp-project/display.txt index 497046e90a2..79046d10434 100644 --- a/test/fixtures/cpp-project/display.txt +++ b/test/fixtures/cpp-project/display.txt @@ -1,3 +1,9 @@ +Dependency Fingerprints +----------------------- 52d1b046047db9ea0c581cafd4c68fe5 add.cpp aeca71a6e39f99a24ecf4c088eee9cb8 add.h -ad3365b3370ef6b1c3e778f875055f19 main.cpp \ No newline at end of file +ad3365b3370ef6b1c3e778f875055f19 main.cpp + +Issues +------ +Tested 0 dependencies for known issues, found 0 issues. diff --git a/test/fixtures/cpp-project/scan.json b/test/fixtures/cpp-project/scan.json deleted file mode 100644 index c9588ccb691..00000000000 --- a/test/fixtures/cpp-project/scan.json +++ /dev/null @@ -1,23 +0,0 @@ -[ - { - "artifacts": [ - { - "type": "cpp-fingerprints", - "data": [ - { - "filePath": "add.cpp", - "hash": "52d1b046047db9ea0c581cafd4c68fe5" - }, - { - "filePath": "add.h", - "hash": "aeca71a6e39f99a24ecf4c088eee9cb8" - }, - { - "filePath": "main.cpp", - "hash": "ad3365b3370ef6b1c3e778f875055f19" - } - ] - } - ] - } -] diff --git a/test/fixtures/cpp-project/testResults.json b/test/fixtures/cpp-project/testResults.json new file mode 100644 index 00000000000..12bede0e409 --- /dev/null +++ b/test/fixtures/cpp-project/testResults.json @@ -0,0 +1,120 @@ +[ + { + "result": { + "affectedPkgs": {}, + "issuesData": {} + }, + "meta": { + "isPrivate": true, + "isLicensesEnabled": true, + "policy": "# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.\nversion: v1.19.0\nignore: {}\npatch: {}\n", + "ignoreSettings": null, + "org": "snyk", + "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": "" + }, + "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": "" + } + } + } + }, + "depGraph": { + "schemaVersion": "1.2.0", + "pkgManager": { + "name": "cpp" + }, + "pkgs": [ + { + "id": "_root@0.0.0", + "info": { + "name": "_root", + "version": "0.0.0" + } + } + ], + "graph": { + "rootNodeId": "root-node", + "nodes": [ + { + "nodeId": "root-node", + "pkgId": "_root@0.0.0", + "deps": [] + } + ] + } + } + } +] \ No newline at end of file