/**
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/sinks/index.js
 */

'use strict';

const _ = require('lodash');
const logger = require('../../core/logger')('contrast:rules:sinks');
const policyUtil = require('../policy/util');
let signatures;

const { PATCH_TYPES } = require('../../constants');

module.exports = (agent) => {
  const common = require('./common')(agent);
  const { createSinkHandler, isValidSinkDefinition } = common;
  const sinks = {};

  /**
   * removes the instrumentation for a sink by the signature
   * @param {String} key
   * @param {String} ruleId
   * @param {Object} sink
   */
  function disableSink(key, ruleId, sink) {
    const signature = sink.signature || signatures[key];
    policyUtil.removeHookBySignature(
      ruleId,
      signature,
      PATCH_TYPES.ASSESS_SINK
    );
  }

  /**
   * removes instrumentation for a specific
   */
  sinks.disableRule = function disableRule(rule, ruleId) {
    if (rule.type === 'http') {
      // need to get their event type and use their event type and ruleId to unsubscribe them
      // events are set in lib/agent-emitter for response scanning rules
      const { events } = rule;
      policyUtil.removeHttpHook(ruleId, events);
    } else if (rule.type === 'hook') {
      if (rule.sinks) {
        _.forIn(rule.sinks, function(sink, key) {
          disableSink(key, ruleId, sink);
        });
      }
    }
  };

  /**
   * Sets the signatures on a var globally available in sinks.js
   * Loads the sinks policy
   *
   * @param {Object} policy contains both the sinks and corresponding signatures
   * @param {Object} policy.rules config for all sinks
   * @param {Object} policy.signatures config for all corresponding signatures
   *
   */
  sinks.enableRules = function enableRules(policy) {
    ({ signatures } = policy); // referenced in other methods in file, this must be done first

    _.forOwn(policy.rules, sinks.enableRule);
  };

  /**
   * Loads sink policy either from .json config or a sink file
   * @param {Object} rule configuration for a rule
   * @param {String} ruleId the name of a rule (e.g. - reflected-xss, sql-injection)
   *
   */
  sinks.enableRule = function enableRule(rule, ruleId) {
    let ruleOverride = false,
      provider;

    if (!policyUtil.isEnabled(rule)) {
      return;
    }

    if (rule.provider) {
      provider = policyUtil.loadProvider(rule.provider)({ agent, common });
      logger.trace(`loading provider for ${ruleId}`);

      if (['http', 'static'].includes(rule.type)) {
        provider.handle(rule, ruleId);
      } else if (rule.type === 'hook') {
        ruleOverride = true;
      }
    }

    // we pass the provider along to enableSink in case the rule
    // has a provider to override all sinks
    _.forOwn(
      rule.sinks,
      sinks.enableSink.bind(null, ruleId, ruleOverride ? provider : undefined)
    );
  };

  /**
   * Enables sink by adding instrumentation as post hook
   * @param {String} ruleId
   * @param {Object} ruleProvider
   * @param {Object} sink
   * @param {String} name
   */
  sinks.enableSink = function enableSink(ruleId, ruleProvider, sink, name) {
    if (!policyUtil.isEnabled(sink)) {
      return;
    }

    const signature = sink.signature || signatures[name];
    let handler, provider;

    if (sink.handler) {
      // attempt to reuse previously created sinks
      // this only happens if an enabled rule gets disabled
      // and then reenabled
      ({ handler } = sink);
    } else if (ruleProvider) {
      handler = ruleProvider.handle;
    } else if (sink.provider) {
      provider = policyUtil.loadProvider(sink.provider)({
        agent,
        common,
        signature
      });
      handler = provider.handle;
    } else {
      if (!policyUtil.isValidSignature(signature)) {
        logger.error(
          'Invalid signature for sink %s. Unable to apply hook.',
          name
        );
        return;
      }

      if (!isValidSinkDefinition(sink)) {
        logger.error(
          `Invalid sink definition for ${ruleId}:${name}:\n${JSON.stringify(
            sink
          )}`
        );
        return;
      }

      handler = createSinkHandler(signature, sink, ruleId);
    }

    // assigning to sink in case rule is disabled/enabled
    sink.handler = handler;
    handler.id = ruleId;

    // default handler to run in post if unspecified
    const options =
      sink.hookPhase === 'pre' ? { pre: handler } : { post: handler };
    policyUtil.createHookFromSignature(
      signature,
      options,
      PATCH_TYPES.ASSESS_SINK
    );
  };

  return sinks;
};
