/**
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 { CustomSearcher } = require('../../signature');
const { SCORES, WEIGHTS: IMPACT } = require('../../../common');

const dataPattern = /^data:/;
const hexPattern = /&#x((\d|A-F|a-f){2,4});?/;
const htmlPattern = /&#\d{2,4};?/;
const httpPattern = /^https?:\/\//;
const ipPattern = /(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)/;
const linkPattern = /<a.*href\s*=\s*(.+)>/gi;
const numericPattern = /http:\/\/\d+/;
const scriptPattern = /^((java|live)script)/;
const srcPattern = /<img.*src\s*=\s*(.+)>/gi;
const urlPattern = /%([67])(\d|([A-F]))/;

/**
 * @param {string} target
 * @returns {string}
 */
const stripQuotes = (target) => {
  if (!target) return target;

  const firstChar = target.charAt(0);
  if (firstChar === "'" || firstChar === '"' || firstChar === '`') {
    const nextTick = target.substring(1).indexOf(firstChar);
    const actualSrc = target.substring(1, nextTick + 1);
    return actualSrc;
  }
  return target;
};

/**
 * @param {string} target
 * @returns {boolean}
 */
const isHighImpact = (target) =>
  dataPattern.test(target) ||
  scriptPattern.test(target) ||
  htmlPattern.test(target) ||
  hexPattern.test(target);

/**
 * @param {string} value
 * @returns {number}
 */
const calculateSuspiciousScore = (value) => {
  if (!value) return IMPACT.NONE;

  const target = value.replace(/s*/, '');
  if (isHighImpact(target)) return IMPACT.HIGH;

  let score = IMPACT.NONE;
  if (httpPattern.test(target)) score += 1;
  if (urlPattern.test(target)) score += 2;
  if (ipPattern.test(target)) score += 2;
  if (numericPattern.test(target)) score += 2;
  return score;
};

/**
 * @param {string} value
 * @param {RegExp} pattern
 * @returns {number}
 */
const getImpactOfPattern = (value, pattern) => {
  let maxScore = IMPACT.NONE;
  let matches;
  while ((matches = pattern.exec(value)) !== null) {
    if (matches) {
      for (let i = 0; i < matches.length; i++) {
        if (matches[i]) {
          const target = stripQuotes(matches[i]);
          const targetScore = calculateSuspiciousScore(target);
          if (targetScore >= IMPACT.CRITICAL) return IMPACT.CRITICAL;
          maxScore = Math.max(targetScore, maxScore);
        }
      }
    }
  }
  return maxScore;
};

/**
 * @param {number} score
 * @returns {string}
 */
const getScoreRanking = (score) => {
  if (score >= IMPACT.CRITICAL) return SCORES.CRITICAL;
  if (score >= IMPACT.HIGH) return SCORES.HIGH;
  if (score >= IMPACT.MEDIUM) return SCORES.MEDIUM;
  if (score >= IMPACT.LOW) return SCORES.LOW;
  return SCORES.NONE;
};

class LinkAndSrcTargetSearcher extends CustomSearcher {
  constructor() {
    super({
      description: 'Searches for a link & src.',
      id: 'CS-TARGET-1',
      name: 'Link and Src Searcher'
    });
  }

  /**
   * Returns an impact score for the provided user input.
   * @param {Object} userInput
   * @param {string} userInput.value
   * @returns {number}
   */
  getImpact({ value }) {
    const linksImpact = getImpactOfPattern(value, linkPattern);
    if (linksImpact > IMPACT.CRITICAL) return SCORES.CRITICAL;
    const srcImpact = getImpactOfPattern(value, srcPattern);
    const score = Math.max(linksImpact, srcImpact, IMPACT.NONE);
    const ranking = getScoreRanking(score);
    return ranking;
  }

  /**
   * Returns a string with surrounding quotes (single, double, and backticks)
   * removed.
   * @param {string} target
   * @returns {string}
   */
  stripQuotes(target) {
    return stripQuotes(target);
  }
}

module.exports = new LinkAndSrcTargetSearcher();
