[8.8] [Security Solution] Add Search Bar to Security D&R and EA Dashboards (#156832) (#157115)

# Backport

This will backport the following commits from `main` to `8.8`:
- [[Security Solution] Add Search Bar to Security D&R and EA Dashboards
(#156832)](https://github.com/elastic/kibana/pull/156832)

<!--- Backport version: 8.9.7 -->

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

<!--BACKPORT [{"author":{"name":"Pablo
Machado","email":"pablo.nevesmachado@elastic.co"},"sourceCommit":{"committedDate":"2023-05-08T23:58:14Z","message":"[Security
Solution] Add Search Bar to Security D&R and EA Dashboards
(#156832)\n\nMore details on the
issue:\r\nhttps://github.com/elastic/security-team/issues/6504\r\n##
TODO\r\n\r\n- [x] Unit tests\r\n- [ ] Cypress tests (follow-up
PR)\r\n\r\n\r\n\r\n## Summary\r\n\r\n* Add global search bar and filter
to EA and D&R pages.\r\n* Create `useGlobalFilterQuery` hook to simplify
adding global search\r\nbar filters to a page\r\n* Filter alert column
in risk table by time range
\r\n\r\n\r\n![May-05-2023\r\n15-12-34](236467191-df8cc05a-3c0c-4f37-929f-4d7723e23055.gif)\r\n\r\n<img
width=\"1402\" alt=\"Screenshot 2023-05-08 at 13 27
54\"\r\nsrc=\"https://user-images.githubusercontent.com/1490444/236812677-e6021d99-4be1-44d7-8449-26f9330d8b78.png\">\r\n\r\n###
Tooltips explaining that some pages are not affected by the
KQL\r\nsearch bar (Last minute addition)\r\n\r\n<img width=\"747\"
alt=\"Screenshot 2023-05-08 at 17 57
32\"\r\nsrc=\"https://user-images.githubusercontent.com/1490444/236871990-3ebd60fa-ea45-4f98-a8d9-5813ac2b10de.png\">\r\n<img
width=\"1512\" alt=\"Screenshot 2023-05-08 at 17 57
37\"\r\nsrc=\"https://user-images.githubusercontent.com/1490444/236871998-94969be6-b194-4d19-b83e-12f9b96eda1b.png\">\r\n<img
width=\"1512\" alt=\"Screenshot 2023-05-08 at 17 57
51\"\r\nsrc=\"https://user-images.githubusercontent.com/1490444/236872002-5255f799-f30b-44f1-bd90-8f19037b6915.png\">\r\n\r\n\r\n###
Glossary\r\n* **EA:** Entity Analytics\r\n* **D&R:** Detection &
Response\r\n\r\n### Checklist\r\n\r\n- [x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios","sha":"7fd9ca64b0fe99122584fa134e89c1abab9df613","branchLabelMapping":{"^v8.9.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:fix","Team:Threat
Hunting","Team: SecuritySolution","Team:Threat
Hunting:Explore","ci:cloud-deploy","v8.8.0","v8.9.0"],"number":156832,"url":"https://github.com/elastic/kibana/pull/156832","mergeCommit":{"message":"[Security
Solution] Add Search Bar to Security D&R and EA Dashboards
(#156832)\n\nMore details on the
issue:\r\nhttps://github.com/elastic/security-team/issues/6504\r\n##
TODO\r\n\r\n- [x] Unit tests\r\n- [ ] Cypress tests (follow-up
PR)\r\n\r\n\r\n\r\n## Summary\r\n\r\n* Add global search bar and filter
to EA and D&R pages.\r\n* Create `useGlobalFilterQuery` hook to simplify
adding global search\r\nbar filters to a page\r\n* Filter alert column
in risk table by time range
\r\n\r\n\r\n![May-05-2023\r\n15-12-34](236467191-df8cc05a-3c0c-4f37-929f-4d7723e23055.gif)\r\n\r\n<img
width=\"1402\" alt=\"Screenshot 2023-05-08 at 13 27
54\"\r\nsrc=\"https://user-images.githubusercontent.com/1490444/236812677-e6021d99-4be1-44d7-8449-26f9330d8b78.png\">\r\n\r\n###
Tooltips explaining that some pages are not affected by the
KQL\r\nsearch bar (Last minute addition)\r\n\r\n<img width=\"747\"
alt=\"Screenshot 2023-05-08 at 17 57
32\"\r\nsrc=\"https://user-images.githubusercontent.com/1490444/236871990-3ebd60fa-ea45-4f98-a8d9-5813ac2b10de.png\">\r\n<img
width=\"1512\" alt=\"Screenshot 2023-05-08 at 17 57
37\"\r\nsrc=\"https://user-images.githubusercontent.com/1490444/236871998-94969be6-b194-4d19-b83e-12f9b96eda1b.png\">\r\n<img
width=\"1512\" alt=\"Screenshot 2023-05-08 at 17 57
51\"\r\nsrc=\"https://user-images.githubusercontent.com/1490444/236872002-5255f799-f30b-44f1-bd90-8f19037b6915.png\">\r\n\r\n\r\n###
Glossary\r\n* **EA:** Entity Analytics\r\n* **D&R:** Detection &
Response\r\n\r\n### Checklist\r\n\r\n- [x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios","sha":"7fd9ca64b0fe99122584fa134e89c1abab9df613"}},"sourceBranch":"main","suggestedTargetBranches":["8.8"],"targetPullRequestStates":[{"branch":"8.8","label":"v8.8.0","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v8.9.0","labelRegex":"^v8.9.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/156832","number":156832,"mergeCommit":{"message":"[Security
Solution] Add Search Bar to Security D&R and EA Dashboards
(#156832)\n\nMore details on the
issue:\r\nhttps://github.com/elastic/security-team/issues/6504\r\n##
TODO\r\n\r\n- [x] Unit tests\r\n- [ ] Cypress tests (follow-up
PR)\r\n\r\n\r\n\r\n## Summary\r\n\r\n* Add global search bar and filter
to EA and D&R pages.\r\n* Create `useGlobalFilterQuery` hook to simplify
adding global search\r\nbar filters to a page\r\n* Filter alert column
in risk table by time range
\r\n\r\n\r\n![May-05-2023\r\n15-12-34](236467191-df8cc05a-3c0c-4f37-929f-4d7723e23055.gif)\r\n\r\n<img
width=\"1402\" alt=\"Screenshot 2023-05-08 at 13 27
54\"\r\nsrc=\"https://user-images.githubusercontent.com/1490444/236812677-e6021d99-4be1-44d7-8449-26f9330d8b78.png\">\r\n\r\n###
Tooltips explaining that some pages are not affected by the
KQL\r\nsearch bar (Last minute addition)\r\n\r\n<img width=\"747\"
alt=\"Screenshot 2023-05-08 at 17 57
32\"\r\nsrc=\"https://user-images.githubusercontent.com/1490444/236871990-3ebd60fa-ea45-4f98-a8d9-5813ac2b10de.png\">\r\n<img
width=\"1512\" alt=\"Screenshot 2023-05-08 at 17 57
37\"\r\nsrc=\"https://user-images.githubusercontent.com/1490444/236871998-94969be6-b194-4d19-b83e-12f9b96eda1b.png\">\r\n<img
width=\"1512\" alt=\"Screenshot 2023-05-08 at 17 57
51\"\r\nsrc=\"https://user-images.githubusercontent.com/1490444/236872002-5255f799-f30b-44f1-bd90-8f19037b6915.png\">\r\n\r\n\r\n###
Glossary\r\n* **EA:** Entity Analytics\r\n* **D&R:** Detection &
Response\r\n\r\n### Checklist\r\n\r\n- [x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios","sha":"7fd9ca64b0fe99122584fa134e89c1abab9df613"}}]}]
BACKPORT-->

Co-authored-by: Pablo Machado <pablo.nevesmachado@elastic.co>
This commit is contained in:
Kibana Machine 2023-05-09 15:16:10 -04:00 committed by GitHub
parent d10ba61d9f
commit bea88026b7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 512 additions and 97 deletions

View file

@ -15,6 +15,7 @@ export interface RiskScoreRequestOptions extends IEsSearchRequest {
defaultIndex: string[];
riskScoreEntity: RiskScoreEntity;
timerange?: TimerangeInput;
alertsTimerange?: TimerangeInput;
includeAlertsCount?: boolean;
onlyLatest?: boolean;
pagination?: {

View file

@ -0,0 +1,141 @@
/*
* 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 { TestProviders } from '../mock';
import { useGlobalFilterQuery } from './use_global_filter_query';
import type { Filter, Query } from '@kbn/es-query';
const DEFAULT_QUERY: Query = { query: '', language: 'kuery' };
const mockGlobalFiltersQuerySelector = jest.fn();
const mockGlobalQuerySelector = jest.fn();
const mockUseInvalidFilterQuery = jest.fn();
jest.mock('../store', () => {
const original = jest.requireActual('../store');
return {
...original,
inputsSelectors: {
...original.inputsSelectors,
globalFiltersQuerySelector: () => mockGlobalFiltersQuerySelector,
globalQuerySelector: () => mockGlobalQuerySelector,
},
};
});
jest.mock('./use_invalid_filter_query', () => ({
useInvalidFilterQuery: (...args: unknown[]) => mockUseInvalidFilterQuery(...args),
}));
describe('useGlobalFilterQuery', () => {
beforeEach(() => {
mockGlobalFiltersQuerySelector.mockReturnValue([]);
mockGlobalQuerySelector.mockReturnValue(DEFAULT_QUERY);
});
it('returns filterQuery', () => {
const { result } = renderHook(() => useGlobalFilterQuery(), { wrapper: TestProviders });
expect(result.current.filterQuery).toEqual({
bool: { must: [], filter: [], should: [], must_not: [] },
});
});
it('filters by KQL search', () => {
mockGlobalQuerySelector.mockReturnValue({ query: 'test: 123', language: 'kuery' });
const { result } = renderHook(() => useGlobalFilterQuery(), { wrapper: TestProviders });
expect(result.current.filterQuery).toEqual({
bool: {
must: [],
filter: [
{
bool: {
minimum_should_match: 1,
should: [
{
match: {
test: '123',
},
},
],
},
},
],
should: [],
must_not: [],
},
});
});
it('filters by global filters', () => {
const query = {
match_phrase: {
test: '1234',
},
};
const globalFilter: Filter[] = [
{
meta: {
disabled: false,
},
query,
},
];
mockGlobalFiltersQuerySelector.mockReturnValue(globalFilter);
const { result } = renderHook(() => useGlobalFilterQuery(), { wrapper: TestProviders });
expect(result.current.filterQuery).toEqual({
bool: {
must: [],
filter: [query],
should: [],
must_not: [],
},
});
});
it('filters by extra filter', () => {
const query = {
match_phrase: {
test: '12345',
},
};
const extraFilter: Filter = {
meta: {
disabled: false,
},
query,
};
const { result } = renderHook(() => useGlobalFilterQuery({ extraFilter }), {
wrapper: TestProviders,
});
expect(result.current.filterQuery).toEqual({
bool: {
must: [],
filter: [query],
should: [],
must_not: [],
},
});
});
it('displays the KQL error when query is invalid', () => {
mockGlobalQuerySelector.mockReturnValue({ query: ': :', language: 'kuery' });
const { result } = renderHook(() => useGlobalFilterQuery(), { wrapper: TestProviders });
expect(result.current.filterQuery).toEqual(undefined);
expect(mockUseInvalidFilterQuery).toHaveBeenLastCalledWith(
expect.objectContaining({
kqlError: expect.anything(),
})
);
});
});

View file

@ -0,0 +1,78 @@
/*
* 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 { useMemo } from 'react';
import { getEsQueryConfig } from '@kbn/data-plugin/common';
import type { DataViewBase, EsQueryConfig, Filter, Query } from '@kbn/es-query';
import { buildEsQuery } from '@kbn/es-query';
import { useGlobalTime } from '../containers/use_global_time';
import { useKibana } from '../lib/kibana';
import { inputsSelectors } from '../store';
import { useDeepEqualSelector } from './use_selector';
import { useInvalidFilterQuery } from './use_invalid_filter_query';
import type { ESBoolQuery } from '../../../common/typed_json';
interface GlobalFilterQueryProps {
extraFilter?: Filter;
dataView?: DataViewBase;
}
/**
* It builds a global filterQuery from KQL search bar and global filters.
* It also validates the query and shows a warning if it's invalid.
*/
export const useGlobalFilterQuery = ({ extraFilter, dataView }: GlobalFilterQueryProps = {}) => {
const { from, to } = useGlobalTime();
const getGlobalFiltersQuerySelector = useMemo(
() => inputsSelectors.globalFiltersQuerySelector(),
[]
);
const getGlobalQuerySelector = useMemo(() => inputsSelectors.globalQuerySelector(), []);
const query = useDeepEqualSelector(getGlobalQuerySelector);
const globalFilters = useDeepEqualSelector(getGlobalFiltersQuerySelector);
const { uiSettings } = useKibana().services;
const filters = useMemo(() => {
const enabledFilters = globalFilters.filter((f) => f.meta.disabled === false);
return extraFilter ? [...enabledFilters, extraFilter] : enabledFilters;
}, [extraFilter, globalFilters]);
const { filterQuery, kqlError } = useMemo(
() => buildQueryOrError(query, filters, getEsQueryConfig(uiSettings), dataView),
[dataView, query, filters, uiSettings]
);
const filterQueryStringified = useMemo(
() => (filterQuery ? JSON.stringify(filterQuery) : undefined),
[filterQuery]
);
useInvalidFilterQuery({
id: 'GlobalFilterQuery', // It prevents displaying multiple times the same error popup
filterQuery: filterQueryStringified,
kqlError,
query,
startDate: from,
endDate: to,
});
return { filterQuery };
};
const buildQueryOrError = (
query: Query,
filters: Filter[],
config: EsQueryConfig,
dataView?: DataViewBase
): { filterQuery?: ESBoolQuery; kqlError?: Error } => {
try {
return { filterQuery: buildEsQuery(dataView, [query], filters, config) };
} catch (kqlError) {
return { kqlError };
}
};

View file

@ -30,14 +30,14 @@ export const USER_WARNING_TITLE = i18n.translate(
export const HOST_WARNING_BODY = i18n.translate(
'xpack.securitySolution.riskScore.hostsDashboardWarningPanelBody',
{
defaultMessage: `We haven't detected any host risk score data from the hosts in your environment. The data might need an hour to be generated after enabling the module.`,
defaultMessage: `We havent found any host risk score data. Check if you have any global filters in the global KQL search bar. If you have just enabled the host risk module, the risk engine might need an hour to generate host risk score data and display in this panel.`,
}
);
export const USER_WARNING_BODY = i18n.translate(
'xpack.securitySolution.riskScore.usersDashboardWarningPanelBody',
{
defaultMessage: `We haven't detected any user risk score data from the users in your environment. The data might need an hour to be generated after enabling the module.`,
defaultMessage: `We havent found any user risk score data. Check if you have any global filters in the global KQL search bar. If you have just enabled the user risk module, the risk engine might need an hour to generate user risk score data and display in this panel.`,
}
);

View file

@ -175,6 +175,7 @@ export const useRiskScore = <T extends RiskScoreEntity.host | RiskScoreEntity.us
: undefined,
sort,
timerange: onlyLatest ? undefined : requestTimerange,
alertsTimerange: includeAlertsCount ? requestTimerange : undefined,
}
: null,
[

View file

@ -17,7 +17,7 @@ import {
} from '../../../../../common/search_strategy';
import * as i18n from './translations';
import { isIndexNotFoundError } from '../../../../common/utils/exceptions';
import type { ESTermQuery } from '../../../../../common/typed_json';
import type { ESQuery } from '../../../../../common/typed_json';
import type { SeverityCount } from '../../../components/risk_score/severity/types';
import { useSpaceId } from '../../../../common/hooks/use_space_id';
import { useMlCapabilities } from '../../../../common/components/ml/hooks/use_ml_capabilities';
@ -37,7 +37,7 @@ interface RiskScoreKpi {
}
interface UseRiskScoreKpiProps {
filterQuery?: string | ESTermQuery;
filterQuery?: string | ESQuery;
skip?: boolean;
riskEntity: RiskScoreEntity;
timerange?: { to: string; from: string };

View file

@ -19,6 +19,7 @@ import { HeaderSection } from '../../../../common/components/header_section';
import {
CASES,
CASES_BY_STATUS_SECTION_TITLE,
CASES_BY_STATUS_SECTION_TOOLTIP,
STATUS_CLOSED,
STATUS_IN_PROGRESS,
STATUS_OPEN,
@ -143,6 +144,7 @@ const CasesByStatusComponent: React.FC = () => {
toggleQuery={setToggleStatus}
subtitle={<LastUpdatedAt updatedAt={updatedAt} isUpdating={isLoading} />}
showInspectButton={false}
tooltip={CASES_BY_STATUS_SECTION_TOOLTIP}
>
<EuiFlexGroup alignItems="center" gutterSize="none">
<EuiFlexItem grow={false}>

View file

@ -71,6 +71,7 @@ export const CasesTable = React.memo(() => {
toggleQuery={setToggleStatus}
subtitle={<LastUpdatedAt updatedAt={updatedAt} isUpdating={isLoading} />}
showInspectButton={false}
tooltip={i18n.CASES_TABLE_SECTION_TOOLTIP}
/>
{toggleStatus && (

View file

@ -21,6 +21,12 @@ jest.mock('../../../../common/hooks/use_navigate_to_alerts_page_with_filters', (
};
});
jest.mock('../../../../common/hooks/use_global_filter_query', () => {
return {
useGlobalFilterQuery: () => ({}),
};
});
type UseHostAlertsItemsReturn = ReturnType<UseHostAlertsItems>;
const defaultUseHostAlertsItemsReturn: UseHostAlertsItemsReturn = {
items: [],

View file

@ -38,6 +38,7 @@ import {
SecurityCellActions,
SecurityCellActionsTrigger,
} from '../../../../common/components/cell_actions';
import { useGlobalFilterQuery } from '../../../../common/hooks/use_global_filter_query';
interface HostAlertsTableProps {
signalIndexName: string | null;
@ -51,6 +52,7 @@ const DETECTION_RESPONSE_HOST_SEVERITY_QUERY_ID = 'vulnerableHostsBySeverityQuer
export const HostAlertsTable = React.memo(({ signalIndexName }: HostAlertsTableProps) => {
const openAlertsPageWithFilters = useNavigateToAlertsPageWithFilters();
const { filterQuery } = useGlobalFilterQuery();
const openHostInAlerts = useCallback(
({ hostName, severity }: { hostName: string; severity?: string }) =>
@ -81,6 +83,7 @@ export const HostAlertsTable = React.memo(({ signalIndexName }: HostAlertsTableP
skip: !toggleStatus,
queryId: DETECTION_RESPONSE_HOST_SEVERITY_QUERY_ID,
signalIndexName,
filterQuery,
});
const columns = useMemo(() => getTableColumns(openHostInAlerts), [openHostInAlerts]);

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { firstNonNullValue } from '../../../../../common/endpoint/models/ecs_safety_helpers';
import { useQueryInspector } from '../../../../common/components/page/manage_query';
@ -14,6 +14,7 @@ import type { GenericBuckets } from '../../../../../common/search_strategy';
import { useQueryAlerts } from '../../../../detections/containers/detection_engine/alerts/use_query';
import { ALERTS_QUERY_NAMES } from '../../../../detections/containers/detection_engine/alerts/constants';
import { getPageCount, ITEMS_PER_PAGE } from '../utils';
import type { ESBoolQuery } from '../../../../../common/typed_json';
const HOSTS_BY_SEVERITY_AGG = 'hostsBySeverity';
const defaultPagination = {
@ -30,6 +31,7 @@ export interface UseHostAlertsItemsProps {
skip: boolean;
queryId: string;
signalIndexName: string | null;
filterQuery?: ESBoolQuery;
}
export interface HostAlertsItem {
@ -53,13 +55,28 @@ interface Pagination {
currentPage: number;
}
export const useHostAlertsItems: UseHostAlertsItems = ({ skip, queryId, signalIndexName }) => {
export const useHostAlertsItems: UseHostAlertsItems = ({
skip,
queryId,
signalIndexName,
filterQuery,
}) => {
const [updatedAt, setUpdatedAt] = useState(Date.now());
const [items, setItems] = useState<HostAlertsItem[]>([]);
const [paginationData, setPaginationData] = useState<Pagination>(defaultPagination);
const { to, from, setQuery: setGlobalQuery, deleteQuery } = useGlobalTime();
const query = useMemo(
() =>
buildVulnerableHostAggregationQuery({
from,
to,
currentPage: paginationData.currentPage,
filterQuery,
}),
[filterQuery, from, paginationData.currentPage, to]
);
const {
data,
request,
@ -68,21 +85,15 @@ export const useHostAlertsItems: UseHostAlertsItems = ({ skip, queryId, signalIn
loading,
refetch: refetchQuery,
} = useQueryAlerts<{}, AlertCountersBySeverityAndHostAggregation>({
query: buildVulnerableHostAggregationQuery({
from,
to,
currentPage: paginationData.currentPage,
}),
query,
indexName: signalIndexName,
skip,
queryName: ALERTS_QUERY_NAMES.VULNERABLE_HOSTS,
});
useEffect(() => {
setQuery(
buildVulnerableHostAggregationQuery({ from, to, currentPage: paginationData.currentPage })
);
}, [setQuery, from, to, paginationData.currentPage]);
setQuery(query);
}, [setQuery, paginationData.currentPage, query]);
useEffect(() => {
if (data == null || !data.aggregations) {
@ -138,7 +149,8 @@ export const buildVulnerableHostAggregationQuery = ({
from,
to,
currentPage,
}: TimeRange & { currentPage: number }) => {
filterQuery,
}: TimeRange & { currentPage: number; filterQuery?: ESBoolQuery }) => {
const fromValue = ITEMS_PER_PAGE * currentPage;
return {
@ -158,6 +170,7 @@ export const buildVulnerableHostAggregationQuery = ({
},
},
},
...(filterQuery ? [filterQuery] : []),
],
},
},

View file

@ -34,6 +34,12 @@ jest.mock('../../../../common/hooks/use_navigate_to_alerts_page_with_filters', (
};
});
jest.mock('../../../../common/hooks/use_global_filter_query', () => {
return {
useGlobalFilterQuery: () => ({}),
};
});
type UseRuleAlertsItemsReturn = ReturnType<UseRuleAlertsItems>;
const defaultUseRuleAlertsItemsReturn: UseRuleAlertsItemsReturn = {
items: [],

View file

@ -39,6 +39,7 @@ import { BUTTON_CLASS as INSPECT_BUTTON_CLASS } from '../../../../common/compone
import { LastUpdatedAt } from '../../../../common/components/last_updated_at';
import { FormattedCount } from '../../../../common/components/formatted_number';
import { SecurityCellActions } from '../../../../common/components/cell_actions';
import { useGlobalFilterQuery } from '../../../../common/hooks/use_global_filter_query';
export interface RuleAlertsTableProps {
signalIndexName: string | null;
@ -134,10 +135,13 @@ export const getTableColumns: GetTableColumns = ({
export const RuleAlertsTable = React.memo<RuleAlertsTableProps>(({ signalIndexName }) => {
const { getAppUrl, navigateTo } = useNavigation();
const { toggleStatus, setToggleStatus } = useQueryToggle(DETECTION_RESPONSE_RULE_ALERTS_QUERY_ID);
const { filterQuery } = useGlobalFilterQuery();
const { items, isLoading, updatedAt } = useRuleAlertsItems({
signalIndexName,
queryId: DETECTION_RESPONSE_RULE_ALERTS_QUERY_ID,
skip: !toggleStatus,
filterQuery,
});
const openAlertsPageWithFilter = useNavigateToAlertsPageWithFilters();

View file

@ -17,6 +17,7 @@ import {
} from './mock_data';
import type { UseRuleAlertsItems, UseRuleAlertsItemsProps } from './use_rule_alerts_items';
import { useRuleAlertsItems } from './use_rule_alerts_items';
import type { ESBoolQuery } from '../../../../../common/typed_json';
const dateNow = new Date('2022-04-08T12:00:00.000Z').valueOf();
const mockDateNow = jest.fn().mockReturnValue(dateNow);
@ -129,4 +130,19 @@ describe('useRuleAlertsItems', () => {
updatedAt: dateNow,
});
});
it('should add filterQuery to query', () => {
const filterQuery: ESBoolQuery = {
bool: {
filter: [{ match_phrase: { test: '123' } }],
must: [],
must_not: [],
should: [],
},
};
renderUseRuleAlertsItems({ filterQuery });
expect(mockUseQueryAlerts.mock.calls[0][0].query.query.bool.filter).toContain(filterQuery);
});
});

View file

@ -5,12 +5,13 @@
* 2.0.
*/
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useEffect, useState, useMemo } from 'react';
import type { Severity } from '@kbn/securitysolution-io-ts-alerting-types';
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';
import { useQueryInspector } from '../../../../common/components/page/manage_query';
import type { ESBoolQuery } from '../../../../../common/typed_json';
// Formatted item result
export interface RuleAlertsItem {
@ -48,13 +49,22 @@ export interface SeverityRuleAlertsAggsResponse {
};
}
const getSeverityRuleAlertsQuery = ({ from, to }: { from: string; to: string }) => ({
const getSeverityRuleAlertsQuery = ({
from,
to,
filterQuery,
}: {
from: string;
to: string;
filterQuery?: ESBoolQuery;
}) => ({
size: 0,
query: {
bool: {
filter: [
{ term: { 'kibana.alert.workflow_status': 'open' } },
{ range: { '@timestamp': { gte: from, lte: to } } },
...(filterQuery ? [filterQuery] : []),
],
},
},
@ -107,6 +117,7 @@ export interface UseRuleAlertsItemsProps {
queryId: string;
signalIndexName: string | null;
skip?: boolean;
filterQuery?: ESBoolQuery;
}
export type UseRuleAlertsItems = (props: UseRuleAlertsItemsProps) => {
items: RuleAlertsItem[];
@ -118,11 +129,22 @@ export const useRuleAlertsItems: UseRuleAlertsItems = ({
queryId,
signalIndexName,
skip = false,
filterQuery,
}) => {
const [items, setItems] = useState<RuleAlertsItem[]>([]);
const [updatedAt, setUpdatedAt] = useState(Date.now());
const { to, from, deleteQuery, setQuery } = useGlobalTime();
const query = useMemo(
() =>
getSeverityRuleAlertsQuery({
from,
to,
filterQuery,
}),
[filterQuery, from, to]
);
const {
loading: isLoading,
data,
@ -131,23 +153,15 @@ export const useRuleAlertsItems: UseRuleAlertsItems = ({
request,
refetch: refetchQuery,
} = useQueryAlerts<{}, SeverityRuleAlertsAggsResponse>({
query: getSeverityRuleAlertsQuery({
from,
to,
}),
query,
indexName: signalIndexName,
skip,
queryName: ALERTS_QUERY_NAMES.BY_SEVERITY,
});
useEffect(() => {
setAlertsQuery(
getSeverityRuleAlertsQuery({
from,
to,
})
);
}, [setAlertsQuery, from, to]);
setAlertsQuery(query);
}, [setAlertsQuery, query]);
useEffect(() => {
if (data == null) {

View file

@ -73,11 +73,13 @@ export const UPDATING = i18n.translate('xpack.securitySolution.detectionResponse
export const UPDATED = i18n.translate('xpack.securitySolution.detectionResponse.updated', {
defaultMessage: 'Updated',
});
export const CASES = (totalCases: number) =>
i18n.translate('xpack.securitySolution.detectionResponse.casesByStatus.totalCases', {
values: { totalCases },
defaultMessage: 'total {totalCases, plural, =1 {case} other {cases}}',
});
export const CASES_BY_STATUS_SECTION_TITLE = i18n.translate(
'xpack.securitySolution.detectionResponse.casesByStatusSectionTitle',
{
@ -85,6 +87,13 @@ export const CASES_BY_STATUS_SECTION_TITLE = i18n.translate(
}
);
export const CASES_BY_STATUS_SECTION_TOOLTIP = i18n.translate(
'xpack.securitySolution.detectionResponse.casesByStatusSectionTooltip',
{
defaultMessage: 'The cases table is not filterable via the SIEM global KQL search.',
}
);
export const VIEW_CASES = i18n.translate('xpack.securitySolution.detectionResponse.viewCases', {
defaultMessage: 'View cases',
});
@ -117,6 +126,14 @@ export const CASES_TABLE_SECTION_TITLE = i18n.translate(
}
);
export const CASES_TABLE_SECTION_TOOLTIP = i18n.translate(
'xpack.securitySolution.detectionResponse.caseSectionTooltip',
{
defaultMessage:
'The recently created cases table is not filterable via the SIEM global KQL search.',
}
);
export const NO_ALERTS_FOUND = i18n.translate(
'xpack.securitySolution.detectionResponse.noRuleAlerts',
{

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { firstNonNullValue } from '../../../../../common/endpoint/models/ecs_safety_helpers';
import { useQueryInspector } from '../../../../common/components/page/manage_query';
@ -14,6 +14,7 @@ import type { GenericBuckets } from '../../../../../common/search_strategy';
import { useQueryAlerts } from '../../../../detections/containers/detection_engine/alerts/use_query';
import { ALERTS_QUERY_NAMES } from '../../../../detections/containers/detection_engine/alerts/constants';
import { getPageCount, ITEMS_PER_PAGE } from '../utils';
import type { ESBoolQuery } from '../../../../../common/typed_json';
const USERS_BY_SEVERITY_AGG = 'usersBySeverity';
const defaultPagination = {
@ -30,6 +31,7 @@ export interface UseUserAlertsItemsProps {
skip: boolean;
queryId: string;
signalIndexName: string | null;
filterQuery?: ESBoolQuery;
}
export interface UserAlertsItem {
@ -53,12 +55,27 @@ interface Pagination {
currentPage: number;
}
export const useUserAlertsItems: UseUserAlertsItems = ({ skip, queryId, signalIndexName }) => {
export const useUserAlertsItems: UseUserAlertsItems = ({
skip,
queryId,
signalIndexName,
filterQuery,
}) => {
const [updatedAt, setUpdatedAt] = useState(Date.now());
const [items, setItems] = useState<UserAlertsItem[]>([]);
const [paginationData, setPaginationData] = useState<Pagination>(defaultPagination);
const { to, from, setQuery: setGlobalQuery, deleteQuery } = useGlobalTime();
const query = useMemo(
() =>
buildVulnerableUserAggregationQuery({
from,
to,
currentPage: paginationData.currentPage,
filterQuery,
}),
[filterQuery, from, paginationData.currentPage, to]
);
const {
setQuery,
@ -68,21 +85,15 @@ export const useUserAlertsItems: UseUserAlertsItems = ({ skip, queryId, signalIn
response,
refetch: refetchQuery,
} = useQueryAlerts<{}, AlertCountersBySeverityAggregation>({
query: buildVulnerableUserAggregationQuery({
from,
to,
currentPage: paginationData.currentPage,
}),
query,
indexName: signalIndexName,
skip,
queryName: ALERTS_QUERY_NAMES.VULNERABLE_USERS,
});
useEffect(() => {
setQuery(
buildVulnerableUserAggregationQuery({ from, to, currentPage: paginationData.currentPage })
);
}, [setQuery, from, to, paginationData.currentPage]);
setQuery(query);
}, [setQuery, paginationData.currentPage, query]);
useEffect(() => {
if (data == null || !data.aggregations) {
@ -138,7 +149,8 @@ export const buildVulnerableUserAggregationQuery = ({
from,
to,
currentPage,
}: TimeRange & { currentPage: number }) => {
filterQuery,
}: TimeRange & { currentPage: number; filterQuery?: ESBoolQuery }) => {
const fromValue = ITEMS_PER_PAGE * currentPage;
return {
@ -158,6 +170,7 @@ export const buildVulnerableUserAggregationQuery = ({
},
},
},
...(filterQuery ? [filterQuery] : []),
],
},
},

View file

@ -33,6 +33,12 @@ jest.mock('../../../../common/hooks/use_navigate_to_alerts_page_with_filters', (
};
});
jest.mock('../../../../common/hooks/use_global_filter_query', () => {
return {
useGlobalFilterQuery: () => ({}),
};
});
type UseUserAlertsItemsReturn = ReturnType<UseUserAlertsItems>;
const defaultUseUserAlertsItemsReturn: UseUserAlertsItemsReturn = {
items: [],

View file

@ -35,6 +35,7 @@ import { ITEMS_PER_PAGE, SEVERITY_COLOR } from '../utils';
import type { UserAlertsItem } from './use_user_alerts_items';
import { useUserAlertsItems } from './use_user_alerts_items';
import { SecurityCellActions } from '../../../../common/components/cell_actions';
import { useGlobalFilterQuery } from '../../../../common/hooks/use_global_filter_query';
interface UserAlertsTableProps {
signalIndexName: string | null;
@ -48,6 +49,7 @@ const DETECTION_RESPONSE_USER_SEVERITY_QUERY_ID = 'vulnerableUsersBySeverityQuer
export const UserAlertsTable = React.memo(({ signalIndexName }: UserAlertsTableProps) => {
const openAlertsPageWithFilters = useNavigateToAlertsPageWithFilters();
const { filterQuery } = useGlobalFilterQuery();
const openUserInAlerts = useCallback(
({ userName, severity }: { userName: string; severity?: string }) =>
@ -78,6 +80,7 @@ export const UserAlertsTable = React.memo(({ signalIndexName }: UserAlertsTableP
skip: !toggleStatus,
queryId: DETECTION_RESPONSE_USER_SEVERITY_QUERY_ID,
signalIndexName,
filterQuery,
});
const columns = useMemo(() => getTableColumns(openUserInAlerts), [openUserInAlerts]);

View file

@ -129,6 +129,7 @@ export const EntityAnalyticsAnomalies = () => {
subtitle={<LastUpdatedAt isUpdating={isSearchLoading} updatedAt={updatedAt} />}
toggleStatus={toggleStatus}
toggleQuery={setToggleStatus}
tooltip={i18n.ANOMALIES_TOOLTIP}
>
<EuiFlexGroup alignItems="center">
<EuiFlexItem>

View file

@ -97,3 +97,10 @@ export const ANOMALY_DETECTION_DOCS = i18n.translate(
defaultMessage: 'Anomaly Detection with Machine Learning',
}
);
export const ANOMALIES_TOOLTIP = i18n.translate(
'xpack.securitySolution.entityAnalytics.anomalies.anomaliesTooltip',
{
defaultMessage: 'The anomalies table is not filterable via the SIEM global KQL search.',
}
);

View file

@ -33,6 +33,7 @@ import { ENTITY_ANALYTICS_ANOMALIES_PANEL } from '../anomalies';
import { isJobStarted } from '../../../../../common/machine_learning/helpers';
import { FormattedCount } from '../../../../common/components/formatted_number';
import { SEVERITY_COLOR } from '../../detection_response/utils';
import { useGlobalFilterQuery } from '../../../../common/hooks/use_global_filter_query';
const StyledEuiTitle = styled(EuiTitle)`
color: ${({ theme: { eui } }) => SEVERITY_COLOR.critical};
@ -43,6 +44,7 @@ const USER_RISK_QUERY_ID = 'userRiskScoreKpiQuery';
export const EntityAnalyticsHeader = () => {
const { from, to } = useGlobalTime();
const { filterQuery } = useGlobalFilterQuery();
const timerange = useMemo(
() => ({
from,
@ -59,6 +61,7 @@ export const EntityAnalyticsHeader = () => {
} = useRiskScoreKpi({
timerange,
riskEntity: RiskScoreEntity.host,
filterQuery,
});
const {
@ -67,6 +70,7 @@ export const EntityAnalyticsHeader = () => {
refetch: refetchUserRiskScore,
inspect: inspectUserRiskScore,
} = useRiskScoreKpi({
filterQuery,
timerange,
riskEntity: RiskScoreEntity.user,
});

View file

@ -27,13 +27,14 @@ import { useRefetchQueries } from '../../../../common/hooks/use_refetch_queries'
import { Loader } from '../../../../common/components/loader';
import { Panel } from '../../../../common/components/panel';
import * as commonI18n from '../common/translations';
import * as i18n from './translations';
import { useEntityInfo } from './use_entity';
import { RiskScoreHeaderContent } from './header_content';
import { ChartContent } from './chart_content';
import { useNavigateToAlertsPageWithFilters } from '../../../../common/hooks/use_navigate_to_alerts_page_with_filters';
import { getRiskEntityTranslation } from './translations';
import { useKibana } from '../../../../common/lib/kibana';
import { useGlobalFilterQuery } from '../../../../common/hooks/use_global_filter_query';
const EntityAnalyticsRiskScoresComponent = ({ riskEntity }: { riskEntity: RiskScoreEntity }) => {
const { deleteQuery, setQuery, from, to } = useGlobalTime();
@ -69,10 +70,13 @@ const EntityAnalyticsRiskScoresComponent = ({ riskEntity }: { riskEntity: RiskSc
const severityFilter = useMemo(() => {
const [filter] = generateSeverityFilter(selectedSeverity, riskEntity);
return filter ? JSON.stringify(filter.query) : undefined;
return filter ? filter : undefined;
}, [riskEntity, selectedSeverity]);
const { filterQuery } = useGlobalFilterQuery({
extraFilter: severityFilter,
});
const timerange = useMemo(
() => ({
from,
@ -87,7 +91,7 @@ const EntityAnalyticsRiskScoresComponent = ({ riskEntity }: { riskEntity: RiskSc
refetch: refetchKpi,
inspect: inspectKpi,
} = useRiskScoreKpi({
filterQuery: severityFilter,
filterQuery,
skip: !toggleStatus,
timerange,
riskEntity,
@ -110,7 +114,7 @@ const EntityAnalyticsRiskScoresComponent = ({ riskEntity }: { riskEntity: RiskSc
isLicenseValid,
isModuleEnabled,
} = useRiskScore({
filterQuery: severityFilter,
filterQuery,
skip: !toggleStatus,
pagination: {
cursorStart: 0,
@ -174,8 +178,8 @@ const EntityAnalyticsRiskScoresComponent = ({ riskEntity }: { riskEntity: RiskSc
toggleQuery={setToggleStatus}
tooltip={
riskEntity === RiskScoreEntity.host
? commonI18n.HOST_RISK_TABLE_TOOLTIP
: commonI18n.USER_RISK_TABLE_TOOLTIP
? i18n.HOST_RISK_TABLE_TOOLTIP
: i18n.USER_RISK_TABLE_TOOLTIP
}
tooltipTitle={commonI18n.RISK_TABLE_TOOLTIP_TITLE}
>

View file

@ -41,3 +41,19 @@ export const LEARN_MORE = i18n.translate(
defaultMessage: 'Learn more',
}
);
export const HOST_RISK_TABLE_TOOLTIP = i18n.translate(
'xpack.securitySolution.entityAnalytics.riskDashboard.hostsTableTooltip',
{
defaultMessage:
'The Host Risk Score panel displays the list of risky hosts and their latest risk score. You may filter this list using global filters in the KQL search bar. The time-range picker filter will display Alerts within the selected time range only and does not filter the list of risky hosts.',
}
);
export const USER_RISK_TABLE_TOOLTIP = i18n.translate(
'xpack.securitySolution.entityAnalytics.riskDashboard.usersTableTooltip',
{
defaultMessage:
'The User Risk Score panel displays the list of risky users and their latest risk score. You may filter this list using global filters in the KQL search bar. The time-range picker filter will display Alerts within the selected time range only and does not filter the list of risky users.',
}
);

View file

@ -37,7 +37,11 @@ jest.mock('../components/detection_response/cases_by_status', () => ({
}));
jest.mock('../../common/components/search_bar', () => ({
SiemSearchBar: () => <div data-test-subj="mock_globalDatePicker" />,
SiemSearchBar: () => <div data-test-subj="mock_globalSearchBar" />,
}));
jest.mock('../../common/components/filters_global', () => ({
FiltersGlobal: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
}));
const defaultUseSourcererReturn = {
@ -97,7 +101,7 @@ describe('DetectionResponse', () => {
);
expect(result.queryByTestId('detectionResponsePage')).toBeInTheDocument();
expect(result.queryByTestId('mock_globalDatePicker')).toBeInTheDocument();
expect(result.queryByTestId('mock_globalSearchBar')).toBeInTheDocument();
expect(result.queryByTestId('detectionResponseSections')).toBeInTheDocument();
expect(result.queryByTestId('detectionResponseLoader')).not.toBeInTheDocument();
expect(result.getByText('Detection & Response')).toBeInTheDocument();
@ -119,7 +123,7 @@ describe('DetectionResponse', () => {
expect(result.getByTestId('siem-landing-page')).toBeInTheDocument();
expect(result.queryByTestId('detectionResponsePage')).not.toBeInTheDocument();
expect(result.queryByTestId('mock_globalDatePicker')).not.toBeInTheDocument();
expect(result.queryByTestId('mock_globalSearchBar')).not.toBeInTheDocument();
});
it('should render loader if sourcerer is loading', () => {
@ -137,7 +141,7 @@ describe('DetectionResponse', () => {
);
expect(result.queryByTestId('detectionResponsePage')).toBeInTheDocument();
expect(result.queryByTestId('mock_globalDatePicker')).toBeInTheDocument();
expect(result.queryByTestId('mock_globalSearchBar')).toBeInTheDocument();
expect(result.queryByTestId('detectionResponseLoader')).toBeInTheDocument();
expect(result.queryByTestId('detectionResponseSections')).not.toBeInTheDocument();
});

View file

@ -29,8 +29,11 @@ import * as i18n from './translations';
import { CasesTable } from '../components/detection_response/cases_table';
import { CasesByStatus } from '../components/detection_response/cases_by_status';
import { NoPrivileges } from '../../common/components/no_privileges';
import { FiltersGlobal } from '../../common/components/filters_global';
import { useGlobalFilterQuery } from '../../common/hooks/use_global_filter_query';
const DetectionResponseComponent = () => {
const { filterQuery } = useGlobalFilterQuery();
const { indicesExist, indexPattern, loading: isSourcererLoading } = useSourcererDataView();
const { signalIndexName } = useSignalIndex();
const { hasKibanaREAD, hasIndexRead } = useAlertsPrivileges();
@ -45,16 +48,11 @@ const DetectionResponseComponent = () => {
<>
{indicesExist ? (
<>
<FiltersGlobal>
<SiemSearchBar id={InputsModelId.global} indexPattern={indexPattern} />
</FiltersGlobal>
<SecuritySolutionPageWrapper data-test-subj="detectionResponsePage">
<HeaderPage title={i18n.DETECTION_RESPONSE_TITLE}>
<SiemSearchBar
id={InputsModelId.global}
indexPattern={indexPattern}
hideFilterBar
hideQueryInput
/>
</HeaderPage>
<HeaderPage title={i18n.DETECTION_RESPONSE_TITLE} />
{isSourcererLoading ? (
<EuiLoadingSpinner size="l" data-test-subj="detectionResponseLoader" />
) : (
@ -63,7 +61,10 @@ const DetectionResponseComponent = () => {
<EuiFlexGroup>
{canReadAlerts && (
<EuiFlexItem>
<AlertsByStatus signalIndexName={signalIndexName} />
<AlertsByStatus
signalIndexName={signalIndexName}
additionalFilters={filterQuery ? [filterQuery] : undefined}
/>
</EuiFlexItem>
)}
{canReadCases && (

View file

@ -24,6 +24,7 @@ import { EntityAnalyticsHeader } from '../components/entity_analytics/header';
import { EntityAnalyticsAnomalies } from '../components/entity_analytics/anomalies';
import { SiemSearchBar } from '../../common/components/search_bar';
import { InputsModelId } from '../../common/store/inputs/constants';
import { FiltersGlobal } from '../../common/components/filters_global';
const EntityAnalyticsComponent = () => {
const { indicesExist, loading: isSourcererLoading, indexPattern } = useSourcererDataView();
@ -33,17 +34,14 @@ const EntityAnalyticsComponent = () => {
<>
{indicesExist ? (
<>
{isPlatinumOrTrialLicense && capabilitiesFetched && (
<FiltersGlobal>
<SiemSearchBar id={InputsModelId.global} indexPattern={indexPattern} />
</FiltersGlobal>
)}
<SecuritySolutionPageWrapper data-test-subj="entityAnalyticsPage">
<HeaderPage title={ENTITY_ANALYTICS}>
{isPlatinumOrTrialLicense && capabilitiesFetched && (
<SiemSearchBar
id={InputsModelId.global}
indexPattern={indexPattern}
hideFilterBar
hideQueryInput
/>
)}
</HeaderPage>
<HeaderPage title={ENTITY_ANALYTICS} />
{!isPlatinumOrTrialLicense && capabilitiesFetched ? (
<Paywall heading={i18n.ENTITY_ANALYTICS_LICENSE_DESC} />
) : isSourcererLoading ? (

View file

@ -97,6 +97,10 @@ export const mockOptions: RiskScoreRequestOptions = {
describe('buildRiskScoreQuery search strategy', () => {
const buildKpiRiskScoreQuery = jest.spyOn(buildQuery, 'buildRiskScoreQuery');
afterEach(() => {
jest.clearAllMocks();
});
describe('buildDsl', () => {
test('should build dsl query', () => {
riskScore.buildDsl(mockOptions);
@ -167,4 +171,29 @@ describe('buildRiskScoreQuery search strategy', () => {
expect(get('data[0].oldestAlertTimestamp', result)).toBe(oldestAlertTimestamp);
});
test('should filter enhance query by time range', async () => {
await riskScore.parse(
{
...mockOptions,
alertsTimerange: {
from: 'now-5m',
to: 'now',
interval: '1m',
},
},
mockSearchStrategyResponse,
mockDeps
);
expect(searchMock.mock.calls[0][0].query.bool.filter).toEqual(
expect.arrayContaining([
{
range: {
'@timestamp': { format: 'strict_date_optional_time', gte: 'now-5m', lte: 'now' },
},
},
])
);
});
});

View file

@ -5,9 +5,8 @@
* 2.0.
*/
import type { IEsSearchResponse, SearchRequest } from '@kbn/data-plugin/common';
import type { IEsSearchResponse, SearchRequest, TimeRange } from '@kbn/data-plugin/common';
import { get, getOr } from 'lodash/fp';
import type { IRuleDataClient } from '@kbn/rule-registry-plugin/server';
import type { AggregationsMinAggregate } from '@elastic/elasticsearch/lib/api/types';
import type { SecuritySolutionFactory } from '../../types';
@ -54,7 +53,14 @@ export const riskScore: SecuritySolutionFactory<
const enhancedData =
deps && options.includeAlertsCount
? await enhanceData(data, names, nameField, deps.ruleDataClient, deps.spaceId)
? await enhanceData(
data,
names,
nameField,
deps.ruleDataClient,
deps.spaceId,
options.alertsTimerange
)
: data;
return {
@ -75,10 +81,11 @@ async function enhanceData(
names: string[],
nameField: string,
ruleDataClient?: IRuleDataClient | null,
spaceId?: string
spaceId?: string,
timerange?: TimeRange
): Promise<Array<HostRiskScore | UserRiskScore>> {
const ruleDataReader = ruleDataClient?.getReader({ namespace: spaceId });
const query = getAlertsQueryForEntity(names, nameField);
const query = getAlertsQueryForEntity(names, nameField, timerange);
const response = await ruleDataReader?.search(query);
const buckets: EnhancedDataBucket[] = getOr([], 'aggregations.alertsByEntity.buckets', response);
@ -101,26 +108,45 @@ async function enhanceData(
}));
}
const getAlertsQueryForEntity = (names: string[], nameField: string): SearchRequest => ({
size: 0,
query: {
bool: {
filter: [
{ term: { 'kibana.alert.workflow_status': 'open' } },
{ terms: { [nameField]: names } },
],
},
},
aggs: {
alertsByEntity: {
terms: {
field: nameField,
const getAlertsQueryForEntity = (
names: string[],
nameField: string,
timerange: TimeRange | undefined
): SearchRequest => {
return {
size: 0,
query: {
bool: {
filter: [
{ term: { 'kibana.alert.workflow_status': 'open' } },
{ terms: { [nameField]: names } },
...(timerange
? [
{
range: {
'@timestamp': {
gte: timerange.from,
lte: timerange.to,
format: 'strict_date_optional_time',
},
},
},
]
: []),
],
},
aggs: {
oldestAlertTimestamp: {
min: { field: '@timestamp' },
},
aggs: {
alertsByEntity: {
terms: {
field: nameField,
},
aggs: {
oldestAlertTimestamp: {
min: { field: '@timestamp' },
},
},
},
},
},
});
};
};