/**
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.
*/
'use strict';

/**
 * @module lib/FeatureSet
 */

const _ = require('lodash');

const logger = require('./core/logger');
const {
  PROTECTION_MODES,
  RULES,
  PROTECT_RULE_SUPPORTED_MODES
} = require('./constants');

const OFF = 'OFF';

/**
 * Provides an API for querying feature and policy data. Data sources are the
 * agent's configuration data and TS settings. Rules of precedence are accounted
 * for when returning feature/policy information.
 */
class FeatureSet {
  constructor(config = {}) {
    this._subscribers = [];
    this._logger = null;
    this.config = config;
    this.applicationSettings = {};
    this.serverFeatures = {};
  }

  get logger() {
    if (!this._logger) {
      try {
        this._logger = logger('contrast:feature-set');
      } catch (err) {
        // in case logger is not yet initialized, we can swallow this
      }
    }

    return this._logger;
  }

  /**
   * Update current application settings with new settings.
   * @param {Object} applicationSettings
   */
  updateApplicationSettings(applicationSettings) {
    if (applicationSettings) {
      if (applicationSettings.sessionId === '') {
        applicationSettings.sessionId = this.applicationSettings.sessionId;
      }
      _.merge(this.applicationSettings, applicationSettings);
      this.publishApplicationSettings(applicationSettings);
    }
  }

  /**
   * Update current server features with new settings.
   * @param {Object} serverFeatures
   */
  updateServerFeatures(serverFeatures) {
    if (serverFeatures) {
      _.merge(this.serverFeatures, serverFeatures);
      this.publishServerFeatures(serverFeatures);
    }
  }

  /**
   * Publish events that provide ApplicationSettings updates.
   */
  publishApplicationSettings(applicationSettings) {
    this._subscribers.forEach((subscriber) => {
      subscriber.emit('application-settings', applicationSettings);
    });
  }

  /**
   * Publish events that provide ServerFeature updates.
   */
  publishServerFeatures(serverFeatures) {
    const syslog = _.get(serverFeatures, 'defend.syslog');

    this._subscribers.forEach((subscriber) => {
      subscriber.emit('server-features', this.serverFeatures);

      if (syslog) {
        subscriber.emit('features.server.defend.syslog', syslog);
      }
    });
  }

  /**
   * This will resolve the `mode` of the Protect rule by looking in the
   * configuration and TS application settings. Config value will be used if
   * present, otherwise the value from TS settings will be returned. `NO_ACTION`
   * will be returned by default.
   * @param {String} ruleId Rule ID for which to get mode
   * @returns {common.PROTECTION_MODES}
   */
  getProtectRuleMode(ruleId) {
    let mode = _.get(
      this,
      `config.protect.rules.${ruleId}.mode`,
      ''
    ).toUpperCase();
    const supportedModes = _.get(PROTECT_RULE_SUPPORTED_MODES, ruleId, []);

    mode = mode === OFF ? PROTECTION_MODES.NO_ACTION : PROTECTION_MODES[mode];
    if (mode) {
      if (supportedModes.includes(mode)) {
        return mode;
      } else {
        if (this.logger) {
          this.logger.info(`Invalid mode configuration for ${ruleId}: ${mode}`);
        }
        mode = '';
      }
    }
    for (const ruleDef of _.get(
      this,
      'applicationSettings.protectionRulesList',
      []
    )) {
      if (ruleDef.id === ruleId) {
        ({ mode } = ruleDef);
        break;
      }
    }

    return mode || PROTECTION_MODES.NO_ACTION;
  }

  /**
   * Returns whether the rule having the provided id has been disabled. The
   * only way to explicitly disable a rule is via the configuration option:
   * `protect.rules.disabled_rules: 'reflected-xss'`
   * @param {String} ruleId Protect rule ID
   * @returns {Boolean}
   */
  isProtectRuleDisabled(id) {
    const disabledConfig = _.get(
      this,
      'config.protect.rules.disabled_rules',
      ''
    );

    return disabledConfig
      .split(',')
      .map((c) => c.trim())
      .some((disabledId) => id === disabledId);
  }

  /**
   * Reads agent configuration and TS settings and returns the Protect policy
   * for the Protect rule having the provided ID.
   * @param {String} ruleId The rule id for which to get policy information
   * @returns {Object}
   */
  /**
   * @returns {Object}
   */
  getProtectRulePolicy(ruleId) {
    if (ruleId === RULES.VIRTUAL_PATCH) {
      return this.getVirtualPatchPolicy();
    }
    if (ruleId === RULES.BOT_BLOCKER) {
      return this.getBotBlockerPolicy();
    }
    if (ruleId === RULES.IP_DENYLIST) {
      return this.getIpDenylistPolicy();
    }

    const ruleConfig = _.get(this.config, `protect.rules.${ruleId}`, {});
    const ruleSettings = _.get(
      this,
      'applicationSettings.protectionRulesList',
      []
    ).find(({ id }) => id === ruleId);
    const mode = this.getProtectRuleMode(ruleId);
    const enable = !!(
      !this.isProtectRuleDisabled(ruleId) && mode !== PROTECTION_MODES.NO_ACTION
    );

    return {
      ...(ruleSettings || {}),
      ...ruleConfig,
      id: ruleId,
      mode,
      enable
    };
  }

  /**
   * Reads agent configuration and TS settings and returns the Protect policy
   * for the IP Denylist rule.
   * @returns {Object}
   */
  getIpDenylistPolicy() {
    const config = this.getProtectRuleConfig(RULES.IP_DENYLIST);
    const settings = _.get(this, 'serverFeatures.defend.ipDenylistsList', []);

    return {
      id: RULES.IP_DENYLIST,
      enable: !config.disable && config.mode !== OFF && settings.length > 0,
      mode: PROTECTION_MODES.BLOCK_AT_PERIMETER,
      data: settings
    };
  }

  /**
   * Reads agent configuration and TS settings and returns the Protect policy
   * for the Virtual-Patch rules.
   * @returns {Object}
   */
  getVirtualPatchPolicy() {
    const config = this.getProtectRuleConfig(RULES.VIRTUAL_PATCH);
    const settings = _.get(this, 'applicationSettings.virtualPatchesList', []);

    return settings.map((setting) => ({
      id: RULES.VIRTUAL_PATCH,
      enable: !config.disable && settings.length > 0 && config.mode !== OFF,
      mode: PROTECTION_MODES.BLOCK_AT_PERIMETER,
      ...setting
    }));
  }

  /**
   * Reads agent configuration and TS settings and returns the Protect policy
   * for the Bot-Blocker rule.
   * @returns {Object}
   */
  getBotBlockerPolicy() {
    const config = this.getProtectRuleConfig(RULES.BOT_BLOCKER);
    const settings = {
      enable: _.get(this, 'serverFeatures.defend.botBlocker', false),
      data: _.get(this, 'serverFeatures.defend.botBlockersList', [])
    };

    return {
      id: RULES.BOT_BLOCKER,
      enable: !config.disable && config.mode !== OFF && settings.enable,
      mode: PROTECTION_MODES.BLOCK_AT_PERIMETER,
      data: settings.data
    };
  }

  /**
   * Returns the protect rules policy data from the agent's configuration.
   * @param {String} ruleId The rule id for which to return config
   * @returns {Object}
   */
  getProtectRuleConfig(ruleId) {
    const modeConfig = String(
      _.get(this, `config.protect.rules.${ruleId}`, '')
    ).toUpperCase();

    return {
      mode: modeConfig,
      disable: this.isProtectRuleDisabled(ruleId)
    };
  }

  /**
   * Convienience method for setting a mode for all Protect rules or for a
   * particular Protect rule.
   * @param {String} mode Set the rule(s) to this mode
   * @param {String} [ruleId] Set mode only for this rule ID.
   */
  setProtectMode(mode, ruleId) {
    for (const rule of _.get(
      this,
      'applicationSettings.protectionRulesList',
      []
    )) {
      if (!ruleId || (ruleId && rule.id === ruleId)) {
        rule.mode = mode;
      }
    }
  }
}

module.exports = FeatureSet;
