mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[8.10] [Security Solution] [Detections] Display shard failure messages for threshold rules (#164231) (#164843)
# Backport This will backport the following commits from `main` to `8.10`: - [[Security Solution] [Detections] Display shard failure messages for threshold rules (#164231)](https://github.com/elastic/kibana/pull/164231) <!--- Backport version: 8.9.7 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Devin W. Hurley","email":"devin.hurley@elastic.co"},"sourceCommit":{"committedDate":"2023-08-25T13:52:35Z","message":"[Security Solution] [Detections] Display shard failure messages for threshold rules (#164231)\n\n## Summary\r\n\r\nref: https://github.com/elastic/kibana/issues/163369\r\n\r\nprevents threshold rules from throwing error message that covers up\r\nshard failures messages\r\n\r\n\r\n<img width=\"1245\" alt=\"threshold_search_errors\"\r\nsrc=\"9ed9050b
-dcc8-456a-957b-a96407da6fe0\">","sha":"19fdc91af9fe81ba360829ab9b0c2250868ce3c1","branchLabelMapping":{"^v8.11.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["review","release_note:fix","fixed","Feature:Threshold Rule","Team:Detection Engine","v8.10.0","v8.11.0"],"number":164231,"url":"https://github.com/elastic/kibana/pull/164231","mergeCommit":{"message":"[Security Solution] [Detections] Display shard failure messages for threshold rules (#164231)\n\n## Summary\r\n\r\nref: https://github.com/elastic/kibana/issues/163369\r\n\r\nprevents threshold rules from throwing error message that covers up\r\nshard failures messages\r\n\r\n\r\n<img width=\"1245\" alt=\"threshold_search_errors\"\r\nsrc=\"9ed9050b
-dcc8-456a-957b-a96407da6fe0\">","sha":"19fdc91af9fe81ba360829ab9b0c2250868ce3c1"}},"sourceBranch":"main","suggestedTargetBranches":["8.10"],"targetPullRequestStates":[{"branch":"8.10","label":"v8.10.0","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v8.11.0","labelRegex":"^v8.11.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/164231","number":164231,"mergeCommit":{"message":"[Security Solution] [Detections] Display shard failure messages for threshold rules (#164231)\n\n## Summary\r\n\r\nref: https://github.com/elastic/kibana/issues/163369\r\n\r\nprevents threshold rules from throwing error message that covers up\r\nshard failures messages\r\n\r\n\r\n<img width=\"1245\" alt=\"threshold_search_errors\"\r\nsrc=\"9ed9050b
-dcc8-456a-957b-a96407da6fe0\">","sha":"19fdc91af9fe81ba360829ab9b0c2250868ce3c1"}}]}] BACKPORT--> Co-authored-by: Devin W. Hurley <devin.hurley@elastic.co>
This commit is contained in:
parent
c861e5b533
commit
093a7afb0f
3 changed files with 57 additions and 38 deletions
|
@ -6,6 +6,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||||
|
import { isEmpty } from 'lodash';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
AlertInstanceContext,
|
AlertInstanceContext,
|
||||||
|
@ -28,7 +29,7 @@ import type {
|
||||||
ThresholdBucket,
|
ThresholdBucket,
|
||||||
ThresholdSingleBucketAggregationResult,
|
ThresholdSingleBucketAggregationResult,
|
||||||
} from './types';
|
} from './types';
|
||||||
import { shouldFilterByCardinality } from './utils';
|
import { shouldFilterByCardinality, searchResultHasAggs } from './utils';
|
||||||
import type { IRuleExecutionLogForExecutors } from '../../rule_monitoring';
|
import type { IRuleExecutionLogForExecutors } from '../../rule_monitoring';
|
||||||
import { getMaxSignalsWarning } from '../utils/utils';
|
import { getMaxSignalsWarning } from '../utils/utils';
|
||||||
|
|
||||||
|
@ -74,7 +75,6 @@ export const findThresholdSignals = async ({
|
||||||
warnings: string[];
|
warnings: string[];
|
||||||
}> => {
|
}> => {
|
||||||
// Leaf aggregations used below
|
// Leaf aggregations used below
|
||||||
let sortKeys;
|
|
||||||
const buckets: ThresholdBucket[] = [];
|
const buckets: ThresholdBucket[] = [];
|
||||||
const searchAfterResults: SearchAfterResults = {
|
const searchAfterResults: SearchAfterResults = {
|
||||||
searchDurations: [],
|
searchDurations: [],
|
||||||
|
@ -85,6 +85,7 @@ export const findThresholdSignals = async ({
|
||||||
const includeCardinalityFilter = shouldFilterByCardinality(threshold);
|
const includeCardinalityFilter = shouldFilterByCardinality(threshold);
|
||||||
|
|
||||||
if (hasThresholdFields(threshold)) {
|
if (hasThresholdFields(threshold)) {
|
||||||
|
let sortKeys: Record<string, string | number | null> | undefined;
|
||||||
do {
|
do {
|
||||||
const { searchResult, searchDuration, searchErrors } = await singleSearchAfter({
|
const { searchResult, searchDuration, searchErrors } = await singleSearchAfter({
|
||||||
aggregations: buildThresholdMultiBucketAggregation({
|
aggregations: buildThresholdMultiBucketAggregation({
|
||||||
|
@ -106,20 +107,22 @@ export const findThresholdSignals = async ({
|
||||||
secondaryTimestamp,
|
secondaryTimestamp,
|
||||||
});
|
});
|
||||||
|
|
||||||
const searchResultWithAggs = searchResult as ThresholdMultiBucketAggregationResult;
|
searchAfterResults.searchDurations.push(searchDuration);
|
||||||
if (!searchResultWithAggs.aggregations) {
|
if (!isEmpty(searchErrors)) {
|
||||||
|
searchAfterResults.searchErrors.push(...searchErrors);
|
||||||
|
sortKeys = undefined; // this will eject us out of the loop
|
||||||
|
// if a search failure occurs on a secondary iteration,
|
||||||
|
// we will return early.
|
||||||
|
} else if (searchResultHasAggs<ThresholdMultiBucketAggregationResult>(searchResult)) {
|
||||||
|
const thresholdTerms = searchResult.aggregations?.thresholdTerms;
|
||||||
|
sortKeys = thresholdTerms?.after_key;
|
||||||
|
|
||||||
|
buckets.push(
|
||||||
|
...((searchResult.aggregations?.thresholdTerms.buckets as ThresholdBucket[]) ?? [])
|
||||||
|
);
|
||||||
|
} else {
|
||||||
throw new Error('Aggregations were missing on threshold rule search result');
|
throw new Error('Aggregations were missing on threshold rule search result');
|
||||||
}
|
}
|
||||||
|
|
||||||
searchAfterResults.searchDurations.push(searchDuration);
|
|
||||||
searchAfterResults.searchErrors.push(...searchErrors);
|
|
||||||
|
|
||||||
const thresholdTerms = searchResultWithAggs.aggregations?.thresholdTerms;
|
|
||||||
sortKeys = thresholdTerms.after_key;
|
|
||||||
|
|
||||||
buckets.push(
|
|
||||||
...(searchResultWithAggs.aggregations.thresholdTerms.buckets as ThresholdBucket[])
|
|
||||||
);
|
|
||||||
} while (sortKeys && buckets.length <= maxSignals);
|
} while (sortKeys && buckets.length <= maxSignals);
|
||||||
} else {
|
} else {
|
||||||
const { searchResult, searchDuration, searchErrors } = await singleSearchAfter({
|
const { searchResult, searchDuration, searchErrors } = await singleSearchAfter({
|
||||||
|
@ -142,30 +145,32 @@ export const findThresholdSignals = async ({
|
||||||
secondaryTimestamp,
|
secondaryTimestamp,
|
||||||
});
|
});
|
||||||
|
|
||||||
const searchResultWithAggs = searchResult as ThresholdSingleBucketAggregationResult;
|
|
||||||
if (!searchResultWithAggs.aggregations) {
|
|
||||||
throw new Error('Aggregations were missing on threshold rule search result');
|
|
||||||
}
|
|
||||||
|
|
||||||
searchAfterResults.searchDurations.push(searchDuration);
|
searchAfterResults.searchDurations.push(searchDuration);
|
||||||
searchAfterResults.searchErrors.push(...searchErrors);
|
searchAfterResults.searchErrors.push(...searchErrors);
|
||||||
|
|
||||||
const docCount = searchResultWithAggs.hits.total.value;
|
|
||||||
if (
|
if (
|
||||||
docCount >= threshold.value &&
|
!searchResultHasAggs<ThresholdSingleBucketAggregationResult>(searchResult) &&
|
||||||
(!includeCardinalityFilter ||
|
isEmpty(searchErrors)
|
||||||
(searchResultWithAggs.aggregations.cardinality_count?.value ?? 0) >=
|
|
||||||
threshold.cardinality[0].value)
|
|
||||||
) {
|
) {
|
||||||
buckets.push({
|
throw new Error('Aggregations were missing on threshold rule search result');
|
||||||
doc_count: docCount,
|
} else if (searchResultHasAggs<ThresholdSingleBucketAggregationResult>(searchResult)) {
|
||||||
key: {},
|
const docCount = searchResult.hits.total.value;
|
||||||
max_timestamp: searchResultWithAggs.aggregations.max_timestamp,
|
if (
|
||||||
min_timestamp: searchResultWithAggs.aggregations.min_timestamp,
|
docCount >= threshold.value &&
|
||||||
...(includeCardinalityFilter
|
(!includeCardinalityFilter ||
|
||||||
? { cardinality_count: searchResultWithAggs.aggregations.cardinality_count }
|
(searchResult?.aggregations?.cardinality_count?.value ?? 0) >=
|
||||||
: {}),
|
threshold.cardinality[0].value)
|
||||||
});
|
) {
|
||||||
|
buckets.push({
|
||||||
|
doc_count: docCount,
|
||||||
|
key: {},
|
||||||
|
max_timestamp: searchResult.aggregations?.max_timestamp ?? { value: null },
|
||||||
|
min_timestamp: searchResult.aggregations?.min_timestamp ?? { value: null },
|
||||||
|
...(includeCardinalityFilter
|
||||||
|
? { cardinality_count: searchResult.aggregations?.cardinality_count }
|
||||||
|
: {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { isEmpty } from 'lodash';
|
||||||
import type { SearchHit } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
import type { SearchHit } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||||
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||||
import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
|
import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
|
||||||
|
@ -135,8 +136,6 @@ export const thresholdExecutor = async ({
|
||||||
aggregatableTimestampField,
|
aggregatableTimestampField,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Build and index new alerts
|
|
||||||
|
|
||||||
const createResult = await bulkCreateThresholdSignals({
|
const createResult = await bulkCreateThresholdSignals({
|
||||||
buckets,
|
buckets,
|
||||||
completeRule,
|
completeRule,
|
||||||
|
@ -152,7 +151,10 @@ export const thresholdExecutor = async ({
|
||||||
ruleExecutionLogger,
|
ruleExecutionLogger,
|
||||||
});
|
});
|
||||||
|
|
||||||
addToSearchAfterReturn({ current: result, next: createResult });
|
addToSearchAfterReturn({
|
||||||
|
current: result,
|
||||||
|
next: { ...createResult, success: createResult.success && isEmpty(searchErrors) },
|
||||||
|
});
|
||||||
|
|
||||||
result.errors.push(...previousSearchErrors);
|
result.errors.push(...previousSearchErrors);
|
||||||
result.errors.push(...searchErrors);
|
result.errors.push(...searchErrors);
|
||||||
|
|
|
@ -5,14 +5,20 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { estypes } from '@elastic/elasticsearch';
|
||||||
import { createHash } from 'crypto';
|
import { createHash } from 'crypto';
|
||||||
import { v5 as uuidv5 } from 'uuid';
|
import { v5 as uuidv5 } from 'uuid';
|
||||||
import type {
|
import type {
|
||||||
ThresholdNormalized,
|
ThresholdNormalized,
|
||||||
ThresholdWithCardinality,
|
ThresholdWithCardinality,
|
||||||
} from '../../../../../common/api/detection_engine/model/rule_schema';
|
} from '../../../../../common/api/detection_engine/model/rule_schema';
|
||||||
import type { RuleRangeTuple } from '../types';
|
import type { RuleRangeTuple, SignalSearchResponse } from '../types';
|
||||||
import type { ThresholdSignalHistory, ThresholdAlertState } from './types';
|
import type {
|
||||||
|
ThresholdSignalHistory,
|
||||||
|
ThresholdAlertState,
|
||||||
|
ThresholdSingleBucketAggregationResult,
|
||||||
|
ThresholdMultiBucketAggregationResult,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a new signal history based on what the previous
|
* Returns a new signal history based on what the previous
|
||||||
|
@ -82,3 +88,9 @@ export const getThresholdTermsHash = (
|
||||||
)
|
)
|
||||||
.digest('hex');
|
.digest('hex');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const searchResultHasAggs = <
|
||||||
|
T extends ThresholdSingleBucketAggregationResult | ThresholdMultiBucketAggregationResult
|
||||||
|
>(
|
||||||
|
obj: SignalSearchResponse<Record<estypes.AggregateName, estypes.AggregationsAggregate>>
|
||||||
|
): obj is T => obj?.aggregations != null;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue