@contrast/agent 4.21.0 → 4.22.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.
package/bin/VERSION CHANGED
@@ -1 +1 @@
1
- 2.28.20
1
+ 2.28.22
Binary file
@@ -103,18 +103,25 @@ module.exports = class CallContext {
103
103
  return !!(str && typeof str === 'object' && str[PROXY_TARGET]);
104
104
  }
105
105
 
106
- static getDisplayRange(arg) {
106
+ static getDisplayRange(arg, orgArg = arg, iteration = 0) {
107
107
  if (tracker.getData(arg)) {
108
108
  return new TagRange(0, arg.length - 1, 'untrusted');
109
109
  }
110
110
 
111
111
  if (arg && typeof arg === 'object') {
112
112
  for (const key in arg) {
113
+ if (arg[key] && typeof arg[key] === 'object' && iteration < 5) {
114
+ const nestedDisplayRange = CallContext.getDisplayRange(arg[key], orgArg, iteration += 1);
115
+ if (!_.isEmpty(nestedDisplayRange)) {
116
+ return nestedDisplayRange;
117
+ }
118
+ }
113
119
  const trackedData = tracker.getData(arg[key]);
114
- if (trackedData) {
120
+ if (trackedData && trackedData.tagRanges.length > 0) {
115
121
  const { start, stop } = trackedData.tagRanges[0];
122
+ const offset = Array.isArray(orgArg) ? 2 : 0;
116
123
  const taintedString = arg[key].substring(start, stop + 1);
117
- const taintRangeStart = CallContext.valueString(arg).indexOf(taintedString);
124
+ const taintRangeStart = CallContext.valueString(orgArg).indexOf(taintedString) + offset;
118
125
  if (taintRangeStart === -1) {
119
126
  // If tracked string is not in the abbreviated stringified obj, disable highlighting
120
127
  return new TagRange(0, 0, 'disable-highlighting');
@@ -178,9 +185,9 @@ module.exports = class CallContext {
178
185
  return value.toString();
179
186
  }
180
187
 
181
- const constructorName = _.get(value, 'constructor.name', 'null');
188
+ const type = _.get(value, 'constructor.name', 'null');
182
189
 
183
- if (constructorName === 'Object' && value) {
190
+ if ((type === 'Object' || type === 'Array') && value) {
184
191
  // make string representation uniform with no new lines and consistent spaces
185
192
  let str = util
186
193
  .inspect(value)
@@ -192,7 +199,7 @@ module.exports = class CallContext {
192
199
 
193
200
  return str;
194
201
  }
195
- return constructorName;
202
+ return type;
196
203
  }
197
204
  };
198
205
 
@@ -37,7 +37,6 @@ const requiredTags = ['untrusted'];
37
37
  const trackSchemaCommands = {
38
38
  'scan': {
39
39
  attributes: [
40
- 'ExpressionAttributeValues',
41
40
  'ExclusiveStartKey',
42
41
  'ScanFilter'
43
42
  ]
@@ -19,36 +19,74 @@ const { PATCH_TYPES } = require('../../constants');
19
19
  const ModuleHook = require('../../hooks/require');
20
20
  const patcher = require('../../hooks/patcher');
21
21
  const logger = require('../logger')('contrast:arch-component');
22
- const semver = require('semver');
23
22
 
23
+ // Architecture component for versions <3.0.0
24
24
  ModuleHook.resolve(
25
25
  {
26
26
  name: 'mongodb',
27
27
  file: 'lib/mongo_client.js',
28
- version: '>=3.3.0'
28
+ version: '<3.0.0'
29
29
  },
30
- (MongoClient, { version }) => {
31
- if (semver.lt(version, '4.0.0')) {
32
- patcher.patch(MongoClient.prototype, 'connect', {
33
- name: 'MongoClient.connect.arch_component',
34
- patchType: PATCH_TYPES.ARCH_COMPONENT,
35
- alwaysRun: true,
36
- post(ctx) {
37
- if (!ctx.result || !ctx.result.then) {
38
- return;
39
- }
40
-
41
- // We should report only when connection is successful
42
- ctx.result.then(function(client) {
30
+ (MongoClient) => {
31
+ patcher.patch(MongoClient, 'connect', {
32
+ name: 'MongoClient.connect.arch_component',
33
+ patchType: PATCH_TYPES.ARCH_COMPONENT,
34
+ alwaysRun: true,
35
+ pre(ctx) {
36
+ // check if typeof callback == 'function'
37
+ // if yes:
38
+ // - MongoClient.connect executes a cb function, which has access to the connection status
39
+ // if not:
40
+ // - MongoClient.connect should return a promise and we can check it's result in another hook, just as before
41
+ const callbackIndex = ctx.args[2] ? 2 : 1;
42
+ if (ctx.args[callbackIndex] instanceof Function || typeof ctx.args[callbackIndex] === 'function') {
43
+ ctx.args[callbackIndex] = patcher.patch(ctx.args[callbackIndex], {
44
+ name: 'MongoClient.connect.callback.arch_component',
45
+ patchType: PATCH_TYPES.ARCH_COMPONENT,
46
+ alwaysRun: true,
47
+ pre(ctx) {
48
+ const [, db] = ctx.args;
49
+ if (db && db.s.topology && db.s.topology.s) {
50
+ try {
51
+ const server = db.s.topology.s.server.s;
52
+ if (server.pool && server.pool.state == 'connected') {
53
+ const connections = server.pool.availableConnections;
54
+ for (const c of connections) {
55
+ agentEmitter.emit('architectureComponent', {
56
+ vendor: 'MongoDB',
57
+ url: `mongodb://${c.host}:${c.port}`,
58
+ remoteHost: '',
59
+ remotePort: c.port
60
+ });
61
+ }
62
+ }
63
+ } catch (err) {
64
+ logger.warn('unable to report MongoDB architecture component, err: %o', err);
65
+ }
66
+ }
67
+ }
68
+ });
69
+ }
70
+ },
71
+ post(ctx) {
72
+ if (!ctx.result || !ctx.result.then) {
73
+ return;
74
+ }
75
+ // it never gets here if callbacks are used, because the result won't be then-able
76
+ ctx.result.then(db => {
77
+ if (db && db.s && db.s.topology && db.s.topology.s) {
43
78
  try {
44
- const { servers = [] } = ctx.obj.s && ctx.obj.s.options;
45
- for (const server of servers) {
46
- agentEmitter.emit('architectureComponent', {
47
- vendor: 'MongoDB',
48
- url: `mongodb://${server.host}:${server.port}`,
49
- remoteHost: '',
50
- remotePort: server.port,
51
- });
79
+ const server = db.s.topology.s.server.s;
80
+ if (server.pool && server.pool.state == 'connected') {
81
+ const connections = server.pool.availableConnections;
82
+ for (const c of connections) {
83
+ agentEmitter.emit('architectureComponent', {
84
+ vendor: 'MongoDB',
85
+ url: `mongodb://${c.host}:${c.port}`,
86
+ remoteHost: '',
87
+ remotePort: c.port
88
+ });
89
+ }
52
90
  }
53
91
  } catch (err) {
54
92
  logger.warn(
@@ -56,60 +94,96 @@ ModuleHook.resolve(
56
94
  err,
57
95
  );
58
96
  }
59
- });
60
- },
61
- });
62
- }
97
+ }
98
+ });
99
+ }
100
+ });
101
+ }
102
+ );
103
+
104
+ // Architecture component for versions >=3.3.0 <4.0.0
105
+ ModuleHook.resolve(
106
+ {
107
+ name: 'mongodb',
108
+ file: 'lib/mongo_client.js',
109
+ version: '>=3.0.0 <4.0.0'
110
+ },
111
+ (MongoClient, { version }) => {
112
+ patcher.patch(MongoClient.prototype, 'connect', {
113
+ name: 'MongoClient.connect.arch_component',
114
+ patchType: PATCH_TYPES.ARCH_COMPONENT,
115
+ alwaysRun: true,
116
+ post(ctx) {
117
+ if (!ctx.result || !ctx.result.then) {
118
+ return;
119
+ }
120
+
121
+ // We should report only when connection is successful
122
+ ctx.result.then(function(client) {
123
+ try {
124
+ const { servers = [] } = ctx.obj.s && ctx.obj.s.options;
125
+ for (const server of servers) {
126
+ agentEmitter.emit('architectureComponent', {
127
+ vendor: 'MongoDB',
128
+ url: `mongodb://${server.host}:${server.port}`,
129
+ remoteHost: '',
130
+ remotePort: server.port,
131
+ });
132
+ }
133
+ } catch (err) {
134
+ logger.warn(
135
+ 'unable to report MongoDB architecture component, err: %o',
136
+ err,
137
+ );
138
+ }
139
+ });
140
+ },
141
+ });
63
142
  },
64
143
  );
65
144
 
66
- /* Architecture component for >= mongodb@4
67
- * It's not limited in the require hook to >=4.0.0 because
68
- * that would result in confusing logs for the customer that
69
- * we don't support older versions (which is not true) */
145
+ // Architecture component for versions >=4.0.0
70
146
  ModuleHook.resolve(
71
147
  {
72
148
  name: 'mongodb',
73
- version: '>=3.3.0'
149
+ version: '>=4.0.0'
74
150
  },
75
151
  (MongoDB, { version }) => {
76
- if (semver.gte(version, '4.0.0')) {
77
- patcher.patch(MongoDB.MongoClient.prototype, 'connect', {
78
- name: 'MongoClient.connect.arch_component',
79
- patchType: PATCH_TYPES.ARCH_COMPONENT,
80
- alwaysRun: true,
81
- post(ctx) {
82
- if (!ctx.result || !ctx.result.then) {
83
- return;
84
- }
152
+ patcher.patch(MongoDB.MongoClient.prototype, 'connect', {
153
+ name: 'MongoClient.connect.arch_component',
154
+ patchType: PATCH_TYPES.ARCH_COMPONENT,
155
+ alwaysRun: true,
156
+ post(ctx) {
157
+ if (!ctx.result || !ctx.result.then) {
158
+ return;
159
+ }
85
160
 
86
- // We should report only when connection is successful
87
- ctx.result.then(function(client) {
88
- if (client && client.topology && client.topology.s) {
89
- try {
90
- const { servers } = client.topology.s;
91
- for (const [, server] of servers) {
92
- if (server.s && server.s.state === 'connected') {
93
- const { srvServiceName } = server.s.options;
94
- const { address } = server.s.description;
95
- agentEmitter.emit('architectureComponent', {
96
- vendor: 'MongoDB',
97
- url: `${srvServiceName}://${address}`,
98
- remoteHost: '',
99
- remotePort: address.split(':').pop()
100
- });
101
- }
161
+ // We should report only when connection is successful
162
+ ctx.result.then(function(client) {
163
+ if (client && client.topology && client.topology.s) {
164
+ try {
165
+ const { servers } = client.topology.s;
166
+ for (const [, server] of servers) {
167
+ if (server.s && server.s.state === 'connected') {
168
+ const { srvServiceName } = server.s.options;
169
+ const { address } = server.s.description;
170
+ agentEmitter.emit('architectureComponent', {
171
+ vendor: 'MongoDB',
172
+ url: `${srvServiceName}://${address}`,
173
+ remoteHost: '',
174
+ remotePort: address.split(':').pop()
175
+ });
102
176
  }
103
- } catch (err) {
104
- logger.warn(
105
- 'unable to report MongoDB architecture component, err: %o',
106
- err,
107
- );
108
177
  }
178
+ } catch (err) {
179
+ logger.warn(
180
+ 'unable to report MongoDB architecture component, err: %o',
181
+ err,
182
+ );
109
183
  }
110
- });
111
- },
112
- });
113
- }
184
+ }
185
+ });
186
+ },
187
+ });
114
188
  },
115
189
  );
@@ -16,7 +16,6 @@ Copyright: 2022 Contrast Security, Inc
16
16
 
17
17
  const cp = require('child_process');
18
18
  const fs = require('fs');
19
- const os = require('os');
20
19
  const path = require('path');
21
20
  // according to https://nodejs.org/api/process.html#process_process the process
22
21
  // global can be required. We're only doing so here so we can mock it out in tests
@@ -37,6 +36,21 @@ const SuccessConnectionState = require('./success-connection-state');
37
36
 
38
37
  const RESEND_WAIT_MS = 100;
39
38
 
39
+ /**
40
+ * We might have the /bin/* files in /target directory if we're in
41
+ * development. Packaged/installed agents will have the executables in a
42
+ * top-level directory, e.g. /node_modules/@contrast/agent/bin/*
43
+ * @returns {'target' | '.'}
44
+ */
45
+ const maybeTargetDir = () => {
46
+ try {
47
+ fs.statSync(path.resolve(__dirname, '..', '..', '..', 'target'));
48
+ return 'target';
49
+ } catch (error) {
50
+ return '.';
51
+ }
52
+ };
53
+
40
54
  class Speedracer {
41
55
  constructor({ agent, logger }) {
42
56
  this.agent = agent;
@@ -155,7 +169,35 @@ class Speedracer {
155
169
  this.logger.info('starting contrast-service');
156
170
  this.startTime = Date.now();
157
171
 
158
- this.serviceProcess = cp.spawn(this.startCommand, this.startOptions);
172
+ const speedracerPath = path.resolve(
173
+ __dirname,
174
+ '..',
175
+ '..',
176
+ '..',
177
+ maybeTargetDir(),
178
+ 'bin',
179
+ `contrast-service-${process.platform}-${process.arch}`
180
+ );
181
+
182
+ try {
183
+ fs.statSync(speedracerPath);
184
+ } catch (error) {
185
+ this.logger.error(
186
+ 'unable to locate a speedracer binary for the current platform (%s) and architecture (%s)',
187
+ process.platform,
188
+ process.arch
189
+ );
190
+ return Promise.reject();
191
+ }
192
+
193
+ this.serviceProcess = cp.spawn(speedracerPath, {
194
+ env: {
195
+ ...process.env,
196
+ CONTRAST_CONFIG_PATH: this.agent.config.configFile
197
+ },
198
+ stdio: 'ignore',
199
+ windowsHide: true
200
+ });
159
201
 
160
202
  this.serviceProcess.on('error', (err) => {
161
203
  this.serviceProcess = null;
@@ -262,39 +304,6 @@ class Speedracer {
262
304
  }
263
305
  }
264
306
 
265
- /**
266
- * The start command for spawning the speedracer
267
- */
268
- get startCommand() {
269
- return path.resolve(
270
- __dirname,
271
- path.join(
272
- '..',
273
- '..',
274
- '..',
275
- utils.maybeTargetDir(),
276
- 'bin',
277
- utils.getOsDir(),
278
- 'contrast-service'
279
- )
280
- );
281
- }
282
-
283
- /**
284
- * The start options for spawning the speedracer child process. We provide the
285
- * service the location of our config file via environment variable.
286
- */
287
- get startOptions() {
288
- return {
289
- env: {
290
- ...process.env,
291
- CONTRAST_CONFIG_PATH: this.agent.config.configFile
292
- },
293
- stdio: 'ignore',
294
- windowsHide: true
295
- };
296
- }
297
-
298
307
  /**
299
308
  * GRPC is enabled when `agent.service.grpc` is not false, e.g., undefined
300
309
  * defaults to enabled, and `agent.service.socket` is not configured
@@ -314,39 +323,4 @@ class Speedracer {
314
323
  }
315
324
  }
316
325
 
317
- const utils = {
318
- /**
319
- * We might have the /bin/* files in /target directory if we're in
320
- * development. Packaged/installed agents will have the executables in a
321
- * top-level directory, e.g. /node_modules/@contrast/agent/bin/*
322
- * @returns {String}
323
- */
324
- maybeTargetDir() {
325
- try {
326
- fs.statSync(
327
- path.resolve(__dirname, path.join('..', '..', '..', 'target'))
328
- );
329
- return 'target';
330
- } catch (error) {
331
- return '';
332
- }
333
- },
334
- /**
335
- * Map platform values to folder names based on OS. The folder names are
336
- * determined by the packaging of the S-R artifacts and how they unzip.
337
- * @returns {String}
338
- */
339
- getOsDir() {
340
- switch (os.platform()) {
341
- case 'linux':
342
- return 'linux';
343
- case 'darwin':
344
- return 'mac';
345
- case 'win32':
346
- return 'windows';
347
- }
348
- }
349
- };
350
-
351
326
  module.exports = Speedracer;
352
- module.exports.utils = utils;
@@ -12,51 +12,63 @@ Copyright: 2022 Contrast Security, Inc
12
12
  engineered, modified, repackaged, sold, redistributed or otherwise used in a
13
13
  way not consistent with the End User License Agreement.
14
14
  */
15
- const path = require('path');
15
+ 'use strict';
16
+
16
17
  const fs = require('fs');
18
+ const path = require('path');
19
+ const v8 = require('v8');
17
20
  const logger = require('../core/logger')('contrast:heapDump');
18
- const heapUtil = module.exports;
19
- let heapdump;
20
21
 
21
22
  /**
22
- * setup n heap dumps to be created every n seconds after n seconds.
23
+ * @param {string} localPath location from config.path
24
+ */
25
+ const writeHeapSnapshot = function writeHeapSnapshot(localPath) {
26
+ const dumpPath = path.join(process.cwd(), localPath);
27
+
28
+ fs.mkdir(dumpPath, { recursive: true }, (err) => {
29
+ if (err) {
30
+ logger.error('Unable to create directory for heap snapshots: %o', err);
31
+ return;
32
+ }
33
+
34
+ // create dump at ${path}/${time}.heapdump
35
+ const filename = path.format({
36
+ dir: dumpPath,
37
+ name: `${Date.now()}-contrast`,
38
+ ext: '.heapsnapshot',
39
+ });
40
+
41
+ logger.info('Writing heap snapshot at %s', filename);
42
+ v8.writeHeapSnapshot(filename);
43
+ });
44
+ };
45
+
46
+ /**
47
+ * setup x heap dumps to be created every y seconds after z seconds.
23
48
  * @param {Object} config config blob from config/options.js
24
- * must have: window_ms (Number), delay_ms (Number), dumpPath (String), count (Number)
49
+ * @param {boolean} config.enable
50
+ * @param {string} config.path
51
+ * @param {number} config.count x
52
+ * @param {number} config.window_ms y
53
+ * @param {number} config.delay_ms z
25
54
  */
26
- heapUtil.init = function(config) {
55
+ const init = function init(config) {
27
56
  if (!config.enable) return;
28
- // NODE-1200: make this optional based on if config
29
- // bit is flipped
30
- heapdump = require('@contrast/heapdump');
31
57
 
32
58
  setTimeout(() => {
33
59
  let count = 0;
34
- const timeout = setInterval(() => {
60
+
61
+ const interval = setInterval(() => {
35
62
  if (count >= config.count) {
36
- clearInterval(timeout);
63
+ clearInterval(interval);
37
64
  return;
38
65
  }
39
- heapUtil.buildDump(config.path);
66
+
67
+ writeHeapSnapshot(config.path);
40
68
 
41
69
  count++;
42
70
  }, config.window_ms);
43
71
  }, config.delay_ms).unref();
44
72
  };
45
73
 
46
- heapUtil.buildDump = function(dumpPath) {
47
- // create directory in cwd
48
- dumpPath = `${path.join(process.cwd(), dumpPath)}`;
49
- fs.mkdir(dumpPath, (err) => {
50
- if (!err || (err && err.code == 'EEXIST')) {
51
- // create dump at ${path}/${time}.heapdump
52
- const fileName = `${path.join(
53
- dumpPath,
54
- String(Date.now())
55
- )}-contrast.heapsnapshot`;
56
- logger.info(`Building heap snapshot at ${fileName}`);
57
- heapdump.writeSnapshot(fileName);
58
- } else {
59
- logger.error(`Unable to create directory for heap snapshots: ${err}`);
60
- }
61
- });
62
- };
74
+ module.exports = { writeHeapSnapshot, init };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contrast/agent",
3
- "version": "4.21.0",
3
+ "version": "4.22.1",
4
4
  "description": "Node.js security instrumentation by Contrast Security",
5
5
  "keywords": [
6
6
  "security",
@@ -76,13 +76,12 @@
76
76
  "@babel/template": "^7.10.4",
77
77
  "@babel/traverse": "^7.12.1",
78
78
  "@babel/types": "^7.12.1",
79
- "@contrast/agent-lib": "^4.2.0",
80
- "@contrast/distringuish-prebuilt": "^3.0.1",
79
+ "@contrast/agent-lib": "^4.3.0",
80
+ "@contrast/distringuish-prebuilt": "^3.2.0",
81
81
  "@contrast/flat": "^4.1.1",
82
- "@contrast/fn-inspect": "^3.0.0",
83
- "@contrast/heapdump": "^1.1.0",
82
+ "@contrast/fn-inspect": "^3.1.0",
84
83
  "@contrast/protobuf-api": "^3.2.5",
85
- "@contrast/require-hook": "^3.0.0",
84
+ "@contrast/require-hook": "^3.2.1",
86
85
  "@contrast/synchronous-source-maps": "^1.1.0",
87
86
  "amqp-connection-manager": "^3.2.2",
88
87
  "amqplib": "^0.8.0",