mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[ResponseOps] adds mustache lambdas and array.asJSON (#150572)
partially resolves some issues in https://github.com/elastic/kibana/issues/84217 Adds Mustache lambdas for alerting actions to format dates with `{{#FormatDate}}`, evaluate math expressions with `{{#EvalMath}}`, and provide easier JSON formatting with `{{#ParseHjson}}` and a new `asJSON` property added to arrays.
This commit is contained in:
parent
953437f05d
commit
4382e1cf32
9 changed files with 545 additions and 243 deletions
201
x-pack/plugins/actions/server/lib/mustache_lambdas.test.ts
Normal file
201
x-pack/plugins/actions/server/lib/mustache_lambdas.test.ts
Normal file
|
@ -0,0 +1,201 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import dedent from 'dedent';
|
||||
|
||||
import { renderMustacheString } from './mustache_renderer';
|
||||
|
||||
describe('mustache lambdas', () => {
|
||||
describe('FormatDate', () => {
|
||||
it('date with defaults is successful', () => {
|
||||
const timeStamp = '2022-11-29T15:52:44Z';
|
||||
const template = dedent`
|
||||
{{#FormatDate}} {{timeStamp}} {{/FormatDate}}
|
||||
`.trim();
|
||||
|
||||
expect(renderMustacheString(template, { timeStamp }, 'none')).toEqual('2022-11-29 03:52pm');
|
||||
});
|
||||
|
||||
it('date with a time zone is successful', () => {
|
||||
const timeStamp = '2022-11-29T15:52:44Z';
|
||||
const template = dedent`
|
||||
{{#FormatDate}} {{timeStamp}} ; America/New_York {{/FormatDate}}
|
||||
`.trim();
|
||||
|
||||
expect(renderMustacheString(template, { timeStamp }, 'none')).toEqual('2022-11-29 10:52am');
|
||||
});
|
||||
|
||||
it('date with a format is successful', () => {
|
||||
const timeStamp = '2022-11-29T15:52:44Z';
|
||||
const template = dedent`
|
||||
{{#FormatDate}} {{timeStamp}} ;; dddd MMM Do YYYY HH:mm:ss.SSS {{/FormatDate}}
|
||||
`.trim();
|
||||
|
||||
expect(renderMustacheString(template, { timeStamp }, 'none')).toEqual(
|
||||
'Tuesday Nov 29th 2022 15:52:44.000'
|
||||
);
|
||||
});
|
||||
|
||||
it('date with a format and timezone is successful', () => {
|
||||
const timeStamp = '2022-11-29T15:52:44Z';
|
||||
const template = dedent`
|
||||
{{#FormatDate}} {{timeStamp}};America/New_York;dddd MMM Do YYYY HH:mm:ss.SSS {{/FormatDate}}
|
||||
`.trim();
|
||||
|
||||
expect(renderMustacheString(template, { timeStamp }, 'none').trim()).toEqual(
|
||||
'Tuesday Nov 29th 2022 10:52:44.000'
|
||||
);
|
||||
});
|
||||
|
||||
it('empty date produces error', () => {
|
||||
const timeStamp = '';
|
||||
const template = dedent`
|
||||
{{#FormatDate}} {{/FormatDate}}
|
||||
`.trim();
|
||||
|
||||
expect(renderMustacheString(template, { timeStamp }, 'none').trim()).toEqual(
|
||||
'error rendering mustache template "{{#FormatDate}} {{/FormatDate}}": date is empty'
|
||||
);
|
||||
});
|
||||
|
||||
it('invalid date produces error', () => {
|
||||
const timeStamp = 'this is not a d4t3';
|
||||
const template = dedent`
|
||||
{{#FormatDate}}{{timeStamp}}{{/FormatDate}}
|
||||
`.trim();
|
||||
|
||||
expect(renderMustacheString(template, { timeStamp }, 'none').trim()).toEqual(
|
||||
'error rendering mustache template "{{#FormatDate}}{{timeStamp}}{{/FormatDate}}": invalid date "this is not a d4t3"'
|
||||
);
|
||||
});
|
||||
|
||||
it('invalid timezone produces error', () => {
|
||||
const timeStamp = '2023-04-10T23:52:39';
|
||||
const template = dedent`
|
||||
{{#FormatDate}}{{timeStamp}};NotATime Zone!{{/FormatDate}}
|
||||
`.trim();
|
||||
|
||||
expect(renderMustacheString(template, { timeStamp }, 'none').trim()).toEqual(
|
||||
'error rendering mustache template "{{#FormatDate}}{{timeStamp}};NotATime Zone!{{/FormatDate}}": unknown timeZone value "NotATime Zone!"'
|
||||
);
|
||||
});
|
||||
|
||||
it('invalid format produces error', () => {
|
||||
const timeStamp = '2023-04-10T23:52:39';
|
||||
const template = dedent`
|
||||
{{#FormatDate}}{{timeStamp}};;garbage{{/FormatDate}}
|
||||
`.trim();
|
||||
|
||||
// not clear how to force an error, it pretty much does something with
|
||||
// ANY string
|
||||
expect(renderMustacheString(template, { timeStamp }, 'none').trim()).toEqual(
|
||||
'gamrbamg2' // a => am/pm (so am here); e => day of week
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('EvalMath', () => {
|
||||
it('math is successful', () => {
|
||||
const vars = {
|
||||
context: {
|
||||
a: { b: 1 },
|
||||
c: { d: 2 },
|
||||
},
|
||||
};
|
||||
const template = dedent`
|
||||
{{#EvalMath}} 1 + 0 {{/EvalMath}}
|
||||
{{#EvalMath}} 1 + context.a.b {{/EvalMath}}
|
||||
{{#context}}
|
||||
{{#EvalMath}} 1 + c.d {{/EvalMath}}
|
||||
{{/context}}
|
||||
`.trim();
|
||||
|
||||
const result = renderMustacheString(template, vars, 'none');
|
||||
expect(result).toEqual(`1\n2\n3\n`);
|
||||
});
|
||||
|
||||
it('invalid expression produces error', () => {
|
||||
const vars = {
|
||||
context: {
|
||||
a: { b: 1 },
|
||||
c: { d: 2 },
|
||||
},
|
||||
};
|
||||
const template = dedent`
|
||||
{{#EvalMath}} ) 1 ++++ 0 ( {{/EvalMath}}
|
||||
`.trim();
|
||||
|
||||
const result = renderMustacheString(template, vars, 'none');
|
||||
expect(result).toEqual(
|
||||
`error rendering mustache template "{{#EvalMath}} ) 1 ++++ 0 ( {{/EvalMath}}": error evaluating tinymath expression ") 1 ++++ 0 (": Failed to parse expression. Expected "(", function, literal, or whitespace but ")" found.`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ParseHJson', () => {
|
||||
it('valid Hjson is successful', () => {
|
||||
const vars = {
|
||||
context: {
|
||||
a: { b: 1 },
|
||||
c: { d: 2 },
|
||||
},
|
||||
};
|
||||
const hjson = `
|
||||
{
|
||||
# specify rate in requests/second (because comments are helpful!)
|
||||
rate: 1000
|
||||
|
||||
a: {{context.a}}
|
||||
a_b: {{context.a.b}}
|
||||
c: {{context.c}}
|
||||
c_d: {{context.c.d}}
|
||||
|
||||
# list items can be separated by lines, or commas, and trailing
|
||||
# commas permitted
|
||||
list: [
|
||||
1 2
|
||||
3
|
||||
4,5,6,
|
||||
]
|
||||
}`;
|
||||
const template = dedent`
|
||||
{{#ParseHjson}} ${hjson} {{/ParseHjson}}
|
||||
`.trim();
|
||||
|
||||
const result = renderMustacheString(template, vars, 'none');
|
||||
expect(JSON.parse(result)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"a": Object {
|
||||
"b": 1,
|
||||
},
|
||||
"a_b": 1,
|
||||
"c": Object {
|
||||
"d": 2,
|
||||
},
|
||||
"c_d": 2,
|
||||
"list": Array [
|
||||
"1 2",
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
],
|
||||
"rate": 1000,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('renders an error message on parse errors', () => {
|
||||
const template = dedent`
|
||||
{{#ParseHjson}} [1,2,3,,] {{/ParseHjson}}
|
||||
`.trim();
|
||||
|
||||
const result = renderMustacheString(template, {}, 'none');
|
||||
expect(result).toMatch(/^error rendering mustache template .*/);
|
||||
});
|
||||
});
|
||||
});
|
114
x-pack/plugins/actions/server/lib/mustache_lambdas.ts
Normal file
114
x-pack/plugins/actions/server/lib/mustache_lambdas.ts
Normal file
|
@ -0,0 +1,114 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import * as tinymath from '@kbn/tinymath';
|
||||
import { parse as hjsonParse } from 'hjson';
|
||||
|
||||
import moment from 'moment-timezone';
|
||||
|
||||
type Variables = Record<string, unknown>;
|
||||
|
||||
const DefaultDateTimeZone = 'UTC';
|
||||
const DefaultDateFormat = 'YYYY-MM-DD hh:mma';
|
||||
|
||||
export function getMustacheLambdas(): Variables {
|
||||
return getLambdas();
|
||||
}
|
||||
|
||||
const TimeZoneSet = new Set(moment.tz.names());
|
||||
|
||||
type RenderFn = (text: string) => string;
|
||||
|
||||
function getLambdas() {
|
||||
return {
|
||||
EvalMath: () =>
|
||||
// mustache invokes lamdas with `this` set to the current "view" (variables)
|
||||
function (this: Variables, text: string, render: RenderFn) {
|
||||
return evalMath(this, render(text.trim()));
|
||||
},
|
||||
ParseHjson: () =>
|
||||
function (text: string, render: RenderFn) {
|
||||
return parseHjson(render(text.trim()));
|
||||
},
|
||||
FormatDate: () =>
|
||||
function (text: string, render: RenderFn) {
|
||||
const dateString = render(text.trim()).trim();
|
||||
return formatDate(dateString);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function evalMath(vars: Variables, o: unknown): string {
|
||||
const expr = `${o}`;
|
||||
try {
|
||||
const result = tinymath.evaluate(expr, vars);
|
||||
return `${result}`;
|
||||
} catch (err) {
|
||||
throw new Error(`error evaluating tinymath expression "${expr}": ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function parseHjson(o: unknown): string {
|
||||
const hjsonObject = `${o}`;
|
||||
let object: unknown;
|
||||
|
||||
try {
|
||||
object = hjsonParse(hjsonObject);
|
||||
} catch (err) {
|
||||
throw new Error(`error parsing Hjson "${hjsonObject}": ${err.message}`);
|
||||
}
|
||||
|
||||
return JSON.stringify(object);
|
||||
}
|
||||
|
||||
function formatDate(dateString: unknown): string {
|
||||
const { date, timeZone, format } = splitDateString(`${dateString}`);
|
||||
|
||||
if (date === '') {
|
||||
throw new Error(`date is empty`);
|
||||
}
|
||||
|
||||
if (isNaN(new Date(date).valueOf())) {
|
||||
throw new Error(`invalid date "${date}"`);
|
||||
}
|
||||
|
||||
let mDate: moment.Moment;
|
||||
try {
|
||||
mDate = moment(date);
|
||||
if (!mDate.isValid()) {
|
||||
throw new Error(`date is invalid`);
|
||||
}
|
||||
} catch (err) {
|
||||
throw new Error(`error evaluating moment date "${date}": ${err.message}`);
|
||||
}
|
||||
|
||||
if (!TimeZoneSet.has(timeZone)) {
|
||||
throw new Error(`unknown timeZone value "${timeZone}"`);
|
||||
}
|
||||
|
||||
try {
|
||||
mDate.tz(timeZone);
|
||||
} catch (err) {
|
||||
throw new Error(`error evaluating moment timeZone "${timeZone}": ${err.message}`);
|
||||
}
|
||||
|
||||
try {
|
||||
return mDate.format(format);
|
||||
} catch (err) {
|
||||
throw new Error(`error evaluating moment format "${format}": ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function splitDateString(dateString: string) {
|
||||
const parts = dateString.split(';', 3).map((s) => s.trim());
|
||||
const [date = '', timeZone = '', format = ''] = parts;
|
||||
return {
|
||||
date,
|
||||
timeZone: timeZone || DefaultDateTimeZone,
|
||||
format: format || DefaultDateFormat,
|
||||
};
|
||||
}
|
|
@ -58,6 +58,12 @@ describe('mustache_renderer', () => {
|
|||
expect(renderMustacheString('{{f.g}}', variables, escape)).toBe('3');
|
||||
expect(renderMustacheString('{{f.h}}', variables, escape)).toBe('');
|
||||
expect(renderMustacheString('{{i}}', variables, escape)).toBe('42,43,44');
|
||||
|
||||
if (escape === 'markdown') {
|
||||
expect(renderMustacheString('{{i.asJSON}}', variables, escape)).toBe('\\[42,43,44\\]');
|
||||
} else {
|
||||
expect(renderMustacheString('{{i.asJSON}}', variables, escape)).toBe('[42,43,44]');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -339,6 +345,11 @@ describe('mustache_renderer', () => {
|
|||
|
||||
const expected = '1 - {"c":2,"d":[3,4]} -- 5,{"f":6,"g":7}';
|
||||
expect(renderMustacheString('{{a}} - {{b}} -- {{e}}', deepVariables, 'none')).toEqual(expected);
|
||||
|
||||
expect(renderMustacheString('{{e}}', deepVariables, 'none')).toEqual('5,{"f":6,"g":7}');
|
||||
expect(renderMustacheString('{{e.asJSON}}', deepVariables, 'none')).toEqual(
|
||||
'[5,{"f":6,"g":7}]'
|
||||
);
|
||||
});
|
||||
|
||||
describe('converting dot variables', () => {
|
||||
|
|
|
@ -7,8 +7,10 @@
|
|||
|
||||
import Mustache from 'mustache';
|
||||
import { isString, isPlainObject, cloneDeepWith, merge } from 'lodash';
|
||||
import { getMustacheLambdas } from './mustache_lambdas';
|
||||
|
||||
export type Escape = 'markdown' | 'slack' | 'json' | 'none';
|
||||
|
||||
type Variables = Record<string, unknown>;
|
||||
|
||||
// return a rendered mustache template with no escape given the specified variables and escape
|
||||
|
@ -25,11 +27,13 @@ export function renderMustacheStringNoEscape(string: string, variables: Variable
|
|||
// return a rendered mustache template given the specified variables and escape
|
||||
export function renderMustacheString(string: string, variables: Variables, escape: Escape): string {
|
||||
const augmentedVariables = augmentObjectVariables(variables);
|
||||
const lambdas = getMustacheLambdas();
|
||||
|
||||
const previousMustacheEscape = Mustache.escape;
|
||||
Mustache.escape = getEscape(escape);
|
||||
|
||||
try {
|
||||
return Mustache.render(`${string}`, augmentedVariables);
|
||||
return Mustache.render(`${string}`, { ...lambdas, ...augmentedVariables });
|
||||
} catch (err) {
|
||||
// log error; the mustache code does not currently leak variables
|
||||
return `error rendering mustache template "${string}": ${err.message}`;
|
||||
|
@ -98,6 +102,9 @@ function addToStringDeep(object: unknown): void {
|
|||
|
||||
// walk arrays, but don't add a toString() as mustache already does something
|
||||
if (Array.isArray(object)) {
|
||||
// instead, add an asJSON()
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(object as any).asJSON = () => JSON.stringify(object);
|
||||
object.forEach((element) => addToStringDeep(element));
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -33,7 +33,8 @@
|
|||
"@kbn/logging-mocks",
|
||||
"@kbn/core-elasticsearch-client-server-mocks",
|
||||
"@kbn/safer-lodash-set",
|
||||
"@kbn/core-http-server-mocks"
|
||||
"@kbn/core-http-server-mocks",
|
||||
"@kbn/tinymath",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -35,7 +35,7 @@ export async function initPlugin() {
|
|||
}
|
||||
|
||||
// store a message that was posted to be remembered
|
||||
const match = text.match(/^message (.*)$/);
|
||||
const match = text.match(/^message ([\S\s]*)$/);
|
||||
if (match) {
|
||||
messages.push(match[1]);
|
||||
response.statusCode = 200;
|
||||
|
|
|
@ -79,7 +79,7 @@ function createServerCallback() {
|
|||
}
|
||||
|
||||
// store a payload that was posted to be remembered
|
||||
const match = data.match(/^payload (.*)$/);
|
||||
const match = data.match(/^payload ([\S\s]*)$/);
|
||||
if (match) {
|
||||
payloads.push(match[1]);
|
||||
response.statusCode = 200;
|
||||
|
@ -89,6 +89,8 @@ function createServerCallback() {
|
|||
|
||||
response.statusCode = 400;
|
||||
response.end(`unexpected body ${data}`);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`webhook simulator received unexpected body: ${data}`);
|
||||
return;
|
||||
});
|
||||
} else {
|
||||
|
|
|
@ -47,6 +47,7 @@ export const DeepContextVariables = {
|
|||
arrayI: [44, 45],
|
||||
nullJ: null,
|
||||
undefinedK: undefined,
|
||||
dateL: '2023-04-20T04:13:17.858Z',
|
||||
};
|
||||
|
||||
function getAlwaysFiringAlertType() {
|
||||
|
|
|
@ -14,13 +14,16 @@
|
|||
|
||||
import http from 'http';
|
||||
import getPort from 'get-port';
|
||||
import { URL, format as formatUrl } from 'url';
|
||||
import axios from 'axios';
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { getWebhookServer, getSlackServer } from '@kbn/actions-simulators-plugin/server/plugin';
|
||||
import { Spaces } from '../../../scenarios';
|
||||
import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../../common/lib';
|
||||
import {
|
||||
getUrlPrefix,
|
||||
getTestRuleData as getCoreTestRuleData,
|
||||
ObjectRemover,
|
||||
} from '../../../../common/lib';
|
||||
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
|
@ -32,8 +35,10 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon
|
|||
const objectRemover = new ObjectRemover(supertest);
|
||||
let webhookSimulatorURL: string = '';
|
||||
let webhookServer: http.Server;
|
||||
let webhookConnector: any;
|
||||
let slackSimulatorURL: string = '';
|
||||
let slackServer: http.Server;
|
||||
let slackConnector: any;
|
||||
|
||||
before(async () => {
|
||||
let availablePort: number;
|
||||
|
@ -42,6 +47,7 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon
|
|||
availablePort = await getPort({ port: 9000 });
|
||||
webhookServer.listen(availablePort);
|
||||
webhookSimulatorURL = `http://localhost:${availablePort}`;
|
||||
webhookConnector = await createWebhookConnector(webhookSimulatorURL);
|
||||
|
||||
slackServer = await getSlackServer();
|
||||
availablePort = await getPort({ port: getPort.makeRange(9000, 9100) });
|
||||
|
@ -49,6 +55,7 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon
|
|||
slackServer.listen(availablePort);
|
||||
}
|
||||
slackSimulatorURL = `http://localhost:${availablePort}`;
|
||||
slackConnector = await createSlackConnector(slackSimulatorURL);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
|
@ -57,13 +64,177 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon
|
|||
slackServer.close();
|
||||
});
|
||||
|
||||
it('should handle escapes in webhook', async () => {
|
||||
const url = formatUrl(new URL(webhookSimulatorURL), { auth: false });
|
||||
const actionResponse = await supertest
|
||||
describe('escaping', () => {
|
||||
it('should handle escapes in webhook', async () => {
|
||||
// from x-pack/test/alerting_api_integration/common/plugins/alerts/server/alert_types.ts,
|
||||
// const EscapableStrings
|
||||
const template = '{{context.escapableDoubleQuote}} -- {{context.escapableLineFeed}}';
|
||||
const rule = await createRule({
|
||||
id: webhookConnector.id,
|
||||
group: 'default',
|
||||
params: {
|
||||
body: `payload {{alertId}} - ${template}`,
|
||||
},
|
||||
});
|
||||
const body = await retry.try(async () => waitForActionBody(webhookSimulatorURL, rule.id));
|
||||
expect(body).to.be(`\\"double quote\\" -- line\\nfeed`);
|
||||
});
|
||||
|
||||
it('should handle escapes in slack', async () => {
|
||||
// from x-pack/test/alerting_api_integration/common/plugins/alerts/server/alert_types.ts,
|
||||
// const EscapableStrings
|
||||
const template =
|
||||
'{{context.escapableBacktic}} -- {{context.escapableBold}} -- {{context.escapableBackticBold}} -- {{context.escapableHtml}}';
|
||||
|
||||
const rule = await createRule({
|
||||
id: slackConnector.id,
|
||||
group: 'default',
|
||||
params: {
|
||||
message: `message {{alertId}} - ${template}`,
|
||||
},
|
||||
});
|
||||
|
||||
const body = await retry.try(async () => waitForActionBody(slackSimulatorURL, rule.id));
|
||||
expect(body).to.be("back'tic -- `*bold*` -- `'*bold*'` -- <&>");
|
||||
});
|
||||
|
||||
it('should handle context variable object expansion', async () => {
|
||||
// from x-pack/test/alerting_api_integration/common/plugins/alerts/server/alert_types.ts,
|
||||
// const DeepContextVariables
|
||||
const template = '{{context.deep}}';
|
||||
const rule = await createRule({
|
||||
id: slackConnector.id,
|
||||
group: 'default',
|
||||
params: {
|
||||
message: `message {{alertId}} - ${template}`,
|
||||
},
|
||||
});
|
||||
const body = await retry.try(async () => waitForActionBody(slackSimulatorURL, rule.id));
|
||||
expect(body).to.be(
|
||||
'{"objectA":{"stringB":"B","arrayC":[{"stringD":"D1","numberE":42},{"stringD":"D2","numberE":43}],"objectF":{"stringG":"G","nullG":null}},"stringH":"H","arrayI":[44,45],"nullJ":null,"dateL":"2023-04-20T04:13:17.858Z"}'
|
||||
);
|
||||
});
|
||||
|
||||
it('should render kibanaBaseUrl as empty string since not configured', async () => {
|
||||
const template = 'kibanaBaseUrl: "{{kibanaBaseUrl}}"';
|
||||
const rule = await createRule({
|
||||
id: slackConnector.id,
|
||||
group: 'default',
|
||||
params: {
|
||||
message: `message {{alertId}} - ${template}`,
|
||||
},
|
||||
});
|
||||
|
||||
const body = await retry.try(async () => waitForActionBody(slackSimulatorURL, rule.id));
|
||||
expect(body).to.be('kibanaBaseUrl: ""');
|
||||
});
|
||||
|
||||
it('should render action variables in rule action', async () => {
|
||||
const rule = await createRule({
|
||||
id: webhookConnector.id,
|
||||
group: 'default',
|
||||
params: {
|
||||
body: `payload {{rule.id}} - old id variable: {{alertId}}, new id variable: {{rule.id}}, old name variable: {{alertName}}, new name variable: {{rule.name}}`,
|
||||
},
|
||||
});
|
||||
|
||||
const body = await retry.try(async () => waitForActionBody(webhookSimulatorURL, rule.id));
|
||||
expect(body).to.be(
|
||||
`old id variable: ${rule.id}, new id variable: ${rule.id}, old name variable: ${rule.name}, new name variable: ${rule.name}`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('lambdas', () => {
|
||||
it('should handle ParseHjson', async () => {
|
||||
const template = `{{#ParseHjson}} {
|
||||
ruleId: {{rule.id}}
|
||||
ruleName: {{rule.name}}
|
||||
} {{/ParseHjson}}`;
|
||||
const rule = await createRule({
|
||||
id: webhookConnector.id,
|
||||
group: 'default',
|
||||
params: {
|
||||
body: `payload {{alertId}} - ${template}`,
|
||||
},
|
||||
});
|
||||
const body = await retry.try(async () => waitForActionBody(webhookSimulatorURL, rule.id));
|
||||
expect(body).to.be(`{"ruleId":"${rule.id}","ruleName":"testing mustache templates"}`);
|
||||
});
|
||||
|
||||
it('should handle asJSON', async () => {
|
||||
// from x-pack/test/alerting_api_integration/common/plugins/alerts/server/alert_types.ts,
|
||||
// const DeepContextVariables
|
||||
const template = `{{#context.deep.objectA}}
|
||||
{{{arrayC}}} {{{arrayC.asJSON}}}
|
||||
{{/context.deep.objectA}}
|
||||
`;
|
||||
const rule = await createRule({
|
||||
id: webhookConnector.id,
|
||||
group: 'default',
|
||||
params: {
|
||||
body: `payload {{alertId}} - ${template}`,
|
||||
},
|
||||
});
|
||||
const body = await retry.try(async () => waitForActionBody(webhookSimulatorURL, rule.id));
|
||||
const expected1 = `{"stringD":"D1","numberE":42},{"stringD":"D2","numberE":43}`;
|
||||
const expected2 = `[{"stringD":"D1","numberE":42},{"stringD":"D2","numberE":43}]`;
|
||||
expect(body.trim()).to.be(`${expected1} ${expected2}`);
|
||||
});
|
||||
|
||||
it('should handle EvalMath', async () => {
|
||||
// from x-pack/test/alerting_api_integration/common/plugins/alerts/server/alert_types.ts,
|
||||
// const DeepContextVariables
|
||||
const template = `{{#context.deep}}avg({{arrayI.0}}, {{arrayI.1}})/100 => {{#EvalMath}}
|
||||
round((arrayI[0] + arrayI[1]) / 2 / 100, 2)
|
||||
{{/EvalMath}}{{/context.deep}}`;
|
||||
const rule = await createRule({
|
||||
id: slackConnector.id,
|
||||
group: 'default',
|
||||
params: {
|
||||
message: `message {{alertId}} - ${template}`,
|
||||
},
|
||||
});
|
||||
const body = await retry.try(async () => waitForActionBody(slackSimulatorURL, rule.id));
|
||||
expect(body).to.be(`avg(44, 45)/100 => 0.45`);
|
||||
});
|
||||
|
||||
it('should handle FormatDate', async () => {
|
||||
// from x-pack/test/alerting_api_integration/common/plugins/alerts/server/alert_types.ts,
|
||||
// const DeepContextVariables
|
||||
const template = `{{#context.deep}}{{#FormatDate}}
|
||||
{{{dateL}}} ; America/New_York; dddd MMM Do YYYY HH:mm:ss
|
||||
{{/FormatDate}}{{/context.deep}}`;
|
||||
const rule = await createRule({
|
||||
id: slackConnector.id,
|
||||
group: 'default',
|
||||
params: {
|
||||
message: `message {{alertId}} - ${template}`,
|
||||
},
|
||||
});
|
||||
const body = await retry.try(async () => waitForActionBody(slackSimulatorURL, rule.id));
|
||||
expect(body.trim()).to.be(`Thursday Apr 20th 2023 00:13:17`);
|
||||
});
|
||||
});
|
||||
|
||||
async function createRule(action: any) {
|
||||
const ruleResponse = await supertest
|
||||
.post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(getTestRuleData({ actions: [action] }));
|
||||
expect(ruleResponse.status).to.eql(200);
|
||||
const rule = ruleResponse.body;
|
||||
objectRemover.add(Spaces.space1.id, rule.id, 'rule', 'alerting');
|
||||
|
||||
return rule;
|
||||
}
|
||||
|
||||
async function createWebhookConnector(url: string) {
|
||||
const createResponse = await supertest
|
||||
.post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`)
|
||||
.set('kbn-xsrf', 'test')
|
||||
.send({
|
||||
name: 'testing mustache escapes for webhook',
|
||||
name: 'testing mustache for webhook',
|
||||
connector_type_id: '.webhook',
|
||||
secrets: {},
|
||||
config: {
|
||||
|
@ -73,248 +244,30 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon
|
|||
url,
|
||||
},
|
||||
});
|
||||
expect(actionResponse.status).to.eql(200);
|
||||
const createdAction = actionResponse.body;
|
||||
objectRemover.add(Spaces.space1.id, createdAction.id, 'connector', 'actions');
|
||||
expect(createResponse.status).to.eql(200);
|
||||
const connector = createResponse.body;
|
||||
objectRemover.add(Spaces.space1.id, connector.id, 'connector', 'actions');
|
||||
|
||||
// from x-pack/test/alerting_api_integration/common/plugins/alerts/server/alert_types.ts,
|
||||
// const EscapableStrings
|
||||
const varsTemplate = '{{context.escapableDoubleQuote}} -- {{context.escapableLineFeed}}';
|
||||
return connector;
|
||||
}
|
||||
|
||||
const alertResponse = await supertest
|
||||
.post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(
|
||||
getTestRuleData({
|
||||
name: 'testing variable escapes for webhook',
|
||||
rule_type_id: 'test.patternFiring',
|
||||
params: {
|
||||
pattern: { instance: [true] },
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
id: createdAction.id,
|
||||
group: 'default',
|
||||
params: {
|
||||
body: `payload {{alertId}} - ${varsTemplate}`,
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
expect(alertResponse.status).to.eql(200);
|
||||
const createdAlert = alertResponse.body;
|
||||
objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting');
|
||||
|
||||
const body = await retry.try(async () =>
|
||||
waitForActionBody(webhookSimulatorURL, createdAlert.id)
|
||||
);
|
||||
expect(body).to.be(`\\"double quote\\" -- line\\nfeed`);
|
||||
});
|
||||
|
||||
it('should handle escapes in slack', async () => {
|
||||
const actionResponse = await supertest
|
||||
async function createSlackConnector(url: string) {
|
||||
const createResponse = await supertest
|
||||
.post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`)
|
||||
.set('kbn-xsrf', 'test')
|
||||
.send({
|
||||
name: "testing backtic'd mustache escapes for slack",
|
||||
name: 'testing mustache for slack',
|
||||
connector_type_id: '.slack',
|
||||
secrets: {
|
||||
webhookUrl: slackSimulatorURL,
|
||||
webhookUrl: url,
|
||||
},
|
||||
});
|
||||
expect(actionResponse.status).to.eql(200);
|
||||
const createdAction = actionResponse.body;
|
||||
objectRemover.add(Spaces.space1.id, createdAction.id, 'connector', 'actions');
|
||||
expect(createResponse.status).to.eql(200);
|
||||
const connector = createResponse.body;
|
||||
objectRemover.add(Spaces.space1.id, connector.id, 'connector', 'actions');
|
||||
|
||||
// from x-pack/test/alerting_api_integration/common/plugins/alerts/server/alert_types.ts,
|
||||
// const EscapableStrings
|
||||
const varsTemplate =
|
||||
'{{context.escapableBacktic}} -- {{context.escapableBold}} -- {{context.escapableBackticBold}} -- {{context.escapableHtml}}';
|
||||
|
||||
const alertResponse = await supertest
|
||||
.post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(
|
||||
getTestRuleData({
|
||||
name: 'testing variable escapes for slack',
|
||||
rule_type_id: 'test.patternFiring',
|
||||
params: {
|
||||
pattern: { instance: [true] },
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
id: createdAction.id,
|
||||
group: 'default',
|
||||
params: {
|
||||
message: `message {{alertId}} - ${varsTemplate}`,
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
expect(alertResponse.status).to.eql(200);
|
||||
const createdAlert = alertResponse.body;
|
||||
objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting');
|
||||
|
||||
const body = await retry.try(async () =>
|
||||
waitForActionBody(slackSimulatorURL, createdAlert.id)
|
||||
);
|
||||
expect(body).to.be("back'tic -- `*bold*` -- `'*bold*'` -- <&>");
|
||||
});
|
||||
|
||||
it('should handle context variable object expansion', async () => {
|
||||
const actionResponse = await supertest
|
||||
.post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`)
|
||||
.set('kbn-xsrf', 'test')
|
||||
.send({
|
||||
name: 'testing context variable expansion',
|
||||
connector_type_id: '.slack',
|
||||
secrets: {
|
||||
webhookUrl: slackSimulatorURL,
|
||||
},
|
||||
});
|
||||
expect(actionResponse.status).to.eql(200);
|
||||
const createdAction = actionResponse.body;
|
||||
objectRemover.add(Spaces.space1.id, createdAction.id, 'connector', 'actions');
|
||||
|
||||
// from x-pack/test/alerting_api_integration/common/plugins/alerts/server/alert_types.ts,
|
||||
// const DeepContextVariables
|
||||
const varsTemplate = '{{context.deep}}';
|
||||
|
||||
const alertResponse = await supertest
|
||||
.post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(
|
||||
getTestRuleData({
|
||||
name: 'testing context variable expansion',
|
||||
rule_type_id: 'test.patternFiring',
|
||||
params: {
|
||||
pattern: { instance: [true, true] },
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
id: createdAction.id,
|
||||
group: 'default',
|
||||
params: {
|
||||
message: `message {{alertId}} - ${varsTemplate}`,
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
expect(alertResponse.status).to.eql(200);
|
||||
const createdAlert = alertResponse.body;
|
||||
objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting');
|
||||
|
||||
const body = await retry.try(async () =>
|
||||
waitForActionBody(slackSimulatorURL, createdAlert.id)
|
||||
);
|
||||
expect(body).to.be(
|
||||
'{"objectA":{"stringB":"B","arrayC":[{"stringD":"D1","numberE":42},{"stringD":"D2","numberE":43}],"objectF":{"stringG":"G","nullG":null}},"stringH":"H","arrayI":[44,45],"nullJ":null}'
|
||||
);
|
||||
});
|
||||
|
||||
it('should render kibanaBaseUrl as empty string since not configured', async () => {
|
||||
const actionResponse = await supertest
|
||||
.post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`)
|
||||
.set('kbn-xsrf', 'test')
|
||||
.send({
|
||||
name: 'testing context variable expansion',
|
||||
connector_type_id: '.slack',
|
||||
secrets: {
|
||||
webhookUrl: slackSimulatorURL,
|
||||
},
|
||||
});
|
||||
expect(actionResponse.status).to.eql(200);
|
||||
const createdAction = actionResponse.body;
|
||||
objectRemover.add(Spaces.space1.id, createdAction.id, 'connector', 'actions');
|
||||
|
||||
const varsTemplate = 'kibanaBaseUrl: "{{kibanaBaseUrl}}"';
|
||||
|
||||
const alertResponse = await supertest
|
||||
.post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(
|
||||
getTestRuleData({
|
||||
name: 'testing context variable kibanaBaseUrl',
|
||||
rule_type_id: 'test.patternFiring',
|
||||
params: {
|
||||
pattern: { instance: [true, true] },
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
id: createdAction.id,
|
||||
group: 'default',
|
||||
params: {
|
||||
message: `message {{alertId}} - ${varsTemplate}`,
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
expect(alertResponse.status).to.eql(200);
|
||||
const createdAlert = alertResponse.body;
|
||||
objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting');
|
||||
|
||||
const body = await retry.try(async () =>
|
||||
waitForActionBody(slackSimulatorURL, createdAlert.id)
|
||||
);
|
||||
expect(body).to.be('kibanaBaseUrl: ""');
|
||||
});
|
||||
|
||||
it('should render action variables in rule action', async () => {
|
||||
const url = formatUrl(new URL(webhookSimulatorURL), { auth: false });
|
||||
const actionResponse = await supertest
|
||||
.post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`)
|
||||
.set('kbn-xsrf', 'test')
|
||||
.send({
|
||||
name: 'testing action variable rendering',
|
||||
connector_type_id: '.webhook',
|
||||
secrets: {},
|
||||
config: {
|
||||
headers: {
|
||||
'Content-Type': 'text/plain',
|
||||
},
|
||||
url,
|
||||
},
|
||||
});
|
||||
expect(actionResponse.status).to.eql(200);
|
||||
const createdAction = actionResponse.body;
|
||||
objectRemover.add(Spaces.space1.id, createdAction.id, 'connector', 'actions');
|
||||
|
||||
const alertResponse = await supertest
|
||||
.post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(
|
||||
getTestRuleData({
|
||||
name: 'testing variable escapes for webhook',
|
||||
rule_type_id: 'test.patternFiring',
|
||||
params: {
|
||||
pattern: { instance: [true] },
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
id: createdAction.id,
|
||||
group: 'default',
|
||||
params: {
|
||||
body: `payload {{rule.id}} - old id variable: {{alertId}}, new id variable: {{rule.id}}, old name variable: {{alertName}}, new name variable: {{rule.name}}`,
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
expect(alertResponse.status).to.eql(200);
|
||||
const createdAlert = alertResponse.body;
|
||||
objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting');
|
||||
|
||||
const body = await retry.try(async () =>
|
||||
waitForActionBody(webhookSimulatorURL, createdAlert.id)
|
||||
);
|
||||
expect(body).to.be(
|
||||
`old id variable: ${createdAlert.id}, new id variable: ${createdAlert.id}, old name variable: ${createdAlert.name}, new name variable: ${createdAlert.name}`
|
||||
);
|
||||
});
|
||||
return connector;
|
||||
}
|
||||
});
|
||||
|
||||
async function waitForActionBody(url: string, id: string): Promise<string> {
|
||||
|
@ -322,7 +275,7 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon
|
|||
expect(response.status).to.eql(200);
|
||||
|
||||
for (const datum of response.data) {
|
||||
const match = datum.match(/^(.*) - (.*)$/);
|
||||
const match = datum.match(/^(.*) - ([\S\s]*)$/);
|
||||
if (match == null) continue;
|
||||
|
||||
if (match[1] === id) return match[2];
|
||||
|
@ -331,3 +284,15 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon
|
|||
throw new Error(`no action body posted yet for id ${id}`);
|
||||
}
|
||||
}
|
||||
|
||||
function getTestRuleData(overrides: any) {
|
||||
const defaults = {
|
||||
name: 'testing mustache templates',
|
||||
rule_type_id: 'test.patternFiring',
|
||||
params: {
|
||||
pattern: { instance: [true] },
|
||||
},
|
||||
};
|
||||
|
||||
return getCoreTestRuleData({ ...overrides, ...defaults });
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue