diff --git a/nodes/PayloadValidator.js b/nodes/PayloadValidator.js index 273ee1d40..ef90079b2 100644 --- a/nodes/PayloadValidator.js +++ b/nodes/PayloadValidator.js @@ -1,4 +1,5 @@ const clone = require('clone'); +const _ = require('lodash'); const variablesToCheck = [ 'logger.metadata.organization', @@ -41,40 +42,86 @@ module.exports = class PayloadValidator { } } + /** + * Gets the value at the given location in the given object + * + * @param {*} object + * @param {*} location + * @returns + */ getValue(object, location) { - return location.split('.').reduce((p, c) => (p && p[c]) || null, object); + return _.get(object, location); } - verify(_after) { - if (this.isValidBefore) { - try { - let after = _after; - if (Array.isArray(after)) { - after = after.find((msg) => !!msg); + /** + * Sets the value at the given location in the given object to the given value + * + * @param {*} object + * @param {*} location + * @param {*} value + * @returns + */ + setValue(object, location, value) { + return _.set(object, location, value); + } + + /** + * Log that we had a invalid payload modification + * @param {*} beforeValue + * @param {*} afterValue + * @param {*} location + */ + logException(beforeValue, afterValue, location) { + const details = { + message: `msg.${location} changed from "${beforeValue}" to "${afterValue}" for bot "${this.bot}"`, + nodeId: this.nodeId, + workerId: this.workerId + }; + this.logger.error(details.message); + this.logger.app.platform.organization({ + srn: `srn:botnet:${this.region}:${this.organization}:bot:${this.bot}`, + action: 'exception', + actionType: 'invalid-payload-modification', + details, + conversationId: this.conversationId + }); + } + + /** + * Ensures that the organization in the before matches the org in the after + * If they are different then it attempts to set the organization back to the correct one + * @param {*} after + * @returns + */ + ensureOrganizationNotModified(after) { + variablesToCheck.forEach((location) => { + const beforeValue = this.getValue(this.before, location); + const afterValue = this.getValue(after, location); + if (beforeValue !== afterValue) { + this.logException(beforeValue, afterValue, location); + + // attempt to set the value back to its correct one + after = this.setValue(after, location, beforeValue); + + if (!_.has(after, location, beforeValue)) { + this.logger.error(`Cant set value as ${location} is no longer assessable in after`); } - variablesToCheck.forEach((location) => { - if (this.getValue(this.before, location) !== this.getValue(after, location)) { - const details = { - message: `msg.${location} changed from "${this.getValue(this.before, location)}" to "${this.getValue(after, location)}" for bot "${this.bot}"`, - nodeId: this.nodeId, - workerId: this.workerId - }; - this.logger.error(details.message); - this.logger.app.platform.organization({ - srn: `srn:botnet:${this.region}:${this.organization}:bot:${this.bot}`, - action: 'exception', - actionType: 'invalid-payload-modification', - details, - conversationId: this.conversationId - }); - } - }); - } catch (e) { - console.log('Error while trying to verify variable changes'); - console.log(e); } - } else { - console.log('Error while trying to verify variable changes, wasn\'t initted with correct object'); + }); + + return after; + } + + verify(after) { + if (this.isValidBefore) { + if (Array.isArray(after)) { + const afterIndex = after.findIndex(msg => !!msg); + after[afterIndex] = this.ensureOrganizationNotModified(after[afterIndex]); + return after; + } + return this.ensureOrganizationNotModified(after); } + console.log('Error while trying to verify variable changes, wasn\'t initted with correct object'); + return after; } }; diff --git a/package-lock.json b/package-lock.json index 7ef7a2c12..55a2c917f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5104,8 +5104,7 @@ "lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "lodash.assign": { "version": "4.2.0", diff --git a/package.json b/package.json index bb09a0a71..145f9f1d0 100644 --- a/package.json +++ b/package.json @@ -11,9 +11,8 @@ "main": "red/red.js", "scripts": { "start": "node red.js", - "test": "grunt test-nodes", "build": "grunt build", - "test:unit": "nyc node_modules/.bin/mocha --recursive test/unit/*" + "test": "nyc node_modules/.bin/mocha --recursive test/unit/*" }, "bin": { "node-red": "./red.js", @@ -54,6 +53,7 @@ "js-yaml": "3.11.0", "json-stringify-safe": "5.0.1", "jsonata": "1.5.4", + "lodash": "^4.17.21", "media-typer": "0.3.0", "memorystore": "1.6.0", "mqtt": "2.18.0", diff --git a/test/unit/test-payloadValidator.js b/test/unit/test-payloadValidator.js index 36d7e3f6d..7c8334e06 100644 --- a/test/unit/test-payloadValidator.js +++ b/test/unit/test-payloadValidator.js @@ -1,149 +1,219 @@ -const PayloadValidator = require('../../nodes/PayloadValidator') -const orgEvent = require('./fixtures/data/orgEvent') const sinon = require('sinon'); const assert = require('assert'); +const PayloadValidator = require('../../nodes/PayloadValidator'); +const orgEvent = require('./fixtures/data/orgEvent'); -describe.only('Unit: PayloadValidator', () => { +describe('Unit: PayloadValidator', () => { it('Should not log when no changes', () => { - const nodeId = 'abc-id' - const workerId = 'worker-id' + const nodeId = 'abc-id'; + const workerId = 'worker-id'; const beforeEvent = orgEvent('before', workerId); const payloadValidator = new PayloadValidator(beforeEvent, `before-${workerId}-${nodeId}`); payloadValidator.verify(beforeEvent); }); - it('Should warn when org is overwritten', () => { - const nodeId = 'abc-id' - const workerId = 'worker-id' + it('Should warn when org is overwritten and fix it', () => { + const nodeId = 'abc-id'; + const workerId = 'worker-id'; const beforeEvent = orgEvent('before', workerId); - errorLogStub = sinon.stub(); - appLogStub = sinon.stub(); + const errorLogStub = sinon.stub(); + const appLogStub = sinon.stub(); beforeEvent.logger.error = errorLogStub; beforeEvent.logger.app = { - platform:{ + platform: { organization: appLogStub } }; - + const payloadValidator = new PayloadValidator(beforeEvent, `before-${workerId}-${nodeId}`); - const modifiedEvent = orgEvent('after'); + const result = payloadValidator.verify(modifiedEvent); - payloadValidator.verify(modifiedEvent); - assert(errorLogStub.callCount === 4) - assert(appLogStub.callCount === 4) - const [[log1], [log2], [log3], [log4]] = appLogStub.args - assert(log1.details.message.includes('logger.metadata.organization')) - assert.strictEqual(log1.details.nodeId, nodeId) - assert.strictEqual(log1.details.workerId, workerId) - assert(log2.details.message.includes('payload.system.organization')) - assert.strictEqual(log2.details.nodeId, nodeId) - assert.strictEqual(log2.details.workerId, workerId) - assert(log3.details.message.includes('event.event.organization')) - assert.strictEqual(log3.details.nodeId, nodeId) - assert.strictEqual(log3.details.workerId, workerId) - assert(log4.details.message.includes('event.event.token.contents.organization')) - assert.strictEqual(log4.details.nodeId, nodeId) - assert.strictEqual(log4.details.workerId, workerId) + assert(errorLogStub.callCount === 4); + assert(appLogStub.callCount === 4); + const [[log1], [log2], [log3], [log4]] = appLogStub.args; + assert(log1.details.message.includes('logger.metadata.organization')); + assert.strictEqual(log1.details.nodeId, nodeId); + assert.strictEqual(log1.details.workerId, workerId); + assert(log2.details.message.includes('payload.system.organization')); + assert.strictEqual(log2.details.nodeId, nodeId); + assert.strictEqual(log2.details.workerId, workerId); + assert(log3.details.message.includes('event.event.organization')); + assert.strictEqual(log3.details.nodeId, nodeId); + assert.strictEqual(log3.details.workerId, workerId); + assert(log4.details.message.includes('event.event.token.contents.organization')); + assert.strictEqual(log4.details.nodeId, nodeId); + assert.strictEqual(log4.details.workerId, workerId); + // deleting due to sinon funness + delete result.logger.error; + delete result.logger.app; + delete beforeEvent.logger.error; + delete beforeEvent.logger.app; + assert.deepStrictEqual(result, beforeEvent); }); - - it('Should warn when org is deleted', () => { - const nodeId = 'abc-id' - const workerId = 'worker-id' + it('Should warn when org is overwritten and fail to fix it due to overwriting payload', () => { + const nodeId = 'abc-id'; + const workerId = 'worker-id'; const beforeEvent = orgEvent('before', workerId); - errorLogStub = sinon.stub(); - appLogStub = sinon.stub(); + const errorLogStub = sinon.stub(); + const appLogStub = sinon.stub(); beforeEvent.logger.error = errorLogStub; beforeEvent.logger.app = { - platform:{ + platform: { organization: appLogStub } }; const payloadValidator = new PayloadValidator(beforeEvent, `before-${workerId}-${nodeId}`); + const modifiedEvent = orgEvent('before'); + modifiedEvent.payload = 'some text'; + const result = payloadValidator.verify(modifiedEvent); + assert(errorLogStub.callCount === 1); + assert(appLogStub.callCount === 1); + const [[log]] = appLogStub.args; + assert(log.details.message.includes('msg.payload.system.organization')); + assert.strictEqual(log.details.nodeId, nodeId); + assert.strictEqual(log.details.workerId, workerId); + // deleting due to sinon funness + delete result.logger.error; + delete result.logger.app; + delete modifiedEvent.logger.error; + delete modifiedEvent.logger.app; + assert.deepStrictEqual(result, modifiedEvent); + }); + it('Should warn when org is deleted and fix it', () => { + const nodeId = 'abc-id'; + const workerId = 'worker-id'; + const beforeEvent = orgEvent('before', workerId); + const errorLogStub = sinon.stub(); + const appLogStub = sinon.stub(); + beforeEvent.logger.error = errorLogStub; + beforeEvent.logger.app = { + platform: { + organization: appLogStub + } + }; + + const payloadValidator = new PayloadValidator(beforeEvent, `before-${workerId}-${nodeId}`); delete beforeEvent.logger.metadata.organization; delete beforeEvent.payload.system.organization; delete beforeEvent.event.event.organization; delete beforeEvent.event.event.token.contents.organization; - payloadValidator.verify(beforeEvent); - assert(errorLogStub.callCount === 4) - assert(appLogStub.callCount === 4) - const [[log1], [log2], [log3], [log4]] = appLogStub.args - assert(log1.details.message.includes('logger.metadata.organization')) - assert.strictEqual(log1.details.nodeId, nodeId) - assert.strictEqual(log1.details.workerId, workerId) - assert(log2.details.message.includes('payload.system.organization')) - assert.strictEqual(log2.details.nodeId, nodeId) - assert.strictEqual(log2.details.workerId, workerId) - assert(log3.details.message.includes('event.event.organization')) - assert.strictEqual(log3.details.nodeId, nodeId) - assert.strictEqual(log3.details.workerId, workerId) - assert(log4.details.message.includes('event.event.token.contents.organization')) - assert.strictEqual(log4.details.nodeId, nodeId) - assert.strictEqual(log4.details.workerId, workerId) + const result = payloadValidator.verify(beforeEvent); + assert(errorLogStub.callCount === 4); + assert(appLogStub.callCount === 4); + const [[log1], [log2], [log3], [log4]] = appLogStub.args; + assert(log1.details.message.includes('logger.metadata.organization')); + assert.strictEqual(log1.details.nodeId, nodeId); + assert.strictEqual(log1.details.workerId, workerId); + assert(log2.details.message.includes('payload.system.organization')); + assert.strictEqual(log2.details.nodeId, nodeId); + assert.strictEqual(log2.details.workerId, workerId); + assert(log3.details.message.includes('event.event.organization')); + assert.strictEqual(log3.details.nodeId, nodeId); + assert.strictEqual(log3.details.workerId, workerId); + assert(log4.details.message.includes('event.event.token.contents.organization')); + assert.strictEqual(log4.details.nodeId, nodeId); + assert.strictEqual(log4.details.workerId, workerId); + + // deleting due to sinon funness + delete result.logger.error; + delete result.logger.app; + delete beforeEvent.logger.error; + delete beforeEvent.logger.app; + assert.deepStrictEqual(result, beforeEvent); }); - it('Should handle when after is an array', () =>{ - const nodeId = 'abc-id' - const workerId = 'worker-id' + + it('Should handle when after is an array and not find any issues', () => { + const nodeId = 'abc-id'; + const workerId = 'worker-id'; const beforeEvent = orgEvent('before', workerId); - errorLogStub = sinon.stub(); - appLogStub = sinon.stub(); + const errorLogStub = sinon.stub(); + const appLogStub = sinon.stub(); beforeEvent.logger.error = errorLogStub; beforeEvent.logger.app = { - platform:{ + platform: { organization: appLogStub } }; - + const payloadValidator = new PayloadValidator(beforeEvent, `before-${workerId}-${nodeId}`); - const modifiedEvent = orgEvent('before'); - modifiedEvent.logger.metadata.organization = 'after' - payloadValidator.verify([modifiedEvent]); - assert(errorLogStub.callCount === 1) - assert(appLogStub.callCount === 1) - const [[log1]] = appLogStub.args - assert(log1.details.message.includes('logger.metadata.organization')) - assert.strictEqual(log1.details.nodeId, nodeId) - assert.strictEqual(log1.details.workerId, workerId) - }) + const after = orgEvent('before'); + const [result] = payloadValidator.verify([after]); + // deleting due to sinon funness + delete result.logger.error; + delete result.logger.app; + delete beforeEvent.logger.error; + delete beforeEvent.logger.app; + assert.deepStrictEqual(result, beforeEvent); + }); - it('Should handle when after is an array in something other than first position', () =>{ - const nodeId = 'abc-id' - const workerId = 'worker-id' + it('Should handle when after is an array', () => { + const nodeId = 'abc-id'; + const workerId = 'worker-id'; const beforeEvent = orgEvent('before', workerId); - errorLogStub = sinon.stub(); - appLogStub = sinon.stub(); + const errorLogStub = sinon.stub(); + const appLogStub = sinon.stub(); beforeEvent.logger.error = errorLogStub; beforeEvent.logger.app = { - platform:{ + platform: { organization: appLogStub } }; - + const payloadValidator = new PayloadValidator(beforeEvent, `before-${workerId}-${nodeId}`); const modifiedEvent = orgEvent('before'); - modifiedEvent.logger.metadata.organization = 'after' - payloadValidator.verify([null, null, modifiedEvent, null]); - assert(errorLogStub.callCount === 1) - assert(appLogStub.callCount === 1) - const [[log1]] = appLogStub.args - assert(log1.details.message.includes('logger.metadata.organization')) - assert.strictEqual(log1.details.nodeId, nodeId) - assert.strictEqual(log1.details.workerId, workerId) - }) - - it('Should not die when error', () => { - const beforeEvent = orgEvent('before'); - const payloadValidator = new PayloadValidator(beforeEvent); + modifiedEvent.logger.metadata.organization = 'after'; + const [result] = payloadValidator.verify([modifiedEvent]); + assert(errorLogStub.callCount === 1); + assert(appLogStub.callCount === 1); + const [[log1]] = appLogStub.args; + assert(log1.details.message.includes('logger.metadata.organization')); + assert.strictEqual(log1.details.nodeId, nodeId); + assert.strictEqual(log1.details.workerId, workerId); + // deleting due to sinon funness + delete result.logger.error; + delete result.logger.app; + delete beforeEvent.logger.error; + delete beforeEvent.logger.app; + assert.deepStrictEqual(result, beforeEvent); + }); - const modifiedEvent = orgEvent('after'); + it('Should handle when after is an array in something other than first position', () => { + const nodeId = 'abc-id'; + const workerId = 'worker-id'; + const beforeEvent = orgEvent('before', workerId); + const errorLogStub = sinon.stub(); + const appLogStub = sinon.stub(); + beforeEvent.logger.error = errorLogStub; + beforeEvent.logger.app = { + platform: { + organization: appLogStub + } + }; - payloadValidator.verify(modifiedEvent); + const payloadValidator = new PayloadValidator(beforeEvent, `before-${workerId}-${nodeId}`); + + const modifiedEvent = orgEvent('before'); + modifiedEvent.logger.metadata.organization = 'after'; + const [,, result] = payloadValidator.verify([null, null, modifiedEvent, null]); + assert(errorLogStub.callCount === 1); + assert(appLogStub.callCount === 1); + const [[log1]] = appLogStub.args; + assert(log1.details.message.includes('logger.metadata.organization')); + assert.strictEqual(log1.details.nodeId, nodeId); + assert.strictEqual(log1.details.workerId, workerId); + // deleting due to sinon funness + delete result.logger.error; + delete result.logger.app; + delete beforeEvent.logger.error; + delete beforeEvent.logger.app; + assert.deepStrictEqual(result, beforeEvent); }); it('Should not die with initiating the class with bad object', () => { @@ -155,5 +225,58 @@ describe.only('Unit: PayloadValidator', () => { payloadValidator.verify({}); }); - -}); \ No newline at end of file + describe('Get Value', () => { + let payloadValidator; + before(() => { + const event = orgEvent('event'); + payloadValidator = new PayloadValidator(event, 'before-worker-id-nodeId'); + }); + it('Should set a root level variable', () => { + const location = 'hello'; + const value = 'world'; + const object = { + [location]: value + }; + const result = payloadValidator.getValue(object, location); + assert.strictEqual(result, value); + }); + + it('Should set a nested variable', () => { + const value = 'world'; + const object = { + should: { + have: { + hello: value + } + } + }; + const location = 'should.have.hello'; + + const result = payloadValidator.getValue(object, location); + assert.strictEqual(result, value); + }); + }); + + describe('Set Value', () => { + let payloadValidator; + before(() => { + const event = orgEvent('event'); + payloadValidator = new PayloadValidator(event, 'before-worker-id-nodeId'); + }); + it('Should set a root level variable', () => { + const object = {}; + const location = 'hello'; + const value = 'world'; + const result = payloadValidator.setValue(object, location, value); + assert.strictEqual(result[location], value); + }); + + it('Should set a nested variable', () => { + const object = {}; + const location = 'should.set.hello'; + const value = 'world'; + const result = payloadValidator.setValue(object, location, value); + assert.strictEqual(result.should.set.hello, value); + }); + }); +});