From 07290bfac955c7d62ba93b52d888499dd6006cf3 Mon Sep 17 00:00:00 2001 From: Robert Jaszczurek <92210485+rbrtj@users.noreply.github.com> Date: Fri, 27 Sep 2024 12:30:53 +0200 Subject: [PATCH 1/9] [ML] Single Metric Viewer: Enable cross-filtering for 'by', 'over' and 'partition' field values (#193255) ## Summary Enables cross-filtering for 'by', 'over' and 'partition' field values in the Single Metric Viewer. Fixes [#171932](https://github.com/elastic/kibana/issues/171932) Before: https://github.com/user-attachments/assets/9a279375-7d0b-4422-b9eb-644ae3c0d291 After: https://github.com/user-attachments/assets/d86d0688-dc69-43f0-aa24-130ff38935e6 --- x-pack/plugins/ml/common/types/storage.ts | 1 + .../entity_control/entity_control.tsx | 2 +- .../series_controls/series_controls.tsx | 58 ++++++---- .../get_partition_fields_values.ts | 17 ++- .../routes/schemas/results_service_schema.ts | 1 + .../ml/results/get_partition_fields_values.ts | 100 +++++++++++++++++- 6 files changed, 154 insertions(+), 25 deletions(-) diff --git a/x-pack/plugins/ml/common/types/storage.ts b/x-pack/plugins/ml/common/types/storage.ts index 7213fa134c1a5..145be087fcfdd 100644 --- a/x-pack/plugins/ml/common/types/storage.ts +++ b/x-pack/plugins/ml/common/types/storage.ts @@ -35,6 +35,7 @@ export type PartitionFieldConfig = by: 'anomaly_score' | 'name'; order: 'asc' | 'desc'; }; + value: string; } | undefined; diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/entity_control/entity_control.tsx b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/entity_control/entity_control.tsx index 04f8944376fe5..9a877d8c52fc7 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/entity_control/entity_control.tsx +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/entity_control/entity_control.tsx @@ -188,7 +188,7 @@ export class EntityControl extends Component; * Provides default fields configuration. */ const getDefaultFieldConfig = ( - fieldTypes: MlEntityFieldType[], + entities: Entity[], isAnomalousOnly: boolean, applyTimeRange: boolean ): UiPartitionFieldsConfig => { - return fieldTypes.reduce((acc, f) => { - acc[f] = { + return entities.reduce((acc, f) => { + acc[f.fieldType] = { applyTimeRange, anomalousOnly: isAnomalousOnly, sort: { by: 'anomaly_score', order: 'desc' }, + ...(f.fieldValue && { value: f.fieldValue }), }; return acc; }, {} as UiPartitionFieldsConfig); @@ -141,18 +142,28 @@ export const SeriesControls: FC> = ({ // Merge the default config with the one from the local storage const resultFieldsConfig = useMemo(() => { - return { - ...getDefaultFieldConfig( - entityControls.map((v) => v.fieldType), - !storageFieldsConfig - ? true - : Object.values(storageFieldsConfig).some((v) => !!v?.anomalousOnly), - !storageFieldsConfig - ? true - : Object.values(storageFieldsConfig).some((v) => !!v?.applyTimeRange) - ), - ...(!storageFieldsConfig ? {} : storageFieldsConfig), - }; + const resultFieldConfig = getDefaultFieldConfig( + entityControls, + !storageFieldsConfig + ? true + : Object.values(storageFieldsConfig).some((v) => !!v?.anomalousOnly), + !storageFieldsConfig + ? true + : Object.values(storageFieldsConfig).some((v) => !!v?.applyTimeRange) + ); + + // Early return to prevent unnecessary looping through the default config + if (!storageFieldsConfig) return resultFieldConfig; + + // Override only the fields properties stored in the local storage + for (const key of Object.keys(resultFieldConfig) as MlEntityFieldType[]) { + resultFieldConfig[key] = { + ...resultFieldConfig[key], + ...storageFieldsConfig[key], + } as UiPartitionFieldConfig; + } + + return resultFieldConfig; }, [entityControls, storageFieldsConfig]); /** @@ -286,9 +297,20 @@ export const SeriesControls: FC> = ({ } } + // Remove the value from the field config to avoid storing it in the local storage + const { value, ...updatedFieldConfigWithoutValue } = updatedFieldConfig; + + // Remove the value from the result config to avoid storing it in the local storage + const updatedResultConfigWithoutValues = Object.fromEntries( + Object.entries(updatedResultConfig).map(([key, fieldValue]) => { + const { value: _, ...rest } = fieldValue; + return [key, rest]; + }) + ); + setStorageFieldsConfig({ - ...updatedResultConfig, - [fieldType]: updatedFieldConfig, + ...updatedResultConfigWithoutValues, + [fieldType]: updatedFieldConfigWithoutValue, }); }, [resultFieldsConfig, setStorageFieldsConfig] diff --git a/x-pack/plugins/ml/server/models/results_service/get_partition_fields_values.ts b/x-pack/plugins/ml/server/models/results_service/get_partition_fields_values.ts index c709c2754953e..5e7d01b4bf9fb 100644 --- a/x-pack/plugins/ml/server/models/results_service/get_partition_fields_values.ts +++ b/x-pack/plugins/ml/server/models/results_service/get_partition_fields_values.ts @@ -35,16 +35,24 @@ function getFieldAgg( fieldType: MlPartitionFieldsType, isModelPlotSearch: boolean, query?: string, - fieldConfig?: FieldConfig + fieldsConfig?: FieldsConfig ) { const AGG_SIZE = 100; + const fieldConfig = fieldsConfig?.[fieldType]; const fieldNameKey = `${fieldType}_name`; const fieldValueKey = `${fieldType}_value`; const sortByField = fieldConfig?.sort?.by === 'name' || isModelPlotSearch ? '_key' : 'maxRecordScore'; + const splitFieldFilterValues = Object.entries(fieldsConfig ?? {}) + .filter(([key, field]) => key !== fieldType && field.value) + .map(([key, field]) => ({ + fieldValueKey: `${key}_value`, + fieldValue: field.value, + })); + return { [fieldNameKey]: { terms: { @@ -77,6 +85,11 @@ function getFieldAgg( }, ] : []), + ...splitFieldFilterValues.map((filterValue) => ({ + term: { + [filterValue.fieldValueKey]: filterValue.fieldValue, + }, + })), ], }, }, @@ -233,7 +246,7 @@ export const getPartitionFieldsValuesFactory = (mlClient: MlClient) => ...ML_PARTITION_FIELDS.reduce((acc, key) => { return Object.assign( acc, - getFieldAgg(key, isModelPlotSearch, searchTerm[key], fieldsConfig[key]) + getFieldAgg(key, isModelPlotSearch, searchTerm[key], fieldsConfig) ); }, {}), }, diff --git a/x-pack/plugins/ml/server/routes/schemas/results_service_schema.ts b/x-pack/plugins/ml/server/routes/schemas/results_service_schema.ts index 5b3d268c2fffd..43a3516b9d6c6 100644 --- a/x-pack/plugins/ml/server/routes/schemas/results_service_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/results_service_schema.ts @@ -61,6 +61,7 @@ const fieldConfig = schema.maybe( by: schema.string(), order: schema.maybe(schema.string()), }), + value: schema.maybe(schema.string()), }) ); diff --git a/x-pack/test/api_integration/apis/ml/results/get_partition_fields_values.ts b/x-pack/test/api_integration/apis/ml/results/get_partition_fields_values.ts index cdb6b1df28c9e..b1124bc5b4f44 100644 --- a/x-pack/test/api_integration/apis/ml/results/get_partition_fields_values.ts +++ b/x-pack/test/api_integration/apis/ml/results/get_partition_fields_values.ts @@ -38,10 +38,38 @@ export default ({ getService }: FtrProviderContext) => { } as Job; } - function getDatafeedConfig(jobId: string) { + function getJobConfigWithByField(jobId: string) { + return { + job_id: jobId, + description: + 'count by geoip.city_name partition=day_of_week on ecommerce dataset with 1h bucket span', + analysis_config: { + bucket_span: '1h', + influencers: ['geoip.city_name', 'day_of_week'], + detectors: [ + { + function: 'count', + by_field_name: 'geoip.city_name', + partition_field_name: 'day_of_week', + }, + ], + }, + data_description: { + time_field: 'order_date', + time_format: 'epoch_ms', + }, + analysis_limits: { + model_memory_limit: '11mb', + categorization_examples_limit: 4, + }, + model_plot_config: { enabled: false }, + } as Job; + } + + function getDatafeedConfig(jobId: string, indices: string[]) { return { datafeed_id: `datafeed-${jobId}`, - indices: ['ft_farequote'], + indices, job_id: jobId, query: { bool: { must: [{ match_all: {} }] } }, } as Datafeed; @@ -50,12 +78,17 @@ export default ({ getService }: FtrProviderContext) => { async function createMockJobs() { await ml.api.createAndRunAnomalyDetectionLookbackJob( getJobConfig('fq_multi_1_ae'), - getDatafeedConfig('fq_multi_1_ae') + getDatafeedConfig('fq_multi_1_ae', ['ft_farequote']) ); await ml.api.createAndRunAnomalyDetectionLookbackJob( getJobConfig('fq_multi_2_ae', false), - getDatafeedConfig('fq_multi_2_ae') + getDatafeedConfig('fq_multi_2_ae', ['ft_farequote']) + ); + + await ml.api.createAndRunAnomalyDetectionLookbackJob( + getJobConfigWithByField('ecommerce_advanced_1'), + getDatafeedConfig('ecommerce_advanced_1', ['ft_ecommerce']) ); } @@ -72,6 +105,7 @@ export default ({ getService }: FtrProviderContext) => { describe('PartitionFieldsValues', function () { before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/ecommerce'); await ml.testResources.setKibanaTimeZoneToUTC(); await createMockJobs(); }); @@ -229,5 +263,63 @@ export default ({ getService }: FtrProviderContext) => { expect(body.partition_field.values.length).to.eql(19); }); }); + + describe('cross filtering', () => { + it('should return filtered values for by_field when partition_field is set', async () => { + const requestBody = { + jobId: 'ecommerce_advanced_1', + criteriaFields: [{ fieldName: 'detector_index', fieldValue: 0 }], + earliestMs: 1687968000000, // June 28, 2023 16:00:00 GMT + latestMs: 1688140800000, // June 30, 2023 16:00:00 GMT + searchTerm: {}, + fieldsConfig: { + by_field: { + anomalousOnly: true, + applyTimeRange: true, + sort: { order: 'desc', by: 'anomaly_score' }, + }, + partition_field: { + anomalousOnly: true, + applyTimeRange: true, + sort: { order: 'desc', by: 'anomaly_score' }, + value: 'Saturday', + }, + }, + }; + const body = await runRequest(requestBody); + + expect(body.by_field.values.length).to.eql(1); + expect(body.by_field.values[0].value).to.eql('Abu Dhabi'); + }); + + it('should return filtered values for partition_field when by_field is set', async () => { + const requestBody = { + jobId: 'ecommerce_advanced_1', + criteriaFields: [{ fieldName: 'detector_index', fieldValue: 0 }], + earliestMs: 1687968000000, // June 28, 2023 16:00:00 GMT + latestMs: 1688140800000, // June 30, 2023 16:00:00 GMT + searchTerm: {}, + fieldsConfig: { + by_field: { + anomalousOnly: true, + applyTimeRange: true, + sort: { order: 'desc', by: 'anomaly_score' }, + value: 'Abu Dhabi', + }, + partition_field: { + anomalousOnly: true, + applyTimeRange: true, + sort: { order: 'desc', by: 'anomaly_score' }, + }, + }, + }; + + const body = await runRequest(requestBody); + + expect(body.partition_field.values.length).to.eql(2); + expect(body.partition_field.values[0].value).to.eql('Saturday'); + expect(body.partition_field.values[1].value).to.eql('Monday'); + }); + }); }); }; From f4ca9e4730ee6b13d19808be9fcc633dce9087d5 Mon Sep 17 00:00:00 2001 From: Sid Date: Fri, 27 Sep 2024 12:59:28 +0200 Subject: [PATCH 2/9] [Roles] Fix bug with roles grid not sorting on clicking table header (#194196) Fixes https://github.com/elastic/kibana/issues/193786 ## Summary Reverts a few changes made when the Roles grid page was moved to a functional component. Fixes regression in table sorting. ### Notes When preparing for the Query Roles API, we had moved the roles grid page to be a functional component. In doing so, we also migrated away from the In Memory table in favor of the basic table. EUIBasicTable does not support sorting out of the box and is meant to be used for server-side sorting, etc (unless we implement custom sorting logic). I've made a few changes: - Bring back the InMemoryTable but keep the Search Bar. - Remove few (now) unused functions which are to be brought back whenever the Query Roles API is ready. - Update tests ### Screen recording https://github.com/user-attachments/assets/4ac4f771-e7d1-4e17-807e-d6262767d100 ### Release notes Fixes UI regression in Roles listing page where users could not sort table by using the headers. --------- Co-authored-by: Elastic Machine --- .../roles/roles_grid/roles_grid_page.test.tsx | 115 +++++++++++++++++- .../roles/roles_grid/roles_grid_page.tsx | 52 ++------ 2 files changed, 120 insertions(+), 47 deletions(-) diff --git a/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.test.tsx b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.test.tsx index 8951fd3e5c202..57281f5ec754c 100644 --- a/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.test.tsx +++ b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiBasicTable, EuiIcon } from '@elastic/eui'; +import { EuiIcon, EuiInMemoryTable } from '@elastic/eui'; import type { ReactWrapper } from 'enzyme'; import React from 'react'; @@ -209,7 +209,7 @@ describe('', () => { return updatedWrapper.find(EuiIcon).length > initialIconCount; }); - expect(wrapper.find(EuiBasicTable).props().items).toEqual([ + expect(wrapper.find(EuiInMemoryTable).props().items).toEqual([ { name: 'test-role-1', elasticsearch: { @@ -296,7 +296,7 @@ describe('', () => { findTestSubject(wrapper, 'showReservedRolesSwitch').simulate('click'); - expect(wrapper.find(EuiBasicTable).props().items).toEqual([ + expect(wrapper.find(EuiInMemoryTable).props().items).toEqual([ { name: 'test-role-1', elasticsearch: { cluster: [], indices: [], run_as: [] }, @@ -322,6 +322,115 @@ describe('', () => { ]); }); + it('sorts columns on clicking the column header', async () => { + const wrapper = mountWithIntl( + + ); + const initialIconCount = wrapper.find(EuiIcon).length; + + await waitForRender(wrapper, (updatedWrapper) => { + return updatedWrapper.find(EuiIcon).length > initialIconCount; + }); + + expect(wrapper.find(EuiInMemoryTable).props().items).toEqual([ + { + name: 'test-role-1', + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [ + { + base: [], + spaces: [], + feature: {}, + }, + ], + }, + { + name: 'test-role-with-description', + description: 'role-description', + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [ + { + base: [], + spaces: [], + feature: {}, + }, + ], + }, + { + name: 'reserved-role', + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [ + { + base: [], + spaces: [], + feature: {}, + }, + ], + metadata: { + _reserved: true, + }, + }, + { + name: 'disabled-role', + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [ + { + base: [], + spaces: [], + feature: {}, + }, + ], + transient_metadata: { + enabled: false, + }, + }, + { + name: 'special%chars%role', + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [ + { + base: [], + spaces: [], + feature: {}, + }, + ], + }, + ]); + + findTestSubject(wrapper, 'tableHeaderCell_name_0').simulate('click'); + + const firstRowElement = findTestSubject(wrapper, 'roleRowName').first(); + expect(firstRowElement.text()).toBe('disabled-role'); + }); + it('hides controls when `readOnly` is enabled', async () => { const wrapper = mountWithIntl( { return `/${action}${roleName ? `/${encodeURIComponent(roleName)}` : ''}`; }; @@ -79,17 +68,6 @@ const getVisibleRoles = (roles: Role[], filter: string, includeReservedRoles: bo }); }; -const DEFAULT_TABLE_STATE = { - query: EuiSearchBar.Query.MATCH_ALL, - sort: { - field: 'creation' as const, - direction: 'desc' as const, - }, - from: 0, - size: 25, - filters: {}, -}; - export const RolesGridPage: FC = ({ notifications, rolesAPIClient, @@ -109,7 +87,6 @@ export const RolesGridPage: FC = ({ const [permissionDenied, setPermissionDenied] = useState(false); const [includeReservedRoles, setIncludeReservedRoles] = useState(true); const [isLoading, setIsLoading] = useState(false); - const [tableState, setTableState] = useState(DEFAULT_TABLE_STATE); useEffect(() => { loadRoles(); @@ -235,15 +212,6 @@ export const RolesGridPage: FC = ({ } }; - const onTableChange = ({ page, sort }: CriteriaWithPagination) => { - const newState = { - ...tableState, - from: page?.index! * page?.size!, - size: page?.size!, - }; - setTableState(newState); - }; - const getColumnConfig = (): Array> => { const config: Array> = [ { @@ -365,12 +333,6 @@ export const RolesGridPage: FC = ({ setShowDeleteConfirmation(false); }; - const pagination = { - pageIndex: tableState.from / tableState.size, - pageSize: tableState.size, - totalItemCount: visibleRoles.length, - pageSizeOptions: [25, 50, 100], - }; return permissionDenied ? ( ) : ( @@ -466,7 +428,7 @@ export const RolesGridPage: FC = ({ toolsRight={renderToolsRight()} /> - = ({ selected: selection, } } - onChange={onTableChange} - pagination={pagination} - noItemsMessage={ + pagination={{ + initialPageSize: 20, + pageSizeOptions: [10, 20, 30, 50, 100], + }} + message={ buildFlavor === 'serverless' ? ( Date: Fri, 27 Sep 2024 13:04:20 +0200 Subject: [PATCH 3/9] [SLOs] Slo form little things (#193990) ## Summary Slo form little things !! Will auto add values for APM defaults to all instead of forcing user !! ### Changes Details | Before | After -- | -- | -- Data View component | image | image APM SLI | image | image --- .../dataview_picker/change_dataview.tsx | 38 ++--- .../apm_availability_indicator_type_form.tsx | 11 +- .../apm_common/use_apm_default_values.ts | 46 +++++++ .../apm_latency_indicator_type_form.tsx | 130 ++++++++---------- .../common/index_field_selector.tsx | 78 +++++------ .../index_and_timestamp_field.tsx | 51 +++++++ .../custom_common/index_selection.tsx | 6 +- .../custom_kql_indicator_type_form.tsx | 25 +--- .../custom_metric/custom_metric_type_form.tsx | 26 +--- .../histogram_indicator_type_form.tsx | 26 +--- .../slo_edit/components/slo_edit_form.tsx | 2 +- ...etics_availability_indicator_type_form.tsx | 18 ++- .../timeslice_metric_indicator.tsx | 26 +--- .../public/pages/slo_edit/slo_edit.test.tsx | 4 +- 14 files changed, 254 insertions(+), 233 deletions(-) create mode 100644 x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/apm_common/use_apm_default_values.ts create mode 100644 x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/custom_common/index_and_timestamp_field.tsx diff --git a/src/plugins/unified_search/public/dataview_picker/change_dataview.tsx b/src/plugins/unified_search/public/dataview_picker/change_dataview.tsx index f952c962032ed..ade754004b8ef 100644 --- a/src/plugins/unified_search/public/dataview_picker/change_dataview.tsx +++ b/src/plugins/unified_search/public/dataview_picker/change_dataview.tsx @@ -238,7 +238,7 @@ export function ChangeDataView({ return ( <> - + - setPopoverIsOpen(false)} - panelPaddingSize="none" - initialFocus={`[id="${searchListInputId}"]`} - display="block" - buffer={8} - > -
- -
-
+ + setPopoverIsOpen(false)} + panelPaddingSize="none" + initialFocus={`[id="${searchListInputId}"]`} + display="block" + buffer={8} + > +
+ +
+
+
diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/apm_availability/apm_availability_indicator_type_form.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/apm_availability/apm_availability_indicator_type_form.tsx index 7424db8a448e6..0dcddcdb232b5 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/apm_availability/apm_availability_indicator_type_form.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/apm_availability/apm_availability_indicator_type_form.tsx @@ -8,8 +8,9 @@ import { EuiFlexGroup, EuiFlexItem, EuiIconTip } from '@elastic/eui'; import { APMTransactionErrorRateIndicator } from '@kbn/slo-schema'; import { i18n } from '@kbn/i18n'; -import React, { useEffect } from 'react'; +import React from 'react'; import { useFormContext } from 'react-hook-form'; +import { useApmDefaultValues } from '../apm_common/use_apm_default_values'; import { DATA_VIEW_FIELD } from '../custom_common/index_selection'; import { useCreateDataView } from '../../../../hooks/use_create_data_view'; import { GroupByField } from '../common/group_by_field'; @@ -22,7 +23,7 @@ import { formatAllFilters } from '../../helpers/format_filters'; import { getGroupByCardinalityFilters } from '../apm_common/get_group_by_cardinality_filters'; export function ApmAvailabilityIndicatorTypeForm() { - const { watch, setValue } = useFormContext>(); + const { watch } = useFormContext>(); const { data: apmIndex } = useFetchApmIndex(); const dataViewId = watch(DATA_VIEW_FIELD); @@ -47,11 +48,7 @@ export function ApmAvailabilityIndicatorTypeForm() { }); const allFilters = formatAllFilters(globalFilters, indicatorParamsFilters); - useEffect(() => { - if (apmIndex !== '') { - setValue('indicator.params.index', apmIndex); - } - }, [setValue, apmIndex]); + useApmDefaultValues(); const { dataView, loading: isIndexFieldsLoading } = useCreateDataView({ indexPatternString: apmIndex, diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/apm_common/use_apm_default_values.ts b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/apm_common/use_apm_default_values.ts new file mode 100644 index 0000000000000..d61578bed8594 --- /dev/null +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/apm_common/use_apm_default_values.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useFormContext } from 'react-hook-form'; +import { ALL_VALUE, APMTransactionErrorRateIndicator } from '@kbn/slo-schema'; +import { useEffect } from 'react'; +import { useFetchApmIndex } from '../../../../hooks/use_fetch_apm_indices'; +import { CreateSLOForm } from '../../types'; + +export const useApmDefaultValues = () => { + const { watch, setValue } = useFormContext>(); + const { data: apmIndex } = useFetchApmIndex(); + + const [serviceName = '', environment = '', transactionType = '', transactionName = ''] = watch([ + 'indicator.params.service', + 'indicator.params.environment', + 'indicator.params.transactionType', + 'indicator.params.transactionName', + ]); + + useEffect(() => { + if (apmIndex !== '') { + setValue('indicator.params.index', apmIndex); + } + }, [setValue, apmIndex]); + + useEffect(() => { + if (serviceName) { + if (!environment) { + setValue('indicator.params.environment', ALL_VALUE); + } + + if (!transactionType) { + setValue('indicator.params.transactionType', ALL_VALUE); + } + + if (!transactionName) { + setValue('indicator.params.transactionName', ALL_VALUE); + } + } + }, [environment, serviceName, setValue, transactionName, transactionType]); +}; diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/apm_latency/apm_latency_indicator_type_form.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/apm_latency/apm_latency_indicator_type_form.tsx index fc2cf6f643c62..03b47aafe4150 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/apm_latency/apm_latency_indicator_type_form.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/apm_latency/apm_latency_indicator_type_form.tsx @@ -5,11 +5,12 @@ * 2.0. */ -import { EuiFieldNumber, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiIconTip } from '@elastic/eui'; +import { EuiFieldNumber, EuiFlexGroup, EuiFormRow, EuiIconTip } from '@elastic/eui'; import { APMTransactionDurationIndicator } from '@kbn/slo-schema'; import { i18n } from '@kbn/i18n'; -import React, { useEffect } from 'react'; +import React from 'react'; import { Controller, useFormContext } from 'react-hook-form'; +import { useApmDefaultValues } from '../apm_common/use_apm_default_values'; import { DATA_VIEW_FIELD } from '../custom_common/index_selection'; import { GroupByField } from '../common/group_by_field'; import { useCreateDataView } from '../../../../hooks/use_create_data_view'; @@ -22,7 +23,7 @@ import { formatAllFilters } from '../../helpers/format_filters'; import { getGroupByCardinalityFilters } from '../apm_common/get_group_by_cardinality_filters'; export function ApmLatencyIndicatorTypeForm() { - const { control, watch, getFieldState, setValue } = + const { control, watch, getFieldState } = useFormContext>(); const { data: apmIndex } = useFetchApmIndex(); @@ -47,11 +48,7 @@ export function ApmLatencyIndicatorTypeForm() { }); const allFilters = formatAllFilters(globalFilters, indicatorParamsFilters); - useEffect(() => { - if (apmIndex !== '') { - setValue('indicator.params.index', apmIndex); - } - }, [setValue, apmIndex]); + useApmDefaultValues(); const dataViewId = watch(DATA_VIEW_FIELD); @@ -124,70 +121,65 @@ export function ApmLatencyIndicatorTypeForm() { />
- - - - {i18n.translate('xpack.slo.sloEdit.apmLatency.threshold.placeholder', { - defaultMessage: 'Threshold (ms)', - })}{' '} - - - } - isInvalid={getFieldState('indicator.params.threshold').invalid} - > - ( - field.onChange(Number(event.target.value))} - /> - )} + + {i18n.translate('xpack.slo.sloEdit.apmLatency.threshold.placeholder', { + defaultMessage: 'Threshold (ms)', + })}{' '} + - - - - + } + isInvalid={getFieldState('indicator.params.threshold').invalid} + > + ( + field.onChange(Number(event.target.value))} + /> + )} + /> + + + - } + position="top" /> - - + } + /> diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/common/index_field_selector.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/common/index_field_selector.tsx index d744968d3bab8..0a277900ac31f 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/common/index_field_selector.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/common/index_field_selector.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiComboBox, EuiComboBoxOptionOption, EuiFlexItem, EuiFormRow } from '@elastic/eui'; +import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow } from '@elastic/eui'; import React, { useEffect, useState, ReactNode } from 'react'; import { Controller, useFormContext } from 'react-hook-form'; import { FieldSpec } from '@kbn/data-views-plugin/common'; @@ -53,45 +53,43 @@ export function IndexFieldSelector({ }; return ( - - - { - return ( - - {...field} - async - placeholder={placeholder} - aria-label={placeholder} - isClearable - isDisabled={isLoading || isDisabled} - isInvalid={fieldState.invalid} - isLoading={isLoading} - onChange={(selected: EuiComboBoxOptionOption[]) => { - if (selected.length) { - return field.onChange(selected.map((selection) => selection.value)); - } - - field.onChange(defaultValue); - }} - options={options} - onSearchChange={(searchValue: string) => { - setOptions( - createOptionsFromFields(indexFields, ({ value }) => value.includes(searchValue)) - ); - }} - selectedOptions={ - !!indexFields && !!field.value ? getSelectedItems(field.value, indexFields) : [] + + { + return ( + + {...field} + async + placeholder={placeholder} + aria-label={placeholder} + isClearable + isDisabled={isLoading || isDisabled} + isInvalid={fieldState.invalid} + isLoading={isLoading} + onChange={(selected: EuiComboBoxOptionOption[]) => { + if (selected.length) { + return field.onChange(selected.map((selection) => selection.value)); } - /> - ); - }} - /> - - + + field.onChange(defaultValue); + }} + options={options} + onSearchChange={(searchValue: string) => { + setOptions( + createOptionsFromFields(indexFields, ({ value }) => value.includes(searchValue)) + ); + }} + selectedOptions={ + !!indexFields && !!field.value ? getSelectedItems(field.value, indexFields) : [] + } + /> + ); + }} + /> + ); } diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/custom_common/index_and_timestamp_field.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/custom_common/index_and_timestamp_field.tsx new file mode 100644 index 0000000000000..b4b5bdd4557d4 --- /dev/null +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/custom_common/index_and_timestamp_field.tsx @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { DataView } from '@kbn/data-views-plugin/public'; +import { useFormContext } from 'react-hook-form'; +import { IndexSelection } from './index_selection'; +import { IndexFieldSelector } from '../common/index_field_selector'; +import { CreateSLOForm } from '../../types'; + +export function IndexAndTimestampField({ + dataView, + isLoading, +}: { + dataView?: DataView; + isLoading: boolean; +}) { + const { watch } = useFormContext(); + const index = watch('indicator.params.index'); + + const timestampFields = dataView?.fields?.filter((field) => field.type === 'date') ?? []; + + return ( + + + + + + + + + ); +} diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/custom_common/index_selection.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/custom_common/index_selection.tsx index 526c955f2c6d8..1a33685a6f019 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/custom_common/index_selection.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/custom_common/index_selection.tsx @@ -53,7 +53,7 @@ export function IndexSelection({ selectedDataView }: { selectedDataView?: DataVi ]); return ( - + { setValue( diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/custom_kql/custom_kql_indicator_type_form.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/custom_kql/custom_kql_indicator_type_form.tsx index 43f0648084086..92ba2cac50e7f 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/custom_kql/custom_kql_indicator_type_form.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/custom_kql/custom_kql_indicator_type_form.tsx @@ -9,13 +9,13 @@ import { EuiFlexGroup, EuiFlexItem, EuiIconTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { useFormContext } from 'react-hook-form'; +import { IndexAndTimestampField } from '../custom_common/index_and_timestamp_field'; import { GroupByField } from '../common/group_by_field'; import { useCreateDataView } from '../../../../hooks/use_create_data_view'; import { CreateSLOForm } from '../../types'; import { DataPreviewChart } from '../common/data_preview_chart'; -import { IndexFieldSelector } from '../common/index_field_selector'; import { QueryBuilder } from '../common/query_builder'; -import { DATA_VIEW_FIELD, IndexSelection } from '../custom_common/index_selection'; +import { DATA_VIEW_FIELD } from '../custom_common/index_selection'; export function CustomKqlIndicatorTypeForm() { const { watch } = useFormContext(); @@ -26,29 +26,10 @@ export function CustomKqlIndicatorTypeForm() { indexPatternString: index, dataViewId, }); - const timestampFields = dataView?.fields?.filter((field) => field.type === 'date') ?? []; return ( - - - - - - - + field.type === 'date'); const metricFields = dataView?.fields.filter((field) => SUPPORTED_METRIC_FIELD_TYPES.includes(field.type) ); @@ -57,26 +56,7 @@ export function CustomMetricIndicatorTypeForm() { - - - - - - - - + field.type === 'histogram'); - const timestampFields = dataView?.fields.filter((field) => field.type === 'date'); return ( <> @@ -51,26 +50,7 @@ export function HistogramIndicatorTypeForm() { - - - - - - - - + void; } -export const maxWidth = 775; +export const maxWidth = 900; export function SloEditForm({ slo, initialValues, onSave }: Props) { const isEditMode = slo !== undefined; diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/synthetics_availability/synthetics_availability_indicator_type_form.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/synthetics_availability/synthetics_availability_indicator_type_form.tsx index 417b276cd1b1b..07f2f86663292 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/synthetics_availability/synthetics_availability_indicator_type_form.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/synthetics_availability/synthetics_availability_indicator_type_form.tsx @@ -14,7 +14,7 @@ import { SyntheticsAvailabilityIndicator, } from '@kbn/slo-schema'; import moment from 'moment'; -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { useFormContext } from 'react-hook-form'; import { DATA_VIEW_FIELD } from '../custom_common/index_selection'; import { useCreateDataView } from '../../../../hooks/use_create_data_view'; @@ -26,7 +26,8 @@ import { QueryBuilder } from '../common/query_builder'; import { FieldSelector } from '../synthetics_common/field_selector'; export function SyntheticsAvailabilityIndicatorTypeForm() { - const { watch } = useFormContext>(); + const { watch, setValue, getValues } = + useFormContext>(); const dataViewId = watch(DATA_VIEW_FIELD); const [monitorIds = [], projects = [], tags = [], index, globalFilters] = watch([ @@ -59,6 +60,19 @@ export function SyntheticsAvailabilityIndicatorTypeForm() { ); const allFilters = formatAllFilters(globalFilters, groupByCardinalityFilters); + const currentMonitors = getValues('indicator.params.monitorIds'); + + useEffect(() => { + if (!currentMonitors || !currentMonitors.length) { + setValue('indicator.params.monitorIds', [ + { + value: '*', + label: 'All', + }, + ]); + } + }, [currentMonitors, setValue]); + return ( diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/timeslice_metric/timeslice_metric_indicator.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/timeslice_metric/timeslice_metric_indicator.tsx index 82736e4e24a25..548f0bd0ab3e5 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/timeslice_metric/timeslice_metric_indicator.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/timeslice_metric/timeslice_metric_indicator.tsx @@ -18,13 +18,13 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import React from 'react'; import { useFormContext } from 'react-hook-form'; +import { IndexAndTimestampField } from '../custom_common/index_and_timestamp_field'; import { useKibana } from '../../../../utils/kibana_react'; import { GroupByField } from '../common/group_by_field'; import { CreateSLOForm } from '../../types'; import { DataPreviewChart } from '../common/data_preview_chart'; -import { IndexFieldSelector } from '../common/index_field_selector'; import { QueryBuilder } from '../common/query_builder'; -import { DATA_VIEW_FIELD, IndexSelection } from '../custom_common/index_selection'; +import { DATA_VIEW_FIELD } from '../custom_common/index_selection'; import { MetricIndicator } from './metric_indicator'; import { COMPARATOR_MAPPING } from '../../constants'; import { useCreateDataView } from '../../../../hooks/use_create_data_view'; @@ -41,7 +41,6 @@ export function TimesliceMetricIndicatorTypeForm() { dataViewId, }); - const timestampFields = dataView?.fields.filter((field) => field.type === 'date'); const { uiSettings } = useKibana().services; const threshold = watch('indicator.params.metric.threshold'); const comparator = watch('indicator.params.metric.comparator'); @@ -56,26 +55,7 @@ export function TimesliceMetricIndicatorTypeForm() { - - - - - - - - + { expect(queryByTestId('apmLatencyServiceSelector')).toHaveTextContent('cartService'); expect(queryByTestId('apmLatencyEnvironmentSelector')).toHaveTextContent('prod'); - expect(queryByTestId('sloEditFormObjectiveSection')).toBeFalsy(); - expect(queryByTestId('sloEditFormDescriptionSection')).toBeFalsy(); + expect(queryByTestId('sloEditFormObjectiveSection')).toBeTruthy(); + expect(queryByTestId('sloEditFormDescriptionSection')).toBeTruthy(); }); }); From c84cb0a783ebdf5d84e6397e427c655790a8c7a7 Mon Sep 17 00:00:00 2001 From: Gerard Soldevila Date: Fri, 27 Sep 2024 13:21:47 +0200 Subject: [PATCH 4/9] [Migrations] Reenable tests fixed by #194151 (#194270) ## Summary * Unskips https://github.com/elastic/kibana/issues/193756 * Unskips https://github.com/elastic/kibana/issues/166190 Both should be addressed by https://github.com/elastic/kibana/pull/194151 --- .../saved_objects/migrations/group3/actions/actions.test.ts | 3 +-- .../saved_objects/migrations/group3/fail_on_rollback.test.ts | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/core/server/integration_tests/saved_objects/migrations/group3/actions/actions.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group3/actions/actions.test.ts index deecbda117c7e..cd138e47caafc 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/group3/actions/actions.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/group3/actions/actions.test.ts @@ -61,8 +61,7 @@ const { startES } = createTestServers({ }); let esServer: TestElasticsearchUtils; -// Failing: See https://github.com/elastic/kibana/issues/166190 -describe.skip('migration actions', () => { +describe('migration actions', () => { let client: ElasticsearchClient; let esCapabilities: ReturnType; diff --git a/src/core/server/integration_tests/saved_objects/migrations/group3/fail_on_rollback.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group3/fail_on_rollback.test.ts index 6edee15ba132c..11af0f27beeea 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/group3/fail_on_rollback.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/group3/fail_on_rollback.test.ts @@ -20,8 +20,7 @@ import { delay } from '../test_utils'; import { getUpToDateMigratorTestKit } from '../kibana_migrator_test_kit.fixtures'; import { BASELINE_TEST_ARCHIVE_1K } from '../kibana_migrator_archive_utils'; -// Failing: See https://github.com/elastic/kibana/issues/193756 -describe.skip('when rolling back to an older version', () => { +describe('when rolling back to an older version', () => { let esServer: TestElasticsearchUtils['es']; beforeAll(async () => { From 89f64384ef513ae00fcd71a8eb3b797c95a4c36f Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Fri, 27 Sep 2024 14:27:27 +0300 Subject: [PATCH 5/9] fix: [Obs Alerts > Rule Detail][SCREEN READER]: H1 tag should not include secondary information: 0001 (#193961) Closes: https://github.com/elastic/observability-accessibility/issues/61 # Description Observability has a few pages that wrap related information like alert counts in the H1 tag. This presents a challenge to screen readers because all of that information now becomes the heading level one. It clogs up the Headings menu and makes it harder to reason about the page and what's primary information vs. secondary. # What was changed?: - `pageTitle` was renamed to `pageTitleContent`. The title portion was moved out of that component. - `ObservabilityPageTemplate.pageHeader` for the `Alert Detail` page was updated to separate the title from the other content. > [!NOTE] > Related PR: https://github.com/elastic/kibana/pull/193958 for `Alerts Detail` # Screen: image --- .../{page_title.tsx => page_title_content.tsx} | 10 ++-------- .../public/pages/rule_details/rule_details.tsx | 8 ++++++-- 2 files changed, 8 insertions(+), 10 deletions(-) rename x-pack/plugins/observability_solution/observability/public/pages/rule_details/components/{page_title.tsx => page_title_content.tsx} (89%) diff --git a/x-pack/plugins/observability_solution/observability/public/pages/rule_details/components/page_title.tsx b/x-pack/plugins/observability_solution/observability/public/pages/rule_details/components/page_title_content.tsx similarity index 89% rename from x-pack/plugins/observability_solution/observability/public/pages/rule_details/components/page_title.tsx rename to x-pack/plugins/observability_solution/observability/public/pages/rule_details/components/page_title_content.tsx index f951ffdff44ba..fe53dd84c48d1 100644 --- a/x-pack/plugins/observability_solution/observability/public/pages/rule_details/components/page_title.tsx +++ b/x-pack/plugins/observability_solution/observability/public/pages/rule_details/components/page_title_content.tsx @@ -12,24 +12,18 @@ import type { Rule } from '@kbn/triggers-actions-ui-plugin/public'; import { useKibana } from '../../../utils/kibana_react'; import { getHealthColor } from '../helpers/get_health_color'; -interface PageTitleProps { +interface PageTitleContentProps { rule: Rule; } -export function PageTitle({ rule }: PageTitleProps) { +export function PageTitleContent({ rule }: PageTitleContentProps) { const { triggersActionsUi: { getRuleTagBadge: RuleTagBadge }, } = useKibana().services; return ( <> - - - {rule.name} - - - {rule.executionStatus.status.charAt(0).toUpperCase() + diff --git a/x-pack/plugins/observability_solution/observability/public/pages/rule_details/rule_details.tsx b/x-pack/plugins/observability_solution/observability/public/pages/rule_details/rule_details.tsx index 31ae9e41d0529..e8270434c12b2 100644 --- a/x-pack/plugins/observability_solution/observability/public/pages/rule_details/rule_details.tsx +++ b/x-pack/plugins/observability_solution/observability/public/pages/rule_details/rule_details.tsx @@ -18,7 +18,7 @@ import { usePluginContext } from '../../hooks/use_plugin_context'; import { useFetchRule } from '../../hooks/use_fetch_rule'; import { useFetchRuleTypes } from '../../hooks/use_fetch_rule_types'; import { useGetFilteredRuleTypes } from '../../hooks/use_get_filtered_rule_types'; -import { PageTitle } from './components/page_title'; +import { PageTitleContent } from './components/page_title_content'; import { DeleteConfirmationModal } from './components/delete_confirmation_modal'; import { CenterJustifiedSpinner } from '../../components/center_justified_spinner'; import { NoRuleFoundPanel } from './components/no_rule_found_panel'; @@ -200,7 +200,11 @@ export function RuleDetailsPage() { , + pageTitle: rule.name, + pageTitleProps: { + 'data-test-subj': 'ruleName', + }, + children: , bottomBorder: false, rightSideItems: [ Date: Fri, 27 Sep 2024 15:09:46 +0200 Subject: [PATCH 6/9] [Console] Fix small UX bugs (#193887) --- .../application/components/help_popover.tsx | 193 +++++++++--------- .../editor/monaco_editor_actions_provider.ts | 19 +- .../monaco_editor_output_actions_provider.ts | 36 +++- .../containers/history/history.tsx | 4 +- test/functional/apps/console/_output_panel.ts | 1 + test/functional/apps/console/_text_input.ts | 2 +- test/functional/page_objects/console_page.ts | 7 + 7 files changed, 153 insertions(+), 109 deletions(-) diff --git a/src/plugins/console/public/application/components/help_popover.tsx b/src/plugins/console/public/application/components/help_popover.tsx index 16e9465d4d388..614a132b770e6 100644 --- a/src/plugins/console/public/application/components/help_popover.tsx +++ b/src/plugins/console/public/application/components/help_popover.tsx @@ -7,17 +7,10 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import React from 'react'; +import React, { useMemo } from 'react'; import { i18n } from '@kbn/i18n'; -import { - EuiPopover, - EuiTitle, - EuiText, - EuiFlexGroup, - EuiFlexItem, - EuiButtonIcon, - EuiSpacer, -} from '@elastic/eui'; +import { EuiPopover, EuiTitle, EuiText, EuiPanel, EuiSpacer, EuiListGroup } from '@elastic/eui'; +import { css } from '@emotion/react'; import { useServicesContext } from '../contexts'; interface HelpPopoverProps { @@ -27,9 +20,81 @@ interface HelpPopoverProps { resetTour: () => void; } +const styles = { + // Hide the external svg icon for the link given that we have a custom icon for it. + // Also remove the the hover effect on the action icon since it's a bit distracting. + listItem: css` + .euiListGroupItem__button { + > svg { + display: none; + } + } + + .euiButtonIcon:hover { + background: transparent; + } + `, +}; + export const HelpPopover = ({ button, isOpen, closePopover, resetTour }: HelpPopoverProps) => { const { docLinks } = useServicesContext(); + const listItems = useMemo( + () => [ + { + label: i18n.translate('console.helpPopover.aboutConsoleLabel', { + defaultMessage: 'About Console', + }), + href: docLinks.console.guide, + target: '_blank', + css: styles.listItem, + extraAction: { + iconType: 'popout', + href: docLinks.console.guide, + target: '_blank', + alwaysShow: true, + 'aria-label': i18n.translate('console.helpPopover.aboutConsoleButtonAriaLabel', { + defaultMessage: 'About Console link', + }), + }, + }, + { + label: i18n.translate('console.helpPopover.aboutQueryDSLLabel', { + defaultMessage: 'About Query DSL', + }), + href: docLinks.query.queryDsl, + target: '_blank', + css: styles.listItem, + extraAction: { + iconType: 'popout', + href: docLinks.query.queryDsl, + target: '_blank', + alwaysShow: true, + 'aria-label': i18n.translate('console.helpPopover.aboutQueryDSLButtonAriaLabel', { + defaultMessage: 'About QueryDSL link', + }), + }, + }, + { + label: i18n.translate('console.helpPopover.rerunTourLabel', { + defaultMessage: 'Re-run feature tour', + }), + css: styles.listItem, + onClick: resetTour, + extraAction: { + iconType: 'refresh', + alwaysShow: true, + onClick: resetTour, + 'data-test-subj': 'consoleRerunTourButton', + 'aria-label': i18n.translate('console.helpPopover.rerunTourButtonAriaLabel', { + defaultMessage: 'Re-run feature tour button', + }), + }, + }, + ], + [docLinks, resetTour] + ); + return ( - -

- {i18n.translate('console.helpPopover.title', { - defaultMessage: 'Elastic Console', - })} -

-
- - - - -

- {i18n.translate('console.helpPopover.description', { - defaultMessage: - 'Console is an interactive UI for calling Elasticsearch and Kibana APIs and viewing their responses. Search your data, manage settings, and more, using Query DSL and REST API syntax.', - })} -

-
- - + + +

+ {i18n.translate('console.helpPopover.title', { + defaultMessage: 'Elastic Console', + })} +

+
- - - - -

- {i18n.translate('console.helpPopover.aboutConsoleLabel', { - defaultMessage: 'About Console', - })} -

-
- - - -
-
+ - - - -

- {i18n.translate('console.helpPopover.aboutQueryDSLLabel', { - defaultMessage: 'About Query DSL', - })} -

-
- - - -
-
+ +

+ {i18n.translate('console.helpPopover.description', { + defaultMessage: + 'Console is an interactive UI for calling Elasticsearch and Kibana APIs and viewing their responses. Search your data, manage settings, and more, using Query DSL and REST API syntax.', + })} +

+
+
- - - -

- {i18n.translate('console.helpPopover.rerunTourLabel', { - defaultMessage: 'Re-run feature tour', - })} -

-
- - - -
-
-
+ ); }; diff --git a/src/plugins/console/public/application/containers/editor/monaco_editor_actions_provider.ts b/src/plugins/console/public/application/containers/editor/monaco_editor_actions_provider.ts index 8c66d31b2b57e..8fe6a33332379 100644 --- a/src/plugins/console/public/application/containers/editor/monaco_editor_actions_provider.ts +++ b/src/plugins/console/public/application/containers/editor/monaco_editor_actions_provider.ts @@ -62,17 +62,23 @@ export class MonacoEditorActionsProvider { private setEditorActionsCss: (css: CSSProperties) => void, private isDevMode: boolean ) { + this.editor.focus(); this.parsedRequestsProvider = getParsedRequestsProvider(this.editor.getModel()); this.highlightedLines = this.editor.createDecorationsCollection(); const debouncedHighlightRequests = debounce( - () => this.highlightRequests(), + async () => { + if (editor.hasTextFocus()) { + await this.highlightRequests(); + } else { + this.clearEditorDecorations(); + } + }, DEBOUNCE_HIGHLIGHT_WAIT_MS, { leading: true, } ); - debouncedHighlightRequests(); const debouncedTriggerSuggestions = debounce( () => { @@ -110,6 +116,15 @@ export class MonacoEditorActionsProvider { }); } + private clearEditorDecorations() { + // remove the highlighted lines + this.highlightedLines.clear(); + // hide action buttons + this.setEditorActionsCss({ + visibility: 'hidden', + }); + } + private updateEditorActions(lineNumber?: number) { // if no request is currently selected, hide the actions buttons if (!lineNumber) { diff --git a/src/plugins/console/public/application/containers/editor/monaco_editor_output_actions_provider.ts b/src/plugins/console/public/application/containers/editor/monaco_editor_output_actions_provider.ts index fe20af6a1bcb4..bd9d0c9e73490 100644 --- a/src/plugins/console/public/application/containers/editor/monaco_editor_output_actions_provider.ts +++ b/src/plugins/console/public/application/containers/editor/monaco_editor_output_actions_provider.ts @@ -30,30 +30,52 @@ export class MonacoEditorOutputActionsProvider { private setEditorActionsCss: (css: CSSProperties) => void ) { this.highlightedLines = this.editor.createDecorationsCollection(); - this.editor.focus(); const debouncedHighlightRequests = debounce( - () => this.highlightRequests(), + async () => { + if (editor.hasTextFocus()) { + await this.highlightRequests(); + } else { + this.clearEditorDecorations(); + } + }, DEBOUNCE_HIGHLIGHT_WAIT_MS, { leading: true, } ); - debouncedHighlightRequests(); // init all listeners - editor.onDidChangeCursorPosition(async (event) => { + editor.onDidChangeCursorPosition(async () => { await debouncedHighlightRequests(); }); - editor.onDidScrollChange(async (event) => { + editor.onDidScrollChange(async () => { await debouncedHighlightRequests(); }); - editor.onDidChangeCursorSelection(async (event) => { + editor.onDidChangeCursorSelection(async () => { await debouncedHighlightRequests(); }); - editor.onDidContentSizeChange(async (event) => { + editor.onDidContentSizeChange(async () => { await debouncedHighlightRequests(); }); + + editor.onDidBlurEditorText(() => { + // Since the actions buttons are placed outside of the editor, we need to delay + // the clearing of the editor decorations to ensure that the actions buttons + // are not hidden. + setTimeout(() => { + this.clearEditorDecorations(); + }, 100); + }); + } + + private clearEditorDecorations() { + // remove the highlighted lines + this.highlightedLines.clear(); + // hide action buttons + this.setEditorActionsCss({ + visibility: 'hidden', + }); } private updateEditorActions(lineNumber?: number) { diff --git a/src/plugins/console/public/application/containers/history/history.tsx b/src/plugins/console/public/application/containers/history/history.tsx index f6d18d6d06dd1..384503e5df084 100644 --- a/src/plugins/console/public/application/containers/history/history.tsx +++ b/src/plugins/console/public/application/containers/history/history.tsx @@ -66,7 +66,9 @@ const CheckeableCardLabel = ({ historyItem }: { historyItem: HistoryProps }) => - {historyItem.endpoint} + + {historyItem.method} {historyItem.endpoint} + diff --git a/test/functional/apps/console/_output_panel.ts b/test/functional/apps/console/_output_panel.ts index 49c41ae7a6ccc..1da032328493b 100644 --- a/test/functional/apps/console/_output_panel.ts +++ b/test/functional/apps/console/_output_panel.ts @@ -47,6 +47,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should be able to copy the response of a request', async () => { await sendRequest('GET /_search?pretty'); + await PageObjects.console.focusOutputEditor(); await PageObjects.console.clickCopyOutput(); const resultToast = await toasts.getElementByIndex(1); diff --git a/test/functional/apps/console/_text_input.ts b/test/functional/apps/console/_text_input.ts index 640a31bd19edf..31c832990042b 100644 --- a/test/functional/apps/console/_text_input.ts +++ b/test/functional/apps/console/_text_input.ts @@ -84,7 +84,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await retry.try(async () => { const history = await PageObjects.console.getHistoryEntries(); - expect(history).to.eql(['/_search?pretty\na few seconds ago']); + expect(history).to.eql(['GET /_search?pretty\na few seconds ago']); }); await PageObjects.console.clickClearHistory(); diff --git a/test/functional/page_objects/console_page.ts b/test/functional/page_objects/console_page.ts index 080167c995ccb..29b88787e7ec2 100644 --- a/test/functional/page_objects/console_page.ts +++ b/test/functional/page_objects/console_page.ts @@ -51,6 +51,13 @@ export class ConsolePageObject extends FtrService { await textArea.clearValueWithKeyboard(); } + public async focusOutputEditor() { + const outputEditor = await this.testSubjects.find('consoleMonacoOutput'); + // Simply clicking on the output editor doesn't focus it, so we need to click + // on the margin view overlays + await (await outputEditor.findByClassName('margin-view-overlays')).click(); + } + public async getOutputText() { const outputPanel = await this.testSubjects.find('consoleMonacoOutput'); const outputViewDiv = await outputPanel.findByClassName('monaco-scrollable-element'); From a002a1b142ea61665206253aca6c85b5d83866a2 Mon Sep 17 00:00:00 2001 From: Kurt Date: Fri, 27 Sep 2024 09:21:12 -0400 Subject: [PATCH 7/9] Shutdown Kibana on usages of PKCS12 truststore/keystore config (#192627) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Closes #169741 PKCS12 truststores/keystores are not FIPS compliant and should not be used when running if FIPS mode. Users will be notified when they try to start KB in FIPS mode and are using the flagged settings and KB will exit. ## Testing You will need to generate a PKCS12 container (*.p12) file and have it stored somewhere that your local KB can access. To generate a PKCS12 to use: - `openssl req -x509 -newkey rsa:4096 -keyout myPrivateKey.pem -out myCertificate.crt` - `openssl pkcs12 -export -out keyStore.p12 -inkey myPrivateKey.pem -in myCertificate.crt` - Set password to `test` Put the `.p12` file in your `config` directory (not required, but you can copy and paste these commands easier) Start an ES instance in a method of your choosing, but not using yarn es snapshot. I like to use an 8.16.0-snapshot from the .es/cache directory by running tar -xzvf elasticsearch-8.16.0-SNAPSHOT-darwin-aarch64.tar.gz and cd into the new directory's bin folder to run ./elasticsearch In a new terminal window, navigate to your the top level of your elasticsearch folder and run: `curl -X POST --cacert config/certs/http_ca.crt -u elastic:YOUR_PASSWORD_HERE "https://localhost:9200/_license/start_trial?acknowledge=true&pretty"` This will enable the trial license for ES. Ensure you have Docker running locally. From any command line, run: ``` docker run --rm -it \ -v "$(pwd)"/config/keyStore.p12:/keyStore.p12:ro \ -e XPACK_SECURITY_FIPSMODE_ENABLED='true' \ -e ELASTICSEARCH_SSL_TRUSTSTORE_PATH='/keyStore.p12' \ -e ELASTICSEARCH_SSL_TRUSTSTORE_PASSWORD='test' \ -e ELASTICSEARCH_SSL_KEYSTORE_PATH='/keyStore.p12' \ -e ELASTICSEARCH_SSL_KEYSTORE_PASSWORD='test' \ -e SERVER_SSL_TRUSTSTORE_PATH='/keyStore.p12' \ -e SERVER_SSL_TRUSTSTORE_PASSWORD='test' \ -e SERVER_SSL_KEYSTORE_PATH='/keyStore.p12' \ -e SERVER_SSL_KEYSTORE_PASSWORD='test' \ -p 5601:5601/tcp docker.elastic.co/kibana-ci/kibana-ubi-fips:9.0.0-SNAPSHOT-92aeabf477867dc1768f9048b159f01f2ab1fcc3 ``` This will start Kibana into Interactive Setup mode, copy and paste the token from the ES startup logs. In your logs, you will see an error letting users know that PKCS12 settings are not allowed in FIPS It should look like: Screenshot 2024-09-11 at 1 57 22 PM ## Release note When running in FIPS mode, Kibana will forbid usage of PKCS12 configuration options --------- Co-authored-by: Jean-Louis Leysens --- .../src/fips/fips.test.ts | 153 +++++++++++++----- .../src/fips/fips.ts | 56 ++++++- .../src/security_service.ts | 5 +- .../src/utils/index.ts | 11 ++ 4 files changed, 176 insertions(+), 49 deletions(-) diff --git a/packages/core/security/core-security-server-internal/src/fips/fips.test.ts b/packages/core/security/core-security-server-internal/src/fips/fips.test.ts index 8726e3b5a34ee..ff610493e1322 100644 --- a/packages/core/security/core-security-server-internal/src/fips/fips.test.ts +++ b/packages/core/security/core-security-server-internal/src/fips/fips.test.ts @@ -7,6 +7,8 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ +import { CriticalError } from '@kbn/core-base-server-internal'; + const mockGetFipsFn = jest.fn(); jest.mock('crypto', () => ({ randomBytes: jest.fn(), @@ -21,54 +23,41 @@ import { isFipsEnabled, checkFipsConfig } from './fips'; import { loggingSystemMock } from '@kbn/core-logging-server-mocks'; describe('fips', () => { - let config: SecurityServiceConfigType; + let securityConfig: SecurityServiceConfigType; describe('#isFipsEnabled', () => { it('should return `true` if config.experimental.fipsMode.enabled is `true`', () => { - config = { experimental: { fipsMode: { enabled: true } } }; + securityConfig = { experimental: { fipsMode: { enabled: true } } }; - expect(isFipsEnabled(config)).toBe(true); + expect(isFipsEnabled(securityConfig)).toBe(true); }); it('should return `false` if config.experimental.fipsMode.enabled is `false`', () => { - config = { experimental: { fipsMode: { enabled: false } } }; + securityConfig = { experimental: { fipsMode: { enabled: false } } }; - expect(isFipsEnabled(config)).toBe(false); + expect(isFipsEnabled(securityConfig)).toBe(false); }); it('should return `false` if config.experimental.fipsMode.enabled is `undefined`', () => { - expect(isFipsEnabled(config)).toBe(false); + expect(isFipsEnabled(securityConfig)).toBe(false); }); }); describe('checkFipsConfig', () => { - let mockExit: jest.SpyInstance; - - beforeAll(() => { - mockExit = jest.spyOn(process, 'exit').mockImplementation((exitCode) => { - throw new Error(`Fake Exit: ${exitCode}`); - }); - }); - - afterAll(() => { - mockExit.mockRestore(); - }); - it('should log an error message if FIPS mode is misconfigured - xpack.security.experimental.fipsMode.enabled true, Nodejs FIPS mode false', async () => { - config = { experimental: { fipsMode: { enabled: true } } }; + securityConfig = { experimental: { fipsMode: { enabled: true } } }; const logger = loggingSystemMock.create().get(); + let fipsException: undefined | CriticalError; try { - checkFipsConfig(config, logger); + checkFipsConfig(securityConfig, {}, {}, logger); } catch (e) { - expect(mockExit).toHaveBeenNthCalledWith(1, 78); + fipsException = e; } - expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(` - Array [ - Array [ - "Configuration mismatch error. xpack.security.experimental.fipsMode.enabled is set to true and the configured Node.js environment has FIPS disabled", - ], - ] - `); + expect(fipsException).toBeInstanceOf(CriticalError); + expect(fipsException!.processExitCode).toBe(78); + expect(fipsException!.message).toEqual( + 'Configuration mismatch error. xpack.security.experimental.fipsMode.enabled is set to true and the configured Node.js environment has FIPS disabled' + ); }); it('should log an error message if FIPS mode is misconfigured - xpack.security.experimental.fipsMode.enabled false, Nodejs FIPS mode true', async () => { @@ -76,22 +65,20 @@ describe('fips', () => { return 1; }); - config = { experimental: { fipsMode: { enabled: false } } }; + securityConfig = { experimental: { fipsMode: { enabled: false } } }; const logger = loggingSystemMock.create().get(); + let fipsException: undefined | CriticalError; try { - checkFipsConfig(config, logger); + checkFipsConfig(securityConfig, {}, {}, logger); } catch (e) { - expect(mockExit).toHaveBeenNthCalledWith(1, 78); + fipsException = e; } - - expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(` - Array [ - Array [ - "Configuration mismatch error. xpack.security.experimental.fipsMode.enabled is set to false and the configured Node.js environment has FIPS enabled", - ], - ] - `); + expect(fipsException).toBeInstanceOf(CriticalError); + expect(fipsException!.processExitCode).toBe(78); + expect(fipsException!.message).toEqual( + 'Configuration mismatch error. xpack.security.experimental.fipsMode.enabled is set to false and the configured Node.js environment has FIPS enabled' + ); }); it('should log an info message if FIPS mode is properly configured - xpack.security.experimental.fipsMode.enabled true, Nodejs FIPS mode true', async () => { @@ -99,11 +86,11 @@ describe('fips', () => { return 1; }); - config = { experimental: { fipsMode: { enabled: true } } }; + securityConfig = { experimental: { fipsMode: { enabled: true } } }; const logger = loggingSystemMock.create().get(); try { - checkFipsConfig(config, logger); + checkFipsConfig(securityConfig, {}, {}, logger); } catch (e) { logger.error('Should not throw error!'); } @@ -116,5 +103,89 @@ describe('fips', () => { ] `); }); + + describe('PKCS12 Config settings', function () { + let serverConfig = {}; + let elasticsearchConfig = {}; + + beforeEach(function () { + mockGetFipsFn.mockImplementationOnce(() => { + return 1; + }); + + securityConfig = { experimental: { fipsMode: { enabled: true } } }; + }); + + afterEach(function () { + serverConfig = {}; + elasticsearchConfig = {}; + }); + + it('should log an error message for each PKCS12 configuration option that is set', async () => { + elasticsearchConfig = { + ssl: { + keystore: { + path: '/test', + }, + truststore: { + path: '/test', + }, + }, + }; + + serverConfig = { + ssl: { + keystore: { + path: '/test', + }, + truststore: { + path: '/test', + }, + }, + }; + + const logger = loggingSystemMock.create().get(); + + let fipsException: undefined | CriticalError; + try { + checkFipsConfig(securityConfig, elasticsearchConfig, serverConfig, logger); + } catch (e) { + fipsException = e; + } + + expect(fipsException).toBeInstanceOf(CriticalError); + expect(fipsException!.processExitCode).toBe(78); + expect(fipsException!.message).toEqual( + 'Configuration mismatch error: elasticsearch.ssl.keystore.path, elasticsearch.ssl.truststore.path, server.ssl.keystore.path, server.ssl.truststore.path are set, PKCS12 configurations are not allowed while running in FIPS mode.' + ); + }); + + it('should log an error message for one PKCS12 configuration option that is set', async () => { + elasticsearchConfig = { + ssl: { + keystore: { + path: '/test', + }, + }, + }; + + serverConfig = {}; + + const logger = loggingSystemMock.create().get(); + + let fipsException: undefined | CriticalError; + try { + checkFipsConfig(securityConfig, elasticsearchConfig, serverConfig, logger); + } catch (e) { + fipsException = e; + } + + expect(fipsException).toBeInstanceOf(CriticalError); + expect(fipsException!.processExitCode).toBe(78); + expect(fipsException!.message).toEqual( + 'Configuration mismatch error: elasticsearch.ssl.keystore.path is set, PKCS12 configurations are not allowed while running in FIPS mode.' + ); + }); + }); }); }); diff --git a/packages/core/security/core-security-server-internal/src/fips/fips.ts b/packages/core/security/core-security-server-internal/src/fips/fips.ts index 8f09facb554b5..0d9dea9e467fe 100644 --- a/packages/core/security/core-security-server-internal/src/fips/fips.ts +++ b/packages/core/security/core-security-server-internal/src/fips/fips.ts @@ -9,28 +9,70 @@ import type { Logger } from '@kbn/logging'; import { getFips } from 'crypto'; -import { SecurityServiceConfigType } from '../utils'; - +import { CriticalError } from '@kbn/core-base-server-internal'; +import { PKCS12ConfigType, SecurityServiceConfigType } from '../utils'; export function isFipsEnabled(config: SecurityServiceConfigType): boolean { return config?.experimental?.fipsMode?.enabled ?? false; } -export function checkFipsConfig(config: SecurityServiceConfigType, logger: Logger) { +export function checkFipsConfig( + config: SecurityServiceConfigType, + elasticsearchConfig: PKCS12ConfigType, + serverConfig: PKCS12ConfigType, + logger: Logger +) { const isFipsConfigEnabled = isFipsEnabled(config); const isNodeRunningWithFipsEnabled = getFips() === 1; // Check if FIPS is enabled in either setting if (isFipsConfigEnabled || isNodeRunningWithFipsEnabled) { - // FIPS must be enabled on both or log and error an exit Kibana + const definedPKCS12ConfigOptions = findDefinedPKCS12ConfigOptions( + elasticsearchConfig, + serverConfig + ); + // FIPS must be enabled on both, or, log/error an exit Kibana if (isFipsConfigEnabled !== isNodeRunningWithFipsEnabled) { - logger.error( + throw new CriticalError( `Configuration mismatch error. xpack.security.experimental.fipsMode.enabled is set to ${isFipsConfigEnabled} and the configured Node.js environment has FIPS ${ isNodeRunningWithFipsEnabled ? 'enabled' : 'disabled' - }` + }`, + 'invalidConfig', + 78 + ); + } else if (definedPKCS12ConfigOptions.length > 0) { + throw new CriticalError( + `Configuration mismatch error: ${definedPKCS12ConfigOptions.join(', ')} ${ + definedPKCS12ConfigOptions.length > 1 ? 'are' : 'is' + } set, PKCS12 configurations are not allowed while running in FIPS mode.`, + 'invalidConfig', + 78 ); - process.exit(78); } else { logger.info('Kibana is running in FIPS mode.'); } } } + +function findDefinedPKCS12ConfigOptions( + elasticsearchConfig: PKCS12ConfigType, + serverConfig: PKCS12ConfigType +): string[] { + const result = []; + if (elasticsearchConfig?.ssl?.keystore?.path) { + result.push('elasticsearch.ssl.keystore.path'); + } + + if (elasticsearchConfig?.ssl?.truststore?.path) { + result.push('elasticsearch.ssl.truststore.path'); + } + + if (serverConfig?.ssl?.keystore?.path) { + result.push('server.ssl.keystore.path'); + } + + if (serverConfig?.ssl?.truststore?.path) { + result.push('server.ssl.truststore.path'); + } + + return result; +} diff --git a/packages/core/security/core-security-server-internal/src/security_service.ts b/packages/core/security/core-security-server-internal/src/security_service.ts index cf39664bd46a0..81a337db47569 100644 --- a/packages/core/security/core-security-server-internal/src/security_service.ts +++ b/packages/core/security/core-security-server-internal/src/security_service.ts @@ -21,6 +21,7 @@ import { getDefaultSecurityImplementation, convertSecurityApi, SecurityServiceConfigType, + PKCS12ConfigType, } from './utils'; export class SecurityService @@ -50,8 +51,10 @@ export class SecurityService public setup(): InternalSecurityServiceSetup { const config = this.getConfig(); const securityConfig: SecurityServiceConfigType = config.get(['xpack', 'security']); + const elasticsearchConfig: PKCS12ConfigType = config.get(['elasticsearch']); + const serverConfig: PKCS12ConfigType = config.get(['server']); - checkFipsConfig(securityConfig, this.log); + checkFipsConfig(securityConfig, elasticsearchConfig, serverConfig, this.log); return { registerSecurityDelegate: (api) => { diff --git a/packages/core/security/core-security-server-internal/src/utils/index.ts b/packages/core/security/core-security-server-internal/src/utils/index.ts index 1e3a370057135..666afcce38afd 100644 --- a/packages/core/security/core-security-server-internal/src/utils/index.ts +++ b/packages/core/security/core-security-server-internal/src/utils/index.ts @@ -17,3 +17,14 @@ export interface SecurityServiceConfigType { }; }; } + +export interface PKCS12ConfigType { + ssl?: { + keystore?: { + path?: string; + }; + truststore?: { + path?: string; + }; + }; +} From eea06c0d64d2424601552bd905b2b020ba4dcd56 Mon Sep 17 00:00:00 2001 From: Pablo Machado Date: Fri, 27 Sep 2024 16:05:52 +0200 Subject: [PATCH 8/9] [SecuritySolution] List Entities UI (#193167) This PR creates a UI component to list entities inside the Entity Store. ### What is included - Create `EntitiesList` component - Duplicate `MultiselectFilter` component - Display `EntitiesList` in the entity analytics dashboard - Use the `entityStoreEnabled` experimental flag ### What is NOT included - Asset criticality - Source field - Risk score fields ![Screenshot 2024-09-20 at 15 27 23](https://github.com/user-attachments/assets/87295c76-a7d4-4303-b1ea-46d644bf21f4) ### How to test 1. Add some host/user data * Easiest is to use [elastic/security-data-generator](https://github.com/elastic/security-documents-generator) 2. Make sure to add `entityStoreEnabled` under `xpack.securitySolution.enableExperimental` in your `kibana.dev.yml` 3. In kibana dev tools or your terminal, call the `INIT` route for either `user` or `host`. 4. You should now see 2 transforms in kibana. Make sure to re-trigger them if needed so they process the documents. 5. Enable the experimental flag `entityStoreEnabled` 6. Go to entity analytics dashboard and you should see an populated entities page Implements https://github.com/elastic/security-team/issues/10536 ### 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: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../output/kibana.serverless.staging.yaml | 155 ++++++++++++++ oas_docs/output/kibana.staging.yaml | 155 ++++++++++++++ .../entity_store/entities/common.gen.ts | 17 ++ .../entity_store/entities/common.schema.yaml | 24 +++ ...alytics_api_2023_10_31.bundled.schema.yaml | 150 ++++++++++++++ ...alytics_api_2023_10_31.bundled.schema.yaml | 150 ++++++++++++++ .../components/multiselect_filter/index.tsx | 122 +++++++++++ .../events/entity_analytics/index.ts | 2 +- .../events/entity_analytics/types.ts | 2 +- .../filters/multiselect_filter/index.tsx | 3 + .../asset_criticality_filter.tsx | 46 +++++ .../header_content.test.tsx | 9 +- .../header_content.tsx | 25 +-- .../entity_analytics_risk_score/index.tsx | 5 +- .../components/entity_source_filter.tsx | 37 ++++ .../components/entity_store/constants.ts | 28 +++ .../entity_store/entities_list.test.tsx | 138 +++++++++++++ .../components/entity_store/entities_list.tsx | 158 +++++++++++++++ .../components/entity_store/helpers.test.ts | 34 ++++ .../components/entity_store/helpers.ts | 14 ++ .../hooks/use_entities_list_columns.tsx | 191 ++++++++++++++++++ .../hooks/use_entities_list_filters.test.ts | 148 ++++++++++++++ .../hooks/use_entities_list_filters.ts | 69 +++++++ .../hooks/use_entities_list_query.test.tsx | 57 ++++++ .../hooks/use_entities_list_query.ts | 32 +++ .../host_risk_score_table/index.tsx | 17 +- .../components/risk_score/translations.ts | 21 +- ...roup.test.tsx => severity_filter.test.tsx} | 58 +----- .../components/severity/severity_filter.tsx | 60 ++++++ .../severity/severity_filter_group.tsx | 134 ------------ .../user_risk_score_table/index.tsx | 17 +- .../pages/entity_analytics_dashboard.tsx | 10 + .../authentications_host_table.test.tsx.snap | 2 +- .../authentications_user_table.test.tsx.snap | 2 +- .../__snapshots__/index.test.tsx.snap | 2 +- .../components/paginated_table/index.tsx | 6 +- .../entity_store_data_client.test.ts.snap | 7 +- .../entity_store_data_client.test.ts | 2 +- .../entity_store/entity_store_data_client.ts | 9 +- .../translations/translations/fr-FR.json | 1 - .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - .../cypress/screens/hosts/host_risk.ts | 2 +- 43 files changed, 1867 insertions(+), 256 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/components/multiselect_filter/index.tsx create mode 100644 x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality/asset_criticality_filter.tsx create mode 100644 x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/entity_source_filter.tsx create mode 100644 x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/constants.ts create mode 100644 x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/entities_list.test.tsx create mode 100644 x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/entities_list.tsx create mode 100644 x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/helpers.test.ts create mode 100644 x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/helpers.ts create mode 100644 x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/hooks/use_entities_list_columns.tsx create mode 100644 x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/hooks/use_entities_list_filters.test.ts create mode 100644 x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/hooks/use_entities_list_filters.ts create mode 100644 x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/hooks/use_entities_list_query.test.tsx create mode 100644 x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/hooks/use_entities_list_query.ts rename x-pack/plugins/security_solution/public/entity_analytics/components/severity/{severity_filter_group.test.tsx => severity_filter.test.tsx} (51%) create mode 100644 x-pack/plugins/security_solution/public/entity_analytics/components/severity/severity_filter.tsx delete mode 100644 x-pack/plugins/security_solution/public/entity_analytics/components/severity/severity_filter_group.tsx diff --git a/oas_docs/output/kibana.serverless.staging.yaml b/oas_docs/output/kibana.serverless.staging.yaml index 993fadf1c87cc..cf5cdbac0e9a4 100644 --- a/oas_docs/output/kibana.serverless.staging.yaml +++ b/oas_docs/output/kibana.serverless.staging.yaml @@ -29833,6 +29833,96 @@ components: oneOf: - $ref: '#/components/schemas/Security_Entity_Analytics_API_UserEntity' - $ref: '#/components/schemas/Security_Entity_Analytics_API_HostEntity' + Security_Entity_Analytics_API_EntityRiskLevels: + enum: + - Unknown + - Low + - Moderate + - High + - Critical + type: string + Security_Entity_Analytics_API_EntityRiskScoreRecord: + type: object + properties: + '@timestamp': + description: The time at which the risk score was calculated. + example: '2017-07-21T17:32:28Z' + format: date-time + type: string + calculated_level: + $ref: '#/components/schemas/Security_Entity_Analytics_API_EntityRiskLevels' + description: Lexical description of the entity's risk. + example: Critical + calculated_score: + description: The raw numeric value of the given entity's risk score. + format: double + type: number + calculated_score_norm: + description: >- + The normalized numeric value of the given entity's risk score. + Useful for comparing with other entities. + format: double + maximum: 100 + minimum: 0 + type: number + category_1_count: + description: >- + The number of risk input documents that contributed to the Category + 1 score (`category_1_score`). + format: integer + type: number + category_1_score: + description: >- + The contribution of Category 1 to the overall risk score + (`calculated_score`). Category 1 contains Detection Engine Alerts. + format: double + type: number + category_2_count: + format: integer + type: number + category_2_score: + format: double + type: number + criticality_level: + $ref: >- + #/components/schemas/Security_Entity_Analytics_API_AssetCriticalityLevel + criticality_modifier: + format: double + type: number + id_field: + description: >- + The identifier field defining this risk score. Coupled with + `id_value`, uniquely identifies the entity being scored. + example: host.name + type: string + id_value: + description: >- + The identifier value defining this risk score. Coupled with + `id_field`, uniquely identifies the entity being scored. + example: example.host + type: string + inputs: + description: >- + A list of the highest-risk documents contributing to this risk + score. Useful for investigative purposes. + items: + $ref: '#/components/schemas/Security_Entity_Analytics_API_RiskScoreInput' + type: array + notes: + items: + type: string + type: array + required: + - '@timestamp' + - id_field + - id_value + - calculated_level + - calculated_score + - calculated_score_norm + - category_1_score + - category_1_count + - inputs + - notes Security_Entity_Analytics_API_EntityType: enum: - user @@ -29841,6 +29931,14 @@ components: Security_Entity_Analytics_API_HostEntity: type: object properties: + asset: + type: object + properties: + criticality: + $ref: >- + #/components/schemas/Security_Entity_Analytics_API_AssetCriticalityLevel + required: + - criticality entity: type: object properties: @@ -29864,6 +29962,8 @@ components: type: string schemaVersion: type: string + source: + type: string type: enum: - node @@ -29907,6 +30007,9 @@ components: type: array name: type: string + risk: + $ref: >- + #/components/schemas/Security_Entity_Analytics_API_EntityRiskScoreRecord type: items: type: string @@ -29949,6 +30052,44 @@ components: properties: success: type: boolean + Security_Entity_Analytics_API_RiskScoreInput: + description: A generic representation of a document contributing to a Risk Score. + type: object + properties: + category: + description: The risk category of the risk input document. + example: category_1 + type: string + contribution_score: + format: double + type: number + description: + description: A human-readable description of the risk input document. + example: 'Generated from Detection Engine Rule: Malware Prevention Alert' + type: string + id: + description: The unique identifier (`_id`) of the original source document + example: 91a93376a507e86cfbf282166275b89f9dbdb1f0be6c8103c6ff2909ca8e1a1c + type: string + index: + description: The unique index (`_index`) of the original source document + example: .internal.alerts-security.alerts-default-000001 + type: string + risk_score: + description: The weighted risk score of the risk input document. + format: double + maximum: 100 + minimum: 0 + type: number + timestamp: + description: The @timestamp of the risk input document. + example: '2017-07-21T17:32:28Z' + type: string + required: + - id + - index + - description + - category Security_Entity_Analytics_API_TaskManagerUnavailableResponse: description: Task manager is unavailable type: object @@ -29964,6 +30105,14 @@ components: Security_Entity_Analytics_API_UserEntity: type: object properties: + asset: + type: object + properties: + criticality: + $ref: >- + #/components/schemas/Security_Entity_Analytics_API_AssetCriticalityLevel + required: + - criticality entity: type: object properties: @@ -29987,6 +30136,8 @@ components: type: string schemaVersion: type: string + source: + type: string type: enum: - node @@ -30001,6 +30152,7 @@ components: - type - firstSeenTimestamp - definitionId + - source user: type: object properties: @@ -30026,6 +30178,9 @@ components: type: array name: type: string + risk: + $ref: >- + #/components/schemas/Security_Entity_Analytics_API_EntityRiskScoreRecord roles: items: type: string diff --git a/oas_docs/output/kibana.staging.yaml b/oas_docs/output/kibana.staging.yaml index 3d5523fcc071d..6b95c8ed892b3 100644 --- a/oas_docs/output/kibana.staging.yaml +++ b/oas_docs/output/kibana.staging.yaml @@ -37842,6 +37842,96 @@ components: oneOf: - $ref: '#/components/schemas/Security_Entity_Analytics_API_UserEntity' - $ref: '#/components/schemas/Security_Entity_Analytics_API_HostEntity' + Security_Entity_Analytics_API_EntityRiskLevels: + enum: + - Unknown + - Low + - Moderate + - High + - Critical + type: string + Security_Entity_Analytics_API_EntityRiskScoreRecord: + type: object + properties: + '@timestamp': + description: The time at which the risk score was calculated. + example: '2017-07-21T17:32:28Z' + format: date-time + type: string + calculated_level: + $ref: '#/components/schemas/Security_Entity_Analytics_API_EntityRiskLevels' + description: Lexical description of the entity's risk. + example: Critical + calculated_score: + description: The raw numeric value of the given entity's risk score. + format: double + type: number + calculated_score_norm: + description: >- + The normalized numeric value of the given entity's risk score. + Useful for comparing with other entities. + format: double + maximum: 100 + minimum: 0 + type: number + category_1_count: + description: >- + The number of risk input documents that contributed to the Category + 1 score (`category_1_score`). + format: integer + type: number + category_1_score: + description: >- + The contribution of Category 1 to the overall risk score + (`calculated_score`). Category 1 contains Detection Engine Alerts. + format: double + type: number + category_2_count: + format: integer + type: number + category_2_score: + format: double + type: number + criticality_level: + $ref: >- + #/components/schemas/Security_Entity_Analytics_API_AssetCriticalityLevel + criticality_modifier: + format: double + type: number + id_field: + description: >- + The identifier field defining this risk score. Coupled with + `id_value`, uniquely identifies the entity being scored. + example: host.name + type: string + id_value: + description: >- + The identifier value defining this risk score. Coupled with + `id_field`, uniquely identifies the entity being scored. + example: example.host + type: string + inputs: + description: >- + A list of the highest-risk documents contributing to this risk + score. Useful for investigative purposes. + items: + $ref: '#/components/schemas/Security_Entity_Analytics_API_RiskScoreInput' + type: array + notes: + items: + type: string + type: array + required: + - '@timestamp' + - id_field + - id_value + - calculated_level + - calculated_score + - calculated_score_norm + - category_1_score + - category_1_count + - inputs + - notes Security_Entity_Analytics_API_EntityType: enum: - user @@ -37850,6 +37940,14 @@ components: Security_Entity_Analytics_API_HostEntity: type: object properties: + asset: + type: object + properties: + criticality: + $ref: >- + #/components/schemas/Security_Entity_Analytics_API_AssetCriticalityLevel + required: + - criticality entity: type: object properties: @@ -37873,6 +37971,8 @@ components: type: string schemaVersion: type: string + source: + type: string type: enum: - node @@ -37916,6 +38016,9 @@ components: type: array name: type: string + risk: + $ref: >- + #/components/schemas/Security_Entity_Analytics_API_EntityRiskScoreRecord type: items: type: string @@ -37958,6 +38061,44 @@ components: properties: success: type: boolean + Security_Entity_Analytics_API_RiskScoreInput: + description: A generic representation of a document contributing to a Risk Score. + type: object + properties: + category: + description: The risk category of the risk input document. + example: category_1 + type: string + contribution_score: + format: double + type: number + description: + description: A human-readable description of the risk input document. + example: 'Generated from Detection Engine Rule: Malware Prevention Alert' + type: string + id: + description: The unique identifier (`_id`) of the original source document + example: 91a93376a507e86cfbf282166275b89f9dbdb1f0be6c8103c6ff2909ca8e1a1c + type: string + index: + description: The unique index (`_index`) of the original source document + example: .internal.alerts-security.alerts-default-000001 + type: string + risk_score: + description: The weighted risk score of the risk input document. + format: double + maximum: 100 + minimum: 0 + type: number + timestamp: + description: The @timestamp of the risk input document. + example: '2017-07-21T17:32:28Z' + type: string + required: + - id + - index + - description + - category Security_Entity_Analytics_API_TaskManagerUnavailableResponse: description: Task manager is unavailable type: object @@ -37973,6 +38114,14 @@ components: Security_Entity_Analytics_API_UserEntity: type: object properties: + asset: + type: object + properties: + criticality: + $ref: >- + #/components/schemas/Security_Entity_Analytics_API_AssetCriticalityLevel + required: + - criticality entity: type: object properties: @@ -37996,6 +38145,8 @@ components: type: string schemaVersion: type: string + source: + type: string type: enum: - node @@ -38010,6 +38161,7 @@ components: - type - firstSeenTimestamp - definitionId + - source user: type: object properties: @@ -38035,6 +38187,9 @@ components: type: array name: type: string + risk: + $ref: >- + #/components/schemas/Security_Entity_Analytics_API_EntityRiskScoreRecord roles: items: type: string diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/entities/common.gen.ts b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/entities/common.gen.ts index eb123b5a9da1f..697fbd81d36a0 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/entities/common.gen.ts +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/entities/common.gen.ts @@ -16,6 +16,9 @@ import { z } from '@kbn/zod'; +import { EntityRiskScoreRecord } from '../../common/common.gen'; +import { AssetCriticalityLevel } from '../../asset_criticality/common.gen'; + export type UserEntity = z.infer; export const UserEntity = z.object({ user: z @@ -27,6 +30,7 @@ export const UserEntity = z.object({ id: z.array(z.string()).optional(), email: z.array(z.string()).optional(), hash: z.array(z.string()).optional(), + risk: EntityRiskScoreRecord.optional(), }) .optional(), entity: z @@ -40,6 +44,12 @@ export const UserEntity = z.object({ type: z.literal('node'), firstSeenTimestamp: z.string().datetime(), definitionId: z.string(), + source: z.string(), + }) + .optional(), + asset: z + .object({ + criticality: AssetCriticalityLevel, }) .optional(), }); @@ -56,6 +66,7 @@ export const HostEntity = z.object({ type: z.array(z.string()).optional(), mac: z.array(z.string()).optional(), architecture: z.array(z.string()).optional(), + risk: EntityRiskScoreRecord.optional(), }) .optional(), entity: z @@ -69,6 +80,12 @@ export const HostEntity = z.object({ type: z.literal('node'), firstSeenTimestamp: z.string().datetime(), definitionId: z.string(), + source: z.string().optional(), + }) + .optional(), + asset: z + .object({ + criticality: AssetCriticalityLevel, }) .optional(), }); diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/entities/common.schema.yaml b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/entities/common.schema.yaml index 0f7f31792306c..8e345aae57604 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/entities/common.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/entities/common.schema.yaml @@ -38,6 +38,8 @@ components: type: array items: type: string + risk: + $ref: '../../common/common.schema.yaml#/components/schemas/EntityRiskScoreRecord' required: - name entity: @@ -67,6 +69,8 @@ components: format: date-time definitionId: type: string + source: + type: string required: - lastSeenTimestamp - schemaVersion @@ -77,6 +81,14 @@ components: - type - firstSeenTimestamp - definitionId + - source + asset: + type: object + properties: + criticality: + $ref: '../../asset_criticality/common.schema.yaml#/components/schemas/AssetCriticalityLevel' + required: + - criticality HostEntity: type: object properties: @@ -113,8 +125,11 @@ components: type: array items: type: string + risk: + $ref: '../../common/common.schema.yaml#/components/schemas/EntityRiskScoreRecord' required: - name + entity: type: object properties: @@ -142,6 +157,8 @@ components: format: date-time definitionId: type: string + source: + type: string required: - lastSeenTimestamp - schemaVersion @@ -152,6 +169,13 @@ components: - type - firstSeenTimestamp - definitionId + asset: + type: object + properties: + criticality: + $ref: '../../asset_criticality/common.schema.yaml#/components/schemas/AssetCriticalityLevel' + required: + - criticality Entity: oneOf: diff --git a/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml b/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml index 372793a1ffb0a..e0605a637d679 100644 --- a/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml @@ -644,6 +644,95 @@ components: oneOf: - $ref: '#/components/schemas/UserEntity' - $ref: '#/components/schemas/HostEntity' + EntityRiskLevels: + enum: + - Unknown + - Low + - Moderate + - High + - Critical + type: string + EntityRiskScoreRecord: + type: object + properties: + '@timestamp': + description: The time at which the risk score was calculated. + example: '2017-07-21T17:32:28Z' + format: date-time + type: string + calculated_level: + $ref: '#/components/schemas/EntityRiskLevels' + description: Lexical description of the entity's risk. + example: Critical + calculated_score: + description: The raw numeric value of the given entity's risk score. + format: double + type: number + calculated_score_norm: + description: >- + The normalized numeric value of the given entity's risk score. + Useful for comparing with other entities. + format: double + maximum: 100 + minimum: 0 + type: number + category_1_count: + description: >- + The number of risk input documents that contributed to the Category + 1 score (`category_1_score`). + format: integer + type: number + category_1_score: + description: >- + The contribution of Category 1 to the overall risk score + (`calculated_score`). Category 1 contains Detection Engine Alerts. + format: double + type: number + category_2_count: + format: integer + type: number + category_2_score: + format: double + type: number + criticality_level: + $ref: '#/components/schemas/AssetCriticalityLevel' + criticality_modifier: + format: double + type: number + id_field: + description: >- + The identifier field defining this risk score. Coupled with + `id_value`, uniquely identifies the entity being scored. + example: host.name + type: string + id_value: + description: >- + The identifier value defining this risk score. Coupled with + `id_field`, uniquely identifies the entity being scored. + example: example.host + type: string + inputs: + description: >- + A list of the highest-risk documents contributing to this risk + score. Useful for investigative purposes. + items: + $ref: '#/components/schemas/RiskScoreInput' + type: array + notes: + items: + type: string + type: array + required: + - '@timestamp' + - id_field + - id_value + - calculated_level + - calculated_score + - calculated_score_norm + - category_1_score + - category_1_count + - inputs + - notes EntityType: enum: - user @@ -652,6 +741,13 @@ components: HostEntity: type: object properties: + asset: + type: object + properties: + criticality: + $ref: '#/components/schemas/AssetCriticalityLevel' + required: + - criticality entity: type: object properties: @@ -675,6 +771,8 @@ components: type: string schemaVersion: type: string + source: + type: string type: enum: - node @@ -718,6 +816,8 @@ components: type: array name: type: string + risk: + $ref: '#/components/schemas/EntityRiskScoreRecord' type: items: type: string @@ -760,6 +860,44 @@ components: properties: success: type: boolean + RiskScoreInput: + description: A generic representation of a document contributing to a Risk Score. + type: object + properties: + category: + description: The risk category of the risk input document. + example: category_1 + type: string + contribution_score: + format: double + type: number + description: + description: A human-readable description of the risk input document. + example: 'Generated from Detection Engine Rule: Malware Prevention Alert' + type: string + id: + description: The unique identifier (`_id`) of the original source document + example: 91a93376a507e86cfbf282166275b89f9dbdb1f0be6c8103c6ff2909ca8e1a1c + type: string + index: + description: The unique index (`_index`) of the original source document + example: .internal.alerts-security.alerts-default-000001 + type: string + risk_score: + description: The weighted risk score of the risk input document. + format: double + maximum: 100 + minimum: 0 + type: number + timestamp: + description: The @timestamp of the risk input document. + example: '2017-07-21T17:32:28Z' + type: string + required: + - id + - index + - description + - category TaskManagerUnavailableResponse: description: Task manager is unavailable type: object @@ -775,6 +913,13 @@ components: UserEntity: type: object properties: + asset: + type: object + properties: + criticality: + $ref: '#/components/schemas/AssetCriticalityLevel' + required: + - criticality entity: type: object properties: @@ -798,6 +943,8 @@ components: type: string schemaVersion: type: string + source: + type: string type: enum: - node @@ -812,6 +959,7 @@ components: - type - firstSeenTimestamp - definitionId + - source user: type: object properties: @@ -837,6 +985,8 @@ components: type: array name: type: string + risk: + $ref: '#/components/schemas/EntityRiskScoreRecord' roles: items: type: string diff --git a/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml b/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml index 50cfd4e893ca7..b08163eed9f0d 100644 --- a/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml @@ -644,6 +644,95 @@ components: oneOf: - $ref: '#/components/schemas/UserEntity' - $ref: '#/components/schemas/HostEntity' + EntityRiskLevels: + enum: + - Unknown + - Low + - Moderate + - High + - Critical + type: string + EntityRiskScoreRecord: + type: object + properties: + '@timestamp': + description: The time at which the risk score was calculated. + example: '2017-07-21T17:32:28Z' + format: date-time + type: string + calculated_level: + $ref: '#/components/schemas/EntityRiskLevels' + description: Lexical description of the entity's risk. + example: Critical + calculated_score: + description: The raw numeric value of the given entity's risk score. + format: double + type: number + calculated_score_norm: + description: >- + The normalized numeric value of the given entity's risk score. + Useful for comparing with other entities. + format: double + maximum: 100 + minimum: 0 + type: number + category_1_count: + description: >- + The number of risk input documents that contributed to the Category + 1 score (`category_1_score`). + format: integer + type: number + category_1_score: + description: >- + The contribution of Category 1 to the overall risk score + (`calculated_score`). Category 1 contains Detection Engine Alerts. + format: double + type: number + category_2_count: + format: integer + type: number + category_2_score: + format: double + type: number + criticality_level: + $ref: '#/components/schemas/AssetCriticalityLevel' + criticality_modifier: + format: double + type: number + id_field: + description: >- + The identifier field defining this risk score. Coupled with + `id_value`, uniquely identifies the entity being scored. + example: host.name + type: string + id_value: + description: >- + The identifier value defining this risk score. Coupled with + `id_field`, uniquely identifies the entity being scored. + example: example.host + type: string + inputs: + description: >- + A list of the highest-risk documents contributing to this risk + score. Useful for investigative purposes. + items: + $ref: '#/components/schemas/RiskScoreInput' + type: array + notes: + items: + type: string + type: array + required: + - '@timestamp' + - id_field + - id_value + - calculated_level + - calculated_score + - calculated_score_norm + - category_1_score + - category_1_count + - inputs + - notes EntityType: enum: - user @@ -652,6 +741,13 @@ components: HostEntity: type: object properties: + asset: + type: object + properties: + criticality: + $ref: '#/components/schemas/AssetCriticalityLevel' + required: + - criticality entity: type: object properties: @@ -675,6 +771,8 @@ components: type: string schemaVersion: type: string + source: + type: string type: enum: - node @@ -718,6 +816,8 @@ components: type: array name: type: string + risk: + $ref: '#/components/schemas/EntityRiskScoreRecord' type: items: type: string @@ -760,6 +860,44 @@ components: properties: success: type: boolean + RiskScoreInput: + description: A generic representation of a document contributing to a Risk Score. + type: object + properties: + category: + description: The risk category of the risk input document. + example: category_1 + type: string + contribution_score: + format: double + type: number + description: + description: A human-readable description of the risk input document. + example: 'Generated from Detection Engine Rule: Malware Prevention Alert' + type: string + id: + description: The unique identifier (`_id`) of the original source document + example: 91a93376a507e86cfbf282166275b89f9dbdb1f0be6c8103c6ff2909ca8e1a1c + type: string + index: + description: The unique index (`_index`) of the original source document + example: .internal.alerts-security.alerts-default-000001 + type: string + risk_score: + description: The weighted risk score of the risk input document. + format: double + maximum: 100 + minimum: 0 + type: number + timestamp: + description: The @timestamp of the risk input document. + example: '2017-07-21T17:32:28Z' + type: string + required: + - id + - index + - description + - category TaskManagerUnavailableResponse: description: Task manager is unavailable type: object @@ -775,6 +913,13 @@ components: UserEntity: type: object properties: + asset: + type: object + properties: + criticality: + $ref: '#/components/schemas/AssetCriticalityLevel' + required: + - criticality entity: type: object properties: @@ -798,6 +943,8 @@ components: type: string schemaVersion: type: string + source: + type: string type: enum: - node @@ -812,6 +959,7 @@ components: - type - firstSeenTimestamp - definitionId + - source user: type: object properties: @@ -837,6 +985,8 @@ components: type: array name: type: string + risk: + $ref: '#/components/schemas/EntityRiskScoreRecord' roles: items: type: string diff --git a/x-pack/plugins/security_solution/public/common/components/multiselect_filter/index.tsx b/x-pack/plugins/security_solution/public/common/components/multiselect_filter/index.tsx new file mode 100644 index 0000000000000..cfdd1401f5c79 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/multiselect_filter/index.tsx @@ -0,0 +1,122 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useMemo } from 'react'; +import { noop } from 'lodash'; +import type { EuiSelectableProps, FilterChecked } from '@elastic/eui'; +import { EuiPopover, EuiFilterButton, EuiSelectable } from '@elastic/eui'; +import { useBoolState } from '../../hooks/use_bool_state'; + +export interface MultiselectFilterProps { + ['data-test-subj']?: string; + title: string; + items: T[]; + selectedItems: T[]; + onSelectionChange?: (selectedItems: T[], changedOption: T, changedStatus: FilterChecked) => void; + renderItem?: (item: T) => React.ReactChild; + renderLabel?: (item: T) => string; + /** + * Width of the popover content. If undefined, the popover will take the width of the button. + * https://eui.elastic.co/#/forms/selectable#sizing-and-containers + */ + width?: number; +} + +interface MultiselectFilterOption { + originalItem: T; + label: string; + checked?: FilterChecked; +} + +/** + * Please use this component instead of [MultiselectFilter](../../../detection_engine/rule_monitoring/components/basic/filters/multiselect_filter/index.tsx) + * + * The new version uses `EuiSelectable` instead of the deprecated `EuiFilterSelectItem`. That required a width param, which wasn't previously necessary. + * It also exports the filter without the `EuiFilterGroup` wrapper, allowing several filters to be grouped. + * + * To migrate to the new version, you need to: + * 1. Wrap it with EuiFilterGroup + * @example + * + * + * ` + * + * 2. Provide the desired width + * @example + * width={150} /> + */ +const MultiselectFilterComponent = ({ + 'data-test-subj': dataTestSubj = 'multiselectFilter', + title, + items, + selectedItems, + width, + onSelectionChange = noop, + renderLabel = String, + renderItem = renderLabel, +}: MultiselectFilterProps) => { + const [isPopoverOpen, _unused, closePopover, togglePopover] = useBoolState(); + + const options: Array> = useMemo(() => { + const checked: FilterChecked = 'on'; + return items.map((item) => ({ + originalItem: item, + label: renderLabel(item), + checked: selectedItems.includes(item) ? checked : undefined, + })); + }, [items, renderLabel, selectedItems]); + + const onChange = useCallback< + NonNullable>['onChange']> + >( + (newItems, _event, changedOption) => { + onSelectionChange( + newItems.filter(({ checked }) => checked === 'on').map(({ originalItem }) => originalItem), + changedOption.originalItem, + changedOption.checked ?? 'off' + ); + }, + [onSelectionChange] + ); + + return ( + 0} + isSelected={isPopoverOpen} + onClick={togglePopover} + > + {title} + + } + isOpen={isPopoverOpen} + closePopover={closePopover} + panelPaddingSize="none" + repositionOnScroll + > + renderItem(originalItem)} + > + {(list) =>
{list}
} +
+
+ ); +}; + +export const MultiselectFilter = React.memo( + MultiselectFilterComponent +) as typeof MultiselectFilterComponent; // The cast is necessary because React.memo does not support generics diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/entity_analytics/index.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/entity_analytics/index.ts index a3ea313ac20c1..aedbc7eb01fae 100644 --- a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/entity_analytics/index.ts +++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/entity_analytics/index.ts @@ -41,7 +41,7 @@ export const entityRiskFilteredEvent: TelemetryEvent = { type: 'keyword', _meta: { description: 'Entity name (host|user)', - optional: false, + optional: true, }, }, selectedSeverity: { diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/entity_analytics/types.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/entity_analytics/types.ts index d71c48004d756..91a71a7dacca2 100644 --- a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/entity_analytics/types.ts +++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/entity_analytics/types.ts @@ -15,7 +15,7 @@ interface EntityParam { export type ReportEntityDetailsClickedParams = EntityParam; export type ReportEntityAlertsClickedParams = EntityParam; -export interface ReportEntityRiskFilteredParams extends EntityParam { +export interface ReportEntityRiskFilteredParams extends Partial { selectedSeverity: RiskSeverity; } diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/filters/multiselect_filter/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/filters/multiselect_filter/index.tsx index e93942d5a7257..bc2778b7b770f 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/filters/multiselect_filter/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/filters/multiselect_filter/index.tsx @@ -22,6 +22,9 @@ export interface MultiselectFilterProps { } /* eslint-enable react/no-unused-prop-types */ +/** + * @deprecated Please use [MultiselectFilter](../../../../../../common/components/multiselect_filter/index.tsx) instead. + */ const MultiselectFilterComponent = (props: MultiselectFilterProps) => { const { dataTestSubj, title, items, selectedItems, onSelectionChange, renderItem, renderLabel } = initializeProps(props); diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality/asset_criticality_filter.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality/asset_criticality_filter.tsx new file mode 100644 index 0000000000000..0a92dd9b61189 --- /dev/null +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality/asset_criticality_filter.tsx @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback } from 'react'; +import { i18n } from '@kbn/i18n'; +import { MultiselectFilter } from '../../../common/components/multiselect_filter'; +import { CriticalityLevels } from '../../../../common/constants'; +import { AssetCriticalityBadge } from './asset_criticality_badge'; + +interface AssetCriticalityFilterProps { + selectedItems: CriticalityLevels[]; + onChange: (selectedItems: CriticalityLevels[]) => void; +} + +const ASSET_CRITICALITY_OPTIONS = [ + CriticalityLevels.EXTREME_IMPACT, + CriticalityLevels.HIGH_IMPACT, + CriticalityLevels.MEDIUM_IMPACT, + CriticalityLevels.LOW_IMPACT, +]; + +export const AssetCriticalityFilter: React.FC = ({ + selectedItems, + onChange, +}) => { + const renderItem = useCallback((level: CriticalityLevels) => { + return ; + }, []); + + return ( + + title={i18n.translate('xpack.securitySolution.entityAnalytics.assetCriticality.filterTitle', { + defaultMessage: 'Criticality', + })} + items={ASSET_CRITICALITY_OPTIONS} + selectedItems={selectedItems} + onSelectionChange={onChange} + renderItem={renderItem} + width={190} + /> + ); +}; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_analytics_risk_score/header_content.test.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_analytics_risk_score/header_content.test.tsx index b4f24e0d27d6f..797ce6bf21b83 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_analytics_risk_score/header_content.test.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_analytics_risk_score/header_content.test.tsx @@ -11,7 +11,6 @@ import { SecurityPageName } from '../../../../common/constants'; import { RiskScoreEntity } from '../../../../common/search_strategy'; import { useGetSecuritySolutionLinkProps } from '../../../common/components/links'; import { RiskScoreHeaderContent } from './header_content'; -import { mockSeverityCount } from './__mocks__'; jest.mock('../../../common/components/links', () => { const actual = jest.requireActual('../../../common/components/links'); @@ -37,10 +36,9 @@ describe('RiskScoreHeaderContent', () => { onClick: jest.fn(), path: '/userRisk', }} - onSelectSeverityFilterGroup={jest.fn()} + onSelectSeverityFilter={jest.fn()} riskEntity={RiskScoreEntity.user} selectedSeverity={[]} - severityCount={mockSeverityCount} toggleStatus={true} /> ); @@ -54,7 +52,7 @@ describe('RiskScoreHeaderContent', () => { }); it('should render severity filter group', () => { - expect(res.getByTestId(`risk-filter-button`)).toBeInTheDocument(); + expect(res.getByTestId(`risk-filter`)).toBeInTheDocument(); }); it('should render view all button', () => { @@ -69,10 +67,9 @@ describe('RiskScoreHeaderContent', () => { onClick: jest.fn(), path: '/userRisk', }} - onSelectSeverityFilterGroup={jest.fn()} + onSelectSeverityFilter={jest.fn()} riskEntity={RiskScoreEntity.user} selectedSeverity={[]} - severityCount={mockSeverityCount} toggleStatus={false} /> ); diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_analytics_risk_score/header_content.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_analytics_risk_score/header_content.tsx index 10b94b95a886f..409b846831007 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_analytics_risk_score/header_content.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_analytics_risk_score/header_content.tsx @@ -5,11 +5,9 @@ * 2.0. */ import React, { useMemo } from 'react'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiFilterGroup, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import type { RiskSeverity, RiskScoreEntity } from '../../../../common/search_strategy'; -import { SeverityFilterGroup } from '../severity/severity_filter_group'; -import type { SeverityCount } from '../severity/types'; -import { EMPTY_SEVERITY_COUNT } from '../../../../common/search_strategy'; +import { SeverityFilter } from '../severity/severity_filter'; import { LinkButton, useGetSecuritySolutionLinkProps } from '../../../common/components/links'; import type { SecurityPageName } from '../../../../common/constants'; import * as i18n from './translations'; @@ -17,10 +15,9 @@ import { RiskInformationButtonEmpty } from '../risk_information'; const RiskScoreHeaderContentComponent = ({ entityLinkProps, - onSelectSeverityFilterGroup, + onSelectSeverityFilter, riskEntity, selectedSeverity, - severityCount, toggleStatus, }: { entityLinkProps: { @@ -28,10 +25,9 @@ const RiskScoreHeaderContentComponent = ({ path: string; onClick: () => void; }; - onSelectSeverityFilterGroup: (newSelection: RiskSeverity[]) => void; + onSelectSeverityFilter: (newSelection: RiskSeverity[]) => void; riskEntity: RiskScoreEntity; selectedSeverity: RiskSeverity[]; - severityCount: SeverityCount | undefined; toggleStatus: boolean; }) => { const getSecuritySolutionLinkProps = useGetSecuritySolutionLinkProps(); @@ -50,12 +46,13 @@ const RiskScoreHeaderContentComponent = ({
- + + + ([]); - const onSelectSeverityFilterGroup = useCallback((newSelection: RiskSeverity[]) => { + const onSelectSeverityFilter = useCallback((newSelection: RiskSeverity[]) => { setSelectedSeverity(newSelection); }, []); @@ -209,10 +209,9 @@ const EntityAnalyticsRiskScoresComponent = ({ riskEntity }: { riskEntity: RiskSc > diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/entity_source_filter.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/entity_source_filter.tsx new file mode 100644 index 0000000000000..b324adca0945e --- /dev/null +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/entity_source_filter.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { MultiselectFilter } from '../../../../common/components/multiselect_filter'; + +interface SourceFilterProps { + selectedItems: EntitySource[]; + onChange: (selectedItems: EntitySource[]) => void; +} + +export enum EntitySource { + CSV_UPLOAD = 'CSV upload', + EVENTS = 'Events', +} + +export const EntitySourceFilter: React.FC = ({ selectedItems, onChange }) => { + return ( + + title={i18n.translate( + 'xpack.securitySolution.entityAnalytics.entityStore.entitySource.filterTitle', + { + defaultMessage: 'Source', + } + )} + items={Object.values(EntitySource)} + selectedItems={selectedItems} + onSelectionChange={onChange} + width={140} + /> + ); +}; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/constants.ts b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/constants.ts new file mode 100644 index 0000000000000..79c0e3e647d09 --- /dev/null +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/constants.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import type { ItemsPerRow } from '../../../explore/components/paginated_table'; + +export const ENTITIES_LIST_TABLE_ID = 'EntitiesList-table'; + +export const rowItems: ItemsPerRow[] = [ + { + text: i18n.translate('xpack.securitySolution.entityAnalytics.entityStore.entitiesList.rows', { + values: { numRows: 5 }, + defaultMessage: '{numRows} {numRows, plural, =0 {rows} =1 {row} other {rows}}', + }), + numberOfRow: 5, + }, + { + text: i18n.translate('xpack.securitySolution.entityAnalytics.entityStore.entitiesList.rows', { + values: { numRows: 10 }, + defaultMessage: '{numRows} {numRows, plural, =0 {rows} =1 {row} other {rows}}', + }), + numberOfRow: 10, + }, +]; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/entities_list.test.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/entities_list.test.tsx new file mode 100644 index 0000000000000..75fd594dc3581 --- /dev/null +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/entities_list.test.tsx @@ -0,0 +1,138 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { EntitiesList } from './entities_list'; +import { useGlobalTime } from '../../../common/containers/use_global_time'; +import { useQueryToggle } from '../../../common/containers/query_toggle'; +import { useEntitiesListQuery } from './hooks/use_entities_list_query'; +import { useErrorToast } from '../../../common/hooks/use_error_toast'; +import type { ListEntitiesResponse } from '../../../../common/api/entity_analytics/entity_store/entities/list_entities.gen'; +import { useGlobalFilterQuery } from '../../../common/hooks/use_global_filter_query'; +import { TestProviders } from '../../../common/mock'; + +jest.mock('../../../common/containers/use_global_time'); +jest.mock('../../../common/containers/query_toggle'); +jest.mock('./hooks/use_entities_list_query'); +jest.mock('../../../common/hooks/use_error_toast'); +jest.mock('../../../common/hooks/use_global_filter_query'); + +const entityName = 'Entity Name'; +const responseData: ListEntitiesResponse = { + page: 1, + per_page: 10, + total: 1, + records: [ + { + user: { name: entityName }, + entity: { + type: 'node', + id: 'entity-id', + lastSeenTimestamp: '2023-01-01T00:00:00Z', + schemaVersion: '1.0', + definitionVersion: '1.0', + displayName: entityName, + identityFields: ['field1', 'field2'], + firstSeenTimestamp: '2023-01-01T00:00:00Z', + definitionId: 'definition-id', + source: 'source', + }, + }, + ], + inspect: undefined, +}; + +describe('EntitiesList', () => { + const mockUseGlobalTime = useGlobalTime as jest.Mock; + const mockUseQueryToggle = useQueryToggle as jest.Mock; + const mockUseEntitiesListQuery = useEntitiesListQuery as jest.Mock; + const mockUseErrorToast = useErrorToast as jest.Mock; + const mockUseGlobalFilterQuery = useGlobalFilterQuery as jest.Mock; + const mockRefech = jest.fn(); + + beforeEach(() => { + mockUseGlobalTime.mockReturnValue({ + deleteQuery: jest.fn(), + setQuery: jest.fn(), + isInitializing: false, + from: 'now-15m', + to: 'now', + }); + + mockUseQueryToggle.mockReturnValue({ + toggleStatus: true, + }); + + mockUseEntitiesListQuery.mockReturnValue({ + data: responseData, + isLoading: false, + isRefetching: false, + refetch: mockRefech, + error: null, + }); + + mockUseErrorToast.mockReturnValue(jest.fn()); + + mockUseGlobalFilterQuery.mockReturnValue({ filterQuery: null }); + }); + + it('renders the component', () => { + const { getByText } = render(, { wrapper: TestProviders }); + expect(getByText(entityName)).toBeInTheDocument(); + }); + + it('displays the correct number of rows', () => { + render(, { wrapper: TestProviders }); + expect(screen.getAllByRole('row')).toHaveLength(2); + }); + + it('calls refetch on time range change', () => { + const { rerender } = render(, { wrapper: TestProviders }); + mockUseGlobalTime.mockReturnValueOnce({ + deleteQuery: jest.fn(), + setQuery: jest.fn(), + isInitializing: false, + from: 'now-30m', + to: 'now', + }); + mockRefech.mockClear(); + + rerender(); + + expect(mockRefech).toHaveBeenCalled(); + }); + + it('updates sorting when column header is clicked', () => { + render(, { wrapper: TestProviders }); + const columnHeader = screen.getByText('Name'); + fireEvent.click(columnHeader); + expect(mockUseEntitiesListQuery).toHaveBeenCalledWith( + expect.objectContaining({ + sortField: 'entity.displayName.keyword', + sortOrder: 'asc', + }) + ); + }); + + it('displays error toast when there is an error', () => { + const error = new Error('Test error'); + mockUseEntitiesListQuery.mockReturnValueOnce({ + data: null, + isLoading: false, + isRefetching: false, + refetch: jest.fn(), + error, + }); + + render(, { wrapper: TestProviders }); + expect(mockUseErrorToast).toHaveBeenCalledWith( + 'There was an error loading the entities list', + error + ); + }); +}); diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/entities_list.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/entities_list.tsx new file mode 100644 index 0000000000000..c02cbbb930c5c --- /dev/null +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/entities_list.tsx @@ -0,0 +1,158 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { EuiFilterGroup, EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; +import { noop } from 'lodash/fp'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { i18n } from '@kbn/i18n'; +import { useErrorToast } from '../../../common/hooks/use_error_toast'; +import type { CriticalityLevels } from '../../../../common/constants'; +import type { RiskSeverity } from '../../../../common/search_strategy'; +import { useQueryInspector } from '../../../common/components/page/manage_query'; +import { Direction } from '../../../../common/search_strategy/common'; +import { useGlobalTime } from '../../../common/containers/use_global_time'; +import { useQueryToggle } from '../../../common/containers/query_toggle'; +import { EntityType } from '../../../../common/api/entity_analytics/entity_store/common.gen'; +import type { Criteria } from '../../../explore/components/paginated_table'; +import { PaginatedTable } from '../../../explore/components/paginated_table'; +import { SeverityFilter } from '../severity/severity_filter'; +import type { EntitySource } from './components/entity_source_filter'; +import { EntitySourceFilter } from './components/entity_source_filter'; +import { useEntitiesListFilters } from './hooks/use_entities_list_filters'; +import { AssetCriticalityFilter } from '../asset_criticality/asset_criticality_filter'; +import { useEntitiesListQuery } from './hooks/use_entities_list_query'; +import { ENTITIES_LIST_TABLE_ID, rowItems } from './constants'; +import { useEntitiesListColumns } from './hooks/use_entities_list_columns'; + +export const EntitiesList: React.FC = () => { + const { deleteQuery, setQuery, isInitializing, from, to } = useGlobalTime(); + const [activePage, setActivePage] = useState(0); + const [limit, setLimit] = useState(10); + const { toggleStatus } = useQueryToggle(ENTITIES_LIST_TABLE_ID); + const [sorting, setSorting] = useState({ + field: 'entity.lastSeenTimestamp', + direction: Direction.desc, + }); + + const [selectedSeverities, setSelectedSeverities] = useState([]); + const [selectedCriticalities, setSelectedCriticalities] = useState([]); + const [selectedSources, setSelectedSources] = useState([]); + + const filter = useEntitiesListFilters({ + selectedSeverities, + selectedCriticalities, + selectedSources, + }); + + const [querySkip, setQuerySkip] = useState(isInitializing || !toggleStatus); + useEffect(() => { + if (!isInitializing) { + setQuerySkip(isInitializing || !toggleStatus); + } + }, [isInitializing, toggleStatus]); + + const onSort = useCallback( + (criteria: Criteria) => { + if (criteria.sort != null) { + const newSort = criteria.sort; + if (newSort.direction !== sorting.direction || newSort.field !== sorting.field) { + setSorting(newSort); + } + } + }, + [setSorting, sorting] + ); + + const searchParams = useMemo( + () => ({ + entitiesTypes: [EntityType.Enum.user, EntityType.Enum.host], + page: activePage + 1, + perPage: limit, + sortField: sorting.field, + sortOrder: sorting.direction, + skip: querySkip, + filterQuery: JSON.stringify({ + bool: { + filter, + }, + }), + }), + [activePage, limit, querySkip, sorting, filter] + ); + const { data, isLoading, isRefetching, refetch, error } = useEntitiesListQuery(searchParams); + + useQueryInspector({ + queryId: ENTITIES_LIST_TABLE_ID, + loading: isLoading || isRefetching, + refetch, + setQuery, + deleteQuery, + inspect: data?.inspect ?? null, + }); + + const columns = useEntitiesListColumns(); + + // Force a refetch when "refresh" button is clicked. + // If we implement the timerange filter we can get rid of this code + useEffect(() => { + refetch(); + }, [from, to, refetch]); + + useErrorToast( + i18n.translate('xpack.securitySolution.entityAnalytics.entityStore.entitiesList.queryError', { + defaultMessage: 'There was an error loading the entities list', + }), + error + ); + + return ( + +

+ +

+ + } + limit={limit} + loading={isLoading || isRefetching} + isInspect={false} + updateActivePage={setActivePage} + loadPage={noop} // It isn't necessary because the page loads when activePage changes + pageOfItems={data?.records ?? []} + setQuerySkip={setQuerySkip} + showMorePagesIndicator={false} + updateLimitPagination={setLimit} + totalCount={data?.total ?? 0} + itemsPerRow={rowItems} + sorting={sorting} + onChange={onSort} + headerFilters={ + + + + + + + + + + } + /> + ); +}; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/helpers.test.ts b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/helpers.test.ts new file mode 100644 index 0000000000000..bfe515ae47534 --- /dev/null +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/helpers.test.ts @@ -0,0 +1,34 @@ +/* + * 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 { isUserEntity } from './helpers'; +import type { + Entity, + UserEntity, +} from '../../../../common/api/entity_analytics/entity_store/entities/common.gen'; + +describe('isUserEntity', () => { + it('should return true if the record is a UserEntity', () => { + const userEntity: UserEntity = { + user: { + name: 'test_user', + }, + }; + + expect(isUserEntity(userEntity)).toBe(true); + }); + + it('should return false if the record is not a UserEntity', () => { + const nonUserEntity: Entity = { + host: { + name: 'test_host', + }, + }; + + expect(isUserEntity(nonUserEntity)).toBe(false); + }); +}); diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/helpers.ts b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/helpers.ts new file mode 100644 index 0000000000000..61e9b2be8b0ab --- /dev/null +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/helpers.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { + Entity, + UserEntity, +} from '../../../../common/api/entity_analytics/entity_store/entities/common.gen'; + +export const isUserEntity = (record: Entity): record is UserEntity => + !!(record as UserEntity)?.user; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/hooks/use_entities_list_columns.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/hooks/use_entities_list_columns.tsx new file mode 100644 index 0000000000000..5b64c8e8e2733 --- /dev/null +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/hooks/use_entities_list_columns.tsx @@ -0,0 +1,191 @@ +/* + * 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 { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; +import { EuiButtonIcon, EuiIcon, useEuiTheme } from '@elastic/eui'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { UserPanelKey } from '../../../../flyout/entity_details/user_right'; +import { HostPanelKey } from '../../../../flyout/entity_details/host_right'; +import { FormattedRelativePreferenceDate } from '../../../../common/components/formatted_date'; +import { RiskScoreLevel } from '../../severity/common'; +import { getEmptyTagValue } from '../../../../common/components/empty_value'; +import type { Columns } from '../../../../explore/components/paginated_table'; +import type { Entity } from '../../../../../common/api/entity_analytics/entity_store/entities/common.gen'; +import type { CriticalityLevels } from '../../../../../common/constants'; +import { ENTITIES_LIST_TABLE_ID } from '../constants'; +import { isUserEntity } from '../helpers'; + +export type EntitiesListColumns = [ + Columns, + Columns, + Columns, + Columns, + Columns, + Columns, + Columns +]; + +export const useEntitiesListColumns = (): EntitiesListColumns => { + const { openRightPanel } = useExpandableFlyoutApi(); + const { euiTheme } = useEuiTheme(); + + return [ + { + name: ( + + ), + + render: (record: Entity) => { + const field = record.entity?.identityFields[0]; + const value = record.entity?.displayName; + const onClick = () => { + const id = isUserEntity(record) ? UserPanelKey : HostPanelKey; + const params = { + [isUserEntity(record) ? 'userName' : 'hostName']: value, + contextID: ENTITIES_LIST_TABLE_ID, + scopeId: ENTITIES_LIST_TABLE_ID, + }; + + openRightPanel({ id, params }); + }; + + if (!field || !value) { + return null; + } + + return ( + + ); + }, + width: '5%', + }, + { + field: 'entity.displayName.keyword', + name: ( + + ), + sortable: true, + render: (_: string, record: Entity) => { + return ( + + {isUserEntity(record) ? : } + {record.entity?.displayName} + + ); + }, + width: '30%', + }, + { + field: 'entity.source', + name: ( + + ), + width: '10%', + render: (source: string | undefined) => { + if (source != null) { + return {source}; + } + + return getEmptyTagValue(); + }, + }, + { + field: 'asset.criticality', + name: ( + + ), + width: '10%', + render: (criticality: CriticalityLevels) => { + if (criticality != null) { + return criticality; + } + + return getEmptyTagValue(); + }, + }, + { + name: ( + + ), + width: '10%', + render: (entity: Entity) => { + const riskScore = isUserEntity(entity) + ? entity.user?.risk?.calculated_score_norm + : entity.host?.risk?.calculated_score_norm; + + if (riskScore != null) { + return ( + + {Math.round(riskScore)} + + ); + } + return getEmptyTagValue(); + }, + }, + { + name: ( + + ), + width: '10%', + render: (entity: Entity) => { + const riskLevel = isUserEntity(entity) + ? entity.user?.risk?.calculated_level + : entity.host?.risk?.calculated_level; + + if (riskLevel != null) { + return ; + } + return getEmptyTagValue(); + }, + }, + { + field: 'entity.lastSeenTimestamp', + name: ( + + ), + sortable: true, + render: (lastUpdate: string) => { + return ; + }, + width: '25%', + }, + ]; +}; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/hooks/use_entities_list_filters.test.ts b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/hooks/use_entities_list_filters.test.ts new file mode 100644 index 0000000000000..f2fcd3e4f7685 --- /dev/null +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/hooks/use_entities_list_filters.test.ts @@ -0,0 +1,148 @@ +/* + * 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 { renderHook } from '@testing-library/react-hooks'; +import { useEntitiesListFilters } from './use_entities_list_filters'; +import { useGlobalFilterQuery } from '../../../../common/hooks/use_global_filter_query'; +import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; +import { CriticalityLevels } from '../../../../../common/constants'; +import { RiskSeverity } from '../../../../../common/search_strategy'; +import { EntitySource } from '../components/entity_source_filter'; + +jest.mock('../../../../common/hooks/use_global_filter_query'); + +describe('useEntitiesListFilters', () => { + const mockUseGlobalFilterQuery = useGlobalFilterQuery as jest.Mock; + + beforeEach(() => { + mockUseGlobalFilterQuery.mockReturnValue({ filterQuery: null }); + }); + + it('should return empty array when no filters are selected', () => { + const { result } = renderHook(() => + useEntitiesListFilters({ + selectedSeverities: [], + selectedCriticalities: [], + selectedSources: [], + }) + ); + + expect(result.current).toEqual([]); + }); + + it('should return severity filters when severities are selected', () => { + const { result } = renderHook(() => + useEntitiesListFilters({ + selectedSeverities: [RiskSeverity.Low, RiskSeverity.High], + selectedCriticalities: [], + selectedSources: [], + }) + ); + + const expectedFilters: QueryDslQueryContainer[] = [ + { + bool: { + should: [ + { term: { 'host.risk.calculated_level': RiskSeverity.Low } }, + { term: { 'user.risk.calculated_level': RiskSeverity.Low } }, + ], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [ + { term: { 'host.risk.calculated_level': RiskSeverity.High } }, + { term: { 'user.risk.calculated_level': RiskSeverity.High } }, + ], + minimum_should_match: 1, + }, + }, + ]; + + expect(result.current).toEqual(expectedFilters); + }); + + it('should return criticality filters when criticalities are selected', () => { + const { result } = renderHook(() => + useEntitiesListFilters({ + selectedSeverities: [], + selectedCriticalities: [CriticalityLevels.EXTREME_IMPACT, CriticalityLevels.MEDIUM_IMPACT], + selectedSources: [], + }) + ); + + const expectedFilters: QueryDslQueryContainer[] = [ + { term: { 'asset.criticality': CriticalityLevels.EXTREME_IMPACT } }, + { term: { 'asset.criticality': CriticalityLevels.MEDIUM_IMPACT } }, + ]; + + expect(result.current).toEqual(expectedFilters); + }); + + it('should return source filters when sources are selected', () => { + const { result } = renderHook(() => + useEntitiesListFilters({ + selectedSeverities: [], + selectedCriticalities: [], + selectedSources: [EntitySource.CSV_UPLOAD, EntitySource.EVENTS], + }) + ); + + const expectedFilters: QueryDslQueryContainer[] = [ + { term: { 'entity.source': EntitySource.CSV_UPLOAD } }, + { term: { 'entity.source': EntitySource.EVENTS } }, + ]; + + expect(result.current).toEqual(expectedFilters); + }); + + it('should include global query if it exists', () => { + const globalQuery: QueryDslQueryContainer = { term: { 'global.field': 'value' } }; + mockUseGlobalFilterQuery.mockReturnValue({ filterQuery: globalQuery }); + + const { result } = renderHook(() => + useEntitiesListFilters({ + selectedSeverities: [], + selectedCriticalities: [], + selectedSources: [], + }) + ); + + expect(result.current).toEqual([globalQuery]); + }); + + it('should combine all filters', () => { + const globalQuery: QueryDslQueryContainer = { term: { 'global.field': 'value' } }; + mockUseGlobalFilterQuery.mockReturnValue({ filterQuery: globalQuery }); + + const { result } = renderHook(() => + useEntitiesListFilters({ + selectedSeverities: [RiskSeverity.Low], + selectedCriticalities: [CriticalityLevels.HIGH_IMPACT], + selectedSources: [EntitySource.CSV_UPLOAD], + }) + ); + + const expectedFilters: QueryDslQueryContainer[] = [ + { + bool: { + should: [ + { term: { 'host.risk.calculated_level': RiskSeverity.Low } }, + { term: { 'user.risk.calculated_level': RiskSeverity.Low } }, + ], + minimum_should_match: 1, + }, + }, + { term: { 'asset.criticality': CriticalityLevels.HIGH_IMPACT } }, + { term: { 'entity.source': EntitySource.CSV_UPLOAD } }, + globalQuery, + ]; + + expect(result.current).toEqual(expectedFilters); + }); +}); diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/hooks/use_entities_list_filters.ts b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/hooks/use_entities_list_filters.ts new file mode 100644 index 0000000000000..7e9c25441a501 --- /dev/null +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/hooks/use_entities_list_filters.ts @@ -0,0 +1,69 @@ +/* + * 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 { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; +import { useMemo } from 'react'; +import type { CriticalityLevels } from '../../../../../common/constants'; +import type { RiskSeverity } from '../../../../../common/search_strategy'; +import { useGlobalFilterQuery } from '../../../../common/hooks/use_global_filter_query'; +import type { EntitySource } from '../components/entity_source_filter'; + +interface UseEntitiesListFiltersParams { + selectedSeverities: RiskSeverity[]; + selectedCriticalities: CriticalityLevels[]; + selectedSources: EntitySource[]; +} + +export const useEntitiesListFilters = ({ + selectedSeverities, + selectedCriticalities, + selectedSources, +}: UseEntitiesListFiltersParams) => { + const { filterQuery: globalQuery } = useGlobalFilterQuery(); + + return useMemo(() => { + const criticalityFilter: QueryDslQueryContainer[] = selectedCriticalities.map((value) => ({ + term: { + 'asset.criticality': value, + }, + })); + + const sourceFilter: QueryDslQueryContainer[] = selectedSources.map((value) => ({ + term: { + 'entity.source': value, + }, + })); + + const severityFilter: QueryDslQueryContainer[] = selectedSeverities.map((value) => ({ + bool: { + should: [ + { + term: { + 'host.risk.calculated_level': value, + }, + }, + { + term: { + 'user.risk.calculated_level': value, + }, + }, + ], + minimum_should_match: 1, + }, + })); + + const filterList: QueryDslQueryContainer[] = [ + ...severityFilter, + ...criticalityFilter, + ...sourceFilter, + ]; + if (globalQuery) { + filterList.push(globalQuery); + } + return filterList; + }, [globalQuery, selectedCriticalities, selectedSeverities, selectedSources]); +}; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/hooks/use_entities_list_query.test.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/hooks/use_entities_list_query.test.tsx new file mode 100644 index 0000000000000..5f2363272791b --- /dev/null +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/hooks/use_entities_list_query.test.tsx @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook } from '@testing-library/react-hooks'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { useEntitiesListQuery } from './use_entities_list_query'; +import { useEntityAnalyticsRoutes } from '../../../api/api'; +import React from 'react'; + +jest.mock('../../../api/api'); + +describe('useEntitiesListQuery', () => { + const fetchEntitiesListMock = jest.fn(); + const TestWrapper = ({ children }: { children: React.ReactNode }) => ( + {children} + ); + + beforeEach(() => { + jest.clearAllMocks(); + (useEntityAnalyticsRoutes as jest.Mock).mockReturnValue({ + fetchEntitiesList: fetchEntitiesListMock, + }); + }); + + it('should call fetchEntitiesList with correct parameters', async () => { + const searchParams = { entitiesTypes: [], page: 7 }; + + fetchEntitiesListMock.mockResolvedValueOnce({ data: 'test data' }); + + const { result, waitFor } = renderHook( + () => useEntitiesListQuery({ ...searchParams, skip: false }), + { + wrapper: TestWrapper, + } + ); + + await waitFor(() => result.current.isSuccess); + + expect(fetchEntitiesListMock).toHaveBeenCalledWith({ params: searchParams }); + expect(result.current.data).toEqual({ data: 'test data' }); + }); + + it('should not call fetchEntitiesList if skip is true', async () => { + const searchParams = { entitiesTypes: [], page: 7 }; + + const { result } = renderHook(() => useEntitiesListQuery({ ...searchParams, skip: true }), { + wrapper: TestWrapper, + }); + + expect(fetchEntitiesListMock).not.toHaveBeenCalled(); + expect(result.current.data).toBeUndefined(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/hooks/use_entities_list_query.ts b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/hooks/use_entities_list_query.ts new file mode 100644 index 0000000000000..ae664b1c153de --- /dev/null +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/hooks/use_entities_list_query.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useQuery } from '@tanstack/react-query'; +import type { IHttpFetchError } from '@kbn/core/public'; +import type { ListEntitiesResponse } from '../../../../../common/api/entity_analytics/entity_store/entities/list_entities.gen'; +import type { FetchEntitiesListParams } from '../../../api/api'; +import { useEntityAnalyticsRoutes } from '../../../api/api'; + +const ENTITY_STORE_ENTITIES_LIST = 'ENTITY_STORE_ENTITIES_LIST'; + +interface UseEntitiesListParams extends FetchEntitiesListParams { + skip: boolean; +} + +export const useEntitiesListQuery = (params: UseEntitiesListParams) => { + const { skip, ...fetchParams } = params; + const { fetchEntitiesList } = useEntityAnalyticsRoutes(); + + return useQuery({ + queryKey: [ENTITY_STORE_ENTITIES_LIST, fetchParams], + queryFn: () => fetchEntitiesList({ params: fetchParams }), + cacheTime: 0, + enabled: !skip, + refetchOnWindowFocus: false, + keepPreviousData: true, + }); +}; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/host_risk_score_table/index.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/host_risk_score_table/index.tsx index a451c5030212d..ca815e4c255c4 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/host_risk_score_table/index.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/host_risk_score_table/index.tsx @@ -8,7 +8,7 @@ import React, { useMemo, useCallback } from 'react'; import { useDispatch } from 'react-redux'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiFilterGroup, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import type { Columns, Criteria, @@ -33,7 +33,7 @@ import * as i18nHosts from './translations'; import { SeverityBadges } from '../severity/severity_badges'; import { SeverityBar } from '../severity/severity_bar'; -import { SeverityFilterGroup } from '../severity/severity_filter_group'; +import { SeverityFilter } from '../severity/severity_filter'; import type { SeverityCount } from '../severity/types'; import { RiskInformationButtonEmpty } from '../risk_information'; @@ -185,12 +185,13 @@ const HostRiskScoreTableComponent: React.FC = ({
- + + +
} diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/risk_score/translations.ts b/x-pack/plugins/security_solution/public/entity_analytics/components/risk_score/translations.ts index 4ad2314afb859..f4e858163b782 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/risk_score/translations.ts +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/risk_score/translations.ts @@ -47,13 +47,20 @@ export const RISK_SCORING_TITLE = i18n.translate( } ); -export const ENTITY_RISK_LEVEL = (riskEntity: RiskScoreEntity) => - i18n.translate('xpack.securitySolution.entityAnalytics.riskDashboard.riskLevelTitle', { - defaultMessage: '{riskEntity} risk level', - values: { - riskEntity: getRiskEntityTranslation(riskEntity), - }, - }); +export const ENTITY_RISK_LEVEL = (riskEntity?: RiskScoreEntity) => + riskEntity + ? i18n.translate('xpack.securitySolution.entityAnalytics.riskDashboard.riskLevelTitle', { + defaultMessage: '{riskEntity} risk level', + values: { + riskEntity: getRiskEntityTranslation(riskEntity), + }, + }) + : i18n.translate( + 'xpack.securitySolution.entityAnalytics.riskDashboard.noEntity.riskLevelTitle', + { + defaultMessage: 'Risk level', + } + ); export const getRiskEntityTranslation = ( riskEntity?: RiskScoreEntity, diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/severity/severity_filter_group.test.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/severity/severity_filter.test.tsx similarity index 51% rename from x-pack/plugins/security_solution/public/entity_analytics/components/severity/severity_filter_group.test.tsx rename to x-pack/plugins/security_solution/public/entity_analytics/components/severity/severity_filter.test.tsx index 0ea834a4bfc95..8adbc2c7578df 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/severity/severity_filter_group.test.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/severity/severity_filter.test.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; import { render, fireEvent } from '@testing-library/react'; -import { SeverityFilterGroup } from './severity_filter_group'; +import { SeverityFilter } from './severity_filter'; import { RiskScoreEntity, RiskSeverity } from '../../../../common/search_strategy'; import { TestProviders } from '../../../common/mock'; import { createTelemetryServiceMock } from '../../../common/lib/telemetry/telemetry_service.mock'; @@ -22,85 +22,43 @@ jest.mock('../../../common/lib/kibana', () => { }; }); -describe('SeverityFilterGroup', () => { +describe('SeverityFilter', () => { beforeEach(() => { mockedTelemetry.reportEntityRiskFiltered.mockClear(); }); - it('preserves sort order when severityCount is out of order', () => { - const { getByTestId } = render( - - - - ); - - fireEvent.click(getByTestId('risk-filter-button')); - - expect(getByTestId('risk-filter-selectable').textContent).toEqual( - ['Unknown', 'Low', 'Moderate', 'High', 'Critical'].join('') - ); - }); - it('sends telemetry when selecting a classification', () => { const { getByTestId } = render( - + ); - fireEvent.click(getByTestId('risk-filter-button')); + fireEvent.click(getByTestId('risk-filter-popoverButton')); fireEvent.click(getByTestId('risk-filter-item-Unknown')); + expect(mockedTelemetry.reportEntityRiskFiltered).toHaveBeenCalledTimes(1); }); it('does not send telemetry when deselecting a classification', () => { const { getByTestId } = render( - ); - fireEvent.click(getByTestId('risk-filter-button')); + fireEvent.click(getByTestId('risk-filter-popoverButton')); fireEvent.click(getByTestId('risk-filter-item-Unknown')); expect(mockedTelemetry.reportEntityRiskFiltered).toHaveBeenCalledTimes(0); diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/severity/severity_filter.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/severity/severity_filter.tsx new file mode 100644 index 0000000000000..6aa150e40afae --- /dev/null +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/severity/severity_filter.tsx @@ -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 React, { useCallback } from 'react'; +import type { MultiselectFilterProps } from '../../../common/components/multiselect_filter'; +import { MultiselectFilter } from '../../../common/components/multiselect_filter'; +import { SEVERITY_UI_SORT_ORDER } from '../../common/utils'; +import type { RiskScoreEntity, RiskSeverity } from '../../../../common/search_strategy'; +import { RiskScoreLevel } from './common'; +import { ENTITY_RISK_LEVEL } from '../risk_score/translations'; +import { useKibana } from '../../../common/lib/kibana'; + +export interface SeverityFilterProps { + riskEntity?: RiskScoreEntity; + onSelect: (newSelection: RiskSeverity[]) => void; + selectedItems: RiskSeverity[]; +} + +export const SeverityFilter: React.FC = ({ + onSelect, + selectedItems, + riskEntity, +}) => { + const { telemetry } = useKibana().services; + const renderItem = useCallback((severity: RiskSeverity) => { + return ; + }, []); + + const updateSeverityFilter = useCallback< + NonNullable['onSelectionChange']> + >( + (newSelection, changedSeverity, changedStatus) => { + if (changedStatus === 'on') { + telemetry.reportEntityRiskFiltered({ + entity: riskEntity, + selectedSeverity: changedSeverity, + }); + } + + onSelect(newSelection); + }, + [onSelect, riskEntity, telemetry] + ); + + return ( + + data-test-subj="risk-filter" + title={ENTITY_RISK_LEVEL(riskEntity)} + items={SEVERITY_UI_SORT_ORDER} + selectedItems={selectedItems} + onSelectionChange={updateSeverityFilter} + renderItem={renderItem} + width={150} + /> + ); +}; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/severity/severity_filter_group.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/severity/severity_filter_group.tsx deleted file mode 100644 index df99f39091a92..0000000000000 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/severity/severity_filter_group.tsx +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import React, { useCallback, useMemo, useState } from 'react'; - -import type { FilterChecked, EuiSelectableProps } from '@elastic/eui'; -import { - EuiFilterButton, - EuiFilterGroup, - EuiPopover, - useGeneratedHtmlId, - EuiSelectable, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { SEVERITY_UI_SORT_ORDER } from '../../common/utils'; -import type { RiskScoreEntity, RiskSeverity } from '../../../../common/search_strategy'; -import type { SeverityCount } from './types'; -import { RiskScoreLevel } from './common'; -import { ENTITY_RISK_LEVEL } from '../risk_score/translations'; -import { useKibana } from '../../../common/lib/kibana'; - -interface SeverityItems { - risk: RiskSeverity; - count: number; - checked?: FilterChecked; - label: string; -} - -const SEVERITY_FILTER_ARIA_LABEL = i18n.translate( - 'xpack.securitySolution.entityAnalytics.severityFilter.ariaLabel', - { - defaultMessage: 'Select the severity level to filter by', - } -); - -export const SeverityFilterGroup: React.FC<{ - severityCount: SeverityCount; - selectedSeverities: RiskSeverity[]; - onSelect: (newSelection: RiskSeverity[]) => void; - riskEntity: RiskScoreEntity; -}> = ({ severityCount, selectedSeverities, onSelect, riskEntity }) => { - const { telemetry } = useKibana().services; - const [isPopoverOpen, setIsPopoverOpen] = useState(false); - - const onButtonClick = useCallback(() => { - setIsPopoverOpen(!isPopoverOpen); - }, [isPopoverOpen]); - - const closePopover = useCallback(() => { - setIsPopoverOpen(false); - }, []); - - const filterGroupPopoverId = useGeneratedHtmlId({ - prefix: 'filterGroupPopover', - }); - - const items: SeverityItems[] = useMemo(() => { - const checked: FilterChecked = 'on'; - return SEVERITY_UI_SORT_ORDER.map((severity) => ({ - risk: severity, - count: severityCount[severity], - label: severity, - checked: selectedSeverities.includes(severity) ? checked : undefined, - })); - }, [severityCount, selectedSeverities]); - - const updateSeverityFilter = useCallback< - NonNullable['onChange']> - >( - (newSelection, _, changedSeverity) => { - if (changedSeverity.checked === 'on') { - telemetry.reportEntityRiskFiltered({ - entity: riskEntity, - selectedSeverity: changedSeverity.risk, - }); - } - - const newSelectedSeverities = newSelection - .filter((item) => item.checked === 'on') - .map((item) => item.risk); - - onSelect(newSelectedSeverities); - }, - [onSelect, riskEntity, telemetry] - ); - - const totalActiveItem = useMemo( - () => items.reduce((total, item) => (item.checked === 'on' ? total + item.count : total), 0), - [items] - ); - - const button = useMemo( - () => ( - item.checked === 'on')} - iconType="arrowDown" - isSelected={isPopoverOpen} - numActiveFilters={totalActiveItem} - onClick={onButtonClick} - > - {ENTITY_RISK_LEVEL(riskEntity)} - - ), - [isPopoverOpen, items, onButtonClick, totalActiveItem, riskEntity] - ); - - return ( - - - ( - - )} - > - {(list) =>
{list}
} -
-
-
- ); -}; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/user_risk_score_table/index.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/user_risk_score_table/index.tsx index 0e2c64f84d610..6a2fbbe159b7c 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/user_risk_score_table/index.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/user_risk_score_table/index.tsx @@ -8,7 +8,7 @@ import React, { useMemo, useCallback } from 'react'; import { useDispatch } from 'react-redux'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiFilterGroup, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import type { Columns, Criteria, @@ -26,7 +26,7 @@ import type { UserRiskScoreItem } from '../../../../common/search_strategy/secur import type { SeverityCount } from '../severity/types'; import { SeverityBadges } from '../severity/severity_badges'; import { SeverityBar } from '../severity/severity_bar'; -import { SeverityFilterGroup } from '../severity/severity_filter_group'; +import { SeverityFilter } from '../severity/severity_filter'; import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; import type { State } from '../../../common/store'; import type { @@ -186,12 +186,13 @@ const UserRiskScoreTableComponent: React.FC = ({
- + + +
} diff --git a/x-pack/plugins/security_solution/public/entity_analytics/pages/entity_analytics_dashboard.tsx b/x-pack/plugins/security_solution/public/entity_analytics/pages/entity_analytics_dashboard.tsx index b15ceae25c8b9..48d2911e7c36a 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/pages/entity_analytics_dashboard.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/pages/entity_analytics_dashboard.tsx @@ -24,12 +24,16 @@ import { useHasSecurityCapability } from '../../helper_hooks'; import { EntityAnalyticsHeader } from '../components/entity_analytics_header'; import { EntityAnalyticsAnomalies } from '../components/entity_analytics_anomalies'; import { EntityAnalyticsRiskScores } from '../components/entity_analytics_risk_score'; +import { EntitiesList } from '../components/entity_store/entities_list'; +import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features'; const EntityAnalyticsComponent = () => { const { data: riskScoreEngineStatus } = useRiskEngineStatus(); const { indicesExist, loading: isSourcererLoading, sourcererDataView } = useSourcererDataView(); const isRiskScoreModuleLicenseAvailable = useHasSecurityCapability('entity-analytics'); + const isEntityStoreEnabled = useIsExperimentalFeatureEnabled('entityStoreEnabled'); + return ( <> {indicesExist ? ( @@ -66,6 +70,12 @@ const EntityAnalyticsComponent = () => { + + {isEntityStoreEnabled ? ( + + + + ) : null}
)} diff --git a/x-pack/plugins/security_solution/public/explore/components/authentication/__snapshots__/authentications_host_table.test.tsx.snap b/x-pack/plugins/security_solution/public/explore/components/authentication/__snapshots__/authentications_host_table.test.tsx.snap index a14cc0ab81115..45a1e70d9ba81 100644 --- a/x-pack/plugins/security_solution/public/explore/components/authentication/__snapshots__/authentications_host_table.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/explore/components/authentication/__snapshots__/authentications_host_table.test.tsx.snap @@ -352,7 +352,7 @@ exports[`Authentication Host Table Component rendering it renders the host authe
; @@ -307,7 +309,7 @@ const PaginatedTableComponent: FC = ({ onChange={onChange} sorting={tableSorting} /> - + {itemsPerRow && itemsPerRow.length > 0 && diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/__snapshots__/entity_store_data_client.test.ts.snap b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/__snapshots__/entity_store_data_client.test.ts.snap index 9991f215a5240..fc369949c31c6 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/__snapshots__/entity_store_data_client.test.ts.snap +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/__snapshots__/entity_store_data_client.test.ts.snap @@ -6,12 +6,7 @@ Object { "{ \\"index\\": [ \\".entities.v1.latest.ea_default_host_entity_store\\" - ], - \\"body\\": { - \\"bool\\": { - \\"filter\\": [] - } - } + ] }", ], "response": Array [ diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.test.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.test.ts index ad01fada2e8be..db666cf783108 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.test.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.test.ts @@ -82,7 +82,7 @@ describe('EntityStoreDataClient', () => { }); expect(esClientMock.search).toHaveBeenCalledWith( - expect.objectContaining({ query: { bool: { filter: [{ match_all: {} }] } } }) + expect.objectContaining({ query: { match_all: {} } }) ); }); diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.ts index ac1a99b761ed8..45994bb68cf09 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.ts @@ -10,7 +10,6 @@ import type { EntityClient } from '@kbn/entityManager-plugin/server/lib/entity_c import type { SortOrder } from '@elastic/elasticsearch/lib/api/types'; import type { Entity } from '../../../../common/api/entity_analytics/entity_store/entities/common.gen'; -import { createQueryFilterClauses } from '../../../utils/build_query'; import type { InitEntityEngineRequestBody, InitEntityEngineResponse, @@ -137,13 +136,7 @@ export class EntityStoreDataClient { const index = entityTypes.map((type) => getEntitiesIndexName(type, this.options.namespace)); const from = (page - 1) * perPage; const sort = sortField ? [{ [sortField]: sortOrder }] : undefined; - - const filter = [...createQueryFilterClauses(filterQuery)]; - const query = { - bool: { - filter, - }, - }; + const query = filterQuery ? JSON.parse(filterQuery) : undefined; const response = await this.options.esClient.search({ index, diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index eb427185d3fe1..4dd7938aeee0e 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -38517,7 +38517,6 @@ "xpack.securitySolution.entityAnalytics.riskEngine.unauthorized": "L'utilisateur ne dispose pas des privilèges requis pour modifier le moteur de risque.", "xpack.securitySolution.entityAnalytics.riskScore.chart.totalLabel": "Total", "xpack.securitySolution.entityAnalytics.riskScore.donut_chart.totalLabel": "Total", - "xpack.securitySolution.entityAnalytics.severityFilter.ariaLabel": "Sélectionner le filtre à appliquer pour le niveau de sévérité", "xpack.securitySolution.entityAnalytics.technicalPreviewLabel": "Version d'évaluation technique", "xpack.securitySolution.entityAnalytics.usersRiskDashboard.title": "Scores de risque de l'utilisateur", "xpack.securitySolution.entityDetails.userPanel.error": "Une erreur a été rencontrée lors du calcul du score de risque de {entity}", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 91fb104c16df1..14827bdf3b7d1 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -38258,7 +38258,6 @@ "xpack.securitySolution.entityAnalytics.riskEngine.unauthorized": "ユーザーにはリスクエンジン権限がありません。", "xpack.securitySolution.entityAnalytics.riskScore.chart.totalLabel": "合計", "xpack.securitySolution.entityAnalytics.riskScore.donut_chart.totalLabel": "合計", - "xpack.securitySolution.entityAnalytics.severityFilter.ariaLabel": "フィルタリングする重要度レベルを選択", "xpack.securitySolution.entityAnalytics.technicalPreviewLabel": "テクニカルプレビュー", "xpack.securitySolution.entityAnalytics.usersRiskDashboard.title": "ユーザーリスクスコア", "xpack.securitySolution.entityDetails.userPanel.error": "{entity}リスクスコアの計算中に問題が発生しました", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 7615268da55dc..116a07f28c787 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -38305,7 +38305,6 @@ "xpack.securitySolution.entityAnalytics.riskEngine.unauthorized": "用户缺少风险引擎权限。", "xpack.securitySolution.entityAnalytics.riskScore.chart.totalLabel": "合计", "xpack.securitySolution.entityAnalytics.riskScore.donut_chart.totalLabel": "合计", - "xpack.securitySolution.entityAnalytics.severityFilter.ariaLabel": "选择要依据其进行筛选的严重性级别", "xpack.securitySolution.entityAnalytics.technicalPreviewLabel": "技术预览", "xpack.securitySolution.entityAnalytics.usersRiskDashboard.title": "用户风险分数", "xpack.securitySolution.entityDetails.userPanel.error": "计算 {entity} 风险分数时出现问题", diff --git a/x-pack/test/security_solution_cypress/cypress/screens/hosts/host_risk.ts b/x-pack/test/security_solution_cypress/cypress/screens/hosts/host_risk.ts index fc62b4752dd44..f39b616497ed2 100644 --- a/x-pack/test/security_solution_cypress/cypress/screens/hosts/host_risk.ts +++ b/x-pack/test/security_solution_cypress/cypress/screens/hosts/host_risk.ts @@ -14,7 +14,7 @@ export const HOST_BY_RISK_TABLE_CELL = export const HOST_BY_RISK_TABLE = '[data-test-subj="table-hostRisk-loading-false"]'; -export const HOST_BY_RISK_TABLE_FILTER = '[data-test-subj="risk-filter-button"]'; +export const HOST_BY_RISK_TABLE_FILTER = '[data-test-subj="risk-filter-popoverButton"]'; export const HOST_BY_RISK_TABLE_FILTER_CRITICAL = '[data-test-subj="risk-filter-item-Critical"]'; From d9ca7c66670dc55467799b0e0b1262c5372616b0 Mon Sep 17 00:00:00 2001 From: Liam Thompson <32779855+leemthompo@users.noreply.github.com> Date: Fri, 27 Sep 2024 16:06:47 +0200 Subject: [PATCH 9/9] [DOCS] Create Search landing page skeleton and stubs (#194293) - Create Search landing page with links to all features available in the Elastic Cloud hosted UI - Create skeleton of a table to quickly provide links to all relevant docs and release notes for each feature - Currently commented out, to be filled in a follow-up PR by @kosabogi - Create a stub page for connection details page (Docs equivalent to [find your CloudID and create API keys](https://www.elastic.co/search-labs/tutorials/install-elasticsearch/elastic-cloud#finding-your-cloud-id)) - @kosabogi will update this page in follow-up - Nest existing Playground docs under there - Create a stub page for AI Assistant for Search docs - Create a stub page for inference endpoints UI --- docs/search/index.asciidoc | 73 ++++++++++++++++++ .../search/inference-endpoints/index.asciidoc | 5 ++ .../playground/images/chat-interface.png | Bin .../playground/images/data-button.png | Bin .../playground/images/get-started.png | Bin .../playground/images/query-interface.png | Bin .../playground/images/view-code-button.png | Bin docs/{ => search}/playground/index.asciidoc | 22 +++--- .../playground/playground-context.asciidoc | 10 +-- .../playground/playground-query.asciidoc | 6 +- .../playground-troubleshooting.asciidoc | 2 +- .../search/search-ai-assistant/index.asciidoc | 5 ++ .../search/search-connection-details.asciidoc | 7 ++ docs/user/index.asciidoc | 2 +- 14 files changed, 111 insertions(+), 21 deletions(-) create mode 100644 docs/search/index.asciidoc create mode 100644 docs/search/inference-endpoints/index.asciidoc rename docs/{ => search}/playground/images/chat-interface.png (100%) rename docs/{ => search}/playground/images/data-button.png (100%) rename docs/{ => search}/playground/images/get-started.png (100%) rename docs/{ => search}/playground/images/query-interface.png (100%) rename docs/{ => search}/playground/images/view-code-button.png (100%) rename docs/{ => search}/playground/index.asciidoc (96%) rename docs/{ => search}/playground/playground-context.asciidoc (94%) rename docs/{ => search}/playground/playground-query.asciidoc (97%) rename docs/{ => search}/playground/playground-troubleshooting.asciidoc (98%) create mode 100644 docs/search/search-ai-assistant/index.asciidoc create mode 100644 docs/search/search-connection-details.asciidoc diff --git a/docs/search/index.asciidoc b/docs/search/index.asciidoc new file mode 100644 index 0000000000000..f046330ac13e9 --- /dev/null +++ b/docs/search/index.asciidoc @@ -0,0 +1,73 @@ +[role="xpack"] +[[search-space]] += Search + +The *Search* space in {kib} comprises the following features: + +* https://www.elastic.co/guide/en/enterprise-search/current/connectors.html[Connectors] +* https://www.elastic.co/guide/en/enterprise-search/current/crawler.html[Web crawler] +* <> +* https://www.elastic.co/guide/en/elasticsearch/reference/current/search-application-overview.html[Search Applications] +* https://www.elastic.co/guide/en/elasticsearch/reference/current/behavioral-analytics-overview.html[Behavioral Analytics] +* Inference Endpoints UI +* AI Assistant for Search +* Persistent Dev Tools <> + +[float] +[[search-release-notes]] +== Docs and release notes + +The Search solution and use case is made up of many tools and features across the {stack}. +As a result, the release notes for your features of interest might live in different Elastic docs. +// Use the following table to find links to the appropriate documentation, API references (if applicable), and release notes. + +// [options="header"] +// |=== +// | Name | API reference | Documentation | Release notes + +// | Connectors +// | link:https://example.com/connectors/api[API reference] +// | link:https://example.com/connectors/docs[Documentation] +// | link:https://example.com/connectors/notes[Release notes] + +// | Web crawler +// | link:https://example.com/web_crawlers/api[API reference] +// | link:https://example.com/web_crawlers/docs[Documentation] +// | link:https://example.com/web_crawlers/notes[Release notes] + +// | Playground +// | link:https://example.com/playground/api[API reference] +// | link:https://example.com/playground/docs[Documentation] +// | link:https://example.com/playground/notes[Release notes] + +// | Search Applications +// | link:https://example.com/search_apps/api[API reference] +// | link:https://example.com/search_apps/docs[Documentation] +// | link:https://example.com/search_apps/notes[Release notes] + +// | Behavioral Analytics +// | link:https://example.com/behavioral_analytics/api[API reference] +// | link:https://example.com/behavioral_analytics/docs[Documentation] +// | link:https://example.com/behavioral_analytics/notes[Release notes] + +// | Inference Endpoints +// | link:https://example.com/inference_endpoints/api[API reference] +// | link:https://example.com/inference_endpoints/docs[Documentation] +// | link:https://example.com/inference_endpoints/notes[Release notes] + +// | Console +// | link:https://example.com/console/api[API reference] +// | link:https://example.com/console/docs[Documentation] +// | link:https://example.com/console/notes[Release notes] + +// | Search UI +// | link:https://www.elastic.co/docs/current/search-ui/api/architecture[API reference] +// | link:https://www.elastic.co/docs/current/search-ui/overview[Documentation] +// | link:https://example.com/search_ui/notes[Release notes] + +// |=== + +include::search-connection-details.asciidoc[] +include::playground/index.asciidoc[] +include::search-ai-assistant/index.asciidoc[] +include::inference-endpoints/index.asciidoc[] \ No newline at end of file diff --git a/docs/search/inference-endpoints/index.asciidoc b/docs/search/inference-endpoints/index.asciidoc new file mode 100644 index 0000000000000..30ead243d1510 --- /dev/null +++ b/docs/search/inference-endpoints/index.asciidoc @@ -0,0 +1,5 @@ +[role="xpack"] +[[inference-endpoints]] +== Inference endpoints UI + +(coming in 8.16.0) \ No newline at end of file diff --git a/docs/playground/images/chat-interface.png b/docs/search/playground/images/chat-interface.png similarity index 100% rename from docs/playground/images/chat-interface.png rename to docs/search/playground/images/chat-interface.png diff --git a/docs/playground/images/data-button.png b/docs/search/playground/images/data-button.png similarity index 100% rename from docs/playground/images/data-button.png rename to docs/search/playground/images/data-button.png diff --git a/docs/playground/images/get-started.png b/docs/search/playground/images/get-started.png similarity index 100% rename from docs/playground/images/get-started.png rename to docs/search/playground/images/get-started.png diff --git a/docs/playground/images/query-interface.png b/docs/search/playground/images/query-interface.png similarity index 100% rename from docs/playground/images/query-interface.png rename to docs/search/playground/images/query-interface.png diff --git a/docs/playground/images/view-code-button.png b/docs/search/playground/images/view-code-button.png similarity index 100% rename from docs/playground/images/view-code-button.png rename to docs/search/playground/images/view-code-button.png diff --git a/docs/playground/index.asciidoc b/docs/search/playground/index.asciidoc similarity index 96% rename from docs/playground/index.asciidoc rename to docs/search/playground/index.asciidoc index efb9b6261d8dd..e810767e57546 100644 --- a/docs/playground/index.asciidoc +++ b/docs/search/playground/index.asciidoc @@ -1,6 +1,6 @@ [role="xpack"] [[playground]] -= Playground +== Playground preview::[] @@ -22,7 +22,7 @@ Refer to the following for more advanced topics: [float] [[playground-how-it-works]] -== How {x} works +=== How {x} works Here's a simpified overview of how {x} works: @@ -43,7 +43,7 @@ Here's a simpified overview of how {x} works: [float] [[playground-availability-prerequisites]] -== Availability and prerequisites +=== Availability and prerequisites For Elastic Cloud and self-managed deployments {x} is available in the *Search* space in {kib}, under *Content* > *{x}*. @@ -102,14 +102,14 @@ Refer to the following for examples: [float] [[playground-getting-started]] -== Getting started +=== Getting started [.screenshot] image::get-started.png[width=600] [float] [[playground-getting-started-connect]] -=== Connect to LLM provider +==== Connect to LLM provider To get started with {x}, you need to create a <> for your LLM provider. You can also connect to <> which are compatible with the OpenAI API, by using the OpenAI connector. @@ -129,7 +129,7 @@ If you need to update a connector, or add a new one, click the 🔧 *Manage* but [float] [[playground-getting-started-ingest]] -=== Ingest data (optional) +==== Ingest data (optional) _You can skip this step if you already have data in one or more {es} indices._ @@ -168,7 +168,7 @@ These notebooks use the official {es} Python client. [float] [[playground-getting-started-index]] -=== Select {es} indices +==== Select {es} indices Once you've connected to your LLM provider, it's time to choose the data you want to search. @@ -186,7 +186,7 @@ image::images/data-button.png[width=100] [float] [[playground-getting-started-chat-query-modes]] -=== Chat and query modes +==== Chat and query modes Since 8.15.0 (and earlier for {es} Serverless), the main {x} UI has two modes: @@ -210,7 +210,7 @@ Learn more about the underlying {es} queries used to search your data in <