/**
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.
*/
/**
 * Builds the policy from the rules/configuration files and initializes
 * modules requiring it.
 * It also provides methods that allow policies to be disabled programmatically.
 * @module lib/assess/policy/init
 */
'use strict';
const os = require('os');
const path = require('path');
const _ = require('lodash');
const agentEmitter = require('../../agent-emitter');
const logger = require('../../core/logger')('contrast:rules:policy');
const { setEnabled, findDifference } = require('./util');
const policy = require('./');
const policyInit = module.exports;
let sinks;

/**
 * Stores the feature set from the last time there was an update
 */
const featureCache = {
  assess: false,
  disabledRules: [],
  sanitizers: [],
  validators: []
};

/**
 * Loads individual policy files
 */
policyInit.init = function init(agent) {
  logger.debug('applying policy');
  const customRulesPath =
    agent.config.get('node.dev.custom_policy_path') || agent.config.policy;
  let policyFiles = [];

  try {
    policyFiles = [
      require('./signatures.json'),
      require('./propagators.json'),
      require('./non-dataflow-rules.json'),
      require('./rules.json'),
      require('./deadzones.json')
    ];
    if (customRulesPath) {
      const customRules = require(path.resolve(customRulesPath));
      policyFiles.push(customRules);
    }
  } catch (e) {
    logger.error(
      `${'Unable to process rules file. ' +
        `Proceeding without instrumentation.${os.EOL}`}${e}`
    );
  }

  policyInit.load({ agent, policyFiles });
};

/**
 * Loads all propagators, sinks, sources, deadzones by policy file
 * Note: This is decoupled from init for testing purposes
 *
 * @param {Array} a list of policy files
 */
policyInit.load = function load({ agent, policyFiles }) {
  const { tsFeatureSet, config } = agent;
  mergePolicyFiles(policyFiles);

  initSinks({ agent, policy });

  if (config.assess.enable_propagators) {
    const { applicationSettings, serverFeatures } = tsFeatureSet;
    initPropagation({ agent, policy });

    updateApplicationSettings(applicationSettings);
    updateServerFeatures(agent, serverFeatures);
  }

  initDeadzones({ agent, policy });
  require('../sources').registerListeners(agent);
  require('../response-scanning/app-activity').handle(agent);
  registerConfigListeners(agent);

  return policy;
};

/**
 * delimeter used by TeamServer to separate module and method names
 */
const DELIM = '###';

function getApi(obj) {
  return obj.api;
}

function registerConfigListeners(agent) {
  agentEmitter.on('application-settings', updateApplicationSettings);
  agentEmitter.on('server-features', (serverFeatures) =>
    updateServerFeatures(agent, serverFeatures)
  );
}

function updateApplicationSettings(applicationSettings) {
  const disabledRules = _.get(
    applicationSettings,
    'disabledAssessRulesList',
    []
  );
  const { disable, enable } = findDifference(
    featureCache.disabledRules,
    disabledRules
  );

  featureCache.disabledRules = _.cloneDeep(disabledRules);
  disableSinks(disable);
  enableSinks(enable);
}

function updateServerFeatures(agent, serverFeatures) {
  const assess = _.get(serverFeatures, 'assess', {});
  const { enabled, sanitizersList, validatorsList } = assess;

  if (enabled) {
    let enable, disable;

    ({ enable, disable } = findDifference(
      sanitizersList,
      featureCache.sanitizers,
      getApi
    ));
    disableSanitizers(disable);
    enableSanitizers(agent, enable);
    featureCache.sanitizers = sanitizersList;

    ({ enable, disable } = findDifference(
      validatorsList,
      featureCache.validators,
      getApi
    ));
    disableValidators(disable);
    enableValidators(agent, enable);
    featureCache.validators = validatorsList;
  }
}

function enableValidators(agent, validators) {
  enableSecurityControls(agent, 'validator', validators);
}

function disableValidators(validators) {
  disableSecurityControls('validator', validators);
}

function enableSanitizers(agent, sanitizers) {
  enableSecurityControls(agent, 'sanitizer', sanitizers);
}

function disableSanitizers(sanitizers) {
  disableSecurityControls('sanitizer', sanitizers);
}

function disableSecurityControls(type, controls) {
  controls.forEach(function(control) {
    disableSecurityControl(type, control);
  });
}

function disableSecurityControl(type, control) {
  const { api } = control;

  logger.info('disabling %s: %s', type, api);
  disablePropagator(api);
}

function enableSecurityControls(agent, type, controls) {
  controls.forEach(function(control) {
    enableSecurityControl(agent, type, control);
  });
}

function enableSecurityControl(agent, type, control) {
  const tag = type === 'sanitizer' ? 'sanitized' : 'validated';
  const { api } = control,
    delim = api.indexOf(DELIM),
    moduleName = api.substring(0, delim),
    methodName = api.substring(delim + DELIM.length, api.indexOf('('));

  logger.info('enabling %s: %s', type, api);
  addSecurityControl(api, tag, moduleName, methodName);
  enablePropagator(agent, api);
}

function addSecurityControl(api, tag, moduleName, methodName) {
  policy.propagators[api] = policy.propagators[api] || {
    enabled: true,
    //'source': 'P',
    target: 'R',
    tags: [],
    command: {
      type: 'all'
    },
    signature: {
      moduleName,
      methodName,
      isModule: moduleName !== 'global'
    }
  };

  if (policy.propagators[api].tags.indexOf(tag) === -1) {
    policy.propagators[api].tags.push(tag);
  }
}

function initDeadzones({ agent, policy }) {
  const Deadzones = require('../deadzones');
  new Deadzones(agent, policy.deadzones);
}

function initPropagation({ agent, policy }) {
  const propagation = require('../propagators');
  propagation.init(policy);
  propagation.enablePropagators(agent, policy.propagators);
}

function initSinks({ agent, policy }) {
  disableSinks(
    _.get(agent, 'tsFeatureSet.applicationSettings.disabledRulesList', [])
  );
  // be sure to only require this after the policy is made
  sinks = require('../sinks')(agent);
  sinks.enableRules(policy);
}

/**
 * Merges the global policy with the merged
 * policy files(rules,propagators,nonDataflowRules, deadzones, etc)
 */
function mergePolicyFiles(policyFiles) {
  policyFiles.forEach(function(policyFile) {
    _.merge(policy, policyFile);
  });
}

/**
 * @param ruleNames
 */
function disableSinks(ruleNames) {
  toggleSinks(false, ruleNames);
}

/**
 * @param ruleNames
 */
function enableSinks(ruleNames) {
  toggleSinks(true, ruleNames);
}

/**
 * @param ruleNames
 * @param {boolean} on Whether to enable or disable the rules
 */
function toggleSinks(on, ruleNames) {
  ruleNames.forEach(function(ruleId) {
    const rule = policy.rules[ruleId];

    if (rule && sinks) {
      // toggle policy
      setEnabled(rule, on);

      // toggle instrumentation
      if (on) {
        sinks.enableRule(rule, ruleId);
      } else {
        sinks.disableRule(rule, ruleId);
      }
    }
  });
}

/**
 * @param {Agent} agent
 * @param {string} key
 */
function enablePropagator(agent, key) {
  const propagation = require('../propagators');
  propagation.enablePropagator(agent, policy.propagators[key], key);
}

/**
 * @param key
 */
function disablePropagator(key) {
  const propagation = require('../propagators');
  propagation.disablePropagator(policy.propagators[key], key);
}
