@contrast/config 1.33.0 → 1.35.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 CHANGED
@@ -39,8 +39,18 @@ const {
39
39
  UNTRUSTED_DESERIALIZATION,
40
40
  XXE,
41
41
  },
42
+ primordials: { StringPrototypeToLowerCase },
43
+ get,
44
+ isString,
42
45
  } = require('@contrast/common');
43
46
 
47
+ function coerceLowerCase(path) {
48
+ return function(remoteData) {
49
+ const value = get(remoteData, path);
50
+ if (value && isString(value)) return StringPrototypeToLowerCase.call(value);
51
+ };
52
+ }
53
+
44
54
  function protectModeReader(ruleId) {
45
55
  return function (msg) {
46
56
  const remoteSetting = msg?.protect?.rules?.[ruleId];
@@ -62,20 +72,12 @@ const ConfigSource = {
62
72
  USER_CONFIGURATION_FILE: 'USER_CONFIGURATION_FILE',
63
73
  };
64
74
 
75
+ // these should return `undefined` if there is no remote value corresponding to the effective config name.
65
76
  const mappings = {
66
- // 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
+ // agent startup (v1) or application startup (ng fallback)
78
+ 'application.session_id': (remoteData) =>
79
+ remoteData.identification?.session_id ?? remoteData.settings?.assessment?.session_id,
77
80
  // application settings
78
- 'assess.enable': (remoteData) => remoteData.assess?.enable,
79
81
  'protect.enable': (remoteData) => remoteData.protect?.enable,
80
82
  'protect.rules.cmd-injection.mode': protectModeReader(CMD_INJECTION),
81
83
  'protect.rules.cmd-injection-command-backdoors.mode': protectModeReader(CMD_INJECTION_COMMAND_BACKDOORS),
@@ -92,6 +94,27 @@ const mappings = {
92
94
  'protect.rules.unsafe-file-upload.mode': protectModeReader(UNSAFE_FILE_UPLOAD),
93
95
  'protect.rules.untrusted-deserialization.mode': protectModeReader(UNTRUSTED_DESERIALIZATION),
94
96
  'protect.rules.xxe.mode': protectModeReader(XXE),
97
+ // server features
98
+ 'assess.enable': (remoteData) => remoteData.assess?.enable,
99
+ 'assess.probabilistic_sampling.enable': (remoteData) => remoteData.assess?.sampling?.enable,
100
+ 'assess.probabilistic_sampling.base_probability': (remoteData) => {
101
+ const request_frequency = remoteData.assess?.sampling?.request_frequency;
102
+ if (request_frequency > 0) {
103
+ const baseProbability = 1 / request_frequency;
104
+ if (!isNaN(baseProbability)) return baseProbability;
105
+ }
106
+ },
107
+ 'agent.logger.level': coerceLowerCase('logger.level'),
108
+ 'agent.logger.path': (remoteData) => remoteData.logger?.path,
109
+ 'agent.security_logger.syslog.enable': (remoteData) => remoteData.security_logger?.syslog?.enable,
110
+ 'agent.security_logger.syslog.ip': (remoteData) => remoteData.security_logger?.syslog?.ip,
111
+ 'agent.security_logger.syslog.port': (remoteData) => remoteData.security_logger?.syslog?.port,
112
+ 'agent.security_logger.syslog.facility': (remoteData) => remoteData.security_logger?.syslog?.facility,
113
+ 'agent.security_logger.syslog.severity_exploited': coerceLowerCase('security_logger.syslog.severity_exploited'),
114
+ 'agent.security_logger.syslog.severity_blocked': coerceLowerCase('security_logger.syslog.severity_blocked'),
115
+ 'agent.security_logger.syslog.severity_probed': coerceLowerCase('security_logger.syslog.severity_probed'),
116
+ 'server.environment': (remoteData) => remoteData.environment,
117
+
95
118
  };
96
119
 
97
120
  /*
@@ -0,0 +1,183 @@
1
+ 'use strict';
2
+
3
+ const { expect } = require('chai');
4
+ const { ProtectRuleMode } = require('@contrast/common');
5
+ const { mappings } = require('./common');
6
+
7
+ describe('config effective value mappings', function () {
8
+ describe('"simple" mappings', function() {
9
+ const simplyMappedNames = [
10
+ 'assess.enable',
11
+ 'agent.logger.path',
12
+ 'agent.security_logger.syslog.enable',
13
+ 'agent.security_logger.syslog.ip',
14
+ 'agent.security_logger.syslog.port',
15
+ 'agent.security_logger.syslog.facility',
16
+ 'server.environment',
17
+ 'protect.enable',
18
+ ];
19
+
20
+ it('"simple" mappings will return any value for corresponding remote setting (todo: validation for each)', function () {
21
+ ['', 'foo', null, 0.3, 3, {}, [], true, false].forEach((val) => {
22
+ const remoteData = {
23
+ // server
24
+ assess: {
25
+ enable: val,
26
+ sampling: { enable: val }
27
+ },
28
+ logger: {
29
+ path: val,
30
+ },
31
+ security_logger: {
32
+ syslog: {
33
+ enable: val,
34
+ ip: val,
35
+ port: val,
36
+ facility: val,
37
+ serverity_exploited: val,
38
+ serverity_blocked: val,
39
+ exploited_probed: val,
40
+ }
41
+ },
42
+ environment: val,
43
+ // app
44
+ protect: {
45
+ enable: val,
46
+ },
47
+ };
48
+
49
+ for (const name of simplyMappedNames) {
50
+ const result = mappings[name](remoteData);
51
+ expect(result).to.equal(val);
52
+ }
53
+ });
54
+ });
55
+
56
+ it('"simple" mappings will return `undefined` if there is no corresponding remote setting', function () {
57
+ for (const name of simplyMappedNames) {
58
+ expect(mappings[name]({})).to.equal(undefined);
59
+ }
60
+ });
61
+ });
62
+
63
+ describe('maps \'assess.probabilistic_sampling.base_probability\'', function() {
64
+ [
65
+ {
66
+ desc: 'sets value by inverting request_frequency',
67
+ remoteData: {
68
+ assess: { sampling: { request_frequency: 20 } }
69
+ },
70
+ expected: 1 / 20,
71
+ },
72
+ {
73
+ desc: 'ignores and returns undefined if request_frequency is 0',
74
+ remoteData: {
75
+ assess: { sampling: { request_frequency: 0 } }
76
+ },
77
+ expected: undefined,
78
+ },
79
+ {
80
+ desc: 'ignores and returns undefined if calculating probability gives NaN',
81
+ remoteData: {
82
+ assess: { sampling: { request_frequency: 'forty five' } }
83
+ },
84
+ expected: undefined,
85
+ },
86
+ {
87
+ desc: 'ignores and returns undefined if value is negative',
88
+ remoteData: {
89
+ assess: { sampling: { request_frequency: -12 } }
90
+ },
91
+ expected: undefined,
92
+ },
93
+ ].forEach(({ desc, remoteData, expected }) => {
94
+ it(desc, function() {
95
+ expect(mappings['assess.probabilistic_sampling.base_probability'](remoteData)).to.equal(expected);
96
+ });
97
+ });
98
+ });
99
+
100
+ describe('maps Protect rule modes', function() {
101
+ [
102
+ {
103
+ tsValue: 'OFF',
104
+ expected: ProtectRuleMode.OFF,
105
+ },
106
+ {
107
+ tsValue: 'MONITORING',
108
+ expected: ProtectRuleMode.MONITOR,
109
+ },
110
+ {
111
+ tsValue: 'BLOCKING',
112
+ expected: ProtectRuleMode.BLOCK,
113
+ },
114
+ {
115
+ tsValue: 'BLOCK_AT_PERIMETER',
116
+ expected: ProtectRuleMode.BLOCK_AT_PERIMETER,
117
+ },
118
+ {
119
+ tsValue: 'FOO',
120
+ expected: undefined
121
+ },
122
+ ].forEach(({ desc, tsValue, expected }) => {
123
+ [
124
+ 'protect.rules.cmd-injection.mode',
125
+ 'protect.rules.cmd-injection-command-backdoors.mode',
126
+ 'protect.rules.cmd-injection-semantic-chained-commands.mode',
127
+ 'protect.rules.cmd-injection-semantic-dangerous-paths.mode',
128
+ 'protect.rules.method-tampering.mode',
129
+ 'protect.rules.nosql-injection.mode',
130
+ 'protect.rules.nosql-injection-mongo.mode',
131
+ 'protect.rules.path-traversal.mode',
132
+ 'protect.rules.path-traversal-semantic-file-security-bypass.mode',
133
+ 'protect.rules.reflected-xss.mode',
134
+ 'protect.rules.sql-injection.mode',
135
+ 'protect.rules.ssjs-injection.mode',
136
+ 'protect.rules.unsafe-file-upload.mode',
137
+ 'protect.rules.untrusted-deserialization.mode',
138
+ 'protect.rules.xxe.mode',
139
+ ].forEach((fullName) => {
140
+ const ruleName = /rules\.(.*)\.mode/.exec(fullName)[1];
141
+ it(`sets ${ruleName} to ${expected} when value is ${tsValue}`, function() {
142
+ expect(mappings[fullName]({
143
+ protect: {
144
+ rules: {
145
+ [ruleName]: { mode: tsValue },
146
+ }
147
+ }
148
+ })).to.equal(expected);
149
+ });
150
+ });
151
+ });
152
+ });
153
+
154
+ describe('maps security logger severity levels (lowercase)', function() {
155
+ [
156
+ {
157
+ tsValue: 'FOO',
158
+ expected: 'foo',
159
+ },
160
+ {
161
+ tsValue: '',
162
+ expected: undefined,
163
+ }
164
+ ].forEach(({ tsValue, expected }) => {
165
+ [
166
+ 'agent.security_logger.syslog.severity_exploited',
167
+ 'agent.security_logger.syslog.severity_blocked',
168
+ 'agent.security_logger.syslog.severity_probed',
169
+ ].forEach((fullName) => {
170
+ const severity = fullName.substr(fullName.lastIndexOf('.') + 1);
171
+ it(`maps to ${expected} when tsValue is ${tsValue}`, function() {
172
+ expect(mappings[fullName]({
173
+ security_logger: {
174
+ syslog: {
175
+ [severity]: tsValue,
176
+ },
177
+ }
178
+ })).to.equal(expected);
179
+ });
180
+ });
181
+ });
182
+ });
183
+ });
package/lib/config.js CHANGED
@@ -20,7 +20,7 @@ const path = require('path');
20
20
  const fs = require('fs');
21
21
  const os = require('os');
22
22
  const yaml = require('yaml');
23
- const { Event, get, set } = require('@contrast/common');
23
+ const { Event, get, set, primordials: { ArrayPrototypeJoin, StringPrototypeToUpperCase, JSONParse } } = require('@contrast/common');
24
24
  const options = require('./options');
25
25
  const {
26
26
  ConfigSource: {
@@ -74,7 +74,7 @@ module.exports = class Config {
74
74
 
75
75
  // report all the errors found during initialization.
76
76
  if (this._errors.length) {
77
- const errors = this._errors.map((e, ix) => `${ix + 1}) ${e.message}`).join('; ');
77
+ const errors = ArrayPrototypeJoin.call(this._errors.map((e, ix) => `${ix + 1}) ${e.message}`), '; ');
78
78
  throw new Error(`Errors found in configuration ${errors}`);
79
79
  }
80
80
 
@@ -104,7 +104,7 @@ module.exports = class Config {
104
104
  let parsedEnv;
105
105
 
106
106
  try {
107
- parsedEnv = JSON.parse(env.pm2_env);
107
+ parsedEnv = JSONParse(env.pm2_env);
108
108
  } catch (_err) {
109
109
  const err = new Error(`Unable to parse pm2 environment variable: '${env.pm2_env}'`);
110
110
  err.cause = _err;
@@ -122,7 +122,7 @@ module.exports = class Config {
122
122
  }
123
123
 
124
124
  return Object.entries(env).reduce((acc, [key, val]) => {
125
- const name = key.toUpperCase();
125
+ const name = StringPrototypeToUpperCase.call(key);
126
126
  if (name.startsWith('CONTRAST_')) {
127
127
  acc[name] = val;
128
128
  }
@@ -161,11 +161,12 @@ module.exports = class Config {
161
161
 
162
162
  const { _filepath } = this;
163
163
 
164
- if (_filepath) {
164
+ // deliberately ignore /dev/null (linux) and \\.\\nul (windows)
165
+ if (_filepath && _filepath !== os.devNull) {
165
166
  let fileContents;
166
167
 
167
168
  try {
168
- fileContents = fs.readFileSync(_filepath).toString('utf-8');
169
+ fileContents = fs.readFileSync(_filepath, 'utf-8');
169
170
  } catch (e) {
170
171
  const err = new Error(`Unable to read Contrast configuration file: '${_filepath}'`);
171
172
  err.cause = e;
@@ -242,9 +243,8 @@ module.exports = class Config {
242
243
  Array.from(this._effectiveMap.values()).forEach((v) => {
243
244
  let { value } = v;
244
245
  if (redact) value = this._redact(v.name, v.value);
245
- if (value === undefined) value = null;
246
246
 
247
- const redacted = { ...v, value: String(value) };
247
+ const redacted = { ...v, value: value !== null ? String(value) : null };
248
248
  effective_config.push(redacted);
249
249
  if (v.source === ENVIRONMENT_VARIABLE) environment_variable.push(redacted);
250
250
  if (v.source === CONTRAST_UI) contrast_ui.push(redacted);
package/lib/index.d.ts CHANGED
@@ -262,35 +262,24 @@ export interface Config {
262
262
  };
263
263
 
264
264
  application: {
265
- /** override the reported application name. */
265
+ /** Override the reported application name. */
266
266
  name?: string;
267
- /** override the reported application path. Default: `'/'` */
267
+ /** Override the reported application path. Default: `'/'` */
268
268
  path: string;
269
- /** override the reported application version */
269
+ /** Add the name of the application group with which this application should be associated in the Contrast UI. */
270
+ group?: string;
271
+ /** Add the application code this application should use in the Contrast UI. */
272
+ code?: string;
273
+ /** Override the reported application version. */
270
274
  version?: string;
271
-
272
- /**
273
- * Provide the ID of a session existing within Contrast UI.
274
- * Default: `null`
275
- */
276
- session_id: string | null;
277
-
278
- /**
279
- * How to report the application's group for auto-grouping
280
- */
281
- group: string | null;
282
-
283
- /**
284
- * Comma-separated list of key=value pairs that are applied to each application reported by the agent.
285
- */
286
- metadata: string | null;
287
-
288
- /**
289
- * Provide metadata used to create a new session within Contrast UI.
290
- * Default: `null`
291
- */
292
- session_metadata: string | null;
293
-
275
+ /** Apply labels to an application. Labels must be formatted as a comma-delimited list. Example - `label1,label2,label3` */
276
+ tags?: string;
277
+ /** Comma-separated list of key=value pairs that are applied to each application reported by the agent. */
278
+ metadata?: string;
279
+ /** Provide the ID of a session existing within Contrast UI. Exclusive with `session_metadata` */
280
+ session_id?: string;
281
+ /** Provide metadata used to create a new session within Contrast UI. Exclusive with `session_id` */
282
+ session_metadata?: string;
294
283
  };
295
284
 
296
285
  /** Reported server information overrides */
package/lib/index.test.js CHANGED
@@ -371,11 +371,12 @@ describe('config', function () {
371
371
  // there needs to be a valid config file or env var set so config doesn't throw
372
372
  // an error.
373
373
  process.env.CONTRAST_CONFIG_PATH = path.resolve(__dirname, '../../test/fixtures/protect_contrast_security.yaml');
374
+ //process.env.CONTRAST__PROTECT__RULES__DISABLED_RULES = 'cmd-injection,sql-injection';
374
375
  config = require('.')(core);
375
376
 
376
377
  config.setValue('contrast_config_path', path.resolve(__dirname, '../../test/fixtures/protect_contrast_security.yaml'), ENVIRONMENT_VARIABLE);
377
378
  config.setValue('contrast__agent__stack_trace_limit', 20, ENVIRONMENT_VARIABLE);
378
- config.setValue('contrast.protect.rules.disabled_rules', ['cmd-injection', 'sql-injection'], ENVIRONMENT_VARIABLE);
379
+ config.setValue('protect.rules.disabled_rules', ['cmd-injection', 'sql-injection'], ENVIRONMENT_VARIABLE);
379
380
  config.setValue('api.enable', true, ENVIRONMENT_VARIABLE);
380
381
  config.setValue('api.api_key', 'ABCDEFGHIJ', ENVIRONMENT_VARIABLE);
381
382
  config.setValue('api.service_key', 'KLMNOPQRST', ENVIRONMENT_VARIABLE);
@@ -407,11 +408,12 @@ describe('config', function () {
407
408
  const {
408
409
  config: {
409
410
  effective_config,
411
+ environment_variable,
410
412
  }
411
413
  } = result;
412
414
 
413
415
  expect(result.config).to.have.property('status', 'Success');
414
- expect(effective_config).to.deep.include(
416
+ const expectedResults = [
415
417
  {
416
418
  canonical_name: 'api.enable',
417
419
  name: 'api.enable',
@@ -422,13 +424,13 @@ describe('config', function () {
422
424
  canonical_name: 'api.api_key',
423
425
  name: 'api.api_key',
424
426
  source: 'ENVIRONMENT_VARIABLE',
425
- value: 'contrast-redacted-api.api_key',
427
+ value: 'ABCDEFGHIJ',
426
428
  },
427
429
  {
428
430
  canonical_name: 'api.service_key',
429
431
  name: 'api.service_key',
430
432
  source: 'ENVIRONMENT_VARIABLE',
431
- value: 'contrast-redacted-api.service_key',
433
+ value: 'KLMNOPQRST',
432
434
  },
433
435
  {
434
436
  canonical_name: 'api.proxy.enable',
@@ -496,7 +498,37 @@ describe('config', function () {
496
498
  value: '150',
497
499
  source: 'DEFAULT_VALUE'
498
500
  }
499
- );
501
+ ];
502
+
503
+ const failures = [];
504
+ let effective;
505
+ let envs;
506
+ const envFailures = [];
507
+
508
+ for (const expected of expectedResults) {
509
+ effective = effective_config.filter(i => i.canonical_name === expected.canonical_name);
510
+ expect(effective).an('array').lengthOf(1); // eslint-disable-line
511
+ effective = effective[0];
512
+ try {
513
+ expect(expected).to.deep.equal(effective);
514
+ } catch (e) {
515
+ failures.push({ expected, actual: effective });
516
+ }
517
+ if (expected.source !== 'ENVIRONMENT_VARIABLE') continue;
518
+
519
+ envs = environment_variable.filter(i => i.canonical_name === expected.canonical_name);
520
+ expect(envs).an('array')
521
+ .lengthOf(1, `Expected to find ${expected.name} in environment_variable`);
522
+ envs = envs[0];
523
+ try {
524
+ expect(expected).to.deep.equal(envs);
525
+ } catch (e) {
526
+ envFailures.push({ expected, actual: envs });
527
+ }
528
+ }
529
+ // this provides a useful error message
530
+ expect(failures).eql([]);
531
+ expect(envFailures).eql([]);
500
532
  });
501
533
 
502
534
  it('stringifies values and redacts api keys when `redact` is set to true', function () {
@@ -504,11 +536,13 @@ describe('config', function () {
504
536
  const {
505
537
  config: {
506
538
  effective_config,
539
+ environment_variable,
507
540
  }
508
541
  } = result;
509
542
 
510
543
  expect(result.config).to.have.property('status', 'Success');
511
- expect(effective_config).to.deep.include(
544
+
545
+ const expectedResults = [
512
546
  {
513
547
  canonical_name: 'api.enable',
514
548
  name: 'api.enable',
@@ -593,7 +627,37 @@ describe('config', function () {
593
627
  value: '150',
594
628
  source: 'DEFAULT_VALUE'
595
629
  }
596
- );
630
+ ];
631
+
632
+ const failures = [];
633
+ let effective;
634
+ let envs;
635
+ const envFailures = [];
636
+
637
+ for (const expected of expectedResults) {
638
+ effective = effective_config.filter(i => i.canonical_name === expected.canonical_name);
639
+ expect(effective).an('array').lengthOf(1); // eslint-disable-line
640
+ effective = effective[0];
641
+ try {
642
+ expect(expected).to.deep.equal(effective);
643
+ } catch (e) {
644
+ failures.push({ expected, actual: effective });
645
+ }
646
+ if (expected.source !== 'ENVIRONMENT_VARIABLE') continue;
647
+
648
+ envs = environment_variable.filter(i => i.canonical_name === expected.canonical_name);
649
+ expect(envs).an('array')
650
+ .lengthOf(1, `Expected to find ${expected.name} in environment_variable`);
651
+ envs = envs[0];
652
+ try {
653
+ expect(expected).to.deep.equal(envs);
654
+ } catch (e) {
655
+ envFailures.push({ expected, actual: envs });
656
+ }
657
+ }
658
+ // this provides a useful error message
659
+ expect(failures).eql([]);
660
+ expect(envFailures).eql([]);
597
661
  });
598
662
  });
599
663
  });
package/lib/options.js CHANGED
@@ -20,7 +20,7 @@
20
20
  const os = require('os');
21
21
  const url = require('url');
22
22
  const path = require('path');
23
- const { Rule } = require('@contrast/common');
23
+ const { Rule, primordials: { BufferFrom, BufferPrototypeToString, StringPrototypeReplace, StringPrototypeReplaceAll, StringPrototypeSplit, StringPrototypeToLowerCase, StringPrototypeToUpperCase, JSONParse } } = require('@contrast/common');
24
24
  const { ConfigSource: { DEFAULT_VALUE } } = require('./common');
25
25
 
26
26
  /**
@@ -33,7 +33,7 @@ function castBoolean(value) {
33
33
  if (type !== 'string' && type !== 'boolean') {
34
34
  return;
35
35
  }
36
- value = value.toString().toLowerCase();
36
+ value = StringPrototypeToLowerCase.call(value.toString());
37
37
  return value === 'true' || value === 't'
38
38
  ? true
39
39
  : value === 'false' || value === 'f'
@@ -61,11 +61,11 @@ function clearBaseCase(val) {
61
61
  const split = (val) => {
62
62
  if (val === '') return [];
63
63
  if (val === undefined) return val;
64
- return val.split(',');
64
+ return StringPrototypeSplit.call(val, ',');
65
65
  };
66
66
 
67
- const uppercase = (val = '') => clearBaseCase(`${val}`.toUpperCase());
68
- const lowercase = (val = '') => clearBaseCase(`${val}`.toLowerCase());
67
+ const uppercase = (val = '') => clearBaseCase(`${StringPrototypeToUpperCase.call(val)}`);
68
+ const lowercase = (val = '') => clearBaseCase(`${StringPrototypeToLowerCase.call(val)}`);
69
69
  const parseNum = (val) => {
70
70
  const float = parseFloat(val);
71
71
  return clearBaseCase(Math.ceil(float));
@@ -129,7 +129,7 @@ const options = [
129
129
 
130
130
  if (uri.pathname) {
131
131
  // kill trailling /
132
- uri.pathname = uri.pathname.replace(/\/+$/, '');
132
+ uri.pathname = StringPrototypeReplace.call(uri.pathname, /\/+$/, '');
133
133
 
134
134
  if (!uri.pathname.endsWith('Contrast')) {
135
135
  uri.pathname += 'Contrast';
@@ -163,7 +163,7 @@ const options = [
163
163
  fn(value, cfg, source) {
164
164
  try {
165
165
  // parse the base64 encoded value
166
- const parsed = JSON.parse(Buffer.from(value, 'base64').toString('utf8'));
166
+ const parsed = JSONParse(BufferPrototypeToString.call(BufferFrom(value, 'base64'), 'utf8'));
167
167
  // set the top level `api` keys only if they aren't already present.
168
168
  // since this value comes after the others, they should be set first if present in the config file or environment.
169
169
  ['url', 'api_key', 'service_key', 'user_name'].forEach(key => {
@@ -439,7 +439,7 @@ Example - \`/opt/Contrast/contrast.log\` creates a log in the \`/opt/Contrast\`
439
439
  name: 'agent.node.cmd_ignore_list',
440
440
  arg: '<commands>',
441
441
  default: '',
442
- fn: (arg) => arg.split(',').filter((v) => v),
442
+ fn: (arg) => StringPrototypeSplit.call(arg, ',').filter((v) => v),
443
443
  desc: 'comma-separated list of commands that will not startup the agent if agent is required; npm* will ignore all npm executables but not your application\'s scripts'
444
444
  },
445
445
  {
@@ -533,6 +533,29 @@ Example - \`/opt/Contrast/contrast.log\` creates a log in the \`/opt/Contrast\`
533
533
  fn: castBoolean,
534
534
  desc: 'Include this property to determine if the Assess feature should be enabled. If this property is not present, the decision is delegated to the Contrast UI.',
535
535
  },
536
+ {
537
+ name: 'assess.probabilistic_sampling.enable',
538
+ arg: '[true]',
539
+ default: false,
540
+ fn: castBoolean,
541
+ desc: 'Set to true to enable sampling of requests for dataflow and other Assess features',
542
+ },
543
+ {
544
+ name: 'assess.probabilistic_sampling.base_probability',
545
+ arg: '<probability>',
546
+ fn: (val) => {
547
+ const p = parseFloat(val);
548
+ if (p >= 0 && p <= 1) return p;
549
+
550
+ if (val && val != 'undefined') {
551
+ throw new Error('Invalid option: assess.probabilistic_sampling.base_probability', {
552
+ cause: `${val} is not not in interval 0 <= p <= 1. value as float: ${p}`
553
+ });
554
+ }
555
+ },
556
+ default: 0.01,
557
+ desc: 'A value p within the interval [0, 1]. Each request will share same probability p of being sampled.',
558
+ },
536
559
  {
537
560
  name: 'assess.tags',
538
561
  arg: '<tags>',
@@ -611,7 +634,7 @@ Example - \`label1, label2, label3\``,
611
634
  {
612
635
  name: 'application.name',
613
636
  arg: '<name>',
614
- desc: "Override the reported application name. Defaults to the `name` field from an application's `package.json`",
637
+ desc: 'Override the reported application name.',
615
638
  },
616
639
  {
617
640
  name: 'application.path',
@@ -619,33 +642,41 @@ Example - \`label1, label2, label3\``,
619
642
  default: '/',
620
643
  desc: 'Override the reported application path.',
621
644
  },
645
+ {
646
+ name: 'application.group',
647
+ arg: '<group>',
648
+ desc: 'Add the name of the application group with which this application should be associated in the Contrast UI.',
649
+ },
650
+ {
651
+ name: 'application.code',
652
+ arg: '<code>',
653
+ desc: 'Add the application code this application should use in the Contrast UI.'
654
+ },
622
655
  {
623
656
  name: 'application.version',
624
657
  arg: '<version>',
625
- desc: "Override the reported application version. Defaults to the `version` field from an application's `package.json`",
658
+ desc: 'Override the reported application version.',
659
+ },
660
+ {
661
+ name: 'application.tags',
662
+ arg: '<tags>',
663
+ desc: 'Apply labels to an application. Labels must be formatted as a comma-delimited list. Example - `label1,label2,label3`'
664
+ },
665
+ {
666
+ name: 'application.metadata',
667
+ arg: '<metadata>',
668
+ desc: 'Define a set of `key=value` pairs (which conforms to RFC 2253) for specifying user-defined metadata associated with the application. The set must be formatted as a comma-delimited list of `key=value` pairs. Example - `business-unit=accounting, office=Baltimore`',
626
669
  },
627
670
  {
628
671
  name: 'application.session_id',
629
672
  arg: '<session_id>',
630
- default: null,
631
673
  desc: 'Provide the ID of a session which already exists in the Contrast UI. Vulnerabilities discovered by the agent are associated with this session. If an invalid ID is supplied, the agent will be disabled. This option and `application.session_metadata` are mutually exclusive; if both are set, the agent will be disabled.',
632
674
  },
633
675
  {
634
676
  name: 'application.session_metadata',
635
677
  arg: '<session_metadata>',
636
- default: null,
637
678
  desc: 'Provide metadata which is used to create a new session ID in the Contrast UI. Vulnerabilities discovered by the agent are associated with this new session. This value should be formatted as `key=value` pairs (conforming to RFC 2253). Available key names for this configuration are branchName, buildNumber, commitHash, committer, gitTag, repository, testRun, and version. This option and `application.session_id` are mutually exclusive; if both are set the agent will be disabled.',
638
679
  },
639
- {
640
- name: 'application.group',
641
- arg: '<tags>',
642
- desc: "how to report the application's group for auto-grouping",
643
- },
644
- {
645
- name: 'application.metadata',
646
- arg: '<metadata>',
647
- desc: 'comma-separated list of key=value pairs that are applied to each application reported by the agent.',
648
- },
649
680
  // server
650
681
  {
651
682
  name: 'server.name',
@@ -686,10 +717,11 @@ Example - \`label1, label2, label3\``,
686
717
  fn: castBoolean,
687
718
  desc: 'Set to `false` to disable detection of cloud provider metadata such as resource identifiers.'
688
719
  },
689
- ].map((opt) => Object.assign(opt, {
690
- env: `CONTRAST__${opt.name.toUpperCase().replaceAll('.', '__')
691
- .replaceAll('-', '_')}`
692
- }));
720
+ ].map((opt) => {
721
+ let env = StringPrototypeReplaceAll.call(StringPrototypeToUpperCase.call(opt.name), '.', '__');
722
+ env = StringPrototypeReplaceAll.call(env, '-', '_');
723
+ return Object.assign(opt, { env: `CONTRAST__${env}` });
724
+ });
693
725
 
694
726
  module.exports = options;
695
727
  module.exports.clearBaseCase = clearBaseCase;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contrast/config",
3
- "version": "1.33.0",
3
+ "version": "1.35.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.25.0",
20
+ "@contrast/common": "1.26.0",
21
21
  "yaml": "^2.2.2"
22
22
  }
23
23
  }