@contrast/assess 1.35.0 → 1.36.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/crypto-analysis/install/crypto.js +1 -1
- package/lib/dataflow/propagation/install/JSON/parse-fn.js +1 -1
- package/lib/dataflow/propagation/install/JSON/parse.js +3 -2
- package/lib/dataflow/propagation/install/JSON/parse.test.js +2 -2
- package/lib/dataflow/propagation/install/JSON/stringify.js +11 -10
- package/lib/dataflow/propagation/install/JSON/stringify.test.js +3 -3
- package/lib/dataflow/propagation/install/array-prototype-join.js +4 -3
- package/lib/dataflow/propagation/install/array-prototype-join.test.js +3 -3
- package/lib/dataflow/propagation/install/buffer.js +2 -3
- package/lib/dataflow/propagation/install/contrast-methods/tag.test.js +2 -2
- package/lib/dataflow/propagation/install/decode-uri-component.js +5 -8
- package/lib/dataflow/propagation/install/decode-uri-component.test.js +1 -1
- package/lib/dataflow/propagation/install/ejs/escape-xml.js +6 -9
- package/lib/dataflow/propagation/install/ejs/escape-xml.test.js +2 -2
- package/lib/dataflow/propagation/install/ejs/template.js +2 -2
- package/lib/dataflow/propagation/install/encode-uri.js +4 -6
- package/lib/dataflow/propagation/install/encode-uri.test.js +2 -2
- package/lib/dataflow/propagation/install/escape-html.js +5 -8
- package/lib/dataflow/propagation/install/escape-html.test.js +3 -3
- package/lib/dataflow/propagation/install/escape.js +5 -8
- package/lib/dataflow/propagation/install/escape.test.js +2 -2
- package/lib/dataflow/propagation/install/fastify-send.js +3 -5
- package/lib/dataflow/propagation/install/handlebars-utils-escape-expression.js +6 -9
- package/lib/dataflow/propagation/install/handlebars-utils-escape-expression.test.js +1 -1
- package/lib/dataflow/propagation/install/joi/boolean.js +50 -52
- package/lib/dataflow/propagation/install/joi/expression.js +3 -10
- package/lib/dataflow/propagation/install/joi/index.js +98 -101
- package/lib/dataflow/propagation/install/joi/keys.js +10 -5
- package/lib/dataflow/propagation/install/joi/number.js +50 -52
- package/lib/dataflow/propagation/install/joi/string-schema.js +9 -14
- package/lib/dataflow/propagation/install/joi/utils.js +7 -4
- package/lib/dataflow/propagation/install/joi/values.js +5 -7
- package/lib/dataflow/propagation/install/mongoose/schema-map.js +5 -4
- package/lib/dataflow/propagation/install/mongoose/schema-map.test.js +4 -4
- package/lib/dataflow/propagation/install/mongoose/schema-mixed.js +5 -4
- package/lib/dataflow/propagation/install/mongoose/schema-mixed.test.js +4 -5
- package/lib/dataflow/propagation/install/mongoose/schema-string.js +3 -4
- package/lib/dataflow/propagation/install/mustache-escape.js +5 -8
- package/lib/dataflow/propagation/install/mustache-escape.test.js +2 -2
- package/lib/dataflow/propagation/install/mysql-connection-escape.js +5 -8
- package/lib/dataflow/propagation/install/mysql-connection-escape.test.js +2 -2
- package/lib/dataflow/propagation/install/parse-int.js +3 -3
- package/lib/dataflow/propagation/install/path/basename.js +7 -12
- package/lib/dataflow/propagation/install/path/basename.test.js +2 -2
- package/lib/dataflow/propagation/install/path/common.js +2 -2
- package/lib/dataflow/propagation/install/path/dirname.js +5 -10
- package/lib/dataflow/propagation/install/path/dirname.test.js +2 -2
- package/lib/dataflow/propagation/install/path/extname.js +6 -11
- package/lib/dataflow/propagation/install/path/extname.test.js +2 -2
- package/lib/dataflow/propagation/install/path/format.js +7 -13
- package/lib/dataflow/propagation/install/path/format.test.js +2 -2
- package/lib/dataflow/propagation/install/path/join-and-resolve.js +7 -12
- package/lib/dataflow/propagation/install/path/join-and-resolve.test.js +2 -2
- package/lib/dataflow/propagation/install/path/normalize.js +4 -11
- package/lib/dataflow/propagation/install/path/normalize.test.js +2 -2
- package/lib/dataflow/propagation/install/path/parse.js +3 -8
- package/lib/dataflow/propagation/install/path/parse.test.js +2 -2
- package/lib/dataflow/propagation/install/path/relative.js +5 -11
- package/lib/dataflow/propagation/install/path/relative.test.js +2 -2
- package/lib/dataflow/propagation/install/path/toNamespacedPath.js +5 -11
- package/lib/dataflow/propagation/install/path/toNamespacedPath.test.js +2 -2
- package/lib/dataflow/propagation/install/pug/index.js +8 -3
- package/lib/dataflow/propagation/install/pug-runtime-escape.js +5 -8
- package/lib/dataflow/propagation/install/pug-runtime-escape.test.js +1 -1
- package/lib/dataflow/propagation/install/querystring/escape.js +3 -3
- package/lib/dataflow/propagation/install/querystring/parse.js +7 -11
- package/lib/dataflow/propagation/install/querystring/stringify.js +3 -3
- package/lib/dataflow/propagation/install/reg-exp-prototype-exec.js +4 -3
- package/lib/dataflow/propagation/install/reg-exp-prototype-exec.test.js +5 -3
- package/lib/dataflow/propagation/install/send.js +5 -10
- package/lib/dataflow/propagation/install/sequelize/query-generator.js +3 -4
- package/lib/dataflow/propagation/install/sequelize/sql-string.js +8 -12
- package/lib/dataflow/propagation/install/sequelize/sql-string.test.js +2 -13
- package/lib/dataflow/propagation/install/sql-template-strings.js +3 -5
- package/lib/dataflow/propagation/install/sql-template-strings.test.js +2 -2
- package/lib/dataflow/propagation/install/string/concat.js +2 -1
- package/lib/dataflow/propagation/install/string/concat.test.js +15 -2
- package/lib/dataflow/propagation/install/string/format-methods.js +4 -2
- package/lib/dataflow/propagation/install/string/format-methods.test.js +15 -2
- package/lib/dataflow/propagation/install/string/html-methods.js +1 -1
- package/lib/dataflow/propagation/install/string/html-methods.test.js +15 -2
- package/lib/dataflow/propagation/install/string/index.js +2 -2
- package/lib/dataflow/propagation/install/string/match-all.js +2 -1
- package/lib/dataflow/propagation/install/string/match-all.test.js +13 -0
- package/lib/dataflow/propagation/install/string/match.js +11 -10
- package/lib/dataflow/propagation/install/string/match.test.js +13 -0
- package/lib/dataflow/propagation/install/string/replace.js +15 -9
- package/lib/dataflow/propagation/install/string/replace.test.js +13 -0
- package/lib/dataflow/propagation/install/string/slice.js +2 -1
- package/lib/dataflow/propagation/install/string/slice.test.js +13 -0
- package/lib/dataflow/propagation/install/string/split.js +2 -1
- package/lib/dataflow/propagation/install/string/split.test.js +13 -0
- package/lib/dataflow/propagation/install/string/substring.js +2 -1
- package/lib/dataflow/propagation/install/string/substring.test.js +13 -0
- package/lib/dataflow/propagation/install/string/trim.js +4 -1
- package/lib/dataflow/propagation/install/string/trim.test.js +13 -0
- package/lib/dataflow/propagation/install/unescape.js +5 -8
- package/lib/dataflow/propagation/install/unescape.test.js +2 -2
- package/lib/dataflow/propagation/install/url/domain-parsers.js +4 -5
- package/lib/dataflow/propagation/install/url/domain-parsers.test.js +2 -2
- package/lib/dataflow/propagation/install/url/parse.js +3 -2
- package/lib/dataflow/propagation/install/url/parse.test.js +2 -2
- package/lib/dataflow/propagation/install/url/searchParams.js +5 -5
- package/lib/dataflow/propagation/install/url/searchParams.test.js +2 -2
- package/lib/dataflow/propagation/install/url/url.js +6 -3
- package/lib/dataflow/propagation/install/url/url.test.js +2 -2
- package/lib/dataflow/propagation/install/util-format.js +7 -6
- package/lib/dataflow/propagation/install/util-format.test.js +2 -2
- package/lib/dataflow/propagation/install/validator/hooks.js +7 -2
- package/lib/dataflow/sinks/install/child-process.js +1 -1
- package/lib/dataflow/sinks/install/child-process.test.js +1 -1
- package/lib/dataflow/sinks/install/fs.js +1 -1
- package/lib/dataflow/sinks/install/fs.test.js +1 -1
- package/lib/dataflow/sinks/install/function.js +1 -1
- package/lib/dataflow/sinks/install/http/request.js +2 -1
- package/lib/dataflow/sinks/install/http/request.test.js +1 -1
- package/lib/dataflow/sinks/install/http/server-response.test.js +3 -5
- package/lib/dataflow/sinks/install/restify.js +1 -1
- package/lib/dataflow/sinks/install/vm.js +4 -2
- package/lib/dataflow/sinks/install/vm.test.js +1 -1
- package/lib/dataflow/sources/handler.js +5 -2
- package/lib/dataflow/sources/install/body-parser1.test.js +4 -4
- package/lib/dataflow/sources/install/busboy.js +8 -3
- package/lib/dataflow/sources/install/busboy.test.js +2 -2
- package/lib/dataflow/sources/install/cookie-parser1.test.js +2 -2
- package/lib/dataflow/sources/install/express/params.js +14 -11
- package/lib/dataflow/sources/install/express/params.test.js +5 -7
- package/lib/dataflow/sources/install/express/parsedUrl.js +3 -2
- package/lib/dataflow/sources/install/fastify/fastify.js +7 -6
- package/lib/dataflow/sources/install/fastify/fastify.test.js +2 -2
- package/lib/dataflow/sources/install/formidable1.js +7 -6
- package/lib/dataflow/sources/install/formidable1.test.js +2 -2
- package/lib/dataflow/sources/install/hapi/hapi.js +8 -10
- package/lib/dataflow/sources/install/hapi/hapi.test.js +0 -1
- package/lib/dataflow/sources/install/http.js +20 -16
- package/lib/dataflow/sources/install/http.test.js +28 -34
- package/lib/dataflow/sources/install/koa/koa-bodyparsers.js +7 -7
- package/lib/dataflow/sources/install/koa/koa-bodyparsers.test.js +3 -4
- package/lib/dataflow/sources/install/koa/koa-multer.js +8 -4
- package/lib/dataflow/sources/install/koa/koa-routers.js +7 -6
- package/lib/dataflow/sources/install/koa/koa-routers.test.js +2 -2
- package/lib/dataflow/sources/install/koa/koa2.js +7 -3
- package/lib/dataflow/sources/install/koa/koa2.test.js +1 -1
- package/lib/dataflow/sources/install/multer1.js +6 -2
- package/lib/dataflow/sources/install/qs6.js +1 -1
- package/lib/dataflow/sources/install/querystring.js +1 -1
- package/lib/dataflow/sources/install/restify/fieldedTextBodyParser.js +1 -4
- package/lib/dataflow/sources/install/restify/fieldedTextBodyParser.test.js +6 -8
- package/lib/dataflow/sources/install/restify/jsonBodyParser.js +0 -1
- package/lib/dataflow/sources/install/restify/jsonBodyParser.test.js +4 -8
- package/lib/dataflow/sources/install/restify/router.test.js +2 -2
- package/lib/dataflow/tag-utils.js +1 -1
- package/lib/dataflow/tracker.js +1 -1
- package/lib/dataflow/utils/is-safe-content-type.js +3 -2
- package/lib/event-factory.js +4 -4
- package/lib/get-policy.js +2 -2
- package/lib/index.js +18 -7
- package/lib/index.test.js +4 -0
- package/lib/make-source-context.js +37 -28
- package/lib/make-source-context.test.js +7 -7
- package/lib/response-scanning/handlers/index.js +7 -5
- package/lib/response-scanning/handlers/utils.js +11 -8
- package/lib/response-scanning/install/http.js +1 -1
- package/lib/sampler.js +136 -0
- package/lib/sampler.test.js +296 -0
- package/lib/session-configuration/install/express-session.js +1 -1
- package/lib/session-configuration/install/fastify-cookie.js +1 -1
- package/package.json +10 -10
package/lib/sampler.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
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
|
+
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const { Event } = require('@contrast/common');
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {{
|
|
22
|
+
* assess: import('.').Assess,
|
|
23
|
+
* config: import('@contrast/config').Config,
|
|
24
|
+
* logger: import('@contrast/logger').Logger,
|
|
25
|
+
* messages: import('@contrast/common').Messages,
|
|
26
|
+
* }} core
|
|
27
|
+
* @returns {import('@contrast/common').Installable}
|
|
28
|
+
*/
|
|
29
|
+
module.exports = function assess(core) {
|
|
30
|
+
const {
|
|
31
|
+
assess,
|
|
32
|
+
config,
|
|
33
|
+
logger,
|
|
34
|
+
messages,
|
|
35
|
+
} = core;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Initializes the Assess sampler only if there's a need, otherwise sets it to null.
|
|
39
|
+
* Clients will call the instance methods optionally: assess.sampler?.getSampleInfo().
|
|
40
|
+
* Determines sampler strategy and options based on effective config values.
|
|
41
|
+
*/
|
|
42
|
+
function initSampler() {
|
|
43
|
+
const baseProbability = config.getEffectiveValue('assess.probabilistic_sampling.base_probability');
|
|
44
|
+
|
|
45
|
+
let strategy;
|
|
46
|
+
let opts;
|
|
47
|
+
|
|
48
|
+
if (!config.getEffectiveValue('assess.enable') || baseProbability === 0) {
|
|
49
|
+
// if Assess was disabled by TS use disabled sampler
|
|
50
|
+
strategy = 'disabled';
|
|
51
|
+
} else if (baseProbability < 1) {
|
|
52
|
+
if (
|
|
53
|
+
config.getEffectiveValue('assess.probabilistic_sampling.enable') ||
|
|
54
|
+
config.getEffectiveValue('server.environment') === 'PRODUCTION'
|
|
55
|
+
) {
|
|
56
|
+
// strategy and opts can be more dynamic in the future
|
|
57
|
+
strategy = 'probabilistic';
|
|
58
|
+
opts = {
|
|
59
|
+
base_probability: baseProbability
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (strategy) {
|
|
65
|
+
// only re-init if the strategy and options are different than current sampler
|
|
66
|
+
if (
|
|
67
|
+
assess.sampler?.strategy !== strategy ||
|
|
68
|
+
assess.sampler?.opts?.base_probability !== opts?.base_probability
|
|
69
|
+
) {
|
|
70
|
+
logger.info({ strategy, opts }, 'updating assess sampler');
|
|
71
|
+
assess.sampler = SamplerFactory[strategy](opts);
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
// disable sampling by setting sampler to null
|
|
75
|
+
assess.sampler = null;
|
|
76
|
+
logger.info('assess sampling disabled');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// initialize a first time
|
|
81
|
+
initSampler();
|
|
82
|
+
// and re-init again upon settings updates. we don't use the settings message argument, since all
|
|
83
|
+
// effective sampling configs will have have been updated by @contrast/config (it registers
|
|
84
|
+
// listeners first).
|
|
85
|
+
messages.on(Event.SERVER_SETTINGS_UPDATE, initSampler);
|
|
86
|
+
|
|
87
|
+
return core.assess.sampler;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
class BaseSampler {
|
|
91
|
+
constructor(strategy, opts) {
|
|
92
|
+
// save strategy and opts on instance so they can be checked before re-initializing
|
|
93
|
+
this.strategy = strategy;
|
|
94
|
+
this.opts = opts;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
class DisabledSampler extends BaseSampler {
|
|
99
|
+
constructor() {
|
|
100
|
+
super('disabled');
|
|
101
|
+
this._sampleInfo = Object.seal({ canSample: false });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
getSampleInfo() {
|
|
105
|
+
return this._sampleInfo;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
class ProbabilisticSampler extends BaseSampler {
|
|
110
|
+
constructor(opts) {
|
|
111
|
+
super('probabilistic', opts);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
getSampleInfo() {
|
|
115
|
+
const { base_probability } = this.opts;
|
|
116
|
+
const rand = Math.random();
|
|
117
|
+
const canSample = rand < base_probability;
|
|
118
|
+
return { canSample, base_probability, rand };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
class SamplerFactory {
|
|
123
|
+
/**
|
|
124
|
+
* Used when Assess is disabled by TS; always returns value instructing not to sample
|
|
125
|
+
*/
|
|
126
|
+
static disabled() {
|
|
127
|
+
return new DisabledSampler();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Each request has fixed chance e.g. 0.05.
|
|
132
|
+
*/
|
|
133
|
+
static probabilistic(opts = {}) {
|
|
134
|
+
return new ProbabilisticSampler(opts);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { EventEmitter } = require('events');
|
|
4
|
+
const { expect } = require('chai');
|
|
5
|
+
const { Event } = require('@contrast/common');
|
|
6
|
+
const mocks = require('@contrast/test/mocks');
|
|
7
|
+
|
|
8
|
+
const TRIALS = 1000;
|
|
9
|
+
const ZSCORE = 3.891; // 99.99% confidence
|
|
10
|
+
|
|
11
|
+
describe('assess sampler', function() {
|
|
12
|
+
it('sampling is disabled by default and assess.sampler is null', function() {
|
|
13
|
+
const core = initMockCore();
|
|
14
|
+
require('./sampler')(core);
|
|
15
|
+
expect(core.assess.sampler).to.be.null;
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('samples at default rate of 0.01', function() {
|
|
19
|
+
const core = initMockCore();
|
|
20
|
+
core.config.setValue('assess.enable', true, 'CONTRAST_UI');
|
|
21
|
+
core.config.setValue('assess.probabilistic_sampling.enable', true, 'CONTRAST_UI');
|
|
22
|
+
// initialize after configs are set
|
|
23
|
+
const sampler = require('./sampler')(core);
|
|
24
|
+
|
|
25
|
+
// verify configs set in before setup
|
|
26
|
+
expect(sampler).to.have.property('strategy', 'probabilistic');
|
|
27
|
+
expect(sampler.opts).to.have.property('base_probability', 0.01);
|
|
28
|
+
expect(sampler).to.have.property('getSampleInfo').to.be.a('function');
|
|
29
|
+
// test behavior
|
|
30
|
+
testProbabilisticSampler(sampler);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('samples at p=base_probability', function() {
|
|
34
|
+
const core = initMockCore();
|
|
35
|
+
core.config.setValue('assess.probabilistic_sampling.enable', true, 'CONTRAST_UI');
|
|
36
|
+
core.config.setValue('assess.probabilistic_sampling.base_probability', 0.25, 'CONTRAST_UI');
|
|
37
|
+
// initialize after configs are set
|
|
38
|
+
const sampler = require('./sampler')(core);
|
|
39
|
+
|
|
40
|
+
// verify configs
|
|
41
|
+
expect(sampler).to.have.property('strategy', 'probabilistic');
|
|
42
|
+
expect(sampler.opts).to.have.property('base_probability', 0.25);
|
|
43
|
+
expect(sampler).to.have.property('getSampleInfo').to.be.a('function');
|
|
44
|
+
// test behavior
|
|
45
|
+
testProbabilisticSampler(sampler);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('probability is dynamic and set by TS server setting: assess.sampling.request_frequency', function() {
|
|
49
|
+
const core = initMockCore();
|
|
50
|
+
core.config.setValue('assess.probabilistic_sampling.enable', true, 'CONTRAST_UI');
|
|
51
|
+
core.config.setValue('assess.probabilistic_sampling.base_probability', 0.25, 'CONTRAST_UI');
|
|
52
|
+
// initialize after configs are set
|
|
53
|
+
require('./sampler')(core);
|
|
54
|
+
// don't destructure to just { sampler } since the value of assess.sampler changes on update.
|
|
55
|
+
const { assess } = core;
|
|
56
|
+
|
|
57
|
+
// verify configs
|
|
58
|
+
expect(assess.sampler).to.have.property('strategy', 'probabilistic');
|
|
59
|
+
expect(assess.sampler.opts).to.have.property('base_probability', 0.25);
|
|
60
|
+
expect(assess.sampler).to.have.property('getSampleInfo').to.be.a('function');
|
|
61
|
+
// test behavior
|
|
62
|
+
testProbabilisticSampler(assess.sampler);
|
|
63
|
+
|
|
64
|
+
// update
|
|
65
|
+
core.messages.emit(Event.SERVER_SETTINGS_UPDATE, {
|
|
66
|
+
assess: {
|
|
67
|
+
sampling: {
|
|
68
|
+
enable: true,
|
|
69
|
+
request_frequency: 2, // 1 / 2 = 0.50 base_probability
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// verify sampler was updated
|
|
75
|
+
expect(assess.sampler.opts.base_probability).to.equal(0.50);
|
|
76
|
+
// test behavior
|
|
77
|
+
testProbabilisticSampler(assess.sampler);
|
|
78
|
+
|
|
79
|
+
// test logging throughout init/updates
|
|
80
|
+
expect(core.logger.info.getCalls().map(c => c.args)).to.deep.equal([
|
|
81
|
+
// when initialized
|
|
82
|
+
[
|
|
83
|
+
{ strategy: 'probabilistic', opts: { base_probability: 0.25 } },
|
|
84
|
+
'updating assess sampler'
|
|
85
|
+
],
|
|
86
|
+
// update 1
|
|
87
|
+
[
|
|
88
|
+
{ strategy: 'probabilistic', opts: { base_probability: 0.5 } },
|
|
89
|
+
'updating assess sampler'
|
|
90
|
+
]
|
|
91
|
+
]);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('sampler always returns `canSample: false` if TS `assess.enable` setting is false', function() {
|
|
95
|
+
const core = initMockCore();
|
|
96
|
+
core.config.setValue('assess.enable', true, 'CONTRAST_UI');
|
|
97
|
+
core.config.setValue('assess.probabilistic_sampling.enable', true, 'CONTRAST_UI');
|
|
98
|
+
core.config.setValue('assess.probabilistic_sampling.base_probability', 0.50, 'CONTRAST_UI');
|
|
99
|
+
// initializes after configs are set
|
|
100
|
+
require('./sampler')(core);
|
|
101
|
+
// don't destructure to just { sampler } since the value of assess.sampler changes on update.
|
|
102
|
+
const { assess } = core;
|
|
103
|
+
|
|
104
|
+
// verify configs set in before setup
|
|
105
|
+
expect(assess.sampler).to.have.property('strategy', 'probabilistic');
|
|
106
|
+
expect(assess.sampler.opts).to.have.property('base_probability', 0.50);
|
|
107
|
+
expect(assess.sampler).to.have.property('getSampleInfo').to.be.a('function');
|
|
108
|
+
// test behavior
|
|
109
|
+
testProbabilisticSampler(assess.sampler);
|
|
110
|
+
|
|
111
|
+
// runtime update that says to disable assess
|
|
112
|
+
core.messages.emit(Event.SERVER_SETTINGS_UPDATE, {
|
|
113
|
+
assess: { enable: false }
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// verify updated sampling strategy
|
|
117
|
+
expect(assess.sampler.strategy).to.equal('disabled');
|
|
118
|
+
// test behavior
|
|
119
|
+
testDisabledSampler(assess.sampler);
|
|
120
|
+
|
|
121
|
+
// test logging throughout init/updates
|
|
122
|
+
expect(core.logger.info.getCalls().map(c => c.args)).to.deep.equal([
|
|
123
|
+
// when initialized
|
|
124
|
+
[
|
|
125
|
+
{ strategy: 'probabilistic', opts: { base_probability: 0.5 } },
|
|
126
|
+
'updating assess sampler'
|
|
127
|
+
],
|
|
128
|
+
// update 1
|
|
129
|
+
[
|
|
130
|
+
{ strategy: 'disabled', opts: undefined },
|
|
131
|
+
'updating assess sampler'
|
|
132
|
+
]
|
|
133
|
+
]);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('sampler behavior adjusts to series of TS updates', function() {
|
|
137
|
+
// setup
|
|
138
|
+
const core = initMockCore();
|
|
139
|
+
core.config.setValue('assess.enable', true, 'CONTRAST_UI');
|
|
140
|
+
core.config.setValue('assess.probabilistic_sampling.enable', false, 'DEFAULT_VALUE');
|
|
141
|
+
core.config.setValue('assess.probabilistic_sampling.base_probability', 0.50, 'CONTRAST_UI');
|
|
142
|
+
// initializes after configs are set
|
|
143
|
+
require('./sampler')(core);
|
|
144
|
+
// don't destructure to just { sampler } since the value of assess.sampler changes on update.
|
|
145
|
+
const { assess } = core;
|
|
146
|
+
|
|
147
|
+
// disabled by config
|
|
148
|
+
expect(assess.sampler).to.be.null;
|
|
149
|
+
|
|
150
|
+
// set to PRODUCTION to enable sampling
|
|
151
|
+
core.messages.emit(Event.SERVER_SETTINGS_UPDATE, {
|
|
152
|
+
environment: 'PRODUCTION',
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// verify sampler was updated
|
|
156
|
+
expect(assess.sampler.opts.base_probability).to.equal(0.50);
|
|
157
|
+
// test behavior
|
|
158
|
+
testProbabilisticSampler(assess.sampler);
|
|
159
|
+
|
|
160
|
+
// disable sampling by setting to DEVELOPMENT
|
|
161
|
+
core.messages.emit(Event.SERVER_SETTINGS_UPDATE, {
|
|
162
|
+
environment: 'DEVELOPMENT',
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// since assess.probabilistic_sampling is off and env is no longer PRODUCTION
|
|
166
|
+
expect(assess.sampler).to.be.null;
|
|
167
|
+
|
|
168
|
+
// disable Assess
|
|
169
|
+
core.messages.emit(Event.SERVER_SETTINGS_UPDATE, {
|
|
170
|
+
assess: { enable: false },
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// verify updated sampling strategy
|
|
174
|
+
expect(assess.sampler.strategy).to.equal('disabled');
|
|
175
|
+
// test behavior
|
|
176
|
+
testDisabledSampler(assess.sampler);
|
|
177
|
+
|
|
178
|
+
// re-enable Assess and set new probability via request_frequency
|
|
179
|
+
core.messages.emit(Event.SERVER_SETTINGS_UPDATE, {
|
|
180
|
+
assess: {
|
|
181
|
+
enable: true,
|
|
182
|
+
sampling: {
|
|
183
|
+
enable: true,
|
|
184
|
+
request_frequency: 50, //
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// verify sampler was updated
|
|
190
|
+
expect(assess.sampler.opts.base_probability).to.equal(1 / 50);
|
|
191
|
+
// test behavior
|
|
192
|
+
testProbabilisticSampler(assess.sampler);
|
|
193
|
+
|
|
194
|
+
// test logging throughout init/updates
|
|
195
|
+
expect(core.logger.info.getCalls().map(c => c.args)).to.deep.equal([
|
|
196
|
+
// when initialized
|
|
197
|
+
['assess sampling disabled'],
|
|
198
|
+
// update 1: env=PRODUCTION enables sampling
|
|
199
|
+
[
|
|
200
|
+
{ strategy: 'probabilistic', opts: { base_probability: 0.5 } },
|
|
201
|
+
'updating assess sampler'
|
|
202
|
+
],
|
|
203
|
+
// update 2: env = DEVELOPMENT disables sampling
|
|
204
|
+
['assess sampling disabled'],
|
|
205
|
+
// update 3: assess.enable=false sets disabled sampling strategy
|
|
206
|
+
[
|
|
207
|
+
{ strategy: 'disabled', opts: undefined },
|
|
208
|
+
'updating assess sampler'
|
|
209
|
+
],
|
|
210
|
+
// update 5: assess.enable=true and request_frequency=50 re-enables assess and new sampling probability
|
|
211
|
+
[
|
|
212
|
+
{ strategy: 'probabilistic', opts: { base_probability: 0.02 } },
|
|
213
|
+
'updating assess sampler'
|
|
214
|
+
]
|
|
215
|
+
]);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
function initMockCore() {
|
|
220
|
+
const { CONTRAST_CONFIG_PATH } = process.env;
|
|
221
|
+
let core;
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
// ensure default config if devs use this
|
|
225
|
+
process.env.CONTRAST_CONFIG_PATH = '';
|
|
226
|
+
core = {
|
|
227
|
+
// sampler needs this namespace to exist
|
|
228
|
+
assess: {},
|
|
229
|
+
// mocks
|
|
230
|
+
messages: new EventEmitter(),
|
|
231
|
+
logger: mocks.logger(),
|
|
232
|
+
};
|
|
233
|
+
// use actual config so we can get dynamic effective values with TS message
|
|
234
|
+
// updates (mock doesn't). we can also test new effective config mappings.
|
|
235
|
+
require('@contrast/config')(core);
|
|
236
|
+
core.config.setValue('assess.enable', true, 'CONTRAST_UI');
|
|
237
|
+
} catch (err) {
|
|
238
|
+
console.dir(err);
|
|
239
|
+
throw err;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// reset to orig value
|
|
243
|
+
if (CONTRAST_CONFIG_PATH) process.env.CONTRAST_CONFIG_PATH = CONTRAST_CONFIG_PATH;
|
|
244
|
+
|
|
245
|
+
return core;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// https://en.wikipedia.org/wiki/Binomial_distribution
|
|
249
|
+
function getStats(p) {
|
|
250
|
+
const mean = TRIALS * p;
|
|
251
|
+
const variance = mean * (1 - p);
|
|
252
|
+
const standardDev = Math.sqrt(variance);
|
|
253
|
+
const marginOfError = ZSCORE * standardDev;
|
|
254
|
+
const confidenceInterval = [
|
|
255
|
+
Math.max(0, mean - marginOfError),
|
|
256
|
+
mean + marginOfError
|
|
257
|
+
];
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
p,
|
|
261
|
+
mean,
|
|
262
|
+
variance,
|
|
263
|
+
standardDev,
|
|
264
|
+
confidenceInterval,
|
|
265
|
+
trials: TRIALS,
|
|
266
|
+
zscore: ZSCORE,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function testProbabilisticSampler(sampler) {
|
|
271
|
+
const { opts } = sampler;
|
|
272
|
+
|
|
273
|
+
// run trials and count how many times sampler says okay to analyze
|
|
274
|
+
let observedSampleCount = 0;
|
|
275
|
+
for (let n = 0; n < TRIALS; n++) {
|
|
276
|
+
const info = sampler.getSampleInfo();
|
|
277
|
+
if (info.canSample) observedSampleCount++;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const stats = getStats(opts.base_probability, observedSampleCount);
|
|
281
|
+
const { confidenceInterval: [low, high] } = stats;
|
|
282
|
+
|
|
283
|
+
// for debugging/visibility
|
|
284
|
+
// console.log({ observedSampleCount, ...stats })
|
|
285
|
+
expect(observedSampleCount).to.be.greaterThan(low).and.lessThan(high);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function testDisabledSampler(sampler) {
|
|
289
|
+
// run trials and count how many times sampler says okay to analyze
|
|
290
|
+
let observedSampleCount = 0;
|
|
291
|
+
for (let n = 0; n < TRIALS; n++) {
|
|
292
|
+
const info = sampler.getSampleInfo();
|
|
293
|
+
if (info.canSample) observedSampleCount++;
|
|
294
|
+
}
|
|
295
|
+
expect(observedSampleCount).to.equal(0);
|
|
296
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contrast/assess",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.36.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)",
|
|
@@ -17,16 +17,16 @@
|
|
|
17
17
|
"test": "../scripts/test.sh"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@contrast/common": "1.
|
|
21
|
-
"@contrast/config": "1.
|
|
22
|
-
"@contrast/core": "1.
|
|
23
|
-
"@contrast/dep-hooks": "1.
|
|
20
|
+
"@contrast/common": "1.26.0",
|
|
21
|
+
"@contrast/config": "1.34.0",
|
|
22
|
+
"@contrast/core": "1.38.0",
|
|
23
|
+
"@contrast/dep-hooks": "1.6.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/scopes": "1.
|
|
25
|
+
"@contrast/instrumentation": "1.16.0",
|
|
26
|
+
"@contrast/logger": "1.11.0",
|
|
27
|
+
"@contrast/patcher": "1.10.0",
|
|
28
|
+
"@contrast/rewriter": "1.14.0",
|
|
29
|
+
"@contrast/scopes": "1.7.0",
|
|
30
30
|
"semver": "^7.6.0"
|
|
31
31
|
}
|
|
32
32
|
}
|