@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.
- package/bootstrap.js +2 -3
- package/esm.mjs +9 -35
- package/lib/assess/membrane/debraner.js +0 -2
- package/lib/assess/membrane/index.js +1 -3
- package/lib/assess/models/tag-range/util.js +1 -2
- package/lib/assess/policy/propagators.json +13 -4
- package/lib/assess/policy/rules.json +42 -0
- package/lib/assess/policy/signatures.json +18 -0
- package/lib/assess/policy/util.js +3 -2
- package/lib/assess/propagators/JSON/stringify.js +6 -11
- package/lib/assess/propagators/ajv/conditionals.js +0 -3
- package/lib/assess/propagators/ajv/json-schema-type-evaluators.js +5 -4
- package/lib/assess/propagators/ajv/refs.js +1 -2
- package/lib/assess/propagators/ajv/schema-context.js +2 -3
- package/lib/assess/propagators/joi/any.js +1 -1
- package/lib/assess/propagators/joi/object.js +1 -1
- package/lib/assess/propagators/joi/string-base.js +16 -3
- package/lib/assess/propagators/mongoose/map.js +1 -1
- package/lib/assess/propagators/mongoose/mixed.js +1 -1
- package/lib/assess/propagators/mongoose/string.js +1 -1
- package/lib/assess/propagators/path/common.js +38 -29
- package/lib/assess/propagators/path/resolve.js +1 -0
- package/lib/assess/propagators/sequelize/utils.js +1 -2
- package/lib/assess/propagators/v8/init-hooks.js +0 -1
- package/lib/assess/sinks/dynamo.js +65 -30
- package/lib/assess/static/hardcoded.js +3 -3
- package/lib/assess/static/read-findings-from-cache.js +40 -0
- package/lib/assess/technologies/index.js +12 -13
- package/lib/cli-rewriter/index.js +65 -6
- package/lib/core/async-storage/hooks/mysql.js +57 -6
- package/lib/core/config/options.js +12 -6
- package/lib/core/config/util.js +15 -33
- package/lib/core/exclusions/input.js +6 -1
- package/lib/core/express/index.js +2 -4
- package/lib/core/logger/debug-logger.js +2 -2
- package/lib/core/stacktrace.js +2 -1
- package/lib/hooks/http.js +81 -81
- package/lib/hooks/require.js +1 -0
- package/lib/instrumentation.js +17 -0
- package/lib/protect/analysis/aho-corasick.js +1 -1
- package/lib/protect/errors/handler-async-errors.js +66 -0
- package/lib/protect/input-analysis.js +7 -13
- package/lib/protect/listeners.js +27 -23
- package/lib/protect/rules/base-scanner/index.js +2 -2
- package/lib/protect/rules/bot-blocker/bot-blocker-rule.js +4 -2
- package/lib/protect/rules/cmd-injection/cmdinjection-rule.js +57 -2
- package/lib/protect/rules/cmd-injection-semantic-chained-commands/cmd-injection-semantic-chained-commands-rule.js +31 -2
- package/lib/protect/rules/cmd-injection-semantic-dangerous-paths/cmd-injection-semantic-dangerous-paths-rule.js +32 -2
- package/lib/protect/rules/index.js +42 -21
- package/lib/protect/rules/ip-denylist/ip-denylist-rule.js +2 -2
- package/lib/protect/rules/nosqli/nosql-injection-rule.js +104 -39
- package/lib/protect/rules/path-traversal/path-traversal-rule.js +3 -0
- package/lib/protect/rules/rule-factory.js +6 -7
- package/lib/protect/rules/signatures/signature.js +3 -0
- package/lib/protect/rules/sqli/sql-injection-rule.js +98 -5
- package/lib/protect/rules/sqli/sql-scanner/labels.json +0 -3
- package/lib/protect/rules/xss/reflected-xss-rule.js +3 -3
- package/lib/protect/sample-aggregator.js +65 -57
- package/lib/protect/service.js +709 -104
- package/lib/reporter/models/app-activity/sample.js +6 -0
- package/lib/reporter/speedracer/unknown-connection-state.js +20 -32
- package/lib/reporter/translations/to-protobuf/settings/assess-features.js +4 -6
- package/lib/reporter/ts-reporter.js +1 -1
- package/lib/util/get-file-type.js +43 -0
- package/package.json +11 -11
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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 (
|
|
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[
|
|
248
|
+
if (resultMeta.str[segmentOffset + segmentValue.length] === '.') {
|
|
237
249
|
accumulator.fileDotSeparator = 1;
|
|
238
250
|
}
|
|
239
|
-
|
|
240
|
-
|
|
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 =
|
|
284
|
-
|
|
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,
|
|
294
|
-
argMeta.offset = calculateNewOffset(argMeta.offset,
|
|
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(
|
|
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
|
-
|
|
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-
|
|
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
|
|
31
|
-
|
|
32
|
-
'ExclusiveStartKey',
|
|
33
|
-
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
180
|
-
//
|
|
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');
|
|
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: "''",
|
|
66
|
-
signature: "''",
|
|
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']
|
|
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
|
-
|
|
81
|
+
const assessEnabled = this.config.get('assess.enable');
|
|
82
|
+
if (!cacheEnabled || !assessEnabled) {
|
|
79
83
|
this.logger.error(
|
|
80
|
-
|
|
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
|
|
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
|
/**
|