/**
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 constants = require('../../../constants');
const XXEFinder = require('../../../util/xml-analyzer/external-entity-finder');
const Sample = require('../../../reporter/models/app-activity/sample');
const BaseRule = require('../');

const { ENTITY_TYPES } = XXEFinder;
const { INPUT_TYPES, SINK_TYPES, IMPORTANCE } = constants;

const INPUT_NAME = 'XML Prolog';

class XXERule extends BaseRule {
  constructor(settings) {
    super(settings);

    this.id = 'xxe';
    this.name = 'External XML Entity Processing';
    this.inputClassification = false;
    this.applicableInputs = [INPUT_TYPES.GENERIC_XML];
    this.applicableSinks = [SINK_TYPES.XML_STRING];
  }

  /**
   * There's no classifiers or other evaluation needed. We capture the data
   * for *all* source events we care about (see applicableInputs) and
   * evaluate all of them within our sink handler.
   * @param {UserInput[]} inputs
   * @param {ApplicationContext} event
   */
  preFilterUserInput(input, event, samples) {
    const sample = new Sample(input, event);
    samples.addRawSample(this.id, sample, IMPORTANCE.WORTH_WATCHING);
  }

  /**
   * Scans the XML string for external entities.
   * Note that we do not look for existing samples, since this rule
   * does not require that the XML come from user-controlled data.
   * @param {SinkEvent} event
   */
  evaluateAtSink({ event, applicableSamples }) {
    const { data, id } = event;

    if (!applicableSamples.size) {
      return;
    }

    if (!_.isString(data)) {
      logger.warn(
        `rule ${this.id} was provided non-string value for evaluation.`
      );
      return;
    }

    logger.info(`evaluating ${this.id} for ${id}`);

    for (const sample of applicableSamples) {
      const findings = this.getEvaluator().evaluate(sample.input.value);

      if (findings.entities.length) {
        sample.input.name = INPUT_NAME;
        sample.input.type = INPUT_TYPES.UNKNOWN;
        sample.input.value = findings.prolog;

        sample.blocked = this.blocks();
        sample.confirmedAttack = true;
        sample.effective = true;
        this.buildDetails(sample, findings);

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

        continue;
      }

      applicableSamples.delete(sample);
    }
  }

  /**
   * Builds the Sample details per DTM spec.
   * @param {Sample} sample The sample to which to append details
   * @param {Object} findings The XXE findings
   */
  buildDetails(sample, findings) {
    const { prolog, entities } = findings;

    sample.details = {
      xml: prolog,
      declaredEntities: [],
      entitiesResolved: []
    };

    // Report for all entities found
    entities.reduce((memo, entity) => {
      memo.declaredEntities.push({
        start: entity.start,
        end: entity.finish
      });

      memo.entitiesResolved.push({
        publicId: entity.type === ENTITY_TYPES.PUBLIC ? entity.uri : undefined,
        systemId: entity.type === ENTITY_TYPES.SYSTEM ? entity.uri : undefined
      });

      return memo;
    }, sample.details);
  }

  /**
   * Returns the evaluator for this rule.
   * @returns {IEvaluate}
   */
  getEvaluator() {
    if (!this.evaluator) {
      this.evaluator = new Evaluator();
    }
    return this.evaluator;
  }

  /**
   * 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) {
    const uri = sample.request
      ? sample.request.uri
      : sample.request.uri.replace(' ', '<space>')
      ? sample.url
      : '';
    const method = sample.request ? sample.request.method : '';
    const ip = sample.request ? sample.request.ip : '';

    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: uri,
      requestMethod: method,
      src: ip,
      value: sample.input.value
    };
  }
}

class Evaluator {
  /**
   * Performs analysis on a string to return all found ext. entity data.
   * The return value is an <code>ExternalEntityFinder</code> with a
   * captured set of entities and an XML prolog.
   * @param {String} string XML input string
   * @returns {ExternalEntityFinder}
   */
  evaluate(string) {
    return XXEFinder.create(string);
  }
}

module.exports = XXERule;
XXERule.evaluator = Evaluator;
