mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[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\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\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\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:
parent
7a6ea82147
commit
6cfaf54ea8
23 changed files with 454 additions and 216 deletions
|
@ -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';
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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'
|
||||
);
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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'
|
||||
);
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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';
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
) : (
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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'],
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 }),
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue