/**
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.
*/
/**
 * @module lib/assess/static/hardcoded
 */
'use strict';
const path = require('path');
const agentEmitter = require('../../agent-emitter');
const logger = require('../../core/logger')('contrast:rules:static');
const _ = require('lodash');
const generate = require('@babel/generator').default;
const { createHash } = require('../../util/trace-util');
const natives = process.binding('natives');

module.exports = ({ agent }) => {
  const rule = {};

  /**
   * Creates a handler for each ruleId
   * and attaches to agent.staticVisitors
   *
   * @param {String} ruleId name of rule
   * @param {Object} rulePolicy policy for rule
   *
   */
  rule.handle = function(rulePolicy, ruleId) {
    logger.debug(`creating rule visitor for ${ruleId}`);
    const ruleVisitor = rule.makeRuleVisitor(ruleId, rulePolicy);
    agent.staticVisitors.push(ruleVisitor);
  };

  /**
   * Constructs trace details and emits a finding
   *
   * @param {String} ruleId name of rule
   * @param {Object} node AST for code snippet
   * @param {String} name name of vulnerable variable
   * @param {String} filename file path
   */
  rule.report = function(ruleId, node, name, filename) {
    // only show the path relative to the app directory
    if (_.get(agent, 'appInfo.path')) {
      filename = path.relative(agent.appInfo.path, filename);
    }

    const lineNum = node.loc.start.line;
    const codeSource = generate(node).code;
    const finding = {
      ruleId,
      hash: createHash([ruleId, filename, lineNum]),
      // some empty props required for backwards compatibility
      props: {
        value: "''",
        signature: "''",
        access: 1,
        name,
        className: '""',
        source: filename,
        lineNumber: lineNum,
        codeSource
      }
    };
    agentEmitter.emit('finding', finding);
    logger.debug('%s: %s:%d %s', ruleId, filename, lineNum, codeSource);
  };

  /**
   * Checks if key is vulnerable then obfuscates and reports node for a given rule
   *
   * @param {Object} params
   * @param {Object} params.node AST for code snippet
   * @param {String} params.filename file path
   * @param {String} params.ruleId name of rule
   * @param {Object} params.rulePolicy policy for rule
   * @param {String} params.key name of key
   * @param {String} params.nodeKey name of key to get value in AST
   *
   */
  function maybeReportNode({
    node,
    filename,
    ruleId,
    rulePolicy,
    key,
    nodeKey
  }) {
    if (rule.isVulnerable(key, rulePolicy)) {
      if (rule.isStringLiteral(node[nodeKey])) {
        node = rule.cleanStringLiteral(node, nodeKey);
      } else {
        node[nodeKey] = rule.cleanTemplate(node[nodeKey]);
      }
      rule.report(ruleId, node, key, filename);
    }
  }

  /**
   * Checks a property for rule violation.
   *
   * Ex: { password: 'woot' }
   *
   * If value of property
   * is string literal, it obfuscates value for reporting purposes
   *
   *
   * @param {Object} params
   * @param {Object} params.node AST for code snippet
   * @param {String} params.filename file path
   * @param {String} params.ruleId name of rule
   * @param {Object} params.rulePolicy policy for rule
   *
   */
  function checkPropertyNode({ node, filename, ruleId, rulePolicy }) {
    let key = '';
    if (isCompatibleString(node.value) && node.key) {
      if (node.key.name) {
        key = node.key.name;
      } else if (node.key.type && node.key.type === 'StringLiteral') {
        // support quoted keys:  {'password': 'wubalubadubdub'}
        key = node.key.value;
      }

      maybeReportNode({
        key,
        rulePolicy,
        node,
        filename,
        ruleId,
        nodeKey: 'value'
      });
    }
  }

  /**
   * Checks a variable assignment for rule violation.
   *
   * Ex: password = 'wubalubadubdub';
   *
   * If value of property
   * is string literal, it obfuscates value for reporting purposes
   *
   *
   * @param {Object} params
   * @param {Object} params.node AST for code snippet
   * @param {String} params.ruleId name of rule
   * @param {String} params.filename file path
   * @param {Object} params.rulePolicy policy for rule
   *
   */
  function checkAssignmentExpression({ node, filename, ruleId, rulePolicy }) {
    let key = '';
    if (isCompatibleString(node.right)) {
      if (node.left.property) {
        // config.password = 'wubalubadubdub'
        if (node.left.property.name) {
          key = node.left.property.name;
        } else if (node.left.property.type === 'StringLiteral') {
          // config['password'] = 'wubalubadubdub'
          key = node.left.property.value;
        }
      } else {
        // password = 'wubalubadubdub'
        key = node.left.name;
      }

      maybeReportNode({
        key,
        rulePolicy,
        node,
        filename,
        ruleId,
        nodeKey: 'right'
      });
    }
  }

  /**
   * Checks a new variable declaration for rule violation.
   *
   * Ex: const password = 'wubalubadubdub';
   *
   * If value of property
   * is string literal, it obfuscates value for reporting purposes
   *
   *
   * @param {Object} params
   * @param {Object} params.node AST for code snippet
   * @param {String} params.ruleId name of rule
   * @param {String} params.filename file path
   * @param {Object} params.rulePolicy policy for rule
   *
   */
  function checkVariableDeclaration({ node, filename, ruleId, rulePolicy }) {
    if (node.init) {
      if (isCompatibleString(node.init)) {
        const key = node.id.name;
        maybeReportNode({
          key,
          rulePolicy,
          node,
          filename,
          ruleId,
          nodeKey: 'init'
        });
      }
    }
  }

  /**
   * @param {string} ruleId
   * @param {Object} rule The policy node for this rule
   */
  rule.makeRuleVisitor = function(ruleId, rulePolicy) {
    /**
     * Analyzes node for whether it is hardcoded assignment. The checks won't
     * execute on files in node_modules directory, since we only want to report
     * for custom code and not dependencies. Also in multi-process apps running
     * in cluster mode we only run checks in primary thread to avoid dupicating
     * findings when code in forked threads execute.
     * @param {object} node AST node
     * @param {string} filename filename being rewritten
     * @param {object} state rewriter state
     */
    return function(node, filename, state) {
      if (
        _.get(state, 'agent.cluster.isPrimary') &&
        filename &&
        filename.indexOf('node_modules') === -1 &&
        !natives[filename]
      ) {
        switch (node.type) {
          case 'ObjectProperty':
            checkPropertyNode({ node, filename, ruleId, rulePolicy });
            break;
          case 'AssignmentExpression':
            checkAssignmentExpression({ node, filename, ruleId, rulePolicy });
            break;
          case 'VariableDeclarator':
            checkVariableDeclaration({ node, filename, ruleId, rulePolicy });
            break;
        }
      }
    };
  };

  /**
   * Return whether or not the key matches the criteria for a rule.
   *
   * @param {string} key The variable name being tested
   * @param {Object} rulePolicy The policy node for the rule
   * @return {bool} Whether or not the key matches the criteria for this rule
   */
  rule.isVulnerable = function(key, rulePolicy) {
    if (!key || typeof key !== 'string') {
      return false;
    }

    key = key.toLowerCase();
    const keyList = rulePolicy.keyList || [];
    // Exact matches alone will trigger; our keys are lowercase
    for (let i = 0; i < keyList.length; i++) {
      if (key === keyList[i]) {
        return true;
      }
    }

    return false;
  };

  /**
   * @param {Expression} node Expression type AST node
   * @return {bool} Whether or not the node is a string or template
   */
  function isCompatibleString(node) {
    return rule.isStringLiteral(node) || rule.isTemplateString(node);
  }

  /**
   * @param {Expression} node Expression type AST node
   * @return {bool} Whether or not the node is a string literal
   */
  rule.isStringLiteral = function(node) {
    return (
      node.type === 'StringLiteral' &&
      node.value &&
      typeof node.value === 'string'
    );
  };

  /**
   * @param {Expression} node Expression type AST node
   * @return {bool} Whether or not the given node is a template expression.
   */
  rule.isTemplateString = function(node) {
    return (
      (node.type === 'TemplateLiteral' && node.expressions.length === 0) ||
      (node.type === 'TaggedTemplateExpression' &&
        node.quasi.expressions.length === 0)
    );
  };
  /**
   * clean up a template node and remove sensitive data.
   * @param {Expression} node Expression type AST node
   * @return {Expression} cleaned version of given node.
   */
  rule.cleanTemplate = function(node) {
    node = _.cloneDeep(node);
    if (node.type === 'TemplateLiteral') {
      node.quasis[0].value.raw = '****';
      node.quasis[0].value.cooked = '****';
    } else if (node.type === 'TaggedTemplateExpression') {
      node.quasi.quasis[0].value.raw = '****';
      node.quasi.quasis[0].value.cooked = '****';
    } else {
      return node;
    }
    return node;
  };

  /**
   * clean a string literal by obfuscating the value
   * @param {Expression} node Expression type AST node
   * @param {String} key key of AST node
   * @return {Expression} cleaned version of given node.
   */
  rule.cleanStringLiteral = function(node, key) {
    node = _.cloneDeep(node);
    node[key].value = '****';
    return node;
  };

  return rule;
};
