From 45f5f3d14c414d7873475e0e49b74919c62c64c3 Mon Sep 17 00:00:00 2001 From: Julia Bardi <90178898+juliaElastic@users.noreply.github.com> Date: Mon, 13 Feb 2023 12:58:04 +0100 Subject: [PATCH 01/20] [Fleet] fixed bug where installed beta integration was visible multiple times (#150979) ## Summary Fixes https://github.com/elastic/kibana/issues/150969 Filtering for only uploaded packages that are not in registry. This fixes of bug of linux integration showing up multiple times when the `/packages` call with `prerelease:false` and `prerelease:true` options are quickly following each other. To verify: - Navigate to Integrations, search `linux`. - Add linux integration to agent policy. - Repeat the same process again. - Navigate back to Integrations and search `linux`. - Only one Linux metrics integration card should be visible. image Uploaded integrations are still visible: image ### 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 --- .../fleet/server/services/epm/packages/get.test.ts | 6 +++--- x-pack/plugins/fleet/server/services/epm/packages/get.ts | 8 +++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/fleet/server/services/epm/packages/get.test.ts b/x-pack/plugins/fleet/server/services/epm/packages/get.test.ts index 52170f6c302fd..661784a99ced5 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/get.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/get.test.ts @@ -209,7 +209,7 @@ describe('When using EPM `get` services', () => { attributes: { name: 'elasticsearch', version: '0.0.1', - install_status: 'upload', + install_source: 'upload', }, }, ], @@ -221,16 +221,16 @@ describe('When using EPM `get` services', () => { }) ).resolves.toMatchObject([ { + id: 'elasticsearch', name: 'elasticsearch', version: '0.0.1', title: 'Elasticsearch', - status: 'upload', savedObject: { id: 'elasticsearch', attributes: { name: 'elasticsearch', version: '0.0.1', - install_status: 'upload', + install_source: 'upload', }, }, }, diff --git a/x-pack/plugins/fleet/server/services/epm/packages/get.ts b/x-pack/plugins/fleet/server/services/epm/packages/get.ts index 83db878ef5b83..7baddd428076b 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/get.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/get.ts @@ -74,9 +74,11 @@ export async function getPackages( // get the installed packages const packageSavedObjects = await getPackageSavedObjects(savedObjectsClient); - const packagesNotInRegistry = packageSavedObjects.saved_objects + const uploadedPackagesNotInRegistry = packageSavedObjects.saved_objects .filter((pkg) => !registryItems.some((item) => item.name === pkg.id)) - .map((pkg) => createInstallableFrom({ ...pkg.attributes, title: nameAsTitle(pkg.id) }, pkg)); + .map((pkg) => + createInstallableFrom({ ...pkg.attributes, title: nameAsTitle(pkg.id), id: pkg.id }, pkg) + ); const packageList = registryItems .map((item) => @@ -85,7 +87,7 @@ export async function getPackages( packageSavedObjects.saved_objects.find(({ id }) => id === item.name) ) ) - .concat(packagesNotInRegistry as Installable) + .concat(uploadedPackagesNotInRegistry as Installable) .sort(sortByName); if (!excludeInstallStatus) { From 2315285e37274d8aa724d517055a34272efbe3d7 Mon Sep 17 00:00:00 2001 From: Pablo Machado Date: Mon, 13 Feb 2023 13:30:26 +0100 Subject: [PATCH 02/20] [Security Solutions] Fix link to ML Job is wrong when searching within ML Job Settings UI (#150881) issue: https://github.com/elastic/kibana/issues/150875 ## Summary Describe the bug: The link to an ML job is wrong when searching for a job. Steps to reproduce: 1. Open ML Job Settings UI 2. Type DNS in the search bar 3. Verify that the links have the wrong job Id. ### Before https://user-images.githubusercontent.com/1490444/218104808-94c93669-2de3-4fba-88c9-5f2b7ccaa391.mp4 ### After https://user-images.githubusercontent.com/1490444/218105267-b5aa118c-9d78-4648-9892-68afd5e2707f.mov --- .../analyze_dataset_in_ml_action.tsx | 31 +++++++++-------- .../plugins/ml/public/locator/use_ml_href.ts | 6 ++-- .../ml/links/create_explorer_link.tsx | 33 +++++++++++-------- .../ml_popover/jobs_table/jobs_table.tsx | 15 ++++++--- .../rules/ml_job_link/ml_job_link.tsx | 15 ++++++--- 5 files changed, 61 insertions(+), 39 deletions(-) diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/analyze_dataset_in_ml_action.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/analyze_dataset_in_ml_action.tsx index 4d17b6ed9e8c5..6af0607580c4d 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/analyze_dataset_in_ml_action.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/analyze_dataset_in_ml_action.tsx @@ -25,21 +25,26 @@ export const AnalyzeCategoryDatasetInMlAction: React.FunctionComponent<{ services: { ml, http, application }, } = useKibanaContextForPlugin(); - const viewAnomalyInMachineLearningLink = useMlHref(ml, http.basePath.get(), { - page: ML_PAGES.SINGLE_METRIC_VIEWER, - pageState: { - jobIds: [categorizationJobId], - timeRange: { - from: moment(timeRange.startTime).format('YYYY-MM-DDTHH:mm:ss.SSSZ'), - to: moment(timeRange.endTime).format('YYYY-MM-DDTHH:mm:ss.SSSZ'), - mode: 'absolute', - }, - entities: { - [partitionField]: dataset, - mlcategory: `${categoryId}`, + const viewAnomalyInMachineLearningLink = useMlHref( + ml, + http.basePath.get(), + { + page: ML_PAGES.SINGLE_METRIC_VIEWER, + pageState: { + jobIds: [categorizationJobId], + timeRange: { + from: moment(timeRange.startTime).format('YYYY-MM-DDTHH:mm:ss.SSSZ'), + to: moment(timeRange.endTime).format('YYYY-MM-DDTHH:mm:ss.SSSZ'), + mode: 'absolute', + }, + entities: { + [partitionField]: dataset, + mlcategory: `${categoryId}`, + }, }, }, - }); + [categorizationJobId] + ); const handleClick = useCallback( (e) => { diff --git a/x-pack/plugins/ml/public/locator/use_ml_href.ts b/x-pack/plugins/ml/public/locator/use_ml_href.ts index 59b34dffa7c13..1ed7321f3fd2f 100644 --- a/x-pack/plugins/ml/public/locator/use_ml_href.ts +++ b/x-pack/plugins/ml/public/locator/use_ml_href.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { DependencyList } from 'react'; import { MlPluginStart } from '..'; import { MlLocatorParams } from '../../common/types/locator'; @@ -15,10 +16,11 @@ import { MlLocatorParams } from '../../common/types/locator'; export const useMlHref = ( ml: MlPluginStart | undefined, basePath: string | undefined, - params: MlLocatorParams + params: MlLocatorParams, + dependencies?: DependencyList ) => { return ml && ml.locator - ? ml.locator!.useUrl(params) + ? ml.locator.useUrl(params, undefined, dependencies) : basePath !== undefined ? `${basePath}/app/ml/${params.page}` : ''; diff --git a/x-pack/plugins/security_solution/public/common/components/ml/links/create_explorer_link.tsx b/x-pack/plugins/security_solution/public/common/components/ml/links/create_explorer_link.tsx index 9f7ed8a526e12..400d715754e92 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/links/create_explorer_link.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/links/create_explorer_link.tsx @@ -28,22 +28,27 @@ export const ExplorerLink: React.FC = ({ services: { ml, http }, } = useKibana(); - const explorerUrl = useMlHref(ml, http.basePath.get(), { - page: 'explorer', - pageState: { - jobIds: [score.jobId], - timeRange: { - from: new Date(startDate).toISOString(), - to: new Date(endDate).toISOString(), - mode: 'absolute', - }, - refreshInterval: { - pause: true, - value: 0, - display: 'Off', + const explorerUrl = useMlHref( + ml, + http.basePath.get(), + { + page: 'explorer', + pageState: { + jobIds: [score.jobId], + timeRange: { + from: new Date(startDate).toISOString(), + to: new Date(endDate).toISOString(), + mode: 'absolute', + }, + refreshInterval: { + pause: true, + value: 0, + display: 'Off', + }, }, }, - }); + [score.jobId] + ); if (!explorerUrl) return null; diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/jobs_table.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/jobs_table.tsx index f8c27cd71b1d2..8b3bbfa3f5aeb 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/jobs_table.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/jobs_table.tsx @@ -47,12 +47,17 @@ const JobName = ({ id, name, description, basePath }: JobNameProps) => { services: { ml }, } = useKibana(); - const jobUrl = useMlHref(ml, basePath, { - page: ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE, - pageState: { - jobId: id, + const jobUrl = useMlHref( + ml, + basePath, + { + page: ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE, + pageState: { + jobId: id, + }, }, - }); + [id] + ); return ( diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/ml_job_link/ml_job_link.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/ml_job_link/ml_job_link.tsx index 7680b5b72dff3..cbdddae858085 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/ml_job_link/ml_job_link.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/ml_job_link/ml_job_link.tsx @@ -25,12 +25,17 @@ const MlJobLinkComponent: React.FC = ({ jobId, jobName }) => { const { services: { http, ml }, } = useKibana(); - const jobUrl = useMlHref(ml, http.basePath.get(), { - page: ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE, - pageState: { - jobId: [jobId], + const jobUrl = useMlHref( + ml, + http.basePath.get(), + { + page: ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE, + pageState: { + jobId: [jobId], + }, }, - }); + [jobId] + ); return ( From a37376e72022c6c6e884ed94edbf63a99c70789d Mon Sep 17 00:00:00 2001 From: Gloria Hornero Date: Mon, 13 Feb 2023 14:45:29 +0100 Subject: [PATCH 03/20] [Security Solution] Automates C18001 (#150973) Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../cypress/e2e/cases/creation.cy.ts | 11 ++++++++++- .../security_solution/cypress/screens/overview.ts | 3 +++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/cypress/e2e/cases/creation.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/cases/creation.cy.ts index 0eaa67b3e1096..46bfd1f388ea6 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/cases/creation.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/cases/creation.cy.ts @@ -37,6 +37,8 @@ import { } from '../../screens/case_details'; import { TIMELINE_DESCRIPTION, TIMELINE_QUERY, TIMELINE_TITLE } from '../../screens/timeline'; +import { OVERVIEW_CASE_DESCRIPTION, OVERVIEW_CASE_NAME } from '../../screens/overview'; + import { goToCaseDetails, goToCreateNewCase } from '../../tasks/all_cases'; import { createTimeline } from '../../tasks/api_calls/timelines'; import { openCaseTimeline } from '../../tasks/case_details'; @@ -50,7 +52,7 @@ import { } from '../../tasks/create_new_case'; import { loginWithUser, visitWithoutDateRange } from '../../tasks/login'; -import { CASES_URL } from '../../urls/navigation'; +import { CASES_URL, OVERVIEW_URL } from '../../urls/navigation'; describe('Cases', () => { before(() => { @@ -120,5 +122,12 @@ describe('Cases', () => { cy.get(TIMELINE_TITLE).contains(this.mycase.timeline.title); cy.get(TIMELINE_DESCRIPTION).contains(this.mycase.timeline.description); cy.get(TIMELINE_QUERY).should('have.text', this.mycase.timeline.query); + + cy.visit(OVERVIEW_URL); + cy.get(OVERVIEW_CASE_NAME).should('have.text', this.mycase.name); + cy.get(OVERVIEW_CASE_DESCRIPTION).should( + 'have.text', + `${this.mycase.description} ${this.mycase.timeline.title}` + ); }); }); diff --git a/x-pack/plugins/security_solution/cypress/screens/overview.ts b/x-pack/plugins/security_solution/cypress/screens/overview.ts index 12de337a51aa3..e7357adc2353d 100644 --- a/x-pack/plugins/security_solution/cypress/screens/overview.ts +++ b/x-pack/plugins/security_solution/cypress/screens/overview.ts @@ -140,6 +140,9 @@ export const NETWORK_STATS = [ STAT_TLS, ]; +export const OVERVIEW_CASE_NAME = '[data-test-subj="case-details-link"]'; +export const OVERVIEW_CASE_DESCRIPTION = '.euiText.euiMarkdownFormat'; + export const OVERVIEW_HOST_STATS = '[data-test-subj="overview-hosts-stats"]'; export const OVERVIEW_NETWORK_STATS = '[data-test-subj="overview-network-stats"]'; From 39197628d2be02fb9a97666993c0514c1950b382 Mon Sep 17 00:00:00 2001 From: Kevin Logan <56395104+kevinlog@users.noreply.github.com> Date: Mon, 13 Feb 2023 09:14:40 -0500 Subject: [PATCH 04/20] [Security Solution] Update error codes for reporting es connection errors (#150792) ## Summary This PR fixes a bug where we weren't checking the uppercase version of input statuses. In addition, it adds another error code to check for Endpoint when it cannot ship data to ES. We also needed to check for `DEGRADED` status in addition to `FAILED` Mac OS full disk access reporting image ES connection errors: ![image](https://user-images.githubusercontent.com/56395104/217945339-1bac8935-6453-45b6-ad47-d1d546a7dfc2.png) Response from Agent/Endpoint image ### 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] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../plugins/fleet/common/constants/agent.ts | 14 +++++------ .../agent_details_integrations.tsx | 4 ++- .../common/endpoint/constants.ts | 1 + .../data_generators/fleet_agent_generator.ts | 2 +- .../package_action.formatter.test.ts | 25 +++++++++++++++++-- .../package_action_formatter.ts | 7 ++++-- .../endpoint_generic_errors_list.tsx | 2 +- 7 files changed, 41 insertions(+), 14 deletions(-) diff --git a/x-pack/plugins/fleet/common/constants/agent.ts b/x-pack/plugins/fleet/common/constants/agent.ts index 6d4133c32c6c7..d05dd66bb096b 100644 --- a/x-pack/plugins/fleet/common/constants/agent.ts +++ b/x-pack/plugins/fleet/common/constants/agent.ts @@ -27,11 +27,11 @@ export const AGENT_ACTIONS_INDEX = '.fleet-actions'; export const AGENT_ACTIONS_RESULTS_INDEX = '.fleet-actions-results'; export const FleetServerAgentComponentStatuses = [ - 'starting', - 'configuring', - 'healthy', - 'degraded', - 'failed', - 'stopping', - 'stopped', + 'STARTING', + 'CONFIGURING', + 'HEALTHY', + 'DEGRADED', + 'FAILED', + 'STOPPING', + 'STOPPED', ] as const; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_integrations.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_integrations.tsx index 16504516c72ab..9283416a111a8 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_integrations.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_integrations.tsx @@ -131,7 +131,9 @@ export const AgentDetailsIntegration: React.FunctionComponent<{ }); filteredPackageComponents.forEach((component) => { - packageErrorUnits.push(...filter(component.units, { status: 'failed' })); + packageErrorUnits.push( + ...filter(component.units, (u) => u.status === 'DEGRADED' || u.status === 'FAILED') + ); }); return packageErrorUnits; }, [agent.components, packagePolicy]); diff --git a/x-pack/plugins/security_solution/common/endpoint/constants.ts b/x-pack/plugins/security_solution/common/endpoint/constants.ts index d005f449d76a3..175f1cfdf14e4 100644 --- a/x-pack/plugins/security_solution/common/endpoint/constants.ts +++ b/x-pack/plugins/security_solution/common/endpoint/constants.ts @@ -94,6 +94,7 @@ export const ENDPOINT_DEFAULT_PAGE_SIZE = 10; export const ENDPOINT_ERROR_CODES: Record = { ES_CONNECTION_ERROR: -272, + OUTPUT_SERVER_ERROR: -273, }; export const ENDPOINT_FIELDS_SEARCH_STRATEGY = 'endpointFields'; diff --git a/x-pack/plugins/security_solution/common/endpoint/data_generators/fleet_agent_generator.ts b/x-pack/plugins/security_solution/common/endpoint/data_generators/fleet_agent_generator.ts index 39e7efc47c455..ea5c377102b17 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_generators/fleet_agent_generator.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_generators/fleet_agent_generator.ts @@ -88,7 +88,7 @@ export class FleetAgentGenerator extends BaseDataGenerator { FleetServerAgentComponentStatuses ); const componentInputPayload = - componentStatus === 'failed' + componentStatus === 'FAILED' ? { error: { code: ENDPOINT_ERROR_CODES.ES_CONNECTION_ERROR, diff --git a/x-pack/plugins/security_solution/public/management/components/package_action_item/package_action.formatter.test.ts b/x-pack/plugins/security_solution/public/management/components/package_action_item/package_action.formatter.test.ts index dc5c443e15e2e..2dee21fcc89da 100644 --- a/x-pack/plugins/security_solution/public/management/components/package_action_item/package_action.formatter.test.ts +++ b/x-pack/plugins/security_solution/public/management/components/package_action_item/package_action.formatter.test.ts @@ -15,7 +15,7 @@ describe('PackageActionFormatter', () => { const unit: FleetServerAgentComponentUnit = { id: 'test-id', type: 'input', - status: 'failed', + status: 'FAILED', message: 'test message', payload: { error: { @@ -32,11 +32,32 @@ describe('PackageActionFormatter', () => { expect(formatter.linkUrl).toBe(docLinks.es_connection); }); + it('correctly formats output connection error', () => { + const unit: FleetServerAgentComponentUnit = { + id: 'test-id', + type: 'input', + status: 'DEGRADED', + message: 'test message', + payload: { + error: { + code: ENDPOINT_ERROR_CODES.OUTPUT_SERVER_ERROR, + message: 'an error message', + }, + }, + }; + const docLinks = { es_connection: 'somedoclink' }; + const formatter = new PackageActionFormatter(unit, docLinks); + expect(formatter.key).toBe('es_connection'); + expect(formatter.title).toBe(titles.get('es_connection')); + expect(formatter.description).toBe(descriptions.get('es_connection')); + expect(formatter.linkUrl).toBe(docLinks.es_connection); + }); + it('correct formats generic error', () => { const unit: FleetServerAgentComponentUnit = { id: 'test-id', type: 'input', - status: 'failed', + status: 'FAILED', message: 'test message', }; const docLinks = { es_connection: 'somedoclink' }; diff --git a/x-pack/plugins/security_solution/public/management/components/package_action_item/package_action_formatter.ts b/x-pack/plugins/security_solution/public/management/components/package_action_item/package_action_formatter.ts index 9af6983ecfcdb..f611f3d78f1c1 100644 --- a/x-pack/plugins/security_solution/public/management/components/package_action_item/package_action_formatter.ts +++ b/x-pack/plugins/security_solution/public/management/components/package_action_item/package_action_formatter.ts @@ -101,9 +101,12 @@ export class PackageActionFormatter { code: number, status: FleetServerAgentComponentStatus ): PackageActions { - if (code === ENDPOINT_ERROR_CODES.ES_CONNECTION_ERROR) { + if ( + code === ENDPOINT_ERROR_CODES.ES_CONNECTION_ERROR || + code === ENDPOINT_ERROR_CODES.OUTPUT_SERVER_ERROR + ) { return 'es_connection'; - } else if (status === 'failed') { + } else if (status === 'FAILED' || status === 'DEGRADED') { return 'policy_failure'; } else { throw new Error(`Invalid error code ${code}`); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_generic_errors_list.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_generic_errors_list.tsx index babb4c78af147..e3902e965632a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_generic_errors_list.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_generic_errors_list.tsx @@ -23,7 +23,7 @@ export const EndpointGenericErrorsList = memo( const globalEndpointErrors = useMemo(() => { const errors: PackageActionFormatter[] = []; packageErrors.forEach((unit) => { - if (unit.status === 'failed') { + if (unit.status === 'FAILED' || unit.status === 'DEGRADED') { errors.push( new PackageActionFormatter( unit, From 2749e95fe0a5f0d1617129765eb3b84ea0cec097 Mon Sep 17 00:00:00 2001 From: "Devin W. Hurley" Date: Mon, 13 Feb 2023 09:29:58 -0500 Subject: [PATCH 05/20] [Security Solution] [Exceptions] fixes download from list details page (#150933) ## Summary Fixes: https://github.com/elastic/kibana/issues/148139 When exporting a shared exception list from that lists' detailed view, the exported file would not have the `.ndjson` extension appended to it. I think we should update this to be a single hook used between the two views. --- .../public/exceptions/hooks/use_list_detail_view/index.ts | 5 +++++ .../public/exceptions/pages/list_detail_view/index.tsx | 4 +++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/exceptions/hooks/use_list_detail_view/index.ts b/x-pack/plugins/security_solution/public/exceptions/hooks/use_list_detail_view/index.ts index 949854d78ae65..3359ccb760b05 100644 --- a/x-pack/plugins/security_solution/public/exceptions/hooks/use_list_detail_view/index.ts +++ b/x-pack/plugins/security_solution/public/exceptions/hooks/use_list_detail_view/index.ts @@ -190,6 +190,10 @@ export const useListDetailsView = (exceptionListId: string) => { [list, exportExceptionList, handleErrorStatus, toasts] ); + const handleOnDownload = useCallback(() => { + setExportedList(undefined); + }, []); + // #region DeleteList const handleDeleteSuccess = useCallback( @@ -366,6 +370,7 @@ export const useListDetailsView = (exceptionListId: string) => { canUserEditList, linkedRules, exportedList, + handleOnDownload, viewerStatus, showManageRulesFlyout, headerBackOptions, diff --git a/x-pack/plugins/security_solution/public/exceptions/pages/list_detail_view/index.tsx b/x-pack/plugins/security_solution/public/exceptions/pages/list_detail_view/index.tsx index 0be51bf851ba1..fc09a603b2e13 100644 --- a/x-pack/plugins/security_solution/public/exceptions/pages/list_detail_view/index.tsx +++ b/x-pack/plugins/security_solution/public/exceptions/pages/list_detail_view/index.tsx @@ -39,6 +39,7 @@ export const ListsDetailViewComponent: FC = () => { listId, linkedRules, exportedList, + handleOnDownload, viewerStatus, listName, listDescription, @@ -91,7 +92,7 @@ export const ListsDetailViewComponent: FC = () => { onManageRules={onManageRules} /> - + { canUserEditList, disableManageButton, exportedList, + handleOnDownload, headerBackOptions, invalidListId, isLoading, From 4f1f2a84fdf3421d6b56aac82274dee2e881d376 Mon Sep 17 00:00:00 2001 From: Dmitrii Shevchenko Date: Mon, 13 Feb 2023 16:00:20 +0100 Subject: [PATCH 06/20] [Security Solution] Invalidate prebuilt rules status after package upgrade or installation (#150292) **Resolves: https://github.com/elastic/kibana/issues/150306** ## Summary Fixes the Load Prebuilt rules button not visible when users visit the rules management page for the first time (no prebuilt detection rules package installed). ## Steps to test 1. Ensure that the detection engine package is not installed: 2. Navigate to the rules management page. ### Previously The "Load Elastic Prebuilt Rules" button is not visible, and users cannot install prebuilt rules. ### With the fix Users now see loading animation, indicating that the package installation happens in the background. Once the package installation finishes, users see the Load Prebuilt rules button appear. https://user-images.githubusercontent.com/1938181/217585144-879fe288-0ede-4e01-b585-6aced1d89379.mov --- .../public/app/home/index.tsx | 2 +- .../hooks/use_upgrade_security_packages.ts | 113 ------------------ .../rule_management/api/api.ts | 61 ++++++++++ ...se_bulk_install_fleet_packages_mutation.ts | 44 +++++++ .../use_install_fleet_package_mutation.ts | 41 +++++++ .../logic/use_install_pre_packaged_rules.ts | 11 +- .../use_upgrade_secuirty_packages.test.tsx | 109 +++++++---------- .../logic/use_upgrade_security_packages.ts | 95 +++++++++++++++ .../components/rules_table/rules_tables.tsx | 4 +- .../load_prepackaged_rules.tsx | 10 +- 10 files changed, 304 insertions(+), 186 deletions(-) delete mode 100644 x-pack/plugins/security_solution/public/common/hooks/use_upgrade_security_packages.ts create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_bulk_install_fleet_packages_mutation.ts create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_install_fleet_package_mutation.ts rename x-pack/plugins/security_solution/public/{common/hooks => detection_engine/rule_management/logic}/use_upgrade_secuirty_packages.test.tsx (57%) create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_upgrade_security_packages.ts diff --git a/x-pack/plugins/security_solution/public/app/home/index.tsx b/x-pack/plugins/security_solution/public/app/home/index.tsx index 5aecd3cdc62ba..d74eb3eed5af7 100644 --- a/x-pack/plugins/security_solution/public/app/home/index.tsx +++ b/x-pack/plugins/security_solution/public/app/home/index.tsx @@ -18,7 +18,6 @@ import { getScopeFromPath, useSourcererDataView, } from '../../common/containers/sourcerer'; -import { useUpgradeSecurityPackages } from '../../common/hooks/use_upgrade_security_packages'; import { GlobalHeader } from './global_header'; import { ConsoleManager } from '../../management/components/console/components/console_manager'; @@ -26,6 +25,7 @@ import { TourContextProvider } from '../../common/components/guided_onboarding_t import { useUrlState } from '../../common/hooks/use_url_state'; import { useUpdateBrowserTitle } from '../../common/hooks/use_update_browser_title'; +import { useUpgradeSecurityPackages } from '../../detection_engine/rule_management/logic/use_upgrade_security_packages'; interface HomePageProps { children: React.ReactNode; diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_upgrade_security_packages.ts b/x-pack/plugins/security_solution/public/common/hooks/use_upgrade_security_packages.ts deleted file mode 100644 index 3ffb3ca149b20..0000000000000 --- a/x-pack/plugins/security_solution/public/common/hooks/use_upgrade_security_packages.ts +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { useEffect } from 'react'; -import type { HttpFetchOptions, HttpStart } from '@kbn/core/public'; -import type { BulkInstallPackagesResponse } from '@kbn/fleet-plugin/common'; -import { epmRouteService } from '@kbn/fleet-plugin/common'; -import type { InstallPackageResponse } from '@kbn/fleet-plugin/common/types'; -import { KibanaServices, useKibana } from '../lib/kibana'; -import { useUserPrivileges } from '../components/user_privileges'; -import { PREBUILT_RULES_PACKAGE_NAME } from '../../../common/detection_engine/constants'; - -/** - * Requests that the endpoint and security_detection_engine package be upgraded to the latest version - * - * @param http an http client for sending the request - * @param options an object containing options for the request - * @param prebuiltRulesPackageVersion specific version of the prebuilt rules package to install - */ -const sendUpgradeSecurityPackages = async ( - http: HttpStart, - options: HttpFetchOptions = {}, - prebuiltRulesPackageVersion?: string -): Promise => { - const packages = ['endpoint', PREBUILT_RULES_PACKAGE_NAME]; - const requests: Array> = []; - - // If `prebuiltRulesPackageVersion` is provided, try to install that version - // Must be done as two separate requests as bulk API doesn't support versions - if (prebuiltRulesPackageVersion != null) { - packages.splice(packages.indexOf(PREBUILT_RULES_PACKAGE_NAME), 1); - requests.push( - http.post( - epmRouteService.getInstallPath(PREBUILT_RULES_PACKAGE_NAME, prebuiltRulesPackageVersion), - { - ...options, - body: JSON.stringify({ - force: true, - }), - } - ) - ); - } - - // Note: if `prerelease:true` option is provided, endpoint package will also be installed as prerelease - requests.push( - http.post(epmRouteService.getBulkInstallPath(), { - ...options, - body: JSON.stringify({ - packages, - }), - }) - ); - - await Promise.allSettled(requests); -}; - -export const useUpgradeSecurityPackages = () => { - const context = useKibana(); - const canAccessFleet = useUserPrivileges().endpointPrivileges.canAccessFleet; - - useEffect(() => { - const abortController = new AbortController(); - - // cancel any ongoing requests - const abortRequests = () => { - abortController.abort(); - }; - - if (canAccessFleet) { - const signal = abortController.signal; - - (async () => { - try { - // Make sure fleet is initialized first - await context.services.fleet?.isInitialized(); - - // Always install the latest package if in dev env or snapshot build - const isPrerelease = - KibanaServices.getKibanaVersion().includes('-SNAPSHOT') || - KibanaServices.getKibanaBranch() === 'main'; - - // ignore the response for now since we aren't notifying the user - // Note: response would be Promise.allSettled, so must iterate all responses for errors and throw manually - await sendUpgradeSecurityPackages( - context.services.http, - { - query: { - prerelease: isPrerelease, - }, - signal, - }, - KibanaServices.getPrebuiltRulesPackageVersion() - ); - } catch (error) { - // Ignore Errors, since this should not hinder the user's ability to use the UI - - // log to console, except if the error occurred due to aborting a request - if (!abortController.signal.aborted) { - // eslint-disable-next-line no-console - console.error(error); - } - } - })(); - - return abortRequests; - } - }, [canAccessFleet, context.services.fleet, context.services.http]); -}; 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 87334a121c993..386dbf3c7b525 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 @@ -10,6 +10,9 @@ import type { ExceptionListItemSchema, } from '@kbn/securitysolution-io-ts-list-types'; +import type { BulkInstallPackagesResponse } from '@kbn/fleet-plugin/common'; +import { epmRouteService } from '@kbn/fleet-plugin/common'; +import type { InstallPackageResponse } from '@kbn/fleet-plugin/common/types'; import type { RuleManagementFiltersResponse } from '../../../../common/detection_engine/rule_management/api/rules/filters/response_schema'; import { RULE_MANAGEMENT_FILTERS_URL } from '../../../../common/detection_engine/rule_management/api/urls'; import type { BulkActionsDryRunErrCode } from '../../../../common/constants'; @@ -481,3 +484,61 @@ export const addRuleExceptions = async ({ signal, } ); + +export interface InstallFleetPackageProps { + packageName: string; + packageVersion: string; + prerelease?: boolean; + force?: boolean; +} + +/** + * Install a Fleet package from the registry + * + * @param packageName Name of the package to install + * @param packageVersion Version of the package to install + * @param prerelease Whether to install a prerelease version of the package + * @param force Whether to force install the package. If false, the package will only be installed if it is not already installed + * + * @returns The response from the Fleet API + */ +export const installFleetPackage = ({ + packageName, + packageVersion, + prerelease = false, + force = true, +}: InstallFleetPackageProps): Promise => { + return KibanaServices.get().http.post( + epmRouteService.getInstallPath(packageName, packageVersion), + { + query: { prerelease }, + body: JSON.stringify({ force }), + } + ); +}; + +export interface BulkInstallFleetPackagesProps { + packages: string[]; + prerelease?: boolean; +} + +/** + * Install multiple Fleet packages from the registry + * + * @param packages Array of package names to install + * @param prerelease Whether to install prerelease versions of the packages + * + * @returns The response from the Fleet API + */ +export const bulkInstallFleetPackages = ({ + packages, + prerelease = false, +}: BulkInstallFleetPackagesProps): Promise => { + return KibanaServices.get().http.post( + epmRouteService.getBulkInstallPath(), + { + query: { prerelease }, + body: JSON.stringify({ packages }), + } + ); +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_bulk_install_fleet_packages_mutation.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_bulk_install_fleet_packages_mutation.ts new file mode 100644 index 0000000000000..adbcec981ca3c --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_bulk_install_fleet_packages_mutation.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { EPM_API_ROUTES } from '@kbn/fleet-plugin/common'; +import type { BulkInstallPackagesResponse } from '@kbn/fleet-plugin/common/types'; +import type { UseMutationOptions } from '@tanstack/react-query'; +import { useMutation } from '@tanstack/react-query'; +import { PREBUILT_RULES_PACKAGE_NAME } from '../../../../../common/detection_engine/constants'; +import type { BulkInstallFleetPackagesProps } from '../api'; +import { bulkInstallFleetPackages } from '../api'; +import { useInvalidateFetchPrebuiltRulesStatusQuery } from './use_fetch_prebuilt_rules_status_query'; + +export const BULK_INSTALL_FLEET_PACKAGES_MUTATION_KEY = [ + 'POST', + EPM_API_ROUTES.BULK_INSTALL_PATTERN, +]; + +export const useBulkInstallFleetPackagesMutation = ( + options?: UseMutationOptions +) => { + const invalidatePrePackagedRulesStatus = useInvalidateFetchPrebuiltRulesStatusQuery(); + + return useMutation((props: BulkInstallFleetPackagesProps) => bulkInstallFleetPackages(props), { + ...options, + mutationKey: BULK_INSTALL_FLEET_PACKAGES_MUTATION_KEY, + onSettled: (...args) => { + const response = args[0]; + const rulesPackage = response?.items.find( + (item) => item.name === PREBUILT_RULES_PACKAGE_NAME + ); + if (rulesPackage && 'result' in rulesPackage && rulesPackage.result.status === 'installed') { + // The rules package was installed/updated, so invalidate the pre-packaged rules status query + invalidatePrePackagedRulesStatus(); + } + + if (options?.onSettled) { + options.onSettled(...args); + } + }, + }); +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_install_fleet_package_mutation.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_install_fleet_package_mutation.ts new file mode 100644 index 0000000000000..0e6927e1745dd --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_install_fleet_package_mutation.ts @@ -0,0 +1,41 @@ +/* + * 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 { EPM_API_ROUTES } from '@kbn/fleet-plugin/common'; +import type { InstallPackageResponse } from '@kbn/fleet-plugin/common/types'; +import type { UseMutationOptions } from '@tanstack/react-query'; +import { useMutation } from '@tanstack/react-query'; +import { PREBUILT_RULES_PACKAGE_NAME } from '../../../../../common/detection_engine/constants'; +import type { InstallFleetPackageProps } from '../api'; +import { installFleetPackage } from '../api'; +import { useInvalidateFetchPrebuiltRulesStatusQuery } from './use_fetch_prebuilt_rules_status_query'; + +export const INSTALL_FLEET_PACKAGE_MUTATION_KEY = [ + 'POST', + EPM_API_ROUTES.INSTALL_FROM_REGISTRY_PATTERN, +]; + +export const useInstallFleetPackageMutation = ( + options?: UseMutationOptions +) => { + const invalidatePrePackagedRulesStatus = useInvalidateFetchPrebuiltRulesStatusQuery(); + + return useMutation((props: InstallFleetPackageProps) => installFleetPackage(props), { + ...options, + mutationKey: INSTALL_FLEET_PACKAGE_MUTATION_KEY, + onSettled: (...args) => { + const { packageName } = args[2]; + if (packageName === PREBUILT_RULES_PACKAGE_NAME) { + // Invalidate the pre-packaged rules status query as there might be new rules to install + invalidatePrePackagedRulesStatus(); + } + + if (options?.onSettled) { + options.onSettled(...args); + } + }, + }); +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_install_pre_packaged_rules.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_install_pre_packaged_rules.ts index 21ea298986598..b7fa307c0fedc 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_install_pre_packaged_rules.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_install_pre_packaged_rules.ts @@ -4,8 +4,12 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { useIsMutating } from '@tanstack/react-query'; import { useAppToasts } from '../../../common/hooks/use_app_toasts'; -import { useCreatePrebuiltRulesMutation } from '../api/hooks/use_create_prebuilt_rules_mutation'; +import { + CREATE_PREBUILT_RULES_MUTATION_KEY, + useCreatePrebuiltRulesMutation, +} from '../api/hooks/use_create_prebuilt_rules_mutation'; import * as i18n from './translations'; export const useInstallPrePackagedRules = () => { @@ -21,6 +25,11 @@ export const useInstallPrePackagedRules = () => { }); }; +export const useIsInstallingPrePackagedRules = () => { + const mutationsCount = useIsMutating(CREATE_PREBUILT_RULES_MUTATION_KEY); + return mutationsCount > 0; +}; + const getSuccessToastMessage = (result: { rules_installed: number; rules_updated: number; diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_upgrade_secuirty_packages.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_upgrade_secuirty_packages.test.tsx similarity index 57% rename from x-pack/plugins/security_solution/public/common/hooks/use_upgrade_secuirty_packages.test.tsx rename to x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_upgrade_secuirty_packages.test.tsx index 0352dd03bbcff..dd01465b8875a 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/use_upgrade_secuirty_packages.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_upgrade_secuirty_packages.test.tsx @@ -5,14 +5,13 @@ * 2.0. */ -import React, { memo } from 'react'; -import { KibanaServices, useKibana } from '../lib/kibana'; -import type { RenderHookResult } from '@testing-library/react-hooks'; -import { renderHook as _renderHook } from '@testing-library/react-hooks'; -import { useUpgradeSecurityPackages } from './use_upgrade_security_packages'; import { epmRouteService } from '@kbn/fleet-plugin/common'; +import { renderHook } from '@testing-library/react-hooks'; +import { useKibana, KibanaServices } from '../../../common/lib/kibana'; +import { TestProviders } from '../../../common/mock'; +import { useUpgradeSecurityPackages } from './use_upgrade_security_packages'; -jest.mock('../components/user_privileges', () => { +jest.mock('../../../common/components/user_privileges', () => { return { useUserPrivileges: jest.fn().mockReturnValue({ endpointPrivileges: { @@ -21,48 +20,30 @@ jest.mock('../components/user_privileges', () => { }), }; }); -jest.mock('../lib/kibana'); +jest.mock('../../../common/lib/kibana'); -describe('When using the `useUpgradeSecurityPackages()` hook', () => { - const mockGetPrebuiltRulesPackageVersion = - KibanaServices.getPrebuiltRulesPackageVersion as jest.Mock; - const mockGetKibanaVersion = KibanaServices.getKibanaVersion as jest.Mock; - const mockGetKibanaBranch = KibanaServices.getKibanaBranch as jest.Mock; - let renderResult: RenderHookResult; - let renderHook: () => RenderHookResult; - let kibana: ReturnType; - - // eslint-disable-next-line react/display-name - const Wrapper = memo(({ children }) => { - kibana = useKibana(); - return <>{children}; - }); +const mockGetPrebuiltRulesPackageVersion = + KibanaServices.getPrebuiltRulesPackageVersion as jest.Mock; +const mockGetKibanaVersion = KibanaServices.getKibanaVersion as jest.Mock; +const mockGetKibanaBranch = KibanaServices.getKibanaBranch as jest.Mock; +const useKibanaMock = useKibana as jest.MockedFunction; +describe('When using the `useUpgradeSecurityPackages()` hook', () => { beforeEach(() => { - renderHook = () => { - renderResult = _renderHook(() => useUpgradeSecurityPackages(), { wrapper: Wrapper }); - return renderResult; - }; - }); - - afterEach(() => { jest.clearAllMocks(); - if (renderResult) { - renderResult.unmount(); - } }); it('should call fleet setup first via `isInitialized()` and then send upgrade request', async () => { - renderHook(); + const { waitFor } = renderHook(() => useUpgradeSecurityPackages(), { + wrapper: TestProviders, + }); - expect(kibana.services.fleet?.isInitialized).toHaveBeenCalled(); - expect(kibana.services.http.post).not.toHaveBeenCalled(); + expect(useKibanaMock().services.fleet?.isInitialized).toHaveBeenCalled(); + expect(useKibanaMock().services.http.post).not.toHaveBeenCalled(); - await renderResult.waitFor( - () => (kibana.services.http.post as jest.Mock).mock.calls.length > 0 - ); + await waitFor(() => (useKibanaMock().services.http.post as jest.Mock).mock.calls.length > 0); - expect(kibana.services.http.post).toHaveBeenCalledWith( + expect(useKibanaMock().services.http.post).toHaveBeenCalledWith( `${epmRouteService.getBulkInstallPath()}`, expect.objectContaining({ body: '{"packages":["endpoint","security_detection_engine"]}', @@ -74,13 +55,13 @@ describe('When using the `useUpgradeSecurityPackages()` hook', () => { mockGetKibanaVersion.mockReturnValue('8.0.0'); mockGetKibanaBranch.mockReturnValue('release'); - renderHook(); + const { waitFor } = renderHook(() => useUpgradeSecurityPackages(), { + wrapper: TestProviders, + }); - await renderResult.waitFor( - () => (kibana.services.http.post as jest.Mock).mock.calls.length > 0 - ); + await waitFor(() => (useKibanaMock().services.http.post as jest.Mock).mock.calls.length > 0); - expect(kibana.services.http.post).toHaveBeenCalledWith( + expect(useKibanaMock().services.http.post).toHaveBeenCalledWith( `${epmRouteService.getBulkInstallPath()}`, expect.objectContaining({ body: '{"packages":["endpoint","security_detection_engine"]}', @@ -93,13 +74,13 @@ describe('When using the `useUpgradeSecurityPackages()` hook', () => { mockGetKibanaVersion.mockReturnValue('8.0.0-SNAPSHOT'); mockGetKibanaBranch.mockReturnValue('main'); - renderHook(); + const { waitFor } = renderHook(() => useUpgradeSecurityPackages(), { + wrapper: TestProviders, + }); - await renderResult.waitFor( - () => (kibana.services.http.post as jest.Mock).mock.calls.length > 0 - ); + await waitFor(() => (useKibanaMock().services.http.post as jest.Mock).mock.calls.length > 0); - expect(kibana.services.http.post).toHaveBeenCalledWith( + expect(useKibanaMock().services.http.post).toHaveBeenCalledWith( `${epmRouteService.getBulkInstallPath()}`, expect.objectContaining({ body: '{"packages":["endpoint","security_detection_engine"]}', @@ -112,13 +93,13 @@ describe('When using the `useUpgradeSecurityPackages()` hook', () => { mockGetKibanaVersion.mockReturnValue('8.0.0-SNAPSHOT'); mockGetKibanaBranch.mockReturnValue('release'); - renderHook(); + const { waitFor } = renderHook(() => useUpgradeSecurityPackages(), { + wrapper: TestProviders, + }); - await renderResult.waitFor( - () => (kibana.services.http.post as jest.Mock).mock.calls.length > 0 - ); + await waitFor(() => (useKibanaMock().services.http.post as jest.Mock).mock.calls.length > 0); - expect(kibana.services.http.post).toHaveBeenCalledWith( + expect(useKibanaMock().services.http.post).toHaveBeenCalledWith( `${epmRouteService.getBulkInstallPath()}`, expect.objectContaining({ body: '{"packages":["endpoint","security_detection_engine"]}', @@ -131,13 +112,13 @@ describe('When using the `useUpgradeSecurityPackages()` hook', () => { mockGetKibanaVersion.mockReturnValue('8.0.0'); mockGetKibanaBranch.mockReturnValue('main'); - renderHook(); + const { waitFor } = renderHook(() => useUpgradeSecurityPackages(), { + wrapper: TestProviders, + }); - await renderResult.waitFor( - () => (kibana.services.http.post as jest.Mock).mock.calls.length > 0 - ); + await waitFor(() => (useKibanaMock().services.http.post as jest.Mock).mock.calls.length > 0); - expect(kibana.services.http.post).toHaveBeenCalledWith( + expect(useKibanaMock().services.http.post).toHaveBeenCalledWith( `${epmRouteService.getBulkInstallPath()}`, expect.objectContaining({ body: '{"packages":["endpoint","security_detection_engine"]}', @@ -149,18 +130,18 @@ describe('When using the `useUpgradeSecurityPackages()` hook', () => { it('should send separate upgrade requests if prebuiltRulesPackageVersion is provided', async () => { mockGetPrebuiltRulesPackageVersion.mockReturnValue('8.2.1'); - renderHook(); + const { waitFor } = renderHook(() => useUpgradeSecurityPackages(), { + wrapper: TestProviders, + }); - await renderResult.waitFor( - () => (kibana.services.http.post as jest.Mock).mock.calls.length > 0 - ); + await waitFor(() => (useKibanaMock().services.http.post as jest.Mock).mock.calls.length > 0); - expect(kibana.services.http.post).toHaveBeenNthCalledWith( + expect(useKibanaMock().services.http.post).toHaveBeenNthCalledWith( 1, `${epmRouteService.getInstallPath('security_detection_engine', '8.2.1')}`, expect.objectContaining({ query: { prerelease: true } }) ); - expect(kibana.services.http.post).toHaveBeenNthCalledWith( + expect(useKibanaMock().services.http.post).toHaveBeenNthCalledWith( 2, `${epmRouteService.getBulkInstallPath()}`, expect.objectContaining({ diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_upgrade_security_packages.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_upgrade_security_packages.ts new file mode 100644 index 0000000000000..296d041002f21 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_upgrade_security_packages.ts @@ -0,0 +1,95 @@ +/* + * 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 { useIsMutating } from '@tanstack/react-query'; +import { useEffect } from 'react'; +import { PREBUILT_RULES_PACKAGE_NAME } from '../../../../common/detection_engine/constants'; +import { useUserPrivileges } from '../../../common/components/user_privileges'; +import { KibanaServices, useKibana } from '../../../common/lib/kibana'; +import type { BulkInstallFleetPackagesProps, InstallFleetPackageProps } from '../api/api'; +import { + BULK_INSTALL_FLEET_PACKAGES_MUTATION_KEY, + useBulkInstallFleetPackagesMutation, +} from '../api/hooks/use_bulk_install_fleet_packages_mutation'; +import { + INSTALL_FLEET_PACKAGE_MUTATION_KEY, + useInstallFleetPackageMutation, +} from '../api/hooks/use_install_fleet_package_mutation'; + +/** + * Install or upgrade the security packages (endpoint and prebuilt rules) + */ +export const useUpgradeSecurityPackages = () => { + const context = useKibana(); + const canAccessFleet = useUserPrivileges().endpointPrivileges.canAccessFleet; + const { mutate: bulkInstallFleetPackages } = useBulkInstallFleetPackagesMutation(); + const { mutate: installFleetPackage } = useInstallFleetPackageMutation(); + + useEffect(() => { + if (!canAccessFleet) { + return; + } + + (async () => { + // Make sure fleet is initialized first + await context.services.fleet?.isInitialized(); + + // Always install the latest package if in dev env or snapshot build + const prerelease = + KibanaServices.getKibanaVersion().includes('-SNAPSHOT') || + KibanaServices.getKibanaBranch() === 'main'; + + const prebuiltRulesPackageVersion = KibanaServices.getPrebuiltRulesPackageVersion(); + // ignore the response for now since we aren't notifying the user + const packages = ['endpoint', PREBUILT_RULES_PACKAGE_NAME]; + + // If `prebuiltRulesPackageVersion` is provided, try to install that version + // Must be done as two separate requests as bulk API doesn't support versions + if (prebuiltRulesPackageVersion != null) { + installFleetPackage({ + packageName: PREBUILT_RULES_PACKAGE_NAME, + packageVersion: prebuiltRulesPackageVersion, + prerelease, + force: true, + }); + packages.splice(packages.indexOf(PREBUILT_RULES_PACKAGE_NAME), 1); + } + + // Note: if `prerelease:true` option is provided, endpoint package will also be installed as prerelease + bulkInstallFleetPackages({ + packages, + prerelease, + }); + })(); + }, [bulkInstallFleetPackages, canAccessFleet, context.services.fleet, installFleetPackage]); +}; + +/** + * @returns true if the security packages are being installed or upgraded + */ +export const useIsUpgradingSecurityPackages = () => { + const isInstallingPackages = useIsMutating({ + predicate: ({ options }) => { + const { mutationKey, variables } = options; + + // The mutation is bulk Fleet packages installation. Check if the packages include the prebuilt rules package + if (mutationKey === BULK_INSTALL_FLEET_PACKAGES_MUTATION_KEY) { + return (variables as BulkInstallFleetPackagesProps).packages.includes( + PREBUILT_RULES_PACKAGE_NAME + ); + } + + // The mutation is single Fleet package installation. Check if the package is the prebuilt rules package + if (mutationKey === INSTALL_FLEET_PACKAGE_MUTATION_KEY) { + return (variables as InstallFleetPackageProps).packageName === PREBUILT_RULES_PACKAGE_NAME; + } + return false; + }, + }); + + return isInstallingPackages > 0; +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_tables.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_tables.tsx index bbe5659e7481b..31ae3af97f0ae 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_tables.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_tables.tsx @@ -40,6 +40,7 @@ import { useStartMlJobs } from '../../../rule_management/logic/use_start_ml_jobs import { RULES_TABLE_PAGE_SIZE_OPTIONS } from './constants'; import { useRuleManagementFilters } from '../../../rule_management/logic/use_rule_management_filters'; import type { FindRulesSortField } from '../../../../../common/detection_engine/rule_management'; +import { useIsUpgradingSecurityPackages } from '../../../rule_management/logic/use_upgrade_security_packages'; const INITIAL_SORT_FIELD = 'enabled'; @@ -63,6 +64,7 @@ const NO_ITEMS_MESSAGE = ( export const RulesTables = React.memo(({ selectedTab }) => { const [{ canUserCRUD }] = useUserData(); const hasPermissions = hasUserCRUDPermission(canUserCRUD); + const isUpgradingSecurityPackages = useIsUpgradingSecurityPackages(); const tableRef = useRef(null); const rulesTableContext = useRulesTableContext(); @@ -227,7 +229,7 @@ export const RulesTables = React.memo(({ selectedTab }) => { } : { 'data-test-subj': 'monitoring-table', columns: monitoringColumns }; - const shouldShowLinearProgress = isFetched && isRefetching; + const shouldShowLinearProgress = (isFetched && isRefetching) || isUpgradingSecurityPackages; const shouldShowLoadingOverlay = (!isFetched && isRefetching) || isPreflightInProgress; return ( diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_prepackaged_rules.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_prepackaged_rules.tsx index e5d05d7e7fbb0..9ec37ecfcb7c0 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_prepackaged_rules.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_prepackaged_rules.tsx @@ -11,6 +11,7 @@ import { useBoolState } from '../../../../common/hooks/use_bool_state'; import { RULES_TABLE_ACTIONS } from '../../../../common/lib/apm/user_actions'; import { useStartTransaction } from '../../../../common/lib/apm/use_start_transaction'; import { useCreatePrePackagedRules } from '../../../../detection_engine/rule_management/logic/use_create_pre_packaged_rules'; +import { useIsInstallingPrePackagedRules } from '../../../../detection_engine/rule_management/logic/use_install_pre_packaged_rules'; import { usePrePackagedRulesStatus } from '../../../../detection_engine/rule_management/logic/use_pre_packaged_rules_status'; import { affectedJobIds } from '../../callouts/ml_job_compatibility_callout/affected_job_ids'; import { MlJobUpgradeModal } from '../../modals/ml_job_upgrade_modal'; @@ -27,11 +28,8 @@ interface LoadPrePackagedRulesProps { export const LoadPrePackagedRules = ({ children }: LoadPrePackagedRulesProps) => { const { isFetching: isFetchingPrepackagedStatus } = usePrePackagedRulesStatus(); - const { - createPrePackagedRules, - canCreatePrePackagedRules, - isLoading: loadingCreatePrePackagedRules, - } = useCreatePrePackagedRules(); + const isInstallingPrebuiltRules = useIsInstallingPrePackagedRules(); + const { createPrePackagedRules, canCreatePrePackagedRules } = useCreatePrePackagedRules(); const { startTransaction } = useStartTransaction(); const handleCreatePrePackagedRules = useCallback(async () => { @@ -63,7 +61,7 @@ export const LoadPrePackagedRules = ({ children }: LoadPrePackagedRulesProps) => return ( <> {children({ - isLoading: loadingCreatePrePackagedRules, + isLoading: isInstallingPrebuiltRules, isDisabled, onClick: handleInstallPrePackagedRules, })} From 745e9ad9d7d1a3958842bfe63343e2e4d00041f6 Mon Sep 17 00:00:00 2001 From: Dzmitry Lemechko Date: Mon, 13 Feb 2023 16:06:11 +0100 Subject: [PATCH 07/20] [ftr] split x-pack api integration tests based on plugin (#150837) ## Summary Currently we run all x-pack api integration tests as a single piece (config) and it takes on average **33+ minutes** If a single test fails, buildkite retries the config and you have to wait another 30+ minutes to see if test passed (flaky test sign) or failed (PR broke test) image Splitting config into many small ones will not only speedup overall CI run (configs will be assigned to different workers based on its historical run time) but also speedup retry by running only a sub set of tests related to the particular config file. --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .buildkite/ftr_configs.yml | 32 +++++++++++++- .../test/api_integration/apis/aiops/config.ts | 17 ++++++++ .../test/api_integration/apis/cases/config.ts | 17 ++++++++ .../apis/cloud_security_posture/config.ts | 17 ++++++++ .../api_integration/apis/console/config.ts | 17 ++++++++ x-pack/test/api_integration/apis/es/config.ts | 17 ++++++++ .../api_integration/apis/features/config.ts | 17 ++++++++ .../apis/file_upload/config.ts | 17 ++++++++ x-pack/test/api_integration/apis/index.ts | 43 ------------------- .../api_integration/apis/kibana/config.ts | 17 ++++++++ .../test/api_integration/apis/lists/config.ts | 17 ++++++++ .../api_integration/apis/logs_ui/config.ts | 17 ++++++++ .../api_integration/apis/logstash/config.ts | 17 ++++++++ .../api_integration/apis/management/config.ts | 17 ++++++++ .../test/api_integration/apis/maps/config.ts | 17 ++++++++ .../api_integration/apis/metrics_ui/config.ts | 17 ++++++++ x-pack/test/api_integration/apis/ml/config.ts | 17 ++++++++ .../api_integration/apis/monitoring/config.ts | 17 ++++++++ .../apis/monitoring_collection/config.ts | 17 ++++++++ .../api_integration/apis/osquery/config.ts | 17 ++++++++ .../apis/painless_lab/config.ts | 17 ++++++++ .../api_integration/apis/search/config.ts | 17 ++++++++ .../apis/searchprofiler/config.ts | 17 ++++++++ .../api_integration/apis/security/config.ts | 17 ++++++++ .../apis/security_solution/config.ts | 17 ++++++++ .../api_integration/apis/spaces/config.ts | 17 ++++++++ .../api_integration/apis/synthetics/config.ts | 17 ++++++++ .../api_integration/apis/telemetry/config.ts | 17 ++++++++ .../api_integration/apis/transform/config.ts | 17 ++++++++ .../apis/upgrade_assistant/config.ts | 17 ++++++++ .../api_integration/apis/uptime/config.ts | 17 ++++++++ .../monitor_states_real_data.snap | 2 +- .../api_integration/apis/watcher/config.ts | 17 ++++++++ x-pack/test/api_integration/config.ts | 1 - .../api_integration/config_security_basic.ts | 32 ++++++++------ .../api_integration/config_security_trial.ts | 16 ++++--- 36 files changed, 570 insertions(+), 66 deletions(-) create mode 100644 x-pack/test/api_integration/apis/aiops/config.ts create mode 100644 x-pack/test/api_integration/apis/cases/config.ts create mode 100644 x-pack/test/api_integration/apis/cloud_security_posture/config.ts create mode 100644 x-pack/test/api_integration/apis/console/config.ts create mode 100644 x-pack/test/api_integration/apis/es/config.ts create mode 100644 x-pack/test/api_integration/apis/features/config.ts create mode 100644 x-pack/test/api_integration/apis/file_upload/config.ts delete mode 100644 x-pack/test/api_integration/apis/index.ts create mode 100644 x-pack/test/api_integration/apis/kibana/config.ts create mode 100644 x-pack/test/api_integration/apis/lists/config.ts create mode 100644 x-pack/test/api_integration/apis/logs_ui/config.ts create mode 100644 x-pack/test/api_integration/apis/logstash/config.ts create mode 100644 x-pack/test/api_integration/apis/management/config.ts create mode 100644 x-pack/test/api_integration/apis/maps/config.ts create mode 100644 x-pack/test/api_integration/apis/metrics_ui/config.ts create mode 100644 x-pack/test/api_integration/apis/ml/config.ts create mode 100644 x-pack/test/api_integration/apis/monitoring/config.ts create mode 100644 x-pack/test/api_integration/apis/monitoring_collection/config.ts create mode 100644 x-pack/test/api_integration/apis/osquery/config.ts create mode 100644 x-pack/test/api_integration/apis/painless_lab/config.ts create mode 100644 x-pack/test/api_integration/apis/search/config.ts create mode 100644 x-pack/test/api_integration/apis/searchprofiler/config.ts create mode 100644 x-pack/test/api_integration/apis/security/config.ts create mode 100644 x-pack/test/api_integration/apis/security_solution/config.ts create mode 100644 x-pack/test/api_integration/apis/spaces/config.ts create mode 100644 x-pack/test/api_integration/apis/synthetics/config.ts create mode 100644 x-pack/test/api_integration/apis/telemetry/config.ts create mode 100644 x-pack/test/api_integration/apis/transform/config.ts create mode 100644 x-pack/test/api_integration/apis/upgrade_assistant/config.ts create mode 100644 x-pack/test/api_integration/apis/uptime/config.ts create mode 100644 x-pack/test/api_integration/apis/watcher/config.ts diff --git a/.buildkite/ftr_configs.yml b/.buildkite/ftr_configs.yml index 27516d92de621..2bfcce064d5ce 100644 --- a/.buildkite/ftr_configs.yml +++ b/.buildkite/ftr_configs.yml @@ -11,6 +11,7 @@ disabled: - x-pack/test/localization/config.base.ts - test/server_integration/config.base.js - x-pack/test/functional_with_es_ssl/config.base.ts + - x-pack/test/api_integration/config.ts # QA suites that are run out-of-band - x-pack/test/stack_functional_integration/configs/config.stack_functional_integration_base.js @@ -143,7 +144,36 @@ enabled: - x-pack/test/api_integration_basic/config.ts - x-pack/test/api_integration/config_security_basic.ts - x-pack/test/api_integration/config_security_trial.ts - - x-pack/test/api_integration/config.ts + - x-pack/test/api_integration/apis/aiops/config.ts + - x-pack/test/api_integration/apis/cases/config.ts + - x-pack/test/api_integration/apis/cloud_security_posture/config.ts + - x-pack/test/api_integration/apis/console/config.ts + - x-pack/test/api_integration/apis/es/config.ts + - x-pack/test/api_integration/apis/features/config.ts + - x-pack/test/api_integration/apis/file_upload/config.ts + - x-pack/test/api_integration/apis/kibana/config.ts + - x-pack/test/api_integration/apis/lists/config.ts + - x-pack/test/api_integration/apis/logs_ui/config.ts + - x-pack/test/api_integration/apis/logstash/config.ts + - x-pack/test/api_integration/apis/management/config.ts + - x-pack/test/api_integration/apis/maps/config.ts + - x-pack/test/api_integration/apis/metrics_ui/config.ts + - x-pack/test/api_integration/apis/ml/config.ts + - x-pack/test/api_integration/apis/monitoring/config.ts + - x-pack/test/api_integration/apis/monitoring_collection/config.ts + - x-pack/test/api_integration/apis/osquery/config.ts + - x-pack/test/api_integration/apis/painless_lab/config.ts + - x-pack/test/api_integration/apis/search/config.ts + - x-pack/test/api_integration/apis/searchprofiler/config.ts + - x-pack/test/api_integration/apis/security/config.ts + - x-pack/test/api_integration/apis/security_solution/config.ts + - x-pack/test/api_integration/apis/spaces/config.ts + - x-pack/test/api_integration/apis/synthetics/config.ts + - x-pack/test/api_integration/apis/telemetry/config.ts + - x-pack/test/api_integration/apis/transform/config.ts + - x-pack/test/api_integration/apis/upgrade_assistant/config.ts + - x-pack/test/api_integration/apis/uptime/config.ts + - x-pack/test/api_integration/apis/watcher/config.ts - x-pack/test/apm_api_integration/basic/config.ts - x-pack/test/apm_api_integration/rules/config.ts - x-pack/test/apm_api_integration/trial/config.ts diff --git a/x-pack/test/api_integration/apis/aiops/config.ts b/x-pack/test/api_integration/apis/aiops/config.ts new file mode 100644 index 0000000000000..5f335f116fefe --- /dev/null +++ b/x-pack/test/api_integration/apis/aiops/config.ts @@ -0,0 +1,17 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const baseIntegrationTestsConfig = await readConfigFile(require.resolve('../../config.ts')); + + return { + ...baseIntegrationTestsConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/api_integration/apis/cases/config.ts b/x-pack/test/api_integration/apis/cases/config.ts new file mode 100644 index 0000000000000..5f335f116fefe --- /dev/null +++ b/x-pack/test/api_integration/apis/cases/config.ts @@ -0,0 +1,17 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const baseIntegrationTestsConfig = await readConfigFile(require.resolve('../../config.ts')); + + return { + ...baseIntegrationTestsConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/api_integration/apis/cloud_security_posture/config.ts b/x-pack/test/api_integration/apis/cloud_security_posture/config.ts new file mode 100644 index 0000000000000..5f335f116fefe --- /dev/null +++ b/x-pack/test/api_integration/apis/cloud_security_posture/config.ts @@ -0,0 +1,17 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const baseIntegrationTestsConfig = await readConfigFile(require.resolve('../../config.ts')); + + return { + ...baseIntegrationTestsConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/api_integration/apis/console/config.ts b/x-pack/test/api_integration/apis/console/config.ts new file mode 100644 index 0000000000000..5f335f116fefe --- /dev/null +++ b/x-pack/test/api_integration/apis/console/config.ts @@ -0,0 +1,17 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const baseIntegrationTestsConfig = await readConfigFile(require.resolve('../../config.ts')); + + return { + ...baseIntegrationTestsConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/api_integration/apis/es/config.ts b/x-pack/test/api_integration/apis/es/config.ts new file mode 100644 index 0000000000000..5f335f116fefe --- /dev/null +++ b/x-pack/test/api_integration/apis/es/config.ts @@ -0,0 +1,17 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const baseIntegrationTestsConfig = await readConfigFile(require.resolve('../../config.ts')); + + return { + ...baseIntegrationTestsConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/api_integration/apis/features/config.ts b/x-pack/test/api_integration/apis/features/config.ts new file mode 100644 index 0000000000000..5f335f116fefe --- /dev/null +++ b/x-pack/test/api_integration/apis/features/config.ts @@ -0,0 +1,17 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const baseIntegrationTestsConfig = await readConfigFile(require.resolve('../../config.ts')); + + return { + ...baseIntegrationTestsConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/api_integration/apis/file_upload/config.ts b/x-pack/test/api_integration/apis/file_upload/config.ts new file mode 100644 index 0000000000000..5f335f116fefe --- /dev/null +++ b/x-pack/test/api_integration/apis/file_upload/config.ts @@ -0,0 +1,17 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const baseIntegrationTestsConfig = await readConfigFile(require.resolve('../../config.ts')); + + return { + ...baseIntegrationTestsConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/api_integration/apis/index.ts b/x-pack/test/api_integration/apis/index.ts deleted file mode 100644 index 0278c439c5995..0000000000000 --- a/x-pack/test/api_integration/apis/index.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { FtrProviderContext } from '../ftr_provider_context'; - -export default function ({ loadTestFile }: FtrProviderContext) { - describe('apis', function () { - loadTestFile(require.resolve('./search')); - loadTestFile(require.resolve('./es')); - loadTestFile(require.resolve('./security')); - loadTestFile(require.resolve('./spaces')); - loadTestFile(require.resolve('./monitoring')); - loadTestFile(require.resolve('./features')); - loadTestFile(require.resolve('./telemetry')); - loadTestFile(require.resolve('./logstash')); - loadTestFile(require.resolve('./kibana')); - loadTestFile(require.resolve('./metrics_ui')); - loadTestFile(require.resolve('./console')); - loadTestFile(require.resolve('./management')); - loadTestFile(require.resolve('./uptime')); - loadTestFile(require.resolve('./synthetics')); - loadTestFile(require.resolve('./maps')); - loadTestFile(require.resolve('./security_solution')); - loadTestFile(require.resolve('./transform')); - loadTestFile(require.resolve('./lists')); - loadTestFile(require.resolve('./upgrade_assistant')); - loadTestFile(require.resolve('./searchprofiler')); - loadTestFile(require.resolve('./painless_lab')); - loadTestFile(require.resolve('./file_upload')); - loadTestFile(require.resolve('./aiops')); - loadTestFile(require.resolve('./ml')); - loadTestFile(require.resolve('./watcher')); - loadTestFile(require.resolve('./logs_ui')); - loadTestFile(require.resolve('./osquery')); - loadTestFile(require.resolve('./cases')); - loadTestFile(require.resolve('./monitoring_collection')); - loadTestFile(require.resolve('./cloud_security_posture')); - }); -} diff --git a/x-pack/test/api_integration/apis/kibana/config.ts b/x-pack/test/api_integration/apis/kibana/config.ts new file mode 100644 index 0000000000000..5f335f116fefe --- /dev/null +++ b/x-pack/test/api_integration/apis/kibana/config.ts @@ -0,0 +1,17 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const baseIntegrationTestsConfig = await readConfigFile(require.resolve('../../config.ts')); + + return { + ...baseIntegrationTestsConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/api_integration/apis/lists/config.ts b/x-pack/test/api_integration/apis/lists/config.ts new file mode 100644 index 0000000000000..5f335f116fefe --- /dev/null +++ b/x-pack/test/api_integration/apis/lists/config.ts @@ -0,0 +1,17 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const baseIntegrationTestsConfig = await readConfigFile(require.resolve('../../config.ts')); + + return { + ...baseIntegrationTestsConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/api_integration/apis/logs_ui/config.ts b/x-pack/test/api_integration/apis/logs_ui/config.ts new file mode 100644 index 0000000000000..5f335f116fefe --- /dev/null +++ b/x-pack/test/api_integration/apis/logs_ui/config.ts @@ -0,0 +1,17 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const baseIntegrationTestsConfig = await readConfigFile(require.resolve('../../config.ts')); + + return { + ...baseIntegrationTestsConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/api_integration/apis/logstash/config.ts b/x-pack/test/api_integration/apis/logstash/config.ts new file mode 100644 index 0000000000000..5f335f116fefe --- /dev/null +++ b/x-pack/test/api_integration/apis/logstash/config.ts @@ -0,0 +1,17 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const baseIntegrationTestsConfig = await readConfigFile(require.resolve('../../config.ts')); + + return { + ...baseIntegrationTestsConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/api_integration/apis/management/config.ts b/x-pack/test/api_integration/apis/management/config.ts new file mode 100644 index 0000000000000..5f335f116fefe --- /dev/null +++ b/x-pack/test/api_integration/apis/management/config.ts @@ -0,0 +1,17 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const baseIntegrationTestsConfig = await readConfigFile(require.resolve('../../config.ts')); + + return { + ...baseIntegrationTestsConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/api_integration/apis/maps/config.ts b/x-pack/test/api_integration/apis/maps/config.ts new file mode 100644 index 0000000000000..5f335f116fefe --- /dev/null +++ b/x-pack/test/api_integration/apis/maps/config.ts @@ -0,0 +1,17 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const baseIntegrationTestsConfig = await readConfigFile(require.resolve('../../config.ts')); + + return { + ...baseIntegrationTestsConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/api_integration/apis/metrics_ui/config.ts b/x-pack/test/api_integration/apis/metrics_ui/config.ts new file mode 100644 index 0000000000000..5f335f116fefe --- /dev/null +++ b/x-pack/test/api_integration/apis/metrics_ui/config.ts @@ -0,0 +1,17 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const baseIntegrationTestsConfig = await readConfigFile(require.resolve('../../config.ts')); + + return { + ...baseIntegrationTestsConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/api_integration/apis/ml/config.ts b/x-pack/test/api_integration/apis/ml/config.ts new file mode 100644 index 0000000000000..5f335f116fefe --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/config.ts @@ -0,0 +1,17 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const baseIntegrationTestsConfig = await readConfigFile(require.resolve('../../config.ts')); + + return { + ...baseIntegrationTestsConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/api_integration/apis/monitoring/config.ts b/x-pack/test/api_integration/apis/monitoring/config.ts new file mode 100644 index 0000000000000..5f335f116fefe --- /dev/null +++ b/x-pack/test/api_integration/apis/monitoring/config.ts @@ -0,0 +1,17 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const baseIntegrationTestsConfig = await readConfigFile(require.resolve('../../config.ts')); + + return { + ...baseIntegrationTestsConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/api_integration/apis/monitoring_collection/config.ts b/x-pack/test/api_integration/apis/monitoring_collection/config.ts new file mode 100644 index 0000000000000..5f335f116fefe --- /dev/null +++ b/x-pack/test/api_integration/apis/monitoring_collection/config.ts @@ -0,0 +1,17 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const baseIntegrationTestsConfig = await readConfigFile(require.resolve('../../config.ts')); + + return { + ...baseIntegrationTestsConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/api_integration/apis/osquery/config.ts b/x-pack/test/api_integration/apis/osquery/config.ts new file mode 100644 index 0000000000000..5f335f116fefe --- /dev/null +++ b/x-pack/test/api_integration/apis/osquery/config.ts @@ -0,0 +1,17 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const baseIntegrationTestsConfig = await readConfigFile(require.resolve('../../config.ts')); + + return { + ...baseIntegrationTestsConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/api_integration/apis/painless_lab/config.ts b/x-pack/test/api_integration/apis/painless_lab/config.ts new file mode 100644 index 0000000000000..5f335f116fefe --- /dev/null +++ b/x-pack/test/api_integration/apis/painless_lab/config.ts @@ -0,0 +1,17 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const baseIntegrationTestsConfig = await readConfigFile(require.resolve('../../config.ts')); + + return { + ...baseIntegrationTestsConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/api_integration/apis/search/config.ts b/x-pack/test/api_integration/apis/search/config.ts new file mode 100644 index 0000000000000..5f335f116fefe --- /dev/null +++ b/x-pack/test/api_integration/apis/search/config.ts @@ -0,0 +1,17 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const baseIntegrationTestsConfig = await readConfigFile(require.resolve('../../config.ts')); + + return { + ...baseIntegrationTestsConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/api_integration/apis/searchprofiler/config.ts b/x-pack/test/api_integration/apis/searchprofiler/config.ts new file mode 100644 index 0000000000000..5f335f116fefe --- /dev/null +++ b/x-pack/test/api_integration/apis/searchprofiler/config.ts @@ -0,0 +1,17 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const baseIntegrationTestsConfig = await readConfigFile(require.resolve('../../config.ts')); + + return { + ...baseIntegrationTestsConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/api_integration/apis/security/config.ts b/x-pack/test/api_integration/apis/security/config.ts new file mode 100644 index 0000000000000..5f335f116fefe --- /dev/null +++ b/x-pack/test/api_integration/apis/security/config.ts @@ -0,0 +1,17 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const baseIntegrationTestsConfig = await readConfigFile(require.resolve('../../config.ts')); + + return { + ...baseIntegrationTestsConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/api_integration/apis/security_solution/config.ts b/x-pack/test/api_integration/apis/security_solution/config.ts new file mode 100644 index 0000000000000..5f335f116fefe --- /dev/null +++ b/x-pack/test/api_integration/apis/security_solution/config.ts @@ -0,0 +1,17 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const baseIntegrationTestsConfig = await readConfigFile(require.resolve('../../config.ts')); + + return { + ...baseIntegrationTestsConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/api_integration/apis/spaces/config.ts b/x-pack/test/api_integration/apis/spaces/config.ts new file mode 100644 index 0000000000000..5f335f116fefe --- /dev/null +++ b/x-pack/test/api_integration/apis/spaces/config.ts @@ -0,0 +1,17 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const baseIntegrationTestsConfig = await readConfigFile(require.resolve('../../config.ts')); + + return { + ...baseIntegrationTestsConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/api_integration/apis/synthetics/config.ts b/x-pack/test/api_integration/apis/synthetics/config.ts new file mode 100644 index 0000000000000..5f335f116fefe --- /dev/null +++ b/x-pack/test/api_integration/apis/synthetics/config.ts @@ -0,0 +1,17 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const baseIntegrationTestsConfig = await readConfigFile(require.resolve('../../config.ts')); + + return { + ...baseIntegrationTestsConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/api_integration/apis/telemetry/config.ts b/x-pack/test/api_integration/apis/telemetry/config.ts new file mode 100644 index 0000000000000..5f335f116fefe --- /dev/null +++ b/x-pack/test/api_integration/apis/telemetry/config.ts @@ -0,0 +1,17 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const baseIntegrationTestsConfig = await readConfigFile(require.resolve('../../config.ts')); + + return { + ...baseIntegrationTestsConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/api_integration/apis/transform/config.ts b/x-pack/test/api_integration/apis/transform/config.ts new file mode 100644 index 0000000000000..5f335f116fefe --- /dev/null +++ b/x-pack/test/api_integration/apis/transform/config.ts @@ -0,0 +1,17 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const baseIntegrationTestsConfig = await readConfigFile(require.resolve('../../config.ts')); + + return { + ...baseIntegrationTestsConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/api_integration/apis/upgrade_assistant/config.ts b/x-pack/test/api_integration/apis/upgrade_assistant/config.ts new file mode 100644 index 0000000000000..5f335f116fefe --- /dev/null +++ b/x-pack/test/api_integration/apis/upgrade_assistant/config.ts @@ -0,0 +1,17 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const baseIntegrationTestsConfig = await readConfigFile(require.resolve('../../config.ts')); + + return { + ...baseIntegrationTestsConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/api_integration/apis/uptime/config.ts b/x-pack/test/api_integration/apis/uptime/config.ts new file mode 100644 index 0000000000000..5f335f116fefe --- /dev/null +++ b/x-pack/test/api_integration/apis/uptime/config.ts @@ -0,0 +1,17 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const baseIntegrationTestsConfig = await readConfigFile(require.resolve('../../config.ts')); + + return { + ...baseIntegrationTestsConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/api_integration/apis/uptime/rest/__snapshots__/monitor_states_real_data.snap b/x-pack/test/api_integration/apis/uptime/rest/__snapshots__/monitor_states_real_data.snap index 81348cfa2bb28..5bdccdcbb0c49 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/__snapshots__/monitor_states_real_data.snap +++ b/x-pack/test/api_integration/apis/uptime/rest/__snapshots__/monitor_states_real_data.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`apis uptime uptime REST endpoints with real-world data monitor states endpoint will fetch monitor state data for the given down filters 1`] = ` +exports[`uptime uptime REST endpoints with real-world data monitor states endpoint will fetch monitor state data for the given down filters 1`] = ` Object { "nextPagePagination": "{\\"cursorDirection\\":\\"AFTER\\",\\"sortOrder\\":\\"ASC\\",\\"cursorKey\\":{\\"monitor_id\\":\\"0020-down\\"}}", "prevPagePagination": null, diff --git a/x-pack/test/api_integration/apis/watcher/config.ts b/x-pack/test/api_integration/apis/watcher/config.ts new file mode 100644 index 0000000000000..5f335f116fefe --- /dev/null +++ b/x-pack/test/api_integration/apis/watcher/config.ts @@ -0,0 +1,17 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const baseIntegrationTestsConfig = await readConfigFile(require.resolve('../../config.ts')); + + return { + ...baseIntegrationTestsConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/api_integration/config.ts b/x-pack/test/api_integration/config.ts index cf87502f5c0c7..5766a9efdf982 100644 --- a/x-pack/test/api_integration/config.ts +++ b/x-pack/test/api_integration/config.ts @@ -14,7 +14,6 @@ export async function getApiIntegrationConfig({ readConfigFile }: FtrConfigProvi ); return { - testFiles: [require.resolve('./apis')], services, servers: xPackFunctionalTestsConfig.get('servers'), security: xPackFunctionalTestsConfig.get('security'), diff --git a/x-pack/test/api_integration/config_security_basic.ts b/x-pack/test/api_integration/config_security_basic.ts index fc32e66c63e9e..0164e8f54f822 100644 --- a/x-pack/test/api_integration/config_security_basic.ts +++ b/x-pack/test/api_integration/config_security_basic.ts @@ -8,19 +8,25 @@ /* eslint-disable import/no-default-export */ import { FtrConfigProviderContext } from '@kbn/test'; -import { default as createTestConfig } from './config'; -export default async function (context: FtrConfigProviderContext) { +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const baseIntegrationTestsConfig = await readConfigFile(require.resolve('./config.ts')); // security APIs should function the same under a basic or trial license - return createTestConfig(context).then((config) => { - config.esTestCluster.license = 'basic'; - config.esTestCluster.serverArgs = [ - 'xpack.license.self_generated.type=basic', - 'xpack.security.enabled=true', - 'xpack.security.authc.api_key.enabled=true', - ]; - config.testFiles = [require.resolve('./apis/security/security_basic')]; - config.junit.reportName = 'X-Pack API Integration Tests (Security Basic)'; - return config; - }); + return { + ...baseIntegrationTestsConfig.getAll(), + testFiles: [require.resolve('./apis/security/security_basic')], + esTestCluster: { + ...baseIntegrationTestsConfig.get('esTestCluster'), + license: 'basic', + serverArgs: [ + ...baseIntegrationTestsConfig.get('esTestCluster.serverArgs'), + 'xpack.license.self_generated.type=basic', + 'xpack.security.enabled=true', + 'xpack.security.authc.api_key.enabled=true', + ], + }, + junit: { + reportName: 'X-Pack API Integration Tests (Security Basic)', + }, + }; } diff --git a/x-pack/test/api_integration/config_security_trial.ts b/x-pack/test/api_integration/config_security_trial.ts index 93b1eefd350e9..4559dcd7b0ebb 100644 --- a/x-pack/test/api_integration/config_security_trial.ts +++ b/x-pack/test/api_integration/config_security_trial.ts @@ -8,12 +8,14 @@ /* eslint-disable import/no-default-export */ import { FtrConfigProviderContext } from '@kbn/test'; -import { default as createTestConfig } from './config'; -export default async function (context: FtrConfigProviderContext) { - return createTestConfig(context).then((config) => { - config.testFiles = [require.resolve('./apis/security/security_trial')]; - config.junit.reportName = 'X-Pack API Integration Tests (Security Trial)'; - return config; - }); +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const baseIntegrationTestsConfig = await readConfigFile(require.resolve('./config.ts')); + return { + ...baseIntegrationTestsConfig.getAll(), + testFiles: [require.resolve('./apis/security/security_trial')], + junit: { + reportName: 'X-Pack API Integration Tests (Security Trial)', + }, + }; } From 76dd2dcc2dbd0ee4bc7e140a293adce45688431d Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 13 Feb 2023 15:15:21 +0000 Subject: [PATCH 08/20] skip flaky suite (#150962) --- .../security_and_spaces/tests/trial/cases/push_case.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/cases/push_case.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/cases/push_case.ts index a18c24c62652d..d6d0919be8b4a 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/cases/push_case.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/cases/push_case.ts @@ -616,7 +616,8 @@ export default ({ getService }: FtrProviderContext): void => { }); }); - describe('alerts', () => { + // FLAKY: https://github.com/elastic/kibana/issues/150962 + describe.skip('alerts', () => { const defaultSignalsIndex = '.siem-signals-default-000001'; const signalID = '4679431ee0ba3209b6fcd60a255a696886fe0a7d18f5375de510ff5b68fa6b78'; const signalID2 = '1023bcfea939643c5e51fd8df53797e0ea693cee547db579ab56d96402365c1e'; From 075793031faf1a2de9aa57cf04a43dfa80b8448b Mon Sep 17 00:00:00 2001 From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> Date: Mon, 13 Feb 2023 10:36:35 -0500 Subject: [PATCH 09/20] [Security Solution][Endpoint] Update 'run endpoint' CLI tool so that the created Endpoint Policy has all protections enabled (#150789) ## Summary - Updated the Endpoint Run CLI tool to enable all policy protections whenever it creates a policy for testing. --- .../endpoint_agent_runner/elastic_endpoint.ts | 44 +++++++++++++++++-- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/elastic_endpoint.ts b/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/elastic_endpoint.ts index 7df5030a70091..e0938707ea5a0 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/elastic_endpoint.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/elastic_endpoint.ts @@ -8,8 +8,14 @@ import { userInfo } from 'os'; import execa from 'execa'; import nodeFetch from 'node-fetch'; -import { AGENT_POLICY_SAVED_OBJECT_TYPE } from '@kbn/fleet-plugin/common'; +import { + AGENT_POLICY_SAVED_OBJECT_TYPE, + packagePolicyRouteService, + type UpdatePackagePolicyResponse, + type UpdatePackagePolicy, +} from '@kbn/fleet-plugin/common'; import chalk from 'chalk'; +import { inspect } from 'util'; import { getEndpointPackageInfo } from '../../../common/endpoint/index_data'; import { indexFleetEndpointPolicy } from '../../../common/endpoint/data_loaders/index_fleet_endpoint_policy'; import { @@ -19,6 +25,7 @@ import { waitForHostToEnroll, } from '../common/fleet_services'; import { getRuntimeServices } from './runtime'; +import { type PolicyData, ProtectionModes } from '../../../common/endpoint/types'; interface ElasticArtifactSearchResponse { manifest: { @@ -153,7 +160,7 @@ export const enrollEndpointHost = async () => { Delete VM: ${chalk.bold(`multipass delete -p ${vmName}${await getVmCountNotice()}`)} `); } catch (error) { - log.error(error); + log.error(inspect(error, { depth: 4 })); log.indent(-4); throw error; } @@ -215,9 +222,40 @@ const getOrCreateAgentPolicyId = async (): Promise => { endpointPackageVersion, agentPolicyName ); - const agentPolicy = response.agentPolicies[0]; + // Update the Endpoint integration policy to enable all protections + // eslint-disable-next-line @typescript-eslint/naming-convention + const { created_by, created_at, updated_at, updated_by, id, version, revision, ...restOfPolicy } = + response.integrationPolicies[0]; + const updatedEndpointPolicy: UpdatePackagePolicy = restOfPolicy; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const policy = updatedEndpointPolicy!.inputs[0]!.config!.policy.value; + + policy.mac.malware.mode = ProtectionModes.prevent; + policy.windows.malware.mode = ProtectionModes.prevent; + policy.linux.malware.mode = ProtectionModes.prevent; + + policy.mac.memory_protection.mode = ProtectionModes.prevent; + policy.windows.memory_protection.mode = ProtectionModes.prevent; + policy.linux.memory_protection.mode = ProtectionModes.prevent; + + policy.mac.behavior_protection.mode = ProtectionModes.prevent; + policy.windows.behavior_protection.mode = ProtectionModes.prevent; + policy.linux.behavior_protection.mode = ProtectionModes.prevent; + + policy.windows.ransomware.mode = ProtectionModes.prevent; + + response.integrationPolicies[0] = ( + await kbnClient + .request({ + method: 'PUT', + path: packagePolicyRouteService.getUpdatePath(response.integrationPolicies[0].id), + body: updatedEndpointPolicy, + }) + .then((res) => res.data) + ).item as PolicyData; + log.info(`New agent policy with Endpoint integration created: Name: ${agentPolicy.name} Id: ${agentPolicy.id}`); From 8f4e5c53ff57e7b32d7d52d9ed9528ea216aafd1 Mon Sep 17 00:00:00 2001 From: "Christiane (Tina) Heiligers" Date: Mon, 13 Feb 2023 09:32:08 -0700 Subject: [PATCH 10/20] [Saved Objects] Logs warning when deprecated SO APIs are called (#150775) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../src/routes/bulk_create.ts | 7 ++++- .../src/routes/bulk_delete.ts | 7 ++++- .../src/routes/bulk_get.ts | 5 +++- .../src/routes/bulk_resolve.ts | 7 ++++- .../src/routes/bulk_update.ts | 7 ++++- .../src/routes/create.ts | 5 +++- .../src/routes/delete.ts | 5 +++- .../src/routes/find.ts | 5 +++- .../src/routes/get.ts | 5 +++- .../src/routes/index.ts | 22 +++++++------- .../src/routes/resolve.ts | 7 ++++- .../src/routes/update.ts | 5 +++- .../saved_objects/routes/bulk_create.test.ts | 23 ++++++++++++++- .../saved_objects/routes/bulk_delete.test.ts | 19 +++++++++++- .../saved_objects/routes/bulk_get.test.ts | 19 +++++++++++- .../saved_objects/routes/bulk_resolve.test.ts | 19 +++++++++++- .../saved_objects/routes/bulk_update.test.ts | 29 ++++++++++++++++++- .../saved_objects/routes/create.test.ts | 18 +++++++++++- .../saved_objects/routes/delete.test.ts | 13 ++++++++- .../saved_objects/routes/find.test.ts | 13 ++++++++- .../saved_objects/routes/get.test.ts | 13 ++++++++- .../saved_objects/routes/resolve.test.ts | 13 ++++++++- .../saved_objects/routes/update.test.ts | 14 ++++++++- 23 files changed, 247 insertions(+), 33 deletions(-) diff --git a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_create.ts b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_create.ts index fd32068d79ebf..9c85b1aa0620c 100644 --- a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_create.ts +++ b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_create.ts @@ -8,16 +8,18 @@ import { schema } from '@kbn/config-schema'; import type { InternalCoreUsageDataSetup } from '@kbn/core-usage-data-base-server-internal'; +import type { Logger } from '@kbn/logging'; import type { InternalSavedObjectRouter } from '../internal_types'; import { catchAndReturnBoomErrors, throwIfAnyTypeNotVisibleByAPI } from './utils'; interface RouteDependencies { coreUsageData: InternalCoreUsageDataSetup; + logger: Logger; } export const registerBulkCreateRoute = ( router: InternalSavedObjectRouter, - { coreUsageData }: RouteDependencies + { coreUsageData, logger }: RouteDependencies ) => { router.post( { @@ -49,6 +51,9 @@ export const registerBulkCreateRoute = ( }, }, catchAndReturnBoomErrors(async (context, req, res) => { + logger.warn( + "The bulk create saved object API '/api/saved_objects/_bulk_create' is deprecated." + ); const { overwrite } = req.query; const usageStatsClient = coreUsageData.getClient(); diff --git a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_delete.ts b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_delete.ts index 720bd6ecdb7e1..cbd22f827a642 100644 --- a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_delete.ts +++ b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_delete.ts @@ -8,16 +8,18 @@ import { schema } from '@kbn/config-schema'; import type { InternalCoreUsageDataSetup } from '@kbn/core-usage-data-base-server-internal'; +import type { Logger } from '@kbn/logging'; import type { InternalSavedObjectRouter } from '../internal_types'; import { catchAndReturnBoomErrors, throwIfAnyTypeNotVisibleByAPI } from './utils'; interface RouteDependencies { coreUsageData: InternalCoreUsageDataSetup; + logger: Logger; } export const registerBulkDeleteRoute = ( router: InternalSavedObjectRouter, - { coreUsageData }: RouteDependencies + { coreUsageData, logger }: RouteDependencies ) => { router.post( { @@ -35,6 +37,9 @@ export const registerBulkDeleteRoute = ( }, }, catchAndReturnBoomErrors(async (context, req, res) => { + logger.warn( + "The bulk update saved object API '/api/saved_objects/_bulk_update' is deprecated." + ); const { force } = req.query; const usageStatsClient = coreUsageData.getClient(); usageStatsClient.incrementSavedObjectsBulkDelete({ request: req }).catch(() => {}); diff --git a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_get.ts b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_get.ts index 30a9a1625c39d..455657d9ca640 100644 --- a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_get.ts +++ b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_get.ts @@ -8,16 +8,18 @@ import { schema } from '@kbn/config-schema'; import type { InternalCoreUsageDataSetup } from '@kbn/core-usage-data-base-server-internal'; +import type { Logger } from '@kbn/logging'; import type { InternalSavedObjectRouter } from '../internal_types'; import { catchAndReturnBoomErrors, throwIfAnyTypeNotVisibleByAPI } from './utils'; interface RouteDependencies { coreUsageData: InternalCoreUsageDataSetup; + logger: Logger; } export const registerBulkGetRoute = ( router: InternalSavedObjectRouter, - { coreUsageData }: RouteDependencies + { coreUsageData, logger }: RouteDependencies ) => { router.post( { @@ -34,6 +36,7 @@ export const registerBulkGetRoute = ( }, }, catchAndReturnBoomErrors(async (context, req, res) => { + logger.warn("The bulk get saved object API '/api/saved_objects/_bulk_get' is deprecated."); const usageStatsClient = coreUsageData.getClient(); usageStatsClient.incrementSavedObjectsBulkGet({ request: req }).catch(() => {}); diff --git a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_resolve.ts b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_resolve.ts index 77f3d480bcdef..874df9f59bb8c 100644 --- a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_resolve.ts +++ b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_resolve.ts @@ -8,16 +8,18 @@ import { schema } from '@kbn/config-schema'; import type { InternalCoreUsageDataSetup } from '@kbn/core-usage-data-base-server-internal'; +import type { Logger } from '@kbn/logging'; import type { InternalSavedObjectRouter } from '../internal_types'; import { catchAndReturnBoomErrors, throwIfAnyTypeNotVisibleByAPI } from './utils'; interface RouteDependencies { coreUsageData: InternalCoreUsageDataSetup; + logger: Logger; } export const registerBulkResolveRoute = ( router: InternalSavedObjectRouter, - { coreUsageData }: RouteDependencies + { coreUsageData, logger }: RouteDependencies ) => { router.post( { @@ -32,6 +34,9 @@ export const registerBulkResolveRoute = ( }, }, catchAndReturnBoomErrors(async (context, req, res) => { + logger.warn( + "The bulk resolve saved object API '/api/saved_objects/_bulk_resolve' is deprecated." + ); const usageStatsClient = coreUsageData.getClient(); usageStatsClient.incrementSavedObjectsBulkResolve({ request: req }).catch(() => {}); diff --git a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_update.ts b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_update.ts index b198ef2ad5fec..b51e39fd1c6d5 100644 --- a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_update.ts +++ b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_update.ts @@ -8,16 +8,18 @@ import { schema } from '@kbn/config-schema'; import type { InternalCoreUsageDataSetup } from '@kbn/core-usage-data-base-server-internal'; +import type { Logger } from '@kbn/logging'; import type { InternalSavedObjectRouter } from '../internal_types'; import { catchAndReturnBoomErrors, throwIfAnyTypeNotVisibleByAPI } from './utils'; interface RouteDependencies { coreUsageData: InternalCoreUsageDataSetup; + logger: Logger; } export const registerBulkUpdateRoute = ( router: InternalSavedObjectRouter, - { coreUsageData }: RouteDependencies + { coreUsageData, logger }: RouteDependencies ) => { router.put( { @@ -44,6 +46,9 @@ export const registerBulkUpdateRoute = ( }, }, catchAndReturnBoomErrors(async (context, req, res) => { + logger.warn( + "The bulk update saved object API '/api/saved_objects/_bulk_update' is deprecated." + ); const usageStatsClient = coreUsageData.getClient(); usageStatsClient.incrementSavedObjectsBulkUpdate({ request: req }).catch(() => {}); diff --git a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/create.ts b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/create.ts index fcdb1fef13889..af0be90481c33 100644 --- a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/create.ts +++ b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/create.ts @@ -8,16 +8,18 @@ import { schema } from '@kbn/config-schema'; import type { InternalCoreUsageDataSetup } from '@kbn/core-usage-data-base-server-internal'; +import type { Logger } from '@kbn/logging'; import type { InternalSavedObjectRouter } from '../internal_types'; import { catchAndReturnBoomErrors, throwIfTypeNotVisibleByAPI } from './utils'; interface RouteDependencies { coreUsageData: InternalCoreUsageDataSetup; + logger: Logger; } export const registerCreateRoute = ( router: InternalSavedObjectRouter, - { coreUsageData }: RouteDependencies + { coreUsageData, logger }: RouteDependencies ) => { router.post( { @@ -48,6 +50,7 @@ export const registerCreateRoute = ( }, }, catchAndReturnBoomErrors(async (context, req, res) => { + logger.warn("The create saved object API '/api/saved_objects/{type}/{id}' is deprecated."); const { type, id } = req.params; const { overwrite } = req.query; const { attributes, migrationVersion, coreMigrationVersion, references, initialNamespaces } = diff --git a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/delete.ts b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/delete.ts index eea862e64008b..71124fee2ca38 100644 --- a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/delete.ts +++ b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/delete.ts @@ -8,16 +8,18 @@ import { schema } from '@kbn/config-schema'; import type { InternalCoreUsageDataSetup } from '@kbn/core-usage-data-base-server-internal'; +import type { Logger } from '@kbn/logging'; import type { InternalSavedObjectRouter } from '../internal_types'; import { catchAndReturnBoomErrors, throwIfTypeNotVisibleByAPI } from './utils'; interface RouteDependencies { coreUsageData: InternalCoreUsageDataSetup; + logger: Logger; } export const registerDeleteRoute = ( router: InternalSavedObjectRouter, - { coreUsageData }: RouteDependencies + { coreUsageData, logger }: RouteDependencies ) => { router.delete( { @@ -33,6 +35,7 @@ export const registerDeleteRoute = ( }, }, catchAndReturnBoomErrors(async (context, req, res) => { + logger.warn("The delete saved object API '/api/saved_objects/{type}/{id}' is deprecated."); const { type, id } = req.params; const { force } = req.query; const { getClient, typeRegistry } = (await context.core).savedObjects; diff --git a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/find.ts b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/find.ts index a2d4497dd5f6e..42cf0290b52d2 100644 --- a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/find.ts +++ b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/find.ts @@ -8,16 +8,18 @@ import { schema } from '@kbn/config-schema'; import type { InternalCoreUsageDataSetup } from '@kbn/core-usage-data-base-server-internal'; +import type { Logger } from '@kbn/logging'; import type { InternalSavedObjectRouter } from '../internal_types'; import { catchAndReturnBoomErrors, throwOnHttpHiddenTypes } from './utils'; interface RouteDependencies { coreUsageData: InternalCoreUsageDataSetup; + logger: Logger; } export const registerFindRoute = ( router: InternalSavedObjectRouter, - { coreUsageData }: RouteDependencies + { coreUsageData, logger }: RouteDependencies ) => { const referenceSchema = schema.object({ type: schema.string(), @@ -59,6 +61,7 @@ export const registerFindRoute = ( }, }, catchAndReturnBoomErrors(async (context, req, res) => { + logger.warn("The find saved object API '/api/saved_objects/_find' is deprecated."); const query = req.query; const namespaces = diff --git a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/get.ts b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/get.ts index b8c1ab5614b7a..ecacdc4452c67 100644 --- a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/get.ts +++ b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/get.ts @@ -8,16 +8,18 @@ import { schema } from '@kbn/config-schema'; import type { InternalCoreUsageDataSetup } from '@kbn/core-usage-data-base-server-internal'; +import type { Logger } from '@kbn/logging'; import type { InternalSavedObjectRouter } from '../internal_types'; import { catchAndReturnBoomErrors, throwIfTypeNotVisibleByAPI } from './utils'; interface RouteDependencies { coreUsageData: InternalCoreUsageDataSetup; + logger: Logger; } export const registerGetRoute = ( router: InternalSavedObjectRouter, - { coreUsageData }: RouteDependencies + { coreUsageData, logger }: RouteDependencies ) => { router.get( { @@ -30,6 +32,7 @@ export const registerGetRoute = ( }, }, catchAndReturnBoomErrors(async (context, req, res) => { + logger.warn("The get saved object API '/api/saved_objects/{type}/{id}' is deprecated."); const { type, id } = req.params; const usageStatsClient = coreUsageData.getClient(); diff --git a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/index.ts b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/index.ts index 89d5b41dd8885..8c85017064498 100644 --- a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/index.ts +++ b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/index.ts @@ -53,17 +53,17 @@ export function registerRoutes({ const router = http.createRouter('/api/saved_objects/'); - registerGetRoute(router, { coreUsageData }); - registerResolveRoute(router, { coreUsageData }); - registerCreateRoute(router, { coreUsageData }); - registerDeleteRoute(router, { coreUsageData }); - registerFindRoute(router, { coreUsageData }); - registerUpdateRoute(router, { coreUsageData }); - registerBulkGetRoute(router, { coreUsageData }); - registerBulkCreateRoute(router, { coreUsageData }); - registerBulkResolveRoute(router, { coreUsageData }); - registerBulkUpdateRoute(router, { coreUsageData }); - registerBulkDeleteRoute(router, { coreUsageData }); + registerGetRoute(router, { coreUsageData, logger }); + registerResolveRoute(router, { coreUsageData, logger }); + registerCreateRoute(router, { coreUsageData, logger }); + registerDeleteRoute(router, { coreUsageData, logger }); + registerFindRoute(router, { coreUsageData, logger }); + registerUpdateRoute(router, { coreUsageData, logger }); + registerBulkGetRoute(router, { coreUsageData, logger }); + registerBulkCreateRoute(router, { coreUsageData, logger }); + registerBulkResolveRoute(router, { coreUsageData, logger }); + registerBulkUpdateRoute(router, { coreUsageData, logger }); + registerBulkDeleteRoute(router, { coreUsageData, logger }); registerExportRoute(router, { config, coreUsageData }); registerImportRoute(router, { config, coreUsageData }); registerResolveImportErrorsRoute(router, { config, coreUsageData }); diff --git a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/resolve.ts b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/resolve.ts index ac8d4fdcc5ba7..be77423cba09b 100644 --- a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/resolve.ts +++ b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/resolve.ts @@ -8,16 +8,18 @@ import { schema } from '@kbn/config-schema'; import type { InternalCoreUsageDataSetup } from '@kbn/core-usage-data-base-server-internal'; +import type { Logger } from '@kbn/logging'; import type { InternalSavedObjectRouter } from '../internal_types'; import { throwIfTypeNotVisibleByAPI } from './utils'; interface RouteDependencies { coreUsageData: InternalCoreUsageDataSetup; + logger: Logger; } export const registerResolveRoute = ( router: InternalSavedObjectRouter, - { coreUsageData }: RouteDependencies + { coreUsageData, logger }: RouteDependencies ) => { router.get( { @@ -30,6 +32,9 @@ export const registerResolveRoute = ( }, }, router.handleLegacyErrors(async (context, req, res) => { + logger.warn( + "The resolve saved object API '/api/saved_objects/resolve/{type}/{id}' is deprecated." + ); const { type, id } = req.params; const { savedObjects } = await context.core; diff --git a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/update.ts b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/update.ts index 491add9a182ca..d1b544519efa3 100644 --- a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/update.ts +++ b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/update.ts @@ -8,17 +8,19 @@ import { schema } from '@kbn/config-schema'; import type { SavedObjectsUpdateOptions } from '@kbn/core-saved-objects-api-server'; +import type { Logger } from '@kbn/logging'; import type { InternalCoreUsageDataSetup } from '@kbn/core-usage-data-base-server-internal'; import type { InternalSavedObjectRouter } from '../internal_types'; import { catchAndReturnBoomErrors, throwIfTypeNotVisibleByAPI } from './utils'; interface RouteDependencies { coreUsageData: InternalCoreUsageDataSetup; + logger: Logger; } export const registerUpdateRoute = ( router: InternalSavedObjectRouter, - { coreUsageData }: RouteDependencies + { coreUsageData, logger }: RouteDependencies ) => { router.put( { @@ -45,6 +47,7 @@ export const registerUpdateRoute = ( }, }, catchAndReturnBoomErrors(async (context, req, res) => { + logger.warn("The update saved object API '/api/saved_objects/{type}/{id}' is deprecated."); const { type, id } = req.params; const { attributes, version, references, upsert } = req.body; const options: SavedObjectsUpdateOptions = { version, references, upsert }; diff --git a/src/core/server/integration_tests/saved_objects/routes/bulk_create.test.ts b/src/core/server/integration_tests/saved_objects/routes/bulk_create.test.ts index aba1f322f3bd2..096d7f330abca 100644 --- a/src/core/server/integration_tests/saved_objects/routes/bulk_create.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/bulk_create.test.ts @@ -19,6 +19,7 @@ import { type InternalSavedObjectsRequestHandlerContext, } from '@kbn/core-saved-objects-server-internal'; import { createHiddenTypeVariants, setupServer } from '@kbn/core-test-helpers-test-utils'; +import { loggerMock } from '@kbn/logging-mocks'; type SetupServerReturn = Awaited>; @@ -33,6 +34,7 @@ describe('POST /api/saved_objects/_bulk_create', () => { let handlerContext: SetupServerReturn['handlerContext']; let savedObjectsClient: ReturnType; let coreUsageStatsClient: jest.Mocked; + let loggerWarnSpy: jest.SpyInstance; beforeEach(async () => { ({ server, httpSetup, handlerContext } = await setupServer()); @@ -50,7 +52,9 @@ describe('POST /api/saved_objects/_bulk_create', () => { coreUsageStatsClient = coreUsageStatsClientMock.create(); coreUsageStatsClient.incrementSavedObjectsBulkCreate.mockRejectedValue(new Error('Oh no!')); // intentionally throw this error, which is swallowed, so we can assert that the operation does not fail const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient); - registerBulkCreateRoute(router, { coreUsageData }); + const logger = loggerMock.create(); + loggerWarnSpy = jest.spyOn(logger, 'warn').mockImplementation(); + registerBulkCreateRoute(router, { coreUsageData, logger }); await server.start(); }); @@ -161,4 +165,21 @@ describe('POST /api/saved_objects/_bulk_create', () => { 'Unsupported saved object type(s): hidden-from-http: Bad Request' ); }); + + it('logs a warning message when called', async () => { + await supertest(httpSetup.server.listener) + .post('/api/saved_objects/_bulk_create') + .send([ + { + id: 'abc1234', + type: 'index-pattern', + attributes: { + title: 'foo', + }, + references: [], + }, + ]) + .expect(200); + expect(loggerWarnSpy).toHaveBeenCalledTimes(1); + }); }); diff --git a/src/core/server/integration_tests/saved_objects/routes/bulk_delete.test.ts b/src/core/server/integration_tests/saved_objects/routes/bulk_delete.test.ts index f05780cc0fd65..47559aecf9769 100644 --- a/src/core/server/integration_tests/saved_objects/routes/bulk_delete.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/bulk_delete.test.ts @@ -18,6 +18,7 @@ import { registerBulkDeleteRoute, type InternalSavedObjectsRequestHandlerContext, } from '@kbn/core-saved-objects-server-internal'; +import { loggerMock } from '@kbn/logging-mocks'; type SetupServerReturn = Awaited>; @@ -32,6 +33,7 @@ describe('POST /api/saved_objects/_bulk_delete', () => { let handlerContext: SetupServerReturn['handlerContext']; let savedObjectsClient: ReturnType; let coreUsageStatsClient: jest.Mocked; + let loggerWarnSpy: jest.SpyInstance; beforeEach(async () => { ({ server, httpSetup, handlerContext } = await setupServer()); @@ -52,7 +54,9 @@ describe('POST /api/saved_objects/_bulk_delete', () => { coreUsageStatsClient = coreUsageStatsClientMock.create(); coreUsageStatsClient.incrementSavedObjectsBulkDelete.mockRejectedValue(new Error('Oh no!')); // intentionally throw this error, which is swallowed, so we can assert that the operation does not fail const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient); - registerBulkDeleteRoute(router, { coreUsageData }); + const logger = loggerMock.create(); + loggerWarnSpy = jest.spyOn(logger, 'warn').mockImplementation(); + registerBulkDeleteRoute(router, { coreUsageData, logger }); await server.start(); }); @@ -133,4 +137,17 @@ describe('POST /api/saved_objects/_bulk_delete', () => { .expect(400); expect(result.body.message).toContain('Unsupported saved object type(s):'); }); + + it('logs a warning message when called', async () => { + await supertest(httpSetup.server.listener) + .post('/api/saved_objects/_bulk_delete') + .send([ + { + id: 'hiddenID', + type: 'hidden-from-http', + }, + ]) + .expect(400); + expect(loggerWarnSpy).toHaveBeenCalledTimes(1); + }); }); diff --git a/src/core/server/integration_tests/saved_objects/routes/bulk_get.test.ts b/src/core/server/integration_tests/saved_objects/routes/bulk_get.test.ts index fdd23217891c4..7c894c250dceb 100644 --- a/src/core/server/integration_tests/saved_objects/routes/bulk_get.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/bulk_get.test.ts @@ -18,6 +18,7 @@ import { registerBulkGetRoute, type InternalSavedObjectsRequestHandlerContext, } from '@kbn/core-saved-objects-server-internal'; +import { loggerMock } from '@kbn/logging-mocks'; type SetupServerReturn = Awaited>; @@ -32,6 +33,7 @@ describe('POST /api/saved_objects/_bulk_get', () => { let handlerContext: SetupServerReturn['handlerContext']; let savedObjectsClient: ReturnType; let coreUsageStatsClient: jest.Mocked; + let loggerWarnSpy: jest.SpyInstance; beforeEach(async () => { ({ server, httpSetup, handlerContext } = await setupServer()); @@ -51,7 +53,9 @@ describe('POST /api/saved_objects/_bulk_get', () => { coreUsageStatsClient = coreUsageStatsClientMock.create(); coreUsageStatsClient.incrementSavedObjectsBulkGet.mockRejectedValue(new Error('Oh no!')); // intentionally throw this error, which is swallowed, so we can assert that the operation does not fail const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient); - registerBulkGetRoute(router, { coreUsageData }); + const logger = loggerMock.create(); + loggerWarnSpy = jest.spyOn(logger, 'warn').mockImplementation(); + registerBulkGetRoute(router, { coreUsageData, logger }); await server.start(); }); @@ -120,4 +124,17 @@ describe('POST /api/saved_objects/_bulk_get', () => { .expect(400); expect(result.body.message).toContain('Unsupported saved object type(s):'); }); + + it('logs a warning message when called', async () => { + await supertest(httpSetup.server.listener) + .post('/api/saved_objects/_bulk_get') + .send([ + { + id: 'abc123', + type: 'index-pattern', + }, + ]) + .expect(200); + expect(loggerWarnSpy).toHaveBeenCalledTimes(1); + }); }); diff --git a/src/core/server/integration_tests/saved_objects/routes/bulk_resolve.test.ts b/src/core/server/integration_tests/saved_objects/routes/bulk_resolve.test.ts index 3ea1e0c9580ac..98253fabb2fa4 100644 --- a/src/core/server/integration_tests/saved_objects/routes/bulk_resolve.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/bulk_resolve.test.ts @@ -18,6 +18,7 @@ import { registerBulkResolveRoute, type InternalSavedObjectsRequestHandlerContext, } from '@kbn/core-saved-objects-server-internal'; +import { loggerMock } from '@kbn/logging-mocks'; type SetupServerReturn = Awaited>; @@ -32,6 +33,7 @@ describe('POST /api/saved_objects/_bulk_resolve', () => { let handlerContext: SetupServerReturn['handlerContext']; let savedObjectsClient: ReturnType; let coreUsageStatsClient: jest.Mocked; + let loggerWarnSpy: jest.SpyInstance; beforeEach(async () => { ({ server, httpSetup, handlerContext } = await setupServer()); @@ -52,7 +54,9 @@ describe('POST /api/saved_objects/_bulk_resolve', () => { coreUsageStatsClient = coreUsageStatsClientMock.create(); coreUsageStatsClient.incrementSavedObjectsBulkResolve.mockRejectedValue(new Error('Oh no!')); // intentionally throw this error, which is swallowed, so we can assert that the operation does not fail const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient); - registerBulkResolveRoute(router, { coreUsageData }); + const logger = loggerMock.create(); + loggerWarnSpy = jest.spyOn(logger, 'warn').mockImplementation(); + registerBulkResolveRoute(router, { coreUsageData, logger }); await server.start(); }); @@ -124,4 +128,17 @@ describe('POST /api/saved_objects/_bulk_resolve', () => { .expect(400); expect(result.body.message).toContain('Unsupported saved object type(s):'); }); + + it('logs a warning message when called', async () => { + await supertest(httpSetup.server.listener) + .post('/api/saved_objects/_bulk_resolve') + .send([ + { + id: 'abc123', + type: 'index-pattern', + }, + ]) + .expect(200); + expect(loggerWarnSpy).toHaveBeenCalledTimes(1); + }); }); diff --git a/src/core/server/integration_tests/saved_objects/routes/bulk_update.test.ts b/src/core/server/integration_tests/saved_objects/routes/bulk_update.test.ts index 04ad9dcd40592..eb50fd141e2af 100644 --- a/src/core/server/integration_tests/saved_objects/routes/bulk_update.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/bulk_update.test.ts @@ -18,6 +18,7 @@ import { registerBulkUpdateRoute, type InternalSavedObjectsRequestHandlerContext, } from '@kbn/core-saved-objects-server-internal'; +import { loggerMock } from '@kbn/logging-mocks'; type SetupServerReturn = Awaited>; @@ -34,6 +35,7 @@ describe('PUT /api/saved_objects/_bulk_update', () => { let handlerContext: SetupServerReturn['handlerContext']; let savedObjectsClient: ReturnType; let coreUsageStatsClient: jest.Mocked; + let loggerWarnSpy: jest.SpyInstance; beforeEach(async () => { ({ server, httpSetup, handlerContext } = await setupServer()); @@ -51,7 +53,9 @@ describe('PUT /api/saved_objects/_bulk_update', () => { coreUsageStatsClient = coreUsageStatsClientMock.create(); coreUsageStatsClient.incrementSavedObjectsBulkUpdate.mockRejectedValue(new Error('Oh no!')); // intentionally throw this error, which is swallowed, so we can assert that the operation does not fail const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient); - registerBulkUpdateRoute(router, { coreUsageData }); + const logger = loggerMock.create(); + loggerWarnSpy = jest.spyOn(logger, 'warn').mockImplementation(); + registerBulkUpdateRoute(router, { coreUsageData, logger }); await server.start(); }); @@ -169,4 +173,27 @@ describe('PUT /api/saved_objects/_bulk_update', () => { .expect(400); expect(result.body.message).toContain('Unsupported saved object type(s):'); }); + + it('logs a warning message when called', async () => { + await supertest(httpSetup.server.listener) + .put('/api/saved_objects/_bulk_update') + .send([ + { + type: 'visualization', + id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', + attributes: { + title: 'An existing visualization', + }, + }, + { + type: 'dashboard', + id: 'be3733a0-9efe-11e7-acb3-3dab96693fab', + attributes: { + title: 'An existing dashboard', + }, + }, + ]) + .expect(200); + expect(loggerWarnSpy).toHaveBeenCalledTimes(1); + }); }); diff --git a/src/core/server/integration_tests/saved_objects/routes/create.test.ts b/src/core/server/integration_tests/saved_objects/routes/create.test.ts index 50791be209547..bf67325a8e756 100644 --- a/src/core/server/integration_tests/saved_objects/routes/create.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/create.test.ts @@ -18,6 +18,7 @@ import { registerCreateRoute, type InternalSavedObjectsRequestHandlerContext, } from '@kbn/core-saved-objects-server-internal'; +import { loggerMock } from '@kbn/logging-mocks'; type SetupServerReturn = Awaited>; @@ -32,6 +33,7 @@ describe('POST /api/saved_objects/{type}', () => { let handlerContext: SetupServerReturn['handlerContext']; let savedObjectsClient: ReturnType; let coreUsageStatsClient: jest.Mocked; + let loggerWarnSpy: jest.SpyInstance; const clientResponse = { id: 'logstash-*', @@ -52,7 +54,9 @@ describe('POST /api/saved_objects/{type}', () => { coreUsageStatsClient = coreUsageStatsClientMock.create(); coreUsageStatsClient.incrementSavedObjectsCreate.mockRejectedValue(new Error('Oh no!')); // intentionally throw this error, which is swallowed, so we can assert that the operation does not fail const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient); - registerCreateRoute(router, { coreUsageData }); + const logger = loggerMock.create(); + loggerWarnSpy = jest.spyOn(logger, 'warn').mockImplementation(); + registerCreateRoute(router, { coreUsageData, logger }); handlerContext.savedObjects.typeRegistry.getType.mockImplementation((typename: string) => { return testTypes @@ -145,4 +149,16 @@ describe('POST /api/saved_objects/{type}', () => { expect(result.body.message).toContain("Unsupported saved object type: 'hidden-from-http'"); }); + + it('logs a warning message when called', async () => { + await supertest(httpSetup.server.listener) + .post('/api/saved_objects/index-pattern') + .send({ + attributes: { + title: 'Logging test', + }, + }) + .expect(200); + expect(loggerWarnSpy).toHaveBeenCalledTimes(1); + }); }); diff --git a/src/core/server/integration_tests/saved_objects/routes/delete.test.ts b/src/core/server/integration_tests/saved_objects/routes/delete.test.ts index 42c97c0c565da..538cc2d721485 100644 --- a/src/core/server/integration_tests/saved_objects/routes/delete.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/delete.test.ts @@ -18,6 +18,7 @@ import { registerDeleteRoute, type InternalSavedObjectsRequestHandlerContext, } from '@kbn/core-saved-objects-server-internal'; +import { loggerMock } from '@kbn/logging-mocks'; type SetupServerReturn = Awaited>; @@ -33,6 +34,7 @@ describe('DELETE /api/saved_objects/{type}/{id}', () => { let handlerContext: SetupServerReturn['handlerContext']; let savedObjectsClient: ReturnType; let coreUsageStatsClient: jest.Mocked; + let loggerWarnSpy: jest.SpyInstance; beforeEach(async () => { ({ server, httpSetup, handlerContext } = await setupServer()); @@ -49,7 +51,9 @@ describe('DELETE /api/saved_objects/{type}/{id}', () => { coreUsageStatsClient = coreUsageStatsClientMock.create(); coreUsageStatsClient.incrementSavedObjectsDelete.mockRejectedValue(new Error('Oh no!')); // intentionally throw this error, which is swallowed, so we can assert that the operation does not fail const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient); - registerDeleteRoute(router, { coreUsageData }); + const logger = loggerMock.create(); + loggerWarnSpy = jest.spyOn(logger, 'warn').mockImplementation(); + registerDeleteRoute(router, { coreUsageData, logger }); await server.start(); }); @@ -104,4 +108,11 @@ describe('DELETE /api/saved_objects/{type}/{id}', () => { .expect(400); expect(result.body.message).toContain("Unsupported saved object type: 'hidden-from-http'"); }); + + it('logs a warning message when called', async () => { + await supertest(httpSetup.server.listener) + .delete('/api/saved_objects/index-pattern/logstash-*') + .expect(200); + expect(loggerWarnSpy).toHaveBeenCalledTimes(1); + }); }); diff --git a/src/core/server/integration_tests/saved_objects/routes/find.test.ts b/src/core/server/integration_tests/saved_objects/routes/find.test.ts index 2185edd9f5e19..25fd8a32fc9ef 100644 --- a/src/core/server/integration_tests/saved_objects/routes/find.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/find.test.ts @@ -16,6 +16,7 @@ import { coreUsageDataServiceMock, } from '@kbn/core-usage-data-server-mocks'; import { createHiddenTypeVariants, setupServer } from '@kbn/core-test-helpers-test-utils'; +import { loggerMock } from '@kbn/logging-mocks'; import { registerFindRoute, type InternalSavedObjectsRequestHandlerContext, @@ -38,6 +39,7 @@ describe('GET /api/saved_objects/_find', () => { let handlerContext: SetupServerReturn['handlerContext']; let savedObjectsClient: ReturnType; let coreUsageStatsClient: jest.Mocked; + let loggerWarnSpy: jest.SpyInstance; const clientResponse = { total: 0, @@ -64,7 +66,9 @@ describe('GET /api/saved_objects/_find', () => { coreUsageStatsClient = coreUsageStatsClientMock.create(); coreUsageStatsClient.incrementSavedObjectsFind.mockRejectedValue(new Error('Oh no!')); // intentionally throw this error, which is swallowed, so we can assert that the operation does not fail const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient); - registerFindRoute(router, { coreUsageData }); + const logger = loggerMock.create(); + loggerWarnSpy = jest.spyOn(logger, 'warn').mockImplementation(); + registerFindRoute(router, { coreUsageData, logger }); await server.start(); }); @@ -428,4 +432,11 @@ describe('GET /api/saved_objects/_find', () => { }) ); }); + + it('logs a warning message when called', async () => { + await supertest(httpSetup.server.listener) + .get('/api/saved_objects/_find?type=foo&type=bar') + .expect(200); + expect(loggerWarnSpy).toHaveBeenCalledTimes(1); + }); }); diff --git a/src/core/server/integration_tests/saved_objects/routes/get.test.ts b/src/core/server/integration_tests/saved_objects/routes/get.test.ts index efd7f25938592..363f0406b2148 100644 --- a/src/core/server/integration_tests/saved_objects/routes/get.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/get.test.ts @@ -23,6 +23,7 @@ import { type InternalSavedObjectsRequestHandlerContext, } from '@kbn/core-saved-objects-server-internal'; import { createHiddenTypeVariants } from '@kbn/core-test-helpers-test-utils'; +import { loggerMock } from '@kbn/logging-mocks'; const coreId = Symbol('core'); @@ -38,6 +39,7 @@ describe('GET /api/saved_objects/{type}/{id}', () => { let handlerContext: ReturnType; let savedObjectsClient: ReturnType; let coreUsageStatsClient: jest.Mocked; + let loggerWarnSpy: jest.SpyInstance; beforeEach(async () => { const coreContext = createCoreContext({ coreId }); @@ -72,7 +74,9 @@ describe('GET /api/saved_objects/{type}/{id}', () => { coreUsageStatsClient = coreUsageStatsClientMock.create(); coreUsageStatsClient.incrementSavedObjectsGet.mockRejectedValue(new Error('Oh no!')); // intentionally throw this error, which is swallowed, so we can assert that the operation does not fail const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient); - registerGetRoute(router, { coreUsageData }); + const logger = loggerMock.create(); + loggerWarnSpy = jest.spyOn(logger, 'warn').mockImplementation(); + registerGetRoute(router, { coreUsageData, logger }); await server.start(); }); @@ -121,4 +125,11 @@ describe('GET /api/saved_objects/{type}/{id}', () => { .expect(400); expect(result.body.message).toContain("Unsupported saved object type: 'hidden-from-http'"); }); + + it('logs a warning message when called', async () => { + await supertest(httpSetup.server.listener) + .get('/api/saved_objects/index-pattern/logstash-*') + .expect(200); + expect(loggerWarnSpy).toHaveBeenCalledTimes(1); + }); }); diff --git a/src/core/server/integration_tests/saved_objects/routes/resolve.test.ts b/src/core/server/integration_tests/saved_objects/routes/resolve.test.ts index 6f58e31334764..0ecc6221730ff 100644 --- a/src/core/server/integration_tests/saved_objects/routes/resolve.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/resolve.test.ts @@ -23,6 +23,7 @@ import { type InternalSavedObjectsRequestHandlerContext, } from '@kbn/core-saved-objects-server-internal'; import { createHiddenTypeVariants } from '@kbn/core-test-helpers-test-utils'; +import { loggerMock } from '@kbn/logging-mocks'; const coreId = Symbol('core'); @@ -38,6 +39,7 @@ describe('GET /api/saved_objects/resolve/{type}/{id}', () => { let handlerContext: ReturnType; let savedObjectsClient: ReturnType; let coreUsageStatsClient: jest.Mocked; + let loggerWarnSpy: jest.SpyInstance; beforeEach(async () => { const coreContext = createCoreContext({ coreId }); @@ -73,7 +75,9 @@ describe('GET /api/saved_objects/resolve/{type}/{id}', () => { coreUsageStatsClient = coreUsageStatsClientMock.create(); coreUsageStatsClient.incrementSavedObjectsResolve.mockRejectedValue(new Error('Oh no!')); // intentionally throw this error, which is swallowed, so we can assert that the operation does not fail const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient); - registerResolveRoute(router, { coreUsageData }); + const logger = loggerMock.create(); + loggerWarnSpy = jest.spyOn(logger, 'warn').mockImplementation(); + registerResolveRoute(router, { coreUsageData, logger }); await server.start(); }); @@ -122,4 +126,11 @@ describe('GET /api/saved_objects/resolve/{type}/{id}', () => { .expect(400); expect(result.body.message).toContain("Unsupported saved object type: 'hidden-from-http'"); }); + + it('logs a warning message when called', async () => { + await supertest(httpSetup.server.listener) + .get('/api/saved_objects/resolve/index-pattern/logstash-*') + .expect(200); + expect(loggerWarnSpy).toHaveBeenCalledTimes(1); + }); }); diff --git a/src/core/server/integration_tests/saved_objects/routes/update.test.ts b/src/core/server/integration_tests/saved_objects/routes/update.test.ts index 639f503d050d7..8333159b8e1c6 100644 --- a/src/core/server/integration_tests/saved_objects/routes/update.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/update.test.ts @@ -18,6 +18,7 @@ import { registerUpdateRoute, type InternalSavedObjectsRequestHandlerContext, } from '@kbn/core-saved-objects-server-internal'; +import { loggerMock } from '@kbn/logging-mocks'; type SetupServerReturn = Awaited>; @@ -33,6 +34,7 @@ describe('PUT /api/saved_objects/{type}/{id?}', () => { let handlerContext: SetupServerReturn['handlerContext']; let savedObjectsClient: ReturnType; let coreUsageStatsClient: jest.Mocked; + let loggerWarnSpy: jest.SpyInstance; beforeEach(async () => { const clientResponse = { @@ -60,7 +62,9 @@ describe('PUT /api/saved_objects/{type}/{id?}', () => { coreUsageStatsClient = coreUsageStatsClientMock.create(); coreUsageStatsClient.incrementSavedObjectsUpdate.mockRejectedValue(new Error('Oh no!')); // intentionally throw this error, which is swallowed, so we can assert that the operation does not fail const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient); - registerUpdateRoute(router, { coreUsageData }); + const logger = loggerMock.create(); + loggerWarnSpy = jest.spyOn(logger, 'warn').mockImplementation(); + registerUpdateRoute(router, { coreUsageData, logger }); await server.start(); }); @@ -123,4 +127,12 @@ describe('PUT /api/saved_objects/{type}/{id?}', () => { .expect(400); expect(result.body.message).toContain("Unsupported saved object type: 'hidden-from-http'"); }); + + it('logs a warning message when called', async () => { + await supertest(httpSetup.server.listener) + .put('/api/saved_objects/index-pattern/logstash-*') + .send({ attributes: { title: 'Logging test' }, version: 'log' }) + .expect(200); + expect(loggerWarnSpy).toHaveBeenCalledTimes(1); + }); }); From b37d3fbf01c357bcd226a5fd1369d4f1f461cc9d Mon Sep 17 00:00:00 2001 From: Angelo Gulina Date: Mon, 13 Feb 2023 17:38:28 +0100 Subject: [PATCH 11/20] Fix function expression in example (#150978) ## Summary The MR fixes the function expression syntax in an example in docs. The changes have no visual/UI impact. ### For maintainers - [x] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --- src/core/CORE_CONVENTIONS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/CORE_CONVENTIONS.md b/src/core/CORE_CONVENTIONS.md index 1cd997d570b6a..9571be679ce57 100644 --- a/src/core/CORE_CONVENTIONS.md +++ b/src/core/CORE_CONVENTIONS.md @@ -122,7 +122,7 @@ area of Core API's and does not apply to internal types. ```ts // -- good -- - const createMock => { + const createMock = () => { const mocked: jest.Mocked = { start: jest.fn(), }; From b0c1d5c1b574fa6a6a52eb4460646e25225dc673 Mon Sep 17 00:00:00 2001 From: Or Ouziel Date: Mon, 13 Feb 2023 18:58:48 +0200 Subject: [PATCH 12/20] [Cloud Posture] Update readme with info on testing (#150430) --- .../plugins/cloud_security_posture/README.md | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/cloud_security_posture/README.md b/x-pack/plugins/cloud_security_posture/README.md index a655d292c39ee..f9c760fbeb99f 100755 --- a/x-pack/plugins/cloud_security_posture/README.md +++ b/x-pack/plugins/cloud_security_posture/README.md @@ -10,7 +10,15 @@ read [Kibana Contributing Guide](https://github.com/elastic/kibana/blob/main/CON ## Testing -read [Kibana Testing Guide](https://www.elastic.co/guide/en/kibana/current/development-tests.html) for more details +for general guidelines, read [Kibana Testing Guide](https://www.elastic.co/guide/en/kibana/current/development-tests.html) for more details + +### Tests + +1. Unit Tests (Jest) - located in sibling files to the source code +2. [Integration Tests](../../test/api_integration/apis/cloud_security_posture/index.ts) +3. [End-to-End Tests](../../test/cloud_security_posture_functional/pages/index.ts) + +### Tools Run **TypeScript**: @@ -24,7 +32,7 @@ Run **ESLint**: yarn lint:es x-pack/plugins/cloud_security_posture ``` -Run **Unit Tests**: +Run [**Unit Tests**](https://www.elastic.co/guide/en/kibana/current/development-tests.html#_unit_testing): ```bash yarn test:jest --config x-pack/plugins/cloud_security_posture/jest.config.js @@ -33,24 +41,23 @@ yarn test:jest --config x-pack/plugins/cloud_security_posture/jest.config.js > **Note** > for a coverage report, add the `--coverage` flag, and run `open target/kibana-coverage/jest/x-pack/plugins/cloud_security_posture/index.html` -Run **API Integration**: +Run [**Integration Tests**](https://docs.elastic.dev/kibana-dev-docs/tutorials/testing-plugins#): ```bash yarn test:ftr --config x-pack/test/api_integration/config.ts ``` -Run **Functional UI Tests**: +Run [**End-to-End Tests**](https://www.elastic.co/guide/en/kibana/current/development-tests.html#_running_functional_tests): ```bash -yarn test:ftr --config x-pack/test/cloud_security_posture_functional/config.ts +yarn test:ftr --config x-pack/test/cloud_security_posture_functional/config.ts --debug ```
-> **Note** -> in development, run them separately with `ftr:runner` and `ftr:server` +test runner (FTR) can be used separately with `ftr:runner` and `ftr:server`: ```bash yarn test:ftr:server --config x-pack/test/api_integration/config.ts yarn test:ftr:runner --include-tag=cloud_security_posture --config x-pack/test/api_integration/config.ts -``` \ No newline at end of file +``` From 6e3551d9dad5fc15be91b37b2632064a69d491b4 Mon Sep 17 00:00:00 2001 From: Drew Tate Date: Mon, 13 Feb 2023 11:07:23 -0600 Subject: [PATCH 13/20] [Lens] Reinstate config errors in embeddable (#150650) --- .../get_application_user_messages.test.tsx | 4 +- .../get_application_user_messages.tsx | 10 +- .../editor_frame/state_helpers.ts | 2 +- .../public/embeddable/embeddable.test.tsx | 125 +++++++++--------- .../lens/public/embeddable/embeddable.tsx | 54 ++++---- .../apps/lens/group3/error_handling.ts | 22 +++ ...undamental_config_errors_on_dashboard.json | 9 ++ 7 files changed, 130 insertions(+), 96 deletions(-) create mode 100644 x-pack/test/functional/fixtures/kbn_archiver/lens/fundamental_config_errors_on_dashboard.json diff --git a/x-pack/plugins/lens/public/app_plugin/get_application_user_messages.test.tsx b/x-pack/plugins/lens/public/app_plugin/get_application_user_messages.test.tsx index 3691f507d6a4d..c49ef7f349f18 100644 --- a/x-pack/plugins/lens/public/app_plugin/get_application_user_messages.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/get_application_user_messages.test.tsx @@ -37,12 +37,12 @@ describe('application-level user messages', () => { Object { "displayLocations": Array [ Object { - "id": "visualization", + "id": "visualizationOnEmbeddable", }, ], "fixableInEditor": true, "longMessage": "Visualization type not found.", - "severity": "warning", + "severity": "error", "shortMessage": "", }, ] diff --git a/x-pack/plugins/lens/public/app_plugin/get_application_user_messages.tsx b/x-pack/plugins/lens/public/app_plugin/get_application_user_messages.tsx index f7b5d1eb8a135..f5190ec334157 100644 --- a/x-pack/plugins/lens/public/app_plugin/get_application_user_messages.tsx +++ b/x-pack/plugins/lens/public/app_plugin/get_application_user_messages.tsx @@ -33,9 +33,9 @@ export const getApplicationUserMessages = ({ core, }: { visualizationType: string | null | undefined; - visualization: VisualizationState; + visualization: VisualizationState | undefined; visualizationMap: VisualizationMap; - activeDatasource: Datasource | null; + activeDatasource: Datasource | null | undefined; activeDatasourceState: { state: unknown } | null; dataViews: DataViewsState; core: CoreStart; @@ -46,7 +46,7 @@ export const getApplicationUserMessages = ({ messages.push(getMissingVisTypeError()); } - if (visualization.activeId && !visualizationMap[visualization.activeId]) { + if (visualization?.activeId && !visualizationMap[visualization.activeId]) { messages.push(getUnknownVisualizationTypeError(visualization.activeId)); } @@ -69,8 +69,8 @@ export const getApplicationUserMessages = ({ function getMissingVisTypeError(): UserMessage { return { - severity: 'warning', - displayLocations: [{ id: 'visualization' }], + severity: 'error', + displayLocations: [{ id: 'visualizationOnEmbeddable' }], fixableInEditor: true, shortMessage: '', longMessage: i18n.translate('xpack.lens.editorFrame.expressionMissingVisualizationType', { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts index 4cda8d1c7c0bd..02410b6994c34 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts @@ -401,7 +401,7 @@ export async function persistedStateToExpression( } export function getMissingIndexPattern( - currentDatasource: Datasource | null, + currentDatasource: Datasource | null | undefined, currentDatasourceState: { state: unknown } | null, indexPatterns: IndexPatternMap ) { diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx index db00db6a87f7f..92bfa95eb2aef 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx +++ b/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx @@ -30,23 +30,36 @@ import { OnSaveProps } from '@kbn/saved-objects-plugin/public/save_modal'; import { act } from 'react-dom/test-utils'; import { inspectorPluginMock } from '@kbn/inspector-plugin/public/mocks'; import { Visualization } from '../types'; +import { createMockDatasource, createMockVisualization } from '../mocks'; jest.mock('@kbn/inspector-plugin/public', () => ({ isAvailable: false, open: false, })); +const defaultVisualizationId = 'lnsSomeVisType'; +const defaultDatasourceId = 'someDatasource'; + const savedVis: Document = { state: { - visualization: {}, - datasourceStates: {}, + visualization: { activeId: defaultVisualizationId }, + datasourceStates: { [defaultDatasourceId]: {} }, query: { query: '', language: 'lucene' }, filters: [], }, references: [], title: 'My title', - visualizationType: '', + visualizationType: defaultVisualizationId, +}; + +const defaultVisualizationMap = { + [defaultVisualizationId]: createMockVisualization(), +}; + +const defaultDatasourceMap = { + [defaultDatasourceId]: createMockDatasource(defaultDatasourceId), }; + const defaultSaveMethod = ( testAttributes: LensSavedObjectAttributes, savedObjectId?: string @@ -155,8 +168,8 @@ describe('embeddable', () => { inspector: inspectorPluginMock.createStartContract(), getTrigger, theme: themeServiceMock.createStartContract(), - visualizationMap: {}, - datasourceMap: {}, + visualizationMap: defaultVisualizationMap, + datasourceMap: defaultDatasourceMap, injectFilterReferences: jest.fn(mockInjectFilterReferences), documentToExpression: () => Promise.resolve({ @@ -208,8 +221,8 @@ describe('embeddable', () => { inspector: inspectorPluginMock.createStartContract(), getTrigger, theme: themeServiceMock.createStartContract(), - visualizationMap: {}, - datasourceMap: {}, + visualizationMap: defaultVisualizationMap, + datasourceMap: defaultDatasourceMap, injectFilterReferences: jest.fn(mockInjectFilterReferences), documentToExpression: () => Promise.resolve({ @@ -268,8 +281,8 @@ describe('embeddable', () => { navLinks: {}, }, getTrigger, - visualizationMap: {}, - datasourceMap: {}, + visualizationMap: defaultVisualizationMap, + datasourceMap: defaultDatasourceMap, injectFilterReferences: jest.fn(mockInjectFilterReferences), theme: themeServiceMock.createStartContract(), documentToExpression: () => @@ -322,8 +335,8 @@ describe('embeddable', () => { navLinks: {}, }, getTrigger, - visualizationMap: {}, - datasourceMap: {}, + visualizationMap: defaultVisualizationMap, + datasourceMap: defaultDatasourceMap, injectFilterReferences: jest.fn(mockInjectFilterReferences), theme: themeServiceMock.createStartContract(), documentToExpression: () => @@ -398,8 +411,8 @@ describe('embeddable', () => { navLinks: {}, }, getTrigger, - visualizationMap: {}, - datasourceMap: {}, + visualizationMap: defaultVisualizationMap, + datasourceMap: defaultDatasourceMap, injectFilterReferences: jest.fn(mockInjectFilterReferences), theme: themeServiceMock.createStartContract(), documentToExpression: () => @@ -452,8 +465,8 @@ describe('embeddable', () => { navLinks: {}, }, getTrigger, - visualizationMap: {}, - datasourceMap: {}, + visualizationMap: defaultVisualizationMap, + datasourceMap: defaultDatasourceMap, injectFilterReferences: jest.fn(mockInjectFilterReferences), theme: themeServiceMock.createStartContract(), documentToExpression: () => @@ -505,8 +518,8 @@ describe('embeddable', () => { navLinks: {}, }, getTrigger, - visualizationMap: {}, - datasourceMap: {}, + visualizationMap: defaultVisualizationMap, + datasourceMap: defaultDatasourceMap, injectFilterReferences: jest.fn(mockInjectFilterReferences), theme: themeServiceMock.createStartContract(), documentToExpression: () => @@ -554,8 +567,8 @@ describe('embeddable', () => { navLinks: {}, }, getTrigger, - visualizationMap: {}, - datasourceMap: {}, + visualizationMap: defaultVisualizationMap, + datasourceMap: defaultDatasourceMap, injectFilterReferences: jest.fn(mockInjectFilterReferences), theme: themeServiceMock.createStartContract(), documentToExpression: () => @@ -609,8 +622,8 @@ describe('embeddable', () => { navLinks: {}, }, getTrigger, - visualizationMap: {}, - datasourceMap: {}, + visualizationMap: defaultVisualizationMap, + datasourceMap: defaultDatasourceMap, injectFilterReferences: jest.fn(mockInjectFilterReferences), theme: themeServiceMock.createStartContract(), documentToExpression: () => @@ -668,8 +681,8 @@ describe('embeddable', () => { navLinks: {}, }, getTrigger, - visualizationMap: {}, - datasourceMap: {}, + visualizationMap: defaultVisualizationMap, + datasourceMap: defaultDatasourceMap, injectFilterReferences: jest.fn(mockInjectFilterReferences), theme: themeServiceMock.createStartContract(), documentToExpression: () => @@ -725,8 +738,8 @@ describe('embeddable', () => { navLinks: {}, }, getTrigger, - visualizationMap: {}, - datasourceMap: {}, + visualizationMap: defaultVisualizationMap, + datasourceMap: defaultDatasourceMap, injectFilterReferences: jest.fn(mockInjectFilterReferences), theme: themeServiceMock.createStartContract(), documentToExpression: () => @@ -789,8 +802,8 @@ describe('embeddable', () => { navLinks: {}, }, getTrigger, - visualizationMap: {}, - datasourceMap: {}, + visualizationMap: defaultVisualizationMap, + datasourceMap: defaultDatasourceMap, injectFilterReferences: jest.fn(mockInjectFilterReferences), theme: themeServiceMock.createStartContract(), documentToExpression: () => @@ -854,8 +867,8 @@ describe('embeddable', () => { navLinks: {}, }, getTrigger, - visualizationMap: {}, - datasourceMap: {}, + visualizationMap: defaultVisualizationMap, + datasourceMap: defaultDatasourceMap, injectFilterReferences: jest.fn(mockInjectFilterReferences), theme: themeServiceMock.createStartContract(), documentToExpression: () => @@ -922,8 +935,8 @@ describe('embeddable', () => { navLinks: {}, }, getTrigger, - visualizationMap: {}, - datasourceMap: {}, + visualizationMap: defaultVisualizationMap, + datasourceMap: defaultDatasourceMap, injectFilterReferences: jest.fn(mockInjectFilterReferences), theme: themeServiceMock.createStartContract(), documentToExpression: () => @@ -975,8 +988,8 @@ describe('embeddable', () => { navLinks: {}, }, getTrigger, - visualizationMap: {}, - datasourceMap: {}, + visualizationMap: defaultVisualizationMap, + datasourceMap: defaultDatasourceMap, injectFilterReferences: jest.fn(mockInjectFilterReferences), theme: themeServiceMock.createStartContract(), documentToExpression: () => @@ -1030,8 +1043,8 @@ describe('embeddable', () => { navLinks: {}, }, getTrigger, - visualizationMap: {}, - datasourceMap: {}, + visualizationMap: defaultVisualizationMap, + datasourceMap: defaultDatasourceMap, injectFilterReferences: jest.fn(mockInjectFilterReferences), theme: themeServiceMock.createStartContract(), documentToExpression: () => @@ -1082,8 +1095,8 @@ describe('embeddable', () => { navLinks: {}, }, getTrigger, - visualizationMap: {}, - datasourceMap: {}, + visualizationMap: defaultVisualizationMap, + datasourceMap: defaultDatasourceMap, injectFilterReferences: jest.fn(mockInjectFilterReferences), theme: themeServiceMock.createStartContract(), documentToExpression: () => @@ -1150,8 +1163,8 @@ describe('embeddable', () => { navLinks: {}, }, getTrigger, - visualizationMap: {}, - datasourceMap: {}, + visualizationMap: defaultVisualizationMap, + datasourceMap: defaultDatasourceMap, injectFilterReferences: jest.fn(mockInjectFilterReferences), theme: themeServiceMock.createStartContract(), documentToExpression: () => @@ -1236,8 +1249,8 @@ describe('embeddable', () => { navLinks: {}, }, getTrigger, - visualizationMap: {}, - datasourceMap: {}, + visualizationMap: defaultVisualizationMap, + datasourceMap: defaultDatasourceMap, injectFilterReferences: jest.fn(mockInjectFilterReferences), theme: themeServiceMock.createStartContract(), documentToExpression: () => @@ -1297,8 +1310,8 @@ describe('embeddable', () => { navLinks: {}, }, getTrigger, - visualizationMap: {}, - datasourceMap: {}, + visualizationMap: defaultVisualizationMap, + datasourceMap: defaultDatasourceMap, injectFilterReferences: jest.fn(mockInjectFilterReferences), theme: themeServiceMock.createStartContract(), documentToExpression: () => @@ -1355,8 +1368,8 @@ describe('embeddable', () => { navLinks: {}, }, getTrigger, - visualizationMap: {}, - datasourceMap: {}, + visualizationMap: defaultVisualizationMap, + datasourceMap: defaultDatasourceMap, injectFilterReferences: jest.fn(mockInjectFilterReferences), theme: themeServiceMock.createStartContract(), documentToExpression: () => @@ -1408,7 +1421,7 @@ describe('embeddable', () => { const visDocument: Document = { state: { visualization: {}, - datasourceStates: {}, + datasourceStates: { [defaultDatasourceId]: {} }, query: { query: '', language: 'lucene' }, filters: [], }, @@ -1443,7 +1456,7 @@ describe('embeddable', () => { initialize: () => {}, } as unknown as Visualization, }, - datasourceMap: {}, + datasourceMap: defaultDatasourceMap, documentToExpression: documentToExpressionMock, }, { id: '123' } as unknown as LensEmbeddableInput @@ -1475,23 +1488,11 @@ describe('embeddable', () => { it('should override noPadding in the display options if noPadding is set in the embeddable input', async () => { expressionRenderer = jest.fn((_) => null); - const visDocument: Document = { - state: { - visualization: {}, - datasourceStates: {}, - query: { query: '', language: 'lucene' }, - filters: [], - }, - references: [], - title: 'My title', - visualizationType: 'testVis', - }; - const createEmbeddable = (displayOptions?: { noPadding: boolean }, noPadding?: boolean) => { return new Embeddable( { timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter, - attributeService: attributeServiceMockFromSavedVis(visDocument), + attributeService: attributeServiceMockFromSavedVis(savedVis), data: dataMock, expressionRenderer, coreStart: {} as CoreStart, @@ -1507,12 +1508,12 @@ describe('embeddable', () => { getTrigger, theme: themeServiceMock.createStartContract(), visualizationMap: { - [visDocument.visualizationType as string]: { + [savedVis.visualizationType as string]: { getDisplayOptions: displayOptions ? () => displayOptions : undefined, initialize: () => {}, } as unknown as Visualization, }, - datasourceMap: {}, + datasourceMap: defaultDatasourceMap, injectFilterReferences: jest.fn(mockInjectFilterReferences), documentToExpression: () => Promise.resolve({ diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.tsx index 1a22222155677..e89e8c61e3185 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable.tsx +++ b/x-pack/plugins/lens/public/embeddable/embeddable.tsx @@ -513,25 +513,23 @@ export class Embeddable private loadUserMessages() { const userMessages: UserMessage[] = []; - if (this.activeVisualizationState && this.activeDatasource) { - userMessages.push( - ...getApplicationUserMessages({ - visualizationType: this.savedVis?.visualizationType, - visualization: { - state: this.activeVisualizationState, - activeId: this.activeVisualizationId, - }, - visualizationMap: this.deps.visualizationMap, - activeDatasource: this.activeDatasource, - activeDatasourceState: { state: this.activeDatasourceState }, - dataViews: { - indexPatterns: this.indexPatterns, - indexPatternRefs: this.indexPatternRefs, // TODO - are these actually used? - }, - core: this.deps.coreStart, - }) - ); - } + userMessages.push( + ...getApplicationUserMessages({ + visualizationType: this.savedVis?.visualizationType, + visualization: { + state: this.activeVisualizationState, + activeId: this.activeVisualizationId, + }, + visualizationMap: this.deps.visualizationMap, + activeDatasource: this.activeDatasource, + activeDatasourceState: { state: this.activeDatasourceState }, + dataViews: { + indexPatterns: this.indexPatterns, + indexPatternRefs: this.indexPatternRefs, // TODO - are these actually used? + }, + core: this.deps.coreStart, + }) + ); const mergedSearchContext = this.getMergedSearchContext(); @@ -634,14 +632,18 @@ export class Embeddable savedObjectId: (input as LensByReferenceInput)?.savedObjectId, }; - const { ast, indexPatterns, indexPatternRefs } = await getExpressionFromDocument( - this.savedVis, - this.deps.documentToExpression - ); + try { + const { ast, indexPatterns, indexPatternRefs } = await getExpressionFromDocument( + this.savedVis, + this.deps.documentToExpression + ); - this.expression = ast; - this.indexPatterns = indexPatterns; - this.indexPatternRefs = indexPatternRefs; + this.expression = ast; + this.indexPatterns = indexPatterns; + this.indexPatternRefs = indexPatternRefs; + } catch { + // nothing, errors should be reported via getUserMessages + } if (metaInfo?.sharingSavedObjectProps?.outcome === 'conflict' && !!this.deps.spaces) { this.addUserMessages([ diff --git a/x-pack/test/functional/apps/lens/group3/error_handling.ts b/x-pack/test/functional/apps/lens/group3/error_handling.ts index 85f9cad73992c..997066343509c 100644 --- a/x-pack/test/functional/apps/lens/group3/error_handling.ts +++ b/x-pack/test/functional/apps/lens/group3/error_handling.ts @@ -129,5 +129,27 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'x-pack/test/functional/fixtures/kbn_archiver/lens/missing_fields' ); }); + + it('displays fundamental configuration issues on dashboard', async () => { + await kibanaServer.importExport.load( + 'x-pack/test/functional/fixtures/kbn_archiver/lens/fundamental_config_errors_on_dashboard' + ); + + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.loadSavedDashboard('lens fundamental config errors dash'); + + const failureElements = await testSubjects.findAll('errorMessageMarkdown'); + const errorMessages = await Promise.all(failureElements.map((el) => el.getVisibleText())); + + expect(errorMessages).to.eql([ + 'Visualization type not found.', + 'The visualization type lnsUNKNOWN could not be resolved.', + 'Could not find datasource for the visualization', + ]); + + await kibanaServer.importExport.unload( + 'x-pack/test/functional/fixtures/kbn_archiver/lens/fundamental_config_errors_on_dashboard' + ); + }); }); } diff --git a/x-pack/test/functional/fixtures/kbn_archiver/lens/fundamental_config_errors_on_dashboard.json b/x-pack/test/functional/fixtures/kbn_archiver/lens/fundamental_config_errors_on_dashboard.json new file mode 100644 index 0000000000000..435c5f43eb1b9 --- /dev/null +++ b/x-pack/test/functional/fixtures/kbn_archiver/lens/fundamental_config_errors_on_dashboard.json @@ -0,0 +1,9 @@ +{"attributes":{"fieldFormatMap":"{\"hour_of_day\":{}}","name":"Kibana Sample Data Logs","runtimeFieldMap":"{\"hour_of_day\":{\"type\":\"long\",\"script\":{\"source\":\"emit(doc['timestamp'].value.getHour());\"}}}","timeFieldName":"timestamp","title":"kibana_sample_data_logs"},"coreMigrationVersion":"8.8.0","created_at":"2023-02-08T22:00:06.671Z","id":"90943e30-9a47-11e8-b64d-95841ca0b247","migrationVersion":{"index-pattern":"8.0.0"},"references":[],"type":"index-pattern","updated_at":"2023-02-08T22:00:06.671Z","version":"WzE1MywxXQ=="} + +{"attributes":{"state":{"adHocDataViews":{},"datasourceStates":{"formBased":{"layers":{"19b05f1b-1187-4c55-abe1-a0bfc45d35f7":{"columnOrder":["bf200ba8-e158-41b4-a701-1121910912e9","b39bfe4c-3699-4f11-a211-aa8cf6d19e1c"],"columns":{"b39bfe4c-3699-4f11-a211-aa8cf6d19e1c":{"dataType":"number","isBucketed":false,"label":"Median of bytes","operationType":"median","params":{"emptyAsNull":true},"scale":"ratio","sourceField":"bytes"},"bf200ba8-e158-41b4-a701-1121910912e9":{"dataType":"date","isBucketed":true,"label":"timestamp","operationType":"date_histogram","params":{"dropPartials":false,"includeEmptyRows":true,"interval":"auto"},"scale":"interval","sourceField":"timestamp"}},"incompleteColumns":{},"sampling":1}}},"textBased":{"layers":{}}},"filters":[],"internalReferences":[],"query":{"language":"kuery","query":""},"visualization":{"axisTitlesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"fittingFunction":"None","gridlinesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"labelsOrientation":{"x":0,"yLeft":0,"yRight":0},"layers":[{"accessors":["b39bfe4c-3699-4f11-a211-aa8cf6d19e1c"],"layerId":"19b05f1b-1187-4c55-abe1-a0bfc45d35f7","layerType":"data","position":"top","seriesType":"bar_stacked","showGridlines":false,"xAccessor":"bf200ba8-e158-41b4-a701-1121910912e9"}],"legend":{"isVisible":true,"position":"right"},"preferredSeriesType":"bar_stacked","tickLabelsVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"valueLabels":"hide"}},"title":"missing visualization type","visualizationType":null},"coreMigrationVersion":"8.8.0","created_at":"2023-02-08T22:00:21.379Z","id":"323d9ca0-a7f8-11ed-a593-e73d16536d96","migrationVersion":{"lens":"8.6.0"},"references":[{"id":"90943e30-9a47-11e8-b64d-95841ca0b247","name":"indexpattern-datasource-layer-19b05f1b-1187-4c55-abe1-a0bfc45d35f7","type":"index-pattern"}],"type":"lens","updated_at":"2023-02-08T22:00:21.379Z","version":"WzE2NywxXQ=="} + +{"attributes":{"state":{"adHocDataViews":{},"datasourceStates":{"formBased":{"layers":{"19b05f1b-1187-4c55-abe1-a0bfc45d35f7":{"columnOrder":["bf200ba8-e158-41b4-a701-1121910912e9","b39bfe4c-3699-4f11-a211-aa8cf6d19e1c"],"columns":{"b39bfe4c-3699-4f11-a211-aa8cf6d19e1c":{"dataType":"number","isBucketed":false,"label":"Median of bytes","operationType":"median","params":{"emptyAsNull":true},"scale":"ratio","sourceField":"bytes"},"bf200ba8-e158-41b4-a701-1121910912e9":{"dataType":"date","isBucketed":true,"label":"timestamp","operationType":"date_histogram","params":{"dropPartials":false,"includeEmptyRows":true,"interval":"auto"},"scale":"interval","sourceField":"timestamp"}},"incompleteColumns":{},"sampling":1}}},"textBased":{"layers":{}}},"filters":[],"internalReferences":[],"query":{"language":"kuery","query":""},"visualization":{"axisTitlesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"fittingFunction":"None","gridlinesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"labelsOrientation":{"x":0,"yLeft":0,"yRight":0},"layers":[{"accessors":["b39bfe4c-3699-4f11-a211-aa8cf6d19e1c"],"layerId":"19b05f1b-1187-4c55-abe1-a0bfc45d35f7","layerType":"data","position":"top","seriesType":"bar_stacked","showGridlines":false,"xAccessor":"bf200ba8-e158-41b4-a701-1121910912e9"}],"legend":{"isVisible":true,"position":"right"},"preferredSeriesType":"bar_stacked","tickLabelsVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"valueLabels":"hide"}},"title":"unknown visualization ID","visualizationType":"lnsUNKNOWN"},"coreMigrationVersion":"8.8.0","created_at":"2023-02-08T22:00:21.379Z","id":"40e08f60-a7f8-11ed-a593-e73d16536d96","migrationVersion":{"lens":"8.6.0"},"references":[{"id":"90943e30-9a47-11e8-b64d-95841ca0b247","name":"indexpattern-datasource-layer-19b05f1b-1187-4c55-abe1-a0bfc45d35f7","type":"index-pattern"}],"type":"lens","updated_at":"2023-02-08T22:00:21.379Z","version":"WzE2OCwxXQ=="} + +{"attributes":{"state":{"adHocDataViews":{},"datasourceStates":{"UNKNOWN":{"layers":{"19b05f1b-1187-4c55-abe1-a0bfc45d35f7":{"columnOrder":["bf200ba8-e158-41b4-a701-1121910912e9","b39bfe4c-3699-4f11-a211-aa8cf6d19e1c"],"columns":{"b39bfe4c-3699-4f11-a211-aa8cf6d19e1c":{"dataType":"number","isBucketed":false,"label":"Median of bytes","operationType":"median","params":{"emptyAsNull":true},"scale":"ratio","sourceField":"bytes"},"bf200ba8-e158-41b4-a701-1121910912e9":{"dataType":"date","isBucketed":true,"label":"timestamp","operationType":"date_histogram","params":{"dropPartials":false,"includeEmptyRows":true,"interval":"auto"},"scale":"interval","sourceField":"timestamp"}},"incompleteColumns":{},"sampling":1}}},"textBased":{"layers":{}}},"filters":[],"internalReferences":[],"query":{"language":"kuery","query":""},"visualization":{"axisTitlesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"fittingFunction":"None","gridlinesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"labelsOrientation":{"x":0,"yLeft":0,"yRight":0},"layers":[{"accessors":["b39bfe4c-3699-4f11-a211-aa8cf6d19e1c"],"layerId":"19b05f1b-1187-4c55-abe1-a0bfc45d35f7","layerType":"data","position":"top","seriesType":"bar_stacked","showGridlines":false,"xAccessor":"bf200ba8-e158-41b4-a701-1121910912e9"}],"legend":{"isVisible":true,"position":"right"},"preferredSeriesType":"bar_stacked","tickLabelsVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"valueLabels":"hide"}},"title":"unknown datasource ID","visualizationType":"lnsXY"},"coreMigrationVersion":"8.8.0","created_at":"2023-02-08T22:00:21.379Z","id":"49600170-a7f8-11ed-a593-e73d16536d96","migrationVersion":{"lens":"8.6.0"},"references":[{"id":"90943e30-9a47-11e8-b64d-95841ca0b247","name":"indexpattern-datasource-layer-19b05f1b-1187-4c55-abe1-a0bfc45d35f7","type":"index-pattern"}],"type":"lens","updated_at":"2023-02-08T22:00:21.379Z","version":"WzE2OSwxXQ=="} + +{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}"},"optionsJSON":"{\"useMargins\":true,\"syncColors\":false,\"syncCursor\":true,\"syncTooltips\":false,\"hidePanelTitles\":false}","panelsJSON":"[{\"version\":\"8.8.0\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":15,\"i\":\"56667b2c-e6c4-49f7-9c07-e27e798d7fea\"},\"panelIndex\":\"56667b2c-e6c4-49f7-9c07-e27e798d7fea\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_56667b2c-e6c4-49f7-9c07-e27e798d7fea\"},{\"version\":\"8.8.0\",\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":0,\"w\":24,\"h\":15,\"i\":\"802298ec-2195-460c-a6ca-5b0a46271584\"},\"panelIndex\":\"802298ec-2195-460c-a6ca-5b0a46271584\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_802298ec-2195-460c-a6ca-5b0a46271584\"},{\"version\":\"8.8.0\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":15,\"w\":24,\"h\":15,\"i\":\"12060b3e-ec82-4c33-9f67-aa713e512211\"},\"panelIndex\":\"12060b3e-ec82-4c33-9f67-aa713e512211\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_12060b3e-ec82-4c33-9f67-aa713e512211\"}]","timeRestore":false,"title":"lens fundamental config errors dash","version":1},"coreMigrationVersion":"8.8.0","created_at":"2023-02-08T22:14:41.552Z","id":"674f1f40-a7f8-11ed-a593-e73d16536d96","migrationVersion":{"dashboard":"8.7.0"},"references":[{"id":"323d9ca0-a7f8-11ed-a593-e73d16536d96","name":"56667b2c-e6c4-49f7-9c07-e27e798d7fea:panel_56667b2c-e6c4-49f7-9c07-e27e798d7fea","type":"lens"},{"id":"40e08f60-a7f8-11ed-a593-e73d16536d96","name":"802298ec-2195-460c-a6ca-5b0a46271584:panel_802298ec-2195-460c-a6ca-5b0a46271584","type":"lens"},{"id":"49600170-a7f8-11ed-a593-e73d16536d96","name":"12060b3e-ec82-4c33-9f67-aa713e512211:panel_12060b3e-ec82-4c33-9f67-aa713e512211","type":"lens"}],"type":"dashboard","updated_at":"2023-02-08T22:14:41.552Z","version":"WzI0MCwxXQ=="} \ No newline at end of file From fcc1d48669a7bbaf495a1e2d31a9c61a7aefb414 Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Mon, 13 Feb 2023 18:10:45 +0100 Subject: [PATCH 14/20] [Security Solution] integrate CellActions in Events/Alerts DataTables (#149934) issue: https://github.com/elastic/kibana/issues/145666 ## Summary Migrate DataTable component, used to render the alerts/events data grids in different pages: Alerts, Explore pages (Hosts, Users, Network), and also Rule preview. In summary, all data grids but Timeline. The integration won't modify any action or change any functionality, everything is supposed to keep working the same way from the User's perspective. But it has a minor UI update in the actions tooltip layout: *old* ![old](https://user-images.githubusercontent.com/17747913/215819948-9ef23f3f-58b2-4bd5-a9c3-6c365c2e9921.png) *new* ![new](https://user-images.githubusercontent.com/17747913/215820225-9a736bf3-db8d-41e2-86c8-b709ef7a5f66.png) This change was needed since the custom tooltip (old one) was adding too many Security-related customizations to the generic EuiDataGrid's _columnCellActions_, and also it had a lot of "action-specific" complexity, which is against the idea of having an action-agnostic generic component, to begin with. So, in order to unify the CellActions user experience (icon, text, layout...), we needed to use a more uniformed component, that will render all the actions without any special case. --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../src/components/cell_actions.tsx | 9 +- ...use_data_grid_column_cell_actions.test.tsx | 68 +++++--- .../use_data_grid_column_cell_actions.tsx | 97 ++++++++--- .../kbn-cell-actions/src/mocks/helpers.ts | 1 - packages/kbn-cell-actions/src/types.ts | 6 - .../common/types/header_actions/index.ts | 1 - .../common/types/timeline/cells/index.ts | 3 - .../alerts_cell_actions.cy.ts | 130 ++++++++++++++ .../investigate_in_timeline.cy.ts | 39 +---- .../cypress/screens/alerts.ts | 18 ++ .../cypress/screens/alerts_details.ts | 2 +- .../cypress/screens/timeline.ts | 3 +- .../security_solution/cypress/tasks/alerts.ts | 26 ++- .../cypress/tasks/search_bar.ts | 5 +- .../show_top_n/default/show_top_n.test.tsx | 3 - .../actions/show_top_n/default/show_top_n.tsx | 7 +- .../show_top_n/show_top_n_component.test.tsx | 3 - .../transform_control_columns.test.tsx | 1 - .../transform_control_columns.tsx | 1 - .../components/data_table/index.test.tsx | 164 +++++++----------- .../common/components/data_table/index.tsx | 120 ++++++------- .../__snapshots__/index.test.tsx.snap | 9 +- .../events_tab/events_query_tab_body.tsx | 2 - .../components/events_viewer/index.test.tsx | 6 +- .../common/components/events_viewer/index.tsx | 13 +- .../public/common/components/page/index.tsx | 11 +- .../common/lib/cell_actions/constants.ts | 16 -- .../expanded_cell_value_actions.test.tsx | 33 ---- .../expanded_cell_value_actions.tsx | 85 --------- .../data_table/epic_local_storage.test.tsx | 2 - .../components/alerts_table/index.tsx | 3 - .../rules/rule_preview/preview_histogram.tsx | 1 + .../preview_table_cell_renderer.tsx | 2 +- .../render_cell_value.tsx | 2 - .../default_cell_renderer.test.tsx | 36 ---- .../cell_rendering/default_cell_renderer.tsx | 50 ++---- .../page_objects/detections/index.ts | 2 +- 37 files changed, 462 insertions(+), 518 deletions(-) create mode 100644 x-pack/plugins/security_solution/cypress/e2e/detection_alerts/alerts_cell_actions.cy.ts delete mode 100644 x-pack/plugins/security_solution/public/common/lib/cell_actions/constants.ts delete mode 100644 x-pack/plugins/security_solution/public/common/lib/cell_actions/expanded_cell_value_actions.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/common/lib/cell_actions/expanded_cell_value_actions.tsx diff --git a/packages/kbn-cell-actions/src/components/cell_actions.tsx b/packages/kbn-cell-actions/src/components/cell_actions.tsx index 013fc372fcd9e..3d843a3168f79 100644 --- a/packages/kbn-cell-actions/src/components/cell_actions.tsx +++ b/packages/kbn-cell-actions/src/components/cell_actions.tsx @@ -23,14 +23,12 @@ export const CellActions: React.FC = ({ metadata, className, }) => { - const extraContentNodeRef = useRef(null); const nodeRef = useRef(null); const actionContext: CellActionExecutionContext = useMemo( () => ({ field, trigger: { id: triggerId }, - extraContentNodeRef, nodeRef, metadata, }), @@ -49,8 +47,6 @@ export const CellActions: React.FC = ({ > {children} - -
); } @@ -62,16 +58,17 @@ export const CellActions: React.FC = ({ ref={nodeRef} gutterSize="none" justifyContent="flexStart" + className={className} + data-test-subj={dataTestSubj} > {children} - + -
); diff --git a/packages/kbn-cell-actions/src/hooks/use_data_grid_column_cell_actions.test.tsx b/packages/kbn-cell-actions/src/hooks/use_data_grid_column_cell_actions.test.tsx index db6a02b918ca1..7cb321a6a3f67 100644 --- a/packages/kbn-cell-actions/src/hooks/use_data_grid_column_cell_actions.test.tsx +++ b/packages/kbn-cell-actions/src/hooks/use_data_grid_column_cell_actions.test.tsx @@ -6,13 +6,14 @@ * Side Public License, v 1. */ -import React, { JSXElementConstructor } from 'react'; +import React, { JSXElementConstructor, MutableRefObject } from 'react'; import { EuiButtonEmpty, EuiDataGridColumnCellActionProps, + EuiDataGridRefProps, type EuiDataGridColumnCellAction, } from '@elastic/eui'; -import { render } from '@testing-library/react'; +import { render, waitFor } from '@testing-library/react'; import { act, renderHook } from '@testing-library/react-hooks'; import { makeAction } from '../mocks/helpers'; import { @@ -35,10 +36,14 @@ const field1 = { name: 'column1', values: ['0.0', '0.1', '0.2', '0.3'], type: 't const field2 = { name: 'column2', values: ['1.0', '1.1', '1.2', '1.3'], type: 'keyword' }; const columns = [{ id: field1.name }, { id: field2.name }]; +const mockCloseCellPopover = jest.fn(); const useDataGridColumnsCellActionsProps: UseDataGridColumnsCellActionsProps = { fields: [field1, field2], triggerId: 'testTriggerId', metadata: { some: 'value' }, + dataGridRef: { + current: { closeCellPopover: mockCloseCellPopover }, + } as unknown as MutableRefObject, }; const renderCellAction = ( @@ -115,7 +120,9 @@ describe('useDataGridColumnsCellActions', () => { cellAction.getByTestId(`dataGridColumnCellAction-${action1.id}`).click(); - expect(action1.execute).toHaveBeenCalled(); + waitFor(() => { + expect(action1.execute).toHaveBeenCalled(); + }); }); it('should execute the action with correct context', async () => { @@ -128,23 +135,27 @@ describe('useDataGridColumnsCellActions', () => { cellAction1.getByTestId(`dataGridColumnCellAction-${action1.id}`).click(); - expect(action1.execute).toHaveBeenCalledWith( - expect.objectContaining({ - field: { name: field1.name, type: field1.type, value: field1.values[1] }, - trigger: { id: useDataGridColumnsCellActionsProps.triggerId }, - }) - ); + await waitFor(() => { + expect(action1.execute).toHaveBeenCalledWith( + expect.objectContaining({ + field: { name: field1.name, type: field1.type, value: field1.values[1] }, + trigger: { id: useDataGridColumnsCellActionsProps.triggerId }, + }) + ); + }); const cellAction2 = renderCellAction(result.current[1][1], { rowIndex: 2 }); cellAction2.getByTestId(`dataGridColumnCellAction-${action2.id}`).click(); - expect(action2.execute).toHaveBeenCalledWith( - expect.objectContaining({ - field: { name: field2.name, type: field2.type, value: field2.values[2] }, - trigger: { id: useDataGridColumnsCellActionsProps.triggerId }, - }) - ); + await waitFor(() => { + expect(action2.execute).toHaveBeenCalledWith( + expect.objectContaining({ + field: { name: field2.name, type: field2.type, value: field2.values[2] }, + trigger: { id: useDataGridColumnsCellActionsProps.triggerId }, + }) + ); + }); }); it('should execute the action with correct page value', async () => { @@ -157,10 +168,27 @@ describe('useDataGridColumnsCellActions', () => { cellAction.getByTestId(`dataGridColumnCellAction-${action1.id}`).click(); - expect(action1.execute).toHaveBeenCalledWith( - expect.objectContaining({ - field: { name: field1.name, type: field1.type, value: field1.values[1] }, - }) - ); + await waitFor(() => { + expect(action1.execute).toHaveBeenCalledWith( + expect.objectContaining({ + field: { name: field1.name, type: field1.type, value: field1.values[1] }, + }) + ); + }); + }); + + it('should close popover then action executed', async () => { + const { result, waitForNextUpdate } = renderHook(useDataGridColumnsCellActions, { + initialProps: useDataGridColumnsCellActionsProps, + }); + await waitForNextUpdate(); + + const cellAction = renderCellAction(result.current[0][0], { rowIndex: 25 }); + + cellAction.getByTestId(`dataGridColumnCellAction-${action1.id}`).click(); + + await waitFor(() => { + expect(mockCloseCellPopover).toHaveBeenCalled(); + }); }); }); diff --git a/packages/kbn-cell-actions/src/hooks/use_data_grid_column_cell_actions.tsx b/packages/kbn-cell-actions/src/hooks/use_data_grid_column_cell_actions.tsx index 0c78561909fc0..2fe9668097f1b 100644 --- a/packages/kbn-cell-actions/src/hooks/use_data_grid_column_cell_actions.tsx +++ b/packages/kbn-cell-actions/src/hooks/use_data_grid_column_cell_actions.tsx @@ -6,8 +6,12 @@ * Side Public License, v 1. */ -import React, { useMemo, useRef } from 'react'; -import { EuiLoadingSpinner, type EuiDataGridColumnCellAction } from '@elastic/eui'; +import React, { MutableRefObject, useCallback, useMemo, useRef } from 'react'; +import { + EuiDataGridRefProps, + EuiLoadingSpinner, + type EuiDataGridColumnCellAction, +} from '@elastic/eui'; import type { CellAction, CellActionCompatibilityContext, @@ -27,11 +31,13 @@ interface BulkField extends Pick { export interface UseDataGridColumnsCellActionsProps extends Pick { fields: BulkField[]; + dataGridRef: MutableRefObject; } export const useDataGridColumnsCellActions = ({ fields, triggerId, metadata, + dataGridRef, disabledActions = [], }: UseDataGridColumnsCellActionsProps): EuiDataGridColumnCellAction[][] => { const bulkContexts: CellActionCompatibilityContext[] = useMemo( @@ -57,15 +63,22 @@ export const useDataGridColumnsCellActions = ({ } return columnsActions.map((actions, columnIndex) => actions.map((action) => - createColumnCellAction({ action, metadata, triggerId, field: fields[columnIndex] }) + createColumnCellAction({ + action, + metadata, + triggerId, + field: fields[columnIndex], + dataGridRef, + }) ) ); - }, [columnsActions, fields, loading, metadata, triggerId]); + }, [columnsActions, fields, loading, metadata, triggerId, dataGridRef]); return columnsCellActions; }; -interface CreateColumnCellActionParams extends Pick { +interface CreateColumnCellActionParams + extends Pick { field: BulkField; action: CellAction; } @@ -74,36 +87,76 @@ const createColumnCellAction = ({ action, metadata, triggerId, + dataGridRef, }: CreateColumnCellActionParams): EuiDataGridColumnCellAction => - function ColumnCellAction({ Component, rowIndex }) { + function ColumnCellAction({ Component, rowIndex, isExpanded }) { const nodeRef = useRef(null); - const extraContentNodeRef = useRef(null); + const buttonRef = useRef(null); - const { name, type, values } = field; - // rowIndex refers to all pages, we need to use the row index relative to the page to get the value - const value = values[rowIndex % values.length]; + const actionContext: CellActionExecutionContext = useMemo(() => { + const { name, type, values } = field; + // rowIndex refers to all pages, we need to use the row index relative to the page to get the value + const value = values[rowIndex % values.length]; + return { + field: { name, type, value }, + trigger: { id: triggerId }, + nodeRef, + metadata, + }; + }, [rowIndex]); - const actionContext: CellActionExecutionContext = { - field: { name, type, value }, - trigger: { id: triggerId }, - extraContentNodeRef, - nodeRef, - metadata, - }; + const onClick = useCallback(async () => { + actionContext.nodeRef.current = await closeAndGetCellElement({ + dataGrid: dataGridRef.current, + isExpanded, + buttonRef, + }); + action.execute(actionContext); + }, [actionContext, isExpanded]); return ( { - action.execute(actionContext); - }} + onClick={onClick} > {action.getDisplayName(actionContext)} -
); }; + +const closeAndGetCellElement = ({ + dataGrid, + isExpanded, + buttonRef, +}: { + dataGrid?: EuiDataGridRefProps | null; + isExpanded: boolean; + buttonRef: MutableRefObject; +}): Promise => + new Promise((resolve) => { + const gridCellElement = isExpanded + ? // if actions popover is expanded the button is outside dataGrid, using euiDataGridRowCell--open class + document.querySelector('div[role="gridcell"].euiDataGridRowCell--open') + : // if not expanded the button is inside the cell, get the parent cell from the button + getParentCellElement(buttonRef.current); + // close the popover if needed + dataGrid?.closeCellPopover(); + // closing the popover updates the cell content, get the first child after all updates + setTimeout(() => { + resolve((gridCellElement?.firstElementChild as HTMLElement) ?? null); + }); + }); + +const getParentCellElement = (element?: HTMLElement | null): HTMLElement | null => { + if (element == null) { + return null; + } + if (element.nodeName === 'div' && element.getAttribute('role') === 'gridcell') { + return element; + } + return getParentCellElement(element.parentElement); +}; diff --git a/packages/kbn-cell-actions/src/mocks/helpers.ts b/packages/kbn-cell-actions/src/mocks/helpers.ts index 75e4399199815..acb1afd1bc21e 100644 --- a/packages/kbn-cell-actions/src/mocks/helpers.ts +++ b/packages/kbn-cell-actions/src/mocks/helpers.ts @@ -32,7 +32,6 @@ export const makeActionContext = ( type: 'keyword', value: 'some value', }, - extraContentNodeRef: {} as MutableRefObject, nodeRef: {} as MutableRefObject, ...override, }); diff --git a/packages/kbn-cell-actions/src/types.ts b/packages/kbn-cell-actions/src/types.ts index aaf2f745f7f61..bb58ebdc69f51 100644 --- a/packages/kbn-cell-actions/src/types.ts +++ b/packages/kbn-cell-actions/src/types.ts @@ -95,16 +95,10 @@ type Metadata = Record | undefined; export interface CellActionExecutionContext extends ActionExecutionContext { field: CellActionField; - /** - * Ref to a DOM node where the action can add custom HTML. - */ - extraContentNodeRef: React.MutableRefObject; - /** * Ref to the node where the cell action are rendered. */ nodeRef: React.MutableRefObject; - /** * Extra configurations for actions. */ diff --git a/x-pack/plugins/security_solution/common/types/header_actions/index.ts b/x-pack/plugins/security_solution/common/types/header_actions/index.ts index 6c533736bf150..0cb080ee23db1 100644 --- a/x-pack/plugins/security_solution/common/types/header_actions/index.ts +++ b/x-pack/plugins/security_solution/common/types/header_actions/index.ts @@ -83,7 +83,6 @@ export type ColumnHeaderOptions = Pick< | 'isResizable' > & { aggregatable?: boolean; - dataTableCellActions?: DataTableCellAction[]; category?: string; columnHeaderType: ColumnHeaderType; description?: string | null; diff --git a/x-pack/plugins/security_solution/common/types/timeline/cells/index.ts b/x-pack/plugins/security_solution/common/types/timeline/cells/index.ts index f5ca9b28628e1..134b659116ee0 100644 --- a/x-pack/plugins/security_solution/common/types/timeline/cells/index.ts +++ b/x-pack/plugins/security_solution/common/types/timeline/cells/index.ts @@ -6,7 +6,6 @@ */ import type { EuiDataGridCellValueElementProps } from '@elastic/eui'; -import type { Filter } from '@kbn/es-query'; import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs'; import type { ColumnHeaderOptions, RowRenderer } from '../..'; import type { BrowserFields, TimelineNonEcsData } from '../../../search_strategy'; @@ -18,7 +17,6 @@ export type CellValueElementProps = EuiDataGridCellValueElementProps & { data: TimelineNonEcsData[]; ecsData?: Ecs; eventId: string; // _id - globalFilters?: Filter[]; header: ColumnHeaderOptions; isDraggable: boolean; isTimeline?: boolean; // Default cell renderer is used for both the alert table and timeline. This allows us to cheaply separate concerns @@ -30,5 +28,4 @@ export type CellValueElementProps = EuiDataGridCellValueElementProps & { truncate?: boolean; key?: string; closeCellPopover?: () => void; - enableActions?: boolean; }; diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/alerts_cell_actions.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/alerts_cell_actions.cy.ts new file mode 100644 index 0000000000000..b322a87929d52 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/alerts_cell_actions.cy.ts @@ -0,0 +1,130 @@ +/* + * 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 { getNewRule } from '../../objects/rule'; +import { CELL_COPY_BUTTON, FILTER_BADGE, SHOW_TOP_N_HEADER } from '../../screens/alerts'; +import { + ALERT_TABLE_FILE_NAME_HEADER, + ALERT_TABLE_FILE_NAME_VALUES, + ALERT_TABLE_SEVERITY_VALUES, + PROVIDER_BADGE, +} from '../../screens/timeline'; + +import { + scrollAlertTableColumnIntoView, + addAlertPropertyToTimeline, + filterForAlertProperty, + showTopNAlertProperty, + clickExpandActions, +} from '../../tasks/alerts'; +import { createCustomRuleEnabled } from '../../tasks/api_calls/rules'; +import { cleanKibana } from '../../tasks/common'; +import { waitForAlertsToPopulate } from '../../tasks/create_new_rule'; +import { login, visit } from '../../tasks/login'; +import { fillAddFilterForm, openAddFilterPopover } from '../../tasks/search_bar'; +import { openActiveTimeline } from '../../tasks/timeline'; + +import { ALERTS_URL } from '../../urls/navigation'; +describe('Alerts cell actions', () => { + before(() => { + cleanKibana(); + login(); + }); + + context('Opening alerts', () => { + before(() => { + createCustomRuleEnabled(getNewRule()); + }); + + beforeEach(() => { + visit(ALERTS_URL); + waitForAlertsToPopulate(); + }); + + describe('Filter', () => { + it('should filter for a non-empty property', () => { + cy.get(ALERT_TABLE_SEVERITY_VALUES) + .first() + .invoke('text') + .then((severityVal) => { + scrollAlertTableColumnIntoView(ALERT_TABLE_FILE_NAME_HEADER); + filterForAlertProperty(ALERT_TABLE_SEVERITY_VALUES, 0); + cy.get(FILTER_BADGE) + .first() + .should('have.text', `kibana.alert.severity: ${severityVal}`); + }); + }); + + it('should filter for an empty property', () => { + // add condition to make sure the field is empty + openAddFilterPopover(); + fillAddFilterForm({ key: 'file.name', operator: 'does not exist' }); + scrollAlertTableColumnIntoView(ALERT_TABLE_FILE_NAME_HEADER); + filterForAlertProperty(ALERT_TABLE_FILE_NAME_VALUES, 0); + cy.get(FILTER_BADGE).first().should('have.text', 'NOT file.name: exists'); + }); + }); + + describe('Add to timeline', () => { + it('should add a non-empty property to default timeline', () => { + cy.get(ALERT_TABLE_SEVERITY_VALUES) + .first() + .invoke('text') + .then((severityVal) => { + scrollAlertTableColumnIntoView(ALERT_TABLE_FILE_NAME_HEADER); + addAlertPropertyToTimeline(ALERT_TABLE_SEVERITY_VALUES, 0); + openActiveTimeline(); + cy.get(PROVIDER_BADGE) + .first() + .should('have.text', `kibana.alert.severity: "${severityVal}"`); + }); + }); + + it('should add an empty property to default timeline', () => { + // add condition to make sure the field is empty + openAddFilterPopover(); + fillAddFilterForm({ key: 'file.name', operator: 'does not exist' }); + scrollAlertTableColumnIntoView(ALERT_TABLE_FILE_NAME_HEADER); + addAlertPropertyToTimeline(ALERT_TABLE_FILE_NAME_VALUES, 0); + openActiveTimeline(); + cy.get(PROVIDER_BADGE).first().should('have.text', 'NOT file.name exists'); + }); + }); + + describe('Show Top N', () => { + it('should show top for a property', () => { + cy.get(ALERT_TABLE_SEVERITY_VALUES) + .first() + .invoke('text') + .then(() => { + scrollAlertTableColumnIntoView(ALERT_TABLE_FILE_NAME_HEADER); + showTopNAlertProperty(ALERT_TABLE_SEVERITY_VALUES, 0); + cy.get(SHOW_TOP_N_HEADER).first().should('have.text', `Top kibana.alert.severity`); + }); + }); + }); + + describe('Copy to clipboard', () => { + it('should copy to clipboard', () => { + cy.get(ALERT_TABLE_SEVERITY_VALUES) + .first() + .invoke('text') + .then(() => { + scrollAlertTableColumnIntoView(ALERT_TABLE_FILE_NAME_HEADER); + cy.window().then((win) => { + cy.stub(win, 'prompt').returns('DISABLED WINDOW PROMPT'); + }); + clickExpandActions(ALERT_TABLE_SEVERITY_VALUES, 0); + cy.get(CELL_COPY_BUTTON).should('exist'); + // We are not able to test the "copy to clipboard" action execution + // due to browsers security limitation accessing the clipboard services. + // We assume external `copy` library works + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/investigate_in_timeline.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/investigate_in_timeline.cy.ts index 9095b5d83f4ff..5f52041e75d17 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/investigate_in_timeline.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/investigate_in_timeline.cy.ts @@ -6,26 +6,15 @@ */ import { getNewRule } from '../../objects/rule'; -import { - ALERT_TABLE_FILE_NAME_HEADER, - ALERT_TABLE_FILE_NAME_VALUES, - ALERT_TABLE_SEVERITY_VALUES, - PROVIDER_BADGE, -} from '../../screens/timeline'; +import { PROVIDER_BADGE } from '../../screens/timeline'; -import { - addAlertPropertyToTimeline, - investigateFirstAlertInTimeline, - scrollAlertTableColumnIntoView, -} from '../../tasks/alerts'; +import { investigateFirstAlertInTimeline } from '../../tasks/alerts'; import { createCustomRuleEnabled } from '../../tasks/api_calls/rules'; import { cleanKibana } from '../../tasks/common'; import { waitForAlertsToPopulate } from '../../tasks/create_new_rule'; import { login, visit } from '../../tasks/login'; -import { openActiveTimeline } from '../../tasks/timeline'; import { ALERTS_URL } from '../../urls/navigation'; -import { fillAddFilterForm, openAddFilterPopover } from '../../tasks/search_bar'; describe('Alerts timeline', () => { before(() => { @@ -48,28 +37,4 @@ describe('Alerts timeline', () => { cy.get(PROVIDER_BADGE).filter(':visible').should('have.text', eventId); }); }); - - it('Add a non-empty property to default timeline', () => { - cy.get(ALERT_TABLE_SEVERITY_VALUES) - .first() - .invoke('text') - .then((severityVal) => { - scrollAlertTableColumnIntoView(ALERT_TABLE_FILE_NAME_HEADER); - addAlertPropertyToTimeline(ALERT_TABLE_SEVERITY_VALUES, 0); - openActiveTimeline(); - cy.get(PROVIDER_BADGE) - .first() - .should('have.text', `kibana.alert.severity: "${severityVal}"`); - }); - }); - - it('Add an empty property to default timeline', () => { - // add condition to make sure the field is empty - openAddFilterPopover(); - fillAddFilterForm({ key: 'file.name', operator: 'does not exist' }); - scrollAlertTableColumnIntoView(ALERT_TABLE_FILE_NAME_HEADER); - addAlertPropertyToTimeline(ALERT_TABLE_FILE_NAME_VALUES, 0); - openActiveTimeline(); - cy.get(PROVIDER_BADGE).first().should('have.text', 'NOT file.name exists'); - }); }); diff --git a/x-pack/plugins/security_solution/cypress/screens/alerts.ts b/x-pack/plugins/security_solution/cypress/screens/alerts.ts index 046962a5d9685..1b5ca92e1eb1c 100644 --- a/x-pack/plugins/security_solution/cypress/screens/alerts.ts +++ b/x-pack/plugins/security_solution/cypress/screens/alerts.ts @@ -134,3 +134,21 @@ export const EVENT_CONTAINER_TABLE_LOADING = '[data-test-subj="events-container- export const EVENT_CONTAINER_TABLE_NOT_LOADING = '[data-test-subj="events-container-loading-false"]'; + +export const FILTER_BADGE = '[data-test-subj^="filter-badge"]'; + +export const CELL_FILTER_IN_BUTTON = + '[data-test-subj="dataGridColumnCellAction-security_filterIn"]'; +export const CELL_FILTER_OUT_BUTTON = + '[data-test-subj="dataGridColumnCellAction-security_filterOut"]'; +export const CELL_ADD_TO_TIMELINE_BUTTON = + '[data-test-subj="dataGridColumnCellAction-security_addToTimeline"]'; +export const CELL_SHOW_TOP_FIELD_BUTTON = + '[data-test-subj="dataGridColumnCellAction-security_showTopN"]'; +export const CELL_COPY_BUTTON = + '[data-test-subj="dataGridColumnCellAction-security_copyToClipboard"]'; + +export const ACTIONS_EXPAND_BUTTON = '[data-test-subj="euiDataGridCellExpandButton"]'; + +export const SHOW_TOP_N_HEADER = + '[data-test-subj="topN-container"] [data-test-subj="header-section-title"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/alerts_details.ts b/x-pack/plugins/security_solution/cypress/screens/alerts_details.ts index 9a1ac0b8d08f1..23b15524305ed 100644 --- a/x-pack/plugins/security_solution/cypress/screens/alerts_details.ts +++ b/x-pack/plugins/security_solution/cypress/screens/alerts_details.ts @@ -50,7 +50,7 @@ export const CELL_EXPAND_VALUE = '[data-test-subj="euiDataGridCellExpandButton"] export const CELL_EXPANSION_POPOVER = '[data-test-subj="euiDataGridExpansionPopover"]'; -export const USER_DETAILS_LINK = '[data-test-subj="data-grid-user-details"]'; +export const USER_DETAILS_LINK = '[data-test-subj="users-link-anchor"]'; export const TABLE_TAB = '[data-test-subj="tableTab"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/timeline.ts b/x-pack/plugins/security_solution/cypress/screens/timeline.ts index 670c709b67bee..a9cdb896cdea4 100644 --- a/x-pack/plugins/security_solution/cypress/screens/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/screens/timeline.ts @@ -296,7 +296,8 @@ export const ALERT_TABLE_FILE_NAME_HEADER = '[data-gridcell-column-id="file.name export const ALERT_TABLE_FILE_NAME_VALUES = '[data-gridcell-column-id="file.name"][data-test-subj="dataGridRowCell"]'; // empty column for the test data -export const ALERT_TABLE_CELL_ACTIONS_ADD_TO_TIMELINE = '[data-test-subj="add-to-timeline"]'; +export const ALERT_TABLE_CELL_ACTIONS_ADD_TO_TIMELINE = + '[data-test-subj="dataGridColumnCellAction-security_addToTimeline"]'; export const ACTIVE_TIMELINE_BOTTOM_BAR = '[data-test-subj="flyoutBottomBar"] .active-timeline-button'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/alerts.ts b/x-pack/plugins/security_solution/cypress/tasks/alerts.ts index c7810db6ae21d..a17d4bb004671 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/alerts.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/alerts.ts @@ -32,12 +32,13 @@ import { CLOSED_ALERTS_FILTER_BTN, OPENED_ALERTS_FILTER_BTN, ACKNOWLEDGED_ALERTS_FILTER_BTN, + CELL_ADD_TO_TIMELINE_BUTTON, + CELL_FILTER_IN_BUTTON, + CELL_SHOW_TOP_FIELD_BUTTON, + ACTIONS_EXPAND_BUTTON, } from '../screens/alerts'; import { LOADING_INDICATOR, REFRESH_BUTTON } from '../screens/security_header'; -import { - ALERT_TABLE_CELL_ACTIONS_ADD_TO_TIMELINE, - TIMELINE_COLUMN_SPINNER, -} from '../screens/timeline'; +import { TIMELINE_COLUMN_SPINNER } from '../screens/timeline'; import { UPDATE_ENRICHMENT_RANGE_BUTTON, ENRICHMENT_QUERY_END_INPUT, @@ -299,9 +300,22 @@ export const openAnalyzerForFirstAlertInTimeline = () => { cy.get(OPEN_ANALYZER_BTN).first().click({ force: true }); }; -export const addAlertPropertyToTimeline = (propertySelector: string, rowIndex: number) => { +const clickAction = (propertySelector: string, rowIndex: number, actionSelector: string) => { cy.get(propertySelector).eq(rowIndex).trigger('mouseover'); - cy.get(ALERT_TABLE_CELL_ACTIONS_ADD_TO_TIMELINE).first().click({ force: true }); + cy.get(actionSelector).first().click({ force: true }); +}; +export const clickExpandActions = (propertySelector: string, rowIndex: number) => { + clickAction(propertySelector, rowIndex, ACTIONS_EXPAND_BUTTON); +}; +export const addAlertPropertyToTimeline = (propertySelector: string, rowIndex: number) => { + clickAction(propertySelector, rowIndex, CELL_ADD_TO_TIMELINE_BUTTON); +}; +export const filterForAlertProperty = (propertySelector: string, rowIndex: number) => { + clickAction(propertySelector, rowIndex, CELL_FILTER_IN_BUTTON); +}; +export const showTopNAlertProperty = (propertySelector: string, rowIndex: number) => { + clickExpandActions(propertySelector, rowIndex); + cy.get(CELL_SHOW_TOP_FIELD_BUTTON).first().click({ force: true }); }; export const waitForAlerts = () => { diff --git a/x-pack/plugins/security_solution/cypress/tasks/search_bar.ts b/x-pack/plugins/security_solution/cypress/tasks/search_bar.ts index fb5e978befdda..fa3dbbc2589e5 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/search_bar.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/search_bar.ts @@ -14,7 +14,6 @@ import { ADD_FILTER_FORM_FIELD_INPUT, ADD_FILTER_FORM_OPERATOR_OPTION_IS, ADD_FILTER_FORM_OPERATOR_FIELD, - ADD_FILTER_FORM_FIELD_OPTION, ADD_FILTER_FORM_FILTER_VALUE_INPUT, GLOBAL_KQL_INPUT, } from '../screens/search_bar'; @@ -38,9 +37,7 @@ export const fillKqlQueryBar = (query: string) => { export const fillAddFilterForm = ({ key, value, operator }: SearchBarFilter) => { cy.get(ADD_FILTER_FORM_FIELD_INPUT).should('exist'); cy.get(ADD_FILTER_FORM_FIELD_INPUT).should('be.visible'); - cy.get(ADD_FILTER_FORM_FIELD_INPUT).type(`${key}{downarrow}`); - cy.get(ADD_FILTER_FORM_FIELD_INPUT).click(); - cy.get(ADD_FILTER_FORM_FIELD_OPTION(key)).click({ force: true }); + cy.get(ADD_FILTER_FORM_FIELD_INPUT).type(`${key}{downarrow}{enter}`); if (!operator) { cy.get(ADD_FILTER_FORM_OPERATOR_FIELD).click(); cy.get(ADD_FILTER_FORM_OPERATOR_OPTION_IS).click(); diff --git a/x-pack/plugins/security_solution/public/actions/show_top_n/default/show_top_n.test.tsx b/x-pack/plugins/security_solution/public/actions/show_top_n/default/show_top_n.test.tsx index 26b7ab8e5c052..a2e047bc5e415 100644 --- a/x-pack/plugins/security_solution/public/actions/show_top_n/default/show_top_n.test.tsx +++ b/x-pack/plugins/security_solution/public/actions/show_top_n/default/show_top_n.test.tsx @@ -53,9 +53,6 @@ describe('createShowTopNAction', () => { const context = { field: { name: 'user.name', value: 'the-value', type: 'keyword', aggregatable: true }, trigger: { id: 'trigger' }, - extraContentNodeRef: { - current: element, - }, nodeRef: { current: element, }, diff --git a/x-pack/plugins/security_solution/public/actions/show_top_n/default/show_top_n.tsx b/x-pack/plugins/security_solution/public/actions/show_top_n/default/show_top_n.tsx index 4ff35bea5118c..215033409a46f 100644 --- a/x-pack/plugins/security_solution/public/actions/show_top_n/default/show_top_n.tsx +++ b/x-pack/plugins/security_solution/public/actions/show_top_n/default/show_top_n.tsx @@ -68,11 +68,14 @@ export const createShowTopNAction = ({ !UNSUPPORTED_FIELD_TYPES.includes(field.type) && !!field.aggregatable, execute: async (context) => { - const node = context.extraContentNodeRef?.current; - if (!node) return; + if (!context.nodeRef.current) return; + + const node = document.createElement('div'); + document.body.appendChild(node); const onClose = () => { unmountComponentAtNode(node); + document.body.removeChild(node); }; const element = ( diff --git a/x-pack/plugins/security_solution/public/actions/show_top_n/show_top_n_component.test.tsx b/x-pack/plugins/security_solution/public/actions/show_top_n/show_top_n_component.test.tsx index 49ddc83563fd7..1737a65596a78 100644 --- a/x-pack/plugins/security_solution/public/actions/show_top_n/show_top_n_component.test.tsx +++ b/x-pack/plugins/security_solution/public/actions/show_top_n/show_top_n_component.test.tsx @@ -35,9 +35,6 @@ const context = { nodeRef: { current: element, }, - extraContentNodeRef: { - current: null, - }, } as CellActionExecutionContext; describe('TopNAction', () => { diff --git a/x-pack/plugins/security_solution/public/common/components/control_columns/transform_control_columns.test.tsx b/x-pack/plugins/security_solution/public/common/components/control_columns/transform_control_columns.test.tsx index eb0c2379ff609..e4596cd1fb7f3 100644 --- a/x-pack/plugins/security_solution/public/common/components/control_columns/transform_control_columns.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/control_columns/transform_control_columns.test.tsx @@ -19,7 +19,6 @@ describe('transformControlColumns', () => { setEventsDeleted: jest.fn(), columnHeaders: [], controlColumns: [], - disabledCellActions: [], selectedEventIds: {}, tabType: '', isSelectAllChecked: false, diff --git a/x-pack/plugins/security_solution/public/common/components/control_columns/transform_control_columns.tsx b/x-pack/plugins/security_solution/public/common/components/control_columns/transform_control_columns.tsx index 94980890d1530..651310755b183 100644 --- a/x-pack/plugins/security_solution/public/common/components/control_columns/transform_control_columns.tsx +++ b/x-pack/plugins/security_solution/public/common/components/control_columns/transform_control_columns.tsx @@ -34,7 +34,6 @@ export interface TransformColumnsProps { columnHeaders: ColumnHeaderOptions[]; controlColumns: ControlColumnProps[]; data: TimelineItem[]; - disabledCellActions: string[]; fieldBrowserOptions?: FieldBrowserOptions; loadingEventIds: string[]; onRowSelected: OnRowSelected; diff --git a/x-pack/plugins/security_solution/public/common/components/data_table/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/data_table/index.test.tsx index b4110c1e78340..527eb6e3d0ab9 100644 --- a/x-pack/plugins/security_solution/public/common/components/data_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/data_table/index.test.tsx @@ -14,21 +14,32 @@ import { REMOVE_COLUMN } from './column_headers/translations'; import { useMountAppended } from '../../utils/use_mount_appended'; import type { EuiDataGridColumn } from '@elastic/eui'; import { defaultHeaders, mockGlobalState, mockTimelineData, TestProviders } from '../../mock'; -import { defaultColumnHeaderType } from '../../store/data_table/defaults'; import { mockBrowserFields } from '../../containers/source/mock'; import { getMappedNonEcsValue } from '../../../timelines/components/timeline/body/data_driven_columns'; import type { CellValueElementProps } from '../../../../common/types'; import { TableId } from '../../../../common/types'; +import { CELL_ACTIONS_DEFAULT_TRIGGER } from '../../../../common/constants'; const mockDispatch = jest.fn(); -jest.mock('react-redux', () => { - const original = jest.requireActual('react-redux'); +jest.mock('react-redux', () => ({ + ...jest.requireActual('react-redux'), + useDispatch: () => mockDispatch, +})); - return { - ...original, - useDispatch: () => mockDispatch, - }; -}); +const mockUseDataGridColumnsCellActions = jest.fn( + (_: object): Array JSX.Element>> => [] +); +jest.mock('@kbn/cell-actions', () => ({ + ...jest.requireActual('@kbn/cell-actions'), + useDataGridColumnsCellActions: (params: object) => mockUseDataGridColumnsCellActions(params), +})); + +const headersJustTimestamp = defaultHeaders.filter((h) => h.id === '@timestamp'); +const mockGetColumnHeaders = jest.fn(() => headersJustTimestamp); +jest.mock('./column_headers/helpers', () => ({ + ...jest.requireActual('./column_headers/helpers'), + getColumnHeaders: () => mockGetColumnHeaders(), +})); jest.mock('@kbn/kibana-react-plugin/public', () => { const originalModule = jest.requireActual('@kbn/kibana-react-plugin/public'); @@ -80,8 +91,6 @@ describe('DataTable', () => { const props: DataTableProps = { browserFields: mockBrowserFields, data: mockTimelineData, - defaultCellActions: [], - disabledCellActions: ['signal.rule.risk_score', 'signal.reason'], id: TableId.test, loadPage: jest.fn(), renderCellValue: TestCellRenderer, @@ -98,7 +107,8 @@ describe('DataTable', () => { }; beforeEach(() => { - mockDispatch.mockReset(); + mockDispatch.mockClear(); + mockUseDataGridColumnsCellActions.mockClear(); }); describe('rendering', () => { @@ -142,10 +152,8 @@ describe('DataTable', () => { }); test('it renders cell value', () => { - const headersJustTimestamp = defaultHeaders.filter((h) => h.id === '@timestamp'); const testProps = { ...props, - columnHeaders: headersJustTimestamp, data: mockTimelineData.slice(0, 1), }; const wrapper = mount( @@ -163,49 +171,55 @@ describe('DataTable', () => { .text() ).toEqual(mockTimelineData[0].ecs.timestamp); }); + }); - test('timestamp column renders cell actions', () => { - const headersJustTimestamp = defaultHeaders.filter((h) => h.id === '@timestamp'); - const testProps = { - ...props, - columnHeaders: headersJustTimestamp, - data: mockTimelineData.slice(0, 1), - }; + describe('cellActions', () => { + test('calls useDataGridColumnsCellActions properly', () => { + const data = mockTimelineData.slice(0, 1); const wrapper = mount( - + ); wrapper.update(); - expect( - wrapper - .find('[data-test-subj="body-data-grid"]') - .first() - .prop('columns') - .find((c) => c.id === '@timestamp')?.cellActions - ).toBeDefined(); + expect(mockUseDataGridColumnsCellActions).toHaveBeenCalledWith({ + triggerId: CELL_ACTIONS_DEFAULT_TRIGGER, + fields: [ + { + name: '@timestamp', + values: [data[0]?.data[0]?.value], + type: 'date', + aggregatable: true, + }, + ], + metadata: { + scopeId: 'table-test', + }, + dataGridRef: expect.any(Object), + }); }); - test("signal.rule.risk_score column doesn't render cell actions", () => { - const columnHeaders = [ - { - category: 'signal', - columnHeaderType: defaultColumnHeaderType, - id: 'signal.rule.risk_score', - type: 'number', - aggregatable: true, - initialWidth: 105, - }, - ]; - const testProps = { - ...props, - columnHeaders, - data: mockTimelineData.slice(0, 1), - }; + test('does not render cell actions if disableCellActions is true', () => { const wrapper = mount( - + + + ); + wrapper.update(); + + expect(mockUseDataGridColumnsCellActions).toHaveBeenCalledWith( + expect.objectContaining({ + fields: [], + }) + ); + }); + + test('does not render cell actions if empty actions returned', () => { + mockUseDataGridColumnsCellActions.mockReturnValueOnce([]); + const wrapper = mount( + + ); wrapper.update(); @@ -215,29 +229,15 @@ describe('DataTable', () => { .find('[data-test-subj="body-data-grid"]') .first() .prop('columns') - .find((c) => c.id === 'signal.rule.risk_score')?.cellActions - ).toBeUndefined(); + .find((c) => c.id === '@timestamp')?.cellActions + ).toHaveLength(0); }); - test("signal.reason column doesn't render cell actions", () => { - const columnHeaders = [ - { - category: 'signal', - columnHeaderType: defaultColumnHeaderType, - id: 'signal.reason', - type: 'string', - aggregatable: true, - initialWidth: 450, - }, - ]; - const testProps = { - ...props, - columnHeaders, - data: mockTimelineData.slice(0, 1), - }; + test('renders returned cell actions', () => { + mockUseDataGridColumnsCellActions.mockReturnValueOnce([[() =>
]]); const wrapper = mount( - + ); wrapper.update(); @@ -247,43 +247,11 @@ describe('DataTable', () => { .find('[data-test-subj="body-data-grid"]') .first() .prop('columns') - .find((c) => c.id === 'signal.reason')?.cellActions - ).toBeUndefined(); + .find((c) => c.id === '@timestamp')?.cellActions + ).toHaveLength(1); }); }); - test("signal.rule.risk_score column doesn't render cell actions", () => { - const columnHeaders = [ - { - category: 'signal', - columnHeaderType: defaultColumnHeaderType, - id: 'signal.rule.risk_score', - type: 'number', - aggregatable: true, - initialWidth: 105, - }, - ]; - const testProps = { - ...props, - columnHeaders, - data: mockTimelineData.slice(0, 1), - }; - const wrapper = mount( - - - - ); - wrapper.update(); - - expect( - wrapper - .find('[data-test-subj="body-data-grid"]') - .first() - .prop('columns') - .find((c) => c.id === 'signal.rule.risk_score')?.cellActions - ).toBeUndefined(); - }); - test('it does NOT render switches for hiding columns in the `EuiDataGrid` `Columns` popover', async () => { render( diff --git a/x-pack/plugins/security_solution/public/common/components/data_table/index.tsx b/x-pack/plugins/security_solution/public/common/components/data_table/index.tsx index 85189cd221b2f..bfddac06f64b5 100644 --- a/x-pack/plugins/security_solution/public/common/components/data_table/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/data_table/index.tsx @@ -22,11 +22,13 @@ import React, { useCallback, useEffect, useMemo, useContext, useRef } from 'reac import { useDispatch } from 'react-redux'; import styled, { ThemeContext } from 'styled-components'; -import type { Filter } from '@kbn/es-query'; import type { EuiTheme } from '@kbn/kibana-react-plugin/common'; import type { FieldBrowserOptions } from '@kbn/triggers-actions-ui-plugin/public'; import { i18n } from '@kbn/i18n'; -import type { DataTableCellAction } from '../../../../common/types'; +import { + useDataGridColumnsCellActions, + type UseDataGridColumnsCellActionsProps, +} from '@kbn/cell-actions'; import type { CellValueElementProps, ColumnHeaderOptions, @@ -36,12 +38,7 @@ import type { import type { TimelineItem } from '../../../../common/search_strategy/timeline'; import { getColumnHeader, getColumnHeaders } from './column_headers/helpers'; -import { - addBuildingBlockStyle, - hasCellActions, - mapSortDirectionToDirection, - mapSortingColumns, -} from './helpers'; +import { addBuildingBlockStyle, mapSortDirectionToDirection, mapSortingColumns } from './helpers'; import type { BrowserFields } from '../../../../common/search_strategy/index_fields'; import { REMOVE_COLUMN } from './column_headers/translations'; @@ -52,6 +49,7 @@ import { getPageRowIndex } from './pagination'; import { UnitCount } from '../toolbar/unit'; import { useShallowEqualSelector } from '../../hooks/use_selector'; import { tableDefaults } from '../../store/data_table/defaults'; +import { CELL_ACTIONS_DEFAULT_TRIGGER } from '../../../../common/constants'; const DATA_TABLE_ARIA_LABEL = i18n.translate('xpack.securitySolution.dataTable.ariaLabel', { defaultMessage: 'Alerts', @@ -62,10 +60,8 @@ export interface DataTableProps { browserFields: BrowserFields; bulkActions?: BulkActionsProp; data: TimelineItem[]; - defaultCellActions?: DataTableCellAction[]; - disabledCellActions: string[]; + disableCellActions?: boolean; fieldBrowserOptions?: FieldBrowserOptions; - filters?: Filter[]; id: string; leadingControlColumns: EuiDataGridControlColumn[]; loadPage: (newActivePage: number) => void; @@ -107,7 +103,7 @@ const EuiDataGridContainer = styled.div<{ hideLastPage: boolean }>` } `; -const memoizedColumnHeaders: ( +const memoizedGetColumnHeaders: ( headers: ColumnHeaderOptions[], browserFields: BrowserFields, isEventRenderedView: boolean @@ -119,10 +115,8 @@ export const DataTableComponent = React.memo( browserFields, bulkActions = true, data, - defaultCellActions, - disabledCellActions, + disableCellActions = false, fieldBrowserOptions, - filters, hasCrudPermissions, id, leadingControlColumns, @@ -143,7 +137,7 @@ export const DataTableComponent = React.memo( const { columns, selectedEventIds, showCheckboxes, sort, isLoading, defaultColumns } = dataTable; - const columnHeaders = memoizedColumnHeaders(columns, browserFields, isEventRenderedView); + const columnHeaders = memoizedGetColumnHeaders(columns, browserFields, isEventRenderedView); const dataGridRef = useRef(null); @@ -309,57 +303,52 @@ export const DataTableComponent = React.memo( [dispatch, id] ); + const columnsCellActionsProps = useMemo((): UseDataGridColumnsCellActionsProps => { + const fields: UseDataGridColumnsCellActionsProps['fields'] = disableCellActions + ? [] + : columnHeaders.map((column) => ({ + name: column.id, + type: column.type ?? 'keyword', + values: data.map( + ({ data: columnData }) => + columnData.find((rowData) => rowData.field === column.id)?.value + ), + aggregatable: column.aggregatable, + })); + + return { + triggerId: CELL_ACTIONS_DEFAULT_TRIGGER, + fields, + metadata: { + scopeId: id, + }, + dataGridRef, + }; + }, [disableCellActions, columnHeaders, data, id]); + + const columnsCellActions = useDataGridColumnsCellActions(columnsCellActionsProps); + const columnsWithCellActions: EuiDataGridColumn[] = useMemo( () => - columnHeaders.map((header) => { - const buildAction = (dataTableCellAction: DataTableCellAction) => - dataTableCellAction({ - browserFields, - data: data.map((row) => row.data), - ecsData: data.map((row) => row.ecs), - header: columnHeaders.find((h) => h.id === header.id), - pageSize: pagination.pageSize, - scopeId: id, - closeCellPopover: dataGridRef.current?.closeCellPopover, - }); - return { - ...header, - actions: { - ...header.actions, - additional: [ - { - iconType: 'cross', - label: REMOVE_COLUMN, - onClick: () => { - dispatch(dataTableActions.removeColumn({ id, columnId: header.id })); - }, - size: 'xs', + columnHeaders.map((header, columnIndex) => ({ + ...header, + actions: { + ...header.actions, + additional: [ + { + iconType: 'cross', + label: REMOVE_COLUMN, + onClick: () => { + dispatch(dataTableActions.removeColumn({ id, columnId: header.id })); }, - ], - }, - ...(hasCellActions({ - columnId: header.id, - disabledCellActions, - }) - ? { - cellActions: - header.dataTableCellActions?.map(buildAction) ?? - defaultCellActions?.map(buildAction), - visibleCellActions: 3, - } - : {}), - }; - }), - [ - browserFields, - columnHeaders, - data, - defaultCellActions, - disabledCellActions, - dispatch, - id, - pagination.pageSize, - ] + size: 'xs', + }, + ], + }, + cellActions: columnsCellActions[columnIndex] ?? [], + visibleCellActions: 3, + })), + [columnHeaders, columnsCellActions, dispatch, id] ); const renderTableCellValue = useMemo(() => { @@ -392,12 +381,12 @@ export const DataTableComponent = React.memo( } return renderCellValue({ + asPlainText: false, browserFields, columnId: header.id, data: rowData, ecsData: ecs, eventId, - globalFilters: filters, header, isDetails, isDraggable: false, @@ -417,7 +406,6 @@ export const DataTableComponent = React.memo( browserFields, columnHeaders, data, - filters, id, pagination.pageSize, renderCellValue, diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/overview/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/event_details/overview/__snapshots__/index.test.tsx.snap index 23d6db8adf75e..89d876e5efa88 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/overview/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/event_details/overview/__snapshots__/index.test.tsx.snap @@ -116,19 +116,18 @@ exports[`Event Details Overview Cards renders rows and spacers correctly 1`] = ` >
-
@@ -163,19 +162,18 @@ exports[`Event Details Overview Cards renders rows and spacers correctly 1`] = ` >
-
@@ -222,19 +220,18 @@ exports[`Event Details Overview Cards renders rows and spacers correctly 1`] = ` >
-
diff --git a/x-pack/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.tsx b/x-pack/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.tsx index a72e0aa35f3ad..af8b54f6f5c90 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.tsx @@ -30,7 +30,6 @@ import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell import { SourcererScopeName } from '../../store/sourcerer/model'; import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features'; import { DEFAULT_COLUMN_MIN_WIDTH } from '../../../timelines/components/timeline/body/constants'; -import { defaultCellActions } from '../../lib/cell_actions/default_cell_actions'; import type { GlobalTimeArgs } from '../../containers/use_global_time'; import type { QueryTabBodyProps as UserQueryTabBodyProps } from '../../../explore/users/pages/navigation/types'; import type { QueryTabBodyProps as HostQueryTabBodyProps } from '../../../explore/hosts/pages/navigation/types'; @@ -182,7 +181,6 @@ const EventsQueryTabBodyComponent: React.FC = )} void; - renderCellValue: (props: CellValueElementProps) => React.ReactNode; + renderCellValue: React.FC; rowRenderers: RowRenderer[]; additionalFilters?: React.ReactNode; hasCrudPermissions?: boolean; @@ -105,8 +103,8 @@ export interface EventsViewerProps { * NOTE: As of writting, it is not used in the Case_View component */ const StatefulEventsViewerComponent: React.FC = ({ - defaultCellActions, defaultModel, + disableCellActions, end, entityType = 'events', tableId, @@ -441,7 +439,6 @@ const StatefulEventsViewerComponent: React.FC css` SIDE EFFECT: the following `createGlobalStyle` overrides default styling in angular code that was not theme-friendly and `EuiPopover`, `EuiToolTip` global styles */ -export const AppGlobalStyle = createGlobalStyle<{ theme: { eui: { euiColorPrimary: string } } }>` +export const AppGlobalStyle = createGlobalStyle<{ + theme: { eui: { euiColorPrimary: string; euiColorLightShade: string; euiSizeS: string } }; +}>` ${TIMELINE_OVERRIDES_CSS_STYLESHEET} @@ -103,11 +105,16 @@ export const AppGlobalStyle = createGlobalStyle<{ theme: { eui: { euiColorPrimar .euiPopoverFooter { border: 0; - margin-top: 0 !important; + margin-top: 0; .euiFlexGroup { flex-direction: column; } } + + .euiText + .euiPopoverFooter { + border-top: 1px solid ${({ theme }) => theme.eui.euiColorLightShade}; + margin-top: ${({ theme }) => theme.eui.euiSizeS}; + } } /* overrides default styling in angular code that was not theme-friendly */ diff --git a/x-pack/plugins/security_solution/public/common/lib/cell_actions/constants.ts b/x-pack/plugins/security_solution/public/common/lib/cell_actions/constants.ts deleted file mode 100644 index bccd4efa8f98c..0000000000000 --- a/x-pack/plugins/security_solution/public/common/lib/cell_actions/constants.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ALERT_RISK_SCORE } from '@kbn/rule-data-utils'; - -/** actions are disabled for these fields in tables and popovers */ -export const FIELDS_WITHOUT_CELL_ACTIONS = [ - 'signal.rule.risk_score', - 'signal.reason', - ALERT_RISK_SCORE, - 'kibana.alert.reason', -]; diff --git a/x-pack/plugins/security_solution/public/common/lib/cell_actions/expanded_cell_value_actions.test.tsx b/x-pack/plugins/security_solution/public/common/lib/cell_actions/expanded_cell_value_actions.test.tsx deleted file mode 100644 index e20c4887c0df9..0000000000000 --- a/x-pack/plugins/security_solution/public/common/lib/cell_actions/expanded_cell_value_actions.test.tsx +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { shallow } from 'enzyme'; -import React from 'react'; -import { ExpandedCellValueActions } from './expanded_cell_value_actions'; -import type { ColumnHeaderType } from '@kbn/timelines-plugin/common/types'; - -jest.mock('../kibana'); - -describe('ExpandedCellValueActions', () => { - const props = { - field: { - id: 'host.name', - type: 'keyword', - columnHeaderType: 'not-filtered' as ColumnHeaderType, - aggregatable: true, - }, - globalFilters: [], - onFilterAdded: () => {}, - scopeId: 'mockTimelineId', - value: ['mock value'], - }; - const wrapper = shallow(); - - test('renders show topN button', () => { - expect(wrapper.find('[data-test-subj="data-grid-expanded-show-top-n"]').exists()).toBeTruthy(); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/lib/cell_actions/expanded_cell_value_actions.tsx b/x-pack/plugins/security_solution/public/common/lib/cell_actions/expanded_cell_value_actions.tsx deleted file mode 100644 index 4d36c450fd177..0000000000000 --- a/x-pack/plugins/security_solution/public/common/lib/cell_actions/expanded_cell_value_actions.tsx +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiButtonEmpty } from '@elastic/eui'; -import { noop } from 'lodash/fp'; -import React, { useMemo, useState, useCallback } from 'react'; -import styled from 'styled-components'; -import type { Filter } from '@kbn/es-query'; -import type { ColumnHeaderOptions } from '../../../../common/types'; -import { allowTopN } from '../../components/drag_and_drop/helpers'; -import { ShowTopNButton } from '../../components/hover_actions/actions/show_top_n'; -import { SHOW_TOP_VALUES, HIDE_TOP_VALUES } from './translations'; - -interface Props { - field: ColumnHeaderOptions; - globalFilters?: Filter[]; - scopeId: string; - value: string[] | undefined; - onFilterAdded?: () => void; -} - -const StyledContent = styled.div<{ $isDetails: boolean }>` - border-bottom: 1px solid #d3dae6; - padding: ${({ $isDetails }) => ($isDetails ? '0 8px' : undefined)}; -`; - -const ExpandedCellValueActionsComponent: React.FC = ({ - field, - globalFilters, - onFilterAdded, - scopeId, - value, -}) => { - const showButton = useMemo( - () => - allowTopN({ - fieldName: field.id, - fieldType: field.type ?? '', - isAggregatable: field.aggregatable ?? false, - hideTopN: false, - }), - [field] - ); - - const [showTopN, setShowTopN] = useState(false); - const onClick = useCallback(() => setShowTopN(!showTopN), [showTopN]); - - return ( - <> - - {showButton ? ( - - ) : null} - - - ); -}; - -ExpandedCellValueActionsComponent.displayName = 'ExpandedCellValueActionsComponent'; - -export const ExpandedCellValueActions = React.memo(ExpandedCellValueActionsComponent); diff --git a/x-pack/plugins/security_solution/public/common/store/data_table/epic_local_storage.test.tsx b/x-pack/plugins/security_solution/public/common/store/data_table/epic_local_storage.test.tsx index 4eb160b644904..0ffd6c33aa3ed 100644 --- a/x-pack/plugins/security_solution/public/common/store/data_table/epic_local_storage.test.tsx +++ b/x-pack/plugins/security_solution/public/common/store/data_table/epic_local_storage.test.tsx @@ -40,7 +40,6 @@ import { addTableInStorage } from '../../../timelines/containers/local_storage'; import { Direction } from '../../../../common/search_strategy'; import { StatefulEventsViewer } from '../../components/events_viewer'; import { eventsDefaultModel } from '../../components/events_viewer/default_model'; -import { defaultCellActions } from '../../lib/cell_actions/default_cell_actions'; import { EntityType } from '@kbn/timelines-plugin/common'; import { getDefaultControlColumn } from '../../../timelines/components/timeline/body/control_columns'; import { SourcererScopeName } from '../sourcerer/model'; @@ -64,7 +63,6 @@ describe('epicLocalStorage', () => { const ACTION_BUTTON_COUNT = 4; testProps = { - defaultCellActions, defaultModel: eventsDefaultModel, end: to, entityType: EntityType.ALERTS, 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 b6de0e4f80c7b..c191dcf20e98e 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 @@ -39,7 +39,6 @@ import { StatefulEventsViewer } from '../../../common/components/events_viewer'; import { useSourcererDataView } from '../../../common/containers/sourcerer'; import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; import { useInvalidFilterQuery } from '../../../common/hooks/use_invalid_filter_query'; -import { defaultCellActions } from '../../../common/lib/cell_actions/default_cell_actions'; import { useKibana } from '../../../common/lib/kibana'; import type { inputsModel, State } from '../../../common/store'; import { inputsSelectors } from '../../../common/store'; @@ -416,7 +415,6 @@ export const AlertsTableComponent: React.FC = ({ = ({ = (props) => RenderCellValue({ ...props, enableActions: false, asPlainText: true }); +> = (props) => RenderCellValue({ ...props, asPlainText: true }); diff --git a/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/render_cell_value.tsx b/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/render_cell_value.tsx index 21d0d3a199f55..476e15fce02a3 100644 --- a/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/render_cell_value.tsx +++ b/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/render_cell_value.tsx @@ -91,7 +91,6 @@ export const useRenderCellValue = ({ data, ecsData, eventId, - globalFilters, header, isDetails = false, isDraggable = false, @@ -120,7 +119,6 @@ export const useRenderCellValue = ({ data={data} ecsData={ecsData} eventId={eventId} - globalFilters={globalFilters} header={myHeader} isDetails={isDetails} isDraggable={isDraggable} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.test.tsx index 34f57d567951c..3487e2770ff45 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.test.tsx @@ -127,42 +127,6 @@ describe('DefaultCellRenderer', () => { values: ['2018-11-05T19:03:25.937Z'], }); }); - - test('if in tgrid expanded value, it renders ExpandedCellValueActions', () => { - const data = cloneDeep(mockTimelineData[0].data); - const header = cloneDeep(defaultHeaders[1]); - const isDetails = true; - const id = 'event.severity'; - const wrapper = mount( - - - - - - - - ); - - expect( - wrapper.find('[data-test-subj="data-grid-expanded-cell-value-actions"]').exists() - ).toBeTruthy(); - }); }); describe('host link rendering', () => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.tsx index 1fafa85162ea1..8056a07fb39de 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.tsx @@ -13,12 +13,6 @@ import { columnRenderers } from '../body/renderers'; import { getColumnRenderer } from '../body/renderers/get_column_renderer'; import type { CellValueElementProps } from '.'; import { getLinkColumnDefinition } from '../../../../common/lib/cell_actions/helpers'; -import { FIELDS_WITHOUT_CELL_ACTIONS } from '../../../../common/lib/cell_actions/constants'; -import { ExpandedCellValueActions } from '../../../../common/lib/cell_actions/expanded_cell_value_actions'; - -const hasCellActions = (columnId?: string) => { - return columnId && !FIELDS_WITHOUT_CELL_ACTIONS.includes(columnId); -}; const StyledContent = styled.div<{ $isDetails: boolean }>` padding: ${({ $isDetails }) => ($isDetails ? '0 8px' : undefined)}; @@ -28,7 +22,6 @@ export const DefaultCellRenderer: React.FC = ({ data, ecsData, eventId, - globalFilters, header, isDetails, isDraggable, @@ -37,7 +30,6 @@ export const DefaultCellRenderer: React.FC = ({ rowRenderers, scopeId, truncate, - enableActions = true, asPlainText, }) => { const asPlainTextDefault = useMemo(() => { @@ -54,31 +46,21 @@ export const DefaultCellRenderer: React.FC = ({ ? 'eui-textBreakWord' : 'eui-displayInlineBlock eui-textTruncate'; return ( - <> - - {getColumnRenderer(header.id, columnRenderers, data).renderColumn({ - asPlainText: asPlainText ?? asPlainTextDefault, // we want to render value with links as plain text but keep other formatters like badge. Except rule name for non preview tables - columnName: header.id, - ecsData, - eventId, - field: header, - isDetails, - isDraggable, - linkValues, - rowRenderers, - scopeId, - truncate, - values, - })} - - {enableActions && isDetails && hasCellActions(header.id) && ( - - )} - + + {getColumnRenderer(header.id, columnRenderers, data).renderColumn({ + asPlainText: asPlainText ?? asPlainTextDefault, // we want to render value with links as plain text but keep other formatters like badge. Except rule name for non preview tables + columnName: header.id, + ecsData, + eventId, + field: header, + isDetails, + isDraggable, + linkValues, + rowRenderers, + scopeId, + truncate, + values, + })} + ); }; diff --git a/x-pack/test/security_solution_ftr/page_objects/detections/index.ts b/x-pack/test/security_solution_ftr/page_objects/detections/index.ts index 1b2b6628afe69..e633bd9b62dda 100644 --- a/x-pack/test/security_solution_ftr/page_objects/detections/index.ts +++ b/x-pack/test/security_solution_ftr/page_objects/detections/index.ts @@ -172,7 +172,7 @@ export class DetectionsPageObject extends FtrService { for (const eventRow of allEvents) { const hostNameButton = await this.testSubjects.findDescendant( - 'formatted-field-host.name', + 'host-details-button', eventRow ); const eventRowHostName = (await hostNameButton.getVisibleText()).trim(); From 910bd6e6e0faee30f0fdb0a5f343e87a53135820 Mon Sep 17 00:00:00 2001 From: "Joey F. Poon" Date: Mon, 13 Feb 2023 11:21:53 -0600 Subject: [PATCH 15/20] [fleet] add message signing service (#149785) --- .../group2/check_registered_types.test.ts | 1 + .../group3/type_registrations.test.ts | 3 +- .../plugins/fleet/common/constants/index.ts | 1 + .../common/constants/message_signing_keys.ts | 8 + x-pack/plugins/fleet/common/index.ts | 1 + .../plugins/fleet/server/constants/index.ts | 1 + x-pack/plugins/fleet/server/index.ts | 1 + x-pack/plugins/fleet/server/mocks/index.ts | 6 + x-pack/plugins/fleet/server/plugin.ts | 17 +- .../fleet/server/saved_objects/index.ts | 28 ++- .../fleet/server/services/app_context.ts | 7 + x-pack/plugins/fleet/server/services/index.ts | 2 + .../fleet/server/services/security/index.ts | 2 + .../security/message_signing_service.test.ts | 102 ++++++++++ .../security/message_signing_service.ts | 177 ++++++++++++++++++ x-pack/plugins/fleet/server/services/setup.ts | 7 + 16 files changed, 353 insertions(+), 11 deletions(-) create mode 100644 x-pack/plugins/fleet/common/constants/message_signing_keys.ts create mode 100644 x-pack/plugins/fleet/server/services/security/message_signing_service.test.ts create mode 100644 x-pack/plugins/fleet/server/services/security/message_signing_service.ts diff --git a/src/core/server/integration_tests/saved_objects/migrations/group2/check_registered_types.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group2/check_registered_types.test.ts index 5558e5156324d..be38a8225d0f4 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/group2/check_registered_types.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/group2/check_registered_types.test.ts @@ -93,6 +93,7 @@ describe('checking migration metadata changes on all registered SO types', () => "file-upload-usage-collection-telemetry": "c6fcb9a7efcf19b2bb66ca6e005bfee8961f6073", "fileShare": "f07d346acbb724eacf139a0fb781c38dc5280115", "fleet-fleet-server-host": "67180a54a689111fb46403c3603c9b3a329c698d", + "fleet-message-signing-keys": "0c6da6a680807e568540b2aa263ae52331ba66db", "fleet-preconfiguration-deletion-record": "3afad160748b430427086985a3445fd8697566d5", "fleet-proxy": "94d0a902a0fd22578d7d3a20873b95d902e25245", "graph-workspace": "565642a208fe7413b487aea979b5b153e4e74abe", diff --git a/src/core/server/integration_tests/saved_objects/migrations/group3/type_registrations.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group3/type_registrations.test.ts index 2c8dbabf878a1..7f4ca3fb70dc2 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/group3/type_registrations.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/group3/type_registrations.test.ts @@ -58,8 +58,9 @@ const previouslyRegisteredTypes = [ 'fleet-agent-events', 'fleet-agents', 'fleet-enrollment-api-keys', - 'fleet-preconfiguration-deletion-record', 'fleet-fleet-server-host', + 'fleet-message-signing-keys', + 'fleet-preconfiguration-deletion-record', 'fleet-proxy', 'graph-workspace', 'guided-setup-state', diff --git a/x-pack/plugins/fleet/common/constants/index.ts b/x-pack/plugins/fleet/common/constants/index.ts index f42b2a372ebb6..4dcc2d58d65ba 100644 --- a/x-pack/plugins/fleet/common/constants/index.ts +++ b/x-pack/plugins/fleet/common/constants/index.ts @@ -19,6 +19,7 @@ export * from './download_source'; export * from './fleet_server_policy_config'; export * from './authz'; export * from './file_storage'; +export * from './message_signing_keys'; // TODO: This is the default `index.max_result_window` ES setting, which dictates // the maximum amount of results allowed to be returned from a search. It's possible diff --git a/x-pack/plugins/fleet/common/constants/message_signing_keys.ts b/x-pack/plugins/fleet/common/constants/message_signing_keys.ts new file mode 100644 index 0000000000000..a51cff3e376c9 --- /dev/null +++ b/x-pack/plugins/fleet/common/constants/message_signing_keys.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const MESSAGE_SIGNING_KEYS_SAVED_OBJECT_TYPE = 'fleet-message-signing-keys'; diff --git a/x-pack/plugins/fleet/common/index.ts b/x-pack/plugins/fleet/common/index.ts index 3dc816f9f3090..3cdfa354a8c5f 100644 --- a/x-pack/plugins/fleet/common/index.ts +++ b/x-pack/plugins/fleet/common/index.ts @@ -26,6 +26,7 @@ export { OUTPUT_SAVED_OBJECT_TYPE, PRECONFIGURATION_DELETION_RECORD_SAVED_OBJECT_TYPE, ASSETS_SAVED_OBJECT_TYPE, + MESSAGE_SIGNING_KEYS_SAVED_OBJECT_TYPE, // Fleet server index FLEET_SERVER_SERVERS_INDEX, FLEET_SERVER_ARTIFACTS_INDEX, diff --git a/x-pack/plugins/fleet/server/constants/index.ts b/x-pack/plugins/fleet/server/constants/index.ts index 93dc93051e98e..ff7c70d8f64e8 100644 --- a/x-pack/plugins/fleet/server/constants/index.ts +++ b/x-pack/plugins/fleet/server/constants/index.ts @@ -44,6 +44,7 @@ export { PACKAGES_SAVED_OBJECT_TYPE, ASSETS_SAVED_OBJECT_TYPE, GLOBAL_SETTINGS_SAVED_OBJECT_TYPE, + MESSAGE_SIGNING_KEYS_SAVED_OBJECT_TYPE, // Defaults DEFAULT_OUTPUT, DEFAULT_OUTPUT_ID, diff --git a/x-pack/plugins/fleet/server/index.ts b/x-pack/plugins/fleet/server/index.ts index cfc063cccbeca..b2540552b62be 100644 --- a/x-pack/plugins/fleet/server/index.ts +++ b/x-pack/plugins/fleet/server/index.ts @@ -20,6 +20,7 @@ export type { ArtifactsClientInterface, Artifact, ListArtifactsProps, + MessageSigningServiceInterface, } from './services'; export { getRegistryUrl } from './services'; diff --git a/x-pack/plugins/fleet/server/mocks/index.ts b/x-pack/plugins/fleet/server/mocks/index.ts index d6620fe4c3fb9..75f55a1b353d4 100644 --- a/x-pack/plugins/fleet/server/mocks/index.ts +++ b/x-pack/plugins/fleet/server/mocks/index.ts @@ -58,6 +58,7 @@ export const createAppContextStartContractMock = ( elasticsearch: elasticsearchServiceMock.createStart(), data: dataPluginMock.createStartContract(), encryptedSavedObjectsStart: encryptedSavedObjectsMock.createStart(), + encryptedSavedObjectsSetup: encryptedSavedObjectsMock.createSetup({ canEncrypt: true }), savedObjects: savedObjectsServiceMock.createStartContract(), securitySetup: securityMock.createSetup(), securityStart: securityMock.createStart(), @@ -74,6 +75,11 @@ export const createAppContextStartContractMock = ( kibanaBranch: 'main', telemetryEventsSender: createMockTelemetryEventsSender(), bulkActionsResolver: {} as any, + messageSigningService: { + generateKeyPair: jest.fn(), + sign: jest.fn(), + getPublicKey: jest.fn(), + }, }; }; diff --git a/x-pack/plugins/fleet/server/plugin.ts b/x-pack/plugins/fleet/server/plugin.ts index 714ea06d0eefe..265431a87e265 100644 --- a/x-pack/plugins/fleet/server/plugin.ts +++ b/x-pack/plugins/fleet/server/plugin.ts @@ -54,14 +54,16 @@ import type { FleetConfigType } from '../common/types'; import type { FleetAuthz } from '../common'; import type { ExperimentalFeatures } from '../common/experimental_features'; -import { INTEGRATIONS_PLUGIN_ID } from '../common'; +import { MESSAGE_SIGNING_KEYS_SAVED_OBJECT_TYPE, INTEGRATIONS_PLUGIN_ID } from '../common'; import { parseExperimentalConfigValue } from '../common/experimental_features'; +import type { MessageSigningServiceInterface } from './services/security'; import { getRouteRequiredAuthz, makeRouterWithFleetAuthz, calculateRouteAuthz, getAuthzFromRequest, + MessageSigningService, } from './services/security'; import { @@ -151,6 +153,7 @@ export interface FleetAppContext { httpSetup?: HttpServiceSetup; telemetryEventsSender: TelemetryEventsSender; bulkActionsResolver: BulkActionsResolver; + messageSigningService: MessageSigningServiceInterface; } export type FleetSetupContract = void; @@ -198,6 +201,8 @@ export interface FleetStartContract { * @param packageName */ createArtifactsClient: (packageName: string) => FleetArtifactsClient; + + messageSigningService: MessageSigningServiceInterface; } export class FleetPlugin @@ -248,7 +253,7 @@ export class FleetPlugin core.status.set(this.fleetStatus$.asObservable()); - registerSavedObjects(core.savedObjects, deps.encryptedSavedObjects); + registerSavedObjects(core.savedObjects); registerEncryptedSavedObjects(deps.encryptedSavedObjects); // Register feature @@ -422,6 +427,12 @@ export class FleetPlugin } public start(core: CoreStart, plugins: FleetStartDeps): FleetStartContract { + const messageSigningService = new MessageSigningService( + plugins.encryptedSavedObjects.getClient({ + includedHiddenTypes: [MESSAGE_SIGNING_KEYS_SAVED_OBJECT_TYPE], + }) + ); + appContextService.start({ elasticsearch: core.elasticsearch, data: plugins.data, @@ -444,6 +455,7 @@ export class FleetPlugin logger: this.logger, telemetryEventsSender: this.telemetryEventsSender, bulkActionsResolver: this.bulkActionsResolver!, + messageSigningService, }); licenseService.start(plugins.licensing.license$); @@ -530,6 +542,7 @@ export class FleetPlugin createArtifactsClient(packageName: string) { return new FleetArtifactsClient(core.elasticsearch.client.asInternalUser, packageName); }, + messageSigningService, }; } diff --git a/x-pack/plugins/fleet/server/saved_objects/index.ts b/x-pack/plugins/fleet/server/saved_objects/index.ts index 1a8178890b1dc..16480b0031628 100644 --- a/x-pack/plugins/fleet/server/saved_objects/index.ts +++ b/x-pack/plugins/fleet/server/saved_objects/index.ts @@ -20,6 +20,7 @@ import { DOWNLOAD_SOURCE_SAVED_OBJECT_TYPE, FLEET_SERVER_HOST_SAVED_OBJECT_TYPE, FLEET_PROXY_SAVED_OBJECT_TYPE, + MESSAGE_SIGNING_KEYS_SAVED_OBJECT_TYPE, } from '../constants'; import { @@ -61,9 +62,7 @@ import { migratePackagePolicyToV870 } from './migrations/security_solution'; * Please update typings in `/common/types` as well as * schemas in `/server/types` if mappings are updated. */ -const getSavedObjectTypes = ( - encryptedSavedObjects: EncryptedSavedObjectsPluginSetup -): { [key: string]: SavedObjectsType } => ({ +const getSavedObjectTypes = (): { [key: string]: SavedObjectsType } => ({ // Deprecated [GLOBAL_SETTINGS_SAVED_OBJECT_TYPE]: { name: GLOBAL_SETTINGS_SAVED_OBJECT_TYPE, @@ -366,13 +365,22 @@ const getSavedObjectTypes = ( }, }, }, + [MESSAGE_SIGNING_KEYS_SAVED_OBJECT_TYPE]: { + name: MESSAGE_SIGNING_KEYS_SAVED_OBJECT_TYPE, + hidden: true, + namespaceType: 'agnostic', + management: { + importableAndExportable: false, + }, + mappings: { + dynamic: false, + properties: {}, + }, + }, }); -export function registerSavedObjects( - savedObjects: SavedObjectsServiceSetup, - encryptedSavedObjects: EncryptedSavedObjectsPluginSetup -) { - const savedObjectTypes = getSavedObjectTypes(encryptedSavedObjects); +export function registerSavedObjects(savedObjects: SavedObjectsServiceSetup) { + const savedObjectTypes = getSavedObjectTypes(); Object.values(savedObjectTypes).forEach((type) => { savedObjects.registerType(type); }); @@ -400,4 +408,8 @@ export function registerEncryptedSavedObjects( ]), }); // Encrypted saved objects + encryptedSavedObjects.registerType({ + type: MESSAGE_SIGNING_KEYS_SAVED_OBJECT_TYPE, + attributesToEncrypt: new Set(['private_key', 'passphrase']), + }); } diff --git a/x-pack/plugins/fleet/server/services/app_context.ts b/x-pack/plugins/fleet/server/services/app_context.ts index c8c6c8b4c0e59..71c09621c59a0 100644 --- a/x-pack/plugins/fleet/server/services/app_context.ts +++ b/x-pack/plugins/fleet/server/services/app_context.ts @@ -43,6 +43,7 @@ import type { } from '../types'; import type { FleetAppContext } from '../plugin'; import type { TelemetryEventsSender } from '../telemetry/sender'; +import type { MessageSigningServiceInterface } from '..'; import type { BulkActionsResolver } from './agents'; @@ -67,6 +68,7 @@ class AppContextService { private telemetryEventsSender: TelemetryEventsSender | undefined; private savedObjectsTagging: SavedObjectTaggingStart | undefined; private bulkActionsResolver: BulkActionsResolver | undefined; + private messageSigningService: MessageSigningServiceInterface | undefined; public start(appContext: FleetAppContext) { this.data = appContext.data; @@ -86,6 +88,7 @@ class AppContextService { this.telemetryEventsSender = appContext.telemetryEventsSender; this.savedObjectsTagging = appContext.savedObjectsTagging; this.bulkActionsResolver = appContext.bulkActionsResolver; + this.messageSigningService = appContext.messageSigningService; if (appContext.config$) { this.config$ = appContext.config$; @@ -243,6 +246,10 @@ class AppContextService { public getBulkActionsResolver() { return this.bulkActionsResolver; } + + public getMessageSigningService() { + return this.messageSigningService; + } } export const appContextService = new AppContextService(); diff --git a/x-pack/plugins/fleet/server/services/index.ts b/x-pack/plugins/fleet/server/services/index.ts index cfd8eb95ceb4d..c2abd872d5df9 100644 --- a/x-pack/plugins/fleet/server/services/index.ts +++ b/x-pack/plugins/fleet/server/services/index.ts @@ -67,3 +67,5 @@ export { migrateSettingsToFleetServerHost } from './fleet_server_host'; export { FleetUsageSender } from './telemetry/fleet_usage_sender'; export { checkAllowedPackages } from './check_allowed_packages'; + +export type { MessageSigningServiceInterface } from './security'; diff --git a/x-pack/plugins/fleet/server/services/security/index.ts b/x-pack/plugins/fleet/server/services/security/index.ts index c41c769c58d8d..6b38c6f6c7d5b 100644 --- a/x-pack/plugins/fleet/server/services/security/index.ts +++ b/x-pack/plugins/fleet/server/services/security/index.ts @@ -15,3 +15,5 @@ export { getAuthzFromRequest, doesNotHaveRequiredFleetAuthz, } from './security'; +export type { MessageSigningServiceInterface } from './message_signing_service'; +export { MessageSigningService } from './message_signing_service'; diff --git a/x-pack/plugins/fleet/server/services/security/message_signing_service.test.ts b/x-pack/plugins/fleet/server/services/security/message_signing_service.test.ts new file mode 100644 index 0000000000000..7416e2af49ec5 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/security/message_signing_service.test.ts @@ -0,0 +1,102 @@ +/* + * 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 { createVerify } from 'crypto'; + +import type { KibanaRequest } from '@kbn/core-http-server'; +import type { SavedObjectsClientContract } from '@kbn/core/server'; +import type { EncryptedSavedObjectsClient } from '@kbn/encrypted-saved-objects-plugin/server'; + +import { MESSAGE_SIGNING_KEYS_SAVED_OBJECT_TYPE } from '../../constants'; +import { createAppContextStartContractMock } from '../../mocks'; +import { appContextService } from '../app_context'; + +import { + type MessageSigningServiceInterface, + MessageSigningService, +} from './message_signing_service'; + +describe('MessageSigningService', () => { + let soClientMock: jest.Mocked; + let esoClientMock: jest.Mocked; + let messageSigningService: MessageSigningServiceInterface; + const keyPairObj = { + id: 'id1', + type: MESSAGE_SIGNING_KEYS_SAVED_OBJECT_TYPE, + attributes: { + private_key: + 'MIHsMFcGCSqGSIb3DQEFDTBKMCkGCSqGSIb3DQEFDDAcBAgtNcDFoj07+QICCAAwDAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEELajFPDz2bpD2qfPCRHphAgEgZCq0eUxTOEGrefdeNgHR2VVxXjWRZG+cGn+e8LW4auBCwwMiZsAZPKKvzLdlLi5sQhH+qWPM7Z9/OLbF/0ZKvyDM2/+4/9+5Iwna7vueTZtcdSIuGIFRjqUZbgNLejPSPcBMM9SP1V6I8TjDguGAQ3Nj95t7g7cbl0x48nQZ9bNDJyvy4ytHl+ubzdanLlFkLc=', + public_key: + 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE6E5aKP8dAa+TlBuSKrrgl9UtkzHjn6YUQO+72vi3khGfUQIpD9qq9MsjsWz6Bvm6tnSOyyPXv+Koh80lNCKw5A==', + passphrase: 'eb35af2291344a51c9a8bb81e653281c38892d564db617a2cb0bc660f0ae96f2', + }, + }; + + function mockCreatePointInTimeFinderAsInternalUser(savedObjects: unknown[] = []) { + esoClientMock.createPointInTimeFinderDecryptedAsInternalUser = jest.fn().mockResolvedValue({ + close: jest.fn(), + find: function* asyncGenerator() { + yield { saved_objects: savedObjects }; + }, + }); + } + + beforeEach(() => { + const mockContext = createAppContextStartContractMock(); + appContextService.start(mockContext); + esoClientMock = + mockContext.encryptedSavedObjectsStart!.getClient() as jest.Mocked; + soClientMock = appContextService + .getSavedObjects() + .getScopedClient({} as unknown as KibanaRequest) as jest.Mocked; + + messageSigningService = new MessageSigningService(esoClientMock); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it('can correctly generate key pair if none exist', async () => { + mockCreatePointInTimeFinderAsInternalUser(); + + await messageSigningService.generateKeyPair(); + expect(soClientMock.create).toBeCalledWith(MESSAGE_SIGNING_KEYS_SAVED_OBJECT_TYPE, { + private_key: expect.any(String), + public_key: expect.any(String), + passphrase: expect.any(String), + }); + }); + + it('does not generate key pair if one exists', async () => { + mockCreatePointInTimeFinderAsInternalUser([keyPairObj]); + + await messageSigningService.generateKeyPair(); + expect(soClientMock.create).not.toBeCalled(); + }); + + it('can correctly sign messages', async () => { + mockCreatePointInTimeFinderAsInternalUser([keyPairObj]); + + const message = Buffer.from(JSON.stringify({ message: 'foobar' }), 'utf8'); + const { data, signature } = await messageSigningService.sign(message); + + const verifier = createVerify('SHA256'); + verifier.update(data); + verifier.end(); + + const serializedPublicKey = await messageSigningService.getPublicKey(); + const publicKey = Buffer.from(serializedPublicKey, 'base64'); + const isVerified = verifier.verify( + { key: publicKey, format: 'der', type: 'spki' }, + signature, + 'base64' + ); + expect(isVerified).toBe(true); + expect(data).toBe(message); + }); +}); diff --git a/x-pack/plugins/fleet/server/services/security/message_signing_service.ts b/x-pack/plugins/fleet/server/services/security/message_signing_service.ts new file mode 100644 index 0000000000000..6e323a55fc6f8 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/security/message_signing_service.ts @@ -0,0 +1,177 @@ +/* + * 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 { generateKeyPairSync, createSign, randomBytes } from 'crypto'; + +import type { KibanaRequest } from '@kbn/core-http-server'; +import type { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server'; +import type { EncryptedSavedObjectsClient } from '@kbn/encrypted-saved-objects-plugin/server'; +import { SECURITY_EXTENSION_ID } from '@kbn/core-saved-objects-server'; + +import { MESSAGE_SIGNING_KEYS_SAVED_OBJECT_TYPE } from '../../constants'; +import { appContextService } from '../app_context'; + +interface MessageSigningKeys { + private_key: string; + public_key: string; + passphrase: string; +} + +export interface MessageSigningServiceInterface { + generateKeyPair(providedPassphrase?: string): Promise; + sign(serializedMessage: Buffer | object): Promise<{ data: Buffer; signature: string }>; + getPublicKey(): Promise; +} + +export class MessageSigningService implements MessageSigningServiceInterface { + private _soClient: SavedObjectsClientContract | undefined; + + constructor(private esoClient: EncryptedSavedObjectsClient) {} + + public async generateKeyPair(providedPassphrase?: string) { + this.checkForEncryptionKey(); + + const currentKeyPair = await this.getCurrentKeyPair(); + if (currentKeyPair.privateKey && currentKeyPair.publicKey && currentKeyPair.passphrase) { + return; + } + + const passphrase = providedPassphrase || this.generatePassphrase(); + + const keyPair = generateKeyPairSync('ec', { + namedCurve: 'prime256v1', + privateKeyEncoding: { + type: 'pkcs8', + format: 'der', + cipher: 'aes-256-cbc', + passphrase, + }, + publicKeyEncoding: { + type: 'spki', + format: 'der', + }, + }); + + await this.soClient.create(MESSAGE_SIGNING_KEYS_SAVED_OBJECT_TYPE, { + private_key: keyPair.privateKey.toString('base64'), + public_key: keyPair.publicKey.toString('base64'), + passphrase, + }); + + return; + } + + public async sign( + message: Buffer | Record + ): Promise<{ data: Buffer; signature: string }> { + this.checkForEncryptionKey(); + + const msgBuffer = Buffer.isBuffer(message) + ? message + : Buffer.from(JSON.stringify(message), 'utf8'); + + const signer = createSign('SHA256'); + signer.update(msgBuffer); + signer.end(); + + const { privateKey: serializedPrivateKey, passphrase } = await this.getCurrentKeyPair(); + + if (!serializedPrivateKey) { + throw new Error('unable to find private key'); + } + if (!passphrase) { + throw new Error('unable to find passphrase'); + } + + const privateKey = Buffer.from(serializedPrivateKey, 'base64'); + const signature = signer.sign( + { key: privateKey, passphrase, format: 'der', type: 'pkcs8' }, + 'base64' + ); + return { + data: msgBuffer, + signature, + }; + } + + public async getPublicKey(): Promise { + this.checkForEncryptionKey(); + + const { publicKey } = await this.getCurrentKeyPair(); + + if (!publicKey) { + throw new Error('unable to find public key'); + } + + return publicKey; + } + + private get soClient() { + if (this._soClient) { + return this._soClient; + } + + const fakeRequest = { + headers: {}, + getBasePath: () => '', + path: '/', + route: { settings: {} }, + url: { href: {} }, + raw: { req: { url: '/' } }, + } as unknown as KibanaRequest; + + this._soClient = appContextService.getSavedObjects().getScopedClient(fakeRequest, { + excludedExtensions: [SECURITY_EXTENSION_ID], + includedHiddenTypes: [MESSAGE_SIGNING_KEYS_SAVED_OBJECT_TYPE], + }); + + return this._soClient; + } + + private async getCurrentKeyPair(): Promise<{ + privateKey: string; + publicKey: string; + passphrase: string; + }> { + const finder = + await this.esoClient.createPointInTimeFinderDecryptedAsInternalUser({ + type: MESSAGE_SIGNING_KEYS_SAVED_OBJECT_TYPE, + perPage: 1, + sortField: 'created_at', + sortOrder: 'desc', + }); + let keyPair = { + privateKey: '', + publicKey: '', + passphrase: '', + }; + for await (const result of finder.find()) { + const attributes = result.saved_objects[0]?.attributes; + if (!attributes?.private_key) { + break; + } + keyPair = { + privateKey: attributes.private_key, + publicKey: attributes.public_key, + passphrase: attributes.passphrase, + }; + break; + } + + return keyPair; + } + + private generatePassphrase(): string { + return randomBytes(32).toString('hex'); + } + + private checkForEncryptionKey(): void { + if (!appContextService.getEncryptedSavedObjectsSetup()?.canEncrypt) { + throw new Error('encryption key not set, message signing service is disabled'); + } + } +} diff --git a/x-pack/plugins/fleet/server/services/setup.ts b/x-pack/plugins/fleet/server/services/setup.ts index 2802fd34bc001..7acf46408367b 100644 --- a/x-pack/plugins/fleet/server/services/setup.ts +++ b/x-pack/plugins/fleet/server/services/setup.ts @@ -171,6 +171,13 @@ async function createSetupSideEffects( logger.debug('Setting up Fleet enrollment keys'); await ensureDefaultEnrollmentAPIKeysExists(soClient, esClient); + if (appContextService.getEncryptedSavedObjectsSetup()?.canEncrypt) { + logger.debug('Generating key pair for message signing'); + await appContextService.getMessageSigningService()?.generateKeyPair(); + } else { + logger.info('No encryption key set, skipping key pair generation for message signing'); + } + if (nonFatalErrors.length > 0) { logger.info('Encountered non fatal errors during Fleet setup'); formatNonFatalErrors(nonFatalErrors).forEach((error) => logger.info(JSON.stringify(error))); From 1526749ba4793f25549326ca1ff6924cbf2bf885 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20R=C3=BChsen?= Date: Mon, 13 Feb 2023 19:15:40 +0100 Subject: [PATCH 16/20] [Profiling] Fix TopN diff functions title text (#151031) Fixes the title texts in the differential TopN functions view (turning 'flamegraph' into 'functions'). Fixes https://github.com/elastic/prodfiler/issues/3000 --- .../primary_and_comparison_search_bar.tsx | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/profiling/public/components/primary_and_comparison_search_bar.tsx b/x-pack/plugins/profiling/public/components/primary_and_comparison_search_bar.tsx index 33f5f3e8850ff..5298d43deacf3 100644 --- a/x-pack/plugins/profiling/public/components/primary_and_comparison_search_bar.tsx +++ b/x-pack/plugins/profiling/public/components/primary_and_comparison_search_bar.tsx @@ -61,15 +61,29 @@ export function PrimaryAndComparisonSearchBar() { } } + let baselineTitle: string; + let comparisonTitle: string; + + if (routePath === '/flamegraphs/differential') { + baselineTitle = i18n.translate('xpack.profiling.comparisonSearch.baselineTitleFlamegraph', { + defaultMessage: 'Baseline flamegraph', + }); + comparisonTitle = i18n.translate('xpack.profiling.comparisonSearch.comparisonTitleFlamegraph', { + defaultMessage: 'Comparison flamegraph', + }); + } else { + baselineTitle = i18n.translate('xpack.profiling.comparisonSearch.baselineTitleFunctions', { + defaultMessage: 'Baseline functions', + }); + comparisonTitle = i18n.translate('xpack.profiling.comparisonSearch.comparisonTitleFunctions', { + defaultMessage: 'Comparison functions', + }); + } return ( -

- {i18n.translate('xpack.profiling.comparisonSearch.baselineTitle', { - defaultMessage: 'Baseline flamegraph', - })} -

+

{baselineTitle}

@@ -107,11 +121,7 @@ export function PrimaryAndComparisonSearchBar() {
-

- {i18n.translate('xpack.profiling.comparisonSearch.comparisonTitle', { - defaultMessage: 'Comparison flamegraph', - })} -

+

{comparisonTitle}

Date: Mon, 13 Feb 2023 18:53:08 +0000 Subject: [PATCH 17/20] [Logs] Fix legacy logFilter handling via the URL (#150902) Fix legacy logFilter handling via the URL --- .../src/url_state_storage_service.ts | 17 +++- x-pack/test/functional/apps/infra/index.ts | 1 + .../test/functional/apps/infra/log_stream.ts | 88 +++++++++++++++++++ 3 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 x-pack/test/functional/apps/infra/log_stream.ts diff --git a/x-pack/plugins/infra/public/observability_logs/log_stream_query_state/src/url_state_storage_service.ts b/x-pack/plugins/infra/public/observability_logs/log_stream_query_state/src/url_state_storage_service.ts index c18ee71ed4f8e..fabd26cc03d22 100644 --- a/x-pack/plugins/infra/public/observability_logs/log_stream_query_state/src/url_state_storage_service.ts +++ b/x-pack/plugins/infra/public/observability_logs/log_stream_query_state/src/url_state_storage_service.ts @@ -165,11 +165,24 @@ const legacyFilterStateInUrlRT = rt.union([ }), ]); +const legacyLegacyFilterStateWithExpressionInUrlRT = rt.type({ + kind: rt.literal('kuery'), + expression: rt.string, +}); + const decodeQueryValueFromUrl = (queryValueFromUrl: unknown) => Either.getAltValidation(Array.getMonoid()).alt( pipe( - legacyFilterStateInUrlRT.decode(queryValueFromUrl), - Either.map((legacyQuery) => ({ query: legacyQuery })) + pipe( + legacyLegacyFilterStateWithExpressionInUrlRT.decode(queryValueFromUrl), + Either.map(({ expression, kind }) => ({ query: { language: kind, query: expression } })) + ), + Either.alt(() => + pipe( + legacyFilterStateInUrlRT.decode(queryValueFromUrl), + Either.map((legacyQuery) => ({ query: legacyQuery })) + ) + ) ), () => filterStateInUrlRT.decode(queryValueFromUrl) ); diff --git a/x-pack/test/functional/apps/infra/index.ts b/x-pack/test/functional/apps/infra/index.ts index 9e6cb3f74cc96..197481c431394 100644 --- a/x-pack/test/functional/apps/infra/index.ts +++ b/x-pack/test/functional/apps/infra/index.ts @@ -25,6 +25,7 @@ export default ({ loadTestFile }: FtrProviderContext) => { loadTestFile(require.resolve('./log_entry_rate_tab')); loadTestFile(require.resolve('./logs_source_configuration')); loadTestFile(require.resolve('./link_to')); + loadTestFile(require.resolve('./log_stream')); }); }); }; diff --git a/x-pack/test/functional/apps/infra/log_stream.ts b/x-pack/test/functional/apps/infra/log_stream.ts new file mode 100644 index 0000000000000..a0836e4ef57e9 --- /dev/null +++ b/x-pack/test/functional/apps/infra/log_stream.ts @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { URL } from 'url'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +const SERVICE_ID = '49a18510598271e924253ed2581d7ada'; + +export default ({ getPageObjects, getService }: FtrProviderContext) => { + const pageObjects = getPageObjects(['common']); + const retry = getService('retry'); + const browser = getService('browser'); + const esArchiver = getService('esArchiver'); + + describe('Log stream', function () { + describe('Legacy URL handling', async () => { + describe('Correctly handles legacy versions of logFilter', async () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/infra/8.0.0/logs_and_metrics'); + }); + after(async () => { + await esArchiver.unload( + 'x-pack/test/functional/es_archives/infra/8.0.0/logs_and_metrics' + ); + }); + it('Expression and kind', async () => { + const location = { + hash: '', + pathname: '/stream', + search: `logFilter=(expression:'service.id:"${SERVICE_ID}"',kind:kuery)`, + state: undefined, + }; + + await pageObjects.common.navigateToUrlWithBrowserHistory( + 'infraLogs', + location.pathname, + location.search, + { + ensureCurrentUrl: false, + } + ); + + await retry.tryForTime(5000, async () => { + const currentUrl = await browser.getCurrentUrl(); + const parsedUrl = new URL(currentUrl); + + expect(parsedUrl.pathname).to.be('/app/logs/stream'); + expect(parsedUrl.searchParams.get('logFilter')).to.be( + `(filters:!(),query:(language:kuery,query:\'service.id:"${SERVICE_ID}"\'))` + ); + }); + }); + it('Top-level query and language', async () => { + const location = { + hash: '', + pathname: '/stream', + search: `logFilter=(query:'service.id:"${SERVICE_ID}"',language:kuery)`, + state: undefined, + }; + + await pageObjects.common.navigateToUrlWithBrowserHistory( + 'infraLogs', + location.pathname, + location.search, + { + ensureCurrentUrl: false, + } + ); + + await retry.tryForTime(5000, async () => { + const currentUrl = await browser.getCurrentUrl(); + const parsedUrl = new URL(currentUrl); + + expect(parsedUrl.pathname).to.be('/app/logs/stream'); + expect(parsedUrl.searchParams.get('logFilter')).to.be( + `(filters:!(),query:(language:kuery,query:\'service.id:"${SERVICE_ID}"\'))` + ); + }); + }); + }); + }); + }); +}; From eb4b92d876376d280c41dc352915e5e6e975cc07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Mon, 13 Feb 2023 14:30:42 -0500 Subject: [PATCH 18/20] [APM] adding API test for alert error count threshold (#150764) Add new api test for error count threshold `tests/alerts/error_count_threshold.spec.ts` --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../src/lib/timerange.ts | 18 +- .../kbn-apm-synthtrace-client/tsconfig.json | 3 + .../common/rules/default_action_message.ts | 60 +++++++ x-pack/plugins/apm/common/rules/schema.ts | 71 ++++++++ .../rule_types/register_apm_rule_types.ts | 58 ++---- .../anomaly/register_anomaly_rule_type.ts | 21 +-- .../register_error_count_rule_type.test.ts | 6 +- .../register_error_count_rule_type.ts | 48 +++-- ...ter_transaction_duration_rule_type.test.ts | 2 +- ...register_transaction_duration_rule_type.ts | 74 ++++---- ...r_transaction_error_rate_rule_type.test.ts | 2 +- ...gister_transaction_error_rate_rule_type.ts | 56 +++--- .../tests/alerts/alerting_api_helper.ts | 117 +++++++++++++ .../tests/alerts/anomaly_alert.spec.ts | 52 +++--- .../alerts/error_count_threshold.spec.ts | 165 ++++++++++++++++++ .../tests/alerts/wait_for_rule_status.ts | 73 ++++---- .../service_group_count.spec.ts | 42 ++--- .../tests/services/service_alerts.spec.ts | 42 ++--- 18 files changed, 624 insertions(+), 286 deletions(-) create mode 100644 x-pack/plugins/apm/common/rules/default_action_message.ts create mode 100644 x-pack/plugins/apm/common/rules/schema.ts create mode 100644 x-pack/test/apm_api_integration/tests/alerts/alerting_api_helper.ts create mode 100644 x-pack/test/apm_api_integration/tests/alerts/error_count_threshold.spec.ts diff --git a/packages/kbn-apm-synthtrace-client/src/lib/timerange.ts b/packages/kbn-apm-synthtrace-client/src/lib/timerange.ts index 2dd0659f9cc19..0817ea3d0e34f 100644 --- a/packages/kbn-apm-synthtrace-client/src/lib/timerange.ts +++ b/packages/kbn-apm-synthtrace-client/src/lib/timerange.ts @@ -5,7 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - +import datemath from '@kbn/datemath'; import type { Moment } from 'moment'; import { Interval } from './interval'; @@ -23,12 +23,22 @@ export class Timerange { type DateLike = Date | number | Moment | string; -function getDateFrom(date: DateLike): Date { +function getDateFrom(date: DateLike, now: Date): Date { if (date instanceof Date) return date; + + if (typeof date === 'string') { + const parsed = datemath.parse(date, { forceNow: now }); + if (parsed && parsed.isValid()) { + return parsed.toDate(); + } + } + if (typeof date === 'number' || typeof date === 'string') return new Date(date); + return date.toDate(); } -export function timerange(from: Date | number | Moment, to: Date | number | Moment) { - return new Timerange(getDateFrom(from), getDateFrom(to)); +export function timerange(from: DateLike, to: DateLike) { + const now = new Date(); + return new Timerange(getDateFrom(from, now), getDateFrom(to, now)); } diff --git a/packages/kbn-apm-synthtrace-client/tsconfig.json b/packages/kbn-apm-synthtrace-client/tsconfig.json index 8d1c9cae899a3..8286fda7455b0 100644 --- a/packages/kbn-apm-synthtrace-client/tsconfig.json +++ b/packages/kbn-apm-synthtrace-client/tsconfig.json @@ -11,5 +11,8 @@ "include": ["**/*.ts"], "exclude": [ "target/**/*", + ], + "kbn_references": [ + "@kbn/datemath", ] } diff --git a/x-pack/plugins/apm/common/rules/default_action_message.ts b/x-pack/plugins/apm/common/rules/default_action_message.ts new file mode 100644 index 0000000000000..503bc1ca3cd26 --- /dev/null +++ b/x-pack/plugins/apm/common/rules/default_action_message.ts @@ -0,0 +1,60 @@ +/* + * 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 errorCountMessage = i18n.translate( + 'xpack.apm.alertTypes.errorCount.defaultActionMessage', + { + defaultMessage: `\\{\\{alertName\\}\\} alert is firing because of the following conditions: + +- Service name: \\{\\{context.serviceName\\}\\} +- Environment: \\{\\{context.environment\\}\\} +- Threshold: \\{\\{context.threshold\\}\\} +- Triggered value: \\{\\{context.triggerValue\\}\\} errors over the last \\{\\{context.interval\\}\\}`, + } +); + +export const transactionDurationMessage = i18n.translate( + 'xpack.apm.alertTypes.transactionDuration.defaultActionMessage', + { + defaultMessage: `\\{\\{alertName\\}\\} alert is firing because of the following conditions: + +- Service name: \\{\\{context.serviceName\\}\\} +- Type: \\{\\{context.transactionType\\}\\} +- Environment: \\{\\{context.environment\\}\\} +- Latency threshold: \\{\\{context.threshold\\}\\}ms +- Latency observed: \\{\\{context.triggerValue\\}\\} over the last \\{\\{context.interval\\}\\}`, + } +); + +export const transactionErrorRateMessage = i18n.translate( + 'xpack.apm.alertTypes.transactionErrorRate.defaultActionMessage', + { + defaultMessage: `\\{\\{alertName\\}\\} alert is firing because of the following conditions: + +- Service name: \\{\\{context.serviceName\\}\\} +- Type: \\{\\{context.transactionType\\}\\} +- Environment: \\{\\{context.environment\\}\\} +- Threshold: \\{\\{context.threshold\\}\\}% +- Triggered value: \\{\\{context.triggerValue\\}\\}% of errors over the last \\{\\{context.interval\\}\\}`, + } +); + +export const anomalyMessage = i18n.translate( + 'xpack.apm.alertTypes.transactionDurationAnomaly.defaultActionMessage', + { + defaultMessage: `\\{\\{alertName\\}\\} alert is firing because of the following conditions: + +- Service name: \\{\\{context.serviceName\\}\\} +- Type: \\{\\{context.transactionType\\}\\} +- Environment: \\{\\{context.environment\\}\\} +- Severity threshold: \\{\\{context.threshold\\}\\} +- Severity value: \\{\\{context.triggerValue\\}\\} +`, + } +); diff --git a/x-pack/plugins/apm/common/rules/schema.ts b/x-pack/plugins/apm/common/rules/schema.ts new file mode 100644 index 0000000000000..58a5b40da41f2 --- /dev/null +++ b/x-pack/plugins/apm/common/rules/schema.ts @@ -0,0 +1,71 @@ +/* + * 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 { schema, TypeOf } from '@kbn/config-schema'; +import { ANOMALY_SEVERITY } from '../ml_constants'; +import { AggregationType, ApmRuleType } from './apm_rule_types'; + +export const errorCountParamsSchema = schema.object({ + windowSize: schema.number(), + windowUnit: schema.string(), + threshold: schema.number(), + serviceName: schema.maybe(schema.string()), + environment: schema.string(), +}); + +export const transactionDurationParamsSchema = schema.object({ + serviceName: schema.maybe(schema.string()), + transactionType: schema.maybe(schema.string()), + windowSize: schema.number(), + windowUnit: schema.string(), + threshold: schema.number(), + aggregationType: schema.oneOf([ + schema.literal(AggregationType.Avg), + schema.literal(AggregationType.P95), + schema.literal(AggregationType.P99), + ]), + environment: schema.string(), +}); + +export const anomalyParamsSchema = schema.object({ + serviceName: schema.maybe(schema.string()), + transactionType: schema.maybe(schema.string()), + windowSize: schema.number(), + windowUnit: schema.string(), + environment: schema.string(), + anomalySeverityType: schema.oneOf([ + schema.literal(ANOMALY_SEVERITY.CRITICAL), + schema.literal(ANOMALY_SEVERITY.MAJOR), + schema.literal(ANOMALY_SEVERITY.MINOR), + schema.literal(ANOMALY_SEVERITY.WARNING), + ]), +}); + +export const transactionErrorRateParamsSchema = schema.object({ + windowSize: schema.number(), + windowUnit: schema.string(), + threshold: schema.number(), + transactionType: schema.maybe(schema.string()), + serviceName: schema.maybe(schema.string()), + environment: schema.string(), +}); + +type ErrorCountParamsType = TypeOf; +type TransactionDurationParamsType = TypeOf< + typeof transactionDurationParamsSchema +>; +type AnomalyParamsType = TypeOf; +type TransactionErrorRateParamsType = TypeOf< + typeof transactionErrorRateParamsSchema +>; + +export interface ApmRuleParamsType { + [ApmRuleType.TransactionDuration]: TransactionDurationParamsType; + [ApmRuleType.ErrorCount]: ErrorCountParamsType; + [ApmRuleType.Anomaly]: AnomalyParamsType; + [ApmRuleType.TransactionErrorRate]: TransactionErrorRateParamsType; +} diff --git a/x-pack/plugins/apm/public/components/alerting/rule_types/register_apm_rule_types.ts b/x-pack/plugins/apm/public/components/alerting/rule_types/register_apm_rule_types.ts index ed9971307bf64..f355ea4c2f6eb 100644 --- a/x-pack/plugins/apm/public/components/alerting/rule_types/register_apm_rule_types.ts +++ b/x-pack/plugins/apm/public/components/alerting/rule_types/register_apm_rule_types.ts @@ -14,6 +14,12 @@ import { getAlertUrlTransaction, } from '../../../../common/utils/formatters'; import { ApmRuleType } from '../../../../common/rules/apm_rule_types'; +import { + anomalyMessage, + errorCountMessage, + transactionDurationMessage, + transactionErrorRateMessage, +} from '../../../../common/rules/default_action_message'; // copied from elasticsearch_fieldnames.ts to limit page load bundle size const SERVICE_ENVIRONMENT = 'service.environment'; @@ -54,17 +60,7 @@ export function registerApmRuleTypes( ) ), requiresAppContext: false, - defaultActionMessage: i18n.translate( - 'xpack.apm.alertTypes.errorCount.defaultActionMessage', - { - defaultMessage: `\\{\\{alertName\\}\\} alert is firing because of the following conditions: - -- Service name: \\{\\{context.serviceName\\}\\} -- Environment: \\{\\{context.environment\\}\\} -- Threshold: \\{\\{context.threshold\\}\\} errors -- Triggered value: \\{\\{context.triggerValue\\}\\} errors over the last \\{\\{context.interval\\}\\}`, - } - ), + defaultActionMessage: errorCountMessage, }); observabilityRuleTypeRegistry.register({ @@ -104,18 +100,7 @@ export function registerApmRuleTypes( ) ), requiresAppContext: false, - defaultActionMessage: i18n.translate( - 'xpack.apm.alertTypes.transactionDuration.defaultActionMessage', - { - defaultMessage: `\\{\\{alertName\\}\\} alert is firing because of the following conditions: - -- Service name: \\{\\{context.serviceName\\}\\} -- Type: \\{\\{context.transactionType\\}\\} -- Environment: \\{\\{context.environment\\}\\} -- Latency threshold: \\{\\{context.threshold\\}\\}ms -- Latency observed: \\{\\{context.triggerValue\\}\\} over the last \\{\\{context.interval\\}\\}`, - } - ), + defaultActionMessage: transactionDurationMessage, }); observabilityRuleTypeRegistry.register({ @@ -153,18 +138,7 @@ export function registerApmRuleTypes( ) ), requiresAppContext: false, - defaultActionMessage: i18n.translate( - 'xpack.apm.alertTypes.transactionErrorRate.defaultActionMessage', - { - defaultMessage: `\\{\\{alertName\\}\\} alert is firing because of the following conditions: - -- Service name: \\{\\{context.serviceName\\}\\} -- Type: \\{\\{context.transactionType\\}\\} -- Environment: \\{\\{context.environment\\}\\} -- Threshold: \\{\\{context.threshold\\}\\}% -- Triggered value: \\{\\{context.triggerValue\\}\\}% of errors over the last \\{\\{context.interval\\}\\}`, - } - ), + defaultActionMessage: transactionErrorRateMessage, }); observabilityRuleTypeRegistry.register({ @@ -199,18 +173,6 @@ export function registerApmRuleTypes( ) ), requiresAppContext: false, - defaultActionMessage: i18n.translate( - 'xpack.apm.alertTypes.transactionDurationAnomaly.defaultActionMessage', - { - defaultMessage: `\\{\\{alertName\\}\\} alert is firing because of the following conditions: - -- Service name: \\{\\{context.serviceName\\}\\} -- Type: \\{\\{context.transactionType\\}\\} -- Environment: \\{\\{context.environment\\}\\} -- Severity threshold: \\{\\{context.threshold\\}\\} -- Severity value: \\{\\{context.triggerValue\\}\\} -`, - } - ), + defaultActionMessage: anomalyMessage, }); } diff --git a/x-pack/plugins/apm/server/routes/alerts/rule_types/anomaly/register_anomaly_rule_type.ts b/x-pack/plugins/apm/server/routes/alerts/rule_types/anomaly/register_anomaly_rule_type.ts index 529efb583b7dd..a10d803ef6d86 100644 --- a/x-pack/plugins/apm/server/routes/alerts/rule_types/anomaly/register_anomaly_rule_type.ts +++ b/x-pack/plugins/apm/server/routes/alerts/rule_types/anomaly/register_anomaly_rule_type.ts @@ -5,7 +5,6 @@ * 2.0. */ import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { schema } from '@kbn/config-schema'; import { KibanaRequest } from '@kbn/core/server'; import datemath from '@kbn/datemath'; import type { ESSearchResponse } from '@kbn/es-types'; @@ -36,7 +35,6 @@ import { getEnvironmentEsField, getEnvironmentLabel, } from '../../../../../common/environment_filter_values'; -import { ANOMALY_SEVERITY } from '../../../../../common/ml_constants'; import { ANOMALY_ALERT_SEVERITY_TYPES, ApmRuleType, @@ -49,20 +47,7 @@ import { getMLJobs } from '../../../service_map/get_service_anomalies'; import { apmActionVariables } from '../../action_variables'; import { RegisterRuleDependencies } from '../../register_apm_rule_types'; import { getServiceGroupFieldsForAnomaly } from './get_service_group_fields_for_anomaly'; - -const paramsSchema = schema.object({ - serviceName: schema.maybe(schema.string()), - transactionType: schema.maybe(schema.string()), - windowSize: schema.number(), - windowUnit: schema.string(), - environment: schema.string(), - anomalySeverityType: schema.oneOf([ - schema.literal(ANOMALY_SEVERITY.CRITICAL), - schema.literal(ANOMALY_SEVERITY.MAJOR), - schema.literal(ANOMALY_SEVERITY.MINOR), - schema.literal(ANOMALY_SEVERITY.WARNING), - ]), -}); +import { anomalyParamsSchema } from '../../../../../common/rules/schema'; const ruleTypeConfig = RULE_TYPES_CONFIG[ApmRuleType.Anomaly]; @@ -86,9 +71,7 @@ export function registerAnomalyRuleType({ name: ruleTypeConfig.name, actionGroups: ruleTypeConfig.actionGroups, defaultActionGroupId: ruleTypeConfig.defaultActionGroupId, - validate: { - params: paramsSchema, - }, + validate: { params: anomalyParamsSchema }, actionVariables: { context: [ ...(observability.getAlertDetailsConfig()?.apm.enabled diff --git a/x-pack/plugins/apm/server/routes/alerts/rule_types/error_count/register_error_count_rule_type.test.ts b/x-pack/plugins/apm/server/routes/alerts/rule_types/error_count/register_error_count_rule_type.test.ts index 705804cfa74f4..6d6752eba0ce5 100644 --- a/x-pack/plugins/apm/server/routes/alerts/rule_types/error_count/register_error_count_rule_type.test.ts +++ b/x-pack/plugins/apm/server/routes/alerts/rule_types/error_count/register_error_count_rule_type.test.ts @@ -146,7 +146,7 @@ describe('Error count alert', () => { threshold: 2, triggerValue: 5, reason: 'Error count is 5 in the last 5 mins for foo. Alert when > 2.', - interval: '5m', + interval: '5 mins', viewInAppUrl: 'http://localhost:5601/eyr/app/apm/services/foo/errors?environment=env-foo', }); @@ -159,7 +159,7 @@ describe('Error count alert', () => { threshold: 2, triggerValue: 4, reason: 'Error count is 4 in the last 5 mins for foo. Alert when > 2.', - interval: '5m', + interval: '5 mins', viewInAppUrl: 'http://localhost:5601/eyr/app/apm/services/foo/errors?environment=env-foo-2', }); @@ -172,7 +172,7 @@ describe('Error count alert', () => { reason: 'Error count is 3 in the last 5 mins for bar. Alert when > 2.', threshold: 2, triggerValue: 3, - interval: '5m', + interval: '5 mins', viewInAppUrl: 'http://localhost:5601/eyr/app/apm/services/bar/errors?environment=env-bar', }); diff --git a/x-pack/plugins/apm/server/routes/alerts/rule_types/error_count/register_error_count_rule_type.ts b/x-pack/plugins/apm/server/routes/alerts/rule_types/error_count/register_error_count_rule_type.ts index 54bfb00d468b0..c811e71fe1f17 100644 --- a/x-pack/plugins/apm/server/routes/alerts/rule_types/error_count/register_error_count_rule_type.ts +++ b/x-pack/plugins/apm/server/routes/alerts/rule_types/error_count/register_error_count_rule_type.ts @@ -5,54 +5,49 @@ * 2.0. */ -import { schema } from '@kbn/config-schema'; -import { firstValueFrom } from 'rxjs'; +import { getAlertDetailsUrl } from '@kbn/infra-plugin/server/lib/alerting/common/utils'; +import { + formatDurationFromTimeUnitChar, + ProcessorEvent, + TimeUnitChar, +} from '@kbn/observability-plugin/common'; +import { termQuery } from '@kbn/observability-plugin/server'; import { ALERT_EVALUATION_THRESHOLD, ALERT_EVALUATION_VALUE, ALERT_REASON, } from '@kbn/rule-data-utils'; import { createLifecycleRuleTypeFactory } from '@kbn/rule-registry-plugin/server'; -import { termQuery } from '@kbn/observability-plugin/server'; -import { ProcessorEvent } from '@kbn/observability-plugin/common'; -import { getAlertDetailsUrl } from '@kbn/infra-plugin/server/lib/alerting/common/utils'; import { addSpaceIdToPath } from '@kbn/spaces-plugin/common'; - +import { firstValueFrom } from 'rxjs'; import { ENVIRONMENT_NOT_DEFINED, getEnvironmentEsField, getEnvironmentLabel, } from '../../../../../common/environment_filter_values'; -import { getAlertUrlErrorCount } from '../../../../../common/utils/formatters'; -import { - ApmRuleType, - APM_SERVER_FEATURE_ID, - RULE_TYPES_CONFIG, - formatErrorCountReason, -} from '../../../../../common/rules/apm_rule_types'; import { PROCESSOR_EVENT, SERVICE_ENVIRONMENT, SERVICE_NAME, } from '../../../../../common/es_fields/apm'; +import { + ApmRuleType, + APM_SERVER_FEATURE_ID, + formatErrorCountReason, + RULE_TYPES_CONFIG, +} from '../../../../../common/rules/apm_rule_types'; +import { errorCountParamsSchema } from '../../../../../common/rules/schema'; import { environmentQuery } from '../../../../../common/utils/environment_query'; +import { getAlertUrlErrorCount } from '../../../../../common/utils/formatters'; import { getApmIndices } from '../../../settings/apm_indices/get_apm_indices'; import { apmActionVariables } from '../../action_variables'; import { alertingEsClient } from '../../alerting_es_client'; import { RegisterRuleDependencies } from '../../register_apm_rule_types'; import { - getServiceGroupFieldsAgg, getServiceGroupFields, + getServiceGroupFieldsAgg, } from '../get_service_group_fields'; -const paramsSchema = schema.object({ - windowSize: schema.number(), - windowUnit: schema.string(), - threshold: schema.number(), - serviceName: schema.maybe(schema.string()), - environment: schema.string(), -}); - const ruleTypeConfig = RULE_TYPES_CONFIG[ApmRuleType.ErrorCount]; export function registerErrorCountRuleType({ @@ -74,9 +69,7 @@ export function registerErrorCountRuleType({ name: ruleTypeConfig.name, actionGroups: ruleTypeConfig.actionGroups, defaultActionGroupId: ruleTypeConfig.defaultActionGroupId, - validate: { - params: paramsSchema, - }, + validate: { params: errorCountParamsSchema }, actionVariables: { context: [ ...(observability.getAlertDetailsConfig()?.apm.enabled @@ -214,7 +207,10 @@ export function registerErrorCountRuleType({ .scheduleActions(ruleTypeConfig.defaultActionGroupId, { alertDetailsUrl, environment: getEnvironmentLabel(environment), - interval: `${ruleParams.windowSize}${ruleParams.windowUnit}`, + interval: formatDurationFromTimeUnitChar( + ruleParams.windowSize, + ruleParams.windowUnit as TimeUnitChar + ), reason: alertReason, serviceName, threshold: ruleParams.threshold, diff --git a/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_duration/register_transaction_duration_rule_type.test.ts b/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_duration/register_transaction_duration_rule_type.test.ts index cebf71f1bad97..dcd8994860ae7 100644 --- a/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_duration/register_transaction_duration_rule_type.test.ts +++ b/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_duration/register_transaction_duration_rule_type.test.ts @@ -60,7 +60,7 @@ describe('registerTransactionDurationRuleType', () => { 'http://localhost:5601/eyr/app/observability/alerts/' ), environment: 'Not defined', - interval: `5m`, + interval: `5 mins`, reason: 'Avg. latency is 5,500 ms in the last 5 mins for opbeans-java. Alert when > 3,000 ms.', transactionType: 'request', diff --git a/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_duration/register_transaction_duration_rule_type.ts b/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_duration/register_transaction_duration_rule_type.ts index 803945568494b..bc11cee03a506 100644 --- a/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_duration/register_transaction_duration_rule_type.ts +++ b/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_duration/register_transaction_duration_rule_type.ts @@ -6,41 +6,46 @@ */ import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { schema } from '@kbn/config-schema'; +import { getAlertDetailsUrl } from '@kbn/infra-plugin/server/lib/alerting/common/utils'; +import { + formatDurationFromTimeUnitChar, + ProcessorEvent, + TimeUnitChar, +} from '@kbn/observability-plugin/common'; +import { asDuration } from '@kbn/observability-plugin/common/utils/formatters'; +import { termQuery } from '@kbn/observability-plugin/server'; import { ALERT_EVALUATION_THRESHOLD, ALERT_EVALUATION_VALUE, ALERT_REASON, } from '@kbn/rule-data-utils'; -import { firstValueFrom } from 'rxjs'; -import { asDuration } from '@kbn/observability-plugin/common/utils/formatters'; -import { termQuery } from '@kbn/observability-plugin/server'; import { createLifecycleRuleTypeFactory } from '@kbn/rule-registry-plugin/server'; -import { ProcessorEvent } from '@kbn/observability-plugin/common'; -import { getAlertDetailsUrl } from '@kbn/infra-plugin/server/lib/alerting/common/utils'; import { addSpaceIdToPath } from '@kbn/spaces-plugin/common'; -import { getAlertUrlTransaction } from '../../../../../common/utils/formatters'; +import { firstValueFrom } from 'rxjs'; import { SearchAggregatedTransactionSetting } from '../../../../../common/aggregated_transactions'; import { - ApmRuleType, - AggregationType, - RULE_TYPES_CONFIG, - APM_SERVER_FEATURE_ID, - formatTransactionDurationReason, -} from '../../../../../common/rules/apm_rule_types'; + ENVIRONMENT_NOT_DEFINED, + getEnvironmentEsField, + getEnvironmentLabel, +} from '../../../../../common/environment_filter_values'; import { PROCESSOR_EVENT, + SERVICE_ENVIRONMENT, SERVICE_NAME, TRANSACTION_TYPE, - SERVICE_ENVIRONMENT, } from '../../../../../common/es_fields/apm'; import { - ENVIRONMENT_NOT_DEFINED, - getEnvironmentEsField, - getEnvironmentLabel, -} from '../../../../../common/environment_filter_values'; + ApmRuleType, + APM_SERVER_FEATURE_ID, + formatTransactionDurationReason, + RULE_TYPES_CONFIG, +} from '../../../../../common/rules/apm_rule_types'; +import { transactionDurationParamsSchema } from '../../../../../common/rules/schema'; import { environmentQuery } from '../../../../../common/utils/environment_query'; -import { getDurationFormatter } from '../../../../../common/utils/formatters'; +import { + getAlertUrlTransaction, + getDurationFormatter, +} from '../../../../../common/utils/formatters'; import { getDocumentTypeFilterForTransactions, getDurationFieldForTransactions, @@ -49,28 +54,14 @@ import { getApmIndices } from '../../../settings/apm_indices/get_apm_indices'; import { apmActionVariables } from '../../action_variables'; import { alertingEsClient } from '../../alerting_es_client'; import { RegisterRuleDependencies } from '../../register_apm_rule_types'; -import { - averageOrPercentileAgg, - getMultiTermsSortOrder, -} from './average_or_percentile_agg'; import { getServiceGroupFields, getServiceGroupFieldsAgg, } from '../get_service_group_fields'; - -const paramsSchema = schema.object({ - serviceName: schema.maybe(schema.string()), - transactionType: schema.maybe(schema.string()), - windowSize: schema.number(), - windowUnit: schema.string(), - threshold: schema.number(), - aggregationType: schema.oneOf([ - schema.literal(AggregationType.Avg), - schema.literal(AggregationType.P95), - schema.literal(AggregationType.P99), - ]), - environment: schema.string(), -}); +import { + averageOrPercentileAgg, + getMultiTermsSortOrder, +} from './average_or_percentile_agg'; const ruleTypeConfig = RULE_TYPES_CONFIG[ApmRuleType.TransactionDuration]; @@ -92,9 +83,7 @@ export function registerTransactionDurationRuleType({ name: ruleTypeConfig.name, actionGroups: ruleTypeConfig.actionGroups, defaultActionGroupId: ruleTypeConfig.defaultActionGroupId, - validate: { - params: paramsSchema, - }, + validate: { params: transactionDurationParamsSchema }, actionVariables: { context: [ ...(observability.getAlertDetailsConfig()?.apm.enabled @@ -289,7 +278,10 @@ export function registerTransactionDurationRuleType({ .scheduleActions(ruleTypeConfig.defaultActionGroupId, { alertDetailsUrl, environment: environmentLabel, - interval: `${ruleParams.windowSize}${ruleParams.windowUnit}`, + interval: formatDurationFromTimeUnitChar( + ruleParams.windowSize, + ruleParams.windowUnit as TimeUnitChar + ), reason, serviceName, threshold: ruleParams.threshold, diff --git a/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/register_transaction_error_rate_rule_type.test.ts b/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/register_transaction_error_rate_rule_type.test.ts index 38de7d48cce4c..02eb14e782df3 100644 --- a/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/register_transaction_error_rate_rule_type.test.ts +++ b/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/register_transaction_error_rate_rule_type.test.ts @@ -131,7 +131,7 @@ describe('Transaction error rate alert', () => { 'Failed transactions is 10% in the last 5 mins for foo. Alert when > 10%.', threshold: 10, triggerValue: '10', - interval: '5m', + interval: '5 mins', viewInAppUrl: 'http://localhost:5601/eyr/app/apm/services/foo?transactionType=type-foo&environment=env-foo', }); diff --git a/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/register_transaction_error_rate_rule_type.ts b/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/register_transaction_error_rate_rule_type.ts index 308ed32e3b5a1..df94a3e8a3c75 100644 --- a/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/register_transaction_error_rate_rule_type.ts +++ b/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/register_transaction_error_rate_rule_type.ts @@ -5,31 +5,28 @@ * 2.0. */ -import { schema } from '@kbn/config-schema'; -import { firstValueFrom } from 'rxjs'; +import { getAlertDetailsUrl } from '@kbn/infra-plugin/server/lib/alerting/common/utils'; +import { + formatDurationFromTimeUnitChar, + ProcessorEvent, + TimeUnitChar, +} from '@kbn/observability-plugin/common'; +import { asPercent } from '@kbn/observability-plugin/common/utils/formatters'; +import { termQuery } from '@kbn/observability-plugin/server'; import { ALERT_EVALUATION_THRESHOLD, ALERT_EVALUATION_VALUE, ALERT_REASON, } from '@kbn/rule-data-utils'; import { createLifecycleRuleTypeFactory } from '@kbn/rule-registry-plugin/server'; -import { asPercent } from '@kbn/observability-plugin/common/utils/formatters'; -import { termQuery } from '@kbn/observability-plugin/server'; import { addSpaceIdToPath } from '@kbn/spaces-plugin/common'; -import { ProcessorEvent } from '@kbn/observability-plugin/common'; -import { getAlertDetailsUrl } from '@kbn/infra-plugin/server/lib/alerting/common/utils'; +import { firstValueFrom } from 'rxjs'; +import { SearchAggregatedTransactionSetting } from '../../../../../common/aggregated_transactions'; import { ENVIRONMENT_NOT_DEFINED, getEnvironmentEsField, getEnvironmentLabel, } from '../../../../../common/environment_filter_values'; -import { getAlertUrlTransaction } from '../../../../../common/utils/formatters'; -import { - ApmRuleType, - RULE_TYPES_CONFIG, - APM_SERVER_FEATURE_ID, - formatTransactionErrorRateReason, -} from '../../../../../common/rules/apm_rule_types'; import { EVENT_OUTCOME, PROCESSOR_EVENT, @@ -38,28 +35,28 @@ import { TRANSACTION_TYPE, } from '../../../../../common/es_fields/apm'; import { EventOutcome } from '../../../../../common/event_outcome'; -import { asDecimalOrInteger } from '../../../../../common/utils/formatters'; +import { + ApmRuleType, + APM_SERVER_FEATURE_ID, + formatTransactionErrorRateReason, + RULE_TYPES_CONFIG, +} from '../../../../../common/rules/apm_rule_types'; +import { transactionErrorRateParamsSchema } from '../../../../../common/rules/schema'; import { environmentQuery } from '../../../../../common/utils/environment_query'; +import { + asDecimalOrInteger, + getAlertUrlTransaction, +} from '../../../../../common/utils/formatters'; +import { getDocumentTypeFilterForTransactions } from '../../../../lib/helpers/transactions'; import { getApmIndices } from '../../../settings/apm_indices/get_apm_indices'; import { apmActionVariables } from '../../action_variables'; import { alertingEsClient } from '../../alerting_es_client'; import { RegisterRuleDependencies } from '../../register_apm_rule_types'; -import { SearchAggregatedTransactionSetting } from '../../../../../common/aggregated_transactions'; -import { getDocumentTypeFilterForTransactions } from '../../../../lib/helpers/transactions'; import { getServiceGroupFields, getServiceGroupFieldsAgg, } from '../get_service_group_fields'; -const paramsSchema = schema.object({ - windowSize: schema.number(), - windowUnit: schema.string(), - threshold: schema.number(), - transactionType: schema.maybe(schema.string()), - serviceName: schema.maybe(schema.string()), - environment: schema.string(), -}); - const ruleTypeConfig = RULE_TYPES_CONFIG[ApmRuleType.TransactionErrorRate]; export function registerTransactionErrorRateRuleType({ @@ -81,9 +78,7 @@ export function registerTransactionErrorRateRuleType({ name: ruleTypeConfig.name, actionGroups: ruleTypeConfig.actionGroups, defaultActionGroupId: ruleTypeConfig.defaultActionGroupId, - validate: { - params: paramsSchema, - }, + validate: { params: transactionErrorRateParamsSchema }, actionVariables: { context: [ ...(observability.getAlertDetailsConfig()?.apm.enabled @@ -285,7 +280,10 @@ export function registerTransactionErrorRateRuleType({ .scheduleActions(ruleTypeConfig.defaultActionGroupId, { alertDetailsUrl, environment: getEnvironmentLabel(environment), - interval: `${ruleParams.windowSize}${ruleParams.windowUnit}`, + interval: formatDurationFromTimeUnitChar( + ruleParams.windowSize, + ruleParams.windowUnit as TimeUnitChar + ), reason: reasonMessage, serviceName, threshold: ruleParams.threshold, diff --git a/x-pack/test/apm_api_integration/tests/alerts/alerting_api_helper.ts b/x-pack/test/apm_api_integration/tests/alerts/alerting_api_helper.ts new file mode 100644 index 0000000000000..b6c5b891b8353 --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/alerts/alerting_api_helper.ts @@ -0,0 +1,117 @@ +/* + * 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 { SuperTest, Test } from 'supertest'; +import { ApmRuleType } from '@kbn/apm-plugin/common/rules/apm_rule_types'; +import { ApmRuleParamsType } from '@kbn/apm-plugin/common/rules/schema'; +import { ApmDocumentType } from '@kbn/apm-plugin/common/document_type'; +import { RollupInterval } from '@kbn/apm-plugin/common/rollup'; +import { ApmApiClient } from '../../common/config'; + +export async function createIndexConnector({ + supertest, + name, + indexName, +}: { + supertest: SuperTest; + name: string; + indexName: string; +}) { + const { body } = await supertest + .post(`/api/actions/connector`) + .set('kbn-xsrf', 'foo') + .send({ + name, + config: { + index: indexName, + refresh: true, + }, + connector_type_id: '.index', + }); + return body.id as string; +} + +export async function createApmRule({ + supertest, + name, + ruleTypeId, + params, + actions = [], +}: { + supertest: SuperTest; + ruleTypeId: T; + name: string; + params: ApmRuleParamsType[T]; + actions?: any[]; +}) { + const { body } = await supertest + .post(`/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send({ + params, + consumer: 'apm', + schedule: { + interval: '1m', + }, + tags: ['apm'], + name, + rule_type_id: ruleTypeId, + actions, + }); + return body; +} + +function getTimerange() { + return { + start: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(), + end: new Date(Date.now() + 5 * 60 * 1000).toISOString(), + }; +} + +export async function fetchServiceInventoryAlertCounts(apmApiClient: ApmApiClient) { + const timerange = getTimerange(); + const serviceInventoryResponse = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services', + params: { + query: { + ...timerange, + environment: 'ENVIRONMENT_ALL', + kuery: '', + probability: 1, + documentType: ApmDocumentType.ServiceTransactionMetric, + rollupInterval: RollupInterval.SixtyMinutes, + }, + }, + }); + return serviceInventoryResponse.body.items.reduce>((acc, item) => { + return { ...acc, [item.serviceName]: item.alertsCount ?? 0 }; + }, {}); +} + +export async function fetchServiceTabAlertCount({ + apmApiClient, + serviceName, +}: { + apmApiClient: ApmApiClient; + serviceName: string; +}) { + const timerange = getTimerange(); + const alertsCountReponse = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services/{serviceName}/alerts_count', + params: { + path: { + serviceName, + }, + query: { + ...timerange, + environment: 'ENVIRONMENT_ALL', + }, + }, + }); + + return alertsCountReponse.body.alertsCount; +} diff --git a/x-pack/test/apm_api_integration/tests/alerts/anomaly_alert.spec.ts b/x-pack/test/apm_api_integration/tests/alerts/anomaly_alert.spec.ts index 39c81e71948a0..a72760ca339ce 100644 --- a/x-pack/test/apm_api_integration/tests/alerts/anomaly_alert.spec.ts +++ b/x-pack/test/apm_api_integration/tests/alerts/anomaly_alert.spec.ts @@ -9,8 +9,10 @@ import { ApmRuleType } from '@kbn/apm-plugin/common/rules/apm_rule_types'; import { apm, timerange } from '@kbn/apm-synthtrace-client'; import expect from '@kbn/expect'; import { range } from 'lodash'; +import { ANOMALY_SEVERITY } from '@kbn/apm-plugin/common/ml_constants'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { createAndRunApmMlJobs } from '../../common/utils/create_and_run_apm_ml_jobs'; +import { createApmRule } from './alerting_api_helper'; import { waitForRuleStatus } from './wait_for_rule_status'; export default function ApiTest({ getService }: FtrProviderContext) { @@ -18,7 +20,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const ml = getService('ml'); - const log = getService('log'); const es = getService('es'); const synthtraceEsClient = getService('synthtraceEsClient'); @@ -81,36 +82,29 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); it('checks if alert is active', async () => { - const { body: createdRule } = await supertest - .post(`/api/alerting/rule`) - .set('kbn-xsrf', 'foo') - .send({ - params: { - environment: 'production', - windowSize: 99, - windowUnit: 'y', - anomalySeverityType: 'warning', - }, - consumer: 'apm', - schedule: { - interval: '1m', - }, - tags: ['apm', 'service.name:service-a'], - name: 'Latency anomaly | service-a', - rule_type_id: ApmRuleType.Anomaly, - notify_when: 'onActiveAlert', - actions: [], - }); - - ruleId = createdRule.id; - - const executionStatus = await waitForRuleStatus({ - id: ruleId, - expectedStatus: 'active', + const createdRule = await createApmRule({ supertest, - log, + name: 'Latency anomaly | service-a', + params: { + environment: 'production', + windowSize: 99, + windowUnit: 'y', + anomalySeverityType: ANOMALY_SEVERITY.WARNING, + }, + ruleTypeId: ApmRuleType.Anomaly, }); - expect(executionStatus.status).to.be('active'); + + ruleId = createdRule.id; + if (!ruleId) { + expect(ruleId).to.not.eql(undefined); + } else { + const executionStatus = await waitForRuleStatus({ + id: ruleId, + expectedStatus: 'active', + supertest, + }); + expect(executionStatus.status).to.be('active'); + } }); }); } diff --git a/x-pack/test/apm_api_integration/tests/alerts/error_count_threshold.spec.ts b/x-pack/test/apm_api_integration/tests/alerts/error_count_threshold.spec.ts new file mode 100644 index 0000000000000..a19f1ef93503a --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/alerts/error_count_threshold.spec.ts @@ -0,0 +1,165 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ApmRuleType } from '@kbn/apm-plugin/common/rules/apm_rule_types'; +import { errorCountMessage } from '@kbn/apm-plugin/common/rules/default_action_message'; +import { apm, timerange } from '@kbn/apm-synthtrace-client'; +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + createApmRule, + createIndexConnector, + fetchServiceInventoryAlertCounts, + fetchServiceTabAlertCount, +} from './alerting_api_helper'; +import { waitForRuleStatus, waitForDocumentInIndex } from './wait_for_rule_status'; + +export default function ApiTest({ getService }: FtrProviderContext) { + const registry = getService('registry'); + + const supertest = getService('supertest'); + const es = getService('es'); + const apmApiClient = getService('apmApiClient'); + const esDeleteAllIndices = getService('esDeleteAllIndices'); + + const synthtraceEsClient = getService('synthtraceEsClient'); + + registry.when('error count threshold alert', { config: 'basic', archives: [] }, () => { + let ruleId: string; + let actionId: string | undefined; + + const INDEX_NAME = 'error-count'; + + before(async () => { + const opbeansJava = apm + .service({ name: 'opbeans-java', environment: 'production', agentName: 'java' }) + .instance('instance'); + const opbeansNode = apm + .service({ name: 'opbeans-node', environment: 'production', agentName: 'node' }) + .instance('instance'); + const events = timerange('now-15m', 'now') + .ratePerMinute(1) + .generator((timestamp) => { + return [ + opbeansJava + .transaction({ transactionName: 'tx-java' }) + .timestamp(timestamp) + .duration(100) + .failure() + .errors( + opbeansJava + .error({ message: '[ResponseError] index_not_found_exception' }) + .timestamp(timestamp + 50) + ), + opbeansNode + .transaction({ transactionName: 'tx-node' }) + .timestamp(timestamp) + .duration(100) + .success(), + ]; + }); + await synthtraceEsClient.index(events); + }); + + after(async () => { + await synthtraceEsClient.clean(); + await supertest.delete(`/api/alerting/rule/${ruleId}`).set('kbn-xsrf', 'foo'); + await supertest.delete(`/api/actions/connector/${actionId}`).set('kbn-xsrf', 'foo'); + await esDeleteAllIndices(['.alerts*', INDEX_NAME]); + await es.deleteByQuery({ + index: '.kibana-event-log-*', + query: { term: { 'kibana.alert.rule.consumer': 'apm' } }, + }); + }); + + describe('create alert', () => { + before(async () => { + actionId = await createIndexConnector({ + supertest, + name: 'Error count API test', + indexName: INDEX_NAME, + }); + const createdRule = await createApmRule({ + supertest, + ruleTypeId: ApmRuleType.ErrorCount, + name: 'Apm error count', + params: { + environment: 'production', + threshold: 1, + windowSize: 1, + windowUnit: 'h', + }, + actions: [ + { + group: 'threshold_met', + id: actionId, + params: { + documents: [{ message: errorCountMessage }], + }, + frequency: { + notify_when: 'onActionGroupChange', + throttle: null, + summary: false, + }, + }, + ], + }); + expect(createdRule.id).to.not.eql(undefined); + ruleId = createdRule.id; + }); + + it('checks if alert is active', async () => { + const executionStatus = await waitForRuleStatus({ + id: ruleId, + expectedStatus: 'active', + supertest, + }); + expect(executionStatus.status).to.be('active'); + }); + + it('returns correct message', async () => { + const resp = await waitForDocumentInIndex<{ message: string }>({ + es, + indexName: INDEX_NAME, + }); + + expect(resp.hits.hits[0]._source?.message).eql( + `Apm error count alert is firing because of the following conditions: + +- Service name: opbeans-java +- Environment: production +- Threshold: 1 +- Triggered value: 15 errors over the last 1 hr` + ); + }); + + it('shows the correct alert count for each service on service inventory', async () => { + const serviceInventoryAlertCounts = await fetchServiceInventoryAlertCounts(apmApiClient); + expect(serviceInventoryAlertCounts).to.eql({ + 'opbeans-node': 0, + 'opbeans-java': 1, + }); + }); + + it('shows the correct alert count in opbeans-java service', async () => { + const serviceTabAlertCount = await fetchServiceTabAlertCount({ + apmApiClient, + serviceName: 'opbeans-java', + }); + expect(serviceTabAlertCount).to.be(1); + }); + + it('shows the correct alert count in opbeans-node service', async () => { + const serviceTabAlertCount = await fetchServiceTabAlertCount({ + apmApiClient, + serviceName: 'opbeans-node', + }); + expect(serviceTabAlertCount).to.be(0); + }); + }); + }); +} diff --git a/x-pack/test/apm_api_integration/tests/alerts/wait_for_rule_status.ts b/x-pack/test/apm_api_integration/tests/alerts/wait_for_rule_status.ts index f4814358f1636..44da2bea36bf2 100644 --- a/x-pack/test/apm_api_integration/tests/alerts/wait_for_rule_status.ts +++ b/x-pack/test/apm_api_integration/tests/alerts/wait_for_rule_status.ts @@ -4,53 +4,52 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { ToolingLog } from '@kbn/tooling-log'; -import expect from '@kbn/expect'; +import type { Client } from '@elastic/elasticsearch'; +import type { + AggregationsAggregate, + SearchResponse, +} from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import pRetry from 'p-retry'; import type SuperTest from 'supertest'; -const WAIT_FOR_STATUS_INCREMENT = 500; - export async function waitForRuleStatus({ id, expectedStatus, - waitMillis = 10000, supertest, - log, }: { + id: string; expectedStatus: string; supertest: SuperTest.SuperTest; - log: ToolingLog; - waitMillis?: number; - id?: string; }): Promise> { - if (waitMillis < 0 || !id) { - expect().fail(`waiting for alert ${id} status ${expectedStatus} timed out`); - } - - const response = await supertest.get(`/api/alerting/rule/${id}`); - expect(response.status).to.eql(200); - - const { execution_status: executionStatus } = response.body || {}; - const { status } = executionStatus || {}; - - const message = `waitForStatus(${expectedStatus}): got ${JSON.stringify(executionStatus)}`; - - if (status === expectedStatus) { - return executionStatus; - } - - log.debug(`${message}, retrying`); - - await delay(WAIT_FOR_STATUS_INCREMENT); - return await waitForRuleStatus({ - id, - expectedStatus, - waitMillis: waitMillis - WAIT_FOR_STATUS_INCREMENT, - supertest, - log, - }); + return pRetry( + async () => { + const response = await supertest.get(`/api/alerting/rule/${id}`); + const { execution_status: executionStatus } = response.body || {}; + const { status } = executionStatus || {}; + if (status !== expectedStatus) { + throw new Error(`waitForStatus(${expectedStatus}): got ${status}`); + } + return executionStatus; + }, + { retries: 10 } + ); } -async function delay(millis: number): Promise { - await new Promise((resolve) => setTimeout(resolve, millis)); +export async function waitForDocumentInIndex({ + es, + indexName, +}: { + es: Client; + indexName: string; +}): Promise>> { + return pRetry( + async () => { + const response = await es.search({ index: indexName }); + if (response.hits.hits.length === 0) { + throw new Error('No hits found'); + } + return response; + }, + { retries: 10 } + ); } diff --git a/x-pack/test/apm_api_integration/tests/service_groups/service_group_count/service_group_count.spec.ts b/x-pack/test/apm_api_integration/tests/service_groups/service_group_count/service_group_count.spec.ts index 69e3d7678ca9e..abae62f2012a6 100644 --- a/x-pack/test/apm_api_integration/tests/service_groups/service_group_count/service_group_count.spec.ts +++ b/x-pack/test/apm_api_integration/tests/service_groups/service_group_count/service_group_count.spec.ts @@ -4,10 +4,11 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { ApmRuleType } from '@kbn/apm-plugin/common/rules/apm_rule_types'; +import { AggregationType, ApmRuleType } from '@kbn/apm-plugin/common/rules/apm_rule_types'; import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; import { waitForActiveAlert } from '../../../common/utils/wait_for_active_alert'; +import { createApmRule } from '../../alerts/alerting_api_helper'; import { createServiceGroupApi, deleteAllServiceGroups, @@ -26,28 +27,21 @@ export default function ApiTest({ getService }: FtrProviderContext) { const start = Date.now() - 24 * 60 * 60 * 1000; const end = Date.now(); - async function createRule() { - return supertest - .post(`/api/alerting/rule`) - .set('kbn-xsrf', 'true') - .send({ - params: { - serviceName: 'synth-go', - transactionType: '', - windowSize: 99, - windowUnit: 'y', - threshold: 100, - aggregationType: 'avg', - environment: 'testing', - }, - consumer: 'apm', - schedule: { interval: '1m' }, - tags: ['apm'], - name: 'Latency threshold | synth-go', - rule_type_id: ApmRuleType.TransactionDuration, - notify_when: 'onActiveAlert', - actions: [], - }); + function createRule() { + return createApmRule({ + supertest, + name: 'Latency threshold | synth-go', + params: { + serviceName: 'synth-go', + transactionType: '', + windowSize: 99, + windowUnit: 'y', + threshold: 100, + aggregationType: AggregationType.Avg, + environment: 'testing', + }, + ruleTypeId: ApmRuleType.TransactionDuration, + }); } registry.when('Service group counts', { config: 'basic', archives: [] }, () => { @@ -89,7 +83,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { describe('with alerts', () => { let ruleId: string; before(async () => { - const { body: createdRule } = await createRule(); + const createdRule = await createRule(); ruleId = createdRule.id; await waitForActiveAlert({ ruleId, esClient, log }); }); diff --git a/x-pack/test/apm_api_integration/tests/services/service_alerts.spec.ts b/x-pack/test/apm_api_integration/tests/services/service_alerts.spec.ts index a8c92dfdd256e..35ee8da8ba39b 100644 --- a/x-pack/test/apm_api_integration/tests/services/service_alerts.spec.ts +++ b/x-pack/test/apm_api_integration/tests/services/service_alerts.spec.ts @@ -5,10 +5,11 @@ * 2.0. */ import expect from '@kbn/expect'; -import { ApmRuleType } from '@kbn/apm-plugin/common/rules/apm_rule_types'; +import { AggregationType, ApmRuleType } from '@kbn/apm-plugin/common/rules/apm_rule_types'; import { apm, timerange } from '@kbn/apm-synthtrace-client'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { waitForActiveAlert } from '../../common/utils/wait_for_active_alert'; +import { createApmRule } from '../alerts/alerting_api_helper'; export default function ServiceAlerts({ getService }: FtrProviderContext) { const registry = getService('registry'); @@ -42,28 +43,21 @@ export default function ServiceAlerts({ getService }: FtrProviderContext) { }); } - async function createRule() { - return supertest - .post(`/api/alerting/rule`) - .set('kbn-xsrf', 'true') - .send({ - params: { - serviceName: goService, - transactionType: '', - windowSize: 99, - windowUnit: 'y', - threshold: 100, - aggregationType: 'avg', - environment: 'testing', - }, - consumer: 'apm', - schedule: { interval: '1m' }, - tags: ['apm'], - name: `Latency threshold | ${goService}`, - rule_type_id: ApmRuleType.TransactionDuration, - notify_when: 'onActiveAlert', - actions: [], - }); + function createRule() { + return createApmRule({ + supertest, + name: `Latency threshold | ${goService}`, + params: { + serviceName: goService, + transactionType: '', + windowSize: 99, + windowUnit: 'y', + threshold: 100, + aggregationType: AggregationType.Avg, + environment: 'testing', + }, + ruleTypeId: ApmRuleType.TransactionDuration, + }); } registry.when('Service alerts', { config: 'basic', archives: [] }, () => { @@ -121,7 +115,7 @@ export default function ServiceAlerts({ getService }: FtrProviderContext) { describe('with alerts', () => { let ruleId: string; before(async () => { - const { body: createdRule } = await createRule(); + const createdRule = await createRule(); ruleId = createdRule.id; await waitForActiveAlert({ ruleId, esClient, log }); }); From c6641c9b3a696699cda160b2273e14245e9eb1dc Mon Sep 17 00:00:00 2001 From: Jiawei Wu <74562234+JiaweiWu@users.noreply.github.com> Date: Mon, 13 Feb 2023 11:53:53 -0800 Subject: [PATCH 19/20] [RAM][Flapping] Convert rule flapping settings API to snake case (#150951) ## Summary Resolves: https://github.com/elastic/kibana/issues/150623 Convert the rule flapping settings API properties from camel case to snake case. The properties are rewritten once it hits the application code to camel case again. ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../routes/get_flapping_settings.test.ts | 12 ++++- .../server/routes/get_flapping_settings.ts | 24 ++++++++-- .../routes/update_flapping_settings.test.ts | 37 ++++++++++----- .../server/routes/update_flapping_settings.ts | 46 ++++++++++++++++--- .../.storybook/context/http.ts | 4 +- .../lib/rule_api/get_flapping_settings.ts | 16 ++++++- .../lib/rule_api/update_flapping_settings.ts | 24 ++++++++-- .../common/lib/reset_rules_settings.ts | 6 ++- .../tests/alerting/get_flapping_settings.ts | 14 +++--- .../alerting/update_flapping_settings.ts | 40 ++++++++-------- .../tests/alerting/group1/event_log.ts | 8 ++-- .../triggers_actions_ui/rules_settings.ts | 4 +- 12 files changed, 173 insertions(+), 62 deletions(-) diff --git a/x-pack/plugins/alerting/server/routes/get_flapping_settings.test.ts b/x-pack/plugins/alerting/server/routes/get_flapping_settings.test.ts index 156ab604fb905..80354da80b784 100644 --- a/x-pack/plugins/alerting/server/routes/get_flapping_settings.test.ts +++ b/x-pack/plugins/alerting/server/routes/get_flapping_settings.test.ts @@ -58,6 +58,16 @@ describe('getFlappingSettingsRoute', () => { await handler(context, req, res); expect(rulesSettingsClient.flapping().get).toHaveBeenCalledTimes(1); - expect(res.ok).toHaveBeenCalled(); + expect(res.ok).toHaveBeenCalledWith({ + body: expect.objectContaining({ + enabled: true, + look_back_window: 10, + status_change_threshold: 10, + created_by: 'test name', + updated_by: 'test name', + created_at: expect.any(String), + updated_at: expect.any(String), + }), + }); }); }); diff --git a/x-pack/plugins/alerting/server/routes/get_flapping_settings.ts b/x-pack/plugins/alerting/server/routes/get_flapping_settings.ts index 6ae039032994d..5d4795d664ed5 100644 --- a/x-pack/plugins/alerting/server/routes/get_flapping_settings.ts +++ b/x-pack/plugins/alerting/server/routes/get_flapping_settings.ts @@ -8,8 +8,26 @@ import { IRouter } from '@kbn/core/server'; import { ILicenseState } from '../lib'; import { AlertingRequestHandlerContext, INTERNAL_BASE_ALERTING_API_PATH } from '../types'; -import { verifyAccessAndContext } from './lib'; -import { API_PRIVILEGES } from '../../common'; +import { verifyAccessAndContext, RewriteResponseCase } from './lib'; +import { API_PRIVILEGES, RulesSettingsFlapping } from '../../common'; + +const rewriteBodyRes: RewriteResponseCase = ({ + lookBackWindow, + statusChangeThreshold, + createdBy, + updatedBy, + createdAt, + updatedAt, + ...rest +}) => ({ + ...rest, + look_back_window: lookBackWindow, + status_change_threshold: statusChangeThreshold, + created_by: createdBy, + updated_by: updatedBy, + created_at: createdAt, + updated_at: updatedAt, +}); export const getFlappingSettingsRoute = ( router: IRouter, @@ -27,7 +45,7 @@ export const getFlappingSettingsRoute = ( verifyAccessAndContext(licenseState, async function (context, req, res) { const rulesSettingsClient = (await context.alerting).getRulesSettingsClient(); const flappingSettings = await rulesSettingsClient.flapping().get(); - return res.ok({ body: flappingSettings }); + return res.ok({ body: rewriteBodyRes(flappingSettings) }); }) ) ); diff --git a/x-pack/plugins/alerting/server/routes/update_flapping_settings.test.ts b/x-pack/plugins/alerting/server/routes/update_flapping_settings.test.ts index 28914e71e7dd3..84fb238b8509b 100644 --- a/x-pack/plugins/alerting/server/routes/update_flapping_settings.test.ts +++ b/x-pack/plugins/alerting/server/routes/update_flapping_settings.test.ts @@ -22,6 +22,16 @@ beforeEach(() => { rulesSettingsClient = rulesSettingsClientMock.create(); }); +const mockFlappingSettings = { + enabled: true, + lookBackWindow: 10, + statusChangeThreshold: 10, + createdBy: 'test name', + updatedBy: 'test name', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), +}; + describe('updateFlappingSettingsRoute', () => { test('updates flapping settings', async () => { const licenseState = licenseStateMock.create(); @@ -40,20 +50,13 @@ describe('updateFlappingSettingsRoute', () => { } `); - (rulesSettingsClient.flapping().get as jest.Mock).mockResolvedValue({ - enabled: true, - lookBackWindow: 10, - statusChangeThreshold: 10, - createdBy: 'test name', - updatedBy: 'test name', - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - }); + (rulesSettingsClient.flapping().get as jest.Mock).mockResolvedValue(mockFlappingSettings); + (rulesSettingsClient.flapping().update as jest.Mock).mockResolvedValue(mockFlappingSettings); const updateResult = { enabled: false, - lookBackWindow: 6, - statusChangeThreshold: 5, + look_back_window: 6, + status_change_threshold: 5, }; const [context, req, res] = mockHandlerArguments( @@ -77,6 +80,16 @@ describe('updateFlappingSettingsRoute', () => { }, ] `); - expect(res.ok).toHaveBeenCalled(); + expect(res.ok).toHaveBeenCalledWith({ + body: expect.objectContaining({ + enabled: true, + look_back_window: 10, + status_change_threshold: 10, + created_by: 'test name', + updated_by: 'test name', + created_at: expect.any(String), + updated_at: expect.any(String), + }), + }); }); }); diff --git a/x-pack/plugins/alerting/server/routes/update_flapping_settings.ts b/x-pack/plugins/alerting/server/routes/update_flapping_settings.ts index ede33a7d36a95..6df16434d3833 100644 --- a/x-pack/plugins/alerting/server/routes/update_flapping_settings.ts +++ b/x-pack/plugins/alerting/server/routes/update_flapping_settings.ts @@ -8,14 +8,46 @@ import { IRouter } from '@kbn/core/server'; import { schema } from '@kbn/config-schema'; import { ILicenseState } from '../lib'; -import { verifyAccessAndContext } from './lib'; +import { verifyAccessAndContext, RewriteResponseCase, RewriteRequestCase } from './lib'; import { AlertingRequestHandlerContext, INTERNAL_BASE_ALERTING_API_PATH } from '../types'; -import { API_PRIVILEGES } from '../../common'; +import { + API_PRIVILEGES, + RulesSettingsFlapping, + RulesSettingsFlappingProperties, +} from '../../common'; const bodySchema = schema.object({ enabled: schema.boolean(), - lookBackWindow: schema.number(), - statusChangeThreshold: schema.number(), + look_back_window: schema.number(), + status_change_threshold: schema.number(), +}); + +const rewriteQueryReq: RewriteRequestCase = ({ + look_back_window: lookBackWindow, + status_change_threshold: statusChangeThreshold, + ...rest +}) => ({ + ...rest, + lookBackWindow, + statusChangeThreshold, +}); + +const rewriteBodyRes: RewriteResponseCase = ({ + lookBackWindow, + statusChangeThreshold, + createdBy, + updatedBy, + createdAt, + updatedAt, + ...rest +}) => ({ + ...rest, + look_back_window: lookBackWindow, + status_change_threshold: statusChangeThreshold, + created_by: createdBy, + updated_by: updatedBy, + created_at: createdAt, + updated_at: updatedAt, }); export const updateFlappingSettingsRoute = ( @@ -36,10 +68,12 @@ export const updateFlappingSettingsRoute = ( verifyAccessAndContext(licenseState, async function (context, req, res) { const rulesSettingsClient = (await context.alerting).getRulesSettingsClient(); - const updatedFlappingSettings = await rulesSettingsClient.flapping().update(req.body); + const updatedFlappingSettings = await rulesSettingsClient + .flapping() + .update(rewriteQueryReq(req.body)); return res.ok({ - body: updatedFlappingSettings, + body: updatedFlappingSettings && rewriteBodyRes(updatedFlappingSettings), }); }) ) diff --git a/x-pack/plugins/triggers_actions_ui/.storybook/context/http.ts b/x-pack/plugins/triggers_actions_ui/.storybook/context/http.ts index 69e1cf2f64b91..ba7cc79901801 100644 --- a/x-pack/plugins/triggers_actions_ui/.storybook/context/http.ts +++ b/x-pack/plugins/triggers_actions_ui/.storybook/context/http.ts @@ -279,8 +279,8 @@ const rulesSettingsGetResponse = (path: string) => { if (path.endsWith('/settings/_flapping')) { return { enabled: true, - lookBackWindow: 20, - statusChangeThreshold: 4, + look_back_window: 20, + status_change_threshold: 4, }; } }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/get_flapping_settings.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/get_flapping_settings.ts index 68947de984fb4..931b1037ef729 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/get_flapping_settings.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/get_flapping_settings.ts @@ -6,11 +6,23 @@ */ import { HttpSetup } from '@kbn/core/public'; +import { AsApiContract, RewriteRequestCase } from '@kbn/actions-plugin/common'; import { RulesSettingsFlapping } from '@kbn/alerting-plugin/common'; import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants'; -export const getFlappingSettings = ({ http }: { http: HttpSetup }) => { - return http.get( +const rewriteBodyRes: RewriteRequestCase = ({ + look_back_window: lookBackWindow, + status_change_threshold: statusChangeThreshold, + ...rest +}: any) => ({ + ...rest, + lookBackWindow, + statusChangeThreshold, +}); + +export const getFlappingSettings = async ({ http }: { http: HttpSetup }) => { + const res = await http.get>( `${INTERNAL_BASE_ALERTING_API_PATH}/rules/settings/_flapping` ); + return rewriteBodyRes(res); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/update_flapping_settings.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/update_flapping_settings.ts index f38393b591d72..9e03da7e6e100 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/update_flapping_settings.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/update_flapping_settings.ts @@ -10,9 +10,20 @@ import { RulesSettingsFlapping, RulesSettingsFlappingProperties, } from '@kbn/alerting-plugin/common'; +import { AsApiContract, RewriteRequestCase } from '@kbn/actions-plugin/common'; import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants'; -export const updateFlappingSettings = ({ +const rewriteBodyRes: RewriteRequestCase = ({ + look_back_window: lookBackWindow, + status_change_threshold: statusChangeThreshold, + ...rest +}: any) => ({ + ...rest, + lookBackWindow, + statusChangeThreshold, +}); + +export const updateFlappingSettings = async ({ http, flappingSettings, }: { @@ -21,14 +32,21 @@ export const updateFlappingSettings = ({ }) => { let body: string; try { - body = JSON.stringify(flappingSettings); + body = JSON.stringify({ + enabled: flappingSettings.enabled, + look_back_window: flappingSettings.lookBackWindow, + status_change_threshold: flappingSettings.statusChangeThreshold, + }); } catch (e) { throw new Error(`Unable to parse flapping settings update params: ${e}`); } - return http.post( + + const res = await http.post>( `${INTERNAL_BASE_ALERTING_API_PATH}/rules/settings/_flapping`, { body, } ); + + return rewriteBodyRes(res); }; diff --git a/x-pack/test/alerting_api_integration/common/lib/reset_rules_settings.ts b/x-pack/test/alerting_api_integration/common/lib/reset_rules_settings.ts index b1b24856e9ef8..17ce4985e1f0c 100644 --- a/x-pack/test/alerting_api_integration/common/lib/reset_rules_settings.ts +++ b/x-pack/test/alerting_api_integration/common/lib/reset_rules_settings.ts @@ -14,6 +14,10 @@ export const resetRulesSettings = (supertest: any, space: string) => { .post(`${getUrlPrefix(space)}/internal/alerting/rules/settings/_flapping`) .set('kbn-xsrf', 'foo') .auth(Superuser.username, Superuser.password) - .send(DEFAULT_FLAPPING_SETTINGS) + .send({ + enabled: DEFAULT_FLAPPING_SETTINGS.enabled, + look_back_window: DEFAULT_FLAPPING_SETTINGS.lookBackWindow, + status_change_threshold: DEFAULT_FLAPPING_SETTINGS.statusChangeThreshold, + }) .expect(200); }; diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/get_flapping_settings.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/get_flapping_settings.ts index 7bc307f41e6d4..386e32b8e5778 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/get_flapping_settings.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/get_flapping_settings.ts @@ -51,14 +51,16 @@ export default function getFlappingSettingsTests({ getService }: FtrProviderCont case 'space_1_all at space1': expect(response.statusCode).to.eql(200); expect(response.body.enabled).to.eql(DEFAULT_FLAPPING_SETTINGS.enabled); - expect(response.body.lookBackWindow).to.eql(DEFAULT_FLAPPING_SETTINGS.lookBackWindow); - expect(response.body.statusChangeThreshold).to.eql( + expect(response.body.look_back_window).to.eql( + DEFAULT_FLAPPING_SETTINGS.lookBackWindow + ); + expect(response.body.status_change_threshold).to.eql( DEFAULT_FLAPPING_SETTINGS.statusChangeThreshold ); - expect(response.body.createdBy).to.be.a('string'); - expect(response.body.updatedBy).to.be.a('string'); - expect(Date.parse(response.body.createdAt)).to.be.greaterThan(0); - expect(Date.parse(response.body.updatedAt)).to.be.greaterThan(0); + expect(response.body.created_by).to.be.a('string'); + expect(response.body.updated_by).to.be.a('string'); + expect(Date.parse(response.body.created_at)).to.be.greaterThan(0); + expect(Date.parse(response.body.updated_at)).to.be.greaterThan(0); break; default: throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/update_flapping_settings.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/update_flapping_settings.ts index 93659256d2e97..a4d39ad4be89b 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/update_flapping_settings.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/update_flapping_settings.ts @@ -30,8 +30,8 @@ export default function updateFlappingSettingsTest({ getService }: FtrProviderCo .auth(user.username, user.password) .send({ enabled: false, - lookBackWindow: 20, - statusChangeThreshold: 20, + look_back_window: 20, + status_change_threshold: 20, }); switch (scenario.id) { @@ -51,12 +51,12 @@ export default function updateFlappingSettingsTest({ getService }: FtrProviderCo case 'space_1_all at space1': expect(response.statusCode).to.eql(200); expect(response.body.enabled).to.eql(false); - expect(response.body.lookBackWindow).to.eql(20); - expect(response.body.statusChangeThreshold).to.eql(20); - expect(response.body.createdBy).to.eql(user.username); - expect(response.body.updatedBy).to.eql(user.username); - expect(Date.parse(response.body.createdAt)).to.be.greaterThan(0); - expect(Date.parse(response.body.updatedAt)).to.be.greaterThan(0); + expect(response.body.look_back_window).to.eql(20); + expect(response.body.status_change_threshold).to.eql(20); + expect(response.body.created_by).to.eql(user.username); + expect(response.body.updated_by).to.eql(user.username); + expect(Date.parse(response.body.created_at)).to.be.greaterThan(0); + expect(Date.parse(response.body.updated_at)).to.be.greaterThan(0); break; default: throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); @@ -72,8 +72,8 @@ export default function updateFlappingSettingsTest({ getService }: FtrProviderCo .auth(Superuser.username, Superuser.password) .send({ enabled: true, - lookBackWindow: 200, - statusChangeThreshold: 200, + look_back_window: 200, + status_change_threshold: 200, }) .expect(400); @@ -87,8 +87,8 @@ export default function updateFlappingSettingsTest({ getService }: FtrProviderCo .auth(Superuser.username, Superuser.password) .send({ enabled: true, - lookBackWindow: 20, - statusChangeThreshold: 200, + look_back_window: 20, + status_change_threshold: 200, }) .expect(400); @@ -102,8 +102,8 @@ export default function updateFlappingSettingsTest({ getService }: FtrProviderCo .auth(Superuser.username, Superuser.password) .send({ enabled: true, - lookBackWindow: 5, - statusChangeThreshold: 10, + look_back_window: 5, + status_change_threshold: 10, }) .expect(400); @@ -121,14 +121,14 @@ export default function updateFlappingSettingsTest({ getService }: FtrProviderCo .auth(Superuser.username, Superuser.password) .send({ enabled: false, - lookBackWindow: 20, - statusChangeThreshold: 20, + look_back_window: 20, + status_change_threshold: 20, }); expect(postResponse.statusCode).to.eql(200); expect(postResponse.body.enabled).to.eql(false); - expect(postResponse.body.lookBackWindow).to.eql(20); - expect(postResponse.body.statusChangeThreshold).to.eql(20); + expect(postResponse.body.look_back_window).to.eql(20); + expect(postResponse.body.status_change_threshold).to.eql(20); // Get the rules settings in space2 const getResponse = await supertestWithoutAuth @@ -137,8 +137,8 @@ export default function updateFlappingSettingsTest({ getService }: FtrProviderCo expect(getResponse.statusCode).to.eql(200); expect(getResponse.body.enabled).to.eql(true); - expect(getResponse.body.lookBackWindow).to.eql(20); - expect(getResponse.body.statusChangeThreshold).to.eql(4); + expect(getResponse.body.look_back_window).to.eql(20); + expect(getResponse.body.status_change_threshold).to.eql(4); }); }); }); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/event_log.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/event_log.ts index 5525631c2a534..6574c2164b06c 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/event_log.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/event_log.ts @@ -546,8 +546,8 @@ export default function eventLogTests({ getService }: FtrProviderContext) { .auth('superuser', 'superuser') .send({ enabled: true, - lookBackWindow: 3, - statusChangeThreshold: 2, + look_back_window: 3, + status_change_threshold: 2, }) .expect(200); const { body: createdAction } = await supertest @@ -630,8 +630,8 @@ export default function eventLogTests({ getService }: FtrProviderContext) { .auth('superuser', 'superuser') .send({ enabled: true, - lookBackWindow: 3, - statusChangeThreshold: 2, + look_back_window: 3, + status_change_threshold: 2, }) .expect(200); const { body: createdAction } = await supertest diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rules_settings.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rules_settings.ts index 6b4297f2dc153..56b6d08253ec4 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rules_settings.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rules_settings.ts @@ -47,8 +47,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { .set('kbn-xsrf', 'foo') .send({ enabled: true, - lookBackWindow: 10, - statusChangeThreshold: 10, + look_back_window: 10, + status_change_threshold: 10, }) .expect(200); }); From 6bd55dab203f4df8286cf3dc50bb1ee4a79419d9 Mon Sep 17 00:00:00 2001 From: Ying Mao Date: Mon, 13 Feb 2023 16:09:23 -0500 Subject: [PATCH 20/20] [Response Ops][Alerting] Adding ability for rule types to specify custom formatting for `getSummarizedAlerts` function (#150829) Resolves https://github.com/elastic/kibana/issues/150776 ## Summary As part of the [POC to onboard detection rules onto alert summaries](https://github.com/elastic/kibana/pull/147539/files), we uncovered a need to allow rule types to specify a custom format function for the alerts returned from the `getSummarizedAlerts` function. This will allow detection rules to perform some custom transformations before detection alerts are made available for notifications. This PR adds the necessary hook that can be used later on. --- .../create_get_summarized_alerts_fn.test.ts | 341 +++++++++++++++++- .../utils/create_get_summarized_alerts_fn.ts | 70 ++-- 2 files changed, 386 insertions(+), 25 deletions(-) diff --git a/x-pack/plugins/rule_registry/server/utils/create_get_summarized_alerts_fn.test.ts b/x-pack/plugins/rule_registry/server/utils/create_get_summarized_alerts_fn.test.ts index 387bd0174dbc4..597d6f056fa15 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_get_summarized_alerts_fn.test.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_get_summarized_alerts_fn.test.ts @@ -9,6 +9,7 @@ import { RuleDataClientMock, } from '../rule_data_client/rule_data_client.mock'; import { + ALERT_ACTION_GROUP, ALERT_END, ALERT_INSTANCE_ID, ALERT_RULE_EXECUTION_UUID, @@ -18,7 +19,7 @@ import { EVENT_ACTION, TIMESTAMP, } from '../../common/technical_rule_data_field_names'; -import { createGetSummarizedAlertsFn } from './create_get_summarized_alerts_fn'; +import { AlertDocument, createGetSummarizedAlertsFn } from './create_get_summarized_alerts_fn'; describe('createGetSummarizedAlertsFn', () => { let ruleDataClientMock: RuleDataClientMock; @@ -1644,6 +1645,344 @@ describe('createGetSummarizedAlertsFn', () => { expect(summarizedAlerts.recovered.data).toEqual([]); }); + it('creates function that uses a custom format alerts function if defined', async () => { + ruleDataClientMock.getReader().search.mockResolvedValueOnce({ + hits: { + total: { + value: 6, + }, + hits: [ + { + _id: '1', + _index: '.alerts-default-000001', + _source: { + [TIMESTAMP]: '2020-01-01T12:00:00.000Z', + [ALERT_RULE_EXECUTION_UUID]: 'abc', + [ALERT_RULE_UUID]: 'rule-id', + [ALERT_INSTANCE_ID]: 'TEST_ALERT_3', + [ALERT_UUID]: 'uuid1', + kibana: { + alert: { + instance: { + id: 'TEST_ALERT_3', + }, + rule: { + execution: { + uuid: 'abc', + }, + }, + uuid: 'uuid1', + }, + }, + }, + }, + { + _id: '2', + _index: '.alerts-default-000001', + _source: { + [TIMESTAMP]: '2020-01-01T12:00:00.000Z', + [ALERT_RULE_EXECUTION_UUID]: 'abc', + [ALERT_RULE_UUID]: 'rule-id', + [ALERT_INSTANCE_ID]: 'TEST_ALERT_4', + [ALERT_UUID]: 'uuid2', + kibana: { + alert: { + instance: { + id: 'TEST_ALERT_4', + }, + rule: { + execution: { + uuid: 'abc', + }, + }, + uuid: 'uuid2', + }, + }, + }, + }, + { + _id: '3', + _index: '.alerts-default-000001', + _source: { + [TIMESTAMP]: '2020-01-01T12:10:00.000Z', + [ALERT_RULE_EXECUTION_UUID]: 'abc', + [ALERT_RULE_UUID]: 'rule-id', + [ALERT_INSTANCE_ID]: 'TEST_ALERT_1', + [ALERT_UUID]: 'uuid3', + kibana: { + alert: { + instance: { + id: 'TEST_ALERT_1', + }, + rule: { + execution: { + uuid: 'abc', + }, + }, + uuid: 'uuid3', + }, + }, + }, + }, + { + _id: '4', + _index: '.alerts-default-000001', + _source: { + [TIMESTAMP]: '2020-01-01T12:20:00.000Z', + [ALERT_RULE_EXECUTION_UUID]: 'abc', + [ALERT_RULE_UUID]: 'rule-id', + [ALERT_INSTANCE_ID]: 'TEST_ALERT_2', + [ALERT_UUID]: 'uuid4', + kibana: { + alert: { + instance: { + id: 'TEST_ALERT_2', + }, + rule: { + execution: { + uuid: 'abc', + }, + }, + uuid: 'uuid4', + }, + }, + }, + }, + { + _id: '5', + _index: '.alerts-default-000001', + _source: { + [TIMESTAMP]: '2020-01-01T12:00:00.000Z', + [ALERT_RULE_EXECUTION_UUID]: 'abc', + [ALERT_RULE_UUID]: 'rule-id', + [ALERT_INSTANCE_ID]: 'TEST_ALERT_5', + [ALERT_UUID]: 'uuid5', + kibana: { + alert: { + instance: { + id: 'TEST_ALERT_5', + }, + rule: { + execution: { + uuid: 'abc', + }, + }, + uuid: 'uuid5', + }, + }, + }, + }, + { + _id: '6', + _index: '.alerts-default-000001', + _source: { + [TIMESTAMP]: '2020-01-01T12:20:00.000Z', + [ALERT_RULE_EXECUTION_UUID]: 'abc', + [ALERT_RULE_UUID]: 'rule-id', + [ALERT_INSTANCE_ID]: 'TEST_ALERT_9', + [ALERT_UUID]: 'uuid6', + kibana: { + alert: { + instance: { + id: 'TEST_ALERT_9', + }, + rule: { + execution: { + uuid: 'abc', + }, + }, + uuid: 'uuid6', + }, + }, + }, + }, + ], + }, + } as any); + const getSummarizedAlertsFn = createGetSummarizedAlertsFn({ + ruleDataClient: ruleDataClientMock, + useNamespace: true, + isLifecycleAlert: false, + formatAlert: (alert: AlertDocument) => { + return { + ...alert, + [ALERT_ACTION_GROUP]: 'boopboopdedoo', + }; + }, + })(); + + const summarizedAlerts = await getSummarizedAlertsFn({ + start: new Date('2020-01-01T11:00:00.000Z'), + end: new Date('2020-01-01T12:25:00.000Z'), + ruleId: 'rule-id', + spaceId: 'space-id', + excludedAlertInstanceIds: ['TEST_ALERT_10'], + }); + expect(ruleDataClientMock.getReader).toHaveBeenCalledWith({ namespace: 'space-id' }); + expect(ruleDataClientMock.getReader().search).toHaveBeenCalledTimes(1); + expect(ruleDataClientMock.getReader().search).toHaveBeenCalledWith({ + body: { + size: 100, + track_total_hits: true, + query: { + bool: { + filter: [ + { + range: { + [TIMESTAMP]: { + gte: '2020-01-01T11:00:00.000Z', + lt: '2020-01-01T12:25:00.000Z', + }, + }, + }, + { + term: { + [ALERT_RULE_UUID]: 'rule-id', + }, + }, + { + bool: { + must_not: { + terms: { + [ALERT_INSTANCE_ID]: ['TEST_ALERT_10'], + }, + }, + }, + }, + ], + }, + }, + }, + }); + expect(summarizedAlerts.new.count).toEqual(6); + expect(summarizedAlerts.ongoing.count).toEqual(0); + expect(summarizedAlerts.recovered.count).toEqual(0); + expect(summarizedAlerts.new.data).toEqual([ + { + _id: '1', + _index: '.alerts-default-000001', + [TIMESTAMP]: '2020-01-01T12:00:00.000Z', + kibana: { + alert: { + action_group: 'boopboopdedoo', + instance: { + id: 'TEST_ALERT_3', + }, + rule: { + execution: { + uuid: 'abc', + }, + uuid: 'rule-id', + }, + uuid: 'uuid1', + }, + }, + }, + { + _id: '2', + _index: '.alerts-default-000001', + [TIMESTAMP]: '2020-01-01T12:00:00.000Z', + kibana: { + alert: { + action_group: 'boopboopdedoo', + instance: { + id: 'TEST_ALERT_4', + }, + rule: { + execution: { + uuid: 'abc', + }, + uuid: 'rule-id', + }, + uuid: 'uuid2', + }, + }, + }, + { + _id: '3', + _index: '.alerts-default-000001', + [TIMESTAMP]: '2020-01-01T12:10:00.000Z', + kibana: { + alert: { + action_group: 'boopboopdedoo', + instance: { + id: 'TEST_ALERT_1', + }, + rule: { + execution: { + uuid: 'abc', + }, + uuid: 'rule-id', + }, + uuid: 'uuid3', + }, + }, + }, + { + _id: '4', + _index: '.alerts-default-000001', + [TIMESTAMP]: '2020-01-01T12:20:00.000Z', + kibana: { + alert: { + action_group: 'boopboopdedoo', + instance: { + id: 'TEST_ALERT_2', + }, + rule: { + execution: { + uuid: 'abc', + }, + uuid: 'rule-id', + }, + uuid: 'uuid4', + }, + }, + }, + { + _id: '5', + _index: '.alerts-default-000001', + [TIMESTAMP]: '2020-01-01T12:00:00.000Z', + kibana: { + alert: { + action_group: 'boopboopdedoo', + instance: { + id: 'TEST_ALERT_5', + }, + rule: { + execution: { + uuid: 'abc', + }, + uuid: 'rule-id', + }, + uuid: 'uuid5', + }, + }, + }, + { + _id: '6', + _index: '.alerts-default-000001', + [TIMESTAMP]: '2020-01-01T12:20:00.000Z', + kibana: { + alert: { + action_group: 'boopboopdedoo', + instance: { + id: 'TEST_ALERT_9', + }, + rule: { + execution: { + uuid: 'abc', + }, + uuid: 'rule-id', + }, + uuid: 'uuid6', + }, + }, + }, + ]); + expect(summarizedAlerts.ongoing.data).toEqual([]); + expect(summarizedAlerts.recovered.data).toEqual([]); + }); + it('throws error if search throws error', async () => { ruleDataClientMock.getReader().search.mockImplementation(() => { throw new Error('search error'); diff --git a/x-pack/plugins/rule_registry/server/utils/create_get_summarized_alerts_fn.ts b/x-pack/plugins/rule_registry/server/utils/create_get_summarized_alerts_fn.ts index 4754d47f236e3..95c971230317e 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_get_summarized_alerts_fn.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_get_summarized_alerts_fn.ts @@ -27,12 +27,13 @@ import { ParsedExperimentalFields } from '../../common/parse_experimental_fields import { IRuleDataClient, IRuleDataReader } from '../rule_data_client'; const MAX_ALERT_DOCS_TO_RETURN = 100; -type AlertDocument = Partial; +export type AlertDocument = Partial; interface CreateGetSummarizedAlertsFnOpts { ruleDataClient: PublicContract; useNamespace: boolean; isLifecycleAlert: boolean; + formatAlert?: (alert: AlertDocument) => AlertDocument; } export const createGetSummarizedAlertsFn = @@ -73,6 +74,7 @@ export const createGetSummarizedAlertsFn = ruleId, executionUuid: executionUuid!, isLifecycleAlert: opts.isLifecycleAlert, + formatAlert: opts.formatAlert, excludedAlertInstanceIds, }); } @@ -83,6 +85,7 @@ export const createGetSummarizedAlertsFn = start: start!, end: end!, isLifecycleAlert: opts.isLifecycleAlert, + formatAlert: opts.formatAlert, excludedAlertInstanceIds, }); }; @@ -93,6 +96,7 @@ interface GetAlertsByExecutionUuidOpts { ruleDataClientReader: IRuleDataReader; isLifecycleAlert: boolean; excludedAlertInstanceIds: string[]; + formatAlert?: (alert: AlertDocument) => AlertDocument; } const getAlertsByExecutionUuid = async ({ @@ -101,12 +105,14 @@ const getAlertsByExecutionUuid = async ({ ruleDataClientReader, isLifecycleAlert, excludedAlertInstanceIds, + formatAlert, }: GetAlertsByExecutionUuidOpts) => { if (isLifecycleAlert) { return getLifecycleAlertsByExecutionUuid({ executionUuid, ruleId, ruleDataClientReader, + formatAlert, excludedAlertInstanceIds, }); } @@ -115,6 +121,7 @@ const getAlertsByExecutionUuid = async ({ executionUuid, ruleId, ruleDataClientReader, + formatAlert, excludedAlertInstanceIds, }); }; @@ -124,6 +131,7 @@ interface GetAlertsByExecutionUuidHelperOpts { ruleId: string; ruleDataClientReader: IRuleDataReader; excludedAlertInstanceIds: string[]; + formatAlert?: (alert: AlertDocument) => AlertDocument; } const getPersistentAlertsByExecutionUuid = async ({ @@ -131,17 +139,15 @@ const getPersistentAlertsByExecutionUuid = async { // persistent alerts only create new alerts so query by execution UUID to // get all alerts created during an execution const request = getQueryByExecutionUuid(executionUuid, ruleId, excludedAlertInstanceIds); - const response = (await ruleDataClientReader.search(request)) as ESSearchResponse< - AlertDocument, - TSearchRequest - >; + const response = await doSearch(ruleDataClientReader, request, formatAlert); return { - new: getHitsWithCount(response), + new: response, ongoing: { count: 0, data: [], @@ -158,6 +164,7 @@ const getLifecycleAlertsByExecutionUuid = async ({ ruleId, ruleDataClientReader, excludedAlertInstanceIds, + formatAlert, }: GetAlertsByExecutionUuidHelperOpts) => { // lifecycle alerts assign a different action to an alert depending // on whether it is new/ongoing/recovered. query for each action in order @@ -170,13 +177,13 @@ const getLifecycleAlertsByExecutionUuid = async ({ ]; const responses = await Promise.all( - requests.map((request) => ruleDataClientReader.search(request)) + requests.map((request) => doSearch(ruleDataClientReader, request, formatAlert)) ); return { - new: getHitsWithCount(responses[0]), - ongoing: getHitsWithCount(responses[1]), - recovered: getHitsWithCount(responses[2]), + new: responses[0], + ongoing: responses[1], + recovered: responses[2], }; }; @@ -197,24 +204,35 @@ const expandFlattenedAlert = (alert: object) => { }; const getHitsWithCount = ( - response: ESSearchResponse + response: ESSearchResponse, + formatAlert?: (alert: AlertDocument) => AlertDocument ) => { return { count: (response.hits.total as SearchTotalHits).value, data: response.hits.hits.map((hit) => { const { _id, _index, _source } = hit; - const rawAlert = { + const formattedSource = formatAlert ? formatAlert(_source) : _source; + + const expandedSource = expandFlattenedAlert(formattedSource as object); + return { _id, _index, - ..._source, + ...expandedSource, }; - - return expandFlattenedAlert(rawAlert as object); }), }; }; +const doSearch = async ( + ruleDataClientReader: IRuleDataReader, + request: ESSearchRequest, + formatAlert?: (alert: AlertDocument) => AlertDocument +) => { + const response = await ruleDataClientReader.search(request); + return getHitsWithCount(response, formatAlert); +}; + const getQueryByExecutionUuid = ( executionUuid: string, ruleId: string, @@ -272,6 +290,7 @@ interface GetAlertsByTimeRangeOpts { ruleDataClientReader: IRuleDataReader; isLifecycleAlert: boolean; excludedAlertInstanceIds: string[]; + formatAlert?: (alert: AlertDocument) => AlertDocument; } const getAlertsByTimeRange = async ({ @@ -281,6 +300,7 @@ const getAlertsByTimeRange = async ({ ruleDataClientReader, isLifecycleAlert, excludedAlertInstanceIds, + formatAlert, }: GetAlertsByTimeRangeOpts) => { if (isLifecycleAlert) { return getLifecycleAlertsByTimeRange({ @@ -288,6 +308,7 @@ const getAlertsByTimeRange = async ({ end, ruleId, ruleDataClientReader, + formatAlert, excludedAlertInstanceIds, }); } @@ -297,6 +318,7 @@ const getAlertsByTimeRange = async ({ end, ruleId, ruleDataClientReader, + formatAlert, excludedAlertInstanceIds, }); }; @@ -306,6 +328,7 @@ interface GetAlertsByTimeRangeHelperOpts { end: Date; ruleId: string; ruleDataClientReader: IRuleDataReader; + formatAlert?: (alert: AlertDocument) => AlertDocument; excludedAlertInstanceIds: string[]; } @@ -320,18 +343,16 @@ const getPersistentAlertsByTimeRange = async { // persistent alerts only create new alerts so query for all alerts within the time // range and treat them as NEW const request = getQueryByTimeRange(start, end, ruleId, excludedAlertInstanceIds); - const response = (await ruleDataClientReader.search(request)) as ESSearchResponse< - AlertDocument, - TSearchRequest - >; + const response = await doSearch(ruleDataClientReader, request, formatAlert); return { - new: getHitsWithCount(response), + new: response, ongoing: { count: 0, data: [], @@ -348,6 +369,7 @@ const getLifecycleAlertsByTimeRange = async ({ end, ruleId, ruleDataClientReader, + formatAlert, excludedAlertInstanceIds, }: GetAlertsByTimeRangeHelperOpts) => { const requests = [ @@ -357,13 +379,13 @@ const getLifecycleAlertsByTimeRange = async ({ ]; const responses = await Promise.all( - requests.map((request) => ruleDataClientReader.search(request)) + requests.map((request) => doSearch(ruleDataClientReader, request, formatAlert)) ); return { - new: getHitsWithCount(responses[0]), - ongoing: getHitsWithCount(responses[1]), - recovered: getHitsWithCount(responses[2]), + new: responses[0], + ongoing: responses[1], + recovered: responses[2], }; };