diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/helpers.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/helpers.tsx index 57292d91953d8..476302ab06f5b 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/helpers.tsx @@ -27,7 +27,6 @@ import { FilterBadgeGroup } from '@kbn/unified-search-plugin/public'; import { IntervalAbbrScreenReader } from '../../../../common/components/accessibility'; import type { RequiredFieldArray, - Threshold, AlertSuppressionMissingFieldsStrategy, } from '../../../../../common/api/detection_engine/model/rule_schema'; import { AlertSuppressionMissingFieldsStrategyEnum } from '../../../../../common/api/detection_engine/model/rule_schema'; @@ -50,6 +49,7 @@ import { defaultToEmptyTag } from '../../../../common/components/empty_value'; import { RequiredFieldIcon } from '../../../rule_management/components/rule_details/required_field_icon'; import { ThreatEuiFlexGroup } from './threat_description'; import { AlertSuppressionLabel } from './alert_suppression_label'; +import type { FieldValueThreshold } from '../threshold_input'; const NoteDescriptionContainer = styled(EuiFlexItem)` height: 105px; @@ -490,20 +490,29 @@ export const buildRuleTypeDescription = (label: string, ruleType: Type): ListIte } }; -export const buildThresholdDescription = (label: string, threshold: Threshold): ListItems[] => [ - { - title: label, - description: ( - <> - {isEmpty(threshold.field[0]) - ? `${i18n.THRESHOLD_RESULTS_ALL} >= ${threshold.value}` - : `${i18n.THRESHOLD_RESULTS_AGGREGATED_BY} ${ - Array.isArray(threshold.field) ? threshold.field.join(',') : threshold.field - } >= ${threshold.value}`} - > - ), - }, -]; +export const buildThresholdDescription = ( + label: string, + threshold: FieldValueThreshold +): ListItems[] => { + let thresholdDescription = isEmpty(threshold.field[0]) + ? `${i18n.THRESHOLD_RESULTS_ALL} >= ${threshold.value}` + : `${i18n.THRESHOLD_RESULTS_AGGREGATED_BY} ${threshold.field.join(',')} >= ${threshold.value}`; + + if (threshold.cardinality?.value && threshold.cardinality?.field.length > 0) { + thresholdDescription = i18n.THRESHOLD_CARDINALITY( + thresholdDescription, + threshold.cardinality.field[0], + threshold.cardinality.value + ); + } + + return [ + { + title: label, + description: <>{thresholdDescription}>, + }, + ]; +}; export const buildThreatMappingDescription = ( title: string, diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/index.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/index.test.tsx index de46d09065f4e..b45f1a50414ac 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/index.test.tsx @@ -451,10 +451,62 @@ describe('description_step', () => { expect(result[0].title).toEqual('Threshold label'); expect(React.isValidElement(result[0].description)).toBeTruthy(); - expect(mount(result[0].description as React.ReactElement).html()).toContain( + expect(mount(result[0].description as React.ReactElement).html()).toEqual( + 'Results aggregated by user.name >= 100' + ); + }); + + test('returns threshold description when threshold exist, field is set, and cardinality is not set', () => { + const mockThreshold = { + threshold: { + field: ['user.name'], + value: 100, + cardinality: { + field: [], + value: 0, + }, + }, + }; + const result: ListItems[] = getDescriptionItem( + 'threshold', + 'Threshold label', + mockThreshold, + mockFilterManager, + mockLicenseService + ); + + expect(result[0].title).toEqual('Threshold label'); + expect(React.isValidElement(result[0].description)).toBeTruthy(); + expect(mount(result[0].description as React.ReactElement).html()).toEqual( 'Results aggregated by user.name >= 100' ); }); + + test('returns threshold description when threshold exist, field is set and cardinality is set', () => { + const mockThreshold = { + threshold: { + field: ['user.name'], + value: 100, + cardinality: { + field: ['host.test_value'], + value: 10, + }, + }, + }; + const result: ListItems[] = getDescriptionItem( + 'threshold', + 'Threshold label', + mockThreshold, + mockFilterManager, + mockLicenseService + ); + + expect(result[0].title).toEqual('Threshold label'); + expect(React.isValidElement(result[0].description)).toBeTruthy(); + expect(mount(result[0].description as React.ReactElement).html()).toContain( + 'Results aggregated by user.name >= 100 when unique values count of host.test_value >= 10' + ); + }); }); describe('references', () => { diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/translations.ts index 5c43b9181adcb..74a9faa4efd4c 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/translations.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/translations.ts @@ -126,6 +126,24 @@ export const THRESHOLD_RESULTS_AGGREGATED_BY = i18n.translate( } ); +export const THRESHOLD_CARDINALITY = ( + thresholdFieldsGroupedBy: string, + cardinalityField: string, + cardinalityValue: string | number +) => + i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDescription.thresholdResultsCardinalityDescription', + { + defaultMessage: + '{thresholdFieldsGroupedBy} when unique values count of {cardinalityField} >= {cardinalityValue}', + values: { + thresholdFieldsGroupedBy, + cardinalityField, + cardinalityValue, + }, + } + ); + export const EQL_EVENT_CATEGORY_FIELD_LABEL = i18n.translate( 'xpack.securitySolution.detectionEngine.ruleDescription.eqlEventCategoryFieldLabel', { diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_definition_section.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_definition_section.tsx index 295323017f06a..70f267ac94ba4 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_definition_section.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_definition_section.tsx @@ -185,15 +185,23 @@ interface ThresholdProps { threshold: ThresholdType; } -export const Threshold = ({ threshold }: ThresholdProps) => ( -