From 798431c0f12f753c905c6687a3b950317d531dad Mon Sep 17 00:00:00 2001 From: Ethan Arrowood Date: Mon, 17 Sep 2018 16:26:09 -0400 Subject: [PATCH] Feature/add auditing monkeypatch (#28) Feature/add auditing monkeypatch --- .vscode/launch.json | 14 +++++ README.md | 19 ++++++- audit.js | 65 +++++++++++++++++++++ index.js | 3 +- package-lock.json | 134 ++++++++++++++++++++++++++++++++++++++++++++ package.json | 8 ++- test/basic.js | 95 +++++++++++++++++++++++++++++++ 7 files changed, 335 insertions(+), 3 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 audit.js diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..3cc50f7 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,14 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "attach", + "name": "Attach", + "port": 9229 + }, + ] +} \ No newline at end of file diff --git a/README.md b/README.md index 348c426..e5361b7 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # platform-chaos -[![Build Status](https://travis-ci.org/azure/platform-chaos.svg?branch=master)](https://travis-ci.org/azure/platform-chaos) +[![Build Status](https://travis-ci.org/Azure/platform-chaos.svg?branch=master)](https://travis-ci.org/Azure/platform-chaos) A node sdk for building services capable of injecting chaos into PaaS offerings. ⚙️ 🌩 @@ -83,6 +83,23 @@ Inflates the `resources` from a `req` objects `body` into a collection of object const objs = require('azure-chaos-fn/parsers').resourcesToObjects(req) ``` +### auditer + +A documented implementation of the verbose logging format defined in [Auditing](https://github.com/Azure/platform-chaos/wiki/Auditing). The auditer is implemented by monkeypatching the `context` instance _log_ and _done_ methods. As a developer there is little extra effort you need to do to start using the auditer. At the beginning of your extension file, initialize the auditer by passing in the eventName and resources. Then use `context.log` as usual. Everything you log will be added to an intern audit list. When you call `context.done` at the end of your extension, the internal audit list is appended to the `context.res.body` under the `__audits` property. + +Initialize the auditer by using the following method: +```js +const index = require('azure-chaos-fn') + +index.auditer(/* Azure Function context */, { + eventName: /* Chaos event name : string */, + resources: /* Target resources : string */ +}) +``` + +See a fully implemented example in [this]() chaos event. +> TODO: Add an example project for 'this' + ## Related Projects * [platform-chaos-api](https://github.com/Azure/platform-chaos-api) - An API for introducing chaos into Azure PaaS offerings using configurable extensions. diff --git a/audit.js b/audit.js new file mode 100644 index 0000000..c760283 --- /dev/null +++ b/audit.js @@ -0,0 +1,65 @@ +const shimmer = require('shimmer') +const assert = require('assert') +const os = require('os') +const uuidv4 = require('uuid/v4') + +const auditSystem = `${os.hostname()}-${os.platform()}` +let auditQueue = [] + +const audit = (extensionLogArgs, auditOptions) => { + const { eventName, resources } = auditOptions + + auditQueue.push({ + auditId: uuidv4(), + eventName: eventName, + system: auditSystem, + date: new Date().toISOString(), + resources: resources, + extensionLog: typeof extensionLogArgs === 'string' ? [ extensionLogArgs ] : Array.from(extensionLogArgs) + }) +} + +module.exports = { + audit: audit, + initialize: (context, opts) => { + assert(opts, 'Options object must be defined') + assert(typeof opts.eventName === 'string', 'Event name must be a string') + assert(typeof opts.resources === 'string', 'Resources must be a string') + + const auditOptions = opts + + auditQueue = [] + + shimmer.wrap(context, 'log', function (original) { + return function () { + audit(arguments, auditOptions) + return original.apply(this, arguments) + } + }) + + shimmer.wrap(context, 'done', function (original) { + return function () { + const audits = auditQueue + + if (typeof context.res.body === 'undefined') { + context.rex.body = { + __audits: audits + } + } else if (typeof context.res.body === 'string') { + const body = JSON.parse(context.res.body) + body['__audits'] = audits + context.res.body = JSON.stringify(body) + } else if (typeof context.res.body === 'object') { + context.res.body['__audits'] = audits + } + /* + * If context.res.body is not of type string or object + * do not append the audits list to it and return it + * as usual. + * */ + + return original.apply(this, arguments) + } + }) + } +} diff --git a/index.js b/index.js index 712021f..092a434 100644 --- a/index.js +++ b/index.js @@ -1,4 +1,5 @@ module.exports = { parsers: require('./parsers'), - validators: require('./validators') + validators: require('./validators'), + auditer: require('./audit') } diff --git a/package-lock.json b/package-lock.json index 7de3a65..3507825 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,6 +4,30 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@sinonjs/commons": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.0.2.tgz", + "integrity": "sha512-WR3dlgqJP4QNrLC4iXN/5/2WaLQQ0VijOOkmflqFGVJ6wLEpbSjo7c0ZeGIdtY8Crk7xBBp87sM6+Mkerz7alw==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/formatio": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-2.0.0.tgz", + "integrity": "sha512-ls6CAMA6/5gG+O/IdsBcblvnd8qcO/l1TYoNeAzp3wcISOxlPXQEus0mLcdwazEkWjaBdaJ3TaxmNgCLWwvWzg==", + "dev": true, + "requires": { + "samsam": "1.3.0" + } + }, + "@sinonjs/samsam": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-2.0.0.tgz", + "integrity": "sha512-D7VxhADdZbDJ0HjUTMnSQ5xIGb4H2yWpg8k9Sf1T08zfFiQYlaxM8LZydpR4FQ2E6LZJX8IlabNZ5io4vdChwg==", + "dev": true + }, "acorn": { "version": "5.7.2", "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.2.tgz", @@ -889,6 +913,12 @@ "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", "dev": true }, + "just-extend": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-3.0.0.tgz", + "integrity": "sha512-Fu3T6pKBuxjWT/p4DkqGHFRsysc8OauWr4ZRTY9dIx07Y9O0RkoR5jcv28aeD1vuAwhm3nLkDurwLXoALp4DpQ==", + "dev": true + }, "levn": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", @@ -935,6 +965,24 @@ "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==", "dev": true }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", + "dev": true + }, + "lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=", + "dev": true + }, + "lolex": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-2.7.4.tgz", + "integrity": "sha512-Gh6Vffq/piTeHwunLNFR1jFVaqlwK9GMNUxFcsO1cwHyvbRKHwX8UDkxmrDnbcPdHNmpv7z2kxtkkSx5xkNpMw==", + "dev": true + }, "mimic-fn": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", @@ -1033,6 +1081,19 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, + "nise": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/nise/-/nise-1.4.4.tgz", + "integrity": "sha512-pxE0c9PzgrUTyhfv5p+5eMIdfU2bLEsq8VQEuE0kxM4zP7SujSar7rk9wpI2F7RyyCEvLyj5O7Is3RER5F36Fg==", + "dev": true, + "requires": { + "@sinonjs/formatio": "2.0.0", + "just-extend": "3.0.0", + "lolex": "2.7.4", + "path-to-regexp": "1.7.0", + "text-encoding": "0.6.4" + } + }, "normalize-package-data": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", @@ -1155,6 +1216,23 @@ "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", "dev": true }, + "path-to-regexp": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", + "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", + "dev": true, + "requires": { + "isarray": "0.0.1" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + } + } + }, "path-type": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", @@ -1324,6 +1402,12 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true }, + "samsam": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/samsam/-/samsam-1.3.0.tgz", + "integrity": "sha512-1HwIYD/8UlOtFS3QO3w7ey+SdSDFE4HRNLZoZRYVQefrOY3l17epswImeB1ijgJFQJodIaHcwkp3r/myBjFVbg==", + "dev": true + }, "semver": { "version": "5.5.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.1.tgz", @@ -1345,12 +1429,45 @@ "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", "dev": true }, + "shimmer": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.0.tgz", + "integrity": "sha512-xTCx2vohXC2EWWDqY/zb4+5Mu28D+HYNSOuFzsyRDRvI/e1ICb69afwaUwfjr+25ZXldbOLyp+iDUZHq8UnTag==" + }, "signal-exit": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", "dev": true }, + "sinon": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-6.2.0.tgz", + "integrity": "sha512-gLFZz5UYvOhYzQ+DBzw/OCkmWaLAHlAyQiE2wxUOmAGVdasP9Yw93E+OwZ0UuhW3ReMu1FKniuNsL6VukvC77w==", + "dev": true, + "requires": { + "@sinonjs/commons": "1.0.2", + "@sinonjs/formatio": "2.0.0", + "@sinonjs/samsam": "2.0.0", + "diff": "3.5.0", + "lodash.get": "4.4.2", + "lolex": "2.7.4", + "nise": "1.4.4", + "supports-color": "5.5.0", + "type-detect": "4.0.8" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "3.0.0" + } + } + } + }, "slice-ansi": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-1.0.0.tgz", @@ -1463,6 +1580,12 @@ "string-width": "2.1.1" } }, + "text-encoding": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.6.4.tgz", + "integrity": "sha1-45mpgiV6J22uQou5KEXLcb3CbRk=", + "dev": true + }, "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -1493,6 +1616,12 @@ "prelude-ls": "1.1.2" } }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, "uri-js": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", @@ -1502,6 +1631,11 @@ "punycode": "2.1.1" } }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + }, "validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", diff --git a/package.json b/package.json index 9470dc3..31a4309 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,12 @@ "eslint-plugin-node": "^7.0.1", "eslint-plugin-promise": "^4.0.0", "eslint-plugin-standard": "^4.0.0", - "mocha": "^5.2.0" + "lodash.isequal": "^4.5.0", + "mocha": "^5.2.0", + "sinon": "^6.2.0" + }, + "dependencies": { + "shimmer": "^1.2.0", + "uuid": "^3.3.2" } } diff --git a/test/basic.js b/test/basic.js index b5cb72e..00565b0 100644 --- a/test/basic.js +++ b/test/basic.js @@ -1,5 +1,7 @@ const assert = require('assert') const index = require('../') +const sinon = require('sinon') +const isEqual = require('lodash.isequal') /* eslint-env node, mocha */ @@ -97,4 +99,97 @@ describe('platform-chaos', () => { assert.equal(res.headers['Authorization'], expectedAccessToken) }) + + it('audits correctly', () => { + function contextLog () { + // in reality this would be `console.log(...arguments)` + // but in order to not cluter test we noop this + return () => null + } + function contextDone () { + return () => null + } + + const contextLogSpy = sinon.spy(contextLog) + const contextDoneSpy = sinon.spy(contextDone) + + const context = { + log: contextLogSpy, + done: contextDoneSpy, + res: { body: {} } + } + + index.auditer.initialize(context, { + eventName: 'testEvent', + resources: 'testResource' + }) + + const logItem1 = { + 'prop1': 'important information', + 'anotherProp': 'more important info' + } + const logItem2 = 'Hello, World!' + const logItem3 = ['abc', { a: 12 }] + + context.log(logItem1) + context.log(logItem2) + context.log(...logItem3) + + context.res.body = { + message: 'I am writing to the body' + } + + context.done() + + assert(contextLogSpy.called, 'context.log should be called') + assert(contextDoneSpy.called, 'context.done should be called') + + const body = context.res.body + + assert(typeof body === 'object', 'context.res.body should exist as an object') + assert(body.hasOwnProperty('__audits'), 'body contains __audits property') + assert(isEqual(body.__audits[0].extensionLog[0], logItem1), 'log item 1 is added to audit correctly') + assert(isEqual(body.__audits[1].extensionLog[0], logItem2), 'log item 2 is added to audit correctly') + assert(isEqual(body.__audits[2].extensionLog, logItem3), 'log item 3 is added to audit correctly') + }) + + it('allows user to audit directly', () => { + function contextLog () { + // in reality this would be `console.log(...arguments)` + // but in order to not cluter test we noop this + return () => null + } + function contextDone () { + return () => null + } + + const contextLogSpy = sinon.spy(contextLog) + const contextDoneSpy = sinon.spy(contextDone) + + const context = { + log: contextLogSpy, + done: contextDoneSpy, + res: { body: {} } + } + + const opts = { + eventName: 'testEvent', + resources: 'testResource' + } + + index.auditer.initialize(context, opts) + + index.auditer.audit('Hello, World!', opts) + + context.done() + + assert(contextLogSpy.notCalled, 'context.log should not be called') + assert(contextDoneSpy.called, 'context.done should be called') + + const body = context.res.body + + assert(typeof body === 'object', 'context.res.body should exist as an object') + assert(body.hasOwnProperty('__audits'), 'body contains __audits property') + assert(body.__audits[0].extensionLog[0] === 'Hello, World!', 'log item 1 is added to audit correctly') + }) })