@contrast/config 1.16.0 → 1.18.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 +102 -0
- package/lib/config.js +263 -0
- package/lib/index.d.ts +16 -4
- package/lib/index.js +6 -3
- package/lib/options.js +7 -18
- package/package.json +3 -3
- package/lib/util.js +0 -288
package/lib/common.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright: 2023 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 {
|
|
19
|
+
ProtectRuleMode: {
|
|
20
|
+
OFF,
|
|
21
|
+
MONITOR,
|
|
22
|
+
BLOCK,
|
|
23
|
+
BLOCK_AT_PERIMETER
|
|
24
|
+
},
|
|
25
|
+
Rule: {
|
|
26
|
+
CMD_INJECTION,
|
|
27
|
+
CMD_INJECTION_COMMAND_BACKDOORS,
|
|
28
|
+
CMD_INJECTION_SEMANTIC_CHAINED_COMMANDS,
|
|
29
|
+
CMD_INJECTION_SEMANTIC_DANGEROUS_PATHS,
|
|
30
|
+
METHOD_TAMPERING,
|
|
31
|
+
NOSQL_INJECTION,
|
|
32
|
+
NOSQL_INJECTION_MONGO,
|
|
33
|
+
PATH_TRAVERSAL,
|
|
34
|
+
PATH_TRAVERSAL_SEMANTIC_FILE_SECURITY_BYPASS,
|
|
35
|
+
REFLECTED_XSS,
|
|
36
|
+
SQL_INJECTION,
|
|
37
|
+
SSJS_INJECTION,
|
|
38
|
+
UNSAFE_FILE_UPLOAD,
|
|
39
|
+
UNTRUSTED_DESERIALIZATION,
|
|
40
|
+
XXE,
|
|
41
|
+
},
|
|
42
|
+
} = require('@contrast/common');
|
|
43
|
+
|
|
44
|
+
function protectModeReader(ruleId) {
|
|
45
|
+
return function (msg) {
|
|
46
|
+
const remoteSetting = msg?.protect?.rules?.[ruleId];
|
|
47
|
+
switch (remoteSetting?.mode) {
|
|
48
|
+
case 'OFF': return OFF;
|
|
49
|
+
case 'MONITOR':
|
|
50
|
+
case 'MONITORING': return MONITOR;
|
|
51
|
+
case 'BLOCK':
|
|
52
|
+
case 'BLOCKING': return BLOCK;
|
|
53
|
+
case 'BLOCK_AT_PERIMETER': return BLOCK_AT_PERIMETER;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const ConfigSource = {
|
|
59
|
+
CONTRAST_UI: 'CONTRAST_UI',
|
|
60
|
+
DEFAULT_VALUE: 'DEFAULT_VALUE',
|
|
61
|
+
ENVIRONMENT_VARIABLE: 'ENVIRONMENT_VARIABLE',
|
|
62
|
+
USER_CONFIGURATION_FILE: 'USER_CONFIGURATION_FILE',
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const mappings = {
|
|
66
|
+
// server features
|
|
67
|
+
'agent.logger.level': (remoteData) => remoteData.logger?.level?.toLowerCase?.(),
|
|
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 settings
|
|
78
|
+
'protect.rules.cmd-injection.mode': protectModeReader(CMD_INJECTION),
|
|
79
|
+
'protect.rules.cmd-injection-command-backdoors.mode': protectModeReader(CMD_INJECTION_COMMAND_BACKDOORS),
|
|
80
|
+
'protect.rules.cmd-injection-semantic-chained-commands.mode': protectModeReader(CMD_INJECTION_SEMANTIC_CHAINED_COMMANDS),
|
|
81
|
+
'protect.rules.cmd-injection-semantic-dangerous-paths.mode': protectModeReader(CMD_INJECTION_SEMANTIC_DANGEROUS_PATHS),
|
|
82
|
+
'protect.rules.method-tampering.mode': protectModeReader(METHOD_TAMPERING),
|
|
83
|
+
'protect.rules.nosql-injection.mode': protectModeReader(NOSQL_INJECTION),
|
|
84
|
+
'protect.rules.nosql-injection-mongo.mode': protectModeReader(NOSQL_INJECTION_MONGO),
|
|
85
|
+
'protect.rules.path-traversal.mode': protectModeReader(PATH_TRAVERSAL),
|
|
86
|
+
'protect.rules.path-traversal-semantic-file-security-bypass.mode': protectModeReader(PATH_TRAVERSAL_SEMANTIC_FILE_SECURITY_BYPASS),
|
|
87
|
+
'protect.rules.reflected-xss.mode': protectModeReader(REFLECTED_XSS),
|
|
88
|
+
'protect.rules.sql-injection.mode': protectModeReader(SQL_INJECTION),
|
|
89
|
+
'protect.rules.ssjs-injection.mode': protectModeReader(SSJS_INJECTION),
|
|
90
|
+
'protect.rules.unsafe-file-upload.mode': protectModeReader(UNSAFE_FILE_UPLOAD),
|
|
91
|
+
'protect.rules.untrusted-deserialization.mode': protectModeReader(UNTRUSTED_DESERIALIZATION),
|
|
92
|
+
'protect.rules.xxe.mode': protectModeReader(XXE),
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/*
|
|
96
|
+
* Keys are canonical name and values are functions which read the equivalent value
|
|
97
|
+
* from the TS response object message.
|
|
98
|
+
*/
|
|
99
|
+
module.exports = {
|
|
100
|
+
ConfigSource,
|
|
101
|
+
mappings,
|
|
102
|
+
};
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright: 2023 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 process = require('process');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const os = require('os');
|
|
22
|
+
const yaml = require('yaml');
|
|
23
|
+
const { Event, get, set } = require('@contrast/common');
|
|
24
|
+
const options = require('./options');
|
|
25
|
+
const {
|
|
26
|
+
ConfigSource: {
|
|
27
|
+
CONTRAST_UI,
|
|
28
|
+
DEFAULT_VALUE,
|
|
29
|
+
ENVIRONMENT_VARIABLE,
|
|
30
|
+
USER_CONFIGURATION_FILE,
|
|
31
|
+
},
|
|
32
|
+
mappings,
|
|
33
|
+
} = require('./common');
|
|
34
|
+
|
|
35
|
+
const CONTRAST_CONFIG_PATH = 'CONTRAST_CONFIG_PATH';
|
|
36
|
+
const CONTRAST_PREFIX = 'CONTRAST_';
|
|
37
|
+
const OS_CONFIG_DIR = os.platform() === 'win32'
|
|
38
|
+
? path.resolve(process.env.ProgramData || '', 'contrast')
|
|
39
|
+
: '/etc/contrast';
|
|
40
|
+
const REDACTED_KEYS = ['api.api_key', 'api.service_key'];
|
|
41
|
+
|
|
42
|
+
module.exports = class Config {
|
|
43
|
+
constructor(core) {
|
|
44
|
+
// internals
|
|
45
|
+
this._filepath = '';
|
|
46
|
+
this._errors = [];
|
|
47
|
+
this._effectiveMap = new Map();
|
|
48
|
+
this._status = '';
|
|
49
|
+
|
|
50
|
+
// config object
|
|
51
|
+
this.api = {};
|
|
52
|
+
this.agent = {
|
|
53
|
+
diagnostics: {},
|
|
54
|
+
reporters: {},
|
|
55
|
+
security_logger: {},
|
|
56
|
+
logger: {},
|
|
57
|
+
node: {},
|
|
58
|
+
};
|
|
59
|
+
this.application = {};
|
|
60
|
+
this.assess = {};
|
|
61
|
+
this.inventory = {};
|
|
62
|
+
this.protect = {
|
|
63
|
+
rules: {},
|
|
64
|
+
disabled_rules: ''
|
|
65
|
+
};
|
|
66
|
+
this.server = {};
|
|
67
|
+
|
|
68
|
+
// initialize
|
|
69
|
+
this._build();
|
|
70
|
+
this._validate();
|
|
71
|
+
core.messages?.on?.(Event.SERVER_SETTINGS_UPDATE, (msg) => {
|
|
72
|
+
if (!this._status) this._status = 'Success';
|
|
73
|
+
|
|
74
|
+
for (const [name, mapper] of Object.entries(mappings)) {
|
|
75
|
+
if (this.getEffectiveSource(name) === DEFAULT_VALUE) {
|
|
76
|
+
const remoteValue = mapper(msg);
|
|
77
|
+
if (remoteValue != null) {
|
|
78
|
+
this._effectiveMap.set(name, {
|
|
79
|
+
canonical_name: name,
|
|
80
|
+
name,
|
|
81
|
+
value: remoteValue,
|
|
82
|
+
source: CONTRAST_UI,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
_initEnv() {
|
|
91
|
+
const { env } = process;
|
|
92
|
+
|
|
93
|
+
if (env.pm2_env?.includes?.(CONTRAST_PREFIX)) {
|
|
94
|
+
let parsedEnv;
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
parsedEnv = JSON.parse(env.pm2_env);
|
|
98
|
+
} catch (_err) {
|
|
99
|
+
const err = new Error(`Unable to parse pm2 environment variable: '${env.pm2_env}'`);
|
|
100
|
+
err.cause = _err;
|
|
101
|
+
this._errors.push(err);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
for (const pm2EnvConfig of [parsedEnv?.env, parsedEnv]) {
|
|
105
|
+
if (!pm2EnvConfig) continue;
|
|
106
|
+
|
|
107
|
+
for (const [name, value] of Object.entries(pm2EnvConfig)) {
|
|
108
|
+
if (env[name] || !name.startsWith(CONTRAST_PREFIX)) continue;
|
|
109
|
+
env[name] = value;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return Object.entries(env).reduce((acc, [key, val]) => {
|
|
115
|
+
const name = key.toUpperCase();
|
|
116
|
+
if (name.startsWith('CONTRAST_')) {
|
|
117
|
+
acc[name] = val;
|
|
118
|
+
}
|
|
119
|
+
return acc;
|
|
120
|
+
}, {});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Returns the locations to search for configuration files. Being a function
|
|
125
|
+
* allows us to stub these locations within tests.
|
|
126
|
+
*/
|
|
127
|
+
_configDirs() {
|
|
128
|
+
return [
|
|
129
|
+
process.cwd(),
|
|
130
|
+
path.resolve(OS_CONFIG_DIR, 'node'),
|
|
131
|
+
OS_CONFIG_DIR,
|
|
132
|
+
path.resolve(os.homedir(), '.config', 'contrast'),
|
|
133
|
+
];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
_initFile() {
|
|
137
|
+
let fileConfig = {};
|
|
138
|
+
|
|
139
|
+
this._filepath = process.env[CONTRAST_CONFIG_PATH];
|
|
140
|
+
|
|
141
|
+
if (!this._filepath) {
|
|
142
|
+
for (const dir of this._configDirs()) {
|
|
143
|
+
const currentPath = path.resolve(dir, 'contrast_security.yaml');
|
|
144
|
+
if (fs.existsSync(currentPath)) {
|
|
145
|
+
this._filepath = currentPath;
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const { _filepath } = this;
|
|
152
|
+
|
|
153
|
+
if (_filepath) {
|
|
154
|
+
let fileContents;
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
fileContents = fs.readFileSync(_filepath).toString('utf-8');
|
|
158
|
+
} catch (e) {
|
|
159
|
+
const err = new Error(`Unable to read Contrast configuration file: '${_filepath}'`);
|
|
160
|
+
err.cause = e;
|
|
161
|
+
this._errors.push(err);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (fileContents) {
|
|
165
|
+
try {
|
|
166
|
+
fileConfig = yaml.parse(fileContents, { prettyErrors: true });
|
|
167
|
+
} catch (e) {
|
|
168
|
+
const err = new Error(`Contrast configuration file is malformed: '${_filepath}'`);
|
|
169
|
+
this._errors.push(err);
|
|
170
|
+
err.cause = e;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return fileConfig;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
_build() {
|
|
179
|
+
const envOptions = this._initEnv();
|
|
180
|
+
const fileOptions = this._initFile();
|
|
181
|
+
|
|
182
|
+
this._effectiveMap.clear();
|
|
183
|
+
|
|
184
|
+
for (const opt of options) {
|
|
185
|
+
const envValue = envOptions[opt.env];
|
|
186
|
+
const fileValue = get(fileOptions, opt.name);
|
|
187
|
+
let source, value;
|
|
188
|
+
|
|
189
|
+
if (envValue != null) {
|
|
190
|
+
source = ENVIRONMENT_VARIABLE;
|
|
191
|
+
value = envValue;
|
|
192
|
+
} else if (fileValue != null) {
|
|
193
|
+
source = USER_CONFIGURATION_FILE;
|
|
194
|
+
value = fileValue;
|
|
195
|
+
} else {
|
|
196
|
+
value = opt.default;
|
|
197
|
+
source = DEFAULT_VALUE;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (opt.fn) value = opt.fn(value);
|
|
201
|
+
if (opt.enum && !opt.enum.includes(value)) value = opt.default;
|
|
202
|
+
|
|
203
|
+
set(this, opt.name, value);
|
|
204
|
+
this._effectiveMap.set(opt.name, {
|
|
205
|
+
canonical_name: opt.name,
|
|
206
|
+
name: opt.name,
|
|
207
|
+
value,
|
|
208
|
+
source,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
_redact(name, value) {
|
|
214
|
+
if (value == null) return value;
|
|
215
|
+
return REDACTED_KEYS.includes(name) ? `contrast-redacted-${name}` : value;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
_validate() {
|
|
219
|
+
if (
|
|
220
|
+
get(this, 'application.session_id') &&
|
|
221
|
+
get(this, 'application.session_metadata')
|
|
222
|
+
) {
|
|
223
|
+
const err = new Error('Cannot set both `application.session_id` and `application.session_metadata`');
|
|
224
|
+
this._errors.push(err);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
getReport({ redact = true }) {
|
|
229
|
+
const effective_config = [], environment_variable = [], contrast_ui = [];
|
|
230
|
+
|
|
231
|
+
Array.from(this._effectiveMap.values()).forEach((v) => {
|
|
232
|
+
let value = redact ? this._redact(v.name, v.value) : v.value;
|
|
233
|
+
if (value === undefined) value = null;
|
|
234
|
+
|
|
235
|
+
effective_config.push({ ...v, value: String(value) });
|
|
236
|
+
if (v.source === ENVIRONMENT_VARIABLE) environment_variable.push(v);
|
|
237
|
+
if (v.source === CONTRAST_UI) contrast_ui.push(v);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
report_create: new Date(),
|
|
242
|
+
config: {
|
|
243
|
+
status: this._status,
|
|
244
|
+
effective_config,
|
|
245
|
+
environment_variable,
|
|
246
|
+
contrast_ui,
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
getEffectiveSource(canonicalName) {
|
|
252
|
+
return this._effectiveMap.get(canonicalName)?.source;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
getEffectiveValue(canonicalName) {
|
|
256
|
+
return this._effectiveMap.get(canonicalName)?.value;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
setValue(name, value, source) {
|
|
260
|
+
this._effectiveMap.set(name, { canonical_name: name, name, source, value });
|
|
261
|
+
set(this, name, value);
|
|
262
|
+
}
|
|
263
|
+
};
|
package/lib/index.d.ts
CHANGED
|
@@ -15,6 +15,14 @@
|
|
|
15
15
|
|
|
16
16
|
import { ProtectRuleMode, Rule } from '@contrast/common';
|
|
17
17
|
import { LevelWithSilent } from 'pino';
|
|
18
|
+
export { ConfigSource } from './common.js';
|
|
19
|
+
|
|
20
|
+
export interface EffectiveEntry {
|
|
21
|
+
canonical_name: string;
|
|
22
|
+
name: string;
|
|
23
|
+
value: any;
|
|
24
|
+
source: ConfigSource;
|
|
25
|
+
}
|
|
18
26
|
|
|
19
27
|
export type Level =
|
|
20
28
|
| 'error'
|
|
@@ -44,10 +52,10 @@ export interface ConfigOption<T> {
|
|
|
44
52
|
}
|
|
45
53
|
|
|
46
54
|
export interface Config {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
55
|
+
_filepath: string;
|
|
56
|
+
_effectiveMap: Map<EffectiveEntry>;
|
|
57
|
+
_errors: Error[];
|
|
58
|
+
_status: string,
|
|
51
59
|
|
|
52
60
|
api: {
|
|
53
61
|
/** Default: `true` */
|
|
@@ -263,6 +271,10 @@ export interface Config {
|
|
|
263
271
|
tags?: string;
|
|
264
272
|
version?: string;
|
|
265
273
|
};
|
|
274
|
+
getEffectiveSource(cannonicalName: string): any;
|
|
275
|
+
getEffectiveValue(cannonicalName: string): any;
|
|
276
|
+
getReport({ redact: boolean }): any;
|
|
277
|
+
setValue(name: string, value: any, source: ConfigSource): void;
|
|
266
278
|
}
|
|
267
279
|
|
|
268
280
|
interface Core {
|
package/lib/index.js
CHANGED
|
@@ -15,8 +15,11 @@
|
|
|
15
15
|
|
|
16
16
|
'use strict';
|
|
17
17
|
|
|
18
|
-
const
|
|
18
|
+
const Config = require('./config');
|
|
19
|
+
const { ConfigSource } = require('./common');
|
|
19
20
|
|
|
20
|
-
module.exports = function
|
|
21
|
-
return core.config =
|
|
21
|
+
module.exports = function (core = {}) {
|
|
22
|
+
return core.config = new Config(core);
|
|
22
23
|
};
|
|
24
|
+
|
|
25
|
+
module.exports.ConfigSource = ConfigSource;
|
package/lib/options.js
CHANGED
|
@@ -77,7 +77,6 @@ const parseNum = (val) => {
|
|
|
77
77
|
* The module currently houses all new common config settings.
|
|
78
78
|
*
|
|
79
79
|
* Other settings include:
|
|
80
|
-
* - env: environment variable to check for value in
|
|
81
80
|
* - fn: a function to run on the original value (eg type coercion or sanitizing). returns undefined if it can't do anything with the value it is given.
|
|
82
81
|
* - enum: validation of whether type matches enumerated value
|
|
83
82
|
*
|
|
@@ -90,15 +89,6 @@ const parseNum = (val) => {
|
|
|
90
89
|
* @type {import('.').ConfigOption[]}
|
|
91
90
|
*/
|
|
92
91
|
const options = [
|
|
93
|
-
// config
|
|
94
|
-
{
|
|
95
|
-
name: 'configFile',
|
|
96
|
-
abbrev: 'c',
|
|
97
|
-
// special case this guy because it should be settable via ENV
|
|
98
|
-
env: 'CONTRAST_CONFIG_PATH',
|
|
99
|
-
arg: '<path>',
|
|
100
|
-
desc: 'set config file location. defaults to <app_root>/contrast_security.yaml',
|
|
101
|
-
},
|
|
102
92
|
// api
|
|
103
93
|
{
|
|
104
94
|
name: 'api.enable',
|
|
@@ -109,7 +99,6 @@ const options = [
|
|
|
109
99
|
},
|
|
110
100
|
{
|
|
111
101
|
name: 'api.url',
|
|
112
|
-
env: 'CONTRASTSECURITY_URL',
|
|
113
102
|
arg: '<url>',
|
|
114
103
|
default: 'https://app.contrastsecurity.com/Contrast',
|
|
115
104
|
// The old json spec used to expect that the url would not end in /Contrast
|
|
@@ -146,19 +135,16 @@ const options = [
|
|
|
146
135
|
},
|
|
147
136
|
{
|
|
148
137
|
name: 'api.api_key',
|
|
149
|
-
env: 'CONTRASTSECURITY_API_KEY',
|
|
150
138
|
arg: '<key>',
|
|
151
139
|
desc: 'Set the API key needed to communicate with the Contrast UI.',
|
|
152
140
|
},
|
|
153
141
|
{
|
|
154
142
|
name: 'api.service_key',
|
|
155
|
-
env: 'CONTRASTSECURITY_SECRET_KEY',
|
|
156
143
|
arg: '<key>',
|
|
157
144
|
desc: 'Set the service key needed to communicate with the Contrast UI. It is used to calculate the Authorization header.',
|
|
158
145
|
},
|
|
159
146
|
{
|
|
160
147
|
name: 'api.user_name',
|
|
161
|
-
env: 'CONTRASTSECURITY_UID',
|
|
162
148
|
arg: '<name>',
|
|
163
149
|
desc: 'Set the user name used to communicate with the Contrast UI. It is used to calculate the Authorization header.',
|
|
164
150
|
},
|
|
@@ -270,7 +256,7 @@ Example - \`/opt/Contrast/contrast.log\` creates a log in the \`/opt/Contrast\`
|
|
|
270
256
|
name: 'agent.logger.level',
|
|
271
257
|
arg: '<level>',
|
|
272
258
|
enum: ['error', 'warn', 'info', 'debug', 'trace'],
|
|
273
|
-
|
|
259
|
+
default: 'info',
|
|
274
260
|
fn: lowercase,
|
|
275
261
|
desc: 'Set the the log output level. Valid options are `ERROR`, `WARN`, `INFO`, `DEBUG`, and `TRACE`.',
|
|
276
262
|
},
|
|
@@ -503,13 +489,13 @@ Example - \`label1, label2, label3\``,
|
|
|
503
489
|
.map((ruleId) => ({
|
|
504
490
|
name: `protect.rules.${ruleId}.mode`,
|
|
505
491
|
arg: '<mode>',
|
|
492
|
+
default: 'off',
|
|
506
493
|
enum: ['monitor', 'block', 'block_at_perimeter', 'off'],
|
|
507
494
|
desc: 'Set the mode of the rule. Value options are `monitor`, `block`, `block_at_perimeter`, or `off`.',
|
|
508
495
|
})),
|
|
509
496
|
// application
|
|
510
497
|
{
|
|
511
498
|
name: 'application.name',
|
|
512
|
-
env: 'CONTRASTSECURITY_APP_NAME',
|
|
513
499
|
arg: '<name>',
|
|
514
500
|
desc: "Override the reported application name. Defaults to the `name` field from an application's `package.json`",
|
|
515
501
|
},
|
|
@@ -563,8 +549,11 @@ Example - \`label1, label2, label3\``,
|
|
|
563
549
|
arg: '<version>',
|
|
564
550
|
desc: "override the reported server version (if different from 'version' field in the application's package.json)",
|
|
565
551
|
},
|
|
566
|
-
]
|
|
552
|
+
].map((opt) => Object.assign(opt, {
|
|
553
|
+
env: `CONTRAST__${opt.name.toUpperCase().replace(/\./g, '__')
|
|
554
|
+
.replace(/-/g, '_')}`
|
|
555
|
+
}));
|
|
567
556
|
|
|
568
|
-
module.exports
|
|
557
|
+
module.exports = options;
|
|
569
558
|
module.exports.clearBaseCase = clearBaseCase;
|
|
570
559
|
module.exports.castBoolean = castBoolean;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contrast/config",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.18.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.15.0",
|
|
21
21
|
"yaml": "^2.2.2"
|
|
22
22
|
}
|
|
23
|
-
}
|
|
23
|
+
}
|
package/lib/util.js
DELETED
|
@@ -1,288 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Copyright: 2023 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 process = require('process');
|
|
19
|
-
const path = require('path');
|
|
20
|
-
const fs = require('fs');
|
|
21
|
-
const os = require('os');
|
|
22
|
-
const yaml = require('yaml');
|
|
23
|
-
|
|
24
|
-
const { set } = require('@contrast/common');
|
|
25
|
-
const { configOptions } = require('./options');
|
|
26
|
-
const util = module.exports;
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Sets initial config values to the config.
|
|
30
|
-
*
|
|
31
|
-
* @param {Config} conf the config. mutates.
|
|
32
|
-
* @param {string} name name of the option, eg 'agent.logger.stdout'
|
|
33
|
-
* @param {*} value
|
|
34
|
-
* @param {boolean} def set from default or not
|
|
35
|
-
*/
|
|
36
|
-
function setConfig(conf, name, value, def, origin) {
|
|
37
|
-
set(conf, name, value);
|
|
38
|
-
conf._default[name] = def;
|
|
39
|
-
conf._flat[name] = value;
|
|
40
|
-
conf._sources[name] = origin;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
class ConfigurationError extends Error {
|
|
44
|
-
constructor(message) {
|
|
45
|
-
super(`Configuration Error: ${message}`);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
class Config {
|
|
50
|
-
constructor() {
|
|
51
|
-
Object.assign(this, {
|
|
52
|
-
_default: {},
|
|
53
|
-
_flat: {},
|
|
54
|
-
_sources: {},
|
|
55
|
-
api: {},
|
|
56
|
-
agent: {
|
|
57
|
-
diagnostics: {},
|
|
58
|
-
reporters: {},
|
|
59
|
-
security_logger: {},
|
|
60
|
-
logger: {},
|
|
61
|
-
node: {},
|
|
62
|
-
},
|
|
63
|
-
inventory: {},
|
|
64
|
-
assess: {},
|
|
65
|
-
protect: {
|
|
66
|
-
rules: {},
|
|
67
|
-
},
|
|
68
|
-
application: {},
|
|
69
|
-
server: {},
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Override the setting, if set from its default.
|
|
75
|
-
*
|
|
76
|
-
* @param {string} name name of the option, eg 'agent.logger.stdout'
|
|
77
|
-
* @param {*} value
|
|
78
|
-
*/
|
|
79
|
-
override(name, value) {
|
|
80
|
-
if (this._default[name]) {
|
|
81
|
-
setConfig(this, name, value, true);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
get(name) {
|
|
86
|
-
return name.split('.').reduce((obj, prop) => obj?.[prop], this);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Custom validation logic.
|
|
91
|
-
* @throws {Error}
|
|
92
|
-
*/
|
|
93
|
-
validate() {
|
|
94
|
-
// Mutually exclusive options:
|
|
95
|
-
if (
|
|
96
|
-
this.get('application.session_id') &&
|
|
97
|
-
this.get('application.session_metadata')
|
|
98
|
-
) {
|
|
99
|
-
throw new ConfigurationError(
|
|
100
|
-
'Cannot set both `application.session_id` and `application.session_metadata`'
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Find location of config given options and name of config file
|
|
108
|
-
* @return {string|void} path, if valid
|
|
109
|
-
*/
|
|
110
|
-
function checkConfigPath() {
|
|
111
|
-
const configDir =
|
|
112
|
-
os.platform() === 'win32'
|
|
113
|
-
? `${process.env['ProgramData']}\\contrast`
|
|
114
|
-
: '/etc/contrast';
|
|
115
|
-
const configSubDir = `${configDir}${path.sep}node`;
|
|
116
|
-
|
|
117
|
-
for (const dir of [process.cwd(), configSubDir, configDir]) {
|
|
118
|
-
const checkPath = path.resolve(dir, 'contrast_security.yaml');
|
|
119
|
-
if (fs.existsSync(checkPath)) {
|
|
120
|
-
return checkPath;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Read config into object
|
|
128
|
-
* @return {Object} configuration
|
|
129
|
-
*/
|
|
130
|
-
function readConfig() {
|
|
131
|
-
let config = {};
|
|
132
|
-
let fileContents;
|
|
133
|
-
const configPath = process.env['CONTRAST_CONFIG_PATH'] || checkConfigPath();
|
|
134
|
-
|
|
135
|
-
if (configPath) {
|
|
136
|
-
try {
|
|
137
|
-
fileContents = fs.readFileSync(configPath, 'utf-8');
|
|
138
|
-
} catch (e) {
|
|
139
|
-
console.error(
|
|
140
|
-
'Unable to read config file at %s: %s',
|
|
141
|
-
configPath,
|
|
142
|
-
e.message
|
|
143
|
-
);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
if (fileContents) {
|
|
148
|
-
try {
|
|
149
|
-
config = yaml.parse(fileContents, {
|
|
150
|
-
prettyErrors: true,
|
|
151
|
-
});
|
|
152
|
-
} catch (e) {
|
|
153
|
-
console.error(
|
|
154
|
-
'YAML validator found an error in %s: %s',
|
|
155
|
-
configPath,
|
|
156
|
-
e.message
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
return config;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* We want to "un-flatten" the config, in the
|
|
166
|
-
* event that there are any dot-delimited keys remaining.
|
|
167
|
-
* FIXME: when we get rid of JSON, don't do this anymore
|
|
168
|
-
* @return {Object} "un-flattened" file options
|
|
169
|
-
*/
|
|
170
|
-
function getFileOptions() {
|
|
171
|
-
const config = readConfig();
|
|
172
|
-
return Object.values(config).reduce((memo, value, idx) => {
|
|
173
|
-
const key = Object.keys(config)[idx];
|
|
174
|
-
// Merge if necessary,
|
|
175
|
-
if (memo[key]) {
|
|
176
|
-
Object.assign(memo[key], value);
|
|
177
|
-
} else {
|
|
178
|
-
// but otherwise set the config path.
|
|
179
|
-
set(memo, key, value);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
return memo;
|
|
183
|
-
}, {});
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* only set autoEnv if it's a common config option
|
|
188
|
-
* @param {string} name
|
|
189
|
-
*/
|
|
190
|
-
function getAutoEnv(name) {
|
|
191
|
-
// fix rule names too e.g. sql-injecxtion -> SQL_INJECTION
|
|
192
|
-
const envName = name.toUpperCase().replace(/\./g, '__')
|
|
193
|
-
.replace(/-/g, '_');
|
|
194
|
-
return name === 'configFile'
|
|
195
|
-
? undefined
|
|
196
|
-
: process.env[`CONTRAST__${envName}`] || process.env[envName];
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* When you run an application with pm2 on cluster mode, pm2 attaches the
|
|
201
|
-
* process.env to a property pm2_env(only for the agent since we start it with -r flag), so
|
|
202
|
-
* the function checks if there is a pm2_env property and merge contrast
|
|
203
|
-
* related properties to process.env
|
|
204
|
-
*/
|
|
205
|
-
function mergePM2Envs() {
|
|
206
|
-
if (!process.env.pm2_env) return;
|
|
207
|
-
|
|
208
|
-
const pm2_env = JSON.parse(process.env.pm2_env);
|
|
209
|
-
|
|
210
|
-
const objectEntries = Object.entries(pm2_env.env).concat(Object.entries(pm2_env));
|
|
211
|
-
|
|
212
|
-
const cfgPath = 'CONTRAST_CONFIG_PATH';
|
|
213
|
-
const pm2CfgPath = pm2_env.env[cfgPath] || pm2_env[cfgPath];
|
|
214
|
-
if (pm2CfgPath) process.env[cfgPath] = pm2CfgPath;
|
|
215
|
-
|
|
216
|
-
objectEntries.forEach(([key, value]) => {
|
|
217
|
-
if (
|
|
218
|
-
!process.env[key] && key.toLocaleLowerCase().includes('contrast')
|
|
219
|
-
) {
|
|
220
|
-
process.env[key] = value;
|
|
221
|
-
}
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* @return {Config} merged options
|
|
227
|
-
*/
|
|
228
|
-
function mergeOptions() {
|
|
229
|
-
const fileOptions = getFileOptions();
|
|
230
|
-
|
|
231
|
-
return configOptions.reduce((options, option) => {
|
|
232
|
-
const {
|
|
233
|
-
default: optDefault,
|
|
234
|
-
enum: optEnum,
|
|
235
|
-
fn = (arg) => arg,
|
|
236
|
-
name,
|
|
237
|
-
} = option;
|
|
238
|
-
|
|
239
|
-
const env = process.env[option.env];
|
|
240
|
-
const autoEnv = getAutoEnv(name);
|
|
241
|
-
const fileFlag = name
|
|
242
|
-
.split('.')
|
|
243
|
-
.reduce((obj, prop) => obj?.[prop], fileOptions);
|
|
244
|
-
|
|
245
|
-
// For some values, we want to know if we assigned by falling back to default
|
|
246
|
-
let isFromDefault, origin;
|
|
247
|
-
|
|
248
|
-
if (env != null || autoEnv != null) {
|
|
249
|
-
origin = 'ENVIRONMENT_VARIABLE';
|
|
250
|
-
} else if (fileFlag != null) {
|
|
251
|
-
origin = 'USER_CONFIGURATION_FILE';
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// env > file > default
|
|
255
|
-
let value = [env, autoEnv, fileFlag]
|
|
256
|
-
.map((v) => fn(v)).find((flag) => flag !== undefined);
|
|
257
|
-
|
|
258
|
-
// if it's an enum, find it in the enum or set the value to default
|
|
259
|
-
// ineffective if optDefault wasn't in the enum;
|
|
260
|
-
// optDefault won't get passed through fn, so it needs to be valid.
|
|
261
|
-
if (optEnum && optEnum.indexOf(value) === -1) {
|
|
262
|
-
value = fn(optDefault);
|
|
263
|
-
isFromDefault = true;
|
|
264
|
-
origin = 'DEFAULT_VALUE';
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// set default last and separately, so that we can mark that the option was
|
|
268
|
-
// set from default
|
|
269
|
-
if (value === undefined) {
|
|
270
|
-
value = fn(optDefault);
|
|
271
|
-
isFromDefault = true;
|
|
272
|
-
origin = 'DEFAULT_VALUE';
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
setConfig(options, name, value, isFromDefault, origin);
|
|
276
|
-
return options;
|
|
277
|
-
}, new Config());
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
util.Config = Config;
|
|
281
|
-
util.setup = function setup() {
|
|
282
|
-
mergePM2Envs();
|
|
283
|
-
const mergedOptions = mergeOptions();
|
|
284
|
-
mergedOptions.validate();
|
|
285
|
-
return mergedOptions;
|
|
286
|
-
};
|
|
287
|
-
// We want to use the set method used here for creating a correct mock object for tests
|
|
288
|
-
util.setValue = set;
|