/**
Copyright: 2024 Contrast Security, Inc
  Contact: support@contrastsecurity.com
  License: Commercial

  NOTICE: This Software and the patented inventions embodied within may only be
  used as part of Contrast Security’s commercial offerings. Even though it is
  made available through public repositories, use of this Software is subject to
  the applicable End User Licensing Agreement found at
  https://www.contrastsecurity.com/enduser-terms-0317a or as otherwise agreed
  between Contrast Security and the End User. The Software may not be reverse
  engineered, modified, repackaged, sold, redistributed or otherwise used in a
  way not consistent with the End User License Agreement.
*/
/**
 * Sets up the agent config and CLI options. All options include a name and a description to be programatically
 * added to the commander program. Where the setting is not a boolean, they include args as well.
 *
 * The module currently houses all new common config settings.
 *
 * 	Other settings include:
 * 	  feature: a property in the TS feature set to tie the config option to
 * 		env: environment variable to check for value in
 * 		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.
 * 		enum: validation of whether type matches enumerated value
 *
 * NOTE: I'm not sure if validation should also be specified and handled here.
 *
 * TODO: add defaults to all new options
 * TODO: add mapping for TeamServer FeatureSet analogues where they differ
 *
 * @module lib/core/config/options
 */
'use strict';
const { Command } = require('commander');

const program = new Command();
program.allowUnknownOption();

const os = require('os');
const url = require('url');
const path = require('path');
const util = require('util');
const _ = require('lodash');

/**
 * Takes strings "true"|"t" or "false"|"f" (case insensitive) and return the appropriate boolean.
 * If we can't match one of the two words, return true;
 *
 * @param {boolean|string} value passed arg; never undefined or the function isn't called
 * @return {boolean}
 */
function castBoolean(value) {
  if (typeof value === 'boolean') {
    return value;
  }

  if (typeof value === 'string') {
    value = value.toLowerCase();
    if (value === 'true' || value === 't') {
      return true;
    }

    if (value === 'false' || value === 'f') {
      return false;
    }
  }

  return undefined;
}

/**
 * Takes string path and resolves absolute path based on current working dir
 *
 * @param {string} value passed arg; never undefined or the function isn't called
 * @return {string} absolute path resolve from process.cwd()
 */
function toAbsolutePath(value) {
  return value ? path.resolve(process.cwd(), String(value)) : undefined;
}

function clearBaseCase(val) {
  let isBase;
  switch (typeof val) {
    case 'string':
      isBase = val === '';
      break;
    case 'number':
      isBase = isNaN(val);
      break;
  }

  if (!isBase) {
    return val;
  }
}

const uppercase = (val) => clearBaseCase(_.upperCase(val));
const lowercase = (val) => clearBaseCase(_.lowerCase(val));
const parseNum = (val) => {
  const float = parseFloat(val);
  return clearBaseCase(Math.ceil(float));
};

const misc = [
  {
    name: 'configFile',
    abbrev: 'c',
    // special case this guy because it should be settable via ENV
    env: 'CONTRAST_CONFIG_PATH',
    arg: '<path>',
    desc: 'set config file location. defaults to <app_root>/contrast_security.yaml',
  },
];

const api = [
  {
    name: 'enable',
    arg: '[false]',
    fn: castBoolean,
    default: true,
    desc: 'set false to disable reporting',
  },
  {
    name: 'api.api_key',
    env: 'CONTRASTSECURITY_API_KEY',
    arg: '<key>',
    desc: 'the organization API key',
    required: true,
  },
  {
    name: 'api.service_key',
    env: 'CONTRASTSECURITY_SECRET_KEY',
    arg: '<key>',
    desc: 'account service key',
    required: true,
  },
  {
    name: 'api.url',
    env: 'CONTRASTSECURITY_URL',
    arg: '<url>',
    default: 'https://app.contrastsecurity.com/Contrast',
    // The old json spec used to expect that the url would not end in /Contrast
    // Common config expects this to be there. So, we need to do a bit of massaging
    // to make sure our reporter gets a consistent URL one way or the other.
    fn: (value, logger) => {
      if (value == undefined) {
        return value;
      }

      value = String(value);
      let uri;
      try {
        uri = new url.URL(value);
      } catch (e) {
        // the url customer provided is invalid, return null and this will eventually
        // fail when trying to talk to TS
        logger.error('api.url is invalid %s', value);
        return null;
      }

      if (uri.pathname) {
        // kill trailling /
        uri.pathname = uri.pathname.replace(/\/+$/, '');

        if (!uri.pathname.endsWith('Contrast')) {
          uri.pathname += 'Contrast';
        }

        return url.format(uri);
      }
      return value;
    },
    desc: 'url to report on',
    required: true,
  },
  {
    name: 'api.user_name',
    env: 'CONTRASTSECURITY_UID',
    arg: '<name>',
    desc: 'account user name',
    required: true,
  },

  {
    name: 'api.certificate.enable',
    arg: '[false]',
    default: 'false',
    desc: 'If set to false, the certificate configuration in this section will be ignored.',
  },
  {
    name: 'api.certificate.ca_file',
    arg: '<path>',
    desc: 'Set the absolute or relative path to a CA for communication with Contrast UI using a self-signed certificate.',
  },
  {
    name: 'api.certificate.cert_file',
    arg: '<path>',
    desc: 'Set the absolute or relative path to the Certificate PEM file for communication with Contrast UI',
  },
  {
    name: 'api.certificate.key_file',
    arg: '<path>',
    desc: 'Set the absolute or relative path to the Key PEM file for communication with Contrast UI',
  },
  {
    name: 'api.certificate.key_password',
    arg: '<passphrase>',
    desc: 'If the Key file requires a password, set it here',
  },
  {
    name: 'api.certificate.ignore_cert_errors',
    arg: '[true]',
    default: true,
    desc: 'When set to `true`, the agent ignores certificate verification errors when the agent communicates with the Contrast UI.',
  },
  {
    name: 'api.proxy.enable',
    arg: '[true]',
    default: false,
    desc: 'if false, no proxy is being used for communication of data',
  },
  {
    name: 'api.proxy.url',
    arg: '<url>',
    desc: 'url of proxy for communicating agent data',
  },
  {
    name: 'api.timeout_ms',
    arg: '<ms>',
    default: 10000,
    fn: parseNum,
    desc: 'http timeout value (in ms)',
  },
];

const agent = [
  {
    name: 'agent.diagnostics.enable',
    arg: '[false]',
    default: true,
    fn: castBoolean,
    desc: 'If true the agent will try to create both diagnostic files at startup',
  },
  {
    name: 'agent.diagnostics.quiet',
    arg: '[false]',
    default: true,
    fn: castBoolean,
    desc: 'If true the agent will print all diagnostic results to stdout as well',
  },
  {
    name: 'agent.diagnostics.report_path',
    arg: '<path>',
    desc: 'path indicating where to report all diagnostics results',
  },
  {
    name: 'agent.logger.append',
    arg: '[false]',
    fn: castBoolean,
    default: true,
    desc: 'if false, create a new log file on startup instead of appending and rolling daily',
  },
  {
    name: 'agent.logger.level',
    arg: '<level>',
    fn: lowercase,
    enum: ['error', 'warn', 'info', 'debug', 'trace'],
    default: 'error',
    desc: 'logging level (error, warn, info, debug, trace). overrides FeatureSet:logLevel',
  },
  {
    name: 'agent.logger.path',
    default: 'node-contrast',
    fn: toAbsolutePath,
    arg: '<path>',
    desc: 'where contrast will put its debug log',
  },
  {
    name: 'agent.logger.stdout',
    arg: '[false]',
    fn: castBoolean,
    default: true,
    desc: 'if false, suppress output to STDOUT',
  },
  {
    name: 'agent.node.dataflow_monitor',
    arg: '[true]',
    default: false,
    fn: castBoolean,
    desc: 'throw all propagation event details into the AsyncStorage context and spit them to a log file when the request ends',
  },
  {
    name: 'agent.node.array_request_sampling.enable',
    arg: '[false]',
    default: false,
    fn: castBoolean,
    desc: 'enable sampling of array members for dataflow',
  },
  {
    name: 'agent.node.array_request_sampling.threshold',
    arg: '<num>',
    default: 50,
    fn: parseNum,
    desc: 'after reaching the threshold, the agent will not continue to track array members for dataflow',
  },
  {
    name: 'agent.node.array_request_sampling.interval',
    arg: '<num>',
    default: 5,
    fn: parseNum,
    desc: 'adjust the array member sampling interval. until the threshold is reached, the agent will track 1 in every <interval> member for dataflow',
  },
  {
    name: 'agent.node.enable_catch_log',
    arg: '[true]',
    fn: castBoolean,
    default: false,
    desc: 'enable source rewriting of try/catch blocks so that Contrast can log caught errors for debugging',
  },
  {
    name: 'agent.node.enable_property_descriptor_tracking',
    arg: '[false]',
    fn: castBoolean,
    default: false,
    desc: 'enable tracking the value property returned by getOwnPropertyDescriptor',
  },
  {
    name: 'agent.node.enable_rewrite',
    arg: '[false]',
    fn: castBoolean,
    default: true,
    desc: 'if false, disable source rewriting (not recommended)',
  },
  {
    name: 'agent.node.rewrite_cache.enable',
    arg: '[true]',
    fn: castBoolean,
    default: true,
    desc: 'if false, disable caching source rewriting (degrades startup performance)',
  },
  {
    name: 'agent.node.rewrite_cache.path',
    arg: '<path>',
    desc: 'set the location of the source rewriting cache',
  },
  {
    name: 'agent.node.enable_source_maps',
    arg: '[false]',
    fn: castBoolean,
    default: true,
    desc: 'enable source map support in reporting',
  },
  {
    name: 'agent.node.enable_native_rewrite_log',
    arg: '[true]',
    fn: castBoolean,
    default: false,
    desc: 'log contents of native modules that have been rewritten',
  },
  {
    name: 'agent.node.enable_rewrite_log',
    arg: '[true]',
    fn: castBoolean,
    default: false,
    desc: 'log contents of modules that have been rewritten for debugging purposes',
  },
  {
    name: 'agent.node.enable_sources_in_deadzones',
    arg: '[false]',
    fn: castBoolean,
    default: false,
    desc: 'create Source Events in deadzoned code',
  },
  {
    name: 'agent.node.library_usage.read_extraneous_libraries',
    arg: '[true]',
    fn: castBoolean,
    default: false,
    desc: 'if true, read libraries marked by npm as extraneous',
  },
  {
    name: 'agent.node.library_usage.reporting.interval',
    arg: '<num>',
    fn: parseNum,
    default: 1,
    desc: 'frequency of collecting code events for library usage in milliseconds, defaults to 1 ms',
  },
  {
    name: 'agent.node.library_usage.reporting.enable',
    arg: '[false]',
    default: true,
    fn: castBoolean,
    desc: 'add enhanced library usage features (i.e. scanning for composition of dependencies, reporting usage)',
  },
  {
    name: 'agent.node.app_root',
    arg: '<path>',
    desc: "set location to look for the app's package.json",
  },
  {
    name: 'agent.node.req_perf_logging',
    arg: '[false]',
    default: false,
    desc: 'enable debug logging of instrumentation overhead on a per request basis',
  },
  {
    name: 'agent.node.skip_nested_tracking',
    arg: '[true]',
    fn: castBoolean,
    default: false,
    desc: 'do not traverse nested properties to look for tracking during propagation (not recommended)',
  },
  {
    name: 'agent.node.speedracer_input_analysis',
    arg: '[false]',
    default: true,
    fn: castBoolean,
    desc: 'Should be set to [true] in order to communicate with TeamServer. The place of input analysis depends on the value of agent.node.native_input_analysis',
  },
  {
    name: 'agent.node.native_input_analysis',
    arg: '[true]',
    default: false,
    fn: castBoolean,
    desc: 'if true - input analysis is done via agent-lib, otherwise it is done by SpeedRacer',
  },
  {
    name: 'agent.node.analysis_log_dir',
    arg: '<path>',
    default: '.',
    desc: 'directory to use for the native input analysis log file'
  },
  {
    name: 'agent.node.unsafe.deadzones',
    arg: '<modules>',
    fn: (str) => {
      if (str) {
        return str.split(',').map((s) => s.trim());
      }
    },
    desc: 'Add a comma-separated list of values to the deadzone regardless of its presence in the policy file.',
    default: '',
  },
  {
    name: 'agent.node.unsafe.sr_max_body_size',
    arg: '<size in mb>',
    default: 16,
    desc: 'set the maximum body size that will be sent to speedracer for input analysis',
  },
  {
    // SEE NODE-2886
    name: 'agent.node.cmd_ignore_list',
    arg: '<commands>',
    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'
  },
  {
    // SEE NODE-2886
    name: 'agent.node.exclusive_entrypoint',
    arg: '<entrypoint.js>',
    desc: 'an entrypoint for an application that, when specified, will prevent the agent instrumenting on anything else'
  },
  {
    // SEE NODE-3299
    name: 'agent.node.npm_path',
    arg: '</path/to/npm>',
    default: 'npm',
    desc: 'The full path of the npm executable, used for library analysis'
  },
  {
    name: 'agent.heap_dump.enable',
    arg: '[true]',
    fn: castBoolean,
    default: false,
    desc: 'Every n seconds create a heap snapshot of the current process',
  },
  {
    name: 'agent.heap_dump.path',
    arg: '<path>',
    default: 'contrast_heap_dumps',
    desc: "Set the location to which to save the heap dump files. If relative, the path is determined based on the process' working directory.",
  },
  {
    name: 'agent.heap_dump.delay_ms',
    arg: '<time>',
    default: 10000,
    desc: 'Set the amount of time to wait, in milliseconds, after agent startup to begin taking heap dumps.',
  },
  {
    name: 'agent.heap_dump.window_ms',
    arg: '<number>',
    default: 30000,
    desc: 'How frequently the heap snapshot is created',
  },
  {
    name: 'agent.heap_dump.count',
    arg: '<number>',
    default: 5,
    desc: 'Set the number of heap dumps to take before disabling this feature.',
  },
  {
    name: 'agent.stack_trace_limit',
    arg: '<limit>',
    default: 10,
    fn: parseNum,
    desc: 'set limit for stack trace size (larger limits will improve accuracy but increase memory usage)',
  },
  {
    name: 'agent.traverse_and_track',
    arg: '<traverse-and-track>',
    default: false,
    fn: (val, logger) => {
      if (val) {
        logger.error('agent.traverse_and_track option is deprecated. Please use assess.enable_lazy_tracking from now on. It\'s value should be the opposite of this one');
      }
      return val;
    },
    desc: 'source membrane alternative (DEPRECATED)',
  },
  {
    name: 'agent.polling.app_activity_ms',
    arg: '<ms>',
    default: 30000,
    fn: parseNum,
    desc: 'how often (in ms), application activity messages are sent',
  },
  {
    name: 'agent.polling.app_update_ms',
    arg: '<ms>',
    default: 60000,
    fn: parseNum,
    desc: 'how often (in ms), application update messages (libraries, technologies, etc.) are sent',
  },
  {
    name: 'agent.route_coverage.enable',
    arg: '[true]',
    default: true,
    fn: castBoolean,
    desc: 'if false, do not send coverage data to the Contrast UI',
  },
  {
    name: 'agent.security_logger.level',
    arg: '<level>',
    fn: lowercase,
    enum: ['error', 'warn', 'debug', 'info', 'trace'],
    default: 'error',
    desc: 'security logging level (error, warn, debug, info, trace)',
  },
  {
    name: 'agent.security_logger.path',
    default: 'security',
    fn: toAbsolutePath,
    arg: '<path>',
    desc: 'where to log security events',
  },
  {
    name: 'agent.security_logger.syslog.enable',
    fn: castBoolean,
    desc: 'Set to true to enable Syslog logging',
  },
  {
    name: 'agent.security_logger.syslog.ip',
    desc: 'Set the IP address of the Syslog server to which the agent should send messages',
    arg: '<ip>',
  },
  {
    name: 'agent.security_logger.syslog.port',
    desc: 'Set the port of the Syslog server to which the agent should send messages',
    arg: '<port>',
  },
  {
    name: 'agent.security_logger.syslog.facility',
    desc: 'Set the facility code of the messages the agent sends to Syslog',
    enum: [
      0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
      21, 22, 23,
    ],
    arg: '<facility>',
  },
  {
    name: 'agent.security_logger.syslog.severity_exploited',
    desc: 'Set the log level of Exploited attacks. Value options are ALERT/CRITICAL/ERROR/WARNING/NOTICE/INFO/DEBUG',
    enum: ['alert', 'critical', 'error', 'warning', 'notice', 'info', 'debug'],
    arg: '<level>',
    default: 'notice',
    fn: lowercase,
  },
  {
    name: 'agent.security_logger.syslog.severity_blocked',
    desc: 'Set the log level of Exploited attacks. Value options are ALERT/CRITICAL/ERROR/WARNING/NOTICE/INFO/DEBUG',
    enum: ['alert', 'critical', 'error', 'warning', 'notice', 'info', 'debug'],
    arg: '<level>',
    default: 'notice',
    fn: lowercase,
  },
  {
    name: 'agent.security_logger.syslog.severity_probed',
    desc: 'Set the log level of Probed attacks. Value options are ALERT/CRITICAL/ERROR/WARNING/NOTICE/INFO/DEBUG',
    enum: ['alert', 'critical', 'error', 'warning', 'notice', 'info', 'debug'],
    arg: '<level>',
    default: 'warning',
    fn: lowercase,
  },
  {
    name: 'agent.logger.log_outbound_http',
    arg: '[true]',
    default: false,
    desc: 'log all outbound http responses',
  },
  {
    name: 'agent.logger.backups',
    arg: '<number>',
    desc: "set the max number of log files or days to keep files. For days, add 'd' after number",
  },
  {
    name: 'agent.logger.roll_size',
    arg: '<size>',
    desc: "set the maximum file size in bytes for logs. Add 'k', 'm', 'g' after number for Kb, Mb, Gb",
  },
  {
    name: 'agent.service.socket',
    arg: '<socket>',
    desc: 'If this property is defined, the service is listening on a Unix socket at the defined path',
  },
  {
    name: 'agent.service.host',
    arg: '<host>',
    default: '127.0.0.1',
    desc: 'set the the hostname or IP address of the Contrast service to which the Contrast agent should report',
  },
  {
    name: 'agent.service.port',
    arg: '<port>',
    default: 30555,
    desc: 'set the the port of the Contrast service to which the Contrast agent should report',
  },
  {
    name: 'agent.service.grpc',
    arg: '[true]',
    default: false,
    fn: castBoolean,
    desc: 'set to `true` to enable communication to speedracer via GRPC.',
  },
];

const application = [
  {
    name: 'application.args',
    arg: '<args>',
    default: [],
    fn: (val) => {
      if (Array.isArray(val)) {
        return val;
      }

      if (typeof val === 'string') {
        return val.split(' ');
      }

      return undefined;
    },
    desc: 'string containing args to pass verbatim to the application, eg --application.args "-A -S -D -F foo bar"',
  },
  {
    name: 'application.code',
    arg: '<code>',
    desc: 'add the application code this application should use in the Contrast UI',
  },
  {
    name: 'application.group',
    arg: '<tags>',
    desc: "how to report the application's group for auto-grouping",
  },
  {
    name: 'application.language',
    arg: '<language>',
    default: 'Node',
    desc: "override the reported application language (if different from 'Node')",
  },
  {
    name: 'application.name',
    arg: '<name>',
    env: 'CONTRASTSECURITY_APP_NAME',
    desc: 'override the reported application name. (default: package.json:name)',
  },
  {
    name: 'application.path',
    arg: '<path>',
    default: '/',
    desc: 'override the reported application path',
  },
  {
    name: 'application.tags',
    arg: '<tags>',
    desc: 'comma-separated list of tags to apply to each application reported by the agent',
  },
  {
    name: 'application.metadata',
    args: '<metadata>',
    desc: 'comma-separated list of key=value pairs that are applied to each application reported by the agent.',
  },
  {
    name: 'application.version',
    arg: '<version>',
    desc: "override the reported application version (if different from 'version' field in the application's package.json)",
  },
  {
    name: 'application.session_id',
    arg: '<session_id>',
    default: null,
    desc: 'provide the ID of a session existing within Contrast UI',
  },
  {
    name: 'application.session_metadata',
    arg: '<session_metadata>',
    default: null,
    desc: 'provide metadata used to create a new session within Contrast UI',
  },
];

const inventory = [
  {
    name: 'inventory.analyze_libraries',
    env: 'INVENTORY__ANALYZE_LIBRARIES',
    arg: '[false]',
    fn: castBoolean,
    default: true,
    desc: 'Set to false to disable reading or reporting libraries used by the application',
  },
  {
    name: 'inventory.tags',
    arg: '<tags>',
    desc: 'comma-separated list of tags to apply to each application library reported by the agent',
  },
  {
    name: 'inventory.enable',
    arg: '[false]',
    fn: castBoolean,
    default: true,
    desc: 'Set to `false` to disable inventory features in the agent.',
  },
];

const assess = [
  {
    name: 'assess.enable',
    arg: '[false]',
    fn: castBoolean,
    desc: 'if false, disable assess for this agent. A restart is required to re-enable',
  },
  {
    name: 'assess.trust_custom_validators',
    arg: '<trust-custom-validators>',
    default: false,
    desc: 'trust incoming strings when they pass custom validators (Mongoose, Joi)',
  },
  {
    name: 'assess.enable_sanitizer_serve_static',
    arg: '<enable-sanitizer-serve-static>',
    default: true,
    fn: castBoolean,
    desc: 'Enables the serve-static module as a path-traversal sanitizer. Express uses serve-static in a safe way but manual setup of serve-static can be vulnerable. Even with Express there is a possibility for "traversing-down" the served folder or user misconfiguration if not configured with an absolute path',
  },
  {
    name: 'assess.enable_preflight',
    arg: '[false]',
    fn: castBoolean,
    default: true,
    desc: 'if false, disable preflight spooling of traces (not recommended)',
  },
  {
    name: 'assess.enable_propagators',
    arg: '[false]',
    fn: castBoolean,
    default: true,
    desc: 'if false, disable dataflow propagation (not recommended)',
  },
  {
    name: 'assess.sampling.enable',
    arg: '[false]',
    fn: castBoolean,
    default: true,
    desc: 'if false, disable sampling',
  },
  {
    name: 'assess.sampling.baseline',
    arg: '<rule limit>',
    fn: parseNum,
    default: 5,
    desc: 'max number of times to report the same rule for a single',
  },
  {
    name: 'assess.tags',
    arg: '<tags>',
    desc: 'comma-separated list of tags to apply to each application vulnerability reported by the agent',
  },
  {
    name: 'assess.enable_lazy_tracking',
    arg: '[true]',
    default: true,
    fn: castBoolean,
    desc: 'When set to `false` won\'t track the source events lazily but will track the first up to 250 source events',
  },
  {
    name: 'assess.enable_lazy_stacktraces',
    arg: '[false]',
    default: false,
    fn: castBoolean,
    desc: 'When set to `true` it will evaluate stacktraces lazily, this improves performance, but causes a memory leak if used in a longer running application',
  },
];

const protect = [
  {
    name: 'protect.enable',
    arg: '[false]',
    fn: castBoolean,
    desc: 'if false, disable protect for this agent',
  },
  {
    name: 'protect.enable_rep',
    arg: '[false]',
    default: true,
    fn: castBoolean,
    desc: 'if false, disable Runtime Exploit Prevention(REP) for this agent',
  },
  {
    name: 'protect.auth.mode',
    arg: '<mode>',
    default: 'OFF',
    enum: ['OFF', 'MONITOR'],
    desc: 'whether to report authentication framework login attempts (OFF, MONITOR)',
  },
  {
    name: 'protect.samples.blocked',
    arg: '<count>',
    fn: parseNum,
    default: 25,
    desc: 'limit the reporting of "blocked" protect events to this number (per report cycle)',
  },
  {
    name: 'protect.samples.blocked_at_perimeter',
    arg: '<count>',
    fn: parseNum,
    default: 25,
    desc: 'limit the reporting of "blocked-at-perim" protect events to this number (per report cycle)',
  },
  {
    name: 'protect.samples.exploited',
    arg: '<count>',
    fn: parseNum,
    default: 100,
    desc: 'limit the reporting of "effective" protect events to this number (per report cycle)',
  },
  {
    name: 'protect.samples.probed',
    arg: '<count>',
    fn: parseNum,
    default: 50,
    desc: 'limit the reporting of "ineffective" protect events to this number (per report cycle)',
  },
  {
    name: 'protect.rules.disabled_rules',
    arg: '<rules>',
    default: '',
    desc: 'comma-delimited list of rule IDs to disable',
  },
  ...Object.values(require('../../constants').RULES).map((ruleId) => ({
    name: `protect.rules.${ruleId}.mode`,
    arg: '<mode>',
    enum: ['monitor', 'block', 'block_at_perimeter', 'off'],
    desc: `the mode in which to run the ${ruleId} rule`,
  })),
];

const server = [
  {
    name: 'server.build',
    arg: '<version>',
    desc: 'set reported server build option',
  },
  {
    name: 'server.environment',
    arg: '<name>',
    fn: uppercase,
    // enum: ['QA', 'PRODUCTION', 'DEVELOPMENT'], none of the other agents validate this
    desc: 'environment the server is running in (QA, PRODUCTION, or DEVELOPMENT)',
  },
  {
    name: 'server.name',
    arg: '<name>',
    default: os.hostname(),
    desc: 'override the reported server name',
  },
  {
    name: 'server.path',
    arg: '<name>',
    desc: 'override the reported server path',
  },
  {
    name: 'server.tags',
    arg: '<tags>',
    desc: 'comma-separated list of tags to apply to each server reported by the agent',
  },
  {
    name: 'server.type',
    arg: '<type>',
    default: util.format('node.js %s', process.version),
    desc: 'override the reported server type',
  },
  {
    name: 'server.version',
    arg: '<version>',
    desc: "override the reported server version (if different from 'version' field in the application's package.json)",
  },
];

let dev = [];
if (process.env.CONTRAST_DEV) {
  dev = [
    {
      name: 'agent.node.dev.global_agent',
      arg: '[true]',
      default: true,
      desc: 'add global.contrast_agent so that apps can reference agent and its emitters',
    },
    {
      name: 'agent.node.dev.global_tracker',
      arg: '[true]',
      default: true,
      desc: 'add global.contrast_tracker so that apps can reference dataflow metadata',
    },
    {
      name: 'agent.node.dev.metrics',
      arg: '[false]',
      default: false,
      desc: 'add tracking and periodically outputting diagnostic metrics to a file',
    },
    {
      name: 'agent.node.dev.screener.enable',
      arg: '[false]',
      default: false,
      desc: 'use message queue for (most) reporting rather than send to SR',
    },
    {
      name: 'agent.node.dev.screener.mq_location',
      arg: '<location>',
      default: 'amqp://localhost',
      desc: 'location of the messaging queue server to which to publish messages',
    },
    {
      name: 'agent.node.dev.screener.exchange_name',
      arg: '<name>',
      desc: 'name of the exchange to which to publish messages (unique per test run)',
    },
    {
      name: 'agent.node.dev.custom_policy_path',
      arg: '<path>',
      fn: toAbsolutePath,
      desc: 'provide an additional, custom policy',
    },
    {
      name: 'agent.node.unsafe.generate_umbrella',
      arg: '[true]',
      fn: castBoolean,
      default: false,
      desc: 'Generate Umbrella test-cases based on what was found by the agent into umbrella_test.json',
    },
  ];
}

const options = [].concat(
  misc,
  api,
  agent,
  application,
  assess,
  inventory,
  protect,
  server,
  dev,
);

// run
program.description('run the application with the Contrast Agent.');
options.forEach((option) => {
  let name = '';
  if (option.abbrev) {
    name = `-${option.abbrev}, `;
  }

  name = `${name}--${option.name}`;

  if (option.default) {
    option.desc = `${option.desc}. (default: ${option.default})`;
  }

  const arg = option.arg ? `=${option.arg}` : '';
  const envVar = `CONTRAST__${option.name.toUpperCase().replace(/\./g, '__')}`;
  option.desc = `${option.desc}\n\t${envVar}${arg}`;

  if (option.arg) {
    name = `${name} ${option.arg}`;
  }

  // program.option(name, option.desc, option.fn, option.default);
  // don't actually add fn and default as arguments here;
  // we want to apply these ourselves
  program.option(name, option.desc);
});

function getDefault(optionName) {
  let option;
  options.forEach((entry) => {
    if (entry.name === optionName) {
      option = entry;
    }
  });
  return option.default;
}

module.exports.options = options;
module.exports.program = program;
module.exports.clearBaseCase = clearBaseCase;
module.exports.castBoolean = castBoolean;
module.exports.getDefault = getDefault;
