From 4e39685c011f48bfe907ea8312e3a1810ee21098 Mon Sep 17 00:00:00 2001 From: Lee Drengenberg Date: Mon, 25 Apr 2022 17:10:15 -0500 Subject: [PATCH 01/29] new file used by Kibana QA team to pull PRs for testing (#130411) * new file used by Kibana QA team to pull PRs for testing * moved file * updated with more labels to exclude * added readme Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- src/dev/prs/README.md | 13 +++ src/dev/prs/kibana_qa_pr_list.json | 135 +++++++++++++++++++++++++++++ 2 files changed, 148 insertions(+) create mode 100644 src/dev/prs/README.md create mode 100644 src/dev/prs/kibana_qa_pr_list.json diff --git a/src/dev/prs/README.md b/src/dev/prs/README.md new file mode 100644 index 0000000000000..b24f1ff2f79cb --- /dev/null +++ b/src/dev/prs/README.md @@ -0,0 +1,13 @@ +# Pulling a list of PRs + +This folder contains files used to pull lists of Kibana PRs for release testing. + +`scripts/download_pr_list.js` is the cli wrapper. + +You must have a `GITHUB_TOKEN` either set in your environment or on command line like + +`GITHUB_TOKEN= node scripts/download_pr_list.js` + +Run it with `--help` or without arguments for help. + +`kibana_qa_pr_list.json` is the file currently used by the Kibana QA team and also serves as an example. \ No newline at end of file diff --git a/src/dev/prs/kibana_qa_pr_list.json b/src/dev/prs/kibana_qa_pr_list.json new file mode 100644 index 0000000000000..e8d27ba9f2f0a --- /dev/null +++ b/src/dev/prs/kibana_qa_pr_list.json @@ -0,0 +1,135 @@ +{ + "include": [ +"v8.3.0" +], +"exclude": [ +"v8.2.0", +"v8.1.3", +"v8.1.2", +"v8.1.1", +"v8.1.0", +"v8.0.1", +"v8.0.0", +"v7.17.3", +"v7.17.2", +"v7.17.1", +"v7.17.0", +"v7.16.3", +"v7.16.2", +"v7.16.1", +"v7.16.0", +"v7.15.3", +"v7.15.2", +"v7.15.1", +"v7.15.0", +"v7.14.3", +"v7.14.2", +"v7.14.1", +"v7.14.0", +"v7.13.3", +"v7.13.2", +"v7.13.1", +"v7.13.0", +"v7.12.3", +"v7.12.2", +"v7.12.1", +"v7.12.0", +"v7.11.3", +"v7.11.2", +"v7.11.1", +"v7.11.0", +"v7.10.3", +"v7.10.2", +"v7.10.1", +"v7.10.0", +"v7.9.3", +"v7.9.2", +"v7.9.1", +"v7.9.0", +"v7.8.3", +"v7.8.2", +"v7.8.1", +"v7.8.0", +"v7.7.3", +"v7.7.2", +"v7.7.1", +"v7.7.0", +"v7.6.3", +"v7.6.2", +"v7.6.1", +"v7.6.0", +"v7.5.3", +"v7.5.2", +"v7.5.1", +"v7.5.0", +"v7.4.3", +"v7.4.2", +"v7.4.1", +"v7.4.0", +"v7.3.3", +"v7.3.2", +"v7.3.1", +"v7.3.0", +"v7.2.3", +"v7.2.2", +"v7.2.1", +"v7.2.0", +"v7.1.3", +"v7.1.2", +"v7.1.1", +"v7.1.0", +"v7.0.3", +"v7.0.2", +"v7.0.1", +"v7.0.0", +":ml", +"Feature:Anomaly Detection", +"Feature:Detections", +"Feature:Endpoint", +"Feature:Observability Landing - Milestone 1", +"Feature:Osquery", +"Feature:Transforms", +"Synthetics", +"Team: AWL: Platform", +"Team: Actionable Observability", +"Team: CTI", +"Team: SecuritySolution", +"Team:Asset Management", +"Team:Cloud Security Posture", +"Team:Detection Alerts", +"Team:Detection Rules", +"Team:Detections and Resp", +"Team:Docs", +"Team:Elasticsearch UI", +"Team:Endpoint Data Visibility", +"Team:Endpoint Management", +"Team:Endpoint Response", +"Team:EnterpriseSearch", +"Team:Fleet", +"Team:Infra Monitoring UI", +"Team:Ingest Management", +"Team:Observability", +"Team:Onboarding and Lifecycle Mgt", +"Team:Operations", +"Team:QA", +"Team:ResponseOps", +"Team:SIEM", +"Team:Security Solution Platform", +"Team:Security", +"Team:Threat Hunting", +"Team:Threat Hunting:Explore", +"Team:Threat Hunting:Investigations", +"Team:WorkplaceSearch", +"Team:WorkplaceSearch", +"Team:apm", +"Team:logs-metrics-ui", +"Team:uptime", +"bump", +"docs", +"failed-test", +"Feature:Unit Testing", +"Feature:Functional Testing", +"test_xpack_functional" +] +} + From 3ad6452166ba23c0bfa9bfa0a6e6622ee47efb22 Mon Sep 17 00:00:00 2001 From: Andrew Goldstein Date: Mon, 25 Apr 2022 16:14:59 -0600 Subject: [PATCH 02/29] [Security Solution] [Investigations] [Tech Debt] removes redundant code from timelines plugin (#130928) ## [Security Solution] [Investigations] [Tech Debt] removes redundant code from the timelines plugin This follow-up PR removes redundant code from the `timelines` plugin, identified while implementing https://github.com/elastic/kibana/pull/130740 --- .../timeline/events/common/index.ts | 26 - .../plugins/timelines/common/utility_types.ts | 7 - .../components/actions/action_icon_item.tsx | 54 - .../draggables/field_badge/index.tsx | 48 - .../draggables/field_badge/translations.ts | 34 - .../public/components/draggables/index.tsx | 8 - .../__snapshots__/empty_value.test.tsx.snap | 7 - .../empty_value/empty_value.test.tsx | 154 +-- .../public/components/empty_value/index.tsx | 41 - .../components/empty_value/translations.ts | 12 - .../exit_full_screen/index.test.tsx | 60 -- .../components/exit_full_screen/index.tsx | 64 -- .../exit_full_screen/translations.ts | 12 - .../timelines/public/components/index.tsx | 1 - .../body/column_headers/actions/index.tsx | 69 -- .../common/dragging_container.tsx | 25 - .../body/column_headers/common/styles.tsx | 19 - .../header/__snapshots__/index.test.tsx.snap | 51 - .../column_headers/header/header_content.tsx | 85 -- .../body/column_headers/header/helpers.ts | 54 - .../body/column_headers/header/index.test.tsx | 331 ------ .../body/column_headers/header/index.tsx | 95 -- .../__snapshots__/index.test.tsx.snap | 66 -- .../header_tooltip_content/index.test.tsx | 72 -- .../header_tooltip_content/index.tsx | 81 -- .../components/t_grid/body/constants.ts | 6 - .../__snapshots__/index.test.tsx.snap | 967 ------------------ .../body/data_driven_columns/index.test.tsx | 59 -- .../t_grid/body/data_driven_columns/index.tsx | 389 ------- .../stateful_cell.test.tsx | 180 ---- .../data_driven_columns/stateful_cell.tsx | 68 -- .../body/data_driven_columns/translations.ts | 28 - .../body/events/event_column_view.test.tsx | 118 --- .../t_grid/body/events/event_column_view.tsx | 192 ---- .../components/t_grid/body/events/index.tsx | 99 -- .../t_grid/body/events/stateful_event.tsx | 218 ---- .../components/t_grid/body/helpers.test.tsx | 169 --- .../public/components/t_grid/body/helpers.tsx | 28 - .../public/components/t_grid/types.ts | 11 +- .../translations/translations/fr-FR.json | 10 - .../translations/translations/ja-JP.json | 10 - .../translations/translations/zh-CN.json | 10 - 42 files changed, 2 insertions(+), 4036 deletions(-) delete mode 100644 x-pack/plugins/timelines/common/search_strategy/timeline/events/common/index.ts delete mode 100644 x-pack/plugins/timelines/public/components/actions/action_icon_item.tsx delete mode 100644 x-pack/plugins/timelines/public/components/draggables/field_badge/index.tsx delete mode 100644 x-pack/plugins/timelines/public/components/draggables/field_badge/translations.ts delete mode 100644 x-pack/plugins/timelines/public/components/draggables/index.tsx delete mode 100644 x-pack/plugins/timelines/public/components/empty_value/__snapshots__/empty_value.test.tsx.snap delete mode 100644 x-pack/plugins/timelines/public/components/empty_value/translations.ts delete mode 100644 x-pack/plugins/timelines/public/components/exit_full_screen/index.test.tsx delete mode 100644 x-pack/plugins/timelines/public/components/exit_full_screen/index.tsx delete mode 100644 x-pack/plugins/timelines/public/components/exit_full_screen/translations.ts delete mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/column_headers/actions/index.tsx delete mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/column_headers/common/dragging_container.tsx delete mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/column_headers/common/styles.tsx delete mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header/__snapshots__/index.test.tsx.snap delete mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header/header_content.tsx delete mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header/helpers.ts delete mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header/index.test.tsx delete mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header/index.tsx delete mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header_tooltip_content/__snapshots__/index.test.tsx.snap delete mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header_tooltip_content/index.test.tsx delete mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header_tooltip_content/index.tsx delete mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/data_driven_columns/__snapshots__/index.test.tsx.snap delete mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/data_driven_columns/index.test.tsx delete mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/data_driven_columns/stateful_cell.test.tsx delete mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/data_driven_columns/stateful_cell.tsx delete mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/data_driven_columns/translations.ts delete mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/events/event_column_view.test.tsx delete mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/events/event_column_view.tsx delete mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/events/index.tsx delete mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/events/stateful_event.tsx diff --git a/x-pack/plugins/timelines/common/search_strategy/timeline/events/common/index.ts b/x-pack/plugins/timelines/common/search_strategy/timeline/events/common/index.ts deleted file mode 100644 index 4a5bd2c99a0eb..0000000000000 --- a/x-pack/plugins/timelines/common/search_strategy/timeline/events/common/index.ts +++ /dev/null @@ -1,26 +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 { Ecs } from '../../../../ecs'; -import { CursorType, Maybe } from '../../../common'; - -export interface TimelineEdges { - node: TimelineItem; - cursor: CursorType; -} - -export interface TimelineItem { - _id: string; - _index?: Maybe; - data: TimelineNonEcsData[]; - ecs: Ecs; -} - -export interface TimelineNonEcsData { - field: string; - value?: Maybe; -} diff --git a/x-pack/plugins/timelines/common/utility_types.ts b/x-pack/plugins/timelines/common/utility_types.ts index ccdbc254033fd..13be4c00102b9 100644 --- a/x-pack/plugins/timelines/common/utility_types.ts +++ b/x-pack/plugins/timelines/common/utility_types.ts @@ -6,13 +6,6 @@ */ import * as runtimeTypes from 'io-ts'; -import { ReactNode } from 'react'; - -// This type is for typing EuiDescriptionList -export interface DescriptionList { - title: NonNullable; - description: NonNullable; -} export const unionWithNullType = (type: T) => runtimeTypes.union([type, runtimeTypes.null]); diff --git a/x-pack/plugins/timelines/public/components/actions/action_icon_item.tsx b/x-pack/plugins/timelines/public/components/actions/action_icon_item.tsx deleted file mode 100644 index d979354e5548b..0000000000000 --- a/x-pack/plugins/timelines/public/components/actions/action_icon_item.tsx +++ /dev/null @@ -1,54 +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 React, { MouseEvent } from 'react'; -import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; - -import { EventsTdContent } from '../t_grid/styles'; -import { DEFAULT_ACTION_BUTTON_WIDTH } from '../t_grid/body/constants'; - -interface ActionIconItemProps { - ariaLabel?: string; - width?: number; - dataTestSubj?: string; - content?: string; - iconType?: string; - isDisabled?: boolean; - onClick?: (event: MouseEvent) => void; - children?: React.ReactNode; -} - -const ActionIconItemComponent: React.FC = ({ - width = DEFAULT_ACTION_BUTTON_WIDTH, - dataTestSubj, - content, - ariaLabel, - iconType = '', - isDisabled = false, - onClick, - children, -}) => ( -
- - {children ?? ( - - - - )} - -
-); - -ActionIconItemComponent.displayName = 'ActionIconItemComponent'; - -export const ActionIconItem = React.memo(ActionIconItemComponent); diff --git a/x-pack/plugins/timelines/public/components/draggables/field_badge/index.tsx b/x-pack/plugins/timelines/public/components/draggables/field_badge/index.tsx deleted file mode 100644 index 62f7e091fae9c..0000000000000 --- a/x-pack/plugins/timelines/public/components/draggables/field_badge/index.tsx +++ /dev/null @@ -1,48 +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 { rgba } from 'polished'; -import React from 'react'; -import styled from 'styled-components'; - -interface WidthProp { - width?: number; -} - -const Field = styled.div.attrs(({ width }) => { - if (width) { - return { - style: { - width: `${width}px`, - }, - }; - } -})` - background-color: ${({ theme }) => theme.eui.euiColorEmptyShade}; - border: ${({ theme }) => theme.eui.euiBorderThin}; - box-shadow: 0 2px 2px -1px ${({ theme }) => rgba(theme.eui.euiColorMediumShade, 0.3)}, - 0 1px 5px -2px ${({ theme }) => rgba(theme.eui.euiColorMediumShade, 0.3)}; - font-size: ${({ theme }) => theme.eui.euiFontSizeXS}; - font-weight: ${({ theme }) => theme.eui.euiFontWeightSemiBold}; - line-height: ${({ theme }) => theme.eui.euiLineHeight}; - padding: ${({ theme }) => theme.eui.paddingSizes.xs}; -`; -Field.displayName = 'Field'; - -/** - * Renders a field (e.g. `event.action`) as a draggable badge - */ - -export const DraggableFieldBadge = React.memo<{ fieldId: string; fieldWidth?: number }>( - ({ fieldId, fieldWidth }) => ( - - {fieldId} - - ) -); - -DraggableFieldBadge.displayName = 'DraggableFieldBadge'; diff --git a/x-pack/plugins/timelines/public/components/draggables/field_badge/translations.ts b/x-pack/plugins/timelines/public/components/draggables/field_badge/translations.ts deleted file mode 100644 index 6c8143c228e14..0000000000000 --- a/x-pack/plugins/timelines/public/components/draggables/field_badge/translations.ts +++ /dev/null @@ -1,34 +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 { i18n } from '@kbn/i18n'; - -export const CATEGORY = i18n.translate('xpack.timelines.draggables.field.categoryLabel', { - defaultMessage: 'Category', -}); - -export const COPY_TO_CLIPBOARD = i18n.translate( - 'xpack.timelines.eventDetails.copyToClipboardTooltip', - { - defaultMessage: 'Copy to Clipboard', - } -); - -export const FIELD = i18n.translate('xpack.timelines.draggables.field.fieldLabel', { - defaultMessage: 'Field', -}); - -export const TYPE = i18n.translate('xpack.timelines.draggables.field.typeLabel', { - defaultMessage: 'Type', -}); - -export const VIEW_CATEGORY = i18n.translate( - 'xpack.timelines.draggables.field.viewCategoryTooltip', - { - defaultMessage: 'View Category', - } -); diff --git a/x-pack/plugins/timelines/public/components/draggables/index.tsx b/x-pack/plugins/timelines/public/components/draggables/index.tsx deleted file mode 100644 index a87d97b7ea74a..0000000000000 --- a/x-pack/plugins/timelines/public/components/draggables/index.tsx +++ /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 './field_badge'; diff --git a/x-pack/plugins/timelines/public/components/empty_value/__snapshots__/empty_value.test.tsx.snap b/x-pack/plugins/timelines/public/components/empty_value/__snapshots__/empty_value.test.tsx.snap deleted file mode 100644 index 142ed7a0d7175..0000000000000 --- a/x-pack/plugins/timelines/public/components/empty_value/__snapshots__/empty_value.test.tsx.snap +++ /dev/null @@ -1,7 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`EmptyValue it renders against snapshot 1`] = ` -

- (Empty String) -

-`; diff --git a/x-pack/plugins/timelines/public/components/empty_value/empty_value.test.tsx b/x-pack/plugins/timelines/public/components/empty_value/empty_value.test.tsx index 0e5ee638d29e8..78f0ec9f42712 100644 --- a/x-pack/plugins/timelines/public/components/empty_value/empty_value.test.tsx +++ b/x-pack/plugins/timelines/public/components/empty_value/empty_value.test.tsx @@ -5,162 +5,10 @@ * 2.0. */ -import { mount, shallow } from 'enzyme'; -import React from 'react'; -import { ThemeProvider } from 'styled-components'; -import { mountWithIntl } from '@kbn/test-jest-helpers'; - -import { - defaultToEmptyTag, - getEmptyString, - getEmptyStringTag, - getEmptyTagValue, - getEmptyValue, - getOrEmptyTag, -} from '.'; -import { getMockTheme } from '../../mock/kibana_react.mock'; +import { getEmptyValue } from '.'; describe('EmptyValue', () => { - const mockTheme = getMockTheme({ eui: { euiColorMediumShade: '#ece' } }); - - test('it renders against snapshot', () => { - const wrapper = shallow(

{getEmptyString()}

); - expect(wrapper).toMatchSnapshot(); - }); - describe('#getEmptyValue', () => { test('should return an empty value', () => expect(getEmptyValue()).toBe('—')); }); - - describe('#getEmptyString', () => { - test('should turn into an empty string place holder', () => { - const wrapper = mountWithIntl( - -

{getEmptyString()}

-
- ); - expect(wrapper.text()).toBe('(Empty String)'); - }); - }); - - describe('#getEmptyTagValue', () => { - const wrapper = mount( - -

{getEmptyTagValue()}

-
- ); - test('should return an empty tag value', () => expect(wrapper.text()).toBe('—')); - }); - - describe('#getEmptyStringTag', () => { - test('should turn into an span that has length of 1', () => { - const wrapper = mountWithIntl( - -

{getEmptyStringTag()}

-
- ); - expect(wrapper.find('span')).toHaveLength(1); - }); - - test('should turn into an empty string tag place holder', () => { - const wrapper = mountWithIntl( - -

{getEmptyStringTag()}

-
- ); - expect(wrapper.text()).toBe(getEmptyString()); - }); - }); - - describe('#defaultToEmptyTag', () => { - test('should default to an empty value when a value is null', () => { - const wrapper = mount( - -

{defaultToEmptyTag(null)}

-
- ); - expect(wrapper.text()).toBe(getEmptyValue()); - }); - - test('should default to an empty value when a value is undefined', () => { - const wrapper = mount( - -

{defaultToEmptyTag(undefined)}

-
- ); - expect(wrapper.text()).toBe(getEmptyValue()); - }); - - test('should return a deep path value', () => { - const test = { - a: { - b: { - c: 1, - }, - }, - }; - const wrapper = mount(

{defaultToEmptyTag(test.a.b.c)}

); - expect(wrapper.text()).toBe('1'); - }); - }); - - describe('#getOrEmptyTag', () => { - test('should default empty value when a deep rooted value is null', () => { - const test = { - a: { - b: { - c: null, - }, - }, - }; - const wrapper = mount( - -

{getOrEmptyTag('a.b.c', test)}

-
- ); - expect(wrapper.text()).toBe(getEmptyValue()); - }); - - test('should default empty value when a deep rooted value is undefined', () => { - const test = { - a: { - b: { - c: undefined, - }, - }, - }; - const wrapper = mount( - -

{getOrEmptyTag('a.b.c', test)}

-
- ); - expect(wrapper.text()).toBe(getEmptyValue()); - }); - - test('should default empty value when a deep rooted value is missing', () => { - const test = { - a: { - b: {}, - }, - }; - const wrapper = mount( - -

{getOrEmptyTag('a.b.c', test)}

-
- ); - expect(wrapper.text()).toBe(getEmptyValue()); - }); - - test('should return a deep path value', () => { - const test = { - a: { - b: { - c: 1, - }, - }, - }; - const wrapper = mount(

{getOrEmptyTag('a.b.c', test)}

); - expect(wrapper.text()).toBe('1'); - }); - }); }); diff --git a/x-pack/plugins/timelines/public/components/empty_value/index.tsx b/x-pack/plugins/timelines/public/components/empty_value/index.tsx index 86efb4a78277a..9de9e22eb5b89 100644 --- a/x-pack/plugins/timelines/public/components/empty_value/index.tsx +++ b/x-pack/plugins/timelines/public/components/empty_value/index.tsx @@ -5,45 +5,4 @@ * 2.0. */ -import { get, isString } from 'lodash/fp'; -import React from 'react'; -import styled from 'styled-components'; - -import * as i18n from './translations'; - -const EmptyWrapper = styled.span` - color: ${(props) => props.theme.eui.euiColorMediumShade}; -`; - -EmptyWrapper.displayName = 'EmptyWrapper'; - export const getEmptyValue = () => '—'; -export const getEmptyString = () => `(${i18n.EMPTY_STRING})`; - -export const getEmptyTagValue = () => {getEmptyValue()}; -export const getEmptyStringTag = () => {getEmptyString()}; - -export const defaultToEmptyTag = (item: T): JSX.Element => { - if (item == null) { - return getEmptyTagValue(); - } else if (isString(item) && item === '') { - return getEmptyStringTag(); - } else { - return <>{item}; - } -}; - -export const getOrEmptyTag = (path: string, item: unknown): JSX.Element => { - const text = get(path, item); - return getOrEmptyTagFromValue(text); -}; - -export const getOrEmptyTagFromValue = (value: string | number | null | undefined): JSX.Element => { - if (value == null) { - return getEmptyTagValue(); - } else if (value === '') { - return getEmptyStringTag(); - } else { - return <>{value}; - } -}; diff --git a/x-pack/plugins/timelines/public/components/empty_value/translations.ts b/x-pack/plugins/timelines/public/components/empty_value/translations.ts deleted file mode 100644 index 20c822c67dfb3..0000000000000 --- a/x-pack/plugins/timelines/public/components/empty_value/translations.ts +++ /dev/null @@ -1,12 +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 { i18n } from '@kbn/i18n'; - -export const EMPTY_STRING = i18n.translate('xpack.timelines.emptyString.emptyStringDescription', { - defaultMessage: 'Empty String', -}); diff --git a/x-pack/plugins/timelines/public/components/exit_full_screen/index.test.tsx b/x-pack/plugins/timelines/public/components/exit_full_screen/index.test.tsx deleted file mode 100644 index b60bdafd0835f..0000000000000 --- a/x-pack/plugins/timelines/public/components/exit_full_screen/index.test.tsx +++ /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 { mount } from 'enzyme'; -import React from 'react'; - -import { TestProviders } from '../../mock/test_providers'; -import * as i18n from './translations'; -import { ExitFullScreen, EXIT_FULL_SCREEN_CLASS_NAME } from '.'; - -describe('ExitFullScreen', () => { - test('it returns null when fullScreen is false', () => { - const exitFullScreen = mount( - - - - ); - - expect(exitFullScreen.find('[data-test-subj="exit-full-screen"]').exists()).toBe(false); - }); - - test('it renders a button with the exported EXIT_FULL_SCREEN_CLASS_NAME class when fullScreen is true', () => { - const exitFullScreen = mount( - - - - ); - - expect(exitFullScreen.find(`button.${EXIT_FULL_SCREEN_CLASS_NAME}`).exists()).toBe(true); - }); - - test('it renders the expected button text when fullScreen is true', () => { - const exitFullScreen = mount( - - - - ); - - expect(exitFullScreen.find('[data-test-subj="exit-full-screen"]').first().text()).toBe( - i18n.EXIT_FULL_SCREEN - ); - }); - - test('it invokes setFullScreen with a value of false when the button is clicked', () => { - const setFullScreen = jest.fn(); - - const exitFullScreen = mount( - - - - ); - - exitFullScreen.find('[data-test-subj="exit-full-screen"]').first().simulate('click'); - expect(setFullScreen).toBeCalledWith(false); - }); -}); diff --git a/x-pack/plugins/timelines/public/components/exit_full_screen/index.tsx b/x-pack/plugins/timelines/public/components/exit_full_screen/index.tsx deleted file mode 100644 index 5ae537128bee6..0000000000000 --- a/x-pack/plugins/timelines/public/components/exit_full_screen/index.tsx +++ /dev/null @@ -1,64 +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 { EuiButton, EuiWindowEvent } from '@elastic/eui'; -import React, { useCallback } from 'react'; -import styled from 'styled-components'; - -import * as i18n from './translations'; - -export const EXIT_FULL_SCREEN_CLASS_NAME = 'exit-full-screen'; - -const StyledEuiButton = styled(EuiButton)` - margin: ${({ theme }) => theme.eui.paddingSizes.s}; -`; - -interface Props { - fullScreen: boolean; - setFullScreen: (fullScreen: boolean) => void; -} - -const ExitFullScreenComponent: React.FC = ({ fullScreen, setFullScreen }) => { - const exitFullScreen = useCallback(() => { - setFullScreen(false); - }, [setFullScreen]); - - const onKeyDown = useCallback( - (event: KeyboardEvent) => { - if (event.key === 'Escape') { - event.preventDefault(); - - exitFullScreen(); - } - }, - [exitFullScreen] - ); - - if (!fullScreen) { - return null; - } - - return ( - <> - - - {i18n.EXIT_FULL_SCREEN} - - - ); -}; - -ExitFullScreenComponent.displayName = 'ExitFullScreenComponent'; - -export const ExitFullScreen = React.memo(ExitFullScreenComponent); diff --git a/x-pack/plugins/timelines/public/components/exit_full_screen/translations.ts b/x-pack/plugins/timelines/public/components/exit_full_screen/translations.ts deleted file mode 100644 index 22aecebf12a07..0000000000000 --- a/x-pack/plugins/timelines/public/components/exit_full_screen/translations.ts +++ /dev/null @@ -1,12 +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 { i18n } from '@kbn/i18n'; - -export const EXIT_FULL_SCREEN = i18n.translate('xpack.timelines.exitFullScreenButton', { - defaultMessage: 'Exit full screen', -}); diff --git a/x-pack/plugins/timelines/public/components/index.tsx b/x-pack/plugins/timelines/public/components/index.tsx index ea1e6fdcf3b6d..c2ba5202aa76b 100644 --- a/x-pack/plugins/timelines/public/components/index.tsx +++ b/x-pack/plugins/timelines/public/components/index.tsx @@ -56,7 +56,6 @@ export const TGrid = (props: TGridComponent) => { export { TGrid as default }; export * from './drag_and_drop'; -export * from './draggables'; export * from './last_updated'; export * from './loading'; export * from './fields_browser'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/actions/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/actions/index.tsx deleted file mode 100644 index 322059576d2b7..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/actions/index.tsx +++ /dev/null @@ -1,69 +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 { EuiButtonIcon } from '@elastic/eui'; -import React, { useCallback } from 'react'; -import type { ColumnHeaderOptions } from '../../../../../../common/types/timeline'; -import { EventsHeadingExtra, EventsLoading } from '../../../styles'; -import type { OnColumnRemoved } from '../../../types'; -import type { Sort } from '../../sort'; - -import * as i18n from '../translations'; - -interface Props { - header: ColumnHeaderOptions; - isLoading: boolean; - onColumnRemoved: OnColumnRemoved; - sort: Sort[]; -} - -/** Given a `header`, returns the `SortDirection` applicable to it */ - -export const CloseButton = React.memo<{ - columnId: string; - onColumnRemoved: OnColumnRemoved; -}>(({ columnId, onColumnRemoved }) => { - const handleClick = useCallback( - (event: React.MouseEvent) => { - // To avoid a re-sorting when you delete a column - event.preventDefault(); - event.stopPropagation(); - onColumnRemoved(columnId); - }, - [columnId, onColumnRemoved] - ); - - return ( - - ); -}); - -CloseButton.displayName = 'CloseButton'; - -export const Actions = React.memo(({ header, onColumnRemoved, sort, isLoading }) => { - return ( - <> - {sort.some((i) => i.columnId === header.id) && isLoading ? ( - - - - ) : ( - - - - )} - - ); -}); - -Actions.displayName = 'Actions'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/common/dragging_container.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/common/dragging_container.tsx deleted file mode 100644 index 0d7ed0a91121e..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/common/dragging_container.tsx +++ /dev/null @@ -1,25 +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 { FC, memo, useEffect } from 'react'; - -interface DraggingContainerProps { - children: JSX.Element; - onDragging: Function; -} - -const DraggingContainerComponent: FC = ({ children, onDragging }) => { - useEffect(() => { - onDragging(true); - - return () => onDragging(false); - }); - - return children; -}; - -export const DraggingContainer = memo(DraggingContainerComponent); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/common/styles.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/common/styles.tsx deleted file mode 100644 index 254c7076fcf5a..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/common/styles.tsx +++ /dev/null @@ -1,19 +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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import styled from 'styled-components'; - -export const FullHeightFlexGroup = styled(EuiFlexGroup)` - height: 100%; -`; -FullHeightFlexGroup.displayName = 'FullHeightFlexGroup'; - -export const FullHeightFlexItem = styled(EuiFlexItem)` - height: 100%; -`; -FullHeightFlexItem.displayName = 'FullHeightFlexItem'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header/__snapshots__/index.test.tsx.snap b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header/__snapshots__/index.test.tsx.snap deleted file mode 100644 index ff2bdf2f643a0..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,51 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Header renders correctly against snapshot 1`] = ` - - - - - -`; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header/header_content.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header/header_content.tsx deleted file mode 100644 index 04004b3e90314..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header/header_content.tsx +++ /dev/null @@ -1,85 +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 { EuiToolTip } from '@elastic/eui'; -import { noop } from 'lodash/fp'; -import React from 'react'; - -import type { ColumnHeaderOptions } from '../../../../../../common/types/timeline'; -import { TruncatableText } from '../../../../truncatable_text'; - -import { EventsHeading, EventsHeadingTitleButton, EventsHeadingTitleSpan } from '../../../styles'; -import { Sort } from '../../sort'; -import { SortIndicator } from '../../sort/sort_indicator'; -import { HeaderToolTipContent } from '../header_tooltip_content'; -import { getSortDirection, getSortIndex } from './helpers'; -interface HeaderContentProps { - children: React.ReactNode; - header: ColumnHeaderOptions; - isLoading: boolean; - isResizing: boolean; - onClick: () => void; - showSortingCapability: boolean; - sort: Sort[]; -} - -const HeaderContentComponent: React.FC = ({ - children, - header, - isLoading, - isResizing, - onClick, - showSortingCapability, - sort, -}) => ( - - {header.aggregatable && showSortingCapability ? ( - - - } - > - <> - {React.isValidElement(header.display) - ? header.display - : header.displayAsText ?? header.id} - - - - - - - ) : ( - - - } - > - <> - {React.isValidElement(header.display) - ? header.display - : header.displayAsText ?? header.id} - - - - - )} - - {children} - -); - -export const HeaderContent = React.memo(HeaderContentComponent); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header/helpers.ts b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header/helpers.ts deleted file mode 100644 index 7a3cad47bdcba..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header/helpers.ts +++ /dev/null @@ -1,54 +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 { Direction } from '../../../../../../common/search_strategy'; -import type { ColumnHeaderOptions } from '../../../../../../common/types'; -import { assertUnreachable } from '../../../../../../common/utility_types'; -import { Sort, SortDirection } from '../../sort'; - -interface GetNewSortDirectionOnClickParams { - clickedHeader: ColumnHeaderOptions; - currentSort: Sort[]; -} - -/** Given a `header`, returns the `SortDirection` applicable to it */ -export const getNewSortDirectionOnClick = ({ - clickedHeader, - currentSort, -}: GetNewSortDirectionOnClickParams): Direction => - currentSort.reduce( - (acc, item) => (clickedHeader.id === item.columnId ? getNextSortDirection(item) : acc), - Direction.desc - ); - -/** Given a current sort direction, it returns the next sort direction */ -export const getNextSortDirection = (currentSort: Sort): Direction => { - switch (currentSort.sortDirection) { - case Direction.desc: - return Direction.asc; - case Direction.asc: - return Direction.desc; - case 'none': - return Direction.desc; - default: - return assertUnreachable(currentSort.sortDirection as never, 'Unhandled sort direction'); - } -}; - -interface GetSortDirectionParams { - header: ColumnHeaderOptions; - sort: Sort[]; -} - -export const getSortDirection = ({ header, sort }: GetSortDirectionParams): SortDirection => - sort.reduce( - (acc, item) => (header.id === item.columnId ? item.sortDirection : acc), - 'none' - ); - -export const getSortIndex = ({ header, sort }: GetSortDirectionParams): number => - sort.findIndex((s) => s.columnId === header.id); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header/index.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header/index.test.tsx deleted file mode 100644 index 4685af483c21e..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header/index.test.tsx +++ /dev/null @@ -1,331 +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 { mount, shallow } from 'enzyme'; -import React from 'react'; - -import { Sort } from '../../sort'; -import { CloseButton } from '../actions'; -import { defaultHeaders } from '../default_headers'; - -import { HeaderComponent } from '.'; -import { getNewSortDirectionOnClick, getNextSortDirection, getSortDirection } from './helpers'; -import { Direction } from '../../../../../../common/search_strategy'; -import { TestProviders } from '../../../../../mock'; -import { tGridActions } from '../../../../../store/t_grid'; -import { mockGlobalState } from '../../../../../mock/global_state'; - -const mockDispatch = jest.fn(); -jest.mock('../../../../../hooks/use_selector', () => ({ - useShallowEqualSelector: () => mockGlobalState.timelineById.test, - useDeepEqualSelector: () => mockGlobalState.timelineById.test, -})); - -jest.mock('react-redux', () => { - const original = jest.requireActual('react-redux'); - - return { - ...original, - useSelector: jest.fn(), - useDispatch: () => mockDispatch, - }; -}); - -describe('Header', () => { - const columnHeader = defaultHeaders[0]; - const sort: Sort[] = [ - { - columnId: columnHeader.id, - columnType: columnHeader.type ?? 'number', - sortDirection: Direction.desc, - }, - ]; - const timelineId = 'test'; - - test('renders correctly against snapshot', () => { - const wrapper = shallow( - - - - ); - expect(wrapper.find('HeaderComponent').dive()).toMatchSnapshot(); - }); - - describe('rendering', () => { - test('it renders the header text', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper.find(`[data-test-subj="header-text-${columnHeader.id}"]`).first().text() - ).toEqual(columnHeader.id); - }); - - test('it renders the header text alias when displayAsText is provided', () => { - const displayAsText = 'Timestamp'; - const headerWithLabel = { ...columnHeader, displayAsText }; - const wrapper = mount( - - - - ); - - expect( - wrapper.find(`[data-test-subj="header-text-${columnHeader.id}"]`).first().text() - ).toEqual(displayAsText); - }); - - test('it renders the header as a `ReactNode` when `display` is provided', () => { - const display: React.ReactNode = ( -
- {'The display property renders the column heading as a ReactNode'} -
- ); - const headerWithLabel = { ...columnHeader, display }; - const wrapper = mount( - - - - ); - - expect(wrapper.find(`[data-test-subj="rendered-via-display"]`).exists()).toBe(true); - }); - - test('it prefers to render `display` instead of `displayAsText` when both are provided', () => { - const displayAsText = 'this text should NOT be rendered'; - const display: React.ReactNode = ( -
{'this text is rendered via display'}
- ); - const headerWithLabel = { ...columnHeader, display, displayAsText }; - const wrapper = mount( - - - - ); - - expect(wrapper.text()).toBe('this text is rendered via display'); - }); - - test('it falls back to rendering header.id when `display` is not a valid React node', () => { - const display = {}; // a plain object is NOT a `ReactNode` - const headerWithLabel = { ...columnHeader, display }; - const wrapper = mount( - - - - ); - - expect( - wrapper.find(`[data-test-subj="header-text-${columnHeader.id}"]`).first().text() - ).toEqual(columnHeader.id); - }); - - test('it renders a sort indicator', () => { - const headerSortable = { ...columnHeader, aggregatable: true }; - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="header-sort-indicator"]').first().exists()).toEqual( - true - ); - }); - }); - - describe('onColumnSorted', () => { - test('it invokes the onColumnSorted callback when the header sort button is clicked', () => { - const headerSortable = { ...columnHeader, aggregatable: true }; - const wrapper = mount( - - - - ); - - wrapper.find('[data-test-subj="header-sort-button"]').first().simulate('click'); - - expect(mockDispatch).toBeCalledWith( - tGridActions.updateSort({ - id: timelineId, - sort: [ - { - columnId: columnHeader.id, - columnType: columnHeader.type ?? 'number', - sortDirection: Direction.asc, // (because the previous state was Direction.desc) - }, - ], - }) - ); - }); - - test('it does NOT render the header sort button when aggregatable is false', () => { - const headerSortable = { ...columnHeader, aggregatable: false }; - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="header-sort-button"]').length).toEqual(0); - }); - - test('it does NOT render the header sort button when aggregatable is missing', () => { - const headerSortable = { ...columnHeader }; - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="header-sort-button"]').length).toEqual(0); - }); - - test('it does NOT invoke the onColumnSorted callback when the header is clicked and aggregatable is undefined', () => { - const mockOnColumnSorted = jest.fn(); - const headerSortable = { ...columnHeader, aggregatable: undefined }; - const wrapper = mount( - - - - ); - - wrapper.find(`[data-test-subj="header-${columnHeader.id}"]`).first().simulate('click'); - - expect(mockOnColumnSorted).not.toHaveBeenCalled(); - }); - }); - - describe('CloseButton', () => { - test('it invokes the onColumnRemoved callback with the column ID when the close button is clicked', () => { - const mockOnColumnRemoved = jest.fn(); - - const wrapper = mount( - - ); - - wrapper.find('[data-test-subj="remove-column"]').first().simulate('click'); - - expect(mockOnColumnRemoved).toBeCalledWith(columnHeader.id); - }); - }); - - describe('getSortDirection', () => { - test('it returns the sort direction when the header id matches the sort column id', () => { - expect(getSortDirection({ header: columnHeader, sort })).toEqual(sort[0].sortDirection); - }); - - test('it returns "none" when sort direction when the header id does NOT match the sort column id', () => { - const nonMatching: Sort[] = [ - { - columnId: 'differentSocks', - columnType: columnHeader.type ?? 'number', - sortDirection: Direction.desc, - }, - ]; - - expect(getSortDirection({ header: columnHeader, sort: nonMatching })).toEqual('none'); - }); - }); - - describe('getNextSortDirection', () => { - test('it returns "asc" when the current direction is "desc"', () => { - const sortDescending: Sort = { - columnId: columnHeader.id, - columnType: columnHeader.type ?? 'number', - sortDirection: Direction.desc, - }; - - expect(getNextSortDirection(sortDescending)).toEqual('asc'); - }); - - test('it returns "desc" when the current direction is "asc"', () => { - const sortAscending: Sort = { - columnId: columnHeader.id, - columnType: columnHeader.type ?? 'number', - sortDirection: Direction.asc, - }; - - expect(getNextSortDirection(sortAscending)).toEqual(Direction.desc); - }); - - test('it returns "desc" by default', () => { - const sortNone: Sort = { - columnId: columnHeader.id, - columnType: columnHeader.type ?? 'number', - sortDirection: 'none', - }; - - expect(getNextSortDirection(sortNone)).toEqual(Direction.desc); - }); - }); - - describe('getNewSortDirectionOnClick', () => { - test('it returns the expected new sort direction when the header id matches the sort column id', () => { - const sortMatches: Sort[] = [ - { - columnId: columnHeader.id, - columnType: columnHeader.type ?? 'number', - sortDirection: Direction.desc, - }, - ]; - - expect( - getNewSortDirectionOnClick({ - clickedHeader: columnHeader, - currentSort: sortMatches, - }) - ).toEqual(Direction.asc); - }); - - test('it returns the expected new sort direction when the header id does NOT match the sort column id', () => { - const sortDoesNotMatch: Sort[] = [ - { - columnId: 'someOtherColumn', - columnType: columnHeader.type ?? 'number', - sortDirection: 'none', - }, - ]; - - expect( - getNewSortDirectionOnClick({ - clickedHeader: columnHeader, - currentSort: sortDoesNotMatch, - }) - ).toEqual(Direction.desc); - }); - }); - - describe('text truncation styling', () => { - test('truncates the header text with an ellipsis', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper.find(`[data-test-subj="header-text-${columnHeader.id}"]`).at(1) - ).toHaveStyleRule('text-overflow', 'ellipsis'); - }); - }); - - describe('header tooltip', () => { - test('it has a tooltip to display the properties of the field', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="header-tooltip"]').exists()).toEqual(true); - }); - }); -}); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header/index.tsx deleted file mode 100644 index 1ddade2b58968..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header/index.tsx +++ /dev/null @@ -1,95 +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 React, { useCallback, useMemo } from 'react'; -import { useDispatch } from 'react-redux'; -import { isDataViewFieldSubtypeNested } from '@kbn/es-query'; - -import type { ColumnHeaderOptions } from '../../../../../../common/types/timeline'; -import type { Sort } from '../../sort'; -import { Actions } from '../actions'; -import { getNewSortDirectionOnClick } from './helpers'; -import { HeaderContent } from './header_content'; -import { tGridActions, tGridSelectors } from '../../../../../store/t_grid'; -import { useDeepEqualSelector } from '../../../../../hooks/use_selector'; -interface Props { - header: ColumnHeaderOptions; - sort: Sort[]; - timelineId: string; -} - -export const HeaderComponent: React.FC = ({ header, sort, timelineId }) => { - const dispatch = useDispatch(); - - const onColumnSort = useCallback(() => { - const columnId = header.id; - const columnType = header.type ?? 'text'; - const sortDirection = getNewSortDirectionOnClick({ - clickedHeader: header, - currentSort: sort, - }); - const headerIndex = sort.findIndex((col) => col.columnId === columnId); - let newSort = []; - if (headerIndex === -1) { - newSort = [ - ...sort, - { - columnId, - columnType, - sortDirection, - }, - ]; - } else { - newSort = [ - ...sort.slice(0, headerIndex), - { - columnId, - columnType, - sortDirection, - }, - ...sort.slice(headerIndex + 1), - ]; - } - dispatch( - tGridActions.updateSort({ - id: timelineId, - sort: newSort, - }) - ); - }, [dispatch, header, sort, timelineId]); - - const onColumnRemoved = useCallback( - (columnId) => dispatch(tGridActions.removeColumn({ id: timelineId, columnId })), - [dispatch, timelineId] - ); - - const getManageTimeline = useMemo(() => tGridSelectors.getManageTimelineById(), []); - const { isLoading } = useDeepEqualSelector((state) => getManageTimeline(state, timelineId ?? '')); - const showSortingCapability = !isDataViewFieldSubtypeNested(header); - - return ( - <> - - - - - ); -}; - -export const Header = React.memo(HeaderComponent); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header_tooltip_content/__snapshots__/index.test.tsx.snap b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header_tooltip_content/__snapshots__/index.test.tsx.snap deleted file mode 100644 index 945a9a7aee698..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header_tooltip_content/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,66 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`HeaderToolTipContent it renders the expected table content 1`] = ` - -

- - Category - : - - - base - -

-

- - Field - : - - - @timestamp - -

-

- - Type - : - - - - - date - - -

-

- - Description - : - - - Date/time when the event originated. -For log events this is the date/time when the event was generated, and not when it was read. -Required field for all events. - -

-
-`; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header_tooltip_content/index.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header_tooltip_content/index.test.tsx deleted file mode 100644 index a38261994267c..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header_tooltip_content/index.test.tsx +++ /dev/null @@ -1,72 +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 { mount, shallow } from 'enzyme'; -import { cloneDeep } from 'lodash/fp'; -import React from 'react'; - -import { ColumnHeaderOptions } from '../../../../../../common/types/timeline'; -import { HeaderToolTipContent } from '.'; -import { defaultHeaders } from '../../../../../mock/header'; - -describe('HeaderToolTipContent', () => { - let header: ColumnHeaderOptions; - beforeEach(() => { - header = cloneDeep(defaultHeaders[0]); - }); - - test('it renders the category', () => { - const wrapper = mount(); - - expect(wrapper.find('[data-test-subj="category-value"]').first().text()).toEqual( - header.category - ); - }); - - test('it renders the name of the field', () => { - const wrapper = mount(); - - expect(wrapper.find('[data-test-subj="field-value"]').first().text()).toEqual(header.id); - }); - - test('it renders the expected icon for the header type', () => { - const wrapper = mount(); - - expect(wrapper.find('[data-test-subj="type-icon"]').first().props().type).toEqual('clock'); - }); - - test('it renders the type of the field', () => { - const wrapper = mount(); - - expect(wrapper.find('[data-test-subj="type-value"]').first().text()).toEqual(header.type); - }); - - test('it renders the description of the field', () => { - const wrapper = mount(); - - expect(wrapper.find('[data-test-subj="description-value"]').first().text()).toEqual( - header.description - ); - }); - - test('it does NOT render the description column when the field does NOT contain a description', () => { - const noDescription = { - ...header, - description: '', - }; - - const wrapper = mount(); - - expect(wrapper.find('[data-test-subj="description"]').exists()).toEqual(false); - }); - - test('it renders the expected table content', () => { - const wrapper = shallow(); - - expect(wrapper).toMatchSnapshot(); - }); -}); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header_tooltip_content/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header_tooltip_content/index.tsx deleted file mode 100644 index 91dd64d8fed3a..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header_tooltip_content/index.tsx +++ /dev/null @@ -1,81 +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 { EuiIcon } from '@elastic/eui'; -import { isEmpty } from 'lodash/fp'; -import React from 'react'; -import styled from 'styled-components'; - -import type { ColumnHeaderOptions } from '../../../../../../common/types/timeline'; -import { getIconFromType } from '../../../../utils/helpers'; -import * as i18n from '../translations'; - -const IconType = styled(EuiIcon)` - margin-right: 3px; - position: relative; - top: -2px; -`; -IconType.displayName = 'IconType'; - -const P = styled.span` - margin-bottom: 5px; -`; -P.displayName = 'P'; - -const ToolTipTableMetadata = styled.span` - margin-right: 5px; - display: block; -`; -ToolTipTableMetadata.displayName = 'ToolTipTableMetadata'; - -const ToolTipTableValue = styled.span` - word-wrap: break-word; -`; -ToolTipTableValue.displayName = 'ToolTipTableValue'; - -export const HeaderToolTipContent = React.memo<{ header: ColumnHeaderOptions }>(({ header }) => ( - <> - {!isEmpty(header.category) && ( -

- - {i18n.CATEGORY} - {':'} - - {header.category} -

- )} -

- - {i18n.FIELD} - {':'} - - {header.id} -

-

- - {i18n.TYPE} - {':'} - - - - {header.type} - -

- {!isEmpty(header.description) && ( -

- - {i18n.DESCRIPTION} - {':'} - - - {header.description} - -

- )} - -)); -HeaderToolTipContent.displayName = 'HeaderToolTipContent'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/constants.ts b/x-pack/plugins/timelines/public/components/t_grid/body/constants.ts index 3f1889732483d..0af2efb593f55 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/constants.ts +++ b/x-pack/plugins/timelines/public/components/t_grid/body/constants.ts @@ -24,14 +24,8 @@ import { euiThemeVars } from '@kbn/ui-theme'; export const DEFAULT_ACTION_BUTTON_WIDTH = parseInt(euiThemeVars.euiSizeXL, 10) - parseInt(euiThemeVars.euiSizeXS, 10); // px -/** Additional column width to include when checkboxes are shown **/ -export const SHOW_CHECK_BOXES_COLUMN_WIDTH = 24; // px; - /** The default minimum width of a column (when a width for the column type is not specified) */ export const DEFAULT_COLUMN_MIN_WIDTH = 180; // px -/** The minimum width of a resized column */ -export const RESIZED_COLUMN_MIN_WITH = 70; // px - /** The default minimum width of a column of type `date` */ export const DEFAULT_DATE_COLUMN_MIN_WIDTH = 190; // px diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/data_driven_columns/__snapshots__/index.test.tsx.snap b/x-pack/plugins/timelines/public/components/t_grid/body/data_driven_columns/__snapshots__/index.test.tsx.snap deleted file mode 100644 index cbec3a3baa695..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/body/data_driven_columns/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,967 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Columns it renders the expected columns 1`] = ` - - - - - - - - - -`; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/data_driven_columns/index.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/data_driven_columns/index.test.tsx deleted file mode 100644 index be7114be67b04..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/body/data_driven_columns/index.test.tsx +++ /dev/null @@ -1,59 +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 { shallow } from 'enzyme'; - -import React from 'react'; - -import { defaultHeaders } from '../column_headers/default_headers'; - -import { DataDrivenColumns } from '.'; -import { mockTimelineData } from '../../../../mock/mock_timeline_data'; -import { TestCellRenderer } from '../../../../mock/cell_renderer'; - -window.matchMedia = jest.fn().mockImplementation((query) => { - return { - matches: false, - media: query, - onchange: null, - addListener: jest.fn(), - removeListener: jest.fn(), - }; -}); - -describe('Columns', () => { - const headersSansTimestamp = defaultHeaders.filter((h) => h.id !== '@timestamp'); - - test('it renders the expected columns', () => { - const wrapper = shallow( - - ); - - expect(wrapper).toMatchSnapshot(); - }); -}); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/data_driven_columns/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/data_driven_columns/index.tsx index 935b1c2e1d469..570581bcb640e 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/data_driven_columns/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/data_driven_columns/index.tsx @@ -5,396 +5,7 @@ * 2.0. */ -import { EuiScreenReaderOnly } from '@elastic/eui'; -import React, { useMemo } from 'react'; -import { getOr } from 'lodash/fp'; - -import { DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME } from '@kbn/securitysolution-t-grid'; -import { OnRowSelected } from '../../types'; - -import { - EventsTd, - EVENTS_TD_CLASS_NAME, - EventsTdContent, - EventsTdGroupData, - EventsTdGroupActions, -} from '../../styles'; - -import { StatefulCell } from './stateful_cell'; -import * as i18n from './translations'; -import { - SetEventsDeleted, - SetEventsLoading, - TimelineTabs, -} from '../../../../../common/types/timeline'; -import type { - ActionProps, - CellValueElementProps, - ColumnHeaderOptions, - ControlColumnProps, - RowCellRender, -} from '../../../../../common/types/timeline'; import type { TimelineNonEcsData } from '../../../../../common/search_strategy'; -import { ARIA_COLUMN_INDEX_OFFSET } from '../../helpers'; -import type { Ecs } from '../../../../../common/ecs'; - -interface CellProps { - _id: string; - ariaRowindex: number; - index: number; - header: ColumnHeaderOptions; - data: TimelineNonEcsData[]; - ecsData: Ecs; - hasRowRenderers: boolean; - renderCellValue: (props: CellValueElementProps) => React.ReactNode; - tabType?: TimelineTabs; - timelineId: string; -} - -interface DataDrivenColumnProps { - id: string; - actionsColumnWidth: number; - ariaRowindex: number; - checked: boolean; - columnHeaders: ColumnHeaderOptions[]; - columnValues: string; - data: TimelineNonEcsData[]; - ecsData: Ecs; - isEventViewer?: boolean; - loadingEventIds: Readonly; - onEventDetailsPanelOpened: () => void; - onRowSelected: OnRowSelected; - onRuleChange?: () => void; - hasRowRenderers: boolean; - selectedEventIds: Readonly>; - showCheckboxes: boolean; - renderCellValue: (props: CellValueElementProps) => React.ReactNode; - tabType?: TimelineTabs; - timelineId: string; - trailingControlColumns: ControlColumnProps[]; - leadingControlColumns: ControlColumnProps[]; - setEventsLoading: SetEventsLoading; - setEventsDeleted: SetEventsDeleted; -} - -const SPACE = ' '; - -export const shouldForwardKeyDownEvent = (key: string): boolean => { - switch (key) { - case SPACE: // fall through - case 'Enter': - return true; - default: - return false; - } -}; - -export const onKeyDown = (keyboardEvent: React.KeyboardEvent) => { - const { altKey, ctrlKey, key, metaKey, shiftKey, target, type } = keyboardEvent; - - const targetElement = target as Element; - - // we *only* forward the event to the (child) draggable keyboard wrapper - // if the keyboard event originated from the container (TD) element - if (shouldForwardKeyDownEvent(key) && targetElement.className?.includes(EVENTS_TD_CLASS_NAME)) { - const draggableKeyboardWrapper = targetElement.querySelector( - `.${DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME}` - ); - - const newEvent = new KeyboardEvent(type, { - altKey, - bubbles: true, - cancelable: true, - ctrlKey, - key, - metaKey, - shiftKey, - }); - - if (key === ' ') { - // prevent the default behavior of scrolling the table when space is pressed - keyboardEvent.preventDefault(); - } - - draggableKeyboardWrapper?.dispatchEvent(newEvent); - } -}; - -const TgridActionTdCell = ({ - action: Action, - width, - actionsColumnWidth, - ariaRowindex, - columnId, - columnValues, - data, - ecsData, - eventIdToNoteIds, - index, - isEventPinned, - isEventViewer, - eventId, - loadingEventIds, - onEventDetailsPanelOpened, - onRowSelected, - rowIndex, - hasRowRenderers, - onRuleChange, - selectedEventIds = {}, - showCheckboxes, - showNotes = false, - tabType, - timelineId, - toggleShowNotes, - setEventsLoading, - setEventsDeleted, -}: ActionProps & { - columnId: string; - hasRowRenderers: boolean; - actionsColumnWidth: number; - selectedEventIds: Readonly>; -}) => { - const displayWidth = width ? width : actionsColumnWidth; - return ( - - - - <> - -

{i18n.YOU_ARE_IN_A_TABLE_CELL({ row: ariaRowindex, column: index + 2 })}

-
- {Action && ( - - )} - -
- {hasRowRenderers ? ( - -

{i18n.EVENT_HAS_AN_EVENT_RENDERER(ariaRowindex)}

-
- ) : null} -
-
- ); -}; - -const TgridTdCell = ({ - _id, - ariaRowindex, - index, - header, - data, - ecsData, - hasRowRenderers, - renderCellValue, - tabType, - timelineId, -}: CellProps) => { - const ariaColIndex = index + ARIA_COLUMN_INDEX_OFFSET; - return ( - - - <> - -

{i18n.YOU_ARE_IN_A_TABLE_CELL({ row: ariaRowindex, column: ariaColIndex })}

-
- - -
- {hasRowRenderers ? ( - -

{i18n.EVENT_HAS_AN_EVENT_RENDERER(ariaRowindex)}

-
- ) : null} -
- ); -}; - -export const DataDrivenColumns = React.memo( - ({ - ariaRowindex, - actionsColumnWidth, - columnHeaders, - columnValues, - data, - ecsData, - isEventViewer, - id: _id, - loadingEventIds, - onEventDetailsPanelOpened, - onRowSelected, - hasRowRenderers, - onRuleChange, - renderCellValue, - selectedEventIds = {}, - showCheckboxes, - tabType, - timelineId, - trailingControlColumns, - leadingControlColumns, - setEventsLoading, - setEventsDeleted, - }) => { - const trailingActionCells = useMemo( - () => - trailingControlColumns ? trailingControlColumns.map((column) => column.rowCellRender) : [], - [trailingControlColumns] - ); - const leadingAndDataColumnCount = useMemo( - () => leadingControlColumns.length + columnHeaders.length, - [leadingControlColumns, columnHeaders] - ); - const TrailingActions = useMemo( - () => - trailingActionCells.map((Action: RowCellRender | undefined, index) => { - return ( - Action && ( - - ) - ); - }), - [ - trailingControlColumns, - _id, - data, - ecsData, - onRowSelected, - isEventViewer, - actionsColumnWidth, - ariaRowindex, - columnValues, - hasRowRenderers, - leadingAndDataColumnCount, - loadingEventIds, - onEventDetailsPanelOpened, - onRuleChange, - selectedEventIds, - showCheckboxes, - tabType, - timelineId, - trailingActionCells, - setEventsLoading, - setEventsDeleted, - ] - ); - const ColumnHeaders = useMemo( - () => - columnHeaders.map((header, index) => ( - - )), - [ - _id, - ariaRowindex, - columnHeaders, - data, - ecsData, - hasRowRenderers, - renderCellValue, - tabType, - timelineId, - ] - ); - return ( - - {ColumnHeaders} - {TrailingActions} - - ); - } -); - -DataDrivenColumns.displayName = 'DataDrivenColumns'; export const getMappedNonEcsValue = ({ data, diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/data_driven_columns/stateful_cell.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/data_driven_columns/stateful_cell.test.tsx deleted file mode 100644 index d79a59422ff43..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/body/data_driven_columns/stateful_cell.test.tsx +++ /dev/null @@ -1,180 +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 { mount } from 'enzyme'; -import { cloneDeep } from 'lodash/fp'; -import React, { useEffect } from 'react'; - -import { StatefulCell } from './stateful_cell'; -import { getMappedNonEcsValue } from '.'; -import { defaultHeaders } from '../../../../mock/header'; -import { - CellValueElementProps, - ColumnHeaderOptions, - TimelineTabs, -} from '../../../../../common/types/timeline'; -import { TimelineNonEcsData } from '../../../../../common/search_strategy'; -import { mockTimelineData } from '../../../../mock/mock_timeline_data'; - -/** - * This (test) component implement's `EuiDataGrid`'s `renderCellValue` interface, - * as documented here: https://elastic.github.io/eui/#/tabular-content/data-grid - * - * Its `CellValueElementProps` props are a superset of `EuiDataGridCellValueElementProps`. - * The `setCellProps` function, defined by the `EuiDataGridCellValueElementProps` interface, - * is typically called in a `useEffect`, as illustrated by `EuiDataGrid`'s code sandbox example: - * https://codesandbox.io/s/zhxmo - */ -const RenderCellValue: React.FC = ({ columnId, data, setCellProps }) => { - useEffect(() => { - // branching logic that conditionally renders a specific cell green: - if (columnId === defaultHeaders[0].id) { - const value = getMappedNonEcsValue({ - data, - fieldName: columnId, - }); - - if (value?.length) { - setCellProps({ - style: { - backgroundColor: 'green', - }, - }); - } - } - }, [columnId, data, setCellProps]); - - return ( -
- {getMappedNonEcsValue({ - data, - fieldName: columnId, - })} -
- ); -}; - -describe('StatefulCell', () => { - const rowIndex = 123; - const colIndex = 0; - const eventId = '_id-123'; - const linkValues = ['foo', 'bar', '@baz']; - const tabType = TimelineTabs.query; - const timelineId = 'test'; - - let header: ColumnHeaderOptions; - let data: TimelineNonEcsData[]; - beforeEach(() => { - data = cloneDeep(mockTimelineData[0].data); - header = cloneDeep(defaultHeaders[0]); - }); - - test('it invokes renderCellValue with the expected arguments when tabType is specified', () => { - const renderCellValue = jest.fn(); - - mount( - - ); - - expect(renderCellValue).toBeCalledWith( - expect.objectContaining({ - columnId: header.id, - eventId, - data, - header, - isExpandable: true, - isExpanded: false, - isDetails: false, - linkValues, - rowIndex, - colIndex, - timelineId: `${timelineId}-${tabType}`, - }) - ); - }); - - test('it invokes renderCellValue with the expected arguments when tabType is NOT specified', () => { - const renderCellValue = jest.fn(); - - mount( - - ); - - expect(renderCellValue).toBeCalledWith( - expect.objectContaining({ - columnId: header.id, - eventId, - data, - header, - isExpandable: true, - isExpanded: false, - isDetails: false, - linkValues, - rowIndex, - colIndex, - timelineId, - }) - ); - }); - - test('it renders the React.Node returned by renderCellValue', () => { - const renderCellValue = () =>
; - - const wrapper = mount( - - ); - - expect(wrapper.find('[data-test-subj="renderCellValue"]').exists()).toBe(true); - }); - - test("it renders a div with the styles set by `renderCellValue`'s `setCellProps` argument", () => { - const wrapper = mount( - - ); - - expect( - wrapper.find('[data-test-subj="statefulCell"]').getDOMNode().getAttribute('style') - ).toEqual('background-color: green;'); - }); -}); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/data_driven_columns/stateful_cell.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/data_driven_columns/stateful_cell.tsx deleted file mode 100644 index f66b4d88ba876..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/body/data_driven_columns/stateful_cell.tsx +++ /dev/null @@ -1,68 +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 React, { HTMLAttributes, useState } from 'react'; -import type { TimelineNonEcsData } from '../../../../../common/search_strategy'; - -import { TimelineTabs } from '../../../../../common/types/timeline'; -import type { - CellValueElementProps, - ColumnHeaderOptions, -} from '../../../../../common/types/timeline'; - -export interface CommonProps { - className?: string; - 'aria-label'?: string; - 'data-test-subj'?: string; -} - -const StatefulCellComponent = ({ - rowIndex, - colIndex, - data, - header, - eventId, - linkValues, - renderCellValue, - tabType, - timelineId, -}: { - rowIndex: number; - colIndex: number; - data: TimelineNonEcsData[]; - header: ColumnHeaderOptions; - eventId: string; - linkValues: string[] | undefined; - renderCellValue: (props: CellValueElementProps) => React.ReactNode; - tabType?: TimelineTabs; - timelineId: string; -}) => { - const [cellProps, setCellProps] = useState>({}); - return ( -
- {renderCellValue({ - columnId: header.id, - eventId, - data, - header, - isDraggable: true, - isExpandable: true, - isExpanded: false, - isDetails: false, - linkValues, - rowIndex, - colIndex, - setCellProps, - timelineId: tabType != null ? `${timelineId}-${tabType}` : timelineId, - })} -
- ); -}; - -StatefulCellComponent.displayName = 'StatefulCellComponent'; - -export const StatefulCell = React.memo(StatefulCellComponent); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/data_driven_columns/translations.ts b/x-pack/plugins/timelines/public/components/t_grid/body/data_driven_columns/translations.ts deleted file mode 100644 index 1e5b10bb7cbc2..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/body/data_driven_columns/translations.ts +++ /dev/null @@ -1,28 +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 { i18n } from '@kbn/i18n'; - -export const YOU_ARE_IN_A_TABLE_CELL = ({ column, row }: { column: number; row: number }) => - i18n.translate('xpack.timelines.timeline.youAreInATableCellScreenReaderOnly', { - values: { column, row }, - defaultMessage: 'You are in a table cell. row: {row}, column: {column}', - }); - -export const EVENT_HAS_AN_EVENT_RENDERER = (row: number) => - i18n.translate('xpack.timelines.timeline.eventHasEventRendererScreenReaderOnly', { - values: { row }, - defaultMessage: - 'The event in row {row} has an event renderer. Press shift + down arrow to focus it.', - }); - -export const EVENT_HAS_NOTES = ({ notesCount, row }: { notesCount: number; row: number }) => - i18n.translate('xpack.timelines.timeline.eventHasNotesScreenReaderOnly', { - values: { notesCount, row }, - defaultMessage: - 'The event in row {row} has {notesCount, plural, =1 {a note} other {{notesCount} notes}}. Press shift + right arrow to focus notes.', - }); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/events/event_column_view.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/events/event_column_view.test.tsx deleted file mode 100644 index 366108a19b873..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/body/events/event_column_view.test.tsx +++ /dev/null @@ -1,118 +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 { mount } from 'enzyme'; -import React from 'react'; - -import { getActionsColumnWidth } from '../column_headers/helpers'; - -import { EventColumnView } from './event_column_view'; -import { TestCellRenderer } from '../../../../mock/cell_renderer'; -import { TimelineId, TimelineTabs } from '../../../../../common/types/timeline'; -import { TestProviders } from '../../../../mock/test_providers'; -import { testLeadingControlColumn } from '../../../../mock/mock_timeline_control_columns'; -import { mockGlobalState } from '../../../../mock/global_state'; - -jest.mock('../../../../hooks/use_selector', () => ({ - useShallowEqualSelector: () => mockGlobalState.timelineById.test, - useDeepEqualSelector: () => mockGlobalState.timelineById.test, -})); - -describe('EventColumnView', () => { - const ACTION_BUTTON_COUNT = 4; - const props = { - ariaRowindex: 2, - id: 'event-id', - actionsColumnWidth: getActionsColumnWidth(ACTION_BUTTON_COUNT), - associateNote: jest.fn(), - columnHeaders: [], - columnRenderers: [], - data: [ - { - field: 'host.name', - }, - ], - ecsData: { - _id: 'id', - }, - eventIdToNoteIds: {}, - expanded: false, - hasRowRenderers: false, - loading: false, - loadingEventIds: [], - notesCount: 0, - onEventDetailsPanelOpened: jest.fn(), - onPinEvent: jest.fn(), - onRowSelected: jest.fn(), - onUnPinEvent: jest.fn(), - refetch: jest.fn(), - renderCellValue: TestCellRenderer, - selectedEventIds: {}, - showCheckboxes: false, - showNotes: false, - tabType: TimelineTabs.query, - timelineId: TimelineId.active, - toggleShowNotes: jest.fn(), - updateNote: jest.fn(), - isEventPinned: false, - leadingControlColumns: [], - trailingControlColumns: [], - setEventsLoading: jest.fn(), - setEventsDeleted: jest.fn(), - }; - - // TODO: next 3 tests will be re-enabled in the future. - test.skip('it render AddToCaseAction if timelineId === TimelineId.detectionsPage', () => { - const wrapper = mount(, { - wrappingComponent: TestProviders, - }); - - expect(wrapper.find('[data-test-subj="add-to-case-action"]').exists()).toBeTruthy(); - }); - - test.skip('it render AddToCaseAction if timelineId === TimelineId.detectionsRulesDetailsPage', () => { - const wrapper = mount( - , - { - wrappingComponent: TestProviders, - } - ); - - expect(wrapper.find('[data-test-subj="add-to-case-action"]').exists()).toBeTruthy(); - }); - - test.skip('it render AddToCaseAction if timelineId === TimelineId.active', () => { - const wrapper = mount(, { - wrappingComponent: TestProviders, - }); - - expect(wrapper.find('[data-test-subj="add-to-case-action"]').exists()).toBeTruthy(); - }); - - test.skip('it does NOT render AddToCaseAction when timelineId is not in the allowed list', () => { - const wrapper = mount(, { - wrappingComponent: TestProviders, - }); - - expect(wrapper.find('[data-test-subj="add-to-case-action"]').exists()).toBeFalsy(); - }); - - test('it renders a custom control column in addition to the default control column', () => { - const wrapper = mount( - , - { - wrappingComponent: TestProviders, - } - ); - - expect(wrapper.find('[data-test-subj="test-body-control-column-cell"]').exists()).toBeTruthy(); - }); -}); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/events/event_column_view.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/events/event_column_view.tsx deleted file mode 100644 index 31123476a9a38..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/body/events/event_column_view.tsx +++ /dev/null @@ -1,192 +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 React, { useMemo } from 'react'; - -import type { OnRowSelected } from '../../types'; -import { EventsTrData, EventsTdGroupActions } from '../../styles'; -import { DataDrivenColumns, getMappedNonEcsValue } from '../data_driven_columns'; -import { TimelineTabs } from '../../../../../common/types/timeline'; -import type { - CellValueElementProps, - ColumnHeaderOptions, - ControlColumnProps, - RowCellRender, - SetEventsDeleted, - SetEventsLoading, -} from '../../../../../common/types/timeline'; -import type { TimelineNonEcsData } from '../../../../../common/search_strategy'; -import type { Ecs } from '../../../../../common/ecs'; - -interface Props { - id: string; - actionsColumnWidth: number; - ariaRowindex: number; - columnHeaders: ColumnHeaderOptions[]; - data: TimelineNonEcsData[]; - ecsData: Ecs; - isEventViewer?: boolean; - loadingEventIds: Readonly; - onEventDetailsPanelOpened: () => void; - onRowSelected: OnRowSelected; - renderCellValue: (props: CellValueElementProps) => React.ReactNode; - onRuleChange?: () => void; - hasRowRenderers: boolean; - selectedEventIds: Readonly>; - showCheckboxes: boolean; - tabType?: TimelineTabs; - timelineId: string; - leadingControlColumns: ControlColumnProps[]; - trailingControlColumns: ControlColumnProps[]; - setEventsLoading: SetEventsLoading; - setEventsDeleted: SetEventsDeleted; -} - -export const EventColumnView = React.memo( - ({ - id, - actionsColumnWidth, - ariaRowindex, - columnHeaders, - data, - ecsData, - isEventViewer = false, - loadingEventIds, - onEventDetailsPanelOpened, - onRowSelected, - hasRowRenderers, - onRuleChange, - renderCellValue, - selectedEventIds = {}, - showCheckboxes, - tabType, - timelineId, - leadingControlColumns, - trailingControlColumns, - setEventsLoading, - setEventsDeleted, - }) => { - // Each action button shall announce itself to screen readers via an `aria-label` - // in the following format: - // "button description, for the event in row {ariaRowindex}, with columns {columnValues}", - // so we combine the column values here: - const columnValues = useMemo( - () => - columnHeaders - .map( - (header) => - getMappedNonEcsValue({ - data, - fieldName: header.id, - }) ?? [] - ) - .join(' '), - [columnHeaders, data] - ); - const leadingActionCells = useMemo( - () => - leadingControlColumns ? leadingControlColumns.map((column) => column.rowCellRender) : [], - [leadingControlColumns] - ); - const LeadingActions = useMemo( - () => - leadingActionCells.map((Action: RowCellRender | undefined, index) => { - const width = leadingControlColumns[index].width - ? leadingControlColumns[index].width - : actionsColumnWidth; - return ( - - {Action && ( - - )} - - ); - }), - [ - actionsColumnWidth, - ariaRowindex, - columnValues, - data, - ecsData, - id, - isEventViewer, - leadingActionCells, - leadingControlColumns, - loadingEventIds, - onEventDetailsPanelOpened, - onRowSelected, - onRuleChange, - selectedEventIds, - showCheckboxes, - tabType, - timelineId, - setEventsLoading, - setEventsDeleted, - ] - ); - return ( - - {LeadingActions} - - - ); - } -); - -EventColumnView.displayName = 'EventColumnView'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/events/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/events/index.tsx deleted file mode 100644 index 708cf9fae0361..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/body/events/index.tsx +++ /dev/null @@ -1,99 +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 React from 'react'; -import { isEmpty } from 'lodash'; - -import { EventsTbody } from '../../styles'; -import { StatefulEvent } from './stateful_event'; -import type { BrowserFields } from '../../../../../common/search_strategy/index_fields'; -import { TimelineTabs } from '../../../../../common/types/timeline'; -import type { - CellValueElementProps, - ColumnHeaderOptions, - ControlColumnProps, - OnRowSelected, - RowRenderer, -} from '../../../../../common/types/timeline'; - -import { TimelineItem, TimelineNonEcsData } from '../../../../../common/search_strategy'; - -/** This offset begins at two, because the header row counts as "row 1", and aria-rowindex starts at "1" */ -const ARIA_ROW_INDEX_OFFSET = 2; - -interface Props { - actionsColumnWidth: number; - browserFields: BrowserFields; - columnHeaders: ColumnHeaderOptions[]; - containerRef: React.MutableRefObject; - data: TimelineItem[]; - id: string; - isEventViewer?: boolean; - lastFocusedAriaColindex: number; - loadingEventIds: Readonly; - onRowSelected: OnRowSelected; - renderCellValue: (props: CellValueElementProps) => React.ReactNode; - onRuleChange?: () => void; - rowRenderers: RowRenderer[]; - selectedEventIds: Readonly>; - showCheckboxes: boolean; - tabType?: TimelineTabs; - leadingControlColumns: ControlColumnProps[]; - trailingControlColumns: ControlColumnProps[]; -} - -const EventsComponent: React.FC = ({ - actionsColumnWidth, - browserFields, - columnHeaders, - containerRef, - data, - id, - isEventViewer = false, - lastFocusedAriaColindex, - loadingEventIds, - onRowSelected, - onRuleChange, - renderCellValue, - rowRenderers, - selectedEventIds, - showCheckboxes, - tabType, - leadingControlColumns, - trailingControlColumns, -}) => ( - - {data.map((event, i) => ( - - ))} - -); - -export const Events = React.memo(EventsComponent); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/events/stateful_event.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/events/stateful_event.tsx deleted file mode 100644 index 8ad15580d7bfe..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/body/events/stateful_event.tsx +++ /dev/null @@ -1,218 +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 React, { useCallback, useMemo, useRef, useState } from 'react'; -import { useDispatch } from 'react-redux'; - -import { STATEFUL_EVENT_CSS_CLASS_NAME } from '../../helpers'; -import { EventsTrGroup, EventsTrSupplement } from '../../styles'; -import type { OnRowSelected } from '../../types'; -import { isEventBuildingBlockType, getEventType, isEvenEqlSequence } from '../helpers'; -import { EventColumnView } from './event_column_view'; -import { getRowRenderer } from '../renderers/get_row_renderer'; -import { StatefulRowRenderer } from './stateful_row_renderer'; -import { getMappedNonEcsValue } from '../data_driven_columns'; -import { StatefulEventContext } from './stateful_event_context'; -import type { BrowserFields } from '../../../../../common/search_strategy/index_fields'; -import { - SetEventsDeleted, - SetEventsLoading, - TimelineTabs, -} from '../../../../../common/types/timeline'; -import type { - CellValueElementProps, - ColumnHeaderOptions, - ControlColumnProps, - RowRenderer, - TimelineExpandedDetailType, -} from '../../../../../common/types/timeline'; - -import type { TimelineItem, TimelineNonEcsData } from '../../../../../common/search_strategy'; -import { tGridActions, tGridSelectors } from '../../../../store/t_grid'; -import { useDeepEqualSelector } from '../../../../hooks/use_selector'; - -interface Props { - actionsColumnWidth: number; - containerRef: React.MutableRefObject; - browserFields: BrowserFields; - columnHeaders: ColumnHeaderOptions[]; - event: TimelineItem; - isEventViewer?: boolean; - lastFocusedAriaColindex: number; - loadingEventIds: Readonly; - onRowSelected: OnRowSelected; - ariaRowindex: number; - onRuleChange?: () => void; - renderCellValue: (props: CellValueElementProps) => React.ReactNode; - rowRenderers: RowRenderer[]; - selectedEventIds: Readonly>; - showCheckboxes: boolean; - tabType?: TimelineTabs; - timelineId: string; - leadingControlColumns: ControlColumnProps[]; - trailingControlColumns: ControlColumnProps[]; -} - -const StatefulEventComponent: React.FC = ({ - actionsColumnWidth, - browserFields, - containerRef, - columnHeaders, - event, - isEventViewer = false, - lastFocusedAriaColindex, - loadingEventIds, - onRowSelected, - renderCellValue, - rowRenderers, - onRuleChange, - ariaRowindex, - selectedEventIds, - showCheckboxes, - tabType, - timelineId, - leadingControlColumns, - trailingControlColumns, -}) => { - const trGroupRef = useRef(null); - const dispatch = useDispatch(); - // Store context in state rather than creating object in provider value={} to prevent re-renders caused by a new object being created - const [activeStatefulEventContext] = useState({ timelineID: timelineId, tabType }); - const getTGrid = useMemo(() => tGridSelectors.getTGridByIdSelector(), []); - const expandedDetail = useDeepEqualSelector( - (state) => getTGrid(state, timelineId).expandedDetail ?? {} - ); - const hostName = useMemo(() => { - const hostNameArr = getMappedNonEcsValue({ data: event?.data, fieldName: 'host.name' }); - return hostNameArr && hostNameArr.length > 0 ? hostNameArr[0] : null; - }, [event?.data]); - - const hostIPAddresses = useMemo(() => { - const hostIpList = getMappedNonEcsValue({ data: event?.data, fieldName: 'host.ip' }) ?? []; - const sourceIpList = getMappedNonEcsValue({ data: event?.data, fieldName: 'source.ip' }) ?? []; - const destinationIpList = - getMappedNonEcsValue({ - data: event?.data, - fieldName: 'destination.ip', - }) ?? []; - return new Set([...hostIpList, ...sourceIpList, ...destinationIpList]); - }, [event?.data]); - - const activeTab = tabType ?? TimelineTabs.query; - const activeExpandedDetail = expandedDetail[activeTab]; - - const isDetailPanelExpanded: boolean = - (activeExpandedDetail?.panelView === 'eventDetail' && - activeExpandedDetail?.params?.eventId === event._id) || - (activeExpandedDetail?.panelView === 'hostDetail' && - activeExpandedDetail?.params?.hostName === hostName) || - (activeExpandedDetail?.panelView === 'networkDetail' && - activeExpandedDetail?.params?.ip && - hostIPAddresses?.has(activeExpandedDetail?.params?.ip)) || - false; - - const hasRowRenderers: boolean = useMemo( - () => getRowRenderer(event.ecs, rowRenderers) != null, - [event.ecs, rowRenderers] - ); - - const handleOnEventDetailPanelOpened = useCallback(() => { - const eventId = event._id; - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const indexName = event._index!; - - const updatedExpandedDetail: TimelineExpandedDetailType = { - panelView: 'eventDetail', - params: { - eventId, - indexName, - }, - }; - - dispatch( - tGridActions.toggleDetailPanel({ - ...updatedExpandedDetail, - tabType, - timelineId, - }) - ); - }, [dispatch, event._id, event._index, tabType, timelineId]); - - const setEventsLoading = useCallback( - ({ eventIds, isLoading }) => { - dispatch(tGridActions.setEventsLoading({ id: timelineId, eventIds, isLoading })); - }, - [dispatch, timelineId] - ); - - const setEventsDeleted = useCallback( - ({ eventIds, isDeleted }) => { - dispatch(tGridActions.setEventsDeleted({ id: timelineId, eventIds, isDeleted })); - }, - [dispatch, timelineId] - ); - - const RowRendererContent = useMemo( - () => ( - - - - ), - [ariaRowindex, containerRef, event, lastFocusedAriaColindex, rowRenderers, timelineId] - ); - - return ( - - - - -
{RowRendererContent}
-
-
- ); -}; - -export const StatefulEvent = React.memo(StatefulEventComponent); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/helpers.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/helpers.test.tsx index e655037732650..2e2cf23a87df8 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/helpers.test.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/helpers.test.tsx @@ -8,13 +8,11 @@ import { omit } from 'lodash/fp'; import { ColumnHeaderOptions } from '../../../../common/types'; -import { Ecs } from '../../../../common/ecs'; import { allowSorting, hasCellActions, mapSortDirectionToDirection, mapSortingColumns, - stringifyEvent, addBuildingBlockStyle, } from './helpers'; @@ -22,173 +20,6 @@ import { euiThemeVars } from '@kbn/ui-theme'; import { mockDnsEvent } from '../../../mock'; describe('helpers', () => { - describe('stringifyEvent', () => { - test('it omits __typename when it appears at arbitrary levels', () => { - const toStringify: Ecs = { - __typename: 'level 0', - _id: '4', - timestamp: '2018-11-08T19:03:25.937Z', - host: { - __typename: 'level 1', - name: ['suricata'], - ip: ['192.168.0.1'], - }, - event: { - id: ['4'], - category: ['Attempted Administrator Privilege Gain'], - type: ['Alert'], - module: ['suricata'], - severity: [1], - }, - source: { - ip: ['192.168.0.3'], - port: [53], - }, - destination: { - ip: ['192.168.0.3'], - port: [6343], - }, - suricata: { - eve: { - flow_id: [4], - proto: [''], - alert: { - signature: ['ET PHONE HOME Stack Overflow (CVE-2019-90210)'], - signature_id: [4], - __typename: 'level 2', - }, - }, - }, - user: { - id: ['4'], - name: ['jack.black'], - }, - geo: { - region_name: ['neither'], - country_iso_code: ['sasquatch'], - }, - } as Ecs; // as cast so that `__typename` can be added for the tests even though it is not part of ECS - const expected: Ecs = { - _id: '4', - timestamp: '2018-11-08T19:03:25.937Z', - host: { - name: ['suricata'], - ip: ['192.168.0.1'], - }, - event: { - id: ['4'], - category: ['Attempted Administrator Privilege Gain'], - type: ['Alert'], - module: ['suricata'], - severity: [1], - }, - source: { - ip: ['192.168.0.3'], - port: [53], - }, - destination: { - ip: ['192.168.0.3'], - port: [6343], - }, - suricata: { - eve: { - flow_id: [4], - proto: [''], - alert: { - signature: ['ET PHONE HOME Stack Overflow (CVE-2019-90210)'], - signature_id: [4], - }, - }, - }, - user: { - id: ['4'], - name: ['jack.black'], - }, - geo: { - region_name: ['neither'], - country_iso_code: ['sasquatch'], - }, - }; - expect(JSON.parse(stringifyEvent(toStringify))).toEqual(expected); - }); - - test('it omits null and undefined values at arbitrary levels, for arbitrary data types', () => { - const expected: Ecs = { - _id: '4', - host: {}, - event: { - id: ['4'], - category: ['theory'], - type: ['Alert'], - module: ['me'], - severity: [1], - }, - source: { - port: [53], - }, - destination: { - ip: ['192.168.0.3'], - port: [6343], - }, - suricata: { - eve: { - flow_id: [4], - proto: [''], - alert: { - signature: ['dance moves'], - }, - }, - }, - user: { - id: ['4'], - name: ['no use for a'], - }, - geo: { - region_name: ['bizzaro'], - country_iso_code: ['world'], - }, - }; - const toStringify: Ecs = { - _id: '4', - host: {}, - event: { - id: ['4'], - category: ['theory'], - type: ['Alert'], - module: ['me'], - severity: [1], - }, - source: { - ip: undefined, - port: [53], - }, - destination: { - ip: ['192.168.0.3'], - port: [6343], - }, - suricata: { - eve: { - flow_id: [4], - proto: [''], - alert: { - signature: ['dance moves'], - signature_id: undefined, - }, - }, - }, - user: { - id: ['4'], - name: ['no use for a'], - }, - geo: { - region_name: ['bizzaro'], - country_iso_code: ['world'], - }, - }; - expect(JSON.parse(stringifyEvent(toStringify))).toEqual(expected); - }); - }); - describe('mapSortDirectionToDirection', () => { test('it returns the expected direction when sortDirection is `asc`', () => { expect(mapSortDirectionToDirection('asc')).toBe('asc'); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/helpers.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/helpers.tsx index e17bb341b1408..dabaecfdeea2b 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/helpers.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/helpers.tsx @@ -20,15 +20,8 @@ import type { ColumnHeaderOptions, SortColumnTimeline, SortDirection, - TimelineEventsType, } from '../../../../common/types/timeline'; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const omitTypenameAndEmpty = (k: string, v: any): any | undefined => - k !== '__typename' && v != null ? v : undefined; - -export const stringifyEvent = (ecs: Ecs): string => JSON.stringify(ecs, omitTypenameAndEmpty, 2); - /** * Creates mapping of eventID -> fieldData for given fieldsToKeep. Used to store additional field * data necessary for custom timeline actions in conjunction with selection state @@ -76,27 +69,6 @@ export const getEventIdToDataMapping = ( export const isEventBuildingBlockType = (event: Ecs): boolean => !isEmpty(event.kibana?.alert?.building_block_type); -export const isEvenEqlSequence = (event: Ecs): boolean => { - if (!isEmpty(event.eql?.sequenceNumber)) { - try { - const sequenceNumber = (event.eql?.sequenceNumber ?? '').split('-')[0]; - return parseInt(sequenceNumber, 10) % 2 === 0; - } catch { - return false; - } - } - return false; -}; -/** Return eventType raw or signal or eql */ -export const getEventType = (event: Ecs): Omit => { - if (!isEmpty(event.signal?.rule?.id)) { - return 'signal'; - } else if (!isEmpty(event.eql?.parentId)) { - return 'eql'; - } - return 'raw'; -}; - /** Maps (Redux) `SortDirection` to the `direction` values used by `EuiDataGrid` */ export const mapSortDirectionToDirection = (sortDirection: SortDirection): 'asc' | 'desc' => { switch (sortDirection) { diff --git a/x-pack/plugins/timelines/public/components/t_grid/types.ts b/x-pack/plugins/timelines/public/components/t_grid/types.ts index 494e06c9f2e0c..0d7d307b8b05f 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/types.ts +++ b/x-pack/plugins/timelines/public/components/t_grid/types.ts @@ -5,13 +5,4 @@ * 2.0. */ -export type { - OnColumnSorted, - OnColumnsSorted, - OnColumnRemoved, - OnColumnResized, - OnChangePage, - OnRowSelected, - OnSelectAll, - OnUpdateColumns, -} from '../../../common/types/timeline'; +export type { OnChangePage, OnRowSelected, OnSelectAll } from '../../../common/types/timeline'; diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 9f51bbd41f106..9840a808ee27b 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -28050,13 +28050,6 @@ "xpack.timelines.clipboard.copy.to.the.clipboard": "Copier dans le presse-papiers", "xpack.timelines.clipboard.to.the.clipboard": "dans le presse-papiers", "xpack.timelines.copyToClipboardTooltip": "Copier dans le presse-papiers", - "xpack.timelines.draggables.field.categoryLabel": "Catégorie", - "xpack.timelines.draggables.field.fieldLabel": "Champ", - "xpack.timelines.draggables.field.typeLabel": "Type", - "xpack.timelines.draggables.field.viewCategoryTooltip": "Afficher la catégorie", - "xpack.timelines.emptyString.emptyStringDescription": "Chaîne vide", - "xpack.timelines.eventDetails.copyToClipboardTooltip": "Copier dans le presse-papiers", - "xpack.timelines.exitFullScreenButton": "Quitter le plein écran", "xpack.timelines.fieldBrowser.categoriesCountTitle": "{totalCount} {totalCount, plural, =1 {catégorie} other {catégories}}", "xpack.timelines.fieldBrowser.categoriesTitle": "Catégories", "xpack.timelines.fieldBrowser.categoryLabel": "Catégorie", @@ -28147,8 +28140,6 @@ "xpack.timelines.timeline.closedAlertSuccessToastMessage": "Fermeture réussie de {totalAlerts} {totalAlerts, plural, =1 {alerte} other {alertes}}.", "xpack.timelines.timeline.closeSelectedTitle": "Marquer comme fermé", "xpack.timelines.timeline.descriptionTooltip": "Description", - "xpack.timelines.timeline.eventHasEventRendererScreenReaderOnly": "L'événement de la ligne {row} possède un outil de rendu d'événement. Appuyez sur Maj + flèche vers le bas pour faire la mise au point dessus.", - "xpack.timelines.timeline.eventHasNotesScreenReaderOnly": "L'événement de la ligne {row} possède {notesCount, plural, =1 {une note} other {{notesCount} des notes}}. Appuyez sur Maj + flèche vers la droite pour faire la mise au point sur les notes.", "xpack.timelines.timeline.eventsTableAriaLabel": "events; Page {activePage} sur {totalPages}", "xpack.timelines.timeline.fieldTooltip": "Champ", "xpack.timelines.timeline.flyout.pane.removeColumnButtonLabel": "Retirer une colonne", @@ -28165,7 +28156,6 @@ "xpack.timelines.timeline.updateAlertStatusFailedDetailed": "{ updated } {updated, plural, =1 {alerte a été mise à jour} other {alertes ont été mises à jour}} correctement, mais { conflicts } n'ont pas pu être mis à jour\n car { conflicts, plural, =1 {elle était} other {elles étaient}} déjà en cours de modification.", "xpack.timelines.timeline.updateAlertStatusFailedSingleAlert": "Impossible de mettre à jour l'alerte, car elle était déjà en cours de modification.", "xpack.timelines.timeline.youAreInAnEventRendererScreenReaderOnly": "Vous êtes dans un outil de rendu d'événement pour la ligne : {row}. Appuyez sur la touche fléchée vers le haut pour quitter et revenir à la ligne en cours, ou sur la touche fléchée vers le bas pour quitter et passer à la ligne suivante.", - "xpack.timelines.timeline.youAreInATableCellScreenReaderOnly": "Vous êtes dans une cellule de tableau. Ligne : {row}, colonne : {column}", "xpack.timelines.timelineEvents.errorSearchDescription": "Une erreur s'est produite lors de la recherche d'événements de la chronologie", "xpack.timelines.toolbar.bulkActions.clearSelectionTitle": "Effacer la sélection", "xpack.timelines.toolbar.bulkActions.selectAllAlertsTitle": "Sélectionner un total de {totalAlertsFormatted} {totalAlerts, plural, =1 {alerte} other {alertes}}", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index d3630af845a80..ef4216a0ee22c 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -28227,13 +28227,6 @@ "xpack.timelines.clipboard.copy.to.the.clipboard": "クリップボードにコピー", "xpack.timelines.clipboard.to.the.clipboard": "クリップボードに", "xpack.timelines.copyToClipboardTooltip": "クリップボードにコピー", - "xpack.timelines.draggables.field.categoryLabel": "カテゴリー", - "xpack.timelines.draggables.field.fieldLabel": "フィールド", - "xpack.timelines.draggables.field.typeLabel": "型", - "xpack.timelines.draggables.field.viewCategoryTooltip": "カテゴリーを表示します", - "xpack.timelines.emptyString.emptyStringDescription": "空の文字列", - "xpack.timelines.eventDetails.copyToClipboardTooltip": "クリップボードにコピー", - "xpack.timelines.exitFullScreenButton": "全画面を終了", "xpack.timelines.fieldBrowser.categoriesCountTitle": "{totalCount} {totalCount, plural, other {カテゴリ}}", "xpack.timelines.fieldBrowser.categoriesTitle": "カテゴリー", "xpack.timelines.fieldBrowser.categoryLabel": "カテゴリー", @@ -28324,8 +28317,6 @@ "xpack.timelines.timeline.closedAlertSuccessToastMessage": "{totalAlerts} {totalAlerts, plural, other {件のアラート}}を正常にクローズしました。", "xpack.timelines.timeline.closeSelectedTitle": "クローズ済みに設定", "xpack.timelines.timeline.descriptionTooltip": "説明", - "xpack.timelines.timeline.eventHasEventRendererScreenReaderOnly": "行{row}のイベントにはイベントレンダラーがあります。Shiftと下矢印を押すとフォーカスします。", - "xpack.timelines.timeline.eventHasNotesScreenReaderOnly": "行{row}のイベントには{notesCount, plural, other {{notesCount}個のメモ}}があります。Shiftと右矢印を押すとメモをフォーカスします。", "xpack.timelines.timeline.eventsTableAriaLabel": "イベント; {activePage}/{totalPages} ページ", "xpack.timelines.timeline.fieldTooltip": "フィールド", "xpack.timelines.timeline.flyout.pane.removeColumnButtonLabel": "列を削除", @@ -28342,7 +28333,6 @@ "xpack.timelines.timeline.updateAlertStatusFailedDetailed": "{ updated } {updated, plural, other {アラート}}が正常に更新されましたが、{ conflicts }は更新できませんでした。\n { conflicts, plural, other {}}すでに修正されています。", "xpack.timelines.timeline.updateAlertStatusFailedSingleAlert": "アラートを更新できませんでした。アラートはすでに修正されています。", "xpack.timelines.timeline.youAreInAnEventRendererScreenReaderOnly": "行 {row} のイベントレンダラーを表示しています。上矢印キーを押すと、終了して現在の行に戻ります。下矢印キーを押すと、終了して次の行に進みます。", - "xpack.timelines.timeline.youAreInATableCellScreenReaderOnly": "表セルの行 {row}、列 {column} にいます", "xpack.timelines.timelineEvents.errorSearchDescription": "タイムラインイベント検索でエラーが発生しました", "xpack.timelines.toolbar.bulkActions.clearSelectionTitle": "選択した項目をクリア", "xpack.timelines.toolbar.bulkActions.selectAllAlertsTitle": "すべての{totalAlertsFormatted} {totalAlerts, plural, other {件のアラート}}を選択", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index e081071266643..6f56727710d9d 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -28261,13 +28261,6 @@ "xpack.timelines.clipboard.copy.to.the.clipboard": "复制到剪贴板", "xpack.timelines.clipboard.to.the.clipboard": "至剪贴板", "xpack.timelines.copyToClipboardTooltip": "复制到剪贴板", - "xpack.timelines.draggables.field.categoryLabel": "类别", - "xpack.timelines.draggables.field.fieldLabel": "字段", - "xpack.timelines.draggables.field.typeLabel": "类型", - "xpack.timelines.draggables.field.viewCategoryTooltip": "查看类别", - "xpack.timelines.emptyString.emptyStringDescription": "空字符串", - "xpack.timelines.eventDetails.copyToClipboardTooltip": "复制到剪贴板", - "xpack.timelines.exitFullScreenButton": "退出全屏", "xpack.timelines.fieldBrowser.categoriesCountTitle": "{totalCount} {totalCount, plural, other {个类别}}", "xpack.timelines.fieldBrowser.categoriesTitle": "类别", "xpack.timelines.fieldBrowser.categoryLabel": "类别", @@ -28358,8 +28351,6 @@ "xpack.timelines.timeline.closedAlertSuccessToastMessage": "已成功关闭 {totalAlerts} 个{totalAlerts, plural, other {告警}}。", "xpack.timelines.timeline.closeSelectedTitle": "标记为已关闭", "xpack.timelines.timeline.descriptionTooltip": "描述", - "xpack.timelines.timeline.eventHasEventRendererScreenReaderOnly": "位于行 {row} 的事件具有事件呈现程序。按 shift + 向下箭头键以对其聚焦。", - "xpack.timelines.timeline.eventHasNotesScreenReaderOnly": "位于行 {row} 的事件有{notesCount, plural, =1 {备注} other { {notesCount} 个备注}}。按 shift + 右箭头键以聚焦备注。", "xpack.timelines.timeline.eventsTableAriaLabel": "事件;第 {activePage} 页,共 {totalPages} 页", "xpack.timelines.timeline.fieldTooltip": "字段", "xpack.timelines.timeline.flyout.pane.removeColumnButtonLabel": "移除列", @@ -28376,7 +28367,6 @@ "xpack.timelines.timeline.updateAlertStatusFailedDetailed": "{ updated } 个{updated, plural, other {告警}}已成功更新,但是 { conflicts } 个无法更新,\n 因为{ conflicts, plural, other {其}}已被修改。", "xpack.timelines.timeline.updateAlertStatusFailedSingleAlert": "无法更新告警,因为它已被修改。", "xpack.timelines.timeline.youAreInAnEventRendererScreenReaderOnly": "您正处于第 {row} 行的事件呈现器中。按向上箭头键退出并返回当前行,或按向下箭头键退出并前进到下一行。", - "xpack.timelines.timeline.youAreInATableCellScreenReaderOnly": "您处在表单元格中。行:{row},列:{column}", "xpack.timelines.timelineEvents.errorSearchDescription": "搜索时间线事件时发生错误", "xpack.timelines.toolbar.bulkActions.clearSelectionTitle": "清除所选内容", "xpack.timelines.toolbar.bulkActions.selectAllAlertsTitle": "选择全部 {totalAlertsFormatted} 个{totalAlerts, plural, other {告警}}", From f44e1987842041e66a4ace4c29d0b6dfaead9ae9 Mon Sep 17 00:00:00 2001 From: Ersin Erdal <92688503+ersin-erdal@users.noreply.github.com> Date: Tue, 26 Apr 2022 03:35:34 +0200 Subject: [PATCH 03/29] Add circuit breaker for max number of actions by connector type (#128319) * connectorTypeOverrides key in kibana.yml can create a connector type specific action config. * Update docs and docker allowed keys --- docs/settings/alert-action-settings.asciidoc | 21 ++- .../resources/base/bin/kibana-docker | 1 + .../alerting/common/rule_task_instance.ts | 4 +- x-pack/plugins/alerting/server/config.ts | 9 +- .../server/lib/alert_execution_store.test.ts | 115 ++++++++++++ .../server/lib/alert_execution_store.ts | 106 +++++++++++ .../server/lib/get_actions_config_map.test.ts | 44 +++++ .../server/lib/get_actions_config_map.ts | 27 +++ .../server/lib/get_rules_config.test.ts | 52 ------ .../alerting/server/lib/get_rules_config.ts | 28 --- .../server/lib/rule_execution_status.test.ts | 10 +- .../server/lib/rule_execution_status.ts | 6 +- x-pack/plugins/alerting/server/plugin.test.ts | 62 ------- x-pack/plugins/alerting/server/plugin.ts | 7 +- .../server/rule_type_registry.test.ts | 92 --------- .../server/rules_client/tests/create.test.ts | 20 -- .../alerting/server/rules_client/tests/lib.ts | 5 - .../create_execution_handler.test.ts | 174 ++++++++++++++---- .../task_runner/create_execution_handler.ts | 43 ++++- .../alerting/server/task_runner/fixtures.ts | 5 - .../server/task_runner/task_runner.test.ts | 171 +++++++++++++++-- .../server/task_runner/task_runner.ts | 18 +- .../task_runner/task_runner_cancel.test.ts | 10 +- .../task_runner/task_runner_factory.test.ts | 5 + .../server/task_runner/task_runner_factory.ts | 2 + .../alerting/server/task_runner/types.ts | 9 +- x-pack/plugins/alerting/server/types.ts | 2 - .../alerting_api_integration/common/config.ts | 4 + .../plugins/alerts/server/action_types.ts | 9 + .../tests/alerting/capped_action_type.ts | 127 +++++++++++++ .../spaces_only/tests/alerting/index.ts | 1 + 31 files changed, 827 insertions(+), 362 deletions(-) create mode 100644 x-pack/plugins/alerting/server/lib/alert_execution_store.test.ts create mode 100644 x-pack/plugins/alerting/server/lib/alert_execution_store.ts create mode 100644 x-pack/plugins/alerting/server/lib/get_actions_config_map.test.ts create mode 100644 x-pack/plugins/alerting/server/lib/get_actions_config_map.ts delete mode 100644 x-pack/plugins/alerting/server/lib/get_rules_config.test.ts delete mode 100644 x-pack/plugins/alerting/server/lib/get_rules_config.ts create mode 100644 x-pack/test/alerting_api_integration/spaces_only/tests/alerting/capped_action_type.ts diff --git a/docs/settings/alert-action-settings.asciidoc b/docs/settings/alert-action-settings.asciidoc index 3d15c3cc5e368..384c4b696521d 100644 --- a/docs/settings/alert-action-settings.asciidoc +++ b/docs/settings/alert-action-settings.asciidoc @@ -211,13 +211,26 @@ For example, `20m`, `24h`, `7d`, `1w`. Default: `5m`. `xpack.alerting.rules.run.ruleTypeOverrides`:: Overrides the configs under `xpack.alerting.rules.run` for the rule type with the given ID. List the rule identifier and its settings in an array of objects. + --- For example: -``` +[source,yaml] +-- xpack.alerting.rules.run: timeout: '5m' ruleTypeOverrides: - id: '.index-threshold' timeout: '15m' -``` --- \ No newline at end of file +-- + +`xpack.alerting.rules.run.actions.connectorTypeOverrides`:: +Overrides the configs under `xpack.alerting.rules.run.actions` for the connector type with the given ID. List the connector type identifier and its settings in an array of objects. ++ +For example: +[source,yaml] +-- +xpack.alerting.rules.run: + actions: + max: 10 + connectorTypeOverrides: + - id: '.server-log' + max: 5 +-- diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker index ca81383ba639f..6ec12aab3c002 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker @@ -211,6 +211,7 @@ kibana_vars=( xpack.alerting.rules.minimumScheduleInterval.value xpack.alerting.rules.minimumScheduleInterval.enforce xpack.alerting.rules.run.actions.max + xpack.alerting.rules.run.actions.connectorTypeOverrides xpack.alerts.healthCheck.interval xpack.alerts.invalidateApiKeysTask.interval xpack.alerts.invalidateApiKeysTask.removalDelay diff --git a/x-pack/plugins/alerting/common/rule_task_instance.ts b/x-pack/plugins/alerting/common/rule_task_instance.ts index 8465222c0ff62..529b65e719ddd 100644 --- a/x-pack/plugins/alerting/common/rule_task_instance.ts +++ b/x-pack/plugins/alerting/common/rule_task_instance.ts @@ -22,7 +22,7 @@ const ruleExecutionMetricsSchema = t.partial({ esSearchDurationMs: t.number, }); -const alertExecutionStore = t.partial({ +const alertExecutionMetrics = t.partial({ numberOfTriggeredActions: t.number, numberOfGeneratedActions: t.number, triggeredActionsStatus: t.string, @@ -32,7 +32,7 @@ export type RuleExecutionMetrics = t.TypeOf; export type RuleTaskState = t.TypeOf; export type RuleExecutionState = RuleTaskState & { metrics: RuleExecutionMetrics; - alertExecutionStore: t.TypeOf; + alertExecutionMetrics: t.TypeOf; }; export const ruleParamsSchema = t.intersection([ diff --git a/x-pack/plugins/alerting/server/config.ts b/x-pack/plugins/alerting/server/config.ts index 64c09a9b4bb09..c07d5bbf0f3f2 100644 --- a/x-pack/plugins/alerting/server/config.ts +++ b/x-pack/plugins/alerting/server/config.ts @@ -14,6 +14,11 @@ const ruleTypeSchema = schema.object({ timeout: schema.maybe(schema.string({ validate: validateDurationSchema })), }); +const connectorTypeSchema = schema.object({ + id: schema.string(), + max: schema.maybe(schema.number({ max: 100000 })), +}); + const rulesSchema = schema.object({ minimumScheduleInterval: schema.object({ value: schema.string({ @@ -36,6 +41,7 @@ const rulesSchema = schema.object({ timeout: schema.maybe(schema.string({ validate: validateDurationSchema })), actions: schema.object({ max: schema.number({ defaultValue: 100000, max: 100000 }), + connectorTypeOverrides: schema.maybe(schema.arrayOf(connectorTypeSchema)), }), ruleTypeOverrides: schema.maybe(schema.arrayOf(ruleTypeSchema)), }), @@ -59,5 +65,6 @@ export const configSchema = schema.object({ export type AlertingConfig = TypeOf; export type RulesConfig = TypeOf; -export type RuleTypeConfig = Omit; export type AlertingRulesConfig = Pick; +export type ActionsConfig = RulesConfig['run']['actions']; +export type ActionTypeConfig = Omit; diff --git a/x-pack/plugins/alerting/server/lib/alert_execution_store.test.ts b/x-pack/plugins/alerting/server/lib/alert_execution_store.test.ts new file mode 100644 index 0000000000000..905ef53cda9db --- /dev/null +++ b/x-pack/plugins/alerting/server/lib/alert_execution_store.test.ts @@ -0,0 +1,115 @@ +/* + * 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 { AlertExecutionStore } from './alert_execution_store'; +import { ActionsCompletion } from '../task_runner/types'; + +describe('AlertExecutionStore', () => { + const alertExecutionStore = new AlertExecutionStore(); + const testConnectorId = 'test-connector-id'; + + // Getter Setter + test('returns the default values if there is no change', () => { + expect(alertExecutionStore.getTriggeredActionsStatus()).toBe(ActionsCompletion.COMPLETE); + expect(alertExecutionStore.getNumberOfTriggeredActions()).toBe(0); + expect(alertExecutionStore.getNumberOfGeneratedActions()).toBe(0); + expect(alertExecutionStore.getStatusByConnectorType('any')).toBe(undefined); + }); + + test('sets and returns numberOfTriggeredActions', () => { + alertExecutionStore.setNumberOfTriggeredActions(5); + expect(alertExecutionStore.getNumberOfTriggeredActions()).toBe(5); + }); + + test('sets and returns numberOfGeneratedActions', () => { + alertExecutionStore.setNumberOfGeneratedActions(15); + expect(alertExecutionStore.getNumberOfGeneratedActions()).toBe(15); + }); + + test('sets and returns triggeredActionsStatusByConnectorType', () => { + alertExecutionStore.setTriggeredActionsStatusByConnectorType({ + actionTypeId: testConnectorId, + status: ActionsCompletion.PARTIAL, + }); + expect( + alertExecutionStore.getStatusByConnectorType(testConnectorId).triggeredActionsStatus + ).toBe(ActionsCompletion.PARTIAL); + expect(alertExecutionStore.getTriggeredActionsStatus()).toBe(ActionsCompletion.PARTIAL); + }); + + // increment + test('increments numberOfTriggeredActions by 1', () => { + alertExecutionStore.incrementNumberOfTriggeredActions(); + expect(alertExecutionStore.getNumberOfTriggeredActions()).toBe(6); + }); + + test('increments incrementNumberOfGeneratedActions by x', () => { + alertExecutionStore.incrementNumberOfGeneratedActions(2); + expect(alertExecutionStore.getNumberOfGeneratedActions()).toBe(17); + }); + + test('increments numberOfTriggeredActionsByConnectorType by 1', () => { + alertExecutionStore.incrementNumberOfTriggeredActionsByConnectorType(testConnectorId); + expect( + alertExecutionStore.getStatusByConnectorType(testConnectorId).numberOfTriggeredActions + ).toBe(1); + }); + + test('increments NumberOfGeneratedActionsByConnectorType by 1', () => { + alertExecutionStore.incrementNumberOfGeneratedActionsByConnectorType(testConnectorId); + expect( + alertExecutionStore.getStatusByConnectorType(testConnectorId).numberOfGeneratedActions + ).toBe(1); + }); + + // Checker + test('checks if it has reached the executable actions limit', () => { + expect(alertExecutionStore.hasReachedTheExecutableActionsLimit({ default: { max: 10 } })).toBe( + false + ); + + expect(alertExecutionStore.hasReachedTheExecutableActionsLimit({ default: { max: 5 } })).toBe( + true + ); + }); + + test('checks if it has reached the executable actions limit by connector type', () => { + alertExecutionStore.incrementNumberOfTriggeredActionsByConnectorType(testConnectorId); + alertExecutionStore.incrementNumberOfTriggeredActionsByConnectorType(testConnectorId); + alertExecutionStore.incrementNumberOfTriggeredActionsByConnectorType(testConnectorId); + alertExecutionStore.incrementNumberOfTriggeredActionsByConnectorType(testConnectorId); + alertExecutionStore.incrementNumberOfTriggeredActionsByConnectorType(testConnectorId); + + expect( + alertExecutionStore.hasReachedTheExecutableActionsLimitByConnectorType({ + actionsConfigMap: { + default: { max: 20 }, + [testConnectorId]: { + max: 5, + }, + }, + actionTypeId: testConnectorId, + }) + ).toBe(true); + + expect( + alertExecutionStore.hasReachedTheExecutableActionsLimitByConnectorType({ + actionsConfigMap: { + default: { max: 20 }, + [testConnectorId]: { + max: 8, + }, + }, + actionTypeId: testConnectorId, + }) + ).toBe(false); + }); + + test('checks if a connector type it has already reached the executable actions limit', () => { + expect(alertExecutionStore.hasConnectorTypeReachedTheLimit(testConnectorId)).toBe(true); + }); +}); diff --git a/x-pack/plugins/alerting/server/lib/alert_execution_store.ts b/x-pack/plugins/alerting/server/lib/alert_execution_store.ts new file mode 100644 index 0000000000000..b601a76b809a4 --- /dev/null +++ b/x-pack/plugins/alerting/server/lib/alert_execution_store.ts @@ -0,0 +1,106 @@ +/* + * 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 { ActionsConfigMap } from './get_actions_config_map'; +import { ActionsCompletion } from '../task_runner/types'; + +interface State { + numberOfTriggeredActions: number; + numberOfGeneratedActions: number; + connectorTypes: { + [key: string]: { + triggeredActionsStatus: ActionsCompletion; + numberOfTriggeredActions: number; + numberOfGeneratedActions: number; + }; + }; +} + +export class AlertExecutionStore { + private state: State = { + numberOfTriggeredActions: 0, + numberOfGeneratedActions: 0, + connectorTypes: {}, + }; + + // Getters + public getTriggeredActionsStatus = () => { + const hasPartial = Object.values(this.state.connectorTypes).some( + (connectorType) => connectorType?.triggeredActionsStatus === ActionsCompletion.PARTIAL + ); + return hasPartial ? ActionsCompletion.PARTIAL : ActionsCompletion.COMPLETE; + }; + public getNumberOfTriggeredActions = () => { + return this.state.numberOfTriggeredActions; + }; + public getNumberOfGeneratedActions = () => { + return this.state.numberOfGeneratedActions; + }; + public getStatusByConnectorType = (actionTypeId: string) => { + return this.state.connectorTypes[actionTypeId]; + }; + + // Setters + public setNumberOfTriggeredActions = (numberOfTriggeredActions: number) => { + this.state.numberOfTriggeredActions = numberOfTriggeredActions; + }; + + public setNumberOfGeneratedActions = (numberOfGeneratedActions: number) => { + this.state.numberOfGeneratedActions = numberOfGeneratedActions; + }; + + public setTriggeredActionsStatusByConnectorType = ({ + actionTypeId, + status, + }: { + actionTypeId: string; + status: ActionsCompletion; + }) => { + set(this.state, `connectorTypes["${actionTypeId}"].triggeredActionsStatus`, status); + }; + + // Checkers + public hasReachedTheExecutableActionsLimit = (actionsConfigMap: ActionsConfigMap): boolean => + this.state.numberOfTriggeredActions >= actionsConfigMap.default.max; + + public hasReachedTheExecutableActionsLimitByConnectorType = ({ + actionsConfigMap, + actionTypeId, + }: { + actionsConfigMap: ActionsConfigMap; + actionTypeId: string; + }): boolean => { + const numberOfTriggeredActionsByConnectorType = + this.state.connectorTypes[actionTypeId]?.numberOfTriggeredActions || 0; + const executableActionsLimitByConnectorType = + actionsConfigMap[actionTypeId]?.max || actionsConfigMap.default.max; + + return numberOfTriggeredActionsByConnectorType >= executableActionsLimitByConnectorType; + }; + + public hasConnectorTypeReachedTheLimit = (actionTypeId: string) => + this.state.connectorTypes[actionTypeId]?.triggeredActionsStatus === ActionsCompletion.PARTIAL; + + // Incrementer + public incrementNumberOfTriggeredActions = () => { + this.state.numberOfTriggeredActions++; + }; + + public incrementNumberOfGeneratedActions = (incrementBy: number) => { + this.state.numberOfGeneratedActions += incrementBy; + }; + + public incrementNumberOfTriggeredActionsByConnectorType = (actionTypeId: string) => { + const currentVal = this.state.connectorTypes[actionTypeId]?.numberOfTriggeredActions || 0; + set(this.state, `connectorTypes["${actionTypeId}"].numberOfTriggeredActions`, currentVal + 1); + }; + public incrementNumberOfGeneratedActionsByConnectorType = (actionTypeId: string) => { + const currentVal = this.state.connectorTypes[actionTypeId]?.numberOfGeneratedActions || 0; + set(this.state, `connectorTypes["${actionTypeId}"].numberOfGeneratedActions`, currentVal + 1); + }; +} diff --git a/x-pack/plugins/alerting/server/lib/get_actions_config_map.test.ts b/x-pack/plugins/alerting/server/lib/get_actions_config_map.test.ts new file mode 100644 index 0000000000000..db67925e3c61f --- /dev/null +++ b/x-pack/plugins/alerting/server/lib/get_actions_config_map.test.ts @@ -0,0 +1,44 @@ +/* + * 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 { getActionsConfigMap } from './get_actions_config_map'; + +const connectorTypeId = 'test-connector-type-id'; +const actionsConfig = { + max: 1000, +}; + +const actionsConfigWithConnectorType = { + ...actionsConfig, + connectorTypeOverrides: [ + { + id: connectorTypeId, + max: 20, + }, + ], +}; + +describe('get actions config map', () => { + test('returns the default actions config', () => { + expect(getActionsConfigMap(actionsConfig)).toEqual({ + default: { + max: 1000, + }, + }); + }); + + test('applies the connector type specific config', () => { + expect(getActionsConfigMap(actionsConfigWithConnectorType)).toEqual({ + default: { + max: 1000, + }, + [connectorTypeId]: { + max: 20, + }, + }); + }); +}); diff --git a/x-pack/plugins/alerting/server/lib/get_actions_config_map.ts b/x-pack/plugins/alerting/server/lib/get_actions_config_map.ts new file mode 100644 index 0000000000000..966f25e30f824 --- /dev/null +++ b/x-pack/plugins/alerting/server/lib/get_actions_config_map.ts @@ -0,0 +1,27 @@ +/* + * 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 { omit } from 'lodash'; +import { ActionsConfig, ActionTypeConfig } from '../config'; + +export interface ActionsConfigMap { + default: ActionTypeConfig; + [key: string]: ActionTypeConfig; +} + +export const getActionsConfigMap = (actionsConfig: ActionsConfig): ActionsConfigMap => { + const configsByConnectorType = actionsConfig.connectorTypeOverrides?.reduce( + (config, configByConnectorType) => { + return { ...config, [configByConnectorType.id]: omit(configByConnectorType, 'id') }; + }, + {} + ); + return { + default: omit(actionsConfig, 'connectorTypeOverrides'), + ...configsByConnectorType, + }; +}; diff --git a/x-pack/plugins/alerting/server/lib/get_rules_config.test.ts b/x-pack/plugins/alerting/server/lib/get_rules_config.test.ts deleted file mode 100644 index e6477945eebda..0000000000000 --- a/x-pack/plugins/alerting/server/lib/get_rules_config.test.ts +++ /dev/null @@ -1,52 +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 { getExecutionConfigForRuleType } from './get_rules_config'; -import { RulesConfig } from '../config'; - -const ruleTypeId = 'test-rule-type-id'; -const config = { - minimumScheduleInterval: { - value: '2m', - enforce: false, - }, - run: { - timeout: '1m', - actions: { max: 1000 }, - }, -} as RulesConfig; - -const configWithRuleType = { - ...config, - run: { - ...config.run, - ruleTypeOverrides: [ - { - id: ruleTypeId, - actions: { max: 20 }, - }, - ], - }, -}; - -describe('get rules config', () => { - test('returns the rule type specific config and keeps the default values that are not overwritten', () => { - expect(getExecutionConfigForRuleType({ config: configWithRuleType, ruleTypeId })).toEqual({ - run: { - id: ruleTypeId, - timeout: '1m', - actions: { max: 20 }, - }, - }); - }); - - test('returns the default config when there is no rule type specific config', () => { - expect(getExecutionConfigForRuleType({ config, ruleTypeId })).toEqual({ - run: config.run, - }); - }); -}); diff --git a/x-pack/plugins/alerting/server/lib/get_rules_config.ts b/x-pack/plugins/alerting/server/lib/get_rules_config.ts deleted file mode 100644 index 14d3bdc36285f..0000000000000 --- a/x-pack/plugins/alerting/server/lib/get_rules_config.ts +++ /dev/null @@ -1,28 +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 { omit } from 'lodash'; -import { RulesConfig, RuleTypeConfig } from '../config'; - -export const getExecutionConfigForRuleType = ({ - config, - ruleTypeId, -}: { - config: RulesConfig; - ruleTypeId: string; -}): RuleTypeConfig => { - const ruleTypeExecutionConfig = config.run.ruleTypeOverrides?.find( - (ruleType) => ruleType.id === ruleTypeId - ); - - return { - run: { - ...omit(config.run, 'ruleTypeOverrides'), - ...ruleTypeExecutionConfig, - }, - }; -}; diff --git a/x-pack/plugins/alerting/server/lib/rule_execution_status.test.ts b/x-pack/plugins/alerting/server/lib/rule_execution_status.test.ts index 19da38e283a11..52de76d4b4dd9 100644 --- a/x-pack/plugins/alerting/server/lib/rule_execution_status.test.ts +++ b/x-pack/plugins/alerting/server/lib/rule_execution_status.test.ts @@ -32,7 +32,7 @@ describe('RuleExecutionStatus', () => { describe('executionStatusFromState()', () => { test('empty task state', () => { const status = executionStatusFromState({ - alertExecutionStore: { + alertExecutionMetrics: { numberOfTriggeredActions: 0, numberOfGeneratedActions: 0, triggeredActionsStatus: ActionsCompletion.COMPLETE, @@ -49,7 +49,7 @@ describe('RuleExecutionStatus', () => { test('task state with no instances', () => { const status = executionStatusFromState({ alertInstances: {}, - alertExecutionStore: { + alertExecutionMetrics: { numberOfTriggeredActions: 0, numberOfGeneratedActions: 0, triggeredActionsStatus: ActionsCompletion.COMPLETE, @@ -68,7 +68,7 @@ describe('RuleExecutionStatus', () => { test('task state with one instance', () => { const status = executionStatusFromState({ alertInstances: { a: {} }, - alertExecutionStore: { + alertExecutionMetrics: { numberOfTriggeredActions: 0, numberOfGeneratedActions: 0, triggeredActionsStatus: ActionsCompletion.COMPLETE, @@ -86,7 +86,7 @@ describe('RuleExecutionStatus', () => { test('task state with numberOfTriggeredActions', () => { const status = executionStatusFromState({ - alertExecutionStore: { + alertExecutionMetrics: { numberOfTriggeredActions: 1, numberOfGeneratedActions: 2, triggeredActionsStatus: ActionsCompletion.COMPLETE, @@ -106,7 +106,7 @@ describe('RuleExecutionStatus', () => { test('task state with warning', () => { const status = executionStatusFromState({ alertInstances: { a: {} }, - alertExecutionStore: { + alertExecutionMetrics: { numberOfTriggeredActions: 3, triggeredActionsStatus: ActionsCompletion.PARTIAL, }, diff --git a/x-pack/plugins/alerting/server/lib/rule_execution_status.ts b/x-pack/plugins/alerting/server/lib/rule_execution_status.ts index f4161022e9544..0b9d2520ae455 100644 --- a/x-pack/plugins/alerting/server/lib/rule_execution_status.ts +++ b/x-pack/plugins/alerting/server/lib/rule_execution_status.ts @@ -23,7 +23,7 @@ export function executionStatusFromState(state: RuleExecutionState): RuleExecuti const alertIds = Object.keys(state.alertInstances ?? {}); const hasIncompleteAlertExecution = - state.alertExecutionStore.triggeredActionsStatus === ActionsCompletion.PARTIAL; + state.alertExecutionMetrics.triggeredActionsStatus === ActionsCompletion.PARTIAL; let status: RuleExecutionStatuses = alertIds.length === 0 ? RuleExecutionStatusValues[0] : RuleExecutionStatusValues[1]; @@ -34,8 +34,8 @@ export function executionStatusFromState(state: RuleExecutionState): RuleExecuti return { metrics: state.metrics, - numberOfTriggeredActions: state.alertExecutionStore.numberOfTriggeredActions, - numberOfGeneratedActions: state.alertExecutionStore.numberOfGeneratedActions, + numberOfTriggeredActions: state.alertExecutionMetrics.numberOfTriggeredActions, + numberOfGeneratedActions: state.alertExecutionMetrics.numberOfGeneratedActions, lastExecutionDate: new Date(), status, ...(hasIncompleteAlertExecution && { diff --git a/x-pack/plugins/alerting/server/plugin.test.ts b/x-pack/plugins/alerting/server/plugin.test.ts index 949328d065536..29739d2ca53d9 100644 --- a/x-pack/plugins/alerting/server/plugin.test.ts +++ b/x-pack/plugins/alerting/server/plugin.test.ts @@ -50,13 +50,6 @@ const sampleRuleType: RuleType = { actionGroups: [], defaultActionGroupId: 'default', producer: 'test', - config: { - run: { - actions: { - max: 1000, - }, - }, - }, async executor() {}, }; @@ -122,61 +115,6 @@ describe('Alerting Plugin', () => { }); }); - it(`applies the default config if there is no rule type specific config `, async () => { - const context = coreMock.createPluginInitializerContext({ - ...generateAlertingConfig(), - rules: { - minimumScheduleInterval: { value: '1m', enforce: false }, - run: { - actions: { - max: 123, - }, - }, - }, - }); - plugin = new AlertingPlugin(context); - - const setupContract = await plugin.setup(setupMocks, mockPlugins); - - const ruleType = { ...sampleRuleType }; - setupContract.registerType(ruleType); - - expect(ruleType.config).toEqual({ - run: { - actions: { max: 123 }, - }, - }); - }); - - it(`applies rule type specific config if defined in config`, async () => { - const context = coreMock.createPluginInitializerContext({ - ...generateAlertingConfig(), - rules: { - minimumScheduleInterval: { value: '1m', enforce: false }, - run: { - actions: { max: 123 }, - ruleTypeOverrides: [{ id: sampleRuleType.id, timeout: '1d' }], - }, - }, - }); - plugin = new AlertingPlugin(context); - - const setupContract = await plugin.setup(setupMocks, mockPlugins); - - const ruleType = { ...sampleRuleType }; - setupContract.registerType(ruleType); - - expect(ruleType.config).toEqual({ - run: { - id: sampleRuleType.id, - actions: { - max: 123, - }, - timeout: '1d', - }, - }); - }); - describe('registerType()', () => { let setup: PluginSetupContract; beforeEach(async () => { diff --git a/x-pack/plugins/alerting/server/plugin.ts b/x-pack/plugins/alerting/server/plugin.ts index 9696c523b095b..cfd2701e1f885 100644 --- a/x-pack/plugins/alerting/server/plugin.ts +++ b/x-pack/plugins/alerting/server/plugin.ts @@ -77,8 +77,8 @@ import { AlertingAuthorizationClientFactory } from './alerting_authorization_cli import { AlertingAuthorization } from './authorization'; import { getSecurityHealth, SecurityHealth } from './lib/get_security_health'; import { registerNodeCollector, registerClusterCollector, InMemoryMetrics } from './monitoring'; -import { getExecutionConfigForRuleType } from './lib/get_rules_config'; import { getRuleTaskTimeout } from './lib/get_rule_task_timeout'; +import { getActionsConfigMap } from './lib/get_actions_config_map'; export const EVENT_LOG_PROVIDER = 'alerting'; export const EVENT_LOG_ACTIONS = { @@ -319,10 +319,6 @@ export class AlertingPlugin { if (!(ruleType.minimumLicenseRequired in LICENSE_TYPE)) { throw new Error(`"${ruleType.minimumLicenseRequired}" is not a valid license type`); } - ruleType.config = getExecutionConfigForRuleType({ - config: this.config.rules, - ruleTypeId: ruleType.id, - }); ruleType.ruleTaskTimeout = getRuleTaskTimeout({ config: this.config.rules, ruleTaskTimeout: ruleType.ruleTaskTimeout, @@ -437,6 +433,7 @@ export class AlertingPlugin { supportsEphemeralTasks: plugins.taskManager.supportsEphemeralTasks(), maxEphemeralActionsPerRule: this.config.maxEphemeralActionsPerAlert, cancelAlertsOnRuleTimeout: this.config.cancelAlertsOnRuleTimeout, + actionsConfigMap: getActionsConfigMap(this.config.rules.run.actions), usageCounter: this.usageCounter, }); diff --git a/x-pack/plugins/alerting/server/rule_type_registry.test.ts b/x-pack/plugins/alerting/server/rule_type_registry.test.ts index abaeee60759e9..9a6b2232c47d4 100644 --- a/x-pack/plugins/alerting/server/rule_type_registry.test.ts +++ b/x-pack/plugins/alerting/server/rule_type_registry.test.ts @@ -60,11 +60,6 @@ describe('Create Lifecycle', () => { isExportable: true, executor: jest.fn(), producer: 'alerts', - config: { - run: { - actions: { max: 1000 }, - }, - }, }); expect(registry.has('foo')).toEqual(true); }); @@ -86,11 +81,6 @@ describe('Create Lifecycle', () => { isExportable: true, executor: jest.fn(), producer: 'alerts', - config: { - run: { - actions: { max: 1000 }, - }, - }, }; const registry = new RuleTypeRegistry(ruleTypeRegistryParams); @@ -124,11 +114,6 @@ describe('Create Lifecycle', () => { isExportable: true, executor: jest.fn(), producer: 'alerts', - config: { - run: { - actions: { max: 1000 }, - }, - }, }; const registry = new RuleTypeRegistry(ruleTypeRegistryParams); @@ -153,11 +138,6 @@ describe('Create Lifecycle', () => { isExportable: true, executor: jest.fn(), producer: 'alerts', - config: { - run: { - actions: { max: 1000 }, - }, - }, }; const registry = new RuleTypeRegistry(ruleTypeRegistryParams); @@ -185,11 +165,6 @@ describe('Create Lifecycle', () => { executor: jest.fn(), producer: 'alerts', defaultScheduleInterval: 'foobar', - config: { - run: { - actions: { max: 1000 }, - }, - }, }; const registry = new RuleTypeRegistry(ruleTypeRegistryParams); @@ -278,11 +253,6 @@ describe('Create Lifecycle', () => { isExportable: true, executor: jest.fn(), producer: 'alerts', - config: { - run: { - actions: { max: 1000 }, - }, - }, }; const registry = new RuleTypeRegistry(ruleTypeRegistryParams); @@ -312,11 +282,6 @@ describe('Create Lifecycle', () => { producer: 'alerts', minimumLicenseRequired: 'basic', isExportable: true, - config: { - run: { - actions: { max: 1000 }, - }, - }, }; const registry = new RuleTypeRegistry(ruleTypeRegistryParams); registry.register(ruleType); @@ -350,11 +315,6 @@ describe('Create Lifecycle', () => { producer: 'alerts', minimumLicenseRequired: 'basic', isExportable: true, - config: { - run: { - actions: { max: 1000 }, - }, - }, }; const registry = new RuleTypeRegistry(ruleTypeRegistryParams); registry.register(ruleType); @@ -392,11 +352,6 @@ describe('Create Lifecycle', () => { isExportable: true, executor: jest.fn(), producer: 'alerts', - config: { - run: { - actions: { max: 1000 }, - }, - }, }; const registry = new RuleTypeRegistry(ruleTypeRegistryParams); @@ -423,11 +378,6 @@ describe('Create Lifecycle', () => { executor: jest.fn(), producer: 'alerts', ruleTaskTimeout: '20m', - config: { - run: { - actions: { max: 1000 }, - }, - }, }; const registry = new RuleTypeRegistry(ruleTypeRegistryParams); registry.register(ruleType); @@ -460,11 +410,6 @@ describe('Create Lifecycle', () => { isExportable: true, executor: jest.fn(), producer: 'alerts', - config: { - run: { - actions: { max: 1000 }, - }, - }, }; const registry = new RuleTypeRegistry(ruleTypeRegistryParams); registry.register(ruleType); @@ -488,11 +433,6 @@ describe('Create Lifecycle', () => { isExportable: true, executor: jest.fn(), producer: 'alerts', - config: { - run: { - actions: { max: 1000 }, - }, - }, }); expect(() => registry.register({ @@ -509,11 +449,6 @@ describe('Create Lifecycle', () => { isExportable: true, executor: jest.fn(), producer: 'alerts', - config: { - run: { - actions: { max: 1000 }, - }, - }, }) ).toThrowErrorMatchingInlineSnapshot(`"Rule type \\"test\\" is already registered."`); }); @@ -536,11 +471,6 @@ describe('Create Lifecycle', () => { isExportable: true, executor: jest.fn(), producer: 'alerts', - config: { - run: { - actions: { max: 1000 }, - }, - }, }); const ruleType = registry.get('test'); expect(ruleType).toMatchInlineSnapshot(` @@ -560,13 +490,6 @@ describe('Create Lifecycle', () => { "params": Array [], "state": Array [], }, - "config": Object { - "run": Object { - "actions": Object { - "max": 1000, - }, - }, - }, "defaultActionGroupId": "default", "executor": [MockFunction], "id": "test", @@ -615,11 +538,6 @@ describe('Create Lifecycle', () => { minimumLicenseRequired: 'basic', executor: jest.fn(), producer: 'alerts', - config: { - run: { - actions: { max: 1000 }, - }, - }, }); const result = registry.list(); expect(result).toMatchInlineSnapshot(` @@ -714,11 +632,6 @@ describe('Create Lifecycle', () => { isExportable: true, minimumLicenseRequired: 'basic', recoveryActionGroup: { id: 'recovered', name: 'Recovered' }, - config: { - run: { - actions: { max: 1000 }, - }, - }, }); }); @@ -752,11 +665,6 @@ function ruleTypeWithVariables( minimumLicenseRequired: 'basic', async executor() {}, producer: 'alerts', - config: { - run: { - actions: { max: 1000 }, - }, - }, }; if (!context && !state) return baseAlert; diff --git a/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts index d695acf574aeb..72e74f058bb90 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts @@ -1166,11 +1166,6 @@ describe('create()', () => { extractReferences: extractReferencesFn, injectReferences: injectReferencesFn, }, - config: { - run: { - actions: { max: 1000 }, - }, - }, })); const data = getMockData({ params: ruleParams, @@ -1339,11 +1334,6 @@ describe('create()', () => { extractReferences: extractReferencesFn, injectReferences: injectReferencesFn, }, - config: { - run: { - actions: { max: 1000 }, - }, - }, })); const data = getMockData({ params: ruleParams, @@ -2098,11 +2088,6 @@ describe('create()', () => { isExportable: true, async executor() {}, producer: 'alerts', - config: { - run: { - actions: { max: 1000 }, - }, - }, }); await expect(rulesClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( `"params invalid: [param1]: expected value of type [string] but got [undefined]"` @@ -2628,11 +2613,6 @@ describe('create()', () => { extractReferences: jest.fn(), injectReferences: jest.fn(), }, - config: { - run: { - actions: { max: 1000 }, - }, - }, })); const data = getMockData({ schedule: { interval: '1s' } }); diff --git a/x-pack/plugins/alerting/server/rules_client/tests/lib.ts b/x-pack/plugins/alerting/server/rules_client/tests/lib.ts index b25fd2ab2c489..194ca6c8279a7 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/lib.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/lib.ts @@ -91,11 +91,6 @@ export function getBeforeSetup( isExportable: true, async executor() {}, producer: 'alerts', - config: { - run: { - actions: { max: 1000 }, - }, - }, })); rulesClientParams.getEventLogClient.mockResolvedValue( eventLogClient ?? eventLogClientMock.create() diff --git a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts index db84953291c73..0a51bc06b6e43 100644 --- a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts @@ -6,7 +6,7 @@ */ import { createExecutionHandler } from './create_execution_handler'; -import { ActionsCompletion, AlertExecutionStore, CreateExecutionHandlerOptions } from './types'; +import { ActionsCompletion, CreateExecutionHandlerOptions } from './types'; import { loggingSystemMock } from '@kbn/core/server/mocks'; import { actionsClientMock, @@ -19,6 +19,7 @@ import { asSavedObjectExecutionSource } from '@kbn/actions-plugin/server'; import { InjectActionParamsOpts } from './inject_action_params'; import { NormalizedRuleType } from '../rule_type_registry'; import { AlertInstanceContext, AlertInstanceState, RuleTypeParams, RuleTypeState } from '../types'; +import { AlertExecutionStore } from '../lib/alert_execution_store'; jest.mock('./inject_action_params', () => ({ injectActionParams: jest.fn(), @@ -48,11 +49,6 @@ const ruleType: NormalizedRuleType< }, executor: jest.fn(), producer: 'alerts', - config: { - run: { - actions: { max: 1000 }, - }, - }, }; const actionsClient = actionsClientMock.create(); @@ -103,6 +99,11 @@ const createExecutionHandlerParams: jest.Mocked< }, supportsEphemeralTasks: false, maxEphemeralActionsPerRule: 10, + actionsConfigMap: { + default: { + max: 1000, + }, + }, }; let alertExecutionStore: AlertExecutionStore; @@ -120,11 +121,7 @@ describe('Create Execution Handler', () => { mockActionsPlugin.renderActionParameterTemplates.mockImplementation( renderActionParameterTemplatesDefault ); - alertExecutionStore = { - numberOfTriggeredActions: 0, - numberOfGeneratedActions: 0, - triggeredActionsStatus: ActionsCompletion.COMPLETE, - }; + alertExecutionStore = new AlertExecutionStore(); }); test('enqueues execution per selected action', async () => { @@ -136,7 +133,8 @@ describe('Create Execution Handler', () => { alertId: '2', alertExecutionStore, }); - expect(alertExecutionStore.numberOfTriggeredActions).toBe(1); + expect(alertExecutionStore.getNumberOfTriggeredActions()).toBe(1); + expect(alertExecutionStore.getNumberOfGeneratedActions()).toBe(1); expect(mockActionsPlugin.getActionsClientWithRequest).toHaveBeenCalledWith( createExecutionHandlerParams.request ); @@ -244,7 +242,7 @@ describe('Create Execution Handler', () => { }, }); - expect(alertExecutionStore.triggeredActionsStatus).toBe(ActionsCompletion.COMPLETE); + expect(alertExecutionStore.getTriggeredActionsStatus()).toBe(ActionsCompletion.COMPLETE); }); test(`doesn't call actionsPlugin.execute for disabled actionTypes`, async () => { @@ -255,7 +253,16 @@ describe('Create Execution Handler', () => { const executionHandler = createExecutionHandler({ ...createExecutionHandlerParams, actions: [ - ...createExecutionHandlerParams.actions, + { + id: '2', + group: 'default', + actionTypeId: 'test2', + params: { + foo: true, + contextVal: 'My other {{context.value}} goes here', + stateVal: 'My other {{state.value}} goes here', + }, + }, { id: '2', group: 'default', @@ -275,7 +282,8 @@ describe('Create Execution Handler', () => { alertId: '2', alertExecutionStore, }); - expect(alertExecutionStore.numberOfTriggeredActions).toBe(1); + expect(alertExecutionStore.getNumberOfTriggeredActions()).toBe(1); + expect(alertExecutionStore.getNumberOfGeneratedActions()).toBe(2); expect(actionsClient.enqueueExecution).toHaveBeenCalledTimes(1); expect(actionsClient.enqueueExecution).toHaveBeenCalledWith({ consumer: 'rule-consumer', @@ -310,7 +318,14 @@ describe('Create Execution Handler', () => { const executionHandler = createExecutionHandler({ ...createExecutionHandlerParams, actions: [ - ...createExecutionHandlerParams.actions, + { + id: '1', + group: 'default', + actionTypeId: '.slack', + params: { + foo: true, + }, + }, { id: '2', group: 'default', @@ -331,7 +346,8 @@ describe('Create Execution Handler', () => { alertId: '2', alertExecutionStore, }); - expect(alertExecutionStore.numberOfTriggeredActions).toBe(0); + expect(alertExecutionStore.getNumberOfTriggeredActions()).toBe(0); + expect(alertExecutionStore.getNumberOfGeneratedActions()).toBe(2); expect(actionsClient.enqueueExecution).toHaveBeenCalledTimes(0); mockActionsPlugin.isActionExecutable.mockImplementation(() => true); @@ -358,7 +374,8 @@ describe('Create Execution Handler', () => { alertId: '2', alertExecutionStore, }); - expect(alertExecutionStore.numberOfTriggeredActions).toBe(0); + expect(alertExecutionStore.getNumberOfTriggeredActions()).toBe(0); + expect(alertExecutionStore.getNumberOfGeneratedActions()).toBe(0); expect(actionsClient.enqueueExecution).not.toHaveBeenCalled(); }); @@ -371,7 +388,8 @@ describe('Create Execution Handler', () => { alertId: '2', alertExecutionStore, }); - expect(alertExecutionStore.numberOfTriggeredActions).toBe(1); + expect(alertExecutionStore.getNumberOfTriggeredActions()).toBe(1); + expect(alertExecutionStore.getNumberOfGeneratedActions()).toBe(1); expect(actionsClient.enqueueExecution).toHaveBeenCalledTimes(1); expect(actionsClient.enqueueExecution.mock.calls[0]).toMatchInlineSnapshot(` Array [ @@ -466,23 +484,30 @@ describe('Create Execution Handler', () => { 'Invalid action group "invalid-group" for rule "test".' ); - expect(alertExecutionStore.numberOfTriggeredActions).toBe(0); - expect(alertExecutionStore.triggeredActionsStatus).toBe(ActionsCompletion.COMPLETE); + expect(alertExecutionStore.getNumberOfTriggeredActions()).toBe(0); + expect(alertExecutionStore.getNumberOfGeneratedActions()).toBe(0); + expect(alertExecutionStore.getTriggeredActionsStatus()).toBe(ActionsCompletion.COMPLETE); }); test('Stops triggering actions when the number of total triggered actions is reached the number of max executable actions', async () => { const executionHandler = createExecutionHandler({ ...createExecutionHandlerParams, - ruleType: { - ...ruleType, - config: { - run: { - actions: { max: 2 }, - }, + actionsConfigMap: { + default: { + max: 2, }, }, actions: [ - ...createExecutionHandlerParams.actions, + { + id: '1', + group: 'default', + actionTypeId: 'test2', + params: { + foo: true, + contextVal: 'My other {{context.value}} goes here', + stateVal: 'My other {{state.value}} goes here', + }, + }, { id: '2', group: 'default', @@ -506,11 +531,7 @@ describe('Create Execution Handler', () => { ], }); - alertExecutionStore = { - numberOfTriggeredActions: 0, - numberOfGeneratedActions: 0, - triggeredActionsStatus: ActionsCompletion.COMPLETE, - }; + alertExecutionStore = new AlertExecutionStore(); await executionHandler({ actionGroup: 'default', @@ -520,9 +541,90 @@ describe('Create Execution Handler', () => { alertExecutionStore, }); - expect(alertExecutionStore.numberOfTriggeredActions).toBe(2); - expect(alertExecutionStore.numberOfGeneratedActions).toBe(3); - expect(alertExecutionStore.triggeredActionsStatus).toBe(ActionsCompletion.PARTIAL); + expect(alertExecutionStore.getNumberOfTriggeredActions()).toBe(2); + expect(alertExecutionStore.getNumberOfGeneratedActions()).toBe(3); + expect(alertExecutionStore.getTriggeredActionsStatus()).toBe(ActionsCompletion.PARTIAL); + expect(createExecutionHandlerParams.logger.debug).toHaveBeenCalledTimes(1); expect(actionsClient.enqueueExecution).toHaveBeenCalledTimes(2); }); + + test('Skips triggering actions for a specific action type when it reaches the limit for that specific action type', async () => { + const executionHandler = createExecutionHandler({ + ...createExecutionHandlerParams, + actionsConfigMap: { + default: { + max: 4, + }, + 'test-action-type-id': { + max: 1, + }, + }, + actions: [ + ...createExecutionHandlerParams.actions, + { + id: '2', + group: 'default', + actionTypeId: 'test-action-type-id', + params: { + foo: true, + contextVal: 'My other {{context.value}} goes here', + stateVal: 'My other {{state.value}} goes here', + }, + }, + { + id: '3', + group: 'default', + actionTypeId: 'test-action-type-id', + params: { + foo: true, + contextVal: '{{context.value}} goes here', + stateVal: '{{state.value}} goes here', + }, + }, + { + id: '4', + group: 'default', + actionTypeId: 'another-action-type-id', + params: { + foo: true, + contextVal: '{{context.value}} goes here', + stateVal: '{{state.value}} goes here', + }, + }, + { + id: '5', + group: 'default', + actionTypeId: 'another-action-type-id', + params: { + foo: true, + contextVal: '{{context.value}} goes here', + stateVal: '{{state.value}} goes here', + }, + }, + ], + }); + + alertExecutionStore = new AlertExecutionStore(); + + await executionHandler({ + actionGroup: 'default', + context: {}, + state: { value: 'state-val' }, + alertId: '2', + alertExecutionStore, + }); + + expect(alertExecutionStore.getNumberOfTriggeredActions()).toBe(4); + expect(alertExecutionStore.getNumberOfGeneratedActions()).toBe(5); + expect(alertExecutionStore.getStatusByConnectorType('test').numberOfTriggeredActions).toBe(1); + expect( + alertExecutionStore.getStatusByConnectorType('test-action-type-id').numberOfTriggeredActions + ).toBe(1); + expect( + alertExecutionStore.getStatusByConnectorType('another-action-type-id') + .numberOfTriggeredActions + ).toBe(2); + expect(alertExecutionStore.getTriggeredActionsStatus()).toBe(ActionsCompletion.PARTIAL); + expect(actionsClient.enqueueExecution).toHaveBeenCalledTimes(4); + }); }); diff --git a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts index b6b6eb0eaf880..bc0e92c954f23 100644 --- a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts +++ b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts @@ -46,6 +46,7 @@ export function createExecutionHandler< ruleParams, supportsEphemeralTasks, maxEphemeralActionsPerRule, + actionsConfigMap, }: CreateExecutionHandlerOptions< Params, ExtractedParams, @@ -58,6 +59,7 @@ export function createExecutionHandler< const ruleTypeActionGroups = new Map( ruleType.actionGroups.map((actionGroup) => [actionGroup.id, actionGroup.name]) ); + return async ({ actionGroup, actionSubgroup, @@ -107,7 +109,7 @@ export function createExecutionHandler< }), })); - alertExecutionStore.numberOfGeneratedActions += actions.length; + alertExecutionStore.incrementNumberOfGeneratedActions(actions.length); const ruleLabel = `${ruleType.id}:${ruleId}: '${ruleName}'`; @@ -115,21 +117,48 @@ export function createExecutionHandler< let ephemeralActionsToSchedule = maxEphemeralActionsPerRule; for (const action of actions) { - if (alertExecutionStore.numberOfTriggeredActions >= ruleType.config!.run.actions.max) { - alertExecutionStore.triggeredActionsStatus = ActionsCompletion.PARTIAL; + const { actionTypeId } = action; + + alertExecutionStore.incrementNumberOfGeneratedActionsByConnectorType(actionTypeId); + + if (alertExecutionStore.hasReachedTheExecutableActionsLimit(actionsConfigMap)) { + alertExecutionStore.setTriggeredActionsStatusByConnectorType({ + actionTypeId, + status: ActionsCompletion.PARTIAL, + }); + logger.debug( + `Rule "${ruleId}" skipped scheduling action "${action.id}" because the maximum number of allowed actions has been reached.` + ); break; } if ( - !actionsPlugin.isActionExecutable(action.id, action.actionTypeId, { notifyUsage: true }) + alertExecutionStore.hasReachedTheExecutableActionsLimitByConnectorType({ + actionTypeId, + actionsConfigMap, + }) ) { + if (!alertExecutionStore.hasConnectorTypeReachedTheLimit(actionTypeId)) { + logger.debug( + `Rule "${ruleId}" skipped scheduling action "${action.id}" because the maximum number of allowed actions for connector type ${actionTypeId} has been reached.` + ); + } + alertExecutionStore.setTriggeredActionsStatusByConnectorType({ + actionTypeId, + status: ActionsCompletion.PARTIAL, + }); + continue; + } + + if (!actionsPlugin.isActionExecutable(action.id, actionTypeId, { notifyUsage: true })) { logger.warn( `Rule "${ruleId}" skipped scheduling action "${action.id}" because it is disabled` ); continue; } - alertExecutionStore.numberOfTriggeredActions++; + alertExecutionStore.incrementNumberOfTriggeredActions(); + alertExecutionStore.incrementNumberOfTriggeredActionsByConnectorType(actionTypeId); const namespace = spaceId === 'default' ? {} : { namespace: spaceId }; @@ -155,7 +184,7 @@ export function createExecutionHandler< }; // TODO would be nice to add the action name here, but it's not available - const actionLabel = `${action.actionTypeId}:${action.id}`; + const actionLabel = `${actionTypeId}:${action.id}`; if (supportsEphemeralTasks && ephemeralActionsToSchedule > 0) { ephemeralActionsToSchedule--; try { @@ -190,7 +219,7 @@ export function createExecutionHandler< { type: 'action', id: action.id, - typeId: action.actionTypeId, + typeId: actionTypeId, }, ], ...namespace, diff --git a/x-pack/plugins/alerting/server/task_runner/fixtures.ts b/x-pack/plugins/alerting/server/task_runner/fixtures.ts index 7a851a713c961..fae450ac7e60d 100644 --- a/x-pack/plugins/alerting/server/task_runner/fixtures.ts +++ b/x-pack/plugins/alerting/server/task_runner/fixtures.ts @@ -108,11 +108,6 @@ export const ruleType: jest.Mocked = { recoveryActionGroup: RecoveredActionGroup, executor: jest.fn(), producer: 'alerts', - config: { - run: { - actions: { max: 1000 }, - }, - }, }; export const mockRunNowResponse = { diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts index 5ed33093cd8bc..4a5be740949ad 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts @@ -134,6 +134,11 @@ describe('Task Runner', () => { maxEphemeralActionsPerRule: 10, cancelAlertsOnRuleTimeout: true, usageCounter: mockUsageCounter, + actionsConfigMap: { + default: { + max: 10000, + }, + }, }; const ephemeralTestParams: Array< @@ -2641,13 +2646,11 @@ describe('Task Runner', () => { const runnerResult = await taskRunner.run(); expect(runnerResult.monitoring?.execution.history.length).toBe(200); }); + test('Actions circuit breaker kicked in, should set status as warning and log a message in event log', async () => { - const ruleTypeWithConfig = { - ...ruleType, - config: { - run: { - actions: { max: 3 }, - }, + const actionsConfigMap = { + default: { + max: 3, }, }; @@ -2705,21 +2708,22 @@ describe('Task Runner', () => { ...mockedRuleTypeSavedObject, actions: mockActions, } as jest.ResolvedValue); - ruleTypeRegistry.get.mockReturnValue(ruleTypeWithConfig); + ruleTypeRegistry.get.mockReturnValue(ruleType); encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(SAVED_OBJECT); const taskRunner = new TaskRunner( - ruleTypeWithConfig, + ruleType, mockedTaskInstance, - taskRunnerFactoryInitializerParams, + { + ...taskRunnerFactoryInitializerParams, + actionsConfigMap, + }, inMemoryMetrics ); const runnerResult = await taskRunner.run(); - expect(actionsClient.enqueueExecution).toHaveBeenCalledTimes( - ruleTypeWithConfig.config.run.actions.max - ); + expect(actionsClient.enqueueExecution).toHaveBeenCalledTimes(actionsConfigMap.default.max); expect( taskRunnerFactoryInitializerParams.internalSavedObjectsRepository.update @@ -2745,6 +2749,15 @@ describe('Task Runner', () => { }, }) ); + + const logger = taskRunnerFactoryInitializerParams.logger; + expect(logger.debug).toHaveBeenCalledTimes(5); + + expect(logger.debug).nthCalledWith( + 3, + 'Rule "1" skipped scheduling action "4" because the maximum number of allowed actions has been reached.' + ); + const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; expect(eventLogger.logEvent).toHaveBeenCalledTimes(7); @@ -2818,7 +2831,7 @@ describe('Task Runner', () => { action: EVENT_LOG_ACTIONS.execute, outcome: 'success', status: 'warning', - numberOfTriggeredActions: ruleTypeWithConfig.config.run.actions.max, + numberOfTriggeredActions: actionsConfigMap.default.max, numberOfGeneratedActions: mockActions.length, reason: RuleExecutionStatusWarningReasons.MAX_EXECUTABLE_ACTIONS, task: true, @@ -2827,6 +2840,138 @@ describe('Task Runner', () => { ); }); + test('Actions circuit breaker kicked in with connectorType specific config and multiple alerts', async () => { + const actionsConfigMap = { + default: { + max: 30, + }, + '.server-log': { + max: 1, + }, + }; + + const warning = { + reason: RuleExecutionStatusWarningReasons.MAX_EXECUTABLE_ACTIONS, + message: translations.taskRunner.warning.maxExecutableActions, + }; + + taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true); + taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true); + + ruleType.executor.mockImplementation( + async ({ + services: executorServices, + }: RuleExecutorOptions< + RuleTypeParams, + RuleTypeState, + AlertInstanceState, + AlertInstanceContext, + string + >) => { + executorServices.alertFactory.create('1').scheduleActions('default'); + executorServices.alertFactory.create('2').scheduleActions('default'); + } + ); + + rulesClient.get.mockResolvedValue({ + ...mockedRuleTypeSavedObject, + actions: [ + { + group: 'default', + id: '1', + actionTypeId: '.server-log', + }, + { + group: 'default', + id: '2', + actionTypeId: '.server-log', + }, + { + group: 'default', + id: '3', + actionTypeId: '.server-log', + }, + { + group: 'default', + id: '4', + actionTypeId: 'any-action', + }, + { + group: 'default', + id: '5', + actionTypeId: 'any-action', + }, + ], + } as jest.ResolvedValue); + + ruleTypeRegistry.get.mockReturnValue(ruleType); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(SAVED_OBJECT); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(SAVED_OBJECT); + + const taskRunner = new TaskRunner( + ruleType, + mockedTaskInstance, + { + ...taskRunnerFactoryInitializerParams, + actionsConfigMap, + }, + inMemoryMetrics + ); + + const runnerResult = await taskRunner.run(); + + // 1x(.server-log) and 2x(any-action) per alert + expect(actionsClient.enqueueExecution).toHaveBeenCalledTimes(5); + + expect( + taskRunnerFactoryInitializerParams.internalSavedObjectsRepository.update + ).toHaveBeenCalledWith(...generateSavedObjectParams({ status: 'warning', warning })); + + expect(runnerResult).toEqual( + generateRunnerResult({ + state: true, + history: [true], + alertInstances: { + '1': { + meta: { + lastScheduledActions: { + date: new Date(DATE_1970), + group: 'default', + }, + }, + state: { + duration: 0, + start: '1970-01-01T00:00:00.000Z', + }, + }, + '2': { + meta: { + lastScheduledActions: { + date: new Date(DATE_1970), + group: 'default', + }, + }, + state: { + duration: 0, + start: '1970-01-01T00:00:00.000Z', + }, + }, + }, + }) + ); + + const logger = taskRunnerFactoryInitializerParams.logger; + expect(logger.debug).toHaveBeenCalledTimes(5); + + expect(logger.debug).nthCalledWith( + 3, + 'Rule "1" skipped scheduling action "1" because the maximum number of allowed actions for connector type .server-log has been reached.' + ); + + const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; + expect(eventLogger.logEvent).toHaveBeenCalledTimes(11); + }); + test('increments monitoring metrics after execution', async () => { const taskRunner = new TaskRunner( ruleType, 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 36703e7c28dd7..00c62af65f382 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.ts @@ -65,8 +65,6 @@ import { } from '../lib/create_alert_event_log_record_object'; import { InMemoryMetrics, IN_MEMORY_METRICS } from '../monitoring'; import { - ActionsCompletion, - AlertExecutionStore, GenerateNewAndRecoveredAlertEventsParams, LogActiveAndRecoveredAlertsParams, RuleTaskInstance, @@ -74,6 +72,7 @@ import { ScheduleActionsForRecoveredAlertsParams, TrackAlertDurationsParams, } from './types'; +import { AlertExecutionStore } from '../lib/alert_execution_store'; const FALLBACK_RETRY_INTERVAL = '5m'; const CONNECTIVITY_RETRY_INTERVAL = '5m'; @@ -231,6 +230,7 @@ export class TaskRunner< ruleParams, supportsEphemeralTasks: this.context.supportsEphemeralTasks, maxEphemeralActionsPerRule: this.context.maxEphemeralActionsPerRule, + actionsConfigMap: this.context.actionsConfigMap, }); } @@ -491,11 +491,7 @@ export class TaskRunner< }); } - const alertExecutionStore: AlertExecutionStore = { - numberOfTriggeredActions: 0, - numberOfGeneratedActions: 0, - triggeredActionsStatus: ActionsCompletion.COMPLETE, - }; + const alertExecutionStore = new AlertExecutionStore(); const ruleIsSnoozed = this.isRuleSnoozed(rule); if (!ruleIsSnoozed && this.shouldLogAndScheduleActionsForAlerts()) { @@ -567,7 +563,11 @@ export class TaskRunner< return { metrics: searchMetrics, - alertExecutionStore, + alertExecutionMetrics: { + numberOfTriggeredActions: alertExecutionStore.getNumberOfTriggeredActions(), + numberOfGeneratedActions: alertExecutionStore.getNumberOfGeneratedActions(), + triggeredActionsStatus: alertExecutionStore.getTriggeredActionsStatus(), + }, alertTypeState: updatedRuleTypeState || undefined, alertInstances: mapValues< Record>, @@ -874,7 +874,7 @@ export class TaskRunner< executionState: RuleExecutionState ): RuleTaskState => { return { - ...omit(executionState, ['alertExecutionStore', 'metrics']), + ...omit(executionState, ['alertExecutionMetrics', 'metrics']), previousStartedAt: startedAt, }; }; diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts index 66a7b85a94259..e0b7449d09b41 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts @@ -57,11 +57,6 @@ const ruleType: jest.Mocked = { producer: 'alerts', cancelAlertsOnRuleTimeout: true, ruleTaskTimeout: '5m', - config: { - run: { - actions: { max: 1000 }, - }, - }, }; let fakeTimer: sinon.SinonFakeTimers; @@ -134,6 +129,11 @@ describe('Task Runner Cancel', () => { maxEphemeralActionsPerRule: 10, cancelAlertsOnRuleTimeout: true, usageCounter: mockUsageCounter, + actionsConfigMap: { + default: { + max: 1000, + }, + }, }; const mockDate = new Date('2019-02-12T21:01:22.479Z'); diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts index 0fe747ab7a93e..e787617800356 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts @@ -101,6 +101,11 @@ describe('Task Runner Factory', () => { cancelAlertsOnRuleTimeout: true, executionContext, usageCounter: mockUsageCounter, + actionsConfigMap: { + default: { + max: 1000, + }, + }, }; beforeEach(() => { diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner_factory.ts b/x-pack/plugins/alerting/server/task_runner/task_runner_factory.ts index 5a32e86593fa4..e7c483b944ed1 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner_factory.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner_factory.ts @@ -34,6 +34,7 @@ import { TaskRunner } from './task_runner'; import { RulesClient } from '../rules_client'; import { NormalizedRuleType } from '../rule_type_registry'; import { InMemoryMetrics } from '../monitoring'; +import { ActionsConfigMap } from '../lib/get_actions_config_map'; export interface TaskRunnerContext { logger: Logger; @@ -53,6 +54,7 @@ export interface TaskRunnerContext { kibanaBaseUrl: string | undefined; supportsEphemeralTasks: boolean; maxEphemeralActionsPerRule: number; + actionsConfigMap: ActionsConfigMap; cancelAlertsOnRuleTimeout: boolean; usageCounter?: UsageCounter; } diff --git a/x-pack/plugins/alerting/server/task_runner/types.ts b/x-pack/plugins/alerting/server/task_runner/types.ts index 735470cf7220b..246e7721d6ed4 100644 --- a/x-pack/plugins/alerting/server/task_runner/types.ts +++ b/x-pack/plugins/alerting/server/task_runner/types.ts @@ -27,6 +27,8 @@ import { Alert } from '../alert'; import { NormalizedRuleType } from '../rule_type_registry'; import { ExecutionHandler } from './create_execution_handler'; import { RawRule } from '../types'; +import { ActionsConfigMap } from '../lib/get_actions_config_map'; +import { AlertExecutionStore } from '../lib/alert_execution_store'; export interface RuleTaskRunResultWithActions { state: RuleExecutionState; @@ -145,6 +147,7 @@ export interface CreateExecutionHandlerOptions< ruleParams: RuleTypeParams; supportsEphemeralTasks: boolean; maxEphemeralActionsPerRule: number; + actionsConfigMap: ActionsConfigMap; } export interface ExecutionHandlerOptions { @@ -160,9 +163,3 @@ export enum ActionsCompletion { COMPLETE = 'complete', PARTIAL = 'partial', } - -export interface AlertExecutionStore { - numberOfTriggeredActions: number; - numberOfGeneratedActions: number; - triggeredActionsStatus: ActionsCompletion; -} diff --git a/x-pack/plugins/alerting/server/types.ts b/x-pack/plugins/alerting/server/types.ts index 32c6e41c63268..4a2290d0bde33 100644 --- a/x-pack/plugins/alerting/server/types.ts +++ b/x-pack/plugins/alerting/server/types.ts @@ -41,7 +41,6 @@ import { RuleMonitoring, MappedParams, } from '../common'; -import { RuleTypeConfig } from './config'; export type WithoutQueryAndParams = Pick>; export type SpaceIdToNamespaceFunction = (spaceId?: string) => string | undefined; @@ -170,7 +169,6 @@ export interface RuleType< ruleTaskTimeout?: string; cancelAlertsOnRuleTimeout?: boolean; doesSetRecoveryContext?: boolean; - config?: RuleTypeConfig; } export type UntypedRuleType = RuleType< RuleTypeParams, diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts index 8c103ef8ce52c..93b1ba7d76a47 100644 --- a/x-pack/test/alerting_api_integration/common/config.ts +++ b/x-pack/test/alerting_api_integration/common/config.ts @@ -50,6 +50,7 @@ const enabledActionTypes = [ 'test.no-attempts-rate-limit', 'test.throw', 'test.excluded', + 'test.capped', ]; export function createTestConfig(name: string, options: CreateTestConfigOptions) { @@ -163,6 +164,9 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) '--xpack.alerting.invalidateApiKeysTask.interval="15s"', '--xpack.alerting.healthCheck.interval="1s"', '--xpack.alerting.rules.minimumScheduleInterval.value="1s"', + `--xpack.alerting.rules.run.actions.connectorTypeOverrides=${JSON.stringify([ + { id: 'test.capped', max: '1' }, + ])}`, `--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`, `--xpack.actions.rejectUnauthorized=${rejectUnauthorized}`, `--xpack.actions.microsoftGraphApiUrl=${servers.kibana.protocol}://${servers.kibana.hostname}:${servers.kibana.port}/api/_actions-FTS-external-service-simulators/exchange/users/test@/sendMail`, diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/action_types.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/action_types.ts index 357458cc38e41..c83a1c543b5a7 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/action_types.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/action_types.ts @@ -31,8 +31,17 @@ export function defineActionTypes( throw new Error('this action is intended to fail'); }, }; + const cappedActionType: ActionType = { + id: 'test.capped', + name: 'Test: Capped', + minimumLicenseRequired: 'gold', + async executor() { + return { status: 'ok', actionId: '' }; + }, + }; actions.registerType(noopActionType); actions.registerType(throwActionType); + actions.registerType(cappedActionType); actions.registerType(getIndexRecordActionType()); actions.registerType(getDelayedActionType()); actions.registerType(getFailingActionType()); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/capped_action_type.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/capped_action_type.ts new file mode 100644 index 0000000000000..374dbddfc17b4 --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/capped_action_type.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 expect from '@kbn/expect'; +import { Spaces } from '../../scenarios'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { getEventLog, getTestRuleData, getUrlPrefix, ObjectRemover } from '../../../common/lib'; + +// eslint-disable-next-line import/no-default-export +export default function createCappedActionsTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const retry = getService('retry'); + + describe('Capped action type', () => { + const objectRemover = new ObjectRemover(supertest); + + after(() => objectRemover.removeAll()); + + it('should not trigger actions more than connector types limit', async () => { + const { body: createdAction01 } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'MY action', + connector_type_id: 'test.capped', + config: {}, + secrets: {}, + }) + .expect(200); + const { body: createdAction02 } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'MY action', + connector_type_id: 'test.capped', + config: {}, + secrets: {}, + }) + .expect(200); + const { body: createdAction03 } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'MY action', + connector_type_id: 'test.capped', + config: {}, + secrets: {}, + }) + .expect(200); + + objectRemover.add(Spaces.space1.id, createdAction01.id, 'action', 'actions'); + objectRemover.add(Spaces.space1.id, createdAction02.id, 'action', 'actions'); + objectRemover.add(Spaces.space1.id, createdAction03.id, 'action', 'actions'); + + const { body: createdRule } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send( + getTestRuleData({ + name: 'should not trigger actions when connector type limit is reached', + rule_type_id: 'test.patternFiring', + schedule: { interval: '1s' }, + throttle: null, + notify_when: 'onActiveAlert', + params: { + pattern: { instance: arrayOfTrues(100) }, + }, + actions: [ + { + id: createdAction01.id, + group: 'default', + params: {}, + }, + { + id: createdAction02.id, + group: 'default', + params: {}, + }, + { + id: createdAction03.id, + group: 'default', + params: {}, + }, + ], + }) + ) + .expect(200); + + objectRemover.add(Spaces.space1.id, createdRule.id, 'rule', 'alerting'); + + await getRuleEvents(createdRule.id); + const [executionEvent] = await getRuleEvents(createdRule.id, 1); + + expect( + executionEvent?.kibana?.alert?.rule?.execution?.metrics?.number_of_generated_actions + ).to.be.eql(3, 'all the generated actions'); + expect( + executionEvent?.kibana?.alert?.rule?.execution?.metrics?.number_of_triggered_actions + ).to.be.eql(1, 'only 1 action was triggered'); + }); + }); + + async function getRuleEvents(id: string, minActions: number = 1) { + return await retry.try(async () => { + return await getEventLog({ + getService, + spaceId: Spaces.space1.id, + type: 'alert', + id, + provider: 'alerting', + actions: new Map([['execute', { gte: minActions }]]), + }); + }); + } +} + +function arrayOfTrues(length: number) { + const result = []; + for (let i = 0; i < length; i++) { + result.push(true); + } + return result; +} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts index bdc5a6c5ef646..4975207c02391 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts @@ -45,6 +45,7 @@ export default function alertingTests({ loadTestFile, getService }: FtrProviderC loadTestFile(require.resolve('./ephemeral')); loadTestFile(require.resolve('./event_log_alerts')); loadTestFile(require.resolve('./snooze')); + loadTestFile(require.resolve('./capped_action_type')); loadTestFile(require.resolve('./scheduled_task_id')); // Do not place test files here, due to https://github.com/elastic/kibana/issues/123059 From a52829d0404ab915192b0ce1073995245cd9db1d Mon Sep 17 00:00:00 2001 From: Shahzad Date: Tue, 26 Apr 2022 10:33:47 +0200 Subject: [PATCH 04/29] [Exploratory view] Move refresh button to header and also fix it's working (#130665) --- .../configurations/lens_attributes.ts | 4 +- .../exploratory_view.test.tsx | 1 - .../exploratory_view/exploratory_view.tsx | 42 ++++++------------- .../header/refresh_button.tsx | 36 ++++++++++++++++ .../hooks/use_lens_attributes.ts | 2 +- .../hooks/use_series_storage.tsx | 7 ++++ .../shared/exploratory_view/index.tsx | 18 +++++--- .../exploratory_view/lens_embeddable.tsx | 2 +- .../shared/exploratory_view/rtl_helpers.tsx | 1 + 9 files changed, 72 insertions(+), 41 deletions(-) create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/header/refresh_button.tsx diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts index 6ff6e8ed3f586..cce0a0a3bc5f6 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts @@ -785,7 +785,7 @@ export class LensAttributes { }; } - getJSON(): TypedLensByValueInput['attributes'] { + getJSON(lastRefresh?: number): TypedLensByValueInput['attributes'] { const uniqueIndexPatternsIds = Array.from( new Set([...this.layerConfigs.map(({ indexPattern }) => indexPattern.id)]) ); @@ -794,7 +794,7 @@ export class LensAttributes { return { title: 'Prefilled from exploratory view app', - description: '', + description: lastRefresh ? `Last refreshed at ${new Date(lastRefresh).toISOString()}` : '', visualizationType: 'lnsXY', references: [ ...uniqueIndexPatternsIds.map((patternId) => ({ diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx index f6a41fb6bef67..2bd79116dc45b 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx @@ -63,7 +63,6 @@ describe('ExploratoryView', () => { it('shows/hides the chart', async () => { render(); - expect(screen.queryByText('Refresh')).toBeInTheDocument(); const toggleButton = await screen.findByText('Hide chart'); expect(toggleButton).toBeInTheDocument(); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx index 905f6f20a2e6b..ac4e094e5abbc 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx @@ -9,7 +9,6 @@ import { i18n } from '@kbn/i18n'; import React, { useEffect, useRef, useState } from 'react'; import styled from 'styled-components'; import { - EuiButton, EuiButtonEmpty, EuiResizableContainer, EuiTitle, @@ -26,7 +25,6 @@ import { useAppDataViewContext } from './hooks/use_app_data_view'; import { SeriesViews } from './views/series_views'; import { LensEmbeddable } from './lens_embeddable'; import { EmptyView } from './components/empty_view'; -import { ChartTimeRange, LastUpdated } from './header/last_updated'; import { useExpViewTimeRange } from './hooks/use_time_range'; import { ExpViewActionMenu } from './components/action_menu'; import { useExploratoryView } from './contexts/exploratory_view_config'; @@ -49,15 +47,14 @@ export function ExploratoryView({ const { isEditMode } = useExploratoryView(); - const [chartTimeRangeContext, setChartTimeRangeContext] = useState(); - const [lensAttributes, setLensAttributes] = useState( null ); const { loadDataView, loading } = useAppDataViewContext(); - const { firstSeries, allSeries, lastRefresh, reportType, setLastRefresh } = useSeriesStorage(); + const { firstSeries, allSeries, lastRefresh, reportType, setChartTimeRangeContext } = + useSeriesStorage(); const lensAttributesT = useLensAttributes(); const timeRange = useExpViewTimeRange(); @@ -115,7 +112,7 @@ export function ExploratoryView({ return ( <> - + - {hiddenPanel === 'chartPanel' ? null : ( - <> - - - - - setLastRefresh(Date.now())} - size="s" - > - {REFRESH_LABEL} - - - - )} .euiPanel { padding-bottom: 0; + padding-top: 0; + } + .expExpressionRenderer__expression { + padding-bottom: 0 !important; + padding-top: 0 !important; } } `; @@ -218,6 +204,10 @@ const ShowPreview = styled(EuiButtonEmpty)` bottom: 34px; `; +const PREVIEW_LABEL = i18n.translate('xpack.observability.overview.exploratoryView.preview', { + defaultMessage: 'Preview', +}); + const HIDE_CHART_LABEL = i18n.translate('xpack.observability.overview.exploratoryView.hideChart', { defaultMessage: 'Hide chart', }); @@ -226,14 +216,6 @@ const SHOW_CHART_LABEL = i18n.translate('xpack.observability.overview.explorator defaultMessage: 'Show chart', }); -const PREVIEW_LABEL = i18n.translate('xpack.observability.overview.exploratoryView.preview', { - defaultMessage: 'Preview', -}); - -const REFRESH_LABEL = i18n.translate('xpack.observability.overview.exploratoryView.refresh', { - defaultMessage: 'Refresh', -}); - const LENS_NOT_AVAILABLE = i18n.translate( 'xpack.observability.overview.exploratoryView.lensDisabled', { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/refresh_button.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/refresh_button.tsx new file mode 100644 index 0000000000000..eb31669453dbb --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/refresh_button.tsx @@ -0,0 +1,36 @@ +/* + * 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 React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { LastUpdated } from './last_updated'; +import { useSeriesStorage } from '../hooks/use_series_storage'; + +export function RefreshButton() { + const { setLastRefresh, chartTimeRangeContext } = useSeriesStorage(); + + return ( + + + + + + setLastRefresh(Date.now())}> + {REFRESH_LABEL} + + + + ); +} + +export const REFRESH_LABEL = i18n.translate( + 'xpack.observability.overview.exploratoryView.refresh', + { + defaultMessage: 'Refresh', + } +); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts index 8d47d42e2007e..0e4620f1d520b 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts @@ -121,7 +121,7 @@ export const useLensAttributes = (): TypedLensByValueInput['attributes'] | null const lensAttributes = new LensAttributes(layerConfigs); - return lensAttributes.getJSON(); + return lensAttributes.getJSON(lastRefresh); // we also want to check the state on allSeries changes // eslint-disable-next-line react-hooks/exhaustive-deps }, [dataViews, reportType, storage, theme, lastRefresh, allSeries]); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx index 24d5fc1d20615..8da1a4591a73d 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx @@ -8,6 +8,7 @@ import React, { createContext, useContext, useState, useEffect, useCallback } from 'react'; import { IKbnUrlStateStorage, ISessionStorageStateStorage } from '@kbn/kibana-utils-plugin/public'; import { OperationType, SeriesType } from '@kbn/lens-plugin/public'; +import { ChartTimeRange } from '../header/last_updated'; import { useUiTracker } from '../../../../hooks/use_track_metric'; import type { AppDataType, @@ -32,6 +33,8 @@ export interface SeriesContextValue { setReportType: (reportType: ReportViewType) => void; storage: IKbnUrlStateStorage | ISessionStorageStateStorage; reportType: ReportViewType; + chartTimeRangeContext?: ChartTimeRange; + setChartTimeRangeContext: React.Dispatch>; } export const UrlStorageContext = createContext({} as SeriesContextValue); @@ -56,6 +59,8 @@ export function UrlStorageContextProvider({ const [lastRefresh, setLastRefresh] = useState(() => Date.now()); + const [chartTimeRangeContext, setChartTimeRangeContext] = useState(); + const [reportType, setReportType] = useState( () => ((storage as IKbnUrlStateStorage).get(reportTypeKey) ?? '') as ReportViewType ); @@ -135,6 +140,8 @@ export function UrlStorageContextProvider({ setLastRefresh, setReportType, reportType, + chartTimeRangeContext, + setChartTimeRangeContext, firstSeries: firstSeries!, }; return {children}; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx index f9a58a8ff7433..82713f152aa4a 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx @@ -22,6 +22,7 @@ import { DataViewContextProvider } from './hooks/use_app_data_view'; import { UrlStorageContextProvider } from './hooks/use_series_storage'; import { useTrackPageview } from '../../..'; import { usePluginContext } from '../../../hooks/use_plugin_context'; +import { RefreshButton } from './header/refresh_button'; const PAGE_TITLE = i18n.translate('xpack.observability.expView.heading.label', { defaultMessage: 'Explore data', @@ -72,13 +73,18 @@ export function ExploratoryViewPage({ }); return ( - - - + + ], + }} + > + - - - + + + ); } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/lens_embeddable.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/lens_embeddable.tsx index cd1fd4cc014bc..3f7583f874bee 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/lens_embeddable.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/lens_embeddable.tsx @@ -46,7 +46,7 @@ export function LensEmbeddable(props: Props) { (isLoading) => { const timeLoaded = Date.now(); - setChartTimeRangeContext({ + setChartTimeRangeContext?.({ lastUpdated: timeLoaded, to: parseRelativeDate(timeRange?.to || '')?.valueOf(), from: parseRelativeDate(timeRange?.from || '')?.valueOf(), diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx index 23c635be3ba81..fe7fe29ee4637 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx @@ -319,6 +319,7 @@ function mockSeriesStorageContext({ firstSeries: mockDataSeries[0], allSeries: mockDataSeries, setReportType: jest.fn(), + setChartTimeRangeContext: jest.fn(), storage: { get: jest .fn() From eb043f77028be5f63b822b2412e6b401fa4d3fa1 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Tue, 26 Apr 2022 11:04:34 +0100 Subject: [PATCH 05/29] [Connectors] moves the connector deprecation check to the server side (#130541) This PR fixes a UI glitch where preconfigured connectors couldn't show a deprecation warning on SN connectors as the check on the client side couldn't support them. --- .../actions-and-connectors/create.asciidoc | 1 + docs/api/actions-and-connectors/get.asciidoc | 1 + .../actions-and-connectors/get_all.asciidoc | 2 + .../legacy/create.asciidoc | 1 + .../legacy/get.asciidoc | 1 + .../legacy/get_all.asciidoc | 4 +- .../legacy/update.asciidoc | 1 + .../actions-and-connectors/update.asciidoc | 1 + .../cases/cases-api-find-connectors.asciidoc | 1 + .../testing-connectors.asciidoc | 1 + .../server/action_type_registry.test.ts | 1 + .../actions/server/actions_client.test.ts | 20 +++++++ .../plugins/actions/server/actions_client.ts | 18 +++++- .../server/create_execute_function.test.ts | 3 + .../server/lib/action_executor.test.ts | 9 +++ .../server/lib/is_conector_deprecated.test.ts | 51 ++++++++++++++++ .../server/lib/is_conector_deprecated.ts | 29 +++++++++ x-pack/plugins/actions/server/plugin.ts | 10 +++- .../alert_history_es_index.ts | 1 + .../actions/server/routes/create.test.ts | 11 +++- .../plugins/actions/server/routes/create.ts | 2 + .../plugins/actions/server/routes/get.test.ts | 5 ++ x-pack/plugins/actions/server/routes/get.ts | 2 + .../plugins/actions/server/routes/get_all.ts | 10 +++- .../server/routes/legacy/create.test.ts | 3 + .../actions/server/routes/legacy/get.test.ts | 4 ++ .../server/routes/legacy/update.test.ts | 3 + .../actions/server/routes/update.test.ts | 4 ++ .../plugins/actions/server/routes/update.ts | 2 + .../action_task_params_migrations.test.ts | 1 + x-pack/plugins/actions/server/types.ts | 1 + .../server/usage/actions_telemetry.test.ts | 5 ++ .../server/rules_client/tests/create.test.ts | 7 +++ .../alerting/server/rules_client/tests/lib.ts | 3 + .../server/rules_client/tests/update.test.ts | 12 ++++ .../server/client/configure/client.test.ts | 4 ++ .../routes/__mocks__/request_responses.ts | 2 + .../routes/rules/utils.test.ts | 11 ++++ .../synthetics/public/state/api/alerts.ts | 2 + .../builtin_action_types/email/email.test.tsx | 6 ++ .../es_index/es_index_params.test.tsx | 2 + .../builtin_action_types/jira/jira.test.tsx | 1 + .../jira/jira_connectors.test.tsx | 5 ++ .../jira/jira_params.test.tsx | 1 + .../resilient/resilient.test.tsx | 1 + .../resilient/resilient_connectors.test.tsx | 5 ++ .../resilient/resilient_params.test.tsx | 1 + .../server_log/server_log.test.tsx | 1 + .../servicenow/helpers.test.ts | 15 +++-- .../servicenow/helpers.ts | 9 +-- .../servicenow/servicenow.test.tsx | 1 + .../servicenow/servicenow_connectors.test.tsx | 4 ++ .../servicenow/servicenow_connectors.tsx | 3 +- .../servicenow_itom_params.test.tsx | 1 + .../servicenow_itsm_params.test.tsx | 1 + .../servicenow/servicenow_itsm_params.tsx | 3 +- .../servicenow/servicenow_sir_params.test.tsx | 1 + .../servicenow/servicenow_sir_params.tsx | 3 +- .../servicenow/update_connector.test.tsx | 1 + .../servicenow/use_choices.test.tsx | 1 + .../servicenow/use_get_app_info.test.tsx | 1 + .../servicenow/use_get_choices.test.tsx | 1 + .../swimlane/swimlane_params.test.tsx | 1 + .../swimlane/use_get_application.test.tsx | 1 + .../webhook/webhook.test.tsx | 2 + .../webhook/webhook_connectors.test.tsx | 4 ++ .../xmatters/xmatters.test.tsx | 6 ++ .../xmatters/xmatters_connectors.test.tsx | 7 +++ .../lib/action_connector_api/connectors.ts | 2 + .../lib/action_connector_api/create.test.ts | 4 +- .../lib/action_connector_api/create.ts | 5 +- .../lib/action_connector_api/update.test.ts | 2 + .../lib/action_connector_api/update.ts | 2 + .../lib/check_action_type_enabled.test.tsx | 2 + .../application/lib/value_validators.test.ts | 3 + .../action_connector_form.test.tsx | 1 + .../action_form.test.tsx | 7 +++ .../action_type_form.test.tsx | 3 + .../connector_edit_flyout.test.tsx | 2 + .../connector_reducer.test.ts | 1 + .../connectors_selection.test.tsx | 1 + .../actions_connectors_list.test.tsx | 7 +++ .../components/actions_connectors_list.tsx | 5 +- .../components/rule_details.test.tsx | 2 + .../common/connectors_seleciton.test.tsx | 59 ------------------- .../public/common/connectors_selection.tsx | 43 -------------- .../triggers_actions_ui/public/types.ts | 1 + .../alerting_api_integration/common/config.ts | 12 ++++ .../actions/builtin_action_types/email.ts | 8 +++ .../actions/builtin_action_types/es_index.ts | 4 ++ .../actions/builtin_action_types/jira.ts | 2 + .../actions/builtin_action_types/pagerduty.ts | 2 + .../actions/builtin_action_types/resilient.ts | 2 + .../builtin_action_types/server_log.ts | 2 + .../builtin_action_types/servicenow_itom.ts | 2 + .../builtin_action_types/servicenow_itsm.ts | 2 + .../builtin_action_types/servicenow_sir.ts | 2 + .../actions/builtin_action_types/slack.ts | 2 + .../actions/builtin_action_types/swimlane.ts | 2 + .../actions/builtin_action_types/webhook.ts | 4 ++ .../actions/builtin_action_types/xmatters.ts | 2 + .../tests/actions/create.ts | 1 + .../security_and_spaces/tests/actions/get.ts | 2 + .../tests/actions/get_all.ts | 38 ++++++++++++ .../tests/actions/update.ts | 1 + .../tests/telemetry/actions_telemetry.ts | 2 +- .../actions/builtin_action_types/es_index.ts | 4 ++ .../spaces_only/tests/actions/create.ts | 2 + .../spaces_only/tests/actions/get.ts | 16 +++++ .../spaces_only/tests/actions/get_all.ts | 41 +++++++++++++ .../tests/actions/type_not_enabled.ts | 2 + .../spaces_only/tests/actions/update.ts | 2 + .../tests/trial/configure/get_connectors.ts | 4 ++ .../tests/trial/configure/get_connectors.ts | 4 ++ 114 files changed, 526 insertions(+), 132 deletions(-) create mode 100644 x-pack/plugins/actions/server/lib/is_conector_deprecated.test.ts create mode 100644 x-pack/plugins/actions/server/lib/is_conector_deprecated.ts delete mode 100644 x-pack/plugins/triggers_actions_ui/public/common/connectors_seleciton.test.tsx diff --git a/docs/api/actions-and-connectors/create.asciidoc b/docs/api/actions-and-connectors/create.asciidoc index c9ea31c98cf19..401f4c5372688 100644 --- a/docs/api/actions-and-connectors/create.asciidoc +++ b/docs/api/actions-and-connectors/create.asciidoc @@ -74,6 +74,7 @@ The API returns the following: "executionTimeField": null }, "is_preconfigured": false, + "is_deprecated": false, "is_missing_secrets": false } -------------------------------------------------- diff --git a/docs/api/actions-and-connectors/get.asciidoc b/docs/api/actions-and-connectors/get.asciidoc index 95336e7f55d30..bc6b5fa8f364c 100644 --- a/docs/api/actions-and-connectors/get.asciidoc +++ b/docs/api/actions-and-connectors/get.asciidoc @@ -51,6 +51,7 @@ The API returns the following: "executionTimeField": null }, "is_preconfigured": false, + "is_deprecated": false, "is_missing_secrets": false } -------------------------------------------------- diff --git a/docs/api/actions-and-connectors/get_all.asciidoc b/docs/api/actions-and-connectors/get_all.asciidoc index 943c7d123775f..26bb7247e2ce1 100644 --- a/docs/api/actions-and-connectors/get_all.asciidoc +++ b/docs/api/actions-and-connectors/get_all.asciidoc @@ -44,6 +44,7 @@ The API returns the following: "connector_type_id": ".email", "name": "email: preconfigured-mail-connector", "is_preconfigured": true, + "is_deprecated": false, "referenced_by_count": 0 <1> }, { @@ -56,6 +57,7 @@ The API returns the following: "executionTimeField": null }, "is_preconfigured": false, + "is_deprecated": false, "is_missing_secrets": false, "referenced_by_count": 3 } diff --git a/docs/api/actions-and-connectors/legacy/create.asciidoc b/docs/api/actions-and-connectors/legacy/create.asciidoc index e0d531a2befb9..5b5b71b1d6daa 100644 --- a/docs/api/actions-and-connectors/legacy/create.asciidoc +++ b/docs/api/actions-and-connectors/legacy/create.asciidoc @@ -76,6 +76,7 @@ The API returns the following: "executionTimeField": null }, "isPreconfigured": false, + "isDeprecated": false, "isMissingSecrets": false } -------------------------------------------------- diff --git a/docs/api/actions-and-connectors/legacy/get.asciidoc b/docs/api/actions-and-connectors/legacy/get.asciidoc index dab462e3ae4fb..d6554d606a286 100644 --- a/docs/api/actions-and-connectors/legacy/get.asciidoc +++ b/docs/api/actions-and-connectors/legacy/get.asciidoc @@ -53,6 +53,7 @@ The API returns the following: "executionTimeField": null }, "isPreconfigured": false, + "isDeprecated": false, "isMissingSecrets": false } -------------------------------------------------- diff --git a/docs/api/actions-and-connectors/legacy/get_all.asciidoc b/docs/api/actions-and-connectors/legacy/get_all.asciidoc index 2180720ce6542..9e52b6883ec11 100644 --- a/docs/api/actions-and-connectors/legacy/get_all.asciidoc +++ b/docs/api/actions-and-connectors/legacy/get_all.asciidoc @@ -45,7 +45,8 @@ The API returns the following: "id": "preconfigured-mail-action", "actionTypeId": ".email", "name": "email: preconfigured-mail-action", - "isPreconfigured": true + "isPreconfigured": true, + "isDeprecated": false }, { "id": "c55b6eb0-6bad-11eb-9f3b-611eebc6c3ad", @@ -57,6 +58,7 @@ The API returns the following: "executionTimeField": null }, "isPreconfigured": false, + "isDeprecated": false, "isMissingSecrets": false } ] diff --git a/docs/api/actions-and-connectors/legacy/update.asciidoc b/docs/api/actions-and-connectors/legacy/update.asciidoc index 5202f8124e6a8..1e6a4e71c8b81 100644 --- a/docs/api/actions-and-connectors/legacy/update.asciidoc +++ b/docs/api/actions-and-connectors/legacy/update.asciidoc @@ -71,6 +71,7 @@ The API returns the following: "executionTimeField": null }, "isPreconfigured": false, + "isDeprecated": false, "isMissingSecrets": false } -------------------------------------------------- diff --git a/docs/api/actions-and-connectors/update.asciidoc b/docs/api/actions-and-connectors/update.asciidoc index 0b7dcc898a122..7ccb10714f474 100644 --- a/docs/api/actions-and-connectors/update.asciidoc +++ b/docs/api/actions-and-connectors/update.asciidoc @@ -69,6 +69,7 @@ The API returns the following: "executionTimeField": null }, "is_preconfigured": false, + "is_deprecated": false, "is_missing_secrets": false } -------------------------------------------------- diff --git a/docs/api/cases/cases-api-find-connectors.asciidoc b/docs/api/cases/cases-api-find-connectors.asciidoc index 0a1554cfde4f8..b12be5621e991 100644 --- a/docs/api/cases/cases-api-find-connectors.asciidoc +++ b/docs/api/cases/cases-api-find-connectors.asciidoc @@ -55,6 +55,7 @@ The API returns a JSON object describing the connectors and their settings: "projectKey":"ES" }, "isPreconfigured":false, + "isDeprecated": false, "referencedByCount":0 }] -------------------------------------------------- \ No newline at end of file diff --git a/docs/user/alerting/troubleshooting/testing-connectors.asciidoc b/docs/user/alerting/troubleshooting/testing-connectors.asciidoc index f90a7ebc35614..64ba106655321 100644 --- a/docs/user/alerting/troubleshooting/testing-connectors.asciidoc +++ b/docs/user/alerting/troubleshooting/testing-connectors.asciidoc @@ -39,6 +39,7 @@ $ kbn-action ls "service": null }, "isPreconfigured": false, + "isDeprecated": false, "referencedByCount": 0 } ] diff --git a/x-pack/plugins/actions/server/action_type_registry.test.ts b/x-pack/plugins/actions/server/action_type_registry.test.ts index 760aafbefbba8..ec88ba5fb12a7 100644 --- a/x-pack/plugins/actions/server/action_type_registry.test.ts +++ b/x-pack/plugins/actions/server/action_type_registry.test.ts @@ -42,6 +42,7 @@ beforeEach(() => { name: 'Slack #xyz', secrets: {}, isPreconfigured: true, + isDeprecated: false, }, ], }; diff --git a/x-pack/plugins/actions/server/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client.test.ts index 0b20e7078dd3b..afee13b8c9bca 100644 --- a/x-pack/plugins/actions/server/actions_client.test.ts +++ b/x-pack/plugins/actions/server/actions_client.test.ts @@ -311,6 +311,7 @@ describe('create()', () => { expect(result).toEqual({ id: '1', isPreconfigured: false, + isDeprecated: false, name: 'my name', actionTypeId: 'my-action-type', isMissingSecrets: false, @@ -443,6 +444,7 @@ describe('create()', () => { expect(result).toEqual({ id: '1', isPreconfigured: false, + isDeprecated: false, name: 'my name', actionTypeId: 'my-action-type', isMissingSecrets: false, @@ -632,6 +634,7 @@ describe('get()', () => { test: 'test1', }, isPreconfigured: true, + isDeprecated: false, name: 'test', config: { foo: 'bar', @@ -689,6 +692,7 @@ describe('get()', () => { test: 'test1', }, isPreconfigured: true, + isDeprecated: false, name: 'test', config: { foo: 'bar', @@ -778,6 +782,7 @@ describe('get()', () => { expect(result).toEqual({ id: '1', isPreconfigured: false, + isDeprecated: false, }); expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1); expect(unsecuredSavedObjectsClient.get.mock.calls[0]).toMatchInlineSnapshot(` @@ -807,6 +812,7 @@ describe('get()', () => { test: 'test1', }, isPreconfigured: true, + isDeprecated: false, name: 'test', config: { foo: 'bar', @@ -821,6 +827,7 @@ describe('get()', () => { id: 'testPreconfigured', actionTypeId: '.slack', isPreconfigured: true, + isDeprecated: false, name: 'test', }); expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); @@ -876,6 +883,7 @@ describe('getAll()', () => { actionTypeId: '.slack', secrets: {}, isPreconfigured: true, + isDeprecated: false, name: 'test', config: { foo: 'bar', @@ -1015,6 +1023,7 @@ describe('getAll()', () => { actionTypeId: '.slack', secrets: {}, isPreconfigured: true, + isDeprecated: false, name: 'test', config: { foo: 'bar', @@ -1028,6 +1037,7 @@ describe('getAll()', () => { { id: '1', isPreconfigured: false, + isDeprecated: false, name: 'test', config: { foo: 'bar', @@ -1039,6 +1049,7 @@ describe('getAll()', () => { id: 'testPreconfigured', actionTypeId: '.slack', isPreconfigured: true, + isDeprecated: false, name: 'test', referencedByCount: 2, }, @@ -1092,6 +1103,7 @@ describe('getBulk()', () => { actionTypeId: '.slack', secrets: {}, isPreconfigured: true, + isDeprecated: false, name: 'test', config: { foo: 'bar', @@ -1225,6 +1237,7 @@ describe('getBulk()', () => { actionTypeId: '.slack', secrets: {}, isPreconfigured: true, + isDeprecated: false, name: 'test', config: { foo: 'bar', @@ -1242,6 +1255,7 @@ describe('getBulk()', () => { }, id: 'testPreconfigured', isPreconfigured: true, + isDeprecated: false, name: 'test', secrets: {}, }, @@ -1253,6 +1267,7 @@ describe('getBulk()', () => { id: '1', isMissingSecrets: false, isPreconfigured: false, + isDeprecated: false, name: 'test', }, ]); @@ -1458,6 +1473,7 @@ describe('update()', () => { expect(result).toEqual({ id: 'my-action', isPreconfigured: false, + isDeprecated: false, actionTypeId: 'my-action-type', isMissingSecrets: false, name: 'my name', @@ -1529,6 +1545,7 @@ describe('update()', () => { expect(result).toEqual({ id: 'my-action', isPreconfigured: false, + isDeprecated: false, actionTypeId: 'my-action-type', isMissingSecrets: true, name: 'my name', @@ -1668,6 +1685,7 @@ describe('update()', () => { expect(result).toEqual({ id: 'my-action', isPreconfigured: false, + isDeprecated: false, actionTypeId: 'my-action-type', isMissingSecrets: true, name: 'my name', @@ -2000,6 +2018,7 @@ describe('isPreconfigured()', () => { test: 'test1', }, isPreconfigured: true, + isDeprecated: false, name: 'test', config: { foo: 'bar', @@ -2035,6 +2054,7 @@ describe('isPreconfigured()', () => { test: 'test1', }, isPreconfigured: true, + isDeprecated: false, name: 'test', config: { foo: 'bar', diff --git a/x-pack/plugins/actions/server/actions_client.ts b/x-pack/plugins/actions/server/actions_client.ts index cafbc6e6bd999..dacf6de36bd37 100644 --- a/x-pack/plugins/actions/server/actions_client.ts +++ b/x-pack/plugins/actions/server/actions_client.ts @@ -45,6 +45,7 @@ import { } from './authorization/get_authorization_mode_by_source'; import { connectorAuditEvent, ConnectorAuditAction } from './lib/audit_events'; import { trackLegacyRBACExemption } from './lib/track_legacy_rbac_exemption'; +import { isConnectorDeprecated } from './lib/is_conector_deprecated'; // We are assuming there won't be many actions. This is why we will load // all the actions in advance and assume the total count to not go over 10000. @@ -187,6 +188,7 @@ export class ActionsClient { name: result.attributes.name, config: result.attributes.config, isPreconfigured: false, + isDeprecated: isConnectorDeprecated(result.attributes), }; } @@ -270,6 +272,7 @@ export class ActionsClient { name: result.attributes.name as string, config: result.attributes.config as Record, isPreconfigured: false, + isDeprecated: isConnectorDeprecated(result.attributes), }; } @@ -306,6 +309,7 @@ export class ActionsClient { actionTypeId: preconfiguredActionsList.actionTypeId, name: preconfiguredActionsList.name, isPreconfigured: true, + isDeprecated: isConnectorDeprecated(preconfiguredActionsList), }; } @@ -325,6 +329,7 @@ export class ActionsClient { name: result.attributes.name, config: result.attributes.config, isPreconfigured: false, + isDeprecated: isConnectorDeprecated(result.attributes), }; } @@ -349,7 +354,9 @@ export class ActionsClient { perPage: MAX_ACTIONS_RETURNED, type: 'action', }) - ).saved_objects.map(actionFromSavedObject); + ).saved_objects.map((rawAction) => + actionFromSavedObject(rawAction, isConnectorDeprecated(rawAction.attributes)) + ); savedObjectsActions.forEach(({ id }) => this.auditLogger?.log( @@ -367,6 +374,7 @@ export class ActionsClient { actionTypeId: preconfiguredAction.actionTypeId, name: preconfiguredAction.name, isPreconfigured: true, + isDeprecated: isConnectorDeprecated(preconfiguredAction), })), ].sort((a, b) => a.name.localeCompare(b.name)); return await injectExtraFindData( @@ -435,7 +443,7 @@ export class ActionsClient { `Failed to load action ${action.id} (${action.error.statusCode}): ${action.error.message}` ); } - actionResults.push(actionFromSavedObject(action)); + actionResults.push(actionFromSavedObject(action, isConnectorDeprecated(action.attributes))); } return actionResults; } @@ -559,11 +567,15 @@ export class ActionsClient { } } -function actionFromSavedObject(savedObject: SavedObject): ActionResult { +function actionFromSavedObject( + savedObject: SavedObject, + isDeprecated: boolean +): ActionResult { return { id: savedObject.id, ...savedObject.attributes, isPreconfigured: false, + isDeprecated, }; } diff --git a/x-pack/plugins/actions/server/create_execute_function.test.ts b/x-pack/plugins/actions/server/create_execute_function.test.ts index a503b1adeb40c..f81dcf1ba1404 100644 --- a/x-pack/plugins/actions/server/create_execute_function.test.ts +++ b/x-pack/plugins/actions/server/create_execute_function.test.ts @@ -247,6 +247,7 @@ describe('execute()', () => { actionTypeId: 'mock-action-preconfigured', config: {}, isPreconfigured: true, + isDeprecated: false, name: 'x', secrets: {}, }, @@ -324,6 +325,7 @@ describe('execute()', () => { actionTypeId: 'mock-action-preconfigured', config: {}, isPreconfigured: true, + isDeprecated: false, name: 'x', secrets: {}, }, @@ -506,6 +508,7 @@ describe('execute()', () => { name: 'Slack #xyz', secrets: {}, isPreconfigured: true, + isDeprecated: false, }, ], }); diff --git a/x-pack/plugins/actions/server/lib/action_executor.test.ts b/x-pack/plugins/actions/server/lib/action_executor.test.ts index 881877d74bb9d..093236c939aa1 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.test.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.test.ts @@ -80,6 +80,7 @@ test('successfully executes', async () => { name: actionSavedObject.id, ...pick(actionSavedObject.attributes, 'actionTypeId', 'config'), isPreconfigured: false, + isDeprecated: false, }; actionsClient.get.mockResolvedValueOnce(actionResult); encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(actionSavedObject); @@ -203,6 +204,7 @@ test('successfully executes as a task', async () => { name: actionSavedObject.id, ...pick(actionSavedObject.attributes, 'actionTypeId', 'config'), isPreconfigured: false, + isDeprecated: false, }; actionsClient.get.mockResolvedValueOnce(actionResult); encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(actionSavedObject); @@ -246,6 +248,7 @@ test('provides empty config when config and / or secrets is empty', async () => name: actionSavedObject.id, actionTypeId: actionSavedObject.attributes.actionTypeId, isPreconfigured: false, + isDeprecated: false, }; actionsClient.get.mockResolvedValueOnce(actionResult); encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(actionSavedObject); @@ -282,6 +285,7 @@ test('throws an error when config is invalid', async () => { name: actionSavedObject.id, actionTypeId: actionSavedObject.attributes.actionTypeId, isPreconfigured: false, + isDeprecated: false, }; actionsClient.get.mockResolvedValueOnce(actionResult); encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(actionSavedObject); @@ -321,6 +325,7 @@ test('throws an error when connector is invalid', async () => { name: actionSavedObject.id, actionTypeId: actionSavedObject.attributes.actionTypeId, isPreconfigured: false, + isDeprecated: false, }; actionsClient.get.mockResolvedValueOnce(actionResult); encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(actionSavedObject); @@ -360,6 +365,7 @@ test('throws an error when params is invalid', async () => { name: actionSavedObject.id, actionTypeId: actionSavedObject.attributes.actionTypeId, isPreconfigured: false, + isDeprecated: false, }; actionsClient.get.mockResolvedValueOnce(actionResult); encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(actionSavedObject); @@ -401,6 +407,7 @@ test('throws an error if actionType is not enabled', async () => { name: actionSavedObject.id, actionTypeId: actionSavedObject.attributes.actionTypeId, isPreconfigured: false, + isDeprecated: false, }; actionsClient.get.mockResolvedValueOnce(actionResult); encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(actionSavedObject); @@ -441,6 +448,7 @@ test('should not throws an error if actionType is preconfigured', async () => { name: actionSavedObject.id, ...pick(actionSavedObject.attributes, 'actionTypeId', 'config', 'secrets'), isPreconfigured: false, + isDeprecated: false, }; actionsClient.get.mockResolvedValueOnce(actionResult); encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(actionSavedObject); @@ -753,6 +761,7 @@ function setupActionExecutorMock() { name: actionSavedObject.name, ...pick(actionSavedObject.attributes, 'actionTypeId', 'config'), isPreconfigured: false, + isDeprecated: false, }; actionsClient.get.mockResolvedValueOnce(actionResult); encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(actionSavedObject); diff --git a/x-pack/plugins/actions/server/lib/is_conector_deprecated.test.ts b/x-pack/plugins/actions/server/lib/is_conector_deprecated.test.ts new file mode 100644 index 0000000000000..f5ace7e055254 --- /dev/null +++ b/x-pack/plugins/actions/server/lib/is_conector_deprecated.test.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. + */ + +import { isConnectorDeprecated } from './is_conector_deprecated'; + +describe('isConnectorDeprecated', () => { + const connector = { + id: 'test', + actionTypeId: '.webhook', + name: 'Test', + config: { apiUrl: 'http://example.com', usesTableApi: false }, + secrets: { username: 'test', password: 'test' }, + isPreconfigured: false as const, + }; + + it('returns false if the connector is not ITSM or SecOps', () => { + expect(isConnectorDeprecated(connector)).toBe(false); + }); + + it('returns false if the connector is .servicenow and the usesTableApi=false', () => { + expect(isConnectorDeprecated({ ...connector, actionTypeId: '.servicenow' })).toBe(false); + }); + + it('returns false if the connector is .servicenow-sir and the usesTableApi=false', () => { + expect(isConnectorDeprecated({ ...connector, actionTypeId: '.servicenow-sir' })).toBe(false); + }); + + it('returns true if the connector is .servicenow and the usesTableApi=true', () => { + expect( + isConnectorDeprecated({ + ...connector, + actionTypeId: '.servicenow', + config: { ...connector.config, usesTableApi: true }, + }) + ).toBe(true); + }); + + it('returns true if the connector is .servicenow-sir and the usesTableApi=true', () => { + expect( + isConnectorDeprecated({ + ...connector, + actionTypeId: '.servicenow-sir', + config: { ...connector.config, usesTableApi: true }, + }) + ).toBe(true); + }); +}); diff --git a/x-pack/plugins/actions/server/lib/is_conector_deprecated.ts b/x-pack/plugins/actions/server/lib/is_conector_deprecated.ts new file mode 100644 index 0000000000000..210631cb532f6 --- /dev/null +++ b/x-pack/plugins/actions/server/lib/is_conector_deprecated.ts @@ -0,0 +1,29 @@ +/* + * 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 { PreConfiguredAction, RawAction } from '../types'; + +export type ConnectorWithOptionalDeprecation = Omit & + Pick, 'isDeprecated'>; + +export const isConnectorDeprecated = ( + connector: RawAction | ConnectorWithOptionalDeprecation +): boolean => { + if (connector.actionTypeId === '.servicenow' || connector.actionTypeId === '.servicenow-sir') { + /** + * Connectors after the Elastic ServiceNow application use the + * Import Set API (https://developer.servicenow.com/dev.do#!/reference/api/rome/rest/c_ImportSetAPI) + * A ServiceNow connector is considered deprecated if it uses the Table API. + * + * All other connectors do not have the usesTableApi config property + * so the function will always return false for them. + */ + return !!connector.config?.usesTableApi; + } + + return false; +}; diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index ba8757b70b522..3cdf28a8d80f3 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -93,6 +93,10 @@ import { ACTIONS_FEATURE_ID, AlertHistoryEsIndexConnectorId } from '../common'; import { EVENT_LOG_ACTIONS, EVENT_LOG_PROVIDER } from './constants/event_log'; import { ConnectorTokenClient } from './builtin_action_types/lib/connector_token_client'; import { InMemoryMetrics, registerClusterCollector, registerNodeCollector } from './monitoring'; +import { + isConnectorDeprecated, + ConnectorWithOptionalDeprecation, +} from './lib/is_conector_deprecated'; export interface PluginSetupContract { registerType< @@ -221,10 +225,14 @@ export class ActionsPlugin implements Plugin { actionTypeId: EsIndexActionTypeId, id: AlertHistoryEsIndexConnectorId, isPreconfigured: true, + isDeprecated: false, config: { index: AlertHistoryDefaultIndexName, }, diff --git a/x-pack/plugins/actions/server/routes/create.test.ts b/x-pack/plugins/actions/server/routes/create.test.ts index 5089b7f96286f..7ddffd72fe8db 100644 --- a/x-pack/plugins/actions/server/routes/create.test.ts +++ b/x-pack/plugins/actions/server/routes/create.test.ts @@ -39,13 +39,20 @@ describe('createActionRoute', () => { actionTypeId: 'abc', config: { foo: true }, isPreconfigured: false, + isDeprecated: false, isMissingSecrets: false, }; const createApiResult = { - ...omit(createResult, ['actionTypeId', 'isPreconfigured', 'isMissingSecrets']), + ...omit(createResult, [ + 'actionTypeId', + 'isPreconfigured', + 'isDeprecated', + 'isMissingSecrets', + ]), connector_type_id: createResult.actionTypeId, is_preconfigured: createResult.isPreconfigured, + is_deprecated: createResult.isDeprecated, is_missing_secrets: createResult.isMissingSecrets, }; @@ -104,6 +111,7 @@ describe('createActionRoute', () => { isMissingSecrets: false, config: { foo: true }, isPreconfigured: false, + isDeprecated: false, }); const [context, req, res] = mockHandlerArguments( @@ -143,6 +151,7 @@ describe('createActionRoute', () => { config: { foo: true }, isMissingSecrets: false, isPreconfigured: false, + isDeprecated: false, }); const [context, req, res] = mockHandlerArguments( diff --git a/x-pack/plugins/actions/server/routes/create.ts b/x-pack/plugins/actions/server/routes/create.ts index 0dd050c55328e..1f14b030f0676 100644 --- a/x-pack/plugins/actions/server/routes/create.ts +++ b/x-pack/plugins/actions/server/routes/create.ts @@ -29,12 +29,14 @@ const rewriteBodyReq: RewriteRequestCase = ({ const rewriteBodyRes: RewriteResponseCase = ({ actionTypeId, isPreconfigured, + isDeprecated, isMissingSecrets, ...res }) => ({ ...res, connector_type_id: actionTypeId, is_preconfigured: isPreconfigured, + is_deprecated: isDeprecated, is_missing_secrets: isMissingSecrets, }); diff --git a/x-pack/plugins/actions/server/routes/get.test.ts b/x-pack/plugins/actions/server/routes/get.test.ts index bf51ccaa4b0e5..7f9736ad7dfb2 100644 --- a/x-pack/plugins/actions/server/routes/get.test.ts +++ b/x-pack/plugins/actions/server/routes/get.test.ts @@ -38,6 +38,7 @@ describe('getActionRoute', () => { name: 'action name', config: {}, isPreconfigured: false, + isDeprecated: false, isMissingSecrets: false, }; @@ -58,6 +59,7 @@ describe('getActionRoute', () => { "config": Object {}, "connector_type_id": "2", "id": "1", + "is_deprecated": false, "is_missing_secrets": false, "is_preconfigured": false, "name": "action name", @@ -75,6 +77,7 @@ describe('getActionRoute', () => { name: 'action name', config: {}, is_preconfigured: false, + is_deprecated: false, is_missing_secrets: false, }, }); @@ -95,6 +98,7 @@ describe('getActionRoute', () => { name: 'action name', config: {}, isPreconfigured: false, + isDeprecated: false, }); const [context, req, res] = mockHandlerArguments( @@ -129,6 +133,7 @@ describe('getActionRoute', () => { name: 'action name', config: {}, isPreconfigured: false, + isDeprecated: false, }); const [context, req, res] = mockHandlerArguments( diff --git a/x-pack/plugins/actions/server/routes/get.ts b/x-pack/plugins/actions/server/routes/get.ts index 352670bd5b170..207ed694f2985 100644 --- a/x-pack/plugins/actions/server/routes/get.ts +++ b/x-pack/plugins/actions/server/routes/get.ts @@ -20,11 +20,13 @@ const rewriteBodyRes: RewriteResponseCase = ({ actionTypeId, isPreconfigured, isMissingSecrets, + isDeprecated, ...res }) => ({ ...res, connector_type_id: actionTypeId, is_preconfigured: isPreconfigured, + is_deprecated: isDeprecated, is_missing_secrets: isMissingSecrets, }); diff --git a/x-pack/plugins/actions/server/routes/get_all.ts b/x-pack/plugins/actions/server/routes/get_all.ts index 3710b60ce5101..61da031db99d4 100644 --- a/x-pack/plugins/actions/server/routes/get_all.ts +++ b/x-pack/plugins/actions/server/routes/get_all.ts @@ -13,10 +13,18 @@ import { verifyAccessAndContext } from './verify_access_and_context'; const rewriteBodyRes: RewriteResponseCase = (results) => { return results.map( - ({ actionTypeId, isPreconfigured, referencedByCount, isMissingSecrets, ...res }) => ({ + ({ + actionTypeId, + isPreconfigured, + isDeprecated, + referencedByCount, + isMissingSecrets, + ...res + }) => ({ ...res, connector_type_id: actionTypeId, is_preconfigured: isPreconfigured, + is_deprecated: isDeprecated, referenced_by_count: referencedByCount, is_missing_secrets: isMissingSecrets, }) diff --git a/x-pack/plugins/actions/server/routes/legacy/create.test.ts b/x-pack/plugins/actions/server/routes/legacy/create.test.ts index a79720c8f21e4..656c24d118d30 100644 --- a/x-pack/plugins/actions/server/routes/legacy/create.test.ts +++ b/x-pack/plugins/actions/server/routes/legacy/create.test.ts @@ -47,6 +47,7 @@ describe('createActionRoute', () => { actionTypeId: 'abc', config: { foo: true }, isPreconfigured: false, + isDeprecated: false, }; const actionsClient = actionsClientMock.create(); @@ -103,6 +104,7 @@ describe('createActionRoute', () => { actionTypeId: 'abc', config: { foo: true }, isPreconfigured: false, + isDeprecated: false, }); const [context, req, res] = mockHandlerArguments({ actionsClient }, {}); @@ -131,6 +133,7 @@ describe('createActionRoute', () => { actionTypeId: 'abc', config: { foo: true }, isPreconfigured: false, + isDeprecated: false, }); const [context, req, res] = mockHandlerArguments({ actionsClient }, {}); diff --git a/x-pack/plugins/actions/server/routes/legacy/get.test.ts b/x-pack/plugins/actions/server/routes/legacy/get.test.ts index df3c3822ab08c..a79185a4ea882 100644 --- a/x-pack/plugins/actions/server/routes/legacy/get.test.ts +++ b/x-pack/plugins/actions/server/routes/legacy/get.test.ts @@ -46,6 +46,7 @@ describe('getActionRoute', () => { name: 'action name', config: {}, isPreconfigured: false, + isDeprecated: false, }; const actionsClient = actionsClientMock.create(); @@ -65,6 +66,7 @@ describe('getActionRoute', () => { "actionTypeId": "2", "config": Object {}, "id": "1", + "isDeprecated": false, "isPreconfigured": false, "name": "action name", }, @@ -94,6 +96,7 @@ describe('getActionRoute', () => { name: 'action name', config: {}, isPreconfigured: false, + isDeprecated: false, }); const [context, req, res] = mockHandlerArguments( @@ -128,6 +131,7 @@ describe('getActionRoute', () => { name: 'action name', config: {}, isPreconfigured: false, + isDeprecated: false, }); const [context, req, res] = mockHandlerArguments( diff --git a/x-pack/plugins/actions/server/routes/legacy/update.test.ts b/x-pack/plugins/actions/server/routes/legacy/update.test.ts index d9c956d3fd35c..49bb0ff2b86e0 100644 --- a/x-pack/plugins/actions/server/routes/legacy/update.test.ts +++ b/x-pack/plugins/actions/server/routes/legacy/update.test.ts @@ -46,6 +46,7 @@ describe('updateActionRoute', () => { name: 'My name', config: { foo: true }, isPreconfigured: false, + isDeprecated: false, }; const actionsClient = actionsClientMock.create(); @@ -103,6 +104,7 @@ describe('updateActionRoute', () => { name: 'My name', config: { foo: true }, isPreconfigured: false, + isDeprecated: false, }; const actionsClient = actionsClientMock.create(); @@ -146,6 +148,7 @@ describe('updateActionRoute', () => { name: 'My name', config: { foo: true }, isPreconfigured: false, + isDeprecated: false, }; const actionsClient = actionsClientMock.create(); diff --git a/x-pack/plugins/actions/server/routes/update.test.ts b/x-pack/plugins/actions/server/routes/update.test.ts index 2856ea6014fd8..8fd11c59c77ce 100644 --- a/x-pack/plugins/actions/server/routes/update.test.ts +++ b/x-pack/plugins/actions/server/routes/update.test.ts @@ -38,6 +38,7 @@ describe('updateActionRoute', () => { name: 'My name', config: { foo: true }, isPreconfigured: false, + isDeprecated: false, }; const actionsClient = actionsClientMock.create(); @@ -65,6 +66,7 @@ describe('updateActionRoute', () => { name: 'My name', config: { foo: true }, is_preconfigured: false, + is_deprecated: false, }, }); @@ -103,6 +105,7 @@ describe('updateActionRoute', () => { name: 'My name', config: { foo: true }, isPreconfigured: false, + isDeprecated: false, }; const actionsClient = actionsClientMock.create(); @@ -146,6 +149,7 @@ describe('updateActionRoute', () => { name: 'My name', config: { foo: true }, isPreconfigured: false, + isDeprecated: false, }; const actionsClient = actionsClientMock.create(); diff --git a/x-pack/plugins/actions/server/routes/update.ts b/x-pack/plugins/actions/server/routes/update.ts index a8892c814fc66..364a583625b4f 100644 --- a/x-pack/plugins/actions/server/routes/update.ts +++ b/x-pack/plugins/actions/server/routes/update.ts @@ -26,11 +26,13 @@ const rewriteBodyRes: RewriteResponseCase = ({ actionTypeId, isPreconfigured, isMissingSecrets, + isDeprecated, ...res }) => ({ ...res, connector_type_id: actionTypeId, is_preconfigured: isPreconfigured, + is_deprecated: isDeprecated, is_missing_secrets: isMissingSecrets, }); diff --git a/x-pack/plugins/actions/server/saved_objects/action_task_params_migrations.test.ts b/x-pack/plugins/actions/server/saved_objects/action_task_params_migrations.test.ts index 399efe023c3fd..c352b5dfa3a21 100644 --- a/x-pack/plugins/actions/server/saved_objects/action_task_params_migrations.test.ts +++ b/x-pack/plugins/actions/server/saved_objects/action_task_params_migrations.test.ts @@ -26,6 +26,7 @@ const preconfiguredActions = [ name: 'Slack #xyz', secrets: {}, isPreconfigured: true, + isDeprecated: false, }, ]; diff --git a/x-pack/plugins/actions/server/types.ts b/x-pack/plugins/actions/server/types.ts index 9cb4f1aec63e8..491d7ab5be2e4 100644 --- a/x-pack/plugins/actions/server/types.ts +++ b/x-pack/plugins/actions/server/types.ts @@ -74,6 +74,7 @@ export interface ActionResult { isMissingSecrets: false, name: 'email connector', isPreconfigured: false, + isDeprecated: false, }, ]); taskManager.schedule.mockResolvedValue({ @@ -687,6 +688,7 @@ describe('create()', () => { isMissingSecrets: false, name: 'email connector', isPreconfigured: false, + isDeprecated: false, }, { id: '2', @@ -702,6 +704,7 @@ describe('create()', () => { isMissingSecrets: false, name: 'email connector', isPreconfigured: false, + isDeprecated: false, }, ]); unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ @@ -857,6 +860,7 @@ describe('create()', () => { isMissingSecrets: false, name: 'email connector', isPreconfigured: false, + isDeprecated: false, }, { id: '2', @@ -872,6 +876,7 @@ describe('create()', () => { isMissingSecrets: false, name: 'another email connector', isPreconfigured: false, + isDeprecated: false, }, { id: 'preconfigured', @@ -887,6 +892,7 @@ describe('create()', () => { isMissingSecrets: false, name: 'preconfigured email connector', isPreconfigured: true, + isDeprecated: false, }, ]); actionsClient.isPreconfigured.mockReset(); @@ -2523,6 +2529,7 @@ describe('create()', () => { isMissingSecrets: true, name: 'email connector', isPreconfigured: false, + isDeprecated: false, }, ]); await expect(rulesClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( diff --git a/x-pack/plugins/alerting/server/rules_client/tests/lib.ts b/x-pack/plugins/alerting/server/rules_client/tests/lib.ts index 194ca6c8279a7..d7eaf71038988 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/lib.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/lib.ts @@ -57,6 +57,7 @@ export function getBeforeSetup( { id: '1', isPreconfigured: false, + isDeprecated: false, actionTypeId: 'test', name: 'test', config: { @@ -66,6 +67,7 @@ export function getBeforeSetup( { id: '2', isPreconfigured: false, + isDeprecated: false, actionTypeId: 'test2', name: 'test2', config: { @@ -76,6 +78,7 @@ export function getBeforeSetup( id: 'testPreconfigured', actionTypeId: '.slack', isPreconfigured: true, + isDeprecated: false, name: 'test', }, ]); diff --git a/x-pack/plugins/alerting/server/rules_client/tests/update.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/update.test.ts index c2005d8a061b5..573ae98ba49f0 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/update.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/update.test.ts @@ -122,6 +122,7 @@ describe('update()', () => { isMissingSecrets: false, name: 'email connector', isPreconfigured: false, + isDeprecated: false, }, ]); rulesClientParams.getActionsClient.mockResolvedValue(actionsClient); @@ -160,6 +161,7 @@ describe('update()', () => { isMissingSecrets: false, name: 'email connector', isPreconfigured: false, + isDeprecated: false, }, { id: '2', @@ -175,6 +177,7 @@ describe('update()', () => { isMissingSecrets: false, name: 'email connector', isPreconfigured: false, + isDeprecated: false, }, ]); unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ @@ -435,6 +438,7 @@ describe('update()', () => { isMissingSecrets: false, name: 'email connector', isPreconfigured: false, + isDeprecated: false, }, { id: '2', @@ -450,6 +454,7 @@ describe('update()', () => { isMissingSecrets: false, name: 'another email connector', isPreconfigured: false, + isDeprecated: false, }, { id: 'preconfigured', @@ -465,6 +470,7 @@ describe('update()', () => { isMissingSecrets: false, name: 'preconfigured email connector', isPreconfigured: true, + isDeprecated: false, }, ]); actionsClient.isPreconfigured.mockReset(); @@ -1362,6 +1368,7 @@ describe('update()', () => { isMissingSecrets: false, name: 'email connector', isPreconfigured: false, + isDeprecated: false, }, { id: '2', @@ -1377,6 +1384,7 @@ describe('update()', () => { isMissingSecrets: false, name: 'email connector', isPreconfigured: false, + isDeprecated: false, }, ]); unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ @@ -1749,6 +1757,7 @@ describe('update()', () => { isMissingSecrets: false, name: 'email connector', isPreconfigured: false, + isDeprecated: false, }, { id: '2', @@ -1764,6 +1773,7 @@ describe('update()', () => { isMissingSecrets: true, name: 'another connector', isPreconfigured: false, + isDeprecated: false, }, ]); @@ -1826,6 +1836,7 @@ describe('update()', () => { isMissingSecrets: false, name: 'email connector', isPreconfigured: false, + isDeprecated: false, }, { id: '2', @@ -1841,6 +1852,7 @@ describe('update()', () => { isMissingSecrets: false, name: 'email connector', isPreconfigured: false, + isDeprecated: false, }, ]); unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ diff --git a/x-pack/plugins/cases/server/client/configure/client.test.ts b/x-pack/plugins/cases/server/client/configure/client.test.ts index fa3f2b3f987f1..f96f55a823aac 100644 --- a/x-pack/plugins/cases/server/client/configure/client.test.ts +++ b/x-pack/plugins/cases/server/client/configure/client.test.ts @@ -40,6 +40,7 @@ describe('client', () => { actionTypeId: '.jira', name: '1', isPreconfigured: false, + isDeprecated: false, referencedByCount: 1, }, ]); @@ -57,6 +58,7 @@ describe('client', () => { name: '1', config: {}, isPreconfigured: true, + isDeprecated: false, referencedByCount: 1, }, ]); @@ -84,6 +86,7 @@ describe('client', () => { name: '1', config: {}, isPreconfigured: false, + isDeprecated: false, referencedByCount: 1, }, { @@ -92,6 +95,7 @@ describe('client', () => { name: '2', config: {}, isPreconfigured: false, + isDeprecated: false, referencedByCount: 1, }, ]; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts index 1304a06fa7dc1..10607e352000d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -379,6 +379,7 @@ export const createActionResult = (): ActionResult => ({ name: '', config: {}, isPreconfigured: false, + isDeprecated: false, }); export const nonRuleAlert = () => ({ @@ -426,6 +427,7 @@ export const updateActionResult = (): ActionResult => ({ name: '', config: {}, isPreconfigured: false, + isDeprecated: false, }); export const getMockPrivilegesResult = () => ({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.test.ts index 450741c5bf70b..6e2a5d08becbc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.test.ts @@ -999,6 +999,7 @@ describe('utils', () => { actionTypeId: 'default', name: 'name', isPreconfigured: false, + isDeprecated: false, }, ]); const [errors, output] = await getInvalidConnectors(rules, clients.actionsClient); @@ -1042,6 +1043,7 @@ describe('utils', () => { actionTypeId: 'default', name: 'name', isPreconfigured: false, + isDeprecated: false, }, { id: '789', @@ -1049,6 +1051,7 @@ describe('utils', () => { actionTypeId: 'default', name: 'name', isPreconfigured: false, + isDeprecated: false, }, ]); const [errors, output] = await getInvalidConnectors(rules, clients.actionsClient); @@ -1098,6 +1101,7 @@ describe('utils', () => { actionTypeId: 'default', name: 'name', isPreconfigured: false, + isDeprecated: false, }, { id: '789', @@ -1105,6 +1109,7 @@ describe('utils', () => { actionTypeId: 'default', name: 'name', isPreconfigured: false, + isDeprecated: false, }, ]); const [errors, output] = await getInvalidConnectors(rules, clients.actionsClient); @@ -1161,6 +1166,7 @@ describe('utils', () => { actionTypeId: 'default', name: 'name', isPreconfigured: false, + isDeprecated: false, }, { id: '789', @@ -1168,6 +1174,7 @@ describe('utils', () => { actionTypeId: 'default', name: 'name', isPreconfigured: false, + isDeprecated: false, }, ]); const [errors, output] = await getInvalidConnectors(rules, clients.actionsClient); @@ -1226,6 +1233,7 @@ describe('utils', () => { actionTypeId: 'default', name: 'name', isPreconfigured: false, + isDeprecated: false, }, { id: '789', @@ -1233,6 +1241,7 @@ describe('utils', () => { actionTypeId: 'default', name: 'name', isPreconfigured: false, + isDeprecated: false, }, ]); const [errors, output] = await getInvalidConnectors(rules, clients.actionsClient); @@ -1332,6 +1341,7 @@ describe('utils', () => { actionTypeId: 'default', name: 'name', isPreconfigured: false, + isDeprecated: false, }, { id: '789', @@ -1339,6 +1349,7 @@ describe('utils', () => { actionTypeId: 'default', name: 'name', isPreconfigured: false, + isDeprecated: false, }, ]); const [errors, output] = await getInvalidConnectors(rules, clients.actionsClient); diff --git a/x-pack/plugins/synthetics/public/state/api/alerts.ts b/x-pack/plugins/synthetics/public/state/api/alerts.ts index 29cef1b08e5fd..1b97422cc67a5 100644 --- a/x-pack/plugins/synthetics/public/state/api/alerts.ts +++ b/x-pack/plugins/synthetics/public/state/api/alerts.ts @@ -30,12 +30,14 @@ export const fetchConnectors = async (): Promise => { connector_type_id: actionTypeId, referenced_by_count: referencedByCount, is_preconfigured: isPreconfigured, + is_deprecated: isDeprecated, is_missing_secrets: isMissingSecrets, ...res }) => ({ ...res, actionTypeId, referencedByCount, + isDeprecated, isPreconfigured, isMissingSecrets, }) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.test.tsx index f91ef64ca28a3..ab2cb5332d452 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.test.tsx @@ -54,6 +54,7 @@ describe('connector validation', () => { actionTypeId: '.email', name: 'email', isPreconfigured: false, + isDeprecated: false, config: { from: 'test@test.com', port: 2323, @@ -95,6 +96,7 @@ describe('connector validation', () => { id: 'test', actionTypeId: '.email', isPreconfigured: false, + isDeprecated: false, name: 'email', config: { from: 'test@test.com', @@ -172,6 +174,7 @@ describe('connector validation', () => { id: 'test', actionTypeId: '.email', isPreconfigured: false, + isDeprecated: false, name: 'email', config: { from: 'test@test.com', @@ -213,6 +216,7 @@ describe('connector validation', () => { id: 'test', actionTypeId: '.email', isPreconfigured: false, + isDeprecated: false, name: 'email', config: { from: 'test@test.com', @@ -253,6 +257,7 @@ describe('connector validation', () => { id: 'test', actionTypeId: '.email', isPreconfigured: false, + isDeprecated: false, name: 'email', config: { from: 'test@test.com', @@ -296,6 +301,7 @@ describe('connector validation', () => { actionTypeId: '.email', name: 'email', isPreconfigured: false, + isDeprecated: false, config: { from: 'test@test.com', hasAuth: true, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.test.tsx index 6125255b3a52a..910627b0c851b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.test.tsx @@ -33,6 +33,7 @@ const actionConnector = { }, id: 'es index connector', isPreconfigured: false, + isDeprecated: false, name: 'test name', secrets: {}, }; @@ -44,6 +45,7 @@ const preconfiguredActionConnector = { }, id: AlertHistoryEsIndexConnectorId, isPreconfigured: true, + isDeprecated: false, name: 'Alert history Elasticsearch index', secrets: {}, }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx index 425bcf651f104..4becccec3483d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx @@ -39,6 +39,7 @@ describe('jira connector validation', () => { actionTypeId: '.jira', name: 'jira', isPreconfigured: false, + isDeprecated: false, config: { apiUrl: 'https://siem-kibana.atlassian.net', projectKey: 'CK', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.test.tsx index 1c8c58c7c4a16..22d154373ea66 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.test.tsx @@ -21,6 +21,7 @@ describe('JiraActionConnectorFields renders', () => { id: 'test', actionTypeId: '.jira', isPreconfigured: false, + isDeprecated: false, name: 'jira', config: { apiUrl: 'https://test/', @@ -62,6 +63,7 @@ describe('JiraActionConnectorFields renders', () => { id: 'test', actionTypeId: '.jira', isPreconfigured: false, + isDeprecated: false, name: 'jira', config: { apiUrl: 'https://test/', @@ -98,6 +100,7 @@ describe('JiraActionConnectorFields renders', () => { const actionConnector = { actionTypeId: '.jira', isPreconfigured: false, + isDeprecated: false, secrets: {}, config: {}, } as JiraActionConnector; @@ -120,6 +123,7 @@ describe('JiraActionConnectorFields renders', () => { const actionConnector = { actionTypeId: '.jira', isPreconfigured: false, + isDeprecated: false, isMissingSecrets: true, secrets: {}, config: {}, @@ -147,6 +151,7 @@ describe('JiraActionConnectorFields renders', () => { id: 'test', actionTypeId: '.jira', isPreconfigured: false, + isDeprecated: false, name: 'jira', config: { apiUrl: 'https://test/', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.test.tsx index 6c39236b3ff98..16581008c9e6a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.test.tsx @@ -44,6 +44,7 @@ const connector: ActionConnector = { actionTypeId: '.test', name: 'Test', isPreconfigured: false, + isDeprecated: false, }; const editAction = jest.fn(); const defaultProps = { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.test.tsx index 89b4b6ee668db..67a51964dcb03 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.test.tsx @@ -38,6 +38,7 @@ describe('resilient connector validation', () => { id: 'test', actionTypeId: '.resilient', isPreconfigured: false, + isDeprecated: false, name: 'resilient', config: { apiUrl: 'https://test/', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_connectors.test.tsx index 13459888ac365..7db5cc113fcb9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_connectors.test.tsx @@ -21,6 +21,7 @@ describe('ResilientActionConnectorFields renders', () => { id: 'test', actionTypeId: '.resilient', isPreconfigured: false, + isDeprecated: false, name: 'resilient', config: { apiUrl: 'https://test/', @@ -62,6 +63,7 @@ describe('ResilientActionConnectorFields renders', () => { id: 'test', actionTypeId: '.resilient', isPreconfigured: false, + isDeprecated: false, name: 'resilient', config: { apiUrl: 'https://test/', @@ -99,6 +101,7 @@ describe('ResilientActionConnectorFields renders', () => { const actionConnector = { actionTypeId: '.resilient', isPreconfigured: false, + isDeprecated: false, config: {}, secrets: {}, } as ResilientActionConnector; @@ -121,6 +124,7 @@ describe('ResilientActionConnectorFields renders', () => { const actionConnector = { actionTypeId: '.resilient', isPreconfigured: false, + isDeprecated: false, config: {}, secrets: {}, isMissingSecrets: true, @@ -148,6 +152,7 @@ describe('ResilientActionConnectorFields renders', () => { id: 'test', actionTypeId: '.resilient', isPreconfigured: false, + isDeprecated: false, name: 'resilient', config: { apiUrl: 'https://test/', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_params.test.tsx index 247e968d08fc6..09ae6fe002d41 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_params.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_params.test.tsx @@ -39,6 +39,7 @@ const connector = { actionTypeId: '.test', name: 'Test', isPreconfigured: false, + isDeprecated: false, }; const editAction = jest.fn(); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log.test.tsx index e91f76d94d2fb..69c1c18bd06ea 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log.test.tsx @@ -37,6 +37,7 @@ describe('server-log connector validation', () => { name: 'server-log', config: {}, isPreconfigured: false, + isDeprecated: false, }; expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/helpers.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/helpers.test.ts index 912b308d8d79c..d76cb5ddb5725 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/helpers.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/helpers.test.ts @@ -22,9 +22,14 @@ const deprecatedConnector: ActionConnector = { actionTypeId: '.servicenow', name: 'Test', isPreconfigured: false, + isDeprecated: true, }; -const validConnector = { ...deprecatedConnector, config: { usesTableApi: false } }; +const validConnector = { + ...deprecatedConnector, + config: { usesTableApi: false }, + isDeprecated: false, +}; describe('helpers', () => { describe('isRESTApiError', () => { @@ -69,21 +74,21 @@ describe('helpers', () => { }); describe('getConnectorDescriptiveTitle', () => { - it('adds deprecated to the connector name when the connector usesTableApi', () => { + it('adds deprecated to the connector name when the connector is deprectaed', () => { expect(getConnectorDescriptiveTitle(deprecatedConnector)).toEqual('Test (deprecated)'); }); - it('does not add deprecated when the connector has usesTableApi:false', () => { + it('does not add deprecated when the connector is not deprectaed', () => { expect(getConnectorDescriptiveTitle(validConnector)).toEqual('Test'); }); }); describe('getSelectedConnectorIcon', () => { - it('returns undefined when the connector has usesTableApi:false', () => { + it('returns undefined when the connector is not deprectaed', () => { expect(getSelectedConnectorIcon(validConnector)).toBeUndefined(); }); - it('returns a component when the connector has usesTableApi:true', () => { + it('returns a component when the connector is deprectaed', () => { expect(getSelectedConnectorIcon(deprecatedConnector)).toBeDefined(); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/helpers.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/helpers.ts index 610759a569fd3..49ae38c12947f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/helpers.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/helpers.ts @@ -9,10 +9,7 @@ import { lazy, ComponentType } from 'react'; import { EuiSelectOption } from '@elastic/eui'; import { AppInfo, Choice, RESTApiError } from './types'; import { ActionConnector, IErrorObject } from '../../../../types'; -import { - deprecatedMessage, - checkConnectorIsDeprecated, -} from '../../../../common/connectors_selection'; +import { deprecatedMessage } from '../../../../common/connectors_selection'; export const DEFAULT_CORRELATION_ID = '{{rule.id}}:{{alert.id}}'; @@ -30,7 +27,7 @@ export const isFieldInvalid = ( export const getConnectorDescriptiveTitle = (connector: ActionConnector) => { let title = connector.name; - if (checkConnectorIsDeprecated(connector)) { + if (connector.isDeprecated) { title += ` ${deprecatedMessage}`; } @@ -40,7 +37,7 @@ export const getConnectorDescriptiveTitle = (connector: ActionConnector) => { export const getSelectedConnectorIcon = ( actionConnector: ActionConnector ): React.LazyExoticComponent> | undefined => { - if (checkConnectorIsDeprecated(actionConnector)) { + if (actionConnector.isDeprecated) { return lazy(() => import('./servicenow_selection_row')); } }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.test.tsx index 0e6eb4d8ff0f2..a52c48d4e8e27 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.test.tsx @@ -50,6 +50,7 @@ describe('servicenow connector validation', () => { actionTypeId: id, name: 'ServiceNow', isPreconfigured: false, + isDeprecated: false, config: { apiUrl: 'https://dev94428.service-now.com/', usesTableApi: false, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.test.tsx index 786c84942e08f..315ac6db4ad92 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.test.tsx @@ -33,6 +33,7 @@ describe('ServiceNowActionConnectorFields renders', () => { id: 'test', actionTypeId: '.servicenow', isPreconfigured: false, + isDeprecated: true, name: 'SN', config: { apiUrl: 'https://test/', @@ -42,6 +43,7 @@ describe('ServiceNowActionConnectorFields renders', () => { const usesImportSetApiConnector = { ...usesTableApiConnector, + isDeprecated: false, config: { ...usesTableApiConnector.config, usesTableApi: false, @@ -94,6 +96,7 @@ describe('ServiceNowActionConnectorFields renders', () => { const actionConnector = { actionTypeId: '.servicenow', isPreconfigured: false, + isDeprecated: false, config: {}, secrets: {}, } as ServiceNowActionConnector; @@ -117,6 +120,7 @@ describe('ServiceNowActionConnectorFields renders', () => { const actionConnector = { actionTypeId: '.servicenow', isPreconfigured: false, + isDeprecated: false, isMissingSecrets: true, config: {}, secrets: {}, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx index a12d43c35369e..a9ff9497bdf19 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx @@ -23,7 +23,6 @@ import { InstallationCallout } from './installation_callout'; import { UpdateConnector } from './update_connector'; import { updateActionConnector } from '../../../lib/action_connector_api'; import { Credentials } from './credentials'; -import { checkConnectorIsDeprecated } from '../../../../common/connectors_selection'; // eslint-disable-next-line import/no-default-export export { ServiceNowConnectorFields as default }; @@ -46,7 +45,7 @@ const ServiceNowConnectorFields: React.FC< } = useKibana().services; const { apiUrl, usesTableApi } = action.config; const { username, password } = action.secrets; - const requiresNewApplication = !checkConnectorIsDeprecated(action); + const requiresNewApplication = !action.isDeprecated; const [showUpdateConnector, setShowUpdateConnector] = useState(false); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_itom_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_itom_params.test.tsx index ef934d4ebacd7..d17c77da1f820 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_itom_params.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_itom_params.test.tsx @@ -41,6 +41,7 @@ const connector: ActionConnector = { actionTypeId: '.test', name: 'Test', isPreconfigured: false, + isDeprecated: false, }; const editAction = jest.fn(); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_itsm_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_itsm_params.test.tsx index cdbfad469b68d..f8375a5aaeb6e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_itsm_params.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_itsm_params.test.tsx @@ -46,6 +46,7 @@ const connector: ActionConnector = { actionTypeId: '.test', name: 'Test', isPreconfigured: false, + isDeprecated: false, }; const editAction = jest.fn(); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_itsm_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_itsm_params.tsx index 6c3c9528e5b19..3bae1c3b858d6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_itsm_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_itsm_params.tsx @@ -26,7 +26,6 @@ import { useGetChoices } from './use_get_choices'; import { choicesToEuiOptions, DEFAULT_CORRELATION_ID } from './helpers'; import * as i18n from './translations'; -import { checkConnectorIsDeprecated } from '../../../../common/connectors_selection'; const useGetChoicesFields = ['urgency', 'severity', 'impact', 'category', 'subcategory']; const defaultFields: Fields = { @@ -47,7 +46,7 @@ const ServiceNowParamsFields: React.FunctionComponent< notifications: { toasts }, } = useKibana().services; - const isDeprecatedActionConnector = checkConnectorIsDeprecated(actionConnector); + const isDeprecatedActionConnector = actionConnector?.isDeprecated; const actionConnectorRef = useRef(actionConnector?.id ?? ''); const { incident, comments } = useMemo( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_sir_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_sir_params.test.tsx index 0a426aeade4c0..9f15cb07f92e1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_sir_params.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_sir_params.test.tsx @@ -48,6 +48,7 @@ const connector: ActionConnector = { actionTypeId: '.test', name: 'Test', isPreconfigured: false, + isDeprecated: false, }; const editAction = jest.fn(); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_sir_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_sir_params.tsx index daeaea1c86137..a341dca3f255a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_sir_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_sir_params.tsx @@ -27,7 +27,6 @@ import { useGetChoices } from './use_get_choices'; import { ServiceNowSIRActionParams, Fields, Choice } from './types'; import { choicesToEuiOptions, DEFAULT_CORRELATION_ID } from './helpers'; import { DeprecatedCallout } from './deprecated_callout'; -import { checkConnectorIsDeprecated } from '../../../../common/connectors_selection'; const useGetChoicesFields = ['category', 'subcategory', 'priority']; const defaultFields: Fields = { @@ -45,7 +44,7 @@ const ServiceNowSIRParamsFields: React.FunctionComponent< notifications: { toasts }, } = useKibana().services; - const isDeprecatedActionConnector = checkConnectorIsDeprecated(actionConnector); + const isDeprecatedActionConnector = actionConnector?.isDeprecated; const actionConnectorRef = useRef(actionConnector?.id ?? ''); const { incident, comments } = useMemo( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/update_connector.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/update_connector.test.tsx index 00cce14906e82..0c7bc3b938ef8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/update_connector.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/update_connector.test.tsx @@ -19,6 +19,7 @@ const actionConnector: ServiceNowActionConnector = { id: 'test', actionTypeId: '.servicenow', isPreconfigured: false, + isDeprecated: false, name: 'servicenow', config: { apiUrl: 'https://test/', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/use_choices.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/use_choices.test.tsx index 175a80c63d4b7..22839c03d3bb7 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/use_choices.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/use_choices.test.tsx @@ -27,6 +27,7 @@ const actionConnector = { actionTypeId: '.servicenow', name: 'ServiceNow ITSM', isPreconfigured: false, + isDeprecated: false, config: { apiUrl: 'https://dev94428.service-now.com/', }, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/use_get_app_info.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/use_get_app_info.test.tsx index f842f6863676a..1e496e66c373b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/use_get_app_info.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/use_get_app_info.test.tsx @@ -32,6 +32,7 @@ const actionConnector = { actionTypeId: '.servicenow', name: 'ServiceNow ITSM', isPreconfigured: false, + isDeprecated: false, config: { apiUrl: 'https://test.service-now.com/', usesTableApi: false, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/use_get_choices.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/use_get_choices.test.tsx index ecbc9512a4d3a..06956e6402300 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/use_get_choices.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/use_get_choices.test.tsx @@ -28,6 +28,7 @@ const actionConnector = { actionTypeId: '.servicenow', name: 'ServiceNow ITSM', isPreconfigured: false, + isDeprecated: false, config: { apiUrl: 'https://dev94428.service-now.com/', }, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.test.tsx index 03f20546b1003..302e5c80af15c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.test.tsx @@ -36,6 +36,7 @@ describe('SwimlaneParamsFields renders', () => { actionTypeId: '.test', name: 'Test', isPreconfigured: false, + isDeprecated: false, }; const defaultProps = { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/use_get_application.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/use_get_application.test.tsx index 4744c4d22fdc9..fc0ae22efe377 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/use_get_application.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/use_get_application.test.tsx @@ -24,6 +24,7 @@ const action = { actionTypeId: '.swimlane', name: 'Swimlane', isPreconfigured: false, + isDeprecated: false, config: { apiUrl: 'https://test.swimlane.com/', appId: 'bcq16kdTbz5jlwM6h', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook.test.tsx index 8a17b1c0b206a..7551f647a5b4a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook.test.tsx @@ -40,6 +40,7 @@ describe('webhook connector validation', () => { actionTypeId: '.webhook', name: 'webhook', isPreconfigured: false, + isDeprecated: false, config: { method: 'PUT', url: 'http://test.com', @@ -74,6 +75,7 @@ describe('webhook connector validation', () => { actionTypeId: '.webhook', name: 'webhook', isPreconfigured: false, + isDeprecated: false, config: { method: 'PUT', url: 'http://test.com', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.test.tsx index 2579d8dd1a93b..533d6d9a9b605 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.test.tsx @@ -20,6 +20,7 @@ describe('WebhookActionConnectorFields renders', () => { id: 'test', actionTypeId: '.webhook', isPreconfigured: false, + isDeprecated: false, name: 'webhook', config: { method: 'PUT', @@ -53,6 +54,7 @@ describe('WebhookActionConnectorFields renders', () => { secrets: {}, actionTypeId: '.webhook', isPreconfigured: false, + isDeprecated: false, config: { hasAuth: true, }, @@ -81,6 +83,7 @@ describe('WebhookActionConnectorFields renders', () => { id: 'test', actionTypeId: '.webhook', isPreconfigured: false, + isDeprecated: false, name: 'webhook', config: { method: 'PUT', @@ -113,6 +116,7 @@ describe('WebhookActionConnectorFields renders', () => { id: 'test', actionTypeId: '.webhook', isPreconfigured: false, + isDeprecated: false, isMissingSecrets: true, name: 'webhook', config: { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/xmatters/xmatters.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/xmatters/xmatters.test.tsx index 93d8c6f5d39db..957302d18a6fc 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/xmatters/xmatters.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/xmatters/xmatters.test.tsx @@ -40,6 +40,7 @@ describe('xmatters connector validation', () => { actionTypeId: '.xmatters', name: 'xmatters', isPreconfigured: false, + isDeprecated: false, config: { configUrl: 'http://test.com', usesBasic: true, @@ -73,6 +74,7 @@ describe('xmatters connector validation', () => { actionTypeId: '.xmatters', name: 'xmatters', isPreconfigured: false, + isDeprecated: false, config: { usesBasic: false, }, @@ -105,6 +107,8 @@ describe('xmatters connector validation', () => { config: { usesBasic: true, }, + isPreconfigured: false, + isDeprecated: false, } as XmattersActionConnector; expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ @@ -136,6 +140,8 @@ describe('xmatters connector validation', () => { configUrl: 'invalid.url', usesBasic: true, }, + isPreconfigured: false, + isDeprecated: false, } as XmattersActionConnector; expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/xmatters/xmatters_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/xmatters/xmatters_connectors.test.tsx index a96e2bf679240..7da6cfe0a4a37 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/xmatters/xmatters_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/xmatters/xmatters_connectors.test.tsx @@ -20,6 +20,7 @@ describe('XmattersActionConnectorFields renders', () => { id: 'test', actionTypeId: '.xmatters', isPreconfigured: false, + isDeprecated: false, name: 'xmatters', config: { configUrl: 'http:\\test', @@ -51,6 +52,7 @@ describe('XmattersActionConnectorFields renders', () => { id: 'test', actionTypeId: '.xmatters', isPreconfigured: false, + isDeprecated: false, name: 'xmatters', config: { configUrl: 'http:\\test', @@ -81,6 +83,7 @@ describe('XmattersActionConnectorFields renders', () => { id: 'test', actionTypeId: '.xmatters', isPreconfigured: false, + isDeprecated: false, name: 'xmatters', config: { usesBasic: false, @@ -107,6 +110,7 @@ describe('XmattersActionConnectorFields renders', () => { secrets: {}, actionTypeId: '.xmatters', isPreconfigured: false, + isDeprecated: false, config: { usesBasic: true, }, @@ -135,6 +139,7 @@ describe('XmattersActionConnectorFields renders', () => { id: 'test', actionTypeId: '.xmatters', isPreconfigured: false, + isDeprecated: false, name: 'xmatters', config: { configUrl: 'http:\\test', @@ -165,6 +170,7 @@ describe('XmattersActionConnectorFields renders', () => { id: 'test', actionTypeId: '.xmatters', isPreconfigured: false, + isDeprecated: false, isMissingSecrets: true, name: 'xmatters', config: { @@ -194,6 +200,7 @@ describe('XmattersActionConnectorFields renders', () => { id: 'test', actionTypeId: '.xmatters', isPreconfigured: false, + isDeprecated: false, name: 'xmatters', config: { usesBasic: false, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/connectors.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/connectors.ts index fbef1521b2024..c08232ca5eea5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/connectors.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/connectors.ts @@ -22,12 +22,14 @@ const transformConnector: RewriteRequestCase< > = ({ connector_type_id: actionTypeId, is_preconfigured: isPreconfigured, + is_deprecated: isDeprecated, referenced_by_count: referencedByCount, is_missing_secrets: isMissingSecrets, ...res }) => ({ actionTypeId, isPreconfigured, + isDeprecated, referencedByCount, isMissingSecrets, ...res, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/create.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/create.test.ts index 42588610e036b..a2ed111daa2be 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/create.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/create.test.ts @@ -18,6 +18,7 @@ describe('createActionConnector', () => { const apiResponse = { connector_type_id: 'test', is_preconfigured: false, + is_deprecated: false, name: 'My test', config: {}, secrets: {}, @@ -28,6 +29,7 @@ describe('createActionConnector', () => { const connector: ActionConnectorWithoutId<{}, {}> = { actionTypeId: 'test', isPreconfigured: false, + isDeprecated: false, name: 'My test', config: {}, secrets: {}, @@ -40,7 +42,7 @@ describe('createActionConnector', () => { Array [ "/api/actions/connector", Object { - "body": "{\\"name\\":\\"My test\\",\\"config\\":{},\\"secrets\\":{},\\"connector_type_id\\":\\"test\\",\\"is_preconfigured\\":false}", + "body": "{\\"name\\":\\"My test\\",\\"config\\":{},\\"secrets\\":{},\\"connector_type_id\\":\\"test\\",\\"is_preconfigured\\":false,\\"is_deprecated\\":false}", }, ] `); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/create.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/create.ts index 2799848909446..9227c4747c84a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/create.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/create.ts @@ -15,10 +15,11 @@ import type { const rewriteBodyRequest: RewriteResponseCase< Omit -> = ({ actionTypeId, isPreconfigured, ...res }) => ({ +> = ({ actionTypeId, isPreconfigured, isDeprecated, ...res }) => ({ ...res, connector_type_id: actionTypeId, is_preconfigured: isPreconfigured, + is_deprecated: isDeprecated, }); const rewriteBodyRes: RewriteRequestCase< @@ -26,12 +27,14 @@ const rewriteBodyRes: RewriteRequestCase< > = ({ connector_type_id: actionTypeId, is_preconfigured: isPreconfigured, + is_deprecated: isDeprecated, is_missing_secrets: isMissingSecrets, ...res }) => ({ ...res, actionTypeId, isPreconfigured, + isDeprecated, isMissingSecrets, }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/update.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/update.test.ts index 90c0d3adb9235..957f682d1fd6c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/update.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/update.test.ts @@ -19,6 +19,7 @@ describe('updateActionConnector', () => { const apiResponse = { connector_type_id: 'te/st', is_preconfigured: false, + is_deprecated: false, name: 'My test', config: {}, secrets: {}, @@ -29,6 +30,7 @@ describe('updateActionConnector', () => { const connector: ActionConnectorWithoutId<{}, {}> = { actionTypeId: 'te/st', isPreconfigured: false, + isDeprecated: false, name: 'My test', config: {}, secrets: {}, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/update.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/update.ts index 18f4bdd60ad27..34aa1e127ad95 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/update.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/update.ts @@ -18,12 +18,14 @@ const rewriteBodyRes: RewriteRequestCase< > = ({ connector_type_id: actionTypeId, is_preconfigured: isPreconfigured, + is_deprecated: isDeprecated, is_missing_secrets: isMissingSecrets, ...res }) => ({ ...res, actionTypeId, isPreconfigured, + isDeprecated, isMissingSecrets, }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.test.tsx index 6b115abc590cc..93aade03b22b2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.test.tsx @@ -99,6 +99,7 @@ describe('checkActionFormActionTypeEnabled', () => { actionTypeId: '1', id: 'test1', isPreconfigured: true, + isDeprecated: true, name: 'test', referencedByCount: 0, }, @@ -106,6 +107,7 @@ describe('checkActionFormActionTypeEnabled', () => { actionTypeId: '2', id: 'test2', isPreconfigured: true, + isDeprecated: true, name: 'test', referencedByCount: 0, }, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/value_validators.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/value_validators.test.ts index 162cb222b78fe..6587fc669b97e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/value_validators.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/value_validators.test.ts @@ -112,6 +112,7 @@ describe('getConnectorWithInvalidatedFields', () => { name: 'slack', config: {}, isPreconfigured: false, + isDeprecated: false, }; const secretsErrors = { webhookUrl: ['Webhook URL is required.'] }; const configErrors = {}; @@ -128,6 +129,7 @@ describe('getConnectorWithInvalidatedFields', () => { name: 'jira', config: {} as any, isPreconfigured: false, + isDeprecated: false, }; const secretsErrors = {}; const configErrors = { apiUrl: ['apiUrl is required'] }; @@ -146,6 +148,7 @@ describe('getConnectorWithInvalidatedFields', () => { name: 'slack', config: {}, isPreconfigured: false, + isDeprecated: false, }; const secretsErrors = { webhookUrl: ['Webhook URL must start with https://.'] }; const configErrors = {}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.test.tsx index 4addfc3833aab..b86a3952eb0a6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.test.tsx @@ -41,6 +41,7 @@ describe('action_connector_form', () => { config: {}, secrets: {}, isPreconfigured: false, + isDeprecated: false, }; const wrapper = mountWithIntl( { name: 'Test connector', config: {}, isPreconfigured: false, + isDeprecated: false, }, { secrets: {}, @@ -142,6 +143,7 @@ describe('action_form', () => { name: 'Test connector 2', config: {}, isPreconfigured: true, + isDeprecated: false, }, { secrets: {}, @@ -151,6 +153,7 @@ describe('action_form', () => { name: 'Preconfigured Only', config: {}, isPreconfigured: true, + isDeprecated: false, }, { secrets: {}, @@ -160,6 +163,7 @@ describe('action_form', () => { name: 'Regular connector', config: {}, isPreconfigured: false, + isDeprecated: false, }, { secrets: {}, @@ -169,6 +173,7 @@ describe('action_form', () => { name: 'Non consumer connector', config: {}, isPreconfigured: false, + isDeprecated: false, }, { secrets: {}, @@ -178,6 +183,7 @@ describe('action_form', () => { name: 'Connector with disabled action group', config: {}, isPreconfigured: false, + isDeprecated: false, }, { secrets: null, @@ -187,6 +193,7 @@ describe('action_form', () => { name: 'Connector with disabled action group', config: {}, isPreconfigured: false, + isDeprecated: false, }, ]; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.test.tsx index 484f6698a8b29..7b89d720eabe3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.test.tsx @@ -154,6 +154,7 @@ function getActionTypeForm( }, id: 'test', isPreconfigured: false, + isDeprecated: false, name: 'test name', secrets: {}, }; @@ -176,6 +177,7 @@ function getActionTypeForm( }, id: 'test', isPreconfigured: false, + isDeprecated: false, name: 'test name', secrets: {}, }, @@ -184,6 +186,7 @@ function getActionTypeForm( name: 'Server log', actionTypeId: '.server-log', isPreconfigured: false, + isDeprecated: false, config: {}, secrets: {}, }, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.test.tsx index 524b848dbb275..e01d325ba3ef5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.test.tsx @@ -43,6 +43,7 @@ describe('connector_edit_flyout', () => { actionType: 'test-action-type-name', name: 'action-connector', isPreconfigured: false, + isDeprecated: false, referencedByCount: 0, config: {}, }; @@ -87,6 +88,7 @@ describe('connector_edit_flyout', () => { actionType: 'test-action-type-name', name: 'preconfigured-connector', isPreconfigured: true, + isDeprecated: false, referencedByCount: 0, config: {}, }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_reducer.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_reducer.test.ts index ab9ff5bfa9ffe..bc23b87176598 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_reducer.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_reducer.test.ts @@ -21,6 +21,7 @@ describe('connector reducer', () => { name: 'action-connector', referencedByCount: 0, isPreconfigured: false, + isDeprecated: false, config: {}, }; }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connectors_selection.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connectors_selection.test.tsx index 3bef8e19abff1..b56e245633ece 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connectors_selection.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connectors_selection.test.tsx @@ -66,6 +66,7 @@ describe('connectors_selection', () => { }, id: 'testId', isPreconfigured: false, + isDeprecated: false, name: 'test pagerduty', secrets: {}, }, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx index 4b51c9b306a75..9d6e464f25dd3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx @@ -105,6 +105,7 @@ describe('actions_connectors_list component with items', () => { actionTypeId: 'test', description: 'My test', isPreconfigured: false, + isDeprecated: false, referencedByCount: 1, config: {}, }, @@ -114,6 +115,7 @@ describe('actions_connectors_list component with items', () => { description: 'My test 2', referencedByCount: 1, isPreconfigured: false, + isDeprecated: false, config: {}, }, { @@ -123,6 +125,7 @@ describe('actions_connectors_list component with items', () => { isMissingSecrets: true, referencedByCount: 1, isPreconfigured: true, + isDeprecated: false, config: {}, }, { @@ -131,6 +134,7 @@ describe('actions_connectors_list component with items', () => { description: 'My invalid connector type', referencedByCount: 1, isPreconfigured: false, + isDeprecated: false, config: {}, }, ] @@ -237,6 +241,7 @@ describe('actions_connectors_list component with items', () => { secrets: {}, description: `My test ${index}`, isPreconfigured: false, + isDeprecated: false, referencedByCount: 1, config: {}, })) @@ -472,6 +477,7 @@ describe('actions_connectors_list component with deprecated connectors', () => { description: 'My test', referencedByCount: 1, config: { usesTableApi: true }, + isDeprecated: true, }, { id: '2', @@ -479,6 +485,7 @@ describe('actions_connectors_list component with deprecated connectors', () => { description: 'My test 2', referencedByCount: 1, config: { usesTableApi: true }, + isDeprecated: true, }, ]); loadActionTypes.mockResolvedValueOnce([ diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx index 80d1839b231df..a1867bb6362b8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx @@ -50,7 +50,6 @@ import ConnectorAddFlyout from '../../action_connector_form/connector_add_flyout import { connectorDeprecatedMessage, deprecatedMessage, - checkConnectorIsDeprecated, } from '../../../../common/connectors_selection'; const ConnectorIconTipWithSpacing = withTheme(({ theme }: { theme: EuiTheme }) => { @@ -203,7 +202,7 @@ const ActionsConnectorsList: React.FunctionComponent = () => { * TODO: Remove when connectors can provide their own UX message. * Issue: https://github.com/elastic/kibana/issues/114507 */ - const showDeprecatedTooltip = checkConnectorIsDeprecated(item); + const showDeprecatedTooltip = item.isDeprecated; const name = getConnectorName(value, item); const link = ( @@ -490,7 +489,7 @@ function getActionsCountByActionType(actions: ActionConnector[], actionTypeId: s } function getConnectorName(name: string, connector: ActionConnector): string { - return checkConnectorIsDeprecated(connector) ? `${name} ${deprecatedMessage}` : name; + return connector.isDeprecated ? `${name} ${deprecatedMessage}` : name; } const DeleteOperation: React.FunctionComponent<{ diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.test.tsx index b41af16ccab47..fe17dde8c1282 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.test.tsx @@ -685,6 +685,7 @@ describe('broken connector indicator', () => { name: 'Test connector', config: {}, isPreconfigured: false, + isDeprecated: false, }, { secrets: {}, @@ -694,6 +695,7 @@ describe('broken connector indicator', () => { name: 'Test connector 2', config: {}, isPreconfigured: false, + isDeprecated: false, }, ]); diff --git a/x-pack/plugins/triggers_actions_ui/public/common/connectors_seleciton.test.tsx b/x-pack/plugins/triggers_actions_ui/public/common/connectors_seleciton.test.tsx deleted file mode 100644 index 8e55b71699d65..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/common/connectors_seleciton.test.tsx +++ /dev/null @@ -1,59 +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 { checkConnectorIsDeprecated } from './connectors_selection'; - -describe('Connectors select', () => { - describe('checkConnectorIsDeprecated', () => { - const connector = { - id: 'test', - actionTypeId: '.webhook', - name: 'Test', - config: { apiUrl: 'http://example.com', usesTableApi: false }, - secrets: { username: 'test', password: 'test' }, - isPreconfigured: false as const, - }; - - it('returns false if the connector is not defined', () => { - expect(checkConnectorIsDeprecated()).toBe(false); - }); - - it('returns false if the connector is not ITSM or SecOps', () => { - expect(checkConnectorIsDeprecated(connector)).toBe(false); - }); - - it('returns false if the connector is .servicenow and the usesTableApi=false', () => { - expect(checkConnectorIsDeprecated({ ...connector, actionTypeId: '.servicenow' })).toBe(false); - }); - - it('returns false if the connector is .servicenow-sir and the usesTableApi=false', () => { - expect(checkConnectorIsDeprecated({ ...connector, actionTypeId: '.servicenow-sir' })).toBe( - false - ); - }); - - it('returns true if the connector is .servicenow and the usesTableApi=true', () => { - expect( - checkConnectorIsDeprecated({ - ...connector, - actionTypeId: '.servicenow', - config: { ...connector.config, usesTableApi: true }, - }) - ).toBe(true); - }); - - it('returns true if the connector is .servicenow-sir and the usesTableApi=true', () => { - expect( - checkConnectorIsDeprecated({ - ...connector, - actionTypeId: '.servicenow-sir', - config: { ...connector.config, usesTableApi: true }, - }) - ).toBe(true); - }); - }); -}); diff --git a/x-pack/plugins/triggers_actions_ui/public/common/connectors_selection.tsx b/x-pack/plugins/triggers_actions_ui/public/common/connectors_selection.tsx index 334a10c95e112..555b2d4c6b71d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/connectors_selection.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/common/connectors_selection.tsx @@ -6,8 +6,6 @@ */ import { i18n } from '@kbn/i18n'; -import { ServiceNowActionConnector } from '../application/components/builtin_action_types/servicenow/types'; -import { ActionConnector, UserConfiguredActionConnector } from '../types'; export const preconfiguredMessage = i18n.translate( 'xpack.triggersActionsUI.sections.actionForm.preconfiguredTitleMessage', @@ -27,44 +25,3 @@ export const connectorDeprecatedMessage = i18n.translate( 'xpack.triggersActionsUI.sections.isDeprecatedDescription', { defaultMessage: 'This connector is deprecated. Update it, or create a new one.' } ); - -export const checkConnectorIsDeprecated = ( - connector?: ActionConnector | ServiceNowActionConnector -): boolean => { - if (connector == null) { - return false; - } - - if ( - isConnectorWithConfig(connector) && - (connector.actionTypeId === '.servicenow' || connector.actionTypeId === '.servicenow-sir') - ) { - /** - * Connectors after the Elastic ServiceNow application use the - * Import Set API (https://developer.servicenow.com/dev.do#!/reference/api/rome/rest/c_ImportSetAPI) - * A ServiceNow connector is considered deprecated if it uses the Table API. - * - * All other connectors do not have the usesTableApi config property - * so the function will always return false for them. - */ - return !!connector.config.usesTableApi; - } - - return false; -}; - -type ConnectorWithUnknownConfig = UserConfiguredActionConnector< - Record, - Record ->; - -const isConnectorWithConfig = ( - connector: ActionConnector | ServiceNowActionConnector -): connector is ConnectorWithUnknownConfig => { - const unsafeConnector = connector as UserConfiguredActionConnector< - Record, - Record - >; - - return unsafeConnector.config != null; -}; diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index 2d4268252a353..cb68aab1899be 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -195,6 +195,7 @@ export interface ActionConnectorProps { referencedByCount?: number; config: Config; isPreconfigured: boolean; + isDeprecated: boolean; isMissingSecrets?: boolean; } diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts index 93b1ba7d76a47..02f1f42af5270 100644 --- a/x-pack/test/alerting_api_integration/common/config.ts +++ b/x-pack/test/alerting_api_integration/common/config.ts @@ -187,6 +187,18 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz', }, }, + 'my-deprecated-servicenow': { + actionTypeId: '.servicenow', + name: 'ServiceNow#xyz', + config: { + apiUrl: 'https://ven04334.service-now.com', + usesTableApi: true, + }, + secrets: { + username: 'elastic_integration', + password: 'somepassword', + }, + }, 'custom-system-abc-connector': { actionTypeId: 'system-abc-action-type', name: 'SystemABC', diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/email.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/email.ts index d7716c5f30f66..6fb2315956b69 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/email.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/email.ts @@ -45,6 +45,7 @@ export default function emailTest({ getService }: FtrProviderContext) { expect(createdAction).to.eql({ id: createdActionId, is_preconfigured: false, + is_deprecated: false, name: 'An email action', connector_type_id: '.email', is_missing_secrets: false, @@ -70,6 +71,7 @@ export default function emailTest({ getService }: FtrProviderContext) { expect(fetchedAction).to.eql({ id: fetchedAction.id, is_preconfigured: false, + is_deprecated: false, name: 'An email action', connector_type_id: '.email', is_missing_secrets: false, @@ -354,6 +356,7 @@ export default function emailTest({ getService }: FtrProviderContext) { expect(createdAction).to.eql({ id: createdAction.id, is_preconfigured: false, + is_deprecated: false, name: 'An email action', connector_type_id: '.email', is_missing_secrets: false, @@ -379,6 +382,7 @@ export default function emailTest({ getService }: FtrProviderContext) { expect(fetchedAction).to.eql({ id: fetchedAction.id, is_preconfigured: false, + is_deprecated: false, name: 'An email action', connector_type_id: '.email', is_missing_secrets: false, @@ -418,6 +422,7 @@ export default function emailTest({ getService }: FtrProviderContext) { expect(createdAction).to.eql({ id: createdAction.id, is_preconfigured: false, + is_deprecated: false, name: 'An email action', connector_type_id: '.email', is_missing_secrets: false, @@ -443,6 +448,7 @@ export default function emailTest({ getService }: FtrProviderContext) { expect(fetchedAction).to.eql({ id: fetchedAction.id, is_preconfigured: false, + is_deprecated: false, name: 'An email action', connector_type_id: '.email', is_missing_secrets: false, @@ -487,6 +493,7 @@ export default function emailTest({ getService }: FtrProviderContext) { expect(createdAction).to.eql({ id: createdMSExchangeActionId, is_preconfigured: false, + is_deprecated: false, name: 'An email action', connector_type_id: '.email', is_missing_secrets: false, @@ -514,6 +521,7 @@ export default function emailTest({ getService }: FtrProviderContext) { expect(fetchedAction).to.eql({ id: fetchedAction.id, is_preconfigured: false, + is_deprecated: false, name: 'An email action', connector_type_id: '.email', is_missing_secrets: false, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/es_index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/es_index.ts index fc40d036f925a..edf352936e979 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/es_index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/es_index.ts @@ -41,6 +41,7 @@ export default function indexTest({ getService }: FtrProviderContext) { expect(createdAction).to.eql({ id: createdAction.id, is_preconfigured: false, + is_deprecated: false, name: 'An index action', connector_type_id: '.index', is_missing_secrets: false, @@ -60,6 +61,7 @@ export default function indexTest({ getService }: FtrProviderContext) { expect(fetchedAction).to.eql({ id: fetchedAction.id, is_preconfigured: false, + is_deprecated: false, is_missing_secrets: false, name: 'An index action', connector_type_id: '.index', @@ -84,6 +86,7 @@ export default function indexTest({ getService }: FtrProviderContext) { expect(createdActionWithIndex).to.eql({ id: createdActionWithIndex.id, is_preconfigured: false, + is_deprecated: false, name: 'An index action with index config', connector_type_id: '.index', is_missing_secrets: false, @@ -103,6 +106,7 @@ export default function indexTest({ getService }: FtrProviderContext) { expect(fetchedActionWithIndex).to.eql({ id: fetchedActionWithIndex.id, is_preconfigured: false, + is_deprecated: false, name: 'An index action with index config', connector_type_id: '.index', is_missing_secrets: false, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts index 41a0d65b624b7..33185a20c9249 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts @@ -76,6 +76,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { expect(createdAction).to.eql({ id: createdAction.id, is_preconfigured: false, + is_deprecated: false, name: 'A jira action', connector_type_id: '.jira', is_missing_secrets: false, @@ -92,6 +93,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { expect(fetchedAction).to.eql({ id: fetchedAction.id, is_preconfigured: false, + is_deprecated: false, name: 'A jira action', connector_type_id: '.jira', is_missing_secrets: false, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts index 26b59d93e0073..05dba49236197 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts @@ -62,6 +62,7 @@ export default function pagerdutyTest({ getService }: FtrProviderContext) { expect(createdAction).to.eql({ id: createdAction.id, is_preconfigured: false, + is_deprecated: false, name: 'A pagerduty action', connector_type_id: '.pagerduty', is_missing_secrets: false, @@ -79,6 +80,7 @@ export default function pagerdutyTest({ getService }: FtrProviderContext) { expect(fetchedAction).to.eql({ id: fetchedAction.id, is_preconfigured: false, + is_deprecated: false, name: 'A pagerduty action', connector_type_id: '.pagerduty', is_missing_secrets: false, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts index 8e2036ce688ea..e2a92701b62cb 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts @@ -79,6 +79,7 @@ export default function resilientTest({ getService }: FtrProviderContext) { expect(createdAction).to.eql({ id: createdAction.id, is_preconfigured: false, + is_deprecated: false, name: 'An IBM Resilient action', connector_type_id: '.resilient', is_missing_secrets: false, @@ -95,6 +96,7 @@ export default function resilientTest({ getService }: FtrProviderContext) { expect(fetchedAction).to.eql({ id: fetchedAction.id, is_preconfigured: false, + is_deprecated: false, name: 'An IBM Resilient action', connector_type_id: '.resilient', is_missing_secrets: false, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/server_log.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/server_log.ts index 6dacd19460295..fb7bac7d81e9c 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/server_log.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/server_log.ts @@ -30,6 +30,7 @@ export default function serverLogTest({ getService }: FtrProviderContext) { expect(createdAction).to.eql({ id: createdAction.id, is_preconfigured: false, + is_deprecated: false, is_missing_secrets: false, name: 'A server.log action', connector_type_id: '.server-log', @@ -45,6 +46,7 @@ export default function serverLogTest({ getService }: FtrProviderContext) { expect(fetchedAction).to.eql({ id: fetchedAction.id, is_preconfigured: false, + is_deprecated: false, name: 'A server.log action', connector_type_id: '.server-log', is_missing_secrets: false, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow_itom.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow_itom.ts index 6f1ddc6ee2748..9dcc3ef05266e 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow_itom.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow_itom.ts @@ -93,6 +93,7 @@ export default function serviceNowITOMTest({ getService }: FtrProviderContext) { expect(createdAction).to.eql({ id: createdAction.id, is_preconfigured: false, + is_deprecated: false, name: 'A servicenow action', connector_type_id: '.servicenow-itom', is_missing_secrets: false, @@ -108,6 +109,7 @@ export default function serviceNowITOMTest({ getService }: FtrProviderContext) { expect(fetchedAction).to.eql({ id: fetchedAction.id, is_preconfigured: false, + is_deprecated: false, name: 'A servicenow action', connector_type_id: '.servicenow-itom', is_missing_secrets: false, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow_itsm.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow_itsm.ts index 1308959ebbacf..4cc65d7103a58 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow_itsm.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow_itsm.ts @@ -100,6 +100,7 @@ export default function serviceNowITSMTest({ getService }: FtrProviderContext) { expect(createdAction).to.eql({ id: createdAction.id, is_preconfigured: false, + is_deprecated: false, name: 'A servicenow action', connector_type_id: '.servicenow', is_missing_secrets: false, @@ -116,6 +117,7 @@ export default function serviceNowITSMTest({ getService }: FtrProviderContext) { expect(fetchedAction).to.eql({ id: fetchedAction.id, is_preconfigured: false, + is_deprecated: false, name: 'A servicenow action', connector_type_id: '.servicenow', is_missing_secrets: false, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow_sir.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow_sir.ts index c27634ecf6aca..305bbef7cf70a 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow_sir.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow_sir.ts @@ -104,6 +104,7 @@ export default function serviceNowSIRTest({ getService }: FtrProviderContext) { expect(createdAction).to.eql({ id: createdAction.id, is_preconfigured: false, + is_deprecated: false, name: 'A servicenow action', connector_type_id: '.servicenow-sir', is_missing_secrets: false, @@ -120,6 +121,7 @@ export default function serviceNowSIRTest({ getService }: FtrProviderContext) { expect(fetchedAction).to.eql({ id: fetchedAction.id, is_preconfigured: false, + is_deprecated: false, name: 'A servicenow action', connector_type_id: '.servicenow-sir', is_missing_secrets: false, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts index 8945c54c843b9..66b988fb9b4eb 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts @@ -59,6 +59,7 @@ export default function slackTest({ getService }: FtrProviderContext) { expect(createdAction).to.eql({ id: createdAction.id, is_preconfigured: false, + is_deprecated: false, is_missing_secrets: false, name: 'A slack action', connector_type_id: '.slack', @@ -74,6 +75,7 @@ export default function slackTest({ getService }: FtrProviderContext) { expect(fetchedAction).to.eql({ id: fetchedAction.id, is_preconfigured: false, + is_deprecated: false, is_missing_secrets: false, name: 'A slack action', connector_type_id: '.slack', diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/swimlane.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/swimlane.ts index 9647d083460fd..a55e8e30d419a 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/swimlane.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/swimlane.ts @@ -151,6 +151,7 @@ export default function swimlaneTest({ getService }: FtrProviderContext) { id: createdAction.id, is_missing_secrets: false, is_preconfigured: false, + is_deprecated: false, name: 'A swimlane action', }); @@ -163,6 +164,7 @@ export default function swimlaneTest({ getService }: FtrProviderContext) { expect(fetchedAction).to.eql({ id: fetchedAction.id, is_preconfigured: false, + is_deprecated: false, is_missing_secrets: false, name: 'A swimlane action', connector_type_id: '.swimlane', diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts index 8d879633be2ec..44a74a7a31571 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts @@ -117,6 +117,7 @@ export default function webhookTest({ getService }: FtrProviderContext) { expect(createdAction).to.eql({ id: createdAction.id, is_preconfigured: false, + is_deprecated: false, name: 'A generic Webhook action', connector_type_id: '.webhook', is_missing_secrets: false, @@ -135,6 +136,7 @@ export default function webhookTest({ getService }: FtrProviderContext) { expect(fetchedAction).to.eql({ id: fetchedAction.id, is_preconfigured: false, + is_deprecated: false, name: 'A generic Webhook action', connector_type_id: '.webhook', is_missing_secrets: false, @@ -168,6 +170,7 @@ export default function webhookTest({ getService }: FtrProviderContext) { expect(createdAction).to.eql({ id: createdAction.id, is_preconfigured: false, + is_deprecated: false, name: 'A generic Webhook action', connector_type_id: '.webhook', is_missing_secrets: false, @@ -205,6 +208,7 @@ export default function webhookTest({ getService }: FtrProviderContext) { expect(fetchedAction).to.eql({ id: fetchedAction.id, is_preconfigured: false, + is_deprecated: false, name: 'A generic Webhook action', connector_type_id: '.webhook', is_missing_secrets: false, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/xmatters.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/xmatters.ts index 83edc6f5a2984..7ce357bc62e36 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/xmatters.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/xmatters.ts @@ -62,6 +62,7 @@ export default function xmattersTest({ getService }: FtrProviderContext) { expect(createdAction).to.eql({ id: createdAction.id, is_preconfigured: false, + is_deprecated: false, name: 'An xmatters action', connector_type_id: '.xmatters', is_missing_secrets: false, @@ -95,6 +96,7 @@ export default function xmattersTest({ getService }: FtrProviderContext) { expect(createdAction).to.eql({ id: createdAction.id, is_preconfigured: false, + is_deprecated: false, name: 'An xmatters action', connector_type_id: '.xmatters', is_missing_secrets: false, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/create.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/create.ts index 87804395fc2b7..06e8017177138 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/create.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/create.ts @@ -59,6 +59,7 @@ export default function createActionTests({ getService }: FtrProviderContext) { expect(response.body).to.eql({ id: response.body.id, is_preconfigured: false, + is_deprecated: false, is_missing_secrets: false, name: 'My action', connector_type_id: 'test.index-record', diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get.ts index 5a445a4f5f112..9842b13a9745d 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get.ts @@ -64,6 +64,7 @@ export default function getActionTests({ getService }: FtrProviderContext) { id: createdAction.id, is_preconfigured: false, connector_type_id: 'test.index-record', + is_deprecated: false, is_missing_secrets: false, name: 'My action', config: { @@ -150,6 +151,7 @@ export default function getActionTests({ getService }: FtrProviderContext) { connector_type_id: '.slack', name: 'Slack#xyz', is_preconfigured: true, + is_deprecated: false, }); break; default: diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts index 8ae50b9158487..ab6181369755f 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts @@ -70,6 +70,7 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { { id: createdAction.id, is_preconfigured: false, + is_deprecated: false, name: 'My action', connector_type_id: 'test.index-record', is_missing_secrets: false, @@ -81,13 +82,23 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { { id: 'preconfigured-es-index-action', is_preconfigured: true, + is_deprecated: false, connector_type_id: '.index', name: 'preconfigured_es_index_action', referenced_by_count: 0, }, + { + connector_type_id: '.servicenow', + id: 'my-deprecated-servicenow', + is_preconfigured: true, + is_deprecated: true, + name: 'ServiceNow#xyz', + referenced_by_count: 0, + }, { id: 'my-slack1', is_preconfigured: true, + is_deprecated: false, connector_type_id: '.slack', name: 'Slack#xyz', referenced_by_count: 0, @@ -95,6 +106,7 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { { id: 'custom-system-abc-connector', is_preconfigured: true, + is_deprecated: false, connector_type_id: 'system-abc-action-type', name: 'SystemABC', referenced_by_count: 0, @@ -102,6 +114,7 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { { id: 'preconfigured.test.index-record', is_preconfigured: true, + is_deprecated: false, connector_type_id: 'test.index-record', name: 'Test:_Preconfigured_Index_Record', referenced_by_count: 0, @@ -184,6 +197,7 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { { id: createdAction.id, is_preconfigured: false, + is_deprecated: false, name: 'My action', connector_type_id: 'test.index-record', is_missing_secrets: false, @@ -195,13 +209,23 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { { id: 'preconfigured-es-index-action', is_preconfigured: true, + is_deprecated: false, connector_type_id: '.index', name: 'preconfigured_es_index_action', referenced_by_count: 0, }, + { + connector_type_id: '.servicenow', + id: 'my-deprecated-servicenow', + is_deprecated: true, + is_preconfigured: true, + name: 'ServiceNow#xyz', + referenced_by_count: 0, + }, { id: 'my-slack1', is_preconfigured: true, + is_deprecated: false, connector_type_id: '.slack', name: 'Slack#xyz', referenced_by_count: 0, @@ -209,6 +233,7 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { { id: 'custom-system-abc-connector', is_preconfigured: true, + is_deprecated: false, connector_type_id: 'system-abc-action-type', name: 'SystemABC', referenced_by_count: 0, @@ -216,6 +241,7 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { { id: 'preconfigured.test.index-record', is_preconfigured: true, + is_deprecated: false, connector_type_id: 'test.index-record', name: 'Test:_Preconfigured_Index_Record', referenced_by_count: 0, @@ -274,13 +300,23 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { { id: 'preconfigured-es-index-action', is_preconfigured: true, + is_deprecated: false, connector_type_id: '.index', name: 'preconfigured_es_index_action', referenced_by_count: 0, }, + { + connector_type_id: '.servicenow', + id: 'my-deprecated-servicenow', + is_preconfigured: true, + is_deprecated: true, + name: 'ServiceNow#xyz', + referenced_by_count: 0, + }, { id: 'my-slack1', is_preconfigured: true, + is_deprecated: false, connector_type_id: '.slack', name: 'Slack#xyz', referenced_by_count: 0, @@ -288,6 +324,7 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { { id: 'custom-system-abc-connector', is_preconfigured: true, + is_deprecated: false, connector_type_id: 'system-abc-action-type', name: 'SystemABC', referenced_by_count: 0, @@ -295,6 +332,7 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { { id: 'preconfigured.test.index-record', is_preconfigured: true, + is_deprecated: false, connector_type_id: 'test.index-record', name: 'Test:_Preconfigured_Index_Record', referenced_by_count: 0, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/update.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/update.ts index 31cb9ac03b91b..92bb7084ec534 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/update.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/update.ts @@ -73,6 +73,7 @@ export default function updateActionTests({ getService }: FtrProviderContext) { expect(response.body).to.eql({ id: createdAction.id, is_preconfigured: false, + is_deprecated: false, connector_type_id: 'test.index-record', is_missing_secrets: false, name: 'My action updated', diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/telemetry/actions_telemetry.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/telemetry/actions_telemetry.ts index 350f0019641b8..1d8d0091b67de 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/telemetry/actions_telemetry.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/telemetry/actions_telemetry.ts @@ -188,7 +188,7 @@ export default function createActionsTelemetryTests({ getService }: FtrProviderC const telemetry = JSON.parse(taskState!); // total number of connectors - expect(telemetry.count_total).to.equal(17); + expect(telemetry.count_total).to.equal(18); // total number of active connectors (used by a rule) expect(telemetry.count_active_total).to.equal(7); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/es_index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/es_index.ts index 3bc8cec9bf163..5424b18599f64 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/es_index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/es_index.ts @@ -39,6 +39,7 @@ export default function indexTest({ getService }: FtrProviderContext) { expect(createdAction).to.eql({ id: createdAction.id, isPreconfigured: false, + isDeprecated: false, name: 'An index action', actionTypeId: '.index', isMissingSecrets: false, @@ -58,6 +59,7 @@ export default function indexTest({ getService }: FtrProviderContext) { expect(fetchedAction).to.eql({ id: fetchedAction.id, isPreconfigured: false, + isDeprecated: false, isMissingSecrets: false, name: 'An index action', actionTypeId: '.index', @@ -82,6 +84,7 @@ export default function indexTest({ getService }: FtrProviderContext) { expect(createdActionWithIndex).to.eql({ id: createdActionWithIndex.id, isPreconfigured: false, + isDeprecated: false, name: 'An index action with index config', actionTypeId: '.index', isMissingSecrets: false, @@ -101,6 +104,7 @@ export default function indexTest({ getService }: FtrProviderContext) { expect(fetchedActionWithIndex).to.eql({ id: fetchedActionWithIndex.id, isPreconfigured: false, + isDeprecated: false, name: 'An index action with index config', actionTypeId: '.index', isMissingSecrets: false, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/create.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/create.ts index 95b465e08647f..9312ad8a90335 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/create.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/create.ts @@ -39,6 +39,7 @@ export default function createActionTests({ getService }: FtrProviderContext) { expect(response.body).to.eql({ id: response.body.id, is_preconfigured: false, + is_deprecated: false, name: 'My action', connector_type_id: 'test.index-record', is_missing_secrets: false, @@ -78,6 +79,7 @@ export default function createActionTests({ getService }: FtrProviderContext) { expect(response.body).to.eql({ id: response.body.id, isPreconfigured: false, + isDeprecated: false, name: 'My action', actionTypeId: 'test.index-record', isMissingSecrets: false, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get.ts index e08d99a694399..d5d5109b6e738 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get.ts @@ -40,6 +40,7 @@ export default function getActionTests({ getService }: FtrProviderContext) { .expect(200, { id: createdAction.id, is_preconfigured: false, + is_deprecated: false, is_missing_secrets: false, connector_type_id: 'test.index-record', name: 'My action', @@ -81,11 +82,24 @@ export default function getActionTests({ getService }: FtrProviderContext) { .expect(200, { id: 'my-slack1', is_preconfigured: true, + is_deprecated: false, connector_type_id: '.slack', name: 'Slack#xyz', }); }); + it('should handle get action request for deprecated connectors from preconfigured list', async () => { + await supertest + .get(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector/my-deprecated-servicenow`) + .expect(200, { + id: 'my-deprecated-servicenow', + is_preconfigured: true, + is_deprecated: true, + connector_type_id: '.servicenow', + name: 'ServiceNow#xyz', + }); + }); + describe('legacy', () => { it('should handle get action request appropriately', async () => { const { body: createdAction } = await supertest @@ -109,6 +123,7 @@ export default function getActionTests({ getService }: FtrProviderContext) { .expect(200, { id: createdAction.id, isPreconfigured: false, + isDeprecated: false, actionTypeId: 'test.index-record', isMissingSecrets: false, name: 'My action', @@ -150,6 +165,7 @@ export default function getActionTests({ getService }: FtrProviderContext) { .expect(200, { id: 'my-slack1', isPreconfigured: true, + isDeprecated: false, actionTypeId: '.slack', name: 'Slack#xyz', }); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all.ts index a965b1716a671..54a0e6e10a198 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all.ts @@ -52,11 +52,13 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { name: 'Alert history Elasticsearch index', connector_type_id: '.index', is_preconfigured: true, + is_deprecated: false, referenced_by_count: 0, }, { id: createdAction.id, is_preconfigured: false, + is_deprecated: false, name: 'My action', connector_type_id: 'test.index-record', is_missing_secrets: false, @@ -68,13 +70,23 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { { id: 'preconfigured-es-index-action', is_preconfigured: true, + is_deprecated: false, connector_type_id: '.index', name: 'preconfigured_es_index_action', referenced_by_count: 0, }, + { + connector_type_id: '.servicenow', + id: 'my-deprecated-servicenow', + is_deprecated: true, + is_preconfigured: true, + name: 'ServiceNow#xyz', + referenced_by_count: 0, + }, { id: 'my-slack1', is_preconfigured: true, + is_deprecated: false, connector_type_id: '.slack', name: 'Slack#xyz', referenced_by_count: 0, @@ -82,6 +94,7 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { { id: 'custom-system-abc-connector', is_preconfigured: true, + is_deprecated: false, connector_type_id: 'system-abc-action-type', name: 'SystemABC', referenced_by_count: 0, @@ -89,6 +102,7 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { { id: 'preconfigured.test.index-record', is_preconfigured: true, + is_deprecated: false, connector_type_id: 'test.index-record', name: 'Test:_Preconfigured_Index_Record', referenced_by_count: 0, @@ -129,18 +143,29 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { name: 'Alert history Elasticsearch index', connector_type_id: '.index', is_preconfigured: true, + is_deprecated: false, referenced_by_count: 0, }, { id: 'preconfigured-es-index-action', is_preconfigured: true, + is_deprecated: false, connector_type_id: '.index', name: 'preconfigured_es_index_action', referenced_by_count: 0, }, + { + connector_type_id: '.servicenow', + id: 'my-deprecated-servicenow', + is_deprecated: true, + is_preconfigured: true, + name: 'ServiceNow#xyz', + referenced_by_count: 0, + }, { id: 'my-slack1', is_preconfigured: true, + is_deprecated: false, connector_type_id: '.slack', name: 'Slack#xyz', referenced_by_count: 0, @@ -148,6 +173,7 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { { id: 'custom-system-abc-connector', is_preconfigured: true, + is_deprecated: false, connector_type_id: 'system-abc-action-type', name: 'SystemABC', referenced_by_count: 0, @@ -155,6 +181,7 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { { id: 'preconfigured.test.index-record', is_preconfigured: true, + is_deprecated: false, connector_type_id: 'test.index-record', name: 'Test:_Preconfigured_Index_Record', referenced_by_count: 0, @@ -196,11 +223,13 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { name: 'Alert history Elasticsearch index', actionTypeId: '.index', isPreconfigured: true, + isDeprecated: false, referencedByCount: 0, }, { id: createdAction.id, isPreconfigured: false, + isDeprecated: false, name: 'My action', actionTypeId: 'test.index-record', isMissingSecrets: false, @@ -212,13 +241,23 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { { id: 'preconfigured-es-index-action', isPreconfigured: true, + isDeprecated: false, actionTypeId: '.index', name: 'preconfigured_es_index_action', referencedByCount: 0, }, + { + actionTypeId: '.servicenow', + id: 'my-deprecated-servicenow', + isDeprecated: true, + isPreconfigured: true, + name: 'ServiceNow#xyz', + referencedByCount: 0, + }, { id: 'my-slack1', isPreconfigured: true, + isDeprecated: false, actionTypeId: '.slack', name: 'Slack#xyz', referencedByCount: 0, @@ -226,6 +265,7 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { { id: 'custom-system-abc-connector', isPreconfigured: true, + isDeprecated: false, actionTypeId: 'system-abc-action-type', name: 'SystemABC', referencedByCount: 0, @@ -233,6 +273,7 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { { id: 'preconfigured.test.index-record', isPreconfigured: true, + isDeprecated: false, actionTypeId: 'test.index-record', name: 'Test:_Preconfigured_Index_Record', referencedByCount: 0, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/type_not_enabled.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/type_not_enabled.ts index eafe4f487773f..0ab9baad0b554 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/type_not_enabled.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/type_not_enabled.ts @@ -62,6 +62,7 @@ export default function typeNotEnabledTests({ getService }: FtrProviderContext) config: {}, id: 'uuid-actionId', isPreconfigured: false, + isDeprecated: false, isMissingSecrets: false, name: 'an action created before test.not-enabled was disabled', }); @@ -90,6 +91,7 @@ export default function typeNotEnabledTests({ getService }: FtrProviderContext) config: {}, id: 'uuid-actionId', isPreconfigured: false, + isDeprecated: false, isMissingSecrets: false, name: 'an action created before test.not-enabled was disabled', }); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/update.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/update.ts index f779a97eafe2e..fad31d0bd3a12 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/update.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/update.ts @@ -51,6 +51,7 @@ export default function updateActionTests({ getService }: FtrProviderContext) { .expect(200, { id: createdAction.id, is_preconfigured: false, + is_deprecated: false, connector_type_id: 'test.index-record', is_missing_secrets: false, name: 'My action updated', @@ -193,6 +194,7 @@ export default function updateActionTests({ getService }: FtrProviderContext) { .expect(200, { id: createdAction.id, isPreconfigured: false, + isDeprecated: false, actionTypeId: 'test.index-record', isMissingSecrets: false, name: 'My action updated', diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts index b5a58d3af5086..020159253d086 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts @@ -88,6 +88,7 @@ export default ({ getService }: FtrProviderContext): void => { projectKey: 'pkey', }, isPreconfigured: false, + isDeprecated: false, isMissingSecrets: false, referencedByCount: 0, }, @@ -100,6 +101,7 @@ export default ({ getService }: FtrProviderContext): void => { orgId: 'pkey', }, isPreconfigured: false, + isDeprecated: false, isMissingSecrets: false, referencedByCount: 0, }, @@ -112,6 +114,7 @@ export default ({ getService }: FtrProviderContext): void => { usesTableApi: false, }, isPreconfigured: false, + isDeprecated: false, isMissingSecrets: false, referencedByCount: 0, }, @@ -124,6 +127,7 @@ export default ({ getService }: FtrProviderContext): void => { usesTableApi: false, }, isPreconfigured: false, + isDeprecated: false, isMissingSecrets: false, referencedByCount: 0, }, diff --git a/x-pack/test/cases_api_integration/spaces_only/tests/trial/configure/get_connectors.ts b/x-pack/test/cases_api_integration/spaces_only/tests/trial/configure/get_connectors.ts index 02b91c9f0b918..2abe2996d5e21 100644 --- a/x-pack/test/cases_api_integration/spaces_only/tests/trial/configure/get_connectors.ts +++ b/x-pack/test/cases_api_integration/spaces_only/tests/trial/configure/get_connectors.ts @@ -88,6 +88,7 @@ export default ({ getService }: FtrProviderContext): void => { projectKey: 'pkey', }, isPreconfigured: false, + isDeprecated: false, isMissingSecrets: false, referencedByCount: 0, }, @@ -100,6 +101,7 @@ export default ({ getService }: FtrProviderContext): void => { orgId: 'pkey', }, isPreconfigured: false, + isDeprecated: false, isMissingSecrets: false, referencedByCount: 0, }, @@ -112,6 +114,7 @@ export default ({ getService }: FtrProviderContext): void => { usesTableApi: false, }, isPreconfigured: false, + isDeprecated: false, isMissingSecrets: false, referencedByCount: 0, }, @@ -124,6 +127,7 @@ export default ({ getService }: FtrProviderContext): void => { usesTableApi: false, }, isPreconfigured: false, + isDeprecated: false, isMissingSecrets: false, referencedByCount: 0, }, From b013b9695e9c7f97f66363aeba70d923b3df22d8 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Tue, 26 Apr 2022 12:40:23 +0200 Subject: [PATCH 06/29] [Synthetics] Remove deprecated api (index pattern) usage (#130949) --- x-pack/plugins/synthetics/kibana.json | 1 + .../plugins/synthetics/public/apps/plugin.ts | 2 ++ .../synthetics/public/apps/uptime_app.tsx | 6 +++--- .../alerts/alert_query_bar/query_bar.tsx | 6 +++--- .../alerts/alerts_containers/use_snap_shot.ts | 6 +++--- .../filters_expression_select.tsx | 8 +++---- .../monitor_status_alert/add_filter_btn.tsx | 6 +++--- .../overview/filter_group/filter_group.tsx | 8 +++---- .../filter_group/selected_filters.tsx | 10 ++++----- .../overview/query_bar/query_bar.tsx | 6 +++--- .../overview/query_bar/use_query_bar.ts | 6 +++--- ...ntext.tsx => uptime_data_view_context.tsx} | 21 +++++++++---------- .../plugins/synthetics/public/hooks/index.ts | 2 +- .../public/hooks/update_kuery_string.ts | 8 +++---- .../lazy_wrapper/monitor_status.tsx | 6 +++--- 15 files changed, 52 insertions(+), 50 deletions(-) rename x-pack/plugins/synthetics/public/contexts/{uptime_index_pattern_context.tsx => uptime_data_view_context.tsx} (50%) diff --git a/x-pack/plugins/synthetics/kibana.json b/x-pack/plugins/synthetics/kibana.json index d65a89a16161f..bb827019fc70a 100644 --- a/x-pack/plugins/synthetics/kibana.json +++ b/x-pack/plugins/synthetics/kibana.json @@ -8,6 +8,7 @@ "cases", "embeddable", "discover", + "dataViews", "encryptedSavedObjects", "features", "inspector", diff --git a/x-pack/plugins/synthetics/public/apps/plugin.ts b/x-pack/plugins/synthetics/public/apps/plugin.ts index 4a3b3668dab0a..7f1a773376688 100644 --- a/x-pack/plugins/synthetics/public/apps/plugin.ts +++ b/x-pack/plugins/synthetics/public/apps/plugin.ts @@ -38,6 +38,7 @@ import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; import { Start as InspectorPluginStart } from '@kbn/inspector-plugin/public'; import { CasesUiStart } from '@kbn/cases-plugin/public'; import { CloudSetup } from '@kbn/cloud-plugin/public'; +import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import { PLUGIN } from '../../common/constants/plugin'; import { LazySyntheticsPolicyCreateExtension, @@ -67,6 +68,7 @@ export interface ClientPluginsStart { share: SharePluginStart; triggersActionsUi: TriggersAndActionsUIPublicPluginStart; cases: CasesUiStart; + dataViews: DataViewsPublicPluginStart; } export interface UptimePluginServices extends Partial { diff --git a/x-pack/plugins/synthetics/public/apps/uptime_app.tsx b/x-pack/plugins/synthetics/public/apps/uptime_app.tsx index 1ca9573308dbc..9887fa81393bc 100644 --- a/x-pack/plugins/synthetics/public/apps/uptime_app.tsx +++ b/x-pack/plugins/synthetics/public/apps/uptime_app.tsx @@ -33,7 +33,7 @@ import { UptimeAlertsFlyoutWrapper } from '../components/overview'; import { store, storage } from '../state'; import { kibanaService } from '../state/kibana_service'; import { ActionMenu } from '../components/common/header/action_menu'; -import { UptimeIndexPatternContextProvider } from '../contexts/uptime_index_pattern_context'; +import { UptimeDataViewContextProvider } from '../contexts/uptime_data_view_context'; export interface UptimeAppColors { danger: string; @@ -124,7 +124,7 @@ const Application = (props: UptimeAppProps) => { - +
{
-
+
diff --git a/x-pack/plugins/synthetics/public/components/overview/alerts/alert_query_bar/query_bar.tsx b/x-pack/plugins/synthetics/public/components/overview/alerts/alert_query_bar/query_bar.tsx index b990fb70a224c..288cd7232a3d5 100644 --- a/x-pack/plugins/synthetics/public/components/overview/alerts/alert_query_bar/query_bar.tsx +++ b/x-pack/plugins/synthetics/public/components/overview/alerts/alert_query_bar/query_bar.tsx @@ -11,7 +11,7 @@ import { i18n } from '@kbn/i18n'; import { QueryStringInput } from '@kbn/unified-search-plugin/public'; import { isValidKuery } from '../../query_bar/query_bar'; import * as labels from '../translations'; -import { useIndexPattern } from '../../../../hooks'; +import { useUptimeDataView } from '../../../../hooks'; interface Props { query: string; @@ -19,7 +19,7 @@ interface Props { } export const AlertQueryBar = ({ query = '', onChange }: Props) => { - const indexPattern = useIndexPattern(); + const dataView = useUptimeDataView(); const [inputVal, setInputVal] = useState(query); @@ -31,7 +31,7 @@ export const AlertQueryBar = ({ query = '', onChange }: Props) => { return ( { diff --git a/x-pack/plugins/synthetics/public/components/overview/alerts/alerts_containers/use_snap_shot.ts b/x-pack/plugins/synthetics/public/components/overview/alerts/alerts_containers/use_snap_shot.ts index e6aee6207093e..7fd0f0b3a410b 100644 --- a/x-pack/plugins/synthetics/public/components/overview/alerts/alerts_containers/use_snap_shot.ts +++ b/x-pack/plugins/synthetics/public/components/overview/alerts/alerts_containers/use_snap_shot.ts @@ -6,7 +6,7 @@ */ import { useFetcher } from '@kbn/observability-plugin/public'; -import { useIndexPattern, generateUpdatedKueryString } from '../../../../hooks'; +import { useUptimeDataView, generateUpdatedKueryString } from '../../../../hooks'; import { fetchSnapshotCount } from '../../../../state/api'; export const useSnapShotCount = ({ query, filters }: { query: string; filters: [] | string }) => { @@ -15,9 +15,9 @@ export const useSnapShotCount = ({ query, filters }: { query: string; filters: [ ? '' : JSON.stringify(Array.from(Object.entries(filters))); - const indexPattern = useIndexPattern(); + const dataView = useUptimeDataView(); - const [esKuery, error] = generateUpdatedKueryString(indexPattern, query, parsedFilters); + const [esKuery, error] = generateUpdatedKueryString(dataView, query, parsedFilters); const { data, loading } = useFetcher( () => diff --git a/x-pack/plugins/synthetics/public/components/overview/alerts/monitor_expressions/filters_expression_select.tsx b/x-pack/plugins/synthetics/public/components/overview/alerts/monitor_expressions/filters_expression_select.tsx index e34caf1b95b8b..3720179838899 100644 --- a/x-pack/plugins/synthetics/public/components/overview/alerts/monitor_expressions/filters_expression_select.tsx +++ b/x-pack/plugins/synthetics/public/components/overview/alerts/monitor_expressions/filters_expression_select.tsx @@ -10,7 +10,7 @@ import { EuiButtonIcon, EuiExpression, EuiFlexGroup, EuiFlexItem, EuiSpacer } fr import { FieldValueSuggestions } from '@kbn/observability-plugin/public'; import { filterLabels } from '../../filter_group/translations'; import { alertFilterLabels, filterAriaLabels } from './translations'; -import { useIndexPattern } from '../../../../contexts/uptime_index_pattern_context'; +import { useUptimeDataView } from '../../../../contexts/uptime_data_view_context'; import { FILTER_FIELDS } from '../../../../../common/constants'; import { useGetUrlParams } from '../../../../hooks'; @@ -122,7 +122,7 @@ export const FiltersExpressionsSelect: React.FC = (curr) => curr.selectedItems.length > 0 || newFilters?.includes(curr.fieldName) ); - const indexPattern = useIndexPattern(); + const dataView = useUptimeDataView(); return ( <> @@ -130,11 +130,11 @@ export const FiltersExpressionsSelect: React.FC = ({ description, id, title, value, fieldName, ariaLabel, selectedItems }) => ( - {indexPattern && ( + {dataView && ( { diff --git a/x-pack/plugins/synthetics/public/components/overview/alerts/monitor_status_alert/add_filter_btn.tsx b/x-pack/plugins/synthetics/public/components/overview/alerts/monitor_status_alert/add_filter_btn.tsx index 58b8e7bb085da..81e782696d58f 100644 --- a/x-pack/plugins/synthetics/public/components/overview/alerts/monitor_status_alert/add_filter_btn.tsx +++ b/x-pack/plugins/synthetics/public/components/overview/alerts/monitor_status_alert/add_filter_btn.tsx @@ -8,7 +8,7 @@ import React, { useState } from 'react'; import { EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui'; import * as labels from '../translations'; -import { useIndexPattern } from '../../../../contexts/uptime_index_pattern_context'; +import { useUptimeDataView } from '../../../../contexts/uptime_data_view_context'; interface Props { newFilters: string[]; @@ -21,7 +21,7 @@ export const AddFilterButton: React.FC = ({ newFilters, onNewFilter, aler const getSelectedItems = (fieldName: string) => alertFilters?.[fieldName] ?? []; - const indexPattern = useIndexPattern(); + const dataView = useUptimeDataView(); const onButtonClick = () => { setPopover(!isPopoverOpen); @@ -65,7 +65,7 @@ export const AddFilterButton: React.FC = ({ newFilters, onNewFilter, aler onClick={onButtonClick} size="s" flush="left" - isLoading={!indexPattern} + isLoading={!dataView} > {labels.ADD_FILTER} diff --git a/x-pack/plugins/synthetics/public/components/overview/filter_group/filter_group.tsx b/x-pack/plugins/synthetics/public/components/overview/filter_group/filter_group.tsx index d73c2df73919b..8a0ac4cbe81b1 100644 --- a/x-pack/plugins/synthetics/public/components/overview/filter_group/filter_group.tsx +++ b/x-pack/plugins/synthetics/public/components/overview/filter_group/filter_group.tsx @@ -13,7 +13,7 @@ import { FieldValueSuggestions, useInspectorContext } from '@kbn/observability-p import { useFilterUpdate } from '../../../hooks/use_filter_update'; import { useSelectedFilters } from '../../../hooks/use_selected_filters'; import { SelectedFilters } from './selected_filters'; -import { useIndexPattern } from '../../../contexts/uptime_index_pattern_context'; +import { useUptimeDataView } from '../../../contexts/uptime_data_view_context'; import { useGetUrlParams } from '../../../hooks'; import { EXCLUDE_RUN_ONCE_FILTER } from '../../../../common/constants/client_defaults'; @@ -40,7 +40,7 @@ export const FilterGroup = () => { const { filtersList } = useSelectedFilters(); - const indexPattern = useIndexPattern(); + const dataView = useUptimeDataView(); const onFilterFieldChange = useCallback( (fieldName: string, values: string[], notValues: string[]) => { @@ -52,12 +52,12 @@ export const FilterGroup = () => { return ( <> - {indexPattern && + {dataView && filtersList.map(({ field, label, selectedItems, excludedItems }) => ( void; } export const SelectedFilters = ({ onChange }: Props) => { - const indexPattern = useIndexPattern(); + const dataView = useUptimeDataView(); const { filtersList } = useSelectedFilters(); - if (!indexPattern) return null; + if (!dataView) return null; return ( @@ -26,7 +26,7 @@ export const SelectedFilters = ({ onChange }: Props) => { ...selectedItems.map((value) => ( { onChange( field, @@ -51,7 +51,7 @@ export const SelectedFilters = ({ onChange }: Props) => { ...excludedItems.map((value) => ( { onChange( field, diff --git a/x-pack/plugins/synthetics/public/components/overview/query_bar/query_bar.tsx b/x-pack/plugins/synthetics/public/components/overview/query_bar/query_bar.tsx index c2ecc5a299178..3784bc58b76b2 100644 --- a/x-pack/plugins/synthetics/public/components/overview/query_bar/query_bar.tsx +++ b/x-pack/plugins/synthetics/public/components/overview/query_bar/query_bar.tsx @@ -11,7 +11,7 @@ import { EuiFlexItem } from '@elastic/eui'; import { QueryStringInput } from '@kbn/unified-search-plugin/public'; import { SyntaxType, useQueryBar } from './use_query_bar'; import { KQL_PLACE_HOLDER, SIMPLE_SEARCH_PLACEHOLDER } from './translations'; -import { useGetUrlParams, useIndexPattern } from '../../../hooks'; +import { useGetUrlParams, useUptimeDataView } from '../../../hooks'; const SYNTAX_STORAGE = 'uptime:queryBarSyntax'; @@ -35,7 +35,7 @@ export const QueryBar = () => { const { query, setQuery, submitImmediately } = useQueryBar(); - const indexPattern = useIndexPattern(); + const dataView = useUptimeDataView(); const [inputVal, setInputVal] = useState(query.query as string); @@ -49,7 +49,7 @@ export const QueryBar = () => { return ( { } ); - const indexPattern = useIndexPattern(); + const dataView = useUptimeDataView(); const [, updateUrlParams] = useUrlParams(); const [esFilters, error] = generateUpdatedKueryString( - indexPattern, + dataView, query.language === SyntaxType.kuery ? (query.query as string) : undefined, paramFilters, excludedFilters diff --git a/x-pack/plugins/synthetics/public/contexts/uptime_index_pattern_context.tsx b/x-pack/plugins/synthetics/public/contexts/uptime_data_view_context.tsx similarity index 50% rename from x-pack/plugins/synthetics/public/contexts/uptime_index_pattern_context.tsx rename to x-pack/plugins/synthetics/public/contexts/uptime_data_view_context.tsx index 765d9f8527c88..518d2061d87ae 100644 --- a/x-pack/plugins/synthetics/public/contexts/uptime_index_pattern_context.tsx +++ b/x-pack/plugins/synthetics/public/contexts/uptime_data_view_context.tsx @@ -7,27 +7,26 @@ import React, { createContext, useContext } from 'react'; import { useFetcher } from '@kbn/observability-plugin/public'; -import { DataPublicPluginStart, IndexPattern } from '@kbn/data-plugin/public'; +import { DataViewsPublicPluginStart, DataView } from '@kbn/data-views-plugin/public'; import { useHasData } from '../components/overview/empty_state/use_has_data'; -export const UptimeIndexPatternContext = createContext({} as IndexPattern); +export const UptimeDataViewContext = createContext({} as DataView); -export const UptimeIndexPatternContextProvider: React.FC<{ data: DataPublicPluginStart }> = ({ - children, - data: { indexPatterns }, -}) => { +export const UptimeDataViewContextProvider: React.FC<{ + dataViews: DataViewsPublicPluginStart; +}> = ({ children, dataViews }) => { const { settings, data: indexStatus } = useHasData(); const heartbeatIndices = settings?.heartbeatIndices || ''; - const { data } = useFetcher>(async () => { + const { data } = useFetcher>(async () => { if (heartbeatIndices && indexStatus?.indexExists) { - // this only creates an index pattern in memory, not as saved object - return indexPatterns.create({ title: heartbeatIndices }); + // this only creates an dateView in memory, not as saved object + return dataViews.create({ title: heartbeatIndices }); } }, [heartbeatIndices, indexStatus?.indexExists]); - return ; + return ; }; -export const useIndexPattern = () => useContext(UptimeIndexPatternContext); +export const useUptimeDataView = () => useContext(UptimeDataViewContext); diff --git a/x-pack/plugins/synthetics/public/hooks/index.ts b/x-pack/plugins/synthetics/public/hooks/index.ts index e96d746a05514..b93373481f9f3 100644 --- a/x-pack/plugins/synthetics/public/hooks/index.ts +++ b/x-pack/plugins/synthetics/public/hooks/index.ts @@ -13,4 +13,4 @@ export * from './use_cert_status'; export * from './use_telemetry'; export * from './use_url_params'; export * from './use_breakpoints'; -export { useIndexPattern } from '../contexts/uptime_index_pattern_context'; +export { useUptimeDataView } from '../contexts/uptime_data_view_context'; diff --git a/x-pack/plugins/synthetics/public/hooks/update_kuery_string.ts b/x-pack/plugins/synthetics/public/hooks/update_kuery_string.ts index 8d31ef0abbd91..a8884279f49ba 100644 --- a/x-pack/plugins/synthetics/public/hooks/update_kuery_string.ts +++ b/x-pack/plugins/synthetics/public/hooks/update_kuery_string.ts @@ -6,7 +6,7 @@ */ import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; -import type { IndexPattern } from '@kbn/data-plugin/public'; +import { DataView } from '@kbn/data-views-plugin/public'; import { combineFiltersAndUserSearch, stringifyKueries } from '../../common/lib'; const getKueryString = (urlFilters: string, excludedFilters?: string): string => { @@ -42,7 +42,7 @@ const getKueryString = (urlFilters: string, excludedFilters?: string): string => }; export const generateUpdatedKueryString = ( - indexPattern: IndexPattern | null, + dataView: DataView | null, filterQueryString = '', urlFilters: string, excludedFilters?: string @@ -55,10 +55,10 @@ export const generateUpdatedKueryString = ( // this try catch is necessary to evaluate user input in kuery bar, // this error will be actually shown in UI for user to see try { - if ((filterQueryString || urlFilters || excludedFilters) && indexPattern) { + if ((filterQueryString || urlFilters || excludedFilters) && dataView) { const ast = fromKueryExpression(combinedFilterString); - const elasticsearchQuery = toElasticsearchQuery(ast, indexPattern); + const elasticsearchQuery = toElasticsearchQuery(ast, dataView); esFilters = JSON.stringify(elasticsearchQuery); } diff --git a/x-pack/plugins/synthetics/public/lib/alert_types/lazy_wrapper/monitor_status.tsx b/x-pack/plugins/synthetics/public/lib/alert_types/lazy_wrapper/monitor_status.tsx index 362263fa006ab..10aa71fa533e3 100644 --- a/x-pack/plugins/synthetics/public/lib/alert_types/lazy_wrapper/monitor_status.tsx +++ b/x-pack/plugins/synthetics/public/lib/alert_types/lazy_wrapper/monitor_status.tsx @@ -13,7 +13,7 @@ import { store } from '../../../state'; import { ClientPluginsStart } from '../../../apps/plugin'; import { kibanaService } from '../../../state/kibana_service'; import { AlertMonitorStatus } from '../../../components/overview/alerts/alerts_containers/alert_monitor_status'; -import { UptimeIndexPatternContextProvider } from '../../../contexts/uptime_index_pattern_context'; +import { UptimeDataViewContextProvider } from '../../../contexts/uptime_data_view_context'; interface Props { core: CoreStart; @@ -27,9 +27,9 @@ export default function MonitorStatusAlert({ core, plugins, params }: Props) { return ( - + - + ); From f8d0a2f0e0cf617b2b199fa95fcffb2b6f765486 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Tue, 26 Apr 2022 13:17:05 +0200 Subject: [PATCH 07/29] ux app, remove deprecated api usage (#130952) * ux app, remove deprecated api usage * update --- x-pack/plugins/ux/kibana.json | 1 + x-pack/plugins/ux/public/application/ux_app.tsx | 3 ++- .../app/rum_dashboard/local_uifilters/use_data_view.ts | 10 ++++------ x-pack/plugins/ux/public/plugin.ts | 2 ++ 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/ux/kibana.json b/x-pack/plugins/ux/kibana.json index bc57b115b7299..5b403e8d3a331 100644 --- a/x-pack/plugins/ux/kibana.json +++ b/x-pack/plugins/ux/kibana.json @@ -5,6 +5,7 @@ "requiredPlugins": [ "features", "data", + "dataViews", "licensing", "triggersActionsUi", "embeddable", diff --git a/x-pack/plugins/ux/public/application/ux_app.tsx b/x-pack/plugins/ux/public/application/ux_app.tsx index d2879d4fa2797..0ea4f9ce83893 100644 --- a/x-pack/plugins/ux/public/application/ux_app.tsx +++ b/x-pack/plugins/ux/public/application/ux_app.tsx @@ -106,7 +106,7 @@ export function UXAppRoot({ appMountParameters, core, deps, - corePlugins: { embeddable, inspector, maps, observability, data }, + corePlugins: { embeddable, inspector, maps, observability, data, dataViews }, }: { appMountParameters: AppMountParameters; core: CoreStart; @@ -130,6 +130,7 @@ export function UXAppRoot({ observability, embeddable, data, + dataViews, }} > diff --git a/x-pack/plugins/ux/public/components/app/rum_dashboard/local_uifilters/use_data_view.ts b/x-pack/plugins/ux/public/components/app/rum_dashboard/local_uifilters/use_data_view.ts index bb80fd68ff283..f5c446908a801 100644 --- a/x-pack/plugins/ux/public/components/app/rum_dashboard/local_uifilters/use_data_view.ts +++ b/x-pack/plugins/ux/public/components/app/rum_dashboard/local_uifilters/use_data_view.ts @@ -5,9 +5,9 @@ * 2.0. */ -import { DataView } from '@kbn/data-plugin/common'; +import { DataView } from '@kbn/data-views-plugin/common'; import { useKibana } from '@kbn/kibana-react-plugin/public'; -import { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import { useFetcher } from '../../../../hooks/use_fetcher'; import { useDynamicDataViewFetcher } from '../../../../hooks/use_dynamic_data_view'; @@ -15,10 +15,8 @@ export function useDataView() { const { dataView } = useDynamicDataViewFetcher(); const { - services: { - data: { dataViews }, - }, - } = useKibana<{ data: DataPublicPluginStart }>(); + services: { dataViews }, + } = useKibana<{ dataViews: DataViewsPublicPluginStart }>(); const { data } = useFetcher>(async () => { if (dataView?.title) { diff --git a/x-pack/plugins/ux/public/plugin.ts b/x-pack/plugins/ux/public/plugin.ts index f56c7a140ade5..fe98a490b5b80 100644 --- a/x-pack/plugins/ux/public/plugin.ts +++ b/x-pack/plugins/ux/public/plugin.ts @@ -32,6 +32,7 @@ import { LicensingPluginSetup } from '@kbn/licensing-plugin/public'; import { EmbeddableStart } from '@kbn/embeddable-plugin/public'; import { MapsStartApi } from '@kbn/maps-plugin/public'; import { Start as InspectorPluginStart } from '@kbn/inspector-plugin/public'; +import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; export type UxPluginSetup = void; export type UxPluginStart = void; @@ -52,6 +53,7 @@ export interface ApmPluginStartDeps { maps?: MapsStartApi; inspector: InspectorPluginStart; observability: ObservabilityPublicStart; + dataViews: DataViewsPublicPluginStart; } export class UxPlugin implements Plugin { From 2d4ee2ed85fba49953651c58eb44b8b1e31b5bcc Mon Sep 17 00:00:00 2001 From: Dmitry Tomashevich <39378793+Dmitriynj@users.noreply.github.com> Date: Tue, 26 Apr 2022 16:27:33 +0500 Subject: [PATCH 08/29] [Discover] Fix links in helper callouts (#130873) * [Discover] fix doc explorer link in callout * [Discover] improve the fix Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../document_explorer_callout/document_explorer_callout.tsx | 2 +- .../document_explorer_update_callout.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/plugins/discover/public/application/main/components/document_explorer_callout/document_explorer_callout.tsx b/src/plugins/discover/public/application/main/components/document_explorer_callout/document_explorer_callout.tsx index 69dac6b6ce30a..9087d380d8bf2 100644 --- a/src/plugins/discover/public/application/main/components/document_explorer_callout/document_explorer_callout.tsx +++ b/src/plugins/discover/public/application/main/components/document_explorer_callout/document_explorer_callout.tsx @@ -99,7 +99,7 @@ export const DocumentExplorerCallout = () => { - + { Learn more about the structure of your data with {fieldStatistics}." values={{ fieldStatistics: ( - + { ), documentExplorer: ( - + Date: Tue, 26 Apr 2022 13:37:10 +0200 Subject: [PATCH 09/29] Add SecuritySolutionLinkButton and withSecuritySolutionLink (#130879) --- .../common/components/links/index.test.tsx | 29 ++++++++ .../public/common/components/links/index.tsx | 70 +++++++++++++++++-- 2 files changed, 95 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/links/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/links/index.test.tsx index adab4db904d6a..e7c82420ef9e0 100644 --- a/x-pack/plugins/security_solution/public/common/components/links/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/links/index.test.tsx @@ -24,12 +24,15 @@ import { PortOrServiceNameLink, DEFAULT_NUMBER_OF_LINK, ExternalLink, + SecuritySolutionLinkButton, } from '.'; +import { SecurityPageName } from '../../../app/types'; jest.mock('../link_to'); jest.mock('../../../overview/components/events_by_dataset'); +const mockNavigateTo = jest.fn(); jest.mock('../../lib/kibana', () => { return { useUiSetting$: jest.fn(), @@ -40,6 +43,9 @@ jest.mock('../../lib/kibana', () => { }, }, }), + useNavigation: () => ({ + navigateTo: mockNavigateTo, + }), }; }); @@ -580,4 +586,27 @@ describe('Custom Links', () => { ); }); }); + + describe('SecuritySolutionLinkButton', () => { + it('injects href prop with hosts page path', () => { + const path = 'testTabPath'; + + const wrapper = mount( + + ); + + expect(wrapper.find('LinkButton').prop('href')).toEqual(path); + }); + + it('injects onClick prop that calls navigateTo', () => { + const path = 'testTabPath'; + + const wrapper = mount( + + ); + wrapper.find('LinkButton').simulate('click'); + + expect(mockNavigateTo).toHaveBeenLastCalledWith({ url: path }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/links/index.tsx b/x-pack/plugins/security_solution/public/common/components/links/index.tsx index 6020b0def0aed..fb244c40d6e3d 100644 --- a/x-pack/plugins/security_solution/public/common/components/links/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/links/index.tsx @@ -13,9 +13,8 @@ import { EuiLink, EuiToolTip, } from '@elastic/eui'; -import React, { useMemo, useCallback, SyntheticEvent } from 'react'; +import React, { useMemo, useCallback, SyntheticEvent, MouseEventHandler, MouseEvent } from 'react'; import { isArray, isNil } from 'lodash/fp'; - import { IP_REPUTATION_LINKS_SETTING, APP_UI_ID } from '../../../../common/constants'; import { DefaultFieldRendererOverflow, @@ -34,13 +33,13 @@ import { FlowTarget, FlowTargetSourceDest, } from '../../../../common/search_strategy/security_solution/network'; -import { useUiSetting$, useKibana } from '../../lib/kibana'; +import { useUiSetting$, useKibana, useNavigation } from '../../lib/kibana'; import { isUrlInvalid } from '../../utils/validators'; import * as i18n from './translations'; import { SecurityPageName } from '../../../app/types'; import { getUsersDetailsUrl } from '../link_to/redirect_to_users'; -import { LinkAnchor, GenericLinkButton, PortContainer, Comma } from './helpers'; +import { LinkAnchor, GenericLinkButton, PortContainer, Comma, LinkButton } from './helpers'; import { HostsTableType } from '../../../hosts/store/model'; export { LinkButton, LinkAnchor } from './helpers'; @@ -514,3 +513,66 @@ export const WhoIsLink = React.memo<{ children?: React.ReactNode; domain: string ); WhoIsLink.displayName = 'WhoIsLink'; + +interface SecuritySolutionLinkProps { + deepLinkId: SecurityPageName; + path?: string; +} + +type LinkClickEvent = MouseEvent; +type LinkClickEventHandler = MouseEventHandler; + +interface SecuritySolutionInjectedLinkProps { + onClick?: LinkClickEventHandler; + href?: string; +} + +/** + * HOC that wraps any Link component and makes it a Security solutions internal navigation Link. + * + * It injects `onClick` and 'href' into the Link component calculated based on the` deepLinkId` and `path` parameters. + */ +export const withSecuritySolutionLink = ( + WrappedComponent: React.FC +) => { + const SecuritySolutionLink: React.FC> = ({ + deepLinkId, + path, + onClick: onClickProps, + ...rest + }) => { + const { formatUrl } = useFormatUrl(deepLinkId); + const { navigateTo } = useNavigation(); + const url = useMemo(() => formatUrl(path ?? ''), [formatUrl, path]); + + const onClick = useCallback( + (ev: LinkClickEvent) => { + ev.preventDefault(); + + if (onClickProps) { + onClickProps(ev); + } + + navigateTo({ url }); + }, + [navigateTo, url, onClickProps] + ); + + return ; + }; + return SecuritySolutionLink; +}; + +/** + * Security Solutions internal link. + * + * `const example = () => ;` + */ +export const SecuritySolutionLinkButton = withSecuritySolutionLink(LinkButton); + +/** + * Security Solutions internal link. + * + * `const example = () => ;` + */ +export const SecuritySolutionLinkAnchor = withSecuritySolutionLink(LinkAnchor); From bdf8f2c52736020b6b37281355daa986d68cec1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= Date: Tue, 26 Apr 2022 14:41:16 +0200 Subject: [PATCH 10/29] [Stack Monitoring] Convert beats routes to TypeScript (#130725) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/monitoring/common/ccs_utils.ts | 4 +-- .../common/http_api/apm/post_apm_instance.ts | 12 +++---- .../common/http_api/apm/post_apm_instances.ts | 12 +++---- .../common/http_api/apm/post_apm_overview.ts | 12 +++---- .../monitoring/common/http_api/beats/index.ts | 10 ++++++ .../common/http_api/beats/post_beat_detail.ts | 29 +++++++++++++++ .../http_api/beats/post_beats_listing.ts | 28 +++++++++++++++ .../http_api/beats/post_beats_overview.ts | 28 +++++++++++++++ .../monitoring/common/http_api/shared/ccs.ts | 10 ++++++ .../common/http_api/shared/cluster.ts | 10 ++++++ .../common/http_api/shared/index.ts | 10 ++++++ .../common/http_api/shared/time_range.ts | 13 +++++++ .../server/lib/details/get_metrics.ts | 13 +++++-- .../routes/api/v1/apm/metric_set_instance.ts | 4 ++- .../routes/api/v1/apm/metric_set_overview.ts | 4 ++- .../beats/{beat_detail.js => beat_detail.ts} | 35 ++++++++---------- .../api/v1/beats/{beats.js => beats.ts} | 36 +++++++++---------- .../api/v1/beats/{index.js => index.ts} | 0 ...ric_set_detail.js => metric_set_detail.ts} | 4 ++- ...set_overview.js => metric_set_overview.ts} | 4 ++- .../api/v1/beats/{overview.js => overview.ts} | 36 +++++++++---------- 21 files changed, 224 insertions(+), 90 deletions(-) create mode 100644 x-pack/plugins/monitoring/common/http_api/beats/index.ts create mode 100644 x-pack/plugins/monitoring/common/http_api/beats/post_beat_detail.ts create mode 100644 x-pack/plugins/monitoring/common/http_api/beats/post_beats_listing.ts create mode 100644 x-pack/plugins/monitoring/common/http_api/beats/post_beats_overview.ts create mode 100644 x-pack/plugins/monitoring/common/http_api/shared/ccs.ts create mode 100644 x-pack/plugins/monitoring/common/http_api/shared/cluster.ts create mode 100644 x-pack/plugins/monitoring/common/http_api/shared/index.ts create mode 100644 x-pack/plugins/monitoring/common/http_api/shared/time_range.ts rename x-pack/plugins/monitoring/server/routes/api/v1/beats/{beat_detail.js => beat_detail.ts} (71%) rename x-pack/plugins/monitoring/server/routes/api/v1/beats/{beats.js => beats.ts} (61%) rename x-pack/plugins/monitoring/server/routes/api/v1/beats/{index.js => index.ts} (100%) rename x-pack/plugins/monitoring/server/routes/api/v1/beats/{metric_set_detail.js => metric_set_detail.ts} (91%) rename x-pack/plugins/monitoring/server/routes/api/v1/beats/{metric_set_overview.js => metric_set_overview.ts} (89%) rename x-pack/plugins/monitoring/server/routes/api/v1/beats/{overview.js => overview.ts} (68%) diff --git a/x-pack/plugins/monitoring/common/ccs_utils.ts b/x-pack/plugins/monitoring/common/ccs_utils.ts index 69cc1c25c372a..fea8f79bbfbbf 100644 --- a/x-pack/plugins/monitoring/common/ccs_utils.ts +++ b/x-pack/plugins/monitoring/common/ccs_utils.ts @@ -23,7 +23,7 @@ export function prefixIndexPatternWithCcs( config: MonitoringConfig, indexPattern: string, ccs?: string -) { +): string { const ccsEnabled = config.ui.ccs.enabled; if (!ccsEnabled || !ccs) { return indexPattern; @@ -67,7 +67,7 @@ export function prefixIndexPatternWithCcs( * @param {String} indexName The index's name, possibly including the cross-cluster prefix * @return {String} {@code null} if none. Otherwise the cluster prefix. */ -export function parseCrossClusterPrefix(indexName: string) { +export function parseCrossClusterPrefix(indexName: string): string | null { const colonIndex = indexName.indexOf(':'); if (colonIndex === -1) { diff --git a/x-pack/plugins/monitoring/common/http_api/apm/post_apm_instance.ts b/x-pack/plugins/monitoring/common/http_api/apm/post_apm_instance.ts index 537b68dabd888..c0bd4688ce0c0 100644 --- a/x-pack/plugins/monitoring/common/http_api/apm/post_apm_instance.ts +++ b/x-pack/plugins/monitoring/common/http_api/apm/post_apm_instance.ts @@ -6,26 +6,24 @@ */ import * as rt from 'io-ts'; +import { ccsRT, clusterUuidRT, timeRangeRT } from '../shared'; export const postApmInstanceRequestParamsRT = rt.type({ - clusterUuid: rt.string, + clusterUuid: clusterUuidRT, apmUuid: rt.string, }); export const postApmInstanceRequestPayloadRT = rt.intersection([ rt.partial({ - ccs: rt.string, + ccs: ccsRT, }), rt.type({ - timeRange: rt.type({ - min: rt.string, - max: rt.string, - }), + timeRange: timeRangeRT, }), ]); export type PostApmInstanceRequestPayload = rt.TypeOf; export const postApmInstanceResponsePayloadRT = rt.type({ - data: rt.string, + // TODO: add payload entries }); diff --git a/x-pack/plugins/monitoring/common/http_api/apm/post_apm_instances.ts b/x-pack/plugins/monitoring/common/http_api/apm/post_apm_instances.ts index 9e5510ed7a8f9..f6b10eccbea91 100644 --- a/x-pack/plugins/monitoring/common/http_api/apm/post_apm_instances.ts +++ b/x-pack/plugins/monitoring/common/http_api/apm/post_apm_instances.ts @@ -6,25 +6,23 @@ */ import * as rt from 'io-ts'; +import { ccsRT, clusterUuidRT, timeRangeRT } from '../shared'; export const postApmInstancesRequestParamsRT = rt.type({ - clusterUuid: rt.string, + clusterUuid: clusterUuidRT, }); export const postApmInstancesRequestPayloadRT = rt.intersection([ rt.partial({ - ccs: rt.string, + ccs: ccsRT, }), rt.type({ - timeRange: rt.type({ - min: rt.string, - max: rt.string, - }), + timeRange: timeRangeRT, }), ]); export type PostApmInstancesRequestPayload = rt.TypeOf; export const postApmInstancesResponsePayloadRT = rt.type({ - data: rt.string, + // TODO: add payload entries }); diff --git a/x-pack/plugins/monitoring/common/http_api/apm/post_apm_overview.ts b/x-pack/plugins/monitoring/common/http_api/apm/post_apm_overview.ts index e7da157984b1d..951d8103249d8 100644 --- a/x-pack/plugins/monitoring/common/http_api/apm/post_apm_overview.ts +++ b/x-pack/plugins/monitoring/common/http_api/apm/post_apm_overview.ts @@ -6,25 +6,23 @@ */ import * as rt from 'io-ts'; +import { ccsRT, clusterUuidRT, timeRangeRT } from '../shared'; export const postApmOverviewRequestParamsRT = rt.type({ - clusterUuid: rt.string, + clusterUuid: clusterUuidRT, }); export const postApmOverviewRequestPayloadRT = rt.intersection([ rt.partial({ - ccs: rt.string, + ccs: ccsRT, }), rt.type({ - timeRange: rt.type({ - min: rt.string, - max: rt.string, - }), + timeRange: timeRangeRT, }), ]); export type PostApmOverviewRequestPayload = rt.TypeOf; export const postApmOverviewResponsePayloadRT = rt.type({ - data: rt.string, + // TODO: add payload entries }); diff --git a/x-pack/plugins/monitoring/common/http_api/beats/index.ts b/x-pack/plugins/monitoring/common/http_api/beats/index.ts new file mode 100644 index 0000000000000..c10908392b84b --- /dev/null +++ b/x-pack/plugins/monitoring/common/http_api/beats/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 * from './post_beats_overview'; +export * from './post_beats_listing'; +export * from './post_beat_detail'; diff --git a/x-pack/plugins/monitoring/common/http_api/beats/post_beat_detail.ts b/x-pack/plugins/monitoring/common/http_api/beats/post_beat_detail.ts new file mode 100644 index 0000000000000..6db962395a42d --- /dev/null +++ b/x-pack/plugins/monitoring/common/http_api/beats/post_beat_detail.ts @@ -0,0 +1,29 @@ +/* + * 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 rt from 'io-ts'; +import { ccsRT, clusterUuidRT, timeRangeRT } from '../shared'; + +export const postBeatDetailRequestParamsRT = rt.type({ + clusterUuid: clusterUuidRT, + beatUuid: rt.string, +}); + +export const postBeatDetailRequestPayloadRT = rt.intersection([ + rt.partial({ + ccs: ccsRT, + }), + rt.type({ + timeRange: timeRangeRT, + }), +]); + +export type PostBeatDetailRequestPayload = rt.TypeOf; + +export const postBeatDetailResponsePayloadRT = rt.type({ + // TODO: add payload entries +}); diff --git a/x-pack/plugins/monitoring/common/http_api/beats/post_beats_listing.ts b/x-pack/plugins/monitoring/common/http_api/beats/post_beats_listing.ts new file mode 100644 index 0000000000000..007982849506b --- /dev/null +++ b/x-pack/plugins/monitoring/common/http_api/beats/post_beats_listing.ts @@ -0,0 +1,28 @@ +/* + * 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 rt from 'io-ts'; +import { ccsRT, clusterUuidRT, timeRangeRT } from '../shared'; + +export const postBeatsListingRequestParamsRT = rt.type({ + clusterUuid: clusterUuidRT, +}); + +export const postBeatsListingRequestPayloadRT = rt.intersection([ + rt.partial({ + ccs: ccsRT, + }), + rt.type({ + timeRange: timeRangeRT, + }), +]); + +export type PostBeatsListingRequestPayload = rt.TypeOf; + +export const postBeatsListingResponsePayloadRT = rt.type({ + // TODO: add payload entries +}); diff --git a/x-pack/plugins/monitoring/common/http_api/beats/post_beats_overview.ts b/x-pack/plugins/monitoring/common/http_api/beats/post_beats_overview.ts new file mode 100644 index 0000000000000..b7d73dd0d2eab --- /dev/null +++ b/x-pack/plugins/monitoring/common/http_api/beats/post_beats_overview.ts @@ -0,0 +1,28 @@ +/* + * 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 rt from 'io-ts'; +import { clusterUuidRT, ccsRT, timeRangeRT } from '../shared'; + +export const postBeatsOverviewRequestParamsRT = rt.type({ + clusterUuid: clusterUuidRT, +}); + +export const postBeatsOverviewRequestPayloadRT = rt.intersection([ + rt.partial({ + ccs: ccsRT, + }), + rt.type({ + timeRange: timeRangeRT, + }), +]); + +export type PostBeatsOverviewRequestPayload = rt.TypeOf; + +export const postBeatsOverviewResponsePayloadRT = rt.type({ + // TODO: add payload entries +}); diff --git a/x-pack/plugins/monitoring/common/http_api/shared/ccs.ts b/x-pack/plugins/monitoring/common/http_api/shared/ccs.ts new file mode 100644 index 0000000000000..041b14fa531e3 --- /dev/null +++ b/x-pack/plugins/monitoring/common/http_api/shared/ccs.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. + */ + +import * as rt from 'io-ts'; + +export const ccsRT = rt.string; diff --git a/x-pack/plugins/monitoring/common/http_api/shared/cluster.ts b/x-pack/plugins/monitoring/common/http_api/shared/cluster.ts new file mode 100644 index 0000000000000..43a3c587c1db7 --- /dev/null +++ b/x-pack/plugins/monitoring/common/http_api/shared/cluster.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. + */ + +import * as rt from 'io-ts'; + +export const clusterUuidRT = rt.string; diff --git a/x-pack/plugins/monitoring/common/http_api/shared/index.ts b/x-pack/plugins/monitoring/common/http_api/shared/index.ts new file mode 100644 index 0000000000000..db7d3dce6c463 --- /dev/null +++ b/x-pack/plugins/monitoring/common/http_api/shared/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 * from './cluster'; +export * from './ccs'; +export * from './time_range'; diff --git a/x-pack/plugins/monitoring/common/http_api/shared/time_range.ts b/x-pack/plugins/monitoring/common/http_api/shared/time_range.ts new file mode 100644 index 0000000000000..edeb56d1e2ea1 --- /dev/null +++ b/x-pack/plugins/monitoring/common/http_api/shared/time_range.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 * as rt from 'io-ts'; + +export const timeRangeRT = rt.type({ + min: rt.string, + max: rt.string, +}); diff --git a/x-pack/plugins/monitoring/server/lib/details/get_metrics.ts b/x-pack/plugins/monitoring/server/lib/details/get_metrics.ts index 83db64add1c69..475d2c681596e 100644 --- a/x-pack/plugins/monitoring/server/lib/details/get_metrics.ts +++ b/x-pack/plugins/monitoring/server/lib/details/get_metrics.ts @@ -13,13 +13,20 @@ import { getTimezone } from '../get_timezone'; import { LegacyRequest } from '../../types'; import { INDEX_PATTERN_TYPES } from '../../../common/constants'; -type Metric = string | { keys: string | string[]; name: string }; +export interface NamedMetricDescriptor { + keys: string | string[]; + name: string; +} + +export type SimpleMetricDescriptor = string; + +export type MetricDescriptor = SimpleMetricDescriptor | NamedMetricDescriptor; // TODO: Switch to an options object argument here export async function getMetrics( req: LegacyRequest, moduleType: INDEX_PATTERN_TYPES, - metricSet: Metric[] = [], + metricSet: MetricDescriptor[] = [], filters: Array> = [], metricOptions: Record = {}, numOfBuckets: number = 0, @@ -42,7 +49,7 @@ export async function getMetrics( } return Promise.all( - metricSet.map((metric: Metric) => { + metricSet.map((metric: MetricDescriptor) => { // metric names match the literal metric name, but they can be supplied in groups or individually let metricNames; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/apm/metric_set_instance.ts b/x-pack/plugins/monitoring/server/routes/api/v1/apm/metric_set_instance.ts index d6fc7cbd2c076..427c0610832d3 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/apm/metric_set_instance.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/apm/metric_set_instance.ts @@ -5,7 +5,9 @@ * 2.0. */ -export const metricSet = [ +import { NamedMetricDescriptor } from '../../../../lib/details/get_metrics'; + +export const metricSet: NamedMetricDescriptor[] = [ { name: 'apm_cpu', keys: ['apm_cpu_total'], diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/apm/metric_set_overview.ts b/x-pack/plugins/monitoring/server/routes/api/v1/apm/metric_set_overview.ts index b0dccb8dd34df..c6f126ca2f6b9 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/apm/metric_set_overview.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/apm/metric_set_overview.ts @@ -5,7 +5,9 @@ * 2.0. */ -export const metricSet = [ +import { NamedMetricDescriptor } from '../../../../lib/details/get_metrics'; + +export const metricSet: NamedMetricDescriptor[] = [ { name: 'apm_cpu', keys: ['apm_cpu_total'], diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/beats/beat_detail.js b/x-pack/plugins/monitoring/server/routes/api/v1/beats/beat_detail.ts similarity index 71% rename from x-pack/plugins/monitoring/server/routes/api/v1/beats/beat_detail.js rename to x-pack/plugins/monitoring/server/routes/api/v1/beats/beat_detail.ts index 8c28dd420675a..18f6de905dbfe 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/beats/beat_detail.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/beats/beat_detail.ts @@ -5,32 +5,27 @@ * 2.0. */ -import { schema } from '@kbn/config-schema'; import { prefixIndexPatternWithCcs } from '../../../../../common/ccs_utils'; +import { INDEX_PATTERN_BEATS } from '../../../../../common/constants'; +import { + postBeatDetailRequestParamsRT, + postBeatDetailRequestPayloadRT, + postBeatDetailResponsePayloadRT, +} from '../../../../../common/http_api/beats'; import { getBeatSummary } from '../../../../lib/beats'; +import { createValidationFunction } from '../../../../lib/create_route_validation_function'; import { getMetrics } from '../../../../lib/details/get_metrics'; import { handleError } from '../../../../lib/errors'; +import { MonitoringCore } from '../../../../types'; import { metricSet } from './metric_set_detail'; -import { INDEX_PATTERN_BEATS } from '../../../../../common/constants'; -export function beatsDetailRoute(server) { +export function beatsDetailRoute(server: MonitoringCore) { server.route({ - method: 'POST', + method: 'post', path: '/api/monitoring/v1/clusters/{clusterUuid}/beats/beat/{beatUuid}', - config: { - validate: { - params: schema.object({ - clusterUuid: schema.string(), - beatUuid: schema.string(), - }), - body: schema.object({ - ccs: schema.maybe(schema.string()), - timeRange: schema.object({ - min: schema.string(), - max: schema.string(), - }), - }), - }, + validate: { + params: createValidationFunction(postBeatDetailRequestParamsRT), + body: createValidationFunction(postBeatDetailRequestPayloadRT), }, async handler(req) { const clusterUuid = req.params.clusterUuid; @@ -52,10 +47,10 @@ export function beatsDetailRoute(server) { getMetrics(req, 'beats', metricSet, [{ term: { 'beats_stats.beat.uuid': beatUuid } }]), ]); - return { + return postBeatDetailResponsePayloadRT.encode({ summary, metrics, - }; + }); } catch (err) { throw handleError(err, req); } diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/beats/beats.js b/x-pack/plugins/monitoring/server/routes/api/v1/beats/beats.ts similarity index 61% rename from x-pack/plugins/monitoring/server/routes/api/v1/beats/beats.js rename to x-pack/plugins/monitoring/server/routes/api/v1/beats/beats.ts index 83403eaa355bd..8bf73bb115f74 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/beats/beats.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/beats/beats.ts @@ -5,29 +5,25 @@ * 2.0. */ -import { schema } from '@kbn/config-schema'; import { prefixIndexPatternWithCcs } from '../../../../../common/ccs_utils'; -import { getStats, getBeats } from '../../../../lib/beats'; -import { handleError } from '../../../../lib/errors'; import { INDEX_PATTERN_BEATS } from '../../../../../common/constants'; +import { + postBeatsListingRequestParamsRT, + postBeatsListingRequestPayloadRT, + postBeatsListingResponsePayloadRT, +} from '../../../../../common/http_api/beats'; +import { getBeats, getStats } from '../../../../lib/beats'; +import { createValidationFunction } from '../../../../lib/create_route_validation_function'; +import { handleError } from '../../../../lib/errors'; +import { MonitoringCore } from '../../../../types'; -export function beatsListingRoute(server) { +export function beatsListingRoute(server: MonitoringCore) { server.route({ - method: 'POST', + method: 'post', path: '/api/monitoring/v1/clusters/{clusterUuid}/beats/beats', - config: { - validate: { - params: schema.object({ - clusterUuid: schema.string(), - }), - body: schema.object({ - ccs: schema.maybe(schema.string()), - timeRange: schema.object({ - min: schema.string(), - max: schema.string(), - }), - }), - }, + validate: { + params: createValidationFunction(postBeatsListingRequestParamsRT), + body: createValidationFunction(postBeatsListingRequestPayloadRT), }, async handler(req) { const config = server.config; @@ -41,10 +37,10 @@ export function beatsListingRoute(server) { getBeats(req, beatsIndexPattern, clusterUuid), ]); - return { + return postBeatsListingResponsePayloadRT.encode({ stats, listing, - }; + }); } catch (err) { throw handleError(err, req); } diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/beats/index.js b/x-pack/plugins/monitoring/server/routes/api/v1/beats/index.ts similarity index 100% rename from x-pack/plugins/monitoring/server/routes/api/v1/beats/index.js rename to x-pack/plugins/monitoring/server/routes/api/v1/beats/index.ts diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/beats/metric_set_detail.js b/x-pack/plugins/monitoring/server/routes/api/v1/beats/metric_set_detail.ts similarity index 91% rename from x-pack/plugins/monitoring/server/routes/api/v1/beats/metric_set_detail.js rename to x-pack/plugins/monitoring/server/routes/api/v1/beats/metric_set_detail.ts index c6c1e7598658a..a3cc7f409a7f7 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/beats/metric_set_detail.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/beats/metric_set_detail.ts @@ -5,7 +5,9 @@ * 2.0. */ -export const metricSet = [ +import { MetricDescriptor } from '../../../../lib/details/get_metrics'; + +export const metricSet: MetricDescriptor[] = [ { keys: [ 'beat_pipeline_events_total_rate', diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/beats/metric_set_overview.js b/x-pack/plugins/monitoring/server/routes/api/v1/beats/metric_set_overview.ts similarity index 89% rename from x-pack/plugins/monitoring/server/routes/api/v1/beats/metric_set_overview.js rename to x-pack/plugins/monitoring/server/routes/api/v1/beats/metric_set_overview.ts index c5f0c73dd082d..64f066799c193 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/beats/metric_set_overview.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/beats/metric_set_overview.ts @@ -5,7 +5,9 @@ * 2.0. */ -export const metricSet = [ +import { MetricDescriptor } from '../../../../lib/details/get_metrics'; + +export const metricSet: MetricDescriptor[] = [ { keys: [ 'beat_cluster_pipeline_events_total_rate', diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/beats/overview.js b/x-pack/plugins/monitoring/server/routes/api/v1/beats/overview.ts similarity index 68% rename from x-pack/plugins/monitoring/server/routes/api/v1/beats/overview.js rename to x-pack/plugins/monitoring/server/routes/api/v1/beats/overview.ts index 9cd9bcd9787e2..6497c5568a1a8 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/beats/overview.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/beats/overview.ts @@ -5,31 +5,27 @@ * 2.0. */ -import { schema } from '@kbn/config-schema'; import { prefixIndexPatternWithCcs } from '../../../../../common/ccs_utils'; -import { getMetrics } from '../../../../lib/details/get_metrics'; +import { INDEX_PATTERN_BEATS } from '../../../../../common/constants'; +import { + postBeatsOverviewRequestParamsRT, + postBeatsOverviewRequestPayloadRT, + postBeatsOverviewResponsePayloadRT, +} from '../../../../../common/http_api/beats'; import { getLatestStats, getStats } from '../../../../lib/beats'; +import { createValidationFunction } from '../../../../lib/create_route_validation_function'; +import { getMetrics } from '../../../../lib/details/get_metrics'; import { handleError } from '../../../../lib/errors'; +import { MonitoringCore } from '../../../../types'; import { metricSet } from './metric_set_overview'; -import { INDEX_PATTERN_BEATS } from '../../../../../common/constants'; -export function beatsOverviewRoute(server) { +export function beatsOverviewRoute(server: MonitoringCore) { server.route({ - method: 'POST', + method: 'post', path: '/api/monitoring/v1/clusters/{clusterUuid}/beats', - config: { - validate: { - params: schema.object({ - clusterUuid: schema.string(), - }), - body: schema.object({ - ccs: schema.maybe(schema.string()), - timeRange: schema.object({ - min: schema.string(), - max: schema.string(), - }), - }), - }, + validate: { + params: createValidationFunction(postBeatsOverviewRequestParamsRT), + body: createValidationFunction(postBeatsOverviewRequestPayloadRT), }, async handler(req) { const config = server.config; @@ -44,11 +40,11 @@ export function beatsOverviewRoute(server) { getMetrics(req, 'beats', metricSet), ]); - return { + return postBeatsOverviewResponsePayloadRT.encode({ ...latest, stats, metrics, - }; + }); } catch (err) { throw handleError(err, req); } From dad4963bf0cadce113c51de6a066337fdcc51006 Mon Sep 17 00:00:00 2001 From: Dominique Clarke Date: Tue, 26 Apr 2022 08:42:50 -0400 Subject: [PATCH 11/29] Update synthetics.sh (#130940) --- .buildkite/scripts/steps/functional/synthetics.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.buildkite/scripts/steps/functional/synthetics.sh b/.buildkite/scripts/steps/functional/synthetics.sh index ecb2922f89c8d..b72e85c7b6a4c 100644 --- a/.buildkite/scripts/steps/functional/synthetics.sh +++ b/.buildkite/scripts/steps/functional/synthetics.sh @@ -14,4 +14,4 @@ echo "--- Uptime @elastic/synthetics Tests" cd "$XPACK_DIR" checks-reporter-with-killswitch "Uptime @elastic/synthetics Tests" \ - node plugins/uptime/scripts/e2e.js --kibana-install-dir "$KIBANA_BUILD_LOCATION" --grep "MonitorManagement-monitor*" \ No newline at end of file + node plugins/synthetics/scripts/e2e.js --kibana-install-dir "$KIBANA_BUILD_LOCATION" --grep "MonitorManagement-monitor*" From 6ad418b275da3f57b26c0b3e62781a5bc7ad0f9a Mon Sep 17 00:00:00 2001 From: Patrick Mueller Date: Tue, 26 Apr 2022 10:05:16 -0400 Subject: [PATCH 12/29] [ResponseOps][actions] add config for allow-listing email address domains (#129001) resolves https://github.com/elastic/kibana/issues/126944 Adds a new configuration setting for the actions plugin, xpack.actions.email.domain_allowlist, which is an array of domain name strings which are allowed to be sent emails by the email connector. --- .../connectors/action-types/email.asciidoc | 2 + docs/settings/alert-action-settings.asciidoc | 5 + package.json | 1 + packages/kbn-optimizer/limits.yml | 1 + .../resources/base/bin/kibana-docker | 1 + .../test_suites/core_plugins/rendering.ts | 1 + x-pack/plugins/actions/common/index.ts | 3 + .../actions/common/mustache_template.test.ts | 33 ++ .../actions/common/mustache_template.ts | 18 ++ .../actions/common/servicenow_config.ts | 56 ++++ x-pack/plugins/actions/common/types.ts | 15 + .../common/validate_email_addresses.test.ts | 284 ++++++++++++++++++ .../common/validate_email_addresses.ts | 114 +++++++ x-pack/plugins/actions/kibana.json | 3 +- x-pack/plugins/actions/public/index.ts | 15 + x-pack/plugins/actions/public/plugin.test.ts | 65 ++++ x-pack/plugins/actions/public/plugin.ts | 44 +++ .../actions/server/actions_config.mock.ts | 1 + .../actions/server/actions_config.test.ts | 44 +++ .../plugins/actions/server/actions_config.ts | 26 +- .../server/builtin_action_types/email.test.ts | 134 ++++++--- .../server/builtin_action_types/email.ts | 74 +++-- .../lib/send_email.test.ts | 264 +--------------- .../builtin_action_types/servicenow/types.ts | 14 +- .../server/builtin_action_types/teams.test.ts | 28 +- .../builtin_action_types/webhook.test.ts | 28 +- x-pack/plugins/actions/server/config.test.ts | 19 ++ x-pack/plugins/actions/server/config.ts | 5 + x-pack/plugins/actions/server/index.ts | 3 + x-pack/plugins/actions/server/plugin.ts | 1 + x-pack/plugins/actions/server/routes/index.ts | 2 + x-pack/plugins/actions/tsconfig.json | 3 +- .../plugins/triggers_actions_ui/kibana.json | 6 +- .../builtin_action_types/email/email.test.tsx | 54 +++- .../builtin_action_types/email/email.tsx | 82 ++++- .../email/translations.ts | 17 ++ .../es_index/es_index.test.tsx | 3 +- .../components/builtin_action_types/index.ts | 12 +- .../builtin_action_types/jira/jira.test.tsx | 3 +- .../pagerduty/pagerduty.test.tsx | 3 +- .../resilient/resilient.test.tsx | 3 +- .../server_log/server_log.test.tsx | 3 +- .../builtin_action_types/servicenow/api.ts | 4 +- .../servicenow/servicenow.test.tsx | 3 +- .../servicenow/servicenow_connectors.tsx | 3 +- .../servicenow/update_connector.tsx | 3 +- .../builtin_action_types/slack/slack.test.tsx | 3 +- .../swimlane/swimlane.test.tsx | 3 +- .../builtin_action_types/teams/teams.test.tsx | 3 +- .../webhook/webhook.test.tsx | 3 +- .../xmatters/xmatters.test.tsx | 3 +- .../triggers_actions_ui/public/mocks.ts | 9 +- .../triggers_actions_ui/public/plugin.ts | 5 + .../alerting_api_integration/common/config.ts | 7 + .../spaces_only/config.ts | 3 + .../actions/builtin_action_types/email.ts | 175 +++++++++++ .../spaces_only/tests/actions/index.ts | 1 + yarn.lock | 5 + 58 files changed, 1299 insertions(+), 427 deletions(-) create mode 100644 x-pack/plugins/actions/common/mustache_template.test.ts create mode 100644 x-pack/plugins/actions/common/mustache_template.ts create mode 100644 x-pack/plugins/actions/common/servicenow_config.ts create mode 100644 x-pack/plugins/actions/common/validate_email_addresses.test.ts create mode 100644 x-pack/plugins/actions/common/validate_email_addresses.ts create mode 100644 x-pack/plugins/actions/public/index.ts create mode 100644 x-pack/plugins/actions/public/plugin.test.ts create mode 100644 x-pack/plugins/actions/public/plugin.ts create mode 100644 x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/email.ts diff --git a/docs/management/connectors/action-types/email.asciidoc b/docs/management/connectors/action-types/email.asciidoc index c080c412f0f6b..af408fd36a23c 100644 --- a/docs/management/connectors/action-types/email.asciidoc +++ b/docs/management/connectors/action-types/email.asciidoc @@ -9,6 +9,8 @@ The email connector uses the SMTP protocol to send mail messages, using an integ NOTE: For emails to have a footer with a link back to {kib}, set the <> configuration setting. +NOTE: When the <> configuration setting is used, the email addresses used for all of the Sender (from), To, CC, and BCC properties must have email domains specified in the configuration setting. + [float] [[email-connector-configuration]] ==== Connector configuration diff --git a/docs/settings/alert-action-settings.asciidoc b/docs/settings/alert-action-settings.asciidoc index 384c4b696521d..7579ec207c835 100644 --- a/docs/settings/alert-action-settings.asciidoc +++ b/docs/settings/alert-action-settings.asciidoc @@ -125,6 +125,11 @@ The contents of a PEM-encoded certificate file, or multiple files appended into a single string. This configuration can be used for environments where the files cannot be made available. +[[action-config-email-domain-allowlist]] `xpack.actions.email.domain_allowlist` {ess-icon}:: +A list of allowed email domains which can be used with the email connector. When this setting is not used, all email domains are allowed. When this setting is used, if any email is attempted to be sent that includes an addressee with an email domain that is not in the allowlist, or the from address domain is not in the allowlist, the run of the connector will fail with a message indicating the emails not allowed. + +WARNING: This feature is available in {kib} 7.17.4 and 8.3.0 onwards but is not supported in {kib} 8.0, 8.1 or 8.2. As such this settings should be removed before upgrading from 7.17 to 8.0, 8.1 or 8.2. It is possible to configure the settings in 7.17.4 and then upgrade to 8.3.0 directly. + `xpack.actions.enabledActionTypes` {ess-icon}:: A list of action types that are enabled. It defaults to `[*]`, enabling all types. The names for built-in {kib} action types are prefixed with a `.` and include: `.email`, `.index`, `.jira`, `.pagerduty`, `.resilient`, `.server-log`, `.servicenow`, .`servicenow-itom`, `.servicenow-sir`, `.slack`, `.swimlane`, `.teams`, `.xmatters`, and `.webhook`. An empty list `[]` will disable all action types. + diff --git a/package.json b/package.json index 8f30166439b8b..4c7db193368a6 100644 --- a/package.json +++ b/package.json @@ -250,6 +250,7 @@ "deepmerge": "^4.2.2", "del": "^5.1.0", "elastic-apm-node": "^3.31.0", + "email-addresses": "^5.0.0", "execa": "^4.0.2", "exit-hook": "^2.2.0", "expiry-js": "0.1.7", diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 98089f1f1c5fb..dc16c080306ad 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -1,5 +1,6 @@ pageLoadAssetSize: advancedSettings: 27596 + actions: 20000 alerting: 106936 apm: 64385 canvas: 1066647 diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker index 6ec12aab3c002..7b569f8d02068 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker @@ -188,6 +188,7 @@ kibana_vars=( vis_type_vega.enableExternalUrls xpack.actions.allowedHosts xpack.actions.customHostSettings + xpack.actions.email.domain_allowlist xpack.actions.enabledActionTypes xpack.actions.maxResponseContentLength xpack.actions.preconfigured diff --git a/test/plugin_functional/test_suites/core_plugins/rendering.ts b/test/plugin_functional/test_suites/core_plugins/rendering.ts index 5f124adc92960..b7adbfa17301c 100644 --- a/test/plugin_functional/test_suites/core_plugins/rendering.ts +++ b/test/plugin_functional/test_suites/core_plugins/rendering.ts @@ -136,6 +136,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) { 'usageCollection.uiCounters.debug (boolean)', 'usageCollection.uiCounters.enabled (boolean)', 'vis_type_vega.enableExternalUrls (boolean)', + 'xpack.actions.email.domain_allowlist (array)', 'xpack.apm.profilingEnabled (boolean)', 'xpack.apm.serviceMapEnabled (boolean)', 'xpack.apm.ui.enabled (boolean)', diff --git a/x-pack/plugins/actions/common/index.ts b/x-pack/plugins/actions/common/index.ts index 1e51adf3e9d09..1b26f04e72265 100644 --- a/x-pack/plugins/actions/common/index.ts +++ b/x-pack/plugins/actions/common/index.ts @@ -11,6 +11,9 @@ export * from './types'; export * from './alert_history_schema'; export * from './rewrite_request_case'; +export * from './mustache_template'; +export * from './validate_email_addresses'; +export * from './servicenow_config'; export const BASE_ACTION_API_PATH = '/api/actions'; export const INTERNAL_BASE_ACTION_API_PATH = '/internal/actions'; diff --git a/x-pack/plugins/actions/common/mustache_template.test.ts b/x-pack/plugins/actions/common/mustache_template.test.ts new file mode 100644 index 0000000000000..2d35c00a8efa1 --- /dev/null +++ b/x-pack/plugins/actions/common/mustache_template.test.ts @@ -0,0 +1,33 @@ +/* + * 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 { hasMustacheTemplate, withoutMustacheTemplate } from './mustache_template'; + +const nonMustacheEmails = ['', 'zero@a.b.c', '}}{{']; +const mustacheEmails = ['{{}}', '"bob" {{}}@elastic.co', 'sneaky{{\n}}pete']; + +describe('mustache_template', () => { + it('hasMustacheTemplate', () => { + for (const email of nonMustacheEmails) { + expect(hasMustacheTemplate(email)).toBe(false); + } + for (const email of mustacheEmails) { + expect(hasMustacheTemplate(email)).toBe(true); + } + }); + + it('withoutMustacheTemplate', () => { + let result = withoutMustacheTemplate(nonMustacheEmails); + expect(result).toEqual(nonMustacheEmails); + + result = withoutMustacheTemplate(mustacheEmails); + expect(result).toEqual([]); + + result = withoutMustacheTemplate(mustacheEmails.concat(nonMustacheEmails)); + expect(result).toEqual(nonMustacheEmails); + }); +}); diff --git a/x-pack/plugins/actions/common/mustache_template.ts b/x-pack/plugins/actions/common/mustache_template.ts new file mode 100644 index 0000000000000..45e4b6d86ce66 --- /dev/null +++ b/x-pack/plugins/actions/common/mustache_template.ts @@ -0,0 +1,18 @@ +/* + * 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 MustacheInEmailRegExp = /\{\{((.|\n)*)\}\}/; + +/** does the string contain `{{.*}}`? */ +export function hasMustacheTemplate(string: string): boolean { + return !!string.match(MustacheInEmailRegExp); +} + +/** filter strings that do not contain `{{.*}}` */ +export function withoutMustacheTemplate(strings: string[]): string[] { + return strings.filter((string) => !hasMustacheTemplate(string)); +} diff --git a/x-pack/plugins/actions/common/servicenow_config.ts b/x-pack/plugins/actions/common/servicenow_config.ts new file mode 100644 index 0000000000000..994f6cb33524f --- /dev/null +++ b/x-pack/plugins/actions/common/servicenow_config.ts @@ -0,0 +1,56 @@ +/* + * 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 serviceNowITSMTable = 'incident'; +export const serviceNowSIRTable = 'sn_si_incident'; + +export const ServiceNowITSMActionTypeId = '.servicenow'; +export const ServiceNowSIRActionTypeId = '.servicenow-sir'; +export const ServiceNowITOMActionTypeId = '.servicenow-itom'; + +const SN_ITSM_APP_ID = '7148dbc91bf1f450ced060a7234bcb88'; +const SN_SIR_APP_ID = '2f0746801baeb01019ae54e4604bcb0f'; + +export interface SNProductsConfigValue { + table: string; + appScope: string; + useImportAPI: boolean; + importSetTable: string; + commentFieldKey: string; + appId?: string; +} + +export type SNProductsConfig = Record; + +export const snExternalServiceConfig: SNProductsConfig = { + '.servicenow': { + importSetTable: 'x_elas2_inc_int_elastic_incident', + appScope: 'x_elas2_inc_int', + table: 'incident', + useImportAPI: true, + commentFieldKey: 'work_notes', + appId: SN_ITSM_APP_ID, + }, + '.servicenow-sir': { + importSetTable: 'x_elas2_sir_int_elastic_si_incident', + appScope: 'x_elas2_sir_int', + table: 'sn_si_incident', + useImportAPI: true, + commentFieldKey: 'work_notes', + appId: SN_SIR_APP_ID, + }, + '.servicenow-itom': { + importSetTable: 'x_elas2_inc_int_elastic_incident', + appScope: 'x_elas2_inc_int', + table: 'em_event', + useImportAPI: false, + commentFieldKey: 'work_notes', + }, +}; + +export const FIELD_PREFIX = 'u_'; +export const DEFAULT_ALERTS_GROUPING_KEY = '{{rule.id}}:{{alert.id}}'; diff --git a/x-pack/plugins/actions/common/types.ts b/x-pack/plugins/actions/common/types.ts index 20de64a7cc3b5..751b403780080 100644 --- a/x-pack/plugins/actions/common/types.ts +++ b/x-pack/plugins/actions/common/types.ts @@ -16,6 +16,17 @@ export interface ActionType { minimumLicenseRequired: LicenseType; } +export enum InvalidEmailReason { + invalid = 'invalid', + notAllowed = 'notAllowed', +} + +export interface ValidatedEmail { + address: string; + valid: boolean; + reason?: InvalidEmailReason; +} + export interface ActionResult { id: string; actionTypeId: string; @@ -49,3 +60,7 @@ export function isActionTypeExecutorResult( ActionTypeExecutorResultStatusValues.includes(unsafeResult?.status) ); } + +export interface ActionsPublicConfigType { + allowedEmailDomains: string[]; +} diff --git a/x-pack/plugins/actions/common/validate_email_addresses.test.ts b/x-pack/plugins/actions/common/validate_email_addresses.test.ts new file mode 100644 index 0000000000000..af76cdef72966 --- /dev/null +++ b/x-pack/plugins/actions/common/validate_email_addresses.test.ts @@ -0,0 +1,284 @@ +/* + * 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 { ValidatedEmail, InvalidEmailReason } from './types'; +import { + validateEmailAddressesAsAlwaysValid, + validateEmailAddresses, + invalidEmailsAsMessage, +} from './validate_email_addresses'; + +const AllowedDomains = ['elastic.co', 'dev.elastic.co', 'found.no']; +const Emails = [ + 'bob@elastic.co', + '"Dr Tom" ', + 'jim@dev.elastic.co', + 'rex@found.no', + 'sal@alerting.dev.elastic.co', + 'nancy@example.com', + '"Dr RFC 5322" ', + 'totally invalid', + '{{sneaky}}', +]; + +describe('validate_email_address', () => { + test('validateEmailAddressesAsAlwaysValid()', () => { + const emails = ['bob@example.com', 'invalid-email', '']; + const validatedEmails = validateEmailAddressesAsAlwaysValid(emails); + + expect(validatedEmails).toMatchInlineSnapshot(` + Array [ + Object { + "address": "bob@example.com", + "valid": true, + }, + Object { + "address": "invalid-email", + "valid": true, + }, + Object { + "address": "", + "valid": true, + }, + ] + `); + }); + + describe('validateEmailAddresses()', () => { + test('with configured allowlist and no mustache filtering', () => { + const result = validateEmailAddresses(AllowedDomains, Emails); + expect(result).toMatchInlineSnapshot(` + Array [ + Object { + "address": "bob@elastic.co", + "valid": true, + }, + Object { + "address": "\\"Dr Tom\\" ", + "valid": true, + }, + Object { + "address": "jim@dev.elastic.co", + "valid": true, + }, + Object { + "address": "rex@found.no", + "valid": true, + }, + Object { + "address": "sal@alerting.dev.elastic.co", + "reason": "notAllowed", + "valid": false, + }, + Object { + "address": "nancy@example.com", + "reason": "notAllowed", + "valid": false, + }, + Object { + "address": "\\"Dr RFC 5322\\" ", + "reason": "notAllowed", + "valid": false, + }, + Object { + "address": "totally invalid", + "reason": "invalid", + "valid": false, + }, + Object { + "address": "{{sneaky}}", + "reason": "invalid", + "valid": false, + }, + ] + `); + }); + + test('with configured allowlist and mustache filtering', () => { + const result = validateEmailAddresses(AllowedDomains, Emails, { + treatMustacheTemplatesAsValid: true, + }); + expect(result).toMatchInlineSnapshot(` + Array [ + Object { + "address": "bob@elastic.co", + "valid": true, + }, + Object { + "address": "\\"Dr Tom\\" ", + "valid": true, + }, + Object { + "address": "jim@dev.elastic.co", + "valid": true, + }, + Object { + "address": "rex@found.no", + "valid": true, + }, + Object { + "address": "sal@alerting.dev.elastic.co", + "reason": "notAllowed", + "valid": false, + }, + Object { + "address": "nancy@example.com", + "reason": "notAllowed", + "valid": false, + }, + Object { + "address": "\\"Dr RFC 5322\\" ", + "reason": "notAllowed", + "valid": false, + }, + Object { + "address": "totally invalid", + "reason": "invalid", + "valid": false, + }, + Object { + "address": "{{sneaky}}", + "valid": true, + }, + ] + `); + }); + + test('with no configured allowlist and no mustache filtering', () => { + const result = validateEmailAddresses(null, Emails); + expect(result).toMatchInlineSnapshot(` + Array [ + Object { + "address": "bob@elastic.co", + "valid": true, + }, + Object { + "address": "\\"Dr Tom\\" ", + "valid": true, + }, + Object { + "address": "jim@dev.elastic.co", + "valid": true, + }, + Object { + "address": "rex@found.no", + "valid": true, + }, + Object { + "address": "sal@alerting.dev.elastic.co", + "valid": true, + }, + Object { + "address": "nancy@example.com", + "valid": true, + }, + Object { + "address": "\\"Dr RFC 5322\\" ", + "valid": true, + }, + Object { + "address": "totally invalid", + "valid": true, + }, + Object { + "address": "{{sneaky}}", + "valid": true, + }, + ] + `); + }); + + test('with no configured allowlist and mustache filtering', () => { + const result = validateEmailAddresses(null, Emails, { treatMustacheTemplatesAsValid: true }); + expect(result).toMatchInlineSnapshot(` + Array [ + Object { + "address": "bob@elastic.co", + "valid": true, + }, + Object { + "address": "\\"Dr Tom\\" ", + "valid": true, + }, + Object { + "address": "jim@dev.elastic.co", + "valid": true, + }, + Object { + "address": "rex@found.no", + "valid": true, + }, + Object { + "address": "sal@alerting.dev.elastic.co", + "valid": true, + }, + Object { + "address": "nancy@example.com", + "valid": true, + }, + Object { + "address": "\\"Dr RFC 5322\\" ", + "valid": true, + }, + Object { + "address": "totally invalid", + "valid": true, + }, + Object { + "address": "{{sneaky}}", + "valid": true, + }, + ] + `); + }); + }); + + const entriesGood: ValidatedEmail[] = [ + { address: 'a', valid: true }, + { address: 'b', valid: true }, + ]; + + const entriesInvalid: ValidatedEmail[] = [ + { address: 'c', valid: false, reason: InvalidEmailReason.invalid }, + { address: 'd', valid: false, reason: InvalidEmailReason.invalid }, + ]; + + const entriesNotAllowed: ValidatedEmail[] = [ + { address: 'e', valid: false, reason: InvalidEmailReason.notAllowed }, + { address: 'f', valid: false, reason: InvalidEmailReason.notAllowed }, + ]; + + describe('invalidEmailsAsMessage()', () => { + test('with all valid entries', () => { + expect(invalidEmailsAsMessage(entriesGood)).toMatchInlineSnapshot(`undefined`); + expect(invalidEmailsAsMessage([entriesGood[0]])).toMatchInlineSnapshot(`undefined`); + }); + + test('with some invalid entries', () => { + let entries = entriesGood.concat(entriesInvalid); + expect(invalidEmailsAsMessage(entries)).toMatchInlineSnapshot(`"not valid emails: c, d"`); + + entries = entriesGood.concat(entriesInvalid[0]); + expect(invalidEmailsAsMessage(entries)).toMatchInlineSnapshot(`"not valid emails: c"`); + }); + + test('with some not allowed entries', () => { + let entries = entriesGood.concat(entriesNotAllowed); + expect(invalidEmailsAsMessage(entries)).toMatchInlineSnapshot(`"not allowed emails: e, f"`); + + entries = entriesGood.concat(entriesNotAllowed[0]); + expect(invalidEmailsAsMessage(entries)).toMatchInlineSnapshot(`"not allowed emails: e"`); + }); + + test('with some invalid and not allowed entries', () => { + const entries = entriesGood.concat(entriesInvalid).concat(entriesNotAllowed); + expect(invalidEmailsAsMessage(entries)).toMatchInlineSnapshot( + `"not valid emails: c, d; not allowed emails: e, f"` + ); + }); + }); +}); diff --git a/x-pack/plugins/actions/common/validate_email_addresses.ts b/x-pack/plugins/actions/common/validate_email_addresses.ts new file mode 100644 index 0000000000000..9a1d6f0e1405a --- /dev/null +++ b/x-pack/plugins/actions/common/validate_email_addresses.ts @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { parseAddressList } from 'email-addresses'; +import { ValidatedEmail, InvalidEmailReason } from './types'; +import { hasMustacheTemplate } from './mustache_template'; + +/** Options that can be used when validating email addresses */ +export interface ValidateEmailAddressesOptions { + /** treat any address which contains a mustache template as valid */ + treatMustacheTemplatesAsValid?: boolean; +} + +// this can be useful for cases where a plugin needs this function, +// but the actions plugin may not be available. This could be used +// as a stub for the real implementation. +export function validateEmailAddressesAsAlwaysValid(addresses: string[]): ValidatedEmail[] { + return addresses.map((address) => ({ address, valid: true })); +} + +export function validateEmailAddresses( + allowedDomains: string[] | null, + addresses: string[], + options: ValidateEmailAddressesOptions = {} +): ValidatedEmail[] { + // note: this is the legacy default, which would in theory allow + // mustache strings, so options.allowMustache is ignored in this + // case - everything is valid! + if (allowedDomains == null) { + return validateEmailAddressesAsAlwaysValid(addresses); + } + + return addresses.map((address) => validateEmailAddress(allowedDomains, address, options)); +} + +export function invalidEmailsAsMessage(validatedEmails: ValidatedEmail[]): string | undefined { + const invalid = validatedEmails.filter( + (validated) => !validated.valid && validated.reason === InvalidEmailReason.invalid + ); + const notAllowed = validatedEmails.filter( + (validated) => !validated.valid && validated.reason === InvalidEmailReason.notAllowed + ); + + const messages: string[] = []; + if (invalid.length !== 0) { + messages.push(`not valid emails: ${addressesFromValidatedEmails(invalid).join(', ')}`); + } + if (notAllowed.length !== 0) { + messages.push(`not allowed emails: ${addressesFromValidatedEmails(notAllowed).join(', ')}`); + } + + if (messages.length === 0) return; + + return messages.join('; '); +} + +// in case the npm email-addresses returns unexpected things ... +function validateEmailAddress( + allowedDomains: string[], + address: string, + options: ValidateEmailAddressesOptions +): ValidatedEmail { + // The reason we bypass the validation in this case, is that email addresses + // used in an alerting action could contain mustache templates which render + // as the actual values. So we can't really validate them. Fear not! + // We always do a final validation in the executor where we do NOT + // have this flag on. + if (options.treatMustacheTemplatesAsValid && hasMustacheTemplate(address)) { + return { address, valid: true }; + } + + try { + return validateEmailAddress_(allowedDomains, address); + } catch (err) { + return { address, valid: false, reason: InvalidEmailReason.invalid }; + } +} + +function validateEmailAddress_(allowedDomains: string[], address: string): ValidatedEmail { + const emailAddresses = parseAddressList(address); + if (emailAddresses == null) { + return { address, valid: false, reason: InvalidEmailReason.invalid }; + } + + const allowedDomainsSet = new Set(allowedDomains); + + for (const emailAddress of emailAddresses) { + let domains: string[] = []; + + if (emailAddress.type === 'group') { + domains = emailAddress.addresses.map((groupAddress) => groupAddress.domain); + } else if (emailAddress.type === 'mailbox') { + domains = [emailAddress.domain]; + } else { + return { address, valid: false, reason: InvalidEmailReason.invalid }; + } + + for (const domain of domains) { + if (!allowedDomainsSet.has(domain)) { + return { address, valid: false, reason: InvalidEmailReason.notAllowed }; + } + } + } + + return { address, valid: true }; +} + +function addressesFromValidatedEmails(validatedEmails: ValidatedEmail[]) { + return validatedEmails.map((validatedEmail) => validatedEmail.address); +} diff --git a/x-pack/plugins/actions/kibana.json b/x-pack/plugins/actions/kibana.json index 4970d2b0870c8..ad5ca87949848 100644 --- a/x-pack/plugins/actions/kibana.json +++ b/x-pack/plugins/actions/kibana.json @@ -10,5 +10,6 @@ "configPath": ["xpack", "actions"], "requiredPlugins": ["licensing", "taskManager", "encryptedSavedObjects", "eventLog", "features"], "optionalPlugins": ["usageCollection", "spaces", "security", "monitoringCollection"], - "ui": false + "extraPublicDirs": ["common"], + "ui": true } diff --git a/x-pack/plugins/actions/public/index.ts b/x-pack/plugins/actions/public/index.ts new file mode 100644 index 0000000000000..31d4d08b0dbe2 --- /dev/null +++ b/x-pack/plugins/actions/public/index.ts @@ -0,0 +1,15 @@ +/* + * 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 { PluginInitializerContext } from '@kbn/core/public'; +import { Plugin, ActionsPublicPluginSetup } from './plugin'; + +export type { ActionsPublicPluginSetup }; +export { Plugin }; +export function plugin(context: PluginInitializerContext) { + return new Plugin(context); +} diff --git a/x-pack/plugins/actions/public/plugin.test.ts b/x-pack/plugins/actions/public/plugin.test.ts new file mode 100644 index 0000000000000..903fe917f32fa --- /dev/null +++ b/x-pack/plugins/actions/public/plugin.test.ts @@ -0,0 +1,65 @@ +/* + * 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 { coreMock } from '@kbn/core/public/mocks'; +import { Plugin } from './plugin'; + +describe('Actions Plugin', () => { + describe('setup()', () => { + const emails = ['bob@elastic.co', 'jim@somewhere.org', 'not an email']; + + it('should allow all emails when not using email allowlist config', async () => { + const context = coreMock.createPluginInitializerContext({}); + const plugin = new Plugin(context); + const pluginSetup = plugin.setup(); + const validated = pluginSetup.validateEmailAddresses(emails); + expect(validated).toMatchInlineSnapshot(` + Array [ + Object { + "address": "bob@elastic.co", + "valid": true, + }, + Object { + "address": "jim@somewhere.org", + "valid": true, + }, + Object { + "address": "not an email", + "valid": true, + }, + ] + `); + }); + + it('should validate correctly when using email allowlist config', async () => { + const context = coreMock.createPluginInitializerContext({ + email: { domain_allowlist: ['elastic.co'] }, + }); + const plugin = new Plugin(context); + const pluginSetup = plugin.setup(); + const validated = pluginSetup.validateEmailAddresses(emails); + expect(validated).toMatchInlineSnapshot(` + Array [ + Object { + "address": "bob@elastic.co", + "valid": true, + }, + Object { + "address": "jim@somewhere.org", + "reason": "notAllowed", + "valid": false, + }, + Object { + "address": "not an email", + "reason": "invalid", + "valid": false, + }, + ] + `); + }); + }); +}); diff --git a/x-pack/plugins/actions/public/plugin.ts b/x-pack/plugins/actions/public/plugin.ts new file mode 100644 index 0000000000000..7a64d1796fbec --- /dev/null +++ b/x-pack/plugins/actions/public/plugin.ts @@ -0,0 +1,44 @@ +/* + * 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 { Plugin as CorePlugin, PluginInitializerContext } from '@kbn/core/public'; +import { + ValidatedEmail, + validateEmailAddresses as validateEmails, + ValidateEmailAddressesOptions, +} from '../common'; + +export interface ActionsPublicPluginSetup { + validateEmailAddresses( + emails: string[], + options?: ValidateEmailAddressesOptions + ): ValidatedEmail[]; +} + +export interface Config { + email: { + domain_allowlist: string[]; + }; +} + +export class Plugin implements CorePlugin { + private readonly allowedEmailDomains: string[] | null = null; + + constructor(ctx: PluginInitializerContext) { + const config = ctx.config.get(); + this.allowedEmailDomains = config.email?.domain_allowlist || null; + } + + public setup(): ActionsPublicPluginSetup { + return { + validateEmailAddresses: (emails: string[], options: ValidateEmailAddressesOptions) => + validateEmails(this.allowedEmailDomains, emails, options), + }; + } + + public start(): void {} +} diff --git a/x-pack/plugins/actions/server/actions_config.mock.ts b/x-pack/plugins/actions/server/actions_config.mock.ts index f6110a48f785a..bf0ebb4e4791d 100644 --- a/x-pack/plugins/actions/server/actions_config.mock.ts +++ b/x-pack/plugins/actions/server/actions_config.mock.ts @@ -25,6 +25,7 @@ const createActionsConfigMock = () => { }), getCustomHostSettings: jest.fn().mockReturnValue(undefined), getMicrosoftGraphApiUrl: jest.fn().mockReturnValue(undefined), + validateEmailAddresses: jest.fn().mockReturnValue(undefined), }; return mocked; }; diff --git a/x-pack/plugins/actions/server/actions_config.test.ts b/x-pack/plugins/actions/server/actions_config.test.ts index 15261da6fd952..470e6ce8cdc8e 100644 --- a/x-pack/plugins/actions/server/actions_config.test.ts +++ b/x-pack/plugins/actions/server/actions_config.test.ts @@ -470,3 +470,47 @@ describe('getSSLSettings', () => { expect(sslSettings.verificationMode).toBe('none'); }); }); + +const testEmailsOk = ['bob@elastic.co', 'jim@elastic.co']; +const testEmailsNotAllowed = ['hal@bad.com', 'lou@notgood.org']; +const testEmailsInvalid = ['invalid-email-address', '(garbage)']; +const testEmailsAll = testEmailsOk.concat(testEmailsNotAllowed).concat(testEmailsInvalid); + +describe('validateEmailAddresses()', () => { + test('all domains allowed if config not set', () => { + const acu = getActionsConfigurationUtilities(defaultActionsConfig); + const message = acu.validateEmailAddresses(testEmailsAll); + expect(message).toEqual(undefined); + }); + + test('only filtered domains allowed if config set', () => { + const acu = getActionsConfigurationUtilities({ + ...defaultActionsConfig, + email: { + domain_allowlist: ['elastic.co'], + }, + }); + + let message = acu.validateEmailAddresses(testEmailsOk); + expect(message).toBe(undefined); + + message = acu.validateEmailAddresses(testEmailsAll); + expect(message).toMatchInlineSnapshot( + `"not valid emails: invalid-email-address, (garbage); not allowed emails: hal@bad.com, lou@notgood.org"` + ); + }); + + test('no domains allowed if config set to empty array', () => { + const acu = getActionsConfigurationUtilities({ + ...defaultActionsConfig, + email: { + domain_allowlist: [], + }, + }); + + const message = acu.validateEmailAddresses(testEmailsAll); + expect(message).toMatchInlineSnapshot( + `"not valid emails: invalid-email-address, (garbage); not allowed emails: bob@elastic.co, jim@elastic.co, hal@bad.com, lou@notgood.org"` + ); + }); +}); diff --git a/x-pack/plugins/actions/server/actions_config.ts b/x-pack/plugins/actions/server/actions_config.ts index f3f5216c2c3b5..35e08bb5cfe66 100644 --- a/x-pack/plugins/actions/server/actions_config.ts +++ b/x-pack/plugins/actions/server/actions_config.ts @@ -16,7 +16,11 @@ import { getCanonicalCustomHostUrl } from './lib/custom_host_settings'; import { ActionTypeDisabledError } from './lib'; import { ProxySettings, ResponseSettings, SSLSettings } from './types'; import { getSSLSettingsFromConfig } from './builtin_action_types/lib/get_node_ssl_options'; - +import { + ValidateEmailAddressesOptions, + validateEmailAddresses, + invalidEmailsAsMessage, +} from '../common'; export { AllowedHosts, EnabledActionTypes } from './config'; enum AllowListingField { @@ -36,6 +40,10 @@ export interface ActionsConfigurationUtilities { getResponseSettings: () => ResponseSettings; getCustomHostSettings: (targetUrl: string) => CustomHostSettings | undefined; getMicrosoftGraphApiUrl: () => undefined | string; + validateEmailAddresses( + addresses: string[], + options?: ValidateEmailAddressesOptions + ): string | undefined; } function allowListErrorMessage(field: AllowListingField, value: string) { @@ -139,12 +147,26 @@ function getCustomHostSettings( return customHostSettings.find((settings) => settings.url === canonicalUrl); } +function validateEmails( + config: ActionsConfig, + addresses: string[], + options: ValidateEmailAddressesOptions +): string | undefined { + if (config.email == null) { + return; + } + + const validated = validateEmailAddresses(config.email.domain_allowlist, addresses, options); + return invalidEmailsAsMessage(validated); +} + export function getActionsConfigurationUtilities( config: ActionsConfig ): ActionsConfigurationUtilities { const isHostnameAllowed = curry(isAllowed)(config); const isUriAllowed = curry(isHostnameAllowedInUri)(config); const isActionTypeEnabled = curry(isActionTypeEnabledInConfig)(config); + const validatedEmailCurried = curry(validateEmails)(config); return { isHostnameAllowed, isUriAllowed, @@ -170,5 +192,7 @@ export function getActionsConfigurationUtilities( }, getCustomHostSettings: (targetUrl: string) => getCustomHostSettings(config, targetUrl), getMicrosoftGraphApiUrl: () => getMicrosoftGraphApiUrlFromConfig(config), + validateEmailAddresses: (addresses: string[], options: ValidateEmailAddressesOptions) => + validatedEmailCurried(addresses, options), }; } diff --git a/x-pack/plugins/actions/server/builtin_action_types/email.test.ts b/x-pack/plugins/actions/server/builtin_action_types/email.test.ts index 729e1e8c9ac2f..55f0fc0cb5f70 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/email.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/email.test.ts @@ -24,6 +24,7 @@ import { EmailActionType, EmailActionTypeExecutorOptions, } from './email'; +import { ValidateEmailAddressesOptions } from '../../common'; const sendEmailMock = sendEmail as jest.Mock; @@ -269,6 +270,26 @@ describe('config validation', () => { `"error validating action type config: [host] value 'smtp.gmail.com' is not in the allowedHosts configuration"` ); }); + + test('config validation for emails calls validateEmailAddresses', async () => { + const configurationUtilities = actionsConfigMock.create(); + configurationUtilities.validateEmailAddresses.mockImplementation(validateEmailAddressesImpl); + + const basicActionType = getActionType({ + logger: mockedLogger, + configurationUtilities, + }); + + expect(() => { + validateConfig(basicActionType, { + from: 'badmail', + service: 'gmail', + }); + }).toThrowErrorMatchingInlineSnapshot( + `"error validating action type config: [from]: stub for actual message"` + ); + expect(configurationUtilities.validateEmailAddresses).toHaveBeenNthCalledWith(1, ['badmail']); + }); }); describe('secrets validation', () => { @@ -404,6 +425,33 @@ describe('params validation', () => { `"error validating action params: [subject]: expected value of type [string] but got [undefined]"` ); }); + + test('params validation for emails calls validateEmailAddresses', async () => { + const configurationUtilities = actionsConfigMock.create(); + configurationUtilities.validateEmailAddresses.mockImplementation(validateEmailAddressesImpl); + + const basicActionType = getActionType({ + logger: mockedLogger, + configurationUtilities, + }); + + expect(() => { + validateParams(basicActionType, { + to: ['to@example.com'], + cc: ['cc@example.com'], + bcc: ['bcc@example.com'], + subject: 'this is a test', + message: 'this is the message', + }); + }).toThrowErrorMatchingInlineSnapshot( + `"error validating action params: [to/cc/bcc]: stub for actual message"` + ); + + const allEmails = ['to@example.com', 'cc@example.com', 'bcc@example.com']; + expect(configurationUtilities.validateEmailAddresses).toHaveBeenNthCalledWith(1, allEmails, { + treatMustacheTemplatesAsValid: true, + }); + }); }); describe('execute()', () => { @@ -454,21 +502,9 @@ describe('execute()', () => { "status": "ok", } `); + delete sendEmailMock.mock.calls[0][1].configurationUtilities; expect(sendEmailMock.mock.calls[0][1]).toMatchInlineSnapshot(` Object { - "configurationUtilities": Object { - "ensureActionTypeEnabled": [MockFunction], - "ensureHostnameAllowed": [MockFunction], - "ensureUriAllowed": [MockFunction], - "getCustomHostSettings": [MockFunction], - "getMicrosoftGraphApiUrl": [MockFunction], - "getProxySettings": [MockFunction], - "getResponseSettings": [MockFunction], - "getSSLSettings": [MockFunction], - "isActionTypeEnabled": [MockFunction], - "isHostnameAllowed": [MockFunction], - "isUriAllowed": [MockFunction], - }, "connectorId": "some-id", "content": Object { "message": "a message to you @@ -517,21 +553,9 @@ describe('execute()', () => { sendEmailMock.mockReset(); await actionType.executor(customExecutorOptions); + delete sendEmailMock.mock.calls[0][1].configurationUtilities; expect(sendEmailMock.mock.calls[0][1]).toMatchInlineSnapshot(` Object { - "configurationUtilities": Object { - "ensureActionTypeEnabled": [MockFunction], - "ensureHostnameAllowed": [MockFunction], - "ensureUriAllowed": [MockFunction], - "getCustomHostSettings": [MockFunction], - "getMicrosoftGraphApiUrl": [MockFunction], - "getProxySettings": [MockFunction], - "getResponseSettings": [MockFunction], - "getSSLSettings": [MockFunction], - "isActionTypeEnabled": [MockFunction], - "isHostnameAllowed": [MockFunction], - "isUriAllowed": [MockFunction], - }, "connectorId": "some-id", "content": Object { "message": "a message to you @@ -580,21 +604,9 @@ describe('execute()', () => { sendEmailMock.mockReset(); await actionType.executor(customExecutorOptions); + delete sendEmailMock.mock.calls[0][1].configurationUtilities; expect(sendEmailMock.mock.calls[0][1]).toMatchInlineSnapshot(` Object { - "configurationUtilities": Object { - "ensureActionTypeEnabled": [MockFunction], - "ensureHostnameAllowed": [MockFunction], - "ensureUriAllowed": [MockFunction], - "getCustomHostSettings": [MockFunction], - "getMicrosoftGraphApiUrl": [MockFunction], - "getProxySettings": [MockFunction], - "getResponseSettings": [MockFunction], - "getSSLSettings": [MockFunction], - "isActionTypeEnabled": [MockFunction], - "isHostnameAllowed": [MockFunction], - "isUriAllowed": [MockFunction], - }, "connectorId": "some-id", "content": Object { "message": "a message to you @@ -745,4 +757,48 @@ describe('execute()', () => { This message was sent by Kibana. [View this in Kibana](https://localhost:1234/foo/bar/my/app)." `); }); + + test('ensure execution runs validator with allowMustache false', async () => { + const configurationUtilities = actionsConfigMock.create(); + configurationUtilities.validateEmailAddresses.mockImplementation(validateEmailAddressesImpl); + + const testActionType = getActionType({ + logger: mockedLogger, + configurationUtilities, + }); + + const customExecutorOptions: EmailActionTypeExecutorOptions = { + ...executorOptions, + params: { + ...params, + }, + }; + + const result = await testActionType.executor(customExecutorOptions); + expect(result).toMatchInlineSnapshot(` + Object { + "actionId": "some-id", + "message": "[to/cc/bcc]: stub for actual message", + "status": "error", + } + `); + expect(configurationUtilities.validateEmailAddresses.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Array [ + "jim@example.com", + "james@example.com", + "jimmy@example.com", + ], + ], + ] + `); + }); }); + +function validateEmailAddressesImpl( + addresses: string[], + options?: ValidateEmailAddressesOptions +): string | undefined { + return 'stub for actual message'; +} diff --git a/x-pack/plugins/actions/server/builtin_action_types/email.ts b/x-pack/plugins/actions/server/builtin_action_types/email.ts index 29c4f9fb595bd..b2c0549bd7ea4 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/email.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/email.ts @@ -10,8 +10,9 @@ import { i18n } from '@kbn/i18n'; import { schema, TypeOf } from '@kbn/config-schema'; import nodemailerGetService from 'nodemailer/lib/well-known'; import SMTPConnection from 'nodemailer/lib/smtp-connection'; - import { Logger } from '@kbn/core/server'; +import { withoutMustacheTemplate } from '../../common'; + import { sendEmail, JSON_TRANSPORT_SERVICE, SendEmailOptions, Transport } from './lib/send_email'; import { portSchema } from './lib/schemas'; import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../types'; @@ -65,6 +66,12 @@ function validateConfig( ): string | void { const config = configObject; + const emails = [config.from]; + const invalidEmailsMessage = configurationUtilities.validateEmailAddresses(emails); + if (!!invalidEmailsMessage) { + return `[from]: ${invalidEmailsMessage}`; + } + // If service is set as JSON_TRANSPORT_SERVICE or EXCHANGE, host/port are ignored, when the email is sent. // Note, not currently making these message translated, as will be // emitted alongside messages from @kbn/config-schema, which does not @@ -128,30 +135,30 @@ const SecretsSchema = schema.object(SecretsSchemaProps); export type ActionParamsType = TypeOf; -const ParamsSchema = schema.object( - { - to: schema.arrayOf(schema.string(), { defaultValue: [] }), - cc: schema.arrayOf(schema.string(), { defaultValue: [] }), - bcc: schema.arrayOf(schema.string(), { defaultValue: [] }), - subject: schema.string(), - message: schema.string(), - // kibanaFooterLink isn't inteded for users to set, this is here to be able to programatically - // provide a more contextual URL in the footer (ex: URL to the alert details page) - kibanaFooterLink: schema.object({ - path: schema.string({ defaultValue: '/' }), - text: schema.string({ - defaultValue: i18n.translate('xpack.actions.builtin.email.kibanaFooterLinkText', { - defaultMessage: 'Go to Kibana', - }), +const ParamsSchemaProps = { + to: schema.arrayOf(schema.string(), { defaultValue: [] }), + cc: schema.arrayOf(schema.string(), { defaultValue: [] }), + bcc: schema.arrayOf(schema.string(), { defaultValue: [] }), + subject: schema.string(), + message: schema.string(), + // kibanaFooterLink isn't inteded for users to set, this is here to be able to programatically + // provide a more contextual URL in the footer (ex: URL to the alert details page) + kibanaFooterLink: schema.object({ + path: schema.string({ defaultValue: '/' }), + text: schema.string({ + defaultValue: i18n.translate('xpack.actions.builtin.email.kibanaFooterLinkText', { + defaultMessage: 'Go to Kibana', }), }), - }, - { - validate: validateParams, - } -); + }), +}; + +const ParamsSchema = schema.object(ParamsSchemaProps); -function validateParams(paramsObject: unknown): string | void { +function validateParams( + configurationUtilities: ActionsConfigurationUtilities, + paramsObject: unknown +): string | void { // avoids circular reference ... const params = paramsObject as ActionParamsType; @@ -161,6 +168,14 @@ function validateParams(paramsObject: unknown): string | void { if (addrs === 0) { return 'no [to], [cc], or [bcc] entries'; } + + const emails = withoutMustacheTemplate(to.concat(cc).concat(bcc)); + const invalidEmailsMessage = configurationUtilities.validateEmailAddresses(emails, { + treatMustacheTemplatesAsValid: true, + }); + if (invalidEmailsMessage) { + return `[to/cc/bcc]: ${invalidEmailsMessage}`; + } } interface GetActionTypeParams { @@ -203,7 +218,9 @@ export function getActionType(params: GetActionTypeParams): EmailActionType { validate: curry(validateConfig)(configurationUtilities), }), secrets: SecretsSchema, - params: ParamsSchema, + params: schema.object(ParamsSchemaProps, { + validate: curry(validateParams)(configurationUtilities), + }), connector: validateConnector, }, renderParameterTemplates, @@ -243,6 +260,17 @@ async function executor( const params = execOptions.params; const connectorTokenClient = execOptions.services.connectorTokenClient; + const emails = params.to.concat(params.cc).concat(params.bcc); + let invalidEmailsMessage = configurationUtilities.validateEmailAddresses(emails); + if (invalidEmailsMessage) { + return { status: 'error', actionId, message: `[to/cc/bcc]: ${invalidEmailsMessage}` }; + } + + invalidEmailsMessage = configurationUtilities.validateEmailAddresses([config.from]); + if (invalidEmailsMessage) { + return { status: 'error', actionId, message: `[from]: ${invalidEmailsMessage}` }; + } + const transport: Transport = {}; if (secrets.user != null) { diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts index 65aad21db447e..8c36113032461 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts @@ -131,6 +131,7 @@ describe('send_email module', () => { page: 1, }); await sendEmail(mockLogger, sendEmailOptions, connectorTokenClient); + requestOAuthClientCredentialsTokenMock.mock.calls[0].pop(); expect(requestOAuthClientCredentialsTokenMock.mock.calls[0]).toMatchInlineSnapshot(` Array [ "https://login.microsoftonline.com/undefined/oauth2/v2.0/token", @@ -150,32 +151,11 @@ describe('send_email module', () => { "clientSecret": "sdfhkdsjhfksdjfh", "scope": "https://graph.microsoft.com/.default", }, - Object { - "ensureActionTypeEnabled": [MockFunction], - "ensureHostnameAllowed": [MockFunction], - "ensureUriAllowed": [MockFunction], - "getCustomHostSettings": [MockFunction], - "getMicrosoftGraphApiUrl": [MockFunction] { - "calls": Array [ - Array [], - ], - "results": Array [ - Object { - "type": "return", - "value": undefined, - }, - ], - }, - "getProxySettings": [MockFunction], - "getResponseSettings": [MockFunction], - "getSSLSettings": [MockFunction], - "isActionTypeEnabled": [MockFunction], - "isHostnameAllowed": [MockFunction], - "isUriAllowed": [MockFunction], - }, ] `); + delete sendEmailGraphApiMock.mock.calls[0][0].options.configurationUtilities; + sendEmailGraphApiMock.mock.calls[0].pop(); expect(sendEmailGraphApiMock.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { @@ -187,29 +167,6 @@ describe('send_email module', () => { "messageHTML": "

a message

", "options": Object { - "configurationUtilities": Object { - "ensureActionTypeEnabled": [MockFunction], - "ensureHostnameAllowed": [MockFunction], - "ensureUriAllowed": [MockFunction], - "getCustomHostSettings": [MockFunction], - "getMicrosoftGraphApiUrl": [MockFunction] { - "calls": Array [ - Array [], - ], - "results": Array [ - Object { - "type": "return", - "value": undefined, - }, - ], - }, - "getProxySettings": [MockFunction], - "getResponseSettings": [MockFunction], - "getSSLSettings": [MockFunction], - "isActionTypeEnabled": [MockFunction], - "isHostnameAllowed": [MockFunction], - "isUriAllowed": [MockFunction], - }, "connectorId": "1", "content": Object { "message": "a message", @@ -247,29 +204,6 @@ describe('send_email module', () => { "trace": [MockFunction], "warn": [MockFunction], }, - Object { - "ensureActionTypeEnabled": [MockFunction], - "ensureHostnameAllowed": [MockFunction], - "ensureUriAllowed": [MockFunction], - "getCustomHostSettings": [MockFunction], - "getMicrosoftGraphApiUrl": [MockFunction] { - "calls": Array [ - Array [], - ], - "results": Array [ - Object { - "type": "return", - "value": undefined, - }, - ], - }, - "getProxySettings": [MockFunction], - "getResponseSettings": [MockFunction], - "getSSLSettings": [MockFunction], - "isActionTypeEnabled": [MockFunction], - "isHostnameAllowed": [MockFunction], - "isUriAllowed": [MockFunction], - }, ] `); @@ -331,6 +265,8 @@ describe('send_email module', () => { await sendEmail(mockLogger, sendEmailOptions, connectorTokenClient); expect(requestOAuthClientCredentialsTokenMock.mock.calls.length).toBe(0); + delete sendEmailGraphApiMock.mock.calls[0][0].options.configurationUtilities; + sendEmailGraphApiMock.mock.calls[0].pop(); expect(sendEmailGraphApiMock.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { @@ -342,29 +278,6 @@ describe('send_email module', () => { "messageHTML": "

a message

", "options": Object { - "configurationUtilities": Object { - "ensureActionTypeEnabled": [MockFunction], - "ensureHostnameAllowed": [MockFunction], - "ensureUriAllowed": [MockFunction], - "getCustomHostSettings": [MockFunction], - "getMicrosoftGraphApiUrl": [MockFunction] { - "calls": Array [ - Array [], - ], - "results": Array [ - Object { - "type": "return", - "value": undefined, - }, - ], - }, - "getProxySettings": [MockFunction], - "getResponseSettings": [MockFunction], - "getSSLSettings": [MockFunction], - "isActionTypeEnabled": [MockFunction], - "isHostnameAllowed": [MockFunction], - "isUriAllowed": [MockFunction], - }, "connectorId": "1", "content": Object { "message": "a message", @@ -402,29 +315,6 @@ describe('send_email module', () => { "trace": [MockFunction], "warn": [MockFunction], }, - Object { - "ensureActionTypeEnabled": [MockFunction], - "ensureHostnameAllowed": [MockFunction], - "ensureUriAllowed": [MockFunction], - "getCustomHostSettings": [MockFunction], - "getMicrosoftGraphApiUrl": [MockFunction] { - "calls": Array [ - Array [], - ], - "results": Array [ - Object { - "type": "return", - "value": undefined, - }, - ], - }, - "getProxySettings": [MockFunction], - "getResponseSettings": [MockFunction], - "getSSLSettings": [MockFunction], - "isActionTypeEnabled": [MockFunction], - "isHostnameAllowed": [MockFunction], - "isUriAllowed": [MockFunction], - }, ] `); @@ -510,6 +400,8 @@ describe('send_email module', () => { await sendEmail(mockLogger, sendEmailOptions, connectorTokenClient); expect(requestOAuthClientCredentialsTokenMock.mock.calls.length).toBe(1); + delete sendEmailGraphApiMock.mock.calls[0][0].options.configurationUtilities; + sendEmailGraphApiMock.mock.calls[0].pop(); expect(sendEmailGraphApiMock.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { @@ -521,29 +413,6 @@ describe('send_email module', () => { "messageHTML": "

a message

", "options": Object { - "configurationUtilities": Object { - "ensureActionTypeEnabled": [MockFunction], - "ensureHostnameAllowed": [MockFunction], - "ensureUriAllowed": [MockFunction], - "getCustomHostSettings": [MockFunction], - "getMicrosoftGraphApiUrl": [MockFunction] { - "calls": Array [ - Array [], - ], - "results": Array [ - Object { - "type": "return", - "value": undefined, - }, - ], - }, - "getProxySettings": [MockFunction], - "getResponseSettings": [MockFunction], - "getSSLSettings": [MockFunction], - "isActionTypeEnabled": [MockFunction], - "isHostnameAllowed": [MockFunction], - "isUriAllowed": [MockFunction], - }, "connectorId": "1", "content": Object { "message": "a message", @@ -581,29 +450,6 @@ describe('send_email module', () => { "trace": [MockFunction], "warn": [MockFunction], }, - Object { - "ensureActionTypeEnabled": [MockFunction], - "ensureHostnameAllowed": [MockFunction], - "ensureUriAllowed": [MockFunction], - "getCustomHostSettings": [MockFunction], - "getMicrosoftGraphApiUrl": [MockFunction] { - "calls": Array [ - Array [], - ], - "results": Array [ - Object { - "type": "return", - "value": undefined, - }, - ], - }, - "getProxySettings": [MockFunction], - "getResponseSettings": [MockFunction], - "getSSLSettings": [MockFunction], - "isActionTypeEnabled": [MockFunction], - "isHostnameAllowed": [MockFunction], - "isUriAllowed": [MockFunction], - }, ] `); @@ -647,6 +493,8 @@ describe('send_email module', () => { `Not able to update connector token for connectorId: 1 due to error: Fail`, ]); + delete sendEmailGraphApiMock.mock.calls[0][0].options.configurationUtilities; + sendEmailGraphApiMock.mock.calls[0].pop(); expect(sendEmailGraphApiMock.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { @@ -658,29 +506,6 @@ describe('send_email module', () => { "messageHTML": "

a message

", "options": Object { - "configurationUtilities": Object { - "ensureActionTypeEnabled": [MockFunction], - "ensureHostnameAllowed": [MockFunction], - "ensureUriAllowed": [MockFunction], - "getCustomHostSettings": [MockFunction], - "getMicrosoftGraphApiUrl": [MockFunction] { - "calls": Array [ - Array [], - ], - "results": Array [ - Object { - "type": "return", - "value": undefined, - }, - ], - }, - "getProxySettings": [MockFunction], - "getResponseSettings": [MockFunction], - "getSSLSettings": [MockFunction], - "isActionTypeEnabled": [MockFunction], - "isHostnameAllowed": [MockFunction], - "isUriAllowed": [MockFunction], - }, "connectorId": "1", "content": Object { "message": "a message", @@ -742,29 +567,6 @@ describe('send_email module', () => { ], }, }, - Object { - "ensureActionTypeEnabled": [MockFunction], - "ensureHostnameAllowed": [MockFunction], - "ensureUriAllowed": [MockFunction], - "getCustomHostSettings": [MockFunction], - "getMicrosoftGraphApiUrl": [MockFunction] { - "calls": Array [ - Array [], - ], - "results": Array [ - Object { - "type": "return", - "value": undefined, - }, - ], - }, - "getProxySettings": [MockFunction], - "getResponseSettings": [MockFunction], - "getSSLSettings": [MockFunction], - "isActionTypeEnabled": [MockFunction], - "isHostnameAllowed": [MockFunction], - "isUriAllowed": [MockFunction], - }, ] `); }); @@ -801,6 +603,8 @@ describe('send_email module', () => { expect(requestOAuthClientCredentialsTokenMock.mock.calls.length).toBe(1); expect(connectorTokenClientM.deleteConnectorTokens.mock.calls.length).toBe(1); + delete sendEmailGraphApiMock.mock.calls[0][0].options.configurationUtilities; + sendEmailGraphApiMock.mock.calls[0].pop(); expect(sendEmailGraphApiMock.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { @@ -812,29 +616,6 @@ describe('send_email module', () => { "messageHTML": "

a message

", "options": Object { - "configurationUtilities": Object { - "ensureActionTypeEnabled": [MockFunction], - "ensureHostnameAllowed": [MockFunction], - "ensureUriAllowed": [MockFunction], - "getCustomHostSettings": [MockFunction], - "getMicrosoftGraphApiUrl": [MockFunction] { - "calls": Array [ - Array [], - ], - "results": Array [ - Object { - "type": "return", - "value": undefined, - }, - ], - }, - "getProxySettings": [MockFunction], - "getResponseSettings": [MockFunction], - "getSSLSettings": [MockFunction], - "isActionTypeEnabled": [MockFunction], - "isHostnameAllowed": [MockFunction], - "isUriAllowed": [MockFunction], - }, "connectorId": "1", "content": Object { "message": "a message", @@ -872,29 +653,6 @@ describe('send_email module', () => { "trace": [MockFunction], "warn": [MockFunction], }, - Object { - "ensureActionTypeEnabled": [MockFunction], - "ensureHostnameAllowed": [MockFunction], - "ensureUriAllowed": [MockFunction], - "getCustomHostSettings": [MockFunction], - "getMicrosoftGraphApiUrl": [MockFunction] { - "calls": Array [ - Array [], - ], - "results": Array [ - Object { - "type": "return", - "value": undefined, - }, - ], - }, - "getProxySettings": [MockFunction], - "getResponseSettings": [MockFunction], - "getSSLSettings": [MockFunction], - "isActionTypeEnabled": [MockFunction], - "isHostnameAllowed": [MockFunction], - "isUriAllowed": [MockFunction], - }, ] `); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts index 270abfed3fb35..4475832e1a7f7 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts @@ -26,6 +26,9 @@ import { ExternalIncidentServiceConfigurationBaseSchema, } from './schema'; import { ActionsConfigurationUtilities } from '../../actions_config'; +import { SNProductsConfigValue } from '../../../common'; + +export type { SNProductsConfigValue, SNProductsConfig } from '../../../common'; export type ServiceNowPublicConfigurationBaseType = TypeOf< typeof ExternalIncidentServiceConfigurationBaseSchema @@ -247,17 +250,6 @@ export interface GetApplicationInfoResponse { version: string; } -export interface SNProductsConfigValue { - table: string; - appScope: string; - useImportAPI: boolean; - importSetTable: string; - commentFieldKey: string; - appId?: string; -} - -export type SNProductsConfig = Record; - export enum ObservableTypes { ip4 = 'ipv4-addr', url = 'URL', diff --git a/x-pack/plugins/actions/server/builtin_action_types/teams.test.ts b/x-pack/plugins/actions/server/builtin_action_types/teams.test.ts index eb4ce4fda4cc6..23bc0fba603d4 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/teams.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/teams.test.ts @@ -160,22 +160,10 @@ describe('execute()', () => { secrets: { webhookUrl: 'http://example.com' }, params: { message: 'this invocation should succeed' }, }); + delete requestMock.mock.calls[0][0].configurationUtilities; expect(requestMock.mock.calls[0][0]).toMatchInlineSnapshot(` Object { "axios": undefined, - "configurationUtilities": Object { - "ensureActionTypeEnabled": [MockFunction], - "ensureHostnameAllowed": [MockFunction], - "ensureUriAllowed": [MockFunction], - "getCustomHostSettings": [MockFunction], - "getMicrosoftGraphApiUrl": [MockFunction], - "getProxySettings": [MockFunction], - "getResponseSettings": [MockFunction], - "getSSLSettings": [MockFunction], - "isActionTypeEnabled": [MockFunction], - "isHostnameAllowed": [MockFunction], - "isUriAllowed": [MockFunction], - }, "data": Object { "text": "this invocation should succeed", }, @@ -225,22 +213,10 @@ describe('execute()', () => { secrets: { webhookUrl: 'http://example.com' }, params: { message: 'this invocation should succeed' }, }); + delete requestMock.mock.calls[0][0].configurationUtilities; expect(requestMock.mock.calls[0][0]).toMatchInlineSnapshot(` Object { "axios": undefined, - "configurationUtilities": Object { - "ensureActionTypeEnabled": [MockFunction], - "ensureHostnameAllowed": [MockFunction], - "ensureUriAllowed": [MockFunction], - "getCustomHostSettings": [MockFunction], - "getMicrosoftGraphApiUrl": [MockFunction], - "getProxySettings": [MockFunction], - "getResponseSettings": [MockFunction], - "getSSLSettings": [MockFunction], - "isActionTypeEnabled": [MockFunction], - "isHostnameAllowed": [MockFunction], - "isUriAllowed": [MockFunction], - }, "data": Object { "text": "this invocation should succeed", }, diff --git a/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts b/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts index 2f68c6465fa18..6b8a1a4e8447b 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts @@ -279,6 +279,7 @@ describe('execute()', () => { params: { body: 'some data' }, }); + delete requestMock.mock.calls[0][0].configurationUtilities; expect(requestMock.mock.calls[0][0]).toMatchInlineSnapshot(` Object { "auth": Object { @@ -286,19 +287,6 @@ describe('execute()', () => { "username": "abc", }, "axios": undefined, - "configurationUtilities": Object { - "ensureActionTypeEnabled": [MockFunction], - "ensureHostnameAllowed": [MockFunction], - "ensureUriAllowed": [MockFunction], - "getCustomHostSettings": [MockFunction], - "getMicrosoftGraphApiUrl": [MockFunction], - "getProxySettings": [MockFunction], - "getResponseSettings": [MockFunction], - "getSSLSettings": [MockFunction], - "isActionTypeEnabled": [MockFunction], - "isHostnameAllowed": [MockFunction], - "isUriAllowed": [MockFunction], - }, "data": "some data", "headers": Object { "aheader": "a value", @@ -377,22 +365,10 @@ describe('execute()', () => { params: { body: 'some data' }, }); + delete requestMock.mock.calls[0][0].configurationUtilities; expect(requestMock.mock.calls[0][0]).toMatchInlineSnapshot(` Object { "axios": undefined, - "configurationUtilities": Object { - "ensureActionTypeEnabled": [MockFunction], - "ensureHostnameAllowed": [MockFunction], - "ensureUriAllowed": [MockFunction], - "getCustomHostSettings": [MockFunction], - "getMicrosoftGraphApiUrl": [MockFunction], - "getProxySettings": [MockFunction], - "getResponseSettings": [MockFunction], - "getSSLSettings": [MockFunction], - "isActionTypeEnabled": [MockFunction], - "isHostnameAllowed": [MockFunction], - "isUriAllowed": [MockFunction], - }, "data": "some data", "headers": Object { "aheader": "a value", diff --git a/x-pack/plugins/actions/server/config.test.ts b/x-pack/plugins/actions/server/config.test.ts index 4f39956d910a9..e6e3a24db5214 100644 --- a/x-pack/plugins/actions/server/config.test.ts +++ b/x-pack/plugins/actions/server/config.test.ts @@ -212,6 +212,25 @@ describe('config validation', () => { } `); }); + + test('validates email.domain_allowlist', () => { + const config: Record = {}; + let result = configSchema.validate(config); + expect(result.email === undefined); + + config.email = {}; + expect(() => configSchema.validate(config)).toThrowErrorMatchingInlineSnapshot( + `"[email.domain_allowlist]: expected value of type [array] but got [undefined]"` + ); + + config.email = { domain_allowlist: [] }; + result = configSchema.validate(config); + expect(result.email?.domain_allowlist).toEqual([]); + + config.email = { domain_allowlist: ['a.com', 'b.c.com', 'd.e.f.com'] }; + result = configSchema.validate(config); + expect(result.email?.domain_allowlist).toEqual(['a.com', 'b.c.com', 'd.e.f.com']); + }); }); // object creator that ensures we can create a property named __proto__ on an diff --git a/x-pack/plugins/actions/server/config.ts b/x-pack/plugins/actions/server/config.ts index 1f279f20c27c3..4c8ca7ff9fff7 100644 --- a/x-pack/plugins/actions/server/config.ts +++ b/x-pack/plugins/actions/server/config.ts @@ -112,6 +112,11 @@ export const configSchema = schema.object({ pageSize: schema.number({ defaultValue: 100 }), }), microsoftGraphApiUrl: schema.maybe(schema.string()), + email: schema.maybe( + schema.object({ + domain_allowlist: schema.arrayOf(schema.string()), + }) + ), }); export type ActionsConfig = TypeOf; diff --git a/x-pack/plugins/actions/server/index.ts b/x-pack/plugins/actions/server/index.ts index 7b5863eabf0a9..6b0070af0b022 100644 --- a/x-pack/plugins/actions/server/index.ts +++ b/x-pack/plugins/actions/server/index.ts @@ -57,6 +57,9 @@ export const plugin = (initContext: PluginInitializerContext) => new ActionsPlug export const config: PluginConfigDescriptor = { schema: configSchema, + exposeToBrowser: { + email: { domain_allowlist: true }, + }, deprecations: ({ renameFromRoot, unused }) => [ renameFromRoot('xpack.actions.whitelistedHosts', 'xpack.actions.allowedHosts', { level: 'warning', diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index 3cdf28a8d80f3..1fad2a6189693 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -314,6 +314,7 @@ export class ActionsPlugin implements Plugin(), this.licenseState, + actionsConfigUtils, this.usageCounter ); diff --git a/x-pack/plugins/actions/server/routes/index.ts b/x-pack/plugins/actions/server/routes/index.ts index 48d87e9d62c58..ab90141ae1c80 100644 --- a/x-pack/plugins/actions/server/routes/index.ts +++ b/x-pack/plugins/actions/server/routes/index.ts @@ -18,10 +18,12 @@ import { connectorTypesRoute } from './connector_types'; import { updateActionRoute } from './update'; import { getWellKnownEmailServiceRoute } from './get_well_known_email_service'; import { defineLegacyRoutes } from './legacy'; +import { ActionsConfigurationUtilities } from '../actions_config'; export function defineRoutes( router: IRouter, licenseState: ILicenseState, + actionsConfigUtils: ActionsConfigurationUtilities, usageCounter?: UsageCounter ) { defineLegacyRoutes(router, licenseState, usageCounter); diff --git a/x-pack/plugins/actions/tsconfig.json b/x-pack/plugins/actions/tsconfig.json index 95788811e43f8..aad127a6ca94c 100644 --- a/x-pack/plugins/actions/tsconfig.json +++ b/x-pack/plugins/actions/tsconfig.json @@ -10,7 +10,8 @@ "server/**/*", // have to declare *.json explicitly due to https://github.com/microsoft/TypeScript/issues/25636 "server/**/*.json", - "common/*" + "public/**/*", + "common/**/*" ], "references": [ { "path": "../../../src/core/tsconfig.json" }, diff --git a/x-pack/plugins/triggers_actions_ui/kibana.json b/x-pack/plugins/triggers_actions_ui/kibana.json index 4be95ed2db91d..a872ad6fab5c0 100644 --- a/x-pack/plugins/triggers_actions_ui/kibana.json +++ b/x-pack/plugins/triggers_actions_ui/kibana.json @@ -7,9 +7,9 @@ "version": "kibana", "server": true, "ui": true, - "optionalPlugins": ["alerting", "cloud", "features", "home", "spaces"], - "requiredPlugins": ["management", "charts", "data", "kibanaReact", "kibanaUtils", "savedObjects", "unifiedSearch", "dataViews"], + "optionalPlugins": ["cloud", "features", "home", "spaces"], + "requiredPlugins": ["management", "charts", "data", "kibanaReact", "kibanaUtils", "savedObjects", "unifiedSearch", "dataViews", "alerting", "actions"], "configPath": ["xpack", "trigger_actions_ui"], "extraPublicDirs": ["public/common", "public/common/constants"], - "requiredBundles": ["alerting", "esUiShared", "kibanaReact", "kibanaUtils"] + "requiredBundles": ["alerting", "esUiShared", "kibanaReact", "kibanaUtils", "actions"] } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.test.tsx index ab2cb5332d452..31ff85a20ed3b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.test.tsx @@ -10,13 +10,44 @@ import { registerBuiltInActionTypes } from '..'; import { ActionTypeModel } from '../../../../types'; import { EmailActionConnector } from '../types'; import { getEmailServices } from './email'; +import { + ValidatedEmail, + InvalidEmailReason, + ValidateEmailAddressesOptions, + MustacheInEmailRegExp, +} from '@kbn/actions-plugin/common'; const ACTION_TYPE_ID = '.email'; let actionTypeModel: ActionTypeModel; +const RegistrationServices = { + validateEmailAddresses: validateEmails, +}; + +// stub for the real validator +function validateEmails( + addresses: string[], + options?: ValidateEmailAddressesOptions +): ValidatedEmail[] { + return addresses.map((address) => { + if (address.includes('invalid')) + return { address, valid: false, reason: InvalidEmailReason.invalid }; + else if (address.includes('notallowed')) + return { address, valid: false, reason: InvalidEmailReason.notAllowed }; + else if (options?.treatMustacheTemplatesAsValid) return { address, valid: true }; + else if (address.match(MustacheInEmailRegExp)) + return { address, valid: false, reason: InvalidEmailReason.invalid }; + else return { address, valid: true }; + }); +} + +beforeEach(() => { + jest.resetAllMocks(); +}); + beforeAll(() => { const actionTypeRegistry = new TypeRegistry(); - registerBuiltInActionTypes({ actionTypeRegistry }); + registerBuiltInActionTypes({ actionTypeRegistry, services: RegistrationServices }); const getResult = actionTypeRegistry.get(ACTION_TYPE_ID); if (getResult !== null) { actionTypeModel = getResult; @@ -138,7 +169,7 @@ describe('connector validation', () => { actionTypeId: '.email', name: 'email', config: { - from: 'test@test.com', + from: 'test@notallowed.com', hasAuth: true, service: 'other', }, @@ -147,7 +178,7 @@ describe('connector validation', () => { expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ config: { errors: { - from: [], + from: ['Email address test@notallowed.com is not allowed.'], port: ['Port is required.'], host: ['Host is required.'], service: [], @@ -163,7 +194,13 @@ describe('connector validation', () => { }, }, }); + + // also check that mustache is not valid + actionConnector.config.from = '{{mustached}}'; + const validation = await actionTypeModel.validateConnector(actionConnector); + expect(validation?.config?.errors?.from).toEqual(['Email address {{mustached}} is not valid.']); }); + test('connector validation fails when user specified but not password', async () => { const actionConnector = { secrets: { @@ -336,6 +373,7 @@ describe('action params validation', () => { const actionParams = { to: [], cc: ['test1@test.com'], + bcc: ['mustache {{\n}} template'], message: 'message {test}', subject: 'test', }; @@ -353,15 +391,17 @@ describe('action params validation', () => { test('action params validation fails when action params is not valid', async () => { const actionParams = { - to: ['test@test.com'], + to: ['invalid.com'], + cc: ['bob@notallowed.com'], + bcc: ['another-invalid.com'], subject: 'test', }; expect(await actionTypeModel.validateParams(actionParams)).toEqual({ errors: { - to: [], - cc: [], - bcc: [], + to: ['Email address invalid.com is not valid.'], + cc: ['Email address bob@notallowed.com is not allowed.'], + bcc: ['Email address another-invalid.com is not valid.'], message: ['Message is required.'], subject: [], }, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.tsx index c5da19c305268..0add2396a74d0 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.tsx @@ -5,16 +5,18 @@ * 2.0. */ +import { uniq } from 'lodash'; import { lazy } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiSelectOption } from '@elastic/eui'; -import { AdditionalEmailServices } from '@kbn/actions-plugin/common'; +import { AdditionalEmailServices, InvalidEmailReason } from '@kbn/actions-plugin/common'; import { ActionTypeModel, ConnectorValidationResult, GenericValidationResult, } from '../../../../types'; import { EmailActionParams, EmailConfig, EmailSecrets, EmailActionConnector } from '../types'; +import { RegistrationServices } from '..'; const emailServices: EuiSelectOption[] = [ { @@ -79,8 +81,9 @@ export function getEmailServices(isCloudEnabled: boolean) { : emailServices.filter((service) => service.value !== 'elastic_cloud'); } -export function getActionType(): ActionTypeModel { - const mailformat = /^[^@\s]+@[^@\s]+$/; +export function getActionType( + services: RegistrationServices +): ActionTypeModel { return { id: '.email', iconClass: 'email', @@ -122,9 +125,15 @@ export function getActionType(): ActionTypeModel(), }; const validationResult = { errors }; - if ( - (!(actionParams.to instanceof Array) || actionParams.to.length === 0) && - (!(actionParams.cc instanceof Array) || actionParams.cc.length === 0) && - (!(actionParams.bcc instanceof Array) || actionParams.bcc.length === 0) - ) { - const errorText = translations.TO_CC_REQUIRED; - errors.to.push(errorText); - errors.cc.push(errorText); - errors.bcc.push(errorText); - } + if (!actionParams.message?.length) { errors.message.push(translations.MESSAGE_REQUIRED); } if (!actionParams.subject?.length) { errors.subject.push(translations.SUBJECT_REQUIRED); } + + const toEmails = getToFields(actionParams); + const ccEmails = getCcFields(actionParams); + const bccEmails = getBccFields(actionParams); + + if (toEmails.length === 0 && ccEmails.length === 0 && bccEmails.length === 0) { + const errorText = translations.TO_CC_REQUIRED; + errors.to.push(errorText); + errors.cc.push(errorText); + errors.bcc.push(errorText); + } + + const allEmails = uniq(toEmails.concat(ccEmails).concat(bccEmails)); + const validatedEmails = services.validateEmailAddresses(allEmails, { + treatMustacheTemplatesAsValid: true, + }); + + const toEmailSet = new Set(toEmails); + const ccEmailSet = new Set(ccEmails); + const bccEmailSet = new Set(bccEmails); + + for (const validated of validatedEmails) { + if (!validated.valid) { + const email = validated.address; + const message = + validated.reason === InvalidEmailReason.notAllowed + ? translations.getNotAllowedEmailAddress(email) + : translations.getInvalidEmailAddress(email); + + if (toEmailSet.has(email)) errors.to.push(message); + if (ccEmailSet.has(email)) errors.cc.push(message); + if (bccEmailSet.has(email)) errors.bcc.push(message); + } + } + return validationResult; }, actionConnectorFields: lazy(() => import('./email_connector')), actionParamsFields: lazy(() => import('./email_params')), }; } + +function getToFields(actionParams: EmailActionParams): string[] { + if (!(actionParams.to instanceof Array)) return []; + return actionParams.to; +} + +function getCcFields(actionParams: EmailActionParams): string[] { + if (!(actionParams.cc instanceof Array)) return []; + return actionParams.cc; +} + +function getBccFields(actionParams: EmailActionParams): string[] { + if (!(actionParams.bcc instanceof Array)) return []; + return actionParams.bcc; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/translations.ts index 38e16f6046184..e1bee12d98993 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/translations.ts @@ -104,3 +104,20 @@ export const SUBJECT_REQUIRED = i18n.translate( defaultMessage: 'Subject is required.', } ); + +export function getInvalidEmailAddress(email: string) { + return i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.error.invalidEmail', + { + defaultMessage: 'Email address {email} is not valid.', + values: { email }, + } + ); +} + +export function getNotAllowedEmailAddress(email: string) { + return i18n.translate('xpack.triggersActionsUI.components.builtinActionTypes.error.notAllowed', { + defaultMessage: 'Email address {email} is not allowed.', + values: { email }, + }); +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index.test.tsx index 4097adb7b067f..42dc8a16a8b19 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index.test.tsx @@ -9,13 +9,14 @@ import { TypeRegistry } from '../../../type_registry'; import { registerBuiltInActionTypes } from '..'; import { ActionTypeModel } from '../../../../types'; import { EsIndexActionConnector } from '../types'; +import { registrationServicesMock } from '../../../../mocks'; const ACTION_TYPE_ID = '.index'; let actionTypeModel: ActionTypeModel; beforeAll(() => { const actionTypeRegistry = new TypeRegistry(); - registerBuiltInActionTypes({ actionTypeRegistry }); + registerBuiltInActionTypes({ actionTypeRegistry, services: registrationServicesMock }); const getResult = actionTypeRegistry.get(ACTION_TYPE_ID); if (getResult !== null) { actionTypeModel = getResult; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/index.ts index 6817631e2150a..e2089221b4d60 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/index.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { ValidatedEmail, ValidateEmailAddressesOptions } from '@kbn/actions-plugin/common'; import { getServerLogActionType } from './server_log'; import { getSlackActionType } from './slack'; import { getEmailActionType } from './email'; @@ -24,14 +25,23 @@ import { getJiraActionType } from './jira'; import { getResilientActionType } from './resilient'; import { getTeamsActionType } from './teams'; +export interface RegistrationServices { + validateEmailAddresses: ( + addresses: string[], + options?: ValidateEmailAddressesOptions + ) => ValidatedEmail[]; +} + export function registerBuiltInActionTypes({ actionTypeRegistry, + services, }: { actionTypeRegistry: TypeRegistry; + services: RegistrationServices; }) { actionTypeRegistry.register(getServerLogActionType()); actionTypeRegistry.register(getSlackActionType()); - actionTypeRegistry.register(getEmailActionType()); + actionTypeRegistry.register(getEmailActionType(services)); actionTypeRegistry.register(getIndexActionType()); actionTypeRegistry.register(getPagerDutyActionType()); actionTypeRegistry.register(getSwimlaneActionType()); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx index 4becccec3483d..3bb023b135c40 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx @@ -9,13 +9,14 @@ import { TypeRegistry } from '../../../type_registry'; import { registerBuiltInActionTypes } from '..'; import { ActionTypeModel } from '../../../../types'; import { JiraActionConnector } from './types'; +import { registrationServicesMock } from '../../../../mocks'; const ACTION_TYPE_ID = '.jira'; let actionTypeModel: ActionTypeModel; beforeAll(() => { const actionTypeRegistry = new TypeRegistry(); - registerBuiltInActionTypes({ actionTypeRegistry }); + registerBuiltInActionTypes({ actionTypeRegistry, services: registrationServicesMock }); const getResult = actionTypeRegistry.get(ACTION_TYPE_ID); if (getResult !== null) { actionTypeModel = getResult; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.test.tsx index 10bf1a45806e8..a8274729506af 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.test.tsx @@ -9,13 +9,14 @@ import { TypeRegistry } from '../../../type_registry'; import { registerBuiltInActionTypes } from '..'; import { ActionTypeModel } from '../../../../types'; import { PagerDutyActionConnector } from '../types'; +import { registrationServicesMock } from '../../../../mocks'; const ACTION_TYPE_ID = '.pagerduty'; let actionTypeModel: ActionTypeModel; beforeAll(() => { const actionTypeRegistry = new TypeRegistry(); - registerBuiltInActionTypes({ actionTypeRegistry }); + registerBuiltInActionTypes({ actionTypeRegistry, services: registrationServicesMock }); const getResult = actionTypeRegistry.get(ACTION_TYPE_ID); if (getResult !== null) { actionTypeModel = getResult; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.test.tsx index 67a51964dcb03..209606913dce6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.test.tsx @@ -9,13 +9,14 @@ import { TypeRegistry } from '../../../type_registry'; import { registerBuiltInActionTypes } from '..'; import { ActionTypeModel } from '../../../../types'; import { ResilientActionConnector } from './types'; +import { registrationServicesMock } from '../../../../mocks'; const ACTION_TYPE_ID = '.resilient'; let actionTypeModel: ActionTypeModel; beforeAll(() => { const actionTypeRegistry = new TypeRegistry(); - registerBuiltInActionTypes({ actionTypeRegistry }); + registerBuiltInActionTypes({ actionTypeRegistry, services: registrationServicesMock }); const getResult = actionTypeRegistry.get(ACTION_TYPE_ID); if (getResult !== null) { actionTypeModel = getResult; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log.test.tsx index 69c1c18bd06ea..012a233973012 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log.test.tsx @@ -8,13 +8,14 @@ import { TypeRegistry } from '../../../type_registry'; import { registerBuiltInActionTypes } from '..'; import { ActionTypeModel, UserConfiguredActionConnector } from '../../../../types'; +import { registrationServicesMock } from '../../../../mocks'; const ACTION_TYPE_ID = '.server-log'; let actionTypeModel: ActionTypeModel; beforeAll(() => { const actionTypeRegistry = new TypeRegistry(); - registerBuiltInActionTypes({ actionTypeRegistry }); + registerBuiltInActionTypes({ actionTypeRegistry, services: registrationServicesMock }); const getResult = actionTypeRegistry.get(ACTION_TYPE_ID); if (getResult !== null) { actionTypeModel = getResult; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/api.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/api.ts index dad3e3e0d0170..a9e8b8f544d42 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/api.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/api.ts @@ -7,9 +7,7 @@ import { HttpSetup } from '@kbn/core/public'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { snExternalServiceConfig } from '@kbn/actions-plugin/server/builtin_action_types/servicenow/config'; -import { ActionTypeExecutorResult } from '@kbn/actions-plugin/common'; +import { ActionTypeExecutorResult, snExternalServiceConfig } from '@kbn/actions-plugin/common'; import { BASE_ACTION_API_PATH } from '../../../constants'; import { API_INFO_ERROR } from './translations'; import { AppInfo, RESTApiError } from './types'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.test.tsx index a52c48d4e8e27..9a634170fa793 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.test.tsx @@ -9,6 +9,7 @@ import { TypeRegistry } from '../../../type_registry'; import { registerBuiltInActionTypes } from '..'; import { ActionTypeModel } from '../../../../types'; import { ServiceNowActionConnector } from './types'; +import { registrationServicesMock } from '../../../../mocks'; const SERVICENOW_ITSM_ACTION_TYPE_ID = '.servicenow'; const SERVICENOW_SIR_ACTION_TYPE_ID = '.servicenow-sir'; @@ -17,7 +18,7 @@ let actionTypeRegistry: TypeRegistry; beforeAll(() => { actionTypeRegistry = new TypeRegistry(); - registerBuiltInActionTypes({ actionTypeRegistry }); + registerBuiltInActionTypes({ actionTypeRegistry, services: registrationServicesMock }); }); describe('actionTypeRegistry.get() works', () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx index a9ff9497bdf19..22afcd5255e44 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx @@ -8,8 +8,7 @@ import React, { useCallback, useEffect, useState } from 'react'; import { EuiSpacer } from '@elastic/eui'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { snExternalServiceConfig } from '@kbn/actions-plugin/server/builtin_action_types/servicenow/config'; +import { snExternalServiceConfig } from '@kbn/actions-plugin/common'; import { ActionConnectorFieldsProps } from '../../../../types'; import * as i18n from './translations'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/update_connector.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/update_connector.tsx index 732aeed6313ac..de5cf4df5731a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/update_connector.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/update_connector.tsx @@ -21,8 +21,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { snExternalServiceConfig } from '@kbn/actions-plugin/server/builtin_action_types/servicenow/config'; +import { snExternalServiceConfig } from '@kbn/actions-plugin/common'; import { ActionConnectorFieldsProps } from '../../../../types'; import { ServiceNowActionConnector } from './types'; import { CredentialsApiUrl } from './credentials_api_url'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack.test.tsx index 95ce25a03349e..76a23ab94d972 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack.test.tsx @@ -9,13 +9,14 @@ import { TypeRegistry } from '../../../type_registry'; import { registerBuiltInActionTypes } from '..'; import { ActionTypeModel } from '../../../../types'; import { SlackActionConnector } from '../types'; +import { registrationServicesMock } from '../../../../mocks'; const ACTION_TYPE_ID = '.slack'; let actionTypeModel: ActionTypeModel; beforeAll(async () => { const actionTypeRegistry = new TypeRegistry(); - registerBuiltInActionTypes({ actionTypeRegistry }); + registerBuiltInActionTypes({ actionTypeRegistry, services: registrationServicesMock }); const getResult = actionTypeRegistry.get(ACTION_TYPE_ID); if (getResult !== null) { actionTypeModel = getResult; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.test.tsx index 5d69af2d08779..45d68c8ab39e8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.test.tsx @@ -9,13 +9,14 @@ import { TypeRegistry } from '../../../type_registry'; import { registerBuiltInActionTypes } from '..'; import { ActionTypeModel } from '../../../../types'; import { SwimlaneActionConnector } from './types'; +import { registrationServicesMock } from '../../../../mocks'; const ACTION_TYPE_ID = '.swimlane'; let actionTypeModel: ActionTypeModel; beforeAll(() => { const actionTypeRegistry = new TypeRegistry(); - registerBuiltInActionTypes({ actionTypeRegistry }); + registerBuiltInActionTypes({ actionTypeRegistry, services: registrationServicesMock }); const getResult = actionTypeRegistry.get(ACTION_TYPE_ID); if (getResult !== null) { actionTypeModel = getResult; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams.test.tsx index da0a67fe79a20..8590433f39cc0 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams.test.tsx @@ -9,13 +9,14 @@ import { TypeRegistry } from '../../../type_registry'; import { registerBuiltInActionTypes } from '..'; import { ActionTypeModel } from '../../../../types'; import { TeamsActionConnector } from '../types'; +import { registrationServicesMock } from '../../../../mocks'; const ACTION_TYPE_ID = '.teams'; let actionTypeModel: ActionTypeModel; beforeAll(async () => { const actionTypeRegistry = new TypeRegistry(); - registerBuiltInActionTypes({ actionTypeRegistry }); + registerBuiltInActionTypes({ actionTypeRegistry, services: registrationServicesMock }); const getResult = actionTypeRegistry.get(ACTION_TYPE_ID); if (getResult !== null) { actionTypeModel = getResult; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook.test.tsx index 7551f647a5b4a..771786157ed4c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook.test.tsx @@ -9,13 +9,14 @@ import { TypeRegistry } from '../../../type_registry'; import { registerBuiltInActionTypes } from '..'; import { ActionTypeModel } from '../../../../types'; import { WebhookActionConnector } from '../types'; +import { registrationServicesMock } from '../../../../mocks'; const ACTION_TYPE_ID = '.webhook'; let actionTypeModel: ActionTypeModel; beforeAll(() => { const actionTypeRegistry = new TypeRegistry(); - registerBuiltInActionTypes({ actionTypeRegistry }); + registerBuiltInActionTypes({ actionTypeRegistry, services: registrationServicesMock }); const getResult = actionTypeRegistry.get(ACTION_TYPE_ID); if (getResult !== null) { actionTypeModel = getResult; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/xmatters/xmatters.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/xmatters/xmatters.test.tsx index 957302d18a6fc..7e9dbc4cf885a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/xmatters/xmatters.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/xmatters/xmatters.test.tsx @@ -9,13 +9,14 @@ import { TypeRegistry } from '../../../type_registry'; import { registerBuiltInActionTypes } from '..'; import { ActionTypeModel } from '../../../../types'; import { XmattersActionConnector } from '../types'; +import { registrationServicesMock } from '../../../../mocks'; const ACTION_TYPE_ID = '.xmatters'; let actionTypeModel: ActionTypeModel; beforeAll(() => { const actionTypeRegistry = new TypeRegistry(); - registerBuiltInActionTypes({ actionTypeRegistry }); + registerBuiltInActionTypes({ actionTypeRegistry, services: registrationServicesMock }); const getResult = actionTypeRegistry.get(ACTION_TYPE_ID); if (getResult !== null) { actionTypeModel = getResult; diff --git a/x-pack/plugins/triggers_actions_ui/public/mocks.ts b/x-pack/plugins/triggers_actions_ui/public/mocks.ts index 79edc1f08ac97..007b906e8747b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/mocks.ts +++ b/x-pack/plugins/triggers_actions_ui/public/mocks.ts @@ -5,13 +5,14 @@ * 2.0. */ +import type { ValidatedEmail } from '@kbn/actions-plugin/common'; import type { TriggersAndActionsUIPublicPluginStart } from './plugin'; import { getAddConnectorFlyoutLazy } from './common/get_add_connector_flyout'; import { getEditConnectorFlyoutLazy } from './common/get_edit_connector_flyout'; import { getAddAlertFlyoutLazy } from './common/get_add_alert_flyout'; import { getEditAlertFlyoutLazy } from './common/get_edit_alert_flyout'; - +import { RegistrationServices } from './application/components/builtin_action_types'; import { TypeRegistry } from './application/type_registry'; import { ActionTypeModel, @@ -69,3 +70,9 @@ function createStartMock(): TriggersAndActionsUIPublicPluginStart { export const triggersActionsUiMock = { createStart: createStartMock, }; + +function validateEmailAddresses(addresses: string[]): ValidatedEmail[] { + return addresses.map((address) => ({ address, valid: true })); +} + +export const registrationServicesMock: RegistrationServices = { validateEmailAddresses }; diff --git a/x-pack/plugins/triggers_actions_ui/public/plugin.ts b/x-pack/plugins/triggers_actions_ui/public/plugin.ts index ba2e869c82e0f..7997552a81023 100644 --- a/x-pack/plugins/triggers_actions_ui/public/plugin.ts +++ b/x-pack/plugins/triggers_actions_ui/public/plugin.ts @@ -16,6 +16,7 @@ import { ManagementAppMountParams, ManagementSetup } from '@kbn/management-plugi import type { HomePublicPluginSetup } from '@kbn/home-plugin/public'; import { ChartsPluginStart } from '@kbn/charts-plugin/public'; import { PluginStartContract as AlertingStart } from '@kbn/alerting-plugin/public'; +import { ActionsPublicPluginSetup } from '@kbn/actions-plugin/public'; import { DataPublicPluginStart } from '@kbn/data-plugin/public'; import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import { Storage } from '@kbn/kibana-utils-plugin/public'; @@ -81,6 +82,7 @@ interface PluginsSetup { management: ManagementSetup; home?: HomePublicPluginSetup; cloud?: { isCloudEnabled: boolean }; + actions: ActionsPublicPluginSetup; } interface PluginsStart { @@ -193,6 +195,9 @@ export class Plugin registerBuiltInActionTypes({ actionTypeRegistry: this.actionTypeRegistry, + services: { + validateEmailAddresses: plugins.actions.validateEmailAddresses, + }, }); if (this.experimentalFeatures.internalAlertsTable) { diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts index 02f1f42af5270..14039ad3360a0 100644 --- a/x-pack/test/alerting_api_integration/common/config.ts +++ b/x-pack/test/alerting_api_integration/common/config.ts @@ -24,6 +24,7 @@ interface CreateTestConfigOptions { preconfiguredAlertHistoryEsIndex?: boolean; customizeLocalHostSsl?: boolean; rejectUnauthorized?: boolean; // legacy + emailDomainsAllowed?: string[]; } // test.not-enabled is specifically not enabled @@ -62,6 +63,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) preconfiguredAlertHistoryEsIndex = false, customizeLocalHostSsl = false, rejectUnauthorized = true, // legacy + emailDomainsAllowed = undefined, } = options; return async ({ readConfigFile }: FtrConfigProviderContext) => { @@ -132,6 +134,10 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) ? [`--xpack.actions.customHostSettings=${JSON.stringify(customHostSettingsValue)}`] : []; + const emailSettings = emailDomainsAllowed + ? [`--xpack.actions.email.domain_allowlist=${JSON.stringify(emailDomainsAllowed)}`] + : []; + return { testFiles: [require.resolve(`../${name}/tests/`)], servers, @@ -173,6 +179,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) `--xpack.actions.ssl.verificationMode=${verificationMode}`, ...actionsProxyUrl, ...customHostSettings, + ...emailSettings, '--xpack.eventLog.logEntries=true', '--xpack.task_manager.ephemeral_tasks.enabled=false', `--xpack.task_manager.unsafe.exclude_task_types=${JSON.stringify([ diff --git a/x-pack/test/alerting_api_integration/spaces_only/config.ts b/x-pack/test/alerting_api_integration/spaces_only/config.ts index 204f5b27da9d5..dcf1c70cf8dca 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/config.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/config.ts @@ -7,6 +7,8 @@ import { createTestConfig } from '../common/config'; +export const EmailDomainsAllowed = ['example.org', 'test.com']; + // eslint-disable-next-line import/no-default-export export default createTestConfig('spaces_only', { disabledPlugins: ['security'], @@ -15,4 +17,5 @@ export default createTestConfig('spaces_only', { verificationMode: 'none', customizeLocalHostSsl: true, preconfiguredAlertHistoryEsIndex: true, + emailDomainsAllowed: EmailDomainsAllowed, }); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/email.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/email.ts new file mode 100644 index 0000000000000..22d46b3918932 --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/email.ts @@ -0,0 +1,175 @@ +/* + * 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 expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { ObjectRemover } from '../../../../common/lib'; +import { EmailDomainsAllowed } from '../../../config'; + +const EmailDomainAllowed = EmailDomainsAllowed[EmailDomainsAllowed.length - 1]; + +// eslint-disable-next-line import/no-default-export +export default function emailTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const objectRemover = new ObjectRemover(supertest); + + describe('email connector', () => { + afterEach(() => objectRemover.removeAll()); + + it('succeeds with allowed email domains', async () => { + const from = `bob@${EmailDomainAllowed}`; + const conn = await createConnector(from); + expect(conn.status).to.be(200); + + const { id } = conn.body; + expect(id).to.be.a('string'); + + const to = EmailDomainsAllowed.map((domain) => `jeb@${domain}`).sort(); + const cc = EmailDomainsAllowed.map((domain) => `jim@${domain}`).sort(); + const bcc = EmailDomainsAllowed.map((domain) => `joe@${domain}`).sort(); + + const ccNames = cc.map((email) => `Jimmy Jack <${email}>`); + + const run = await runConnector(id, to, ccNames, bcc); + expect(run.status).to.be(200); + + const { status, data } = run.body || {}; + expect(status).to.be('ok'); + + const { message } = data || {}; + const { from: fromMsg } = message || {}; + + expect(fromMsg?.address).to.be(from); + expect(addressesFromMessage(message, 'to')).to.eql(to); + expect(addressesFromMessage(message, 'cc')).to.eql(cc); + expect(addressesFromMessage(message, 'bcc')).to.eql(bcc); + + const ccNamesMsg = namesFromMessage(message, 'cc'); + for (const ccName of ccNamesMsg) { + expect(ccName).to.be('Jimmy Jack'); + } + }); + + describe('fails for invalid email domains', () => { + it('in create when invalid "from" used', async () => { + const from = `bob@not.allowed`; + const { status, body } = await createConnector(from); + expect(status).to.be(400); + + const { message = 'no message returned' } = body || {}; + expect(message).to.match(/not allowed emails: bob@not.allowed/); + }); + + it('in execute when invalid "to", "cc" or "bcc" used', async () => { + const from = `bob@${EmailDomainAllowed}`; + const conn = await createConnector(from); + expect(conn.status).to.be(200); + + const { id } = conn.body || {}; + expect(id).to.be.a('string'); + + const to = EmailDomainsAllowed.map((domain) => `jeb@${domain}`).sort(); + const cc = EmailDomainsAllowed.map((domain) => `jim@${domain}`).sort(); + const bcc = EmailDomainsAllowed.map((domain) => `joe@${domain}`).sort(); + + to.push('jeb1@not.allowed'); + cc.push('jeb2@not.allowed'); + bcc.push('jeb3@not.allowed'); + + const { status, body } = await runConnector(id, to, cc, bcc); + expect(status).to.be(200); + + expect(body?.status).to.be('error'); + expect(body?.message).to.match( + /not allowed emails: jeb1@not.allowed, jeb2@not.allowed, jeb3@not.allowed/ + ); + }); + }); + }); + + /* returns the following `body`, for the special email __json service: + { + "status": "ok", + "data": { + "envelope": { + "from": "bob@example.org", + "to": [ "jeb@example.com", ...] + }, + "messageId": "", + "message": { + "from": { "address": "bob@example.org", "name": "" }, + "to": [ { "address": "jeb@example.com", "name": "" }, ...], + "cc": [ ... ], + "bcc": [ ... ], + ... + } + }, + ... + } + */ + async function createConnector(from: string): Promise<{ status: number; body: any }> { + const { status, body } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: `An email connector from ${__filename}`, + connector_type_id: '.email', + config: { + service: '__json', + from, + hasAuth: true, + }, + secrets: { + user: 'bob', + password: 'changeme', + }, + }); + + if (status === 200) { + objectRemover.add('default', body.id, 'connector', 'actions'); + } + + return { status, body }; + } + + async function runConnector( + id: string, + to: string[], + cc: string[], + bcc: string[] + ): Promise<{ status: number; body: any }> { + const subject = 'email-subject'; + const message = 'email-message'; + const { status, body } = await supertest + .post(`/api/actions/connector/${id}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ params: { to, cc, bcc, subject, message } }); + + return { status, body }; + } +} + +function addressesFromMessage(message: any, which: 'to' | 'cc' | 'bcc'): string[] { + return addressFieldFromMessage(message, which, 'address'); +} + +function namesFromMessage(message: any, which: 'to' | 'cc' | 'bcc'): string[] { + return addressFieldFromMessage(message, which, 'name'); +} + +function addressFieldFromMessage( + message: any, + which1: 'to' | 'cc' | 'bcc', + which2: 'name' | 'address' +): string[] { + const result: string[] = []; + + const list = message?.[which1]; + if (!Array.isArray(list)) return result; + + return list.map((entry) => `${entry?.[which2]}`).sort(); +} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/index.ts index fc0b23290a865..20498981ac2eb 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/index.ts @@ -22,6 +22,7 @@ export default function actionsTests({ loadTestFile, getService }: FtrProviderCo loadTestFile(require.resolve('./update')); loadTestFile(require.resolve('./execute')); loadTestFile(require.resolve('./enqueue')); + loadTestFile(require.resolve('./builtin_action_types/email')); loadTestFile(require.resolve('./builtin_action_types/es_index')); loadTestFile(require.resolve('./builtin_action_types/webhook')); loadTestFile(require.resolve('./builtin_action_types/preconfigured_alert_history_connector')); diff --git a/yarn.lock b/yarn.lock index bd4e88bf2fcd6..4d1428bc991a4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13094,6 +13094,11 @@ elliptic@^6.0.0: minimalistic-assert "^1.0.1" minimalistic-crypto-utils "^1.0.1" +email-addresses@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/email-addresses/-/email-addresses-5.0.0.tgz#7ae9e7f58eef7d5e3e2c2c2d3ea49b78dc854fa6" + integrity sha512-4OIPYlA6JXqtVn8zpHpGiI7vE6EQOAg16aGnDMIAlZVinnoZ8208tW1hAbjWydgN/4PLTT9q+O1K6AH/vALJGw== + emittery@^0.7.1: version "0.7.1" resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.7.1.tgz#c02375a927a40948c0345cc903072597f5270451" From 823dec208c6e88118d3171c58cac1298e6061522 Mon Sep 17 00:00:00 2001 From: "Joey F. Poon" Date: Tue, 26 Apr 2022 09:34:10 -0500 Subject: [PATCH 13/29] [Security Solution] fix flashing authz on endpoint integration (#130933) --- .../endpoint_package_custom_extension.test.tsx | 12 ++++++++++++ .../endpoint_package_custom_extension.tsx | 12 +++++++++--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/endpoint_package_custom_extension.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/endpoint_package_custom_extension.test.tsx index 6aba0483fc945..eff4671fe2fa0 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/endpoint_package_custom_extension.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/endpoint_package_custom_extension.test.tsx @@ -97,4 +97,16 @@ describe('When displaying the EndpointPackageCustomExtension fleet UI extension' expect(renderResult.queryByTestId('hostIsolationExceptions-fleetCard')).toBeNull(); }); }); + + it('should only show loading spinner if loading', () => { + useEndpointPrivilegesMock.mockReturnValue({ + ...getEndpointPrivilegesInitialStateMock(), + loading: true, + }); + render(); + + expect(renderResult.getByTestId('endpointExtensionLoadingSpinner')).toBeInTheDocument(); + expect(renderResult.queryByTestId('fleetEndpointPackageCustomContent')).toBeNull(); + expect(renderResult.queryByTestId('noIngestPermissions')).toBeNull(); + }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/endpoint_package_custom_extension.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/endpoint_package_custom_extension.tsx index 7700c1f71fbab..72cc9852b0e7d 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/endpoint_package_custom_extension.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/endpoint_package_custom_extension.tsx @@ -6,7 +6,7 @@ */ import React, { memo, useMemo } from 'react'; -import { EuiSpacer } from '@elastic/eui'; +import { EuiSpacer, EuiLoadingSpinner } from '@elastic/eui'; import { PackageCustomExtensionComponentProps } from '@kbn/fleet-plugin/public'; import { useHttp } from '../../../../../../common/lib/kibana'; import { useCanSeeHostIsolationExceptionsMenu } from '../../../../host_isolation_exceptions/view/hooks'; @@ -34,7 +34,7 @@ export const EndpointPackageCustomExtension = memo { const http = useHttp(); const canSeeHostIsolationExceptions = useCanSeeHostIsolationExceptionsMenu(); - const { canAccessEndpointManagement } = useEndpointPrivileges(); + const { loading, canAccessEndpointManagement } = useEndpointPrivileges(); const trustedAppsApiClientInstance = useMemo( () => TrustedAppsApiClient.getInstance(http), @@ -106,7 +106,13 @@ export const EndpointPackageCustomExtension = memo; + return loading ? ( + + ) : canAccessEndpointManagement ? ( + artifactCards + ) : ( + + ); } ); From e0f8fac7ce2193ea796f36ab9783d6104977b7d5 Mon Sep 17 00:00:00 2001 From: Miriam <31922082+MiriamAparicio@users.noreply.github.com> Date: Tue, 26 Apr 2022 16:48:09 +0200 Subject: [PATCH 14/29] add-field-to-correlations-exclude-list (#130950) --- x-pack/plugins/apm/common/correlations/constants.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/apm/common/correlations/constants.ts b/x-pack/plugins/apm/common/correlations/constants.ts index 11b9a9a109dbf..115e50582823a 100644 --- a/x-pack/plugins/apm/common/correlations/constants.ts +++ b/x-pack/plugins/apm/common/correlations/constants.ts @@ -25,6 +25,7 @@ export const FIELDS_TO_EXCLUDE_AS_CANDIDATE = new Set([ 'transaction.id', 'process.pid', 'process.ppid', + 'process.parent.pid', 'processor.event', 'processor.name', 'transaction.sampled', From 5d85976da454f050dd8743178cccf81403753370 Mon Sep 17 00:00:00 2001 From: Kyle Pollich Date: Tue, 26 Apr 2022 10:50:11 -0400 Subject: [PATCH 15/29] [Fleet] Add `cache-control` headers to key `/epm` endpoints in Fleet API (#130921) * Add cache-control header to /categories endpoint * Add cache-control header + includeInstallStatus parameter to /packages endpoint * Add cache-control header parameter to filepath endpoint * Fix installation status type * Fix getLimitedPackages call * Fix cypress tests * Fix checks + integration test * Swap includeInstallStatus -> excludeInstallStatus query parameter Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../plugins/fleet/common/openapi/bundled.yaml | 10 ++++++- .../common/openapi/paths/epm@packages.yaml | 8 ++++- .../plugins/fleet/common/types/models/epm.ts | 5 ++++ .../fleet/common/types/rest_spec/epm.ts | 1 + .../integration/integrations_real.spec.ts | 29 +++++++++++++++++-- .../epm/screens/home/available_packages.tsx | 1 + .../fleet/server/routes/epm/handlers.ts | 17 +++++++---- .../fleet/server/services/epm/packages/get.ts | 26 +++++++++++++++-- .../fleet/server/types/rest_spec/epm.ts | 1 + 9 files changed, 86 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/fleet/common/openapi/bundled.yaml b/x-pack/plugins/fleet/common/openapi/bundled.yaml index 1592f4124fbb7..011a242df7fdc 100644 --- a/x-pack/plugins/fleet/common/openapi/bundled.yaml +++ b/x-pack/plugins/fleet/common/openapi/bundled.yaml @@ -184,7 +184,15 @@ paths: schema: $ref: '#/components/schemas/get_packages_response' operationId: list-all-packages - parameters: [] + parameters: + - in: query + name: includeInstallStatus + schema: + type: boolean + default: false + description: >- + Whether to include the install status of each package. Defaults to + false to allow for caching of package requests. /epm/packages/_bulk: post: summary: Packages - Bulk install diff --git a/x-pack/plugins/fleet/common/openapi/paths/epm@packages.yaml b/x-pack/plugins/fleet/common/openapi/paths/epm@packages.yaml index fe1aadedbcd1c..295ab298745fe 100644 --- a/x-pack/plugins/fleet/common/openapi/paths/epm@packages.yaml +++ b/x-pack/plugins/fleet/common/openapi/paths/epm@packages.yaml @@ -9,4 +9,10 @@ get: schema: $ref: ../components/schemas/get_packages_response.yaml operationId: list-all-packages -parameters: [] +parameters: + - in: query + name: includeInstallStatus + schema: + type: boolean + default: false + description: Whether to include the install status of each package. Defaults to false to allow for caching of package requests. diff --git a/x-pack/plugins/fleet/common/types/models/epm.ts b/x-pack/plugins/fleet/common/types/models/epm.ts index 41564fdb10b3b..5217a6232a18c 100644 --- a/x-pack/plugins/fleet/common/types/models/epm.ts +++ b/x-pack/plugins/fleet/common/types/models/epm.ts @@ -427,12 +427,17 @@ export interface PackageUsageStats { } export type Installable = + | InstallStatusExcluded | InstalledRegistry | Installing | NotInstalled | InstallFailed | InstalledBundled; +export type InstallStatusExcluded = T & { + status: undefined; +}; + export type InstalledRegistry = T & { status: InstallationStatus['Installed']; savedObject: SavedObject; diff --git a/x-pack/plugins/fleet/common/types/rest_spec/epm.ts b/x-pack/plugins/fleet/common/types/rest_spec/epm.ts index f1ccaae05487b..e12bdbb202321 100644 --- a/x-pack/plugins/fleet/common/types/rest_spec/epm.ts +++ b/x-pack/plugins/fleet/common/types/rest_spec/epm.ts @@ -32,6 +32,7 @@ export interface GetPackagesRequest { query: { category?: string; experimental?: boolean; + excludeInstallStatus?: boolean; }; } diff --git a/x-pack/plugins/fleet/cypress/integration/integrations_real.spec.ts b/x-pack/plugins/fleet/cypress/integration/integrations_real.spec.ts index e06b3d3ed5670..9bfd725497ffc 100644 --- a/x-pack/plugins/fleet/cypress/integration/integrations_real.spec.ts +++ b/x-pack/plugins/fleet/cypress/integration/integrations_real.spec.ts @@ -50,7 +50,19 @@ describe('Add Integration - Real API', () => { }); function addAndVerifyIntegration() { - cy.intercept('GET', '/api/fleet/epm/packages?*').as('packages'); + cy.intercept( + '/api/fleet/epm/packages?*', + { + middleware: true, + }, + (req) => { + req.on('before:response', (res) => { + // force all API responses to not be cached + res.headers['cache-control'] = 'no-store'; + }); + } + ).as('packages'); + navigateTo(INTEGRATIONS); cy.wait('@packages'); cy.get('.euiLoadingSpinner').should('not.exist'); @@ -75,7 +87,20 @@ describe('Add Integration - Real API', () => { .map((policy: any) => policy.id); cy.visit(`/app/fleet/policies/${agentPolicyId}`); - cy.intercept('GET', '/api/fleet/epm/packages?*').as('packages'); + + cy.intercept( + '/api/fleet/epm/packages?*', + { + middleware: true, + }, + (req) => { + req.on('before:response', (res) => { + // force all API responses to not be cached + res.headers['cache-control'] = 'no-store'; + }); + } + ).as('packages'); + cy.getBySel(ADD_PACKAGE_POLICY_BTN).click(); cy.wait('@packages'); cy.get('.euiLoadingSpinner').should('not.exist'); diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/available_packages.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/available_packages.tsx index 88343528afa0c..154669409b457 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/available_packages.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/available_packages.tsx @@ -214,6 +214,7 @@ export const AvailablePackages: React.FC = memo(() => { error: eprPackageLoadingError, } = useGetPackages({ category: '', + excludeInstallStatus: true, }); const eprIntegrationList = useMemo( () => packageListToIntegrationsList(eprPackages?.items || []), diff --git a/x-pack/plugins/fleet/server/routes/epm/handlers.ts b/x-pack/plugins/fleet/server/routes/epm/handlers.ts index c5ac0a6c8202e..38c5042b6987e 100644 --- a/x-pack/plugins/fleet/server/routes/epm/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/epm/handlers.ts @@ -10,7 +10,7 @@ import path from 'path'; import type { TypeOf } from '@kbn/config-schema'; import mime from 'mime-types'; import semverValid from 'semver/functions/valid'; -import type { ResponseHeaders, KnownHeaders } from '@kbn/core/server'; +import type { ResponseHeaders, KnownHeaders, HttpResponseOptions } from '@kbn/core/server'; import type { GetInfoResponse, @@ -62,6 +62,10 @@ import { getAsset } from '../../services/epm/archive/storage'; import { getPackageUsageStats } from '../../services/epm/packages/get'; import { updatePackage } from '../../services/epm/packages/update'; +const CACHE_CONTROL_10_MINUTES_HEADER: HttpResponseOptions['headers'] = { + 'cache-control': 'max-age=600', +}; + export const getCategoriesHandler: FleetRequestHandler< undefined, TypeOf @@ -72,7 +76,7 @@ export const getCategoriesHandler: FleetRequestHandler< items: res, response: res, }; - return response.ok({ body }); + return response.ok({ body, headers: { ...CACHE_CONTROL_10_MINUTES_HEADER } }); } catch (error) { return defaultIngestErrorHandler({ error, response }); } @@ -94,6 +98,9 @@ export const getListHandler: FleetRequestHandler< }; return response.ok({ body, + // Only cache responses where the installation status is excluded, otherwise the request + // needs up-to-date information on whether the package is installed so we can't cache it + headers: request.query.excludeInstallStatus ? { ...CACHE_CONTROL_10_MINUTES_HEADER } : {}, }); } catch (error) { return defaultIngestErrorHandler({ error, response }); @@ -164,13 +171,13 @@ export const getFileHandler: FleetRequestHandler< body: buffer, statusCode: 200, headers: { - 'cache-control': 'max-age=10, public', + ...CACHE_CONTROL_10_MINUTES_HEADER, 'content-type': contentType, }, }); } else { const registryResponse = await getFile(pkgName, pkgVersion, filePath); - const headersToProxy: KnownHeaders[] = ['content-type', 'cache-control']; + const headersToProxy: KnownHeaders[] = ['content-type']; const proxiedHeaders = headersToProxy.reduce((headers, knownHeader) => { const value = registryResponse.headers.get(knownHeader); if (value !== null) { @@ -182,7 +189,7 @@ export const getFileHandler: FleetRequestHandler< return response.custom({ body: registryResponse.body, statusCode: registryResponse.status, - headers: proxiedHeaders, + headers: { ...CACHE_CONTROL_10_MINUTES_HEADER, ...proxiedHeaders }, }); } } catch (error) { diff --git a/x-pack/plugins/fleet/server/services/epm/packages/get.ts b/x-pack/plugins/fleet/server/services/epm/packages/get.ts index 0ba42585c1601..27468e77c8e9f 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/get.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/get.ts @@ -45,9 +45,10 @@ export async function getCategories(options: GetCategoriesRequest['query']) { export async function getPackages( options: { savedObjectsClient: SavedObjectsClientContract; + excludeInstallStatus?: boolean; } & Registry.SearchParams ) { - const { savedObjectsClient, experimental, category } = options; + const { savedObjectsClient, experimental, category, excludeInstallStatus = false } = options; const registryItems = await Registry.fetchList({ category, experimental }).then((items) => { return items.map((item) => Object.assign({}, item, { title: item.title || nameAsTitle(item.name) }, { id: item.name }) @@ -63,7 +64,23 @@ export async function getPackages( ) ) .sort(sortByName); - return packageList; + + if (!excludeInstallStatus) { + return packageList; + } + + // Exclude the `installStatus` value if the `excludeInstallStatus` query parameter is set to true + // to better facilitate response caching + const packageListWithoutStatus = packageList.map((pkg) => { + const newPkg = { + ...pkg, + status: undefined, + }; + + return newPkg; + }); + + return packageListWithoutStatus; } // Get package names for packages which cannot have more than one package policy on an agent policy @@ -71,7 +88,10 @@ export async function getLimitedPackages(options: { savedObjectsClient: SavedObjectsClientContract; }): Promise { const { savedObjectsClient } = options; - const allPackages = await getPackages({ savedObjectsClient, experimental: true }); + const allPackages = await getPackages({ + savedObjectsClient, + experimental: true, + }); const installedPackages = allPackages.filter( (pkg) => pkg.status === installationStatuses.Installed ); diff --git a/x-pack/plugins/fleet/server/types/rest_spec/epm.ts b/x-pack/plugins/fleet/server/types/rest_spec/epm.ts index c51a0127c2e29..1385c110f2e4e 100644 --- a/x-pack/plugins/fleet/server/types/rest_spec/epm.ts +++ b/x-pack/plugins/fleet/server/types/rest_spec/epm.ts @@ -18,6 +18,7 @@ export const GetPackagesRequestSchema = { query: schema.object({ category: schema.maybe(schema.string()), experimental: schema.maybe(schema.boolean()), + excludeInstallStatus: schema.maybe(schema.boolean({ defaultValue: false })), }), }; From c052e50b54a79b7f943ac29e8c7b51687f6d9ce8 Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Tue, 26 Apr 2022 11:10:46 -0400 Subject: [PATCH 16/29] [CI] Balance various Default CI Groups (#130932) --- x-pack/test/functional/apps/canvas/index.js | 2 +- .../test/observability_functional/apps/observability/index.ts | 2 +- .../reporting_and_deprecated_security/index.ts | 2 +- .../test/reporting_functional/reporting_and_security/index.ts | 2 +- x-pack/test/security_api_integration/tests/kerberos/index.ts | 2 +- x-pack/test/security_api_integration/tests/pki/index.ts | 2 +- x-pack/test/security_api_integration/tests/saml/index.ts | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/x-pack/test/functional/apps/canvas/index.js b/x-pack/test/functional/apps/canvas/index.js index e4d59a038af74..784aeb6655768 100644 --- a/x-pack/test/functional/apps/canvas/index.js +++ b/x-pack/test/functional/apps/canvas/index.js @@ -27,7 +27,7 @@ export default function canvasApp({ loadTestFile, getService }) { await security.testUser.restoreDefaults(); }); - this.tags('ciGroup2'); // CI requires tags ヽ(゜Q。)ノ? + this.tags('ciGroup2'); loadTestFile(require.resolve('./smoke_test')); loadTestFile(require.resolve('./expression')); loadTestFile(require.resolve('./filters')); diff --git a/x-pack/test/observability_functional/apps/observability/index.ts b/x-pack/test/observability_functional/apps/observability/index.ts index 972067b2a6b68..9ec3791aef35f 100644 --- a/x-pack/test/observability_functional/apps/observability/index.ts +++ b/x-pack/test/observability_functional/apps/observability/index.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('ObservabilityApp', function () { - this.tags('ciGroup6'); + this.tags('ciGroup22'); loadTestFile(require.resolve('./alerts')); loadTestFile(require.resolve('./alerts/add_to_case')); diff --git a/x-pack/test/reporting_functional/reporting_and_deprecated_security/index.ts b/x-pack/test/reporting_functional/reporting_and_deprecated_security/index.ts index a59939deeae0b..4725cb1eae82e 100644 --- a/x-pack/test/reporting_functional/reporting_and_deprecated_security/index.ts +++ b/x-pack/test/reporting_functional/reporting_and_deprecated_security/index.ts @@ -43,7 +43,7 @@ export default function (context: FtrProviderContext) { }; describe('Reporting Functional Tests with Deprecated Security configuration enabled', function () { - this.tags('ciGroup2'); + this.tags('ciGroup20'); before(async () => { const reportingAPI = context.getService('reportingAPI'); diff --git a/x-pack/test/reporting_functional/reporting_and_security/index.ts b/x-pack/test/reporting_functional/reporting_and_security/index.ts index 22057c9be77dc..4b06eb426389e 100644 --- a/x-pack/test/reporting_functional/reporting_and_security/index.ts +++ b/x-pack/test/reporting_functional/reporting_and_security/index.ts @@ -10,7 +10,7 @@ import { FtrProviderContext } from '../ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function ({ getService, loadTestFile }: FtrProviderContext) { describe('Reporting Functional Tests with Security enabled', function () { - this.tags('ciGroup2'); + this.tags('ciGroup20'); before(async () => { const reportingFunctional = getService('reportingFunctional'); diff --git a/x-pack/test/security_api_integration/tests/kerberos/index.ts b/x-pack/test/security_api_integration/tests/kerberos/index.ts index 39aac8cc4ca2f..cec92939a5194 100644 --- a/x-pack/test/security_api_integration/tests/kerberos/index.ts +++ b/x-pack/test/security_api_integration/tests/kerberos/index.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - Kerberos', function () { - this.tags('ciGroup16'); + this.tags('ciGroup31'); loadTestFile(require.resolve('./kerberos_login')); }); diff --git a/x-pack/test/security_api_integration/tests/pki/index.ts b/x-pack/test/security_api_integration/tests/pki/index.ts index 4ec858da28dbd..c3b733d0b31f8 100644 --- a/x-pack/test/security_api_integration/tests/pki/index.ts +++ b/x-pack/test/security_api_integration/tests/pki/index.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - PKI', function () { - this.tags('ciGroup6'); + this.tags('ciGroup22'); loadTestFile(require.resolve('./pki_auth')); }); diff --git a/x-pack/test/security_api_integration/tests/saml/index.ts b/x-pack/test/security_api_integration/tests/saml/index.ts index dbabb835ee980..e7ffdbd410de3 100644 --- a/x-pack/test/security_api_integration/tests/saml/index.ts +++ b/x-pack/test/security_api_integration/tests/saml/index.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - SAML', function () { - this.tags('ciGroup18'); + this.tags('ciGroup27'); loadTestFile(require.resolve('./saml_login')); }); From 799ec1b2d4c6ec4ed1d2ed4ed240d2f275b35d62 Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Tue, 26 Apr 2022 11:11:15 -0400 Subject: [PATCH 17/29] [CI] Various lens test/combobox speedups (#130934) --- test/functional/services/combo_box.ts | 11 ++++++-- .../web_element_wrapper.ts | 28 +++++++++++-------- .../test/functional/page_objects/lens_page.ts | 2 +- 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/test/functional/services/combo_box.ts b/test/functional/services/combo_box.ts index 88201b0ec7e19..8b43364c23a22 100644 --- a/test/functional/services/combo_box.ts +++ b/test/functional/services/combo_box.ts @@ -157,7 +157,7 @@ export class ComboBoxService extends FtrService { * @param comboBoxElement element that wraps up EuiComboBox */ private async waitForOptionsListLoading(comboBoxElement: WebElementWrapper): Promise { - await comboBoxElement.waitForDeletedByCssSelector('.euiLoadingSpinner'); + await comboBoxElement.waitForDeletedByCssSelector('.euiLoadingSpinner', 50); } /** @@ -255,7 +255,9 @@ export class ComboBoxService extends FtrService { * @param comboBoxElement element that wraps up EuiComboBox */ public async closeOptionsList(comboBoxElement: WebElementWrapper): Promise { - const isOptionsListOpen = await this.testSubjects.exists('~comboBoxOptionsList'); + const isOptionsListOpen = await this.testSubjects.exists('~comboBoxOptionsList', { + timeout: 50, + }); if (isOptionsListOpen) { const input = await comboBoxElement.findByTagName('input'); await input.pressKeys(this.browser.keys.ESCAPE); @@ -268,7 +270,10 @@ export class ComboBoxService extends FtrService { * @param comboBoxElement element that wraps up EuiComboBox */ public async openOptionsList(comboBoxElement: WebElementWrapper): Promise { - const isOptionsListOpen = await this.testSubjects.exists('~comboBoxOptionsList'); + const isOptionsListOpen = await this.testSubjects.exists('~comboBoxOptionsList', { + timeout: 50, + }); + if (!isOptionsListOpen) { await this.retry.try(async () => { const toggleBtn = await comboBoxElement.findByTestSubject('comboBoxInput'); diff --git a/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts b/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts index 1a35f7c39aa28..a4f0d40b6db95 100644 --- a/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts +++ b/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts @@ -686,17 +686,23 @@ export class WebElementWrapper { * @param {string} className * @return {Promise} */ - public async waitForDeletedByCssSelector(selector: string): Promise { - await this.driver.manage().setTimeouts({ implicit: 1000 }); - await this.driver.wait( - async () => { - const found = await this._webElement.findElements(this.By.css(selector)); - return found.length === 0; - }, - this.timeout, - `The element with ${selector} selector was still present after ${this.timeout} sec.` - ); - await this.driver.manage().setTimeouts({ implicit: this.timeout }); + public async waitForDeletedByCssSelector( + selector: string, + implicitTimeout = 1000 + ): Promise { + try { + await this.driver.manage().setTimeouts({ implicit: implicitTimeout }); + await this.driver.wait( + async () => { + const found = await this._webElement.findElements(this.By.css(selector)); + return found.length === 0; + }, + this.timeout, + `The element with ${selector} selector was still present after ${this.timeout} sec.` + ); + } finally { + await this.driver.manage().setTimeouts({ implicit: this.timeout }); + } } /** diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index 00bee191a6368..244e69c43f8e3 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -737,7 +737,7 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont }, async openChartSwitchPopover() { - if (await testSubjects.exists('lnsChartSwitchList')) { + if (await testSubjects.exists('lnsChartSwitchList', { timeout: 50 })) { return; } await retry.try(async () => { From 8168e7e888ff554c7d727da2463088e3e45120ba Mon Sep 17 00:00:00 2001 From: Spencer Date: Tue, 26 Apr 2022 08:20:46 -0700 Subject: [PATCH 18/29] [bootstrap] fix windows support (#130938) --- packages/kbn-tooling-log/BUILD.bazel | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/kbn-tooling-log/BUILD.bazel b/packages/kbn-tooling-log/BUILD.bazel index a4953521171cb..e5d196bd4649c 100644 --- a/packages/kbn-tooling-log/BUILD.bazel +++ b/packages/kbn-tooling-log/BUILD.bazel @@ -54,6 +54,7 @@ TYPES_DEPS = [ "@npm//tslib", "@npm//@types/node", "@npm//@types/jest", + "//packages/kbn-jest-serializers:npm_module_types" # needed for windows development, only used in tests ] jsts_transpiler( From 60049fd085b38d608321a5802d7039034596146d Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Tue, 26 Apr 2022 17:25:00 +0200 Subject: [PATCH 19/29] [Discover] Fix adding/removing a filter for a scripted field in Document Explorer view (#130895) --- .../discover_grid_cell_actions.test.tsx | 12 +++++-- .../discover_grid_cell_actions.tsx | 31 +++++++++++-------- 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid_cell_actions.test.tsx b/src/plugins/discover/public/components/discover_grid/discover_grid_cell_actions.test.tsx index c94b660b26893..9a75a74396ff0 100644 --- a/src/plugins/discover/public/components/discover_grid/discover_grid_cell_actions.test.tsx +++ b/src/plugins/discover/public/components/discover_grid/discover_grid_cell_actions.test.tsx @@ -48,7 +48,11 @@ describe('Discover cell actions ', function () { ); const button = findTestSubject(component, 'filterForButton'); await button.simulate('click'); - expect(contextMock.onFilter).toHaveBeenCalledWith('extension', 'jpg', '+'); + expect(contextMock.onFilter).toHaveBeenCalledWith( + indexPatternMock.fields.getByName('extension'), + 'jpg', + '+' + ); }); it('triggers filter function when FilterOutBtn is clicked', async () => { const contextMock = { @@ -76,6 +80,10 @@ describe('Discover cell actions ', function () { ); const button = findTestSubject(component, 'filterOutButton'); await button.simulate('click'); - expect(contextMock.onFilter).toHaveBeenCalledWith('extension', 'jpg', '-'); + expect(contextMock.onFilter).toHaveBeenCalledWith( + indexPatternMock.fields.getByName('extension'), + 'jpg', + '-' + ); }); }); diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid_cell_actions.tsx b/src/plugins/discover/public/components/discover_grid/discover_grid_cell_actions.tsx index 1ac99da2ee8c0..318e1719c08f8 100644 --- a/src/plugins/discover/public/components/discover_grid/discover_grid_cell_actions.tsx +++ b/src/plugins/discover/public/components/discover_grid/discover_grid_cell_actions.tsx @@ -11,7 +11,22 @@ import { EuiDataGridColumnCellActionProps } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { DataViewField } from '@kbn/data-views-plugin/public'; import { flattenHit } from '@kbn/data-plugin/public'; -import { DiscoverGridContext } from './discover_grid_context'; +import { DiscoverGridContext, GridContext } from './discover_grid_context'; + +function onFilterCell( + context: GridContext, + rowIndex: EuiDataGridColumnCellActionProps['rowIndex'], + columnId: EuiDataGridColumnCellActionProps['columnId'], + mode: '+' | '-' +) { + const row = context.rows[rowIndex]; + const flattened = flattenHit(row, context.indexPattern); + const field = context.indexPattern.fields.getByName(columnId); + + if (flattened && field) { + context.onFilter(field, flattened[columnId], mode); + } +} export const FilterInBtn = ({ Component, @@ -27,12 +42,7 @@ export const FilterInBtn = ({ return ( { - const row = context.rows[rowIndex]; - const flattened = flattenHit(row, context.indexPattern); - - if (flattened) { - context.onFilter(columnId, flattened[columnId], '+'); - } + onFilterCell(context, rowIndex, columnId, '+'); }} iconType="plusInCircle" aria-label={buttonTitle} @@ -60,12 +70,7 @@ export const FilterOutBtn = ({ return ( { - const row = context.rows[rowIndex]; - const flattened = flattenHit(row, context.indexPattern); - - if (flattened) { - context.onFilter(columnId, flattened[columnId], '-'); - } + onFilterCell(context, rowIndex, columnId, '-'); }} iconType="minusInCircle" aria-label={buttonTitle} From 7a93538c5a3cb69835bee44cd8bab4715a327e8b Mon Sep 17 00:00:00 2001 From: Cristina Amico Date: Tue, 26 Apr 2022 17:40:06 +0200 Subject: [PATCH 20/29] [Fleet] Remove licence check in UI for upcoming agents rolling upgrade work (#130981) --- .../sections/agents/agent_list_page/index.tsx | 40 ++++++++----------- 1 file changed, 17 insertions(+), 23 deletions(-) diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx index 8e8091d13a794..7b3f1fedb160a 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx @@ -32,7 +32,6 @@ import { useUrlParams, useLink, useBreadcrumbs, - useLicense, useKibanaVersion, useStartServices, } from '../../../hooks'; @@ -153,7 +152,6 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { const { getHref } = useLink(); const defaultKuery: string = (useUrlParams().urlParams.kuery as string) || ''; const hasFleetAllPrivileges = useAuthz().fleet.all; - const isGoldPlus = useLicense().isGoldPlus(); const kibanaVersion = useKibanaVersion(); // Agent data states @@ -667,27 +665,23 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { pageSizeOptions, }} isSelectable={true} - selection={ - isGoldPlus - ? { - onSelectionChange: (newAgents: Agent[]) => { - setSelectedAgents(newAgents); - setSelectionMode('manual'); - }, - selectable: isAgentSelectable, - selectableMessage: (selectable, agent) => { - if (selectable) return ''; - if (!agent.active) { - return 'This agent is not active'; - } - if (agent.policy_id && agentPoliciesIndexedById[agent.policy_id].is_managed) { - return 'This action is not available for agents enrolled in an externally managed agent policy'; - } - return ''; - }, - } - : undefined - } + selection={{ + onSelectionChange: (newAgents: Agent[]) => { + setSelectedAgents(newAgents); + setSelectionMode('manual'); + }, + selectable: isAgentSelectable, + selectableMessage: (selectable, agent) => { + if (selectable) return ''; + if (!agent.active) { + return 'This agent is not active'; + } + if (agent.policy_id && agentPoliciesIndexedById[agent.policy_id].is_managed) { + return 'This action is not available for agents enrolled in an externally managed agent policy'; + } + return ''; + }, + }} onChange={({ page }: { page: { index: number; size: number } }) => { const newPagination = { ...pagination, From fc04b3088a61624d7b3beae1d2e736d8188827b6 Mon Sep 17 00:00:00 2001 From: Ying Mao Date: Tue, 26 Apr 2022 11:47:28 -0400 Subject: [PATCH 21/29] [Alerting] Tracking number of alerts in event log `execute` document and adding telemetry for it. (#130479) * Adding telemetry for number of scheduled actions * Adding percentile by type types * Parsing percentiles by rule type and adding tests * Adding functional tests * Adding alert count fields to event log * wip * Cleanup * Fixing checks * Tests and checks * Adding functional tests * Adding total number of alerts * Adding telemetry for number of alerts * Cleanup * PR feedback Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/alerting/common/rule.ts | 4 - .../alerting/common/rule_task_instance.ts | 30 +- .../server/lib/alert_execution_store.test.ts | 115 ------ .../server/lib/rule_execution_status.test.ts | 112 +++--- .../server/lib/rule_execution_status.ts | 60 ++-- .../server/lib/rule_run_metrics_store.test.ts | 165 +++++++++ ...ion_store.ts => rule_run_metrics_store.ts} | 66 +++- .../server/lib/wrap_scoped_cluster_client.ts | 9 +- .../create_execution_handler.test.ts | 86 ++--- .../task_runner/create_execution_handler.ts | 30 +- .../alerting/server/task_runner/fixtures.ts | 9 + .../server/task_runner/task_runner.test.ts | 85 ++++- .../server/task_runner/task_runner.ts | 142 +++++--- .../task_runner/task_runner_cancel.test.ts | 28 +- .../alerting/server/task_runner/types.ts | 28 +- .../server/usage/alerting_telemetry.test.ts | 78 +++++ .../server/usage/alerting_telemetry.ts | 28 ++ .../server/usage/alerting_usage_collector.ts | 12 + x-pack/plugins/alerting/server/usage/task.ts | 3 + x-pack/plugins/alerting/server/usage/types.ts | 10 + .../plugins/event_log/generated/mappings.json | 12 + x-pack/plugins/event_log/generated/schemas.ts | 4 + x-pack/plugins/event_log/scripts/mappings.js | 12 + .../schema/xpack_plugins.json | 326 ++++++++++++++++++ .../tests/alerting/alerts.ts | 4 + .../tests/telemetry/alerting_telemetry.ts | 74 +++- .../spaces_only/tests/alerting/event_log.ts | 163 ++++++--- 27 files changed, 1264 insertions(+), 431 deletions(-) delete mode 100644 x-pack/plugins/alerting/server/lib/alert_execution_store.test.ts create mode 100644 x-pack/plugins/alerting/server/lib/rule_run_metrics_store.test.ts rename x-pack/plugins/alerting/server/lib/{alert_execution_store.ts => rule_run_metrics_store.ts} (64%) diff --git a/x-pack/plugins/alerting/common/rule.ts b/x-pack/plugins/alerting/common/rule.ts index 3d3b66f18a436..c8f282bf695d7 100644 --- a/x-pack/plugins/alerting/common/rule.ts +++ b/x-pack/plugins/alerting/common/rule.ts @@ -11,7 +11,6 @@ import { SavedObjectsResolveResponse, // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '@kbn/core/server'; -import { RuleExecutionMetrics } from '.'; import { RuleNotifyWhenType } from './rule_notify_when_type'; export type RuleTypeState = Record; @@ -49,9 +48,6 @@ export enum RuleExecutionStatusWarningReasons { export interface RuleExecutionStatus { status: RuleExecutionStatuses; - numberOfTriggeredActions?: number; - numberOfGeneratedActions?: number; - metrics?: RuleExecutionMetrics; lastExecutionDate: Date; lastDuration?: number; error?: { diff --git a/x-pack/plugins/alerting/common/rule_task_instance.ts b/x-pack/plugins/alerting/common/rule_task_instance.ts index 529b65e719ddd..77537bafc4bb9 100644 --- a/x-pack/plugins/alerting/common/rule_task_instance.ts +++ b/x-pack/plugins/alerting/common/rule_task_instance.ts @@ -8,7 +8,11 @@ import * as t from 'io-ts'; import { rawAlertInstance } from './alert_instance'; import { DateFromString } from './date_from_string'; -import { IntervalSchedule, RuleMonitoring } from './rule'; + +export enum ActionsCompletion { + COMPLETE = 'complete', + PARTIAL = 'partial', +} export const ruleStateSchema = t.partial({ alertTypeState: t.record(t.string, t.unknown), @@ -16,24 +20,8 @@ export const ruleStateSchema = t.partial({ previousStartedAt: t.union([t.null, DateFromString]), }); -const ruleExecutionMetricsSchema = t.partial({ - numSearches: t.number, - totalSearchDurationMs: t.number, - esSearchDurationMs: t.number, -}); - -const alertExecutionMetrics = t.partial({ - numberOfTriggeredActions: t.number, - numberOfGeneratedActions: t.number, - triggeredActionsStatus: t.string, -}); - -export type RuleExecutionMetrics = t.TypeOf; +// This is serialized in the rule task document export type RuleTaskState = t.TypeOf; -export type RuleExecutionState = RuleTaskState & { - metrics: RuleExecutionMetrics; - alertExecutionMetrics: t.TypeOf; -}; export const ruleParamsSchema = t.intersection([ t.type({ @@ -44,9 +32,3 @@ export const ruleParamsSchema = t.intersection([ }), ]); export type RuleTaskParams = t.TypeOf; - -export interface RuleExecutionRunResult { - state: RuleExecutionState; - monitoring: RuleMonitoring | undefined; - schedule: IntervalSchedule | undefined; -} diff --git a/x-pack/plugins/alerting/server/lib/alert_execution_store.test.ts b/x-pack/plugins/alerting/server/lib/alert_execution_store.test.ts deleted file mode 100644 index 905ef53cda9db..0000000000000 --- a/x-pack/plugins/alerting/server/lib/alert_execution_store.test.ts +++ /dev/null @@ -1,115 +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 { AlertExecutionStore } from './alert_execution_store'; -import { ActionsCompletion } from '../task_runner/types'; - -describe('AlertExecutionStore', () => { - const alertExecutionStore = new AlertExecutionStore(); - const testConnectorId = 'test-connector-id'; - - // Getter Setter - test('returns the default values if there is no change', () => { - expect(alertExecutionStore.getTriggeredActionsStatus()).toBe(ActionsCompletion.COMPLETE); - expect(alertExecutionStore.getNumberOfTriggeredActions()).toBe(0); - expect(alertExecutionStore.getNumberOfGeneratedActions()).toBe(0); - expect(alertExecutionStore.getStatusByConnectorType('any')).toBe(undefined); - }); - - test('sets and returns numberOfTriggeredActions', () => { - alertExecutionStore.setNumberOfTriggeredActions(5); - expect(alertExecutionStore.getNumberOfTriggeredActions()).toBe(5); - }); - - test('sets and returns numberOfGeneratedActions', () => { - alertExecutionStore.setNumberOfGeneratedActions(15); - expect(alertExecutionStore.getNumberOfGeneratedActions()).toBe(15); - }); - - test('sets and returns triggeredActionsStatusByConnectorType', () => { - alertExecutionStore.setTriggeredActionsStatusByConnectorType({ - actionTypeId: testConnectorId, - status: ActionsCompletion.PARTIAL, - }); - expect( - alertExecutionStore.getStatusByConnectorType(testConnectorId).triggeredActionsStatus - ).toBe(ActionsCompletion.PARTIAL); - expect(alertExecutionStore.getTriggeredActionsStatus()).toBe(ActionsCompletion.PARTIAL); - }); - - // increment - test('increments numberOfTriggeredActions by 1', () => { - alertExecutionStore.incrementNumberOfTriggeredActions(); - expect(alertExecutionStore.getNumberOfTriggeredActions()).toBe(6); - }); - - test('increments incrementNumberOfGeneratedActions by x', () => { - alertExecutionStore.incrementNumberOfGeneratedActions(2); - expect(alertExecutionStore.getNumberOfGeneratedActions()).toBe(17); - }); - - test('increments numberOfTriggeredActionsByConnectorType by 1', () => { - alertExecutionStore.incrementNumberOfTriggeredActionsByConnectorType(testConnectorId); - expect( - alertExecutionStore.getStatusByConnectorType(testConnectorId).numberOfTriggeredActions - ).toBe(1); - }); - - test('increments NumberOfGeneratedActionsByConnectorType by 1', () => { - alertExecutionStore.incrementNumberOfGeneratedActionsByConnectorType(testConnectorId); - expect( - alertExecutionStore.getStatusByConnectorType(testConnectorId).numberOfGeneratedActions - ).toBe(1); - }); - - // Checker - test('checks if it has reached the executable actions limit', () => { - expect(alertExecutionStore.hasReachedTheExecutableActionsLimit({ default: { max: 10 } })).toBe( - false - ); - - expect(alertExecutionStore.hasReachedTheExecutableActionsLimit({ default: { max: 5 } })).toBe( - true - ); - }); - - test('checks if it has reached the executable actions limit by connector type', () => { - alertExecutionStore.incrementNumberOfTriggeredActionsByConnectorType(testConnectorId); - alertExecutionStore.incrementNumberOfTriggeredActionsByConnectorType(testConnectorId); - alertExecutionStore.incrementNumberOfTriggeredActionsByConnectorType(testConnectorId); - alertExecutionStore.incrementNumberOfTriggeredActionsByConnectorType(testConnectorId); - alertExecutionStore.incrementNumberOfTriggeredActionsByConnectorType(testConnectorId); - - expect( - alertExecutionStore.hasReachedTheExecutableActionsLimitByConnectorType({ - actionsConfigMap: { - default: { max: 20 }, - [testConnectorId]: { - max: 5, - }, - }, - actionTypeId: testConnectorId, - }) - ).toBe(true); - - expect( - alertExecutionStore.hasReachedTheExecutableActionsLimitByConnectorType({ - actionsConfigMap: { - default: { max: 20 }, - [testConnectorId]: { - max: 8, - }, - }, - actionTypeId: testConnectorId, - }) - ).toBe(false); - }); - - test('checks if a connector type it has already reached the executable actions limit', () => { - expect(alertExecutionStore.hasConnectorTypeReachedTheLimit(testConnectorId)).toBe(true); - }); -}); diff --git a/x-pack/plugins/alerting/server/lib/rule_execution_status.test.ts b/x-pack/plugins/alerting/server/lib/rule_execution_status.test.ts index 52de76d4b4dd9..8551bb00287c7 100644 --- a/x-pack/plugins/alerting/server/lib/rule_execution_status.test.ts +++ b/x-pack/plugins/alerting/server/lib/rule_execution_status.test.ts @@ -7,9 +7,9 @@ import { loggingSystemMock } from '@kbn/core/server/mocks'; import { + ActionsCompletion, RuleExecutionStatusErrorReasons, RuleExecutionStatusWarningReasons, - RuleExecutionState, } from '../types'; import { executionStatusFromState, @@ -19,98 +19,80 @@ import { } from './rule_execution_status'; import { ErrorWithReason } from './error_with_reason'; import { translations } from '../constants/translations'; -import { ActionsCompletion } from '../task_runner/types'; +import { RuleRunMetrics, RuleRunMetricsStore } from './rule_run_metrics_store'; const MockLogger = loggingSystemMock.create().get(); -const metrics = { numSearches: 1, esSearchDurationMs: 10, totalSearchDurationMs: 20 }; +const executionMetrics = { + numSearches: 1, + esSearchDurationMs: 10, + totalSearchDurationMs: 20, + numberOfTriggeredActions: 32, + numberOfGeneratedActions: 11, + numberOfActiveAlerts: 2, + numberOfNewAlerts: 3, + numberOfRecoveredAlerts: 13, + triggeredActionsStatus: ActionsCompletion.COMPLETE, +}; describe('RuleExecutionStatus', () => { beforeEach(() => { jest.resetAllMocks(); }); + function testExpectedMetrics(received: RuleRunMetrics, expected: RuleRunMetrics) { + expect(received.numSearches).toEqual(expected.numSearches); + expect(received.totalSearchDurationMs).toEqual(expected.totalSearchDurationMs); + expect(received.esSearchDurationMs).toEqual(expected.esSearchDurationMs); + expect(received.numberOfTriggeredActions).toEqual(expected.numberOfTriggeredActions); + expect(received.numberOfGeneratedActions).toEqual(expected.numberOfGeneratedActions); + expect(received.numberOfActiveAlerts).toEqual(expected.numberOfActiveAlerts); + expect(received.numberOfRecoveredAlerts).toEqual(expected.numberOfRecoveredAlerts); + expect(received.numberOfNewAlerts).toEqual(expected.numberOfNewAlerts); + expect(received.triggeredActionsStatus).toEqual(expected.triggeredActionsStatus); + } + describe('executionStatusFromState()', () => { test('empty task state', () => { - const status = executionStatusFromState({ - alertExecutionMetrics: { - numberOfTriggeredActions: 0, - numberOfGeneratedActions: 0, - triggeredActionsStatus: ActionsCompletion.COMPLETE, - }, - } as RuleExecutionState); + const emptyRuleRunState = new RuleRunMetricsStore().getMetrics(); + const { status, metrics } = executionStatusFromState({ metrics: emptyRuleRunState }); checkDateIsNearNow(status.lastExecutionDate); - expect(status.numberOfTriggeredActions).toBe(0); - expect(status.numberOfGeneratedActions).toBe(0); expect(status.status).toBe('ok'); expect(status.error).toBe(undefined); expect(status.warning).toBe(undefined); + + testExpectedMetrics(metrics!, emptyRuleRunState); }); test('task state with no instances', () => { - const status = executionStatusFromState({ + const { status, metrics } = executionStatusFromState({ alertInstances: {}, - alertExecutionMetrics: { - numberOfTriggeredActions: 0, - numberOfGeneratedActions: 0, - triggeredActionsStatus: ActionsCompletion.COMPLETE, - }, - metrics, + metrics: executionMetrics, }); checkDateIsNearNow(status.lastExecutionDate); - expect(status.numberOfTriggeredActions).toBe(0); - expect(status.numberOfGeneratedActions).toBe(0); expect(status.status).toBe('ok'); expect(status.error).toBe(undefined); expect(status.warning).toBe(undefined); - expect(status.metrics).toBe(metrics); + + testExpectedMetrics(metrics!, executionMetrics); }); test('task state with one instance', () => { - const status = executionStatusFromState({ + const { status, metrics } = executionStatusFromState({ alertInstances: { a: {} }, - alertExecutionMetrics: { - numberOfTriggeredActions: 0, - numberOfGeneratedActions: 0, - triggeredActionsStatus: ActionsCompletion.COMPLETE, - }, - metrics, + metrics: executionMetrics, }); checkDateIsNearNow(status.lastExecutionDate); - expect(status.numberOfTriggeredActions).toBe(0); - expect(status.numberOfGeneratedActions).toBe(0); expect(status.status).toBe('active'); expect(status.error).toBe(undefined); expect(status.warning).toBe(undefined); - expect(status.metrics).toBe(metrics); - }); - test('task state with numberOfTriggeredActions', () => { - const status = executionStatusFromState({ - alertExecutionMetrics: { - numberOfTriggeredActions: 1, - numberOfGeneratedActions: 2, - triggeredActionsStatus: ActionsCompletion.COMPLETE, - }, - alertInstances: { a: {} }, - metrics, - }); - checkDateIsNearNow(status.lastExecutionDate); - expect(status.numberOfTriggeredActions).toBe(1); - expect(status.numberOfGeneratedActions).toBe(2); - expect(status.status).toBe('active'); - expect(status.error).toBe(undefined); - expect(status.warning).toBe(undefined); - expect(status.metrics).toBe(metrics); + testExpectedMetrics(metrics!, executionMetrics); }); test('task state with warning', () => { - const status = executionStatusFromState({ + const { status, metrics } = executionStatusFromState({ alertInstances: { a: {} }, - alertExecutionMetrics: { - numberOfTriggeredActions: 3, - triggeredActionsStatus: ActionsCompletion.PARTIAL, - }, - metrics, + metrics: { ...executionMetrics, triggeredActionsStatus: ActionsCompletion.PARTIAL }, }); checkDateIsNearNow(status.lastExecutionDate); expect(status.warning).toEqual({ @@ -119,12 +101,17 @@ describe('RuleExecutionStatus', () => { }); expect(status.status).toBe('warning'); expect(status.error).toBe(undefined); + + testExpectedMetrics(metrics!, { + ...executionMetrics, + triggeredActionsStatus: ActionsCompletion.PARTIAL, + }); }); }); describe('executionStatusFromError()', () => { test('error with no reason', () => { - const status = executionStatusFromError(new Error('boo!')); + const { status, metrics } = executionStatusFromError(new Error('boo!')); expect(status.status).toBe('error'); expect(status.error).toMatchInlineSnapshot(` Object { @@ -132,10 +119,11 @@ describe('RuleExecutionStatus', () => { "reason": "unknown", } `); + expect(metrics).toBeNull(); }); test('error with a reason', () => { - const status = executionStatusFromError( + const { status, metrics } = executionStatusFromError( new ErrorWithReason(RuleExecutionStatusErrorReasons.Execute, new Error('hoo!')) ); expect(status.status).toBe('error'); @@ -145,6 +133,7 @@ describe('RuleExecutionStatus', () => { "reason": "execute", } `); + expect(metrics).toBeNull(); }); }); @@ -195,9 +184,12 @@ describe('RuleExecutionStatus', () => { `); }); - test('status with a numberOfTriggeredActions', () => { + test('status with a alerts and actions counts', () => { expect( - ruleExecutionStatusToRaw({ lastExecutionDate: date, status, numberOfTriggeredActions: 5 }) + ruleExecutionStatusToRaw({ + lastExecutionDate: date, + status, + }) ).toMatchInlineSnapshot(` Object { "error": null, diff --git a/x-pack/plugins/alerting/server/lib/rule_execution_status.ts b/x-pack/plugins/alerting/server/lib/rule_execution_status.ts index 0b9d2520ae455..28eaf24ef8a2d 100644 --- a/x-pack/plugins/alerting/server/lib/rule_execution_status.ts +++ b/x-pack/plugins/alerting/server/lib/rule_execution_status.ts @@ -11,19 +11,27 @@ import { RuleExecutionStatusValues, RuleExecutionStatusWarningReasons, RawRuleExecutionStatus, - RuleExecutionState, } from '../types'; import { getReasonFromError } from './error_with_reason'; import { getEsErrorMessage } from './errors'; -import { RuleExecutionStatuses } from '../../common'; +import { ActionsCompletion, RuleExecutionStatuses } from '../../common'; import { translations } from '../constants/translations'; -import { ActionsCompletion } from '../task_runner/types'; +import { RuleTaskStateAndMetrics } from '../task_runner/types'; +import { RuleRunMetrics } from './rule_run_metrics_store'; -export function executionStatusFromState(state: RuleExecutionState): RuleExecutionStatus { - const alertIds = Object.keys(state.alertInstances ?? {}); +export interface IExecutionStatusAndMetrics { + status: RuleExecutionStatus; + metrics: RuleRunMetrics | null; +} + +export function executionStatusFromState( + stateWithMetrics: RuleTaskStateAndMetrics, + lastExecutionDate?: Date +): IExecutionStatusAndMetrics { + const alertIds = Object.keys(stateWithMetrics.alertInstances ?? {}); const hasIncompleteAlertExecution = - state.alertExecutionMetrics.triggeredActionsStatus === ActionsCompletion.PARTIAL; + stateWithMetrics.metrics.triggeredActionsStatus === ActionsCompletion.PARTIAL; let status: RuleExecutionStatuses = alertIds.length === 0 ? RuleExecutionStatusValues[0] : RuleExecutionStatusValues[1]; @@ -33,28 +41,34 @@ export function executionStatusFromState(state: RuleExecutionState): RuleExecuti } return { - metrics: state.metrics, - numberOfTriggeredActions: state.alertExecutionMetrics.numberOfTriggeredActions, - numberOfGeneratedActions: state.alertExecutionMetrics.numberOfGeneratedActions, - lastExecutionDate: new Date(), - status, - ...(hasIncompleteAlertExecution && { - warning: { - reason: RuleExecutionStatusWarningReasons.MAX_EXECUTABLE_ACTIONS, - message: translations.taskRunner.warning.maxExecutableActions, - }, - }), + status: { + lastExecutionDate: lastExecutionDate ?? new Date(), + status, + ...(hasIncompleteAlertExecution && { + warning: { + reason: RuleExecutionStatusWarningReasons.MAX_EXECUTABLE_ACTIONS, + message: translations.taskRunner.warning.maxExecutableActions, + }, + }), + }, + metrics: stateWithMetrics.metrics, }; } -export function executionStatusFromError(error: Error): RuleExecutionStatus { +export function executionStatusFromError( + error: Error, + lastExecutionDate?: Date +): IExecutionStatusAndMetrics { return { - lastExecutionDate: new Date(), - status: 'error', - error: { - reason: getReasonFromError(error), - message: getEsErrorMessage(error), + status: { + lastExecutionDate: lastExecutionDate ?? new Date(), + status: 'error', + error: { + reason: getReasonFromError(error), + message: getEsErrorMessage(error), + }, }, + metrics: null, }; } diff --git a/x-pack/plugins/alerting/server/lib/rule_run_metrics_store.test.ts b/x-pack/plugins/alerting/server/lib/rule_run_metrics_store.test.ts new file mode 100644 index 0000000000000..b27dc37a459e0 --- /dev/null +++ b/x-pack/plugins/alerting/server/lib/rule_run_metrics_store.test.ts @@ -0,0 +1,165 @@ +/* + * 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 { RuleRunMetricsStore } from './rule_run_metrics_store'; +import { ActionsCompletion } from '../types'; + +describe('RuleRunMetricsStore', () => { + const ruleRunMetricsStore = new RuleRunMetricsStore(); + const testConnectorId = 'test-connector-id'; + + // Getter Setter + test('returns the default values if there is no change', () => { + expect(ruleRunMetricsStore.getTriggeredActionsStatus()).toBe(ActionsCompletion.COMPLETE); + expect(ruleRunMetricsStore.getNumSearches()).toBe(0); + expect(ruleRunMetricsStore.getTotalSearchDurationMs()).toBe(0); + expect(ruleRunMetricsStore.getEsSearchDurationMs()).toBe(0); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toBe(0); + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toBe(0); + expect(ruleRunMetricsStore.getNumberOfActiveAlerts()).toBe(0); + expect(ruleRunMetricsStore.getNumberOfRecoveredAlerts()).toBe(0); + expect(ruleRunMetricsStore.getNumberOfNewAlerts()).toBe(0); + expect(ruleRunMetricsStore.getStatusByConnectorType('any')).toBe(undefined); + }); + + test('sets and returns numSearches', () => { + ruleRunMetricsStore.setNumSearches(1); + expect(ruleRunMetricsStore.getNumSearches()).toBe(1); + }); + + test('sets and returns totalSearchDurationMs', () => { + ruleRunMetricsStore.setTotalSearchDurationMs(2); + expect(ruleRunMetricsStore.getTotalSearchDurationMs()).toBe(2); + }); + + test('sets and returns esSearchDurationMs', () => { + ruleRunMetricsStore.setEsSearchDurationMs(3); + expect(ruleRunMetricsStore.getEsSearchDurationMs()).toBe(3); + }); + + test('sets and returns numberOfTriggeredActions', () => { + ruleRunMetricsStore.setNumberOfTriggeredActions(5); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toBe(5); + }); + + test('sets and returns numberOfGeneratedActions', () => { + ruleRunMetricsStore.setNumberOfGeneratedActions(15); + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toBe(15); + }); + + test('sets and returns numberOfActiveAlerts', () => { + ruleRunMetricsStore.setNumberOfActiveAlerts(10); + expect(ruleRunMetricsStore.getNumberOfActiveAlerts()).toBe(10); + }); + + test('sets and returns numberOfRecoveredAlerts', () => { + ruleRunMetricsStore.setNumberOfRecoveredAlerts(11); + expect(ruleRunMetricsStore.getNumberOfRecoveredAlerts()).toBe(11); + }); + + test('sets and returns numberOfNewAlerts', () => { + ruleRunMetricsStore.setNumberOfNewAlerts(12); + expect(ruleRunMetricsStore.getNumberOfNewAlerts()).toBe(12); + }); + + test('sets and returns triggeredActionsStatusByConnectorType', () => { + ruleRunMetricsStore.setTriggeredActionsStatusByConnectorType({ + actionTypeId: testConnectorId, + status: ActionsCompletion.PARTIAL, + }); + expect( + ruleRunMetricsStore.getStatusByConnectorType(testConnectorId).triggeredActionsStatus + ).toBe(ActionsCompletion.PARTIAL); + expect(ruleRunMetricsStore.getTriggeredActionsStatus()).toBe(ActionsCompletion.PARTIAL); + }); + + test('gets metrics', () => { + expect(ruleRunMetricsStore.getMetrics()).toEqual({ + triggeredActionsStatus: 'partial', + esSearchDurationMs: 3, + numSearches: 1, + numberOfActiveAlerts: 10, + numberOfGeneratedActions: 15, + numberOfNewAlerts: 12, + numberOfRecoveredAlerts: 11, + numberOfTriggeredActions: 5, + totalSearchDurationMs: 2, + }); + }); + + // increment + test('increments numberOfTriggeredActions by 1', () => { + ruleRunMetricsStore.incrementNumberOfTriggeredActions(); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toBe(6); + }); + + test('increments incrementNumberOfGeneratedActions by x', () => { + ruleRunMetricsStore.incrementNumberOfGeneratedActions(2); + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toBe(17); + }); + + test('increments numberOfTriggeredActionsByConnectorType by 1', () => { + ruleRunMetricsStore.incrementNumberOfTriggeredActionsByConnectorType(testConnectorId); + expect( + ruleRunMetricsStore.getStatusByConnectorType(testConnectorId).numberOfTriggeredActions + ).toBe(1); + }); + + test('increments NumberOfGeneratedActionsByConnectorType by 1', () => { + ruleRunMetricsStore.incrementNumberOfGeneratedActionsByConnectorType(testConnectorId); + expect( + ruleRunMetricsStore.getStatusByConnectorType(testConnectorId).numberOfGeneratedActions + ).toBe(1); + }); + + // Checker + test('checks if it has reached the executable actions limit', () => { + expect(ruleRunMetricsStore.hasReachedTheExecutableActionsLimit({ default: { max: 10 } })).toBe( + false + ); + + expect(ruleRunMetricsStore.hasReachedTheExecutableActionsLimit({ default: { max: 5 } })).toBe( + true + ); + }); + + test('checks if it has reached the executable actions limit by connector type', () => { + ruleRunMetricsStore.incrementNumberOfTriggeredActionsByConnectorType(testConnectorId); + ruleRunMetricsStore.incrementNumberOfTriggeredActionsByConnectorType(testConnectorId); + ruleRunMetricsStore.incrementNumberOfTriggeredActionsByConnectorType(testConnectorId); + ruleRunMetricsStore.incrementNumberOfTriggeredActionsByConnectorType(testConnectorId); + ruleRunMetricsStore.incrementNumberOfTriggeredActionsByConnectorType(testConnectorId); + + expect( + ruleRunMetricsStore.hasReachedTheExecutableActionsLimitByConnectorType({ + actionsConfigMap: { + default: { max: 20 }, + [testConnectorId]: { + max: 5, + }, + }, + actionTypeId: testConnectorId, + }) + ).toBe(true); + + expect( + ruleRunMetricsStore.hasReachedTheExecutableActionsLimitByConnectorType({ + actionsConfigMap: { + default: { max: 20 }, + [testConnectorId]: { + max: 8, + }, + }, + actionTypeId: testConnectorId, + }) + ).toBe(false); + }); + + test('checks if a connector type it has already reached the executable actions limit', () => { + expect(ruleRunMetricsStore.hasConnectorTypeReachedTheLimit(testConnectorId)).toBe(true); + }); +}); diff --git a/x-pack/plugins/alerting/server/lib/alert_execution_store.ts b/x-pack/plugins/alerting/server/lib/rule_run_metrics_store.ts similarity index 64% rename from x-pack/plugins/alerting/server/lib/alert_execution_store.ts rename to x-pack/plugins/alerting/server/lib/rule_run_metrics_store.ts index b601a76b809a4..97fa14214e4f7 100644 --- a/x-pack/plugins/alerting/server/lib/alert_execution_store.ts +++ b/x-pack/plugins/alerting/server/lib/rule_run_metrics_store.ts @@ -6,12 +6,18 @@ */ import { set } from 'lodash'; +import { ActionsCompletion } from '../types'; import { ActionsConfigMap } from './get_actions_config_map'; -import { ActionsCompletion } from '../task_runner/types'; interface State { + numSearches: number; + totalSearchDurationMs: number; + esSearchDurationMs: number; numberOfTriggeredActions: number; numberOfGeneratedActions: number; + numberOfActiveAlerts: number; + numberOfRecoveredAlerts: number; + numberOfNewAlerts: number; connectorTypes: { [key: string]: { triggeredActionsStatus: ActionsCompletion; @@ -21,10 +27,19 @@ interface State { }; } -export class AlertExecutionStore { +export type RuleRunMetrics = Omit & { + triggeredActionsStatus: ActionsCompletion; +}; +export class RuleRunMetricsStore { private state: State = { + numSearches: 0, + totalSearchDurationMs: 0, + esSearchDurationMs: 0, numberOfTriggeredActions: 0, numberOfGeneratedActions: 0, + numberOfActiveAlerts: 0, + numberOfRecoveredAlerts: 0, + numberOfNewAlerts: 0, connectorTypes: {}, }; @@ -35,25 +50,66 @@ export class AlertExecutionStore { ); return hasPartial ? ActionsCompletion.PARTIAL : ActionsCompletion.COMPLETE; }; + public getNumSearches = () => { + return this.state.numSearches; + }; + public getTotalSearchDurationMs = () => { + return this.state.totalSearchDurationMs; + }; + public getEsSearchDurationMs = () => { + return this.state.esSearchDurationMs; + }; public getNumberOfTriggeredActions = () => { return this.state.numberOfTriggeredActions; }; public getNumberOfGeneratedActions = () => { return this.state.numberOfGeneratedActions; }; + public getNumberOfActiveAlerts = () => { + return this.state.numberOfActiveAlerts; + }; + public getNumberOfRecoveredAlerts = () => { + return this.state.numberOfRecoveredAlerts; + }; + public getNumberOfNewAlerts = () => { + return this.state.numberOfNewAlerts; + }; public getStatusByConnectorType = (actionTypeId: string) => { return this.state.connectorTypes[actionTypeId]; }; + public getMetrics = (): RuleRunMetrics => { + const { connectorTypes, ...metrics } = this.state; + return { + ...metrics, + triggeredActionsStatus: this.getTriggeredActionsStatus(), + }; + }; // Setters + public setNumSearches = (numSearches: number) => { + this.state.numSearches = numSearches; + }; + public setTotalSearchDurationMs = (totalSearchDurationMs: number) => { + this.state.totalSearchDurationMs = totalSearchDurationMs; + }; + public setEsSearchDurationMs = (esSearchDurationMs: number) => { + this.state.esSearchDurationMs = esSearchDurationMs; + }; public setNumberOfTriggeredActions = (numberOfTriggeredActions: number) => { this.state.numberOfTriggeredActions = numberOfTriggeredActions; }; - public setNumberOfGeneratedActions = (numberOfGeneratedActions: number) => { this.state.numberOfGeneratedActions = numberOfGeneratedActions; }; - + public setNumberOfActiveAlerts = (numberOfActiveAlerts: number) => { + this.state.numberOfActiveAlerts = numberOfActiveAlerts; + }; + public setNumberOfRecoveredAlerts = (numberOfRecoveredAlerts: number) => { + this.state.numberOfRecoveredAlerts = numberOfRecoveredAlerts; + }; + public setNumberOfNewAlerts = (numberOfNewAlerts: number) => { + this.state.numberOfNewAlerts = numberOfNewAlerts; + }; public setTriggeredActionsStatusByConnectorType = ({ actionTypeId, status, @@ -90,11 +146,9 @@ export class AlertExecutionStore { public incrementNumberOfTriggeredActions = () => { this.state.numberOfTriggeredActions++; }; - public incrementNumberOfGeneratedActions = (incrementBy: number) => { this.state.numberOfGeneratedActions += incrementBy; }; - public incrementNumberOfTriggeredActionsByConnectorType = (actionTypeId: string) => { const currentVal = this.state.connectorTypes[actionTypeId]?.numberOfTriggeredActions || 0; set(this.state, `connectorTypes["${actionTypeId}"].numberOfTriggeredActions`, currentVal + 1); diff --git a/x-pack/plugins/alerting/server/lib/wrap_scoped_cluster_client.ts b/x-pack/plugins/alerting/server/lib/wrap_scoped_cluster_client.ts index af77191474d89..28c5301e9a8b9 100644 --- a/x-pack/plugins/alerting/server/lib/wrap_scoped_cluster_client.ts +++ b/x-pack/plugins/alerting/server/lib/wrap_scoped_cluster_client.ts @@ -21,10 +21,15 @@ import type { AggregationsAggregate, } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { IScopedClusterClient, ElasticsearchClient, Logger } from '@kbn/core/server'; -import { RuleExecutionMetrics } from '../types'; import { Rule } from '../types'; +import { RuleRunMetrics } from './rule_run_metrics_store'; type RuleInfo = Pick & { spaceId: string }; +type SearchMetrics = Pick< + RuleRunMetrics, + 'numSearches' | 'totalSearchDurationMs' | 'esSearchDurationMs' +>; + interface WrapScopedClusterClientFactoryOpts { scopedClusterClient: IScopedClusterClient; rule: RuleInfo; @@ -61,7 +66,7 @@ export function createWrappedScopedClusterClientFactory(opts: WrapScopedClusterC return { client: () => wrappedClient, - getMetrics: (): RuleExecutionMetrics => { + getMetrics: (): SearchMetrics => { return { esSearchDurationMs, totalSearchDurationMs, diff --git a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts index 0a51bc06b6e43..81f7fa7da02d2 100644 --- a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts @@ -6,7 +6,7 @@ */ import { createExecutionHandler } from './create_execution_handler'; -import { ActionsCompletion, CreateExecutionHandlerOptions } from './types'; +import { CreateExecutionHandlerOptions } from './types'; import { loggingSystemMock } from '@kbn/core/server/mocks'; import { actionsClientMock, @@ -18,8 +18,14 @@ import { KibanaRequest } from '@kbn/core/server'; import { asSavedObjectExecutionSource } from '@kbn/actions-plugin/server'; import { InjectActionParamsOpts } from './inject_action_params'; import { NormalizedRuleType } from '../rule_type_registry'; -import { AlertInstanceContext, AlertInstanceState, RuleTypeParams, RuleTypeState } from '../types'; -import { AlertExecutionStore } from '../lib/alert_execution_store'; +import { + ActionsCompletion, + AlertInstanceContext, + AlertInstanceState, + RuleTypeParams, + RuleTypeState, +} from '../types'; +import { RuleRunMetricsStore } from '../lib/rule_run_metrics_store'; jest.mock('./inject_action_params', () => ({ injectActionParams: jest.fn(), @@ -105,7 +111,7 @@ const createExecutionHandlerParams: jest.Mocked< }, }, }; -let alertExecutionStore: AlertExecutionStore; +let ruleRunMetricsStore: RuleRunMetricsStore; describe('Create Execution Handler', () => { beforeEach(() => { @@ -121,7 +127,7 @@ describe('Create Execution Handler', () => { mockActionsPlugin.renderActionParameterTemplates.mockImplementation( renderActionParameterTemplatesDefault ); - alertExecutionStore = new AlertExecutionStore(); + ruleRunMetricsStore = new RuleRunMetricsStore(); }); test('enqueues execution per selected action', async () => { @@ -131,10 +137,10 @@ describe('Create Execution Handler', () => { state: {}, context: {}, alertId: '2', - alertExecutionStore, + ruleRunMetricsStore, }); - expect(alertExecutionStore.getNumberOfTriggeredActions()).toBe(1); - expect(alertExecutionStore.getNumberOfGeneratedActions()).toBe(1); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toBe(1); + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toBe(1); expect(mockActionsPlugin.getActionsClientWithRequest).toHaveBeenCalledWith( createExecutionHandlerParams.request ); @@ -242,7 +248,7 @@ describe('Create Execution Handler', () => { }, }); - expect(alertExecutionStore.getTriggeredActionsStatus()).toBe(ActionsCompletion.COMPLETE); + expect(ruleRunMetricsStore.getTriggeredActionsStatus()).toBe(ActionsCompletion.COMPLETE); }); test(`doesn't call actionsPlugin.execute for disabled actionTypes`, async () => { @@ -280,10 +286,10 @@ describe('Create Execution Handler', () => { state: {}, context: {}, alertId: '2', - alertExecutionStore, + ruleRunMetricsStore, }); - expect(alertExecutionStore.getNumberOfTriggeredActions()).toBe(1); - expect(alertExecutionStore.getNumberOfGeneratedActions()).toBe(2); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toBe(1); + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toBe(2); expect(actionsClient.enqueueExecution).toHaveBeenCalledTimes(1); expect(actionsClient.enqueueExecution).toHaveBeenCalledWith({ consumer: 'rule-consumer', @@ -344,10 +350,10 @@ describe('Create Execution Handler', () => { state: {}, context: {}, alertId: '2', - alertExecutionStore, + ruleRunMetricsStore, }); - expect(alertExecutionStore.getNumberOfTriggeredActions()).toBe(0); - expect(alertExecutionStore.getNumberOfGeneratedActions()).toBe(2); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toBe(0); + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toBe(2); expect(actionsClient.enqueueExecution).toHaveBeenCalledTimes(0); mockActionsPlugin.isActionExecutable.mockImplementation(() => true); @@ -360,7 +366,7 @@ describe('Create Execution Handler', () => { state: {}, context: {}, alertId: '2', - alertExecutionStore, + ruleRunMetricsStore, }); expect(actionsClient.enqueueExecution).toHaveBeenCalledTimes(1); }); @@ -372,10 +378,10 @@ describe('Create Execution Handler', () => { state: {}, context: {}, alertId: '2', - alertExecutionStore, + ruleRunMetricsStore, }); - expect(alertExecutionStore.getNumberOfTriggeredActions()).toBe(0); - expect(alertExecutionStore.getNumberOfGeneratedActions()).toBe(0); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toBe(0); + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toBe(0); expect(actionsClient.enqueueExecution).not.toHaveBeenCalled(); }); @@ -386,10 +392,10 @@ describe('Create Execution Handler', () => { context: { value: 'context-val' }, state: {}, alertId: '2', - alertExecutionStore, + ruleRunMetricsStore, }); - expect(alertExecutionStore.getNumberOfTriggeredActions()).toBe(1); - expect(alertExecutionStore.getNumberOfGeneratedActions()).toBe(1); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toBe(1); + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toBe(1); expect(actionsClient.enqueueExecution).toHaveBeenCalledTimes(1); expect(actionsClient.enqueueExecution.mock.calls[0]).toMatchInlineSnapshot(` Array [ @@ -432,7 +438,7 @@ describe('Create Execution Handler', () => { context: {}, state: { value: 'state-val' }, alertId: '2', - alertExecutionStore, + ruleRunMetricsStore, }); expect(actionsClient.enqueueExecution).toHaveBeenCalledTimes(1); expect(actionsClient.enqueueExecution.mock.calls[0]).toMatchInlineSnapshot(` @@ -478,15 +484,15 @@ describe('Create Execution Handler', () => { context: {}, state: {}, alertId: '2', - alertExecutionStore, + ruleRunMetricsStore, }); expect(createExecutionHandlerParams.logger.error).toHaveBeenCalledWith( 'Invalid action group "invalid-group" for rule "test".' ); - expect(alertExecutionStore.getNumberOfTriggeredActions()).toBe(0); - expect(alertExecutionStore.getNumberOfGeneratedActions()).toBe(0); - expect(alertExecutionStore.getTriggeredActionsStatus()).toBe(ActionsCompletion.COMPLETE); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toBe(0); + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toBe(0); + expect(ruleRunMetricsStore.getTriggeredActionsStatus()).toBe(ActionsCompletion.COMPLETE); }); test('Stops triggering actions when the number of total triggered actions is reached the number of max executable actions', async () => { @@ -531,19 +537,19 @@ describe('Create Execution Handler', () => { ], }); - alertExecutionStore = new AlertExecutionStore(); + ruleRunMetricsStore = new RuleRunMetricsStore(); await executionHandler({ actionGroup: 'default', context: {}, state: { value: 'state-val' }, alertId: '2', - alertExecutionStore, + ruleRunMetricsStore, }); - expect(alertExecutionStore.getNumberOfTriggeredActions()).toBe(2); - expect(alertExecutionStore.getNumberOfGeneratedActions()).toBe(3); - expect(alertExecutionStore.getTriggeredActionsStatus()).toBe(ActionsCompletion.PARTIAL); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toBe(2); + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toBe(3); + expect(ruleRunMetricsStore.getTriggeredActionsStatus()).toBe(ActionsCompletion.PARTIAL); expect(createExecutionHandlerParams.logger.debug).toHaveBeenCalledTimes(1); expect(actionsClient.enqueueExecution).toHaveBeenCalledTimes(2); }); @@ -604,27 +610,27 @@ describe('Create Execution Handler', () => { ], }); - alertExecutionStore = new AlertExecutionStore(); + ruleRunMetricsStore = new RuleRunMetricsStore(); await executionHandler({ actionGroup: 'default', context: {}, state: { value: 'state-val' }, alertId: '2', - alertExecutionStore, + ruleRunMetricsStore, }); - expect(alertExecutionStore.getNumberOfTriggeredActions()).toBe(4); - expect(alertExecutionStore.getNumberOfGeneratedActions()).toBe(5); - expect(alertExecutionStore.getStatusByConnectorType('test').numberOfTriggeredActions).toBe(1); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toBe(4); + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toBe(5); + expect(ruleRunMetricsStore.getStatusByConnectorType('test').numberOfTriggeredActions).toBe(1); expect( - alertExecutionStore.getStatusByConnectorType('test-action-type-id').numberOfTriggeredActions + ruleRunMetricsStore.getStatusByConnectorType('test-action-type-id').numberOfTriggeredActions ).toBe(1); expect( - alertExecutionStore.getStatusByConnectorType('another-action-type-id') + ruleRunMetricsStore.getStatusByConnectorType('another-action-type-id') .numberOfTriggeredActions ).toBe(2); - expect(alertExecutionStore.getTriggeredActionsStatus()).toBe(ActionsCompletion.PARTIAL); + expect(ruleRunMetricsStore.getTriggeredActionsStatus()).toBe(ActionsCompletion.PARTIAL); expect(actionsClient.enqueueExecution).toHaveBeenCalledTimes(4); }); }); diff --git a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts index bc0e92c954f23..ce212a3cbff1b 100644 --- a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts +++ b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts @@ -10,11 +10,17 @@ import { isEphemeralTaskRejectedDueToCapacityError } from '@kbn/task-manager-plu import { transformActionParams } from './transform_action_params'; import { EVENT_LOG_ACTIONS } from '../plugin'; import { injectActionParams } from './inject_action_params'; -import { AlertInstanceContext, AlertInstanceState, RuleTypeParams, RuleTypeState } from '../types'; +import { + ActionsCompletion, + AlertInstanceContext, + AlertInstanceState, + RuleTypeParams, + RuleTypeState, +} from '../types'; import { UntypedNormalizedRuleType } from '../rule_type_registry'; import { createAlertEventLogRecordObject } from '../lib/create_alert_event_log_record_object'; -import { ActionsCompletion, CreateExecutionHandlerOptions, ExecutionHandlerOptions } from './types'; +import { CreateExecutionHandlerOptions, ExecutionHandlerOptions } from './types'; export type ExecutionHandler = ( options: ExecutionHandlerOptions @@ -65,7 +71,7 @@ export function createExecutionHandler< actionSubgroup, context, state, - alertExecutionStore, + ruleRunMetricsStore, alertId, }: ExecutionHandlerOptions) => { if (!ruleTypeActionGroups.has(actionGroup)) { @@ -109,7 +115,7 @@ export function createExecutionHandler< }), })); - alertExecutionStore.incrementNumberOfGeneratedActions(actions.length); + ruleRunMetricsStore.incrementNumberOfGeneratedActions(actions.length); const ruleLabel = `${ruleType.id}:${ruleId}: '${ruleName}'`; @@ -119,10 +125,10 @@ export function createExecutionHandler< for (const action of actions) { const { actionTypeId } = action; - alertExecutionStore.incrementNumberOfGeneratedActionsByConnectorType(actionTypeId); + ruleRunMetricsStore.incrementNumberOfGeneratedActionsByConnectorType(actionTypeId); - if (alertExecutionStore.hasReachedTheExecutableActionsLimit(actionsConfigMap)) { - alertExecutionStore.setTriggeredActionsStatusByConnectorType({ + if (ruleRunMetricsStore.hasReachedTheExecutableActionsLimit(actionsConfigMap)) { + ruleRunMetricsStore.setTriggeredActionsStatusByConnectorType({ actionTypeId, status: ActionsCompletion.PARTIAL, }); @@ -133,17 +139,17 @@ export function createExecutionHandler< } if ( - alertExecutionStore.hasReachedTheExecutableActionsLimitByConnectorType({ + ruleRunMetricsStore.hasReachedTheExecutableActionsLimitByConnectorType({ actionTypeId, actionsConfigMap, }) ) { - if (!alertExecutionStore.hasConnectorTypeReachedTheLimit(actionTypeId)) { + if (!ruleRunMetricsStore.hasConnectorTypeReachedTheLimit(actionTypeId)) { logger.debug( `Rule "${ruleId}" skipped scheduling action "${action.id}" because the maximum number of allowed actions for connector type ${actionTypeId} has been reached.` ); } - alertExecutionStore.setTriggeredActionsStatusByConnectorType({ + ruleRunMetricsStore.setTriggeredActionsStatusByConnectorType({ actionTypeId, status: ActionsCompletion.PARTIAL, }); @@ -157,8 +163,8 @@ export function createExecutionHandler< continue; } - alertExecutionStore.incrementNumberOfTriggeredActions(); - alertExecutionStore.incrementNumberOfTriggeredActionsByConnectorType(actionTypeId); + ruleRunMetricsStore.incrementNumberOfTriggeredActions(); + ruleRunMetricsStore.incrementNumberOfTriggeredActionsByConnectorType(actionTypeId); const namespace = spaceId === 'default' ? {} : { namespace: spaceId }; diff --git a/x-pack/plugins/alerting/server/task_runner/fixtures.ts b/x-pack/plugins/alerting/server/task_runner/fixtures.ts index fae450ac7e60d..74a121e9c8026 100644 --- a/x-pack/plugins/alerting/server/task_runner/fixtures.ts +++ b/x-pack/plugins/alerting/server/task_runner/fixtures.ts @@ -212,6 +212,9 @@ export const generateEventLog = ({ status, numberOfTriggeredActions, numberOfGeneratedActions, + numberOfActiveAlerts, + numberOfRecoveredAlerts, + numberOfNewAlerts, savedObjects = [generateAlertSO('1')], }: GeneratorParams = {}) => ({ ...(status === 'error' && { @@ -239,6 +242,12 @@ export const generateEventLog = ({ metrics: { number_of_triggered_actions: numberOfTriggeredActions, number_of_generated_actions: numberOfGeneratedActions, + number_of_active_alerts: numberOfActiveAlerts ?? 0, + number_of_new_alerts: numberOfNewAlerts ?? 0, + number_of_recovered_alerts: numberOfRecoveredAlerts ?? 0, + total_number_of_alerts: + ((numberOfActiveAlerts ?? 0) as number) + + ((numberOfRecoveredAlerts ?? 0) as number), number_of_searches: 3, es_search_duration_ms: 33, total_search_duration_ms: 23423, diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts index 4a5be740949ad..111c0a7689504 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts @@ -236,11 +236,15 @@ describe('Task Runner', () => { expect(call.services).toBeTruthy(); const logger = taskRunnerFactoryInitializerParams.logger; - expect(logger.debug).toHaveBeenCalledTimes(3); + expect(logger.debug).toHaveBeenCalledTimes(4); expect(logger.debug).nthCalledWith(1, 'executing rule test:1 at 1970-01-01T00:00:00.000Z'); expect(logger.debug).nthCalledWith( 2, - 'ruleExecutionStatus for test:1: {"metrics":{"numSearches":3,"esSearchDurationMs":33,"totalSearchDurationMs":23423},"numberOfTriggeredActions":0,"numberOfGeneratedActions":0,"lastExecutionDate":"1970-01-01T00:00:00.000Z","status":"ok"}' + 'ruleRunStatus for test:1: {"lastExecutionDate":"1970-01-01T00:00:00.000Z","status":"ok"}' + ); + expect(logger.debug).nthCalledWith( + 3, + 'ruleRunMetrics for test:1: {"numSearches":3,"totalSearchDurationMs":23423,"esSearchDurationMs":33,"numberOfTriggeredActions":0,"numberOfGeneratedActions":0,"numberOfActiveAlerts":0,"numberOfRecoveredAlerts":0,"numberOfNewAlerts":0,"triggeredActionsStatus":"complete"}' ); const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; @@ -312,7 +316,7 @@ describe('Task Runner', () => { expect(enqueueFunction).toHaveBeenCalledWith(generateEnqueueFunctionInput()); const logger = customTaskRunnerFactoryInitializerParams.logger; - expect(logger.debug).toHaveBeenCalledTimes(4); + expect(logger.debug).toHaveBeenCalledTimes(5); expect(logger.debug).nthCalledWith(1, 'executing rule test:1 at 1970-01-01T00:00:00.000Z'); expect(logger.debug).nthCalledWith( 2, @@ -320,7 +324,11 @@ describe('Task Runner', () => { ); expect(logger.debug).nthCalledWith( 3, - 'ruleExecutionStatus for test:1: {"metrics":{"numSearches":3,"esSearchDurationMs":33,"totalSearchDurationMs":23423},"numberOfTriggeredActions":1,"numberOfGeneratedActions":1,"lastExecutionDate":"1970-01-01T00:00:00.000Z","status":"active"}' + 'ruleRunStatus for test:1: {"lastExecutionDate":"1970-01-01T00:00:00.000Z","status":"active"}' + ); + expect(logger.debug).nthCalledWith( + 4, + 'ruleRunMetrics for test:1: {"numSearches":3,"totalSearchDurationMs":23423,"esSearchDurationMs":33,"numberOfTriggeredActions":1,"numberOfGeneratedActions":1,"numberOfActiveAlerts":1,"numberOfRecoveredAlerts":0,"numberOfNewAlerts":1,"triggeredActionsStatus":"complete"}' ); const eventLogger = customTaskRunnerFactoryInitializerParams.eventLogger; @@ -377,6 +385,8 @@ describe('Task Runner', () => { status: 'active', numberOfTriggeredActions: 1, numberOfGeneratedActions: 1, + numberOfActiveAlerts: 1, + numberOfNewAlerts: 1, task: true, consumer: 'bar', }) @@ -416,7 +426,7 @@ describe('Task Runner', () => { expect(actionsClient.ephemeralEnqueuedExecution).toHaveBeenCalledTimes(0); const logger = taskRunnerFactoryInitializerParams.logger; - expect(logger.debug).toHaveBeenCalledTimes(5); + expect(logger.debug).toHaveBeenCalledTimes(6); expect(logger.debug).nthCalledWith(1, 'executing rule test:1 at 1970-01-01T00:00:00.000Z'); expect(logger.debug).nthCalledWith( 2, @@ -428,7 +438,11 @@ describe('Task Runner', () => { ); expect(logger.debug).nthCalledWith( 4, - 'ruleExecutionStatus for test:1: {"metrics":{"numSearches":3,"esSearchDurationMs":33,"totalSearchDurationMs":23423},"numberOfTriggeredActions":0,"numberOfGeneratedActions":0,"lastExecutionDate":"1970-01-01T00:00:00.000Z","status":"active"}' + 'ruleRunStatus for test:1: {"lastExecutionDate":"1970-01-01T00:00:00.000Z","status":"active"}' + ); + expect(logger.debug).nthCalledWith( + 5, + 'ruleRunMetrics for test:1: {"numSearches":3,"totalSearchDurationMs":23423,"esSearchDurationMs":33,"numberOfTriggeredActions":0,"numberOfGeneratedActions":0,"numberOfActiveAlerts":1,"numberOfRecoveredAlerts":0,"numberOfNewAlerts":1,"triggeredActionsStatus":"complete"}' ); const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; @@ -472,6 +486,8 @@ describe('Task Runner', () => { status: 'active', numberOfTriggeredActions: 0, numberOfGeneratedActions: 0, + numberOfActiveAlerts: 1, + numberOfNewAlerts: 1, task: true, consumer: 'bar', }) @@ -581,7 +597,7 @@ describe('Task Runner', () => { expect(enqueueFunction).toHaveBeenCalledTimes(1); const logger = customTaskRunnerFactoryInitializerParams.logger; - expect(logger.debug).toHaveBeenCalledTimes(5); + expect(logger.debug).toHaveBeenCalledTimes(6); expect(logger.debug).nthCalledWith(1, 'executing rule test:1 at 1970-01-01T00:00:00.000Z'); expect(logger.debug).nthCalledWith( 2, @@ -593,7 +609,11 @@ describe('Task Runner', () => { ); expect(logger.debug).nthCalledWith( 4, - 'ruleExecutionStatus for test:1: {"metrics":{"numSearches":3,"esSearchDurationMs":33,"totalSearchDurationMs":23423},"numberOfTriggeredActions":1,"numberOfGeneratedActions":1,"lastExecutionDate":"1970-01-01T00:00:00.000Z","status":"active"}' + 'ruleRunStatus for test:1: {"lastExecutionDate":"1970-01-01T00:00:00.000Z","status":"active"}' + ); + expect(logger.debug).nthCalledWith( + 5, + 'ruleRunMetrics for test:1: {"numSearches":3,"totalSearchDurationMs":23423,"esSearchDurationMs":33,"numberOfTriggeredActions":1,"numberOfGeneratedActions":1,"numberOfActiveAlerts":2,"numberOfRecoveredAlerts":0,"numberOfNewAlerts":2,"triggeredActionsStatus":"complete"}' ); expect(mockUsageCounter.incrementCounter).not.toHaveBeenCalled(); } @@ -698,7 +718,7 @@ describe('Task Runner', () => { await taskRunner.run(); expect(enqueueFunction).toHaveBeenCalledTimes(1); const logger = customTaskRunnerFactoryInitializerParams.logger; - expect(logger.debug).toHaveBeenCalledTimes(5); + expect(logger.debug).toHaveBeenCalledTimes(6); expect(logger.debug).nthCalledWith( 3, `skipping scheduling of actions for '2' in rule test:1: '${RULE_NAME}': rule is muted` @@ -783,6 +803,7 @@ describe('Task Runner', () => { status: 'active', numberOfTriggeredActions: 0, numberOfGeneratedActions: 0, + numberOfActiveAlerts: 1, task: true, consumer: 'bar', }) @@ -848,6 +869,7 @@ describe('Task Runner', () => { status: 'active', numberOfTriggeredActions: 1, numberOfGeneratedActions: 1, + numberOfActiveAlerts: 1, task: true, consumer: 'bar', }) @@ -922,6 +944,7 @@ describe('Task Runner', () => { status: 'active', numberOfTriggeredActions: 1, numberOfGeneratedActions: 1, + numberOfActiveAlerts: 1, task: true, consumer: 'bar', }) @@ -1040,6 +1063,8 @@ describe('Task Runner', () => { status: 'active', numberOfTriggeredActions: 1, numberOfGeneratedActions: 1, + numberOfActiveAlerts: 1, + numberOfNewAlerts: 1, task: true, consumer: 'bar', }) @@ -1109,7 +1134,7 @@ describe('Task Runner', () => { ); const logger = customTaskRunnerFactoryInitializerParams.logger; - expect(logger.debug).toHaveBeenCalledTimes(5); + expect(logger.debug).toHaveBeenCalledTimes(6); expect(logger.debug).nthCalledWith(1, 'executing rule test:1 at 1970-01-01T00:00:00.000Z'); expect(logger.debug).nthCalledWith( 2, @@ -1121,7 +1146,11 @@ describe('Task Runner', () => { ); expect(logger.debug).nthCalledWith( 4, - 'ruleExecutionStatus for test:1: {"metrics":{"numSearches":3,"esSearchDurationMs":33,"totalSearchDurationMs":23423},"numberOfTriggeredActions":2,"numberOfGeneratedActions":2,"lastExecutionDate":"1970-01-01T00:00:00.000Z","status":"active"}' + 'ruleRunStatus for test:1: {"lastExecutionDate":"1970-01-01T00:00:00.000Z","status":"active"}' + ); + expect(logger.debug).nthCalledWith( + 5, + 'ruleRunMetrics for test:1: {"numSearches":3,"totalSearchDurationMs":23423,"esSearchDurationMs":33,"numberOfTriggeredActions":2,"numberOfGeneratedActions":2,"numberOfActiveAlerts":1,"numberOfRecoveredAlerts":1,"numberOfNewAlerts":0,"triggeredActionsStatus":"complete"}' ); const eventLogger = customTaskRunnerFactoryInitializerParams.eventLogger; @@ -1189,6 +1218,8 @@ describe('Task Runner', () => { status: 'active', numberOfTriggeredActions: 2, numberOfGeneratedActions: 2, + numberOfActiveAlerts: 1, + numberOfRecoveredAlerts: 1, task: true, consumer: 'bar', }) @@ -1262,7 +1293,11 @@ describe('Task Runner', () => { ); expect(logger.debug).nthCalledWith( 4, - `ruleExecutionStatus for test:${alertId}: {"metrics":{"numSearches":3,"esSearchDurationMs":33,"totalSearchDurationMs":23423},"numberOfTriggeredActions":2,"numberOfGeneratedActions":2,"lastExecutionDate":"1970-01-01T00:00:00.000Z","status":"active"}` + `ruleRunStatus for test:${alertId}: {"lastExecutionDate":"1970-01-01T00:00:00.000Z","status":"active"}` + ); + expect(logger.debug).nthCalledWith( + 5, + `ruleRunMetrics for test:${alertId}: {"numSearches":3,"totalSearchDurationMs":23423,"esSearchDurationMs":33,"numberOfTriggeredActions":2,"numberOfGeneratedActions":2,"numberOfActiveAlerts":1,"numberOfRecoveredAlerts":1,"numberOfNewAlerts":0,"triggeredActionsStatus":"complete"}` ); const eventLogger = customTaskRunnerFactoryInitializerParams.eventLogger; @@ -1451,6 +1486,8 @@ describe('Task Runner', () => { status: 'active', numberOfTriggeredActions: 0, numberOfGeneratedActions: 2, + numberOfActiveAlerts: 1, + numberOfRecoveredAlerts: 1, task: true, consumer: 'bar', }) @@ -1458,7 +1495,7 @@ describe('Task Runner', () => { expect(mockUsageCounter.incrementCounter).not.toHaveBeenCalled(); }); - test('validates params before executing the alert type', async () => { + test('validates params before running the rule type', async () => { const taskRunner = new TaskRunner( { ...ruleType, @@ -1546,7 +1583,7 @@ describe('Task Runner', () => { expect(mockUsageCounter.incrementCounter).not.toHaveBeenCalled(); }); - test('rescheduled the Alert if the schedule has update during a task run', async () => { + test('rescheduled the rule if the schedule has update during a task run', async () => { const taskRunner = new TaskRunner( ruleType, mockedTaskInstance, @@ -2057,6 +2094,8 @@ describe('Task Runner', () => { status: 'active', numberOfTriggeredActions: 0, numberOfGeneratedActions: 0, + numberOfActiveAlerts: 2, + numberOfNewAlerts: 2, task: true, consumer: 'bar', }) @@ -2160,6 +2199,7 @@ describe('Task Runner', () => { status: 'active', numberOfTriggeredActions: 0, numberOfGeneratedActions: 0, + numberOfActiveAlerts: 2, task: true, consumer: 'bar', }) @@ -2252,6 +2292,7 @@ describe('Task Runner', () => { consumer: 'bar', numberOfTriggeredActions: 0, numberOfGeneratedActions: 0, + numberOfActiveAlerts: 2, task: true, }) ); @@ -2342,6 +2383,7 @@ describe('Task Runner', () => { consumer: 'bar', numberOfTriggeredActions: 0, numberOfGeneratedActions: 0, + numberOfRecoveredAlerts: 2, task: true, }) ); @@ -2428,6 +2470,7 @@ describe('Task Runner', () => { consumer: 'bar', numberOfTriggeredActions: 0, numberOfGeneratedActions: 0, + numberOfRecoveredAlerts: 2, task: true, }) ); @@ -2485,11 +2528,15 @@ describe('Task Runner', () => { expect(call.services).toBeTruthy(); const logger = taskRunnerFactoryInitializerParams.logger; - expect(logger.debug).toHaveBeenCalledTimes(3); + expect(logger.debug).toHaveBeenCalledTimes(4); expect(logger.debug).nthCalledWith(1, 'executing rule test:1 at 1970-01-01T00:00:00.000Z'); expect(logger.debug).nthCalledWith( 2, - 'ruleExecutionStatus for test:1: {"metrics":{"numSearches":3,"esSearchDurationMs":33,"totalSearchDurationMs":23423},"numberOfTriggeredActions":0,"numberOfGeneratedActions":0,"lastExecutionDate":"1970-01-01T00:00:00.000Z","status":"ok"}' + 'ruleRunStatus for test:1: {"lastExecutionDate":"1970-01-01T00:00:00.000Z","status":"ok"}' + ); + expect(logger.debug).nthCalledWith( + 3, + 'ruleRunMetrics for test:1: {"numSearches":3,"totalSearchDurationMs":23423,"esSearchDurationMs":33,"numberOfTriggeredActions":0,"numberOfGeneratedActions":0,"numberOfActiveAlerts":0,"numberOfRecoveredAlerts":0,"numberOfNewAlerts":0,"triggeredActionsStatus":"complete"}' ); const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; @@ -2751,7 +2798,7 @@ describe('Task Runner', () => { ); const logger = taskRunnerFactoryInitializerParams.logger; - expect(logger.debug).toHaveBeenCalledTimes(5); + expect(logger.debug).toHaveBeenCalledTimes(6); expect(logger.debug).nthCalledWith( 3, @@ -2833,6 +2880,8 @@ describe('Task Runner', () => { status: 'warning', numberOfTriggeredActions: actionsConfigMap.default.max, numberOfGeneratedActions: mockActions.length, + numberOfActiveAlerts: 1, + numberOfNewAlerts: 1, reason: RuleExecutionStatusWarningReasons.MAX_EXECUTABLE_ACTIONS, task: true, consumer: 'bar', @@ -2961,7 +3010,7 @@ describe('Task Runner', () => { ); const logger = taskRunnerFactoryInitializerParams.logger; - expect(logger.debug).toHaveBeenCalledTimes(5); + expect(logger.debug).toHaveBeenCalledTimes(6); expect(logger.debug).nthCalledWith( 3, 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 00c62af65f382..6c161332bb58b 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.ts @@ -34,8 +34,6 @@ import { RawAlertInstance, RawRule, RawRuleExecutionStatus, - RuleExecutionRunResult, - RuleExecutionState, RuleMonitoring, RuleMonitoringHistory, RuleTaskState, @@ -71,8 +69,11 @@ import { RuleTaskRunResult, ScheduleActionsForRecoveredAlertsParams, TrackAlertDurationsParams, + RuleRunResult, + RuleTaskStateAndMetrics, } from './types'; -import { AlertExecutionStore } from '../lib/alert_execution_store'; +import { IExecutionStatusAndMetrics } from '../lib/rule_execution_status'; +import { RuleRunMetricsStore } from '../lib/rule_run_metrics_store'; const FALLBACK_RETRY_INTERVAL = '5m'; const CONNECTIVITY_RETRY_INTERVAL = '5m'; @@ -290,7 +291,7 @@ export class TaskRunner< alertId: string, alert: Alert, executionHandler: ExecutionHandler, - alertExecutionStore: AlertExecutionStore + ruleRunMetricsStore: RuleRunMetricsStore ) { const { actionGroup, @@ -306,18 +307,18 @@ export class TaskRunner< context, state, alertId, - alertExecutionStore, + ruleRunMetricsStore, }); } - private async executeAlerts( + private async executeRule( fakeRequest: KibanaRequest, rule: SanitizedRule, params: Params, executionHandler: ExecutionHandler, spaceId: string, event: Event - ): Promise { + ): Promise { const { alertTypeId, consumer, @@ -451,7 +452,12 @@ export class TaskRunner< name: rule.name, }; + const ruleRunMetricsStore = new RuleRunMetricsStore(); + const searchMetrics = wrappedScopedClusterClient.getMetrics(); + ruleRunMetricsStore.setNumSearches(searchMetrics.numSearches); + ruleRunMetricsStore.setTotalSearchDurationMs(searchMetrics.totalSearchDurationMs); + ruleRunMetricsStore.setEsSearchDurationMs(searchMetrics.esSearchDurationMs); // Cleanup alerts that are no longer scheduling actions to avoid over populating the alertInstances object const alertsWithScheduledActions = pickBy( @@ -488,11 +494,10 @@ export class TaskRunner< ruleType, rule, spaceId, + ruleRunMetricsStore, }); } - const alertExecutionStore = new AlertExecutionStore(); - const ruleIsSnoozed = this.isRuleSnoozed(rule); if (!ruleIsSnoozed && this.shouldLogAndScheduleActionsForAlerts()) { const mutedAlertIdsSet = new Set(mutedInstanceIds); @@ -527,7 +532,7 @@ export class TaskRunner< await Promise.all( alertsWithExecutableActions.map( ([alertId, alert]: [string, Alert]) => - this.executeAlert(alertId, alert, executionHandler, alertExecutionStore) + this.executeAlert(alertId, alert, executionHandler, ruleRunMetricsStore) ) ); @@ -542,7 +547,7 @@ export class TaskRunner< mutedAlertIdsSet, logger: this.logger, ruleLabel, - alertExecutionStore, + ruleRunMetricsStore, }); } else { if (ruleIsSnoozed) { @@ -562,12 +567,7 @@ export class TaskRunner< } return { - metrics: searchMetrics, - alertExecutionMetrics: { - numberOfTriggeredActions: alertExecutionStore.getNumberOfTriggeredActions(), - numberOfGeneratedActions: alertExecutionStore.getNumberOfGeneratedActions(), - triggeredActionsStatus: alertExecutionStore.getTriggeredActionsStatus(), - }, + metrics: ruleRunMetricsStore.getMetrics(), alertTypeState: updatedRuleTypeState || undefined, alertInstances: mapValues< Record>, @@ -599,12 +599,10 @@ export class TaskRunner< rule.params, fakeRequest ); - return this.executeAlerts(fakeRequest, rule, validatedParams, executionHandler, spaceId, event); + return this.executeRule(fakeRequest, rule, validatedParams, executionHandler, spaceId, event); } - private async loadRuleAttributesAndRun( - event: Event - ): Promise> { + private async loadRuleAttributesAndRun(event: Event): Promise> { const { params: { alertId: ruleId, spaceId }, } = this.taskInstance; @@ -670,7 +668,7 @@ export class TaskRunner< } return { monitoring: asOk(rule.monitoring), - state: await promiseResult( + stateWithMetrics: await promiseResult( this.validateAndExecuteRule(fakeRequest, apiKey, rule, event) ), schedule: asOk( @@ -751,7 +749,7 @@ export class TaskRunner< eventLogger.logEvent(startEvent); - const { state, schedule, monitoring } = await errorAsRuleTaskRunResult( + const { stateWithMetrics, schedule, monitoring } = await errorAsRuleTaskRunResult( this.loadRuleAttributesAndRun(event) ); @@ -760,10 +758,14 @@ export class TaskRunner< return getDefaultRuleMonitoring(); }) ?? getDefaultRuleMonitoring(); - const executionStatus = map( - state, - (ruleExecutionState) => executionStatusFromState(ruleExecutionState), - (err: ElasticsearchError) => executionStatusFromError(err) + const { status: executionStatus, metrics: executionMetrics } = map< + RuleTaskStateAndMetrics, + ElasticsearchError, + IExecutionStatusAndMetrics + >( + stateWithMetrics, + (ruleRunStateWithMetrics) => executionStatusFromState(ruleRunStateWithMetrics, runDate), + (err: ElasticsearchError) => executionStatusFromError(err, runDate) ); // set the executionStatus date to same as event, if it's set if (event.event?.start) { @@ -779,8 +781,13 @@ export class TaskRunner< } this.logger.debug( - `ruleExecutionStatus for ${this.ruleType.id}:${ruleId}: ${JSON.stringify(executionStatus)}` + `ruleRunStatus for ${this.ruleType.id}:${ruleId}: ${JSON.stringify(executionStatus)}` ); + if (executionMetrics) { + this.logger.debug( + `ruleRunMetrics for ${this.ruleType.id}:${ruleId}: ${JSON.stringify(executionMetrics)}` + ); + } eventLogger.stopTiming(event); set(event, 'kibana.alerting.status', executionStatus.status); @@ -815,34 +822,57 @@ export class TaskRunner< set(event, 'event.reason', executionStatus.warning?.reason || 'unknown'); set(event, 'message', executionStatus.warning?.message || event?.message); } - set( - event, - 'kibana.alert.rule.execution.metrics.number_of_triggered_actions', - executionStatus.numberOfTriggeredActions - ); - set( - event, - 'kibana.alert.rule.execution.metrics.number_of_generated_actions', - executionStatus.numberOfGeneratedActions - ); + if (executionMetrics) { + set( + event, + 'kibana.alert.rule.execution.metrics.number_of_triggered_actions', + executionMetrics.numberOfTriggeredActions + ); + set( + event, + 'kibana.alert.rule.execution.metrics.number_of_generated_actions', + executionMetrics.numberOfGeneratedActions + ); + set( + event, + 'kibana.alert.rule.execution.metrics.number_of_active_alerts', + executionMetrics.numberOfActiveAlerts + ); + set( + event, + 'kibana.alert.rule.execution.metrics.number_of_new_alerts', + executionMetrics.numberOfNewAlerts + ); + set( + event, + 'kibana.alert.rule.execution.metrics.total_number_of_alerts', + (executionMetrics.numberOfActiveAlerts ?? 0) + + (executionMetrics.numberOfRecoveredAlerts ?? 0) + ); + set( + event, + 'kibana.alert.rule.execution.metrics.number_of_recovered_alerts', + executionMetrics.numberOfRecoveredAlerts + ); + } } // Copy search stats into event log - if (executionStatus.metrics) { + if (executionMetrics) { set( event, 'kibana.alert.rule.execution.metrics.number_of_searches', - executionStatus.metrics.numSearches ?? 0 + executionMetrics.numSearches ?? 0 ); set( event, 'kibana.alert.rule.execution.metrics.es_search_duration_ms', - executionStatus.metrics.esSearchDurationMs ?? 0 + executionMetrics.esSearchDurationMs ?? 0 ); set( event, 'kibana.alert.rule.execution.metrics.total_search_duration_ms', - executionStatus.metrics.totalSearchDurationMs ?? 0 + executionMetrics.totalSearchDurationMs ?? 0 ); } @@ -870,19 +900,20 @@ export class TaskRunner< }); } - const transformExecutionStateToTaskState = ( - executionState: RuleExecutionState + const transformRunStateToTaskState = ( + runStateWithMetrics: RuleTaskStateAndMetrics ): RuleTaskState => { return { - ...omit(executionState, ['alertExecutionMetrics', 'metrics']), + ...omit(runStateWithMetrics, ['metrics']), previousStartedAt: startedAt, }; }; return { - state: map( - state, - (executionState: RuleExecutionState) => transformExecutionStateToTaskState(executionState), + state: map( + stateWithMetrics, + (ruleRunStateWithMetrics: RuleTaskStateAndMetrics) => + transformRunStateToTaskState(ruleRunStateWithMetrics), (err: ElasticsearchError) => { const message = `Executing Rule ${spaceId}:${ this.ruleType.id @@ -1070,6 +1101,7 @@ function generateNewAndRecoveredAlertEvents< rule, ruleType, spaceId, + ruleRunMetricsStore, } = params; const originalAlertIds = Object.keys(originalAlerts); const currentAlertIds = Object.keys(currentAlerts); @@ -1082,6 +1114,10 @@ function generateNewAndRecoveredAlertEvents< }); } + ruleRunMetricsStore.setNumberOfActiveAlerts(currentAlertIds.length); + ruleRunMetricsStore.setNumberOfNewAlerts(newIds.length); + ruleRunMetricsStore.setNumberOfRecoveredAlerts(recoveredAlertIds.length); + for (const id of recoveredAlertIds) { const { group: actionGroup, subgroup: actionSubgroup } = recoveredAlerts[id].getLastScheduledActions() ?? {}; @@ -1198,7 +1234,7 @@ async function scheduleActionsForRecoveredAlerts< executionHandler, mutedAlertIdsSet, ruleLabel, - alertExecutionStore, + ruleRunMetricsStore, } = params; const recoveredIds = Object.keys(recoveredAlerts); @@ -1216,7 +1252,7 @@ async function scheduleActionsForRecoveredAlerts< context: alert.getContext(), state: {}, alertId: id, - alertExecutionStore, + ruleRunMetricsStore, }); alert.scheduleActions(recoveryActionGroup.id); } @@ -1281,13 +1317,13 @@ function logActiveAndRecoveredAlerts< * so that we can treat each field independantly */ async function errorAsRuleTaskRunResult( - future: Promise> -): Promise> { + future: Promise> +): Promise> { try { return await future; } catch (e) { return { - state: asErr(e), + stateWithMetrics: asErr(e), schedule: asErr(e), monitoring: asErr(e), }; diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts index e0b7449d09b41..4986359115377 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts @@ -334,6 +334,10 @@ describe('Task Runner Cancel', () => { number_of_searches: 3, number_of_triggered_actions: 0, number_of_generated_actions: 0, + number_of_active_alerts: 0, + number_of_new_alerts: 0, + number_of_recovered_alerts: 0, + total_number_of_alerts: 0, es_search_duration_ms: 33, total_search_duration_ms: 23423, }, @@ -499,7 +503,7 @@ describe('Task Runner Cancel', () => { await promise; const logger = taskRunnerFactoryInitializerParams.logger; - expect(logger.debug).toHaveBeenCalledTimes(7); + expect(logger.debug).toHaveBeenCalledTimes(8); expect(logger.debug).nthCalledWith(1, 'executing rule test:1 at 1970-01-01T00:00:00.000Z'); expect(logger.debug).nthCalledWith( 2, @@ -523,7 +527,11 @@ describe('Task Runner Cancel', () => { ); expect(logger.debug).nthCalledWith( 7, - 'ruleExecutionStatus for test:1: {"metrics":{"numSearches":3,"esSearchDurationMs":33,"totalSearchDurationMs":23423},"numberOfTriggeredActions":0,"numberOfGeneratedActions":0,"lastExecutionDate":"1970-01-01T00:00:00.000Z","status":"active"}' + 'ruleRunStatus for test:1: {"lastExecutionDate":"1970-01-01T00:00:00.000Z","status":"active"}' + ); + expect(logger.debug).nthCalledWith( + 8, + 'ruleRunMetrics for test:1: {"numSearches":3,"totalSearchDurationMs":23423,"esSearchDurationMs":33,"numberOfTriggeredActions":0,"numberOfGeneratedActions":0,"numberOfActiveAlerts":0,"numberOfRecoveredAlerts":0,"numberOfNewAlerts":0,"triggeredActionsStatus":"complete"}' ); const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; @@ -618,6 +626,10 @@ describe('Task Runner Cancel', () => { number_of_searches: 3, number_of_triggered_actions: 0, number_of_generated_actions: 0, + number_of_active_alerts: 0, + number_of_recovered_alerts: 0, + number_of_new_alerts: 0, + total_number_of_alerts: 0, es_search_duration_ms: 33, total_search_duration_ms: 23423, }, @@ -663,7 +675,7 @@ describe('Task Runner Cancel', () => { function testActionsExecute() { const logger = taskRunnerFactoryInitializerParams.logger; - expect(logger.debug).toHaveBeenCalledTimes(6); + expect(logger.debug).toHaveBeenCalledTimes(7); expect(logger.debug).nthCalledWith(1, 'executing rule test:1 at 1970-01-01T00:00:00.000Z'); expect(logger.debug).nthCalledWith( 2, @@ -683,7 +695,11 @@ describe('Task Runner Cancel', () => { ); expect(logger.debug).nthCalledWith( 6, - 'ruleExecutionStatus for test:1: {"metrics":{"numSearches":3,"esSearchDurationMs":33,"totalSearchDurationMs":23423},"numberOfTriggeredActions":1,"numberOfGeneratedActions":1,"lastExecutionDate":"1970-01-01T00:00:00.000Z","status":"active"}' + 'ruleRunStatus for test:1: {"lastExecutionDate":"1970-01-01T00:00:00.000Z","status":"active"}' + ); + expect(logger.debug).nthCalledWith( + 7, + 'ruleRunMetrics for test:1: {"numSearches":3,"totalSearchDurationMs":23423,"esSearchDurationMs":33,"numberOfTriggeredActions":1,"numberOfGeneratedActions":1,"numberOfActiveAlerts":1,"numberOfRecoveredAlerts":0,"numberOfNewAlerts":1,"triggeredActionsStatus":"complete"}' ); const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; @@ -897,6 +913,10 @@ describe('Task Runner Cancel', () => { number_of_searches: 3, number_of_triggered_actions: 1, number_of_generated_actions: 1, + number_of_active_alerts: 1, + number_of_new_alerts: 1, + number_of_recovered_alerts: 0, + total_number_of_alerts: 1, es_search_duration_ms: 33, total_search_duration_ms: 23423, }, diff --git a/x-pack/plugins/alerting/server/task_runner/types.ts b/x-pack/plugins/alerting/server/task_runner/types.ts index 246e7721d6ed4..1f4a31fa1d9ac 100644 --- a/x-pack/plugins/alerting/server/task_runner/types.ts +++ b/x-pack/plugins/alerting/server/task_runner/types.ts @@ -18,7 +18,6 @@ import { RuleTypeParams, RuleTypeState, IntervalSchedule, - RuleExecutionState, RuleMonitoring, RuleTaskState, SanitizedRule, @@ -28,13 +27,7 @@ import { NormalizedRuleType } from '../rule_type_registry'; import { ExecutionHandler } from './create_execution_handler'; import { RawRule } from '../types'; import { ActionsConfigMap } from '../lib/get_actions_config_map'; -import { AlertExecutionStore } from '../lib/alert_execution_store'; - -export interface RuleTaskRunResultWithActions { - state: RuleExecutionState; - monitoring: RuleMonitoring | undefined; - schedule: IntervalSchedule | undefined; -} +import { RuleRunMetrics, RuleRunMetricsStore } from '../lib/rule_run_metrics_store'; export interface RuleTaskRunResult { state: RuleTaskState; @@ -42,6 +35,15 @@ export interface RuleTaskRunResult { schedule: IntervalSchedule | undefined; } +// This is the state of the alerting task after rule execution, which includes run metrics plus the task state +export type RuleTaskStateAndMetrics = RuleTaskState & { + metrics: RuleRunMetrics; +}; + +export type RuleRunResult = Pick & { + stateWithMetrics: RuleTaskStateAndMetrics; +}; + export interface RuleTaskInstance extends ConcreteTaskInstance { state: RuleTaskState; } @@ -82,6 +84,7 @@ export interface GenerateNewAndRecoveredAlertEventsParams< >; rule: SanitizedRule; spaceId: string; + ruleRunMetricsStore: RuleRunMetricsStore; } export interface ScheduleActionsForRecoveredAlertsParams< @@ -95,7 +98,7 @@ export interface ScheduleActionsForRecoveredAlertsParams< executionHandler: ExecutionHandler; mutedAlertIdsSet: Set; ruleLabel: string; - alertExecutionStore: AlertExecutionStore; + ruleRunMetricsStore: RuleRunMetricsStore; } export interface LogActiveAndRecoveredAlertsParams< @@ -156,10 +159,5 @@ export interface ExecutionHandlerOptions { alertId: string; context: AlertInstanceContext; state: AlertInstanceState; - alertExecutionStore: AlertExecutionStore; -} - -export enum ActionsCompletion { - COMPLETE = 'complete', - PARTIAL = 'partial', + ruleRunMetricsStore: RuleRunMetricsStore; } diff --git a/x-pack/plugins/alerting/server/usage/alerting_telemetry.test.ts b/x-pack/plugins/alerting/server/usage/alerting_telemetry.test.ts index e43283e14e822..2989bc6a47015 100644 --- a/x-pack/plugins/alerting/server/usage/alerting_telemetry.test.ts +++ b/x-pack/plugins/alerting/server/usage/alerting_telemetry.test.ts @@ -189,6 +189,13 @@ Object { '99.0': 26.0, }, }, + percentileAlerts: { + values: { + '50.0': 10.0, + '90.0': 22.0, + '99.0': 22.0, + }, + }, aggsByType: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, @@ -203,6 +210,13 @@ Object { '99.0': 26.0, }, }, + percentileAlerts: { + values: { + '50.0': 10.0, + '90.0': 22.0, + '99.0': 22.0, + }, + }, }, { key: 'logs.alert.document.count', @@ -214,6 +228,13 @@ Object { '99.0': 10.0, }, }, + percentileAlerts: { + values: { + '50.0': 5.0, + '90.0': 13.0, + '99.0': 13.0, + }, + }, }, ], }, @@ -283,6 +304,25 @@ Object { logs__alert__document__count: 10, }, }, + alertsPercentiles: { + p50: 10, + p90: 22, + p99: 22, + }, + alertsPercentilesByType: { + p50: { + '__index-threshold': 10, + logs__alert__document__count: 5, + }, + p90: { + '__index-threshold': 22, + logs__alert__document__count: 13, + }, + p99: { + '__index-threshold': 22, + logs__alert__document__count: 13, + }, + }, }); }); @@ -387,6 +427,13 @@ Object { '99.0': 26.0, }, }, + percentileAlerts: { + values: { + '50.0': 3.0, + '90.0': 22.0, + '99.0': 22.0, + }, + }, }, { key: 'logs.alert.document.count', @@ -398,6 +445,13 @@ Object { '99.0': 10.0, }, }, + percentileAlerts: { + values: { + '50.0': 5.0, + '90.0': 16.0, + '99.0': 16.0, + }, + }, }, { key: 'document.test.', @@ -409,6 +463,13 @@ Object { '99.0': null, }, }, + percentileAlerts: { + values: { + '50.0': null, + '90.0': null, + '99.0': null, + }, + }, }, ], }; @@ -431,6 +492,23 @@ Object { logs__alert__document__count: 10, }, }); + expect(parsePercentileAggsByRuleType(aggsByType.buckets, 'percentileAlerts.values')).toEqual({ + p50: { + '__index-threshold': 3, + document__test__: 0, + logs__alert__document__count: 5, + }, + p90: { + '__index-threshold': 22, + document__test__: 0, + logs__alert__document__count: 16, + }, + p99: { + '__index-threshold': 22, + document__test__: 0, + logs__alert__document__count: 16, + }, + }); }); test('parsePercentileAggsByRuleType handles unknown path', () => { diff --git a/x-pack/plugins/alerting/server/usage/alerting_telemetry.ts b/x-pack/plugins/alerting/server/usage/alerting_telemetry.ts index 78ec88d959a8b..17dfd957e6df9 100644 --- a/x-pack/plugins/alerting/server/usage/alerting_telemetry.ts +++ b/x-pack/plugins/alerting/server/usage/alerting_telemetry.ts @@ -54,6 +54,13 @@ const generatedActionsPercentilesAgg = { }, }; +const alertsPercentilesAgg = { + percentiles: { + field: 'kibana.alert.rule.execution.metrics.total_number_of_alerts', + percents: [50, 90, 99], + }, +}; + const ruleTypeExecutionsWithDurationMetric = { scripted_metric: { init_script: @@ -426,6 +433,7 @@ export async function getExecutionsPerDayCount( avg: { field: 'kibana.alert.rule.execution.metrics.total_search_duration_ms' }, }, percentileScheduledActions: generatedActionsPercentilesAgg, + percentileAlerts: alertsPercentilesAgg, aggsByType: { terms: { field: 'rule.category', @@ -433,6 +441,7 @@ export async function getExecutionsPerDayCount( }, aggs: { percentileScheduledActions: generatedActionsPercentilesAgg, + percentileAlerts: alertsPercentilesAgg, }, }, }, @@ -469,6 +478,10 @@ export async function getExecutionsPerDayCount( // @ts-expect-error aggegation type is not specified searchResult.aggregations.percentileScheduledActions.values; + const aggsAlertsPercentiles = + // @ts-expect-error aggegation type is not specified + searchResult.aggregations.percentileAlerts.values; + const aggsByTypeBuckets = // @ts-expect-error aggegation type is not specified searchResult.aggregations.aggsByType.buckets; @@ -586,6 +599,21 @@ export async function getExecutionsPerDayCount( aggsByTypeBuckets, 'percentileScheduledActions.values' ), + alertsPercentiles: Object.keys(aggsAlertsPercentiles).reduce( + // ES DSL aggregations are returned as `any` by esClient.search + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (acc: any, curr: string) => ({ + ...acc, + ...(percentileFieldNameMapping[curr] + ? { [percentileFieldNameMapping[curr]]: aggsAlertsPercentiles[curr] } + : {}), + }), + {} + ), + alertsPercentilesByType: parsePercentileAggsByRuleType( + aggsByTypeBuckets, + 'percentileAlerts.values' + ), }; } diff --git a/x-pack/plugins/alerting/server/usage/alerting_usage_collector.ts b/x-pack/plugins/alerting/server/usage/alerting_usage_collector.ts index b80afbc81e77e..c3bca512a2bbd 100644 --- a/x-pack/plugins/alerting/server/usage/alerting_usage_collector.ts +++ b/x-pack/plugins/alerting/server/usage/alerting_usage_collector.ts @@ -186,6 +186,16 @@ export function createAlertingUsageCollector( p90: {}, p99: {}, }, + percentile_num_alerts_per_day: { + p50: 0, + p90: 0, + p99: 0, + }, + percentile_num_alerts_by_type_per_day: { + p50: {}, + p90: {}, + p99: {}, + }, }; } }, @@ -239,6 +249,8 @@ export function createAlertingUsageCollector( avg_total_search_duration_by_type_per_day: byTypeSchema, percentile_num_generated_actions_per_day: byPercentileSchema, percentile_num_generated_actions_by_type_per_day: byPercentileSchemaByType, + percentile_num_alerts_per_day: byPercentileSchema, + percentile_num_alerts_by_type_per_day: byPercentileSchemaByType, }, }); } diff --git a/x-pack/plugins/alerting/server/usage/task.ts b/x-pack/plugins/alerting/server/usage/task.ts index cb2f1e7af1f2f..b64ee27fb9968 100644 --- a/x-pack/plugins/alerting/server/usage/task.ts +++ b/x-pack/plugins/alerting/server/usage/task.ts @@ -148,6 +148,9 @@ export function telemetryTaskRunner( dailyExecutionCounts.generatedActionsPercentiles, percentile_num_generated_actions_by_type_per_day: dailyExecutionCounts.generatedActionsPercentilesByType, + percentile_num_alerts_per_day: dailyExecutionCounts.alertsPercentiles, + percentile_num_alerts_by_type_per_day: + dailyExecutionCounts.alertsPercentilesByType, }, runAt: getNextMidnight(), }; diff --git a/x-pack/plugins/alerting/server/usage/types.ts b/x-pack/plugins/alerting/server/usage/types.ts index 3023d9ab0e13c..b045fd5009c70 100644 --- a/x-pack/plugins/alerting/server/usage/types.ts +++ b/x-pack/plugins/alerting/server/usage/types.ts @@ -35,6 +35,16 @@ export interface AlertingUsage { p90: Record; p99: Record; }; + percentile_num_alerts_per_day: { + p50: number; + p90: number; + p99: number; + }; + percentile_num_alerts_by_type_per_day: { + p50: Record; + p90: Record; + p99: Record; + }; avg_execution_time_per_day: number; avg_execution_time_by_type_per_day: Record; avg_es_search_duration_per_day: number; diff --git a/x-pack/plugins/event_log/generated/mappings.json b/x-pack/plugins/event_log/generated/mappings.json index 982699274e1e3..2ceeb99685dda 100644 --- a/x-pack/plugins/event_log/generated/mappings.json +++ b/x-pack/plugins/event_log/generated/mappings.json @@ -299,6 +299,18 @@ "number_of_generated_actions": { "type": "long" }, + "number_of_new_alerts": { + "type": "long" + }, + "number_of_active_alerts": { + "type": "long" + }, + "number_of_recovered_alerts": { + "type": "long" + }, + "total_number_of_alerts": { + "type": "long" + }, "number_of_searches": { "type": "long" }, diff --git a/x-pack/plugins/event_log/generated/schemas.ts b/x-pack/plugins/event_log/generated/schemas.ts index e030a0ce1e99c..990db4dd4c4ff 100644 --- a/x-pack/plugins/event_log/generated/schemas.ts +++ b/x-pack/plugins/event_log/generated/schemas.ts @@ -131,6 +131,10 @@ export const EventSchema = schema.maybe( schema.object({ number_of_triggered_actions: ecsNumber(), number_of_generated_actions: ecsNumber(), + number_of_new_alerts: ecsNumber(), + number_of_active_alerts: ecsNumber(), + number_of_recovered_alerts: ecsNumber(), + total_number_of_alerts: ecsNumber(), number_of_searches: ecsNumber(), total_indexing_duration_ms: ecsNumber(), es_search_duration_ms: ecsNumber(), diff --git a/x-pack/plugins/event_log/scripts/mappings.js b/x-pack/plugins/event_log/scripts/mappings.js index b4bb17eba42a1..4c21329863269 100644 --- a/x-pack/plugins/event_log/scripts/mappings.js +++ b/x-pack/plugins/event_log/scripts/mappings.js @@ -81,6 +81,18 @@ exports.EcsCustomPropertyMappings = { number_of_generated_actions: { type: 'long', }, + number_of_new_alerts: { + type: 'long', + }, + number_of_active_alerts: { + type: 'long', + }, + number_of_recovered_alerts: { + type: 'long', + }, + total_number_of_alerts: { + type: 'long', + }, number_of_searches: { type: 'long', }, diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json index c8cd2b27210ff..9eba9b7fde512 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -2269,6 +2269,332 @@ } } } + }, + "percentile_num_alerts_per_day": { + "properties": { + "p50": { + "type": "long" + }, + "p90": { + "type": "long" + }, + "p99": { + "type": "long" + } + } + }, + "percentile_num_alerts_by_type_per_day": { + "properties": { + "p50": { + "properties": { + "DYNAMIC_KEY": { + "type": "long" + }, + "__index-threshold": { + "type": "long" + }, + "__es-query": { + "type": "long" + }, + "transform_health": { + "type": "long" + }, + "apm__error_rate": { + "type": "long" + }, + "apm__transaction_error_rate": { + "type": "long" + }, + "apm__transaction_duration": { + "type": "long" + }, + "apm__transaction_duration_anomaly": { + "type": "long" + }, + "metrics__alert__threshold": { + "type": "long" + }, + "metrics__alert__inventory__threshold": { + "type": "long" + }, + "logs__alert__document__count": { + "type": "long" + }, + "monitoring_alert_cluster_health": { + "type": "long" + }, + "monitoring_alert_cpu_usage": { + "type": "long" + }, + "monitoring_alert_disk_usage": { + "type": "long" + }, + "monitoring_alert_elasticsearch_version_mismatch": { + "type": "long" + }, + "monitoring_alert_kibana_version_mismatch": { + "type": "long" + }, + "monitoring_alert_license_expiration": { + "type": "long" + }, + "monitoring_alert_logstash_version_mismatch": { + "type": "long" + }, + "monitoring_alert_nodes_changed": { + "type": "long" + }, + "siem__signals": { + "type": "long" + }, + "siem__notifications": { + "type": "long" + }, + "siem__eqlRule": { + "type": "long" + }, + "siem__indicatorRule": { + "type": "long" + }, + "siem__mlRule": { + "type": "long" + }, + "siem__queryRule": { + "type": "long" + }, + "siem__savedQueryRule": { + "type": "long" + }, + "siem__thresholdRule": { + "type": "long" + }, + "xpack__uptime__alerts__monitorStatus": { + "type": "long" + }, + "xpack__uptime__alerts__tls": { + "type": "long" + }, + "xpack__uptime__alerts__durationAnomaly": { + "type": "long" + }, + "__geo-containment": { + "type": "long" + }, + "xpack__ml__anomaly_detection_alert": { + "type": "long" + }, + "xpack__ml__anomaly_detection_jobs_health": { + "type": "long" + } + } + }, + "p90": { + "properties": { + "DYNAMIC_KEY": { + "type": "long" + }, + "__index-threshold": { + "type": "long" + }, + "__es-query": { + "type": "long" + }, + "transform_health": { + "type": "long" + }, + "apm__error_rate": { + "type": "long" + }, + "apm__transaction_error_rate": { + "type": "long" + }, + "apm__transaction_duration": { + "type": "long" + }, + "apm__transaction_duration_anomaly": { + "type": "long" + }, + "metrics__alert__threshold": { + "type": "long" + }, + "metrics__alert__inventory__threshold": { + "type": "long" + }, + "logs__alert__document__count": { + "type": "long" + }, + "monitoring_alert_cluster_health": { + "type": "long" + }, + "monitoring_alert_cpu_usage": { + "type": "long" + }, + "monitoring_alert_disk_usage": { + "type": "long" + }, + "monitoring_alert_elasticsearch_version_mismatch": { + "type": "long" + }, + "monitoring_alert_kibana_version_mismatch": { + "type": "long" + }, + "monitoring_alert_license_expiration": { + "type": "long" + }, + "monitoring_alert_logstash_version_mismatch": { + "type": "long" + }, + "monitoring_alert_nodes_changed": { + "type": "long" + }, + "siem__signals": { + "type": "long" + }, + "siem__notifications": { + "type": "long" + }, + "siem__eqlRule": { + "type": "long" + }, + "siem__indicatorRule": { + "type": "long" + }, + "siem__mlRule": { + "type": "long" + }, + "siem__queryRule": { + "type": "long" + }, + "siem__savedQueryRule": { + "type": "long" + }, + "siem__thresholdRule": { + "type": "long" + }, + "xpack__uptime__alerts__monitorStatus": { + "type": "long" + }, + "xpack__uptime__alerts__tls": { + "type": "long" + }, + "xpack__uptime__alerts__durationAnomaly": { + "type": "long" + }, + "__geo-containment": { + "type": "long" + }, + "xpack__ml__anomaly_detection_alert": { + "type": "long" + }, + "xpack__ml__anomaly_detection_jobs_health": { + "type": "long" + } + } + }, + "p99": { + "properties": { + "DYNAMIC_KEY": { + "type": "long" + }, + "__index-threshold": { + "type": "long" + }, + "__es-query": { + "type": "long" + }, + "transform_health": { + "type": "long" + }, + "apm__error_rate": { + "type": "long" + }, + "apm__transaction_error_rate": { + "type": "long" + }, + "apm__transaction_duration": { + "type": "long" + }, + "apm__transaction_duration_anomaly": { + "type": "long" + }, + "metrics__alert__threshold": { + "type": "long" + }, + "metrics__alert__inventory__threshold": { + "type": "long" + }, + "logs__alert__document__count": { + "type": "long" + }, + "monitoring_alert_cluster_health": { + "type": "long" + }, + "monitoring_alert_cpu_usage": { + "type": "long" + }, + "monitoring_alert_disk_usage": { + "type": "long" + }, + "monitoring_alert_elasticsearch_version_mismatch": { + "type": "long" + }, + "monitoring_alert_kibana_version_mismatch": { + "type": "long" + }, + "monitoring_alert_license_expiration": { + "type": "long" + }, + "monitoring_alert_logstash_version_mismatch": { + "type": "long" + }, + "monitoring_alert_nodes_changed": { + "type": "long" + }, + "siem__signals": { + "type": "long" + }, + "siem__notifications": { + "type": "long" + }, + "siem__eqlRule": { + "type": "long" + }, + "siem__indicatorRule": { + "type": "long" + }, + "siem__mlRule": { + "type": "long" + }, + "siem__queryRule": { + "type": "long" + }, + "siem__savedQueryRule": { + "type": "long" + }, + "siem__thresholdRule": { + "type": "long" + }, + "xpack__uptime__alerts__monitorStatus": { + "type": "long" + }, + "xpack__uptime__alerts__tls": { + "type": "long" + }, + "xpack__uptime__alerts__durationAnomaly": { + "type": "long" + }, + "__geo-containment": { + "type": "long" + }, + "xpack__ml__anomaly_detection_alert": { + "type": "long" + }, + "xpack__ml__anomaly_detection_jobs_health": { + "type": "long" + } + } + } + } } } }, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts index 15e648bb2166c..601efa34341cb 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts @@ -1332,6 +1332,10 @@ instanceStateValue: true expect(event?.kibana?.alert?.rule?.execution?.metrics?.number_of_searches).to.be(0); expect(event?.kibana?.alert?.rule?.execution?.metrics?.es_search_duration_ms).to.be(0); expect(event?.kibana?.alert?.rule?.execution?.metrics?.total_search_duration_ms).to.be(0); + expect(event?.kibana?.alert?.rule?.execution?.metrics?.number_of_active_alerts).to.be(1); + expect(event?.kibana?.alert?.rule?.execution?.metrics?.number_of_new_alerts).to.be(1); + expect(event?.kibana?.alert?.rule?.execution?.metrics?.number_of_recovered_alerts).to.be(0); + expect(event?.kibana?.alert?.rule?.execution?.metrics?.total_number_of_alerts).to.be(1); expect(event?.rule).to.eql({ id: alertId, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/telemetry/alerting_telemetry.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/telemetry/alerting_telemetry.ts index 4a5395b76c694..afc39bf1c6b74 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/telemetry/alerting_telemetry.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/telemetry/alerting_telemetry.ts @@ -382,8 +382,8 @@ export default function createAlertingTelemetryTests({ getService }: FtrProvider // percentile calculations for number of scheduled actions expect(telemetry.percentile_num_generated_actions_per_day.p50 >= 0).to.be(true); - expect(telemetry.percentile_num_generated_actions_per_day.p90 > 0).to.be(true); - expect(telemetry.percentile_num_generated_actions_per_day.p99 > 0).to.be(true); + expect(telemetry.percentile_num_generated_actions_per_day.p90).to.be.greaterThan(0); + expect(telemetry.percentile_num_generated_actions_per_day.p99).to.be.greaterThan(0); // percentile calculations by rule type. most of these rule types don't schedule actions so they should all be 0 expect( @@ -432,17 +432,69 @@ export default function createAlertingTelemetryTests({ getService }: FtrProvider // this rule type does schedule actions so should be least 1 action scheduled expect( - telemetry.percentile_num_generated_actions_by_type_per_day.p50['test__cumulative-firing'] >= - 1 - ).to.be(true); + telemetry.percentile_num_generated_actions_by_type_per_day.p50['test__cumulative-firing'] + ).to.be.greaterThan(0); expect( - telemetry.percentile_num_generated_actions_by_type_per_day.p90['test__cumulative-firing'] >= - 1 - ).to.be(true); + telemetry.percentile_num_generated_actions_by_type_per_day.p90['test__cumulative-firing'] + ).to.be.greaterThan(0); expect( - telemetry.percentile_num_generated_actions_by_type_per_day.p99['test__cumulative-firing'] >= - 1 - ).to.be(true); + telemetry.percentile_num_generated_actions_by_type_per_day.p99['test__cumulative-firing'] + ).to.be.greaterThan(0); + + // percentile calculations for number of alerts + expect(telemetry.percentile_num_alerts_per_day.p50 >= 0).to.be(true); + expect(telemetry.percentile_num_alerts_per_day.p90).to.be.greaterThan(0); + expect(telemetry.percentile_num_alerts_per_day.p99).to.be.greaterThan(0); + + // percentile calculations by rule type. most of these rule types don't generate alerts so they should all be 0 + expect( + telemetry.percentile_num_alerts_by_type_per_day.p50['example__always-firing'] + ).to.equal(0); + expect( + telemetry.percentile_num_alerts_by_type_per_day.p90['example__always-firing'] + ).to.equal(0); + expect( + telemetry.percentile_num_alerts_by_type_per_day.p99['example__always-firing'] + ).to.equal(0); + + expect( + telemetry.percentile_num_alerts_by_type_per_day.p50.test__onlyContextVariables + ).to.equal(0); + expect( + telemetry.percentile_num_alerts_by_type_per_day.p90.test__onlyContextVariables + ).to.equal(0); + expect( + telemetry.percentile_num_alerts_by_type_per_day.p99.test__onlyContextVariables + ).to.equal(0); + + expect(telemetry.percentile_num_alerts_by_type_per_day.p50.test__noop).to.equal(0); + expect(telemetry.percentile_num_alerts_by_type_per_day.p90.test__noop).to.equal(0); + expect(telemetry.percentile_num_alerts_by_type_per_day.p99.test__noop).to.equal(0); + + expect(telemetry.percentile_num_alerts_by_type_per_day.p50.test__throw).to.equal(0); + expect(telemetry.percentile_num_alerts_by_type_per_day.p90.test__throw).to.equal(0); + expect(telemetry.percentile_num_alerts_by_type_per_day.p99.test__throw).to.equal(0); + + expect(telemetry.percentile_num_alerts_by_type_per_day.p50.test__multipleSearches).to.equal( + 0 + ); + expect(telemetry.percentile_num_alerts_by_type_per_day.p90.test__multipleSearches).to.equal( + 0 + ); + expect(telemetry.percentile_num_alerts_by_type_per_day.p99.test__multipleSearches).to.equal( + 0 + ); + + // this rule type does generate alerts so should be least 1 alert + expect( + telemetry.percentile_num_alerts_by_type_per_day.p50['test__cumulative-firing'] + ).to.be.greaterThan(0); + expect( + telemetry.percentile_num_alerts_by_type_per_day.p90['test__cumulative-firing'] + ).to.be.greaterThan(0); + expect( + telemetry.percentile_num_alerts_by_type_per_day.p99['test__cumulative-firing'] + ).to.be.greaterThan(0); }); }); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts index 909b3315600c2..5777b4978894f 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts @@ -139,6 +139,9 @@ export default function eventLogTests({ getService }: FtrProviderContext) { // validate each event let executeCount = 0; + let numActiveAlerts = 0; + let numNewAlerts = 0; + let numRecoveredAlerts = 0; let currentExecutionId; const executionIds = []; const executeStatuses = ['ok', 'active', 'active']; @@ -165,28 +168,6 @@ export default function eventLogTests({ getService }: FtrProviderContext) { consumer: 'alertsFixture', }); break; - case 'execute': - validateEvent(event, { - spaceId: space.id, - savedObjects: [ - { type: 'alert', id: alertId, rel: 'primary', type_id: 'test.patternFiring' }, - ], - outcome: 'success', - message: `rule executed: test.patternFiring:${alertId}: 'abc'`, - status: executeStatuses[executeCount++], - shouldHaveTask: true, - executionId: currentExecutionId, - ruleTypeId: response.body.rule_type_id, - rule: { - id: alertId, - category: response.body.rule_type_id, - license: 'basic', - ruleset: 'alertsFixture', - name: response.body.name, - }, - consumer: 'alertsFixture', - }); - break; case 'execute-action': validateEvent(event, { spaceId: space.id, @@ -210,6 +191,7 @@ export default function eventLogTests({ getService }: FtrProviderContext) { }); break; case 'new-instance': + numNewAlerts++; validateInstanceEvent( event, `created new alert: 'instance'`, @@ -218,6 +200,7 @@ export default function eventLogTests({ getService }: FtrProviderContext) { ); break; case 'recovered-instance': + numRecoveredAlerts++; validateInstanceEvent( event, `alert 'instance' has recovered`, @@ -226,6 +209,7 @@ export default function eventLogTests({ getService }: FtrProviderContext) { ); break; case 'active-instance': + numActiveAlerts++; validateInstanceEvent( event, `active alert: 'instance' in actionGroup: 'default'`, @@ -233,6 +217,34 @@ export default function eventLogTests({ getService }: FtrProviderContext) { currentExecutionId ); break; + case 'execute': + validateEvent(event, { + spaceId: space.id, + savedObjects: [ + { type: 'alert', id: alertId, rel: 'primary', type_id: 'test.patternFiring' }, + ], + outcome: 'success', + message: `rule executed: test.patternFiring:${alertId}: 'abc'`, + status: executeStatuses[executeCount++], + shouldHaveTask: true, + executionId: currentExecutionId, + ruleTypeId: response.body.rule_type_id, + rule: { + id: alertId, + category: response.body.rule_type_id, + license: 'basic', + ruleset: 'alertsFixture', + name: response.body.name, + }, + consumer: 'alertsFixture', + numActiveAlerts, + numNewAlerts, + numRecoveredAlerts, + }); + numActiveAlerts = 0; + numNewAlerts = 0; + numRecoveredAlerts = 0; + break; // this will get triggered as we add new event actions default: throw new Error(`unexpected event action "${event?.event?.action}"`); @@ -343,11 +355,23 @@ export default function eventLogTests({ getService }: FtrProviderContext) { // validate each event let currentExecutionId; + let numActiveAlerts = 0; + let numNewAlerts = 0; + let numRecoveredAlerts = 0; for (const event of events) { switch (event?.event?.action) { case 'execute-start': currentExecutionId = event?.kibana?.alert?.rule?.execution?.uuid; break; + case 'new-instance': + numNewAlerts++; + break; + case 'recovered-instance': + numRecoveredAlerts++; + break; + case 'active-instance': + numActiveAlerts++; + break; case 'execute': validateEvent(event, { spaceId: space.id, @@ -369,7 +393,13 @@ export default function eventLogTests({ getService }: FtrProviderContext) { name: response.body.name, }, consumer: 'alertsFixture', + numActiveAlerts, + numNewAlerts, + numRecoveredAlerts, }); + numActiveAlerts = 0; + numNewAlerts = 0; + numRecoveredAlerts = 0; expect(event?.kibana?.alert?.rule?.execution?.metrics?.number_of_searches).to.be( numSearches ); @@ -477,6 +507,9 @@ export default function eventLogTests({ getService }: FtrProviderContext) { // validate each event let executeCount = 0; + let numActiveAlerts = 0; + let numNewAlerts = 0; + let numRecoveredAlerts = 0; let currentExecutionId; const executeStatuses = ['ok', 'active', 'active']; for (const event of events) { @@ -501,28 +534,6 @@ export default function eventLogTests({ getService }: FtrProviderContext) { consumer: 'alertsFixture', }); break; - case 'execute': - validateEvent(event, { - spaceId: space.id, - savedObjects: [ - { type: 'alert', id: alertId, rel: 'primary', type_id: 'test.patternFiring' }, - ], - outcome: 'success', - message: `rule executed: test.patternFiring:${alertId}: 'abc'`, - status: executeStatuses[executeCount++], - shouldHaveTask: true, - executionId: currentExecutionId, - ruleTypeId: response.body.rule_type_id, - rule: { - id: alertId, - category: response.body.rule_type_id, - license: 'basic', - ruleset: 'alertsFixture', - name: response.body.name, - }, - consumer: 'alertsFixture', - }); - break; case 'execute-action': expect( [firstSubgroup, secondSubgroup].includes( @@ -551,6 +562,7 @@ export default function eventLogTests({ getService }: FtrProviderContext) { }); break; case 'new-instance': + numNewAlerts++; validateInstanceEvent( event, `created new alert: 'instance'`, @@ -559,6 +571,7 @@ export default function eventLogTests({ getService }: FtrProviderContext) { ); break; case 'recovered-instance': + numRecoveredAlerts++; validateInstanceEvent( event, `alert 'instance' has recovered`, @@ -567,6 +580,7 @@ export default function eventLogTests({ getService }: FtrProviderContext) { ); break; case 'active-instance': + numActiveAlerts++; expect( [firstSubgroup, secondSubgroup].includes( event?.kibana?.alerting?.action_subgroup! @@ -579,6 +593,34 @@ export default function eventLogTests({ getService }: FtrProviderContext) { currentExecutionId ); break; + case 'execute': + validateEvent(event, { + spaceId: space.id, + savedObjects: [ + { type: 'alert', id: alertId, rel: 'primary', type_id: 'test.patternFiring' }, + ], + outcome: 'success', + message: `rule executed: test.patternFiring:${alertId}: 'abc'`, + status: executeStatuses[executeCount++], + shouldHaveTask: true, + executionId: currentExecutionId, + ruleTypeId: response.body.rule_type_id, + rule: { + id: alertId, + category: response.body.rule_type_id, + license: 'basic', + ruleset: 'alertsFixture', + name: response.body.name, + }, + consumer: 'alertsFixture', + numActiveAlerts, + numNewAlerts, + numRecoveredAlerts, + }); + numActiveAlerts = 0; + numNewAlerts = 0; + numRecoveredAlerts = 0; + break; // this will get triggered as we add new event actions default: throw new Error(`unexpected event action "${event?.event?.action}"`); @@ -687,6 +729,9 @@ export default function eventLogTests({ getService }: FtrProviderContext) { ruleset: 'alertsFixture', }, consumer: 'alertsFixture', + numActiveAlerts: 0, + numNewAlerts: 0, + numRecoveredAlerts: 0, }); }); }); @@ -715,6 +760,9 @@ interface ValidateEventLogParams { reason?: string; executionId?: string; numTriggeredActions?: number; + numActiveAlerts?: number; + numRecoveredAlerts?: number; + numNewAlerts?: number; consumer?: string; ruleTypeId: string; rule?: { @@ -741,6 +789,9 @@ export function validateEvent(event: IValidatedEvent, params: ValidateEventLogPa shouldHaveTask, executionId, numTriggeredActions = 1, + numActiveAlerts, + numNewAlerts, + numRecoveredAlerts, consumer, ruleTypeId, } = params; @@ -776,6 +827,30 @@ export function validateEvent(event: IValidatedEvent, params: ValidateEventLogPa expect(event?.kibana?.alert?.rule?.consumer).to.be(consumer); } + if (numActiveAlerts) { + expect(event?.kibana?.alert?.rule?.execution?.metrics?.number_of_active_alerts).to.be( + numActiveAlerts + ); + } + + if (numRecoveredAlerts) { + expect(event?.kibana?.alert?.rule?.execution?.metrics?.number_of_recovered_alerts).to.be( + numRecoveredAlerts + ); + } + + if (numNewAlerts) { + expect(event?.kibana?.alert?.rule?.execution?.metrics?.number_of_new_alerts).to.be( + numNewAlerts + ); + } + + if (numActiveAlerts && numRecoveredAlerts) { + expect(event?.kibana?.alert?.rule?.execution?.metrics?.total_number_of_alerts).to.be( + numActiveAlerts + numRecoveredAlerts + ); + } + expect(event?.kibana?.alert?.rule?.rule_type_id).to.be(ruleTypeId); expect(event?.kibana?.space_ids?.[0]).to.equal(spaceId); From 801301954e25509c481b98b409a5bc7db7a1a0e2 Mon Sep 17 00:00:00 2001 From: Gloria Hornero Date: Tue, 26 Apr 2022 18:00:03 +0200 Subject: [PATCH 22/29] groups add exceptions tests in one spec file (#130908) --- ...om_alert.spec.ts => add_exception.spec.ts} | 47 ++++++++-- .../integration/exceptions/from_rule.spec.ts | 89 ------------------- .../es_archives/exceptions_2/data.json | 2 +- .../es_archives/exceptions_2/mappings.json | 2 +- 4 files changed, 42 insertions(+), 98 deletions(-) rename x-pack/plugins/security_solution/cypress/integration/exceptions/{from_alert.spec.ts => add_exception.spec.ts} (70%) delete mode 100644 x-pack/plugins/security_solution/cypress/integration/exceptions/from_rule.spec.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/exceptions/from_alert.spec.ts b/x-pack/plugins/security_solution/cypress/integration/exceptions/add_exception.spec.ts similarity index 70% rename from x-pack/plugins/security_solution/cypress/integration/exceptions/from_alert.spec.ts rename to x-pack/plugins/security_solution/cypress/integration/exceptions/add_exception.spec.ts index 9ab4c1559c4cd..6d43ed81cd68e 100644 --- a/x-pack/plugins/security_solution/cypress/integration/exceptions/from_alert.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/exceptions/add_exception.spec.ts @@ -18,6 +18,7 @@ import { esArchiverLoad, esArchiverUnload } from '../../tasks/es_archiver'; import { login, visitWithoutDateRange } from '../../tasks/login'; import { addsException, + addsExceptionFromRuleSettings, goToAlertsTab, goToExceptionsTab, removeException, @@ -27,16 +28,16 @@ import { import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../urls/navigation'; import { cleanKibana, deleteAlertsAndRules } from '../../tasks/common'; -describe('From alert', () => { +describe('Adds rule exception', () => { const NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS = '1 alert'; before(() => { cleanKibana(); login(); + esArchiverLoad('exceptions'); }); beforeEach(() => { - esArchiverLoad('exceptions'); deleteAlertsAndRules(); createCustomRule( { ...getNewRule(), customQuery: 'agent.name:*', index: ['exceptions*'] }, @@ -48,17 +49,19 @@ describe('From alert', () => { goToRuleDetails(); waitForTheRuleToBeExecuted(); waitForAlertsToPopulate(); - - cy.get(ALERTS_COUNT).should('exist'); - cy.get(NUMBER_OF_ALERTS).should('have.text', NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS); }); afterEach(() => { - esArchiverUnload('exceptions'); esArchiverUnload('exceptions_2'); }); - it('Creates an exception and deletes it', () => { + after(() => { + esArchiverUnload('exceptions'); + }); + + it('Creates an exception from an alert and deletes it', () => { + cy.get(ALERTS_COUNT).should('exist'); + cy.get(NUMBER_OF_ALERTS).should('have.text', NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS); // Create an exception from the alerts actions menu that matches // the existing alert addExceptionFromFirstAlert(); @@ -86,4 +89,34 @@ describe('From alert', () => { cy.get(ALERTS_COUNT).should('exist'); cy.get(NUMBER_OF_ALERTS).should('have.text', `${NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS}`); }); + + it('Creates an exception from a rule and deletes it', () => { + // Create an exception from the exception tab that matches + // the existing alert + goToExceptionsTab(); + addsExceptionFromRuleSettings(getException()); + + // Alerts table should now be empty from having added exception and closed + // matching alert + goToAlertsTab(); + cy.get(EMPTY_ALERT_TABLE).should('exist'); + + // Closed alert should appear in table + goToClosedAlerts(); + cy.get(ALERTS_COUNT).should('exist'); + cy.get(NUMBER_OF_ALERTS).should('have.text', `${NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS}`); + + // Remove the exception and load an event that would have matched that exception + // to show that said exception now starts to show up again + goToExceptionsTab(); + removeException(); + esArchiverLoad('exceptions_2'); + goToAlertsTab(); + goToOpenedAlerts(); + waitForTheRuleToBeExecuted(); + waitForAlertsToPopulate(); + + cy.get(ALERTS_COUNT).should('exist'); + cy.get(NUMBER_OF_ALERTS).should('have.text', `${NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS}`); + }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/exceptions/from_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/exceptions/from_rule.spec.ts deleted file mode 100644 index 5e8c542b369a6..0000000000000 --- a/x-pack/plugins/security_solution/cypress/integration/exceptions/from_rule.spec.ts +++ /dev/null @@ -1,89 +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 { getException } from '../../objects/exception'; -import { getNewRule } from '../../objects/rule'; - -import { ALERTS_COUNT, EMPTY_ALERT_TABLE, NUMBER_OF_ALERTS } from '../../screens/alerts'; - -import { goToClosedAlerts, goToOpenedAlerts } from '../../tasks/alerts'; -import { createCustomRule } from '../../tasks/api_calls/rules'; -import { goToRuleDetails } from '../../tasks/alerts_detection_rules'; -import { waitForAlertsToPopulate } from '../../tasks/create_new_rule'; -import { esArchiverLoad, esArchiverUnload } from '../../tasks/es_archiver'; -import { login, visitWithoutDateRange } from '../../tasks/login'; -import { - addsExceptionFromRuleSettings, - goToAlertsTab, - goToExceptionsTab, - removeException, - waitForTheRuleToBeExecuted, -} from '../../tasks/rule_details'; - -import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../urls/navigation'; -import { cleanKibana, deleteAlertsAndRules } from '../../tasks/common'; - -describe('From rule', () => { - const NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS = '1'; - before(() => { - cleanKibana(); - login(); - }); - - beforeEach(() => { - esArchiverLoad('exceptions'); - deleteAlertsAndRules(); - createCustomRule( - { ...getNewRule(), customQuery: 'agent.name:*', index: ['exceptions*'] }, - 'rule_testing', - '5s', - true - ); - visitWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL); - goToRuleDetails(); - waitForTheRuleToBeExecuted(); - waitForAlertsToPopulate(); - - cy.get(ALERTS_COUNT).should('exist'); - cy.get(NUMBER_OF_ALERTS).should('have.text', `${NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS} alert`); - }); - - afterEach(() => { - esArchiverUnload('exceptions'); - esArchiverUnload('exceptions_2'); - }); - - it('Creates an exception and deletes it', () => { - // Create an exception from the exception tab that matches - // the existing alert - goToExceptionsTab(); - addsExceptionFromRuleSettings(getException()); - - // Alerts table should now be empty from having added exception and closed - // matching alert - goToAlertsTab(); - cy.get(EMPTY_ALERT_TABLE).should('exist'); - - // Closed alert should appear in table - goToClosedAlerts(); - cy.get(ALERTS_COUNT).should('exist'); - cy.get(NUMBER_OF_ALERTS).should('have.text', `${NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS} alert`); - - // Remove the exception and load an event that would have matched that exception - // to show that said exception now starts to show up again - goToExceptionsTab(); - removeException(); - esArchiverLoad('exceptions_2'); - goToAlertsTab(); - goToOpenedAlerts(); - waitForTheRuleToBeExecuted(); - waitForAlertsToPopulate(); - - cy.get(ALERTS_COUNT).should('exist'); - cy.get(NUMBER_OF_ALERTS).should('have.text', `${NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS} alert`); - }); -}); diff --git a/x-pack/test/security_solution_cypress/es_archives/exceptions_2/data.json b/x-pack/test/security_solution_cypress/es_archives/exceptions_2/data.json index ae2e267abc7cf..3ad636b20a9cc 100644 --- a/x-pack/test/security_solution_cypress/es_archives/exceptions_2/data.json +++ b/x-pack/test/security_solution_cypress/es_archives/exceptions_2/data.json @@ -2,7 +2,7 @@ "type": "doc", "value": { "id": "_aZE5nwBOpWiDweSth_E", - "index": "exceptions-0001", + "index": "exceptions-0002", "source": { "@timestamp": "2019-09-02T00:41:06.527Z", "agent": { diff --git a/x-pack/test/security_solution_cypress/es_archives/exceptions_2/mappings.json b/x-pack/test/security_solution_cypress/es_archives/exceptions_2/mappings.json index 3b5cc2dae545c..f5f07c23046aa 100644 --- a/x-pack/test/security_solution_cypress/es_archives/exceptions_2/mappings.json +++ b/x-pack/test/security_solution_cypress/es_archives/exceptions_2/mappings.json @@ -11,7 +11,7 @@ "refresh_interval": "5s" } }, - "index": "exceptions-0001", + "index": "exceptions-0002", "mappings": { "properties": { "@timestamp": { From 3f20878699597e396f988e6cab4908abea2c3ebd Mon Sep 17 00:00:00 2001 From: Jack Date: Tue, 26 Apr 2022 12:23:34 -0400 Subject: [PATCH 23/29] [8.2.1] [Session View] Fix jump to alert can add extra space to the right of detail panel (#130935) * Fix jump to alert can add extra space to the right of detail panel * Fix resizable panels initial size * Fix alerts tab toggle view bottom gap --- .../detail_panel_alert_tab/styles.ts | 2 - .../public/components/session_view/index.tsx | 160 +++++++++--------- .../session_view_detail_panel/helpers.ts | 2 +- .../session_view_detail_panel/index.test.tsx | 4 +- .../session_view_detail_panel/index.tsx | 2 +- 5 files changed, 82 insertions(+), 88 deletions(-) diff --git a/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/styles.ts b/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/styles.ts index a906744cdafb2..702d4f20f3554 100644 --- a/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/styles.ts +++ b/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/styles.ts @@ -24,12 +24,10 @@ export const useStyles = () => { top: 0, zIndex: 1, backgroundColor: colors.emptyShade, - paddingTop: size.base, }; const viewMode: CSSObject = { margin: size.base, - marginBottom: 0, }; return { diff --git a/x-pack/plugins/session_view/public/components/session_view/index.tsx b/x-pack/plugins/session_view/public/components/session_view/index.tsx index b1678a5265ee2..7d11096ff18bd 100644 --- a/x-pack/plugins/session_view/public/components/session_view/index.tsx +++ b/x-pack/plugins/session_view/public/components/session_view/index.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { useState, useCallback, useEffect, useMemo } from 'react'; +import React, { useState, useCallback, useEffect, useMemo, useRef } from 'react'; import { EuiEmptyPrompt, EuiButton, @@ -78,6 +78,8 @@ export const SessionView = ({ const styles = useStyles({ height, isFullScreen }); + const detailPanelCollapseFn = useRef(() => {}); + // to give an indication to the user that there may be more search results if they turn on verbose mode. const showVerboseSearchTooltip = useMemo(() => { return !!(!displayOptions?.verboseMode && searchQuery && searchResults?.length === 0); @@ -122,7 +124,6 @@ export const SessionView = ({ const hasData = alerts && data && data.pages?.[0].events.length > 0; const hasError = error || alertsError; const renderIsLoading = (isFetching || alertsFetching) && !(data && alerts); - const renderDetails = isDetailOpen && selectedProcess; const { data: newUpdatedAlertsStatus } = useFetchAlertStatus( updatedAlertsStatus, fetchAlertStatus[0] ?? '' @@ -141,6 +142,7 @@ export const SessionView = ({ }, []); const toggleDetailPanel = useCallback(() => { + detailPanelCollapseFn.current(); setIsDetailOpen(!isDetailOpen); }, [isDetailOpen]); @@ -240,89 +242,83 @@ export const SessionView = ({ - {(EuiResizablePanel, EuiResizableButton) => ( - <> - - {hasError && ( - - - - } - body={ -

- -

- } - /> - )} + {(EuiResizablePanel, EuiResizableButton, { togglePanel }) => { + detailPanelCollapseFn.current = () => { + togglePanel?.('session-detail-panel', { direction: 'left' }); + }; - {hasData && ( -
- + + {hasError && ( + + + + } + body={ +

+ +

+ } /> -
- )} -
+ )} - {renderDetails ? ( - <> - - - - - - ) : ( - <> - {/* Returning an empty element here (instead of false) to avoid a bug in EuiResizableContainer */} - - )} - - )} + {hasData && ( +
+ +
+ )} +
+ + + + + + + ); + }}
diff --git a/x-pack/plugins/session_view/public/components/session_view_detail_panel/helpers.ts b/x-pack/plugins/session_view/public/components/session_view_detail_panel/helpers.ts index 083bc35ac49d4..e4e6cb0134bbc 100644 --- a/x-pack/plugins/session_view/public/components/session_view_detail_panel/helpers.ts +++ b/x-pack/plugins/session_view/public/components/session_view_detail_panel/helpers.ts @@ -37,7 +37,7 @@ const getDetailPanelProcessLeader = (leader: ProcessFields | undefined) => ({ entryMetaSourceIp: leader?.entry_meta?.source?.ip ?? DEFAULT_PROCESS_DATA.entryMetaSourceIp, }); -export const getDetailPanelProcess = (process: Process | undefined) => { +export const getDetailPanelProcess = (process: Process | null) => { const processData = {} as DetailPanelProcess; if (!process) { return { diff --git a/x-pack/plugins/session_view/public/components/session_view_detail_panel/index.test.tsx b/x-pack/plugins/session_view/public/components/session_view_detail_panel/index.test.tsx index f2d7249e43c90..9ddefa25cea07 100644 --- a/x-pack/plugins/session_view/public/components/session_view_detail_panel/index.test.tsx +++ b/x-pack/plugins/session_view/public/components/session_view_detail_panel/index.test.tsx @@ -38,10 +38,10 @@ describe('SessionView component', () => { expect(renderResult.queryByText('8e4daeb2-4a4e-56c4-980e-f0dcfdbc3726')).toBeVisible(); }); - it('should should default state with selectedProcess undefined', async () => { + it('should should default state with selectedProcess null', async () => { renderResult = mockedContext.render( diff --git a/x-pack/plugins/session_view/public/components/session_view_detail_panel/index.tsx b/x-pack/plugins/session_view/public/components/session_view_detail_panel/index.tsx index 86cfacfb72d06..130d15ca4df30 100644 --- a/x-pack/plugins/session_view/public/components/session_view_detail_panel/index.tsx +++ b/x-pack/plugins/session_view/public/components/session_view_detail_panel/index.tsx @@ -17,7 +17,7 @@ import { DetailPanelAlertTab } from '../detail_panel_alert_tab'; import { ALERT_COUNT_THRESHOLD } from '../../../common/constants'; interface SessionViewDetailPanelDeps { - selectedProcess: Process | undefined; + selectedProcess: Process | null; alerts?: ProcessEvent[]; investigatedAlertId?: string; onJumpToEvent: (event: ProcessEvent) => void; From ffe1cdddc76b7bc0a2b9410fc9a0a501e545e6f9 Mon Sep 17 00:00:00 2001 From: Vitalii Dmyterko <92328789+vitaliidm@users.noreply.github.com> Date: Tue, 26 Apr 2022 17:27:09 +0100 Subject: [PATCH 24/29] [Security Solution] Removes internal tags (#130664) Addresses: - https://github.com/elastic/kibana/issues/124899 ## Summary - removes usage of internal tags (tags start with `__internal`) in codebase - migrates rules (removes existing internal tags from rules) ### 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 ### 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) --- .../server/saved_objects/migrations.test.ts | 35 +++++ .../server/saved_objects/migrations.ts | 33 +++++ .../fixtures/saved_objects/rule.ndjson | 5 +- .../security_solution/common/constants.ts | 9 -- .../detection_engine/alerts/mock.ts | 28 +--- .../detection_engine/rules/api.test.ts | 6 +- .../rules/use_rule_with_fallback.test.tsx | 2 - .../detection_engine/rules/utils.test.ts | 17 ++- .../detection_engine/rules/utils.ts | 13 +- .../notifications/legacy_add_tags.test.ts | 28 ---- .../notifications/legacy_add_tags.ts | 14 -- .../legacy_create_notifications.ts | 4 +- .../legacy_read_notifications.ts | 3 +- .../routes/__mocks__/request_responses.ts | 6 +- .../get_prepackaged_rules_status_route.ts | 2 +- .../legacy_create_legacy_notification.ts | 4 +- .../routes/rules/utils.test.ts | 23 +-- .../detection_engine/routes/rules/utils.ts | 5 - .../rule_types/factories/utils/build_alert.ts | 3 +- .../detection_engine/rules/add_tags.test.ts | 49 ------- .../lib/detection_engine/rules/add_tags.ts | 20 --- .../detection_engine/rules/create_rules.ts | 3 +- .../rules/duplicate_rule.test.ts | 5 +- .../detection_engine/rules/duplicate_rule.ts | 3 +- .../lib/detection_engine/rules/edit_rule.ts | 6 +- .../rules/get_existing_prepackaged_rules.ts | 5 +- .../rules/get_export_by_object_ids.ts | 7 +- .../lib/detection_engine/rules/patch_rules.ts | 3 +- .../lib/detection_engine/rules/read_rules.ts | 5 +- .../detection_engine/rules/update_rules.ts | 3 +- .../schemas/rule_converters.ts | 6 +- .../scripts/find_rule_by_filter.sh | 6 +- .../signals/saved_object_references/README.md | 5 +- .../detection_engine/tags/read_tags.test.ts | 136 +----------------- .../lib/detection_engine/tags/read_tags.ts | 16 +-- .../usage/detections/get_metrics.test.ts | 2 +- .../detections/ml_jobs/get_metrics.mocks.ts | 15 +- .../get_rule_object_correlations.ts | 6 +- .../usage/queries/utils/is_elastic_rule.ts | 11 -- .../spaces_only/tests/alerting/migrations.ts | 20 ++- .../tests/resolve_read_rules.ts | 5 +- .../utils/downgrade_immutable_rule.ts | 3 +- .../utils/find_immutable_rule_by_id.ts | 8 +- .../functional/es_archives/alerts/data.json | 110 ++++++++++++-- 44 files changed, 252 insertions(+), 446 deletions(-) delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_add_tags.test.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_add_tags.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/add_tags.test.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/add_tags.ts delete mode 100644 x-pack/plugins/security_solution/server/usage/queries/utils/is_elastic_rule.ts diff --git a/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts b/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts index 6b736006fc074..921412d4e79e8 100644 --- a/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts +++ b/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts @@ -2253,6 +2253,41 @@ describe('successful migrations', () => { }); }); + describe('8.3.0', () => { + test('removes internal tags', () => { + const migration830 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['8.3.0']; + const alert = getMockData( + { + tags: [ + '__internal_immutable:false', + '__internal_rule_id:064e3fed-6328-416b-bb85-c08265088f41', + 'test-tag', + ], + alertTypeId: 'siem.queryRule', + }, + true + ); + + const migratedAlert830 = migration830(alert, migrationContext); + + expect(migratedAlert830.attributes.tags).toEqual(['test-tag']); + }); + + test('do not remove internal tags if rule is not Security solution rule', () => { + const migration830 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['8.3.0']; + const alert = getMockData( + { + tags: ['__internal_immutable:false', 'tag-1'], + }, + true + ); + + const migratedAlert830 = migration830(alert, migrationContext); + + expect(migratedAlert830.attributes.tags).toEqual(['__internal_immutable:false', 'tag-1']); + }); + }); + describe('Metrics Inventory Threshold rule', () => { test('Migrates incorrect action group spelling', () => { const migration800 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['8.0.0']; diff --git a/x-pack/plugins/alerting/server/saved_objects/migrations.ts b/x-pack/plugins/alerting/server/saved_objects/migrations.ts index 18de27eb6919e..69d88e196dcfd 100644 --- a/x-pack/plugins/alerting/server/saved_objects/migrations.ts +++ b/x-pack/plugins/alerting/server/saved_objects/migrations.ts @@ -152,6 +152,12 @@ export function getMigrations( pipeMigrations(addMappedParams) ); + const migrationRules830 = createEsoMigration( + encryptedSavedObjects, + (doc: SavedObjectUnsanitizedDoc): doc is SavedObjectUnsanitizedDoc => true, + pipeMigrations(removeInternalTags) + ); + return { '7.10.0': executeMigrationWithErrorHandling(migrationWhenRBACWasIntroduced, '7.10.0'), '7.11.0': executeMigrationWithErrorHandling(migrationAlertUpdatedAtAndNotifyWhen, '7.11.0'), @@ -163,6 +169,7 @@ export function getMigrations( '8.0.0': executeMigrationWithErrorHandling(migrationRules800, '8.0.0'), '8.0.1': executeMigrationWithErrorHandling(migrationRules801, '8.0.1'), '8.2.0': executeMigrationWithErrorHandling(migrationRules820, '8.2.0'), + '8.3.0': executeMigrationWithErrorHandling(migrationRules830, '8.3.0'), }; } @@ -864,6 +871,32 @@ function getCorrespondingAction( ) as RawRuleAction; } } +/** + * removes internal tags(starts with '__internal') from Security Solution rules + * @param doc rule to be migrated + * @returns migrated rule if it's Security Solution rule or unchanged if not + */ +function removeInternalTags( + doc: SavedObjectUnsanitizedDoc +): SavedObjectUnsanitizedDoc { + if (!isDetectionEngineAADRuleType(doc)) { + return doc; + } + + const { + attributes: { tags }, + } = doc; + + const filteredTags = (tags ?? []).filter((tag) => !tag.startsWith('__internal_')); + + return { + ...doc, + attributes: { + ...doc.attributes, + tags: filteredTags, + }, + }; +} function pipeMigrations(...migrations: AlertMigration[]): AlertMigration { return (doc: SavedObjectUnsanitizedDoc) => diff --git a/x-pack/plugins/osquery/cypress/fixtures/saved_objects/rule.ndjson b/x-pack/plugins/osquery/cypress/fixtures/saved_objects/rule.ndjson index 75bdecb5be428..f688dc0731c7f 100644 --- a/x-pack/plugins/osquery/cypress/fixtures/saved_objects/rule.ndjson +++ b/x-pack/plugins/osquery/cypress/fixtures/saved_objects/rule.ndjson @@ -8,10 +8,7 @@ "version": "WzE5MjksMV0=", "attributes": { "name": "Test-rule", - "tags": [ - "__internal_rule_id:22308402-5e0e-421b-8d22-a47ddc4b0188", - "__internal_immutable:false" - ], + "tags": [], "alertTypeId": "siem.queryRule", "consumer": "siem", "params": { diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 74f9bff078b89..d71c001f64dd1 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -206,15 +206,6 @@ export const IP_REPUTATION_LINKS_SETTING_DEFAULT = `[ */ export const LEGACY_NOTIFICATIONS_ID = `siem.notifications` as const; -/** - * Special internal structure for tags for signals. This is used - * to filter out tags that have internal structures within them. - */ -export const INTERNAL_IDENTIFIER = '__internal' as const; -export const INTERNAL_RULE_ID_KEY = `${INTERNAL_IDENTIFIER}_rule_id` as const; -export const INTERNAL_RULE_ALERT_ID_KEY = `${INTERNAL_IDENTIFIER}_rule_alert_id` as const; -export const INTERNAL_IMMUTABLE_KEY = `${INTERNAL_IDENTIFIER}_immutable` as const; - /** * Internal actions route */ diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/mock.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/mock.ts index 58ec564c6911b..f35c4158fa236 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/mock.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/mock.ts @@ -188,12 +188,7 @@ export const alertsMock: AlertSearchResponse = { query: 'host.name : *', references: ['https://google.com'], severity: 'high', - tags: [ - 'host.name exists', - 'for testing', - '__internal_rule_id:82b2b065-a2ee-49fc-9d6d-781a75c3d280', - '__internal_immutable:false', - ], + tags: ['host.name exists', 'for testing'], type: 'query', to: 'now', enabled: true, @@ -428,12 +423,7 @@ export const alertsMock: AlertSearchResponse = { query: 'host.name : *', references: ['https://google.com'], severity: 'high', - tags: [ - 'host.name exists', - 'for testing', - '__internal_rule_id:82b2b065-a2ee-49fc-9d6d-781a75c3d280', - '__internal_immutable:false', - ], + tags: ['host.name exists', 'for testing'], type: 'query', to: 'now', enabled: true, @@ -634,12 +624,7 @@ export const alertsMock: AlertSearchResponse = { query: 'host.name : *', references: ['https://google.com'], severity: 'high', - tags: [ - 'host.name exists', - 'for testing', - '__internal_rule_id:82b2b065-a2ee-49fc-9d6d-781a75c3d280', - '__internal_immutable:false', - ], + tags: ['host.name exists', 'for testing'], type: 'query', to: 'now', enabled: true, @@ -838,12 +823,7 @@ export const alertsMock: AlertSearchResponse = { query: 'host.name : *', references: ['https://google.com'], severity: 'high', - tags: [ - 'host.name exists', - 'for testing', - '__internal_rule_id:82b2b065-a2ee-49fc-9d6d-781a75c3d280', - '__internal_immutable:false', - ], + tags: ['host.name exists', 'for testing'], type: 'query', to: 'now', enabled: true, diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts index 3c534ca7294a5..b999040674a91 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts @@ -200,7 +200,7 @@ describe('Detections Rules API', () => { expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_find', { method: 'GET', query: { - filter: 'alert.attributes.tags: "__internal_immutable:false"', + filter: 'alert.attributes.params.immutable: false', page: 1, per_page: 20, sort_field: 'enabled', @@ -228,7 +228,7 @@ describe('Detections Rules API', () => { expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_find', { method: 'GET', query: { - filter: 'alert.attributes.tags: "__internal_immutable:true"', + filter: 'alert.attributes.params.immutable: true', page: 1, per_page: 20, sort_field: 'enabled', @@ -383,7 +383,7 @@ describe('Detections Rules API', () => { method: 'GET', query: { filter: - 'alert.attributes.tags: "__internal_immutable:false" AND alert.attributes.tags: "__internal_immutable:true" AND alert.attributes.tags:("hello" AND "world") AND (alert.attributes.name: "ruleName" OR alert.attributes.params.index: "ruleName" OR alert.attributes.params.threat.tactic.id: "ruleName" OR alert.attributes.params.threat.tactic.name: "ruleName" OR alert.attributes.params.threat.technique.id: "ruleName" OR alert.attributes.params.threat.technique.name: "ruleName" OR alert.attributes.params.threat.technique.subtechnique.id: "ruleName" OR alert.attributes.params.threat.technique.subtechnique.name: "ruleName")', + 'alert.attributes.tags:("hello" AND "world") AND (alert.attributes.name: "ruleName" OR alert.attributes.params.index: "ruleName" OR alert.attributes.params.threat.tactic.id: "ruleName" OR alert.attributes.params.threat.tactic.name: "ruleName" OR alert.attributes.params.threat.technique.id: "ruleName" OR alert.attributes.params.threat.technique.name: "ruleName" OR alert.attributes.params.threat.technique.subtechnique.id: "ruleName" OR alert.attributes.params.threat.technique.subtechnique.name: "ruleName")', page: 1, per_page: 20, sort_field: 'enabled', diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_with_fallback.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_with_fallback.test.tsx index 40d2e8663b618..d7c4ad8772bd2 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_with_fallback.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_with_fallback.test.tsx @@ -154,8 +154,6 @@ describe('useRuleWithFallback', () => { "tags": Array [ "host.name exists", "for testing", - "__internal_rule_id:82b2b065-a2ee-49fc-9d6d-781a75c3d280", - "__internal_immutable:false", ], "threat": Array [ Object { diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/utils.test.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/utils.test.ts index a26a4aec3ec02..dff02aa3d3a75 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/utils.test.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/utils.test.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { INTERNAL_IMMUTABLE_KEY } from '../../../../../common/constants'; import { FilterOptions } from './types'; import { convertRulesFilterToKQL } from './utils'; @@ -42,13 +41,23 @@ describe('convertRulesFilterToKQL', () => { it('handles presence of "showCustomRules" properly', () => { const kql = convertRulesFilterToKQL({ ...filterOptions, showCustomRules: true }); - expect(kql).toBe(`alert.attributes.tags: "${INTERNAL_IMMUTABLE_KEY}:false"`); + expect(kql).toBe(`alert.attributes.params.immutable: false`); }); it('handles presence of "showElasticRules" properly', () => { const kql = convertRulesFilterToKQL({ ...filterOptions, showElasticRules: true }); - expect(kql).toBe(`alert.attributes.tags: "${INTERNAL_IMMUTABLE_KEY}:true"`); + expect(kql).toBe(`alert.attributes.params.immutable: true`); + }); + + it('handles presence of "showElasticRules" and "showCustomRules" at the same time properly', () => { + const kql = convertRulesFilterToKQL({ + ...filterOptions, + showElasticRules: true, + showCustomRules: true, + }); + + expect(kql).toBe(''); }); it('handles presence of "tags" properly', () => { @@ -66,7 +75,7 @@ describe('convertRulesFilterToKQL', () => { }); expect(kql).toBe( - `alert.attributes.tags: "${INTERNAL_IMMUTABLE_KEY}:true" AND alert.attributes.tags:("tag1" AND "tag2") AND (alert.attributes.name: "foo" OR alert.attributes.params.index: "foo" OR alert.attributes.params.threat.tactic.id: "foo" OR alert.attributes.params.threat.tactic.name: "foo" OR alert.attributes.params.threat.technique.id: "foo" OR alert.attributes.params.threat.technique.name: "foo" OR alert.attributes.params.threat.technique.subtechnique.id: "foo" OR alert.attributes.params.threat.technique.subtechnique.name: "foo")` + `alert.attributes.params.immutable: true AND alert.attributes.tags:("tag1" AND "tag2") AND (alert.attributes.name: "foo" OR alert.attributes.params.index: "foo" OR alert.attributes.params.threat.tactic.id: "foo" OR alert.attributes.params.threat.tactic.name: "foo" OR alert.attributes.params.threat.technique.id: "foo" OR alert.attributes.params.threat.technique.name: "foo" OR alert.attributes.params.threat.technique.subtechnique.id: "foo" OR alert.attributes.params.threat.technique.subtechnique.name: "foo")` ); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/utils.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/utils.ts index 069746223731c..8c576979a15c0 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/utils.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/utils.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { INTERNAL_IMMUTABLE_KEY } from '../../../../../common/constants'; import { escapeKuery } from '../../../../common/lib/keury'; import { FilterOptions } from './types'; @@ -35,12 +34,12 @@ export const convertRulesFilterToKQL = ({ }: FilterOptions): string => { const filters: string[] = []; - if (showCustomRules) { - filters.push(`alert.attributes.tags: "${INTERNAL_IMMUTABLE_KEY}:false"`); - } - - if (showElasticRules) { - filters.push(`alert.attributes.tags: "${INTERNAL_IMMUTABLE_KEY}:true"`); + if (showCustomRules && showElasticRules) { + // if both showCustomRules && showElasticRules selected we omit filter, as it includes all existing rules + } else if (showElasticRules) { + filters.push('alert.attributes.params.immutable: true'); + } else if (showCustomRules) { + filters.push('alert.attributes.params.immutable: false'); } if (tags.length > 0) { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_add_tags.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_add_tags.test.ts deleted file mode 100644 index 95051ad3d8021..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_add_tags.test.ts +++ /dev/null @@ -1,28 +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. - */ - -// eslint-disable-next-line no-restricted-imports -import { legacyAddTags } from './legacy_add_tags'; -import { INTERNAL_RULE_ALERT_ID_KEY } from '../../../../common/constants'; - -describe('legacyAdd_tags', () => { - test('it should add a rule id as an internal structure', () => { - const tags = legacyAddTags([], 'rule-1'); - expect(tags).toEqual([`${INTERNAL_RULE_ALERT_ID_KEY}:rule-1`]); - }); - - test('it should not allow duplicate tags to be created', () => { - const tags = legacyAddTags(['tag-1', 'tag-1'], 'rule-1'); - expect(tags).toEqual(['tag-1', `${INTERNAL_RULE_ALERT_ID_KEY}:rule-1`]); - }); - - test('it should not allow duplicate internal tags to be created when called two times in a row', () => { - const tags1 = legacyAddTags(['tag-1'], 'rule-1'); - const tags2 = legacyAddTags(tags1, 'rule-1'); - expect(tags2).toEqual(['tag-1', `${INTERNAL_RULE_ALERT_ID_KEY}:rule-1`]); - }); -}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_add_tags.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_add_tags.ts deleted file mode 100644 index b17d8d7226a64..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_add_tags.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 { INTERNAL_RULE_ALERT_ID_KEY } from '../../../../common/constants'; - -/** - * @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function - */ -export const legacyAddTags = (tags: string[], ruleAlertId: string): string[] => - Array.from(new Set([...tags, `${INTERNAL_RULE_ALERT_ID_KEY}:${ruleAlertId}`])); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_create_notifications.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_create_notifications.ts index dec6d51aa9082..e82b19faa40bd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_create_notifications.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_create_notifications.ts @@ -9,8 +9,6 @@ import { SanitizedRule } from '@kbn/alerting-plugin/common'; import { SERVER_APP_ID, LEGACY_NOTIFICATIONS_ID } from '../../../../common/constants'; // eslint-disable-next-line no-restricted-imports import { CreateNotificationParams, LegacyRuleNotificationAlertTypeParams } from './legacy_types'; -// eslint-disable-next-line no-restricted-imports -import { legacyAddTags } from './legacy_add_tags'; /** * @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function @@ -26,7 +24,7 @@ export const legacyCreateNotifications = async ({ rulesClient.create({ data: { name, - tags: legacyAddTags([], ruleAlertId), + tags: [], alertTypeId: LEGACY_NOTIFICATIONS_ID, consumer: SERVER_APP_ID, params: { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_read_notifications.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_read_notifications.ts index 05f64e19df26e..3e28ac879e2c3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_read_notifications.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_read_notifications.ts @@ -10,7 +10,6 @@ import { RuleTypeParams, SanitizedRule } from '@kbn/alerting-plugin/common'; import { LegacyReadNotificationParams, legacyIsAlertType } from './legacy_types'; // eslint-disable-next-line no-restricted-imports import { legacyFindNotifications } from './legacy_find_notifications'; -import { INTERNAL_RULE_ALERT_ID_KEY } from '../../../../common/constants'; /** * @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function @@ -39,7 +38,7 @@ export const legacyReadNotifications = async ({ } else if (ruleAlertId != null) { const notificationFromFind = await legacyFindNotifications({ rulesClient, - filter: `alert.attributes.tags: "${INTERNAL_RULE_ALERT_ID_KEY}:${ruleAlertId}"`, + filter: `alert.attributes.params.ruleAlertId: "${ruleAlertId}"`, page: 1, }); if ( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts index 10607e352000d..8e2f0da4e65c9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -17,8 +17,6 @@ import { DETECTION_ENGINE_SIGNALS_STATUS_URL, DETECTION_ENGINE_PRIVILEGES_URL, DETECTION_ENGINE_QUERY_SIGNALS_URL, - INTERNAL_RULE_ID_KEY, - INTERNAL_IMMUTABLE_KEY, DETECTION_ENGINE_PREPACKAGED_URL, DETECTION_ENGINE_SIGNALS_FINALIZE_MIGRATION_URL, DETECTION_ENGINE_SIGNALS_MIGRATION_STATUS_URL, @@ -393,7 +391,7 @@ export const nonRuleAlert = () => ({ export const getRuleMock = (params: T): SanitizedRule => ({ id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', name: 'Detect Root/Admin Users', - tags: [`${INTERNAL_RULE_ID_KEY}:rule-1`, `${INTERNAL_IMMUTABLE_KEY}:false`], + tags: [], alertTypeId: ruleTypeMappings[params.type], consumer: 'siem', params, @@ -693,7 +691,7 @@ export const getSignalsMigrationStatusRequest = () => export const legacyGetNotificationResult = (): LegacyRuleNotificationAlertType => ({ id: '200dbf2f-b269-4bf9-aa85-11ba32ba73ba', name: 'Notification for Rule Test', - tags: ['__internal_rule_alert_id:85b64e8a-2e40-4096-86af-5ac172c10825'], + tags: [], alertTypeId: 'siem.notifications', consumer: 'siem', params: { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts index d35df4d51ee02..0f70a12e463cf 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts @@ -61,7 +61,7 @@ export const getPrepackagedRulesStatusRoute = ( page: 1, sortField: 'enabled', sortOrder: 'desc', - filter: 'alert.attributes.tags:"__internal_immutable:false"', + filter: 'alert.attributes.params.immutable: false', fields: undefined, }); const frameworkRequest = await buildFrameworkRequest(context, security, request); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/legacy_create_legacy_notification.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/legacy_create_legacy_notification.ts index 6b6e65329c06f..ffccedb691db4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/legacy_create_legacy_notification.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/legacy_create_legacy_notification.ts @@ -16,8 +16,6 @@ import { legacyReadNotifications } from '../../notifications/legacy_read_notific // eslint-disable-next-line no-restricted-imports import { LegacyRuleNotificationAlertTypeParams } from '../../notifications/legacy_types'; // eslint-disable-next-line no-restricted-imports -import { legacyAddTags } from '../../notifications/legacy_add_tags'; -// eslint-disable-next-line no-restricted-imports import { legacyCreateNotifications } from '../../notifications/legacy_create_notifications'; /** @@ -72,7 +70,7 @@ export const legacyCreateLegacyNotificationRoute = ( await rulesClient.update({ id: notification.id, data: { - tags: legacyAddTags([], ruleAlertId), + tags: [], name, schedule: { interval, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.test.ts index 6e2a5d08becbc..2fb1dccc64f80 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.test.ts @@ -13,7 +13,6 @@ import { getIdError, transformFindAlerts, transform, - transformTags, getIdBulkError, transformAlertsToRules, getDuplicates, @@ -23,7 +22,6 @@ import { migrateLegacyActionsIds, } from './utils'; import { getRuleMock } from '../__mocks__/request_responses'; -import { INTERNAL_IDENTIFIER } from '../../../../../common/constants'; import { PartialFilter } from '../../types'; import { BulkError, createBulkErrorObject } from '../utils'; import { getOutputRuleAlertForRest } from '../__mocks__/utils'; @@ -99,9 +97,9 @@ describe('utils', () => { expect(ruleWithEnabledFalse).toEqual(expected); }); - test('should work with tags but filter out any internal tags', () => { + test('should work with tags', () => { const fullRule = getRuleMock(getQueryRuleParams()); - fullRule.tags = ['tag 1', 'tag 2', `${INTERNAL_IDENTIFIER}_some_other_value`]; + fullRule.tags = ['tag 1', 'tag 2']; const rule = internalRuleToAPIResponse(fullRule); const expected = getOutputRuleAlertForRest(); expected.tags = ['tag 1', 'tag 2']; @@ -382,23 +380,6 @@ describe('utils', () => { }); }); - describe('transformTags', () => { - test('it returns tags that have no internal structures', () => { - expect(transformTags(['tag 1', 'tag 2'])).toEqual(['tag 1', 'tag 2']); - }); - - test('it returns empty tags given empty tags', () => { - expect(transformTags([])).toEqual([]); - }); - - test('it returns tags with internal tags stripped out', () => { - expect(transformTags(['tag 1', `${INTERNAL_IDENTIFIER}_some_value`, 'tag 2'])).toEqual([ - 'tag 1', - 'tag 2', - ]); - }); - }); - describe('getIdBulkError', () => { test('outputs message about id and rule_id not being found if both are not null', () => { const error = getIdBulkError({ id: '123', ruleId: '456' }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts index 27f3f160a5288..850d971b39c59 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts @@ -17,7 +17,6 @@ import { RuleExecutionSummary } from '../../../../../common/detection_engine/sch import { RulesSchema } from '../../../../../common/detection_engine/schemas/response/rules_schema'; import { ImportRulesSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/import_rules_schema'; import { CreateRulesBulkSchema } from '../../../../../common/detection_engine/schemas/request/create_rules_bulk_schema'; -import { INTERNAL_IDENTIFIER } from '../../../../../common/constants'; import { RuleAlertType, isAlertType } from '../../rules/types'; import { createBulkErrorObject, BulkError, OutputError } from '../utils'; import { internalRuleToAPIResponse } from '../../schemas/rule_converters'; @@ -88,10 +87,6 @@ export const getIdBulkError = ({ } }; -export const transformTags = (tags: string[]): string[] => { - return tags.filter((tag) => !tag.startsWith(INTERNAL_IDENTIFIER)); -}; - export const transformAlertsToRules = ( rules: RuleAlertType[], legacyRuleActions: Record diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.ts index e1259be5062a0..8492ec6cd2c26 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.ts @@ -78,7 +78,6 @@ import { commonParamsCamelToSnake, typeSpecificCamelToSnake, } from '../../../schemas/rule_converters'; -import { transformTags } from '../../../routes/rules/utils'; import { transformAlertToRuleAction } from '../../../../../../common/detection_engine/transform_actions'; import { AncestorLatest, @@ -215,7 +214,7 @@ export const buildAlert = ( [ALERT_RULE_RULE_ID]: params.ruleId, [ALERT_RULE_RULE_NAME_OVERRIDE]: params.ruleNameOverride, [ALERT_RULE_SEVERITY_MAPPING]: params.severityMapping, - [ALERT_RULE_TAGS]: transformTags(tags), + [ALERT_RULE_TAGS]: tags, [ALERT_RULE_THREAT]: params.threat, [ALERT_RULE_THROTTLE]: throttle ?? undefined, [ALERT_RULE_TIMELINE_ID]: params.timelineId, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/add_tags.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/add_tags.test.ts deleted file mode 100644 index 93fddc06b8068..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/add_tags.test.ts +++ /dev/null @@ -1,49 +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 { addTags } from './add_tags'; -import { INTERNAL_RULE_ID_KEY, INTERNAL_IMMUTABLE_KEY } from '../../../../common/constants'; - -describe('add_tags', () => { - test('it should add a rule id as an internal structure with immutable true', () => { - const tags = addTags([], 'rule-1', true); - expect(tags).toEqual([`${INTERNAL_RULE_ID_KEY}:rule-1`, `${INTERNAL_IMMUTABLE_KEY}:true`]); - }); - - test('it should add a rule id as an internal structure with immutable false', () => { - const tags = addTags([], 'rule-1', false); - expect(tags).toEqual([`${INTERNAL_RULE_ID_KEY}:rule-1`, `${INTERNAL_IMMUTABLE_KEY}:false`]); - }); - - test('it should not allow duplicate tags to be created', () => { - const tags = addTags(['tag-1', 'tag-1'], 'rule-1', false); - expect(tags).toEqual([ - 'tag-1', - `${INTERNAL_RULE_ID_KEY}:rule-1`, - `${INTERNAL_IMMUTABLE_KEY}:false`, - ]); - }); - - test('it should not allow duplicate internal tags to be created when called two times in a row', () => { - const tags1 = addTags(['tag-1'], 'rule-1', false); - const tags2 = addTags(tags1, 'rule-1', false); - expect(tags2).toEqual([ - 'tag-1', - `${INTERNAL_RULE_ID_KEY}:rule-1`, - `${INTERNAL_IMMUTABLE_KEY}:false`, - ]); - }); - - test('it should overwrite existing immutable tag if it exists', () => { - const tags1 = addTags(['tag-1', `${INTERNAL_IMMUTABLE_KEY}:true`], 'rule-1', false); - expect(tags1).toEqual([ - 'tag-1', - `${INTERNAL_RULE_ID_KEY}:rule-1`, - `${INTERNAL_IMMUTABLE_KEY}:false`, - ]); - }); -}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/add_tags.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/add_tags.ts deleted file mode 100644 index d66f961b38598..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/add_tags.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 { INTERNAL_RULE_ID_KEY, INTERNAL_IMMUTABLE_KEY } from '../../../../common/constants'; - -export const addTags = (tags: string[], ruleId: string, immutable: boolean): string[] => { - return Array.from( - new Set([ - ...tags.filter( - (tag) => !(tag.startsWith(INTERNAL_RULE_ID_KEY) || tag.startsWith(INTERNAL_IMMUTABLE_KEY)) - ), - `${INTERNAL_RULE_ID_KEY}:${ruleId}`, - `${INTERNAL_IMMUTABLE_KEY}:${immutable}`, - ]) - ); -}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts index ed2d1e823cd1c..24017adc20626 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts @@ -19,7 +19,6 @@ import { SERVER_APP_ID, } from '../../../../common/constants'; import { CreateRulesOptions } from './types'; -import { addTags } from './add_tags'; import { PartialFilter } from '../types'; import { transformToAlertThrottle, transformToNotifyWhen } from './utils'; @@ -83,7 +82,7 @@ export const createRules = async ({ }, data: { name, - tags: addTags(tags, ruleId, immutable), + tags, alertTypeId: ruleTypeMappings[type], consumer: SERVER_APP_ID, params: { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/duplicate_rule.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/duplicate_rule.test.ts index f93312ade4cfc..04d8e66a076fb 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/duplicate_rule.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/duplicate_rule.test.ts @@ -6,7 +6,6 @@ */ import uuid from 'uuid'; -import { INTERNAL_IMMUTABLE_KEY } from '../../../../common/constants'; import { duplicateRule } from './duplicate_rule'; jest.mock('uuid', () => ({ @@ -22,7 +21,7 @@ describe('duplicateRule', () => { id: 'oldTestRuleId', notifyWhen: 'onActiveAlert', name: 'test', - tags: ['test', '__internal_rule_id:oldTestRuleId', `${INTERNAL_IMMUTABLE_KEY}:false`], + tags: ['test'], alertTypeId: 'siem.signals', consumer: 'siem', params: { @@ -125,8 +124,6 @@ describe('duplicateRule', () => { }, "tags": Array [ "test", - "__internal_rule_id:newId", - "__internal_immutable:false", ], "throttle": null, } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/duplicate_rule.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/duplicate_rule.ts index d3dba0f61df2e..4ef21d0450517 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/duplicate_rule.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/duplicate_rule.ts @@ -13,7 +13,6 @@ import { ruleTypeMappings } from '@kbn/securitysolution-rules'; import { SanitizedRule } from '@kbn/alerting-plugin/common'; import { SERVER_APP_ID } from '../../../../common/constants'; import { InternalRuleCreate, RuleParams } from '../schemas/rule_schemas'; -import { addTags } from './add_tags'; const DUPLICATE_TITLE = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.cloneRule.duplicateTitle', @@ -26,7 +25,7 @@ export const duplicateRule = (rule: SanitizedRule): InternalRuleCrea const newRuleId = uuid.v4(); return { name: `${rule.name} [${DUPLICATE_TITLE}]`, - tags: addTags(rule.tags, newRuleId, false), + tags: rule.tags, alertTypeId: ruleTypeMappings[rule.params.type], consumer: SERVER_APP_ID, params: { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/edit_rule.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/edit_rule.ts index 93a297e6965b4..c7f17e100205c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/edit_rule.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/edit_rule.ts @@ -11,7 +11,6 @@ import { validate } from '@kbn/securitysolution-io-ts-utils'; import type { RulesClient } from '@kbn/alerting-plugin/server'; import { RuleAlertType } from './types'; import { InternalRuleUpdate, internalRuleUpdate } from '../schemas/rule_schemas'; -import { addTags } from './add_tags'; class EditRuleError extends Error { public readonly statusCode: number; @@ -103,10 +102,7 @@ const validateAndSanitizeChanges = ( throw new EditRuleError(`Internal rule editing error: can't change "params.version"`, 500); } - return { - ...changed, - tags: addTags(changed.tags, changed.params.ruleId, changed.params.immutable), - }; + return changed; }; const isRuleChanged = (originalRule: RuleAlertType, editedRule: RuleAlertType): boolean => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_existing_prepackaged_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_existing_prepackaged_rules.ts index a15cc1d3eec1f..440d3db36bd63 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_existing_prepackaged_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_existing_prepackaged_rules.ts @@ -6,12 +6,11 @@ */ import { RulesClient } from '@kbn/alerting-plugin/server'; -import { INTERNAL_IMMUTABLE_KEY } from '../../../../common/constants'; import { RuleAlertType, isAlertTypes } from './types'; import { findRules } from './find_rules'; -export const FILTER_NON_PREPACKED_RULES = `alert.attributes.tags: "${INTERNAL_IMMUTABLE_KEY}:false"`; -export const FILTER_PREPACKED_RULES = `alert.attributes.tags: "${INTERNAL_IMMUTABLE_KEY}:true"`; +export const FILTER_NON_PREPACKED_RULES = 'alert.attributes.params.immutable: false'; +export const FILTER_PREPACKED_RULES = 'alert.attributes.params.immutable: true'; export const getNonPackagedRulesCount = async ({ rulesClient, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.ts index 836c9b035f305..dcf3f7532b53c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.ts @@ -16,7 +16,6 @@ import { RulesSchema } from '../../../../common/detection_engine/schemas/respons import { getExportDetailsNdjson } from './get_export_details_ndjson'; import { isAlertType } from './types'; -import { INTERNAL_RULE_ID_KEY } from '../../../../common/constants'; import { findRules } from './find_rules'; import { getRuleExceptionsForExport } from './get_export_rule_exceptions'; @@ -92,10 +91,8 @@ export const getRulesFromObjects = async ( const chunkedObjects = chunk(objects, 1024); const filter = chunkedObjects .map((chunkedArray) => { - const joinedIds = chunkedArray - .map((object) => `"${INTERNAL_RULE_ID_KEY}:${object.rule_id}"`) - .join(' OR '); - return `alert.attributes.tags: (${joinedIds})`; + const joinedIds = chunkedArray.map((object) => object.rule_id).join(' OR '); + return `alert.attributes.params.ruleId: (${joinedIds})`; }) .join(' OR '); const rules = await findRules({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts index 05f58e44ecaa2..ad2443b34fa95 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts @@ -14,7 +14,6 @@ import { normalizeThresholdObject, } from '../../../../common/detection_engine/utils'; import { internalRuleUpdate, RuleParams } from '../schemas/rule_schemas'; -import { addTags } from './add_tags'; import { PatchRulesOptions } from './types'; import { calculateInterval, @@ -190,7 +189,7 @@ export const patchRules = async ({ ); const newRule = { - tags: addTags(tags ?? rule.tags, rule.params.ruleId, rule.params.immutable), + tags: tags ?? rule.tags, name: calculateName({ updatedName: name, originalName: rule.name }), schedule: { interval: calculateInterval(interval, rule.schedule.interval), diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/read_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/read_rules.ts index 14a63df87ebe8..ef9d867105e10 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/read_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/read_rules.ts @@ -6,7 +6,6 @@ */ import { ResolvedSanitizedRule, SanitizedRule } from '@kbn/alerting-plugin/common'; -import { INTERNAL_RULE_ID_KEY } from '../../../../common/constants'; import { RuleParams } from '../schemas/rule_schemas'; import { findRules } from './find_rules'; import { isAlertType, ReadRuleOptions } from './types'; @@ -17,7 +16,7 @@ import { isAlertType, ReadRuleOptions } from './types'; * and the id will either be found through `rulesClient.get({ id })` or it will not * be returned as a not-found or a thrown error that is not 404. * @param ruleId - This is a close second to being fast as long as it can find the rule_id from - * a filter query against the tags using `alert.attributes.tags: "__internal:${ruleId}"]` + * a filter query against the ruleId property in params using `alert.attributes.params.ruleId: "${ruleId}"` */ export const readRules = async ({ rulesClient, @@ -49,7 +48,7 @@ export const readRules = async ({ } else if (ruleId != null) { const ruleFromFind = await findRules({ rulesClient, - filter: `alert.attributes.tags: "${INTERNAL_RULE_ID_KEY}:${ruleId}"`, + filter: `alert.attributes.params.ruleId: "${ruleId}"`, page: 1, fields: undefined, perPage: undefined, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts index df9a09ba98bb5..ba65b76f01c4a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts @@ -12,7 +12,6 @@ import { DEFAULT_MAX_SIGNALS } from '../../../../common/constants'; import { transformRuleToAlertAction } from '../../../../common/detection_engine/transform_actions'; import { UpdateRulesOptions } from './types'; -import { addTags } from './add_tags'; import { typeSpecificSnakeToCamel } from '../schemas/rule_converters'; import { internalRuleUpdate, RuleParams } from '../schemas/rule_schemas'; import { maybeMute, transformToAlertThrottle, transformToNotifyWhen } from './utils'; @@ -39,7 +38,7 @@ export const updateRules = async ({ const enabled = ruleUpdate.enabled ?? true; const newInternalRule = { name: ruleUpdate.name, - tags: addTags(ruleUpdate.tags ?? [], existingRule.params.ruleId, existingRule.params.immutable), + tags: ruleUpdate.tags ?? [], params: { author: ruleUpdate.author ?? [], buildingBlockType: ruleUpdate.building_block_type, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_converters.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_converters.ts index 8b7fdcf568539..fd80bec1f6ad9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_converters.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_converters.ts @@ -29,10 +29,8 @@ import { ResponseTypeSpecific, } from '../../../../common/detection_engine/schemas/request'; import { AppClient } from '../../../types'; -import { addTags } from '../rules/add_tags'; import { DEFAULT_MAX_SIGNALS, SERVER_APP_ID } from '../../../../common/constants'; import { transformRuleToAlertAction } from '../../../../common/detection_engine/transform_actions'; -import { transformTags } from '../routes/rules/utils'; import { transformFromAlertThrottle, transformToAlertThrottle, @@ -133,7 +131,7 @@ export const convertCreateAPIToInternalSchema = ( const newRuleId = input.rule_id ?? uuid.v4(); return { name: input.name, - tags: addTags(input.tags ?? [], newRuleId, false), + tags: input.tags ?? [], alertTypeId: ruleTypeMappings[input.type], consumer: SERVER_APP_ID, params: { @@ -301,7 +299,7 @@ export const internalRuleToAPIResponse = ( created_at: rule.createdAt.toISOString(), created_by: rule.createdBy ?? 'elastic', name: rule.name, - tags: transformTags(rule.tags), + tags: rule.tags, interval: rule.schedule.interval, enabled: rule.enabled, // Security solution shared rule params diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/find_rule_by_filter.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/find_rule_by_filter.sh index 6aab0cd3c9728..35199b775b33c 100755 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/find_rule_by_filter.sh +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/find_rule_by_filter.sh @@ -26,13 +26,13 @@ FILTER=${1:-'alert.attributes.enabled:%20true'} # ./find_rule_by_filter.sh "alert.attributes.tags:tag_1" # Example get all pre-packaged rules -# ./find_rule_by_filter.sh "alert.attributes.tags:%20%22__internal_immutable:true%22" +# ./find_rule_by_filter.sh "alert.attributes.params.immutable:%20true" # Example get all non pre-packaged rules -# ./find_rule_by_filter.sh "alert.attributes.tags:%20%22__internal_immutable:false%22" +# ./find_rule_by_filter.sh "alert.attributes.params.immutable:%20false" # Example get all non pre-packaged rules and a tag_1 -# ./find_rule_by_filter.sh "alert.attributes.tags:%20%22__internal_immutable:false%22%20AND%20alert.attributes.tags:tag_1" +# ./find_rule_by_filter.sh "alert.attributes.params.immutable:%20false%20AND%20alert.attributes.tags:tag_1" curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ -X GET ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules/_find?filter=$FILTER | jq . diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/README.md b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/README.md index c76a69db084ca..cf8e3262e2826 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/README.md +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/README.md @@ -53,10 +53,7 @@ to any newly saved rule: "_source" : { "alert" : { "name" : "kql test rule 1", - "tags" : [ - "__internal_rule_id:4ec223b9-77fa-4895-8539-6b3e586a2858", - "__internal_immutable:false" - ], + "tags" : [], "alertTypeId" : "siem.signals", "other data... other data": "other data...other data", "exceptionsList" : [ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/tags/read_tags.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/tags/read_tags.test.ts index 4d6327e05f2d3..2154e672e0089 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/tags/read_tags.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/tags/read_tags.test.ts @@ -6,10 +6,8 @@ */ import { rulesClientMock } from '@kbn/alerting-plugin/server/mocks'; - import { getRuleMock, getFindResultWithMultiHits } from '../routes/__mocks__/request_responses'; -import { INTERNAL_RULE_ID_KEY, INTERNAL_IDENTIFIER } from '../../../../common/constants'; -import { readRawTags, readTags, convertTagsToSet, convertToTags, isTags } from './read_tags'; +import { readTags, convertTagsToSet, convertToTags, isTags } from './read_tags'; import { getQueryRuleParams } from '../schemas/rule_schemas.mock'; describe('read_tags', () => { @@ -17,88 +15,6 @@ describe('read_tags', () => { jest.resetAllMocks(); }); - describe('readRawTags', () => { - test('it should return the intersection of tags to where none are repeating', async () => { - const result1 = getRuleMock(getQueryRuleParams()); - result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result1.params.ruleId = 'rule-1'; - result1.tags = ['tag 1', 'tag 2', 'tag 3']; - - const result2 = getRuleMock(getQueryRuleParams()); - result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result2.params.ruleId = 'rule-2'; - result2.tags = ['tag 1', 'tag 2', 'tag 3', 'tag 4']; - - const rulesClient = rulesClientMock.create(); - rulesClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1, result2] })); - - const tags = await readRawTags({ rulesClient }); - expect(tags).toEqual(['tag 1', 'tag 2', 'tag 3', 'tag 4']); - }); - - test('it should return the intersection of tags to where some are repeating values', async () => { - const result1 = getRuleMock(getQueryRuleParams()); - result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result1.params.ruleId = 'rule-1'; - result1.tags = ['tag 1', 'tag 2', 'tag 2', 'tag 3']; - - const result2 = getRuleMock(getQueryRuleParams()); - result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result2.params.ruleId = 'rule-2'; - result2.tags = ['tag 1', 'tag 2', 'tag 2', 'tag 3', 'tag 4']; - - const rulesClient = rulesClientMock.create(); - rulesClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1, result2] })); - - const tags = await readRawTags({ rulesClient }); - expect(tags).toEqual(['tag 1', 'tag 2', 'tag 3', 'tag 4']); - }); - - test('it should work with no tags defined between two results', async () => { - const result1 = getRuleMock(getQueryRuleParams()); - result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result1.params.ruleId = 'rule-1'; - result1.tags = []; - - const result2 = getRuleMock(getQueryRuleParams()); - result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result2.params.ruleId = 'rule-2'; - result2.tags = []; - - const rulesClient = rulesClientMock.create(); - rulesClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1, result2] })); - - const tags = await readRawTags({ rulesClient }); - expect(tags).toEqual([]); - }); - - test('it should work with a single tag which has repeating values in it', async () => { - const result1 = getRuleMock(getQueryRuleParams()); - result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result1.params.ruleId = 'rule-1'; - result1.tags = ['tag 1', 'tag 1', 'tag 1', 'tag 2']; - - const rulesClient = rulesClientMock.create(); - rulesClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1] })); - - const tags = await readRawTags({ rulesClient }); - expect(tags).toEqual(['tag 1', 'tag 2']); - }); - - test('it should work with a single tag which has empty tags', async () => { - const result1 = getRuleMock(getQueryRuleParams()); - result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result1.params.ruleId = 'rule-1'; - result1.tags = []; - - const rulesClient = rulesClientMock.create(); - rulesClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1] })); - - const tags = await readRawTags({ rulesClient }); - expect(tags).toEqual([]); - }); - }); - describe('readTags', () => { test('it should return the intersection of tags to where none are repeating', async () => { const result1 = getRuleMock(getQueryRuleParams()); @@ -179,56 +95,6 @@ describe('read_tags', () => { const tags = await readTags({ rulesClient }); expect(tags).toEqual([]); }); - - test('it should filter out any __internal tags for things such as alert_id', async () => { - const result1 = getRuleMock(getQueryRuleParams()); - result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result1.params.ruleId = 'rule-1'; - result1.tags = [ - `${INTERNAL_IDENTIFIER}_some_value`, - `${INTERNAL_RULE_ID_KEY}_some_value`, - 'tag 1', - ]; - - const rulesClient = rulesClientMock.create(); - rulesClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1] })); - - const tags = await readTags({ rulesClient }); - expect(tags).toEqual(['tag 1']); - }); - - test('it should filter out any __internal tags with two different results', async () => { - const result1 = getRuleMock(getQueryRuleParams()); - result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result1.params.ruleId = 'rule-1'; - result1.tags = [ - `${INTERNAL_IDENTIFIER}_some_value`, - `${INTERNAL_RULE_ID_KEY}_some_value`, - 'tag 1', - 'tag 2', - 'tag 3', - 'tag 4', - 'tag 5', - ]; - - const result2 = getRuleMock(getQueryRuleParams()); - result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result2.params.ruleId = 'rule-2'; - result2.tags = [ - `${INTERNAL_IDENTIFIER}_some_value`, - `${INTERNAL_RULE_ID_KEY}_some_value`, - 'tag 1', - 'tag 2', - 'tag 3', - 'tag 4', - ]; - - const rulesClient = rulesClientMock.create(); - rulesClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1] })); - - const tags = await readTags({ rulesClient }); - expect(tags).toEqual(['tag 1', 'tag 2', 'tag 3', 'tag 4', 'tag 5']); - }); }); describe('convertTagsToSet', () => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/tags/read_tags.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/tags/read_tags.ts index 8f3e1468278cf..4ab3ccc831af1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/tags/read_tags.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/tags/read_tags.ts @@ -7,7 +7,6 @@ import { has } from 'lodash/fp'; import { RulesClient } from '@kbn/alerting-plugin/server'; -import { INTERNAL_IDENTIFIER } from '../../../../common/constants'; import { findRules } from '../rules/find_rules'; export interface TagType { @@ -20,11 +19,11 @@ export const isTags = (obj: object): obj is TagType => { }; export const convertToTags = (tagObjects: object[]): string[] => { - const tags = tagObjects.reduce((accum, tagObj) => { + const tags = tagObjects.reduce((acc, tagObj) => { if (isTags(tagObj)) { - return [...accum, ...tagObj.tags]; + return [...acc, ...tagObj.tags]; } else { - return accum; + return acc; } }, []); return tags; @@ -41,15 +40,6 @@ export const convertTagsToSet = (tagObjects: object[]): Set => { // Ref: https://www.elastic.co/guide/en/kibana/master/saved-objects-api.html export const readTags = async ({ rulesClient, -}: { - rulesClient: RulesClient; -}): Promise => { - const tags = await readRawTags({ rulesClient }); - return tags.filter((tag) => !tag.startsWith(INTERNAL_IDENTIFIER)); -}; - -export const readRawTags = async ({ - rulesClient, }: { rulesClient: RulesClient; perPage?: number; diff --git a/x-pack/plugins/security_solution/server/usage/detections/get_metrics.test.ts b/x-pack/plugins/security_solution/server/usage/detections/get_metrics.test.ts index 4e782f914df70..9c40d662544c1 100644 --- a/x-pack/plugins/security_solution/server/usage/detections/get_metrics.test.ts +++ b/x-pack/plugins/security_solution/server/usage/detections/get_metrics.test.ts @@ -131,7 +131,7 @@ describe('Detections Usage and Metrics', () => { esClient.search.mockResponseOnce(getEventLogElasticRules()); esClient.search.mockResponseOnce(getElasticLogCustomRules()); esClient.search.mockResponseOnce(getMockRuleAlertsResponse(800)); - savedObjectsClient.find.mockResolvedValueOnce(getMockRuleSearchResponse('not_immutable')); + savedObjectsClient.find.mockResolvedValueOnce(getMockRuleSearchResponse(false)); savedObjectsClient.find.mockResolvedValueOnce(getMockAlertCaseCommentsResponse()); // Get empty saved object for legacy notification system. savedObjectsClient.find.mockResolvedValueOnce(getEmptySavedObjectResponse()); diff --git a/x-pack/plugins/security_solution/server/usage/detections/ml_jobs/get_metrics.mocks.ts b/x-pack/plugins/security_solution/server/usage/detections/ml_jobs/get_metrics.mocks.ts index 0863ca6cef411..4ad8f2213a682 100644 --- a/x-pack/plugins/security_solution/server/usage/detections/ml_jobs/get_metrics.mocks.ts +++ b/x-pack/plugins/security_solution/server/usage/detections/ml_jobs/get_metrics.mocks.ts @@ -291,7 +291,7 @@ export const getMockMlDatafeedStatsResponse = () => ({ }); export const getMockRuleSearchResponse = ( - immutableTag: string = '__internal_immutable:true' + immutable: boolean = true ): SavedObjectsFindResponse => ({ page: 1, @@ -304,16 +304,7 @@ export const getMockRuleSearchResponse = ( namespaces: ['default'], attributes: { name: 'Azure Diagnostic Settings Deletion', - tags: [ - 'Elastic', - 'Cloud', - 'Azure', - 'Continuous Monitoring', - 'SecOps', - 'Monitoring', - '__internal_rule_id:5370d4cd-2bb3-4d71-abf5-1e1d0ff5a2de', - `${immutableTag}`, - ], + tags: ['Elastic', 'Cloud', 'Azure', 'Continuous Monitoring', 'SecOps', 'Monitoring'], alertTypeId: 'siem.queryRule', consumer: 'siem', params: { @@ -326,7 +317,7 @@ export const getMockRuleSearchResponse = ( 'Deletion of diagnostic settings may be done by a system or network administrator. Verify whether the username, hostname, and/or resource name should be making changes in your environment. Diagnostic settings deletion from unfamiliar users or hosts should be investigated. If known behavior is causing false positives, it can be exempted from the rule.', ], from: 'now-25m', - immutable: true, + immutable, query: 'event.dataset:azure.activitylogs and azure.activitylogs.operation_name:"MICROSOFT.INSIGHTS/DIAGNOSTICSETTINGS/DELETE" and event.outcome:(Success or success)', language: 'kuery', diff --git a/x-pack/plugins/security_solution/server/usage/detections/rules/transform_utils/get_rule_object_correlations.ts b/x-pack/plugins/security_solution/server/usage/detections/rules/transform_utils/get_rule_object_correlations.ts index 169cb502521df..5387c63aace3e 100644 --- a/x-pack/plugins/security_solution/server/usage/detections/rules/transform_utils/get_rule_object_correlations.ts +++ b/x-pack/plugins/security_solution/server/usage/detections/rules/transform_utils/get_rule_object_correlations.ts @@ -9,8 +9,6 @@ import type { SavedObjectsFindResult } from '@kbn/core/server'; import type { RuleMetric } from '../types'; import type { RuleSearchResult } from '../../../types'; -import { isElasticRule } from '../../../queries/utils/is_elastic_rule'; - export interface RuleObjectCorrelationsOptions { ruleResults: Array>; legacyNotificationRuleIds: Map< @@ -32,7 +30,6 @@ export const getRuleObjectCorrelations = ({ return ruleResults.map((result) => { const ruleId = result.id; const { attributes } = result; - const isElastic = isElasticRule(attributes.tags); // Even if the legacy notification is set to "no_actions" we still count the rule as having a legacy notification that is not migrated yet. const hasLegacyNotification = legacyNotificationRuleIds.get(ruleId) != null; @@ -50,7 +47,8 @@ export const getRuleObjectCorrelations = ({ rule_type: attributes.params.type, rule_version: attributes.params.version, enabled: attributes.enabled, - elastic_rule: isElastic, + // if rule immutable, it's Elastic/prebuilt + elastic_rule: attributes.params.immutable, created_on: attributes.createdAt, updated_on: attributes.updatedAt, alert_count_daily: alertsCounts.get(ruleId) || 0, diff --git a/x-pack/plugins/security_solution/server/usage/queries/utils/is_elastic_rule.ts b/x-pack/plugins/security_solution/server/usage/queries/utils/is_elastic_rule.ts deleted file mode 100644 index f08959702b290..0000000000000 --- a/x-pack/plugins/security_solution/server/usage/queries/utils/is_elastic_rule.ts +++ /dev/null @@ -1,11 +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 { INTERNAL_IMMUTABLE_KEY } from '../../../../common/constants'; - -export const isElasticRule = (tags: string[] = []) => - tags.includes(`${INTERNAL_IMMUTABLE_KEY}:true`); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts index 33cb64dcdbed1..4622e84081506 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts @@ -398,14 +398,9 @@ export default function createGetTests({ getService }: FtrProviderContext) { // Only the rule that was enabled should be tagged expect(responseEnabledBeforeMigration.body._source?.alert?.tags).to.eql([ - '__internal_rule_id:064e3fed-6328-416b-bb85-c08265088f41', - '__internal_immutable:false', 'auto_disabled_8.0', ]); - expect(responseDisabledBeforeMigration.body._source?.alert?.tags).to.eql([ - '__internal_rule_id:364e3fed-6328-416b-bb85-c08265088f41', - '__internal_immutable:false', - ]); + expect(responseDisabledBeforeMigration.body._source?.alert?.tags).to.eql([]); }); it('8.2.0 migrates params to mapped_params for specific params properties', async () => { @@ -423,5 +418,18 @@ export default function createGetTests({ getService }: FtrProviderContext) { severity: '80-critical', }); }); + + it('8.3.0 removes internal tags in Security Solution rule', async () => { + const response = await es.get<{ alert: RawRule }>( + { + index: '.kibana', + id: 'alert:8990af61-c09a-11ec-9164-4bfd6fc32c43', + }, + { meta: true } + ); + + expect(response.statusCode).to.equal(200); + expect(response.body._source?.alert?.tags).to.eql(['test-tag-1', 'foo-tag']); + }); }); } diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/resolve_read_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/resolve_read_rules.ts index 2b4b31bb5510b..5c2bdafbd5c32 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/resolve_read_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/resolve_read_rules.ts @@ -65,10 +65,7 @@ export default ({ getService }: FtrProviderContext) => { body: { alert: { name: 'test 7.14', - tags: [ - '__internal_rule_id:82747bb8-bae0-4b59-8119-7f65ac564e14', - '__internal_immutable:false', - ], + tags: [], alertTypeId: 'siem.queryRule', consumer: 'siem', params: { diff --git a/x-pack/test/detection_engine_api_integration/utils/downgrade_immutable_rule.ts b/x-pack/test/detection_engine_api_integration/utils/downgrade_immutable_rule.ts index 36ebfaee231d9..57f20a1c0e645 100644 --- a/x-pack/test/detection_engine_api_integration/utils/downgrade_immutable_rule.ts +++ b/x-pack/test/detection_engine_api_integration/utils/downgrade_immutable_rule.ts @@ -7,7 +7,6 @@ import type { ToolingLog } from '@kbn/tooling-log'; import type { Client } from '@elastic/elasticsearch'; -import { INTERNAL_RULE_ID_KEY } from '@kbn/security-solution-plugin/common/constants'; import { countDownES } from './count_down_es'; export const downgradeImmutableRule = async ( @@ -29,7 +28,7 @@ export const downgradeImmutableRule = async ( }, query: { term: { - 'alert.tags': `${INTERNAL_RULE_ID_KEY}:${ruleId}`, + 'alert.params.ruleId': ruleId, }, }, }, diff --git a/x-pack/test/detection_engine_api_integration/utils/find_immutable_rule_by_id.ts b/x-pack/test/detection_engine_api_integration/utils/find_immutable_rule_by_id.ts index 2d77cffcdfe41..0e6fe73685c48 100644 --- a/x-pack/test/detection_engine_api_integration/utils/find_immutable_rule_by_id.ts +++ b/x-pack/test/detection_engine_api_integration/utils/find_immutable_rule_by_id.ts @@ -9,11 +9,7 @@ import type { ToolingLog } from '@kbn/tooling-log'; import type SuperTest from 'supertest'; import type { FullResponseSchema } from '@kbn/security-solution-plugin/common/detection_engine/schemas/request'; -import { - DETECTION_ENGINE_RULES_URL, - INTERNAL_IMMUTABLE_KEY, - INTERNAL_RULE_ID_KEY, -} from '@kbn/security-solution-plugin/common/constants'; +import { DETECTION_ENGINE_RULES_URL } from '@kbn/security-solution-plugin/common/constants'; /** * Helper to cut down on the noise in some of the tests. This @@ -32,7 +28,7 @@ export const findImmutableRuleById = async ( }> => { const response = await supertest .get( - `${DETECTION_ENGINE_RULES_URL}/_find?filter=alert.attributes.tags: "${INTERNAL_IMMUTABLE_KEY}:true" AND alert.attributes.tags: "${INTERNAL_RULE_ID_KEY}:${ruleId}"` + `${DETECTION_ENGINE_RULES_URL}/_find?filter=alert.attributes.params.immutable: true AND alert.attributes.params.ruleId: "${ruleId}"` ) .set('kbn-xsrf', 'true') .send(); diff --git a/x-pack/test/functional/es_archives/alerts/data.json b/x-pack/test/functional/es_archives/alerts/data.json index 39ce6248c7ebb..1c096d9df9930 100644 --- a/x-pack/test/functional/es_archives/alerts/data.json +++ b/x-pack/test/functional/es_archives/alerts/data.json @@ -671,10 +671,7 @@ "source":{ "alert":{ "name":"enabled 7.16.1 query rule", - "tags":[ - "__internal_rule_id:064e3fed-6328-416b-bb85-c08265088f41", - "__internal_immutable:false" - ], + "tags":[], "alertTypeId":"siem.signals", "consumer":"siem", "params":{ @@ -767,10 +764,7 @@ "source":{ "alert":{ "name":"disabled 7.16.1 query rule", - "tags":[ - "__internal_rule_id:364e3fed-6328-416b-bb85-c08265088f41", - "__internal_immutable:false" - ], + "tags":[], "alertTypeId":"siem.signals", "consumer":"siem", "params":{ @@ -895,4 +889,104 @@ ] } } +} + +{ + "type":"doc", + "value":{ + "id":"alert:8990af61-c09a-11ec-9164-4bfd6fc32c43", + "index":".kibana_1", + "source":{ + "alert":{ + "name":"Test remove internal tags", + "alertTypeId":"siem.queryRule", + "consumer":"siem", + "params":{ + "immutable":true, + "ruleId":"bf9638eb-7d3c-4f40-83d7-8c40a7c80f2e", + "author":[ + + ], + "description":"remove interns tags mock rule", + "falsePositives":[ + + ], + "from":"now-36000060s", + "license":"", + "outputIndex":".siem-signals-default", + "meta":{ + "from":"10000h" + }, + "maxSignals":100, + "riskScore":21, + "riskScoreMapping":[ + + ], + "severity":"low", + "severityMapping":[ + + ], + "threat":[ + + ], + "to":"now", + "references":[ + + ], + "version":4, + "exceptionsList":[ + ], + "type":"query", + "language":"kuery", + "index":[ + "apm-*-transaction*", + "traces-apm*", + "auditbeat-*", + "endgame-*", + "filebeat-*", + "logs-*", + "packetbeat-*", + "winlogbeat-*", + "test-index-3" + ], + "query":"*:*", + "filters":[ + + ] + }, + "schedule":{ + "interval":"1m" + }, + "enabled":true, + "actions":[ + + ], + "throttle":null, + "apiKeyOwner":null, + "apiKey":null, + "createdBy":"elastic", + "updatedBy":"elastic", + "createdAt":"2021-07-27T20:42:55.896Z", + "muteAll":false, + "mutedInstanceIds":[ + + ], + "scheduledTaskId":null, + "tags":[ + "__internal_rule_id:364e3fed-6328-416b-bb85-c08265088f41", + "__internal_immutable:false", + "test-tag-1", + "foo-tag" + ] + }, + "type":"alert", + "migrationVersion":{ + "alert":"8.2.0" + }, + "updated_at":"2021-08-13T23:00:11.985Z", + "references":[ + + ] + } + } } \ No newline at end of file From caedd861b0958f74f954a9e188db8c3884377a29 Mon Sep 17 00:00:00 2001 From: Kyle Pollich Date: Tue, 26 Apr 2022 12:49:07 -0400 Subject: [PATCH 25/29] Fix open API docs for excludeInstallStatus query param (#130986) --- .../plugins/fleet/common/openapi/bundled.json | 559 ++++++++++++++---- .../plugins/fleet/common/openapi/bundled.yaml | 9 +- .../common/openapi/paths/epm@packages.yaml | 8 +- 3 files changed, 459 insertions(+), 117 deletions(-) diff --git a/x-pack/plugins/fleet/common/openapi/bundled.json b/x-pack/plugins/fleet/common/openapi/bundled.json index 77dd66a5c20d1..b3c37f5e567c3 100644 --- a/x-pack/plugins/fleet/common/openapi/bundled.json +++ b/x-pack/plugins/fleet/common/openapi/bundled.json @@ -115,7 +115,10 @@ "type": "string" } }, - "required": ["name", "version"] + "required": [ + "name", + "version" + ] } } } @@ -292,7 +295,17 @@ }, "operationId": "list-all-packages" }, - "parameters": [] + "parameters": [ + { + "in": "query", + "name": "excludeInstallStatus", + "schema": { + "type": "boolean", + "default": false + }, + "description": "Whether to exclude the install status of each package. Enabling this option will opt in to caching for the response via `cache-control` headers. If you don't need up-to-date installation info for a package, and are querying for a list of available packages, providing this flag can improve performance substantially." + } + ] }, "/epm/packages/_bulk": { "post": { @@ -337,13 +350,21 @@ "properties": { "status": { "type": "string", - "enum": ["installed", "installing", "install_failed", "not_installed"] + "enum": [ + "installed", + "installing", + "install_failed", + "not_installed" + ] }, "savedObject": { "type": "string" } }, - "required": ["status", "savedObject"] + "required": [ + "status", + "savedObject" + ] } ] } @@ -399,11 +420,16 @@ ] } }, - "required": ["id", "type"] + "required": [ + "id", + "type" + ] } } }, - "required": ["response"] + "required": [ + "response" + ] } } } @@ -462,11 +488,16 @@ ] } }, - "required": ["id", "type"] + "required": [ + "id", + "type" + ] } } }, - "required": ["response"] + "required": [ + "response" + ] } } } @@ -518,13 +549,21 @@ "properties": { "status": { "type": "string", - "enum": ["installed", "installing", "install_failed", "not_installed"] + "enum": [ + "installed", + "installing", + "install_failed", + "not_installed" + ] }, "savedObject": { "type": "string" } }, - "required": ["status", "savedObject"] + "required": [ + "status", + "savedObject" + ] } ] } @@ -587,7 +626,10 @@ ] } }, - "required": ["id", "type"] + "required": [ + "id", + "type" + ] } }, "_meta": { @@ -595,12 +637,18 @@ "properties": { "install_source": { "type": "string", - "enum": ["registry", "upload", "bundled"] + "enum": [ + "registry", + "upload", + "bundled" + ] } } } }, - "required": ["items"] + "required": [ + "items" + ] } } } @@ -661,11 +709,16 @@ ] } }, - "required": ["id", "type"] + "required": [ + "id", + "type" + ] } } }, - "required": ["items"] + "required": [ + "items" + ] } } } @@ -736,11 +789,16 @@ ] } }, - "required": ["id", "type"] + "required": [ + "id", + "type" + ] } } }, - "required": ["items"] + "required": [ + "items" + ] } } } @@ -840,7 +898,9 @@ "$ref": "#/components/schemas/package_usage_stats" } }, - "required": ["response"] + "required": [ + "response" + ] } } } @@ -915,7 +975,10 @@ "type": "string" } }, - "required": ["admin_username", "admin_password"] + "required": [ + "admin_username", + "admin_password" + ] } } } @@ -1156,7 +1219,9 @@ "type": "string" } }, - "required": ["success"] + "required": [ + "success" + ] } } } @@ -1217,7 +1282,9 @@ "$ref": "#/components/schemas/agent" } }, - "required": ["item"] + "required": [ + "item" + ] } } } @@ -1240,7 +1307,9 @@ "$ref": "#/components/schemas/agent" } }, - "required": ["item"] + "required": [ + "item" + ] } } } @@ -1266,10 +1335,14 @@ "properties": { "action": { "type": "string", - "enum": ["deleted"] + "enum": [ + "deleted" + ] } }, - "required": ["action"] + "required": [ + "action" + ] } } } @@ -1389,7 +1462,9 @@ "type": "string" } }, - "required": ["policy_id"] + "required": [ + "policy_id" + ] } } } @@ -1436,7 +1511,9 @@ }, "statusCode": { "type": "number", - "enum": [400] + "enum": [ + 400 + ] } } } @@ -1544,7 +1621,9 @@ "type": "string" } }, - "required": ["success"] + "required": [ + "success" + ] } } } @@ -1580,7 +1659,10 @@ ] } }, - "required": ["policy_id", "agents"] + "required": [ + "policy_id", + "agents" + ] } } } @@ -1608,7 +1690,9 @@ "type": "string" } }, - "required": ["success"] + "required": [ + "success" + ] } } } @@ -1647,7 +1731,9 @@ ] } }, - "required": ["agents"] + "required": [ + "agents" + ] } } } @@ -1682,7 +1768,12 @@ "type": "number" } }, - "required": ["items", "total", "page", "perPage"] + "required": [ + "items", + "total", + "page", + "perPage" + ] } } } @@ -1766,7 +1857,9 @@ "$ref": "#/components/schemas/agent_policy" } }, - "required": ["item"] + "required": [ + "item" + ] } } } @@ -1791,7 +1884,9 @@ "$ref": "#/components/schemas/agent_policy" } }, - "required": ["item"] + "required": [ + "item" + ] } } } @@ -1845,7 +1940,9 @@ "$ref": "#/components/schemas/agent_policy" } }, - "required": ["item"] + "required": [ + "item" + ] } } } @@ -1864,7 +1961,9 @@ "type": "string" } }, - "required": ["name"] + "required": [ + "name" + ] } } }, @@ -1891,7 +1990,10 @@ "type": "boolean" } }, - "required": ["id", "success"] + "required": [ + "id", + "success" + ] } } } @@ -1907,7 +2009,9 @@ "type": "string" } }, - "required": ["agentPolicyId"] + "required": [ + "agentPolicyId" + ] } } } @@ -1983,7 +2087,12 @@ "type": "number" } }, - "required": ["items", "page", "perPage", "total"] + "required": [ + "items", + "page", + "perPage", + "total" + ] } } } @@ -2009,7 +2118,9 @@ }, "action": { "type": "string", - "enum": ["created"] + "enum": [ + "created" + ] } } } @@ -2052,7 +2163,9 @@ "$ref": "#/components/schemas/enrollment_api_key" } }, - "required": ["item"] + "required": [ + "item" + ] } } } @@ -2074,10 +2187,14 @@ "properties": { "action": { "type": "string", - "enum": ["deleted"] + "enum": [ + "deleted" + ] } }, - "required": ["action"] + "required": [ + "action" + ] } } } @@ -2127,7 +2244,12 @@ "type": "number" } }, - "required": ["items", "page", "perPage", "total"] + "required": [ + "items", + "page", + "perPage", + "total" + ] } } } @@ -2152,7 +2274,9 @@ }, "action": { "type": "string", - "enum": ["created"] + "enum": [ + "created" + ] } } } @@ -2194,7 +2318,9 @@ "$ref": "#/components/schemas/enrollment_api_key" } }, - "required": ["item"] + "required": [ + "item" + ] } } } @@ -2215,10 +2341,14 @@ "properties": { "action": { "type": "string", - "enum": ["deleted"] + "enum": [ + "deleted" + ] } }, - "required": ["action"] + "required": [ + "action" + ] } } } @@ -2260,7 +2390,9 @@ "type": "number" } }, - "required": ["items"] + "required": [ + "items" + ] } } } @@ -2286,7 +2418,9 @@ "$ref": "#/components/schemas/package_policy" } }, - "required": ["item"] + "required": [ + "item" + ] } } } @@ -2348,7 +2482,9 @@ "type": "boolean" } }, - "required": ["packagePolicyIds"] + "required": [ + "packagePolicyIds" + ] } } } @@ -2373,7 +2509,10 @@ "type": "boolean" } }, - "required": ["id", "success"] + "required": [ + "id", + "success" + ] } } } @@ -2404,7 +2543,9 @@ } } }, - "required": ["packagePolicyIds"] + "required": [ + "packagePolicyIds" + ] } } } @@ -2429,7 +2570,10 @@ "type": "boolean" } }, - "required": ["id", "success"] + "required": [ + "id", + "success" + ] } } } @@ -2458,7 +2602,9 @@ "type": "string" } }, - "required": ["packagePolicyIds"] + "required": [ + "packagePolicyIds" + ] } } } @@ -2483,7 +2629,9 @@ "$ref": "#/components/schemas/upgrade_agent_diff" } }, - "required": ["hasErrors"] + "required": [ + "hasErrors" + ] } } } @@ -2508,7 +2656,9 @@ "$ref": "#/components/schemas/package_policy" } }, - "required": ["item"] + "required": [ + "item" + ] } } } @@ -2553,7 +2703,10 @@ "type": "boolean" } }, - "required": ["item", "sucess"] + "required": [ + "item", + "sucess" + ] } } } @@ -2636,7 +2789,9 @@ }, "type": { "type": "string", - "enum": ["elasticsearch"] + "enum": [ + "elasticsearch" + ] }, "is_default": { "type": "boolean" @@ -2657,7 +2812,10 @@ "type": "string" } }, - "required": ["name", "type"] + "required": [ + "name", + "type" + ] } } } @@ -2681,7 +2839,9 @@ "$ref": "#/components/schemas/output" } }, - "required": ["item"] + "required": [ + "item" + ] } } } @@ -2714,7 +2874,9 @@ "type": "string" } }, - "required": ["id"] + "required": [ + "id" + ] } } } @@ -2740,7 +2902,9 @@ }, "type": { "type": "string", - "enum": ["elasticsearch"] + "enum": [ + "elasticsearch" + ] }, "is_default": { "type": "boolean" @@ -2764,7 +2928,10 @@ "type": "string" } }, - "required": ["name", "type"] + "required": [ + "name", + "type" + ] } } } @@ -2781,7 +2948,9 @@ "$ref": "#/components/schemas/output" } }, - "required": ["item"] + "required": [ + "item" + ] } } } @@ -2900,11 +3069,17 @@ "type": "string" } }, - "required": ["name", "message"] + "required": [ + "name", + "message" + ] } } }, - "required": ["isInitialized", "nonFatalErrors"] + "required": [ + "isInitialized", + "nonFatalErrors" + ] }, "preconfigured_agent_policies": { "title": "Preconfigured agent policies", @@ -2926,7 +3101,10 @@ "type": "array", "items": { "type": "string", - "enum": ["logs", "metrics"] + "enum": [ + "logs", + "metrics" + ] } }, "namespace": { @@ -3023,14 +3201,20 @@ } } }, - "required": ["type"] + "required": [ + "type" + ] } } } } } }, - "required": ["name", "namespace", "package_policies"] + "required": [ + "name", + "namespace", + "package_policies" + ] }, "settings": { "title": "Settings", @@ -3052,7 +3236,10 @@ } } }, - "required": ["fleet_server_hosts", "id"] + "required": [ + "fleet_server_hosts", + "id" + ] }, "fleet_settings_response": { "title": "Fleet settings response", @@ -3062,7 +3249,9 @@ "$ref": "#/components/schemas/settings" } }, - "required": ["item"] + "required": [ + "item" + ] }, "get_categories_response": { "title": "Get categories response", @@ -3084,7 +3273,11 @@ "type": "number" } }, - "required": ["id", "title", "count"] + "required": [ + "id", + "title", + "count" + ] } }, "items": { @@ -3102,11 +3295,17 @@ "type": "number" } }, - "required": ["id", "title", "count"] + "required": [ + "id", + "title", + "count" + ] } } }, - "required": ["items"] + "required": [ + "items" + ] }, "search_result": { "title": "Search result", @@ -3173,7 +3372,9 @@ } } }, - "required": ["items"] + "required": [ + "items" + ] }, "bulk_install_packages_response": { "title": "Bulk install packages response", @@ -3209,7 +3410,9 @@ } } }, - "required": ["items"] + "required": [ + "items" + ] }, "package_info": { "title": "Package information", @@ -3289,7 +3492,10 @@ "type": "string" } }, - "required": ["src", "path"] + "required": [ + "src", + "path" + ] } }, "icons": { @@ -3339,7 +3545,10 @@ "type": "string" } }, - "required": ["name", "default"] + "required": [ + "name", + "default" + ] } }, "type": { @@ -3349,7 +3558,14 @@ "type": "string" } }, - "required": ["title", "name", "release", "ingeset_pipeline", "type", "package"] + "required": [ + "title", + "name", + "release", + "ingeset_pipeline", + "type", + "package" + ] } }, "download": { @@ -3411,7 +3627,9 @@ "type": "integer" } }, - "required": ["agent_policy_count"] + "required": [ + "agent_policy_count" + ] }, "fleet_status_response": { "title": "Fleet status response", @@ -3424,23 +3642,38 @@ "type": "array", "items": { "type": "string", - "enum": ["tls_required", "api_keys", "fleet_admin_user", "fleet_server"] + "enum": [ + "tls_required", + "api_keys", + "fleet_admin_user", + "fleet_server" + ] } }, "missing_optional_features": { "type": "array", "items": { "type": "string", - "enum": ["encrypted_saved_object_encryption_key_required"] + "enum": [ + "encrypted_saved_object_encryption_key_required" + ] } } }, - "required": ["isReady", "missing_requirements", "missing_optional_features"] + "required": [ + "isReady", + "missing_requirements", + "missing_optional_features" + ] }, "agent_type": { "type": "string", "title": "Agent type", - "enum": ["PERMANENT", "EPHEMERAL", "TEMPORARY"] + "enum": [ + "PERMANENT", + "EPHEMERAL", + "TEMPORARY" + ] }, "agent_metadata": { "title": "Agent metadata", @@ -3449,7 +3682,13 @@ "agent_status": { "type": "string", "title": "Agent status", - "enum": ["offline", "error", "online", "inactive", "warning"] + "enum": [ + "offline", + "error", + "online", + "inactive", + "warning" + ] }, "agent": { "title": "Agent", @@ -3504,7 +3743,13 @@ "type": "string" } }, - "required": ["type", "active", "enrolled_at", "id", "status"] + "required": [ + "type", + "active", + "enrolled_at", + "id", + "status" + ] }, "get_agents_response": { "title": "Get Agent response", @@ -3533,7 +3778,12 @@ "type": "number" } }, - "required": ["items", "total", "page", "perPage"] + "required": [ + "items", + "total", + "page", + "perPage" + ] }, "bulk_upgrade_agents": { "title": "Bulk upgrade agents", @@ -3559,7 +3809,10 @@ ] } }, - "required": ["agents", "version"] + "required": [ + "agents", + "version" + ] }, "upgrade_agent": { "title": "Upgrade agent", @@ -3571,7 +3824,9 @@ "type": "string" } }, - "required": ["version"] + "required": [ + "version" + ] }, { "type": "object", @@ -3583,7 +3838,9 @@ "type": "string" } }, - "required": ["version"] + "required": [ + "version" + ] } ] }, @@ -3600,7 +3857,12 @@ }, "type": { "type": "string", - "enum": ["POLICY_CHANGE", "UNENROLL", "UPGRADE", "POLICY_REASSIGN"] + "enum": [ + "POLICY_CHANGE", + "UNENROLL", + "UPGRADE", + "POLICY_REASSIGN" + ] } } }, @@ -3614,7 +3876,12 @@ "properties": { "log_level": { "type": "string", - "enum": ["debug", "info", "warning", "error"] + "enum": [ + "debug", + "info", + "warning", + "error" + ] } } } @@ -3642,7 +3909,10 @@ "type": "array", "items": { "type": "string", - "enum": ["metrics", "logs"] + "enum": [ + "metrics", + "logs" + ] } }, "data_output_id": { @@ -3654,7 +3924,10 @@ "nullable": true } }, - "required": ["name", "namespace"] + "required": [ + "name", + "namespace" + ] }, "new_package_policy": { "title": "New package policy", @@ -3677,7 +3950,10 @@ "type": "string" } }, - "required": ["name", "version"] + "required": [ + "name", + "version" + ] }, "namespace": { "type": "string" @@ -3713,7 +3989,10 @@ "type": "object" } }, - "required": ["type", "enabled"] + "required": [ + "type", + "enabled" + ] } }, "policy_id": { @@ -3726,7 +4005,10 @@ "type": "string" } }, - "required": ["inputs", "name"] + "required": [ + "inputs", + "name" + ] }, "package_policy": { "title": "Package policy", @@ -3745,7 +4027,10 @@ "items": {} } }, - "required": ["id", "revision"] + "required": [ + "id", + "revision" + ] }, { "$ref": "#/components/schemas/new_package_policy" @@ -3765,7 +4050,10 @@ }, "status": { "type": "string", - "enum": ["active", "inactive"] + "enum": [ + "active", + "inactive" + ] }, "packagePolicies": { "oneOf": [ @@ -3803,7 +4091,10 @@ "type": "number" } }, - "required": ["id", "status"] + "required": [ + "id", + "status" + ] } ] }, @@ -3880,7 +4171,13 @@ "type": "string" } }, - "required": ["id", "api_key_id", "api_key", "active", "created_at"] + "required": [ + "id", + "api_key_id", + "api_key", + "active", + "created_at" + ] }, "upgrade_diff": { "title": "Package policy Upgrade dryrun", @@ -3946,10 +4243,16 @@ "type": "string" } }, - "required": ["dataset", "type"] + "required": [ + "dataset", + "type" + ] } }, - "required": ["id", "data_stream"] + "required": [ + "id", + "data_stream" + ] } ] }, @@ -3979,7 +4282,9 @@ "type": "string" } }, - "required": ["namespace"] + "required": [ + "namespace" + ] }, "use_output": { "type": "string" @@ -3998,7 +4303,10 @@ "type": "string" } }, - "required": ["name", "version"] + "required": [ + "name", + "version" + ] } } }, @@ -4006,7 +4314,14 @@ "$ref": "#/components/schemas/full_agent_policy_input_stream" } }, - "required": ["id", "name", "revision", "type", "data_stream", "use_output"] + "required": [ + "id", + "name", + "revision", + "type", + "data_stream", + "use_output" + ] } ] }, @@ -4044,7 +4359,11 @@ "type": "string" } }, - "required": ["name", "title", "version"] + "required": [ + "name", + "title", + "version" + ] }, "namespace": { "type": "string" @@ -4080,7 +4399,11 @@ "type": "object" } }, - "required": ["type", "enabled", "streams"] + "required": [ + "type", + "enabled", + "streams" + ] } }, "policy_id": { @@ -4096,7 +4419,12 @@ "type": "boolean" } }, - "required": ["name", "namespace", "policy_id", "enabled"] + "required": [ + "name", + "namespace", + "policy_id", + "enabled" + ] }, "output": { "title": "Output", @@ -4116,7 +4444,9 @@ }, "type": { "type": "string", - "enum": ["elasticsearch"] + "enum": [ + "elasticsearch" + ] }, "hosts": { "type": "array", @@ -4154,7 +4484,12 @@ } } }, - "required": ["id", "is_default", "name", "type"] + "required": [ + "id", + "is_default", + "name", + "type" + ] } } }, @@ -4163,4 +4498,4 @@ "basicAuth": [] } ] -} +} \ No newline at end of file diff --git a/x-pack/plugins/fleet/common/openapi/bundled.yaml b/x-pack/plugins/fleet/common/openapi/bundled.yaml index 011a242df7fdc..36175ffc59a88 100644 --- a/x-pack/plugins/fleet/common/openapi/bundled.yaml +++ b/x-pack/plugins/fleet/common/openapi/bundled.yaml @@ -186,13 +186,16 @@ paths: operationId: list-all-packages parameters: - in: query - name: includeInstallStatus + name: excludeInstallStatus schema: type: boolean default: false description: >- - Whether to include the install status of each package. Defaults to - false to allow for caching of package requests. + Whether to exclude the install status of each package. Enabling this + option will opt in to caching for the response via `cache-control` + headers. If you don't need up-to-date installation info for a package, + and are querying for a list of available packages, providing this flag + can improve performance substantially. /epm/packages/_bulk: post: summary: Packages - Bulk install diff --git a/x-pack/plugins/fleet/common/openapi/paths/epm@packages.yaml b/x-pack/plugins/fleet/common/openapi/paths/epm@packages.yaml index 295ab298745fe..9c29b9d18357c 100644 --- a/x-pack/plugins/fleet/common/openapi/paths/epm@packages.yaml +++ b/x-pack/plugins/fleet/common/openapi/paths/epm@packages.yaml @@ -11,8 +11,12 @@ get: operationId: list-all-packages parameters: - in: query - name: includeInstallStatus + name: excludeInstallStatus schema: type: boolean default: false - description: Whether to include the install status of each package. Defaults to false to allow for caching of package requests. + description: >- + Whether to exclude the install status of each package. Enabling this option will opt in to + caching for the response via `cache-control` headers. If you don't need up-to-date installation + info for a package, and are querying for a list of available packages, providing this flag can + improve performance substantially. From 7ff1ffca5e8a684157bcbba59df4456a8afe190f Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Tue, 26 Apr 2022 13:07:32 -0400 Subject: [PATCH 26/29] [CI] Split OSS CI Group 11 (#130927) --- .buildkite/pipelines/es_snapshots/verify.yml | 2 +- .buildkite/pipelines/on_merge.yml | 2 +- .buildkite/pipelines/pull_request/base.yml | 2 +- .ci/ci_groups.yml | 1 + test/functional/apps/visualize/index.ts | 7 ++++++- 5 files changed, 10 insertions(+), 4 deletions(-) diff --git a/.buildkite/pipelines/es_snapshots/verify.yml b/.buildkite/pipelines/es_snapshots/verify.yml index 23ec38085c3d9..a4a6b1303f70d 100755 --- a/.buildkite/pipelines/es_snapshots/verify.yml +++ b/.buildkite/pipelines/es_snapshots/verify.yml @@ -42,7 +42,7 @@ steps: - command: .buildkite/scripts/steps/functional/oss_cigroup.sh label: 'OSS CI Group' - parallelism: 11 + parallelism: 12 agents: queue: ci-group-4d depends_on: build diff --git a/.buildkite/pipelines/on_merge.yml b/.buildkite/pipelines/on_merge.yml index 8702493d9f4cf..8b94f4d7c22c7 100644 --- a/.buildkite/pipelines/on_merge.yml +++ b/.buildkite/pipelines/on_merge.yml @@ -66,7 +66,7 @@ steps: - command: .buildkite/scripts/steps/functional/oss_cigroup.sh label: 'OSS CI Group' - parallelism: 11 + parallelism: 12 agents: queue: n2-4-spot depends_on: build diff --git a/.buildkite/pipelines/pull_request/base.yml b/.buildkite/pipelines/pull_request/base.yml index 658d855d86cfd..fc12a96964fb2 100644 --- a/.buildkite/pipelines/pull_request/base.yml +++ b/.buildkite/pipelines/pull_request/base.yml @@ -32,7 +32,7 @@ steps: - command: .buildkite/scripts/steps/functional/oss_cigroup.sh label: 'OSS CI Group' - parallelism: 11 + parallelism: 12 agents: queue: n2-4-spot depends_on: build diff --git a/.ci/ci_groups.yml b/.ci/ci_groups.yml index c3786f299d4c0..508f118ce9dd7 100644 --- a/.ci/ci_groups.yml +++ b/.ci/ci_groups.yml @@ -10,6 +10,7 @@ root: - ciGroup9 - ciGroup10 - ciGroup11 + - ciGroup12 xpack: - ciGroup1 diff --git a/test/functional/apps/visualize/index.ts b/test/functional/apps/visualize/index.ts index 5ccb59e17880e..d68fb4b253123 100644 --- a/test/functional/apps/visualize/index.ts +++ b/test/functional/apps/visualize/index.ts @@ -97,8 +97,13 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { describe('visualize ciGroup11', function () { this.tags('ciGroup11'); - loadTestFile(require.resolve('./_tag_cloud')); loadTestFile(require.resolve('./_tsvb_time_series')); + }); + + describe('visualize ciGroup12', function () { + this.tags('ciGroup12'); + + loadTestFile(require.resolve('./_tag_cloud')); loadTestFile(require.resolve('./_tsvb_markdown')); loadTestFile(require.resolve('./_tsvb_table')); loadTestFile(require.resolve('./_vega_chart')); From 181626b9c665327226cbb183b5909f2c26270e1d Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Tue, 26 Apr 2022 13:32:08 -0400 Subject: [PATCH 27/29] [CI] Split default ciGroup24 up a bit (#130997) --- .../tests/exception_operators_data_types/index.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/index.ts index 85cc484146032..ded3df8b6716c 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/index.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/index.ts @@ -22,13 +22,23 @@ export default ({ loadTestFile }: FtrProviderContext): void => { describe('', function () { this.tags('ciGroup24'); - loadTestFile(require.resolve('./ip')); - loadTestFile(require.resolve('./ip_array')); loadTestFile(require.resolve('./keyword')); loadTestFile(require.resolve('./keyword_array')); loadTestFile(require.resolve('./long')); loadTestFile(require.resolve('./text')); loadTestFile(require.resolve('./text_array')); }); + + describe('', function () { + this.tags('ciGroup16'); + + loadTestFile(require.resolve('./ip')); + }); + + describe('', function () { + this.tags('ciGroup21'); + + loadTestFile(require.resolve('./ip_array')); + }); }); }; From 26217f3a7a0c266bf9ddf079633a0fcc10877e27 Mon Sep 17 00:00:00 2001 From: liza-mae Date: Tue, 26 Apr 2022 11:52:36 -0600 Subject: [PATCH 28/29] Upgrade patch reporting issue, add more tests, reorganize files (#130839) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- test/functional/page_objects/home_page.ts | 15 +++++ .../functional/page_objects/graph_page.ts | 5 ++ .../upgrade/apps/canvas/canvas_smoke_tests.ts | 20 +++---- .../apps/canvas/{index.js => index.ts} | 6 +- .../apps/dashboard/dashboard_smoke_tests.ts | 4 +- .../apps/dashboard/{index.js => index.ts} | 6 +- .../apps/discover/discover_smoke_tests.ts | 57 +++++++++++++++++++ x-pack/test/upgrade/apps/discover/index.ts | 14 +++++ .../upgrade/apps/graph/graph_smoke_tests.ts | 45 +++++++++++++++ x-pack/test/upgrade/apps/graph/index.ts | 14 +++++ x-pack/test/upgrade/apps/logs/index.ts | 14 +++++ .../upgrade/apps/logs/logs_smoke_tests.ts | 39 +++++++++++++ .../upgrade/apps/maps/{index.js => index.ts} | 6 +- .../upgrade/apps/maps/maps_smoke_tests.ts | 8 +-- .../apps/reporting/{index.js => index.ts} | 6 +- .../apps/reporting/reporting_smoke_tests.ts | 23 +++++--- x-pack/test/upgrade/config.ts | 9 ++- x-pack/test/upgrade/ftr_provider_context.d.ts | 2 +- x-pack/test/upgrade/page_objects.ts | 10 ---- .../{services.ts => services/index.ts} | 4 +- .../{ => services}/maps_upgrade_services.ts | 2 +- .../reporting_upgrade_services.ts} | 8 +-- 22 files changed, 265 insertions(+), 52 deletions(-) rename x-pack/test/upgrade/apps/canvas/{index.js => index.ts} (72%) rename x-pack/test/upgrade/apps/dashboard/{index.js => index.ts} (72%) create mode 100644 x-pack/test/upgrade/apps/discover/discover_smoke_tests.ts create mode 100644 x-pack/test/upgrade/apps/discover/index.ts create mode 100644 x-pack/test/upgrade/apps/graph/graph_smoke_tests.ts create mode 100644 x-pack/test/upgrade/apps/graph/index.ts create mode 100644 x-pack/test/upgrade/apps/logs/index.ts create mode 100644 x-pack/test/upgrade/apps/logs/logs_smoke_tests.ts rename x-pack/test/upgrade/apps/maps/{index.js => index.ts} (72%) rename x-pack/test/upgrade/apps/reporting/{index.js => index.ts} (72%) delete mode 100644 x-pack/test/upgrade/page_objects.ts rename x-pack/test/upgrade/{services.ts => services/index.ts} (73%) rename x-pack/test/upgrade/{ => services}/maps_upgrade_services.ts (97%) rename x-pack/test/upgrade/{reporting_services.ts => services/reporting_upgrade_services.ts} (95%) diff --git a/test/functional/page_objects/home_page.ts b/test/functional/page_objects/home_page.ts index 235afa27f0c82..1e3e6a9634f4c 100644 --- a/test/functional/page_objects/home_page.ts +++ b/test/functional/page_objects/home_page.ts @@ -93,6 +93,21 @@ export class HomePageObject extends FtrService { await this.find.clickByLinkText('Map'); } + async launchSampleLogs(id: string) { + await this.launchSampleDataSet(id); + await this.find.clickByLinkText('Logs'); + } + + async launchSampleGraph(id: string) { + await this.launchSampleDataSet(id); + await this.find.clickByLinkText('Graph'); + } + + async launchSampleML(id: string) { + await this.launchSampleDataSet(id); + await this.find.clickByLinkText('ML jobs'); + } + async launchSampleDataSet(id: string) { await this.addSampleDataSet(id); await this.common.closeToastIfExists(); diff --git a/x-pack/test/functional/page_objects/graph_page.ts b/x-pack/test/functional/page_objects/graph_page.ts index 1203d4077b5b8..aec58d27acbbe 100644 --- a/x-pack/test/functional/page_objects/graph_page.ts +++ b/x-pack/test/functional/page_objects/graph_page.ts @@ -283,4 +283,9 @@ export class GraphPageObject extends FtrService { const el = await this.find.byCssSelector('small.gphLinkSummary__term--2'); return await el.getVisibleText(); } + + async getAllGraphNodes() { + const el = await this.find.allByCssSelector('.gphNode'); + return el.length; + } } diff --git a/x-pack/test/upgrade/apps/canvas/canvas_smoke_tests.ts b/x-pack/test/upgrade/apps/canvas/canvas_smoke_tests.ts index b12b751e94c07..7de0e1247f01a 100644 --- a/x-pack/test/upgrade/apps/canvas/canvas_smoke_tests.ts +++ b/x-pack/test/upgrade/apps/canvas/canvas_smoke_tests.ts @@ -14,7 +14,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const browser = getService('browser'); - describe('canvas smoke tests', function describeIndexTests() { + describe('upgrade canvas smoke tests', function describeIndexTests() { const spaces = [ { space: 'default', basePath: '' }, { space: 'automation', basePath: 's/automation' }, @@ -28,17 +28,17 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { ]; spaces.forEach(({ space, basePath }) => { - describe('space ' + space, () => { - beforeEach(async () => { - await PageObjects.common.navigateToActualUrl('home', '/tutorial_directory/sampleData', { - basePath, - }); - await PageObjects.header.waitUntilLoadingHasFinished(); - }); - canvasTests.forEach(({ name, numElements, page }) => { - it('renders elements on workpad ' + name + ' page ' + page, async () => { + canvasTests.forEach(({ name, numElements, page }) => { + describe('space: ' + space, () => { + before(async () => { + await PageObjects.common.navigateToActualUrl('home', '/tutorial_directory/sampleData', { + basePath, + }); + await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.home.launchSampleCanvas(name); await PageObjects.header.waitUntilLoadingHasFinished(); + }); + it('renders elements on workpad ' + name + ' page ' + page, async () => { const currentUrl = await browser.getCurrentUrl(); const [, hash] = currentUrl.split('#/'); if (hash.length === 0) { diff --git a/x-pack/test/upgrade/apps/canvas/index.js b/x-pack/test/upgrade/apps/canvas/index.ts similarity index 72% rename from x-pack/test/upgrade/apps/canvas/index.js rename to x-pack/test/upgrade/apps/canvas/index.ts index 0ecc2e98ea67a..f28bff9a1820c 100644 --- a/x-pack/test/upgrade/apps/canvas/index.js +++ b/x-pack/test/upgrade/apps/canvas/index.ts @@ -5,8 +5,10 @@ * 2.0. */ -export default ({ loadTestFile }) => { +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { describe('upgrade', function () { loadTestFile(require.resolve('./canvas_smoke_tests')); }); -}; +} diff --git a/x-pack/test/upgrade/apps/dashboard/dashboard_smoke_tests.ts b/x-pack/test/upgrade/apps/dashboard/dashboard_smoke_tests.ts index 2f35f0e1e12d3..b87ec4dfe4159 100644 --- a/x-pack/test/upgrade/apps/dashboard/dashboard_smoke_tests.ts +++ b/x-pack/test/upgrade/apps/dashboard/dashboard_smoke_tests.ts @@ -18,7 +18,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const browser = getService('browser'); - describe('dashboard smoke tests', function describeIndexTests() { + describe('upgrade dashboard smoke tests', function describeIndexTests() { const spaces = [ { space: 'default', basePath: '' }, { space: 'automation', basePath: 's/automation' }, @@ -31,7 +31,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { ]; spaces.forEach(({ space, basePath }) => { - describe('space ' + space, () => { + describe('space: ' + space, () => { beforeEach(async () => { await PageObjects.common.navigateToActualUrl('home', '/tutorial_directory/sampleData', { basePath, diff --git a/x-pack/test/upgrade/apps/dashboard/index.js b/x-pack/test/upgrade/apps/dashboard/index.ts similarity index 72% rename from x-pack/test/upgrade/apps/dashboard/index.js rename to x-pack/test/upgrade/apps/dashboard/index.ts index b12d1270f79f9..1f380a18dfb70 100644 --- a/x-pack/test/upgrade/apps/dashboard/index.js +++ b/x-pack/test/upgrade/apps/dashboard/index.ts @@ -5,8 +5,10 @@ * 2.0. */ -export default ({ loadTestFile }) => { +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { describe('upgrade', function () { loadTestFile(require.resolve('./dashboard_smoke_tests')); }); -}; +} diff --git a/x-pack/test/upgrade/apps/discover/discover_smoke_tests.ts b/x-pack/test/upgrade/apps/discover/discover_smoke_tests.ts new file mode 100644 index 0000000000000..150458919d41d --- /dev/null +++ b/x-pack/test/upgrade/apps/discover/discover_smoke_tests.ts @@ -0,0 +1,57 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const PageObjects = getPageObjects(['common', 'header', 'home', 'discover', 'timePicker']); + + describe('upgrade discover smoke tests', function describeIndexTests() { + const spaces = [ + { space: 'default', basePath: '' }, + { space: 'automation', basePath: 's/automation' }, + ]; + + const discoverTests = [ + { name: 'kibana_sample_data_flights', timefield: true, hits: '' }, + { name: 'kibana_sample_data_logs', timefield: true, hits: '' }, + { name: 'kibana_sample_data_ecommerce', timefield: true, hits: '' }, + ]; + + spaces.forEach(({ space, basePath }) => { + discoverTests.forEach(({ name, timefield, hits }) => { + describe('space: ' + space + ', name: ' + name, () => { + before(async () => { + await PageObjects.common.navigateToApp('discover', { + basePath, + }); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.discover.selectIndexPattern(name); + await PageObjects.discover.waitUntilSearchingHasFinished(); + if (timefield) { + await PageObjects.timePicker.setCommonlyUsedTime('Last_24 hours'); + await PageObjects.discover.waitUntilSearchingHasFinished(); + } + }); + it('shows hit count greater than zero', async () => { + const hitCount = await PageObjects.discover.getHitCount(); + if (hits === '') { + expect(hitCount).to.be.greaterThan(0); + } else { + expect(hitCount).to.be.equal(hits); + } + }); + it('shows table rows not empty', async () => { + const tableRows = await PageObjects.discover.getDocTableRows(); + expect(tableRows.length).to.be.greaterThan(0); + }); + }); + }); + }); + }); +} diff --git a/x-pack/test/upgrade/apps/discover/index.ts b/x-pack/test/upgrade/apps/discover/index.ts new file mode 100644 index 0000000000000..18fdd6992b892 --- /dev/null +++ b/x-pack/test/upgrade/apps/discover/index.ts @@ -0,0 +1,14 @@ +/* + * 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 { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('upgrade', function () { + loadTestFile(require.resolve('./discover_smoke_tests')); + }); +} diff --git a/x-pack/test/upgrade/apps/graph/graph_smoke_tests.ts b/x-pack/test/upgrade/apps/graph/graph_smoke_tests.ts new file mode 100644 index 0000000000000..cca4e220c7aa5 --- /dev/null +++ b/x-pack/test/upgrade/apps/graph/graph_smoke_tests.ts @@ -0,0 +1,45 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const PageObjects = getPageObjects(['common', 'header', 'home', 'graph']); + + describe('upgrade graph smoke tests', function describeIndexTests() { + const spaces = [ + { space: 'default', basePath: '' }, + { space: 'automation', basePath: 's/automation' }, + ]; + + const graphTests = [ + { name: 'flights', numNodes: 91 }, + { name: 'logs', numNodes: 27 }, + { name: 'ecommerce', numNodes: 12 }, + ]; + + spaces.forEach(({ space, basePath }) => { + graphTests.forEach(({ name, numNodes }) => { + describe('space: ' + space, () => { + before(async () => { + await PageObjects.common.navigateToActualUrl('home', '/tutorial_directory/sampleData', { + basePath, + }); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.home.launchSampleGraph(name); + await PageObjects.header.waitUntilLoadingHasFinished(); + }); + it('renders graph for ' + name, async () => { + const elements = await PageObjects.graph.getAllGraphNodes(); + expect(elements).to.be.equal(numNodes); + }); + }); + }); + }); + }); +} diff --git a/x-pack/test/upgrade/apps/graph/index.ts b/x-pack/test/upgrade/apps/graph/index.ts new file mode 100644 index 0000000000000..649b1a740bb6c --- /dev/null +++ b/x-pack/test/upgrade/apps/graph/index.ts @@ -0,0 +1,14 @@ +/* + * 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 { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('upgrade', function () { + loadTestFile(require.resolve('./graph_smoke_tests')); + }); +} diff --git a/x-pack/test/upgrade/apps/logs/index.ts b/x-pack/test/upgrade/apps/logs/index.ts new file mode 100644 index 0000000000000..ed4181f75d76d --- /dev/null +++ b/x-pack/test/upgrade/apps/logs/index.ts @@ -0,0 +1,14 @@ +/* + * 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 { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('upgrade', function () { + loadTestFile(require.resolve('./logs_smoke_tests')); + }); +} diff --git a/x-pack/test/upgrade/apps/logs/logs_smoke_tests.ts b/x-pack/test/upgrade/apps/logs/logs_smoke_tests.ts new file mode 100644 index 0000000000000..8d9964f25422e --- /dev/null +++ b/x-pack/test/upgrade/apps/logs/logs_smoke_tests.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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const PageObjects = getPageObjects(['common', 'header', 'home']); + const logsUi = getService('logsUi'); + + describe('upgrade logs smoke tests', function describeIndexTests() { + const spaces = [ + { space: 'default', basePath: '' }, + { space: 'automation', basePath: 's/automation' }, + ]; + + spaces.forEach(({ space, basePath }) => { + describe('space: ' + space, () => { + before(async () => { + await PageObjects.common.navigateToActualUrl('home', '/tutorial_directory/sampleData', { + basePath, + }); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.home.launchSampleLogs('logs'); + await PageObjects.header.waitUntilLoadingHasFinished(); + }); + + it('should show log streams', async () => { + const logStreamEntries = await logsUi.logStreamPage.getStreamEntries(); + expect(logStreamEntries.length).to.be.greaterThan(100); + }); + }); + }); + }); +} diff --git a/x-pack/test/upgrade/apps/maps/index.js b/x-pack/test/upgrade/apps/maps/index.ts similarity index 72% rename from x-pack/test/upgrade/apps/maps/index.js rename to x-pack/test/upgrade/apps/maps/index.ts index 57d175a62ceb3..7fac9ae891128 100644 --- a/x-pack/test/upgrade/apps/maps/index.js +++ b/x-pack/test/upgrade/apps/maps/index.ts @@ -5,8 +5,10 @@ * 2.0. */ -export default ({ loadTestFile }) => { +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { describe('upgrade', function () { loadTestFile(require.resolve('./maps_smoke_tests')); }); -}; +} diff --git a/x-pack/test/upgrade/apps/maps/maps_smoke_tests.ts b/x-pack/test/upgrade/apps/maps/maps_smoke_tests.ts index 98e31129a3ccd..ecb000d691ab0 100644 --- a/x-pack/test/upgrade/apps/maps/maps_smoke_tests.ts +++ b/x-pack/test/upgrade/apps/maps/maps_smoke_tests.ts @@ -82,7 +82,7 @@ export default function ({ // Only update the baseline images from Jenkins session images after comparing them // These tests might fail locally because of scaling factors and resolution. - describe('maps smoke tests', function describeIndexTests() { + describe('upgrade maps smoke tests', function describeIndexTests() { const spaces = [ { space: 'default', basePath: '' }, { space: 'automation', basePath: 's/automation' }, @@ -101,7 +101,7 @@ export default function ({ }); spaces.forEach(({ space, basePath }) => { - describe('space ' + space + ' ecommerce', () => { + describe('space: ' + space + ', name: ecommerce', () => { before(async () => { await PageObjects.common.navigateToActualUrl('home', '/tutorial_directory/sampleData', { basePath, @@ -129,7 +129,7 @@ export default function ({ expect(percentDifference.toFixed(3)).to.be.lessThan(0.031); }); }); - describe('space ' + space + ' flights', () => { + describe('space: ' + space + ', name: flights', () => { before(async () => { await PageObjects.common.navigateToActualUrl('home', '/tutorial_directory/sampleData', { basePath, @@ -153,7 +153,7 @@ export default function ({ expect(percentDifference.toFixed(3)).to.be.lessThan(0.031); }); }); - describe('space ' + space + ' web logs', () => { + describe('space: ' + space + ', name: web logs', () => { before(async () => { await PageObjects.common.navigateToActualUrl('home', '/tutorial_directory/sampleData', { basePath, diff --git a/x-pack/test/upgrade/apps/reporting/index.js b/x-pack/test/upgrade/apps/reporting/index.ts similarity index 72% rename from x-pack/test/upgrade/apps/reporting/index.js rename to x-pack/test/upgrade/apps/reporting/index.ts index 5bd5081a3568c..c4c70660935ad 100644 --- a/x-pack/test/upgrade/apps/reporting/index.js +++ b/x-pack/test/upgrade/apps/reporting/index.ts @@ -5,8 +5,10 @@ * 2.0. */ -export default ({ loadTestFile }) => { +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { describe('upgrade', function () { loadTestFile(require.resolve('./reporting_smoke_tests')); }); -}; +} diff --git a/x-pack/test/upgrade/apps/reporting/reporting_smoke_tests.ts b/x-pack/test/upgrade/apps/reporting/reporting_smoke_tests.ts index 14136b23abfd5..f7b0eef003c91 100644 --- a/x-pack/test/upgrade/apps/reporting/reporting_smoke_tests.ts +++ b/x-pack/test/upgrade/apps/reporting/reporting_smoke_tests.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; import { parse } from 'url'; import { FtrProviderContext } from '../../ftr_provider_context'; -import { ReportingUsageStats } from '../../reporting_services'; +import { ReportingUsageStats } from '../../services/reporting_upgrade_services'; interface UsageStats { reporting: ReportingUsageStats; @@ -21,6 +21,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const browser = getService('browser'); const PageObjects = getPageObjects(['common', 'header', 'home', 'dashboard', 'share']); const testSubjects = getService('testSubjects'); + const log = getService('log'); const spaces = [ { space: 'default', basePath: '' }, @@ -39,7 +40,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { { name: 'ecommerce', type: 'png', link: 'PNG Reports' }, ]; - describe('reporting smoke tests', () => { + describe('upgrade reporting smoke tests', () => { let completedReportCount: number; let usage: UsageStats; describe('initial state', () => { @@ -53,7 +54,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); spaces.forEach(({ space, basePath }) => { - describe('generate report space ' + space, () => { + describe('generate report for space ' + space, () => { beforeEach(async () => { await PageObjects.common.navigateToActualUrl('home', '/tutorial_directory/sampleData', { basePath, @@ -63,7 +64,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { completedReportCount = reportingAPI.getCompletedReportCount(usage); }); reportingTests.forEach(({ name, type, link }) => { - it('name ' + name + ' type ' + type, async () => { + it('name: ' + name + ' type: ' + type, async () => { await PageObjects.home.launchSampleDashboard(name); await PageObjects.share.openShareMenuItem(link); if (type === 'pdf_optimize') { @@ -89,9 +90,17 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const postUrl = await find.byXPath(`//button[descendant::*[text()='Copy POST URL']]`); await postUrl.click(); const url = await browser.getClipboardValue(); - await reportingAPI.expectAllJobsToFinishSuccessfully([ - await reportingAPI.postJob(parse(url).pathname + '?' + parse(url).query), - ]); + // Add try/catch for https://github.com/elastic/elastic-stack-testing/issues/1199 + // Waiting for job to finish sometimes gets socket hang up error, from what I + // observed during debug testing the command does complete. + // Checking expected report count will still fail if the job did not finish. + try { + await reportingAPI.expectAllJobsToFinishSuccessfully([ + await reportingAPI.postJob(parse(url).pathname + '?' + parse(url).query), + ]); + } catch (e) { + log.debug(`Error waiting for job to finish: ${e}`); + } usage = (await usageAPI.getUsageStats()) as UsageStats; reportingAPI.expectCompletedReportCount(usage, completedReportCount + 1); }); diff --git a/x-pack/test/upgrade/config.ts b/x-pack/test/upgrade/config.ts index 7722c244223cf..78d61d5239556 100644 --- a/x-pack/test/upgrade/config.ts +++ b/x-pack/test/upgrade/config.ts @@ -6,9 +6,9 @@ */ import { FtrConfigProviderContext } from '@kbn/test'; -import { pageObjects } from './page_objects'; -import { ReportingAPIProvider } from './reporting_services'; -import { MapsHelper } from './maps_upgrade_services'; +import { pageObjects } from '../functional/page_objects'; +import { ReportingAPIProvider } from './services/reporting_upgrade_services'; +import { MapsHelper } from './services/maps_upgrade_services'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { const apiConfig = await readConfigFile(require.resolve('../api_integration/config')); @@ -20,6 +20,9 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { testFiles: [ require.resolve('./apps/canvas'), require.resolve('./apps/dashboard'), + require.resolve('./apps/discover'), + require.resolve('./apps/graph'), + require.resolve('./apps/logs'), require.resolve('./apps/maps'), require.resolve('./apps/reporting'), ], diff --git a/x-pack/test/upgrade/ftr_provider_context.d.ts b/x-pack/test/upgrade/ftr_provider_context.d.ts index 24f5087ef7fe2..2cd67b6698a70 100644 --- a/x-pack/test/upgrade/ftr_provider_context.d.ts +++ b/x-pack/test/upgrade/ftr_provider_context.d.ts @@ -7,7 +7,7 @@ import { GenericFtrProviderContext } from '@kbn/test'; -import { pageObjects } from './page_objects'; +import { pageObjects } from '../functional/page_objects'; import { services } from './services'; export type FtrProviderContext = GenericFtrProviderContext; diff --git a/x-pack/test/upgrade/page_objects.ts b/x-pack/test/upgrade/page_objects.ts deleted file mode 100644 index c8b0c9050dbb9..0000000000000 --- a/x-pack/test/upgrade/page_objects.ts +++ /dev/null @@ -1,10 +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 { pageObjects } from '../functional/page_objects'; - -export { pageObjects }; diff --git a/x-pack/test/upgrade/services.ts b/x-pack/test/upgrade/services/index.ts similarity index 73% rename from x-pack/test/upgrade/services.ts rename to x-pack/test/upgrade/services/index.ts index ca5c23ba335e3..cfd227cd86aff 100644 --- a/x-pack/test/upgrade/services.ts +++ b/x-pack/test/upgrade/services/index.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { services as functionalServices } from '../functional/services'; -import { services as reportingServices } from './reporting_services'; +import { services as functionalServices } from '../../functional/services'; +import { services as reportingServices } from './reporting_upgrade_services'; import { services as mapsUpgradeServices } from './maps_upgrade_services'; export const services = { diff --git a/x-pack/test/upgrade/maps_upgrade_services.ts b/x-pack/test/upgrade/services/maps_upgrade_services.ts similarity index 97% rename from x-pack/test/upgrade/maps_upgrade_services.ts rename to x-pack/test/upgrade/services/maps_upgrade_services.ts index b5553eeb9366d..7b80bce7682f0 100644 --- a/x-pack/test/upgrade/maps_upgrade_services.ts +++ b/x-pack/test/upgrade/services/maps_upgrade_services.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from './ftr_provider_context'; +import { FtrProviderContext } from '../ftr_provider_context'; export function MapsHelper({ getPageObjects, getService }: FtrProviderContext) { const PageObjects = getPageObjects(['maps']); diff --git a/x-pack/test/upgrade/reporting_services.ts b/x-pack/test/upgrade/services/reporting_upgrade_services.ts similarity index 95% rename from x-pack/test/upgrade/reporting_services.ts rename to x-pack/test/upgrade/services/reporting_upgrade_services.ts index 2467cee0ad822..1a1e6113c3735 100644 --- a/x-pack/test/upgrade/reporting_services.ts +++ b/x-pack/test/upgrade/services/reporting_upgrade_services.ts @@ -7,9 +7,9 @@ import expect from '@kbn/expect'; import { indexTimestamp } from '@kbn/reporting-plugin/server/lib/store/index_timestamp'; -import { services as xpackServices } from '../functional/services'; -import { services as apiIntegrationServices } from '../api_integration/services'; -import { FtrProviderContext } from './ftr_provider_context'; +import { services as xpackServices } from '../../functional/services'; +import { services as apiIntegrationServices } from '../../api_integration/services'; +import { FtrProviderContext } from '../ftr_provider_context'; interface PDFAppCounts { app: { @@ -104,7 +104,7 @@ export function ReportingAPIProvider({ getService }: FtrProviderContext) { * * @return {Promise} A function to call to clean up the index alias that was added. */ - async coerceReportsIntoExistingIndex(indexName: string) { + async coerceReportsIntoExistingIndex(indexName: string): Promise { log.debug(`ReportingAPI.coerceReportsIntoExistingIndex(${indexName})`); // Adding an index alias coerces the report to be generated on an existing index which means any new From ac8df39f079da6182f8ba7f17cfd398a90866aec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Tue, 26 Apr 2022 20:27:30 +0200 Subject: [PATCH 29/29] Move `node-libs-browser` to `ui-shared-deps-npm` (#130877) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- ...libs_browser_polyfills_in_entry_bundles.ts | 53 +++++++++++++++++++ .../src/babel_runtime_helpers/index.ts | 1 + .../src/babel_runtime_helpers/parse_stats.ts | 1 + .../kbn-ui-shared-deps-npm/webpack.config.js | 4 ++ ...find_node_libs_browser_polyfills_in_use.js | 10 ++++ 5 files changed, 69 insertions(+) create mode 100644 packages/kbn-optimizer/src/babel_runtime_helpers/find_node_libs_browser_polyfills_in_entry_bundles.ts create mode 100644 scripts/find_node_libs_browser_polyfills_in_use.js diff --git a/packages/kbn-optimizer/src/babel_runtime_helpers/find_node_libs_browser_polyfills_in_entry_bundles.ts b/packages/kbn-optimizer/src/babel_runtime_helpers/find_node_libs_browser_polyfills_in_entry_bundles.ts new file mode 100644 index 0000000000000..06ad13da2b2f2 --- /dev/null +++ b/packages/kbn-optimizer/src/babel_runtime_helpers/find_node_libs_browser_polyfills_in_entry_bundles.ts @@ -0,0 +1,53 @@ +/* + * 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 Path from 'path'; + +import { run } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; + +import { OptimizerConfig } from '../optimizer'; +import { parseStats, inAnyEntryChunk } from './parse_stats'; + +export async function runFindNodeLibsBrowserPolyfillsInEntryBundlesCli() { + run(async ({ log }) => { + const config = OptimizerConfig.create({ + includeCoreBundle: true, + repoRoot: REPO_ROOT, + }); + + const paths = config.bundles.map((b) => Path.resolve(b.outputDir, 'stats.json')); + + log.info('analyzing', paths.length, 'stats files'); + log.verbose(paths); + + const imports = new Set(); + for (const path of paths) { + const stats = parseStats(path); + + for (const module of stats.modules) { + if (!inAnyEntryChunk(stats, module)) { + continue; + } + + // Relying on module name instead of actual imports because these are usual polyfills that assume the global + // Node.js environment when development (i.e.: Buffer doesn't require an import to be used). + if (module.name.includes('node-libs-browser/node_modules/')) { + imports.add(module.name); + } + } + } + + log.success('found', imports.size, 'node-libs-browser/* imports in entry bundles'); + log.write( + Array.from(imports, (i) => `'${i}',`) + .sort() + .join('\n') + ); + }); +} diff --git a/packages/kbn-optimizer/src/babel_runtime_helpers/index.ts b/packages/kbn-optimizer/src/babel_runtime_helpers/index.ts index 58a3ddf263a1d..3a7987f867bc5 100644 --- a/packages/kbn-optimizer/src/babel_runtime_helpers/index.ts +++ b/packages/kbn-optimizer/src/babel_runtime_helpers/index.ts @@ -7,3 +7,4 @@ */ export * from './find_babel_runtime_helpers_in_entry_bundles'; +export * from './find_node_libs_browser_polyfills_in_entry_bundles'; diff --git a/packages/kbn-optimizer/src/babel_runtime_helpers/parse_stats.ts b/packages/kbn-optimizer/src/babel_runtime_helpers/parse_stats.ts index fac0b099b5195..9b9ba11f90c9a 100644 --- a/packages/kbn-optimizer/src/babel_runtime_helpers/parse_stats.ts +++ b/packages/kbn-optimizer/src/babel_runtime_helpers/parse_stats.ts @@ -20,6 +20,7 @@ const partialObject =

(props: P) => { export type Module = TypeOf; const moduleSchema = partialObject({ identifier: schema.string(), + name: schema.string(), chunks: schema.arrayOf(schema.oneOf([schema.string(), schema.number()])), reasons: schema.arrayOf( partialObject({ diff --git a/packages/kbn-ui-shared-deps-npm/webpack.config.js b/packages/kbn-ui-shared-deps-npm/webpack.config.js index 0a709181b04d2..ddcb71fdb2c86 100644 --- a/packages/kbn-ui-shared-deps-npm/webpack.config.js +++ b/packages/kbn-ui-shared-deps-npm/webpack.config.js @@ -37,6 +37,10 @@ module.exports = (_, argv) => { 'regenerator-runtime/runtime', 'whatwg-fetch', 'symbol-observable', + // Parts of node-libs-browser that are used in many places across Kibana + 'buffer', + 'punycode', + 'util', /** * babel runtime helpers referenced from entry chunks diff --git a/scripts/find_node_libs_browser_polyfills_in_use.js b/scripts/find_node_libs_browser_polyfills_in_use.js new file mode 100644 index 0000000000000..4e53e5e551075 --- /dev/null +++ b/scripts/find_node_libs_browser_polyfills_in_use.js @@ -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 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. + */ + +require('../src/setup_node_env/no_transpilation'); +require('@kbn/optimizer').runFindNodeLibsBrowserPolyfillsInEntryBundlesCli();