/**
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 { writeFileSync } = require('fs');
const logger = require('../logger')('contrast:tsReporter:umbrella');

/**
 *
 * It would be really painful to produce these tests by hand, so we want to spit out
 * at least the basics automatically and then go through and verify by hand to make sure
 * that stuff like line numbers is correct.
 *
 * The test spec is defined at https://bitbucket.org/contrastsecurity/umbrella-acceptance-tests/src/master/DESIGN.md
 *
 */

// not all rules will be captured in SAST, so missing IAST rules arent worth recording
const ruleExclusions = [
  'csp-header-missing',
  'hsts-header-missing',
  'clickjacking-control-missing',
  'xxssprotection-header-disabled',
  'xcontenttype-header-missing',
  'autocomplete-missing'
];

class UmbrellaLogger {
  constructor(agent) {
    this.model = {
      name: `${agent.config.application.name}`,
      language: 'node',
      version: `${process.version}`,
      findings: []
    };
  }

  static formatEvent(event) {
    const formatted = {
      // the stack is being left completely intact in this case because
      // the java model is insufficient
      stack: event.stack,
      signature: event.signature
    };

    // for static rules
    if (event.action === 'CREATION') formatted.type = 'source';
    else if (event.action === 'TRIGGER') formatted.type = 'sink';
    else formatted.type = event.action.toLowerCase();

    return formatted;
  }

  /**
   * format a finding as per the shared test spec
   * https://bitbucket.org/contrastsecurity/umbrella-acceptance-tests/src/master/DESIGN.md
   * @param   {Finding} finding rule finding
   * @returns {Object}          formatted finding as per the spec
   */
  static formatFinding(finding) {
    /*
      spec:
      "route": {
        "url": "/header-multipart"
      },
      "ruleId": "path-traversal",
      "events": {
        "type": "source",
        "signature": {
          "class": "org.apache.catalina.core.ApplicationPart",
          "method": "getHeader",
          "arguments": ["java.lang.String"],
          "constructor": false
        },
        "stack": [
          {
            "file": "SomeSourceFile.java",
            "lineNumber": 54
          },
          {
            "file": "SomeOtherSourceFile.java",
            "lineNumber": 140
          }
        ]
      },+*/
    const formatted = {
      hash: finding.hash,
      ruleId: finding.ruleId,
      events: finding.events.map((e) => UmbrellaLogger.formatEvent(e))
    };

    if (finding.routes[0]) {
      // get the actual path to the route
      formatted.route = finding.routes[0].signature.split('"')[1];
    }

    // handle static rules
    // static rules are not really described in the spec, so this is just
    // getting them into the best possible case for fitting
    if (formatted.events.length === 0) {
      formatted.events = [
        {
          type: 'source',
          stack: [
            {
              file: finding.properties.source,
              lineNumber: finding.properties.lineNumber
            }
          ],
          signature: finding.signature
        }
      ];
    }

    return formatted;
  }

  /**
   * write the umbrella structure to disk
   */
  static write(model) {
    writeFileSync(
      `${process.cwd()}/umbrella_log.json`,
      JSON.stringify(model, null, 2)
    );
  }

  /**
   * record a given node finding into the interanal structure
   * @param {Finding} finding the generated finding
   */
  log(finding) {
    try {
      // don't record rules SAST doesn't use
      if (!ruleExclusions.includes(finding.ruleId)) {
        // de-dupe findings
        if (!this.model.findings.some((f) => f.hash === finding.hash)) {
          this.model.findings.push(UmbrellaLogger.formatFinding(finding));

          // overwrites the file every time a finding is found
          UmbrellaLogger.write(this.model);
        }
      }
    } catch (e) {
      logger.error(`${e}`);
    }
  }
}

module.exports = UmbrellaLogger;
module.exports.ruleExclusions = ruleExclusions;
