[8.12] [Security Solution][Flyout] - fix analyzer preview loading and update hover actions in rule preview (#175282) (#176243)

# Backport

This will backport the following commits from `main` to `8.12`:
- [[Security Solution][Flyout] - fix analyzer preview loading and update
hover actions in rule preview
(#175282)](https://github.com/elastic/kibana/pull/175282)

<!--- Backport version: 8.9.8 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT
[{"author":{"name":"christineweng","email":"18648970+christineweng@users.noreply.github.com"},"sourceCommit":{"committedDate":"2024-01-29T23:16:30Z","message":"[Security
Solution][Flyout] - fix analyzer preview loading and update hover
actions in rule preview (#175282)\n\n## Summary\r\n\r\n- Fixed a bug
introduced by\r\nhttps://github.com/elastic/kibana/pull/174651: analyzer
preview is stuck\r\nin loading state because `_id` is not in the index
for a preview alert.\r\nAdded back `kibana.alert.ancestor.id` when
flyout is open in alert\r\npreview.\r\n\r\n- Refactor the use of
security hover actions in flyout. The hover action\r\nwrapper checks the
type of document/scope (whether it is an alert, or in\r\na preview) to
determine what actions to show on hover. Most hover\r\nactions should
behave consistently when flyout is in rule preview (do\r\nnot show
filter options)\r\n - Related:
https://github.com/elastic/kibana/issues/173608 \r\n- Not included in
this pr: 1) hover actions in alert reason preview, 2)\r\nhover actions
in left panel entity details as the component is owned by\r\na different
team and required greater refactor effort\r\n\r\n- Fixed a UI bug on
assignees breaking into multiple
lines\r\n\r\n![image](96d909e3-b6bd-4a46-bc86-fbb473ce3b62)\r\n
\r\n### Checklist\r\n\r\n- [x] Any text added follows [EUI's
writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\r\nsentence case text and includes
[i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n-
[x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios","sha":"d51fddb332f824889c24c6a8278c81259ad445ae","branchLabelMapping":{"^v8.13.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","backport
missing","Team:Threat Hunting","Team:Threat
Hunting:Investigations","v8.12.1","v8.13.0"],"number":175282,"url":"https://github.com/elastic/kibana/pull/175282","mergeCommit":{"message":"[Security
Solution][Flyout] - fix analyzer preview loading and update hover
actions in rule preview (#175282)\n\n## Summary\r\n\r\n- Fixed a bug
introduced by\r\nhttps://github.com/elastic/kibana/pull/174651: analyzer
preview is stuck\r\nin loading state because `_id` is not in the index
for a preview alert.\r\nAdded back `kibana.alert.ancestor.id` when
flyout is open in alert\r\npreview.\r\n\r\n- Refactor the use of
security hover actions in flyout. The hover action\r\nwrapper checks the
type of document/scope (whether it is an alert, or in\r\na preview) to
determine what actions to show on hover. Most hover\r\nactions should
behave consistently when flyout is in rule preview (do\r\nnot show
filter options)\r\n - Related:
https://github.com/elastic/kibana/issues/173608 \r\n- Not included in
this pr: 1) hover actions in alert reason preview, 2)\r\nhover actions
in left panel entity details as the component is owned by\r\na different
team and required greater refactor effort\r\n\r\n- Fixed a UI bug on
assignees breaking into multiple
lines\r\n\r\n![image](96d909e3-b6bd-4a46-bc86-fbb473ce3b62)\r\n
\r\n### Checklist\r\n\r\n- [x] Any text added follows [EUI's
writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\r\nsentence case text and includes
[i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n-
[x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios","sha":"d51fddb332f824889c24c6a8278c81259ad445ae"}},"sourceBranch":"main","suggestedTargetBranches":["8.12"],"targetPullRequestStates":[{"branch":"8.12","label":"v8.12.1","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v8.13.0","labelRegex":"^v8.13.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/175282","number":175282,"mergeCommit":{"message":"[Security
Solution][Flyout] - fix analyzer preview loading and update hover
actions in rule preview (#175282)\n\n## Summary\r\n\r\n- Fixed a bug
introduced by\r\nhttps://github.com/elastic/kibana/pull/174651: analyzer
preview is stuck\r\nin loading state because `_id` is not in the index
for a preview alert.\r\nAdded back `kibana.alert.ancestor.id` when
flyout is open in alert\r\npreview.\r\n\r\n- Refactor the use of
security hover actions in flyout. The hover action\r\nwrapper checks the
type of document/scope (whether it is an alert, or in\r\na preview) to
determine what actions to show on hover. Most hover\r\nactions should
behave consistently when flyout is in rule preview (do\r\nnot show
filter options)\r\n - Related:
https://github.com/elastic/kibana/issues/173608 \r\n- Not included in
this pr: 1) hover actions in alert reason preview, 2)\r\nhover actions
in left panel entity details as the component is owned by\r\na different
team and required greater refactor effort\r\n\r\n- Fixed a UI bug on
assignees breaking into multiple
lines\r\n\r\n![image](96d909e3-b6bd-4a46-bc86-fbb473ce3b62)\r\n
\r\n### Checklist\r\n\r\n- [x] Any text added follows [EUI's
writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\r\nsentence case text and includes
[i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n-
[x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios","sha":"d51fddb332f824889c24c6a8278c81259ad445ae"}}]}]
BACKPORT-->
This commit is contained in:
christineweng 2024-02-07 17:54:21 -06:00 committed by GitHub
parent 7a6ea82147
commit 6cfaf54ea8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 454 additions and 216 deletions

View file

@ -0,0 +1,72 @@
/*
* 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 { FC } from 'react';
import React, { useMemo } from 'react';
import { useLeftPanelContext } from '../context';
import { getSourcererScopeId } from '../../../../helpers';
import { useBasicDataFromDetailsData } from '../../../../timelines/components/side_panel/event_details/helpers';
import { SecurityCellActionType } from '../../../../actions/constants';
import {
CellActionsMode,
SecurityCellActions,
SecurityCellActionsTrigger,
} from '../../../../common/components/cell_actions';
interface CellActionsProps {
/**
* Field name
*/
field: string;
/**
* Field value
*/
value: string[] | string | null | undefined;
/**
* Boolean to indicate if value is an object array
*/
isObjectArray?: boolean;
/**
* React components to render
*/
children: React.ReactNode | string;
}
/**
* Security cell action wrapper for document details flyout
*/
export const CellActions: FC<CellActionsProps> = ({ field, value, isObjectArray, children }) => {
const { dataFormattedForFieldBrowser, scopeId, isPreview } = useLeftPanelContext();
const { isAlert } = useBasicDataFromDetailsData(dataFormattedForFieldBrowser);
const triggerId = isAlert
? SecurityCellActionsTrigger.DETAILS_FLYOUT
: SecurityCellActionsTrigger.DEFAULT;
const data = useMemo(() => ({ field, value }), [field, value]);
const metadata = useMemo(() => ({ scopeId, isObjectArray }), [scopeId, isObjectArray]);
const disabledActionTypes = useMemo(
() => (isPreview ? [SecurityCellActionType.FILTER, SecurityCellActionType.TOGGLE_COLUMN] : []),
[isPreview]
);
return (
<SecurityCellActions
data={data}
mode={CellActionsMode.HOVER_RIGHT}
triggerId={triggerId}
visibleCellActions={6}
sourcererScopeId={getSourcererScopeId(scopeId)}
metadata={metadata}
disabledActionTypes={disabledActionTypes}
>
{children}
</SecurityCellActions>
);
};
CellActions.displayName = 'CellActions';

View file

@ -67,7 +67,7 @@ describe('CorrelationsDetails', () => {
it('renders all sections', () => {
jest
.mocked(useShowRelatedAlertsByAncestry)
.mockReturnValue({ show: true, indices: ['index1'] });
.mockReturnValue({ show: true, documentId: 'event-id', indices: ['index1'] });
jest
.mocked(useShowRelatedAlertsBySameSourceEvent)
.mockReturnValue({ show: true, originalEventId: 'originalEventId' });
@ -115,7 +115,7 @@ describe('CorrelationsDetails', () => {
it('should render no section and show error message if show values are false', () => {
jest
.mocked(useShowRelatedAlertsByAncestry)
.mockReturnValue({ show: false, indices: ['index1'] });
.mockReturnValue({ show: false, documentId: 'event-id', indices: ['index1'] });
jest
.mocked(useShowRelatedAlertsBySameSourceEvent)
.mockReturnValue({ show: false, originalEventId: 'originalEventId' });
@ -144,7 +144,9 @@ describe('CorrelationsDetails', () => {
});
it('should render no section if values are null', () => {
jest.mocked(useShowRelatedAlertsByAncestry).mockReturnValue({ show: true });
jest
.mocked(useShowRelatedAlertsByAncestry)
.mockReturnValue({ show: true, documentId: 'event-id' });
jest.mocked(useShowRelatedAlertsBySameSourceEvent).mockReturnValue({ show: true });
jest.mocked(useShowRelatedAlertsBySession).mockReturnValue({ show: true });
jest.mocked(useShowRelatedCases).mockReturnValue(false);

View file

@ -27,13 +27,25 @@ export const CORRELATIONS_TAB_ID = 'correlations-details';
* Correlations displayed in the document details expandable flyout left section under the Insights tab
*/
export const CorrelationsDetails: React.FC = () => {
const { dataAsNestedObject, dataFormattedForFieldBrowser, eventId, getFieldsData, scopeId } =
useLeftPanelContext();
const {
dataAsNestedObject,
dataFormattedForFieldBrowser,
eventId,
getFieldsData,
scopeId,
isPreview,
} = useLeftPanelContext();
const { show: showAlertsByAncestry, indices } = useShowRelatedAlertsByAncestry({
const {
show: showAlertsByAncestry,
indices,
documentId,
} = useShowRelatedAlertsByAncestry({
getFieldsData,
dataAsNestedObject,
dataFormattedForFieldBrowser,
eventId,
isPreview,
});
const { show: showSameSourceAlerts, originalEventId } = useShowRelatedAlertsBySameSourceEvent({
getFieldsData,
@ -82,9 +94,13 @@ export const CorrelationsDetails: React.FC = () => {
<RelatedAlertsBySession entityId={entityId} scopeId={scopeId} eventId={eventId} />
</EuiFlexItem>
)}
{showAlertsByAncestry && indices && (
{showAlertsByAncestry && documentId && indices && (
<EuiFlexItem>
<RelatedAlertsByAncestry indices={indices} scopeId={scopeId} documentId={eventId} />
<RelatedAlertsByAncestry
indices={indices}
scopeId={scopeId}
documentId={documentId}
/>
</EuiFlexItem>
)}
</EuiFlexGroup>

View file

@ -8,6 +8,7 @@
import React from 'react';
import { render } from '@testing-library/react';
import type { Anomalies } from '../../../../common/components/ml/types';
import { LeftPanelContext } from '../context';
import { TestProviders } from '../../../../common/mock';
import { HostDetails } from './host_details';
import { useMlCapabilities } from '../../../../common/components/ml/hooks/use_ml_capabilities';
@ -22,6 +23,7 @@ import {
HOST_DETAILS_RELATED_USERS_TABLE_TEST_ID,
} from './test_ids';
import { EXPANDABLE_PANEL_CONTENT_TEST_ID } from '../../../shared/components/test_ids';
import { mockContextValue } from '../mocks/mock_context';
jest.mock('react-router-dom', () => {
const actual = jest.requireActual('react-router-dom');
@ -122,10 +124,12 @@ const mockRelatedUsersResponse = {
loading: false,
};
const renderHostDetails = () =>
const renderHostDetails = (contextValue: LeftPanelContext) =>
render(
<TestProviders>
<HostDetails {...defaultProps} />
<LeftPanelContext.Provider value={contextValue}>
<HostDetails {...defaultProps} />
</LeftPanelContext.Provider>
</TestProviders>
);
@ -139,13 +143,13 @@ describe('<HostDetails />', () => {
});
it('should render host details correctly', () => {
const { getByTestId } = renderHostDetails();
const { getByTestId } = renderHostDetails(mockContextValue);
expect(getByTestId(EXPANDABLE_PANEL_CONTENT_TEST_ID(HOST_DETAILS_TEST_ID))).toBeInTheDocument();
});
describe('Host overview', () => {
it('should render the HostOverview with correct dates and indices', () => {
const { getByTestId } = renderHostDetails();
const { getByTestId } = renderHostDetails(mockContextValue);
expect(mockUseHostDetails).toBeCalledWith({
id: 'entities-hosts-details-uuid',
startDate: from,
@ -164,20 +168,20 @@ describe('<HostDetails />', () => {
});
mockUseRiskScore.mockReturnValue({ data: [], isAuthorized: true });
const { getByText } = renderHostDetails();
const { getByText } = renderHostDetails(mockContextValue);
expect(getByText('Host risk score')).toBeInTheDocument();
});
it('should not render host risk score when unauthorized', () => {
mockUseRiskScore.mockReturnValue({ data: [], isAuthorized: false });
const { queryByText } = renderHostDetails();
const { queryByText } = renderHostDetails(mockContextValue);
expect(queryByText('Host risk score')).not.toBeInTheDocument();
});
});
describe('Related users', () => {
it('should render the related user table with correct dates and indices', () => {
const { getByTestId } = renderHostDetails();
const { getByTestId } = renderHostDetails(mockContextValue);
expect(mockUseHostsRelatedUsers).toBeCalledWith({
from: timestamp,
hostName: 'test host',
@ -194,7 +198,7 @@ describe('<HostDetails />', () => {
});
mockUseHasSecurityCapability.mockReturnValue(true);
const { queryAllByRole } = renderHostDetails();
const { queryAllByRole } = renderHostDetails(mockContextValue);
expect(queryAllByRole('columnheader').length).toBe(3);
expect(queryAllByRole('row')[1].textContent).toContain('test user');
expect(queryAllByRole('row')[1].textContent).toContain('100.XXX.XXX');
@ -208,12 +212,12 @@ describe('<HostDetails />', () => {
});
mockUseHasSecurityCapability.mockReturnValue(false);
const { queryAllByRole } = renderHostDetails();
const { queryAllByRole } = renderHostDetails(mockContextValue);
expect(queryAllByRole('columnheader').length).toBe(2);
});
it('should not render host risk score column when license is not valid', () => {
const { queryAllByRole } = renderHostDetails();
const { queryAllByRole } = renderHostDetails(mockContextValue);
expect(queryAllByRole('columnheader').length).toBe(2);
});
@ -224,7 +228,7 @@ describe('<HostDetails />', () => {
loading: false,
});
const { getByTestId } = renderHostDetails();
const { getByTestId } = renderHostDetails(mockContextValue);
expect(getByTestId(HOST_DETAILS_RELATED_USERS_TABLE_TEST_ID).textContent).toContain(
'No users identified'
);

View file

@ -21,7 +21,6 @@ import {
} from '@elastic/eui';
import type { EuiBasicTableColumn } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { getSourcererScopeId } from '../../../../helpers';
import { ExpandablePanel } from '../../../shared/components/expandable_panel';
import type { RelatedUser } from '../../../../../common/search_strategy/security_solution/related_entities/related_users';
import type { RiskSeverity } from '../../../../../common/search_strategy';
@ -33,11 +32,7 @@ import { RiskScoreEntity } from '../../../../../common/search_strategy';
import { RiskScoreLevel } from '../../../../explore/components/risk_score/severity/common';
import { DefaultFieldRenderer } from '../../../../timelines/components/field_renderers/field_renderers';
import { InputsModelId } from '../../../../common/store/inputs/constants';
import {
SecurityCellActions,
CellActionsMode,
SecurityCellActionsTrigger,
} from '../../../../common/components/cell_actions';
import { CellActions } from './cell_actions';
import { useGlobalTime } from '../../../../common/containers/use_global_time';
import { useSourcererDataView } from '../../../../common/containers/sourcerer';
import { manageQuery } from '../../../../common/components/page/manage_query';
@ -135,20 +130,9 @@ export const HostDetails: React.FC<HostDetailsProps> = ({ hostName, timestamp, s
),
render: (user: string) => (
<EuiText grow={false} size="xs">
<SecurityCellActions
data={{
field: 'user.name',
value: user,
}}
mode={CellActionsMode.HOVER_RIGHT}
triggerId={SecurityCellActionsTrigger.DETAILS_FLYOUT}
visibleCellActions={6}
sourcererScopeId={getSourcererScopeId(scopeId)}
metadata={{ scopeId }}
showActionTooltips
>
<CellActions field={'user.name'} value={user}>
{user}
</SecurityCellActions>
</CellActions>
</EuiText>
),
},
@ -190,7 +174,7 @@ export const HostDetails: React.FC<HostDetailsProps> = ({ hostName, timestamp, s
]
: []),
],
[isEntityAnalyticsAuthorized, scopeId]
[isEntityAnalyticsAuthorized]
);
const relatedUsersCount = useMemo(

View file

@ -9,6 +9,7 @@ import React from 'react';
import { render } from '@testing-library/react';
import type { Anomalies } from '../../../../common/components/ml/types';
import { TestProviders } from '../../../../common/mock';
import { LeftPanelContext } from '../context';
import { UserDetails } from './user_details';
import { useMlCapabilities } from '../../../../common/components/ml/hooks/use_ml_capabilities';
import { useRiskScore } from '../../../../explore/containers/risk_score';
@ -22,6 +23,7 @@ import {
USER_DETAILS_RELATED_HOSTS_TABLE_TEST_ID,
} from './test_ids';
import { EXPANDABLE_PANEL_CONTENT_TEST_ID } from '../../../shared/components/test_ids';
import { mockContextValue } from '../mocks/mock_context';
jest.mock('react-router-dom', () => {
const actual = jest.requireActual('react-router-dom');
@ -119,10 +121,12 @@ const mockRelatedHostsResponse = {
loading: false,
};
const renderUserDetails = () =>
const renderUserDetails = (contextValue: LeftPanelContext) =>
render(
<TestProviders>
<UserDetails {...defaultProps} />
<LeftPanelContext.Provider value={contextValue}>
<UserDetails {...defaultProps} />
</LeftPanelContext.Provider>
</TestProviders>
);
@ -136,13 +140,13 @@ describe('<UserDetails />', () => {
});
it('should render host details correctly', () => {
const { getByTestId } = renderUserDetails();
const { getByTestId } = renderUserDetails(mockContextValue);
expect(getByTestId(EXPANDABLE_PANEL_CONTENT_TEST_ID(USER_DETAILS_TEST_ID))).toBeInTheDocument();
});
describe('Host overview', () => {
it('should render the HostOverview with correct dates and indices', () => {
const { getByTestId } = renderUserDetails();
const { getByTestId } = renderUserDetails(mockContextValue);
expect(mockUseObservedUserDetails).toBeCalledWith({
id: 'entities-users-details-uuid',
startDate: from,
@ -159,20 +163,20 @@ describe('<UserDetails />', () => {
isPlatinumOrTrialLicense: true,
capabilities: {},
});
const { getByText } = renderUserDetails();
const { getByText } = renderUserDetails(mockContextValue);
expect(getByText('User risk score')).toBeInTheDocument();
});
it('should not render user risk score when license is not valid', () => {
mockUseRiskScore.mockReturnValue({ data: [], isAuthorized: false });
const { queryByText } = renderUserDetails();
const { queryByText } = renderUserDetails(mockContextValue);
expect(queryByText('User risk score')).not.toBeInTheDocument();
});
});
describe('Related hosts', () => {
it('should render the related host table with correct dates and indices', () => {
const { getByTestId } = renderUserDetails();
const { getByTestId } = renderUserDetails(mockContextValue);
expect(mockUseUsersRelatedHosts).toBeCalledWith({
from: timestamp,
userName: 'test user',
@ -187,7 +191,7 @@ describe('<UserDetails />', () => {
isPlatinumOrTrialLicense: true,
capabilities: {},
});
const { queryAllByRole } = renderUserDetails();
const { queryAllByRole } = renderUserDetails(mockContextValue);
expect(queryAllByRole('columnheader').length).toBe(3);
expect(queryAllByRole('row')[1].textContent).toContain('test host');
expect(queryAllByRole('row')[1].textContent).toContain('100.XXX.XXX');
@ -195,7 +199,7 @@ describe('<UserDetails />', () => {
});
it('should not render host risk score column when license is not valid', () => {
const { queryAllByRole } = renderUserDetails();
const { queryAllByRole } = renderUserDetails(mockContextValue);
expect(queryAllByRole('columnheader').length).toBe(2);
});
@ -206,7 +210,7 @@ describe('<UserDetails />', () => {
loading: false,
});
const { getByTestId } = renderUserDetails();
const { getByTestId } = renderUserDetails(mockContextValue);
expect(getByTestId(USER_DETAILS_RELATED_HOSTS_TABLE_TEST_ID).textContent).toContain(
'No hosts identified'
);

View file

@ -21,7 +21,6 @@ import {
} from '@elastic/eui';
import type { EuiBasicTableColumn } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { getSourcererScopeId } from '../../../../helpers';
import { ExpandablePanel } from '../../../shared/components/expandable_panel';
import type { RelatedHost } from '../../../../../common/search_strategy/security_solution/related_entities/related_hosts';
import type { RiskSeverity } from '../../../../../common/search_strategy';
@ -32,11 +31,7 @@ import { NetworkDetailsLink } from '../../../../common/components/links';
import { RiskScoreEntity } from '../../../../../common/search_strategy';
import { RiskScoreLevel } from '../../../../explore/components/risk_score/severity/common';
import { DefaultFieldRenderer } from '../../../../timelines/components/field_renderers/field_renderers';
import {
SecurityCellActions,
CellActionsMode,
SecurityCellActionsTrigger,
} from '../../../../common/components/cell_actions';
import { CellActions } from './cell_actions';
import { InputsModelId } from '../../../../common/store/inputs/constants';
import { useGlobalTime } from '../../../../common/containers/use_global_time';
import { useSourcererDataView } from '../../../../common/containers/sourcerer';
@ -136,20 +131,9 @@ export const UserDetails: React.FC<UserDetailsProps> = ({ userName, timestamp, s
),
render: (host: string) => (
<EuiText grow={false} size="xs">
<SecurityCellActions
data={{
value: host,
field: 'host.name',
}}
mode={CellActionsMode.HOVER_RIGHT}
triggerId={SecurityCellActionsTrigger.DETAILS_FLYOUT}
visibleCellActions={6}
sourcererScopeId={getSourcererScopeId(scopeId)}
metadata={{ scopeId }}
showActionTooltips
>
<CellActions field={'host.name'} value={host}>
{host}
</SecurityCellActions>
</CellActions>
</EuiText>
),
},
@ -191,7 +175,7 @@ export const UserDetails: React.FC<UserDetailsProps> = ({ userName, timestamp, s
]
: []),
],
[isEntityAnalyticsAuthorized, scopeId]
[isEntityAnalyticsAuthorized]
);
const relatedHostsCount = useMemo(

View file

@ -59,6 +59,30 @@ describe('<AnalyzerPreview />', () => {
expect(wrapper.getByTestId(ANALYZER_PREVIEW_TEST_ID)).toBeInTheDocument();
});
it('should use ancestor id when in preview', () => {
mockUseAlertPrevalenceFromProcessTree.mockReturnValue({
loading: false,
error: false,
alertIds: ['alertid'],
statsNodes: mock.mockStatsNodes,
});
const contextValue = {
...mockContextValue,
getFieldsData: () => 'ancestors-id',
dataFormattedForFieldBrowser: mockDataFormattedForFieldBrowser,
isPreview: true,
};
const wrapper = renderAnalyzerPreview(contextValue);
expect(mockUseAlertPrevalenceFromProcessTree).toHaveBeenCalledWith({
isActiveTimeline: false,
documentId: 'ancestors-id',
indices: ['rule-indices'],
});
expect(wrapper.getByTestId(ANALYZER_PREVIEW_TEST_ID)).toBeInTheDocument();
});
it('shows error message when index is not present', () => {
mockUseAlertPrevalenceFromProcessTree.mockReturnValue({
loading: false,

View file

@ -11,11 +11,12 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { ANALYZER_PREVIEW_TEST_ID, ANALYZER_PREVIEW_LOADING_TEST_ID } from './test_ids';
import { getTreeNodes } from '../utils/analyzer_helpers';
import { RULE_INDICES } from '../../shared/constants/field_names';
import { ANCESTOR_ID, RULE_INDICES } from '../../shared/constants/field_names';
import { useRightPanelContext } from '../context';
import { useAlertPrevalenceFromProcessTree } from '../../../../common/containers/alerts/use_alert_prevalence_from_process_tree';
import type { StatsNode } from '../../../../common/containers/alerts/use_alert_prevalence_from_process_tree';
import { isActiveTimeline } from '../../../../helpers';
import { getField } from '../../shared/utils';
const CHILD_COUNT_LIMIT = 3;
const ANCESTOR_LEVEL = 3;
@ -33,14 +34,23 @@ interface Cache {
*/
export const AnalyzerPreview: React.FC = () => {
const [cache, setCache] = useState<Partial<Cache>>({});
const { dataFormattedForFieldBrowser: data, scopeId, eventId } = useRightPanelContext();
const {
dataFormattedForFieldBrowser: data,
getFieldsData,
scopeId,
eventId,
isPreview,
} = useRightPanelContext();
const ancestorId = getField(getFieldsData(ANCESTOR_ID)) ?? '';
const documentId = isPreview ? ancestorId : eventId; // use ancestor as fallback for alert preview
const index = find({ category: 'kibana', field: RULE_INDICES }, data);
const indices = index?.values ?? [];
const { statsNodes, loading, error } = useAlertPrevalenceFromProcessTree({
isActiveTimeline: isActiveTimeline(scopeId),
documentId: eventId,
documentId,
indices,
});

View file

@ -12,7 +12,7 @@ import React, { memo, useCallback, useMemo, useState } from 'react';
import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiTitle, EuiToolTip } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { getEmptyTagValue } from '../../../../common/components/empty_value';
import { useUpsellingMessage } from '../../../../common/hooks/use_upselling';
import { useLicense } from '../../../../common/hooks/use_license';
import { useAlertsPrivileges } from '../../../../detections/containers/detection_engine/alerts/use_alerts_privileges';
@ -60,13 +60,18 @@ export interface AssigneesProps {
* Callback to handle the successful assignees update
*/
onAssigneesUpdated?: () => void;
/**
* Boolean to indicate whether it is a preview flyout
*/
isPreview?: boolean;
}
/**
* Document assignees details displayed in flyout right section header
*/
export const Assignees: FC<AssigneesProps> = memo(
({ eventId, assignedUserIds, onAssigneesUpdated }) => {
({ eventId, assignedUserIds, onAssigneesUpdated, isPreview }) => {
const isPlatinumPlus = useLicense().isPlatinumPlus();
const upsellingMessage = useUpsellingMessage('alert_assignments');
@ -123,34 +128,40 @@ export const Assignees: FC<AssigneesProps> = memo(
</h3>
</EuiTitle>
</EuiFlexItem>
{assignedUsers && (
<EuiFlexItem grow={false}>
<UsersAvatarsPanel userProfiles={assignedUsers} maxVisibleAvatars={2} />
</EuiFlexItem>
)}
<EuiFlexItem grow={false}>
<AssigneesPopover
assignedUserIds={assignedUserIds}
button={
<UpdateAssigneesButton
togglePopover={togglePopover}
isDisabled={!hasIndexWrite || !isPlatinumPlus}
toolTipMessage={
upsellingMessage ??
i18n.translate(
'xpack.securitySolution.flyout.right.visualizations.assignees.popoverTooltip',
{
defaultMessage: 'Assign alert',
{isPreview ? (
getEmptyTagValue()
) : (
<EuiFlexGroup gutterSize="none" responsive={false}>
{assignedUsers && (
<EuiFlexItem grow={false}>
<UsersAvatarsPanel userProfiles={assignedUsers} maxVisibleAvatars={2} />
</EuiFlexItem>
)}
<EuiFlexItem grow={false}>
<AssigneesPopover
assignedUserIds={assignedUserIds}
button={
<UpdateAssigneesButton
togglePopover={togglePopover}
isDisabled={!hasIndexWrite || !isPlatinumPlus}
toolTipMessage={
upsellingMessage ??
i18n.translate(
'xpack.securitySolution.flyout.right.visualizations.assignees.popoverTooltip',
{
defaultMessage: 'Assign alert',
}
)
}
)
/>
}
isPopoverOpen={isPopoverOpen}
closePopover={togglePopover}
onAssigneesApply={onAssigneesApply}
/>
}
isPopoverOpen={isPopoverOpen}
closePopover={togglePopover}
onAssigneesApply={onAssigneesApply}
/>
</EuiFlexItem>
</EuiFlexItem>
</EuiFlexGroup>
)}
</EuiFlexGroup>
);
}

View file

@ -0,0 +1,72 @@
/*
* 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 { FC } from 'react';
import React, { useMemo } from 'react';
import { useRightPanelContext } from '../context';
import { getSourcererScopeId } from '../../../../helpers';
import { useBasicDataFromDetailsData } from '../../../../timelines/components/side_panel/event_details/helpers';
import { SecurityCellActionType } from '../../../../actions/constants';
import {
CellActionsMode,
SecurityCellActions,
SecurityCellActionsTrigger,
} from '../../../../common/components/cell_actions';
interface CellActionsProps {
/**
* Field name
*/
field: string;
/**
* Field value
*/
value: string[] | string | null | undefined;
/**
* Boolean to indicate if value is an object array
*/
isObjectArray?: boolean;
/**
* React components to render
*/
children: React.ReactNode | string;
}
/**
* Security cell action wrapper for document details flyout
*/
export const CellActions: FC<CellActionsProps> = ({ field, value, isObjectArray, children }) => {
const { dataFormattedForFieldBrowser, scopeId, isPreview } = useRightPanelContext();
const { isAlert } = useBasicDataFromDetailsData(dataFormattedForFieldBrowser);
const triggerId = isAlert
? SecurityCellActionsTrigger.DETAILS_FLYOUT
: SecurityCellActionsTrigger.DEFAULT;
const data = useMemo(() => ({ field, value }), [field, value]);
const metadata = useMemo(() => ({ scopeId, isObjectArray }), [scopeId, isObjectArray]);
const disabledActionTypes = useMemo(
() => (isPreview ? [SecurityCellActionType.FILTER, SecurityCellActionType.TOGGLE_COLUMN] : []),
[isPreview]
);
return (
<SecurityCellActions
data={data}
mode={CellActionsMode.HOVER_RIGHT}
triggerId={triggerId}
visibleCellActions={6}
sourcererScopeId={getSourcererScopeId(scopeId)}
metadata={metadata}
disabledActionTypes={disabledActionTypes}
>
{children}
</SecurityCellActions>
);
};
CellActions.displayName = 'CellActions';

View file

@ -86,7 +86,9 @@ const NO_DATA_MESSAGE = 'No correlations data available.';
describe('<CorrelationsOverview />', () => {
it('should render wrapper component', () => {
jest.mocked(useShowRelatedAlertsByAncestry).mockReturnValue({ show: false });
jest
.mocked(useShowRelatedAlertsByAncestry)
.mockReturnValue({ show: false, documentId: 'event-id' });
jest.mocked(useShowRelatedAlertsBySameSourceEvent).mockReturnValue({ show: false });
jest.mocked(useShowRelatedAlertsBySession).mockReturnValue({ show: false });
jest.mocked(useShowRelatedCases).mockReturnValue(false);
@ -102,7 +104,7 @@ describe('<CorrelationsOverview />', () => {
it('should show component with all rows in expandable panel', () => {
jest
.mocked(useShowRelatedAlertsByAncestry)
.mockReturnValue({ show: true, indices: ['index1'] });
.mockReturnValue({ show: true, documentId: 'event-id', indices: ['index1'] });
jest
.mocked(useShowRelatedAlertsBySameSourceEvent)
.mockReturnValue({ show: true, originalEventId: 'originalEventId' });
@ -145,7 +147,7 @@ describe('<CorrelationsOverview />', () => {
it('should hide rows and show error message if show values are false', () => {
jest
.mocked(useShowRelatedAlertsByAncestry)
.mockReturnValue({ show: false, indices: ['index1'] });
.mockReturnValue({ show: false, documentId: 'event-id', indices: ['index1'] });
jest
.mocked(useShowRelatedAlertsBySameSourceEvent)
.mockReturnValue({ show: false, originalEventId: 'originalEventId' });
@ -165,7 +167,9 @@ describe('<CorrelationsOverview />', () => {
});
it('should hide rows if values are null', () => {
jest.mocked(useShowRelatedAlertsByAncestry).mockReturnValue({ show: true });
jest
.mocked(useShowRelatedAlertsByAncestry)
.mockReturnValue({ show: true, documentId: 'event-id' });
jest.mocked(useShowRelatedAlertsBySameSourceEvent).mockReturnValue({ show: true });
jest.mocked(useShowRelatedAlertsBySession).mockReturnValue({ show: true });
jest.mocked(useShowRelatedCases).mockReturnValue(false);

View file

@ -38,6 +38,7 @@ export const CorrelationsOverview: React.FC = () => {
indexName,
getFieldsData,
scopeId,
isPreview,
} = useRightPanelContext();
const { openLeftPanel } = useExpandableFlyoutContext();
@ -56,10 +57,16 @@ export const CorrelationsOverview: React.FC = () => {
});
}, [eventId, openLeftPanel, indexName, scopeId]);
const { show: showAlertsByAncestry, indices } = useShowRelatedAlertsByAncestry({
const {
show: showAlertsByAncestry,
documentId,
indices,
} = useShowRelatedAlertsByAncestry({
getFieldsData,
dataAsNestedObject,
dataFormattedForFieldBrowser,
eventId,
isPreview,
});
const { show: showSameSourceAlerts, originalEventId } = useShowRelatedAlertsBySameSourceEvent({
getFieldsData,
@ -111,8 +118,8 @@ export const CorrelationsOverview: React.FC = () => {
{showAlertsBySession && entityId && (
<RelatedAlertsBySession entityId={entityId} scopeId={scopeId} />
)}
{showAlertsByAncestry && indices && (
<RelatedAlertsByAncestry documentId={eventId} indices={indices} scopeId={scopeId} />
{showAlertsByAncestry && documentId && indices && (
<RelatedAlertsByAncestry documentId={documentId} indices={indices} scopeId={scopeId} />
)}
</EuiFlexGroup>
) : (

View file

@ -68,11 +68,13 @@ export const EntitiesOverview: React.FC = () => {
{userName || hostName ? (
<EuiFlexGroup direction="column" gutterSize="s" responsive={false}>
{userName && (
<EuiFlexItem>
<UserEntityOverview userName={userName} />
</EuiFlexItem>
<>
<EuiFlexItem>
<UserEntityOverview userName={userName} />
</EuiFlexItem>
<EuiSpacer size="s" />
</>
)}
<EuiSpacer size="s" />
{hostName && (
<EuiFlexItem>
<HostEntityOverview hostName={hostName} />

View file

@ -10,17 +10,11 @@ import React, { useMemo } from 'react';
import type { EuiBasicTableColumn } from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem, EuiInMemoryTable, EuiPanel, EuiTitle } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { getSourcererScopeId } from '../../../../helpers';
import { convertHighlightedFieldsToTableRow } from '../../shared/utils/highlighted_fields_helpers';
import { useRuleWithFallback } from '../../../../detection_engine/rule_management/logic/use_rule_with_fallback';
import { useBasicDataFromDetailsData } from '../../../../timelines/components/side_panel/event_details/helpers';
import { HighlightedFieldsCell } from './highlighted_fields_cell';
import { SecurityCellActionType } from '../../../../actions/constants';
import {
CellActionsMode,
SecurityCellActions,
SecurityCellActionsTrigger,
} from '../../../../common/components/cell_actions';
import { CellActions } from './cell_actions';
import { HIGHLIGHTED_FIELDS_DETAILS_TEST_ID, HIGHLIGHTED_FIELDS_TITLE_TEST_ID } from './test_ids';
import { useRightPanelContext } from '../context';
import { useHighlightedFields } from '../../shared/hooks/use_highlighted_fields';
@ -83,28 +77,13 @@ const columns: Array<EuiBasicTableColumn<HighlightedFieldsTableRow>> = [
scopeId: string;
isPreview: boolean;
}) => (
<SecurityCellActions
data={{
field: description.field,
value: description.values,
}}
mode={CellActionsMode.HOVER_RIGHT}
triggerId={SecurityCellActionsTrigger.DETAILS_FLYOUT}
visibleCellActions={6}
sourcererScopeId={getSourcererScopeId(description.scopeId)}
metadata={{ scopeId: description.scopeId }}
disabledActionTypes={
description.isPreview
? [SecurityCellActionType.FILTER, SecurityCellActionType.TOGGLE_COLUMN]
: []
}
>
<CellActions field={description.field} value={description.values}>
<HighlightedFieldsCell
values={description.values}
field={description.field}
originalField={description.originalField}
/>
</SecurityCellActions>
</CellActions>
),
},
];

View file

@ -27,7 +27,6 @@ import {
} from '../../../../common/components/first_last_seen/first_last_seen';
import { buildHostNamesFilter, RiskScoreEntity } from '../../../../../common/search_strategy';
import { getEmptyTagValue } from '../../../../common/components/empty_value';
import { DefaultFieldRenderer } from '../../../../timelines/components/field_renderers/field_renderers';
import { DescriptionListStyled } from '../../../../common/components/page';
import { OverviewDescriptionList } from '../../../../common/components/overview_description_list';
import { RiskScoreLevel } from '../../../../explore/components/risk_score/severity/common';
@ -35,6 +34,8 @@ import { useSourcererDataView } from '../../../../common/containers/sourcerer';
import { useGlobalTime } from '../../../../common/containers/use_global_time';
import { useRiskScore } from '../../../../explore/containers/risk_score';
import { useHostDetails } from '../../../../explore/hosts/containers/hosts/details';
import { getField } from '../../shared/utils';
import { CellActions } from './cell_actions';
import {
FAMILY,
LAST_SEEN,
@ -53,7 +54,6 @@ import { LeftPanelInsightsTab, DocumentDetailsLeftPanelKey } from '../../left';
import { RiskScoreDocTooltip } from '../../../../overview/components/common';
const HOST_ICON = 'storage';
const CONTEXT_ID = `flyout-host-entity-overview`;
export interface HostEntityOverviewProps {
/**
@ -114,21 +114,24 @@ export const HostEntityOverview: React.FC<HostEntityOverviewProps> = ({ hostName
endDate: to,
});
const hostOSFamilyValue = useMemo(
() => getField(getOr([], 'host.os.family', hostDetails)),
[hostDetails]
);
const hostOSFamily: DescriptionList[] = useMemo(
() => [
{
title: FAMILY,
description: (
<DefaultFieldRenderer
rowItems={getOr([], 'host.os.family', hostDetails)}
attrName={'host.os.family'}
idPrefix={CONTEXT_ID}
isDraggable={false}
/>
description: hostOSFamilyValue ? (
<CellActions field={'host.os.family'} value={hostOSFamilyValue}>
{hostOSFamilyValue}
</CellActions>
) : (
getEmptyTagValue()
),
},
],
[hostDetails]
[hostOSFamilyValue]
);
const hostLastSeen: DescriptionList[] = useMemo(

View file

@ -9,10 +9,7 @@ import type { FC } from 'react';
import React, { memo } from 'react';
import { ALERT_SEVERITY } from '@kbn/rule-data-utils';
import type { Severity } from '@kbn/securitysolution-io-ts-alerting-types';
import { CellActionsMode } from '@kbn/cell-actions';
import { getSourcererScopeId } from '../../../../helpers';
import { SecurityCellActions } from '../../../../common/components/cell_actions';
import { SecurityCellActionsTrigger } from '../../../../actions/constants';
import { CellActions } from './cell_actions';
import { useRightPanelContext } from '../context';
import { SeverityBadge } from '../../../../detections/components/rules/severity_badge';
@ -23,7 +20,7 @@ const isSeverity = (x: unknown): x is Severity =>
* Document details severity displayed in flyout right section header
*/
export const DocumentSeverity: FC = memo(() => {
const { getFieldsData, scopeId } = useRightPanelContext();
const { getFieldsData } = useRightPanelContext();
const fieldsData = getFieldsData(ALERT_SEVERITY);
if (!fieldsData) {
@ -40,19 +37,9 @@ export const DocumentSeverity: FC = memo(() => {
}
return (
<SecurityCellActions
data={{
field: ALERT_SEVERITY,
value: alertSeverity,
}}
mode={CellActionsMode.HOVER_RIGHT}
triggerId={SecurityCellActionsTrigger.DETAILS_FLYOUT}
visibleCellActions={6}
sourcererScopeId={getSourcererScopeId(scopeId)}
metadata={{ scopeId }}
>
<CellActions field={ALERT_SEVERITY} value={alertSeverity}>
<SeverityBadge value={alertSeverity} />
</SecurityCellActions>
</CellActions>
);
});

View file

@ -8,10 +8,10 @@
import type { FC } from 'react';
import React, { useMemo } from 'react';
import { find } from 'lodash/fp';
import { FormattedMessage } from '@kbn/i18n-react';
import { useExpandableFlyoutContext } from '@kbn/expandable-flyout';
import { CellActionsMode } from '@kbn/cell-actions';
import { getSourcererScopeId } from '../../../../helpers';
import { SecurityCellActions } from '../../../../common/components/cell_actions';
import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui';
import { getEmptyTagValue } from '../../../../common/components/empty_value';
import type {
EnrichedFieldInfo,
EnrichedFieldInfoWithValues,
@ -20,7 +20,8 @@ import { SIGNAL_STATUS_FIELD_NAME } from '../../../../timelines/components/timel
import { StatusPopoverButton } from '../../../../common/components/event_details/overview/status_popover_button';
import { useRightPanelContext } from '../context';
import { getEnrichedFieldInfo } from '../../../../common/components/event_details/helpers';
import { SecurityCellActionsTrigger } from '../../../../actions/constants';
import { CellActions } from './cell_actions';
import { STATUS_TITLE_TEST_ID } from './test_ids';
/**
* Checks if the field info has data to convert EnrichedFieldInfo into EnrichedFieldInfoWithValues
@ -34,7 +35,8 @@ function hasData(fieldInfo?: EnrichedFieldInfo): fieldInfo is EnrichedFieldInfoW
*/
export const DocumentStatus: FC = () => {
const { closeFlyout } = useExpandableFlyoutContext();
const { eventId, browserFields, dataFormattedForFieldBrowser, scopeId } = useRightPanelContext();
const { eventId, browserFields, dataFormattedForFieldBrowser, scopeId, isPreview } =
useRightPanelContext();
const statusData = useMemo(() => {
const item = find(
@ -56,25 +58,33 @@ export const DocumentStatus: FC = () => {
if (!statusData || !hasData(statusData)) return null;
return (
<SecurityCellActions
data={{
field: SIGNAL_STATUS_FIELD_NAME,
value: statusData.values[0],
}}
mode={CellActionsMode.HOVER_RIGHT}
triggerId={SecurityCellActionsTrigger.DETAILS_FLYOUT}
visibleCellActions={6}
sourcererScopeId={getSourcererScopeId(scopeId)}
metadata={{ scopeId }}
>
<StatusPopoverButton
eventId={eventId}
contextId={scopeId}
enrichedFieldInfo={statusData}
scopeId={scopeId}
handleOnEventClosed={closeFlyout}
/>
</SecurityCellActions>
<EuiFlexGroup direction="column" gutterSize="xs" responsive={false}>
<EuiFlexItem grow={false}>
<EuiTitle size="xxs" data-test-subj={STATUS_TITLE_TEST_ID}>
<h3>
<FormattedMessage
id="xpack.securitySolution.flyout.right.header.statusTitle"
defaultMessage="Status"
/>
</h3>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
{!statusData || !hasData(statusData) || isPreview ? (
getEmptyTagValue()
) : (
<CellActions field={SIGNAL_STATUS_FIELD_NAME} value={statusData.values[0]}>
<StatusPopoverButton
eventId={eventId}
contextId={scopeId}
enrichedFieldInfo={statusData}
scopeId={scopeId}
handleOnEventClosed={closeFlyout}
/>
</CellActions>
)}
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -12,6 +12,7 @@ import { CONTENT_TEST_ID, HEADER_TEST_ID } from './expandable_section';
const FLYOUT_HEADER_TEST_ID = `${PREFIX}Header` as const;
export const FLYOUT_HEADER_TITLE_TEST_ID = `${FLYOUT_HEADER_TEST_ID}Title` as const;
export const STATUS_TITLE_TEST_ID = `${FLYOUT_HEADER_TEST_ID}StatusTitle` as const;
export const STATUS_BUTTON_TEST_ID = 'rule-status-badge' as const;
export const SEVERITY_VALUE_TEST_ID = 'severity' as const;
export const RISK_SCORE_TITLE_TEST_ID = `${FLYOUT_HEADER_TEST_ID}RiskScoreTitle` as const;

View file

@ -23,13 +23,14 @@ import { LeftPanelInsightsTab, DocumentDetailsLeftPanelKey } from '../../left';
import { ENTITIES_TAB_ID } from '../../left/components/entities_details';
import { useRightPanelContext } from '../context';
import type { DescriptionList } from '../../../../../common/utility_types';
import { getField } from '../../shared/utils';
import { CellActions } from './cell_actions';
import {
FirstLastSeen,
FirstLastSeenType,
} from '../../../../common/components/first_last_seen/first_last_seen';
import { buildUserNamesFilter, RiskScoreEntity } from '../../../../../common/search_strategy';
import { getEmptyTagValue } from '../../../../common/components/empty_value';
import { DefaultFieldRenderer } from '../../../../timelines/components/field_renderers/field_renderers';
import { DescriptionListStyled } from '../../../../common/components/page';
import { OverviewDescriptionList } from '../../../../common/components/overview_description_list';
import { RiskScoreLevel } from '../../../../explore/components/risk_score/severity/common';
@ -53,7 +54,6 @@ import { useObservedUserDetails } from '../../../../explore/users/containers/use
import { RiskScoreDocTooltip } from '../../../../overview/components/common';
const USER_ICON = 'user';
const CONTEXT_ID = `flyout-user-entity-overview`;
export interface UserEntityOverviewProps {
/**
@ -112,21 +112,24 @@ export const UserEntityOverview: React.FC<UserEntityOverviewProps> = ({ userName
timerange,
});
const userDomainValue = useMemo(
() => getField(getOr([], 'user.domain', userDetails)),
[userDetails]
);
const userDomain: DescriptionList[] = useMemo(
() => [
{
title: USER_DOMAIN,
description: (
<DefaultFieldRenderer
rowItems={getOr([], 'user.domain', userDetails)}
attrName={'domain'}
idPrefix={CONTEXT_ID}
isDraggable={false}
/>
description: userDomainValue ? (
<CellActions field={'user.domain'} value={userDomainValue}>
{userDomainValue}
</CellActions>
) : (
getEmptyTagValue()
),
},
],
[userDetails]
[userDomainValue]
);
const userLastSeen: DescriptionList[] = useMemo(

View file

@ -14,12 +14,7 @@ import type { EventFieldsData } from '../../../../common/components/event_detail
import { FieldValueCell } from '../../../../common/components/event_details/table/field_value_cell';
import type { BrowserField, BrowserFields } from '../../../../../common/search_strategy';
import { FieldNameCell } from '../../../../common/components/event_details/table/field_name_cell';
import {
CellActionsMode,
SecurityCellActions,
SecurityCellActionsTrigger,
} from '../../../../common/components/cell_actions';
import { getSourcererScopeId } from '../../../../helpers';
import { CellActions } from '../components/cell_actions';
import * as i18n from '../../../../common/components/event_details/translations';
import { useRightPanelContext } from '../context';
import type { ColumnsProvider } from '../../../../common/components/event_details/event_fields_browser';
@ -68,17 +63,7 @@ export const getColumns: ColumnsProvider = ({
browserFields
);
return (
<SecurityCellActions
data={{
field: data.field,
value: values,
}}
triggerId={SecurityCellActionsTrigger.DETAILS_FLYOUT}
mode={CellActionsMode.HOVER_RIGHT}
visibleCellActions={6}
sourcererScopeId={getSourcererScopeId(scopeId)}
metadata={{ scopeId, isObjectArray: data.isObjectArray }}
>
<CellActions field={data.field} value={values} isObjectArray={data.isObjectArray}>
<FieldValueCell
contextId={contextId}
data={data as EventFieldsData}
@ -88,7 +73,7 @@ export const getColumns: ColumnsProvider = ({
isDraggable={isDraggable}
values={values}
/>
</SecurityCellActions>
</CellActions>
);
},
},

View file

@ -34,7 +34,7 @@ jest.mock(
'../../../../detections/components/alerts_table/timeline_actions/investigate_in_resolver'
);
const licenseServiceMock = licenseService as jest.Mocked<typeof licenseService>;
const eventId = 'event-id';
const dataAsNestedObject = mockDataAsNestedObject;
const dataFormattedForFieldBrowser = mockDataFormattedForFieldBrowser;
@ -54,10 +54,16 @@ describe('useShowRelatedAlertsByAncestry', () => {
getFieldsData,
dataAsNestedObject,
dataFormattedForFieldBrowser,
eventId,
isPreview: false,
})
);
expect(hookResult.result.current).toEqual({ show: false, indices: ['rule-parameters-index'] });
expect(hookResult.result.current).toEqual({
show: false,
documentId: 'event-id',
indices: ['rule-parameters-index'],
});
});
it(`should return false if feature isn't enabled`, () => {
@ -69,11 +75,14 @@ describe('useShowRelatedAlertsByAncestry', () => {
getFieldsData,
dataAsNestedObject,
dataFormattedForFieldBrowser,
eventId,
isPreview: false,
})
);
expect(hookResult.result.current).toEqual({
show: false,
documentId: 'event-id',
indices: ['rule-parameters-index'],
});
});
@ -87,11 +96,58 @@ describe('useShowRelatedAlertsByAncestry', () => {
getFieldsData,
dataAsNestedObject,
dataFormattedForFieldBrowser,
eventId,
isPreview: false,
})
);
expect(hookResult.result.current).toEqual({
show: false,
documentId: 'event-id',
indices: ['rule-parameters-index'],
});
});
it('should return true and event id as document id by default ', () => {
(useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(true);
(useIsInvestigateInResolverActionEnabled as jest.Mock).mockReturnValue(true);
licenseServiceMock.isPlatinumPlus.mockReturnValue(true);
const getFieldsData = () => 'ancestors-id';
hookResult = renderHook(() =>
useShowRelatedAlertsByAncestry({
getFieldsData,
dataAsNestedObject,
dataFormattedForFieldBrowser,
eventId,
isPreview: false,
})
);
expect(hookResult.result.current).toEqual({
show: true,
documentId: 'event-id',
indices: ['rule-parameters-index'],
});
});
it('should return true and ancestor id as document id if flyout is open in preview', () => {
(useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(true);
(useIsInvestigateInResolverActionEnabled as jest.Mock).mockReturnValue(true);
licenseServiceMock.isPlatinumPlus.mockReturnValue(true);
const getFieldsData = () => 'ancestors-id';
hookResult = renderHook(() =>
useShowRelatedAlertsByAncestry({
getFieldsData,
dataAsNestedObject,
dataFormattedForFieldBrowser,
eventId,
isPreview: true,
})
);
expect(hookResult.result.current).toEqual({
show: true,
documentId: 'ancestors-id',
indices: ['rule-parameters-index'],
});
});

View file

@ -13,7 +13,8 @@ import type { GetFieldsData } from '../../../../common/hooks/use_get_fields_data
import { useIsInvestigateInResolverActionEnabled } from '../../../../detections/components/alerts_table/timeline_actions/investigate_in_resolver';
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
import { useLicense } from '../../../../common/hooks/use_license';
import { RULE_PARAMETERS_INDEX } from '../constants/field_names';
import { ANCESTOR_ID, RULE_PARAMETERS_INDEX } from '../constants/field_names';
import { getField } from '../utils';
export interface UseShowRelatedAlertsByAncestryParams {
/**
@ -28,6 +29,14 @@ export interface UseShowRelatedAlertsByAncestryParams {
* An array of field objects with category and value
*/
dataFormattedForFieldBrowser: TimelineEventsDetailsItem[];
/**
* Id of the event document
*/
eventId: string;
/**
* Boolean indicating if the flyout is open in preview
*/
isPreview: boolean;
}
export interface UseShowRelatedAlertsByAncestryResult {
@ -39,6 +48,10 @@ export interface UseShowRelatedAlertsByAncestryResult {
* Values of the kibana.alert.rule.parameters.index field
*/
indices?: string[];
/**
* Value of the document id for fetching ancestry alerts
*/
documentId: string;
}
/**
@ -48,12 +61,16 @@ export const useShowRelatedAlertsByAncestry = ({
getFieldsData,
dataAsNestedObject,
dataFormattedForFieldBrowser,
eventId,
isPreview,
}: UseShowRelatedAlertsByAncestryParams): UseShowRelatedAlertsByAncestryResult => {
const isRelatedAlertsByProcessAncestryEnabled = useIsExperimentalFeatureEnabled(
'insightsRelatedAlertsByProcessAncestry'
);
const hasProcessEntityInfo = useIsInvestigateInResolverActionEnabled(dataAsNestedObject);
const ancestorId = getField(getFieldsData(ANCESTOR_ID)) ?? '';
const documentId = isPreview ? ancestorId : eventId;
// can't use getFieldsData here as the kibana.alert.rule.parameters is different and can be nested
const originalDocumentIndex = useMemo(
() => find({ category: 'kibana', field: RULE_PARAMETERS_INDEX }, dataFormattedForFieldBrowser),
@ -70,6 +87,7 @@ export const useShowRelatedAlertsByAncestry = ({
return {
show,
documentId,
...(originalDocumentIndex &&
originalDocumentIndex.values && { indices: originalDocumentIndex.values }),
};