/**
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.
*/
/**
 * Responsible for initializing all the agent instrumentation.
 * It also collects the application's library information.
 *
 * @module lib/instrumentation
 */
'use strict';
const agentEmitter = require('./agent-emitter');
const LoggerKit = require('./core/logger');
const libraries = require('./libraries');
const logger = LoggerKit('contrast:instrumentation');
const libUsage = require('./library-usage');

/**
 * Starts the application script with instrumentation. After collecting library
 * information and policies
 *
 * @param {Agent} agent The agent
 * @param {TSReporter} reporter TSReporter instance
 */
module.exports = function instrument(agent, reporter) {
  if (!agent.isInAssessMode() && !agent.isInDefendMode()) {
    logger.warn(
      'Assess nor Protect mode are enabled. Continuing without instrumentation'
    );
    return;
  }

  // The order of these matters, do not re-order
  collectLibInfo(agent);
  // (CONTRAST-31526) this must be required first to load global.ContrastMethods
  require('./core/rewrite/injections');
  assessModeFeatures(agent);
  protectModeFeatures({ agent, reporter });
  nonPolicyHooks(agent);
  anyModeFeatures(agent);
  // this should go before we call user code to avoid rewriting our own code
  instrumentCore({ agent, logger });

  /**
   * CAN'T GO ASYNC AFTER CALLING THIS - STARTING MAIN HAS TO HAPPEN
   * IN THE SAME EVENT LOOP TICK AS SENDING THE ONLINE EVENT B/C THE
   * PRIMARY MIGHT IMMEDIATELY SEND MESSAGES ONCE THE WORKER IS ONLINE
   * IF THE WORKER USER CODE DOESN'T REGISTER A LISTENER IN THIS TICK
   * THE MESSAGE WILL BE LOST
   */
  if (agent.cluster.isWorker) {
    logger.debug("cluster fork online; emitting 'online' event to primary");
    process.send({
      cmd: 'NODE_CLUSTER',
      act: 'online'
    });
  }

  return;
};

/**
 * Retrieves a list of installed dependencies and
 * then emits the list as appInfo to TS
 */
function collectLibInfo(agent) {
  if (
    agent.cluster.isPrimary &&
    agent.config.inventory.enable &&
    agent.config.inventory.analyze_libraries
  ) {
    // only do libraries in the master process (they should be the same for the forks)
    logger.info('collecting library information');

    const eluEnabled = agent.config.agent.node.library_usage.reporting.enable;
    const evalFrequency =
      agent.config.agent.node.library_usage.reporting.interval;
    if (eluEnabled) {
      require('./hooks/module/extensions')();
      libUsage.listen(evalFrequency);
    }
    libraries.getLibInfo(agent, eluEnabled).then((libs) => {
      agentEmitter.emit('appinfo', { libs });
    });
  }
}

/**
 * Initializes the asses mode features
 */
function assessModeFeatures(agent) {
  if (agent.isInAssessMode()) {
    logger.debug('initializing assess mode features');
    require('./assess/policy/init').init(agent);
    require('./assess/static/read-findings-from-cache')(agent);
    require('./hooks/encoding')();
    require('./hooks/object-to-primitive')();
    require('./hooks/array')();
    require('./hooks/object-is')();
    require('./hooks/dataflow-monitor')(agent);
    require('./assess')(agent);
    require('./coverage')();
  }
}

/**
 * Initializes the protect mode features
 *
 */
function protectModeFeatures({ agent, reporter }) {
  if (agent.isInDefendMode()) {
    logger.debug('initializing protect mode features');

    require('./protect')();

    // if it's native_input_analysis then use agent-lib
    if (agent.config.agent.node.native_input_analysis) {
      const lib = require('@contrast/agent-lib');
      // needs the || '.' for testing...
      const logDir = agent.config.agent.node.analysis_log_dir || '.';
      const agentLib = new lib.Agent(
        { enableLogging: true, logDir, logLevel: 'INFO' }
      );
      // attach the constants so lib.Agent() isn't exposed.
      for (const c in lib.constants) {
        agentLib[c] = lib.constants[c];
      }
      agent.agentLib = agentLib;
    }

    const protectService = require('./protect/listeners')(agent, reporter);
    const InputAnalysisSensor = require('./protect/input-analysis');
    require('./protect/sources')(agent);
    require('./protect/sinks')(agent);

    new InputAnalysisSensor({
      agent,
      reporter,
      logger: LoggerKit,
      protectService
    }).install();
  }
}

/**
 * Instruments non policy related hooks
 *
 */
function nonPolicyHooks(agent) {
  require('./core/async-storage/hooks/bluebird')();
  require('./core/async-storage/hooks/redis')();
  require('./core/async-storage/hooks/mongodb')();
  require('./core/async-storage/hooks/mysql')();
  require('./hooks/require');
  require('./hooks/cluster')(agent);
  require('./hooks/http')(agent);
  require('./hooks/newrelic')();
  require('./hooks/express-session')();
  require('./hooks/express-fileupload')();
}

/**
 * Initializes the listeners for route-discovery and route-observation on sensors.
 * Installs the source hooks and transplies native modules
 */
function anyModeFeatures(agent) {
  require('./core')(agent);
  require('./hooks/frameworks')(agent);
}

/**
 * Hooks Function.toString to unwrite our rewritten code
 * Monkey patches module._compile to rewrite user code
 * proxy magic for stealthy-require to not break
 */
function instrumentCore({ agent, logger }) {
  require('./hooks/fn-to-string')(agent);
  require('./hooks/stealthy-require')();
  require('./hooks/module')(agent);
}
