@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
package/LICENSE ADDED
@@ -0,0 +1,12 @@
1
+ Copyright: 2022 Contrast Security, Inc
2
+ Contact: support@contrastsecurity.com
3
+ License: Commercial
4
+
5
+ NOTICE: This Software and the patented inventions embodied within may only be
6
+ used as part of Contrast Security’s commercial offerings. Even though it is
7
+ made available through public repositories, use of this Software is subject to
8
+ the applicable End User Licensing Agreement found at
9
+ https://www.contrastsecurity.com/enduser-terms-0317a or as otherwise agreed
10
+ between Contrast Security and the End User. The Software may not be reverse
11
+ engineered, modified, repackaged, sold, redistributed or otherwise used in a
12
+ way not consistent with the End User License Agreement.
package/README.md ADDED
@@ -0,0 +1,9 @@
1
+ # `@contrast/protect`
2
+
3
+ ## Sub-Components
4
+
5
+ ### Request Correlation
6
+
7
+ ### Input Analysis
8
+
9
+ ### Input Tracing
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+
3
+ 'use strict';
4
+
5
+ const { cliRewriter } = require('@contrast/core')();
6
+
7
+ cliRewriter((deps) => {
8
+ if (deps.config.protect.enable) {
9
+ try {
10
+ const protect = require('./index')(deps);
11
+ protect.rewriting.install();
12
+ } catch (err) {
13
+ // TODO: something else
14
+ throw err;
15
+ }
16
+ } else {
17
+ deps.logger.error('Configuration Error: mode \'Protect\' is not enabled');
18
+ process.exit(1);
19
+ }
20
+ });
@@ -0,0 +1,5 @@
1
+ 'use strict';
2
+
3
+ module.exports = {
4
+ patchType: 'protect-error-handling'
5
+ };
@@ -0,0 +1,13 @@
1
+ 'use strict';
2
+
3
+ module.exports = function(core) {
4
+ const errorHandlers = core.protect.errorHandlers = {};
5
+
6
+ require('./install/fastify3')(core);
7
+
8
+ errorHandlers.install = function() {
9
+ errorHandlers.fastify3ErrorHandler.install();
10
+ };
11
+
12
+ return errorHandlers;
13
+ };
@@ -0,0 +1,88 @@
1
+ 'use strict';
2
+
3
+ const { isSecurityException } = require('../../security-exception');
4
+ const { patchType } = require('../constants');
5
+
6
+ module.exports = function(core) {
7
+ const {
8
+ logger,
9
+ depHooks,
10
+ patcher,
11
+ scopes: { sources },
12
+ protect,
13
+ } = core;
14
+
15
+ const fastify3ErrorHandler = protect.errorHandlers.fastify3ErrorHandler = {
16
+ _userHandler: null
17
+ };
18
+
19
+ /**
20
+ * This is the default handler from fastify's source code. If it's not a
21
+ * Contrast error and the user didn't supply their own we should use this.
22
+ */
23
+ fastify3ErrorHandler.defaultErrorHandler = function (error, request, reply) {
24
+ if (reply.statusCode < 500) {
25
+ reply.log.info({ res: reply, err: error }, error && error.message);
26
+ } else {
27
+ reply.log.error(
28
+ { req: request, res: reply, err: error },
29
+ error && error.message
30
+ );
31
+ }
32
+ reply.send(error);
33
+ };
34
+
35
+ /**
36
+ * Check if the error being handled was thrown by Contrast. If not,
37
+ * use either the default fastify3 error handler or the user-defined handler
38
+ * if one was specified by calling fastify.setErrorHandler(fn).
39
+ * @param {error} err error being handled
40
+ * @param {object} request fastify request
41
+ * @param {object} reply fastify repoly
42
+ */
43
+ fastify3ErrorHandler.handler = function(err, request, reply) {
44
+ const normalHandler = fastify3ErrorHandler._userHandler || fastify3ErrorHandler.defaultErrorHandler;
45
+
46
+ if (isSecurityException(err)) {
47
+ const sourceContext = sources.getStore()?.protect;
48
+ if (!sourceContext) {
49
+ logger.info('source context not found; unable to handle response');
50
+ normalHandler.call(this, err, request, reply);
51
+ } else {
52
+ const blockInfo = sourceContext.findings.securityException;
53
+ sourceContext.block(...blockInfo);
54
+ }
55
+ } else {
56
+ normalHandler.call(this, err, request, reply);
57
+ }
58
+ };
59
+
60
+ /**
61
+ * Instruments fastify in order to add our custom error handler.
62
+ */
63
+ fastify3ErrorHandler.install = function() {
64
+ depHooks.resolve({ name: 'fastify', version: '<=4.0.0' }, (fastify) => patcher.patch(fastify, {
65
+ name: 'fastify',
66
+ patchType,
67
+ post(data) {
68
+ const { result: server } = data;
69
+ // Set our custom handler initially
70
+ server.setErrorHandler(fastify3ErrorHandler.handler);
71
+
72
+ // Patch, so that if someone sets their own, we override with ours. But,
73
+ // we do need to keep a reference to it so we can still call it for when
74
+ // non-Contrast errors need handling.
75
+ patcher.patch(server, 'setErrorHandler', {
76
+ name: 'fastify.setErrorHandler',
77
+ patchType,
78
+ pre({ args }) {
79
+ fastify3ErrorHandler._userHandler = args[0];
80
+ args[0] = fastify3ErrorHandler.handler;
81
+ }
82
+ });
83
+ }
84
+ }));
85
+ };
86
+
87
+ return fastify3ErrorHandler;
88
+ };
@@ -0,0 +1,142 @@
1
+ 'use strict';
2
+
3
+ const sinon = require('sinon');
4
+ const { expect } = require('chai');
5
+ const securityException = require('../../security-exception');
6
+ const mocks = require('../../../../test/mocks');
7
+
8
+ describe('protect error-handlers: fastify3', function() {
9
+ let core;
10
+ let errorHandler;
11
+ let fastify;
12
+ let server;
13
+ let reply;
14
+
15
+ beforeEach(function() {
16
+ core = mocks.core();
17
+ core.config = mocks.config();
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
+ server = {
25
+ setErrorHandler: sinon.stub(),
26
+ errorHandler: sinon.stub()
27
+ };
28
+ fastify = function() {
29
+ return server;
30
+ };
31
+ core.depHooks.resolve.callsFake((desc, cb) => {
32
+ fastify = cb(fastify);
33
+ });
34
+ reply = {
35
+ log: {
36
+ info: sinon.stub(),
37
+ error: sinon.stub()
38
+ },
39
+ send: sinon.stub(),
40
+ statusCode: 500,
41
+ };
42
+
43
+ errorHandler = require('./fastify3')(core);
44
+ sinon.spy(errorHandler, 'defaultErrorHandler');
45
+ errorHandler.install();
46
+
47
+ fastify();
48
+ });
49
+
50
+ describe('instrumentation adds custom handler', function() {
51
+ it('patches fastify', function() {
52
+ expect(server.setErrorHandler).to.have.been.calledWith(errorHandler.handler);
53
+ });
54
+ });
55
+
56
+ describe('defaultErrorHandler()', function() {
57
+ let request;
58
+ let err;
59
+
60
+ beforeEach(function() {
61
+ request = {};
62
+ err = {};
63
+ });
64
+
65
+ it('logs at error level when 500 status code', function() {
66
+ errorHandler.handler(err, request, reply);
67
+ expect(reply.log.error).calledWith({ req: request, res: reply, err });
68
+ });
69
+
70
+ it('logs at info level when <500 status code', function() {
71
+ reply.statusCode = 404;
72
+ errorHandler.handler(err, request, reply);
73
+ expect(reply.log.info).calledWith({ res: reply, err });
74
+ });
75
+ });
76
+
77
+ describe('handler()', function() {
78
+ let request;
79
+ let err;
80
+ let store;
81
+ let userHandler;
82
+
83
+ beforeEach(function() {
84
+ request = {};
85
+ err = securityException.create();
86
+ store = {
87
+ protect: {
88
+ block: sinon.stub(),
89
+ findings: {
90
+ securityException: ['block', 'cmd-injection']
91
+ }
92
+ }
93
+ };
94
+ userHandler = sinon.stub();
95
+ });
96
+
97
+ it('calls default handler when the source context is not available', function() {
98
+ errorHandler.handler(err, request, reply);
99
+ expect(core.logger.info).calledWith('source context not found; unable to handle response');
100
+ });
101
+
102
+ it('when set, calls user handler when the source context is not available', function() {
103
+ server.setErrorHandler(userHandler);
104
+ errorHandler.handler(err, request, reply);
105
+ expect(core.logger.info).calledWith('source context not found; unable to handle response');
106
+ expect(userHandler).calledWith(err, request, reply);
107
+ });
108
+
109
+ it('calls source context\'s .block() with mode and id of rule which raised error', function() {
110
+ const {
111
+ protect: {
112
+ block,
113
+ findings: { securityException }
114
+ }
115
+ } = store;
116
+ core.scopes.sources.run(store, () => {
117
+ errorHandler.handler(err, request, reply);
118
+ expect(core.logger.info).not.called;
119
+ expect(block).calledWith(...securityException);
120
+ });
121
+ });
122
+
123
+ it('calls default error handler when error is not security exception', function() {
124
+ err = {};
125
+ core.scopes.sources.run(store, () => {
126
+ errorHandler.handler(err, request, reply);
127
+ expect(core.logger.info).not.called;
128
+ expect(errorHandler.defaultErrorHandler).calledWith(err, request, reply);
129
+ });
130
+ });
131
+
132
+ it('when set, calls user handler when error is ont seecurity exception', function() {
133
+ err = {};
134
+ server.setErrorHandler(userHandler);
135
+ core.scopes.sources.run(store, () => {
136
+ errorHandler.handler(err, request, reply);
137
+ expect(core.logger.info).not.called;
138
+ expect(userHandler).calledWith(err, request, reply);
139
+ });
140
+ });
141
+ });
142
+ });
@@ -0,0 +1,2 @@
1
+ import esmHooks from '@contrast/esm-hooks';
2
+ export const { getSource, transformSource, load } = esmHooks();
@@ -0,0 +1,11 @@
1
+ import chai from 'chai';
2
+ const { expect } = chai;
3
+
4
+ describe('protect esm-loader', function() {
5
+ it('exports the ESM hooks', async function() {
6
+ const hooks = await import('./esm-loader.mjs');
7
+ expect(hooks).itself.to.respondTo('getSource');
8
+ expect(hooks).itself.to.respondTo('transformSource');
9
+ expect(hooks).itself.to.respondTo('load');
10
+ });
11
+ });
package/lib/index.d.ts ADDED
@@ -0,0 +1,36 @@
1
+ import { Core } from '@contrast/core';
2
+
3
+ // TODO
4
+ export interface Protect {
5
+ makeResponseBlocker: () => void,
6
+ makeSourceContext: () => void,
7
+ inputAnalysis: {
8
+ handleConnect: () => void,
9
+ handleRequestEnd: () => void,
10
+ handleRawBody: () => void,
11
+ handleQueryParams: () => void,
12
+ handleUrlParams: () => void,
13
+ handleCookies: () => void,
14
+ handleParsedBody: () => void,
15
+ handleFileuploadName: () => void,
16
+ install: () => void,
17
+ },
18
+ inputTracing: {
19
+ getResultsByRuleId: () => void,
20
+ handlerFactory: () => void,
21
+ handlePathTraversal: () => void,
22
+ handleCommandInjection: () => void,
23
+ handleSqlInjection: () => void,
24
+ install: () => void
25
+ }
26
+ errorHandlers: {
27
+ install: () => void,
28
+ },
29
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
30
+ agentLib: any;
31
+ version: string,
32
+ }
33
+
34
+ declare function init(core: Core): Protect;
35
+
36
+ export default init;
package/lib/index.js ADDED
@@ -0,0 +1,89 @@
1
+ 'use strict';
2
+
3
+ const agentLib = require('@contrast/agent-lib');
4
+
5
+ module.exports = function(core) {
6
+ const protect = core.protect = {
7
+ agentLib: module.exports.instantiateAgentLib(agentLib),
8
+ };
9
+
10
+ const rules = instantiateRulesFromConfig(
11
+ core.config.protect.rules,
12
+ core.config.protect.disabled_rules,
13
+ protect.agentLib,
14
+ );
15
+
16
+ protect.rules = rules;
17
+
18
+ require('./throw-security-exception')(core);
19
+ require('./make-response-blocker')(core);
20
+ require('./make-source-context')(core);
21
+ require('./input-analysis')(core);
22
+ require('./input-tracing')(core);
23
+ require('./error-handlers')(core);
24
+
25
+ const pkj = require('../package.json');
26
+ protect.version = pkj.version;
27
+
28
+ protect.install = function() {
29
+ protect.inputAnalysis.install();
30
+ protect.inputTracing.install();
31
+ protect.errorHandlers.install();
32
+ };
33
+
34
+ return protect;
35
+ };
36
+
37
+ module.exports.instantiateAgentLib = instantiateAgentLib;
38
+
39
+ /**
40
+ * Create an instance of agent-lib and attach the constants directly to it.
41
+ *
42
+ * @param {Object} lib the value returned by require('@contrast/agent-lib')
43
+ * @returns {Object} an agent-lib instance with the constants attached to it.
44
+ */
45
+ function instantiateAgentLib(lib) {
46
+ const agentLib = new lib.Agent();
47
+ for (const c in lib.constants) {
48
+ agentLib[c] = lib.constants[c];
49
+ }
50
+ if ('version' in lib) {
51
+ agentLib.version = lib.version;
52
+ }
53
+ return agentLib;
54
+ }
55
+
56
+ /**
57
+ * This function instatiates the rules as defined in the configuration into
58
+ * some structure. I'm in no way convinced or asserting that this is the right
59
+ * structure but it does get a usable definition of rules in place. The final
60
+ * structure will change based on what exactly TS sends as well as what the needs
61
+ * of the code accessing the rules, exclusions, virtual-patches, etc.
62
+ *
63
+ * @param {Object} rules the rules object in the config.protect object.
64
+ * @param {string[]} disabled array of disabled rules from config.protect
65
+ * @param {Object} agentLib the agent-lib instance
66
+ * @returns {Object} { agentLibRules, agentLibRulesMask, agentRules }
67
+ */
68
+ function instantiateRulesFromConfig(rules, disabled, agentLib) {
69
+ const agentLibRules = {};
70
+ let agentLibRulesMask = 0;
71
+ const agentRules = {};
72
+
73
+ for (const ruleId in rules) {
74
+ if (disabled.indexOf(ruleId) >= 0 || rules[ruleId].mode === 'off') {
75
+ continue;
76
+ }
77
+ // [matt] this is awkward. we should probably make each nosql-injection-x
78
+ // rule separate in the config and only convert them to 'nosql-injection'
79
+ // for reporting.
80
+ if (agentLib.RuleType[ruleId]) {
81
+ agentLibRules[ruleId] = rules[ruleId];
82
+ agentLibRulesMask = agentLibRulesMask | agentLib.RuleType[ruleId];
83
+ } else {
84
+ agentRules[ruleId] = rules[ruleId];
85
+ }
86
+ }
87
+
88
+ return { agentLibRules, agentLibRulesMask, agentRules };
89
+ }
@@ -0,0 +1,32 @@
1
+ 'use strict';
2
+
3
+ const { expect } = require('chai');
4
+ const proxyquire = require('proxyquire');
5
+
6
+ const moduleMock = (moduleName, mock) => (deps) => {
7
+ deps.protect[moduleName] = mock[moduleName];
8
+ };
9
+
10
+ describe('protect', function () {
11
+ let mocks, coreMock, protectMock, protectModule, modulesWithInstall;
12
+ beforeEach(function () {
13
+ mocks = require('../../test/mocks');
14
+ protectMock = mocks.protect();
15
+ coreMock = { protect: {}, config: mocks.config() };
16
+ protectModule = proxyquire('.', {
17
+ './make-response-blocker': moduleMock('makeResponseBlocker', protectMock),
18
+ './make-source-context': moduleMock('makeSourceContext', protectMock),
19
+ './input-analysis': moduleMock('inputAnalysis', protectMock),
20
+ './input-tracing': moduleMock('inputTracing', protectMock),
21
+ './error-handlers': moduleMock('errorHandlers', protectMock),
22
+ });
23
+ modulesWithInstall = ['inputAnalysis', 'inputTracing', 'errorHandlers'];
24
+ });
25
+
26
+ it('calls the install() the specified modules', function () {
27
+ protectModule(coreMock).install();
28
+ modulesWithInstall.forEach((module) => {
29
+ expect(protectMock[module].install).to.have.been.calledOnce;
30
+ });
31
+ });
32
+ });