[Security Solution] Expandable flyout - add suppressed alerts to correlations (#164649)

This commit is contained in:
christineweng 2023-08-26 10:28:27 -05:00 committed by GitHub
parent b88235aafe
commit ef7ea49b1e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 755 additions and 54 deletions

View file

@ -97,6 +97,7 @@ const RowActionComponent = ({
},
};
// excluding rule preview page as some sections in new flyout are not applicable when user is creating a new rule
if (isSecurityFlyoutEnabled && tableId !== TableId.rulePreview) {
openFlyout({
right: {

View file

@ -7,6 +7,7 @@
import React, { useMemo, useCallback } from 'react';
import { EuiButton, EuiButtonEmpty } from '@elastic/eui';
import type { IconType } from '@elastic/eui';
import type { Filter } from '@kbn/es-query';
import { useDispatch } from 'react-redux';
@ -31,11 +32,21 @@ export interface InvestigateInTimelineButtonProps {
timeRange?: TimeRange;
keepDataView?: boolean;
isDisabled?: boolean;
iconType?: IconType;
}
export const InvestigateInTimelineButton: React.FunctionComponent<
InvestigateInTimelineButtonProps
> = ({ asEmptyButton, children, dataProviders, filters, timeRange, keepDataView, ...rest }) => {
> = ({
asEmptyButton,
children,
dataProviders,
filters,
timeRange,
keepDataView,
iconType,
...rest
}) => {
const dispatch = useDispatch();
const getDataViewsSelector = useMemo(
@ -113,6 +124,7 @@ export const InvestigateInTimelineButton: React.FunctionComponent<
onClick={configureAndOpenTimeline}
flush="right"
size="xs"
iconType={iconType}
>
{children}
</EuiButtonEmpty>

View file

@ -7,7 +7,13 @@
import type { MouseEvent } from 'react';
import React from 'react';
import { EuiContextMenuItem, EuiButtonIcon, EuiToolTip, EuiText } from '@elastic/eui';
import {
EuiContextMenuItem,
EuiButtonIcon,
EuiToolTip,
EuiText,
EuiButtonEmpty,
} from '@elastic/eui';
import { EventsTdContent } from '../../../timelines/components/timeline/styles';
import { DEFAULT_ACTION_BUTTON_WIDTH } from '.';
@ -20,7 +26,7 @@ interface ActionIconItemProps {
isDisabled?: boolean;
onClick?: (event: MouseEvent) => void;
children?: React.ReactNode;
buttonType?: 'text' | 'icon';
buttonType?: 'text' | 'icon' | 'emptyButton';
}
const ActionIconItemComponent: React.FC<ActionIconItemProps> = ({
@ -67,6 +73,17 @@ const ActionIconItemComponent: React.FC<ActionIconItemProps> = ({
</EuiText>
</EuiContextMenuItem>
)}
{buttonType === 'emptyButton' && (
<EuiButtonEmpty
onClick={onClick}
iconType="timeline"
flush="right"
size="xs"
data-test-subj={dataTestSubj}
>
{content}
</EuiButtonEmpty>
)}
</>
);

View file

@ -18,7 +18,7 @@ import { useInvestigateInTimeline } from './use_investigate_in_timeline';
interface InvestigateInTimelineActionProps {
ecsRowData?: Ecs | null;
ariaLabel?: string;
buttonType?: 'text' | 'icon';
buttonType?: 'text' | 'icon' | 'emptyButton';
onInvestigateInTimelineAlertClick?: () => void;
}

View file

@ -14,15 +14,13 @@ import { useShowRelatedAlertsByAncestry } from '../../shared/hooks/use_show_rela
import { useShowRelatedAlertsBySameSourceEvent } from '../../shared/hooks/use_show_related_alerts_by_same_source_event';
import { useShowRelatedAlertsBySession } from '../../shared/hooks/use_show_related_alerts_by_session';
import { useShowRelatedCases } from '../../shared/hooks/use_show_related_cases';
import { useShowSuppressedAlerts } from '../../shared/hooks/use_show_suppressed_alerts';
import {
CORRELATIONS_DETAILS_BY_ANCESTRY_SECTION_TABLE_TEST_ID,
CORRELATIONS_DETAILS_BY_ANCESTRY_SECTION_TEST_ID,
CORRELATIONS_DETAILS_BY_SESSION_SECTION_TABLE_TEST_ID,
CORRELATIONS_DETAILS_BY_SESSION_SECTION_TEST_ID,
CORRELATIONS_DETAILS_BY_SOURCE_SECTION_TABLE_TEST_ID,
CORRELATIONS_DETAILS_BY_SOURCE_SECTION_TEST_ID,
CORRELATIONS_DETAILS_CASES_SECTION_TABLE_TEST_ID,
CORRELATIONS_DETAILS_CASES_SECTION_TEST_ID,
CORRELATIONS_DETAILS_SUPPRESSED_ALERTS_SECTION_TEST_ID,
CORRELATIONS_DETAILS_TEST_ID,
} from './test_ids';
import { useFetchRelatedAlertsBySession } from '../../shared/hooks/use_fetch_related_alerts_by_session';
@ -30,6 +28,7 @@ import { useFetchRelatedAlertsByAncestry } from '../../shared/hooks/use_fetch_re
import { useFetchRelatedAlertsBySameSourceEvent } from '../../shared/hooks/use_fetch_related_alerts_by_same_source_event';
import { useFetchRelatedCases } from '../../shared/hooks/use_fetch_related_cases';
import { mockContextValue } from '../mocks/mock_context';
import { EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID } from '../../shared/components/test_ids';
jest.mock('react-router-dom', () => {
const actual = jest.requireActual('react-router-dom');
@ -39,6 +38,7 @@ jest.mock('../../shared/hooks/use_show_related_alerts_by_ancestry');
jest.mock('../../shared/hooks/use_show_related_alerts_by_same_source_event');
jest.mock('../../shared/hooks/use_show_related_alerts_by_session');
jest.mock('../../shared/hooks/use_show_related_cases');
jest.mock('../../shared/hooks/use_show_suppressed_alerts');
jest.mock('../../shared/hooks/use_fetch_related_alerts_by_session');
jest.mock('../../shared/hooks/use_fetch_related_alerts_by_ancestry');
jest.mock('../../shared/hooks/use_fetch_related_alerts_by_same_source_event');
@ -54,6 +54,11 @@ const renderCorrelationDetails = () => {
);
};
const CORRELATIONS_DETAILS_SUPPRESSED_ALERTS_TITLE_TEST_ID =
EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID(
CORRELATIONS_DETAILS_SUPPRESSED_ALERTS_SECTION_TEST_ID
);
describe('CorrelationsDetails', () => {
beforeEach(() => {
jest.clearAllMocks();
@ -70,6 +75,7 @@ describe('CorrelationsDetails', () => {
.mocked(useShowRelatedAlertsBySession)
.mockReturnValue({ show: true, entityId: 'entityId' });
jest.mocked(useShowRelatedCases).mockReturnValue(true);
jest.mocked(useShowSuppressedAlerts).mockReturnValue({ show: true, alertSuppressionCount: 1 });
(useFetchRelatedAlertsByAncestry as jest.Mock).mockReturnValue({
loading: false,
@ -102,6 +108,7 @@ describe('CorrelationsDetails', () => {
expect(getByTestId(CORRELATIONS_DETAILS_BY_SOURCE_SECTION_TABLE_TEST_ID)).toBeInTheDocument();
expect(getByTestId(CORRELATIONS_DETAILS_BY_SESSION_SECTION_TABLE_TEST_ID)).toBeInTheDocument();
expect(getByTestId(CORRELATIONS_DETAILS_CASES_SECTION_TABLE_TEST_ID)).toBeInTheDocument();
expect(getByTestId(CORRELATIONS_DETAILS_SUPPRESSED_ALERTS_TITLE_TEST_ID)).toBeInTheDocument();
});
it('should render no section and show error message if show values are false', () => {
@ -115,13 +122,23 @@ describe('CorrelationsDetails', () => {
.mocked(useShowRelatedAlertsBySession)
.mockReturnValue({ show: false, entityId: 'entityId' });
jest.mocked(useShowRelatedCases).mockReturnValue(false);
jest.mocked(useShowSuppressedAlerts).mockReturnValue({ show: false, alertSuppressionCount: 0 });
const { getByTestId, queryByTestId } = renderCorrelationDetails();
expect(queryByTestId(CORRELATIONS_DETAILS_BY_ANCESTRY_SECTION_TEST_ID)).not.toBeInTheDocument();
expect(queryByTestId(CORRELATIONS_DETAILS_BY_SOURCE_SECTION_TEST_ID)).not.toBeInTheDocument();
expect(queryByTestId(CORRELATIONS_DETAILS_BY_SESSION_SECTION_TEST_ID)).not.toBeInTheDocument();
expect(queryByTestId(CORRELATIONS_DETAILS_CASES_SECTION_TEST_ID)).not.toBeInTheDocument();
expect(
queryByTestId(CORRELATIONS_DETAILS_BY_ANCESTRY_SECTION_TABLE_TEST_ID)
).not.toBeInTheDocument();
expect(
queryByTestId(CORRELATIONS_DETAILS_BY_SOURCE_SECTION_TABLE_TEST_ID)
).not.toBeInTheDocument();
expect(
queryByTestId(CORRELATIONS_DETAILS_BY_SESSION_SECTION_TABLE_TEST_ID)
).not.toBeInTheDocument();
expect(queryByTestId(CORRELATIONS_DETAILS_CASES_SECTION_TABLE_TEST_ID)).not.toBeInTheDocument();
expect(
queryByTestId(CORRELATIONS_DETAILS_SUPPRESSED_ALERTS_TITLE_TEST_ID)
).not.toBeInTheDocument();
expect(getByTestId(`${CORRELATIONS_DETAILS_TEST_ID}Error`)).toBeInTheDocument();
});
@ -130,12 +147,22 @@ describe('CorrelationsDetails', () => {
jest.mocked(useShowRelatedAlertsBySameSourceEvent).mockReturnValue({ show: true });
jest.mocked(useShowRelatedAlertsBySession).mockReturnValue({ show: true });
jest.mocked(useShowRelatedCases).mockReturnValue(false);
jest.mocked(useShowSuppressedAlerts).mockReturnValue({ show: false, alertSuppressionCount: 0 });
const { queryByTestId } = renderCorrelationDetails();
expect(queryByTestId(CORRELATIONS_DETAILS_BY_ANCESTRY_SECTION_TEST_ID)).not.toBeInTheDocument();
expect(queryByTestId(CORRELATIONS_DETAILS_BY_SOURCE_SECTION_TEST_ID)).not.toBeInTheDocument();
expect(queryByTestId(CORRELATIONS_DETAILS_BY_SESSION_SECTION_TEST_ID)).not.toBeInTheDocument();
expect(queryByTestId(CORRELATIONS_DETAILS_CASES_SECTION_TEST_ID)).not.toBeInTheDocument();
expect(
queryByTestId(CORRELATIONS_DETAILS_BY_ANCESTRY_SECTION_TABLE_TEST_ID)
).not.toBeInTheDocument();
expect(
queryByTestId(CORRELATIONS_DETAILS_BY_SOURCE_SECTION_TABLE_TEST_ID)
).not.toBeInTheDocument();
expect(
queryByTestId(CORRELATIONS_DETAILS_BY_SESSION_SECTION_TABLE_TEST_ID)
).not.toBeInTheDocument();
expect(queryByTestId(CORRELATIONS_DETAILS_CASES_SECTION_TABLE_TEST_ID)).not.toBeInTheDocument();
expect(
queryByTestId(CORRELATIONS_DETAILS_SUPPRESSED_ALERTS_TITLE_TEST_ID)
).not.toBeInTheDocument();
});
});

View file

@ -6,7 +6,7 @@
*/
import React from 'react';
import { EuiSpacer } from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { CORRELATIONS_ERROR_MESSAGE } from './translations';
import { CORRELATIONS_DETAILS_TEST_ID } from './test_ids';
import { RelatedAlertsBySession } from './related_alerts_by_session';
@ -14,11 +14,12 @@ import { RelatedAlertsBySameSourceEvent } from './related_alerts_by_same_source_
import { RelatedCases } from './related_cases';
import { useShowRelatedCases } from '../../shared/hooks/use_show_related_cases';
import { useShowRelatedAlertsByAncestry } from '../../shared/hooks/use_show_related_alerts_by_ancestry';
import { useShowSuppressedAlerts } from '../../shared/hooks/use_show_suppressed_alerts';
import { useLeftPanelContext } from '../context';
import { useShowRelatedAlertsBySameSourceEvent } from '../../shared/hooks/use_show_related_alerts_by_same_source_event';
import { useShowRelatedAlertsBySession } from '../../shared/hooks/use_show_related_alerts_by_session';
import { RelatedAlertsByAncestry } from './related_alerts_by_ancestry';
import { SuppressedAlerts } from './suppressed_alerts';
export const CORRELATIONS_TAB_ID = 'correlations-details';
@ -43,34 +44,59 @@ export const CorrelationsDetails: React.FC = () => {
});
const { show: showAlertsBySession, entityId } = useShowRelatedAlertsBySession({ getFieldsData });
const showCases = useShowRelatedCases();
const { show: showSuppressedAlerts, alertSuppressionCount } = useShowSuppressedAlerts({
getFieldsData,
});
const canShowAtLeastOneInsight =
showAlertsByAncestry || showSameSourceAlerts || showAlertsBySession || showCases;
showAlertsByAncestry ||
showSameSourceAlerts ||
showAlertsBySession ||
showCases ||
showSuppressedAlerts;
return (
<>
{canShowAtLeastOneInsight ? (
<>
{showAlertsByAncestry && documentId && indices && (
<RelatedAlertsByAncestry documentId={documentId} indices={indices} scopeId={scopeId} />
<EuiFlexGroup gutterSize="l" direction="column">
{showSuppressedAlerts && (
<EuiFlexItem>
<SuppressedAlerts
alertSuppressionCount={alertSuppressionCount}
dataAsNestedObject={dataAsNestedObject}
/>
</EuiFlexItem>
)}
{showCases && (
<EuiFlexItem>
<RelatedCases eventId={eventId} />
</EuiFlexItem>
)}
<EuiSpacer />
{showSameSourceAlerts && originalEventId && (
<RelatedAlertsBySameSourceEvent originalEventId={originalEventId} scopeId={scopeId} />
<EuiFlexItem>
<RelatedAlertsBySameSourceEvent
originalEventId={originalEventId}
scopeId={scopeId}
eventId={eventId}
/>
</EuiFlexItem>
)}
<EuiSpacer />
{showAlertsBySession && entityId && (
<RelatedAlertsBySession entityId={entityId} scopeId={scopeId} />
<EuiFlexItem>
<RelatedAlertsBySession entityId={entityId} scopeId={scopeId} eventId={eventId} />
</EuiFlexItem>
)}
<EuiSpacer />
{showCases && <RelatedCases eventId={eventId} />}
</>
{showAlertsByAncestry && documentId && indices && (
<EuiFlexItem>
<RelatedAlertsByAncestry
documentId={documentId}
indices={indices}
scopeId={scopeId}
eventId={eventId}
/>
</EuiFlexItem>
)}
</EuiFlexGroup>
) : (
<div data-test-subj={`${CORRELATIONS_DETAILS_TEST_ID}Error`}>
{CORRELATIONS_ERROR_MESSAGE}

View file

@ -7,6 +7,7 @@
import React from 'react';
import { render } from '@testing-library/react';
import { TestProviders } from '../../../common/mock';
import { EuiBasicTable } from '@elastic/eui';
import { CorrelationsDetailsAlertsTable, columns } from './correlations_details_alerts_table';
import { usePaginatedAlerts } from '../hooks/use_paginated_alerts';
@ -17,7 +18,11 @@ jest.mock('@elastic/eui', () => ({
EuiBasicTable: jest.fn(() => <div data-testid="mock-euibasictable" />),
}));
describe('AlertsTable', () => {
const TEST_ID = 'TEST';
const scopeId = 'scopeId';
const eventId = 'eventId';
describe('CorrelationsDetailsAlertsTable', () => {
const alertIds = ['id1', 'id2', 'id3'];
beforeEach(() => {
@ -59,7 +64,19 @@ describe('AlertsTable', () => {
});
it('renders EuiBasicTable with correct props', () => {
render(<CorrelationsDetailsAlertsTable title={'title'} loading={false} alertIds={alertIds} />);
const { getByTestId } = render(
<TestProviders>
<CorrelationsDetailsAlertsTable
title={'title'}
loading={false}
alertIds={alertIds}
scopeId={scopeId}
eventId={eventId}
data-test-subj={TEST_ID}
/>
</TestProviders>
);
expect(getByTestId(`${TEST_ID}InvestigateInTimeline`)).toBeInTheDocument();
expect(jest.mocked(usePaginatedAlerts)).toHaveBeenCalled();

View file

@ -7,16 +7,21 @@
import React, { type FC, useMemo, useCallback } from 'react';
import { type Criteria, EuiBasicTable, formatDate } from '@elastic/eui';
import { Severity } from '@kbn/securitysolution-io-ts-alerting-types';
import type { Filter } from '@kbn/es-query';
import { isRight } from 'fp-ts/lib/Either';
import { ALERT_REASON, ALERT_RULE_NAME } from '@kbn/rule-data-utils';
import type { DataProvider } from '../../../../common/types';
import { SeverityBadge } from '../../../detections/components/rules/severity_badge';
import { usePaginatedAlerts } from '../hooks/use_paginated_alerts';
import * as i18n from './translations';
import { ExpandablePanel } from '../../shared/components/expandable_panel';
import { InvestigateInTimelineButton } from '../../../common/components/event_details/table/investigate_in_timeline_button';
import { ACTION_INVESTIGATE_IN_TIMELINE } from '../../../detections/components/alerts_table/translations';
import { getDataProvider } from '../../../common/components/event_details/table/use_action_cell_data_provider';
export const TIMESTAMP_DATE_FORMAT = 'MMM D, YYYY @ HH:mm:ss.SSS';
const dataProviderLimit = 5;
export const columns = [
{
@ -60,6 +65,14 @@ export interface CorrelationsDetailsAlertsTableProps {
* Ids of alerts to display in the table
*/
alertIds: string[] | undefined;
/**
* Maintain backwards compatibility // TODO remove when possible
*/
scopeId: string;
/**
* Id of the document
*/
eventId: string;
/**
* Data test subject string for testing
*/
@ -73,6 +86,8 @@ export const CorrelationsDetailsAlertsTable: FC<CorrelationsDetailsAlertsTablePr
title,
loading,
alertIds,
scopeId,
eventId,
'data-test-subj': dataTestSubj,
}) => {
const {
@ -110,11 +125,35 @@ export const CorrelationsDetailsAlertsTable: FC<CorrelationsDetailsAlertsTablePr
);
}, [data]);
const shouldUseFilters = Boolean(
alertIds && alertIds.length && alertIds.length >= dataProviderLimit
);
const dataProviders = useMemo(
() => (shouldUseFilters ? null : getDataProviders(scopeId, eventId, alertIds)),
[alertIds, shouldUseFilters, scopeId, eventId]
);
const filters: Filter[] | null = useMemo(
() => (shouldUseFilters ? getFilters(alertIds) : null),
[alertIds, shouldUseFilters]
);
return (
<ExpandablePanel
header={{
title,
iconType: 'warning',
headerContent: (
<div data-test-subj={`${dataTestSubj}InvestigateInTimeline`}>
<InvestigateInTimelineButton
dataProviders={dataProviders}
filters={filters}
asEmptyButton
iconType="timeline"
>
{ACTION_INVESTIGATE_IN_TIMELINE}
</InvestigateInTimelineButton>
</div>
),
}}
content={{ error }}
expand={{
@ -135,3 +174,45 @@ export const CorrelationsDetailsAlertsTable: FC<CorrelationsDetailsAlertsTablePr
</ExpandablePanel>
);
};
const getFilters = (alertIds?: string[]) => {
if (alertIds && alertIds.length) {
return [
{
meta: {
alias: i18n.CORRELATIONS_DETAILS_TABLE_FILTER,
type: 'phrases',
key: '_id',
params: [...alertIds],
negate: false,
disabled: false,
value: alertIds.join(),
},
query: {
bool: {
should: alertIds.map((id) => {
return {
match_phrase: {
_id: id,
},
};
}),
minimum_should_match: 1,
},
},
},
];
}
return null;
};
const getDataProviders = (scopeId: string, eventId: string, alertIds?: string[]) => {
if (alertIds && alertIds.length) {
return alertIds.reduce<DataProvider[]>((result, alertId, index) => {
const id = `${scopeId}-${eventId}-event.id-${index}-${alertId}`;
result.push(getDataProvider('_id', id, alertId));
return result;
}, []);
}
return null;
};

View file

@ -7,6 +7,7 @@
import React from 'react';
import { render } from '@testing-library/react';
import { TestProviders } from '../../../common/mock';
import {
CORRELATIONS_DETAILS_BY_ANCESTRY_SECTION_TABLE_TEST_ID,
CORRELATIONS_DETAILS_BY_ANCESTRY_SECTION_TEST_ID,
@ -26,6 +27,7 @@ jest.mock('../hooks/use_paginated_alerts');
const documentId = 'documentId';
const indices = ['index1'];
const scopeId = 'scopeId';
const eventId = 'eventId';
const TOGGLE_ICON = EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID(
CORRELATIONS_DETAILS_BY_ANCESTRY_SECTION_TEST_ID
@ -73,11 +75,21 @@ describe('<RelatedAlertsByAncestry />', () => {
});
const { getByTestId } = render(
<RelatedAlertsByAncestry documentId={documentId} indices={indices} scopeId={scopeId} />
<TestProviders>
<RelatedAlertsByAncestry
documentId={documentId}
indices={indices}
scopeId={scopeId}
eventId={eventId}
/>
</TestProviders>
);
expect(getByTestId(TOGGLE_ICON)).toBeInTheDocument();
expect(getByTestId(TITLE_ICON)).toBeInTheDocument();
expect(getByTestId(TITLE_TEXT)).toBeInTheDocument();
expect(
getByTestId(`${CORRELATIONS_DETAILS_BY_ANCESTRY_SECTION_TEST_ID}InvestigateInTimeline`)
).toBeInTheDocument();
expect(getByTestId(CORRELATIONS_DETAILS_BY_ANCESTRY_SECTION_TABLE_TEST_ID)).toBeInTheDocument();
});
@ -88,7 +100,14 @@ describe('<RelatedAlertsByAncestry />', () => {
});
const { container } = render(
<RelatedAlertsByAncestry documentId={documentId} indices={indices} scopeId={scopeId} />
<TestProviders>
<RelatedAlertsByAncestry
documentId={documentId}
indices={indices}
scopeId={scopeId}
eventId={eventId}
/>
</TestProviders>
);
expect(container).toBeEmptyDOMElement();
});

View file

@ -24,6 +24,10 @@ export interface RelatedAlertsByAncestryProps {
* Maintain backwards compatibility // TODO remove when possible
*/
scopeId: string;
/**
* Id of the document
*/
eventId: string;
}
/**
@ -33,6 +37,7 @@ export const RelatedAlertsByAncestry: React.VFC<RelatedAlertsByAncestryProps> =
documentId,
indices,
scopeId,
eventId,
}) => {
const { loading, error, data, dataCount } = useFetchRelatedAlertsByAncestry({
documentId,
@ -50,6 +55,8 @@ export const RelatedAlertsByAncestry: React.VFC<RelatedAlertsByAncestryProps> =
title={title}
loading={loading}
alertIds={data}
scopeId={scopeId}
eventId={eventId}
data-test-subj={CORRELATIONS_DETAILS_BY_ANCESTRY_SECTION_TEST_ID}
/>
);

View file

@ -7,6 +7,7 @@
import React from 'react';
import { render } from '@testing-library/react';
import { TestProviders } from '../../../common/mock';
import {
CORRELATIONS_DETAILS_BY_SOURCE_SECTION_TEST_ID,
CORRELATIONS_DETAILS_BY_SOURCE_SECTION_TABLE_TEST_ID,
@ -25,6 +26,7 @@ jest.mock('../hooks/use_paginated_alerts');
const originalEventId = 'originalEventId';
const scopeId = 'scopeId';
const eventId = 'eventId';
const TOGGLE_ICON = EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID(
CORRELATIONS_DETAILS_BY_SOURCE_SECTION_TEST_ID
@ -72,11 +74,20 @@ describe('<RelatedAlertsBySameSourceEvent />', () => {
});
const { getByTestId } = render(
<RelatedAlertsBySameSourceEvent originalEventId={originalEventId} scopeId={scopeId} />
<TestProviders>
<RelatedAlertsBySameSourceEvent
originalEventId={originalEventId}
scopeId={scopeId}
eventId={eventId}
/>
</TestProviders>
);
expect(getByTestId(TOGGLE_ICON)).toBeInTheDocument();
expect(getByTestId(TITLE_ICON)).toBeInTheDocument();
expect(getByTestId(TITLE_TEXT)).toBeInTheDocument();
expect(
getByTestId(`${CORRELATIONS_DETAILS_BY_SOURCE_SECTION_TEST_ID}InvestigateInTimeline`)
).toBeInTheDocument();
expect(getByTestId(CORRELATIONS_DETAILS_BY_SOURCE_SECTION_TABLE_TEST_ID)).toBeInTheDocument();
});
@ -87,7 +98,13 @@ describe('<RelatedAlertsBySameSourceEvent />', () => {
});
const { container } = render(
<RelatedAlertsBySameSourceEvent originalEventId={originalEventId} scopeId={scopeId} />
<TestProviders>
<RelatedAlertsBySameSourceEvent
originalEventId={originalEventId}
scopeId={scopeId}
eventId={eventId}
/>
</TestProviders>
);
expect(container).toBeEmptyDOMElement();
});

View file

@ -20,6 +20,10 @@ export interface RelatedAlertsBySameSourceEventProps {
* Maintain backwards compatibility // TODO remove when possible
*/
scopeId: string;
/**
* Id of the document
*/
eventId: string;
}
/**
@ -28,6 +32,7 @@ export interface RelatedAlertsBySameSourceEventProps {
export const RelatedAlertsBySameSourceEvent: React.VFC<RelatedAlertsBySameSourceEventProps> = ({
originalEventId,
scopeId,
eventId,
}) => {
const { loading, error, data, dataCount } = useFetchRelatedAlertsBySameSourceEvent({
originalEventId,
@ -44,6 +49,8 @@ export const RelatedAlertsBySameSourceEvent: React.VFC<RelatedAlertsBySameSource
title={title}
loading={loading}
alertIds={data}
scopeId={scopeId}
eventId={eventId}
data-test-subj={CORRELATIONS_DETAILS_BY_SOURCE_SECTION_TEST_ID}
/>
);

View file

@ -7,6 +7,7 @@
import React from 'react';
import { render } from '@testing-library/react';
import { TestProviders } from '../../../common/mock';
import {
CORRELATIONS_DETAILS_BY_SESSION_SECTION_TABLE_TEST_ID,
CORRELATIONS_DETAILS_BY_SESSION_SECTION_TEST_ID,
@ -25,6 +26,7 @@ jest.mock('../hooks/use_paginated_alerts');
const entityId = 'entityId';
const scopeId = 'scopeId';
const eventId = 'eventId';
const TOGGLE_ICON = EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID(
CORRELATIONS_DETAILS_BY_SESSION_SECTION_TEST_ID
@ -72,11 +74,16 @@ describe('<RelatedAlertsBySession />', () => {
});
const { getByTestId } = render(
<RelatedAlertsBySession entityId={entityId} scopeId={scopeId} />
<TestProviders>
<RelatedAlertsBySession entityId={entityId} scopeId={scopeId} eventId={eventId} />
</TestProviders>
);
expect(getByTestId(TOGGLE_ICON)).toBeInTheDocument();
expect(getByTestId(TITLE_ICON)).toBeInTheDocument();
expect(getByTestId(TITLE_TEXT)).toBeInTheDocument();
expect(
getByTestId(`${CORRELATIONS_DETAILS_BY_SESSION_SECTION_TEST_ID}InvestigateInTimeline`)
).toBeInTheDocument();
expect(getByTestId(CORRELATIONS_DETAILS_BY_SESSION_SECTION_TABLE_TEST_ID)).toBeInTheDocument();
});
@ -86,7 +93,11 @@ describe('<RelatedAlertsBySession />', () => {
error: true,
});
const { container } = render(<RelatedAlertsBySession entityId={entityId} scopeId={scopeId} />);
const { container } = render(
<TestProviders>
<RelatedAlertsBySession entityId={entityId} scopeId={scopeId} eventId={eventId} />
</TestProviders>
);
expect(container).toBeEmptyDOMElement();
});
});

View file

@ -20,6 +20,10 @@ export interface RelatedAlertsBySessionProps {
* Maintain backwards compatibility // TODO remove when possible
*/
scopeId: string;
/**
* Id of the document
*/
eventId: string;
}
/**
@ -28,6 +32,7 @@ export interface RelatedAlertsBySessionProps {
export const RelatedAlertsBySession: React.VFC<RelatedAlertsBySessionProps> = ({
entityId,
scopeId,
eventId,
}) => {
const { loading, error, data, dataCount } = useFetchRelatedAlertsBySession({
entityId,
@ -44,6 +49,8 @@ export const RelatedAlertsBySession: React.VFC<RelatedAlertsBySessionProps> = ({
title={title}
loading={loading}
alertIds={data}
scopeId={scopeId}
eventId={eventId}
data-test-subj={CORRELATIONS_DETAILS_BY_SESSION_SECTION_TEST_ID}
/>
);

View file

@ -0,0 +1,87 @@
/*
* 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 React from 'react';
import { render } from '@testing-library/react';
import { TestProviders } from '../../../common/mock';
import {
CORRELATIONS_DETAILS_SUPPRESSED_ALERTS_SECTION_TEST_ID,
SUPPRESSED_ALERTS_SECTION_TECHNICAL_PREVIEW_TEST_ID,
} from './test_ids';
import { SuppressedAlerts } from './suppressed_alerts';
import {
EXPANDABLE_PANEL_HEADER_TITLE_ICON_TEST_ID,
EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID,
EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID,
} from '../../shared/components/test_ids';
import { LeftPanelContext } from '../context';
import { mockContextValue } from '../mocks/mock_context';
const mockDataAsNestedObject = {
_id: 'testId',
};
const TOGGLE_ICON = EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID(
CORRELATIONS_DETAILS_SUPPRESSED_ALERTS_SECTION_TEST_ID
);
const TITLE_ICON = EXPANDABLE_PANEL_HEADER_TITLE_ICON_TEST_ID(
CORRELATIONS_DETAILS_SUPPRESSED_ALERTS_SECTION_TEST_ID
);
const TITLE_TEXT = EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID(
CORRELATIONS_DETAILS_SUPPRESSED_ALERTS_SECTION_TEST_ID
);
const INVESTIGATE_IN_TIMELINE_BUTTON_TEST_ID = `${CORRELATIONS_DETAILS_SUPPRESSED_ALERTS_SECTION_TEST_ID}InvestigateInTimeline`;
describe('<SuppressedAlerts />', () => {
it('should render zero component correctly', () => {
const { getByTestId, queryByTestId } = render(
<TestProviders>
<LeftPanelContext.Provider value={mockContextValue}>
<SuppressedAlerts alertSuppressionCount={0} dataAsNestedObject={mockDataAsNestedObject} />
</LeftPanelContext.Provider>
</TestProviders>
);
expect(getByTestId(TITLE_ICON)).toBeInTheDocument();
expect(getByTestId(TITLE_TEXT)).toHaveTextContent('0 suppressed alert');
expect(queryByTestId(INVESTIGATE_IN_TIMELINE_BUTTON_TEST_ID)).not.toBeInTheDocument();
expect(queryByTestId(TOGGLE_ICON)).not.toBeInTheDocument();
expect(getByTestId(SUPPRESSED_ALERTS_SECTION_TECHNICAL_PREVIEW_TEST_ID)).toBeInTheDocument();
});
it('should render single component correctly', () => {
const { getByTestId, queryByTestId } = render(
<TestProviders>
<LeftPanelContext.Provider value={mockContextValue}>
<SuppressedAlerts alertSuppressionCount={1} dataAsNestedObject={mockDataAsNestedObject} />
</LeftPanelContext.Provider>
</TestProviders>
);
expect(getByTestId(TITLE_ICON)).toBeInTheDocument();
expect(getByTestId(TITLE_TEXT)).toHaveTextContent('1 suppressed alert');
expect(getByTestId(INVESTIGATE_IN_TIMELINE_BUTTON_TEST_ID)).toBeInTheDocument();
expect(queryByTestId(TOGGLE_ICON)).not.toBeInTheDocument();
expect(getByTestId(SUPPRESSED_ALERTS_SECTION_TECHNICAL_PREVIEW_TEST_ID)).toBeInTheDocument();
});
it('should render multiple component correctly', () => {
const { getByTestId, queryByTestId } = render(
<TestProviders>
<LeftPanelContext.Provider value={mockContextValue}>
<SuppressedAlerts alertSuppressionCount={2} dataAsNestedObject={mockDataAsNestedObject} />
</LeftPanelContext.Provider>
</TestProviders>
);
expect(getByTestId(TITLE_ICON)).toBeInTheDocument();
expect(getByTestId(TITLE_TEXT)).toHaveTextContent('2 suppressed alerts');
expect(getByTestId(INVESTIGATE_IN_TIMELINE_BUTTON_TEST_ID)).toBeInTheDocument();
expect(queryByTestId(TOGGLE_ICON)).not.toBeInTheDocument();
expect(getByTestId(SUPPRESSED_ALERTS_SECTION_TECHNICAL_PREVIEW_TEST_ID)).toBeInTheDocument();
});
});

View file

@ -0,0 +1,74 @@
/*
* 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 React from 'react';
import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs';
import { EuiBetaBadge, EuiFlexItem, EuiFlexGroup } from '@elastic/eui';
import { CORRELATIONS_SUPPRESSED_ALERTS } from '../../shared/translations';
import { ExpandablePanel } from '../../shared/components/expandable_panel';
import {
CORRELATIONS_DETAILS_SUPPRESSED_ALERTS_SECTION_TEST_ID,
SUPPRESSED_ALERTS_SECTION_TECHNICAL_PREVIEW_TEST_ID,
} from './test_ids';
import { SUPPRESSED_ALERTS_COUNT_TECHNICAL_PREVIEW } from '../../../common/components/event_details/insights/translations';
import { InvestigateInTimelineAction } from '../../../detections/components/alerts_table/timeline_actions/investigate_in_timeline_action';
export interface SuppressedAlertsProps {
/**
* An object with top level fields from the ECS object
*/
dataAsNestedObject: Ecs | null;
/**
* Value of the kibana.alert.suppression.doc_count field
*/
alertSuppressionCount: number;
}
/**
* Displays number of suppressed alerts and investigate in timeline icon
*/
export const SuppressedAlerts: React.VFC<SuppressedAlertsProps> = ({
dataAsNestedObject,
alertSuppressionCount,
}) => {
const title = (
<EuiFlexGroup alignItems="center" gutterSize="s">
<EuiFlexItem>
{`${alertSuppressionCount} ${CORRELATIONS_SUPPRESSED_ALERTS(alertSuppressionCount)}`}
</EuiFlexItem>
<EuiFlexItem>
<EuiBetaBadge
label={SUPPRESSED_ALERTS_COUNT_TECHNICAL_PREVIEW}
style={{ verticalAlign: 'middle' }}
size="s"
data-test-subj={SUPPRESSED_ALERTS_SECTION_TECHNICAL_PREVIEW_TEST_ID}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
const headerContent = alertSuppressionCount > 0 && (
<div
data-test-subj={`${CORRELATIONS_DETAILS_SUPPRESSED_ALERTS_SECTION_TEST_ID}InvestigateInTimeline`}
>
<InvestigateInTimelineAction ecsRowData={dataAsNestedObject} buttonType={'emptyButton'} />
</div>
);
return (
<ExpandablePanel
header={{
title,
iconType: 'layers',
headerContent,
}}
data-test-subj={CORRELATIONS_DETAILS_SUPPRESSED_ALERTS_SECTION_TEST_ID}
/>
);
};
SuppressedAlerts.displayName = 'SuppressedAlerts';

View file

@ -73,6 +73,10 @@ export const CORRELATIONS_DETAILS_CASES_SECTION_TEST_ID =
`${CORRELATIONS_DETAILS_TEST_ID}CasesSection` as const;
export const CORRELATIONS_DETAILS_CASES_SECTION_TABLE_TEST_ID =
`${CORRELATIONS_DETAILS_CASES_SECTION_TEST_ID}Table` as const;
export const CORRELATIONS_DETAILS_SUPPRESSED_ALERTS_SECTION_TEST_ID =
`${CORRELATIONS_DETAILS_TEST_ID}SuppressedAlertsSection` as const;
export const SUPPRESSED_ALERTS_SECTION_TECHNICAL_PREVIEW_TEST_ID =
`${CORRELATIONS_DETAILS_TEST_ID}SuppressedAlertsSectionTechnicalPreview` as const;
export const RESPONSE_BASE_TEST_ID = `${PREFIX}Responses` as const;
export const RESPONSE_DETAILS_TEST_ID = `${RESPONSE_BASE_TEST_ID}Details` as const;
export const RESPONSE_EMPTY_TEST_ID = `${RESPONSE_BASE_TEST_ID}Empty` as const;

View file

@ -197,3 +197,10 @@ export const CORRELATIONS_CASE_NAME_COLUMN_TITLE = i18n.translate(
defaultMessage: 'Name',
}
);
export const CORRELATIONS_DETAILS_TABLE_FILTER = i18n.translate(
'xpack.securitySolution.flyout.correlations.correlationsDetailsTableFilter',
{
defaultMessage: 'Correlations Details Table Alert IDs',
}
);

View file

@ -18,6 +18,7 @@ import {
INSIGHTS_CORRELATIONS_RELATED_ALERTS_BY_SAME_SOURCE_EVENT_TEST_ID,
INSIGHTS_CORRELATIONS_RELATED_ALERTS_BY_SESSION_TEST_ID,
INSIGHTS_CORRELATIONS_RELATED_CASES_TEST_ID,
INSIGHTS_CORRELATIONS_SUPPRESSED_ALERTS_TEST_ID,
INSIGHTS_CORRELATIONS_TEST_ID,
SUMMARY_ROW_VALUE_TEST_ID,
} from './test_ids';
@ -25,6 +26,7 @@ import { useShowRelatedAlertsByAncestry } from '../../shared/hooks/use_show_rela
import { useShowRelatedAlertsBySameSourceEvent } from '../../shared/hooks/use_show_related_alerts_by_same_source_event';
import { useShowRelatedAlertsBySession } from '../../shared/hooks/use_show_related_alerts_by_session';
import { useShowRelatedCases } from '../../shared/hooks/use_show_related_cases';
import { useShowSuppressedAlerts } from '../../shared/hooks/use_show_suppressed_alerts';
import { useFetchRelatedAlertsByAncestry } from '../../shared/hooks/use_fetch_related_alerts_by_ancestry';
import { useFetchRelatedAlertsBySameSourceEvent } from '../../shared/hooks/use_fetch_related_alerts_by_same_source_event';
import { useFetchRelatedAlertsBySession } from '../../shared/hooks/use_fetch_related_alerts_by_session';
@ -40,6 +42,7 @@ jest.mock('../../shared/hooks/use_show_related_alerts_by_ancestry');
jest.mock('../../shared/hooks/use_show_related_alerts_by_same_source_event');
jest.mock('../../shared/hooks/use_show_related_alerts_by_session');
jest.mock('../../shared/hooks/use_show_related_cases');
jest.mock('../../shared/hooks/use_show_suppressed_alerts');
jest.mock('../../shared/hooks/use_fetch_related_alerts_by_session');
jest.mock('../../shared/hooks/use_fetch_related_alerts_by_ancestry');
jest.mock('../../shared/hooks/use_fetch_related_alerts_by_same_source_event');
@ -56,6 +59,9 @@ const TITLE_TEXT_TEST_ID = EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID(
INSIGHTS_CORRELATIONS_TEST_ID
);
const SUPPRESSED_ALERTS_TEST_ID = SUMMARY_ROW_VALUE_TEST_ID(
INSIGHTS_CORRELATIONS_SUPPRESSED_ALERTS_TEST_ID
);
const RELATED_ALERTS_BY_ANCESTRY_TEST_ID = SUMMARY_ROW_VALUE_TEST_ID(
INSIGHTS_CORRELATIONS_RELATED_ALERTS_BY_ANCESTRY_TEST_ID
);
@ -92,6 +98,7 @@ describe('<CorrelationsOverview />', () => {
jest.mocked(useShowRelatedAlertsBySameSourceEvent).mockReturnValue({ show: false });
jest.mocked(useShowRelatedAlertsBySession).mockReturnValue({ show: false });
jest.mocked(useShowRelatedCases).mockReturnValue(false);
jest.mocked(useShowSuppressedAlerts).mockReturnValue({ show: false, alertSuppressionCount: 0 });
const { getByTestId, queryByTestId } = render(renderCorrelationsOverview(panelContextValue));
expect(queryByTestId(TOGGLE_ICON_TEST_ID)).not.toBeInTheDocument();
@ -111,6 +118,7 @@ describe('<CorrelationsOverview />', () => {
.mocked(useShowRelatedAlertsBySession)
.mockReturnValue({ show: true, entityId: 'entityId' });
jest.mocked(useShowRelatedCases).mockReturnValue(true);
jest.mocked(useShowSuppressedAlerts).mockReturnValue({ show: true, alertSuppressionCount: 1 });
(useFetchRelatedAlertsByAncestry as jest.Mock).mockReturnValue({
loading: false,
@ -138,6 +146,7 @@ describe('<CorrelationsOverview />', () => {
expect(getByTestId(RELATED_ALERTS_BY_SAME_SOURCE_EVENT_TEST_ID)).toBeInTheDocument();
expect(getByTestId(RELATED_ALERTS_BY_SESSION_TEST_ID)).toBeInTheDocument();
expect(getByTestId(RELATED_CASES_TEST_ID)).toBeInTheDocument();
expect(getByTestId(SUPPRESSED_ALERTS_TEST_ID)).toBeInTheDocument();
});
it('should hide rows and show error message if show values are false', () => {
@ -151,12 +160,14 @@ describe('<CorrelationsOverview />', () => {
.mocked(useShowRelatedAlertsBySession)
.mockReturnValue({ show: false, entityId: 'entityId' });
jest.mocked(useShowRelatedCases).mockReturnValue(false);
jest.mocked(useShowSuppressedAlerts).mockReturnValue({ show: false, alertSuppressionCount: 0 });
const { getByTestId, queryByTestId } = render(renderCorrelationsOverview(panelContextValue));
expect(queryByTestId(RELATED_ALERTS_BY_ANCESTRY_TEST_ID)).not.toBeInTheDocument();
expect(queryByTestId(RELATED_ALERTS_BY_SAME_SOURCE_EVENT_TEST_ID)).not.toBeInTheDocument();
expect(queryByTestId(RELATED_ALERTS_BY_SESSION_TEST_ID)).not.toBeInTheDocument();
expect(queryByTestId(RELATED_CASES_TEST_ID)).not.toBeInTheDocument();
expect(queryByTestId(SUPPRESSED_ALERTS_TEST_ID)).not.toBeInTheDocument();
expect(getByTestId(CORRELATIONS_ERROR_TEST_ID)).toBeInTheDocument();
});
@ -165,12 +176,14 @@ describe('<CorrelationsOverview />', () => {
jest.mocked(useShowRelatedAlertsBySameSourceEvent).mockReturnValue({ show: true });
jest.mocked(useShowRelatedAlertsBySession).mockReturnValue({ show: true });
jest.mocked(useShowRelatedCases).mockReturnValue(false);
jest.mocked(useShowSuppressedAlerts).mockReturnValue({ show: false, alertSuppressionCount: 0 });
const { queryByTestId } = render(renderCorrelationsOverview(panelContextValue));
expect(queryByTestId(RELATED_ALERTS_BY_ANCESTRY_TEST_ID)).not.toBeInTheDocument();
expect(queryByTestId(RELATED_ALERTS_BY_SAME_SOURCE_EVENT_TEST_ID)).not.toBeInTheDocument();
expect(queryByTestId(RELATED_ALERTS_BY_SESSION_TEST_ID)).not.toBeInTheDocument();
expect(queryByTestId(RELATED_CASES_TEST_ID)).not.toBeInTheDocument();
expect(queryByTestId(SUPPRESSED_ALERTS_TEST_ID)).not.toBeInTheDocument();
});
it('should navigate to the left section Insights tab when clicking on button', () => {

View file

@ -15,6 +15,8 @@ import { useShowRelatedAlertsBySameSourceEvent } from '../../shared/hooks/use_sh
import { RelatedAlertsBySameSourceEvent } from './related_alerts_by_same_source_event';
import { RelatedAlertsByAncestry } from './related_alerts_by_ancestry';
import { useShowRelatedAlertsByAncestry } from '../../shared/hooks/use_show_related_alerts_by_ancestry';
import { SuppressedAlerts } from './suppressed_alerts';
import { useShowSuppressedAlerts } from '../../shared/hooks/use_show_suppressed_alerts';
import { RelatedCases } from './related_cases';
import { useShowRelatedCases } from '../../shared/hooks/use_show_related_cases';
import { INSIGHTS_CORRELATIONS_TEST_ID } from './test_ids';
@ -68,9 +70,16 @@ export const CorrelationsOverview: React.FC = () => {
});
const { show: showAlertsBySession, entityId } = useShowRelatedAlertsBySession({ getFieldsData });
const showCases = useShowRelatedCases();
const { show: showSuppressedAlerts, alertSuppressionCount } = useShowSuppressedAlerts({
getFieldsData,
});
const canShowAtLeastOneInsight =
showAlertsByAncestry || showSameSourceAlerts || showAlertsBySession || showCases;
showAlertsByAncestry ||
showSameSourceAlerts ||
showAlertsBySession ||
showCases ||
showSuppressedAlerts;
return (
<ExpandablePanel
@ -83,16 +92,19 @@ export const CorrelationsOverview: React.FC = () => {
>
{canShowAtLeastOneInsight ? (
<EuiFlexGroup direction="column" gutterSize="none">
{showAlertsByAncestry && documentId && indices && (
<RelatedAlertsByAncestry documentId={documentId} indices={indices} scopeId={scopeId} />
{showSuppressedAlerts && (
<SuppressedAlerts alertSuppressionCount={alertSuppressionCount} />
)}
{showCases && <RelatedCases eventId={eventId} />}
{showSameSourceAlerts && originalEventId && (
<RelatedAlertsBySameSourceEvent originalEventId={originalEventId} scopeId={scopeId} />
)}
{showAlertsBySession && entityId && (
<RelatedAlertsBySession entityId={entityId} scopeId={scopeId} />
)}
{showCases && <RelatedCases eventId={eventId} />}
{showAlertsByAncestry && documentId && indices && (
<RelatedAlertsByAncestry documentId={documentId} indices={indices} scopeId={scopeId} />
)}
</EuiFlexGroup>
) : (
<div data-test-subj={`${INSIGHTS_CORRELATIONS_TEST_ID}Error`}>{CORRELATIONS_ERROR}</div>

View file

@ -0,0 +1,54 @@
/*
* 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 React from 'react';
import { render } from '@testing-library/react';
import {
SUMMARY_ROW_ICON_TEST_ID,
SUMMARY_ROW_VALUE_TEST_ID,
INSIGHTS_CORRELATIONS_SUPPRESSED_ALERTS_TEST_ID,
SUPPRESSED_ALERTS_TECHNICAL_PREVIEW_TEST_ID,
} from './test_ids';
import { SuppressedAlerts } from './suppressed_alerts';
const ICON_TEST_ID = SUMMARY_ROW_ICON_TEST_ID(INSIGHTS_CORRELATIONS_SUPPRESSED_ALERTS_TEST_ID);
const VALUE_TEST_ID = SUMMARY_ROW_VALUE_TEST_ID(INSIGHTS_CORRELATIONS_SUPPRESSED_ALERTS_TEST_ID);
describe('<SuppressedAlerts />', () => {
it('should render zero suppressed alert correctly', () => {
const { getByTestId } = render(<SuppressedAlerts alertSuppressionCount={0} />);
expect(getByTestId(ICON_TEST_ID)).toBeInTheDocument();
const value = getByTestId(VALUE_TEST_ID);
expect(value).toBeInTheDocument();
expect(value).toHaveTextContent('0 suppressed alert');
expect(getByTestId(VALUE_TEST_ID)).toBeInTheDocument();
expect(getByTestId(SUPPRESSED_ALERTS_TECHNICAL_PREVIEW_TEST_ID)).toBeInTheDocument();
});
it('should render single suppressed alert correctly', () => {
const { getByTestId } = render(<SuppressedAlerts alertSuppressionCount={1} />);
expect(getByTestId(ICON_TEST_ID)).toBeInTheDocument();
const value = getByTestId(VALUE_TEST_ID);
expect(value).toBeInTheDocument();
expect(value).toHaveTextContent('1 suppressed alert');
expect(getByTestId(VALUE_TEST_ID)).toBeInTheDocument();
expect(getByTestId(SUPPRESSED_ALERTS_TECHNICAL_PREVIEW_TEST_ID)).toBeInTheDocument();
});
it('should render multiple suppressed alerts row correctly', () => {
const { getByTestId } = render(<SuppressedAlerts alertSuppressionCount={2} />);
expect(getByTestId(ICON_TEST_ID)).toBeInTheDocument();
const value = getByTestId(VALUE_TEST_ID);
expect(value).toBeInTheDocument();
expect(value).toHaveTextContent('2 suppressed alerts');
expect(getByTestId(VALUE_TEST_ID)).toBeInTheDocument();
expect(getByTestId(SUPPRESSED_ALERTS_TECHNICAL_PREVIEW_TEST_ID)).toBeInTheDocument();
});
});

View file

@ -0,0 +1,57 @@
/*
* 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 React from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiBetaBadge } from '@elastic/eui';
import {
INSIGHTS_CORRELATIONS_SUPPRESSED_ALERTS_TEST_ID,
SUPPRESSED_ALERTS_TECHNICAL_PREVIEW_TEST_ID,
} from './test_ids';
import { CORRELATIONS_SUPPRESSED_ALERTS } from '../../shared/translations';
import { InsightsSummaryRow } from './insights_summary_row';
import { SUPPRESSED_ALERTS_COUNT_TECHNICAL_PREVIEW } from '../../../common/components/event_details/insights/translations';
import { TECHNICAL_PREVIEW_MESSAGE } from './translations';
export interface SuppressedAlertsProps {
/**
* Value of the kibana.alert.suppression.doc_count field
*/
alertSuppressionCount: number;
}
/**
* Show related alerts by ancestry in summary row
*/
export const SuppressedAlerts: React.VFC<SuppressedAlertsProps> = ({ alertSuppressionCount }) => {
return (
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
<InsightsSummaryRow
loading={false}
error={false}
icon={'layers'}
value={alertSuppressionCount}
text={CORRELATIONS_SUPPRESSED_ALERTS(alertSuppressionCount)}
data-test-subj={INSIGHTS_CORRELATIONS_SUPPRESSED_ALERTS_TEST_ID}
key={`correlation-row-suppressed-alerts`}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiBetaBadge
label={SUPPRESSED_ALERTS_COUNT_TECHNICAL_PREVIEW}
size="s"
iconType="beaker"
tooltipContent={TECHNICAL_PREVIEW_MESSAGE}
tooltipPosition="bottom"
data-test-subj={SUPPRESSED_ALERTS_TECHNICAL_PREVIEW_TEST_ID}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
};
SuppressedAlerts.displayName = 'SuppressedAlerts';

View file

@ -106,6 +106,10 @@ export const INSIGHTS_THREAT_INTELLIGENCE_CONTAINER_TEST_ID = `${INSIGHTS_THREAT
export const INSIGHTS_CORRELATIONS_TEST_ID =
'securitySolutionDocumentDetailsFlyoutInsightsCorrelations';
export const INSIGHTS_CORRELATIONS_SUPPRESSED_ALERTS_TEST_ID =
'securitySolutionDocumentDetailsFlyoutInsightsCorrelationsSupressedAlerts';
export const SUPPRESSED_ALERTS_TECHNICAL_PREVIEW_TEST_ID =
'securitySolutionDocumentDetailsFlyoutSupressedAlertsTechnicalPreview';
export const INSIGHTS_CORRELATIONS_RELATED_CASES_TEST_ID =
'securitySolutionDocumentDetailsFlyoutInsightsCorrelationsRelatedCases';
export const INSIGHTS_CORRELATIONS_RELATED_ALERTS_BY_SESSION_TEST_ID =

View file

@ -299,3 +299,11 @@ export const RESPONSE_TITLE = i18n.translate(
export const RESPONSE_EMPTY = i18n.translate('xpack.securitySolution.flyout.response.empty', {
defaultMessage: 'There are no response actions defined for this event.',
});
export const TECHNICAL_PREVIEW_MESSAGE = i18n.translate(
'xpack.securitySolution.flyout.documentDetails.technicalPreviewMessage',
{
defaultMessage:
'This functionality is in technical preview and may be changed or removed completely in a future release. Elastic will take a best effort approach to fix any issues, but features in technical preview are not subject to the support SLA of official GA features.',
}
);

View file

@ -5,7 +5,12 @@
* 2.0.
*/
import { ALERT_REASON, ALERT_RISK_SCORE, ALERT_SEVERITY } from '@kbn/rule-data-utils';
import {
ALERT_REASON,
ALERT_RISK_SCORE,
ALERT_SEVERITY,
ALERT_SUPPRESSION_DOCS_COUNT,
} from '@kbn/rule-data-utils';
/**
* Returns mocked data for field (mock this method: x-pack/plugins/security_solution/public/common/hooks/use_get_fields_data.ts)
@ -24,6 +29,8 @@ export const mockGetFieldsData = (field: string): string[] => {
return ['user1'];
case ALERT_REASON:
return ['reason'];
case ALERT_SUPPRESSION_DOCS_COUNT:
return ['1'];
default:
return [];
}

View file

@ -19,6 +19,7 @@ import {
EuiLoadingSpinner,
useEuiTheme,
} from '@elastic/eui';
import type { IconType } from '@elastic/eui';
import { css } from '@emotion/react';
export interface ExpandablePanelPanelProps {
@ -26,7 +27,7 @@ export interface ExpandablePanelPanelProps {
/**
* String value of the title to be displayed in the header of panel
*/
title: string;
title: string | React.ReactNode;
/**
* Callback function to be called when the title is clicked
*/
@ -34,9 +35,9 @@ export interface ExpandablePanelPanelProps {
/**
* Icon string for displaying the specified icon in the header
*/
iconType: string;
iconType: IconType;
/**
* Optional content and actions to be displayed on the right side of header
* Optional content and actions to be displayed next to header or on the right side of header
*/
headerContent?: React.ReactNode;
};
@ -106,7 +107,7 @@ export const ExpandablePanel: React.FC<ExpandablePanelPanelProps> = ({
const headerLeftSection = useMemo(
() => (
<EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup
alignItems="center"
gutterSize="s"

View file

@ -0,0 +1,32 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { RenderHookResult } from '@testing-library/react-hooks';
import { renderHook } from '@testing-library/react-hooks';
import type {
ShowSuppressedAlertsParams,
ShowSuppressedAlertsResult,
} from './use_show_suppressed_alerts';
import { useShowSuppressedAlerts } from './use_show_suppressed_alerts';
describe('useShowSuppressedAlerts', () => {
let hookResult: RenderHookResult<ShowSuppressedAlertsParams, ShowSuppressedAlertsResult>;
it('should return false if getFieldsData returns null', () => {
const getFieldsData = () => null;
hookResult = renderHook(() => useShowSuppressedAlerts({ getFieldsData }));
expect(hookResult.result.current).toEqual({ show: false, alertSuppressionCount: 0 });
});
it('should return true if getFieldsData has the correct field', () => {
const getFieldsData = () => '2';
hookResult = renderHook(() => useShowSuppressedAlerts({ getFieldsData }));
expect(hookResult.result.current).toEqual({ show: true, alertSuppressionCount: 2 });
});
});

View file

@ -0,0 +1,42 @@
/*
* 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 { ALERT_SUPPRESSION_DOCS_COUNT } from '@kbn/rule-data-utils';
import type { GetFieldsData } from '../../../common/hooks/use_get_fields_data';
export interface ShowSuppressedAlertsParams {
/**
* Retrieves searchHit values for the provided field
*/
getFieldsData: GetFieldsData;
}
export interface ShowSuppressedAlertsResult {
/**
* Returns true if the document has kibana.alert.original_event.id field with values
*/
show: boolean;
/**
* Number of suppressed alerts
*/
alertSuppressionCount: number;
}
/**
* Returns true if document has kibana.alert.suppression.docs_count field with values
*/
export const useShowSuppressedAlerts = ({
getFieldsData,
}: ShowSuppressedAlertsParams): ShowSuppressedAlertsResult => {
const alertSuppressionField = getFieldsData(ALERT_SUPPRESSION_DOCS_COUNT);
const alertSuppressionCount = alertSuppressionField ? parseInt(alertSuppressionField[0], 10) : 0;
return {
show: Boolean(alertSuppressionField),
alertSuppressionCount,
};
};

View file

@ -19,6 +19,12 @@ export const ERROR_MESSAGE = (message: string) =>
defaultMessage: 'There was an error displaying {message}',
});
export const CORRELATIONS_SUPPRESSED_ALERTS = (count: number) =>
i18n.translate('xpack.securitySolution.flyout.documentDetails.correlations.suppressedAlerts', {
defaultMessage: 'suppressed {count, plural, =1 {alert} other {alerts}}',
values: { count },
});
export const CORRELATIONS_ANCESTRY_ALERTS = (count: number) =>
i18n.translate('xpack.securitySolution.flyout.documentDetails.correlations.ancestryAlerts', {
defaultMessage: '{count, plural, one {alert} other {alerts}} related by ancestry',

View file

@ -7,10 +7,12 @@
import { createRule } from '../../../../tasks/api_calls/rules';
import { getNewRule } from '../../../../objects/rule';
import {
CORRELATIONS_ANCESTRY_SECTION_INVESTIGATE_IN_TIMELINE_BUTTON,
CORRELATIONS_ANCESTRY_SECTION_TABLE,
CORRELATIONS_ANCESTRY_SECTION_TITLE,
CORRELATIONS_CASES_SECTION_TABLE,
CORRELATIONS_CASES_SECTION_TITLE,
CORRELATIONS_SESSION_SECTION_INVESTIGATE_IN_TIMELINE_BUTTON,
CORRELATIONS_SESSION_SECTION_TABLE,
CORRELATIONS_SESSION_SECTION_TITLE,
DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_CORRELATIONS_BUTTON,
@ -73,6 +75,7 @@ describe(
.should('be.visible')
.and('contain.text', '1 alert related by ancestry');
cy.get(CORRELATIONS_ANCESTRY_SECTION_TABLE).should('be.visible');
cy.get(CORRELATIONS_ANCESTRY_SECTION_INVESTIGATE_IN_TIMELINE_BUTTON).should('be.visible');
// TODO get proper data to test this section
// cy.get(CORRELATIONS_SOURCE_SECTION).scrollIntoView();
@ -80,18 +83,27 @@ describe(
// .should('be.visible')
// .and('contain.text', '0 alerts related by source event');
// cy.get(CORRELATIONS_SOURCE_SECTION_TABLE).should('be.visible');
// cy.get(CORRELATIONS_SESSION_SECTION_INVESTIGATE_IN_TIMELINE_BUTTON).should('be.visible');
cy.get(CORRELATIONS_SESSION_SECTION_TITLE).scrollIntoView();
cy.get(CORRELATIONS_SESSION_SECTION_TITLE)
.should('be.visible')
.and('contain.text', '1 alert related by session');
cy.get(CORRELATIONS_SESSION_SECTION_TABLE).should('be.visible');
cy.get(CORRELATIONS_SESSION_SECTION_INVESTIGATE_IN_TIMELINE_BUTTON).should('be.visible');
cy.get(CORRELATIONS_CASES_SECTION_TITLE).scrollIntoView();
cy.get(CORRELATIONS_CASES_SECTION_TITLE)
.should('be.visible')
.and('contain.text', '1 related case');
cy.get(CORRELATIONS_CASES_SECTION_TABLE).should('be.visible');
// TODO get proper data to test suppressed alerts
// cy.get(CORRELATIONS_SUPPRESSED_ALERTS_TITLE).scrollIntoView();
// cy.get(CORRELATIONS_SUPPRESSED_ALERTS_TITLE)
// .should('be.visible')
// .and('contain.text', '1 suppressed alert');
// cy.get(CORRELATIONS_SUPPRESSED_ALERTS_INVESTIGATE_IN_TIMELINE_BUTTON).should('be.visible');
});
}
);

View file

@ -293,7 +293,9 @@ describe(
cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_CORRELATIONS_CONTENT)
.should('be.visible')
.within(() => {
// TODO the order in which these appear is not deterministic currently, hence this can cause flakiness
// cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_CORRELATIONS_VALUES_SUPPRESSED_ALERTS)
// .should('be.visible')
// .and('have.text', '1 suppressed alert'); // TODO populate rule with alert suppression
cy.get(
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_CORRELATIONS_VALUES_RELATED_ALERTS_BY_ANCESTRY
)

View file

@ -11,6 +11,7 @@ import {
CORRELATIONS_DETAILS_BY_SESSION_SECTION_TEST_ID,
CORRELATIONS_DETAILS_BY_SOURCE_SECTION_TEST_ID,
CORRELATIONS_DETAILS_CASES_SECTION_TEST_ID,
CORRELATIONS_DETAILS_SUPPRESSED_ALERTS_SECTION_TEST_ID,
} from '@kbn/security-solution-plugin/public/flyout/left/components/test_ids';
import { EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID } from '@kbn/security-solution-plugin/public/flyout/shared/components/test_ids';
import { getDataTestSubjectSelector } from '../../helpers/common';
@ -27,6 +28,11 @@ export const CORRELATIONS_ANCESTRY_SECTION_TABLE = getDataTestSubjectSelector(
`${CORRELATIONS_DETAILS_BY_ANCESTRY_SECTION_TEST_ID}Table`
);
export const CORRELATIONS_ANCESTRY_SECTION_INVESTIGATE_IN_TIMELINE_BUTTON =
getDataTestSubjectSelector(
`${CORRELATIONS_DETAILS_BY_ANCESTRY_SECTION_TEST_ID}InvestigateInTimeline`
);
export const CORRELATIONS_SOURCE_SECTION_TITLE = getDataTestSubjectSelector(
EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID(CORRELATIONS_DETAILS_BY_SOURCE_SECTION_TEST_ID)
);
@ -35,6 +41,11 @@ export const CORRELATIONS_SOURCE_SECTION_TABLE = getDataTestSubjectSelector(
`${CORRELATIONS_DETAILS_BY_SOURCE_SECTION_TEST_ID}Table`
);
export const CORRELATIONS_SOURCE_SECTION_INVESTIGATE_IN_TIMELINE_BUTTON =
getDataTestSubjectSelector(
`${CORRELATIONS_DETAILS_BY_SOURCE_SECTION_TEST_ID}InvestigateInTimeline`
);
export const CORRELATIONS_SESSION_SECTION_TITLE = getDataTestSubjectSelector(
EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID(CORRELATIONS_DETAILS_BY_SESSION_SECTION_TEST_ID)
);
@ -43,6 +54,11 @@ export const CORRELATIONS_SESSION_SECTION_TABLE = getDataTestSubjectSelector(
`${CORRELATIONS_DETAILS_BY_SESSION_SECTION_TEST_ID}Table`
);
export const CORRELATIONS_SESSION_SECTION_INVESTIGATE_IN_TIMELINE_BUTTON =
getDataTestSubjectSelector(
`${CORRELATIONS_DETAILS_BY_SESSION_SECTION_TEST_ID}InvestigateInTimeline`
);
export const CORRELATIONS_CASES_SECTION_TITLE = getDataTestSubjectSelector(
EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID(CORRELATIONS_DETAILS_CASES_SECTION_TEST_ID)
);
@ -50,3 +66,12 @@ export const CORRELATIONS_CASES_SECTION_TITLE = getDataTestSubjectSelector(
export const CORRELATIONS_CASES_SECTION_TABLE = getDataTestSubjectSelector(
`${CORRELATIONS_DETAILS_CASES_SECTION_TEST_ID}Table`
);
export const CORRELATIONS_SUPPRESSED_ALERTS_TITLE = getDataTestSubjectSelector(
EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID(CORRELATIONS_DETAILS_SUPPRESSED_ALERTS_SECTION_TEST_ID)
);
export const CORRELATIONS_SUPPRESSED_ALERTS_INVESTIGATE_IN_TIMELINE_BUTTON =
getDataTestSubjectSelector(
`${CORRELATIONS_DETAILS_SUPPRESSED_ALERTS_SECTION_TEST_ID}InvestigateInTimeline`
);

View file

@ -37,6 +37,7 @@ import {
INSIGHTS_CORRELATIONS_RELATED_ALERTS_BY_SAME_SOURCE_EVENT_TEST_ID,
INSIGHTS_CORRELATIONS_RELATED_ALERTS_BY_SESSION_TEST_ID,
INSIGHTS_CORRELATIONS_RELATED_CASES_TEST_ID,
INSIGHTS_CORRELATIONS_SUPPRESSED_ALERTS_TEST_ID,
INSIGHTS_ENTITIES_TEST_ID,
REASON_DETAILS_PREVIEW_BUTTON_TEST_ID,
ANALYZER_PREVIEW_TEST_ID,
@ -122,6 +123,10 @@ export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_CORRELATIONS_HEADER =
);
export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_CORRELATIONS_CONTENT =
getDataTestSubjectSelector(EXPANDABLE_PANEL_CONTENT_TEST_ID(INSIGHTS_CORRELATIONS_TEST_ID));
export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_CORRELATIONS_VALUES_SUPPRESSED_ALERTS =
getDataTestSubjectSelector(
SUMMARY_ROW_VALUE_TEST_ID(INSIGHTS_CORRELATIONS_SUPPRESSED_ALERTS_TEST_ID)
);
export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_CORRELATIONS_VALUES_RELATED_ALERTS_BY_ANCESTRY =
getDataTestSubjectSelector(
SUMMARY_ROW_VALUE_TEST_ID(INSIGHTS_CORRELATIONS_RELATED_ALERTS_BY_ANCESTRY_TEST_ID)