UNPKG

@mdf.js/openc2-core

Version:

MMS - API Core - OpenC2

247 lines 9.85 kB
"use strict"; 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