master
ermisw 2024-02-09 09:13:43 +01:00
commit b1e0651885
9 changed files with 8053 additions and 0 deletions

130
.gitignore vendored Normal file
View File

@ -0,0 +1,130 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

7143
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

28
package.json Normal file
View File

@ -0,0 +1,28 @@
{
"name": "derdackbhomwebhook",
"version": "1.0.0",
"description": "",
"main": "index.js",
"type": "commonjs",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "npx webpack --config webpack.config.js"
},
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.23.7",
"@babel/preset-env": "^7.23.8",
"babel-loader": "^9.1.3",
"webpack": "^5.89.0",
"webpack-bundle-analyzer": "^4.10.1",
"webpack-cli": "^5.1.4",
"webpack-node-externals": "^3.0.0"
},
"dependencies": {
"deepmerge": "^4.3.1",
"dotenv": "^16.3.1",
"mustache": "^4.2.0",
"request": "^2.8.2"
}
}

27
src/Logger.js Normal file
View File

@ -0,0 +1,27 @@
var mockPrefixEnabled = false;
function enableMockPrefix()
{
mockPrefixEnabled = true;
}
function writeLog(identifier, message)
{
if (!mockPrefixEnabled)
{
console.log(`${identifier} | ${message}`);
return;
}
var date = `${new Date()} | `;
console.log(`${date}${identifier} | ${message}`);
}
module.exports = {
writeLog: writeLog,
enableMockPrefix: enableMockPrefix
}

195
src/Mock.js Normal file
View File

@ -0,0 +1,195 @@
const Script = require('./Main.js');
class Mock {
constructor()
{
this.Logger = require('./Logger');
this.Logger.enableMockPrefix();
this.Express = null;
this.Webapp = null;
this.callbackOnSubscriptionEvent = null;
this.listenPort = 8080;
}
getLogger()
{
return this.Logger;
}
getLogFnName(fnName)
{
return `Mock::${fnName}`;
}
mockOnGetProcessId(pid)
{
this.Logger.writeLog(this.getLogFnName(this.mockOnGetProcessId.name), `*** Node script reported the following PID: ${pid} ***`);
}
mockCallbackSaveState(state)
{
this.Logger.writeLog(this.getLogFnName(this.mockCallbackSaveState.name), `*** State Saved ***`);
}
mockCallbackSetStatusError(message)
{
this.Logger.writeLog(this.getLogFnName(this.mockCallbackSetStatusError.name), `*** Script status changed to ERROR. User message: ${message} ***`);
}
mockCallbackSetStatusOK(message)
{
this.Logger.writeLog(this.getLogFnName(this.mockCallbackSetStatusOK.name), `*** Script status changed to OK. User message: ${message} ***`);
}
mockOnSubscriptionEvent(callbackOnSubscriptionEvent, scriptName)
{
this.Express = require('express');
this.Webapp = this.Express();
this.callbackOnSubscriptionEvent = callbackOnSubscriptionEvent;
this.Webapp.use(this.Express.json());
this.Webapp.post(`/eventFromS4-${scriptName}`, function (request, response) {
try
{
this.callbackOnSubscriptionEvent((object1, returnValue) => {}, request.body);
}
catch (error)
{
this.Logger.writeLog(this.getLogFnName(this.mockOnSubscriptionEvent.name), `Error on handling subscription event: ${error}`);
}
response.set('Content-Type', 'text/plain');
response.send('OK');
}.bind(this));
this.Webapp.listen(this.listenPort, function (error) {
if (error)
{
this.Logger.writeLog(this.getLogFnName(this.mockOnSubscriptionEvent.name), `Error while trying to listen on port: ${this.listenPort}: ${error}`);
return;
}
this.Logger.writeLog(this.getLogFnName(this.mockOnSubscriptionEvent.name), `eventFromS4 endpoint is listening on port ${this.listenPort}..`);
}.bind(this));
}
async mockCallbackRaiseEvent(event, targetURL)
{
var request = require('request');
let options = {
url: targetURL,
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(event, null, 2)
};
return new Promise((resolve, reject) => {
request.post(options, function (error, response, body) {
if (error)
{
this.Logger.writeLog(this.getLogFnName(this.mockCallbackRaiseEvent.name), `Error while sending event to S4: ${error.message}`);
return reject(error);
}
this.Logger.writeLog(this.getLogFnName(this.mockCallbackRaiseEvent.name), `Event SENT to S4. Status code received: ${response.statusCode}`);
return resolve(true);
}.bind(this));
}).catch((error) => {
this.Logger.writeLog(this.getLogFnName(this.mockCallbackRaiseEvent.name), `Following error occurred while sending to S4: ${error}`);
return false;
});
}
}
function mockCallbackSetStatusError(message)
{
new Mock().mockCallbackSetStatusError(message);
}
function mockCallbackSetStatusOK(message)
{
new Mock().mockCallbackSetStatusOK(message);
}
async function mockCallbackRaiseEvent(event)
{
await new Mock().mockCallbackRaiseEvent(event, "https://connect.signl4.com/webhook/seo6p3wh");
}
function mockCallbackSaveState(state)
{
new Mock().mockCallbackSaveState(state);
}
var appContext = {
config: {
serverURL: '',
username: '',
password: ''
},
runtimeInfo: {
subscriptionId: '',
scriptId: '413f5213-40ab-468a-b0f2-cd898f749cbb', // Choose what you want for dev from https://www.uuidgenerator.net/version4
scriptName: 'ConnectorZabbix',
callbackRaiseEvent: mockCallbackRaiseEvent,
callbackSetStatusError: mockCallbackSetStatusError,
callbackSetStatusOK: mockCallbackSetStatusOK
},
state: {
items: new Array(),
callbackSaveState: mockCallbackSaveState
}
}
// Creat mock subscription event handler with use of a webhook listener via express
//
var mockSubscriptionEvent = new Mock();
mockSubscriptionEvent.mockOnSubscriptionEvent(Script.onSubscriptionEvent, appContext.runtimeInfo.scriptName);
// Have mocked-prefix logging in the app
//
Script.setLogger(mockSubscriptionEvent.getLogger());
// Get Process Id for Actor
//
Script.onGetProcessId((object1, returnValue) => {
new Mock().mockOnGetProcessId(returnValue);
});
// Configure app
//
Script.onConfigureApp((object1, returnValue) => {
}, appContext);
// Start
//
Script.onStartBackgroundJobs((object1, returnValue) => {
});
//const timeoutObj = setTimeout(onStopBackgroundJobs, 9500);

25
src/Result.js Normal file
View File

@ -0,0 +1,25 @@
class Result {
constructor(ok, data) {
this._ok = ok;
this._data = data;
}
get ok() {
return this._ok;
}
get data() {
return this._data;
}
}
module.exports = Result;

14
src/config.json Normal file
View File

@ -0,0 +1,14 @@
{
"open": {
"active": false,
"note": "Opened in Derdack with id"
},
"acknowledge": {
"active": true,
"note": "EA: Event was acknowledged by user "
},
"close": {
"active": false,
"note": "Test event id {{id}}"
}
}

445
src/index.js Normal file
View File

@ -0,0 +1,445 @@
var dotenv = require('dotenv').config({ path: __dirname + '/.env' })
var request = require('request');
const Result = require('./Result');
var Logger = require('./Logger');
const { readFileSync } = require('fs');
const merge = require('deepmerge');
const Mustache = require('mustache');
var defaultConfig = {
"open": {
"id": 0,
"active": false,
"note": "Opened in Derdack with id"
},
"acknowledge": {
"endpoint": "/events-service/api/v1.0/events/{{alert.externalEventId}}",
"sendF": request.patch,
"sendNF": request.post,
"id": 1,
"active": true,
"note": "EA: Event was acknowledged by user {{user.username}} and owner set",
"body": {
"status": "ACK",
"user_assigned": "{{user.username}}"
}
},
"close": {
"endpoint": "/events-service/api/v1.0/events/operations/close",
"sendF": request.post,
"id": 2,
"active": false,
"body": {
"eventIds": [
"{{alert.externalEventId}}"
],
"slots": {
"notes": "EA: Event was close by user {{user.username}}"
}
}
}
};
module.exports = {
onDispose: async function (callback) {
callback(null, await onDispose());
},
// Useful to let this app be configured in a UI by the user
onGetAppConfig: async function (callback) {
callback(null, await onGetAppConfig());
},
// Useful to let this app be configured in a UI by the user
onConfigureApp: async function (callback, context) {
callback(null, await onConfigureApp(context));
},
// Handle various events from EA related to the team or alerts
onSubscriptionEvent: async function (callback, event) {
callback(null, await onSubscriptionEvent(event));
},
onGetProcessId: function (callback) {
callback(null, (() => {
let process = require('process');
return process.pid;
})());
},
setLogger: (logger) => {
Logger = logger;
}
};
// Context with config and callbacks from EA
var appContext = null;
async function onConfigureApp(context) {
let fnName = onConfigureApp.name;
try {
// Save app context locally here in that script. This is not failure save, of course
//
appContext = context;
// Build callbacks that allow us to send events to EA
//
if (appContext.state.callbackSaveState != undefined)
appContext.state.callbackSaveState = eval(appContext.state.callbackSaveState);
else
appContext.state.callbackSaveState = () => { };
if (appContext.runtimeInfo.callbackSetStatusError != undefined)
appContext.runtimeInfo.callbackSetStatusError = eval(appContext.runtimeInfo.callbackSetStatusError);
else
appContext.runtimeInfo.callbackSetStatusError = () => { };
if (appContext.runtimeInfo.callbackSetStatusOK != undefined)
appContext.runtimeInfo.callbackSetStatusOK = eval(appContext.runtimeInfo.callbackSetStatusOK);
else
appContext.runtimeInfo.callbackSetStatusOK = () => { };
if (appContext.runtimeInfo.callbackSendMail != undefined)
appContext.runtimeInfo.callbackSendMail = eval(appContext.runtimeInfo.callbackSendMail);
else
appContext.runtimeInfo.callbackSendMail = () => { };
Logger.writeLog(fnName, `Configured Target Url: ${appContext.config.targetUrl}`);
let customConfig = JSON.parse(readFileSync(__dirname + '/config.json'));
appContext.config["webhookConfig"]= merge(defaultConfig,customConfig);
console.log(appContext.config["webhookConfig"]);
//Check first if EnvironmentVaribales are set.
//Both JWT Token or apiKey are possible
if (process.env["bhome_access_key"] && process.env["bhome_access_secret_key"]) {
appContext.config["access_key"] = process.env["bhome_access_key"];
appContext.config["access_secret_key"] = process.env["bhome_access_secret_key"];
appContext.config["authmod"] = "Bearer";
} else if (process.env["bhome_api_token"]) {
appContext.config["api_key"] = process.env["bhome_api_key"];
appContext.config["authmod"] = "apiKey";
}
// Get Parameters in URL overrides Environment Variables
let bhomUrl = new URL(appContext.config.targetUrl);
if(bhomUrl.searchParams.get("access_key") && bhomUrl.searchParams.get("access_secret_key")) {
appContext.config["access_key"] = bhomUrl.searchParams.get("access_key");
appContext.config["access_secret_key"] = bhomUrl.searchParams.get("access_secret_key");
appContext.config["authmod"] = "Bearer";
}
else if(bhomUrl.searchParams.get("apiKey")) {
appContext.config["api_key"] = bhomUrl.searchParams.get("apiKey");
appContext.config["authmod"] = "apiKey";
}
return true;
}
catch (error) {
Logger.writeLog(fnName, `Error on handling configuration: ${error.message}`);
return false;
}
}
async function onGetAppConfig() {
// App could update config with e.g. a current connection status
return appContext.config;
}
function getWebhookConfig(actionCode) {
for (var key of Object.keys(appContext.config["webhookConfig"])) {
if(appContext.config["webhookConfig"][key]["id"] == actionCode && appContext.config["webhookConfig"][key]["active"]==true) {
return appContext.config["webhookConfig"][key];
}
}
console.log("no active WebhookConfig found for action code "+ actionCode);
return null;
}
async function onSubscriptionEvent(event) {
let fnName = onSubscriptionEvent.name;
Logger.writeLog(fnName, event.eventType);
if (!appContext.config["authmod"]) {
console.log("bhome_access_key, bhome_access_secret_key OR bhome_api_key must be set as environment variables");
return true;
}
try {
// Get Event ID
var eventId = event.alert.externalEventId;
Logger.writeLog(fnName, "Value EventID: " + eventId);
// Exit if no ZabbixEventID found
if (eventId == "") {
return true;
}
let bhom_event_id = "";
for (param of event.alert.parameters) {
if (param.name.toLocaleLowerCase() == "event id") {
//Logger.writeLog(fnName, param.value);
bhom_event_id = param.value;
break;
}
}
if (bhom_event_id == "") {
Logger.writeLog(fnName, "Event update in BHOM failed! No bhome_event_id found! Please make sure to include Event ID in the webhook payload that gets sent from BHOM to EnterpriseAlert..");
return true;
}
Logger.writeLog(fnName, `bhom_event_id: ${bhom_event_id}`);
Logger.writeLog(fnName, `event type: ${event.eventType} and statusCode: ${event.alert.statusCode}`);
// Handle this update
//
var message = "";
var actionCode = 0;
if (event.eventType === 201 && event.alert.statusCode === 2) {
Logger.writeLog(fnName, `=== Alert was acked....`);
// ------------------
// Alert acknowledged
// ------------------
// Update status
actionCode = 1;
message = "Acknowledged by user: " + event.user.username;
}
else if (event.eventType === 201 && event.alert.statusCode === 4) {
Logger.writeLog(fnName, `=== Alert was resolved...`);
// -------------------
// Alert was resolved
// -------------------
// Updata Zabbix status
actionCode = 2;
message = "Closed by user: " + event.user.username;
}
else if (event.eventType === 203) {
Logger.writeLog(fnName, `=== Alert was annotated...`);
// -------------------
// Alert annotated
// -------------------
// Updata status
actionCode = 3;
//message = event.user.username + ": " + event.annotation.message;
}
if (actionCode > 0) {
// Forward status update
//
//sendStatusUpdate(eventId, actionCode, event.user.username, message);
var currentConfigWebook=getWebhookConfig(actionCode);
if(currentConfigWebook)
sendBhome(bhom_event_id, {event: event, config: currentConfigWebook});
}
return true;
}
catch (error) {
Logger.writeLog(fnName, `Error on handling subscription event: ${error.message}`);
return false;
}
finally {
Logger.writeLog(fnName, 'onSubscriptionEvent end');
}
}
async function onDispose() {
let fnName = onDispose.name;
try {
Logger.writeLog(fnName, 'onDispose start');
Logger.writeLog(fnName, 'onDispose end');
}
catch (error) {
Logger.writeLog(fnName, `Unexpected error on dispose: ${error.message}`);
}
}
function sendBhome(eventId, context) {
//console.log("callback config access: " + currentConfigWebook);
authenticate(callbackSendRequest);
function callbackSendRequest(authHeaderValue) {
let replacedBodyString = Mustache.render(JSON.stringify(context.config.body), context.event);
let data = JSON.parse(replacedBodyString);
//var url = appContext.config.targetUrl+"/events-service/api/v1.0/events/" + eventId;
var url = appContext.config.targetUrl+Mustache.render(context.config.endpoint, context.event);
console.log(url);
var options = {
//method: "PATCH",
body: data,
json: true,
url: url,
headers: {'Authorization': authHeaderValue}
};
console.log("request authheadervalue: "+ authHeaderValue);
console.log("request options: "+ JSON.stringify(options));
console.log("request body: "+ JSON.stringify(data));
//request.patch(options, (error, response, body) => {
context.config.sendF(options, (error, response, body) => {
console.log("bhome respnse: \n" + JSON.stringify(response));
if (error) {
console.log("Error: "+ error);
return false;
}
if(response.statusCode == 200)
sendNote(authHeaderValue, context, eventId);
});
}
}
function sendNote(authHeaderValue, context, eventId) {
if(typeof context.config.sendNF == undefined)
return;
if(!context.config.note)
return;
console.log("SEND Note: "+JSON.stringify(context));
let replacedNote= Mustache.render(context.config.note, context.event);
console.log("Replaced Note: "+ replacedNote);
var url = appContext.config.targetUrl+"/events-service/api/v1.0/events/operations/addNote";
let data = {
"eventIds": [eventId],
"slots": {
"notes": replacedNote
}
}
var options = {
method: 'POST',
body: data,
json: true,
url: url,
headers: {'Authorization': authHeaderValue}
};
context.config.sendNF(options, (error, response, body) => {
console.log("Add note bhome respnse: \n" + JSON.stringify(response));
if (error) {
console.log("Error: "+error)
return false;
}
if(response.statusCode == 200)
console.log("note successfully added!");
});
}
function authenticate(callbackSendRequest) {
let fnName = sendBhome.name;
Logger.writeLog(fnName, 'Sending to bhom');
if (appContext.config.authmod == "Bearer") {
var auth_data = {
"access_key": appContext.config.access_key,
"access_secret_key": appContext.config.access_secret_key
}
var auth_options = {
method: 'POST',
body: auth_data,
json: true,
url: "https://nttkub1-private-dev.at.nttdata-emea.com/ims/api/v1/access_keys/login",
};
request(auth_options, (error, response, body) => {
console.log("Authentication Request status code: " + response.statusCode);
if (error) {
console.log("Error during authentication: " + error);
}
// Printing body
if (response.statusCode == "200") {
let auth="Bearer " + body.json_web_token;
callbackSendRequest(auth);
}
});
}
else if (appContext.config.authmod == "apiKey") {
let auth="apiKey " + appContext.config.apiKey;
callbackSendRequest(auth);
}
}

46
webpack.config.js Normal file
View File

@ -0,0 +1,46 @@
const path = require('path');
const webpack = require('webpack');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
var nodeExternals = require('webpack-node-externals');
module.exports = {
target: 'node',
//externals: [nodeExternals()],
externals: {
request: 'request'
},
externalsPresets: {
node: true // in order to ignore built-in modules like path, fs, etc.
},
mode: 'development',
entry: {
main: __dirname + '/src/index.js',
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'Main.js',
library: "derdakbhom",
libraryTarget: "umd"
},
// module: {
// rules: [
// {
// test: /\.js$/,
// exclude: /node_modules/,
// loader: 'babel-loader'
// },
// {
// test: /\.jsx$/,
// exclude: /node_modules/,
// loader: 'babel-loader'
// }
// ]
// },
// plugins: [
// new BundleAnalyzerPlugin(),
// new webpack.IgnorePlugin({
// resourceRegExp: /^\.\/locale$/,
// contextRegExp: /request/,
// }),
// ]
};