mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[8.10] [Security Solution] expandable flyout - prevalence details UI improvements (#164016) (#164915)
# Backport This will backport the following commits from `main` to `8.10`: - [[Security Solution] expandable flyout - prevalence details UI improvements (#164016)](https://github.com/elastic/kibana/pull/164016) <!--- Backport version: 8.9.7 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Philippe Oberti","email":"philippe.oberti@elastic.co"},"sourceCommit":{"committedDate":"2023-08-26T10:20:58Z","message":"[Security Solution] expandable flyout - prevalence details UI improvements (#164016)","sha":"225fc95488f8c5f52b26a8715ce76b7d19f7301f","branchLabelMapping":{"^v8.11.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:enhancement","Team:Threat Hunting:Investigations","v8.10.0","v8.11.0"],"number":164016,"url":"https://github.com/elastic/kibana/pull/164016","mergeCommit":{"message":"[Security Solution] expandable flyout - prevalence details UI improvements (#164016)","sha":"225fc95488f8c5f52b26a8715ce76b7d19f7301f"}},"sourceBranch":"main","suggestedTargetBranches":["8.10"],"targetPullRequestStates":[{"branch":"8.10","label":"v8.10.0","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v8.11.0","labelRegex":"^v8.11.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/164016","number":164016,"mergeCommit":{"message":"[Security Solution] expandable flyout - prevalence details UI improvements (#164016)","sha":"225fc95488f8c5f52b26a8715ce76b7d19f7301f"}}]}] BACKPORT--> Co-authored-by: Philippe Oberti <philippe.oberti@elastic.co>
This commit is contained in:
parent
ac42a09c3c
commit
5e12811ad4
45 changed files with 1168 additions and 1679 deletions
|
@ -7,7 +7,6 @@
|
|||
*/
|
||||
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { css } from '@emotion/react';
|
||||
import type { EuiFlyoutProps } from '@elastic/eui';
|
||||
import { EuiFlexGroup, EuiFlyout } from '@elastic/eui';
|
||||
import { useExpandableFlyoutContext } from './context';
|
||||
|
@ -28,10 +27,6 @@ export interface ExpandableFlyoutProps extends Omit<EuiFlyoutProps, 'onClose'> {
|
|||
handleOnFlyoutClosed?: () => void;
|
||||
}
|
||||
|
||||
const flyoutStyles = css`
|
||||
overflow-y: scroll;
|
||||
`;
|
||||
|
||||
const flyoutInnerStyles = { height: '100%' };
|
||||
|
||||
/**
|
||||
|
@ -88,13 +83,7 @@ export const ExpandableFlyout: React.FC<ExpandableFlyoutProps> = ({
|
|||
const previewSectionWidth: number = leftSection ? 0.4 : 1;
|
||||
|
||||
return (
|
||||
<EuiFlyout
|
||||
css={flyoutStyles}
|
||||
{...flyoutProps}
|
||||
size={flyoutWidth}
|
||||
ownFocus={false}
|
||||
onClose={onClose}
|
||||
>
|
||||
<EuiFlyout {...flyoutProps} size={flyoutWidth} ownFocus={false} onClose={onClose}>
|
||||
<EuiFlexGroup
|
||||
direction={leftSection ? 'row' : 'column'}
|
||||
wrap={false}
|
||||
|
|
|
@ -7,21 +7,16 @@
|
|||
|
||||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { getSummaryRows } from '../../../common/components/event_details/get_alert_summary_rows';
|
||||
import { LeftPanelContext } from '../context';
|
||||
import { PrevalenceDetails } from './prevalence_details';
|
||||
import {
|
||||
PREVALENCE_DETAILS_LOADING_TEST_ID,
|
||||
PREVALENCE_DETAILS_TABLE_ERROR_TEST_ID,
|
||||
PREVALENCE_DETAILS_TABLE_TEST_ID,
|
||||
} from './test_ids';
|
||||
import { useFetchFieldValuePairByEventType } from '../../shared/hooks/use_fetch_field_value_pair_by_event_type';
|
||||
import { useFetchFieldValuePairWithAggregation } from '../../shared/hooks/use_fetch_field_value_pair_with_aggregation';
|
||||
import { useFetchUniqueByField } from '../../shared/hooks/use_fetch_unique_by_field';
|
||||
import { usePrevalence } from '../../shared/hooks/use_prevalence';
|
||||
|
||||
jest.mock('../../../common/components/event_details/get_alert_summary_rows');
|
||||
jest.mock('../../shared/hooks/use_fetch_field_value_pair_by_event_type');
|
||||
jest.mock('../../shared/hooks/use_fetch_field_value_pair_with_aggregation');
|
||||
jest.mock('../../shared/hooks/use_fetch_unique_by_field');
|
||||
jest.mock('../../shared/hooks/use_prevalence');
|
||||
|
||||
const panelContextValue = {
|
||||
eventId: 'event id',
|
||||
|
@ -31,33 +26,31 @@ const panelContextValue = {
|
|||
} as unknown as LeftPanelContext;
|
||||
|
||||
describe('PrevalenceDetails', () => {
|
||||
jest.mocked(useFetchFieldValuePairByEventType).mockReturnValue({
|
||||
loading: false,
|
||||
error: false,
|
||||
count: 1,
|
||||
});
|
||||
jest.mocked(useFetchFieldValuePairWithAggregation).mockReturnValue({
|
||||
loading: false,
|
||||
error: false,
|
||||
count: 1,
|
||||
});
|
||||
jest.mocked(useFetchUniqueByField).mockReturnValue({
|
||||
loading: false,
|
||||
error: false,
|
||||
count: 1,
|
||||
});
|
||||
|
||||
it('should render the table', () => {
|
||||
const mockSummaryRow = {
|
||||
title: 'test',
|
||||
description: {
|
||||
data: {
|
||||
field: 'field',
|
||||
const field1 = 'field1';
|
||||
const field2 = 'field2';
|
||||
(usePrevalence as jest.Mock).mockReturnValue({
|
||||
loading: false,
|
||||
error: false,
|
||||
data: [
|
||||
{
|
||||
field: field1,
|
||||
value: 'value1',
|
||||
alertCount: 1,
|
||||
docCount: 1,
|
||||
hostPrevalence: 0.05,
|
||||
userPrevalence: 0.1,
|
||||
},
|
||||
values: ['value'],
|
||||
},
|
||||
};
|
||||
(getSummaryRows as jest.Mock).mockReturnValue([mockSummaryRow]);
|
||||
{
|
||||
field: field2,
|
||||
value: 'value2',
|
||||
alertCount: 1,
|
||||
docCount: 1,
|
||||
hostPrevalence: 0.5,
|
||||
userPrevalence: 0.05,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { getByTestId } = render(
|
||||
<LeftPanelContext.Provider value={panelContextValue}>
|
||||
|
@ -68,8 +61,28 @@ describe('PrevalenceDetails', () => {
|
|||
expect(getByTestId(PREVALENCE_DETAILS_TABLE_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render the error message if no highlighted fields', () => {
|
||||
jest.mocked(getSummaryRows).mockReturnValue([]);
|
||||
it('should render loading', () => {
|
||||
(usePrevalence as jest.Mock).mockReturnValue({
|
||||
loading: true,
|
||||
error: false,
|
||||
data: [],
|
||||
});
|
||||
|
||||
const { getByTestId } = render(
|
||||
<LeftPanelContext.Provider value={panelContextValue}>
|
||||
<PrevalenceDetails />
|
||||
</LeftPanelContext.Provider>
|
||||
);
|
||||
|
||||
expect(getByTestId(PREVALENCE_DETAILS_LOADING_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render error if call errors out', () => {
|
||||
(usePrevalence as jest.Mock).mockReturnValue({
|
||||
loading: false,
|
||||
error: true,
|
||||
data: [],
|
||||
});
|
||||
|
||||
const { getByTestId } = render(
|
||||
<LeftPanelContext.Provider value={panelContextValue}>
|
||||
|
@ -79,4 +92,64 @@ describe('PrevalenceDetails', () => {
|
|||
|
||||
expect(getByTestId(PREVALENCE_DETAILS_TABLE_ERROR_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render error if event is null', () => {
|
||||
const contextValue = {
|
||||
...panelContextValue,
|
||||
eventId: null,
|
||||
} as unknown as LeftPanelContext;
|
||||
(usePrevalence as jest.Mock).mockReturnValue({
|
||||
loading: false,
|
||||
error: true,
|
||||
data: [],
|
||||
});
|
||||
|
||||
const { getByTestId } = render(
|
||||
<LeftPanelContext.Provider value={contextValue}>
|
||||
<PrevalenceDetails />
|
||||
</LeftPanelContext.Provider>
|
||||
);
|
||||
|
||||
expect(getByTestId(PREVALENCE_DETAILS_TABLE_ERROR_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render error if dataFormattedForFieldBrowser is null', () => {
|
||||
const contextValue = {
|
||||
...panelContextValue,
|
||||
dataFormattedForFieldBrowser: null,
|
||||
};
|
||||
(usePrevalence as jest.Mock).mockReturnValue({
|
||||
loading: false,
|
||||
error: true,
|
||||
data: [],
|
||||
});
|
||||
|
||||
const { getByTestId } = render(
|
||||
<LeftPanelContext.Provider value={contextValue}>
|
||||
<PrevalenceDetails />
|
||||
</LeftPanelContext.Provider>
|
||||
);
|
||||
|
||||
expect(getByTestId(PREVALENCE_DETAILS_TABLE_ERROR_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render error if browserFields is null', () => {
|
||||
const contextValue = {
|
||||
...panelContextValue,
|
||||
browserFields: null,
|
||||
};
|
||||
(usePrevalence as jest.Mock).mockReturnValue({
|
||||
loading: false,
|
||||
error: true,
|
||||
data: [],
|
||||
});
|
||||
|
||||
const { getByTestId } = render(
|
||||
<LeftPanelContext.Provider value={contextValue}>
|
||||
<PrevalenceDetails />
|
||||
</LeftPanelContext.Provider>
|
||||
);
|
||||
|
||||
expect(getByTestId(PREVALENCE_DETAILS_TABLE_ERROR_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,102 +5,118 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import type { EuiBasicTableColumn } from '@elastic/eui';
|
||||
import { EuiBasicTable, EuiEmptyPrompt } from '@elastic/eui';
|
||||
import React, { useState } from 'react';
|
||||
import type { EuiBasicTableColumn, OnTimeChangeProps } from '@elastic/eui';
|
||||
import {
|
||||
EuiEmptyPrompt,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiInMemoryTable,
|
||||
EuiLoadingSpinner,
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
EuiSuperDatePicker,
|
||||
} from '@elastic/eui';
|
||||
import type { PrevalenceData } from '../../shared/hooks/use_prevalence';
|
||||
import { usePrevalence } from '../../shared/hooks/use_prevalence';
|
||||
import { ERROR_MESSAGE, ERROR_TITLE } from '../../shared/translations';
|
||||
import {
|
||||
HOST_TITLE,
|
||||
PREVALENCE_ERROR_MESSAGE,
|
||||
PREVALENCE_TABLE_ALERT_COUNT_COLUMN_TITLE,
|
||||
PREVALENCE_TABLE_COUNT_COLUMN_TITLE,
|
||||
PREVALENCE_TABLE_DOC_COUNT_COLUMN_TITLE,
|
||||
PREVALENCE_TABLE_HOST_PREVALENCE_COLUMN_TITLE,
|
||||
PREVALENCE_TABLE_NAME_COLUMN_TITLE,
|
||||
PREVALENCE_TABLE_TYPE_COLUMN_TITLE,
|
||||
PREVALENCE_TABLE_USER_PREVALENCE_COLUMN_TITLE,
|
||||
PREVALENCE_TABLE_VALUE_COLUMN_TITLE,
|
||||
PREVALENCE_TABLE_PREVALENCE_COLUMN_TITLE,
|
||||
PREVALENCE_TABLE_FIELD_COLUMN_TITLE,
|
||||
USER_TITLE,
|
||||
PREVALENCE_NO_DATA_MESSAGE,
|
||||
} from './translations';
|
||||
import {
|
||||
PREVALENCE_DETAILS_LOADING_TEST_ID,
|
||||
PREVALENCE_DETAILS_TABLE_ALERT_COUNT_CELL_TEST_ID,
|
||||
PREVALENCE_DETAILS_TABLE_DOC_COUNT_CELL_TEST_ID,
|
||||
PREVALENCE_DETAILS_TABLE_ERROR_TEST_ID,
|
||||
PREVALENCE_DETAILS_TABLE_HOST_PREVALENCE_CELL_TEST_ID,
|
||||
PREVALENCE_DETAILS_TABLE_NAME_CELL_TEST_ID,
|
||||
PREVALENCE_DETAILS_TABLE_TEST_ID,
|
||||
PREVALENCE_DETAILS_TABLE_TYPE_CELL_TEST_ID,
|
||||
PREVALENCE_DETAILS_TABLE_VALUE_CELL_TEST_ID,
|
||||
PREVALENCE_DETAILS_TABLE_FIELD_CELL_TEST_ID,
|
||||
PREVALENCE_DETAILS_TABLE_USER_PREVALENCE_CELL_TEST_ID,
|
||||
PREVALENCE_DETAILS_TABLE_NO_DATA_TEST_ID,
|
||||
PREVALENCE_DETAILS_DATE_PICKER_TEST_ID,
|
||||
PREVALENCE_DETAILS_TABLE_TEST_ID,
|
||||
} from './test_ids';
|
||||
import { PrevalenceDetailsCountCell } from './prevalence_details_count_cell';
|
||||
import type { AlertSummaryRow } from '../../../common/components/event_details/helpers';
|
||||
import { PrevalenceDetailsPrevalenceCell } from './prevalence_details_prevalence_cell';
|
||||
import { getSummaryRows } from '../../../common/components/event_details/get_alert_summary_rows';
|
||||
import { useLeftPanelContext } from '../context';
|
||||
import { EventKind } from '../../shared/hooks/use_fetch_field_value_pair_by_event_type';
|
||||
|
||||
interface PrevalenceDetailsTableCell {
|
||||
highlightedField: { name: string; values: string[] };
|
||||
}
|
||||
|
||||
export const PREVALENCE_TAB_ID = 'prevalence-details';
|
||||
const DEFAULT_FROM = 'now-30d';
|
||||
const DEFAULT_TO = 'now';
|
||||
|
||||
const columns: Array<EuiBasicTableColumn<unknown>> = [
|
||||
const columns: Array<EuiBasicTableColumn<PrevalenceData>> = [
|
||||
{
|
||||
field: 'type',
|
||||
name: PREVALENCE_TABLE_TYPE_COLUMN_TITLE,
|
||||
'data-test-subj': PREVALENCE_DETAILS_TABLE_TYPE_CELL_TEST_ID,
|
||||
field: 'field',
|
||||
name: PREVALENCE_TABLE_FIELD_COLUMN_TITLE,
|
||||
'data-test-subj': PREVALENCE_DETAILS_TABLE_FIELD_CELL_TEST_ID,
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
name: PREVALENCE_TABLE_NAME_COLUMN_TITLE,
|
||||
'data-test-subj': PREVALENCE_DETAILS_TABLE_NAME_CELL_TEST_ID,
|
||||
field: 'value',
|
||||
name: PREVALENCE_TABLE_VALUE_COLUMN_TITLE,
|
||||
'data-test-subj': PREVALENCE_DETAILS_TABLE_VALUE_CELL_TEST_ID,
|
||||
},
|
||||
{
|
||||
field: 'alertCount',
|
||||
name: PREVALENCE_TABLE_ALERT_COUNT_COLUMN_TITLE,
|
||||
'data-test-subj': PREVALENCE_DETAILS_TABLE_ALERT_COUNT_CELL_TEST_ID,
|
||||
render: (data: PrevalenceDetailsTableCell) => (
|
||||
<PrevalenceDetailsCountCell
|
||||
highlightedField={data.highlightedField}
|
||||
type={{
|
||||
eventKind: EventKind.signal,
|
||||
include: true,
|
||||
}}
|
||||
/>
|
||||
name: (
|
||||
<EuiFlexGroup direction="column" gutterSize="none">
|
||||
<EuiFlexItem>{PREVALENCE_TABLE_ALERT_COUNT_COLUMN_TITLE}</EuiFlexItem>
|
||||
<EuiFlexItem>{PREVALENCE_TABLE_COUNT_COLUMN_TITLE}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
'data-test-subj': PREVALENCE_DETAILS_TABLE_ALERT_COUNT_CELL_TEST_ID,
|
||||
width: '10%',
|
||||
},
|
||||
{
|
||||
field: 'docCount',
|
||||
name: PREVALENCE_TABLE_DOC_COUNT_COLUMN_TITLE,
|
||||
'data-test-subj': PREVALENCE_DETAILS_TABLE_DOC_COUNT_CELL_TEST_ID,
|
||||
render: (data: PrevalenceDetailsTableCell) => (
|
||||
<PrevalenceDetailsCountCell
|
||||
highlightedField={data.highlightedField}
|
||||
type={{
|
||||
eventKind: EventKind.signal,
|
||||
exclude: true,
|
||||
}}
|
||||
/>
|
||||
name: (
|
||||
<EuiFlexGroup direction="column" gutterSize="none">
|
||||
<EuiFlexItem>{PREVALENCE_TABLE_DOC_COUNT_COLUMN_TITLE}</EuiFlexItem>
|
||||
<EuiFlexItem>{PREVALENCE_TABLE_COUNT_COLUMN_TITLE}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
'data-test-subj': PREVALENCE_DETAILS_TABLE_DOC_COUNT_CELL_TEST_ID,
|
||||
width: '10%',
|
||||
},
|
||||
{
|
||||
field: 'hostPrevalence',
|
||||
name: PREVALENCE_TABLE_HOST_PREVALENCE_COLUMN_TITLE,
|
||||
'data-test-subj': PREVALENCE_DETAILS_TABLE_HOST_PREVALENCE_CELL_TEST_ID,
|
||||
render: (data: PrevalenceDetailsTableCell) => (
|
||||
<PrevalenceDetailsPrevalenceCell
|
||||
highlightedField={data.highlightedField}
|
||||
aggregationField={'host.name'}
|
||||
/>
|
||||
name: (
|
||||
<EuiFlexGroup direction="column" gutterSize="none">
|
||||
<EuiFlexItem>{HOST_TITLE}</EuiFlexItem>
|
||||
<EuiFlexItem>{PREVALENCE_TABLE_PREVALENCE_COLUMN_TITLE}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
'data-test-subj': PREVALENCE_DETAILS_TABLE_HOST_PREVALENCE_CELL_TEST_ID,
|
||||
render: (hostPrevalence: number) => (
|
||||
<>
|
||||
{Math.round(hostPrevalence * 100)}
|
||||
{'%'}
|
||||
</>
|
||||
),
|
||||
width: '10%',
|
||||
},
|
||||
{
|
||||
field: 'userPrevalence',
|
||||
name: PREVALENCE_TABLE_USER_PREVALENCE_COLUMN_TITLE,
|
||||
'data-test-subj': PREVALENCE_DETAILS_TABLE_USER_PREVALENCE_CELL_TEST_ID,
|
||||
render: (data: PrevalenceDetailsTableCell) => (
|
||||
<PrevalenceDetailsPrevalenceCell
|
||||
highlightedField={data.highlightedField}
|
||||
aggregationField={'user.name'}
|
||||
/>
|
||||
name: (
|
||||
<EuiFlexGroup direction="column" gutterSize="none">
|
||||
<EuiFlexItem>{USER_TITLE}</EuiFlexItem>
|
||||
<EuiFlexItem>{PREVALENCE_TABLE_PREVALENCE_COLUMN_TITLE}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
'data-test-subj': PREVALENCE_DETAILS_TABLE_USER_PREVALENCE_CELL_TEST_ID,
|
||||
render: (userPrevalence: number) => (
|
||||
<>
|
||||
{Math.round(userPrevalence * 100)}
|
||||
{'%'}
|
||||
</>
|
||||
),
|
||||
width: '10%',
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -108,40 +124,40 @@ const columns: Array<EuiBasicTableColumn<unknown>> = [
|
|||
* Prevalence table displayed in the document details expandable flyout left section under the Insights tab
|
||||
*/
|
||||
export const PrevalenceDetails: React.FC = () => {
|
||||
const { browserFields, dataFormattedForFieldBrowser, eventId, scopeId, investigationFields } =
|
||||
const { browserFields, dataFormattedForFieldBrowser, eventId, investigationFields } =
|
||||
useLeftPanelContext();
|
||||
|
||||
const data = useMemo(() => {
|
||||
const summaryRows = getSummaryRows({
|
||||
browserFields: browserFields || {},
|
||||
data: dataFormattedForFieldBrowser || [],
|
||||
eventId,
|
||||
scopeId,
|
||||
investigationFields,
|
||||
isReadOnly: false,
|
||||
});
|
||||
const [start, setStart] = useState(DEFAULT_FROM);
|
||||
const [end, setEnd] = useState(DEFAULT_TO);
|
||||
|
||||
const getCellRenderFields = (summaryRow: AlertSummaryRow): PrevalenceDetailsTableCell => ({
|
||||
highlightedField: {
|
||||
name: summaryRow.description.data.field,
|
||||
values: summaryRow.description.values || [],
|
||||
},
|
||||
});
|
||||
const onTimeChange = ({ start: s, end: e }: OnTimeChangeProps) => {
|
||||
setStart(s);
|
||||
setEnd(e);
|
||||
};
|
||||
|
||||
return (summaryRows || []).map((summaryRow) => {
|
||||
const fields = getCellRenderFields(summaryRow);
|
||||
return {
|
||||
type: summaryRow.description.data.field,
|
||||
name: summaryRow.description.values,
|
||||
alertCount: fields,
|
||||
docCount: fields,
|
||||
hostPrevalence: fields,
|
||||
userPrevalence: fields,
|
||||
};
|
||||
});
|
||||
}, [browserFields, investigationFields, dataFormattedForFieldBrowser, eventId, scopeId]);
|
||||
const { loading, error, data } = usePrevalence({
|
||||
dataFormattedForFieldBrowser,
|
||||
investigationFields,
|
||||
interval: {
|
||||
from: start,
|
||||
to: end,
|
||||
},
|
||||
});
|
||||
|
||||
if (!eventId || !dataFormattedForFieldBrowser || !browserFields || !data || data.length === 0) {
|
||||
if (loading) {
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
justifyContent="spaceAround"
|
||||
data-test-subj={PREVALENCE_DETAILS_LOADING_TEST_ID}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLoadingSpinner size="m" />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
if (!eventId || !dataFormattedForFieldBrowser || !browserFields || error) {
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
iconType="error"
|
||||
|
@ -154,12 +170,29 @@ export const PrevalenceDetails: React.FC = () => {
|
|||
}
|
||||
|
||||
return (
|
||||
<EuiBasicTable
|
||||
tableCaption=""
|
||||
items={data}
|
||||
columns={columns}
|
||||
data-test-subj={PREVALENCE_DETAILS_TABLE_TEST_ID}
|
||||
/>
|
||||
<>
|
||||
<EuiPanel>
|
||||
<EuiSuperDatePicker
|
||||
start={start}
|
||||
end={end}
|
||||
onTimeChange={onTimeChange}
|
||||
data-test-subj={PREVALENCE_DETAILS_DATE_PICKER_TEST_ID}
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
{data.length > 0 ? (
|
||||
<EuiInMemoryTable
|
||||
items={data}
|
||||
columns={columns}
|
||||
data-test-subj={PREVALENCE_DETAILS_TABLE_TEST_ID}
|
||||
tableLayout="auto"
|
||||
/>
|
||||
) : (
|
||||
<div data-test-subj={`${PREVALENCE_DETAILS_TABLE_NO_DATA_TEST_ID}Error`}>
|
||||
{PREVALENCE_NO_DATA_MESSAGE}
|
||||
</div>
|
||||
)}
|
||||
</EuiPanel>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,90 +0,0 @@
|
|||
/*
|
||||
* 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 { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import {
|
||||
EventKind,
|
||||
useFetchFieldValuePairByEventType,
|
||||
} from '../../shared/hooks/use_fetch_field_value_pair_by_event_type';
|
||||
import {
|
||||
PREVALENCE_DETAILS_COUNT_CELL_ERROR_TEST_ID,
|
||||
PREVALENCE_DETAILS_COUNT_CELL_LOADING_TEST_ID,
|
||||
PREVALENCE_DETAILS_COUNT_CELL_VALUE_TEST_ID,
|
||||
} from './test_ids';
|
||||
import { PrevalenceDetailsCountCell } from './prevalence_details_count_cell';
|
||||
|
||||
jest.mock('../../shared/hooks/use_fetch_field_value_pair_by_event_type');
|
||||
|
||||
const highlightedField = {
|
||||
name: 'field',
|
||||
values: ['values'],
|
||||
};
|
||||
const type = {
|
||||
eventKind: EventKind.signal,
|
||||
include: true,
|
||||
};
|
||||
|
||||
describe('PrevalenceDetailsAlertCountCell', () => {
|
||||
it('should show loading spinner', () => {
|
||||
jest.mocked(useFetchFieldValuePairByEventType).mockReturnValue({
|
||||
loading: true,
|
||||
error: false,
|
||||
count: 0,
|
||||
});
|
||||
|
||||
const { getByTestId } = render(
|
||||
<PrevalenceDetailsCountCell highlightedField={highlightedField} type={type} />
|
||||
);
|
||||
|
||||
expect(getByTestId(PREVALENCE_DETAILS_COUNT_CELL_LOADING_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should return error icon', () => {
|
||||
jest.mocked(useFetchFieldValuePairByEventType).mockReturnValue({
|
||||
loading: false,
|
||||
error: true,
|
||||
count: 0,
|
||||
});
|
||||
|
||||
const { getByTestId } = render(
|
||||
<PrevalenceDetailsCountCell highlightedField={highlightedField} type={type} />
|
||||
);
|
||||
|
||||
expect(getByTestId(PREVALENCE_DETAILS_COUNT_CELL_ERROR_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show count value with eventKind undefined', () => {
|
||||
jest.mocked(useFetchFieldValuePairByEventType).mockReturnValue({
|
||||
loading: false,
|
||||
error: false,
|
||||
count: 1,
|
||||
});
|
||||
|
||||
const { getByTestId } = render(
|
||||
<PrevalenceDetailsCountCell highlightedField={highlightedField} type={type} />
|
||||
);
|
||||
|
||||
expect(getByTestId(PREVALENCE_DETAILS_COUNT_CELL_VALUE_TEST_ID)).toBeInTheDocument();
|
||||
expect(getByTestId(PREVALENCE_DETAILS_COUNT_CELL_VALUE_TEST_ID)).toHaveTextContent('1');
|
||||
});
|
||||
|
||||
it('should show count value with eventKind passed via props', () => {
|
||||
jest.mocked(useFetchFieldValuePairByEventType).mockReturnValue({
|
||||
loading: false,
|
||||
error: false,
|
||||
count: 1,
|
||||
});
|
||||
|
||||
const { getByTestId } = render(
|
||||
<PrevalenceDetailsCountCell highlightedField={highlightedField} type={type} />
|
||||
);
|
||||
|
||||
expect(getByTestId(PREVALENCE_DETAILS_COUNT_CELL_VALUE_TEST_ID)).toBeInTheDocument();
|
||||
expect(getByTestId(PREVALENCE_DETAILS_COUNT_CELL_VALUE_TEST_ID)).toHaveTextContent('1');
|
||||
});
|
||||
});
|
|
@ -1,63 +0,0 @@
|
|||
/*
|
||||
* 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 { VFC } from 'react';
|
||||
import React from 'react';
|
||||
import { EuiIcon, EuiLoadingSpinner } from '@elastic/eui';
|
||||
import {
|
||||
PREVALENCE_DETAILS_COUNT_CELL_ERROR_TEST_ID,
|
||||
PREVALENCE_DETAILS_COUNT_CELL_LOADING_TEST_ID,
|
||||
PREVALENCE_DETAILS_COUNT_CELL_VALUE_TEST_ID,
|
||||
} from './test_ids';
|
||||
import type { EventType } from '../../shared/hooks/use_fetch_field_value_pair_by_event_type';
|
||||
import { useFetchFieldValuePairByEventType } from '../../shared/hooks/use_fetch_field_value_pair_by_event_type';
|
||||
|
||||
export interface PrevalenceDetailsCountCellProps {
|
||||
/**
|
||||
* The highlighted field name and values
|
||||
* */
|
||||
highlightedField: { name: string; values: string[] };
|
||||
/**
|
||||
* Limit the search to include or exclude a specific value for the event.kind field
|
||||
* (alert, asset, enrichment, event, metric, state, pipeline_error, signal)
|
||||
*/
|
||||
type: EventType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component displaying a value in many PrevalenceDetails table cells. It is used for the third and fourth columns,
|
||||
* which display the number of alerts and documents for a given field/value pair.
|
||||
* For the doc columns, type should "signal" for its eventKind property, and exclude set to true.
|
||||
* For the alert columns, type should have "signal" for its eventKind property, and include should be true.
|
||||
*/
|
||||
export const PrevalenceDetailsCountCell: VFC<PrevalenceDetailsCountCellProps> = ({
|
||||
highlightedField,
|
||||
type,
|
||||
}) => {
|
||||
const { loading, error, count } = useFetchFieldValuePairByEventType({
|
||||
highlightedField,
|
||||
type,
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return <EuiLoadingSpinner data-test-subj={PREVALENCE_DETAILS_COUNT_CELL_LOADING_TEST_ID} />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<EuiIcon
|
||||
data-test-subj={PREVALENCE_DETAILS_COUNT_CELL_ERROR_TEST_ID}
|
||||
type="error"
|
||||
color="danger"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <div data-test-subj={PREVALENCE_DETAILS_COUNT_CELL_VALUE_TEST_ID}>{count}</div>;
|
||||
};
|
||||
|
||||
PrevalenceDetailsCountCell.displayName = 'PrevalenceDetailsCountCell';
|
|
@ -1,161 +0,0 @@
|
|||
/*
|
||||
* 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 { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import {
|
||||
PREVALENCE_DETAILS_PREVALENCE_CELL_ERROR_TEST_ID,
|
||||
PREVALENCE_DETAILS_PREVALENCE_CELL_LOADING_TEST_ID,
|
||||
PREVALENCE_DETAILS_PREVALENCE_CELL_VALUE_TEST_ID,
|
||||
} from './test_ids';
|
||||
import { PrevalenceDetailsPrevalenceCell } from './prevalence_details_prevalence_cell';
|
||||
import { useFetchFieldValuePairWithAggregation } from '../../shared/hooks/use_fetch_field_value_pair_with_aggregation';
|
||||
import { useFetchUniqueByField } from '../../shared/hooks/use_fetch_unique_by_field';
|
||||
|
||||
jest.mock('../../shared/hooks/use_fetch_field_value_pair_with_aggregation');
|
||||
jest.mock('../../shared/hooks/use_fetch_unique_by_field');
|
||||
|
||||
const highlightedField = {
|
||||
name: 'field',
|
||||
values: ['values'],
|
||||
};
|
||||
const aggregationField = 'aggregationField';
|
||||
|
||||
describe('PrevalenceDetailsAlertCountCell', () => {
|
||||
it('should show loading spinner when useFetchFieldValuePairWithAggregation loading', () => {
|
||||
jest.mocked(useFetchFieldValuePairWithAggregation).mockReturnValue({
|
||||
loading: true,
|
||||
error: false,
|
||||
count: 0,
|
||||
});
|
||||
jest.mocked(useFetchUniqueByField).mockReturnValue({
|
||||
loading: false,
|
||||
error: false,
|
||||
count: 0,
|
||||
});
|
||||
|
||||
const { getByTestId } = render(
|
||||
<PrevalenceDetailsPrevalenceCell
|
||||
highlightedField={highlightedField}
|
||||
aggregationField={aggregationField}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(getByTestId(PREVALENCE_DETAILS_PREVALENCE_CELL_LOADING_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show loading spinner when useFetchUniqueByField loading', () => {
|
||||
jest.mocked(useFetchFieldValuePairWithAggregation).mockReturnValue({
|
||||
loading: false,
|
||||
error: false,
|
||||
count: 0,
|
||||
});
|
||||
jest.mocked(useFetchUniqueByField).mockReturnValue({
|
||||
loading: true,
|
||||
error: false,
|
||||
count: 0,
|
||||
});
|
||||
|
||||
const { getByTestId } = render(
|
||||
<PrevalenceDetailsPrevalenceCell
|
||||
highlightedField={highlightedField}
|
||||
aggregationField={aggregationField}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(getByTestId(PREVALENCE_DETAILS_PREVALENCE_CELL_LOADING_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should return null if useFetchFieldValuePairWithAggregation errors out', () => {
|
||||
jest.mocked(useFetchFieldValuePairWithAggregation).mockReturnValue({
|
||||
loading: false,
|
||||
error: true,
|
||||
count: 0,
|
||||
});
|
||||
jest.mocked(useFetchUniqueByField).mockReturnValue({
|
||||
loading: false,
|
||||
error: false,
|
||||
count: 0,
|
||||
});
|
||||
|
||||
const { getByTestId } = render(
|
||||
<PrevalenceDetailsPrevalenceCell
|
||||
highlightedField={highlightedField}
|
||||
aggregationField={aggregationField}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(getByTestId(PREVALENCE_DETAILS_PREVALENCE_CELL_ERROR_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should return null if useFetchUniqueByField errors out', () => {
|
||||
jest.mocked(useFetchFieldValuePairWithAggregation).mockReturnValue({
|
||||
loading: false,
|
||||
error: false,
|
||||
count: 0,
|
||||
});
|
||||
jest.mocked(useFetchUniqueByField).mockReturnValue({
|
||||
loading: false,
|
||||
error: true,
|
||||
count: 0,
|
||||
});
|
||||
|
||||
const { getByTestId } = render(
|
||||
<PrevalenceDetailsPrevalenceCell
|
||||
highlightedField={highlightedField}
|
||||
aggregationField={aggregationField}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(getByTestId(PREVALENCE_DETAILS_PREVALENCE_CELL_ERROR_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should return null if prevalence is infinite', () => {
|
||||
jest.mocked(useFetchFieldValuePairWithAggregation).mockReturnValue({
|
||||
loading: false,
|
||||
error: false,
|
||||
count: 0,
|
||||
});
|
||||
jest.mocked(useFetchUniqueByField).mockReturnValue({
|
||||
loading: false,
|
||||
error: false,
|
||||
count: 0,
|
||||
});
|
||||
|
||||
const { getByTestId } = render(
|
||||
<PrevalenceDetailsPrevalenceCell
|
||||
highlightedField={highlightedField}
|
||||
aggregationField={aggregationField}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(getByTestId(PREVALENCE_DETAILS_PREVALENCE_CELL_ERROR_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show prevalence value', () => {
|
||||
jest.mocked(useFetchFieldValuePairWithAggregation).mockReturnValue({
|
||||
loading: false,
|
||||
error: false,
|
||||
count: 1,
|
||||
});
|
||||
jest.mocked(useFetchUniqueByField).mockReturnValue({
|
||||
loading: false,
|
||||
error: false,
|
||||
count: 1,
|
||||
});
|
||||
|
||||
const { getByTestId } = render(
|
||||
<PrevalenceDetailsPrevalenceCell
|
||||
highlightedField={highlightedField}
|
||||
aggregationField={aggregationField}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(getByTestId(PREVALENCE_DETAILS_PREVALENCE_CELL_VALUE_TEST_ID)).toBeInTheDocument();
|
||||
expect(getByTestId(PREVALENCE_DETAILS_PREVALENCE_CELL_VALUE_TEST_ID)).toHaveTextContent('1');
|
||||
});
|
||||
});
|
|
@ -1,78 +0,0 @@
|
|||
/*
|
||||
* 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 { VFC } from 'react';
|
||||
import React from 'react';
|
||||
import { EuiIcon, EuiLoadingSpinner } from '@elastic/eui';
|
||||
import {
|
||||
PREVALENCE_DETAILS_PREVALENCE_CELL_ERROR_TEST_ID,
|
||||
PREVALENCE_DETAILS_PREVALENCE_CELL_LOADING_TEST_ID,
|
||||
PREVALENCE_DETAILS_PREVALENCE_CELL_VALUE_TEST_ID,
|
||||
} from './test_ids';
|
||||
import { useFetchFieldValuePairWithAggregation } from '../../shared/hooks/use_fetch_field_value_pair_with_aggregation';
|
||||
import { useFetchUniqueByField } from '../../shared/hooks/use_fetch_unique_by_field';
|
||||
|
||||
export interface PrevalenceDetailsPrevalenceCellProps {
|
||||
/**
|
||||
* The highlighted field name and values
|
||||
* */
|
||||
highlightedField: { name: string; values: string[] };
|
||||
/**
|
||||
* The aggregation field
|
||||
*/
|
||||
aggregationField: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component displaying a value in many PrevalenceDetails table cells. It is used for the fifth and sixth columns,
|
||||
* which displays the prevalence percentage for host.name and user.name fields.
|
||||
*/
|
||||
export const PrevalenceDetailsPrevalenceCell: VFC<PrevalenceDetailsPrevalenceCellProps> = ({
|
||||
highlightedField,
|
||||
aggregationField,
|
||||
}) => {
|
||||
const {
|
||||
loading: aggregationLoading,
|
||||
error: aggregationError,
|
||||
count: aggregationCount,
|
||||
} = useFetchFieldValuePairWithAggregation({
|
||||
highlightedField,
|
||||
aggregationField,
|
||||
});
|
||||
|
||||
const {
|
||||
loading: uniqueLoading,
|
||||
error: uniqueError,
|
||||
count: uniqueCount,
|
||||
} = useFetchUniqueByField({ field: aggregationField });
|
||||
|
||||
if (aggregationLoading || uniqueLoading) {
|
||||
return (
|
||||
<EuiLoadingSpinner data-test-subj={PREVALENCE_DETAILS_PREVALENCE_CELL_LOADING_TEST_ID} />
|
||||
);
|
||||
}
|
||||
|
||||
const prevalence = aggregationCount / uniqueCount;
|
||||
if (aggregationError || uniqueError || !isFinite(prevalence)) {
|
||||
return (
|
||||
<EuiIcon
|
||||
data-test-subj={PREVALENCE_DETAILS_PREVALENCE_CELL_ERROR_TEST_ID}
|
||||
type="error"
|
||||
color="danger"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div data-test-subj={PREVALENCE_DETAILS_PREVALENCE_CELL_VALUE_TEST_ID}>
|
||||
{Math.round(prevalence * 100)}
|
||||
{'%'}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
PrevalenceDetailsPrevalenceCell.displayName = 'PrevalenceDetailsPrevalenceCell';
|
|
@ -18,11 +18,14 @@ export const SESSION_VIEW_ERROR_TEST_ID = `${PREFIX}SessionViewError` as const;
|
|||
|
||||
/* Prevalence */
|
||||
|
||||
export const PREVALENCE_DETAILS_DATE_PICKER_TEST_ID =
|
||||
`${PREFIX}PrevalenceDetailsDatePicker` as const;
|
||||
export const PREVALENCE_DETAILS_TABLE_TEST_ID = `${PREFIX}PrevalenceDetailsTable` as const;
|
||||
export const PREVALENCE_DETAILS_TABLE_TYPE_CELL_TEST_ID =
|
||||
`${PREFIX}PrevalenceDetailsTableTypeCell` as const;
|
||||
export const PREVALENCE_DETAILS_TABLE_NAME_CELL_TEST_ID =
|
||||
`${PREFIX}PrevalenceDetailsTableNameCell` as const;
|
||||
export const PREVALENCE_DETAILS_LOADING_TEST_ID = `${PREFIX}PrevalenceDetailsLoading` as const;
|
||||
export const PREVALENCE_DETAILS_TABLE_FIELD_CELL_TEST_ID =
|
||||
`${PREFIX}PrevalenceDetailsTableFieldCell` as const;
|
||||
export const PREVALENCE_DETAILS_TABLE_VALUE_CELL_TEST_ID =
|
||||
`${PREFIX}PrevalenceDetailsTableValueCell` as const;
|
||||
export const PREVALENCE_DETAILS_TABLE_ALERT_COUNT_CELL_TEST_ID =
|
||||
`${PREFIX}PrevalenceDetailsTableAlertCountCell` as const;
|
||||
export const PREVALENCE_DETAILS_TABLE_DOC_COUNT_CELL_TEST_ID =
|
||||
|
@ -31,19 +34,10 @@ export const PREVALENCE_DETAILS_TABLE_HOST_PREVALENCE_CELL_TEST_ID =
|
|||
`${PREFIX}PrevalenceDetailsTableHostPrevalenceCell` as const;
|
||||
export const PREVALENCE_DETAILS_TABLE_USER_PREVALENCE_CELL_TEST_ID =
|
||||
`${PREFIX}PrevalenceDetailsTableUserPrevalenceCell` as const;
|
||||
export const PREVALENCE_DETAILS_TABLE_ERROR_TEST_ID = `${PREFIX}PrevalenceDetailsTable` as const;
|
||||
export const PREVALENCE_DETAILS_COUNT_CELL_LOADING_TEST_ID =
|
||||
`${PREFIX}PrevalenceDetailsCountCellLoading` as const;
|
||||
export const PREVALENCE_DETAILS_COUNT_CELL_ERROR_TEST_ID =
|
||||
`${PREFIX}PrevalenceDetailsCountCellError` as const;
|
||||
export const PREVALENCE_DETAILS_COUNT_CELL_VALUE_TEST_ID =
|
||||
`${PREFIX}PrevalenceDetailsCountCellValue` as const;
|
||||
export const PREVALENCE_DETAILS_PREVALENCE_CELL_LOADING_TEST_ID =
|
||||
`${PREFIX}PrevalenceDetailsPrevalenceCellLoading` as const;
|
||||
export const PREVALENCE_DETAILS_PREVALENCE_CELL_ERROR_TEST_ID =
|
||||
`${PREFIX}PrevalenceDetailsPrevalenceCellError` as const;
|
||||
export const PREVALENCE_DETAILS_PREVALENCE_CELL_VALUE_TEST_ID =
|
||||
`${PREFIX}PrevalenceDetailsPrevalenceCellValue` as const;
|
||||
export const PREVALENCE_DETAILS_TABLE_ERROR_TEST_ID =
|
||||
`${PREFIX}PrevalenceDetailsTableError` as const;
|
||||
export const PREVALENCE_DETAILS_TABLE_NO_DATA_TEST_ID =
|
||||
`${PREFIX}PrevalenceDetailsTableNoData` as const;
|
||||
|
||||
/* Entities */
|
||||
|
||||
|
|
|
@ -99,45 +99,52 @@ export const PREVALENCE_ERROR_MESSAGE = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const PREVALENCE_TABLE_TYPE_COLUMN_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.flyout.prevalenceTableTypeColumnTitle',
|
||||
export const PREVALENCE_NO_DATA_MESSAGE = i18n.translate(
|
||||
'xpack.securitySolution.flyout.prevalenceNoDataMessage',
|
||||
{
|
||||
defaultMessage: 'Type',
|
||||
defaultMessage: 'No prevalence data available',
|
||||
}
|
||||
);
|
||||
|
||||
export const PREVALENCE_TABLE_NAME_COLUMN_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.flyout.prevalenceTableNameColumnTitle',
|
||||
export const PREVALENCE_TABLE_FIELD_COLUMN_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.flyout.prevalenceTableFieldColumnTitle',
|
||||
{
|
||||
defaultMessage: 'Name',
|
||||
defaultMessage: 'Field',
|
||||
}
|
||||
);
|
||||
|
||||
export const PREVALENCE_TABLE_VALUE_COLUMN_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.flyout.prevalenceTableValueColumnTitle',
|
||||
{
|
||||
defaultMessage: 'Value',
|
||||
}
|
||||
);
|
||||
|
||||
export const PREVALENCE_TABLE_ALERT_COUNT_COLUMN_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.flyout.prevalenceTableAlertCountColumnTitle',
|
||||
{
|
||||
defaultMessage: 'Alert count',
|
||||
defaultMessage: 'Alert',
|
||||
}
|
||||
);
|
||||
|
||||
export const PREVALENCE_TABLE_DOC_COUNT_COLUMN_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.flyout.prevalenceTableDocCountColumnTitle',
|
||||
{
|
||||
defaultMessage: 'Doc count',
|
||||
defaultMessage: 'Document',
|
||||
}
|
||||
);
|
||||
|
||||
export const PREVALENCE_TABLE_HOST_PREVALENCE_COLUMN_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.flyout.prevalenceTableHostPrevalenceColumnTitle',
|
||||
export const PREVALENCE_TABLE_COUNT_COLUMN_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.flyout.prevalenceTableCountColumnTitle',
|
||||
{
|
||||
defaultMessage: 'Host prevalence',
|
||||
defaultMessage: 'count',
|
||||
}
|
||||
);
|
||||
|
||||
export const PREVALENCE_TABLE_USER_PREVALENCE_COLUMN_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.flyout.prevalenceTableUserPrevalenceColumnTitle',
|
||||
export const PREVALENCE_TABLE_PREVALENCE_COLUMN_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.flyout.prevalenceTablePrevalenceColumnTitle',
|
||||
{
|
||||
defaultMessage: 'User prevalence',
|
||||
defaultMessage: 'prevalence',
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -11,11 +11,11 @@ import { RightPanelContext } from '../context';
|
|||
import { HIGHLIGHTED_FIELDS_DETAILS_TEST_ID, HIGHLIGHTED_FIELDS_TITLE_TEST_ID } from './test_ids';
|
||||
import { HighlightedFields } from './highlighted_fields';
|
||||
import { mockDataFormattedForFieldBrowser } from '../mocks/mock_context';
|
||||
import { useHighlightedFields } from '../hooks/use_highlighted_fields';
|
||||
import { useHighlightedFields } from '../../shared/hooks/use_highlighted_fields';
|
||||
import { TestProviders } from '../../../common/mock';
|
||||
import { useRuleWithFallback } from '../../../detection_engine/rule_management/logic/use_rule_with_fallback';
|
||||
|
||||
jest.mock('../hooks/use_highlighted_fields');
|
||||
jest.mock('../../shared/hooks/use_highlighted_fields');
|
||||
jest.mock('../../../detection_engine/rule_management/logic/use_rule_with_fallback');
|
||||
|
||||
describe('<HighlightedFields />', () => {
|
||||
|
@ -27,15 +27,11 @@ describe('<HighlightedFields />', () => {
|
|||
const panelContextValue = {
|
||||
dataFormattedForFieldBrowser: mockDataFormattedForFieldBrowser,
|
||||
} as unknown as RightPanelContext;
|
||||
(useHighlightedFields as jest.Mock).mockReturnValue([
|
||||
{
|
||||
field: 'field',
|
||||
description: {
|
||||
field: 'field',
|
||||
values: ['value'],
|
||||
},
|
||||
(useHighlightedFields as jest.Mock).mockReturnValue({
|
||||
field: {
|
||||
values: ['value'],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
|
@ -53,7 +49,7 @@ describe('<HighlightedFields />', () => {
|
|||
const panelContextValue = {
|
||||
dataFormattedForFieldBrowser: mockDataFormattedForFieldBrowser,
|
||||
} as unknown as RightPanelContext;
|
||||
(useHighlightedFields as jest.Mock).mockReturnValue([]);
|
||||
(useHighlightedFields as jest.Mock).mockReturnValue({});
|
||||
|
||||
const { container } = render(
|
||||
<RightPanelContext.Provider value={panelContextValue}>
|
||||
|
@ -68,15 +64,11 @@ describe('<HighlightedFields />', () => {
|
|||
const panelContextValue = {
|
||||
dataFormattedForFieldBrowser: null,
|
||||
} as unknown as RightPanelContext;
|
||||
(useHighlightedFields as jest.Mock).mockReturnValue([
|
||||
{
|
||||
field: 'field',
|
||||
description: {
|
||||
field: 'field',
|
||||
values: ['value'],
|
||||
},
|
||||
(useHighlightedFields as jest.Mock).mockReturnValue({
|
||||
field: {
|
||||
values: ['value'],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
const { container } = render(
|
||||
<RightPanelContext.Provider value={panelContextValue}>
|
||||
|
|
|
@ -6,9 +6,10 @@
|
|||
*/
|
||||
|
||||
import type { FC } from 'react';
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import type { EuiBasicTableColumn } from '@elastic/eui';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiInMemoryTable, EuiPanel, EuiTitle } from '@elastic/eui';
|
||||
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';
|
||||
|
@ -17,7 +18,6 @@ import {
|
|||
SecurityCellActions,
|
||||
SecurityCellActionsTrigger,
|
||||
} from '../../../common/components/cell_actions';
|
||||
import type { UseHighlightedFieldsResult } from '../hooks/use_highlighted_fields';
|
||||
import { HIGHLIGHTED_FIELDS_DETAILS_TEST_ID, HIGHLIGHTED_FIELDS_TITLE_TEST_ID } from './test_ids';
|
||||
import {
|
||||
HIGHLIGHTED_FIELDS_FIELD_COLUMN,
|
||||
|
@ -25,14 +25,30 @@ import {
|
|||
HIGHLIGHTED_FIELDS_VALUE_COLUMN,
|
||||
} from './translations';
|
||||
import { useRightPanelContext } from '../context';
|
||||
import { useHighlightedFields } from '../hooks/use_highlighted_fields';
|
||||
import { useHighlightedFields } from '../../shared/hooks/use_highlighted_fields';
|
||||
|
||||
const columns: Array<EuiBasicTableColumn<UseHighlightedFieldsResult>> = [
|
||||
export interface HighlightedFieldsTableRow {
|
||||
/**
|
||||
* Highlighted field name (overrideField or if null, falls back to id)
|
||||
*/
|
||||
field: string;
|
||||
description: {
|
||||
/**
|
||||
* Highlighted field name (overrideField or if null, falls back to id)
|
||||
*/
|
||||
field: string;
|
||||
/**
|
||||
* Highlighted field value
|
||||
*/
|
||||
values: string[] | null | undefined;
|
||||
};
|
||||
}
|
||||
|
||||
const columns: Array<EuiBasicTableColumn<HighlightedFieldsTableRow>> = [
|
||||
{
|
||||
field: 'field',
|
||||
name: HIGHLIGHTED_FIELDS_FIELD_COLUMN,
|
||||
'data-test-subj': 'fieldCell',
|
||||
width: '125px',
|
||||
},
|
||||
{
|
||||
field: 'description',
|
||||
|
@ -66,8 +82,12 @@ export const HighlightedFields: FC = () => {
|
|||
dataFormattedForFieldBrowser,
|
||||
investigationFields: maybeRule?.investigation_fields ?? [],
|
||||
});
|
||||
const items = useMemo(
|
||||
() => convertHighlightedFieldsToTableRow(highlightedFields),
|
||||
[highlightedFields]
|
||||
);
|
||||
|
||||
if (!dataFormattedForFieldBrowser || highlightedFields.length === 0) {
|
||||
if (!dataFormattedForFieldBrowser || items.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -80,7 +100,7 @@ export const HighlightedFields: FC = () => {
|
|||
</EuiFlexItem>
|
||||
<EuiFlexItem data-test-subj={HIGHLIGHTED_FIELDS_DETAILS_TEST_ID}>
|
||||
<EuiPanel hasBorder hasShadow={false}>
|
||||
<EuiInMemoryTable items={highlightedFields} columns={columns} compressed />
|
||||
<EuiInMemoryTable items={items} columns={columns} compressed tableLayout="auto" />
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -13,152 +13,187 @@ import { INSIGHTS_PREVALENCE_TEST_ID } from './test_ids';
|
|||
import { LeftPanelInsightsTab, LeftPanelKey } from '../../left';
|
||||
import React from 'react';
|
||||
import { PrevalenceOverview } from './prevalence_overview';
|
||||
import { usePrevalence } from '../hooks/use_prevalence';
|
||||
import { PrevalenceOverviewRow } from './prevalence_overview_row';
|
||||
import { useFetchFieldValuePairWithAggregation } from '../../shared/hooks/use_fetch_field_value_pair_with_aggregation';
|
||||
import { useFetchUniqueByField } from '../../shared/hooks/use_fetch_unique_by_field';
|
||||
import { PREVALENCE_TAB_ID } from '../../left/components/prevalence_details';
|
||||
import type { BrowserFields, TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common';
|
||||
import {
|
||||
EXPANDABLE_PANEL_HEADER_TITLE_ICON_TEST_ID,
|
||||
EXPANDABLE_PANEL_HEADER_TITLE_LINK_TEST_ID,
|
||||
EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID,
|
||||
EXPANDABLE_PANEL_LOADING_TEST_ID,
|
||||
EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID,
|
||||
} from '../../shared/components/test_ids';
|
||||
import { usePrevalence } from '../../shared/hooks/use_prevalence';
|
||||
import { mockContextValue } from '../mocks/mock_right_panel_context';
|
||||
|
||||
jest.mock('../../shared/hooks/use_fetch_field_value_pair_with_aggregation');
|
||||
jest.mock('../../shared/hooks/use_fetch_unique_by_field');
|
||||
jest.mock('../hooks/use_prevalence');
|
||||
jest.mock('../../shared/hooks/use_prevalence');
|
||||
|
||||
const TOGGLE_ICON_TEST_ID = EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID(INSIGHTS_PREVALENCE_TEST_ID);
|
||||
const TITLE_LINK_TEST_ID = EXPANDABLE_PANEL_HEADER_TITLE_LINK_TEST_ID(INSIGHTS_PREVALENCE_TEST_ID);
|
||||
const TITLE_ICON_TEST_ID = EXPANDABLE_PANEL_HEADER_TITLE_ICON_TEST_ID(INSIGHTS_PREVALENCE_TEST_ID);
|
||||
const TITLE_TEXT_TEST_ID = EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID(INSIGHTS_PREVALENCE_TEST_ID);
|
||||
|
||||
const highlightedField = {
|
||||
name: 'field',
|
||||
values: ['values'],
|
||||
};
|
||||
const panelContextValue = (
|
||||
eventId: string | null,
|
||||
browserFields: BrowserFields | null,
|
||||
dataFormattedForFieldBrowser: TimelineEventsDetailsItem[] | null
|
||||
) =>
|
||||
({
|
||||
eventId,
|
||||
indexName: 'indexName',
|
||||
browserFields,
|
||||
dataFormattedForFieldBrowser,
|
||||
scopeId: 'scopeId',
|
||||
} as unknown as RightPanelContext);
|
||||
const flyoutContextValue = {
|
||||
openLeftPanel: jest.fn(),
|
||||
} as unknown as ExpandableFlyoutContext;
|
||||
|
||||
const renderPrevalenceOverview = (contextValue: RightPanelContext) => (
|
||||
const renderPrevalenceOverview = (contextValue: RightPanelContext = mockContextValue) => (
|
||||
<TestProviders>
|
||||
<RightPanelContext.Provider value={contextValue}>
|
||||
<PrevalenceOverview />
|
||||
</RightPanelContext.Provider>
|
||||
<ExpandableFlyoutContext.Provider value={flyoutContextValue}>
|
||||
<RightPanelContext.Provider value={contextValue}>
|
||||
<PrevalenceOverview />
|
||||
</RightPanelContext.Provider>
|
||||
</ExpandableFlyoutContext.Provider>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
describe('<PrevalenceOverview />', () => {
|
||||
it('should render wrapper component', () => {
|
||||
(useFetchFieldValuePairWithAggregation as jest.Mock).mockReturnValue({
|
||||
(usePrevalence as jest.Mock).mockReturnValue({
|
||||
loading: false,
|
||||
error: false,
|
||||
count: 1,
|
||||
data: [],
|
||||
});
|
||||
(useFetchUniqueByField as jest.Mock).mockReturnValue({
|
||||
loading: false,
|
||||
error: false,
|
||||
count: 10,
|
||||
});
|
||||
(usePrevalence as jest.Mock).mockReturnValue([]);
|
||||
|
||||
const { getByTestId, queryByTestId } = render(
|
||||
renderPrevalenceOverview(panelContextValue('eventId', {}, []))
|
||||
);
|
||||
const { getByTestId, queryByTestId } = render(renderPrevalenceOverview());
|
||||
expect(queryByTestId(TOGGLE_ICON_TEST_ID)).not.toBeInTheDocument();
|
||||
expect(getByTestId(TITLE_LINK_TEST_ID)).toBeInTheDocument();
|
||||
expect(getByTestId(TITLE_LINK_TEST_ID)).toHaveTextContent('Prevalence');
|
||||
expect(getByTestId(TITLE_ICON_TEST_ID)).toBeInTheDocument();
|
||||
expect(queryByTestId(TITLE_TEXT_TEST_ID)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render component', () => {
|
||||
(useFetchFieldValuePairWithAggregation as jest.Mock).mockReturnValue({
|
||||
loading: false,
|
||||
it('should render loading', () => {
|
||||
(usePrevalence as jest.Mock).mockReturnValue({
|
||||
loading: true,
|
||||
error: false,
|
||||
count: 1,
|
||||
data: [],
|
||||
});
|
||||
(useFetchUniqueByField as jest.Mock).mockReturnValue({
|
||||
loading: false,
|
||||
error: false,
|
||||
count: 10,
|
||||
});
|
||||
(usePrevalence as jest.Mock).mockReturnValue([
|
||||
<PrevalenceOverviewRow highlightedField={highlightedField} data-test-subj={'test'} />,
|
||||
]);
|
||||
|
||||
const { getByTestId } = render(renderPrevalenceOverview(panelContextValue('eventId', {}, [])));
|
||||
const { getByTestId } = render(renderPrevalenceOverview());
|
||||
|
||||
expect(
|
||||
getByTestId(EXPANDABLE_PANEL_LOADING_TEST_ID(INSIGHTS_PREVALENCE_TEST_ID))
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render no-data message', () => {
|
||||
(usePrevalence as jest.Mock).mockReturnValue({
|
||||
loading: false,
|
||||
error: false,
|
||||
data: [],
|
||||
});
|
||||
|
||||
const { getByTestId } = render(renderPrevalenceOverview());
|
||||
|
||||
expect(getByTestId(`${INSIGHTS_PREVALENCE_TEST_ID}Error`)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render only data with prevalence less than 10%', () => {
|
||||
const field1 = 'field1';
|
||||
const field2 = 'field2';
|
||||
(usePrevalence as jest.Mock).mockReturnValue({
|
||||
loading: false,
|
||||
error: false,
|
||||
data: [
|
||||
{
|
||||
field: field1,
|
||||
value: 'value1',
|
||||
alertCount: 1,
|
||||
docCount: 1,
|
||||
hostPrevalence: 0.05,
|
||||
userPrevalence: 0.1,
|
||||
},
|
||||
{
|
||||
field: field2,
|
||||
value: 'value2',
|
||||
alertCount: 1,
|
||||
docCount: 1,
|
||||
hostPrevalence: 0.5,
|
||||
userPrevalence: 0.05,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { queryByTestId, getByTestId } = render(renderPrevalenceOverview());
|
||||
|
||||
expect(getByTestId(TITLE_LINK_TEST_ID)).toHaveTextContent('Prevalence');
|
||||
|
||||
const iconDataTestSubj = 'testIcon';
|
||||
const valueDataTestSubj = 'testValue';
|
||||
expect(getByTestId(iconDataTestSubj)).toBeInTheDocument();
|
||||
expect(getByTestId(valueDataTestSubj)).toBeInTheDocument();
|
||||
const iconDataTestSubj1 = `${INSIGHTS_PREVALENCE_TEST_ID}${field1}Icon`;
|
||||
const valueDataTestSubj1 = `${INSIGHTS_PREVALENCE_TEST_ID}${field1}Value`;
|
||||
expect(getByTestId(iconDataTestSubj1)).toBeInTheDocument();
|
||||
expect(getByTestId(valueDataTestSubj1)).toBeInTheDocument();
|
||||
expect(getByTestId(valueDataTestSubj1)).toHaveTextContent('field1, value1 is uncommon');
|
||||
|
||||
const iconDataTestSubj2 = `${INSIGHTS_PREVALENCE_TEST_ID}${field2}Icon`;
|
||||
const valueDataTestSubj2 = `${INSIGHTS_PREVALENCE_TEST_ID}${field2}Value`;
|
||||
expect(queryByTestId(iconDataTestSubj2)).not.toBeInTheDocument();
|
||||
expect(queryByTestId(valueDataTestSubj2)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render null if eventId is null', () => {
|
||||
(usePrevalence as jest.Mock).mockReturnValue([]);
|
||||
(usePrevalence as jest.Mock).mockReturnValue({
|
||||
loading: false,
|
||||
error: false,
|
||||
data: [],
|
||||
});
|
||||
const contextValue = {
|
||||
...mockContextValue,
|
||||
eventId: null,
|
||||
} as unknown as RightPanelContext;
|
||||
|
||||
const { container } = render(renderPrevalenceOverview(panelContextValue(null, {}, [])));
|
||||
const { container } = render(renderPrevalenceOverview(contextValue));
|
||||
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('should render null if browserFields is null', () => {
|
||||
(usePrevalence as jest.Mock).mockReturnValue([]);
|
||||
(usePrevalence as jest.Mock).mockReturnValue({
|
||||
loading: false,
|
||||
error: false,
|
||||
data: [],
|
||||
});
|
||||
const contextValue = {
|
||||
...mockContextValue,
|
||||
browserFields: null,
|
||||
};
|
||||
|
||||
const { container } = render(renderPrevalenceOverview(panelContextValue('eventId', null, [])));
|
||||
const { container } = render(renderPrevalenceOverview(contextValue));
|
||||
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('should render null if dataFormattedForFieldBrowser is null', () => {
|
||||
(usePrevalence as jest.Mock).mockReturnValue([]);
|
||||
(usePrevalence as jest.Mock).mockReturnValue({
|
||||
loading: false,
|
||||
error: false,
|
||||
data: [],
|
||||
});
|
||||
const contextValue = {
|
||||
...mockContextValue,
|
||||
dataFormattedForFieldBrowser: null,
|
||||
};
|
||||
|
||||
const { container } = render(renderPrevalenceOverview(panelContextValue('eventId', {}, null)));
|
||||
const { container } = render(renderPrevalenceOverview(contextValue));
|
||||
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('should navigate to left section Insights tab when clicking on button', () => {
|
||||
(useFetchFieldValuePairWithAggregation as jest.Mock).mockReturnValue({
|
||||
(usePrevalence as jest.Mock).mockReturnValue({
|
||||
loading: false,
|
||||
error: false,
|
||||
count: 1,
|
||||
data: [
|
||||
{
|
||||
field: 'field1',
|
||||
value: 'value1',
|
||||
alertCount: 1,
|
||||
docCount: 1,
|
||||
hostPrevalence: 0.05,
|
||||
userPrevalence: 0.1,
|
||||
},
|
||||
],
|
||||
});
|
||||
(useFetchUniqueByField as jest.Mock).mockReturnValue({
|
||||
loading: false,
|
||||
error: false,
|
||||
count: 10,
|
||||
});
|
||||
(usePrevalence as jest.Mock).mockReturnValue([
|
||||
<PrevalenceOverviewRow highlightedField={highlightedField} data-test-subj={'test'} />,
|
||||
]);
|
||||
const flyoutContextValue = {
|
||||
openLeftPanel: jest.fn(),
|
||||
} as unknown as ExpandableFlyoutContext;
|
||||
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<ExpandableFlyoutContext.Provider value={flyoutContextValue}>
|
||||
<RightPanelContext.Provider value={panelContextValue('eventId', {}, [])}>
|
||||
<PrevalenceOverview />
|
||||
</RightPanelContext.Provider>
|
||||
</ExpandableFlyoutContext.Provider>
|
||||
</TestProviders>
|
||||
);
|
||||
const { getByTestId } = render(renderPrevalenceOverview());
|
||||
|
||||
getByTestId(TITLE_LINK_TEST_ID).click();
|
||||
expect(flyoutContextValue.openLeftPanel).toHaveBeenCalledWith({
|
||||
|
@ -166,7 +201,7 @@ describe('<PrevalenceOverview />', () => {
|
|||
path: { tab: LeftPanelInsightsTab, subTab: PREVALENCE_TAB_ID },
|
||||
params: {
|
||||
id: 'eventId',
|
||||
indexName: 'indexName',
|
||||
indexName: 'index',
|
||||
scopeId: 'scopeId',
|
||||
},
|
||||
});
|
||||
|
|
|
@ -6,21 +6,25 @@
|
|||
*/
|
||||
|
||||
import type { FC } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { EuiFlexGroup } from '@elastic/eui';
|
||||
import { useExpandableFlyoutContext } from '@kbn/expandable-flyout';
|
||||
import { ExpandablePanel } from '../../shared/components/expandable_panel';
|
||||
import { usePrevalence } from '../hooks/use_prevalence';
|
||||
import { usePrevalence } from '../../shared/hooks/use_prevalence';
|
||||
import { INSIGHTS_PREVALENCE_TEST_ID } from './test_ids';
|
||||
import { useRightPanelContext } from '../context';
|
||||
import { PREVALENCE_TITLE } from './translations';
|
||||
import { PREVALENCE_NO_DATA, PREVALENCE_ROW_UNCOMMON, PREVALENCE_TITLE } from './translations';
|
||||
import { LeftPanelKey, LeftPanelInsightsTab } from '../../left';
|
||||
import { PREVALENCE_TAB_ID } from '../../left/components/prevalence_details';
|
||||
import { InsightsSummaryRow } from './insights_summary_row';
|
||||
|
||||
const PERCENTAGE_THRESHOLD = 0.1; // we show the prevalence if its value is below 10%
|
||||
const DEFAULT_FROM = 'now-30d';
|
||||
const DEFAULT_TO = 'now';
|
||||
|
||||
/**
|
||||
* Prevalence section under Insights section, overview tab.
|
||||
* The component fetches the necessary data, then pass it down to the InsightsSubSection component for loading and error state,
|
||||
* and the SummaryPanel component for data rendering.
|
||||
* The component fetches the necessary data at once. The loading and error states are handled by the ExpandablePanel component.
|
||||
*/
|
||||
export const PrevalenceOverview: FC = () => {
|
||||
const {
|
||||
|
@ -48,14 +52,27 @@ export const PrevalenceOverview: FC = () => {
|
|||
});
|
||||
}, [eventId, openLeftPanel, indexName, scopeId]);
|
||||
|
||||
const prevalenceRows = usePrevalence({
|
||||
eventId,
|
||||
browserFields,
|
||||
const { loading, error, data } = usePrevalence({
|
||||
dataFormattedForFieldBrowser,
|
||||
investigationFields,
|
||||
scopeId,
|
||||
interval: {
|
||||
from: DEFAULT_FROM,
|
||||
to: DEFAULT_TO,
|
||||
},
|
||||
});
|
||||
|
||||
// only show data if the host prevalence is below 10%
|
||||
const uncommonData = useMemo(
|
||||
() =>
|
||||
data.filter(
|
||||
(d) =>
|
||||
isFinite(d.hostPrevalence) &&
|
||||
d.hostPrevalence > 0 &&
|
||||
d.hostPrevalence < PERCENTAGE_THRESHOLD
|
||||
),
|
||||
[data]
|
||||
);
|
||||
|
||||
if (!eventId || !browserFields || !dataFormattedForFieldBrowser) {
|
||||
return null;
|
||||
}
|
||||
|
@ -67,10 +84,21 @@ export const PrevalenceOverview: FC = () => {
|
|||
callback: goToCorrelationsTab,
|
||||
iconType: 'arrowStart',
|
||||
}}
|
||||
content={{ loading, error }}
|
||||
data-test-subj={INSIGHTS_PREVALENCE_TEST_ID}
|
||||
>
|
||||
<EuiFlexGroup direction="column" gutterSize="none">
|
||||
{prevalenceRows}
|
||||
{uncommonData.length > 0 ? (
|
||||
uncommonData.map((d) => (
|
||||
<InsightsSummaryRow
|
||||
icon={'warning'}
|
||||
text={`${d.field}, ${d.value} ${PREVALENCE_ROW_UNCOMMON}`}
|
||||
data-test-subj={`${INSIGHTS_PREVALENCE_TEST_ID}${d.field}`}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<div data-test-subj={`${INSIGHTS_PREVALENCE_TEST_ID}Error`}>{PREVALENCE_NO_DATA}</div>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</ExpandablePanel>
|
||||
);
|
||||
|
|
|
@ -1,108 +0,0 @@
|
|||
/*
|
||||
* 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 { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { PrevalenceOverviewRow } from './prevalence_overview_row';
|
||||
import { useFetchFieldValuePairWithAggregation } from '../../shared/hooks/use_fetch_field_value_pair_with_aggregation';
|
||||
import { useFetchUniqueByField } from '../../shared/hooks/use_fetch_unique_by_field';
|
||||
|
||||
jest.mock('../../shared/hooks/use_fetch_field_value_pair_with_aggregation');
|
||||
jest.mock('../../shared/hooks/use_fetch_unique_by_field');
|
||||
|
||||
const highlightedField = {
|
||||
name: 'field',
|
||||
values: ['values'],
|
||||
};
|
||||
const dataTestSubj = 'test';
|
||||
const iconDataTestSubj = 'testIcon';
|
||||
const valueDataTestSubj = 'testValue';
|
||||
const colorDataTestSubj = 'testColor';
|
||||
const loadingDataTestSubj = 'testLoading';
|
||||
|
||||
describe('<PrevalenceOverviewRow />', () => {
|
||||
it('should display row if prevalence is below or equal threshold', () => {
|
||||
(useFetchFieldValuePairWithAggregation as jest.Mock).mockReturnValue({
|
||||
loading: false,
|
||||
error: false,
|
||||
count: 1,
|
||||
});
|
||||
(useFetchUniqueByField as jest.Mock).mockReturnValue({
|
||||
loading: false,
|
||||
error: false,
|
||||
count: 10,
|
||||
});
|
||||
|
||||
const { getByTestId, getAllByText, queryByTestId } = render(
|
||||
<PrevalenceOverviewRow highlightedField={highlightedField} data-test-subj={dataTestSubj} />
|
||||
);
|
||||
|
||||
const { name, values } = highlightedField;
|
||||
|
||||
expect(getByTestId(iconDataTestSubj)).toBeInTheDocument();
|
||||
expect(getByTestId(valueDataTestSubj)).toBeInTheDocument();
|
||||
expect(getAllByText(`${name}, ${values} is uncommon`)).toHaveLength(1);
|
||||
expect(queryByTestId(colorDataTestSubj)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not display row if prevalence is higher than threshold', () => {
|
||||
(useFetchFieldValuePairWithAggregation as jest.Mock).mockReturnValue({
|
||||
loading: false,
|
||||
error: false,
|
||||
count: 1,
|
||||
});
|
||||
(useFetchUniqueByField as jest.Mock).mockReturnValue({
|
||||
loading: false,
|
||||
error: false,
|
||||
count: 2,
|
||||
});
|
||||
|
||||
const { queryAllByAltText } = render(
|
||||
<PrevalenceOverviewRow highlightedField={highlightedField} data-test-subj={dataTestSubj} />
|
||||
);
|
||||
|
||||
expect(queryAllByAltText('is uncommon')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should not display row if error retrieving data', () => {
|
||||
(useFetchFieldValuePairWithAggregation as jest.Mock).mockReturnValue({
|
||||
loading: false,
|
||||
error: true,
|
||||
count: 0,
|
||||
});
|
||||
(useFetchUniqueByField as jest.Mock).mockReturnValue({
|
||||
loading: false,
|
||||
error: true,
|
||||
count: 0,
|
||||
});
|
||||
|
||||
const { queryAllByAltText } = render(
|
||||
<PrevalenceOverviewRow highlightedField={highlightedField} data-test-subj={dataTestSubj} />
|
||||
);
|
||||
|
||||
expect(queryAllByAltText('is uncommon')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should display loading', () => {
|
||||
(useFetchFieldValuePairWithAggregation as jest.Mock).mockReturnValue({
|
||||
loading: true,
|
||||
error: false,
|
||||
count: 1,
|
||||
});
|
||||
(useFetchUniqueByField as jest.Mock).mockReturnValue({
|
||||
loading: false,
|
||||
error: false,
|
||||
count: 10,
|
||||
});
|
||||
|
||||
const { getByTestId } = render(
|
||||
<PrevalenceOverviewRow highlightedField={highlightedField} data-test-subj={dataTestSubj} />
|
||||
);
|
||||
|
||||
expect(getByTestId(loadingDataTestSubj)).toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -1,75 +0,0 @@
|
|||
/*
|
||||
* 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 { VFC } from 'react';
|
||||
import React from 'react';
|
||||
import { PREVALENCE_ROW_UNCOMMON } from './translations';
|
||||
import { useFetchFieldValuePairWithAggregation } from '../../shared/hooks/use_fetch_field_value_pair_with_aggregation';
|
||||
import { useFetchUniqueByField } from '../../shared/hooks/use_fetch_unique_by_field';
|
||||
import { InsightsSummaryRow } from './insights_summary_row';
|
||||
|
||||
const HOST_FIELD = 'host.name';
|
||||
const PERCENTAGE_THRESHOLD = 0.1; // we show the prevalence if its value is below 10%
|
||||
|
||||
export interface PrevalenceOverviewRowProps {
|
||||
/**
|
||||
* The highlighted field name and values
|
||||
* */
|
||||
highlightedField: { name: string; values: string[] };
|
||||
/**
|
||||
* Prefix data-test-subj because this component will be used in multiple places
|
||||
*/
|
||||
['data-test-subj']?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the unique hosts for the field/value pair as well as the total number of unique hosts,
|
||||
* calculate the prevalence. If the prevalence is higher than 0.1, the row will render null.
|
||||
*/
|
||||
export const PrevalenceOverviewRow: VFC<PrevalenceOverviewRowProps> = ({
|
||||
highlightedField,
|
||||
'data-test-subj': dataTestSubj,
|
||||
}) => {
|
||||
const {
|
||||
loading: hostsLoading,
|
||||
error: hostsError,
|
||||
count: hostsCount,
|
||||
} = useFetchFieldValuePairWithAggregation({
|
||||
highlightedField,
|
||||
aggregationField: HOST_FIELD,
|
||||
});
|
||||
|
||||
const {
|
||||
loading: uniqueHostsLoading,
|
||||
error: uniqueHostsError,
|
||||
count: uniqueHostsCount,
|
||||
} = useFetchUniqueByField({ field: HOST_FIELD });
|
||||
|
||||
const { name, values } = highlightedField;
|
||||
|
||||
// prevalence is number of host(s) where the field/value pair was found divided by the total number of hosts in the environment
|
||||
const prevalence = hostsCount / uniqueHostsCount;
|
||||
const loading = hostsLoading || uniqueHostsLoading;
|
||||
const error = hostsError || uniqueHostsError;
|
||||
const text = `${name}, ${values} ${PREVALENCE_ROW_UNCOMMON}`;
|
||||
|
||||
// we do not want to render the row is the prevalence is Infinite, 0 or above the decided threshold
|
||||
const shouldNotRender =
|
||||
isFinite(prevalence) && (prevalence === 0 || prevalence > PERCENTAGE_THRESHOLD);
|
||||
|
||||
return (
|
||||
<InsightsSummaryRow
|
||||
loading={loading}
|
||||
error={error || shouldNotRender}
|
||||
icon={'warning'}
|
||||
text={text}
|
||||
data-test-subj={dataTestSubj}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
PrevalenceOverviewRow.displayName = 'PrevalenceOverviewRow';
|
|
@ -119,8 +119,6 @@ export const INSIGHTS_CORRELATIONS_RELATED_ALERTS_BY_ANCESTRY_TEST_ID =
|
|||
|
||||
export const INSIGHTS_PREVALENCE_TEST_ID =
|
||||
'securitySolutionDocumentDetailsFlyoutInsightsPrevalence';
|
||||
export const INSIGHTS_PREVALENCE_ROW_TEST_ID =
|
||||
'securitySolutionDocumentDetailsFlyoutInsightsPrevalenceRow';
|
||||
|
||||
/* Visualizations section */
|
||||
|
||||
|
|
|
@ -157,6 +157,11 @@ export const PREVALENCE_TITLE = i18n.translate(
|
|||
{ defaultMessage: 'Prevalence' }
|
||||
);
|
||||
|
||||
export const PREVALENCE_NO_DATA = i18n.translate(
|
||||
'xpack.securitySolution.flyout.documentDetails.prevalenceNoData',
|
||||
{ defaultMessage: 'No field/value pairs are uncommon' }
|
||||
);
|
||||
|
||||
export const THREAT_MATCH_DETECTED = i18n.translate(
|
||||
'xpack.securitySolution.flyout.documentDetails.overviewTab.threatIntelligence.threatMatch',
|
||||
{
|
||||
|
|
|
@ -1,53 +0,0 @@
|
|||
/*
|
||||
* 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 { ReactElement } from 'react';
|
||||
import { getSummaryRows } from '../../../common/components/event_details/get_alert_summary_rows';
|
||||
import { usePrevalence } from './use_prevalence';
|
||||
import type { RenderHookResult } from '@testing-library/react-hooks';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { mockDataFormattedForFieldBrowser } from '../mocks/mock_context';
|
||||
|
||||
jest.mock('../../../common/components/event_details/get_alert_summary_rows');
|
||||
|
||||
const eventId = 'eventId';
|
||||
const browserFields = null;
|
||||
const dataFormattedForFieldBrowser = mockDataFormattedForFieldBrowser;
|
||||
const scopeId = 'scopeId';
|
||||
|
||||
describe('usePrevalence', () => {
|
||||
let hookResult: RenderHookResult<unknown, ReactElement[]>;
|
||||
|
||||
it('should return 1 row to render', () => {
|
||||
const mockSummaryRow = {
|
||||
title: 'test',
|
||||
description: {
|
||||
data: {
|
||||
field: 'field',
|
||||
},
|
||||
values: ['value'],
|
||||
},
|
||||
};
|
||||
(getSummaryRows as jest.Mock).mockReturnValue([mockSummaryRow]);
|
||||
|
||||
hookResult = renderHook(() =>
|
||||
usePrevalence({ browserFields, dataFormattedForFieldBrowser, eventId, scopeId })
|
||||
);
|
||||
|
||||
expect(hookResult.result.current.length).toEqual(1);
|
||||
});
|
||||
|
||||
it('should return empty true', () => {
|
||||
(getSummaryRows as jest.Mock).mockReturnValue([]);
|
||||
|
||||
hookResult = renderHook(() =>
|
||||
usePrevalence({ browserFields, dataFormattedForFieldBrowser, eventId, scopeId })
|
||||
);
|
||||
|
||||
expect(hookResult.result.current.length).toEqual(0);
|
||||
});
|
||||
});
|
|
@ -1,83 +0,0 @@
|
|||
/*
|
||||
* 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 { BrowserFields, TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common';
|
||||
import type { ReactElement } from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { getSummaryRows } from '../../../common/components/event_details/get_alert_summary_rows';
|
||||
import { PrevalenceOverviewRow } from '../components/prevalence_overview_row';
|
||||
import { INSIGHTS_PREVALENCE_ROW_TEST_ID } from '../components/test_ids';
|
||||
|
||||
export interface UsePrevalenceParams {
|
||||
/**
|
||||
* Id of the document
|
||||
*/
|
||||
eventId: string;
|
||||
/**
|
||||
* An object containing fields by type
|
||||
*/
|
||||
browserFields: BrowserFields | null;
|
||||
/**
|
||||
* An array of field objects with category and value
|
||||
*/
|
||||
dataFormattedForFieldBrowser: TimelineEventsDetailsItem[] | null;
|
||||
/**
|
||||
* Maintain backwards compatibility // TODO remove when possible
|
||||
*/
|
||||
scopeId: string;
|
||||
/**
|
||||
* User defined fields to highlight (defined on rule)
|
||||
*/
|
||||
investigationFields?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* This hook retrieves the highlighted fields from the {@link getSummaryRows} method, then iterates through them
|
||||
* and generate a {@link PrevalenceOverviewRow} for each.
|
||||
* We use a callback method passed down to the {@link PrevalenceOverviewRow} component to know when it's rendered as null.
|
||||
* We need to let the parent know when all the {@link PrevalenceOverviewRow} are null, so it can hide then entire section.
|
||||
*/
|
||||
export const usePrevalence = ({
|
||||
eventId,
|
||||
browserFields,
|
||||
dataFormattedForFieldBrowser,
|
||||
investigationFields,
|
||||
scopeId,
|
||||
}: UsePrevalenceParams): ReactElement[] => {
|
||||
// retrieves the highlighted fields
|
||||
const summaryRows = useMemo(
|
||||
() =>
|
||||
getSummaryRows({
|
||||
browserFields: browserFields || {},
|
||||
data: dataFormattedForFieldBrowser || [],
|
||||
investigationFields: investigationFields || [],
|
||||
eventId,
|
||||
scopeId,
|
||||
isReadOnly: false,
|
||||
}),
|
||||
[browserFields, investigationFields, dataFormattedForFieldBrowser, eventId, scopeId]
|
||||
);
|
||||
|
||||
return useMemo(
|
||||
() =>
|
||||
summaryRows.map((row) => {
|
||||
const highlightedField = {
|
||||
name: row.description.data.field,
|
||||
values: row.description.values || [],
|
||||
};
|
||||
|
||||
return (
|
||||
<PrevalenceOverviewRow
|
||||
highlightedField={highlightedField}
|
||||
data-test-subj={INSIGHTS_PREVALENCE_ROW_TEST_ID}
|
||||
key={row.description.data.field}
|
||||
/>
|
||||
);
|
||||
}),
|
||||
[summaryRows]
|
||||
);
|
||||
};
|
|
@ -9,7 +9,7 @@ import type { FC } from 'react';
|
|||
import React, { memo, useMemo } from 'react';
|
||||
import type { FlyoutPanelProps, PanelPath } from '@kbn/expandable-flyout';
|
||||
import { useExpandableFlyoutContext } from '@kbn/expandable-flyout';
|
||||
import { EventKind } from '../shared/hooks/use_fetch_field_value_pair_by_event_type';
|
||||
import { EventKind } from '../shared/constants/event_kinds';
|
||||
import { getField } from '../shared/utils';
|
||||
import { useRightPanelContext } from './context';
|
||||
import { PanelHeader } from './header';
|
||||
|
|
|
@ -17,7 +17,7 @@ export const mockContextValue: RightPanelContext = {
|
|||
scopeId: 'scopeId',
|
||||
getFieldsData: mockGetFieldsData,
|
||||
dataFormattedForFieldBrowser: mockDataFormattedForFieldBrowser,
|
||||
browserFields: null,
|
||||
browserFields: {},
|
||||
dataAsNestedObject: null,
|
||||
searchHit: undefined,
|
||||
investigationFields: [],
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export enum EventKind {
|
||||
alert = 'alert',
|
||||
asset = 'asset',
|
||||
enrichment = 'enrichment',
|
||||
event = 'event',
|
||||
metric = 'metric',
|
||||
state = 'state',
|
||||
pipeline_error = 'pipeline_error',
|
||||
signal = 'signal',
|
||||
}
|
|
@ -1,85 +0,0 @@
|
|||
/*
|
||||
* 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 { useQuery } from '@tanstack/react-query';
|
||||
import type { RenderHookResult } from '@testing-library/react-hooks';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import type {
|
||||
UseFetchFieldValuePairByEventTypeParams,
|
||||
UseFetchFieldValuePairByEventTypeResult,
|
||||
} from './use_fetch_field_value_pair_by_event_type';
|
||||
import {
|
||||
EventKind,
|
||||
useFetchFieldValuePairByEventType,
|
||||
} from './use_fetch_field_value_pair_by_event_type';
|
||||
|
||||
jest.mock('@tanstack/react-query');
|
||||
jest.mock('../../../common/lib/kibana');
|
||||
|
||||
const highlightedField = {
|
||||
name: 'field',
|
||||
values: ['values'],
|
||||
};
|
||||
const type = {
|
||||
eventKind: EventKind.alert,
|
||||
include: true,
|
||||
};
|
||||
|
||||
describe('useFetchFieldValuePairByEventType', () => {
|
||||
let hookResult: RenderHookResult<
|
||||
UseFetchFieldValuePairByEventTypeParams,
|
||||
UseFetchFieldValuePairByEventTypeResult
|
||||
>;
|
||||
(useKibana as jest.Mock).mockReturnValue({
|
||||
services: {
|
||||
data: { search: jest.fn() },
|
||||
},
|
||||
});
|
||||
|
||||
it('should return loading true while data is being fetched', () => {
|
||||
(useQuery as jest.Mock).mockReturnValue({
|
||||
isLoading: true,
|
||||
isError: false,
|
||||
data: 0,
|
||||
});
|
||||
|
||||
hookResult = renderHook(() => useFetchFieldValuePairByEventType({ highlightedField, type }));
|
||||
|
||||
expect(hookResult.result.current.loading).toBeTruthy();
|
||||
expect(hookResult.result.current.error).toBeFalsy();
|
||||
expect(hookResult.result.current.count).toBe(0);
|
||||
});
|
||||
|
||||
it('should return error true when data fetching has errored out', () => {
|
||||
(useQuery as jest.Mock).mockReturnValue({
|
||||
isLoading: false,
|
||||
isError: true,
|
||||
data: 0,
|
||||
});
|
||||
|
||||
hookResult = renderHook(() => useFetchFieldValuePairByEventType({ highlightedField, type }));
|
||||
|
||||
expect(hookResult.result.current.loading).toBeFalsy();
|
||||
expect(hookResult.result.current.error).toBeTruthy();
|
||||
expect(hookResult.result.current.count).toBe(0);
|
||||
});
|
||||
|
||||
it('should return count on success', () => {
|
||||
(useQuery as jest.Mock).mockReturnValue({
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
data: 1,
|
||||
});
|
||||
|
||||
hookResult = renderHook(() => useFetchFieldValuePairByEventType({ highlightedField, type }));
|
||||
|
||||
expect(hookResult.result.current.loading).toBeFalsy();
|
||||
expect(hookResult.result.current.error).toBeFalsy();
|
||||
expect(hookResult.result.current.count).toBe(1);
|
||||
});
|
||||
});
|
|
@ -1,167 +0,0 @@
|
|||
/*
|
||||
* 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 { buildEsQuery } from '@kbn/es-query';
|
||||
import type { IEsSearchRequest } from '@kbn/data-plugin/public';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { createFetchData } from '../utils/fetch_data';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import type { RawResponse } from '../utils/fetch_data';
|
||||
|
||||
const QUERY_KEY = 'FetchFieldValuePairByEventType';
|
||||
const DEFAULT_FROM = 'now-30d';
|
||||
const DEFAULT_TO = 'now';
|
||||
|
||||
export enum EventKind {
|
||||
alert = 'alert',
|
||||
asset = 'asset',
|
||||
enrichment = 'enrichment',
|
||||
event = 'event',
|
||||
metric = 'metric',
|
||||
state = 'state',
|
||||
pipeline_error = 'pipeline_error',
|
||||
signal = 'signal',
|
||||
}
|
||||
|
||||
export interface EventType {
|
||||
eventKind: EventKind;
|
||||
include?: boolean;
|
||||
exclude?: boolean;
|
||||
}
|
||||
|
||||
export interface UseFetchFieldValuePairByEventTypeParams {
|
||||
/**
|
||||
* The highlighted field name and values
|
||||
* */
|
||||
highlightedField: { name: string; values: string[] };
|
||||
/**
|
||||
* Limit the search to include or exclude a specific value for the event.kind field
|
||||
* (alert, asset, enrichment, event, metric, state, pipeline_error, signal)
|
||||
*/
|
||||
type: EventType;
|
||||
}
|
||||
|
||||
export interface UseFetchFieldValuePairByEventTypeResult {
|
||||
/**
|
||||
* Returns true if data is being loaded
|
||||
*/
|
||||
loading: boolean;
|
||||
/**
|
||||
* Returns true if fetching data has errored out
|
||||
*/
|
||||
error: boolean;
|
||||
/**
|
||||
* Number of unique hosts found for the field/value pair
|
||||
*/
|
||||
count: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to retrieve all the unique hosts in the environment that have the field/value pair, using ReactQuery.
|
||||
*/
|
||||
export const useFetchFieldValuePairByEventType = ({
|
||||
highlightedField,
|
||||
type,
|
||||
}: UseFetchFieldValuePairByEventTypeParams): UseFetchFieldValuePairByEventTypeResult => {
|
||||
const {
|
||||
services: {
|
||||
data: { search: searchService },
|
||||
},
|
||||
} = useKibana();
|
||||
|
||||
const { from, to } = { from: DEFAULT_FROM, to: DEFAULT_TO };
|
||||
|
||||
const { name, values } = highlightedField;
|
||||
|
||||
const req: IEsSearchRequest = buildSearchRequest(name, values, from, to, type);
|
||||
|
||||
const { data, isLoading, isError } = useQuery(
|
||||
[QUERY_KEY, name, values, from, to, type],
|
||||
() => createFetchData<RawResponse>(searchService, req),
|
||||
{
|
||||
select: (res) => res.hits.total,
|
||||
keepPreviousData: true,
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
loading: isLoading,
|
||||
error: isError,
|
||||
count: data || 0,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Build the search request for the field/values pair, for a date range from/to.
|
||||
* We set the size to 0 as we only care about the total number of documents.
|
||||
* Passing signalEventKind as true will return only alerts (event.kind === "signal"), otherwise return all other documents (event.kind !== "signal")
|
||||
*/
|
||||
const buildSearchRequest = (
|
||||
field: string,
|
||||
values: string[],
|
||||
from: string,
|
||||
to: string,
|
||||
type: EventType
|
||||
): IEsSearchRequest => {
|
||||
const query = buildEsQuery(
|
||||
undefined,
|
||||
[],
|
||||
[
|
||||
{
|
||||
query: {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
match: {
|
||||
[field]: values[0],
|
||||
},
|
||||
},
|
||||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: from,
|
||||
lte: to,
|
||||
},
|
||||
},
|
||||
},
|
||||
...(type.include
|
||||
? [
|
||||
{
|
||||
match: {
|
||||
'event.kind': type.eventKind,
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
...(type.exclude
|
||||
? {
|
||||
must_not: [
|
||||
{
|
||||
match: {
|
||||
'event.kind': type.eventKind,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
},
|
||||
meta: {},
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
return {
|
||||
params: {
|
||||
body: {
|
||||
query,
|
||||
size: 1000,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
|
@ -1,94 +0,0 @@
|
|||
/*
|
||||
* 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 { useQuery } from '@tanstack/react-query';
|
||||
import type { RenderHookResult } from '@testing-library/react-hooks';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import type {
|
||||
UseFetchFieldValuePairWithAggregationParams,
|
||||
UseFetchFieldValuePairWithAggregationResult,
|
||||
} from './use_fetch_field_value_pair_with_aggregation';
|
||||
import { useFetchFieldValuePairWithAggregation } from './use_fetch_field_value_pair_with_aggregation';
|
||||
|
||||
jest.mock('@tanstack/react-query');
|
||||
jest.mock('../../../common/lib/kibana');
|
||||
|
||||
const highlightedField = {
|
||||
name: 'field',
|
||||
values: ['values'],
|
||||
};
|
||||
const aggregationField = 'aggregationField';
|
||||
|
||||
describe('useFetchFieldValuePairWithAggregation', () => {
|
||||
let hookResult: RenderHookResult<
|
||||
UseFetchFieldValuePairWithAggregationParams,
|
||||
UseFetchFieldValuePairWithAggregationResult
|
||||
>;
|
||||
(useKibana as jest.Mock).mockReturnValue({
|
||||
services: {
|
||||
data: { search: jest.fn() },
|
||||
},
|
||||
});
|
||||
|
||||
it('should return loading true while data is being fetched', () => {
|
||||
(useQuery as jest.Mock).mockReturnValue({
|
||||
isLoading: true,
|
||||
isError: false,
|
||||
data: 0,
|
||||
});
|
||||
|
||||
hookResult = renderHook(() =>
|
||||
useFetchFieldValuePairWithAggregation({
|
||||
highlightedField,
|
||||
aggregationField,
|
||||
})
|
||||
);
|
||||
|
||||
expect(hookResult.result.current.loading).toBeTruthy();
|
||||
expect(hookResult.result.current.error).toBeFalsy();
|
||||
expect(hookResult.result.current.count).toBe(0);
|
||||
});
|
||||
|
||||
it('should return error true when data fetching has errored out', () => {
|
||||
(useQuery as jest.Mock).mockReturnValue({
|
||||
isLoading: false,
|
||||
isError: true,
|
||||
data: 0,
|
||||
});
|
||||
|
||||
hookResult = renderHook(() =>
|
||||
useFetchFieldValuePairWithAggregation({
|
||||
highlightedField,
|
||||
aggregationField,
|
||||
})
|
||||
);
|
||||
|
||||
expect(hookResult.result.current.loading).toBeFalsy();
|
||||
expect(hookResult.result.current.error).toBeTruthy();
|
||||
expect(hookResult.result.current.count).toBe(0);
|
||||
});
|
||||
|
||||
it('should return count on success', () => {
|
||||
(useQuery as jest.Mock).mockReturnValue({
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
data: 1,
|
||||
});
|
||||
|
||||
hookResult = renderHook(() =>
|
||||
useFetchFieldValuePairWithAggregation({
|
||||
highlightedField,
|
||||
aggregationField,
|
||||
})
|
||||
);
|
||||
|
||||
expect(hookResult.result.current.loading).toBeFalsy();
|
||||
expect(hookResult.result.current.error).toBeFalsy();
|
||||
expect(hookResult.result.current.count).toBe(1);
|
||||
});
|
||||
});
|
|
@ -1,122 +0,0 @@
|
|||
/*
|
||||
* 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 { buildEsQuery } from '@kbn/es-query';
|
||||
import type { IEsSearchRequest } from '@kbn/data-plugin/public';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { buildAggregationSearchRequest } from '../utils/build_requests';
|
||||
import type { RawAggregatedDataResponse } from '../utils/fetch_data';
|
||||
import { AGG_KEY, createFetchData } from '../utils/fetch_data';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
|
||||
const QUERY_KEY = 'useFetchFieldValuePairWithAggregation';
|
||||
const DEFAULT_FROM = 'now-30d';
|
||||
const DEFAULT_TO = 'now';
|
||||
|
||||
export interface UseFetchFieldValuePairWithAggregationParams {
|
||||
/**
|
||||
* The highlighted field name and values
|
||||
* */
|
||||
highlightedField: { name: string; values: string[] };
|
||||
/**
|
||||
* Field to aggregate value by
|
||||
*/
|
||||
aggregationField: string;
|
||||
}
|
||||
|
||||
export interface UseFetchFieldValuePairWithAggregationResult {
|
||||
/**
|
||||
* Returns true if data is being loaded
|
||||
*/
|
||||
loading: boolean;
|
||||
/**
|
||||
* Returns true if fetching data has errored out
|
||||
*/
|
||||
error: boolean;
|
||||
/**
|
||||
* Number of unique hosts found for the field/value pair
|
||||
*/
|
||||
count: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to retrieve all the unique documents for the aggregationField in the environment that have the field/value pair, using ReactQuery.
|
||||
*
|
||||
* Foe example, passing 'host.name' via the aggregationField props will return the number of unique hosts in the environment that have the field/value pair.
|
||||
*/
|
||||
export const useFetchFieldValuePairWithAggregation = ({
|
||||
highlightedField,
|
||||
aggregationField,
|
||||
}: UseFetchFieldValuePairWithAggregationParams): UseFetchFieldValuePairWithAggregationResult => {
|
||||
const {
|
||||
services: {
|
||||
data: { search: searchService },
|
||||
},
|
||||
} = useKibana();
|
||||
|
||||
const { from, to } = { from: DEFAULT_FROM, to: DEFAULT_TO };
|
||||
const { name, values } = highlightedField;
|
||||
|
||||
const searchRequest = buildSearchRequest(name, values, from, to, aggregationField);
|
||||
|
||||
const { data, isLoading, isError } = useQuery(
|
||||
[QUERY_KEY, name, values, from, to, aggregationField],
|
||||
() => createFetchData<RawAggregatedDataResponse>(searchService, searchRequest),
|
||||
{
|
||||
select: (res) => res.aggregations[AGG_KEY].buckets.length,
|
||||
keepPreviousData: true,
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
loading: isLoading,
|
||||
error: isError,
|
||||
count: data || 0,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Build the search request for the field/values pair, for a date range from/to.
|
||||
* The request contains aggregation by aggregationField.
|
||||
*/
|
||||
const buildSearchRequest = (
|
||||
field: string,
|
||||
values: string[],
|
||||
from: string,
|
||||
to: string,
|
||||
aggregationField: string
|
||||
): IEsSearchRequest => {
|
||||
const query = buildEsQuery(
|
||||
undefined,
|
||||
[],
|
||||
[
|
||||
{
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
match: {
|
||||
[field]: values[0],
|
||||
},
|
||||
},
|
||||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: from,
|
||||
lte: to,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
meta: {},
|
||||
},
|
||||
]
|
||||
);
|
||||
return buildAggregationSearchRequest(aggregationField, AGG_KEY, query);
|
||||
};
|
|
@ -0,0 +1,203 @@
|
|||
/*
|
||||
* 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 { buildEsQuery } from '@kbn/es-query';
|
||||
import type { IEsSearchRequest } from '@kbn/data-plugin/public';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { createFetchData } from '../utils/fetch_data';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
|
||||
const QUERY_KEY = 'useFetchFieldValuePairWithAggregation';
|
||||
|
||||
export const FIELD_NAMES_AGG_KEY = 'fieldNames';
|
||||
export const EVENT_KIND_AGG_KEY = 'eventKind';
|
||||
export const HOST_NAME_AGG_KEY = 'hostName';
|
||||
export const USER_NAME_AGG_KEY = 'userName';
|
||||
export const HOSTS_AGG_KEY = 'hosts';
|
||||
export const USERS_AGG_KEY = 'users';
|
||||
|
||||
export interface AggregationValue {
|
||||
doc_count: number;
|
||||
key: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for a specific aggregation schema with nested aggregations, used in the prevalence components
|
||||
*/
|
||||
export interface RawAggregatedDataResponse {
|
||||
aggregations: {
|
||||
[FIELD_NAMES_AGG_KEY]: {
|
||||
buckets: {
|
||||
[key: string]: {
|
||||
eventKind: { buckets: AggregationValue[] };
|
||||
hostName: { value: number };
|
||||
userName: { value: number };
|
||||
};
|
||||
};
|
||||
};
|
||||
[HOSTS_AGG_KEY]: {
|
||||
value: number;
|
||||
};
|
||||
[USERS_AGG_KEY]: {
|
||||
value: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface UseFetchPrevalenceParams {
|
||||
/**
|
||||
* The highlighted field name and values, already formatted for the query
|
||||
* */
|
||||
highlightedFieldsFilters: Record<string, QueryDslQueryContainer>;
|
||||
/**
|
||||
* The from and to values for the query
|
||||
*/
|
||||
interval: { from: string; to: string };
|
||||
}
|
||||
|
||||
export interface UseFetchPrevalenceResult {
|
||||
/**
|
||||
* Returns true if data is being loaded
|
||||
*/
|
||||
loading: boolean;
|
||||
/**
|
||||
* Returns true if fetching data has errored out
|
||||
*/
|
||||
error: boolean;
|
||||
/**
|
||||
* Returns the prevalence raw aggregated data
|
||||
*/
|
||||
data: RawAggregatedDataResponse | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to fetch prevalence data for both the PrevalenceDetails and PrevalenceOverview components.
|
||||
* Here's how we fetch the data:
|
||||
* - the query filter is just limiting to the from/to datetime range
|
||||
* - we do 3 top level aggregations:
|
||||
* - one for each field/value pairs
|
||||
* - one for all the unique hosts in the environment
|
||||
* - one for all the unique users in the environment
|
||||
* For each field/value pair aggregated, we do 3 sub aggregations:
|
||||
* - one to retrieve the unique hosts which have the field/value pair
|
||||
* - one to retrieve the unique users which have the field/value pair
|
||||
* - one to retrieve how many documents are of the different type of event.kind
|
||||
* All of these values are then used to calculate the alert count, document count, host and user prevalence values.
|
||||
*/
|
||||
export const useFetchPrevalence = ({
|
||||
highlightedFieldsFilters,
|
||||
interval: { from, to },
|
||||
}: UseFetchPrevalenceParams): UseFetchPrevalenceResult => {
|
||||
const {
|
||||
services: {
|
||||
data: { search: searchService },
|
||||
},
|
||||
} = useKibana();
|
||||
|
||||
const searchRequest = buildSearchRequest(highlightedFieldsFilters, from, to);
|
||||
|
||||
const { data, isLoading, isError } = useQuery(
|
||||
[QUERY_KEY, highlightedFieldsFilters, from, to],
|
||||
() => createFetchData<RawAggregatedDataResponse>(searchService, searchRequest)
|
||||
);
|
||||
|
||||
return {
|
||||
loading: isLoading,
|
||||
error: isError,
|
||||
data,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Build the search request for the field/values pair, for a date range from/to.
|
||||
* The request contains aggregation by aggregationField.
|
||||
*/
|
||||
const buildSearchRequest = (
|
||||
highlightedFieldsFilters: Record<string, QueryDslQueryContainer>,
|
||||
from: string,
|
||||
to: string
|
||||
): IEsSearchRequest => {
|
||||
const query = buildEsQuery(
|
||||
undefined,
|
||||
[],
|
||||
[
|
||||
{
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: from,
|
||||
lte: to,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
meta: {},
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
return buildAggregationSearchRequest(query, highlightedFieldsFilters);
|
||||
};
|
||||
|
||||
const buildAggregationSearchRequest = (
|
||||
query: QueryDslQueryContainer,
|
||||
highlightedFieldsFilters: Record<string, QueryDslQueryContainer>
|
||||
): IEsSearchRequest => ({
|
||||
params: {
|
||||
body: {
|
||||
query,
|
||||
aggs: {
|
||||
// with this aggregation, we can in a single call retrieve all the values for each field/value pairs
|
||||
[FIELD_NAMES_AGG_KEY]: {
|
||||
filters: {
|
||||
filters: highlightedFieldsFilters,
|
||||
},
|
||||
aggs: {
|
||||
// this sub aggregation allows us to retrieve all the hosts which have the field/value pair
|
||||
[HOST_NAME_AGG_KEY]: {
|
||||
cardinality: {
|
||||
field: 'host.name',
|
||||
},
|
||||
},
|
||||
// this sub aggregation allows us to retrieve all the users which have the field/value pair
|
||||
[USER_NAME_AGG_KEY]: {
|
||||
cardinality: {
|
||||
field: 'user.name',
|
||||
},
|
||||
},
|
||||
// we use this sub aggregation to differentiate between alerts (event.kind === 'signal') and documents (event.kind !== 'signal')
|
||||
[EVENT_KIND_AGG_KEY]: {
|
||||
terms: {
|
||||
field: 'event.kind',
|
||||
size: 10, // there should be only 8 different value for the event.kind field
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// retrieve all the unique hosts in the environment
|
||||
[HOSTS_AGG_KEY]: {
|
||||
cardinality: {
|
||||
field: 'host.name',
|
||||
},
|
||||
},
|
||||
// retrieve all the unique users in the environment
|
||||
[USERS_AGG_KEY]: {
|
||||
cardinality: {
|
||||
field: 'user.name',
|
||||
},
|
||||
},
|
||||
},
|
||||
size: 0,
|
||||
},
|
||||
},
|
||||
});
|
|
@ -1,72 +0,0 @@
|
|||
/*
|
||||
* 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 { useQuery } from '@tanstack/react-query';
|
||||
import type { RenderHookResult } from '@testing-library/react-hooks';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import type {
|
||||
UseFetchUniqueByFieldParams,
|
||||
UseFetchUniqueByFieldValue,
|
||||
} from './use_fetch_unique_by_field';
|
||||
import { useFetchUniqueByField } from './use_fetch_unique_by_field';
|
||||
|
||||
jest.mock('@tanstack/react-query');
|
||||
jest.mock('../../../common/lib/kibana');
|
||||
|
||||
const field = 'host.name';
|
||||
|
||||
describe('useFetchUniqueByField', () => {
|
||||
let hookResult: RenderHookResult<UseFetchUniqueByFieldParams, UseFetchUniqueByFieldValue>;
|
||||
(useKibana as jest.Mock).mockReturnValue({
|
||||
services: {
|
||||
data: { search: jest.fn() },
|
||||
},
|
||||
});
|
||||
|
||||
it('should return loading true while data is being fetched', () => {
|
||||
(useQuery as jest.Mock).mockReturnValue({
|
||||
isLoading: true,
|
||||
isError: false,
|
||||
data: 0,
|
||||
});
|
||||
|
||||
hookResult = renderHook(() => useFetchUniqueByField({ field }));
|
||||
|
||||
expect(hookResult.result.current.loading).toBeTruthy();
|
||||
expect(hookResult.result.current.error).toBeFalsy();
|
||||
expect(hookResult.result.current.count).toBe(0);
|
||||
});
|
||||
|
||||
it('should return error true when data fetching has errored out', () => {
|
||||
(useQuery as jest.Mock).mockReturnValue({
|
||||
isLoading: false,
|
||||
isError: true,
|
||||
data: 0,
|
||||
});
|
||||
|
||||
hookResult = renderHook(() => useFetchUniqueByField({ field }));
|
||||
|
||||
expect(hookResult.result.current.loading).toBeFalsy();
|
||||
expect(hookResult.result.current.error).toBeTruthy();
|
||||
expect(hookResult.result.current.count).toBe(0);
|
||||
});
|
||||
|
||||
it('should return count on success', () => {
|
||||
(useQuery as jest.Mock).mockReturnValue({
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
data: 1,
|
||||
});
|
||||
|
||||
hookResult = renderHook(() => useFetchUniqueByField({ field }));
|
||||
|
||||
expect(hookResult.result.current.loading).toBeFalsy();
|
||||
expect(hookResult.result.current.error).toBeFalsy();
|
||||
expect(hookResult.result.current.count).toBe(1);
|
||||
});
|
||||
});
|
|
@ -1,67 +0,0 @@
|
|||
/*
|
||||
* 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 { useQuery } from '@tanstack/react-query';
|
||||
import type { IEsSearchRequest } from '@kbn/data-plugin/common';
|
||||
import type { RawAggregatedDataResponse } from '../utils/fetch_data';
|
||||
import { AGG_KEY, createFetchData } from '../utils/fetch_data';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import { buildAggregationSearchRequest } from '../utils/build_requests';
|
||||
|
||||
const QUERY_KEY = 'useFetchUniqueByField';
|
||||
|
||||
export interface UseFetchUniqueByFieldParams {
|
||||
/**
|
||||
* Field to aggregate by
|
||||
*/
|
||||
field: string;
|
||||
}
|
||||
|
||||
export interface UseFetchUniqueByFieldValue {
|
||||
/**
|
||||
* Returns true if data is being loaded
|
||||
*/
|
||||
loading: boolean;
|
||||
/**
|
||||
* Returns true if fetching data has errored out
|
||||
*/
|
||||
error: boolean;
|
||||
/**
|
||||
* Number of unique document by field found in the environment
|
||||
*/
|
||||
count: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to retrieve all unique documents by field in the environment, using ReactQuery.
|
||||
*
|
||||
* For example, passing 'host.name' via the field props will return the number of unique hosts in the environment.
|
||||
*/
|
||||
export const useFetchUniqueByField = ({
|
||||
field,
|
||||
}: UseFetchUniqueByFieldParams): UseFetchUniqueByFieldValue => {
|
||||
const {
|
||||
services: {
|
||||
data: { search: searchService },
|
||||
},
|
||||
} = useKibana();
|
||||
const searchRequest: IEsSearchRequest = buildAggregationSearchRequest(field, AGG_KEY);
|
||||
const { data, isLoading, isError } = useQuery(
|
||||
[QUERY_KEY, field],
|
||||
() => createFetchData<RawAggregatedDataResponse>(searchService, searchRequest),
|
||||
{
|
||||
select: (res) => res.aggregations[AGG_KEY].buckets.length,
|
||||
keepPreviousData: true,
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
loading: isLoading,
|
||||
error: isError,
|
||||
count: data || 0,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
|
||||
import { mockDataFormattedForFieldBrowser } from '../mocks/mock_context';
|
||||
import { useHighlightedFields } from './use_highlighted_fields';
|
||||
|
||||
const dataFormattedForFieldBrowser = mockDataFormattedForFieldBrowser;
|
||||
|
||||
describe('useHighlightedFields', () => {
|
||||
it('should return data', () => {
|
||||
const hookResult = renderHook(() => useHighlightedFields({ dataFormattedForFieldBrowser }));
|
||||
expect(hookResult.result.current).toEqual({
|
||||
'kibana.alert.rule.type': {
|
||||
values: ['query'],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
|
@ -26,19 +26,15 @@ export interface UseHighlightedFieldsParams {
|
|||
}
|
||||
|
||||
export interface UseHighlightedFieldsResult {
|
||||
/**
|
||||
* Highlighted field name (label or if null, falls back to id)
|
||||
*/
|
||||
field: string;
|
||||
description: {
|
||||
[fieldName: string]: {
|
||||
/**
|
||||
* Highlighted field name (overrideField or if null, falls back to id)
|
||||
* If the field has a custom override
|
||||
*/
|
||||
field: string;
|
||||
overrideField?: string;
|
||||
/**
|
||||
* Highlighted field value
|
||||
* Values for the field
|
||||
*/
|
||||
values: string[] | null | undefined;
|
||||
values: string[];
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -48,8 +44,8 @@ export interface UseHighlightedFieldsResult {
|
|||
export const useHighlightedFields = ({
|
||||
dataFormattedForFieldBrowser,
|
||||
investigationFields,
|
||||
}: UseHighlightedFieldsParams): UseHighlightedFieldsResult[] => {
|
||||
if (!dataFormattedForFieldBrowser) return [];
|
||||
}: UseHighlightedFieldsParams): UseHighlightedFieldsResult => {
|
||||
if (!dataFormattedForFieldBrowser) return {};
|
||||
|
||||
const eventCategories = getEventCategoriesFromData(dataFormattedForFieldBrowser);
|
||||
|
||||
|
@ -78,19 +74,26 @@ export const useHighlightedFields = ({
|
|||
highlightedFieldsOverride: investigationFields ?? [],
|
||||
});
|
||||
|
||||
return tableFields.reduce<UseHighlightedFieldsResult[]>((acc, field) => {
|
||||
return tableFields.reduce<UseHighlightedFieldsResult>((acc, field) => {
|
||||
const item = dataFormattedForFieldBrowser.find(
|
||||
(data) => data.field === field.id || (field.legacyId && data.field === field.legacyId)
|
||||
);
|
||||
if (!item || isEmpty(item.values)) {
|
||||
if (!item) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
// If we found the data by its legacy id we swap the ids to display the correct one
|
||||
// if there aren't any values we can skip this highlighted field
|
||||
const fieldValues = item.values;
|
||||
if (!fieldValues || isEmpty(fieldValues)) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
// if we found the data by its legacy id we swap the ids to display the correct one
|
||||
if (item.field === field.legacyId) {
|
||||
field.id = field.legacyId;
|
||||
}
|
||||
|
||||
// if the field is agent.id and the event is not an endpoint event we skip it
|
||||
if (
|
||||
field.id === 'agent.id' &&
|
||||
!isAlertFromEndpointEvent({ data: dataFormattedForFieldBrowser })
|
||||
|
@ -98,15 +101,12 @@ export const useHighlightedFields = ({
|
|||
return acc;
|
||||
}
|
||||
|
||||
return [
|
||||
return {
|
||||
...acc,
|
||||
{
|
||||
field: field.label ?? field.id,
|
||||
description: {
|
||||
field: field.overrideField ?? field.id,
|
||||
values: item.values,
|
||||
},
|
||||
[field.id]: {
|
||||
...(field.overrideField && { overrideField: field.overrideField }),
|
||||
values: fieldValues,
|
||||
},
|
||||
];
|
||||
}, []);
|
||||
};
|
||||
}, {});
|
||||
};
|
|
@ -0,0 +1,135 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
|
||||
import { usePrevalence } from './use_prevalence';
|
||||
import { mockDataFormattedForFieldBrowser } from '../mocks/mock_context';
|
||||
import { useHighlightedFields } from './use_highlighted_fields';
|
||||
import {
|
||||
FIELD_NAMES_AGG_KEY,
|
||||
HOSTS_AGG_KEY,
|
||||
useFetchPrevalence,
|
||||
USERS_AGG_KEY,
|
||||
} from './use_fetch_prevalence';
|
||||
|
||||
jest.mock('./use_highlighted_fields');
|
||||
jest.mock('./use_fetch_prevalence');
|
||||
|
||||
const interval = {
|
||||
from: 'now-30d',
|
||||
to: 'now',
|
||||
};
|
||||
const dataFormattedForFieldBrowser = mockDataFormattedForFieldBrowser;
|
||||
const investigationFields = ['host.name', 'user.name'];
|
||||
|
||||
describe('usePrevalence', () => {
|
||||
it('should return loading true', () => {
|
||||
(useHighlightedFields as jest.Mock).mockReturnValue({
|
||||
'host.name': {
|
||||
values: ['host-1'],
|
||||
},
|
||||
});
|
||||
(useFetchPrevalence as jest.Mock).mockReturnValue({
|
||||
loading: true,
|
||||
error: false,
|
||||
data: undefined,
|
||||
});
|
||||
|
||||
const hookResult = renderHook(() =>
|
||||
usePrevalence({ interval, dataFormattedForFieldBrowser, investigationFields })
|
||||
);
|
||||
|
||||
expect(hookResult.result.current.loading).toEqual(true);
|
||||
expect(hookResult.result.current.error).toEqual(false);
|
||||
expect(hookResult.result.current.data).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return error true', () => {
|
||||
(useHighlightedFields as jest.Mock).mockReturnValue({
|
||||
'host.name': {
|
||||
values: ['host-1'],
|
||||
},
|
||||
});
|
||||
(useFetchPrevalence as jest.Mock).mockReturnValue({
|
||||
loading: false,
|
||||
error: true,
|
||||
data: undefined,
|
||||
});
|
||||
|
||||
const hookResult = renderHook(() =>
|
||||
usePrevalence({ interval, dataFormattedForFieldBrowser, investigationFields })
|
||||
);
|
||||
|
||||
expect(hookResult.result.current.loading).toEqual(false);
|
||||
expect(hookResult.result.current.error).toEqual(true);
|
||||
expect(hookResult.result.current.data).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return data', () => {
|
||||
(useHighlightedFields as jest.Mock).mockReturnValue({
|
||||
'host.name': {
|
||||
values: ['host-1'],
|
||||
},
|
||||
});
|
||||
(useFetchPrevalence as jest.Mock).mockReturnValue({
|
||||
loading: false,
|
||||
error: false,
|
||||
data: {
|
||||
aggregations: {
|
||||
[FIELD_NAMES_AGG_KEY]: {
|
||||
buckets: {
|
||||
'host.name': {
|
||||
eventKind: {
|
||||
buckets: [
|
||||
{
|
||||
key: 'signal',
|
||||
doc_count: 1,
|
||||
},
|
||||
{
|
||||
key: 'event',
|
||||
doc_count: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
hostName: {
|
||||
value: 10,
|
||||
},
|
||||
userName: {
|
||||
value: 20,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
[HOSTS_AGG_KEY]: {
|
||||
value: 100,
|
||||
},
|
||||
[USERS_AGG_KEY]: {
|
||||
value: 200,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const hookResult = renderHook(() =>
|
||||
usePrevalence({ interval, dataFormattedForFieldBrowser, investigationFields })
|
||||
);
|
||||
|
||||
expect(hookResult.result.current.loading).toEqual(false);
|
||||
expect(hookResult.result.current.error).toEqual(false);
|
||||
expect(hookResult.result.current.data).toEqual([
|
||||
{
|
||||
field: 'host.name',
|
||||
value: 'host-1',
|
||||
alertCount: 1,
|
||||
docCount: 1,
|
||||
hostPrevalence: 0.1,
|
||||
userPrevalence: 0.1,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,148 @@
|
|||
/*
|
||||
* 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 { TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common';
|
||||
import { isArray } from 'lodash/fp';
|
||||
import { useMemo } from 'react';
|
||||
import { useHighlightedFields } from './use_highlighted_fields';
|
||||
import { convertHighlightedFieldsToPrevalenceFilters } from '../utils/highlighted_fields_helpers';
|
||||
import type { AggregationValue } from './use_fetch_prevalence';
|
||||
import {
|
||||
EVENT_KIND_AGG_KEY,
|
||||
FIELD_NAMES_AGG_KEY,
|
||||
HOST_NAME_AGG_KEY,
|
||||
HOSTS_AGG_KEY,
|
||||
useFetchPrevalence,
|
||||
USER_NAME_AGG_KEY,
|
||||
USERS_AGG_KEY,
|
||||
} from './use_fetch_prevalence';
|
||||
import { EventKind } from '../constants/event_kinds';
|
||||
|
||||
export interface PrevalenceData {
|
||||
field: string;
|
||||
value: string;
|
||||
alertCount: number;
|
||||
docCount: number;
|
||||
hostPrevalence: number;
|
||||
userPrevalence: number;
|
||||
}
|
||||
export interface UsePrevalenceParams {
|
||||
/**
|
||||
* The from and to values for the query
|
||||
*/
|
||||
interval: { from: string; to: string };
|
||||
/**
|
||||
* An array of field objects with category and value
|
||||
*/
|
||||
dataFormattedForFieldBrowser: TimelineEventsDetailsItem[] | null;
|
||||
/**
|
||||
* User defined fields to highlight (defined on the rule)
|
||||
*/
|
||||
investigationFields?: string[];
|
||||
}
|
||||
|
||||
export interface UsePrevalenceResult {
|
||||
/**
|
||||
* Returns true if data is being loaded
|
||||
*/
|
||||
loading: boolean;
|
||||
/**
|
||||
* Returns true if fetching data has errored out
|
||||
*/
|
||||
error: boolean;
|
||||
/**
|
||||
* Returns the prevalence data formatted for the EuiInMemoryTable component
|
||||
*/
|
||||
data: PrevalenceData[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to fetch the prevalence data, then prepares the data to be consumed by the EuiInMemoryTable component
|
||||
* in the PrevalenceDetails component
|
||||
*/
|
||||
export const usePrevalence = ({
|
||||
interval,
|
||||
dataFormattedForFieldBrowser,
|
||||
investigationFields,
|
||||
}: UsePrevalenceParams): UsePrevalenceResult => {
|
||||
const highlightedFields = useHighlightedFields({
|
||||
dataFormattedForFieldBrowser,
|
||||
investigationFields,
|
||||
});
|
||||
const highlightedFieldsFilters = useMemo(
|
||||
() => convertHighlightedFieldsToPrevalenceFilters(highlightedFields),
|
||||
[highlightedFields]
|
||||
);
|
||||
const { data, loading, error } = useFetchPrevalence({ highlightedFieldsFilters, interval });
|
||||
|
||||
const items: PrevalenceData[] = [];
|
||||
|
||||
if (data && data.aggregations) {
|
||||
// total number of unique hosts in the environment
|
||||
const uniqueHostsInEnvironment = data.aggregations[HOSTS_AGG_KEY].value;
|
||||
|
||||
// total number of unique users in the environment
|
||||
const uniqueUsersInEnvironment = data.aggregations[USERS_AGG_KEY].value;
|
||||
|
||||
const fieldNames = Object.keys(data.aggregations[FIELD_NAMES_AGG_KEY].buckets);
|
||||
|
||||
fieldNames.forEach((fieldName: string) => {
|
||||
const fieldValue = highlightedFields[fieldName].values;
|
||||
|
||||
// retrieves the number of signals for the current field/value pair
|
||||
const alertCount =
|
||||
data.aggregations[FIELD_NAMES_AGG_KEY].buckets[fieldName][EVENT_KIND_AGG_KEY].buckets.find(
|
||||
(aggregationValue: AggregationValue) => aggregationValue.key === EventKind.signal
|
||||
)?.doc_count || 0;
|
||||
|
||||
// calculate the number of documents (non-signal) for the current field/value pair
|
||||
let docCount = 0;
|
||||
data.aggregations[FIELD_NAMES_AGG_KEY].buckets[fieldName][EVENT_KIND_AGG_KEY].buckets.reduce(
|
||||
(acc, curr) => {
|
||||
if (curr.key !== EventKind.signal) {
|
||||
docCount += curr.doc_count;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
docCount
|
||||
);
|
||||
|
||||
// number of unique hosts in which the current field/value pair is present
|
||||
const uniqueHostsForCurrentFieldValuePair =
|
||||
data.aggregations[FIELD_NAMES_AGG_KEY].buckets[fieldName][HOST_NAME_AGG_KEY].value;
|
||||
|
||||
// number of unique users in which the current field/value pair is present
|
||||
const uniqueUsersForCurrentFieldValuePair =
|
||||
data.aggregations[FIELD_NAMES_AGG_KEY].buckets[fieldName][USER_NAME_AGG_KEY].value;
|
||||
|
||||
// calculate host prevalence
|
||||
const hostPrevalence = uniqueHostsInEnvironment
|
||||
? uniqueHostsForCurrentFieldValuePair / uniqueHostsInEnvironment
|
||||
: 0;
|
||||
|
||||
// calculate user prevalence
|
||||
const userPrevalence = uniqueUsersInEnvironment
|
||||
? uniqueUsersForCurrentFieldValuePair / uniqueUsersInEnvironment
|
||||
: 0;
|
||||
|
||||
items.push({
|
||||
field: fieldName,
|
||||
value: isArray(fieldValue) ? fieldValue[0] : fieldValue,
|
||||
alertCount,
|
||||
docCount,
|
||||
hostPrevalence,
|
||||
userPrevalence,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
loading,
|
||||
error,
|
||||
data: items,
|
||||
};
|
||||
};
|
|
@ -88,4 +88,18 @@ export const mockDataFormattedForFieldBrowser = [
|
|||
originalValue: ['process-entity_id'],
|
||||
isObjectArray: false,
|
||||
},
|
||||
{
|
||||
category: 'event',
|
||||
field: 'event.category',
|
||||
values: ['registry'],
|
||||
originalValue: ['registry'],
|
||||
isObjectArray: false,
|
||||
},
|
||||
{
|
||||
category: 'kibana',
|
||||
field: 'kibana.alert.rule.type',
|
||||
values: ['query'],
|
||||
originalValue: ['query'],
|
||||
isObjectArray: false,
|
||||
},
|
||||
];
|
||||
|
|
|
@ -8,28 +8,6 @@
|
|||
import type { IEsSearchRequest, IKibanaSearchResponse } from '@kbn/data-plugin/common';
|
||||
import type { ISearchStart } from '@kbn/data-plugin/public';
|
||||
|
||||
export const AGG_KEY = 'aggregation';
|
||||
|
||||
/**
|
||||
* Interface for aggregation responses
|
||||
*/
|
||||
export interface RawAggregatedDataResponse {
|
||||
aggregations: {
|
||||
[AGG_KEY]: {
|
||||
buckets: unknown[];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for non-aggregated responses
|
||||
*/
|
||||
export interface RawResponse {
|
||||
hits: {
|
||||
total: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reusable method that returns a promise wrapping the search functionality of Kibana search service
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* 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 {
|
||||
convertHighlightedFieldsToPrevalenceFilters,
|
||||
convertHighlightedFieldsToTableRow,
|
||||
} from './highlighted_fields_helpers';
|
||||
|
||||
describe('convertHighlightedFieldsToTableRow', () => {
|
||||
it('should convert highlighted fields to a table row', () => {
|
||||
const highlightedFields = {
|
||||
'host.name': {
|
||||
values: ['host-1'],
|
||||
},
|
||||
};
|
||||
expect(convertHighlightedFieldsToTableRow(highlightedFields)).toEqual([
|
||||
{
|
||||
field: 'host.name',
|
||||
description: {
|
||||
field: 'host.name',
|
||||
values: ['host-1'],
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should convert take override name over default name', () => {
|
||||
const highlightedFields = {
|
||||
'host.name': {
|
||||
overrideField: 'host.name-override',
|
||||
values: ['host-1'],
|
||||
},
|
||||
};
|
||||
expect(convertHighlightedFieldsToTableRow(highlightedFields)).toEqual([
|
||||
{
|
||||
field: 'host.name-override',
|
||||
description: {
|
||||
field: 'host.name-override',
|
||||
values: ['host-1'],
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('convertHighlightedFieldsToPrevalenceFilters', () => {
|
||||
it('should convert highlighted fields to prevalence filters', () => {
|
||||
const highlightedFields = {
|
||||
'host.name': {
|
||||
values: ['host-1'],
|
||||
},
|
||||
'user.name': {
|
||||
values: ['user-1'],
|
||||
},
|
||||
};
|
||||
expect(convertHighlightedFieldsToPrevalenceFilters(highlightedFields)).toEqual({
|
||||
'host.name': { match: { 'host.name': 'host-1' } },
|
||||
'user.name': { match: { 'user.name': 'user-1' } },
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* 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 { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
|
||||
import type { UseHighlightedFieldsResult } from '../hooks/use_highlighted_fields';
|
||||
import type { HighlightedFieldsTableRow } from '../../right/components/highlighted_fields';
|
||||
|
||||
/**
|
||||
* Converts the highlighted fields to a format that can be consumed by the HighlightedFields component
|
||||
* @param highlightedFields
|
||||
*/
|
||||
export const convertHighlightedFieldsToTableRow = (
|
||||
highlightedFields: UseHighlightedFieldsResult
|
||||
): HighlightedFieldsTableRow[] => {
|
||||
const fieldNames = Object.keys(highlightedFields);
|
||||
return fieldNames.map((fieldName) => {
|
||||
const values = highlightedFields[fieldName].values;
|
||||
const overrideFieldName = highlightedFields[fieldName].overrideField;
|
||||
const field = overrideFieldName ? overrideFieldName : fieldName;
|
||||
|
||||
return {
|
||||
field,
|
||||
description: {
|
||||
field,
|
||||
values,
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts the highlighted fields to a format that can be consumed by the prevalence query
|
||||
* @param highlightedFields
|
||||
*/
|
||||
export const convertHighlightedFieldsToPrevalenceFilters = (
|
||||
highlightedFields: UseHighlightedFieldsResult
|
||||
): Record<string, QueryDslQueryContainer> => {
|
||||
const fieldNames = Object.keys(highlightedFields);
|
||||
return fieldNames.reduce((acc, curr) => {
|
||||
const values = highlightedFields[curr].values;
|
||||
|
||||
return {
|
||||
...acc,
|
||||
[curr]: { match: { [curr]: Array.isArray(values) ? values[0] : values } },
|
||||
};
|
||||
}, []) as unknown as Record<string, QueryDslQueryContainer>;
|
||||
};
|
|
@ -33352,10 +33352,6 @@
|
|||
"xpack.securitySolution.flyout.prevalenceErrorMessage": "prévalence",
|
||||
"xpack.securitySolution.flyout.prevalenceTableAlertCountColumnTitle": "Nombre d'alertes",
|
||||
"xpack.securitySolution.flyout.prevalenceTableDocCountColumnTitle": "Compte du document",
|
||||
"xpack.securitySolution.flyout.prevalenceTableHostPrevalenceColumnTitle": "Prévalence de l’hôte",
|
||||
"xpack.securitySolution.flyout.prevalenceTableNameColumnTitle": "Nom",
|
||||
"xpack.securitySolution.flyout.prevalenceTableTypeColumnTitle": "Type",
|
||||
"xpack.securitySolution.flyout.prevalenceTableUserPrevalenceColumnTitle": "Prévalence de l’utilisateur",
|
||||
"xpack.securitySolution.flyout.response.empty": "Il n’y a pas d’actions de réponse définies pour cet évènement.",
|
||||
"xpack.securitySolution.flyout.response.title": "Réponses",
|
||||
"xpack.securitySolution.flyout.sessionViewErrorMessage": "vue de session",
|
||||
|
|
|
@ -33351,10 +33351,6 @@
|
|||
"xpack.securitySolution.flyout.prevalenceErrorMessage": "発生率",
|
||||
"xpack.securitySolution.flyout.prevalenceTableAlertCountColumnTitle": "アラート件数",
|
||||
"xpack.securitySolution.flyout.prevalenceTableDocCountColumnTitle": "ドキュメントカウント",
|
||||
"xpack.securitySolution.flyout.prevalenceTableHostPrevalenceColumnTitle": "ホスト発生率",
|
||||
"xpack.securitySolution.flyout.prevalenceTableNameColumnTitle": "名前",
|
||||
"xpack.securitySolution.flyout.prevalenceTableTypeColumnTitle": "型",
|
||||
"xpack.securitySolution.flyout.prevalenceTableUserPrevalenceColumnTitle": "ユーザー発生率",
|
||||
"xpack.securitySolution.flyout.response.empty": "このイベントに対する対応アクションは定義されていません。",
|
||||
"xpack.securitySolution.flyout.response.title": "対応",
|
||||
"xpack.securitySolution.flyout.sessionViewErrorMessage": "セッションビュー",
|
||||
|
|
|
@ -33347,10 +33347,6 @@
|
|||
"xpack.securitySolution.flyout.prevalenceErrorMessage": "普及率",
|
||||
"xpack.securitySolution.flyout.prevalenceTableAlertCountColumnTitle": "告警计数",
|
||||
"xpack.securitySolution.flyout.prevalenceTableDocCountColumnTitle": "文档计数",
|
||||
"xpack.securitySolution.flyout.prevalenceTableHostPrevalenceColumnTitle": "主机普及率",
|
||||
"xpack.securitySolution.flyout.prevalenceTableNameColumnTitle": "名称",
|
||||
"xpack.securitySolution.flyout.prevalenceTableTypeColumnTitle": "类型",
|
||||
"xpack.securitySolution.flyout.prevalenceTableUserPrevalenceColumnTitle": "用户普及率",
|
||||
"xpack.securitySolution.flyout.response.empty": "没有为此事件定义响应操作。",
|
||||
"xpack.securitySolution.flyout.response.title": "响应",
|
||||
"xpack.securitySolution.flyout.sessionViewErrorMessage": "会话视图",
|
||||
|
|
|
@ -23,6 +23,7 @@ import {
|
|||
DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_TABLE_HOST_PREVALENCE_CELL,
|
||||
DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_TABLE_USER_PREVALENCE_CELL,
|
||||
DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_TABLE,
|
||||
DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_DATE_PICKER,
|
||||
} from '../../../../screens/expandable_flyout/alert_details_left_panel_prevalence_tab';
|
||||
import { cleanKibana } from '../../../../tasks/common';
|
||||
import { login, visit } from '../../../../tasks/login';
|
||||
|
@ -53,6 +54,8 @@ describe('Alert details expandable flyout left panel prevalence', () => {
|
|||
.should('be.visible')
|
||||
.and('have.text', 'Prevalence');
|
||||
|
||||
cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_DATE_PICKER).should('be.visible');
|
||||
|
||||
cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_TABLE).should('be.visible');
|
||||
cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_TABLE_TYPE_CELL)
|
||||
.should('contain.text', 'host.name')
|
||||
|
|
|
@ -9,10 +9,11 @@ import {
|
|||
PREVALENCE_DETAILS_TABLE_ALERT_COUNT_CELL_TEST_ID,
|
||||
PREVALENCE_DETAILS_TABLE_DOC_COUNT_CELL_TEST_ID,
|
||||
PREVALENCE_DETAILS_TABLE_HOST_PREVALENCE_CELL_TEST_ID,
|
||||
PREVALENCE_DETAILS_TABLE_NAME_CELL_TEST_ID,
|
||||
PREVALENCE_DETAILS_TABLE_VALUE_CELL_TEST_ID,
|
||||
PREVALENCE_DETAILS_TABLE_TEST_ID,
|
||||
PREVALENCE_DETAILS_TABLE_TYPE_CELL_TEST_ID,
|
||||
PREVALENCE_DETAILS_TABLE_FIELD_CELL_TEST_ID,
|
||||
PREVALENCE_DETAILS_TABLE_USER_PREVALENCE_CELL_TEST_ID,
|
||||
PREVALENCE_DETAILS_DATE_PICKER_TEST_ID,
|
||||
} from '@kbn/security-solution-plugin/public/flyout/left/components/test_ids';
|
||||
import { INSIGHTS_TAB_PREVALENCE_BUTTON_TEST_ID } from '@kbn/security-solution-plugin/public/flyout/left/tabs/test_ids';
|
||||
import { getDataTestSubjectSelector } from '../../helpers/common';
|
||||
|
@ -20,13 +21,15 @@ import { getDataTestSubjectSelector } from '../../helpers/common';
|
|||
export const DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_BUTTON = getDataTestSubjectSelector(
|
||||
INSIGHTS_TAB_PREVALENCE_BUTTON_TEST_ID
|
||||
);
|
||||
export const DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_DATE_PICKER =
|
||||
getDataTestSubjectSelector(PREVALENCE_DETAILS_DATE_PICKER_TEST_ID);
|
||||
export const DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_TABLE = getDataTestSubjectSelector(
|
||||
PREVALENCE_DETAILS_TABLE_TEST_ID
|
||||
);
|
||||
export const DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_TABLE_TYPE_CELL =
|
||||
getDataTestSubjectSelector(PREVALENCE_DETAILS_TABLE_TYPE_CELL_TEST_ID);
|
||||
getDataTestSubjectSelector(PREVALENCE_DETAILS_TABLE_FIELD_CELL_TEST_ID);
|
||||
export const DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_TABLE_NAME_CELL =
|
||||
getDataTestSubjectSelector(PREVALENCE_DETAILS_TABLE_NAME_CELL_TEST_ID);
|
||||
getDataTestSubjectSelector(PREVALENCE_DETAILS_TABLE_VALUE_CELL_TEST_ID);
|
||||
export const DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_TABLE_ALERT_COUNT_CELL =
|
||||
getDataTestSubjectSelector(PREVALENCE_DETAILS_TABLE_ALERT_COUNT_CELL_TEST_ID);
|
||||
export const DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_TABLE_DOC_COUNT_CELL =
|
||||
|
|
|
@ -6,7 +6,10 @@
|
|||
*/
|
||||
|
||||
import { JSON_TAB_CONTENT_TEST_ID } from '@kbn/security-solution-plugin/public/flyout/right/tabs/test_ids';
|
||||
import { RIGHT_SECTION } from '@kbn/expandable-flyout/src/components/test_ids';
|
||||
import { getDataTestSubjectSelector } from '../../helpers/common';
|
||||
|
||||
export const DOCUMENT_DETAILS_FLYOUT_RIGHT_PANEL_CONTENT =
|
||||
getDataTestSubjectSelector(RIGHT_SECTION);
|
||||
export const DOCUMENT_DETAILS_FLYOUT_JSON_TAB_CONTENT =
|
||||
getDataTestSubjectSelector(JSON_TAB_CONTENT_TEST_ID);
|
||||
|
|
|
@ -5,11 +5,11 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { getClassSelector } from '../../helpers/common';
|
||||
import { DOCUMENT_DETAILS_FLYOUT_RIGHT_PANEL_CONTENT } from '../../screens/expandable_flyout/alert_details_right_panel_json_tab';
|
||||
|
||||
/**
|
||||
* Scroll to x-y positions within the right section of the document details expandable flyout
|
||||
* // TODO revisit this as it seems very fragile: the first element found is the timeline flyout, which isn't visible but still exist in the DOM
|
||||
*/
|
||||
export const scrollWithinDocumentDetailsExpandableFlyoutRightSection = (x: number, y: number) =>
|
||||
cy.get(getClassSelector('euiFlyout')).last().scrollTo(x, y);
|
||||
cy.get(DOCUMENT_DETAILS_FLYOUT_RIGHT_PANEL_CONTENT).last().scrollTo(x, y);
|
||||
|
|
|
@ -43,5 +43,6 @@
|
|||
"@kbn/fleet-plugin",
|
||||
"@kbn/cases-components",
|
||||
"@kbn/security-solution-plugin",
|
||||
"@kbn/expandable-flyout",
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue