@contrast/config 1.32.0 → 1.34.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 +34 -12
- package/lib/common.test.js +183 -0
- package/lib/config.js +15 -12
- package/lib/index.d.ts +13 -6
- package/lib/index.test.js +114 -7
- package/lib/options.js +67 -13
- package/lib/validators.js +23 -0
- package/lib/validators.test.js +42 -0
- 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,11 @@ 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
|
-
'agent.logger.path': (remoteData) => remoteData.logger?.path,
|
|
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
|
+
// application-create
|
|
78
|
+
'application.session_id': (remoteData) => remoteData.settings?.assessment?.session_id,
|
|
77
79
|
// application settings
|
|
78
|
-
'assess.enable': (remoteData) => remoteData.assess?.enable,
|
|
79
80
|
'protect.enable': (remoteData) => remoteData.protect?.enable,
|
|
80
81
|
'protect.rules.cmd-injection.mode': protectModeReader(CMD_INJECTION),
|
|
81
82
|
'protect.rules.cmd-injection-command-backdoors.mode': protectModeReader(CMD_INJECTION_COMMAND_BACKDOORS),
|
|
@@ -92,6 +93,27 @@ const mappings = {
|
|
|
92
93
|
'protect.rules.unsafe-file-upload.mode': protectModeReader(UNSAFE_FILE_UPLOAD),
|
|
93
94
|
'protect.rules.untrusted-deserialization.mode': protectModeReader(UNTRUSTED_DESERIALIZATION),
|
|
94
95
|
'protect.rules.xxe.mode': protectModeReader(XXE),
|
|
96
|
+
// server features
|
|
97
|
+
'assess.enable': (remoteData) => remoteData.assess?.enable,
|
|
98
|
+
'assess.probabilistic_sampling.enable': (remoteData) => remoteData.assess?.sampling?.enable,
|
|
99
|
+
'assess.probabilistic_sampling.base_probability': (remoteData) => {
|
|
100
|
+
const request_frequency = remoteData.assess?.sampling?.request_frequency;
|
|
101
|
+
if (request_frequency > 0) {
|
|
102
|
+
const baseProbability = 1 / request_frequency;
|
|
103
|
+
if (!isNaN(baseProbability)) return baseProbability;
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
'agent.logger.level': coerceLowerCase('logger.level'),
|
|
107
|
+
'agent.logger.path': (remoteData) => remoteData.logger?.path,
|
|
108
|
+
'agent.security_logger.syslog.enable': (remoteData) => remoteData.security_logger?.syslog?.enable,
|
|
109
|
+
'agent.security_logger.syslog.ip': (remoteData) => remoteData.security_logger?.syslog?.ip,
|
|
110
|
+
'agent.security_logger.syslog.port': (remoteData) => remoteData.security_logger?.syslog?.port,
|
|
111
|
+
'agent.security_logger.syslog.facility': (remoteData) => remoteData.security_logger?.syslog?.facility,
|
|
112
|
+
'agent.security_logger.syslog.severity_exploited': coerceLowerCase('security_logger.syslog.severity_exploited'),
|
|
113
|
+
'agent.security_logger.syslog.severity_blocked': coerceLowerCase('security_logger.syslog.severity_blocked'),
|
|
114
|
+
'agent.security_logger.syslog.severity_probed': coerceLowerCase('security_logger.syslog.severity_probed'),
|
|
115
|
+
'server.environment': (remoteData) => remoteData.environment,
|
|
116
|
+
|
|
95
117
|
};
|
|
96
118
|
|
|
97
119
|
/*
|
|
@@ -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, BufferPrototypeToString, StringPrototypeToUpperCase, JSONParse } } = require('@contrast/common');
|
|
24
24
|
const options = require('./options');
|
|
25
25
|
const {
|
|
26
26
|
ConfigSource: {
|
|
@@ -38,7 +38,7 @@ const HOME_CONFIG_DIR = path.resolve(os.homedir(), '.config', 'contrast');
|
|
|
38
38
|
const OS_CONFIG_DIR = os.platform() === 'win32'
|
|
39
39
|
? path.resolve(process.env.ProgramData || '', 'contrast')
|
|
40
40
|
: '/etc/contrast';
|
|
41
|
-
const REDACTED_KEYS = ['api.api_key', 'api.service_key'];
|
|
41
|
+
const REDACTED_KEYS = ['api.api_key', 'api.service_key', 'api.token'];
|
|
42
42
|
const OVERRIDABLE_SOURCES = [DEFAULT_VALUE, CONTRAST_UI];
|
|
43
43
|
|
|
44
44
|
module.exports = class Config {
|
|
@@ -48,6 +48,7 @@ module.exports = class Config {
|
|
|
48
48
|
this._errors = [];
|
|
49
49
|
this._effectiveMap = new Map();
|
|
50
50
|
this._status = '';
|
|
51
|
+
this._logs = [];
|
|
51
52
|
|
|
52
53
|
// config object
|
|
53
54
|
this.api = {};
|
|
@@ -73,7 +74,7 @@ module.exports = class Config {
|
|
|
73
74
|
|
|
74
75
|
// report all the errors found during initialization.
|
|
75
76
|
if (this._errors.length) {
|
|
76
|
-
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}`), '; ');
|
|
77
78
|
throw new Error(`Errors found in configuration ${errors}`);
|
|
78
79
|
}
|
|
79
80
|
|
|
@@ -103,7 +104,7 @@ module.exports = class Config {
|
|
|
103
104
|
let parsedEnv;
|
|
104
105
|
|
|
105
106
|
try {
|
|
106
|
-
parsedEnv =
|
|
107
|
+
parsedEnv = JSONParse(env.pm2_env);
|
|
107
108
|
} catch (_err) {
|
|
108
109
|
const err = new Error(`Unable to parse pm2 environment variable: '${env.pm2_env}'`);
|
|
109
110
|
err.cause = _err;
|
|
@@ -121,7 +122,7 @@ module.exports = class Config {
|
|
|
121
122
|
}
|
|
122
123
|
|
|
123
124
|
return Object.entries(env).reduce((acc, [key, val]) => {
|
|
124
|
-
const name =
|
|
125
|
+
const name = StringPrototypeToUpperCase.call(key);
|
|
125
126
|
if (name.startsWith('CONTRAST_')) {
|
|
126
127
|
acc[name] = val;
|
|
127
128
|
}
|
|
@@ -164,7 +165,7 @@ module.exports = class Config {
|
|
|
164
165
|
let fileContents;
|
|
165
166
|
|
|
166
167
|
try {
|
|
167
|
-
fileContents = fs.readFileSync(_filepath)
|
|
168
|
+
fileContents = BufferPrototypeToString.call(fs.readFileSync(_filepath), 'utf-8');
|
|
168
169
|
} catch (e) {
|
|
169
170
|
const err = new Error(`Unable to read Contrast configuration file: '${_filepath}'`);
|
|
170
171
|
err.cause = e;
|
|
@@ -207,7 +208,7 @@ module.exports = class Config {
|
|
|
207
208
|
source = DEFAULT_VALUE;
|
|
208
209
|
}
|
|
209
210
|
|
|
210
|
-
if (opt.fn) value = opt.fn(value);
|
|
211
|
+
if (opt.fn) value = opt.fn(value, this, source);
|
|
211
212
|
if (opt.enum && !opt.enum.includes(value)) value = opt.default;
|
|
212
213
|
|
|
213
214
|
set(this, opt.name, value);
|
|
@@ -221,7 +222,7 @@ module.exports = class Config {
|
|
|
221
222
|
}
|
|
222
223
|
|
|
223
224
|
_redact(name, value) {
|
|
224
|
-
if (value
|
|
225
|
+
if (value === null) return value;
|
|
225
226
|
return REDACTED_KEYS.includes(name) ? `contrast-redacted-${name}` : value;
|
|
226
227
|
}
|
|
227
228
|
|
|
@@ -239,12 +240,14 @@ module.exports = class Config {
|
|
|
239
240
|
const effective_config = [], environment_variable = [], contrast_ui = [];
|
|
240
241
|
|
|
241
242
|
Array.from(this._effectiveMap.values()).forEach((v) => {
|
|
242
|
-
let value =
|
|
243
|
+
let { value } = v;
|
|
244
|
+
if (redact) value = this._redact(v.name, v.value);
|
|
243
245
|
if (value === undefined) value = null;
|
|
244
246
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
if (v.source ===
|
|
247
|
+
const redacted = { ...v, value: String(value) };
|
|
248
|
+
effective_config.push(redacted);
|
|
249
|
+
if (v.source === ENVIRONMENT_VARIABLE) environment_variable.push(redacted);
|
|
250
|
+
if (v.source === CONTRAST_UI) contrast_ui.push(redacted);
|
|
248
251
|
});
|
|
249
252
|
|
|
250
253
|
return {
|
package/lib/index.d.ts
CHANGED
|
@@ -15,13 +15,13 @@
|
|
|
15
15
|
|
|
16
16
|
import { ProtectRuleMode, Rule } from '@contrast/common';
|
|
17
17
|
import { LevelWithSilent } from 'pino';
|
|
18
|
-
export { ConfigSource } from './common
|
|
18
|
+
export { ConfigSource } from './common';
|
|
19
19
|
|
|
20
20
|
export interface EffectiveEntry<T> {
|
|
21
21
|
canonical_name: string;
|
|
22
22
|
name: string;
|
|
23
23
|
value: T;
|
|
24
|
-
source:
|
|
24
|
+
source: string;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
export type Level =
|
|
@@ -47,7 +47,7 @@ export interface ConfigOption<T> {
|
|
|
47
47
|
arg: string;
|
|
48
48
|
enum?: T[];
|
|
49
49
|
default?: T;
|
|
50
|
-
fn?: (arg: any) => T;
|
|
50
|
+
fn?: (arg: any, cfg: Config, source: string) => T;
|
|
51
51
|
desc: string;
|
|
52
52
|
}
|
|
53
53
|
|
|
@@ -56,6 +56,13 @@ export interface Config {
|
|
|
56
56
|
_effectiveMap: Map<string, EffectiveEntry<any>>;
|
|
57
57
|
_errors: Error[];
|
|
58
58
|
_status: string,
|
|
59
|
+
_logs: {
|
|
60
|
+
level: import('pino').LevelWithSilentOrString;
|
|
61
|
+
obj?: any;
|
|
62
|
+
msg: string;
|
|
63
|
+
args?: any[];
|
|
64
|
+
}[];
|
|
65
|
+
|
|
59
66
|
|
|
60
67
|
api: {
|
|
61
68
|
/** Default: `true` */
|
|
@@ -296,10 +303,10 @@ export interface Config {
|
|
|
296
303
|
/** Default: `true` */
|
|
297
304
|
discover_cloud_resource: boolean;
|
|
298
305
|
};
|
|
299
|
-
getEffectiveSource(cannonicalName: string):
|
|
300
|
-
getEffectiveValue(cannonicalName: string):
|
|
306
|
+
getEffectiveSource(cannonicalName: string): string;
|
|
307
|
+
getEffectiveValue<T = any>(cannonicalName: string): T;
|
|
301
308
|
getReport({ redact: boolean }): any;
|
|
302
|
-
setValue(name: string, value:
|
|
309
|
+
setValue<T = any>(name: string, value: T, source: string): void;
|
|
303
310
|
}
|
|
304
311
|
|
|
305
312
|
declare function init(core: { config?: Config }): Config;
|
package/lib/index.test.js
CHANGED
|
@@ -186,6 +186,49 @@ describe('config', function () {
|
|
|
186
186
|
});
|
|
187
187
|
});
|
|
188
188
|
|
|
189
|
+
describe('api.token handling', function () {
|
|
190
|
+
it('sets api config vars when they are not present', function () {
|
|
191
|
+
env['CONTRAST_CONFIG_PATH'] = getGoodConfig('token');
|
|
192
|
+
const cfg = config();
|
|
193
|
+
|
|
194
|
+
expect(cfg.api).to.include({
|
|
195
|
+
url: 'http://localhost:12345/Contrast',
|
|
196
|
+
api_key: 'secret',
|
|
197
|
+
service_key: 'secret',
|
|
198
|
+
user_name: 'tokenuser'
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('does not override api config vars when they are explicitly set in config', function () {
|
|
203
|
+
// url: 'http://localhost:12345/Contrast',
|
|
204
|
+
// api_key: 'secret',
|
|
205
|
+
// service_key: 'secret',
|
|
206
|
+
// user_name: 'tokenuser'
|
|
207
|
+
env['CONTRAST__API__TOKEN'] = 'eyJ1cmwiOiJodHRwOi8vbG9jYWxob3N0OjEyMzQ1L0NvbnRyYXN0IiwiYXBpX2tleSI6InNlY3JldCIsInNlcnZpY2Vfa2V5Ijoic2VjcmV0IiwidXNlcl9uYW1lIjoidG9rZW51c2VyIn0=';
|
|
208
|
+
const cfg = config();
|
|
209
|
+
|
|
210
|
+
expect(cfg.api).to.include({
|
|
211
|
+
url: 'http://localhost:19080/Contrast',
|
|
212
|
+
api_key: 'demo',
|
|
213
|
+
service_key: 'demo',
|
|
214
|
+
user_name: 'contrast_admin'
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('observes the correct precedence', function () {
|
|
219
|
+
env['CONTRAST_CONFIG_PATH'] = getGoodConfig('token');
|
|
220
|
+
env['CONTRAST__API__API_KEY'] = 'something else';
|
|
221
|
+
const cfg = config();
|
|
222
|
+
|
|
223
|
+
expect(cfg.api).to.include({
|
|
224
|
+
url: 'http://localhost:12345/Contrast',
|
|
225
|
+
api_key: 'something else',
|
|
226
|
+
service_key: 'secret',
|
|
227
|
+
user_name: 'tokenuser'
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
189
232
|
describe('multiple errors can be thrown in an exception', function() {
|
|
190
233
|
it('should report multiple errors', function() {
|
|
191
234
|
const options = getDefaultConfig();
|
|
@@ -328,11 +371,12 @@ describe('config', function () {
|
|
|
328
371
|
// there needs to be a valid config file or env var set so config doesn't throw
|
|
329
372
|
// an error.
|
|
330
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';
|
|
331
375
|
config = require('.')(core);
|
|
332
376
|
|
|
333
377
|
config.setValue('contrast_config_path', path.resolve(__dirname, '../../test/fixtures/protect_contrast_security.yaml'), ENVIRONMENT_VARIABLE);
|
|
334
378
|
config.setValue('contrast__agent__stack_trace_limit', 20, ENVIRONMENT_VARIABLE);
|
|
335
|
-
config.setValue('
|
|
379
|
+
config.setValue('protect.rules.disabled_rules', ['cmd-injection', 'sql-injection'], ENVIRONMENT_VARIABLE);
|
|
336
380
|
config.setValue('api.enable', true, ENVIRONMENT_VARIABLE);
|
|
337
381
|
config.setValue('api.api_key', 'ABCDEFGHIJ', ENVIRONMENT_VARIABLE);
|
|
338
382
|
config.setValue('api.service_key', 'KLMNOPQRST', ENVIRONMENT_VARIABLE);
|
|
@@ -364,11 +408,12 @@ describe('config', function () {
|
|
|
364
408
|
const {
|
|
365
409
|
config: {
|
|
366
410
|
effective_config,
|
|
411
|
+
environment_variable,
|
|
367
412
|
}
|
|
368
413
|
} = result;
|
|
369
414
|
|
|
370
415
|
expect(result.config).to.have.property('status', 'Success');
|
|
371
|
-
|
|
416
|
+
const expectedResults = [
|
|
372
417
|
{
|
|
373
418
|
canonical_name: 'api.enable',
|
|
374
419
|
name: 'api.enable',
|
|
@@ -379,13 +424,13 @@ describe('config', function () {
|
|
|
379
424
|
canonical_name: 'api.api_key',
|
|
380
425
|
name: 'api.api_key',
|
|
381
426
|
source: 'ENVIRONMENT_VARIABLE',
|
|
382
|
-
value: '
|
|
427
|
+
value: 'ABCDEFGHIJ',
|
|
383
428
|
},
|
|
384
429
|
{
|
|
385
430
|
canonical_name: 'api.service_key',
|
|
386
431
|
name: 'api.service_key',
|
|
387
432
|
source: 'ENVIRONMENT_VARIABLE',
|
|
388
|
-
value: '
|
|
433
|
+
value: 'KLMNOPQRST',
|
|
389
434
|
},
|
|
390
435
|
{
|
|
391
436
|
canonical_name: 'api.proxy.enable',
|
|
@@ -453,7 +498,37 @@ describe('config', function () {
|
|
|
453
498
|
value: '150',
|
|
454
499
|
source: 'DEFAULT_VALUE'
|
|
455
500
|
}
|
|
456
|
-
|
|
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([]);
|
|
457
532
|
});
|
|
458
533
|
|
|
459
534
|
it('stringifies values and redacts api keys when `redact` is set to true', function () {
|
|
@@ -461,11 +536,13 @@ describe('config', function () {
|
|
|
461
536
|
const {
|
|
462
537
|
config: {
|
|
463
538
|
effective_config,
|
|
539
|
+
environment_variable,
|
|
464
540
|
}
|
|
465
541
|
} = result;
|
|
466
542
|
|
|
467
543
|
expect(result.config).to.have.property('status', 'Success');
|
|
468
|
-
|
|
544
|
+
|
|
545
|
+
const expectedResults = [
|
|
469
546
|
{
|
|
470
547
|
canonical_name: 'api.enable',
|
|
471
548
|
name: 'api.enable',
|
|
@@ -550,7 +627,37 @@ describe('config', function () {
|
|
|
550
627
|
value: '150',
|
|
551
628
|
source: 'DEFAULT_VALUE'
|
|
552
629
|
}
|
|
553
|
-
|
|
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([]);
|
|
554
661
|
});
|
|
555
662
|
});
|
|
556
663
|
});
|
package/lib/options.js
CHANGED
|
@@ -20,7 +20,8 @@
|
|
|
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
|
+
const { ConfigSource: { DEFAULT_VALUE } } = require('./common');
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
27
|
* Takes strings "true"|"t" or "false"|"f" (case insensitive) and return the appropriate boolean.
|
|
@@ -32,7 +33,7 @@ function castBoolean(value) {
|
|
|
32
33
|
if (type !== 'string' && type !== 'boolean') {
|
|
33
34
|
return;
|
|
34
35
|
}
|
|
35
|
-
value = value.toString()
|
|
36
|
+
value = StringPrototypeToLowerCase.call(value.toString());
|
|
36
37
|
return value === 'true' || value === 't'
|
|
37
38
|
? true
|
|
38
39
|
: value === 'false' || value === 'f'
|
|
@@ -60,11 +61,11 @@ function clearBaseCase(val) {
|
|
|
60
61
|
const split = (val) => {
|
|
61
62
|
if (val === '') return [];
|
|
62
63
|
if (val === undefined) return val;
|
|
63
|
-
return
|
|
64
|
+
return StringPrototypeSplit.call(val, ',');
|
|
64
65
|
};
|
|
65
66
|
|
|
66
|
-
const uppercase = (val = '') => clearBaseCase(`${val}
|
|
67
|
-
const lowercase = (val = '') => clearBaseCase(`${val}
|
|
67
|
+
const uppercase = (val = '') => clearBaseCase(`${StringPrototypeToUpperCase.call(val)}`);
|
|
68
|
+
const lowercase = (val = '') => clearBaseCase(`${StringPrototypeToLowerCase.call(val)}`);
|
|
68
69
|
const parseNum = (val) => {
|
|
69
70
|
const float = parseFloat(val);
|
|
70
71
|
return clearBaseCase(Math.ceil(float));
|
|
@@ -128,7 +129,7 @@ const options = [
|
|
|
128
129
|
|
|
129
130
|
if (uri.pathname) {
|
|
130
131
|
// kill trailling /
|
|
131
|
-
uri.pathname = uri.pathname
|
|
132
|
+
uri.pathname = StringPrototypeReplace.call(uri.pathname, /\/+$/, '');
|
|
132
133
|
|
|
133
134
|
if (!uri.pathname.endsWith('Contrast')) {
|
|
134
135
|
uri.pathname += 'Contrast';
|
|
@@ -155,6 +156,35 @@ const options = [
|
|
|
155
156
|
arg: '<name>',
|
|
156
157
|
desc: 'Set the user name used to communicate with the Contrast UI. It is used to calculate the Authorization header.',
|
|
157
158
|
},
|
|
159
|
+
{
|
|
160
|
+
name: 'api.token',
|
|
161
|
+
arg: '<token>',
|
|
162
|
+
desc: 'base64 encoded JSON object containing the `url`, `api_key`, `service_key`, and `user_name` config options, allowing them all to be set in a single variable.',
|
|
163
|
+
fn(value, cfg, source) {
|
|
164
|
+
try {
|
|
165
|
+
// parse the base64 encoded value
|
|
166
|
+
const parsed = JSONParse(BufferPrototypeToString.call(BufferFrom(value, 'base64'), 'utf8'));
|
|
167
|
+
// set the top level `api` keys only if they aren't already present.
|
|
168
|
+
// since this value comes after the others, they should be set first if present in the config file or environment.
|
|
169
|
+
['url', 'api_key', 'service_key', 'user_name'].forEach(key => {
|
|
170
|
+
const canonicalName = `api.${key}`;
|
|
171
|
+
const existingSource = cfg.getEffectiveSource(canonicalName);
|
|
172
|
+
if (existingSource !== DEFAULT_VALUE) {
|
|
173
|
+
cfg._logs.push({
|
|
174
|
+
level: 'info',
|
|
175
|
+
msg: 'Using configured value for `%s` (set by %s) instead of `api.token`.',
|
|
176
|
+
args: [canonicalName, existingSource]
|
|
177
|
+
});
|
|
178
|
+
} else {
|
|
179
|
+
cfg.setValue(canonicalName, parsed[key], source);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
return value;
|
|
183
|
+
} catch {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
},
|
|
158
188
|
// api.proxy
|
|
159
189
|
{
|
|
160
190
|
name: 'api.proxy.enable',
|
|
@@ -409,7 +439,7 @@ Example - \`/opt/Contrast/contrast.log\` creates a log in the \`/opt/Contrast\`
|
|
|
409
439
|
name: 'agent.node.cmd_ignore_list',
|
|
410
440
|
arg: '<commands>',
|
|
411
441
|
default: '',
|
|
412
|
-
fn: (arg) =>
|
|
442
|
+
fn: (arg) => StringPrototypeSplit.call(arg, ',').filter((v) => v),
|
|
413
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'
|
|
414
444
|
},
|
|
415
445
|
{
|
|
@@ -503,6 +533,29 @@ Example - \`/opt/Contrast/contrast.log\` creates a log in the \`/opt/Contrast\`
|
|
|
503
533
|
fn: castBoolean,
|
|
504
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.',
|
|
505
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
|
+
},
|
|
506
559
|
{
|
|
507
560
|
name: 'assess.tags',
|
|
508
561
|
arg: '<tags>',
|
|
@@ -512,10 +565,10 @@ Example - \`label1, label2, label3\``,
|
|
|
512
565
|
{
|
|
513
566
|
name: 'assess.stacktraces',
|
|
514
567
|
arg: '<level>',
|
|
515
|
-
enum: ['ALL', 'SOME', 'NONE'],
|
|
568
|
+
enum: ['ALL', 'SOME', 'SINK', 'NONE'],
|
|
516
569
|
default: 'ALL',
|
|
517
570
|
fn: uppercase,
|
|
518
|
-
desc: 'Select the level of collected stacktraces. ALL - for all assess events, SOME - for Source and Sink events, NONE - no stacktraces collected',
|
|
571
|
+
desc: 'Select the level of collected stacktraces. ALL - for all assess events, SOME - for Source and Sink events, SINK - for Sink events, NONE - no stacktraces collected',
|
|
519
572
|
},
|
|
520
573
|
{
|
|
521
574
|
name: 'assess.max_context_source_events',
|
|
@@ -656,10 +709,11 @@ Example - \`label1, label2, label3\``,
|
|
|
656
709
|
fn: castBoolean,
|
|
657
710
|
desc: 'Set to `false` to disable detection of cloud provider metadata such as resource identifiers.'
|
|
658
711
|
},
|
|
659
|
-
].map((opt) =>
|
|
660
|
-
env
|
|
661
|
-
|
|
662
|
-
})
|
|
712
|
+
].map((opt) => {
|
|
713
|
+
let env = StringPrototypeReplaceAll.call(StringPrototypeToUpperCase.call(opt.name), '.', '__');
|
|
714
|
+
env = StringPrototypeReplaceAll.call(env, '-', '_');
|
|
715
|
+
return Object.assign(opt, { env: `CONTRAST__${env}` });
|
|
716
|
+
});
|
|
663
717
|
|
|
664
718
|
module.exports = options;
|
|
665
719
|
module.exports.clearBaseCase = clearBaseCase;
|
|
@@ -0,0 +1,23 @@
|
|
|
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
|
+
// Abusing the `validators` pattern to allow us to log after core has been set up.
|
|
19
|
+
module.exports.config = function config(core) {
|
|
20
|
+
core.config._logs.forEach(({ level, obj, msg, args = [] }) => {
|
|
21
|
+
core.logger[level](obj, msg, ...args);
|
|
22
|
+
});
|
|
23
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { expect } = require('chai');
|
|
4
|
+
const mocks = require('@contrast/test/mocks');
|
|
5
|
+
const validators = require('./validators');
|
|
6
|
+
|
|
7
|
+
describe('config validators', function () {
|
|
8
|
+
let core;
|
|
9
|
+
|
|
10
|
+
beforeEach(function () {
|
|
11
|
+
core = { logger: mocks.logger() };
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe('config', function () {
|
|
15
|
+
it('calls `logger` for each log message provided', function () {
|
|
16
|
+
core.config = {
|
|
17
|
+
_logs: [
|
|
18
|
+
{ level: 'info', msg: 'some info level message' },
|
|
19
|
+
{
|
|
20
|
+
level: 'debug',
|
|
21
|
+
obj: { foo: 'bar' },
|
|
22
|
+
msg: 'some debug level message with %d %s',
|
|
23
|
+
args: [2, 'args'],
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
validators.config(core);
|
|
29
|
+
|
|
30
|
+
expect(core.logger.info).to.have.been.calledWith(
|
|
31
|
+
undefined,
|
|
32
|
+
'some info level message'
|
|
33
|
+
);
|
|
34
|
+
expect(core.logger.debug).to.have.been.calledWith(
|
|
35
|
+
{ foo: 'bar' },
|
|
36
|
+
'some debug level message with %d %s',
|
|
37
|
+
2,
|
|
38
|
+
'args',
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contrast/config",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.34.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
|
}
|