@contrast/config 1.0.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/LICENSE +12 -0
- package/README.md +44 -0
- package/lib/index.d.ts +122 -0
- package/lib/index.js +7 -0
- package/lib/index.test.js +331 -0
- package/lib/options.js +335 -0
- package/lib/util.js +237 -0
- package/package.json +23 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Copyright: 2022 Contrast Security, Inc
|
|
2
|
+
Contact: support@contrastsecurity.com
|
|
3
|
+
License: Commercial
|
|
4
|
+
|
|
5
|
+
NOTICE: This Software and the patented inventions embodied within may only be
|
|
6
|
+
used as part of Contrast Security’s commercial offerings. Even though it is
|
|
7
|
+
made available through public repositories, use of this Software is subject to
|
|
8
|
+
the applicable End User Licensing Agreement found at
|
|
9
|
+
https://www.contrastsecurity.com/enduser-terms-0317a or as otherwise agreed
|
|
10
|
+
between Contrast Security and the End User. The Software may not be reverse
|
|
11
|
+
engineered, modified, repackaged, sold, redistributed or otherwise used in a
|
|
12
|
+
way not consistent with the End User License Agreement.
|
package/README.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# `@contrast/config`
|
|
2
|
+
|
|
3
|
+
<br>
|
|
4
|
+
|
|
5
|
+
> Note: This package needs help.
|
|
6
|
+
> * Needlessly dependent on `commander`, `lodash`, and `json-stable-stringify`
|
|
7
|
+
> * Can be simplified
|
|
8
|
+
> * Could benefit from schema-based approach for defaults
|
|
9
|
+
|
|
10
|
+
<br>
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
|
|
14
|
+
This is legacy code ported from `node-agent` repo.
|
|
15
|
+
|
|
16
|
+
To discover and log configuration data, try
|
|
17
|
+
|
|
18
|
+
```shell
|
|
19
|
+
node -e "console.log(new (require('.').Config)())"
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
An agent should use a single instance of a config. On instantiation, the config will detect both yaml file and environment variable sources and build out full config object. The object will have defaults set for values not having been set by file or env vars.
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
const { AgentConfig } = require('@contrast/config');
|
|
28
|
+
const config = new AgentConfig();
|
|
29
|
+
|
|
30
|
+
// do stuff with config
|
|
31
|
+
if (config.protect.enable) {
|
|
32
|
+
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## New V5 Options
|
|
37
|
+
|
|
38
|
+
- `agent.stack_trace_filters`
|
|
39
|
+
|
|
40
|
+
This allows agent stackframes to be filtered via configuration
|
|
41
|
+
Default: `agent-,@contrast,node-agent`
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { RulesConfig } from '@contrast/common';
|
|
2
|
+
import { Level } from 'pino';
|
|
3
|
+
|
|
4
|
+
export interface Config {
|
|
5
|
+
configFile: string;
|
|
6
|
+
|
|
7
|
+
api: {
|
|
8
|
+
enable: boolean;
|
|
9
|
+
api_key: string;
|
|
10
|
+
service_key: string;
|
|
11
|
+
|
|
12
|
+
/** Default: `'https://app.contrastsecurity.com/Contrast'` */
|
|
13
|
+
url: string;
|
|
14
|
+
|
|
15
|
+
user_name: string;
|
|
16
|
+
proxy: {
|
|
17
|
+
enable: boolean;
|
|
18
|
+
url: string;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
agent: {
|
|
23
|
+
polling: {
|
|
24
|
+
app_activity_ms: number;
|
|
25
|
+
},
|
|
26
|
+
reporters: {
|
|
27
|
+
/** Path indicating where to report all agent findings. */
|
|
28
|
+
file?: string | number;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
logger: {
|
|
32
|
+
/**
|
|
33
|
+
* When false, create a new log file on startup instead of appending and
|
|
34
|
+
* rolling daily. Default: `true`
|
|
35
|
+
*/
|
|
36
|
+
append: boolean;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Minimum log level. 'silent' disables logging entirely.
|
|
40
|
+
* Default: `'error'`
|
|
41
|
+
*/
|
|
42
|
+
level: Level | 'silent';
|
|
43
|
+
|
|
44
|
+
/** Default: `'node-contrast'` */
|
|
45
|
+
path: string;
|
|
46
|
+
|
|
47
|
+
/** Suppress output when `false`. Default: `true` */
|
|
48
|
+
stdout: boolean;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
node: {
|
|
52
|
+
/** Default: `true` */
|
|
53
|
+
enable_rewrite: boolean;
|
|
54
|
+
|
|
55
|
+
/** Default: `true` */
|
|
56
|
+
enable_source_maps: boolean;
|
|
57
|
+
|
|
58
|
+
/** Location to look for the app's package.json. Default: `process.cwd()` */
|
|
59
|
+
app_root: string;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Limit for stack trace size (larger limits will improve accuracy but
|
|
64
|
+
* increase memory usage). Default: `10`
|
|
65
|
+
*/
|
|
66
|
+
stack_trace_limit: number;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* List of patterns to ignore within stack traces.
|
|
70
|
+
* Default: `['agent', '@contrast', 'node-agent']
|
|
71
|
+
*/
|
|
72
|
+
stack_trace_filters: string[];
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
application: {
|
|
76
|
+
/** override the reported application name. */
|
|
77
|
+
name?: string;
|
|
78
|
+
|
|
79
|
+
/** override the reported application path. Default: `'/'` */
|
|
80
|
+
path: string;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Override the reported application version (if different from 'version'
|
|
84
|
+
* field in the application's package.json).
|
|
85
|
+
*/
|
|
86
|
+
version?: string;
|
|
87
|
+
|
|
88
|
+
/** Provide the ID of a session existing within Contrast UI. */
|
|
89
|
+
session_id: string | null;
|
|
90
|
+
|
|
91
|
+
/** Provide metadata used to create a new session within Contrast UI/ */
|
|
92
|
+
session_metadtata: string | null;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
protect: {
|
|
96
|
+
enable: boolean;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* List of rule ids to disable.
|
|
100
|
+
* Default: `[]`
|
|
101
|
+
*/
|
|
102
|
+
disabled_rules: string[];
|
|
103
|
+
|
|
104
|
+
rules: RulesConfig;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/** Reported server information overrides */
|
|
108
|
+
server: {
|
|
109
|
+
environment?: string;
|
|
110
|
+
/** Default: `os.hostname()` */
|
|
111
|
+
name: string;
|
|
112
|
+
version?: string;
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
interface Core {
|
|
117
|
+
config: Config;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
declare function init(core: Core): Config;
|
|
121
|
+
|
|
122
|
+
export = init;
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const sinon = require('sinon');
|
|
4
|
+
const mockfs = require('mock-fs');
|
|
5
|
+
const { expect } = require('chai');
|
|
6
|
+
const config = require('.');
|
|
7
|
+
const configOptions = require('./options');
|
|
8
|
+
const mocks = require('../../test/mocks');
|
|
9
|
+
const AppInfo = require('../../core/lib/app-info');
|
|
10
|
+
|
|
11
|
+
const {
|
|
12
|
+
getGoodConfig,
|
|
13
|
+
getBadConfig,
|
|
14
|
+
getAbsolutePath,
|
|
15
|
+
getDefaultConfig,
|
|
16
|
+
getProperties,
|
|
17
|
+
containsAllProps,
|
|
18
|
+
} = require('../test/helpers');
|
|
19
|
+
|
|
20
|
+
describe('config', function () {
|
|
21
|
+
beforeEach(function () {
|
|
22
|
+
|
|
23
|
+
// removing as some agent engineers have this env var set
|
|
24
|
+
delete process.env['CONTRAST_CONFIG_PATH'];
|
|
25
|
+
sinon.stub(console, 'error');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterEach(function () {
|
|
29
|
+
mockfs.restore();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('default config behavior', function () {
|
|
33
|
+
it('sets all default properties in the config', function () {
|
|
34
|
+
const defaultConfigProps = getProperties(getDefaultConfig());
|
|
35
|
+
const loadConfigProps = getProperties(config());
|
|
36
|
+
expect(containsAllProps(loadConfigProps, defaultConfigProps)).to.be.true;
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('logs an error a config file path is given and no config is found', function () {
|
|
40
|
+
process.env['CONTRAST_CONFIG_PATH'] = 'fake config';
|
|
41
|
+
config();
|
|
42
|
+
expect(console.error).to.have.been.calledWith(
|
|
43
|
+
'Unable to read config file at %s: %s',
|
|
44
|
+
'fake config',
|
|
45
|
+
sinon.match('ENOENT'),
|
|
46
|
+
);
|
|
47
|
+
delete process.env['CONTRAST_CONFIG_PATH'];
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('logs an error when the config cannot be parsed', function () {
|
|
51
|
+
mockfs({
|
|
52
|
+
'contrast_security.yaml': mockfs.load(getBadConfig()),
|
|
53
|
+
});
|
|
54
|
+
config();
|
|
55
|
+
expect(console.error).to.have.been.calledWith(
|
|
56
|
+
'YAML validator found an error in %s: %s',
|
|
57
|
+
sinon.match(/contrast_security.yaml$/),
|
|
58
|
+
sinon.match(/^Tabs are not allowed as indentation/),
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('accepts overrides from env', function () {
|
|
63
|
+
process.env['CONTRAST__API__API_KEY'] = 'demo';
|
|
64
|
+
const cfg = config();
|
|
65
|
+
expect(cfg.api.api_key).to.be.equal('demo');
|
|
66
|
+
delete process.env['CONTRAST__API__API_KEY'];
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('loads and sets properties from a valid config', function () {
|
|
70
|
+
mockfs({
|
|
71
|
+
'contrast_security.yaml': mockfs.load(getGoodConfig()),
|
|
72
|
+
});
|
|
73
|
+
const cfg = config();
|
|
74
|
+
expect(console.error).to.not.have.been.called;
|
|
75
|
+
expect(cfg.api.enable).to.be.true;
|
|
76
|
+
expect(cfg.api.url).to.be.equal('http://localhost:19080/Contrast');
|
|
77
|
+
expect(cfg.api.api_key).to.be.equal('demo');
|
|
78
|
+
expect(cfg.api.service_key).to.be.equal('demo');
|
|
79
|
+
expect(cfg.api.user_name).to.be.equal('contrast_admin');
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('config file path precedence', function () {
|
|
84
|
+
it('should set config from environment variable even with one in cwd', function () {
|
|
85
|
+
mockfs({
|
|
86
|
+
'contrast_security.yaml': mockfs.load(getGoodConfig()),
|
|
87
|
+
'/otherDir/config/contrast_security.yaml': mockfs.load(
|
|
88
|
+
getGoodConfig('2')
|
|
89
|
+
),
|
|
90
|
+
});
|
|
91
|
+
process.env['CONTRAST_CONFIG_PATH'] =
|
|
92
|
+
'/otherDir/config/contrast_security.yaml';
|
|
93
|
+
const cfg = config();
|
|
94
|
+
expect(cfg.api.url).to.be.equal('https://localhost:9999/Contrast');
|
|
95
|
+
expect(cfg.api.api_key).to.be.equal('QWERTYUIOP');
|
|
96
|
+
expect(cfg.api.service_key).to.be.equal('ASDFGHJKL');
|
|
97
|
+
expect(cfg.api.user_name).to.be.equal('contrast_user');
|
|
98
|
+
delete process.env['CONTRAST_CONFIG_PATH'];
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should set config from etc/contrast if envVar not set and no config in cwd', function () {
|
|
102
|
+
mockfs({
|
|
103
|
+
'/etc/contrast/contrast_security.yaml': mockfs.load(getGoodConfig()),
|
|
104
|
+
});
|
|
105
|
+
const cfg = config();
|
|
106
|
+
expect(cfg.api.url).to.be.equal('http://localhost:19080/Contrast');
|
|
107
|
+
expect(cfg.api.api_key).to.be.equal('demo');
|
|
108
|
+
expect(cfg.api.service_key).to.be.equal('demo');
|
|
109
|
+
expect(cfg.api.user_name).to.be.equal('contrast_admin');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should set config from cwd even when present in etc/contrast', function () {
|
|
113
|
+
mockfs({
|
|
114
|
+
'/etc/contrast/contrast_security.yaml': mockfs.load(getGoodConfig()),
|
|
115
|
+
'contrast_security.yaml': mockfs.load(getGoodConfig('2')),
|
|
116
|
+
});
|
|
117
|
+
const cfg = config();
|
|
118
|
+
expect(cfg.api.url).to.be.equal('https://localhost:9999/Contrast');
|
|
119
|
+
expect(cfg.api.api_key).to.be.equal('QWERTYUIOP');
|
|
120
|
+
expect(cfg.api.service_key).to.be.equal('ASDFGHJKL');
|
|
121
|
+
expect(cfg.api.user_name).to.be.equal('contrast_user');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should set from env variable even when present in cwd and etc/contrast', function () {
|
|
125
|
+
mockfs({
|
|
126
|
+
'/etc/contrast/contrast_security.yaml': mockfs.load(getGoodConfig()),
|
|
127
|
+
'contrast_security.yaml': mockfs.load(getGoodConfig('2')),
|
|
128
|
+
'/otherDir/config/contrast_security.yaml': mockfs.load(
|
|
129
|
+
getGoodConfig('3')
|
|
130
|
+
),
|
|
131
|
+
});
|
|
132
|
+
process.env['CONTRAST_CONFIG_PATH'] =
|
|
133
|
+
'/otherDir/config/contrast_security.yaml';
|
|
134
|
+
const cfg = config();
|
|
135
|
+
expect(cfg.api.url).to.be.equal('https://localhost:8080/Contrast');
|
|
136
|
+
expect(cfg.api.api_key).to.be.equal('QAZWSX');
|
|
137
|
+
expect(cfg.api.service_key).to.be.equal('EDCRFV');
|
|
138
|
+
expect(cfg.api.user_name).to.be.equal('super_admin');
|
|
139
|
+
delete process.env['CONTRAST_CONFIG_PATH'];
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe('config value precedence', function () {
|
|
144
|
+
let options;
|
|
145
|
+
|
|
146
|
+
beforeEach(function () {
|
|
147
|
+
options = getDefaultConfig();
|
|
148
|
+
delete process.env.CONTRAST__API__API_KEY;
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should use env var before config file value', function () {
|
|
152
|
+
process.env.CONTRAST__API__API_KEY = 'NOPE';
|
|
153
|
+
const cfg = config(options);
|
|
154
|
+
expect(cfg.api.api_key).to.be.equal('NOPE');
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe('environment variables', function () {
|
|
159
|
+
it('should support CONTRAST__ options', function () {
|
|
160
|
+
const options = getDefaultConfig();
|
|
161
|
+
process.env['CONTRAST__AGENT__NODE__ENABLE_REWRITE'] = 'f';
|
|
162
|
+
const cfg = config(options);
|
|
163
|
+
|
|
164
|
+
expect(cfg.agent.node.enable_rewrite).to.be.equal(false);
|
|
165
|
+
delete process.env['CONTRAST__AGENT__NODE__ENABLE_REWRITE'];
|
|
166
|
+
});
|
|
167
|
+
it('Should support CONTRAST__API__ options', function () {
|
|
168
|
+
const options = getDefaultConfig();
|
|
169
|
+
process.env['CONTRAST__API__API_KEY'] = 'abcdefg';
|
|
170
|
+
const cfg = config(options);
|
|
171
|
+
|
|
172
|
+
expect(cfg.api.api_key).to.equal('abcdefg');
|
|
173
|
+
delete process.env['CONTRAST__API__API_KEY'];
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe('config options functions', function () {
|
|
178
|
+
describe('clearBaseCase', function () {
|
|
179
|
+
it('should return string if defined', function () {
|
|
180
|
+
const string = 'asdf';
|
|
181
|
+
expect(configOptions.clearBaseCase(string)).to.equal(string);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('should return undefined if string is empty', function () {
|
|
185
|
+
const string = '';
|
|
186
|
+
expect(configOptions.clearBaseCase(string)).to.be.undefined;
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should return int if and number and defined', function () {
|
|
190
|
+
const num = 1;
|
|
191
|
+
expect(configOptions.clearBaseCase(num)).to.equal(num);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('should return undefined if value is NaN', function () {
|
|
195
|
+
const num = NaN;
|
|
196
|
+
expect(configOptions.clearBaseCase(num)).to.be.undefined;
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
describe('castBoolean', function () {
|
|
201
|
+
[true, 'true', 'TRUE', 't', 'T'].forEach(val => {
|
|
202
|
+
it(`should return true if string value is ${val}`, function () {
|
|
203
|
+
expect(configOptions.castBoolean(val)).to.equal(true);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
[false, 'false', 'FALSE', 'f', 'F'].forEach(val => {
|
|
208
|
+
it(`should return false if string value is ${val}`, function () {
|
|
209
|
+
expect(configOptions.castBoolean(val)).to.equal(false);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
['rando', [1, 2, 3], {}, 100, null, undefined].forEach(val => {
|
|
214
|
+
it(`should return undefined if ${val} not a boolean or string or not true/t/false/f`, function () {
|
|
215
|
+
expect(configOptions.castBoolean(val)).to.equal(undefined);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
describe('enum', function () {
|
|
222
|
+
let logger_level;
|
|
223
|
+
|
|
224
|
+
beforeEach(function () {
|
|
225
|
+
logger_level = 'CONTRAST__AGENT__LOGGER__LEVEL';
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
afterEach(function () {
|
|
229
|
+
delete process.env[logger_level];
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('should use value from enum if there is a match', function () {
|
|
233
|
+
process.env[logger_level] = 'info';
|
|
234
|
+
const cfg = config();
|
|
235
|
+
expect(cfg.agent.logger.level).to.be.equal('info');
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('should use default when there is no match within enum', function () {
|
|
239
|
+
process.env[logger_level] = 'doggo';
|
|
240
|
+
const cfg = config();
|
|
241
|
+
expect(cfg.agent.logger.level).to.be.equal('error');
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
describe('uppercase, lowercase, parsenum transformations', function () {
|
|
246
|
+
it('should uppercase server.environment', function () {
|
|
247
|
+
process.env['CONTRAST__SERVER__ENVIRONMENT'] = 'qa';
|
|
248
|
+
const cfg = config();
|
|
249
|
+
expect(cfg.server.environment).to.equal('QA');
|
|
250
|
+
delete process.env['CONTRAST__SERVER__ENVIRONMENT'];
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('should lowercase logger level', function () {
|
|
254
|
+
process.env['CONTRAST__AGENT__LOGGER__LEVEL'] = 'DEBUG';
|
|
255
|
+
const cfg = config();
|
|
256
|
+
expect(cfg.agent.logger.level).to.equal('debug');
|
|
257
|
+
delete process.env['CONTRAST__AGENT__LOGGER__LEVEL'];
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('should parse stack_trace_limit into a number', function () {
|
|
261
|
+
process.env['CONTRAST__AGENT__STACK_TRACE_LIMIT'] = '25';
|
|
262
|
+
const cfg = config();
|
|
263
|
+
expect(cfg.agent.stack_trace_limit).to.equal(25);
|
|
264
|
+
delete process.env['CONTRAST__AGENT__STACK_TRACE_LIMIT'];
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('agent.stack_trace_limit (Infinity string --> number)', function () {
|
|
268
|
+
process.env['CONTRAST__AGENT__STACK_TRACE_LIMIT'] = 'Infinity';
|
|
269
|
+
const cfg = config();
|
|
270
|
+
expect(cfg.agent.stack_trace_limit).to.be.equal(Infinity);
|
|
271
|
+
delete process.env['CONTRAST__AGENT__STACK_TRACE_LIMIT'];
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
describe('application', function () {
|
|
276
|
+
describe('validation', function () {
|
|
277
|
+
afterEach(function () {
|
|
278
|
+
delete process.env['CONTRAST__APPLICATION__SESSION_ID'];
|
|
279
|
+
delete process.env['CONTRAST__APPLICATION__SESSION_METADATA'];
|
|
280
|
+
});
|
|
281
|
+
it('allows one to be set without error', function () {
|
|
282
|
+
process.env['CONTRAST__APPLICATION__SESSION_ID'] = 'abcd-1234';
|
|
283
|
+
expect(() => {
|
|
284
|
+
config();
|
|
285
|
+
}).to.not.throw(/Configuration Error:/);
|
|
286
|
+
});
|
|
287
|
+
it('session options are mutually exclusive', function () {
|
|
288
|
+
process.env['CONTRAST__APPLICATION__SESSION_ID'] = 'abcd-1234';
|
|
289
|
+
process.env['CONTRAST__APPLICATION__SESSION_METADATA'] = 'a=1,b=2';
|
|
290
|
+
expect(() => {
|
|
291
|
+
config();
|
|
292
|
+
}).to.throw(/Configuration Error:/);
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
describe('server', function () {
|
|
298
|
+
describe('environment', function () {
|
|
299
|
+
it('set from env var', function () {
|
|
300
|
+
const env = Date.now();
|
|
301
|
+
const options = getDefaultConfig();
|
|
302
|
+
process.env.SERVER__ENVIRONMENT = env;
|
|
303
|
+
|
|
304
|
+
const cfg = config(options);
|
|
305
|
+
const ai = new AppInfo({ appInfo: 'asdf', config: cfg, logger: mocks.logger() });
|
|
306
|
+
expect(ai.serverEnvironment).to.be.equal(String(env));
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
describe('mapping', function () {
|
|
312
|
+
describe('logger', function () {
|
|
313
|
+
it('casts logger path to absolute path', function () {
|
|
314
|
+
process.env['CONTRAST__AGENT__LOGGER__PATH'] = 'loggy.log';
|
|
315
|
+
const cfg = config();
|
|
316
|
+
expect(cfg.agent.logger.path).to.be.equal(getAbsolutePath('loggy.log'));
|
|
317
|
+
delete process.env['CONTRAST__AGENT__LOGGER__PATH'];
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('--debug does not overwrite explicitly set log level', function () {
|
|
321
|
+
process.env['DEBUG'] = 'true';
|
|
322
|
+
process.env['CONTRAST__AGENT__LOGGER__LEVEL'] = 'info';
|
|
323
|
+
|
|
324
|
+
const cfg = config();
|
|
325
|
+
expect(cfg.agent.logger.level).to.be.equal('info');
|
|
326
|
+
delete process.env['DEBUG'];
|
|
327
|
+
delete process.env['CONTRAST__AGENT__LOGGER__LEVEL'];
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
});
|
package/lib/options.js
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sets up the agent config. All options include a name and a description.
|
|
3
|
+
* Where the setting is not a boolean, they include args as well.
|
|
4
|
+
*
|
|
5
|
+
* The module currently houses all new common config settings.
|
|
6
|
+
*
|
|
7
|
+
* Other settings include:
|
|
8
|
+
* feature: a property in the TS feature set to tie the config option to
|
|
9
|
+
* env: environment variable to check for value in
|
|
10
|
+
* fn: a function to run on the original value (eg type coercion or sanitizing).
|
|
11
|
+
* returns undefined if it can't do anything with the value it is given.
|
|
12
|
+
* enum: validation of whether type matches enumerated value
|
|
13
|
+
*
|
|
14
|
+
* NOTE: I'm not sure if validation should also be specified and handled here.
|
|
15
|
+
*
|
|
16
|
+
* TODO: add defaults to all new options
|
|
17
|
+
* TODO: add mapping for TeamServer FeatureSet analogues where they differ
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
'use strict';
|
|
21
|
+
|
|
22
|
+
const os = require('os');
|
|
23
|
+
const url = require('url');
|
|
24
|
+
const path = require('path');
|
|
25
|
+
const { Rule } = require('@contrast/common');
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Takes strings "true"|"t" or "false"|"f" (case insensitive) and return the appropriate boolean.
|
|
29
|
+
* If we can't match one of the two words, return true;
|
|
30
|
+
*
|
|
31
|
+
* @param {boolean|string} value passed arg; never undefined or the function isn't called
|
|
32
|
+
* @return {boolean}
|
|
33
|
+
*/
|
|
34
|
+
function castBoolean(value) {
|
|
35
|
+
const type = typeof value;
|
|
36
|
+
if (type !== 'string' && type !== 'boolean') {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
value = value.toString().toLowerCase();
|
|
40
|
+
return (value === 'true' || value === 't')
|
|
41
|
+
? true
|
|
42
|
+
: (value === 'false' || value === 'f')
|
|
43
|
+
? false
|
|
44
|
+
: undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Takes string path and resolves absolute path based on current working dir
|
|
49
|
+
*
|
|
50
|
+
* @param {string} value passed arg; never undefined or the function isn't called
|
|
51
|
+
* @return {string} absolute path resolve from process.cwd()
|
|
52
|
+
*/
|
|
53
|
+
function toAbsolutePath(value) {
|
|
54
|
+
return value ? path.resolve(process.cwd(), String(value)) : undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function clearBaseCase(val) {
|
|
58
|
+
const type = typeof val;
|
|
59
|
+
return (type === 'string' && val === '') || (type === 'number' && isNaN(val))
|
|
60
|
+
? undefined
|
|
61
|
+
: val;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const split = (val) => {
|
|
65
|
+
if (val === '') return [];
|
|
66
|
+
if (val === undefined) return val;
|
|
67
|
+
return val.split(',');
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const uppercase = (val = '') => clearBaseCase(`${val}`.toUpperCase());
|
|
71
|
+
const lowercase = (val = '') => clearBaseCase(`${val}`.toLowerCase());
|
|
72
|
+
const parseNum = (val) => {
|
|
73
|
+
const float = parseFloat(val);
|
|
74
|
+
return clearBaseCase(Math.ceil(float));
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const config = [
|
|
78
|
+
{
|
|
79
|
+
name: 'configFile',
|
|
80
|
+
abbrev: 'c',
|
|
81
|
+
// special case this guy because it should be settable via ENV
|
|
82
|
+
env: 'CONTRAST_CONFIG_PATH',
|
|
83
|
+
arg: '<path>',
|
|
84
|
+
desc:
|
|
85
|
+
'set config file location. defaults to <app_root>/contrast_security.yaml'
|
|
86
|
+
}
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
const api = [
|
|
90
|
+
{
|
|
91
|
+
name: 'api.enable',
|
|
92
|
+
arg: '[false]',
|
|
93
|
+
fn: castBoolean,
|
|
94
|
+
default: true,
|
|
95
|
+
desc: 'set false to disable reporting'
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
name: 'api.api_key',
|
|
99
|
+
env: 'CONTRASTSECURITY_API_KEY',
|
|
100
|
+
arg: '<key>',
|
|
101
|
+
desc: 'the organization API key'
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: 'api.service_key',
|
|
105
|
+
env: 'CONTRASTSECURITY_SECRET_KEY',
|
|
106
|
+
arg: '<key>',
|
|
107
|
+
desc: 'account service key'
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: 'api.url',
|
|
111
|
+
env: 'CONTRASTSECURITY_URL',
|
|
112
|
+
arg: '<url>',
|
|
113
|
+
default: 'https://app.contrastsecurity.com/Contrast',
|
|
114
|
+
// The old json spec used to expect that the url would not end in /Contrast
|
|
115
|
+
// Common config expects this to be there. So, we need to do a bit of massaging
|
|
116
|
+
// to make sure our reporter gets a consistent URL one way or the other.
|
|
117
|
+
fn: (value) => {
|
|
118
|
+
if (value === undefined) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
value = String(value);
|
|
123
|
+
let uri;
|
|
124
|
+
try {
|
|
125
|
+
uri = new url.URL(value);
|
|
126
|
+
} catch (e) {
|
|
127
|
+
// the url customer provided is invalid, return null and this will eventually
|
|
128
|
+
// fail when trying to talk to TS
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (uri.pathname) {
|
|
133
|
+
// kill trailling /
|
|
134
|
+
uri.pathname = uri.pathname.replace(/\/+$/, '');
|
|
135
|
+
|
|
136
|
+
if (!uri.pathname.endsWith('Contrast')) {
|
|
137
|
+
uri.pathname += 'Contrast';
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return url.format(uri);
|
|
141
|
+
}
|
|
142
|
+
return value;
|
|
143
|
+
},
|
|
144
|
+
desc: 'url to report on'
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: 'api.user_name',
|
|
148
|
+
env: 'CONTRASTSECURITY_UID',
|
|
149
|
+
arg: '<name>',
|
|
150
|
+
desc: 'account user name'
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
name: 'api.proxy.enable',
|
|
154
|
+
arg: '[true]',
|
|
155
|
+
default: false,
|
|
156
|
+
desc: 'if false, no proxy is being used for communication of data',
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
name: 'api.proxy.url',
|
|
160
|
+
arg: '<url>',
|
|
161
|
+
desc: 'url of proxy for communicating agent data',
|
|
162
|
+
},
|
|
163
|
+
];
|
|
164
|
+
|
|
165
|
+
const agent = [
|
|
166
|
+
{
|
|
167
|
+
name: 'agent.reporters.file',
|
|
168
|
+
arg: '<path>',
|
|
169
|
+
desc: 'path indicating where to report all agent findings'
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
name: 'agent.logger.append',
|
|
173
|
+
arg: '[false]',
|
|
174
|
+
fn: castBoolean,
|
|
175
|
+
default: true,
|
|
176
|
+
desc:
|
|
177
|
+
'if false, create a new log file on startup instead of appending and rolling daily'
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
name: 'agent.logger.level',
|
|
181
|
+
arg: '<level>',
|
|
182
|
+
fn: lowercase,
|
|
183
|
+
enum: ['error', 'warn', 'info', 'debug', 'trace'],
|
|
184
|
+
default: 'error',
|
|
185
|
+
desc:
|
|
186
|
+
'logging level (error, warn, info, debug, trace). overrides FeatureSet:logLevel'
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
name: 'agent.logger.path',
|
|
190
|
+
default: 'contrast.log',
|
|
191
|
+
fn: toAbsolutePath,
|
|
192
|
+
arg: '<path>',
|
|
193
|
+
desc: 'where contrast will put its debug log'
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
name: 'agent.logger.stdout',
|
|
197
|
+
arg: '[false]',
|
|
198
|
+
fn: castBoolean,
|
|
199
|
+
default: true,
|
|
200
|
+
desc: 'if false, suppress output to STDOUT'
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
name: 'agent.node.enable_rewrite',
|
|
204
|
+
arg: '[false]',
|
|
205
|
+
fn: castBoolean,
|
|
206
|
+
default: true,
|
|
207
|
+
desc: 'if false, disable source rewriting (not recommended)'
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
name: 'agent.node.enable_source_maps',
|
|
211
|
+
arg: '[false]',
|
|
212
|
+
fn: castBoolean,
|
|
213
|
+
default: true,
|
|
214
|
+
desc: 'enable source map support in reporting'
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
name: 'agent.node.app_root',
|
|
218
|
+
arg: '<path>',
|
|
219
|
+
desc: "set location to look for the app's package.json",
|
|
220
|
+
default: process.cwd()
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
name: 'agent.stack_trace_limit',
|
|
224
|
+
arg: '<limit>',
|
|
225
|
+
default: 10,
|
|
226
|
+
fn: parseNum,
|
|
227
|
+
desc:
|
|
228
|
+
'set limit for stack trace size (larger limits will improve accuracy but increase memory usage)'
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
name: 'agent.stack_trace_filters',
|
|
232
|
+
arg: '<list,of,filters>',
|
|
233
|
+
default: 'agent-,@contrast,node-agent',
|
|
234
|
+
fn: split,
|
|
235
|
+
desc: 'comma-separated list of patterns to ignore within stack traces'
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
name: 'agent.polling.app_activity_ms',
|
|
239
|
+
arg: '<ms>',
|
|
240
|
+
default: 30000,
|
|
241
|
+
fn: parseNum,
|
|
242
|
+
desc: 'how often (in ms), application activity messages are sent',
|
|
243
|
+
},
|
|
244
|
+
];
|
|
245
|
+
|
|
246
|
+
const application = [
|
|
247
|
+
{
|
|
248
|
+
name: 'application.name',
|
|
249
|
+
arg: '<name>',
|
|
250
|
+
env: 'CONTRASTSECURITY_APP_NAME',
|
|
251
|
+
desc: 'override the reported application name. (default: package.json:name)'
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
name: 'application.path',
|
|
255
|
+
arg: '<path>',
|
|
256
|
+
default: '/',
|
|
257
|
+
desc: 'override the reported application path'
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
name: 'application.version',
|
|
261
|
+
arg: '<version>',
|
|
262
|
+
desc:
|
|
263
|
+
"override the reported application version (if different from 'version' field in the application's package.json)"
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
name: 'application.session_id',
|
|
267
|
+
arg: '<session_id>',
|
|
268
|
+
default: null,
|
|
269
|
+
desc: 'provide the ID of a session existing within Contrast UI'
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
name: 'application.session_metadata',
|
|
273
|
+
arg: '<session_metadata>',
|
|
274
|
+
default: null,
|
|
275
|
+
desc: 'provide metadata used to create a new session within Contrast UI'
|
|
276
|
+
}
|
|
277
|
+
];
|
|
278
|
+
|
|
279
|
+
const protect = [
|
|
280
|
+
{
|
|
281
|
+
name: 'protect.enable',
|
|
282
|
+
arg: '[false]',
|
|
283
|
+
fn: castBoolean,
|
|
284
|
+
desc: 'if false, disable protect for this agent'
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
name: 'protect.disabled_rules',
|
|
288
|
+
arg: '<list,of,rules>',
|
|
289
|
+
fn: split,
|
|
290
|
+
default: '',
|
|
291
|
+
desc: 'comma-separated list of rule ids to disable'
|
|
292
|
+
},
|
|
293
|
+
...Object.values(Rule).map((ruleId) => ({
|
|
294
|
+
name: `protect.rules.${ruleId}.mode`,
|
|
295
|
+
arg: '<mode>',
|
|
296
|
+
enum: ['monitor', 'block', 'block_at_perimeter', 'off'],
|
|
297
|
+
desc: `the mode in which to run the ${ruleId} rule`
|
|
298
|
+
}))
|
|
299
|
+
];
|
|
300
|
+
|
|
301
|
+
const server = [
|
|
302
|
+
{
|
|
303
|
+
name: 'server.environment',
|
|
304
|
+
arg: '<name>',
|
|
305
|
+
fn: uppercase,
|
|
306
|
+
// enum: ['QA', 'PRODUCTION', 'DEVELOPMENT'], none of the other agents validate this
|
|
307
|
+
desc:
|
|
308
|
+
'environment the server is running in (QA, PRODUCTION, or DEVELOPMENT)'
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
name: 'server.name',
|
|
312
|
+
arg: '<name>',
|
|
313
|
+
default: os.hostname(),
|
|
314
|
+
desc: 'override the reported server name'
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
name: 'server.version',
|
|
318
|
+
arg: '<version>',
|
|
319
|
+
desc:
|
|
320
|
+
"override the reported server version (if different from 'version' field in the application's package.json)"
|
|
321
|
+
}
|
|
322
|
+
];
|
|
323
|
+
|
|
324
|
+
const options = [].concat(
|
|
325
|
+
config,
|
|
326
|
+
api,
|
|
327
|
+
agent,
|
|
328
|
+
application,
|
|
329
|
+
protect,
|
|
330
|
+
server
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
module.exports.configOptions = options;
|
|
334
|
+
module.exports.clearBaseCase = clearBaseCase;
|
|
335
|
+
module.exports.castBoolean = castBoolean;
|
package/lib/util.js
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const process = require('process');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const yaml = require('yaml');
|
|
8
|
+
|
|
9
|
+
const { configOptions } = require('./options');
|
|
10
|
+
const util = module.exports;
|
|
11
|
+
|
|
12
|
+
function set(obj, name, value) {
|
|
13
|
+
const props = name.split('.');
|
|
14
|
+
const lastProp = props.pop();
|
|
15
|
+
for (const p of props) {
|
|
16
|
+
if (!obj[p]) obj[p] = {};
|
|
17
|
+
obj = obj[p];
|
|
18
|
+
}
|
|
19
|
+
obj[lastProp] = value;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Sets initial config values to the config.
|
|
24
|
+
*
|
|
25
|
+
* @param {Config} conf the config. mutates.
|
|
26
|
+
* @param {string} name name of the option, eg 'agent.logger.stdout'
|
|
27
|
+
* @param {*} value
|
|
28
|
+
* @param {boolean} def set from default or not
|
|
29
|
+
*/
|
|
30
|
+
function setConfig(conf, name, value, def) {
|
|
31
|
+
set(conf, name, value);
|
|
32
|
+
conf._default[name] = def;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
class ConfigurationError extends Error {
|
|
36
|
+
constructor(message) {
|
|
37
|
+
super(`Configuration Error: ${message}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
class Config {
|
|
42
|
+
constructor() {
|
|
43
|
+
Object.assign(this, {
|
|
44
|
+
_default: {},
|
|
45
|
+
api: {},
|
|
46
|
+
agent: {
|
|
47
|
+
reporters: {},
|
|
48
|
+
logger: {},
|
|
49
|
+
node: {},
|
|
50
|
+
},
|
|
51
|
+
application: {},
|
|
52
|
+
protect: {
|
|
53
|
+
rules: {},
|
|
54
|
+
},
|
|
55
|
+
server: {},
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Override the setting, if set from its default.
|
|
61
|
+
*
|
|
62
|
+
* @param {string} name name of the option, eg 'agent.logger.stdout'
|
|
63
|
+
* @param {*} value
|
|
64
|
+
*/
|
|
65
|
+
override(name, value) {
|
|
66
|
+
if (this._default[name]) {
|
|
67
|
+
setConfig(this, name, value, true);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
get(name) {
|
|
72
|
+
return name.split('.').reduce((obj, prop) => obj?.[prop], this);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Custom validation logic.
|
|
77
|
+
* @throws {Error}
|
|
78
|
+
*/
|
|
79
|
+
validate() {
|
|
80
|
+
// Mutually exclusive options:
|
|
81
|
+
if (
|
|
82
|
+
this.get('application.session_id') &&
|
|
83
|
+
this.get('application.session_metadata')
|
|
84
|
+
) {
|
|
85
|
+
throw new ConfigurationError(
|
|
86
|
+
'Cannot set both `application.session_id` and `application.session_metadata`'
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Find location of config given options and name of config file
|
|
94
|
+
* @return {string|void} path, if valid
|
|
95
|
+
*/
|
|
96
|
+
function checkConfigPath() {
|
|
97
|
+
const configDir =
|
|
98
|
+
os.platform() === 'win32'
|
|
99
|
+
? `${process.env['ProgramData']}\\contrast`
|
|
100
|
+
: '/etc/contrast';
|
|
101
|
+
|
|
102
|
+
for (const dir of [process.cwd(), configDir]) {
|
|
103
|
+
const checkPath = path.resolve(dir, 'contrast_security.yaml');
|
|
104
|
+
if (fs.existsSync(checkPath)) {
|
|
105
|
+
return checkPath;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Read config into object
|
|
113
|
+
* @return {Object} configuration
|
|
114
|
+
*/
|
|
115
|
+
function readConfig() {
|
|
116
|
+
let config = {};
|
|
117
|
+
let fileContents;
|
|
118
|
+
const configPath = process.env['CONTRAST_CONFIG_PATH'] || checkConfigPath();
|
|
119
|
+
|
|
120
|
+
if (configPath) {
|
|
121
|
+
try {
|
|
122
|
+
fileContents = fs.readFileSync(configPath, 'utf-8');
|
|
123
|
+
} catch (e) {
|
|
124
|
+
console.error(
|
|
125
|
+
'Unable to read config file at %s: %s',
|
|
126
|
+
configPath,
|
|
127
|
+
e.message
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (fileContents) {
|
|
133
|
+
try {
|
|
134
|
+
config = yaml.parse(fileContents, {
|
|
135
|
+
prettyErrors: true,
|
|
136
|
+
});
|
|
137
|
+
} catch (e) {
|
|
138
|
+
console.error(
|
|
139
|
+
'YAML validator found an error in %s: %s',
|
|
140
|
+
configPath,
|
|
141
|
+
e.message
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return config;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* We want to "un-flatten" the config, in the
|
|
151
|
+
* event that there are any dot-delimited keys remaining.
|
|
152
|
+
* FIXME: when we get rid of JSON, don't do this anymore
|
|
153
|
+
* @return {Object} "un-flattened" file options
|
|
154
|
+
*/
|
|
155
|
+
function getFileOptions() {
|
|
156
|
+
const config = readConfig();
|
|
157
|
+
return Object.values(config).reduce((memo, value, idx) => {
|
|
158
|
+
const key = Object.keys(config)[idx];
|
|
159
|
+
// Merge if necessary,
|
|
160
|
+
if (memo[key]) {
|
|
161
|
+
Object.assign(memo[key], value);
|
|
162
|
+
} else {
|
|
163
|
+
// but otherwise set the config path.
|
|
164
|
+
set(memo, key, value);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return memo;
|
|
168
|
+
}, {});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* only set autoEnv if it's a common config option
|
|
173
|
+
* @param {string} name
|
|
174
|
+
*/
|
|
175
|
+
function getAutoEnv(name) {
|
|
176
|
+
const envName = name.toUpperCase().replace(/\./g, '__');
|
|
177
|
+
return name === 'configFile'
|
|
178
|
+
? undefined
|
|
179
|
+
: process.env[`CONTRAST__${envName}`] || process.env[envName];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* @return {Config} merged options
|
|
184
|
+
*/
|
|
185
|
+
function mergeOptions() {
|
|
186
|
+
const fileOptions = getFileOptions();
|
|
187
|
+
|
|
188
|
+
return configOptions.reduce((options, option) => {
|
|
189
|
+
const {
|
|
190
|
+
default: optDefault,
|
|
191
|
+
enum: optEnum,
|
|
192
|
+
fn = (arg) => arg,
|
|
193
|
+
name,
|
|
194
|
+
} = option;
|
|
195
|
+
|
|
196
|
+
const env = process.env[option.env];
|
|
197
|
+
const autoEnv = getAutoEnv(name);
|
|
198
|
+
const fileFlag = name
|
|
199
|
+
.split('.')
|
|
200
|
+
.reduce((obj, prop) => obj?.[prop], fileOptions);
|
|
201
|
+
|
|
202
|
+
// For some values, we want to know if we assigned by falling back to default
|
|
203
|
+
let isFromDefault;
|
|
204
|
+
|
|
205
|
+
// env > file > default
|
|
206
|
+
let value = [env, autoEnv, fileFlag]
|
|
207
|
+
.map((v) => fn(v))
|
|
208
|
+
.find((flag) => flag !== undefined);
|
|
209
|
+
|
|
210
|
+
// if it's an enum, find it in the enum or set the value to default
|
|
211
|
+
// ineffective if optDefault wasn't in the enum;
|
|
212
|
+
// optDefault won't get passed through fn, so it needs to be valid.
|
|
213
|
+
if (optEnum && optEnum.indexOf(value) === -1) {
|
|
214
|
+
value = fn(optDefault);
|
|
215
|
+
isFromDefault = true;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// set default last and separately, so that we can mark that the option was
|
|
219
|
+
// set from default
|
|
220
|
+
if (value === undefined) {
|
|
221
|
+
value = fn(optDefault);
|
|
222
|
+
isFromDefault = true;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
setConfig(options, name, value, isFromDefault);
|
|
226
|
+
return options;
|
|
227
|
+
}, new Config());
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
util.Config = Config;
|
|
231
|
+
util.setup = function setup() {
|
|
232
|
+
const mergedOptions = mergeOptions();
|
|
233
|
+
mergedOptions.validate();
|
|
234
|
+
return mergedOptions;
|
|
235
|
+
};
|
|
236
|
+
// We want to use the set method used here for creating a correct mock object for tests
|
|
237
|
+
util.setValue = set;
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@contrast/config",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "An API for discovering Contrast agent configuration data",
|
|
5
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
6
|
+
"author": "Contrast Security <nodejs@contrastsecurity.com> (https://www.contrastsecurity.com)",
|
|
7
|
+
"files": [
|
|
8
|
+
"lib/"
|
|
9
|
+
],
|
|
10
|
+
"main": "lib/index.js",
|
|
11
|
+
"types": "lib/index.d.ts",
|
|
12
|
+
"engines": {
|
|
13
|
+
"npm": ">= 8.4.0",
|
|
14
|
+
"node": ">= 14.15.0"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"test": "../scripts/test.sh"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@contrast/common": "1.0.0",
|
|
21
|
+
"yaml": "^2.0.1"
|
|
22
|
+
}
|
|
23
|
+
}
|