[Security Solution] Invalid KQL Query Bug (#99442)

## Summary

Addresses #98283

Currently, our method of converting KQL to Elasticsearch queries silently suppresses errors bubbled up by ES and returns an empty query string. This makes it so the entire query, including filters, etc. gets wiped out and potentially incorrect data is displayed. 

This PR addresses that by bubbling up the errors and putting them in a toast component as well as cancelling any request that was made with the invalid query so that incorrect data is never fetched.

![Screen Shot 2021-05-11 at 5 05 24 PM](https://user-images.githubusercontent.com/56367316/117895214-e8bf9500-b28b-11eb-83a6-522deebecbe2.png)


### Checklist

Delete any items that are not applicable to this PR.

- [x] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials
- [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios

### For maintainers

- [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
This commit is contained in:
Davis Plumlee 2021-06-30 11:17:52 -04:00 committed by GitHub
parent 3ac067fc91
commit 36b21b4007
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 403 additions and 116 deletions

View file

@ -50,6 +50,7 @@ jest.mock('../../../common/hooks/use_selector', () => ({
useShallowEqualSelector: jest.fn(),
useDeepEqualSelector: jest.fn(),
}));
jest.mock('../../../common/hooks/use_invalid_filter_query.tsx');
const mockUiSettingsForFilterManager = coreMock.createStart().uiSettings;
const timelineId = TimelineId.active;

View file

@ -49,9 +49,9 @@ import {
import { GraphOverlay } from '../../../timelines/components/graph_overlay';
import { CellValueElementProps } from '../../../timelines/components/timeline/cell_rendering';
import { SELECTOR_TIMELINE_GLOBAL_CONTAINER } from '../../../timelines/components/timeline/styles';
import { defaultControlColumn } from '../../../timelines/components/timeline/body/control_columns';
import { timelineSelectors, timelineActions } from '../../../timelines/store/timeline';
import { useDeepEqualSelector } from '../../hooks/use_selector';
import { defaultControlColumn } from '../../../timelines/components/timeline/body/control_columns';
export const EVENTS_VIEWER_HEADER_HEIGHT = 90; // px
const UTILITY_BAR_HEIGHT = 19; // px
@ -243,7 +243,7 @@ const EventsViewerComponent: React.FC<Props> = ({
sort: sortField,
startDate: start,
endDate: end,
skip: !canQueryTimeline,
skip: !canQueryTimeline || combinedQueries?.filterQuery === undefined, // When the filterQuery comes back as undefined, it means an error has been thrown and the request should be skipped
});
const totalCountMinusDeleted = useMemo(
@ -296,6 +296,7 @@ const EventsViewerComponent: React.FC<Props> = ({
height={headerFilterGroup ? COMPACT_HEADER_HEIGHT : EVENTS_VIEWER_HEADER_HEIGHT}
subtitle={utilityBar ? undefined : subtitle}
title={globalFullScreen ? titleWithExitFullScreen : justTitle}
isInspectDisabled={combinedQueries!.filterQuery === undefined}
>
{HeaderSectionContent}
</HeaderSection>

View file

@ -41,6 +41,7 @@ export interface HeaderSectionProps extends HeaderProps {
children?: React.ReactNode;
height?: number;
id?: string;
isInspectDisabled?: boolean;
split?: boolean;
subtitle?: string | React.ReactNode;
title: string | React.ReactNode;
@ -55,6 +56,7 @@ const HeaderSectionComponent: React.FC<HeaderSectionProps> = ({
children,
height,
id,
isInspectDisabled,
split,
subtitle,
title,
@ -85,7 +87,12 @@ const HeaderSectionComponent: React.FC<HeaderSectionProps> = ({
{id && (
<EuiFlexItem grow={false}>
<InspectButton queryId={id} multiple={inspectMultiple} title={title} />
<InspectButton
isDisabled={isInspectDisabled}
queryId={id}
multiple={inspectMultiple}
title={title}
/>
</EuiFlexItem>
)}
</EuiFlexGroup>

View file

@ -91,6 +91,7 @@ export const MatrixHistogramComponent: React.FC<MatrixHistogramComponentProps> =
title,
titleSize,
yTickFormatter,
skip,
}) => {
const dispatch = useDispatch();
const handleBrushEnd = useCallback(
@ -146,6 +147,7 @@ export const MatrixHistogramComponent: React.FC<MatrixHistogramComponentProps> =
stackByField: selectedStackByOption.value,
isPtrIncluded,
docValueFields,
skip,
};
const [loading, { data, inspect, totalCount, refetch }] = useMatrixHistogramCombined(
@ -216,6 +218,7 @@ export const MatrixHistogramComponent: React.FC<MatrixHistogramComponentProps> =
titleSize={titleSize}
subtitle={subtitleWithCounts}
inspectMultiple
isInspectDisabled={filterQuery === undefined}
>
<EuiFlexGroup alignItems="center" gutterSize="none">
<EuiFlexItem grow={false}>

View file

@ -75,6 +75,7 @@ const AnomaliesHostTableComponent: React.FC<AnomaliesHostTableProps> = ({
)}`}
title={i18n.ANOMALIES}
tooltip={i18n.TOOLTIP}
isInspectDisabled={skip}
/>
<BasicTable

View file

@ -65,6 +65,7 @@ const AnomaliesNetworkTableComponent: React.FC<AnomaliesNetworkTableProps> = ({
)}`}
title={i18n.ANOMALIES}
tooltip={i18n.TOOLTIP}
isInspectDisabled={skip}
/>
<BasicTable

View file

@ -262,7 +262,7 @@ export const useMatrixHistogramCombined = (
const [missingDataLoading, missingDataResponse] = useMatrixHistogram({
...matrixHistogramQueryProps,
includeMissingData: false,
skip: skipMissingData,
skip: skipMissingData || matrixHistogramQueryProps.filterQuery === undefined,
});
const combinedLoading = useMemo<boolean>(() => mainLoading || missingDataLoading, [

View file

@ -0,0 +1,68 @@
/*
* 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 { useEffect, useMemo } from 'react';
import { useDispatch } from 'react-redux';
import { Query } from 'src/plugins/data/public';
import { appSelectors } from '../store';
import { appActions } from '../store/app';
import { useAppToasts } from './use_app_toasts';
import { useDeepEqualSelector } from './use_selector';
/**
* Adds a toast error message whenever invalid KQL is submitted through the search bar
*/
export const useInvalidFilterQuery = ({
id,
filterQuery,
kqlError,
query,
startDate,
endDate,
}: {
id: string;
filterQuery?: string;
kqlError?: Error;
query: Query;
startDate: string;
endDate: string;
}) => {
const { addError } = useAppToasts();
const dispatch = useDispatch();
const getErrorsSelector = useMemo(() => appSelectors.errorsSelector(), []);
const errors = useDeepEqualSelector(getErrorsSelector);
useEffect(() => {
if (filterQuery === undefined && kqlError != null) {
// Local util for creating an replicatable error hash
const hashCode = kqlError.message
.split('')
// eslint-disable-next-line no-bitwise
.reduce((a, b) => ((a << 5) - a + b.charCodeAt(0)) | 0, 0)
.toString();
dispatch(
appActions.addErrorHash({
id,
hash: hashCode,
title: kqlError.name,
message: [kqlError.message],
})
);
}
// This disable is required to only trigger the toast once per render
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id, filterQuery, addError, query, startDate, endDate]);
useEffect(() => {
const myError = errors.find((e) => e.id === id);
if (myError != null && myError.displayError && kqlError != null) {
// Removes error stack from user view
delete kqlError.stack; // Mutates the error object and can possibly lead to side effects, only going this route for type issues. Change when we add a stackless toast error
addError(kqlError, { title: kqlError.name });
}
}, [addError, errors, id, kqlError]);
};

View file

@ -80,20 +80,23 @@ export const convertToBuildEsQuery = ({
indexPattern: IIndexPattern;
queries: Query[];
filters: Filter[];
}) => {
}): [string, undefined] | [undefined, Error] => {
try {
return JSON.stringify(
esQuery.buildEsQuery(
indexPattern,
queries,
filters.filter((f) => f.meta.disabled === false),
{
...config,
dateFormatTZ: undefined,
}
)
);
} catch (exp) {
return '';
return [
JSON.stringify(
esQuery.buildEsQuery(
indexPattern,
queries,
filters.filter((f) => f.meta.disabled === false),
{
...config,
dateFormatTZ: undefined,
}
)
),
undefined,
];
} catch (error) {
return [undefined, error];
}
};

View file

@ -20,3 +20,10 @@ export const addError = actionCreator<{ id: string; title: string; message: stri
);
export const removeError = actionCreator<{ id: string }>('REMOVE_ERRORS');
export const addErrorHash = actionCreator<{
id: string;
hash: string;
title: string;
message: string[];
}>('ADD_ERROR_HASH');

View file

@ -18,6 +18,8 @@ export interface Error {
id: string;
title: string;
message: string[];
hash?: string;
displayError?: boolean;
}
export type ErrorModel = Error[];

View file

@ -9,7 +9,7 @@ import { reducerWithInitialState } from 'typescript-fsa-reducers';
import { Note } from '../../lib/note';
import { addError, addNotes, removeError, updateNote } from './actions';
import { addError, addErrorHash, addNotes, removeError, updateNote } from './actions';
import { AppModel, NotesById } from './model';
export type AppState = AppModel;
@ -46,4 +46,26 @@ export const appReducer = reducerWithInitialState(initialAppState)
...state,
errors: state.errors.filter((error) => error.id !== id),
}))
.case(addErrorHash, (state, { id, hash, title, message }) => {
const errorIdx = state.errors.findIndex((e) => e.id === id);
const errorObj = state.errors.find((e) => e.id === id) || { id, title, message };
if (errorIdx === -1) {
return {
...state,
errors: state.errors.concat({
...errorObj,
hash,
displayError: !state.errors.some((e) => e.hash === hash),
}),
};
}
return {
...state,
errors: [
...state.errors.slice(0, errorIdx),
{ ...errorObj, hash, displayError: !state.errors.some((e) => e.hash === hash) },
...state.errors.slice(errorIdx + 1),
],
};
})
.build();

View file

@ -129,6 +129,7 @@ export const AlertsHistogramPanel = memo<AlertsHistogramPanelProps>(
// create a unique, but stable (across re-renders) query id
const uniqueQueryId = useMemo(() => `${DETECTIONS_HISTOGRAM_ID}-${uuid.v4()}`, []);
const [isInitialLoading, setIsInitialLoading] = useState(true);
const [isInspectDisabled, setIsInspectDisabled] = useState(false);
const [defaultNumberFormat] = useUiSetting$<string>(DEFAULT_NUMBER_FORMAT);
const [totalAlertsObj, setTotalAlertsObj] = useState<AlertsTotal>(defaultTotalAlertsObj);
const [selectedStackByOption, setSelectedStackByOption] = useState<AlertsHistogramOption>(
@ -261,7 +262,7 @@ export const AlertsHistogramPanel = memo<AlertsHistogramPanelProps>(
}
);
}
setIsInspectDisabled(false);
setAlertsQuery(
getAlertsHistogramQuery(
selectedStackByOption.value,
@ -271,6 +272,7 @@ export const AlertsHistogramPanel = memo<AlertsHistogramPanelProps>(
)
);
} catch (e) {
setIsInspectDisabled(true);
setAlertsQuery(getAlertsHistogramQuery(selectedStackByOption.value, from, to, []));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
@ -305,6 +307,7 @@ export const AlertsHistogramPanel = memo<AlertsHistogramPanelProps>(
title={titleText}
titleSize={onlyField == null ? 'm' : 's'}
subtitle={!isInitialLoading && showTotalAlertsCount && totalAlerts}
isInspectDisabled={isInspectDisabled}
>
<EuiFlexGroup alignItems="center" gutterSize="none">
<EuiFlexItem grow={false}>

View file

@ -51,6 +51,7 @@ import { useSourcererScope } from '../../../common/containers/sourcerer';
import { buildTimeRangeFilter } from './helpers';
import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers';
import { columns, RenderCellValue } from '../../configurations/security_solution_detections';
import { useInvalidFilterQuery } from '../../../common/hooks/use_invalid_filter_query';
interface OwnProps {
defaultFilters?: Filter[];
@ -132,6 +133,15 @@ export const AlertsTableComponent: React.FC<AlertsTableComponentProps> = ({
[browserFields, defaultFilters, globalFilters, globalQuery, indexPatterns, kibana, to, from]
);
useInvalidFilterQuery({
id: timelineId,
filterQuery: getGlobalQuery([])?.filterQuery,
kqlError: getGlobalQuery([])?.kqlError,
query: globalQuery,
startDate: from,
endDate: to,
});
const setEventsLoadingCallback = useCallback(
({ eventIds, isLoading }: SetEventsLoadingProps) => {
setEventsLoading!({ id: timelineId, eventIds, isLoading });

View file

@ -9,7 +9,7 @@ import { UpdateDateRange } from '../../../common/components/charts/common';
import { GlobalTimeArgs } from '../../../common/containers/use_global_time';
export interface HostsKpiProps {
filterQuery: string;
filterQuery?: string;
from: string;
to: string;
indexNames: string[];

View file

@ -33,7 +33,7 @@ import { InspectResponse } from '../../../types';
import { useTransforms } from '../../../transforms/containers/use_transforms';
import { useAppToasts } from '../../../common/hooks/use_app_toasts';
const ID = 'hostsAllQuery';
export const ID = 'hostsAllQuery';
type LoadPage = (newActivePage: number) => void;
export interface HostsArgs {

View file

@ -70,7 +70,7 @@ export const HostDetailsTabs = React.memo<HostDetailsTabsProps>(
deleteQuery,
endDate: to,
filterQuery,
skip: isInitializing,
skip: isInitializing || filterQuery === undefined,
setQuery,
startDate: from,
type,

View file

@ -49,8 +49,9 @@ import { TimelineId } from '../../../../common/types/timeline';
import { timelineDefaults } from '../../../timelines/store/timeline/defaults';
import { useSourcererScope } from '../../../common/containers/sourcerer';
import { useDeepEqualSelector, useShallowEqualSelector } from '../../../common/hooks/use_selector';
import { useHostDetails } from '../../containers/hosts/details';
import { ID, useHostDetails } from '../../containers/hosts/details';
import { manageQuery } from '../../../common/components/page/manage_query';
import { useInvalidFilterQuery } from '../../../common/hooks/use_invalid_filter_query';
const HostOverviewManage = manageQuery(HostOverview);
@ -103,13 +104,15 @@ const HostDetailsComponent: React.FC<HostDetailsProps> = ({ detailName, hostDeta
indexNames: selectedPatterns,
skip: selectedPatterns.length === 0,
});
const filterQuery = convertToBuildEsQuery({
const [filterQuery, kqlError] = convertToBuildEsQuery({
config: esQuery.getEsQueryConfig(kibana.services.uiSettings),
indexPattern,
queries: [query],
filters: getFilters(),
});
useInvalidFilterQuery({ id: ID, filterQuery, kqlError, query, startDate: from, endDate: to });
useEffect(() => {
dispatch(setHostDetailsTablesActivePageToZero());
}, [dispatch, detailName]);

View file

@ -61,7 +61,7 @@ export type HostDetailsTabsProps = HostBodyComponentDispatchProps &
docValueFields?: DocValueFields[];
indexNames: string[];
pageFilters?: Filter[];
filterQuery: string;
filterQuery?: string;
indexPattern: IIndexPattern;
type: hostsModel.HostsType;
};

View file

@ -52,6 +52,8 @@ import { timelineSelectors } from '../../timelines/store/timeline';
import { timelineDefaults } from '../../timelines/store/timeline/defaults';
import { useSourcererScope } from '../../common/containers/sourcerer';
import { useDeepEqualSelector, useShallowEqualSelector } from '../../common/hooks/use_selector';
import { useInvalidFilterQuery } from '../../common/hooks/use_invalid_filter_query';
import { ID } from '../containers/hosts';
/**
* Need a 100% height here to account for the graph/analyze tool, which sets no explicit height parameters, but fills the available space.
@ -110,7 +112,7 @@ const HostsComponent = () => {
[dispatch]
);
const { docValueFields, indicesExist, indexPattern, selectedPatterns } = useSourcererScope();
const filterQuery = useMemo(
const [filterQuery, kqlError] = useMemo(
() =>
convertToBuildEsQuery({
config: esQuery.getEsQueryConfig(uiSettings),
@ -120,7 +122,7 @@ const HostsComponent = () => {
}),
[filters, indexPattern, uiSettings, query]
);
const tabsFilterQuery = useMemo(
const [tabsFilterQuery] = useMemo(
() =>
convertToBuildEsQuery({
config: esQuery.getEsQueryConfig(uiSettings),
@ -131,6 +133,8 @@ const HostsComponent = () => {
[indexPattern, query, tabsFilters, uiSettings]
);
useInvalidFilterQuery({ id: ID, filterQuery, kqlError, query, startDate: from, endDate: to });
const onSkipFocusBeforeEventsTable = useCallback(() => {
containerElement.current
?.querySelector<HTMLButtonElement>('.inspectButtonComponent:last-of-type')
@ -183,7 +187,7 @@ const HostsComponent = () => {
from={from}
setQuery={setQuery}
to={to}
skip={isInitializing}
skip={isInitializing || !filterQuery}
narrowDateRange={narrowDateRange}
/>
@ -200,7 +204,7 @@ const HostsComponent = () => {
deleteQuery={deleteQuery}
docValueFields={docValueFields}
to={to}
filterQuery={tabsFilterQuery}
filterQuery={tabsFilterQuery || ''}
isInitializing={isInitializing}
indexNames={selectedPatterns}
setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker}

View file

@ -69,7 +69,7 @@ export const HostsTabs = memo<HostsTabsProps>(
endDate: to,
filterQuery,
indexNames,
skip: isInitializing,
skip: isInitializing || filterQuery === undefined,
setQuery,
startDate: from,
type,

View file

@ -44,7 +44,7 @@ export type HostsComponentsQueryProps = QueryTabBodyProps & {
};
export type AlertsComponentQueryProps = HostsComponentsQueryProps & {
filterQuery: string;
filterQuery?: string;
pageFilters?: Filter[];
};

View file

@ -9,7 +9,7 @@ import { UpdateDateRange } from '../../../common/components/charts/common';
import { GlobalTimeArgs } from '../../../common/containers/use_global_time';
export interface NetworkKpiProps {
filterQuery: string;
filterQuery?: string;
from: string;
indexNames: string[];
to: string;

View file

@ -26,7 +26,7 @@ import { getInspectResponse } from '../../../helpers';
import { InspectResponse } from '../../../types';
import { useAppToasts } from '../../../common/hooks/use_app_toasts';
const ID = 'networkDetailsQuery';
export const ID = 'networkDetailsQuery';
export interface NetworkDetailsArgs {
id: string;

View file

@ -29,7 +29,7 @@ import { FlowTargetSelectConnected } from '../../components/flow_target_select_c
import { IpOverview } from '../../components/details';
import { SiemSearchBar } from '../../../common/components/search_bar';
import { SecuritySolutionPageWrapper } from '../../../common/components/page_wrapper';
import { useNetworkDetails } from '../../containers/details';
import { useNetworkDetails, ID } from '../../containers/details';
import { useKibana } from '../../../common/lib/kibana';
import { decodeIpv6 } from '../../../common/lib/helpers';
import { convertToBuildEsQuery } from '../../../common/lib/keury';
@ -49,6 +49,7 @@ import { esQuery } from '../../../../../../../src/plugins/data/public';
import { networkModel } from '../../store';
import { SecurityPageName } from '../../../app/types';
import { useSourcererScope } from '../../../common/containers/sourcerer';
import { useInvalidFilterQuery } from '../../../common/hooks/use_invalid_filter_query';
export { getBreadcrumbs } from './utils';
const NetworkDetailsManage = manageQuery(IpOverview);
@ -93,13 +94,15 @@ const NetworkDetailsComponent: React.FC = () => {
const { docValueFields, indicesExist, indexPattern, selectedPatterns } = useSourcererScope();
const ip = decodeIpv6(detailName);
const filterQuery = convertToBuildEsQuery({
const [filterQuery, kqlError] = convertToBuildEsQuery({
config: esQuery.getEsQueryConfig(uiSettings),
indexPattern,
queries: [query],
filters,
});
useInvalidFilterQuery({ id: ID, filterQuery, kqlError, query, startDate: from, endDate: to });
const [loading, { id, inspect, networkDetails, refetch }] = useNetworkDetails({
docValueFields,
skip: isInitializing,
@ -120,6 +123,12 @@ const NetworkDetailsComponent: React.FC = () => {
ip,
]);
// When the filterQuery comes back as undefined, it means an error has been thrown and the request should be skipped
const shouldSkip = useMemo(() => isInitializing || filterQuery === undefined, [
isInitializing,
filterQuery,
]);
return (
<div data-test-subj="network-details-page">
{indicesExist ? (
@ -174,7 +183,7 @@ const NetworkDetailsComponent: React.FC = () => {
flowTarget={FlowTargetSourceDest.source}
indexNames={selectedPatterns}
ip={ip}
skip={isInitializing}
skip={shouldSkip}
startDate={from}
type={type}
setQuery={setQuery}
@ -189,7 +198,7 @@ const NetworkDetailsComponent: React.FC = () => {
filterQuery={filterQuery}
indexNames={selectedPatterns}
ip={ip}
skip={isInitializing}
skip={shouldSkip}
startDate={from}
type={type}
setQuery={setQuery}
@ -208,7 +217,7 @@ const NetworkDetailsComponent: React.FC = () => {
flowTarget={FlowTargetSourceDest.source}
indexNames={selectedPatterns}
ip={ip}
skip={isInitializing}
skip={shouldSkip}
startDate={from}
type={type}
setQuery={setQuery}
@ -223,7 +232,7 @@ const NetworkDetailsComponent: React.FC = () => {
filterQuery={filterQuery}
indexNames={selectedPatterns}
ip={ip}
skip={isInitializing}
skip={shouldSkip}
startDate={from}
type={type}
setQuery={setQuery}
@ -240,7 +249,7 @@ const NetworkDetailsComponent: React.FC = () => {
flowTarget={flowTarget}
indexNames={selectedPatterns}
ip={ip}
skip={isInitializing}
skip={shouldSkip}
startDate={from}
type={type}
setQuery={setQuery}
@ -253,7 +262,7 @@ const NetworkDetailsComponent: React.FC = () => {
filterQuery={filterQuery}
indexNames={selectedPatterns}
ip={ip}
skip={isInitializing}
skip={shouldSkip}
startDate={from}
type={type}
setQuery={setQuery}
@ -268,7 +277,7 @@ const NetworkDetailsComponent: React.FC = () => {
indexNames={selectedPatterns}
ip={ip}
setQuery={setQuery}
skip={isInitializing}
skip={shouldSkip}
startDate={from}
type={type}
/>
@ -280,7 +289,7 @@ const NetworkDetailsComponent: React.FC = () => {
setQuery={setQuery}
startDate={from}
endDate={to}
skip={isInitializing}
skip={shouldSkip}
indexNames={selectedPatterns}
ip={ip}
type={type}

View file

@ -21,7 +21,7 @@ export interface OwnProps {
type: NetworkType;
startDate: string;
endDate: string;
filterQuery: string | ESTermQuery;
filterQuery?: string | ESTermQuery;
ip: string;
indexNames: string[];
skip: boolean;

View file

@ -51,7 +51,7 @@ import { TimelineId } from '../../../common/types/timeline';
import { timelineDefaults } from '../../timelines/store/timeline/defaults';
import { useSourcererScope } from '../../common/containers/sourcerer';
import { useDeepEqualSelector, useShallowEqualSelector } from '../../common/hooks/use_selector';
import { useInvalidFilterQuery } from '../../common/hooks/use_invalid_filter_query';
/**
* Need a 100% height here to account for the graph/analyze tool, which sets no explicit height parameters, but fills the available space.
*/
@ -61,6 +61,8 @@ const StyledFullHeightContainer = styled.div`
flex: 1 1 auto;
`;
const ID = 'NetworkQueryId';
const NetworkComponent = React.memo<NetworkComponentProps>(
({ hasMlUserPermissions, capabilitiesFetched }) => {
const dispatch = useDispatch();
@ -133,19 +135,21 @@ const NetworkComponent = React.memo<NetworkComponentProps>(
[containerElement, onSkipFocusBeforeEventsTable, onSkipFocusAfterEventsTable]
);
const filterQuery = convertToBuildEsQuery({
const [filterQuery, kqlError] = convertToBuildEsQuery({
config: esQuery.getEsQueryConfig(kibana.services.uiSettings),
indexPattern,
queries: [query],
filters,
});
const tabsFilterQuery = convertToBuildEsQuery({
const [tabsFilterQuery] = convertToBuildEsQuery({
config: esQuery.getEsQueryConfig(kibana.services.uiSettings),
indexPattern,
queries: [query],
filters: tabsFilters,
});
useInvalidFilterQuery({ id: ID, filterQuery, kqlError, query, startDate: from, endDate: to });
return (
<>
{indicesExist ? (
@ -184,7 +188,7 @@ const NetworkComponent = React.memo<NetworkComponentProps>(
indexNames={selectedPatterns}
narrowDateRange={narrowDateRange}
setQuery={setQuery}
skip={isInitializing}
skip={isInitializing || filterQuery === undefined}
to={to}
/>
</Display>

View file

@ -33,6 +33,7 @@ import { GlobalTimeArgs } from '../../../common/containers/use_global_time';
import { SecurityPageName } from '../../../app/types';
import { useFormatUrl } from '../../../common/components/link_to';
import { LinkButton } from '../../../common/components/links';
import { useInvalidFilterQuery } from '../../../common/hooks/use_invalid_filter_query';
const ID = 'alertsByCategoryOverview';
@ -101,7 +102,7 @@ const AlertsByCategoryComponent: React.FC<Props> = ({
[]
);
const filterQuery = useMemo(
const [filterQuery, kqlError] = useMemo(
() =>
convertToBuildEsQuery({
config: esQuery.getEsQueryConfig(uiSettings),
@ -112,6 +113,8 @@ const AlertsByCategoryComponent: React.FC<Props> = ({
[filters, indexPattern, uiSettings, query]
);
useInvalidFilterQuery({ id: ID, filterQuery, kqlError, query, startDate: from, endDate: to });
useEffect(() => {
return () => {
if (deleteQuery) {

View file

@ -9,6 +9,7 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import React, { useMemo } from 'react';
import styled from 'styled-components';
import { ID as OverviewHostQueryId } from '../../containers/overview_host';
import { OverviewHost } from '../overview_host';
import { OverviewNetwork } from '../overview_network';
import { filterHostData } from '../../../hosts/pages/navigation/alerts_query_tab_body';
@ -22,6 +23,7 @@ import {
Query,
} from '../../../../../../../src/plugins/data/public';
import { GlobalTimeArgs } from '../../../common/containers/use_global_time';
import { useInvalidFilterQuery } from '../../../common/hooks/use_invalid_filter_query';
const HorizontalSpacer = styled(EuiFlexItem)`
width: 24px;
@ -45,7 +47,7 @@ const EventCountsComponent: React.FC<Props> = ({
}) => {
const { uiSettings } = useKibana().services;
const hostFilterQuery = useMemo(
const [hostFilterQuery, hostKqlError] = useMemo(
() =>
convertToBuildEsQuery({
config: esQuery.getEsQueryConfig(uiSettings),
@ -56,7 +58,7 @@ const EventCountsComponent: React.FC<Props> = ({
[filters, indexPattern, query, uiSettings]
);
const networkFilterQuery = useMemo(
const [networkFilterQuery] = useMemo(
() =>
convertToBuildEsQuery({
config: esQuery.getEsQueryConfig(uiSettings),
@ -67,6 +69,15 @@ const EventCountsComponent: React.FC<Props> = ({
[filters, indexPattern, uiSettings, query]
);
useInvalidFilterQuery({
id: OverviewHostQueryId,
filterQuery: hostFilterQuery || networkFilterQuery,
kqlError: hostKqlError,
query,
startDate: from,
endDate: to,
});
return (
<EuiFlexGroup gutterSize="none" justifyContent="spaceBetween">
<EuiFlexItem grow={true}>

View file

@ -36,6 +36,7 @@ import * as i18n from '../../pages/translations';
import { SecurityPageName } from '../../../app/types';
import { useFormatUrl } from '../../../common/components/link_to';
import { LinkButton } from '../../../common/components/links';
import { useInvalidFilterQuery } from '../../../common/hooks/use_invalid_filter_query';
const DEFAULT_STACK_BY = 'event.dataset';
@ -116,18 +117,26 @@ const EventsByDatasetComponent: React.FC<Props> = ({
[goToHostEvents, formatUrl]
);
const filterQuery = useMemo(
() =>
combinedQueries == null
? convertToBuildEsQuery({
config: esQuery.getEsQueryConfig(kibana.services.uiSettings),
indexPattern,
queries: [query],
filters,
})
: combinedQueries,
[combinedQueries, kibana, indexPattern, query, filters]
);
const [filterQuery, kqlError] = useMemo(() => {
if (combinedQueries == null) {
return convertToBuildEsQuery({
config: esQuery.getEsQueryConfig(kibana.services.uiSettings),
indexPattern,
queries: [query],
filters,
});
}
return [combinedQueries];
}, [combinedQueries, kibana, indexPattern, query, filters]);
useInvalidFilterQuery({
id: uniqueQueryId,
filterQuery,
kqlError,
query,
startDate: from,
endDate: to,
});
const eventsByDatasetHistogramConfigs: MatrixHistogramConfigs = useMemo(
() => ({
@ -171,6 +180,7 @@ const EventsByDatasetComponent: React.FC<Props> = ({
setAbsoluteRangeDatePickerTarget={setAbsoluteRangeDatePickerTarget}
setQuery={setQuery}
showSpacer={showSpacer}
skip={filterQuery === undefined}
startDate={from}
timelineId={timelineId}
{...eventsByDatasetHistogramConfigs}

View file

@ -51,6 +51,7 @@ const OverviewHostComponent: React.FC<OverviewHostProps> = ({
filterQuery,
indexNames,
startDate,
skip: filterQuery === undefined,
});
const goToHost = useCallback(
@ -117,7 +118,12 @@ const OverviewHostComponent: React.FC<OverviewHostProps> = ({
<EuiFlexItem>
<InspectButtonContainer>
<EuiPanel hasBorder>
<HeaderSection id={OverviewHostQueryId} subtitle={subtitle} title={title}>
<HeaderSection
id={OverviewHostQueryId}
subtitle={subtitle}
title={title}
isInspectDisabled={filterQuery === undefined}
>
<>{hostPageButton}</>
</HeaderSection>

View file

@ -53,6 +53,7 @@ const OverviewNetworkComponent: React.FC<OverviewNetworkProps> = ({
filterQuery,
indexNames,
startDate,
skip: filterQuery === undefined,
});
const goToNetwork = useCallback(
@ -123,7 +124,12 @@ const OverviewNetworkComponent: React.FC<OverviewNetworkProps> = ({
<InspectButtonContainer>
<EuiPanel hasBorder data-test-subj="overview-network-query">
<>
<HeaderSection id={OverviewNetworkQueryId} subtitle={subtitle} title={title}>
<HeaderSection
id={OverviewNetworkQueryId}
subtitle={subtitle}
title={title}
isInspectDisabled={filterQuery === undefined}
>
{networkPageButton}
</HeaderSection>

View file

@ -16,36 +16,38 @@ import { useKibana } from '../../../common/lib/kibana';
export const useRequestEventCounts = (to: string, from: string) => {
const { uiSettings } = useKibana().services;
const [filterQuery] = convertToBuildEsQuery({
config: esQuery.getEsQueryConfig(uiSettings),
indexPattern: {
fields: [
{
name: 'event.kind',
aggregatable: true,
searchable: true,
type: 'string',
esTypes: ['keyword'],
},
],
title: 'filebeat-*',
},
queries: [{ query: 'event.type:indicator', language: 'kuery' }],
filters: [],
});
const matrixHistogramRequest = useMemo(() => {
return {
endDate: to,
errorMessage: i18n.translate('xpack.securitySolution.overview.errorFetchingEvents', {
defaultMessage: 'Error fetching events',
}),
filterQuery: convertToBuildEsQuery({
config: esQuery.getEsQueryConfig(uiSettings),
indexPattern: {
fields: [
{
name: 'event.kind',
aggregatable: true,
searchable: true,
type: 'string',
esTypes: ['keyword'],
},
],
title: 'filebeat-*',
},
queries: [{ query: 'event.type:indicator', language: 'kuery' }],
filters: [],
}),
filterQuery,
histogramType: MatrixHistogramType.events,
indexNames: DEFAULT_CTI_SOURCE_INDEX,
stackByField: EVENT_DATASET,
startDate: from,
size: 0,
};
}, [to, from, uiSettings]);
}, [to, from, filterQuery]);
const results = useMatrixHistogram(matrixHistogramRequest);

View file

@ -74,7 +74,7 @@ export const useNetworkOverview = ({
const overviewNetworkSearch = useCallback(
(request: NetworkOverviewRequestOptions | null) => {
if (request == null) {
if (request == null || skip) {
return;
}
@ -118,7 +118,7 @@ export const useNetworkOverview = ({
asyncSearch();
refetch.current = asyncSearch;
},
[data.search, addError, addWarning]
[data.search, addError, addWarning, skip]
);
useEffect(() => {

View file

@ -69,6 +69,9 @@ interface FlyoutHeaderPanelProps {
const FlyoutHeaderPanelComponent: React.FC<FlyoutHeaderPanelProps> = ({ timelineId }) => {
const dispatch = useDispatch();
const { indexPattern, browserFields } = useSourcererScope(SourcererScopeName.timeline);
const { uiSettings } = useKibana().services;
const esQueryConfig = useMemo(() => esQuery.getEsQueryConfig(uiSettings), [uiSettings]);
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
const {
activeTab,
@ -79,6 +82,8 @@ const FlyoutHeaderPanelComponent: React.FC<FlyoutHeaderPanelProps> = ({ timeline
status: timelineStatus,
updated,
show,
filters,
kqlMode,
} = useDeepEqualSelector((state) =>
pick(
[
@ -90,6 +95,8 @@ const FlyoutHeaderPanelComponent: React.FC<FlyoutHeaderPanelProps> = ({ timeline
'timelineType',
'updated',
'show',
'filters',
'kqlMode',
],
getTimeline(state, timelineId) ?? timelineDefaults
)
@ -98,6 +105,30 @@ const FlyoutHeaderPanelComponent: React.FC<FlyoutHeaderPanelProps> = ({ timeline
() => !isEmpty(dataProviders) || !isEmpty(get('filterQuery.kuery.expression', kqlQuery)),
[dataProviders, kqlQuery]
);
const getKqlQueryTimeline = useMemo(() => timelineSelectors.getKqlFilterQuerySelector(), []);
const kqlQueryTimeline = useSelector((state: State) => getKqlQueryTimeline(state, timelineId)!);
const kqlQueryExpression =
isEmpty(dataProviders) && isEmpty(kqlQueryTimeline) && timelineType === 'template'
? ' '
: kqlQueryTimeline;
const kqlQueryTest = useMemo(() => ({ query: kqlQueryExpression, language: 'kuery' }), [
kqlQueryExpression,
]);
const combinedQueries = useMemo(
() =>
combineQueries({
config: esQueryConfig,
dataProviders,
indexPattern,
browserFields,
filters: filters ? filters : [],
kqlQuery: kqlQueryTest,
kqlMode,
}),
[browserFields, dataProviders, esQueryConfig, filters, indexPattern, kqlMode, kqlQueryTest]
);
const handleClose = useCallback(() => {
dispatch(timelineActions.showTimeline({ id: timelineId, show: false }));
@ -134,7 +165,7 @@ const FlyoutHeaderPanelComponent: React.FC<FlyoutHeaderPanelProps> = ({ timeline
queryId={`${timelineId}-${activeTab}`}
inputId="timeline"
inspectIndex={0}
isDisabled={!isDataInTimeline}
isDisabled={!isDataInTimeline || combinedQueries?.filterQuery === undefined}
title={i18n.INSPECT_TIMELINE_TITLE}
/>
</EuiFlexItem>
@ -295,10 +326,6 @@ const FlyoutHeaderComponent: React.FC<FlyoutHeaderProps> = ({ timelineId }) => {
kqlQueryExpression,
]);
const isBlankTimeline: boolean = useMemo(
() => isEmpty(dataProviders) && isEmpty(filters) && isEmpty(kqlQuery.query),
[dataProviders, filters, kqlQuery]
);
const combinedQueries = useMemo(
() =>
combineQueries({
@ -312,6 +339,14 @@ const FlyoutHeaderComponent: React.FC<FlyoutHeaderProps> = ({ timelineId }) => {
}),
[browserFields, dataProviders, esQueryConfig, filters, indexPattern, kqlMode, kqlQuery]
);
const isBlankTimeline: boolean = useMemo(
() =>
(isEmpty(dataProviders) && isEmpty(filters) && isEmpty(kqlQuery.query)) ||
combinedQueries?.filterQuery === undefined,
[dataProviders, filters, kqlQuery, combinedQueries]
);
const [loading, kpis] = useTimelineKpis({
defaultIndex: selectedPatterns,
docValueFields,

View file

@ -10,6 +10,7 @@ import { i18n } from '@kbn/i18n';
import styled from 'styled-components';
import React, { useCallback, useMemo } from 'react';
import { useDispatch } from 'react-redux';
import { useInvalidFilterQuery } from '../../../../common/hooks/use_invalid_filter_query';
import { FlowTarget } from '../../../../../common/search_strategy';
import { NetworkDetailsLink } from '../../../../common/components/links';
import { IpOverview } from '../../../../network/components/details';
@ -97,7 +98,7 @@ export const ExpandableNetworkDetails = ({
} = useKibana();
const { docValueFields, indicesExist, indexPattern, selectedPatterns } = useSourcererScope();
const filterQuery = convertToBuildEsQuery({
const [filterQuery, kqlError] = convertToBuildEsQuery({
config: esQuery.getEsQueryConfig(uiSettings),
indexPattern,
queries: [query],
@ -106,12 +107,14 @@ export const ExpandableNetworkDetails = ({
const [loading, { id, networkDetails }] = useNetworkDetails({
docValueFields,
skip: isInitializing,
skip: isInitializing || filterQuery === undefined,
filterQuery,
indexNames: selectedPatterns,
ip,
});
useInvalidFilterQuery({ id, filterQuery, kqlError, query, startDate: from, endDate: to });
const [isLoadingAnomaliesData, anomaliesData] = useAnomaliesTableData({
criteriaFields: networkToCriteria(ip, flowTarget),
startDate: from,

View file

@ -305,7 +305,7 @@ describe('Combined Queries', () => {
test('Only Data Provider', () => {
const dataProviders = cloneDeep(mockDataProviders.slice(0, 1));
const { filterQuery } = combineQueries({
const { filterQuery, kqlError } = combineQueries({
config,
dataProviders,
indexPattern: mockIndexPattern,
@ -317,13 +317,14 @@ describe('Combined Queries', () => {
expect(filterQuery).toEqual(
'{"bool":{"must":[],"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}}],"should":[],"must_not":[]}}'
);
expect(kqlError).toBeUndefined();
});
test('Only Data Provider with timestamp (string input)', () => {
const dataProviders = cloneDeep(mockDataProviders.slice(0, 1));
dataProviders[0].queryMatch.field = '@timestamp';
dataProviders[0].queryMatch.value = '2018-03-23T23:36:23.232Z';
const { filterQuery } = combineQueries({
const { filterQuery, kqlError } = combineQueries({
config,
dataProviders,
indexPattern: mockIndexPattern,
@ -335,13 +336,14 @@ describe('Combined Queries', () => {
expect(filterQuery).toMatchInlineSnapshot(
`"{\\"bool\\":{\\"must\\":[],\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"range\\":{\\"@timestamp\\":{\\"gte\\":\\"1521848183232\\",\\"lte\\":\\"1521848183232\\"}}}],\\"minimum_should_match\\":1}}],\\"should\\":[],\\"must_not\\":[]}}"`
);
expect(kqlError).toBeUndefined();
});
test('Only Data Provider with timestamp (numeric input)', () => {
const dataProviders = cloneDeep(mockDataProviders.slice(0, 1));
dataProviders[0].queryMatch.field = '@timestamp';
dataProviders[0].queryMatch.value = 1521848183232;
const { filterQuery } = combineQueries({
const { filterQuery, kqlError } = combineQueries({
config,
dataProviders,
indexPattern: mockIndexPattern,
@ -353,13 +355,14 @@ describe('Combined Queries', () => {
expect(filterQuery).toMatchInlineSnapshot(
`"{\\"bool\\":{\\"must\\":[],\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"range\\":{\\"@timestamp\\":{\\"gte\\":\\"1521848183232\\",\\"lte\\":\\"1521848183232\\"}}}],\\"minimum_should_match\\":1}}],\\"should\\":[],\\"must_not\\":[]}}"`
);
expect(kqlError).toBeUndefined();
});
test('Only Data Provider with a date type (string input)', () => {
const dataProviders = cloneDeep(mockDataProviders.slice(0, 1));
dataProviders[0].queryMatch.field = 'event.end';
dataProviders[0].queryMatch.value = '2018-03-23T23:36:23.232Z';
const { filterQuery } = combineQueries({
const { filterQuery, kqlError } = combineQueries({
config,
dataProviders,
indexPattern: mockIndexPattern,
@ -371,13 +374,14 @@ describe('Combined Queries', () => {
expect(filterQuery).toMatchInlineSnapshot(
`"{\\"bool\\":{\\"must\\":[],\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"event.end\\":\\"1521848183232\\"}}],\\"minimum_should_match\\":1}}],\\"should\\":[],\\"must_not\\":[]}}"`
);
expect(kqlError).toBeUndefined();
});
test('Only Data Provider with date type (numeric input)', () => {
const dataProviders = cloneDeep(mockDataProviders.slice(0, 1));
dataProviders[0].queryMatch.field = 'event.end';
dataProviders[0].queryMatch.value = 1521848183232;
const { filterQuery } = combineQueries({
const { filterQuery, kqlError } = combineQueries({
config,
dataProviders,
indexPattern: mockIndexPattern,
@ -389,10 +393,11 @@ describe('Combined Queries', () => {
expect(filterQuery).toMatchInlineSnapshot(
`"{\\"bool\\":{\\"must\\":[],\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"event.end\\":\\"1521848183232\\"}}],\\"minimum_should_match\\":1}}],\\"should\\":[],\\"must_not\\":[]}}"`
);
expect(kqlError).toBeUndefined();
});
test('Only KQL search/filter query', () => {
const { filterQuery } = combineQueries({
const { filterQuery, kqlError } = combineQueries({
config,
dataProviders: [],
indexPattern: mockIndexPattern,
@ -404,11 +409,26 @@ describe('Combined Queries', () => {
expect(filterQuery).toEqual(
'{"bool":{"must":[],"filter":[{"bool":{"should":[{"match_phrase":{"host.name":"host-1"}}],"minimum_should_match":1}}],"should":[],"must_not":[]}}'
);
expect(kqlError).toBeUndefined();
});
test('Invalid KQL search/filter query', () => {
const { filterQuery, kqlError } = combineQueries({
config,
dataProviders: [],
indexPattern: mockIndexPattern,
browserFields: mockBrowserFields,
filters: [],
kqlQuery: { query: 'host.name: "host-1', language: 'kuery' },
kqlMode: 'search',
})!;
expect(filterQuery).toBeUndefined();
expect(kqlError).toBeDefined(); // Not testing on the error message since we don't control changes to them
});
test('Data Provider & KQL search query', () => {
const dataProviders = cloneDeep(mockDataProviders.slice(0, 1));
const { filterQuery } = combineQueries({
const { filterQuery, kqlError } = combineQueries({
config,
dataProviders,
indexPattern: mockIndexPattern,
@ -420,11 +440,12 @@ describe('Combined Queries', () => {
expect(filterQuery).toEqual(
'{"bool":{"must":[],"filter":[{"bool":{"should":[{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"host.name":"host-1"}}],"minimum_should_match":1}}],"minimum_should_match":1}}],"should":[],"must_not":[]}}'
);
expect(kqlError).toBeUndefined();
});
test('Data Provider & KQL filter query', () => {
const dataProviders = cloneDeep(mockDataProviders.slice(0, 1));
const { filterQuery } = combineQueries({
const { filterQuery, kqlError } = combineQueries({
config,
dataProviders,
indexPattern: mockIndexPattern,
@ -436,13 +457,14 @@ describe('Combined Queries', () => {
expect(filterQuery).toEqual(
'{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"host.name":"host-1"}}],"minimum_should_match":1}}]}}],"should":[],"must_not":[]}}'
);
expect(kqlError).toBeUndefined();
});
test('Data Provider & KQL search query multiple', () => {
const dataProviders = cloneDeep(mockDataProviders.slice(0, 2));
dataProviders[0].and = cloneDeep(mockDataProviders.slice(2, 4));
dataProviders[1].and = cloneDeep(mockDataProviders.slice(4, 5));
const { filterQuery } = combineQueries({
const { filterQuery, kqlError } = combineQueries({
config,
dataProviders,
indexPattern: mockIndexPattern,
@ -454,13 +476,14 @@ describe('Combined Queries', () => {
expect(filterQuery).toMatchInlineSnapshot(
`"{\\"bool\\":{\\"must\\":[],\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 1\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 3\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 4\\"}}],\\"minimum_should_match\\":1}}]}},{\\"bool\\":{\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 2\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 5\\"}}],\\"minimum_should_match\\":1}}]}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"host.name\\":\\"host-1\\"}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}}],\\"should\\":[],\\"must_not\\":[]}}"`
);
expect(kqlError).toBeUndefined();
});
test('Data Provider & KQL filter query multiple', () => {
const dataProviders = cloneDeep(mockDataProviders.slice(0, 2));
dataProviders[0].and = cloneDeep(mockDataProviders.slice(2, 4));
dataProviders[1].and = cloneDeep(mockDataProviders.slice(4, 5));
const { filterQuery } = combineQueries({
const { filterQuery, kqlError } = combineQueries({
config,
dataProviders,
indexPattern: mockIndexPattern,
@ -472,6 +495,7 @@ describe('Combined Queries', () => {
expect(filterQuery).toMatchInlineSnapshot(
`"{\\"bool\\":{\\"must\\":[],\\"filter\\":[{\\"bool\\":{\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 1\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 3\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 4\\"}}],\\"minimum_should_match\\":1}}]}},{\\"bool\\":{\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 2\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 5\\"}}],\\"minimum_should_match\\":1}}]}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"host.name\\":\\"host-1\\"}}],\\"minimum_should_match\\":1}}]}}],\\"should\\":[],\\"must_not\\":[]}}"`
);
expect(kqlError).toBeUndefined();
});
test('Data Provider & kql filter query with nested field that exists', () => {

View file

@ -161,27 +161,48 @@ export const combineQueries = ({
kqlQuery: Query;
kqlMode: string;
isEventViewer?: boolean;
}): { filterQuery: string } | null => {
}): { filterQuery?: string; kqlError?: Error } | null => {
const kuery: Query = { query: '', language: kqlQuery.language };
if (isEmpty(dataProviders) && isEmpty(kqlQuery.query) && isEmpty(filters) && !isEventViewer) {
return null;
} else if (isEmpty(dataProviders) && isEmpty(kqlQuery.query) && isEventViewer) {
} else if (
isEmpty(dataProviders) &&
isEmpty(kqlQuery.query) &&
(isEventViewer || !isEmpty(filters))
) {
const [filterQuery, kqlError] = convertToBuildEsQuery({
config,
queries: [kuery],
indexPattern,
filters,
});
return {
filterQuery: convertToBuildEsQuery({ config, queries: [kuery], indexPattern, filters }),
};
} else if (isEmpty(dataProviders) && isEmpty(kqlQuery.query) && !isEmpty(filters)) {
return {
filterQuery: convertToBuildEsQuery({ config, queries: [kuery], indexPattern, filters }),
filterQuery,
kqlError,
};
} else if (isEmpty(dataProviders) && !isEmpty(kqlQuery.query)) {
kuery.query = `(${kqlQuery.query})`;
const [filterQuery, kqlError] = convertToBuildEsQuery({
config,
queries: [kuery],
indexPattern,
filters,
});
return {
filterQuery: convertToBuildEsQuery({ config, queries: [kuery], indexPattern, filters }),
filterQuery,
kqlError,
};
} else if (!isEmpty(dataProviders) && isEmpty(kqlQuery)) {
kuery.query = `(${buildGlobalQuery(dataProviders, browserFields)})`;
const [filterQuery, kqlError] = convertToBuildEsQuery({
config,
queries: [kuery],
indexPattern,
filters,
});
return {
filterQuery: convertToBuildEsQuery({ config, queries: [kuery], indexPattern, filters }),
filterQuery,
kqlError,
};
}
const operatorKqlQuery = kqlMode === 'filter' ? 'and' : 'or';
@ -189,8 +210,15 @@ export const combineQueries = ({
kuery.query = `((${buildGlobalQuery(dataProviders, browserFields)})${postpend(
kqlQuery.query as string
)})`;
const [filterQuery, kqlError] = convertToBuildEsQuery({
config,
queries: [kuery],
indexPattern,
filters,
});
return {
filterQuery: convertToBuildEsQuery({ config, queries: [kuery], indexPattern, filters }),
filterQuery,
kqlError,
};
};

View file

@ -21,6 +21,7 @@ import { connect, ConnectedProps, useDispatch } from 'react-redux';
import deepEqual from 'fast-deep-equal';
import { InPortal } from 'react-reverse-portal';
import { useInvalidFilterQuery } from '../../../../common/hooks/use_invalid_filter_query';
import { timelineActions, timelineSelectors } from '../../../store/timeline';
import { CellValueElementProps } from '../cell_rendering';
import { Direction, TimelineItem } from '../../../../../common/search_strategy';
@ -197,7 +198,7 @@ export const QueryTabContentComponent: React.FC<Props> = ({
const kqlQuery: {
query: string;
language: KueryFilterQueryKind;
} = { query: kqlQueryExpression, language: 'kuery' };
} = useMemo(() => ({ query: kqlQueryExpression, language: 'kuery' }), [kqlQueryExpression]);
const combinedQueries = combineQueries({
config: esQueryConfig,
@ -209,6 +210,15 @@ export const QueryTabContentComponent: React.FC<Props> = ({
kqlMode,
});
useInvalidFilterQuery({
id: timelineId,
filterQuery: combinedQueries?.filterQuery,
kqlError: combinedQueries?.kqlError,
query: kqlQuery,
startDate: start,
endDate: end,
});
const isBlankTimeline: boolean =
isEmpty(dataProviders) && isEmpty(filters) && isEmpty(kqlQuery.query);
@ -252,9 +262,9 @@ export const QueryTabContentComponent: React.FC<Props> = ({
fields: getTimelineQueryFields(),
language: kqlQuery.language,
limit: itemsPerPage,
filterQuery: combinedQueries?.filterQuery ?? '',
filterQuery: combinedQueries?.filterQuery,
startDate: start,
skip: !canQueryTimeline(),
skip: !canQueryTimeline() || combinedQueries?.filterQuery === undefined,
sort: timelineQuerySortField,
timerangeKind,
});