From 2fe61d2c5cc3d51c641aae588252e05ee72e2d82 Mon Sep 17 00:00:00 2001 From: Caleb Cartwright Date: Thu, 20 Dec 2018 15:28:54 -0600 Subject: [PATCH] Add [Snyk] badges (#2566) Adds vulnerability badges from Snyk.io, closes #1642 - [X] Vulnerability badge for GitHub repos - [x] Vulnerability badge for npm package --- services/snyk/snyk-test-helpers.js | 11 +++ services/snyk/snyk-vulnerability-base.js | 46 +++++++++ .../snyk/snyk-vulnerability-github.service.js | 56 +++++++++++ .../snyk/snyk-vulnerability-github.tester.js | 94 +++++++++++++++++++ .../snyk/snyk-vulnerability-npm.service.js | 66 +++++++++++++ .../snyk/snyk-vulnerability-npm.tester.js | 92 ++++++++++++++++++ 6 files changed, 365 insertions(+) create mode 100644 services/snyk/snyk-test-helpers.js create mode 100644 services/snyk/snyk-vulnerability-base.js create mode 100644 services/snyk/snyk-vulnerability-github.service.js create mode 100644 services/snyk/snyk-vulnerability-github.tester.js create mode 100644 services/snyk/snyk-vulnerability-npm.service.js create mode 100644 services/snyk/snyk-vulnerability-npm.tester.js diff --git a/services/snyk/snyk-test-helpers.js b/services/snyk/snyk-test-helpers.js new file mode 100644 index 0000000000000..3fea094774e10 --- /dev/null +++ b/services/snyk/snyk-test-helpers.js @@ -0,0 +1,11 @@ +'use strict' + +const zeroVulnerabilitiesSvg = + 'vulnerabilitiesvulnerabilities00' +const twoVulnerabilitiesSvg = + 'vulnerabilitiesvulnerabilities22' + +module.exports = { + zeroVulnerabilitiesSvg, + twoVulnerabilitiesSvg, +} diff --git a/services/snyk/snyk-vulnerability-base.js b/services/snyk/snyk-vulnerability-base.js new file mode 100644 index 0000000000000..3b3b76ea91a79 --- /dev/null +++ b/services/snyk/snyk-vulnerability-base.js @@ -0,0 +1,46 @@ +'use strict' + +const Joi = require('joi') +const BaseSvgScrapingService = require('../base-svg-scraping') + +const schema = Joi.object({ + message: Joi.alternatives() + .try([/^\d*$/, Joi.equal('unknown')]) + .required(), +}).required() + +module.exports = class SnykVulnerabilityBase extends BaseSvgScrapingService { + static render({ vulnerabilities }) { + let color = 'red' + if (vulnerabilities === '0') { + color = 'brightgreen' + } + return { + message: vulnerabilities, + color, + } + } + + async fetch({ url, qs, errorMessages }) { + const { message: vulnerabilities } = await this._requestSvg({ + url, + schema, + options: { + qs, + }, + errorMessages, + }) + + return { vulnerabilities } + } + + static get category() { + return 'quality' + } + + static get defaultBadgeData() { + return { + label: 'vulnerabilities', + } + } +} diff --git a/services/snyk/snyk-vulnerability-github.service.js b/services/snyk/snyk-vulnerability-github.service.js new file mode 100644 index 0000000000000..fe9980878a6a8 --- /dev/null +++ b/services/snyk/snyk-vulnerability-github.service.js @@ -0,0 +1,56 @@ +'use strict' + +const SynkVulnerabilityBase = require('./snyk-vulnerability-base') + +module.exports = class SnykVulnerabilityGitHub extends SynkVulnerabilityBase { + static get route() { + return { + base: 'snyk/vulnerabilities/github', + format: '([^/]+)/([^/]+)(?:/(.+))?', + capture: ['user', 'repo', 'manifestFilePath'], + } + } + + static get examples() { + return [ + { + title: 'Snyk Vulnerabilities for GitHub Repo', + pattern: ':user/:repo', + namedParams: { + user: 'badges', + repo: 'shields', + }, + staticExample: this.render({ vulnerabilities: '0' }), + }, + { + title: 'Snyk Vulnerabilities for GitHub Repo (Specific Manifest)', + pattern: ':user/:repo/:manifestFilePath', + namedParams: { + user: 'badges', + repo: 'shields', + manifestFilePath: 'gh-badges/package.json', + }, + staticExample: this.render({ vulnerabilities: '0' }), + documentation: ` +

+ Provide the path to your target manifest file relative to the base of your repository. + Snyk does not support using a specific branch for this, so do not include "blob" nor a branch name. +

+ `, + }, + ] + } + + async handle({ user, repo, manifestFilePath }) { + const url = `https://snyk.io/test/github/${user}/${repo}/badge.svg` + const qs = { targetFile: manifestFilePath } + const { vulnerabilities } = await this.fetch({ + url, + qs, + errorMessages: { + 404: 'repo or manifest not found', + }, + }) + return this.constructor.render({ vulnerabilities }) + } +} diff --git a/services/snyk/snyk-vulnerability-github.tester.js b/services/snyk/snyk-vulnerability-github.tester.js new file mode 100644 index 0000000000000..17f3250d36cb3 --- /dev/null +++ b/services/snyk/snyk-vulnerability-github.tester.js @@ -0,0 +1,94 @@ +'use strict' + +const Joi = require('joi') +const t = (module.exports = require('../create-service-tester')()) +const { colorScheme } = require('../test-helpers') +const { + twoVulnerabilitiesSvg, + zeroVulnerabilitiesSvg, +} = require('./snyk-test-helpers') + +t.create('live: valid repo') + .get('/badges/shields.json') + .expectJSONTypes( + Joi.object().keys({ + name: 'vulnerabilities', + value: Joi.number().required(), + }) + ) + +t.create('live: non existent repo') + .get('/badges/not-real.json') + .timeout(10000) + .expectJSON({ name: 'vulnerabilities', value: 'repo or manifest not found' }) + +t.create('live: valid target manifest path') + .get('/badges/shields/gh-badges/package.json.json') + .expectJSONTypes( + Joi.object().keys({ + name: 'vulnerabilities', + value: Joi.number().required(), + }) + ) + +t.create('live: invalid target manifest path') + .get('/badges/shields/gh-badges/requirements.txt.json') + .expectJSON({ name: 'vulnerabilities', value: 'repo or manifest not found' }) + +t.create('repo has no vulnerabilities') + .get('/badges/shields.json?style=_shields_test') + .intercept(nock => + nock('https://snyk.io/test/github/badges/shields') + .get('/badge.svg') + .reply(200, zeroVulnerabilitiesSvg) + ) + .expectJSON({ + name: 'vulnerabilities', + value: '0', + colorB: colorScheme.brightgreen, + }) + +t.create('repo has vulnerabilities') + .get('/badges/shields.json?style=_shields_test') + .intercept(nock => + nock('https://snyk.io/test/github/badges/shields') + .get('/badge.svg') + .reply(200, twoVulnerabilitiesSvg) + ) + .expectJSON({ + name: 'vulnerabilities', + value: '2', + colorB: colorScheme.red, + }) + +t.create('target manifest file has no vulnerabilities') + .get('/badges/shields/gh-badges/package.json.json?style=_shields_test') + .intercept(nock => + nock('https://snyk.io/test/github/badges/shields') + .get('/badge.svg') + .query({ + targetFile: 'gh-badges/package.json', + }) + .reply(200, zeroVulnerabilitiesSvg) + ) + .expectJSON({ + name: 'vulnerabilities', + value: '0', + colorB: colorScheme.brightgreen, + }) + +t.create('target manifest file has vulnerabilities') + .get('/badges/shields/gh-badges/package.json.json?style=_shields_test') + .intercept(nock => + nock('https://snyk.io/test/github/badges/shields') + .get('/badge.svg') + .query({ + targetFile: 'gh-badges/package.json', + }) + .reply(200, twoVulnerabilitiesSvg) + ) + .expectJSON({ + name: 'vulnerabilities', + value: '2', + colorB: colorScheme.red, + }) diff --git a/services/snyk/snyk-vulnerability-npm.service.js b/services/snyk/snyk-vulnerability-npm.service.js new file mode 100644 index 0000000000000..344544922411f --- /dev/null +++ b/services/snyk/snyk-vulnerability-npm.service.js @@ -0,0 +1,66 @@ +'use strict' + +const { NotFound } = require('../errors') +const SynkVulnerabilityBase = require('./snyk-vulnerability-base') + +module.exports = class SnykVulnerabilityNpm extends SynkVulnerabilityBase { + static get route() { + return { + base: 'snyk/vulnerabilities/npm', + pattern: ':packageName(.+)', + } + } + + static get examples() { + return [ + { + title: 'Snyk Vulnerabilities for npm package', + pattern: ':packageName', + namedParams: { + packageName: 'mocha', + }, + staticExample: this.render({ vulnerabilities: '0' }), + }, + { + title: 'Snyk Vulnerabilities for npm package version', + pattern: ':packageName', + namedParams: { + packageName: 'mocha@4.0.0', + }, + staticExample: this.render({ vulnerabilities: '1' }), + }, + { + title: 'Snyk Vulnerabilities for npm scoped package', + pattern: ':packageName', + namedParams: { + packageName: '@babel/core', + }, + staticExample: this.render({ vulnerabilities: '0' }), + }, + ] + } + + async handle({ packageName }) { + const url = `https://snyk.io/test/npm/${packageName}/badge.svg` + + try { + const { vulnerabilities } = await this.fetch({ + url, + // Snyk returns an HTTP 200 with an HTML page when the specified + // npm package is not found that contains the text 404. + // Including this in case Snyk starts returning a 404 response code instead. + errorMessages: { + 404: 'npm package is invalid or does not exist', + }, + }) + return this.constructor.render({ vulnerabilities }) + } catch (e) { + // If the package is invalid/nonexistent Snyk will return an HTML page + // which will result in an InvalidResponse error being thrown by the valueFromSvgBadge() + // function. Catching it here to switch to a more contextualized error message. + throw new NotFound({ + prettyMessage: 'npm package is invalid or does not exist', + }) + } + } +} diff --git a/services/snyk/snyk-vulnerability-npm.tester.js b/services/snyk/snyk-vulnerability-npm.tester.js new file mode 100644 index 0000000000000..27e05f5a573f1 --- /dev/null +++ b/services/snyk/snyk-vulnerability-npm.tester.js @@ -0,0 +1,92 @@ +'use strict' + +const Joi = require('joi') +const t = (module.exports = require('../create-service-tester')()) +const { colorScheme } = require('../test-helpers') +const { + twoVulnerabilitiesSvg, + zeroVulnerabilitiesSvg, +} = require('./snyk-test-helpers') + +t.create('live: valid package latest version') + .get('/mocha.json') + .timeout(7500) + .expectJSONTypes( + Joi.object().keys({ + name: 'vulnerabilities', + value: Joi.number().required(), + }) + ) + +t.create('live: valid scoped package latest version') + .get('/@babel/core.json') + .timeout(7500) + .expectJSONTypes( + Joi.object().keys({ + name: 'vulnerabilities', + value: Joi.number().required(), + }) + ) + +t.create('live: non existent package') + .get('/mochaabcdef.json') + .timeout(7500) + .expectJSON({ + name: 'vulnerabilities', + value: 'npm package is invalid or does not exist', + }) + +t.create('live: valid package specific version') + .get('/mocha@4.0.0.json?style=_shields_test') + .expectJSON({ + name: 'vulnerabilities', + value: '1', + colorB: colorScheme.red, + }) + +t.create('live: non existent package version') + .get('/gh-badges@0.3.4.json') + .timeout(7500) + .expectJSON({ + name: 'vulnerabilities', + value: 'npm package is invalid or does not exist', + }) + +t.create('package has no vulnerabilities') + .get('/mocha.json?style=_shields_test') + .intercept(nock => + nock('https://snyk.io/test/npm/mocha') + .get('/badge.svg') + .reply(200, zeroVulnerabilitiesSvg) + ) + .expectJSON({ + name: 'vulnerabilities', + value: '0', + colorB: colorScheme.brightgreen, + }) + +t.create('package has vulnerabilities') + .get('/mocha.json?style=_shields_test') + .intercept(nock => + nock('https://snyk.io/test/npm/mocha') + .get('/badge.svg') + .reply(200, twoVulnerabilitiesSvg) + ) + .expectJSON({ + name: 'vulnerabilities', + value: '2', + colorB: colorScheme.red, + }) + +t.create('package not found') + .get('/not-mocha-fake-ish@13.0.0.json?style=_shields_test') + .intercept(nock => + nock('https://snyk.io/test/npm/not-mocha-fake-ish@13.0.0') + .get('/badge.svg') + .reply(200, 'foo') + ) + .expectJSON({ + name: 'vulnerabilities', + value: 'npm package is invalid or does not exist', + colorB: colorScheme.red, + })