[Response Ops][Actions] Logging errors from the mustache lambda format functions (#176014)

Resolves https://github.com/elastic/kibana/issues/173972

## Summary

Changing to log and return error string instead of throwing error inside
the mustache format lambda functions. When an error occurs, the lambda
will return the error message. This allows the rest of the action
message to be rendered, while giving the user some indication an error
occurred and logging the error in a way that is discoverable in the
logs.

## To verify

Create a rule and add a message like:

```
{{alert.id}} - {{#FormatDate}} {{{context.nope}}}  ; America/New_York ; dddd MMM Do YYYY HH:mm:ss.SSS {{/FormatDate}}
```

When the rule runs and triggers and alert, you should see the alert ID
in the notification message along with a `date is empty` message and an
error log indicating that the date formatting was unsuccessful due to
empty date.

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Ying Mao 2024-02-05 09:29:33 -05:00 committed by GitHub
parent 450f9f62e2
commit 11593f0b75
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 404 additions and 194 deletions

View file

@ -6,10 +6,16 @@
*/
import dedent from 'dedent';
import { loggingSystemMock } from '@kbn/core/server/mocks';
import { renderMustacheString } from './mustache_renderer';
const logger = loggingSystemMock.create().get();
describe('mustache lambdas', () => {
beforeEach(() => {
jest.resetAllMocks();
});
describe('FormatDate', () => {
it('date with defaults is successful', () => {
const timeStamp = '2022-11-29T15:52:44Z';
@ -17,7 +23,9 @@ describe('mustache lambdas', () => {
{{#FormatDate}} {{timeStamp}} {{/FormatDate}}
`.trim();
expect(renderMustacheString(template, { timeStamp }, 'none')).toEqual('2022-11-29 03:52pm');
expect(renderMustacheString(logger, template, { timeStamp }, 'none')).toEqual(
'2022-11-29 03:52pm'
);
});
it('date with a time zone is successful', () => {
@ -26,7 +34,9 @@ describe('mustache lambdas', () => {
{{#FormatDate}} {{timeStamp}} ; America/New_York {{/FormatDate}}
`.trim();
expect(renderMustacheString(template, { timeStamp }, 'none')).toEqual('2022-11-29 10:52am');
expect(renderMustacheString(logger, template, { timeStamp }, 'none')).toEqual(
'2022-11-29 10:52am'
);
});
it('date with a format is successful', () => {
@ -35,7 +45,7 @@ describe('mustache lambdas', () => {
{{#FormatDate}} {{timeStamp}} ;; dddd MMM Do YYYY HH:mm:ss.SSS {{/FormatDate}}
`.trim();
expect(renderMustacheString(template, { timeStamp }, 'none')).toEqual(
expect(renderMustacheString(logger, template, { timeStamp }, 'none')).toEqual(
'Tuesday Nov 29th 2022 15:52:44.000'
);
});
@ -46,41 +56,48 @@ describe('mustache lambdas', () => {
{{#FormatDate}} {{timeStamp}};America/New_York;dddd MMM Do YYYY HH:mm:ss.SSS {{/FormatDate}}
`.trim();
expect(renderMustacheString(template, { timeStamp }, 'none').trim()).toEqual(
expect(renderMustacheString(logger, template, { timeStamp }, 'none').trim()).toEqual(
'Tuesday Nov 29th 2022 10:52:44.000'
);
});
it('empty date produces error', () => {
it('empty date logs and returns error string', () => {
const timeStamp = '';
const template = dedent`
{{#FormatDate}} {{/FormatDate}}
`.trim();
expect(renderMustacheString(template, { timeStamp }, 'none').trim()).toEqual(
'error rendering mustache template "{{#FormatDate}} {{/FormatDate}}": date is empty'
expect(renderMustacheString(logger, template, { timeStamp }, 'none').trim()).toEqual(
'date is empty'
);
expect(logger.warn).toHaveBeenCalledWith(`mustache render error: date is empty`);
});
it('invalid date produces error', () => {
it('invalid date logs and returns error string', () => {
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"'
expect(renderMustacheString(logger, template, { timeStamp }, 'none').trim()).toEqual(
'invalid date "this is not a d4t3"'
);
expect(logger.warn).toHaveBeenCalledWith(
`mustache render error: invalid date "this is not a d4t3"`
);
});
it('invalid timezone produces error', () => {
it('invalid timezone logs and returns error string', () => {
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!"'
expect(renderMustacheString(logger, template, { timeStamp }, 'none').trim()).toEqual(
'unknown timeZone value "NotATime Zone!"'
);
expect(logger.warn).toHaveBeenCalledWith(
`mustache render error: unknown timeZone value "NotATime Zone!"`
);
});
@ -92,7 +109,7 @@ describe('mustache lambdas', () => {
// not clear how to force an error, it pretty much does something with
// ANY string
expect(renderMustacheString(template, { timeStamp }, 'none').trim()).toEqual(
expect(renderMustacheString(logger, template, { timeStamp }, 'none').trim()).toEqual(
'gamrbamg2' // a => am/pm (so am here); e => day of week
);
});
@ -114,11 +131,11 @@ describe('mustache lambdas', () => {
{{/context}}
`.trim();
const result = renderMustacheString(template, vars, 'none');
const result = renderMustacheString(logger, template, vars, 'none');
expect(result).toEqual(`1\n2\n3\n`);
});
it('invalid expression produces error', () => {
it('invalid expression logs and returns error string', () => {
const vars = {
context: {
a: { b: 1 },
@ -129,9 +146,12 @@ describe('mustache lambdas', () => {
{{#EvalMath}} ) 1 ++++ 0 ( {{/EvalMath}}
`.trim();
const result = renderMustacheString(template, vars, 'none');
const result = renderMustacheString(logger, 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.`
'error evaluating tinymath expression ") 1 ++++ 0 (": Failed to parse expression. Expected "(", function, literal, or whitespace but ")" found.'
);
expect(logger.warn).toHaveBeenCalledWith(
`mustache render error: error evaluating tinymath expression ") 1 ++++ 0 (": Failed to parse expression. Expected "(", function, literal, or whitespace but ")" found.`
);
});
});
@ -147,7 +167,7 @@ describe('mustache lambdas', () => {
const hjson = `
{
# specify rate in requests/second (because comments are helpful!)
rate: 1000
rate: 1000
a: {{context.a}}
a_b: {{context.a.b}}
@ -166,7 +186,7 @@ describe('mustache lambdas', () => {
{{#ParseHjson}} ${hjson} {{/ParseHjson}}
`.trim();
const result = renderMustacheString(template, vars, 'none');
const result = renderMustacheString(logger, template, vars, 'none');
expect(JSON.parse(result)).toMatchInlineSnapshot(`
Object {
"a": Object {
@ -189,13 +209,18 @@ describe('mustache lambdas', () => {
`);
});
it('renders an error message on parse errors', () => {
it('logs an error message and returns error string on parse errors', () => {
const template = dedent`
{{#ParseHjson}} [1,2,3,,] {{/ParseHjson}}
`.trim();
const result = renderMustacheString(template, {}, 'none');
expect(result).toMatch(/^error rendering mustache template .*/);
const result = renderMustacheString(logger, template, {}, 'none');
expect(result).toEqual(
`error parsing Hjson \"[1,2,3,,]\": Found a punctuator character ',' when expecting a quoteless string (check your syntax) at line 1,7 >>>1,2,3,,] ...`
);
expect(logger.warn).toHaveBeenCalledWith(
`mustache render error: error parsing Hjson \"[1,2,3,,]\": Found a punctuator character ',' when expecting a quoteless string (check your syntax) at line 1,7 >>>1,2,3,,] ...`
);
});
});
@ -206,16 +231,19 @@ describe('mustache lambdas', () => {
{{#FormatNumber}} {{num}}; en-US; style: currency, currency: EUR {{/FormatNumber}}
`.trim();
expect(renderMustacheString(template, { num }, 'none')).toEqual('€42.00');
expect(renderMustacheString(logger, template, { num }, 'none')).toEqual('€42.00');
});
it('renders an error message on errors', () => {
it('logs an error message and returns empty string on errors', () => {
const num = 'nope;;';
const template = dedent`
{{#FormatNumber}} {{num}} {{/FormatNumber}}
`.trim();
expect(renderMustacheString(template, { num }, 'none')).toEqual(`invalid number: 'nope'`);
expect(renderMustacheString(logger, template, { num }, 'none')).toEqual(
`invalid number: 'nope'`
);
expect(logger.warn).toHaveBeenCalledWith(`mustache render error: invalid number: 'nope'`);
});
});
});

View file

@ -8,6 +8,7 @@
import * as tinymath from '@kbn/tinymath';
import { parse as hjsonParse } from 'hjson';
import moment from 'moment-timezone';
import { Logger } from '@kbn/core/server';
import { formatNumber } from './number_formatter';
@ -16,96 +17,102 @@ type Variables = Record<string, unknown>;
const DefaultDateTimeZone = 'UTC';
const DefaultDateFormat = 'YYYY-MM-DD hh:mma';
export function getMustacheLambdas(): Variables {
return getLambdas();
export function getMustacheLambdas(logger: Logger): Variables {
return getLambdas(logger);
}
const TimeZoneSet = new Set(moment.tz.names());
type RenderFn = (text: string) => string;
function getLambdas() {
function getLambdas(logger: Logger) {
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()));
return evalMath(this, render(text.trim()), logger);
},
ParseHjson: () =>
function (text: string, render: RenderFn) {
return parseHjson(render(text.trim()));
return parseHjson(render(text.trim()), logger);
},
FormatDate: () =>
function (text: string, render: RenderFn) {
const dateString = render(text.trim()).trim();
return formatDate(dateString);
return formatDate(dateString, logger);
},
FormatNumber: () =>
function (text: string, render: RenderFn) {
const numberString = render(text.trim()).trim();
return formatNumber(numberString);
return formatNumber(logger, numberString);
},
};
}
function evalMath(vars: Variables, o: unknown): string {
function evalMath(vars: Variables, o: unknown, logger: Logger): 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}`);
return logAndReturnErr(
logger,
`error evaluating tinymath expression "${expr}": ${err.message}`
);
}
}
function parseHjson(o: unknown): string {
function parseHjson(o: unknown, logger: Logger): string {
const hjsonObject = `${o}`;
let object: unknown;
try {
object = hjsonParse(hjsonObject);
} catch (err) {
throw new Error(`error parsing Hjson "${hjsonObject}": ${err.message}`);
return logAndReturnErr(logger, `error parsing Hjson "${hjsonObject}": ${err.message}`);
}
return JSON.stringify(object);
}
function formatDate(dateString: unknown): string {
function formatDate(dateString: unknown, logger: Logger): string {
const { date, timeZone, format } = splitDateString(`${dateString}`);
if (date === '') {
throw new Error(`date is empty`);
return logAndReturnErr(logger, `date is empty`);
}
if (isNaN(new Date(date).valueOf())) {
throw new Error(`invalid date "${date}"`);
return logAndReturnErr(logger, `invalid date "${date}"`);
}
let mDate: moment.Moment;
try {
mDate = moment(date);
if (!mDate.isValid()) {
throw new Error(`date is invalid`);
return logAndReturnErr(logger, `invalid date "${date}"`);
}
} catch (err) {
throw new Error(`error evaluating moment date "${date}": ${err.message}`);
return logAndReturnErr(logger, `error evaluating moment date "${date}": ${err.message}`);
}
if (!TimeZoneSet.has(timeZone)) {
throw new Error(`unknown timeZone value "${timeZone}"`);
return logAndReturnErr(logger, `unknown timeZone value "${timeZone}"`);
}
try {
mDate.tz(timeZone);
} catch (err) {
throw new Error(`error evaluating moment timeZone "${timeZone}": ${err.message}`);
return logAndReturnErr(
logger,
`error evaluating moment timeZone "${timeZone}": ${err.message}`
);
}
try {
return mDate.format(format);
} catch (err) {
throw new Error(`error evaluating moment format "${format}": ${err.message}`);
return logAndReturnErr(logger, `error evaluating moment format "${format}": ${err.message}`);
}
}
@ -118,3 +125,8 @@ function splitDateString(dateString: string) {
format: format || DefaultDateFormat,
};
}
function logAndReturnErr(logger: Logger, errMessage: string): string {
logger.warn(`mustache render error: ${errMessage}`);
return errMessage;
}

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { loggingSystemMock } from '@kbn/core/server/mocks';
import {
renderMustacheString,
renderMustacheStringNoEscape,
@ -12,6 +13,8 @@ import {
Escape,
} from './mustache_renderer';
const logger = loggingSystemMock.create().get();
const variables = {
a: 1,
b: '2',
@ -38,97 +41,121 @@ const variables = {
};
describe('mustache_renderer', () => {
beforeEach(() => {
jest.resetAllMocks();
});
describe('renderMustacheString()', () => {
for (const escapeVal of ['none', 'slack', 'markdown', 'json']) {
const escape = escapeVal as Escape;
it(`handles basic templating that does not need escaping for ${escape}`, () => {
expect(renderMustacheString('', variables, escape)).toBe('');
expect(renderMustacheString('{{a}}', variables, escape)).toBe('1');
expect(renderMustacheString('{{b}}', variables, escape)).toBe('2');
expect(renderMustacheString('{{c}}', variables, escape)).toBe('false');
expect(renderMustacheString('{{d}}', variables, escape)).toBe('');
expect(renderMustacheString('{{e}}', variables, escape)).toBe('');
expect(renderMustacheString(logger, '', variables, escape)).toBe('');
expect(renderMustacheString(logger, '{{a}}', variables, escape)).toBe('1');
expect(renderMustacheString(logger, '{{b}}', variables, escape)).toBe('2');
expect(renderMustacheString(logger, '{{c}}', variables, escape)).toBe('false');
expect(renderMustacheString(logger, '{{d}}', variables, escape)).toBe('');
expect(renderMustacheString(logger, '{{e}}', variables, escape)).toBe('');
if (escape === 'json') {
expect(renderMustacheString('{{f}}', variables, escape)).toBe('{\\"g\\":3,\\"h\\":null}');
expect(renderMustacheString(logger, '{{f}}', variables, escape)).toBe(
'{\\"g\\":3,\\"h\\":null}'
);
} else if (escape === 'markdown') {
expect(renderMustacheString('{{f}}', variables, escape)).toBe('\\{"g":3,"h":null\\}');
expect(renderMustacheString(logger, '{{f}}', variables, escape)).toBe(
'\\{"g":3,"h":null\\}'
);
} else {
expect(renderMustacheString('{{f}}', variables, escape)).toBe('{"g":3,"h":null}');
expect(renderMustacheString(logger, '{{f}}', variables, escape)).toBe('{"g":3,"h":null}');
}
expect(renderMustacheString('{{f.g}}', variables, escape)).toBe('3');
expect(renderMustacheString('{{f.h}}', variables, escape)).toBe('');
expect(renderMustacheString('{{i}}', variables, escape)).toBe('42,43,44');
expect(renderMustacheString(logger, '{{f.g}}', variables, escape)).toBe('3');
expect(renderMustacheString(logger, '{{f.h}}', variables, escape)).toBe('');
expect(renderMustacheString(logger, '{{i}}', variables, escape)).toBe('42,43,44');
if (escape === 'markdown') {
expect(renderMustacheString('{{i.asJSON}}', variables, escape)).toBe('\\[42,43,44\\]');
expect(renderMustacheString(logger, '{{i.asJSON}}', variables, escape)).toBe(
'\\[42,43,44\\]'
);
} else {
expect(renderMustacheString('{{i.asJSON}}', variables, escape)).toBe('[42,43,44]');
expect(renderMustacheString(logger, '{{i.asJSON}}', variables, escape)).toBe(
'[42,43,44]'
);
}
});
}
it('handles escape:none with commonly escaped strings', () => {
expect(renderMustacheString('{{lt}}', variables, 'none')).toBe(variables.lt);
expect(renderMustacheString('{{gt}}', variables, 'none')).toBe(variables.gt);
expect(renderMustacheString('{{amp}}', variables, 'none')).toBe(variables.amp);
expect(renderMustacheString('{{nl}}', variables, 'none')).toBe(variables.nl);
expect(renderMustacheString('{{dq}}', variables, 'none')).toBe(variables.dq);
expect(renderMustacheString('{{bt}}', variables, 'none')).toBe(variables.bt);
expect(renderMustacheString('{{bs}}', variables, 'none')).toBe(variables.bs);
expect(renderMustacheString('{{st}}', variables, 'none')).toBe(variables.st);
expect(renderMustacheString('{{ul}}', variables, 'none')).toBe(variables.ul);
expect(renderMustacheString(logger, '{{lt}}', variables, 'none')).toBe(variables.lt);
expect(renderMustacheString(logger, '{{gt}}', variables, 'none')).toBe(variables.gt);
expect(renderMustacheString(logger, '{{amp}}', variables, 'none')).toBe(variables.amp);
expect(renderMustacheString(logger, '{{nl}}', variables, 'none')).toBe(variables.nl);
expect(renderMustacheString(logger, '{{dq}}', variables, 'none')).toBe(variables.dq);
expect(renderMustacheString(logger, '{{bt}}', variables, 'none')).toBe(variables.bt);
expect(renderMustacheString(logger, '{{bs}}', variables, 'none')).toBe(variables.bs);
expect(renderMustacheString(logger, '{{st}}', variables, 'none')).toBe(variables.st);
expect(renderMustacheString(logger, '{{ul}}', variables, 'none')).toBe(variables.ul);
});
it('handles escape:markdown with commonly escaped strings', () => {
expect(renderMustacheString('{{lt}}', variables, 'markdown')).toBe(variables.lt);
expect(renderMustacheString('{{gt}}', variables, 'markdown')).toBe(variables.gt);
expect(renderMustacheString('{{amp}}', variables, 'markdown')).toBe(variables.amp);
expect(renderMustacheString('{{nl}}', variables, 'markdown')).toBe(variables.nl);
expect(renderMustacheString('{{dq}}', variables, 'markdown')).toBe(variables.dq);
expect(renderMustacheString('{{bt}}', variables, 'markdown')).toBe('\\' + variables.bt);
expect(renderMustacheString('{{bs}}', variables, 'markdown')).toBe('\\' + variables.bs);
expect(renderMustacheString('{{st}}', variables, 'markdown')).toBe('\\' + variables.st);
expect(renderMustacheString('{{ul}}', variables, 'markdown')).toBe('\\' + variables.ul);
expect(renderMustacheString('{{vl}}', variables, 'markdown')).toBe('\\' + variables.vl);
expect(renderMustacheString(logger, '{{lt}}', variables, 'markdown')).toBe(variables.lt);
expect(renderMustacheString(logger, '{{gt}}', variables, 'markdown')).toBe(variables.gt);
expect(renderMustacheString(logger, '{{amp}}', variables, 'markdown')).toBe(variables.amp);
expect(renderMustacheString(logger, '{{nl}}', variables, 'markdown')).toBe(variables.nl);
expect(renderMustacheString(logger, '{{dq}}', variables, 'markdown')).toBe(variables.dq);
expect(renderMustacheString(logger, '{{bt}}', variables, 'markdown')).toBe(
'\\' + variables.bt
);
expect(renderMustacheString(logger, '{{bs}}', variables, 'markdown')).toBe(
'\\' + variables.bs
);
expect(renderMustacheString(logger, '{{st}}', variables, 'markdown')).toBe(
'\\' + variables.st
);
expect(renderMustacheString(logger, '{{ul}}', variables, 'markdown')).toBe(
'\\' + variables.ul
);
expect(renderMustacheString(logger, '{{vl}}', variables, 'markdown')).toBe(
'\\' + variables.vl
);
});
it('handles triple escapes', () => {
expect(renderMustacheString('{{{bt}}}', variables, 'markdown')).toBe(variables.bt);
expect(renderMustacheString('{{{bs}}}', variables, 'markdown')).toBe(variables.bs);
expect(renderMustacheString('{{{st}}}', variables, 'markdown')).toBe(variables.st);
expect(renderMustacheString('{{{ul}}}', variables, 'markdown')).toBe(variables.ul);
expect(renderMustacheString(logger, '{{{bt}}}', variables, 'markdown')).toBe(variables.bt);
expect(renderMustacheString(logger, '{{{bs}}}', variables, 'markdown')).toBe(variables.bs);
expect(renderMustacheString(logger, '{{{st}}}', variables, 'markdown')).toBe(variables.st);
expect(renderMustacheString(logger, '{{{ul}}}', variables, 'markdown')).toBe(variables.ul);
});
it('handles escape:slack with commonly escaped strings', () => {
expect(renderMustacheString('{{lt}}', variables, 'slack')).toBe('&lt;');
expect(renderMustacheString('{{gt}}', variables, 'slack')).toBe('&gt;');
expect(renderMustacheString('{{amp}}', variables, 'slack')).toBe('&amp;');
expect(renderMustacheString('{{nl}}', variables, 'slack')).toBe(variables.nl);
expect(renderMustacheString('{{dq}}', variables, 'slack')).toBe(variables.dq);
expect(renderMustacheString('{{bt}}', variables, 'slack')).toBe(`'`);
expect(renderMustacheString('{{bs}}', variables, 'slack')).toBe(variables.bs);
expect(renderMustacheString('{{st}}', variables, 'slack')).toBe('`*`');
expect(renderMustacheString('{{ul}}', variables, 'slack')).toBe('`_`');
expect(renderMustacheString(logger, '{{lt}}', variables, 'slack')).toBe('&lt;');
expect(renderMustacheString(logger, '{{gt}}', variables, 'slack')).toBe('&gt;');
expect(renderMustacheString(logger, '{{amp}}', variables, 'slack')).toBe('&amp;');
expect(renderMustacheString(logger, '{{nl}}', variables, 'slack')).toBe(variables.nl);
expect(renderMustacheString(logger, '{{dq}}', variables, 'slack')).toBe(variables.dq);
expect(renderMustacheString(logger, '{{bt}}', variables, 'slack')).toBe(`'`);
expect(renderMustacheString(logger, '{{bs}}', variables, 'slack')).toBe(variables.bs);
expect(renderMustacheString(logger, '{{st}}', variables, 'slack')).toBe('`*`');
expect(renderMustacheString(logger, '{{ul}}', variables, 'slack')).toBe('`_`');
// html escapes not needed when using backtic escaping
expect(renderMustacheString('{{st_lt}}', variables, 'slack')).toBe('`*<`');
expect(renderMustacheString('{{link}}', variables, 'slack')).toBe('https://te_st.com/');
expect(renderMustacheString(logger, '{{st_lt}}', variables, 'slack')).toBe('`*<`');
expect(renderMustacheString(logger, '{{link}}', variables, 'slack')).toBe(
'https://te_st.com/'
);
});
it('handles escape:json with commonly escaped strings', () => {
expect(renderMustacheString('{{lt}}', variables, 'json')).toBe(variables.lt);
expect(renderMustacheString('{{gt}}', variables, 'json')).toBe(variables.gt);
expect(renderMustacheString('{{amp}}', variables, 'json')).toBe(variables.amp);
expect(renderMustacheString('{{nl}}', variables, 'json')).toBe('\\n');
expect(renderMustacheString('{{dq}}', variables, 'json')).toBe('\\"');
expect(renderMustacheString('{{bt}}', variables, 'json')).toBe(variables.bt);
expect(renderMustacheString('{{bs}}', variables, 'json')).toBe('\\\\');
expect(renderMustacheString('{{st}}', variables, 'json')).toBe(variables.st);
expect(renderMustacheString('{{ul}}', variables, 'json')).toBe(variables.ul);
expect(renderMustacheString(logger, '{{lt}}', variables, 'json')).toBe(variables.lt);
expect(renderMustacheString(logger, '{{gt}}', variables, 'json')).toBe(variables.gt);
expect(renderMustacheString(logger, '{{amp}}', variables, 'json')).toBe(variables.amp);
expect(renderMustacheString(logger, '{{nl}}', variables, 'json')).toBe('\\n');
expect(renderMustacheString(logger, '{{dq}}', variables, 'json')).toBe('\\"');
expect(renderMustacheString(logger, '{{bt}}', variables, 'json')).toBe(variables.bt);
expect(renderMustacheString(logger, '{{bs}}', variables, 'json')).toBe('\\\\');
expect(renderMustacheString(logger, '{{st}}', variables, 'json')).toBe(variables.st);
expect(renderMustacheString(logger, '{{ul}}', variables, 'json')).toBe(variables.ul);
});
it('handles errors', () => {
expect(renderMustacheString('{{a}', variables, 'none')).toMatchInlineSnapshot(
expect(renderMustacheString(logger, '{{a}', variables, 'none')).toMatchInlineSnapshot(
`"error rendering mustache template \\"{{a}\\": Unclosed tag at 4"`
);
});
@ -285,7 +312,7 @@ describe('mustache_renderer', () => {
describe('renderMustacheObject()', () => {
it('handles deep objects', () => {
expect(renderMustacheObject(object, variables)).toMatchInlineSnapshot(`
expect(renderMustacheObject(logger, object, variables)).toMatchInlineSnapshot(`
Object {
"list": Array [
"1",
@ -311,12 +338,12 @@ describe('mustache_renderer', () => {
});
it('handles primitive objects', () => {
expect(renderMustacheObject(undefined, variables)).toMatchInlineSnapshot(`undefined`);
expect(renderMustacheObject(null, variables)).toMatchInlineSnapshot(`null`);
expect(renderMustacheObject(0, variables)).toMatchInlineSnapshot(`0`);
expect(renderMustacheObject(true, variables)).toMatchInlineSnapshot(`true`);
expect(renderMustacheObject('{{a}}', variables)).toMatchInlineSnapshot(`"1"`);
expect(renderMustacheObject(['{{a}}'], variables)).toMatchInlineSnapshot(`
expect(renderMustacheObject(logger, undefined, variables)).toMatchInlineSnapshot(`undefined`);
expect(renderMustacheObject(logger, null, variables)).toMatchInlineSnapshot(`null`);
expect(renderMustacheObject(logger, 0, variables)).toMatchInlineSnapshot(`0`);
expect(renderMustacheObject(logger, true, variables)).toMatchInlineSnapshot(`true`);
expect(renderMustacheObject(logger, '{{a}}', variables)).toMatchInlineSnapshot(`"1"`);
expect(renderMustacheObject(logger, ['{{a}}'], variables)).toMatchInlineSnapshot(`
Array [
"1",
]
@ -324,7 +351,7 @@ describe('mustache_renderer', () => {
});
it('handles errors', () => {
expect(renderMustacheObject({ a: '{{a}' }, variables)).toMatchInlineSnapshot(`
expect(renderMustacheObject(logger, { a: '{{a}' }, variables)).toMatchInlineSnapshot(`
Object {
"a": "error rendering mustache template \\"{{a}\\": Unclosed tag at 4",
}
@ -338,7 +365,7 @@ describe('mustache_renderer', () => {
b: { c: 2, d: [3, 4] },
e: [5, { f: 6, g: 7 }],
};
expect(renderMustacheObject({ x: '{{a}} - {{b}} -- {{e}} ' }, deepVariables))
expect(renderMustacheObject(logger, { x: '{{a}} - {{b}} -- {{e}} ' }, deepVariables))
.toMatchInlineSnapshot(`
Object {
"x": "1 - {\\"c\\":2,\\"d\\":[3,4]} -- 5,{\\"f\\":6,\\"g\\":7} ",
@ -346,10 +373,12 @@ 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(logger, '{{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(
expect(renderMustacheString(logger, '{{e}}', deepVariables, 'none')).toEqual('5,{"f":6,"g":7}');
expect(renderMustacheString(logger, '{{e.asJSON}}', deepVariables, 'none')).toEqual(
'[5,{"f":6,"g":7}]'
);
});
@ -395,6 +424,7 @@ describe('mustache_renderer', () => {
expect(
renderMustacheObject(
logger,
{
x: '{{context.0._source.kibana.alert.rule.category}} - {{context.0._score.test}} - {{context.0._source.kibana.alert.time_range.gte}}',
},
@ -408,6 +438,7 @@ describe('mustache_renderer', () => {
expect(
renderMustacheString(
logger,
'{{context.0._source.kibana.alert.rule.category}} - {{context.0._score.test}} - {{context.0._source.kibana.alert.time_range.gte}}',
dotVariables,
'none'
@ -416,12 +447,13 @@ describe('mustache_renderer', () => {
});
it('should replace single value with the object', () => {
expect(renderMustacheObject({ x: '{{a}}' }, { a: 1, 'a.b': 2 })).toMatchInlineSnapshot(`
expect(renderMustacheObject(logger, { x: '{{a}}' }, { a: 1, 'a.b': 2 }))
.toMatchInlineSnapshot(`
Object {
"x": "{\\"b\\":2}",
}
`);
expect(renderMustacheString('{{a}}', { a: 1, 'a.b': 2 }, 'none')).toEqual('{"b":2}');
expect(renderMustacheString(logger, '{{a}}', { a: 1, 'a.b': 2 }, 'none')).toEqual('{"b":2}');
});
});
});

View file

@ -7,6 +7,7 @@
import Mustache from 'mustache';
import { isString, isPlainObject, cloneDeepWith, merge } from 'lodash';
import { Logger } from '@kbn/core/server';
import { getMustacheLambdas } from './mustache_lambdas';
export type Escape = 'markdown' | 'slack' | 'json' | 'none';
@ -25,9 +26,14 @@ 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 {
export function renderMustacheString(
logger: Logger,
string: string,
variables: Variables,
escape: Escape
): string {
const augmentedVariables = augmentObjectVariables(variables);
const lambdas = getMustacheLambdas();
const lambdas = getMustacheLambdas(logger);
const previousMustacheEscape = Mustache.escape;
Mustache.escape = getEscape(escape);
@ -43,13 +49,17 @@ export function renderMustacheString(string: string, variables: Variables, escap
}
// return a cloned object with all strings rendered as mustache templates
export function renderMustacheObject<Params>(params: Params, variables: Variables): Params {
export function renderMustacheObject<Params>(
logger: Logger,
params: Params,
variables: Variables
): Params {
const augmentedVariables = augmentObjectVariables(variables);
const result = cloneDeepWith(params, (value: unknown) => {
if (!isString(value)) return;
// since we're rendering a JS object, no escaping needed
return renderMustacheString(value, augmentedVariables, 'none');
return renderMustacheString(logger, value, augmentedVariables, 'none');
});
// The return type signature for `cloneDeep()` ends up taking the return

View file

@ -5,52 +5,76 @@
* 2.0.
*/
import { loggingSystemMock } from '@kbn/core/server/mocks';
import { formatNumber } from './number_formatter';
const logger = loggingSystemMock.create().get();
describe('formatNumber()', () => {
beforeEach(() => {
jest.restoreAllMocks();
});
it('using defaults is successful', () => {
expect(formatNumber('1;;')).toMatchInlineSnapshot(`"1"`);
expect(formatNumber(logger, '1;;')).toMatchInlineSnapshot(`"1"`);
});
it('error cases handled', () => {
expect(formatNumber('1')).toMatchInlineSnapshot(`"invalid format, missing semicolons: '1'"`);
expect(formatNumber('nope;;')).toMatchInlineSnapshot(`"invalid number: 'nope'"`);
expect(formatNumber('1;; nah')).toMatchInlineSnapshot(
`"invalid options: missing colon in option: 'nah'"`
expect(formatNumber(logger, '1')).toEqual(`invalid format, missing semicolons: '1'`);
expect(logger.warn).toHaveBeenCalledWith(
`mustache render error: invalid format, missing semicolons: '1'`
);
expect(formatNumber('1;; minimumIntegerDigits: N.O.')).toMatchInlineSnapshot(
`"error formatting number: minimumIntegerDigits value is out of range."`
expect(formatNumber(logger, 'nope;;')).toEqual(`invalid number: 'nope'`);
expect(logger.warn).toHaveBeenCalledWith(`mustache render error: invalid number: 'nope'`);
expect(formatNumber(logger, '1;; nah')).toEqual(
`invalid options: missing colon in option: 'nah'`
);
expect(formatNumber('1;; compactDisplay: uhuh')).toMatchInlineSnapshot(
`"error formatting number: Value uhuh out of range for Intl.NumberFormat options property compactDisplay"`
expect(logger.warn).toHaveBeenCalledWith(
`mustache render error: invalid options: missing colon in option: 'nah'`
);
expect(formatNumber(logger, '1;; minimumIntegerDigits: N.O.')).toEqual(
'error formatting number: minimumIntegerDigits value is out of range.'
);
expect(logger.warn).toHaveBeenCalledWith(
`mustache render error: error formatting number: minimumIntegerDigits value is out of range.`
);
expect(formatNumber(logger, '1;; compactDisplay: uhuh')).toEqual(
'error formatting number: Value uhuh out of range for Intl.NumberFormat options property compactDisplay'
);
expect(logger.warn).toHaveBeenCalledWith(
`mustache render error: error formatting number: Value uhuh out of range for Intl.NumberFormat options property compactDisplay`
);
});
it('using locales is successful', () => {
expect(formatNumber('1000; de-DE;')).toMatchInlineSnapshot(`"1.000"`);
expect(formatNumber(logger, '1000; de-DE;')).toMatchInlineSnapshot(`"1.000"`);
});
it('option compactDisplay is successful', () => {
expect(
formatNumber(' 1000;; notation: compact, compactDisplay: short, ')
formatNumber(logger, ' 1000;; notation: compact, compactDisplay: short, ')
).toMatchInlineSnapshot(`"1K"`);
});
it('option currency is successful', () => {
expect(formatNumber('1000;; currency: EUR, style: currency')).toMatchInlineSnapshot(
expect(formatNumber(logger, '1000;; currency: EUR, style: currency')).toMatchInlineSnapshot(
`"€1,000.00"`
);
});
it('option currencyDisplay is successful', () => {
expect(
formatNumber('1000;; currency: EUR, style: currency, currencyDisplay: name')
formatNumber(logger, '1000;; currency: EUR, style: currency, currencyDisplay: name')
).toMatchInlineSnapshot(`"1,000.00 euros"`);
});
it('option currencySign is successful', () => {
expect(
formatNumber('-1;; currency: EUR, style: currency, currencySign: accounting')
formatNumber(logger, '-1;; currency: EUR, style: currency, currencySign: accounting')
).toMatchInlineSnapshot(`"(€1.00)"`);
});
@ -60,34 +84,36 @@ describe('formatNumber()', () => {
it.skip('option localeMatcher is successful', () => {});
it('option notation is successful', () => {
expect(formatNumber('1000;; notation: engineering')).toMatchInlineSnapshot(`"1E3"`);
expect(formatNumber(logger, '1000;; notation: engineering')).toMatchInlineSnapshot(`"1E3"`);
});
it('option numberingSystem is successful', () => {
expect(formatNumber('1;; numberingSystem: fullwide')).toMatchInlineSnapshot(`""`);
expect(formatNumber(logger, '1;; numberingSystem: fullwide')).toMatchInlineSnapshot(`""`);
});
it('option signDisplay is successful', () => {
expect(formatNumber('1;; signDisplay: always')).toMatchInlineSnapshot(`"+1"`);
expect(formatNumber(logger, '1;; signDisplay: always')).toMatchInlineSnapshot(`"+1"`);
});
it('option style is successful', () => {
expect(formatNumber('1;; style: percent')).toMatchInlineSnapshot(`"100%"`);
expect(formatNumber(logger, '1;; style: percent')).toMatchInlineSnapshot(`"100%"`);
});
it('option unit is successful', () => {
expect(formatNumber('1;; style: unit, unit: acre-per-liter')).toMatchInlineSnapshot(`"1 ac/L"`);
expect(formatNumber(logger, '1;; style: unit, unit: acre-per-liter')).toMatchInlineSnapshot(
`"1 ac/L"`
);
});
it('option unitDisplay is successful', () => {
expect(
formatNumber('1;; style: unit, unit: petabyte, unitDisplay: narrow')
formatNumber(logger, '1;; style: unit, unit: petabyte, unitDisplay: narrow')
).toMatchInlineSnapshot(`"1PB"`);
});
it('option useGrouping is successful', () => {
expect(formatNumber('1000;; useGrouping: true ')).toMatchInlineSnapshot(`"1,000"`);
expect(formatNumber('1000;; useGrouping: false')).toMatchInlineSnapshot(`"1000"`);
expect(formatNumber(logger, '1000;; useGrouping: true ')).toMatchInlineSnapshot(`"1,000"`);
expect(formatNumber(logger, '1000;; useGrouping: false')).toMatchInlineSnapshot(`"1000"`);
});
// not yet supported in node.js
@ -103,22 +129,28 @@ describe('formatNumber()', () => {
it.skip('option trailingZeroDisplay is successful', () => {});
it('option minimumIntegerDigits is successful', () => {
expect(formatNumber('1;; minimumIntegerDigits: 7')).toMatchInlineSnapshot(`"0,000,001"`);
expect(formatNumber(logger, '1;; minimumIntegerDigits: 7')).toMatchInlineSnapshot(
`"0,000,001"`
);
});
it('option minimumFractionDigits is successful', () => {
expect(formatNumber('1;; minimumFractionDigits: 3')).toMatchInlineSnapshot(`"1.000"`);
expect(formatNumber(logger, '1;; minimumFractionDigits: 3')).toMatchInlineSnapshot(`"1.000"`);
});
it('option maximumFractionDigits is successful', () => {
expect(formatNumber('1.234;; maximumFractionDigits: 2')).toMatchInlineSnapshot(`"1.23"`);
expect(formatNumber(logger, '1.234;; maximumFractionDigits: 2')).toMatchInlineSnapshot(
`"1.23"`
);
});
it('option minimumSignificantDigits is successful', () => {
expect(formatNumber('1;; minimumSignificantDigits: 3')).toMatchInlineSnapshot(`"1.00"`);
expect(formatNumber(logger, '1;; minimumSignificantDigits: 3')).toMatchInlineSnapshot(`"1.00"`);
});
it('option maximumSignificantDigits is successful', () => {
expect(formatNumber('123456;; maximumSignificantDigits: 4')).toMatchInlineSnapshot(`"123,500"`);
expect(formatNumber(logger, '123456;; maximumSignificantDigits: 4')).toMatchInlineSnapshot(
`"123,500"`
);
});
});

View file

@ -4,6 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { Logger } from '@kbn/core/server';
const DEFAULT_LOCALES = ['en-US'];
@ -30,24 +31,28 @@ const DEFAULT_LOCALES = ['en-US'];
* @param numberAndFormat string containing a number and formatting options
* @returns number formatted according to the options
*/
export function formatNumber(numberLocalesOptions: string): string {
export function formatNumber(logger: Logger, numberLocalesOptions: string): string {
const [numString, localesString, optionsString] = splitNumberLocalesOptions(numberLocalesOptions);
if (localesString === undefined || optionsString === undefined) {
return `invalid format, missing semicolons: '${numberLocalesOptions}'`;
return logAndReturnErr(logger, `invalid format, missing semicolons: '${numberLocalesOptions}'`);
}
const num = parseFloat(numString);
if (isNaN(num)) return `invalid number: '${numString}'`;
if (isNaN(num)) {
return logAndReturnErr(logger, `invalid number: '${numString}'`);
}
const locales = getLocales(localesString);
const [options, optionsError] = getOptions(optionsString);
if (optionsError) return `invalid options: ${optionsError}`;
if (optionsError) {
return logAndReturnErr(logger, `invalid options: ${optionsError}`);
}
try {
return new Intl.NumberFormat(locales, options).format(num);
} catch (err) {
return `error formatting number: ${err.message}`;
return logAndReturnErr(logger, `error formatting number: ${err.message}`);
}
}
@ -110,3 +115,8 @@ function splitNumberLocalesOptions(
const [num, locales, options] = numberLocalesOptions.split(';', 3);
return [num.trim(), locales?.trim(), options?.trim()];
}
function logAndReturnErr(logger: Logger, errMessage: string): string {
logger.warn(`mustache render error: ${errMessage}`);
return errMessage;
}

View file

@ -59,7 +59,14 @@ export function renderActionParameterTemplatesDefault<RecordType>(
params: Record<string, unknown>,
variables: Record<string, unknown>
) {
return renderActionParameterTemplates(undefined, actionTypeId, actionId, params, variables);
return renderActionParameterTemplates(
logger,
undefined,
actionTypeId,
actionId,
params,
variables
);
}
const createServicesMock = () => {

View file

@ -585,7 +585,7 @@ export class ActionsPlugin implements Plugin<PluginSetupContract, PluginStartCon
getUnsecuredActionsClient,
inMemoryConnectors: this.inMemoryConnectors,
renderActionParameterTemplates: (...args) =>
renderActionParameterTemplates(actionTypeRegistry, ...args),
renderActionParameterTemplates(this.logger, actionTypeRegistry, ...args),
};
}
@ -743,6 +743,7 @@ export class ActionsPlugin implements Plugin<PluginSetupContract, PluginStartCon
}
export function renderActionParameterTemplates<Params extends ActionTypeParams = ActionTypeParams>(
logger: Logger,
actionTypeRegistry: ActionTypeRegistry | undefined,
actionTypeId: string,
actionId: string,
@ -751,8 +752,8 @@ export function renderActionParameterTemplates<Params extends ActionTypeParams =
): Params {
const actionType = actionTypeRegistry?.get(actionTypeId);
if (actionType?.renderParameterTemplates) {
return actionType.renderParameterTemplates(params, variables, actionId) as Params;
return actionType.renderParameterTemplates(logger, params, variables, actionId) as Params;
} else {
return renderMustacheObject(params, variables);
return renderMustacheObject(logger, params, variables);
}
}

View file

@ -77,9 +77,9 @@ describe('Registration', () => {
const actionId = 'action-id';
const { renderParameterTemplates } = actionTypeRegistry.register.mock.calls[0][0];
const rendered = renderParameterTemplates?.(params, variables, actionId);
const rendered = renderParameterTemplates?.(logger, params, variables, actionId);
expect(mockRenderParameterTemplates).toHaveBeenCalledWith(params, variables, actionId);
expect(mockRenderParameterTemplates).toHaveBeenCalledWith(logger, params, variables, actionId);
expect(rendered).toBe(renderedVariables);
});
});

View file

@ -119,6 +119,7 @@ export interface ActionValidationService {
}
export type RenderParameterTemplates<Params extends ActionTypeParams> = (
logger: Logger,
params: Params,
variables: Record<string, unknown>,
actionId?: string

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { loggingSystemMock } from '@kbn/core/server/mocks';
import { renderParameterTemplates } from './render';
import Mustache from 'mustache';
@ -16,16 +17,20 @@ const params = {
};
const variables = { domain: 'm0zepcuuu2' };
const logger = loggingSystemMock.createLogger();
describe('Bedrock - renderParameterTemplates', () => {
beforeEach(() => {
jest.resetAllMocks();
});
it('should not render body on test action', () => {
const testParams = { subAction: 'test', subActionParams: { body: 'test_json' } };
const result = renderParameterTemplates(testParams, variables);
const result = renderParameterTemplates(logger, testParams, variables);
expect(result).toEqual(testParams);
});
it('should rendered body with variables', () => {
const result = renderParameterTemplates(params, variables);
const result = renderParameterTemplates(logger, params, variables);
expect(result.subActionParams.body).toEqual(
JSON.stringify({
@ -39,7 +44,7 @@ describe('Bedrock - renderParameterTemplates', () => {
jest.spyOn(Mustache, 'render').mockImplementation(() => {
throw new Error(errorMessage);
});
const result = renderParameterTemplates(params, variables);
const result = renderParameterTemplates(logger, params, variables);
expect(result.subActionParams.body).toEqual(
'error rendering mustache template "{"domain":"{{domain}}"}": test error'
);

View file

@ -11,6 +11,7 @@ import { RenderParameterTemplates } from '@kbn/actions-plugin/server/types';
import { SUB_ACTION } from '../../../common/bedrock/constants';
export const renderParameterTemplates: RenderParameterTemplates<ExecutorParams> = (
logger,
params,
variables
) => {
@ -20,7 +21,7 @@ export const renderParameterTemplates: RenderParameterTemplates<ExecutorParams>
...params,
subActionParams: {
...params.subActionParams,
body: renderMustacheString(params.subActionParams.body as string, variables, 'json'),
body: renderMustacheString(logger, params.subActionParams.body as string, variables, 'json'),
},
};
};

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { loggingSystemMock } from '@kbn/core/server/mocks';
import { renderParameterTemplates } from './render';
import Mustache from 'mustache';
@ -17,20 +18,25 @@ const params = {
},
};
const logger = loggingSystemMock.createLogger();
const variables = { domain: 'm0zepcuuu2' };
describe('D3 Security - renderParameterTemplates', () => {
beforeEach(() => {
jest.resetAllMocks();
});
it('should not render body on test action', () => {
const testParams = {
subAction: 'test',
subActionParams: { ...params.subActionParams, body: 'test_json' },
};
const result = renderParameterTemplates(testParams, variables);
const result = renderParameterTemplates(logger, testParams, variables);
expect(result).toEqual(testParams);
});
it('should rendered body with variables', () => {
const result = renderParameterTemplates(params, variables);
const result = renderParameterTemplates(logger, params, variables);
expect(result.subActionParams.body).toEqual(
JSON.stringify({
@ -44,7 +50,7 @@ describe('D3 Security - renderParameterTemplates', () => {
jest.spyOn(Mustache, 'render').mockImplementation(() => {
throw new Error(errorMessage);
});
const result = renderParameterTemplates(params, variables);
const result = renderParameterTemplates(logger, params, variables);
expect(result.subActionParams.body).toEqual(
'error rendering mustache template "{"domain":"{{domain}}"}": test error'
);

View file

@ -11,6 +11,7 @@ import { RenderParameterTemplates } from '@kbn/actions-plugin/server/types';
import { SUB_ACTION } from '../../../common/d3security/constants';
export const renderParameterTemplates: RenderParameterTemplates<ExecutorParams> = (
logger,
params,
variables
) => {
@ -20,7 +21,7 @@ export const renderParameterTemplates: RenderParameterTemplates<ExecutorParams>
...params,
subActionParams: {
...params.subActionParams,
body: renderMustacheString(params.subActionParams.body as string, variables, 'json'),
body: renderMustacheString(logger, params.subActionParams.body as string, variables, 'json'),
},
};
};

View file

@ -845,7 +845,11 @@ describe('execute()', () => {
const variables = {
rogue: '*bold*',
};
const renderedParams = connectorType.renderParameterTemplates!(paramsWithTemplates, variables);
const renderedParams = connectorType.renderParameterTemplates!(
mockedLogger,
paramsWithTemplates,
variables
);
expect(renderedParams.message).toBe('\\*bold\\*');
expect(renderedParams).toMatchInlineSnapshot(`
@ -887,7 +891,11 @@ describe('execute()', () => {
const variables = {
rogue: '*bold*',
};
const renderedParams = connectorType.renderParameterTemplates!(paramsWithTemplates, variables);
const renderedParams = connectorType.renderParameterTemplates!(
mockedLogger,
paramsWithTemplates,
variables
);
// Yes, this is tested in the snapshot below, but it's double-escaped there,
// so easier to see here that the escaping is correct.
expect(renderedParams.message).toBe('\\*bold\\*');

View file

@ -8,6 +8,7 @@
import { curry } from 'lodash';
import { i18n } from '@kbn/i18n';
import { schema, TypeOf } from '@kbn/config-schema';
import { Logger } from '@kbn/core/server';
import nodemailerGetService from 'nodemailer/lib/well-known';
import SMTPConnection from 'nodemailer/lib/smtp-connection';
import type {
@ -250,14 +251,15 @@ export function getConnectorType(params: GetConnectorTypeParams): EmailConnector
}
function renderParameterTemplates(
logger: Logger,
params: ActionParamsType,
variables: Record<string, unknown>
): ActionParamsType {
return {
// most of the params need no escaping
...renderMustacheObject(params, variables),
...renderMustacheObject(logger, params, variables),
// message however, needs to escaped as markdown
message: renderMustacheString(params.message, variables, 'markdown'),
message: renderMustacheString(logger, params.message, variables, 'markdown'),
};
}

View file

@ -371,6 +371,7 @@ describe('execute()', () => {
who: 'world',
};
const renderedParams = connectorType.renderParameterTemplates!(
mockedLogger,
paramsWithTemplates,
variables,
'action-type-id'
@ -397,6 +398,7 @@ describe('execute()', () => {
who: 'world',
};
const renderedParams = connectorType.renderParameterTemplates!(
mockedLogger,
paramsWithTemplates,
variables,
'action-type-id'
@ -446,6 +448,7 @@ describe('execute()', () => {
},
};
const renderedParams = connectorType.renderParameterTemplates!(
mockedLogger,
paramsWithTemplates,
variables,
AlertHistoryEsIndexConnectorId
@ -526,6 +529,7 @@ describe('execute()', () => {
},
};
const renderedParams = connectorType.renderParameterTemplates!(
mockedLogger,
paramsWithTemplates,
variables,
AlertHistoryEsIndexConnectorId
@ -583,6 +587,7 @@ describe('execute()', () => {
expect(() =>
connectorType.renderParameterTemplates!(
mockedLogger,
paramsWithTemplates,
variables,
AlertHistoryEsIndexConnectorId

View file

@ -160,11 +160,16 @@ async function executor(
}
function renderParameterTemplates(
logger: Logger,
params: ActionParamsType,
variables: Record<string, unknown>,
actionId?: string
): ActionParamsType {
const { documents, indexOverride } = renderMustacheObject<ActionParamsType>(params, variables);
const { documents, indexOverride } = renderMustacheObject<ActionParamsType>(
logger,
params,
variables
);
if (actionId === AlertHistoryEsIndexConnectorId) {
const alertHistoryDoc = buildAlertHistoryDocument(variables);

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { loggingSystemMock } from '@kbn/core/server/mocks';
import { renderParameterTemplates } from './render';
import Mustache from 'mustache';
@ -16,16 +17,17 @@ const params = {
};
const variables = { domain: 'm0zepcuuu2' };
const logger = loggingSystemMock.createLogger();
describe('OpenAI - renderParameterTemplates', () => {
it('should not render body on test action', () => {
const testParams = { subAction: 'test', subActionParams: { body: 'test_json' } };
const result = renderParameterTemplates(testParams, variables);
const result = renderParameterTemplates(logger, testParams, variables);
expect(result).toEqual(testParams);
});
it('should rendered body with variables', () => {
const result = renderParameterTemplates(params, variables);
const result = renderParameterTemplates(logger, params, variables);
expect(result.subActionParams.body).toEqual(
JSON.stringify({
@ -39,7 +41,7 @@ describe('OpenAI - renderParameterTemplates', () => {
jest.spyOn(Mustache, 'render').mockImplementation(() => {
throw new Error(errorMessage);
});
const result = renderParameterTemplates(params, variables);
const result = renderParameterTemplates(logger, params, variables);
expect(result.subActionParams.body).toEqual(
'error rendering mustache template "{"domain":"{{domain}}"}": test error'
);

View file

@ -11,6 +11,7 @@ import { RenderParameterTemplates } from '@kbn/actions-plugin/server/types';
import { SUB_ACTION } from '../../../common/openai/constants';
export const renderParameterTemplates: RenderParameterTemplates<ExecutorParams> = (
logger,
params,
variables
) => {
@ -20,7 +21,7 @@ export const renderParameterTemplates: RenderParameterTemplates<ExecutorParams>
...params,
subActionParams: {
...params.subActionParams,
body: renderMustacheString(params.subActionParams.body as string, variables, 'json'),
body: renderMustacheString(logger, params.subActionParams.body as string, variables, 'json'),
},
};
};

View file

@ -5,10 +5,12 @@
* 2.0.
*/
import { loggingSystemMock } from '@kbn/core/server/mocks';
import { OpsgenieSubActions } from '../../../common';
import { renderParameterTemplates } from './render_template_variables';
const ruleTagsTemplate = '{{rule.tags}}';
const logger = loggingSystemMock.createLogger();
describe('renderParameterTemplates', () => {
const variables = {
@ -20,6 +22,7 @@ describe('renderParameterTemplates', () => {
it('renders the rule.tags as a single string if subAction is not set to CreateAlert', () => {
expect(
renderParameterTemplates(
logger,
{
subAction: '',
subActionParams: {
@ -43,6 +46,7 @@ describe('renderParameterTemplates', () => {
it('does not transform the tags if the rule.tags string is not found', () => {
expect(
renderParameterTemplates(
logger,
{
subAction: OpsgenieSubActions.CreateAlert,
subActionParams: {
@ -66,6 +70,7 @@ describe('renderParameterTemplates', () => {
it('transforms the rule.tags to an empty array when the field does not exist in the variable', () => {
expect(
renderParameterTemplates(
logger,
{
subAction: OpsgenieSubActions.CreateAlert,
subActionParams: {
@ -87,6 +92,7 @@ describe('renderParameterTemplates', () => {
it('does not transform the tags when the field does not exist in the params', () => {
expect(
renderParameterTemplates(
logger,
{
subAction: OpsgenieSubActions.CreateAlert,
subActionParams: {},
@ -104,6 +110,7 @@ describe('renderParameterTemplates', () => {
it('replaces the rule.tags template with an array of strings', () => {
expect(
renderParameterTemplates(
logger,
{
subAction: OpsgenieSubActions.CreateAlert,
subActionParams: {
@ -132,6 +139,7 @@ describe('renderParameterTemplates', () => {
it('replaces the rule.tags template with only a single instance of the rule.tags even when the mustache template exists multiple times', () => {
expect(
renderParameterTemplates(
logger,
{
subAction: OpsgenieSubActions.CreateAlert,
subActionParams: {
@ -161,6 +169,7 @@ describe('renderParameterTemplates', () => {
it('replaces the rule.tags template with empty array and preserves the other values already in the array', () => {
expect(
renderParameterTemplates(
logger,
{
subAction: OpsgenieSubActions.CreateAlert,
subActionParams: {
@ -186,6 +195,7 @@ describe('renderParameterTemplates', () => {
it('replaces the rule.tags template with variable value when the path is a full string', () => {
expect(
renderParameterTemplates(
logger,
{
subAction: OpsgenieSubActions.CreateAlert,
subActionParams: {
@ -212,6 +222,7 @@ describe('renderParameterTemplates', () => {
it('replaces the rule.tags template and other templates', () => {
expect(
renderParameterTemplates(
logger,
{
subAction: OpsgenieSubActions.CreateAlert,
subActionParams: {

View file

@ -15,17 +15,18 @@ import { OpsgenieSubActions } from '../../../common';
import { CreateAlertSubActionParams } from './types';
export const renderParameterTemplates: RenderParameterTemplates<ExecutorParams> = (
logger,
params,
variables
) => {
if (!isCreateAlertSubAction(params) || !params.subActionParams.tags) {
return renderMustacheObject(params, variables);
return renderMustacheObject(logger, params, variables);
}
const foundRuleTagsTemplate = params.subActionParams.tags.includes(RULE_TAGS_TEMPLATE);
if (!foundRuleTagsTemplate) {
return renderMustacheObject(params, variables);
return renderMustacheObject(logger, params, variables);
}
const paramsCopy = cloneDeep(params);
@ -39,7 +40,7 @@ export const renderParameterTemplates: RenderParameterTemplates<ExecutorParams>
...getRuleTags(variables),
]);
return renderMustacheObject(paramsCopy, variables);
return renderMustacheObject(logger, paramsCopy, variables);
};
type CreateAlertParams = CreateAlertSubActionParams & Record<string, unknown>;

View file

@ -9,6 +9,7 @@ import { map } from 'lodash';
import { set } from '@kbn/safer-lodash-set/fp';
import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs';
import { ExecutorParams } from '@kbn/actions-plugin/server/sub_action_framework/types';
import { Logger } from '@kbn/core/server';
import { SUB_ACTION } from '../../../common/sentinelone/constants';
interface Context {
@ -16,6 +17,7 @@ interface Context {
}
export const renderParameterTemplates = (
logger: Logger,
params: ExecutorParams,
variables: Record<string, unknown>
) => {

View file

@ -344,7 +344,11 @@ describe('execute()', () => {
const variables = {
rogue: '*bold*',
};
const params = connectorType.renderParameterTemplates!(paramsWithTemplates, variables);
const params = connectorType.renderParameterTemplates!(
mockedLogger,
paramsWithTemplates,
variables
);
expect(params.message).toBe('`*bold*`');
});
});

View file

@ -9,6 +9,7 @@ import { URL } from 'url';
import HttpProxyAgent from 'http-proxy-agent';
import { HttpsProxyAgent } from 'https-proxy-agent';
import { i18n } from '@kbn/i18n';
import { Logger } from '@kbn/core/server';
import { schema, TypeOf } from '@kbn/config-schema';
import { IncomingWebhook, IncomingWebhookResult } from '@slack/webhook';
import { pipe } from 'fp-ts/lib/pipeable';
@ -94,11 +95,12 @@ export function getConnectorType({
}
function renderParameterTemplates(
logger: Logger,
params: ActionParamsType,
variables: Record<string, unknown>
): ActionParamsType {
return {
message: renderMustacheString(params.message, variables, 'slack'),
message: renderMustacheString(logger, params.message, variables, 'slack'),
};
}

View file

@ -240,6 +240,7 @@ describe('execute', () => {
};
const variables = { injected: '*foo*' };
const params = connectorType.renderParameterTemplates!(
mockedLogger,
paramsWithTemplates,
variables
) as PostMessageParams;
@ -266,6 +267,7 @@ describe('execute', () => {
};
const variables = { name: '"Dwight"' };
const params = connectorType.renderParameterTemplates!(
mockedLogger,
paramsWithTemplates,
variables
) as PostMessageParams;

View file

@ -10,6 +10,7 @@ import {
AlertingConnectorFeatureId,
SecurityConnectorFeatureId,
} from '@kbn/actions-plugin/common/types';
import { Logger } from '@kbn/core/server';
import { renderMustacheString } from '@kbn/actions-plugin/server/lib/mustache_renderer';
import type { ValidatorServices } from '@kbn/actions-plugin/server/types';
import { i18n } from '@kbn/i18n';
@ -69,13 +70,17 @@ const validateSlackUrl = (secretsObject: SlackApiSecrets, validatorServices: Val
}
};
const renderParameterTemplates = (params: SlackApiParams, variables: Record<string, unknown>) => {
const renderParameterTemplates = (
logger: Logger,
params: SlackApiParams,
variables: Record<string, unknown>
) => {
if (params.subAction === 'postMessage') {
return {
subAction: params.subAction,
subActionParams: {
...params.subActionParams,
text: renderMustacheString(params.subActionParams.text, variables, 'slack'),
text: renderMustacheString(logger, params.subActionParams.text, variables, 'slack'),
},
};
} else if (params.subAction === 'postBlockkit') {
@ -83,7 +88,7 @@ const renderParameterTemplates = (params: SlackApiParams, variables: Record<stri
subAction: params.subAction,
subActionParams: {
...params.subActionParams,
text: renderMustacheString(params.subActionParams.text, variables, 'json'),
text: renderMustacheString(logger, params.subActionParams.text, variables, 'json'),
},
};
}

View file

@ -6,6 +6,7 @@
*/
import { set } from '@kbn/safer-lodash-set/fp';
import { loggingSystemMock } from '@kbn/core/server/mocks';
import { renderParameterTemplates } from './render';
const params = {
@ -15,6 +16,7 @@ const params = {
},
};
const logger = loggingSystemMock.createLogger();
const alert1Expected = {
_id: 'l8jb2e55f2740ab15ba4e2717a96c93396c4ce323ac7a486c7dc179d67rg3ft',
_index: '.internal.alerts-security.alerts-default-000001',
@ -59,12 +61,12 @@ describe('Tines body render', () => {
describe('renderParameterTemplates', () => {
it('should not render body on test action', () => {
const testParams = { subAction: 'test', subActionParams: { body: 'test_json' } };
const result = renderParameterTemplates(testParams, variables);
const result = renderParameterTemplates(logger, testParams, variables);
expect(result).toEqual(testParams);
});
it('should rendered body from variables with cleaned alerts on run action', () => {
const result = renderParameterTemplates(params, variables);
const result = renderParameterTemplates(logger, params, variables);
expect(result.subActionParams.body).toEqual(
JSON.stringify({
@ -79,14 +81,14 @@ describe('Tines body render', () => {
it('should rendered body from variables on run action without context.alerts', () => {
const variablesWithoutAlerts = set('context.alerts', undefined, variables);
const result = renderParameterTemplates(params, variablesWithoutAlerts);
const result = renderParameterTemplates(logger, params, variablesWithoutAlerts);
expect(result.subActionParams.body).toEqual(JSON.stringify(variablesWithoutAlerts));
});
it('should rendered body from variables on run action without context', () => {
const variablesWithoutContext = set('context', undefined, variables);
const result = renderParameterTemplates(params, variablesWithoutContext);
const result = renderParameterTemplates(logger, params, variablesWithoutContext);
expect(result.subActionParams.body).toEqual(JSON.stringify(variablesWithoutContext));
});
@ -96,7 +98,7 @@ describe('Tines body render', () => {
jest.spyOn(JSON, 'stringify').mockImplementationOnce(() => {
throw new Error(errorMessage);
});
const result = renderParameterTemplates(params, variables);
const result = renderParameterTemplates(logger, params, variables);
expect(result.subActionParams.body).toEqual(
JSON.stringify({ error: { message: errorMessage } })
);

View file

@ -15,6 +15,7 @@ interface Context {
}
export const renderParameterTemplates: RenderParameterTemplates<ExecutorParams> = (
logger,
params,
variables
) => {

View file

@ -227,7 +227,11 @@ describe('execute Torq action', () => {
scalar: '1970',
scalar_with_json_chars: 'noinjection", "here": "',
};
const params = actionType.renderParameterTemplates!(paramsWithTemplates, variables);
const params = actionType.renderParameterTemplates!(
mockedLogger,
paramsWithTemplates,
variables
);
expect(params.body).toBe(
`{"x": ${templatedObject}, "y": "${variables.scalar}", "z": "${variables.scalar_with_json_chars}"}`
);

View file

@ -88,11 +88,12 @@ export function getActionType(): TorqActionType {
}
function renderParameterTemplates(
logger: Logger,
params: ActionParamsType,
variables: Record<string, unknown>
): ActionParamsType {
if (!params.body) return params;
return renderMustacheObject(params, variables);
return renderMustacheObject(logger, params, variables);
}
function validateActionTypeConfig(

View file

@ -676,7 +676,11 @@ describe('execute()', () => {
const variables = {
rogue,
};
const params = connectorType.renderParameterTemplates!(paramsWithTemplates, variables);
const params = connectorType.renderParameterTemplates!(
mockedLogger,
paramsWithTemplates,
variables
);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let paramsObject: any;

View file

@ -9,6 +9,7 @@ import { i18n } from '@kbn/i18n';
import { isString } from 'lodash';
import axios, { AxiosError, AxiosResponse } from 'axios';
import { schema, TypeOf } from '@kbn/config-schema';
import { Logger } from '@kbn/core/server';
import { pipe } from 'fp-ts/lib/pipeable';
import { map, getOrElse } from 'fp-ts/lib/Option';
import type {
@ -139,12 +140,13 @@ export function getConnectorType(): WebhookConnectorType {
}
function renderParameterTemplates(
logger: Logger,
params: ActionParamsType,
variables: Record<string, unknown>
): ActionParamsType {
if (!params.body) return params;
return {
body: renderMustacheString(params.body, variables, 'json'),
body: renderMustacheString(logger, params.body, variables, 'json'),
};
}