Skip to content

Commit 5e02f5e

Browse files
authored
[9.0] [Security Solution] Fix "too many clauses" error on prebuilt rules installation page (#223240) (#224282)
# Backport This will backport the following commits from `main` to `9.0`: - [[Security Solution] Fix "too many clauses" error on prebuilt rules installation page (#223240)](#223240) <!--- Backport version: 10.0.1 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport) <!--BACKPORT [{"author":{"name":"Nikita Indik","email":"[email protected]"},"sourceCommit":{"committedDate":"2025-06-17T14:14:56Z","message":"[Security Solution] Fix \"too many clauses\" error on prebuilt rules installation page (#223240)\n\n**Resolves: https://github.com/elastic/kibana/issues/223399**\n\n## Summary\nThis PR fixes an error on the \"Add Elastic rules\" page. The error is\nshown when running a local dev environment from `main` branch and going\nto the \"Add Elastic rules\" page.\n\n<img width=\"1741\" alt=\"Screenshot 2025-06-10 at 11 28 19\"\nsrc=\"https://github.com/user-attachments/assets/f8f81f88-3749-491f-bcdb-cd51f465bda6\"\n/>\n\n## Changes\nPR updates methods of `PrebuiltRuleAssetsClient` to split requests to ES\ninto smaller chunks to avoid the error.\n\n## Cause\nKibana makes a search request to ES with a filter that has too many\nclauses, so ES rejects with an error.\n\nMore specifically, `/prebuilt_rules/installation/_review` route handler\ncalls `PrebuiltRuleAssetsClient.fetchAssetsByVersion` to fetch all\ninstallable rules. To do this, we construct a request with thousands of\nclauses in a filter. ES counts the number of clauses in a filter and\nrejects because it's bigger than `maxClauseCount`. `maxClauseCount`\nvalue is computed dynamically by ES and its size depends on hardware and\navailable resources\n([docs](https://www.elastic.co/guide/en/elasticsearch/reference/8.18/search-settings.html),\n[code](https://github.com/elastic/elasticsearch/blob/main/server/src/main/java/org/elasticsearch/search/SearchUtils.java)).\nThe minimum value for `maxClauseCount` is 1024.\n\n## Why it didn't fail before\nTwo reasons:\n1. ES changed how `maxClauseCount` is computed. They've recently merged\na [PR](elastic/elasticsearch#128293) that made\nqueries against numeric types count three times towards the\n`maxClauseCount` limit. They plan to revert the change in [this\nPR](https://github.com/elastic/elasticsearch/pull/129206).\n2. Prebuilt rule packages are growing bigger with each version,\nresulting in a bigger number of clauses. I've tested behaviour with ES\nchange in place on different package versions:\n- 8.17.1 (contains 1262 rule versions) - no \"too many clauses\" error\n- 8.18.1 (contains 1356 rule versions) - causes \"too many clauses\" error\n- 9.0.1 (also contains 1356 rule versions) - causes \"too many clauses\"\nerror\nThe precise number of versions that start to cause errors is 1293 on my\nlaptop.\n\nSo even if ES team rolls back their change, we still need to make sure\nwe don't go over the limit with ever-growing prebuilt rule package\nsizes.","sha":"482953ddc5a9e1494a3182c9cedfa4214179a297","branchLabelMapping":{"^v9.1.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["bug","release_note:skip","Team:Detections and Resp","Team: SecuritySolution","Team:Detection Rule Management","Feature:Prebuilt Detection Rules","backport:version","v9.1.0","v8.19.0","v9.0.3","v8.18.3"],"title":"[Security Solution] Fix \"too many clauses\" error on prebuilt rules installation page","number":223240,"url":"https://github.com/elastic/kibana/pull/223240","mergeCommit":{"message":"[Security Solution] Fix \"too many clauses\" error on prebuilt rules installation page (#223240)\n\n**Resolves: https://github.com/elastic/kibana/issues/223399**\n\n## Summary\nThis PR fixes an error on the \"Add Elastic rules\" page. The error is\nshown when running a local dev environment from `main` branch and going\nto the \"Add Elastic rules\" page.\n\n<img width=\"1741\" alt=\"Screenshot 2025-06-10 at 11 28 19\"\nsrc=\"https://github.com/user-attachments/assets/f8f81f88-3749-491f-bcdb-cd51f465bda6\"\n/>\n\n## Changes\nPR updates methods of `PrebuiltRuleAssetsClient` to split requests to ES\ninto smaller chunks to avoid the error.\n\n## Cause\nKibana makes a search request to ES with a filter that has too many\nclauses, so ES rejects with an error.\n\nMore specifically, `/prebuilt_rules/installation/_review` route handler\ncalls `PrebuiltRuleAssetsClient.fetchAssetsByVersion` to fetch all\ninstallable rules. To do this, we construct a request with thousands of\nclauses in a filter. ES counts the number of clauses in a filter and\nrejects because it's bigger than `maxClauseCount`. `maxClauseCount`\nvalue is computed dynamically by ES and its size depends on hardware and\navailable resources\n([docs](https://www.elastic.co/guide/en/elasticsearch/reference/8.18/search-settings.html),\n[code](https://github.com/elastic/elasticsearch/blob/main/server/src/main/java/org/elasticsearch/search/SearchUtils.java)).\nThe minimum value for `maxClauseCount` is 1024.\n\n## Why it didn't fail before\nTwo reasons:\n1. ES changed how `maxClauseCount` is computed. They've recently merged\na [PR](elastic/elasticsearch#128293) that made\nqueries against numeric types count three times towards the\n`maxClauseCount` limit. They plan to revert the change in [this\nPR](https://github.com/elastic/elasticsearch/pull/129206).\n2. Prebuilt rule packages are growing bigger with each version,\nresulting in a bigger number of clauses. I've tested behaviour with ES\nchange in place on different package versions:\n- 8.17.1 (contains 1262 rule versions) - no \"too many clauses\" error\n- 8.18.1 (contains 1356 rule versions) - causes \"too many clauses\" error\n- 9.0.1 (also contains 1356 rule versions) - causes \"too many clauses\"\nerror\nThe precise number of versions that start to cause errors is 1293 on my\nlaptop.\n\nSo even if ES team rolls back their change, we still need to make sure\nwe don't go over the limit with ever-growing prebuilt rule package\nsizes.","sha":"482953ddc5a9e1494a3182c9cedfa4214179a297"}},"sourceBranch":"main","suggestedTargetBranches":["9.0","8.18"],"targetPullRequestStates":[{"branch":"main","label":"v9.1.0","branchLabelMappingKey":"^v9.1.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/223240","number":223240,"mergeCommit":{"message":"[Security Solution] Fix \"too many clauses\" error on prebuilt rules installation page (#223240)\n\n**Resolves: https://github.com/elastic/kibana/issues/223399**\n\n## Summary\nThis PR fixes an error on the \"Add Elastic rules\" page. The error is\nshown when running a local dev environment from `main` branch and going\nto the \"Add Elastic rules\" page.\n\n<img width=\"1741\" alt=\"Screenshot 2025-06-10 at 11 28 19\"\nsrc=\"https://github.com/user-attachments/assets/f8f81f88-3749-491f-bcdb-cd51f465bda6\"\n/>\n\n## Changes\nPR updates methods of `PrebuiltRuleAssetsClient` to split requests to ES\ninto smaller chunks to avoid the error.\n\n## Cause\nKibana makes a search request to ES with a filter that has too many\nclauses, so ES rejects with an error.\n\nMore specifically, `/prebuilt_rules/installation/_review` route handler\ncalls `PrebuiltRuleAssetsClient.fetchAssetsByVersion` to fetch all\ninstallable rules. To do this, we construct a request with thousands of\nclauses in a filter. ES counts the number of clauses in a filter and\nrejects because it's bigger than `maxClauseCount`. `maxClauseCount`\nvalue is computed dynamically by ES and its size depends on hardware and\navailable resources\n([docs](https://www.elastic.co/guide/en/elasticsearch/reference/8.18/search-settings.html),\n[code](https://github.com/elastic/elasticsearch/blob/main/server/src/main/java/org/elasticsearch/search/SearchUtils.java)).\nThe minimum value for `maxClauseCount` is 1024.\n\n## Why it didn't fail before\nTwo reasons:\n1. ES changed how `maxClauseCount` is computed. They've recently merged\na [PR](elastic/elasticsearch#128293) that made\nqueries against numeric types count three times towards the\n`maxClauseCount` limit. They plan to revert the change in [this\nPR](https://github.com/elastic/elasticsearch/pull/129206).\n2. Prebuilt rule packages are growing bigger with each version,\nresulting in a bigger number of clauses. I've tested behaviour with ES\nchange in place on different package versions:\n- 8.17.1 (contains 1262 rule versions) - no \"too many clauses\" error\n- 8.18.1 (contains 1356 rule versions) - causes \"too many clauses\" error\n- 9.0.1 (also contains 1356 rule versions) - causes \"too many clauses\"\nerror\nThe precise number of versions that start to cause errors is 1293 on my\nlaptop.\n\nSo even if ES team rolls back their change, we still need to make sure\nwe don't go over the limit with ever-growing prebuilt rule package\nsizes.","sha":"482953ddc5a9e1494a3182c9cedfa4214179a297"}},{"branch":"8.19","label":"v8.19.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"url":"https://github.com/elastic/kibana/pull/224269","number":224269,"state":"OPEN"},{"branch":"9.0","label":"v9.0.3","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"8.18","label":"v8.18.3","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"}]}] BACKPORT-->
1 parent c5a46f4 commit 5e02f5e

File tree

1 file changed

+125
-52
lines changed

1 file changed

+125
-52
lines changed

x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_assets/prebuilt_rule_assets_client.ts

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

8-
import { uniqBy } from 'lodash';
8+
import { chunk, uniqBy } from 'lodash';
9+
import pMap from 'p-map';
910
import type {
1011
AggregationsMultiBucketAggregateBase,
1112
AggregationsTopHitsAggregate,
@@ -18,7 +19,10 @@ import { validatePrebuiltRuleAssets } from './prebuilt_rule_assets_validation';
1819
import { PREBUILT_RULE_ASSETS_SO_TYPE } from './prebuilt_rule_assets_type';
1920
import type { RuleVersionSpecifier } from '../rule_versions/rule_version_specifier';
2021

22+
const RULE_ASSET_ATTRIBUTES = `${PREBUILT_RULE_ASSETS_SO_TYPE}.attributes`;
2123
const MAX_PREBUILT_RULES_COUNT = 10_000;
24+
const ES_MAX_CLAUSE_COUNT = 1024;
25+
const ES_MAX_CONCURRENT_REQUESTS = 2;
2226

2327
export interface IPrebuiltRuleAssetsClient {
2428
fetchLatestAssets: () => Promise<PrebuiltRuleAsset[]>;
@@ -75,52 +79,66 @@ export const createPrebuiltRuleAssetsClient = (
7579
});
7680
},
7781

78-
fetchLatestVersions: (ruleIds: string[] = []): Promise<RuleVersionSpecifier[]> => {
82+
fetchLatestVersions: (ruleIds?: string[]): Promise<RuleVersionSpecifier[]> => {
7983
return withSecuritySpan('IPrebuiltRuleAssetsClient.fetchLatestVersions', async () => {
80-
const filter = ruleIds
81-
.map((ruleId) => `${PREBUILT_RULE_ASSETS_SO_TYPE}.attributes.rule_id: ${ruleId}`)
82-
.join(' OR ');
84+
if (ruleIds && ruleIds.length === 0) {
85+
return [];
86+
}
8387

84-
const findResult = await savedObjectsClient.find<
85-
PrebuiltRuleAsset,
86-
{
87-
rules: AggregationsMultiBucketAggregateBase<{
88-
latest_version: AggregationsTopHitsAggregate;
89-
}>;
90-
}
91-
>({
92-
type: PREBUILT_RULE_ASSETS_SO_TYPE,
93-
filter,
94-
aggs: {
95-
rules: {
96-
terms: {
97-
field: `${PREBUILT_RULE_ASSETS_SO_TYPE}.attributes.rule_id`,
98-
size: MAX_PREBUILT_RULES_COUNT,
99-
},
100-
aggs: {
101-
latest_version: {
102-
top_hits: {
103-
size: 1,
104-
sort: [
105-
{
106-
[`${PREBUILT_RULE_ASSETS_SO_TYPE}.version`]: 'desc',
107-
},
108-
],
109-
_source: [
110-
`${PREBUILT_RULE_ASSETS_SO_TYPE}.rule_id`,
111-
`${PREBUILT_RULE_ASSETS_SO_TYPE}.version`,
112-
],
88+
const fetchLatestVersionInfo = async (filter?: string) => {
89+
const findResult = await savedObjectsClient.find<
90+
PrebuiltRuleAsset,
91+
{
92+
rules: AggregationsMultiBucketAggregateBase<{
93+
latest_version: AggregationsTopHitsAggregate;
94+
}>;
95+
}
96+
>({
97+
type: PREBUILT_RULE_ASSETS_SO_TYPE,
98+
filter,
99+
aggs: {
100+
rules: {
101+
terms: {
102+
field: `${PREBUILT_RULE_ASSETS_SO_TYPE}.attributes.rule_id`,
103+
size: MAX_PREBUILT_RULES_COUNT,
104+
},
105+
aggs: {
106+
latest_version: {
107+
top_hits: {
108+
size: 1,
109+
sort: [
110+
{
111+
[`${PREBUILT_RULE_ASSETS_SO_TYPE}.version`]: 'desc',
112+
},
113+
],
114+
_source: [
115+
`${PREBUILT_RULE_ASSETS_SO_TYPE}.rule_id`,
116+
`${PREBUILT_RULE_ASSETS_SO_TYPE}.version`,
117+
],
118+
},
113119
},
114120
},
115121
},
116122
},
117-
},
118-
});
123+
});
119124

120-
const buckets = findResult.aggregations?.rules?.buckets ?? [];
121-
invariant(Array.isArray(buckets), 'Expected buckets to be an array');
125+
const aggregatedBuckets = findResult.aggregations?.rules?.buckets ?? [];
126+
invariant(Array.isArray(aggregatedBuckets), 'Expected buckets to be an array');
127+
128+
return aggregatedBuckets;
129+
};
130+
131+
const filters = ruleIds
132+
? createChunkedFilters({
133+
items: ruleIds,
134+
mapperFn: (ruleId) => `${PREBUILT_RULE_ASSETS_SO_TYPE}.attributes.rule_id: ${ruleId}`,
135+
clausesPerItem: 2,
136+
})
137+
: undefined;
122138

123-
return buckets.map((bucket) => {
139+
const buckets = await chunkedFetch(fetchLatestVersionInfo, filters);
140+
141+
const latestVersions = buckets.map((bucket) => {
124142
const hit = bucket.latest_version.hits.hits[0];
125143
const soAttributes = hit._source[PREBUILT_RULE_ASSETS_SO_TYPE];
126144
const versionInfo: RuleVersionSpecifier = {
@@ -129,31 +147,39 @@ export const createPrebuiltRuleAssetsClient = (
129147
};
130148
return versionInfo;
131149
});
150+
151+
return latestVersions;
132152
});
133153
},
134154

135155
fetchAssetsByVersion: (versions: RuleVersionSpecifier[]): Promise<PrebuiltRuleAsset[]> => {
136156
return withSecuritySpan('IPrebuiltRuleAssetsClient.fetchAssetsByVersion', async () => {
157+
// debugger;
137158
if (versions.length === 0) {
138159
// NOTE: without early return it would build incorrect filter and fetch all existing saved objects
139160
return [];
140161
}
141162

142-
const attr = `${PREBUILT_RULE_ASSETS_SO_TYPE}.attributes`;
143-
const filter = versions
144-
.map((v) => `(${attr}.rule_id: ${v.rule_id} AND ${attr}.version: ${v.version})`)
145-
.join(' OR ');
146-
147-
// Usage of savedObjectsClient.bulkGet() is ~25% more performant and
148-
// simplifies deduplication but too many tests get broken.
149-
// See https://github.com/elastic/kibana/issues/218198
150-
const findResult = await savedObjectsClient.find<PrebuiltRuleAsset>({
151-
type: PREBUILT_RULE_ASSETS_SO_TYPE,
152-
filter,
153-
perPage: MAX_PREBUILT_RULES_COUNT,
163+
const filters = createChunkedFilters({
164+
items: versions,
165+
mapperFn: (versionSpecifier) =>
166+
`(${RULE_ASSET_ATTRIBUTES}.rule_id: ${versionSpecifier.rule_id} AND ${RULE_ASSET_ATTRIBUTES}.version: ${versionSpecifier.version})`,
167+
clausesPerItem: 4,
154168
});
155169

156-
const ruleAssets = findResult.saved_objects.map((so) => so.attributes);
170+
const ruleAssets = await chunkedFetch(async (filter) => {
171+
// Usage of savedObjectsClient.bulkGet() is ~25% more performant and
172+
// simplifies deduplication but too many tests get broken.
173+
// See https://github.com/elastic/kibana/issues/218198
174+
const findResult = await savedObjectsClient.find<PrebuiltRuleAsset>({
175+
type: PREBUILT_RULE_ASSETS_SO_TYPE,
176+
filter,
177+
perPage: MAX_PREBUILT_RULES_COUNT,
178+
});
179+
180+
return findResult.saved_objects.map((so) => so.attributes);
181+
}, filters);
182+
157183
// Rule assets may have duplicates we have to get rid of.
158184
// In particular prebuilt rule assets package v8.17.1 has duplicates.
159185
const uniqueRuleAssets = uniqBy(ruleAssets, 'rule_id');
@@ -163,3 +189,50 @@ export const createPrebuiltRuleAssetsClient = (
163189
},
164190
};
165191
};
192+
193+
/**
194+
* Creates an array of KQL filter strings for a collection of items.
195+
* Uses chunking to ensure that the number of filter clauses does not exceed the ES "too_many_clauses" limit.
196+
* See: https://github.com/elastic/kibana/pull/223240
197+
*
198+
* @param {object} options
199+
* @param {T[]} options.items - Array of items to create filters for.
200+
* @param {(item: T) => string} options.mapperFn - A function that maps an item to a filter string.
201+
* @param {number} options.clausesPerItem - Number of Elasticsearch clauses generated per item. Determined empirically by converting a KQL filter into a Query DSL query.
202+
* More complex filters will result in more clauses. Info about clauses in docs: https://www.elastic.co/docs/explore-analyze/query-filter/languages/querydsl#query-dsl
203+
* @returns {string[]} An array of filter strings
204+
*/
205+
function createChunkedFilters<T>({
206+
items,
207+
mapperFn,
208+
clausesPerItem,
209+
}: {
210+
items: T[];
211+
mapperFn: (item: T) => string;
212+
clausesPerItem: number;
213+
}): string[] {
214+
return chunk(items, ES_MAX_CLAUSE_COUNT / clausesPerItem).map((singleChunk) =>
215+
singleChunk.map(mapperFn).join(' OR ')
216+
);
217+
}
218+
219+
/**
220+
* Fetches objects using a provided function.
221+
* If filters are provided fetches concurrently in chunks.
222+
*
223+
* @param {(filter?: string) => Promise<T[]>} chunkFetchFn - Function that fetches a chunk.
224+
* @param {string[]} [filters] - An optional array of filter strings. If provided, `chunkFetchFn` will be called for each filter concurrently.
225+
* @returns {Promise<T[]>} A promise that resolves to an array of fetched objects.
226+
*/
227+
function chunkedFetch<T>(
228+
chunkFetchFn: (filter?: string) => Promise<T[]>,
229+
filters?: string[]
230+
): Promise<T[]> {
231+
if (filters?.length) {
232+
return pMap(filters, chunkFetchFn, {
233+
concurrency: ES_MAX_CONCURRENT_REQUESTS,
234+
}).then((results) => results.flat());
235+
}
236+
237+
return chunkFetchFn();
238+
}

0 commit comments

Comments
 (0)