[Security Solution][Entity details] - move code to get url link to flyout folder (#190111)

This commit is contained in:
Philippe Oberti 2024-08-19 17:59:43 +02:00 committed by GitHub
parent 439c7fa84c
commit 6d1426acd8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 192 additions and 48 deletions

View file

@ -14,13 +14,11 @@ import { useAssistant } from '../hooks/use_assistant';
import { mockGetFieldsData } from '../../shared/mocks/mock_get_fields_data';
import { mockDataFormattedForFieldBrowser } from '../../shared/mocks/mock_data_formatted_for_field_browser';
import { TestProvidersComponent } from '../../../../common/mock';
import { useGetAlertDetailsFlyoutLink } from '../../../../timelines/components/side_panel/event_details/use_get_alert_details_flyout_link';
import { useGetFlyoutLink } from '../hooks/use_get_flyout_link';
jest.mock('../../../../common/lib/kibana');
jest.mock('../hooks/use_assistant');
jest.mock(
'../../../../timelines/components/side_panel/event_details/use_get_alert_details_flyout_link'
);
jest.mock('../hooks/use_get_flyout_link');
jest.mock('@elastic/eui', () => ({
...jest.requireActual('@elastic/eui'),
@ -53,7 +51,7 @@ describe('<HeaderAction />', () => {
beforeEach(() => {
window.location.search = '?';
jest.mocked(useGetAlertDetailsFlyoutLink).mockReturnValue(alertUrl);
jest.mocked(useGetFlyoutLink).mockReturnValue(alertUrl);
jest.mocked(useAssistant).mockReturnValue({ showAssistant: true, promptContextId: '' });
});
@ -65,7 +63,7 @@ describe('<HeaderAction />', () => {
});
it('should not render share button in the title if alert is missing url info', () => {
jest.mocked(useGetAlertDetailsFlyoutLink).mockReturnValue(null);
jest.mocked(useGetFlyoutLink).mockReturnValue(null);
const { queryByTestId } = renderHeaderActions(mockContextValue);
expect(queryByTestId(SHARE_BUTTON_TEST_ID)).not.toBeInTheDocument();
});

View file

@ -10,7 +10,7 @@ import React, { memo } from 'react';
import { EuiButtonIcon, EuiCopy, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { NewChatByTitle } from '@kbn/elastic-assistant';
import { useGetAlertDetailsFlyoutLink } from '../../../../timelines/components/side_panel/event_details/use_get_alert_details_flyout_link';
import { useGetFlyoutLink } from '../hooks/use_get_flyout_link';
import { useBasicDataFromDetailsData } from '../../shared/hooks/use_basic_data_from_details_data';
import { useAssistant } from '../hooks/use_assistant';
import {
@ -27,9 +27,9 @@ export const HeaderActions: VFC = memo(() => {
const { dataFormattedForFieldBrowser, eventId, indexName } = useDocumentDetailsContext();
const { isAlert, timestamp } = useBasicDataFromDetailsData(dataFormattedForFieldBrowser);
const alertDetailsLink = useGetAlertDetailsFlyoutLink({
_id: eventId,
_index: indexName,
const alertDetailsLink = useGetFlyoutLink({
eventId,
indexName,
timestamp,
});

View file

@ -8,7 +8,7 @@
import { useMemo } from 'react';
import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs';
import { getField, getFieldArray } from '../../shared/utils';
import type { GetFieldsData } from '../../../../common/hooks/use_get_fields_data';
import type { GetFieldsData } from '../../shared/hooks/use_get_fields_data';
import { getRowRenderer } from '../../../../timelines/components/timeline/body/renderers/get_row_renderer';
import { defaultRowRenderers } from '../../../../timelines/components/timeline/body/renderers';
import { isEcsAllowedValue } from '../utils/event_utils';

View file

@ -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 { renderHook } from '@testing-library/react-hooks';
import { useGetFlyoutLink } from './use_get_flyout_link';
import { useGetAppUrl } from '@kbn/security-solution-navigation';
import { ALERT_DETAILS_REDIRECT_PATH } from '../../../../../common/constants';
jest.mock('@kbn/security-solution-navigation');
const eventId = 'eventId';
const indexName = 'indexName';
const timestamp = 'timestamp';
describe('useGetFlyoutLink', () => {
it('should return url', () => {
(useGetAppUrl as jest.Mock).mockReturnValue({
getAppUrl: (data: { path: string }) => data.path,
});
const hookResult = renderHook(() =>
useGetFlyoutLink({
eventId,
indexName,
timestamp,
})
);
const origin = 'http://localhost';
const path = `${ALERT_DETAILS_REDIRECT_PATH}/${eventId}?index=${indexName}&timestamp=${timestamp}`;
expect(hookResult.result.current).toBe(`${origin}${path}`);
});
});

View file

@ -11,19 +11,36 @@ import { DEFAULT_PREVIEW_INDEX } from '../../../../../common/constants';
import { buildAlertDetailPath } from '../../../../../common/utils/alert_detail_path';
import { useAppUrl } from '../../../../common/lib/kibana/hooks';
// TODO: MOVE TO FLYOUT FOLDER - https://github.com/elastic/security-team/issues/7462
export const useGetAlertDetailsFlyoutLink = ({
_id,
_index,
timestamp,
}: {
_id: string;
_index: string;
export interface UseGetFlyoutLinkProps {
/**
* Id of the document
*/
eventId: string;
/**
* Name of the index used in the parent's page
*/
indexName: string;
/**
* Timestamp of the document
*/
timestamp: string;
}) => {
}
/**
* Hook to get the link to the alert details page
*/
export const useGetFlyoutLink = ({
eventId,
indexName,
timestamp,
}: UseGetFlyoutLinkProps): string | null => {
const { getAppUrl } = useAppUrl();
const alertDetailPath = buildAlertDetailPath({ alertId: _id, index: _index, timestamp });
const isPreviewAlert = _index.includes(DEFAULT_PREVIEW_INDEX);
const alertDetailPath = buildAlertDetailPath({
alertId: eventId,
index: indexName,
timestamp,
});
const isPreviewAlert = indexName.includes(DEFAULT_PREVIEW_INDEX);
// getAppUrl accounts for the users selected space
const alertDetailsLink = useMemo(() => {

View file

@ -7,7 +7,7 @@
import { useMemo } from 'react';
import { ALERT_RULE_NAME, ALERT_RULE_UUID } from '@kbn/rule-data-utils';
import type { GetFieldsData } from '../../../../common/hooks/use_get_fields_data';
import type { GetFieldsData } from '../../shared/hooks/use_get_fields_data';
import { getField } from '../../shared/utils';
import { useDocumentDetailsContext } from '../../shared/context';

View file

@ -10,7 +10,7 @@ import { renderHook } from '@testing-library/react-hooks';
import type { UseSessionPreviewParams } from './use_session_preview';
import { useSessionPreview } from './use_session_preview';
import type { SessionViewConfig } from '@kbn/securitysolution-data-table/common/types';
import type { GetFieldsData } from '../../../../common/hooks/use_get_fields_data';
import type { GetFieldsData } from '../../shared/hooks/use_get_fields_data';
import { mockDataFormattedForFieldBrowser } from '../../shared/mocks/mock_data_formatted_for_field_browser';
import { mockFieldData, mockGetFieldsData } from '../../shared/mocks/mock_get_fields_data';

View file

@ -7,7 +7,7 @@
import type { TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common';
import type { SessionViewConfig } from '@kbn/securitysolution-data-table/common/types';
import type { GetFieldsData } from '../../../../common/hooks/use_get_fields_data';
import type { GetFieldsData } from '../../shared/hooks/use_get_fields_data';
import { getField } from '../../shared/utils';
import { useBasicDataFromDetailsData } from '../../shared/hooks/use_basic_data_from_details_data';

View file

@ -15,7 +15,7 @@ import { FlyoutLoading } from '../../shared/components/flyout_loading';
import type { SearchHit } from '../../../../common/search_strategy';
import { useBasicDataFromDetailsData } from './hooks/use_basic_data_from_details_data';
import type { DocumentDetailsProps } from './types';
import type { GetFieldsData } from '../../../common/hooks/use_get_fields_data';
import type { GetFieldsData } from './hooks/use_get_fields_data';
import { useRuleWithFallback } from '../../../detection_engine/rule_management/logic/use_rule_with_fallback';
export interface DocumentDetailsContext {

View file

@ -8,22 +8,42 @@
import type { RenderHookResult } from '@testing-library/react-hooks';
import { renderHook } from '@testing-library/react-hooks';
import type { UseEventDetailsParams, UseEventDetailsResult } from './use_event_details';
import { useEventDetails } from './use_event_details';
import { getAlertIndexAlias, useEventDetails } from './use_event_details';
import { useSpaceId } from '../../../../common/hooks/use_space_id';
import { useRouteSpy } from '../../../../common/utils/route/use_route_spy';
import { useSourcererDataView } from '../../../../sourcerer/containers';
import { useTimelineEventsDetails } from '../../../../timelines/containers/details';
import { useGetFieldsData } from '../../../../common/hooks/use_get_fields_data';
import { useGetFieldsData } from './use_get_fields_data';
jest.mock('../../../../common/hooks/use_space_id');
jest.mock('../../../../common/utils/route/use_route_spy');
jest.mock('../../../../sourcerer/containers');
jest.mock('../../../../timelines/containers/details');
jest.mock('../../../../common/hooks/use_get_fields_data');
jest.mock('./use_get_fields_data');
const eventId = 'eventId';
const indexName = 'indexName';
describe('getAlertIndexAlias', () => {
it('should handle default alert index', () => {
expect(getAlertIndexAlias('.internal.alerts-security.alerts')).toEqual(
'.alerts-security.alerts-default'
);
});
it('should handle default preview index', () => {
expect(getAlertIndexAlias('.internal.preview.alerts-security.alerts')).toEqual(
'.preview.alerts-security.alerts-default'
);
});
it('should handle non default space id', () => {
expect(getAlertIndexAlias('.internal.preview.alerts-security.alerts', 'test')).toEqual(
'.preview.alerts-security.alerts-test'
);
});
});
describe('useEventDetails', () => {
let hookResult: RenderHookResult<UseEventDetailsParams, UseEventDetailsResult>;
@ -35,7 +55,7 @@ describe('useEventDetails', () => {
indexPattern: {},
});
(useTimelineEventsDetails as jest.Mock).mockReturnValue([false, [], {}, {}, jest.fn()]);
jest.mocked(useGetFieldsData).mockReturnValue((field: string) => field);
jest.mocked(useGetFieldsData).mockReturnValue({ getFieldsData: (field: string) => field });
hookResult = renderHook(() => useEventDetails({ eventId, indexName }));

View file

@ -9,16 +9,31 @@ import type { BrowserFields, TimelineEventsDetailsItem } from '@kbn/timelines-pl
import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs';
import { SecurityPageName } from '@kbn/security-solution-navigation';
import type { DataViewBase } from '@kbn/es-query';
import { DEFAULT_ALERTS_INDEX, DEFAULT_PREVIEW_INDEX } from '../../../../../common/constants';
import type { RunTimeMappings } from '../../../../../common/api/search_strategy';
import { useSpaceId } from '../../../../common/hooks/use_space_id';
import { getAlertIndexAlias } from '../../../../timelines/components/side_panel/event_details/helpers';
import { useRouteSpy } from '../../../../common/utils/route/use_route_spy';
import { SourcererScopeName } from '../../../../sourcerer/store/model';
import { useSourcererDataView } from '../../../../sourcerer/containers';
import { useTimelineEventsDetails } from '../../../../timelines/containers/details';
import { useGetFieldsData } from '../../../../common/hooks/use_get_fields_data';
import type { SearchHit } from '../../../../../common/search_strategy';
import type { GetFieldsData } from '../../../../common/hooks/use_get_fields_data';
import type { GetFieldsData } from './use_get_fields_data';
import { useGetFieldsData } from './use_get_fields_data';
/**
* The referenced alert _index in the flyout uses the `.internal.` such as `.internal.alerts-security.alerts-spaceId` in the alert page flyout and .internal.preview.alerts-security.alerts-spaceId` in the rule creation preview flyout,
* but we always want to use their respective aliase indices rather than accessing their backing .internal. indices.
*/
export const getAlertIndexAlias = (
index: string,
spaceId: string = 'default'
): string | undefined => {
if (index.startsWith(`.internal${DEFAULT_ALERTS_INDEX}`)) {
return `${DEFAULT_ALERTS_INDEX}-${spaceId}`;
} else if (index.startsWith(`.internal${DEFAULT_PREVIEW_INDEX}`)) {
return `${DEFAULT_PREVIEW_INDEX}-${spaceId}`;
}
};
export interface UseEventDetailsParams {
/**
@ -90,7 +105,7 @@ export const useEventDetails = ({
runtimeMappings: sourcererDataView?.sourcererDataView?.runtimeFieldMap as RunTimeMappings,
skip: !eventId,
});
const getFieldsData = useGetFieldsData(searchHit?.fields);
const { getFieldsData } = useGetFieldsData({ fieldsData: searchHit?.fields });
return {
browserFields: sourcererDataView.browserFields,

View file

@ -0,0 +1,36 @@
/*
* 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 { RenderHookResult } from '@testing-library/react-hooks';
import { renderHook } from '@testing-library/react-hooks';
import { mockSearchHit } from '../mocks/mock_search_hit';
import type { UseGetFieldsDataParams, UseGetFieldsDataResult } from './use_get_fields_data';
import { useGetFieldsData } from './use_get_fields_data';
const fieldsData = {
...mockSearchHit.fields,
field: ['value'],
};
describe('useGetFieldsData', () => {
let hookResult: RenderHookResult<UseGetFieldsDataParams, UseGetFieldsDataResult>;
it('should return the value for a field', () => {
hookResult = renderHook(() => useGetFieldsData({ fieldsData }));
const getFieldsData = hookResult.result.current.getFieldsData;
expect(getFieldsData('field')).toEqual(['value']);
expect(getFieldsData('wrong_field')).toEqual(undefined);
});
it('should handle undefined', () => {
hookResult = renderHook(() => useGetFieldsData({ fieldsData: undefined }));
const getFieldsData = hookResult.result.current.getFieldsData;
expect(getFieldsData('field')).toEqual(undefined);
});
});

View file

@ -7,7 +7,7 @@
import { useCallback, useMemo } from 'react';
import { getOr } from 'lodash/fp';
import type { SearchHit } from '../../../common/search_strategy';
import type { SearchHit } from '../../../../../common/search_strategy';
/**
* Since the fields api may return a string array as well as an object array
@ -37,7 +37,6 @@ const getAllDotIndicesInReverse = (dotField: string): number[] => {
/**
* We get the dot paths so we can look up each path to see if any of the nested fields exist
* */
const getAllPotentialDotPaths = (dotField: string): string[][] => {
const reverseDotIndices = getAllDotIndicesInReverse(dotField);
@ -49,6 +48,9 @@ const getAllPotentialDotPaths = (dotField: string): string[][] => {
return pathTuples;
};
/**
* We get the nested value
*/
const getNestedValue = (startPath: string, endPath: string, data: Record<string, unknown>) => {
const foundPrimaryPath = data[startPath];
if (Array.isArray(foundPrimaryPath)) {
@ -63,7 +65,7 @@ const getNestedValue = (startPath: string, endPath: string, data: Record<string,
};
/**
* we get the field value from a fields response and by breaking down to look at each individual path,
* We get the field value from a fields response and by breaking down to look at each individual path,
* we're able to get both top level fields as well as nested fields that don't provide index information.
* In the case where a user enters kibana.alert.parameters.someField, a mapped array of the subfield value will be returned
*/
@ -93,11 +95,28 @@ const getFieldsValue = (
return undefined;
};
export type GetFieldsDataValue = string | string[] | null | undefined;
export type GetFieldsData = (field: string) => GetFieldsDataValue;
export type GetFieldsData = (field: string) => string | string[] | null | undefined;
// TODO: MOVE TO FLYOUT FOLDER - https://github.com/elastic/security-team/issues/7462
export const useGetFieldsData = (fieldsData: SearchHit['fields'] | undefined): GetFieldsData => {
export interface UseGetFieldsDataParams {
/**
* All fields from the searchHit result
*/
fieldsData: SearchHit['fields'] | undefined;
}
export interface UseGetFieldsDataResult {
/**
* Retrieves the value for the provided field (reading from the searchHit result)
*/
getFieldsData: GetFieldsData;
}
/**
* Hook that returns a function to retrieve the values for a field (reading from the searchHit result)
*/
export const useGetFieldsData = ({
fieldsData,
}: UseGetFieldsDataParams): UseGetFieldsDataResult => {
// TODO: Move cache to top level container such as redux or context. Make it store type agnostic if possible
// TODO: Handle updates where data is re-requested and the cache is reset.
const cachedOriginalData = useMemo(() => fieldsData, [fieldsData]);
@ -111,7 +130,7 @@ export const useGetFieldsData = (fieldsData: SearchHit['fields'] | undefined): G
[cachedExpensiveNestedValues]
);
return useCallback(
const getFieldsData = useCallback(
(field: string) => {
let fieldsValue;
// Get an expensive value from the cache if it exists, otherwise search for the value
@ -133,4 +152,6 @@ export const useGetFieldsData = (fieldsData: SearchHit['fields'] | undefined): G
},
[cacheNestedValues, cachedExpensiveNestedValues, cachedOriginalData]
);
return { getFieldsData };
};

View file

@ -6,7 +6,7 @@
*/
import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs';
import type { GetFieldsData } from '../../../../common/hooks/use_get_fields_data';
import type { GetFieldsData } from './use_get_fields_data';
import { useIsInvestigateInResolverActionEnabled } from '../../../../detections/components/alerts_table/timeline_actions/investigate_in_resolver';
import { useLicense } from '../../../../common/hooks/use_license';
import { ANCESTOR_ID } from '../constants/field_names';

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import type { GetFieldsData } from '../../../../common/hooks/use_get_fields_data';
import type { GetFieldsData } from './use_get_fields_data';
import { ANCESTOR_ID } from '../constants/field_names';
import { getField } from '../utils';

View file

@ -6,7 +6,7 @@
*/
import { ENTRY_LEADER_ENTITY_ID } from '../constants/field_names';
import type { GetFieldsData } from '../../../../common/hooks/use_get_fields_data';
import type { GetFieldsData } from './use_get_fields_data';
import { getField } from '../utils';
export interface UseShowRelatedAlertsBySessionParams {

View file

@ -7,7 +7,7 @@
import { APP_ID } from '../../../../../common';
import { useKibana } from '../../../../common/lib/kibana/kibana_react';
import type { GetFieldsData } from '../../../../common/hooks/use_get_fields_data';
import type { GetFieldsData } from './use_get_fields_data';
import { getField } from '../utils';
export interface UseShowRelatedCasesParams {

View file

@ -6,7 +6,7 @@
*/
import { ALERT_SUPPRESSION_DOCS_COUNT } from '@kbn/rule-data-utils';
import type { GetFieldsData } from '../../../../common/hooks/use_get_fields_data';
import type { GetFieldsData } from './use_get_fields_data';
export interface ShowSuppressedAlertsParams {
/**

View file

@ -12,7 +12,7 @@ import {
ALERT_SUPPRESSION_DOCS_COUNT,
} from '@kbn/rule-data-utils';
import { EventKind } from '../constants/event_kinds';
import type { GetFieldsData } from '../../../../common/hooks/use_get_fields_data';
import type { GetFieldsData } from '../hooks/use_get_fields_data';
export const mockFieldData: Record<string, string[]> = {
[ALERT_SEVERITY]: ['low'],