[Security Solution] Add runtime field support to query signals route (#122738) (#123270)

* Add runtime field support to query signals route

* Fix tests

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
(cherry picked from commit 3a59ba4b21)

Co-authored-by: Marshall Main <55718608+marshallmain@users.noreply.github.com>
This commit is contained in:
Kibana Machine 2022-01-18 14:35:52 -05:00 committed by GitHub
parent 18d3e54bcf
commit f489182683
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 93 additions and 16 deletions

View file

@ -15,6 +15,7 @@ export const querySignalsSchema = t.exact(
size: PositiveInteger,
track_total_hits: t.boolean,
_source: t.array(t.string),
runtime_mappings: t.unknown,
})
);

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../../common/constants';
import type { AlertsStackByField } from '../common/types';
@ -14,7 +15,8 @@ export const getAlertsCountQuery = (
to: string,
additionalFilters: Array<{
bool: { filter: unknown[]; should: unknown[]; must_not: unknown[]; must: unknown[] };
}> = []
}> = [],
runtimeMappings?: MappingRuntimeFields
) => {
return {
size: 0,
@ -44,5 +46,6 @@ export const getAlertsCountQuery = (
],
},
},
runtime_mappings: runtimeMappings,
};
};

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/types';
import React, { memo, useMemo, useState, useEffect } from 'react';
import uuid from 'uuid';
@ -31,10 +32,11 @@ interface AlertsCountPanelProps {
filters?: Filter[];
query?: Query;
signalIndexName: string | null;
runtimeMappings?: MappingRuntimeFields;
}
export const AlertsCountPanel = memo<AlertsCountPanelProps>(
({ filters, query, signalIndexName }) => {
({ filters, query, signalIndexName, runtimeMappings }) => {
const { to, from, deleteQuery, setQuery } = useGlobalTime();
// create a unique, but stable (across re-renders) query id
@ -72,13 +74,21 @@ export const AlertsCountPanel = memo<AlertsCountPanelProps>(
request,
refetch,
} = useQueryAlerts<{}, AlertsCountAggregation>({
query: getAlertsCountQuery(selectedStackByOption, from, to, additionalFilters),
query: getAlertsCountQuery(
selectedStackByOption,
from,
to,
additionalFilters,
runtimeMappings
),
indexName: signalIndexName,
});
useEffect(() => {
setAlertsQuery(getAlertsCountQuery(selectedStackByOption, from, to, additionalFilters));
}, [setAlertsQuery, selectedStackByOption, from, to, additionalFilters]);
setAlertsQuery(
getAlertsCountQuery(selectedStackByOption, from, to, additionalFilters, runtimeMappings)
);
}, [setAlertsQuery, selectedStackByOption, from, to, additionalFilters, runtimeMappings]);
useInspectButton({
setQuery,

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { isEmpty } from 'lodash/fp';
import moment from 'moment';
@ -38,7 +39,8 @@ export const getAlertsHistogramQuery = (
to: string,
additionalFilters: Array<{
bool: { filter: unknown[]; should: unknown[]; must_not: unknown[]; must: unknown[] };
}>
}>,
runtimeMappings?: MappingRuntimeFields
) => {
return {
aggs: {
@ -80,6 +82,7 @@ export const getAlertsHistogramQuery = (
],
},
},
runtime_mappings: runtimeMappings,
};
};

View file

@ -184,6 +184,7 @@ describe('AlertsHistogramPanel', () => {
},
},
],
undefined,
]);
});
wrapper.unmount();
@ -237,6 +238,7 @@ describe('AlertsHistogramPanel', () => {
},
},
],
undefined,
]);
});
wrapper.unmount();

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/types';
import type { Position } from '@elastic/charts';
import { EuiFlexGroup, EuiFlexItem, EuiTitleSize } from '@elastic/eui';
import numeral from '@elastic/numeral';
@ -76,6 +77,7 @@ interface AlertsHistogramPanelProps {
timelineId?: string;
title?: string;
updateDateRange: UpdateDateRange;
runtimeMappings?: MappingRuntimeFields;
}
const NO_LEGEND_DATA: LegendItem[] = [];
@ -100,6 +102,7 @@ export const AlertsHistogramPanel = memo<AlertsHistogramPanelProps>(
title = i18n.HISTOGRAM_HEADER,
updateDateRange,
titleSize = 'm',
runtimeMappings,
}) => {
const { to, from, deleteQuery, setQuery } = useGlobalTime(false);
@ -125,7 +128,8 @@ export const AlertsHistogramPanel = memo<AlertsHistogramPanelProps>(
selectedStackByOption,
from,
to,
buildCombinedQueries(combinedQueries)
buildCombinedQueries(combinedQueries),
runtimeMappings
),
indexName: signalIndexName,
});
@ -231,15 +235,18 @@ export const AlertsHistogramPanel = memo<AlertsHistogramPanelProps>(
selectedStackByOption,
from,
to,
!isEmpty(converted) ? [converted] : []
!isEmpty(converted) ? [converted] : [],
runtimeMappings
)
);
} catch (e) {
setIsInspectDisabled(true);
setAlertsQuery(getAlertsHistogramQuery(selectedStackByOption, from, to, []));
setAlertsQuery(
getAlertsHistogramQuery(selectedStackByOption, from, to, [], runtimeMappings)
);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedStackByOption, from, to, query, filters, combinedQueries]);
}, [selectedStackByOption, from, to, query, filters, combinedQueries, runtimeMappings]);
const linkButton = useMemo(() => {
if (showLinkToAlerts) {

View file

@ -130,10 +130,17 @@ const DetectionEnginePageComponent: React.FC<DetectionEngineComponentProps> = ({
] = useUserData();
const { loading: listsConfigLoading, needsConfiguration: needsListsConfiguration } =
useListsConfig();
const {
indexPattern,
runtimeMappings,
loading: isLoadingIndexPattern,
} = useSourcererDataView(SourcererScopeName.detections);
const { formatUrl } = useFormatUrl(SecurityPageName.rules);
const [showBuildingBlockAlerts, setShowBuildingBlockAlerts] = useState(false);
const [showOnlyThreatIndicatorAlerts, setShowOnlyThreatIndicatorAlerts] = useState(false);
const loading = userInfoLoading || listsConfigLoading;
const loading = userInfoLoading || listsConfigLoading || isLoadingIndexPattern;
const {
application: { navigateToUrl },
timelines: timelinesUi,
@ -212,8 +219,6 @@ const DetectionEnginePageComponent: React.FC<DetectionEngineComponentProps> = ({
[setShowOnlyThreatIndicatorAlerts]
);
const { indexPattern } = useSourcererDataView(SourcererScopeName.detections);
const { signalIndexNeedsInit, pollForSignalIndex } = useSignalHelpers();
const onSkipFocusBeforeEventsTable = useCallback(() => {
@ -340,6 +345,7 @@ const DetectionEnginePageComponent: React.FC<DetectionEngineComponentProps> = ({
filters={alertsHistogramDefaultFilters}
query={query}
signalIndexName={signalIndexName}
runtimeMappings={runtimeMappings}
/>
</EuiFlexItem>
<EuiFlexItem grow={2}>
@ -351,6 +357,7 @@ const DetectionEnginePageComponent: React.FC<DetectionEngineComponentProps> = ({
titleSize={'s'}
signalIndexName={signalIndexName}
updateDateRange={updateDateRangeCallback}
runtimeMappings={runtimeMappings}
/>
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -208,7 +208,14 @@ const RuleDetailsPageComponent: React.FC<DetectionEngineComponentProps> = ({
] = useUserData();
const { loading: listsConfigLoading, needsConfiguration: needsListsConfiguration } =
useListsConfig();
const loading = userInfoLoading || listsConfigLoading;
const {
indexPattern,
runtimeMappings,
loading: isLoadingIndexPattern,
} = useSourcererDataView(SourcererScopeName.detections);
const loading = userInfoLoading || listsConfigLoading || isLoadingIndexPattern;
const { detailName: ruleId } = useParams<{ detailName: string }>();
const {
rule: maybeRule,
@ -601,7 +608,6 @@ const RuleDetailsPageComponent: React.FC<DetectionEngineComponentProps> = ({
[setShowOnlyThreatIndicatorAlerts]
);
const { indexPattern } = useSourcererDataView(SourcererScopeName.detections);
const exceptionLists = useMemo((): {
lists: ExceptionListIdentifiers[];
allowedExceptionListTypes: ExceptionListTypeEnum[];
@ -819,6 +825,7 @@ const RuleDetailsPageComponent: React.FC<DetectionEngineComponentProps> = ({
signalIndexName={signalIndexName}
defaultStackByOption={defaultRuleStackByOption}
updateDateRange={updateDateRangeCallback}
runtimeMappings={runtimeMappings}
/>
<EuiSpacer />
</Display>

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { transformError } from '@kbn/securitysolution-es-utils';
import type { SecuritySolutionPluginRouter } from '../../../../types';
import { DETECTION_ENGINE_QUERY_SIGNALS_URL } from '../../../../../common/constants';
@ -35,7 +36,7 @@ export const querySignalsRoute = (
},
async (context, request, response) => {
// eslint-disable-next-line @typescript-eslint/naming-convention
const { query, aggs, _source, track_total_hits, size } = request.body;
const { query, aggs, _source, track_total_hits, size, runtime_mappings } = request.body;
const siemResponse = buildSiemResponse(response);
if (
query == null &&
@ -61,6 +62,7 @@ export const querySignalsRoute = (
_source,
track_total_hits,
size,
runtime_mappings: runtime_mappings as MappingRuntimeFields,
},
ignore_unavailable: true,
});

View file

@ -106,6 +106,41 @@ export default ({ getService }: FtrProviderContext) => {
});
});
describe('runtime fields', () => {
before(async () => {
await esArchiver.load('x-pack/test/functional/es_archives/endpoint/resolver/signals');
await createSignalsIndex(supertest, log);
});
after(async () => {
await esArchiver.unload('x-pack/test/functional/es_archives/endpoint/resolver/signals');
await deleteSignalsIndex(supertest, log);
});
it('should be able to filter using a runtime field defined in the request', async () => {
const query = {
query: {
bool: {
should: [{ match_phrase: { signal_status_querytime: 'open' } }],
},
},
runtime_mappings: {
signal_status_querytime: {
type: 'keyword',
script: {
source: `emit(doc['signal.status'].value)`,
},
},
},
};
const { body } = await supertest
.post(DETECTION_ENGINE_QUERY_SIGNALS_URL)
.set('kbn-xsrf', 'true')
.send(query)
.expect(200);
expect(body.hits.total.value).to.eql(3);
});
});
describe('find_alerts_route', () => {
describe('validation checks', () => {
it('should not give errors when querying and the signals index does not exist yet', async () => {