/**
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/rules/protect/Rule
 */
const _ = require('lodash');

const logger = require('../../core/logger')('contrast:rules:protect');
const SecurityException = require('../errors/security-exception');
const {
  ATTACK_STATUS,
  IMPORTANCE,
  PROTECTION_MODES: { NO_ACTION, BLOCK, BLOCK_AT_PERIMETER }
} = require('./common');

class Rule {
  /**
   * Rules that need to make use of the agent lib can also take
   * a second argument: `agent' which is a reference to the agent object.
   */
  constructor(policy, agent) {
    if (!policy) {
      policy = { mode: NO_ACTION };
    }
    this.policy = policy;

    this.blockAtEntry = policy.mode === BLOCK_AT_PERIMETER;
    this.id = policy.id;
    this.mode = policy.mode;
    this.name = policy.name;
    this.applicableInputs = [];
    this.applicableSinks = [];
    this.agent = agent;
    const config = (this.agent && this.agent.config) || { agent: { node: {} } };

    this.agentLibEnabled =
      config.agent.node.native_input_analysis &&
      config.agent.node.speedracer_input_analysis;

    if (this.agentLibEnabled) {
      this.evaluator = null;
    }

    // override this value if the rule uses agent library input analysis.
    // all rules that agentLibBit applies to use this field with the single
    // exception of nosqli which uses node input analysis.
    this.usesLibInputAnalysis = false;

    // Samples --> {}
    // This lets us manage non-sample state for each rule. This
    // will go away when we go back to storing state in rules.
    this.storage = new WeakMap();
  }

  /**
   * returns the storage we use for inputs for a single request
   */
  getStorage(samples) {
    if (this.storage.has(samples)) {
      return this.storage.get(samples);
    }

    const s = {};
    this.storage.set(samples, s);
    return s;
  }

  /**
   * Whether the rules applies to the provided input type.
   * When an InputExclusion applies to the current context, the name
   * argument will be used to provide a more granular level of detail
   * for certain input types. In that case, the implementation of
   * this method will be provided by decoration.
   * @param   {String}  type The type of input, e.g. HEADER, COOKIE
   * @param   {String}  name Specific name for type, e.g. User-Agent
   * @returns {Boolean}
   */
  appliesToInputType(type, name) {
    return this.applicableInputs.length === 0 || this.applicableInputs.includes(type);
  }

  /**
   * Whether the sink applies to a given rule's applicableSinks
   *
   * @param {string} type The type of input for a sink e.g. PATH, CODE_ENV
   * @returns {Boolean}
   */
  appliesToSink(type) {
    return this.applicableSinks.includes(type);
  }

  /**
   * Whether the input meets the minimum length requirements for the rule.
   * @param   {String}  input Input string to check
   * @returns {Boolean}
   */
  inputLongEnough(input) {
    return input.length >= this.minimumInputLength;
  }

  /**
   * Returns the main evaluator for this rule. The evaluator applies
   * all of the rule's tests, e.g. pattern, keyword, custom searchers.
   * @returns {Object}
   */
  getEvaluator() {
    if (this.signature) {
      return this.signature;
    }

    return this.evaluator;
  }

  /**
   * Whether the rule is enabled. The rule could be disabled globally
   * by TS, or for a specific request because of an exclusion filter.
   * In the latter case the exclusion module will have decorated this.
   */
  isEnabled() {
    return this.policy.enable;
  }

  /**
   * If in a blocking mode, blocks a request by throwing a SecurityException.
   * The abstract source provider will be able to handle the error
   * appropriately for the application's web-server framework.
   *
   * @throws {SecurityException}
   */
  blockRequest(sample) {
    if (!this.blocks()) {
      return;
    }
    sample.blocked = true;

    const {
      id,
      policy: { mode }
    } = this;

    const name = sample.input ? sample.input.name : '<anon>';
    const type = sample.input ? sample.input.type : '<no-type>';

    // TODO: should this be ||?
    if (!this.blockAtEntry && !sample.effective) {
      return;
    }

    logger.info(`${mode} - Request blocked for ${id} - ${type}:${name}`);
    throw new SecurityException();
  }

  /**
   * Whether this rule is EVER blocking - at sink OR perimeter.
   * @returns {Boolean}
   */
  blocks() {
    const { mode } = this.policy;
    return mode === BLOCK || mode === BLOCK_AT_PERIMETER;
  }

  /**
   * Evaluate the input(s) at the source and build an attack assessment.
   * Depends only on inputs and statically configured rules.
   *
   * @param {UserInput[]} inputs Potentially dangerous user inputs
   * @param {ApplicationContext} event Event published by instrumentation hooks
   * @param {Samples} samples Async-local samples collection
   */
  preFilterUserInput(input, event, samples) {
    const evaluator = this.getEvaluator();

    if (!evaluator) {
      return;
    }

    // This is to support the newer API accepting a UserInput,
    let evaluation;
    if (this.signature) {
      evaluation = evaluator.evaluate(input);
    } else {
      // but we can still use the old one and just pass strings.
      evaluation = evaluator.evaluate(input.value);
    }

    const sample = this.createAndSaveSample({
      input,
      evaluation,
      event,
      samples
    });

    if (sample && sample.confirmedAttack === true && this.mode === BLOCK_AT_PERIMETER) {
      this.blockRequest(sample);
    }
  }

  /**
   * Evaluates an event in the most-common of ways. The data for the event
   * is presumed to be a string. We iterate over all of our samples collected
   * and check the event data string for sample values.
   * @param {Object} params
   * @param {SinkEvent} params.event
   * @param {Set} params.applicableSamples samples applicable to rule id
   */
  evaluateAtSink({ event, applicableSamples }) {
    for (const sample of applicableSamples) {
      const result =
        event.data.indexOf(_.get(sample, 'input.value')) < 0
          ? null
          : event.data;

      if (result) {
        this.appendAttackDetails(sample, result);
        sample.captureAppContext(event);
        logger.warn(`EFFECTIVE - rule: ${this.id}, mode: ${this.mode}`);
        this.blockRequest(sample);
      }
    }
  }

  /**
   * Creates and saves a sample. This can be called for source and sink events.
   * @param {Object} opts
   * @param {Object} opts.input
   * @param {Object} opts.evaluation
   * @param {string} opts.classification
   * @param {Object} opts.event
   * @param {Request} opts.request
   * @param {Samples} opts.samples
   * @returns {}
   */
  createAndSaveSample({
    input,
    attributes,
    evaluation,
    classification,
    event,
    request,
    samples
  }) {
    const { id } = this;
    const { name, type, value } = input;
    const classifiedAs = _.get(
      evaluation,
      'results.importance',
      classification
    );

    logger.debug(`${classifiedAs}: ${id} - ${type}.${name}.${value}`);
    if (classifiedAs === IMPORTANCE.NONE) {
      return null;
    }

    const sample = samples.addRuleSample({
      id,
      attributes,
      input,
      classification: classifiedAs,
      evaluation,
      appContext: event
    });

    return sample;
  }

  /**
   * Builds the base event for which the security logger will use
   * to log to either syslog or file system.
   * @param   {Sample} sample The sample collected via analysis
   * @returns {Object}
   */
  buildBaseEvent(sample) {
    return {
      // for CEF message details
      inputType: sample.input.type,
      name: sample.input.name,
      ruleId: this.id,
      // for CEF extensions
      outcome: this.getAttackStatus(sample),
      pri: this.id,
      request: (sample.request.uri || '').replace(/ /g, '<space>'),
      spt: sample.request.port || '',
      requestMethod: sample.request.method,
      src: sample.request.ip,
      value: sample.input.value
    };
  }

  /**
   * Builds the event containing details used for security logging.
   *
   * Values of the hash match those defined here:
   * https://contrast.atlassian.net/wiki/display/PROD/Contrast+CEF+Logging+Spec
   *
   * @param   {Sample} sample     The Sample DTM
   * @returns {Object}            The event used for logging.
   */
  buildBlockEvent(sample) {
    const event = this.buildBaseEvent(sample);

    event.address = sample.input.value;
    event.ip = sample.details.ip;
    event.id = sample.details.uuid;

    return event;
  }

  buildProbedEvent(sample) {
    return this.buildBaseEvent(sample);
  }

  buildExploitEvent(sample) {
    return this.buildBaseEvent(sample);
  }

  getAttackStatus(sample) {
    return {
      [ATTACK_STATUS.BLOCKED]: 'Blocked',
      [ATTACK_STATUS.BLOCKED_AT_PERIMETER]: 'Blocked',
      [ATTACK_STATUS.EXPLOITED]: 'Exploited',
      [ATTACK_STATUS.INEFFECTIVE]: 'Probed'
    }[sample.getStatus()];
  }

  /**
   * When a probe is recognized to be a successful attack, we build the
   * attack details on the sample and mark the input as effective.
   * @param {Sample} sample       Sample used to build details
   * @param {*}      sinkFindings Findings from the sink
   */
  appendAttackDetails(sample, findings) {
    sample.effective = true;

    if (!(this.usesLibInputAnalysis && this.agentLibEnabled) || !this.buildDetailsForLib) {
      sample.details = this.buildDetails(sample, findings);
    } else {
      sample.details = this.buildDetailsForLib(sample, findings);
    }
  }
}

module.exports = Rule;
