From 2438c36fd9bfabad9b09775d031c8cdd3d43d105 Mon Sep 17 00:00:00 2001 From: Drew Tate Date: Mon, 22 Jul 2024 08:06:27 -0600 Subject: [PATCH 01/54] [ES|QL] improve `SORT` command suggestions (#188579) ## Summary - Suggests options in uppercase - Applies syntax highlighting **Before** https://github.com/user-attachments/assets/5f04d8fc-d61a-4779-906b-a7f4f42b4014 **After** https://github.com/user-attachments/assets/cd585306-020a-4a55-867a-affe373666f6 --------- Co-authored-by: Stratoula Kalafateli --- .../src/autocomplete/autocomplete.test.ts | 4 ++-- .../src/definitions/commands.ts | 4 ++-- .../kbn-monaco/src/esql/lib/esql_theme.ts | 3 +-- .../src/esql/lib/esql_token_helpers.ts | 19 ++++++++++++++++--- .../src/esql/lib/esql_tokens_provider.ts | 5 +++-- 5 files changed, 24 insertions(+), 11 deletions(-) diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.test.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.test.ts index 687684f6fcf346..c417562499d813 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.test.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.test.ts @@ -329,8 +329,8 @@ describe('autocomplete', () => { ...getFieldNamesByType('any'), ...getFunctionSignaturesByReturnType('sort', 'any', { evalMath: true }), ]); - testSuggestions('from a | sort stringField ', ['asc', 'desc', ',', '|']); - testSuggestions('from a | sort stringField desc ', ['nulls first', 'nulls last', ',', '|']); + testSuggestions('from a | sort stringField ', ['ASC', 'DESC', ',', '|']); + testSuggestions('from a | sort stringField desc ', ['NULLS FIRST', 'NULLS LAST', ',', '|']); // @TODO: improve here // testSuggestions('from a | sort stringField desc ', ['first', 'last']); }); diff --git a/packages/kbn-esql-validation-autocomplete/src/definitions/commands.ts b/packages/kbn-esql-validation-autocomplete/src/definitions/commands.ts index 2485b32837a5bf..9bbc8a5b903d2a 100644 --- a/packages/kbn-esql-validation-autocomplete/src/definitions/commands.ts +++ b/packages/kbn-esql-validation-autocomplete/src/definitions/commands.ts @@ -372,8 +372,8 @@ export const commandDefinitions: CommandDefinition[] = [ multipleParams: true, params: [ { name: 'expression', type: 'any' }, - { name: 'direction', type: 'string', optional: true, values: ['asc', 'desc'] }, - { name: 'nulls', type: 'string', optional: true, values: ['nulls first', 'nulls last'] }, + { name: 'direction', type: 'string', optional: true, values: ['ASC', 'DESC'] }, + { name: 'nulls', type: 'string', optional: true, values: ['NULLS FIRST', 'NULLS LAST'] }, ], }, }, diff --git a/packages/kbn-monaco/src/esql/lib/esql_theme.ts b/packages/kbn-monaco/src/esql/lib/esql_theme.ts index a6907847c7ade6..511fcbf9114f40 100644 --- a/packages/kbn-monaco/src/esql/lib/esql_theme.ts +++ b/packages/kbn-monaco/src/esql/lib/esql_theme.ts @@ -78,14 +78,13 @@ export const buildESQlTheme = (): monaco.editor.IStandaloneThemeData => ({ 'as', 'expr_ws', 'limit', - 'nulls_ordering_direction', - 'nulls_ordering', 'null', 'enrich', 'on', 'with', 'asc', 'desc', + 'nulls_order', ], euiThemeVars.euiColorAccentText, true // isBold diff --git a/packages/kbn-monaco/src/esql/lib/esql_token_helpers.ts b/packages/kbn-monaco/src/esql/lib/esql_token_helpers.ts index e77b9ccfe6e40f..a43360f48e9c9b 100644 --- a/packages/kbn-monaco/src/esql/lib/esql_token_helpers.ts +++ b/packages/kbn-monaco/src/esql/lib/esql_token_helpers.ts @@ -13,9 +13,7 @@ function nonNullable(value: T | undefined): value is T { return value != null; } -export function enrichTokensWithFunctionsMetadata( - tokens: monaco.languages.IToken[] -): monaco.languages.IToken[] { +export function addFunctionTokens(tokens: monaco.languages.IToken[]): monaco.languages.IToken[] { // need to trim spaces as "abs (arg)" is still valid as function const myTokensWithoutSpaces = tokens.filter( ({ scopes }) => scopes !== 'expr_ws' + ESQL_TOKEN_POSTFIX @@ -34,3 +32,18 @@ export function enrichTokensWithFunctionsMetadata( } return [...tokens]; } + +export function addNullsOrder(tokens: monaco.languages.IToken[]): void { + const nullsIndex = tokens.findIndex((token) => token.scopes === 'nulls' + ESQL_TOKEN_POSTFIX); + if ( + // did we find a "nulls"? + nullsIndex > -1 && + // is the next non-whitespace token an order? + ['first' + ESQL_TOKEN_POSTFIX, 'last' + ESQL_TOKEN_POSTFIX].includes( + tokens[nullsIndex + 2]?.scopes + ) + ) { + tokens[nullsIndex].scopes = 'nulls_order' + ESQL_TOKEN_POSTFIX; + tokens.splice(nullsIndex + 1, 2); + } +} diff --git a/packages/kbn-monaco/src/esql/lib/esql_tokens_provider.ts b/packages/kbn-monaco/src/esql/lib/esql_tokens_provider.ts index 378e86cbfb27d9..d5cbdf4349b4c6 100644 --- a/packages/kbn-monaco/src/esql/lib/esql_tokens_provider.ts +++ b/packages/kbn-monaco/src/esql/lib/esql_tokens_provider.ts @@ -15,7 +15,7 @@ import { ESQLLineTokens } from './esql_line_tokens'; import { ESQLState } from './esql_state'; import { ESQL_TOKEN_POSTFIX } from './constants'; -import { enrichTokensWithFunctionsMetadata } from './esql_token_helpers'; +import { addFunctionTokens, addNullsOrder } from './esql_token_helpers'; const EOF = -1; @@ -77,7 +77,8 @@ export class ESQLTokensProvider implements monaco.languages.TokensProvider { // special treatment for functions // the previous custom Kibana grammar baked functions directly as tokens, so highlight was easier // The ES grammar doesn't have the token concept of "function" - const tokensWithFunctions = enrichTokensWithFunctionsMetadata(myTokens); + const tokensWithFunctions = addFunctionTokens(myTokens); + addNullsOrder(tokensWithFunctions); return new ESQLLineTokens(tokensWithFunctions, prevState.getLineNumber() + 1); } From 76c6f550dc5c945f1410bb53d54923c7363c7660 Mon Sep 17 00:00:00 2001 From: Drew Tate Date: Mon, 22 Jul 2024 08:25:30 -0600 Subject: [PATCH 02/54] [ES|QL] distinguish between trigger kinds in tests (#188604) ## Summary Part of https://github.com/elastic/kibana/issues/188677 Monaco editor has different [kinds of completion triggers](https://microsoft.github.io/monaco-editor/typedoc/enums/languages.CompletionTriggerKind.html). However, the current tests only validate the "TriggerCharacter" events. This PR prepares the tests to support validating "Invoke" as well. **Note:** It does change many of the tests from a "TriggerCharacter" to an "Invoke" scenario. I think this is okay because - there are still plenty of "TriggerCharacter" tests - it would take a lot of work to update all the tests - I will be adding a full set of tests to cover both scenarios as part of https://github.com/elastic/kibana/issues/188677 - We may rely less and less on trigger characters in the future --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Stratoula Kalafateli --- .../src/autocomplete/__tests__/helpers.ts | 7 +- .../src/autocomplete/autocomplete.test.ts | 316 ++++++++++-------- 2 files changed, 182 insertions(+), 141 deletions(-) diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/helpers.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/helpers.ts index 657b5de67896ed..2ceb7ae2cd45a1 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/helpers.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/helpers.ts @@ -247,15 +247,12 @@ export function createCustomCallbackMocks( }; } -export function createSuggestContext(text: string, triggerCharacter?: string) { +export function createCompletionContext(triggerCharacter?: string) { if (triggerCharacter) { return { triggerCharacter, triggerKind: 1 }; // any number is fine here } - const foundTriggerCharIndexes = triggerCharacters.map((char) => text.lastIndexOf(char)); - const maxIndex = Math.max(...foundTriggerCharIndexes); return { - triggerCharacter: text[maxIndex], - triggerKind: 1, + triggerKind: 0, }; } diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.test.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.test.ts index c417562499d813..26e0159e701028 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.test.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.test.ts @@ -23,7 +23,7 @@ import { getLiteralsByType, getDateLiteralsByFieldType, createCustomCallbackMocks, - createSuggestContext, + createCompletionContext, getPolicyFields, } from './__tests__/helpers'; @@ -31,31 +31,30 @@ describe('autocomplete', () => { type TestArgs = [ string, string[], - (string | number)?, + string?, + number?, Parameters? ]; - const testSuggestionsFn = ( + const _testSuggestionsFn = ( + { only, skip }: { only?: boolean; skip?: boolean } = {}, statement: string, expected: string[], - triggerCharacter: string | number = '', + triggerCharacter?: string, + _offset?: number, customCallbacksArgs: Parameters = [ undefined, undefined, undefined, - ], - { only, skip }: { only?: boolean; skip?: boolean } = {} + ] ) => { - const triggerCharacterString = - triggerCharacter == null || typeof triggerCharacter === 'string' - ? triggerCharacter - : statement[triggerCharacter + 1]; - const context = createSuggestContext(statement, triggerCharacterString); - const offset = - typeof triggerCharacter === 'string' - ? statement.lastIndexOf(context.triggerCharacter) + 1 - : triggerCharacter; + const context = createCompletionContext(triggerCharacter); const testFn = only ? test.only : skip ? test.skip : test; + const offset = _offset + ? _offset + : triggerCharacter + ? statement.lastIndexOf(triggerCharacter) + 1 + : statement.length; testFn(statement, async () => { const callbackMocks = createCustomCallbackMocks(...customCallbacksArgs); @@ -79,24 +78,12 @@ describe('autocomplete', () => { // DO NOT CHANGE THE NAME OF THIS FUNCTION WITHOUT ALSO CHANGING // THE LINTER RULE IN packages/kbn-eslint-config/typescript.js // - const testSuggestions = Object.assign(testSuggestionsFn, { + const testSuggestions = Object.assign(_testSuggestionsFn.bind(null, {}), { skip: (...args: TestArgs) => { - const paddingArgs = ['', [undefined, undefined, undefined]].slice(args.length - 2); - return testSuggestionsFn( - ...((args.length > 1 ? [...args, ...paddingArgs] : args) as TestArgs), - { - skip: true, - } - ); + return _testSuggestionsFn({ skip: true }, ...args); }, only: (...args: TestArgs) => { - const paddingArgs = ['', [undefined, undefined, undefined]].slice(args.length - 2); - return testSuggestionsFn( - ...((args.length > 1 ? [...args, ...paddingArgs] : args) as TestArgs), - { - only: true, - } - ); + return _testSuggestionsFn({ only: true }, ...args); }, }); @@ -223,7 +210,8 @@ describe('autocomplete', () => { testSuggestions( 'from a | stats a=avg(numberField) | where numberField ', [], - '', + undefined, + undefined, // make the fields suggest aware of the previous STATS, leave the other callbacks untouched [[{ name: 'a', type: 'number' }], undefined, undefined] ); @@ -277,6 +265,7 @@ describe('autocomplete', () => { ), ...getFunctionSignaturesByReturnType('where', 'number', { evalMath: true }), ], + undefined, 54 // after the first suggestions ); testSuggestions( @@ -287,42 +276,53 @@ describe('autocomplete', () => { ), ...getFunctionSignaturesByReturnType('where', 'number', { evalMath: true }), ], + undefined, 58 // after the first suggestions ); }); - for (const command of ['grok', 'dissect']) { - describe(command, () => { - const constantPattern = command === 'grok' ? '"%{WORD:firstWord}"' : '"%{firstWord}"'; - const subExpressions = [ - '', - `${command} stringField |`, - `${command} stringField ${constantPattern} |`, - `dissect stringField ${constantPattern} append_separator = ":" |`, - ]; - if (command === 'grok') { - subExpressions.push(`dissect stringField ${constantPattern} |`); - } - for (const subExpression of subExpressions) { - testSuggestions(`from a | ${subExpression} ${command} `, getFieldNamesByType('string')); - testSuggestions(`from a | ${subExpression} ${command} stringField `, [constantPattern]); - testSuggestions( - `from a | ${subExpression} ${command} stringField ${constantPattern} `, - (command === 'dissect' ? ['APPEND_SEPARATOR = $0'] : []).concat(['|']) - ); - if (command === 'dissect') { - testSuggestions( - `from a | ${subExpression} ${command} stringField ${constantPattern} append_separator = `, - ['":"', '";"'] - ); - testSuggestions( - `from a | ${subExpression} ${command} stringField ${constantPattern} append_separator = ":" `, - ['|'] - ); - } - } - }); - } + describe('grok', () => { + const constantPattern = '"%{WORD:firstWord}"'; + const subExpressions = [ + '', + `grok stringField |`, + `grok stringField ${constantPattern} |`, + `dissect stringField ${constantPattern} append_separator = ":" |`, + `dissect stringField ${constantPattern} |`, + ]; + for (const subExpression of subExpressions) { + testSuggestions(`from a | ${subExpression} grok `, getFieldNamesByType('string')); + testSuggestions(`from a | ${subExpression} grok stringField `, [constantPattern], ' '); + testSuggestions(`from a | ${subExpression} grok stringField ${constantPattern} `, ['|']); + } + }); + + describe('dissect', () => { + const constantPattern = '"%{firstWord}"'; + const subExpressions = [ + '', + `dissect stringField |`, + `dissect stringField ${constantPattern} |`, + `dissect stringField ${constantPattern} append_separator = ":" |`, + ]; + for (const subExpression of subExpressions) { + testSuggestions(`from a | ${subExpression} dissect `, getFieldNamesByType('string')); + testSuggestions(`from a | ${subExpression} dissect stringField `, [constantPattern], ' '); + testSuggestions( + `from a | ${subExpression} dissect stringField ${constantPattern} `, + ['APPEND_SEPARATOR = $0', '|'], + ' ' + ); + testSuggestions( + `from a | ${subExpression} dissect stringField ${constantPattern} append_separator = `, + ['":"', '";"'] + ); + testSuggestions( + `from a | ${subExpression} dissect stringField ${constantPattern} append_separator = ":" `, + ['|'] + ); + } + }); describe('sort', () => { testSuggestions('from a | sort ', [ @@ -347,7 +347,7 @@ describe('autocomplete', () => { describe('rename', () => { testSuggestions('from a | rename ', getFieldNamesByType('any')); - testSuggestions('from a | rename stringField ', ['AS $0']); + testSuggestions('from a | rename stringField ', ['AS $0'], ' '); testSuggestions('from a | rename stringField as ', ['var0']); }); @@ -408,10 +408,11 @@ describe('autocomplete', () => { 'kubernetes.something.something', ]); testSuggestions(`from a ${prevCommand}| enrich policy on b `, ['WITH $0', ',', '|']); - testSuggestions(`from a ${prevCommand}| enrich policy on b with `, [ - 'var0 =', - ...getPolicyFields('policy'), - ]); + testSuggestions( + `from a ${prevCommand}| enrich policy on b with `, + ['var0 =', ...getPolicyFields('policy')], + ' ' + ); testSuggestions(`from a ${prevCommand}| enrich policy on b with var0 `, ['= $0', ',', '|']); testSuggestions(`from a ${prevCommand}| enrich policy on b with var0 = `, [ ...getPolicyFields('policy'), @@ -433,10 +434,11 @@ describe('autocomplete', () => { `from a ${prevCommand}| enrich policy on b with var0 = stringField, var1 = `, [...getPolicyFields('policy')] ); - testSuggestions(`from a ${prevCommand}| enrich policy with `, [ - 'var0 =', - ...getPolicyFields('policy'), - ]); + testSuggestions( + `from a ${prevCommand}| enrich policy with `, + ['var0 =', ...getPolicyFields('policy')], + ' ' + ); testSuggestions(`from a ${prevCommand}| enrich policy with stringField `, ['= $0', ',', '|']); } }); @@ -512,11 +514,13 @@ describe('autocomplete', () => { ); testSuggestions( 'from a | eval raund(5, ', // note the typo in round - [] + [], + ' ' ); testSuggestions( 'from a | eval var0 = raund(5, ', // note the typo in round - [] + [], + ' ' ); testSuggestions('from a | eval a=round(numberField) ', [ ',', @@ -525,18 +529,26 @@ describe('autocomplete', () => { 'number', ]), ]); - testSuggestions('from a | eval a=round(numberField, ', [ - ...getFieldNamesByType('number'), - ...getFunctionSignaturesByReturnType('eval', 'number', { evalMath: true }, undefined, [ - 'round', - ]), - ]); - testSuggestions('from a | eval round(numberField, ', [ - ...getFieldNamesByType('number'), - ...getFunctionSignaturesByReturnType('eval', 'number', { evalMath: true }, undefined, [ - 'round', - ]), - ]); + testSuggestions( + 'from a | eval a=round(numberField, ', + [ + ...getFieldNamesByType('number'), + ...getFunctionSignaturesByReturnType('eval', 'number', { evalMath: true }, undefined, [ + 'round', + ]), + ], + ' ' + ); + testSuggestions( + 'from a | eval round(numberField, ', + [ + ...getFieldNamesByType('number'), + ...getFunctionSignaturesByReturnType('eval', 'number', { evalMath: true }, undefined, [ + 'round', + ]), + ], + ' ' + ); testSuggestions('from a | eval a=round(numberField),', [ 'var0 =', ...getFieldNamesByType('any'), @@ -571,6 +583,7 @@ describe('autocomplete', () => { ...getFunctionSignaturesByReturnType('eval', 'any', { evalMath: true }), ], ' ', + undefined, // make aware EVAL of the previous STATS command [[], undefined, undefined] ); @@ -592,6 +605,7 @@ describe('autocomplete', () => { ...getFunctionSignaturesByReturnType('eval', 'any', { evalMath: true }), ], ' ', + undefined, // make aware EVAL of the previous STATS command with the buggy field name from expression [[{ name: 'avg_numberField_', type: 'number' }], undefined, undefined] ); @@ -604,6 +618,7 @@ describe('autocomplete', () => { ...getFunctionSignaturesByReturnType('eval', 'any', { evalMath: true }), ], ' ', + undefined, // make aware EVAL of the previous STATS command with the buggy field name from expression [ [ @@ -631,19 +646,27 @@ describe('autocomplete', () => { 'concat', ]).map((v) => `${v},`), ]); - testSuggestions('from a | eval a=concat(stringField, ', [ - ...getFieldNamesByType('string'), - ...getFunctionSignaturesByReturnType('eval', 'string', { evalMath: true }, undefined, [ - 'concat', - ]), - ]); + testSuggestions( + 'from a | eval a=concat(stringField, ', + [ + ...getFieldNamesByType('string'), + ...getFunctionSignaturesByReturnType('eval', 'string', { evalMath: true }, undefined, [ + 'concat', + ]), + ], + ' ' + ); // test that the arg type is correct after minParams - testSuggestions('from a | eval a=cidr_match(ipField, stringField,', [ - ...getFieldNamesByType('string'), - ...getFunctionSignaturesByReturnType('eval', 'string', { evalMath: true }, undefined, [ - 'cidr_match', - ]), - ]); + testSuggestions( + 'from a | eval a=cidr_match(ipField, stringField, ', + [ + ...getFieldNamesByType('string'), + ...getFunctionSignaturesByReturnType('eval', 'string', { evalMath: true }, undefined, [ + 'cidr_match', + ]), + ], + ' ' + ); // test that comma is correctly added to the suggestions if minParams is not reached yet testSuggestions('from a | eval a=cidr_match( ', [ ...getFieldNamesByType('ip').map((v) => `${v},`), @@ -651,12 +674,16 @@ describe('autocomplete', () => { 'cidr_match', ]).map((v) => `${v},`), ]); - testSuggestions('from a | eval a=cidr_match(ipField, ', [ - ...getFieldNamesByType('string'), - ...getFunctionSignaturesByReturnType('eval', 'string', { evalMath: true }, undefined, [ - 'cidr_match', - ]), - ]); + testSuggestions( + 'from a | eval a=cidr_match(ipField, ', + [ + ...getFieldNamesByType('string'), + ...getFunctionSignaturesByReturnType('eval', 'string', { evalMath: true }, undefined, [ + 'cidr_match', + ]), + ], + ' ' + ); // test deep function nesting suggestions (and check that the same function is not suggested) // round(round( // round(round(round( @@ -684,6 +711,7 @@ describe('autocomplete', () => { 'number', ]), ], + undefined, 38 /* " " after abs(b) */ ); testSuggestions( @@ -694,6 +722,7 @@ describe('autocomplete', () => { 'abs', ]), ], + undefined, 26 /* b column in abs */ ); @@ -747,7 +776,8 @@ describe('autocomplete', () => { ...getLiteralsByType(getTypesFromParamDefs(constantOnlyParamDefs)).map((d) => requiresMoreArgs ? `${d},` : d ), - ] + ], + ' ' ); testSuggestions( `from a | eval var0 = ${fn.name}(${Array(i).fill('field').join(', ')}${ @@ -772,7 +802,8 @@ describe('autocomplete', () => { ...getLiteralsByType(getTypesFromParamDefs(constantOnlyParamDefs)).map((d) => requiresMoreArgs ? `${d},` : d ), - ] + ], + ' ' ); } }); @@ -780,19 +811,23 @@ describe('autocomplete', () => { } } - testSuggestions('from a | eval var0 = bucket(@timestamp,', getUnitDuration(1)); + testSuggestions('from a | eval var0 = bucket(@timestamp, ', getUnitDuration(1), ' '); describe('date math', () => { const dateSuggestions = timeUnitsToSuggest.map(({ name }) => name); // If a literal number is detected then suggest also date period keywords - testSuggestions('from a | eval a = 1 ', [ - ...dateSuggestions, - ',', - '|', - ...getFunctionSignaturesByReturnType('eval', 'any', { builtin: true, skipAssign: true }, [ - 'number', - ]), - ]); + testSuggestions( + 'from a | eval a = 1 ', + [ + ...dateSuggestions, + ',', + '|', + ...getFunctionSignaturesByReturnType('eval', 'any', { builtin: true, skipAssign: true }, [ + 'number', + ]), + ], + ' ' + ); testSuggestions('from a | eval a = 1 year ', [ ',', '|', @@ -800,20 +835,28 @@ describe('autocomplete', () => { 'time_interval', ]), ]); - testSuggestions('from a | eval a = 1 day + 2 ', [ - ...dateSuggestions, - ',', - '|', - ...getFunctionSignaturesByReturnType('eval', 'any', { builtin: true, skipAssign: true }, [ - 'number', - ]), - ]); - testSuggestions('from a | eval 1 day + 2 ', [ - ...dateSuggestions, - ...getFunctionSignaturesByReturnType('eval', 'any', { builtin: true, skipAssign: true }, [ - 'number', - ]), - ]); + testSuggestions( + 'from a | eval a = 1 day + 2 ', + [ + ...dateSuggestions, + ',', + '|', + ...getFunctionSignaturesByReturnType('eval', 'any', { builtin: true, skipAssign: true }, [ + 'number', + ]), + ], + ' ' + ); + testSuggestions( + 'from a | eval 1 day + 2 ', + [ + ...dateSuggestions, + ...getFunctionSignaturesByReturnType('eval', 'any', { builtin: true, skipAssign: true }, [ + 'number', + ]), + ], + ' ' + ); testSuggestions( 'from a | eval var0=date_trunc()', [ @@ -826,10 +869,11 @@ describe('autocomplete', () => { ], '(' ); - testSuggestions('from a | eval var0=date_trunc(2 )', [ - ...dateSuggestions.map((t) => `${t},`), - ',', - ]); + testSuggestions( + 'from a | eval var0=date_trunc(2 )', + [...dateSuggestions.map((t) => `${t},`), ','], + ' ' + ); }); }); @@ -838,7 +882,7 @@ describe('autocomplete', () => { const callbackMocks = createCustomCallbackMocks(undefined, undefined, undefined); const statement = 'from a | drop stringField | eval var0 = abs(numberField) '; const triggerOffset = statement.lastIndexOf(' '); - const context = createSuggestContext(statement, statement[triggerOffset]); + const context = createCompletionContext(statement[triggerOffset]); await suggest( statement, triggerOffset + 1, @@ -854,7 +898,7 @@ describe('autocomplete', () => { const callbackMocks = createCustomCallbackMocks(undefined, undefined, undefined); const statement = 'from a | drop | eval var0 = abs(numberField) '; const triggerOffset = statement.lastIndexOf('p') + 1; // drop - const context = createSuggestContext(statement, statement[triggerOffset]); + const context = createCompletionContext(statement[triggerOffset]); await suggest( statement, triggerOffset + 1, @@ -870,7 +914,7 @@ describe('autocomplete', () => { function getSuggestionsFor(statement: string) { const callbackMocks = createCustomCallbackMocks(undefined, undefined, undefined); const triggerOffset = statement.lastIndexOf(' ') + 1; // drop - const context = createSuggestContext(statement, statement[triggerOffset]); + const context = createCompletionContext(statement[triggerOffset]); return suggest( statement, triggerOffset + 1, From b7b3260db2b150911f283351655a721d7f16e711 Mon Sep 17 00:00:00 2001 From: Marta Bondyra <4283304+mbondyra@users.noreply.github.com> Date: Mon, 22 Jul 2024 16:59:40 +0200 Subject: [PATCH 03/54] [Dashboard][ES|QL] Unable to load page error on edit/add ES|QL panel (#188664) ## Summary Fixes https://github.com/elastic/kibana/issues/184544 --- .../metric/dimension_editor.test.tsx | 33 +++++++++++-------- .../metric/dimension_editor.tsx | 2 +- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/x-pack/plugins/lens/public/visualizations/metric/dimension_editor.test.tsx b/x-pack/plugins/lens/public/visualizations/metric/dimension_editor.test.tsx index a239b12deb5beb..0b34d4453b2f10 100644 --- a/x-pack/plugins/lens/public/visualizations/metric/dimension_editor.test.tsx +++ b/x-pack/plugins/lens/public/visualizations/metric/dimension_editor.test.tsx @@ -249,9 +249,9 @@ describe('dimension editor', () => { userEvent.type(customPrefixTextbox, prefix); }; return { - settingNone: screen.getByTitle(/none/i), - settingAuto: screen.getByTitle(/auto/i), - settingCustom: screen.getByTitle(/custom/i), + settingNone: () => screen.getByTitle(/none/i), + settingAuto: () => screen.getByTitle(/auto/i), + settingCustom: () => screen.getByTitle(/custom/i), customPrefixTextbox, typePrefix, ...rtlRender, @@ -266,6 +266,11 @@ describe('dimension editor', () => { expect(screen.queryByTestId(SELECTORS.BREAKDOWN_EDITOR)).not.toBeInTheDocument(); }); + it(`doesn't break when layer data is missing`, () => { + renderSecondaryMetricEditor({ frame: { activeData: { first: undefined } } }); + expect(screen.getByTestId(SELECTORS.SECONDARY_METRIC_EDITOR)).toBeInTheDocument(); + }); + describe('metric prefix', () => { const NONE_PREFIX = ''; const AUTO_PREFIX = undefined; @@ -280,9 +285,9 @@ describe('dimension editor', () => { state: localState, }); - expect(settingAuto).toHaveAttribute('aria-pressed', 'true'); - expect(settingNone).toHaveAttribute('aria-pressed', 'false'); - expect(settingCustom).toHaveAttribute('aria-pressed', 'false'); + expect(settingAuto()).toHaveAttribute('aria-pressed', 'true'); + expect(settingNone()).toHaveAttribute('aria-pressed', 'false'); + expect(settingCustom()).toHaveAttribute('aria-pressed', 'false'); expect(customPrefixTextbox).not.toBeInTheDocument(); }); @@ -290,9 +295,9 @@ describe('dimension editor', () => { const { settingAuto, settingCustom, settingNone, customPrefixTextbox } = renderSecondaryMetricEditor({ state: { ...localState, secondaryPrefix: NONE_PREFIX } }); - expect(settingNone).toHaveAttribute('aria-pressed', 'true'); - expect(settingAuto).toHaveAttribute('aria-pressed', 'false'); - expect(settingCustom).toHaveAttribute('aria-pressed', 'false'); + expect(settingNone()).toHaveAttribute('aria-pressed', 'true'); + expect(settingAuto()).toHaveAttribute('aria-pressed', 'false'); + expect(settingCustom()).toHaveAttribute('aria-pressed', 'false'); expect(customPrefixTextbox).not.toBeInTheDocument(); }); @@ -301,9 +306,9 @@ describe('dimension editor', () => { const { settingAuto, settingCustom, settingNone, customPrefixTextbox } = renderSecondaryMetricEditor({ state: customPrefixState }); - expect(settingAuto).toHaveAttribute('aria-pressed', 'false'); - expect(settingNone).toHaveAttribute('aria-pressed', 'false'); - expect(settingCustom).toHaveAttribute('aria-pressed', 'true'); + expect(settingAuto()).toHaveAttribute('aria-pressed', 'false'); + expect(settingNone()).toHaveAttribute('aria-pressed', 'false'); + expect(settingCustom()).toHaveAttribute('aria-pressed', 'true'); expect(customPrefixTextbox).toHaveValue(customPrefixState.secondaryPrefix); }); @@ -316,12 +321,12 @@ describe('dimension editor', () => { state: { ...localState, secondaryPrefix: customPrefix }, }); - userEvent.click(settingNone); + userEvent.click(settingNone()); expect(setState).toHaveBeenCalledWith( expect.objectContaining({ secondaryPrefix: NONE_PREFIX }) ); - userEvent.click(settingAuto); + userEvent.click(settingAuto()); expect(setState).toHaveBeenCalledWith( expect.objectContaining({ secondaryPrefix: AUTO_PREFIX }) ); diff --git a/x-pack/plugins/lens/public/visualizations/metric/dimension_editor.tsx b/x-pack/plugins/lens/public/visualizations/metric/dimension_editor.tsx index f040c6dc86fa4a..24248621c0982e 100644 --- a/x-pack/plugins/lens/public/visualizations/metric/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/visualizations/metric/dimension_editor.tsx @@ -131,7 +131,7 @@ function MaximumEditor({ setState, state, idPrefix }: SubProps) { } function SecondaryMetricEditor({ accessor, idPrefix, frame, layerId, setState, state }: SubProps) { - const columnName = getColumnByAccessor(accessor, frame.activeData?.[layerId].columns)?.name; + const columnName = getColumnByAccessor(accessor, frame.activeData?.[layerId]?.columns)?.name; const defaultPrefix = columnName || ''; return ( From 240d988ce301cccecf3799263a3d5afe2cfe9038 Mon Sep 17 00:00:00 2001 From: Pablo Machado Date: Mon, 22 Jul 2024 17:06:33 +0200 Subject: [PATCH 04/54] [Observability][SecuritySolution] Update entity manager to support extension of mappings and ingest pipeline (#188410) ## Summary ### Acceptance Criteria - [x] When starting Kibana, the global entity index templates are no longer created - [x] When installing a definition, an index template is generated and installed scoped to the definition ID - [x] When deleting a definition, the related index template is also deleted - [x] The index template composes the current component templates (base, entity, event) as well as the new custom component templates with the setting ignore_missing_component_templates set to true - [x] The new component templates should be named: @platform, -history@platform, -latest@platform, @custom, -history@custom and -latest@custom - [x] The ingest pipelines include a pipeline processor that calls out the pipelines named @platform and -history@platform or -latest@platform, @custom and -history@custom or -latest@custom if they exist - [x] The index template should have a priority of 200 and be set to managed - [x] The @custom component template should take precedence over the @platform component template, allowing users to override things we have set if they so wish - [x] set managed_by to 'elastic_entity_model', ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: Kevin Lacabane Co-authored-by: Elastic Machine --- .../common/constants_entities.ts | 4 -- .../entity_manager/common/helpers.test.ts | 22 ++++++++++ .../entity_manager/common/helpers.ts | 19 +++++++++ .../server/lib/auth/privileges.ts | 8 +++- .../generate_history_processors.test.ts.snap | 24 +++++++++++ .../generate_latest_processors.test.ts.snap | 24 +++++++++++ .../generate_history_processors.ts | 25 ++++++++++++ .../generate_latest_processors.ts | 25 ++++++++++++ .../install_entity_definition.test.ts | 26 ++++++++++++ .../lib/entities/install_entity_definition.ts | 40 +++++++++++++++++++ .../entities/uninstall_entity_definition.ts | 8 ++++ .../server/lib/manage_index_templates.ts | 29 ++++++++++++-- .../entity_manager/server/plugin.ts | 21 +--------- .../templates/components/helpers.test.ts | 32 +++++++++++++++ .../server/templates/components/helpers.ts | 20 ++++++++++ .../templates/entities_history_template.ts | 16 +++++--- .../templates/entities_latest_template.ts | 14 +++++-- 17 files changed, 320 insertions(+), 37 deletions(-) create mode 100644 x-pack/plugins/observability_solution/entity_manager/common/helpers.test.ts create mode 100644 x-pack/plugins/observability_solution/entity_manager/common/helpers.ts create mode 100644 x-pack/plugins/observability_solution/entity_manager/server/templates/components/helpers.test.ts create mode 100644 x-pack/plugins/observability_solution/entity_manager/server/templates/components/helpers.ts diff --git a/x-pack/plugins/observability_solution/entity_manager/common/constants_entities.ts b/x-pack/plugins/observability_solution/entity_manager/common/constants_entities.ts index 28e9823c15620b..633dfa2f9fd293 100644 --- a/x-pack/plugins/observability_solution/entity_manager/common/constants_entities.ts +++ b/x-pack/plugins/observability_solution/entity_manager/common/constants_entities.ts @@ -18,8 +18,6 @@ export const ENTITY_EVENT_COMPONENT_TEMPLATE_V1 = // History constants export const ENTITY_HISTORY = 'history' as const; -export const ENTITY_HISTORY_INDEX_TEMPLATE_V1 = - `${ENTITY_BASE_PREFIX}_${ENTITY_SCHEMA_VERSION_V1}_${ENTITY_HISTORY}_index_template` as const; export const ENTITY_HISTORY_BASE_COMPONENT_TEMPLATE_V1 = `${ENTITY_BASE_PREFIX}_${ENTITY_SCHEMA_VERSION_V1}_${ENTITY_HISTORY}_base` as const; export const ENTITY_HISTORY_PREFIX_V1 = @@ -29,8 +27,6 @@ export const ENTITY_HISTORY_INDEX_PREFIX_V1 = // Latest constants export const ENTITY_LATEST = 'latest' as const; -export const ENTITY_LATEST_INDEX_TEMPLATE_V1 = - `${ENTITY_BASE_PREFIX}_${ENTITY_SCHEMA_VERSION_V1}_${ENTITY_LATEST}_index_template` as const; export const ENTITY_LATEST_BASE_COMPONENT_TEMPLATE_V1 = `${ENTITY_BASE_PREFIX}_${ENTITY_SCHEMA_VERSION_V1}_${ENTITY_LATEST}_base` as const; export const ENTITY_LATEST_PREFIX_V1 = diff --git a/x-pack/plugins/observability_solution/entity_manager/common/helpers.test.ts b/x-pack/plugins/observability_solution/entity_manager/common/helpers.test.ts new file mode 100644 index 00000000000000..50ce0caeba0a38 --- /dev/null +++ b/x-pack/plugins/observability_solution/entity_manager/common/helpers.test.ts @@ -0,0 +1,22 @@ +/* + * 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 { getEntityHistoryIndexTemplateV1, getEntityLatestIndexTemplateV1 } from './helpers'; + +describe('helpers', () => { + it('getEntityHistoryIndexTemplateV1 should return the correct value', () => { + const definitionId = 'test'; + const result = getEntityHistoryIndexTemplateV1(definitionId); + expect(result).toEqual('entities_v1_history_test_index_template'); + }); + + it('getEntityLatestIndexTemplateV1 should return the correct value', () => { + const definitionId = 'test'; + const result = getEntityLatestIndexTemplateV1(definitionId); + expect(result).toEqual('entities_v1_latest_test_index_template'); + }); +}); diff --git a/x-pack/plugins/observability_solution/entity_manager/common/helpers.ts b/x-pack/plugins/observability_solution/entity_manager/common/helpers.ts new file mode 100644 index 00000000000000..97a6317fee2832 --- /dev/null +++ b/x-pack/plugins/observability_solution/entity_manager/common/helpers.ts @@ -0,0 +1,19 @@ +/* + * 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 { + ENTITY_BASE_PREFIX, + ENTITY_HISTORY, + ENTITY_LATEST, + ENTITY_SCHEMA_VERSION_V1, +} from './constants_entities'; + +export const getEntityHistoryIndexTemplateV1 = (definitionId: string) => + `${ENTITY_BASE_PREFIX}_${ENTITY_SCHEMA_VERSION_V1}_${ENTITY_HISTORY}_${definitionId}_index_template` as const; + +export const getEntityLatestIndexTemplateV1 = (definitionId: string) => + `${ENTITY_BASE_PREFIX}_${ENTITY_SCHEMA_VERSION_V1}_${ENTITY_LATEST}_${definitionId}_index_template` as const; diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/auth/privileges.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/auth/privileges.ts index 00f09209fb3b68..3bc88127a59646 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/auth/privileges.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/auth/privileges.ts @@ -21,7 +21,13 @@ export const requiredRunTimePrivileges = { privileges: ['read', 'view_index_metadata'], }, ], - cluster: ['manage_transform', 'monitor_transform', 'manage_ingest_pipelines', 'monitor'], + cluster: [ + 'manage_transform', + 'monitor_transform', + 'manage_ingest_pipelines', + 'monitor', + 'manage_index_templates', + ], application: [ { application: 'kibana-.kibana', diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/__snapshots__/generate_history_processors.test.ts.snap b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/__snapshots__/generate_history_processors.test.ts.snap index 925c62d97710f9..9e62633a0a7d6c 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/__snapshots__/generate_history_processors.test.ts.snap +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/__snapshots__/generate_history_processors.test.ts.snap @@ -148,5 +148,29 @@ if (ctx.entity?.metadata?.sourceIndex != null) { "index_name_prefix": ".entities.v1.history.admin-console-services.", }, }, + Object { + "pipeline": Object { + "ignore_missing_pipeline": true, + "name": "admin-console-services@platform", + }, + }, + Object { + "pipeline": Object { + "ignore_missing_pipeline": true, + "name": "admin-console-services-history@platform", + }, + }, + Object { + "pipeline": Object { + "ignore_missing_pipeline": true, + "name": "admin-console-services@custom", + }, + }, + Object { + "pipeline": Object { + "ignore_missing_pipeline": true, + "name": "admin-console-services-history@custom", + }, + }, ] `; diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/__snapshots__/generate_latest_processors.test.ts.snap b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/__snapshots__/generate_latest_processors.test.ts.snap index 69e63abd0cb94c..e96d7366e7e04d 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/__snapshots__/generate_latest_processors.test.ts.snap +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/__snapshots__/generate_latest_processors.test.ts.snap @@ -108,5 +108,29 @@ ctx.event.category = ctx.entity.identity.event.category.keySet().toArray()[0];", "value": ".entities.v1.latest.admin-console-services", }, }, + Object { + "pipeline": Object { + "ignore_missing_pipeline": true, + "name": "admin-console-services@platform", + }, + }, + Object { + "pipeline": Object { + "ignore_missing_pipeline": true, + "name": "admin-console-services-latest@platform", + }, + }, + Object { + "pipeline": Object { + "ignore_missing_pipeline": true, + "name": "admin-console-services@custom", + }, + }, + Object { + "pipeline": Object { + "ignore_missing_pipeline": true, + "name": "admin-console-services-latest@custom", + }, + }, ] `; diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/generate_history_processors.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/generate_history_processors.ts index 45ee008f9c6b5f..43f18b2b81bf00 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/generate_history_processors.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/generate_history_processors.ts @@ -163,5 +163,30 @@ export function generateHistoryProcessors(definition: EntityDefinition) { date_formats: ['UNIX_MS', 'ISO8601', "yyyy-MM-dd'T'HH:mm:ss.SSSXX"], }, }, + { + pipeline: { + ignore_missing_pipeline: true, + name: `${definition.id}@platform`, + }, + }, + { + pipeline: { + ignore_missing_pipeline: true, + name: `${definition.id}-history@platform`, + }, + }, + + { + pipeline: { + ignore_missing_pipeline: true, + name: `${definition.id}@custom`, + }, + }, + { + pipeline: { + ignore_missing_pipeline: true, + name: `${definition.id}-history@custom`, + }, + }, ]; } diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/generate_latest_processors.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/generate_latest_processors.ts index 22b2ac19775a13..b9a18e8b7a2b68 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/generate_latest_processors.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/generate_latest_processors.ts @@ -122,5 +122,30 @@ export function generateLatestProcessors(definition: EntityDefinition) { value: `${generateLatestIndexName(definition)}`, }, }, + { + pipeline: { + ignore_missing_pipeline: true, + name: `${definition.id}@platform`, + }, + }, + { + pipeline: { + ignore_missing_pipeline: true, + name: `${definition.id}-latest@platform`, + }, + }, + { + pipeline: { + ignore_missing_pipeline: true, + name: `${definition.id}@custom`, + }, + }, + + { + pipeline: { + ignore_missing_pipeline: true, + name: `${definition.id}-latest@custom`, + }, + }, ]; } diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/install_entity_definition.test.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/install_entity_definition.test.ts index 8560f0a4f1f4f2..95eb63253f40cb 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/install_entity_definition.test.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/install_entity_definition.test.ts @@ -34,6 +34,18 @@ const assertHasCreatedDefinition = ( overwrite: true, }); + expect(esClient.indices.putIndexTemplate).toBeCalledTimes(2); + expect(esClient.indices.putIndexTemplate).toBeCalledWith( + expect.objectContaining({ + name: `entities_v1_history_${definition.id}_index_template`, + }) + ); + expect(esClient.indices.putIndexTemplate).toBeCalledWith( + expect.objectContaining({ + name: `entities_v1_latest_${definition.id}_index_template`, + }) + ); + expect(esClient.ingest.putPipeline).toBeCalledTimes(2); expect(esClient.ingest.putPipeline).toBeCalledWith({ id: generateHistoryIngestPipelineId(builtInServicesFromLogsEntityDefinition), @@ -111,6 +123,20 @@ const assertHasUninstalledDefinition = ( expect(esClient.transform.deleteTransform).toBeCalledTimes(2); expect(esClient.ingest.deletePipeline).toBeCalledTimes(2); expect(soClient.delete).toBeCalledTimes(1); + + expect(esClient.indices.deleteIndexTemplate).toBeCalledTimes(2); + expect(esClient.indices.deleteIndexTemplate).toBeCalledWith( + { + name: `entities_v1_history_${definition.id}_index_template`, + }, + { ignore: [404] } + ); + expect(esClient.indices.deleteIndexTemplate).toBeCalledWith( + { + name: `entities_v1_latest_${definition.id}_index_template`, + }, + { ignore: [404] } + ); }; describe('install_entity_definition', () => { diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/install_entity_definition.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/install_entity_definition.ts index 980c743575fe25..b47f17b6b00fa1 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/install_entity_definition.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/install_entity_definition.ts @@ -9,6 +9,10 @@ import { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; import { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server'; import { EntityDefinition } from '@kbn/entities-schema'; import { Logger } from '@kbn/logging'; +import { + getEntityHistoryIndexTemplateV1, + getEntityLatestIndexTemplateV1, +} from '../../../common/helpers'; import { createAndInstallHistoryIngestPipeline, createAndInstallLatestIngestPipeline, @@ -28,6 +32,9 @@ import { stopAndDeleteLatestTransform, } from './stop_and_delete_transform'; import { uninstallEntityDefinition } from './uninstall_entity_definition'; +import { deleteTemplate, upsertTemplate } from '../manage_index_templates'; +import { getEntitiesLatestIndexTemplateConfig } from '../../templates/entities_latest_template'; +import { getEntitiesHistoryIndexTemplateConfig } from '../../templates/entities_history_template'; export interface InstallDefinitionParams { esClient: ElasticsearchClient; @@ -52,6 +59,10 @@ export async function installEntityDefinition({ latest: false, }, definition: false, + indexTemplates: { + history: false, + latest: false, + }, }; try { @@ -62,6 +73,20 @@ export async function installEntityDefinition({ const entityDefinition = await saveEntityDefinition(soClient, definition); installState.definition = true; + // install scoped index template + await upsertTemplate({ + esClient, + logger, + template: getEntitiesHistoryIndexTemplateConfig(definition.id), + }); + installState.indexTemplates.history = true; + await upsertTemplate({ + esClient, + logger, + template: getEntitiesLatestIndexTemplateConfig(definition.id), + }); + installState.indexTemplates.latest = true; + // install ingest pipelines logger.debug(`Installing ingest pipelines for definition ${definition.id}`); await createAndInstallHistoryIngestPipeline(esClient, entityDefinition, logger); @@ -99,6 +124,21 @@ export async function installEntityDefinition({ await stopAndDeleteLatestTransform(esClient, definition, logger); } + if (installState.indexTemplates.history) { + await deleteTemplate({ + esClient, + logger, + name: getEntityHistoryIndexTemplateV1(definition.id), + }); + } + if (installState.indexTemplates.latest) { + await deleteTemplate({ + esClient, + logger, + name: getEntityLatestIndexTemplateV1(definition.id), + }); + } + throw e; } } diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/uninstall_entity_definition.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/uninstall_entity_definition.ts index 8642ebafa904b0..9b8685031642a0 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/uninstall_entity_definition.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/uninstall_entity_definition.ts @@ -9,6 +9,10 @@ import { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; import { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server'; import { EntityDefinition } from '@kbn/entities-schema'; import { Logger } from '@kbn/logging'; +import { + getEntityHistoryIndexTemplateV1, + getEntityLatestIndexTemplateV1, +} from '../../../common/helpers'; import { deleteEntityDefinition } from './delete_entity_definition'; import { deleteIndices } from './delete_index'; import { deleteHistoryIngestPipeline, deleteLatestIngestPipeline } from './delete_ingest_pipeline'; @@ -17,6 +21,7 @@ import { stopAndDeleteHistoryTransform, stopAndDeleteLatestTransform, } from './stop_and_delete_transform'; +import { deleteTemplate } from '../manage_index_templates'; export async function uninstallEntityDefinition({ definition, @@ -36,6 +41,9 @@ export async function uninstallEntityDefinition({ await deleteHistoryIngestPipeline(esClient, definition, logger); await deleteLatestIngestPipeline(esClient, definition, logger); await deleteEntityDefinition(soClient, definition, logger); + await deleteTemplate({ esClient, logger, name: getEntityHistoryIndexTemplateV1(definition.id) }); + await deleteTemplate({ esClient, logger, name: getEntityLatestIndexTemplateV1(definition.id) }); + if (deleteData) { await deleteIndices(esClient, definition, logger); } diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/manage_index_templates.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/manage_index_templates.ts index 0f73ba7715bfd9..f300df4a92c1d8 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/manage_index_templates.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/manage_index_templates.ts @@ -10,6 +10,7 @@ import { IndicesPutIndexTemplateRequest, } from '@elastic/elasticsearch/lib/api/types'; import { ElasticsearchClient, Logger } from '@kbn/core/server'; +import { retryTransientEsErrors } from './entities/helpers/retry'; interface TemplateManagementOptions { esClient: ElasticsearchClient; @@ -23,12 +24,18 @@ interface ComponentManagementOptions { logger: Logger; } +interface DeleteTemplateOptions { + esClient: ElasticsearchClient; + name: string; + logger: Logger; +} + export async function upsertTemplate({ esClient, template, logger }: TemplateManagementOptions) { try { - await esClient.indices.putIndexTemplate(template); + await retryTransientEsErrors(() => esClient.indices.putIndexTemplate(template), { logger }); } catch (error: any) { logger.error(`Error updating entity manager index template: ${error.message}`); - return; + throw error; } logger.info( @@ -37,12 +44,26 @@ export async function upsertTemplate({ esClient, template, logger }: TemplateMan logger.debug(() => `Entity manager index template: ${JSON.stringify(template)}`); } +export async function deleteTemplate({ esClient, name, logger }: DeleteTemplateOptions) { + try { + await retryTransientEsErrors( + () => esClient.indices.deleteIndexTemplate({ name }, { ignore: [404] }), + { logger } + ); + } catch (error: any) { + logger.error(`Error deleting entity manager index template: ${error.message}`); + throw error; + } +} + export async function upsertComponent({ esClient, component, logger }: ComponentManagementOptions) { try { - await esClient.cluster.putComponentTemplate(component); + await retryTransientEsErrors(() => esClient.cluster.putComponentTemplate(component), { + logger, + }); } catch (error: any) { logger.error(`Error updating entity manager component template: ${error.message}`); - return; + throw error; } logger.info( diff --git a/x-pack/plugins/observability_solution/entity_manager/server/plugin.ts b/x-pack/plugins/observability_solution/entity_manager/server/plugin.ts index 3a519888417666..80154149e24023 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/plugin.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/plugin.ts @@ -14,7 +14,7 @@ import { PluginConfigDescriptor, Logger, } from '@kbn/core/server'; -import { upsertComponent, upsertTemplate } from './lib/manage_index_templates'; +import { upsertComponent } from './lib/manage_index_templates'; import { setupRoutes } from './routes'; import { EntityManagerPluginSetupDependencies, @@ -27,8 +27,6 @@ import { entityDefinition, EntityDiscoveryApiKeyType } from './saved_objects'; import { entitiesEntityComponentTemplateConfig } from './templates/components/entity'; import { entitiesLatestBaseComponentTemplateConfig } from './templates/components/base_latest'; import { entitiesHistoryBaseComponentTemplateConfig } from './templates/components/base_history'; -import { entitiesHistoryIndexTemplateConfig } from './templates/entities_history_template'; -import { entitiesLatestIndexTemplateConfig } from './templates/entities_latest_template'; export type EntityManagerServerPluginSetup = ReturnType; export type EntityManagerServerPluginStart = ReturnType; @@ -113,22 +111,7 @@ export class EntityManagerServerPlugin logger: this.logger, component: entitiesEntityComponentTemplateConfig, }), - ]) - .then(() => - upsertTemplate({ - esClient, - logger: this.logger, - template: entitiesHistoryIndexTemplateConfig, - }) - ) - .then(() => - upsertTemplate({ - esClient, - logger: this.logger, - template: entitiesLatestIndexTemplateConfig, - }) - ) - .catch(() => {}); + ]).catch(() => {}); return {}; } diff --git a/x-pack/plugins/observability_solution/entity_manager/server/templates/components/helpers.test.ts b/x-pack/plugins/observability_solution/entity_manager/server/templates/components/helpers.test.ts new file mode 100644 index 00000000000000..3321ee39edeb42 --- /dev/null +++ b/x-pack/plugins/observability_solution/entity_manager/server/templates/components/helpers.test.ts @@ -0,0 +1,32 @@ +/* + * 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 { getCustomHistoryTemplateComponents, getCustomLatestTemplateComponents } from './helpers'; + +describe('helpers', () => { + it('getCustomLatestTemplateComponents should return template component in the right sort order', () => { + const definitionId = 'test'; + const result = getCustomLatestTemplateComponents(definitionId); + expect(result).toEqual([ + 'test@platform', + 'test-latest@platform', + 'test@custom', + 'test-latest@custom', + ]); + }); + + it('getCustomHistoryTemplateComponents should return template component in the right sort order', () => { + const definitionId = 'test'; + const result = getCustomHistoryTemplateComponents(definitionId); + expect(result).toEqual([ + 'test@platform', + 'test-history@platform', + 'test@custom', + 'test-history@custom', + ]); + }); +}); diff --git a/x-pack/plugins/observability_solution/entity_manager/server/templates/components/helpers.ts b/x-pack/plugins/observability_solution/entity_manager/server/templates/components/helpers.ts new file mode 100644 index 00000000000000..e976a216da97ba --- /dev/null +++ b/x-pack/plugins/observability_solution/entity_manager/server/templates/components/helpers.ts @@ -0,0 +1,20 @@ +/* + * 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. + */ + +export const getCustomLatestTemplateComponents = (definitionId: string) => [ + `${definitionId}@platform`, // @platform goes before so it can be overwritten by custom + `${definitionId}-latest@platform`, + `${definitionId}@custom`, + `${definitionId}-latest@custom`, +]; + +export const getCustomHistoryTemplateComponents = (definitionId: string) => [ + `${definitionId}@platform`, // @platform goes before so it can be overwritten by custom + `${definitionId}-history@platform`, + `${definitionId}@custom`, + `${definitionId}-history@custom`, +]; diff --git a/x-pack/plugins/observability_solution/entity_manager/server/templates/entities_history_template.ts b/x-pack/plugins/observability_solution/entity_manager/server/templates/entities_history_template.ts index d5ceeecd448289..63d589bfaa7549 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/templates/entities_history_template.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/templates/entities_history_template.ts @@ -6,29 +6,35 @@ */ import { IndicesPutIndexTemplateRequest } from '@elastic/elasticsearch/lib/api/types'; +import { getEntityHistoryIndexTemplateV1 } from '../../common/helpers'; import { ENTITY_ENTITY_COMPONENT_TEMPLATE_V1, ENTITY_EVENT_COMPONENT_TEMPLATE_V1, ENTITY_HISTORY_BASE_COMPONENT_TEMPLATE_V1, ENTITY_HISTORY_INDEX_PREFIX_V1, - ENTITY_HISTORY_INDEX_TEMPLATE_V1, } from '../../common/constants_entities'; +import { getCustomHistoryTemplateComponents } from './components/helpers'; -export const entitiesHistoryIndexTemplateConfig: IndicesPutIndexTemplateRequest = { - name: ENTITY_HISTORY_INDEX_TEMPLATE_V1, +export const getEntitiesHistoryIndexTemplateConfig = ( + definitionId: string +): IndicesPutIndexTemplateRequest => ({ + name: getEntityHistoryIndexTemplateV1(definitionId), _meta: { description: "Index template for indices managed by the Elastic Entity Model's entity discovery framework for the history dataset", ecs_version: '8.0.0', managed: true, + managed_by: 'elastic_entity_model', }, + ignore_missing_component_templates: getCustomHistoryTemplateComponents(definitionId), composed_of: [ ENTITY_HISTORY_BASE_COMPONENT_TEMPLATE_V1, ENTITY_ENTITY_COMPONENT_TEMPLATE_V1, ENTITY_EVENT_COMPONENT_TEMPLATE_V1, + ...getCustomHistoryTemplateComponents(definitionId), ], index_patterns: [`${ENTITY_HISTORY_INDEX_PREFIX_V1}.*`], - priority: 1, + priority: 200, template: { mappings: { _meta: { @@ -72,4 +78,4 @@ export const entitiesHistoryIndexTemplateConfig: IndicesPutIndexTemplateRequest }, }, }, -}; +}); diff --git a/x-pack/plugins/observability_solution/entity_manager/server/templates/entities_latest_template.ts b/x-pack/plugins/observability_solution/entity_manager/server/templates/entities_latest_template.ts index f601c3aa9d57d0..3ad09e7257a1ab 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/templates/entities_latest_template.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/templates/entities_latest_template.ts @@ -6,26 +6,32 @@ */ import { IndicesPutIndexTemplateRequest } from '@elastic/elasticsearch/lib/api/types'; +import { getEntityLatestIndexTemplateV1 } from '../../common/helpers'; import { ENTITY_ENTITY_COMPONENT_TEMPLATE_V1, ENTITY_EVENT_COMPONENT_TEMPLATE_V1, ENTITY_LATEST_BASE_COMPONENT_TEMPLATE_V1, ENTITY_LATEST_INDEX_PREFIX_V1, - ENTITY_LATEST_INDEX_TEMPLATE_V1, } from '../../common/constants_entities'; +import { getCustomLatestTemplateComponents } from './components/helpers'; -export const entitiesLatestIndexTemplateConfig: IndicesPutIndexTemplateRequest = { - name: ENTITY_LATEST_INDEX_TEMPLATE_V1, +export const getEntitiesLatestIndexTemplateConfig = ( + definitionId: string +): IndicesPutIndexTemplateRequest => ({ + name: getEntityLatestIndexTemplateV1(definitionId), _meta: { description: "Index template for indices managed by the Elastic Entity Model's entity discovery framework for the latest dataset", ecs_version: '8.0.0', managed: true, + managed_by: 'elastic_entity_model', }, + ignore_missing_component_templates: getCustomLatestTemplateComponents(definitionId), composed_of: [ ENTITY_LATEST_BASE_COMPONENT_TEMPLATE_V1, ENTITY_ENTITY_COMPONENT_TEMPLATE_V1, ENTITY_EVENT_COMPONENT_TEMPLATE_V1, + ...getCustomLatestTemplateComponents(definitionId), ], index_patterns: [`${ENTITY_LATEST_INDEX_PREFIX_V1}.*`], priority: 1, @@ -72,4 +78,4 @@ export const entitiesLatestIndexTemplateConfig: IndicesPutIndexTemplateRequest = }, }, }, -}; +}); From 1ac9c8e2dcfc95fdef19f67de7878c55fa1e8de7 Mon Sep 17 00:00:00 2001 From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> Date: Mon, 22 Jul 2024 11:34:28 -0400 Subject: [PATCH 05/54] [Security Solution][Endpoint] Fix authz on File Info/Download APIs for `execute` response action (#188698) ## Summary - Fixes the API route for response actions file information and file download to ensure that user only needs Authz to the Execute action. - Centralizes the logic to determine the platform for a given host which was (under certain data conditions) causing the platform icon to not be shown in the response console. --- .../use_alert_response_actions_support.ts | 13 +--- .../endpoint/utils/get_host_platform.test.ts | 52 ++++++++++++++ .../lib/endpoint/utils/get_host_platform.ts | 39 +++++++++++ .../endpoint/header_endpoint_info.tsx | 3 +- .../view/hooks/use_endpoint_action_items.tsx | 4 +- .../actions/file_download_handler.test.ts | 7 +- .../routes/actions/file_download_handler.ts | 2 +- .../routes/actions/file_info_handler.test.ts | 7 +- .../routes/actions/file_info_handler.ts | 2 +- .../endpoint/routes/with_endpoint_authz.ts | 37 +++++++++- ...rity_solution_edr_workflows_roles_users.ts | 23 +++++- .../trial_license_complete_tier/execute.ts | 70 ++++++++++++++++++- .../tsconfig.json | 1 + 13 files changed, 238 insertions(+), 22 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/lib/endpoint/utils/get_host_platform.test.ts create mode 100644 x-pack/plugins/security_solution/public/common/lib/endpoint/utils/get_host_platform.ts diff --git a/x-pack/plugins/security_solution/public/common/hooks/endpoint/use_alert_response_actions_support.ts b/x-pack/plugins/security_solution/public/common/hooks/endpoint/use_alert_response_actions_support.ts index e56c10d589f5fe..a483a5c465b3f2 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/endpoint/use_alert_response_actions_support.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/endpoint/use_alert_response_actions_support.ts @@ -9,6 +9,7 @@ import type { TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common'; import { useMemo } from 'react'; import { find, some } from 'lodash/fp'; import { i18n } from '@kbn/i18n'; +import { getHostPlatform } from '../../lib/endpoint/utils/get_host_platform'; import { getAlertDetailsFieldValue } from '../../lib/endpoint/utils/get_event_details_field_values'; import { isAgentTypeAndActionSupported } from '../../lib/endpoint'; import type { @@ -176,16 +177,8 @@ export const useAlertResponseActionsSupport = ( }, [eventData]); const platform = useMemo(() => { - // TODO:TC I couldn't find host.os.family in the example data, thus using host.os.type and host.os.platform which are present one at a time in different type of events - if (agentType === 'crowdstrike') { - return ( - getAlertDetailsFieldValue({ category: 'host', field: 'host.os.type' }, eventData) || - getAlertDetailsFieldValue({ category: 'host', field: 'host.os.platform' }, eventData) - ); - } - - return getAlertDetailsFieldValue({ category: 'host', field: 'host.os.type' }, eventData); - }, [agentType, eventData]); + return getHostPlatform(eventData ?? []); + }, [eventData]); const unsupportedReason = useMemo(() => { if (!doesHostSupportResponseActions) { diff --git a/x-pack/plugins/security_solution/public/common/lib/endpoint/utils/get_host_platform.test.ts b/x-pack/plugins/security_solution/public/common/lib/endpoint/utils/get_host_platform.test.ts new file mode 100644 index 00000000000000..61cc2053eb8fcb --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/lib/endpoint/utils/get_host_platform.test.ts @@ -0,0 +1,52 @@ +/* + * 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 { set } from 'lodash'; +import { getHostPlatform } from './get_host_platform'; +import type { TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common'; + +describe('getHostPlatform() util', () => { + const buildEcsData = (data: Record) => { + const ecsData = {}; + + for (const [key, value] of Object.entries(data)) { + set(ecsData, `host.os.${key}`, value); + } + + return ecsData; + }; + + const buildEventDetails = (data: Record) => { + const eventDetails: TimelineEventsDetailsItem[] = []; + + for (const [key, value] of Object.entries(data)) { + eventDetails.push({ + category: 'host', + field: `host.os.${key}`, + values: [value], + originalValue: value, + isObjectArray: false, + }); + } + + return eventDetails; + }; + + it.each` + title | setupData | expectedResult + ${'ECS data with host.os.platform info'} | ${buildEcsData({ platform: 'windows' })} | ${'windows'} + ${'ECS data with host.os.type info'} | ${buildEcsData({ type: 'Linux' })} | ${'linux'} + ${'ECS data with host.os.name info'} | ${buildEcsData({ name: 'MACOS' })} | ${'macos'} + ${'ECS data with all os info'} | ${buildEcsData({ platform: 'macos', type: 'windows', name: 'linux' })} | ${'macos'} + ${'Event Details data with host.os.platform info'} | ${buildEventDetails({ platform: 'windows' })} | ${'windows'} + ${'Event Details data with host.os.type info'} | ${buildEventDetails({ type: 'Linux' })} | ${'linux'} + ${'Event Details data with host.os.name info'} | ${buildEventDetails({ name: 'MACOS' })} | ${'macos'} + ${'Event Details data with all os info'} | ${buildEventDetails({ platform: 'macos', type: 'windows', name: 'linux' })} | ${'macos'} + `(`should handle $title`, ({ setupData, expectedResult }) => { + expect(getHostPlatform(setupData)).toEqual(expectedResult); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/lib/endpoint/utils/get_host_platform.ts b/x-pack/plugins/security_solution/public/common/lib/endpoint/utils/get_host_platform.ts new file mode 100644 index 00000000000000..52df785cabff04 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/lib/endpoint/utils/get_host_platform.ts @@ -0,0 +1,39 @@ +/* + * 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 type { Ecs } from '@elastic/ecs'; +import type { TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common'; +import type { MaybeImmutable } from '../../../../../common/endpoint/types'; +import { getAlertDetailsFieldValue } from './get_event_details_field_values'; +import type { Platform } from '../../../../management/components/endpoint_responder/components/header_info/platforms'; + +type EcsHostData = MaybeImmutable>; + +const isTimelineEventDetailsItems = ( + data: EcsHostData | TimelineEventsDetailsItem[] +): data is TimelineEventsDetailsItem[] => { + return Array.isArray(data); +}; + +/** + * Retrieve a host's platform type from either ECS data or Event Details list of items + * @param data + */ +export const getHostPlatform = (data: EcsHostData | TimelineEventsDetailsItem[]): Platform => { + let platform = ''; + + if (isTimelineEventDetailsItems(data)) { + platform = (getAlertDetailsFieldValue({ category: 'host', field: 'host.os.platform' }, data) || + getAlertDetailsFieldValue({ category: 'host', field: 'host.os.type' }, data) || + getAlertDetailsFieldValue({ category: 'host', field: 'host.os.name' }, data)) as Platform; + } else { + platform = + ((data.host?.os?.platform || data.host?.os?.type || data.host?.os?.name) as Platform) || ''; + } + + return platform.toLowerCase() as Platform; +}; diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/components/header_info/endpoint/header_endpoint_info.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/components/header_info/endpoint/header_endpoint_info.tsx index f302a31c5f48ef..0cd96b4f3acf08 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/components/header_info/endpoint/header_endpoint_info.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/components/header_info/endpoint/header_endpoint_info.tsx @@ -7,6 +7,7 @@ import React, { memo } from 'react'; import { EuiSkeletonText } from '@elastic/eui'; +import { getHostPlatform } from '../../../../../../common/lib/endpoint/utils/get_host_platform'; import { AgentStatus } from '../../../../../../common/components/endpoint/agents/agent_status'; import { HeaderAgentInfo } from '../header_agent_info'; import { useGetEndpointDetails } from '../../../../../hooks'; @@ -31,7 +32,7 @@ export const HeaderEndpointInfo = memo(({ endpointId }) return ( { it('should error if user has no authz to api', async () => { ( (await httpHandlerContextMock.securitySolution).getEndpointAuthz as jest.Mock - ).mockResolvedValue(getEndpointAuthzInitialStateMock({ canWriteFileOperations: false })); + ).mockResolvedValue( + getEndpointAuthzInitialStateMock({ + canWriteFileOperations: false, + canWriteExecuteOperations: false, + }) + ); await apiTestSetup .getRegisteredVersionedRoute('get', ACTION_AGENT_FILE_DOWNLOAD_ROUTE, '2023-10-31') diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/file_download_handler.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/file_download_handler.ts index 0037d5dded81f0..7095b7d87a50c8 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/file_download_handler.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/file_download_handler.ts @@ -47,7 +47,7 @@ export const registerActionFileDownloadRoutes = ( }, }, withEndpointAuthz( - { all: ['canWriteFileOperations'] }, + { any: ['canWriteFileOperations', 'canWriteExecuteOperations'] }, logger, getActionFileDownloadRouteHandler(endpointContext) ) diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/file_info_handler.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/file_info_handler.test.ts index e6554ee14ad6d8..e9914dc4232d97 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/file_info_handler.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/file_info_handler.test.ts @@ -69,7 +69,12 @@ describe('Response Action file info API', () => { it('should error if user has no authz to api', async () => { ( (await httpHandlerContextMock.securitySolution).getEndpointAuthz as jest.Mock - ).mockResolvedValue(getEndpointAuthzInitialStateMock({ canWriteFileOperations: false })); + ).mockResolvedValue( + getEndpointAuthzInitialStateMock({ + canWriteFileOperations: false, + canWriteExecuteOperations: false, + }) + ); await apiTestSetup .getRegisteredVersionedRoute('get', ACTION_AGENT_FILE_INFO_ROUTE, '2023-10-31') diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/file_info_handler.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/file_info_handler.ts index abc576fe3c9d94..a84f3b3a8bf6f5 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/file_info_handler.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/file_info_handler.ts @@ -83,7 +83,7 @@ export const registerActionFileInfoRoute = ( }, }, withEndpointAuthz( - { all: ['canWriteFileOperations'] }, + { any: ['canWriteFileOperations', 'canWriteExecuteOperations'] }, endpointContext.logFactory.get('actionFileInfo'), getActionFileInfoRouteHandler(endpointContext) ) diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/with_endpoint_authz.ts b/x-pack/plugins/security_solution/server/endpoint/routes/with_endpoint_authz.ts index 8822db6c68367e..a241148c7b714b 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/with_endpoint_authz.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/with_endpoint_authz.ts @@ -6,6 +6,7 @@ */ import type { RequestHandler, Logger } from '@kbn/core/server'; +import { stringify } from '../utils/stringify'; import type { EndpointAuthzKeyList } from '../../../common/endpoint/types/authz'; import type { SecuritySolutionRequestHandlerContext } from '../../types'; import { EndpointAuthorizationError } from '../errors'; @@ -39,6 +40,21 @@ export const withEndpointAuthz = ( const validateAll = needAll.length > 0; const validateAny = needAny.length > 0; const enforceAuthz = validateAll || validateAny; + const logAuthzFailure = ( + user: string, + authzValidationResults: Record, + needed: string[] + ) => { + logger.debug( + `Unauthorized: user ${user} ${ + needed === needAll ? 'needs ALL' : 'needs at least one' + } of the following privileges:\n${stringify(needed)}\nbut is missing: ${stringify( + Object.entries(authzValidationResults) + .filter(([_, value]) => !value) + .map(([key]) => key) + )}` + ); + }; if (!enforceAuthz) { logger.warn(`Authorization disabled for API route: ${new Error('').stack ?? '?'}`); @@ -51,18 +67,37 @@ export const withEndpointAuthz = ( SecuritySolutionRequestHandlerContext > = async (context, request, response) => { if (enforceAuthz) { + const coreServices = await context.core; const endpointAuthz = await (await context.securitySolution).getEndpointAuthz(); - const permissionChecker = (permission: EndpointAuthzKeyList[0]) => endpointAuthz[permission]; + let authzValidationResults: Record = {}; + const permissionChecker = (permission: EndpointAuthzKeyList[0]) => { + authzValidationResults[permission] = endpointAuthz[permission]; + return endpointAuthz[permission]; + }; // has `all`? if (validateAll && !needAll.every(permissionChecker)) { + logAuthzFailure( + coreServices.security.authc.getCurrentUser()?.username ?? '', + authzValidationResults, + needAll + ); + return response.forbidden({ body: new EndpointAuthorizationError({ need_all: [...needAll] }), }); } + authzValidationResults = {}; + // has `any`? if (validateAny && !needAny.some(permissionChecker)) { + logAuthzFailure( + coreServices.security.authc.getCurrentUser()?.username ?? '', + authzValidationResults, + needAny + ); + return response.forbidden({ body: new EndpointAuthorizationError({ need_any: [...needAny] }), }); diff --git a/x-pack/test/security_solution_api_integration/config/services/security_solution_edr_workflows_roles_users.ts b/x-pack/test/security_solution_api_integration/config/services/security_solution_edr_workflows_roles_users.ts index f364943164322c..92e0cc9ba1f138 100644 --- a/x-pack/test/security_solution_api_integration/config/services/security_solution_edr_workflows_roles_users.ts +++ b/x-pack/test/security_solution_api_integration/config/services/security_solution_edr_workflows_roles_users.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { Role } from '@kbn/security-plugin/common'; import { EndpointSecurityRoleNames, ENDPOINT_SECURITY_ROLE_NAMES, @@ -61,9 +62,25 @@ export function RolesUsersProvider({ getService }: FtrProviderContext) { await security.role.create(predefinedRole, roleConfig); } if (customRole) { - await security.role.create(customRole.roleName, { - permissions: { feature: { siem: [...customRole.extraPrivileges] } }, - }); + const role: Omit = { + description: '', + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [ + { + spaces: ['*'], + base: [], + feature: { + siem: customRole.extraPrivileges, + }, + }, + ], + }; + + await security.role.create(customRole.roleName, role); } }, diff --git a/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/response_actions/trial_license_complete_tier/execute.ts b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/response_actions/trial_license_complete_tier/execute.ts index 4178fd80b653a9..6e50f67e3510d1 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/response_actions/trial_license_complete_tier/execute.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/response_actions/trial_license_complete_tier/execute.ts @@ -6,14 +6,21 @@ */ import { wrapErrorAndRejectPromise } from '@kbn/security-solution-plugin/common/endpoint/data_loaders/utils'; import expect from '@kbn/expect'; -import { EXECUTE_ROUTE } from '@kbn/security-solution-plugin/common/endpoint/constants'; +import { + ACTION_AGENT_FILE_INFO_ROUTE, + EXECUTE_ROUTE, +} from '@kbn/security-solution-plugin/common/endpoint/constants'; import { IndexedHostsAndAlertsResponse } from '@kbn/security-solution-plugin/common/endpoint/index_data'; +import { ActionDetails } from '@kbn/security-solution-plugin/common/endpoint/types'; +import { getFileDownloadId } from '@kbn/security-solution-plugin/common/endpoint/service/response_actions/get_file_download_id'; import { FtrProviderContext } from '../../../../ftr_provider_context_edr_workflows'; import { ROLE } from '../../../../config/services/security_solution_edr_workflows_roles_users'; export default function ({ getService }: FtrProviderContext) { const supertestWithoutAuth = getService('supertestWithoutAuth'); const endpointTestResources = getService('endpointTestResources'); + const rolesUsersProvider = getService('rolesUsersProvider'); + // @skipInServerlessMKI - this test uses internal index manipulation in before/after hooks describe('@ess @serverless @skipInServerlessMKI Endpoint `execute` response action', function () { let indexedData: IndexedHostsAndAlertsResponse; @@ -150,5 +157,66 @@ export default function ({ getService }: FtrProviderContext) { expect(data.parameters.command).to.eql('ls -la'); expect(data.parameters.timeout).to.eql(2000); }); + + // Test checks to ensure API works with a custom role + describe('@skipInServerless @skipInServerlessMKI and with minimal authz', () => { + const username = 'execute_limited'; + const password = 'changeme'; + let fileInfoApiRoutePath: string = ''; + + before(async () => { + await rolesUsersProvider.createRole({ + customRole: { + roleName: username, + extraPrivileges: ['minimal_all', 'execute_operations_all'], + }, + }); + await rolesUsersProvider.createUser({ name: username, password, roles: [username] }); + + const { + body: { data }, + } = await supertestWithoutAuth + .post(EXECUTE_ROUTE) + .auth(username, password) + .set('kbn-xsrf', 'true') + .set('Elastic-Api-Version', '2023-10-31') + .send({ endpoint_ids: [agentId], parameters: { command: 'ls -la' } }) + .expect(200); + + const actionDetails = data as ActionDetails; + + fileInfoApiRoutePath = ACTION_AGENT_FILE_INFO_ROUTE.replace('{action_id}', data.id).replace( + '{file_id}', + getFileDownloadId(actionDetails) + ); + }); + + after(async () => { + await rolesUsersProvider.deleteRoles([username]); + await rolesUsersProvider.deleteUsers([username]); + }); + + it('should have access to file info api', async () => { + await supertestWithoutAuth + .get(fileInfoApiRoutePath) + .auth(username, password) + .set('kbn-xsrf', 'true') + .set('Elastic-Api-Version', '2023-10-31') + // We expect 404 because the indexes with the file info don't exist. + // The key here is that we do NOT get a 401 or 403 + .expect(404); + }); + + it('should have access to file download api', async () => { + await supertestWithoutAuth + .get(`${fileInfoApiRoutePath}/download`) + .auth(username, password) + .set('kbn-xsrf', 'true') + .set('Elastic-Api-Version', '2023-10-31') + // We expect 404 because the indexes with the file info don't exist. + // The key here is that we do NOT get a 401 or 403 + .expect(404); + }); + }); }); } diff --git a/x-pack/test/security_solution_api_integration/tsconfig.json b/x-pack/test/security_solution_api_integration/tsconfig.json index 8584cebd03edb2..a4b454cb278700 100644 --- a/x-pack/test/security_solution_api_integration/tsconfig.json +++ b/x-pack/test/security_solution_api_integration/tsconfig.json @@ -46,5 +46,6 @@ "@kbn/utility-types", "@kbn/timelines-plugin", "@kbn/dev-cli-runner", + "@kbn/security-plugin", ] } From 7aae5d9ce1dc84fd3763bba4930e798f0897d453 Mon Sep 17 00:00:00 2001 From: Maxim Palenov Date: Mon, 22 Jul 2024 17:50:40 +0200 Subject: [PATCH 06/54] [Security Solution] Enable OpenAPI schemas linting in Security Solution plugin (#188529) **Relates to:** https://github.com/elastic/security-team/issues/9401 ## Summary Disabling OpenAPI spec linting in https://github.com/elastic/kibana/pull/179074 lead to accumulating invalid OpenAPi specs. This PR enables OpenAPI linting for Security Solution plugin and make appropriate fixes to make the linting pass. ## Details OpenAPI linting is a part of code generation. It runs automatically but can be disabled via `skipLinting: true`. Code generation with disabled linting isn't able to catch all possible problems in processing specs. The majority of problems came from Entity Analytics and Osquery OpenAPI specs. These specs were fixed and refactored to enable code generation and integrate generated artefacts into routes to make sure OpenAPI spec match API endpoints they describe. It helped to catch some subtle inconsistencies. --- .../redocly_linter/config.yaml | 15 +- .../osquery/common/api/asset/assets.gen.ts | 37 ++++ .../common/api/asset/assets.schema.yaml | 24 ++- .../common/api/asset/assets_status.gen.ts | 3 - .../api/asset/assets_status.schema.yaml | 13 +- .../api/fleet_wrapper/fleet_wrapper.gen.ts | 51 +++++ .../fleet_wrapper/fleet_wrapper.schema.yaml | 54 +++-- .../fleet_wrapper/get_agent_details.gen.ts | 23 --- .../get_agent_details.schema.yaml | 20 -- .../fleet_wrapper/get_agent_details_route.ts | 14 -- .../fleet_wrapper/get_agent_policies.gen.ts | 23 --- .../get_agent_policies.schema.yaml | 26 --- .../fleet_wrapper/get_agent_policies_route.ts | 20 -- .../api/fleet_wrapper/get_agent_policy.gen.ts | 27 --- .../get_agent_policy.schema.yaml | 23 --- .../api/fleet_wrapper/get_agent_status.gen.ts | 3 - .../get_agent_status.schema.yaml | 19 +- .../api/fleet_wrapper/get_agents.gen.ts | 23 --- .../api/fleet_wrapper/get_agents.schema.yaml | 20 -- .../fleet_wrapper/get_package_policies.gen.ts | 23 --- .../get_package_policies.schema.yaml | 20 -- x-pack/plugins/osquery/common/api/index.ts | 3 +- .../api/status/privileges_check.schema.yaml | 3 +- .../common/api/status/status.schema.yaml | 3 +- .../osquery/scripts/openapi/generate.js | 2 - .../routes/fleet_wrapper/get_agent_details.ts | 13 +- .../fleet_wrapper/get_agent_policies.ts | 22 +- .../routes/fleet_wrapper/get_agent_policy.ts | 10 +- .../fleet_wrapper/get_package_policies.ts | 12 +- x-pack/plugins/osquery/tsconfig.json | 7 +- .../create_index/create_index.schema.yaml | 1 + .../read_index/read_index.schema.yaml | 1 + .../bulk_upload_asset_criticality.gen.ts | 32 ++- .../bulk_upload_asset_criticality.schema.yaml | 80 ++++++-- .../asset_criticality/common.gen.ts | 43 ---- .../asset_criticality/common.schema.yaml | 61 ------ .../create_asset_criticality.gen.ts | 59 ++++++ .../create_asset_criticality.schema.yaml | 25 ++- .../delete_asset_criticality.gen.ts | 61 ++++++ .../delete_asset_criticality.schema.yaml | 51 ++++- .../get_asset_criticality.gen.ts | 39 ++++ .../get_asset_criticality.schema.yaml | 36 +++- .../get_asset_criticality_status.gen.ts | 4 +- .../get_asset_criticality_status.schema.yaml | 16 +- .../asset_criticality/index.ts | 2 +- .../list_asset_criticality.gen.ts | 35 +++- .../list_asset_criticality.schema.yaml | 53 +++-- .../list_asset_criticality_query_params.ts | 18 -- .../upload_asset_criticality_csv.gen.ts | 46 +++++ .../upload_asset_criticality_csv.schema.yaml | 72 ++++++- .../risk_engine/engine_disable_route.gen.ts | 10 +- .../engine_disable_route.schema.yaml | 12 +- .../risk_engine/engine_enable_route.gen.ts | 14 +- .../engine_enable_route.schema.yaml | 16 +- .../risk_engine/engine_init_route.gen.ts | 18 +- .../risk_engine/engine_init_route.schema.yaml | 23 +-- .../risk_engine/engine_settings_route.gen.ts | 4 +- .../engine_settings_route.schema.yaml | 16 +- .../risk_engine/engine_status_route.gen.ts | 3 + .../engine_status_route.schema.yaml | 2 + .../entity_calculation_route.gen.ts | 26 +++ .../entity_calculation_route.schema.yaml | 5 + .../risk_engine/preview_route.gen.ts | 7 + .../risk_engine/preview_route.schema.yaml | 2 + ...ections_api_2023_10_31.bundled.schema.yaml | 2 + .../public/entity_analytics/api/api.ts | 22 +- .../hooks/use_disable_risk_engine_mutation.ts | 4 +- .../hooks/use_enable_risk_engine_mutation.ts | 8 +- .../hooks/use_init_risk_engine_mutation.ts | 10 +- .../components/result_step.tsx | 4 +- .../reducer.test.ts | 4 +- .../reducer.ts | 6 +- .../scripts/openapi/generate.js | 1 - .../asset_criticality_data_client.ts | 8 +- .../asset_criticality/routes/bulk_upload.ts | 8 +- .../asset_criticality/routes/delete.ts | 11 +- .../asset_criticality/routes/get.ts | 8 +- .../asset_criticality/routes/list.ts | 8 +- .../asset_criticality/routes/status.ts | 4 +- .../asset_criticality/routes/upload_csv.ts | 4 +- .../asset_criticality/routes/upsert.ts | 11 +- .../risk_engine/routes/disable.ts | 4 +- .../risk_engine/routes/enable.ts | 4 +- .../risk_engine/routes/init.ts | 8 +- .../risk_engine/routes/settings.ts | 4 +- .../lib/telemetry/event_based/events.ts | 8 +- .../services/security_solution_api.gen.ts | 191 ++++++++++++++++++ .../utils/asset_criticality.ts | 4 +- 88 files changed, 1077 insertions(+), 718 deletions(-) create mode 100644 x-pack/plugins/osquery/common/api/asset/assets.gen.ts create mode 100644 x-pack/plugins/osquery/common/api/fleet_wrapper/fleet_wrapper.gen.ts delete mode 100644 x-pack/plugins/osquery/common/api/fleet_wrapper/get_agent_details.gen.ts delete mode 100644 x-pack/plugins/osquery/common/api/fleet_wrapper/get_agent_details.schema.yaml delete mode 100644 x-pack/plugins/osquery/common/api/fleet_wrapper/get_agent_details_route.ts delete mode 100644 x-pack/plugins/osquery/common/api/fleet_wrapper/get_agent_policies.gen.ts delete mode 100644 x-pack/plugins/osquery/common/api/fleet_wrapper/get_agent_policies.schema.yaml delete mode 100644 x-pack/plugins/osquery/common/api/fleet_wrapper/get_agent_policies_route.ts delete mode 100644 x-pack/plugins/osquery/common/api/fleet_wrapper/get_agent_policy.gen.ts delete mode 100644 x-pack/plugins/osquery/common/api/fleet_wrapper/get_agent_policy.schema.yaml delete mode 100644 x-pack/plugins/osquery/common/api/fleet_wrapper/get_agents.gen.ts delete mode 100644 x-pack/plugins/osquery/common/api/fleet_wrapper/get_agents.schema.yaml delete mode 100644 x-pack/plugins/osquery/common/api/fleet_wrapper/get_package_policies.gen.ts delete mode 100644 x-pack/plugins/osquery/common/api/fleet_wrapper/get_package_policies.schema.yaml create mode 100644 x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/create_asset_criticality.gen.ts create mode 100644 x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/delete_asset_criticality.gen.ts create mode 100644 x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/get_asset_criticality.gen.ts delete mode 100644 x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/list_asset_criticality_query_params.ts create mode 100644 x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/upload_asset_criticality_csv.gen.ts diff --git a/packages/kbn-openapi-generator/redocly_linter/config.yaml b/packages/kbn-openapi-generator/redocly_linter/config.yaml index b423d9172b1c86..fc4ff630cc2bba 100644 --- a/packages/kbn-openapi-generator/redocly_linter/config.yaml +++ b/packages/kbn-openapi-generator/redocly_linter/config.yaml @@ -5,23 +5,24 @@ plugins: rules: spec: error - spec-strict-refs: warn + spec-strict-refs: error no-path-trailing-slash: error no-identical-paths: error - no-ambiguous-paths: warn + no-ambiguous-paths: error no-unresolved-refs: error no-enum-type-mismatch: error component-name-unique: error path-declaration-must-exist: error path-not-include-query: error - path-parameters-defined: warn - operation-description: warn operation-2xx-response: error - operation-4xx-response: warn operation-operationId: error operation-operationId-unique: error - operation-summary: warn operation-operationId-url-safe: error operation-parameters-unique: error - boolean-parameter-prefixes: warn extra-linter-rules-plugin/valid-x-modify: error + # Disable rules generating the majority of warnings. + # They will be handled separately. + # operation-description: warn + # operation-summary: warn + # operation-4xx-response: warn + # path-parameters-defined: warn diff --git a/x-pack/plugins/osquery/common/api/asset/assets.gen.ts b/x-pack/plugins/osquery/common/api/asset/assets.gen.ts new file mode 100644 index 00000000000000..f0cc5209e13a48 --- /dev/null +++ b/x-pack/plugins/osquery/common/api/asset/assets.gen.ts @@ -0,0 +1,37 @@ +/* + * 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. + */ + +/* + * NOTICE: Do not edit this file manually. + * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator. + * + * info: + * title: Assets Schema + * version: 1 + */ + +import { z } from 'zod'; + +import { AssetsRequestQuery } from './assets_status.gen'; + +export type ReadAssetsStatusRequestParams = z.infer; +export const ReadAssetsStatusRequestParams = z.object({ + query: AssetsRequestQuery, +}); +export type ReadAssetsStatusRequestParamsInput = z.input; + +export type ReadAssetsStatusResponse = z.infer; +export const ReadAssetsStatusResponse = z.object({}); + +export type UpdateAssetsStatusRequestParams = z.infer; +export const UpdateAssetsStatusRequestParams = z.object({ + query: AssetsRequestQuery, +}); +export type UpdateAssetsStatusRequestParamsInput = z.input; + +export type UpdateAssetsStatusResponse = z.infer; +export const UpdateAssetsStatusResponse = z.object({}); diff --git a/x-pack/plugins/osquery/common/api/asset/assets.schema.yaml b/x-pack/plugins/osquery/common/api/asset/assets.schema.yaml index 31688b7ce66cbf..2769bc188ab200 100644 --- a/x-pack/plugins/osquery/common/api/asset/assets.schema.yaml +++ b/x-pack/plugins/osquery/common/api/asset/assets.schema.yaml @@ -5,25 +5,41 @@ info: paths: /internal/osquery/assets: get: + x-codegen-enabled: true + operationId: ReadAssetsStatus summary: Get assets parameters: - - $ref: './assets_status.schema.yaml#/components/parameters/AssetsStatusRequestQueryParameter' + - name: query + in: path + required: true + schema: + $ref: './assets_status.schema.yaml#/components/schemas/AssetsRequestQuery' responses: '200': description: OK content: application/json: schema: - $ref: './assets_status.schema.yaml#/components/schemas/SuccessResponse' + type: object + properties: {} + # Define properties for the success response if needed /internal/osquery/assets/update: post: + x-codegen-enabled: true + operationId: UpdateAssetsStatus summary: Update assets parameters: - - $ref: './assets_status.schema.yaml#/components/parameters/AssetsStatusRequestQueryParameter' + - name: query + in: path + required: true + schema: + $ref: './assets_status.schema.yaml#/components/schemas/AssetsRequestQuery' responses: '200': description: OK content: application/json: schema: - $ref: './assets_status.schema.yaml#/components/schemas/SuccessResponse' + type: object + properties: {} + # Define properties for the success response if needed diff --git a/x-pack/plugins/osquery/common/api/asset/assets_status.gen.ts b/x-pack/plugins/osquery/common/api/asset/assets_status.gen.ts index 53a98b96612ead..fd3c50374943f2 100644 --- a/x-pack/plugins/osquery/common/api/asset/assets_status.gen.ts +++ b/x-pack/plugins/osquery/common/api/asset/assets_status.gen.ts @@ -18,6 +18,3 @@ import { z } from 'zod'; export type AssetsRequestQuery = z.infer; export const AssetsRequestQuery = z.object({}); - -export type SuccessResponse = z.infer; -export const SuccessResponse = z.object({}); diff --git a/x-pack/plugins/osquery/common/api/asset/assets_status.schema.yaml b/x-pack/plugins/osquery/common/api/asset/assets_status.schema.yaml index 48322c1266b07d..fb57329a9992d1 100644 --- a/x-pack/plugins/osquery/common/api/asset/assets_status.schema.yaml +++ b/x-pack/plugins/osquery/common/api/asset/assets_status.schema.yaml @@ -2,19 +2,8 @@ openapi: 3.0.0 info: title: Assets Status Schema version: '1' -paths: { } +paths: {} components: - parameters: - AssetsStatusRequestQueryParameter: - name: query - in: path - required: true - schema: - $ref: '#/components/schemas/AssetsRequestQuery' schemas: AssetsRequestQuery: type: object - SuccessResponse: - type: object - properties: {} - # Define properties for the success response if needed diff --git a/x-pack/plugins/osquery/common/api/fleet_wrapper/fleet_wrapper.gen.ts b/x-pack/plugins/osquery/common/api/fleet_wrapper/fleet_wrapper.gen.ts new file mode 100644 index 00000000000000..1ecea2c4caf190 --- /dev/null +++ b/x-pack/plugins/osquery/common/api/fleet_wrapper/fleet_wrapper.gen.ts @@ -0,0 +1,51 @@ +/* + * 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. + */ + +/* + * NOTICE: Do not edit this file manually. + * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator. + * + * info: + * title: Fleet wrapper schema + * version: 1 + */ + +import { z } from 'zod'; + +import { Id } from '../model/schema/common_attributes.gen'; + +export type GetAgentDetailsRequestParams = z.infer; +export const GetAgentDetailsRequestParams = z.object({ + id: Id, +}); +export type GetAgentDetailsRequestParamsInput = z.input; + +export type GetAgentDetailsResponse = z.infer; +export const GetAgentDetailsResponse = z.object({}); + +export type GetAgentPackagePoliciesResponse = z.infer; +export const GetAgentPackagePoliciesResponse = z.object({}); + +export type GetAgentPoliciesResponse = z.infer; +export const GetAgentPoliciesResponse = z.object({}); + +export type GetAgentPolicyRequestParams = z.infer; +export const GetAgentPolicyRequestParams = z.object({ + id: Id, +}); +export type GetAgentPolicyRequestParamsInput = z.input; + +export type GetAgentPolicyResponse = z.infer; +export const GetAgentPolicyResponse = z.object({}); +export type GetAgentsRequestQuery = z.infer; +export const GetAgentsRequestQuery = z.object({ + query: z.object({}), +}); +export type GetAgentsRequestQueryInput = z.input; + +export type GetAgentsResponse = z.infer; +export const GetAgentsResponse = z.object({}); diff --git a/x-pack/plugins/osquery/common/api/fleet_wrapper/fleet_wrapper.schema.yaml b/x-pack/plugins/osquery/common/api/fleet_wrapper/fleet_wrapper.schema.yaml index 7e46e15abb825c..fa5a576cb1a2e7 100644 --- a/x-pack/plugins/osquery/common/api/fleet_wrapper/fleet_wrapper.schema.yaml +++ b/x-pack/plugins/osquery/common/api/fleet_wrapper/fleet_wrapper.schema.yaml @@ -5,66 +5,94 @@ info: paths: /internal/osquery/fleet_wrapper/agents: get: + x-codegen-enabled: true + operationId: GetAgents summary: Get agents parameters: - - $ref: './get_agents.schema.yaml#/components/parameters/GetAgentsRequestQueryParameter' + - name: query + in: query + required: true + schema: + type: object + additionalProperties: true responses: '200': description: OK content: application/json: schema: - $ref: './get_agents.schema.yaml#/components/schemas/SuccessResponse' + type: object + properties: {} + # Define properties for the success response if needed /internal/osquery/fleet_wrapper/agents/{id}: get: + x-codegen-enabled: true + operationId: GetAgentDetails summary: Get Agent details parameters: - - $ref: './get_agent_details.schema.yaml#/components/parameters/GetAgentDetailsRequestQueryParameter' + - name: id + in: path + required: true + schema: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/Id' responses: '200': description: OK content: application/json: schema: - $ref: './get_agent_details.schema.yaml#/components/schemas/SuccessResponse' + type: object + properties: {} + # Define properties for the success response if needed /internal/osquery/fleet_wrapper/agent_policies: get: + x-codegen-enabled: true + operationId: GetAgentPolicies summary: Get Agent policies - parameters: - - $ref: './get_agent_policies.schema.yaml#/components/parameters/GetAgentPoliciesRequestParameter' - - $ref: './get_agent_policies.schema.yaml#/components/parameters/GetAgentPoliciesRequestQueryParameter' responses: '200': description: OK content: application/json: schema: - $ref: './get_agent_policies.schema.yaml#/components/schemas/SuccessResponse' + type: object + properties: {} + # Define properties for the success response if needed /internal/osquery/fleet_wrapper/agent_policies/{id}: get: + x-codegen-enabled: true + operationId: GetAgentPolicy summary: Get Agent policy parameters: - - $ref: './get_agent_policy.schema.yaml#/components/parameters/GetAgentPolicyRequestParameter' + - name: id + in: path + required: true + schema: + $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/Id' responses: '200': description: OK content: application/json: schema: - $ref: './get_agent_policy.schema.yaml#/components/schemas/SuccessResponse' + type: object + properties: {} + # Define properties for the success response if needed /internal/osquery/fleet_wrapper/package_policies: get: + x-codegen-enabled: true + operationId: GetAgentPackagePolicies summary: Get Agent policy - parameters: - - $ref: './get_package_policies.schema.yaml#/components/parameters/GetPackagePoliciesRequestQueryParameter' responses: '200': description: OK content: application/json: schema: - $ref: './get_package_policies.schema.yaml#/components/schemas/SuccessResponse' + type: object + properties: {} + # Define properties for the success response if needed diff --git a/x-pack/plugins/osquery/common/api/fleet_wrapper/get_agent_details.gen.ts b/x-pack/plugins/osquery/common/api/fleet_wrapper/get_agent_details.gen.ts deleted file mode 100644 index 5d721a018205bb..00000000000000 --- a/x-pack/plugins/osquery/common/api/fleet_wrapper/get_agent_details.gen.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * 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. - */ - -/* - * NOTICE: Do not edit this file manually. - * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator. - * - * info: - * title: Get agent details schema - * version: 1 - */ - -import { z } from 'zod'; - -export type GetAgentDetailsRequestParams = z.infer; -export const GetAgentDetailsRequestParams = z.object({}); - -export type SuccessResponse = z.infer; -export const SuccessResponse = z.object({}); diff --git a/x-pack/plugins/osquery/common/api/fleet_wrapper/get_agent_details.schema.yaml b/x-pack/plugins/osquery/common/api/fleet_wrapper/get_agent_details.schema.yaml deleted file mode 100644 index bdf4cb3329cdfd..00000000000000 --- a/x-pack/plugins/osquery/common/api/fleet_wrapper/get_agent_details.schema.yaml +++ /dev/null @@ -1,20 +0,0 @@ -openapi: 3.0.0 -info: - title: Get agent details schema - version: '1' -paths: { } -components: - parameters: - GetAgentDetailsRequestQueryParameter: - name: query - in: path - required: true - schema: - $ref: '#/components/schemas/GetAgentDetailsRequestParams' - schemas: - GetAgentDetailsRequestParams: - type: object - SuccessResponse: - type: object - properties: {} - # Define properties for the success response if needed diff --git a/x-pack/plugins/osquery/common/api/fleet_wrapper/get_agent_details_route.ts b/x-pack/plugins/osquery/common/api/fleet_wrapper/get_agent_details_route.ts deleted file mode 100644 index fcc7dad089babb..00000000000000 --- a/x-pack/plugins/osquery/common/api/fleet_wrapper/get_agent_details_route.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * 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 t from 'io-ts'; - -export const getAgentDetailsRequestParamsSchema = t.unknown; - -export type GetAgentDetailsRequestParamsSchema = t.OutputOf< - typeof getAgentDetailsRequestParamsSchema ->; diff --git a/x-pack/plugins/osquery/common/api/fleet_wrapper/get_agent_policies.gen.ts b/x-pack/plugins/osquery/common/api/fleet_wrapper/get_agent_policies.gen.ts deleted file mode 100644 index 875c21a600e936..00000000000000 --- a/x-pack/plugins/osquery/common/api/fleet_wrapper/get_agent_policies.gen.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * 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. - */ - -/* - * NOTICE: Do not edit this file manually. - * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator. - * - * info: - * title: Get agent policies schema - * version: 1 - */ - -import { z } from 'zod'; - -export type GetAgentPoliciesRequestParams = z.infer; -export const GetAgentPoliciesRequestParams = z.object({}); - -export type SuccessResponse = z.infer; -export const SuccessResponse = z.object({}); diff --git a/x-pack/plugins/osquery/common/api/fleet_wrapper/get_agent_policies.schema.yaml b/x-pack/plugins/osquery/common/api/fleet_wrapper/get_agent_policies.schema.yaml deleted file mode 100644 index cdfb521712674e..00000000000000 --- a/x-pack/plugins/osquery/common/api/fleet_wrapper/get_agent_policies.schema.yaml +++ /dev/null @@ -1,26 +0,0 @@ -openapi: 3.0.0 -info: - title: Get agent policies schema - version: '1' -paths: { } -components: - parameters: - GetAgentPoliciesRequestQueryParameter: - name: query - in: query - required: true - schema: - $ref: '#/components/schemas/GetAgentPoliciesRequestParams' - GetAgentPoliciesRequestParameter: - name: query - in: path - required: true - schema: - $ref: '#/components/schemas/GetAgentPoliciesRequestParams' - schemas: - GetAgentPoliciesRequestParams: - type: object - SuccessResponse: - type: object - properties: {} - # Define properties for the success response if needed diff --git a/x-pack/plugins/osquery/common/api/fleet_wrapper/get_agent_policies_route.ts b/x-pack/plugins/osquery/common/api/fleet_wrapper/get_agent_policies_route.ts deleted file mode 100644 index 84a68e5fbf4c70..00000000000000 --- a/x-pack/plugins/osquery/common/api/fleet_wrapper/get_agent_policies_route.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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 t from 'io-ts'; - -export const getAgentPoliciesRequestParamsSchema = t.unknown; - -export type GetAgentPoliciesRequestParamsSchema = t.OutputOf< - typeof getAgentPoliciesRequestParamsSchema ->; - -export const getAgentPoliciesRequestQuerySchema = t.unknown; - -export type GetAgentPoliciesRequestQuerySchema = t.OutputOf< - typeof getAgentPoliciesRequestQuerySchema ->; diff --git a/x-pack/plugins/osquery/common/api/fleet_wrapper/get_agent_policy.gen.ts b/x-pack/plugins/osquery/common/api/fleet_wrapper/get_agent_policy.gen.ts deleted file mode 100644 index 3f19e274761bd1..00000000000000 --- a/x-pack/plugins/osquery/common/api/fleet_wrapper/get_agent_policy.gen.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * 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. - */ - -/* - * NOTICE: Do not edit this file manually. - * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator. - * - * info: - * title: Get agent policy schema - * version: 1 - */ - -import { z } from 'zod'; - -import { Id } from '../model/schema/common_attributes.gen'; - -export type GetAgentPolicyRequestParams = z.infer; -export const GetAgentPolicyRequestParams = z.object({ - id: Id.optional(), -}); - -export type SuccessResponse = z.infer; -export const SuccessResponse = z.object({}); diff --git a/x-pack/plugins/osquery/common/api/fleet_wrapper/get_agent_policy.schema.yaml b/x-pack/plugins/osquery/common/api/fleet_wrapper/get_agent_policy.schema.yaml deleted file mode 100644 index dc4a2607bfc6bf..00000000000000 --- a/x-pack/plugins/osquery/common/api/fleet_wrapper/get_agent_policy.schema.yaml +++ /dev/null @@ -1,23 +0,0 @@ -openapi: 3.0.0 -info: - title: Get agent policy schema - version: '1' -paths: { } -components: - parameters: - GetAgentPolicyRequestParameter: - name: query - in: path - required: true - schema: - $ref: '#/components/schemas/GetAgentPolicyRequestParams' - schemas: - GetAgentPolicyRequestParams: - type: object - properties: - id: - $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/Id' - SuccessResponse: - type: object - properties: {} - # Define properties for the success response if needed diff --git a/x-pack/plugins/osquery/common/api/fleet_wrapper/get_agent_status.gen.ts b/x-pack/plugins/osquery/common/api/fleet_wrapper/get_agent_status.gen.ts index 80adc112312a74..041aac0bf23208 100644 --- a/x-pack/plugins/osquery/common/api/fleet_wrapper/get_agent_status.gen.ts +++ b/x-pack/plugins/osquery/common/api/fleet_wrapper/get_agent_status.gen.ts @@ -26,6 +26,3 @@ export const GetAgentStatusRequestQueryParams = z.object({ kuery: KueryOrUndefined.optional(), policyId: Id.optional(), }); - -export type SuccessResponse = z.infer; -export const SuccessResponse = z.object({}); diff --git a/x-pack/plugins/osquery/common/api/fleet_wrapper/get_agent_status.schema.yaml b/x-pack/plugins/osquery/common/api/fleet_wrapper/get_agent_status.schema.yaml index e10174bee26345..af2e9307b4c127 100644 --- a/x-pack/plugins/osquery/common/api/fleet_wrapper/get_agent_status.schema.yaml +++ b/x-pack/plugins/osquery/common/api/fleet_wrapper/get_agent_status.schema.yaml @@ -2,21 +2,8 @@ openapi: 3.0.0 info: title: Get agent status schema version: '1' -paths: { } +paths: {} components: - parameters: - GetAgentStatusRequestQueryParameter: - name: query - in: query - required: true - schema: - $ref: '#/components/schemas/GetAgentStatusRequestQueryParams' - GetAgentStatusRequestParameter: - name: query - in: path - required: true - schema: - $ref: '#/components/schemas/GetAgentStatusRequestParams' schemas: GetAgentStatusRequestParams: type: object @@ -27,7 +14,3 @@ components: $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/KueryOrUndefined' policyId: $ref: '../model/schema/common_attributes.schema.yaml#/components/schemas/Id' - SuccessResponse: - type: object - properties: {} - # Define properties for the success response if needed diff --git a/x-pack/plugins/osquery/common/api/fleet_wrapper/get_agents.gen.ts b/x-pack/plugins/osquery/common/api/fleet_wrapper/get_agents.gen.ts deleted file mode 100644 index b162bcbfd967b5..00000000000000 --- a/x-pack/plugins/osquery/common/api/fleet_wrapper/get_agents.gen.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * 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. - */ - -/* - * NOTICE: Do not edit this file manually. - * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator. - * - * info: - * title: Get agents schema - * version: 1 - */ - -import { z } from 'zod'; - -export type GetAgentsRequestParams = z.infer; -export const GetAgentsRequestParams = z.object({}); - -export type SuccessResponse = z.infer; -export const SuccessResponse = z.object({}); diff --git a/x-pack/plugins/osquery/common/api/fleet_wrapper/get_agents.schema.yaml b/x-pack/plugins/osquery/common/api/fleet_wrapper/get_agents.schema.yaml deleted file mode 100644 index c1a387512c3d3b..00000000000000 --- a/x-pack/plugins/osquery/common/api/fleet_wrapper/get_agents.schema.yaml +++ /dev/null @@ -1,20 +0,0 @@ -openapi: 3.0.0 -info: - title: Get agents schema - version: '1' -paths: { } -components: - parameters: - GetAgentsRequestQueryParameter: - name: query - in: path - required: true - schema: - $ref: '#/components/schemas/GetAgentsRequestParams' - schemas: - GetAgentsRequestParams: - type: object - SuccessResponse: - type: object - properties: {} - # Define properties for the success response if needed diff --git a/x-pack/plugins/osquery/common/api/fleet_wrapper/get_package_policies.gen.ts b/x-pack/plugins/osquery/common/api/fleet_wrapper/get_package_policies.gen.ts deleted file mode 100644 index f4c3be37371eae..00000000000000 --- a/x-pack/plugins/osquery/common/api/fleet_wrapper/get_package_policies.gen.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * 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. - */ - -/* - * NOTICE: Do not edit this file manually. - * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator. - * - * info: - * title: Get package policies schema - * version: 1 - */ - -import { z } from 'zod'; - -export type GetPackagePoliciesRequestParams = z.infer; -export const GetPackagePoliciesRequestParams = z.object({}); - -export type SuccessResponse = z.infer; -export const SuccessResponse = z.object({}); diff --git a/x-pack/plugins/osquery/common/api/fleet_wrapper/get_package_policies.schema.yaml b/x-pack/plugins/osquery/common/api/fleet_wrapper/get_package_policies.schema.yaml deleted file mode 100644 index 708867e8f7fa19..00000000000000 --- a/x-pack/plugins/osquery/common/api/fleet_wrapper/get_package_policies.schema.yaml +++ /dev/null @@ -1,20 +0,0 @@ -openapi: 3.0.0 -info: - title: Get package policies schema - version: '1' -paths: { } -components: - parameters: - GetPackagePoliciesRequestQueryParameter: - name: query - in: path - required: true - schema: - $ref: '#/components/schemas/GetPackagePoliciesRequestParams' - schemas: - GetPackagePoliciesRequestParams: - type: object - SuccessResponse: - type: object - properties: {} - # Define properties for the success response if needed diff --git a/x-pack/plugins/osquery/common/api/index.ts b/x-pack/plugins/osquery/common/api/index.ts index 681eaab583ca88..b1c42a8dc45e61 100644 --- a/x-pack/plugins/osquery/common/api/index.ts +++ b/x-pack/plugins/osquery/common/api/index.ts @@ -7,8 +7,7 @@ export * from './asset/get_assets_status_route'; export * from './asset/update_assets_status_route'; -export * from './fleet_wrapper/get_agent_policies_route'; -export * from './fleet_wrapper/get_agent_details_route'; +export * from './fleet_wrapper/fleet_wrapper.gen'; export * from './fleet_wrapper/get_agent_policy_route'; export * from './fleet_wrapper/get_agent_status_for_agent_policy_route'; export * from './fleet_wrapper/get_agents_route'; diff --git a/x-pack/plugins/osquery/common/api/status/privileges_check.schema.yaml b/x-pack/plugins/osquery/common/api/status/privileges_check.schema.yaml index 2702d1bafa0401..8a8267a83f3368 100644 --- a/x-pack/plugins/osquery/common/api/status/privileges_check.schema.yaml +++ b/x-pack/plugins/osquery/common/api/status/privileges_check.schema.yaml @@ -5,6 +5,7 @@ info: paths: /internal/osquery/privileges_check: get: + operationId: ReadPrivilegesCheck summary: Get Osquery privileges check responses: '200': @@ -13,4 +14,4 @@ paths: application/json: schema: type: object - properties: { } + properties: {} diff --git a/x-pack/plugins/osquery/common/api/status/status.schema.yaml b/x-pack/plugins/osquery/common/api/status/status.schema.yaml index 9ab4d3bd0e6077..1ed1e096ba10e5 100644 --- a/x-pack/plugins/osquery/common/api/status/status.schema.yaml +++ b/x-pack/plugins/osquery/common/api/status/status.schema.yaml @@ -5,6 +5,7 @@ info: paths: /internal/osquery/status: get: + operationId: ReadInstallationStatus summary: Get Osquery installation status responses: '200': @@ -13,4 +14,4 @@ paths: application/json: schema: type: object - properties: { } + properties: {} diff --git a/x-pack/plugins/osquery/scripts/openapi/generate.js b/x-pack/plugins/osquery/scripts/openapi/generate.js index 35c099301e81c0..018a965702c3e1 100644 --- a/x-pack/plugins/osquery/scripts/openapi/generate.js +++ b/x-pack/plugins/osquery/scripts/openapi/generate.js @@ -17,6 +17,4 @@ generate({ rootDir: OSQUERY_ROOT, sourceGlob: './**/*.schema.yaml', templateName: 'zod_operation_schema', - // TODO: Fix lint errors - skipLinting: true, }); diff --git a/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_details.ts b/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_details.ts index b3b6539f9fc35a..c1d445fd40183d 100644 --- a/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_details.ts +++ b/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_details.ts @@ -6,12 +6,11 @@ */ import type { IRouter } from '@kbn/core/server'; -import type { GetAgentDetailsRequestParamsSchema } from '../../../common/api'; -import { buildRouteValidation } from '../../utils/build_validation/route_validation'; +import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; import { API_VERSIONS } from '../../../common/constants'; import { PLUGIN_ID } from '../../../common'; import type { OsqueryAppContext } from '../../lib/osquery_app_context_services'; -import { getAgentDetailsRequestParamsSchema } from '../../../common/api'; +import { GetAgentDetailsRequestParams } from '../../../common/api'; export const getAgentDetailsRoute = (router: IRouter, osqueryContext: OsqueryAppContext) => { router.versioned @@ -25,10 +24,7 @@ export const getAgentDetailsRoute = (router: IRouter, osqueryContext: OsqueryApp version: API_VERSIONS.internal.v1, validate: { request: { - params: buildRouteValidation< - typeof getAgentDetailsRequestParamsSchema, - GetAgentDetailsRequestParamsSchema - >(getAgentDetailsRequestParamsSchema), + params: buildRouteValidationWithZod(GetAgentDetailsRequestParams), }, }, }, @@ -38,8 +34,7 @@ export const getAgentDetailsRoute = (router: IRouter, osqueryContext: OsqueryApp try { agent = await osqueryContext.service .getAgentService() - ?.asInternalUser // @ts-expect-error update types - ?.getAgent(request.params.id); + ?.asInternalUser?.getAgent(request.params.id); } catch (err) { return response.notFound(); } diff --git a/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_policies.ts b/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_policies.ts index ee807586527066..9e844107125066 100644 --- a/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_policies.ts +++ b/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_policies.ts @@ -11,19 +11,10 @@ import { satisfies } from 'semver'; import type { GetAgentPoliciesResponseItem, PackagePolicy } from '@kbn/fleet-plugin/common'; import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '@kbn/fleet-plugin/common'; import type { IRouter } from '@kbn/core/server'; -import type { - GetAgentPoliciesRequestParamsSchema, - GetAgentPoliciesRequestQuerySchema, -} from '../../../common/api'; -import { buildRouteValidation } from '../../utils/build_validation/route_validation'; import { API_VERSIONS } from '../../../common/constants'; import { OSQUERY_INTEGRATION_NAME, PLUGIN_ID } from '../../../common'; import type { OsqueryAppContext } from '../../lib/osquery_app_context_services'; import { getInternalSavedObjectsClient } from '../utils'; -import { - getAgentPoliciesRequestParamsSchema, - getAgentPoliciesRequestQuerySchema, -} from '../../../common/api'; export const getAgentPoliciesRoute = (router: IRouter, osqueryContext: OsqueryAppContext) => { router.versioned @@ -35,18 +26,7 @@ export const getAgentPoliciesRoute = (router: IRouter, osqueryContext: OsqueryAp .addVersion( { version: API_VERSIONS.internal.v1, - validate: { - request: { - params: buildRouteValidation< - typeof getAgentPoliciesRequestParamsSchema, - GetAgentPoliciesRequestParamsSchema - >(getAgentPoliciesRequestParamsSchema), - query: buildRouteValidation< - typeof getAgentPoliciesRequestQuerySchema, - GetAgentPoliciesRequestQuerySchema - >(getAgentPoliciesRequestQuerySchema), - }, - }, + validate: {}, }, async (context, request, response) => { const internalSavedObjectsClient = await getInternalSavedObjectsClient( diff --git a/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_policy.ts b/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_policy.ts index 85de68f7e44d9a..bad5b01289d52c 100644 --- a/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_policy.ts +++ b/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_policy.ts @@ -6,13 +6,12 @@ */ import type { IRouter } from '@kbn/core/server'; -import type { GetAgentPolicyRequestParamsSchema } from '../../../common/api'; -import { buildRouteValidation } from '../../utils/build_validation/route_validation'; +import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; import { API_VERSIONS } from '../../../common/constants'; import { PLUGIN_ID } from '../../../common'; import type { OsqueryAppContext } from '../../lib/osquery_app_context_services'; import { getInternalSavedObjectsClient } from '../utils'; -import { getAgentPolicyRequestParamsSchema } from '../../../common/api'; +import { GetAgentPolicyRequestParams } from '../../../common/api'; export const getAgentPolicyRoute = (router: IRouter, osqueryContext: OsqueryAppContext) => { router.versioned @@ -26,10 +25,7 @@ export const getAgentPolicyRoute = (router: IRouter, osqueryContext: OsqueryAppC version: API_VERSIONS.internal.v1, validate: { request: { - params: buildRouteValidation< - typeof getAgentPolicyRequestParamsSchema, - GetAgentPolicyRequestParamsSchema - >(getAgentPolicyRequestParamsSchema), + params: buildRouteValidationWithZod(GetAgentPolicyRequestParams), }, }, }, diff --git a/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_package_policies.ts b/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_package_policies.ts index 887fa4811e73e4..86719125b97eb8 100644 --- a/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_package_policies.ts +++ b/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_package_policies.ts @@ -7,13 +7,10 @@ import type { IRouter } from '@kbn/core/server'; import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '@kbn/fleet-plugin/common'; -import type { GetPackagePoliciesRequestQuerySchema } from '../../../common/api'; -import { buildRouteValidation } from '../../utils/build_validation/route_validation'; import { API_VERSIONS } from '../../../common/constants'; import { PLUGIN_ID, OSQUERY_INTEGRATION_NAME } from '../../../common'; import type { OsqueryAppContext } from '../../lib/osquery_app_context_services'; import { getInternalSavedObjectsClient } from '../utils'; -import { getPackagePoliciesRequestQuerySchema } from '../../../common/api'; export const getPackagePoliciesRoute = (router: IRouter, osqueryContext: OsqueryAppContext) => { router.versioned @@ -25,14 +22,7 @@ export const getPackagePoliciesRoute = (router: IRouter, osqueryContext: Osquery .addVersion( { version: API_VERSIONS.internal.v1, - validate: { - request: { - query: buildRouteValidation< - typeof getPackagePoliciesRequestQuerySchema, - GetPackagePoliciesRequestQuerySchema - >(getPackagePoliciesRequestQuerySchema), - }, - }, + validate: {}, }, async (context, request, response) => { const internalSavedObjectsClient = await getInternalSavedObjectsClient( diff --git a/x-pack/plugins/osquery/tsconfig.json b/x-pack/plugins/osquery/tsconfig.json index 6d713311c777dc..6cc74e9733a92c 100644 --- a/x-pack/plugins/osquery/tsconfig.json +++ b/x-pack/plugins/osquery/tsconfig.json @@ -3,9 +3,7 @@ "compilerOptions": { "outDir": "target/types" }, - "exclude": [ - "target/**/*" - ], + "exclude": ["target/**/*"], "include": [ // add all the folders contains files to be compiled "common/**/*", @@ -77,6 +75,7 @@ "@kbn/openapi-generator", "@kbn/code-editor", "@kbn/search-types", - "@kbn/react-kibana-context-render" + "@kbn/react-kibana-context-render", + "@kbn/zod-helpers" ] } diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/index_management/create_index/create_index.schema.yaml b/x-pack/plugins/security_solution/common/api/detection_engine/index_management/create_index/create_index.schema.yaml index 63213117bd9fb8..b825f5f7af7c0d 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/index_management/create_index/create_index.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/detection_engine/index_management/create_index/create_index.schema.yaml @@ -35,6 +35,7 @@ paths: schema: $ref: '../../../model/error_responses.schema.yaml#/components/schemas/SiemErrorResponse' 404: + description: Not found content: application/json: schema: diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/index_management/read_index/read_index.schema.yaml b/x-pack/plugins/security_solution/common/api/detection_engine/index_management/read_index/read_index.schema.yaml index 4c38c57da7592c..ddfbf564de2acb 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/index_management/read_index/read_index.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/detection_engine/index_management/read_index/read_index.schema.yaml @@ -38,6 +38,7 @@ paths: schema: $ref: '../../../model/error_responses.schema.yaml#/components/schemas/SiemErrorResponse' 404: + description: Not found content: application/json: schema: diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/bulk_upload_asset_criticality.gen.ts b/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/bulk_upload_asset_criticality.gen.ts index c0d00e394b6b1f..5315edc16ab9f3 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/bulk_upload_asset_criticality.gen.ts +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/bulk_upload_asset_criticality.gen.ts @@ -18,7 +18,35 @@ import { z } from 'zod'; import { CreateAssetCriticalityRecord } from './common.gen'; -export type AssetCriticalityBulkUploadRequest = z.infer; -export const AssetCriticalityBulkUploadRequest = z.object({ +export type AssetCriticalityBulkUploadErrorItem = z.infer< + typeof AssetCriticalityBulkUploadErrorItem +>; +export const AssetCriticalityBulkUploadErrorItem = z.object({ + message: z.string(), + index: z.number().int(), +}); + +export type AssetCriticalityBulkUploadStats = z.infer; +export const AssetCriticalityBulkUploadStats = z.object({ + successful: z.number().int(), + failed: z.number().int(), + total: z.number().int(), +}); + +export type BulkUpsertAssetCriticalityRecordsRequestBody = z.infer< + typeof BulkUpsertAssetCriticalityRecordsRequestBody +>; +export const BulkUpsertAssetCriticalityRecordsRequestBody = z.object({ records: z.array(CreateAssetCriticalityRecord).min(1).max(1000), }); +export type BulkUpsertAssetCriticalityRecordsRequestBodyInput = z.input< + typeof BulkUpsertAssetCriticalityRecordsRequestBody +>; + +export type BulkUpsertAssetCriticalityRecordsResponse = z.infer< + typeof BulkUpsertAssetCriticalityRecordsResponse +>; +export const BulkUpsertAssetCriticalityRecordsResponse = z.object({ + errors: z.array(AssetCriticalityBulkUploadErrorItem), + stats: AssetCriticalityBulkUploadStats, +}); diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/bulk_upload_asset_criticality.schema.yaml b/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/bulk_upload_asset_criticality.schema.yaml index b4b7d5d2f1fe4c..c0fecede6da722 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/bulk_upload_asset_criticality.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/bulk_upload_asset_criticality.schema.yaml @@ -13,40 +13,82 @@ paths: /api/asset_criticality/bulk: post: x-labels: [ess, serverless] + x-codegen-enabled: true + operationId: BulkUpsertAssetCriticalityRecords summary: Bulk upsert asset criticality data, creating or updating records as needed requestBody: content: application/json: schema: - $ref: '#/components/schemas/AssetCriticalityBulkUploadRequest' - + type: object + example: + records: + - id_value: 'host-1' + id_field: 'host.name' + criticality_level: 'low_impact' + - id_value: 'host-2' + id_field: 'host.name' + criticality_level: 'medium_impact' + properties: + records: + type: array + minItems: 1 + maxItems: 1000 + items: + $ref: './common.schema.yaml#/components/schemas/CreateAssetCriticalityRecord' + required: + - records responses: '200': description: Bulk upload successful content: application/json: schema: - $ref: './common.schema.yaml#/components/schemas/AssetCriticalityBulkUploadResponse' + type: object + example: + errors: + - message: 'Invalid ID field' + index: 0 + stats: + successful: 1 + failed: 1 + total: 2 + properties: + errors: + type: array + items: + $ref: '#/components/schemas/AssetCriticalityBulkUploadErrorItem' + stats: + $ref: '#/components/schemas/AssetCriticalityBulkUploadStats' + required: + - errors + - stats '413': description: File too large + components: schemas: - AssetCriticalityBulkUploadRequest: + AssetCriticalityBulkUploadErrorItem: type: object - example: - records: - - id_value: 'host-1' - id_field: 'host.name' - criticality_level: 'low_impact' - - id_value: 'host-2' - id_field: 'host.name' - criticality_level: 'medium_impact' properties: - records: - type: array - minItems: 1 - maxItems: 1000 - items: - $ref: './common.schema.yaml#/components/schemas/CreateAssetCriticalityRecord' + message: + type: string + index: + type: integer required: - - records + - message + - index + + AssetCriticalityBulkUploadStats: + type: object + properties: + successful: + type: integer + failed: + type: integer + total: + type: integer + required: + - successful + - failed + - total diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/common.gen.ts b/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/common.gen.ts index 4b689d22944e1f..dfaa5d852c993c 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/common.gen.ts +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/common.gen.ts @@ -53,28 +53,6 @@ export const CreateAssetCriticalityRecord = AssetCriticalityRecordIdParts.merge( }) ); -export type CreateSingleAssetCriticalityRequest = z.infer< - typeof CreateSingleAssetCriticalityRequest ->; -export const CreateSingleAssetCriticalityRequest = CreateAssetCriticalityRecord.merge( - z.object({ - /** - * If 'wait_for' the request will wait for the index refresh. - */ - refresh: z.literal('wait_for').optional(), - }) -); - -export type DeleteAssetCriticalityRecord = z.infer; -export const DeleteAssetCriticalityRecord = AssetCriticalityRecordIdParts.merge( - z.object({ - /** - * If 'wait_for' the request will wait for the index refresh. - */ - refresh: z.literal('wait_for').optional(), - }) -); - export type AssetCriticalityRecord = z.infer; export const AssetCriticalityRecord = CreateAssetCriticalityRecord.merge( z.object({ @@ -84,24 +62,3 @@ export const AssetCriticalityRecord = CreateAssetCriticalityRecord.merge( '@timestamp': z.string().datetime(), }) ); - -export type AssetCriticalityBulkUploadErrorItem = z.infer< - typeof AssetCriticalityBulkUploadErrorItem ->; -export const AssetCriticalityBulkUploadErrorItem = z.object({ - message: z.string(), - index: z.number().int(), -}); - -export type AssetCriticalityBulkUploadStats = z.infer; -export const AssetCriticalityBulkUploadStats = z.object({ - successful: z.number().int(), - failed: z.number().int(), - total: z.number().int(), -}); - -export type AssetCriticalityBulkUploadResponse = z.infer; -export const AssetCriticalityBulkUploadResponse = z.object({ - errors: z.array(AssetCriticalityBulkUploadErrorItem), - stats: AssetCriticalityBulkUploadStats, -}); diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/common.schema.yaml b/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/common.schema.yaml index 3218ec07e0fe23..8d3e05ab59bac0 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/common.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/common.schema.yaml @@ -58,24 +58,6 @@ components: $ref: '#/components/schemas/AssetCriticalityLevel' required: - criticality_level - CreateSingleAssetCriticalityRequest: - allOf: - - $ref: '#/components/schemas/CreateAssetCriticalityRecord' - - type: object - properties: - refresh: - type: string - enum: [wait_for] - description: If 'wait_for' the request will wait for the index refresh. - DeleteAssetCriticalityRecord: - allOf: - - $ref: '#/components/schemas/AssetCriticalityRecordIdParts' - - type: object - properties: - refresh: - type: string - enum: [wait_for] - description: If 'wait_for' the request will wait for the index refresh. AssetCriticalityRecord: allOf: - $ref: '#/components/schemas/CreateAssetCriticalityRecord' @@ -88,46 +70,3 @@ components: description: The time the record was created or updated. required: - '@timestamp' - AssetCriticalityBulkUploadErrorItem: - type: object - properties: - message: - type: string - index: - type: integer - required: - - message - - index - AssetCriticalityBulkUploadStats: - type: object - properties: - successful: - type: integer - failed: - type: integer - total: - type: integer - required: - - successful - - failed - - total - AssetCriticalityBulkUploadResponse: - type: object - example: - errors: - - message: 'Invalid ID field' - index: 0 - stats: - successful: 1 - failed: 1 - total: 2 - properties: - errors: - type: array - items: - $ref: '#/components/schemas/AssetCriticalityBulkUploadErrorItem' - stats: - $ref: '#/components/schemas/AssetCriticalityBulkUploadStats' - required: - - errors - - stats diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/create_asset_criticality.gen.ts b/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/create_asset_criticality.gen.ts new file mode 100644 index 00000000000000..4836f4fe844ddb --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/create_asset_criticality.gen.ts @@ -0,0 +1,59 @@ +/* + * 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. + */ + +/* + * NOTICE: Do not edit this file manually. + * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator. + * + * info: + * title: Asset Criticality Create Record Schema + * version: 1 + */ + +import { z } from 'zod'; + +import { CreateAssetCriticalityRecord, AssetCriticalityRecord } from './common.gen'; + +export type CreateAssetCriticalityRecordRequestBody = z.infer< + typeof CreateAssetCriticalityRecordRequestBody +>; +export const CreateAssetCriticalityRecordRequestBody = CreateAssetCriticalityRecord.merge( + z.object({ + /** + * If 'wait_for' the request will wait for the index refresh. + */ + refresh: z.literal('wait_for').optional(), + }) +); +export type CreateAssetCriticalityRecordRequestBodyInput = z.input< + typeof CreateAssetCriticalityRecordRequestBody +>; + +export type CreateAssetCriticalityRecordResponse = z.infer< + typeof CreateAssetCriticalityRecordResponse +>; +export const CreateAssetCriticalityRecordResponse = AssetCriticalityRecord; + +export type InternalCreateAssetCriticalityRecordRequestBody = z.infer< + typeof InternalCreateAssetCriticalityRecordRequestBody +>; +export const InternalCreateAssetCriticalityRecordRequestBody = CreateAssetCriticalityRecord.merge( + z.object({ + /** + * If 'wait_for' the request will wait for the index refresh. + */ + refresh: z.literal('wait_for').optional(), + }) +); +export type InternalCreateAssetCriticalityRecordRequestBodyInput = z.input< + typeof InternalCreateAssetCriticalityRecordRequestBody +>; + +export type InternalCreateAssetCriticalityRecordResponse = z.infer< + typeof InternalCreateAssetCriticalityRecordResponse +>; +export const InternalCreateAssetCriticalityRecordResponse = AssetCriticalityRecord; diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/create_asset_criticality.schema.yaml b/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/create_asset_criticality.schema.yaml index d59ce99c8717c3..3d0bbf108d95f2 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/create_asset_criticality.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/create_asset_criticality.schema.yaml @@ -14,14 +14,23 @@ paths: post: x-labels: [ess, serverless] x-internal: true - operationId: AssetCriticalityCreateRecord + x-codegen-enabled: true + operationId: InternalCreateAssetCriticalityRecord summary: Deprecated Internal Create Criticality Record + deprecated: true requestBody: required: true content: application/json: schema: - $ref: './common.schema.yaml#/components/schemas/CreateSingleAssetCriticalityRequest' + allOf: + - $ref: './common.schema.yaml#/components/schemas/CreateAssetCriticalityRecord' + - type: object + properties: + refresh: + type: string + enum: [wait_for] + description: If 'wait_for' the request will wait for the index refresh. responses: '200': description: Successful response @@ -34,14 +43,22 @@ paths: /api/asset_criticality: post: x-labels: [ess, serverless] - operationId: AssetCriticalityCreateRecord + x-codegen-enabled: true + operationId: CreateAssetCriticalityRecord summary: Create Criticality Record requestBody: required: true content: application/json: schema: - $ref: './common.schema.yaml#/components/schemas/CreateSingleAssetCriticalityRequest' + allOf: + - $ref: './common.schema.yaml#/components/schemas/CreateAssetCriticalityRecord' + - type: object + properties: + refresh: + type: string + enum: [wait_for] + description: If 'wait_for' the request will wait for the index refresh. responses: '200': description: Successful response diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/delete_asset_criticality.gen.ts b/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/delete_asset_criticality.gen.ts new file mode 100644 index 00000000000000..fe290a67c6634e --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/delete_asset_criticality.gen.ts @@ -0,0 +1,61 @@ +/* + * 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. + */ + +/* + * NOTICE: Do not edit this file manually. + * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator. + * + * info: + * title: Asset Criticality Delete Record Schema + * version: 1 + */ + +import { z } from 'zod'; + +import { IdField } from './common.gen'; + +export type DeleteAssetCriticalityRecordRequestQuery = z.infer< + typeof DeleteAssetCriticalityRecordRequestQuery +>; +export const DeleteAssetCriticalityRecordRequestQuery = z.object({ + /** + * The ID value of the asset. + */ + id_value: z.string(), + /** + * The field representing the ID. + */ + id_field: IdField, + /** + * If 'wait_for' the request will wait for the index refresh. + */ + refresh: z.literal('wait_for').optional(), +}); +export type DeleteAssetCriticalityRecordRequestQueryInput = z.input< + typeof DeleteAssetCriticalityRecordRequestQuery +>; + +export type InternalDeleteAssetCriticalityRecordRequestQuery = z.infer< + typeof InternalDeleteAssetCriticalityRecordRequestQuery +>; +export const InternalDeleteAssetCriticalityRecordRequestQuery = z.object({ + /** + * The ID value of the asset. + */ + id_value: z.string(), + /** + * The field representing the ID. + */ + id_field: IdField, + /** + * If 'wait_for' the request will wait for the index refresh. + */ + refresh: z.literal('wait_for').optional(), +}); +export type InternalDeleteAssetCriticalityRecordRequestQueryInput = z.input< + typeof InternalDeleteAssetCriticalityRecordRequestQuery +>; diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/delete_asset_criticality.schema.yaml b/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/delete_asset_criticality.schema.yaml index 94e1cc82e15ad4..d66a2283596c0a 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/delete_asset_criticality.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/delete_asset_criticality.schema.yaml @@ -14,11 +14,31 @@ paths: delete: x-labels: [ess, serverless] x-internal: true - operationId: AssetCriticalityDeleteRecord + x-codegen-enabled: true + operationId: InternalDeleteAssetCriticalityRecord summary: Deprecated Internal Delete Criticality Record + deprecated: true parameters: - - $ref: './common.schema.yaml#/components/parameters/id_value' - - $ref: './common.schema.yaml#/components/parameters/id_field' + - name: id_value + in: query + required: true + schema: + type: string + description: The ID value of the asset. + - name: id_field + in: query + required: true + schema: + $ref: './common.schema.yaml#/components/schemas/IdField' + example: 'host.name' + description: The field representing the ID. + - name: refresh + in: query + required: false + schema: + type: string + enum: [wait_for] + description: If 'wait_for' the request will wait for the index refresh. responses: '200': description: Successful response @@ -27,11 +47,30 @@ paths: /api/asset_criticality: delete: x-labels: [ess, serverless] - operationId: AssetCriticalityDeleteRecord + x-codegen-enabled: true + operationId: DeleteAssetCriticalityRecord summary: Delete Criticality Record parameters: - - $ref: './common.schema.yaml#/components/parameters/id_value' - - $ref: './common.schema.yaml#/components/parameters/id_field' + - name: id_value + in: query + required: true + schema: + type: string + description: The ID value of the asset. + - name: id_field + in: query + required: true + schema: + $ref: './common.schema.yaml#/components/schemas/IdField' + example: 'host.name' + description: The field representing the ID. + - name: refresh + in: query + required: false + schema: + type: string + enum: [wait_for] + description: If 'wait_for' the request will wait for the index refresh. responses: '200': description: Successful response diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/get_asset_criticality.gen.ts b/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/get_asset_criticality.gen.ts new file mode 100644 index 00000000000000..7437960ef9cae9 --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/get_asset_criticality.gen.ts @@ -0,0 +1,39 @@ +/* + * 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. + */ + +/* + * NOTICE: Do not edit this file manually. + * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator. + * + * info: + * title: Asset Criticality Get Record Schema + * version: 1 + */ + +import { z } from 'zod'; + +import { IdField, AssetCriticalityRecord } from './common.gen'; + +export type GetAssetCriticalityRecordRequestQuery = z.infer< + typeof GetAssetCriticalityRecordRequestQuery +>; +export const GetAssetCriticalityRecordRequestQuery = z.object({ + /** + * The ID value of the asset. + */ + id_value: z.string(), + /** + * The field representing the ID. + */ + id_field: IdField, +}); +export type GetAssetCriticalityRecordRequestQueryInput = z.input< + typeof GetAssetCriticalityRecordRequestQuery +>; + +export type GetAssetCriticalityRecordResponse = z.infer; +export const GetAssetCriticalityRecordResponse = AssetCriticalityRecord; diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/get_asset_criticality.schema.yaml b/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/get_asset_criticality.schema.yaml index 56f3e37de11264..ca2784c48653de 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/get_asset_criticality.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/get_asset_criticality.schema.yaml @@ -14,11 +14,23 @@ paths: get: x-labels: [ess, serverless] x-internal: true - operationId: AssetCriticalityGetRecord + operationId: InternalGetAssetCriticalityRecord summary: Deprecated Internal Get Criticality Record + deprecated: true parameters: - - $ref: './common.schema.yaml#/components/parameters/id_value' - - $ref: './common.schema.yaml#/components/parameters/id_field' + - name: id_value + in: query + required: true + schema: + type: string + description: The ID value of the asset. + - name: id_field + in: query + required: true + schema: + $ref: './common.schema.yaml#/components/schemas/IdField' + example: 'host.name' + description: The field representing the ID. responses: '200': description: Successful response @@ -33,11 +45,23 @@ paths: /api/asset_criticality: get: x-labels: [ess, serverless] - operationId: AssetCriticalityGetRecord + x-codegen-enabled: true + operationId: GetAssetCriticalityRecord summary: Get Criticality Record parameters: - - $ref: './common.schema.yaml#/components/parameters/id_value' - - $ref: './common.schema.yaml#/components/parameters/id_field' + - name: id_value + in: query + required: true + schema: + type: string + description: The ID value of the asset. + - name: id_field + in: query + required: true + schema: + $ref: './common.schema.yaml#/components/schemas/IdField' + example: 'host.name' + description: The field representing the ID. responses: '200': description: Successful response diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/get_asset_criticality_status.gen.ts b/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/get_asset_criticality_status.gen.ts index bb51693825def0..f9d24b61bbef0e 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/get_asset_criticality_status.gen.ts +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/get_asset_criticality_status.gen.ts @@ -16,7 +16,7 @@ import { z } from 'zod'; -export type AssetCriticalityStatusResponse = z.infer; -export const AssetCriticalityStatusResponse = z.object({ +export type GetAssetCriticalityStatusResponse = z.infer; +export const GetAssetCriticalityStatusResponse = z.object({ asset_criticality_resources_installed: z.boolean().optional(), }); diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/get_asset_criticality_status.schema.yaml b/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/get_asset_criticality_status.schema.yaml index 4052ad8f071771..f8f5dcb7c8ecd6 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/get_asset_criticality_status.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/get_asset_criticality_status.schema.yaml @@ -14,7 +14,8 @@ paths: get: x-labels: [ess, serverless] x-internal: true - operationId: AssetCriticalityGetStatus + x-codegen-enabled: true + operationId: GetAssetCriticalityStatus summary: Get Asset Criticality Status responses: '200': @@ -22,14 +23,9 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/AssetCriticalityStatusResponse' + type: object + properties: + asset_criticality_resources_installed: + type: boolean '400': description: Invalid request - -components: - schemas: - AssetCriticalityStatusResponse: - type: object - properties: - asset_criticality_resources_installed: - type: boolean diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/index.ts b/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/index.ts index 326a20d6c66a72..fb99a69f49f921 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/index.ts +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/index.ts @@ -9,5 +9,5 @@ export * from './common.gen'; export * from './get_asset_criticality_status.gen'; export * from './get_asset_criticality_privileges.gen'; export * from './bulk_upload_asset_criticality.gen'; +export * from './upload_asset_criticality_csv.gen'; export * from './list_asset_criticality.gen'; -export * from './list_asset_criticality_query_params'; diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/list_asset_criticality.gen.ts b/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/list_asset_criticality.gen.ts index 9cf2f7ca7c628b..e17a2b006896c7 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/list_asset_criticality.gen.ts +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/list_asset_criticality.gen.ts @@ -18,8 +18,39 @@ import { z } from 'zod'; import { AssetCriticalityRecord } from './common.gen'; -export type AssetCriticalityListResponse = z.infer; -export const AssetCriticalityListResponse = z.object({ +export type FindAssetCriticalityRecordsRequestQuery = z.infer< + typeof FindAssetCriticalityRecordsRequestQuery +>; +export const FindAssetCriticalityRecordsRequestQuery = z.object({ + /** + * The field to sort by. + */ + sort_field: z.enum(['id_value', 'id_field', 'criticality_level', '@timestamp']).optional(), + /** + * The order to sort by. + */ + sort_direction: z.enum(['asc', 'desc']).optional(), + /** + * The page number to return. + */ + page: z.coerce.number().int().min(1).optional(), + /** + * The number of records to return per page. + */ + per_page: z.coerce.number().int().min(1).max(1000).optional(), + /** + * The kuery to filter by. + */ + kuery: z.string().optional(), +}); +export type FindAssetCriticalityRecordsRequestQueryInput = z.input< + typeof FindAssetCriticalityRecordsRequestQuery +>; + +export type FindAssetCriticalityRecordsResponse = z.infer< + typeof FindAssetCriticalityRecordsResponse +>; +export const FindAssetCriticalityRecordsResponse = z.object({ records: z.array(AssetCriticalityRecord), page: z.number().int().min(1), per_page: z.number().int().min(1).max(1000), diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/list_asset_criticality.schema.yaml b/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/list_asset_criticality.schema.yaml index 7c9a28c4eeaaf4..34c5b98a4617fe 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/list_asset_criticality.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/list_asset_criticality.schema.yaml @@ -13,6 +13,8 @@ paths: /api/asset_criticality/list: post: x-labels: [ess, serverless] + x-codegen-enabled: true + operationId: FindAssetCriticalityRecords summary: List asset criticality data, filtering and sorting as needed parameters: - name: sort_field @@ -26,7 +28,7 @@ paths: - criticality_level - \@timestamp description: The field to sort by. - - name: sort_order + - name: sort_direction in: query required: false schema: @@ -62,31 +64,24 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/AssetCriticalityListResponse' - -components: - schemas: - AssetCriticalityListResponse: - type: object - properties: - records: - type: array - items: - $ref: './common.schema.yaml#/components/schemas/AssetCriticalityRecord' - page: - type: integer - minimum: 1 - per_page: - type: integer - minimum: 1 - maximum: 1000 - total: - type: integer - minimum: 0 - required: - - records - - page - - per_page - - total - - \ No newline at end of file + type: object + properties: + records: + type: array + items: + $ref: './common.schema.yaml#/components/schemas/AssetCriticalityRecord' + page: + type: integer + minimum: 1 + per_page: + type: integer + minimum: 1 + maximum: 1000 + total: + type: integer + minimum: 0 + required: + - records + - page + - per_page + - total diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/list_asset_criticality_query_params.ts b/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/list_asset_criticality_query_params.ts deleted file mode 100644 index b70393056c48ff..00000000000000 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/list_asset_criticality_query_params.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * 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 { z } from 'zod'; - -export const ListAssetCriticalityQueryParams = z.object({ - page: z.coerce.number().min(1).optional(), - per_page: z.coerce.number().min(1).max(10000).optional(), - sort_field: z.enum(['id_field', 'id_value', '@timestamp', 'criticality_level']).optional(), - sort_direction: z.enum(['asc', 'desc']).optional(), - kuery: z.string().optional(), -}); - -export type ListAssetCriticalityQueryParams = z.infer; diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/upload_asset_criticality_csv.gen.ts b/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/upload_asset_criticality_csv.gen.ts new file mode 100644 index 00000000000000..4282056378426e --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/upload_asset_criticality_csv.gen.ts @@ -0,0 +1,46 @@ +/* + * 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. + */ + +/* + * NOTICE: Do not edit this file manually. + * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator. + * + * info: + * title: Asset Criticality CSV Upload Schema + * version: 1 + */ + +import { z } from 'zod'; + +export type AssetCriticalityCsvUploadErrorItem = z.infer; +export const AssetCriticalityCsvUploadErrorItem = z.object({ + message: z.string(), + index: z.number().int(), +}); + +export type AssetCriticalityCsvUploadStats = z.infer; +export const AssetCriticalityCsvUploadStats = z.object({ + successful: z.number().int(), + failed: z.number().int(), + total: z.number().int(), +}); + +export type InternalUploadAssetCriticalityRecordsResponse = z.infer< + typeof InternalUploadAssetCriticalityRecordsResponse +>; +export const InternalUploadAssetCriticalityRecordsResponse = z.object({ + errors: z.array(AssetCriticalityCsvUploadErrorItem), + stats: AssetCriticalityCsvUploadStats, +}); + +export type UploadAssetCriticalityRecordsResponse = z.infer< + typeof UploadAssetCriticalityRecordsResponse +>; +export const UploadAssetCriticalityRecordsResponse = z.object({ + errors: z.array(AssetCriticalityCsvUploadErrorItem), + stats: AssetCriticalityCsvUploadStats, +}); diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/upload_asset_criticality_csv.schema.yaml b/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/upload_asset_criticality_csv.schema.yaml index c348dcefa8b781..77e78f5c6d4d34 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/upload_asset_criticality_csv.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/upload_asset_criticality_csv.schema.yaml @@ -14,7 +14,10 @@ paths: post: x-labels: [ess, serverless] x-internal: true + x-codegen-enabled: true + operationId: InternalUploadAssetCriticalityRecords summary: Deprecated internal API which Uploads a CSV file containing asset criticality data + deprecated: true requestBody: content: multipart/form-data: @@ -33,13 +36,33 @@ paths: content: application/json: schema: - $ref: '#./common/components/schemas/AssetCriticalityBulkUploadResponse' + type: object + example: + errors: + - message: 'Invalid ID field' + index: 0 + stats: + successful: 1 + failed: 1 + total: 2 + properties: + errors: + type: array + items: + $ref: '#/components/schemas/AssetCriticalityCsvUploadErrorItem' + stats: + $ref: '#/components/schemas/AssetCriticalityCsvUploadStats' + required: + - errors + - stats '413': description: File too large /api/asset_criticality/upload_csv: post: x-labels: [ess, serverless] x-internal: true + x-codegen-enabled: true + operationId: UploadAssetCriticalityRecords summary: Uploads a CSV file containing asset criticality data requestBody: content: @@ -59,6 +82,51 @@ paths: content: application/json: schema: - $ref: '#./common/components/schemas/AssetCriticalityBulkUploadResponse' + type: object + example: + errors: + - message: 'Invalid ID field' + index: 0 + stats: + successful: 1 + failed: 1 + total: 2 + properties: + errors: + type: array + items: + $ref: '#/components/schemas/AssetCriticalityCsvUploadErrorItem' + stats: + $ref: '#/components/schemas/AssetCriticalityCsvUploadStats' + required: + - errors + - stats '413': description: File too large + +components: + schemas: + AssetCriticalityCsvUploadErrorItem: + type: object + properties: + message: + type: string + index: + type: integer + required: + - message + - index + + AssetCriticalityCsvUploadStats: + type: object + properties: + successful: + type: integer + failed: + type: integer + total: + type: integer + required: + - successful + - failed + - total diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_disable_route.gen.ts b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_disable_route.gen.ts index 620620c95b888a..b50eb00db63013 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_disable_route.gen.ts +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_disable_route.gen.ts @@ -16,13 +16,13 @@ import { z } from 'zod'; -export type RiskEngineDisableResponse = z.infer; -export const RiskEngineDisableResponse = z.object({ - success: z.boolean().optional(), -}); - export type RiskEngineDisableErrorResponse = z.infer; export const RiskEngineDisableErrorResponse = z.object({ message: z.string(), full_error: z.string(), }); + +export type DisableRiskEngineResponse = z.infer; +export const DisableRiskEngineResponse = z.object({ + success: z.boolean().optional(), +}); diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_disable_route.schema.yaml b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_disable_route.schema.yaml index 33f35aa1bef1b7..c491ec74e2a50b 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_disable_route.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_disable_route.schema.yaml @@ -18,6 +18,8 @@ paths: post: x-labels: [ess, serverless] x-internal: true + x-codegen-enabled: true + operationId: DisableRiskEngine summary: Disable the Risk Engine requestBody: content: @@ -28,7 +30,10 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/RiskEngineDisableResponse' + type: object + properties: + success: + type: boolean '400': description: Task manager is unavailable content: @@ -44,11 +49,6 @@ paths: components: schemas: - RiskEngineDisableResponse: - type: object - properties: - success: - type: boolean RiskEngineDisableErrorResponse: type: object required: diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_enable_route.gen.ts b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_enable_route.gen.ts index cee1121b778aec..7bdbfd17449db3 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_enable_route.gen.ts +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_enable_route.gen.ts @@ -16,13 +16,13 @@ import { z } from 'zod'; -export type RiskEngineEnableResponse = z.infer; -export const RiskEngineEnableResponse = z.object({ - success: z.boolean().optional(), -}); - -export type RiskEngineEnableErrorResponse = z.infer; -export const RiskEngineEnableErrorResponse = z.object({ +export type EnableRiskEngineErrorResponse = z.infer; +export const EnableRiskEngineErrorResponse = z.object({ message: z.string(), full_error: z.string(), }); + +export type EnableRiskEngineResponse = z.infer; +export const EnableRiskEngineResponse = z.object({ + success: z.boolean().optional(), +}); diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_enable_route.schema.yaml b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_enable_route.schema.yaml index 5cfd5ffdd4fdf7..6b2656bbb21b05 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_enable_route.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_enable_route.schema.yaml @@ -18,6 +18,8 @@ paths: post: x-labels: [ess, serverless] x-internal: true + x-codegen-enabled: true + operationId: EnableRiskEngine summary: Enable the Risk Engine requestBody: content: @@ -28,7 +30,10 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/RiskEngineEnableResponse' + type: object + properties: + success: + type: boolean '400': description: Task manager is unavailable content: @@ -40,16 +45,11 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/RiskEngineEnableErrorResponse' + $ref: '#/components/schemas/EnableRiskEngineErrorResponse' components: schemas: - RiskEngineEnableResponse: - type: object - properties: - success: - type: boolean - RiskEngineEnableErrorResponse: + EnableRiskEngineErrorResponse: type: object required: - message diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_init_route.gen.ts b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_init_route.gen.ts index d973a435b9aec7..f9d79cd8f96a60 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_init_route.gen.ts +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_init_route.gen.ts @@ -16,8 +16,8 @@ import { z } from 'zod'; -export type RiskEngineInitResult = z.infer; -export const RiskEngineInitResult = z.object({ +export type InitRiskEngineResult = z.infer; +export const InitRiskEngineResult = z.object({ risk_engine_enabled: z.boolean(), risk_engine_resources_installed: z.boolean(), risk_engine_configuration_created: z.boolean(), @@ -25,13 +25,13 @@ export const RiskEngineInitResult = z.object({ errors: z.array(z.string()), }); -export type RiskEngineInitResponse = z.infer; -export const RiskEngineInitResponse = z.object({ - result: RiskEngineInitResult, -}); - -export type RiskEngineInitErrorResponse = z.infer; -export const RiskEngineInitErrorResponse = z.object({ +export type InitRiskEngineErrorResponse = z.infer; +export const InitRiskEngineErrorResponse = z.object({ message: z.string(), full_error: z.string(), }); + +export type InitRiskEngineResponse = z.infer; +export const InitRiskEngineResponse = z.object({ + result: InitRiskEngineResult, +}); diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_init_route.schema.yaml b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_init_route.schema.yaml index 498ac266a9aa01..d1d35f4a720c67 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_init_route.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_init_route.schema.yaml @@ -16,6 +16,8 @@ paths: post: x-labels: [ess, serverless] x-internal: true + x-codegen-enabled: true + operationId: InitRiskEngine summary: Initialize the Risk Engine description: Initializes the Risk Engine by creating the necessary indices and mappings, removing old transforms, and starting the new risk engine responses: @@ -24,7 +26,12 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/RiskEngineInitResponse' + type: object + required: + - result + properties: + result: + $ref: '#/components/schemas/InitRiskEngineResult' '400': description: Task manager is unavailable content: @@ -36,11 +43,11 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/RiskEngineInitErrorResponse' + $ref: '#/components/schemas/InitRiskEngineErrorResponse' components: schemas: - RiskEngineInitResult: + InitRiskEngineResult: type: object required: - risk_engine_enabled @@ -62,15 +69,7 @@ components: items: type: string - RiskEngineInitResponse: - type: object - required: - - result - properties: - result: - $ref: '#/components/schemas/RiskEngineInitResult' - - RiskEngineInitErrorResponse: + InitRiskEngineErrorResponse: type: object required: - message diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_settings_route.gen.ts b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_settings_route.gen.ts index c8d10bd87d75e9..e01edb31397a68 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_settings_route.gen.ts +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_settings_route.gen.ts @@ -18,7 +18,7 @@ import { z } from 'zod'; import { DateRange } from '../common/common.gen'; -export type RiskEngineSettingsResponse = z.infer; -export const RiskEngineSettingsResponse = z.object({ +export type ReadRiskEngineSettingsResponse = z.infer; +export const ReadRiskEngineSettingsResponse = z.object({ range: DateRange.optional(), }); diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_settings_route.schema.yaml b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_settings_route.schema.yaml index 3622a9ff7c62bf..a5cc6d6b44008b 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_settings_route.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_settings_route.schema.yaml @@ -16,7 +16,8 @@ paths: get: x-labels: [ess, serverless] x-internal: true - operationId: RiskEngineSettingsGet + x-codegen-enabled: true + operationId: ReadRiskEngineSettings summary: Get the settings of the Risk Engine responses: '200': @@ -24,12 +25,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/RiskEngineSettingsResponse' - -components: - schemas: - RiskEngineSettingsResponse: - type: object - properties: - range: - $ref: '../common/common.schema.yaml#/components/schemas/DateRange' + type: object + properties: + range: + $ref: '../common/common.schema.yaml#/components/schemas/DateRange' diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_status_route.gen.ts b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_status_route.gen.ts index 6a6e15d9c71a35..0d3fd0b9f0dd44 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_status_route.gen.ts +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_status_route.gen.ts @@ -30,3 +30,6 @@ export const RiskEngineStatusResponse = z.object({ */ is_max_amount_of_risk_engines_reached: z.boolean(), }); + +export type GetRiskEngineStatusResponse = z.infer; +export const GetRiskEngineStatusResponse = RiskEngineStatusResponse; diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_status_route.schema.yaml b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_status_route.schema.yaml index 3f1cc33e942887..57f46b99f3a772 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_status_route.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_status_route.schema.yaml @@ -16,6 +16,8 @@ paths: get: x-labels: [ess, serverless] x-internal: true + x-codegen-enabled: true + operationId: GetRiskEngineStatus summary: Get the status of the Risk Engine description: Returns the status of both the legacy transform-based risk engine, as well as the new risk engine responses: diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/entity_calculation_route.gen.ts b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/entity_calculation_route.gen.ts index c9b6c8cc47aa3d..ebefbd772ed96a 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/entity_calculation_route.gen.ts +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/entity_calculation_route.gen.ts @@ -41,3 +41,29 @@ export const RiskScoresEntityCalculationResponse = z.object({ success: z.boolean(), score: EntityRiskScoreRecord.optional(), }); + +export type DeprecatedTriggerRiskScoreCalculationRequestBody = z.infer< + typeof DeprecatedTriggerRiskScoreCalculationRequestBody +>; +export const DeprecatedTriggerRiskScoreCalculationRequestBody = RiskScoresEntityCalculationRequest; +export type DeprecatedTriggerRiskScoreCalculationRequestBodyInput = z.input< + typeof DeprecatedTriggerRiskScoreCalculationRequestBody +>; + +export type DeprecatedTriggerRiskScoreCalculationResponse = z.infer< + typeof DeprecatedTriggerRiskScoreCalculationResponse +>; +export const DeprecatedTriggerRiskScoreCalculationResponse = RiskScoresEntityCalculationResponse; + +export type TriggerRiskScoreCalculationRequestBody = z.infer< + typeof TriggerRiskScoreCalculationRequestBody +>; +export const TriggerRiskScoreCalculationRequestBody = RiskScoresEntityCalculationRequest; +export type TriggerRiskScoreCalculationRequestBodyInput = z.input< + typeof TriggerRiskScoreCalculationRequestBody +>; + +export type TriggerRiskScoreCalculationResponse = z.infer< + typeof TriggerRiskScoreCalculationResponse +>; +export const TriggerRiskScoreCalculationResponse = RiskScoresEntityCalculationResponse; diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/entity_calculation_route.schema.yaml b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/entity_calculation_route.schema.yaml index bb943052548851..69be93f7ceb493 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/entity_calculation_route.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/entity_calculation_route.schema.yaml @@ -19,8 +19,11 @@ paths: post: x-labels: [ess, serverless] x-internal: true + x-codegen-enabled: true + operationId: DeprecatedTriggerRiskScoreCalculation summary: Deprecated Trigger calculation of Risk Scores for an entity. Moved to /internal/risk_score/calculation/entity description: Calculates and persists Risk Scores for an entity, returning the calculated risk score. + deprecated: true requestBody: description: The entity type and identifier content: @@ -41,6 +44,8 @@ paths: /internal/risk_score/calculation/entity: post: x-labels: [ess, serverless] + x-codegen-enabled: true + operationId: TriggerRiskScoreCalculation summary: Trigger calculation of Risk Scores for an entity description: Calculates and persists Risk Scores for an entity, returning the calculated risk score. requestBody: diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/preview_route.gen.ts b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/preview_route.gen.ts index fe0b90e5a2e7a9..13515d239c81cc 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/preview_route.gen.ts +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/preview_route.gen.ts @@ -83,3 +83,10 @@ export const RiskScoresPreviewResponse = z.object({ user: z.array(EntityRiskScoreRecord).optional(), }), }); + +export type PreviewRiskScoreRequestBody = z.infer; +export const PreviewRiskScoreRequestBody = RiskScoresPreviewRequest; +export type PreviewRiskScoreRequestBodyInput = z.input; + +export type PreviewRiskScoreResponse = z.infer; +export const PreviewRiskScoreResponse = RiskScoresPreviewResponse; diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/preview_route.schema.yaml b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/preview_route.schema.yaml index a2ce9bcafd6971..424ca98436768b 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/preview_route.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/preview_route.schema.yaml @@ -16,6 +16,8 @@ paths: post: x-labels: [ess, serverless] x-internal: true + x-codegen-enabled: true + operationId: PreviewRiskScore summary: Preview the calculation of Risk Scores description: Calculates and returns a list of Risk Scores, sorted by identifier_type and risk score. requestBody: diff --git a/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_detections_api_2023_10_31.bundled.schema.yaml b/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_detections_api_2023_10_31.bundled.schema.yaml index 6cb21c69c04921..4fd2ec1aed3b69 100644 --- a/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_detections_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_detections_api_2023_10_31.bundled.schema.yaml @@ -91,6 +91,7 @@ paths: application/json: schema: $ref: '#/components/schemas/SiemErrorResponse' + description: Not found '500': content: application/json: @@ -131,6 +132,7 @@ paths: application/json: schema: $ref: '#/components/schemas/SiemErrorResponse' + description: Not found '500': content: application/json: diff --git a/x-pack/plugins/security_solution/public/entity_analytics/api/api.ts b/x-pack/plugins/security_solution/public/entity_analytics/api/api.ts index aa3b432533027c..500c327d86b0c2 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/api/api.ts +++ b/x-pack/plugins/security_solution/public/entity_analytics/api/api.ts @@ -6,10 +6,11 @@ */ import { useMemo } from 'react'; -import type { RiskEngineDisableResponse } from '../../../common/api/entity_analytics/risk_engine/engine_disable_route.gen'; +import type { UploadAssetCriticalityRecordsResponse } from '../../../common/api/entity_analytics/asset_criticality/upload_asset_criticality_csv.gen'; +import type { DisableRiskEngineResponse } from '../../../common/api/entity_analytics/risk_engine/engine_disable_route.gen'; import type { RiskEngineStatusResponse } from '../../../common/api/entity_analytics/risk_engine/engine_status_route.gen'; -import type { RiskEngineInitResponse } from '../../../common/api/entity_analytics/risk_engine/engine_init_route.gen'; -import type { RiskEngineEnableResponse } from '../../../common/api/entity_analytics/risk_engine/engine_enable_route.gen'; +import type { InitRiskEngineResponse } from '../../../common/api/entity_analytics/risk_engine/engine_init_route.gen'; +import type { EnableRiskEngineResponse } from '../../../common/api/entity_analytics/risk_engine/engine_enable_route.gen'; import type { RiskScoresPreviewRequest, RiskScoresPreviewResponse, @@ -18,7 +19,6 @@ import type { RiskScoresEntityCalculationRequest, RiskScoresEntityCalculationResponse, } from '../../../common/api/entity_analytics/risk_engine/entity_calculation_route.gen'; -import type { AssetCriticalityBulkUploadResponse } from '../../../common/entity_analytics/asset_criticality/types'; import type { AssetCriticalityRecord, EntityAnalyticsPrivileges, @@ -39,9 +39,9 @@ import { RISK_SCORE_ENTITY_CALCULATION_URL, API_VERSIONS, } from '../../../common/constants'; -import type { RiskEngineSettingsResponse } from '../../../common/api/entity_analytics/risk_engine'; import type { SnakeToCamelCase } from '../common/utils'; import { useKibana } from '../../common/lib/kibana/kibana_react'; +import type { ReadRiskEngineSettingsResponse } from '../../../common/api/entity_analytics/risk_engine'; export interface DeleteAssetCriticalityResponse { deleted: true; @@ -81,7 +81,7 @@ export const useEntityAnalyticsRoutes = () => { * Init risk score engine */ const initRiskEngine = () => - http.fetch(RISK_ENGINE_INIT_URL, { + http.fetch(RISK_ENGINE_INIT_URL, { version: '1', method: 'POST', }); @@ -90,7 +90,7 @@ export const useEntityAnalyticsRoutes = () => { * Enable risk score engine */ const enableRiskEngine = () => - http.fetch(RISK_ENGINE_ENABLE_URL, { + http.fetch(RISK_ENGINE_ENABLE_URL, { version: '1', method: 'POST', }); @@ -99,7 +99,7 @@ export const useEntityAnalyticsRoutes = () => { * Disable risk score engine */ const disableRiskEngine = () => - http.fetch(RISK_ENGINE_DISABLE_URL, { + http.fetch(RISK_ENGINE_DISABLE_URL, { version: '1', method: 'POST', }); @@ -181,12 +181,12 @@ export const useEntityAnalyticsRoutes = () => { const uploadAssetCriticalityFile = async ( fileContent: string, fileName: string - ): Promise => { + ): Promise => { const file = new File([new Blob([fileContent])], fileName, { type: 'text/csv' }); const body = new FormData(); body.append('file', file); - return http.fetch( + return http.fetch( ASSET_CRITICALITY_PUBLIC_CSV_UPLOAD_URL, { version: API_VERSIONS.public.v1, @@ -224,7 +224,7 @@ export const useEntityAnalyticsRoutes = () => { * Fetches risk engine settings */ const fetchRiskEngineSettings = () => - http.fetch(RISK_ENGINE_SETTINGS_URL, { + http.fetch(RISK_ENGINE_SETTINGS_URL, { version: '1', method: 'GET', }); diff --git a/x-pack/plugins/security_solution/public/entity_analytics/api/hooks/use_disable_risk_engine_mutation.ts b/x-pack/plugins/security_solution/public/entity_analytics/api/hooks/use_disable_risk_engine_mutation.ts index e19cf94fc379f7..fb8a0bbb12972c 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/api/hooks/use_disable_risk_engine_mutation.ts +++ b/x-pack/plugins/security_solution/public/entity_analytics/api/hooks/use_disable_risk_engine_mutation.ts @@ -9,7 +9,7 @@ import { useMutation } from '@tanstack/react-query'; import type { TaskManagerUnavailableResponse } from '../../../../common/api/entity_analytics/common'; import type { RiskEngineDisableErrorResponse, - RiskEngineDisableResponse, + DisableRiskEngineResponse, } from '../../../../common/api/entity_analytics/risk_engine/engine_disable_route.gen'; import { useEntityAnalyticsRoutes } from '../api'; import { useInvalidateRiskEngineStatusQuery } from './use_risk_engine_status'; @@ -21,7 +21,7 @@ export const useDisableRiskEngineMutation = (options?: UseMutationOptions<{}>) = const { disableRiskEngine } = useEntityAnalyticsRoutes(); return useMutation< - RiskEngineDisableResponse, + DisableRiskEngineResponse, { body: RiskEngineDisableErrorResponse | TaskManagerUnavailableResponse } >(() => disableRiskEngine(), { ...options, diff --git a/x-pack/plugins/security_solution/public/entity_analytics/api/hooks/use_enable_risk_engine_mutation.ts b/x-pack/plugins/security_solution/public/entity_analytics/api/hooks/use_enable_risk_engine_mutation.ts index 658c4a5cdb185e..cd5083d13892ec 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/api/hooks/use_enable_risk_engine_mutation.ts +++ b/x-pack/plugins/security_solution/public/entity_analytics/api/hooks/use_enable_risk_engine_mutation.ts @@ -8,8 +8,8 @@ import type { UseMutationOptions } from '@tanstack/react-query'; import { useMutation } from '@tanstack/react-query'; import type { TaskManagerUnavailableResponse } from '../../../../common/api/entity_analytics/common'; import type { - RiskEngineEnableErrorResponse, - RiskEngineEnableResponse, + EnableRiskEngineErrorResponse, + EnableRiskEngineResponse, } from '../../../../common/api/entity_analytics/risk_engine/engine_enable_route.gen'; import { useEntityAnalyticsRoutes } from '../api'; import { useInvalidateRiskEngineStatusQuery } from './use_risk_engine_status'; @@ -19,8 +19,8 @@ export const useEnableRiskEngineMutation = (options?: UseMutationOptions<{}>) => const invalidateRiskEngineStatusQuery = useInvalidateRiskEngineStatusQuery(); const { enableRiskEngine } = useEntityAnalyticsRoutes(); return useMutation< - RiskEngineEnableResponse, - { body: RiskEngineEnableErrorResponse | TaskManagerUnavailableResponse } + EnableRiskEngineResponse, + { body: EnableRiskEngineErrorResponse | TaskManagerUnavailableResponse } >(enableRiskEngine, { ...options, mutationKey: ENABLE_RISK_ENGINE_MUTATION_KEY, diff --git a/x-pack/plugins/security_solution/public/entity_analytics/api/hooks/use_init_risk_engine_mutation.ts b/x-pack/plugins/security_solution/public/entity_analytics/api/hooks/use_init_risk_engine_mutation.ts index 67d94257e91659..d774853c7d026e 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/api/hooks/use_init_risk_engine_mutation.ts +++ b/x-pack/plugins/security_solution/public/entity_analytics/api/hooks/use_init_risk_engine_mutation.ts @@ -6,11 +6,11 @@ */ import type { UseMutationOptions } from '@tanstack/react-query'; import { useMutation } from '@tanstack/react-query'; -import type { TaskManagerUnavailableResponse } from '../../../../common/api/entity_analytics/common'; import type { - RiskEngineInitErrorResponse, - RiskEngineInitResponse, + InitRiskEngineErrorResponse, + InitRiskEngineResponse, } from '../../../../common/api/entity_analytics/risk_engine/engine_init_route.gen'; +import type { TaskManagerUnavailableResponse } from '../../../../common/api/entity_analytics/common'; import { useEntityAnalyticsRoutes } from '../api'; import { useInvalidateRiskEngineStatusQuery } from './use_risk_engine_status'; @@ -21,8 +21,8 @@ export const useInitRiskEngineMutation = (options?: UseMutationOptions<{}>) => { const { initRiskEngine } = useEntityAnalyticsRoutes(); return useMutation< - RiskEngineInitResponse, - { body: RiskEngineInitErrorResponse | TaskManagerUnavailableResponse } + InitRiskEngineResponse, + { body: InitRiskEngineErrorResponse | TaskManagerUnavailableResponse } >(() => initRiskEngine(), { ...options, mutationKey: INIT_RISK_ENGINE_STATUS_KEY, diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/components/result_step.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/components/result_step.tsx index 1652c85eace1f8..c3be648103d7fc 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/components/result_step.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/components/result_step.tsx @@ -18,11 +18,11 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; import { css } from '@emotion/react'; -import type { AssetCriticalityBulkUploadResponse } from '../../../../../common/entity_analytics/asset_criticality/types'; +import type { BulkUpsertAssetCriticalityRecordsResponse } from '../../../../../common/entity_analytics/asset_criticality/types'; import { buildAnnotationsFromError } from '../helpers'; export const AssetCriticalityResultStep: React.FC<{ - result?: AssetCriticalityBulkUploadResponse; + result?: BulkUpsertAssetCriticalityRecordsResponse; validLinesAsText: string; errorMessage?: string; onReturn: () => void; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/reducer.test.ts b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/reducer.test.ts index 60b6191a777d61..3fa2eb89e5d650 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/reducer.test.ts +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/reducer.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { AssetCriticalityBulkUploadResponse } from '../../../../common/api/entity_analytics'; +import type { UploadAssetCriticalityRecordsResponse } from '../../../../common/api/entity_analytics'; import type { ReducerAction, ReducerState, ValidationStepState } from './reducer'; import { reducer } from './reducer'; import { FileUploaderSteps } from './types'; @@ -43,7 +43,7 @@ describe('reducer', () => { }); it('should handle "fileUploaded" action with response', () => { - const response: AssetCriticalityBulkUploadResponse = { + const response: UploadAssetCriticalityRecordsResponse = { errors: [], stats: { total: 10, diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/reducer.ts b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/reducer.ts index e7f233015434f1..eb0153d2618713 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/reducer.ts +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/reducer.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { AssetCriticalityBulkUploadResponse } from '../../../../common/entity_analytics/asset_criticality/types'; +import type { UploadAssetCriticalityRecordsResponse } from '../../../../common/api/entity_analytics'; import { FileUploaderSteps } from './types'; import type { ValidatedFile } from './types'; import { isFilePickerStep, isValidationStep } from './helpers'; @@ -26,7 +26,7 @@ export interface ValidationStepState { export interface ResultStepState { step: FileUploaderSteps.RESULT; - fileUploadResponse?: AssetCriticalityBulkUploadResponse; + fileUploadResponse?: UploadAssetCriticalityRecordsResponse; fileUploadError?: string; validLinesAsText: string; } @@ -46,7 +46,7 @@ export type ReducerAction = | { type: 'uploadingFile' } | { type: 'fileUploaded'; - payload: { response?: AssetCriticalityBulkUploadResponse; errorMessage?: string }; + payload: { response?: UploadAssetCriticalityRecordsResponse; errorMessage?: string }; }; export const INITIAL_STATE: FilePickerState = { diff --git a/x-pack/plugins/security_solution/scripts/openapi/generate.js b/x-pack/plugins/security_solution/scripts/openapi/generate.js index 38eb0fe06f95ae..adfe11192ae494 100644 --- a/x-pack/plugins/security_solution/scripts/openapi/generate.js +++ b/x-pack/plugins/security_solution/scripts/openapi/generate.js @@ -18,7 +18,6 @@ const SECURITY_SOLUTION_ROOT = resolve(__dirname, '../..'); rootDir: SECURITY_SOLUTION_ROOT, sourceGlob: './common/**/*.schema.yaml', templateName: 'zod_operation_schema', - skipLinting: true, }); await generate({ diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/asset_criticality_data_client.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/asset_criticality_data_client.ts index ac22303c09af69..4770d051f2e99a 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/asset_criticality_data_client.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/asset_criticality_data_client.ts @@ -11,7 +11,7 @@ import { mappingFromFieldMap } from '@kbn/alerting-plugin/common'; import type { AuditLogger } from '@kbn/security-plugin-types-server'; import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; import type { - AssetCriticalityBulkUploadResponse, + BulkUpsertAssetCriticalityRecordsResponse, AssetCriticalityUpsert, } from '../../../../common/entity_analytics/asset_criticality/types'; import type { AssetCriticalityRecord } from '../../../../common/api/entity_analytics'; @@ -211,9 +211,9 @@ export class AssetCriticalityDataClient { recordsStream, flushBytes, retries, - }: BulkUpsertFromStreamOptions): Promise => { - const errors: AssetCriticalityBulkUploadResponse['errors'] = []; - const stats: AssetCriticalityBulkUploadResponse['stats'] = { + }: BulkUpsertFromStreamOptions): Promise => { + const errors: BulkUpsertAssetCriticalityRecordsResponse['errors'] = []; + const stats: BulkUpsertAssetCriticalityRecordsResponse['stats'] = { successful: 0, failed: 0, total: 0, diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/bulk_upload.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/bulk_upload.ts index e1eb6872d3a337..822c8a644d9b3c 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/bulk_upload.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/bulk_upload.ts @@ -9,8 +9,8 @@ import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils'; import { transformError } from '@kbn/securitysolution-es-utils'; import { Readable } from 'node:stream'; import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; -import type { AssetCriticalityBulkUploadResponse } from '../../../../../common/api/entity_analytics'; -import { AssetCriticalityBulkUploadRequest } from '../../../../../common/api/entity_analytics'; +import type { BulkUpsertAssetCriticalityRecordsResponse } from '../../../../../common/api/entity_analytics'; +import { BulkUpsertAssetCriticalityRecordsRequestBody } from '../../../../../common/api/entity_analytics'; import type { ConfigType } from '../../../../config'; import { ASSET_CRITICALITY_PUBLIC_BULK_UPLOAD_URL, @@ -42,7 +42,7 @@ export const assetCriticalityPublicBulkUploadRoute = ( version: API_VERSIONS.public.v1, validate: { request: { - body: buildRouteValidationWithZod(AssetCriticalityBulkUploadRequest), + body: buildRouteValidationWithZod(BulkUpsertAssetCriticalityRecordsRequestBody), }, }, }, @@ -90,7 +90,7 @@ export const assetCriticalityPublicBulkUploadRoute = ( () => `Asset criticality Bulk upload completed in ${tookMs}ms ${JSON.stringify(stats)}` ); - const resBody: AssetCriticalityBulkUploadResponse = { errors, stats }; + const resBody: BulkUpsertAssetCriticalityRecordsResponse = { errors, stats }; return response.ok({ body: resBody }); } catch (e) { diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/delete.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/delete.ts index c7a0f07400cc85..b39013359eed44 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/delete.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/delete.ts @@ -8,6 +8,10 @@ import type { IKibanaResponse, KibanaResponseFactory, Logger } from '@kbn/core/s import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils'; import { transformError } from '@kbn/securitysolution-es-utils'; import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; +import { + DeleteAssetCriticalityRecordRequestQuery, + InternalDeleteAssetCriticalityRecordRequestQuery, +} from '../../../../../common/api/entity_analytics/asset_criticality/delete_asset_criticality.gen'; import type { SecuritySolutionRequestHandlerContext } from '../../../../types'; import { ASSET_CRITICALITY_PUBLIC_URL, @@ -16,7 +20,6 @@ import { ENABLE_ASSET_CRITICALITY_SETTING, API_VERSIONS, } from '../../../../../common/constants'; -import { DeleteAssetCriticalityRecord } from '../../../../../common/api/entity_analytics/asset_criticality'; import { checkAndInitAssetCriticalityResources } from '../check_and_init_asset_criticality_resources'; import { assertAdvancedSettingsEnabled } from '../../utils/assert_advanced_setting_enabled'; import type { EntityAnalyticsRoutesDeps } from '../../types'; @@ -26,7 +29,7 @@ import { AUDIT_CATEGORY, AUDIT_OUTCOME, AUDIT_TYPE } from '../../audit'; type DeleteHandler = ( context: SecuritySolutionRequestHandlerContext, request: { - query: DeleteAssetCriticalityRecord; + query: DeleteAssetCriticalityRecordRequestQuery; }, response: KibanaResponseFactory ) => Promise; @@ -88,7 +91,7 @@ export const assetCriticalityInternalDeleteRoute = ( version: API_VERSIONS.internal.v1, validate: { request: { - query: buildRouteValidationWithZod(DeleteAssetCriticalityRecord), + query: buildRouteValidationWithZod(InternalDeleteAssetCriticalityRecordRequestQuery), }, }, }, @@ -113,7 +116,7 @@ export const assetCriticalityPublicDeleteRoute = ( version: API_VERSIONS.public.v1, validate: { request: { - query: buildRouteValidationWithZod(DeleteAssetCriticalityRecord), + query: buildRouteValidationWithZod(DeleteAssetCriticalityRecordRequestQuery), }, }, }, diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/get.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/get.ts index 07d0cb3098dbc4..e1ab013a373b66 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/get.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/get.ts @@ -8,6 +8,7 @@ import type { IKibanaResponse, KibanaResponseFactory, Logger } from '@kbn/core/s import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils'; import { transformError } from '@kbn/securitysolution-es-utils'; import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; +import { GetAssetCriticalityRecordRequestQuery } from '../../../../../common/api/entity_analytics/asset_criticality/get_asset_criticality.gen'; import type { SecuritySolutionRequestHandlerContext } from '../../../../types'; import { ASSET_CRITICALITY_INTERNAL_URL, @@ -17,7 +18,6 @@ import { API_VERSIONS, } from '../../../../../common/constants'; import { checkAndInitAssetCriticalityResources } from '../check_and_init_asset_criticality_resources'; -import { AssetCriticalityRecordIdParts } from '../../../../../common/api/entity_analytics/asset_criticality'; import { assertAdvancedSettingsEnabled } from '../../utils/assert_advanced_setting_enabled'; import type { EntityAnalyticsRoutesDeps } from '../../types'; import { AssetCriticalityAuditActions } from '../audit'; @@ -25,7 +25,7 @@ import { AUDIT_CATEGORY, AUDIT_OUTCOME, AUDIT_TYPE } from '../../audit'; type GetHandler = ( context: SecuritySolutionRequestHandlerContext, request: { - query: AssetCriticalityRecordIdParts; + query: GetAssetCriticalityRecordRequestQuery; }, response: KibanaResponseFactory ) => Promise; @@ -86,7 +86,7 @@ export const assetCriticalityInternalGetRoute = ( version: API_VERSIONS.internal.v1, validate: { request: { - query: buildRouteValidationWithZod(AssetCriticalityRecordIdParts), + query: buildRouteValidationWithZod(GetAssetCriticalityRecordRequestQuery), }, }, }, @@ -111,7 +111,7 @@ export const assetCriticalityPublicGetRoute = ( version: API_VERSIONS.public.v1, validate: { request: { - query: buildRouteValidationWithZod(AssetCriticalityRecordIdParts), + query: buildRouteValidationWithZod(GetAssetCriticalityRecordRequestQuery), }, }, }, diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/list.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/list.ts index 66db32f2bdb170..711426e4df5103 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/list.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/list.ts @@ -15,8 +15,8 @@ import { API_VERSIONS, } from '../../../../../common/constants'; import { checkAndInitAssetCriticalityResources } from '../check_and_init_asset_criticality_resources'; -import type { AssetCriticalityListResponse } from '../../../../../common/api/entity_analytics/asset_criticality'; -import { ListAssetCriticalityQueryParams } from '../../../../../common/api/entity_analytics/asset_criticality'; +import type { FindAssetCriticalityRecordsResponse } from '../../../../../common/api/entity_analytics/asset_criticality'; +import { FindAssetCriticalityRecordsRequestQuery } from '../../../../../common/api/entity_analytics/asset_criticality'; import { assertAdvancedSettingsEnabled } from '../../utils/assert_advanced_setting_enabled'; import type { EntityAnalyticsRoutesDeps } from '../../types'; import { AssetCriticalityAuditActions } from '../audit'; @@ -39,7 +39,7 @@ export const assetCriticalityPublicListRoute = ( version: API_VERSIONS.public.v1, validate: { request: { - query: buildRouteValidationWithZod(ListAssetCriticalityQueryParams), + query: buildRouteValidationWithZod(FindAssetCriticalityRecordsRequestQuery), }, }, }, @@ -81,7 +81,7 @@ export const assetCriticalityPublicListRoute = ( }, }); - const body: AssetCriticalityListResponse = { + const body: FindAssetCriticalityRecordsResponse = { records, total, page, diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/status.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/status.ts index 2afa73ed5a059e..9d77817a20d98f 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/status.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/status.ts @@ -7,7 +7,7 @@ import type { Logger } from '@kbn/core/server'; import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils'; import { transformError } from '@kbn/securitysolution-es-utils'; -import type { AssetCriticalityStatusResponse } from '../../../../../common/api/entity_analytics/asset_criticality'; +import type { GetAssetCriticalityStatusResponse } from '../../../../../common/api/entity_analytics'; import { ASSET_CRITICALITY_INTERNAL_STATUS_URL, APP_ID, @@ -55,7 +55,7 @@ export const assetCriticalityInternalStatusRoute = ( }, }); - const body: AssetCriticalityStatusResponse = { + const body: GetAssetCriticalityStatusResponse = { asset_criticality_resources_installed: result.isAssetCriticalityResourcesInstalled, }; return response.ok({ diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/upload_csv.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/upload_csv.ts index 28c8333c5f5963..7e284bfe042a02 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/upload_csv.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/upload_csv.ts @@ -10,7 +10,7 @@ import { schema } from '@kbn/config-schema'; import Papa from 'papaparse'; import { transformError } from '@kbn/securitysolution-es-utils'; import type internal from 'stream'; -import type { AssetCriticalityBulkUploadResponse } from '../../../../../common/api/entity_analytics'; +import type { UploadAssetCriticalityRecordsResponse } from '../../../../../common/api/entity_analytics/asset_criticality/upload_asset_criticality_csv.gen'; import { CRITICALITY_CSV_MAX_SIZE_BYTES_WITH_TOLERANCE } from '../../../../../common/entity_analytics/asset_criticality'; import type { ConfigType } from '../../../../config'; import type { HapiReadableStream, SecuritySolutionRequestHandlerContext } from '../../../../types'; @@ -90,7 +90,7 @@ const handler: ( ); // type assignment here to ensure that the response body stays in sync with the API schema - const resBody: AssetCriticalityBulkUploadResponse = { errors, stats }; + const resBody: UploadAssetCriticalityRecordsResponse = { errors, stats }; const [eventType, event] = createAssetCriticalityProcessedFileEvent({ startTime: start, diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/upsert.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/upsert.ts index cb3c36f450e430..20ad8173af6662 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/upsert.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/upsert.ts @@ -8,6 +8,10 @@ import type { IKibanaResponse, KibanaResponseFactory, Logger } from '@kbn/core/s import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils'; import { transformError } from '@kbn/securitysolution-es-utils'; import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; +import { + CreateAssetCriticalityRecordRequestBody, + InternalCreateAssetCriticalityRecordRequestBody, +} from '../../../../../common/api/entity_analytics/asset_criticality/create_asset_criticality.gen'; import type { SecuritySolutionRequestHandlerContext } from '../../../../types'; import { ASSET_CRITICALITY_PUBLIC_URL, @@ -17,7 +21,6 @@ import { API_VERSIONS, } from '../../../../../common/constants'; import { checkAndInitAssetCriticalityResources } from '../check_and_init_asset_criticality_resources'; -import { CreateSingleAssetCriticalityRequest } from '../../../../../common/api/entity_analytics'; import type { EntityAnalyticsRoutesDeps } from '../../types'; import { AssetCriticalityAuditActions } from '../audit'; import { AUDIT_CATEGORY, AUDIT_OUTCOME, AUDIT_TYPE } from '../../audit'; @@ -26,7 +29,7 @@ import { assertAdvancedSettingsEnabled } from '../../utils/assert_advanced_setti type UpsertHandler = ( context: SecuritySolutionRequestHandlerContext, request: { - body: CreateSingleAssetCriticalityRequest; + body: CreateAssetCriticalityRecordRequestBody; }, response: KibanaResponseFactory ) => Promise; @@ -93,7 +96,7 @@ export const assetCriticalityInternalUpsertRoute = ( version: API_VERSIONS.internal.v1, validate: { request: { - body: buildRouteValidationWithZod(CreateSingleAssetCriticalityRequest), + body: buildRouteValidationWithZod(InternalCreateAssetCriticalityRecordRequestBody), }, }, }, @@ -118,7 +121,7 @@ export const assetCriticalityPublicUpsertRoute = ( version: API_VERSIONS.public.v1, validate: { request: { - body: buildRouteValidationWithZod(CreateSingleAssetCriticalityRequest), + body: buildRouteValidationWithZod(CreateAssetCriticalityRecordRequestBody), }, }, }, diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/disable.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/disable.ts index f1f0348a69e339..3501d1869d5ed5 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/disable.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/disable.ts @@ -7,7 +7,7 @@ import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils'; import { transformError } from '@kbn/securitysolution-es-utils'; -import type { RiskEngineDisableResponse } from '../../../../../common/api/entity_analytics/risk_engine/engine_disable_route.gen'; +import type { DisableRiskEngineResponse } from '../../../../../common/api/entity_analytics/risk_engine/engine_disable_route.gen'; import { RISK_ENGINE_DISABLE_URL, APP_ID } from '../../../../../common/constants'; import { TASK_MANAGER_UNAVAILABLE_ERROR } from './translations'; import { withRiskEnginePrivilegeCheck } from '../risk_engine_privileges'; @@ -71,7 +71,7 @@ export const riskEngineDisableRoute = ( try { await riskEngineClient.disableRiskEngine({ taskManager }); - const body: RiskEngineDisableResponse = { success: true }; + const body: DisableRiskEngineResponse = { success: true }; return response.ok({ body }); } catch (e) { const error = transformError(e); diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/enable.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/enable.ts index a4eed8701d1e15..9397af65675dae 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/enable.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/enable.ts @@ -7,7 +7,7 @@ import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils'; import { transformError } from '@kbn/securitysolution-es-utils'; -import type { RiskEngineEnableResponse } from '../../../../../common/api/entity_analytics/risk_engine/engine_enable_route.gen'; +import type { EnableRiskEngineResponse } from '../../../../../common/api/entity_analytics/risk_engine/engine_enable_route.gen'; import { RISK_ENGINE_ENABLE_URL, APP_ID } from '../../../../../common/constants'; import { TASK_MANAGER_UNAVAILABLE_ERROR } from './translations'; import { withRiskEnginePrivilegeCheck } from '../risk_engine_privileges'; @@ -69,7 +69,7 @@ export const riskEngineEnableRoute = ( try { await riskEngineClient.enableRiskEngine({ taskManager }); - const body: RiskEngineEnableResponse = { success: true }; + const body: EnableRiskEngineResponse = { success: true }; return response.ok({ body }); } catch (e) { const error = transformError(e); diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/init.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/init.ts index 8360f3652a7f3d..9e50e0b98ccd8b 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/init.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/init.ts @@ -8,8 +8,8 @@ import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils'; import { transformError } from '@kbn/securitysolution-es-utils'; import type { - RiskEngineInitResponse, - RiskEngineInitResult, + InitRiskEngineResponse, + InitRiskEngineResult, } from '../../../../../common/api/entity_analytics/risk_engine/engine_init_route.gen'; import { RISK_ENGINE_INIT_URL, APP_ID } from '../../../../../common/constants'; import { TASK_MANAGER_UNAVAILABLE_ERROR } from './translations'; @@ -64,7 +64,7 @@ export const riskEngineInitRoute = ( riskScoreDataClient, }); - const result: RiskEngineInitResult = { + const result: InitRiskEngineResult = { risk_engine_enabled: initResult.riskEngineEnabled, risk_engine_resources_installed: initResult.riskEngineResourcesInstalled, risk_engine_configuration_created: initResult.riskEngineConfigurationCreated, @@ -72,7 +72,7 @@ export const riskEngineInitRoute = ( errors: initResult.errors, }; - const initResponse: RiskEngineInitResponse = { + const initResponse: InitRiskEngineResponse = { result, }; diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/settings.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/settings.ts index 032114f7871b6b..1d39fbaf184206 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/settings.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/settings.ts @@ -7,7 +7,7 @@ import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils'; import { transformError } from '@kbn/securitysolution-es-utils'; -import type { RiskEngineSettingsResponse } from '../../../../../common/api/entity_analytics/risk_engine'; +import type { ReadRiskEngineSettingsResponse } from '../../../../../common/api/entity_analytics/risk_engine'; import { RISK_ENGINE_SETTINGS_URL, APP_ID } from '../../../../../common/constants'; import { AUDIT_CATEGORY, AUDIT_OUTCOME, AUDIT_TYPE } from '../../audit'; import type { EntityAnalyticsRoutesDeps } from '../../types'; @@ -43,7 +43,7 @@ export const riskEngineSettingsRoute = (router: EntityAnalyticsRoutesDeps['route if (!result) { throw new Error('Unable to get risk engine configuration'); } - const body: RiskEngineSettingsResponse = { + const body: ReadRiskEngineSettingsResponse = { range: result.range, }; return response.ok({ diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/event_based/events.ts b/x-pack/plugins/security_solution/server/lib/telemetry/event_based/events.ts index 97a4d44fcd5949..8eb46b2046c10c 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/event_based/events.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/event_based/events.ts @@ -5,7 +5,7 @@ * 2.0. */ import type { EventTypeOpts } from '@kbn/core/server'; -import type { AssetCriticalityBulkUploadResponse } from '../../../../common/api/entity_analytics'; +import type { BulkUpsertAssetCriticalityRecordsResponse } from '../../../../common/api/entity_analytics'; export const RISK_SCORE_EXECUTION_SUCCESS_EVENT: EventTypeOpts<{ scoresWritten: number; @@ -88,7 +88,7 @@ interface AssetCriticalitySystemProcessedAssignmentFileEvent { endTime: string; tookMs: number; }; - result?: AssetCriticalityBulkUploadResponse['stats']; + result?: BulkUpsertAssetCriticalityRecordsResponse['stats']; status: 'success' | 'partial_success' | 'fail'; } @@ -124,7 +124,7 @@ export const ASSET_CRITICALITY_SYSTEM_PROCESSED_ASSIGNMENT_FILE_EVENT: EventType }; interface CreateAssetCriticalityProcessedFileEvent { - result?: AssetCriticalityBulkUploadResponse['stats']; + result?: BulkUpsertAssetCriticalityRecordsResponse['stats']; startTime: Date; endTime: Date; } @@ -154,7 +154,7 @@ export const createAssetCriticalityProcessedFileEvent = ({ ]; }; -const getUploadStatus = (stats?: AssetCriticalityBulkUploadResponse['stats']) => { +const getUploadStatus = (stats?: BulkUpsertAssetCriticalityRecordsResponse['stats']) => { if (!stats) { return 'fail'; } diff --git a/x-pack/test/api_integration/services/security_solution_api.gen.ts b/x-pack/test/api_integration/services/security_solution_api.gen.ts index f5089b489a6172..91ae460bbb563f 100644 --- a/x-pack/test/api_integration/services/security_solution_api.gen.ts +++ b/x-pack/test/api_integration/services/security_solution_api.gen.ts @@ -26,13 +26,17 @@ import { BulkDeleteRulesRequestBodyInput } from '@kbn/security-solution-plugin/c import { BulkDeleteRulesPostRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/bulk_crud/bulk_delete_rules/bulk_delete_rules_route.gen'; import { BulkPatchRulesRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/bulk_crud/bulk_patch_rules/bulk_patch_rules_route.gen'; import { BulkUpdateRulesRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/bulk_crud/bulk_update_rules/bulk_update_rules_route.gen'; +import { BulkUpsertAssetCriticalityRecordsRequestBodyInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/asset_criticality/bulk_upload_asset_criticality.gen'; import { CreateAlertsMigrationRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/signals_migration/create_signals_migration/create_signals_migration.gen'; +import { CreateAssetCriticalityRecordRequestBodyInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/asset_criticality/create_asset_criticality.gen'; import { CreateRuleRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/crud/create_rule/create_rule_route.gen'; import { CreateUpdateProtectionUpdatesNoteRequestParamsInput, CreateUpdateProtectionUpdatesNoteRequestBodyInput, } from '@kbn/security-solution-plugin/common/api/endpoint/protection_updates_note/protection_updates_note.gen'; +import { DeleteAssetCriticalityRecordRequestQueryInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/asset_criticality/delete_asset_criticality.gen'; import { DeleteRuleRequestQueryInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/crud/delete_rule/delete_rule_route.gen'; +import { DeprecatedTriggerRiskScoreCalculationRequestBodyInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/risk_engine/entity_calculation_route.gen'; import { EndpointIsolateRedirectRequestBodyInput } from '@kbn/security-solution-plugin/common/api/endpoint/actions/isolate_route.gen'; import { EndpointUnisolateRedirectRequestBodyInput } from '@kbn/security-solution-plugin/common/api/endpoint/actions/unisolate_route.gen'; import { @@ -40,9 +44,11 @@ import { ExportRulesRequestBodyInput, } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/export_rules/export_rules_route.gen'; import { FinalizeAlertsMigrationRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/signals_migration/finalize_signals_migration/finalize_signals_migration.gen'; +import { FindAssetCriticalityRecordsRequestQueryInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/asset_criticality/list_asset_criticality.gen'; import { FindRulesRequestQueryInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/find_rules/find_rules_route.gen'; import { GetAgentPolicySummaryRequestQueryInput } from '@kbn/security-solution-plugin/common/api/endpoint/policy/policy.gen'; import { GetAlertsMigrationStatusRequestQueryInput } from '@kbn/security-solution-plugin/common/api/detection_engine/signals_migration/get_signals_migration_status/get_signals_migration_status.gen'; +import { GetAssetCriticalityRecordRequestQueryInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/asset_criticality/get_asset_criticality.gen'; import { GetEndpointSuggestionsRequestParamsInput, GetEndpointSuggestionsRequestBodyInput, @@ -58,18 +64,22 @@ import { GetRuleExecutionResultsRequestParamsInput, } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_monitoring/rule_execution_logs/get_rule_execution_results/get_rule_execution_results_route.gen'; import { ImportRulesRequestQueryInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/import_rules/import_rules_route.gen'; +import { InternalCreateAssetCriticalityRecordRequestBodyInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/asset_criticality/create_asset_criticality.gen'; +import { InternalDeleteAssetCriticalityRecordRequestQueryInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/asset_criticality/delete_asset_criticality.gen'; import { ManageAlertTagsRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/alert_tags/set_alert_tags/set_alert_tags.gen'; import { PatchRuleRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/crud/patch_rule/patch_rule_route.gen'; import { PerformBulkActionRequestQueryInput, PerformBulkActionRequestBodyInput, } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route.gen'; +import { PreviewRiskScoreRequestBodyInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/risk_engine/preview_route.gen'; import { ReadRuleRequestQueryInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/crud/read_rule/read_rule_route.gen'; import { RulePreviewRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_preview/rule_preview.gen'; import { SearchAlertsRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/signals/query_signals/query_signals_route.gen'; import { SetAlertAssigneesRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/alert_assignees/set_alert_assignees_route.gen'; import { SetAlertsStatusRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/signals/set_signal_status/set_signals_status_route.gen'; import { SuggestUserProfilesRequestQueryInput } from '@kbn/security-solution-plugin/common/api/detection_engine/users/suggest_user_profiles_route.gen'; +import { TriggerRiskScoreCalculationRequestBodyInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/risk_engine/entity_calculation_route.gen'; import { UpdateRuleRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/crud/update_rule/update_rule_route.gen'; import { FtrProviderContext } from '../ftr_provider_context'; @@ -153,6 +163,14 @@ after 30 days. It also deletes other artifacts specific to the migration impleme .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .send(props.body as object); }, + bulkUpsertAssetCriticalityRecords(props: BulkUpsertAssetCriticalityRecordsProps) { + return supertest + .post('/api/asset_criticality/bulk') + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .send(props.body as object); + }, createAlertsIndex() { return supertest .post('/api/detection_engine/index') @@ -173,6 +191,14 @@ Migrations are initiated per index. While the process is neither destructive nor .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .send(props.body as object); }, + createAssetCriticalityRecord(props: CreateAssetCriticalityRecordProps) { + return supertest + .post('/api/asset_criticality') + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .send(props.body as object); + }, /** * Create a new detection rule. */ @@ -201,6 +227,14 @@ Migrations are initiated per index. While the process is neither destructive nor .set(ELASTIC_HTTP_VERSION_HEADER, '2023-10-31') .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana'); }, + deleteAssetCriticalityRecord(props: DeleteAssetCriticalityRecordProps) { + return supertest + .delete('/api/asset_criticality') + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .query(props.query); + }, /** * Delete a detection rule using the `rule_id` or `id` field. */ @@ -212,6 +246,31 @@ Migrations are initiated per index. While the process is neither destructive nor .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .query(props.query); }, + /** + * Calculates and persists Risk Scores for an entity, returning the calculated risk score. + */ + deprecatedTriggerRiskScoreCalculation(props: DeprecatedTriggerRiskScoreCalculationProps) { + return supertest + .post('/api/risk_scores/calculation/entity') + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .send(props.body as object); + }, + disableRiskEngine() { + return supertest + .post('/internal/risk_score/engine/disable') + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana'); + }, + enableRiskEngine() { + return supertest + .post('/internal/risk_score/engine/enable') + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana'); + }, endpointIsolateRedirect(props: EndpointIsolateRedirectProps) { return supertest .post('/api/endpoint/isolate') @@ -259,6 +318,14 @@ finalize it. .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .send(props.body as object); }, + findAssetCriticalityRecords(props: FindAssetCriticalityRecordsProps) { + return supertest + .post('/api/asset_criticality/list') + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .query(props.query); + }, /** * Retrieve a paginated list of detection rules. By default, the first page is returned, with 20 results per page. */ @@ -296,6 +363,21 @@ finalize it. .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .query(props.query); }, + getAssetCriticalityRecord(props: GetAssetCriticalityRecordProps) { + return supertest + .get('/api/asset_criticality') + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .query(props.query); + }, + getAssetCriticalityStatus() { + return supertest + .get('/internal/asset_criticality/status') + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana'); + }, getEndpointSuggestions(props: GetEndpointSuggestionsProps) { return supertest .post(replaceParams('/api/endpoint/suggestions/{suggestion_type}', props.params)) @@ -345,6 +427,16 @@ detection engine rules. .set(ELASTIC_HTTP_VERSION_HEADER, '2023-10-31') .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana'); }, + /** + * Returns the status of both the legacy transform-based risk engine, as well as the new risk engine + */ + getRiskEngineStatus() { + return supertest + .get('/internal/risk_score/engine/status') + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana'); + }, getRuleExecutionEvents(props: GetRuleExecutionEventsProps) { return supertest .put( @@ -379,6 +471,16 @@ detection engine rules. .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .query(props.query); }, + /** + * Initializes the Risk Engine by creating the necessary indices and mappings, removing old transforms, and starting the new risk engine + */ + initRiskEngine() { + return supertest + .post('/internal/risk_score/engine/init') + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana'); + }, /** * Install and update all Elastic prebuilt detection rules and Timelines. */ @@ -389,6 +491,29 @@ detection engine rules. .set(ELASTIC_HTTP_VERSION_HEADER, '2023-10-31') .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana'); }, + internalCreateAssetCriticalityRecord(props: InternalCreateAssetCriticalityRecordProps) { + return supertest + .post('/internal/asset_criticality') + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .send(props.body as object); + }, + internalDeleteAssetCriticalityRecord(props: InternalDeleteAssetCriticalityRecordProps) { + return supertest + .delete('/internal/asset_criticality') + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .query(props.query); + }, + internalUploadAssetCriticalityRecords() { + return supertest + .post('/internal/asset_criticality/upload_csv') + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana'); + }, /** * And tags to detection alerts, and remove them from alerts. > info @@ -426,6 +551,24 @@ detection engine rules. .send(props.body as object) .query(props.query); }, + /** + * Calculates and returns a list of Risk Scores, sorted by identifier_type and risk score. + */ + previewRiskScore(props: PreviewRiskScoreProps) { + return supertest + .post('/internal/risk_score/preview') + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .send(props.body as object); + }, + readRiskEngineSettings() { + return supertest + .get('/internal/risk_score/engine/settings') + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana'); + }, /** * Retrieve a detection rule using the `rule_id` or `id` field. */ @@ -502,6 +645,17 @@ detection engine rules. .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .query(props.query); }, + /** + * Calculates and persists Risk Scores for an entity, returning the calculated risk score. + */ + triggerRiskScoreCalculation(props: TriggerRiskScoreCalculationProps) { + return supertest + .post('/internal/risk_score/calculation/entity') + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .send(props.body as object); + }, /** * Update a detection rule using the `rule_id` or `id` field. The original rule is replaced, and all unspecified fields are deleted. > info @@ -516,6 +670,13 @@ detection engine rules. .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .send(props.body as object); }, + uploadAssetCriticalityRecords() { + return supertest + .post('/api/asset_criticality/upload_csv') + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana'); + }, }; } @@ -537,9 +698,15 @@ export interface BulkPatchRulesProps { export interface BulkUpdateRulesProps { body: BulkUpdateRulesRequestBodyInput; } +export interface BulkUpsertAssetCriticalityRecordsProps { + body: BulkUpsertAssetCriticalityRecordsRequestBodyInput; +} export interface CreateAlertsMigrationProps { body: CreateAlertsMigrationRequestBodyInput; } +export interface CreateAssetCriticalityRecordProps { + body: CreateAssetCriticalityRecordRequestBodyInput; +} export interface CreateRuleProps { body: CreateRuleRequestBodyInput; } @@ -547,9 +714,15 @@ export interface CreateUpdateProtectionUpdatesNoteProps { params: CreateUpdateProtectionUpdatesNoteRequestParamsInput; body: CreateUpdateProtectionUpdatesNoteRequestBodyInput; } +export interface DeleteAssetCriticalityRecordProps { + query: DeleteAssetCriticalityRecordRequestQueryInput; +} export interface DeleteRuleProps { query: DeleteRuleRequestQueryInput; } +export interface DeprecatedTriggerRiskScoreCalculationProps { + body: DeprecatedTriggerRiskScoreCalculationRequestBodyInput; +} export interface EndpointIsolateRedirectProps { body: EndpointIsolateRedirectRequestBodyInput; } @@ -563,6 +736,9 @@ export interface ExportRulesProps { export interface FinalizeAlertsMigrationProps { body: FinalizeAlertsMigrationRequestBodyInput; } +export interface FindAssetCriticalityRecordsProps { + query: FindAssetCriticalityRecordsRequestQueryInput; +} export interface FindRulesProps { query: FindRulesRequestQueryInput; } @@ -572,6 +748,9 @@ export interface GetAgentPolicySummaryProps { export interface GetAlertsMigrationStatusProps { query: GetAlertsMigrationStatusRequestQueryInput; } +export interface GetAssetCriticalityRecordProps { + query: GetAssetCriticalityRecordRequestQueryInput; +} export interface GetEndpointSuggestionsProps { params: GetEndpointSuggestionsRequestParamsInput; body: GetEndpointSuggestionsRequestBodyInput; @@ -593,6 +772,12 @@ export interface GetRuleExecutionResultsProps { export interface ImportRulesProps { query: ImportRulesRequestQueryInput; } +export interface InternalCreateAssetCriticalityRecordProps { + body: InternalCreateAssetCriticalityRecordRequestBodyInput; +} +export interface InternalDeleteAssetCriticalityRecordProps { + query: InternalDeleteAssetCriticalityRecordRequestQueryInput; +} export interface ManageAlertTagsProps { body: ManageAlertTagsRequestBodyInput; } @@ -603,6 +788,9 @@ export interface PerformBulkActionProps { query: PerformBulkActionRequestQueryInput; body: PerformBulkActionRequestBodyInput; } +export interface PreviewRiskScoreProps { + body: PreviewRiskScoreRequestBodyInput; +} export interface ReadRuleProps { query: ReadRuleRequestQueryInput; } @@ -621,6 +809,9 @@ export interface SetAlertsStatusProps { export interface SuggestUserProfilesProps { query: SuggestUserProfilesRequestQueryInput; } +export interface TriggerRiskScoreCalculationProps { + body: TriggerRiskScoreCalculationRequestBodyInput; +} export interface UpdateRuleProps { body: UpdateRuleRequestBodyInput; } diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/asset_criticality.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/asset_criticality.ts index 9ae70f540f8977..11343e077eeaff 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/asset_criticality.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/asset_criticality.ts @@ -23,7 +23,7 @@ import { import type { AssetCriticalityRecord, CreateAssetCriticalityRecord, - ListAssetCriticalityQueryParams, + FindAssetCriticalityRecordsRequestQuery, } from '@kbn/security-solution-plugin/common/api/entity_analytics'; import type { Client } from '@elastic/elasticsearch'; import type { ToolingLog } from '@kbn/tooling-log'; @@ -187,7 +187,7 @@ export const assetCriticalityRouteHelpersFactory = ( .expect(expectStatusCode); }, list: async ( - opts: ListAssetCriticalityQueryParams = {}, + opts: FindAssetCriticalityRecordsRequestQuery = {}, { expectStatusCode }: { expectStatusCode: number } = { expectStatusCode: 200 } ) => { const qs = querystring.stringify(opts); From 47b0105ea7b1bb5d53de0c3b341b22633f66f7ed Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 22 Jul 2024 11:06:37 -0500 Subject: [PATCH 07/54] Gemini connector - update test message (#188850) --- docs/management/connectors/action-types/gemini.asciidoc | 2 +- .../public/connector_types/gemini/constants.tsx | 2 +- .../group2/tests/actions/connector_types/gemini.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/management/connectors/action-types/gemini.asciidoc b/docs/management/connectors/action-types/gemini.asciidoc index 3c835d981465c3..610fb4ad48f155 100644 --- a/docs/management/connectors/action-types/gemini.asciidoc +++ b/docs/management/connectors/action-types/gemini.asciidoc @@ -56,7 +56,7 @@ Body:: A stringified JSON payload sent to the {gemini} invoke model API. Fo body: JSON.stringify({ contents: [{ role: user, - parts: [{ text: 'Write the first line of a story about a magic backpack.' }] + parts: [{ text: 'Hello world!' }] }], generation_config: { temperature: 0, diff --git a/x-pack/plugins/stack_connectors/public/connector_types/gemini/constants.tsx b/x-pack/plugins/stack_connectors/public/connector_types/gemini/constants.tsx index 162f78efabc485..e9844a1c39b03f 100644 --- a/x-pack/plugins/stack_connectors/public/connector_types/gemini/constants.tsx +++ b/x-pack/plugins/stack_connectors/public/connector_types/gemini/constants.tsx @@ -27,7 +27,7 @@ const contents = [ role: 'user', parts: [ { - text: 'Write the first line of a story about a magic backpack.', + text: 'Hello world!', }, ], }, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/gemini.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/gemini.ts index d483d11db96eca..54eebf207e7d76 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/gemini.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/gemini.ts @@ -310,7 +310,7 @@ export default function geminiTest({ getService }: FtrProviderContext) { role: 'user', parts: [ { - text: 'Write the first line of a story about a magic backpack.', + text: 'Hello world!', }, ], }, @@ -325,7 +325,7 @@ export default function geminiTest({ getService }: FtrProviderContext) { contents: [ { role: 'user', - parts: [{ text: 'Write the first line of a story about a magic backpack.' }], + parts: [{ text: 'Hello world!' }], }, ], generation_config: { temperature: 0, maxOutputTokens: 8192 }, From e33f010d6d9e160968d8c19645605b8db7968b85 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 22 Jul 2024 17:16:25 +0100 Subject: [PATCH 08/54] skip flaky suite (#188234) --- .../server/integration_tests/telemetry.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/server/integration_tests/telemetry.test.ts b/x-pack/plugins/security_solution/server/integration_tests/telemetry.test.ts index 7a38948c0c46d0..46f85a01f4760a 100644 --- a/x-pack/plugins/security_solution/server/integration_tests/telemetry.test.ts +++ b/x-pack/plugins/security_solution/server/integration_tests/telemetry.test.ts @@ -683,7 +683,8 @@ describe('telemetry tasks', () => { }); }); - describe('telemetry-prebuilt-rule-alerts', () => { + // FLAKY: https://github.com/elastic/kibana/issues/188234 + describe.skip('telemetry-prebuilt-rule-alerts', () => { it('should execute when scheduled', async () => { await mockAndSchedulePrebuiltRulesTask(); From f380962a6e822a597d96c0f96c74f92766eb7452 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 22 Jul 2024 17:20:04 +0100 Subject: [PATCH 09/54] skip flaky suite (#188660) --- .../serverless_metering/cloud_security_metering.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/serverless_metering/cloud_security_metering.ts b/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/serverless_metering/cloud_security_metering.ts index c1ce48215e2e2d..49c223c8d14248 100644 --- a/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/serverless_metering/cloud_security_metering.ts +++ b/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/serverless_metering/cloud_security_metering.ts @@ -40,7 +40,8 @@ export default function (providerContext: FtrProviderContext) { The task manager is running by default in security serverless project in the background and sending usage API requests to the usage API. This test mocks the usage API server and intercepts the usage API request sent by the metering background task manager. */ - describe('Intercept the usage API request sent by the metering background task manager', function () { + // FLAKY: https://github.com/elastic/kibana/issues/188660 + describe.skip('Intercept the usage API request sent by the metering background task manager', function () { this.tags(['skipMKI']); let mockUsageApiServer: http.Server; From e026c2a2a9e8283fbe9fc5700d223fa940bbfe7d Mon Sep 17 00:00:00 2001 From: "Joey F. Poon" Date: Mon, 22 Jul 2024 09:30:17 -0700 Subject: [PATCH 10/54] [Security Solution] unskip endpoint metering integration tests (#187816) --- .../public/management/cypress/e2e/serverless/metering.cy.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/metering.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/metering.cy.ts index 6e436e2a529f8e..8cc4cadda44f2f 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/metering.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/metering.cy.ts @@ -17,8 +17,7 @@ import type { ReturnTypeFromChainable } from '../../types'; import { indexEndpointHeartbeats } from '../../tasks/index_endpoint_heartbeats'; import { login, ROLE } from '../../tasks/login'; -// Failing: See https://github.com/elastic/kibana/issues/187083 -describe.skip( +describe( 'Metering', { tags: ['@serverless', '@skipInServerlessMKI'], @@ -30,6 +29,7 @@ describe.skip( ], }, }, + pageLoadTimeout: 1 * 60 * 1000, }, () => { const HEARTBEAT_COUNT = 2001; From d8302eb2ec96a74444195ed4ba13adfbe9de185e Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 22 Jul 2024 11:31:20 -0500 Subject: [PATCH 11/54] [deb] Add adduser as a dependency (#185048) adduser is used in the deb post install script. Installing kibana.deb in a container won't have the necessary dependencies by default Closes #182537 --------- Co-authored-by: Elastic Machine --- .buildkite/pipelines/pull_request/base.yml | 2 +- src/dev/build/tasks/os_packages/create_os_package_tasks.ts | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.buildkite/pipelines/pull_request/base.yml b/.buildkite/pipelines/pull_request/base.yml index 8b57562c7a3294..e5da8ce788e5b6 100644 --- a/.buildkite/pipelines/pull_request/base.yml +++ b/.buildkite/pipelines/pull_request/base.yml @@ -14,7 +14,7 @@ steps: preemptible: true key: build if: "build.env('KIBANA_BUILD_ID') == null || build.env('KIBANA_BUILD_ID') == ''" - timeout_in_minutes: 60 + timeout_in_minutes: 90 retry: automatic: - exit_status: '-1' diff --git a/src/dev/build/tasks/os_packages/create_os_package_tasks.ts b/src/dev/build/tasks/os_packages/create_os_package_tasks.ts index f422d9fae221a9..052d7592024d7f 100644 --- a/src/dev/build/tasks/os_packages/create_os_package_tasks.ts +++ b/src/dev/build/tasks/os_packages/create_os_package_tasks.ts @@ -28,6 +28,8 @@ export const CreateDebPackage: Task = { 'amd64', '--deb-priority', 'optional', + '--depends', + ' adduser', ]); await runFpm(config, log, build, 'deb', 'arm64', [ @@ -35,6 +37,8 @@ export const CreateDebPackage: Task = { 'arm64', '--deb-priority', 'optional', + '--depends', + ' adduser', ]); }, }; From 8fb8c27fac201892eb58d0a11dce23c6ccb12cbd Mon Sep 17 00:00:00 2001 From: Rachel Shen Date: Mon, 22 Jul 2024 11:03:26 -0600 Subject: [PATCH 12/54] [A11y] aria label for context for try in console open in a new tab or embedded console (#188367) ## Summary Closes https://github.com/elastic/search-team/issues/7627 --- .../components/try_in_console_button.tsx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/kbn-try-in-console/components/try_in_console_button.tsx b/packages/kbn-try-in-console/components/try_in_console_button.tsx index a49011749e14ef..e54b139fe6734e 100644 --- a/packages/kbn-try-in-console/components/try_in_console_button.tsx +++ b/packages/kbn-try-in-console/components/try_in_console_button.tsx @@ -72,12 +72,27 @@ export const TryInConsoleButton = ({ ); } + const getAriaLabel = () => { + if ( + consolePlugin?.openEmbeddedConsole !== undefined && + consolePlugin?.isEmbeddedConsoleAvailable?.() + ) { + return i18n.translate('tryInConsole.embeddedConsoleButton', { + defaultMessage: 'Try the snipped in the Console - opens in embedded console', + }); + } + return i18n.translate('tryInConsole.inNewTab.button', { + defaultMessage: 'Try the below snippet in Console - opens in a new tab', + }); + }; + return ( {content ?? TRY_IN_CONSOLE} From 375c6ffd619ef6bbb5b68e90ff2b647c6287c379 Mon Sep 17 00:00:00 2001 From: Chris Cowan Date: Mon, 22 Jul 2024 11:24:29 -0600 Subject: [PATCH 13/54] [EEM] Convert route validation to Zod (#188691) ## Summary This PR closes https://github.com/elastic/kibana/issues/188171 by converting the route validate to Zod for `get`, `reset`, and `delete` APIs. This also changes the validation for the `create` API to use `buildRouteValidationWithZod` along with adding `strict()` to each of the schemas. Closes https://github.com/elastic/elastic-entity-model/issues/103 --------- Co-authored-by: Kevin Lacabane --- x-pack/packages/kbn-entities-schema/index.ts | 3 +++ .../kbn-entities-schema/src/rest_spec/delete.ts | 16 ++++++++++++++++ .../kbn-entities-schema/src/rest_spec/get.ts | 13 +++++++++++++ .../kbn-entities-schema/src/rest_spec/reset.ts | 11 +++++++++++ .../server/routes/entities/create.ts | 10 ++-------- .../server/routes/entities/delete.ts | 14 +++++++------- .../entity_manager/server/routes/entities/get.ts | 8 +++----- .../server/routes/entities/reset.ts | 7 +++---- 8 files changed, 58 insertions(+), 24 deletions(-) create mode 100644 x-pack/packages/kbn-entities-schema/src/rest_spec/delete.ts create mode 100644 x-pack/packages/kbn-entities-schema/src/rest_spec/get.ts create mode 100644 x-pack/packages/kbn-entities-schema/src/rest_spec/reset.ts diff --git a/x-pack/packages/kbn-entities-schema/index.ts b/x-pack/packages/kbn-entities-schema/index.ts index 92b93b79381251..8251e1c14755f4 100644 --- a/x-pack/packages/kbn-entities-schema/index.ts +++ b/x-pack/packages/kbn-entities-schema/index.ts @@ -8,3 +8,6 @@ export * from './src/schema/entity_definition'; export * from './src/schema/entity'; export * from './src/schema/common'; +export * from './src/rest_spec/delete'; +export * from './src/rest_spec/reset'; +export * from './src/rest_spec/get'; diff --git a/x-pack/packages/kbn-entities-schema/src/rest_spec/delete.ts b/x-pack/packages/kbn-entities-schema/src/rest_spec/delete.ts new file mode 100644 index 00000000000000..b1243d5aa6d9e3 --- /dev/null +++ b/x-pack/packages/kbn-entities-schema/src/rest_spec/delete.ts @@ -0,0 +1,16 @@ +/* + * 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 { z } from 'zod'; + +export const deleteEntityDefinitionParamsSchema = z.object({ + id: z.string(), +}); + +export const deleteEntityDefinitionQuerySchema = z.object({ + deleteData: z.optional(z.coerce.boolean().default(false)), +}); diff --git a/x-pack/packages/kbn-entities-schema/src/rest_spec/get.ts b/x-pack/packages/kbn-entities-schema/src/rest_spec/get.ts new file mode 100644 index 00000000000000..f703da8a7b6b26 --- /dev/null +++ b/x-pack/packages/kbn-entities-schema/src/rest_spec/get.ts @@ -0,0 +1,13 @@ +/* + * 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 { z } from 'zod'; + +export const getEntityDefinitionQuerySchema = z.object({ + page: z.optional(z.coerce.number()), + perPage: z.optional(z.coerce.number()), +}); diff --git a/x-pack/packages/kbn-entities-schema/src/rest_spec/reset.ts b/x-pack/packages/kbn-entities-schema/src/rest_spec/reset.ts new file mode 100644 index 00000000000000..e93b8e789280fb --- /dev/null +++ b/x-pack/packages/kbn-entities-schema/src/rest_spec/reset.ts @@ -0,0 +1,11 @@ +/* + * 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 { z } from 'zod'; + +export const resetEntityDefinitionParamsSchema = z.object({ + id: z.string(), +}); diff --git a/x-pack/plugins/observability_solution/entity_manager/server/routes/entities/create.ts b/x-pack/plugins/observability_solution/entity_manager/server/routes/entities/create.ts index 8d17debc8914d4..9d38cc7c5e716a 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/routes/entities/create.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/routes/entities/create.ts @@ -7,7 +7,7 @@ import { RequestHandlerContext } from '@kbn/core/server'; import { EntityDefinition, entityDefinitionSchema } from '@kbn/entities-schema'; -import { stringifyZodError } from '@kbn/zod-helpers'; +import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; import { SetupRouteOptions } from '../types'; import { EntityIdConflict } from '../../lib/entities/errors/entity_id_conflict_error'; import { EntitySecurityException } from '../../lib/entities/errors/entity_security_exception'; @@ -23,13 +23,7 @@ export function createEntityDefinitionRoute({ { path: '/internal/entities/definition', validate: { - body: (body, res) => { - try { - return res.ok(entityDefinitionSchema.parse(body)); - } catch (e) { - return res.badRequest(stringifyZodError(e)); - } - }, + body: buildRouteValidationWithZod(entityDefinitionSchema.strict()), }, }, async (context, req, res) => { diff --git a/x-pack/plugins/observability_solution/entity_manager/server/routes/entities/delete.ts b/x-pack/plugins/observability_solution/entity_manager/server/routes/entities/delete.ts index f79fdce2368c6c..b0c423a47a4b9b 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/routes/entities/delete.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/routes/entities/delete.ts @@ -6,7 +6,11 @@ */ import { RequestHandlerContext } from '@kbn/core/server'; -import { schema } from '@kbn/config-schema'; +import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; +import { + deleteEntityDefinitionParamsSchema, + deleteEntityDefinitionQuerySchema, +} from '@kbn/entities-schema'; import { SetupRouteOptions } from '../types'; import { EntitySecurityException } from '../../lib/entities/errors/entity_security_exception'; import { InvalidTransformError } from '../../lib/entities/errors/invalid_transform_error'; @@ -22,12 +26,8 @@ export function deleteEntityDefinitionRoute({ { path: '/internal/entities/definition/{id}', validate: { - params: schema.object({ - id: schema.string(), - }), - query: schema.object({ - deleteData: schema.maybe(schema.boolean({ defaultValue: false })), - }), + params: buildRouteValidationWithZod(deleteEntityDefinitionParamsSchema.strict()), + query: buildRouteValidationWithZod(deleteEntityDefinitionQuerySchema.strict()), }, }, async (context, req, res) => { diff --git a/x-pack/plugins/observability_solution/entity_manager/server/routes/entities/get.ts b/x-pack/plugins/observability_solution/entity_manager/server/routes/entities/get.ts index 25a593c05209e2..3f1ffde5afef4d 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/routes/entities/get.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/routes/entities/get.ts @@ -6,7 +6,8 @@ */ import { RequestHandlerContext } from '@kbn/core/server'; -import { schema } from '@kbn/config-schema'; +import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; +import { getEntityDefinitionQuerySchema } from '@kbn/entities-schema'; import { SetupRouteOptions } from '../types'; import { findEntityDefinitions } from '../../lib/entities/find_entity_definition'; @@ -17,10 +18,7 @@ export function getEntityDefinitionRoute({ { path: '/internal/entities/definition', validate: { - query: schema.object({ - page: schema.maybe(schema.number()), - perPage: schema.maybe(schema.number()), - }), + query: buildRouteValidationWithZod(getEntityDefinitionQuerySchema.strict()), }, }, async (context, req, res) => { diff --git a/x-pack/plugins/observability_solution/entity_manager/server/routes/entities/reset.ts b/x-pack/plugins/observability_solution/entity_manager/server/routes/entities/reset.ts index ffa85931a3beff..6f97a5fbe0d519 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/routes/entities/reset.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/routes/entities/reset.ts @@ -6,7 +6,8 @@ */ import { RequestHandlerContext } from '@kbn/core/server'; -import { schema } from '@kbn/config-schema'; +import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; +import { resetEntityDefinitionParamsSchema } from '@kbn/entities-schema'; import { SetupRouteOptions } from '../types'; import { EntitySecurityException } from '../../lib/entities/errors/entity_security_exception'; import { InvalidTransformError } from '../../lib/entities/errors/invalid_transform_error'; @@ -39,9 +40,7 @@ export function resetEntityDefinitionRoute({ { path: '/internal/entities/definition/{id}/_reset', validate: { - params: schema.object({ - id: schema.string(), - }), + params: buildRouteValidationWithZod(resetEntityDefinitionParamsSchema.strict()), }, }, async (context, req, res) => { From 013276edac16b3437b7f125b6eaec1b6ad949c87 Mon Sep 17 00:00:00 2001 From: Dzmitry Lemechko Date: Mon, 22 Jul 2024 19:27:40 +0200 Subject: [PATCH 14/54] [kbn-test] improve run_check_ftr_configs_cli script (#188854) ## Summary Follow-up to #188825 @crespocarlos reported that some Oblt configs after missing after #187440 I was using `node scripts/check_ftr_configs.js` to validate I did not miss anything and decided to debug the script. We had a pretty strict config file content validation like `testRunner|testFiles`, that was skipping some FTR configs like `x-pack/test/apm_api_integration/basic/config.ts` I extended file content check to look for default export function and also skip test/suite or Cypress-own config files. In the end 7 FTR configs were discovered, but only 2 are with tests. I will ask owners to confirm if it should be enabled/disabled. Script run output: ``` node scripts/check_ftr_configs.js ERROR The following files look like FTR configs which are not listed in one of manifest files: - x-pack/plugins/observability_solution/uptime/e2e/config.ts - x-pack/test/functional_basic/apps/ml/config.base.ts - x-pack/test/functional_basic/apps/transform/config.base.ts - x-pack/test/security_solution_api_integration/config/ess/config.base.trial.ts - x-pack/test_serverless/functional/test_suites/observability/cypress/oblt_config.base.ts Make sure to add your new FTR config to the correct manifest file. Stateful tests: .buildkite/ftr_platform_stateful_configs.yml .buildkite/ftr_oblt_stateful_configs.yml .buildkite/ftr_security_stateful_configs.yml .buildkite/ftr_search_stateful_configs.yml Serverless tests: .buildkite/ftr_base_serverless_configs.yml .buildkite/ftr_oblt_serverless_configs.yml .buildkite/ftr_security_serverless_configs.yml .buildkite/ftr_search_serverless_configs.yml ERROR Please add the listed paths to the correct manifest file. If it's not an FTR config, you can add it to the IGNORED_PATHS in packages/kbn-test/src/functional_test_runner/lib/config/run_check_ftr_configs_cli.ts or contact #kibana-operations ``` --- .buildkite/ftr_oblt_serverless_configs.yml | 1 + .buildkite/ftr_oblt_stateful_configs.yml | 3 ++ .buildkite/ftr_platform_stateful_configs.yml | 2 + .../ftr_security_serverless_configs.yml | 2 + .buildkite/ftr_security_stateful_configs.yml | 1 + .../lib/config/run_check_ftr_configs_cli.ts | 39 ++++++++++++++++--- 6 files changed, 42 insertions(+), 6 deletions(-) diff --git a/.buildkite/ftr_oblt_serverless_configs.yml b/.buildkite/ftr_oblt_serverless_configs.yml index 9534e62926f068..085c25f2d80a67 100644 --- a/.buildkite/ftr_oblt_serverless_configs.yml +++ b/.buildkite/ftr_oblt_serverless_configs.yml @@ -1,5 +1,6 @@ disabled: # Base config files, only necessary to inform config finding script + - x-pack/test_serverless/functional/test_suites/observability/cypress/oblt_config.base.ts # Cypress configs, for now these are still run manually - x-pack/test_serverless/functional/test_suites/observability/cypress/config_headless.ts diff --git a/.buildkite/ftr_oblt_stateful_configs.yml b/.buildkite/ftr_oblt_stateful_configs.yml index d9f557dac7f6aa..4edf75f385816a 100644 --- a/.buildkite/ftr_oblt_stateful_configs.yml +++ b/.buildkite/ftr_oblt_stateful_configs.yml @@ -10,6 +10,9 @@ disabled: - x-pack/plugins/observability_solution/profiling/e2e/ftr_config_runner.ts - x-pack/plugins/observability_solution/profiling/e2e/ftr_config.ts + #FTR configs + - x-pack/plugins/observability_solution/uptime/e2e/config.ts + # Elastic Synthetics configs - x-pack/plugins/observability_solution/uptime/e2e/uptime/synthetics_run.ts - x-pack/plugins/observability_solution/synthetics/e2e/config.ts diff --git a/.buildkite/ftr_platform_stateful_configs.yml b/.buildkite/ftr_platform_stateful_configs.yml index a0425f766f5696..96c15cce513c68 100644 --- a/.buildkite/ftr_platform_stateful_configs.yml +++ b/.buildkite/ftr_platform_stateful_configs.yml @@ -8,6 +8,8 @@ disabled: - x-pack/test/functional_with_es_ssl/config.base.ts - x-pack/test/api_integration/config.ts - x-pack/test/fleet_api_integration/config.base.ts + - x-pack/test/functional_basic/apps/ml/config.base.ts + - x-pack/test/functional_basic/apps/transform/config.base.ts # QA suites that are run out-of-band - x-pack/test/stack_functional_integration/configs/config.stack_functional_integration_base.js diff --git a/.buildkite/ftr_security_serverless_configs.yml b/.buildkite/ftr_security_serverless_configs.yml index 51e3eba941c6b7..3880175623fdd3 100644 --- a/.buildkite/ftr_security_serverless_configs.yml +++ b/.buildkite/ftr_security_serverless_configs.yml @@ -1,5 +1,7 @@ disabled: # Base config files, only necessary to inform config finding script + - x-pack/test_serverless/functional/test_suites/security/cypress/security_config.base.ts + - x-pack/test_serverless/functional/test_suites/security/cypress/cypress.config.ts - x-pack/test/security_solution_api_integration/config/serverless/config.base.ts - x-pack/test/security_solution_api_integration/config/serverless/config.base.essentials.ts - x-pack/test/security_solution_api_integration/config/serverless/config.base.edr_workflows.ts diff --git a/.buildkite/ftr_security_stateful_configs.yml b/.buildkite/ftr_security_stateful_configs.yml index 8f1605b363e3d9..a7931bab0a68d7 100644 --- a/.buildkite/ftr_security_stateful_configs.yml +++ b/.buildkite/ftr_security_stateful_configs.yml @@ -5,6 +5,7 @@ disabled: - x-pack/test/security_solution_api_integration/config/ess/config.base.edr_workflows.trial.ts - x-pack/test/security_solution_api_integration/config/ess/config.base.edr_workflows.ts - x-pack/test/security_solution_api_integration/config/ess/config.base.basic.ts + - x-pack/test/security_solution_api_integration/config/ess/config.base.trial.ts - x-pack/test/security_solution_endpoint/configs/config.base.ts - x-pack/test/security_solution_endpoint/config.base.ts - x-pack/test/security_solution_endpoint_api_int/config.base.ts diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/run_check_ftr_configs_cli.ts b/packages/kbn-test/src/functional_test_runner/lib/config/run_check_ftr_configs_cli.ts index 31afcac7593572..57f819bb44771f 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/config/run_check_ftr_configs_cli.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/config/run_check_ftr_configs_cli.ts @@ -62,13 +62,36 @@ export async function runCheckFtrConfigsCli() { return false; } - if (file.match(/jest.config.(t|j)s$/)) { + if (file.match(/(jest(\.integration)?)\.config\.(t|j)s$/)) { return false; } - return readFileSync(file) - .toString() - .match(/(testRunner)|(testFiles)/); + if (file.match(/mocks.ts$/)) { + return false; + } + + const fileContent = readFileSync(file).toString(); + + if (fileContent.match(/(testRunner)|(testFiles)/)) { + // test config + return true; + } + + if (fileContent.match(/(describe)|(defineCypressConfig)/)) { + // test file or Cypress config + return false; + } + + // FTR config file should have default export + try { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const exports = require(file); + const defaultExport = exports.__esModule ? exports.default : exports; + return !!defaultExport; + } catch (err) { + log.debug(`Failed to load file: ${err.message}`); + return false; + } }); const { allFtrConfigs, manifestPaths } = getAllFtrConfigsAndManifests(); @@ -77,10 +100,14 @@ export async function runCheckFtrConfigsCli() { if (invalid.length) { const invalidList = invalid.map((path) => Path.relative(REPO_ROOT, path)).join('\n - '); log.error( - `The following files look like FTR configs which are not listed in one of manifest files:\nstateful: ${manifestPaths.stateful}\nserverless: ${manifestPaths.serverless}\n - ${invalidList}` + `The following files look like FTR configs which are not listed in one of manifest files:\n${invalidList}\n +Make sure to add your new FTR config to the correct manifest file.\n +Stateful tests:\n${(manifestPaths.stateful as string[]).join('\n')}\n +Serverless tests:\n${(manifestPaths.serverless as string[]).join('\n')} + ` ); throw createFailError( - `Please add the listed paths to the correct manifest file. If it's not an FTR config, you can add it to the IGNORED_PATHS in ${THIS_REL} or contact #kibana-operations` + `Please add the listed paths to the correct manifest files. If it's not an FTR config, you can add it to the IGNORED_PATHS in ${THIS_REL} or contact #kibana-operations` ); } }, From 232a16637d3ee03a435224eb11852c4f2f1b1810 Mon Sep 17 00:00:00 2001 From: Juan Pablo Djeredjian Date: Mon, 22 Jul 2024 19:36:31 +0200 Subject: [PATCH 15/54] [Security Solution] Implement normalization of ruleSource for API responses (#188631) Fixes: https://github.com/elastic/kibana/issues/180140 ## Summary - Implements normalization of`rule_source` for API responses - `rule_source` field in API responses is calculated out of the `immutable` and `ruleSource` fields. ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --- .../routes/__mocks__/utils.ts | 3 + ...egacy_rules_notification_rule_type.test.ts | 3 + .../internal_rule_to_api_response.ts | 4 +- .../converters/normalize_rule_params.test.ts | 55 ++++++++++++++ .../converters/normalize_rule_params.ts | 48 ++++++++++++ .../logic/export/get_export_all.test.ts | 6 ++ .../rule_management/utils/validate.test.ts | 74 +------------------ .../rule_schema/model/rule_schemas.mock.ts | 3 + .../factories/utils/build_alert.test.ts | 6 ++ 9 files changed, 129 insertions(+), 73 deletions(-) create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/normalize_rule_params.test.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/normalize_rule_params.ts diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts index 819bf87165e126..687bf91655e2ae 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts @@ -47,6 +47,9 @@ export const getOutputRuleAlertForRest = (): RuleResponse => ({ from: 'now-6m', id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', immutable: false, + rule_source: { + type: 'internal', + }, index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], interval: '5m', risk_score: 50, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions_legacy/logic/notifications/legacy_rules_notification_rule_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions_legacy/logic/notifications/legacy_rules_notification_rule_type.test.ts index 767c01f02b1875..4adf71d258e0a0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions_legacy/logic/notifications/legacy_rules_notification_rule_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions_legacy/logic/notifications/legacy_rules_notification_rule_type.test.ts @@ -76,6 +76,9 @@ const reported = { from: 'now-6m', id: 'rule-id', immutable: false, + rule_source: { + type: 'internal', + }, index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], investigation_fields: undefined, language: 'kuery', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/internal_rule_to_api_response.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/internal_rule_to_api_response.ts index 452f59df8dcf93..349f54b1e3b3ca 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/internal_rule_to_api_response.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/internal_rule_to_api_response.ts @@ -17,6 +17,7 @@ import { } from '../../../normalization/rule_actions'; import { typeSpecificCamelToSnake } from './type_specific_camel_to_snake'; import { commonParamsCamelToSnake } from './common_params_camel_to_snake'; +import { normalizeRuleParams } from './normalize_rule_params'; export const internalRuleToAPIResponse = ( rule: SanitizedRule | ResolvedSanitizedRule @@ -31,6 +32,7 @@ export const internalRuleToAPIResponse = ( const alertActions = rule.actions.map(transformAlertToRuleAction); const throttle = transformFromAlertThrottle(rule); const actions = transformToActionFrequency(alertActions, throttle); + const normalizedRuleParams = normalizeRuleParams(rule.params); return { // saved object properties @@ -49,7 +51,7 @@ export const internalRuleToAPIResponse = ( enabled: rule.enabled, revision: rule.revision, // Security solution shared rule params - ...commonParamsCamelToSnake(rule.params), + ...commonParamsCamelToSnake(normalizedRuleParams), // Type specific security solution rule params ...typeSpecificCamelToSnake(rule.params), // Actions diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/normalize_rule_params.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/normalize_rule_params.test.ts new file mode 100644 index 00000000000000..b8b5db137583b0 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/normalize_rule_params.test.ts @@ -0,0 +1,55 @@ +/* + * 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 { normalizeRuleSource } from './normalize_rule_params'; +import type { BaseRuleParams } from '../../../../rule_schema'; + +describe('normalizeRuleSource', () => { + it('should return rule_source of type `internal` when immutable is false and ruleSource is undefined', () => { + const result = normalizeRuleSource({ + immutable: false, + ruleSource: undefined, + }); + expect(result).toEqual({ + type: 'internal', + }); + }); + + it('should return rule_source of type `external` and `isCustomized: false` when immutable is true and ruleSource is undefined', () => { + const result = normalizeRuleSource({ + immutable: true, + ruleSource: undefined, + }); + expect(result).toEqual({ + type: 'external', + isCustomized: false, + }); + }); + + it('should return existing value when ruleSource is present', () => { + const externalRuleSource: BaseRuleParams['ruleSource'] = { + type: 'external', + isCustomized: true, + }; + const externalResult = normalizeRuleSource({ immutable: true, ruleSource: externalRuleSource }); + expect(externalResult).toEqual({ + type: externalRuleSource.type, + isCustomized: externalRuleSource.isCustomized, + }); + + const internalRuleSource: BaseRuleParams['ruleSource'] = { + type: 'internal', + }; + const internalResult = normalizeRuleSource({ + immutable: false, + ruleSource: internalRuleSource, + }); + expect(internalResult).toEqual({ + type: internalRuleSource.type, + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/normalize_rule_params.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/normalize_rule_params.ts new file mode 100644 index 00000000000000..eddd8b0434ba09 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/normalize_rule_params.ts @@ -0,0 +1,48 @@ +/* + * 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 type { BaseRuleParams, RuleSourceCamelCased } from '../../../../rule_schema'; + +interface NormalizeRuleSourceParams { + immutable: BaseRuleParams['immutable']; + ruleSource: BaseRuleParams['ruleSource']; +} + +/* + * Since there's no mechanism to migrate all rules at the same time, + * we cannot guarantee that the ruleSource params is present in all rules. + * This function will normalize the ruleSource param, creating it if does + * not exist in ES, based on the immutable param. + */ +export const normalizeRuleSource = ({ + immutable, + ruleSource, +}: NormalizeRuleSourceParams): RuleSourceCamelCased => { + if (!ruleSource) { + const normalizedRuleSource: RuleSourceCamelCased = immutable + ? { + type: 'external', + isCustomized: false, + } + : { + type: 'internal', + }; + + return normalizedRuleSource; + } + return ruleSource; +}; + +export const normalizeRuleParams = (params: BaseRuleParams) => { + return { + ...params, + // Fields to normalize + ruleSource: normalizeRuleSource({ + immutable: params.immutable, + ruleSource: params.ruleSource, + }), + }; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_all.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_all.test.ts index 0ba0afbce715a1..382df4bfa5ffc7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_all.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_all.test.ts @@ -100,6 +100,9 @@ describe('getExportAll', () => { from: 'now-6m', id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', immutable: false, + rule_source: { + type: 'internal', + }, index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], interval: '5m', rule_id: 'rule-1', @@ -280,6 +283,9 @@ describe('getExportAll', () => { from: 'now-6m', id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', immutable: false, + rule_source: { + type: 'internal', + }, index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], interval: '5m', rule_id: 'rule-1', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/validate.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/validate.test.ts index f11e31691d25b0..c9a5a93a4f1c37 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/validate.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/validate.test.ts @@ -8,85 +8,15 @@ import { transformValidateBulkError } from './validate'; import type { BulkError } from '../../routes/utils'; import { getRuleMock } from '../../routes/__mocks__/request_responses'; -import { getListArrayMock } from '../../../../../common/detection_engine/schemas/types/lists.mock'; -import { getThreatMock } from '../../../../../common/detection_engine/schemas/types/threat.mock'; import { getQueryRuleParams } from '../../rule_schema/mocks'; -import type { RuleResponse } from '../../../../../common/api/detection_engine/model/rule_schema'; - -export const ruleOutput = (): RuleResponse => ({ - actions: [], - author: ['Elastic'], - building_block_type: 'default', - created_at: '2019-12-13T16:40:33.400Z', - updated_at: '2019-12-13T16:40:33.400Z', - created_by: 'elastic', - description: 'Detecting root and admin users', - enabled: true, - false_positives: [], - from: 'now-6m', - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - immutable: false, - interval: '5m', - rule_id: 'rule-1', - language: 'kuery', - license: 'Elastic License', - output_index: '.siem-signals', - max_signals: 10000, - risk_score: 50, - risk_score_mapping: [], - name: 'Detect Root/Admin Users', - query: 'user.name: root or user.name: admin', - references: ['http://example.com', 'https://example.com'], - severity: 'high', - severity_mapping: [], - updated_by: 'elastic', - tags: [], - to: 'now', - type: 'query', - throttle: undefined, - threat: getThreatMock(), - version: 1, - revision: 0, - filters: [ - { - query: { - match_phrase: { - 'host.name': 'some-host', - }, - }, - }, - ], - exceptions_list: getListArrayMock(), - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - meta: { - someMeta: 'someField', - }, - note: '# Investigative notes', - timeline_title: 'some-timeline-title', - timeline_id: 'some-timeline-id', - related_integrations: [], - required_fields: [], - response_actions: undefined, - setup: '', - outcome: undefined, - alias_target_id: undefined, - alias_purpose: undefined, - rule_name_override: undefined, - timestamp_override: undefined, - timestamp_override_fallback_disabled: undefined, - namespace: undefined, - data_view_id: undefined, - saved_id: undefined, - alert_suppression: undefined, - investigation_fields: undefined, -}); +import { getOutputRuleAlertForRest } from '../../routes/__mocks__/utils'; describe('validate', () => { describe('transformValidateBulkError', () => { test('it should do a validation correctly of a rule id', () => { const ruleAlert = getRuleMock(getQueryRuleParams()); const validatedOrError = transformValidateBulkError('rule-1', ruleAlert); - expect(validatedOrError).toEqual(ruleOutput()); + expect(validatedOrError).toEqual(getOutputRuleAlertForRest()); }); test('it should do an in-validation correctly of a rule id', () => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.mock.ts index 3a4fa1dadd7781..8099d7a00049fa 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.mock.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.mock.ts @@ -32,6 +32,9 @@ export const getBaseRuleParams = (): BaseRuleParams => { description: 'Detecting root and admin users', falsePositives: [], immutable: false, + ruleSource: { + type: 'internal', + }, from: 'now-6m', to: 'now', severity: 'high', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.test.ts index ffb5f6ee451702..4aaa0189eefc4e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.test.ts @@ -162,6 +162,9 @@ describe('buildAlert', () => { }, ], immutable: false, + rule_source: { + type: 'internal', + }, type: 'query', language: 'kuery', index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], @@ -357,6 +360,9 @@ describe('buildAlert', () => { }, ], immutable: false, + rule_source: { + type: 'internal', + }, type: 'query', language: 'kuery', index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], From 0c5d7b95c0a6783a54a415797bed3e54065251e7 Mon Sep 17 00:00:00 2001 From: Juan Pablo Djeredjian Date: Mon, 22 Jul 2024 19:53:13 +0200 Subject: [PATCH 16/54] [Security Solution] Remove remaining usage of rule_schema_legacy types (#188079) ## Summary Leftover work from https://github.com/elastic/kibana/pull/186615 - Removes remaining usage of `rule_schema_legacy` types. In this PR, simply inlines the last io-ts types used, to be able to get rid of the legacy folder. - The remaining files that need to be migrated to using Zod schema types are: - `x-pack/plugins/security_solution/common/api/detection_engine/rule_exceptions/find_exception_references/find_exception_references_route.ts` - `x-pack/plugins/security_solution/common/api/timeline/model/api.ts` ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --------- Co-authored-by: Georgii Gorbachev --- .../rule_schema_legacy/common_attributes.ts | 60 ------------------- .../model/rule_schema_legacy/index.ts | 8 --- .../find_exception_references_route.ts | 13 +++- .../common/api/timeline/model/api.ts | 31 ++++++++-- 4 files changed, 35 insertions(+), 77 deletions(-) delete mode 100644 x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema_legacy/common_attributes.ts delete mode 100644 x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema_legacy/index.ts diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema_legacy/common_attributes.ts b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema_legacy/common_attributes.ts deleted file mode 100644 index ba07c49a7b1303..00000000000000 --- a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema_legacy/common_attributes.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* - * 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 t from 'io-ts'; -import { NonEmptyString, UUID } from '@kbn/securitysolution-io-ts-types'; - -/* -IMPORTANT NOTE ON THIS FILE: - -This file contains the remaining rule schema types created manually via io-ts. They have been -migrated to Zod schemas created via code generation out of OpenAPI schemas -(found in x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/common_attributes.gen.ts) - -The remaining types here couldn't easily be deleted/replaced because they are dependencies in -complex derived schemas in two files: - -- x-pack/plugins/security_solution/common/api/detection_engine/rule_exceptions/find_exception_references/find_exception_references_route.ts -- x-pack/plugins/security_solution/common/api/timeline/model/api.ts - -Once those two files are migrated to Zod, the /common/api/detection_engine/model/rule_schema_legacy -folder can be removed. -*/ - -export type RuleObjectId = t.TypeOf; -export const RuleObjectId = UUID; - -/** - * NOTE: Never make this a strict uuid, we allow the rule_id to be any string at the moment - * in case we encounter 3rd party rule systems which might be using auto incrementing numbers - * or other different things. - */ -export type RuleSignatureId = t.TypeOf; -export const RuleSignatureId = t.string; // should be non-empty string? - -export type RuleName = t.TypeOf; -export const RuleName = NonEmptyString; - -/** - * Outcome is a property of the saved object resolve api - * will tell us info about the rule after 8.0 migrations - */ -export type SavedObjectResolveOutcome = t.TypeOf; -export const SavedObjectResolveOutcome = t.union([ - t.literal('exactMatch'), - t.literal('aliasMatch'), - t.literal('conflict'), -]); - -export type SavedObjectResolveAliasTargetId = t.TypeOf; -export const SavedObjectResolveAliasTargetId = t.string; - -export type SavedObjectResolveAliasPurpose = t.TypeOf; -export const SavedObjectResolveAliasPurpose = t.union([ - t.literal('savedObjectConversion'), - t.literal('savedObjectImport'), -]); diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema_legacy/index.ts b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema_legacy/index.ts deleted file mode 100644 index a112f6ca1b29f8..00000000000000 --- a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema_legacy/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * 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. - */ - -export * from './common_attributes'; diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_exceptions/find_exception_references/find_exception_references_route.ts b/x-pack/plugins/security_solution/common/api/detection_engine/rule_exceptions/find_exception_references/find_exception_references_route.ts index cbef9a41de7182..63b9363bb97c40 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/rule_exceptions/find_exception_references/find_exception_references_route.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_exceptions/find_exception_references/find_exception_references_route.ts @@ -12,10 +12,17 @@ import { list_id, DefaultNamespaceArray, } from '@kbn/securitysolution-io-ts-list-types'; -import { NonEmptyStringArray } from '@kbn/securitysolution-io-ts-types'; +import { NonEmptyStringArray, NonEmptyString, UUID } from '@kbn/securitysolution-io-ts-types'; + // TODO https://github.com/elastic/security-team/issues/7491 -// eslint-disable-next-line no-restricted-imports -import { RuleName, RuleObjectId, RuleSignatureId } from '../../model/rule_schema_legacy'; +type RuleObjectId = t.TypeOf; +const RuleObjectId = UUID; + +type RuleSignatureId = t.TypeOf; +const RuleSignatureId = t.string; + +type RuleName = t.TypeOf; +const RuleName = NonEmptyString; // If ids and list_ids are undefined, route will fetch all lists matching the // specified namespace type diff --git a/x-pack/plugins/security_solution/common/api/timeline/model/api.ts b/x-pack/plugins/security_solution/common/api/timeline/model/api.ts index 3e69bd14b646ca..10b12aee32f2fe 100644 --- a/x-pack/plugins/security_solution/common/api/timeline/model/api.ts +++ b/x-pack/plugins/security_solution/common/api/timeline/model/api.ts @@ -15,12 +15,31 @@ import { Direction } from '../../../search_strategy'; import type { PinnedEvent } from '../pinned_events/pinned_events_route'; import { PinnedEventRuntimeType } from '../pinned_events/pinned_events_route'; // TODO https://github.com/elastic/security-team/issues/7491 -// eslint-disable-next-line no-restricted-imports -import { - SavedObjectResolveAliasPurpose, - SavedObjectResolveAliasTargetId, - SavedObjectResolveOutcome, -} from '../../detection_engine/model/rule_schema_legacy'; + +/** + * Outcome is a property of the saved object resolve api + * will tell us info about the rule after 8.0 migrations + */ +export type SavedObjectResolveOutcome = runtimeTypes.TypeOf; +export const SavedObjectResolveOutcome = runtimeTypes.union([ + runtimeTypes.literal('exactMatch'), + runtimeTypes.literal('aliasMatch'), + runtimeTypes.literal('conflict'), +]); + +export type SavedObjectResolveAliasTargetId = runtimeTypes.TypeOf< + typeof SavedObjectResolveAliasTargetId +>; +export const SavedObjectResolveAliasTargetId = runtimeTypes.string; + +export type SavedObjectResolveAliasPurpose = runtimeTypes.TypeOf< + typeof SavedObjectResolveAliasPurpose +>; +export const SavedObjectResolveAliasPurpose = runtimeTypes.union([ + runtimeTypes.literal('savedObjectConversion'), + runtimeTypes.literal('savedObjectImport'), +]); + import { ErrorSchema } from './error_schema'; export const BareNoteSchema = runtimeTypes.intersection([ From 0077b0e645b429184f5b765c1b2353c0f0a3216a Mon Sep 17 00:00:00 2001 From: Ievgen Sorokopud Date: Mon, 22 Jul 2024 20:32:38 +0200 Subject: [PATCH 17/54] [Security Solution][Detections][BUG] Rule execution error when source document has a non-ECS compliant text field (#187630) (#187673) ## Summary - https://github.com/elastic/kibana/issues/187630 - https://github.com/elastic/kibana/issues/187768 These changes fix the error on saving the alert > An error occurred during rule execution: message: "[1:6952] failed to parse field [event.original] of type [keyword] in document with id '330b17dc2ac382dbdd2f2577c28e83b42c5dc66eaf95e857ec0f222abfc486fa'..." The issue happens when source index has non-ECS compliant text field which is expected to be a keyword. If the text value is longer than 32766 bytes and keyword field does not have ignore_above parameter set, then on trying to store the text value in keyword field we will hit the Lucene's term byte-length limit (for more details see [this page](https://www.elastic.co/guide/en/elasticsearch/reference/current/ignore-above.html)). See the main ticket for steps to reproduce the issue. --------- Co-authored-by: Vitalii Dmyterko <92328789+vitaliidm@users.noreply.github.com> --- .../src/field_maps/alert_field_map.ts | 9 + .../src/schemas/generated/alert_schema.ts | 1 + .../src/schemas/generated/security_schema.ts | 1 + .../src/legacy_alerts_as_data.ts | 2 + .../field_maps/mapping_from_field_map.test.ts | 6 + .../alert_as_data_fields.test.ts.snap | 208 ++++++++++++++++++ .../technical_rule_field_map.test.ts | 8 + .../common/field_maps/8.16.0/alerts.ts | 122 ++++++++++ .../common/field_maps/8.16.0/index.ts | 11 + .../common/field_maps/index.ts | 8 +- .../ecs_non_compliant/mappings.json | 6 + .../execution_logic/non_ecs_fields.ts | 41 ++++ 12 files changed, 419 insertions(+), 4 deletions(-) create mode 100644 x-pack/plugins/security_solution/common/field_maps/8.16.0/alerts.ts create mode 100644 x-pack/plugins/security_solution/common/field_maps/8.16.0/index.ts diff --git a/packages/kbn-alerts-as-data-utils/src/field_maps/alert_field_map.ts b/packages/kbn-alerts-as-data-utils/src/field_maps/alert_field_map.ts index 73a3535857041b..6893d6b32bd930 100644 --- a/packages/kbn-alerts-as-data-utils/src/field_maps/alert_field_map.ts +++ b/packages/kbn-alerts-as-data-utils/src/field_maps/alert_field_map.ts @@ -44,6 +44,7 @@ import { VERSION, EVENT_ACTION, EVENT_KIND, + EVENT_ORIGINAL, TAGS, } from '@kbn/rule-data-utils'; import { MultiField } from './types'; @@ -224,11 +225,19 @@ export const alertFieldMap = { type: 'keyword', array: false, required: false, + ignore_above: 1024, }, [EVENT_KIND]: { type: 'keyword', array: false, required: false, + ignore_above: 1024, + }, + [EVENT_ORIGINAL]: { + type: 'keyword', + array: false, + required: false, + ignore_above: 1024, }, [SPACE_IDS]: { type: 'keyword', diff --git a/packages/kbn-alerts-as-data-utils/src/schemas/generated/alert_schema.ts b/packages/kbn-alerts-as-data-utils/src/schemas/generated/alert_schema.ts index 935a09971c6134..bcd28d651616d8 100644 --- a/packages/kbn-alerts-as-data-utils/src/schemas/generated/alert_schema.ts +++ b/packages/kbn-alerts-as-data-utils/src/schemas/generated/alert_schema.ts @@ -84,6 +84,7 @@ const AlertRequired = rt.type({ const AlertOptional = rt.partial({ 'event.action': schemaString, 'event.kind': schemaString, + 'event.original': schemaString, 'kibana.alert.action_group': schemaString, 'kibana.alert.case_ids': schemaStringArray, 'kibana.alert.consecutive_matches': schemaStringOrNumber, diff --git a/packages/kbn-alerts-as-data-utils/src/schemas/generated/security_schema.ts b/packages/kbn-alerts-as-data-utils/src/schemas/generated/security_schema.ts index 14fdb859ed3e9a..8b608ef4cc5cdb 100644 --- a/packages/kbn-alerts-as-data-utils/src/schemas/generated/security_schema.ts +++ b/packages/kbn-alerts-as-data-utils/src/schemas/generated/security_schema.ts @@ -122,6 +122,7 @@ const SecurityAlertOptional = rt.partial({ 'ecs.version': schemaString, 'event.action': schemaString, 'event.kind': schemaString, + 'event.original': schemaString, 'host.asset.criticality': schemaString, 'kibana.alert.action_group': schemaString, 'kibana.alert.ancestors.rule': schemaString, diff --git a/packages/kbn-rule-data-utils/src/legacy_alerts_as_data.ts b/packages/kbn-rule-data-utils/src/legacy_alerts_as_data.ts index 187b7d6bd2f1d1..a6249ff0b948ca 100644 --- a/packages/kbn-rule-data-utils/src/legacy_alerts_as_data.ts +++ b/packages/kbn-rule-data-utils/src/legacy_alerts_as_data.ts @@ -11,6 +11,7 @@ import { ALERT_NAMESPACE, ALERT_RULE_NAMESPACE } from './default_alerts_as_data' const ECS_VERSION = 'ecs.version' as const; const EVENT_ACTION = 'event.action' as const; const EVENT_KIND = 'event.kind' as const; +const EVENT_ORIGINAL = 'event.original' as const; const TAGS = 'tags' as const; // These are the fields that are in the rule registry technical component template @@ -82,5 +83,6 @@ export { ECS_VERSION, EVENT_ACTION, EVENT_KIND, + EVENT_ORIGINAL, TAGS, }; diff --git a/x-pack/plugins/alerting/common/alert_schema/field_maps/mapping_from_field_map.test.ts b/x-pack/plugins/alerting/common/alert_schema/field_maps/mapping_from_field_map.test.ts index aad7bb28236067..d775c38117e4c2 100644 --- a/x-pack/plugins/alerting/common/alert_schema/field_maps/mapping_from_field_map.test.ts +++ b/x-pack/plugins/alerting/common/alert_schema/field_maps/mapping_from_field_map.test.ts @@ -195,9 +195,15 @@ describe('mappingFromFieldMap', () => { properties: { action: { type: 'keyword', + ignore_above: 1024, }, kind: { type: 'keyword', + ignore_above: 1024, + }, + original: { + type: 'keyword', + ignore_above: 1024, }, }, }, diff --git a/x-pack/plugins/alerting/server/integration_tests/__snapshots__/alert_as_data_fields.test.ts.snap b/x-pack/plugins/alerting/server/integration_tests/__snapshots__/alert_as_data_fields.test.ts.snap index 6a274606f420bc..c1a9a0a77a9f77 100644 --- a/x-pack/plugins/alerting/server/integration_tests/__snapshots__/alert_as_data_fields.test.ts.snap +++ b/x-pack/plugins/alerting/server/integration_tests/__snapshots__/alert_as_data_fields.test.ts.snap @@ -721,11 +721,19 @@ Object { }, "event.action": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, "event.kind": Object { "array": false, + "ignore_above": 1024, + "required": false, + "type": "keyword", + }, + "event.original": Object { + "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, @@ -846,21 +854,25 @@ Object { }, "kibana.alert.original_event.action": Object { "array": false, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.agent_id_status": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, "kibana.alert.original_event.category": Object { "array": true, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.code": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, @@ -871,11 +883,13 @@ Object { }, "kibana.alert.original_event.dataset": Object { "array": false, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.duration": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, @@ -886,11 +900,13 @@ Object { }, "kibana.alert.original_event.hash": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, "kibana.alert.original_event.id": Object { "array": false, + "ignore_above": 1024, "required": true, "type": "keyword", }, @@ -901,36 +917,43 @@ Object { }, "kibana.alert.original_event.kind": Object { "array": false, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.module": Object { "array": false, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.original": Object { "array": false, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.outcome": Object { "array": false, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.provider": Object { "array": false, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.reason": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, "kibana.alert.original_event.reference": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, @@ -961,16 +984,19 @@ Object { }, "kibana.alert.original_event.timezone": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, "kibana.alert.original_event.type": Object { "array": true, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.url": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, @@ -1788,11 +1814,19 @@ Object { }, "event.action": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, "event.kind": Object { "array": false, + "ignore_above": 1024, + "required": false, + "type": "keyword", + }, + "event.original": Object { + "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, @@ -1913,21 +1947,25 @@ Object { }, "kibana.alert.original_event.action": Object { "array": false, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.agent_id_status": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, "kibana.alert.original_event.category": Object { "array": true, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.code": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, @@ -1938,11 +1976,13 @@ Object { }, "kibana.alert.original_event.dataset": Object { "array": false, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.duration": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, @@ -1953,11 +1993,13 @@ Object { }, "kibana.alert.original_event.hash": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, "kibana.alert.original_event.id": Object { "array": false, + "ignore_above": 1024, "required": true, "type": "keyword", }, @@ -1968,36 +2010,43 @@ Object { }, "kibana.alert.original_event.kind": Object { "array": false, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.module": Object { "array": false, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.original": Object { "array": false, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.outcome": Object { "array": false, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.provider": Object { "array": false, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.reason": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, "kibana.alert.original_event.reference": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, @@ -2028,16 +2077,19 @@ Object { }, "kibana.alert.original_event.timezone": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, "kibana.alert.original_event.type": Object { "array": true, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.url": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, @@ -2855,11 +2907,19 @@ Object { }, "event.action": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, "event.kind": Object { "array": false, + "ignore_above": 1024, + "required": false, + "type": "keyword", + }, + "event.original": Object { + "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, @@ -2980,21 +3040,25 @@ Object { }, "kibana.alert.original_event.action": Object { "array": false, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.agent_id_status": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, "kibana.alert.original_event.category": Object { "array": true, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.code": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, @@ -3005,11 +3069,13 @@ Object { }, "kibana.alert.original_event.dataset": Object { "array": false, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.duration": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, @@ -3020,11 +3086,13 @@ Object { }, "kibana.alert.original_event.hash": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, "kibana.alert.original_event.id": Object { "array": false, + "ignore_above": 1024, "required": true, "type": "keyword", }, @@ -3035,36 +3103,43 @@ Object { }, "kibana.alert.original_event.kind": Object { "array": false, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.module": Object { "array": false, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.original": Object { "array": false, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.outcome": Object { "array": false, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.provider": Object { "array": false, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.reason": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, "kibana.alert.original_event.reference": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, @@ -3095,16 +3170,19 @@ Object { }, "kibana.alert.original_event.timezone": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, "kibana.alert.original_event.type": Object { "array": true, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.url": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, @@ -3922,11 +4000,19 @@ Object { }, "event.action": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, "event.kind": Object { "array": false, + "ignore_above": 1024, + "required": false, + "type": "keyword", + }, + "event.original": Object { + "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, @@ -4047,21 +4133,25 @@ Object { }, "kibana.alert.original_event.action": Object { "array": false, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.agent_id_status": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, "kibana.alert.original_event.category": Object { "array": true, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.code": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, @@ -4072,11 +4162,13 @@ Object { }, "kibana.alert.original_event.dataset": Object { "array": false, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.duration": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, @@ -4087,11 +4179,13 @@ Object { }, "kibana.alert.original_event.hash": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, "kibana.alert.original_event.id": Object { "array": false, + "ignore_above": 1024, "required": true, "type": "keyword", }, @@ -4102,36 +4196,43 @@ Object { }, "kibana.alert.original_event.kind": Object { "array": false, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.module": Object { "array": false, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.original": Object { "array": false, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.outcome": Object { "array": false, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.provider": Object { "array": false, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.reason": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, "kibana.alert.original_event.reference": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, @@ -4162,16 +4263,19 @@ Object { }, "kibana.alert.original_event.timezone": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, "kibana.alert.original_event.type": Object { "array": true, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.url": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, @@ -4989,11 +5093,19 @@ Object { }, "event.action": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, "event.kind": Object { "array": false, + "ignore_above": 1024, + "required": false, + "type": "keyword", + }, + "event.original": Object { + "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, @@ -5114,21 +5226,25 @@ Object { }, "kibana.alert.original_event.action": Object { "array": false, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.agent_id_status": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, "kibana.alert.original_event.category": Object { "array": true, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.code": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, @@ -5139,11 +5255,13 @@ Object { }, "kibana.alert.original_event.dataset": Object { "array": false, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.duration": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, @@ -5154,11 +5272,13 @@ Object { }, "kibana.alert.original_event.hash": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, "kibana.alert.original_event.id": Object { "array": false, + "ignore_above": 1024, "required": true, "type": "keyword", }, @@ -5169,36 +5289,43 @@ Object { }, "kibana.alert.original_event.kind": Object { "array": false, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.module": Object { "array": false, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.original": Object { "array": false, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.outcome": Object { "array": false, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.provider": Object { "array": false, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.reason": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, "kibana.alert.original_event.reference": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, @@ -5229,16 +5356,19 @@ Object { }, "kibana.alert.original_event.timezone": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, "kibana.alert.original_event.type": Object { "array": true, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.url": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, @@ -6062,11 +6192,19 @@ Object { }, "event.action": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, "event.kind": Object { "array": false, + "ignore_above": 1024, + "required": false, + "type": "keyword", + }, + "event.original": Object { + "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, @@ -6187,21 +6325,25 @@ Object { }, "kibana.alert.original_event.action": Object { "array": false, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.agent_id_status": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, "kibana.alert.original_event.category": Object { "array": true, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.code": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, @@ -6212,11 +6354,13 @@ Object { }, "kibana.alert.original_event.dataset": Object { "array": false, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.duration": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, @@ -6227,11 +6371,13 @@ Object { }, "kibana.alert.original_event.hash": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, "kibana.alert.original_event.id": Object { "array": false, + "ignore_above": 1024, "required": true, "type": "keyword", }, @@ -6242,36 +6388,43 @@ Object { }, "kibana.alert.original_event.kind": Object { "array": false, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.module": Object { "array": false, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.original": Object { "array": false, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.outcome": Object { "array": false, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.provider": Object { "array": false, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.reason": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, "kibana.alert.original_event.reference": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, @@ -6302,16 +6455,19 @@ Object { }, "kibana.alert.original_event.timezone": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, "kibana.alert.original_event.type": Object { "array": true, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.url": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, @@ -7129,11 +7285,19 @@ Object { }, "event.action": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, "event.kind": Object { "array": false, + "ignore_above": 1024, + "required": false, + "type": "keyword", + }, + "event.original": Object { + "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, @@ -7254,21 +7418,25 @@ Object { }, "kibana.alert.original_event.action": Object { "array": false, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.agent_id_status": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, "kibana.alert.original_event.category": Object { "array": true, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.code": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, @@ -7279,11 +7447,13 @@ Object { }, "kibana.alert.original_event.dataset": Object { "array": false, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.duration": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, @@ -7294,11 +7464,13 @@ Object { }, "kibana.alert.original_event.hash": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, "kibana.alert.original_event.id": Object { "array": false, + "ignore_above": 1024, "required": true, "type": "keyword", }, @@ -7309,36 +7481,43 @@ Object { }, "kibana.alert.original_event.kind": Object { "array": false, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.module": Object { "array": false, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.original": Object { "array": false, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.outcome": Object { "array": false, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.provider": Object { "array": false, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.reason": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, "kibana.alert.original_event.reference": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, @@ -7369,16 +7548,19 @@ Object { }, "kibana.alert.original_event.timezone": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, "kibana.alert.original_event.type": Object { "array": true, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.url": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, @@ -8196,11 +8378,19 @@ Object { }, "event.action": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, "event.kind": Object { "array": false, + "ignore_above": 1024, + "required": false, + "type": "keyword", + }, + "event.original": Object { + "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, @@ -8321,21 +8511,25 @@ Object { }, "kibana.alert.original_event.action": Object { "array": false, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.agent_id_status": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, "kibana.alert.original_event.category": Object { "array": true, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.code": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, @@ -8346,11 +8540,13 @@ Object { }, "kibana.alert.original_event.dataset": Object { "array": false, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.duration": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, @@ -8361,11 +8557,13 @@ Object { }, "kibana.alert.original_event.hash": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, "kibana.alert.original_event.id": Object { "array": false, + "ignore_above": 1024, "required": true, "type": "keyword", }, @@ -8376,36 +8574,43 @@ Object { }, "kibana.alert.original_event.kind": Object { "array": false, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.module": Object { "array": false, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.original": Object { "array": false, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.outcome": Object { "array": false, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.provider": Object { "array": false, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.reason": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, "kibana.alert.original_event.reference": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, @@ -8436,16 +8641,19 @@ Object { }, "kibana.alert.original_event.timezone": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, "kibana.alert.original_event.type": Object { "array": true, + "ignore_above": 1024, "required": true, "type": "keyword", }, "kibana.alert.original_event.url": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, diff --git a/x-pack/plugins/rule_registry/common/assets/field_maps/technical_rule_field_map.test.ts b/x-pack/plugins/rule_registry/common/assets/field_maps/technical_rule_field_map.test.ts index 1b4b897664b848..e0fc5d9317fc9b 100644 --- a/x-pack/plugins/rule_registry/common/assets/field_maps/technical_rule_field_map.test.ts +++ b/x-pack/plugins/rule_registry/common/assets/field_maps/technical_rule_field_map.test.ts @@ -24,11 +24,19 @@ it('matches snapshot', () => { }, "event.action": Object { "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, "event.kind": Object { "array": false, + "ignore_above": 1024, + "required": false, + "type": "keyword", + }, + "event.original": Object { + "array": false, + "ignore_above": 1024, "required": false, "type": "keyword", }, diff --git a/x-pack/plugins/security_solution/common/field_maps/8.16.0/alerts.ts b/x-pack/plugins/security_solution/common/field_maps/8.16.0/alerts.ts new file mode 100644 index 00000000000000..9c215937a69613 --- /dev/null +++ b/x-pack/plugins/security_solution/common/field_maps/8.16.0/alerts.ts @@ -0,0 +1,122 @@ +/* + * 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 { alertsFieldMap8130 } from '../8.13.0'; + +export const alertsFieldMap8160 = { + ...alertsFieldMap8130, + 'kibana.alert.original_event.action': { + type: 'keyword', + array: false, + required: true, + ignore_above: 1024, + }, + 'kibana.alert.original_event.agent_id_status': { + type: 'keyword', + array: false, + required: false, + ignore_above: 1024, + }, + 'kibana.alert.original_event.category': { + type: 'keyword', + array: true, + required: true, + ignore_above: 1024, + }, + 'kibana.alert.original_event.code': { + type: 'keyword', + array: false, + required: false, + ignore_above: 1024, + }, + 'kibana.alert.original_event.dataset': { + type: 'keyword', + array: false, + required: true, + ignore_above: 1024, + }, + 'kibana.alert.original_event.duration': { + type: 'keyword', + array: false, + required: false, + ignore_above: 1024, + }, + 'kibana.alert.original_event.hash': { + type: 'keyword', + array: false, + required: false, + ignore_above: 1024, + }, + 'kibana.alert.original_event.id': { + type: 'keyword', + array: false, + required: true, + ignore_above: 1024, + }, + 'kibana.alert.original_event.kind': { + type: 'keyword', + array: false, + required: true, + ignore_above: 1024, + }, + 'kibana.alert.original_event.module': { + type: 'keyword', + array: false, + required: true, + ignore_above: 1024, + }, + 'kibana.alert.original_event.original': { + type: 'keyword', + array: false, + required: true, + ignore_above: 1024, + }, + 'kibana.alert.original_event.outcome': { + type: 'keyword', + array: false, + required: true, + ignore_above: 1024, + }, + 'kibana.alert.original_event.provider': { + type: 'keyword', + array: false, + required: true, + ignore_above: 1024, + }, + 'kibana.alert.original_event.reason': { + type: 'keyword', + array: false, + required: false, + ignore_above: 1024, + }, + 'kibana.alert.original_event.reference': { + type: 'keyword', + array: false, + required: false, + ignore_above: 1024, + }, + 'kibana.alert.original_event.timezone': { + type: 'keyword', + array: false, + required: false, + ignore_above: 1024, + }, + 'kibana.alert.original_event.type': { + type: 'keyword', + array: true, + required: true, + ignore_above: 1024, + }, + 'kibana.alert.original_event.url': { + type: 'keyword', + array: false, + required: false, + ignore_above: 1024, + }, +} as const; + +export type AlertsFieldMap8160 = typeof alertsFieldMap8160; diff --git a/x-pack/plugins/security_solution/common/field_maps/8.16.0/index.ts b/x-pack/plugins/security_solution/common/field_maps/8.16.0/index.ts new file mode 100644 index 00000000000000..d4adbbc18ad944 --- /dev/null +++ b/x-pack/plugins/security_solution/common/field_maps/8.16.0/index.ts @@ -0,0 +1,11 @@ +/* + * 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 type { AlertsFieldMap8160 } from './alerts'; +import { alertsFieldMap8160 } from './alerts'; +export type { AlertsFieldMap8160 }; +export { alertsFieldMap8160 }; diff --git a/x-pack/plugins/security_solution/common/field_maps/index.ts b/x-pack/plugins/security_solution/common/field_maps/index.ts index fe903776d1dd43..5080ff2660533d 100644 --- a/x-pack/plugins/security_solution/common/field_maps/index.ts +++ b/x-pack/plugins/security_solution/common/field_maps/index.ts @@ -5,9 +5,9 @@ * 2.0. */ -import type { AlertsFieldMap8130 } from './8.13.0'; -import { alertsFieldMap8130 } from './8.13.0'; +import type { AlertsFieldMap8160 } from './8.16.0'; +import { alertsFieldMap8160 } from './8.16.0'; import type { RulesFieldMap } from './8.0.0/rules'; import { rulesFieldMap } from './8.0.0/rules'; -export type { AlertsFieldMap8130 as AlertsFieldMap, RulesFieldMap }; -export { alertsFieldMap8130 as alertsFieldMap, rulesFieldMap }; +export type { AlertsFieldMap8160 as AlertsFieldMap, RulesFieldMap }; +export { alertsFieldMap8160 as alertsFieldMap, rulesFieldMap }; diff --git a/x-pack/test/functional/es_archives/security_solution/ecs_non_compliant/mappings.json b/x-pack/test/functional/es_archives/security_solution/ecs_non_compliant/mappings.json index 7a76d0da64667a..e39e5e202f0167 100644 --- a/x-pack/test/functional/es_archives/security_solution/ecs_non_compliant/mappings.json +++ b/x-pack/test/functional/es_archives/security_solution/ecs_non_compliant/mappings.json @@ -51,6 +51,12 @@ "ignore_above": 256 } } + }, + "original": { + "type": "text" + }, + "module": { + "type": "text" } } }, diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/non_ecs_fields.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/non_ecs_fields.ts index c5b04cd202d24e..15ea0c02b6bc2c 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/non_ecs_fields.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/non_ecs_fields.ts @@ -317,6 +317,47 @@ export default ({ getService }: FtrProviderContext) => { expect(alertSource).not.toHaveProperty('dll.code_signature.valid'); }); + // The issue was found by customer and reported in + // https://github.com/elastic/kibana/issues/187630 + describe('saving non-ECS compliant text field in keyword', () => { + it('should remove text field if the length of the string is more than 32766 bytes', async () => { + const document = { + 'event.original': 'z'.repeat(32767), + 'event.module': 'z'.repeat(32767), + 'event.action': 'z'.repeat(32767), + }; + + const { errors, alertSource } = await indexAndCreatePreviewAlert(document); + + expect(errors).toEqual([]); + + // keywords with `ignore_above` attribute which allows long text to be stored + expect(alertSource).toHaveProperty(['kibana.alert.original_event.module']); + expect(alertSource).toHaveProperty(['kibana.alert.original_event.original']); + expect(alertSource).toHaveProperty(['kibana.alert.original_event.action']); + + expect(alertSource).toHaveProperty(['event.module']); + expect(alertSource).toHaveProperty(['event.original']); + expect(alertSource).toHaveProperty(['event.action']); + }); + + it('should not remove text field if the length of the string is less than or equal to 32766 bytes', async () => { + const document = { + 'event.original': 'z'.repeat(100), + 'event.module': 'z'.repeat(32766), + 'event.action': 'z'.repeat(32766), + }; + + const { errors, alertSource } = await indexAndCreatePreviewAlert(document); + + expect(errors).toEqual([]); + + expect(alertSource).toHaveProperty(['kibana.alert.original_event.original']); + expect(alertSource).toHaveProperty(['kibana.alert.original_event.module']); + expect(alertSource).toHaveProperty(['kibana.alert.original_event.action']); + }); + }); + describe('multi-fields', () => { it('should not add multi field .text to ecs compliant nested source', async () => { const document = { From f19af22be65b58630dedb10afb03d61910469ab3 Mon Sep 17 00:00:00 2001 From: Ying Mao Date: Mon, 22 Jul 2024 14:39:11 -0400 Subject: [PATCH 18/54] [Response Ops][Alerting] Refactor `ExecutionHandler` stage 1 (#186666) Resolves https://github.com/elastic/kibana/issues/186533 ## Summary Stage 1 of `ExecutionHandler` refactor: * Rename `ExecutionHandler` to `ActionScheduler`. * Create schedulers to handle the 3 different action types (`SummaryActionScheduler`, `SystemActionScheduler`, `PerAlertActionScheduler`) * Splits `ExecutionHandler.generateExecutables` function into the appropriate action type class and combine the returned executables from each scheduler class. GH is not recognizing the rename from `ExecutionHandler` to `ActionScheduler` so I've called out the primary difference between the two files (other than the rename) which is to get the executables from each scheduler class instead of from a `generateExecutables` function. Removed the `generateExecutables` fn from the `ActionScheduler` and any associated private helper functions. --------- Co-authored-by: Elastic Machine --- .../action_scheduler.test.ts} | 663 +++++------- .../action_scheduler/action_scheduler.ts | 605 +++++++++++ .../get_summarized_alerts.test.ts | 127 +++ .../action_scheduler/get_summarized_alerts.ts | 78 ++ .../task_runner/action_scheduler/index.ts | 10 + .../rule_action_helper.test.ts | 55 +- .../rule_action_helper.ts | 28 +- .../action_scheduler/schedulers/index.ts | 10 + .../per_alert_action_scheduler.test.ts | 849 +++++++++++++++ .../schedulers/per_alert_action_scheduler.ts | 264 +++++ .../summary_action_scheduler.test.ts | 468 +++++++++ .../schedulers/summary_action_scheduler.ts | 127 +++ .../system_action_scheduler.test.ts | 218 ++++ .../schedulers/system_action_scheduler.ts | 80 ++ .../action_scheduler/test_fixtures.ts | 208 ++++ .../task_runner/action_scheduler/types.ts | 111 ++ .../server/task_runner/execution_handler.ts | 975 ------------------ .../task_runner/inject_action_params.ts | 2 +- .../server/task_runner/task_runner.ts | 10 +- .../alerting/server/task_runner/types.ts | 5 +- x-pack/plugins/alerting/tsconfig.json | 3 +- 21 files changed, 3473 insertions(+), 1423 deletions(-) rename x-pack/plugins/alerting/server/task_runner/{execution_handler.test.ts => action_scheduler/action_scheduler.test.ts} (79%) create mode 100644 x-pack/plugins/alerting/server/task_runner/action_scheduler/action_scheduler.ts create mode 100644 x-pack/plugins/alerting/server/task_runner/action_scheduler/get_summarized_alerts.test.ts create mode 100644 x-pack/plugins/alerting/server/task_runner/action_scheduler/get_summarized_alerts.ts create mode 100644 x-pack/plugins/alerting/server/task_runner/action_scheduler/index.ts rename x-pack/plugins/alerting/server/task_runner/{ => action_scheduler}/rule_action_helper.test.ts (91%) rename x-pack/plugins/alerting/server/task_runner/{ => action_scheduler}/rule_action_helper.ts (85%) create mode 100644 x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/index.ts create mode 100644 x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/per_alert_action_scheduler.test.ts create mode 100644 x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/per_alert_action_scheduler.ts create mode 100644 x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/summary_action_scheduler.test.ts create mode 100644 x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/summary_action_scheduler.ts create mode 100644 x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/system_action_scheduler.test.ts create mode 100644 x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/system_action_scheduler.ts create mode 100644 x-pack/plugins/alerting/server/task_runner/action_scheduler/test_fixtures.ts create mode 100644 x-pack/plugins/alerting/server/task_runner/action_scheduler/types.ts delete mode 100644 x-pack/plugins/alerting/server/task_runner/execution_handler.ts diff --git a/x-pack/plugins/alerting/server/task_runner/execution_handler.test.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/action_scheduler.test.ts similarity index 79% rename from x-pack/plugins/alerting/server/task_runner/execution_handler.test.ts rename to x-pack/plugins/alerting/server/task_runner/action_scheduler/action_scheduler.test.ts index b22d7b70a9d49e..600f6aedbe039f 100644 --- a/x-pack/plugins/alerting/server/task_runner/execution_handler.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/action_scheduler.test.ts @@ -5,42 +5,38 @@ * 2.0. */ -import { ExecutionHandler } from './execution_handler'; +import { ActionScheduler } from './action_scheduler'; import { loggingSystemMock } from '@kbn/core/server/mocks'; import { actionsClientMock, actionsMock, renderActionParameterTemplatesDefault, } from '@kbn/actions-plugin/server/mocks'; -import { KibanaRequest } from '@kbn/core/server'; import { ActionsCompletion } from '@kbn/alerting-state-types'; import { ALERT_UUID } from '@kbn/rule-data-utils'; -import { InjectActionParamsOpts, injectActionParams } from './inject_action_params'; -import { NormalizedRuleType } from '../rule_type_registry'; -import { - ThrottledActions, - RuleTypeParams, - RuleTypeState, - SanitizedRule, - GetViewInAppRelativeUrlFnOpts, -} from '../types'; -import { RuleRunMetricsStore } from '../lib/rule_run_metrics_store'; -import { alertingEventLoggerMock } from '../lib/alerting_event_logger/alerting_event_logger.mock'; +import { InjectActionParamsOpts, injectActionParams } from '../inject_action_params'; +import { RuleTypeParams, SanitizedRule, GetViewInAppRelativeUrlFnOpts } from '../../types'; +import { RuleRunMetricsStore } from '../../lib/rule_run_metrics_store'; +import { alertingEventLoggerMock } from '../../lib/alerting_event_logger/alerting_event_logger.mock'; import { ConcreteTaskInstance, TaskErrorSource } from '@kbn/task-manager-plugin/server'; -import { Alert } from '../alert'; -import { AlertInstanceState, AlertInstanceContext, RuleNotifyWhen } from '../../common'; +import { RuleNotifyWhen } from '../../../common'; import { asSavedObjectExecutionSource } from '@kbn/actions-plugin/server'; import sinon from 'sinon'; -import { mockAAD } from './fixtures'; +import { mockAAD } from '../fixtures'; import { schema } from '@kbn/config-schema'; -import { ConnectorAdapterRegistry } from '../connector_adapters/connector_adapter_registry'; -import { alertsClientMock } from '../alerts_client/alerts_client.mock'; +import { alertsClientMock } from '../../alerts_client/alerts_client.mock'; import { ExecutionResponseType } from '@kbn/actions-plugin/server/create_execute_function'; -import { RULE_SAVED_OBJECT_TYPE } from '../saved_objects'; +import { RULE_SAVED_OBJECT_TYPE } from '../../saved_objects'; import { getErrorSource } from '@kbn/task-manager-plugin/server/task_running'; -import { TaskRunnerContext } from './types'; - -jest.mock('./inject_action_params', () => ({ +import { + generateAlert, + generateRecoveredAlert, + getDefaultSchedulerContext, + getRule, + getRuleType, +} from './test_fixtures'; + +jest.mock('../inject_action_params', () => ({ injectActionParams: jest.fn(), })); @@ -51,100 +47,16 @@ const actionsClient = actionsClientMock.create(); const alertsClient = alertsClientMock.create(); const mockActionsPlugin = actionsMock.createStart(); const apiKey = Buffer.from('123:abc').toString('base64'); -const ruleType: NormalizedRuleType< - RuleTypeParams, - RuleTypeParams, - RuleTypeState, - AlertInstanceState, - AlertInstanceContext, - 'default' | 'other-group', - 'recovered', - {} -> = { - id: 'test', - name: 'Test', - actionGroups: [ - { id: 'default', name: 'Default' }, - { id: 'recovered', name: 'Recovered' }, - { id: 'other-group', name: 'Other Group' }, - ], - defaultActionGroupId: 'default', - minimumLicenseRequired: 'basic', - isExportable: true, - recoveryActionGroup: { - id: 'recovered', - name: 'Recovered', - }, - executor: jest.fn(), - category: 'test', - producer: 'alerts', - validate: { - params: schema.any(), - }, - alerts: { - context: 'context', - mappings: { fieldMap: { field: { type: 'fieldType', required: false } } }, - }, - autoRecoverAlerts: false, - validLegacyConsumers: [], -}; -const rule = { - id: '1', - name: 'name-of-alert', - tags: ['tag-A', 'tag-B'], - mutedInstanceIds: [], - params: { - foo: true, - contextVal: 'My other {{context.value}} goes here', - stateVal: 'My other {{state.value}} goes here', - }, - schedule: { interval: '1m' }, - notifyWhen: 'onActiveAlert', - actions: [ - { - id: '1', - group: 'default', - actionTypeId: 'test', - params: { - foo: true, - contextVal: 'My {{context.value}} goes here', - stateVal: 'My {{state.value}} goes here', - alertVal: - 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', - }, - uuid: '111-111', - }, - ], - consumer: 'test-consumer', -} as unknown as SanitizedRule; - -const defaultExecutionParams = { - rule, - ruleType, - logger: loggingSystemMock.create().get(), - taskRunnerContext: { - actionsConfigMap: { - default: { - max: 1000, - }, - }, - actionsPlugin: mockActionsPlugin, - connectorAdapterRegistry: new ConnectorAdapterRegistry(), - } as unknown as TaskRunnerContext, - apiKey, - ruleConsumer: 'rule-consumer', - executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - alertUuid: 'uuid-1', - ruleLabel: 'rule-label', - request: {} as KibanaRequest, + +const rule = getRule(); +const ruleType = getRuleType(); +const defaultSchedulerContext = getDefaultSchedulerContext( + loggingSystemMock.create().get(), + mockActionsPlugin, alertingEventLogger, - previousStartedAt: null, - taskInstance: { - params: { spaceId: 'test1', alertId: '1' }, - } as unknown as ConcreteTaskInstance, actionsClient, - alertsClient, -}; + alertsClient +); const defaultExecutionResponse = { errors: false, @@ -153,74 +65,11 @@ const defaultExecutionResponse = { let ruleRunMetricsStore: RuleRunMetricsStore; let clock: sinon.SinonFakeTimers; -type ActiveActionGroup = 'default' | 'other-group'; -const generateAlert = ({ - id, - group = 'default', - context, - state, - scheduleActions = true, - throttledActions = {}, - lastScheduledActionsGroup = 'default', - maintenanceWindowIds, - pendingRecoveredCount, - activeCount, -}: { - id: number; - group?: ActiveActionGroup | 'recovered'; - context?: AlertInstanceContext; - state?: AlertInstanceState; - scheduleActions?: boolean; - throttledActions?: ThrottledActions; - lastScheduledActionsGroup?: string; - maintenanceWindowIds?: string[]; - pendingRecoveredCount?: number; - activeCount?: number; -}) => { - const alert = new Alert( - String(id), - { - state: state || { test: true }, - meta: { - maintenanceWindowIds, - lastScheduledActions: { - date: new Date().toISOString(), - group: lastScheduledActionsGroup, - actions: throttledActions, - }, - pendingRecoveredCount, - activeCount, - }, - } - ); - if (scheduleActions) { - alert.scheduleActions(group as ActiveActionGroup); - } - if (context) { - alert.setContext(context); - } - - return { [id]: alert }; -}; - -const generateRecoveredAlert = ({ id, state }: { id: number; state?: AlertInstanceState }) => { - const alert = new Alert(String(id), { - state: state || { test: true }, - meta: { - lastScheduledActions: { - date: new Date().toISOString(), - group: 'recovered', - actions: {}, - }, - }, - }); - return { [id]: alert }; -}; // @ts-ignore -const generateExecutionParams = (params = {}) => { +const getSchedulerContext = (params = {}) => { return { - ...defaultExecutionParams, + ...defaultSchedulerContext, ...params, ruleRunMetricsStore, }; @@ -228,11 +77,11 @@ const generateExecutionParams = (params = {}) => { const DATE_1970 = new Date('1970-01-01T00:00:00.000Z'); -describe('Execution Handler', () => { +describe('Action Scheduler', () => { beforeEach(() => { jest.resetAllMocks(); jest - .requireMock('./inject_action_params') + .requireMock('../inject_action_params') .injectActionParams.mockImplementation( ({ actionParams }: InjectActionParamsOpts) => actionParams ); @@ -252,8 +101,8 @@ describe('Execution Handler', () => { test('enqueues execution per selected action', async () => { const alerts = generateAlert({ id: 1 }); - const executionHandler = new ExecutionHandler(generateExecutionParams()); - await executionHandler.run(alerts); + const actionScheduler = new ActionScheduler(getSchedulerContext()); + await actionScheduler.run(alerts); expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toBe(1); expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toBe(1); @@ -302,7 +151,7 @@ describe('Execution Handler', () => { alertGroup: 'default', }); - expect(jest.requireMock('./inject_action_params').injectActionParams).toHaveBeenCalledWith({ + expect(jest.requireMock('../inject_action_params').injectActionParams).toHaveBeenCalledWith({ actionTypeId: 'test', actionParams: { alertVal: 'My 1 name-of-alert test1 tag-A,tag-B 1 goes here', @@ -321,10 +170,10 @@ describe('Execution Handler', () => { mockActionsPlugin.isActionExecutable.mockReturnValueOnce(false); mockActionsPlugin.isActionTypeEnabled.mockReturnValueOnce(false); mockActionsPlugin.isActionTypeEnabled.mockReturnValueOnce(true); - const executionHandler = new ExecutionHandler( - generateExecutionParams({ + const actionScheduler = new ActionScheduler( + getSchedulerContext({ rule: { - ...defaultExecutionParams.rule, + ...defaultSchedulerContext.rule, actions: [ { id: '2', @@ -351,7 +200,7 @@ describe('Execution Handler', () => { }) ); - await executionHandler.run(generateAlert({ id: 1 })); + await actionScheduler.run(generateAlert({ id: 1 })); expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toBe(1); expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toBe(2); expect(actionsClient.bulkEnqueueExecution).toHaveBeenCalledTimes(1); @@ -388,10 +237,10 @@ describe('Execution Handler', () => { mockActionsPlugin.inMemoryConnectors = []; mockActionsPlugin.isActionExecutable.mockReturnValue(false); mockActionsPlugin.isActionTypeEnabled.mockReturnValue(false); - const executionHandler = new ExecutionHandler( - generateExecutionParams({ + const actionScheduler = new ActionScheduler( + getSchedulerContext({ rule: { - ...defaultExecutionParams.rule, + ...defaultSchedulerContext.rule, actions: [ { id: '2', @@ -416,19 +265,19 @@ describe('Execution Handler', () => { }) ); - await executionHandler.run(generateAlert({ id: 2 })); + await actionScheduler.run(generateAlert({ id: 2 })); expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toBe(0); expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toBe(2); expect(actionsClient.bulkEnqueueExecution).toHaveBeenCalledTimes(0); mockActionsPlugin.isActionExecutable.mockImplementation(() => true); - const executionHandlerForPreconfiguredAction = new ExecutionHandler({ - ...defaultExecutionParams, + const actionSchedulerForPreconfiguredAction = new ActionScheduler({ + ...defaultSchedulerContext, ruleRunMetricsStore, }); - await executionHandlerForPreconfiguredAction.run(generateAlert({ id: 2 })); + await actionSchedulerForPreconfiguredAction.run(generateAlert({ id: 2 })); expect(actionsClient.bulkEnqueueExecution).toHaveBeenCalledTimes(1); }); @@ -449,11 +298,11 @@ describe('Execution Handler', () => { }, }, ]; - const executionHandler = new ExecutionHandler( - generateExecutionParams({ - ...defaultExecutionParams, + const actionScheduler = new ActionScheduler( + getSchedulerContext({ + ...defaultSchedulerContext, taskRunnerContext: { - ...defaultExecutionParams.taskRunnerContext, + ...defaultSchedulerContext.taskRunnerContext, actionsConfigMap: { default: { max: 2, @@ -461,30 +310,30 @@ describe('Execution Handler', () => { }, }, rule: { - ...defaultExecutionParams.rule, + ...defaultSchedulerContext.rule, actions, }, }) ); try { - await executionHandler.run(generateAlert({ id: 2, state: { value: 'state-val' } })); + await actionScheduler.run(generateAlert({ id: 2, state: { value: 'state-val' } })); } catch (err) { expect(getErrorSource(err)).toBe(TaskErrorSource.USER); } }); test('limits actionsPlugin.execute per action group', async () => { - const executionHandler = new ExecutionHandler(generateExecutionParams()); - await executionHandler.run(generateAlert({ id: 2, group: 'other-group' })); + const actionScheduler = new ActionScheduler(getSchedulerContext()); + await actionScheduler.run(generateAlert({ id: 2, group: 'other-group' })); expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toBe(0); expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toBe(0); expect(actionsClient.bulkEnqueueExecution).not.toHaveBeenCalled(); }); test('context attribute gets parameterized', async () => { - const executionHandler = new ExecutionHandler(generateExecutionParams()); - await executionHandler.run(generateAlert({ id: 2, context: { value: 'context-val' } })); + const actionScheduler = new ActionScheduler(getSchedulerContext()); + await actionScheduler.run(generateAlert({ id: 2, context: { value: 'context-val' } })); expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toBe(1); expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toBe(1); expect(actionsClient.bulkEnqueueExecution).toHaveBeenCalledTimes(1); @@ -526,8 +375,8 @@ describe('Execution Handler', () => { }); test('state attribute gets parameterized', async () => { - const executionHandler = new ExecutionHandler(generateExecutionParams()); - await executionHandler.run(generateAlert({ id: 2, state: { value: 'state-val' } })); + const actionScheduler = new ActionScheduler(getSchedulerContext()); + await actionScheduler.run(generateAlert({ id: 2, state: { value: 'state-val' } })); expect(actionsClient.bulkEnqueueExecution).toHaveBeenCalledTimes(1); expect(actionsClient.bulkEnqueueExecution.mock.calls[0]).toMatchInlineSnapshot(` Array [ @@ -567,12 +416,12 @@ describe('Execution Handler', () => { }); test(`logs an error when action group isn't part of actionGroups available for the ruleType`, async () => { - const executionHandler = new ExecutionHandler(generateExecutionParams()); - await executionHandler.run( + const actionScheduler = new ActionScheduler(getSchedulerContext()); + await actionScheduler.run( generateAlert({ id: 2, group: 'invalid-group' as 'default' | 'other-group' }) ); - expect(defaultExecutionParams.logger.error).toHaveBeenCalledWith( + expect(defaultSchedulerContext.logger.error).toHaveBeenCalledWith( 'Invalid action group "invalid-group" for rule "test".' ); @@ -629,11 +478,11 @@ describe('Execution Handler', () => { }, }, ]; - const executionHandler = new ExecutionHandler( - generateExecutionParams({ - ...defaultExecutionParams, + const actionScheduler = new ActionScheduler( + getSchedulerContext({ + ...defaultSchedulerContext, taskRunnerContext: { - ...defaultExecutionParams.taskRunnerContext, + ...defaultSchedulerContext.taskRunnerContext, actionsConfigMap: { default: { max: 2, @@ -641,17 +490,17 @@ describe('Execution Handler', () => { }, }, rule: { - ...defaultExecutionParams.rule, + ...defaultSchedulerContext.rule, actions, }, }) ); - await executionHandler.run(generateAlert({ id: 2, state: { value: 'state-val' } })); + await actionScheduler.run(generateAlert({ id: 2, state: { value: 'state-val' } })); expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toBe(2); expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toBe(3); expect(ruleRunMetricsStore.getTriggeredActionsStatus()).toBe(ActionsCompletion.PARTIAL); - expect(defaultExecutionParams.logger.debug).toHaveBeenCalledTimes(1); + expect(defaultSchedulerContext.logger.debug).toHaveBeenCalledTimes(1); expect(actionsClient.bulkEnqueueExecution).toHaveBeenCalledTimes(1); }); @@ -678,7 +527,7 @@ describe('Execution Handler', () => { ], }); const actions = [ - ...defaultExecutionParams.rule.actions, + ...defaultSchedulerContext.rule.actions, { id: '2', group: 'default', @@ -720,11 +569,11 @@ describe('Execution Handler', () => { }, }, ]; - const executionHandler = new ExecutionHandler( - generateExecutionParams({ - ...defaultExecutionParams, + const actionScheduler = new ActionScheduler( + getSchedulerContext({ + ...defaultSchedulerContext, taskRunnerContext: { - ...defaultExecutionParams.taskRunnerContext, + ...defaultSchedulerContext.taskRunnerContext, actionsConfigMap: { default: { max: 4, @@ -735,12 +584,12 @@ describe('Execution Handler', () => { }, }, rule: { - ...defaultExecutionParams.rule, + ...defaultSchedulerContext.rule, actions, }, }) ); - await executionHandler.run(generateAlert({ id: 2, state: { value: 'state-val' } })); + await actionScheduler.run(generateAlert({ id: 2, state: { value: 'state-val' } })); expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toBe(4); expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toBe(5); @@ -809,21 +658,21 @@ describe('Execution Handler', () => { }, }, ]; - const executionHandler = new ExecutionHandler( - generateExecutionParams({ - ...defaultExecutionParams, + const actionScheduler = new ActionScheduler( + getSchedulerContext({ + ...defaultSchedulerContext, rule: { - ...defaultExecutionParams.rule, + ...defaultSchedulerContext.rule, actions, }, }) ); - await executionHandler.run(generateAlert({ id: 2, state: { value: 'state-val' } })); + await actionScheduler.run(generateAlert({ id: 2, state: { value: 'state-val' } })); expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toBe(2); expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toBe(3); expect(ruleRunMetricsStore.getTriggeredActionsStatus()).toBe(ActionsCompletion.PARTIAL); - expect(defaultExecutionParams.logger.debug).toHaveBeenCalledTimes(1); + expect(defaultSchedulerContext.logger.debug).toHaveBeenCalledTimes(1); expect(actionsClient.bulkEnqueueExecution).toHaveBeenCalledTimes(1); }); @@ -842,16 +691,16 @@ describe('Execution Handler', () => { }, }, ]; - const executionHandler = new ExecutionHandler( - generateExecutionParams({ - ...defaultExecutionParams, + const actionScheduler = new ActionScheduler( + getSchedulerContext({ + ...defaultSchedulerContext, rule: { - ...defaultExecutionParams.rule, + ...defaultSchedulerContext.rule, actions, }, }) ); - await executionHandler.run(generateRecoveredAlert({ id: 1 })); + await actionScheduler.run(generateRecoveredAlert({ id: 1 })); expect(actionsClient.bulkEnqueueExecution).toHaveBeenCalledTimes(1); expect(actionsClient.bulkEnqueueExecution.mock.calls[0]).toMatchInlineSnapshot(` @@ -892,11 +741,11 @@ describe('Execution Handler', () => { }); test('does not schedule alerts with recovered actions that are muted', async () => { - const executionHandler = new ExecutionHandler( - generateExecutionParams({ - ...defaultExecutionParams, + const actionScheduler = new ActionScheduler( + getSchedulerContext({ + ...defaultSchedulerContext, rule: { - ...defaultExecutionParams.rule, + ...defaultSchedulerContext.rule, mutedInstanceIds: ['1'], actions: [ { @@ -915,46 +764,46 @@ describe('Execution Handler', () => { }, }) ); - await executionHandler.run(generateRecoveredAlert({ id: 1 })); + await actionScheduler.run(generateRecoveredAlert({ id: 1 })); expect(actionsClient.bulkEnqueueExecution).toHaveBeenCalledTimes(0); - expect(defaultExecutionParams.logger.debug).nthCalledWith( + expect(defaultSchedulerContext.logger.debug).nthCalledWith( 1, - `skipping scheduling of actions for '1' in rule ${defaultExecutionParams.ruleLabel}: rule is muted` + `skipping scheduling of actions for '1' in rule ${defaultSchedulerContext.ruleLabel}: rule is muted` ); }); test('does not schedule active alerts that are throttled', async () => { - const executionHandler = new ExecutionHandler( - generateExecutionParams({ - ...defaultExecutionParams, + const actionScheduler = new ActionScheduler( + getSchedulerContext({ + ...defaultSchedulerContext, rule: { - ...defaultExecutionParams.rule, + ...defaultSchedulerContext.rule, notifyWhen: 'onThrottleInterval', throttle: '1m', }, }) ); - await executionHandler.run(generateAlert({ id: 1 })); + await actionScheduler.run(generateAlert({ id: 1 })); clock.tick(30000); expect(actionsClient.bulkEnqueueExecution).toHaveBeenCalledTimes(0); - expect(defaultExecutionParams.logger.debug).nthCalledWith( + expect(defaultSchedulerContext.logger.debug).nthCalledWith( 1, - `skipping scheduling of actions for '1' in rule ${defaultExecutionParams.ruleLabel}: rule is throttled` + `skipping scheduling of actions for '1' in rule ${defaultSchedulerContext.ruleLabel}: rule is throttled` ); }); test('does not schedule actions that are throttled', async () => { - const executionHandler = new ExecutionHandler( - generateExecutionParams({ - ...defaultExecutionParams, + const actionScheduler = new ActionScheduler( + getSchedulerContext({ + ...defaultSchedulerContext, rule: { - ...defaultExecutionParams.rule, + ...defaultSchedulerContext.rule, actions: [ { - ...defaultExecutionParams.rule.actions[0], + ...defaultSchedulerContext.rule.actions[0], frequency: { summary: false, notifyWhen: 'onThrottleInterval', @@ -965,7 +814,7 @@ describe('Execution Handler', () => { }, }) ); - await executionHandler.run( + await actionScheduler.run( generateAlert({ id: 1, throttledActions: { '111-111': { date: new Date(DATE_1970).toISOString() } }, @@ -975,21 +824,21 @@ describe('Execution Handler', () => { clock.tick(30000); expect(actionsClient.bulkEnqueueExecution).toHaveBeenCalledTimes(0); - expect(defaultExecutionParams.logger.debug).nthCalledWith( + expect(defaultSchedulerContext.logger.debug).nthCalledWith( 1, - `skipping scheduling of actions for '1' in rule ${defaultExecutionParams.ruleLabel}: rule is throttled` + `skipping scheduling of actions for '1' in rule ${defaultSchedulerContext.ruleLabel}: rule is throttled` ); }); test('schedule actions that are throttled but alert has a changed action group', async () => { - const executionHandler = new ExecutionHandler( - generateExecutionParams({ - ...defaultExecutionParams, + const actionScheduler = new ActionScheduler( + getSchedulerContext({ + ...defaultSchedulerContext, rule: { - ...defaultExecutionParams.rule, + ...defaultSchedulerContext.rule, actions: [ { - ...defaultExecutionParams.rule.actions[0], + ...defaultSchedulerContext.rule.actions[0], frequency: { summary: false, notifyWhen: 'onThrottleInterval', @@ -1000,7 +849,7 @@ describe('Execution Handler', () => { }, }) ); - await executionHandler.run(generateAlert({ id: 1, lastScheduledActionsGroup: 'recovered' })); + await actionScheduler.run(generateAlert({ id: 1, lastScheduledActionsGroup: 'recovered' })); clock.tick(30000); @@ -1009,21 +858,21 @@ describe('Execution Handler', () => { }); test('does not schedule active alerts that are muted', async () => { - const executionHandler = new ExecutionHandler( - generateExecutionParams({ - ...defaultExecutionParams, + const actionScheduler = new ActionScheduler( + getSchedulerContext({ + ...defaultSchedulerContext, rule: { - ...defaultExecutionParams.rule, + ...defaultSchedulerContext.rule, mutedInstanceIds: ['1'], }, }) ); - await executionHandler.run(generateAlert({ id: 1 })); + await actionScheduler.run(generateAlert({ id: 1 })); expect(actionsClient.bulkEnqueueExecution).toHaveBeenCalledTimes(0); - expect(defaultExecutionParams.logger.debug).nthCalledWith( + expect(defaultSchedulerContext.logger.debug).nthCalledWith( 1, - `skipping scheduling of actions for '1' in rule ${defaultExecutionParams.ruleLabel}: rule is muted` + `skipping scheduling of actions for '1' in rule ${defaultSchedulerContext.ruleLabel}: rule is muted` ); }); @@ -1046,10 +895,10 @@ describe('Execution Handler', () => { ongoing: { count: 0, data: [] }, recovered: { count: 0, data: [] }, }); - const executionHandler = new ExecutionHandler( - generateExecutionParams({ + const actionScheduler = new ActionScheduler( + getSchedulerContext({ rule: { - ...defaultExecutionParams.rule, + ...defaultSchedulerContext.rule, mutedInstanceIds: ['foo'], actions: [ { @@ -1071,7 +920,7 @@ describe('Execution Handler', () => { }) ); - await executionHandler.run(generateAlert({ id: 1 })); + await actionScheduler.run(generateAlert({ id: 1 })); expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({ executionUuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', @@ -1125,10 +974,10 @@ describe('Execution Handler', () => { ongoing: { count: 0, data: [] }, recovered: { count: 0, data: [] }, }); - const executionHandler = new ExecutionHandler( - generateExecutionParams({ + const actionScheduler = new ActionScheduler( + getSchedulerContext({ rule: { - ...defaultExecutionParams.rule, + ...defaultSchedulerContext.rule, actions: [ { id: '1', @@ -1150,7 +999,7 @@ describe('Execution Handler', () => { }) ); - await executionHandler.run({}); + await actionScheduler.run({}); expect(actionsClient.bulkEnqueueExecution).not.toHaveBeenCalled(); expect(alertingEventLogger.logAction).not.toHaveBeenCalled(); @@ -1175,10 +1024,10 @@ describe('Execution Handler', () => { ongoing: { count: 0, data: [] }, recovered: { count: 0, data: [] }, }); - const executionHandler = new ExecutionHandler( - generateExecutionParams({ + const actionScheduler = new ActionScheduler( + getSchedulerContext({ rule: { - ...defaultExecutionParams.rule, + ...defaultSchedulerContext.rule, mutedInstanceIds: ['foo'], actions: [ { @@ -1201,7 +1050,7 @@ describe('Execution Handler', () => { }) ); - const result = await executionHandler.run({}); + const result = await actionScheduler.run({}); expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({ start: new Date('1969-12-31T00:01:30.000Z'), @@ -1263,10 +1112,10 @@ describe('Execution Handler', () => { ongoing: { count: 0, alerts: [] }, recovered: { count: 0, alerts: [] }, }); - const executionHandler = new ExecutionHandler( - generateExecutionParams({ + const actionScheduler = new ActionScheduler( + getSchedulerContext({ rule: { - ...defaultExecutionParams.rule, + ...defaultSchedulerContext.rule, actions: [ { id: '1', @@ -1286,18 +1135,18 @@ describe('Execution Handler', () => { ], }, taskInstance: { - ...defaultExecutionParams.taskInstance, + ...defaultSchedulerContext.taskInstance, state: { - ...defaultExecutionParams.taskInstance.state, + ...defaultSchedulerContext.taskInstance.state, summaryActions: { '111-111': { date: new Date() } }, }, } as unknown as ConcreteTaskInstance, }) ); - await executionHandler.run({}); - expect(defaultExecutionParams.logger.debug).toHaveBeenCalledTimes(1); - expect(defaultExecutionParams.logger.debug).toHaveBeenCalledWith( + await actionScheduler.run({}); + expect(defaultSchedulerContext.logger.debug).toHaveBeenCalledTimes(1); + expect(defaultSchedulerContext.logger.debug).toHaveBeenCalledWith( "skipping scheduling the action 'testActionTypeId:1', summary action is still being throttled" ); expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled(); @@ -1311,10 +1160,10 @@ describe('Execution Handler', () => { ongoing: { count: 0, data: [] }, recovered: { count: 0, data: [] }, }); - const executionHandler = new ExecutionHandler( - generateExecutionParams({ + const actionScheduler = new ActionScheduler( + getSchedulerContext({ rule: { - ...defaultExecutionParams.rule, + ...defaultSchedulerContext.rule, actions: [ { id: '1', @@ -1344,9 +1193,9 @@ describe('Execution Handler', () => { ], }, taskInstance: { - ...defaultExecutionParams.taskInstance, + ...defaultSchedulerContext.taskInstance, state: { - ...defaultExecutionParams.taskInstance.state, + ...defaultSchedulerContext.taskInstance.state, summaryActions: { '111-111': { date: new Date() }, '222-222': { date: new Date() }, @@ -1357,7 +1206,7 @@ describe('Execution Handler', () => { }) ); - const result = await executionHandler.run({}); + const result = await actionScheduler.run({}); expect(result).toEqual({ throttledSummaryActions: { '111-111': { @@ -1373,15 +1222,15 @@ describe('Execution Handler', () => { test(`skips scheduling actions if the ruleType doesn't have alerts mapping`, async () => { const { alerts, ...ruleTypeWithoutAlertsMapping } = ruleType; - const executionHandler = new ExecutionHandler( - generateExecutionParams({ - ...defaultExecutionParams, + const actionScheduler = new ActionScheduler( + getSchedulerContext({ + ...defaultSchedulerContext, ruleType: ruleTypeWithoutAlertsMapping, rule: { - ...defaultExecutionParams.rule, + ...defaultSchedulerContext.rule, actions: [ { - ...defaultExecutionParams.rule.actions[0], + ...defaultSchedulerContext.rule.actions[0], frequency: { summary: true, notifyWhen: 'onThrottleInterval', @@ -1392,9 +1241,9 @@ describe('Execution Handler', () => { }, }) ); - await executionHandler.run(generateAlert({ id: 2 })); + await actionScheduler.run(generateAlert({ id: 2 })); - expect(defaultExecutionParams.logger.error).toHaveBeenCalledWith( + expect(defaultSchedulerContext.logger.error).toHaveBeenCalledWith( 'Skipping action "1" for rule "1" because the rule type "Test" does not support alert-as-data.' ); @@ -1441,16 +1290,16 @@ describe('Execution Handler', () => { }, }, ]; - const executionHandler = new ExecutionHandler( - generateExecutionParams({ - ...defaultExecutionParams, + const actionScheduler = new ActionScheduler( + getSchedulerContext({ + ...defaultSchedulerContext, rule: { - ...defaultExecutionParams.rule, + ...defaultSchedulerContext.rule, actions, }, }) ); - await executionHandler.run(generateRecoveredAlert({ id: 1 })); + await actionScheduler.run(generateRecoveredAlert({ id: 1 })); expect(actionsClient.bulkEnqueueExecution).toHaveBeenCalledTimes(1); expect(actionsClient.bulkEnqueueExecution.mock.calls[0]).toMatchInlineSnapshot(` @@ -1541,10 +1390,10 @@ describe('Execution Handler', () => { }, recovered: { count: 0, data: [] }, }); - const executionHandler = new ExecutionHandler( - generateExecutionParams({ + const actionScheduler = new ActionScheduler( + getSchedulerContext({ rule: { - ...defaultExecutionParams.rule, + ...defaultSchedulerContext.rule, mutedInstanceIds: ['foo'], actions: [ { @@ -1570,7 +1419,7 @@ describe('Execution Handler', () => { }) ); - await executionHandler.run({ + await actionScheduler.run({ ...generateAlert({ id: 1 }), ...generateAlert({ id: 2 }), }); @@ -1586,8 +1435,8 @@ describe('Execution Handler', () => { }); expect(actionsClient.bulkEnqueueExecution).not.toHaveBeenCalled(); expect(alertingEventLogger.logAction).not.toHaveBeenCalled(); - expect(defaultExecutionParams.logger.debug).toHaveBeenCalledTimes(1); - expect(defaultExecutionParams.logger.debug).toHaveBeenCalledWith( + expect(defaultSchedulerContext.logger.debug).toHaveBeenCalledTimes(1); + expect(defaultSchedulerContext.logger.debug).toHaveBeenCalledWith( '(2) alerts have been filtered out for: testActionTypeId:111' ); }); @@ -1614,10 +1463,10 @@ describe('Execution Handler', () => { }, recovered: { count: 0, data: [] }, }); - const executionHandler = new ExecutionHandler( - generateExecutionParams({ + const actionScheduler = new ActionScheduler( + getSchedulerContext({ rule: { - ...defaultExecutionParams.rule, + ...defaultSchedulerContext.rule, mutedInstanceIds: ['foo'], actions: [ { @@ -1643,7 +1492,7 @@ describe('Execution Handler', () => { }) ); - await executionHandler.run({ + await actionScheduler.run({ ...generateAlert({ id: 1 }), ...generateAlert({ id: 2 }), }); @@ -1684,10 +1533,10 @@ describe('Execution Handler', () => { }, recovered: { count: 0, data: [] }, }); - const executionHandler = new ExecutionHandler( - generateExecutionParams({ + const actionScheduler = new ActionScheduler( + getSchedulerContext({ rule: { - ...defaultExecutionParams.rule, + ...defaultSchedulerContext.rule, mutedInstanceIds: ['foo'], actions: [ { @@ -1710,7 +1559,7 @@ describe('Execution Handler', () => { }) ); - await executionHandler.run({ + await actionScheduler.run({ ...generateAlert({ id: 1 }), ...generateAlert({ id: 2 }), ...generateAlert({ id: 3 }), @@ -1746,8 +1595,8 @@ describe('Execution Handler', () => { id: '1', typeId: 'testActionTypeId', }); - expect(defaultExecutionParams.logger.debug).toHaveBeenCalledTimes(1); - expect(defaultExecutionParams.logger.debug).toHaveBeenCalledWith( + expect(defaultSchedulerContext.logger.debug).toHaveBeenCalledTimes(1); + expect(defaultSchedulerContext.logger.debug).toHaveBeenCalledWith( '(2) alerts have been filtered out for: testActionTypeId:111' ); }); @@ -1790,10 +1639,10 @@ describe('Execution Handler', () => { '2': newAlert2[2], }); - const executionHandler = new ExecutionHandler( - generateExecutionParams({ + const actionScheduler = new ActionScheduler( + getSchedulerContext({ rule: { - ...defaultExecutionParams.rule, + ...defaultSchedulerContext.rule, mutedInstanceIds: ['foo'], actions: [ { @@ -1817,16 +1666,16 @@ describe('Execution Handler', () => { }) ); - await executionHandler.run({ + await actionScheduler.run({ ...generateAlert({ id: 1, maintenanceWindowIds: ['test-id-1'] }), ...generateAlert({ id: 2, maintenanceWindowIds: ['test-id-2'] }), ...generateAlert({ id: 3, maintenanceWindowIds: ['test-id-3'] }), }); expect(actionsClient.bulkEnqueueExecution).not.toHaveBeenCalled(); - expect(defaultExecutionParams.logger.debug).toHaveBeenCalledTimes(1); + expect(defaultSchedulerContext.logger.debug).toHaveBeenCalledTimes(1); - expect(defaultExecutionParams.logger.debug).toHaveBeenNthCalledWith( + expect(defaultSchedulerContext.logger.debug).toHaveBeenNthCalledWith( 1, '(3) alerts have been filtered out for: testActionTypeId:1' ); @@ -1839,10 +1688,10 @@ describe('Execution Handler', () => { recovered: { count: 0, data: [] }, }); - const executionHandler = new ExecutionHandler( - generateExecutionParams({ + const actionScheduler = new ActionScheduler( + getSchedulerContext({ rule: { - ...defaultExecutionParams.rule, + ...defaultSchedulerContext.rule, mutedInstanceIds: ['foo'], actions: [ { @@ -1866,53 +1715,53 @@ describe('Execution Handler', () => { }) ); - await executionHandler.run({ + await actionScheduler.run({ ...generateAlert({ id: 1, maintenanceWindowIds: ['test-id-1'] }), ...generateAlert({ id: 2, maintenanceWindowIds: ['test-id-2'] }), ...generateAlert({ id: 3, maintenanceWindowIds: ['test-id-3'] }), }); expect(actionsClient.bulkEnqueueExecution).not.toHaveBeenCalled(); - expect(defaultExecutionParams.logger.debug).toHaveBeenCalledTimes(1); + expect(defaultSchedulerContext.logger.debug).toHaveBeenCalledTimes(1); - expect(defaultExecutionParams.logger.debug).toHaveBeenNthCalledWith( + expect(defaultSchedulerContext.logger.debug).toHaveBeenNthCalledWith( 1, '(3) alerts have been filtered out for: testActionTypeId:1' ); }); test('does not schedule actions for alerts with maintenance window IDs', async () => { - const executionHandler = new ExecutionHandler(generateExecutionParams()); + const actionScheduler = new ActionScheduler(getSchedulerContext()); - await executionHandler.run({ + await actionScheduler.run({ ...generateAlert({ id: 1, maintenanceWindowIds: ['test-id-1'] }), ...generateAlert({ id: 2, maintenanceWindowIds: ['test-id-2'] }), ...generateAlert({ id: 3, maintenanceWindowIds: ['test-id-3'] }), }); expect(actionsClient.bulkEnqueueExecution).not.toHaveBeenCalled(); - expect(defaultExecutionParams.logger.debug).toHaveBeenCalledTimes(3); + expect(defaultSchedulerContext.logger.debug).toHaveBeenCalledTimes(3); - expect(defaultExecutionParams.logger.debug).toHaveBeenCalledWith( + expect(defaultSchedulerContext.logger.debug).toHaveBeenCalledWith( 'no scheduling of summary actions "1" for rule "1": has active maintenance windows test-id-1.' ); - expect(defaultExecutionParams.logger.debug).toHaveBeenCalledWith( + expect(defaultSchedulerContext.logger.debug).toHaveBeenCalledWith( 'no scheduling of summary actions "1" for rule "1": has active maintenance windows test-id-2.' ); - expect(defaultExecutionParams.logger.debug).toHaveBeenCalledWith( + expect(defaultSchedulerContext.logger.debug).toHaveBeenCalledWith( 'no scheduling of summary actions "1" for rule "1": has active maintenance windows test-id-3.' ); }); test('does not schedule actions with notifyWhen not set to "on status change" for alerts that are flapping', async () => { - const executionHandler = new ExecutionHandler( - generateExecutionParams({ - ...defaultExecutionParams, + const actionScheduler = new ActionScheduler( + getSchedulerContext({ + ...defaultSchedulerContext, rule: { - ...defaultExecutionParams.rule, + ...defaultSchedulerContext.rule, actions: [ { - ...defaultExecutionParams.rule.actions[0], + ...defaultSchedulerContext.rule.actions[0], frequency: { summary: false, notifyWhen: RuleNotifyWhen.ACTIVE, @@ -1924,7 +1773,7 @@ describe('Execution Handler', () => { }) ); - await executionHandler.run({ + await actionScheduler.run({ ...generateAlert({ id: 1, pendingRecoveredCount: 1, lastScheduledActionsGroup: 'recovered' }), ...generateAlert({ id: 2, pendingRecoveredCount: 1, lastScheduledActionsGroup: 'recovered' }), ...generateAlert({ id: 3, pendingRecoveredCount: 1, lastScheduledActionsGroup: 'recovered' }), @@ -1934,14 +1783,14 @@ describe('Execution Handler', () => { }); test('does schedule actions with notifyWhen is set to "on status change" for alerts that are flapping', async () => { - const executionHandler = new ExecutionHandler( - generateExecutionParams({ - ...defaultExecutionParams, + const actionScheduler = new ActionScheduler( + getSchedulerContext({ + ...defaultSchedulerContext, rule: { - ...defaultExecutionParams.rule, + ...defaultSchedulerContext.rule, actions: [ { - ...defaultExecutionParams.rule.actions[0], + ...defaultSchedulerContext.rule.actions[0], frequency: { summary: false, notifyWhen: RuleNotifyWhen.CHANGE, @@ -1953,7 +1802,7 @@ describe('Execution Handler', () => { }) ); - await executionHandler.run({ + await actionScheduler.run({ ...generateAlert({ id: 1, pendingRecoveredCount: 1, lastScheduledActionsGroup: 'recovered' }), ...generateAlert({ id: 2, pendingRecoveredCount: 1, lastScheduledActionsGroup: 'recovered' }), ...generateAlert({ id: 3, pendingRecoveredCount: 1, lastScheduledActionsGroup: 'recovered' }), @@ -2091,16 +1940,16 @@ describe('Execution Handler', () => { it('populates the rule.url in the action params when the base url and rule id are specified', async () => { const execParams = { - ...defaultExecutionParams, + ...defaultSchedulerContext, rule: ruleWithUrl, taskRunnerContext: { - ...defaultExecutionParams.taskRunnerContext, + ...defaultSchedulerContext.taskRunnerContext, kibanaBaseUrl: 'http://localhost:12345', }, }; - const executionHandler = new ExecutionHandler(generateExecutionParams(execParams)); - await executionHandler.run(generateAlert({ id: 1 })); + const actionScheduler = new ActionScheduler(getSchedulerContext(execParams)); + await actionScheduler.run(generateAlert({ id: 1 })); expect(injectActionParamsMock.mock.calls[0]).toMatchInlineSnapshot(` Array [ @@ -2124,16 +1973,16 @@ describe('Execution Handler', () => { it('populates the rule.url in the action params when the base url contains pathname', async () => { const execParams = { - ...defaultExecutionParams, + ...defaultSchedulerContext, rule: ruleWithUrl, taskRunnerContext: { - ...defaultExecutionParams.taskRunnerContext, + ...defaultSchedulerContext.taskRunnerContext, kibanaBaseUrl: 'http://localhost:12345/kbn', }, }; - const executionHandler = new ExecutionHandler(generateExecutionParams(execParams)); - await executionHandler.run(generateAlert({ id: 1 })); + const actionScheduler = new ActionScheduler(getSchedulerContext(execParams)); + await actionScheduler.run(generateAlert({ id: 1 })); expect(injectActionParamsMock.mock.calls[0][0].actionParams).toEqual({ val: 'rule url: http://localhost:12345/kbn/s/test1/app/management/insightsAndAlerting/triggersActions/rule/1', @@ -2159,7 +2008,7 @@ describe('Execution Handler', () => { recovered: { count: 0, data: [] }, }); const execParams = { - ...defaultExecutionParams, + ...defaultSchedulerContext, ruleType: { ...ruleType, getViewInAppRelativeUrl: (opts: GetViewInAppRelativeUrlFnOpts) => @@ -2167,13 +2016,13 @@ describe('Execution Handler', () => { }, rule: summaryRuleWithUrl, taskRunnerContext: { - ...defaultExecutionParams.taskRunnerContext, + ...defaultSchedulerContext.taskRunnerContext, kibanaBaseUrl: 'http://localhost:12345/basePath', }, }; - const executionHandler = new ExecutionHandler(generateExecutionParams(execParams)); - await executionHandler.run(generateAlert({ id: 1 })); + const actionScheduler = new ActionScheduler(getSchedulerContext(execParams)); + await actionScheduler.run(generateAlert({ id: 1 })); expect(injectActionParamsMock.mock.calls[0]).toMatchInlineSnapshot(` Array [ @@ -2197,10 +2046,10 @@ describe('Execution Handler', () => { it('populates the rule.url without the space specifier when the spaceId is the string "default"', async () => { const execParams = { - ...defaultExecutionParams, + ...defaultSchedulerContext, rule: ruleWithUrl, taskRunnerContext: { - ...defaultExecutionParams.taskRunnerContext, + ...defaultSchedulerContext.taskRunnerContext, kibanaBaseUrl: 'http://localhost:12345', }, taskInstance: { @@ -2208,8 +2057,8 @@ describe('Execution Handler', () => { } as unknown as ConcreteTaskInstance, }; - const executionHandler = new ExecutionHandler(generateExecutionParams(execParams)); - await executionHandler.run(generateAlert({ id: 1 })); + const actionScheduler = new ActionScheduler(getSchedulerContext(execParams)); + await actionScheduler.run(generateAlert({ id: 1 })); expect(injectActionParamsMock.mock.calls[0]).toMatchInlineSnapshot(` Array [ @@ -2233,16 +2082,16 @@ describe('Execution Handler', () => { it('populates the rule.url in the action params when the base url has a trailing slash and removes the trailing slash', async () => { const execParams = { - ...defaultExecutionParams, + ...defaultSchedulerContext, rule: ruleWithUrl, taskRunnerContext: { - ...defaultExecutionParams.taskRunnerContext, + ...defaultSchedulerContext.taskRunnerContext, kibanaBaseUrl: 'http://localhost:12345/', }, }; - const executionHandler = new ExecutionHandler(generateExecutionParams(execParams)); - await executionHandler.run(generateAlert({ id: 1 })); + const actionScheduler = new ActionScheduler(getSchedulerContext(execParams)); + await actionScheduler.run(generateAlert({ id: 1 })); expect(injectActionParamsMock.mock.calls[0]).toMatchInlineSnapshot(` Array [ @@ -2266,16 +2115,16 @@ describe('Execution Handler', () => { it('does not populate the rule.url when the base url is not specified', async () => { const execParams = { - ...defaultExecutionParams, + ...defaultSchedulerContext, rule: ruleWithUrl, taskRunnerContext: { - ...defaultExecutionParams.taskRunnerContext, + ...defaultSchedulerContext.taskRunnerContext, kibanaBaseUrl: undefined, }, }; - const executionHandler = new ExecutionHandler(generateExecutionParams(execParams)); - await executionHandler.run(generateAlert({ id: 1 })); + const actionScheduler = new ActionScheduler(getSchedulerContext(execParams)); + await actionScheduler.run(generateAlert({ id: 1 })); expect(injectActionParamsMock.mock.calls[0]).toMatchInlineSnapshot(` Array [ @@ -2293,10 +2142,10 @@ describe('Execution Handler', () => { it('does not populate the rule.url when base url is not a valid url', async () => { const execParams = { - ...defaultExecutionParams, + ...defaultSchedulerContext, rule: ruleWithUrl, taskRunnerContext: { - ...defaultExecutionParams.taskRunnerContext, + ...defaultSchedulerContext.taskRunnerContext, kibanaBaseUrl: 'localhost12345', }, taskInstance: { @@ -2304,8 +2153,8 @@ describe('Execution Handler', () => { } as unknown as ConcreteTaskInstance, }; - const executionHandler = new ExecutionHandler(generateExecutionParams(execParams)); - await executionHandler.run(generateAlert({ id: 1 })); + const actionScheduler = new ActionScheduler(getSchedulerContext(execParams)); + await actionScheduler.run(generateAlert({ id: 1 })); expect(injectActionParamsMock.mock.calls[0]).toMatchInlineSnapshot(` Array [ @@ -2323,10 +2172,10 @@ describe('Execution Handler', () => { it('does not populate the rule.url when base url is a number', async () => { const execParams = { - ...defaultExecutionParams, + ...defaultSchedulerContext, rule: ruleWithUrl, taskRunnerContext: { - ...defaultExecutionParams.taskRunnerContext, + ...defaultSchedulerContext.taskRunnerContext, kibanaBaseUrl: 1, }, taskInstance: { @@ -2334,8 +2183,8 @@ describe('Execution Handler', () => { } as unknown as ConcreteTaskInstance, }; - const executionHandler = new ExecutionHandler(generateExecutionParams(execParams)); - await executionHandler.run(generateAlert({ id: 1 })); + const actionScheduler = new ActionScheduler(getSchedulerContext(execParams)); + await actionScheduler.run(generateAlert({ id: 1 })); expect(injectActionParamsMock.mock.calls[0]).toMatchInlineSnapshot(` Array [ @@ -2353,10 +2202,10 @@ describe('Execution Handler', () => { it('sets the rule.url to the value from getViewInAppRelativeUrl when the rule type has it defined', async () => { const execParams = { - ...defaultExecutionParams, + ...defaultSchedulerContext, rule: ruleWithUrl, taskRunnerContext: { - ...defaultExecutionParams.taskRunnerContext, + ...defaultSchedulerContext.taskRunnerContext, kibanaBaseUrl: 'http://localhost:12345', }, ruleType: { @@ -2367,8 +2216,8 @@ describe('Execution Handler', () => { }, }; - const executionHandler = new ExecutionHandler(generateExecutionParams(execParams)); - await executionHandler.run(generateAlert({ id: 1 })); + const actionScheduler = new ActionScheduler(getSchedulerContext(execParams)); + await actionScheduler.run(generateAlert({ id: 1 })); expect(injectActionParamsMock.mock.calls[0]).toMatchInlineSnapshot(` Array [ @@ -2409,9 +2258,9 @@ describe('Execution Handler', () => { recovered: { count: 0, data: [] }, }); - const executorParams = generateExecutionParams({ + const executorParams = getSchedulerContext({ rule: { - ...defaultExecutionParams.rule, + ...defaultSchedulerContext.rule, systemActions: [ { id: '1', @@ -2434,9 +2283,9 @@ describe('Execution Handler', () => { executorParams.actionsClient.isSystemAction.mockReturnValue(true); executorParams.taskRunnerContext.kibanaBaseUrl = 'https://example.com'; - const executionHandler = new ExecutionHandler(generateExecutionParams(executorParams)); + const actionScheduler = new ActionScheduler(getSchedulerContext(executorParams)); - const res = await executionHandler.run(generateAlert({ id: 1 })); + const res = await actionScheduler.run(generateAlert({ id: 1 })); /** * Verifies that system actions are not throttled @@ -2535,9 +2384,9 @@ describe('Execution Handler', () => { recovered: { count: 0, data: [] }, }); - const executorParams = generateExecutionParams({ + const executorParams = getSchedulerContext({ rule: { - ...defaultExecutionParams.rule, + ...defaultSchedulerContext.rule, systemActions: [ { id: 'action-id', @@ -2554,9 +2403,9 @@ describe('Execution Handler', () => { executorParams.actionsClient.isSystemAction.mockReturnValue(true); executorParams.taskRunnerContext.kibanaBaseUrl = 'https://example.com'; - const executionHandler = new ExecutionHandler(generateExecutionParams(executorParams)); + const actionScheduler = new ActionScheduler(getSchedulerContext(executorParams)); - const res = await executionHandler.run(generateAlert({ id: 1 })); + const res = await actionScheduler.run(generateAlert({ id: 1 })); /** * Verifies that system actions are not throttled @@ -2587,13 +2436,13 @@ describe('Execution Handler', () => { test('do not execute if the rule type does not support summarized alerts', async () => { const actionsParams = { myParams: 'test' }; - const executorParams = generateExecutionParams({ + const executorParams = getSchedulerContext({ ruleType: { ...ruleType, alerts: undefined, }, rule: { - ...defaultExecutionParams.rule, + ...defaultSchedulerContext.rule, systemActions: [ { id: 'action-id', @@ -2610,9 +2459,9 @@ describe('Execution Handler', () => { executorParams.actionsClient.isSystemAction.mockReturnValue(true); executorParams.taskRunnerContext.kibanaBaseUrl = 'https://example.com'; - const executionHandler = new ExecutionHandler(generateExecutionParams(executorParams)); + const actionScheduler = new ActionScheduler(getSchedulerContext(executorParams)); - const res = await executionHandler.run(generateAlert({ id: 1 })); + const res = await actionScheduler.run(generateAlert({ id: 1 })); expect(res).toEqual({ throttledSummaryActions: {} }); expect(buildActionParams).not.toHaveBeenCalled(); @@ -2625,9 +2474,9 @@ describe('Execution Handler', () => { test('do not execute system actions if the rule type does not support summarized alerts', async () => { const actionsParams = { myParams: 'test' }; - const executorParams = generateExecutionParams({ + const executorParams = getSchedulerContext({ rule: { - ...defaultExecutionParams.rule, + ...defaultSchedulerContext.rule, systemActions: [ { id: '1', @@ -2638,7 +2487,7 @@ describe('Execution Handler', () => { ], }, ruleType: { - ...defaultExecutionParams.ruleType, + ...defaultSchedulerContext.ruleType, alerts: undefined, }, }); @@ -2648,9 +2497,9 @@ describe('Execution Handler', () => { executorParams.actionsClient.isSystemAction.mockReturnValue(true); executorParams.taskRunnerContext.kibanaBaseUrl = 'https://example.com'; - const executionHandler = new ExecutionHandler(generateExecutionParams(executorParams)); + const actionScheduler = new ActionScheduler(getSchedulerContext(executorParams)); - await executionHandler.run(generateAlert({ id: 1 })); + await actionScheduler.run(generateAlert({ id: 1 })); expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled(); expect(buildActionParams).not.toHaveBeenCalled(); diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/action_scheduler.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/action_scheduler.ts new file mode 100644 index 00000000000000..3b804ce3da4139 --- /dev/null +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/action_scheduler.ts @@ -0,0 +1,605 @@ +/* + * 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 { getRuleDetailsRoute, triggersActionsRoute } from '@kbn/rule-data-utils'; +import { asSavedObjectExecutionSource } from '@kbn/actions-plugin/server'; +import { + createTaskRunError, + isEphemeralTaskRejectedDueToCapacityError, + TaskErrorSource, +} from '@kbn/task-manager-plugin/server'; +import { + ExecuteOptions as EnqueueExecutionOptions, + ExecutionResponseItem, + ExecutionResponseType, +} from '@kbn/actions-plugin/server/create_execute_function'; +import { ActionsCompletion } from '@kbn/alerting-state-types'; +import { chunk } from 'lodash'; +import { CombinedSummarizedAlerts, ThrottledActions } from '../../types'; +import { injectActionParams } from '../inject_action_params'; +import { ActionSchedulerOptions, IActionScheduler, RuleUrl } from './types'; +import { + transformActionParams, + TransformActionParamsOptions, + transformSummaryActionParams, +} from '../transform_action_params'; +import { Alert } from '../../alert'; +import { + AlertInstanceContext, + AlertInstanceState, + RuleAction, + RuleTypeParams, + RuleTypeState, + SanitizedRule, + RuleAlertData, + RuleSystemAction, +} from '../../../common'; +import { + generateActionHash, + getSummaryActionsFromTaskState, + getSummaryActionTimeBounds, + isActionOnInterval, +} from './rule_action_helper'; +import { RULE_SAVED_OBJECT_TYPE } from '../../saved_objects'; +import { ConnectorAdapter } from '../../connector_adapters/types'; +import { withAlertingSpan } from '../lib'; +import * as schedulers from './schedulers'; + +interface LogAction { + id: string; + typeId: string; + alertId?: string; + alertGroup?: string; + alertSummary?: { + new: number; + ongoing: number; + recovered: number; + }; +} + +interface RunSummarizedActionArgs { + action: RuleAction; + summarizedAlerts: CombinedSummarizedAlerts; + spaceId: string; + bulkActions: EnqueueExecutionOptions[]; +} + +interface RunSystemActionArgs { + action: RuleSystemAction; + connectorAdapter: ConnectorAdapter; + summarizedAlerts: CombinedSummarizedAlerts; + rule: SanitizedRule; + ruleProducer: string; + spaceId: string; + bulkActions: EnqueueExecutionOptions[]; +} + +interface RunActionArgs< + State extends AlertInstanceState, + Context extends AlertInstanceContext, + ActionGroupIds extends string, + RecoveryActionGroupId extends string +> { + action: RuleAction; + alert: Alert; + ruleId: string; + spaceId: string; + bulkActions: EnqueueExecutionOptions[]; +} + +export interface RunResult { + throttledSummaryActions: ThrottledActions; +} + +export class ActionScheduler< + Params extends RuleTypeParams, + ExtractedParams extends RuleTypeParams, + RuleState extends RuleTypeState, + State extends AlertInstanceState, + Context extends AlertInstanceContext, + ActionGroupIds extends string, + RecoveryActionGroupId extends string, + AlertData extends RuleAlertData +> { + private readonly schedulers: Array< + IActionScheduler + > = []; + + private ephemeralActionsToSchedule: number; + private CHUNK_SIZE = 1000; + private ruleTypeActionGroups?: Map; + private previousStartedAt: Date | null; + + constructor( + private readonly context: ActionSchedulerOptions< + Params, + ExtractedParams, + RuleState, + State, + Context, + ActionGroupIds, + RecoveryActionGroupId, + AlertData + > + ) { + this.ephemeralActionsToSchedule = context.taskRunnerContext.maxEphemeralActionsPerRule; + this.ruleTypeActionGroups = new Map( + context.ruleType.actionGroups.map((actionGroup) => [actionGroup.id, actionGroup.name]) + ); + this.previousStartedAt = context.previousStartedAt; + + for (const [_, scheduler] of Object.entries(schedulers)) { + this.schedulers.push(new scheduler(context)); + } + + // sort schedulers by priority + this.schedulers.sort((a, b) => a.priority - b.priority); + } + + public async run( + alerts: Record> + ): Promise { + const throttledSummaryActions: ThrottledActions = getSummaryActionsFromTaskState({ + actions: this.context.rule.actions, + summaryActions: this.context.taskInstance.state?.summaryActions, + }); + + const executables = []; + for (const scheduler of this.schedulers) { + executables.push( + ...(await scheduler.generateExecutables({ alerts, throttledSummaryActions })) + ); + } + + if (executables.length === 0) { + return { throttledSummaryActions }; + } + + const { + CHUNK_SIZE, + context: { + logger, + alertingEventLogger, + ruleRunMetricsStore, + taskRunnerContext: { actionsConfigMap }, + taskInstance: { + params: { spaceId, alertId: ruleId }, + }, + }, + } = this; + + const logActions: Record = {}; + const bulkActions: EnqueueExecutionOptions[] = []; + let bulkActionsResponse: ExecutionResponseItem[] = []; + + this.context.ruleRunMetricsStore.incrementNumberOfGeneratedActions(executables.length); + + for (const { action, alert, summarizedAlerts } of executables) { + const { actionTypeId } = action; + + ruleRunMetricsStore.incrementNumberOfGeneratedActionsByConnectorType(actionTypeId); + if (ruleRunMetricsStore.hasReachedTheExecutableActionsLimit(actionsConfigMap)) { + ruleRunMetricsStore.setTriggeredActionsStatusByConnectorType({ + actionTypeId, + status: ActionsCompletion.PARTIAL, + }); + logger.debug( + `Rule "${this.context.rule.id}" skipped scheduling action "${action.id}" because the maximum number of allowed actions has been reached.` + ); + break; + } + + if ( + ruleRunMetricsStore.hasReachedTheExecutableActionsLimitByConnectorType({ + actionTypeId, + actionsConfigMap, + }) + ) { + if (!ruleRunMetricsStore.hasConnectorTypeReachedTheLimit(actionTypeId)) { + logger.debug( + `Rule "${this.context.rule.id}" skipped scheduling action "${action.id}" because the maximum number of allowed actions for connector type ${actionTypeId} has been reached.` + ); + } + ruleRunMetricsStore.setTriggeredActionsStatusByConnectorType({ + actionTypeId, + status: ActionsCompletion.PARTIAL, + }); + continue; + } + + if (!this.isExecutableAction(action)) { + this.context.logger.warn( + `Rule "${this.context.taskInstance.params.alertId}" skipped scheduling action "${action.id}" because it is disabled` + ); + continue; + } + + ruleRunMetricsStore.incrementNumberOfTriggeredActions(); + ruleRunMetricsStore.incrementNumberOfTriggeredActionsByConnectorType(actionTypeId); + + if (!this.isSystemAction(action) && summarizedAlerts) { + const defaultAction = action as RuleAction; + if (isActionOnInterval(action)) { + throttledSummaryActions[defaultAction.uuid!] = { date: new Date().toISOString() }; + } + + logActions[defaultAction.id] = await this.runSummarizedAction({ + action, + summarizedAlerts, + spaceId, + bulkActions, + }); + } else if (summarizedAlerts && this.isSystemAction(action)) { + const hasConnectorAdapter = this.context.taskRunnerContext.connectorAdapterRegistry.has( + action.actionTypeId + ); + /** + * System actions without an adapter + * cannot be executed + * + */ + if (!hasConnectorAdapter) { + this.context.logger.warn( + `Rule "${this.context.taskInstance.params.alertId}" skipped scheduling system action "${action.id}" because no connector adapter is configured` + ); + + continue; + } + + const connectorAdapter = this.context.taskRunnerContext.connectorAdapterRegistry.get( + action.actionTypeId + ); + logActions[action.id] = await this.runSystemAction({ + action, + connectorAdapter, + summarizedAlerts, + rule: this.context.rule, + ruleProducer: this.context.ruleType.producer, + spaceId, + bulkActions, + }); + } else if (!this.isSystemAction(action) && alert) { + const defaultAction = action as RuleAction; + logActions[defaultAction.id] = await this.runAction({ + action, + spaceId, + alert, + ruleId, + bulkActions, + }); + + const actionGroup = defaultAction.group; + if (!this.isRecoveredAlert(actionGroup)) { + if (isActionOnInterval(action)) { + alert.updateLastScheduledActions( + defaultAction.group as ActionGroupIds, + generateActionHash(action), + defaultAction.uuid + ); + } else { + alert.updateLastScheduledActions(defaultAction.group as ActionGroupIds); + } + alert.unscheduleActions(); + } + } + } + + if (!!bulkActions.length) { + for (const c of chunk(bulkActions, CHUNK_SIZE)) { + let enqueueResponse; + try { + enqueueResponse = await withAlertingSpan('alerting:bulk-enqueue-actions', () => + this.context.actionsClient!.bulkEnqueueExecution(c) + ); + } catch (e) { + if (e.statusCode === 404) { + throw createTaskRunError(e, TaskErrorSource.USER); + } + throw createTaskRunError(e, TaskErrorSource.FRAMEWORK); + } + if (enqueueResponse.errors) { + bulkActionsResponse = bulkActionsResponse.concat( + enqueueResponse.items.filter( + (i) => i.response === ExecutionResponseType.QUEUED_ACTIONS_LIMIT_ERROR + ) + ); + } + } + } + + if (!!bulkActionsResponse.length) { + for (const r of bulkActionsResponse) { + if (r.response === ExecutionResponseType.QUEUED_ACTIONS_LIMIT_ERROR) { + ruleRunMetricsStore.setHasReachedQueuedActionsLimit(true); + ruleRunMetricsStore.decrementNumberOfTriggeredActions(); + ruleRunMetricsStore.decrementNumberOfTriggeredActionsByConnectorType(r.actionTypeId); + ruleRunMetricsStore.setTriggeredActionsStatusByConnectorType({ + actionTypeId: r.actionTypeId, + status: ActionsCompletion.PARTIAL, + }); + + logger.debug( + `Rule "${this.context.rule.id}" skipped scheduling action "${r.id}" because the maximum number of queued actions has been reached.` + ); + + delete logActions[r.id]; + } + } + } + + const logActionsValues = Object.values(logActions); + if (!!logActionsValues.length) { + for (const action of logActionsValues) { + alertingEventLogger.logAction(action); + } + } + + return { throttledSummaryActions }; + } + + private async runSummarizedAction({ + action, + summarizedAlerts, + spaceId, + bulkActions, + }: RunSummarizedActionArgs): Promise { + const { start, end } = getSummaryActionTimeBounds( + action, + this.context.rule.schedule, + this.previousStartedAt + ); + const ruleUrl = this.buildRuleUrl(spaceId, start, end); + const actionToRun = { + ...action, + params: injectActionParams({ + actionTypeId: action.actionTypeId, + ruleUrl, + ruleName: this.context.rule.name, + actionParams: transformSummaryActionParams({ + alerts: summarizedAlerts, + rule: this.context.rule, + ruleTypeId: this.context.ruleType.id, + actionId: action.id, + actionParams: action.params, + spaceId, + actionsPlugin: this.context.taskRunnerContext.actionsPlugin, + actionTypeId: action.actionTypeId, + kibanaBaseUrl: this.context.taskRunnerContext.kibanaBaseUrl, + ruleUrl: ruleUrl?.absoluteUrl, + }), + }), + }; + + await this.actionRunOrAddToBulk({ + enqueueOptions: this.getEnqueueOptions(actionToRun), + bulkActions, + }); + + return { + id: action.id, + typeId: action.actionTypeId, + alertSummary: { + new: summarizedAlerts.new.count, + ongoing: summarizedAlerts.ongoing.count, + recovered: summarizedAlerts.recovered.count, + }, + }; + } + + private async runSystemAction({ + action, + spaceId, + connectorAdapter, + summarizedAlerts, + rule, + ruleProducer, + bulkActions, + }: RunSystemActionArgs): Promise { + const ruleUrl = this.buildRuleUrl(spaceId); + + const connectorAdapterActionParams = connectorAdapter.buildActionParams({ + alerts: summarizedAlerts, + rule: { + id: rule.id, + tags: rule.tags, + name: rule.name, + consumer: rule.consumer, + producer: ruleProducer, + }, + ruleUrl: ruleUrl?.absoluteUrl, + spaceId, + params: action.params, + }); + + const actionToRun = Object.assign(action, { params: connectorAdapterActionParams }); + + await this.actionRunOrAddToBulk({ + enqueueOptions: this.getEnqueueOptions(actionToRun), + bulkActions, + }); + + return { + id: action.id, + typeId: action.actionTypeId, + alertSummary: { + new: summarizedAlerts.new.count, + ongoing: summarizedAlerts.ongoing.count, + recovered: summarizedAlerts.recovered.count, + }, + }; + } + + private async runAction({ + action, + spaceId, + alert, + ruleId, + bulkActions, + }: RunActionArgs): Promise { + const ruleUrl = this.buildRuleUrl(spaceId); + const executableAlert = alert!; + const actionGroup = action.group as ActionGroupIds; + const transformActionParamsOptions: TransformActionParamsOptions = { + actionsPlugin: this.context.taskRunnerContext.actionsPlugin, + alertId: ruleId, + alertType: this.context.ruleType.id, + actionTypeId: action.actionTypeId, + alertName: this.context.rule.name, + spaceId, + tags: this.context.rule.tags, + alertInstanceId: executableAlert.getId(), + alertUuid: executableAlert.getUuid(), + alertActionGroup: actionGroup, + alertActionGroupName: this.ruleTypeActionGroups!.get(actionGroup)!, + context: executableAlert.getContext(), + actionId: action.id, + state: executableAlert.getState(), + kibanaBaseUrl: this.context.taskRunnerContext.kibanaBaseUrl, + alertParams: this.context.rule.params, + actionParams: action.params, + flapping: executableAlert.getFlapping(), + ruleUrl: ruleUrl?.absoluteUrl, + }; + + if (executableAlert.isAlertAsData()) { + transformActionParamsOptions.aadAlert = executableAlert.getAlertAsData(); + } + const actionToRun = { + ...action, + params: injectActionParams({ + actionTypeId: action.actionTypeId, + ruleUrl, + ruleName: this.context.rule.name, + actionParams: transformActionParams(transformActionParamsOptions), + }), + }; + + await this.actionRunOrAddToBulk({ + enqueueOptions: this.getEnqueueOptions(actionToRun), + bulkActions, + }); + + return { + id: action.id, + typeId: action.actionTypeId, + alertId: alert.getId(), + alertGroup: action.group, + }; + } + + private isExecutableAction(action: RuleAction | RuleSystemAction) { + return this.context.taskRunnerContext.actionsPlugin.isActionExecutable( + action.id, + action.actionTypeId, + { + notifyUsage: true, + } + ); + } + + private isSystemAction(action?: RuleAction | RuleSystemAction): action is RuleSystemAction { + return this.context.taskRunnerContext.actionsPlugin.isSystemActionConnector(action?.id ?? ''); + } + + private isRecoveredAlert(actionGroup: string) { + return actionGroup === this.context.ruleType.recoveryActionGroup.id; + } + + private buildRuleUrl(spaceId: string, start?: number, end?: number): RuleUrl | undefined { + if (!this.context.taskRunnerContext.kibanaBaseUrl) { + return; + } + + const relativePath = this.context.ruleType.getViewInAppRelativeUrl + ? this.context.ruleType.getViewInAppRelativeUrl({ rule: this.context.rule, start, end }) + : `${triggersActionsRoute}${getRuleDetailsRoute(this.context.rule.id)}`; + + try { + const basePathname = new URL(this.context.taskRunnerContext.kibanaBaseUrl).pathname; + const basePathnamePrefix = basePathname !== '/' ? `${basePathname}` : ''; + const spaceIdSegment = spaceId !== 'default' ? `/s/${spaceId}` : ''; + + const ruleUrl = new URL( + [basePathnamePrefix, spaceIdSegment, relativePath].join(''), + this.context.taskRunnerContext.kibanaBaseUrl + ); + + return { + absoluteUrl: ruleUrl.toString(), + kibanaBaseUrl: this.context.taskRunnerContext.kibanaBaseUrl, + basePathname: basePathnamePrefix, + spaceIdSegment, + relativePath, + }; + } catch (error) { + this.context.logger.debug( + `Rule "${this.context.rule.id}" encountered an error while constructing the rule.url variable: ${error.message}` + ); + return; + } + } + + private getEnqueueOptions(action: RuleAction | RuleSystemAction): EnqueueExecutionOptions { + const { + context: { + apiKey, + ruleConsumer, + executionId, + taskInstance: { + params: { spaceId, alertId: ruleId }, + }, + }, + } = this; + + const namespace = spaceId === 'default' ? {} : { namespace: spaceId }; + return { + id: action.id, + params: action.params, + spaceId, + apiKey: apiKey ?? null, + consumer: ruleConsumer, + source: asSavedObjectExecutionSource({ + id: ruleId, + type: RULE_SAVED_OBJECT_TYPE, + }), + executionId, + relatedSavedObjects: [ + { + id: ruleId, + type: RULE_SAVED_OBJECT_TYPE, + namespace: namespace.namespace, + typeId: this.context.ruleType.id, + }, + ], + actionTypeId: action.actionTypeId, + }; + } + + private async actionRunOrAddToBulk({ + enqueueOptions, + bulkActions, + }: { + enqueueOptions: EnqueueExecutionOptions; + bulkActions: EnqueueExecutionOptions[]; + }) { + if ( + this.context.taskRunnerContext.supportsEphemeralTasks && + this.ephemeralActionsToSchedule > 0 + ) { + this.ephemeralActionsToSchedule--; + try { + await this.context.actionsClient!.ephemeralEnqueuedExecution(enqueueOptions); + } catch (err) { + if (isEphemeralTaskRejectedDueToCapacityError(err)) { + bulkActions.push(enqueueOptions); + } + } + } else { + bulkActions.push(enqueueOptions); + } + } +} diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/get_summarized_alerts.test.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/get_summarized_alerts.test.ts new file mode 100644 index 00000000000000..9afd0647094eb8 --- /dev/null +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/get_summarized_alerts.test.ts @@ -0,0 +1,127 @@ +/* + * 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 { getSummarizedAlerts } from './get_summarized_alerts'; +import { alertsClientMock } from '../../alerts_client/alerts_client.mock'; +import { mockAAD } from '../fixtures'; +import { ALERT_UUID } from '@kbn/rule-data-utils'; +import { generateAlert } from './test_fixtures'; +import { getErrorSource } from '@kbn/task-manager-plugin/server/task_running'; + +const alertsClient = alertsClientMock.create(); + +describe('getSummarizedAlerts', () => { + const newAlert1 = generateAlert({ id: 1 }); + const newAlert2 = generateAlert({ id: 2 }); + const alerts = { ...newAlert1, ...newAlert2 }; + + beforeEach(() => { + jest.resetAllMocks(); + }); + + test('should call alertsClient.getSummarizedAlerts with the correct params', async () => { + const summarizedAlerts = { + new: { + count: 2, + data: [ + { ...mockAAD, [ALERT_UUID]: alerts[1].getUuid() }, + { ...mockAAD, [ALERT_UUID]: alerts[2].getUuid() }, + ], + }, + ongoing: { count: 0, data: [] }, + recovered: { count: 0, data: [] }, + }; + alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts); + alertsClient.getProcessedAlerts.mockReturnValue(alerts); + const result = await getSummarizedAlerts({ + alertsClient, + queryOptions: { + excludedAlertInstanceIds: [], + executionUuid: '123xyz', + ruleId: '1', + spaceId: 'test1', + }, + }); + + expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({ + excludedAlertInstanceIds: [], + executionUuid: '123xyz', + ruleId: '1', + spaceId: 'test1', + }); + + expect(result).toEqual({ + ...summarizedAlerts, + all: summarizedAlerts.new, + }); + }); + + test('should throw error if alertsClient.getSummarizedAlerts throws error', async () => { + alertsClient.getSummarizedAlerts.mockImplementation(() => { + throw new Error('cannot get summarized alerts'); + }); + + try { + await getSummarizedAlerts({ + alertsClient, + queryOptions: { + excludedAlertInstanceIds: [], + executionUuid: '123xyz', + ruleId: '1', + spaceId: 'test1', + }, + }); + } catch (err) { + expect(getErrorSource(err)).toBe('framework'); + expect(err.message).toBe('cannot get summarized alerts'); + } + }); + + test('should remove alert from summarized alerts if it is new and has a maintenance window', async () => { + const newAlertWithMaintenanceWindow = generateAlert({ + id: 1, + maintenanceWindowIds: ['mw-1'], + }); + const alertsWithMaintenanceWindow = { ...newAlertWithMaintenanceWindow, ...newAlert2 }; + + const newAADAlerts = [ + { ...mockAAD, [ALERT_UUID]: newAlertWithMaintenanceWindow[1].getUuid() }, + { ...mockAAD, [ALERT_UUID]: alerts[2].getUuid() }, + ]; + const summarizedAlerts = { + new: { count: 2, data: newAADAlerts }, + ongoing: { count: 0, data: [] }, + recovered: { count: 0, data: [] }, + }; + alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts); + alertsClient.getProcessedAlerts.mockReturnValue(alertsWithMaintenanceWindow); + + const result = await getSummarizedAlerts({ + alertsClient, + queryOptions: { + excludedAlertInstanceIds: [], + executionUuid: '123xyz', + ruleId: '1', + spaceId: 'test1', + }, + }); + + expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({ + excludedAlertInstanceIds: [], + executionUuid: '123xyz', + ruleId: '1', + spaceId: 'test1', + }); + + expect(result).toEqual({ + new: { count: 1, data: [newAADAlerts[1]] }, + ongoing: { count: 0, data: [] }, + recovered: { count: 0, data: [] }, + all: { count: 1, data: [newAADAlerts[1]] }, + }); + }); +}); diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/get_summarized_alerts.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/get_summarized_alerts.ts new file mode 100644 index 00000000000000..df667a3e207750 --- /dev/null +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/get_summarized_alerts.ts @@ -0,0 +1,78 @@ +/* + * 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 { ALERT_UUID } from '@kbn/rule-data-utils'; +import { createTaskRunError, TaskErrorSource } from '@kbn/task-manager-plugin/server'; +import { GetSummarizedAlertsParams, IAlertsClient } from '../../alerts_client/types'; +import { + AlertInstanceContext, + AlertInstanceState, + CombinedSummarizedAlerts, + RuleAlertData, +} from '../../types'; + +interface GetSummarizedAlertsOpts< + State extends AlertInstanceState, + Context extends AlertInstanceContext, + ActionGroupIds extends string, + RecoveryActionGroupId extends string, + AlertData extends RuleAlertData +> { + alertsClient: IAlertsClient; + queryOptions: GetSummarizedAlertsParams; +} + +export const getSummarizedAlerts = async < + State extends AlertInstanceState, + Context extends AlertInstanceContext, + ActionGroupIds extends string, + RecoveryActionGroupId extends string, + AlertData extends RuleAlertData +>({ + alertsClient, + queryOptions, +}: GetSummarizedAlertsOpts< + State, + Context, + ActionGroupIds, + RecoveryActionGroupId, + AlertData +>): Promise => { + let alerts; + try { + alerts = await alertsClient.getSummarizedAlerts!(queryOptions); + } catch (e) { + throw createTaskRunError(e, TaskErrorSource.FRAMEWORK); + } + + /** + * We need to remove all new alerts with maintenance windows retrieved from + * getSummarizedAlerts because they might not have maintenance window IDs + * associated with them from maintenance windows with scoped query updated + * yet (the update call uses refresh: false). So we need to rely on the in + * memory alerts to do this. + */ + const newAlertsInMemory = Object.values(alertsClient.getProcessedAlerts('new') || {}) || []; + + const newAlertsWithMaintenanceWindowIds = newAlertsInMemory.reduce((result, alert) => { + if (alert.getMaintenanceWindowIds().length > 0) { + result.push(alert.getUuid()); + } + return result; + }, []); + + const newAlerts = alerts.new.data.filter((alert) => { + return !newAlertsWithMaintenanceWindowIds.includes(alert[ALERT_UUID]); + }); + + const total = newAlerts.length + alerts.ongoing.count + alerts.recovered.count; + return { + ...alerts, + new: { count: newAlerts.length, data: newAlerts }, + all: { count: total, data: [...newAlerts, ...alerts.ongoing.data, ...alerts.recovered.data] }, + }; +}; diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/index.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/index.ts new file mode 100644 index 00000000000000..83673d71c78b80 --- /dev/null +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/index.ts @@ -0,0 +1,10 @@ +/* + * 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. + */ + +export { ActionScheduler } from './action_scheduler'; +export type { RunResult } from './action_scheduler'; +export type { RuleUrl } from './types'; diff --git a/x-pack/plugins/alerting/server/task_runner/rule_action_helper.test.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/rule_action_helper.test.ts similarity index 91% rename from x-pack/plugins/alerting/server/task_runner/rule_action_helper.test.ts rename to x-pack/plugins/alerting/server/task_runner/action_scheduler/rule_action_helper.test.ts index 2edd66bc6f43c4..cc8a0a1b0cde5c 100644 --- a/x-pack/plugins/alerting/server/task_runner/rule_action_helper.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/rule_action_helper.test.ts @@ -6,15 +6,16 @@ */ import { Logger } from '@kbn/logging'; -import { RuleAction } from '../types'; +import { loggingSystemMock } from '@kbn/core/server/mocks'; +import { RuleAction } from '../../types'; import { generateActionHash, getSummaryActionsFromTaskState, isActionOnInterval, isSummaryAction, - isSummaryActionOnInterval, isSummaryActionThrottled, getSummaryActionTimeBounds, + logNumberOfFilteredAlerts, } from './rule_action_helper'; const now = '2021-05-13T12:33:37.000Z'; @@ -291,30 +292,6 @@ describe('rule_action_helper', () => { }); }); - describe('isSummaryActionOnInterval', () => { - test('returns true for a summary action on interval', () => { - expect(isSummaryActionOnInterval(mockSummaryAction)).toBe(true); - }); - - test('returns false for a non-summary ', () => { - expect( - isSummaryActionOnInterval({ - ...mockAction, - frequency: { summary: false, notifyWhen: 'onThrottleInterval', throttle: '1h' }, - }) - ).toBe(false); - }); - - test('returns false for a summary per rule run ', () => { - expect( - isSummaryActionOnInterval({ - ...mockAction, - frequency: { summary: true, notifyWhen: 'onActiveAlert', throttle: null }, - }) - ).toBe(false); - }); - }); - describe('getSummaryActionTimeBounds', () => { test('returns undefined start and end action is not summary action', () => { expect(getSummaryActionTimeBounds(mockAction, { interval: '1m' }, null)).toEqual({ @@ -370,4 +347,30 @@ describe('rule_action_helper', () => { expect(start).toEqual(new Date('2021-05-13T12:32:37.000Z').valueOf()); }); }); + + describe('logNumberOfFilteredAlerts', () => { + test('should log when the number of alerts is different than the number of summarized alerts', () => { + const logger = loggingSystemMock.create().get(); + logNumberOfFilteredAlerts({ + logger, + numberOfAlerts: 10, + numberOfSummarizedAlerts: 5, + action: mockSummaryAction, + }); + expect(logger.debug).toHaveBeenCalledWith( + '(5) alerts have been filtered out for: slack:111-111' + ); + }); + + test('should not log when the number of alerts is the same as the number of summarized alerts', () => { + const logger = loggingSystemMock.create().get(); + logNumberOfFilteredAlerts({ + logger, + numberOfAlerts: 10, + numberOfSummarizedAlerts: 10, + action: mockSummaryAction, + }); + expect(logger.debug).not.toHaveBeenCalled(); + }); + }); }); diff --git a/x-pack/plugins/alerting/server/task_runner/rule_action_helper.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/rule_action_helper.ts similarity index 85% rename from x-pack/plugins/alerting/server/task_runner/rule_action_helper.ts rename to x-pack/plugins/alerting/server/task_runner/action_scheduler/rule_action_helper.ts index 8845988e06bd4d..67223b07286898 100644 --- a/x-pack/plugins/alerting/server/task_runner/rule_action_helper.ts +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/rule_action_helper.ts @@ -12,7 +12,7 @@ import { RuleAction, RuleNotifyWhenTypeValues, ThrottledActions, -} from '../../common'; +} from '../../../common'; export const isSummaryAction = (action?: RuleAction) => { return action?.frequency?.summary ?? false; @@ -28,10 +28,6 @@ export const isActionOnInterval = (action?: RuleAction) => { ); }; -export const isSummaryActionOnInterval = (action: RuleAction) => { - return isActionOnInterval(action) && action.frequency?.summary; -}; - export const isSummaryActionThrottled = ({ action, throttledSummaryActions, @@ -129,3 +125,25 @@ export const getSummaryActionTimeBounds = ( return { start: startDate.valueOf(), end: now.valueOf() }; }; + +interface LogNumberOfFilteredAlertsOpts { + logger: Logger; + numberOfAlerts: number; + numberOfSummarizedAlerts: number; + action: RuleAction; +} +export const logNumberOfFilteredAlerts = ({ + logger, + numberOfAlerts = 0, + numberOfSummarizedAlerts = 0, + action, +}: LogNumberOfFilteredAlertsOpts) => { + const count = numberOfAlerts - numberOfSummarizedAlerts; + if (count > 0) { + logger.debug( + `(${count}) alert${count > 1 ? 's' : ''} ${ + count > 1 ? 'have' : 'has' + } been filtered out for: ${action.actionTypeId}:${action.uuid}` + ); + } +}; diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/index.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/index.ts new file mode 100644 index 00000000000000..85754cbae0f6b7 --- /dev/null +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/index.ts @@ -0,0 +1,10 @@ +/* + * 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. + */ + +export { SystemActionScheduler } from './system_action_scheduler'; +export { SummaryActionScheduler } from './summary_action_scheduler'; +export { PerAlertActionScheduler } from './per_alert_action_scheduler'; diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/per_alert_action_scheduler.test.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/per_alert_action_scheduler.test.ts new file mode 100644 index 00000000000000..53e75245d94d0c --- /dev/null +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/per_alert_action_scheduler.test.ts @@ -0,0 +1,849 @@ +/* + * 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 sinon from 'sinon'; +import { actionsClientMock, actionsMock } from '@kbn/actions-plugin/server/mocks'; +import { loggingSystemMock } from '@kbn/core-logging-server-mocks'; +import { alertsClientMock } from '../../../alerts_client/alerts_client.mock'; +import { alertingEventLoggerMock } from '../../../lib/alerting_event_logger/alerting_event_logger.mock'; +import { RuleRunMetricsStore } from '../../../lib/rule_run_metrics_store'; +import { mockAAD } from '../../fixtures'; +import { PerAlertActionScheduler } from './per_alert_action_scheduler'; +import { getRule, getRuleType, getDefaultSchedulerContext, generateAlert } from '../test_fixtures'; +import { SanitizedRuleAction } from '@kbn/alerting-types'; +import { ALERT_UUID } from '@kbn/rule-data-utils'; + +const alertingEventLogger = alertingEventLoggerMock.create(); +const actionsClient = actionsClientMock.create(); +const alertsClient = alertsClientMock.create(); +const mockActionsPlugin = actionsMock.createStart(); +const logger = loggingSystemMock.create().get(); + +let ruleRunMetricsStore: RuleRunMetricsStore; +const rule = getRule({ + actions: [ + { + id: '1', + group: 'default', + actionTypeId: 'test', + frequency: { summary: false, notifyWhen: 'onActiveAlert', throttle: null }, + params: { + foo: true, + contextVal: 'My {{context.value}} goes here', + stateVal: 'My {{state.value}} goes here', + alertVal: + 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', + }, + uuid: '111-111', + }, + { + id: '2', + group: 'default', + actionTypeId: 'test', + frequency: { summary: false, notifyWhen: 'onActiveAlert', throttle: null }, + params: { + foo: true, + contextVal: 'My {{context.value}} goes here', + stateVal: 'My {{state.value}} goes here', + alertVal: + 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', + }, + uuid: '222-222', + }, + { + id: '3', + group: 'default', + actionTypeId: 'test', + frequency: { summary: true, notifyWhen: 'onActiveAlert' }, + params: { + foo: true, + contextVal: 'My {{context.value}} goes here', + stateVal: 'My {{state.value}} goes here', + alertVal: + 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', + }, + uuid: '333-333', + }, + ], +}); +const ruleType = getRuleType(); +const defaultSchedulerContext = getDefaultSchedulerContext( + logger, + mockActionsPlugin, + alertingEventLogger, + actionsClient, + alertsClient +); + +// @ts-ignore +const getSchedulerContext = (params = {}) => { + return { ...defaultSchedulerContext, rule, ...params, ruleRunMetricsStore }; +}; + +let clock: sinon.SinonFakeTimers; + +describe('Per-Alert Action Scheduler', () => { + beforeAll(() => { + clock = sinon.useFakeTimers(); + }); + + beforeEach(() => { + jest.resetAllMocks(); + mockActionsPlugin.isActionTypeEnabled.mockReturnValue(true); + mockActionsPlugin.isActionExecutable.mockReturnValue(true); + mockActionsPlugin.getActionsClientWithRequest.mockResolvedValue(actionsClient); + ruleRunMetricsStore = new RuleRunMetricsStore(); + }); + + afterAll(() => { + clock.restore(); + }); + + test('should initialize with only per-alert actions', () => { + const scheduler = new PerAlertActionScheduler(getSchedulerContext()); + + // @ts-expect-error private variable + expect(scheduler.actions).toHaveLength(2); + // @ts-expect-error private variable + expect(scheduler.actions).toEqual([rule.actions[0], rule.actions[1]]); + expect(logger.error).not.toHaveBeenCalled(); + }); + + test('should not initialize action and log if rule type does not support summarized alerts and action has alertsFilter', () => { + const actions = [ + { + id: '1', + group: 'default', + actionTypeId: 'test', + frequency: { summary: false, notifyWhen: 'onActiveAlert', throttle: null }, + params: { + foo: true, + contextVal: 'My {{context.value}} goes here', + stateVal: 'My {{state.value}} goes here', + alertVal: + 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', + }, + uuid: '111-111', + }, + { + id: '2', + group: 'default', + actionTypeId: 'test', + frequency: { summary: false, notifyWhen: 'onActiveAlert', throttle: null }, + alertsFilter: { + query: { kql: 'kibana.alert.rule.name:foo', dsl: '{}', filters: [] }, + }, + params: { + foo: true, + contextVal: 'My {{context.value}} goes here', + stateVal: 'My {{state.value}} goes here', + alertVal: + 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', + }, + uuid: '222-222', + }, + ]; + const scheduler = new PerAlertActionScheduler( + getSchedulerContext({ + rule: { + ...rule, + actions, + }, + ruleType: { ...ruleType, alerts: undefined }, + }) + ); + + // @ts-expect-error private variable + expect(scheduler.actions).toHaveLength(1); + // @ts-expect-error private variable + expect(scheduler.actions).toEqual([actions[0]]); + expect(logger.error).toHaveBeenCalledTimes(1); + expect(logger.error).toHaveBeenCalledWith( + `Skipping action \"2\" for rule \"1\" because the rule type \"Test\" does not support alert-as-data.` + ); + }); + + describe('generateExecutables', () => { + const newAlert1 = generateAlert({ id: 1 }); + const newAlert2 = generateAlert({ id: 2 }); + const alerts = { ...newAlert1, ...newAlert2 }; + + test('should generate executable for each alert and each action', async () => { + const scheduler = new PerAlertActionScheduler(getSchedulerContext()); + const executables = await scheduler.generateExecutables({ + alerts, + throttledSummaryActions: {}, + }); + + expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled(); + expect(logger.debug).not.toHaveBeenCalled(); + + expect(executables).toHaveLength(4); + + expect(executables).toEqual([ + { action: rule.actions[0], alert: alerts['1'] }, + { action: rule.actions[0], alert: alerts['2'] }, + { action: rule.actions[1], alert: alerts['1'] }, + { action: rule.actions[1], alert: alerts['2'] }, + ]); + }); + + test('should skip generating executable when alert has maintenance window', async () => { + const scheduler = new PerAlertActionScheduler(getSchedulerContext()); + const newAlertWithMaintenanceWindow = generateAlert({ + id: 1, + maintenanceWindowIds: ['mw-1'], + }); + const alertsWithMaintenanceWindow = { ...newAlertWithMaintenanceWindow, ...newAlert2 }; + const executables = await scheduler.generateExecutables({ + alerts: alertsWithMaintenanceWindow, + throttledSummaryActions: {}, + }); + + expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled(); + expect(logger.debug).toHaveBeenCalledTimes(2); + expect(logger.debug).toHaveBeenNthCalledWith( + 1, + `no scheduling of summary actions \"1\" for rule \"1\": has active maintenance windows mw-1.` + ); + expect(logger.debug).toHaveBeenNthCalledWith( + 2, + `no scheduling of summary actions \"2\" for rule \"1\": has active maintenance windows mw-1.` + ); + + expect(executables).toHaveLength(2); + + expect(executables).toEqual([ + { action: rule.actions[0], alert: alerts['2'] }, + { action: rule.actions[1], alert: alerts['2'] }, + ]); + }); + + test('should skip generating executable when alert has invalid action group', async () => { + const scheduler = new PerAlertActionScheduler(getSchedulerContext()); + const newAlertInvalidActionGroup = generateAlert({ + id: 1, + // @ts-expect-error + group: 'invalid', + }); + const alertsWithInvalidActionGroup = { ...newAlertInvalidActionGroup, ...newAlert2 }; + const executables = await scheduler.generateExecutables({ + alerts: alertsWithInvalidActionGroup, + throttledSummaryActions: {}, + }); + + expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled(); + expect(logger.error).toHaveBeenCalledTimes(2); + expect(logger.error).toHaveBeenNthCalledWith( + 1, + `Invalid action group \"invalid\" for rule \"test\".` + ); + expect(logger.error).toHaveBeenNthCalledWith( + 2, + `Invalid action group \"invalid\" for rule \"test\".` + ); + + expect(executables).toHaveLength(2); + + expect(executables).toEqual([ + { action: rule.actions[0], alert: alerts['2'] }, + { action: rule.actions[1], alert: alerts['2'] }, + ]); + }); + + test('should skip generating executable when alert has pending recovered count greater than 0 and notifyWhen is onActiveAlert', async () => { + const scheduler = new PerAlertActionScheduler(getSchedulerContext()); + const newAlertWithPendingRecoveredCount = generateAlert({ + id: 1, + pendingRecoveredCount: 3, + }); + const alertsWithPendingRecoveredCount = { + ...newAlertWithPendingRecoveredCount, + ...newAlert2, + }; + const executables = await scheduler.generateExecutables({ + alerts: alertsWithPendingRecoveredCount, + throttledSummaryActions: {}, + }); + + expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled(); + expect(executables).toHaveLength(2); + + expect(executables).toEqual([ + { action: rule.actions[0], alert: alerts['2'] }, + { action: rule.actions[1], alert: alerts['2'] }, + ]); + }); + + test('should skip generating executable when alert has pending recovered count greater than 0 and notifyWhen is onThrottleInterval', async () => { + const onThrottleIntervalAction: SanitizedRuleAction = { + id: '2', + group: 'default', + actionTypeId: 'test', + frequency: { summary: false, notifyWhen: 'onThrottleInterval', throttle: '1h' }, + params: { + foo: true, + contextVal: 'My {{context.value}} goes here', + stateVal: 'My {{state.value}} goes here', + alertVal: + 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', + }, + uuid: '222-222', + }; + const scheduler = new PerAlertActionScheduler({ + ...getSchedulerContext(), + rule: { ...rule, actions: [rule.actions[0], onThrottleIntervalAction] }, + }); + const newAlertWithPendingRecoveredCount = generateAlert({ + id: 1, + pendingRecoveredCount: 3, + }); + const alertsWithPendingRecoveredCount = { + ...newAlertWithPendingRecoveredCount, + ...newAlert2, + }; + const executables = await scheduler.generateExecutables({ + alerts: alertsWithPendingRecoveredCount, + throttledSummaryActions: {}, + }); + + expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled(); + expect(executables).toHaveLength(2); + + expect(executables).toEqual([ + { action: rule.actions[0], alert: alerts['2'] }, + { action: onThrottleIntervalAction, alert: alerts['2'] }, + ]); + }); + + test('should skip generating executable when alert is muted', async () => { + const scheduler = new PerAlertActionScheduler({ + ...getSchedulerContext(), + rule: { ...rule, mutedInstanceIds: ['2'] }, + }); + const executables = await scheduler.generateExecutables({ + alerts, + throttledSummaryActions: {}, + }); + + expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled(); + expect(logger.debug).toHaveBeenCalledTimes(1); + expect(logger.debug).toHaveBeenNthCalledWith( + 1, + `skipping scheduling of actions for '2' in rule rule-label: rule is muted` + ); + expect(executables).toHaveLength(2); + + // @ts-expect-error private variable + expect(scheduler.skippedAlerts).toEqual({ '2': { reason: 'muted' } }); + + expect(executables).toEqual([ + { action: rule.actions[0], alert: alerts['1'] }, + { action: rule.actions[1], alert: alerts['1'] }, + ]); + }); + + test('should skip generating executable when alert action group has not changed and notifyWhen is onActionGroupChange', async () => { + const onActionGroupChangeAction: SanitizedRuleAction = { + id: '2', + group: 'default', + actionTypeId: 'test', + frequency: { summary: false, notifyWhen: 'onActionGroupChange', throttle: null }, + params: { + foo: true, + contextVal: 'My {{context.value}} goes here', + stateVal: 'My {{state.value}} goes here', + alertVal: + 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', + }, + uuid: '222-222', + }; + + const activeAlert1 = generateAlert({ + id: 1, + group: 'default', + lastScheduledActionsGroup: 'other-group', + }); + const activeAlert2 = generateAlert({ + id: 2, + group: 'default', + lastScheduledActionsGroup: 'default', + }); + const alertsWithOngoingAlert = { ...activeAlert1, ...activeAlert2 }; + + const scheduler = new PerAlertActionScheduler({ + ...getSchedulerContext(), + rule: { ...rule, actions: [rule.actions[0], onActionGroupChangeAction] }, + }); + + const executables = await scheduler.generateExecutables({ + alerts: alertsWithOngoingAlert, + throttledSummaryActions: {}, + }); + + expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled(); + expect(logger.debug).toHaveBeenCalledTimes(1); + expect(logger.debug).toHaveBeenNthCalledWith( + 1, + `skipping scheduling of actions for '2' in rule rule-label: alert is active but action group has not changed` + ); + expect(executables).toHaveLength(3); + + // @ts-expect-error private variable + expect(scheduler.skippedAlerts).toEqual({ '2': { reason: 'actionGroupHasNotChanged' } }); + + expect(executables).toEqual([ + { action: rule.actions[0], alert: alertsWithOngoingAlert['1'] }, + { action: rule.actions[0], alert: alertsWithOngoingAlert['2'] }, + { action: onActionGroupChangeAction, alert: alertsWithOngoingAlert['1'] }, + ]); + }); + + test('should skip generating executable when throttle interval has not passed and notifyWhen is onThrottleInterval', async () => { + const onThrottleIntervalAction: SanitizedRuleAction = { + id: '2', + group: 'default', + actionTypeId: 'test', + frequency: { summary: false, notifyWhen: 'onThrottleInterval', throttle: '1h' }, + params: { + foo: true, + contextVal: 'My {{context.value}} goes here', + stateVal: 'My {{state.value}} goes here', + alertVal: + 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', + }, + uuid: '222-222', + }; + + const activeAlert2 = generateAlert({ + id: 2, + lastScheduledActionsGroup: 'default', + throttledActions: { '222-222': { date: '1969-12-31T23:10:00.000Z' } }, + }); + const alertsWithOngoingAlert = { ...newAlert1, ...activeAlert2 }; + + const scheduler = new PerAlertActionScheduler({ + ...getSchedulerContext(), + rule: { ...rule, actions: [rule.actions[0], onThrottleIntervalAction] }, + }); + + const executables = await scheduler.generateExecutables({ + alerts: alertsWithOngoingAlert, + throttledSummaryActions: {}, + }); + + expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled(); + expect(logger.debug).toHaveBeenCalledTimes(1); + expect(logger.debug).toHaveBeenNthCalledWith( + 1, + `skipping scheduling of actions for '2' in rule rule-label: rule is throttled` + ); + expect(executables).toHaveLength(3); + + // @ts-expect-error private variable + expect(scheduler.skippedAlerts).toEqual({ '2': { reason: 'throttled' } }); + + expect(executables).toEqual([ + { action: rule.actions[0], alert: alertsWithOngoingAlert['1'] }, + { action: rule.actions[0], alert: alertsWithOngoingAlert['2'] }, + { action: onThrottleIntervalAction, alert: alertsWithOngoingAlert['1'] }, + ]); + }); + + test('should not skip generating executable when throttle interval has passed and notifyWhen is onThrottleInterval', async () => { + const onThrottleIntervalAction: SanitizedRuleAction = { + id: '2', + group: 'default', + actionTypeId: 'test', + frequency: { summary: false, notifyWhen: 'onThrottleInterval', throttle: '1h' }, + params: { + foo: true, + contextVal: 'My {{context.value}} goes here', + stateVal: 'My {{state.value}} goes here', + alertVal: + 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', + }, + uuid: '222-222', + }; + + const activeAlert2 = generateAlert({ + id: 2, + lastScheduledActionsGroup: 'default', + throttledActions: { '222-222': { date: '1969-12-31T22:10:00.000Z' } }, + }); + const alertsWithOngoingAlert = { ...newAlert1, ...activeAlert2 }; + + const scheduler = new PerAlertActionScheduler({ + ...getSchedulerContext(), + rule: { ...rule, actions: [rule.actions[0], onThrottleIntervalAction] }, + }); + + const executables = await scheduler.generateExecutables({ + alerts: alertsWithOngoingAlert, + throttledSummaryActions: {}, + }); + + expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled(); + expect(logger.debug).not.toHaveBeenCalled(); + expect(executables).toHaveLength(4); + + // @ts-expect-error private variable + expect(scheduler.skippedAlerts).toEqual({}); + + expect(executables).toEqual([ + { action: rule.actions[0], alert: alertsWithOngoingAlert['1'] }, + { action: rule.actions[0], alert: alertsWithOngoingAlert['2'] }, + { action: onThrottleIntervalAction, alert: alertsWithOngoingAlert['1'] }, + { action: onThrottleIntervalAction, alert: alertsWithOngoingAlert['2'] }, + ]); + }); + + test('should query for summarized alerts if useAlertDataForTemplate is true', async () => { + alertsClient.getProcessedAlerts.mockReturnValue(alerts); + const summarizedAlerts = { + new: { + count: 1, + data: [ + { ...mockAAD, [ALERT_UUID]: alerts[1].getUuid() }, + { ...mockAAD, [ALERT_UUID]: alerts[2].getUuid() }, + ], + }, + ongoing: { count: 0, data: [] }, + recovered: { count: 0, data: [] }, + }; + alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts); + const actionWithUseAlertDataForTemplate: SanitizedRuleAction = { + id: '1', + group: 'default', + actionTypeId: 'test', + frequency: { summary: false, notifyWhen: 'onActiveAlert', throttle: null }, + params: { + foo: true, + contextVal: 'My {{context.value}} goes here', + stateVal: 'My {{state.value}} goes here', + alertVal: + 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', + }, + uuid: '111-111', + useAlertDataForTemplate: true, + }; + const scheduler = new PerAlertActionScheduler({ + ...getSchedulerContext(), + rule: { ...rule, actions: [rule.actions[0], actionWithUseAlertDataForTemplate] }, + }); + const executables = await scheduler.generateExecutables({ + alerts, + throttledSummaryActions: {}, + }); + + expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(1); + expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({ + excludedAlertInstanceIds: [], + executionUuid: defaultSchedulerContext.executionId, + ruleId: '1', + spaceId: 'test1', + }); + + expect(executables).toHaveLength(4); + + expect(executables).toEqual([ + { action: rule.actions[0], alert: alerts['1'] }, + { action: rule.actions[0], alert: alerts['2'] }, + { action: actionWithUseAlertDataForTemplate, alert: alerts['1'] }, + { action: actionWithUseAlertDataForTemplate, alert: alerts['2'] }, + ]); + }); + + test('should query for summarized alerts if useAlertDataForTemplate is true and action has throttle interval', async () => { + alertsClient.getProcessedAlerts.mockReturnValue(alerts); + const summarizedAlerts = { + new: { + count: 1, + data: [ + { ...mockAAD, [ALERT_UUID]: alerts[1].getUuid() }, + { ...mockAAD, [ALERT_UUID]: alerts[2].getUuid() }, + ], + }, + ongoing: { count: 0, data: [] }, + recovered: { count: 0, data: [] }, + }; + alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts); + const actionWithUseAlertDataForTemplate: SanitizedRuleAction = { + id: '1', + group: 'default', + actionTypeId: 'test', + frequency: { summary: false, notifyWhen: 'onThrottleInterval', throttle: '1h' }, + params: { + foo: true, + contextVal: 'My {{context.value}} goes here', + stateVal: 'My {{state.value}} goes here', + alertVal: + 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', + }, + uuid: '111-111', + useAlertDataForTemplate: true, + }; + const scheduler = new PerAlertActionScheduler({ + ...getSchedulerContext(), + rule: { ...rule, actions: [rule.actions[0], actionWithUseAlertDataForTemplate] }, + }); + const executables = await scheduler.generateExecutables({ + alerts, + throttledSummaryActions: {}, + }); + + expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(1); + expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({ + excludedAlertInstanceIds: [], + ruleId: '1', + spaceId: 'test1', + start: new Date('1969-12-31T23:00:00.000Z'), + end: new Date('1970-01-01T00:00:00.000Z'), + }); + + expect(executables).toHaveLength(4); + + expect(executables).toEqual([ + { action: rule.actions[0], alert: alerts['1'] }, + { action: rule.actions[0], alert: alerts['2'] }, + { action: actionWithUseAlertDataForTemplate, alert: alerts['1'] }, + { action: actionWithUseAlertDataForTemplate, alert: alerts['2'] }, + ]); + }); + + test('should query for summarized alerts if action has alertsFilter', async () => { + alertsClient.getProcessedAlerts.mockReturnValue(alerts); + const summarizedAlerts = { + new: { + count: 1, + data: [ + { ...mockAAD, [ALERT_UUID]: alerts[1].getUuid() }, + { ...mockAAD, [ALERT_UUID]: alerts[2].getUuid() }, + ], + }, + ongoing: { count: 0, data: [] }, + recovered: { count: 0, data: [] }, + }; + alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts); + const actionWithAlertsFilter: SanitizedRuleAction = { + id: '1', + group: 'default', + actionTypeId: 'test', + frequency: { summary: false, notifyWhen: 'onActiveAlert', throttle: null }, + params: { + foo: true, + contextVal: 'My {{context.value}} goes here', + stateVal: 'My {{state.value}} goes here', + alertVal: + 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', + }, + uuid: '111-111', + alertsFilter: { query: { kql: 'kibana.alert.rule.name:foo', filters: [] } }, + }; + const scheduler = new PerAlertActionScheduler({ + ...getSchedulerContext(), + rule: { ...rule, actions: [rule.actions[0], actionWithAlertsFilter] }, + }); + const executables = await scheduler.generateExecutables({ + alerts, + throttledSummaryActions: {}, + }); + + expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(1); + expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({ + excludedAlertInstanceIds: [], + executionUuid: defaultSchedulerContext.executionId, + ruleId: '1', + spaceId: 'test1', + alertsFilter: { query: { kql: 'kibana.alert.rule.name:foo', filters: [] } }, + }); + + expect(executables).toHaveLength(4); + + expect(executables).toEqual([ + { action: rule.actions[0], alert: alerts['1'] }, + { action: rule.actions[0], alert: alerts['2'] }, + { action: actionWithAlertsFilter, alert: alerts['1'] }, + { action: actionWithAlertsFilter, alert: alerts['2'] }, + ]); + }); + + test('should query for summarized alerts if action has alertsFilter and action has throttle interval', async () => { + alertsClient.getProcessedAlerts.mockReturnValue(alerts); + const summarizedAlerts = { + new: { + count: 1, + data: [ + { ...mockAAD, [ALERT_UUID]: alerts[1].getUuid() }, + { ...mockAAD, [ALERT_UUID]: alerts[2].getUuid() }, + ], + }, + ongoing: { count: 0, data: [] }, + recovered: { count: 0, data: [] }, + }; + alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts); + const actionWithAlertsFilter: SanitizedRuleAction = { + id: '1', + group: 'default', + actionTypeId: 'test', + frequency: { summary: false, notifyWhen: 'onThrottleInterval', throttle: '6h' }, + params: { + foo: true, + contextVal: 'My {{context.value}} goes here', + stateVal: 'My {{state.value}} goes here', + alertVal: + 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', + }, + uuid: '111-111', + alertsFilter: { query: { kql: 'kibana.alert.rule.name:foo', filters: [] } }, + }; + const scheduler = new PerAlertActionScheduler({ + ...getSchedulerContext(), + rule: { ...rule, actions: [rule.actions[0], actionWithAlertsFilter] }, + }); + const executables = await scheduler.generateExecutables({ + alerts, + throttledSummaryActions: {}, + }); + + expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(1); + expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({ + excludedAlertInstanceIds: [], + ruleId: '1', + spaceId: 'test1', + alertsFilter: { query: { kql: 'kibana.alert.rule.name:foo', filters: [] } }, + start: new Date('1969-12-31T18:00:00.000Z'), + end: new Date('1970-01-01T00:00:00.000Z'), + }); + + expect(executables).toHaveLength(4); + + expect(executables).toEqual([ + { action: rule.actions[0], alert: alerts['1'] }, + { action: rule.actions[0], alert: alerts['2'] }, + { action: actionWithAlertsFilter, alert: alerts['1'] }, + { action: actionWithAlertsFilter, alert: alerts['2'] }, + ]); + }); + + test('should skip generating executable if alert does not match any alerts in summarized alerts', async () => { + alertsClient.getProcessedAlerts.mockReturnValue(alerts); + const summarizedAlerts = { + new: { + count: 1, + data: [ + { ...mockAAD, [ALERT_UUID]: alerts[1].getUuid() }, + { ...mockAAD, [ALERT_UUID]: 'uuid-not-a-match' }, + ], + }, + ongoing: { count: 0, data: [] }, + recovered: { count: 0, data: [] }, + }; + alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts); + const actionWithAlertsFilter: SanitizedRuleAction = { + id: '1', + group: 'default', + actionTypeId: 'test', + frequency: { summary: false, notifyWhen: 'onActiveAlert', throttle: null }, + params: { + foo: true, + contextVal: 'My {{context.value}} goes here', + stateVal: 'My {{state.value}} goes here', + alertVal: + 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', + }, + uuid: '111-111', + alertsFilter: { query: { kql: 'kibana.alert.rule.name:foo', filters: [] } }, + }; + const scheduler = new PerAlertActionScheduler({ + ...getSchedulerContext(), + rule: { ...rule, actions: [rule.actions[0], actionWithAlertsFilter] }, + }); + const executables = await scheduler.generateExecutables({ + alerts, + throttledSummaryActions: {}, + }); + + expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(1); + expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({ + excludedAlertInstanceIds: [], + executionUuid: defaultSchedulerContext.executionId, + ruleId: '1', + spaceId: 'test1', + alertsFilter: { query: { kql: 'kibana.alert.rule.name:foo', filters: [] } }, + }); + + expect(executables).toHaveLength(3); + + expect(executables).toEqual([ + { action: rule.actions[0], alert: alerts['1'] }, + { action: rule.actions[0], alert: alerts['2'] }, + { action: actionWithAlertsFilter, alert: alerts['1'] }, + ]); + }); + + test('should set alerts as data', async () => { + alertsClient.getProcessedAlerts.mockReturnValue(alerts); + const summarizedAlerts = { + new: { + count: 1, + data: [ + { ...mockAAD, _id: alerts[1].getUuid(), [ALERT_UUID]: alerts[1].getUuid() }, + { ...mockAAD, _id: alerts[2].getUuid(), [ALERT_UUID]: alerts[2].getUuid() }, + ], + }, + ongoing: { count: 0, data: [] }, + recovered: { count: 0, data: [] }, + }; + alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts); + const actionWithAlertsFilter: SanitizedRuleAction = { + id: '1', + group: 'default', + actionTypeId: 'test', + frequency: { summary: false, notifyWhen: 'onActiveAlert', throttle: null }, + params: { + foo: true, + contextVal: 'My {{context.value}} goes here', + stateVal: 'My {{state.value}} goes here', + alertVal: + 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', + }, + uuid: '111-111', + alertsFilter: { query: { kql: 'kibana.alert.rule.name:foo', filters: [] } }, + }; + const scheduler = new PerAlertActionScheduler({ + ...getSchedulerContext(), + rule: { ...rule, actions: [rule.actions[0], actionWithAlertsFilter] }, + }); + const executables = await scheduler.generateExecutables({ + alerts, + throttledSummaryActions: {}, + }); + + expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(1); + expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({ + excludedAlertInstanceIds: [], + executionUuid: defaultSchedulerContext.executionId, + ruleId: '1', + spaceId: 'test1', + alertsFilter: { query: { kql: 'kibana.alert.rule.name:foo', filters: [] } }, + }); + + expect(executables).toHaveLength(4); + + expect(alerts['1'].getAlertAsData()).not.toBeUndefined(); + expect(alerts['2'].getAlertAsData()).not.toBeUndefined(); + + expect(executables).toEqual([ + { action: rule.actions[0], alert: alerts['1'] }, + { action: rule.actions[0], alert: alerts['2'] }, + { action: actionWithAlertsFilter, alert: alerts['1'] }, + { action: actionWithAlertsFilter, alert: alerts['2'] }, + ]); + }); + }); +}); diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/per_alert_action_scheduler.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/per_alert_action_scheduler.ts new file mode 100644 index 00000000000000..602d3c31688c10 --- /dev/null +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/per_alert_action_scheduler.ts @@ -0,0 +1,264 @@ +/* + * 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 { AlertInstanceState, AlertInstanceContext } from '@kbn/alerting-state-types'; +import { RuleAction, RuleNotifyWhen, RuleTypeParams } from '@kbn/alerting-types'; +import { compact } from 'lodash'; +import { RuleTypeState, RuleAlertData, parseDuration } from '../../../../common'; +import { GetSummarizedAlertsParams } from '../../../alerts_client/types'; +import { AlertHit } from '../../../types'; +import { Alert } from '../../../alert'; +import { getSummarizedAlerts } from '../get_summarized_alerts'; +import { + generateActionHash, + isActionOnInterval, + isSummaryAction, + logNumberOfFilteredAlerts, +} from '../rule_action_helper'; +import { + ActionSchedulerOptions, + Executable, + GenerateExecutablesOpts, + IActionScheduler, +} from '../types'; + +enum Reasons { + MUTED = 'muted', + THROTTLED = 'throttled', + ACTION_GROUP_NOT_CHANGED = 'actionGroupHasNotChanged', +} + +export class PerAlertActionScheduler< + Params extends RuleTypeParams, + ExtractedParams extends RuleTypeParams, + RuleState extends RuleTypeState, + State extends AlertInstanceState, + Context extends AlertInstanceContext, + ActionGroupIds extends string, + RecoveryActionGroupId extends string, + AlertData extends RuleAlertData +> implements IActionScheduler +{ + private actions: RuleAction[] = []; + private mutedAlertIdsSet: Set = new Set(); + private ruleTypeActionGroups?: Map; + private skippedAlerts: { [key: string]: { reason: string } } = {}; + + constructor( + private readonly context: ActionSchedulerOptions< + Params, + ExtractedParams, + RuleState, + State, + Context, + ActionGroupIds, + RecoveryActionGroupId, + AlertData + > + ) { + this.ruleTypeActionGroups = new Map( + context.ruleType.actionGroups.map((actionGroup) => [actionGroup.id, actionGroup.name]) + ); + this.mutedAlertIdsSet = new Set(context.rule.mutedInstanceIds); + + const canGetSummarizedAlerts = + !!context.ruleType.alerts && !!context.alertsClient.getSummarizedAlerts; + + // filter for per-alert actions; if the action has an alertsFilter, check that + // rule type supports summarized alerts and filter out if not + this.actions = compact( + (context.rule.actions ?? []) + .filter((action) => !isSummaryAction(action)) + .map((action) => { + if (!canGetSummarizedAlerts && action.alertsFilter) { + this.context.logger.error( + `Skipping action "${action.id}" for rule "${this.context.rule.id}" because the rule type "${this.context.ruleType.name}" does not support alert-as-data.` + ); + return null; + } + + return action; + }) + ); + } + + public get priority(): number { + return 2; + } + + public async generateExecutables({ + alerts, + }: GenerateExecutablesOpts): Promise< + Array> + > { + const executables = []; + + const alertsArray = Object.entries(alerts); + for (const action of this.actions) { + let summarizedAlerts = null; + + if (action.useAlertDataForTemplate || action.alertsFilter) { + const optionsBase = { + spaceId: this.context.taskInstance.params.spaceId, + ruleId: this.context.taskInstance.params.alertId, + excludedAlertInstanceIds: this.context.rule.mutedInstanceIds, + alertsFilter: action.alertsFilter, + }; + + let options: GetSummarizedAlertsParams; + if (isActionOnInterval(action)) { + const throttleMills = parseDuration(action.frequency!.throttle!); + const start = new Date(Date.now() - throttleMills); + options = { ...optionsBase, start, end: new Date() }; + } else { + options = { ...optionsBase, executionUuid: this.context.executionId }; + } + summarizedAlerts = await getSummarizedAlerts({ + queryOptions: options, + alertsClient: this.context.alertsClient, + }); + + logNumberOfFilteredAlerts({ + logger: this.context.logger, + numberOfAlerts: Object.entries(alerts).length, + numberOfSummarizedAlerts: summarizedAlerts.all.count, + action, + }); + } + + for (const [alertId, alert] of alertsArray) { + const alertMaintenanceWindowIds = alert.getMaintenanceWindowIds(); + if (alertMaintenanceWindowIds.length !== 0) { + this.context.logger.debug( + `no scheduling of summary actions "${action.id}" for rule "${ + this.context.taskInstance.params.alertId + }": has active maintenance windows ${alertMaintenanceWindowIds.join(', ')}.` + ); + continue; + } + + if (alert.isFilteredOut(summarizedAlerts)) { + continue; + } + + const actionGroup = + alert.getScheduledActionOptions()?.actionGroup || + this.context.ruleType.recoveryActionGroup.id; + + if (!this.ruleTypeActionGroups!.has(actionGroup)) { + this.context.logger.error( + `Invalid action group "${actionGroup}" for rule "${this.context.ruleType.id}".` + ); + continue; + } + + // only actions with notifyWhen set to "on status change" should return + // notifications for flapping pending recovered alerts + if ( + alert.getPendingRecoveredCount() > 0 && + action?.frequency?.notifyWhen !== RuleNotifyWhen.CHANGE + ) { + continue; + } + + if (summarizedAlerts) { + const alertAsData = summarizedAlerts.all.data.find( + (alertHit: AlertHit) => alertHit._id === alert.getUuid() + ); + if (alertAsData) { + alert.setAlertAsData(alertAsData); + } + } + + if (action.group === actionGroup && !this.isAlertMuted(alertId)) { + if ( + this.isRecoveredAlert(action.group) || + this.isExecutableActiveAlert({ alert, action }) + ) { + executables.push({ action, alert }); + } + } + } + } + + return executables; + } + + private isAlertMuted(alertId: string) { + const muted = this.mutedAlertIdsSet.has(alertId); + if (muted) { + if ( + !this.skippedAlerts[alertId] || + (this.skippedAlerts[alertId] && this.skippedAlerts[alertId].reason !== Reasons.MUTED) + ) { + this.context.logger.debug( + `skipping scheduling of actions for '${alertId}' in rule ${this.context.ruleLabel}: rule is muted` + ); + } + this.skippedAlerts[alertId] = { reason: Reasons.MUTED }; + return true; + } + return false; + } + + private isExecutableActiveAlert({ + alert, + action, + }: { + alert: Alert; + action: RuleAction; + }) { + const alertId = alert.getId(); + const { + context: { rule, logger, ruleLabel }, + } = this; + const notifyWhen = action.frequency?.notifyWhen || rule.notifyWhen; + + if (notifyWhen === 'onActionGroupChange' && !alert.scheduledActionGroupHasChanged()) { + if ( + !this.skippedAlerts[alertId] || + (this.skippedAlerts[alertId] && + this.skippedAlerts[alertId].reason !== Reasons.ACTION_GROUP_NOT_CHANGED) + ) { + logger.debug( + `skipping scheduling of actions for '${alertId}' in rule ${ruleLabel}: alert is active but action group has not changed` + ); + } + this.skippedAlerts[alertId] = { reason: Reasons.ACTION_GROUP_NOT_CHANGED }; + return false; + } + + if (notifyWhen === 'onThrottleInterval') { + const throttled = action.frequency?.throttle + ? alert.isThrottled({ + throttle: action.frequency.throttle ?? null, + actionHash: generateActionHash(action), // generateActionHash must be removed once all the hash identifiers removed from the task state + uuid: action.uuid, + }) + : alert.isThrottled({ throttle: rule.throttle ?? null }); + + if (throttled) { + if ( + !this.skippedAlerts[alertId] || + (this.skippedAlerts[alertId] && this.skippedAlerts[alertId].reason !== Reasons.THROTTLED) + ) { + logger.debug( + `skipping scheduling of actions for '${alertId}' in rule ${ruleLabel}: rule is throttled` + ); + } + this.skippedAlerts[alertId] = { reason: Reasons.THROTTLED }; + return false; + } + } + + return alert.hasScheduledActions(); + } + + private isRecoveredAlert(actionGroup: string) { + return actionGroup === this.context.ruleType.recoveryActionGroup.id; + } +} diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/summary_action_scheduler.test.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/summary_action_scheduler.test.ts new file mode 100644 index 00000000000000..600dd0e1951d5b --- /dev/null +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/summary_action_scheduler.test.ts @@ -0,0 +1,468 @@ +/* + * 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 sinon from 'sinon'; +import { actionsClientMock, actionsMock } from '@kbn/actions-plugin/server/mocks'; +import { loggingSystemMock } from '@kbn/core-logging-server-mocks'; +import { alertsClientMock } from '../../../alerts_client/alerts_client.mock'; +import { alertingEventLoggerMock } from '../../../lib/alerting_event_logger/alerting_event_logger.mock'; +import { RuleRunMetricsStore } from '../../../lib/rule_run_metrics_store'; +import { mockAAD } from '../../fixtures'; +import { SummaryActionScheduler } from './summary_action_scheduler'; +import { getRule, getRuleType, getDefaultSchedulerContext, generateAlert } from '../test_fixtures'; +import { RuleAction } from '@kbn/alerting-types'; +import { ALERT_UUID } from '@kbn/rule-data-utils'; +import { + getErrorSource, + TaskErrorSource, +} from '@kbn/task-manager-plugin/server/task_running/errors'; + +const alertingEventLogger = alertingEventLoggerMock.create(); +const actionsClient = actionsClientMock.create(); +const alertsClient = alertsClientMock.create(); +const mockActionsPlugin = actionsMock.createStart(); +const logger = loggingSystemMock.create().get(); + +let ruleRunMetricsStore: RuleRunMetricsStore; +const rule = getRule({ + actions: [ + { + id: '1', + group: 'default', + actionTypeId: 'test', + frequency: { summary: false, notifyWhen: 'onActiveAlert', throttle: null }, + params: { + foo: true, + contextVal: 'My {{context.value}} goes here', + stateVal: 'My {{state.value}} goes here', + alertVal: + 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', + }, + uuid: '111-111', + }, + { + id: '2', + group: 'default', + actionTypeId: 'test', + frequency: { summary: true, notifyWhen: 'onActiveAlert', throttle: null }, + params: { + foo: true, + contextVal: 'My {{context.value}} goes here', + stateVal: 'My {{state.value}} goes here', + alertVal: + 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', + }, + uuid: '222-222', + }, + { + id: '3', + group: 'default', + actionTypeId: 'test', + frequency: { summary: true, notifyWhen: 'onActiveAlert', throttle: null }, + params: { + foo: true, + contextVal: 'My {{context.value}} goes here', + stateVal: 'My {{state.value}} goes here', + alertVal: + 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', + }, + uuid: '333-333', + }, + ], +}); +const ruleType = getRuleType(); +const defaultSchedulerContext = getDefaultSchedulerContext( + logger, + mockActionsPlugin, + alertingEventLogger, + actionsClient, + alertsClient +); + +// @ts-ignore +const getSchedulerContext = (params = {}) => { + return { ...defaultSchedulerContext, rule, ...params, ruleRunMetricsStore }; +}; + +let clock: sinon.SinonFakeTimers; + +describe('Summary Action Scheduler', () => { + beforeAll(() => { + clock = sinon.useFakeTimers(); + }); + + beforeEach(() => { + jest.resetAllMocks(); + mockActionsPlugin.isActionTypeEnabled.mockReturnValue(true); + mockActionsPlugin.isActionExecutable.mockReturnValue(true); + mockActionsPlugin.getActionsClientWithRequest.mockResolvedValue(actionsClient); + ruleRunMetricsStore = new RuleRunMetricsStore(); + }); + + afterAll(() => { + clock.restore(); + }); + + test('should initialize with only summary actions', () => { + const scheduler = new SummaryActionScheduler(getSchedulerContext()); + + // @ts-expect-error private variable + expect(scheduler.actions).toHaveLength(2); + // @ts-expect-error private variable + expect(scheduler.actions).toEqual([rule.actions[1], rule.actions[2]]); + expect(logger.error).not.toHaveBeenCalled(); + }); + + test('should log if rule type does not support summarized alerts and not initialize any actions', () => { + const scheduler = new SummaryActionScheduler( + getSchedulerContext({ ruleType: { ...ruleType, alerts: undefined } }) + ); + + // @ts-expect-error private variable + expect(scheduler.actions).toHaveLength(0); + expect(logger.error).toHaveBeenCalledTimes(2); + expect(logger.error).toHaveBeenNthCalledWith( + 1, + `Skipping action \"2\" for rule \"1\" because the rule type \"Test\" does not support alert-as-data.` + ); + expect(logger.error).toHaveBeenNthCalledWith( + 2, + `Skipping action \"3\" for rule \"1\" because the rule type \"Test\" does not support alert-as-data.` + ); + }); + + describe('generateExecutables', () => { + const newAlert1 = generateAlert({ id: 1 }); + const newAlert2 = generateAlert({ id: 2 }); + const alerts = { ...newAlert1, ...newAlert2 }; + + const summaryActionWithAlertFilter: RuleAction = { + id: '2', + group: 'default', + actionTypeId: 'test', + frequency: { + summary: true, + notifyWhen: 'onActiveAlert', + throttle: null, + }, + params: { + foo: true, + contextVal: 'My {{context.value}} goes here', + stateVal: 'My {{state.value}} goes here', + alertVal: + 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', + }, + alertsFilter: { query: { kql: 'kibana.alert.rule.name:foo', dsl: '{}', filters: [] } }, + uuid: '222-222', + }; + + const summaryActionWithThrottle: RuleAction = { + id: '2', + group: 'default', + actionTypeId: 'test', + frequency: { + summary: true, + notifyWhen: 'onThrottleInterval', + throttle: '1d', + }, + params: { + foo: true, + contextVal: 'My {{context.value}} goes here', + stateVal: 'My {{state.value}} goes here', + alertVal: + 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', + }, + uuid: '222-222', + }; + + test('should generate executable for summary action when summary action is per rule run', async () => { + alertsClient.getProcessedAlerts.mockReturnValue(alerts); + const summarizedAlerts = { + new: { count: 2, data: [mockAAD, mockAAD] }, + ongoing: { count: 0, data: [] }, + recovered: { count: 0, data: [] }, + }; + alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts); + + const scheduler = new SummaryActionScheduler(getSchedulerContext()); + const executables = await scheduler.generateExecutables({ + alerts, + throttledSummaryActions: {}, + }); + + expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(2); + expect(alertsClient.getSummarizedAlerts).toHaveBeenNthCalledWith(1, { + excludedAlertInstanceIds: [], + executionUuid: defaultSchedulerContext.executionId, + ruleId: '1', + spaceId: 'test1', + }); + expect(alertsClient.getSummarizedAlerts).toHaveBeenNthCalledWith(2, { + excludedAlertInstanceIds: [], + executionUuid: defaultSchedulerContext.executionId, + ruleId: '1', + spaceId: 'test1', + }); + expect(logger.debug).not.toHaveBeenCalled(); + + expect(executables).toHaveLength(2); + + const finalSummary = { ...summarizedAlerts, all: { count: 2, data: [mockAAD, mockAAD] } }; + expect(executables).toEqual([ + { action: rule.actions[1], summarizedAlerts: finalSummary }, + { action: rule.actions[2], summarizedAlerts: finalSummary }, + ]); + }); + + test('should generate executable for summary action when summary action has alertsFilter', async () => { + alertsClient.getProcessedAlerts.mockReturnValue(alerts); + const summarizedAlerts = { + new: { count: 2, data: [mockAAD, mockAAD] }, + ongoing: { count: 0, data: [] }, + recovered: { count: 0, data: [] }, + }; + alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts); + + const scheduler = new SummaryActionScheduler({ + ...getSchedulerContext(), + rule: { ...rule, actions: [summaryActionWithAlertFilter] }, + }); + + const executables = await scheduler.generateExecutables({ + alerts, + throttledSummaryActions: {}, + }); + + expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(1); + expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({ + excludedAlertInstanceIds: [], + executionUuid: defaultSchedulerContext.executionId, + ruleId: '1', + spaceId: 'test1', + alertsFilter: { query: { kql: 'kibana.alert.rule.name:foo', dsl: '{}', filters: [] } }, + }); + expect(logger.debug).not.toHaveBeenCalled(); + + expect(executables).toHaveLength(1); + + const finalSummary = { ...summarizedAlerts, all: { count: 2, data: [mockAAD, mockAAD] } }; + expect(executables).toEqual([ + { action: summaryActionWithAlertFilter, summarizedAlerts: finalSummary }, + ]); + }); + + test('should generate executable for summary action when summary action is throttled with no throttle history', async () => { + alertsClient.getProcessedAlerts.mockReturnValue(alerts); + const summarizedAlerts = { + new: { count: 2, data: [mockAAD, mockAAD] }, + ongoing: { count: 0, data: [] }, + recovered: { count: 0, data: [] }, + }; + alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts); + + const scheduler = new SummaryActionScheduler({ + ...getSchedulerContext(), + rule: { ...rule, actions: [summaryActionWithThrottle] }, + }); + + const executables = await scheduler.generateExecutables({ + alerts, + throttledSummaryActions: {}, + }); + + expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(1); + expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({ + excludedAlertInstanceIds: [], + ruleId: '1', + spaceId: 'test1', + start: new Date('1969-12-31T00:00:00.000Z'), + end: new Date(), + }); + expect(logger.debug).not.toHaveBeenCalled(); + + expect(executables).toHaveLength(1); + + const finalSummary = { ...summarizedAlerts, all: { count: 2, data: [mockAAD, mockAAD] } }; + expect(executables).toEqual([ + { action: summaryActionWithThrottle, summarizedAlerts: finalSummary }, + ]); + }); + + test('should skip generating executable for summary action when summary action is throttled', async () => { + const scheduler = new SummaryActionScheduler({ + ...getSchedulerContext(), + rule: { ...rule, actions: [summaryActionWithThrottle] }, + }); + + const executables = await scheduler.generateExecutables({ + alerts, + throttledSummaryActions: { + '222-222': { date: '1969-12-31T13:00:00.000Z' }, + }, + }); + + expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled(); + expect(logger.debug).toHaveBeenCalledWith( + `skipping scheduling the action 'test:2', summary action is still being throttled` + ); + + expect(executables).toHaveLength(0); + }); + + test('should remove new alerts from summary if suppressed by maintenance window', async () => { + const newAlertWithMaintenanceWindow = generateAlert({ + id: 1, + maintenanceWindowIds: ['mw-1'], + }); + const alertsWithMaintenanceWindow = { ...newAlertWithMaintenanceWindow, ...newAlert2 }; + alertsClient.getProcessedAlerts.mockReturnValue(alertsWithMaintenanceWindow); + const newAADAlerts = [ + { ...mockAAD, [ALERT_UUID]: newAlertWithMaintenanceWindow[1].getUuid() }, + mockAAD, + ]; + const summarizedAlerts = { + new: { count: 2, data: newAADAlerts }, + ongoing: { count: 0, data: [] }, + recovered: { count: 0, data: [] }, + }; + alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts); + const scheduler = new SummaryActionScheduler(getSchedulerContext()); + + const executables = await scheduler.generateExecutables({ + alerts, + throttledSummaryActions: {}, + }); + + expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(2); + expect(alertsClient.getSummarizedAlerts).toHaveBeenNthCalledWith(1, { + excludedAlertInstanceIds: [], + executionUuid: defaultSchedulerContext.executionId, + ruleId: '1', + spaceId: 'test1', + }); + expect(alertsClient.getSummarizedAlerts).toHaveBeenNthCalledWith(2, { + excludedAlertInstanceIds: [], + executionUuid: defaultSchedulerContext.executionId, + ruleId: '1', + spaceId: 'test1', + }); + expect(logger.debug).toHaveBeenCalledTimes(2); + expect(logger.debug).toHaveBeenNthCalledWith( + 1, + `(1) alert has been filtered out for: test:222-222` + ); + expect(logger.debug).toHaveBeenNthCalledWith( + 2, + `(1) alert has been filtered out for: test:333-333` + ); + + expect(executables).toHaveLength(2); + + const finalSummary = { + all: { count: 1, data: [newAADAlerts[1]] }, + new: { count: 1, data: [newAADAlerts[1]] }, + ongoing: { count: 0, data: [] }, + recovered: { count: 0, data: [] }, + }; + expect(executables).toEqual([ + { action: rule.actions[1], summarizedAlerts: finalSummary }, + { action: rule.actions[2], summarizedAlerts: finalSummary }, + ]); + }); + + test('should generate executable for summary action and log when alerts have been filtered out by action condition', async () => { + alertsClient.getProcessedAlerts.mockReturnValue(alerts); + const summarizedAlerts = { + new: { count: 1, data: [mockAAD] }, + ongoing: { count: 0, data: [] }, + recovered: { count: 0, data: [] }, + }; + alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts); + + const scheduler = new SummaryActionScheduler({ + ...getSchedulerContext(), + rule: { ...rule, actions: [summaryActionWithAlertFilter] }, + }); + + const executables = await scheduler.generateExecutables({ + alerts, + throttledSummaryActions: {}, + }); + + expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(1); + expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({ + excludedAlertInstanceIds: [], + executionUuid: defaultSchedulerContext.executionId, + ruleId: '1', + spaceId: 'test1', + alertsFilter: { query: { kql: 'kibana.alert.rule.name:foo', dsl: '{}', filters: [] } }, + }); + expect(logger.debug).toHaveBeenCalledTimes(1); + expect(logger.debug).toHaveBeenCalledWith( + `(1) alert has been filtered out for: test:222-222` + ); + + expect(executables).toHaveLength(1); + + const finalSummary = { ...summarizedAlerts, all: { count: 1, data: [mockAAD] } }; + expect(executables).toEqual([ + { action: summaryActionWithAlertFilter, summarizedAlerts: finalSummary }, + ]); + }); + + test('should skip generating executable for summary action when no alerts found', async () => { + alertsClient.getProcessedAlerts.mockReturnValue(alerts); + const summarizedAlerts = { + new: { count: 0, data: [] }, + ongoing: { count: 0, data: [] }, + recovered: { count: 0, data: [] }, + }; + alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts); + + const scheduler = new SummaryActionScheduler({ + ...getSchedulerContext(), + rule: { ...rule, actions: [summaryActionWithThrottle] }, + }); + + const executables = await scheduler.generateExecutables({ + alerts, + throttledSummaryActions: {}, + }); + + expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(1); + expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({ + excludedAlertInstanceIds: [], + ruleId: '1', + spaceId: 'test1', + start: new Date('1969-12-31T00:00:00.000Z'), + end: new Date(), + }); + expect(logger.debug).not.toHaveBeenCalled(); + + expect(executables).toHaveLength(0); + }); + + test('should throw framework error if getSummarizedAlerts throws error', async () => { + alertsClient.getProcessedAlerts.mockReturnValue(alerts); + alertsClient.getSummarizedAlerts.mockImplementation(() => { + throw new Error('no alerts for you'); + }); + + const scheduler = new SummaryActionScheduler(getSchedulerContext()); + + try { + await scheduler.generateExecutables({ + alerts, + throttledSummaryActions: {}, + }); + } catch (err) { + expect(err.message).toEqual(`no alerts for you`); + expect(getErrorSource(err)).toBe(TaskErrorSource.FRAMEWORK); + } + }); + }); +}); diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/summary_action_scheduler.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/summary_action_scheduler.ts new file mode 100644 index 00000000000000..9b67c37e6216ec --- /dev/null +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/summary_action_scheduler.ts @@ -0,0 +1,127 @@ +/* + * 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 { AlertInstanceState, AlertInstanceContext } from '@kbn/alerting-state-types'; +import { RuleAction, RuleTypeParams } from '@kbn/alerting-types'; +import { compact } from 'lodash'; +import { RuleTypeState, RuleAlertData, parseDuration } from '../../../../common'; +import { GetSummarizedAlertsParams } from '../../../alerts_client/types'; +import { getSummarizedAlerts } from '../get_summarized_alerts'; +import { + isActionOnInterval, + isSummaryAction, + isSummaryActionThrottled, + logNumberOfFilteredAlerts, +} from '../rule_action_helper'; +import { + ActionSchedulerOptions, + Executable, + GenerateExecutablesOpts, + IActionScheduler, +} from '../types'; + +export class SummaryActionScheduler< + Params extends RuleTypeParams, + ExtractedParams extends RuleTypeParams, + RuleState extends RuleTypeState, + State extends AlertInstanceState, + Context extends AlertInstanceContext, + ActionGroupIds extends string, + RecoveryActionGroupId extends string, + AlertData extends RuleAlertData +> implements IActionScheduler +{ + private actions: RuleAction[] = []; + + constructor( + private readonly context: ActionSchedulerOptions< + Params, + ExtractedParams, + RuleState, + State, + Context, + ActionGroupIds, + RecoveryActionGroupId, + AlertData + > + ) { + const canGetSummarizedAlerts = + !!context.ruleType.alerts && !!context.alertsClient.getSummarizedAlerts; + + // filter for summary actions where the rule type supports summarized alerts + this.actions = compact( + (context.rule.actions ?? []) + .filter((action) => isSummaryAction(action)) + .map((action) => { + if (!canGetSummarizedAlerts) { + this.context.logger.error( + `Skipping action "${action.id}" for rule "${this.context.rule.id}" because the rule type "${this.context.ruleType.name}" does not support alert-as-data.` + ); + return null; + } + + return action; + }) + ); + } + + public get priority(): number { + return 0; + } + + public async generateExecutables({ + alerts, + throttledSummaryActions, + }: GenerateExecutablesOpts): Promise< + Array> + > { + const executables = []; + for (const action of this.actions) { + if ( + // if summary action is throttled, we won't send any notifications + !isSummaryActionThrottled({ action, throttledSummaryActions, logger: this.context.logger }) + ) { + const actionHasThrottleInterval = isActionOnInterval(action); + const optionsBase = { + spaceId: this.context.taskInstance.params.spaceId, + ruleId: this.context.taskInstance.params.alertId, + excludedAlertInstanceIds: this.context.rule.mutedInstanceIds, + alertsFilter: action.alertsFilter, + }; + + let options: GetSummarizedAlertsParams; + if (actionHasThrottleInterval) { + const throttleMills = parseDuration(action.frequency!.throttle!); + const start = new Date(Date.now() - throttleMills); + options = { ...optionsBase, start, end: new Date() }; + } else { + options = { ...optionsBase, executionUuid: this.context.executionId }; + } + + const summarizedAlerts = await getSummarizedAlerts({ + queryOptions: options, + alertsClient: this.context.alertsClient, + }); + + if (!actionHasThrottleInterval) { + logNumberOfFilteredAlerts({ + logger: this.context.logger, + numberOfAlerts: Object.entries(alerts).length, + numberOfSummarizedAlerts: summarizedAlerts.all.count, + action, + }); + } + + if (summarizedAlerts.all.count !== 0) { + executables.push({ action, summarizedAlerts }); + } + } + } + + return executables; + } +} diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/system_action_scheduler.test.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/system_action_scheduler.test.ts new file mode 100644 index 00000000000000..fd4db6ce346788 --- /dev/null +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/system_action_scheduler.test.ts @@ -0,0 +1,218 @@ +/* + * 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 sinon from 'sinon'; +import { actionsClientMock, actionsMock } from '@kbn/actions-plugin/server/mocks'; +import { loggingSystemMock } from '@kbn/core-logging-server-mocks'; +import { alertsClientMock } from '../../../alerts_client/alerts_client.mock'; +import { alertingEventLoggerMock } from '../../../lib/alerting_event_logger/alerting_event_logger.mock'; +import { RuleRunMetricsStore } from '../../../lib/rule_run_metrics_store'; +import { mockAAD } from '../../fixtures'; +import { getRule, getRuleType, getDefaultSchedulerContext, generateAlert } from '../test_fixtures'; +import { SystemActionScheduler } from './system_action_scheduler'; +import { ALERT_UUID } from '@kbn/rule-data-utils'; +import { + getErrorSource, + TaskErrorSource, +} from '@kbn/task-manager-plugin/server/task_running/errors'; + +const alertingEventLogger = alertingEventLoggerMock.create(); +const actionsClient = actionsClientMock.create(); +const alertsClient = alertsClientMock.create(); +const mockActionsPlugin = actionsMock.createStart(); +const logger = loggingSystemMock.create().get(); + +let ruleRunMetricsStore: RuleRunMetricsStore; +const rule = getRule({ + systemActions: [ + { + id: '1', + actionTypeId: '.test-system-action', + params: { myParams: 'test' }, + uui: 'test', + }, + ], +}); +const ruleType = getRuleType(); +const defaultSchedulerContext = getDefaultSchedulerContext( + logger, + mockActionsPlugin, + alertingEventLogger, + actionsClient, + alertsClient +); + +// @ts-ignore +const getSchedulerContext = (params = {}) => { + return { ...defaultSchedulerContext, rule, ...params, ruleRunMetricsStore }; +}; + +let clock: sinon.SinonFakeTimers; + +describe('System Action Scheduler', () => { + beforeAll(() => { + clock = sinon.useFakeTimers(); + }); + + beforeEach(() => { + jest.resetAllMocks(); + mockActionsPlugin.isActionTypeEnabled.mockReturnValue(true); + mockActionsPlugin.isActionExecutable.mockReturnValue(true); + mockActionsPlugin.getActionsClientWithRequest.mockResolvedValue(actionsClient); + ruleRunMetricsStore = new RuleRunMetricsStore(); + }); + + afterAll(() => { + clock.restore(); + }); + + test('should initialize with only system actions', () => { + const scheduler = new SystemActionScheduler(getSchedulerContext()); + + // @ts-expect-error private variable + expect(scheduler.actions).toHaveLength(1); + // @ts-expect-error private variable + expect(scheduler.actions).toEqual(rule.systemActions); + }); + + test('should not initialize any system actions if rule type does not support summarized alerts', () => { + const scheduler = new SystemActionScheduler( + getSchedulerContext({ ruleType: { ...ruleType, alerts: undefined } }) + ); + + // @ts-expect-error private variable + expect(scheduler.actions).toHaveLength(0); + }); + + describe('generateExecutables', () => { + const newAlert1 = generateAlert({ id: 1 }); + const newAlert2 = generateAlert({ id: 2 }); + const alerts = { ...newAlert1, ...newAlert2 }; + + test('should generate executable for each system action', async () => { + alertsClient.getProcessedAlerts.mockReturnValue(alerts); + const summarizedAlerts = { + new: { count: 2, data: [mockAAD, mockAAD] }, + ongoing: { count: 0, data: [] }, + recovered: { count: 0, data: [] }, + }; + alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts); + + const scheduler = new SystemActionScheduler(getSchedulerContext()); + const executables = await scheduler.generateExecutables({ + alerts, + throttledSummaryActions: {}, + }); + + expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(1); + expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({ + excludedAlertInstanceIds: [], + executionUuid: defaultSchedulerContext.executionId, + ruleId: '1', + spaceId: 'test1', + }); + + expect(executables).toHaveLength(1); + + const finalSummary = { ...summarizedAlerts, all: { count: 2, data: [mockAAD, mockAAD] } }; + expect(executables).toEqual([ + { action: rule.systemActions?.[0], summarizedAlerts: finalSummary }, + ]); + }); + + test('should remove new alerts from summary if suppressed by maintenance window', async () => { + const newAlertWithMaintenanceWindow = generateAlert({ + id: 1, + maintenanceWindowIds: ['mw-1'], + }); + const alertsWithMaintenanceWindow = { ...newAlertWithMaintenanceWindow, ...newAlert2 }; + alertsClient.getProcessedAlerts.mockReturnValue(alertsWithMaintenanceWindow); + const newAADAlerts = [ + { ...mockAAD, [ALERT_UUID]: newAlertWithMaintenanceWindow[1].getUuid() }, + mockAAD, + ]; + const summarizedAlerts = { + new: { count: 2, data: newAADAlerts }, + ongoing: { count: 0, data: [] }, + recovered: { count: 0, data: [] }, + }; + alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts); + const scheduler = new SystemActionScheduler(getSchedulerContext()); + + const executables = await scheduler.generateExecutables({ + alerts, + throttledSummaryActions: {}, + }); + + expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(1); + expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({ + excludedAlertInstanceIds: [], + executionUuid: defaultSchedulerContext.executionId, + ruleId: '1', + spaceId: 'test1', + }); + + expect(executables).toHaveLength(1); + + const finalSummary = { + all: { count: 1, data: [newAADAlerts[1]] }, + new: { count: 1, data: [newAADAlerts[1]] }, + ongoing: { count: 0, data: [] }, + recovered: { count: 0, data: [] }, + }; + expect(executables).toEqual([ + { action: rule.systemActions?.[0], summarizedAlerts: finalSummary }, + ]); + }); + + test('should skip generating executable for summary action when no alerts found', async () => { + alertsClient.getProcessedAlerts.mockReturnValue(alerts); + const summarizedAlerts = { + new: { count: 0, data: [] }, + ongoing: { count: 0, data: [] }, + recovered: { count: 0, data: [] }, + }; + alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts); + + const scheduler = new SystemActionScheduler(getSchedulerContext()); + + const executables = await scheduler.generateExecutables({ + alerts, + throttledSummaryActions: {}, + }); + + expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(1); + expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({ + excludedAlertInstanceIds: [], + executionUuid: defaultSchedulerContext.executionId, + ruleId: '1', + spaceId: 'test1', + }); + + expect(executables).toHaveLength(0); + }); + + test('should throw framework error if getSummarizedAlerts throws error', async () => { + alertsClient.getProcessedAlerts.mockReturnValue(alerts); + alertsClient.getSummarizedAlerts.mockImplementation(() => { + throw new Error('no alerts for you'); + }); + + const scheduler = new SystemActionScheduler(getSchedulerContext()); + + try { + await scheduler.generateExecutables({ + alerts, + throttledSummaryActions: {}, + }); + } catch (err) { + expect(err.message).toEqual(`no alerts for you`); + expect(getErrorSource(err)).toBe(TaskErrorSource.FRAMEWORK); + } + }); + }); +}); diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/system_action_scheduler.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/system_action_scheduler.ts new file mode 100644 index 00000000000000..b923baf8fbf388 --- /dev/null +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/system_action_scheduler.ts @@ -0,0 +1,80 @@ +/* + * 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 { AlertInstanceState, AlertInstanceContext } from '@kbn/alerting-state-types'; +import { RuleSystemAction, RuleTypeParams } from '@kbn/alerting-types'; +import { RuleTypeState, RuleAlertData } from '../../../../common'; +import { GetSummarizedAlertsParams } from '../../../alerts_client/types'; +import { getSummarizedAlerts } from '../get_summarized_alerts'; +import { + ActionSchedulerOptions, + Executable, + GenerateExecutablesOpts, + IActionScheduler, +} from '../types'; + +export class SystemActionScheduler< + Params extends RuleTypeParams, + ExtractedParams extends RuleTypeParams, + RuleState extends RuleTypeState, + State extends AlertInstanceState, + Context extends AlertInstanceContext, + ActionGroupIds extends string, + RecoveryActionGroupId extends string, + AlertData extends RuleAlertData +> implements IActionScheduler +{ + private actions: RuleSystemAction[] = []; + + constructor( + private readonly context: ActionSchedulerOptions< + Params, + ExtractedParams, + RuleState, + State, + Context, + ActionGroupIds, + RecoveryActionGroupId, + AlertData + > + ) { + const canGetSummarizedAlerts = + !!context.ruleType.alerts && !!context.alertsClient.getSummarizedAlerts; + + // only process system actions when rule type supports summarized alerts + this.actions = canGetSummarizedAlerts ? context.rule.systemActions ?? [] : []; + } + + public get priority(): number { + return 1; + } + + public async generateExecutables( + _: GenerateExecutablesOpts + ): Promise>> { + const executables = []; + for (const action of this.actions) { + const options: GetSummarizedAlertsParams = { + spaceId: this.context.taskInstance.params.spaceId, + ruleId: this.context.taskInstance.params.alertId, + excludedAlertInstanceIds: this.context.rule.mutedInstanceIds, + executionUuid: this.context.executionId, + }; + + const summarizedAlerts = await getSummarizedAlerts({ + queryOptions: options, + alertsClient: this.context.alertsClient, + }); + + if (summarizedAlerts && summarizedAlerts.all.count !== 0) { + executables.push({ action, summarizedAlerts }); + } + } + + return executables; + } +} diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/test_fixtures.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/test_fixtures.ts new file mode 100644 index 00000000000000..5d56e03d0a462c --- /dev/null +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/test_fixtures.ts @@ -0,0 +1,208 @@ +/* + * 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 type { Logger } from '@kbn/core/server'; +import { + AlertInstanceState, + AlertInstanceContext, + ThrottledActions, +} from '@kbn/alerting-state-types'; +import { RuleTypeParams, SanitizedRule } from '@kbn/alerting-types'; +import { schema } from '@kbn/config-schema'; +import { KibanaRequest } from '@kbn/core-http-server'; +import { ConcreteTaskInstance } from '@kbn/task-manager-plugin/server'; +import { ActionsClient, PluginStartContract } from '@kbn/actions-plugin/server'; +import { PublicMethodsOf } from '@kbn/utility-types'; +import { RuleAlertData, RuleTypeState } from '../../../common'; +import { ConnectorAdapterRegistry } from '../../connector_adapters/connector_adapter_registry'; +import { NormalizedRuleType } from '../../rule_type_registry'; +import { TaskRunnerContext } from '../types'; +import { AlertingEventLogger } from '../../lib/alerting_event_logger/alerting_event_logger'; +import { Alert } from '../../alert'; + +const apiKey = Buffer.from('123:abc').toString('base64'); + +type ActiveActionGroup = 'default' | 'other-group'; +export const generateAlert = ({ + id, + group = 'default', + context, + state, + scheduleActions = true, + throttledActions = {}, + lastScheduledActionsGroup = 'default', + maintenanceWindowIds, + pendingRecoveredCount, + activeCount, +}: { + id: number; + group?: ActiveActionGroup | 'recovered'; + context?: AlertInstanceContext; + state?: AlertInstanceState; + scheduleActions?: boolean; + throttledActions?: ThrottledActions; + lastScheduledActionsGroup?: string; + maintenanceWindowIds?: string[]; + pendingRecoveredCount?: number; + activeCount?: number; +}) => { + const alert = new Alert( + String(id), + { + state: state || { test: true }, + meta: { + maintenanceWindowIds, + lastScheduledActions: { + date: new Date().toISOString(), + group: lastScheduledActionsGroup, + actions: throttledActions, + }, + pendingRecoveredCount, + activeCount, + }, + } + ); + if (scheduleActions) { + alert.scheduleActions(group as ActiveActionGroup); + } + if (context) { + alert.setContext(context); + } + + return { [id]: alert }; +}; + +export const generateRecoveredAlert = ({ + id, + state, +}: { + id: number; + state?: AlertInstanceState; +}) => { + const alert = new Alert(String(id), { + state: state || { test: true }, + meta: { + lastScheduledActions: { + date: new Date().toISOString(), + group: 'recovered', + actions: {}, + }, + }, + }); + return { [id]: alert }; +}; + +export const getRule = (overrides = {}) => + ({ + id: '1', + name: 'name-of-alert', + tags: ['tag-A', 'tag-B'], + mutedInstanceIds: [], + params: { + foo: true, + contextVal: 'My other {{context.value}} goes here', + stateVal: 'My other {{state.value}} goes here', + }, + schedule: { interval: '1m' }, + notifyWhen: 'onActiveAlert', + actions: [ + { + id: '1', + group: 'default', + actionTypeId: 'test', + params: { + foo: true, + contextVal: 'My {{context.value}} goes here', + stateVal: 'My {{state.value}} goes here', + alertVal: + 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', + }, + uuid: '111-111', + }, + ], + consumer: 'test-consumer', + ...overrides, + } as unknown as SanitizedRule); + +export const getRuleType = (): NormalizedRuleType< + RuleTypeParams, + RuleTypeParams, + RuleTypeState, + AlertInstanceState, + AlertInstanceContext, + 'default' | 'other-group', + 'recovered', + {} +> => ({ + id: 'test', + name: 'Test', + actionGroups: [ + { id: 'default', name: 'Default' }, + { id: 'recovered', name: 'Recovered' }, + { id: 'other-group', name: 'Other Group' }, + ], + defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', + isExportable: true, + recoveryActionGroup: { + id: 'recovered', + name: 'Recovered', + }, + executor: jest.fn(), + category: 'test', + producer: 'alerts', + validate: { + params: schema.any(), + }, + alerts: { + context: 'context', + mappings: { fieldMap: { field: { type: 'fieldType', required: false } } }, + }, + autoRecoverAlerts: false, + validLegacyConsumers: [], +}); + +export const getDefaultSchedulerContext = < + State extends AlertInstanceState, + Context extends AlertInstanceContext, + ActionGroupIds extends string, + RecoveryActionGroupId extends string, + AlertData extends RuleAlertData +>( + loggerMock: Logger, + actionsPluginMock: jest.Mocked, + alertingEventLoggerMock: jest.Mocked, + actionsClientMock: jest.Mocked>, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + alertsClientMock: jest.Mocked +) => ({ + rule: getRule(), + ruleType: getRuleType(), + logger: loggerMock, + taskRunnerContext: { + actionsConfigMap: { + default: { + max: 1000, + }, + }, + actionsPlugin: actionsPluginMock, + connectorAdapterRegistry: new ConnectorAdapterRegistry(), + } as unknown as TaskRunnerContext, + apiKey, + ruleConsumer: 'rule-consumer', + executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + alertUuid: 'uuid-1', + ruleLabel: 'rule-label', + request: {} as KibanaRequest, + alertingEventLogger: alertingEventLoggerMock, + previousStartedAt: null, + taskInstance: { + params: { spaceId: 'test1', alertId: '1' }, + } as unknown as ConcreteTaskInstance, + actionsClient: actionsClientMock, + alertsClient: alertsClientMock, +}); diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/types.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/types.ts new file mode 100644 index 00000000000000..efcb51fcb26980 --- /dev/null +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/types.ts @@ -0,0 +1,111 @@ +/* + * 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 type { Logger } from '@kbn/core/server'; +import { PublicMethodsOf } from '@kbn/utility-types'; +import { ActionsClient } from '@kbn/actions-plugin/server/actions_client'; +import { IAlertsClient } from '../../alerts_client/types'; +import { Alert } from '../../alert'; +import { + AlertInstanceContext, + AlertInstanceState, + RuleTypeParams, + SanitizedRule, + RuleTypeState, + RuleAction, + RuleAlertData, + RuleSystemAction, + ThrottledActions, +} from '../../../common'; +import { NormalizedRuleType } from '../../rule_type_registry'; +import { CombinedSummarizedAlerts, RawRule } from '../../types'; +import { RuleRunMetricsStore } from '../../lib/rule_run_metrics_store'; +import { AlertingEventLogger } from '../../lib/alerting_event_logger/alerting_event_logger'; +import { RuleTaskInstance, TaskRunnerContext } from '../types'; + +export interface ActionSchedulerOptions< + Params extends RuleTypeParams, + ExtractedParams extends RuleTypeParams, + RuleState extends RuleTypeState, + State extends AlertInstanceState, + Context extends AlertInstanceContext, + ActionGroupIds extends string, + RecoveryActionGroupId extends string, + AlertData extends RuleAlertData +> { + ruleType: NormalizedRuleType< + Params, + ExtractedParams, + RuleState, + State, + Context, + ActionGroupIds, + RecoveryActionGroupId, + AlertData + >; + logger: Logger; + alertingEventLogger: PublicMethodsOf; + rule: SanitizedRule; + taskRunnerContext: TaskRunnerContext; + taskInstance: RuleTaskInstance; + ruleRunMetricsStore: RuleRunMetricsStore; + apiKey: RawRule['apiKey']; + ruleConsumer: string; + executionId: string; + ruleLabel: string; + previousStartedAt: Date | null; + actionsClient: PublicMethodsOf; + alertsClient: IAlertsClient; +} + +export type Executable< + State extends AlertInstanceState, + Context extends AlertInstanceContext, + ActionGroupIds extends string, + RecoveryActionGroupId extends string +> = { + action: RuleAction | RuleSystemAction; +} & ( + | { + alert: Alert; + summarizedAlerts?: never; + } + | { + alert?: never; + summarizedAlerts: CombinedSummarizedAlerts; + } +); + +export interface GenerateExecutablesOpts< + State extends AlertInstanceState, + Context extends AlertInstanceContext, + ActionGroupIds extends string, + RecoveryActionGroupId extends string +> { + alerts: Record>; + throttledSummaryActions: ThrottledActions; +} + +export interface IActionScheduler< + State extends AlertInstanceState, + Context extends AlertInstanceContext, + ActionGroupIds extends string, + RecoveryActionGroupId extends string +> { + get priority(): number; + generateExecutables( + opts: GenerateExecutablesOpts + ): Promise>>; +} + +export interface RuleUrl { + absoluteUrl?: string; + kibanaBaseUrl?: string; + basePathname?: string; + spaceIdSegment?: string; + relativePath?: string; +} diff --git a/x-pack/plugins/alerting/server/task_runner/execution_handler.ts b/x-pack/plugins/alerting/server/task_runner/execution_handler.ts deleted file mode 100644 index f5a61bb6ccabce..00000000000000 --- a/x-pack/plugins/alerting/server/task_runner/execution_handler.ts +++ /dev/null @@ -1,975 +0,0 @@ -/* - * 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 type { PublicMethodsOf } from '@kbn/utility-types'; -import { Logger } from '@kbn/core/server'; -import { ALERT_UUID, getRuleDetailsRoute, triggersActionsRoute } from '@kbn/rule-data-utils'; -import { asSavedObjectExecutionSource } from '@kbn/actions-plugin/server'; -import { - createTaskRunError, - isEphemeralTaskRejectedDueToCapacityError, - TaskErrorSource, -} from '@kbn/task-manager-plugin/server'; -import { - ExecuteOptions as EnqueueExecutionOptions, - ExecutionResponseItem, - ExecutionResponseType, -} from '@kbn/actions-plugin/server/create_execute_function'; -import { ActionsCompletion } from '@kbn/alerting-state-types'; -import { ActionsClient } from '@kbn/actions-plugin/server/actions_client'; -import { chunk } from 'lodash'; -import { GetSummarizedAlertsParams, IAlertsClient } from '../alerts_client/types'; -import { AlertingEventLogger } from '../lib/alerting_event_logger/alerting_event_logger'; -import { AlertHit, parseDuration, CombinedSummarizedAlerts, ThrottledActions } from '../types'; -import { RuleRunMetricsStore } from '../lib/rule_run_metrics_store'; -import { injectActionParams } from './inject_action_params'; -import { Executable, ExecutionHandlerOptions, RuleTaskInstance, TaskRunnerContext } from './types'; -import { - transformActionParams, - TransformActionParamsOptions, - transformSummaryActionParams, -} from './transform_action_params'; -import { Alert } from '../alert'; -import { NormalizedRuleType } from '../rule_type_registry'; -import { - AlertInstanceContext, - AlertInstanceState, - RuleAction, - RuleTypeParams, - RuleTypeState, - SanitizedRule, - RuleAlertData, - RuleNotifyWhen, - RuleSystemAction, -} from '../../common'; -import { - generateActionHash, - getSummaryActionsFromTaskState, - getSummaryActionTimeBounds, - isActionOnInterval, - isSummaryAction, - isSummaryActionOnInterval, - isSummaryActionThrottled, -} from './rule_action_helper'; -import { RULE_SAVED_OBJECT_TYPE } from '../saved_objects'; -import { ConnectorAdapter } from '../connector_adapters/types'; -import { withAlertingSpan } from './lib'; - -enum Reasons { - MUTED = 'muted', - THROTTLED = 'throttled', - ACTION_GROUP_NOT_CHANGED = 'actionGroupHasNotChanged', -} - -interface LogAction { - id: string; - typeId: string; - alertId?: string; - alertGroup?: string; - alertSummary?: { - new: number; - ongoing: number; - recovered: number; - }; -} - -interface RunSummarizedActionArgs { - action: RuleAction; - summarizedAlerts: CombinedSummarizedAlerts; - spaceId: string; - bulkActions: EnqueueExecutionOptions[]; -} - -interface RunSystemActionArgs { - action: RuleSystemAction; - connectorAdapter: ConnectorAdapter; - summarizedAlerts: CombinedSummarizedAlerts; - rule: SanitizedRule; - ruleProducer: string; - spaceId: string; - bulkActions: EnqueueExecutionOptions[]; -} - -interface RunActionArgs< - State extends AlertInstanceState, - Context extends AlertInstanceContext, - ActionGroupIds extends string, - RecoveryActionGroupId extends string -> { - action: RuleAction; - alert: Alert; - ruleId: string; - spaceId: string; - bulkActions: EnqueueExecutionOptions[]; -} - -export interface RunResult { - throttledSummaryActions: ThrottledActions; -} - -export interface RuleUrl { - absoluteUrl?: string; - kibanaBaseUrl?: string; - basePathname?: string; - spaceIdSegment?: string; - relativePath?: string; -} - -export class ExecutionHandler< - Params extends RuleTypeParams, - ExtractedParams extends RuleTypeParams, - RuleState extends RuleTypeState, - State extends AlertInstanceState, - Context extends AlertInstanceContext, - ActionGroupIds extends string, - RecoveryActionGroupId extends string, - AlertData extends RuleAlertData -> { - private logger: Logger; - private alertingEventLogger: PublicMethodsOf; - private rule: SanitizedRule; - private ruleType: NormalizedRuleType< - Params, - ExtractedParams, - RuleState, - State, - Context, - ActionGroupIds, - RecoveryActionGroupId, - AlertData - >; - private taskRunnerContext: TaskRunnerContext; - private taskInstance: RuleTaskInstance; - private ruleRunMetricsStore: RuleRunMetricsStore; - private apiKey: string | null; - private ruleConsumer: string; - private executionId: string; - private ruleLabel: string; - private ephemeralActionsToSchedule: number; - private CHUNK_SIZE = 1000; - private skippedAlerts: { [key: string]: { reason: string } } = {}; - private actionsClient: PublicMethodsOf; - private ruleTypeActionGroups?: Map; - private mutedAlertIdsSet: Set = new Set(); - private previousStartedAt: Date | null; - private alertsClient: IAlertsClient< - AlertData, - State, - Context, - ActionGroupIds, - RecoveryActionGroupId - >; - - constructor({ - rule, - ruleType, - logger, - alertingEventLogger, - taskRunnerContext, - taskInstance, - ruleRunMetricsStore, - apiKey, - ruleConsumer, - executionId, - ruleLabel, - previousStartedAt, - actionsClient, - alertsClient, - }: ExecutionHandlerOptions< - Params, - ExtractedParams, - RuleState, - State, - Context, - ActionGroupIds, - RecoveryActionGroupId, - AlertData - >) { - this.logger = logger; - this.alertingEventLogger = alertingEventLogger; - this.rule = rule; - this.ruleType = ruleType; - this.taskRunnerContext = taskRunnerContext; - this.taskInstance = taskInstance; - this.ruleRunMetricsStore = ruleRunMetricsStore; - this.apiKey = apiKey; - this.ruleConsumer = ruleConsumer; - this.executionId = executionId; - this.ruleLabel = ruleLabel; - this.actionsClient = actionsClient; - this.ephemeralActionsToSchedule = taskRunnerContext.maxEphemeralActionsPerRule; - this.ruleTypeActionGroups = new Map( - ruleType.actionGroups.map((actionGroup) => [actionGroup.id, actionGroup.name]) - ); - this.previousStartedAt = previousStartedAt; - this.mutedAlertIdsSet = new Set(rule.mutedInstanceIds); - this.alertsClient = alertsClient; - } - - public async run( - alerts: Record> - ): Promise { - const throttledSummaryActions: ThrottledActions = getSummaryActionsFromTaskState({ - actions: this.rule.actions, - summaryActions: this.taskInstance.state?.summaryActions, - }); - const executables = await this.generateExecutables(alerts, throttledSummaryActions); - - if (executables.length === 0) { - return { throttledSummaryActions }; - } - - const { - CHUNK_SIZE, - logger, - alertingEventLogger, - ruleRunMetricsStore, - taskRunnerContext: { actionsConfigMap }, - taskInstance: { - params: { spaceId, alertId: ruleId }, - }, - } = this; - - const logActions: Record = {}; - const bulkActions: EnqueueExecutionOptions[] = []; - let bulkActionsResponse: ExecutionResponseItem[] = []; - - this.ruleRunMetricsStore.incrementNumberOfGeneratedActions(executables.length); - - for (const { action, alert, summarizedAlerts } of executables) { - const { actionTypeId } = action; - - ruleRunMetricsStore.incrementNumberOfGeneratedActionsByConnectorType(actionTypeId); - if (ruleRunMetricsStore.hasReachedTheExecutableActionsLimit(actionsConfigMap)) { - ruleRunMetricsStore.setTriggeredActionsStatusByConnectorType({ - actionTypeId, - status: ActionsCompletion.PARTIAL, - }); - logger.debug( - `Rule "${this.rule.id}" skipped scheduling action "${action.id}" because the maximum number of allowed actions has been reached.` - ); - break; - } - - if ( - ruleRunMetricsStore.hasReachedTheExecutableActionsLimitByConnectorType({ - actionTypeId, - actionsConfigMap, - }) - ) { - if (!ruleRunMetricsStore.hasConnectorTypeReachedTheLimit(actionTypeId)) { - logger.debug( - `Rule "${this.rule.id}" skipped scheduling action "${action.id}" because the maximum number of allowed actions for connector type ${actionTypeId} has been reached.` - ); - } - ruleRunMetricsStore.setTriggeredActionsStatusByConnectorType({ - actionTypeId, - status: ActionsCompletion.PARTIAL, - }); - continue; - } - - if (!this.isExecutableAction(action)) { - this.logger.warn( - `Rule "${this.taskInstance.params.alertId}" skipped scheduling action "${action.id}" because it is disabled` - ); - continue; - } - - ruleRunMetricsStore.incrementNumberOfTriggeredActions(); - ruleRunMetricsStore.incrementNumberOfTriggeredActionsByConnectorType(actionTypeId); - - if (!this.isSystemAction(action) && summarizedAlerts) { - const defaultAction = action as RuleAction; - if (isActionOnInterval(action)) { - throttledSummaryActions[defaultAction.uuid!] = { date: new Date().toISOString() }; - } - - logActions[defaultAction.id] = await this.runSummarizedAction({ - action, - summarizedAlerts, - spaceId, - bulkActions, - }); - } else if (summarizedAlerts && this.isSystemAction(action)) { - const hasConnectorAdapter = this.taskRunnerContext.connectorAdapterRegistry.has( - action.actionTypeId - ); - /** - * System actions without an adapter - * cannot be executed - * - */ - if (!hasConnectorAdapter) { - this.logger.warn( - `Rule "${this.taskInstance.params.alertId}" skipped scheduling system action "${action.id}" because no connector adapter is configured` - ); - - continue; - } - - const connectorAdapter = this.taskRunnerContext.connectorAdapterRegistry.get( - action.actionTypeId - ); - logActions[action.id] = await this.runSystemAction({ - action, - connectorAdapter, - summarizedAlerts, - rule: this.rule, - ruleProducer: this.ruleType.producer, - spaceId, - bulkActions, - }); - } else if (!this.isSystemAction(action) && alert) { - const defaultAction = action as RuleAction; - logActions[defaultAction.id] = await this.runAction({ - action, - spaceId, - alert, - ruleId, - bulkActions, - }); - - const actionGroup = defaultAction.group; - if (!this.isRecoveredAlert(actionGroup)) { - if (isActionOnInterval(action)) { - alert.updateLastScheduledActions( - defaultAction.group as ActionGroupIds, - generateActionHash(action), - defaultAction.uuid - ); - } else { - alert.updateLastScheduledActions(defaultAction.group as ActionGroupIds); - } - alert.unscheduleActions(); - } - } - } - - if (!!bulkActions.length) { - for (const c of chunk(bulkActions, CHUNK_SIZE)) { - let enqueueResponse; - try { - enqueueResponse = await withAlertingSpan('alerting:bulk-enqueue-actions', () => - this.actionsClient!.bulkEnqueueExecution(c) - ); - } catch (e) { - if (e.statusCode === 404) { - throw createTaskRunError(e, TaskErrorSource.USER); - } - throw createTaskRunError(e, TaskErrorSource.FRAMEWORK); - } - if (enqueueResponse.errors) { - bulkActionsResponse = bulkActionsResponse.concat( - enqueueResponse.items.filter( - (i) => i.response === ExecutionResponseType.QUEUED_ACTIONS_LIMIT_ERROR - ) - ); - } - } - } - - if (!!bulkActionsResponse.length) { - for (const r of bulkActionsResponse) { - if (r.response === ExecutionResponseType.QUEUED_ACTIONS_LIMIT_ERROR) { - ruleRunMetricsStore.setHasReachedQueuedActionsLimit(true); - ruleRunMetricsStore.decrementNumberOfTriggeredActions(); - ruleRunMetricsStore.decrementNumberOfTriggeredActionsByConnectorType(r.actionTypeId); - ruleRunMetricsStore.setTriggeredActionsStatusByConnectorType({ - actionTypeId: r.actionTypeId, - status: ActionsCompletion.PARTIAL, - }); - - logger.debug( - `Rule "${this.rule.id}" skipped scheduling action "${r.id}" because the maximum number of queued actions has been reached.` - ); - - delete logActions[r.id]; - } - } - } - - const logActionsValues = Object.values(logActions); - if (!!logActionsValues.length) { - for (const action of logActionsValues) { - alertingEventLogger.logAction(action); - } - } - - return { throttledSummaryActions }; - } - - private async runSummarizedAction({ - action, - summarizedAlerts, - spaceId, - bulkActions, - }: RunSummarizedActionArgs): Promise { - const { start, end } = getSummaryActionTimeBounds( - action, - this.rule.schedule, - this.previousStartedAt - ); - const ruleUrl = this.buildRuleUrl(spaceId, start, end); - const actionToRun = { - ...action, - params: injectActionParams({ - actionTypeId: action.actionTypeId, - ruleUrl, - ruleName: this.rule.name, - actionParams: transformSummaryActionParams({ - alerts: summarizedAlerts, - rule: this.rule, - ruleTypeId: this.ruleType.id, - actionId: action.id, - actionParams: action.params, - spaceId, - actionsPlugin: this.taskRunnerContext.actionsPlugin, - actionTypeId: action.actionTypeId, - kibanaBaseUrl: this.taskRunnerContext.kibanaBaseUrl, - ruleUrl: ruleUrl?.absoluteUrl, - }), - }), - }; - - await this.actionRunOrAddToBulk({ - enqueueOptions: this.getEnqueueOptions(actionToRun), - bulkActions, - }); - - return { - id: action.id, - typeId: action.actionTypeId, - alertSummary: { - new: summarizedAlerts.new.count, - ongoing: summarizedAlerts.ongoing.count, - recovered: summarizedAlerts.recovered.count, - }, - }; - } - - private async runSystemAction({ - action, - spaceId, - connectorAdapter, - summarizedAlerts, - rule, - ruleProducer, - bulkActions, - }: RunSystemActionArgs): Promise { - const ruleUrl = this.buildRuleUrl(spaceId); - - const connectorAdapterActionParams = connectorAdapter.buildActionParams({ - alerts: summarizedAlerts, - rule: { - id: rule.id, - tags: rule.tags, - name: rule.name, - consumer: rule.consumer, - producer: ruleProducer, - }, - ruleUrl: ruleUrl?.absoluteUrl, - spaceId, - params: action.params, - }); - - const actionToRun = Object.assign(action, { params: connectorAdapterActionParams }); - - await this.actionRunOrAddToBulk({ - enqueueOptions: this.getEnqueueOptions(actionToRun), - bulkActions, - }); - - return { - id: action.id, - typeId: action.actionTypeId, - alertSummary: { - new: summarizedAlerts.new.count, - ongoing: summarizedAlerts.ongoing.count, - recovered: summarizedAlerts.recovered.count, - }, - }; - } - - private async runAction({ - action, - spaceId, - alert, - ruleId, - bulkActions, - }: RunActionArgs): Promise { - const ruleUrl = this.buildRuleUrl(spaceId); - const executableAlert = alert!; - const actionGroup = action.group as ActionGroupIds; - const transformActionParamsOptions: TransformActionParamsOptions = { - actionsPlugin: this.taskRunnerContext.actionsPlugin, - alertId: ruleId, - alertType: this.ruleType.id, - actionTypeId: action.actionTypeId, - alertName: this.rule.name, - spaceId, - tags: this.rule.tags, - alertInstanceId: executableAlert.getId(), - alertUuid: executableAlert.getUuid(), - alertActionGroup: actionGroup, - alertActionGroupName: this.ruleTypeActionGroups!.get(actionGroup)!, - context: executableAlert.getContext(), - actionId: action.id, - state: executableAlert.getState(), - kibanaBaseUrl: this.taskRunnerContext.kibanaBaseUrl, - alertParams: this.rule.params, - actionParams: action.params, - flapping: executableAlert.getFlapping(), - ruleUrl: ruleUrl?.absoluteUrl, - }; - - if (executableAlert.isAlertAsData()) { - transformActionParamsOptions.aadAlert = executableAlert.getAlertAsData(); - } - const actionToRun = { - ...action, - params: injectActionParams({ - actionTypeId: action.actionTypeId, - ruleUrl, - ruleName: this.rule.name, - actionParams: transformActionParams(transformActionParamsOptions), - }), - }; - - await this.actionRunOrAddToBulk({ - enqueueOptions: this.getEnqueueOptions(actionToRun), - bulkActions, - }); - - return { - id: action.id, - typeId: action.actionTypeId, - alertId: alert.getId(), - alertGroup: action.group, - }; - } - - private logNumberOfFilteredAlerts({ - numberOfAlerts = 0, - numberOfSummarizedAlerts = 0, - action, - }: { - numberOfAlerts: number; - numberOfSummarizedAlerts: number; - action: RuleAction | RuleSystemAction; - }) { - const count = numberOfAlerts - numberOfSummarizedAlerts; - if (count > 0) { - this.logger.debug( - `(${count}) alert${count > 1 ? 's' : ''} ${ - count > 1 ? 'have' : 'has' - } been filtered out for: ${action.actionTypeId}:${action.uuid}` - ); - } - } - - private isAlertMuted(alertId: string) { - const muted = this.mutedAlertIdsSet.has(alertId); - if (muted) { - if ( - !this.skippedAlerts[alertId] || - (this.skippedAlerts[alertId] && this.skippedAlerts[alertId].reason !== Reasons.MUTED) - ) { - this.logger.debug( - `skipping scheduling of actions for '${alertId}' in rule ${this.ruleLabel}: rule is muted` - ); - } - this.skippedAlerts[alertId] = { reason: Reasons.MUTED }; - return true; - } - return false; - } - - private isExecutableAction(action: RuleAction | RuleSystemAction) { - return this.taskRunnerContext.actionsPlugin.isActionExecutable(action.id, action.actionTypeId, { - notifyUsage: true, - }); - } - - private isSystemAction(action?: RuleAction | RuleSystemAction): action is RuleSystemAction { - return this.taskRunnerContext.actionsPlugin.isSystemActionConnector(action?.id ?? ''); - } - - private isRecoveredAlert(actionGroup: string) { - return actionGroup === this.ruleType.recoveryActionGroup.id; - } - - private isExecutableActiveAlert({ - alert, - action, - }: { - alert: Alert; - action: RuleAction; - }) { - const alertId = alert.getId(); - const { rule, ruleLabel, logger } = this; - const notifyWhen = action.frequency?.notifyWhen || rule.notifyWhen; - - if (notifyWhen === 'onActionGroupChange' && !alert.scheduledActionGroupHasChanged()) { - if ( - !this.skippedAlerts[alertId] || - (this.skippedAlerts[alertId] && - this.skippedAlerts[alertId].reason !== Reasons.ACTION_GROUP_NOT_CHANGED) - ) { - logger.debug( - `skipping scheduling of actions for '${alertId}' in rule ${ruleLabel}: alert is active but action group has not changed` - ); - } - this.skippedAlerts[alertId] = { reason: Reasons.ACTION_GROUP_NOT_CHANGED }; - return false; - } - - if (notifyWhen === 'onThrottleInterval') { - const throttled = action.frequency?.throttle - ? alert.isThrottled({ - throttle: action.frequency.throttle ?? null, - actionHash: generateActionHash(action), // generateActionHash must be removed once all the hash identifiers removed from the task state - uuid: action.uuid, - }) - : alert.isThrottled({ throttle: rule.throttle ?? null }); - - if (throttled) { - if ( - !this.skippedAlerts[alertId] || - (this.skippedAlerts[alertId] && this.skippedAlerts[alertId].reason !== Reasons.THROTTLED) - ) { - logger.debug( - `skipping scheduling of actions for '${alertId}' in rule ${ruleLabel}: rule is throttled` - ); - } - this.skippedAlerts[alertId] = { reason: Reasons.THROTTLED }; - return false; - } - } - - return alert.hasScheduledActions(); - } - - private getActionGroup(alert: Alert) { - return alert.getScheduledActionOptions()?.actionGroup || this.ruleType.recoveryActionGroup.id; - } - - private buildRuleUrl(spaceId: string, start?: number, end?: number): RuleUrl | undefined { - if (!this.taskRunnerContext.kibanaBaseUrl) { - return; - } - - const relativePath = this.ruleType.getViewInAppRelativeUrl - ? this.ruleType.getViewInAppRelativeUrl({ rule: this.rule, start, end }) - : `${triggersActionsRoute}${getRuleDetailsRoute(this.rule.id)}`; - - try { - const basePathname = new URL(this.taskRunnerContext.kibanaBaseUrl).pathname; - const basePathnamePrefix = basePathname !== '/' ? `${basePathname}` : ''; - const spaceIdSegment = spaceId !== 'default' ? `/s/${spaceId}` : ''; - - const ruleUrl = new URL( - [basePathnamePrefix, spaceIdSegment, relativePath].join(''), - this.taskRunnerContext.kibanaBaseUrl - ); - - return { - absoluteUrl: ruleUrl.toString(), - kibanaBaseUrl: this.taskRunnerContext.kibanaBaseUrl, - basePathname: basePathnamePrefix, - spaceIdSegment, - relativePath, - }; - } catch (error) { - this.logger.debug( - `Rule "${this.rule.id}" encountered an error while constructing the rule.url variable: ${error.message}` - ); - return; - } - } - - private getEnqueueOptions(action: RuleAction | RuleSystemAction): EnqueueExecutionOptions { - const { - apiKey, - ruleConsumer, - executionId, - taskInstance: { - params: { spaceId, alertId: ruleId }, - }, - } = this; - - const namespace = spaceId === 'default' ? {} : { namespace: spaceId }; - return { - id: action.id, - params: action.params, - spaceId, - apiKey: apiKey ?? null, - consumer: ruleConsumer, - source: asSavedObjectExecutionSource({ - id: ruleId, - type: RULE_SAVED_OBJECT_TYPE, - }), - executionId, - relatedSavedObjects: [ - { - id: ruleId, - type: RULE_SAVED_OBJECT_TYPE, - namespace: namespace.namespace, - typeId: this.ruleType.id, - }, - ], - actionTypeId: action.actionTypeId, - }; - } - - private async generateExecutables( - alerts: Record>, - throttledSummaryActions: ThrottledActions - ): Promise>> { - const executables = []; - for (const action of this.rule.actions) { - const alertsArray = Object.entries(alerts); - let summarizedAlerts = null; - - if (this.shouldGetSummarizedAlerts({ action, throttledSummaryActions })) { - summarizedAlerts = await this.getSummarizedAlerts({ - action, - spaceId: this.taskInstance.params.spaceId, - ruleId: this.taskInstance.params.alertId, - }); - - if (!isSummaryActionOnInterval(action)) { - this.logNumberOfFilteredAlerts({ - numberOfAlerts: alertsArray.length, - numberOfSummarizedAlerts: summarizedAlerts.all.count, - action, - }); - } - } - - if (isSummaryAction(action)) { - if (summarizedAlerts && summarizedAlerts.all.count !== 0) { - executables.push({ action, summarizedAlerts }); - } - continue; - } - - for (const [alertId, alert] of alertsArray) { - const alertMaintenanceWindowIds = alert.getMaintenanceWindowIds(); - if (alertMaintenanceWindowIds.length !== 0) { - this.logger.debug( - `no scheduling of summary actions "${action.id}" for rule "${ - this.taskInstance.params.alertId - }": has active maintenance windows ${alertMaintenanceWindowIds.join(', ')}.` - ); - continue; - } - - if (alert.isFilteredOut(summarizedAlerts)) { - continue; - } - - const actionGroup = this.getActionGroup(alert); - - if (!this.ruleTypeActionGroups!.has(actionGroup)) { - this.logger.error( - `Invalid action group "${actionGroup}" for rule "${this.ruleType.id}".` - ); - continue; - } - - // only actions with notifyWhen set to "on status change" should return - // notifications for flapping pending recovered alerts - if ( - alert.getPendingRecoveredCount() > 0 && - action?.frequency?.notifyWhen !== RuleNotifyWhen.CHANGE - ) { - continue; - } - - if (summarizedAlerts) { - const alertAsData = summarizedAlerts.all.data.find( - (alertHit: AlertHit) => alertHit._id === alert.getUuid() - ); - if (alertAsData) { - alert.setAlertAsData(alertAsData); - } - } - - if (action.group === actionGroup && !this.isAlertMuted(alertId)) { - if ( - this.isRecoveredAlert(action.group) || - this.isExecutableActiveAlert({ alert, action }) - ) { - executables.push({ action, alert }); - } - } - } - } - - if (!this.canGetSummarizedAlerts()) { - return executables; - } - - for (const systemAction of this.rule?.systemActions ?? []) { - const summarizedAlerts = await this.getSummarizedAlerts({ - action: systemAction, - spaceId: this.taskInstance.params.spaceId, - ruleId: this.taskInstance.params.alertId, - }); - - if (summarizedAlerts && summarizedAlerts.all.count !== 0) { - executables.push({ action: systemAction, summarizedAlerts }); - } - } - - return executables; - } - - private canGetSummarizedAlerts() { - return !!this.ruleType.alerts && !!this.alertsClient.getSummarizedAlerts; - } - - private shouldGetSummarizedAlerts({ - action, - throttledSummaryActions, - }: { - action: RuleAction; - throttledSummaryActions: ThrottledActions; - }) { - if (!this.canGetSummarizedAlerts()) { - if (action.frequency?.summary) { - this.logger.error( - `Skipping action "${action.id}" for rule "${this.rule.id}" because the rule type "${this.ruleType.name}" does not support alert-as-data.` - ); - } - return false; - } - - if (action.useAlertDataForTemplate) { - return true; - } - // we fetch summarizedAlerts to filter alerts in memory as well - if (!isSummaryAction(action) && !action.alertsFilter) { - return false; - } - if ( - isSummaryAction(action) && - isSummaryActionThrottled({ - action, - throttledSummaryActions, - logger: this.logger, - }) - ) { - return false; - } - - return true; - } - - private async getSummarizedAlerts({ - action, - ruleId, - spaceId, - }: { - action: RuleAction | RuleSystemAction; - ruleId: string; - spaceId: string; - }): Promise { - const optionsBase = { - ruleId, - spaceId, - excludedAlertInstanceIds: this.rule.mutedInstanceIds, - alertsFilter: this.isSystemAction(action) ? undefined : (action as RuleAction).alertsFilter, - }; - - let options: GetSummarizedAlertsParams; - - if (!this.isSystemAction(action) && isActionOnInterval(action)) { - const throttleMills = parseDuration((action as RuleAction).frequency!.throttle!); - const start = new Date(Date.now() - throttleMills); - - options = { - ...optionsBase, - start, - end: new Date(), - }; - } else { - options = { - ...optionsBase, - executionUuid: this.executionId, - }; - } - - let alerts; - try { - alerts = await withAlertingSpan(`alerting:get-summarized-alerts-${action.uuid}`, () => - this.alertsClient.getSummarizedAlerts!(options) - ); - } catch (e) { - throw createTaskRunError(e, TaskErrorSource.FRAMEWORK); - } - - /** - * We need to remove all new alerts with maintenance windows retrieved from - * getSummarizedAlerts because they might not have maintenance window IDs - * associated with them from maintenance windows with scoped query updated - * yet (the update call uses refresh: false). So we need to rely on the in - * memory alerts to do this. - */ - const newAlertsInMemory = - Object.values(this.alertsClient.getProcessedAlerts('new') || {}) || []; - - const newAlertsWithMaintenanceWindowIds = newAlertsInMemory.reduce( - (result, alert) => { - if (alert.getMaintenanceWindowIds().length > 0) { - result.push(alert.getUuid()); - } - return result; - }, - [] - ); - - const newAlerts = alerts.new.data.filter((alert) => { - return !newAlertsWithMaintenanceWindowIds.includes(alert[ALERT_UUID]); - }); - - const total = newAlerts.length + alerts.ongoing.count + alerts.recovered.count; - return { - ...alerts, - new: { - count: newAlerts.length, - data: newAlerts, - }, - all: { - count: total, - data: [...newAlerts, ...alerts.ongoing.data, ...alerts.recovered.data], - }, - }; - } - - private async actionRunOrAddToBulk({ - enqueueOptions, - bulkActions, - }: { - enqueueOptions: EnqueueExecutionOptions; - bulkActions: EnqueueExecutionOptions[]; - }) { - if (this.taskRunnerContext.supportsEphemeralTasks && this.ephemeralActionsToSchedule > 0) { - this.ephemeralActionsToSchedule--; - try { - await this.actionsClient!.ephemeralEnqueuedExecution(enqueueOptions); - } catch (err) { - if (isEphemeralTaskRejectedDueToCapacityError(err)) { - bulkActions.push(enqueueOptions); - } - } - } else { - bulkActions.push(enqueueOptions); - } - } -} diff --git a/x-pack/plugins/alerting/server/task_runner/inject_action_params.ts b/x-pack/plugins/alerting/server/task_runner/inject_action_params.ts index 65cb7f9e65bad6..421796c08bbff2 100644 --- a/x-pack/plugins/alerting/server/task_runner/inject_action_params.ts +++ b/x-pack/plugins/alerting/server/task_runner/inject_action_params.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { RuleActionParams } from '../types'; -import { RuleUrl } from './execution_handler'; +import { RuleUrl } from './action_scheduler'; export interface InjectActionParamsOpts { actionTypeId: string; diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.ts index 9b6d2172d0d5f8..5eb15bff0107bf 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.ts @@ -18,7 +18,7 @@ import { } from '@kbn/task-manager-plugin/server'; import { nanosToMillis } from '@kbn/event-log-plugin/server'; import { getErrorSource, isUserError } from '@kbn/task-manager-plugin/server/task_running'; -import { ExecutionHandler, RunResult } from './execution_handler'; +import { ActionScheduler, type RunResult } from './action_scheduler'; import { RuleRunnerErrorStackTraceLog, RuleTaskInstance, @@ -381,7 +381,7 @@ export class TaskRunner< throw error; } - const executionHandler = new ExecutionHandler({ + const actionScheduler = new ActionScheduler({ rule, ruleType: this.ruleType, logger: this.logger, @@ -398,7 +398,7 @@ export class TaskRunner< alertsClient, }); - let executionHandlerRunResult: RunResult = { throttledSummaryActions: {} }; + let actionSchedulerResult: RunResult = { throttledSummaryActions: {} }; await withAlertingSpan('alerting:schedule-actions', () => this.timer.runWithTimer(TaskRunnerTimerSpan.TriggerActions, async () => { @@ -410,7 +410,7 @@ export class TaskRunner< ); this.countUsageOfActionExecutionAfterRuleCancellation(); } else { - executionHandlerRunResult = await executionHandler.run({ + actionSchedulerResult = await actionScheduler.run({ ...alertsClient.getProcessedAlerts('activeCurrent'), ...alertsClient.getProcessedAlerts('recoveredCurrent'), }); @@ -435,7 +435,7 @@ export class TaskRunner< alertTypeState: updatedRuleTypeState || undefined, alertInstances: alertsToReturn, alertRecoveredInstances: recoveredAlertsToReturn, - summaryActions: executionHandlerRunResult.throttledSummaryActions, + summaryActions: actionSchedulerResult.throttledSummaryActions, }; } diff --git a/x-pack/plugins/alerting/server/task_runner/types.ts b/x-pack/plugins/alerting/server/task_runner/types.ts index e6701d26277e9d..9d40c186bcead5 100644 --- a/x-pack/plugins/alerting/server/task_runner/types.ts +++ b/x-pack/plugins/alerting/server/task_runner/types.ts @@ -83,9 +83,8 @@ export interface RuleTaskInstance extends ConcreteTaskInstance { state: RuleTaskState; } -// / ExecutionHandler - -export interface ExecutionHandlerOptions< +// ActionScheduler +export interface ActionSchedulerOptions< Params extends RuleTypeParams, ExtractedParams extends RuleTypeParams, RuleState extends RuleTypeState, diff --git a/x-pack/plugins/alerting/tsconfig.json b/x-pack/plugins/alerting/tsconfig.json index 63d1ea5768c4e7..0f07c2e8f8b8ec 100644 --- a/x-pack/plugins/alerting/tsconfig.json +++ b/x-pack/plugins/alerting/tsconfig.json @@ -70,7 +70,8 @@ "@kbn/react-kibana-context-render", "@kbn/search-types", "@kbn/alerting-state-types", - "@kbn/core-security-server" + "@kbn/core-security-server", + "@kbn/core-http-server" ], "exclude": [ "target/**/*" From fa0ef37edfe8ea5200eede5c566ddebb480d3ebd Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Mon, 22 Jul 2024 14:00:49 -0600 Subject: [PATCH 19/54] [Embeddable Rebuild] [Controls] Add drag and drop to control group (#188687) ## Summary > [!NOTE] > This PR has **no** user-facing changes - minus one small style change (which is a small selector simplification and doesn't actually change anything), all work is contained in the `examples` plugin. This PR adds drag and drop to the refactored control group in the `examples` plugin. ![Jul-18-2024 16-24-32](https://github.com/user-attachments/assets/c8080af7-4176-473f-92ea-b13f8b1e5def) ### Checklist - [x] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [x] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [x] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --- .../components/control_clone.tsx | 64 +++++++++++ .../control_error.tsx} | 0 .../{ => components}/control_panel.scss | 0 .../{ => components}/control_panel.tsx | 44 ++++++-- .../get_control_group_factory.tsx | 101 +++++++++++++++--- .../control_group/init_controls_manager.ts | 7 +- .../react_controls/control_renderer.tsx | 5 +- .../get_timeslider_control_factory.tsx | 3 +- .../public/react_controls/types.ts | 1 + .../public/control_group/control_group.scss | 7 +- 10 files changed, 195 insertions(+), 37 deletions(-) create mode 100644 examples/controls_example/public/react_controls/components/control_clone.tsx rename examples/controls_example/public/react_controls/{control_error_component.tsx => components/control_error.tsx} (100%) rename examples/controls_example/public/react_controls/{ => components}/control_panel.scss (100%) rename examples/controls_example/public/react_controls/{ => components}/control_panel.tsx (84%) diff --git a/examples/controls_example/public/react_controls/components/control_clone.tsx b/examples/controls_example/public/react_controls/components/control_clone.tsx new file mode 100644 index 00000000000000..d0c7141d615e81 --- /dev/null +++ b/examples/controls_example/public/react_controls/components/control_clone.tsx @@ -0,0 +1,64 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import classNames from 'classnames'; +import React from 'react'; + +import { EuiFlexGroup, EuiFlexItem, EuiFormLabel, EuiIcon } from '@elastic/eui'; +import { + useBatchedOptionalPublishingSubjects, + useStateFromPublishingSubject, +} from '@kbn/presentation-publishing'; + +import { DefaultControlApi } from '../types'; + +/** + * A simplified clone version of the control which is dragged. This version only shows + * the title, because individual controls can be any size, and dragging a wide item + * can be quite cumbersome. + */ +export const ControlClone = ({ + controlStyle, + controlApi, +}: { + controlStyle: string; + controlApi: DefaultControlApi; +}) => { + const width = useStateFromPublishingSubject(controlApi.width); + const [panelTitle, defaultPanelTitle] = useBatchedOptionalPublishingSubjects( + controlApi.panelTitle, + controlApi.defaultPanelTitle + ); + + return ( + + {controlStyle === 'twoLine' ? ( + {panelTitle ?? defaultPanelTitle} + ) : undefined} + + + + + {controlStyle === 'oneLine' ? ( + + + + ) : undefined} + + + ); +}; diff --git a/examples/controls_example/public/react_controls/control_error_component.tsx b/examples/controls_example/public/react_controls/components/control_error.tsx similarity index 100% rename from examples/controls_example/public/react_controls/control_error_component.tsx rename to examples/controls_example/public/react_controls/components/control_error.tsx diff --git a/examples/controls_example/public/react_controls/control_panel.scss b/examples/controls_example/public/react_controls/components/control_panel.scss similarity index 100% rename from examples/controls_example/public/react_controls/control_panel.scss rename to examples/controls_example/public/react_controls/components/control_panel.scss diff --git a/examples/controls_example/public/react_controls/control_panel.tsx b/examples/controls_example/public/react_controls/components/control_panel.tsx similarity index 84% rename from examples/controls_example/public/react_controls/control_panel.tsx rename to examples/controls_example/public/react_controls/components/control_panel.tsx index 95ff67e60b34c6..7127f158511be3 100644 --- a/examples/controls_example/public/react_controls/control_panel.tsx +++ b/examples/controls_example/public/react_controls/components/control_panel.tsx @@ -9,6 +9,8 @@ import classNames from 'classnames'; import React, { useState } from 'react'; +import { useSortable } from '@dnd-kit/sortable'; +import { CSS } from '@dnd-kit/utilities'; import { EuiFlexItem, EuiFormControlLayout, @@ -26,18 +28,16 @@ import { } from '@kbn/presentation-publishing'; import { FloatingActions } from '@kbn/presentation-util-plugin/public'; -import { ControlError } from './control_error_component'; -import { ControlPanelProps, DefaultControlApi } from './types'; +import { ControlPanelProps, DefaultControlApi } from '../types'; +import { ControlError } from './control_error'; import './control_panel.scss'; -/** - * TODO: Handle dragging - */ const DragHandle = ({ isEditable, controlTitle, hideEmptyDragHandle, + ...rest // drag info is contained here }: { isEditable: boolean; controlTitle?: string; @@ -45,6 +45,7 @@ const DragHandle = ({ }) => isEditable ? (