Source: mar-client.js

'use strict'

var inherits = require('inherits')
var dxl = require('@opendxl/dxl-client')
var Request = dxl.Request
var bootstrap = require('@opendxl/dxl-bootstrap')
var Client = bootstrap.Client
var MessageUtils = bootstrap.MessageUtils
var ResultsContext = require('./results-context')

// The McAfee Active Response (MAR) search topic
var MAR_SEARCH_TOPIC = '/mcafee/mar/service/api/search'

// The default amount of time (in seconds) to wait before polling the MAR server
// for results
var DEFAULT_POLL_INTERVAL = 5

// The minimum amount of time (in seconds) to wait before polling the MAR server
// for results
var MIN_POLL_INTERVAL = 5

/**
 * @classdesc Responsible for all communication with the
 * Data Exchange Layer (DXL) fabric.
 * @external DxlClient
 * @see {@link https://opendxl.github.io/opendxl-client-javascript/jsdoc/Client.html}
 */

/**
 * @classdesc This client provides a high level wrapper for communicating with
 * the McAfee Active Response (MAR) DXL service.
 *
 * The purpose of this client is to allow the user to perform MAR searches
 * without having to focus on lower-level details such as MAR-specific DXL
 * topics and message formats.
 * @param {external:DxlClient} dxlClient - The DXL client to use for
 *   communication with the MAR DXL service.
 * @constructor
 */
function MarClient (dxlClient) {
  Client.call(this, dxlClient)
  this._pollInterval = DEFAULT_POLL_INTERVAL

  /**
   * Executes a query against the MAR search API.
   *
   * @param {Object} payload - The payload.
   * @param {Function} callback - Callback function to invoke with the results
   *   of the search. If an error occurs when performing the lookup, the first
   *   parameter supplied to the callback contains an `Error` object with
   *   failure details. On successful completion of the search, the results of
   *   the query is provided as the second parameter to the callback.
   * @private
   */
  this._invokeMarSearchApi = function (payload, callback) {
    var request = new Request(MAR_SEARCH_TOPIC)
    MessageUtils.objectToJsonPayload(request, payload)
    this._dxlClient.asyncRequest(request, function (error, response) {
      if (error) {
        callback(new Error('Error: ' + error.message), response)
      } else {
        var responseObj = MessageUtils.jsonPayloadToObject(response)
        var code = responseObj.code
        if (!code) {
          callback(new Error('Error: unable to find response code'))
        } else if (code >= 200 && code <= 299) {
          callback(null, responseObj)
        } else if (responseObj.body && responseObj.body.applicationErrorList &&
          (responseObj.body.applicationErrorList.length > 0)) {
          var applicationError = responseObj.body.applicationErrorList[0]
          callback(new Error(applicationError.message + ': ' +
            applicationError.code))
        } else if (responseObj.body) {
          callback(new Error(responseObj.body + ': ' + code))
        } else {
          callback(new Error('Error: Received failure response code: ' +
            code))
        }
      }
    })
  }
}

inherits(MarClient, Client)

/**
 * @property {Number} - The amount of time to wait (in seconds) before
 *   polling the MAR server for results. Defaults to `5`. If a value less than
 *   `5` is specified, a `RangeError` is thrown.
 * @name MarClient#pollInterval
 */
Object.defineProperty(MarClient.prototype, 'pollInterval', {
  get: function () {
    return this._pollInterval
  },
  set: function (value) {
    if (value < MIN_POLL_INTERVAL) {
      throw RangeError('Poll interval must be greater than or equal to ' +
        MIN_POLL_INTERVAL)
    }
    this._pollInterval = value
  }
})

/**
 * Executes a search via McAfee Active Response.
 *
 * Once the search has completed a {@link ResultsContext} object is returned
 * which is used to access the search results.
 *
 * **Client Authorization**
 *
 * The OpenDXL JavaScript client invoking this method must have permission
 * to send messages to the `/mcafee/mar/service/api/search` topic.
 *
 * See the following page for details on authorizing a client to
 * perform MAR searches:
 *
 * <https://opendxl.github.io/opendxl-client-python/pydoc/marsendauth.html>
 *
 * Execution of a MAR search requires an array of `projections` and an
 * optional object containing the search `conditions`.
 *
 * **Projections**
 *
 * `Projections` are used to describe the information to collect in the search.
 * Each `projection` consists of a `collector` name and a list of `output names`
 * from the `collector`. For example, the `Processes` collector includes
 * `output names` such as `name`, `sha1`, `md5`, etc.
 *
 * For a complete list of `collectors` and their associated `output names` refer
 * to the `McAfee Active Response Product Guide`.
 *
 * Each `projection` specified must contain the following fields:
 *
 * * `name`: The name of the `collector` to project
 * * `outputs`: An array of `output names` of the `collector` to project
 *
 * The JavaScript array below is equivalent to the `projections` within the
 * following textual search:
 *
 * `Processes name, id where Processes name equals "csrss" and Processes name
 * contains "exe" or Processes size not greater than 200`
 *
 * ```js
 * [{
 *   name: "Processes",
 *   outputs: ["name", "id"]
 * }]
 * ```
 *
 * **Conditions**
 *
 * `Conditions` are used to restrict which items are included in the search
 * results. For example, a search that collects process-related information
 * could be limited to those processes which match a specified name.
 *
 * A condition has a fixed structure starting with an ``or``
 * conditional operator and allowing only one level of ``and``
 * conditions.
 *
 * The JavaScript object below is equivalent to the `conditions` within the
 * following textual search:
 *
 * `Processes name, id where Processes name equals "csrss" and
 * Processes name contains "exe" or Processes size not greater than
 * 200`
 *
 * ```js
 * {
 *   or: [{
 *     and: [{
 *       name: 'Processes',
 *       output: 'name',
 *       op: 'EQUALS',
 *       value: 'csrss'
 *     },
 *     {
 *       name: 'Processes',
 *       output: 'name',
 *       op: 'CONTAINS',
 *       value: 'exe'
 *     }]
 *   },
 *   {
 *     and: [{
 *       name: 'Processes',
 *       output: 'size',
 *       op: 'GREATER_THAN',
 *       value: '200',
 *       negated: 'true'
 *     }]
 *   }]
 * }
 * ```
 *
 * The following fields are used for each `condition`:
 *
 * * `name`: The name of the `collector` from which to retrieve a value for
 *   comparison
 * * `output`: The `output name` from the `collector` that selects the specific
 *   value to use for comparison
 * * `op`: The comparison operator
 * * `value`: The value to compare with the value from the collector
 * * `negated`: (optional) Indicates if the comparison is negated
 *
 * The operators available for each value data type are as follows:
 *
 * | Operator           | NUMBER | STRING | BOOLEAN | DATE | IPV4IPV6 | REG_STR  |
 * | ------------------ | :----: | :----: | :-----: | :--: | :------: | :------: |
 * | GREATER_EQUAL_THAN |    x   |        |         |      |          |          |
 * | GREATER_THAN       |    x   |        |         |      |          |          |
 * | LESS_EQUAL_THAN    |    x   |        |         |      |          |          |
 * | LESS_THAN          |    x   |        |         |      |          |          |
 * | EQUALS             |    x   |   x    |    x    |   x  |     x    |  x `(*)` |
 * | CONTAINS           |        |   x    |         |      |     x    |  x `(*)` |
 * | STARTS_WITH        |        |   x    |         |      |          |  x `(*)` |
 * | ENDS_WITH          |        |   x    |         |      |          |  x `(*)` |
 * | BEFORE             |        |        |         |   x  |          |          |
 * | AFTER              |        |        |         |   x  |          |          ||
 *
 * `(*)` Negated field is not supported in those cases.
 *
 * @example
 * // Create the client
 * var client = new dxl.Client(config)
 *
 * // Connect to the fabric, supplying a callback function which is invoked
 * // when the connection has been established
 * client.connect(function () {
 *   var marClient = new MarClient(client)
 *
 *   marClient.search(
 *     [{
 *       name: 'Processes',
 *       outputs: ['name', 'id']
 *     }],
 *     {
 *       or: [{
 *         and: [{
 *           name: 'Processes',
 *           output: 'name',
 *           op: 'EQUALS',
 *           value: 'csrss'
 *         },
 *         {
 *           name: 'Processes',
 *           output: 'name',
 *           op: 'CONTAINS',
 *           value: 'exe'
 *         }]
 *       },
 *       {
 *         and: [{
 *           name: 'Processes',
 *           output: 'size',
 *           op: 'GREATER_THAN',
 *           value: '200',
 *           negated: 'true'
 *         }]
 *       }]
 *     },
 *     function (searchError, resultContext) {
 *       if (resultContext && resultContext.hasResults) {
 *         // Process results
 *       } else {
 *         // Handle searchError
 *       }
 *     }
 *   )
 * })
 * @param {Array<Object>} projections - An object containing the `projections`
 *   for the search.
 * @param {Object} conditions - An object containing the `conditions` for the
 *   search.
 * @param {Function} callback - Callback function to invoke with the results
 *   of the search. If an error occurs when performing the lookup, the first
 *   parameter supplied to the callback contains an `Error` object with
 *   failure details. On successful completion of the search, a
 *   {@link ResultsContext} object is provided as the second parameter to the
 *   callback.
 */
MarClient.prototype.search = function (projections, conditions, callback) {
  var that = this
  var createRequest = {
    target: '/v1/simple',
    method: 'POST',
    parameters: {},
    body: {projections: projections}
  }

  if (conditions) {
    createRequest.body.condition = conditions
  }

  // Create the search
  this._invokeMarSearchApi(createRequest,
    function (createError, createResponse) {
      if (createError) {
        callback(createError, createResponse)
      } else {
        if (createResponse.body && createResponse.body.id) {
          // Get the search identifier
          var searchId = createResponse.body.id

          // Start the search
          that._invokeMarSearchApi({
            target: '/v1/' + searchId + '/start',
            method: 'PUT',
            parameters: {},
            body: {}
          }, function (startError, startResponse) {
            if (startError) {
              callback(startError, startResponse)
            } else {
              var statusRequest = {
                target: '/v1/' + searchId + '/status',
                method: 'GET',
                parameters: {},
                body: {}
              }

              // Poll for status until the search has finished
              var checkStatus = function () {
                that._invokeMarSearchApi(statusRequest,
                  function (statusError, statusResponse) {
                    if (statusError) {
                      callback(statusError, statusResponse)
                    } else {
                      var body = statusResponse.body
                      // If the status is 'FINISHED', invoke the result
                      // callback. If not, wait for the configured poll interval
                      // and check status again.
                      if (body && body.status === 'FINISHED') {
                        // Deliver the results information to the callback
                        callback(null, new ResultsContext(that, searchId,
                          body.results, body.errors, body.hosts,
                          body.subscribedHosts
                        ))
                      } else {
                        setTimeout(checkStatus, that._pollInterval * 1000)
                      }
                    }
                  })
              }
              checkStatus()
            }
          })
        } else {
          callback(new Error('Error: Did not find id in create search response'))
        }
      }
    }
  )
}

module.exports = MarClient