@contrast/agent 4.14.0 → 4.16.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/README.md CHANGED
@@ -108,4 +108,4 @@ api:
108
108
  | api.service_key | Contrast user account service key |
109
109
  | api.url | Address of the Contrast installation you would like your agent to report to |
110
110
 
111
- For detailed installation and configuration instructions, see the [Node.js Agent documentation](https://docs.contrastsecurity.com/installation-nodeconfig.html).
111
+ For detailed installation and configuration instructions, see the [Node.js Agent documentation](https://docs.contrastsecurity.com/en/install-node-js.html).
package/bin/VERSION CHANGED
@@ -1 +1 @@
1
- 2.28.17
1
+ 2.28.19
Binary file
Binary file
Binary file
@@ -22,6 +22,7 @@ const moduleHook = require('../../../hooks/require');
22
22
  const TagRange = require('../../models/tag-range');
23
23
  const tagRangeUtil = require('../../models/tag-range/util');
24
24
  const { PropagationEvent, Signature, CallContext } = require('../../models');
25
+ const agent = require('../../../agent');
25
26
 
26
27
  /**
27
28
  * this override propagator instruments each of the classes of methods
@@ -88,7 +89,7 @@ module.exports.handle = function() {
88
89
  { name: 'validator', file: `lib/${validator}` },
89
90
  (index, meta) => {
90
91
  function post(data) {
91
- if (data.result) {
92
+ if (data.result && (validator !== 'matches' || (validator === 'matches' && agent.config.assess.trust_custom_validators))) {
92
93
  const trackingData = tracker.getData(data.args[0]);
93
94
  if (trackingData) {
94
95
  tagRangeUtil.addInPlace(
@@ -72,7 +72,8 @@ module.exports = {
72
72
  isSemVer: 'limited-chars',
73
73
  isTaxID: 'limited-chars',
74
74
  isUUID: 'alphanum-space-hyphen',
75
- isVAT: 'alphanum-space-hyphen'
75
+ isVAT: 'alphanum-space-hyphen',
76
+ matches: 'string-type-checked'
76
77
  },
77
78
  untrackers: [
78
79
  // these methods have the tag 'trusted' which the node-agent doesn't support.
@@ -26,34 +26,38 @@ const disallowedTags = [
26
26
  'string-type-checked',
27
27
  ];
28
28
  const requiredTags = ['untrusted'];
29
- const moduleName = 'aws-sdk';
30
- const moduleNameV3 = '@aws-sdk/client-dynamodb';
31
- const relevantKeys = {
32
- v2: ['ExpressionAttributeValues', 'ExclusiveStartKey', 'ScanFilter'],
33
- v3: [
34
- 'ComparisonOperator',
35
- 'FilterExpression',
36
- 'ProjectionExpression',
37
- 'ScanFilter',
38
- ],
39
- };
40
- const requests = new WeakSet();
41
29
 
42
- // map data types to methods for extracting
43
- // values
44
- const dataTypes = {
45
- S: 'value',
46
- N: 'value',
47
- B: 'value',
48
- SS: 'array',
49
- NS: 'array',
50
- BS: 'array',
51
- M: 'object',
52
- L: 'collection',
53
- NULL: 'value',
54
- BOOL: 'value',
30
+ /*
31
+ * Schema of attributes the Node Agent is looking for user-controlled data
32
+ * The only exception to the rule so far is the ScanFilter in ScanCommand.
33
+ * It has nested schema too. So far we are only reporting a NoSQL Injection
34
+ * if ComparisonOperator within the ScanFilter is user-controlled.
35
+ * ScanFilter is handled individually in extractValues function
36
+ * */
37
+ const trackSchemaCommands = {
38
+ 'scan': {
39
+ attributes: [
40
+ 'ExpressionAttributeValues',
41
+ 'ExclusiveStartKey',
42
+ 'ScanFilter'
43
+ ]
44
+ },
45
+ 'executeStatement': { attributes: ['Statement'] },
46
+ 'ScanCommand': {
47
+ attributes: [
48
+ 'ComparisonOperator',
49
+ 'FilterExpression',
50
+ 'ProjectionExpression',
51
+ 'ScanFilter',
52
+ ]
53
+ },
54
+ 'ExecuteStatementCommand': {
55
+ attributes: ['Statement']
56
+ }
55
57
  };
56
58
 
59
+ const requests = new WeakSet();
60
+
57
61
  /**
58
62
  * Extracts all values from either a dynamo document client or client
59
63
  *
@@ -62,44 +66,25 @@ const dataTypes = {
62
66
  * @param {string} version DynamoDB SDK version
63
67
  * @return {Array} all string values from payload
64
68
  */
65
- function extractValues(payload = {}, mode = null, version = 'v2') {
69
+ function extractValues(command, payload = {}) {
66
70
  return _.flatten(
67
- relevantKeys[version].map((key) => {
68
- if (payload[key] === undefined) return;
71
+ trackSchemaCommands[command].attributes.map((key) => {
72
+ if (payload[key] == undefined) return;
69
73
  if (typeof payload[key] === 'string') return payload[key];
74
+
75
+ // ScanFilter is an exception. It is almost safe for any nested attribute
76
+ if (key === 'ScanFilter') {
77
+ // collect the values from ComparisonOperator attributes ONLY
78
+ return getComparisonValues(payload[key]);
79
+ }
80
+
70
81
  const values = _.values(payload[key]);
71
- const extractionMethod =
72
- mode === 'client' ? findTypedValues.bind(this) : findValues.bind(this);
82
+ const extractionMethod = findValues.bind(this);
73
83
  return _.flattenDeep(extractionMethod(values));
74
84
  }),
75
85
  );
76
86
  }
77
87
 
78
- /**
79
- * Extracts all strings from a dynamo client payload
80
- * Note: Each key is typed so we need to properly extract keys based on AttributeValue types
81
- *
82
- * @param {Object} values for all keys in ExpressionAttributeValues, ExclusiveStartKey or ScanFilter
83
- * @return {Array} all values
84
- */
85
- function findTypedValues(values) {
86
- return _.map(values, (value) => {
87
- const [type] = Object.keys(value);
88
- switch (dataTypes[type]) {
89
- case 'value':
90
- return value[type];
91
- case 'array':
92
- return _.values(value[type]);
93
- case 'object': {
94
- const values = _.values(value[type]);
95
- return findTypedValues(values);
96
- }
97
- case 'collection':
98
- return findTypedValues(value[type]);
99
- }
100
- });
101
- }
102
-
103
88
  /**
104
89
  * We only track strings. some data types
105
90
  * can contain strings as keys or values but
@@ -139,6 +124,27 @@ function findValues(values) {
139
124
  });
140
125
  }
141
126
 
127
+ /**
128
+ * Check if key exists in ScanFilter bject and return the value if so
129
+ * Each element of ScanFilter is an object with predictable structure
130
+ * "Genre": {
131
+ * "AttributeValueList":[ {"S":"Rock"} ],
132
+ * "ComparisonOperator": "EQ"
133
+ * }
134
+ *
135
+ * @param {Object} values for all keys in a given payload
136
+ * @param {String} key name we are looking the value of
137
+ * @return {Any} if value is found for a given key
138
+ */
139
+ const getComparisonValues = (obj) => Object.keys(obj).map(field => {
140
+ if (typeof obj[field] === 'object' && Object.prototype.hasOwnProperty.call(
141
+ obj[field],
142
+ 'ComparisonOperator'
143
+ )) {
144
+ return obj[field].ComparisonOperator;
145
+ }
146
+ });
147
+
142
148
  module.exports = ({ common }) => {
143
149
  const { isVulnerable, report } = common;
144
150
 
@@ -147,11 +153,11 @@ module.exports = ({ common }) => {
147
153
  * Registers the hooks for client and document client scan
148
154
  */
149
155
  dynamoSink.handle = function () {
150
- moduleHook.resolve({ name: moduleName }, (aws) => {
156
+ moduleHook.resolve({ name: 'aws-sdk' }, (aws) => {
151
157
  const client = aws.DynamoDB.prototype;
152
158
 
153
159
  patcher.patch(client, 'makeRequest', {
154
- name: `${moduleName}.DynamoDB.prototype`,
160
+ name: 'aws-sdk.DynamoDB.prototype',
155
161
  patchType: PATCH_TYPES.ASSESS_SINK,
156
162
  alwaysRun: true,
157
163
  post(data) {
@@ -163,7 +169,8 @@ module.exports = ({ common }) => {
163
169
  if (!AsyncStorage.getContext()) {
164
170
  return;
165
171
  }
166
- if (data.args[0] === 'scan') {
172
+
173
+ if (Object.keys(trackSchemaCommands).includes(data.args[0]) && data.args[1]) {
167
174
  if (requests.has(data.args[1])) {
168
175
  return;
169
176
  }
@@ -176,10 +183,10 @@ module.exports = ({ common }) => {
176
183
  result: data.result,
177
184
  };
178
185
 
179
- const values = extractValues(data.args[1], 'client', 'v2');
186
+ const values = extractValues(data.args[0], data.args[1]);
180
187
  dynamoSink.check({
181
188
  values,
182
- methodName: 'DynamoDB.prototype.scan',
189
+ methodName: `DynamoDB.prototype.${data.args[0]}`,
183
190
  data: ctxtData,
184
191
  });
185
192
  }
@@ -198,7 +205,7 @@ module.exports = ({ common }) => {
198
205
  name: 'aws-sdk.DynamoDB.DocumentClient.prototype',
199
206
  patchType: PATCH_TYPES.ASSESS_SINK,
200
207
  pre(data) {
201
- if (data.args[0] === 'scan') {
208
+ if (data.args[0] === 'scan' && data.args[1]) {
202
209
  requests.add(data.args[1]);
203
210
  }
204
211
  },
@@ -208,7 +215,7 @@ module.exports = ({ common }) => {
208
215
  name: 'aws-sdk.DynamoDB.DocumentClient.prototype',
209
216
  patchType: PATCH_TYPES.ASSESS_SINK,
210
217
  post(data) {
211
- const values = extractValues(data.args[0], 'docClient', 'v2');
218
+ const values = extractValues('scan', data.args[0]);
212
219
  dynamoSink.check({
213
220
  values,
214
221
  methodName: 'DynamoDB.DocumentClient.prototype.scan',
@@ -219,27 +226,31 @@ module.exports = ({ common }) => {
219
226
  }
220
227
  });
221
228
 
222
- moduleHook.resolve({ name: moduleNameV3 }, (aws) => {
229
+ moduleHook.resolve({ name: '@aws-sdk/client-dynamodb' }, (aws) => {
223
230
  const client = aws.DynamoDBClient.prototype;
224
231
 
225
232
  patcher.patch(client, 'send', {
226
- name: `${moduleNameV3}.ScanCommand.prototype`,
233
+ name: '@aws-sdk/client-dynamodb.ScanCommand.prototype',
227
234
  patchType: PATCH_TYPES.ASSESS_SINK,
228
235
  alwaysRun: true,
229
236
  post(data) {
230
237
  if (!AsyncStorage.getContext()) return;
231
238
 
232
- if (data.args[0] instanceof aws.ScanCommand) {
233
- const values = extractValues(
234
- data.args[0].input,
235
- 'docClient',
236
- 'v3',
237
- ).filter(Boolean);
238
- dynamoSink.check({
239
- data,
240
- values,
241
- methodName: 'DynamoDBClient.ScanCommand',
242
- });
239
+ if (data.args[0] && data.args[0].constructor && data.args[0].input) {
240
+ const sendCommand = data.args[0].constructor.name;
241
+
242
+ if (Object.keys(trackSchemaCommands).includes(sendCommand)) {
243
+ const values = extractValues(
244
+ sendCommand,
245
+ data.args[0].input
246
+ ).filter(Boolean);
247
+
248
+ dynamoSink.check({
249
+ data,
250
+ values,
251
+ methodName: `DynamoDBClient.${sendCommand}`,
252
+ });
253
+ }
243
254
  }
244
255
  },
245
256
  });
@@ -257,7 +268,7 @@ module.exports = ({ common }) => {
257
268
 
258
269
  if (vulnerableString.length) {
259
270
  const ctxt = new CallContext(data);
260
- const signature = new Signature({ moduleName, methodName });
271
+ const signature = new Signature({ moduleName: 'aws-sdk', methodName });
261
272
  report({ ruleId, signature, input: vulnerableString[0], ctxt });
262
273
  }
263
274
  };
package/lib/libraries.js CHANGED
@@ -124,7 +124,7 @@ const getLibInfo = async (agent, eluEnabled) =>
124
124
 
125
125
  if (!nodeModsPath) {
126
126
  logger.error(
127
- `unable to read installed dependencies because a node_modules directory could not be detected given a package.json located at %s - use the agent.node.app_root configuration variable if installed in non-standard location`,
127
+ 'unable to read installed dependencies because a node_modules directory could not be detected given a package.json located at %s - use the agent.node.app_root configuration variable if installed in non-standard location',
128
128
  agent.appInfo.path
129
129
  );
130
130
  return AppUpdate.libraries;
@@ -57,7 +57,7 @@ module.exports.install = function() {
57
57
  handledErrors.add(error);
58
58
  } else {
59
59
  console.warn(
60
- `An Unhandled Rejection has been caught by the Contrast Security node-agent instrumentation. Error: ${error}`,
60
+ 'An Unhandled Rejection has been found in the instrumented code:\n%s', error
61
61
  );
62
62
  }
63
63
  }
@@ -37,6 +37,8 @@ const UserInputKit = require('../reporter/models/utils/user-input-kit');
37
37
  const UserInputFactory = require('../reporter/models/utils/user-input-factory');
38
38
  const blockRequest = require('../util/block-request');
39
39
 
40
+ const evalOptions = { preferWorthWatching: true };
41
+
40
42
  class ProtectService {
41
43
  /**
42
44
  * Configures the service to use the provided agent.
@@ -196,8 +198,6 @@ class ProtectService {
196
198
  }
197
199
 
198
200
  const arg = {
199
- rules,
200
- preferWorthWatching: true,
201
201
  // header names must be lowercase. should this be done in agent-lib?
202
202
  headers: req.rawHeaders.map((h, ix) => (ix & 1 ? h : h.toLowerCase()))
203
203
  };
@@ -207,7 +207,7 @@ class ProtectService {
207
207
  arg.queries = req.url.slice(questionMark + 1);
208
208
  }
209
209
 
210
- const findings = this.agentLib.scoreRequestConnect(arg);
210
+ const findings = this.agentLib.scoreRequestConnect(rules, arg, evalOptions);
211
211
 
212
212
  return findings;
213
213
  }
@@ -218,14 +218,12 @@ class ProtectService {
218
218
  return {};
219
219
  }
220
220
  // also, if content-type has multipart...
221
- const options = { preferWorthWatching: true };
222
-
223
221
  const bodyBuffer = Buffer.concat(chunks);
224
222
 
225
223
  const findings = this.agentLib.scoreRequestUnknownBody(
226
224
  rules,
227
225
  bodyBuffer,
228
- options
226
+ evalOptions
229
227
  );
230
228
 
231
229
  // store body buffer on findings for nosqli sink.
@@ -814,9 +812,9 @@ class ProtectService {
814
812
  // for each key, check out the value. the key is set in the code so
815
813
  // is not vulnerable.
816
814
  for (const key in params) {
817
- // items from scoreAtom() are only [{ruleId, score}, ...] because the key
815
+ // items from scoreAtom() return only [{ruleId, score}, ...] because the key
818
816
  // and inputType are already known and there is no path.
819
- const items = this.agentLib.scoreAtom(params[key], type, libRules);
817
+ const items = this.agentLib.scoreAtom(libRules, params[key], type);
820
818
  if (!items) {
821
819
  continue;
822
820
  }
@@ -866,7 +864,7 @@ class ProtectService {
866
864
  const filenames = Object.keys(event.data);
867
865
 
868
866
  for (const filename of filenames) {
869
- const items = this.agentLib.scoreAtom(filename, type, libRules);
867
+ const items = this.agentLib.scoreAtom(libRules, filename, type);
870
868
  if (!items) {
871
869
  continue;
872
870
  }
@@ -900,13 +898,9 @@ class ProtectService {
900
898
  queries.unshift(...q); return queries;
901
899
  }, []);
902
900
 
903
- const arg = {
904
- rules: rulesMask,
905
- preferWorthWatching: true,
906
- queries,
907
- };
901
+ const arg = { queries };
908
902
 
909
- const findings = this.agentLib.scoreRequestConnect(arg);
903
+ const findings = this.agentLib.scoreRequestConnect(rulesMask, arg, evalOptions);
910
904
 
911
905
  this.handleAgentLibAnalysis({
912
906
  asyncStorageContext: event._ctxt,
@@ -922,11 +916,9 @@ class ProtectService {
922
916
  acc.unshift(key, value);
923
917
  return acc;
924
918
  }, []);
925
- const findings = this.agentLib.scoreRequestConnect({
926
- preferWorthWatching: true,
927
- rules: this.getRulesMask(rules),
928
- cookies
929
- });
919
+ const rulesMask = this.getRulesMask(rules);
920
+ const arg = { cookies };
921
+ const findings = this.agentLib.scoreRequestConnect(rulesMask, arg, evalOptions);
930
922
  this.handleAgentLibAnalysis({
931
923
  asyncStorageContext: event._ctxt,
932
924
  appContext: {},
@@ -1141,7 +1133,7 @@ class ProtectService {
1141
1133
  * @param {Rule[]} rules Rules from which to build findings
1142
1134
  * @returns {Object[]} The findings from the rules
1143
1135
  */
1144
- createFindings(rules, samples) {
1136
+ createFindings(rules = [], samples) {
1145
1137
  const findings = [];
1146
1138
  const speedracer = this.reporter.speedracer &&
1147
1139
  this.config.agent.node.speedracer_input_analysis;
@@ -1172,7 +1164,7 @@ class ProtectService {
1172
1164
  const { _type, _value: input } = finding.sample.input;
1173
1165
  const type = this.agentLib.InputType[_type];
1174
1166
 
1175
- const alFinding = this.agentLib.scoreAtom(input, type, agentLibBit);
1167
+ const alFinding = this.agentLib.scoreAtom(agentLibBit, input, type);
1176
1168
  if (!alFinding) {
1177
1169
  return false;
1178
1170
  }
@@ -13,9 +13,10 @@ Copyright: 2022 Contrast Security, Inc
13
13
  way not consistent with the End User License Agreement.
14
14
  */
15
15
  'use strict';
16
- const readdir = require('recursive-readdir');
17
16
  const LibraryManifest = require('./library-manifest');
18
17
  const logger = require('../../../core/logger')('contrast:libraries');
18
+ const fs = require('fs');
19
+ const pathModule = require('path');
19
20
 
20
21
  module.exports = class Library {
21
22
  /**
@@ -53,7 +54,7 @@ module.exports = class Library {
53
54
  manifest: this.manifest.toSerializable(),
54
55
  usedClassCount: 0,
55
56
  classCount: this.fileCount,
56
- tags: this.tags
57
+ tags: this.tags,
57
58
  };
58
59
  }
59
60
 
@@ -88,15 +89,68 @@ module.exports = class Library {
88
89
  );
89
90
  }
90
91
 
92
+ readdir(path, callback) {
93
+ if (!callback) {
94
+ return new Promise((resolve, reject) => {
95
+ this.readdir(path, (err, data) => {
96
+ if (err) {
97
+ reject(err);
98
+ } else {
99
+ resolve(data);
100
+ }
101
+ });
102
+ });
103
+ }
104
+
105
+ let list = [];
106
+
107
+ fs.readdir(path, (err, files) => {
108
+ if (err) {
109
+ return callback(err);
110
+ }
111
+
112
+ let pending = files.length;
113
+ if (!pending) {
114
+ return callback(null, list);
115
+ }
116
+
117
+ files.forEach((file) => {
118
+ const filePath = pathModule.join(path, file);
119
+ fs.stat(filePath, (_err, stats) => {
120
+ if (_err) {
121
+ return callback(_err);
122
+ }
123
+
124
+ if (stats.isDirectory() && !filePath.endsWith('/node_modules')) {
125
+ this.readdir(filePath, (__err, res) => {
126
+ if (__err) {
127
+ return callback(__err);
128
+ }
129
+
130
+ list = list.concat(res);
131
+ pending -= 1;
132
+ if (!pending) {
133
+ return callback(null, list);
134
+ }
135
+ });
136
+ } else {
137
+ list.push(filePath);
138
+ pending -= 1;
139
+ if (!pending) {
140
+ return callback(null, list);
141
+ }
142
+ }
143
+ });
144
+ });
145
+ });
146
+ }
147
+
91
148
  /**
92
149
  * Counts all the valid files in a module directory
93
150
  */
94
151
  getComposition() {
95
152
  // ignore nested node_modules
96
- return readdir(this._path, [
97
- `${this._path}/node_modules/*`,
98
- `${this._path}/*/node_modules/*`
99
- ])
153
+ return this.readdir(this._path)
100
154
  .then((files) => {
101
155
  this.fileCount = files.filter((file) =>
102
156
  Library.applicableFile(file)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contrast/agent",
3
- "version": "4.14.0",
3
+ "version": "4.16.0",
4
4
  "description": "Node.js security instrumentation by Contrast Security",
5
5
  "keywords": [
6
6
  "security",
@@ -69,14 +69,14 @@
69
69
  "repository": {
70
70
  "type": "git"
71
71
  },
72
- "homepage": "https://docs.contrastsecurity.com/installation-node.html#node-overview",
72
+ "homepage": "https://docs.contrastsecurity.com/en/install-node-js.html",
73
73
  "dependencies": {
74
74
  "@babel/generator": "^7.12.1",
75
75
  "@babel/parser": "^7.12.3",
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": "^2.2.3",
79
+ "@contrast/agent-lib": "^3.0.0",
80
80
  "@contrast/distringuish-prebuilt": "^2.2.0",
81
81
  "@contrast/flat": "^4.1.1",
82
82
  "@contrast/fn-inspect": "^2.4.4",
@@ -107,7 +107,6 @@
107
107
  "parent-package-json": "^2.0.1",
108
108
  "parseurl": "^1.3.3",
109
109
  "prom-client": "^12.0.0",
110
- "recursive-readdir": "^2.2.2",
111
110
  "semver": "^7.3.2",
112
111
  "uuid": "^8.3.2",
113
112
  "winston": "^3.1.0",