Skip to content

Commit

Permalink
fix(addNewMask): fail by default
Browse files Browse the repository at this point in the history
  • Loading branch information
masontikhonov committed Jul 4, 2024
1 parent 166c210 commit 693a884
Show file tree
Hide file tree
Showing 2 changed files with 185 additions and 62 deletions.
39 changes: 31 additions & 8 deletions lib/addNewMask.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
const { getServerAddress } = require('./helpers');

const exitCodes = {
success: 0,
error: 1,
missingArguments: 2,
unexpectedSuccess: 3,
};

/**
* Unexpected exit with code 0 can lead to the leakage of secrets in the build logs.
* The exit should never be successful unless the secret was successfully masked.
*/
let exitWithError = true;
const exitHandler = (exitCode) => {
if ((exitCode === 0 || process.exitCode === 0) && exitWithError) {
console.warn(`Unexpected exit with code 0. Exiting with ${exitCodes.unexpectedSuccess} instead`);
process.exitCode = exitCodes.unexpectedSuccess;
}
};
process.on('exit', exitHandler);

async function updateMasks(secret) {
try {
const serverAddress = await getServerAddress();
Expand All @@ -10,29 +30,32 @@ async function updateMasks(secret) {
const { default: httpClient } = await import('got');
const response = await httpClient.post(url, {
json: secret,
throwHttpErrors: false,
});

if (response.statusCode !== 201) {
console.error(`could not create mask for secret: ${secret.key}, because server responded with: ${response.statusCode}\n\n${JSON.stringify(response.body)}`);
process.exit(1);
if (response.statusCode === 201) {
console.log(`successfully updated masks with secret: ${secret.key}`);
exitWithError = false;
process.exit(exitCodes.success);
} else {
console.error(`could not create mask for secret: ${secret.key}. Server responded with: ${response.statusCode}\n\n${response.body}`);
process.exit(exitCodes.error);
}
console.log(`successfully updated masks with secret: ${secret.key}`);
process.exit(0);
} catch (error) {
console.error(`could not create mask for secret: ${secret.key}. Error: ${error}`);
process.exit(1);
process.exit(exitCodes.error);
}
}

if (require.main === module) {
// first argument is the secret key second argument is the secret value
if (process.argv.length < 4) {
console.log('not enough arguments, need secret key and secret value');
process.exit(2);
process.exit(exitCodes.missingArguments);
}
const key = process.argv[2];
const value = process.argv[3];
updateMasks({ key, value });
} else {
module.exports = updateMasks;
module.exports = { updateMasks, exitHandler };
}
208 changes: 154 additions & 54 deletions test/addNewMask.unit.spec.js
Original file line number Diff line number Diff line change
@@ -1,81 +1,181 @@
/* jshint ignore:start */

const timers = require('node:timers/promises');
const chai = require('chai');
const expect = chai.expect;
const sinon = require('sinon');
const sinonChai = require('sinon-chai');
const { getPromiseWithResolvers } = require('../lib/helpers');
const chai = require('chai');
const sinon = require('sinon');
const sinonChai = require('sinon-chai');
const proxyquire = require('proxyquire').noCallThru();

const expect = chai.expect;
chai.use(sinonChai);

const originalProcessExit = process.exit;

describe('addNewMask', () => {
before(() => {
process.exit = sinon.spy();
const originalProcessExit = process.exit;
const originalConsole = console;

const stubGetServerAddress = sinon.stub();
const stubProcessExit = sinon.stub();
const stubConsole = {
debug: sinon.stub(),
error: sinon.stub(),
warn: sinon.stub(),
log: sinon.stub(),
};
const stubGot = {
post: sinon.stub().resolves({ statusCode: 201 }),
};

const secret = {
key: '123',
value: 'ABC',
};

before(async () => {
process.exit = stubProcessExit;
console = stubConsole;
const { default: httpClient } = await import('got');
sinon.stub(httpClient, 'post').callsFake(stubGot.post);
});

beforeEach(() => {
process.exit.resetHistory();
stubProcessExit.resetHistory();
stubGetServerAddress.resetHistory();
for (const stub in stubConsole) {
stubConsole[stub].resetHistory();
}
for (const stub in stubGot) {
stubGot[stub].resetHistory();
}
});

after(() => {
process.exit = originalProcessExit;
console = originalConsole;
});

describe('positive', () => {
it('should send a request to add a secret', async () => {
const rpSpy = sinon.spy(async () => ({ statusCode: 201 }));
const deferredAddress = getPromiseWithResolvers();
const addNewMask = proxyquire('../lib/addNewMask', {
'request-promise': rpSpy,
'./logger': {
secretsServerAddress: deferredAddress.promise,
},
const serverAddress = 'https://xkcd.com/605/'
stubGetServerAddress.resolves(serverAddress);
stubGot.post.resolves({ statusCode: 201 });

const { updateMasks, exitHandler } = proxyquire('../lib/addNewMask', {
'./helpers': { getServerAddress: stubGetServerAddress },
});

const secret = {
key: '123',
value: 'ABC',
};

deferredAddress.resolve('http://127.0.0.1:1337');
addNewMask(secret);

await timers.setTimeout(10);
expect(rpSpy).to.have.been.calledOnceWith({
uri: `http://127.0.0.1:1337/secrets`,
method: 'POST',
json: true,
body: secret,
resolveWithFullResponse: true,
process.listeners('exit').forEach((listener) => {
if (listener === exitHandler) {
process.removeListener('exit', listener);
}
});
await timers.setTimeout(10);
expect(process.exit).to.have.been.calledOnceWith(0);
await updateMasks(secret);
expect(stubGot.post).to.have.been.calledOnceWith(new URL('secrets', serverAddress), {
json: secret,
throwHttpErrors: false,
});
expect(stubProcessExit).to.have.been.calledOnceWith(0);
});
});

describe('negative', () => {
it('should send a request to add a secret', async () => {
const rpSpy = sinon.spy(async () => { throw 'could not send request';});
deferredAddress = getPromiseWithResolvers();
const addNewMask = proxyquire('../lib/addNewMask', {
'request-promise': rpSpy,
'./logger': {
secretsServerAddress: deferredAddress.promise,
it('should fail if the server address is not available', async () => {
stubGetServerAddress.rejects('could not get server address');
const { updateMasks, exitHandler } = proxyquire('../lib/addNewMask', {
'./helpers': {
getServerAddress: stubGetServerAddress,
},
});

const secret = {
key: '123',
value: 'ABC',
};

deferredAddress.resolve('http://127.0.0.1:1337');
addNewMask(secret);
await timers.setTimeout(10);
expect(process.exit).to.have.been.calledOnceWith(1);
process.listeners('exit').forEach((listener) => {
if (listener === exitHandler) {
process.removeListener('exit', listener);
}
});
await updateMasks(secret);
expect(stubConsole.error).to.have.been.calledOnceWith('could not create mask for secret: 123. Error: could not get server address');
expect(stubProcessExit).to.have.been.calledOnceWith(1);
});

it('should fail if the server address is not valid URL', async () => {
stubGetServerAddress.resolves('foo');
const { updateMasks, exitHandler } = proxyquire('../lib/addNewMask', {
'./helpers': {
getServerAddress: stubGetServerAddress,
},
});
process.listeners('exit').forEach((listener) => {
if (listener === exitHandler) {
process.removeListener('exit', listener);
}
});
await updateMasks(secret);
expect(stubConsole.error).to.have.been.calledOnceWith('could not create mask for secret: 123. Error: TypeError: Invalid URL');
expect(stubProcessExit).to.have.been.calledOnceWith(1);
});

it('should fail if server responded not with 201', async () => {
const serverAddress = 'https://g.codefresh.io'
stubGetServerAddress.resolves(serverAddress);
stubGot.post.resolves({
statusCode: 500,
body: 'Internal Server Error',
});
const { updateMasks, exitHandler } = proxyquire('../lib/addNewMask', {
'./helpers': { getServerAddress: stubGetServerAddress },
});
process.listeners('exit').forEach((listener) => {
if (listener === exitHandler) {
process.removeListener('exit', listener);
}
});
await updateMasks(secret);
expect(stubConsole.error).to.have.been.calledOnceWith('could not create mask for secret: 123. Server responded with: 500\n\nInternal Server Error');
expect(stubProcessExit).to.have.been.calledOnceWith(1);
});
});

describe('exitHandler', () => {
it('should set exit code to 3 if the original exit code is 0 and variable was not masked', () => {
const { exitHandler } = proxyquire('../lib/addNewMask', {});
process.listeners('exit').forEach((listener) => {
if (listener === exitHandler) {
process.removeListener('exit', listener);
}
});
exitHandler(0);
expect(process.exitCode).to.be.equal(3);
expect(stubConsole.warn).to.have.been.calledOnceWith('Unexpected exit with code 0. Exiting with 3 instead');
process.exitCode = undefined;
});

it('should set exit code to 3 if the original exit code is 0 and variable was not masked', () => {
const { exitHandler } = proxyquire('../lib/addNewMask', {});
process.listeners('exit').forEach((listener) => {
if (listener === exitHandler) {
process.removeListener('exit', listener);
}
});
if (process.exitCode !== undefined) {
throw new Error('process.exitCode should be undefined to run this test');
}
process.exitCode = 0;
exitHandler();
expect(process.exitCode).to.be.equal(3);
expect(stubConsole.warn).to.have.been.calledOnceWith('Unexpected exit with code 0. Exiting with 3 instead');
process.exitCode = 0;
});

it('should not change exit code if the variable was masked successfully', async () => {
const serverAddress = 'https://xkcd.com/605/'
stubGetServerAddress.resolves(serverAddress);
stubGot.post.resolves({ statusCode: 201 });
const { updateMasks, exitHandler } = proxyquire('../lib/addNewMask', {
'./helpers': { getServerAddress: stubGetServerAddress },
});
process.listeners('exit').forEach((listener) => {
if (listener === exitHandler) {
process.removeListener('exit', listener);
}
});
await updateMasks(secret);
expect(process.exitCode).not.to.be.equal(3);
expect(stubConsole.warn).not.to.have.been.calledOnceWith('Unexpected exit with code 0. Exiting with 3 instead');
});
});
});

0 comments on commit 693a884

Please sign in to comment.