@contrast/config 1.33.0 → 1.35.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/common.js +35 -12
- package/lib/common.test.js +183 -0
- package/lib/config.js +8 -8
- package/lib/index.d.ts +15 -26
- package/lib/index.test.js +71 -7
- package/lib/options.js +58 -26
- package/package.json +2 -2
package/lib/common.js
CHANGED
|
@@ -39,8 +39,18 @@ const {
|
|
|
39
39
|
UNTRUSTED_DESERIALIZATION,
|
|
40
40
|
XXE,
|
|
41
41
|
},
|
|
42
|
+
primordials: { StringPrototypeToLowerCase },
|
|
43
|
+
get,
|
|
44
|
+
isString,
|
|
42
45
|
} = require('@contrast/common');
|
|
43
46
|
|
|
47
|
+
function coerceLowerCase(path) {
|
|
48
|
+
return function(remoteData) {
|
|
49
|
+
const value = get(remoteData, path);
|
|
50
|
+
if (value && isString(value)) return StringPrototypeToLowerCase.call(value);
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
44
54
|
function protectModeReader(ruleId) {
|
|
45
55
|
return function (msg) {
|
|
46
56
|
const remoteSetting = msg?.protect?.rules?.[ruleId];
|
|
@@ -62,20 +72,12 @@ const ConfigSource = {
|
|
|
62
72
|
USER_CONFIGURATION_FILE: 'USER_CONFIGURATION_FILE',
|
|
63
73
|
};
|
|
64
74
|
|
|
75
|
+
// these should return `undefined` if there is no remote value corresponding to the effective config name.
|
|
65
76
|
const mappings = {
|
|
66
|
-
//
|
|
67
|
-
'
|
|
68
|
-
|
|
69
|
-
'application.session_id': (remoteData) => remoteData?.settings?.assessment?.session_id,
|
|
70
|
-
'agent.security_logger.syslog.enable': (remoteData) => remoteData.security_logger?.syslog?.enable,
|
|
71
|
-
'agent.security_logger.syslog.ip': (remoteData) => remoteData.security_logger?.syslog?.ip,
|
|
72
|
-
'agent.security_logger.syslog.port': (remoteData) => remoteData.security_logger?.syslog?.port,
|
|
73
|
-
'agent.security_logger.syslog.facility': (remoteData) => remoteData.security_logger?.syslog?.facility,
|
|
74
|
-
'agent.security_logger.syslog.severity_exploited': (remoteData) => remoteData.security_logger?.syslog?.severity_exploited?.toLowerCase(),
|
|
75
|
-
'agent.security_logger.syslog.severity_blocked': (remoteData) => remoteData.security_logger?.syslog?.severity_blocked?.toLowerCase(),
|
|
76
|
-
'agent.security_logger.syslog.severity_probed': (remoteData) => remoteData.security_logger?.syslog?.severity_probed?.toLowerCase(),
|
|
77
|
+
// agent startup (v1) or application startup (ng fallback)
|
|
78
|
+
'application.session_id': (remoteData) =>
|
|
79
|
+
remoteData.identification?.session_id ?? remoteData.settings?.assessment?.session_id,
|
|
77
80
|
// application settings
|
|
78
|
-
'assess.enable': (remoteData) => remoteData.assess?.enable,
|
|
79
81
|
'protect.enable': (remoteData) => remoteData.protect?.enable,
|
|
80
82
|
'protect.rules.cmd-injection.mode': protectModeReader(CMD_INJECTION),
|
|
81
83
|
'protect.rules.cmd-injection-command-backdoors.mode': protectModeReader(CMD_INJECTION_COMMAND_BACKDOORS),
|
|
@@ -92,6 +94,27 @@ const mappings = {
|
|
|
92
94
|
'protect.rules.unsafe-file-upload.mode': protectModeReader(UNSAFE_FILE_UPLOAD),
|
|
93
95
|
'protect.rules.untrusted-deserialization.mode': protectModeReader(UNTRUSTED_DESERIALIZATION),
|
|
94
96
|
'protect.rules.xxe.mode': protectModeReader(XXE),
|
|
97
|
+
// server features
|
|
98
|
+
'assess.enable': (remoteData) => remoteData.assess?.enable,
|
|
99
|
+
'assess.probabilistic_sampling.enable': (remoteData) => remoteData.assess?.sampling?.enable,
|
|
100
|
+
'assess.probabilistic_sampling.base_probability': (remoteData) => {
|
|
101
|
+
const request_frequency = remoteData.assess?.sampling?.request_frequency;
|
|
102
|
+
if (request_frequency > 0) {
|
|
103
|
+
const baseProbability = 1 / request_frequency;
|
|
104
|
+
if (!isNaN(baseProbability)) return baseProbability;
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
'agent.logger.level': coerceLowerCase('logger.level'),
|
|
108
|
+
'agent.logger.path': (remoteData) => remoteData.logger?.path,
|
|
109
|
+
'agent.security_logger.syslog.enable': (remoteData) => remoteData.security_logger?.syslog?.enable,
|
|
110
|
+
'agent.security_logger.syslog.ip': (remoteData) => remoteData.security_logger?.syslog?.ip,
|
|
111
|
+
'agent.security_logger.syslog.port': (remoteData) => remoteData.security_logger?.syslog?.port,
|
|
112
|
+
'agent.security_logger.syslog.facility': (remoteData) => remoteData.security_logger?.syslog?.facility,
|
|
113
|
+
'agent.security_logger.syslog.severity_exploited': coerceLowerCase('security_logger.syslog.severity_exploited'),
|
|
114
|
+
'agent.security_logger.syslog.severity_blocked': coerceLowerCase('security_logger.syslog.severity_blocked'),
|
|
115
|
+
'agent.security_logger.syslog.severity_probed': coerceLowerCase('security_logger.syslog.severity_probed'),
|
|
116
|
+
'server.environment': (remoteData) => remoteData.environment,
|
|
117
|
+
|
|
95
118
|
};
|
|
96
119
|
|
|
97
120
|
/*
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { expect } = require('chai');
|
|
4
|
+
const { ProtectRuleMode } = require('@contrast/common');
|
|
5
|
+
const { mappings } = require('./common');
|
|
6
|
+
|
|
7
|
+
describe('config effective value mappings', function () {
|
|
8
|
+
describe('"simple" mappings', function() {
|
|
9
|
+
const simplyMappedNames = [
|
|
10
|
+
'assess.enable',
|
|
11
|
+
'agent.logger.path',
|
|
12
|
+
'agent.security_logger.syslog.enable',
|
|
13
|
+
'agent.security_logger.syslog.ip',
|
|
14
|
+
'agent.security_logger.syslog.port',
|
|
15
|
+
'agent.security_logger.syslog.facility',
|
|
16
|
+
'server.environment',
|
|
17
|
+
'protect.enable',
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
it('"simple" mappings will return any value for corresponding remote setting (todo: validation for each)', function () {
|
|
21
|
+
['', 'foo', null, 0.3, 3, {}, [], true, false].forEach((val) => {
|
|
22
|
+
const remoteData = {
|
|
23
|
+
// server
|
|
24
|
+
assess: {
|
|
25
|
+
enable: val,
|
|
26
|
+
sampling: { enable: val }
|
|
27
|
+
},
|
|
28
|
+
logger: {
|
|
29
|
+
path: val,
|
|
30
|
+
},
|
|
31
|
+
security_logger: {
|
|
32
|
+
syslog: {
|
|
33
|
+
enable: val,
|
|
34
|
+
ip: val,
|
|
35
|
+
port: val,
|
|
36
|
+
facility: val,
|
|
37
|
+
serverity_exploited: val,
|
|
38
|
+
serverity_blocked: val,
|
|
39
|
+
exploited_probed: val,
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
environment: val,
|
|
43
|
+
// app
|
|
44
|
+
protect: {
|
|
45
|
+
enable: val,
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
for (const name of simplyMappedNames) {
|
|
50
|
+
const result = mappings[name](remoteData);
|
|
51
|
+
expect(result).to.equal(val);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('"simple" mappings will return `undefined` if there is no corresponding remote setting', function () {
|
|
57
|
+
for (const name of simplyMappedNames) {
|
|
58
|
+
expect(mappings[name]({})).to.equal(undefined);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('maps \'assess.probabilistic_sampling.base_probability\'', function() {
|
|
64
|
+
[
|
|
65
|
+
{
|
|
66
|
+
desc: 'sets value by inverting request_frequency',
|
|
67
|
+
remoteData: {
|
|
68
|
+
assess: { sampling: { request_frequency: 20 } }
|
|
69
|
+
},
|
|
70
|
+
expected: 1 / 20,
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
desc: 'ignores and returns undefined if request_frequency is 0',
|
|
74
|
+
remoteData: {
|
|
75
|
+
assess: { sampling: { request_frequency: 0 } }
|
|
76
|
+
},
|
|
77
|
+
expected: undefined,
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
desc: 'ignores and returns undefined if calculating probability gives NaN',
|
|
81
|
+
remoteData: {
|
|
82
|
+
assess: { sampling: { request_frequency: 'forty five' } }
|
|
83
|
+
},
|
|
84
|
+
expected: undefined,
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
desc: 'ignores and returns undefined if value is negative',
|
|
88
|
+
remoteData: {
|
|
89
|
+
assess: { sampling: { request_frequency: -12 } }
|
|
90
|
+
},
|
|
91
|
+
expected: undefined,
|
|
92
|
+
},
|
|
93
|
+
].forEach(({ desc, remoteData, expected }) => {
|
|
94
|
+
it(desc, function() {
|
|
95
|
+
expect(mappings['assess.probabilistic_sampling.base_probability'](remoteData)).to.equal(expected);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe('maps Protect rule modes', function() {
|
|
101
|
+
[
|
|
102
|
+
{
|
|
103
|
+
tsValue: 'OFF',
|
|
104
|
+
expected: ProtectRuleMode.OFF,
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
tsValue: 'MONITORING',
|
|
108
|
+
expected: ProtectRuleMode.MONITOR,
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
tsValue: 'BLOCKING',
|
|
112
|
+
expected: ProtectRuleMode.BLOCK,
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
tsValue: 'BLOCK_AT_PERIMETER',
|
|
116
|
+
expected: ProtectRuleMode.BLOCK_AT_PERIMETER,
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
tsValue: 'FOO',
|
|
120
|
+
expected: undefined
|
|
121
|
+
},
|
|
122
|
+
].forEach(({ desc, tsValue, expected }) => {
|
|
123
|
+
[
|
|
124
|
+
'protect.rules.cmd-injection.mode',
|
|
125
|
+
'protect.rules.cmd-injection-command-backdoors.mode',
|
|
126
|
+
'protect.rules.cmd-injection-semantic-chained-commands.mode',
|
|
127
|
+
'protect.rules.cmd-injection-semantic-dangerous-paths.mode',
|
|
128
|
+
'protect.rules.method-tampering.mode',
|
|
129
|
+
'protect.rules.nosql-injection.mode',
|
|
130
|
+
'protect.rules.nosql-injection-mongo.mode',
|
|
131
|
+
'protect.rules.path-traversal.mode',
|
|
132
|
+
'protect.rules.path-traversal-semantic-file-security-bypass.mode',
|
|
133
|
+
'protect.rules.reflected-xss.mode',
|
|
134
|
+
'protect.rules.sql-injection.mode',
|
|
135
|
+
'protect.rules.ssjs-injection.mode',
|
|
136
|
+
'protect.rules.unsafe-file-upload.mode',
|
|
137
|
+
'protect.rules.untrusted-deserialization.mode',
|
|
138
|
+
'protect.rules.xxe.mode',
|
|
139
|
+
].forEach((fullName) => {
|
|
140
|
+
const ruleName = /rules\.(.*)\.mode/.exec(fullName)[1];
|
|
141
|
+
it(`sets ${ruleName} to ${expected} when value is ${tsValue}`, function() {
|
|
142
|
+
expect(mappings[fullName]({
|
|
143
|
+
protect: {
|
|
144
|
+
rules: {
|
|
145
|
+
[ruleName]: { mode: tsValue },
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
})).to.equal(expected);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe('maps security logger severity levels (lowercase)', function() {
|
|
155
|
+
[
|
|
156
|
+
{
|
|
157
|
+
tsValue: 'FOO',
|
|
158
|
+
expected: 'foo',
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
tsValue: '',
|
|
162
|
+
expected: undefined,
|
|
163
|
+
}
|
|
164
|
+
].forEach(({ tsValue, expected }) => {
|
|
165
|
+
[
|
|
166
|
+
'agent.security_logger.syslog.severity_exploited',
|
|
167
|
+
'agent.security_logger.syslog.severity_blocked',
|
|
168
|
+
'agent.security_logger.syslog.severity_probed',
|
|
169
|
+
].forEach((fullName) => {
|
|
170
|
+
const severity = fullName.substr(fullName.lastIndexOf('.') + 1);
|
|
171
|
+
it(`maps to ${expected} when tsValue is ${tsValue}`, function() {
|
|
172
|
+
expect(mappings[fullName]({
|
|
173
|
+
security_logger: {
|
|
174
|
+
syslog: {
|
|
175
|
+
[severity]: tsValue,
|
|
176
|
+
},
|
|
177
|
+
}
|
|
178
|
+
})).to.equal(expected);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
});
|
package/lib/config.js
CHANGED
|
@@ -20,7 +20,7 @@ const path = require('path');
|
|
|
20
20
|
const fs = require('fs');
|
|
21
21
|
const os = require('os');
|
|
22
22
|
const yaml = require('yaml');
|
|
23
|
-
const { Event, get, set } = require('@contrast/common');
|
|
23
|
+
const { Event, get, set, primordials: { ArrayPrototypeJoin, StringPrototypeToUpperCase, JSONParse } } = require('@contrast/common');
|
|
24
24
|
const options = require('./options');
|
|
25
25
|
const {
|
|
26
26
|
ConfigSource: {
|
|
@@ -74,7 +74,7 @@ module.exports = class Config {
|
|
|
74
74
|
|
|
75
75
|
// report all the errors found during initialization.
|
|
76
76
|
if (this._errors.length) {
|
|
77
|
-
const errors = this._errors.map((e, ix) => `${ix + 1}) ${e.message}`)
|
|
77
|
+
const errors = ArrayPrototypeJoin.call(this._errors.map((e, ix) => `${ix + 1}) ${e.message}`), '; ');
|
|
78
78
|
throw new Error(`Errors found in configuration ${errors}`);
|
|
79
79
|
}
|
|
80
80
|
|
|
@@ -104,7 +104,7 @@ module.exports = class Config {
|
|
|
104
104
|
let parsedEnv;
|
|
105
105
|
|
|
106
106
|
try {
|
|
107
|
-
parsedEnv =
|
|
107
|
+
parsedEnv = JSONParse(env.pm2_env);
|
|
108
108
|
} catch (_err) {
|
|
109
109
|
const err = new Error(`Unable to parse pm2 environment variable: '${env.pm2_env}'`);
|
|
110
110
|
err.cause = _err;
|
|
@@ -122,7 +122,7 @@ module.exports = class Config {
|
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
return Object.entries(env).reduce((acc, [key, val]) => {
|
|
125
|
-
const name =
|
|
125
|
+
const name = StringPrototypeToUpperCase.call(key);
|
|
126
126
|
if (name.startsWith('CONTRAST_')) {
|
|
127
127
|
acc[name] = val;
|
|
128
128
|
}
|
|
@@ -161,11 +161,12 @@ module.exports = class Config {
|
|
|
161
161
|
|
|
162
162
|
const { _filepath } = this;
|
|
163
163
|
|
|
164
|
-
|
|
164
|
+
// deliberately ignore /dev/null (linux) and \\.\\nul (windows)
|
|
165
|
+
if (_filepath && _filepath !== os.devNull) {
|
|
165
166
|
let fileContents;
|
|
166
167
|
|
|
167
168
|
try {
|
|
168
|
-
fileContents = fs.readFileSync(_filepath
|
|
169
|
+
fileContents = fs.readFileSync(_filepath, 'utf-8');
|
|
169
170
|
} catch (e) {
|
|
170
171
|
const err = new Error(`Unable to read Contrast configuration file: '${_filepath}'`);
|
|
171
172
|
err.cause = e;
|
|
@@ -242,9 +243,8 @@ module.exports = class Config {
|
|
|
242
243
|
Array.from(this._effectiveMap.values()).forEach((v) => {
|
|
243
244
|
let { value } = v;
|
|
244
245
|
if (redact) value = this._redact(v.name, v.value);
|
|
245
|
-
if (value === undefined) value = null;
|
|
246
246
|
|
|
247
|
-
const redacted = { ...v, value: String(value) };
|
|
247
|
+
const redacted = { ...v, value: value !== null ? String(value) : null };
|
|
248
248
|
effective_config.push(redacted);
|
|
249
249
|
if (v.source === ENVIRONMENT_VARIABLE) environment_variable.push(redacted);
|
|
250
250
|
if (v.source === CONTRAST_UI) contrast_ui.push(redacted);
|
package/lib/index.d.ts
CHANGED
|
@@ -262,35 +262,24 @@ export interface Config {
|
|
|
262
262
|
};
|
|
263
263
|
|
|
264
264
|
application: {
|
|
265
|
-
/**
|
|
265
|
+
/** Override the reported application name. */
|
|
266
266
|
name?: string;
|
|
267
|
-
/**
|
|
267
|
+
/** Override the reported application path. Default: `'/'` */
|
|
268
268
|
path: string;
|
|
269
|
-
/**
|
|
269
|
+
/** Add the name of the application group with which this application should be associated in the Contrast UI. */
|
|
270
|
+
group?: string;
|
|
271
|
+
/** Add the application code this application should use in the Contrast UI. */
|
|
272
|
+
code?: string;
|
|
273
|
+
/** Override the reported application version. */
|
|
270
274
|
version?: string;
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
session_id
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
* How to report the application's group for auto-grouping
|
|
280
|
-
*/
|
|
281
|
-
group: string | null;
|
|
282
|
-
|
|
283
|
-
/**
|
|
284
|
-
* Comma-separated list of key=value pairs that are applied to each application reported by the agent.
|
|
285
|
-
*/
|
|
286
|
-
metadata: string | null;
|
|
287
|
-
|
|
288
|
-
/**
|
|
289
|
-
* Provide metadata used to create a new session within Contrast UI.
|
|
290
|
-
* Default: `null`
|
|
291
|
-
*/
|
|
292
|
-
session_metadata: string | null;
|
|
293
|
-
|
|
275
|
+
/** Apply labels to an application. Labels must be formatted as a comma-delimited list. Example - `label1,label2,label3` */
|
|
276
|
+
tags?: string;
|
|
277
|
+
/** Comma-separated list of key=value pairs that are applied to each application reported by the agent. */
|
|
278
|
+
metadata?: string;
|
|
279
|
+
/** Provide the ID of a session existing within Contrast UI. Exclusive with `session_metadata` */
|
|
280
|
+
session_id?: string;
|
|
281
|
+
/** Provide metadata used to create a new session within Contrast UI. Exclusive with `session_id` */
|
|
282
|
+
session_metadata?: string;
|
|
294
283
|
};
|
|
295
284
|
|
|
296
285
|
/** Reported server information overrides */
|
package/lib/index.test.js
CHANGED
|
@@ -371,11 +371,12 @@ describe('config', function () {
|
|
|
371
371
|
// there needs to be a valid config file or env var set so config doesn't throw
|
|
372
372
|
// an error.
|
|
373
373
|
process.env.CONTRAST_CONFIG_PATH = path.resolve(__dirname, '../../test/fixtures/protect_contrast_security.yaml');
|
|
374
|
+
//process.env.CONTRAST__PROTECT__RULES__DISABLED_RULES = 'cmd-injection,sql-injection';
|
|
374
375
|
config = require('.')(core);
|
|
375
376
|
|
|
376
377
|
config.setValue('contrast_config_path', path.resolve(__dirname, '../../test/fixtures/protect_contrast_security.yaml'), ENVIRONMENT_VARIABLE);
|
|
377
378
|
config.setValue('contrast__agent__stack_trace_limit', 20, ENVIRONMENT_VARIABLE);
|
|
378
|
-
config.setValue('
|
|
379
|
+
config.setValue('protect.rules.disabled_rules', ['cmd-injection', 'sql-injection'], ENVIRONMENT_VARIABLE);
|
|
379
380
|
config.setValue('api.enable', true, ENVIRONMENT_VARIABLE);
|
|
380
381
|
config.setValue('api.api_key', 'ABCDEFGHIJ', ENVIRONMENT_VARIABLE);
|
|
381
382
|
config.setValue('api.service_key', 'KLMNOPQRST', ENVIRONMENT_VARIABLE);
|
|
@@ -407,11 +408,12 @@ describe('config', function () {
|
|
|
407
408
|
const {
|
|
408
409
|
config: {
|
|
409
410
|
effective_config,
|
|
411
|
+
environment_variable,
|
|
410
412
|
}
|
|
411
413
|
} = result;
|
|
412
414
|
|
|
413
415
|
expect(result.config).to.have.property('status', 'Success');
|
|
414
|
-
|
|
416
|
+
const expectedResults = [
|
|
415
417
|
{
|
|
416
418
|
canonical_name: 'api.enable',
|
|
417
419
|
name: 'api.enable',
|
|
@@ -422,13 +424,13 @@ describe('config', function () {
|
|
|
422
424
|
canonical_name: 'api.api_key',
|
|
423
425
|
name: 'api.api_key',
|
|
424
426
|
source: 'ENVIRONMENT_VARIABLE',
|
|
425
|
-
value: '
|
|
427
|
+
value: 'ABCDEFGHIJ',
|
|
426
428
|
},
|
|
427
429
|
{
|
|
428
430
|
canonical_name: 'api.service_key',
|
|
429
431
|
name: 'api.service_key',
|
|
430
432
|
source: 'ENVIRONMENT_VARIABLE',
|
|
431
|
-
value: '
|
|
433
|
+
value: 'KLMNOPQRST',
|
|
432
434
|
},
|
|
433
435
|
{
|
|
434
436
|
canonical_name: 'api.proxy.enable',
|
|
@@ -496,7 +498,37 @@ describe('config', function () {
|
|
|
496
498
|
value: '150',
|
|
497
499
|
source: 'DEFAULT_VALUE'
|
|
498
500
|
}
|
|
499
|
-
|
|
501
|
+
];
|
|
502
|
+
|
|
503
|
+
const failures = [];
|
|
504
|
+
let effective;
|
|
505
|
+
let envs;
|
|
506
|
+
const envFailures = [];
|
|
507
|
+
|
|
508
|
+
for (const expected of expectedResults) {
|
|
509
|
+
effective = effective_config.filter(i => i.canonical_name === expected.canonical_name);
|
|
510
|
+
expect(effective).an('array').lengthOf(1); // eslint-disable-line
|
|
511
|
+
effective = effective[0];
|
|
512
|
+
try {
|
|
513
|
+
expect(expected).to.deep.equal(effective);
|
|
514
|
+
} catch (e) {
|
|
515
|
+
failures.push({ expected, actual: effective });
|
|
516
|
+
}
|
|
517
|
+
if (expected.source !== 'ENVIRONMENT_VARIABLE') continue;
|
|
518
|
+
|
|
519
|
+
envs = environment_variable.filter(i => i.canonical_name === expected.canonical_name);
|
|
520
|
+
expect(envs).an('array')
|
|
521
|
+
.lengthOf(1, `Expected to find ${expected.name} in environment_variable`);
|
|
522
|
+
envs = envs[0];
|
|
523
|
+
try {
|
|
524
|
+
expect(expected).to.deep.equal(envs);
|
|
525
|
+
} catch (e) {
|
|
526
|
+
envFailures.push({ expected, actual: envs });
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
// this provides a useful error message
|
|
530
|
+
expect(failures).eql([]);
|
|
531
|
+
expect(envFailures).eql([]);
|
|
500
532
|
});
|
|
501
533
|
|
|
502
534
|
it('stringifies values and redacts api keys when `redact` is set to true', function () {
|
|
@@ -504,11 +536,13 @@ describe('config', function () {
|
|
|
504
536
|
const {
|
|
505
537
|
config: {
|
|
506
538
|
effective_config,
|
|
539
|
+
environment_variable,
|
|
507
540
|
}
|
|
508
541
|
} = result;
|
|
509
542
|
|
|
510
543
|
expect(result.config).to.have.property('status', 'Success');
|
|
511
|
-
|
|
544
|
+
|
|
545
|
+
const expectedResults = [
|
|
512
546
|
{
|
|
513
547
|
canonical_name: 'api.enable',
|
|
514
548
|
name: 'api.enable',
|
|
@@ -593,7 +627,37 @@ describe('config', function () {
|
|
|
593
627
|
value: '150',
|
|
594
628
|
source: 'DEFAULT_VALUE'
|
|
595
629
|
}
|
|
596
|
-
|
|
630
|
+
];
|
|
631
|
+
|
|
632
|
+
const failures = [];
|
|
633
|
+
let effective;
|
|
634
|
+
let envs;
|
|
635
|
+
const envFailures = [];
|
|
636
|
+
|
|
637
|
+
for (const expected of expectedResults) {
|
|
638
|
+
effective = effective_config.filter(i => i.canonical_name === expected.canonical_name);
|
|
639
|
+
expect(effective).an('array').lengthOf(1); // eslint-disable-line
|
|
640
|
+
effective = effective[0];
|
|
641
|
+
try {
|
|
642
|
+
expect(expected).to.deep.equal(effective);
|
|
643
|
+
} catch (e) {
|
|
644
|
+
failures.push({ expected, actual: effective });
|
|
645
|
+
}
|
|
646
|
+
if (expected.source !== 'ENVIRONMENT_VARIABLE') continue;
|
|
647
|
+
|
|
648
|
+
envs = environment_variable.filter(i => i.canonical_name === expected.canonical_name);
|
|
649
|
+
expect(envs).an('array')
|
|
650
|
+
.lengthOf(1, `Expected to find ${expected.name} in environment_variable`);
|
|
651
|
+
envs = envs[0];
|
|
652
|
+
try {
|
|
653
|
+
expect(expected).to.deep.equal(envs);
|
|
654
|
+
} catch (e) {
|
|
655
|
+
envFailures.push({ expected, actual: envs });
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
// this provides a useful error message
|
|
659
|
+
expect(failures).eql([]);
|
|
660
|
+
expect(envFailures).eql([]);
|
|
597
661
|
});
|
|
598
662
|
});
|
|
599
663
|
});
|
package/lib/options.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
const os = require('os');
|
|
21
21
|
const url = require('url');
|
|
22
22
|
const path = require('path');
|
|
23
|
-
const { Rule } = require('@contrast/common');
|
|
23
|
+
const { Rule, primordials: { BufferFrom, BufferPrototypeToString, StringPrototypeReplace, StringPrototypeReplaceAll, StringPrototypeSplit, StringPrototypeToLowerCase, StringPrototypeToUpperCase, JSONParse } } = require('@contrast/common');
|
|
24
24
|
const { ConfigSource: { DEFAULT_VALUE } } = require('./common');
|
|
25
25
|
|
|
26
26
|
/**
|
|
@@ -33,7 +33,7 @@ function castBoolean(value) {
|
|
|
33
33
|
if (type !== 'string' && type !== 'boolean') {
|
|
34
34
|
return;
|
|
35
35
|
}
|
|
36
|
-
value = value.toString()
|
|
36
|
+
value = StringPrototypeToLowerCase.call(value.toString());
|
|
37
37
|
return value === 'true' || value === 't'
|
|
38
38
|
? true
|
|
39
39
|
: value === 'false' || value === 'f'
|
|
@@ -61,11 +61,11 @@ function clearBaseCase(val) {
|
|
|
61
61
|
const split = (val) => {
|
|
62
62
|
if (val === '') return [];
|
|
63
63
|
if (val === undefined) return val;
|
|
64
|
-
return
|
|
64
|
+
return StringPrototypeSplit.call(val, ',');
|
|
65
65
|
};
|
|
66
66
|
|
|
67
|
-
const uppercase = (val = '') => clearBaseCase(`${val}
|
|
68
|
-
const lowercase = (val = '') => clearBaseCase(`${val}
|
|
67
|
+
const uppercase = (val = '') => clearBaseCase(`${StringPrototypeToUpperCase.call(val)}`);
|
|
68
|
+
const lowercase = (val = '') => clearBaseCase(`${StringPrototypeToLowerCase.call(val)}`);
|
|
69
69
|
const parseNum = (val) => {
|
|
70
70
|
const float = parseFloat(val);
|
|
71
71
|
return clearBaseCase(Math.ceil(float));
|
|
@@ -129,7 +129,7 @@ const options = [
|
|
|
129
129
|
|
|
130
130
|
if (uri.pathname) {
|
|
131
131
|
// kill trailling /
|
|
132
|
-
uri.pathname = uri.pathname
|
|
132
|
+
uri.pathname = StringPrototypeReplace.call(uri.pathname, /\/+$/, '');
|
|
133
133
|
|
|
134
134
|
if (!uri.pathname.endsWith('Contrast')) {
|
|
135
135
|
uri.pathname += 'Contrast';
|
|
@@ -163,7 +163,7 @@ const options = [
|
|
|
163
163
|
fn(value, cfg, source) {
|
|
164
164
|
try {
|
|
165
165
|
// parse the base64 encoded value
|
|
166
|
-
const parsed =
|
|
166
|
+
const parsed = JSONParse(BufferPrototypeToString.call(BufferFrom(value, 'base64'), 'utf8'));
|
|
167
167
|
// set the top level `api` keys only if they aren't already present.
|
|
168
168
|
// since this value comes after the others, they should be set first if present in the config file or environment.
|
|
169
169
|
['url', 'api_key', 'service_key', 'user_name'].forEach(key => {
|
|
@@ -439,7 +439,7 @@ Example - \`/opt/Contrast/contrast.log\` creates a log in the \`/opt/Contrast\`
|
|
|
439
439
|
name: 'agent.node.cmd_ignore_list',
|
|
440
440
|
arg: '<commands>',
|
|
441
441
|
default: '',
|
|
442
|
-
fn: (arg) =>
|
|
442
|
+
fn: (arg) => StringPrototypeSplit.call(arg, ',').filter((v) => v),
|
|
443
443
|
desc: 'comma-separated list of commands that will not startup the agent if agent is required; npm* will ignore all npm executables but not your application\'s scripts'
|
|
444
444
|
},
|
|
445
445
|
{
|
|
@@ -533,6 +533,29 @@ Example - \`/opt/Contrast/contrast.log\` creates a log in the \`/opt/Contrast\`
|
|
|
533
533
|
fn: castBoolean,
|
|
534
534
|
desc: 'Include this property to determine if the Assess feature should be enabled. If this property is not present, the decision is delegated to the Contrast UI.',
|
|
535
535
|
},
|
|
536
|
+
{
|
|
537
|
+
name: 'assess.probabilistic_sampling.enable',
|
|
538
|
+
arg: '[true]',
|
|
539
|
+
default: false,
|
|
540
|
+
fn: castBoolean,
|
|
541
|
+
desc: 'Set to true to enable sampling of requests for dataflow and other Assess features',
|
|
542
|
+
},
|
|
543
|
+
{
|
|
544
|
+
name: 'assess.probabilistic_sampling.base_probability',
|
|
545
|
+
arg: '<probability>',
|
|
546
|
+
fn: (val) => {
|
|
547
|
+
const p = parseFloat(val);
|
|
548
|
+
if (p >= 0 && p <= 1) return p;
|
|
549
|
+
|
|
550
|
+
if (val && val != 'undefined') {
|
|
551
|
+
throw new Error('Invalid option: assess.probabilistic_sampling.base_probability', {
|
|
552
|
+
cause: `${val} is not not in interval 0 <= p <= 1. value as float: ${p}`
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
},
|
|
556
|
+
default: 0.01,
|
|
557
|
+
desc: 'A value p within the interval [0, 1]. Each request will share same probability p of being sampled.',
|
|
558
|
+
},
|
|
536
559
|
{
|
|
537
560
|
name: 'assess.tags',
|
|
538
561
|
arg: '<tags>',
|
|
@@ -611,7 +634,7 @@ Example - \`label1, label2, label3\``,
|
|
|
611
634
|
{
|
|
612
635
|
name: 'application.name',
|
|
613
636
|
arg: '<name>',
|
|
614
|
-
desc:
|
|
637
|
+
desc: 'Override the reported application name.',
|
|
615
638
|
},
|
|
616
639
|
{
|
|
617
640
|
name: 'application.path',
|
|
@@ -619,33 +642,41 @@ Example - \`label1, label2, label3\``,
|
|
|
619
642
|
default: '/',
|
|
620
643
|
desc: 'Override the reported application path.',
|
|
621
644
|
},
|
|
645
|
+
{
|
|
646
|
+
name: 'application.group',
|
|
647
|
+
arg: '<group>',
|
|
648
|
+
desc: 'Add the name of the application group with which this application should be associated in the Contrast UI.',
|
|
649
|
+
},
|
|
650
|
+
{
|
|
651
|
+
name: 'application.code',
|
|
652
|
+
arg: '<code>',
|
|
653
|
+
desc: 'Add the application code this application should use in the Contrast UI.'
|
|
654
|
+
},
|
|
622
655
|
{
|
|
623
656
|
name: 'application.version',
|
|
624
657
|
arg: '<version>',
|
|
625
|
-
desc:
|
|
658
|
+
desc: 'Override the reported application version.',
|
|
659
|
+
},
|
|
660
|
+
{
|
|
661
|
+
name: 'application.tags',
|
|
662
|
+
arg: '<tags>',
|
|
663
|
+
desc: 'Apply labels to an application. Labels must be formatted as a comma-delimited list. Example - `label1,label2,label3`'
|
|
664
|
+
},
|
|
665
|
+
{
|
|
666
|
+
name: 'application.metadata',
|
|
667
|
+
arg: '<metadata>',
|
|
668
|
+
desc: 'Define a set of `key=value` pairs (which conforms to RFC 2253) for specifying user-defined metadata associated with the application. The set must be formatted as a comma-delimited list of `key=value` pairs. Example - `business-unit=accounting, office=Baltimore`',
|
|
626
669
|
},
|
|
627
670
|
{
|
|
628
671
|
name: 'application.session_id',
|
|
629
672
|
arg: '<session_id>',
|
|
630
|
-
default: null,
|
|
631
673
|
desc: 'Provide the ID of a session which already exists in the Contrast UI. Vulnerabilities discovered by the agent are associated with this session. If an invalid ID is supplied, the agent will be disabled. This option and `application.session_metadata` are mutually exclusive; if both are set, the agent will be disabled.',
|
|
632
674
|
},
|
|
633
675
|
{
|
|
634
676
|
name: 'application.session_metadata',
|
|
635
677
|
arg: '<session_metadata>',
|
|
636
|
-
default: null,
|
|
637
678
|
desc: 'Provide metadata which is used to create a new session ID in the Contrast UI. Vulnerabilities discovered by the agent are associated with this new session. This value should be formatted as `key=value` pairs (conforming to RFC 2253). Available key names for this configuration are branchName, buildNumber, commitHash, committer, gitTag, repository, testRun, and version. This option and `application.session_id` are mutually exclusive; if both are set the agent will be disabled.',
|
|
638
679
|
},
|
|
639
|
-
{
|
|
640
|
-
name: 'application.group',
|
|
641
|
-
arg: '<tags>',
|
|
642
|
-
desc: "how to report the application's group for auto-grouping",
|
|
643
|
-
},
|
|
644
|
-
{
|
|
645
|
-
name: 'application.metadata',
|
|
646
|
-
arg: '<metadata>',
|
|
647
|
-
desc: 'comma-separated list of key=value pairs that are applied to each application reported by the agent.',
|
|
648
|
-
},
|
|
649
680
|
// server
|
|
650
681
|
{
|
|
651
682
|
name: 'server.name',
|
|
@@ -686,10 +717,11 @@ Example - \`label1, label2, label3\``,
|
|
|
686
717
|
fn: castBoolean,
|
|
687
718
|
desc: 'Set to `false` to disable detection of cloud provider metadata such as resource identifiers.'
|
|
688
719
|
},
|
|
689
|
-
].map((opt) =>
|
|
690
|
-
env
|
|
691
|
-
|
|
692
|
-
})
|
|
720
|
+
].map((opt) => {
|
|
721
|
+
let env = StringPrototypeReplaceAll.call(StringPrototypeToUpperCase.call(opt.name), '.', '__');
|
|
722
|
+
env = StringPrototypeReplaceAll.call(env, '-', '_');
|
|
723
|
+
return Object.assign(opt, { env: `CONTRAST__${env}` });
|
|
724
|
+
});
|
|
693
725
|
|
|
694
726
|
module.exports = options;
|
|
695
727
|
module.exports.clearBaseCase = clearBaseCase;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contrast/config",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.35.0",
|
|
4
4
|
"description": "An API for discovering Contrast agent configuration data",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"author": "Contrast Security <nodejs@contrastsecurity.com> (https://www.contrastsecurity.com)",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"test": "../scripts/test.sh"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@contrast/common": "1.
|
|
20
|
+
"@contrast/common": "1.26.0",
|
|
21
21
|
"yaml": "^2.2.2"
|
|
22
22
|
}
|
|
23
23
|
}
|