@contrast/assess 1.39.0 → 1.41.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/lib/dataflow/propagation/install/path/index.test.js +1 -1
- package/lib/dataflow/sinks/install/express/reflected-xss.js +1 -1
- package/lib/dataflow/sinks/install/express/unvalidated-redirect.js +1 -1
- package/lib/dataflow/sources/install/body-parser1.js +0 -1
- package/lib/dataflow/sources/install/body-parser1.test.js +4 -8
- package/lib/dataflow/sources/install/cookie-parser1.js +0 -1
- package/lib/dataflow/sources/install/cookie-parser1.test.js +2 -4
- package/lib/dataflow/sources/install/express/params.js +56 -37
- package/lib/dataflow/sources/install/express/params.test.js +80 -73
- package/lib/dataflow/sources/install/express/parsedUrl.js +45 -28
- package/lib/dataflow/sources/install/express/parsedUrl.test.js +70 -29
- package/lib/dataflow/sources/install/qs6.js +0 -1
- package/lib/dataflow/sources/install/restify/router.js +0 -1
- package/lib/dataflow/sources/install/restify/router.test.js +3 -5
- package/lib/get-source-context.js +33 -12
- package/lib/get-source-context.test.js +33 -5
- package/lib/make-source-context.js +6 -3
- package/lib/sampler/common.js +156 -0
- package/lib/sampler/common.test.js +101 -0
- package/lib/{sampler.js → sampler/index.js} +25 -59
- package/lib/{sampler.test.js → sampler/index.test.js} +37 -25
- package/package.json +10 -9
- package/lib/dataflow/sinks/install/fs-original.js +0 -170
|
@@ -6,26 +6,29 @@ const { Event } = require('@contrast/common');
|
|
|
6
6
|
const mocks = require('@contrast/test/mocks');
|
|
7
7
|
const { devNull } = require('node:os');
|
|
8
8
|
|
|
9
|
+
const initSampler = require('.');
|
|
10
|
+
const { SamplingStrategies: { AssessTurnedOff, Probabilistic } } = require('./common');
|
|
11
|
+
|
|
9
12
|
const TRIALS = 1000;
|
|
10
13
|
const ZSCORE = 3.891; // 99.99% confidence
|
|
11
14
|
|
|
12
15
|
describe('assess sampler', function() {
|
|
13
16
|
it('sampling is disabled by default and assess.sampler is null', function() {
|
|
14
17
|
const core = initMockCore();
|
|
15
|
-
|
|
18
|
+
initSampler(core);
|
|
16
19
|
expect(core.assess.sampler).to.be.null;
|
|
17
20
|
});
|
|
18
21
|
|
|
19
|
-
it('samples at default rate of 0.
|
|
22
|
+
it('samples at default rate of 0.10', function() {
|
|
20
23
|
const core = initMockCore();
|
|
21
24
|
core.config.setValue('assess.enable', true, 'CONTRAST_UI');
|
|
22
25
|
core.config.setValue('assess.probabilistic_sampling.enable', true, 'CONTRAST_UI');
|
|
23
26
|
// initialize after configs are set
|
|
24
|
-
const sampler =
|
|
27
|
+
const sampler = initSampler(core);
|
|
25
28
|
|
|
26
29
|
// verify configs set in before setup
|
|
27
|
-
expect(sampler).to.have.property('strategy',
|
|
28
|
-
expect(sampler.opts).to.have.property('base_probability', 0.
|
|
30
|
+
expect(sampler).to.have.property('strategy', Probabilistic);
|
|
31
|
+
expect(sampler.opts).to.have.property('base_probability', 0.10);
|
|
29
32
|
expect(sampler).to.have.property('getSampleInfo').to.be.a('function');
|
|
30
33
|
// test behavior
|
|
31
34
|
testProbabilisticSampler(sampler);
|
|
@@ -36,10 +39,10 @@ describe('assess sampler', function() {
|
|
|
36
39
|
core.config.setValue('assess.probabilistic_sampling.enable', true, 'CONTRAST_UI');
|
|
37
40
|
core.config.setValue('assess.probabilistic_sampling.base_probability', 0.25, 'CONTRAST_UI');
|
|
38
41
|
// initialize after configs are set
|
|
39
|
-
const sampler =
|
|
42
|
+
const sampler = initSampler(core);
|
|
40
43
|
|
|
41
44
|
// verify configs
|
|
42
|
-
expect(sampler).to.have.property('strategy',
|
|
45
|
+
expect(sampler).to.have.property('strategy', Probabilistic);
|
|
43
46
|
expect(sampler.opts).to.have.property('base_probability', 0.25);
|
|
44
47
|
expect(sampler).to.have.property('getSampleInfo').to.be.a('function');
|
|
45
48
|
// test behavior
|
|
@@ -51,12 +54,12 @@ describe('assess sampler', function() {
|
|
|
51
54
|
core.config.setValue('assess.probabilistic_sampling.enable', true, 'CONTRAST_UI');
|
|
52
55
|
core.config.setValue('assess.probabilistic_sampling.base_probability', 0.25, 'CONTRAST_UI');
|
|
53
56
|
// initialize after configs are set
|
|
54
|
-
|
|
57
|
+
initSampler(core);
|
|
55
58
|
// don't destructure to just { sampler } since the value of assess.sampler changes on update.
|
|
56
59
|
const { assess } = core;
|
|
57
60
|
|
|
58
61
|
// verify configs
|
|
59
|
-
expect(assess.sampler).to.have.property('strategy',
|
|
62
|
+
expect(assess.sampler).to.have.property('strategy', Probabilistic);
|
|
60
63
|
expect(assess.sampler.opts).to.have.property('base_probability', 0.25);
|
|
61
64
|
expect(assess.sampler).to.have.property('getSampleInfo').to.be.a('function');
|
|
62
65
|
// test behavior
|
|
@@ -81,12 +84,12 @@ describe('assess sampler', function() {
|
|
|
81
84
|
expect(core.logger.info.getCalls().map(c => c.args)).to.deep.equal([
|
|
82
85
|
// when initialized
|
|
83
86
|
[
|
|
84
|
-
{ strategy:
|
|
87
|
+
{ strategy: Probabilistic, opts: { base_probability: 0.25, route_monitor: { enable: true, ttl_ms: 1800000 } } },
|
|
85
88
|
'updating assess sampler'
|
|
86
89
|
],
|
|
87
90
|
// update 1
|
|
88
91
|
[
|
|
89
|
-
{ strategy:
|
|
92
|
+
{ strategy: Probabilistic, opts: { base_probability: 0.5, route_monitor: { enable: true, ttl_ms: 1800000 } } },
|
|
90
93
|
'updating assess sampler'
|
|
91
94
|
]
|
|
92
95
|
]);
|
|
@@ -98,12 +101,12 @@ describe('assess sampler', function() {
|
|
|
98
101
|
core.config.setValue('assess.probabilistic_sampling.enable', true, 'CONTRAST_UI');
|
|
99
102
|
core.config.setValue('assess.probabilistic_sampling.base_probability', 0.50, 'CONTRAST_UI');
|
|
100
103
|
// initializes after configs are set
|
|
101
|
-
|
|
104
|
+
initSampler(core);
|
|
102
105
|
// don't destructure to just { sampler } since the value of assess.sampler changes on update.
|
|
103
106
|
const { assess } = core;
|
|
104
107
|
|
|
105
108
|
// verify configs set in before setup
|
|
106
|
-
expect(assess.sampler).to.have.property('strategy',
|
|
109
|
+
expect(assess.sampler).to.have.property('strategy', Probabilistic);
|
|
107
110
|
expect(assess.sampler.opts).to.have.property('base_probability', 0.50);
|
|
108
111
|
expect(assess.sampler).to.have.property('getSampleInfo').to.be.a('function');
|
|
109
112
|
// test behavior
|
|
@@ -115,7 +118,7 @@ describe('assess sampler', function() {
|
|
|
115
118
|
});
|
|
116
119
|
|
|
117
120
|
// verify updated sampling strategy
|
|
118
|
-
expect(assess.sampler.strategy).to.equal(
|
|
121
|
+
expect(assess.sampler.strategy).to.equal(AssessTurnedOff);
|
|
119
122
|
// test behavior
|
|
120
123
|
testDisabledSampler(assess.sampler);
|
|
121
124
|
|
|
@@ -123,12 +126,12 @@ describe('assess sampler', function() {
|
|
|
123
126
|
expect(core.logger.info.getCalls().map(c => c.args)).to.deep.equal([
|
|
124
127
|
// when initialized
|
|
125
128
|
[
|
|
126
|
-
{ strategy:
|
|
129
|
+
{ strategy: Probabilistic, opts: { base_probability: 0.5, route_monitor: { enable: true, ttl_ms: 1800000 } } },
|
|
127
130
|
'updating assess sampler'
|
|
128
131
|
],
|
|
129
132
|
// update 1
|
|
130
133
|
[
|
|
131
|
-
{ strategy:
|
|
134
|
+
{ strategy: AssessTurnedOff, opts: undefined },
|
|
132
135
|
'updating assess sampler'
|
|
133
136
|
]
|
|
134
137
|
]);
|
|
@@ -142,11 +145,13 @@ describe('assess sampler', function() {
|
|
|
142
145
|
process.env.CONTRAST_CONFIG_PATH = cfgPath;
|
|
143
146
|
|
|
144
147
|
core.config.setValue('assess.enable', true, 'CONTRAST_UI');
|
|
145
|
-
core.config.setValue('assess.probabilistic_sampling.enable',
|
|
148
|
+
// core.config.setValue('assess.probabilistic_sampling.enable', true, 'DEFAULT_VALUE');
|
|
146
149
|
core.config.setValue('assess.probabilistic_sampling.base_probability', 0.50, 'CONTRAST_UI');
|
|
150
|
+
|
|
147
151
|
// initializes after configs are set
|
|
148
|
-
|
|
149
|
-
|
|
152
|
+
initSampler(core);
|
|
153
|
+
|
|
154
|
+
// don't destructure to just { sampler } since the value of assess.sampler changes on update
|
|
150
155
|
const { assess } = core;
|
|
151
156
|
|
|
152
157
|
// disabled by config
|
|
@@ -176,7 +181,7 @@ describe('assess sampler', function() {
|
|
|
176
181
|
});
|
|
177
182
|
|
|
178
183
|
// verify updated sampling strategy
|
|
179
|
-
expect(assess.sampler.strategy).to.equal(
|
|
184
|
+
expect(assess.sampler.strategy).to.equal(AssessTurnedOff);
|
|
180
185
|
// test behavior
|
|
181
186
|
testDisabledSampler(assess.sampler);
|
|
182
187
|
|
|
@@ -200,19 +205,19 @@ describe('assess sampler', function() {
|
|
|
200
205
|
expect(core.logger.info.getCalls().map(c => c.args)).to.deep.equal([
|
|
201
206
|
// update 1: env=PRODUCTION enables sampling
|
|
202
207
|
[
|
|
203
|
-
{ strategy:
|
|
208
|
+
{ strategy: Probabilistic, opts: { base_probability: 0.5, route_monitor: { enable: true, ttl_ms: 1800000 } } },
|
|
204
209
|
'updating assess sampler'
|
|
205
210
|
],
|
|
206
211
|
// update 2: env = DEVELOPMENT disables sampling
|
|
207
212
|
['assess sampling disabled'],
|
|
208
213
|
// update 3: assess.enable=false sets disabled sampling strategy
|
|
209
214
|
[
|
|
210
|
-
{ strategy:
|
|
215
|
+
{ strategy: AssessTurnedOff, opts: undefined },
|
|
211
216
|
'updating assess sampler'
|
|
212
217
|
],
|
|
213
218
|
// update 5: assess.enable=true and request_frequency=50 re-enables assess and new sampling probability
|
|
214
219
|
[
|
|
215
|
-
{ strategy:
|
|
220
|
+
{ strategy: Probabilistic, opts: { base_probability: 0.02, route_monitor: { enable: true, ttl_ms: 1800000 } } },
|
|
216
221
|
'updating assess sampler'
|
|
217
222
|
]
|
|
218
223
|
]);
|
|
@@ -236,6 +241,8 @@ function initMockCore() {
|
|
|
236
241
|
// use actual config so we can get dynamic effective values with TS message
|
|
237
242
|
// updates (mock doesn't). we can also test new effective config mappings.
|
|
238
243
|
require('@contrast/config')(core);
|
|
244
|
+
require('@contrast/route-coverage')(core);
|
|
245
|
+
|
|
239
246
|
core.config.setValue('assess.enable', true, 'CONTRAST_UI');
|
|
240
247
|
} catch (err) {
|
|
241
248
|
process.env.CONTRAST_CONFIG_PATH = CONTRAST_CONFIG_PATH;
|
|
@@ -274,15 +281,20 @@ function getStats(p) {
|
|
|
274
281
|
|
|
275
282
|
function testProbabilisticSampler(sampler) {
|
|
276
283
|
const { opts } = sampler;
|
|
284
|
+
const store = {
|
|
285
|
+
assess: {
|
|
286
|
+
reqData: { uriPath: '/foo' }
|
|
287
|
+
}
|
|
288
|
+
};
|
|
277
289
|
|
|
278
290
|
// run trials and count how many times sampler says okay to analyze
|
|
279
291
|
let observedSampleCount = 0;
|
|
280
292
|
for (let n = 0; n < TRIALS; n++) {
|
|
281
|
-
const info = sampler.getSampleInfo();
|
|
293
|
+
const info = sampler.getSampleInfo({ store });
|
|
282
294
|
if (info.canSample) observedSampleCount++;
|
|
283
295
|
}
|
|
284
296
|
|
|
285
|
-
const stats = getStats(opts.base_probability
|
|
297
|
+
const stats = getStats(opts.base_probability);
|
|
286
298
|
const { confidenceInterval: [low, high] } = stats;
|
|
287
299
|
|
|
288
300
|
// for debugging/visibility
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contrast/assess",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.41.0",
|
|
4
4
|
"description": "Contrast service providing framework-agnostic Assess support",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"author": "Contrast Security <nodejs@contrastsecurity.com> (https://www.contrastsecurity.com)",
|
|
@@ -18,15 +18,16 @@
|
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@contrast/common": "1.26.0",
|
|
21
|
-
"@contrast/config": "1.
|
|
22
|
-
"@contrast/core": "1.
|
|
23
|
-
"@contrast/dep-hooks": "1.
|
|
21
|
+
"@contrast/config": "1.36.0",
|
|
22
|
+
"@contrast/core": "1.41.1",
|
|
23
|
+
"@contrast/dep-hooks": "1.10.0",
|
|
24
24
|
"@contrast/distringuish": "^5.1.0",
|
|
25
|
-
"@contrast/instrumentation": "1.
|
|
26
|
-
"@contrast/logger": "1.
|
|
27
|
-
"@contrast/patcher": "1.
|
|
28
|
-
"@contrast/rewriter": "1.
|
|
29
|
-
"@contrast/
|
|
25
|
+
"@contrast/instrumentation": "1.20.0",
|
|
26
|
+
"@contrast/logger": "1.14.0",
|
|
27
|
+
"@contrast/patcher": "1.13.0",
|
|
28
|
+
"@contrast/rewriter": "1.17.1",
|
|
29
|
+
"@contrast/route-coverage": "1.31.0",
|
|
30
|
+
"@contrast/scopes": "1.11.0",
|
|
30
31
|
"semver": "^7.6.0"
|
|
31
32
|
}
|
|
32
33
|
}
|
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Copyright: 2024 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
|
-
const { patchType } = require('../common');
|
|
17
|
-
|
|
18
|
-
const {
|
|
19
|
-
DataflowTag: {
|
|
20
|
-
URL_ENCODED,
|
|
21
|
-
LIMITED_CHARS,
|
|
22
|
-
ALPHANUM_SPACE_HYPHEN,
|
|
23
|
-
SAFE_PATH,
|
|
24
|
-
UNTRUSTED,
|
|
25
|
-
},
|
|
26
|
-
FS_METHODS,
|
|
27
|
-
Rule: { PATH_TRAVERSAL: ruleId },
|
|
28
|
-
isString,
|
|
29
|
-
ArrayPrototypeJoin,
|
|
30
|
-
} = require('@contrast/common');
|
|
31
|
-
const { InstrumentationType: { RULE } } = require('../../../constants');
|
|
32
|
-
|
|
33
|
-
module.exports = function(core) {
|
|
34
|
-
const {
|
|
35
|
-
depHooks,
|
|
36
|
-
patcher,
|
|
37
|
-
assess: {
|
|
38
|
-
inspect, // TODO NODE-3455: remove
|
|
39
|
-
getSourceContext,
|
|
40
|
-
eventFactory: { createSinkEvent },
|
|
41
|
-
dataflow: {
|
|
42
|
-
tracker,
|
|
43
|
-
sinks: { isVulnerable, reportFindings },
|
|
44
|
-
},
|
|
45
|
-
},
|
|
46
|
-
} = core;
|
|
47
|
-
|
|
48
|
-
const safeTags = [
|
|
49
|
-
`excluded:${ruleId}`,
|
|
50
|
-
URL_ENCODED,
|
|
51
|
-
LIMITED_CHARS,
|
|
52
|
-
ALPHANUM_SPACE_HYPHEN,
|
|
53
|
-
SAFE_PATH,
|
|
54
|
-
];
|
|
55
|
-
|
|
56
|
-
function getValues(indices, args) {
|
|
57
|
-
return indices.reduce((acc, idx) => {
|
|
58
|
-
const value = args[idx];
|
|
59
|
-
if (value && isString(value)) acc.push(value);
|
|
60
|
-
return acc;
|
|
61
|
-
}, []);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const pre = (name, method, moduleName = 'fs', fullMethodName = '') => (data) => {
|
|
65
|
-
const { name: methodName, indices } = method;
|
|
66
|
-
if (!getSourceContext(RULE, ruleId)) return;
|
|
67
|
-
|
|
68
|
-
const values = getValues(indices, data.args);
|
|
69
|
-
if (!values.length) return;
|
|
70
|
-
|
|
71
|
-
const args = values.map((v) => {
|
|
72
|
-
const strInfo = tracker.getData(v);
|
|
73
|
-
return {
|
|
74
|
-
value: strInfo ? strInfo.value : v,
|
|
75
|
-
tracked: !!strInfo,
|
|
76
|
-
strInfo
|
|
77
|
-
};
|
|
78
|
-
});
|
|
79
|
-
for (let i = 0; i < values.length; i++) {
|
|
80
|
-
const { strInfo } = args[i];
|
|
81
|
-
|
|
82
|
-
if (!strInfo || !isVulnerable(UNTRUSTED, safeTags, strInfo.tags)) {
|
|
83
|
-
continue;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const event = createSinkEvent({
|
|
87
|
-
name,
|
|
88
|
-
moduleName,
|
|
89
|
-
methodName: fullMethodName || methodName,
|
|
90
|
-
context: `${name}(${ArrayPrototypeJoin.call(
|
|
91
|
-
args.map((a) => inspect(a.value)),
|
|
92
|
-
', '
|
|
93
|
-
)})`,
|
|
94
|
-
history: [strInfo],
|
|
95
|
-
object: {
|
|
96
|
-
value: 'fs',
|
|
97
|
-
tracked: false,
|
|
98
|
-
},
|
|
99
|
-
args: args.map(({ value, tracked }) => ({ value, tracked })),
|
|
100
|
-
tags: strInfo.tags,
|
|
101
|
-
source: `P${i}`,
|
|
102
|
-
stacktraceOpts: {
|
|
103
|
-
contructorOpt: data.hooked,
|
|
104
|
-
prependFrames: [data.orig],
|
|
105
|
-
},
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
if (event) {
|
|
109
|
-
reportFindings({
|
|
110
|
-
ruleId,
|
|
111
|
-
sinkEvent: event,
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
core.assess.dataflow.sinks.pathTraversal = {
|
|
118
|
-
install() {
|
|
119
|
-
depHooks.resolve({ name: 'fs' }, (fs) => {
|
|
120
|
-
for (const method of FS_METHODS) {
|
|
121
|
-
// not all methods are available on every OS or Node version.
|
|
122
|
-
if (fs[method.name]) {
|
|
123
|
-
const name = `fs.${method.name}`;
|
|
124
|
-
patcher.patch(fs, method.name, {
|
|
125
|
-
name,
|
|
126
|
-
patchType,
|
|
127
|
-
pre: pre(name, method),
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
if (method.sync) {
|
|
132
|
-
const syncName = `${method.name}Sync`;
|
|
133
|
-
if (fs[syncName]) {
|
|
134
|
-
const name = `fs.${syncName}`;
|
|
135
|
-
patcher.patch(fs, syncName, {
|
|
136
|
-
name,
|
|
137
|
-
patchType,
|
|
138
|
-
pre: pre(name, method, 'fs', syncName),
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
if (method.promises && fs.promises && fs.promises[method.name]) {
|
|
144
|
-
const name = `fs.promises.${method.name}`;
|
|
145
|
-
patcher.patch(fs.promises, method.name, {
|
|
146
|
-
name,
|
|
147
|
-
patchType,
|
|
148
|
-
pre: pre(name, method, 'fs.promises'),
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
depHooks.resolve({ name: 'fs/promises' }, (fsPromises) => {
|
|
155
|
-
for (const method of FS_METHODS) {
|
|
156
|
-
if (method.promises && fsPromises[method.name]) {
|
|
157
|
-
const name = `fsPromises.${method.name}`;
|
|
158
|
-
patcher.patch(fsPromises, method.name, {
|
|
159
|
-
name,
|
|
160
|
-
patchType,
|
|
161
|
-
pre: pre(name, method, 'fsPromises'),
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
});
|
|
166
|
-
},
|
|
167
|
-
};
|
|
168
|
-
|
|
169
|
-
return core.assess.dataflow.sinks.pathTraversal;
|
|
170
|
-
};
|