Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 2 commits into from
May 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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(),
})
);
});
});
Original file line number Diff line number Diff line change
@@ -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 };
}
};
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Loading