diff --git a/test/plugin_functional/test_suites/core_plugins/rendering.ts b/test/plugin_functional/test_suites/core_plugins/rendering.ts index 67ee470764690c..6e4c73864322a6 100644 --- a/test/plugin_functional/test_suites/core_plugins/rendering.ts +++ b/test/plugin_functional/test_suites/core_plugins/rendering.ts @@ -330,6 +330,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) { 'xpack.stack_connectors.enableExperimental (array)', 'xpack.trigger_actions_ui.enableExperimental (array)', 'xpack.trigger_actions_ui.enableGeoTrackingThresholdAlert (boolean)', + 'xpack.alerting.rules.run.alerts.max (number)', 'xpack.upgrade_assistant.featureSet.migrateSystemIndices (boolean)', 'xpack.upgrade_assistant.featureSet.mlSnapshots (boolean)', 'xpack.upgrade_assistant.featureSet.reindexCorrectiveActions (boolean)', diff --git a/x-pack/plugins/alerting/public/mocks.ts b/x-pack/plugins/alerting/public/mocks.ts index 8ed08c7e574042..977447f29f3657 100644 --- a/x-pack/plugins/alerting/public/mocks.ts +++ b/x-pack/plugins/alerting/public/mocks.ts @@ -17,6 +17,7 @@ const createSetupContract = (): Setup => ({ const createStartContract = (): Start => ({ getNavigation: jest.fn(), + getMaxAlertsPerRun: jest.fn(), }); export const alertingPluginMock = { diff --git a/x-pack/plugins/alerting/public/plugin.test.ts b/x-pack/plugins/alerting/public/plugin.test.ts index 3d6165cf18f6ea..87b7e4573c79f2 100644 --- a/x-pack/plugins/alerting/public/plugin.test.ts +++ b/x-pack/plugins/alerting/public/plugin.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { AlertingPublicPlugin } from './plugin'; +import { AlertingPublicPlugin, AlertingUIConfig } from './plugin'; import { coreMock } from '@kbn/core/public/mocks'; import { createManagementSectionMock, @@ -17,7 +17,17 @@ jest.mock('./services/rule_api', () => ({ loadRuleType: jest.fn(), })); -const mockInitializerContext = coreMock.createPluginInitializerContext(); +const mockAlertingUIConfig: AlertingUIConfig = { + rules: { + run: { + alerts: { + max: 1000, + }, + }, + }, +}; + +const mockInitializerContext = coreMock.createPluginInitializerContext(mockAlertingUIConfig); const management = managementPluginMock.createSetupContract(); const mockSection = createManagementSectionMock(); diff --git a/x-pack/plugins/alerting/public/plugin.ts b/x-pack/plugins/alerting/public/plugin.ts index 5d2f96680ca905..71bae6b28c94c9 100644 --- a/x-pack/plugins/alerting/public/plugin.ts +++ b/x-pack/plugins/alerting/public/plugin.ts @@ -57,6 +57,7 @@ export interface PluginSetupContract { } export interface PluginStartContract { getNavigation: (ruleId: Rule['id']) => Promise; + getMaxAlertsPerRun: () => number; } export interface AlertingPluginSetup { management: ManagementSetup; @@ -69,13 +70,28 @@ export interface AlertingPluginStart { data: DataPublicPluginStart; } +export interface AlertingUIConfig { + rules: { + run: { + alerts: { + max: number; + }; + }; + }; +} + export class AlertingPublicPlugin implements Plugin { private alertNavigationRegistry?: AlertNavigationRegistry; + private config: AlertingUIConfig; + readonly maxAlertsPerRun: number; - constructor(private readonly initContext: PluginInitializerContext) {} + constructor(private readonly initContext: PluginInitializerContext) { + this.config = this.initContext.config.get(); + this.maxAlertsPerRun = this.config.rules.run.alerts.max; + } public setup(core: CoreSetup, plugins: AlertingPluginSetup) { this.alertNavigationRegistry = new AlertNavigationRegistry(); @@ -150,6 +166,9 @@ export class AlertingPublicPlugin return rule.viewInAppRelativeUrl; } }, + getMaxAlertsPerRun: () => { + return this.maxAlertsPerRun; + }, }; } } diff --git a/x-pack/plugins/alerting/server/config.ts b/x-pack/plugins/alerting/server/config.ts index a49c393da1e95a..6dd96667f95532 100644 --- a/x-pack/plugins/alerting/server/config.ts +++ b/x-pack/plugins/alerting/server/config.ts @@ -80,7 +80,7 @@ export type AlertingConfig = TypeOf; export type RulesConfig = TypeOf; export type AlertingRulesConfig = Pick< AlertingConfig['rules'], - 'minimumScheduleInterval' | 'maxScheduledPerMinute' + 'minimumScheduleInterval' | 'maxScheduledPerMinute' | 'run' > & { isUsingSecurity: boolean; }; diff --git a/x-pack/plugins/alerting/server/index.ts b/x-pack/plugins/alerting/server/index.ts index 84d99c15d805f4..2f5cd3994e436a 100644 --- a/x-pack/plugins/alerting/server/index.ts +++ b/x-pack/plugins/alerting/server/index.ts @@ -7,8 +7,7 @@ import type { PublicMethodsOf } from '@kbn/utility-types'; import { PluginConfigDescriptor, PluginInitializerContext } from '@kbn/core/server'; import { RulesClient as RulesClientClass } from './rules_client'; -import { configSchema } from './config'; -import { AlertsConfigType } from './types'; +import { AlertingConfig, configSchema } from './config'; export type RulesClient = PublicMethodsOf; @@ -79,8 +78,11 @@ export const plugin = async (initContext: PluginInitializerContext) => { return new AlertingPlugin(initContext); }; -export const config: PluginConfigDescriptor = { +export const config: PluginConfigDescriptor = { schema: configSchema, + exposeToBrowser: { + rules: { run: { alerts: { max: true } } }, + }, deprecations: ({ renameFromRoot, deprecate }) => [ renameFromRoot('xpack.alerts.healthCheck', 'xpack.alerting.healthCheck', { level: 'warning' }), renameFromRoot( diff --git a/x-pack/plugins/alerting/server/plugin.test.ts b/x-pack/plugins/alerting/server/plugin.test.ts index acbad4a27ffe8f..37175ac9604129 100644 --- a/x-pack/plugins/alerting/server/plugin.test.ts +++ b/x-pack/plugins/alerting/server/plugin.test.ts @@ -163,6 +163,7 @@ describe('Alerting Plugin', () => { maxScheduledPerMinute: 10000, isUsingSecurity: false, minimumScheduleInterval: { value: '1m', enforce: false }, + run: { alerts: { max: 1000 }, actions: { max: 1000 } }, }); expect(setupContract.frameworkAlerts.enabled()).toEqual(false); diff --git a/x-pack/plugins/alerting/server/plugin.ts b/x-pack/plugins/alerting/server/plugin.ts index f0006e6974e33c..401256f9f80136 100644 --- a/x-pack/plugins/alerting/server/plugin.ts +++ b/x-pack/plugins/alerting/server/plugin.ts @@ -458,7 +458,7 @@ export class AlertingPlugin { }, getConfig: () => { return { - ...pick(this.config.rules, ['minimumScheduleInterval', 'maxScheduledPerMinute']), + ...pick(this.config.rules, ['minimumScheduleInterval', 'maxScheduledPerMinute', 'run']), isUsingSecurity: this.licenseState ? !!this.licenseState.getIsSecurityEnabled() : false, }; }, diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.schema.yaml b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.schema.yaml index dfb3bfb738a5c2..f8998624f99b1d 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.schema.yaml @@ -123,7 +123,6 @@ components: references: $ref: './common_attributes.schema.yaml#/components/schemas/RuleReferenceArray' - # maxSignals not used in ML rules but probably should be used max_signals: $ref: './common_attributes.schema.yaml#/components/schemas/MaxSignals' threat: diff --git a/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts b/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts index e0e484ea1e0177..d3aa07d0f6cdac 100644 --- a/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts +++ b/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts @@ -58,6 +58,7 @@ import { fieldFormatsMock } from '@kbn/field-formats-plugin/common/mocks'; import { indexPatternFieldEditorPluginMock } from '@kbn/data-view-field-editor-plugin/public/mocks'; import { UpsellingService } from '@kbn/security-solution-upselling/service'; import { calculateBounds } from '@kbn/data-plugin/common'; +import { alertingPluginMock } from '@kbn/alerting-plugin/public/mocks'; const mockUiSettings: Record = { [DEFAULT_TIME_RANGE]: { from: 'now-15m', to: 'now', mode: 'quick' }, @@ -128,6 +129,7 @@ export const createStartServicesMock = ( const cloud = cloudMock.createStart(); const mockSetHeaderActionMenu = jest.fn(); const mockTimelineFilterManager = createFilterManagerMock(); + const alerting = alertingPluginMock.createStartContract(); /* * Below mocks are needed by unified field list @@ -250,6 +252,7 @@ export const createStartServicesMock = ( dataViewFieldEditor: indexPatternFieldEditorPluginMock.createStartContract(), upselling: new UpsellingService(), timelineFilterManager: mockTimelineFilterManager, + alerting, } as unknown as StartServices; }; 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 11f0f28e071062..7950493a1b9894 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 @@ -263,7 +263,7 @@ describe('description_step', () => { mockLicenseService ); - expect(result.length).toEqual(13); + expect(result.length).toEqual(14); }); }); @@ -768,6 +768,33 @@ describe('description_step', () => { }); }); }); + + describe('maxSignals', () => { + test('returns default "max signals" description', () => { + const result: ListItems[] = getDescriptionItem( + 'maxSignals', + 'Max alerts per run', + mockAboutStep, + mockFilterManager, + mockLicenseService + ); + + expect(result[0].title).toEqual('Max alerts per run'); + expect(result[0].description).toEqual(100); + }); + + test('returns empty array when "value" is a undefined', () => { + const result: ListItems[] = getDescriptionItem( + 'maxSignals', + 'Max alerts per run', + { ...mockAboutStep, maxSignals: undefined }, + mockFilterManager, + mockLicenseService + ); + + expect(result.length).toEqual(0); + }); + }); }); }); }); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/index.tsx index 9e38eaa9f69045..7e00e90f391b08 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/index.tsx @@ -342,6 +342,9 @@ export const getDescriptionItem = ( return get('isBuildingBlock', data) ? [{ title: i18n.BUILDING_BLOCK_LABEL, description: i18n.BUILDING_BLOCK_DESCRIPTION }] : []; + } else if (field === 'maxSignals') { + const value: number | undefined = get(field, data); + return value ? [{ title: label, description: value }] : []; } const description: string = get(field, data); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/max_signals/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/max_signals/index.tsx new file mode 100644 index 00000000000000..b7d717b5818029 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/max_signals/index.tsx @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo, useCallback } from 'react'; +import type { EuiFieldNumberProps } from '@elastic/eui'; +import { EuiTextColor, EuiFormRow, EuiFieldNumber, EuiIcon } from '@elastic/eui'; +import type { FieldHook } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { css } from '@emotion/css'; +import { DEFAULT_MAX_SIGNALS } from '../../../../../common/constants'; +import * as i18n from './translations'; +import { useKibana } from '../../../../common/lib/kibana'; + +interface MaxSignalsFieldProps { + dataTestSubj: string; + field: FieldHook; + idAria: string; + isDisabled: boolean; + placeholder?: string; +} + +const MAX_SIGNALS_FIELD_WIDTH = 200; + +export const MaxSignals: React.FC = ({ + dataTestSubj, + field, + idAria, + isDisabled, + placeholder, +}): JSX.Element => { + const { setValue, value } = field; + const { alerting } = useKibana().services; + const maxAlertsPerRun = alerting.getMaxAlertsPerRun(); + + const [isInvalid, error] = useMemo(() => { + if (typeof value === 'number' && !isNaN(value) && value <= 0) { + return [true, i18n.GREATER_THAN_ERROR]; + } + return [false]; + }, [value]); + + const hasWarning = useMemo( + () => typeof value === 'number' && !isNaN(value) && value > maxAlertsPerRun, + [maxAlertsPerRun, value] + ); + + const handleMaxSignalsChange: EuiFieldNumberProps['onChange'] = useCallback( + (e) => { + const maxSignalsValue = (e.target as HTMLInputElement).value; + // Has to handle an empty string as the field is optional + setValue(maxSignalsValue !== '' ? Number(maxSignalsValue.trim()) : ''); + }, + [setValue] + ); + + const helpText = useMemo(() => { + const textToRender = []; + if (hasWarning) { + textToRender.push( + {i18n.LESS_THAN_WARNING(maxAlertsPerRun)} + ); + } + textToRender.push(i18n.MAX_SIGNALS_HELP_TEXT(DEFAULT_MAX_SIGNALS)); + return textToRender; + }, [hasWarning, maxAlertsPerRun]); + + return ( + + : undefined} + /> + + ); +}; + +MaxSignals.displayName = 'MaxSignals'; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/max_signals/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/max_signals/translations.ts new file mode 100644 index 00000000000000..b69c0557a051f8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/max_signals/translations.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const GREATER_THAN_ERROR = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.maxAlertsFieldGreaterThanError', + { + defaultMessage: 'Max alerts must be greater than 0.', + } +); + +export const LESS_THAN_WARNING = (maxNumber: number) => + i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.maxAlertsFieldLessThanWarning', + { + values: { maxNumber }, + defaultMessage: + 'Kibana only allows a maximum of {maxNumber} {maxNumber, plural, =1 {alert} other {alerts}} per rule run.', + } + ); + +export const MAX_SIGNALS_HELP_TEXT = (defaultNumber: number) => + i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldMaxAlertsHelpText', + { + values: { defaultNumber }, + defaultMessage: + 'The maximum number of alerts the rule will create each time it runs. Default is {defaultNumber}.', + } + ); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/default_value.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/default_value.ts index 26f842384ef25a..9d1fff5b8a9efd 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/default_value.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/default_value.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { DEFAULT_MAX_SIGNALS } from '../../../../../common/constants'; import type { AboutStepRule } from '../../../../detections/pages/detection_engine/rules/types'; import { fillEmptySeverityMappings } from '../../../../detections/pages/detection_engine/rules/helpers'; @@ -33,5 +34,6 @@ export const stepAboutDefaultValue: AboutStepRule = { timestampOverride: '', threat: threatDefault, note: '', + maxSignals: DEFAULT_MAX_SIGNALS, setup: '', }; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/index.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/index.test.tsx index dc3fc5645b1384..8cb278ddccdfe9 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/index.test.tsx @@ -34,6 +34,8 @@ import { stepDefineDefaultValue, } from '../../../../detections/pages/detection_engine/rules/utils'; import type { FormHook } from '../../../../shared_imports'; +import { useKibana as mockUseKibana } from '../../../../common/lib/kibana/__mocks__'; +import { useKibana } from '../../../../common/lib/kibana'; jest.mock('../../../../common/lib/kibana'); jest.mock('../../../../common/containers/source'); @@ -50,6 +52,7 @@ jest.mock('@elastic/eui', () => { }, }; }); +const mockedUseKibana = mockUseKibana(); export const stepDefineStepMLRule: DefineStepRule = { ruleType: 'machine_learning', @@ -118,6 +121,7 @@ describe('StepAboutRuleComponent', () => { indexPatterns: stubIndexPattern, }, ]); + (useKibana as jest.Mock).mockReturnValue(mockedUseKibana); useGetInstalledJobMock = (useGetInstalledJob as jest.Mock).mockImplementation(() => ({ jobs: [], })); @@ -282,6 +286,7 @@ describe('StepAboutRuleComponent', () => { }, ], investigationFields: [], + maxSignals: 100, }; await act(async () => { @@ -343,6 +348,7 @@ describe('StepAboutRuleComponent', () => { }, ], investigationFields: [], + maxSignals: 100, }; await act(async () => { diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/index.tsx index 99e65f33e486a6..3fa1852e6aa056 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/index.tsx @@ -34,12 +34,16 @@ import { SeverityField } from '../severity_mapping'; import { RiskScoreField } from '../risk_score_mapping'; import { AutocompleteField } from '../autocomplete_field'; import { useFetchIndex } from '../../../../common/containers/source'; -import { DEFAULT_INDICATOR_SOURCE_PATH } from '../../../../../common/constants'; +import { + DEFAULT_INDICATOR_SOURCE_PATH, + DEFAULT_MAX_SIGNALS, +} from '../../../../../common/constants'; import { useKibana } from '../../../../common/lib/kibana'; import { useRuleIndices } from '../../../rule_management/logic/use_rule_indices'; import { EsqlAutocomplete } from '../esql_autocomplete'; import { MultiSelectFieldsAutocomplete } from '../multi_select_fields'; import { useInvestigationFields } from '../../hooks/use_investigation_fields'; +import { MaxSignals } from '../max_signals'; const CommonUseField = getUseField({ component: Field }); @@ -327,6 +331,18 @@ const StepAboutRuleComponent: FC = ({ /> + + + {isThreatMatchRuleValue && ( <> = { ), labelAppend: OptionalFieldLabel, }, + maxSignals: { + type: FIELD_TYPES.NUMBER, + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.fieldMaxAlertsLabel', + { + defaultMessage: 'Max alerts per run', + } + ), + labelAppend: OptionalFieldLabel, + }, isAssociatedToEndpointList: { type: FIELD_TYPES.CHECKBOX, label: i18n.translate( diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/helpers.test.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/helpers.test.ts index e00e18ed7f6ea4..20a432cdc14206 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/helpers.test.ts @@ -691,12 +691,38 @@ describe('helpers', () => { tags: ['tag1', 'tag2'], threat: getThreatMock(), investigation_fields: { field_names: ['foo', 'bar'] }, + max_signals: 100, setup: '# this is some setup documentation', }; expect(result).toEqual(expected); }); + // Users are allowed to input 0 in the form, but value is validated in the API layer + test('returns formatted object with max_signals set to 0', () => { + const mockDataWithZeroMaxSignals: AboutStepRule = { + ...mockData, + maxSignals: 0, + }; + + const result = formatAboutStepData(mockDataWithZeroMaxSignals); + + expect(result.max_signals).toEqual(0); + }); + + // Strings or empty values are replaced with undefined and overriden with the default value of 1000 + test('returns formatted object with undefined max_signals for non-integer values inputs', () => { + const mockDataWithNonIntegerMaxSignals: AboutStepRule = { + ...mockData, + // @ts-expect-error + maxSignals: '', + }; + + const result = formatAboutStepData(mockDataWithNonIntegerMaxSignals); + + expect(result.max_signals).toEqual(undefined); + }); + test('returns formatted object with endpoint exceptions_list', () => { const result = formatAboutStepData( { @@ -773,6 +799,7 @@ describe('helpers', () => { tags: ['tag1', 'tag2'], threat: getThreatMock(), investigation_fields: { field_names: ['foo', 'bar'] }, + max_signals: 100, setup: '# this is some setup documentation', }; @@ -799,6 +826,7 @@ describe('helpers', () => { tags: ['tag1', 'tag2'], threat: getThreatMock(), investigation_fields: { field_names: ['foo', 'bar'] }, + max_signals: 100, setup: '# this is some setup documentation', }; @@ -844,6 +872,7 @@ describe('helpers', () => { tags: ['tag1', 'tag2'], threat: getThreatMock(), investigation_fields: { field_names: ['foo', 'bar'] }, + max_signals: 100, setup: '# this is some setup documentation', }; @@ -898,6 +927,7 @@ describe('helpers', () => { }, ], investigation_fields: { field_names: ['foo', 'bar'] }, + max_signals: 100, setup: '# this is some setup documentation', }; @@ -928,6 +958,7 @@ describe('helpers', () => { timestamp_override: 'event.ingest', timestamp_override_fallback_disabled: true, investigation_fields: { field_names: ['foo', 'bar'] }, + max_signals: 100, setup: '# this is some setup documentation', }; @@ -959,6 +990,7 @@ describe('helpers', () => { timestamp_override_fallback_disabled: undefined, threat: getThreatMock(), investigation_fields: undefined, + max_signals: 100, setup: '# this is some setup documentation', }; @@ -989,6 +1021,7 @@ describe('helpers', () => { threat_indicator_path: undefined, timestamp_override: undefined, timestamp_override_fallback_disabled: undefined, + max_signals: 100, setup: '# this is some setup documentation', }; @@ -1019,6 +1052,7 @@ describe('helpers', () => { threat_indicator_path: undefined, timestamp_override: undefined, timestamp_override_fallback_disabled: undefined, + max_signals: 100, setup: '# this is some setup documentation', }; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/helpers.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/helpers.ts index 18f23824b77a78..3054a894d0df11 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/helpers.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/helpers.ts @@ -559,6 +559,7 @@ export const formatAboutStepData = ( threat, isAssociatedToEndpointList, isBuildingBlock, + maxSignals, note, ruleNameOverride, threatIndicatorPath, @@ -613,6 +614,7 @@ export const formatAboutStepData = ( timestamp_override: timestampOverride !== '' ? timestampOverride : undefined, timestamp_override_fallback_disabled: timestampOverrideFallbackDisabled, ...(!isEmpty(note) ? { note } : {}), + max_signals: Number.isSafeInteger(maxSignals) ? maxSignals : undefined, ...rest, }; return resp; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/index.tsx index 2922e26da6b4ac..d57198522b388d 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/index.tsx @@ -141,7 +141,6 @@ const CreateRulePageComponent: React.FC = () => { const [indicesConfig] = useUiSetting$(DEFAULT_INDEX_KEY); const [threatIndicesConfig] = useUiSetting$(DEFAULT_THREAT_INDEX_KEY); - const defineStepDefault = useMemo( () => ({ ...stepDefineDefaultValue, @@ -150,6 +149,7 @@ const CreateRulePageComponent: React.FC = () => { }), [indicesConfig, threatIndicesConfig] ); + const kibanaAbsoluteUrl = useMemo( () => application.getUrlForApp(`${APP_UI_ID}`, { diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_editing/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_editing/index.tsx index 807dc4b04f1b3c..768b5a9903691b 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_editing/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_editing/index.tsx @@ -414,7 +414,6 @@ const EditRulePageComponent: FC<{ rule: RuleResponse }> = ({ rule }) => { rule?.exceptions_list ), ...(ruleId ? { id: ruleId } : {}), - ...(rule != null ? { max_signals: rule.max_signals } : {}), }); displaySuccessToast(i18n.SUCCESSFULLY_SAVED_RULE(rule?.name ?? ''), dispatchToaster); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_about_section.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_about_section.tsx index cc54a5bd30a870..7f52bae481160c 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_about_section.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_about_section.tsx @@ -240,6 +240,16 @@ const TimestampOverride = ({ timestampOverride }: TimestampOverrideProps) => ( ); +interface MaxSignalsProps { + maxSignals: number; +} + +const MaxSignals = ({ maxSignals }: MaxSignalsProps) => ( + + {maxSignals} + +); + interface TagsProps { tags: string[]; } @@ -414,6 +424,13 @@ const prepareAboutSectionListItems = ( }); } + if (rule.max_signals) { + aboutSectionListItems.push({ + title: {i18n.MAX_SIGNALS_FIELD_LABEL}, + description: , + }); + } + if (rule.tags && rule.tags.length > 0) { aboutSectionListItems.push({ title: {i18n.TAGS_FIELD_LABEL}, diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/translations.ts index 9bc90aee27717e..9025b184af1d38 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/translations.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/translations.ts @@ -342,3 +342,10 @@ export const FROM_FIELD_LABEL = i18n.translate( defaultMessage: 'Additional look-back time', } ); + +export const MAX_SIGNALS_FIELD_LABEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDetails.maxAlertsFieldLabel', + { + defaultMessage: 'Max alerts per run', + } +); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/__mocks__/mock.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/__mocks__/mock.ts index 80b0d3eedc8b76..7552ac9b711f38 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/__mocks__/mock.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/__mocks__/mock.ts @@ -199,6 +199,7 @@ export const mockAboutStepRule = (): AboutStepRule => ({ note: '# this is some markdown documentation', setup: '# this is some setup documentation', investigationFields: ['foo', 'bar'], + maxSignals: 100, }); export const mockActionsStepRule = (enabled = false): ActionsStepRule => ({ diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx index bcb73b1f9edc29..a1c9908c220532 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx @@ -146,6 +146,7 @@ describe('rule helpers', () => { timestampOverride: 'event.ingested', timestampOverrideFallbackDisabled: false, investigationFields: [], + maxSignals: 100, setup: '# this is some setup documentation', }; const scheduleRuleStepData = { from: '0s', interval: '5m' }; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx index 574397c80e7676..24f871125ce4d3 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx @@ -240,6 +240,7 @@ export const getAboutStepsData = (rule: RuleResponse, detailsView: boolean): Abo investigation_fields: investigationFields, tags, threat, + max_signals: maxSignals, } = rule; const threatIndicatorPath = 'threat_indicator_path' in rule ? rule.threat_indicator_path : undefined; @@ -272,6 +273,7 @@ export const getAboutStepsData = (rule: RuleResponse, detailsView: boolean): Abo investigationFields: investigationFields?.field_names ?? [], threat: threat as Threats, threatIndicatorPath, + maxSignals, setup, }; }; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts index fa2ae6af7876e8..4757c9f29dfdcc 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts @@ -102,6 +102,7 @@ export interface AboutStepRule { threatIndicatorPath?: string; threat: Threats; note: string; + maxSignals?: number; setup: SetupGuide; } @@ -249,6 +250,7 @@ export interface AboutStepRuleJson { timestamp_override_fallback_disabled?: boolean; note?: string; investigation_fields?: InvestigationFields; + max_signals?: number; } export interface ScheduleStepRuleJson { diff --git a/x-pack/plugins/security_solution/public/types.ts b/x-pack/plugins/security_solution/public/types.ts index 96f2538f157f7a..1bd539bd52b6c4 100644 --- a/x-pack/plugins/security_solution/public/types.ts +++ b/x-pack/plugins/security_solution/public/types.ts @@ -58,6 +58,7 @@ import type { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public'; import type { UpsellingService } from '@kbn/security-solution-upselling/service'; import type { ChartsPluginStart } from '@kbn/charts-plugin/public'; import type { SavedSearchPublicPluginStart } from '@kbn/saved-search-plugin/public'; +import type { PluginStartContract } from '@kbn/alerting-plugin/public/plugin'; import type { ResolverPluginSetup } from './resolver/types'; import type { Inspect } from '../common/search_strategy'; import type { Detections } from './detections'; @@ -147,6 +148,7 @@ export interface StartPlugins { dataViewEditor: DataViewEditorStart; charts: ChartsPluginStart; savedSearch: SavedSearchPublicPluginStart; + alerting: PluginStartContract; core: CoreStart; } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/rule_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/rule_type.ts index 228cb67122a267..556a86c7c1f2b2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/rule_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/rule_type.ts @@ -24,6 +24,7 @@ import type { QueryRuleParams, RuleParams } from '../../rule_schema'; // this is only used in tests import { createDefaultAlertExecutorOptions } from '@kbn/rule-registry-plugin/server/utils/rule_executor.test_helpers'; import { getCompleteRuleMock } from '../../rule_schema/mocks'; +import { DEFAULT_MAX_ALERTS } from '@kbn/alerting-plugin/server/config'; export const createRuleTypeMocks = ( ruleType: string = 'query', @@ -45,6 +46,7 @@ export const createRuleTypeMocks = ( registerType: ({ executor }) => { alertExecutor = executor; }, + getConfig: () => ({ run: { alerts: { max: DEFAULT_MAX_ALERTS } } }), } as AlertingPluginSetupContract; const scheduleActions = jest.fn(); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts index 34a0d7f198e225..e5d03da05c0459 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts @@ -75,6 +75,7 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = version, isPreview, experimentalFeatures, + alerting, }) => (type) => { const { alertIgnoreFields: ignoreFields, alertMergeStrategy: mergeStrategy } = config; @@ -306,7 +307,12 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = wroteWarningStatus = true; } - const { tuples, remainingGap } = getRuleRangeTuples({ + const { + tuples, + remainingGap, + wroteWarningStatus: rangeTuplesWarningStatus, + warningStatusMessage: rangeTuplesWarningMessage, + } = await getRuleRangeTuples({ startedAt, previousStartedAt, from, @@ -314,7 +320,12 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = interval, maxSignals: maxSignals ?? DEFAULT_MAX_SIGNALS, ruleExecutionLogger, + alerting, }); + if (rangeTuplesWarningStatus) { + wroteWarningStatus = rangeTuplesWarningStatus; + warningMessage = rangeTuplesWarningMessage; + } if (remainingGap.asMilliseconds() > 0) { hasError = true; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.test.ts index f91f806073ff1c..7ad3c148b88403 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.test.ts @@ -46,6 +46,7 @@ describe('Custom Query Alerts', () => { ruleExecutionLoggerFactory: () => Promise.resolve(ruleExecutionLogMock.forExecutors.create()), version: '8.3', publicBaseUrl, + alerting, }); const eventsTelemetry = createMockTelemetryEventsSender(true); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts index bc9fb374a9f45b..efbbf7888e6ed0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts @@ -137,6 +137,7 @@ export interface CreateSecurityRuleTypeWrapperProps { version: string; isPreview?: boolean; experimentalFeatures?: ExperimentalFeatures; + alerting: SetupPlugins['alerting']; } export type CreateSecurityRuleTypeWrapper = ( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/search_after_bulk_create.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/search_after_bulk_create.test.ts index 31c1e38b08f917..4d414d71cfadfd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/search_after_bulk_create.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/search_after_bulk_create.test.ts @@ -49,6 +49,7 @@ import { ruleExecutionLogMock } from '../../rule_monitoring/mocks'; import type { BuildReasonMessage } from './reason_formatters'; import type { QueryRuleParams } from '../../rule_schema'; import { SERVER_APP_ID } from '../../../../../common/constants'; +import type { PluginSetupContract } from '@kbn/alerting-plugin/server'; describe('searchAfterAndBulkCreate', () => { let mockService: RuleExecutorServicesMock; @@ -58,6 +59,7 @@ describe('searchAfterAndBulkCreate', () => { let wrapHits: WrapHits; let inputIndexPattern: string[] = []; let listClient = listMock.getListClient(); + let alerting: PluginSetupContract; const ruleExecutionLogger = ruleExecutionLogMock.forExecutors.create(); const someGuids = Array.from({ length: 13 }).map(() => uuidv4()); const sampleParams = getQueryRuleParams(); @@ -82,22 +84,27 @@ describe('searchAfterAndBulkCreate', () => { sampleParams.maxSignals = 30; let tuple: RuleRangeTuple; - beforeEach(() => { + beforeEach(async () => { jest.clearAllMocks(); buildReasonMessage = jest.fn().mockResolvedValue('some alert reason message'); listClient = listMock.getListClient(); listClient.searchListItemByValues = jest.fn().mockResolvedValue([]); inputIndexPattern = ['auditbeat-*']; mockService = alertsMock.createRuleExecutorServices(); - tuple = getRuleRangeTuples({ - previousStartedAt: new Date(), - startedAt: new Date(), - from: sampleParams.from, - to: sampleParams.to, - interval: '5m', - maxSignals: sampleParams.maxSignals, - ruleExecutionLogger, - }).tuples[0]; + alerting = alertsMock.createSetup(); + alerting.getConfig = jest.fn().mockReturnValue({ run: { alerts: { max: 1000 } } }); + tuple = ( + await getRuleRangeTuples({ + previousStartedAt: new Date(), + startedAt: new Date(), + from: sampleParams.from, + to: sampleParams.to, + interval: '5m', + maxSignals: sampleParams.maxSignals, + ruleExecutionLogger, + alerting, + }) + ).tuples[0]; mockPersistenceServices = createPersistenceServicesMock(); bulkCreate = bulkCreateFactory( mockPersistenceServices.alertWithPersistence, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/utils.test.ts index abf17f9f81b0ed..afa9583fef41e8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/utils.test.ts @@ -65,6 +65,7 @@ import type { ShardError } from '../../../types'; import { ruleExecutionLogMock } from '../../rule_monitoring/mocks'; import type { GenericBulkCreateResponse } from '../factories'; import type { BaseFieldsLatest } from '../../../../../common/api/detection_engine/model/alerts'; +import type { PluginSetupContract } from '@kbn/alerting-plugin/server'; describe('utils', () => { const anchor = '2020-01-01T06:06:06.666Z'; @@ -442,63 +443,84 @@ describe('utils', () => { }); describe('getRuleRangeTuples', () => { - test('should return a single tuple if no gap', () => { - const { tuples, remainingGap } = getRuleRangeTuples({ - previousStartedAt: moment().subtract(30, 's').toDate(), - startedAt: moment().subtract(30, 's').toDate(), - interval: '30s', - from: 'now-30s', - to: 'now', - maxSignals: 20, - ruleExecutionLogger, - }); + let alerting: PluginSetupContract; + + beforeEach(() => { + alerting = alertsMock.createSetup(); + alerting.getConfig = jest.fn().mockReturnValue({ run: { alerts: { max: 1000 } } }); + }); + + test('should return a single tuple if no gap', async () => { + const { tuples, remainingGap, wroteWarningStatus, warningStatusMessage } = + await getRuleRangeTuples({ + previousStartedAt: moment().subtract(30, 's').toDate(), + startedAt: moment().subtract(30, 's').toDate(), + interval: '30s', + from: 'now-30s', + to: 'now', + maxSignals: 20, + ruleExecutionLogger, + alerting, + }); const someTuple = tuples[0]; expect(moment(someTuple.to).diff(moment(someTuple.from), 's')).toEqual(30); expect(tuples.length).toEqual(1); expect(remainingGap.asMilliseconds()).toEqual(0); - }); - - test('should return a single tuple if malformed interval prevents gap calculation', () => { - const { tuples, remainingGap } = getRuleRangeTuples({ - previousStartedAt: moment().subtract(30, 's').toDate(), - startedAt: moment().subtract(30, 's').toDate(), - interval: 'invalid', - from: 'now-30s', - to: 'now', - maxSignals: 20, - ruleExecutionLogger, - }); + expect(wroteWarningStatus).toEqual(false); + expect(warningStatusMessage).toEqual(undefined); + }); + + test('should return a single tuple if malformed interval prevents gap calculation', async () => { + const { tuples, remainingGap, wroteWarningStatus, warningStatusMessage } = + await getRuleRangeTuples({ + previousStartedAt: moment().subtract(30, 's').toDate(), + startedAt: moment().subtract(30, 's').toDate(), + interval: 'invalid', + from: 'now-30s', + to: 'now', + maxSignals: 20, + ruleExecutionLogger, + alerting, + }); const someTuple = tuples[0]; expect(moment(someTuple.to).diff(moment(someTuple.from), 's')).toEqual(30); expect(tuples.length).toEqual(1); expect(remainingGap.asMilliseconds()).toEqual(0); - }); - - test('should return two tuples if gap and previouslyStartedAt', () => { - const { tuples, remainingGap } = getRuleRangeTuples({ - previousStartedAt: moment().subtract(65, 's').toDate(), - startedAt: moment().toDate(), - interval: '50s', - from: 'now-55s', - to: 'now', - maxSignals: 20, - ruleExecutionLogger, - }); + expect(wroteWarningStatus).toEqual(false); + expect(warningStatusMessage).toEqual(undefined); + }); + + test('should return two tuples if gap and previouslyStartedAt', async () => { + const { tuples, remainingGap, wroteWarningStatus, warningStatusMessage } = + await getRuleRangeTuples({ + previousStartedAt: moment().subtract(65, 's').toDate(), + startedAt: moment().toDate(), + interval: '50s', + from: 'now-55s', + to: 'now', + maxSignals: 20, + ruleExecutionLogger, + alerting, + }); const someTuple = tuples[1]; expect(moment(someTuple.to).diff(moment(someTuple.from), 's')).toEqual(55); expect(remainingGap.asMilliseconds()).toEqual(0); - }); - - test('should return five tuples when give long gap', () => { - const { tuples, remainingGap } = getRuleRangeTuples({ - previousStartedAt: moment().subtract(65, 's').toDate(), // 64 is 5 times the interval + lookback, which will trigger max lookback - startedAt: moment().toDate(), - interval: '10s', - from: 'now-13s', - to: 'now', - maxSignals: 20, - ruleExecutionLogger, - }); + expect(wroteWarningStatus).toEqual(false); + expect(warningStatusMessage).toEqual(undefined); + }); + + test('should return five tuples when give long gap', async () => { + const { tuples, remainingGap, wroteWarningStatus, warningStatusMessage } = + await getRuleRangeTuples({ + previousStartedAt: moment().subtract(65, 's').toDate(), // 64 is 5 times the interval + lookback, which will trigger max lookback + startedAt: moment().toDate(), + interval: '10s', + from: 'now-13s', + to: 'now', + maxSignals: 20, + ruleExecutionLogger, + alerting, + }); expect(tuples.length).toEqual(5); tuples.forEach((item, index) => { if (index === 0) { @@ -509,22 +531,67 @@ describe('utils', () => { expect(item.from.diff(tuples[index - 1].from, 's')).toEqual(10); }); expect(remainingGap.asMilliseconds()).toEqual(12000); + expect(wroteWarningStatus).toEqual(false); + expect(warningStatusMessage).toEqual(undefined); + }); + + test('should return a single tuple when give a negative gap (rule ran sooner than expected)', async () => { + const { tuples, remainingGap, wroteWarningStatus, warningStatusMessage } = + await getRuleRangeTuples({ + previousStartedAt: moment().subtract(-15, 's').toDate(), + startedAt: moment().subtract(-15, 's').toDate(), + interval: '10s', + from: 'now-13s', + to: 'now', + maxSignals: 20, + ruleExecutionLogger, + alerting, + }); + expect(tuples.length).toEqual(1); + const someTuple = tuples[0]; + expect(moment(someTuple.to).diff(moment(someTuple.from), 's')).toEqual(13); + expect(remainingGap.asMilliseconds()).toEqual(0); + expect(wroteWarningStatus).toEqual(false); + expect(warningStatusMessage).toEqual(undefined); }); - test('should return a single tuple when give a negative gap (rule ran sooner than expected)', () => { - const { tuples, remainingGap } = getRuleRangeTuples({ - previousStartedAt: moment().subtract(-15, 's').toDate(), - startedAt: moment().subtract(-15, 's').toDate(), - interval: '10s', - from: 'now-13s', + test('should use alerting framework max alerts value if maxSignals is greater than limit', async () => { + alerting.getConfig = jest.fn().mockReturnValue({ run: { alerts: { max: 10 } } }); + const { tuples, wroteWarningStatus, warningStatusMessage } = await getRuleRangeTuples({ + previousStartedAt: moment().subtract(30, 's').toDate(), + startedAt: moment().subtract(30, 's').toDate(), + interval: '30s', + from: 'now-30s', to: 'now', maxSignals: 20, ruleExecutionLogger, + alerting, }); + const someTuple = tuples[0]; + expect(someTuple.maxSignals).toEqual(10); expect(tuples.length).toEqual(1); + expect(wroteWarningStatus).toEqual(true); + expect(warningStatusMessage).toEqual( + "The rule's max alerts per run setting (20) is greater than the Kibana alerting limit (10). The rule will only write a maximum of 10 alerts per rule run." + ); + }); + + test('should use maxSignals value if maxSignals is less than alerting framework limit', async () => { + const { tuples, wroteWarningStatus, warningStatusMessage } = await getRuleRangeTuples({ + previousStartedAt: moment().subtract(30, 's').toDate(), + startedAt: moment().subtract(30, 's').toDate(), + interval: '30s', + from: 'now-30s', + to: 'now', + maxSignals: 20, + ruleExecutionLogger, + alerting, + }); const someTuple = tuples[0]; - expect(moment(someTuple.to).diff(moment(someTuple.from), 's')).toEqual(13); - expect(remainingGap.asMilliseconds()).toEqual(0); + expect(someTuple.maxSignals).toEqual(20); + expect(tuples.length).toEqual(1); + expect(wroteWarningStatus).toEqual(false); + expect(warningStatusMessage).toEqual(undefined); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/utils.ts index a3b640f0017ddd..abb54d8fa67704 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/utils.ts @@ -26,6 +26,7 @@ import type { import type { AlertInstanceContext, AlertInstanceState, + PluginSetupContract, RuleExecutorServices, } from '@kbn/alerting-plugin/server'; import { parseDuration } from '@kbn/alerting-plugin/server'; @@ -410,7 +411,7 @@ export const errorAggregator = ( }, Object.create(null)); }; -export const getRuleRangeTuples = ({ +export const getRuleRangeTuples = async ({ startedAt, previousStartedAt, from, @@ -418,6 +419,7 @@ export const getRuleRangeTuples = ({ interval, maxSignals, ruleExecutionLogger, + alerting, }: { startedAt: Date; previousStartedAt: Date | null | undefined; @@ -426,18 +428,33 @@ export const getRuleRangeTuples = ({ interval: string; maxSignals: number; ruleExecutionLogger: IRuleExecutionLogForExecutors; + alerting: PluginSetupContract; }) => { const originalFrom = dateMath.parse(from, { forceNow: startedAt }); const originalTo = dateMath.parse(to, { forceNow: startedAt }); + let wroteWarningStatus = false; + let warningStatusMessage; if (originalFrom == null || originalTo == null) { throw new Error('Failed to parse date math of rule.from or rule.to'); } + const maxAlertsAllowed = alerting.getConfig().run.alerts.max; + let maxSignalsToUse = maxSignals; + if (maxSignals > maxAlertsAllowed) { + maxSignalsToUse = maxAlertsAllowed; + warningStatusMessage = `The rule's max alerts per run setting (${maxSignals}) is greater than the Kibana alerting limit (${maxAlertsAllowed}). The rule will only write a maximum of ${maxAlertsAllowed} alerts per rule run.`; + await ruleExecutionLogger.logStatusChange({ + newStatus: RuleExecutionStatusEnum['partial failure'], + message: warningStatusMessage, + }); + wroteWarningStatus = true; + } + const tuples = [ { to: originalTo, from: originalFrom, - maxSignals, + maxSignals: maxSignalsToUse, }, ]; @@ -448,7 +465,7 @@ export const getRuleRangeTuples = ({ interval )}"` ); - return { tuples, remainingGap: moment.duration(0) }; + return { tuples, remainingGap: moment.duration(0), wroteWarningStatus, warningStatusMessage }; } const gap = getGapBetweenRuns({ @@ -464,7 +481,7 @@ export const getRuleRangeTuples = ({ const catchupTuples = getCatchupTuples({ originalTo, originalFrom, - ruleParamsMaxSignals: maxSignals, + ruleParamsMaxSignals: maxSignalsToUse, catchup, intervalDuration, }); @@ -480,6 +497,8 @@ export const getRuleRangeTuples = ({ return { tuples: tuples.reverse(), remainingGap: moment.duration(remainingGapMilliseconds), + wroteWarningStatus, + warningStatusMessage, }; }; diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index e97abbb1f04747..6fc4d52b9719ab 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -308,6 +308,7 @@ export class Plugin implements ISecuritySolutionPlugin { this.ruleMonitoringService.createRuleExecutionLogClientForExecutors, version: pluginContext.env.packageInfo.version, experimentalFeatures: config.experimentalFeatures, + alerting: plugins.alerting, }; const queryRuleAdditionalOptions: CreateQueryRuleAdditionalOptions = { diff --git a/x-pack/plugins/triggers_actions_ui/public/common/lib/kibana/kibana_react.mock.ts b/x-pack/plugins/triggers_actions_ui/public/common/lib/kibana/kibana_react.mock.ts index 490490d92cd143..85822b69d29a5e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/lib/kibana/kibana_react.mock.ts +++ b/x-pack/plugins/triggers_actions_ui/public/common/lib/kibana/kibana_react.mock.ts @@ -43,6 +43,7 @@ export const createStartServicesMock = (): TriggersAndActionsUiServices => { getNavigation: jest.fn(async (id) => id === 'alert-with-nav' ? { path: '/alert' } : undefined ), + getMaxAlertsPerRun: jest.fn(), }, history: scopedHistoryMock.create(), setBreadcrumbs: jest.fn(), diff --git a/x-pack/plugins/triggers_actions_ui/server/routes/config.test.ts b/x-pack/plugins/triggers_actions_ui/server/routes/config.test.ts index c951a5f9d34d29..010a01b46fbdb2 100644 --- a/x-pack/plugins/triggers_actions_ui/server/routes/config.test.ts +++ b/x-pack/plugins/triggers_actions_ui/server/routes/config.test.ts @@ -40,7 +40,7 @@ const ruleTypes = [ ]; describe('createConfigRoute', () => { - it('registers the route and returns config if user is authorized', async () => { + it('registers the route and returns exposed config values if user is authorized', async () => { const router = httpServiceMock.createRouter(); const logger = loggingSystemMock.create().get(); const mockRulesClient = rulesClientMock.create(); @@ -54,6 +54,7 @@ describe('createConfigRoute', () => { isUsingSecurity: true, maxScheduledPerMinute: 10000, minimumScheduleInterval: { value: '1m', enforce: false }, + run: { alerts: { max: 1000 }, actions: { max: 100000 } }, }), getRulesClientWithRequest: async () => mockRulesClient, }); @@ -88,6 +89,7 @@ describe('createConfigRoute', () => { isUsingSecurity: true, maxScheduledPerMinute: 10000, minimumScheduleInterval: { value: '1m', enforce: false }, + run: { alerts: { max: 1000 }, actions: { max: 100000 } }, }), getRulesClientWithRequest: async () => mockRulesClient, }); diff --git a/x-pack/plugins/triggers_actions_ui/server/routes/config.ts b/x-pack/plugins/triggers_actions_ui/server/routes/config.ts index c0212d7dc824fe..e6904681e71b4c 100644 --- a/x-pack/plugins/triggers_actions_ui/server/routes/config.ts +++ b/x-pack/plugins/triggers_actions_ui/server/routes/config.ts @@ -50,8 +50,16 @@ export function createConfigRoute({ // Check that user has access to at least one rule type const rulesClient = await getRulesClientWithRequest(req); const ruleTypes = Array.from(await rulesClient.listRuleTypes()); + const { minimumScheduleInterval, maxScheduledPerMinute, isUsingSecurity } = alertingConfig(); // Only returns exposed config values + if (ruleTypes.length > 0) { - return res.ok({ body: alertingConfig() }); + return res.ok({ + body: { + minimumScheduleInterval, + maxScheduledPerMinute, + isUsingSecurity, + }, + }); } else { return res.forbidden({ body: { message: `Unauthorized to access config` }, diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_bulk_actions/trial_license_complete_tier/perform_bulk_action.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_bulk_actions/trial_license_complete_tier/perform_bulk_action.ts index a3060368d83823..f728b011b6801c 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_bulk_actions/trial_license_complete_tier/perform_bulk_action.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_bulk_actions/trial_license_complete_tier/perform_bulk_action.ts @@ -134,12 +134,13 @@ export default ({ getService }: FtrProviderContext): void => { }); }); - it('should export rules with defaultbale fields when values are set', async () => { + it('should export rules with defaultable fields when values are set', async () => { const defaultableFields: BaseDefaultableFields = { related_integrations: [ { package: 'package-a', version: '^1.2.3' }, { package: 'package-b', integration: 'integration-b', version: '~1.1.1' }, ], + max_signals: 100, setup: '# some setup markdown', }; const mockRule = getCustomQueryRuleParams(defaultableFields); @@ -315,6 +316,7 @@ export default ({ getService }: FtrProviderContext): void => { const ruleId = 'ruleId'; const ruleToDuplicate = getCustomQueryRuleParams({ rule_id: ruleId, + max_signals: 100, setup: '# some setup markdown', related_integrations: [ { package: 'package-a', version: '^1.2.3' }, diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_creation/basic_license_essentials_tier/create_rules.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_creation/basic_license_essentials_tier/create_rules.ts index f0f6eae7b5da04..63b1a4dfecdc77 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_creation/basic_license_essentials_tier/create_rules.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_creation/basic_license_essentials_tier/create_rules.ts @@ -72,6 +72,7 @@ export default ({ getService }: FtrProviderContext) => { it('should create a rule with defaultable fields', async () => { const expectedRule = getCustomQueryRuleParams({ rule_id: 'rule-1', + max_signals: 200, setup: '# some setup markdown', related_integrations: [ { package: 'package-a', version: '^1.2.3' }, @@ -175,6 +176,37 @@ export default ({ getService }: FtrProviderContext) => { status_code: 409, }); }); + + describe('max_signals', () => { + beforeEach(async () => { + await deleteAllRules(supertest, log); + }); + + it('creates a rule with max_signals defaulted to 100 when not present', async () => { + const { body } = await securitySolutionApi + .createRule({ + body: getCustomQueryRuleParams(), + }) + .expect(200); + + expect(body.max_signals).toEqual(100); + }); + + it('does NOT create a rule when max_signals is less than 1', async () => { + const { body } = await securitySolutionApi + .createRule({ + body: { + ...getCustomQueryRuleParams(), + max_signals: 0, + }, + }) + .expect(400); + + expect(body.message).toBe( + '[request body]: max_signals: Number must be greater than or equal to 1' + ); + }); + }); }); }); }; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_creation/basic_license_essentials_tier/create_rules_bulk.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_creation/basic_license_essentials_tier/create_rules_bulk.ts index 052841b442f9b8..dc02f8450f4115 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_creation/basic_license_essentials_tier/create_rules_bulk.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_creation/basic_license_essentials_tier/create_rules_bulk.ts @@ -71,6 +71,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should create a rule with defaultable fields', async () => { const expectedRule = getCustomQueryRuleParams({ rule_id: 'rule-1', + max_signals: 200, setup: '# some setup markdown', related_integrations: [ { package: 'package-a', version: '^1.2.3' }, diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/basic_license_essentials_tier/export_rules.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/basic_license_essentials_tier/export_rules.ts index c217846af4612a..5986e4d40fe3a2 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/basic_license_essentials_tier/export_rules.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/basic_license_essentials_tier/export_rules.ts @@ -51,6 +51,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should export defaultable fields when values are set', async () => { const defaultableFields: BaseDefaultableFields = { + max_signals: 200, related_integrations: [ { package: 'package-a', version: '^1.2.3' }, { package: 'package-b', integration: 'integration-b', version: '~1.1.1' }, diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/basic_license_essentials_tier/import_rules.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/basic_license_essentials_tier/import_rules.ts index c0bf497fbd0ca1..71f40086a29f66 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/basic_license_essentials_tier/import_rules.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/basic_license_essentials_tier/import_rules.ts @@ -119,6 +119,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should be able to import rules with defaultable fields', async () => { const defaultableFields: BaseDefaultableFields = { + max_signals: 100, setup: '# some setup markdown', related_integrations: [ { package: 'package-a', version: '^1.2.3' }, diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/basic_license_essentials_tier/patch_rules.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/basic_license_essentials_tier/patch_rules.ts index 2dc21264ef66cb..bdbbc271c26e55 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/basic_license_essentials_tier/patch_rules.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/basic_license_essentials_tier/patch_rules.ts @@ -63,6 +63,7 @@ export default ({ getService }: FtrProviderContext) => { it('should patch defaultable fields', async () => { const expectedRule = getCustomQueryRuleParams({ rule_id: 'rule-1', + max_signals: 200, setup: '# some setup markdown', related_integrations: [ { package: 'package-a', version: '^1.2.3' }, @@ -78,6 +79,7 @@ export default ({ getService }: FtrProviderContext) => { .patchRule({ body: { rule_id: 'rule-1', + max_signals: expectedRule.max_signals, setup: expectedRule.setup, related_integrations: expectedRule.related_integrations, }, @@ -229,6 +231,31 @@ export default ({ getService }: FtrProviderContext) => { message: 'rule_id: "fake_id" not found', }); }); + + describe('max signals', () => { + afterEach(async () => { + await deleteAllRules(supertest, log); + }); + + it('does NOT patch a rule when max_signals is less than 1', async () => { + await securitySolutionApi.createRule({ + body: getCustomQueryRuleParams({ rule_id: 'rule-1', max_signals: 100 }), + }); + + const { body } = await securitySolutionApi + .patchRule({ + body: { + rule_id: 'rule-1', + max_signals: 0, + }, + }) + .expect(400); + + expect(body.message).toEqual( + '[request body]: max_signals: Number must be greater than or equal to 1' + ); + }); + }); }); }); }; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/basic_license_essentials_tier/patch_rules_bulk.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/basic_license_essentials_tier/patch_rules_bulk.ts index 020d9c0e62b3f5..539c39061aa5f8 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/basic_license_essentials_tier/patch_rules_bulk.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/basic_license_essentials_tier/patch_rules_bulk.ts @@ -62,6 +62,7 @@ export default ({ getService }: FtrProviderContext) => { it('should patch defaultable fields', async () => { const expectedRule = getCustomQueryRuleParams({ rule_id: 'rule-1', + max_signals: 200, setup: '# some setup markdown', related_integrations: [ { package: 'package-a', version: '^1.2.3' }, @@ -78,6 +79,7 @@ export default ({ getService }: FtrProviderContext) => { body: [ { rule_id: 'rule-1', + max_signals: expectedRule.max_signals, setup: expectedRule.setup, related_integrations: expectedRule.related_integrations, }, diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_update/basic_license_essentials_tier/update_rules.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_update/basic_license_essentials_tier/update_rules.ts index abd486b1e080ec..ccf598a00da2e6 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_update/basic_license_essentials_tier/update_rules.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_update/basic_license_essentials_tier/update_rules.ts @@ -68,6 +68,7 @@ export default ({ getService }: FtrProviderContext) => { it('should update a rule with defaultable fields', async () => { const expectedRule = getCustomQueryRuleParams({ rule_id: 'rule-1', + max_signals: 200, setup: '# some setup markdown', related_integrations: [ { package: 'package-a', version: '^1.2.3' }, @@ -225,6 +226,53 @@ export default ({ getService }: FtrProviderContext) => { message: 'rule_id: "fake_id" not found', }); }); + + describe('max signals', () => { + afterEach(async () => { + await deleteAllRules(supertest, log); + }); + + it('should reset max_signals field to default value on update when not present', async () => { + const expectedRule = getCustomQueryRuleParams({ + rule_id: 'rule-1', + max_signals: 100, + }); + + await securitySolutionApi.createRule({ + body: getCustomQueryRuleParams({ rule_id: 'rule-1', max_signals: 200 }), + }); + + const { body: updatedRuleResponse } = await securitySolutionApi + .updateRule({ + body: getCustomQueryRuleParams({ + rule_id: 'rule-1', + max_signals: undefined, + }), + }) + .expect(200); + + expect(updatedRuleResponse).toMatchObject(expectedRule); + }); + + it('does NOT update a rule when max_signals is less than 1', async () => { + await securitySolutionApi.createRule({ + body: getCustomQueryRuleParams({ rule_id: 'rule-1', max_signals: 100 }), + }); + + const { body } = await securitySolutionApi + .updateRule({ + body: getCustomQueryRuleParams({ + rule_id: 'rule-1', + max_signals: 0, + }), + }) + .expect(400); + + expect(body.message).toEqual( + '[request body]: max_signals: Number must be greater than or equal to 1' + ); + }); + }); }); }); }; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_update/basic_license_essentials_tier/update_rules_bulk.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_update/basic_license_essentials_tier/update_rules_bulk.ts index b73b8c0be95ccc..fc7c7229ef1073 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_update/basic_license_essentials_tier/update_rules_bulk.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_update/basic_license_essentials_tier/update_rules_bulk.ts @@ -67,6 +67,7 @@ export default ({ getService }: FtrProviderContext) => { it('should update a rule with defaultable fields', async () => { const expectedRule = getCustomQueryRuleParams({ rule_id: 'rule-1', + max_signals: 200, setup: '# some setup markdown', related_integrations: [ { package: 'package-a', version: '^1.2.3' }, diff --git a/x-pack/test/security_solution_cypress/cypress/data/detection_engine.ts b/x-pack/test/security_solution_cypress/cypress/data/detection_engine.ts index 60ebea7632b50b..ed627bf493da09 100644 --- a/x-pack/test/security_solution_cypress/cypress/data/detection_engine.ts +++ b/x-pack/test/security_solution_cypress/cypress/data/detection_engine.ts @@ -25,6 +25,7 @@ import type { RuleName, RuleReferenceArray, RuleTagArray, + MaxSignals, SetupGuide, } from '@kbn/security-solution-plugin/common/api/detection_engine'; @@ -45,6 +46,7 @@ interface RuleFields { threat: Threat; threatSubtechnique: ThreatSubtechnique; threatTechnique: ThreatTechnique; + maxSignals: MaxSignals; setup: SetupGuide; } @@ -93,4 +95,5 @@ export const ruleFields: RuleFields = { name: 'OS Credential Dumping', reference: 'https://attack.mitre.org/techniques/T1003', }, + maxSignals: 100, }; diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/common_flows.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/common_flows.cy.ts index c718930cdf71eb..bdcbbcf987eb6c 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/common_flows.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/common_flows.cy.ts @@ -16,6 +16,7 @@ import { SCHEDULE_CONTINUE_BUTTON, } from '../../../../screens/create_new_rule'; import { + MAX_SIGNALS_DETAILS, DESCRIPTION_SETUP_GUIDE_BUTTON, DESCRIPTION_SETUP_GUIDE_CONTENT, RULE_NAME_HEADER, @@ -29,6 +30,7 @@ import { fillDescription, fillFalsePositiveExamples, fillFrom, + fillMaxSignals, fillNote, fillReferenceUrls, fillRelatedIntegrations, @@ -81,6 +83,7 @@ describe('Common rule creation flows', { tags: ['@ess', '@serverless'] }, () => fillThreatTechnique(); fillThreatSubtechnique(); fillCustomInvestigationFields(); + fillMaxSignals(); fillNote(); fillSetup(); cy.get(ABOUT_CONTINUE_BTN).click(); @@ -103,6 +106,7 @@ describe('Common rule creation flows', { tags: ['@ess', '@serverless'] }, () => // UI redirects to rule creation page of a created rule cy.get(RULE_NAME_HEADER).should('contain', ruleFields.ruleName); + cy.get(MAX_SIGNALS_DETAILS).should('contain', ruleFields.maxSignals); cy.get(DESCRIPTION_SETUP_GUIDE_BUTTON).click(); cy.get(DESCRIPTION_SETUP_GUIDE_CONTENT).should('contain', 'test setup markdown'); // Markdown formatting should be removed diff --git a/x-pack/test/security_solution_cypress/cypress/screens/create_new_rule.ts b/x-pack/test/security_solution_cypress/cypress/screens/create_new_rule.ts index d9b70b1ddd4e4c..88b8c2192d1bf7 100644 --- a/x-pack/test/security_solution_cypress/cypress/screens/create_new_rule.ts +++ b/x-pack/test/security_solution_cypress/cypress/screens/create_new_rule.ts @@ -132,6 +132,8 @@ export const INDICATOR_MATCH_TYPE = '[data-test-subj="threatMatchRuleType"]'; export const INPUT = '[data-test-subj="input"]'; +export const MAX_SIGNALS_INPUT = '[data-test-subj="detectionEngineStepAboutRuleMaxSignals"]'; + export const INVESTIGATION_NOTES_TEXTAREA = '[data-test-subj="detectionEngineStepAboutRuleNote"] textarea'; diff --git a/x-pack/test/security_solution_cypress/cypress/screens/rule_details.ts b/x-pack/test/security_solution_cypress/cypress/screens/rule_details.ts index d67a07faf60797..53237cac7ec8bd 100644 --- a/x-pack/test/security_solution_cypress/cypress/screens/rule_details.ts +++ b/x-pack/test/security_solution_cypress/cypress/screens/rule_details.ts @@ -156,6 +156,8 @@ export const ALERT_SUPPRESSION_INSUFFICIENT_LICENSING_ICON = export const HIGHLIGHTED_ROWS_IN_TABLE = '[data-test-subj="euiDataGridBody"] .alertsTableHighlightedRow'; +export const MAX_SIGNALS_DETAILS = '[data-test-subj="maxSignalsPropertyValue"]'; + export const DESCRIPTION_SETUP_GUIDE_BUTTON = '[data-test-subj="stepAboutDetailsToggle-setup"]'; export const DESCRIPTION_SETUP_GUIDE_CONTENT = '[data-test-subj="stepAboutDetailsSetupContent"]'; diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/create_new_rule.ts b/x-pack/test/security_solution_cypress/cypress/tasks/create_new_rule.ts index aa97035eddc478..7ee1811760480c 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/create_new_rule.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/create_new_rule.ts @@ -125,6 +125,7 @@ import { ALERTS_INDEX_BUTTON, INVESTIGATIONS_INPUT, QUERY_BAR_ADD_FILTER, + MAX_SIGNALS_INPUT, SETUP_GUIDE_TEXTAREA, RELATED_INTEGRATION_COMBO_BOX_INPUT, } from '../screens/create_new_rule'; @@ -198,6 +199,13 @@ export const expandAdvancedSettings = () => { cy.get(ADVANCED_SETTINGS_BTN).click({ force: true }); }; +export const fillMaxSignals = (maxSignals: number = ruleFields.maxSignals) => { + cy.get(MAX_SIGNALS_INPUT).clear({ force: true }); + cy.get(MAX_SIGNALS_INPUT).type(maxSignals.toString()); + + return maxSignals; +}; + export const fillNote = (note: string = ruleFields.investigationGuide) => { cy.get(INVESTIGATION_NOTES_TEXTAREA).clear({ force: true }); cy.get(INVESTIGATION_NOTES_TEXTAREA).type(note, { force: true });