/**
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 Rule = require('../');
const Sample = require('../../../reporter/models/app-activity/sample');
const {
  evaluate,
  getApplicableInputs,
  getConditions,
  getEvaluators
} = require('./utils');
const {
  INPUT_TYPES_WITH_PARAMS,
  IMPORTANCE,
  PROTECTION_MODES: { BLOCK_AT_PERIMETER }
} = require('../common');

// Virtual patches are weird because they don't use the normal Sample DTM
// for state that needs to be kept across the request lifecycle.
// Still, we may be able to cheat and abuse the Samples collection
// to store non-sample objects.
class VirtualPatchRule extends Rule {
  constructor(policy) {
    super(policy);
    this.id = 'virtual-patch';
    this.name = 'Virtual Patch';
    this.blockAtEntry = true;
    this.mode = BLOCK_AT_PERIMETER;

    // This is the set conditions that we evaluate and update when we
    // get different parts of the request e.g. HEADERs, BODY, etc.
    const conditions = getConditions(policy);
    this.evaluators = getEvaluators(conditions);
    this.applicableInputs = getApplicableInputs(conditions);
    this.uuid = policy.uuid;
  }

  /**
   * Whether the input type applies to this rule.
   * @param   {String}  type The name of the input type
   * @returns {Boolean}
   */
  appliesToInputType(type) {
    // We either support the type directly, or
    return (
      type in this.applicableInputs ||
      // we support parameterized types e.g. BODY, QUERYSTRING, etc
      (this.applicableInputs['PARAMETER'] && type in INPUT_TYPES_WITH_PARAMS)
    );
  }

  /**
   * Builds the details for the report to be sent to TS.
   * @returns {Object} Details for the sample
   */
  buildDetails() {
    return {
      uuid: this.uuid
    };
  }

  /**
   * Builds the event for logging to syslog/security log.
   * @returns {Object} The event.
   */
  buildBaseEvent() {
    return {
      name: this.name
    };
  }

  /**
   * override the base implementation to add and return a list
   */
  getStorage(samples) {
    const storage = super.getStorage(samples);

    if (!storage.evaluators) {
      storage.evaluators = new Set(this.evaluators);
    }

    return storage;
  }

  /**
   * Evaluates a single or collection of UserInputs. We must keep track of
   * which conditions have been met for each call to this, since we cannot
   * look at inputs of all types at once. If all conditions become met
   * during a call to this, then we will collect a sample and our
   * <code>prefilter</code> handler will cause a SecurityException.
   * @param {UserInput[]} inputOrMany Single or collection of UserInput
   * @param {ApplicationContext} sourceEvent
   */
  preFilterUserInput(input, sourceEvent, samples) {
    const { evaluators } = this.getStorage(samples);

    evaluate(evaluators, input);

    // We've depleted the necessary evaluators, it's time to block.
    if (evaluators.size === 0) {
      const sample = this.getSample(sourceEvent, samples);
      this.enabled = false;
      this.blockRequest(sample);
    }
  }

  /**
   * Called when we need to block/report because of the VP. Removes the
   * custom property we used to store unmet condition state.
   */
  blockRequest(sample) {
    sample.details = this.buildDetails();
    sample.blocked = true;
    return super.blockRequest(sample);
  }

  /**
   * Adds a new sample to the rule's collection and returns the sample.
   *
   * @param {ApplicationContext} appContext
   */
  getSample(appContext, samples) {
    let sample;
    const virtualPatchSamples = samples.getWorthWatching(this.id);

    if (virtualPatchSamples.size === 0) {
      sample = new Sample(null, appContext);
      sample.blocked = false;
      sample.effective = false;
      // Virtual patch DTM samples do no have an input.
      delete sample.input;

      samples.addRawSample(this.id, sample, IMPORTANCE.WORTH_WATCHING);
    } else {
      sample = virtualPatchSamples.values().next().value;
    }

    return sample;
  }

  /**
   * This method is typically called within the base class's
   *  `preFilterUserInput` method. While we override that method as well, we
   *  still implement this as a noop.
   */
  createAndSaveSample() {}
}

module.exports = VirtualPatchRule;
