From ad7cec95dbaf6f78fb5ad199817e43ad629bf8c7 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 8 May 2023 17:12:22 +0100 Subject: [PATCH 01/19] skip flaky suite (#155029) --- test/functional/apps/console/_context_menu.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/functional/apps/console/_context_menu.ts b/test/functional/apps/console/_context_menu.ts index 8114d4d05097e..1d7d4a07e3d4d 100644 --- a/test/functional/apps/console/_context_menu.ts +++ b/test/functional/apps/console/_context_menu.ts @@ -16,7 +16,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const browser = getService('browser'); const toasts = getService('toasts'); - describe('console context menu', function testContextMenu() { + // FLAKY: https://github.com/elastic/kibana/issues/155029 + describe.skip('console context menu', function testContextMenu() { before(async () => { await PageObjects.common.navigateToApp('console'); // Ensure that the text area can be interacted with From 20d29e5d2627a89ce6ca546be5f932eb525fa644 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 8 May 2023 17:13:59 +0100 Subject: [PATCH 02/19] skip flaky suite (#156797) --- .../components/timeline/query_tab_content/index.test.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx index a7368dee15ed2..60dbc4b790e41 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx @@ -116,7 +116,8 @@ describe('Timeline', () => { }; }); - describe('rendering', () => { + // FLAKY: https://github.com/elastic/kibana/issues/156797 + describe.skip('rendering', () => { let spyCombineQueries: jest.SpyInstance; beforeEach(() => { From 1cda321d92ec7a038c1c1d33e02406662c57095f Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 8 May 2023 17:20:52 +0100 Subject: [PATCH 03/19] skip failing es promotion suites (#157017) --- .../interactive_setup_functional/tests/manual_configuration.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/interactive_setup_functional/tests/manual_configuration.ts b/test/interactive_setup_functional/tests/manual_configuration.ts index 3f41cf0659567..e1271ede84001 100644 --- a/test/interactive_setup_functional/tests/manual_configuration.ts +++ b/test/interactive_setup_functional/tests/manual_configuration.ts @@ -18,7 +18,8 @@ export default function ({ getService }: FtrProviderContext) { const retry = getService('retry'); const log = getService('log'); - describe('Interactive Setup Functional Tests (Manual configuration)', function () { + // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/157017 + describe.skip('Interactive Setup Functional Tests (Manual configuration)', function () { this.tags('skipCloud'); let verificationCode: string; From f6ec81c32434fcf9c9864b18c78e6c82b79cf7ba Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 8 May 2023 17:24:10 +0100 Subject: [PATCH 04/19] skip failing es promotion suites (#157018) --- .../tests/manual_configuration_without_tls.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/interactive_setup_functional/tests/manual_configuration_without_tls.ts b/test/interactive_setup_functional/tests/manual_configuration_without_tls.ts index 23595150d55a1..bf83308677b40 100644 --- a/test/interactive_setup_functional/tests/manual_configuration_without_tls.ts +++ b/test/interactive_setup_functional/tests/manual_configuration_without_tls.ts @@ -18,7 +18,8 @@ export default function ({ getService }: FtrProviderContext) { const retry = getService('retry'); const log = getService('log'); - describe('Interactive Setup Functional Tests (Manual configuration without TLS)', function () { + // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/157018 + describe.skip('Interactive Setup Functional Tests (Manual configuration without TLS)', function () { this.tags('skipCloud'); let verificationCode: string; From 7df059e418b585f48efdf0f37db1c05e7e856a68 Mon Sep 17 00:00:00 2001 From: Lola Date: Mon, 8 May 2023 12:36:01 -0400 Subject: [PATCH 05/19] [Cloud Posture] Fix Vulnerability flyout pagination over 500 limit (#156772) ## Summary Currently, pagination was broken when we have records more than 500 limit. - The ` totalVulnerabilitiesCount` had the incorrect value passed. We need the total limited account. - We need the current items to give the page index and page size. We give the `usePageSlice` hook the current subset of results during pagination. https://user-images.githubusercontent.com/17135495/236357910-8c06df79-017d-4b16-959e-8948e4b32efb.mov --- .../pages/vulnerabilities/vulnerabilities.tsx | 25 +++++++++++++------ .../vulnerability_overview_tab.tsx | 6 +++-- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities.tsx b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities.tsx index 6b8ef9b05ab7d..7aa56fa6d00b7 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities.tsx @@ -46,6 +46,7 @@ import { severitySortScript, VULNERABILITY_SEVERITY_FIELD, } from './utils/custom_sort_script'; +import { usePageSlice } from '../../common/hooks/use_page_slice'; const getDefaultQuery = ({ query, filters }: any): any => ({ query, @@ -112,6 +113,8 @@ const VulnerabilitiesContent = ({ dataView }: { dataView: DataView }) => { enabled: !queryError, }); + const slicedPage = usePageSlice(data?.page, pageIndex, pageSize); + const invalidIndex = -1; const selectedVulnerability = data?.page[urlQuery.vulnerabilityIndex]; @@ -122,12 +125,21 @@ const VulnerabilitiesContent = ({ dataView }: { dataView: DataView }) => { }; const onOpenFlyout = useCallback( - (rowIndex: number) => { + (vulnerabilityRow: VulnerabilityRecord) => { + const vulnerabilityIndex = slicedPage.findIndex( + (vulnerabilityRecord: VulnerabilityRecord) => + vulnerabilityRecord.vulnerability?.id === vulnerabilityRow.vulnerability?.id && + vulnerabilityRecord.resource?.id === vulnerabilityRow.resource?.id && + vulnerabilityRecord.vulnerability.package.name === + vulnerabilityRow.vulnerability.package.name && + vulnerabilityRecord.vulnerability.package.version === + vulnerabilityRow.vulnerability.package.version + ); setUrlQuery({ - vulnerabilityIndex: rowIndex, + vulnerabilityIndex, }); }, - [setUrlQuery] + [setUrlQuery, slicedPage] ); const { isLastLimitedPage, limitedTotalItemCount } = useLimitProperties({ @@ -232,7 +244,7 @@ const VulnerabilitiesContent = ({ dataView }: { dataView: DataView }) => { iconType="expand" aria-label="View" onClick={() => { - onOpenFlyout(rowIndex); + onOpenFlyout(vulnerabilityRow); }} /> ); @@ -299,7 +311,6 @@ const VulnerabilitiesContent = ({ dataView }: { dataView: DataView }) => { ); const flyoutVulnerabilityIndex = urlQuery?.vulnerabilityIndex; - const error = queryError || null; if (error) { @@ -409,9 +420,9 @@ const VulnerabilitiesContent = ({ dataView }: { dataView: DataView }) => { {isLastLimitedPage && } {showVulnerabilityFlyout && ( diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_overview_tab.tsx b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_overview_tab.tsx index 033732cc4f64d..ba776c5f94ac2 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_overview_tab.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_overview_tab.tsx @@ -59,7 +59,9 @@ const CVSScore = ({ vectorBaseScore, vendor }: CVSScoreProps) => { {vectorScores.length > 0 && - vectorScores.map((vectorScore) => )} + vectorScores.map((vectorScore, i) => ( + + ))} ); @@ -182,7 +184,7 @@ export const VulnerabilityOverviewTab = ({ vulnerability }: VulnerabilityTabProp ? Object.entries(vulnerability.cvss).map( ([vendor, vectorScoreBase]: [string, VectorScoreBase]) => { return ( - + ); From 45ee63a61ec16189e7c48394e592d1de670dad3c Mon Sep 17 00:00:00 2001 From: Michael Olorunnisola Date: Mon, 8 May 2023 12:38:48 -0400 Subject: [PATCH 06/19] [Bug][Security Solution][Investigations] - check for additional filters object (#156990) ## Summary It is possible that the `additionalFilters` object was not set on a user's existing configuration of the alert table in local storage if they never interacted with it since the feature was added. The object is set on new initialization of the alert table, but for prior configurations, only gets set if the user has interacted with the `additionalFilters` dropdown in the alert page. This resulted in a type error when redirecting, which this PR fixes. --- .../public/common/hooks/flyout/use_init_flyout_url_param.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/common/hooks/flyout/use_init_flyout_url_param.ts b/x-pack/plugins/security_solution/public/common/hooks/flyout/use_init_flyout_url_param.ts index a67b605841917..fbbf7ea453efd 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/flyout/use_init_flyout_url_param.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/flyout/use_init_flyout_url_param.ts @@ -42,7 +42,7 @@ export const useInitFlyoutFromUrlParam = () => { const { initialized, isLoading, totalCount, additionalFilters } = dataTableCurrent; const isTableLoaded = initialized && !isLoading && totalCount > 0; if (urlDetails) { - if (!additionalFilters.showBuildingBlockAlerts) { + if (!additionalFilters || !additionalFilters.showBuildingBlockAlerts) { // We want to show building block alerts when loading the flyout in case the alert is a building block alert dispatch( dataTableActions.updateShowBuildingBlockAlertsFilter({ From 8941058f683863446bf4fd49708504c86fe45700 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 8 May 2023 17:46:55 +0100 Subject: [PATCH 07/19] skip failing es promotion (#157023) --- .../functional/apps/dashboard/group3/reporting/screenshots.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/dashboard/group3/reporting/screenshots.ts b/x-pack/test/functional/apps/dashboard/group3/reporting/screenshots.ts index 8fb3c8c7a693b..12b8790d38324 100644 --- a/x-pack/test/functional/apps/dashboard/group3/reporting/screenshots.ts +++ b/x-pack/test/functional/apps/dashboard/group3/reporting/screenshots.ts @@ -191,7 +191,8 @@ export default function ({ }); }); - describe('Preserve Layout', () => { + // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/157023 + describe.skip('Preserve Layout', () => { before(async () => { await loadEcommerce(); }); From 6185b4033c99ad1a40720c271cca5273fe76d2e6 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Mon, 8 May 2023 10:01:56 -0700 Subject: [PATCH 08/19] [Serverless Projects] add recent items to the side navigation (#156729) ## Summary Closes https://github.com/elastic/kibana/issues/154488 Technical document: https://docs.google.com/document/d/1dK-VhH4xZA_EQXRzx9d5tv6xzEh3UhEK8F0NFpLh_sI/edit# The purpose of this is to allow teams to test the UX of recent items. See [comment from below](https://github.com/elastic/kibana/pull/156729#issuecomment-1538729174): > This can be seen as a temporary step that puts the recently accessed feature in front of people, and it's beneficial as it could help us make new decisions about the UX. ### Other changes 1. Use observable for loading count within the header component 2. Unwrap observables at the point where they are used, rather than in NavigationService ### Checklist Delete any items that are not applicable to this PR. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [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 - [x] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [x] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- packages/kbn-optimizer/limits.yml | 2 +- .../chrome/navigation/mocks/src/jest.ts | 7 ++- .../chrome/navigation/mocks/src/storybook.ts | 19 ++++--- .../navigation/src/model/create_side_nav.ts | 4 +- .../chrome/navigation/src/model/index.ts | 1 - .../chrome/navigation/src/services.tsx | 8 ++- .../chrome/navigation/src/ui/i18n_strings.ts | 6 +++ .../navigation/src/ui/navigation.stories.tsx | 19 ++++++- .../navigation/src/ui/navigation.test.tsx | 48 +++++++++++++++-- .../chrome/navigation/src/ui/navigation.tsx | 52 ++++++++++++++++++- .../chrome/navigation/types/index.ts | 9 +++- 11 files changed, 147 insertions(+), 28 deletions(-) diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 895920d372ed4..aa57c432af51b 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -117,7 +117,7 @@ pageLoadAssetSize: securitySolution: 66738 serverless: 16573 serverlessObservability: 16582 - serverlessSearch: 20555 + serverlessSearch: 22555 serverlessSecurity: 41807 sessionView: 77750 share: 71239 diff --git a/packages/shared-ux/chrome/navigation/mocks/src/jest.ts b/packages/shared-ux/chrome/navigation/mocks/src/jest.ts index 1f8356195eadd..27c40f5b52efa 100644 --- a/packages/shared-ux/chrome/navigation/mocks/src/jest.ts +++ b/packages/shared-ux/chrome/navigation/mocks/src/jest.ts @@ -6,16 +6,19 @@ * Side Public License, v 1. */ +import { BehaviorSubject } from 'rxjs'; import { NavigationServices, ChromeNavigationNodeViewModel } from '../../types'; export const getServicesMock = (): NavigationServices => { const navigateToUrl = jest.fn().mockResolvedValue(undefined); const basePath = { prepend: jest.fn((path: string) => `/base${path}`) }; - const loadingCount = 0; + const loadingCount$ = new BehaviorSubject(0); + const recentlyAccessed$ = new BehaviorSubject([]); return { basePath, - loadingCount, + loadingCount$, + recentlyAccessed$, navIsOpen: true, navigateToUrl, }; diff --git a/packages/shared-ux/chrome/navigation/mocks/src/storybook.ts b/packages/shared-ux/chrome/navigation/mocks/src/storybook.ts index d269f5ca56ae5..889a374544429 100644 --- a/packages/shared-ux/chrome/navigation/mocks/src/storybook.ts +++ b/packages/shared-ux/chrome/navigation/mocks/src/storybook.ts @@ -8,12 +8,19 @@ import { AbstractStorybookMock } from '@kbn/shared-ux-storybook-mock'; import { action } from '@storybook/addon-actions'; +import { BehaviorSubject } from 'rxjs'; import { ChromeNavigationViewModel, NavigationServices } from '../../types'; type Arguments = ChromeNavigationViewModel & NavigationServices; export type Params = Pick< Arguments, - 'activeNavItemId' | 'loadingCount' | 'navIsOpen' | 'platformConfig' | 'navigationTree' + | 'activeNavItemId' + | 'loadingCount$' + | 'navIsOpen' + | 'navigationTree' + | 'platformConfig' + | 'recentlyAccessed$' + | 'recentlyAccessedFilter' >; export class StorybookMock extends AbstractStorybookMock< @@ -27,17 +34,11 @@ export class StorybookMock extends AbstractStorybookMock< control: 'boolean', defaultValue: true, }, - loadingCount: { - control: 'number', - defaultValue: 0, - }, }; dependencies = []; getServices(params: Params): NavigationServices { - const { navIsOpen } = params; - const navAction = action('Navigate to'); const navigateToUrl = (url: string) => { navAction(url); @@ -48,7 +49,8 @@ export class StorybookMock extends AbstractStorybookMock< ...params, basePath: { prepend: (suffix: string) => `/basepath${suffix}` }, navigateToUrl, - navIsOpen, + loadingCount$: params.loadingCount$ ?? new BehaviorSubject(0), + recentlyAccessed$: params.recentlyAccessed$ ?? new BehaviorSubject([]), }; } @@ -57,6 +59,7 @@ export class StorybookMock extends AbstractStorybookMock< ...params, homeHref: '#', linkToCloud: 'projects', + recentlyAccessedFilter: params.recentlyAccessedFilter, }; } } diff --git a/packages/shared-ux/chrome/navigation/src/model/create_side_nav.ts b/packages/shared-ux/chrome/navigation/src/model/create_side_nav.ts index eafc3862a51c9..a4c26a675989f 100644 --- a/packages/shared-ux/chrome/navigation/src/model/create_side_nav.ts +++ b/packages/shared-ux/chrome/navigation/src/model/create_side_nav.ts @@ -12,8 +12,8 @@ import type { ChromeNavigationNodeViewModel, PlatformSectionConfig } from '../.. * Navigation node parser. It filers out the nodes disabled through config and * sets the `path` of each of the nodes. * - * @param items Navigation nodes - * @param platformConfig Configuration with flags to disable nodes in the navigation tree + * @param navItems Navigation nodes + * @param platformSectionConfig Configuration with flags to disable nodes in the navigation tree * * @returns The navigation tree filtered */ diff --git a/packages/shared-ux/chrome/navigation/src/model/index.ts b/packages/shared-ux/chrome/navigation/src/model/index.ts index 8e8d94e995019..db3e5a29951ac 100644 --- a/packages/shared-ux/chrome/navigation/src/model/index.ts +++ b/packages/shared-ux/chrome/navigation/src/model/index.ts @@ -21,7 +21,6 @@ export interface NavigationModelDeps { * @public */ export enum Platform { - Recents = 'recents', Analytics = 'analytics', MachineLearning = 'ml', DevTools = 'devTools', diff --git a/packages/shared-ux/chrome/navigation/src/services.tsx b/packages/shared-ux/chrome/navigation/src/services.tsx index 8235963c18681..0b6202591bb53 100644 --- a/packages/shared-ux/chrome/navigation/src/services.tsx +++ b/packages/shared-ux/chrome/navigation/src/services.tsx @@ -7,7 +7,6 @@ */ import React, { FC, useContext } from 'react'; -import useObservable from 'react-use/lib/useObservable'; import { NavigationKibanaDependencies, NavigationServices } from '../types'; const Context = React.createContext(null); @@ -27,15 +26,14 @@ export const NavigationKibanaProvider: FC = ({ ...dependencies }) => { const { core } = dependencies; - const { http } = core; + const { chrome, http } = core; const { basePath } = http; const { navigateToUrl } = core.application; - const loadingCount = useObservable(http.getLoadingCount$(), 0); - const value: NavigationServices = { basePath, - loadingCount, + loadingCount$: http.getLoadingCount$(), + recentlyAccessed$: chrome.recentlyAccessed.get$(), navigateToUrl, navIsOpen: true, }; diff --git a/packages/shared-ux/chrome/navigation/src/ui/i18n_strings.ts b/packages/shared-ux/chrome/navigation/src/ui/i18n_strings.ts index 9f6f3fbadca30..c268e7a42de10 100644 --- a/packages/shared-ux/chrome/navigation/src/ui/i18n_strings.ts +++ b/packages/shared-ux/chrome/navigation/src/ui/i18n_strings.ts @@ -27,4 +27,10 @@ export const getI18nStrings = () => ({ defaultMessage: 'My deployments', } ), + recentlyAccessed: i18n.translate( + 'sharedUXPackages.chrome.sideNavigation.recentlyAccessed.title', + { + defaultMessage: 'Recent', + } + ), }); diff --git a/packages/shared-ux/chrome/navigation/src/ui/navigation.stories.tsx b/packages/shared-ux/chrome/navigation/src/ui/navigation.stories.tsx index 9652d83597119..b666f4d409869 100644 --- a/packages/shared-ux/chrome/navigation/src/ui/navigation.stories.tsx +++ b/packages/shared-ux/chrome/navigation/src/ui/navigation.stories.tsx @@ -13,9 +13,10 @@ import { EuiPopover, EuiThemeProvider, } from '@elastic/eui'; +import { css } from '@emotion/react'; import { ComponentMeta, ComponentStory } from '@storybook/react'; import React, { useCallback, useState } from 'react'; -import { css } from '@emotion/react'; +import { BehaviorSubject } from 'rxjs'; import { getSolutionPropertiesMock, NavigationStorybookMock } from '../../mocks'; import mdx from '../../README.mdx'; import { ChromeNavigationViewModel, NavigationServices } from '../../types'; @@ -132,11 +133,25 @@ ReducedPlatformLinks.argTypes = storybookMock.getArgumentTypes(); export const WithRequestsLoading: ComponentStory = Template.bind({}); WithRequestsLoading.args = { activeNavItemId: 'example_project.root.get_started', - loadingCount: 1, + loadingCount$: new BehaviorSubject(1), navigationTree: [getSolutionPropertiesMock()], }; WithRequestsLoading.argTypes = storybookMock.getArgumentTypes(); +export const WithRecentlyAccessed: ComponentStory = Template.bind({}); +WithRecentlyAccessed.args = { + activeNavItemId: 'example_project.root.get_started', + loadingCount$: new BehaviorSubject(0), + recentlyAccessed$: new BehaviorSubject([ + { label: 'This is an example', link: '/app/example/39859', id: '39850' }, + { label: 'This is not an example', link: '/app/non-example/39458', id: '39458' }, // NOTE: this will be filtered out + ]), + recentlyAccessedFilter: (items) => + items.filter((item) => item.link.indexOf('/app/example') === 0), + navigationTree: [getSolutionPropertiesMock()], +}; +WithRecentlyAccessed.argTypes = storybookMock.getArgumentTypes(); + export const CustomElements: ComponentStory = Template.bind({}); CustomElements.args = { activeNavItemId: 'example_project.custom', diff --git a/packages/shared-ux/chrome/navigation/src/ui/navigation.test.tsx b/packages/shared-ux/chrome/navigation/src/ui/navigation.test.tsx index 653b66887054b..4a39a4e651d7a 100644 --- a/packages/shared-ux/chrome/navigation/src/ui/navigation.test.tsx +++ b/packages/shared-ux/chrome/navigation/src/ui/navigation.test.tsx @@ -8,8 +8,9 @@ import { render } from '@testing-library/react'; import React from 'react'; +import { BehaviorSubject } from 'rxjs'; import { getServicesMock } from '../../mocks/src/jest'; -import { PlatformConfigSet, ChromeNavigationNodeViewModel } from '../../types'; +import { ChromeNavigationNodeViewModel, PlatformConfigSet } from '../../types'; import { Platform } from '../model'; import { NavigationProvider } from '../services'; import { Navigation } from './navigation'; @@ -27,7 +28,7 @@ describe('', () => { }); test('renders the header logo and top-level navigation buckets', async () => { - const { findByTestId, findByText } = render( + const { findByTestId, findByText, queryByTestId } = render( ', () => { expect(await findByTestId('nav-bucket-ml')).toBeVisible(); expect(await findByTestId('nav-bucket-devTools')).toBeVisible(); expect(await findByTestId('nav-bucket-management')).toBeVisible(); + + expect(queryByTestId('nav-bucket-recentlyAccessed')).not.toBeInTheDocument(); }); test('includes link to deployments', async () => { @@ -122,7 +125,7 @@ describe('', () => { }); test('shows loading state', async () => { - services.loadingCount = 5; + services.loadingCount$ = new BehaviorSubject(5); const { findByTestId } = render( @@ -136,4 +139,43 @@ describe('', () => { expect(await findByTestId('nav-header-loading-spinner')).toBeVisible(); }); + + describe('recent items', () => { + const recentlyAccessed = [ + { id: 'dashboard:234', label: 'Recently Accessed Test Item', link: '/app/dashboard/234' }, + ]; + + test('shows recent items', async () => { + services.recentlyAccessed$ = new BehaviorSubject(recentlyAccessed); + + const { findByTestId } = render( + + + + ); + + expect(await findByTestId('nav-bucket-recentlyAccessed')).toBeVisible(); + }); + + test('shows no recent items container when items are filtered', async () => { + services.recentlyAccessed$ = new BehaviorSubject(recentlyAccessed); + + const { queryByTestId } = render( + + []} + /> + + ); + + expect(queryByTestId('nav-bucket-recentlyAccessed')).not.toBeInTheDocument(); + }); + }); }); diff --git a/packages/shared-ux/chrome/navigation/src/ui/navigation.tsx b/packages/shared-ux/chrome/navigation/src/ui/navigation.tsx index ed58e9f57c4e5..776fe07297e64 100644 --- a/packages/shared-ux/chrome/navigation/src/ui/navigation.tsx +++ b/packages/shared-ux/chrome/navigation/src/ui/navigation.tsx @@ -13,16 +13,20 @@ import { EuiHeaderLogo, EuiLink, EuiLoadingSpinner, + EuiSideNav, + EuiSideNavItemType, EuiSpacer, useEuiTheme, } from '@elastic/eui'; import React from 'react'; -import { getI18nStrings } from './i18n_strings'; +import useObservable from 'react-use/lib/useObservable'; import type { ChromeNavigationViewModel } from '../../types'; import { NavigationModel } from '../model'; import { useNavigation } from '../services'; +import { navigationStyles as styles } from '../styles'; import { ElasticMark } from './elastic_mark'; import './header_logo.scss'; +import { getI18nStrings } from './i18n_strings'; import { NavigationBucket, type Props as NavigationBucketProps } from './navigation_bucket'; interface Props extends ChromeNavigationViewModel { @@ -38,8 +42,9 @@ export const Navigation = ({ homeHref, linkToCloud, activeNavItemId: activeNavItemIdProps, + ...props }: Props) => { - const { loadingCount, activeNavItemId, basePath, navIsOpen, navigateToUrl } = useNavigation(); + const { activeNavItemId } = useNavigation(); const { euiTheme } = useEuiTheme(); const activeNav = activeNavItemId ?? activeNavItemIdProps; @@ -52,6 +57,8 @@ export const Navigation = ({ const strings = getI18nStrings(); const NavHeader = () => { + const { basePath, navIsOpen, navigateToUrl, loadingCount$ } = useNavigation(); + const loadingCount = useObservable(loadingCount$, 0); const homeUrl = basePath.prepend(homeHref); const navigateHome = (event: React.MouseEvent) => { event.preventDefault(); @@ -111,6 +118,45 @@ export const Navigation = ({ } }; + const RecentlyAccessed = () => { + const { recentlyAccessed$ } = useNavigation(); + const recentlyAccessed = useObservable(recentlyAccessed$, []); + + // consumer may filter objects from recent that are not applicable to the project + let filteredRecent = recentlyAccessed; + if (props.recentlyAccessedFilter) { + filteredRecent = props.recentlyAccessedFilter(recentlyAccessed); + } + + if (filteredRecent.length > 0) { + const navItems: Array> = [ + { + name: '', // no list header title + id: 'recents_root', + items: filteredRecent.map(({ id, label, link }) => ({ + id, + name: label, + href: link, + })), + }, + ]; + + return ( + + + + ); + } + + return null; + }; + // higher-order-component to keep the common props DRY const NavigationBucketHoc = (outerProps: Omit) => ( @@ -125,6 +171,8 @@ export const Navigation = ({ + + {solutions.map((navTree, idx) => { return ; })} diff --git a/packages/shared-ux/chrome/navigation/types/index.ts b/packages/shared-ux/chrome/navigation/types/index.ts index f58c9dc6836d1..a4ba5c42c39b8 100644 --- a/packages/shared-ux/chrome/navigation/types/index.ts +++ b/packages/shared-ux/chrome/navigation/types/index.ts @@ -17,7 +17,8 @@ import type { BasePathService, NavigateToUrlFn, RecentItem } from './internal'; export interface NavigationServices { activeNavItemId?: string; basePath: BasePathService; - loadingCount: number; + loadingCount$: Observable; + recentlyAccessed$: Observable; navIsOpen: boolean; navigateToUrl: NavigateToUrlFn; } @@ -111,6 +112,10 @@ export interface ChromeNavigation { * above. */ platformConfig?: Partial; + /** + * Filter function to allow consumer to remove items from the recently accessed section + */ + recentlyAccessedFilter?: (items: RecentItem[]) => RecentItem[]; } /** @@ -119,7 +124,7 @@ export interface ChromeNavigation { * @internal */ export interface ChromeNavigationViewModel - extends Pick { + extends Pick { /** * Target for the logo icon */ From a22561a524c73bab055e2a7eb7411cc36158c856 Mon Sep 17 00:00:00 2001 From: Dominique Clarke Date: Mon, 8 May 2023 13:13:19 -0400 Subject: [PATCH 09/19] [Synthetics] quote monitor name to prevent invalid yaml (#156210) ## Summary Appropriately quotes monitor names for synthetics integration policies, to ensure that customers can use monitor names that would otherwise break yaml. ### Testing 1. Create a private location 2. Save a monitor with name `[Synthetics] test` to that private location 3. Navigate to the agent policy for that location. Confirm that the integration policy was added to the agent policy, and that the name is correct --------- Co-authored-by: shahzad31 Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../common/formatters/browser/formatters.ts | 6 ++--- .../common/formatters/common/formatters.ts | 18 ++++++------- .../format_synthetics_policy.test.ts | 24 +++++++++--------- .../common/formatters/formatting_utils.ts | 9 +++++++ .../common/formatters/http/formatters.ts | 16 +++++++----- .../common/formatters/icmp/formatters.ts | 3 ++- .../common/formatters/tcp/formatters.ts | 14 +++++------ .../formatters/format_configs.test.ts | 18 ++++++------- .../synthetics_private_location.test.ts | 8 +++--- .../synthetics_private_location.ts | 7 +++--- .../add_monitor_private_location.ts | 4 +-- .../add_monitor_project_private_location.ts | 8 +++--- .../sample_data/test_browser_policy.ts | 8 +++--- .../synthetics/sample_data/test_policy.ts | 25 +++++++++++++------ .../test_project_monitor_policy.ts | 12 ++++----- 15 files changed, 102 insertions(+), 78 deletions(-) diff --git a/x-pack/plugins/synthetics/common/formatters/browser/formatters.ts b/x-pack/plugins/synthetics/common/formatters/browser/formatters.ts index 9dfa027767851..22ce128fc2ab0 100644 --- a/x-pack/plugins/synthetics/common/formatters/browser/formatters.ts +++ b/x-pack/plugins/synthetics/common/formatters/browser/formatters.ts @@ -38,9 +38,9 @@ export const browserFormatters: BrowserFormatMap = { [ConfigKey.SCREENSHOTS]: null, [ConfigKey.IGNORE_HTTPS_ERRORS]: null, [ConfigKey.PLAYWRIGHT_OPTIONS]: null, - [ConfigKey.TEXT_ASSERTION]: null, - [ConfigKey.PORT]: null, - [ConfigKey.URLS]: null, + [ConfigKey.TEXT_ASSERTION]: stringToJsonFormatter, + [ConfigKey.PORT]: stringToJsonFormatter, + [ConfigKey.URLS]: stringToJsonFormatter, [ConfigKey.METADATA]: objectToJsonFormatter, [ConfigKey.SOURCE_INLINE]: stringToJsonFormatter, [ConfigKey.SYNTHETICS_ARGS]: arrayToJsonFormatter, diff --git a/x-pack/plugins/synthetics/common/formatters/common/formatters.ts b/x-pack/plugins/synthetics/common/formatters/common/formatters.ts index 3122bcdb46442..4db075420f859 100644 --- a/x-pack/plugins/synthetics/common/formatters/common/formatters.ts +++ b/x-pack/plugins/synthetics/common/formatters/common/formatters.ts @@ -6,30 +6,30 @@ */ import { CommonFields, ConfigKey, SourceType } from '../../runtime_types/monitor_management'; -import { arrayToJsonFormatter, FormatterFn } from '../formatting_utils'; +import { arrayToJsonFormatter, stringToJsonFormatter, FormatterFn } from '../formatting_utils'; export type Formatter = null | FormatterFn; export type CommonFormatMap = Record; export const commonFormatters: CommonFormatMap = { - [ConfigKey.APM_SERVICE_NAME]: null, - [ConfigKey.NAME]: null, + [ConfigKey.APM_SERVICE_NAME]: stringToJsonFormatter, + [ConfigKey.NAME]: stringToJsonFormatter, [ConfigKey.LOCATIONS]: null, [ConfigKey.MONITOR_TYPE]: null, [ConfigKey.ENABLED]: null, [ConfigKey.ALERT_CONFIG]: null, [ConfigKey.CONFIG_ID]: null, - [ConfigKey.NAMESPACE]: null, + [ConfigKey.NAMESPACE]: stringToJsonFormatter, [ConfigKey.REVISION]: null, [ConfigKey.MONITOR_SOURCE_TYPE]: null, [ConfigKey.FORM_MONITOR_TYPE]: null, - [ConfigKey.JOURNEY_ID]: null, - [ConfigKey.PROJECT_ID]: null, - [ConfigKey.CUSTOM_HEARTBEAT_ID]: null, - [ConfigKey.ORIGINAL_SPACE]: null, + [ConfigKey.JOURNEY_ID]: stringToJsonFormatter, + [ConfigKey.PROJECT_ID]: stringToJsonFormatter, + [ConfigKey.CUSTOM_HEARTBEAT_ID]: stringToJsonFormatter, + [ConfigKey.ORIGINAL_SPACE]: stringToJsonFormatter, [ConfigKey.CONFIG_HASH]: null, - [ConfigKey.MONITOR_QUERY_ID]: null, + [ConfigKey.MONITOR_QUERY_ID]: stringToJsonFormatter, [ConfigKey.SCHEDULE]: (fields) => JSON.stringify( `@every ${fields[ConfigKey.SCHEDULE]?.number}${fields[ConfigKey.SCHEDULE]?.unit}` diff --git a/x-pack/plugins/synthetics/common/formatters/format_synthetics_policy.test.ts b/x-pack/plugins/synthetics/common/formatters/format_synthetics_policy.test.ts index 2706972456acd..7c045f807e4ca 100644 --- a/x-pack/plugins/synthetics/common/formatters/format_synthetics_policy.test.ts +++ b/x-pack/plugins/synthetics/common/formatters/format_synthetics_policy.test.ts @@ -354,7 +354,7 @@ describe('formatSyntheticsPolicy', () => { }, id: { type: 'text', - value: '00bb3ceb-a242-4c7a-8405-8da963661374', + value: '"00bb3ceb-a242-4c7a-8405-8da963661374"', }, ignore_https_errors: { type: 'bool', @@ -372,7 +372,7 @@ describe('formatSyntheticsPolicy', () => { }, name: { type: 'text', - value: 'Test HTTP Monitor 03', + value: '"Test HTTP Monitor 03"', }, origin: { type: 'text', @@ -401,7 +401,7 @@ describe('formatSyntheticsPolicy', () => { }, 'service.name': { type: 'text', - value: '', + value: '"Local Service"', }, 'source.inline.script': { type: 'yaml', @@ -532,7 +532,7 @@ describe('formatSyntheticsPolicy', () => { }, id: { type: 'text', - value: '51ccd9d9-fc3f-4718-ba9d-b6ef80e73fc5', + value: '"51ccd9d9-fc3f-4718-ba9d-b6ef80e73fc5"', }, location_name: { type: 'text', @@ -550,7 +550,7 @@ describe('formatSyntheticsPolicy', () => { }, name: { type: 'text', - value: 'Test Monitor', + value: '"Test Monitor"', }, origin: { type: 'text', @@ -558,11 +558,11 @@ describe('formatSyntheticsPolicy', () => { }, password: { type: 'password', - value: 'changeme', + value: '"changeme"', }, proxy_url: { type: 'text', - value: 'https://proxy.com', + value: '"https://proxy.com"', }, 'response.include_body': { type: 'text', @@ -582,7 +582,7 @@ describe('formatSyntheticsPolicy', () => { }, 'service.name': { type: 'text', - value: 'LocalService', + value: '"LocalService"', }, 'ssl.certificate': { type: 'yaml', @@ -622,11 +622,11 @@ describe('formatSyntheticsPolicy', () => { }, urls: { type: 'text', - value: 'https://www.google.com', + value: '"https://www.google.com"', }, username: { type: 'text', - value: '', + value: '"admin"', }, }, }, @@ -1110,7 +1110,7 @@ const browserConfig: any = { enabled: true, alert: { status: { enabled: true } }, schedule: { number: '3', unit: 'm' }, - 'service.name': '', + 'service.name': 'Local Service', config_id: '00bb3ceb-a242-4c7a-8405-8da963661374', tags: ['cookie-test', 'browser'], timeout: '16', @@ -1198,7 +1198,7 @@ const httpPolicy: any = { 'check.request.body': { type: 'text', value: '' }, 'check.request.headers': {}, 'check.request.method': 'GET', - username: '', + username: 'admin', 'ssl.certificate_authorities': '', 'ssl.certificate': '', 'ssl.key': '', diff --git a/x-pack/plugins/synthetics/common/formatters/formatting_utils.ts b/x-pack/plugins/synthetics/common/formatters/formatting_utils.ts index 9013496b96eeb..2c69f551cbf7b 100644 --- a/x-pack/plugins/synthetics/common/formatters/formatting_utils.ts +++ b/x-pack/plugins/synthetics/common/formatters/formatting_utils.ts @@ -61,6 +61,15 @@ export const stringToJsonFormatter: FormatterFn = (fields, key) => { return value ? JSON.stringify(value) : null; }; +export const stringifyString = (value?: string) => { + if (!value) return value; + try { + return JSON.stringify(value); + } catch (e) { + return value; + } +}; + export const replaceStringWithParams = ( value: string | boolean | {} | [], params: Record, diff --git a/x-pack/plugins/synthetics/common/formatters/http/formatters.ts b/x-pack/plugins/synthetics/common/formatters/http/formatters.ts index 437112939f283..f485159d34596 100644 --- a/x-pack/plugins/synthetics/common/formatters/http/formatters.ts +++ b/x-pack/plugins/synthetics/common/formatters/http/formatters.ts @@ -9,7 +9,11 @@ import { tlsFormatters } from '../tls/formatters'; import { HTTPFields, ConfigKey } from '../../runtime_types/monitor_management'; import { Formatter, commonFormatters } from '../common/formatters'; -import { arrayToJsonFormatter, objectToJsonFormatter } from '../formatting_utils'; +import { + stringToJsonFormatter, + arrayToJsonFormatter, + objectToJsonFormatter, +} from '../formatting_utils'; export type HTTPFormatMap = Record; @@ -19,12 +23,12 @@ export const httpFormatters: HTTPFormatMap = { [ConfigKey.RESPONSE_BODY_INDEX]: null, [ConfigKey.RESPONSE_HEADERS_INDEX]: null, [ConfigKey.METADATA]: objectToJsonFormatter, - [ConfigKey.URLS]: null, - [ConfigKey.USERNAME]: null, - [ConfigKey.PASSWORD]: null, - [ConfigKey.PROXY_URL]: null, + [ConfigKey.URLS]: stringToJsonFormatter, + [ConfigKey.USERNAME]: stringToJsonFormatter, + [ConfigKey.PASSWORD]: stringToJsonFormatter, + [ConfigKey.PROXY_URL]: stringToJsonFormatter, [ConfigKey.PROXY_HEADERS]: objectToJsonFormatter, - [ConfigKey.PORT]: null, + [ConfigKey.PORT]: stringToJsonFormatter, [ConfigKey.RESPONSE_BODY_CHECK_NEGATIVE]: arrayToJsonFormatter, [ConfigKey.RESPONSE_BODY_CHECK_POSITIVE]: arrayToJsonFormatter, [ConfigKey.RESPONSE_JSON_CHECK]: arrayToJsonFormatter, diff --git a/x-pack/plugins/synthetics/common/formatters/icmp/formatters.ts b/x-pack/plugins/synthetics/common/formatters/icmp/formatters.ts index f58e15c86b3ad..acdccebfb4b57 100644 --- a/x-pack/plugins/synthetics/common/formatters/icmp/formatters.ts +++ b/x-pack/plugins/synthetics/common/formatters/icmp/formatters.ts @@ -9,11 +9,12 @@ import { secondsToCronFormatter } from '../formatting_utils'; import { ICMPFields, ConfigKey } from '../../runtime_types/monitor_management'; import { Formatter, commonFormatters } from '../common/formatters'; +import { stringToJsonFormatter } from '../formatting_utils'; export type ICMPFormatMap = Record; export const icmpFormatters: ICMPFormatMap = { - [ConfigKey.HOSTS]: null, + [ConfigKey.HOSTS]: stringToJsonFormatter, [ConfigKey.WAIT]: secondsToCronFormatter, [ConfigKey.MODE]: null, [ConfigKey.IPV4]: null, diff --git a/x-pack/plugins/synthetics/common/formatters/tcp/formatters.ts b/x-pack/plugins/synthetics/common/formatters/tcp/formatters.ts index 2d850e95ceaf1..a3127c1cb49b4 100644 --- a/x-pack/plugins/synthetics/common/formatters/tcp/formatters.ts +++ b/x-pack/plugins/synthetics/common/formatters/tcp/formatters.ts @@ -10,19 +10,19 @@ import { TCPFields, ConfigKey } from '../../runtime_types/monitor_management'; import { Formatter, commonFormatters } from '../common/formatters'; import { objectToJsonFormatter } from '../formatting_utils'; import { tlsFormatters } from '../tls/formatters'; +import { stringToJsonFormatter } from '../formatting_utils'; export type TCPFormatMap = Record; export const tcpFormatters: TCPFormatMap = { [ConfigKey.METADATA]: objectToJsonFormatter, - [ConfigKey.HOSTS]: null, + [ConfigKey.HOSTS]: stringToJsonFormatter, [ConfigKey.PROXY_USE_LOCAL_RESOLVER]: null, - [ConfigKey.RESPONSE_RECEIVE_CHECK]: null, - [ConfigKey.REQUEST_SEND_CHECK]: null, - [ConfigKey.PROXY_URL]: null, - [ConfigKey.PROXY_URL]: null, - [ConfigKey.PORT]: null, - [ConfigKey.URLS]: null, + [ConfigKey.RESPONSE_RECEIVE_CHECK]: stringToJsonFormatter, + [ConfigKey.REQUEST_SEND_CHECK]: stringToJsonFormatter, + [ConfigKey.PROXY_URL]: stringToJsonFormatter, + [ConfigKey.PORT]: stringToJsonFormatter, + [ConfigKey.URLS]: stringToJsonFormatter, [ConfigKey.MODE]: null, [ConfigKey.IPV4]: null, [ConfigKey.IPV6]: null, diff --git a/x-pack/plugins/synthetics/server/synthetics_service/formatters/format_configs.test.ts b/x-pack/plugins/synthetics/server/synthetics_service/formatters/format_configs.test.ts index 921fd737e5d3e..710946e520646 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/formatters/format_configs.test.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/formatters/format_configs.test.ts @@ -119,15 +119,15 @@ describe('formatMonitorConfig', () => { enabled: true, locations: [], max_redirects: '0', - name: 'Test', - password: '3z9SBOQWW5F0UrdqLVFqlF6z', + name: '"Test"', + password: '"3z9SBOQWW5F0UrdqLVFqlF6z"', 'response.include_body': 'on_error', 'response.include_headers': true, schedule: '@every 3m', timeout: '16s', type: 'http', - urls: 'https://www.google.com', - proxy_url: 'https://www.google.com', + urls: '"https://www.google.com"', + proxy_url: '"https://www.google.com"', }); }); @@ -158,15 +158,15 @@ describe('formatMonitorConfig', () => { enabled: true, locations: [], max_redirects: '0', - name: 'Test', - password: '3z9SBOQWW5F0UrdqLVFqlF6z', - proxy_url: 'https://www.google.com', + name: '"Test"', + password: '"3z9SBOQWW5F0UrdqLVFqlF6z"', + proxy_url: '"https://www.google.com"', 'response.include_body': 'on_error', 'response.include_headers': true, schedule: '@every 3m', timeout: '16s', type: 'http', - urls: 'https://www.google.com', + urls: '"https://www.google.com"', ...(isTLSEnabled ? { 'ssl.verification_mode': 'none' } : {}), }); } @@ -183,7 +183,7 @@ describe('browser fields', () => { enabled: true, 'filter_journeys.tags': ['dev'], ignore_https_errors: false, - name: 'Test', + name: '"Test"', locations: [], schedule: '@every 3m', screenshots: 'on', diff --git a/x-pack/plugins/synthetics/server/synthetics_service/private_location/synthetics_private_location.test.ts b/x-pack/plugins/synthetics/server/synthetics_service/private_location/synthetics_private_location.test.ts index 5b0b1895de3a2..fcab7e6cb06c4 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/private_location/synthetics_private_location.test.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/private_location/synthetics_private_location.test.ts @@ -31,7 +31,7 @@ describe('SyntheticsPrivateLocation', () => { type: 'http', enabled: true, schedule: '@every 3m', - 'service.name': '', + 'service.name': 'test service', locations: [mockPrivateLocation], tags: [], timeout: '16', @@ -226,7 +226,7 @@ describe('SyntheticsPrivateLocation', () => { }, name: { type: 'text', - value: 'Browser monitor', + value: '"Browser monitor"', }, params: { type: 'yaml', @@ -246,7 +246,7 @@ describe('SyntheticsPrivateLocation', () => { }, 'service.name': { type: 'text', - value: '', + value: '"test service"', }, 'source.inline.script': { type: 'yaml', @@ -286,7 +286,7 @@ const dummyBrowserConfig: Partial & { type: DataStream.BROWSER, enabled: true, schedule: { unit: ScheduleUnit.MINUTES, number: '10' }, - 'service.name': '', + 'service.name': 'test service', tags: [], timeout: null, name: 'Browser monitor', diff --git a/x-pack/plugins/synthetics/server/synthetics_service/private_location/synthetics_private_location.ts b/x-pack/plugins/synthetics/server/synthetics_service/private_location/synthetics_private_location.ts index 51545433b9450..9aef89a86a4a9 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/private_location/synthetics_private_location.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/private_location/synthetics_private_location.ts @@ -9,6 +9,7 @@ import { NewPackagePolicy } from '@kbn/fleet-plugin/common'; import { NewPackagePolicyWithId } from '@kbn/fleet-plugin/server/services/package_policy'; import { cloneDeep } from 'lodash'; import { SavedObjectError } from '@kbn/core-saved-objects-common'; +import { stringifyString } from '../../../common/formatters/formatting_utils'; import { formatSyntheticsPolicy } from '../../../common/formatters/format_synthetics_policy'; import { ConfigKey, @@ -86,9 +87,9 @@ export class SyntheticsPrivateLocation { { ...(config as Partial), config_id: config.fields?.config_id, - location_name: privateLocation.label, - 'monitor.project.id': config.fields?.['monitor.project.name'], - 'monitor.project.name': config.fields?.['monitor.project.name'], + location_name: stringifyString(privateLocation.label), + 'monitor.project.id': stringifyString(config.fields?.['monitor.project.name']), + 'monitor.project.name': stringifyString(config.fields?.['monitor.project.name']), }, globalParams ); diff --git a/x-pack/test/api_integration/apis/synthetics/add_monitor_private_location.ts b/x-pack/test/api_integration/apis/synthetics/add_monitor_private_location.ts index 149961f41e084..8a2da70a07009 100644 --- a/x-pack/test/api_integration/apis/synthetics/add_monitor_private_location.ts +++ b/x-pack/test/api_integration/apis/synthetics/add_monitor_private_location.ts @@ -90,7 +90,7 @@ export default function ({ getService }: FtrProviderContext) { it('does not add a monitor if there is an error in creating integration', async () => { const newMonitor = { ...httpMonitorJson }; - const invalidName = '[] - invalid name'; + const invalidName = '!@#$%^&*()_++[\\-\\]- wow'; newMonitor.locations.push({ id: testFleetPolicyID, @@ -109,7 +109,7 @@ export default function ({ getService }: FtrProviderContext) { expect(apiResponse.body).eql({ statusCode: 500, message: - 'YAMLException: end of the stream or a document separator is expected at line 3, column 10:\n name: [] - invalid name\n ^', + 'YAMLException: unknown escape sequence at line 3, column 34:\n name: "!@#$,%,^,&,*,(,),_,+,+,[,\\,\\,-,\\,\\,],-, ,w,o,w,"\n ^', error: 'Internal Server Error', }); diff --git a/x-pack/test/api_integration/apis/synthetics/add_monitor_project_private_location.ts b/x-pack/test/api_integration/apis/synthetics/add_monitor_project_private_location.ts index 7c6517d3cddf1..d53309c48e340 100644 --- a/x-pack/test/api_integration/apis/synthetics/add_monitor_project_private_location.ts +++ b/x-pack/test/api_integration/apis/synthetics/add_monitor_project_private_location.ts @@ -64,13 +64,13 @@ export default function ({ getService }: FtrProviderContext) { }; const testMonitors = [ projectMonitors.monitors[0], - { ...secondMonitor, name: '[] - invalid name' }, + { ...secondMonitor, name: '!@#$%^&*()_++[\\-\\]- wow name' }, ]; try { const body = await monitorTestService.addProjectMonitors(project, testMonitors); expect(body.createdMonitors.length).eql(1); expect(body.failedMonitors[0].reason).eql( - 'end of the stream or a document separator is expected at line 3, column 10:\n name: [] - invalid name\n ^' + 'unknown escape sequence at line 3, column 34:\n name: "!@#$,%,^,&,*,(,),_,+,+,[,\\,\\,-,\\,\\,],-, ,w,o,w, ,n,a,m,e,"\n ^' ); } finally { await Promise.all([ @@ -97,7 +97,7 @@ export default function ({ getService }: FtrProviderContext) { expect(editedBody.createdMonitors.length).eql(0); expect(editedBody.updatedMonitors.length).eql(2); - testMonitors[1].name = '[] - invalid name'; + testMonitors[1].name = '!@#$%^&*()_++[\\-\\]- wow name'; const editedBodyError = await monitorTestService.addProjectMonitors(project, testMonitors); expect(editedBodyError.createdMonitors.length).eql(0); @@ -107,7 +107,7 @@ export default function ({ getService }: FtrProviderContext) { 'Failed to update journey: test-id-2' ); expect(editedBodyError.failedMonitors[0].reason).eql( - 'end of the stream or a document separator is expected at line 3, column 10:\n name: [] - invalid name\n ^' + 'unknown escape sequence at line 3, column 34:\n name: "!@#$,%,^,&,*,(,),_,+,+,[,\\,\\,-,\\,\\,],-, ,w,o,w, ,n,a,m,e,"\n ^' ); } finally { await Promise.all([ diff --git a/x-pack/test/api_integration/apis/synthetics/sample_data/test_browser_policy.ts b/x-pack/test/api_integration/apis/synthetics/sample_data/test_browser_policy.ts index 5eec726cdb328..19cc3998bee0d 100644 --- a/x-pack/test/api_integration/apis/synthetics/sample_data/test_browser_policy.ts +++ b/x-pack/test/api_integration/apis/synthetics/sample_data/test_browser_policy.ts @@ -177,9 +177,9 @@ export const getTestBrowserSyntheticsPolicy = ({ }, enabled: { value: true, type: 'bool' }, type: { value: 'browser', type: 'text' }, - name: { value: 'Test HTTP Monitor 03', type: 'text' }, + name: { value: '"Test HTTP Monitor 03"', type: 'text' }, schedule: { value: '"@every 3m"', type: 'text' }, - 'service.name': { value: '', type: 'text' }, + 'service.name': { value: null, type: 'text' }, timeout: { value: '16s', type: 'text' }, tags: { value: '["cookie-test","browser"]', type: 'yaml' }, 'source.zip_url.url': { type: 'text' }, @@ -210,8 +210,8 @@ export const getTestBrowserSyntheticsPolicy = ({ 'source.zip_url.ssl.verification_mode': { type: 'text' }, 'source.zip_url.ssl.supported_protocols': { type: 'yaml' }, 'source.zip_url.proxy_url': { type: 'text' }, - location_name: { value: 'Test private location 0', type: 'text' }, - id: { value: id, type: 'text' }, + location_name: { value: JSON.stringify('Test private location 0'), type: 'text' }, + id: { value: JSON.stringify(id), type: 'text' }, config_id: { value: id, type: 'text' }, run_once: { value: false, type: 'bool' }, origin: { value: 'ui', type: 'text' }, diff --git a/x-pack/test/api_integration/apis/synthetics/sample_data/test_policy.ts b/x-pack/test/api_integration/apis/synthetics/sample_data/test_policy.ts index b0962e4d285e6..d59aa207c28a2 100644 --- a/x-pack/test/api_integration/apis/synthetics/sample_data/test_policy.ts +++ b/x-pack/test/api_integration/apis/synthetics/sample_data/test_policy.ts @@ -48,17 +48,23 @@ export const getTestSyntheticsPolicy = ( }, enabled: { value: true, type: 'bool' }, type: { value: 'http', type: 'text' }, - name: { value: name, type: 'text' }, + name: { value: `"${name}"`, type: 'text' }, schedule: { value: '"@every 5m"', type: 'text' }, - urls: { value: 'https://nextjs-test-synthetics.vercel.app/api/users', type: 'text' }, - 'service.name': { value: '', type: 'text' }, + urls: { + value: JSON.stringify('https://nextjs-test-synthetics.vercel.app/api/users'), + type: 'text', + }, + 'service.name': { value: null, type: 'text' }, timeout: { value: '3ms', type: 'text' }, max_redirects: { value: '3', type: 'integer' }, - proxy_url: { value: proxyUrl ?? 'http://proxy.com', type: 'text' }, + proxy_url: { + value: JSON.stringify(proxyUrl) ?? JSON.stringify('http://proxy.com'), + type: 'text', + }, proxy_headers: { value: null, type: 'yaml' }, tags: { value: '["tag1","tag2"]', type: 'yaml' }, - username: { value: 'test-username', type: 'text' }, - password: { value: 'test', type: 'password' }, + username: { value: '"test-username"', type: 'text' }, + password: { value: '"test"', type: 'password' }, 'response.include_headers': { value: true, type: 'bool' }, 'response.include_body': { value: 'never', type: 'text' }, 'response.include_body_max_bytes': { value: '1024', type: 'text' }, @@ -85,8 +91,11 @@ export const getTestSyntheticsPolicy = ( value: isTLSEnabled ? '["TLSv1.1","TLSv1.2"]' : null, type: 'yaml', }, - location_name: { value: locationName ?? 'Test private location 0', type: 'text' }, - id: { value: id, type: 'text' }, + location_name: { + value: JSON.stringify(locationName) ?? JSON.stringify('Test private location 0'), + type: 'text', + }, + id: { value: JSON.stringify(id), type: 'text' }, config_id: { value: id, type: 'text' }, run_once: { value: false, type: 'bool' }, origin: { value: 'ui', type: 'text' }, diff --git a/x-pack/test/api_integration/apis/synthetics/sample_data/test_project_monitor_policy.ts b/x-pack/test/api_integration/apis/synthetics/sample_data/test_project_monitor_policy.ts index bc015cb4bd77c..2289fb68d2565 100644 --- a/x-pack/test/api_integration/apis/synthetics/sample_data/test_project_monitor_policy.ts +++ b/x-pack/test/api_integration/apis/synthetics/sample_data/test_project_monitor_policy.ts @@ -202,9 +202,9 @@ export const getTestProjectSyntheticsPolicy = ( }, enabled: { value: true, type: 'bool' }, type: { value: 'browser', type: 'text' }, - name: { value: 'check if title is present', type: 'text' }, + name: { value: '"check if title is present"', type: 'text' }, schedule: { value: '"@every 10m"', type: 'text' }, - 'service.name': { value: '', type: 'text' }, + 'service.name': { value: null, type: 'text' }, timeout: { value: null, type: 'text' }, tags: { value: null, type: 'yaml' }, 'source.zip_url.url': { type: 'text' }, @@ -238,13 +238,13 @@ export const getTestProjectSyntheticsPolicy = ( 'source.zip_url.ssl.verification_mode': { type: 'text' }, 'source.zip_url.ssl.supported_protocols': { type: 'yaml' }, 'source.zip_url.proxy_url': { type: 'text' }, - location_name: { value: 'Test private location 0', type: 'text' }, - id: { value: id, type: 'text' }, + location_name: { value: '"Test private location 0"', type: 'text' }, + id: { value: `"${id}"`, type: 'text' }, config_id: { value: configId, type: 'text' }, run_once: { value: false, type: 'bool' }, origin: { value: 'project', type: 'text' }, - 'monitor.project.id': { value: projectId, type: 'text' }, - 'monitor.project.name': { value: projectId, type: 'text' }, + 'monitor.project.id': { value: JSON.stringify(projectId), type: 'text' }, + 'monitor.project.name': { value: JSON.stringify(projectId), type: 'text' }, ...inputs, }, id: `synthetics/browser-browser-4b6abc6c-118b-4d93-a489-1135500d09f1-${projectId}-default-d70a46e0-22ea-11ed-8c6b-09a2d21dfbc3`, From 6591da49df79f67f20c3739f6ae5c1d739e64e3e Mon Sep 17 00:00:00 2001 From: "Joey F. Poon" Date: Mon, 8 May 2023 12:17:59 -0500 Subject: [PATCH 10/19] [Security Solution] add doc signing tests (#156916) --- .../e2e/endpoint/response_console.cy.ts | 46 +++++++++++++ .../cypress/support/data_loaders.ts | 15 +++++ .../cypress/support/response_actions.ts | 67 +++++++++++++++++++ .../management/cypress_endpoint.config.ts | 3 + .../endpoint/common/endpoint_host_services.ts | 15 +++++ .../endpoint/common/response_actions.ts | 55 ++++++++++++++- 6 files changed, 200 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/security_solution/public/management/cypress/support/response_actions.ts diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint/response_console.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint/response_console.cy.ts index 882cf45465111..a2dc9cde510d0 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint/response_console.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint/response_console.cy.ts @@ -167,4 +167,50 @@ describe('Response console', () => { waitForCommandToBeExecuted(); }); }); + + describe('document signing', () => { + let response: IndexedFleetEndpointPolicyResponse; + let initialAgentData: Agent; + + before(() => { + getAgentByHostName(endpointHostname).then((agentData) => { + initialAgentData = agentData; + }); + + getEndpointIntegrationVersion().then((version) => + createAgentPolicyTask(version).then((data) => { + response = data; + }) + ); + }); + + after(() => { + if (initialAgentData?.policy_id) { + reassignAgentPolicy(initialAgentData.id, initialAgentData.policy_id); + } + if (response) { + cy.task('deleteIndexedFleetEndpointPolicies', response); + } + }); + + it('should fail if data tampered', () => { + waitForEndpointListPageToBeLoaded(endpointHostname); + checkEndpointListForOnlyUnIsolatedHosts(); + openResponseConsoleFromEndpointList(); + performCommandInputChecks('isolate'); + + // stop host so that we ensure tamper happens before endpoint processes the action + cy.task('stopEndpointHost'); + // get action doc before we submit command so we know when the new action doc is indexed + cy.task('getLatestActionDoc').then((previousActionDoc) => { + submitCommand(); + cy.task('tamperActionDoc', previousActionDoc); + }); + cy.task('startEndpointHost'); + + const actionValidationErrorMsg = + 'Fleet action response error: Failed to validate action signature; check Endpoint logs for details'; + cy.contains(actionValidationErrorMsg, { timeout: 120000 }).should('exist'); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/management/cypress/support/data_loaders.ts b/x-pack/plugins/security_solution/public/management/cypress/support/data_loaders.ts index bd0438542dc73..1af2aee01b4e2 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/support/data_loaders.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/support/data_loaders.ts @@ -46,8 +46,11 @@ import { indexEndpointRuleAlerts, } from '../../../../common/endpoint/data_loaders/index_endpoint_rule_alerts'; import { + startEndpointHost, createAndEnrollEndpointHost, destroyEndpointHost, + getEndpointHosts, + stopEndpointHost, } from '../../../../scripts/endpoint/common/endpoint_host_services'; /** @@ -220,5 +223,17 @@ export const dataLoadersForRealEndpoints = ( const { kbnClient } = await stackServicesPromise; return destroyEndpointHost(kbnClient, createdHost).then(() => null); }, + + stopEndpointHost: async () => { + const hosts = await getEndpointHosts(); + const hostName = hosts[0].name; + return stopEndpointHost(hostName); + }, + + startEndpointHost: async () => { + const hosts = await getEndpointHosts(); + const hostName = hosts[0].name; + return startEndpointHost(hostName); + }, }); }; diff --git a/x-pack/plugins/security_solution/public/management/cypress/support/response_actions.ts b/x-pack/plugins/security_solution/public/management/cypress/support/response_actions.ts new file mode 100644 index 0000000000000..f6fbbf7797f90 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/cypress/support/response_actions.ts @@ -0,0 +1,67 @@ +/* + * 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 { get } from 'lodash'; + +import { + getLatestActionDoc, + updateActionDoc, + waitForNewActionDoc, +} from '../../../../scripts/endpoint/common/response_actions'; +import { createRuntimeServices } from '../../../../scripts/endpoint/common/stack_services'; + +export const responseActionTasks = ( + on: Cypress.PluginEvents, + config: Cypress.PluginConfigOptions +): void => { + const stackServicesPromise = createRuntimeServices({ + kibanaUrl: config.env.KIBANA_URL, + elasticsearchUrl: config.env.ELASTICSEARCH_URL, + username: config.env.ELASTICSEARCH_USERNAME, + password: config.env.ELASTICSEARCH_PASSWORD, + asSuperuser: true, + }); + + on('task', { + getLatestActionDoc: async () => { + const { esClient } = await stackServicesPromise; + // cypress doesn't like resolved undefined values + return getLatestActionDoc(esClient).then((doc) => doc || null); + }, + + // previousActionDoc is used to determine when a new action doc is received + tamperActionDoc: async (previousActionDoc) => { + const { esClient } = await stackServicesPromise; + const newActionDoc = await waitForNewActionDoc(esClient, previousActionDoc); + + if (!newActionDoc) { + throw new Error('no action doc found'); + } + + const signed = get(newActionDoc, '_source.signed'); + const signedDataBuffer = Buffer.from(signed.data, 'base64'); + const signedDataJson = JSON.parse(signedDataBuffer.toString()); + const tamperedAgentsList = [...signedDataJson.agents, 'anotheragent']; + const tamperedData = { + ...signedDataJson, + agents: tamperedAgentsList, + }; + const tamperedDataString = Buffer.from(JSON.stringify(tamperedData), 'utf8').toString( + 'base64' + ); + const tamperedDoc = { + signed: { + ...signed, + data: tamperedDataString, + }, + }; + return updateActionDoc(esClient, newActionDoc._id, tamperedDoc); + }, + }); +}; diff --git a/x-pack/plugins/security_solution/public/management/cypress_endpoint.config.ts b/x-pack/plugins/security_solution/public/management/cypress_endpoint.config.ts index 50a9d8f1f5356..c707453ec4c99 100644 --- a/x-pack/plugins/security_solution/public/management/cypress_endpoint.config.ts +++ b/x-pack/plugins/security_solution/public/management/cypress_endpoint.config.ts @@ -8,6 +8,8 @@ import { defineCypressConfig } from '@kbn/cypress-config'; // eslint-disable-next-line @kbn/imports/no_boundary_crossing import { dataLoaders, dataLoadersForRealEndpoints } from './cypress/support/data_loaders'; +// eslint-disable-next-line @kbn/imports/no_boundary_crossing +import { responseActionTasks } from './cypress/support/response_actions'; // eslint-disable-next-line import/no-default-export export default defineCypressConfig({ @@ -43,6 +45,7 @@ export default defineCypressConfig({ dataLoaders(on, config); // Data loaders specific to "real" Endpoint testing dataLoadersForRealEndpoints(on, config); + responseActionTasks(on, config); }, }, }); diff --git a/x-pack/plugins/security_solution/scripts/endpoint/common/endpoint_host_services.ts b/x-pack/plugins/security_solution/scripts/endpoint/common/endpoint_host_services.ts index 5b249ee238436..6add9eb06a39b 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/common/endpoint_host_services.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/common/endpoint_host_services.ts @@ -264,3 +264,18 @@ const enrollHostWithFleet = async ({ agentId: agent.id, }; }; + +export async function getEndpointHosts(): Promise< + Array<{ name: string; state: string; ipv4: string; image: string }> +> { + const output = await execa('multipass', ['list', '--format', 'json']); + return JSON.parse(output.stdout).list; +} + +export function stopEndpointHost(hostName: string) { + return execa('multipass', ['stop', hostName]); +} + +export function startEndpointHost(hostName: string) { + return execa('multipass', ['start', hostName]); +} diff --git a/x-pack/plugins/security_solution/scripts/endpoint/common/response_actions.ts b/x-pack/plugins/security_solution/scripts/endpoint/common/response_actions.ts index cac110ce0cde1..05fbd9c14695d 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/common/response_actions.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/common/response_actions.ts @@ -6,13 +6,15 @@ */ import type { Client } from '@elastic/elasticsearch'; +import type { SearchHit } from '@elastic/elasticsearch/lib/api/types'; import { basename } from 'path'; import * as cborx from 'cbor-x'; -import { AGENT_ACTIONS_RESULTS_INDEX } from '@kbn/fleet-plugin/common'; +import { AGENT_ACTIONS_INDEX, AGENT_ACTIONS_RESULTS_INDEX } from '@kbn/fleet-plugin/common'; import { FleetActionGenerator } from '../../../common/endpoint/data_generators/fleet_action_generator'; import { EndpointActionGenerator } from '../../../common/endpoint/data_generators/endpoint_action_generator'; import type { ActionDetails, + EndpointAction, EndpointActionData, EndpointActionResponse, FileUploadMetadata, @@ -309,3 +311,54 @@ const getOutputDataIfNeeded = (action: ActionDetails): ResponseOutput => { return { output: undefined }; } }; + +export async function getLatestActionDoc( + esClient: Client +): Promise | undefined> { + return ( + await esClient.search({ + index: AGENT_ACTIONS_INDEX, + ignore_unavailable: true, + query: { + match: { + type: 'INPUT_ACTION', + }, + }, + sort: { + '@timestamp': { + order: 'desc', + }, + }, + size: 1, + }) + ).hits.hits.at(0); +} + +export async function waitForNewActionDoc( + esClient: Client, + previousActionDoc?: SearchHit, + options: { + maxAttempts: number; + interval: number; + } = { maxAttempts: 3, interval: 10000 } +): Promise | undefined> { + const { maxAttempts, interval } = options; + let attempts = 1; + let latestDoc = await getLatestActionDoc(esClient); + while ((!latestDoc || latestDoc._id === previousActionDoc?._id) && attempts <= maxAttempts) { + await new Promise((res) => setTimeout(res, interval)); + latestDoc = await getLatestActionDoc(esClient); + attempts++; + } + + return latestDoc; +} + +export function updateActionDoc(esClient: Client, id: string, doc: T) { + return esClient.update({ + index: AGENT_ACTIONS_INDEX, + id, + doc, + refresh: true, + }); +} From a61c63dc07bd027ab93c20cd0bf4b7b1039072ea Mon Sep 17 00:00:00 2001 From: Maxim Palenov Date: Mon, 8 May 2023 19:23:59 +0200 Subject: [PATCH 11/19] [RAM][SecuritySolution] Simplify rules list notify badge (#155500) **Relates to:** https://github.com/elastic/security-team/issues/5308 ## Summary This PR make improvements and simplifications to `RulesListNotifyBadge` component exported by `triggers_actions_ui` plugin spotted during [adoption](https://github.com/elastic/security-team/issues/5308) of this component in Security Solution. ## Details The list of the changes - Mixed controlled and uncontrolled state has been resolved in favour of controlled state. It means an external code is responsible to fetch and provide rule snooze settings to the component. - Loading state management has been moved inside the component. The only way to to set the component in loading state it to provide `undefined` for `snoozeSettings` input prop. - Popover's open/close state management has been moved inside the component. I haven't noticed any problems in tables (Stack Management Rules and Security Solution Rules) if there are multiple rules are shown with `RulesListNotifyBadge` rendered. In attempt to open another snooze popover a previous one closes automatically. - `rule` input prop has been renamed to `snoozeSettings`. - `isEditable` field has been moved to a separate `disabled` prop. `disabled` can accept a string which is considered as a disabled reason and displayed in a tooltip. It's quite helpful to display for example fetching errors. - `onRuleChanged` handler can return a promise so internal loading state persists until this promise resolved. ### Checklist - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [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 - [x] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [x] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [x] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) --- .../rules_list_notify_badge_sandbox.tsx | 41 +----- .../rule_management/api/api.test.ts | 14 +- .../rule_management/api/api.ts | 25 ++-- .../hooks/use_fetch_rules_snooze_settings.ts | 6 +- .../rule_snooze_badge/rule_snooze_badge.tsx | 22 +-- .../use_rule_snooze_settings.ts | 4 +- .../rule_management/logic/types.ts | 13 +- .../rules_table/rules_table_context.test.tsx | 26 ++-- .../rules_table/rules_table_context.tsx | 76 +++++----- .../logs_list/components/logs_list.tsx | 9 +- .../sections/rule_details/components/rule.tsx | 4 +- .../rule_action_error_log_flyout.tsx | 3 +- .../components/rule_details.test.tsx | 2 +- .../rule_details/components/rule_details.tsx | 3 +- .../components/rule_details_route.tsx | 46 ++++-- .../components/rule_error_log.tsx | 3 +- .../components/rule_event_log_list.tsx | 3 +- .../components/rule_event_log_list_kpi.tsx | 3 +- .../components/rule_event_log_list_table.tsx | 14 +- .../rule_execution_summary_and_chart.tsx | 3 +- .../rule_details/components/rule_route.tsx | 3 +- .../components/rule_status_panel.tsx | 13 +- .../sections/rule_details/components/types.ts | 5 + .../components/collapsed_item_actions.tsx | 4 +- .../notify_badge/notify_badge.test.tsx | 138 +++++++----------- .../components/notify_badge/notify_badge.tsx | 123 ++++++++-------- .../notify_badge_with_api.stories.tsx | 10 +- .../notify_badge/notify_badge_with_api.tsx | 80 +++------- .../components/notify_badge/types.ts | 36 +++-- .../components/rule_snooze/panel/index.tsx | 24 ++- .../rules_list/components/rules_list.tsx | 4 +- .../components/rules_list_table.tsx | 11 +- .../triggers_actions_ui/public/types.ts | 6 +- 33 files changed, 353 insertions(+), 424 deletions(-) diff --git a/x-pack/examples/triggers_actions_ui_example/public/components/rules_list_notify_badge_sandbox.tsx b/x-pack/examples/triggers_actions_ui_example/public/components/rules_list_notify_badge_sandbox.tsx index 6ff26f7725f64..de09c88742a74 100644 --- a/x-pack/examples/triggers_actions_ui_example/public/components/rules_list_notify_badge_sandbox.tsx +++ b/x-pack/examples/triggers_actions_ui_example/public/components/rules_list_notify_badge_sandbox.tsx @@ -6,46 +6,15 @@ */ import React from 'react'; -import { - TriggersAndActionsUIPublicPluginStart, - RuleTableItem, -} from '@kbn/triggers-actions-ui-plugin/public'; +import type { RuleSnoozeSettings } from '@kbn/triggers-actions-ui-plugin/public/types'; +import { TriggersAndActionsUIPublicPluginStart } from '@kbn/triggers-actions-ui-plugin/public'; interface SandboxProps { triggersActionsUi: TriggersAndActionsUIPublicPluginStart; } -const mockRule: RuleTableItem = { - id: '1', - enabled: true, - name: 'test rule', - tags: ['tag1'], - ruleTypeId: 'test_rule_type', - consumer: 'rules', - schedule: { interval: '5d' }, - actions: [ - { id: 'test', actionTypeId: 'the_connector', group: 'rule', params: { message: 'test' } }, - ], - params: { name: 'test rule type name' }, - createdBy: null, - updatedBy: null, - createdAt: new Date(), - updatedAt: new Date(), - apiKeyOwner: null, - throttle: '1m', - notifyWhen: 'onActiveAlert', +const mockSnoozeSettings: RuleSnoozeSettings = { muteAll: true, - mutedInstanceIds: [], - executionStatus: { - status: 'active', - lastExecutionDate: new Date('2020-08-20T19:23:38Z'), - }, - actionsCount: 1, - index: 0, - ruleType: 'Test Rule Type', - isEditable: true, - enabledInLicense: true, - revision: 0, }; export const RulesListNotifyBadgeSandbox = ({ triggersActionsUi }: SandboxProps) => { @@ -53,8 +22,8 @@ export const RulesListNotifyBadgeSandbox = ({ triggersActionsUi }: SandboxProps) return (
Promise.resolve()} />
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.test.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.test.ts index 5e3248818bf4e..d1e67923d7b56 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.test.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.test.ts @@ -848,7 +848,7 @@ describe('Detections Rules API', () => { mute_all: false, }, { - id: '1', + id: '2', mute_all: false, active_snoozes: [], is_snoozed_until: '2023-04-24T19:31:46.765Z', @@ -856,21 +856,19 @@ describe('Detections Rules API', () => { ], }); - const result = await fetchRulesSnoozeSettings({ ids: ['id1'] }); + const result = await fetchRulesSnoozeSettings({ ids: ['1', '2'] }); - expect(result).toEqual([ - { - id: '1', + expect(result).toEqual({ + '1': { muteAll: false, activeSnoozes: [], }, - { - id: '1', + '2': { muteAll: false, activeSnoozes: [], isSnoozedUntil: new Date('2023-04-24T19:31:46.765Z'), }, - ]); + }); }); }); }); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.ts index 24b66cada346c..74eb06bb373e9 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.ts @@ -57,8 +57,8 @@ import type { PrePackagedRulesStatusResponse, PreviewRulesProps, Rule, - RuleSnoozeSettings, RulesSnoozeSettingsBatchResponse, + RulesSnoozeSettingsMap, UpdateRulesProps, } from '../logic/types'; import { convertRulesFilterToKQL } from '../logic/utils'; @@ -198,7 +198,7 @@ export const fetchRuleById = async ({ id, signal }: FetchRuleProps): Promise => { +}: FetchRuleSnoozingProps): Promise => { const response = await KibanaServices.get().http.fetch( INTERNAL_ALERTING_API_FIND_RULES_PATH, { @@ -212,15 +212,18 @@ export const fetchRulesSnoozeSettings = async ({ } ); - return response.data?.map((snoozeSettings) => ({ - id: snoozeSettings?.id ?? '', - muteAll: snoozeSettings?.mute_all ?? false, - activeSnoozes: snoozeSettings?.active_snoozes ?? [], - isSnoozedUntil: snoozeSettings?.is_snoozed_until - ? new Date(snoozeSettings.is_snoozed_until) - : undefined, - snoozeSchedule: snoozeSettings?.snooze_schedule, - })); + return response.data?.reduce((result, { id, ...snoozeSettings }) => { + result[id] = { + muteAll: snoozeSettings.mute_all ?? false, + activeSnoozes: snoozeSettings.active_snoozes ?? [], + isSnoozedUntil: snoozeSettings.is_snoozed_until + ? new Date(snoozeSettings.is_snoozed_until) + : undefined, + snoozeSchedule: snoozeSettings.snooze_schedule, + }; + + return result; + }, {} as RulesSnoozeSettingsMap); }; export interface BulkActionSummary { diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_fetch_rules_snooze_settings.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_fetch_rules_snooze_settings.ts index 8e0ef31871826..8963baab0362b 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_fetch_rules_snooze_settings.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_fetch_rules_snooze_settings.ts @@ -9,7 +9,7 @@ import { INTERNAL_ALERTING_API_FIND_RULES_PATH } from '@kbn/alerting-plugin/comm import type { UseQueryOptions } from '@tanstack/react-query'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { useCallback } from 'react'; -import type { RuleSnoozeSettings } from '../../logic'; +import type { RulesSnoozeSettingsMap } from '../../logic'; import { fetchRulesSnoozeSettings } from '../api'; import { DEFAULT_QUERY_OPTIONS } from './constants'; @@ -25,7 +25,7 @@ const FETCH_RULE_SNOOZE_SETTINGS_QUERY_KEY = ['GET', INTERNAL_ALERTING_API_FIND_ */ export const useFetchRulesSnoozeSettings = ( ids: string[], - queryOptions?: UseQueryOptions + queryOptions?: UseQueryOptions ) => { return useQuery( [...FETCH_RULE_SNOOZE_SETTINGS_QUERY_KEY, ...ids], @@ -51,7 +51,7 @@ export const useInvalidateFetchRulesSnoozeSettingsQuery = () => { * Invalidate all queries that start with FIND_RULES_QUERY_KEY. This * includes the in-memory query cache and paged query cache. */ - queryClient.invalidateQueries(FETCH_RULE_SNOOZE_SETTINGS_QUERY_KEY, { + return queryClient.invalidateQueries(FETCH_RULE_SNOOZE_SETTINGS_QUERY_KEY, { refetchType: 'active', }); }, [queryClient]); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_snooze_badge/rule_snooze_badge.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_snooze_badge/rule_snooze_badge.tsx index e488127c25691..8808f6d3033b5 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_snooze_badge/rule_snooze_badge.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_snooze_badge/rule_snooze_badge.tsx @@ -6,7 +6,7 @@ */ import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; -import React, { useMemo } from 'react'; +import React from 'react'; import type { RuleObjectId } from '../../../../../common/detection_engine/rule_schema'; import { useUserData } from '../../../../detections/components/user_info'; import { hasUserCRUDPermission } from '../../../../common/utils/privileges'; @@ -31,20 +31,6 @@ export function RuleSnoozeBadge({ const [{ canUserCRUD }] = useUserData(); const hasCRUDPermissions = hasUserCRUDPermission(canUserCRUD); const invalidateFetchRuleSnoozeSettings = useInvalidateFetchRulesSnoozeSettingsQuery(); - const isLoading = !snoozeSettings; - const rule = useMemo( - () => ({ - id: snoozeSettings?.id ?? '', - muteAll: snoozeSettings?.muteAll ?? false, - activeSnoozes: snoozeSettings?.activeSnoozes ?? [], - isSnoozedUntil: snoozeSettings?.isSnoozedUntil - ? new Date(snoozeSettings.isSnoozedUntil) - : undefined, - snoozeSchedule: snoozeSettings?.snoozeSchedule, - isEditable: hasCRUDPermissions, - }), - [snoozeSettings, hasCRUDPermissions] - ); if (error) { return ( @@ -56,8 +42,10 @@ export function RuleSnoozeBadge({ return ( diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_snooze_badge/use_rule_snooze_settings.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_snooze_badge/use_rule_snooze_settings.ts index 94a857b1e9842..e31851a6a24b0 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_snooze_badge/use_rule_snooze_settings.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_snooze_badge/use_rule_snooze_settings.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { RuleSnoozeSettings } from '../../logic'; +import type { RuleSnoozeSettings } from '@kbn/triggers-actions-ui-plugin/public/types'; import { useFetchRulesSnoozeSettings } from '../../api/hooks/use_fetch_rules_snooze_settings'; import { useRulesTableContextOptional } from '../../../rule_management_ui/components/rules_table/rules_table/rules_table_context'; import * as i18n from './translations'; @@ -26,7 +26,7 @@ export function useRuleSnoozeSettings(id: string): UseRuleSnoozeSettingsResult { } = useFetchRulesSnoozeSettings([id], { enabled: !rulesTableSnoozeSettings?.data[id] && !rulesTableSnoozeSettings?.isFetching, }); - const snoozeSettings = rulesTableSnoozeSettings?.data[id] ?? rulesSnoozeSettings?.[0]; + const snoozeSettings = rulesTableSnoozeSettings?.data[id] ?? rulesSnoozeSettings?.[id]; const isFetching = rulesTableSnoozeSettings?.isFetching || isSingleSnoozeSettingsFetching; const isError = rulesTableSnoozeSettings?.isError || isSingleSnoozeSettingsError; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/types.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/types.ts index e22be9467c6a1..092bc81554016 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/types.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/types.ts @@ -28,6 +28,7 @@ import { type, } from '@kbn/securitysolution-io-ts-alerting-types'; import type { NamespaceType } from '@kbn/securitysolution-io-ts-list-types'; +import type { RuleSnoozeSettings } from '@kbn/triggers-actions-ui-plugin/public/types'; import { PositiveInteger } from '@kbn/securitysolution-io-ts-types'; import type { WarningSchema } from '../../../../common/detection_engine/schemas/response'; @@ -218,15 +219,13 @@ export interface FetchRulesProps { signal?: AbortSignal; } -export interface RuleSnoozeSettings { - id: string; - muteAll: boolean; - snoozeSchedule?: RuleSnooze; - activeSnoozes?: string[]; - isSnoozedUntil?: Date; -} +// Rule snooze settings map keyed by rule SO's id (not ruleId) and valued by rule snooze settings +export type RulesSnoozeSettingsMap = Record; interface RuleSnoozeSettingsResponse { + /** + * Rule's SO id + */ id: string; mute_all: boolean; snooze_schedule?: RuleSnooze; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table/rules_table_context.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table/rules_table_context.test.tsx index abc384cea3bfb..ad9ea30ed9f4d 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table/rules_table_context.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table/rules_table_context.test.tsx @@ -5,11 +5,11 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; -import type { PropsWithChildren } from 'react'; import React from 'react'; +import type { PropsWithChildren } from 'react'; +import { renderHook } from '@testing-library/react-hooks'; import { useUiSetting$ } from '../../../../../common/lib/kibana'; -import type { Rule, RuleSnoozeSettings } from '../../../../rule_management/logic/types'; +import type { Rule, RulesSnoozeSettingsMap } from '../../../../rule_management/logic'; import { useFindRules } from '../../../../rule_management/logic/use_find_rules'; import { useFetchRulesSnoozeSettings } from '../../../../rule_management/api/hooks/use_fetch_rules_snooze_settings'; import type { RulesTableState } from './rules_table_context'; @@ -34,7 +34,7 @@ function renderUseRulesTableContext({ savedState, }: { rules?: Rule[] | Error; - rulesSnoozeSettings?: RuleSnoozeSettings[] | Error; + rulesSnoozeSettings?: RulesSnoozeSettingsMap | Error; savedState?: ReturnType; }): RulesTableState { (useFindRules as jest.Mock).mockReturnValue({ @@ -189,10 +189,10 @@ describe('RulesTableContextProvider', () => { { id: '1', name: 'rule 1' }, { id: '2', name: 'rule 2' }, ] as Rule[], - rulesSnoozeSettings: [ - { id: '1', muteAll: true, snoozeSchedule: [] }, - { id: '2', muteAll: false, snoozeSchedule: [] }, - ], + rulesSnoozeSettings: { + '1': { muteAll: true, snoozeSchedule: [] }, + '2': { muteAll: false, snoozeSchedule: [] }, + }, }); expect(state.rules).toEqual([ @@ -215,20 +215,18 @@ describe('RulesTableContextProvider', () => { { id: '1', name: 'rule 1' }, { id: '2', name: 'rule 2' }, ] as Rule[], - rulesSnoozeSettings: [ - { id: '1', muteAll: true, snoozeSchedule: [] }, - { id: '2', muteAll: false, snoozeSchedule: [] }, - ], + rulesSnoozeSettings: { + '1': { muteAll: true, snoozeSchedule: [] }, + '2': { muteAll: false, snoozeSchedule: [] }, + }, }); expect(state.rulesSnoozeSettings.data).toEqual({ '1': { - id: '1', muteAll: true, snoozeSchedule: [], }, '2': { - id: '2', muteAll: false, snoozeSchedule: [], }, diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table/rules_table_context.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table/rules_table_context.tsx index 938174d0c567d..0c5550a8c66dc 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table/rules_table_context.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table/rules_table_context.tsx @@ -25,7 +25,7 @@ import type { FilterOptions, PaginationOptions, Rule, - RuleSnoozeSettings, + RulesSnoozeSettingsMap, SortingOptions, } from '../../../../rule_management/logic/types'; import { useFindRules } from '../../../../rule_management/logic/use_find_rules'; @@ -39,11 +39,11 @@ import { import { RuleSource } from './rules_table_saved_state'; import { useRulesTableSavedState } from './use_rules_table_saved_state'; -interface RulesSnoozeSettings { +interface RulesSnoozeSettingsState { /** * A map object using rule SO's id (not ruleId) as keys and snooze settings as values */ - data: Record; + data: RulesSnoozeSettingsMap; /** * Sets to true during the first data loading */ @@ -127,7 +127,7 @@ export interface RulesTableState { /** * Rules snooze settings for the current rules */ - rulesSnoozeSettings: RulesSnoozeSettings; + rulesSnoozeSettings: RulesSnoozeSettingsState; } export type LoadingRuleAction = @@ -298,7 +298,7 @@ export const RulesTableContextProvider = ({ children }: RulesTableContextProvide // Fetch rules snooze settings const { - data: rulesSnoozeSettings, + data: rulesSnoozeSettingsMap, isLoading: isSnoozeSettingsLoading, isFetching: isSnoozeSettingsFetching, isError: isSnoozeSettingsFetchError, @@ -346,19 +346,12 @@ export const RulesTableContextProvider = ({ children }: RulesTableContextProvide ] ); - const providerValue = useMemo(() => { - const rulesSnoozeSettingsMap = - rulesSnoozeSettings?.reduce((map, snoozeSettings) => { - map[snoozeSettings.id] = snoozeSettings; - - return map; - }, {} as Record) ?? {}; - - return { + const providerValue = useMemo( + () => ({ state: { rules, rulesSnoozeSettings: { - data: rulesSnoozeSettingsMap, + data: rulesSnoozeSettingsMap ?? {}, isLoading: isSnoozeSettingsLoading, isFetching: isSnoozeSettingsFetching, isError: isSnoozeSettingsFetchError, @@ -389,32 +382,33 @@ export const RulesTableContextProvider = ({ children }: RulesTableContextProvide }), }, actions, - }; - }, [ - rules, - rulesSnoozeSettings, - isSnoozeSettingsLoading, - isSnoozeSettingsFetching, - isSnoozeSettingsFetchError, - page, - perPage, - total, - filterOptions, - isPreflightInProgress, - isActionInProgress, - isAllSelected, - isFetched, - isFetching, - isLoading, - isRefetching, - isRefreshOn, - dataUpdatedAt, - loadingRules.ids, - loadingRules.action, - selectedRuleIds, - sortingOptions, - actions, - ]); + }), + [ + rules, + rulesSnoozeSettingsMap, + isSnoozeSettingsLoading, + isSnoozeSettingsFetching, + isSnoozeSettingsFetchError, + page, + perPage, + total, + filterOptions, + isPreflightInProgress, + isActionInProgress, + isAllSelected, + isFetched, + isFetching, + isLoading, + isRefetching, + isRefreshOn, + dataUpdatedAt, + loadingRules.ids, + loadingRules.action, + selectedRuleIds, + sortingOptions, + actions, + ] + ); return {children}; }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/logs_list/components/logs_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/logs_list/components/logs_list.tsx index c4fd40d11d1dd..d51b0e8d5d169 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/logs_list/components/logs_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/logs_list/components/logs_list.tsx @@ -24,7 +24,14 @@ export const LogsList = ({ 'xl' )({ ruleId: '*', - refreshToken: 0, + refreshToken: { + resolve: () => { + /* noop */ + }, + reject: () => { + /* noop */ + }, + }, initialPageSize: 50, hasRuleNames: true, hasAllSpaceSwitch: true, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.tsx index 8b99cc910d8da..e520d222cec16 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.tsx @@ -17,7 +17,7 @@ import { } from '../../common/components/with_bulk_rule_api_operations'; import './rule.scss'; import type { RuleEventLogListProps } from './rule_event_log_list'; -import { AlertListItem } from './types'; +import { AlertListItem, RefreshToken } from './types'; import { getIsExperimentalFeatureEnabled } from '../../../../common/get_experimental_features'; import { suspendedComponentWithProps } from '../../../lib/suspended_component_with_props'; import { @@ -41,7 +41,7 @@ type RuleProps = { readOnly: boolean; ruleSummary: RuleSummary; requestRefresh: () => Promise; - refreshToken?: number; + refreshToken?: RefreshToken; numberOfExecutions: number; onChangeDuration: (length: number) => void; durationEpoch?: number; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_action_error_log_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_action_error_log_flyout.tsx index aa914e2818c03..04d0ca41303f9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_action_error_log_flyout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_action_error_log_flyout.tsx @@ -23,10 +23,11 @@ import { import { IExecutionLog } from '@kbn/alerting-plugin/common'; import { RuleErrorLogWithApi } from './rule_error_log'; import { RuleActionErrorBadge } from './rule_action_error_badge'; +import { RefreshToken } from './types'; export interface RuleActionErrorLogFlyoutProps { runLog: IExecutionLog; - refreshToken?: number; + refreshToken?: RefreshToken; onClose: () => void; activeSpaceId?: string; } 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 d07ee7460217c..ded0898639756 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 @@ -63,7 +63,7 @@ const mockRuleApis = { muteRule: jest.fn(), unmuteRule: jest.fn(), requestRefresh: jest.fn(), - refreshToken: Date.now(), + refreshToken: { resolve: jest.fn(), reject: jest.fn() }, snoozeRule: jest.fn(), unsnoozeRule: jest.fn(), bulkEnableRules: jest.fn(), diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.tsx index fd125851c36b3..57da8f7d37594 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.tsx @@ -68,13 +68,14 @@ import { MULTIPLE_RULE_TITLE, } from '../../rules_list/translations'; import { useBulkOperationToast } from '../../../hooks/use_bulk_operation_toast'; +import { RefreshToken } from './types'; export type RuleDetailsProps = { rule: Rule; ruleType: RuleType; actionTypes: ActionType[]; requestRefresh: () => Promise; - refreshToken?: number; + refreshToken?: RefreshToken; } & Pick< BulkOperationsComponentOpts, 'bulkDisableRules' | 'bulkEnableRules' | 'bulkDeleteRules' | 'snoozeRule' | 'unsnoozeRule' diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details_route.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details_route.tsx index 30319ce13ae15..116c97ef905d7 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details_route.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details_route.tsx @@ -6,7 +6,7 @@ */ import { i18n } from '@kbn/i18n'; -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { ToastsApi } from '@kbn/core/public'; import { EuiSpacer } from '@elastic/eui'; @@ -49,18 +49,38 @@ export const RuleDetailsRoute: React.FunctionComponent = const [rule, setRule] = useState(null); const [ruleType, setRuleType] = useState(null); const [actionTypes, setActionTypes] = useState(null); - const [refreshToken, requestRefresh] = React.useState(); + const [refreshToken, setRefreshToken] = useState<{ + resolve: () => void; + reject: () => void; + }>(); + const requestRefresh = useCallback( + () => + new Promise((resolve, reject) => { + setRefreshToken({ + resolve, + reject, + }); + }), + [setRefreshToken] + ); + useEffect(() => { - getRuleData( - ruleId, - loadRuleTypes, - resolveRule, - loadActionTypes, - setRule, - setRuleType, - setActionTypes, - toasts - ); + const loadData = async () => { + await getRuleData( + ruleId, + loadRuleTypes, + resolveRule, + loadActionTypes, + setRule, + setRuleType, + setActionTypes, + toasts + ); + + refreshToken?.resolve(); + }; + + loadData(); }, [ruleId, http, loadActionTypes, loadRuleTypes, resolveRule, toasts, refreshToken]); useEffect(() => { @@ -117,7 +137,7 @@ export const RuleDetailsRoute: React.FunctionComponent = rule={rule} ruleType={ruleType} actionTypes={actionTypes} - requestRefresh={async () => requestRefresh(Date.now())} + requestRefresh={requestRefresh} refreshToken={refreshToken} /> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_error_log.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_error_log.tsx index b5940e9d4b1e3..f489a40af478d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_error_log.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_error_log.tsx @@ -30,6 +30,7 @@ import { withBulkRuleOperations, } from '../../common/components/with_bulk_rule_api_operations'; import { EventLogListCellRenderer } from '../../common/components/event_log'; +import { RefreshToken } from './types'; const getParsedDate = (date: string) => { if (date.includes('now')) { @@ -62,7 +63,7 @@ const MAX_RESULTS = 1000; export type RuleErrorLogProps = { ruleId: string; runId?: string; - refreshToken?: number; + refreshToken?: RefreshToken; spaceId?: string; logFromDifferentSpace?: boolean; requestRefresh?: () => Promise; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list.tsx index aec6566d58668..75234562096d0 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list.tsx @@ -12,6 +12,7 @@ import { RuleExecutionSummaryAndChartWithApi } from './rule_execution_summary_an import { RuleSummary, RuleType } from '../../../../types'; import { ComponentOpts as RuleApis } from '../../common/components/with_bulk_rule_api_operations'; import { RuleEventLogListTableWithApi } from './rule_event_log_list_table'; +import { RefreshToken } from './types'; const RULE_EVENT_LOG_LIST_STORAGE_KEY = 'xpack.triggersActionsUI.ruleEventLogList.initialColumns'; @@ -23,7 +24,7 @@ export interface RuleEventLogListCommonProps { ruleId: string; ruleType: RuleType; localStorageKey?: string; - refreshToken?: number; + refreshToken?: RefreshToken; requestRefresh?: () => Promise; loadExecutionLogAggregations?: RuleApis['loadExecutionLogAggregations']; fetchRuleSummary?: boolean; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_kpi.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_kpi.tsx index 7ecf22c1b593a..2f0aa0460c754 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_kpi.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_kpi.tsx @@ -17,6 +17,7 @@ import { import { getIsExperimentalFeatureEnabled } from '../../../../common/get_experimental_features'; import { useKibana } from '../../../../common/lib/kibana'; import { EventLogListStatus, EventLogStat } from '../../common/components/event_log'; +import { RefreshToken } from './types'; const getParsedDate = (date: string) => { if (date.includes('now')) { @@ -59,7 +60,7 @@ export type RuleEventLogListKPIProps = { dateEnd: string; outcomeFilter?: string[]; message?: string; - refreshToken?: number; + refreshToken?: RefreshToken; namespaces?: Array; } & Pick; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_table.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_table.tsx index 7965f58fa8420..aecab951b3b18 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_table.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_table.tsx @@ -52,6 +52,7 @@ import { } from '../../common/components/with_bulk_rule_api_operations'; import { useMultipleSpaces } from '../../../hooks/use_multiple_spaces'; import { RulesSettingsLink } from '../../../components/rules_setting/rules_settings_link'; +import { RefreshToken } from './types'; const getEmptyFunctionComponent: React.FC = ({ children }) => <>{children}; @@ -102,7 +103,7 @@ export type RuleEventLogListOptions = 'stackManagement' | 'default'; export type RuleEventLogListCommonProps = { ruleId: string; localStorageKey?: string; - refreshToken?: number; + refreshToken?: RefreshToken; initialPageSize?: number; // Duplicating these properties is extremely silly but it's the only way to get Jest to cooperate with the way this component is structured overrideLoadExecutionLogAggregations?: RuleApis['loadExecutionLogAggregations']; @@ -142,7 +143,7 @@ export const RuleEventLogListTable = ( const [search, setSearch] = useState(''); const [isFlyoutOpen, setIsFlyoutOpen] = useState(false); const [selectedRunLog, setSelectedRunLog] = useState(); - const [internalRefreshToken, setInternalRefreshToken] = useState( + const [internalRefreshToken, setInternalRefreshToken] = useState( refreshToken ); const [showFromAllSpaces, setShowFromAllSpaces] = useState(false); @@ -298,7 +299,14 @@ export const RuleEventLogListTable = ( ); const onRefresh = () => { - setInternalRefreshToken(Date.now()); + setInternalRefreshToken({ + resolve: () => { + /* noop */ + }, + reject: () => { + /* noop */ + }, + }); loadEventLogs(); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_execution_summary_and_chart.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_execution_summary_and_chart.tsx index c3fb3abf58d0f..f3e7409b66d2b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_execution_summary_and_chart.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_execution_summary_and_chart.tsx @@ -20,6 +20,7 @@ import { ComponentOpts as RuleApis, withBulkRuleOperations, } from '../../common/components/with_bulk_rule_api_operations'; +import { RefreshToken } from './types'; export const DEFAULT_NUMBER_OF_EXECUTIONS = 60; @@ -29,7 +30,7 @@ type RuleExecutionSummaryAndChartProps = { ruleSummary?: RuleSummary; numberOfExecutions?: number; isLoadingRuleSummary?: boolean; - refreshToken?: number; + refreshToken?: RefreshToken; onChangeDuration?: (duration: number) => void; requestRefresh?: () => Promise; fetchRuleSummary?: boolean; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_route.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_route.tsx index 7b72f34d05bac..f1df6af277549 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_route.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_route.tsx @@ -16,13 +16,14 @@ import { import { RuleWithApi as Rules } from './rule'; import { useKibana } from '../../../../common/lib/kibana'; import { CenterJustifiedSpinner } from '../../../components/center_justified_spinner'; +import { RefreshToken } from './types'; type WithRuleSummaryProps = { rule: Rule; ruleType: RuleType; readOnly: boolean; requestRefresh: () => Promise; - refreshToken?: number; + refreshToken?: RefreshToken; } & Pick; export const RuleRoute: React.FunctionComponent = ({ diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_status_panel.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_status_panel.tsx index ae5e4251b87e2..3c02b7bcdbc9b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_status_panel.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_status_panel.tsx @@ -58,12 +58,8 @@ export const RuleStatusPanel: React.FC = ({ statusMessage, loadExecutionLogAggregations, }) => { - const [isSnoozeLoading, setIsSnoozeLoading] = useState(false); - const [isSnoozeOpen, setIsSnoozeOpen] = useState(false); const [lastNumberOfExecutions, setLastNumberOfExecutions] = useState(null); - const openSnooze = useCallback(() => setIsSnoozeOpen(true), [setIsSnoozeOpen]); - const closeSnooze = useCallback(() => setIsSnoozeOpen(false), [setIsSnoozeOpen]); const onSnoozeRule = useCallback( (snoozeSchedule) => snoozeRule(rule, snoozeSchedule), [rule, snoozeRule] @@ -187,12 +183,9 @@ export const RuleStatusPanel: React.FC = ({ void; + reject: () => void; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/collapsed_item_actions.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/collapsed_item_actions.tsx index ba36bea2eac9d..7df59161dffeb 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/collapsed_item_actions.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/collapsed_item_actions.tsx @@ -83,7 +83,7 @@ export const CollapsedItemActions: React.FunctionComponent = ({ try { onLoading(true); await snoozeRule(item, snoozeSchedule); - onRuleChanged(); + await onRuleChanged(); toasts.addSuccess(SNOOZE_SUCCESS_MESSAGE); } catch (e) { toasts.addDanger(SNOOZE_FAILED_MESSAGE); @@ -101,7 +101,7 @@ export const CollapsedItemActions: React.FunctionComponent = ({ try { onLoading(true); await unsnoozeRule(item, scheduleIds); - onRuleChanged(); + await onRuleChanged(); toasts.addSuccess(UNSNOOZE_SUCCESS_MESSAGE); } catch (e) { toasts.addDanger(SNOOZE_FAILED_MESSAGE); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/notify_badge/notify_badge.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/notify_badge/notify_badge.test.tsx index 35cddf249d8e7..3e7843f9ab143 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/notify_badge/notify_badge.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/notify_badge/notify_badge.test.tsx @@ -9,73 +9,27 @@ import { EuiButtonIcon, EuiButton } from '@elastic/eui'; import React from 'react'; import { act } from 'react-dom/test-utils'; import moment from 'moment'; - -import { RuleTableItem } from '../../../../../types'; import { mountWithIntl } from '@kbn/test-jest-helpers'; import { RulesListNotifyBadge } from './notify_badge'; jest.mock('../../../../../common/lib/kibana'); -const onClick = jest.fn(); -const onClose = jest.fn(); -const onLoading = jest.fn(); -const onRuleChanged = jest.fn(); -const snoozeRule = jest.fn(); -const unsnoozeRule = jest.fn(); - -const getRule = (overrides = {}): RuleTableItem => ({ - id: '1', - enabled: true, - name: 'test rule', - tags: ['tag1'], - ruleTypeId: 'test_rule_type', - consumer: 'rules', - schedule: { interval: '5d' }, - actions: [ - { id: 'test', actionTypeId: 'the_connector', group: 'rule', params: { message: 'test' } }, - ], - params: { name: 'test rule type name' }, - createdBy: null, - updatedBy: null, - createdAt: new Date(), - updatedAt: new Date(), - apiKeyOwner: null, - throttle: '1m', - notifyWhen: 'onActiveAlert', - muteAll: false, - mutedInstanceIds: [], - executionStatus: { - status: 'active', - lastExecutionDate: new Date('2020-08-20T19:23:38Z'), - }, - actionsCount: 1, - index: 0, - ruleType: 'Test Rule Type', - isEditable: true, - enabledInLicense: true, - revision: 0, - ...overrides, -}); - describe('RulesListNotifyBadge', () => { + const onRuleChanged = jest.fn(); + const snoozeRule = jest.fn(); + const unsnoozeRule = jest.fn(); + afterEach(() => { jest.clearAllMocks(); }); - it('renders the notify badge correctly', async () => { - jest.useFakeTimers().setSystemTime(moment('1990-01-01').toDate()); - + it('renders an unsnoozed badge', () => { const wrapper = mountWithIntl( { // Rule without snooze const badge = wrapper.find(EuiButtonIcon); expect(badge.first().props().iconType).toEqual('bell'); + }); + + it('renders a snoozed badge', () => { + jest.useFakeTimers().setSystemTime(moment('1990-01-01').toDate()); + + const wrapper = mountWithIntl( + + ); - // Rule with snooze - wrapper.setProps({ - rule: getRule({ - isSnoozedUntil: moment('1990-02-01').format(), - }), - }); const snoozeBadge = wrapper.find(EuiButton); + expect(snoozeBadge.first().props().iconType).toEqual('bellSlash'); expect(snoozeBadge.text()).toEqual('Feb 1'); + }); - // Rule with indefinite snooze - wrapper.setProps({ - rule: getRule({ - isSnoozedUntil: moment('1990-02-01').format(), - muteAll: true, - }), - }); + it('renders an indefinitely snoozed badge', () => { + jest.useFakeTimers().setSystemTime(moment('1990-01-01').toDate()); + + const wrapper = mountWithIntl( + + ); const indefiniteSnoozeBadge = wrapper.find(EuiButtonIcon); + expect(indefiniteSnoozeBadge.first().props().iconType).toEqual('bellSlash'); expect(indefiniteSnoozeBadge.text()).toEqual(''); }); @@ -113,24 +87,21 @@ describe('RulesListNotifyBadge', () => { jest.useFakeTimers().setSystemTime(moment('1990-01-01').toDate()); const wrapper = mountWithIntl( ); + // Open the popover + wrapper.find(EuiButtonIcon).first().simulate('click'); + // Snooze for 1 hour wrapper.find('button[data-test-subj="linkSnooze1h"]').first().simulate('click'); - expect(onLoading).toHaveBeenCalledWith(true); expect(snoozeRule).toHaveBeenCalledWith({ duration: 3600000, id: null, @@ -146,38 +117,31 @@ describe('RulesListNotifyBadge', () => { }); expect(onRuleChanged).toHaveBeenCalled(); - expect(onLoading).toHaveBeenCalledWith(false); - expect(onClose).toHaveBeenCalled(); }); it('should allow the user to unsnooze rules', async () => { jest.useFakeTimers().setSystemTime(moment('1990-01-01').toDate()); const wrapper = mountWithIntl( ); + // Open the popover + wrapper.find(EuiButtonIcon).first().simulate('click'); + // Unsnooze wrapper.find('[data-test-subj="ruleSnoozeCancel"] button').simulate('click'); - expect(onLoading).toHaveBeenCalledWith(true); await act(async () => { jest.runOnlyPendingTimers(); }); expect(unsnoozeRule).toHaveBeenCalled(); - expect(onLoading).toHaveBeenCalledWith(false); - expect(onClose).toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/notify_badge/notify_badge.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/notify_badge/notify_badge.tsx index 92b8820775891..18076102c1fd9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/notify_badge/notify_badge.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/notify_badge/notify_badge.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useCallback, useMemo } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import moment from 'moment'; import { EuiButton, @@ -30,35 +30,38 @@ import { } from './translations'; import { RulesListNotifyBadgeProps } from './types'; -export const RulesListNotifyBadge: React.FunctionComponent = (props) => { - const { - isLoading = false, - rule, - isOpen, - onClick, - onClose, - onLoading, - onRuleChanged, - snoozeRule, - unsnoozeRule, - showOnHover = false, - showTooltipInline = false, - } = props; - - const { isSnoozedUntil, muteAll, isEditable } = rule; - +export const RulesListNotifyBadge: React.FunctionComponent = ({ + snoozeSettings, + loading = false, + disabled = false, + onRuleChanged, + snoozeRule, + unsnoozeRule, + showOnHover = false, + showTooltipInline = false, +}) => { + const [requestInFlight, setRequestInFlightLoading] = useState(false); + const isLoading = loading || requestInFlight; + const isDisabled = Boolean(disabled) || !snoozeSettings; + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const openPopover = useCallback(() => setIsPopoverOpen(true), [setIsPopoverOpen]); + const closePopover = useCallback(() => setIsPopoverOpen(false), [setIsPopoverOpen]); + const isSnoozedUntil = snoozeSettings?.isSnoozedUntil; + const muteAll = snoozeSettings?.muteAll ?? false; const isSnoozedIndefinitely = muteAll; + const isSnoozed = useMemo( + () => (snoozeSettings ? isRuleSnoozed(snoozeSettings) : false), + [snoozeSettings] + ); + const nextScheduledSnooze = useMemo( + () => (snoozeSettings ? getNextRuleSnoozeSchedule(snoozeSettings) : null), + [snoozeSettings] + ); const { notifications: { toasts }, } = useKibana().services; - const isSnoozed = useMemo(() => { - return isRuleSnoozed(rule); - }, [rule]); - - const nextScheduledSnooze = useMemo(() => getNextRuleSnoozeSchedule(rule), [rule]); - const isScheduled = useMemo(() => { return !isSnoozed && Boolean(nextScheduledSnooze); }, [nextScheduledSnooze, isSnoozed]); @@ -124,18 +127,18 @@ export const RulesListNotifyBadge: React.FunctionComponent {formattedSnoozeText} ); - }, [formattedSnoozeText, isLoading, isEditable, onClick]); + }, [formattedSnoozeText, isLoading, isDisabled, openPopover]); const scheduledSnoozeButton = useMemo(() => { // TODO: Implement scheduled snooze button @@ -143,18 +146,18 @@ export const RulesListNotifyBadge: React.FunctionComponent {formattedSnoozeText} ); - }, [formattedSnoozeText, isLoading, isEditable, onClick]); + }, [formattedSnoozeText, isLoading, isDisabled, openPopover]); const unsnoozedButton = useMemo(() => { // This show on hover is needed because we need style sheets to achieve the @@ -165,32 +168,32 @@ export const RulesListNotifyBadge: React.FunctionComponent ); - }, [isOpen, isLoading, isEditable, showOnHover, onClick]); + }, [isPopoverOpen, isLoading, isDisabled, showOnHover, openPopover]); const indefiniteSnoozeButton = useMemo(() => { return ( ); - }, [isLoading, isEditable, onClick]); + }, [isLoading, isDisabled, openPopover]); const button = useMemo(() => { if (isScheduled) { @@ -214,57 +217,55 @@ export const RulesListNotifyBadge: React.FunctionComponent { - if (isOpen || showTooltipInline) { - return button; - } - return {button}; - }, [isOpen, button, snoozeTooltipText, showTooltipInline]); + const tooltipContent = + typeof disabled === 'string' + ? disabled + : isPopoverOpen || showTooltipInline + ? undefined + : snoozeTooltipText; - const onClosePopover = useCallback(() => { - onClose(); - // Set a timeout on closing the scheduler to avoid flicker - // setTimeout(onCloseScheduler, 1000); - }, [onClose]); + return {button}; + }, [disabled, isPopoverOpen, button, snoozeTooltipText, showTooltipInline]); const onApplySnooze = useCallback( async (schedule: SnoozeSchedule) => { try { - onLoading(true); - onClosePopover(); + setRequestInFlightLoading(true); + closePopover(); await snoozeRule(schedule); - onRuleChanged(); + await onRuleChanged(); toasts.addSuccess(SNOOZE_SUCCESS_MESSAGE); } catch (e) { toasts.addDanger(SNOOZE_FAILED_MESSAGE); } finally { - onLoading(false); + setRequestInFlightLoading(false); } }, - [onLoading, snoozeRule, onRuleChanged, toasts, onClosePopover] + [setRequestInFlightLoading, snoozeRule, onRuleChanged, toasts, closePopover] ); const onApplyUnsnooze = useCallback( async (scheduleIds?: string[]) => { try { - onLoading(true); - onClosePopover(); + setRequestInFlightLoading(true); + closePopover(); await unsnoozeRule(scheduleIds); - onRuleChanged(); + await onRuleChanged(); toasts.addSuccess(UNSNOOZE_SUCCESS_MESSAGE); } catch (e) { toasts.addDanger(SNOOZE_FAILED_MESSAGE); } finally { - onLoading(false); + setRequestInFlightLoading(false); } }, - [onLoading, unsnoozeRule, onRuleChanged, toasts, onClosePopover] + [setRequestInFlightLoading, unsnoozeRule, onRuleChanged, toasts, closePopover] ); const popover = ( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/notify_badge/notify_badge_with_api.stories.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/notify_badge/notify_badge_with_api.stories.tsx index 6fefd93632aaa..db2f8d2333c8c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/notify_badge/notify_badge_with_api.stories.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/notify_badge/notify_badge_with_api.stories.tsx @@ -27,7 +27,7 @@ export default { title: 'app/RulesListNotifyBadgeWithApi', component: RulesListNotifyBadgeWithApi, argTypes: { - rule: { + snoozeSettings: { defaultValue: rule, control: { type: 'object', @@ -54,7 +54,7 @@ export default { onRuleChanged: {}, }, args: { - rule, + snoozeSettings: rule, onRuleChanged: (...args: any) => action('onRuleChanged')(args), }, } as Meta; @@ -69,7 +69,7 @@ const IndefinitelyDate = new Date(); IndefinitelyDate.setDate(IndefinitelyDate.getDate() + 1); export const IndefinitelyRuleNotifyBadgeWithApi = Template.bind({}); IndefinitelyRuleNotifyBadgeWithApi.args = { - rule: { + snoozeSettings: { ...rule, muteAll: true, isSnoozedUntil: IndefinitelyDate, @@ -80,7 +80,7 @@ export const ActiveSnoozesRuleNotifyBadgeWithApi = Template.bind({}); const ActiveSnoozeDate = new Date(); ActiveSnoozeDate.setDate(ActiveSnoozeDate.getDate() + 2); ActiveSnoozesRuleNotifyBadgeWithApi.args = { - rule: { + snoozeSettings: { ...rule, activeSnoozes: ['24da3b26-bfa5-4317-b72f-4063dbea618e'], isSnoozedUntil: ActiveSnoozeDate, @@ -111,7 +111,7 @@ export const ScheduleSnoozesRuleNotifyBadgeWithApi: Story = (props) => { - const { onRuleChanged, rule, isLoading, showTooltipInline, showOnHover } = props; +> = ({ + ruleId, + snoozeSettings, + loading, + disabled, + showTooltipInline, + showOnHover, + onRuleChanged, +}) => { const { http } = useKibana().services; - const [currentlyOpenNotify, setCurrentlyOpenNotify] = useState(); - const [loadingSnoozeAction, setLoadingSnoozeAction] = useState(false); - const [ruleSnoozeInfo, setRuleSnoozeInfo] = - useState(rule); - - // This helps to fix problems related to rule prop updates. As component handles the loading state via isLoading prop - // rule prop is obviously not ready atm so when it's ready ruleSnoozeInfo won't be updated without useEffect so - // incorrect state will be shown. - useEffect(() => { - setRuleSnoozeInfo(rule); - }, [rule]); const onSnoozeRule = useCallback( - (snoozeSchedule: SnoozeSchedule) => { - return snoozeRuleApi({ http, id: ruleSnoozeInfo.id, snoozeSchedule }); - }, - [http, ruleSnoozeInfo.id] + (snoozeSchedule: SnoozeSchedule) => + ruleId ? snoozeRuleApi({ http, id: ruleId, snoozeSchedule }) : Promise.resolve(), + [http, ruleId] ); const onUnsnoozeRule = useCallback( - (scheduleIds?: string[]) => { - return unsnoozeRuleApi({ http, id: ruleSnoozeInfo.id, scheduleIds }); - }, - [http, ruleSnoozeInfo.id] + (scheduleIds?: string[]) => + ruleId ? unsnoozeRuleApi({ http, id: ruleId, scheduleIds }) : Promise.resolve(), + [http, ruleId] ); - const onRuleChangedCallback = useCallback(async () => { - const updatedRule = await loadRule({ - http, - ruleId: ruleSnoozeInfo.id, - }); - setLoadingSnoozeAction(false); - setRuleSnoozeInfo((prevRule) => ({ - ...prevRule, - activeSnoozes: updatedRule.activeSnoozes, - isSnoozedUntil: updatedRule.isSnoozedUntil, - muteAll: updatedRule.muteAll, - snoozeSchedule: updatedRule.snoozeSchedule, - })); - onRuleChanged(); - }, [http, ruleSnoozeInfo.id, onRuleChanged]); - - const openSnooze = useCallback(() => { - setCurrentlyOpenNotify(props.rule.id); - }, [props.rule.id]); - - const closeSnooze = useCallback(() => { - setCurrentlyOpenNotify(''); - }, []); - - const onLoading = useCallback((value: boolean) => { - if (value) { - setLoadingSnoozeAction(value); - } - }, []); - return ( ; - isOpen: boolean; - isLoading: boolean; - previousSnoozeInterval?: string | null; - onClick: React.MouseEventHandler; - onClose: () => void; - onLoading: (isLoading: boolean) => void; - onRuleChanged: () => void; + /** + * Rule's snooze settings + */ + snoozeSettings: RuleSnoozeSettings | undefined; + /** + * Displays the component in the loading state. If isLoading = false and snoozeSettings aren't set + * and the component is shown in disabled state. + */ + loading?: boolean; + /** + * Whether the component is disabled or not, string give a disabled reason displayed as a tooltip + */ + disabled?: boolean | string; + onRuleChanged: () => void | Promise; snoozeRule: (schedule: SnoozeSchedule, muteAll?: boolean) => Promise; unsnoozeRule: (scheduleIds?: string[]) => Promise; showTooltipInline?: boolean; @@ -27,5 +30,10 @@ export interface RulesListNotifyBadgeProps { export type RulesListNotifyBadgePropsWithApi = Pick< RulesListNotifyBadgeProps, - 'rule' | 'isLoading' | 'onRuleChanged' | 'showOnHover' | 'showTooltipInline' ->; + 'snoozeSettings' | 'loading' | 'disabled' | 'onRuleChanged' | 'showOnHover' | 'showTooltipInline' +> & { + /** + * Rule's SO id + */ + ruleId: string; +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_snooze/panel/index.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_snooze/panel/index.tsx index 25b97ec81bd91..9c1f1c844c699 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_snooze/panel/index.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_snooze/panel/index.tsx @@ -46,10 +46,12 @@ export const SnoozePanel: React.FC = ({ try { await snoozeRule(schedule); } finally { - setIsLoading(false); + if (!inPopover) { + setIsLoading(false); + } } }, - [setIsLoading, snoozeRule] + [inPopover, setIsLoading, snoozeRule] ); const onUnsnoozeRule = useCallback( @@ -58,10 +60,12 @@ export const SnoozePanel: React.FC = ({ try { await unsnoozeRule(scheduleIds); } finally { - setIsLoading(false); + if (!inPopover) { + setIsLoading(false); + } } }, - [setIsLoading, unsnoozeRule] + [inPopover, setIsLoading, unsnoozeRule] ); const saveSnoozeSchedule = useCallback( @@ -70,10 +74,12 @@ export const SnoozePanel: React.FC = ({ try { await snoozeRule(schedule); } finally { - setIsLoading(false); + if (!inPopover) { + setIsLoading(false); + } } }, - [snoozeRule, setIsLoading] + [inPopover, snoozeRule, setIsLoading] ); const cancelSnoozeSchedules = useCallback( @@ -82,10 +88,12 @@ export const SnoozePanel: React.FC = ({ try { await unsnoozeRule(scheduleIds); } finally { - setIsLoading(false); + if (!inPopover) { + setIsLoading(false); + } } }, - [unsnoozeRule, setIsLoading] + [inPopover, unsnoozeRule, setIsLoading] ); const onOpenScheduler = useCallback( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx index 5bb454c0c29c5..0548855629f48 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx @@ -847,7 +847,7 @@ export const RulesList = ({ itemIdToExpandedRowMap={itemIdToExpandedRowMap} onSort={setSort} onPage={setPage} - onRuleChanged={() => refreshRules()} + onRuleChanged={refreshRules} onRuleClick={(rule) => { const detailsRoute = ruleDetailsRoute ? ruleDetailsRoute : commonRuleDetailsRoute; history.push(detailsRoute.replace(`:ruleId`, rule.id)); @@ -883,7 +883,7 @@ export const RulesList = ({ key={rule.id} item={rule} onLoading={onLoading} - onRuleChanged={() => refreshRules()} + onRuleChanged={refreshRules} onDeleteRule={() => updateRulesToBulkEdit({ action: 'delete', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_table.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_table.tsx index a1aba92763b2d..34c1f83878ef6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_table.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_table.tsx @@ -208,7 +208,6 @@ export const RulesListTable = (props: RulesListTableProps) => { } = props; const [tagPopoverOpenIndex, setTagPopoverOpenIndex] = useState(-1); - const [currentlyOpenNotify, setCurrentlyOpenNotify] = useState(); const [isLoadingMap, setIsLoadingMap] = useState>({}); const isRuleUsingExecutionStatus = getIsExperimentalFeatureEnabled('ruleUseExecutionStatus'); @@ -485,12 +484,9 @@ export const RulesListTable = (props: RulesListTableProps) => { return ( onLoading(rule.id, newIsLoading)} - isOpen={currentlyOpenNotify === rule.id} - onClick={() => setCurrentlyOpenNotify(rule.id)} - onClose={() => setCurrentlyOpenNotify('')} + snoozeSettings={rule} + loading={!!isLoadingMap[rule.id]} + disabled={!rule.isEditable} onRuleChanged={onRuleChanged} snoozeRule={async (snoozeSchedule) => { await onSnoozeRule(rule, snoozeSchedule); @@ -769,7 +765,6 @@ export const RulesListTable = (props: RulesListTableProps) => { ]; }, [ config.minimumScheduleInterval, - currentlyOpenNotify, isLoadingMap, isRuleTypeEditableInContext, onRuleChanged, diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index 4c7d743cb3085..7f411ce1f6600 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -343,6 +343,11 @@ export type SanitizedRuleType = Omit; export type RuleUpdates = Omit; +export type RuleSnoozeSettings = Pick< + Rule, + 'activeSnoozes' | 'isSnoozedUntil' | 'muteAll' | 'snoozeSchedule' +>; + export interface RuleTableItem extends Rule { ruleType: RuleType['name']; index: number; @@ -350,7 +355,6 @@ export interface RuleTableItem extends Rule { isEditable: boolean; enabledInLicense: boolean; showIntervalWarning?: boolean; - activeSnoozes?: string[]; } export interface RuleTypeParamsExpressionProps< From 952489fa71f101ecb83e7cb8cea4581f8a57fb52 Mon Sep 17 00:00:00 2001 From: Tomasz Ciecierski Date: Mon, 8 May 2023 19:32:43 +0200 Subject: [PATCH 12/19] [Defend workflows] Osquery license check + display errors (#156738) --- .../osquery/common/translations/errors.ts | 23 +++++++ .../action_results/action_results_summary.tsx | 7 +- .../handlers/action/create_action_handler.ts | 67 +++++++++++-------- .../handlers/action/create_action_service.ts | 43 ++++++++++++ .../handlers/action/create_queries.test.ts | 7 +- .../server/handlers/action/create_queries.ts | 15 ++--- .../handlers/action/validate_license.ts | 24 +++++++ .../lib/osquery_app_context_services.ts | 2 + x-pack/plugins/osquery/server/plugin.ts | 17 +++-- .../live_query/create_live_query_route.ts | 2 +- x-pack/plugins/osquery/server/types.ts | 10 ++- x-pack/plugins/osquery/tsconfig.json | 1 + .../common/endpoint/constants.ts | 2 +- .../cypress/tasks/response_actions.ts | 12 +++- .../endpoint/endpoint_app_context_services.ts | 3 +- .../server/endpoint/routes/actions/list.ts | 27 ++++---- .../routes/actions/response_actions.test.ts | 6 +- .../endpoint/services/actions/create/index.ts | 20 ++++-- .../osquery_response_action.ts | 6 +- ...dule_notification_response_actions.test.ts | 11 +-- .../schedule_notification_response_actions.ts | 11 +-- .../security_solution/server/plugin.ts | 8 +-- .../translations/translations/fr-FR.json | 1 - .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 25 files changed, 223 insertions(+), 104 deletions(-) create mode 100644 x-pack/plugins/osquery/common/translations/errors.ts create mode 100644 x-pack/plugins/osquery/server/handlers/action/create_action_service.ts create mode 100644 x-pack/plugins/osquery/server/handlers/action/validate_license.ts diff --git a/x-pack/plugins/osquery/common/translations/errors.ts b/x-pack/plugins/osquery/common/translations/errors.ts new file mode 100644 index 0000000000000..ac67ab46032b1 --- /dev/null +++ b/x-pack/plugins/osquery/common/translations/errors.ts @@ -0,0 +1,23 @@ +/* + * 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 LICENSE_TOO_LOW = i18n.translate( + 'xpack.osquery.liveQueryActions.error.licenseTooLow', + { + defaultMessage: 'At least Platinum license is required to use Response Actions.', + } +); + +export const PARAMETER_NOT_FOUND = i18n.translate( + 'xpack.osquery.liveQueryActions.error.notFoundParameters', + { + defaultMessage: + "This query hasn't been called due to parameter used and its value not found in the alert.", + } +); diff --git a/x-pack/plugins/osquery/public/action_results/action_results_summary.tsx b/x-pack/plugins/osquery/public/action_results/action_results_summary.tsx index d8299dd672d35..9bdd9bc5a4528 100644 --- a/x-pack/plugins/osquery/public/action_results/action_results_summary.tsx +++ b/x-pack/plugins/osquery/public/action_results/action_results_summary.tsx @@ -59,12 +59,7 @@ const ActionResultsSummaryComponent: React.FC = ({ if (error) { edges.forEach((edge) => { if (edge.fields) { - edge.fields['error.skipped'] = edge.fields.error = [ - i18n.translate('xpack.osquery.liveQueryActionResults.table.skippedErrorText', { - defaultMessage: - "This query hasn't been called due to parameter used and its value not found in the alert.", - }), - ]; + edge.fields['error.skipped'] = edge.fields.error = [error]; } }); } else if (expired) { diff --git a/x-pack/plugins/osquery/server/handlers/action/create_action_handler.ts b/x-pack/plugins/osquery/server/handlers/action/create_action_handler.ts index 3c776723a2da2..169ac4291a9df 100644 --- a/x-pack/plugins/osquery/server/handlers/action/create_action_handler.ts +++ b/x-pack/plugins/osquery/server/handlers/action/create_action_handler.ts @@ -31,6 +31,7 @@ interface CreateActionHandlerOptions { soClient?: SavedObjectsClientContract; metadata?: Metadata; alertData?: ParsedTechnicalFields; + error?: string; } export const createActionHandler = async ( @@ -43,7 +44,7 @@ export const createActionHandler = async ( const internalSavedObjectsClient = await getInternalSavedObjectsClient( osqueryContext.getStartServices ); - const { soClient, metadata, alertData } = options; + const { soClient, metadata, alertData, error } = options; const savedObjectsClient = soClient ?? coreStartServices.savedObjects.createInternalRepository(); // eslint-disable-next-line @typescript-eslint/naming-convention @@ -98,6 +99,7 @@ export const createActionHandler = async ( action_id: uuidv4(), id: packQueryId, ...replacedQuery, + ...(error ? { error } : {}), ecs_mapping: packQuery.ecs_mapping, version: packQuery.version, platform: packQuery.platform, @@ -106,22 +108,31 @@ export const createActionHandler = async ( (value) => !isEmpty(value) ); }) - : await createDynamicQueries({ params, alertData, agents: selectedAgents, osqueryContext }), + : await createDynamicQueries({ + params, + alertData, + agents: selectedAgents, + osqueryContext, + error, + }), }; - const fleetActions = map( - filter(osqueryAction.queries, (query) => !query.error), - (query) => ({ - action_id: query.action_id, - '@timestamp': moment().toISOString(), - expiration: moment().add(5, 'minutes').toISOString(), - type: 'INPUT_ACTION', - input_type: 'osquery', - agents: query.agents, - user_id: metadata?.currentUser, - data: pick(query, ['id', 'query', 'ecs_mapping', 'version', 'platform']), - }) - ); + const fleetActions = !error + ? map( + filter(osqueryAction.queries, (query) => !query.error), + (query) => ({ + action_id: query.action_id, + '@timestamp': moment().toISOString(), + expiration: moment().add(5, 'minutes').toISOString(), + type: 'INPUT_ACTION', + input_type: 'osquery', + agents: query.agents, + user_id: metadata?.currentUser, + data: pick(query, ['id', 'query', 'ecs_mapping', 'version', 'platform']), + }) + ) + : []; + if (fleetActions.length) { await esClientInternal.bulk({ refresh: 'wait_for', @@ -129,24 +140,24 @@ export const createActionHandler = async ( fleetActions.map((action) => [{ index: { _index: AGENT_ACTIONS_INDEX } }, action]) ), }); + } - const actionsComponentTemplateExists = await esClientInternal.indices.exists({ - index: `${ACTIONS_INDEX}*`, - }); - - if (actionsComponentTemplateExists) { - await esClientInternal.bulk({ - refresh: 'wait_for', - body: [{ index: { _index: `${ACTIONS_INDEX}-default` } }, osqueryAction], - }); - } + const actionsComponentTemplateExists = await esClientInternal.indices.exists({ + index: `${ACTIONS_INDEX}*`, + }); - osqueryContext.telemetryEventsSender.reportEvent(TELEMETRY_EBT_LIVE_QUERY_EVENT, { - ...omit(osqueryAction, ['type', 'input_type', 'user_id']), - agents: osqueryAction.agents.length, + if (actionsComponentTemplateExists) { + await esClientInternal.bulk({ + refresh: 'wait_for', + body: [{ index: { _index: `${ACTIONS_INDEX}-default` } }, osqueryAction], }); } + osqueryContext.telemetryEventsSender.reportEvent(TELEMETRY_EBT_LIVE_QUERY_EVENT, { + ...omit(osqueryAction, ['type', 'input_type', 'user_id', 'error']), + agents: osqueryAction.agents.length, + }); + return { response: osqueryAction, fleetActionsCount: fleetActions.length, diff --git a/x-pack/plugins/osquery/server/handlers/action/create_action_service.ts b/x-pack/plugins/osquery/server/handlers/action/create_action_service.ts new file mode 100644 index 0000000000000..e0e8098fd16bd --- /dev/null +++ b/x-pack/plugins/osquery/server/handlers/action/create_action_service.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ParsedTechnicalFields } from '@kbn/rule-registry-plugin/common'; +import type { Subscription } from 'rxjs'; +import type { OsqueryAppContext } from '../../lib/osquery_app_context_services'; +import type { CreateLiveQueryRequestBodySchema } from '../../../common/schemas/routes/live_query'; +import type { OsqueryActiveLicenses } from './validate_license'; +import { validateLicense } from './validate_license'; +import { createActionHandler } from './create_action_handler'; + +export const createActionService = (osqueryContext: OsqueryAppContext) => { + let licenseSubscription: Subscription | null = null; + const licenses: OsqueryActiveLicenses = { isActivePlatinumLicense: false }; + + licenseSubscription = osqueryContext.licensing.license$.subscribe((license) => { + licenses.isActivePlatinumLicense = license.isActive && license.hasAtLeast('platinum'); + }); + + const create = async ( + params: CreateLiveQueryRequestBodySchema, + alertData?: ParsedTechnicalFields + ) => { + const error = validateLicense(licenses); + + return createActionHandler(osqueryContext, params, { alertData, error }); + }; + + const stop = () => { + if (licenseSubscription) { + licenseSubscription.unsubscribe(); + } + }; + + return { + create, + stop, + }; +}; diff --git a/x-pack/plugins/osquery/server/handlers/action/create_queries.test.ts b/x-pack/plugins/osquery/server/handlers/action/create_queries.test.ts index 4956dfd1245bc..29386c3db90db 100644 --- a/x-pack/plugins/osquery/server/handlers/action/create_queries.test.ts +++ b/x-pack/plugins/osquery/server/handlers/action/create_queries.test.ts @@ -5,9 +5,10 @@ * 2.0. */ -import { createDynamicQueries, PARAMETER_NOT_FOUND } from './create_queries'; +import { createDynamicQueries } from './create_queries'; import type { ParsedTechnicalFields } from '@kbn/rule-registry-plugin/common'; import type { OsqueryAppContext } from '../../lib/osquery_app_context_services'; +import { PARAMETER_NOT_FOUND } from '../../../common/translations/errors'; describe('create queries', () => { const defualtQueryParams = { @@ -65,9 +66,7 @@ describe('create queries', () => { expect(queries[0].query).toBe(`SELECT * FROM processes where pid=${pid};`); expect(queries[0].error).toBe(undefined); expect(queries[1].query).toBe('SELECT * FROM processes where pid={{process.not-existing}};'); - expect(queries[1].error).toBe( - "This query hasn't been called due to parameter used and its value not found in the alert." - ); + expect(queries[1].error).toBe(PARAMETER_NOT_FOUND); expect(queries[2].query).toBe('SELECT * FROM processes;'); expect(queries[2].error).toBe(undefined); }); diff --git a/x-pack/plugins/osquery/server/handlers/action/create_queries.ts b/x-pack/plugins/osquery/server/handlers/action/create_queries.ts index f7d3601722189..4185353fdacff 100644 --- a/x-pack/plugins/osquery/server/handlers/action/create_queries.ts +++ b/x-pack/plugins/osquery/server/handlers/action/create_queries.ts @@ -7,33 +7,28 @@ import { isEmpty, map, pickBy } from 'lodash'; import { v4 as uuidv4 } from 'uuid'; -import { i18n } from '@kbn/i18n'; import type { ParsedTechnicalFields } from '@kbn/rule-registry-plugin/common'; +import { PARAMETER_NOT_FOUND } from '../../../common/translations/errors'; import type { OsqueryAppContext } from '../../lib/osquery_app_context_services'; import type { CreateLiveQueryRequestBodySchema } from '../../../common/schemas/routes/live_query'; import { replaceParamsQuery } from '../../../common/utils/replace_params_query'; import { isSavedQueryPrebuilt } from '../../routes/saved_query/utils'; -export const PARAMETER_NOT_FOUND = i18n.translate( - 'xpack.osquery.liveQueryActions.error.notFoundParameters', - { - defaultMessage: - "This query hasn't been called due to parameter used and its value not found in the alert.", - } -); - interface CreateDynamicQueriesParams { params: CreateLiveQueryRequestBodySchema; alertData?: ParsedTechnicalFields; agents: string[]; osqueryContext: OsqueryAppContext; + error?: string; } + export const createDynamicQueries = async ({ params, alertData, agents, osqueryContext, + error, }: CreateDynamicQueriesParams) => params.queries?.length ? map(params.queries, ({ query, ...restQuery }) => { @@ -43,6 +38,7 @@ export const createDynamicQueries = async ({ { ...replacedQuery, ...restQuery, + ...(error ? { error } : {}), action_id: uuidv4(), alert_ids: params.alert_ids, agents, @@ -66,6 +62,7 @@ export const createDynamicQueries = async ({ ecs_mapping: params.ecs_mapping, alert_ids: params.alert_ids, agents, + ...(error ? { error } : {}), }, (value) => !isEmpty(value) ), diff --git a/x-pack/plugins/osquery/server/handlers/action/validate_license.ts b/x-pack/plugins/osquery/server/handlers/action/validate_license.ts new file mode 100644 index 0000000000000..063683198acbe --- /dev/null +++ b/x-pack/plugins/osquery/server/handlers/action/validate_license.ts @@ -0,0 +1,24 @@ +/* + * 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 { LICENSE_TOO_LOW } from '../../../common/translations/errors'; + +export interface OsqueryActiveLicenses { + isActivePlatinumLicense: boolean; +} + +export const validateLicense = (license?: OsqueryActiveLicenses) => { + if (!license) { + return; + } + + const { isActivePlatinumLicense } = license; + + if (!isActivePlatinumLicense) { + return LICENSE_TOO_LOW; + } +}; diff --git a/x-pack/plugins/osquery/server/lib/osquery_app_context_services.ts b/x-pack/plugins/osquery/server/lib/osquery_app_context_services.ts index 19b5b13495718..25d668c16d93a 100644 --- a/x-pack/plugins/osquery/server/lib/osquery_app_context_services.ts +++ b/x-pack/plugins/osquery/server/lib/osquery_app_context_services.ts @@ -15,6 +15,7 @@ import type { PackagePolicyClient, } from '@kbn/fleet-plugin/server'; import type { RuleRegistryPluginStartContract } from '@kbn/rule-registry-plugin/server'; +import type { LicensingPluginSetup } from '@kbn/licensing-plugin/server'; import type { ConfigType } from '../../common/config'; import type { TelemetryEventsSender } from './telemetry/sender'; @@ -82,6 +83,7 @@ export interface OsqueryAppContext { security: SecurityPluginStart; getStartServices: CoreSetup['getStartServices']; telemetryEventsSender: TelemetryEventsSender; + licensing: LicensingPluginSetup; /** * Object readiness is tied to plugin start method */ diff --git a/x-pack/plugins/osquery/server/plugin.ts b/x-pack/plugins/osquery/server/plugin.ts index 9c74329ef2a69..49152eeba5cc6 100644 --- a/x-pack/plugins/osquery/server/plugin.ts +++ b/x-pack/plugins/osquery/server/plugin.ts @@ -17,12 +17,11 @@ import type { DataRequestHandlerContext } from '@kbn/data-plugin/server'; import type { DataViewsService } from '@kbn/data-views-plugin/common'; import type { NewPackagePolicy, UpdatePackagePolicy } from '@kbn/fleet-plugin/common'; -import type { ParsedTechnicalFields } from '@kbn/rule-registry-plugin/common'; +import type { Subscription } from 'rxjs'; import { upgradeIntegration } from './utils/upgrade_integration'; import type { PackSavedObjectAttributes } from './common/types'; import { updateGlobalPacksCreateCallback } from './lib/update_global_packs'; import { packSavedObjectType } from '../common/types'; -import type { CreateLiveQueryRequestBodySchema } from '../common/schemas/routes/live_query'; import { createConfig } from './create_config'; import type { OsqueryPluginSetup, OsqueryPluginStart, SetupPlugins, StartPlugins } from './types'; import { defineRoutes } from './routes'; @@ -39,10 +38,10 @@ import { TelemetryReceiver } from './lib/telemetry/receiver'; import { initializeTransformsIndices } from './create_indices/create_transforms_indices'; import { initializeTransforms } from './create_transforms/create_transforms'; import { createDataViews } from './create_data_views'; -import { createActionHandler } from './handlers/action'; import { registerFeatures } from './utils/register_features'; import { CASE_ATTACHMENT_TYPE_ID } from '../common/constants'; +import { createActionService } from './handlers/action/create_action_service'; export class OsqueryPlugin implements Plugin { private readonly logger: Logger; @@ -50,6 +49,8 @@ export class OsqueryPlugin implements Plugin | null = null; constructor(private readonly initializerContext: PluginInitializerContext) { this.context = initializerContext; @@ -73,6 +74,7 @@ export class OsqueryPlugin implements Plugin config, security: plugins.security, telemetryEventsSender: this.telemetryEventsSender, + licensing: plugins.licensing, }; initSavedObjects(core.savedObjects); @@ -82,6 +84,8 @@ export class OsqueryPlugin implements Plugin { const osquerySearchStrategy = osquerySearchStrategyProvider( depsStart.data, @@ -97,10 +101,7 @@ export class OsqueryPlugin implements Plugin createActionHandler(osqueryContext, params, { alertData }), + createActionService: this.createActionService, }; } @@ -180,6 +181,8 @@ export class OsqueryPlugin implements Plugin { diff --git a/x-pack/plugins/osquery/server/routes/live_query/create_live_query_route.ts b/x-pack/plugins/osquery/server/routes/live_query/create_live_query_route.ts index 05f857e320066..d9bb8eb8efb25 100644 --- a/x-pack/plugins/osquery/server/routes/live_query/create_live_query_route.ts +++ b/x-pack/plugins/osquery/server/routes/live_query/create_live_query_route.ts @@ -11,7 +11,7 @@ import markdown from 'remark-parse-no-trim'; import { some, filter } from 'lodash'; import deepEqual from 'fast-deep-equal'; import type { ECSMappingOrUndefined } from '@kbn/osquery-io-ts-types'; -import { PARAMETER_NOT_FOUND } from '../../handlers/action/create_queries'; +import { PARAMETER_NOT_FOUND } from '../../../common/translations/errors'; import { replaceParamsQuery } from '../../../common/utils/replace_params_query'; import { createLiveQueryRequestBodySchema } from '../../../common/schemas/routes/live_query'; import type { CreateLiveQueryRequestBodySchema } from '../../../common/schemas/routes/live_query'; diff --git a/x-pack/plugins/osquery/server/types.ts b/x-pack/plugins/osquery/server/types.ts index c620d6035f547..7a6353cccee40 100644 --- a/x-pack/plugins/osquery/server/types.ts +++ b/x-pack/plugins/osquery/server/types.ts @@ -22,15 +22,12 @@ import type { } from '@kbn/task-manager-plugin/server'; import type { PluginStart as DataViewsPluginStart } from '@kbn/data-views-plugin/server'; import type { RuleRegistryPluginStartContract } from '@kbn/rule-registry-plugin/server'; -import type { ParsedTechnicalFields } from '@kbn/rule-registry-plugin/common'; import type { CasesSetup } from '@kbn/cases-plugin/server'; -import type { CreateLiveQueryRequestBodySchema } from '../common/schemas/routes/live_query'; +import type { LicensingPluginSetup } from '@kbn/licensing-plugin/server'; +import type { createActionService } from './handlers/action/create_action_service'; export interface OsqueryPluginSetup { - osqueryCreateAction: ( - payload: CreateLiveQueryRequestBodySchema, - alertData?: ParsedTechnicalFields - ) => void; + createActionService: ReturnType; } // eslint-disable-next-line @typescript-eslint/no-empty-interface @@ -45,6 +42,7 @@ export interface SetupPlugins { security: SecurityPluginStart; taskManager?: TaskManagerPluginSetup; telemetry?: TelemetryPluginSetup; + licensing: LicensingPluginSetup; } export interface StartPlugins { diff --git a/x-pack/plugins/osquery/tsconfig.json b/x-pack/plugins/osquery/tsconfig.json index 6d67d852b6073..c71fbc2871f94 100644 --- a/x-pack/plugins/osquery/tsconfig.json +++ b/x-pack/plugins/osquery/tsconfig.json @@ -71,6 +71,7 @@ "@kbn/safer-lodash-set", "@kbn/shared-ux-router", "@kbn/securitysolution-ecs", + "@kbn/licensing-plugin", "@kbn/core-saved-objects-server" ] } diff --git a/x-pack/plugins/security_solution/common/endpoint/constants.ts b/x-pack/plugins/security_solution/common/endpoint/constants.ts index 2d7c5269737aa..97510abec6ba8 100644 --- a/x-pack/plugins/security_solution/common/endpoint/constants.ts +++ b/x-pack/plugins/security_solution/common/endpoint/constants.ts @@ -73,7 +73,7 @@ export const UNISOLATE_HOST_ROUTE = `${BASE_ENDPOINT_ROUTE}/unisolate`; /** Base Actions route. Used to get a list of all actions and is root to other action related routes */ export const BASE_ENDPOINT_ACTION_ROUTE = `${BASE_ENDPOINT_ROUTE}/action`; -export const BASE_ENDPOINT_ACTION_ALERTS_ROUTE = `${BASE_ENDPOINT_ROUTE}/alerts`; +export const BASE_ENDPOINT_ACTION_ALERTS_ROUTE = `${BASE_ENDPOINT_ACTION_ROUTE}/alerts`; export const ISOLATE_HOST_ROUTE_V2 = `${BASE_ENDPOINT_ACTION_ROUTE}/isolate`; export const UNISOLATE_HOST_ROUTE_V2 = `${BASE_ENDPOINT_ACTION_ROUTE}/unisolate`; diff --git a/x-pack/plugins/security_solution/public/management/cypress/tasks/response_actions.ts b/x-pack/plugins/security_solution/public/management/cypress/tasks/response_actions.ts index 13829f8d3378c..54a33ac863722 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/tasks/response_actions.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/tasks/response_actions.ts @@ -49,7 +49,17 @@ export const fillUpNewRule = (name = 'Test', description = 'Test') => { }; export const visitRuleActions = (ruleId: string) => { cy.visit(`app/security/rules/id/${ruleId}/edit`); - cy.getByTestSubj('edit-rule-actions-tab').wait(500).click(); + cy.getByTestSubj('edit-rule-actions-tab').should('exist'); + // strange rerendering behaviour. the following make sure the test doesn't fail + cy.get('body').then(($body) => { + if ($body.find('[data-test-subj="globalLoadingIndicator"]').length) { + cy.getByTestSubj('globalLoadingIndicator').should('exist'); + cy.getByTestSubj('globalLoadingIndicator').should('not.exist'); + } + cy.getByTestSubj('globalLoadingIndicator').should('not.exist'); + }); + + cy.getByTestSubj('edit-rule-actions-tab').click(); }; export const tryAddingDisabledResponseAction = (itemNumber = 0) => { cy.getByTestSubj('response-actions-wrapper').within(() => { diff --git a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts index eb04fd133a55e..73bd035aa6496 100644 --- a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts +++ b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts @@ -37,6 +37,7 @@ import type { EndpointAuthz } from '../../common/endpoint/types/authz'; import { calculateEndpointAuthz } from '../../common/endpoint/service/authz'; import type { FeatureUsageService } from './services/feature_usage/service'; import type { ExperimentalFeatures } from '../../common/experimental_features'; +import type { ActionCreateService } from './services'; import { doesArtifactHaveData } from './services'; import type { actionCreateService } from './services/actions'; @@ -237,7 +238,7 @@ export class EndpointAppContextService { return this.startDependencies.messageSigningService; } - public getActionCreateService(): ReturnType { + public getActionCreateService(): ActionCreateService { if (!this.startDependencies?.actionCreateService) { throw new EndpointAppContentServicesNotStartedError(); } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/list.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/list.ts index 696fb9d654dbd..82ff69e4d4bda 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/list.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/list.ts @@ -41,17 +41,20 @@ export function registerActionListRoutes( actionListHandler(endpointContext) ) ); + // TODO: This route is a temporary solution until we decide on how RBAC should look like for Actions in Alerts - router.get( - { - path: BASE_ENDPOINT_ACTION_ALERTS_ROUTE, - validate: EndpointActionListRequestSchema, - options: { authRequired: true, tags: ['access:securitySolution'] }, - }, - withEndpointAuthz( - {}, - endpointContext.logFactory.get('endpointActionList'), - actionListHandler(endpointContext) - ) - ); + if (endpointContext.experimentalFeatures.endpointResponseActionsEnabled) { + router.get( + { + path: BASE_ENDPOINT_ACTION_ALERTS_ROUTE, + validate: EndpointActionListRequestSchema, + options: { authRequired: true, tags: ['access:securitySolution'] }, + }, + withEndpointAuthz( + {}, + endpointContext.logFactory.get('endpointActionList'), + actionListHandler(endpointContext) + ) + ); + } } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/response_actions.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/response_actions.test.ts index 696222eab1ed7..b2bc7ea008b10 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/response_actions.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/response_actions.test.ts @@ -132,11 +132,7 @@ describe('Response actions', () => { endpointAppContextService.setup(createMockEndpointAppContextServiceSetupContract()); endpointAppContextService.start({ ...startContract, - actionCreateService: actionCreateService( - mockScopedClient.asInternalUser, - endpointContext, - licenseService - ), + actionCreateService: actionCreateService(mockScopedClient.asInternalUser, endpointContext), licenseService, }); diff --git a/x-pack/plugins/security_solution/server/endpoint/services/actions/create/index.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions/create/index.ts index 1b8629c0ca1d9..8ab1ec7994381 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/actions/create/index.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/actions/create/index.ts @@ -68,12 +68,19 @@ interface CreateActionMetadata { enableActionsWithErrors?: boolean; } +export interface ActionCreateService { + createActionFromAlert: (payload: CreateActionPayload) => Promise; + createAction: ( + payload: CreateActionPayload, + metadata: CreateActionMetadata + ) => Promise; +} + export const actionCreateService = ( esClient: ElasticsearchClient, - endpointContext: EndpointAppContext, - licenseService: LicenseService -) => { - const createActionFromAlert = async (payload: CreateActionPayload) => { + endpointContext: EndpointAppContext +): ActionCreateService => { + const createActionFromAlert = async (payload: CreateActionPayload): Promise => { return createAction({ ...payload }, { minimumLicenseRequired: 'enterprise' }); }; @@ -86,6 +93,8 @@ export const actionCreateService = ( endpointContext.service.getFeatureUsageService().notifyUsage(featureKey); } + const licenseService = endpointContext.service.getLicenseService(); + const logger = endpointContext.logFactory.get('hostIsolation'); // fetch the Agent IDs to send the commands to @@ -341,11 +350,12 @@ interface CheckForAlertsArgs { licenseService: LicenseService; minimumLicenseRequired: LicenseType; } + const checkForAlertErrors = ({ agents, licenseService, minimumLicenseRequired = 'basic', -}: CheckForAlertsArgs) => { +}: CheckForAlertsArgs): string | undefined => { const licenseError = validateEndpointLicense(licenseService, minimumLicenseRequired); const agentsError = validateAgents(agents); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_response_actions/osquery_response_action.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_response_actions/osquery_response_action.ts index 3312c0b42b10f..d9ef5ed1574c0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_response_actions/osquery_response_action.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_response_actions/osquery_response_action.ts @@ -13,7 +13,7 @@ import type { AlertsWithAgentType } from './types'; export const osqueryResponseAction = ( responseAction: RuleResponseOsqueryAction, - osqueryCreateAction: SetupPlugins['osquery']['osqueryCreateAction'], + osqueryCreateActionService: SetupPlugins['osquery']['createActionService'], { alerts, alertIds, agentIds }: AlertsWithAgentType ) => { const temporaryQueries = responseAction.params.queries?.length @@ -27,7 +27,7 @@ export const osqueryResponseAction = ( const { savedQueryId, packId, queries, ecsMapping, ...rest } = responseAction.params; if (!containsDynamicQueries) { - return osqueryCreateAction({ + return osqueryCreateActionService.create({ ...rest, queries, ecs_mapping: ecsMapping, @@ -37,7 +37,7 @@ export const osqueryResponseAction = ( }); } each(alerts, (alert) => { - return osqueryCreateAction( + return osqueryCreateActionService.create( { ...rest, queries, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_response_actions/schedule_notification_response_actions.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_response_actions/schedule_notification_response_actions.test.ts index d41e3a374ba75..0c59d720c4af2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_response_actions/schedule_notification_response_actions.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_response_actions/schedule_notification_response_actions.test.ts @@ -53,11 +53,14 @@ describe('ScheduleNotificationResponseActions', () => { saved_query_id: undefined, ecs_mapping: { testField: { field: 'testField', value: 'testValue' } }, }; - const osqueryActionMock = jest.fn(); + const osqueryActionMock = { + create: jest.fn(), + stop: jest.fn(), + }; const endpointActionMock = jest.fn(); const scheduleNotificationResponseActions = getScheduleNotificationResponseActionsService({ - osqueryCreateAction: osqueryActionMock, + osqueryCreateActionService: osqueryActionMock, endpointAppContextService: endpointActionMock as never, }); @@ -74,7 +77,7 @@ describe('ScheduleNotificationResponseActions', () => { ]; scheduleNotificationResponseActions({ signals, responseActions }); - expect(osqueryActionMock).toHaveBeenCalledWith({ + expect(osqueryActionMock.create).toHaveBeenCalledWith({ ...defaultQueryResultParams, query: simpleQuery, }); @@ -99,7 +102,7 @@ describe('ScheduleNotificationResponseActions', () => { ]; scheduleNotificationResponseActions({ signals, responseActions }); - expect(osqueryActionMock).toHaveBeenCalledWith({ + expect(osqueryActionMock.create).toHaveBeenCalledWith({ ...defaultPackResultParams, queries: [{ ...defaultQueries, id: 'query-1', query: simpleQuery }], }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_response_actions/schedule_notification_response_actions.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_response_actions/schedule_notification_response_actions.ts index 7db003c3b331b..da1bdf7022c37 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_response_actions/schedule_notification_response_actions.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_response_actions/schedule_notification_response_actions.ts @@ -20,12 +20,12 @@ type Alerts = Array; interface ScheduleNotificationResponseActionsService { endpointAppContextService: EndpointAppContextService; - osqueryCreateAction: SetupPlugins['osquery']['osqueryCreateAction']; + osqueryCreateActionService?: SetupPlugins['osquery']['createActionService']; } export const getScheduleNotificationResponseActionsService = ({ - osqueryCreateAction, + osqueryCreateActionService, endpointAppContextService, }: ScheduleNotificationResponseActionsService) => ({ signals, responseActions }: ScheduleNotificationActions) => { @@ -48,8 +48,11 @@ export const getScheduleNotificationResponseActionsService = ); each(responseActions, (responseAction) => { - if (responseAction.actionTypeId === RESPONSE_ACTION_TYPES.OSQUERY && osqueryCreateAction) { - osqueryResponseAction(responseAction, osqueryCreateAction, { + if ( + responseAction.actionTypeId === RESPONSE_ACTION_TYPES.OSQUERY && + osqueryCreateActionService + ) { + osqueryResponseAction(responseAction, osqueryCreateActionService, { alerts, alertIds, agentIds, diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index ce979737a98bf..b25b58c0045c2 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -57,7 +57,7 @@ import { registerEndpointRoutes } from './endpoint/routes/metadata'; import { registerPolicyRoutes } from './endpoint/routes/policy'; import { registerActionRoutes } from './endpoint/routes/actions'; import { registerEndpointSuggestionsRoutes } from './endpoint/routes/suggestions'; -import { actionCreateService, EndpointArtifactClient, ManifestManager } from './endpoint/services'; +import { EndpointArtifactClient, ManifestManager } from './endpoint/services'; import { EndpointAppContextService } from './endpoint/endpoint_app_context_services'; import type { EndpointAppContext } from './endpoint/types'; import { initUsageCollectors } from './usage'; @@ -101,6 +101,7 @@ import type { } from './plugin_contract'; import { EndpointFleetServicesFactory } from './endpoint/services/fleet'; import { featureUsageService } from './endpoint/services/feature_usage'; +import { actionCreateService } from './endpoint/services/actions'; import { setIsElasticCloudDeployment } from './lib/telemetry/helpers'; import { artifactService } from './lib/telemetry/artifact'; import { endpointFieldsProvider } from './search_strategy/endpoint_fields'; @@ -246,7 +247,7 @@ export class Plugin implements ISecuritySolutionPlugin { const queryRuleAdditionalOptions: CreateQueryRuleAdditionalOptions = { scheduleNotificationResponseActionsService: getScheduleNotificationResponseActionsService({ endpointAppContextService: this.endpointAppContextService, - osqueryCreateAction: plugins.osquery.osqueryCreateAction, + osqueryCreateActionService: plugins.osquery.createActionService, }), }; @@ -505,8 +506,7 @@ export class Plugin implements ISecuritySolutionPlugin { messageSigningService: plugins.fleet?.messageSigningService, actionCreateService: actionCreateService( core.elasticsearch.client.asInternalUser, - this.endpointContext, - licenseService + this.endpointContext ), }); diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 533f04aab9796..4088039e32ccc 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -25807,7 +25807,6 @@ "xpack.osquery.liveQueryActionResults.table.expiredStatusText": "expiré", "xpack.osquery.liveQueryActionResults.table.pendingStatusText": "en attente", "xpack.osquery.liveQueryActionResults.table.resultRowsNumberColumnTitle": "Nombre de lignes de résultats", - "xpack.osquery.liveQueryActionResults.table.skippedErrorText": "Cette requête n'a pas été appelée en raison du paramètre utilisé et de sa valeur non trouvée dans l'alerte.", "xpack.osquery.liveQueryActionResults.table.skippedStatusText": "ignoré", "xpack.osquery.liveQueryActionResults.table.statusColumnTitle": "Statut", "xpack.osquery.liveQueryActionResults.table.successStatusText": "réussite", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 6aabae0e6ed5a..6e74ffc57d82d 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -25788,7 +25788,6 @@ "xpack.osquery.liveQueryActionResults.table.expiredStatusText": "期限切れ", "xpack.osquery.liveQueryActionResults.table.pendingStatusText": "保留中", "xpack.osquery.liveQueryActionResults.table.resultRowsNumberColumnTitle": "結果行数", - "xpack.osquery.liveQueryActionResults.table.skippedErrorText": "このクエリは、使用されているパラメーターとその値がアラートで見つからないため、呼び出されていません。", "xpack.osquery.liveQueryActionResults.table.skippedStatusText": "スキップ済み", "xpack.osquery.liveQueryActionResults.table.statusColumnTitle": "ステータス", "xpack.osquery.liveQueryActionResults.table.successStatusText": "成功", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 22fe6e5ab15df..0c770f0dc66fa 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -25805,7 +25805,6 @@ "xpack.osquery.liveQueryActionResults.table.expiredStatusText": "已过期", "xpack.osquery.liveQueryActionResults.table.pendingStatusText": "待处理", "xpack.osquery.liveQueryActionResults.table.resultRowsNumberColumnTitle": "结果行数", - "xpack.osquery.liveQueryActionResults.table.skippedErrorText": "尚未调用此查询,因为在告警中找不到所使用的参数及其值。", "xpack.osquery.liveQueryActionResults.table.skippedStatusText": "已跳过", "xpack.osquery.liveQueryActionResults.table.statusColumnTitle": "状态", "xpack.osquery.liveQueryActionResults.table.successStatusText": "成功", From 73d60085d11cd28b1eadefa63c2fcc1704336ef9 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau Date: Mon, 8 May 2023 13:44:13 -0400 Subject: [PATCH 13/19] [RAM] alert table support runtime field (#156899) ## Summary FIX https://github.com/elastic/kibana/issues/156263 & https://github.com/elastic/kibana/issues/155488 ### 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 --- .../common/search_strategy/index.ts | 2 ++ .../server/search_strategy/search_strategy.ts | 1 + .../components/alerts_table/index.tsx | 14 ++++++--- .../alerts_table/alerts_table_state.tsx | 4 +++ .../alerts_table/hooks/use_fetch_alerts.tsx | 7 ++++- .../tests/basic/search_strategy.ts | 31 +++++++++++++++++++ 6 files changed, 54 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/rule_registry/common/search_strategy/index.ts b/x-pack/plugins/rule_registry/common/search_strategy/index.ts index 213ab694eaa87..c38e5edc6d808 100644 --- a/x-pack/plugins/rule_registry/common/search_strategy/index.ts +++ b/x-pack/plugins/rule_registry/common/search_strategy/index.ts @@ -7,6 +7,7 @@ import { TechnicalRuleDataFieldName, ValidFeatureId } from '@kbn/rule-data-utils'; import { IEsSearchRequest, IEsSearchResponse } from '@kbn/data-plugin/common'; import type { + MappingRuntimeFields, QueryDslFieldAndFormat, QueryDslQueryContainer, SortCombinations, @@ -18,6 +19,7 @@ export type RuleRegistrySearchRequest = IEsSearchRequest & { query?: Pick; sort?: SortCombinations[]; pagination?: RuleRegistrySearchRequestPagination; + runtimeMappings?: MappingRuntimeFields; }; export interface RuleRegistrySearchRequestPagination { diff --git a/x-pack/plugins/rule_registry/server/search_strategy/search_strategy.ts b/x-pack/plugins/rule_registry/server/search_strategy/search_strategy.ts index e2c4e5d388758..d044e52154fd8 100644 --- a/x-pack/plugins/rule_registry/server/search_strategy/search_strategy.ts +++ b/x-pack/plugins/rule_registry/server/search_strategy/search_strategy.ts @@ -136,6 +136,7 @@ export const ruleRegistrySearchStrategyProvider = ( size, from: request.pagination ? request.pagination.pageIndex * size : 0, query, + ...(request.runtimeMappings ? { runtime_mappings: request.runtimeMappings } : {}), }, }; return (siemRequest ? requestUserEs : internalUserEs).search( diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx index aedb45acdf3ef..0b9acf427ee79 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx @@ -122,7 +122,11 @@ export const AlertsTableComponent: FC = ({ enableIpDetailsFlyout: true, onRuleChange, }); - const { browserFields, indexPattern: indexPatterns } = useSourcererDataView(sourcererScope); + const { + browserFields, + indexPattern: indexPatterns, + runtimeMappings, + } = useSourcererDataView(sourcererScope); const license = useLicense(); const getGlobalFiltersQuerySelector = useMemo( @@ -265,23 +269,25 @@ export const AlertsTableComponent: FC = ({ columns: finalColumns, browserFields: finalBrowserFields, onUpdate: onAlertTableUpdate, + runtimeMappings, toolbarVisibility: { showColumnSelector: !isEventRenderedView, showSortSelector: !isEventRenderedView, }, }), [ - finalBoolQuery, - configId, triggersActionsUi.alertsTableConfigurationRegistry, + configId, + tableView, flyoutSize, + finalBoolQuery, gridStyle, rowHeightsOptions, finalColumns, finalBrowserFields, onAlertTableUpdate, + runtimeMappings, isEventRenderedView, - tableView, ] ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table_state.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table_state.tsx index 44c560a732b7a..fa224b0e98b55 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table_state.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table_state.tsx @@ -16,6 +16,7 @@ import { EuiDataGridProps, EuiDataGridToolBarVisibilityOptions, } from '@elastic/eui'; +import type { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { ALERT_CASE_IDS } from '@kbn/rule-data-utils'; import type { ValidFeatureId } from '@kbn/rule-data-utils'; import type { @@ -68,6 +69,7 @@ export type AlertsTableStateProps = { showExpandToDetails: boolean; browserFields?: BrowserFields; onUpdate?: (args: TableUpdateHandlerArgs) => void; + runtimeMappings?: MappingRuntimeFields; showAlertStatusWithFlapping?: boolean; toolbarVisibility?: EuiDataGridToolBarVisibilityOptions; /** @@ -140,6 +142,7 @@ const AlertsTableStateWithQueryProvider = ({ gridStyle, browserFields: propBrowserFields, onUpdate, + runtimeMappings, showAlertStatusWithFlapping, toolbarVisibility, shouldHighlightRow, @@ -233,6 +236,7 @@ const AlertsTableStateWithQueryProvider = ({ featureIds, query, pagination, + runtimeMappings, sort, skip: false, }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_fetch_alerts.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_fetch_alerts.tsx index ab49469477ba6..b6d725fc9c39e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_fetch_alerts.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_fetch_alerts.tsx @@ -18,6 +18,7 @@ import type { RuleRegistrySearchResponse, } from '@kbn/rule-registry-plugin/common/search_strategy'; import type { + MappingRuntimeFields, QueryDslFieldAndFormat, QueryDslQueryContainer, SortCombinations, @@ -35,6 +36,7 @@ export interface FetchAlertsArgs { pageIndex: number; pageSize: number; }; + runtimeMappings?: MappingRuntimeFields; sort: SortCombinations[]; skip: boolean; } @@ -144,6 +146,7 @@ export type UseFetchAlerts = ({ fields, query, pagination, + runtimeMappings, skip, sort, }: FetchAlertsArgs) => [boolean, FetchAlertResp]; @@ -152,6 +155,7 @@ const useFetchAlerts = ({ fields, query, pagination, + runtimeMappings, skip, sort, }: FetchAlertsArgs): [boolean, FetchAlertResp] => { @@ -284,6 +288,7 @@ const useFetchAlerts = ({ fields, pagination, query, + runtimeMappings, sort, }; if ( @@ -295,7 +300,7 @@ const useFetchAlerts = ({ request: newAlertRequest, }); } - }, [featureIds, fields, pagination, query, sort]); + }, [featureIds, fields, pagination, query, sort, runtimeMappings]); useEffect(() => { if (alertRequest.featureIds.length > 0 && !deepEqual(alertRequest, prevAlertRequest.current)) { diff --git a/x-pack/test/rule_registry/security_and_spaces/tests/basic/search_strategy.ts b/x-pack/test/rule_registry/security_and_spaces/tests/basic/search_strategy.ts index 3698839861031..c64f7c78650d2 100644 --- a/x-pack/test/rule_registry/security_and_spaces/tests/basic/search_strategy.ts +++ b/x-pack/test/rule_registry/security_and_spaces/tests/basic/search_strategy.ts @@ -173,6 +173,37 @@ export default ({ getService }: FtrProviderContext) => { `The privateRuleRegistryAlertsSearchStrategy search strategy is unable to accommodate requests containing multiple feature IDs and one of those IDs is SIEM.` ); }); + + it('should be able to handle runtime fields on alerts from siem rules', async () => { + const runtimeFieldValue = 'hello world'; + const runtimeFieldKey = 'hello_world'; + const result = await secureBsearch.send({ + supertestWithoutAuth, + auth: { + username: obsOnlySpacesAllEsRead.username, + password: obsOnlySpacesAllEsRead.password, + }, + referer: 'test', + kibanaVersion, + options: { + featureIds: [AlertConsumers.SIEM], + runtimeMappings: { + [runtimeFieldKey]: { + type: 'keyword', + script: { + source: `emit('${runtimeFieldValue}')`, + }, + }, + }, + }, + strategy: 'privateRuleRegistryAlertsSearchStrategy', + }); + expect(result.rawResponse.hits.total).to.eql(1); + const runtimeFields = result.rawResponse.hits.hits.map( + (hit) => hit.fields?.[runtimeFieldKey] + ); + expect(runtimeFields.every((field) => field === runtimeFieldValue)); + }); }); describe('apm', () => { From 45afa6108b3e62b507afe2c984c81f3154da0df3 Mon Sep 17 00:00:00 2001 From: Karl Godard Date: Mon, 8 May 2023 11:32:47 -0700 Subject: [PATCH 14/19] [k8s dashboard] sessions table default columns updated (#156951) ## Summary Replaced entry user name with user id. added container ### Checklist Delete any items that are not applicable to this PR. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) --- .../public/kubernetes/pages/constants.ts | 24 +++++++------------ .../public/kubernetes/pages/translations.ts | 15 ++++-------- .../translations/translations/fr-FR.json | 1 - .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 5 files changed, 13 insertions(+), 29 deletions(-) diff --git a/x-pack/plugins/security_solution/public/kubernetes/pages/constants.ts b/x-pack/plugins/security_solution/public/kubernetes/pages/constants.ts index 2435d97c148bc..4acc5bf038e75 100644 --- a/x-pack/plugins/security_solution/public/kubernetes/pages/constants.ts +++ b/x-pack/plugins/security_solution/public/kubernetes/pages/constants.ts @@ -13,7 +13,6 @@ import { COLUMN_EXECUTABLE, COLUMN_ENTRY_USER, COLUMN_INTERACTIVE, - COLUMN_ENTRY_TYPE, COLUMN_NODE, COLUMN_CONTAINER, COLUMN_POD, @@ -26,11 +25,6 @@ export const kubernetesSessionsHeaders: ColumnHeaderOptions[] = [ initialWidth: DEFAULT_DATE_COLUMN_MIN_WIDTH, display: COLUMN_SESSION_START, }, - { - columnHeaderType: defaultColumnHeaderType, - id: 'process.entry_leader.user.name', - display: COLUMN_ENTRY_USER, - }, { columnHeaderType: defaultColumnHeaderType, id: 'process.entry_leader.executable', @@ -38,13 +32,8 @@ export const kubernetesSessionsHeaders: ColumnHeaderOptions[] = [ }, { columnHeaderType: defaultColumnHeaderType, - id: 'cloud.instance.name', - display: COLUMN_NODE, - }, - { - columnHeaderType: defaultColumnHeaderType, - id: 'process.entry_leader.entry_meta.type', - display: COLUMN_ENTRY_TYPE, + id: 'process.entry_leader.user.id', + display: COLUMN_ENTRY_USER, }, { columnHeaderType: defaultColumnHeaderType, @@ -53,12 +42,17 @@ export const kubernetesSessionsHeaders: ColumnHeaderOptions[] = [ }, { columnHeaderType: defaultColumnHeaderType, - id: 'container.name', - display: COLUMN_CONTAINER, + id: 'cloud.instance.name', + display: COLUMN_NODE, }, { columnHeaderType: defaultColumnHeaderType, id: 'orchestrator.resource.name', display: COLUMN_POD, }, + { + columnHeaderType: defaultColumnHeaderType, + id: 'container.name', + display: COLUMN_CONTAINER, + }, ]; diff --git a/x-pack/plugins/security_solution/public/kubernetes/pages/translations.ts b/x-pack/plugins/security_solution/public/kubernetes/pages/translations.ts index 9b1024494f4f0..0a190e0a93d05 100644 --- a/x-pack/plugins/security_solution/public/kubernetes/pages/translations.ts +++ b/x-pack/plugins/security_solution/public/kubernetes/pages/translations.ts @@ -10,14 +10,14 @@ import { i18n } from '@kbn/i18n'; export const COLUMN_SESSION_START = i18n.translate( 'xpack.securitySolution.kubernetes.columnSessionStart', { - defaultMessage: 'Date connected', + defaultMessage: 'Date started', } ); export const COLUMN_EXECUTABLE = i18n.translate( 'xpack.securitySolution.kubernetes.columnExecutable', { - defaultMessage: 'Session leader', + defaultMessage: 'Executable', } ); @@ -28,21 +28,14 @@ export const COLUMN_NODE = i18n.translate('xpack.securitySolution.kubernetes.col export const COLUMN_ENTRY_USER = i18n.translate( 'xpack.securitySolution.kubernetes.columnEntryUser', { - defaultMessage: 'Session entry user', + defaultMessage: 'User ID', } ); export const COLUMN_INTERACTIVE = i18n.translate( 'xpack.securitySolution.kubernetes.columnInteractive', { - defaultMessage: 'Interactivity', - } -); - -export const COLUMN_ENTRY_TYPE = i18n.translate( - 'xpack.securitySolution.kubernetes.columnEntryType', - { - defaultMessage: 'Entry type', + defaultMessage: 'Interactive', } ); diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 4088039e32ccc..3e59548b6f3c8 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -30969,7 +30969,6 @@ "xpack.securitySolution.kpiUsers.totalUsers.errorSearchDescription": "Une erreur s'est produite sur la recherche du KPI du total des utilisateurs", "xpack.securitySolution.kpiUsers.totalUsers.title": "Utilisateurs", "xpack.securitySolution.kubernetes.columnContainer": "Conteneur", - "xpack.securitySolution.kubernetes.columnEntryType": "Type d’entrée", "xpack.securitySolution.kubernetes.columnExecutable": "Leader de session", "xpack.securitySolution.kubernetes.columnInteractive": "Interactivité", "xpack.securitySolution.kubernetes.columnNode": "Nœud", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 6e74ffc57d82d..06d5e032cd802 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -30948,7 +30948,6 @@ "xpack.securitySolution.kpiUsers.totalUsers.errorSearchDescription": "統計ユーザーKPI検索でエラーが発生しました", "xpack.securitySolution.kpiUsers.totalUsers.title": "ユーザー", "xpack.securitySolution.kubernetes.columnContainer": "コンテナー", - "xpack.securitySolution.kubernetes.columnEntryType": "エントリタイプ", "xpack.securitySolution.kubernetes.columnExecutable": "セッションリーダー", "xpack.securitySolution.kubernetes.columnInteractive": "インタラクティブ", "xpack.securitySolution.kubernetes.columnNode": "ノード", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 0c770f0dc66fa..1b3c8f4fbe94e 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -30965,7 +30965,6 @@ "xpack.securitySolution.kpiUsers.totalUsers.errorSearchDescription": "搜索总体用户 KPI 时发生错误", "xpack.securitySolution.kpiUsers.totalUsers.title": "用户", "xpack.securitySolution.kubernetes.columnContainer": "容器", - "xpack.securitySolution.kubernetes.columnEntryType": "条目类型", "xpack.securitySolution.kubernetes.columnExecutable": "会话 Leader", "xpack.securitySolution.kubernetes.columnInteractive": "交互性", "xpack.securitySolution.kubernetes.columnNode": "节点", From d14ba925dd80f26c30447e04d605386bb4efc5e8 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Mon, 8 May 2023 11:50:32 -0700 Subject: [PATCH 15/19] [DOCS] Fix Slack connector open API specifications (#157026) --- .../connector-apis-passthru.asciidoc | 63 ++++--- .../plugins/actions/docs/openapi/bundled.json | 172 ++++++++++++++---- .../plugins/actions/docs/openapi/bundled.yaml | 109 +++++++---- .../connector_response_properties.yaml | 3 +- .../s@{spaceid}@api@actions@connector.yaml | 3 +- ...}@api@actions@connector@{connectorid}.yaml | 3 +- 6 files changed, 254 insertions(+), 99 deletions(-) diff --git a/docs/api-generated/connectors/connector-apis-passthru.asciidoc b/docs/api-generated/connectors/connector-apis-passthru.asciidoc index 5bd4a4bc44f45..385807de6a7d4 100644 --- a/docs/api-generated/connectors/connector-apis-passthru.asciidoc +++ b/docs/api-generated/connectors/connector-apis-passthru.asciidoc @@ -46,7 +46,9 @@ Any modifications made to this file will be overwritten.
spaceId (required)
-
Path Parameter — An identifier for the space. If /s/ and the identifier are omitted from the path, the default space is used. default: null
+
Path Parameter — An identifier for the space. If /s/ and the identifier are omitted from the path, the default space is used. default: null
id (required)
+ +
Path Parameter — An UUID v1 or v4 identifier for the connector. If you omit this parameter, an identifier is randomly generated. default: null

Consumes

@@ -970,7 +972,8 @@ Any modifications made to this file will be overwritten.
  • create_connector_request_servicenow - Create ServiceNow ITSM connector request
  • create_connector_request_servicenow_itom - Create ServiceNow ITOM connector request
  • create_connector_request_servicenow_sir - Create ServiceNow SecOps connector request
  • -
  • create_connector_request_slack - Create Slack connector request
  • +
  • create_connector_request_slack_api - Create Slack connector request
  • +
  • create_connector_request_slack_webhook - Create Slack connector request
  • create_connector_request_swimlane - Create Swimlane connector request
  • create_connector_request_teams - Create Microsoft Teams connector request
  • create_connector_request_tines - Create Tines connector request
  • @@ -1016,9 +1019,8 @@ Any modifications made to this file will be overwritten.
  • secrets_properties_opsgenie - Connector secrets properties for an Opsgenie connector
  • secrets_properties_resilient - Connector secrets properties for IBM Resilient connector
  • secrets_properties_servicenow - Connector secrets properties for ServiceNow ITOM, ServiceNow ITSM, and ServiceNow SecOps connectors
  • -
  • secrets_properties_slack - Connector secrets properties for a Slack connector
  • -
  • secrets_properties_slack_oneOf -
  • -
  • secrets_properties_slack_oneOf_1 -
  • +
  • secrets_properties_slack_api - Connector secrets properties for a Web API Slack connector
  • +
  • secrets_properties_slack_webhook - Connector secrets properties for a Webhook Slack connector
  • secrets_properties_swimlane - Connector secrets properties for a Swimlane connector
  • updateConnector_400_response -
  • update_connector_request_cases_webhook - Update Webhook - Case Managment connector request
  • @@ -1029,7 +1031,8 @@ Any modifications made to this file will be overwritten.
  • update_connector_request_serverlog - Update server log connector request
  • update_connector_request_servicenow - Update ServiceNow ITSM connector or ServiceNow SecOps request
  • update_connector_request_servicenow_itom - Create ServiceNow ITOM connector request
  • -
  • update_connector_request_slack - Update Slack connector request
  • +
  • update_connector_request_slack_api - Update Slack connector request
  • +
  • update_connector_request_slack_webhook - Update Slack connector request
  • update_connector_request_swimlane - Update Swimlane connector request
  • @@ -1785,14 +1788,25 @@ Any modifications made to this file will be overwritten.
    -

    create_connector_request_slack - Create Slack connector request Up

    +

    create_connector_request_slack_api - Create Slack connector request Up

    +
    The Slack connector uses Slack Incoming Webhooks.
    +
    +
    connector_type_id
    String The type of connector.
    +
    Enum:
    +
    .slack_api
    +
    name
    String The display name for the connector.
    +
    secrets
    +
    +
    +
    +

    create_connector_request_slack_webhook - Create Slack connector request Up

    The Slack connector uses Slack Incoming Webhooks.
    connector_type_id
    String The type of connector.
    Enum:
    .slack
    name
    String The display name for the connector.
    -
    secrets
    +
    secrets
    @@ -2240,25 +2254,17 @@ Any modifications made to this file will be overwritten.
    -

    secrets_properties_slack - Connector secrets properties for a Slack connector Up

    +

    secrets_properties_slack_api - Connector secrets properties for a Web API Slack connector Up

    Defines secrets for connectors when type is .slack.
    -
    token
    String The Slack bot user OAuth token.
    -
    webhookUrl
    String The Slack webhook url.
    +
    token
    String Slack bot user OAuth token.
    -

    secrets_properties_slack_oneOf - Up

    -
    -
    -
    token
    String The Slack bot user OAuth token.
    -
    -
    -
    -

    secrets_properties_slack_oneOf_1 - Up

    -
    +

    secrets_properties_slack_webhook - Connector secrets properties for a Webhook Slack connector Up

    +
    Defines secrets for connectors when type is .slack.
    -
    webhookUrl
    String The Slack webhook url.
    +
    webhookUrl
    String Slack webhook url.
    @@ -2347,14 +2353,19 @@ Any modifications made to this file will be overwritten.
    +
    diff --git a/x-pack/plugins/actions/docs/openapi/bundled.json b/x-pack/plugins/actions/docs/openapi/bundled.json index 524b09c965130..53f48394d26bd 100644 --- a/x-pack/plugins/actions/docs/openapi/bundled.json +++ b/x-pack/plugins/actions/docs/openapi/bundled.json @@ -39,6 +39,16 @@ }, { "$ref": "#/components/parameters/space_id" + }, + { + "in": "path", + "name": "id", + "description": "An UUID v1 or v4 identifier for the connector. If you omit this parameter, an identifier is randomly generated.\n", + "required": true, + "schema": { + "type": "string", + "example": "ac4e6b90-6be7-11eb-ba0d-9b1c1f912d74" + } } ], "requestBody": { @@ -83,7 +93,10 @@ "$ref": "#/components/schemas/create_connector_request_servicenow_sir" }, { - "$ref": "#/components/schemas/create_connector_request_slack" + "$ref": "#/components/schemas/create_connector_request_slack_api" + }, + { + "$ref": "#/components/schemas/create_connector_request_slack_webhook" }, { "$ref": "#/components/schemas/create_connector_request_swimlane" @@ -318,7 +331,10 @@ "$ref": "#/components/schemas/update_connector_request_servicenow_itom" }, { - "$ref": "#/components/schemas/update_connector_request_slack" + "$ref": "#/components/schemas/update_connector_request_slack_api" + }, + { + "$ref": "#/components/schemas/update_connector_request_slack_webhook" }, { "$ref": "#/components/schemas/update_connector_request_swimlane" @@ -1948,37 +1964,63 @@ } } }, - "secrets_properties_slack": { - "title": "Connector secrets properties for a Slack connector", + "secrets_properties_slack_api": { + "title": "Connector secrets properties for a Web API Slack connector", "description": "Defines secrets for connectors when type is `.slack`.", - "oneOf": [ - { - "type": "object", - "required": [ - "token" + "required": [ + "token" + ], + "type": "object", + "properties": { + "token": { + "type": "string", + "description": "Slack bot user OAuth token." + } + } + }, + "create_connector_request_slack_api": { + "title": "Create Slack connector request", + "description": "The Slack connector uses Slack Incoming Webhooks.", + "type": "object", + "required": [ + "connector_type_id", + "name", + "secrets" + ], + "properties": { + "connector_type_id": { + "type": "string", + "description": "The type of connector.", + "enum": [ + ".slack_api" ], - "properties": { - "token": { - "type": "string", - "description": "The Slack bot user OAuth token." - } - } + "example": ".slack_api" }, - { - "type": "object", - "required": [ - "webhookUrl" - ], - "properties": { - "webhookUrl": { - "type": "string", - "description": "The Slack webhook url." - } - } + "name": { + "type": "string", + "description": "The display name for the connector.", + "example": "my-connector" + }, + "secrets": { + "$ref": "#/components/schemas/secrets_properties_slack_api" } - ] + } }, - "create_connector_request_slack": { + "secrets_properties_slack_webhook": { + "title": "Connector secrets properties for a Webhook Slack connector", + "description": "Defines secrets for connectors when type is `.slack`.", + "required": [ + "webhookUrl" + ], + "type": "object", + "properties": { + "webhookUrl": { + "type": "string", + "description": "Slack webhook url." + } + } + }, + "create_connector_request_slack_webhook": { "title": "Create Slack connector request", "description": "The Slack connector uses Slack Incoming Webhooks.", "type": "object", @@ -2002,7 +2044,7 @@ "example": "my-connector" }, "secrets": { - "$ref": "#/components/schemas/secrets_properties_slack" + "$ref": "#/components/schemas/secrets_properties_slack_webhook" } } }, @@ -2921,7 +2963,44 @@ } } }, - "connector_response_properties_slack": { + "connector_response_properties_slack_api": { + "title": "Connector response properties for a Slack connector", + "type": "object", + "required": [ + "connector_type_id", + "id", + "is_deprecated", + "is_preconfigured", + "name" + ], + "properties": { + "connector_type_id": { + "type": "string", + "description": "The type of connector.", + "enum": [ + ".slack_api" + ] + }, + "id": { + "type": "string", + "description": "The identifier for the connector." + }, + "is_deprecated": { + "$ref": "#/components/schemas/is_deprecated" + }, + "is_missing_secrets": { + "$ref": "#/components/schemas/is_missing_secrets" + }, + "is_preconfigured": { + "$ref": "#/components/schemas/is_preconfigured" + }, + "name": { + "type": "string", + "description": "The display name for the connector." + } + } + }, + "connector_response_properties_slack_webhook": { "title": "Connector response properties for a Slack connector", "type": "object", "required": [ @@ -3197,7 +3276,10 @@ "$ref": "#/components/schemas/connector_response_properties_servicenow_sir" }, { - "$ref": "#/components/schemas/connector_response_properties_slack" + "$ref": "#/components/schemas/connector_response_properties_slack_api" + }, + { + "$ref": "#/components/schemas/connector_response_properties_slack_webhook" }, { "$ref": "#/components/schemas/connector_response_properties_swimlane" @@ -3375,7 +3457,7 @@ } } }, - "update_connector_request_slack": { + "update_connector_request_slack_api": { "title": "Update Slack connector request", "type": "object", "required": [ @@ -3387,15 +3469,29 @@ "type": "string", "description": "The display name for the connector." }, - "config": { + "secrets": { + "type": "object", + "description": "The secrets object containing the necessary fields for authentication.", + "$ref": "#/components/schemas/secrets_properties_slack_api" + } + } + }, + "update_connector_request_slack_webhook": { + "title": "Update Slack connector request", + "type": "object", + "required": [ + "name", + "secrets" + ], + "properties": { + "name": { "type": "string", - "enum": [ - "web_api", - "webhook" - ] + "description": "The display name for the connector." }, "secrets": { - "$ref": "#/components/schemas/secrets_properties_slack" + "type": "object", + "description": "The secrets object containing the necessary fields for authentication.", + "$ref": "#/components/schemas/secrets_properties_slack_webhook" } } }, diff --git a/x-pack/plugins/actions/docs/openapi/bundled.yaml b/x-pack/plugins/actions/docs/openapi/bundled.yaml index 2d02e0dd2b046..8cd264536b8e7 100644 --- a/x-pack/plugins/actions/docs/openapi/bundled.yaml +++ b/x-pack/plugins/actions/docs/openapi/bundled.yaml @@ -26,6 +26,14 @@ paths: parameters: - $ref: '#/components/parameters/kbn_xsrf' - $ref: '#/components/parameters/space_id' + - in: path + name: id + description: | + An UUID v1 or v4 identifier for the connector. If you omit this parameter, an identifier is randomly generated. + required: true + schema: + type: string + example: ac4e6b90-6be7-11eb-ba0d-9b1c1f912d74 requestBody: required: true content: @@ -45,7 +53,8 @@ paths: - $ref: '#/components/schemas/create_connector_request_servicenow' - $ref: '#/components/schemas/create_connector_request_servicenow_itom' - $ref: '#/components/schemas/create_connector_request_servicenow_sir' - - $ref: '#/components/schemas/create_connector_request_slack' + - $ref: '#/components/schemas/create_connector_request_slack_api' + - $ref: '#/components/schemas/create_connector_request_slack_webhook' - $ref: '#/components/schemas/create_connector_request_swimlane' - $ref: '#/components/schemas/create_connector_request_teams' - $ref: '#/components/schemas/create_connector_request_tines' @@ -174,7 +183,8 @@ paths: - $ref: '#/components/schemas/update_connector_request_serverlog' - $ref: '#/components/schemas/update_connector_request_servicenow' - $ref: '#/components/schemas/update_connector_request_servicenow_itom' - - $ref: '#/components/schemas/update_connector_request_slack' + - $ref: '#/components/schemas/update_connector_request_slack_api' + - $ref: '#/components/schemas/update_connector_request_slack_webhook' - $ref: '#/components/schemas/update_connector_request_swimlane' examples: updateIndexConnectorRequest: @@ -1298,25 +1308,48 @@ components: example: my-connector secrets: $ref: '#/components/schemas/secrets_properties_servicenow' - secrets_properties_slack: - title: Connector secrets properties for a Slack connector + secrets_properties_slack_api: + title: Connector secrets properties for a Web API Slack connector description: Defines secrets for connectors when type is `.slack`. - oneOf: - - type: object - required: - - token - properties: - token: - type: string - description: The Slack bot user OAuth token. - - type: object - required: - - webhookUrl - properties: - webhookUrl: - type: string - description: The Slack webhook url. - create_connector_request_slack: + required: + - token + type: object + properties: + token: + type: string + description: Slack bot user OAuth token. + create_connector_request_slack_api: + title: Create Slack connector request + description: The Slack connector uses Slack Incoming Webhooks. + type: object + required: + - connector_type_id + - name + - secrets + properties: + connector_type_id: + type: string + description: The type of connector. + enum: + - .slack_api + example: .slack_api + name: + type: string + description: The display name for the connector. + example: my-connector + secrets: + $ref: '#/components/schemas/secrets_properties_slack_api' + secrets_properties_slack_webhook: + title: Connector secrets properties for a Webhook Slack connector + description: Defines secrets for connectors when type is `.slack`. + required: + - webhookUrl + type: object + properties: + webhookUrl: + type: string + description: Slack webhook url. + create_connector_request_slack_webhook: title: Create Slack connector request description: The Slack connector uses Slack Incoming Webhooks. type: object @@ -1336,7 +1369,7 @@ components: description: The display name for the connector. example: my-connector secrets: - $ref: '#/components/schemas/secrets_properties_slack' + $ref: '#/components/schemas/secrets_properties_slack_webhook' config_properties_swimlane: title: Connector request properties for a Swimlane connector required: @@ -2024,7 +2057,7 @@ components: name: type: string description: The display name for the connector. - connector_response_properties_slack_webhook: + connector_response_properties_slack_api: title: Connector response properties for a Slack connector type: object required: @@ -2038,7 +2071,7 @@ components: type: string description: The type of connector. enum: - - .slack + - .slack_api id: type: string description: The identifier for the connector. @@ -2051,7 +2084,7 @@ components: name: type: string description: The display name for the connector. - connector_response_properties_slack_api: + connector_response_properties_slack_webhook: title: Connector response properties for a Slack connector type: object required: @@ -2065,7 +2098,7 @@ components: type: string description: The type of connector. enum: - - .slack_api + - .slack id: type: string description: The identifier for the connector. @@ -2240,7 +2273,8 @@ components: - $ref: '#/components/schemas/connector_response_properties_servicenow' - $ref: '#/components/schemas/connector_response_properties_servicenow_itom' - $ref: '#/components/schemas/connector_response_properties_servicenow_sir' - - $ref: '#/components/schemas/connector_response_properties_slack' + - $ref: '#/components/schemas/connector_response_properties_slack_api' + - $ref: '#/components/schemas/connector_response_properties_slack_webhook' - $ref: '#/components/schemas/connector_response_properties_swimlane' - $ref: '#/components/schemas/connector_response_properties_teams' - $ref: '#/components/schemas/connector_response_properties_tines' @@ -2359,7 +2393,7 @@ components: description: The display name for the connector. secrets: $ref: '#/components/schemas/secrets_properties_servicenow' - update_connector_request_slack: + update_connector_request_slack_api: title: Update Slack connector request type: object required: @@ -2369,13 +2403,24 @@ components: name: type: string description: The display name for the connector. - config: + secrets: + type: object + description: The secrets object containing the necessary fields for authentication. + $ref: '#/components/schemas/secrets_properties_slack_api' + update_connector_request_slack_webhook: + title: Update Slack connector request + type: object + required: + - name + - secrets + properties: + name: type: string - enum: - - web_api - - webhook + description: The display name for the connector. secrets: - $ref: '#/components/schemas/secrets_properties_slack' + type: object + description: The secrets object containing the necessary fields for authentication. + $ref: '#/components/schemas/secrets_properties_slack_webhook' update_connector_request_swimlane: title: Update Swimlane connector request type: object diff --git a/x-pack/plugins/actions/docs/openapi/components/schemas/connector_response_properties.yaml b/x-pack/plugins/actions/docs/openapi/components/schemas/connector_response_properties.yaml index b73584568df6b..ef72d88e31480 100644 --- a/x-pack/plugins/actions/docs/openapi/components/schemas/connector_response_properties.yaml +++ b/x-pack/plugins/actions/docs/openapi/components/schemas/connector_response_properties.yaml @@ -12,7 +12,8 @@ oneOf: - $ref: 'connector_response_properties_servicenow.yaml' - $ref: 'connector_response_properties_servicenow_itom.yaml' - $ref: 'connector_response_properties_servicenow_sir.yaml' - - $ref: 'connector_response_properties_slack.yaml' + - $ref: 'connector_response_properties_slack_api.yaml' + - $ref: 'connector_response_properties_slack_webhook.yaml' - $ref: 'connector_response_properties_swimlane.yaml' - $ref: 'connector_response_properties_teams.yaml' - $ref: 'connector_response_properties_tines.yaml' diff --git a/x-pack/plugins/actions/docs/openapi/paths/s@{spaceid}@api@actions@connector.yaml b/x-pack/plugins/actions/docs/openapi/paths/s@{spaceid}@api@actions@connector.yaml index b3d42c3f47484..24fddf1a8b972 100644 --- a/x-pack/plugins/actions/docs/openapi/paths/s@{spaceid}@api@actions@connector.yaml +++ b/x-pack/plugins/actions/docs/openapi/paths/s@{spaceid}@api@actions@connector.yaml @@ -36,7 +36,8 @@ post: - $ref: '../components/schemas/create_connector_request_servicenow.yaml' - $ref: '../components/schemas/create_connector_request_servicenow_itom.yaml' - $ref: '../components/schemas/create_connector_request_servicenow_sir.yaml' - - $ref: '../components/schemas/create_connector_request_slack.yaml' + - $ref: '../components/schemas/create_connector_request_slack_api.yaml' + - $ref: '../components/schemas/create_connector_request_slack_webhook.yaml' - $ref: '../components/schemas/create_connector_request_swimlane.yaml' - $ref: '../components/schemas/create_connector_request_teams.yaml' - $ref: '../components/schemas/create_connector_request_tines.yaml' diff --git a/x-pack/plugins/actions/docs/openapi/paths/s@{spaceid}@api@actions@connector@{connectorid}.yaml b/x-pack/plugins/actions/docs/openapi/paths/s@{spaceid}@api@actions@connector@{connectorid}.yaml index 3cee979ed3e91..a417a457a9448 100644 --- a/x-pack/plugins/actions/docs/openapi/paths/s@{spaceid}@api@actions@connector@{connectorid}.yaml +++ b/x-pack/plugins/actions/docs/openapi/paths/s@{spaceid}@api@actions@connector@{connectorid}.yaml @@ -104,7 +104,8 @@ put: - $ref: '../components/schemas/update_connector_request_serverlog.yaml' - $ref: '../components/schemas/update_connector_request_servicenow.yaml' - $ref: '../components/schemas/update_connector_request_servicenow_itom.yaml' - - $ref: '../components/schemas/update_connector_request_slack.yaml' + - $ref: '../components/schemas/update_connector_request_slack_api.yaml' + - $ref: '../components/schemas/update_connector_request_slack_webhook.yaml' - $ref: '../components/schemas/update_connector_request_swimlane.yaml' # - $ref: '../components/schemas/update_connector_request_teams.yaml' # - $ref: '../components/schemas/update_connector_request_tines.yaml' From 14b86951e5a950e20d068efafbd1ee4a43197258 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Mon, 8 May 2023 21:11:11 +0200 Subject: [PATCH 16/19] [Synthetics] Show data retention privileges prompt (#156681) --- packages/kbn-doc-links/src/get_doc_links.ts | 3 + packages/kbn-doc-links/src/types.ts | 3 + .../components/settings/data_retention.tsx | 84 +++++++++++++++++-- .../settings/hooks/use_get_ilm_policies.ts | 2 +- .../components/settings/policy_link.tsx | 42 +++++++++- 5 files changed, 125 insertions(+), 9 deletions(-) diff --git a/packages/kbn-doc-links/src/get_doc_links.ts b/packages/kbn-doc-links/src/get_doc_links.ts index 6ca707eec2998..7505ec280328a 100644 --- a/packages/kbn-doc-links/src/get_doc_links.ts +++ b/packages/kbn-doc-links/src/get_doc_links.ts @@ -740,5 +740,8 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => { appSearch: `${SEARCH_UI_DOCS}tutorials/app-search`, elasticsearch: `${SEARCH_UI_DOCS}tutorials/elasticsearch`, }, + synthetics: { + featureRoles: `${ELASTIC_WEBSITE_URL}guide/en/observability/${DOC_LINK_VERSION}/synthetics-feature-roles.html`, + }, }); }; diff --git a/packages/kbn-doc-links/src/types.ts b/packages/kbn-doc-links/src/types.ts index 38f0829ac3516..a2bc513c5fb1e 100644 --- a/packages/kbn-doc-links/src/types.ts +++ b/packages/kbn-doc-links/src/types.ts @@ -509,4 +509,7 @@ export interface DocLinks { readonly appSearch: string; readonly elasticsearch: string; }; + readonly synthetics: { + readonly featureRoles: string; + }; } diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/data_retention.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/data_retention.tsx index 8f998217117ed..851b64bbb7094 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/data_retention.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/data_retention.tsx @@ -6,14 +6,29 @@ */ import { FormattedMessage } from '@kbn/i18n-react'; -import { EuiBasicTable, EuiCallOut, EuiLink, EuiSpacer } from '@elastic/eui'; +import { + EuiBasicTable, + EuiCallOut, + EuiEmptyPrompt, + EuiIcon, + EuiLink, + EuiMarkdownFormat, + EuiSpacer, +} from '@elastic/eui'; import React from 'react'; import { i18n } from '@kbn/i18n'; -import { PolicyLink } from './policy_link'; +import { css } from '@emotion/react'; +import { IHttpFetchError, ResponseErrorBody } from '@kbn/core-http-browser'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { PolicyLink, PolicyNameLabel } from './policy_link'; import { useGetIlmPolicies } from './hooks/use_get_ilm_policies'; export const DataRetentionTab = () => { - const { data, loading } = useGetIlmPolicies(); + const { data, loading, error } = useGetIlmPolicies(); + + if (error && (error as unknown as IHttpFetchError).body?.statusCode === 403) { + return ; + } const columns = [ { @@ -36,9 +51,7 @@ export const DataRetentionTab = () => { }, { field: 'policy.name', - name: i18n.translate('xpack.synthetics.settingsRoute.table.policy', { - defaultMessage: 'Policy', - }), + name: , render: (name: string, _item: typeof data[0]) => , }, ]; @@ -76,6 +89,65 @@ export const DataRetentionTab = () => { ); }; +const Unprivileged = () => { + const { + services: { docLinks }, + } = useKibana(); + return ( + } + title={ +

    + +

    + } + body={ +

    + + {i18n.translate('xpack.synthetics.monitorManagement.projectDelete.docsLink', { + defaultMessage: 'Learn more', + })} + + ), + }} + /> +

    + } + footer={ + + } + /> + ); +}; + +const INDEX_PRIVILEGES = i18n.translate('xpack.synthetics.dataRetention.unprivileged.index', { + defaultMessage: '`read`, `monitor` on the following Elasticsearch indices: `synthetics-*`', +}); + +const CLUSTER_PRIVILEGES = i18n.translate('xpack.synthetics.dataRetention.unprivileged.cluster', { + defaultMessage: + '`read_ilm`, `monitor` to view and `manage_ilm` to manage ILM policies on the Elasticsearch cluster.', +}); + const CALLOUT_TITLE = i18n.translate('xpack.synthetics.settingsRoute.retentionCalloutTitle', { defaultMessage: 'Synthetics data is configured by managed index lifecycle policies', }); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/hooks/use_get_ilm_policies.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/hooks/use_get_ilm_policies.ts index bae80b567d339..0af2c3b0d4337 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/hooks/use_get_ilm_policies.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/hooks/use_get_ilm_policies.ts @@ -106,7 +106,7 @@ export const useGetIlmPolicies = () => { const formatAge = (age?: string) => { if (!age) { - return ''; + return '--'; } const [value] = age.split('d'); return i18n.translate('xpack.synthetics.settingsRoute.table.retentionPeriodValue', { diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/policy_link.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/policy_link.tsx index 62833e1519fe9..34bb2e40f645a 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/policy_link.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/policy_link.tsx @@ -6,15 +6,17 @@ */ import React from 'react'; -import { EuiLink, EuiLoadingContent } from '@elastic/eui'; +import { EuiIconTip, EuiLink, EuiLoadingContent, EuiToolTip, EuiText } from '@elastic/eui'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import { ILM_LOCATOR_ID } from '@kbn/index-lifecycle-management-plugin/public'; import { useFetcher } from '@kbn/observability-plugin/public'; +import { i18n } from '@kbn/i18n'; import { useSyntheticsSettingsContext } from '../../contexts'; import { ClientPluginsStart } from '../../../../plugin'; export const PolicyLink = ({ name }: { name: string }) => { - const { share } = useKibana().services; + const { share, application } = useKibana().services; + const canManageILM = application.capabilities.management?.data?.index_lifecycle_management; const ilmLocator = share.url.locators.get(ILM_LOCATOR_ID); @@ -28,6 +30,18 @@ export const PolicyLink = ({ name }: { name: string }) => { return ; } + if (!name) { + return <>--; + } + + if (!canManageILM) { + return ( + + {name} + + ); + } + return ( { ); }; + +export const PolicyNameLabel = () => { + const { application } = useKibana().services; + + const canManageILM = application.capabilities.management?.data?.index_lifecycle_management; + + if (canManageILM) { + return <>{POLICY_LABEL}; + } + + return ( + <> + {POLICY_LABEL} + + ); +}; + +const POLICY_LABEL = i18n.translate('xpack.synthetics.settingsRoute.table.policy', { + defaultMessage: 'Policy', +}); + +const PERMISSIONS_NEEDED = i18n.translate('xpack.synthetics.settingsRoute.policy.manageILM', { + defaultMessage: 'You need the "manage_ilm" cluster permission to manage ILM policies.', +}); From 8b45e76d8fa526b63de6de1529cfc593ecf8723a Mon Sep 17 00:00:00 2001 From: Angela Chuang <6295984+angorayc@users.noreply.github.com> Date: Mon, 8 May 2023 20:16:24 +0100 Subject: [PATCH 17/19] [SecuritySolution] Allow query when "Group By Top" field is empty (#156968) ## Summary Fixes https://github.com/elastic/kibana/issues/156923 https://user-images.githubusercontent.com/6295984/236834448-74dd8a03-3d0a-4171-aa30-513244999491.mov ### Checklist Delete any items that are not applicable to this PR. - [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 --- .../__snapshots__/alerts_table.test.ts.snap | 188 ++++++---------- .../common/alerts/alerts_table.test.ts | 57 ++++- .../common/alerts/alerts_table.ts | 200 ++++++++++-------- .../visualization_actions/translations.ts | 6 + .../components/visualization_actions/types.ts | 6 + .../use_lens_attributes.tsx | 6 +- .../alerts_count_panel/chart_content.tsx | 2 +- 7 files changed, 251 insertions(+), 214 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/__snapshots__/alerts_table.test.ts.snap b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/__snapshots__/alerts_table.test.ts.snap index c18b413cd2a57..6148e986fa5e2 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/__snapshots__/alerts_table.test.ts.snap +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/__snapshots__/alerts_table.test.ts.snap @@ -6,7 +6,7 @@ Object { "references": Array [ Object { "id": "security-solution-my-test", - "name": "indexpattern-datasource-layer-4aa7cf71-cf20-4e62-8ca6-ca6be6b0988b", + "name": "indexpattern-datasource-layer-mockLayerId", "type": "index-pattern", }, ], @@ -15,17 +15,28 @@ Object { "datasourceStates": Object { "formBased": Object { "layers": Object { - "4aa7cf71-cf20-4e62-8ca6-ca6be6b0988b": Object { + "mockLayerId": Object { "columnOrder": Array [ - "2881fedd-54b7-42ba-8c97-5175dec86166", - "75ce269b-ee9c-4c7d-a14e-9226ba0fe059", - "f04a71a3-399f-4d32-9efc-8a005e989991", + "mockTopValuesOfStackByFieldColumnId", + "mockTopValuesOfBreakdownFieldColumnId", + "mockCountColumnId", ], "columns": Object { - "2881fedd-54b7-42ba-8c97-5175dec86166": Object { + "mockCountColumnId": Object { + "dataType": "number", + "isBucketed": false, + "label": "Count of agent.type", + "operationType": "count", + "params": Object { + "emptyAsNull": true, + }, + "scale": "ratio", + "sourceField": "agent.type", + }, + "mockTopValuesOfBreakdownFieldColumnId": Object { "dataType": "string", "isBucketed": true, - "label": "Top values of event.category", + "label": "Top values of agent.type", "operationType": "terms", "params": Object { "exclude": Array [], @@ -34,7 +45,7 @@ Object { "includeIsRegex": false, "missingBucket": false, "orderBy": Object { - "columnId": "f04a71a3-399f-4d32-9efc-8a005e989991", + "columnId": "mockCountColumnId", "type": "column", }, "orderDirection": "desc", @@ -45,12 +56,12 @@ Object { "size": 1000, }, "scale": "ordinal", - "sourceField": "event.category", + "sourceField": "agent.type", }, - "75ce269b-ee9c-4c7d-a14e-9226ba0fe059": Object { + "mockTopValuesOfStackByFieldColumnId": Object { "dataType": "string", "isBucketed": true, - "label": "Top values of agent.type", + "label": "Top values of event.category", "operationType": "terms", "params": Object { "exclude": Array [], @@ -59,7 +70,7 @@ Object { "includeIsRegex": false, "missingBucket": false, "orderBy": Object { - "columnId": "f04a71a3-399f-4d32-9efc-8a005e989991", + "columnId": "mockCountColumnId", "type": "column", }, "orderDirection": "desc", @@ -70,18 +81,7 @@ Object { "size": 1000, }, "scale": "ordinal", - "sourceField": "agent.type", - }, - "f04a71a3-399f-4d32-9efc-8a005e989991": Object { - "dataType": "number", - "isBucketed": false, - "label": "Count of agent.type", - "operationType": "count", - "params": Object { - "emptyAsNull": true, - }, - "scale": "ratio", - "sourceField": "agent.type", + "sourceField": "event.category", }, }, "incompleteColumns": Object {}, @@ -144,20 +144,20 @@ Object { "visualization": Object { "columns": Array [ Object { - "columnId": "2881fedd-54b7-42ba-8c97-5175dec86166", + "columnId": "mockTopValuesOfStackByFieldColumnId", "isTransposed": false, "width": 362, }, Object { - "columnId": "f04a71a3-399f-4d32-9efc-8a005e989991", + "columnId": "mockCountColumnId", "isTransposed": false, }, Object { - "columnId": "75ce269b-ee9c-4c7d-a14e-9226ba0fe059", + "columnId": "mockTopValuesOfBreakdownFieldColumnId", "isTransposed": false, }, ], - "layerId": "4aa7cf71-cf20-4e62-8ca6-ca6be6b0988b", + "layerId": "mockLayerId", "layerType": "data", }, }, @@ -172,7 +172,7 @@ Object { "references": Array [ Object { "id": "security-solution-my-test", - "name": "indexpattern-datasource-layer-4aa7cf71-cf20-4e62-8ca6-ca6be6b0988b", + "name": "indexpattern-datasource-layer-mockLayerId", "type": "index-pattern", }, ], @@ -181,42 +181,27 @@ Object { "datasourceStates": Object { "formBased": Object { "layers": Object { - "4aa7cf71-cf20-4e62-8ca6-ca6be6b0988b": Object { + "mockLayerId": Object { "columnOrder": Array [ - "2881fedd-54b7-42ba-8c97-5175dec86166", - "75ce269b-ee9c-4c7d-a14e-9226ba0fe059", - "f04a71a3-399f-4d32-9efc-8a005e989991", + "mockTopValuesOfStackByFieldColumnId", + "mockCountColumnId", ], "columns": Object { - "2881fedd-54b7-42ba-8c97-5175dec86166": Object { - "dataType": "string", - "isBucketed": true, - "label": "Top values of event.category", - "operationType": "terms", + "mockCountColumnId": Object { + "dataType": "number", + "isBucketed": false, + "label": "Count of event.category", + "operationType": "count", "params": Object { - "exclude": Array [], - "excludeIsRegex": false, - "include": Array [], - "includeIsRegex": false, - "missingBucket": false, - "orderBy": Object { - "columnId": "f04a71a3-399f-4d32-9efc-8a005e989991", - "type": "column", - }, - "orderDirection": "desc", - "otherBucket": true, - "parentFormat": Object { - "id": "terms", - }, - "size": 1000, + "emptyAsNull": true, }, - "scale": "ordinal", + "scale": "ratio", "sourceField": "event.category", }, - "75ce269b-ee9c-4c7d-a14e-9226ba0fe059": Object { + "mockTopValuesOfStackByFieldColumnId": Object { "dataType": "string", "isBucketed": true, - "label": "Top values of undefined", + "label": "Top values of event.category", "operationType": "terms", "params": Object { "exclude": Array [], @@ -225,7 +210,7 @@ Object { "includeIsRegex": false, "missingBucket": false, "orderBy": Object { - "columnId": "f04a71a3-399f-4d32-9efc-8a005e989991", + "columnId": "mockCountColumnId", "type": "column", }, "orderDirection": "desc", @@ -236,18 +221,7 @@ Object { "size": 1000, }, "scale": "ordinal", - "sourceField": undefined, - }, - "f04a71a3-399f-4d32-9efc-8a005e989991": Object { - "dataType": "number", - "isBucketed": false, - "label": "Count of undefined", - "operationType": "count", - "params": Object { - "emptyAsNull": true, - }, - "scale": "ratio", - "sourceField": undefined, + "sourceField": "event.category", }, }, "incompleteColumns": Object {}, @@ -334,20 +308,16 @@ Object { "visualization": Object { "columns": Array [ Object { - "columnId": "2881fedd-54b7-42ba-8c97-5175dec86166", + "columnId": "mockTopValuesOfStackByFieldColumnId", "isTransposed": false, "width": 362, }, Object { - "columnId": "f04a71a3-399f-4d32-9efc-8a005e989991", - "isTransposed": false, - }, - Object { - "columnId": "75ce269b-ee9c-4c7d-a14e-9226ba0fe059", + "columnId": "mockCountColumnId", "isTransposed": false, }, ], - "layerId": "4aa7cf71-cf20-4e62-8ca6-ca6be6b0988b", + "layerId": "mockLayerId", "layerType": "data", }, }, @@ -362,7 +332,7 @@ Object { "references": Array [ Object { "id": "security-solution-my-test", - "name": "indexpattern-datasource-layer-4aa7cf71-cf20-4e62-8ca6-ca6be6b0988b", + "name": "indexpattern-datasource-layer-mockLayerId", "type": "index-pattern", }, ], @@ -371,42 +341,27 @@ Object { "datasourceStates": Object { "formBased": Object { "layers": Object { - "4aa7cf71-cf20-4e62-8ca6-ca6be6b0988b": Object { + "mockLayerId": Object { "columnOrder": Array [ - "2881fedd-54b7-42ba-8c97-5175dec86166", - "75ce269b-ee9c-4c7d-a14e-9226ba0fe059", - "f04a71a3-399f-4d32-9efc-8a005e989991", + "mockTopValuesOfStackByFieldColumnId", + "mockCountColumnId", ], "columns": Object { - "2881fedd-54b7-42ba-8c97-5175dec86166": Object { - "dataType": "string", - "isBucketed": true, - "label": "Top values of event.category", - "operationType": "terms", + "mockCountColumnId": Object { + "dataType": "number", + "isBucketed": false, + "label": "Count of event.category", + "operationType": "count", "params": Object { - "exclude": Array [], - "excludeIsRegex": false, - "include": Array [], - "includeIsRegex": false, - "missingBucket": false, - "orderBy": Object { - "columnId": "f04a71a3-399f-4d32-9efc-8a005e989991", - "type": "column", - }, - "orderDirection": "desc", - "otherBucket": true, - "parentFormat": Object { - "id": "terms", - }, - "size": 1000, + "emptyAsNull": true, }, - "scale": "ordinal", + "scale": "ratio", "sourceField": "event.category", }, - "75ce269b-ee9c-4c7d-a14e-9226ba0fe059": Object { + "mockTopValuesOfStackByFieldColumnId": Object { "dataType": "string", "isBucketed": true, - "label": "Top values of undefined", + "label": "Top values of event.category", "operationType": "terms", "params": Object { "exclude": Array [], @@ -415,7 +370,7 @@ Object { "includeIsRegex": false, "missingBucket": false, "orderBy": Object { - "columnId": "f04a71a3-399f-4d32-9efc-8a005e989991", + "columnId": "mockCountColumnId", "type": "column", }, "orderDirection": "desc", @@ -426,18 +381,7 @@ Object { "size": 1000, }, "scale": "ordinal", - "sourceField": undefined, - }, - "f04a71a3-399f-4d32-9efc-8a005e989991": Object { - "dataType": "number", - "isBucketed": false, - "label": "Count of undefined", - "operationType": "count", - "params": Object { - "emptyAsNull": true, - }, - "scale": "ratio", - "sourceField": undefined, + "sourceField": "event.category", }, }, "incompleteColumns": Object {}, @@ -500,20 +444,16 @@ Object { "visualization": Object { "columns": Array [ Object { - "columnId": "2881fedd-54b7-42ba-8c97-5175dec86166", + "columnId": "mockTopValuesOfStackByFieldColumnId", "isTransposed": false, "width": 362, }, Object { - "columnId": "f04a71a3-399f-4d32-9efc-8a005e989991", - "isTransposed": false, - }, - Object { - "columnId": "75ce269b-ee9c-4c7d-a14e-9226ba0fe059", + "columnId": "mockCountColumnId", "isTransposed": false, }, ], - "layerId": "4aa7cf71-cf20-4e62-8ca6-ca6be6b0988b", + "layerId": "mockLayerId", "layerType": "data", }, }, diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_table.test.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_table.test.ts index e8457e8dfb533..82303e529db31 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_table.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_table.test.ts @@ -11,8 +11,20 @@ import { useLensAttributes } from '../../../use_lens_attributes'; import { getAlertsTableLensAttributes } from './alerts_table'; +interface VisualizationState { + visualization: { columns: {} }; + datasourceStates: { + formBased: { layers: Record }; + }; +} + jest.mock('uuid', () => ({ - v4: jest.fn().mockReturnValue('4aa7cf71-cf20-4e62-8ca6-ca6be6b0988b'), + v4: jest + .fn() + .mockReturnValueOnce('mockLayerId') + .mockReturnValueOnce('mockTopValuesOfStackByFieldColumnId') + .mockReturnValueOnce('mockCountColumnId') + .mockReturnValueOnce('mockTopValuesOfBreakdownFieldColumnId'), })); jest.mock('../../../../../containers/sourcerer', () => ({ @@ -95,6 +107,49 @@ describe('getAlertsTableLensAttributes', () => { { wrapper } ); + const state = result?.current?.state as VisualizationState; expect(result?.current).toMatchSnapshot(); + + expect(state.datasourceStates.formBased.layers.mockLayerId.columnOrder).toMatchInlineSnapshot(` + Array [ + "mockTopValuesOfStackByFieldColumnId", + "mockTopValuesOfBreakdownFieldColumnId", + "mockCountColumnId", + ] + `); + }); + + it('should render Without extra options - breakdownField', () => { + const { result } = renderHook( + () => + useLensAttributes({ + extraOptions: { breakdownField: '' }, + getLensAttributes: getAlertsTableLensAttributes, + stackByField: 'event.category', + }), + { wrapper } + ); + + const state = result?.current?.state as VisualizationState; + expect(state.visualization?.columns).toMatchInlineSnapshot(` + Array [ + Object { + "columnId": "mockTopValuesOfStackByFieldColumnId", + "isTransposed": false, + "width": 362, + }, + Object { + "columnId": "mockCountColumnId", + "isTransposed": false, + }, + ] + `); + + expect(state.datasourceStates.formBased.layers.mockLayerId.columnOrder).toMatchInlineSnapshot(` + Array [ + "mockTopValuesOfStackByFieldColumnId", + "mockCountColumnId", + ] + `); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_table.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_table.ts index 678179855557c..9358bcc5db810 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_table.ts +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_table.ts @@ -5,35 +5,127 @@ * 2.0. */ import { v4 as uuidv4 } from 'uuid'; -import type { GetLensAttributes } from '../../../types'; + +import { isEmpty } from 'lodash'; +import type { GetLensAttributes, LensEmbeddableDataTableColumn } from '../../../types'; +import { COUNT_OF, TOP_VALUE } from '../../../translations'; const layerId = uuidv4(); +const topValuesOfStackByFieldColumnId = uuidv4(); +const countColumnId = uuidv4(); +const topValuesOfBreakdownFieldColumnId = uuidv4(); +const defaultColumns = [ + { + columnId: topValuesOfStackByFieldColumnId, + isTransposed: false, + width: 362, + }, + { + columnId: countColumnId, + isTransposed: false, + }, +]; +const breakdownFieldColumns = [ + { + columnId: topValuesOfBreakdownFieldColumnId, + isTransposed: false, + }, +]; +const defaultColumnOrder = [topValuesOfStackByFieldColumnId]; +const getTopValuesOfBreakdownFieldColumnSettings = ( + breakdownField: string +): Record => ({ + [topValuesOfBreakdownFieldColumnId]: { + label: TOP_VALUE(breakdownField), + dataType: 'string', + operationType: 'terms', + scale: 'ordinal', + sourceField: breakdownField, + isBucketed: true, + params: { + size: 1000, + orderBy: { + type: 'column', + columnId: countColumnId, + }, + orderDirection: 'desc', + otherBucket: true, + missingBucket: false, + parentFormat: { + id: 'terms', + }, + include: [], + exclude: [], + includeIsRegex: false, + excludeIsRegex: false, + }, + }, +}); export const getAlertsTableLensAttributes: GetLensAttributes = ( stackByField = 'kibana.alert.rule.name', extraOptions ) => { + const breakdownFieldProvided = !isEmpty(extraOptions?.breakdownField); + const countField = + extraOptions?.breakdownField && breakdownFieldProvided + ? extraOptions?.breakdownField + : stackByField; + const columnOrder = breakdownFieldProvided + ? [...defaultColumnOrder, topValuesOfBreakdownFieldColumnId, countColumnId] + : [...defaultColumnOrder, countColumnId]; + + const columnSettings: Record = { + [topValuesOfStackByFieldColumnId]: { + label: TOP_VALUE(stackByField), + dataType: 'string', + operationType: 'terms', + scale: 'ordinal', + sourceField: stackByField, + isBucketed: true, + params: { + size: 1000, + orderBy: { + type: 'column', + columnId: countColumnId, + }, + orderDirection: 'desc', + otherBucket: true, + missingBucket: false, + parentFormat: { + id: 'terms', + }, + include: [], + exclude: [], + includeIsRegex: false, + excludeIsRegex: false, + }, + }, + [countColumnId]: { + label: COUNT_OF(countField), + dataType: 'number', + operationType: 'count', + isBucketed: false, + scale: 'ratio', + sourceField: countField, + params: { + emptyAsNull: true, + }, + }, + ...(extraOptions?.breakdownField && breakdownFieldProvided + ? getTopValuesOfBreakdownFieldColumnSettings(extraOptions?.breakdownField) + : {}), + }; + return { title: 'Alerts', description: '', visualizationType: 'lnsDatatable', state: { visualization: { - columns: [ - { - columnId: '2881fedd-54b7-42ba-8c97-5175dec86166', - isTransposed: false, - width: 362, - }, - { - columnId: 'f04a71a3-399f-4d32-9efc-8a005e989991', - isTransposed: false, - }, - { - columnId: '75ce269b-ee9c-4c7d-a14e-9226ba0fe059', - isTransposed: false, - }, - ], + columns: breakdownFieldProvided + ? [...defaultColumns, ...breakdownFieldColumns] + : defaultColumns, layerId, layerType: 'data', }, @@ -46,74 +138,16 @@ export const getAlertsTableLensAttributes: GetLensAttributes = ( formBased: { layers: { [layerId]: { - columns: { - '2881fedd-54b7-42ba-8c97-5175dec86166': { - label: `Top values of ${stackByField}`, - dataType: 'string', - operationType: 'terms', - scale: 'ordinal', - sourceField: stackByField, - isBucketed: true, - params: { - size: 1000, - orderBy: { - type: 'column', - columnId: 'f04a71a3-399f-4d32-9efc-8a005e989991', - }, - orderDirection: 'desc', - otherBucket: true, - missingBucket: false, - parentFormat: { - id: 'terms', - }, - include: [], - exclude: [], - includeIsRegex: false, - excludeIsRegex: false, - }, - }, - 'f04a71a3-399f-4d32-9efc-8a005e989991': { - label: `Count of ${extraOptions?.breakdownField}`, - dataType: 'number', - operationType: 'count', - isBucketed: false, - scale: 'ratio', - sourceField: extraOptions?.breakdownField, - params: { - emptyAsNull: true, - }, - }, - '75ce269b-ee9c-4c7d-a14e-9226ba0fe059': { - label: `Top values of ${extraOptions?.breakdownField}`, - dataType: 'string', - operationType: 'terms', - scale: 'ordinal', - sourceField: extraOptions?.breakdownField, - isBucketed: true, - params: { - size: 1000, - orderBy: { - type: 'column', - columnId: 'f04a71a3-399f-4d32-9efc-8a005e989991', - }, - orderDirection: 'desc', - otherBucket: true, - missingBucket: false, - parentFormat: { - id: 'terms', - }, - include: [], - exclude: [], - includeIsRegex: false, - excludeIsRegex: false, - }, + columns: columnOrder.reduce>( + (acc, colId) => { + if (colId && columnSettings[colId]) { + acc[colId] = columnSettings[colId]; + } + return acc; }, - }, - columnOrder: [ - '2881fedd-54b7-42ba-8c97-5175dec86166', - '75ce269b-ee9c-4c7d-a14e-9226ba0fe059', - 'f04a71a3-399f-4d32-9efc-8a005e989991', - ], + {} + ), + columnOrder, sampling: 1, incompleteColumns: {}, }, diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/translations.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/translations.ts index 4dcceb29323b1..8f21d61bc869b 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/translations.ts @@ -105,3 +105,9 @@ export const TOP_VALUE = (field: string) => export const COUNT = i18n.translate('xpack.securitySolution.visualizationActions.countLabel', { defaultMessage: 'Count of records', }); + +export const COUNT_OF = (field: string) => + i18n.translate('xpack.securitySolution.visualizationActions.countOfFieldLabel', { + values: { field }, + defaultMessage: 'Count of {field}', + }); diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/types.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/types.ts index 71c7440d5f1e1..dd2c7f7cd6094 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/types.ts @@ -7,6 +7,7 @@ import type { DatatableVisualizationState, + FieldBasedIndexPatternColumn, FormBasedPersistedState, TypedLensByValueInput, } from '@kbn/lens-plugin/public'; @@ -181,3 +182,8 @@ export interface LensDataTableEmbeddable { id: string; timeRange: { from: string; to: string; fromStr: string; toStr: string }; } + +export interface LensEmbeddableDataTableColumn extends FieldBasedIndexPatternColumn { + operationType: string; + params?: unknown; +} diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_lens_attributes.tsx b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_lens_attributes.tsx index 7276bb9d2e118..81c2708d961cb 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_lens_attributes.tsx +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_lens_attributes.tsx @@ -81,10 +81,7 @@ export const useLensAttributes = ({ const lensAttrsWithInjectedData = useMemo(() => { if ( lensAttributes == null && - (getLensAttributes == null || - stackByField == null || - stackByField?.length === 0 || - (extraOptions?.breakdownField != null && extraOptions?.breakdownField.length === 0)) + (getLensAttributes == null || stackByField == null || stackByField?.length === 0) ) { return null; } @@ -113,7 +110,6 @@ export const useLensAttributes = ({ applyGlobalQueriesAndFilters, attrs, dataViewId, - extraOptions?.breakdownField, filters, getLensAttributes, hasAdHocDataViews, diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/chart_content.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/chart_content.tsx index 93b3f92642941..5e0077918b717 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/chart_content.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/chart_content.tsx @@ -37,7 +37,7 @@ const ChartContentComponent = ({ }: ChartContentProps) => { return isChartEmbeddablesEnabled ? ( Date: Mon, 8 May 2023 15:25:24 -0400 Subject: [PATCH 18/19] Remove 8.0 breaking change template (#157044) ## Summary Removes the 8.0 breaking change template. --- .github/ISSUE_TEMPLATE/v8_breaking_change.md | 60 -------------------- 1 file changed, 60 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/v8_breaking_change.md diff --git a/.github/ISSUE_TEMPLATE/v8_breaking_change.md b/.github/ISSUE_TEMPLATE/v8_breaking_change.md deleted file mode 100644 index 8ef570947dd41..0000000000000 --- a/.github/ISSUE_TEMPLATE/v8_breaking_change.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -name: 8.0 Breaking change -about: Breaking changes from 7.x -> 8.0 -title: "[Breaking change]" -labels: Feature:Upgrade Assistant, Breaking Change -assignees: '' - ---- - - - -## Change description - -**Which release will ship the breaking change?** - -8.0 - -**Is this a Kibana or Elasticsearch breaking change?** - - - -**Describe the change. How will it manifest to users?** - -**How many users will be affected?** - - - - -**Are there any edge cases?** - -**[For Kibana deprecations] Can the change be registered with the [Kibana deprecation service](https://github.com/elastic/kibana/blob/main/docs/development/core/server/kibana-plugin-core-server.deprecationsservicesetup.md)?** - - - - - -**[For Elasticsearch deprecations] Can the Upgrade Assistant make the migration easier for users? Please explain the proposed solution in as much detail as possible.** - - - - -## Test Data - - - -## Cross links - - \ No newline at end of file From 4eb8f1f547eda3bb5d32c7f83aba1fb0a7753fb4 Mon Sep 17 00:00:00 2001 From: Rickyanto Ang Date: Mon, 8 May 2023 12:44:55 -0700 Subject: [PATCH 19/19] [Cloud Security][Bug Fix]Removed Timeout message for Vulnerability Management (#156779) ## Summary This PR is includes the following changes - Vulnerability page showing index-timeout status after waiting for more than 10 minutes (this is changed to 60 minutes now) - Update Vulnerability page index-timeout message, it now no longer shows this message (will handle this case later) - Revert timeout message for CSPM/KSPM to previous iteration (doesn't include time) --- .../public/components/no_findings_states.tsx | 2 +- .../components/no_vulnerabilities_states.tsx | 40 +------------------ .../vulnerabilities/vulnerabilties.test.tsx | 4 +- .../server/routes/status/status.test.ts | 16 ++++++++ .../server/routes/status/status.ts | 7 +++- 5 files changed, 27 insertions(+), 42 deletions(-) diff --git a/x-pack/plugins/cloud_security_posture/public/components/no_findings_states.tsx b/x-pack/plugins/cloud_security_posture/public/components/no_findings_states.tsx index 897de7df552b9..773956b73c7a8 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/no_findings_states.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/no_findings_states.tsx @@ -125,7 +125,7 @@ const IndexTimeout = () => (

    diff --git a/x-pack/plugins/cloud_security_posture/public/components/no_vulnerabilities_states.tsx b/x-pack/plugins/cloud_security_posture/public/components/no_vulnerabilities_states.tsx index 705a0231fce10..cb11c456907cb 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/no_vulnerabilities_states.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/no_vulnerabilities_states.tsx @@ -11,7 +11,6 @@ import { EuiEmptyPrompt, EuiIcon, EuiMarkdownFormat, - EuiLink, EuiButton, EuiButtonEmpty, EuiFlexGroup, @@ -111,40 +110,6 @@ const VulnerabilitiesFindingsInstalledEmptyPrompt = ({ ); }; -const IndexTimeout = () => ( - } - title={ -

    - -

    - } - body={ -

    - - - - ), - }} - /> -

    - } - /> -); - const Unprivileged = ({ unprivilegedIndices }: { unprivilegedIndices: string[] }) => ( { .sort((a, b) => a.localeCompare(b)); const render = () => { - if (status === 'indexing' || status === 'waiting_for_results') - return ; // integration installed, but no agents added - if (status === 'index-timeout') return ; // agent added, index timeout has passed + if (status === 'indexing' || status === 'waiting_for_results' || status === 'index-timeout') + return ; // integration installed, but no agents added// agent added, index timeout has passed if (status === 'not-deployed' || status === 'not-installed') return ( ', () => { renderVulnerabilitiesPage(); expectIdsInDoc({ - be: [NO_VULNERABILITIES_STATUS_TEST_SUBJ.INDEX_TIMEOUT], + be: [NO_VULNERABILITIES_STATUS_TEST_SUBJ.SCANNING_VULNERABILITIES], notToBe: [ VULNERABILITIES_CONTAINER_TEST_SUBJ, - NO_VULNERABILITIES_STATUS_TEST_SUBJ.SCANNING_VULNERABILITIES, + NO_VULNERABILITIES_STATUS_TEST_SUBJ.INDEX_TIMEOUT, NO_VULNERABILITIES_STATUS_TEST_SUBJ.UNPRIVILEGED, ], }); diff --git a/x-pack/plugins/cloud_security_posture/server/routes/status/status.test.ts b/x-pack/plugins/cloud_security_posture/server/routes/status/status.test.ts index 00835a9520c60..ee7674bd7822b 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/status/status.test.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/status/status.test.ts @@ -248,6 +248,22 @@ describe('calculateIntegrationStatus for vul_mgmt', () => { [VULN_MGMT_POLICY_TEMPLATE] ); + expect(statusCode).toMatch('waiting_for_results'); + }); + + it('Verify status when there are installed policies, healthy agents and no vul_mgmt findings and been more than 1 hour', async () => { + const statusCode = calculateIntegrationStatus( + VULN_MGMT_POLICY_TEMPLATE, + { + latest: 'empty', + stream: 'empty', + score: 'empty', + }, + 1, + 61, + [VULN_MGMT_POLICY_TEMPLATE] + ); + expect(statusCode).toMatch('index-timeout'); }); diff --git a/x-pack/plugins/cloud_security_posture/server/routes/status/status.ts b/x-pack/plugins/cloud_security_posture/server/routes/status/status.ts index 69ae5eae2a414..4d394ced13eff 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/status/status.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/status/status.ts @@ -45,6 +45,7 @@ import { import { checkIndexStatus } from '../../lib/check_index_status'; export const INDEX_TIMEOUT_IN_MINUTES = 10; +export const INDEX_TIMEOUT_IN_MINUTES_CNVM = 60; interface CspStatusDependencies { logger: Logger; @@ -100,6 +101,7 @@ export const calculateIntegrationStatus = ( ): CspStatusCode => { // We check privileges only for the relevant indices for our pages to appear const postureTypeCheck: PostureTypes = POSTURE_TYPES[integration]; + if (indicesStatus.latest === 'unprivileged' || indicesStatus.score === 'unprivileged') return 'unprivileged'; if (indicesStatus.latest === 'not-empty') return 'indexed'; @@ -110,7 +112,10 @@ export const calculateIntegrationStatus = ( if ( indicesStatus.latest === 'empty' && indicesStatus.stream === 'empty' && - timeSinceInstallationInMinutes < INDEX_TIMEOUT_IN_MINUTES + timeSinceInstallationInMinutes < + (postureTypeCheck !== VULN_MGMT_POLICY_TEMPLATE + ? INDEX_TIMEOUT_IN_MINUTES + : INDEX_TIMEOUT_IN_MINUTES_CNVM) ) return 'waiting_for_results';