@contrast/agent 4.12.2 → 4.14.0
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.
- package/bootstrap.js +2 -3
- package/esm.mjs +9 -35
- package/lib/assess/membrane/debraner.js +0 -2
- package/lib/assess/membrane/index.js +1 -3
- package/lib/assess/models/tag-range/util.js +1 -2
- package/lib/assess/policy/propagators.json +13 -4
- package/lib/assess/policy/rules.json +42 -0
- package/lib/assess/policy/signatures.json +18 -0
- package/lib/assess/policy/util.js +3 -2
- package/lib/assess/propagators/JSON/stringify.js +6 -11
- package/lib/assess/propagators/ajv/conditionals.js +0 -3
- package/lib/assess/propagators/ajv/json-schema-type-evaluators.js +5 -4
- package/lib/assess/propagators/ajv/refs.js +1 -2
- package/lib/assess/propagators/ajv/schema-context.js +2 -3
- package/lib/assess/propagators/joi/any.js +1 -1
- package/lib/assess/propagators/joi/object.js +1 -1
- package/lib/assess/propagators/joi/string-base.js +16 -3
- package/lib/assess/propagators/mongoose/map.js +1 -1
- package/lib/assess/propagators/mongoose/mixed.js +1 -1
- package/lib/assess/propagators/mongoose/string.js +1 -1
- package/lib/assess/propagators/path/common.js +38 -29
- package/lib/assess/propagators/path/resolve.js +1 -0
- package/lib/assess/propagators/sequelize/utils.js +1 -2
- package/lib/assess/propagators/v8/init-hooks.js +0 -1
- package/lib/assess/sinks/dynamo.js +65 -30
- package/lib/assess/static/hardcoded.js +3 -3
- package/lib/assess/static/read-findings-from-cache.js +40 -0
- package/lib/assess/technologies/index.js +12 -13
- package/lib/cli-rewriter/index.js +65 -6
- package/lib/core/async-storage/hooks/mysql.js +57 -6
- package/lib/core/config/options.js +12 -6
- package/lib/core/config/util.js +15 -33
- package/lib/core/exclusions/input.js +6 -1
- package/lib/core/express/index.js +2 -4
- package/lib/core/logger/debug-logger.js +2 -2
- package/lib/core/stacktrace.js +2 -1
- package/lib/hooks/http.js +81 -81
- package/lib/hooks/require.js +1 -0
- package/lib/instrumentation.js +17 -0
- package/lib/protect/analysis/aho-corasick.js +1 -1
- package/lib/protect/errors/handler-async-errors.js +66 -0
- package/lib/protect/input-analysis.js +7 -13
- package/lib/protect/listeners.js +27 -23
- package/lib/protect/rules/base-scanner/index.js +2 -2
- package/lib/protect/rules/bot-blocker/bot-blocker-rule.js +4 -2
- package/lib/protect/rules/cmd-injection/cmdinjection-rule.js +57 -2
- package/lib/protect/rules/cmd-injection-semantic-chained-commands/cmd-injection-semantic-chained-commands-rule.js +31 -2
- package/lib/protect/rules/cmd-injection-semantic-dangerous-paths/cmd-injection-semantic-dangerous-paths-rule.js +32 -2
- package/lib/protect/rules/index.js +42 -21
- package/lib/protect/rules/ip-denylist/ip-denylist-rule.js +2 -2
- package/lib/protect/rules/nosqli/nosql-injection-rule.js +104 -39
- package/lib/protect/rules/path-traversal/path-traversal-rule.js +3 -0
- package/lib/protect/rules/rule-factory.js +6 -7
- package/lib/protect/rules/signatures/signature.js +3 -0
- package/lib/protect/rules/sqli/sql-injection-rule.js +98 -5
- package/lib/protect/rules/sqli/sql-scanner/labels.json +0 -3
- package/lib/protect/rules/xss/reflected-xss-rule.js +3 -3
- package/lib/protect/sample-aggregator.js +65 -57
- package/lib/protect/service.js +709 -104
- package/lib/reporter/models/app-activity/sample.js +6 -0
- package/lib/reporter/speedracer/unknown-connection-state.js +20 -32
- package/lib/reporter/translations/to-protobuf/settings/assess-features.js +4 -6
- package/lib/reporter/ts-reporter.js +1 -1
- package/lib/util/get-file-type.js +43 -0
- package/package.json +11 -11
- package/perf-logs.js +2 -5
|
@@ -16,13 +16,15 @@ Copyright: 2022 Contrast Security, Inc
|
|
|
16
16
|
|
|
17
17
|
const logger = require('../../logger')('contrast:async-storage:hooks');
|
|
18
18
|
const { AsyncStorage } = require('../index');
|
|
19
|
+
const { Scopes } = require('../scopes');
|
|
19
20
|
const { ASYNC_CONTEXT } = require('../../../constants').PATCH_TYPES;
|
|
20
21
|
const requireHook = require('../../../hooks/require');
|
|
21
22
|
const patcher = require('../../../hooks/patcher');
|
|
23
|
+
const { bindFnArgAtIndex } = require('./utils');
|
|
22
24
|
|
|
23
25
|
module.exports = function init() {
|
|
24
26
|
// done only to stub these fns for tests
|
|
25
|
-
const { patchSequence, patchPool } = module.exports;
|
|
27
|
+
const { patchSequence, patchPool, patchQuery } = module.exports;
|
|
26
28
|
|
|
27
29
|
// this callback _must return_ the patched function to set export
|
|
28
30
|
requireHook.resolve(
|
|
@@ -36,12 +38,20 @@ module.exports = function init() {
|
|
|
36
38
|
requireHook.resolve({ name: 'mysql', file: 'lib/Pool.js' }, (Pool) =>
|
|
37
39
|
patchPool(Pool)
|
|
38
40
|
);
|
|
41
|
+
|
|
42
|
+
requireHook.resolve(
|
|
43
|
+
{ name: 'mysql2', file: 'lib/commands/query.js' },
|
|
44
|
+
(Query) => patchQuery(Query)
|
|
45
|
+
);
|
|
39
46
|
};
|
|
40
47
|
|
|
41
48
|
module.exports.patchSequence = patchSequence;
|
|
42
49
|
module.exports.sequencePostHook = sequencePostHook;
|
|
43
50
|
module.exports.patchPool = patchPool;
|
|
44
51
|
module.exports.poolPreHook = poolPreHook;
|
|
52
|
+
module.exports.patchQuery = patchQuery;
|
|
53
|
+
module.exports.queryPreHook = queryPreHook;
|
|
54
|
+
module.exports.runInAllowAllScope = runInAllowAllScope;
|
|
45
55
|
|
|
46
56
|
/**
|
|
47
57
|
* Patches the Sequence constructor which the protocol classes inherit.
|
|
@@ -62,12 +72,15 @@ function patchSequence(sequenceCtor) {
|
|
|
62
72
|
* Typically in a constructor the data.result would be the instance. But mysql
|
|
63
73
|
* has the subclasses e.g. Query, do Sequence.call(this, cb). In this case the
|
|
64
74
|
* data.obj is the instance.
|
|
65
|
-
* @param {object} data.obj
|
|
75
|
+
* @param {object} data.obj sequence instance
|
|
66
76
|
*/
|
|
67
|
-
function sequencePostHook({ obj }) {
|
|
77
|
+
function sequencePostHook({ obj, funcKey: identifier }) {
|
|
78
|
+
// done only to stub this fn for tests
|
|
79
|
+
const { runInAllowAllScope } = module.exports;
|
|
68
80
|
try {
|
|
69
81
|
if (obj && obj._callback && typeof obj._callback === 'function') {
|
|
70
|
-
|
|
82
|
+
const cb = obj._callback;
|
|
83
|
+
obj._callback = AsyncStorage.bind(runInAllowAllScope(cb, identifier));
|
|
71
84
|
}
|
|
72
85
|
AsyncStorage.getNamespace().bindEmitter(obj);
|
|
73
86
|
} catch (err) {
|
|
@@ -75,6 +88,13 @@ function sequencePostHook({ obj }) {
|
|
|
75
88
|
}
|
|
76
89
|
}
|
|
77
90
|
|
|
91
|
+
// Created only for the purpose of testing
|
|
92
|
+
function runInAllowAllScope (cb, identifier) {
|
|
93
|
+
return function (...args) {
|
|
94
|
+
return Scopes.runInAllowAllScope(() => cb.call(this, ...args), identifier);
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
78
98
|
/**
|
|
79
99
|
* Patches Pool.prototype.getConnection.
|
|
80
100
|
* @param {function} poolCtor Pool constructor fn
|
|
@@ -92,12 +112,43 @@ function patchPool(poolCtor) {
|
|
|
92
112
|
* Binds callback (when present) to cls.
|
|
93
113
|
* @param {object} data.args getConnection arguments
|
|
94
114
|
*/
|
|
95
|
-
function poolPreHook({ args }) {
|
|
115
|
+
function poolPreHook({ args, funcKey: identifier }) {
|
|
96
116
|
try {
|
|
97
117
|
if (args.length && typeof args[0] === 'function') {
|
|
98
|
-
args
|
|
118
|
+
bindFnArgAtIndex({ args, idx: 0, identifier });
|
|
99
119
|
}
|
|
100
120
|
} catch (err) {
|
|
101
121
|
logger.warn('Unable to patch Pool.prototype.getConnection: %o', err);
|
|
102
122
|
}
|
|
103
123
|
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Patches the Query constructor.
|
|
127
|
+
* This _must return_ the patched value to set the export in require hook.
|
|
128
|
+
* @param {function} queryCtor Query constructor fn
|
|
129
|
+
* @returns {function}
|
|
130
|
+
*/
|
|
131
|
+
function patchQuery(queryCtor) {
|
|
132
|
+
return patcher.patch(queryCtor, {
|
|
133
|
+
name: 'mysql2.lib/commands/query.js.Query',
|
|
134
|
+
patchType: ASYNC_CONTEXT,
|
|
135
|
+
alwaysRun: true,
|
|
136
|
+
pre: queryPreHook
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Binds callback (when present) to the context the constructor is called in..
|
|
142
|
+
* @param {object} data the argument for the preHook
|
|
143
|
+
* @param {object} data.args the arguments passed to the Query constructor
|
|
144
|
+
* @param {object} data.funcKey Contrast funcKey identifier for a hooked Query function
|
|
145
|
+
*/
|
|
146
|
+
function queryPreHook({ args, funcKey: identifier }) {
|
|
147
|
+
try {
|
|
148
|
+
if (args.length && args[1] && typeof args[1] === 'function') {
|
|
149
|
+
bindFnArgAtIndex({ args, idx: 1, identifier });
|
|
150
|
+
}
|
|
151
|
+
} catch (err) {
|
|
152
|
+
logger.warn('Unable to patch Query constructor: %o', err);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -411,6 +411,12 @@ const agent = [
|
|
|
411
411
|
fn: castBoolean,
|
|
412
412
|
desc: 'do agent-native input analysis prior to any external analysis',
|
|
413
413
|
},
|
|
414
|
+
{
|
|
415
|
+
name: 'agent.node.analysis_log_dir',
|
|
416
|
+
arg: '<path>',
|
|
417
|
+
default: '.',
|
|
418
|
+
desc: 'directory to use for the native input analysis log file'
|
|
419
|
+
},
|
|
414
420
|
{
|
|
415
421
|
name: 'agent.node.unsafe.deadzones',
|
|
416
422
|
arg: '<modules>',
|
|
@@ -466,12 +472,6 @@ const agent = [
|
|
|
466
472
|
fn: parseNum,
|
|
467
473
|
desc: 'set limit for stack trace size (larger limits will improve accuracy but increase memory usage)',
|
|
468
474
|
},
|
|
469
|
-
{
|
|
470
|
-
name: 'agent.trust_custom_validators',
|
|
471
|
-
arg: '<trust-custom-validators>',
|
|
472
|
-
default: false,
|
|
473
|
-
desc: `trust incoming strings when they pass custom validators (Mongoose, Joi)`,
|
|
474
|
-
},
|
|
475
475
|
{
|
|
476
476
|
name: 'agent.traverse_and_track',
|
|
477
477
|
arg: '<traverse-and-track>',
|
|
@@ -708,6 +708,12 @@ const assess = [
|
|
|
708
708
|
fn: castBoolean,
|
|
709
709
|
desc: 'if false, disable assess for this agent. A restart is required to re-enable',
|
|
710
710
|
},
|
|
711
|
+
{
|
|
712
|
+
name: 'assess.trust_custom_validators',
|
|
713
|
+
arg: '<trust-custom-validators>',
|
|
714
|
+
default: false,
|
|
715
|
+
desc: 'trust incoming strings when they pass custom validators (Mongoose, Joi)',
|
|
716
|
+
},
|
|
711
717
|
{
|
|
712
718
|
name: 'assess.enable_preflight',
|
|
713
719
|
arg: '[false]',
|
package/lib/core/config/util.js
CHANGED
|
@@ -15,6 +15,7 @@ Copyright: 2022 Contrast Security, Inc
|
|
|
15
15
|
'use strict';
|
|
16
16
|
const _ = require('lodash');
|
|
17
17
|
|
|
18
|
+
const os = require('os');
|
|
18
19
|
const process = require('process');
|
|
19
20
|
const path = require('path');
|
|
20
21
|
const fs = require('fs');
|
|
@@ -24,7 +25,6 @@ const stringify = require('json-stable-stringify');
|
|
|
24
25
|
const common = require('./options');
|
|
25
26
|
const configOptions = common.options;
|
|
26
27
|
const { configPathEnvVars } = common;
|
|
27
|
-
const fileFinder = require('../../util/file-finder');
|
|
28
28
|
const util = module.exports;
|
|
29
29
|
|
|
30
30
|
/**
|
|
@@ -143,37 +143,23 @@ class Config {
|
|
|
143
143
|
|
|
144
144
|
/**
|
|
145
145
|
* Find location of config given options and name of config file
|
|
146
|
-
* @param {Object} cliOptions
|
|
147
|
-
* @param {string} cliOptions.script
|
|
148
|
-
* @param {string} filename
|
|
149
146
|
* @return {string|void} path, if valid
|
|
150
147
|
*/
|
|
151
|
-
function checkConfigPath(
|
|
152
|
-
const configDir =
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
{ dir: configDir, attempts: 1 },
|
|
162
|
-
|
|
163
|
-
// The directory of the application under test such as node_modules/mocha/bin or server.
|
|
164
|
-
{ dir: path.dirname(script) },
|
|
165
|
-
|
|
166
|
-
// The directory of this Contrast agent module, assuming it
|
|
167
|
-
// came packaged with an enterprise-wide contrast.json.
|
|
168
|
-
{ dir: path.resolve(__dirname, '..', '..'), attempts: 1 }
|
|
169
|
-
];
|
|
170
|
-
|
|
171
|
-
for (const guess of guesses) {
|
|
172
|
-
const configPath = fileFinder.findFile(guess.dir, filename, guess.attempts);
|
|
173
|
-
if (configPath) return configPath;
|
|
148
|
+
function checkConfigPath() {
|
|
149
|
+
const configDir = os.platform() === 'win32'
|
|
150
|
+
? `${process.env['ProgramData']}\\contrast`
|
|
151
|
+
: '/etc/contrast';
|
|
152
|
+
|
|
153
|
+
for (const dir of [process.cwd(), configDir]) {
|
|
154
|
+
const checkPath = path.resolve(dir, 'contrast_security.yaml');
|
|
155
|
+
if (fs.existsSync(checkPath)) {
|
|
156
|
+
return checkPath;
|
|
157
|
+
}
|
|
174
158
|
}
|
|
159
|
+
return;
|
|
175
160
|
}
|
|
176
161
|
|
|
162
|
+
|
|
177
163
|
/**
|
|
178
164
|
* @param {Object} cliOptions
|
|
179
165
|
* @param {string} cliOptions.script
|
|
@@ -185,11 +171,7 @@ function getConfigPath(cliOptions) {
|
|
|
185
171
|
cliOptions.configFile ||
|
|
186
172
|
process.env[configPathEnvVars.path] ||
|
|
187
173
|
process.env[configPathEnvVars.deprecated] ||
|
|
188
|
-
checkConfigPath(
|
|
189
|
-
checkConfigPath(cliOptions, 'contrast_security.yml') ||
|
|
190
|
-
checkConfigPath(cliOptions, 'contrast.yaml') ||
|
|
191
|
-
checkConfigPath(cliOptions, 'contrast.yml') ||
|
|
192
|
-
checkConfigPath(cliOptions, 'contrast.json')
|
|
174
|
+
checkConfigPath()
|
|
193
175
|
);
|
|
194
176
|
}
|
|
195
177
|
|
|
@@ -312,7 +294,7 @@ function mergeCliOptions(cliOptions, logger) {
|
|
|
312
294
|
// set from default
|
|
313
295
|
if (value === undefined) {
|
|
314
296
|
if (required) {
|
|
315
|
-
logger.error(
|
|
297
|
+
logger.error('Missing required option \'%s\'', name);
|
|
316
298
|
return options;
|
|
317
299
|
}
|
|
318
300
|
|
|
@@ -29,7 +29,8 @@ const generalizedInputTypes = {
|
|
|
29
29
|
[INPUT_TYPES.COOKIE_VALUE]: EXCLUSION_INPUT_TYPES.COOKIE,
|
|
30
30
|
[INPUT_TYPES.HEADER]: EXCLUSION_INPUT_TYPES.HEADER,
|
|
31
31
|
[INPUT_TYPES.PARAMETER_NAME]: EXCLUSION_INPUT_TYPES.PARAMETER,
|
|
32
|
-
[INPUT_TYPES.PARAMETER_VALUE]: EXCLUSION_INPUT_TYPES.PARAMETER
|
|
32
|
+
[INPUT_TYPES.PARAMETER_VALUE]: EXCLUSION_INPUT_TYPES.PARAMETER,
|
|
33
|
+
[INPUT_TYPES.URL_PARAMETER]: EXCLUSION_INPUT_TYPES.PARAMETER
|
|
33
34
|
};
|
|
34
35
|
const { BODY, PARAMETER, QUERYSTRING } = EXCLUSION_INPUT_TYPES;
|
|
35
36
|
|
|
@@ -47,6 +48,10 @@ class InputExclusion extends UrlExclusion {
|
|
|
47
48
|
this.inputName = dtm.inputName;
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
shouldExclude(ruleId, type, name) {
|
|
52
|
+
return this.appliesToProtectRule(ruleId) && this.appliesToInputType(type) && this.matches(name);
|
|
53
|
+
}
|
|
54
|
+
|
|
50
55
|
appliesToInputType(type) {
|
|
51
56
|
return (
|
|
52
57
|
this.inputType === InputExclusion.generalizeInputType(type) ||
|
|
@@ -237,7 +237,7 @@ class ExpressFramework {
|
|
|
237
237
|
|
|
238
238
|
if (!app || !app.defaultConfiguration) {
|
|
239
239
|
logger.error(
|
|
240
|
-
|
|
240
|
+
'non-express application mistakenly registered',
|
|
241
241
|
new Error().stack,
|
|
242
242
|
);
|
|
243
243
|
return;
|
|
@@ -336,9 +336,7 @@ class ExpressFramework {
|
|
|
336
336
|
}, 'textParser');
|
|
337
337
|
|
|
338
338
|
this.useAfter(function ContrastBodyParsed(req, res, next) {
|
|
339
|
-
agentEmitter.emit(EVENTS.BODY_PARSED, req, res,
|
|
340
|
-
type: INPUT_TYPES.BODY,
|
|
341
|
-
});
|
|
339
|
+
agentEmitter.emit(EVENTS.BODY_PARSED, req, res, INPUT_TYPES.BODY);
|
|
342
340
|
next();
|
|
343
341
|
}, 'urlencodedParser');
|
|
344
342
|
|
|
@@ -311,8 +311,8 @@ class DebugLogFactory {
|
|
|
311
311
|
logger.console = this.mute
|
|
312
312
|
? noop
|
|
313
313
|
: function() {
|
|
314
|
-
return console.log(...arguments); // eslint-disable-line
|
|
315
|
-
|
|
314
|
+
return console.log(...arguments); // eslint-disable-line prefer-rest-params
|
|
315
|
+
};
|
|
316
316
|
|
|
317
317
|
[...levelNames, 'console'].forEach((level) => {
|
|
318
318
|
if (level === 'console') {
|
package/lib/core/stacktrace.js
CHANGED
|
@@ -65,7 +65,8 @@ class Factory {
|
|
|
65
65
|
const eventFrames = [];
|
|
66
66
|
const clsFrames = [];
|
|
67
67
|
const callsites = Factory.generateCallsites(target);
|
|
68
|
-
|
|
68
|
+
|
|
69
|
+
// eslint-disable-next-line complexity
|
|
69
70
|
ret = (callsites || []).reduce((acc, callsite) => {
|
|
70
71
|
if (Factory.isCallsiteValid(callsite)) {
|
|
71
72
|
const frame = Factory.makeFrame(callsite);
|
package/lib/hooks/http.js
CHANGED
|
@@ -75,93 +75,93 @@ const hookServer = (server, agent, id) => {
|
|
|
75
75
|
const self = this;
|
|
76
76
|
const [event, req, res] = args;
|
|
77
77
|
|
|
78
|
-
if (event
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
78
|
+
if (event !== 'request') {
|
|
79
|
+
return emit.apply(self, args);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
logger.debug('Received request %s, method %s', req.url, req.method);
|
|
83
|
+
return scopes.runInRequestScope({
|
|
84
|
+
agent,
|
|
85
|
+
req,
|
|
86
|
+
res,
|
|
87
|
+
hookResponse(response, agent) {
|
|
88
|
+
const { writeHead, end } = response;
|
|
89
|
+
// XXX(ehden): we keep the original method available to call when handling
|
|
90
|
+
// blocked requests because of MethodTampering or other rules whose sink
|
|
91
|
+
// type is RESPONSE_STATUS and which can block at perimeter. We call the original
|
|
92
|
+
// when we block to prevent recursing through blocks by setting our own 403.
|
|
93
|
+
response[WRITE_HEAD] = writeHead;
|
|
94
|
+
response[END] = end;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Patch writeHead to emit a sink event before calling writeHead
|
|
98
|
+
*
|
|
99
|
+
*/
|
|
100
|
+
patcher.patch(response, 'writeHead', {
|
|
101
|
+
alwaysRun: true,
|
|
102
|
+
name: 'write-head',
|
|
103
|
+
patchType: PATCH_TYPES.PROTECT_SINK,
|
|
104
|
+
pre(data) {
|
|
105
|
+
const [statusCode, reason] = data.args;
|
|
106
|
+
let [, , obj] = data.args;
|
|
107
|
+
let contentType;
|
|
108
|
+
if (typeof reason !== 'string') {
|
|
109
|
+
obj = reason;
|
|
110
|
+
}
|
|
111
|
+
if (obj && typeof obj === 'object') {
|
|
112
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
113
|
+
if (key.toLowerCase() === 'content-type') {
|
|
114
|
+
contentType = value;
|
|
113
115
|
}
|
|
114
116
|
}
|
|
115
|
-
if (contentType) {
|
|
116
|
-
AsyncStorage.set(KEYS.RESPONSE_CONTENT_TYPE, contentType);
|
|
117
|
-
}
|
|
118
|
-
emitSinkEvent(statusCode, SINK_TYPES.RESPONSE_STATUS, id, false);
|
|
119
117
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
118
|
+
if (contentType) {
|
|
119
|
+
AsyncStorage.set(KEYS.RESPONSE_CONTENT_TYPE, contentType);
|
|
120
|
+
}
|
|
121
|
+
emitSinkEvent(statusCode, SINK_TYPES.RESPONSE_STATUS, id, false);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
patcher.patch(response, 'setHeader', {
|
|
126
|
+
alwaysRun: true,
|
|
127
|
+
name: 'set-header',
|
|
128
|
+
patchType: PATCH_TYPES.ASYNC_CONTEXT,
|
|
129
|
+
pre(data) {
|
|
130
|
+
const [name = '', value] = data.args;
|
|
131
|
+
if (name.toLowerCase() === 'content-type' && value) {
|
|
132
|
+
AsyncStorage.set(KEYS.RESPONSE_CONTENT_TYPE, value);
|
|
131
133
|
}
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
// special HTTP logging for responses.
|
|
135
|
-
if (agent.config.agent.logger.log_outbound_http) {
|
|
136
|
-
['end', 'writeHead', 'write'].forEach((method) => {
|
|
137
|
-
logHook(response, method);
|
|
138
|
-
});
|
|
139
134
|
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
INPUT_TYPES.IP,
|
|
148
|
-
id
|
|
149
|
-
);
|
|
150
|
-
agentEmitter.emit('http.requestStart', req, res, ipEvent);
|
|
151
|
-
|
|
152
|
-
emitSourceEvent(req, req, res, 'method', INPUT_TYPES.METHOD, id);
|
|
153
|
-
emitSourceEvent(req, req, res, 'headers', INPUT_TYPES.HEADER, id);
|
|
154
|
-
emitSourceEvent(req, req, res, 'url', INPUT_TYPES.URL, id);
|
|
155
|
-
|
|
156
|
-
const rv = emit.apply(self, args);
|
|
157
|
-
|
|
158
|
-
agentEmitter.emit('http.requestEnd', req, res);
|
|
159
|
-
return rv;
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// special HTTP logging for responses.
|
|
138
|
+
if (agent.config.agent.logger.log_outbound_http) {
|
|
139
|
+
['end', 'writeHead', 'write'].forEach((method) => {
|
|
140
|
+
logHook(response, method);
|
|
141
|
+
});
|
|
160
142
|
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
143
|
+
},
|
|
144
|
+
callback() {
|
|
145
|
+
const ipEvent = createSourceEvent(
|
|
146
|
+
req,
|
|
147
|
+
req,
|
|
148
|
+
res,
|
|
149
|
+
'socket.remoteAddress',
|
|
150
|
+
INPUT_TYPES.IP,
|
|
151
|
+
id
|
|
152
|
+
);
|
|
153
|
+
agentEmitter.emit('http.requestStart', req, res, ipEvent);
|
|
154
|
+
|
|
155
|
+
emitSourceEvent(req, req, res, 'method', INPUT_TYPES.METHOD, id);
|
|
156
|
+
emitSourceEvent(req, req, res, 'headers', INPUT_TYPES.HEADER, id);
|
|
157
|
+
emitSourceEvent(req, req, res, 'url', INPUT_TYPES.URL, id);
|
|
158
|
+
|
|
159
|
+
const rv = emit.apply(self, args);
|
|
160
|
+
|
|
161
|
+
agentEmitter.emit('http.requestEnd', req, res);
|
|
162
|
+
return rv;
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
165
|
};
|
|
166
166
|
};
|
|
167
167
|
|
package/lib/hooks/require.js
CHANGED
|
@@ -22,6 +22,7 @@ const logger = require('../core/logger')('hooks:require');
|
|
|
22
22
|
class ModuleHook {
|
|
23
23
|
constructor() {
|
|
24
24
|
this.reqHook = new RequireHook(logger);
|
|
25
|
+
this.reqHook.install();
|
|
25
26
|
|
|
26
27
|
// RequireHook takes care of hooking but we still need to patch require to
|
|
27
28
|
// emit 'require' events
|
package/lib/instrumentation.js
CHANGED
|
@@ -102,6 +102,7 @@ function assessModeFeatures(agent) {
|
|
|
102
102
|
if (agent.isInAssessMode()) {
|
|
103
103
|
logger.debug('initializing assess mode features');
|
|
104
104
|
require('./assess/policy/init').init(agent);
|
|
105
|
+
require('./assess/static/read-findings-from-cache')(agent);
|
|
105
106
|
require('./hooks/encoding')();
|
|
106
107
|
require('./hooks/object-to-primitive')();
|
|
107
108
|
require('./hooks/array')();
|
|
@@ -121,6 +122,22 @@ function protectModeFeatures({ agent, reporter }) {
|
|
|
121
122
|
logger.debug('initializing protect mode features');
|
|
122
123
|
|
|
123
124
|
require('./protect')();
|
|
125
|
+
|
|
126
|
+
// if it's native_input_analysis then use agent-lib
|
|
127
|
+
if (agent.config.agent.node.native_input_analysis) {
|
|
128
|
+
const lib = require('@contrast/agent-lib');
|
|
129
|
+
// needs the || '.' for testing...
|
|
130
|
+
const logDir = agent.config.agent.node.analysis_log_dir || '.';
|
|
131
|
+
const agentLib = new lib.Agent(
|
|
132
|
+
{ enableLogging: true, logDir, logLevel: "INFO" }
|
|
133
|
+
);
|
|
134
|
+
// attach the constants so lib.Agent() isn't exposed.
|
|
135
|
+
for (const c in lib.constants) {
|
|
136
|
+
agentLib[c] = lib.constants[c];
|
|
137
|
+
}
|
|
138
|
+
agent.agentLib = agentLib;
|
|
139
|
+
}
|
|
140
|
+
|
|
124
141
|
const protectService = require('./protect/listeners')(agent, reporter);
|
|
125
142
|
const InputAnalysisSensor = require('./protect/input-analysis');
|
|
126
143
|
require('./protect/sources')(agent);
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
Copyright: 2022 Contrast Security, Inc
|
|
3
|
+
Contact: support@contrastsecurity.com
|
|
4
|
+
License: Commercial
|
|
5
|
+
|
|
6
|
+
NOTICE: This Software and the patented inventions embodied within may only be
|
|
7
|
+
used as part of Contrast Security’s commercial offerings. Even though it is
|
|
8
|
+
made available through public repositories, use of this Software is subject to
|
|
9
|
+
the applicable End User Licensing Agreement found at
|
|
10
|
+
https://www.contrastsecurity.com/enduser-terms-0317a or as otherwise agreed
|
|
11
|
+
between Contrast Security and the End User. The Software may not be reverse
|
|
12
|
+
engineered, modified, repackaged, sold, redistributed or otherwise used in a
|
|
13
|
+
way not consistent with the End User License Agreement.
|
|
14
|
+
*/
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
const { KEYS, AsyncStorage } = require('../../core/async-storage');
|
|
18
|
+
const agentEmitter = require('../../agent-emitter');
|
|
19
|
+
const isContrastError = require('../../util/is-contrast-error');
|
|
20
|
+
const process = require('process');
|
|
21
|
+
|
|
22
|
+
const handledErrors = new WeakSet();
|
|
23
|
+
|
|
24
|
+
module.exports.install = function() {
|
|
25
|
+
const originalOn = process.on;
|
|
26
|
+
process.on = function (name, ...args) {
|
|
27
|
+
if (name === 'unhandledRejection') {
|
|
28
|
+
return originalOn.call(
|
|
29
|
+
this,
|
|
30
|
+
name,
|
|
31
|
+
...args.map(
|
|
32
|
+
(origHandler) =>
|
|
33
|
+
function (reason, promise) {
|
|
34
|
+
if (handledErrors.has(reason)) {
|
|
35
|
+
return; // skip
|
|
36
|
+
}
|
|
37
|
+
return origHandler(reason, promise);
|
|
38
|
+
},
|
|
39
|
+
),
|
|
40
|
+
);
|
|
41
|
+
} else {
|
|
42
|
+
return originalOn.call(this, name, ...args);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
process.on('unhandledRejection', function asyncErrorHandling(error) {
|
|
47
|
+
handlerAsyncErrors(error);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
function handlerAsyncErrors(error) {
|
|
51
|
+
let handled = null;
|
|
52
|
+
if (isContrastError(error)) {
|
|
53
|
+
const res = AsyncStorage.fromException(error, KEYS.RES);
|
|
54
|
+
handled = agentEmitter.handleError(error, res);
|
|
55
|
+
}
|
|
56
|
+
if (handled) {
|
|
57
|
+
handledErrors.add(error);
|
|
58
|
+
} else {
|
|
59
|
+
console.warn(
|
|
60
|
+
`An Unhandled Rejection has been caught by the Contrast Security node-agent instrumentation. Error: ${error}`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
|
|
@@ -35,10 +35,7 @@ class InputAnalysisSensor {
|
|
|
35
35
|
* for instrumenting both `http` and `https` modules when they load.
|
|
36
36
|
*/
|
|
37
37
|
install() {
|
|
38
|
-
if (
|
|
39
|
-
this.installed ||
|
|
40
|
-
!this.agent.config.agent.node.speedracer_input_analysis
|
|
41
|
-
) {
|
|
38
|
+
if (this.installed) {
|
|
42
39
|
return;
|
|
43
40
|
}
|
|
44
41
|
this.installed = true;
|
|
@@ -140,7 +137,7 @@ class InputAnalysisSensor {
|
|
|
140
137
|
|
|
141
138
|
let permit = true;
|
|
142
139
|
try {
|
|
143
|
-
const appContext = InputAnalysisSensor.
|
|
140
|
+
const appContext = InputAnalysisSensor.makeApplicationContext(method);
|
|
144
141
|
permit = await this.service.analyzeRequest({
|
|
145
142
|
meta,
|
|
146
143
|
req,
|
|
@@ -249,10 +246,6 @@ class InputAnalysisSensor {
|
|
|
249
246
|
return sizeInMb >= this.maxSize;
|
|
250
247
|
}
|
|
251
248
|
|
|
252
|
-
static isDone(args) {
|
|
253
|
-
return args[0] === 'end';
|
|
254
|
-
}
|
|
255
|
-
|
|
256
249
|
/**
|
|
257
250
|
* We defer calling the original req.emit of data chunks
|
|
258
251
|
* and end until SR analyzes the request body.
|
|
@@ -266,7 +259,8 @@ class InputAnalysisSensor {
|
|
|
266
259
|
*/
|
|
267
260
|
async processBodyAndEmit(meta, context, req, res) {
|
|
268
261
|
const { args, method } = context;
|
|
269
|
-
|
|
262
|
+
// the request is done when the end event is emitted
|
|
263
|
+
const done = args[0] === 'end';
|
|
270
264
|
|
|
271
265
|
let permit = true;
|
|
272
266
|
|
|
@@ -274,7 +268,7 @@ class InputAnalysisSensor {
|
|
|
274
268
|
let appContext;
|
|
275
269
|
|
|
276
270
|
if (done) {
|
|
277
|
-
appContext = InputAnalysisSensor.
|
|
271
|
+
appContext = InputAnalysisSensor.makeApplicationContext(method);
|
|
278
272
|
|
|
279
273
|
permit = await this.service.analyzeRequestStream({
|
|
280
274
|
meta,
|
|
@@ -352,10 +346,10 @@ class InputAnalysisSensor {
|
|
|
352
346
|
}
|
|
353
347
|
|
|
354
348
|
/**
|
|
355
|
-
*
|
|
349
|
+
* Makes a new app context and sets the stack with
|
|
356
350
|
* the proper request handler
|
|
357
351
|
*/
|
|
358
|
-
static
|
|
352
|
+
static makeApplicationContext(handle) {
|
|
359
353
|
const appContext = new ApplicationContext();
|
|
360
354
|
appContext.setStack(handle);
|
|
361
355
|
return appContext;
|