fix: hide sensitive data in relesae notes and fail
/success
plugin params
This commit is contained in:
parent
1aed97e577
commit
dffe148e33
@ -1,5 +1,7 @@
|
|||||||
const {isString, isPlainObject} = require('lodash');
|
const {isString, isPlainObject} = require('lodash');
|
||||||
const {gitHead} = require('../git');
|
const {gitHead} = require('../git');
|
||||||
|
const hideSensitive = require('../hide-sensitive');
|
||||||
|
const {hideSensitiveValues} = require('../utils');
|
||||||
const {RELEASE_TYPE, RELEASE_NOTES_SEPARATOR} = require('./constants');
|
const {RELEASE_TYPE, RELEASE_NOTES_SEPARATOR} = require('./constants');
|
||||||
|
|
||||||
module.exports = {
|
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: {
|
prepare: {
|
||||||
default: ['@semantic-release/npm'],
|
default: ['@semantic-release/npm'],
|
||||||
@ -80,11 +82,13 @@ module.exports = {
|
|||||||
multiple: true,
|
multiple: true,
|
||||||
required: false,
|
required: false,
|
||||||
pipelineConfig: () => ({settleAll: true}),
|
pipelineConfig: () => ({settleAll: true}),
|
||||||
|
preprocess: ({releases, env, ...inputs}) => ({...inputs, env, releases: hideSensitiveValues(env, releases)}),
|
||||||
},
|
},
|
||||||
fail: {
|
fail: {
|
||||||
default: ['@semantic-release/github'],
|
default: ['@semantic-release/github'],
|
||||||
multiple: true,
|
multiple: true,
|
||||||
required: false,
|
required: false,
|
||||||
pipelineConfig: () => ({settleAll: true}),
|
pipelineConfig: () => ({settleAll: true}),
|
||||||
|
preprocess: ({errors, env, ...inputs}) => ({...inputs, env, errors: hideSensitiveValues(env, errors)}),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
const {escapeRegExp, size} = require('lodash');
|
const {escapeRegExp, size, isString} = require('lodash');
|
||||||
const {SECRET_REPLACEMENT, SECRET_MIN_SIZE} = require('./definitions/constants');
|
const {SECRET_REPLACEMENT, SECRET_MIN_SIZE} = require('./definitions/constants');
|
||||||
|
|
||||||
module.exports = env => {
|
module.exports = env => {
|
||||||
@ -7,5 +7,6 @@ module.exports = env => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const regexp = new RegExp(toReplace.map(envVar => escapeRegExp(env[envVar])).join('|'), 'g');
|
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;
|
||||||
};
|
};
|
||||||
|
@ -36,7 +36,10 @@ module.exports = (context, pluginsPath) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
plugins[type] = async input =>
|
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;
|
return plugins;
|
||||||
},
|
},
|
||||||
|
15
lib/utils.js
15
lib/utils.js
@ -1,7 +1,20 @@
|
|||||||
const {isFunction} = require('lodash');
|
const {isFunction} = require('lodash');
|
||||||
|
const hideSensitive = require('./hide-sensitive');
|
||||||
|
|
||||||
function extractErrors(err) {
|
function extractErrors(err) {
|
||||||
return err && isFunction(err[Symbol.iterator]) ? [...err] : [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};
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import test from 'ava';
|
import test from 'ava';
|
||||||
import plugins from '../../lib/definitions/plugins';
|
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 => {
|
test('The "analyzeCommits" plugin output must be either undefined or a valid semver release type', t => {
|
||||||
t.false(plugins.analyzeCommits.outputValidator('invalid'));
|
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(''));
|
t.true(plugins.publish.outputValidator(''));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('The "generateNotes" plugins output are concatenated with separator', t => {
|
test('The "generateNotes" plugins output are concatenated with separator and sensitive data is hidden', t => {
|
||||||
t.is(plugins.generateNotes.postprocess(['note 1', 'note 2']), `note 1${RELEASE_NOTES_SEPARATOR}note 2`);
|
const env = {MY_TOKEN: 'secret token'};
|
||||||
t.is(plugins.generateNotes.postprocess(['', 'note']), 'note');
|
t.is(plugins.generateNotes.postprocess(['note 1', 'note 2'], {env}), `note 1${RELEASE_NOTES_SEPARATOR}note 2`);
|
||||||
t.is(plugins.generateNotes.postprocess([undefined, 'note']), 'note');
|
t.is(plugins.generateNotes.postprocess(['', 'note'], {env}), 'note');
|
||||||
t.is(plugins.generateNotes.postprocess(['note 1', '', 'note 2']), `note 1${RELEASE_NOTES_SEPARATOR}note 2`);
|
t.is(plugins.generateNotes.postprocess([undefined, 'note'], {env}), 'note');
|
||||||
t.is(plugins.generateNotes.postprocess(['note 1', undefined, 'note 2']), `note 1${RELEASE_NOTES_SEPARATOR}note 2`);
|
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}`
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import test from 'ava';
|
import test from 'ava';
|
||||||
|
import {escapeRegExp, isString} from 'lodash';
|
||||||
import proxyquire from 'proxyquire';
|
import proxyquire from 'proxyquire';
|
||||||
import {spy, stub} from 'sinon';
|
import {spy, stub} from 'sinon';
|
||||||
import {WritableStreamBuffer} from 'stream-buffers';
|
import {WritableStreamBuffer} from 'stream-buffers';
|
||||||
import AggregateError from 'aggregate-error';
|
import AggregateError from 'aggregate-error';
|
||||||
import SemanticReleaseError from '@semantic-release/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 {
|
import {
|
||||||
gitHead as getGitHead,
|
gitHead as getGitHead,
|
||||||
gitTagHead,
|
gitTagHead,
|
||||||
@ -1031,6 +1032,87 @@ test('Throw an Error if plugin returns an unexpected value', async t => {
|
|||||||
t.regex(error.details, /string/);
|
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 => {
|
test('Get all commits including the ones not in the shallow clone', async t => {
|
||||||
let {cwd, repositoryUrl} = await gitRepo(true);
|
let {cwd, repositoryUrl} = await gitRepo(true);
|
||||||
await gitTagVersion('v1.0.0', undefined, {cwd});
|
await gitTagVersion('v1.0.0', undefined, {cwd});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user