@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
@@ -59,23 +59,31 @@ function splitString(str, win32) {
59
59
  * @param {number} offset offset of full arg from result of path.resolve
60
60
  * @param {string} segment path segment from one of the args to path.resolve
61
61
  */
62
- function getSegmentOffset({ str, evaluator }, offset, segment, win32) {
62
+ // eslint-disable-next-line complexity
63
+ function getSegmentOffset({ meta: { str, evaluator }, offset, segment, win32, validateAgainstResult }) {
63
64
  // as the segments in each arg and in the result get traversed,
64
65
  // winnow away the string to ensure we are finding the proper
65
66
  // segment within the path
66
67
  const substr = str.substring(offset);
67
- let segmentOffset = substr.indexOf(segment);
68
+ const { sep } = path[win32 ? 'win32' : 'posix'];
69
+ let segmentOffset;
70
+ // Sometimes the segment starts with a separator, but the path method removes the initial separator if the path is not absolute
71
+ if (validateAgainstResult && segment.startsWith(sep) && offset == 0 && !['\\', '/', '.'].includes(substr[0]) && !['\\', '/'].includes(segment)) {
72
+ segment = segment.slice(1);
73
+ }
74
+ segmentOffset = substr.indexOf(segment);
75
+
68
76
  // If tracking separators, evaluator will fail on extensions
69
77
  if (substr === `.${segment}`) {
70
- return segmentOffset + offset;
78
+ return { offset: segmentOffset + offset, value: segment };
71
79
  }
80
+
72
81
  // Adjust offset if the segment does not start with a separator
73
- const { sep } = path[win32 ? 'win32' : 'posix'];
74
82
  if (substr.startsWith(sep) && !segment.startsWith(sep)) {
75
83
  segmentOffset--;
76
84
  }
77
85
 
78
- return evaluator(segmentOffset) ? segmentOffset + offset : -1;
86
+ return { offset: evaluator(segmentOffset) ? segmentOffset + offset : -1, value: segment };
79
87
  }
80
88
 
81
89
  /**
@@ -215,15 +223,19 @@ function filterSegmentsAndCalculateOffset(
215
223
  additionalOffset = hasChange ? (sepAdded ? 1 : -1) : 0;
216
224
  }
217
225
 
218
- const offset = getSegmentOffset(
219
- resultMeta,
220
- accumulator.offset + additionalOffset + accumulator.fileDotSeparator,
226
+
227
+ const validatedSegment = getSegmentOffset({
228
+ meta: resultMeta,
229
+ offset: accumulator.offset + additionalOffset + accumulator.fileDotSeparator,
221
230
  segment,
222
- win32
223
- );
231
+ win32,
232
+ validateAgainstResult: true
233
+ });
234
+
235
+ const { offset: segmentOffset, value: segmentValue } = validatedSegment;
224
236
 
225
237
  // no reason to proceed if segment is not in final result
226
- if (offset === -1) {
238
+ if (segmentOffset === -1) {
227
239
  if (hasChange) {
228
240
  additionalOffset = 0;
229
241
  accumulator.isModified = false;
@@ -233,12 +245,11 @@ function filterSegmentsAndCalculateOffset(
233
245
  }
234
246
 
235
247
  // in case we have a file we need to increment the offset
236
- if (resultMeta.str[offset + segment.length] === '.') {
248
+ if (resultMeta.str[segmentOffset + segmentValue.length] === '.') {
237
249
  accumulator.fileDotSeparator = 1;
238
250
  }
239
-
240
- validSegments.push(segment);
241
- accumulator.offset = calculateNewOffset(accumulator.offset, segment);
251
+ validSegments.push(validatedSegment);
252
+ accumulator.offset = calculateNewOffset(accumulator.offset, segmentValue);
242
253
  accumulator.isModified = true;
243
254
 
244
255
  return accumulator;
@@ -280,26 +291,20 @@ function adjustTagsToPart(resultMeta, argMeta, win32, data, index) {
280
291
  resultMeta.offset += additionalOffset;
281
292
 
282
293
  return segments.reduce((newTags, segment) => {
283
- const offset = getSegmentOffset(
284
- resultMeta,
285
- resultMeta.offset,
286
- segment,
287
- win32
288
- );
289
-
290
- const argOffset = getSegmentOffset(argMeta, argMeta.offset, segment, win32);
294
+ const { offset, value: segmentValue } = segment;
295
+ const { offset: argOffset } = getSegmentOffset({ meta: argMeta, offset: argMeta.offset, segment: segmentValue, win32, validateAgainstResult: false });
291
296
 
292
297
  // updating the offset
293
- resultMeta.offset = calculateNewOffset(resultMeta.offset, segment);
294
- argMeta.offset = calculateNewOffset(argMeta.offset, segment);
298
+ resultMeta.offset = calculateNewOffset(resultMeta.offset, segmentValue);
299
+ argMeta.offset = calculateNewOffset(argMeta.offset, segmentValue);
295
300
 
296
301
  // in case we have a file we need to increment the offset
297
- if (resultMeta.str[offset + segment.length] === '.') {
302
+ if (resultMeta.str[offset + segment.value.length] === '.') {
298
303
  resultMeta.offset += 1;
299
304
  }
300
305
 
301
306
  tagRanges.forEach((tag) => {
302
- if (!isWithinTag(segment, argOffset, tag)) return;
307
+ if (!isWithinTag(segmentValue, argOffset, tag)) return;
303
308
 
304
309
  const start = adjustStart({
305
310
  tag,
@@ -310,7 +315,7 @@ function adjustTagsToPart(resultMeta, argMeta, win32, data, index) {
310
315
  tag,
311
316
  argOffset,
312
317
  offset,
313
- segment
318
+ segment: segmentValue
314
319
  });
315
320
 
316
321
  newTags.push(new TagRange(start, stop, tag.tag));
@@ -327,6 +332,7 @@ function propagate({ resultMeta, data, win32 }) {
327
332
 
328
333
  data.args.forEach((arg, index) => {
329
334
  const argData = tracker.getData(arg);
335
+
330
336
  maybeUpdateResultMeta({ resultMeta, index, arg });
331
337
 
332
338
  const argMeta = {
@@ -354,7 +360,10 @@ function propagate({ resultMeta, data, win32 }) {
354
360
  }
355
361
 
356
362
  function getResultMeta(data, method) {
357
- const offset = path.win32.isAbsolute(data.args[0]) ? 0 : process.cwd().length;
363
+ let offset = 0;
364
+ if (method === 'resolve' && !path.win32.isAbsolute(data.args[0])) {
365
+ offset = process.cwd().length;
366
+ }
358
367
 
359
368
  return {
360
369
  method: `path.${method}`,
@@ -19,6 +19,7 @@ const patcher = require('../../../hooks/patcher');
19
19
  const { PATCH_TYPES } = require('../../../constants');
20
20
  const { propagate, absolutePath, getResultMeta } = require('./common');
21
21
 
22
+
22
23
  /**
23
24
  * Entry point to propagator provider
24
25
  * This instruments path.resolve on
@@ -30,8 +30,7 @@ module.exports.isTracked = function isTracked(value) {
30
30
  * Function to get sql-string export. Caches the export on first call.
31
31
  */
32
32
  module.exports.getSequelizeString = function() {
33
- // eslint-disable-next-line node/no-unpublished-require
34
- const data = require('sequelize/lib/sql-string');
33
+ const data = require('sequelize/lib/sql-string'); // eslint-disable-line node/no-unpublished-require
35
34
  module.exports.getSequelizeString = () => data;
36
35
  return data;
37
36
  };
@@ -14,7 +14,6 @@ Copyright: 2022 Contrast Security, Inc
14
14
  */
15
15
  'use strict';
16
16
 
17
- // eslint-disable-next-line no-unused-vars
18
17
  const logger = require('../../../core/logger')('contrast:v8:propagator');
19
18
  const tracker = require('../../../tracker');
20
19
  const patcher = require('../../../hooks/patcher');
@@ -23,15 +23,20 @@ const ruleId = 'nosql-injection-dynamodb';
23
23
  const disallowedTags = [
24
24
  'limited-chars',
25
25
  'alphanum-space-hyphen',
26
- 'string-type-checked'
26
+ 'string-type-checked',
27
27
  ];
28
28
  const requiredTags = ['untrusted'];
29
29
  const moduleName = 'aws-sdk';
30
- const relevantKeys = [
31
- 'ExpressionAttributeValues',
32
- 'ExclusiveStartKey',
33
- 'ScanFilter'
34
- ];
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
+ };
35
40
  const requests = new WeakSet();
36
41
 
37
42
  // map data types to methods for extracting
@@ -46,24 +51,27 @@ const dataTypes = {
46
51
  M: 'object',
47
52
  L: 'collection',
48
53
  NULL: 'value',
49
- BOOL: 'value'
54
+ BOOL: 'value',
50
55
  };
51
56
 
52
57
  /**
53
58
  * Extracts all values from either a dynamo document client or client
54
59
  *
55
60
  * @param {Object} payload dynamo scan payload
56
- * @param {string} mode client or docClient
61
+ * @param {string} mode client, docClient or command (for aws sdk v3)
62
+ * @param {string} version DynamoDB SDK version
57
63
  * @return {Array} all string values from payload
58
64
  */
59
- function extractValues(payload = {}, mode) {
65
+ function extractValues(payload = {}, mode = null, version = 'v2') {
60
66
  return _.flatten(
61
- relevantKeys.map((key) => {
67
+ relevantKeys[version].map((key) => {
68
+ if (payload[key] === undefined) return;
69
+ if (typeof payload[key] === 'string') return payload[key];
62
70
  const values = _.values(payload[key]);
63
71
  const extractionMethod =
64
72
  mode === 'client' ? findTypedValues.bind(this) : findValues.bind(this);
65
73
  return _.flattenDeep(extractionMethod(values));
66
- })
74
+ }),
67
75
  );
68
76
  }
69
77
 
@@ -76,7 +84,7 @@ function extractValues(payload = {}, mode) {
76
84
  */
77
85
  function findTypedValues(values) {
78
86
  return _.map(values, (value) => {
79
- const type = Object.keys(value)[0];
87
+ const [type] = Object.keys(value);
80
88
  switch (dataTypes[type]) {
81
89
  case 'value':
82
90
  return value[type];
@@ -106,13 +114,13 @@ function notTrackable(value) {
106
114
  }
107
115
 
108
116
  /**
109
- * Extracts all strings from a dynanmo document client payload
117
+ * Extracts all strings from a dynamo document client payload
110
118
  *
111
119
  * Note: There are other data types dynamo supports
112
- * to cast to AttributeValue types but we currently
120
+ * to cast to AttributeValue types, but we currently
113
121
  * do not track null, Boolean, Number, Buffer(a few types here)
114
122
  *
115
- * @param {Object} values for all keys in ExpressionAttributeValues, ExclusiveStartKey or ScanFilter
123
+ * @param {Object} values for all keys in relevant properties
116
124
  * @return {Array} all values
117
125
  */
118
126
  function findValues(values) {
@@ -138,7 +146,7 @@ module.exports = ({ common }) => {
138
146
  /**
139
147
  * Registers the hooks for client and document client scan
140
148
  */
141
- dynamoSink.handle = function() {
149
+ dynamoSink.handle = function () {
142
150
  moduleHook.resolve({ name: moduleName }, (aws) => {
143
151
  const client = aws.DynamoDB.prototype;
144
152
 
@@ -160,24 +168,25 @@ module.exports = ({ common }) => {
160
168
  return;
161
169
  }
162
170
 
163
- // since this is instrumenting a wrapper we only want to to say the args
171
+ // since this is instrumenting a wrapper we only want to say the args
164
172
  // were the 2nd arg which is the dynamo db payload
165
173
  const ctxtData = {
166
174
  args: [data.args[1]],
167
175
  obj: data.obj,
168
- result: data.result
176
+ result: data.result,
169
177
  };
170
- const values = extractValues(data.args[1], 'client');
178
+
179
+ const values = extractValues(data.args[1], 'client', 'v2');
171
180
  dynamoSink.check({
172
181
  values,
173
182
  methodName: 'DynamoDB.prototype.scan',
174
- data: ctxtData
183
+ data: ctxtData,
175
184
  });
176
185
  }
177
- }
186
+ },
178
187
  });
179
- // DocumentClient added in aws-sdk 2.2. Some customers still on more
180
- // ancient versions
188
+
189
+ // DocumentClient added in aws-sdk 2.2.
181
190
  if (aws.DynamoDB.DocumentClient) {
182
191
  const docClient = aws.DynamoDB.DocumentClient.prototype;
183
192
  /**
@@ -192,32 +201,58 @@ module.exports = ({ common }) => {
192
201
  if (data.args[0] === 'scan') {
193
202
  requests.add(data.args[1]);
194
203
  }
195
- }
204
+ },
196
205
  });
197
206
 
198
207
  patcher.patch(docClient, 'scan', {
199
208
  name: 'aws-sdk.DynamoDB.DocumentClient.prototype',
200
209
  patchType: PATCH_TYPES.ASSESS_SINK,
201
210
  post(data) {
202
- const values = extractValues(data.args[0], 'docClient');
211
+ const values = extractValues(data.args[0], 'docClient', 'v2');
203
212
  dynamoSink.check({
204
213
  values,
205
214
  methodName: 'DynamoDB.DocumentClient.prototype.scan',
206
- data
215
+ data,
207
216
  });
208
- }
217
+ },
209
218
  });
210
219
  }
211
220
  });
212
221
 
222
+ moduleHook.resolve({ name: moduleNameV3 }, (aws) => {
223
+ const client = aws.DynamoDBClient.prototype;
224
+
225
+ patcher.patch(client, 'send', {
226
+ name: `${moduleNameV3}.ScanCommand.prototype`,
227
+ patchType: PATCH_TYPES.ASSESS_SINK,
228
+ alwaysRun: true,
229
+ post(data) {
230
+ if (!AsyncStorage.getContext()) return;
231
+
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
+ });
243
+ }
244
+ },
245
+ });
246
+ });
247
+
213
248
  return dynamoSink;
214
249
  };
215
250
 
216
- dynamoSink.check = function({ data, values, methodName }) {
251
+ dynamoSink.check = function ({ data, values, methodName }) {
217
252
  const vulnerableString = _.compact(
218
253
  values.map((input) =>
219
- isVulnerable({ disallowedTags, requiredTags, input })
220
- )
254
+ isVulnerable({ disallowedTags, requiredTags, input }),
255
+ ),
221
256
  );
222
257
 
223
258
  if (vulnerableString.length) {
@@ -22,7 +22,7 @@ const logger = require('../../core/logger')('contrast:rules:static');
22
22
  const _ = require('lodash');
23
23
  const generate = require('@babel/generator').default;
24
24
  const { createHash } = require('../../util/trace-util');
25
- const natives = process.binding('natives'); // eslint-disable-line node/no-deprecated-api
25
+ const natives = process.binding('natives');
26
26
 
27
27
  module.exports = ({ agent }) => {
28
28
  const rule = {};
@@ -62,8 +62,8 @@ module.exports = ({ agent }) => {
62
62
  hash: createHash([ruleId, filename, lineNum]),
63
63
  // some empty props required for backwards compatibility
64
64
  props: {
65
- value: "''", // eslint-disable-line
66
- signature: "''", // eslint-disable-line
65
+ value: "''",
66
+ signature: "''",
67
67
  access: 1,
68
68
  name,
69
69
  className: '""',
@@ -0,0 +1,40 @@
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 agentEmitter = require('../../agent-emitter');
18
+ const logger = require('../../core/logger')('contrast:rules:static');
19
+ const fs = require('fs');
20
+
21
+
22
+ module.exports = (agent) => {
23
+ const cacheDir = agent.getCacheDir();
24
+
25
+ if (cacheDir && fs.existsSync(cacheDir)) {
26
+ try {
27
+ const { findings } = JSON.parse(fs.readFileSync(`${cacheDir}/contrast-static-findings.json`));
28
+ if (findings && findings.length) {
29
+ findings.forEach((finding) => {
30
+ const { ruleId } = finding;
31
+ const { source, lineNumber, codeSource } = finding.props;
32
+ agentEmitter.emit('finding', finding);
33
+ logger.debug('%s: %s:%d %s', ruleId, source, lineNumber, codeSource);
34
+ });
35
+ }
36
+ } catch {
37
+ logger.debug('No valid cli-rewriter static findings file found. If you\'ve used cli-rewriter feature static findings will be missing');
38
+ }
39
+ }
40
+ };
@@ -12,17 +12,16 @@ 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
+ 'use strict';
16
+
17
+ /*
18
+ These technologies should also exist in teamserver/teamserver-app/src/main/resources/flowmap/technologies.json
19
+ Databases and loggers will be listed as "Service", frameworks, templating and transpilers as "Presentation"
20
+ Transpilers are grouped as "Transpiled JavaScript" and templates as "Templating"
21
+ */
22
+
15
23
  const technologies = {
16
- databases: [
17
- // mysql
18
- 'mysql',
19
- // postgres
20
- 'pg',
21
- // mongo
22
- 'mongodb',
23
- // rethink
24
- 'rethinkdb'
25
- ],
24
+ databases: ['mysql', 'pg', 'mongodb', 'rethinkdb'],
26
25
  http: [
27
26
  '@hapi/hapi',
28
27
  'hapi',
@@ -32,16 +31,16 @@ const technologies = {
32
31
  'restify',
33
32
  'loopback',
34
33
  'kraken-js',
35
- 'sails'
34
+ 'sails',
36
35
  ],
37
36
  templating: ['jade', 'ejs', 'nunjucks', 'mustache', 'dust', 'handlebars'],
38
37
  loggers: ['winston', 'debug'],
39
38
  mvc: ['meteor'],
40
- transpilers: ['babel', 'escodegen'] // XXX is this necessary?
39
+ transpilers: ['babel', 'escodegen'],
41
40
  };
42
41
 
43
42
  let all = [];
44
- Object.keys(technologies).forEach(function(type) {
43
+ Object.keys(technologies).forEach(function (type) {
45
44
  all = all.concat(technologies[type]);
46
45
  });
47
46
 
@@ -26,11 +26,13 @@ const loggerFactory = require('../core/logger');
26
26
  const configOptions = require('../core/config/options');
27
27
  const configUtil = require('../core/config/util');
28
28
  const { ContrastAgent } = require('../agent');
29
+ const agentEmitter = require('../agent-emitter');
29
30
  const moduleHelper = require('../hooks/module/helpers');
30
31
  const injections = require('../core/rewrite/injections');
31
32
 
32
33
  const readFile = util.promisify(fs.readFile);
33
34
  const LOGGER_NS = 'contrast:cli-rewriter';
35
+ const getType = require('../util/get-file-type');
34
36
 
35
37
  class CLIRewriter {
36
38
  /**
@@ -53,6 +55,7 @@ class CLIRewriter {
53
55
  }
54
56
 
55
57
  this.initAgent();
58
+ this.initStaticRulesVisitors();
56
59
  this.initRewriter();
57
60
  }
58
61
 
@@ -75,9 +78,10 @@ class CLIRewriter {
75
78
  loggerFactory.init(this.config);
76
79
 
77
80
  const cacheEnabled = this.config.get('agent.node.rewrite_cache.enable');
78
- if (!cacheEnabled) {
81
+ const assessEnabled = this.config.get('assess.enable');
82
+ if (!cacheEnabled || !assessEnabled) {
79
83
  this.logger.error(
80
- `fatal configuration error: contrast-transpile requires 'agent.node.rewrite_cache.enable=true'`
84
+ 'fatal configuration error: contrast-transpile requires \'agent.node.rewrite_cache.enable=true\' AND \'assess.enable=true'
81
85
  );
82
86
  process.exit(1);
83
87
  }
@@ -94,9 +98,34 @@ class CLIRewriter {
94
98
  this.agent = new ContrastAgent();
95
99
  this.agent.config = this.config;
96
100
  this.agent.staticVisitors.push(CLIRewriter.requireDetector);
101
+ this.agent.staticVisitors.push(CLIRewriter.importDetector);
97
102
  this.agent.appInfo = new AppInfo(this.entrypoint, this.config);
98
103
  }
99
104
 
105
+ initStaticRulesVisitors() {
106
+ const { logger } = this;
107
+ const cacheDir = this.agent.getCacheDir();
108
+ const findingsFilePath = `${cacheDir}/contrast-static-findings.json`;
109
+ const rule = require('../assess/static/hardcoded')({ agent: this.agent });
110
+ const policy = require('../assess/policy/non-dataflow-rules.json');
111
+
112
+ agentEmitter.on('finding', function writeFindingToAFile (finding) {
113
+ try {
114
+ if (!fs.existsSync(findingsFilePath)) {
115
+ if (!fs.existsSync(cacheDir)) {
116
+ fs.mkdirSync(cacheDir, { recursive: true });
117
+ }
118
+ fs.writeFileSync(findingsFilePath, '{\n"findings":\n[\n');
119
+ }
120
+ fs.appendFileSync(`${cacheDir}/contrast-static-findings.json`, `${JSON.stringify(finding)},\n`);
121
+ } catch (error) {
122
+ logger.error('Error writing to static findings file. Static finding won\'t be reported. Error: %s', error);
123
+ }
124
+ });
125
+ rule.handle(policy.rules['hardcoded-password'], 'hardcoded-password');
126
+ rule.handle(policy.rules['hardcoded-key'], 'hardcoded-key');
127
+ }
128
+
100
129
  /**
101
130
  * Loads babel rewriter and utils an initializes them.
102
131
  */
@@ -119,9 +148,23 @@ class CLIRewriter {
119
148
  async rewrite() {
120
149
  const parent = CLIRewriter.getModuleData(this.filename);
121
150
  const start = Date.now();
151
+ const cacheDir = this.agent.getCacheDir();
152
+ const findingsFilePath = `${cacheDir}/contrast-static-findings.json`;
122
153
 
123
154
  await this.traverse(this.filename, parent);
124
155
 
156
+ if (fs.existsSync(findingsFilePath)) {
157
+ try {
158
+ const findingsFileSize = fs.statSync(findingsFilePath).size;
159
+ if (findingsFileSize > 20) {
160
+ fs.truncateSync(findingsFilePath, findingsFileSize - 2);
161
+ }
162
+ fs.appendFileSync(findingsFilePath, '\n]\n}');
163
+ } catch (error) {
164
+ this.logger.error('Error formatting static findings file. Static findings won\'t be reported. Error: %s', error);
165
+ }
166
+ }
167
+
125
168
  this.logger.info('rewriting complete [%ss]', (Date.now() - start) / 1000);
126
169
  }
127
170
 
@@ -133,13 +176,14 @@ class CLIRewriter {
133
176
  * @param {object} parent parent module data
134
177
  */
135
178
  async traverse(filename, parent) {
136
- const fileDependencies = await this.visitDependency(filename);
179
+ const type = getType(filename);
180
+ const fileDependencies = await this.visitDependency(filename, type);
137
181
 
138
182
  return Promise.all(
139
183
  fileDependencies.map((request) => {
140
184
  try {
141
185
  const _filename = Module._resolveFilename(request, parent);
142
- if (_filename.endsWith('.js')) {
186
+ if (_filename.endsWith('.js') || _filename.endsWith('.mjs')) {
143
187
  const _parent = CLIRewriter.getModuleData(_filename);
144
188
  return this.traverse(_filename, _parent);
145
189
  }
@@ -162,7 +206,7 @@ class CLIRewriter {
162
206
  * @param {string} filename name of file to rewrite / cache
163
207
  * @returns {array[string]} list of the file's dependencies
164
208
  */
165
- async visitDependency(filename) {
209
+ async visitDependency(filename, type) {
166
210
  if (this.visited.has(filename)) {
167
211
  return [];
168
212
  }
@@ -170,7 +214,9 @@ class CLIRewriter {
170
214
  this.visited.add(filename);
171
215
 
172
216
  const content = await readFile(filename, 'utf8');
173
- const rewriteData = this.rewriter.rewriteFile(content, filename);
217
+ const rewriteData = this.rewriter.rewriteFile(content, filename, {
218
+ sourceType: type
219
+ });
174
220
 
175
221
  if (rewriteData.code) {
176
222
  await moduleHelper.cacheWithSourceMap(this.agent, filename, rewriteData);
@@ -213,6 +259,19 @@ class CLIRewriter {
213
259
  }
214
260
  }
215
261
  }
262
+
263
+ /**
264
+ * Added to agent's static visitors. Runs during rewriting to detect import
265
+ * calls to discover dependencies.
266
+ * @param {object} node AST node being visited during rewrite
267
+ * @param {string} filename file being rewritten
268
+ * @param {object} state rewriter state
269
+ */
270
+ static importDetector(node, filename, state) {
271
+ if (node.type === 'ImportDeclaration') {
272
+ state.deps.push(node.source.extra.rawValue);
273
+ }
274
+ }
216
275
  }
217
276
 
218
277
  /**