UNPKG

@mdf.js/openc2-core

Version:

MMS - API Core - OpenC2

298 lines 11.7 kB
"use strict"; /** * 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. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.Gateway = void 0; const tslib_1 = require("tslib"); const Health_1 = require("@mdf.js/core/dist/Health"); const crash_1 = require("@mdf.js/crash"); const logger_1 = require("@mdf.js/logger"); const events_1 = tslib_1.__importDefault(require("events")); const lodash_1 = require("lodash"); const uuid_1 = require("uuid"); const Router_1 = require("../../Router"); const helpers_1 = require("../../helpers"); const modules_1 = require("../../modules"); const Consumer_1 = require("../Consumer"); const Producer_1 = require("../Producer"); const MIN_LOOKUP_INTERVAL = 10000; const AGING_INTERVAL_FACTOR = 3; const MAX_AGED_FACTOR = 3; const MIN_GATEWAY_DELAY = 1000; class Gateway extends events_1.default { /** * Regular OpenC2 gateway implementation. * @param upstream - upstream consumer adapter interface * @param downstream - downstream producer adapter interface * @param options - configuration options */ constructor(upstream, downstream, options) { var _a, _b; super(); this.options = options; /** Component identification */ this.componentId = (0, uuid_1.v4)(); /** * Forward the command to the downstream producer * @param job - job to be processed */ this.onUpstreamCommandHandler = (job) => { // Stryker disable all this.logger.debug(`Received command from upstream: ${job.data.request_id}`); this.logger.silly(`Direction: ${job.data.from} - ${job.data.to}`); this.logger.silly(`Command: ${job.data.content.action} - ${JSON.stringify(job.data.content.target, null, 2)}`); // Stryker enable all let adaptedCommand; try { adaptedCommand = this.adaptCommand(job.data); } catch (rawError) { const error = crash_1.Crash.from(rawError); // Stryker disable next-line all this.logger.error(`Error adapting command: ${error.message}`); job.done(error); return; } this.producer .command(adaptedCommand) .then(responses => { var _a; for (const response of responses) { // Stryker disable all this.logger.debug(`Command response: ${response.request_id}`); this.logger.silly(`Direction: ${response.from} - ${job.data.to}`); this.logger.silly(`Command: ${(_a = response.content.results) === null || _a === void 0 ? void 0 : _a.pairs}`); // Stryker enable all } job.done(); }) .catch(job.done); }; /** Update the features of the upstream consumer based in the upstream consumer map */ this.updateConsumerOptions = () => { const { pairs, profiles } = this.producer.consumerMap.getGroupedFeatures(); this.consumer.pairs = pairs; this.consumer.profiles = profiles; }; this.logger = (0, logger_1.SetContext)((_a = this.options.logger) !== null && _a !== void 0 ? _a : new logger_1.DebugLogger(`mdf:oc2:gateway:${this.name}`), this.constructor.name, this.componentId); this.register = (_b = this.options.registry) !== null && _b !== void 0 ? _b : new modules_1.Registry(this.options.id, this.options.maxInactivityTime, this.options.registerLimit); this._router = new Router_1.Router(this.register); this.consumer = new Consumer_1.Consumer(upstream, { ...this.options, registry: this.register }); this.producer = new Producer_1.Producer(downstream, { ...this.options, ...(this.options.bypassLookupIntervalChecks ? {} : this.checkLookupTimes(this.options)), registry: this.register, }); this.health = new modules_1.HealthWrapper(this.options.id, [this.consumer, this.producer]); this.consumer.on('command', this.onUpstreamCommandHandler); this.producer.consumerMap.on('updated', this.updateConsumerOptions); this.started = false; // Stryker disable next-line all this.logger.debug(`OpenC2 Gateway created - [${options.id}]`); } /** Component name */ get name() { return this.options.id; } /** Component status */ get status() { return (0, Health_1.overallStatus)(this.health.checks); } /** * Return the status of the Consumer in a standard format * @returns _check object_ as defined in the draft standard * https://datatracker.ietf.org/doc/html/draft-inadarei-api-health-check-05 */ get checks() { return this.health.checks; } /** Return an Express router with access to errors registry */ get router() { return this._router.router; } /** Return links offered by this service */ get links() { return { openc2: { jobs: '/openc2/jobs', pendingJobs: '/openc2/pendingJobs', messages: '/openc2/messages', }, }; } /** Connect the OpenC2 underlayer component and perform the startup of the component */ start() { if (this.started) { return Promise.resolve(); } return new Promise((resolve, reject) => { this.producer .start() .then(() => this.consumer.start()) .then(() => { this.health.on('error', this.onErrorHandler); this.health.on('status', this.onStatusHandler); }) .then(() => { this.started = true; resolve(); }) .catch(reject); }); } /** Disconnect the OpenC2 underlayer component and perform the startup of the component */ stop() { if (!this.started) { return Promise.resolve(); } return new Promise((resolve, reject) => { this.health.off('error', this.onErrorHandler); this.health.off('status', this.onStatusHandler); this.consumer .stop() .then(() => this.producer.stop()) .then(() => { this.started = false; resolve(); }) .catch(reject); }); } /** Close the OpenC2 gateway */ close() { return this.stop(); } /** Consumer Map */ get consumerMap() { return this.producer.consumerMap; } /** * Check the lookup time fix them if area wrong * @param options - configuration options * @returns */ checkLookupTimes(options) { const lookupInterval = this.isHigherThan(MIN_LOOKUP_INTERVAL, options.lookupInterval); const lookupTimeout = this.isLowerThan(lookupInterval / 2, options.lookupTimeout); const agingInterval = this.isHigherThan(lookupInterval * AGING_INTERVAL_FACTOR, options.agingInterval); const maxAge = this.isHigherThan(agingInterval * MAX_AGED_FACTOR, options.maxAge); const delay = this.isHigherThan(MIN_GATEWAY_DELAY, options.delay); const selectedOptions = { lookupInterval, lookupTimeout, agingInterval, maxAge, delay, }; // Stryker disable next-line all this.logger.debug(`Gateway options ${JSON.stringify(selectedOptions, null, 2)}`); return selectedOptions; } /** * Check if the value is higher than the limit, it not returns the limit * @param value - value to be checked * @param limit - limit * @returns */ isHigherThan(limit, value) { return value && value > limit ? value : limit; } /** * Check if the value is lower than the limit, it not returns the limit * @param value - value to be checked * @param limit - limit * @returns */ isLowerThan(limit, value) { return value && value < limit ? value : limit; } /** * Adapt the command to be forwarded * @param command - command to be processed * @returns */ adaptCommand(command) { const adaptedCommand = (0, lodash_1.cloneDeep)(command); adaptedCommand.from = this.options.id; adaptedCommand.to = this.getForwardAddresses(command); adaptedCommand.content.args = this.getArgs(command); return adaptedCommand; } /** * Get the update args for command execution * @param command - command to be processed * @returns */ getArgs(command) { var _a, _b, _c, _d; const actualDelay = helpers_1.Accessors.getDelayFromCommandMessage(command); if (actualDelay - ((_a = this.options.delay) !== null && _a !== void 0 ? _a : MIN_GATEWAY_DELAY) > 0) { return { start_time: (_b = command.content.args) === null || _b === void 0 ? void 0 : _b.start_time, stop_time: undefined, duration: actualDelay - ((_c = this.options.delay) !== null && _c !== void 0 ? _c : MIN_GATEWAY_DELAY), response_requested: (_d = command.content.args) === null || _d === void 0 ? void 0 : _d.response_requested, }; } else { const error = new crash_1.Crash(`No enough time to perform the forwarding of the command`, this.componentId, { info: { command, subject: 'OpenC2 Gateway' } }); this.onErrorHandler(error); throw error; } } /** * Get the addresses to forward the command * @param command - command to be forwarded * @returns */ getForwardAddresses(command) { const action = helpers_1.Accessors.getActionFromCommand(command.content); const target = helpers_1.Accessors.getTargetFromCommand(command.content); const profile = target.split(':')[0]; const destinations = this.producer.consumerMap.getConsumersWithPair(action, target); const actuator = helpers_1.Accessors.getActuatorAssetId(command.content, profile); if (command.to.includes('*')) { return ['*']; } else if (actuator) { return [actuator]; } else if (destinations.length > 0) { return destinations; } else { const error = new crash_1.Crash(`No valid destination found for this command`, this.componentId, { info: { command, subject: 'OpenC2 Gateway' }, }); this.onErrorHandler(error); throw error; } } /** * Manage the error in the producer interface * @param error - error to be processed */ onErrorHandler(error) { const crash = crash_1.Crash.from(error); this.logger.crash(crash); if (this.listenerCount('error') > 0) { this.emit('error', crash); } } /** * Manage the status change in the producer interface * @param status - status to be processed */ onStatusHandler(status) { if (this.listenerCount('status') > 0) { this.emit('status', status); } } } exports.Gateway = Gateway; //# sourceMappingURL=Gateway.js.map