diff --git a/components/framework/index.js b/components/framework/index.js index 12953cc..91d2a38 100644 --- a/components/framework/index.js +++ b/components/framework/index.js @@ -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'); @@ -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, diff --git a/src/utils/redact-args.js b/src/utils/redact-args.js new file mode 100644 index 0000000..7c59f8d --- /dev/null +++ b/src/utils/redact-args.js @@ -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(''); + 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)}`); + continue; + } + + if (value.startsWith('-') && sensitiveOptionNamePattern.test(optionName)) { + redactedArgs.push(value); + redactNext = true; + continue; + } + + redactedArgs.push(value); + } + + return redactedArgs; +}; + +module.exports = redactArgs; diff --git a/src/utils/spawn.js b/src/utils/spawn.js index 9fe5870..af0d44a 100644 --- a/src/utils/spawn.js +++ b/src/utils/spawn.js @@ -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)); @@ -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(''); - 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)}`); - 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); diff --git a/test/unit/components/framework/index.test.js b/test/unit/components/framework/index.test.js index f905004..3fb00da 100644 --- a/test/unit/components/framework/index.test.js +++ b/test/unit/components/framework/index.test.js @@ -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='))).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({ diff --git a/test/unit/src/utils/redact-args.test.js b/test/unit/src/utils/redact-args.test.js new file mode 100644 index 0000000..55ace43 --- /dev/null +++ b/test/unit/src/utils/redact-args.test.js @@ -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=', 'api-key=', '--param', 'secret-token='] + ); + }); + + it('redacts the following value of sensitive flag arguments', () => { + expect(redactArgs(['--password', 'hunter2', '--stage', 'dev'])).to.deep.equal([ + '--password', + '', + '--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']); + }); +});