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

const _ = require('lodash');

const logger = require('../../core/logger')('contrast:protect:rules');
const SignatureKit = require('./signatures');
const { PROTECTION_MODES, RULES } = require('./common');

const DEFAULT_SETTINGS = { serverFeatures: { defend: { enabled: false } } };

/**
 * Hash of constructors by rule id. Once a rule has been fully
 * implemented, we add its constructor path to provide support.
 */
const ctors = {
  // protect rules
  [RULES.CMD_INJECTION]: require('./cmd-injection/cmdinjection-rule'),
  [RULES.CMD_INJECTION_COMMAND_BACKDOORS]: require('./cmd-injection-command-backdoors/cmd-injection-command-backdoors-rule'),
  [RULES.CMD_INJECTION_SEMANTIC_CHAINED_COMMANDS]: require('./cmd-injection-semantic-chained-commands/cmd-injection-semantic-chained-commands-rule'),
  [RULES.CMD_INJECTION_SEMANTIC_DANGEROUS_PATHS]: require('./cmd-injection-semantic-dangerous-paths/cmd-injection-semantic-dangerous-paths-rule.js'),
  [RULES.METHOD_TAMPERING]: require('./method-tampering/method-tampering-rule'),
  [RULES.NOSQL_INJECTION]: require('./nosqli/nosql-injection-rule'),
  [RULES.PATH_TRAVERSAL]: require('./path-traversal/path-traversal-rule'),
  [RULES.REFLECTED_XSS]: require('./xss/reflected-xss-rule'),
  [RULES.SQL_INJECTION]: require('./sqli/sql-injection-rule'),
  [RULES.SSJS_INJECTION]: require('./ssjs-injection/ssjsinjection-rule'),
  [RULES.UNSAFE_FILE_UPLOAD]: require('./unsafe-file-upload/unsafe-file-upload-rule'),
  [RULES.UNTRUSTED_DESERIALIZATION]: require('./untrusted-deserialization/untrusted-deserialization-rule'),
  [RULES.XXE]: require('./xxe/xxerule.js'),
  // blocking rules
  [RULES.BOT_BLOCKER]: require('./bot-blocker/bot-blocker-rule'),
  [RULES.IP_DENYLIST]: require('./ip-denylist/ip-denylist-rule'),
  [RULES.VIRTUAL_PATCH]: require('./virtual-patch/virtual-patch-rule'),
};

class ProtectRuleFactory {
  constructor({ featureSet = DEFAULT_SETTINGS, enabled, agent }) {
    this.featureSet = featureSet;
    this.settings = {
      exceptions: []
    };
    this.agent = agent;
    this.policies = {};
    this.signatures = new SignatureKit();

    this.updateSettings(featureSet.applicationSettings, enabled);
    this.updateSettings(featureSet.serverFeatures, enabled);
  }

  /**
   * Used for when updates are provided by TS feature set.
   * @param {Object} settings ApplicationSettings or ServerFeatures
   * @param {Boolean} enabled flag to say whether protect is enabled(set from merge of config and TS Settings)
   */
  updateSettings(settings, enabled) {
    if (enabled !== undefined) {
      logger.info(`Protect mode ${enabled ? 'enabled' : 'disabled'}`);
      this.enabled = enabled;
    }

    // The `settings` argument can be either of these types:
    this.updateServerFeatures(settings);
    this.updateApplicationSettings(settings);

    this.updatePolicies();

    return this;
  }

  /**
   * Regenerates protect rule policies used to create rule instances. This will
   * run when updates are recieved from TS.
   */
  updatePolicies() {
    for (const id in ctors) {
      const policy = this.featureSet.getProtectRulePolicy(id);

      logger.info(`Protect rule ${id} in mode ${policy.mode}`);
      this.policies[id] = policy;
    }
  }

  /**
   * Reads the ServerFeatures from the settings and updates if found.
   * @param {Object} settings ApplicationSettings or ServerFeatures
   */
  updateServerFeatures(settings) {
    const definitionList = _.get(
      this,
      'featureSet.serverFeatures.defend.ruleDefinitionsList'
    );
    if (definitionList) {
      logger.info(`Definition list updated. Total: ${definitionList.length}`);
      this.signatures = new SignatureKit(definitionList);
    }

    const denylist = _.get(settings, 'defend.ipDenylistsList');
    if (denylist) {
      logger.info(`IP Denylist updated. Total: ${denylist.length}`);
    }
  }

  /**
   * Reads the ApplicationSettings from the settings and updates if found.
   * @param {Object} settings ApplicationSettings or ServerFeatures
   */
  updateApplicationSettings(settings) {
    const ruleSettings = _.get(settings, 'protectionRulesList');
    const virtualPatches = _.get(settings, 'virtualPatchesList');

    if (ruleSettings) {
      logger.info(
        `Defend protection rules updated. Total: ${ruleSettings.length}`
      );
    }

    if (virtualPatches) {
      logger.info('Defend Virtual Patches updated');
    }
  }

  /**
   * Will return a set of active rules for the current request.
   * @param   {String} mode Defaults to 'defend'
   * @returns {Rule[]}
   */
  getRules() {
    // This will disable the factory if settings state is invalid.
    if (!this.ensureValidSettingsState()) {
      return [];
    }

    // Blocking rules are in front so that they can run first. If
    // applicable they will then disable themselves.
    const rules = [
      this.IpDenylist(),
      this.BotBlocker(),
      ...this.VirtualPatchRules(),
      ...this.ProtectionRules()
    ];

    return rules.filter((rule) => rule.isEnabled());
  }

  /**
   * There are cases where protect may be enabled but the requisite settings
   * information has not been received from TS in order for the feature to be
   * fully functional. In these cases we want to log this fact and disable.
   * @returns {Boolean}
   */
  ensureValidSettingsState() {
    if (_.isEmpty(this.signatures) && this.enabled) {
      logger.info('Protect is disabled until rule definitions are obtained');
      this.enabled = false;
      return false;
    }

    return true;
  }

  /**
   * Creates IpDenylistRule.
   * @param   {Object[]} settings Rule settings from TS
   * @returns {IpDenylistRule}
   */
  IpDenylist() {
    const policy = this.policies[RULES.IP_DENYLIST];
    return new ctors[RULES.IP_DENYLIST](policy, this.agent);
  }

  /**
   * Creates BotBlockerRule.
   * @returns {[BotBlockerRule]}
   */
  BotBlocker() {
    const policy = this.policies[RULES.BOT_BLOCKER];
    return new ctors[RULES.BOT_BLOCKER](policy, this.agent);
  }

  /**
   * Creates and returns a collection of virtual patch rules.
   * @returns {Rule[]}
   */
  VirtualPatchRules() {
    return this.policies[RULES.VIRTUAL_PATCH].map(
      (policy) => new ctors[RULES.VIRTUAL_PATCH](policy, this.agent)
    );
  }

  /**
   * Creates Rule instances from the protection rule Settings
   * @returns {Rule[]}
   */
  ProtectionRules() {
    const enableRep = _.get(this, 'featureSet.config.protect.enable_rep');

    return Object.values(this.policies).reduce((memo, policy) => {
      const { id } = policy;

      // These are built and ordered specially
      if (
        id === RULES.VIRTUAL_PATCH ||
        id === RULES.BOT_BLOCKER ||
        id === RULES.IP_DENYLIST
      ) {
        return memo;
      }

      policy.enable_rep = enableRep;

      if (ProtectRuleFactory.shouldBuildRule(policy)) {
        const rule = new ctors[id](policy, this.agent);
        rule.signature = this.signatures.get(id);
        memo.push(rule);
      }

      return memo;
    }, []);
  }

  /**
   * Whether the factory should build a rule described by the given policy.
   * @param {Object} policy Derived from TS settings and configuration
   * @returns {Boolean}
   */
  static shouldBuildRule(policy) {
    return (
      policy.enable && // can be disabled in config
      policy.mode !== PROTECTION_MODES.NO_ACTION && // noop mode
      policy.id in ctors // unknown afa implementation
    );
  }
}

module.exports = ProtectRuleFactory;
module.exports.constructors = ctors;
