Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 = {
Expand All @@ -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 = {
Expand Down Expand Up @@ -87,10 +100,7 @@ describe('AlertEventOverview', () => {

render(<AlertEventOverview hit={localMockHit} dataView={mockDataView} />);

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', () => {
Expand Down Expand Up @@ -142,5 +152,73 @@ describe('AlertEventOverview', () => {
`test-timeline-url?${searchParams}`
);
});

describe('ECS Description', () => {
test('should give ECS description for event.category field', () => {
render(<AlertEventOverview hit={mockHit} dataView={mockDataView} />);
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(<AlertEventOverview hit={mockHit} dataView={mockDataView} />);
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(<AlertEventOverview hit={mockHit} dataView={mockDataView} />);
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(<AlertEventOverview hit={localMockHit} dataView={mockDataView} />);
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(<AlertEventOverview hit={localMockHit} dataView={mockDataView} />);
expect(screen.getByTestId('about')).toHaveTextContent(
"This field doesn't have a description because it's not part of ECS."
);
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -19,6 +19,7 @@ import {
EuiFlexGroup,
EuiFlexItem,
EuiText,
EuiSkeletonText,
} from '@elastic/eui';
import * as i18n from '../translations';
import { getSecurityTimelineRedirectUrl } from '../utils';
Expand Down Expand Up @@ -62,12 +63,18 @@ export const ExpandableSection: FC<PropsWithChildren<{ title: string }>> = ({
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,
Expand Down Expand Up @@ -100,9 +107,18 @@ export const AlertEventOverview: DocViewerComponent = ({ hit }) => {
>
<EuiFlexItem>
<ExpandableSection title={i18n.aboutSectionTitle}>
<EuiText size="s" data-test-subj="about">
{getEcsAllowedValueDescription(eventCategory)}
</EuiText>
{result?.loading ? (
<EuiSkeletonText
lines={2}
size={'s'}
isLoading={result?.loading}
contentAriaLabel={i18n.ecsDescriptionLoadingAriaLable}
/>
) : (
<EuiText size="s" data-test-subj="about">
{getEcsAllowedValueDescription(result?.fieldsMetadata, eventCategory)}
</EuiText>
)}
</ExpandableSection>
</EuiFlexItem>
{description ? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,20 @@
* 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
* @param fieldName
* @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<UseFieldsMetadataHook>['fieldsMetadata'] = {},
value: string
): string => {
const allowedValues = fieldsMetadata['event.category']?.allowed_values ?? [];
const result =
allowedValues?.find((item) => item.name === value)?.description ?? i18n.noEcsDescriptionReason;
return result;
Expand Down