Source: Scanner.js

const Fs = require('fs');

const EventWrapper = require('./base/EventWrapper');
const NmapWorker = require('./worker/NmapWorker');
const Parser = require('./Parser');
const Util = require('./utility/Util');

/**
  * Starts, stops and queues scan requests
  *
  * @param {Object} options - Class options
  * @extends {EventWrapper}
  */
class Scanner extends EventWrapper {
  constructor(options) {
    super(options);

    /**
      * Merge default options with given options
      * @type {Object}
      */
    this.options = Util.mergeDefault({
      autoParse: true, // @type {Boolean} optional
      debug: false, // @type {Boolean} optional
      deleteLogFile: false, // @type {Boolean} optional
      onError: null, // @type {Function} optional
      onWarning: null, // @type {Function} optional
      onDebug: null, // @type {Function} optional
      onReady: null, // @type {Function} optional
      onScanComplete: null, // @type {Function} optional
      onScanAbort: null, // @type {Function} optional
      onNmapOut: null, // @type {Function} optional
      profile: 'default', // @type {String} optional
      target: '', // @type {String} optional
      logDirectory: '', // @type {String} optional
      nmapPath: '', // @type {String} optional
      skipChecks: false, // @type {Boolean} optional
    }, options);

    /**
      * Main Nmap process handler
      * @type {NmapWorker}
      */
    this.worker = new NmapWorker({
      onError: this.throwError.bind(this),
      onWarning: this.throwWarning.bind(this),
      onDebug: this.throwDebug.bind(this),
      onReady: this.workerReady.bind(this),
      onFinish: this.scanCompleted.bind(this),
      onNmapOut: this.emitNmapOutput.bind(this),
      logDirectory: this.options.logDirectory,
      nmapPath: this.options.nmapPath,
      profile: this.options.profile,
      skipChecks: this.options.skipChecks,
    });

    /**
      * Scans waiting to be preformed
      * @type {Array}
      */
    this.scanQueue = [];

    /**
      * Main scan log parser
      * @type {Parser}
      */
    this.parser = new Parser();

    /**
      * Most recent scan object
      * @type {Object}
      */
    this.lastScanData = {
      logPath: '',
      nmapOut: '',
      nmapErr: '',
      exitCode: 0,
    };
  }

  /**
    * NmapWorker `ready` event handler
    * @private
    */
  workerReady() {
    /**
      * We are ready to scan
      * @event Scanner#ready
      */
    this.emit('ready');

    // Callback if required
    if (this.options.onReady) {
      this.options.onReady();
    }

    // Automagically start a scan if specified
    if (this.options.target !== '') {
      this.startScan(this.options.target);
    }
  }

  /**
    * Current profile getter
    * @type {String}
    * @readonly
    */
  get profile() {
    return this.options.profile;
  }

  /**
    * Current profile getter
    * @param {String} newProfile Target scan profile
    * @type {String}
    */
  set profile(newProfile) {
    this.options.profile = newProfile;
    this.worker.profile = newProfile;
  }

  /**
    * Add custom scan profile
    * @param {String} name Scan profile name
    * @param {String} args Nmap arguments
    */
  addProfile(name, args) {
    this.worker.addProfile(name, args);
  }

  /**
    * Indicates if a scan is currently running
    * @type {Boolean}
    * @readonly
    */
  get isBusy() {
    return this.worker.busy;
  }

  /**
    * Immediately starts a scan or queues the scan if one is currently running
    * @param {string} target Target hostname(s), IP address(es), network(s), etc.
    * @returns {Boolean} True if immediate start, false if queued
    * @public
    */
  startScan(target, profile = this.options.profile) {
    let scanOptions;

    // Target will be an object if it's a queued scan
    if (typeof target === 'string') {
      scanOptions = {
        target,
        profile,
        jobId: Math.random().toString(36).substring(2, 15),
        queued: this.isBusy,
      };
    } else {
      scanOptions = target;
    }

    // Queue scan if one is already running
    if (this.isBusy) {
      /**
        * Requested scan has been queued
        * @event Scanner#scanQueued
        */
      this.emit('scanQueued', scanOptions);

      this.scanQueue.push(scanOptions);

      return false;
    }

    /**
      * A scan has begun
      * @event Scanner#scanStarted
      */
    this.emit('scanStarted', scanOptions);

    this.worker.scan(scanOptions);

    return true;
  }

  /**
    * NmapWorker `onFinish` event handler
    * @param {Object} data Contains `logPath`, `nmapOut`, `nmapErr`, `exitCode`
    * @private
    */
  scanCompleted(data) {
    this.lastScanData = data;

    // Parse log data if required
    if (this.options.autoParse) {
      this.parser = new Parser({
        debug: this.options.debug,
        onParsed: this.logParsed.bind(this),
        logPath: data.logPath,
      });

      this.parser.on('error', this.throwError.bind(this));

      this.parser.on('warning', this.throwWarning.bind(this));

      this.parser.on('debug', this.throwDebug.bind(this));

      this.parser.startParse();
    } else {
      this.emitScan(false);
    }
  }

  /**
    * Parser `onParsed` event handler
    * @param {Object} data Contains parsed log data
    * @private
    */
  logParsed(data) {
    this.emitScan(data);
  }

  /**
    * Push scanComplete event with compiled scanData, starts next queued scan
    * @param {Object} data False if the log has not been parsed, or parsed log data
    * @private
    */
  emitScan(data) {
    const scanData = {
      logPath: this.lastScanData.logPath, // @type {String} Path to log file
      nmapOut: this.lastScanData.nmapOut, // @type {String} Nmap process std out
      nmapErr: this.lastScanData.nmapErr, // @type {String} Nmap process std error
      exitCode: this.lastScanData.exitCode, // @type {Number} Nmap process exit code
      scanData: data, // @type {?Object} Parsed log data or false
    };

    // Delete log file if required
    if (this.options.deleteLogFile) {
      Fs.unlink(scanData.logPath, (err) => {
        if (err) {
          this.throwError(`Unable to delete log file: ${scanData.logPath}\n${err}`);
        }
      });

      scanData.logPath = false;
    }

    /**
      * A scan has completed
      * @event Scanner#scanComplete
      */
    this.emit('scanComplete', scanData);

    if (this.options.onScanComplete) {
      this.options.onScanComplete(scanData);
    }

    // Clear previous data
    this.lastScanData = {};

    // Start next scan if any are pending
    if (this.scanQueue.length > 0) {
      const nextTarget = this.scanQueue.shift();

      this.startScan(nextTarget);
    }
  }

  /**
    * Force quit current scan
    * @return {Boolean} true if kill succeeds, and false otherwise
    */
  killScan() {
    if (this.isBusy) {
      const killResult = this.worker.killScan();

      /**
        * A scan has been canceled
        * @event Scanner#scanAborted
        */
      this.emit('scanAborted', killResult);

      if (this.options.onScanAbort) {
        this.options.onScanAbort(killResult);
      }

      return killResult.killed;
    }

    return false;
  }

  /**
    * Event handler for live nmap std out
    * @private
    */
  emitNmapOutput(data) {
    /**
      * Nmap live std output
      * @event Scanner#nmapOut
      */
    this.emit('nmapOut', data.toString('utf8'));

    if (this.options.onNmapOut) {
      this.options.onNmapOut(data.toString('utf8'));
    }
  }
}

module.exports = Scanner;