diff --git a/src/cli/commands/test/index.ts b/src/cli/commands/test/index.ts index 414aa96c69f..8b08efb2e86 100644 --- a/src/cli/commands/test/index.ts +++ b/src/cli/commands/test/index.ts @@ -5,7 +5,7 @@ import chalk from 'chalk'; import * as snyk from '../../../lib'; import * as config from '../../../lib/config'; import { isCI } from '../../../lib/is-ci'; -import { apiTokenExists } from '../../../lib/api-token'; +import { apiTokenExists, getDockerToken } from '../../../lib/api-token'; import { FAIL_ON, FailOn, SEVERITIES } from '../../../lib/snyk-test/common'; import * as Debug from 'debug'; import { @@ -91,7 +91,15 @@ async function test(...args: MethodArgs): Promise { return Promise.reject(chalk.red.bold(error.message)); } - apiTokenExists(); + try { + apiTokenExists(); + } catch (err) { + if (options.docker && getDockerToken()) { + options.testDepGraphDockerEndpoint = '/docker-jwt/test-dep-graph'; + } else { + throw err; + } + } // Promise waterfall to test all other paths sequentially for (const path of args as string[]) { diff --git a/src/lib/api-token.ts b/src/lib/api-token.ts index 361d2f5932b..73bd59d4158 100644 --- a/src/lib/api-token.ts +++ b/src/lib/api-token.ts @@ -8,6 +8,10 @@ export function api() { return config.api || config.TOKEN || userConfig.get('api'); } +export function getDockerToken(): string | undefined { + return process.env.SNYK_DOCKER_TOKEN; +} + export function apiTokenExists() { const configured = api(); if (!configured) { diff --git a/src/lib/snyk-test/run-test.ts b/src/lib/snyk-test/run-test.ts index 21d2549a362..6e21d5a92aa 100644 --- a/src/lib/snyk-test/run-test.ts +++ b/src/lib/snyk-test/run-test.ts @@ -56,6 +56,7 @@ import { Payload, PayloadBody, DepTreeFromResolveDeps } from './types'; import { CallGraphError } from '@snyk/cli-interface/legacy/common'; import * as alerts from '../alerts'; import { abridgeErrorMessage } from '../error-format'; +import { getDockerToken } from '../api-token'; const debug = debugModule('snyk'); @@ -557,14 +558,18 @@ async function assembleLocalPayloads( }); body.callGraph = callGraph; } - + const reqUrl = + config.API + + (options.testDepGraphDockerEndpoint || + options.vulnEndpoint || + '/test-dep-graph'); const payload: Payload = { method: 'POST', - url: config.API + (options.vulnEndpoint || '/test-dep-graph'), + url: reqUrl, json: true, headers: { 'x-is-ci': isCI(), - authorization: 'token ' + (snyk as any).api, + authorization: getAuthHeader(), }, qs: common.assembleQueryString(options), body, @@ -616,6 +621,14 @@ function addPackageAnalytics(name: string, version: string): void { analytics.add('package', name + '@' + version); } +function getAuthHeader() { + const dockerToken = getDockerToken(); + if (dockerToken) { + return 'bearer ' + dockerToken; + } + return 'token ' + snyk.api; +} + function countUniqueVulns(vulns: AnnotatedIssue[]): number { const seen = {}; for (const curr of vulns) { diff --git a/src/lib/types.ts b/src/lib/types.ts index fa7ed9f15b9..405b5326d24 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -21,6 +21,7 @@ export interface TestOptions { reachableVulns?: boolean; reachableVulnsTimeout?: number; yarnWorkspaces?: boolean; + testDepGraphDockerEndpoint?: string | null; } export interface WizardOptions { diff --git a/test/acceptance/docker-token.test.ts b/test/acceptance/docker-token.test.ts new file mode 100644 index 00000000000..af054ef0967 --- /dev/null +++ b/test/acceptance/docker-token.test.ts @@ -0,0 +1,167 @@ +import * as tap from 'tap'; +import * as cli from '../../src/cli/commands'; +import { fakeServer } from './fake-server'; +import * as sinon from 'sinon'; + +const { test } = tap; + +const port = (process.env.PORT = process.env.SNYK_PORT = '12345'); +const BASE_API = '/api/v1'; +process.env.SNYK_API = 'http://localhost:' + port + BASE_API; +process.env.SNYK_HOST = 'http://localhost:' + port; +process.env.LOG_LEVEL = '0'; +const apiKey = '123456789'; +let oldkey; +let oldendpoint; +const server = fakeServer(BASE_API, apiKey); + +// This import needs to come after the server init +// it causes the configured API url to be incorrect. +import * as plugins from '../../src/lib/plugins/index'; + +test('setup', async (t) => { + t.plan(3); + + let key = await cli.config('get', 'api'); + oldkey = key; + t.pass('existing user config captured: ' + oldkey); + + key = await cli.config('get', 'endpoint'); + oldendpoint = key; + t.pass('existing user endpoint captured: ' + oldendpoint); + + await new Promise((resolve) => { + server.listen(port, resolve); + }); + t.pass('started demo server'); + t.end(); +}); + +test('prime config', async (t) => { + await cli.config('unset', 'endpoint'); + t.pass('endpoint removed'); + + await cli.config('unset', 'api'); + t.pass('api key removed'); + + process.env.SNYK_DOCKER_TOKEN = 'docker-jwt-token'; + t.pass('docker token set'); + + t.end(); +}); + +test('`snyk test` with docker flag - docker token and no api key', async (t) => { + stubDockerPluginResponse( + plugins, + { + plugin: { + packageManager: 'deb', + }, + package: {}, + }, + t, + ); + try { + await cli.test('foo:latest', { + docker: true, + }); + const req = server.popRequest(); + t.equal(req.method, 'POST', 'makes POST request'); + t.match(req.url, 'docker-jwt/test-dep-graph', 'posts to correct url'); + } catch (err) { + t.fail('did not expect exception to be thrown ' + err); + } +}); + +test('`snyk test` with docker flag - docker token and api key', async (t) => { + stubDockerPluginResponse( + plugins, + { + plugin: { + packageManager: 'deb', + }, + package: {}, + }, + t, + ); + await cli.config('set', 'api=' + apiKey); + try { + await cli.test('foo:latest', { + docker: true, + }); + const req = server.popRequest(); + t.equal(req.method, 'POST', 'makes POST request'); + t.match(req.url, 'test-dep-graph', 'posts to correct url'); + } catch (err) { + t.fail('did not expect exception to be thrown ' + err); + } + await cli.config('unset', 'api'); + t.end(); +}); + +test('`snyk test` without docker flag - docker token and no api key', async (t) => { + stubDockerPluginResponse( + plugins, + { + plugin: { + packageManager: 'deb', + }, + package: {}, + }, + t, + ); + try { + await cli.test('foo:latest', { + docker: false, + }); + t.fail('expected MissingApiTokenError'); + } catch (err) { + t.equal(err.name, 'MissingApiTokenError', 'should throw if not docker'); + } +}); + +test('teardown', async (t) => { + t.plan(4); + + delete process.env.SNYK_API; + delete process.env.SNYK_HOST; + delete process.env.SNYK_PORT; + delete process.env.SNYK_DOCKER_TOKEN; + t.notOk(process.env.SNYK_PORT, 'fake env values cleared'); + + await new Promise((resolve) => { + server.close(resolve); + }); + t.pass('server shutdown'); + + if (!oldkey) { + await cli.config('unset', 'api'); + } else { + await cli.config('set', 'api=' + oldkey); + } + + if (oldendpoint) { + await cli.config('set', `endpoint=${oldendpoint}`); + t.pass('user endpoint restored'); + } else { + t.pass('no endpoint'); + } + t.pass('user config restored'); + t.end(); +}); + +function stubDockerPluginResponse(plugins, fixture: string | object, t) { + const plugin = { + async inspect() { + return typeof fixture === 'object' ? fixture : require(fixture); + }, + }; + const spyPlugin = sinon.spy(plugin, 'inspect'); + const loadPlugin = sinon.stub(plugins, 'loadPlugin'); + loadPlugin + .withArgs(sinon.match.any, sinon.match({ docker: true })) + .returns(plugin); + t.teardown(loadPlugin.restore); + + return spyPlugin; +} diff --git a/test/acceptance/fake-server.ts b/test/acceptance/fake-server.ts index 33b87359e80..b663286c5bb 100644 --- a/test/acceptance/fake-server.ts +++ b/test/acceptance/fake-server.ts @@ -126,6 +126,16 @@ export function fakeServer(root, apikey) { return next(); }); + server.post(root + '/docker-jwt/test-dep-graph', (req, res, next) => { + res.send({ + result: { + issuesData: {}, + affectedPkgs: {}, + }, + }); + return next(); + }); + server.post(root + '/test-iac', (req, res, next) => { if (req.query.org && req.query.org === 'missing-org') { res.status(404);