Skip to content

Commit

Permalink
[8.8] [Security Solution] Add Search Bar to Security D&R and EA Dashb…
Browse files Browse the repository at this point in the history
…oards (#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)](#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](https://user-images.githubusercontent.com/1490444/236467186-f6e6c435-447b-41f4-a6b6-8bd4a3deb498.gif)\r\n![May-05-2023\r\n15-13-42](https://user-images.githubusercontent.com/1490444/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":"#156832
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](https://user-images.githubusercontent.com/1490444/236467186-f6e6c435-447b-41f4-a6b6-8bd4a3deb498.gif)\r\n![May-05-2023\r\n15-13-42](https://user-images.githubusercontent.com/1490444/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":"#156832
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](https://user-images.githubusercontent.com/1490444/236467186-f6e6c435-447b-41f4-a6b6-8bd4a3deb498.gif)\r\n![May-05-2023\r\n15-13-42](https://user-images.githubusercontent.com/1490444/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>
  • Loading branch information
kibanamachine and machadoum committed May 9, 2023
1 parent d10ba61 commit bea8802
Show file tree
Hide file tree
Showing 29 changed files with 512 additions and 97 deletions.
Expand Up @@ -15,6 +15,7 @@ export interface RiskScoreRequestOptions extends IEsSearchRequest {
defaultIndex: string[];
riskScoreEntity: RiskScoreEntity;
timerange?: TimerangeInput;
alertsTimerange?: TimerangeInput;
includeAlertsCount?: boolean;
onlyLatest?: boolean;
pagination?: {
Expand Down
@@ -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 };
}
};
Expand Up @@ -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.`,
}
);

Expand Down
Expand Up @@ -175,6 +175,7 @@ export const useRiskScore = <T extends RiskScoreEntity.host | RiskScoreEntity.us
: undefined,
sort,
timerange: onlyLatest ? undefined : requestTimerange,
alertsTimerange: includeAlertsCount ? requestTimerange : undefined,
}
: null,
[
Expand Down
Expand Up @@ -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';
Expand All @@ -37,7 +37,7 @@ interface RiskScoreKpi {
}

interface UseRiskScoreKpiProps {
filterQuery?: string | ESTermQuery;
filterQuery?: string | ESQuery;
skip?: boolean;
riskEntity: RiskScoreEntity;
timerange?: { to: string; from: string };
Expand Down
Expand Up @@ -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,
Expand Down Expand Up @@ -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}>
Expand Down
Expand Up @@ -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 && (
Expand Down
Expand Up @@ -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: [],
Expand Down
Expand Up @@ -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;
Expand All @@ -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 }) =>
Expand Down Expand Up @@ -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]);
Expand Down

0 comments on commit bea8802

Please sign in to comment.