mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
141253 further updates to alerts on detail pages (#142669)
* fix language on page as well as font size * allow component to take additional filters * add ability for AlertByStatus to accept additional filters * update tests for Alert by Status * remove unused import * changes from code review * update constant value from imported version * fix bad test :( Co-authored-by: Kristof-Pierre Cummings <kristofpierre.cummings@elastic.co>
This commit is contained in:
parent
98f019a3ac
commit
055f1c5705
12 changed files with 472 additions and 80 deletions
|
@ -11,9 +11,15 @@ import type { EuiBasicTableColumn } from '@elastic/eui';
|
|||
import { EuiBasicTable, EuiEmptyPrompt, EuiLink, EuiPanel, EuiToolTip } from '@elastic/eui';
|
||||
import { euiStyled } from '@kbn/kibana-react-plugin/common';
|
||||
|
||||
import type { ESBoolQuery } from '../../../../common/typed_json';
|
||||
import type { Status } from '../../../../common/detection_engine/schemas/common';
|
||||
import { SecurityPageName } from '../../../../common/constants';
|
||||
import type { Filter } from '../../../overview/components/detection_response/hooks/use_navigate_to_timeline';
|
||||
import { useNavigateToTimeline } from '../../../overview/components/detection_response/hooks/use_navigate_to_timeline';
|
||||
import {
|
||||
SIGNAL_RULE_NAME_FIELD_NAME,
|
||||
SIGNAL_STATUS_FIELD_NAME,
|
||||
} from '../../../timelines/components/timeline/body/renderers/constants';
|
||||
import { useQueryToggle } from '../../containers/query_toggle';
|
||||
import { FormattedCount } from '../formatted_number';
|
||||
import { HeaderSection } from '../header_section';
|
||||
|
@ -33,6 +39,7 @@ interface EntityFilter {
|
|||
}
|
||||
interface AlertCountByStatusProps {
|
||||
entityFilter: EntityFilter;
|
||||
additionalFilters?: ESBoolQuery[];
|
||||
signalIndexName: string | null;
|
||||
}
|
||||
|
||||
|
@ -44,7 +51,6 @@ type GetTableColumns = (
|
|||
openRuleInTimelineWithAdditionalFields: (ruleName: string) => void
|
||||
) => Array<EuiBasicTableColumn<AlertCountByRuleByStatusItem>>;
|
||||
|
||||
const KIBANA_RULE_ALERT_FIELD = 'kibana.alert.rule.name';
|
||||
const STATUSES = ['open', 'acknowledged', 'closed'] as const;
|
||||
const ALERT_COUNT_BY_RULE_BY_STATUS = 'alerts-by-status-by-rule';
|
||||
const LOCAL_STORAGE_KEY = 'alertCountByFieldNameWidgetSettings';
|
||||
|
@ -58,21 +64,13 @@ const StyledEuiPanel = euiStyled(EuiPanel)`
|
|||
`;
|
||||
|
||||
export const AlertCountByRuleByStatus = React.memo(
|
||||
({ entityFilter, signalIndexName }: AlertCountByStatusProps) => {
|
||||
({ entityFilter, signalIndexName, additionalFilters }: AlertCountByStatusProps) => {
|
||||
const { field, value } = entityFilter;
|
||||
|
||||
const queryId = `${ALERT_COUNT_BY_RULE_BY_STATUS}-by-${field}`;
|
||||
const { toggleStatus, setToggleStatus } = useQueryToggle(queryId);
|
||||
|
||||
const { openEntityInTimeline } = useNavigateToTimeline();
|
||||
|
||||
const columns = useMemo(
|
||||
() =>
|
||||
getTableColumns((ruleName: string) =>
|
||||
openEntityInTimeline([{ field: KIBANA_RULE_ALERT_FIELD, value: ruleName }, entityFilter])
|
||||
),
|
||||
[entityFilter, openEntityInTimeline]
|
||||
);
|
||||
const { openTimelineWithFilters } = useNavigateToTimeline();
|
||||
|
||||
const [selectedStatusesByField, setSelectedStatusesByField] = useLocalStorage<StatusSelection>({
|
||||
defaultValue: {
|
||||
|
@ -84,6 +82,24 @@ export const AlertCountByRuleByStatus = React.memo(
|
|||
},
|
||||
});
|
||||
|
||||
const columns = useMemo(() => {
|
||||
return getTableColumns((ruleName: string) => {
|
||||
const timelineFilters: Filter[][] = [];
|
||||
|
||||
for (const status of selectedStatusesByField[field]) {
|
||||
timelineFilters.push([
|
||||
entityFilter,
|
||||
{ field: SIGNAL_RULE_NAME_FIELD_NAME, value: ruleName },
|
||||
{
|
||||
field: SIGNAL_STATUS_FIELD_NAME,
|
||||
value: status,
|
||||
},
|
||||
]);
|
||||
}
|
||||
openTimelineWithFilters(timelineFilters);
|
||||
});
|
||||
}, [entityFilter, field, openTimelineWithFilters, selectedStatusesByField]);
|
||||
|
||||
const updateSelection = useCallback(
|
||||
(selection: Status[]) => {
|
||||
setSelectedStatusesByField({
|
||||
|
@ -95,6 +111,7 @@ export const AlertCountByRuleByStatus = React.memo(
|
|||
);
|
||||
|
||||
const { items, isLoading, updatedAt } = useAlertCountByRuleByStatus({
|
||||
additionalFilters,
|
||||
field,
|
||||
value,
|
||||
queryId,
|
||||
|
@ -110,7 +127,7 @@ export const AlertCountByRuleByStatus = React.memo(
|
|||
<HeaderSection
|
||||
id={queryId}
|
||||
title={i18n.ALERTS_BY_RULE}
|
||||
titleSize="s"
|
||||
titleSize="m"
|
||||
toggleStatus={toggleStatus}
|
||||
toggleQuery={setToggleStatus}
|
||||
subtitle={<LastUpdatedAt updatedAt={updatedAt} isUpdating={isLoading} />}
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import type { ESBoolQuery } from '../../../../common/typed_json';
|
||||
import type { Status } from '../../../../common/detection_engine/schemas/common';
|
||||
import type { GenericBuckets } from '../../../../common/search_strategy';
|
||||
import { ALERTS_QUERY_NAMES } from '../../../detections/containers/detection_engine/alerts/constants';
|
||||
|
@ -21,6 +22,7 @@ export interface AlertCountByRuleByStatusItem {
|
|||
}
|
||||
|
||||
export interface UseAlertCountByRuleByStatusProps {
|
||||
additionalFilters?: ESBoolQuery[];
|
||||
field: string;
|
||||
value: string;
|
||||
queryId: string;
|
||||
|
@ -37,6 +39,7 @@ export type UseAlertCountByRuleByStatus = (props: UseAlertCountByRuleByStatusPro
|
|||
const ALERTS_BY_RULE_AGG = 'alertsByRuleAggregation';
|
||||
|
||||
export const useAlertCountByRuleByStatus: UseAlertCountByRuleByStatus = ({
|
||||
additionalFilters,
|
||||
field,
|
||||
value,
|
||||
queryId,
|
||||
|
@ -58,6 +61,7 @@ export const useAlertCountByRuleByStatus: UseAlertCountByRuleByStatus = ({
|
|||
refetch: refetchQuery,
|
||||
} = useQueryAlerts({
|
||||
query: buildRuleAlertsByEntityQuery({
|
||||
additionalFilters,
|
||||
from,
|
||||
to,
|
||||
field,
|
||||
|
@ -72,6 +76,7 @@ export const useAlertCountByRuleByStatus: UseAlertCountByRuleByStatus = ({
|
|||
useEffect(() => {
|
||||
setAlertsQuery(
|
||||
buildRuleAlertsByEntityQuery({
|
||||
additionalFilters,
|
||||
from,
|
||||
to,
|
||||
field,
|
||||
|
@ -79,7 +84,7 @@ export const useAlertCountByRuleByStatus: UseAlertCountByRuleByStatus = ({
|
|||
statuses,
|
||||
})
|
||||
);
|
||||
}, [setAlertsQuery, from, to, field, value, statuses]);
|
||||
}, [setAlertsQuery, from, to, field, value, statuses, additionalFilters]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!data) {
|
||||
|
@ -112,12 +117,14 @@ export const useAlertCountByRuleByStatus: UseAlertCountByRuleByStatus = ({
|
|||
};
|
||||
|
||||
export const buildRuleAlertsByEntityQuery = ({
|
||||
additionalFilters = [],
|
||||
from,
|
||||
to,
|
||||
field,
|
||||
value,
|
||||
statuses,
|
||||
}: {
|
||||
additionalFilters?: ESBoolQuery[];
|
||||
from: string;
|
||||
to: string;
|
||||
statuses: string[];
|
||||
|
@ -128,6 +135,7 @@ export const buildRuleAlertsByEntityQuery = ({
|
|||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
...additionalFilters,
|
||||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
|
|
|
@ -17,6 +17,7 @@ import React, { useEffect, useCallback, useMemo } from 'react';
|
|||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import type { Filter } from '@kbn/es-query';
|
||||
import { buildEsQuery } from '@kbn/es-query';
|
||||
import { getEsQueryConfig } from '@kbn/data-plugin/common';
|
||||
|
||||
import { AlertsByStatus } from '../../../overview/components/detection_response/alerts_by_status';
|
||||
|
@ -40,7 +41,6 @@ import { SiemSearchBar } from '../../../common/components/search_bar';
|
|||
import { SecuritySolutionPageWrapper } from '../../../common/components/page_wrapper';
|
||||
import { useGlobalTime } from '../../../common/containers/use_global_time';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import { convertToBuildEsQuery } from '../../../common/lib/kuery';
|
||||
import { inputsSelectors } from '../../../common/store';
|
||||
import { setHostDetailsTablesActivePageToZero } from '../../store/actions';
|
||||
import { setAbsoluteRangeDatePicker } from '../../../common/store/inputs/actions';
|
||||
|
@ -88,12 +88,14 @@ const HostDetailsComponent: React.FC<HostDetailsProps> = ({ detailName, hostDeta
|
|||
const { signalIndexName } = useSignalIndex();
|
||||
|
||||
const capabilities = useMlCapabilities();
|
||||
const kibana = useKibana();
|
||||
const {
|
||||
services: { uiSettings },
|
||||
} = useKibana();
|
||||
|
||||
const hostDetailsPageFilters: Filter[] = useMemo(
|
||||
() => getHostDetailsPageFilters(detailName),
|
||||
[detailName]
|
||||
);
|
||||
const getFilters = () => [...hostDetailsPageFilters, ...filters];
|
||||
|
||||
const isEnterprisePlus = useLicense().isEnterprise();
|
||||
|
||||
|
@ -119,14 +121,31 @@ const HostDetailsComponent: React.FC<HostDetailsProps> = ({ detailName, hostDeta
|
|||
indexNames: selectedPatterns,
|
||||
skip: selectedPatterns.length === 0,
|
||||
});
|
||||
const [filterQuery, kqlError] = convertToBuildEsQuery({
|
||||
config: getEsQueryConfig(kibana.services.uiSettings),
|
||||
indexPattern,
|
||||
queries: [query],
|
||||
filters: getFilters(),
|
||||
});
|
||||
|
||||
useInvalidFilterQuery({ id: ID, filterQuery, kqlError, query, startDate: from, endDate: to });
|
||||
const [rawFilteredQuery, kqlError] = useMemo(() => {
|
||||
try {
|
||||
return [
|
||||
buildEsQuery(
|
||||
indexPattern,
|
||||
[query],
|
||||
[...hostDetailsPageFilters, ...filters],
|
||||
getEsQueryConfig(uiSettings)
|
||||
),
|
||||
];
|
||||
} catch (e) {
|
||||
return [undefined, e];
|
||||
}
|
||||
}, [filters, indexPattern, query, uiSettings, hostDetailsPageFilters]);
|
||||
|
||||
const stringifiedAdditionalFilters = JSON.stringify(rawFilteredQuery);
|
||||
useInvalidFilterQuery({
|
||||
id: ID,
|
||||
filterQuery: stringifiedAdditionalFilters,
|
||||
kqlError,
|
||||
query,
|
||||
startDate: from,
|
||||
endDate: to,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(setHostDetailsTablesActivePageToZero());
|
||||
|
@ -206,12 +225,14 @@ const HostDetailsComponent: React.FC<HostDetailsProps> = ({ detailName, hostDeta
|
|||
<AlertsByStatus
|
||||
signalIndexName={signalIndexName}
|
||||
entityFilter={entityFilter}
|
||||
additionalFilters={rawFilteredQuery ? [rawFilteredQuery] : []}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<AlertCountByRuleByStatus
|
||||
entityFilter={entityFilter}
|
||||
signalIndexName={signalIndexName}
|
||||
additionalFilters={rawFilteredQuery ? [rawFilteredQuery] : []}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
@ -241,7 +262,7 @@ const HostDetailsComponent: React.FC<HostDetailsProps> = ({ detailName, hostDeta
|
|||
detailName={detailName}
|
||||
type={type}
|
||||
setQuery={setQuery}
|
||||
filterQuery={filterQuery}
|
||||
filterQuery={stringifiedAdditionalFilters}
|
||||
hostDetailsPagePath={hostDetailsPagePath}
|
||||
indexPattern={indexPattern}
|
||||
/>
|
||||
|
|
|
@ -12,6 +12,7 @@ import { useParams } from 'react-router-dom';
|
|||
import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiSpacer } from '@elastic/eui';
|
||||
import { getEsQueryConfig } from '@kbn/data-plugin/common';
|
||||
|
||||
import { buildEsQuery } from '@kbn/es-query';
|
||||
import { AlertsByStatus } from '../../../overview/components/detection_response/alerts_by_status';
|
||||
import { useSignalIndex } from '../../../detections/containers/detection_engine/alerts/use_signal_index';
|
||||
import { InputsModelId } from '../../../common/store/inputs/constants';
|
||||
|
@ -33,7 +34,6 @@ import { SecuritySolutionPageWrapper } from '../../../common/components/page_wra
|
|||
import { useNetworkDetails, ID } from '../../containers/details';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import { decodeIpv6 } from '../../../common/lib/helpers';
|
||||
import { convertToBuildEsQuery } from '../../../common/lib/kuery';
|
||||
import { inputsSelectors } from '../../../common/store';
|
||||
import { setAbsoluteRangeDatePicker } from '../../../common/store/inputs/actions';
|
||||
import { setNetworkDetailsTablesActivePageToZero } from '../../store/actions';
|
||||
|
@ -103,23 +103,34 @@ const NetworkDetailsComponent: React.FC = () => {
|
|||
const { indicesExist, indexPattern, selectedPatterns } = useSourcererDataView();
|
||||
const ip = decodeIpv6(detailName);
|
||||
|
||||
const queryFilters = useMemo(
|
||||
() => [...getNetworkDetailsPageFilter(ip), ...filters],
|
||||
[filters, ip]
|
||||
);
|
||||
const [rawFilteredQuery, kqlError] = useMemo(() => {
|
||||
try {
|
||||
return [
|
||||
buildEsQuery(
|
||||
indexPattern,
|
||||
[query],
|
||||
[...getNetworkDetailsPageFilter(ip), ...filters],
|
||||
getEsQueryConfig(uiSettings)
|
||||
),
|
||||
];
|
||||
} catch (e) {
|
||||
return [undefined, e];
|
||||
}
|
||||
}, [filters, indexPattern, ip, query, uiSettings]);
|
||||
|
||||
const [filterQuery, kqlError] = convertToBuildEsQuery({
|
||||
config: getEsQueryConfig(uiSettings),
|
||||
indexPattern,
|
||||
queries: [query],
|
||||
filters: queryFilters,
|
||||
const stringifiedAdditionalFilters = JSON.stringify(rawFilteredQuery);
|
||||
useInvalidFilterQuery({
|
||||
id: ID,
|
||||
filterQuery: stringifiedAdditionalFilters,
|
||||
kqlError,
|
||||
query,
|
||||
startDate: from,
|
||||
endDate: to,
|
||||
});
|
||||
|
||||
useInvalidFilterQuery({ id: ID, filterQuery, kqlError, query, startDate: from, endDate: to });
|
||||
|
||||
const [loading, { id, inspect, networkDetails, refetch }] = useNetworkDetails({
|
||||
skip: isInitializing,
|
||||
filterQuery,
|
||||
filterQuery: stringifiedAdditionalFilters,
|
||||
indexNames: selectedPatterns,
|
||||
ip,
|
||||
});
|
||||
|
@ -141,8 +152,8 @@ const NetworkDetailsComponent: React.FC = () => {
|
|||
|
||||
// When the filterQuery comes back as undefined, it means an error has been thrown and the request should be skipped
|
||||
const shouldSkip = useMemo(
|
||||
() => isInitializing || filterQuery === undefined,
|
||||
[isInitializing, filterQuery]
|
||||
() => isInitializing || rawFilteredQuery === undefined,
|
||||
[isInitializing, rawFilteredQuery]
|
||||
);
|
||||
|
||||
const entityFilter = useMemo(
|
||||
|
@ -204,12 +215,17 @@ const NetworkDetailsComponent: React.FC = () => {
|
|||
<>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<AlertsByStatus signalIndexName={signalIndexName} entityFilter={entityFilter} />
|
||||
<AlertsByStatus
|
||||
signalIndexName={signalIndexName}
|
||||
entityFilter={entityFilter}
|
||||
additionalFilters={rawFilteredQuery ? [rawFilteredQuery] : []}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<AlertCountByRuleByStatus
|
||||
entityFilter={entityFilter}
|
||||
signalIndexName={signalIndexName}
|
||||
additionalFilters={rawFilteredQuery ? [rawFilteredQuery] : []}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
@ -225,7 +241,7 @@ const NetworkDetailsComponent: React.FC = () => {
|
|||
ip={ip}
|
||||
endDate={to}
|
||||
startDate={from}
|
||||
filterQuery={filterQuery}
|
||||
filterQuery={stringifiedAdditionalFilters}
|
||||
indexNames={selectedPatterns}
|
||||
skip={shouldSkip}
|
||||
setQuery={setQuery}
|
||||
|
|
|
@ -74,6 +74,28 @@ describe('AlertsByStatus', () => {
|
|||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('shows correct names when no entity filter provided', () => {
|
||||
const { getByText, getByTestId } = render(
|
||||
<TestProviders>
|
||||
<AlertsByStatus {...props} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(getByText('Alerts')).toBeInTheDocument();
|
||||
expect(getByTestId('view-details-button')).toHaveTextContent('View alerts');
|
||||
});
|
||||
|
||||
test('shows correct names when entity filter IS provided', () => {
|
||||
const { getByText, getByTestId } = render(
|
||||
<TestProviders>
|
||||
<AlertsByStatus {...props} entityFilter={{ field: 'name', value: 'val' }} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(getByText('Alerts by Severity')).toBeInTheDocument();
|
||||
expect(getByTestId('view-details-button')).toHaveTextContent('Investigate in Timeline');
|
||||
});
|
||||
|
||||
test('render HeaderSection', () => {
|
||||
const { container } = render(
|
||||
<TestProviders>
|
||||
|
|
|
@ -19,6 +19,8 @@ import React, { useCallback, useMemo } from 'react';
|
|||
import type { ShapeTreeNode } from '@elastic/charts';
|
||||
import type { Severity } from '@kbn/securitysolution-io-ts-alerting-types';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import type { ESBoolQuery } from '../../../../../common/typed_json';
|
||||
import type { FillColor } from '../../../../common/components/charts/donutchart';
|
||||
import { DonutChart } from '../../../../common/components/charts/donutchart';
|
||||
import { SecurityPageName } from '../../../../../common/constants';
|
||||
|
@ -31,7 +33,8 @@ import { useAlertsByStatus } from './use_alerts_by_status';
|
|||
import {
|
||||
ALERTS,
|
||||
ALERTS_TEXT,
|
||||
ALERTS_BY_STATUS_TEXT,
|
||||
ALERTS_BY_SEVERITY_TEXT,
|
||||
INVESTIGATE_IN_TIMELINE,
|
||||
STATUS_ACKNOWLEDGED,
|
||||
STATUS_CLOSED,
|
||||
STATUS_CRITICAL_LABEL,
|
||||
|
@ -63,6 +66,7 @@ const StyledLegendFlexItem = styled(EuiFlexItem)`
|
|||
interface AlertsByStatusProps {
|
||||
signalIndexName: string | null;
|
||||
entityFilter?: EntityFilter;
|
||||
additionalFilters?: ESBoolQuery[];
|
||||
}
|
||||
|
||||
const legendField = 'kibana.alert.severity';
|
||||
|
@ -79,9 +83,13 @@ const eventKindSignalFilter: EntityFilter = {
|
|||
value: 'signal',
|
||||
};
|
||||
|
||||
export const AlertsByStatus = ({ signalIndexName, entityFilter }: AlertsByStatusProps) => {
|
||||
export const AlertsByStatus = ({
|
||||
additionalFilters,
|
||||
signalIndexName,
|
||||
entityFilter,
|
||||
}: AlertsByStatusProps) => {
|
||||
const { toggleStatus, setToggleStatus } = useQueryToggle(DETECTION_RESPONSE_ALERTS_BY_STATUS_ID);
|
||||
const { openEntityInTimeline } = useNavigateToTimeline();
|
||||
const { openTimelineWithFilters } = useNavigateToTimeline();
|
||||
const { onClick: goToAlerts, href } = useGetSecuritySolutionLinkProps()({
|
||||
deepLinkId: SecurityPageName.alerts,
|
||||
});
|
||||
|
@ -92,13 +100,13 @@ export const AlertsByStatus = ({ signalIndexName, entityFilter }: AlertsByStatus
|
|||
|
||||
const detailsButtonOptions = useMemo(
|
||||
() => ({
|
||||
name: VIEW_ALERTS,
|
||||
name: entityFilter ? INVESTIGATE_IN_TIMELINE : VIEW_ALERTS,
|
||||
href: entityFilter ? undefined : href,
|
||||
onClick: entityFilter
|
||||
? () => openEntityInTimeline([entityFilter, eventKindSignalFilter])
|
||||
? () => openTimelineWithFilters([[entityFilter, eventKindSignalFilter]])
|
||||
: goToAlerts,
|
||||
}),
|
||||
[entityFilter, href, goToAlerts, openEntityInTimeline]
|
||||
[entityFilter, href, goToAlerts, openTimelineWithFilters]
|
||||
);
|
||||
|
||||
const {
|
||||
|
@ -106,6 +114,7 @@ export const AlertsByStatus = ({ signalIndexName, entityFilter }: AlertsByStatus
|
|||
isLoading: loading,
|
||||
updatedAt,
|
||||
} = useAlertsByStatus({
|
||||
additionalFilters,
|
||||
entityFilter,
|
||||
signalIndexName,
|
||||
skip: !toggleStatus,
|
||||
|
@ -146,8 +155,8 @@ export const AlertsByStatus = ({ signalIndexName, entityFilter }: AlertsByStatus
|
|||
)}
|
||||
<HeaderSection
|
||||
id={DETECTION_RESPONSE_ALERTS_BY_STATUS_ID}
|
||||
title={entityFilter ? ALERTS_BY_STATUS_TEXT : ALERTS_TEXT}
|
||||
titleSize="s"
|
||||
title={entityFilter ? ALERTS_BY_SEVERITY_TEXT : ALERTS_TEXT}
|
||||
titleSize="m"
|
||||
subtitle={<LastUpdatedAt isUpdating={loading} updatedAt={updatedAt} />}
|
||||
inspectMultiple
|
||||
toggleStatus={toggleStatus}
|
||||
|
|
|
@ -7,6 +7,8 @@
|
|||
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import type { Severity } from '@kbn/securitysolution-io-ts-alerting-types';
|
||||
|
||||
import type { ESBoolQuery } from '../../../../../common/typed_json';
|
||||
import { useGlobalTime } from '../../../../common/containers/use_global_time';
|
||||
import { useQueryAlerts } from '../../../../detections/containers/detection_engine/alerts/use_query';
|
||||
import { ALERTS_QUERY_NAMES } from '../../../../detections/containers/detection_engine/alerts/constants';
|
||||
|
@ -32,6 +34,7 @@ export interface EntityFilter {
|
|||
}
|
||||
|
||||
export const getAlertsByStatusQuery = ({
|
||||
additionalFilters = [],
|
||||
from,
|
||||
to,
|
||||
entityFilter,
|
||||
|
@ -39,11 +42,13 @@ export const getAlertsByStatusQuery = ({
|
|||
from: string;
|
||||
to: string;
|
||||
entityFilter?: EntityFilter;
|
||||
additionalFilters?: ESBoolQuery[];
|
||||
}) => ({
|
||||
size: 0,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
...additionalFilters,
|
||||
{ range: { '@timestamp': { gte: from, lte: to } } },
|
||||
...(entityFilter
|
||||
? [
|
||||
|
@ -104,6 +109,7 @@ export interface UseAlertsByStatusProps {
|
|||
signalIndexName: string | null;
|
||||
skip?: boolean;
|
||||
entityFilter?: EntityFilter;
|
||||
additionalFilters?: ESBoolQuery[];
|
||||
}
|
||||
|
||||
export type UseAlertsByStatus = (props: UseAlertsByStatusProps) => {
|
||||
|
@ -113,6 +119,7 @@ export type UseAlertsByStatus = (props: UseAlertsByStatusProps) => {
|
|||
};
|
||||
|
||||
export const useAlertsByStatus: UseAlertsByStatus = ({
|
||||
additionalFilters,
|
||||
entityFilter,
|
||||
queryId,
|
||||
signalIndexName,
|
||||
|
@ -134,6 +141,7 @@ export const useAlertsByStatus: UseAlertsByStatus = ({
|
|||
from,
|
||||
to,
|
||||
entityFilter,
|
||||
additionalFilters,
|
||||
}),
|
||||
indexName: signalIndexName,
|
||||
skip,
|
||||
|
@ -146,9 +154,10 @@ export const useAlertsByStatus: UseAlertsByStatus = ({
|
|||
from,
|
||||
to,
|
||||
entityFilter,
|
||||
additionalFilters,
|
||||
})
|
||||
);
|
||||
}, [setAlertsQuery, from, to, entityFilter]);
|
||||
}, [setAlertsQuery, from, to, entityFilter, additionalFilters]);
|
||||
|
||||
useEffect(() => {
|
||||
if (data == null) {
|
||||
|
|
|
@ -0,0 +1,158 @@
|
|||
/*
|
||||
* 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 { QueryOperator } from '../../../../../common/types/timeline';
|
||||
|
||||
export const hostFilter = {
|
||||
field: 'host.hostname',
|
||||
value: 'Host-u6ou715rzy',
|
||||
};
|
||||
|
||||
export const ANDFilterGroup1 = [
|
||||
hostFilter,
|
||||
{ field: 'kibana.alerts.workflow_status', value: 'open' },
|
||||
];
|
||||
|
||||
const ANDFilterGroup2 = [hostFilter, { field: 'kibana.alerts.workflow_status', value: 'closed' }];
|
||||
|
||||
const ANDFilterGroup3 = [
|
||||
hostFilter,
|
||||
{ field: 'kibana.alerts.workflow_status', value: 'acknowledged' },
|
||||
];
|
||||
|
||||
export const ORFilterGroup = [ANDFilterGroup1, ANDFilterGroup2, ANDFilterGroup3];
|
||||
|
||||
export const dataProviderWithOneFilter = [
|
||||
{
|
||||
and: [],
|
||||
enabled: true,
|
||||
id: '',
|
||||
name: 'host.hostname',
|
||||
excluded: false,
|
||||
kqlQuery: '',
|
||||
queryMatch: {
|
||||
field: 'host.hostname',
|
||||
value: 'Host-u6ou715rzy',
|
||||
operator: ':' as QueryOperator,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const dataProviderWithAndFilters = [
|
||||
{
|
||||
and: [
|
||||
{
|
||||
and: [],
|
||||
enabled: true,
|
||||
excluded: false,
|
||||
id: '',
|
||||
kqlQuery: '',
|
||||
name: 'kibana.alerts.workflow_status',
|
||||
queryMatch: {
|
||||
field: 'kibana.alerts.workflow_status',
|
||||
operator: ':' as QueryOperator,
|
||||
value: 'open',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
enabled: true,
|
||||
id: '',
|
||||
name: 'host.hostname',
|
||||
excluded: false,
|
||||
kqlQuery: '',
|
||||
queryMatch: {
|
||||
field: 'host.hostname',
|
||||
value: 'Host-u6ou715rzy',
|
||||
operator: ':' as QueryOperator,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const dataProviderWithOrFilters = [
|
||||
{
|
||||
and: [
|
||||
{
|
||||
and: [],
|
||||
enabled: true,
|
||||
id: '',
|
||||
name: 'kibana.alerts.workflow_status',
|
||||
excluded: false,
|
||||
kqlQuery: '',
|
||||
queryMatch: {
|
||||
field: 'kibana.alerts.workflow_status',
|
||||
value: 'open',
|
||||
operator: ':' as QueryOperator,
|
||||
},
|
||||
},
|
||||
],
|
||||
enabled: true,
|
||||
id: '',
|
||||
name: 'host.hostname',
|
||||
excluded: false,
|
||||
kqlQuery: '',
|
||||
queryMatch: {
|
||||
field: 'host.hostname',
|
||||
value: 'Host-u6ou715rzy',
|
||||
operator: ':' as QueryOperator,
|
||||
},
|
||||
},
|
||||
{
|
||||
and: [
|
||||
{
|
||||
and: [],
|
||||
enabled: true,
|
||||
id: '',
|
||||
name: 'kibana.alerts.workflow_status',
|
||||
excluded: false,
|
||||
kqlQuery: '',
|
||||
queryMatch: {
|
||||
field: 'kibana.alerts.workflow_status',
|
||||
value: 'closed',
|
||||
operator: ':' as QueryOperator,
|
||||
},
|
||||
},
|
||||
],
|
||||
enabled: true,
|
||||
id: '',
|
||||
name: 'host.hostname',
|
||||
excluded: false,
|
||||
kqlQuery: '',
|
||||
queryMatch: {
|
||||
field: 'host.hostname',
|
||||
value: 'Host-u6ou715rzy',
|
||||
operator: ':' as QueryOperator,
|
||||
},
|
||||
},
|
||||
{
|
||||
and: [
|
||||
{
|
||||
and: [],
|
||||
enabled: true,
|
||||
id: '',
|
||||
name: 'kibana.alerts.workflow_status',
|
||||
excluded: false,
|
||||
kqlQuery: '',
|
||||
queryMatch: {
|
||||
field: 'kibana.alerts.workflow_status',
|
||||
value: 'acknowledged',
|
||||
operator: ':' as QueryOperator,
|
||||
},
|
||||
},
|
||||
],
|
||||
enabled: true,
|
||||
id: '',
|
||||
name: 'host.hostname',
|
||||
excluded: false,
|
||||
kqlQuery: '',
|
||||
queryMatch: {
|
||||
field: 'host.hostname',
|
||||
value: 'Host-u6ou715rzy',
|
||||
operator: ':' as QueryOperator,
|
||||
},
|
||||
},
|
||||
];
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* 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 { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
|
||||
import { updateProviders } from '../../../../timelines/store/timeline/actions';
|
||||
import { useNavigateToTimeline } from './use_navigate_to_timeline';
|
||||
import * as mock from './mock_data';
|
||||
|
||||
jest.mock('../../../../timelines/components/timeline/properties/use_create_timeline', () => ({
|
||||
useCreateTimeline: () => jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../../../common/hooks/use_selector');
|
||||
(useDeepEqualSelector as jest.Mock).mockImplementation(() => ({
|
||||
defaultDataView: {
|
||||
id: 'someId',
|
||||
},
|
||||
}));
|
||||
|
||||
const mockDispatch = jest.fn();
|
||||
jest.mock('react-redux', () => {
|
||||
const original = jest.requireActual('react-redux');
|
||||
return {
|
||||
...original,
|
||||
useDispatch: () => mockDispatch,
|
||||
useSelector: () => jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const id = 'timeline-1';
|
||||
const renderUseNavigatgeToTimeline = () => renderHook(() => useNavigateToTimeline());
|
||||
|
||||
describe('useAlertCountByRuleByStatus', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should handle an empty array', () => {
|
||||
const { result } = renderUseNavigatgeToTimeline();
|
||||
const openTimelineWithFilters = result.current.openTimelineWithFilters;
|
||||
|
||||
openTimelineWithFilters([]);
|
||||
|
||||
expect(mockDispatch.mock.calls[0][0]).toEqual(
|
||||
updateProviders({
|
||||
id,
|
||||
providers: [],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle 1 filter passed', () => {
|
||||
const { result } = renderUseNavigatgeToTimeline();
|
||||
const openTimelineWithFilters = result.current.openTimelineWithFilters;
|
||||
|
||||
openTimelineWithFilters([[mock.hostFilter]]);
|
||||
|
||||
expect(mockDispatch.mock.calls[0][0]).toEqual(
|
||||
updateProviders({
|
||||
id,
|
||||
providers: mock.dataProviderWithOneFilter,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle many filter passed ( AND query )', () => {
|
||||
const { result } = renderUseNavigatgeToTimeline();
|
||||
const openTimelineWithFilters = result.current.openTimelineWithFilters;
|
||||
|
||||
openTimelineWithFilters([mock.ANDFilterGroup1]);
|
||||
|
||||
expect(mockDispatch.mock.calls[0][0]).toEqual(
|
||||
updateProviders({
|
||||
id,
|
||||
providers: mock.dataProviderWithAndFilters,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle many AND filter groups passed ( OR query with ANDS )', () => {
|
||||
const { result } = renderUseNavigatgeToTimeline();
|
||||
const openTimelineWithFilters = result.current.openTimelineWithFilters;
|
||||
|
||||
openTimelineWithFilters(mock.ORFilterGroup);
|
||||
|
||||
expect(mockDispatch.mock.calls[0][0]).toEqual(
|
||||
updateProviders({
|
||||
id,
|
||||
providers: mock.dataProviderWithOrFilters,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
|
@ -39,14 +39,14 @@ export const useNavigateToTimeline = () => {
|
|||
timelineType: TimelineType.default,
|
||||
});
|
||||
|
||||
const navigateToTimeline = (dataProvider: DataProvider) => {
|
||||
const navigateToTimeline = (dataProviders: DataProvider[]) => {
|
||||
// Reset the current timeline
|
||||
clearTimeline();
|
||||
// Update the timeline's providers to match the current prevalence field query
|
||||
dispatch(
|
||||
updateProviders({
|
||||
id: TimelineId.active,
|
||||
providers: [dataProvider],
|
||||
providers: dataProviders,
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -59,21 +59,32 @@ export const useNavigateToTimeline = () => {
|
|||
);
|
||||
};
|
||||
|
||||
const openEntityInTimeline = (entityFilters: [Filter, ...Filter[]]) => {
|
||||
const mainFilter = entityFilters.shift();
|
||||
/** *
|
||||
* Open a timeline with the given filters prepopulated.
|
||||
* It accepts an array of Filter[]s where each item represents a set of AND queries, and each top level comma represents an OR.
|
||||
*
|
||||
* [[filter1 & filter2] OR [filter3 & filter4]]
|
||||
*
|
||||
*/
|
||||
const openTimelineWithFilters = (filters: Array<[...Filter[]]>) => {
|
||||
const dataProviders = [];
|
||||
|
||||
if (mainFilter) {
|
||||
const dataProvider = getDataProvider(mainFilter.field, '', mainFilter.value);
|
||||
for (const orFilterGroup of filters) {
|
||||
const mainFilter = orFilterGroup[0];
|
||||
|
||||
for (const filter of entityFilters) {
|
||||
dataProvider.and.push(getDataProvider(filter.field, '', filter.value));
|
||||
if (mainFilter) {
|
||||
const dataProvider = getDataProvider(mainFilter.field, '', mainFilter.value);
|
||||
|
||||
for (const filter of orFilterGroup.slice(1)) {
|
||||
dataProvider.and.push(getDataProvider(filter.field, '', filter.value));
|
||||
}
|
||||
dataProviders.push(dataProvider);
|
||||
}
|
||||
|
||||
navigateToTimeline(dataProvider);
|
||||
}
|
||||
navigateToTimeline(dataProviders);
|
||||
};
|
||||
|
||||
// TODO: Replace the usage of functions with openEntityInTimeline
|
||||
// TODO: Replace the usage of functions with openTimelineWithFilters
|
||||
|
||||
const openHostInTimeline = ({ hostName, severity }: { hostName: string; severity?: string }) => {
|
||||
const dataProvider = getDataProvider('host.name', '', hostName);
|
||||
|
@ -82,7 +93,7 @@ export const useNavigateToTimeline = () => {
|
|||
dataProvider.and.push(getDataProvider('kibana.alert.severity', '', severity));
|
||||
}
|
||||
|
||||
navigateToTimeline(dataProvider);
|
||||
navigateToTimeline([dataProvider]);
|
||||
};
|
||||
|
||||
const openUserInTimeline = ({ userName, severity }: { userName: string; severity?: string }) => {
|
||||
|
@ -91,17 +102,17 @@ export const useNavigateToTimeline = () => {
|
|||
if (severity) {
|
||||
dataProvider.and.push(getDataProvider('kibana.alert.severity', '', severity));
|
||||
}
|
||||
navigateToTimeline(dataProvider);
|
||||
navigateToTimeline([dataProvider]);
|
||||
};
|
||||
|
||||
const openRuleInTimeline = (ruleName: string) => {
|
||||
const dataProvider = getDataProvider('kibana.alert.rule.name', '', ruleName);
|
||||
|
||||
navigateToTimeline(dataProvider);
|
||||
navigateToTimeline([dataProvider]);
|
||||
};
|
||||
|
||||
return {
|
||||
openEntityInTimeline,
|
||||
openTimelineWithFilters,
|
||||
openHostInTimeline,
|
||||
openRuleInTimeline,
|
||||
openUserInTimeline,
|
||||
|
|
|
@ -60,10 +60,10 @@ export const ALERTS = (totalAlerts: number) =>
|
|||
export const ALERTS_TEXT = i18n.translate('xpack.securitySolution.detectionResponse.alerts', {
|
||||
defaultMessage: 'Alerts',
|
||||
});
|
||||
export const ALERTS_BY_STATUS_TEXT = i18n.translate(
|
||||
'xpack.securitySolution.detectionResponse.alertsByStatus',
|
||||
export const ALERTS_BY_SEVERITY_TEXT = i18n.translate(
|
||||
'xpack.securitySolution.detectionResponse.alertsBySeverity',
|
||||
{
|
||||
defaultMessage: 'Alerts by Status',
|
||||
defaultMessage: 'Alerts by Severity',
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -255,3 +255,9 @@ export const USER_TOOLTIP = i18n.translate(
|
|||
defaultMessage: 'Maximum of 100 users. Please consult Alerts page for further information.',
|
||||
}
|
||||
);
|
||||
export const INVESTIGATE_IN_TIMELINE = i18n.translate(
|
||||
'xpack.securitySolution.detectionResponse.investigateInTimeline',
|
||||
{
|
||||
defaultMessage: 'Investigate in Timeline',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -18,6 +18,7 @@ import { useDispatch } from 'react-redux';
|
|||
|
||||
import { getEsQueryConfig } from '@kbn/data-plugin/common';
|
||||
import type { Filter } from '@kbn/es-query';
|
||||
import { buildEsQuery } from '@kbn/es-query';
|
||||
|
||||
import { AlertsByStatus } from '../../../overview/components/detection_response/alerts_by_status';
|
||||
import { useSignalIndex } from '../../../detections/containers/detection_engine/alerts/use_signal_index';
|
||||
|
@ -31,7 +32,6 @@ import { SiemSearchBar } from '../../../common/components/search_bar';
|
|||
import { SecuritySolutionPageWrapper } from '../../../common/components/page_wrapper';
|
||||
import { useGlobalTime } from '../../../common/containers/use_global_time';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import { convertToBuildEsQuery } from '../../../common/lib/kuery';
|
||||
import { inputsSelectors } from '../../../common/store';
|
||||
import { useAlertsPrivileges } from '../../../detections/containers/detection_engine/alerts/use_alerts_privileges';
|
||||
import { setUsersDetailsTablesActivePageToZero } from '../../store/actions';
|
||||
|
@ -93,25 +93,36 @@ const UsersDetailsComponent: React.FC<UsersDetailsProps> = ({
|
|||
const { to, from, deleteQuery, setQuery, isInitializing } = useGlobalTime();
|
||||
const { globalFullScreen } = useGlobalFullScreen();
|
||||
|
||||
const kibana = useKibana();
|
||||
const {
|
||||
services: { uiSettings },
|
||||
} = useKibana();
|
||||
|
||||
const usersDetailsPageFilters: Filter[] = useMemo(
|
||||
() => getUsersDetailsPageFilters(detailName),
|
||||
[detailName]
|
||||
);
|
||||
const getFilters = () => [...usersDetailsPageFilters, ...filters];
|
||||
|
||||
const { indicesExist, indexPattern, selectedPatterns } = useSourcererDataView();
|
||||
|
||||
const [filterQuery, kqlError] = convertToBuildEsQuery({
|
||||
config: getEsQueryConfig(kibana.services.uiSettings),
|
||||
indexPattern,
|
||||
queries: [query],
|
||||
filters: getFilters(),
|
||||
});
|
||||
const [rawFilteredQuery, kqlError] = useMemo(() => {
|
||||
try {
|
||||
return [
|
||||
buildEsQuery(
|
||||
indexPattern,
|
||||
[query],
|
||||
[...usersDetailsPageFilters, ...filters],
|
||||
getEsQueryConfig(uiSettings)
|
||||
),
|
||||
];
|
||||
} catch (e) {
|
||||
return [undefined, e];
|
||||
}
|
||||
}, [filters, indexPattern, query, uiSettings, usersDetailsPageFilters]);
|
||||
|
||||
const stringifiedAdditionalFilters = JSON.stringify(rawFilteredQuery);
|
||||
useInvalidFilterQuery({
|
||||
id: QUERY_ID,
|
||||
filterQuery,
|
||||
filterQuery: stringifiedAdditionalFilters,
|
||||
kqlError,
|
||||
query,
|
||||
startDate: from,
|
||||
|
@ -207,12 +218,17 @@ const UsersDetailsComponent: React.FC<UsersDetailsProps> = ({
|
|||
<>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<AlertsByStatus signalIndexName={signalIndexName} entityFilter={entityFilter} />
|
||||
<AlertsByStatus
|
||||
signalIndexName={signalIndexName}
|
||||
entityFilter={entityFilter}
|
||||
additionalFilters={rawFilteredQuery ? [rawFilteredQuery] : []}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<AlertCountByRuleByStatus
|
||||
entityFilter={entityFilter}
|
||||
signalIndexName={signalIndexName}
|
||||
additionalFilters={rawFilteredQuery ? [rawFilteredQuery] : []}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
@ -231,7 +247,7 @@ const UsersDetailsComponent: React.FC<UsersDetailsProps> = ({
|
|||
<UsersDetailsTabs
|
||||
deleteQuery={deleteQuery}
|
||||
detailName={detailName}
|
||||
filterQuery={filterQuery}
|
||||
filterQuery={stringifiedAdditionalFilters}
|
||||
from={from}
|
||||
indexNames={selectedPatterns}
|
||||
indexPattern={indexPattern}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue