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;