/**
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:rules:protect');
const common = require('../common');
const Rule = require('../');
const JSCleanString = require('../../../util/clean-string/jsclean-string');
const ECMAScriptScanner = require('../base-scanner/java-script-scanner');

const searchers = [];
const { INPUT_TYPES } = common;
const { SINK_TYPES } = common;

/**
 * @class SSJSInjectionRule
 */
class SSJSInjectionRule extends Rule {
  /**
   * @param {Object} settings DTM config from
   */
  constructor(policy) {
    policy = _.merge(policy, { searchers });
    super(policy);

    this.id = 'ssjs-injection';
    this.name = 'Node.js Injection';

    this.applicableInputs = [
      INPUT_TYPES.BODY,
      INPUT_TYPES.JSON_VALUE,
      INPUT_TYPES.JSON_ARRAYED_VALUE,
      INPUT_TYPES.PARAMETER_NAME,
      INPUT_TYPES.PARAMETER_VALUE,
      INPUT_TYPES.QUERYSTRING,
      INPUT_TYPES.XML_VALUE,
      INPUT_TYPES.URI,
      INPUT_TYPES.URL_PARAMETER
    ];
    this.applicableSinks = [SINK_TYPES.CODE_ENV, SINK_TYPES.CODE_STRING];
    this._scanner = null;
  }

  /**
   * Returns this rule's evaluator
   * @returns {EvaluatorBase}
   */
  getEvaluator() {
    const evaluator = this.signature;

    // This is where we add indirection between base Rule evaluation behavior
    // and this subclass. We need to provide the signature evaluator with the
    // "normalized" version of the input's value.
    return {
      evaluate(userInput) {
        const value = JSCleanString.clean(userInput.value).toString();
        return evaluator.evaluate({ value });
      }
    };
  }

  findInjection(data, samples) {
    for (const sample of samples) {
      const injection = this.scanner.findInjection(sample.input.value, data);

      if (injection) {
        return { injection, sample };
      }
    }
  }

  /**
   * Evaluates the sink data by scanning the code string for injections
   * by collected sample inputs. The sink data can be a code string or
   * an evaluation context environment object.
   * @param {SinkEvent} event
   */
  evaluateAtSink({ event, applicableSamples }) {
    if (_.isEmpty(applicableSamples) || _.isEmpty(event.data)) {
      return;
    }

    let evidence;

    switch (event.type) {
      case SINK_TYPES.CODE_ENV: {
        const values = _.values(event.data).filter(
          (v) => _.isString(v) && v.length
        );

        for (const value of values) {
          evidence = this.findInjection(value, applicableSamples);
          if (evidence) {
            break;
          }
        }
        break;
      }

      case SINK_TYPES.CODE_STRING: {
        evidence = this.findInjection(event.data, applicableSamples);
        break;
      }

      default: {
        throw new Error(
          'Applicable sinks for ssjs-injection are poorly configured'
        );
      }
    }

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

  /**
   * Builds details for the attack - exploited or blocked.
   * @param   {Sample} sample   The Sample for the attack
   * @param   {Object} findings The results of the sink analysis
   * @returns {Object}          The details
   */
  buildDetails(sample, findings) {
    const { query, location, boundary } = findings;
    const inputBoundaryIndex =
      _.get(boundary, 'previous.start') || _.get(boundary, 'start');

    return {
      start: location[0],
      end: location[1] + 1,
      input: sample.input.toSerializable(),
      boundaryOverrunIndex: boundary.stop + 1,
      inputBoundaryIndex,
      codeString: query
    };
  }

  get scanner() {
    if (!this._scanner) {
      this._scanner = new ECMAScriptScanner();
    }
    return this._scanner;
  }
}

module.exports = SSJSInjectionRule;
