Skip to content

Commit

Permalink
Merge pull request #309 from snyk/feat/support-docker-binaries
Browse files Browse the repository at this point in the history
Feat/support docker binaries
  • Loading branch information
karniwl committed Dec 18, 2018
2 parents 75f5cfb + c04550b commit 397b857
Show file tree
Hide file tree
Showing 5 changed files with 382 additions and 35 deletions.
67 changes: 36 additions & 31 deletions src/cli/commands/test.js
Expand Up @@ -26,7 +26,7 @@ function test() {
}

// populate with default path (cwd) if no path given
if (args.length === 0) {
if (args.length === 0) {
args.unshift(process.cwd());
}

Expand All @@ -43,12 +43,12 @@ function test() {

return apiTokenExists('snyk test')
.then(function () {
// Promise waterfall to test all other paths sequentially
// Promise waterfall to test all other paths sequentially
var testsPromises = args.reduce(function (acc, path) {
return acc.then(function () {
// Create a copy of the options so a specific test can
// modify them i.e. add `options.file` etc. We'll need
// these options later.
// Create a copy of the options so a specific test can
// modify them i.e. add `options.file` etc. We'll need
// these options later.
var testOpts = _.cloneDeep(options);
testOpts.path = path;
resultOptions.push(testOpts);
Expand Down Expand Up @@ -89,12 +89,12 @@ function test() {

return testsPromises;
}).then(function () {
// resultOptions is now an array of 1 or more options used for
// the tests results is now an array of 1 or more test results
// values depend on `options.json` value - string or object
// resultOptions is now an array of 1 or more options used for
// the tests results is now an array of 1 or more test results
// values depend on `options.json` value - string or object
if (options.json) {
results = results.map(function (result) {
// add json for when thrown exception
// add json for when thrown exception
if (result instanceof Error) {
return {
ok: false,
Expand Down Expand Up @@ -142,8 +142,8 @@ function test() {
if (results.length > 1) {
var projects = results.length === 1 ? ' project' : ' projects';
summaryMessage = '\n\n' + '\nTested ' + results.length + projects +
summariseVulnerableResults(vulnerableResults, options) +
summariseErrorResults(errorResults) + '\n';
summariseVulnerableResults(vulnerableResults, options) +
summariseErrorResults(errorResults) + '\n';
}

var notSuccess = vulnerableResults.length > 0 || errorResults.length > 0;
Expand Down Expand Up @@ -183,7 +183,7 @@ function summariseVulnerableResults(vulnerableResults, options) {

function summariseErrorResults(errorResults) {
const projects =
errorResults.length > 1 ? ' projects' : ' project';
errorResults.length > 1 ? ' projects' : ' project';
if (errorResults.length > 0) {
return ' Failed to test ' + errorResults.length + projects +
'.\nRun with `-d` for debug output and contact support@snyk.io';
Expand Down Expand Up @@ -227,13 +227,17 @@ function displayResult(res, options) {
'✓ ' + testedInfoText + vulnPathsText
);
var nextStepsText =
'\n\nNext steps:' +
'\n- Run `snyk monitor` to be notified ' +
'about new related vulnerabilities.' +
'\n- Run `snyk test` as part of ' +
'your CI/test.';
'\n\nNext steps:' +
'\n- Run `snyk monitor` to be notified ' +
'about new related vulnerabilities.' +
'\n- Run `snyk test` as part of ' +
'your CI/test.';
return (
prefix + meta + summaryOKText + (isCI ? '' : dockerAdvice + nextStepsText + dockerSuggestion)
prefix + meta + summaryOKText + (
isCI ? '' :
dockerAdvice +
nextStepsText +
dockerSuggestion)
);
}

Expand Down Expand Up @@ -268,11 +272,11 @@ function displayResult(res, options) {
}

if (options.docker &&
!options.file &&
(config.disableSuggestions !== 'true')) {
!options.file &&
(config.disableSuggestions !== 'true')) {
summary += chalk.bold.white('\n\nPro tip: use `--file` option to get base image remediation advice.' +
`\nExample: $ snyk test --docker ${options.path} --file=path/to/Dockerfile` +
'\n\nTo remove this message in the future, please run `snyk config set disableSuggestions=true`');
`\nExample: $ snyk test --docker ${options.path} --file=path/to/Dockerfile` +
'\n\nTo remove this message in the future, please run `snyk config set disableSuggestions=true`');
}

summary += dockerSuggestion;
Expand All @@ -294,6 +298,7 @@ function displayResult(res, options) {
return i.from;
}))
.join(', ');

var vulnOutput = {
issueHeading: createSeverityBasedIssueHeading(
vuln.metadata.severity,
Expand Down Expand Up @@ -326,7 +331,7 @@ function displayResult(res, options) {

var body = groupedVulnInfoOutput.join('\n\n') + '\n\n' + meta + summary;
return prefix + body + dockerAdvice;
}
};

function createFixedInText(groupedVuln) {
var vulnerableRange = groupedVuln.list[0].semver.vulnerable[0];
Expand All @@ -347,13 +352,13 @@ function createRemediationText(vuln, packageManager) {
if (vuln.isOutdated === true) {
var packageManagerOutdatedText = {
npm: '\n Try deleting node_modules, reinstalling ' +
'and running `snyk test` again. If the problem persists, ' +
'one of your dependencies may be bundling outdated modules.',
'and running `snyk test` again. If the problem persists, ' +
'one of your dependencies may be bundling outdated modules.',
rubygems: '\n Try running `bundle update ' + packageName + '` ' +
'and running `snyk test` again.',
'and running `snyk test` again.',
yarn: '\n Try deleting node_modules, reinstalling ' +
'and running `snyk test` again. If the problem persists, ' +
'one of your dependencies may be bundling outdated modules.',
'and running `snyk test` again. If the problem persists, ' +
'one of your dependencies may be bundling outdated modules.',
};

return chalk.bold(
Expand Down Expand Up @@ -535,7 +540,7 @@ function getSeverityValue(severity) {
}

function titleCaseText(text) {
return text[0].toUpperCase() + text.slice(1) ;
return text[0].toUpperCase() + text.slice(1);
}

// This is all a copy from Registry snapshots/index
Expand Down Expand Up @@ -566,11 +571,11 @@ function groupVulnerabilities(vulns) {
}

if (!map[curr.id].isOutdated) {
map[curr.id].isOutdated = !!curr.isOutdated ;
map[curr.id].isOutdated = !!curr.isOutdated;
}

if (!map[curr.id].note) {
map[curr.id].note = !!curr.note ;
map[curr.id].note = !!curr.note;
}

return map;
Expand Down
21 changes: 21 additions & 0 deletions src/lib/snyk-test/legacy.ts
Expand Up @@ -151,6 +151,27 @@ function convertTestDepGraphResultToLegacy(
}
}

const dockerRes = result.docker as any;

if (dockerRes && dockerRes.binariesVulns) {
const binariesVulns = dockerRes.binariesVulns;
for (const pkgInfo of _.values(binariesVulns.affectedPkgs)) {
for (const pkgIssue of _.values(pkgInfo.issues)) {
const pkgAndVersion =
pkgInfo.pkg.name + '@' + pkgInfo.pkg.version as string;
const annotatedIssue = Object.assign({}, binariesVulns.issuesData[pkgIssue.issueId], {
from: ['Upstream', pkgAndVersion],
upgradePath: [],
isUpgradable: false,
isPatchable: false,
name: pkgInfo.pkg.name,
version: pkgInfo.pkg.version as string,
});
vulns.push(annotatedIssue);
}
}
}

const meta = res.meta || {};

severityThreshold = (severityThreshold === 'low') ? undefined : severityThreshold;
Expand Down
32 changes: 28 additions & 4 deletions src/lib/snyk-test/run-test.ts
Expand Up @@ -32,12 +32,14 @@ async function runTest(packageManager: string, root: string , options): Promise<
const payload = await assemblePayload(root, options, policyLocations);
const filesystemPolicy = payload.body && !!payload.body.policy;
const depGraph = payload.body && payload.body.depGraph;

await spinner(spinnerLbl);
let res = await sendPayload(payload, hasDevDependencies);

if (depGraph) {
res = convertTestDepGraphResultToLegacy(res, depGraph, packageManager, options.severityThreshold);
res = convertTestDepGraphResultToLegacy(
res,
depGraph,
packageManager,
options.severityThreshold);
}

analytics.add('vulns-pre-policy', res.vulnerabilities.length);
Expand Down Expand Up @@ -186,6 +188,8 @@ async function assembleLocalPayload(root, options, policyLocations): Promise<Pay
}
}

const dockerInfo = createDockerInfo(pkg);

const payload: Payload = {
method: 'POST',
url: config.API + '/test-dep-graph',
Expand All @@ -200,7 +204,7 @@ async function assembleLocalPayload(root, options, policyLocations): Promise<Pay
targetFile: pkg.targetFile || options.file,
projectNameOverride: options.projectName,
policy: policy && policy.toString(),
docker: pkg.docker,
docker: dockerInfo,
},
};

Expand All @@ -210,6 +214,26 @@ async function assembleLocalPayload(root, options, policyLocations): Promise<Pay
}
}

function createDockerInfo(pkg: any) {
let objectChanged = false;

const dockerInfo = {
baseImage: undefined,
binaries: undefined,
};

if (pkg.docker && pkg.docker.baseImage) {
dockerInfo.baseImage = pkg.docker.baseImage;
objectChanged = true;
}
if (pkg.docker && pkg.docker.binaries) {
dockerInfo.binaries = pkg.docker.binaries.Analysis;
objectChanged = true;
}

return objectChanged ? dockerInfo : undefined;
}

async function assembleRemotePayload(root, options): Promise<Payload> {
const pkg = moduleToObject(root);
debug('testing remote: %s', pkg.name + '@' + pkg.version);
Expand Down
103 changes: 103 additions & 0 deletions test/acceptance/cli.acceptance.test.ts
Expand Up @@ -1563,6 +1563,109 @@ test('`test foo:latest --docker` supports custom policy', async (t) => {
t.equal(policyString, expected, 'sends correct policy');
});

test('`test foo:latest --docker with binaries`', async (t) => {
const plugin = {
inspect: () => {
return Promise.resolve({
plugin: {
packageManager: 'deb',
},
package: {
docker: {
binaries: {
Analysis: [{name: 'node', version: '5.10.1'}],
},
},
},
});
},
};
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);

const res = await cli.test('foo:latest', {
docker: true,
org: 'explicit-org',
});
const req = server.popRequest();
t.equal(req.method, 'POST', 'makes POST request');
t.match(req.url, '/test-dep-graph', 'posts to correct url');
t.equal(req.body.depGraph.pkgManager.name, 'deb');
t.match(req.body.docker.binaries, [{name: 'node', version: '5.10.1'}],
'posts docker binaries');
t.same(spyPlugin.getCall(0).args,
['foo:latest', null, {
args: null,
file: null,
docker: true,
org: 'explicit-org',
packageManager: null,
path: 'foo:latest',
showVulnPaths: true,
}], 'calls docker plugin with expected arguments');
});

test('`test foo:latest --docker with binaries vulnerabilities`', async (t) => {
const plugin = {
inspect: () => {
return Promise.resolve({
plugin: {
packageManager: 'deb',
},
package: {
name: 'docker-image',
dependencies: {
'apt/libapt-pkg5.0': {
version: '1.6.3ubuntu0.1',
dependencies: {
'bzip2/libbz2-1.0': {
version: '1.0.6-8.1',
},
},
},
'bzip2/libbz2-1.0': {
version: '1.0.6-8.1',
},
},
docker: {
binaries: {
Analysis: [{name: 'node', version: '5.10.1'}],
},
},
},
});
},
};
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);

const vulns = require('./fixtures/docker/find-result-binaries.json');
server.setNextResponse(vulns);

try {
await cli.test('foo:latest', {
docker: true,
org: 'explicit-org',
});
t.fail('should have found vuln');
} catch (err) {
const msg = err.message;
console.log(msg);
t.match(msg, 'Tested 2 dependencies for known vulnerabilities, found 2 vulnerabilities');
t.match(msg, 'From: bzip2/libbz2-1.0@1.0.6-8.1');
t.match(msg, 'From: apt/libapt-pkg5.0@1.6.3ubuntu0.1 > bzip2/libbz2-1.0@1.0.6-8.1');
t.match(msg, 'Info: http://localhost:12345/vuln/SNYK-UPSTREAM-NODE-72359');
t.false(msg.includes('vulnerable paths'),
'docker should not includes number of vulnerable paths');
}
});

test('`test --policy-path`', async (t) => {
t.plan(3);

Expand Down

0 comments on commit 397b857

Please sign in to comment.