@contrast/agent 4.12.2 → 4.13.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/esm.mjs +1 -32
- package/lib/assess/sinks/dynamo.js +65 -30
- 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/config/options.js +6 -0
- 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/hooks/http.js +81 -81
- package/lib/hooks/require.js +1 -0
- package/lib/instrumentation.js +17 -0
- 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 +103 -38
- package/lib/protect/rules/path-traversal/path-traversal-rule.js +3 -0
- package/lib/protect/rules/rule-factory.js +6 -6
- 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/ts-reporter.js +1 -1
- package/lib/util/get-file-type.js +47 -0
- package/package.json +5 -3
package/esm.mjs
CHANGED
|
@@ -24,45 +24,14 @@ if (enabled) {
|
|
|
24
24
|
await loader.resetArgs(process.argv[0], process.argv[1]);
|
|
25
25
|
const { readFile } = require('fs').promises;
|
|
26
26
|
|
|
27
|
-
const path = require('path');
|
|
28
27
|
const agent = require(`./lib/agent.js`);
|
|
29
28
|
const logger = require(`./lib/core/logger/index.js`)('contrast:esm-loaders');
|
|
30
29
|
const rewriter = require(`./lib/core/rewrite/index.js`)(agent);
|
|
31
30
|
const helpers = require(`./lib/hooks/module/helpers.js`);
|
|
32
|
-
const
|
|
31
|
+
const getType = require(`./lib/util/get-file-type.js`);
|
|
33
32
|
|
|
34
33
|
const loadedFromCache = new Set();
|
|
35
34
|
|
|
36
|
-
function getType(url) {
|
|
37
|
-
const {protocol, pathname} = new URL(url);
|
|
38
|
-
|
|
39
|
-
let parentType = 'commonjs';
|
|
40
|
-
try {
|
|
41
|
-
parentType = parent(pathname).parse().type;
|
|
42
|
-
} catch (err) {
|
|
43
|
-
// Node assumes `commonjs ` if there's no `type` set in package.json
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (protocol === 'node:') {
|
|
47
|
-
return 'builtin';
|
|
48
|
-
}
|
|
49
|
-
if (protocol === 'file:') {
|
|
50
|
-
const ext = path.extname(pathname);
|
|
51
|
-
if (
|
|
52
|
-
ext === '.mjs' ||
|
|
53
|
-
(ext === '.js' && parentType === 'module')
|
|
54
|
-
){
|
|
55
|
-
return 'module';
|
|
56
|
-
}
|
|
57
|
-
else if (
|
|
58
|
-
ext === '.cjs' ||
|
|
59
|
-
(ext === '.js' && parentType !== 'module')
|
|
60
|
-
){
|
|
61
|
-
return 'commonjs';
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
return 'unknown';
|
|
65
|
-
}
|
|
66
35
|
/**
|
|
67
36
|
* The `getSource` hook is used to provide a custom method for retrieving source
|
|
68
37
|
* code. In our case, we check for previously rewritten ESM files in our cache
|
|
@@ -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) {
|
|
@@ -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
|
/**
|
|
@@ -411,6 +411,12 @@ const agent = [
|
|
|
411
411
|
fn: castBoolean,
|
|
412
412
|
desc: 'do agent-native input analysis prior to any external analysis',
|
|
413
413
|
},
|
|
414
|
+
{
|
|
415
|
+
name: 'agent.node.analysis_log_dir',
|
|
416
|
+
arg: '<path>',
|
|
417
|
+
default: '.',
|
|
418
|
+
desc: 'directory to use for the native input analysis log file'
|
|
419
|
+
},
|
|
414
420
|
{
|
|
415
421
|
name: 'agent.node.unsafe.deadzones',
|
|
416
422
|
arg: '<modules>',
|
package/lib/core/config/util.js
CHANGED
|
@@ -15,6 +15,7 @@ Copyright: 2022 Contrast Security, Inc
|
|
|
15
15
|
'use strict';
|
|
16
16
|
const _ = require('lodash');
|
|
17
17
|
|
|
18
|
+
const os = require('os');
|
|
18
19
|
const process = require('process');
|
|
19
20
|
const path = require('path');
|
|
20
21
|
const fs = require('fs');
|
|
@@ -24,7 +25,6 @@ const stringify = require('json-stable-stringify');
|
|
|
24
25
|
const common = require('./options');
|
|
25
26
|
const configOptions = common.options;
|
|
26
27
|
const { configPathEnvVars } = common;
|
|
27
|
-
const fileFinder = require('../../util/file-finder');
|
|
28
28
|
const util = module.exports;
|
|
29
29
|
|
|
30
30
|
/**
|
|
@@ -143,37 +143,23 @@ class Config {
|
|
|
143
143
|
|
|
144
144
|
/**
|
|
145
145
|
* Find location of config given options and name of config file
|
|
146
|
-
* @param {Object} cliOptions
|
|
147
|
-
* @param {string} cliOptions.script
|
|
148
|
-
* @param {string} filename
|
|
149
146
|
* @return {string|void} path, if valid
|
|
150
147
|
*/
|
|
151
|
-
function checkConfigPath(
|
|
152
|
-
const configDir =
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
{ dir: configDir, attempts: 1 },
|
|
162
|
-
|
|
163
|
-
// The directory of the application under test such as node_modules/mocha/bin or server.
|
|
164
|
-
{ dir: path.dirname(script) },
|
|
165
|
-
|
|
166
|
-
// The directory of this Contrast agent module, assuming it
|
|
167
|
-
// came packaged with an enterprise-wide contrast.json.
|
|
168
|
-
{ dir: path.resolve(__dirname, '..', '..'), attempts: 1 }
|
|
169
|
-
];
|
|
170
|
-
|
|
171
|
-
for (const guess of guesses) {
|
|
172
|
-
const configPath = fileFinder.findFile(guess.dir, filename, guess.attempts);
|
|
173
|
-
if (configPath) return configPath;
|
|
148
|
+
function checkConfigPath() {
|
|
149
|
+
const configDir = os.platform() === 'win32'
|
|
150
|
+
? `${process.env['ProgramData']}\\contrast`
|
|
151
|
+
: '/etc/contrast';
|
|
152
|
+
|
|
153
|
+
for (const dir of [process.cwd(), configDir]) {
|
|
154
|
+
const checkPath = path.resolve(dir, 'contrast_security.yaml');
|
|
155
|
+
if (fs.existsSync(checkPath)) {
|
|
156
|
+
return checkPath;
|
|
157
|
+
}
|
|
174
158
|
}
|
|
159
|
+
return;
|
|
175
160
|
}
|
|
176
161
|
|
|
162
|
+
|
|
177
163
|
/**
|
|
178
164
|
* @param {Object} cliOptions
|
|
179
165
|
* @param {string} cliOptions.script
|
|
@@ -185,11 +171,7 @@ function getConfigPath(cliOptions) {
|
|
|
185
171
|
cliOptions.configFile ||
|
|
186
172
|
process.env[configPathEnvVars.path] ||
|
|
187
173
|
process.env[configPathEnvVars.deprecated] ||
|
|
188
|
-
checkConfigPath(
|
|
189
|
-
checkConfigPath(cliOptions, 'contrast_security.yml') ||
|
|
190
|
-
checkConfigPath(cliOptions, 'contrast.yaml') ||
|
|
191
|
-
checkConfigPath(cliOptions, 'contrast.yml') ||
|
|
192
|
-
checkConfigPath(cliOptions, 'contrast.json')
|
|
174
|
+
checkConfigPath()
|
|
193
175
|
);
|
|
194
176
|
}
|
|
195
177
|
|
|
@@ -312,7 +294,7 @@ function mergeCliOptions(cliOptions, logger) {
|
|
|
312
294
|
// set from default
|
|
313
295
|
if (value === undefined) {
|
|
314
296
|
if (required) {
|
|
315
|
-
logger.error(
|
|
297
|
+
logger.error('Missing required option \'%s\'', name);
|
|
316
298
|
return options;
|
|
317
299
|
}
|
|
318
300
|
|
|
@@ -29,7 +29,8 @@ const generalizedInputTypes = {
|
|
|
29
29
|
[INPUT_TYPES.COOKIE_VALUE]: EXCLUSION_INPUT_TYPES.COOKIE,
|
|
30
30
|
[INPUT_TYPES.HEADER]: EXCLUSION_INPUT_TYPES.HEADER,
|
|
31
31
|
[INPUT_TYPES.PARAMETER_NAME]: EXCLUSION_INPUT_TYPES.PARAMETER,
|
|
32
|
-
[INPUT_TYPES.PARAMETER_VALUE]: EXCLUSION_INPUT_TYPES.PARAMETER
|
|
32
|
+
[INPUT_TYPES.PARAMETER_VALUE]: EXCLUSION_INPUT_TYPES.PARAMETER,
|
|
33
|
+
[INPUT_TYPES.URL_PARAMETER]: EXCLUSION_INPUT_TYPES.PARAMETER
|
|
33
34
|
};
|
|
34
35
|
const { BODY, PARAMETER, QUERYSTRING } = EXCLUSION_INPUT_TYPES;
|
|
35
36
|
|
|
@@ -47,6 +48,10 @@ class InputExclusion extends UrlExclusion {
|
|
|
47
48
|
this.inputName = dtm.inputName;
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
shouldExclude(ruleId, type, name) {
|
|
52
|
+
return this.appliesToProtectRule(ruleId) && this.appliesToInputType(type) && this.matches(name);
|
|
53
|
+
}
|
|
54
|
+
|
|
50
55
|
appliesToInputType(type) {
|
|
51
56
|
return (
|
|
52
57
|
this.inputType === InputExclusion.generalizeInputType(type) ||
|
|
@@ -237,7 +237,7 @@ class ExpressFramework {
|
|
|
237
237
|
|
|
238
238
|
if (!app || !app.defaultConfiguration) {
|
|
239
239
|
logger.error(
|
|
240
|
-
|
|
240
|
+
'non-express application mistakenly registered',
|
|
241
241
|
new Error().stack,
|
|
242
242
|
);
|
|
243
243
|
return;
|
|
@@ -336,9 +336,7 @@ class ExpressFramework {
|
|
|
336
336
|
}, 'textParser');
|
|
337
337
|
|
|
338
338
|
this.useAfter(function ContrastBodyParsed(req, res, next) {
|
|
339
|
-
agentEmitter.emit(EVENTS.BODY_PARSED, req, res,
|
|
340
|
-
type: INPUT_TYPES.BODY,
|
|
341
|
-
});
|
|
339
|
+
agentEmitter.emit(EVENTS.BODY_PARSED, req, res, INPUT_TYPES.BODY);
|
|
342
340
|
next();
|
|
343
341
|
}, 'urlencodedParser');
|
|
344
342
|
|