Source: Parser.js

const Fs = require('fs');
const XmlParser = require('fast-xml-parser');

const EventWrapper = require('./base/EventWrapper');
const Util = require('./utility/Util');
const Scan = require('./scan/Scan');

// fast-xml-parser class options
const XmlOpts = {
  attributeNamePrefix: '',
  attrNodeName: 'attrib',
  textNodeName: 'text',
  ignoreAttributes: false,
  ignoreNameSpace: false,
  allowBooleanAttributes: true,
  parseNodeValue: true,
  parseAttributeValue: true,
  trimValues: true,
  cdataTagName: '__cdata',
  cdataPositionChar: '\\c',
  localeRange: '',
  parseTrueNumberOnly: false,
};

/**
  * Core parser for nmap xml log files
  * @param {Object} options - Parser options
  * @extends {EventWrapper}
  */
class Parser extends EventWrapper {
  constructor(options) {
    super(options);

    /**
      * Merge default options with given options
      * @type {Object}
      */
    this.options = Util.mergeDefault({
      debug: false, // @type {Boolean} optional
      autoParse: false, // @type {Boolean} optional
      onParsed: null, // @type {Function} optional
      onError: null, // @type {Function} optional
      onWarning: null, // @type {Function} optional
      onDebug: null, // @type {Function} optional
      logPath: null, // @type {String} required
      retainParsedLogs: true, // @type {Boolean} optional
      maxRetainedLogs: 5, // @type {Number} optional
    }, options);

    /**
      * The last generated error by this or it's children
      * @type {Error}
      */
    this.lastError = {};

    /**
      * The last generated warning by this or it's children
      * @type {String}
      */
    this.lastWarning = '';

    /**
      * File data for the current log being parsed, cleared after processing
      * @type {String}
      */
    this.logData = '';

    /**
      * XML structure generated by XmlParser, cleared after processing
      * @type {Object}
      */
    this.logStruct = {};

    /**
      * An array of previous parsed log files, cleared if `retainParsedLogs` is false
      * @type {Array}
      */
    this.scans = [];

    /**
      * Start processing the log right away, if path is set and `autoParse` is true
      */
    if (this.autoParse && this.options.logPath) {
      this.startParse();
    }
  }

  /**
    * Current `options.autoParse` value
    * @type {Boolean}
    * @readonly
    */
  get autoParse() {
    return this.options.autoParse;
  }

  /**
    * Set current `options.autoParse` value
    * @param {Boolean} newAutoParse - New value for `options.autoParse`
    */
  set autoParse(newAutoParse) {
    this.options.autoParse = newAutoParse;
  }

  /**
    * Current `options.logPath` value
    * @type {String}
    * @readonly
    */
  get logPath() {
    return this.options.logPath;
  }

  /**
    * Set current `options.logPath` value and start processing if required
    * @param {String} path - New value for `options.logPath`
    */
  set logPath(path) {
    this.options.logPath = path;

    // Begin processing if required
    if (this.autoParse) {
      this.startParse();
    }
  }

  /**
    * Pull latest scan from the array of processed log files
    * @type {Scan}
    * @readonly
    */
  get newestScan() {
    return this.scans[this.scanCount - 1] || false;
  }

  /**
    * How many scans are currently in memory
    * @type {Scan}
    * @readonly
    */
  get scanCount() {
    return this.scans.length;
  }

  /**
    * Begins processing the log file specified at `options.logPath`
    * Return will be false if an error occured
    * @returns {Boolean || Object<Scan>}
    */
  async startParse() {
    // Fail if the `logPath` in options is not set
    if (!this.logPath) {
      return this.throwError('Missing target log path!');
    }

    // Fail if the file cannot be found
    if (!Fs.existsSync(this.logPath)) {
      return this.throwError(`Cannot locate target log file: ${this.logPath}`);
    }

    // Load log file data from storage or fail on error
    try {
      this.logData = Fs.readFileSync(this.logPath, 'utf8');
    } catch (err) {
      return this.throwError(`Error reading target log file: ${this.logPath}\n${err}`);
    }

    // Run a non-critial validation check
    if (XmlParser.validate(this.logData) !== true) {
      this.throwWarning(`Non-critical Warning: Cannot validate target log file: ${this.logPath}`);
    }

    // Parse the xml log data into a js structure
    this.logStruct = XmlParser.parse(this.logData, XmlOpts);

    // Clear previous scans if required
    if (this.options.retainParsedLogs === false) {
      this.clearLogs();
    } else if (this.scanCount >= this.options.maxRetainedLogs) {
      // Remove oldest scan if too many are stored
      this.scans.shift();
    }

    // Begin processing the js structure into something more sane
    this.scans.push(new Scan({
      onError: this.throwError.bind(this),
      onWarning: this.throwWarning.bind(this),
      onDebug: this.throwDebug.bind(this),
    }, this.logStruct));

    // """Free""" no longer used variables
    this.logData = '';
    this.logStruct = {};

    // Call `onParsed` function with new Scan object if set
    if (this.options.onParsed) {
      this.options.onParsed(this.newestScan);
    } else {
      // Emit Scan object to any listeners
      this.emit('parsed', this.newestScan);
    }

    // Return Scan object
    return this.newestScan;
  }

  /**
    * Clear log cache
    */
  clearLogs() {
    this.scans = [];
  }
}

module.exports = Parser;