mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
# 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\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\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\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:
parent
d10ba61d9f
commit
bea88026b7
29 changed files with 512 additions and 97 deletions
|
@ -15,6 +15,7 @@ export interface RiskScoreRequestOptions extends IEsSearchRequest {
|
|||
defaultIndex: string[];
|
||||
riskScoreEntity: RiskScoreEntity;
|
||||
timerange?: TimerangeInput;
|
||||
alertsTimerange?: TimerangeInput;
|
||||
includeAlertsCount?: boolean;
|
||||
onlyLatest?: boolean;
|
||||
pagination?: {
|
||||
|
|
|
@ -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(),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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 };
|
||||
}
|
||||
};
|
|
@ -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 haven’t 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 haven’t 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.`,
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -175,6 +175,7 @@ export const useRiskScore = <T extends RiskScoreEntity.host | RiskScoreEntity.us
|
|||
: undefined,
|
||||
sort,
|
||||
timerange: onlyLatest ? undefined : requestTimerange,
|
||||
alertsTimerange: includeAlertsCount ? requestTimerange : undefined,
|
||||
}
|
||||
: null,
|
||||
[
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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 && (
|
||||
|
|
|
@ -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: [],
|
||||
|
|
|
@ -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]);
|
||||
|
|
|
@ -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] : []),
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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: [],
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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',
|
||||
{
|
||||
|
|
|
@ -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] : []),
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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: [],
|
||||
|
|
|
@ -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]);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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.',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
|
|
|
@ -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.',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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 && (
|
||||
|
|
|
@ -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 ? (
|
||||
|
|
|
@ -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' },
|
||||
},
|
||||
},
|
||||
])
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue