Last active
September 29, 2021 20:56
-
-
Save pmuellr/17a2b9c88a1e0bdf39fbafc7c1e8130a to your computer and use it in GitHub Desktop.
A cli filter which reads a Kibana export file from stdin, and writes a Kibana export file to stdout. The export is changed to remove all alerting connectors, add a new server log connector, and change all referenced actions in alerting rules to the server log. This renders an export of Kibana alerting rules to the same rules which do not perform…
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env node | |
'use strict' | |
/* | |
Filter for Elastic Kibana .ndjson files with alerting rule exports, | |
that converts all the rule's actions to a new server log connector. | |
node export-to-server-log.js < original-export.ndjson > modified-export.ndjson | |
*/ | |
const path = require('path') | |
const DEBUG = !!process.env.DEBUG | |
const argv = process.argv.slice(2) | |
if (argv.length !== 0) help() | |
const fs = require("fs") | |
const contents = fs.readFileSync(0, { encoding: 'utf8' }).toString('utf8') | |
debugLog('read stdin bytes: ', contents.length) | |
const lines = contents.trim().split(/\n/g) | |
debugLog('read stdin lines: ', lines.length) | |
const imports = lines.map(line => toJSON(line)) | |
const summary = imports.pop() | |
const { excludedObjectsCount, exportedCount, missingRefCount } = summary | |
ensureSummaryOK(excludedObjectsCount, exportedCount, missingRefCount) | |
debugLog(`summary: `, summary) | |
imports.forEach((object) => ensureImportObject(object)) | |
const baseImports = imports.filter(({ type }) => type !== 'action') | |
const date = new Date().toISOString() | |
const serverLogId = 'a3676547-ab0a-4710-830a-f93b09006b66' | |
const version = '7.14.0' | |
const serverLogConnector = { | |
attributes: { | |
actionTypeId: '.server-log', | |
isMissingSecrets: false, | |
name: `server log created for export-to-server-log on ${date}` | |
}, | |
coreMigrationVersion: version, | |
id: serverLogId, | |
migrationVersion: { | |
action: version | |
}, | |
references: [], | |
type: 'action', | |
updated_at: date, | |
version: 'WzQyLDQyXQo=' // echo '[42,42]' | base64 | |
} | |
const serverLogAction = { | |
actionRef: 'action_0', | |
actionTypeId: '.server-log', | |
group: 'fill-in-the-blank', | |
params: { | |
message: 'fill-in-the-blank', | |
} | |
} | |
const finalSummary = { | |
excludedObjects: [], | |
excludedObjectsCount: 0, | |
exportedCount: baseImports.length + 1, | |
missingRefCount:0, | |
missingReferences:[] | |
} | |
const fixedImports = baseImports.map(object => fixImport(object)) | |
console.log(JSON.stringify(serverLogConnector)) | |
fixedImports.forEach(object => console.log(JSON.stringify(object))) | |
console.log(JSON.stringify(finalSummary)) | |
/** @type { (object: any) => void */ | |
function fixImport(object) { | |
const { type } = object | |
if (type !== 'alert') return object | |
const { id, attributes, references } = object | |
const { name, actions } = attributes | |
if (actions == null || actions.length === 0) return object | |
object.attributes.actions = actions.map(action => { | |
const newAction = JSON.parse(JSON.stringify(serverLogAction)) | |
newAction.group = action.group | |
newAction.params.message = `export-to-server-log replacement of action ${action.actionTypeId} ${action.group} for rule "${name}" (${id}) in space "{{rule.spaceId}}"` | |
return newAction | |
}) | |
object.references = [{ | |
id: serverLogId, | |
name: 'action_0', | |
type: 'action' | |
}] | |
return object | |
} | |
/** @type { (object: any) => void */ | |
function ensureImportObject(object) { | |
const { attributes, id, type } = object | |
if (typeof attributes !== 'object') logAndExit('attributes expected to be an object in ', object) | |
if (typeof id !== 'string') logAndExit('id expected to be a string in ', object) | |
if (typeof type !== 'string') logAndExit('type expected to be a string in ', object) | |
} | |
/** @type { (excludedObjectsCount: number, exportedCount: number, missingRefCount: number) => void */ | |
function ensureSummaryOK(excludedObjectsCount, exportedCount, missingRefCount) { | |
const prefix = 'error in final summary line:' | |
ensureZero(`${prefix}: excludedObjectsCount`, excludedObjectsCount) | |
ensureZero(`${prefix}: missingRefCount`, missingRefCount) | |
ensureNotZero(`${prefix}: exportedCount`, exportedCount) | |
} | |
/** @type { (message: string, value: any) => void */ | |
function ensureZero(message, value) { | |
if (typeof value !== 'number') { | |
logAndExit(message, ' is not a number:', value) | |
} | |
if (value !== 0) { | |
logAndExit(message, ' is not zero:', value) | |
} | |
} | |
/** @type { (message: string, value: any) => void */ | |
function ensureNotZero(message, value) { | |
if (typeof value !== 'number') { | |
logAndExit(message, ' is not a number:', value) | |
} | |
if (value == 0) { | |
logAndExit(message, ' is zero:', value) | |
} | |
} | |
/** @type { (string: string) => any */ | |
function toJSON(string) { | |
try { | |
return JSON.parse(string) | |
} catch (err) { | |
logAndExit(`error parsing JSON line: |${string}|`) | |
} | |
} | |
/** | |
* @param {...any} parts | |
*/ | |
function debugLog(...parts) { | |
if (!DEBUG) return | |
console.error(toMessage(parts)) | |
} | |
/** @type { () => void */ | |
function help() { | |
const program = path.basename(process.argv[1]) | |
logAndExit(`${program}: | |
A cli filter which reads a Kibana export file from stdin, and writes a Kibana | |
export file to stdout. The export is changed to remove all alerting connectors, | |
add a new server log connector, and change all referenced actions in alerting | |
rules to the server log. This renders an export of Kibana alerting rules to | |
the same rules which do not perform their usual actions, but log to the server | |
instead. You know, for testing. | |
`.trim()) | |
} | |
/** | |
* @param {...any} parts | |
*/ | |
function logAndExit(...parts) { | |
console.error(toMessage(parts)) | |
process.exit(1) | |
} | |
/** @type { (parts: any[]) => string */ | |
function toMessage(parts) { | |
const stringParts = parts.map(part => { | |
if (typeof part === 'string') { | |
return part | |
} else { | |
try { | |
return JSON.stringify(part) | |
} catch (err) { | |
return `${part}` | |
} | |
} | |
}) | |
return stringParts.join('') | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment