[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:
parent
3ac067fc91
commit
36b21b4007
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -75,6 +75,7 @@ const AnomaliesHostTableComponent: React.FC<AnomaliesHostTableProps> = ({
|
|||
)}`}
|
||||
title={i18n.ANOMALIES}
|
||||
tooltip={i18n.TOOLTIP}
|
||||
isInspectDisabled={skip}
|
||||
/>
|
||||
|
||||
<BasicTable
|
||||
|
|
|
@ -65,6 +65,7 @@ const AnomaliesNetworkTableComponent: React.FC<AnomaliesNetworkTableProps> = ({
|
|||
)}`}
|
||||
title={i18n.ANOMALIES}
|
||||
tooltip={i18n.TOOLTIP}
|
||||
isInspectDisabled={skip}
|
||||
/>
|
||||
|
||||
<BasicTable
|
||||
|
|
|
@ -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, [
|
||||
|
|
|
@ -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]);
|
||||
};
|
|
@ -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];
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -18,6 +18,8 @@ export interface Error {
|
|||
id: string;
|
||||
title: string;
|
||||
message: string[];
|
||||
hash?: string;
|
||||
displayError?: boolean;
|
||||
}
|
||||
|
||||
export type ErrorModel = Error[];
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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[];
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -70,7 +70,7 @@ export const HostDetailsTabs = React.memo<HostDetailsTabsProps>(
|
|||
deleteQuery,
|
||||
endDate: to,
|
||||
filterQuery,
|
||||
skip: isInitializing,
|
||||
skip: isInitializing || filterQuery === undefined,
|
||||
setQuery,
|
||||
startDate: from,
|
||||
type,
|
||||
|
|
|
@ -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]);
|
||||
|
|
|
@ -61,7 +61,7 @@ export type HostDetailsTabsProps = HostBodyComponentDispatchProps &
|
|||
docValueFields?: DocValueFields[];
|
||||
indexNames: string[];
|
||||
pageFilters?: Filter[];
|
||||
filterQuery: string;
|
||||
filterQuery?: string;
|
||||
indexPattern: IIndexPattern;
|
||||
type: hostsModel.HostsType;
|
||||
};
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -69,7 +69,7 @@ export const HostsTabs = memo<HostsTabsProps>(
|
|||
endDate: to,
|
||||
filterQuery,
|
||||
indexNames,
|
||||
skip: isInitializing,
|
||||
skip: isInitializing || filterQuery === undefined,
|
||||
setQuery,
|
||||
startDate: from,
|
||||
type,
|
||||
|
|
|
@ -44,7 +44,7 @@ export type HostsComponentsQueryProps = QueryTabBodyProps & {
|
|||
};
|
||||
|
||||
export type AlertsComponentQueryProps = HostsComponentsQueryProps & {
|
||||
filterQuery: string;
|
||||
filterQuery?: string;
|
||||
pageFilters?: Filter[];
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue