@contrast/config 1.31.0 → 1.33.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/config.js CHANGED
@@ -38,7 +38,7 @@ const HOME_CONFIG_DIR = path.resolve(os.homedir(), '.config', 'contrast');
38
38
  const OS_CONFIG_DIR = os.platform() === 'win32'
39
39
  ? path.resolve(process.env.ProgramData || '', 'contrast')
40
40
  : '/etc/contrast';
41
- const REDACTED_KEYS = ['api.api_key', 'api.service_key'];
41
+ const REDACTED_KEYS = ['api.api_key', 'api.service_key', 'api.token'];
42
42
  const OVERRIDABLE_SOURCES = [DEFAULT_VALUE, CONTRAST_UI];
43
43
 
44
44
  module.exports = class Config {
@@ -48,6 +48,7 @@ module.exports = class Config {
48
48
  this._errors = [];
49
49
  this._effectiveMap = new Map();
50
50
  this._status = '';
51
+ this._logs = [];
51
52
 
52
53
  // config object
53
54
  this.api = {};
@@ -207,7 +208,7 @@ module.exports = class Config {
207
208
  source = DEFAULT_VALUE;
208
209
  }
209
210
 
210
- if (opt.fn) value = opt.fn(value);
211
+ if (opt.fn) value = opt.fn(value, this, source);
211
212
  if (opt.enum && !opt.enum.includes(value)) value = opt.default;
212
213
 
213
214
  set(this, opt.name, value);
@@ -221,7 +222,7 @@ module.exports = class Config {
221
222
  }
222
223
 
223
224
  _redact(name, value) {
224
- if (value == null) return value;
225
+ if (value === null) return value;
225
226
  return REDACTED_KEYS.includes(name) ? `contrast-redacted-${name}` : value;
226
227
  }
227
228
 
@@ -239,12 +240,14 @@ module.exports = class Config {
239
240
  const effective_config = [], environment_variable = [], contrast_ui = [];
240
241
 
241
242
  Array.from(this._effectiveMap.values()).forEach((v) => {
242
- let value = redact ? this._redact(v.name, v.value) : v.value;
243
+ let { value } = v;
244
+ if (redact) value = this._redact(v.name, v.value);
243
245
  if (value === undefined) value = null;
244
246
 
245
- effective_config.push({ ...v, value: String(value) });
246
- if (v.source === ENVIRONMENT_VARIABLE) environment_variable.push(v);
247
- if (v.source === CONTRAST_UI) contrast_ui.push(v);
247
+ const redacted = { ...v, value: String(value) };
248
+ effective_config.push(redacted);
249
+ if (v.source === ENVIRONMENT_VARIABLE) environment_variable.push(redacted);
250
+ if (v.source === CONTRAST_UI) contrast_ui.push(redacted);
248
251
  });
249
252
 
250
253
  return {
package/lib/index.d.ts CHANGED
@@ -15,13 +15,13 @@
15
15
 
16
16
  import { ProtectRuleMode, Rule } from '@contrast/common';
17
17
  import { LevelWithSilent } from 'pino';
18
- export { ConfigSource } from './common.js';
18
+ export { ConfigSource } from './common';
19
19
 
20
20
  export interface EffectiveEntry<T> {
21
21
  canonical_name: string;
22
22
  name: string;
23
23
  value: T;
24
- source: ConfigSource;
24
+ source: string;
25
25
  }
26
26
 
27
27
  export type Level =
@@ -47,7 +47,7 @@ export interface ConfigOption<T> {
47
47
  arg: string;
48
48
  enum?: T[];
49
49
  default?: T;
50
- fn?: (arg: any) => T;
50
+ fn?: (arg: any, cfg: Config, source: string) => T;
51
51
  desc: string;
52
52
  }
53
53
 
@@ -56,6 +56,13 @@ export interface Config {
56
56
  _effectiveMap: Map<string, EffectiveEntry<any>>;
57
57
  _errors: Error[];
58
58
  _status: string,
59
+ _logs: {
60
+ level: import('pino').LevelWithSilentOrString;
61
+ obj?: any;
62
+ msg: string;
63
+ args?: any[];
64
+ }[];
65
+
59
66
 
60
67
  api: {
61
68
  /** Default: `true` */
@@ -296,10 +303,10 @@ export interface Config {
296
303
  /** Default: `true` */
297
304
  discover_cloud_resource: boolean;
298
305
  };
299
- getEffectiveSource(cannonicalName: string): any;
300
- getEffectiveValue(cannonicalName: string): any;
306
+ getEffectiveSource(cannonicalName: string): string;
307
+ getEffectiveValue<T = any>(cannonicalName: string): T;
301
308
  getReport({ redact: boolean }): any;
302
- setValue(name: string, value: any, source: ConfigSource): void;
309
+ setValue<T = any>(name: string, value: T, source: string): void;
303
310
  }
304
311
 
305
312
  declare function init(core: { config?: Config }): Config;
package/lib/index.test.js CHANGED
@@ -186,6 +186,49 @@ describe('config', function () {
186
186
  });
187
187
  });
188
188
 
189
+ describe('api.token handling', function () {
190
+ it('sets api config vars when they are not present', function () {
191
+ env['CONTRAST_CONFIG_PATH'] = getGoodConfig('token');
192
+ const cfg = config();
193
+
194
+ expect(cfg.api).to.include({
195
+ url: 'http://localhost:12345/Contrast',
196
+ api_key: 'secret',
197
+ service_key: 'secret',
198
+ user_name: 'tokenuser'
199
+ });
200
+ });
201
+
202
+ it('does not override api config vars when they are explicitly set in config', function () {
203
+ // url: 'http://localhost:12345/Contrast',
204
+ // api_key: 'secret',
205
+ // service_key: 'secret',
206
+ // user_name: 'tokenuser'
207
+ env['CONTRAST__API__TOKEN'] = 'eyJ1cmwiOiJodHRwOi8vbG9jYWxob3N0OjEyMzQ1L0NvbnRyYXN0IiwiYXBpX2tleSI6InNlY3JldCIsInNlcnZpY2Vfa2V5Ijoic2VjcmV0IiwidXNlcl9uYW1lIjoidG9rZW51c2VyIn0=';
208
+ const cfg = config();
209
+
210
+ expect(cfg.api).to.include({
211
+ url: 'http://localhost:19080/Contrast',
212
+ api_key: 'demo',
213
+ service_key: 'demo',
214
+ user_name: 'contrast_admin'
215
+ });
216
+ });
217
+
218
+ it('observes the correct precedence', function () {
219
+ env['CONTRAST_CONFIG_PATH'] = getGoodConfig('token');
220
+ env['CONTRAST__API__API_KEY'] = 'something else';
221
+ const cfg = config();
222
+
223
+ expect(cfg.api).to.include({
224
+ url: 'http://localhost:12345/Contrast',
225
+ api_key: 'something else',
226
+ service_key: 'secret',
227
+ user_name: 'tokenuser'
228
+ });
229
+ });
230
+ });
231
+
189
232
  describe('multiple errors can be thrown in an exception', function() {
190
233
  it('should report multiple errors', function() {
191
234
  const options = getDefaultConfig();
package/lib/options.js CHANGED
@@ -21,6 +21,7 @@ const os = require('os');
21
21
  const url = require('url');
22
22
  const path = require('path');
23
23
  const { Rule } = require('@contrast/common');
24
+ const { ConfigSource: { DEFAULT_VALUE } } = require('./common');
24
25
 
25
26
  /**
26
27
  * Takes strings "true"|"t" or "false"|"f" (case insensitive) and return the appropriate boolean.
@@ -155,6 +156,35 @@ const options = [
155
156
  arg: '<name>',
156
157
  desc: 'Set the user name used to communicate with the Contrast UI. It is used to calculate the Authorization header.',
157
158
  },
159
+ {
160
+ name: 'api.token',
161
+ arg: '<token>',
162
+ desc: 'base64 encoded JSON object containing the `url`, `api_key`, `service_key`, and `user_name` config options, allowing them all to be set in a single variable.',
163
+ fn(value, cfg, source) {
164
+ try {
165
+ // parse the base64 encoded value
166
+ const parsed = JSON.parse(Buffer.from(value, 'base64').toString('utf8'));
167
+ // set the top level `api` keys only if they aren't already present.
168
+ // since this value comes after the others, they should be set first if present in the config file or environment.
169
+ ['url', 'api_key', 'service_key', 'user_name'].forEach(key => {
170
+ const canonicalName = `api.${key}`;
171
+ const existingSource = cfg.getEffectiveSource(canonicalName);
172
+ if (existingSource !== DEFAULT_VALUE) {
173
+ cfg._logs.push({
174
+ level: 'info',
175
+ msg: 'Using configured value for `%s` (set by %s) instead of `api.token`.',
176
+ args: [canonicalName, existingSource]
177
+ });
178
+ } else {
179
+ cfg.setValue(canonicalName, parsed[key], source);
180
+ }
181
+ });
182
+ return value;
183
+ } catch {
184
+ return null;
185
+ }
186
+ }
187
+ },
158
188
  // api.proxy
159
189
  {
160
190
  name: 'api.proxy.enable',
@@ -512,10 +542,10 @@ Example - \`label1, label2, label3\``,
512
542
  {
513
543
  name: 'assess.stacktraces',
514
544
  arg: '<level>',
515
- enum: ['ALL', 'SOME', 'NONE'],
545
+ enum: ['ALL', 'SOME', 'SINK', 'NONE'],
516
546
  default: 'ALL',
517
547
  fn: uppercase,
518
- desc: 'Select the level of collected stacktraces. ALL - for all assess events, SOME - for Source and Sink events, NONE - no stacktraces collected',
548
+ desc: 'Select the level of collected stacktraces. ALL - for all assess events, SOME - for Source and Sink events, SINK - for Sink events, NONE - no stacktraces collected',
519
549
  },
520
550
  {
521
551
  name: 'assess.max_context_source_events',
@@ -0,0 +1,23 @@
1
+ /*
2
+ * Copyright: 2024 Contrast Security, Inc
3
+ * Contact: support@contrastsecurity.com
4
+ * License: Commercial
5
+
6
+ * NOTICE: This Software and the patented inventions embodied within may only be
7
+ * used as part of Contrast Security’s commercial offerings. Even though it is
8
+ * made available through public repositories, use of this Software is subject to
9
+ * the applicable End User Licensing Agreement found at
10
+ * https://www.contrastsecurity.com/enduser-terms-0317a or as otherwise agreed
11
+ * between Contrast Security and the End User. The Software may not be reverse
12
+ * engineered, modified, repackaged, sold, redistributed or otherwise used in a
13
+ * way not consistent with the End User License Agreement.
14
+ */
15
+
16
+ 'use strict';
17
+
18
+ // Abusing the `validators` pattern to allow us to log after core has been set up.
19
+ module.exports.config = function config(core) {
20
+ core.config._logs.forEach(({ level, obj, msg, args = [] }) => {
21
+ core.logger[level](obj, msg, ...args);
22
+ });
23
+ };
@@ -0,0 +1,42 @@
1
+ 'use strict';
2
+
3
+ const { expect } = require('chai');
4
+ const mocks = require('@contrast/test/mocks');
5
+ const validators = require('./validators');
6
+
7
+ describe('config validators', function () {
8
+ let core;
9
+
10
+ beforeEach(function () {
11
+ core = { logger: mocks.logger() };
12
+ });
13
+
14
+ describe('config', function () {
15
+ it('calls `logger` for each log message provided', function () {
16
+ core.config = {
17
+ _logs: [
18
+ { level: 'info', msg: 'some info level message' },
19
+ {
20
+ level: 'debug',
21
+ obj: { foo: 'bar' },
22
+ msg: 'some debug level message with %d %s',
23
+ args: [2, 'args'],
24
+ },
25
+ ],
26
+ };
27
+
28
+ validators.config(core);
29
+
30
+ expect(core.logger.info).to.have.been.calledWith(
31
+ undefined,
32
+ 'some info level message'
33
+ );
34
+ expect(core.logger.debug).to.have.been.calledWith(
35
+ { foo: 'bar' },
36
+ 'some debug level message with %d %s',
37
+ 2,
38
+ 'args',
39
+ );
40
+ });
41
+ });
42
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contrast/config",
3
- "version": "1.31.0",
3
+ "version": "1.33.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.24.0",
20
+ "@contrast/common": "1.25.0",
21
21
  "yaml": "^2.2.2"
22
22
  }
23
23
  }