Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion components/framework/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
const YAML = require('js-yaml');
const path = require('path');
const spawn = require('../../src/utils/spawn');
const redactArgs = require('../../src/utils/redact-args');
const semver = require('semver');
const calculateCacheHash = require('../../src/utils/cache-hash');
const { configSchema } = require('./configuration');
Expand Down Expand Up @@ -267,7 +268,7 @@ class ServerlessFramework {
args.push('--region', this.inputs.region);
}

this.context.logVerbose(`Running "${command} ${args.join(' ')}"`);
this.context.logVerbose(`Running "${command} ${redactArgs(args).join(' ')}"`);
return new Promise((resolve, reject) => {
const subprocess = spawn(command, args, {
cwd: this.inputs.path,
Expand Down
39 changes: 39 additions & 0 deletions src/utils/redact-args.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
'use strict';

const sensitiveOptionNamePattern =
/(?:^|[-_])(?:auth|authorization|credential|password|passwd|pwd|secret|token|api[-_]?key|access[-_]?key)(?:$|[-_])/i;

const redactArgs = (args) => {
const redactedArgs = [];
let redactNext = false;

for (const arg of args) {
const value = String(arg);

if (redactNext) {
redactedArgs.push('<redacted>');
redactNext = false;
continue;
}

const equalsIndex = value.indexOf('=');
const optionName = value.replace(/^-+/, '').split('=')[0];

if (equalsIndex !== -1 && sensitiveOptionNamePattern.test(optionName)) {
redactedArgs.push(`${value.slice(0, equalsIndex + 1)}<redacted>`);
continue;
}

if (value.startsWith('-') && sensitiveOptionNamePattern.test(optionName)) {
redactedArgs.push(value);
redactNext = true;
continue;
}

redactedArgs.push(value);
}

return redactedArgs;
};

module.exports = redactArgs;
37 changes: 1 addition & 36 deletions src/utils/spawn.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@

const spawn = require('cross-spawn');
const { PassThrough } = require('stream');

const sensitiveOptionNamePattern =
/(?:^|[-_])(?:auth|authorization|credential|password|passwd|pwd|secret|token|api[-_]?key|access[-_]?key)(?:$|[-_])/i;
const redactArgs = require('./redact-args');

const toBuffer = (chunk) => (Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));

Expand Down Expand Up @@ -34,39 +32,6 @@ const getBuffer = (state) => {
return state.buffer;
};

const redactArgs = (args) => {
const redactedArgs = [];
let redactNext = false;

for (const arg of args) {
const value = String(arg);

if (redactNext) {
redactedArgs.push('<redacted>');
redactNext = false;
continue;
}

const equalsIndex = value.indexOf('=');
const optionName = value.replace(/^-+/, '').split('=')[0];

if (equalsIndex !== -1 && sensitiveOptionNamePattern.test(optionName)) {
redactedArgs.push(`${value.slice(0, equalsIndex + 1)}<redacted>`);
continue;
}

if (value.startsWith('-') && sensitiveOptionNamePattern.test(optionName)) {
redactedArgs.push(value);
redactNext = true;
continue;
}

redactedArgs.push(value);
}

return redactedArgs;
};

module.exports = (command, args = [], options = {}) => {
const normalizedCommand = String(command);
const normalizedArgs = args == null ? [] : Array.from(args, String);
Expand Down
27 changes: 27 additions & 0 deletions test/unit/components/framework/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,33 @@ describe('test/unit/components/framework/index.test.js', () => {
expect(context.outputs).to.deep.equal({ Key: 'Output' });
});

it('redacts sensitive parameter values in verbose command logging', async () => {
const spawnStub = createSpawnStub(createClassicSpawnResult({ stdout: INFO_OUTPUT }));
const FrameworkComponent = loadFrameworkComponent(spawnStub);

const context = await getContext();
const logVerbose = sinon.spy(context, 'logVerbose');
const component = new FrameworkComponent('some-id', context, {
path: 'path',
params: { 'api-token': 'secret-value-123' },
});
context.state.detectedFrameworkVersion = '9.9.9';
await component.deploy();

expectSpawnCall(spawnStub, 0, [
'deploy',
'--stage',
'dev',
'--param',
'api-token=secret-value-123',
]);
const loggedCommands = logVerbose.args.map(([message]) => message);
expect(loggedCommands.some((message) => message.includes('api-token=<redacted>'))).to.equal(
true
);
expect(loggedCommands.some((message) => message.includes('secret-value-123'))).to.equal(false);
});

it('supports the shared spawn helper promise shape when executing osls commands', async () => {
const spawnStub = createSpawnStub(
createSpawnExecution({
Expand Down
38 changes: 38 additions & 0 deletions test/unit/src/utils/redact-args.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
'use strict';

const chai = require('chai');

const redactArgs = require('../../../../src/utils/redact-args');

const expect = chai.expect;

describe('test/unit/src/utils/redact-args.test.js', () => {
it('redacts values of sensitive equals-form arguments', () => {
expect(redactArgs(['--token=abc', 'api-key=xyz', '--param', 'secret-token=abc'])).to.deep.equal(
['--token=<redacted>', 'api-key=<redacted>', '--param', 'secret-token=<redacted>']
);
});

it('redacts the following value of sensitive flag arguments', () => {
expect(redactArgs(['--password', 'hunter2', '--stage', 'dev'])).to.deep.equal([
'--password',
'<redacted>',
'--stage',
'dev',
]);
});

it('passes through non-sensitive arguments unchanged', () => {
expect(redactArgs(['deploy', '--stage', 'dev', '--param', 'tableName=users'])).to.deep.equal([
'deploy',
'--stage',
'dev',
'--param',
'tableName=users',
]);
});

it('stringifies non-string arguments', () => {
expect(redactArgs(['--verbose', 1, true])).to.deep.equal(['--verbose', '1', 'true']);
});
});
Loading