From dffe148e33c0051092577d22ff1e15e6ed696688 Mon Sep 17 00:00:00 2001 From: Pierre Vanduynslager Date: Mon, 27 Aug 2018 15:58:53 -0400 Subject: [PATCH] fix: hide sensitive data in relesae notes and `fail`/`success` plugin params --- lib/definitions/plugins.js | 6 ++- lib/hide-sensitive.js | 5 +- lib/plugins/index.js | 5 +- lib/utils.js | 15 +++++- test/definitions/plugins.test.js | 26 +++++++--- test/index.test.js | 84 +++++++++++++++++++++++++++++++- 6 files changed, 128 insertions(+), 13 deletions(-) diff --git a/lib/definitions/plugins.js b/lib/definitions/plugins.js index bd37c3a2..bac4c6c7 100644 --- a/lib/definitions/plugins.js +++ b/lib/definitions/plugins.js @@ -1,5 +1,7 @@ const {isString, isPlainObject} = require('lodash'); const {gitHead} = require('../git'); +const hideSensitive = require('../hide-sensitive'); +const {hideSensitiveValues} = require('../utils'); const {RELEASE_TYPE, RELEASE_NOTES_SEPARATOR} = require('./constants'); module.exports = { @@ -40,7 +42,7 @@ module.exports = { }, }), }), - postprocess: results => results.filter(Boolean).join(RELEASE_NOTES_SEPARATOR), + postprocess: (results, {env}) => hideSensitive(env)(results.filter(Boolean).join(RELEASE_NOTES_SEPARATOR)), }, prepare: { default: ['@semantic-release/npm'], @@ -80,11 +82,13 @@ module.exports = { multiple: true, required: false, pipelineConfig: () => ({settleAll: true}), + preprocess: ({releases, env, ...inputs}) => ({...inputs, env, releases: hideSensitiveValues(env, releases)}), }, fail: { default: ['@semantic-release/github'], multiple: true, required: false, pipelineConfig: () => ({settleAll: true}), + preprocess: ({errors, env, ...inputs}) => ({...inputs, env, errors: hideSensitiveValues(env, errors)}), }, }; diff --git a/lib/hide-sensitive.js b/lib/hide-sensitive.js index bd680567..01dba6af 100644 --- a/lib/hide-sensitive.js +++ b/lib/hide-sensitive.js @@ -1,4 +1,4 @@ -const {escapeRegExp, size} = require('lodash'); +const {escapeRegExp, size, isString} = require('lodash'); const {SECRET_REPLACEMENT, SECRET_MIN_SIZE} = require('./definitions/constants'); module.exports = env => { @@ -7,5 +7,6 @@ module.exports = env => { ); const regexp = new RegExp(toReplace.map(envVar => escapeRegExp(env[envVar])).join('|'), 'g'); - return output => (output && toReplace.length > 0 ? output.toString().replace(regexp, SECRET_REPLACEMENT) : output); + return output => + output && isString(output) && toReplace.length > 0 ? output.toString().replace(regexp, SECRET_REPLACEMENT) : output; }; diff --git a/lib/plugins/index.js b/lib/plugins/index.js index 9abd0581..12a2055c 100644 --- a/lib/plugins/index.js +++ b/lib/plugins/index.js @@ -36,7 +36,10 @@ module.exports = (context, pluginsPath) => { ); plugins[type] = async input => - postprocess(await pipeline(steps, pipelineConfig && pipelineConfig(plugins, logger))(await preprocess(input))); + postprocess( + await pipeline(steps, pipelineConfig && pipelineConfig(plugins, logger))(await preprocess(input)), + input + ); return plugins; }, diff --git a/lib/utils.js b/lib/utils.js index 66fa78ed..b3e1a250 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,7 +1,20 @@ const {isFunction} = require('lodash'); +const hideSensitive = require('./hide-sensitive'); function extractErrors(err) { return err && isFunction(err[Symbol.iterator]) ? [...err] : [err]; } -module.exports = {extractErrors}; +function hideSensitiveValues(env, objs) { + const hideFunction = hideSensitive(env); + return objs.map(obj => { + Object.getOwnPropertyNames(obj).forEach(prop => { + if (obj[prop]) { + obj[prop] = hideFunction(obj[prop]); + } + }); + return obj; + }); +} + +module.exports = {extractErrors, hideSensitiveValues}; diff --git a/test/definitions/plugins.test.js b/test/definitions/plugins.test.js index 1ca6b3cc..61a86c7f 100644 --- a/test/definitions/plugins.test.js +++ b/test/definitions/plugins.test.js @@ -1,6 +1,6 @@ import test from 'ava'; import plugins from '../../lib/definitions/plugins'; -import {RELEASE_NOTES_SEPARATOR} from '../../lib/definitions/constants'; +import {RELEASE_NOTES_SEPARATOR, SECRET_REPLACEMENT} from '../../lib/definitions/constants'; test('The "analyzeCommits" plugin output must be either undefined or a valid semver release type', t => { t.false(plugins.analyzeCommits.outputValidator('invalid')); @@ -32,10 +32,22 @@ test('The "publish" plugin output, if defined, must be an object', t => { t.true(plugins.publish.outputValidator('')); }); -test('The "generateNotes" plugins output are concatenated with separator', t => { - t.is(plugins.generateNotes.postprocess(['note 1', 'note 2']), `note 1${RELEASE_NOTES_SEPARATOR}note 2`); - t.is(plugins.generateNotes.postprocess(['', 'note']), 'note'); - t.is(plugins.generateNotes.postprocess([undefined, 'note']), 'note'); - t.is(plugins.generateNotes.postprocess(['note 1', '', 'note 2']), `note 1${RELEASE_NOTES_SEPARATOR}note 2`); - t.is(plugins.generateNotes.postprocess(['note 1', undefined, 'note 2']), `note 1${RELEASE_NOTES_SEPARATOR}note 2`); +test('The "generateNotes" plugins output are concatenated with separator and sensitive data is hidden', t => { + const env = {MY_TOKEN: 'secret token'}; + t.is(plugins.generateNotes.postprocess(['note 1', 'note 2'], {env}), `note 1${RELEASE_NOTES_SEPARATOR}note 2`); + t.is(plugins.generateNotes.postprocess(['', 'note'], {env}), 'note'); + t.is(plugins.generateNotes.postprocess([undefined, 'note'], {env}), 'note'); + t.is(plugins.generateNotes.postprocess(['note 1', '', 'note 2'], {env}), `note 1${RELEASE_NOTES_SEPARATOR}note 2`); + t.is( + plugins.generateNotes.postprocess(['note 1', undefined, 'note 2'], {env}), + `note 1${RELEASE_NOTES_SEPARATOR}note 2` + ); + + t.is( + plugins.generateNotes.postprocess( + [`Note 1: Exposing token ${env.MY_TOKEN}`, `Note 2: Exposing token ${SECRET_REPLACEMENT}`], + {env} + ), + `Note 1: Exposing token ${SECRET_REPLACEMENT}${RELEASE_NOTES_SEPARATOR}Note 2: Exposing token ${SECRET_REPLACEMENT}` + ); }); diff --git a/test/index.test.js b/test/index.test.js index a57187ef..6c08debb 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -1,10 +1,11 @@ import test from 'ava'; +import {escapeRegExp, isString} from 'lodash'; import proxyquire from 'proxyquire'; import {spy, stub} from 'sinon'; import {WritableStreamBuffer} from 'stream-buffers'; import AggregateError from 'aggregate-error'; import SemanticReleaseError from '@semantic-release/error'; -import {COMMIT_NAME, COMMIT_EMAIL} from '../lib/definitions/constants'; +import {COMMIT_NAME, COMMIT_EMAIL, SECRET_REPLACEMENT} from '../lib/definitions/constants'; import { gitHead as getGitHead, gitTagHead, @@ -1031,6 +1032,87 @@ test('Throw an Error if plugin returns an unexpected value', async t => { t.regex(error.details, /string/); }); +test('Hide sensitive information passed to "fail" plugin', async t => { + const {cwd, repositoryUrl} = await gitRepo(true); + + const fail = stub().resolves(); + const env = {MY_TOKEN: 'secret token'}; + const options = { + branch: 'master', + repositoryUrl, + verifyConditions: stub().throws( + new SemanticReleaseError( + `Message: Exposing token ${env.MY_TOKEN}`, + 'ERR', + `Details: Exposing token ${env.MY_TOKEN}` + ) + ), + success: stub().resolves(), + fail, + }; + + const semanticRelease = requireNoCache('..', { + './lib/get-logger': () => t.context.logger, + 'env-ci': () => ({isCi: true, branch: 'master', isPr: false}), + }); + await t.throws( + semanticRelease(options, {cwd, env, stdout: new WritableStreamBuffer(), stderr: new WritableStreamBuffer()}), + Error + ); + + const error = fail.args[0][1].errors[0]; + + t.is(error.message, `Message: Exposing token ${SECRET_REPLACEMENT}`); + t.is(error.details, `Details: Exposing token ${SECRET_REPLACEMENT}`); + + Object.getOwnPropertyNames(error).forEach(prop => { + if (isString(error[prop])) { + t.notRegex(error[prop], new RegExp(escapeRegExp(env.MY_TOKEN))); + } + }); +}); + +test('Hide sensitive information passed to "success" plugin', async t => { + const {cwd, repositoryUrl} = await gitRepo(true); + await gitCommits(['feat: initial release'], {cwd}); + await gitTagVersion('v1.0.0', undefined, {cwd}); + await gitCommits(['feat: new feature'], {cwd}); + await gitPush(repositoryUrl, 'master', {cwd}); + + const success = stub().resolves(); + const env = {MY_TOKEN: 'secret token'}; + const options = { + branch: 'master', + repositoryUrl, + verifyConditions: false, + verifyRelease: false, + prepare: false, + publish: stub().resolves({ + name: `Name: Exposing token ${env.MY_TOKEN}`, + url: `URL: Exposing token ${env.MY_TOKEN}`, + }), + success, + fail: stub().resolves(), + }; + + const semanticRelease = requireNoCache('..', { + './lib/get-logger': () => t.context.logger, + 'env-ci': () => ({isCi: true, branch: 'master', isPr: false}), + }); + await semanticRelease(options, {cwd, env, stdout: new WritableStreamBuffer(), stderr: new WritableStreamBuffer()}); + + const release = success.args[0][1].releases[0]; + + t.is(release.name, `Name: Exposing token ${SECRET_REPLACEMENT}`); + t.is(release.url, `URL: Exposing token ${SECRET_REPLACEMENT}`); + + Object.getOwnPropertyNames(release).forEach(prop => { + if (isString(release[prop])) { + t.notRegex(release[prop], new RegExp(escapeRegExp(env.MY_TOKEN))); + } + }); +}); + test('Get all commits including the ones not in the shallow clone', async t => { let {cwd, repositoryUrl} = await gitRepo(true); await gitTagVersion('v1.0.0', undefined, {cwd});