@contrast/agent 4.22.1 → 4.24.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.
@@ -112,7 +112,17 @@ class BaseEvent {
112
112
  this._expanded = true;
113
113
  this.thread = process.pid;
114
114
  if (this.source === 'P') {
115
- this.source = 'P0';
115
+ const numArgs = this.context.hasArgsTracked.length;
116
+ if (numArgs === 1) {
117
+ this.source = 'P0'
118
+ } else {
119
+ this.source = '';
120
+ for (let i = 0; i < numArgs; i++) {
121
+ if (this.context.hasArgsTracked[i]) {
122
+ this.source += `P${i},`;
123
+ }
124
+ }
125
+ }
116
126
  }
117
127
 
118
128
  if (this.signature) {
@@ -103,6 +103,25 @@ module.exports = class CallContext {
103
103
  return !!(str && typeof str === 'object' && str[PROXY_TARGET]);
104
104
  }
105
105
 
106
+ static hasTrackedArg(arg, iteration = 0) {
107
+ if (tracker.getData(arg)) {
108
+ return true;
109
+ }
110
+
111
+ if (arg && typeof arg === 'object') {
112
+
113
+ for (const key in arg) {
114
+ if (tracker.getData(arg[key])) {
115
+ return true;
116
+ }
117
+ if (arg[key] && typeof arg[key] === 'object' && iteration < 100) {
118
+ return CallContext.hasTrackedArg(arg[key], iteration += 1);
119
+ }
120
+ }
121
+ }
122
+ return false;
123
+ }
124
+
106
125
  static getDisplayRange(arg, orgArg = arg, iteration = 0) {
107
126
  if (tracker.getData(arg)) {
108
127
  return new TagRange(0, arg.length - 1, 'untrusted');
@@ -144,6 +163,7 @@ module.exports = class CallContext {
144
163
  set args(args) {
145
164
  this.__args = args.map(CallContext.valueString);
146
165
  this.argsTracked = args.map((arg) => CallContext.isTracked(arg));
166
+ this.hasArgsTracked = args.map((arg) => CallContext.hasTrackedArg(arg));
147
167
  this.argsDisplayRanges = args.map((arg) => CallContext.getDisplayRange(arg));
148
168
  }
149
169
 
@@ -331,7 +331,9 @@ module.exports.handle = function() {
331
331
  // always exist, even if an empty array, for production code.
332
332
  if (metadata.sourceEvents && !metadata.sourceEvents.length) {
333
333
  const { sourceEvents, braned } = data.metadata;
334
- if (braned) {
334
+ // If it is a source membrane (could be deserialization membrane), then
335
+ // we need to get a source event.
336
+ if (braned && braned.membrane && braned.membrane.makeReqSourceEvent) {
335
337
  sourceEvents.push(
336
338
  braned.membrane.makeReqSourceEvent(data.result.length)
337
339
  );
@@ -37,6 +37,7 @@ const requiredTags = ['untrusted'];
37
37
  const trackSchemaCommands = {
38
38
  'scan': {
39
39
  attributes: [
40
+ 'FilterExpression',
40
41
  'ExclusiveStartKey',
41
42
  'ScanFilter'
42
43
  ]
@@ -44,7 +44,7 @@ const KEYS = {
44
44
  REQUEST: 'request',
45
45
  REQ: 'req',
46
46
  RES: 'res',
47
- RESPONSE_CONTENT_TYPE: 'res.contentType',
47
+ RESPONSE_CONTENT_TYPE: 'response.contentType',
48
48
  ROUTE: 'route',
49
49
  RULES: 'defend.rules',
50
50
  SAMPLES: 'defend.samples',
@@ -59,6 +59,7 @@ class ExpressFramework {
59
59
  // Express middleware error handler.
60
60
  this.errorHandler = ExpressFramework.ContrastErrorHandler.bind(this);
61
61
  moduleHook.resolve({ name: 'express' }, this.hookExpress.bind(this));
62
+ moduleHook.resolve({ name: 'body-parser' }, this.hookBodyParser.bind(this));
62
63
  }
63
64
 
64
65
  /**
@@ -227,6 +228,80 @@ class ExpressFramework {
227
228
  agentEmitter.on(HTTP_EVENTS.SERVER_LISTEN, this.onServerListen.bind(this));
228
229
  }
229
230
 
231
+ hookBodyParser(bodyParser) {
232
+ const instrumentation = this;
233
+ const origBodyParser = bodyParser;
234
+
235
+ const { json: origJson, raw: origRaw, text: origText, urlencoded: origUrlencoded } = bodyParser;
236
+ const fnArr = [
237
+ {
238
+ key: 'json',
239
+ original: origJson,
240
+ },
241
+ {
242
+ key: 'raw',
243
+ original: origRaw,
244
+ },
245
+ {
246
+ key: 'text',
247
+ original: origText,
248
+ },
249
+ {
250
+ key: 'urlencoded',
251
+ original: origUrlencoded,
252
+ }
253
+ ];
254
+
255
+ bodyParser = function bodyParser(...args) {
256
+ const parser = origBodyParser(...args);
257
+ const hookedParser = function(req, res, next) {
258
+ parser(req, res, instrumentation.contrastNext(req, res, next, 'bodyParser'));
259
+ };
260
+
261
+ Object.defineProperty(hookedParser, 'name', {
262
+ value: 'bodyParser'
263
+ });
264
+
265
+ return hookedParser;
266
+ };
267
+
268
+ fnArr.forEach((fn) => {
269
+ const fnName = `bodyParser.${fn.key}`;
270
+ function contrastHooked(...args) {
271
+ const parser = fn.original(...args);
272
+ const hookedParser = function (req, res, next) {
273
+ parser(req, res, instrumentation.contrastNext(req, res, next, fnName));
274
+ };
275
+
276
+ Object.defineProperty(hookedParser, 'name', {
277
+ value: `${fn.key}Parser`
278
+ });
279
+
280
+ return hookedParser;
281
+ }
282
+
283
+ Object.defineProperty(bodyParser, fn.key, {
284
+ configurable: true,
285
+ enumerable: true,
286
+ get: () => contrastHooked,
287
+ });
288
+ });
289
+
290
+ return bodyParser;
291
+ }
292
+
293
+ contrastNext(req, res, origNext, fnName) {
294
+ return function next() {
295
+ if (fnName == 'bodyParser.json') {
296
+ agentEmitter.emit(EVENTS.BODY_PARSED, req, res, INPUT_TYPES.JSON_VALUE);
297
+ } else {
298
+ agentEmitter.emit(EVENTS.BODY_PARSED, req, res, INPUT_TYPES.BODY);
299
+ }
300
+
301
+ origNext();
302
+ };
303
+ }
304
+
230
305
  onServerListen(args, server) {
231
306
  if (!this.serversSeen.has(server)) {
232
307
  logger.debug('ignore server.listen: non-express handler');
@@ -316,35 +391,6 @@ class ExpressFramework {
316
391
  next();
317
392
  }, 'multerMiddleware');
318
393
 
319
- // ... body-parser ...............................................
320
- this.useAfter(function ContrastRawBodyParsed(req, res, next) {
321
- agentEmitter.emit(EVENTS.BODY_PARSED, req, res, INPUT_TYPES.BODY);
322
- next();
323
- }, 'rawParser');
324
-
325
- // ... bodyParser in Sails Framework ............................
326
- this.useAfter(function ContrastBodyParsed(req, res, next) {
327
- if (req._sails && req.body) {
328
- agentEmitter.emit(EVENTS.BODY_PARSED, req, res, INPUT_TYPES.BODY);
329
- }
330
- next();
331
- }, '_parseHTTPBody');
332
-
333
- this.useAfter(function ContrastTextBodyParsed(req, res, next) {
334
- agentEmitter.emit(EVENTS.BODY_PARSED, req, res, INPUT_TYPES.BODY);
335
- next();
336
- }, 'textParser');
337
-
338
- this.useAfter(function ContrastBodyParsed(req, res, next) {
339
- agentEmitter.emit(EVENTS.BODY_PARSED, req, res, INPUT_TYPES.BODY);
340
- next();
341
- }, 'urlencodedParser');
342
-
343
- this.useAfter(function ContrastJSONParsed(req, res, next) {
344
- agentEmitter.emit(EVENTS.BODY_PARSED, req, res, INPUT_TYPES.JSON_VALUE);
345
- next();
346
- }, 'jsonParser');
347
-
348
394
  // ... cookie-parser .............................................
349
395
  this.useAfter(function ContrastCookiesParsed(req, res, next) {
350
396
  agentEmitter.emit(
@@ -16,13 +16,11 @@ Copyright: 2022 Contrast Security, Inc
16
16
 
17
17
  const _ = require('lodash');
18
18
  const semver = require('semver');
19
- const constants = require('../../constants');
20
- const BaseSensor = require('../../hooks/frameworks/base');
21
19
  const patcher = require('../../hooks/patcher');
22
- const { PATCH_TYPES } = require('../../constants');
20
+ const BaseSensor = require('../../hooks/frameworks/base');
21
+ const { PATCH_TYPES, SINK_TYPES } = require('../../constants');
23
22
  const { emitSinkEvent } = require('../../hooks/frameworks/common');
24
23
 
25
- const { SINK_TYPES } = constants;
26
24
  const ID = 'mongodb';
27
25
 
28
26
  function getCursorQueryData(args, version) {
@@ -61,84 +59,98 @@ class MongoDBSensor extends BaseSensor {
61
59
  }
62
60
 
63
61
  install({ ModuleHook }) {
64
- ModuleHook.resolve({ name: 'mongodb' }, (mongodb, { version }) => {
65
- // command(ns, cmd, options, cb)
66
- patcher.patch(mongodb.CoreServer.prototype, 'command', {
67
- alwaysRun: true,
68
- name: 'mongodb.CoreServer.prototype',
69
- patchType: PATCH_TYPES.PROTECT_SINK,
70
- pre: (wrapCtx) => {
71
- emitSinkEvent(
72
- getCursorQueryData(wrapCtx.args, version),
73
- SINK_TYPES.NOSQL_QUERY,
74
- ID
75
- );
76
- }
77
- });
78
-
79
- // cursor(ns, cmd, options)
80
- patcher.patch(mongodb.CoreServer.prototype, 'cursor', {
81
- alwaysRun: true,
82
- name: 'mongodb.CoreServer.prototype',
83
- patchType: PATCH_TYPES.PROTECT_SINK,
84
- pre: (data) => {
85
- emitSinkEvent(
86
- getCursorQueryData(data.args, version),
87
- SINK_TYPES.NOSQL_QUERY,
88
- ID
89
- );
90
- }
62
+ const v4MethodsWithFilter = [
63
+ 'updateOne',
64
+ 'replaceOne',
65
+ 'updateMany',
66
+ 'deleteOne',
67
+ 'deleteMany',
68
+ 'findOneAndDelete',
69
+ 'findOneAndReplace',
70
+ 'findOneAndUpdate',
71
+ 'countDocuments',
72
+ 'count',
73
+ 'distinct',
74
+ ];
75
+
76
+ ModuleHook.resolve({ name: ID, version: '<4.0.0' }, (mongodb, { version }) => {
77
+ ['command', 'cursor'].forEach(method => {
78
+ patcher.patch(mongodb.CoreServer.prototype, method, {
79
+ alwaysRun: true,
80
+ name: `mongodb.CoreServer.prototype.${method}`,
81
+ patchType: PATCH_TYPES.PROTECT_SINK,
82
+ pre: (wrapCtx) => {
83
+ emitSinkEvent(
84
+ getCursorQueryData(wrapCtx.args, version),
85
+ SINK_TYPES.NOSQL_QUERY,
86
+ ID
87
+ );
88
+ }
89
+ });
91
90
  });
92
91
 
93
- // remove(ns, opts, options, cb)
94
- patcher.patch(mongodb.CoreServer.prototype, 'remove', {
95
- alwaysRun: true,
96
- name: 'mongodb.CoreServer.prototype',
97
- patchType: PATCH_TYPES.PROTECT_SINK,
98
- pre: (data) => {
99
- const ops = Array.isArray(data.args[1])
100
- ? data.args[1]
101
- : [data.args[1]];
102
-
103
- for (const op of ops) {
104
- const eData = getOpQueryData(op);
105
- if (eData) {
106
- emitSinkEvent(eData, SINK_TYPES.NOSQL_QUERY, ID);
92
+ ['remove', 'update'].forEach(method => {
93
+ patcher.patch(mongodb.CoreServer.prototype, method, {
94
+ alwaysRun: true,
95
+ name: 'mongodb.CoreServer.prototype.remove',
96
+ patchType: PATCH_TYPES.PROTECT_SINK,
97
+ pre: (wrapCtx) => {
98
+ const ops = Array.isArray(wrapCtx.args[1]) ? wrapCtx.args[1] : [wrapCtx.args[1]];
99
+
100
+ for (const op of ops) {
101
+ const eData = getOpQueryData(op);
102
+ if (eData) {
103
+ emitSinkEvent(eData, SINK_TYPES.NOSQL_QUERY, ID);
104
+ }
107
105
  }
108
106
  }
109
- }
107
+ });
110
108
  });
111
109
 
112
- // update(ns, opts, options, cb)
113
- patcher.patch(mongodb.CoreServer.prototype, 'update', {
110
+ patcher.patch(mongodb.Db.prototype, 'eval', {
114
111
  alwaysRun: true,
115
- name: 'mongodb.CoreServer.prototype',
112
+ name: 'mongodb.Db.prototype.eval',
116
113
  patchType: PATCH_TYPES.PROTECT_SINK,
117
- pre: (data) => {
118
- const ops = Array.isArray(data.args[1])
119
- ? data.args[1]
120
- : [data.args[1]];
121
-
122
- for (const op of ops) {
123
- const eData = getOpQueryData(op);
124
- if (eData) {
125
- emitSinkEvent(eData, SINK_TYPES.NOSQL_QUERY, ID);
126
- }
127
- }
114
+ pre: (wrapCtx) => {
115
+ emitSinkEvent(wrapCtx.args[0], SINK_TYPES.NOSQL_QUERY, ID);
128
116
  }
129
117
  });
118
+ });
130
119
 
131
- // eval(code, parameters, options, cb)
132
- patcher.patch(mongodb.Db.prototype, 'eval', {
120
+ ModuleHook.resolve({ name: ID, version: '>=4.0.0' }, (mongodb) => {
121
+ v4MethodsWithFilter.forEach((method) => {
122
+ patcher.patch(mongodb.Collection.prototype, method, {
123
+ alwaysRun: true,
124
+ name: `mongodb.Collection.prototype.${method}`,
125
+ patchType: PATCH_TYPES.PROTECT_SINK,
126
+ pre: (wrapCtx) => {
127
+ const value = typeof wrapCtx.args[0] == 'function' ? null : wrapCtx.args[0];
128
+ emitSinkEvent(value, SINK_TYPES.NOSQL_QUERY, ID);
129
+ },
130
+ });
131
+ });
132
+
133
+ patcher.patch(mongodb.Db.prototype, 'command', {
133
134
  alwaysRun: true,
134
- name: 'mongodb.Db.prototype',
135
+ name: 'mongodb.Db.prototype.command',
135
136
  patchType: PATCH_TYPES.PROTECT_SINK,
136
- pre: (data) => {
137
- emitSinkEvent(data.args[0], SINK_TYPES.NOSQL_QUERY, ID);
137
+ pre: (wrapCtx) => {
138
+ const value = wrapCtx.args[0] && wrapCtx.args[0].filter;
139
+ emitSinkEvent(value, SINK_TYPES.NOSQL_QUERY, ID);
138
140
  }
139
141
  });
140
142
  });
141
143
 
144
+ ModuleHook.resolve({ name: ID, file: 'lib/cursor/find_cursor', version: '>=4.0.0' }, (cursor) => patcher.patch(cursor, 'FindCursor', {
145
+ alwaysRun: true,
146
+ name: 'mongodb.FindCursor',
147
+ patchType: PATCH_TYPES.PROTECT_SINK,
148
+ pre: (wrapCtx) => {
149
+ const value = wrapCtx.args[2];
150
+ emitSinkEvent(value, SINK_TYPES.NOSQL_QUERY, ID);
151
+ }
152
+ }));
153
+
142
154
  return this;
143
155
  }
144
156
  }
@@ -102,12 +102,15 @@ class Event {
102
102
  this.args.push(
103
103
  new ObjectDTM(event.context.args[i], event.context.argsTracked[i])
104
104
  );
105
- if (event.tagRanges[i]
106
- && event.context.argsDisplayRanges
107
- && Object.keys(event.context.argsDisplayRanges[i]).length
108
- ) {
109
- event.tagRanges[i] = event.context.argsDisplayRanges[i];
110
- }
105
+ }
106
+
107
+
108
+ const displayRanges = event.context.argsDisplayRanges.filter((tagRange) => Object.keys(tagRange).length);;
109
+
110
+ if (displayRanges.length) {
111
+ // If displayRanges is non-empty (=/= [{}]), use that instead
112
+ // since it's more accurate when reporting
113
+ event.tagRanges = displayRanges;
111
114
  }
112
115
 
113
116
  if (event.code) {
@@ -29,15 +29,19 @@ module.exports = function Finding(details = {}) {
29
29
  }
30
30
 
31
31
  const routes = RoutesListWithObservations(details.routes);
32
+ const properties = objToMap(details.properties).map(
33
+ // ensure string values
34
+ ([key, value]) => [key, String(value)]
35
+ );
32
36
 
33
37
  return new dtm.Finding({
34
- 0: String(details.hash || ''), // 1 hash_code
35
- 2: details.ruleId, // 3 rule_id
36
- 5: objToMap(details.properties), // 6 properties
37
- 6: mapToModelArray(TraceEvent, details.events), // 7 events
38
- 7: preflight, // 8 preflight
39
- 8: details.tags, // 9 tags
40
- 9: details.version, // 10 version
38
+ 0: String(details.hash || ''), // 1 hash_code
39
+ 2: details.ruleId, // 3 rule_id
40
+ 5: properties, // 6 properties
41
+ 6: mapToModelArray(TraceEvent, details.events), // 7 events
42
+ 7: preflight, // 8 preflight
43
+ 8: details.tags, // 9 tags
44
+ 9: details.version, // 10 version
41
45
  10: routes // 11 routes
42
46
  });
43
47
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contrast/agent",
3
- "version": "4.22.1",
3
+ "version": "4.24.0",
4
4
  "description": "Node.js security instrumentation by Contrast Security",
5
5
  "keywords": [
6
6
  "security",
@@ -163,7 +163,7 @@
163
163
  "mock-fs": "^5.1.2",
164
164
  "mongodb": "file:test/mock/mongodb",
165
165
  "mongodb-npm": "npm:mongodb@^3.6.5",
166
- "mongoose": "^6.1.1",
166
+ "mongoose": "^6.4.6",
167
167
  "mustache": "^3.0.1",
168
168
  "mysql": "file:test/mock/mysql",
169
169
  "mysql2": "file:test/mock/mysql2",