@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.
Files changed (66) hide show
  1. package/bootstrap.js +2 -3
  2. package/esm.mjs +9 -35
  3. package/lib/assess/membrane/debraner.js +0 -2
  4. package/lib/assess/membrane/index.js +1 -3
  5. package/lib/assess/models/tag-range/util.js +1 -2
  6. package/lib/assess/policy/propagators.json +13 -4
  7. package/lib/assess/policy/rules.json +42 -0
  8. package/lib/assess/policy/signatures.json +18 -0
  9. package/lib/assess/policy/util.js +3 -2
  10. package/lib/assess/propagators/JSON/stringify.js +6 -11
  11. package/lib/assess/propagators/ajv/conditionals.js +0 -3
  12. package/lib/assess/propagators/ajv/json-schema-type-evaluators.js +5 -4
  13. package/lib/assess/propagators/ajv/refs.js +1 -2
  14. package/lib/assess/propagators/ajv/schema-context.js +2 -3
  15. package/lib/assess/propagators/joi/any.js +1 -1
  16. package/lib/assess/propagators/joi/object.js +1 -1
  17. package/lib/assess/propagators/joi/string-base.js +16 -3
  18. package/lib/assess/propagators/mongoose/map.js +1 -1
  19. package/lib/assess/propagators/mongoose/mixed.js +1 -1
  20. package/lib/assess/propagators/mongoose/string.js +1 -1
  21. package/lib/assess/propagators/path/common.js +38 -29
  22. package/lib/assess/propagators/path/resolve.js +1 -0
  23. package/lib/assess/propagators/sequelize/utils.js +1 -2
  24. package/lib/assess/propagators/v8/init-hooks.js +0 -1
  25. package/lib/assess/sinks/dynamo.js +65 -30
  26. package/lib/assess/static/hardcoded.js +3 -3
  27. package/lib/assess/static/read-findings-from-cache.js +40 -0
  28. package/lib/assess/technologies/index.js +12 -13
  29. package/lib/cli-rewriter/index.js +65 -6
  30. package/lib/core/async-storage/hooks/mysql.js +57 -6
  31. package/lib/core/config/options.js +12 -6
  32. package/lib/core/config/util.js +15 -33
  33. package/lib/core/exclusions/input.js +6 -1
  34. package/lib/core/express/index.js +2 -4
  35. package/lib/core/logger/debug-logger.js +2 -2
  36. package/lib/core/stacktrace.js +2 -1
  37. package/lib/hooks/http.js +81 -81
  38. package/lib/hooks/require.js +1 -0
  39. package/lib/instrumentation.js +17 -0
  40. package/lib/protect/analysis/aho-corasick.js +1 -1
  41. package/lib/protect/errors/handler-async-errors.js +66 -0
  42. package/lib/protect/input-analysis.js +7 -13
  43. package/lib/protect/listeners.js +27 -23
  44. package/lib/protect/rules/base-scanner/index.js +2 -2
  45. package/lib/protect/rules/bot-blocker/bot-blocker-rule.js +4 -2
  46. package/lib/protect/rules/cmd-injection/cmdinjection-rule.js +57 -2
  47. package/lib/protect/rules/cmd-injection-semantic-chained-commands/cmd-injection-semantic-chained-commands-rule.js +31 -2
  48. package/lib/protect/rules/cmd-injection-semantic-dangerous-paths/cmd-injection-semantic-dangerous-paths-rule.js +32 -2
  49. package/lib/protect/rules/index.js +42 -21
  50. package/lib/protect/rules/ip-denylist/ip-denylist-rule.js +2 -2
  51. package/lib/protect/rules/nosqli/nosql-injection-rule.js +104 -39
  52. package/lib/protect/rules/path-traversal/path-traversal-rule.js +3 -0
  53. package/lib/protect/rules/rule-factory.js +6 -7
  54. package/lib/protect/rules/signatures/signature.js +3 -0
  55. package/lib/protect/rules/sqli/sql-injection-rule.js +98 -5
  56. package/lib/protect/rules/sqli/sql-scanner/labels.json +0 -3
  57. package/lib/protect/rules/xss/reflected-xss-rule.js +3 -3
  58. package/lib/protect/sample-aggregator.js +65 -57
  59. package/lib/protect/service.js +709 -104
  60. package/lib/reporter/models/app-activity/sample.js +6 -0
  61. package/lib/reporter/speedracer/unknown-connection-state.js +20 -32
  62. package/lib/reporter/translations/to-protobuf/settings/assess-features.js +4 -6
  63. package/lib/reporter/ts-reporter.js +1 -1
  64. package/lib/util/get-file-type.js +43 -0
  65. package/package.json +11 -11
  66. 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 sequnce instance
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
- obj._callback = AsyncStorage.bind(obj._callback);
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[0] = AsyncStorage.bind(args[0]);
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]',
@@ -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({ script }, filename) {
152
- const configDir =
153
- require('os').platform() === 'win32'
154
- ? `${process.env['ProgramData']}\\contrast`
155
- : '/etc/contrast';
156
- const guesses = [
157
- // try to find in cwd
158
- { dir: process.cwd(), attempts: 1 },
159
-
160
- // try to find in /etc/contrast/.. or %ProgramData%\contrast\..
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(cliOptions, 'contrast_security.yaml') ||
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(`Missing required option '%s'`, name);
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
- `non-express application mistakenly registered`,
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') {
@@ -65,7 +65,8 @@ class Factory {
65
65
  const eventFrames = [];
66
66
  const clsFrames = [];
67
67
  const callsites = Factory.generateCallsites(target);
68
- /* eslint-disable complexity */
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 === 'request') {
79
- logger.debug('Received request %s, method %s', req.url, req.method);
80
- return scopes.runInRequestScope({
81
- agent,
82
- req,
83
- res,
84
- hookResponse(response, agent) {
85
- const { writeHead, end } = response;
86
- // XXX(ehden): we keep the original method available to call when handling
87
- // blocked requests because of MethodTampering or other rules whose sink
88
- // type is RESPONSE_STATUS and which can block at perimeter. We call the original
89
- // when we block to prevent recursing through blocks by setting our own 403.
90
- response[WRITE_HEAD] = writeHead;
91
- response[END] = end;
92
-
93
- /**
94
- * Patch writeHead to emit a sink event before calling writeHead
95
- *
96
- */
97
- patcher.patch(response, 'writeHead', {
98
- alwaysRun: true,
99
- name: 'write-head',
100
- patchType: PATCH_TYPES.PROTECT_SINK,
101
- pre(data) {
102
- const [statusCode, reason] = data.args;
103
- let [, , obj] = data.args;
104
- let contentType;
105
- if (typeof reason !== 'string') {
106
- obj = reason;
107
- }
108
- if (obj && typeof obj === 'object') {
109
- for (const [key, value] of Object.entries(obj)) {
110
- if (key.toLowerCase() === 'content-type') {
111
- contentType = value;
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
- patcher.patch(response, 'setHeader', {
123
- alwaysRun: true,
124
- name: 'set-header',
125
- patchType: PATCH_TYPES.ASYNC_CONTEXT,
126
- pre(data) {
127
- const [name = '', value] = data.args;
128
- if (name.toLowerCase() === 'content-type' && value) {
129
- AsyncStorage.set(KEYS.RESPONSE_CONTENT_TYPE, value);
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
- callback() {
142
- const ipEvent = createSourceEvent(
143
- req,
144
- req,
145
- res,
146
- 'socket.remoteAddress',
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
- } else {
163
- return emit.apply(self, args);
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
 
@@ -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
@@ -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);
@@ -69,7 +69,7 @@ class AhoCorasick {
69
69
  this.state = 0;
70
70
  }
71
71
 
72
- /* eslint-disable complexity */
72
+ // eslint-disable-next-line complexity
73
73
  buildAutomata() {
74
74
  // only the root state (state 0) to start with
75
75
  let stateCount = 1;
@@ -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.getApplicationContext(method);
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
- const done = InputAnalysisSensor.isDone(args);
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.getApplicationContext(method);
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
- * Gets the app context and sets the stack with
349
+ * Makes a new app context and sets the stack with
356
350
  * the proper request handler
357
351
  */
358
- static getApplicationContext(handle) {
352
+ static makeApplicationContext(handle) {
359
353
  const appContext = new ApplicationContext();
360
354
  appContext.setStack(handle);
361
355
  return appContext;