'use strict'
var os = require('os')
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 OutputFormat = require('./output-format')
// The type of the ePO DXL "remote" service that is registered with the fabric
var DXL_EPO_REMOTE_SERVICE_TYPE = '/mcafee/service/epo/remote'
// The prefix for ePO DXL "remote" service request topics
var DXL_EPO_REMOTE_REQUEST_PREFIX = DXL_EPO_REMOTE_SERVICE_TYPE + '/'
// The type of the ePO DXL "commands" service that is registered with the fabric
var DXL_EPO_COMMANDS_SERVICE_TYPE = '/mcafee/service/epo/commands'
// The prefix for ePO DXL "commands" service request topics
var DXL_EPO_COMMANDS_REQUEST_PREFIX = '/mcafee/service/epo/command/'
// The prefix which appears before the remote command name in a request to
// the ePO DXL "commands" service.
var DXL_EPO_COMMANDS_REQUEST_COMMAND_PREFIX = '/remote/'
// The DXL topic to query for registered ePO service instances
var DXL_SERVICE_REGISTRY_QUERY_TOPIC = '/mcafee/service/dxl/svcregistry/query'
// The default DXL topic to listen to for ePO threat event messages
var EPO_THREAT_EVENT_TOPIC = '/mcafee/event/epo/threat/response'
// The name of the ePO 'help' remote command
var EPO_HELP_COMMAND = 'core.help'
// JSON output format for ePO remote command
var OUTPUT_FORMAT_JSON = 'json'
/**
* @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 Event messages are sent using the
* [sendEvent]{@link https://opendxl.github.io/opendxl-client-javascript/jsdoc/Client.html#sendEvent}
* method of a client instance.
* @external Event
* @see {@link https://opendxl.github.io/opendxl-client-javascript/jsdoc/Event.html}
*/
/**
* @classdesc This client provides a high level wrapper for invoking ePO remote
* commands via the Data Exchange Layer (DXL) fabric.
*
* The purpose of this library is to allow users to invoke ePO remote commands
* without having to focus on lower-level details such as ePO-specific DXL
* topics and message formats.
*
* **ePO Unique Identifier**
*
* DXL supports communicating with multiple ePO servers on a single DXL fabric.
* However, each instance of this client can only be associated with one ePO
* server (the server it will be invoking remote commands on).
*
* The ePO unique identifier specified must match the identifier that was
* associated with the particular ePO when a corresponding ePO DXL service was
* started.
*
* If only one ePO server is connected to the DXL fabric this parameter is
* optional (the client will automatically determine the ePO's unique
* identifier).
*
* The {@link EpoClient.lookupEpoUniqueIdentifiers} method can be used to
* determine the unique identifiers for ePO servers that are currently exposed
* to the fabric.
*
* @param {external:DxlClient} dxlClient - The DXL client to use for
* communication with the ePO DXL service
* @param {String} [epoUniqueId] - The unique identifier used to specify the ePO
* server that this client will communicate with.
* @constructor
*/
function EpoClient (dxlClient, epoUniqueId) {
var that = this
Client.call(this, dxlClient)
/**
* Text string used for displaying the status of an asynchronous attempt
* to obtain the unique id of an ePO server.
* @type {string}
* @private
*/
this._epoIdSearchStatus =
'ePO unique identifier not yet determined'
/**
* Unique identifier of the ePO server on which to invoke remote commands.
* @type {String}
* @private
*/
this._epoUniqueId = epoUniqueId
/**
* Controls whether the built-in ePO "commands" (true) or standalone "remote"
* service {@link https://github.com/opendxl/opendxl-epo-service-python}
* (false) is used to make command requests.
* @type {Boolean}
* @private
*/
this._useEpoCommandsService = true
/**
* Whether or not the ePO service type - "commands" or "remote" - and
* unique id has been determined.
* @type {Boolean}
* @private
*/
this._epoServiceDetermined = false
/**
* Determines the ePO service to which commands should be sent the DXL fabric.
* If an empty `epoUniqueId` was provided during client construction, an
* attempt will be made to dynamically determine the unique ID of an ePO
* server running on the fabric.
* @param {Function} [callback] - Callback function which should be invoked
* after the unique identifier and service type - "commands" or "remote"
* has been determined.
*
* If determination of the ePO unique identifier fails, the first parameter
* supplied to the callback contains an `Error` object with failure details.
* An `Error` object could be delivered for any of the following conditions:
*
* * No value is provided for the `epoUniqueId` parameter during client
* construction and zero or more than 1 ePO service is found on the DXL
* fabric.
* * A value is provided for the `epoUniqueId` parameter during client
* construction but no ePO service matching the id is found on the DXL
* fabric.
* * An error occurs when trying to make DXL requests to the broker to
* query for available ePO services.
* @private
*/
this._determineService = function (callback) {
var client = this
var epoUniqueId = client._epoUniqueId
var setServiceInfoFunction = function (error, useEpoCommandsService) {
if (!error) {
client._epoServiceDetermined = true
client._useEpoCommandsService = useEpoCommandsService
}
if (callback) {
callback(error)
}
}
if (epoUniqueId) {
client._isEpoUniqueIdForCommandsService(epoUniqueId,
setServiceInfoFunction)
} else {
client._getEpoId(setServiceInfoFunction)
}
}
/**
* Lookup the ePO unique identifier.
* @param {Function} [callback] - Callback function which should be
* invoked after the ePO unique identifier has been determined and the
* client is ready to use. If an either no unique identifier or more than
* one unique identifier can be found, an `Error` object is specified as
* the first parameter to the callback.
* @private
*/
this._getEpoId = function (callback) {
lookupEpoUniqueIdentifiers(dxlClient,
function (error, epoUniqueIds, useEpoCommandsService) {
if (epoUniqueIds) {
switch (epoUniqueIds.length) {
case 0:
error = new Error(
'No ePO DXL services are registered with the DXL fabric')
break
case 1:
that._epoUniqueId = epoUniqueIds[0]
break
default:
error = new Error(
'Multiple ePO DXL services are registered with the DXL fabric' +
' (' + epoUniqueIds + ').' +
' A specific ePO unique identifier must be specified.'
)
}
}
that._epoIdSearchStatus = error ? error.message : 'ePO id found'
if (callback) {
callback(error, useEpoCommandsService)
}
})
}
/**
* Determines if the supplied `epoUniqueId` maps to an ePO DXL "commands" or
* a "remote" service.
* @param {String} [epoUniqueId] - The unique identifier used to specify the
* ePO server that this client will communicate with.
* @param {Function} [callback] - Callback function which should be invoked
* with the result of the attempt to determine the service type for the
* id. If the attempt to determine the service type fails, the first
* parameter supplied to the callback contains an `Error` object with
* failure details. If the service type can successfully be determined, the
* second parameter supplied to the callback is `true` if the unique
* identifier matches a "commands" service, else `false` if the unique
* identifier matches a "remote" service.
* @private
*/
this._isEpoUniqueIdForCommandsService = function (epoUniqueId, callback) {
lookupEpoRemoteServiceUniqueIds(dxlClient,
function (error, epoIds) {
if (epoIds) {
if (epoIds.indexOf(epoUniqueId) < 0) {
lookupEpoCommandsServiceUniqueIds(dxlClient,
function (error, epoIds) {
var idForCommandsService = false
if (!error) {
if (epoIds.indexOf(epoUniqueId) < 0) {
error = new Error('No ePO DXL services are registered ' +
'with the DXL fabric for id: ' + epoUniqueId)
} else {
idForCommandsService = true
}
}
callback(error, idForCommandsService)
})
} else {
callback(error, false)
}
} else {
callback(error, false)
}
}
)
}
/**
* Invokes the ePO DXL service for the purposes of executing a remote command.
* @param {String} requestTopic - DXL request topic to use for the request.
* @param {Object} payload - The object to use as the payload of the DXL
* request.
* @param {String} outputFormat - The output format for ePO to use when
* returning the response.
* @param {Function} [callback] - Callback function to invoke with the result
* of the remote command execution. If an error occurs when performing the
* command, the first parameter supplied to the callback contains an `Error`
* object with failure details. On successful execution of the command, the
* remote command output is provided as the second parameter to the
* callback.
* @private
*/
this._invokeEpoService = function (requestTopic, payload, outputFormat,
callback) {
if (this._epoUniqueId) {
var request = new Request(requestTopic)
MessageUtils.objectToJsonPayload(request, payload)
this._dxlClient.asyncRequest(request, function (error, response) {
var responsePayload = null
if (response) {
try {
switch (outputFormat) {
case OutputFormat.BINARY:
responsePayload = response.payload
break
case OutputFormat.STRING:
responsePayload = MessageUtils.decodePayload(response)
break
default:
responsePayload = MessageUtils.jsonPayloadToObject(response)
}
} catch (err) {
error = err
}
}
if (callback) {
callback(error, responsePayload)
}
})
} else {
if (callback) {
callback(new Error(this._epoIdSearchStatus))
}
}
}
/**
* Invokes the ePO DXL "commands" service for the purposes of executing a
* remote command.
* @param {String} commandName - The name of the remote command to invoke.
* @param {String} outputFormat - The output format for ePO to use when
* returning the response.
* @param {Object} params - An object containing the parameters for the
* command.
* @param {Function} callback - Callback function to invoke with the result
* of the remote command execution. If an error occurs when performing the
* command, the first parameter supplied to the callback contains an `Error`
* object with failure details. On successful execution of the command, the
* remote command output is provided as the second parameter to the
* callback.
* @private
*/
this._invokeEpoCommandsService = function (commandName, outputFormat,
params, callback) {
this._invokeEpoService(DXL_EPO_COMMANDS_REQUEST_PREFIX +
this._epoUniqueId + DXL_EPO_COMMANDS_REQUEST_COMMAND_PREFIX +
commandName.replace('.', '/'),
params,
outputFormat,
callback
)
}
/**
* Invokes the ePO DXL "remote" service for the purposes of executing a
* remote command.
* @param {String} commandName - The name of the remote command to invoke.
* @param {String} outputFormat - The output format for ePO to use when
* returning the response.
* @param {Object} params - An object containing the parameters for the
* command.
* @param {Function} callback - Callback function to invoke with the result
* of the remote command execution. If an error occurs when performing the
* command, the first parameter supplied to the callback contains an `Error`
* object with failure details. On successful execution of the command, the
* remote command output is provided as the second parameter to the
* callback.
* @private
*/
this._invokeEpoRemoteService = function (commandName, outputFormat, params,
callback) {
this._invokeEpoService(DXL_EPO_REMOTE_REQUEST_PREFIX + this._epoUniqueId,
{
command: commandName,
output: OUTPUT_FORMAT_JSON,
params: params
},
outputFormat,
callback
)
}
this._runCommand = function (commandName, outputFormat, params, callback) {
if (this._useEpoCommandsService) {
this._invokeEpoCommandsService(commandName, outputFormat, params,
callback)
} else {
this._invokeEpoRemoteService(commandName, outputFormat, params,
callback)
}
}
}
inherits(EpoClient, Client)
/**
* Looks up the ePO unique ids for a matching ePO service type.
* @param {external:DxlClient} dxlClient - The DXL client to use for
* communication with the ePO DXL service
* @param {String} serviceType - The service type to return data for.
* @param {Function} processServiceCallback - Callback function which is
* invoked for each service which is found. The first parameter supplied to
* the callback is an array of ePO unique id strings found so far. The
* callback can append additional unique ids that it finds in the service info
* onto the id array. The second parameter supplied to the callback is an
* object containing information for the registered service.
* @param {Function} doneCallback - Callback function which is invoked with
* the result of the unique id lookup. If an error occurs when performing the
* lookup, the first parameter supplied to the callback contains an `Error`
* object with failure details. On successful execution of the lookup, the
* second parameter supplied to the callback is an array of strings whose
* values are the ePO unique ids found in the lookup.
* @private
*/
function lookupEpoUniqueIdsForServiceType (dxlClient, serviceType,
processServiceCallback,
doneCallback) {
// Query the DXL broker registry for info on services matching the service
// type
var request = new Request(DXL_SERVICE_REGISTRY_QUERY_TOPIC)
MessageUtils.objectToJsonPayload(request, {serviceType: serviceType})
dxlClient.asyncRequest(request,
function (error, response) {
var epoIds = null
if (response) {
try {
epoIds = []
var responseObj = MessageUtils.jsonPayloadToObject(response)
var services = responseObj.services
if (services) {
Object.keys(services).forEach(
function (serviceId) {
var epoId = processServiceCallback(epoIds, services[serviceId])
if (epoId && epoId.indexOf(epoId) < 0) {
epoIds.push(epoId)
}
})
}
} catch (err) {
error = err
}
}
doneCallback(error, epoIds)
})
}
/**
* Looks up the unique identifiers for the ePO servers that are currently
* exposed to the DXL fabric via an ePO "commands" service. "commands" services
* are registered by version 5.0 and later of the ePO DXL extensions.
* @param {external:DxlClient} dxlClient - The DXL client to use for
* communication with the ePO DXL service
* @param {Function} callback - Callback function which is invoked with the
* result of the unique id lookup. If an error occurs when performing the
* lookup, the first parameter supplied to the callback contains an `Error`
* object with failure details. On successful execution of the lookup, the
* second parameter supplied to the callback is an array of strings whose
* values are the ePO unique ids found in the lookup.
* @private
*/
function lookupEpoCommandsServiceUniqueIds (dxlClient, callback) {
lookupEpoUniqueIdsForServiceType(dxlClient,
DXL_EPO_COMMANDS_SERVICE_TYPE,
function (epoIds, service) {
if (service.metaData && service.metaData.epoGuid) {
var epoId = service.metaData.epoGuid
if (epoIds.indexOf(epoId) < 0) {
epoIds.push(epoId)
}
}
},
callback)
}
/**
* Looks up the unique identifiers for the ePO servers that are currently
* exposed to the DXL fabric via an ePO "remote" service. "remote" services are
* registered by the standalone
* [ePO DXL Python Service](https://github.com/opendxl/opendxl-epo-service-python).
* @param {external:DxlClient} dxlClient - The DXL client to use for
* communication with the ePO DXL service
* @param {Function} callback - Callback function which is invoked with the
* result of the unique id lookup. If an error occurs when performing the
* lookup, the first parameter supplied to the callback contains an `Error`
* object with failure details. On successful execution of the lookup, the
* second parameter supplied to the callback is an array of strings whose
* values are the ePO unique ids found in the lookup.
* @private
*/
function lookupEpoRemoteServiceUniqueIds (dxlClient, callback) {
lookupEpoUniqueIdsForServiceType(dxlClient,
DXL_EPO_REMOTE_SERVICE_TYPE,
function (epoIds, service) {
var requestChannels = service.requestChannels
if (requestChannels) {
requestChannels.forEach(function (channel) {
if (channel.startsWith(DXL_EPO_REMOTE_REQUEST_PREFIX)) {
var epoId = channel.substring(DXL_EPO_REMOTE_REQUEST_PREFIX.length)
if (epoIds.indexOf(epoId) < 0) {
epoIds.push(epoId)
}
}
})
}
},
callback)
}
/**
* Retrieves an array of strings containing the unique identifiers for the ePO
* servers that are currently exposed to the DXL fabric.
* @param {external:DxlClient} dxlClient - The DXL client with which to perform
* the request
* @param {Function} callback - Callback function to invoke with the unique
* identifiers which are found. If an error occurs when performing the lookup,
* the first parameter supplied to the callback contains an `Error` object
* with failure details. On successful lookup, the array of unique identifier
* strings is provided as the second parameter to the callback and the
* third parameter to the callback is a boolean representing whether a
* "commands" (`true`) or a "remote" (`false`) should be used. If at least
* one "remote" service is available, a "remote" service should be used.
* @private
*/
function lookupEpoUniqueIdentifiers (dxlClient, callback) {
lookupEpoRemoteServiceUniqueIds(dxlClient,
function (error, epoRemoteServiceIds) {
if (epoRemoteServiceIds) {
lookupEpoCommandsServiceUniqueIds(dxlClient,
function (error, epoServiceIds) {
if (epoServiceIds) {
epoRemoteServiceIds.forEach(function (serviceId) {
if (epoServiceIds.indexOf(serviceId) < 0) {
epoServiceIds.push(serviceId)
}
})
epoServiceIds.sort()
callback(error, epoServiceIds, !epoRemoteServiceIds.length)
} else {
callback(error)
}
})
} else {
callback(error)
}
}
)
}
/**
* Retrieves an array of strings containing the unique identifiers for the ePO
* servers that are currently exposed to the DXL fabric.
* @param {external:DxlClient} dxlClient - The DXL client with which to perform
* the request
* @param {Function} callback - Callback function to invoke with the unique
* identifiers which are found. If an error occurs when performing the lookup,
* the first parameter supplied to the callback contains an `Error` object
* with failure details. On successful lookup, the array of unique identifier
* strings is provided as the second parameter to the callback.
*/
EpoClient.lookupEpoUniqueIdentifiers = function (dxlClient, callback) {
lookupEpoUniqueIdentifiers(dxlClient,
function (error, epoIds) {
callback(error, epoIds)
})
}
/**
* Returns the list of remote commands that are supported by the ePO server
* this client is communicating with.
* @param {Function} responseCallback - Callback function to invoke with the
* remote commands which are found. If an error occurs when performing the
* command, the first parameter supplied to the callback contains an `Error`
* object with failure details. On successful execution of the command, the
* remote command output is provided as the second parameter to the callback.
*
* If determination of the ePO unique identifier fails, the first parameter
* supplied to the callback contains an `Error` object with failure details.
* An `Error` object could be delivered for any of the following conditions:
*
* * No value is provided for the `epoUniqueId` parameter during client
* construction and zero or more than 1 ePO service is found on the DXL
* fabric.
* * A value is provided for the `epoUniqueId` parameter during client
* construction but no ePO service matching the id is found on the DXL
* fabric.
* * An error occurs when trying to make DXL requests to the broker to
* query for available ePO services.
* @example <caption>Example Usage</caption>
* epoClient.help(function (helpError, helpText) {
* if (helpError) {
* console.log('Error getting help: ' + helpError.message)
* } else {
* console.log(helpText)
* }
* })
* @example <caption>Example Response Text</caption>
* ComputerMgmt.createAgentDeploymentUrlCmd deployPath groupId [edit] [ahId]
* [fallBackAhId] [urlName] [agentVersionNumber] [agentHotFix] - Create Agent
* Deployment URL Command
* ComputerMgmt.createCustomInstallPackageCmd deployPath [ahId] [fallBackAhId] -
* Create Custom Install Package Command
* ComputerMgmt.createDefaultAgentDeploymentUrlCmd tenantId - Create Default
* Non-Editable Agent Deployment URL Command
* ComputerMgmt.createTagGroup parentTagGroupId newTagGroupName - Create a new
* subgroup under an existing tag group.
* ComputerMgmt.deleteTag tagIds [forceDelete] - Delete one or more tags.
*/
EpoClient.prototype.help = function (responseCallback) {
if (responseCallback) {
this.runCommand(EPO_HELP_COMMAND,
{
responseCallback: function (error, response) {
responseCallback(error, response ? response.join(os.EOL) : null)
},
outputFormat: OutputFormat.OBJECT
})
} else {
throw new TypeError('No callback provided')
}
}
/**
* Invokes an ePO remote command on the ePO server this client is communicating
* with.
* @param {String} commandName - The name of the remote command to invoke.
* @param {Object} [options] - Additional options to supply for the remote
* command.
* @param {Function} [options.responseCallback] - Callback function to invoke
* with the results of the remote command.
*
* If an error occurs when performing the command, the first parameter
* supplied to the callback contains an `Error` object with failure details.
* On successful execution of the command, the remote command output is
* provided as the second parameter to the callback. If supported for the
* remote command, the raw response payload should be formatted in JSON. The
* type of the response payload can be set via the `outputFormat` option.
*
* If determination of the ePO unique identifier fails, the first parameter
* supplied to the callback contains an `Error` object with failure details.
* An `Error` object could be delivered for any of the following conditions:
*
* * No value is provided for the `epoUniqueId` parameter during client
* construction and zero or more than 1 ePO service is found on the DXL
* fabric.
* * A value is provided for the `epoUniqueId` parameter during client
* construction but no ePO service matching the id is found on the DXL
* fabric.
* * An error occurs when trying to make DXL requests to the broker to
* query for available ePO services.
* @param {Object} [options.params] - An object containing the parameters for
* the command.
* @param {String} [options.outputFormat=object] - The output format for ePO
* to use when returning the response. The list of `output formats` can be
* found in the [OutputFormat]{@link module:OutputFormat} constants module.
* @throws {TypeError} If the `outputFormat` is not valid.
* @example <caption>Example Usage</caption>
* epoClient.runCommand('system.find',
* {
* responseCallback: function (searchError, responseObj) {
* if (searchError) {
* console.log('Error finding system: ' + searchError.message)
* } else {
* console.log(JSON.stringify(responseObj, null, 2))
* }
* },
* params: {searchText: 'mySystem'}
* }
* )
* @example <caption>Example Response Text</caption>
* [
* {
* "EPOBranchNode.AutoID": 7,
* "EPOComputerProperties.CPUSerialNum": "N/A",
* "EPOComputerProperties.CPUSpeed": 2794,
* "EPOComputerProperties.CPUType": "Intel(R) Core(TM) i7-4980HQ CPU @ 2.80GHz",
* "EPOComputerProperties.ComputerName": "mySystemForTesting",
* "EPOComputerProperties.DefaultLangID": "0409",
* ...
* }
* ]
*/
EpoClient.prototype.runCommand = function (commandName, options) {
var client = this
var responseCallback = options.responseCallback
var params = options.params || {}
var outputFormat = options.outputFormat
if (outputFormat) {
OutputFormat.validate(outputFormat)
} else {
outputFormat = OutputFormat.OBJECT
}
if (client._epoServiceDetermined) {
client._runCommand(commandName, outputFormat, params, responseCallback)
} else {
client._determineService(function (error) {
if (error) {
if (responseCallback) {
responseCallback(error)
}
} else {
client._runCommand(commandName, outputFormat, params, responseCallback)
}
})
}
}
/**
* Registers an event callback with the client to receive ePO threat events.
* @param {Function} threatEventResponseCallback - The function that will
* receive ePO threat events. The first argument passed to the callback
* function is an object decoded from the JSON payload of the event content.
* The second argument passed to the callback function is the full DXL
* [Event]{@link external:Event} object.
* @param {String} [topic=/mcafee/event/epo/threat/response] - The topic that
* ePO threat events are published to.
* @example
* epoClient.addThreatEventCallback(function (threatEventObj, originalEvent) {
* console.log('Threat event on topic: ' + originalEvent.destinationTopic)
* console.log(threatEventObj)
* })
*/
EpoClient.prototype.addThreatEventCallback =
function (threatEventResponseCallback, topic) {
this._dxlClient.addEventCallback(topic || EPO_THREAT_EVENT_TOPIC,
function (event) {
var payload = MessageUtils.jsonPayloadToObject(event)
threatEventResponseCallback(payload, event)
})
}
/**
* Unregisters an event callback from the client so that it will no longer
* receive ePO threat events.
* @param {Function} threatEventResponseCallback - The function to unregister.
* @param {String} [topic=/mcafee/event/epo/threat/response] - The topic that
* ePO threat events are published to.
*/
EpoClient.prototype.removeThreatEventCallback =
function (threatEventResponseCallback, topic) {
this._dxlClient.removeEventCallback(
topic || EPO_THREAT_EVENT_TOPIC, threatEventResponseCallback)
}
module.exports = EpoClient