From fb0caa005bcca5d0cedc7a0d1e7e082d2efeee80 Mon Sep 17 00:00:00 2001 From: Pierre Vanduynslager Date: Thu, 25 Jan 2018 11:08:28 -0500 Subject: [PATCH] feat: hide sensitive info in stdout/sdtin --- index.js | 5 +++++ lib/hide-sensitive.js | 11 +++++++++++ package.json | 2 ++ test/hide-sensitive.test.js | 34 +++++++++++++++++++++++++++++++++ test/index.test.js | 38 +++++++++++++++++++++++++++++++++---- test/integration.test.js | 2 +- 6 files changed, 87 insertions(+), 5 deletions(-) create mode 100644 lib/hide-sensitive.js create mode 100644 test/hide-sensitive.test.js diff --git a/index.js b/index.js index 2d22841e..8c9a1620 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,8 @@ const marked = require('marked'); const TerminalRenderer = require('marked-terminal'); const envCi = require('env-ci'); +const hookStd = require('hook-std'); +const hideSensitive = require('./lib/hide-sensitive'); const getConfig = require('./lib/get-config'); const getNextVersion = require('./lib/get-next-version'); const getCommits = require('./lib/get-commits'); @@ -96,8 +98,10 @@ async function run(opts) { } module.exports = async opts => { + const unhook = hookStd({silent: false}, hideSensitive); try { const result = await run(opts); + unhook(); return result; } catch (err) { const errors = err.name === 'AggregateError' ? Array.from(err).sort(error => !error.semanticRelease) : [err]; @@ -108,6 +112,7 @@ module.exports = async opts => { logger.error('An error occurred while running semantic-release: %O', error); } } + unhook(); throw err; } }; diff --git a/lib/hide-sensitive.js b/lib/hide-sensitive.js new file mode 100644 index 00000000..9d6e36b4 --- /dev/null +++ b/lib/hide-sensitive.js @@ -0,0 +1,11 @@ +const {escapeRegExp} = require('lodash'); + +const regexp = new RegExp( + Object.keys(process.env) + .filter(envVar => /token|password|credential|secret|private/i.test(envVar)) + .map(envVar => escapeRegExp(process.env[envVar])) + .join('|'), + 'g' +); + +module.exports = output => output.replace(regexp, '[secure]'); diff --git a/package.json b/package.json index 2961c872..f31932ee 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "execa": "^0.9.0", "get-stream": "^3.0.0", "git-log-parser": "^1.2.0", + "hook-std": "^0.4.0", "lodash": "^4.17.4", "marked": "^0.3.9", "marked-terminal": "^2.0.0", @@ -44,6 +45,7 @@ }, "devDependencies": { "ava": "^0.25.0", + "clear-module": "^2.1.0", "codecov": "^3.0.0", "commitizen": "^2.9.6", "cz-conventional-changelog": "^2.0.0", diff --git a/test/hide-sensitive.test.js b/test/hide-sensitive.test.js new file mode 100644 index 00000000..7a8fd55a --- /dev/null +++ b/test/hide-sensitive.test.js @@ -0,0 +1,34 @@ +import test from 'ava'; +import clearModule from 'clear-module'; + +test.beforeEach(() => { + process.env = {}; + clearModule('../lib/hide-sensitive'); +}); + +test.serial('Replace multiple sensitive environment variable values', t => { + process.env.SOME_PASSWORD = 'password'; + process.env.SOME_TOKEN = 'secret'; + t.is( + require('../lib/hide-sensitive')( + `https://user:${process.env.SOME_PASSWORD}@host.com?token=${process.env.SOME_TOKEN}` + ), + 'https://user:[secure]@host.com?token=[secure]' + ); +}); + +test.serial('Replace multiple occurences of sensitive environment variable values', t => { + process.env.secretKey = 'secret'; + t.is( + require('../lib/hide-sensitive')(`https://user:${process.env.secretKey}@host.com?token=${process.env.secretKey}`), + 'https://user:[secure]@host.com?token=[secure]' + ); +}); + +test.serial('Escape regexp special characters', t => { + process.env.SOME_CREDENTIALS = 'p$^{.+}\\w[a-z]o.*rd'; + t.is( + require('../lib/hide-sensitive')(`https://user:${process.env.SOME_CREDENTIALS}@host.com`), + 'https://user:[secure]@host.com' + ); +}); diff --git a/test/index.test.js b/test/index.test.js index 8436aed7..82dc6685 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -2,6 +2,7 @@ import test from 'ava'; import proxyquire from 'proxyquire'; import {stub} from 'sinon'; import tempy from 'tempy'; +import clearModule from 'clear-module'; import SemanticReleaseError from '@semantic-release/error'; import DEFINITIONS from '../lib/plugins/definitions'; import {gitHead as getGitHead} from '../lib/git'; @@ -12,21 +13,25 @@ const envBackup = Object.assign({}, process.env); // Save the current working diretory const cwd = process.cwd(); -stub(process.stdout, 'write'); -stub(process.stderr, 'write'); - test.beforeEach(t => { + clearModule('../lib/hide-sensitive'); + // Stub the logger functions t.context.log = stub(); t.context.error = stub(); t.context.logger = {log: t.context.log, error: t.context.error}; + t.context.stdout = stub(process.stdout, 'write'); + t.context.stderr = stub(process.stderr, 'write'); }); -test.afterEach.always(() => { +test.afterEach.always(t => { // Restore process.env process.env = envBackup; // Restore the current working directory process.chdir(cwd); + + t.context.stdout.restore(); + t.context.stderr.restore(); }); test.serial('Plugins are called with expected values', async t => { @@ -571,6 +576,31 @@ test.serial('Exclude commits with [skip release] or [release skip] from analysis t.deepEqual(analyzeCommits.args[0][1].commits[0].message, commits[commits.length - 1].message); }); +test.serial('Hide sensitive environment variable values from the logs', async t => { + process.env.MY_TOKEN = 'secret token'; + await gitRepo(); + + const options = { + branch: 'master', + repositoryUrl: 'git@hostname.com:owner/module.git', + verifyConditions: async (pluginConfig, {logger}) => { + console.log(`Console: The token ${process.env.MY_TOKEN} is invalid`); + logger.log(`Log: The token ${process.env.MY_TOKEN} is invalid`); + logger.error(`Error: The token ${process.env.MY_TOKEN} is invalid`); + throw new Error(`Invalid token ${process.env.MY_TOKEN}`); + }, + }; + const semanticRelease = proxyquire('..', { + 'env-ci': () => ({isCi: true, branch: 'master', isPr: false}), + }); + + await t.throws(semanticRelease(options)); + t.regex(t.context.stdout.args[7][0], /Console: The token \[secure\] is invalid/); + t.regex(t.context.stdout.args[8][0], /Log: The token \[secure\] is invalid/); + t.regex(t.context.stderr.args[0][0], /Error: The token \[secure\] is invalid/); + t.regex(t.context.stderr.args[1][0], /Invalid token \[secure\]/); +}); + test.serial('Throw SemanticReleaseError if repositoryUrl is not set and cannot be found from repo config', async t => { // Create a git repository, set the current working directory at the root of the repo await gitRepo(); diff --git a/test/integration.test.js b/test/integration.test.js index 11ff0d28..93509bcc 100644 --- a/test/integration.test.js +++ b/test/integration.test.js @@ -61,7 +61,7 @@ test.beforeEach(() => { // Delete all `npm_config` environment variable set by CI as they take precedence over the `.npmrc` because the process that runs the tests is started before the `.npmrc` is created for (let i = 0, keys = Object.keys(process.env); i < keys.length; i++) { - if (keys[i].startsWith('npm_config')) { + if (keys[i].startsWith('npm_')) { delete process.env[keys[i]]; } }