@mdf.js/openc2-core
Version:
MMS - API Core - OpenC2
298 lines • 11.7 kB
JavaScript
"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