Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[backend] Implement log shipping to Graylog via GELF (#9629) #8410

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion opencti-platform/opencti-graphql/config/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"enabled": true,
"enabled_ui": true,
"enabled_dev_features": [],
"enable_logs_safe_shutdown": false,
"https_cert": {
"ca": [],
"key": null,
Expand All @@ -20,6 +21,12 @@
"logs_console": true,
"logs_max_files": 7,
"logs_directory": "./logs",
"logs_shipping": false,
"logs_shipping_level": "info",
"logs_shipping_env_var_prefix": "APP__",
"logs_graylog_host": "127.0.0.1",
"logs_graylog_port": 12201,
"logs_graylog_adapter": "udp",
"logs_redacted_inputs": ["password", "secret", "token"],
"extended_error_message": false
},
Expand All @@ -31,7 +38,13 @@
"logs_files": true,
"logs_console": true,
"logs_max_files": 7,
"logs_directory": "./logs"
"logs_directory": "./logs",
"logs_shipping": false,
"logs_shipping_level": "info",
"logs_shipping_env_var_prefix": "APP_AUDIT_",
"logs_graylog_host": "127.0.0.1",
"logs_graylog_port": 12201,
"logs_graylog_adapter": "udp"
},
"event_loop_logs": {
"enabled": false,
Expand Down
2 changes: 2 additions & 0 deletions opencti-platform/opencti-graphql/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@
"express-session": "1.18.1",
"fast-json-patch": "3.1.1",
"file-type": "19.6.0",
"gelf-pro": "1.4.0",
"github-api": "3.4.0",
"graphql": "16.9.0",
"graphql-constraint-directive": "5.4.3",
Expand Down Expand Up @@ -159,6 +160,7 @@
"validator": "13.12.0",
"winston": "3.17.0",
"winston-daily-rotate-file": "5.0.0",
"winston-transport": "4.9.0",
"ws": "8.18.0",
"xml2js": "0.6.2"
},
Expand Down
13 changes: 12 additions & 1 deletion opencti-platform/opencti-graphql/src/boot.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { environment, getStoppingState, logApp, setStoppingState } from './config/conf';
import { environment, getStoppingState, logApp, setStoppingState, shutdownLoggers } from './config/conf';

Check warning on line 1 in opencti-platform/opencti-graphql/src/boot.js

View check run for this annotation

Codecov / codecov/patch

opencti-platform/opencti-graphql/src/boot.js#L1

Added line #L1 was not covered by tests
import platformInit, { checkFeatureFlags, checkSystemDependencies } from './initialization';
import cacheManager from './manager/cacheManager';
import { shutdownRedisClients } from './database/redis';
Expand Down Expand Up @@ -38,6 +38,17 @@
throw modulesError;
}
} catch (mainError) {
try {
await shutdownLoggers();
} catch (e) {
/*
errors when shutting down the loggers can't be logged to them, so we just try using the standard console as a
"best effort" to give them some visibility
*/
// eslint-disable-next-line no-console
console.error(e);
}

Check warning on line 50 in opencti-platform/opencti-graphql/src/boot.js

View check run for this annotation

Codecov / codecov/patch

opencti-platform/opencti-graphql/src/boot.js#L41-L50

Added lines #L41 - L50 were not covered by tests

process.exit(1);
}
};
Expand Down
31 changes: 31 additions & 0 deletions opencti-platform/opencti-graphql/src/config/conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import { ENTITY_TYPE_PUBLIC_DASHBOARD } from '../modules/publicDashboard/publicDashboard-types';
import { AI_BUS } from '../modules/ai/ai-types';
import { SUPPORT_BUS } from '../modules/support/support-types';
import { createLogShippingTransport } from './log-shipping';

// https://golang.org/src/crypto/x509/root_linux.go
const LINUX_CERTFILES = [
Expand Down Expand Up @@ -97,6 +98,7 @@
const appLogLevel = nconf.get('app:app_logs:logs_level');
const appLogFileTransport = booleanConf('app:app_logs:logs_files', true);
const appLogConsoleTransport = booleanConf('app:app_logs:logs_console', true);
const appLogShippingTransport = booleanConf('app:app_logs:logs_shipping', false);
export const appLogLevelMaxDepthSize = nconf.get('app:app_logs:control:max_depth_size') ?? 5;
export const appLogLevelMaxDepthKeys = nconf.get('app:app_logs:control:max_depth_keys') ?? 30;
export const appLogLevelMaxArraySize = nconf.get('app:app_logs:control:max_array_size') ?? 50;
Expand Down Expand Up @@ -197,6 +199,10 @@
if (appLogConsoleTransport) {
appLogTransports.push(new winston.transports.Console());
}
if (appLogShippingTransport) {
const conf = nconf.get('app:app_logs');
appLogTransports.push(createLogShippingTransport(conf));
}

Check warning on line 205 in opencti-platform/opencti-graphql/src/config/conf.js

View check run for this annotation

Codecov / codecov/patch

opencti-platform/opencti-graphql/src/config/conf.js#L203-L205

Added lines #L203 - L205 were not covered by tests

const appLogger = winston.createLogger({
level: appLogLevel,
Expand All @@ -207,6 +213,7 @@
// Setup audit log logApp
const auditLogFileTransport = booleanConf('app:audit_logs:logs_files', true);
const auditLogConsoleTransport = booleanConf('app:audit_logs:logs_console', true);
const auditLogShippingTransport = booleanConf('app:audit_logs:logs_shipping', false);
const auditLogTransports = [];
if (auditLogFileTransport) {
const dirname = nconf.get('app:audit_logs:logs_directory');
Expand All @@ -222,6 +229,10 @@
if (auditLogConsoleTransport) {
auditLogTransports.push(new winston.transports.Console());
}
if (auditLogShippingTransport) {
const conf = nconf.get('app:audit_logs');
auditLogTransports.push(createLogShippingTransport(conf));
}

Check warning on line 235 in opencti-platform/opencti-graphql/src/config/conf.js

View check run for this annotation

Codecov / codecov/patch

opencti-platform/opencti-graphql/src/config/conf.js#L233-L235

Added lines #L233 - L235 were not covered by tests
const auditLogger = winston.createLogger({
level: 'info',
format: format.combine(timestamp(), format.errors({ stack: true }), format.json()),
Expand Down Expand Up @@ -311,6 +322,26 @@
}
};

export function shutdownLoggers() {
const safeShutdown = booleanConf('app:enable_logs_safe_shutdown', false);

Check warning on line 326 in opencti-platform/opencti-graphql/src/config/conf.js

View check run for this annotation

Codecov / codecov/patch

opencti-platform/opencti-graphql/src/config/conf.js#L326

Added line #L326 was not covered by tests

if (safeShutdown) {
const shutdownPromises = [appLogger, auditLogger, supportLogger].map(
(logger) => new Promise(
(resolve) => {
logger
.end()
.on('finish', resolve);
}
)
);

Check warning on line 337 in opencti-platform/opencti-graphql/src/config/conf.js

View check run for this annotation

Codecov / codecov/patch

opencti-platform/opencti-graphql/src/config/conf.js#L328-L337

Added lines #L328 - L337 were not covered by tests

return Promise.all(shutdownPromises);
}

Check warning on line 340 in opencti-platform/opencti-graphql/src/config/conf.js

View check run for this annotation

Codecov / codecov/patch

opencti-platform/opencti-graphql/src/config/conf.js#L339-L340

Added lines #L339 - L340 were not covered by tests

return Promise.resolve();
}

Check warning on line 343 in opencti-platform/opencti-graphql/src/config/conf.js

View check run for this annotation

Codecov / codecov/patch

opencti-platform/opencti-graphql/src/config/conf.js#L342-L343

Added lines #L342 - L343 were not covered by tests

const BasePathConfig = nconf.get('app:base_path')?.trim() ?? '';
const AppBasePath = BasePathConfig.endsWith('/') ? BasePathConfig.slice(0, -1) : BasePathConfig;
export const basePath = isEmpty(AppBasePath) || AppBasePath.startsWith('/') ? AppBasePath : `/${AppBasePath}`;
Expand Down
52 changes: 52 additions & 0 deletions opencti-platform/opencti-graphql/src/config/gelf-transport.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/* eslint-disable @typescript-eslint/no-var-requires, no-multi-assign, guard-for-in, no-restricted-syntax, no-param-reassign --
* This is a straight copy of https://github.com/fchristle/winston-gelf/blob/5420e52bc6a9830dc4a56494097c752fddcfcabc/index.js
* with a single change, noted below.
* We disable the eslint rules that would cause warnings in the original code.
*/
bnazare marked this conversation as resolved.
Show resolved Hide resolved

const Transport = require('winston-transport');
const logger = require('gelf-pro');

const levels = {
emerg: 'emergency',
alert: 'alert',
crit: 'critical',
error: 'error',
warn: 'warn',
notice: 'notice',
info: 'info',
debug: 'debug',
};

class GelfTransport extends Transport {
constructor(opts) {
super(opts);
this.logger = Object.create(logger);
this.logger.setConfig(opts.gelfPro);
}

Check warning on line 26 in opencti-platform/opencti-graphql/src/config/gelf-transport.js

View check run for this annotation

Codecov / codecov/patch

opencti-platform/opencti-graphql/src/config/gelf-transport.js#L23-L26

Added lines #L23 - L26 were not covered by tests

log({ level, message, ...extra }, callback) {
setImmediate(() => {
this.emit('logged', { level, message, extra });
});

if (typeof extra === 'object') {
for (const key in extra) {
const value = extra[key];
if (value instanceof Error) {
extra = value;
}
}
}

const graylogLevel = levels[level] || levels.info;
// CHANGE: use "callback" as the callback of the logging function
this.logger[graylogLevel](message, extra, () => callback());
}

Check warning on line 45 in opencti-platform/opencti-graphql/src/config/gelf-transport.js

View check run for this annotation

Codecov / codecov/patch

opencti-platform/opencti-graphql/src/config/gelf-transport.js#L29-L45

Added lines #L29 - L45 were not covered by tests

setConfig(opts) {
this.logger.setConfig(opts.gelfPro);
}

Check warning on line 49 in opencti-platform/opencti-graphql/src/config/gelf-transport.js

View check run for this annotation

Codecov / codecov/patch

opencti-platform/opencti-graphql/src/config/gelf-transport.js#L48-L49

Added lines #L48 - L49 were not covered by tests
}

module.exports = exports = GelfTransport;
50 changes: 50 additions & 0 deletions opencti-platform/opencti-graphql/src/config/log-shipping.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { format } from 'winston';
import GelfTransport from './gelf-transport';

/**
* Create a new log shipping transport.
* @param {Object} conf The transport configuration
* @param {string} conf.logs_shipping_level The minimum log level of messages to send to ship
* @param {string} conf.logs_shipping_env_var_prefix The prefix used to match environment variables. Matching
* variables will be added as meta info to the log data. The value of this property will be stripped from the name
* of the environment variable.
* @param {string} conf.logs_graylog_host The Graylog host to connect to
* @param {number} conf.logs_graylog_port The port to use when connecting to the Graylog host
* @param {'tcp'|'udp'} conf.logs_graylog_adapter The adapter (udp/tcp) to use when connecting to the Graylog host
* @returns {import('winston-gelf')} The newly created log shipping transport
*/
export function createLogShippingTransport(conf) {
return new GelfTransport({
level: conf.logs_shipping_level,
format: format.combine(
envVarsFormat(conf.logs_shipping_env_var_prefix)(),
format.json(),
),
gelfPro: {
adapterName: `${conf.logs_graylog_adapter}.js`, // append '.js', as a workaround for https://github.com/evanw/esbuild/issues/3328
adapterOptions: {
host: conf.logs_graylog_host,
port: conf.logs_graylog_port,
},
},
});
}

Check warning on line 31 in opencti-platform/opencti-graphql/src/config/log-shipping.js

View check run for this annotation

Codecov / codecov/patch

opencti-platform/opencti-graphql/src/config/log-shipping.js#L17-L31

Added lines #L17 - L31 were not covered by tests

function envVarsFormat(prefix) {
const envVars = findPrefixedEnvVars(prefix);

Check warning on line 34 in opencti-platform/opencti-graphql/src/config/log-shipping.js

View check run for this annotation

Codecov / codecov/patch

opencti-platform/opencti-graphql/src/config/log-shipping.js#L33-L34

Added lines #L33 - L34 were not covered by tests

return format(
(info) => ({ ...info, ...envVars })
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct me if I'm wrong but this will put in the logs all env variables prefixed with APP__ for instance ?
Why do you need these ?
They might contain sensitive information.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's precisely what it does.

The reasoning is that when you have all the machines in a cluster, shipping all their logs to a central location, it becomes virtually impossible to distinguish between the logs originating from different instances. This feature is intended to help with this issue by allowing the users to expose (as meta-data) information about the originating instance so that it becomes easily identifiable.

The actual prefix is configurable in the JSON config so the users can make it very explicit, e.g. "EXPOSE_TO_GRAYLOG_". This way nothing unintended will be exposed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello, to identify machine you can add the NODE_INSTANCE_ID

export const NODE_INSTANCE_ID = nconf.get('app:node_identifier') || uuid();

And if you need, the node id can be configured per node in env with APP__NODE_IDENTIFIER

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer an opt-in approach indeed : just add what you need. It seems dangerous to be able to gather all secrets in the environment and send them

);
}

Check warning on line 39 in opencti-platform/opencti-graphql/src/config/log-shipping.js

View check run for this annotation

Codecov / codecov/patch

opencti-platform/opencti-graphql/src/config/log-shipping.js#L36-L39

Added lines #L36 - L39 were not covered by tests

function findPrefixedEnvVars(prefix) {
return Object.fromEntries(
Object.entries(process.env)
.flatMap(([key, value]) => {
return key.startsWith(prefix)
? [[key.substring(prefix.length), value]]
: [];
})
);
}

Check warning on line 50 in opencti-platform/opencti-graphql/src/config/log-shipping.js

View check run for this annotation

Codecov / codecov/patch

opencti-platform/opencti-graphql/src/config/log-shipping.js#L41-L50

Added lines #L41 - L50 were not covered by tests
33 changes: 22 additions & 11 deletions opencti-platform/opencti-graphql/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -8649,6 +8649,15 @@ __metadata:
languageName: node
linkType: hard

"gelf-pro@npm:1.4.0":
version: 1.4.0
resolution: "gelf-pro@npm:1.4.0"
dependencies:
lodash: "npm:~4.17.21"
checksum: 10/65fa4a38f1d9c4f48fbbb4bc8cd11fe9333cd26dce387ca96e52ea91b5bfc0eaf5370ab49e7a2db1e7f262d74751a5e5e2c8d5d37fa03aa3b427b1db7b13bcde
languageName: node
linkType: hard

"gensync@npm:^1.0.0-beta.2":
version: 1.0.0-beta.2
resolution: "gensync@npm:1.0.0-beta.2"
Expand Down Expand Up @@ -11469,6 +11478,7 @@ __metadata:
fast-glob: "npm:3.3.3"
fast-json-patch: "npm:3.1.1"
file-type: "npm:19.6.0"
gelf-pro: "npm:1.4.0"
github-api: "npm:3.4.0"
graphql: "npm:16.9.0"
graphql-constraint-directive: "npm:5.4.3"
Expand Down Expand Up @@ -11530,6 +11540,7 @@ __metadata:
vitest: "npm:2.0.5"
winston: "npm:3.17.0"
winston-daily-rotate-file: "npm:5.0.0"
winston-transport: "npm:4.9.0"
ws: "npm:8.18.0"
xml2js: "npm:0.6.2"
languageName: unknown
Expand Down Expand Up @@ -14528,6 +14539,17 @@ __metadata:
languageName: node
linkType: hard

"winston-transport@npm:4.9.0, winston-transport@npm:^4.9.0":
version: 4.9.0
resolution: "winston-transport@npm:4.9.0"
dependencies:
logform: "npm:^2.7.0"
readable-stream: "npm:^3.6.2"
triple-beam: "npm:^1.3.0"
checksum: 10/5946918720baadd7447823929e94cf0935f92c4cff6d9451c6fcb009bd9d20a3b3df9ad606109e79d1e9f4d2ff678477bf09f81cfefce2025baaf27a617129bb
languageName: node
linkType: hard

"winston-transport@npm:^4.7.0":
version: 4.8.0
resolution: "winston-transport@npm:4.8.0"
Expand All @@ -14539,17 +14561,6 @@ __metadata:
languageName: node
linkType: hard

"winston-transport@npm:^4.9.0":
version: 4.9.0
resolution: "winston-transport@npm:4.9.0"
dependencies:
logform: "npm:^2.7.0"
readable-stream: "npm:^3.6.2"
triple-beam: "npm:^1.3.0"
checksum: 10/5946918720baadd7447823929e94cf0935f92c4cff6d9451c6fcb009bd9d20a3b3df9ad606109e79d1e9f4d2ff678477bf09f81cfefce2025baaf27a617129bb
languageName: node
linkType: hard

"winston@npm:3.17.0":
version: 3.17.0
resolution: "winston@npm:3.17.0"
Expand Down