@contrast/protect 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/LICENSE +12 -0
  2. package/README.md +9 -0
  3. package/lib/cli-rewriter.js +20 -0
  4. package/lib/error-handlers/constants.js +5 -0
  5. package/lib/error-handlers/index.js +13 -0
  6. package/lib/error-handlers/install/fastify3.js +88 -0
  7. package/lib/error-handlers/install/fastify3.test.js +142 -0
  8. package/lib/esm-loader.mjs +2 -0
  9. package/lib/esm-loader.test.mjs +11 -0
  10. package/lib/index.d.ts +36 -0
  11. package/lib/index.js +89 -0
  12. package/lib/index.test.js +32 -0
  13. package/lib/input-analysis/handlers.js +462 -0
  14. package/lib/input-analysis/handlers.test.js +898 -0
  15. package/lib/input-analysis/index.js +16 -0
  16. package/lib/input-analysis/index.test.js +28 -0
  17. package/lib/input-analysis/install/fastify3.js +79 -0
  18. package/lib/input-analysis/install/fastify3.test.js +71 -0
  19. package/lib/input-analysis/install/http.js +185 -0
  20. package/lib/input-analysis/install/http.test.js +315 -0
  21. package/lib/input-tracing/constants.js +5 -0
  22. package/lib/input-tracing/handlers/index.js +117 -0
  23. package/lib/input-tracing/handlers/index.test.js +395 -0
  24. package/lib/input-tracing/handlers/nosql-injection-mongo.js +48 -0
  25. package/lib/input-tracing/index.js +32 -0
  26. package/lib/input-tracing/install/README.md +1 -0
  27. package/lib/input-tracing/install/child-process.js +45 -0
  28. package/lib/input-tracing/install/child-process.test.js +112 -0
  29. package/lib/input-tracing/install/fs.js +107 -0
  30. package/lib/input-tracing/install/fs.test.js +118 -0
  31. package/lib/input-tracing/install/mysql.js +57 -0
  32. package/lib/input-tracing/install/mysql.test.js +108 -0
  33. package/lib/input-tracing/install/postgres.js +61 -0
  34. package/lib/input-tracing/install/postgres.test.js +125 -0
  35. package/lib/input-tracing/install/sequelize.js +51 -0
  36. package/lib/input-tracing/install/sequelize.test.js +79 -0
  37. package/lib/input-tracing/install/sqlite3.js +45 -0
  38. package/lib/input-tracing/install/sqlite3.test.js +88 -0
  39. package/lib/make-response-blocker.js +35 -0
  40. package/lib/make-response-blocker.test.js +88 -0
  41. package/lib/make-source-context.js +130 -0
  42. package/lib/make-source-context.test.js +298 -0
  43. package/lib/security-exception.js +12 -0
  44. package/lib/throw-security-exception.js +30 -0
  45. package/lib/throw-security-exception.test.js +50 -0
  46. package/lib/utils.js +88 -0
  47. package/lib/utils.test.js +40 -0
  48. package/package.json +32 -0
@@ -0,0 +1,16 @@
1
+ 'use strict';
2
+
3
+ module.exports = function(core) {
4
+ const inputAnalysis = core.protect.inputAnalysis = {};
5
+
6
+ require('./handlers')(core);
7
+ require('./install/http')(core);
8
+ require('./install/fastify3')(core);
9
+
10
+ inputAnalysis.install = function() {
11
+ inputAnalysis.httpInstrumentation.install();
12
+ inputAnalysis.fastifyInstrumentation.install();
13
+ };
14
+
15
+ return inputAnalysis;
16
+ };
@@ -0,0 +1,28 @@
1
+ 'use strict';
2
+
3
+ const { expect } = require('chai');
4
+ const sinon = require('sinon');
5
+ const proxyquire = require('proxyquire');
6
+
7
+ describe('protect input-analysis', function () {
8
+ let modulesMock, exportsStub, coreMock;
9
+ beforeEach(function () {
10
+ exportsStub = sinon.stub();
11
+ modulesMock = () => exportsStub();
12
+ coreMock = { protect: {} };
13
+ });
14
+
15
+ it('calls the required hooks', function () {
16
+ const inputAnalysis = proxyquire('.', {
17
+ './handlers': modulesMock,
18
+ './install/http': modulesMock,
19
+ './install/fastify3': modulesMock
20
+ });
21
+
22
+ expect(exportsStub).to.not.have.been.called;
23
+ expect(coreMock.protect).to.not.haveOwnProperty('inputAnalysis');
24
+ inputAnalysis(coreMock);
25
+ expect(coreMock.protect).to.haveOwnProperty('inputAnalysis');
26
+ expect(exportsStub).to.have.been.callCount(3);
27
+ });
28
+ });
@@ -0,0 +1,79 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Function that exports an install method to patch Fastify framework with our instrumentation
5
+ * @param {Object} core - the core Contrast object in v5
6
+ * @return {Object} object with install method and the other relative functions exported for testing purposes
7
+ */
8
+ module.exports = (core) => {
9
+ const {
10
+ depHooks,
11
+ patcher,
12
+ logger,
13
+ scopes: { sources },
14
+ protect: { inputAnalysis },
15
+ } = core;
16
+
17
+ /**
18
+ * registers a depHook for fastify module instrumentation
19
+ */
20
+ function install() {
21
+ depHooks.resolve({ name: 'fastify', version: '>=3.0.0' }, (fastify) => patchFastify(fastify));
22
+ }
23
+
24
+ /**
25
+ * The patch function for the depHooks callback
26
+ * @param {Object} fastify the fastify object returned from requiring the module
27
+ * @returns a patched fastify object
28
+ */
29
+ function patchFastify(fastify) {
30
+ return patcher.patch(fastify, {
31
+ name: 'fastify.build',
32
+ patchType: 'framework-patch',
33
+ post({ result: server }) {
34
+ server.addHook('preValidation', preValidationHook);
35
+ },
36
+ });
37
+ }
38
+
39
+ /**
40
+ * Fastify lifecycle hook as defined in official docs.
41
+ * @external https://www.fastify.io/docs/latest/Reference/Hooks/#prevalidation
42
+ * @param {Fastify.Request} request incoming request
43
+ * @param {Fastify.Reply} reply unbuilt outgoing response
44
+ * @param {Function} done callback to signal the hook is finished.
45
+ */
46
+ function preValidationHook(request, reply, done) {
47
+ const sourceContext = sources.getStore()?.protect;
48
+
49
+ if (!sourceContext) {
50
+ logger.debug('source context not available in fastify prevalidation hook');
51
+ } else {
52
+ if (request.params) {
53
+ sourceContext.parsedParams = request.params;
54
+ inputAnalysis.handleUrlParams(sourceContext, request.params);
55
+ }
56
+ if (request.cookies) {
57
+ sourceContext.parsedCookies = request.cookies;
58
+ inputAnalysis.handleCookies(sourceContext, request.cookies);
59
+ }
60
+ if (request.body) {
61
+ sourceContext.parsedBody = request.body;
62
+ inputAnalysis.handleParsedBody(sourceContext, request.body);
63
+ }
64
+
65
+ if (request.query) {
66
+ sourceContext.parsedQuery = request.query;
67
+ inputAnalysis.handleQueryParams(sourceContext, request.query);
68
+ }
69
+ }
70
+ done();
71
+ }
72
+
73
+ const fastifyInstrumentation = inputAnalysis.fastifyInstrumentation = {
74
+ preValidationHook,
75
+ install,
76
+ };
77
+
78
+ return fastifyInstrumentation;
79
+ };
@@ -0,0 +1,71 @@
1
+ 'use strict';
2
+
3
+ const { expect } = require('chai');
4
+ const sinon = require('sinon');
5
+ const mocks = require('../../../../test/mocks');
6
+
7
+ describe('protect input-analysis fastify intrumentation', function () {
8
+ let core;
9
+ let inputAnalysis;
10
+ let fastify;
11
+ let serverMock;
12
+ let reqMock;
13
+ let resMock;
14
+ let doneMock;
15
+
16
+ beforeEach(function () {
17
+ core = mocks.core();
18
+ core.logger = mocks.logger();
19
+ core.scopes = mocks.scopes();
20
+ core.protect = mocks.protect();
21
+ core.depHooks = mocks.depHooks();
22
+ core.patcher = require('@contrast/patcher')(core);
23
+
24
+ reqMock = {
25
+ cookies: { foo: 'bar' },
26
+ params: { foo: 'bar' },
27
+ body: { foo: 'bar' },
28
+ };
29
+ resMock = {};
30
+ doneMock = sinon.stub();
31
+ serverMock = {
32
+ addHook: sinon.stub().yields(reqMock, resMock, doneMock),
33
+ };
34
+
35
+ const fastifyOrig = () => serverMock;
36
+ core.depHooks.resolve.callsFake(function(desc, cb) {
37
+ fastify = cb(fastifyOrig);
38
+ });
39
+
40
+ const fastifyInstr = require('./fastify3')(core);
41
+ inputAnalysis = core.protect.inputAnalysis;
42
+ fastifyInstr.install();
43
+ });
44
+
45
+ describe('preValidationHook', function () {
46
+ it('hook is added and calls appropriate analysis handlers', function () {
47
+ core.scopes.sources.run({ protect: {} }, () => {
48
+ fastify();
49
+ });
50
+
51
+ expect(serverMock.addHook).to.have.been.called;
52
+ expect(doneMock).to.have.been.called;
53
+ expect(core.logger.debug).not.to.have.been.called;
54
+
55
+ expect(inputAnalysis.handleUrlParams).to.have.been.called;
56
+ expect(inputAnalysis.handleCookies).to.have.been.called;
57
+ expect(inputAnalysis.handleParsedBody).to.have.been.called;
58
+ });
59
+ it('input analysis does not occur if there is no protect source context', function () {
60
+ fastify();
61
+ expect(doneMock).to.have.been.called;
62
+ expect(core.logger.debug).to.have.been.calledWith(
63
+ 'source context not available in fastify prevalidation hook'
64
+ );
65
+
66
+ expect(inputAnalysis.handleUrlParams).to.not.have.been.called;
67
+ expect(inputAnalysis.handleCookies).to.not.have.been.called;
68
+ expect(inputAnalysis.handleParsedBody).to.not.have.been.called;
69
+ });
70
+ });
71
+ });
@@ -0,0 +1,185 @@
1
+ 'use strict';
2
+
3
+ const { Event } = require('@contrast/common');
4
+
5
+ // Instruments http `Server` and `IncomingMessage` instances to support input
6
+ // analysis in framework-agnostic manner.
7
+
8
+ module.exports = function(core) {
9
+ const { protect: { inputAnalysis } } = core;
10
+ inputAnalysis.httpInstrumentation = new HttpInstrumentation(core);
11
+ return inputAnalysis.httpInstrumentation;
12
+ };
13
+ class HttpInstrumentation {
14
+ constructor(core) {
15
+ const { logger } = core;
16
+ this.messages = core.messages;
17
+ this.scope = core.scopes.sources;
18
+ this.config = core.config;
19
+ this.logger = logger.child({ name: 'contrast:protect:input-analysis' });
20
+ this.depHooks = core.depHooks;
21
+ this.messages = core.messages;
22
+ this.protect = core.protect;
23
+ this.makeSourceContext = this.protect.makeSourceContext;
24
+ this.maxBodySize = 16 * 1024 * 1024;
25
+ this.installed = false;
26
+ }
27
+
28
+ /**
29
+ * After checking whether the sensor is enabled, will set up `require` hooks
30
+ * for instrumenting both `http` and `https` modules when they load.
31
+ */
32
+ install() {
33
+ if (this.installed) {
34
+ return;
35
+ }
36
+
37
+ this.installed = true;
38
+ this.hookHttp();
39
+ this.hookHttps();
40
+ }
41
+
42
+ uninstall() {}
43
+
44
+ /**
45
+ * Sets hooks to instrument `http.Server.prototype`.
46
+ */
47
+ hookHttp() {
48
+ this.logger.debug('hooking library: http');
49
+ this.depHooks.resolve({ name: 'http' }, this.hookServer.bind(this));
50
+ }
51
+
52
+ /**
53
+ * Sets hooks to instrument `https.Server.prototype`.
54
+ */
55
+ hookHttps() {
56
+ this.logger.debug('hooking library: https');
57
+ this.depHooks.resolve({ name: 'https' }, this.hookServer.bind(this));
58
+ }
59
+
60
+ /**
61
+ * Instruments the `Server` prototype from `http(s)`. This patches `emit` and
62
+ * invokes the protect service to do analysis when appropriate.
63
+ *
64
+ * @param {Object} xport The http(s) module export
65
+ */
66
+ hookServer(xport) {
67
+ const self = this;
68
+
69
+ const {
70
+ Server: {
71
+ prototype: { emit }
72
+ }
73
+ } = xport;
74
+
75
+ xport.Server.prototype.emit = function(...args) {
76
+ const [type] = args;
77
+
78
+ if (type !== 'request') {
79
+ return emit.call(this, ...args);
80
+ }
81
+
82
+ const context = { instance: this, method: emit, args };
83
+ self.initiateRequestHandling(context);
84
+
85
+ return !!this._events[type];
86
+ };
87
+ }
88
+
89
+ /**
90
+ * Creates the sourceContext for the request and invokes the handler for
91
+ * inputs that are present in the 'incomingMessage' object at the time of
92
+ * the 'connect' event.
93
+ *
94
+ * @param {Object} context Function invocation context
95
+ */
96
+ initiateRequestHandling(fnContext) {
97
+ const {
98
+ instance,
99
+ method,
100
+ args,
101
+ args: [, req, res]
102
+ } = fnContext;
103
+
104
+ // URL exclusions should be applied here. there is no point in doing any additional
105
+ // work if the url is excluded for a particular rule, i.e., that rule should be removed
106
+ // from the list of rules for this request. and if all rules are excluded for this url
107
+ // then none of the following needs to be done.
108
+ if (this.protect.rules.agentLibRulesMask === 0) {
109
+ this.logger.debug('no agent-lib rules are enabled, not checking request');
110
+ return;
111
+ }
112
+
113
+ let store;
114
+ let block;
115
+
116
+ try {
117
+ const { messages, protect: { inputAnalysis } } = this; // the functions that do input analysis
118
+
119
+ // this must be invoked by the patching code using scope.sources.run({}, ...)
120
+ // so that an async context is present.
121
+ store = this.scope.getStore();
122
+ // nothing can be done if async context is not available.
123
+ if (!store) {
124
+ this.logger.debug('cannot acquire store for initiateRequestHandling()');
125
+ return;
126
+ }
127
+
128
+ store.protect = this.makeSourceContext(req, res);
129
+ const { reqData } = store.protect;
130
+
131
+ res.on('finish', () => messages.emit(Event.PROTECT, store));
132
+
133
+ // don't put inputs in the store; they are a param to each handler. findings
134
+ // associated with inputs do go into the store. why not put the inputs
135
+ // into the store? after all, the inputs come from the store. mostly because
136
+ // they can really add up to a lot of data that isn't going to be used.
137
+ //
138
+ // how to replace result in resultsList, e.g., queries find something
139
+ // but then framework emits parsed queries? does this only matter for
140
+ // no-sql? index-lookup or hash?
141
+ //
142
+ // create inputs for this handler. we defer cookies until the framework
143
+ // parses them because there is no way to be certain of their formatting
144
+ // and encoding.
145
+ //
146
+ // the primary reason for this is to avoid passing the incomingMessage,
147
+ // req, to all the handlers allowing direct access to it and tightly
148
+ // coupling all handlers to an extensive collection of data.
149
+ const connectInputs = {
150
+ headers: HttpInstrumentation.removeCookies(reqData.headers),
151
+ uriPath: reqData.uriPath,
152
+ // TODO AGENT-203 - need to handle method-tampering rule.
153
+ method: reqData.method,
154
+ };
155
+ // only add queries if it's known that 'qs' or equivalent won't be used.
156
+ /* c8 ignore next 3 */
157
+ if (reqData.standardUrlParsing) {
158
+ connectInputs.queries = reqData.queries;
159
+ }
160
+
161
+ block = inputAnalysis.handleConnect(store.protect, connectInputs);
162
+
163
+ } catch (err) {
164
+ this.logger.error({ err }, 'Error during input analysis');
165
+ }
166
+
167
+ if (!block) {
168
+ setImmediate(() => method.call(instance, ...args));
169
+ } else {
170
+ store.protect.block(...block);
171
+ this.logger.debug({ block }, 'request blocked by not emitting request event');
172
+ }
173
+ }
174
+
175
+ static removeCookies(headers) {
176
+ for (let i = 0; i < headers.length; i += 2) {
177
+ if (headers[i] === 'cookies') {
178
+ headers = headers.slice();
179
+ headers.splice(i, 2);
180
+ return headers;
181
+ }
182
+ }
183
+ return headers;
184
+ }
185
+ }
@@ -0,0 +1,315 @@
1
+ 'use strict';
2
+
3
+ const EventEmitter = require('events');
4
+
5
+ const { expect } = require('chai');
6
+ const sinon = require('sinon');
7
+
8
+ const aLib = require('@contrast/agent-lib');
9
+ const Protect = require('../../..');
10
+ const mocks = require('../../../../test/mocks');
11
+
12
+ describe('protect input-analysis/http', function() {
13
+
14
+ describe('initialization', function() {
15
+ let core;
16
+ let httpInstr;
17
+
18
+ beforeEach(function() {
19
+ core = mocks.core();
20
+ core.depHooks = mocks.depHooks();
21
+ core.config = mocks.config();
22
+ core.logger = mocks.logger();
23
+ core.scopes = mocks.scopes();
24
+ core.patcher = mocks.patcher();
25
+ core.protect = Protect(core);
26
+ httpInstr = require('./http')(core);
27
+ });
28
+
29
+ it('should not be initialized after being required', function() {
30
+ expect(httpInstr.install).a('function');
31
+ expect(core.depHooks.install).not.called;
32
+ expect(core.depHooks.resolve).not.called;
33
+ });
34
+
35
+ it('should be initialized after being installed', function() {
36
+ httpInstr.install();
37
+ // once for http, once for https
38
+ expect(core.depHooks.resolve).callCount(2);
39
+ // no way to check the sensor state. probably should attach it to ?
40
+ });
41
+ });
42
+
43
+ describe('initiateRequestHandling()', function() {
44
+ let core;
45
+ let expectedRules;
46
+ let httpInstr;
47
+ let server;
48
+ let serverEmit;
49
+ let req, res;
50
+ let reqEmitter;
51
+ let context;
52
+ let expectedReqData;
53
+ let connectInputs;
54
+
55
+ beforeEach(function() {
56
+ //console.log(this.currentTest.title);
57
+ core = mocks.core();
58
+ core.depHooks = mocks.depHooks();
59
+ core.config = mocks.config();
60
+ Object.assign(core.config.protect.rules, {
61
+ 'cmd-injection': { mode: 'monitor' }
62
+ });
63
+ core.logger = mocks.logger();
64
+ core.scopes = mocks.scopes();
65
+ core.patcher = mocks.patcher();
66
+ core.protect = Protect(core);
67
+
68
+ expectedRules = makeExpectedRules(core.config.protect.rules);
69
+
70
+ sinon.spy(core.protect.inputAnalysis, 'handleConnect');
71
+
72
+ httpInstr = require('./http')(core);
73
+ sinon.spy(httpInstr, 'makeSourceContext');
74
+ sinon.spy(httpInstr.scope, 'getStore');
75
+
76
+ // mock Server thing...
77
+ server = {};
78
+ serverEmit = sinon.stub();
79
+ server.prototype = { emit: serverEmit };
80
+
81
+ // mock req, res
82
+ reqEmitter = new EventEmitter();
83
+ req = {
84
+ url: '/',
85
+ method: 'GET',
86
+ rawHeaders: ['host', 'vogon.com', 'content-type', 'application/json'],
87
+ // this needs to look sort of like a real incoming message
88
+ emit: (...args) => reqEmitter.emit(...args),
89
+ _events: {},
90
+ socket: { _readableState: { autoDestroy: false } },
91
+ resume: sinon.stub(),
92
+ };
93
+ res = {
94
+ end: sinon.stub(),
95
+ writeHead: sinon.stub(),
96
+ headersSent: false,
97
+ on: sinon.stub(),
98
+ };
99
+
100
+ // setup the context required
101
+ context = {
102
+ instance: server,
103
+ method: serverEmit,
104
+ args: ['request', req, res]
105
+ };
106
+
107
+ // setup a few expected results. these can be modified as needed by
108
+ // different tests.
109
+ expectedReqData = {
110
+ method: req.method,
111
+ headers: req.rawHeaders.map((h, i) => i & 1 ? h : h.toLowerCase()),
112
+ uriPath: '/',
113
+ queries: '',
114
+ contentType: '',
115
+ standardUrlParsing: false,
116
+ };
117
+ connectInputs = {
118
+ method: expectedReqData.method,
119
+ headers: expectedReqData.headers,
120
+ uriPath: expectedReqData.uriPath,
121
+ };
122
+ });
123
+
124
+ //
125
+ // make sure basic things are as expected
126
+ //
127
+ function doBasicChecks(expectedReqData) {
128
+ // scope.run() call invokes scope.getStore(), so it will be called once to execute
129
+ // the test and once more unless no rules were in effect.
130
+ if (core.protect.rules.agentLibRulesMask !== 0) {
131
+ expect(httpInstr.scope.getStore).callCount(2);
132
+ expect(httpInstr.makeSourceContext).calledOnceWith(req, res);
133
+ } else {
134
+ // source context wasn't available, so
135
+ expect(httpInstr.scope.getStore).callCount(1);
136
+ return;
137
+ }
138
+
139
+ const sourceContext = httpInstr.makeSourceContext.returnValues[0];
140
+ const {
141
+ block, exclusions, findings, reqData, rules, virtualPatches
142
+ } = sourceContext;
143
+
144
+ /* eslint-disable newline-per-chained-call */
145
+ expect(block).a('function');
146
+ expect(exclusions).an('array').eql([]);
147
+ expect(findings).an('object').eql({
148
+ trackRequest: false, securityException: undefined, bodyType: undefined, resultsMap: {}
149
+ });
150
+ expect(reqData.method).equal(expectedReqData.method);
151
+ expect(reqData.uriPath).equal(expectedReqData.uriPath);
152
+ expect(reqData.headers).eql(expectedReqData.headers);
153
+ expect(rules).an('object').eql(expectedRules);
154
+ expect(virtualPatches).an('array').eql([]);
155
+
156
+ return sourceContext;
157
+ }
158
+
159
+ // i don't see how to unit test this; xport.Server.prototype.emit()
160
+ // i.e., the real patching function. i think verifying that function
161
+ // will require an integration test of some sort.
162
+ // it('should handle requests on http, https, and http2', ...)
163
+
164
+ // initiateRequestHandling() sets up everything but the inputs for all analysis
165
+ // of this request.
166
+ it('works as expected', function(done) {
167
+ // this is called here because other tests will modify the default data setup in
168
+ // beforeEach().
169
+ httpInstr.scope.run(httpInstr.scope, () => httpInstr.initiateRequestHandling(context));
170
+
171
+ const sourceContext = doBasicChecks(expectedReqData);
172
+
173
+ // and finally, handleConnect() should have been called with context
174
+ // and input.
175
+ expect(core.protect.inputAnalysis.handleConnect).calledOnceWith(sourceContext, connectInputs);
176
+
177
+ // the real emitter is called via setImmediate() so give it a chance to run.
178
+ expect(serverEmit).callCount(0);
179
+ setImmediate(function() {
180
+ expect(serverEmit).callCount(1);
181
+ done();
182
+ });
183
+ });
184
+
185
+ it('debug logs if no async context', function() {
186
+ // this is called here because other tests will modify the default data setup in
187
+ // beforeEach().
188
+ httpInstr.scope.getStore.restore();
189
+ sinon.stub(httpInstr.scope, 'getStore').returns(undefined);
190
+ core.logger.debug = sinon.stub();
191
+ httpInstr.scope.run(httpInstr.scope, () => httpInstr.initiateRequestHandling(context));
192
+
193
+ expect(core.logger.debug).calledOnceWith('cannot acquire store for initiateRequestHandling()');
194
+ });
195
+
196
+ it('error logs on exceptions', function() {
197
+ const err = new Error('sometimes things do not work out');
198
+ const { inputAnalysis } = core.protect;
199
+ inputAnalysis.handleConnect.restore();
200
+ sinon.stub(inputAnalysis, 'handleConnect').throws(err);
201
+ // simulate an incoming request to the server.
202
+ httpInstr.scope.run(httpInstr.scope, () => httpInstr.initiateRequestHandling(context));
203
+
204
+ expect(core.logger.error).calledOnceWith({ err }, 'Error during input analysis');
205
+ });
206
+
207
+ it('connectInputs will contain not a cookies header', function(done) {
208
+ const withoutCookies = req.rawHeaders.slice();
209
+ req.rawHeaders.push('COOKIES', 'this;that;the other;thing');
210
+ expectedReqData.headers.push('cookies', 'this;that;the other;thing');
211
+
212
+ // simulate an incoming request to the server.
213
+ httpInstr.scope.run(httpInstr.scope, () => httpInstr.initiateRequestHandling(context));
214
+
215
+ const sourceContext = doBasicChecks(expectedReqData);
216
+
217
+ // and finally, handleConnect() should have been called with sourceContext and connectInput.
218
+ // the cookies header should not be present because protect waits until the framework has
219
+ // decoded the cookies before scoring them.
220
+ connectInputs.headers = withoutCookies;
221
+ expect(core.protect.inputAnalysis.handleConnect).calledOnceWith(sourceContext, connectInputs);
222
+
223
+ // the real emitter is called via setImmediate() so give it a chance
224
+ // to run.
225
+ expect(serverEmit).callCount(0);
226
+ setImmediate(function() {
227
+ expect(serverEmit).callCount(1);
228
+ done();
229
+ });
230
+ });
231
+
232
+ it('does not analyze the request when no rules are in effect', function() {
233
+ // kind of hacky reset on the rules
234
+ core.protect.rules = {
235
+ agentLibRulesMask: 0,
236
+ agentLibRules: {},
237
+ agentRules: {},
238
+ };
239
+ expectedRules = core.protect.rules;
240
+ core.logger.debug = sinon.stub();
241
+
242
+ // simulate an incoming request to the server.
243
+ httpInstr.scope.run(httpInstr.scope, () => httpInstr.initiateRequestHandling(context));
244
+
245
+ doBasicChecks(expectedReqData);
246
+
247
+ expect(core.logger.debug).calledOnceWith('no agent-lib rules are enabled, not checking request');
248
+ });
249
+
250
+ it('does not emit the original event when blocked', function(done) {
251
+ req.url = '/need/<script%20this&that';
252
+ const sourceContext = core.protect.makeSourceContext(req, res);
253
+ sourceContext.rules.agentLibRules['reflected-xss'] = { mode: 'block' };
254
+ sourceContext.rules.agentLibRulesMask |= aLib.constants.RuleType['reflected-xss'];
255
+ sourceContext.block = sinon.stub();
256
+ core.logger.debug = sinon.stub();
257
+
258
+
259
+ httpInstr.scope.run(httpInstr.scope, () => httpInstr.initiateRequestHandling(context));
260
+
261
+ const block = ['block', 'reflected-xss'];
262
+
263
+ expect(core.protect.inputAnalysis.handleConnect).callCount(1).returned(block);
264
+ expect(core.logger.debug).calledWith({ block }, 'request blocked by not emitting request event');
265
+ setImmediate(function() {
266
+ expect(serverEmit).callCount(0);
267
+ done();
268
+ });
269
+ });
270
+
271
+ it('does not handle a request body when it is not JSON', function(done) {
272
+ req.rawHeaders = ['Content-Type', 'multipart/form-data'];
273
+ expectedReqData.headers = ['content-type', 'multipart/form-data'];
274
+ expectedReqData.contentType = 'multipart/form-data';
275
+
276
+ // simulate an incoming request to the server.
277
+ httpInstr.scope.run(httpInstr.scope, () => httpInstr.initiateRequestHandling(context));
278
+
279
+ const sourceContext = doBasicChecks(expectedReqData);
280
+
281
+ // and finally, handleConnect() should have been called with sourceContext and connectInput.
282
+ connectInputs.headers = expectedReqData.headers;
283
+ expect(core.protect.inputAnalysis.handleConnect).calledOnceWith(sourceContext, connectInputs);
284
+
285
+ expect(serverEmit).callCount(0);
286
+ setImmediate(function() {
287
+ expect(serverEmit).callCount(1);
288
+ done();
289
+ });
290
+ });
291
+ });
292
+ });
293
+
294
+ function makeExpectedRules(rules) {
295
+ const { RuleType } = aLib.constants;
296
+ const agentLibRules = {};
297
+ const agentRules = {};
298
+ let agentLibRulesMask = 0;
299
+
300
+ for (const rule in rules) {
301
+ if (!(rule in RuleType)) {
302
+ // it's a little random, but if the rule isn't an agent-lib rule, presume
303
+ // that it's an agent rule.
304
+ if (rule !== 'disabled_rules') {
305
+ agentRules[rule] = { mode: undefined };
306
+ }
307
+ continue;
308
+ }
309
+ agentLibRules[rule] = rules[rule];
310
+ agentLibRulesMask |= RuleType[rule];
311
+ }
312
+
313
+ return { agentLibRules, agentLibRulesMask, agentRules };
314
+
315
+ }
@@ -0,0 +1,5 @@
1
+ 'use strict';
2
+
3
+ module.exports = {
4
+ patchType: 'protect-input-tracing',
5
+ };