From ae8016eaabfd885874ebc6bde004247f67530498 Mon Sep 17 00:00:00 2001 From: Tom Proctor Date: Fri, 30 Apr 2021 16:23:05 +0100 Subject: [PATCH] Mask each line of multi-line secrets --- dist/index.js | 167 ++++++++++++++++++++++++--------------------- src/action.js | 8 ++- src/action.test.js | 37 ++++++++++ 3 files changed, 131 insertions(+), 81 deletions(-) diff --git a/dist/index.js b/dist/index.js index 5003a33..b0b7403 100644 --- a/dist/index.js +++ b/dist/index.js @@ -216,9 +216,70 @@ function wrappy (fn, cb) { /***/ }), /***/ 16: -/***/ (function(module) { +/***/ (function(module, __unusedexports, __webpack_require__) { + +"use strict"; + +const {constants: BufferConstants} = __webpack_require__(293); +const pump = __webpack_require__(453); +const bufferStream = __webpack_require__(375); + +class MaxBufferError extends Error { + constructor() { + super('maxBuffer exceeded'); + this.name = 'MaxBufferError'; + } +} + +async function getStream(inputStream, options) { + if (!inputStream) { + return Promise.reject(new Error('Expected a stream')); + } + + options = { + maxBuffer: Infinity, + ...options + }; + + const {maxBuffer} = options; + + let stream; + await new Promise((resolve, reject) => { + const rejectPromise = error => { + // Don't retrieve an oversized buffer. + if (error && stream.getBufferedLength() <= BufferConstants.MAX_LENGTH) { + error.bufferedData = stream.getBufferedValue(); + } + + reject(error); + }; + + stream = pump(inputStream, bufferStream(options), error => { + if (error) { + rejectPromise(error); + return; + } + + resolve(); + }); + + stream.on('data', () => { + if (stream.getBufferedLength() > maxBuffer) { + rejectPromise(new MaxBufferError()); + } + }); + }); + + return stream.getBufferedValue(); +} + +module.exports = getStream; +// TODO: Remove this for the next major release +module.exports.default = getStream; +module.exports.buffer = (stream, options) => getStream(stream, {...options, encoding: 'buffer'}); +module.exports.array = (stream, options) => getStream(stream, {...options, array: true}); +module.exports.MaxBufferError = MaxBufferError; -module.exports = require("tls"); /***/ }), @@ -10210,7 +10271,7 @@ module.exports = options => { const EventEmitter = __webpack_require__(614); const urlLib = __webpack_require__(835); const normalizeUrl = __webpack_require__(53); -const getStream = __webpack_require__(997); +const getStream = __webpack_require__(16); const CachePolicy = __webpack_require__(154); const Response = __webpack_require__(93); const lowercaseKeys = __webpack_require__(474); @@ -11227,7 +11288,7 @@ module.exports = url => { "use strict"; -const tls = __webpack_require__(16); +const tls = __webpack_require__(818); module.exports = (options = {}) => new Promise((resolve, reject) => { const socket = tls.connect(options, () => { @@ -12893,12 +12954,14 @@ exports.default = async (body, headers) => { /***/ }), /***/ 790: -/***/ (function(module, exports, __webpack_require__) { +/***/ (function(module, exports) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const tls_1 = __webpack_require__(16); +function isTLSSocket(socket) { + return socket.encrypted; +} const deferToConnect = (socket, fn) => { let listeners; if (typeof fn === 'function') { @@ -12915,7 +12978,7 @@ const deferToConnect = (socket, fn) => { if (hasConnectListener) { listeners.connect(); } - if (socket instanceof tls_1.TLSSocket && hasSecureConnectListener) { + if (isTLSSocket(socket) && hasSecureConnectListener) { if (socket.authorized) { listeners.secureConnect(); } @@ -13072,6 +13135,13 @@ exports.default = (request, delays, options) => { }; +/***/ }), + +/***/ 818: +/***/ (function(module) { + +module.exports = require("tls"); + /***/ }), /***/ 835: @@ -13170,7 +13240,7 @@ module.exports = require("dns"); "use strict"; const EventEmitter = __webpack_require__(614); -const tls = __webpack_require__(16); +const tls = __webpack_require__(818); const http2 = __webpack_require__(565); const QuickLRU = __webpack_require__(904); @@ -14154,8 +14224,12 @@ async function exportSecrets() { const { value, request, cachedResponse } = result; if (cachedResponse) { core.debug('ℹ using cached response'); - } - command.issue('add-mask', value); + } + for (const line of value.split('\n')) { + if (line.length > 0) { + command.issue('add-mask', line); + } + } if (exportEnv) { core.exportVariable(request.envVarName, `${value}`); } @@ -14317,7 +14391,7 @@ const is_response_ok_1 = __webpack_require__(422); const deprecation_warning_1 = __webpack_require__(189); const normalize_arguments_1 = __webpack_require__(992); const calculate_retry_delay_1 = __webpack_require__(594); -const globalDnsCache = new cacheable_lookup_1.default(); +let globalDnsCache; const kRequest = Symbol('request'); const kResponse = Symbol('response'); const kResponseSize = Symbol('responseSize'); @@ -14874,6 +14948,9 @@ class Request extends stream_1.Duplex { options.cacheOptions = { ...options.cacheOptions }; // `options.dnsCache` if (options.dnsCache === true) { + if (!globalDnsCache) { + globalDnsCache = new cacheable_lookup_1.default(); + } options.dnsCache = globalDnsCache; } else if (!is_1.default.undefined(options.dnsCache) && !options.dnsCache.lookup) { @@ -16019,74 +16096,6 @@ const normalizeArguments = (options, defaults) => { exports.default = normalizeArguments; -/***/ }), - -/***/ 997: -/***/ (function(module, __unusedexports, __webpack_require__) { - -"use strict"; - -const {constants: BufferConstants} = __webpack_require__(293); -const pump = __webpack_require__(453); -const bufferStream = __webpack_require__(375); - -class MaxBufferError extends Error { - constructor() { - super('maxBuffer exceeded'); - this.name = 'MaxBufferError'; - } -} - -async function getStream(inputStream, options) { - if (!inputStream) { - return Promise.reject(new Error('Expected a stream')); - } - - options = { - maxBuffer: Infinity, - ...options - }; - - const {maxBuffer} = options; - - let stream; - await new Promise((resolve, reject) => { - const rejectPromise = error => { - // Don't retrieve an oversized buffer. - if (error && stream.getBufferedLength() <= BufferConstants.MAX_LENGTH) { - error.bufferedData = stream.getBufferedValue(); - } - - reject(error); - }; - - stream = pump(inputStream, bufferStream(options), error => { - if (error) { - rejectPromise(error); - return; - } - - resolve(); - }); - - stream.on('data', () => { - if (stream.getBufferedLength() > maxBuffer) { - rejectPromise(new MaxBufferError()); - } - }); - }); - - return stream.getBufferedValue(); -} - -module.exports = getStream; -// TODO: Remove this for the next major release -module.exports.default = getStream; -module.exports.buffer = (stream, options) => getStream(stream, {...options, encoding: 'buffer'}); -module.exports.array = (stream, options) => getStream(stream, {...options, array: true}); -module.exports.MaxBufferError = MaxBufferError; - - /***/ }) /******/ }); \ No newline at end of file diff --git a/src/action.js b/src/action.js index 7a3c072..c16436e 100644 --- a/src/action.js +++ b/src/action.js @@ -77,8 +77,12 @@ async function exportSecrets() { const { value, request, cachedResponse } = result; if (cachedResponse) { core.debug('ℹ using cached response'); - } - command.issue('add-mask', value); + } + for (const line of value.split('\n')) { + if (line.length > 0) { + command.issue('add-mask', line); + } + } if (exportEnv) { core.exportVariable(request.envVarName, `${value}`); } diff --git a/src/action.test.js b/src/action.test.js index cd7828e..1837eaf 100644 --- a/src/action.test.js +++ b/src/action.test.js @@ -2,6 +2,7 @@ jest.mock('got'); jest.mock('@actions/core'); jest.mock('@actions/core/lib/command'); +const command = require('@actions/core/lib/command'); const core = require('@actions/core'); const got = require('got'); const { @@ -294,4 +295,40 @@ describe('exportSecrets', () => { expect(core.exportVariable).toBeCalledWith('KEY', '1'); expect(core.setOutput).toBeCalledWith('key', '1'); }); + + it('single-line secret gets masked', async () => { + mockInput('test key'); + mockVaultData({ + key: 'secret' + }); + mockExportToken("false") + + await exportSecrets(); + + expect(command.issue).toBeCalledTimes(1); + + expect(command.issue).toBeCalledWith('add-mask', 'secret'); + expect(core.setOutput).toBeCalledWith('key', 'secret'); + }) + + it('multi-line secret gets masked for each line', async () => { + const multiLineString = `a multi-line string + +with blank lines + +` + mockInput('test key'); + mockVaultData({ + key: multiLineString + }); + mockExportToken("false") + + await exportSecrets(); + + expect(command.issue).toBeCalledTimes(2); // 1 for each non-empty line. + + expect(command.issue).toBeCalledWith('add-mask', 'a multi-line string'); + expect(command.issue).toBeCalledWith('add-mask', 'with blank lines'); + expect(core.setOutput).toBeCalledWith('key', multiLineString); + }) });