/**
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 log = require('../../../core/logger')(
  'contrast:rules:protect:signatures'
);
const {
  SCORES: { NONE },
  WEIGHTS
} = require('../common');

const SCORES = _.invert(WEIGHTS);

/**
 * Base class for Keyword, Pattern, and Custom Searchers.
 */
class Searcher {
  constructor(definition = {}) {
    this.caseSensitive = definition.caseSensitive;
    this.value = definition.value;
    this.id = definition.id;
    this.score = definition.score;
  }

  /**
   * Implemented by subclasses
   */
  getImpact() {
    throw new Error('Method not implemented: getImpact');
  }
}

/**
 * Searcher that uses index-of searches with keyword value.
 */
class KeywordSearcher extends Searcher {
  constructor(definition) {
    super(definition);
    this.type = 'keyword';
  }

  /**
   * Returns the word score after analyzing user input
   * @param   {UserInput} userInput
   * @param   {object}    options
   * @returns {string}
   */
  getImpact(userInput, options = {}) {
    let value;

    if (!this.caseSensitive) {
      value = options.toLower || userInput.value.toLowerCase();
      options.toLower = value;
      this.value = this.value.toLowerCase();
    } else {
      ({ value } = userInput);
    }

    return 0 <= value.indexOf(this.value) ? this.score : NONE;
  }
}

/**
 * Creates a regular expression from a pattern's definition.
 * @param   {object} definition Rule definition for pattern
 * @returns {RegExp}
 */
const createRegExp = (definition, flags) => {
  const i = definition.caseSensitive ? '' : 'i';
  try {
    return new RegExp(definition.value, `${flags}${i}`);
  } catch (error) {
    log.debug(`Bad Regular expression: ${definition.id}`);
    return null;
  }
};

/**
 * Searcher that uses RegExp searches with pattern value.
 */
class PatternSearcher extends Searcher {
  constructor(definition) {
    super(definition);
    this.type = 'pattern';
    this.rx = createRegExp(definition, 'm');
  }

  /**
   * Returns the word score after analyzing user input
   * @param   {UserInput} userInput
   * @param   {object}    options
   * @returns {string}
   */
  getImpact(userInput) {
    return this.rx.test(userInput.value) ? this.score : NONE;
  }
}

/**
 * Searcher that uses custom logic. It is up to subclasses to
 * implement the <code>getImpact</code> method.
 */
class CustomSearcher extends Searcher {
  constructor(definition) {
    super(definition);
    this.type = 'custom';
  }
}

/**
 * Returns the definition with a <code>score</code> property that
 * is always the "word version".
 * @param   {object} definition Def from TS
 * @returns {object}
 */
const giveWordScore = (definition) => {
  if (/\d/.test(definition.score)) {
    return _.set(definition, 'score', SCORES[definition.score]);
  }
  return definition;
};

/**
 * Ensures that all definitions' score values resolve to word version.
 * @param   {Object[]} definitions Defs from TS
 * @returns {Object[]}
 */
const normalizeScores = (definitions = []) => _.map(definitions, giveWordScore);

/**
 * Maps keyword definitions over constructor fn.
 * @param   {Object[]} keywordDefs Defs from TS
 * @returns {KeywordSearcher[]}
 */
const createKeywords = (keywordDefs) =>
  keywordDefs.map((kw) => new KeywordSearcher(kw));

/**
 * Maps pattern definitions over constructor fn. Filters away
 * instances whose RegExp construction failed.
 * @param   {Object[]} patternDefs Defs from TS
 * @returns {PatternSearcher[]}
 */
const createPatterns = (patternDefs) =>
  patternDefs.map((p) => new PatternSearcher(p)).filter((p) => p.rx);

class Signature {
  constructor(definition) {
    this.name = definition.name;
    // the following is undefined if the rule is not an input to the
    // agent-lib score functions.
    this.agentLibBit = definition.agentLibBit;
    this.keywordSearchers = normalizeScores(
      createKeywords(definition.keywordsList)
    );
    this.patternSearchers = normalizeScores(
      createPatterns(definition.patternsList)
    );

    // Set by subclass or builder. If not set, evaluation will not validate.
    this.id = null;
    this.exploitChars = null;
    this.minimumInputLength = null;
    this.definiteThreshold = null;
    this.watchThreshold = null;
    //
    this.customSearchers = null;
    this.evaluator = null;
  }

  /**
   * Evaluates a UserInput and creates (or appends to) an Assessment.
   * @param {UserInput[]} userInputs
   * @param {Assessment}  [assessment]
   */
  evaluate(userInput, assessment) {
    return this.evaluator.evaluate(userInput, assessment);
  }
}

module.exports = Signature;

module.exports.Searcher = Searcher;
module.exports.KeywordSearcher = KeywordSearcher;
module.exports.PatternSearcher = PatternSearcher;
module.exports.CustomSearcher = CustomSearcher;
