Skip to content

Commit 17ba3ab

Browse files
authored
[8.x] [Security Solution][Detection Engine] Split search request building from search (elastic#216887) (elastic#218262)
# Backport This will backport the following commits from `main` to `8.x`: - [[Security Solution][Detection Engine] Split search request building from search (elastic#216887)](elastic#216887) <!--- Backport version: 9.6.6 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport) <!--BACKPORT [{"author":{"name":"Marshall Main","email":"[email protected]"},"sourceCommit":{"committedDate":"2025-04-15T12:19:34Z","message":"[Security Solution][Detection Engine] Split search request building from search (elastic#216887)\n\n## Summary\n\nThis PR better separates the request building logic in the detection\nengine from query building logic, removes outdated error checking logic,\nupdates the `singleSearchAfter` `search` call to no longer use the\nlegacy `meta: true` param, and improves search response type inference.","sha":"dee4dfbe5995614b82792b692775c150dc79635e","branchLabelMapping":{"^v9.1.0$":"main","^v8.19.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","Team:Detection Engine","backport:version","v9.1.0","v8.19.0"],"title":"[Security Solution][Detection Engine] Split search request building from search","number":216887,"url":"https://github.com/elastic/kibana/pull/216887","mergeCommit":{"message":"[Security Solution][Detection Engine] Split search request building from search (elastic#216887)\n\n## Summary\n\nThis PR better separates the request building logic in the detection\nengine from query building logic, removes outdated error checking logic,\nupdates the `singleSearchAfter` `search` call to no longer use the\nlegacy `meta: true` param, and improves search response type inference.","sha":"dee4dfbe5995614b82792b692775c150dc79635e"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.1.0","branchLabelMappingKey":"^v9.1.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/216887","number":216887,"mergeCommit":{"message":"[Security Solution][Detection Engine] Split search request building from search (elastic#216887)\n\n## Summary\n\nThis PR better separates the request building logic in the detection\nengine from query building logic, removes outdated error checking logic,\nupdates the `singleSearchAfter` `search` call to no longer use the\nlegacy `meta: true` param, and improves search response type inference.","sha":"dee4dfbe5995614b82792b692775c150dc79635e"}},{"branch":"8.x","label":"v8.19.0","branchLabelMappingKey":"^v8.19.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}] BACKPORT-->
1 parent 1db9686 commit 17ba3ab

25 files changed

+768
-1407
lines changed

x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/es_results.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -436,7 +436,7 @@ export const sampleDocRiskScore = (riskScore?: unknown): SignalSourceHit => ({
436436
sort: [],
437437
});
438438

439-
export const sampleEmptyDocSearchResults = (): SignalSearchResponse => ({
439+
export const sampleEmptyDocSearchResults = () => ({
440440
took: 10,
441441
timed_out: false,
442442
_shards: {
@@ -446,7 +446,10 @@ export const sampleEmptyDocSearchResults = (): SignalSearchResponse => ({
446446
skipped: 0,
447447
},
448448
hits: {
449-
total: 0,
449+
total: {
450+
value: 0,
451+
relation: 'eq' as const,
452+
},
450453
max_score: 100,
451454
hits: [],
452455
},

x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/get_event_count.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export const getEventList = async ({
2222
eventListConfig,
2323
indexFields,
2424
sortOrder = 'desc',
25-
}: EventsOptions): Promise<estypes.SearchResponse<EventDoc>> => {
25+
}: EventsOptions): Promise<estypes.SearchResponse<EventDoc, unknown>> => {
2626
const {
2727
inputIndex,
2828
ruleExecutionLogger,
@@ -53,14 +53,13 @@ export const getEventList = async ({
5353
fields: indexFields,
5454
});
5555

56-
const { searchResult } = await singleSearchAfter({
56+
const searchRequest = buildEventsSearchQuery({
57+
aggregations: undefined,
5758
searchAfterSortIds: searchAfter,
5859
index: inputIndex,
5960
from: tuple.from.toISOString(),
6061
to: tuple.to.toISOString(),
61-
services,
62-
ruleExecutionLogger,
63-
pageSize: calculatedPerPage,
62+
size: calculatedPerPage,
6463
filter: queryFilter,
6564
primaryTimestamp,
6665
secondaryTimestamp,
@@ -70,6 +69,12 @@ export const getEventList = async ({
7069
overrideBody: eventListConfig,
7170
});
7271

72+
const { searchResult } = await singleSearchAfter({
73+
searchRequest,
74+
services,
75+
ruleExecutionLogger,
76+
});
77+
7378
ruleExecutionLogger.debug(`Retrieved events items of size: ${searchResult.hits.hits.length}`);
7479
return searchResult;
7580
};
@@ -98,6 +103,7 @@ export const getEventCount = async ({
98103
fields: indexFields,
99104
});
100105
const eventSearchQueryBodyQuery = buildEventsSearchQuery({
106+
aggregations: undefined,
101107
index,
102108
from: tuple.from.toISOString(),
103109
to: tuple.to.toISOString(),
@@ -107,7 +113,7 @@ export const getEventCount = async ({
107113
secondaryTimestamp,
108114
searchAfterSortIds: undefined,
109115
runtimeMappings: undefined,
110-
}).body?.query;
116+
}).query;
111117
const response = await esClient.count({
112118
body: { query: eventSearchQueryBodyQuery },
113119
ignore_unavailable: true,

x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ export interface SignalMatch {
236236

237237
export type GetDocumentListInterface = (params: {
238238
searchAfter: estypes.SortResults | undefined;
239-
}) => Promise<estypes.SearchResponse<EventDoc | ThreatListDoc>>;
239+
}) => Promise<estypes.SearchResponse<EventDoc | ThreatListDoc, unknown>>;
240240

241241
export type CreateSignalInterface = (
242242
params: EventItem[] | ThreatListItem[]

x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/ml/find_ml_signals.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* 2.0.
66
*/
77

8+
import { set } from '@kbn/safer-lodash-set';
89
import dateMath from '@kbn/datemath';
910
import type { KibanaRequest, SavedObjectsClientContract } from '@kbn/core/server';
1011
import type { MlPluginSetup } from '@kbn/ml-plugin/server';
@@ -54,9 +55,9 @@ export const findMlSignals = async ({
5455

5556
if (isLoggedRequestsEnabled) {
5657
const searchQuery = buildAnomalyQuery(params);
57-
searchQuery.index = '.ml-anomalies-*';
58+
set(searchQuery, 'body.index', '.ml-anomalies-*');
5859
loggedRequests.push({
59-
request: logSearchRequest(searchQuery),
60+
request: logSearchRequest(searchQuery.body),
6061
description: i18n.ML_SEARCH_ANOMALIES_DESCRIPTION,
6162
duration: anomalyResults.took,
6263
request_type: 'findAnomalies',

x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/new_terms/build_new_terms_aggregation.ts

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,6 @@ import type { SignalSource } from '../types';
1111
import type { GenericBulkCreateResponse } from '../factories/bulk_create_factory';
1212
import type { NewTermsFieldsLatest } from '../../../../../common/api/detection_engine/model/alerts';
1313

14-
export type RecentTermsAggResult = ESSearchResponse<
15-
SignalSource,
16-
{ body: { aggregations: ReturnType<typeof buildRecentTermsAgg> } }
17-
>;
18-
19-
export type NewTermsAggResult = ESSearchResponse<
20-
SignalSource,
21-
{ body: { aggregations: ReturnType<typeof buildNewTermsAgg> } }
22-
>;
23-
2414
export type CompositeDocFetchAggResult = ESSearchResponse<
2515
SignalSource,
2616
{ body: { aggregations: ReturnType<typeof buildCompositeDocFetchAgg> } }

x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/new_terms/create_new_terms_alert_type.ts

Lines changed: 49 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,12 @@ import { SERVER_APP_ID } from '../../../../../common/constants';
1414
import { NewTermsRuleParams } from '../../rule_schema';
1515
import type { SecurityAlertType } from '../types';
1616
import { singleSearchAfter } from '../utils/single_search_after';
17+
import { buildEventsSearchQuery } from '../utils/build_events_query';
1718
import { getFilter } from '../utils/get_filter';
1819
import { wrapNewTermsAlerts } from './wrap_new_terms_alerts';
1920
import { bulkCreateSuppressedNewTermsAlertsInMemory } from './bulk_create_suppressed_alerts_in_memory';
2021
import type { EventsAndTerms } from './types';
21-
import type {
22-
RecentTermsAggResult,
23-
DocFetchAggResult,
24-
NewTermsAggResult,
25-
CreateAlertsHook,
26-
} from './build_new_terms_aggregation';
22+
import type { CreateAlertsHook } from './build_new_terms_aggregation';
2723
import type { NewTermsFieldsLatest } from '../../../../../common/api/detection_engine/model/alerts';
2824
import {
2925
buildRecentTermsAgg,
@@ -149,7 +145,7 @@ export const createNewTermsAlertType = (): SecurityAlertType<
149145
alertSuppression: params.alertSuppression,
150146
licensing,
151147
});
152-
let afterKey;
148+
let afterKey: Record<string, string | number | null> | undefined;
153149

154150
const result = createSearchAfterReturnType();
155151

@@ -170,12 +166,7 @@ export const createNewTermsAlertType = (): SecurityAlertType<
170166
// PHASE 1: Fetch a page of terms using a composite aggregation. This will collect a page from
171167
// all of the terms seen over the last rule interval. In the next phase we'll determine which
172168
// ones are new.
173-
const {
174-
searchResult,
175-
searchDuration,
176-
searchErrors,
177-
loggedRequests: firstPhaseLoggedRequests = [],
178-
} = await singleSearchAfter({
169+
const searchRequest = buildEventsSearchQuery({
179170
aggregations: buildRecentTermsAgg({
180171
fields: params.newTermsFields,
181172
after: afterKey,
@@ -185,13 +176,22 @@ export const createNewTermsAlertType = (): SecurityAlertType<
185176
// The time range for the initial composite aggregation is the rule interval, `from` and `to`
186177
from: tuple.from.toISOString(),
187178
to: tuple.to.toISOString(),
188-
services,
189-
ruleExecutionLogger,
190179
filter: esFilter,
191-
pageSize: 0,
180+
size: 0,
192181
primaryTimestamp,
193182
secondaryTimestamp,
194183
runtimeMappings,
184+
});
185+
186+
const {
187+
searchResult,
188+
searchDuration,
189+
searchErrors,
190+
loggedRequests: firstPhaseLoggedRequests = [],
191+
} = await singleSearchAfter({
192+
searchRequest,
193+
services,
194+
ruleExecutionLogger,
195195
loggedRequestsConfig: isLoggedRequestsEnabled
196196
? {
197197
type: 'findAllTerms',
@@ -203,8 +203,7 @@ export const createNewTermsAlertType = (): SecurityAlertType<
203203
: undefined,
204204
});
205205
loggedRequests.push(...firstPhaseLoggedRequests);
206-
const searchResultWithAggs = searchResult as RecentTermsAggResult;
207-
if (!searchResultWithAggs.aggregations) {
206+
if (!searchResult.aggregations) {
208207
throw new Error('Aggregations were missing on recent terms search result');
209208
}
210209
logger.debug(`Time spent on composite agg: ${searchDuration}`);
@@ -214,10 +213,10 @@ export const createNewTermsAlertType = (): SecurityAlertType<
214213

215214
// If the aggregation returns no after_key it signals that we've paged through all results
216215
// and the current page is empty so we can immediately break.
217-
if (searchResultWithAggs.aggregations.new_terms.after_key == null) {
216+
if (searchResult.aggregations.new_terms.after_key == null) {
218217
break;
219218
}
220-
const bucketsForField = searchResultWithAggs.aggregations.new_terms.buckets;
219+
const bucketsForField = searchResult.aggregations.new_terms.buckets;
221220

222221
const createAlertsHook: CreateAlertsHook = async (aggResult) => {
223222
const eventsAndTerms: EventsAndTerms[] = (
@@ -311,12 +310,7 @@ export const createNewTermsAlertType = (): SecurityAlertType<
311310
// The aggregation filters out buckets for terms that exist prior to `tuple.from`, so the buckets in the
312311
// response correspond to each new term.
313312
const includeValues = transformBucketsToValues(params.newTermsFields, bucketsForField);
314-
const {
315-
searchResult: pageSearchResult,
316-
searchDuration: pageSearchDuration,
317-
searchErrors: pageSearchErrors,
318-
loggedRequests: pageSearchLoggedRequests = [],
319-
} = await singleSearchAfter({
313+
const pageSearchRequest = buildEventsSearchQuery({
320314
aggregations: buildNewTermsAgg({
321315
newValueWindowStart: tuple.from,
322316
timestampField: aggregatableTimestampField,
@@ -330,12 +324,20 @@ export const createNewTermsAlertType = (): SecurityAlertType<
330324
// in addition to the rule interval
331325
from: parsedHistoryWindowSize.toISOString(),
332326
to: tuple.to.toISOString(),
333-
services,
334-
ruleExecutionLogger,
335327
filter: esFilter,
336-
pageSize: 0,
328+
size: 0,
337329
primaryTimestamp,
338330
secondaryTimestamp,
331+
});
332+
const {
333+
searchResult: pageSearchResult,
334+
searchDuration: pageSearchDuration,
335+
searchErrors: pageSearchErrors,
336+
loggedRequests: pageSearchLoggedRequests = [],
337+
} = await singleSearchAfter({
338+
searchRequest: pageSearchRequest,
339+
services,
340+
ruleExecutionLogger,
339341
loggedRequestsConfig: isLoggedRequestsEnabled
340342
? {
341343
type: 'findNewTerms',
@@ -350,26 +352,20 @@ export const createNewTermsAlertType = (): SecurityAlertType<
350352

351353
logger.debug(`Time spent on phase 2 terms agg: ${pageSearchDuration}`);
352354

353-
const pageSearchResultWithAggs = pageSearchResult as NewTermsAggResult;
354-
if (!pageSearchResultWithAggs.aggregations) {
355+
if (!pageSearchResult.aggregations) {
355356
throw new Error('Aggregations were missing on new terms search result');
356357
}
357358

358359
// PHASE 3: For each term that is not in the history window, fetch the oldest document in
359360
// the rule interval for that term. This is the first document to contain the new term, and will
360361
// become the basis of the resulting alert.
361362
// One document could become multiple alerts if the document contains an array with multiple new terms.
362-
if (pageSearchResultWithAggs.aggregations.new_terms.buckets.length > 0) {
363-
const actualNewTerms = pageSearchResultWithAggs.aggregations.new_terms.buckets.map(
363+
if (pageSearchResult.aggregations.new_terms.buckets.length > 0) {
364+
const actualNewTerms = pageSearchResult.aggregations.new_terms.buckets.map(
364365
(bucket) => bucket.key
365366
);
366367

367-
const {
368-
searchResult: docFetchSearchResult,
369-
searchDuration: docFetchSearchDuration,
370-
searchErrors: docFetchSearchErrors,
371-
loggedRequests: docFetchLoggedRequests = [],
372-
} = await singleSearchAfter({
368+
const docFetchSearchRequest = buildEventsSearchQuery({
373369
aggregations: buildDocFetchAgg({
374370
timestampField: aggregatableTimestampField,
375371
field: params.newTermsFields[0],
@@ -381,12 +377,20 @@ export const createNewTermsAlertType = (): SecurityAlertType<
381377
// For phase 3, we go back to aggregating only over the rule interval - excluding the history window
382378
from: tuple.from.toISOString(),
383379
to: tuple.to.toISOString(),
384-
services,
385-
ruleExecutionLogger,
386380
filter: esFilter,
387-
pageSize: 0,
381+
size: 0,
388382
primaryTimestamp,
389383
secondaryTimestamp,
384+
});
385+
const {
386+
searchResult: docFetchSearchResult,
387+
searchDuration: docFetchSearchDuration,
388+
searchErrors: docFetchSearchErrors,
389+
loggedRequests: docFetchLoggedRequests = [],
390+
} = await singleSearchAfter({
391+
searchRequest: docFetchSearchRequest,
392+
services,
393+
ruleExecutionLogger,
390394
loggedRequestsConfig: isLoggedRequestsEnabled
391395
? {
392396
type: 'findDocuments',
@@ -401,13 +405,11 @@ export const createNewTermsAlertType = (): SecurityAlertType<
401405
result.errors.push(...docFetchSearchErrors);
402406
loggedRequests.push(...docFetchLoggedRequests);
403407

404-
const docFetchResultWithAggs = docFetchSearchResult as DocFetchAggResult;
405-
406-
if (!docFetchResultWithAggs.aggregations) {
408+
if (!docFetchSearchResult.aggregations) {
407409
throw new Error('Aggregations were missing on document fetch search result');
408410
}
409411

410-
const bulkCreateResult = await createAlertsHook(docFetchResultWithAggs);
412+
const bulkCreateResult = await createAlertsHook(docFetchSearchResult);
411413

412414
if (bulkCreateResult.alertsWereTruncated) {
413415
result.warningMessages.push(
@@ -420,7 +422,7 @@ export const createNewTermsAlertType = (): SecurityAlertType<
420422
}
421423
}
422424

423-
afterKey = searchResultWithAggs.aggregations.new_terms.after_key;
425+
afterKey = searchResult.aggregations.new_terms.after_key;
424426
}
425427

426428
scheduleNotificationResponseActionsService({

0 commit comments

Comments
 (0)