@contrast/config 1.17.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 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
- configFile: string;
48
- _default: Record<string, any>;
49
- _flat: Record<string, any>;
50
- _sources: Record<string, 'DEFAULT_VALUE' | 'ENVIRONMENT_VARIABLE' | 'USER_CONFIGURATION_FILE' | 'CONTRAST_UI'>;
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 configUtil = require('./util');
18
+ const Config = require('./config');
19
+ const { ConfigSource } = require('./common');
19
20
 
20
- module.exports = function init(core = {}) {
21
- return core.config = configUtil.setup();
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
- // default: 'info', // this has no default at the config level but is instead handled by `@contrast/logger` to account for TS settings.
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.configOptions = options;
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.17.0",
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.14.0",
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).toString('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.setConfig = setConfig;