mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Security Solution] Allow users to edit max_signals field for custom rules (#179680)
**Resolves: https://github.com/elastic/kibana/issues/173593** **Fixes: https://github.com/elastic/kibana/issues/164234** ## Summary Adds a number component in the create and edit rule forms so that users are able to customize the `max_signals` value for custom rules from the UI. Also adds validations to the rule API's for invalid values being passed in. This PR also exposes the `xpack.alerting.rules.run.alerts.max` config setting from the alerting framework to the frontend and backend so that we can validate against it as it supersedes our own `max_signals` value. [Flaky test run (internal) ](https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/5601) ### Screenshots **Form component** <p align="center"> <img width="887" alt="Screenshot 2024-04-08 at 11 02 12 PM" src="58cd2f6d
-61b6-4343-8025-ff867c050dd7"> </p> **Details Page** <p align="center"> <img width="595" alt="Screenshot 2024-04-08 at 11 04 04 PM" src="d2c61593
-3d35-408e-b047-b4d1f68898f8"> </p> **Error state** <p align="center"> <img width="857" alt="Screenshot 2024-04-08 at 11 01 55 PM" src="86e64280
-7b81-46f2-b223-fde8c20066c8"> </p> **Warning state** <p align="center"> <img width="601" alt="Screenshot 2024-04-16 at 3 20 00 PM" src="eab07d62
-3d3e-4c85-8468-36c3e56c5a99"> </p> ### Checklist Delete any items that are not applicable to this PR. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [x] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --------- Co-authored-by: Juan Pablo Djeredjian <jpdjeredjian@gmail.com>
This commit is contained in:
parent
8575fc52c1
commit
f841763d50
54 changed files with 636 additions and 81 deletions
|
@ -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)',
|
||||
|
|
|
@ -17,6 +17,7 @@ const createSetupContract = (): Setup => ({
|
|||
|
||||
const createStartContract = (): Start => ({
|
||||
getNavigation: jest.fn(),
|
||||
getMaxAlertsPerRun: jest.fn(),
|
||||
});
|
||||
|
||||
export const alertingPluginMock = {
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -57,6 +57,7 @@ export interface PluginSetupContract {
|
|||
}
|
||||
export interface PluginStartContract {
|
||||
getNavigation: (ruleId: Rule['id']) => Promise<string | undefined>;
|
||||
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<PluginSetupContract, PluginStartContract, AlertingPluginSetup, AlertingPluginStart>
|
||||
{
|
||||
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<AlertingUIConfig>();
|
||||
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;
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -80,7 +80,7 @@ export type AlertingConfig = TypeOf<typeof configSchema>;
|
|||
export type RulesConfig = TypeOf<typeof rulesSchema>;
|
||||
export type AlertingRulesConfig = Pick<
|
||||
AlertingConfig['rules'],
|
||||
'minimumScheduleInterval' | 'maxScheduledPerMinute'
|
||||
'minimumScheduleInterval' | 'maxScheduledPerMinute' | 'run'
|
||||
> & {
|
||||
isUsingSecurity: boolean;
|
||||
};
|
||||
|
|
|
@ -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<RulesClientClass>;
|
||||
|
||||
|
@ -79,8 +78,11 @@ export const plugin = async (initContext: PluginInitializerContext) => {
|
|||
return new AlertingPlugin(initContext);
|
||||
};
|
||||
|
||||
export const config: PluginConfigDescriptor<AlertsConfigType> = {
|
||||
export const config: PluginConfigDescriptor<AlertingConfig> = {
|
||||
schema: configSchema,
|
||||
exposeToBrowser: {
|
||||
rules: { run: { alerts: { max: true } } },
|
||||
},
|
||||
deprecations: ({ renameFromRoot, deprecate }) => [
|
||||
renameFromRoot('xpack.alerts.healthCheck', 'xpack.alerting.healthCheck', { level: 'warning' }),
|
||||
renameFromRoot(
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
},
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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<string, unknown> = {
|
||||
[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;
|
||||
};
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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<number | ''>;
|
||||
idAria: string;
|
||||
isDisabled: boolean;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
const MAX_SIGNALS_FIELD_WIDTH = 200;
|
||||
|
||||
export const MaxSignals: React.FC<MaxSignalsFieldProps> = ({
|
||||
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(
|
||||
<EuiTextColor color="warning">{i18n.LESS_THAN_WARNING(maxAlertsPerRun)}</EuiTextColor>
|
||||
);
|
||||
}
|
||||
textToRender.push(i18n.MAX_SIGNALS_HELP_TEXT(DEFAULT_MAX_SIGNALS));
|
||||
return textToRender;
|
||||
}, [hasWarning, maxAlertsPerRun]);
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
css={css`
|
||||
.euiFormControlLayout {
|
||||
width: ${MAX_SIGNALS_FIELD_WIDTH}px;
|
||||
}
|
||||
`}
|
||||
describedByIds={idAria ? [idAria] : undefined}
|
||||
fullWidth
|
||||
helpText={helpText}
|
||||
label={field.label}
|
||||
labelAppend={field.labelAppend}
|
||||
isInvalid={isInvalid}
|
||||
error={error}
|
||||
>
|
||||
<EuiFieldNumber
|
||||
isInvalid={isInvalid}
|
||||
value={value as EuiFieldNumberProps['value']}
|
||||
onChange={handleMaxSignalsChange}
|
||||
isLoading={field.isValidating}
|
||||
placeholder={placeholder}
|
||||
data-test-subj={dataTestSubj}
|
||||
disabled={isDisabled}
|
||||
append={hasWarning ? <EuiIcon size="s" type="warning" color="warning" /> : undefined}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
};
|
||||
|
||||
MaxSignals.displayName = 'MaxSignals';
|
|
@ -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}.',
|
||||
}
|
||||
);
|
|
@ -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: '',
|
||||
};
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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<StepAboutRuleProps> = ({
|
|||
/>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer size="l" />
|
||||
<EuiFormRow fullWidth>
|
||||
<UseField
|
||||
path="maxSignals"
|
||||
component={MaxSignals}
|
||||
componentProps={{
|
||||
idAria: 'detectionEngineStepAboutRuleMaxSignals',
|
||||
dataTestSubj: 'detectionEngineStepAboutRuleMaxSignals',
|
||||
isDisabled: isLoading,
|
||||
placeholder: DEFAULT_MAX_SIGNALS,
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
{isThreatMatchRuleValue && (
|
||||
<>
|
||||
<CommonUseField
|
||||
|
|
|
@ -100,6 +100,16 @@ export const schema: FormSchema<AboutStepRule> = {
|
|||
),
|
||||
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(
|
||||
|
|
|
@ -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',
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -141,7 +141,6 @@ const CreateRulePageComponent: React.FC = () => {
|
|||
|
||||
const [indicesConfig] = useUiSetting$<string[]>(DEFAULT_INDEX_KEY);
|
||||
const [threatIndicesConfig] = useUiSetting$<string[]>(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}`, {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -240,6 +240,16 @@ const TimestampOverride = ({ timestampOverride }: TimestampOverrideProps) => (
|
|||
</EuiText>
|
||||
);
|
||||
|
||||
interface MaxSignalsProps {
|
||||
maxSignals: number;
|
||||
}
|
||||
|
||||
const MaxSignals = ({ maxSignals }: MaxSignalsProps) => (
|
||||
<EuiText size="s" data-test-subj="maxSignalsPropertyValue">
|
||||
{maxSignals}
|
||||
</EuiText>
|
||||
);
|
||||
|
||||
interface TagsProps {
|
||||
tags: string[];
|
||||
}
|
||||
|
@ -414,6 +424,13 @@ const prepareAboutSectionListItems = (
|
|||
});
|
||||
}
|
||||
|
||||
if (rule.max_signals) {
|
||||
aboutSectionListItems.push({
|
||||
title: <span data-test-subj="maxSignalsPropertyTitle">{i18n.MAX_SIGNALS_FIELD_LABEL}</span>,
|
||||
description: <MaxSignals maxSignals={rule.max_signals} />,
|
||||
});
|
||||
}
|
||||
|
||||
if (rule.tags && rule.tags.length > 0) {
|
||||
aboutSectionListItems.push({
|
||||
title: <span data-test-subj="tagsPropertyTitle">{i18n.TAGS_FIELD_LABEL}</span>,
|
||||
|
|
|
@ -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',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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 => ({
|
||||
|
|
|
@ -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' };
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -46,6 +46,7 @@ describe('Custom Query Alerts', () => {
|
|||
ruleExecutionLoggerFactory: () => Promise.resolve(ruleExecutionLogMock.forExecutors.create()),
|
||||
version: '8.3',
|
||||
publicBaseUrl,
|
||||
alerting,
|
||||
});
|
||||
const eventsTelemetry = createMockTelemetryEventsSender(true);
|
||||
|
||||
|
|
|
@ -137,6 +137,7 @@ export interface CreateSecurityRuleTypeWrapperProps {
|
|||
version: string;
|
||||
isPreview?: boolean;
|
||||
experimentalFeatures?: ExperimentalFeatures;
|
||||
alerting: SetupPlugins['alerting'];
|
||||
}
|
||||
|
||||
export type CreateSecurityRuleTypeWrapper = (
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
expect(wroteWarningStatus).toEqual(false);
|
||||
expect(warningStatusMessage).toEqual(undefined);
|
||||
});
|
||||
|
||||
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,
|
||||
});
|
||||
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);
|
||||
expect(wroteWarningStatus).toEqual(false);
|
||||
expect(warningStatusMessage).toEqual(undefined);
|
||||
});
|
||||
|
||||
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,
|
||||
});
|
||||
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);
|
||||
expect(wroteWarningStatus).toEqual(false);
|
||||
expect(warningStatusMessage).toEqual(undefined);
|
||||
});
|
||||
|
||||
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,
|
||||
});
|
||||
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)', () => {
|
||||
const { tuples, remainingGap } = getRuleRangeTuples({
|
||||
previousStartedAt: moment().subtract(-15, 's').toDate(),
|
||||
startedAt: moment().subtract(-15, 's').toDate(),
|
||||
interval: '10s',
|
||||
from: 'now-13s',
|
||||
to: 'now',
|
||||
maxSignals: 20,
|
||||
ruleExecutionLogger,
|
||||
});
|
||||
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 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(someTuple.maxSignals).toEqual(20);
|
||||
expect(tuples.length).toEqual(1);
|
||||
expect(wroteWarningStatus).toEqual(false);
|
||||
expect(warningStatusMessage).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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` },
|
||||
|
|
|
@ -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' },
|
||||
|
|
|
@ -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'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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' },
|
||||
|
|
|
@ -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' },
|
||||
|
|
|
@ -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' },
|
||||
|
|
|
@ -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'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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' },
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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"]';
|
||||
|
|
|
@ -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 });
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue