diff --git a/package.json b/package.json index eb63aeed6ce..f761968fcd7 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "proxy-from-env": "^1.0.0", "semver": "^6.0.0", "snyk-config": "3.1.0", + "snyk-cpp-plugin": "1.2.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 34f142b9bbf..533af71d97b 100644 --- a/src/cli/commands/test/index.ts +++ b/src/cli/commands/test/index.ts @@ -48,6 +48,7 @@ import { } from './formatters'; import * as utils from './utils'; import { getIacDisplayedOutput } from './iac-output'; +import { getEcosystem, testEcosystem } from '../../../lib/ecosystems'; const debug = Debug('snyk-test'); const SEPARATOR = '\n-------------------------------------------------------\n'; @@ -104,6 +105,16 @@ async function test(...args: MethodArgs): Promise { } } + const ecosystem = getEcosystem(options); + if (ecosystem) { + const commandResult = await testEcosystem( + ecosystem, + args as string[], + options, + ); + return commandResult; + } + // Promise waterfall to test all other paths sequentially for (const path of args as string[]) { // Create a copy of the options so a specific test can diff --git a/src/cli/modes.ts b/src/cli/modes.ts index 732aab812e7..a9e94928b98 100644 --- a/src/cli/modes.ts +++ b/src/cli/modes.ts @@ -7,6 +7,13 @@ interface ModeData { } const modes: Record = { + source: { + allowedCommands: ['test'], + config: (args): [] => { + args['source'] = true; + return args; + }, + }, container: { allowedCommands: ['test', 'monitor'], config: (args): [] => { diff --git a/src/lib/ecosystems.ts b/src/lib/ecosystems.ts new file mode 100644 index 00000000000..e881eeded5b --- /dev/null +++ b/src/lib/ecosystems.ts @@ -0,0 +1,62 @@ +import * as cppPlugin from 'snyk-cpp-plugin'; +import { Options } from './types'; +import { TestCommandResult } from '../cli/commands/types'; + +interface Artifact { + type: string; + data: any; + meta?: { [key: string]: any }; +} + +interface ScanResult { + artifacts: Artifact[]; +} + +export interface EcosystemPlugin { + scan: (options: Options) => Promise; + display: (scanResults: ScanResult[]) => Promise; +} + +export type Ecosystem = 'cpp'; + +const EcosystemPlugins: { + readonly [ecosystem in Ecosystem]: EcosystemPlugin; +} = { + cpp: cppPlugin, +}; + +export function getPlugin(ecosystem: Ecosystem): EcosystemPlugin { + return EcosystemPlugins[ecosystem]; +} + +export function getEcosystem(options: Options): Ecosystem | null { + if (options.source) { + return 'cpp'; + } + return null; +} + +export async function testEcosystem( + ecosystem: Ecosystem, + paths: string[], + options: Options, +): Promise { + const plugin = getPlugin(ecosystem); + let allScanResults: ScanResult[] = []; + for (const path of paths) { + options.path = path; + const scanResults = await plugin.scan(options); + allScanResults = allScanResults.concat(scanResults); + } + + const stringifiedData = JSON.stringify(allScanResults, null, 2); + if (options.json) { + return TestCommandResult.createJsonTestCommandResult(stringifiedData); + } + + const readableResult = await plugin.display(allScanResults); + return TestCommandResult.createHumanReadableTestCommandResult( + readableResult, + stringifiedData, + ); +} diff --git a/src/lib/types.ts b/src/lib/types.ts index ef594231c78..3a2ea8a8158 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -45,6 +45,7 @@ export interface Options { path: string; docker?: boolean; iac?: boolean; + source?: boolean; // C/C++ Ecosystem Support file?: string; policy?: string; json?: boolean; diff --git a/test/ecosystems.spec.ts b/test/ecosystems.spec.ts new file mode 100644 index 00000000000..2f45e751d61 --- /dev/null +++ b/test/ecosystems.spec.ts @@ -0,0 +1,86 @@ +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 { 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 expected = cppPlugin; + expect(actual).toBe(expected); + }); + + it('should return undefined when ecosystem is not supported', () => { + const actual = getPlugin('unsupportedEcosystem' as any); + const expected = undefined; + expect(actual).toBe(expected); + }); + }); + + describe('getEcosystem', () => { + it('should return c++ ecosystem when options source is true', () => { + const options: Options = { + source: true, + path: '', + }; + const actual = 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); + }); + }); + + 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'); + } + + beforeAll(() => { + process.chdir(fixturePath); + }); + afterAll(() => { + process.chdir(cwd); + }); + + 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); + }); + + 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', ['.'], { + path: '', + json: true, + }); + expect(actual).toEqual(expected); + }); + }); +}); diff --git a/test/fixtures/cpp-project/add.cpp b/test/fixtures/cpp-project/add.cpp new file mode 100644 index 00000000000..b7d49b4202d --- /dev/null +++ b/test/fixtures/cpp-project/add.cpp @@ -0,0 +1,4 @@ +int add(int x, int y) +{ + return x + y; +} \ No newline at end of file diff --git a/test/fixtures/cpp-project/add.h b/test/fixtures/cpp-project/add.h new file mode 100644 index 00000000000..a234f1f2095 --- /dev/null +++ b/test/fixtures/cpp-project/add.h @@ -0,0 +1 @@ +int add(int x, int y); \ No newline at end of file diff --git a/test/fixtures/cpp-project/display.txt b/test/fixtures/cpp-project/display.txt new file mode 100644 index 00000000000..497046e90a2 --- /dev/null +++ b/test/fixtures/cpp-project/display.txt @@ -0,0 +1,3 @@ +52d1b046047db9ea0c581cafd4c68fe5 add.cpp +aeca71a6e39f99a24ecf4c088eee9cb8 add.h +ad3365b3370ef6b1c3e778f875055f19 main.cpp \ No newline at end of file diff --git a/test/fixtures/cpp-project/main.cpp b/test/fixtures/cpp-project/main.cpp new file mode 100644 index 00000000000..e8dfe70d305 --- /dev/null +++ b/test/fixtures/cpp-project/main.cpp @@ -0,0 +1,7 @@ +#include +#include "add.h" + +int main() { + std::cout << "The sum of 3 and 4 is " << add(3, 4) << '\n'; + return 0; +} \ No newline at end of file diff --git a/test/fixtures/cpp-project/scan.json b/test/fixtures/cpp-project/scan.json new file mode 100644 index 00000000000..c9588ccb691 --- /dev/null +++ b/test/fixtures/cpp-project/scan.json @@ -0,0 +1,23 @@ +[ + { + "artifacts": [ + { + "type": "cpp-fingerprints", + "data": [ + { + "filePath": "add.cpp", + "hash": "52d1b046047db9ea0c581cafd4c68fe5" + }, + { + "filePath": "add.h", + "hash": "aeca71a6e39f99a24ecf4c088eee9cb8" + }, + { + "filePath": "main.cpp", + "hash": "ad3365b3370ef6b1c3e778f875055f19" + } + ] + } + ] + } +] diff --git a/test/modes.spec.ts b/test/modes.spec.ts index 9778ca8dd6a..85470e7d1cb 100644 --- a/test/modes.spec.ts +++ b/test/modes.spec.ts @@ -134,6 +134,23 @@ describe('when have a valid mode and command', () => { expect(cliArgs['docker']).toBeTruthy(); expect(cliArgs['experimental']).toBeTruthy(); }); + + it('"source test" should set source option and test command', () => { + const expectedCommand = 'test'; + const expectedArgs = { + _: [], + source: true, + }; + const cliCommand = 'source'; + const cliArgs = { + _: ['test'], + }; + + const command = parseMode(cliCommand, cliArgs); + expect(command).toBe(expectedCommand); + expect(cliArgs).toEqual(expectedArgs); + expect(cliArgs['source']).toBeTruthy(); + }); }); describe('when have a valid mode, command and exists a command alias', () => {