diff --git a/src/platform/packages/shared/kbn-discover-utils/src/field_constants.ts b/src/platform/packages/shared/kbn-discover-utils/src/field_constants.ts index 9f9be93040361..f27a1a4cef670 100644 --- a/src/platform/packages/shared/kbn-discover-utils/src/field_constants.ts +++ b/src/platform/packages/shared/kbn-discover-utils/src/field_constants.ts @@ -16,6 +16,7 @@ export const ERROR_MESSAGE_FIELD = 'error.message'; export const EVENT_ORIGINAL_FIELD = 'event.original'; export const EVENT_OUTCOME_FIELD = 'event.outcome'; export const INDEX_FIELD = '_index'; +export const EVENT_CATEGORY_FIELD = 'event.category'; // Trace fields export const TRACE_ID_FIELD = 'trace.id'; diff --git a/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/security/components/alert_event_overview.test.tsx b/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/security/components/alert_event_overview.test.tsx index 864faa1813884..eb86d34e3e37c 100644 --- a/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/security/components/alert_event_overview.test.tsx +++ b/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/security/components/alert_event_overview.test.tsx @@ -12,7 +12,6 @@ import { fireEvent, render, screen } from '@testing-library/react'; import { AlertEventOverview } from './alert_event_overview'; import type { DataTableRecord } from '@kbn/discover-utils'; import { dataViewMock } from '@kbn/discover-utils/src/__mocks__'; -import { EcsFlat } from '@elastic/ecs'; import { useDiscoverServices } from '../../../../hooks/use_discover_services'; import { encode } from '@kbn/rison'; import { URLSearchParams } from 'url'; @@ -27,6 +26,19 @@ const mockDiscoverServices = { application: { getUrlForApp: mockGetUrlForApp, }, + fieldsMetadata: { + useFieldsMetadata: jest.fn().mockReturnValue({ + fieldsMetadata: { + 'event.category': { + allowed_values: [ + { name: 'process', description: 'Process events' }, + { name: 'network', description: 'Network events' }, + ], + }, + }, + loading: false, + }), + }, }; const mockRow = { @@ -36,6 +48,7 @@ const mockRow = { _id: 'test-id', '@timestamp': '2021-08-02T14:00:00.000Z', 'kibana.alert.url': 'test-url', + 'event.category': 'process', }; const mockHit = { @@ -87,10 +100,7 @@ describe('AlertEventOverview', () => { render(); - expect(screen.getByTestId('expandableContent-About')).toHaveTextContent( - EcsFlat['event.category'].allowed_values.find((i) => i.name === 'process') - ?.description as string - ); + expect(screen.getByTestId('expandableContent-About')).toHaveTextContent('Process events'); }); test('should display timeline redirect url correctly', () => { @@ -142,5 +152,73 @@ describe('AlertEventOverview', () => { `test-timeline-url?${searchParams}` ); }); + + describe('ECS Description', () => { + test('should give ECS description for event.category field', () => { + render(); + expect(screen.getByTestId('about')).toHaveTextContent('Process events'); + }); + test('should give placeholder ECS description when fieldsMetadata is not available', () => { + (useDiscoverServices as jest.Mock).mockReturnValue({ + ...mockDiscoverServices, + fieldsMetadata: { + useFieldsMetadata: jest.fn().mockReturnValue({ + fieldsMetadata: undefined, + loading: false, + }), + }, + }); + + render(); + expect(screen.getByTestId('about')).toHaveTextContent( + "This field doesn't have a description because it's not part of ECS." + ); + }); + + test('should give placeholder ECS description when event.category field is not present in fieldMetada', () => { + (useDiscoverServices as jest.Mock).mockReturnValue({ + ...mockDiscoverServices, + fieldsMetadata: { + useFieldsMetadata: jest.fn().mockReturnValue({ + fieldsMetadata: {}, + loading: false, + }), + }, + }); + + render(); + expect(screen.getByTestId('about')).toHaveTextContent( + "This field doesn't have a description because it's not part of ECS." + ); + }); + + test('should give placeholder ECS description when event.category field is not present in hit', () => { + const localMockHit = { + flattened: { + ...mockRow, + 'event.category': undefined, + }, + } as unknown as DataTableRecord; + + render(); + expect(screen.getByTestId('about')).toHaveTextContent( + "This field doesn't have a description because it's not part of ECS." + ); + }); + + test('should give placeholder ECS when event.category fields has a value that is not in allowed values', () => { + const localMockHit = { + flattened: { + ...mockRow, + 'event.category': 'unknown', + }, + } as unknown as DataTableRecord; + + render(); + expect(screen.getByTestId('about')).toHaveTextContent( + "This field doesn't have a description because it's not part of ECS." + ); + }); + }); }); }); diff --git a/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/security/components/alert_event_overview.tsx b/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/security/components/alert_event_overview.tsx index c33210d17b7c0..4bb98c9f51974 100644 --- a/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/security/components/alert_event_overview.tsx +++ b/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/security/components/alert_event_overview.tsx @@ -9,7 +9,7 @@ import React, { useMemo, useState } from 'react'; import type { FC, PropsWithChildren } from 'react'; -import { getFieldValue } from '@kbn/discover-utils'; +import { fieldConstants, getFieldValue } from '@kbn/discover-utils'; import type { DocViewerComponent } from '@kbn/unified-doc-viewer/src/services/types'; import { EuiTitle, @@ -19,6 +19,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiText, + EuiSkeletonText, } from '@elastic/eui'; import * as i18n from '../translations'; import { getSecurityTimelineRedirectUrl } from '../utils'; @@ -62,12 +63,18 @@ export const ExpandableSection: FC> = ({ export const AlertEventOverview: DocViewerComponent = ({ hit }) => { const { application: { getUrlForApp }, + fieldsMetadata, } = useDiscoverServices(); const timelinesURL = getUrlForApp('securitySolutionUI', { path: 'alerts', }); + const result = fieldsMetadata?.useFieldsMetadata({ + attributes: ['allowed_values', 'name', 'flat_name'], + fieldNames: [fieldConstants.EVENT_CATEGORY_FIELD], + }); + const reason = useMemo(() => getFieldValue(hit, 'kibana.alert.reason') as string, [hit]); const description = useMemo( () => getFieldValue(hit, 'kibana.alert.rule.description') as string, @@ -100,9 +107,18 @@ export const AlertEventOverview: DocViewerComponent = ({ hit }) => { > - - {getEcsAllowedValueDescription(eventCategory)} - + {result?.loading ? ( + + ) : ( + + {getEcsAllowedValueDescription(result?.fieldsMetadata, eventCategory)} + + )} {description ? ( diff --git a/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/security/translations.ts b/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/security/translations.ts index b20afd9b01eb9..fb2fd398fe333 100644 --- a/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/security/translations.ts +++ b/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/security/translations.ts @@ -54,3 +54,10 @@ export const reasonSectionTitle = i18n.translate( defaultMessage: 'Reason', } ); + +export const ecsDescriptionLoadingAriaLable = i18n.translate( + 'discover.profile.security.flyout.ecsDescriptionLoadingAriaLabel', + { + defaultMessage: 'Loading ECS description', + } +); diff --git a/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/security/utils/ecs_description.ts b/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/security/utils/ecs_description.ts index a26a787b324de..ca4dd264c97a0 100644 --- a/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/security/utils/ecs_description.ts +++ b/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/security/utils/ecs_description.ts @@ -7,9 +7,8 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { EcsFlat } from '@elastic/ecs'; +import type { UseFieldsMetadataHook } from '@kbn/fields-metadata-plugin/public/hooks/use_fields_metadata'; import * as i18n from '../translations'; -export type EcsAllowedValue = (typeof EcsFlat)['event.category']['allowed_values'][0]; /** * Helper function to return the description of an allowed value of the specified field @@ -17,8 +16,11 @@ export type EcsAllowedValue = (typeof EcsFlat)['event.category']['allowed_values * @param value * @returns ecs description of the value */ -export const getEcsAllowedValueDescription = (value: string): string => { - const allowedValues: EcsAllowedValue[] = EcsFlat['event.category']?.allowed_values ?? []; +export const getEcsAllowedValueDescription = ( + fieldsMetadata: ReturnType['fieldsMetadata'] = {}, + value: string +): string => { + const allowedValues = fieldsMetadata['event.category']?.allowed_values ?? []; const result = allowedValues?.find((item) => item.name === value)?.description ?? i18n.noEcsDescriptionReason; return result;