@mdf.js/openc2-core
Version:
MMS - API Core - OpenC2
247 lines • 9.85 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.Consumer = void 0;
/**
* Copyright 2024 Mytra Control S.L. All rights reserved.
*
* Use of this source code is governed by an MIT-style license that can be found in the LICENSE file
* or at https://opensource.org/licenses/MIT.
*/
const core_1 = require("@mdf.js/core");
const crash_1 = require("@mdf.js/crash");
const helpers_1 = require("../../helpers");
const types_1 = require("../../types");
const Component_1 = require("../Component");
const core_2 = require("./core");
class Consumer extends Component_1.Component {
/**
* Regular OpenC2 consumer implementation.
* @param adapter - transport adapter
* @param options - configuration options
*/
constructor(adapter, options) {
super(new core_2.AdapterWrapper(adapter, options.retryOptions), options);
/**
* Process incoming command message from the adapter
* @param incomingMessage - incoming message
* @param done - callback to return the response
*/
this.onCommandHandler = (incomingMessage, done) => {
this.processCommand(incomingMessage)
.then(result => done(undefined, result))
.catch(error => done(error, undefined));
};
/**
* Process incoming message and return a response if the message is a command that should be
* responded
* @param incomingMessage - incoming message
*/
this.processCommand = async (incomingMessage) => {
try {
const message = helpers_1.Checkers.isValidCommandSync(incomingMessage, this.componentId);
// Stryker disable next-line all
this.logger.debug(`New message from ${message.from} - ${message.request_id}`);
if (helpers_1.Checkers.isCommandToInstance(message, this.options.id)) {
return this.classifyCommand(message);
}
// Stryker disable next-line all
this.logger.debug(`${message.request_id} is not a command for this instance`);
return undefined;
}
catch (rawError) {
const error = crash_1.Crash.from(rawError);
const crashError = new crash_1.Crash(`Error processing incoming command message from control chanel: ${error.message}`, this.componentId, { cause: error });
this.onErrorHandler(crashError);
throw crashError;
}
};
this._router.on('command', this.onCommandHandler);
// Stryker disable next-line all
this.logger.debug(`OpenC2 Consumer created - [${options.id}]`);
this.validateResolver(options);
}
/**
* Validate the resolver map if exists
* @param options - configuration options
* @returns
*/
validateResolver(options) {
var _a;
if (!options.resolver) {
return;
}
for (const entry of Object.keys(options.resolver)) {
const { actionType, namespace, target } = this.validateResolverEntry(entry);
if (!this.options.actionTargetPairs[actionType]) {
throw new crash_1.Crash(`Invalid resolver entry, action type not supported: ${entry}`, {
name: 'ValidationError',
});
}
else if (!((_a = this.options.actionTargetPairs[actionType]) === null || _a === void 0 ? void 0 : _a.includes(`${namespace}:${target}`))) {
throw new crash_1.Crash(`Invalid resolver entry, target not supported: ${entry}`, {
name: 'ValidationError',
});
}
else {
return;
}
}
}
/**
* Validate a resolver entry format
* @param entry - resolver entry to validate
*/
validateResolverEntry(entry) {
const { 0: actionType, 1: namespace, 2: target } = entry.split(':');
if (!actionType || !namespace || !target) {
throw new crash_1.Crash(`Invalid resolver entry, invalid format: ${entry}`, {
name: 'ValidationError',
});
}
else if (!types_1.Control.ACTION_TYPES.includes(actionType)) {
throw new crash_1.Crash(`Invalid resolver entry, unknown action type: ${actionType}`, {
name: 'ValidationError',
});
}
else {
return {
actionType: actionType,
namespace: namespace,
target,
};
}
}
/** Consumer actuators */
get actuator() {
return this.options.actuator;
}
set actuator(value) {
this.options.actuator = value;
}
/** Consumer profiles */
get profiles() {
return this.options.profiles;
}
set profiles(value) {
this.options.profiles = value;
}
/** Consumer pairs */
get pairs() {
return this.options.actionTargetPairs;
}
set pairs(value) {
this.options.actionTargetPairs = value;
}
/** Initialize the OpenC2 component */
startup() {
return this.adapter.subscribe(this.onCommandHandler);
}
/** Shutdown the OpenC2 component */
shutdown() {
return this.adapter.unsubscribe(this.onCommandHandler);
}
/**
* Process incoming command message and select a default response or emit a new job to execute
* the command
* @param command - command message to be processed
*/
async classifyCommand(message) {
const defaultResponse = helpers_1.Checkers.hasDefaultResponse(message, this.options);
if (defaultResponse) {
this.register.push(message);
// Stryker disable next-line all
this.logger.debug(`${message.request_id} has default response`);
return defaultResponse;
}
else {
const resolver = this.getResolver(message);
if (resolver) {
return this.resolveCommand(resolver, message);
}
else {
return this.emitCommandJob(this.createJobFromCommand(message));
}
}
}
/**
* Resolve a command message using a resolver
* @param resolver - resolver function to be executed
* @param message - message to be processed
* @returns
*/
async resolveCommand(resolver, message) {
try {
const target = helpers_1.Accessors.getTargetFromCommand(message.content);
const response = await resolver(message.content.target[target]);
this.logger.info(`Command was resolved successfully`);
const result = response !== undefined && response !== null ? { [target]: response } : undefined;
return helpers_1.Helpers.ok(message, this.options.id, result);
}
catch (rawError) {
const cause = crash_1.Crash.from(rawError);
// Stryker disable next-line all
this.logger.error(`Command was resolved with errors: ${cause.message}`);
return helpers_1.Helpers.internalError(message, this.options.id, cause.trace().join(','));
}
}
/**
* Execute a job and wait for the resolution
* @param message - message to be processed as a job for upper layers
*/
async emitCommandJob(job) {
this.register.push(job);
return new Promise(resolve => {
const onCommandJobDone = (uuid) => {
var _a;
let response;
const commandJob = this.register.delete(uuid);
if (!commandJob) {
response = helpers_1.Helpers.internalError(job.data, this.options.id, `Job ${uuid} is not registered in the consumer. It can't be processed`);
}
else if (commandJob.hasErrors) {
// Stryker disable next-line all
this.logger.error(`Job ${commandJob.jobUserId} was finished with errors`);
response = helpers_1.Helpers.internalError(commandJob.data, this.options.id, `${(_a = job.errors) === null || _a === void 0 ? void 0 : _a.toString()}`);
}
else {
// Stryker disable next-line all
this.logger.info(`Job ${commandJob.jobUserId} was finished successfully`);
response = helpers_1.Helpers.ok(commandJob.data, this.options.id);
}
resolve(response);
};
job.once('done', onCommandJobDone);
this.emit('command', job);
});
}
/**
* Create a job from a command message
* @param message - command message
* @returns
*/
createJobFromCommand(message) {
// Stryker disable next-line all
this.logger.info(`Job - ${message.content.action}-${Object.keys(message.content.target)[0]}`);
const jobRequest = {
jobUserId: message.request_id,
data: message,
type: 'command',
options: {
headers: { duration: helpers_1.Accessors.getDelayFromCommandMessage(message) },
},
};
return new core_1.Jobs.JobHandler(jobRequest);
}
/**
* Check if there is a resolver function for the command in the resolver map
* @param message - message to be processed
*/
getResolver(message) {
var _a;
const target = helpers_1.Accessors.getTargetFromCommand(message.content);
const entry = `${message.content.action}:${target}`;
return (_a = this.options.resolver) === null || _a === void 0 ? void 0 : _a[entry];
}
}
exports.Consumer = Consumer;
//# sourceMappingURL=Consumer.js.map